こんにちは、サービスプラットフォームチーム アルバイトの
id:walnuts1018 です。
この記事は、はてなの SRE が毎月交代で書いている SRE 連載の 8 月号です。7 月の記事は
id:taxintt さんの AWS Database Migration Service(AWS DMS)を利用した MySQL から PostgreSQL へのデータ移行 でした。
今回は、EKSにおけるサービスごとのコストを計測するためにOpenCostを導入した事例について紹介します。
EKSクラスタにおけるコスト計測
サービスプラットフォームチームでは、1つのEKSクラスタ上で複数のサービスを運用しています。 そのような構成を取ることでリソースの効率的な利用やコスト削減が可能になりますが、各サービスごとのコストを把握することが難しいという課題がありました。 EKSクラスタを構成するEC2インスタンスやネットワークのコスト全体を知ることはできますが、サービス単体のコストを手動で計算することは容易でないからです。
そこで、OpenCostを導入し、EKSクラスタ上で動作する各サービスのコストを可視化することにしました。
OpenCostとは
OpenCostは、クラウド基盤のコストをKubernetesのコンテキストに紐付けて計算するためのツールです。CNCFのIncubating プロジェクトであり、特定のクラウドプロバイダに依存しない設計となっています。
OpenCostは、以下の流れでコストを計算します。
- クラウドプロバイダからAPI経由で料金体系を取得
- 各Nodeのリソース(CPU、メモリ、GPUなど)の単価を計算
- 各コンテナに割り当てられたリソース量をメトリクスとして出力
- UIで表示する際に、各コンテナに割り当てられたリソース量とNodeの単位リソース量あたりのコストをかけ合わせて、各コンテナのコストを計算
プロバイダの料金体系を取得
OpenCostは、AWS、GCP、Azureなどの主要なクラウドプロバイダのAPIを利用して、各プロバイダの料金体系を取得します。
例えばAWSの場合、https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/ap-northeast-1/index.jsonから料金情報を取得します。
Nodeの料金の計算
コンテナのリソース使用量は主にCPUとメモリの2つで構成されるため、Nodeの料金もCPUとメモリそれぞれのリソースごとの単価が必要となります。 そこでOpenCostは内部にCPUとメモリの単価を持っており、その比率を元にインスタンス料金をCPUとメモリの単価に分解します。
コンテナのリソース割り当て量のメトリクス
コンテナに割り当てられているリソース量は「リソース要求量+要求量を超えて使用している量」だと考えられます。これは単純に、リソース要求量と実際に使用している量のどちらか大きい方を取ることで計算できます。
実際、メモリの割り当て量を計算する実装は、
queryRAMUsageAvg := fmt.Sprintf(`avg(avg_over_time(container_memory_working_set_bytes{container!="", container_name!="POD", container!="POD", %s}[%s])) by (container_name, container, pod_name, pod, namespace, node, instance, %s)`, cfg.ClusterFilter, durStr, cfg.ClusterLabel) queryRAMRequests := fmt.Sprintf(`avg(avg_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", container!="", container!="POD", node!="", %s}[%s])) by (container, pod, namespace, node, %s)`, cfg.ClusterFilter, durStr, cfg.ClusterLabel)
といったクエリで得られた結果を、
var result []*util.Vector if req != nil && used != nil { result = []*util.Vector{ { Value: math.Max(req.Value, used.Value), Timestamp: math.Max(req.Timestamp, used.Timestamp), }, } } else if req != nil { result = []*util.Vector{ { Value: req.Value, Timestamp: req.Timestamp, }, } } else if used != nil { result = []*util.Vector{ { Value: used.Value, Timestamp: used.Timestamp, }, } }
のように、有効な値のどちらか最大値を計算する形になっています*1 *2 *3。
また、OpenCostはkubeletから取得したメトリクスのうち必要なものをそのままエクスポートする実装になっているので、container_memory_working_set_bytesやkube_pod_container_resource_requestsといったメトリクスを我々がPrometheusに送信する必要はありません。
コンテナのコスト計算
実際にコストを計算するのは、UI側の実装になります。 OpenCostのAPIやPrometheusへのクエリなどを通じて、各コンテナのリソース使用量とNodeの単位リソース量あたりのコストを取得し、それらを掛け合わせてコストを計算します。
今回用いたクエリは、後ほどクエリの設計とダッシュボードの作成で紹介します。
システム構成
サービスプラットフォームチームでは、OpenTelemetry CollectorとAmazon Managed Service for Prometheus/Grafanaを利用したメトリクスの収集と可視化がすでに行われていました。 そこで、OpenCostのデータについても同じ仕組みを利用することにしました。
最終的には図のような構成となりました。
インストール
OpenCostはHelm Chartとして提供されているため、Helmを使って簡単にインストールできます。 今回は、Amazon Managed Service for Prometheusをデータソースとして利用するため、AWS SigV4 ProxyやIRSAの設定を追加します。
helm repo add opencost-charts https://opencost.github.io/opencost-helm-chart
cat <<EOF > values.yaml
opencost:
prometheus:
internal:
enabled: false
amp:
enabled: true
workspaceId: <workspace-id>
sigV4Proxy:
region: ap-northeast-1
host: aps-workspaces.ap-northeast-1.amazonaws.com
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: <iam-role-arn>
EOF
helm install opencost opencost-charts/opencost \
--namespace opencost --create-namespace \
--values values.yaml
また、メトリクスの収集にはOpenTelemetry CollectorのPrometheus Receiverを利用するため、以下のような設定を追加します。
config: receivers: prometheus: config: scrape_configs: - job_name: 'opencost' scrape_interval: 1m scrape_timeout: 10s honor_labels: true metrics_path: /metrics scheme: http dns_sd_configs: - names: - opencost.opencost.svc.cluster.local type: 'A' port: 9003 exporters: prometheusremotewrite: endpoint: "https://aps-workspaces.ap-northeast-1.amazonaws.com/workspaces/<workspace-id>/api/v1/remote_write" auth: authenticator: "sigv4auth" resource_to_telemetry_conversion: enabled: true service: pipelines: metrics: receivers: [ prometheus ] processors: <processors> exporters: [ prometheusremotewrite ]
インストールが完了したら、kubecost_cluster_infoクエリを実行して、OpenCostが正しく動作していることを確認します。
クエリの設計とダッシュボードの作成
ダッシュボードの構築にはGrafanaを利用しました。 OpenCostが提供しているGrafanaダッシュボードを参考にしつつ、我々に必要な情報を加えていって、以下のようなダッシュボードとなりました。
このダッシュボードで使われているクエリをいくつか紹介します。
①Namespaceごとの1時間あたりのコスト
円グラフのパネルで使われている、1時間あたりのコストをNamespaceごとに集計するクエリです。
sum by (namespace) (
sum by (namespace, node) (container_memory_allocation_bytes{})
* on (node) group_left () (node_ram_hourly_cost{} / 1024^3)
+
sum by (namespace, node) (container_cpu_allocation{})
* on (node) group_left () (node_cpu_hourly_cost{})
)
CPUとメモリのコストをNodeごとに計算し、その結果をNamespaceごとに合計しています。
node_ram_hourly_costは1GB単位でメモリコストを表すメトリクスであるため、バイト単位に変換するのがポイントです*4。
空きリソースのコストも表示したい場合は、以下のようなクエリを追加します。
sum(
(
kube_node_status_allocatable_memory_bytes
- on (node) sum by (node) (container_memory_allocation_bytes)
) * on (node) group_left () (node_ram_hourly_cost / 1024^3)
+
(
kube_node_status_allocatable_cpu_cores
- on (node) sum by (node) (container_cpu_allocation)
) * on (node) group_left () (node_cpu_hourly_cost)
)
これは、各Nodeの割り当て可能なリソース量から、実際にコンテナに割り当てられているリソース量の合計を引き、その結果にNodeごとのリソース単価を掛け合わせることで空きリソースのコストを計算しています。
②Namespaceごとの1週間/1ヵ月の合計コスト
モザイクでわかりづらいですが、棒グラフとして表示されているNamespaceごとの1週間/1ヵ月の合計コストを求めるためのクエリです。
sum_over_time(
(
sum by (namespace) (
sum by (namespace, node) (container_memory_allocation_bytes{})
* on (node) group_left () (node_ram_hourly_cost{} / 1024 ^ 3)
+
sum by (namespace, node) (container_cpu_allocation{})
* on (node) group_left () (node_cpu_hourly_cost{})
)
)[$__range:1h]
)
Namespaceごとの1時間あたりのコストのクエリを、サブクエリを使って1時間間隔のRange Vectorに変換し、sum_over_time関数を使って指定した期間での合計値を計算しています。
これらのクエリをベースにして、集計方法や表示形式などを変えながら必要なパネルをダッシュボードに追加していきました。
まとめ/ふりかえり
OpenCostを導入することで、今まで把握が困難だったサービスごとのコストを可視化することができました。 この可視化により、ステージング環境や利用頻度の低いコンポーネントのコストが想定より高いことが判明し、リソースの見直しを行うことで、具体的なコスト削減につなげることができました。 また、コストデータを他のメトリクスと統合して分析することで、運用効率の向上やリソース配分の最適化にも役立てることができました。
Kubernetes上のアプリケーションのコスト計測に課題を感じている方は、ぜひOpenCostを導入してみてください。
*1:https://github.com/opencost/opencost/blob/e97b8290246d5e2a202cfa5fe93ea69bf64cdf46/modules/prometheus-source/pkg/prom/metricsquerier.go#L545-L559
*2:https://github.com/opencost/opencost/blob/e97b8290246d5e2a202cfa5fe93ea69bf64cdf46/modules/prometheus-source/pkg/prom/metricsquerier.go#L529-L543
*3:https://github.com/opencost/opencost/blob/e97b8290246d5e2a202cfa5fe93ea69bf64cdf46/pkg/costmodel/costmodel.go#L709-L758
*4:https://opencost.io/docs/integrations/prometheus/#:~:text=on%20this%20node-,node_ram_hourly_cost,-Hourly%20cost%20per