概要
こんにちは、IT 本部 IT 基盤部第三グループの前田洋太です。IT 基盤部では、組織横断的に複数のサービスのインフラ運用を行っており、サービスの安定的な稼働のための運用やコスト削減に取り組んでいます。
サービスの安定的な稼働のためには、適切に状態を監視し、必要に応じてアラートを出すことが重要です。そのための手段は様々ありますが、今回は近年多く使われているであろう Kubernetes + Prometheus の構成上に AWS CloudWatch のデータを取り込んで統一的な監視を行えるようにした事例を紹介したいと思います。
本記事では、CloudWatch のメトリクスを Kubernetes 上の Prometheus に取り込む方法について、 Yet Another CloudWatch Exporter (以下 YACE)を用いた具体的な設定方法や、導入時に留意した点について解説します。特に YACE の細かい設定に関しては、私が調べた範囲では情報が少ないと感じたため、同様の課題をお持ちの方の参考になれば幸いです。
本記事の内容
- CloudWatch のデータを Kubernetes 上の Prometheus に取り込む必要性が生じた背景
- YACE を利用した CloudWatch メトリクス (本稿では主に ALB 関連) の Prometheus への連携手順
- IRSA (IAM Roles for Service Accounts) を用いた IAM 権限設定
- Terraform による YACE Helm Chart のデプロイ構成例
- YACE の
config
設定におけるパラメータ (delay
,length
,nilToZero
など) の考え方 - 導入時に考慮すべき点 (IAM 権限、データ遅延や欠損への対応)
※本記事で紹介する設定は、特定の環境における一例です。ご利用の際は、ご自身の環境に合わせて十分な検証と調整をお願いします。
背景・動機
私たちのグループでは、新たに運用に参画したあるサービスの Service Level Indicator (SLI) として Application Load Balancer (ALB) のエラー率を計測することにしました。
このサービスは AWS の EKS を利用して構築された Kubernetes クラスタ上にデプロイされており、監視基盤としては Prometheus と Alertmanager を利用しています。ALB のメトリクスは CloudWatch で提供されており、アラートも CloudWatch Alarms を使う方法がありますが、アラート設定を含めたモニタリングは Prometheus/Alertmanager で一元管理したいという要求がありました。これを実現するため、CloudWatch メトリクスを収集する Exporter の導入を検討し、今回は Yet Another CloudWatch Exporter (YACE) を採用しました。
実現方法の概要
Prometheus は、Exporter と呼ばれるプロセスを通じて様々なソースからメトリクスを収集します。例えば、 blackbox exporter はエンドポイントの死活監視、 node exporter はノード(マシン)のリソース監視といった役割を担います。
CloudWatch のメトリクスを取得するための Exporter も存在します。代表的なものとしては、Prometheus 公式の CloudWatch Exporter と、コミュニティ製の YACE があります。(どちらも Prometheus community の配下にあります)
今回は、いくつかの理由から YACE を採用しました:
- 後継版であり、色々改良されているらしい
- コスト的な観点
- CloudWatch API の呼び出し数を削減可能 cf. https://sysdig.com/blog/improving-prometheus-cloudwatch-exporter/
- auto discovery(tag などに基づいて、リソースが増えたり入れ替わったりしても自動で監視対象を見つけてくれる)
- ざっと調べた感じYACEのほうがネット上に情報が多そう
Prometheus と同様、YACE も Helm Chart が提供されているため、これを利用して EKS クラスタ上にデプロイします。Helm Chart では values.yaml というファイル、あるいは Helm コマンドの引数を通じて設定をカスタマイズできます。
YACEの設定の詳細
IAM 権限周り
必要な IAM ポリシー
YACE の Pod が AWS CloudWatch API にアクセスするためには、適切な IAM 権限が必要です。YACE が CloudWatch メトリクスを取得するために最低限必要な権限は以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "YACEPolicy",
"Effect": "Allow",
"Action": [
"tag:GetResources",
"cloudwatch:ListTagsForResource",
"cloudwatch:GetMetricData",
"cloudwatch:ListMetrics",
"iam:ListAccountAliases"
],
"Resource": "*"
}
]
}
ポイント: iam:ListAccountAliases
権限について
YACE のドキュメントには明記されていませんが、我々の環境では iam:ListAccountAliases
権限がないと、以下のようなエラーログが出力され、メトリクス取得に失敗しました。
{
"caller": "scrape.go:45",
"err": "AccessDenied: User: arn:aws:sts::ACCOUNT_ID:assumed-role/ROLE_NAME/INSTANCE_ID is not authorized to perform: iam:ListAccountAliases on resource: * because no identity-based policy allows the iam:ListAccountAliases action..."
}
これは、AWS アカウントにアカウントエイリアス(例: my-service-dev
のような、数字 ID の代わりの分かりやすい名前)が設定されている場合に発生するようです (
参考 Issue
)。この権限がないと、YACE が内部でアカウントエイリアスを取得しようとして失敗するため、追加が必要でした。
権限を付与する方法: IRSA (IAM Roles for Service Accounts) の採用
EKS 上の Pod に AWS リソースへのアクセス権限を与える方法としては、主に以下の2つが考えられます。
- EKS ノードグループの IAM Role にポリシーをアタッチする:
- 設定は比較的簡単です。
- しかし、そのノード上で動作する全ての Pod に同じ権限が付与されてしまい、権限過剰になるリスクがあります。
- また、インフラ (EKS) と監視 (Prometheus/YACE) の設定が異なるリポジトリや管理単位に分散することになりかねません。
- IRSA (IAM Roles for Service Accounts) を利用する:
- Kubernetes の ServiceAccount に IAM Role を紐付ける仕組みです。Pod ごとに必要最小限の権限を付与できます。
- 設定には Kubernetes の ServiceAccount や OIDC に関する知識が必要になりますが、権限の最小化が実現でき、関連設定を監視関連リソースの Terraform コード内で完結させやすくなります。
今回は、権限最小化の原則 と 設定管理の集約 の観点から、IRSA を利用することにしました。
IRSA を利用するには、EKS クラスタに IAM OIDC プロバイダーが設定されている必要があります。多くの環境では EKS クラスタ作成時に自動で設定されているか、後から追加設定が可能です。今回は既に設定済みでしたので、そのまま利用できました。(確認方法については、 AWS ドキュメント を参照してください。)
IRSA 用の IAM Role と、それにアタッチする IAM Policy は Terraform で管理しています。
# IAM role for cloudwatch-exporter ServiceAccount
resource "aws_iam_role" "yet_another_cloudwatch_exporter_role" {
name = "yace-role"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Federated" : "${var.oidc_provider_arn}"
},
"Action" : "sts:AssumeRoleWithWebIdentity",
"Condition" : {
"StringEquals" : {
"${local.oidc_issuer}:sub" : "system:serviceaccount:${var.namespace}:${local.yet_another_cloudwatch_exporter_service_account_name}"
}
}
}
]
})
}
resource "aws_iam_role_policy" "cloudwatch_exporter_policy" {
name = "${var.cluster_name}-yace-policy"
role = aws_iam_role.yet_another_cloudwatch_exporter_role.id
policy = data.aws_iam_policy_document.yet_another_cloudwatch_exporter_policy_document.json
}
data "aws_iam_policy_document" "yet_another_cloudwatch_exporter_policy_document" {
statement {
sid = "YACEPolicy"
effect = "Allow"
# If necessary, add permissions to the list below(see https://github.com/prometheus-community/yet-another-cloudwatch-exporter?tab=readme-ov-file#authentication)
actions = [
"tag:GetResources",
"cloudwatch:ListTagsForResource",
"cloudwatch:GetMetricData",
"cloudwatch:ListMetrics",
"iam:ListAccountAliases",
]
resources = ["*"]
}
}
YACE の設定: Helm Chart (values) の設定
YACE の Helm Chart をデプロイする際、values を指定して設定をカスタマイズします。我々の環境では Terraform の helm_release
リソースを使用しており、values は templatefile
関数を使って .tftpl
ファイルから生成しています。これにより、terraform 内で定義されている変数を利用して YAML を記述できます。
resource "helm_release" "yet_another_cloudwatch_exporter" {
name = "${var.service_name}-yace"
repository = "https://prometheus-community.github.io/helm-charts"
chart = "yet-another-cloudwatch-exporter"
version = var.yet_another_cloudwatch_exporter_chart_version
namespace = var.namespace
timeout = 300
values = [
templatefile("${path.module}/prometheus_yet_another_cloudwatch_exporter.yaml.tftpl", {
aws_region = var.aws_region
service_name = var.service_name
namespace = var.namespace
replicas = var.yet_another_cloudwatch_exporter_replicas
pod_cpu = var.yet_another_cloudwatch_exporter_pod_cpu
pod_mem = var.yet_another_cloudwatch_exporter_pod_mem
prometheus_release_name = helm_release.kube_prometheus_stack.name
exporter_role = aws_iam_role.yet_another_cloudwatch_exporter_role.arn
service_account_name = local.yet_another_cloudwatch_exporter_service_account_name
})
]
}
以下では、values.yaml (あるいは .tftpl ファイル) で設定した主要な項目について解説します。型やデフォルト値などの詳細は YACE Helm Chart のドキュメント を参照してください。 (ここでは特に重要と思われる項目に絞って解説します。そのため、必要な設定項目が欠けていたりデフォルト値が意図とは違って設定されたりしていて、そのまま使用しても正常に動作しない可能性があります。あくまで擬似コードのような扱いで読んでください。また、セキュリティ関連の設定 podSecurityContext, securityContext や、リソース要求 resources、nodeSelector, tolerations, affinity などは環境に合わせて設定してください。)
なお、この設定にあたっては以下の資料を参考にしました:
- https://github.com/prometheus-community/helm-charts/blob/main/charts/prometheus-yet-another-cloudwatch-exporter/README.md
- https://artifacthub.io/packages/helm/yet-another-cloudwatch-exporter/yet-another-cloudwatch-exporter
- https://itpfdoc.hitachi.co.jp/manuals/3021/30213L0600e/IMRB0240.HTM
- https://itpfdoc.hitachi.co.jp/manuals/3021/30213L0600/IMRB0240.HTM
# サービスアカウント関連の設定
serviceAccount:
# IRSA を利用するため ServiceAccount を作成する
create: true
# 作成した ServiceAccount に、事前に用意した IAM Role の ARN をアノテーションで指定。これが無いと動きません。
annotations:
eks.amazonaws.com/role-arn: ${iam_role_arn} # Terraform の変数などで渡す
# pod のリソース。実態に応じて適切に決める。
# 我々の環境では、limits, requests ともに cpu: 200m, memory: 50Mi を指定している
resources:
limits:
cpu: ${pod_cpu} # 200m
memory: ${pod_mem} # 50Mi
requests:
cpu: ${pod_cpu} # 200m
memory: ${pod_mem} # 50Mi
# extraArgs: YACE プロセスの追加引数
extraArgs:
# Seconds to wait between scraping the AWS metrics with CloudWatch API
# CloudWatch API のスクレイプ間隔をデフォルトの 300 秒から 60 秒に変更
# 5 分程度のアラートルールを想定しているため、より短い間隔でメトリクスを取得する。
# また、今回取得するメトリクスは CloudWatch 上での取得間隔が 1 分なので、60 より短くする意味は無い
scraping-interval: 60
# serviceMonitor: Prometheus Operator との連携設定
serviceMonitor:
# ServiceMonitor リソースを作成して Prometheus の監視対象に自動追加する
# デフォルトは false なので指定が必要
enabled: true
# Prometheus や YACE の pod が置かれている namespace
namespace: monitoring
# Set how frequently Prometheus should scrape
# 今回はデータが 60s 間隔なのでそれに合わせている
interval: 60s
# Set path to cloudwatch-exporter telemtery-path
# YACE の pod の標準に合わせて指定
telemetryPath: /metrics
# kube-prometheus-stack でデプロイした Prometheus が ServiceMonitor を発見するためのラベル
# 以下のコマンドなどで確認可能:
# `kubectl get prometheus -n <namespace> <prometheus-release-name>-kube-prometheus -o yaml | grep -A 3 serviceMonitorSelector`
labels:
release: ${prometheus_release_name} # 例: myservice-prometheus など、prometheus のリリース名に合わせてください
# config: YACE のコア設定 (メトリクス取得内容の定義)
config:
# ここは次のセクションで説明するので省略
YACE の設定: Config の詳細設定について
config セクションは YACE の挙動を決定する重要な部分です。我々の環境では以下のような構成になっています。各項目の詳細はサンプルに続いて説明します。
apiVersion: v1alpha1
discovery:
exportedTagsOnMetrics:
AWS/ApplicationELB:
- Name
# どの AWS サービスのどのメトリクスを、どのリージョンから取得するかを定義します。
jobs:
# type でサービス名 (例: AWS/ApplicationELB, AWS/EC2, AWS/RDS など) を指定
- type: AWS/ApplicationELB
regions:
- ${aws_region}
dimensionNameRequirements:
- LoadBalancer
# period, length, delay: CloudWatch からデータを取得する際の期間とタイミングを制御します。
period: 60 # 60秒粒度のデータが欲しい。
length: 60 # 今回取得したいデータ点より過去のものが含まれないようにするため(詳細は後述)
delay: 30 # CloudWatch の遅延を吸収するためのバッファ。
# CloudWatch のタイムスタンプを使うかどうか
addCloudwatchTimestamp: false
# データが存在しない場合に 0 として扱うように true に設定します。
nilToZero: true
# metrics で取得したいメトリクス名と統計方法 (statistics) を指定
metrics:
- name: HTTPCode_ELB_5XX_Count
statistics:
- Sum
- name: HTTPCode_Target_5XX_Count
statistics:
- Sum
- name: HTTPCode_ELB_4XX_Count
statistics:
- Sum
- name: HTTPCode_ELB_500_Count
statistics:
- Sum
- name: HTTPCode_ELB_503_Count
statistics:
- Sum
- name: RequestCount
statistics:
- Sum
AWS リソースのタグを使って対象を絞り込む方法
jobs 直下で searchTags を指定することで、タグを使って対象リソースを絞り込むことも可能です。
# サンプル
apiVersion: v1alpha1
discovery:
exportedTagsOnMetrics:
AWS/ApplicationELB:
- Name
# どの AWS サービスのどのメトリクスを、どのリージョンから取得するかを定義します。
jobs:
# type でサービス名 (例: AWS/ApplicationELB, AWS/EC2, AWS/RDS など) を指定
- type: AWS/ApplicationELB
regions:
- ${aws_region}
dimensionNameRequirements:
- LoadBalancer
searchTags:
- key: "env"
value: "dev"
- key: "app"
value: "myapp.*"
注意: searchTags では正規表現が使えますが、内部の実装は Go 言語の regexp
なので、否定先読み・後読み (?!, ?<!) はサポートされていません。特定のタグが付いて「いない」リソースを除外する、といった複雑な指定は難しい場合があります。
period, length, delay の設定
YACE の config セクションでは、CloudWatch からデータを取得する際の期間やタイミングを制御するための設定がいくつかあります。以下にそれぞれの設定項目について説明します。
period: 60 # 60秒粒度のデータが欲しい。
length: 60 # 今回取得したいデータ点より過去のものが含まれないようにするため
delay: 30 # CloudWatch の遅延を吸収するためのバッファ。
period: 60
- CloudWatch からデータを取得する際の取得間隔を指定します。
- CloudWatch のメトリクスは、1 分間隔で出力されるものが多く、ALB のメトリクスもそうです。
length: 60
- length は、CloudWatch から取得するデータの期間を指定します。
- YACE は、現在時刻から delay 秒過去の時刻を基準点とし、そこからさらに length 秒遡った時刻までの期間のデータを CloudWatch から取得します。
- 上記の説明だけだと、60 以上の値であれば何でも良いように思えますが、実はそうではなさそうです。
- ALB のメトリクスなど一部のメトリクスには、値が 0 の場合にデータポイント自体が出力されないものがあります。
- 実験したところ、そのような状況のときに、YACE は取得できたデータのうち最新のものを現在の Prometheus のスクレイプ時刻に対応する値として報告する、という挙動のようです (※ addCloudwatchTimestamp: false の場合)。
- つまり、length をデータの取得間隔より長い値にしてしまうと、現時刻のデータポイントが存在しない(値が 0 だったため)場合に、取得対象期間内の古いデータポイントを現在時刻のデータとして採用してしまう可能性があります。
- 実際、我々の環境ではそのようになりました。
- length は、CloudWatch から取得するデータの期間を指定します。
delay: 30
- データが出力される各時刻において、時刻 00 秒に出力されるわけではなく、少し遅れます。
- そのため、delay を 0 (デフォルト) にすると、CloudWatch 側の遅延によりデータが取得できない可能性があります。
addCloudwatchTimestamp の設定
この項目は、Prometheus 上でのメトリクスのタイムスタンプとして、CloudWatch のタイムスタンプを使用するかどうかを決めるものです。false
にした場合は、Prometheus がデータを取得した時刻をそのデータのタイムスタンプとして扱います。
CloudWatch のメトリクスは、記録されてから API で取得可能になるまでに 2-3 分の遅延 があります。 つまり、00:05 に出力されるデータは 00:02 頃のものである可能性が高く、false の場合は Prometheus 上のタイムスタンプがデータの実際の時刻とずれることになります。 この項目を True にすると CloudWatch 上での正確な時刻を利用できますが、
- Prometheus から見れば過去時刻なので、データの上書きが発生する
- AlertManager は現時刻を見るので、クエリに offset が必要になってくる
といった不都合もあります。 我々は正確な時刻が必要なときには AWS のマネジメントコンソールを見に行けば良い、という判断のもと、false に設定して Prometheus 内での不都合を回避しています。
nilToZero の設定
上述の通り、CloudWatch には値が 0 の場合にデータポイントが出力されないメトリクスがあります。
Prometheus で rate()
や increase()
などの関数を正しく計算したり、グラフで値が途切れるのを防いだりするために、nilToZero
を true
に設定します。
metricsの設定
どのメトリクスを、どの集計方法で取得するかを設定する項目です。
metrics:
- name: HTTPCode_ELB_5XX_Count
statistics:
- Sum
- name: HTTPCode_Target_5XX_Count
statistics:
- Sum
- name: HTTPCode_ELB_4XX_Count
statistics:
- Sum
- name: HTTPCode_ELB_500_Count
statistics:
- Sum
- name: HTTPCode_ELB_503_Count
statistics:
- Sum
- name: RequestCount
statistics:
- Sum
ALB について取得できるメトリクス名については
Application Load Balancer の CloudWatch メトリクス
を参照しました。
また、今回は 1 分間の対象事象の数をカウントする必要があるので、集計方法は Sum
を選んでいます。
はまりどころ
ここまで設定方法をいろいろと解説してきましたが、私が実装を行っていた際に直面したはまりどころを再度まとめます。
- IAM 権限
iam:ListAccountAliases
が必要:- AWS アカウントにエイリアスが設定されている場合、この権限がないと YACE がエラーで起動に失敗する可能性があります。
- ドキュメントには明記されていないため注意が必要です。
- CloudWatch メトリクスの遅延と欠損:
- CloudWatch メトリクスには数分の遅延があります。
- 一部メトリクスは値が 0 の場合にデータポイントが出力されません。
- これらに対処するため、YACE の config で delay, length, nilToZero を適切に設定する必要があります。これらの値は、取得したいメトリクスの特性や許容できる遅延に応じて調整が必要です。
- タイムスタンプの扱い (addCloudwatchTimestamp):
- true にすると CloudWatch 上でのデータの時刻と一致しますが、Prometheus 上では上記の遅延のために取得した時点より過去のデータとして扱われます。
- false だと Prometheus のスクレイプ時刻がそのデータのタイムスタンプとして使われますが、CloudWatch 上の時刻とはズレます。
- どちらの挙動がシステムにとって都合が良いかを検討して選択する必要があります。
まとめ
本記事では、YACE を利用して EKS 上の Prometheus 環境に CloudWatch メトリクスを収集する手順と設定例、および注意点について解説しました。以下に内容をまとめます。
この構成の利点
- モニタリングデータの一元化: Kubernetes クラスタ内部、アプリケーション、AWS マネージドサービス (CloudWatch経由) のメトリクスを Prometheus クエリで横断的に分析できます。
- アラート管理の統合: Alertmanager を用いて、多様なソースからのアラートを一元的に管理できます。
考慮すべき事項
- CloudWatch API のコスト: GetMetricData API 等の呼び出しにはコストが発生します。監視対象メトリクス数、リソース数、取得頻度に応じてコストが増加する可能性があるため注意が必要です。metrics や searchTags によって取得する情報を絞り込むことが重要です。
- 設定パラメータの調整: 特に config セクションの delay, length は、CloudWatch の挙動やメトリクスの特性を理解した上で、環境に応じた調整が必要です。
- データ遅延の許容: CloudWatch 起因のデータ遅延は避けられません。リアルタイム性が極めて重要な監視には適さない場合があります。
この構成が適していると考えられるケース
- 既存の Prometheus ベースのモニタリング環境に、AWS マネージドサービスのメトリクスを統合したい場合。
- PromQL や Alertmanager を活用し、柔軟な SLI/SLO 計測やアラート設定を行いたい場合。
YACE は、いくつかの注意点はあるものの、CloudWatch メトリクスを Prometheus エコシステムに連携させるための有力な選択肢の一つです。本記事が、同様の課題に取り組む方々の一助となれば幸いです。今後は、他の AWS サービスのメトリクス収集でも YACE の活用を検討しています。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。