Prometheus形式のメトリクスを収集するCloudWatch Agent on FargateをCDKでデプロイする
はじめに
Amazon ECS環境でPrometheus形式のメトリクスを収集するにあたって、CloudWatch Agentを使えばPrometheusの構築なしにCloudWatchのメトリクスで管理できるという話を最近知ったので、試してみた。また、最近はCDKを使って自動構築することが多いので、CDKでFargateを使って試してみた。
方式について
公式ガイド[1]に書かれているのは、独立したCloudWatch Agentをデプロイして、ECSのAPIを叩いてECSクラスター内の各コンテナのPrometheusエンドポイントを自動判別し、メトリクスを収集するというやり方。
一方、アプリケーションのサイドカーとしてCloudWatch Agentをデプロイして、そのアプリケーションのPrometheusエンドポイントを叩いてメトリクスを収集するやり方も設定上できそうなので、それも試してみた。
独立したCloudWatch Agentをデプロイする場合
以下、CDKによる実装例の抜粋。取得元となるアプリケーションはKeycloakのメトリクスの例となっている。ポイントとなる、CloudWatch Agentコンテナの環境変数として渡すPROMETHEUS_CONFIG_CONTENT
とCW_CONFIG_CONTENT
の設定内容については、以下の記事も参考になる。
- https://www.grugrut.net/posts/202204241336/
- https://qiita.com/SightSeekerTw/items/931834efbcff5b61eb0f
// ECSタスク実行用IAMロール作成
const ecsTaskExecutionRole = new iam.Role(this, "ecs-task-execution-role", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonECSTaskExecutionRolePolicy"
),
],
});
// ECR Publicからpullできるように許可
ecr.PublicGalleryAuthorizationToken.grantRead(ecsTaskExecutionRole);
// ECSタスク用IAMロール作成
const ecsTaskRole = new iam.Role(this, "ecs-task-role", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
// ECSタスクからCloudWatchメトリクスをputできるように
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"CloudWatchAgentServerPolicy"
),
],
});
// PrometheusのECSサービスディスカバリ用にECSの参照権限追加
new iam.Policy(this, "ecs-metrics", {
roles: [this.ecsTaskRole],
statements: [
new iam.PolicyStatement({
resources: ["*"],
actions: [
"ecs:ListTasks",
"ecs:ListServices",
"ecs:DescribeContainerInstances",
"ecs:DescribeServices",
"ecs:DescribeTasks",
"ecs:DescribeTaskDefinition",
],
}),
],
});
// Fargateタスク定義作成
const fargateTaskDefinition = new ecs.FargateTaskDefinition(
this,
"cloudwatch-agent-task",
{
family: "cloudwatch-agent",
cpu: 256,
memoryLimitMiB: 512,
taskRole: ecsTaskRole,
executionRole: ecsTaskExecutionRole,
}
);
// ロググループ作成
const logGroup = new logs.LogGroup(this, "log-group", {
logGroupName: `/ecs/cloudwatch-agent`,
retention: 7,
removalPolicy: RemovalPolicy.DESTROY,
});
// Fargateタスク定義にCloudWatch Agentコンテナ定義を追加
const cloudWatchAgent = fargateTaskDefinition.addContainer(
"cloudwatch-agent",
{
image: ecs.ContainerImage.fromRegistry(
"public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest"
),
essential: true,
environment: {
PROMETHEUS_CONFIG_CONTENT,
CW_CONFIG_CONTENT: JSON.stringify(
toCWConfig(logGroup.logGroupName, "keycloak", "keycloak")
),
},
memoryReservationMiB: 50,
portMappings: [{ containerPort: 8080 }],
logging: new ecs.AwsLogDriver({
streamPrefix: "ecs",
logGroup,
}),
}
);
// Fargateサービス作成
const service = new ecs.FargateService(this, "cloudwatch-agent-service", {
cluster: ...,
serviceName: "cloudwatch-agent",
taskDefinition: fargateTaskDefinition,
desiredCount: 1,
... (適宜、その他必要な定義)
});
上記のコード内で利用しているPROMETHEUS_CONFIG_CONTENT
定数とtoCWConfig
関数は以下のような定義となる。
// PROMETHEUS_CONFIG_CONTENTとtoCWConfig関数の定義
const PROMETHEUS_CONFIG_CONTENT = `
global:
scrape_interval: 1m
scrape_timeout: 10s
scrape_configs:
- job_name: cwagent-ecs-file-sd-config
sample_limit: 10000
file_sd_configs:
- files: ["/tmp/cwagent_ecs_auto_sd.yaml"]
`;
function toCWConfig(logGroupName: string, targetFamilyName: string, targetContainerName: string) {
return {
agent: {
debug: false,
},
logs: {
metrics_collected: {
prometheus: {
log_group_name: logGroupName,
prometheus_config_path: "env:PROMETHEUS_CONFIG_CONTENT",
ecs_service_discovery: {
sd_frequency: "1m",
sd_result_file: "/tmp/cwagent_ecs_auto_sd.yaml",
docker_label: {},
task_definition_list: [
{
sd_job_name: targetFamilyName,
sd_metrics_ports: "8080",
sd_task_definition_arn_pattern: `.*:task-definition/${targetFamilyName}:*`,
sd_metrics_path: "/metrics",
},
],
},
emf_processor: {
metric_namespace: "CWAgent",
metric_declaration: [
{
source_labels: ["container_name"],
label_matcher: `^${targetContainerName}$`,
dimensions: [["ClusterName", "TaskDefinitionFamily"]],
metric_selectors: [
"^base_classloader_loadedClasses_count$",
"^base_classloader_loadedClasses_total$",
... // 取得したいメトリクス名を並べる
],
},
{
source_labels: ["container_name"],
label_matcher: `^${targetContainerName}$`,
dimensions: [["ClusterName", "TaskDefinitionFamily", "name"]],
metric_selectors: [
"^base_gc_time_total_seconds$",
"^base_gc_total$",
... // 取得したいメトリクス名を並べる
],
},
],
},
},
},
force_flush_interval: 5,
},
};
}
サイドカーでCloudWatch Agentをデプロイする場合
アプリケーションコンテナのサイドカーとしてCloudWatch Agentを追加するようにする。
// ECSタスク実行用IAMロール作成
const ecsTaskExecutionRole = new iam.Role(this, "ecs-task-execution-role", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonECSTaskExecutionRolePolicy"
),
],
});
// ECR Publicからpullできるように許可
ecr.PublicGalleryAuthorizationToken.grantRead(ecsTaskExecutionRole);
// ECSタスク用IAMロール作成
const ecsTaskRole = new iam.Role(this, "ecs-task-role", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
// ECSタスクからCloudWatchメトリクスをputできるように
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"CloudWatchAgentServerPolicy"
),
],
});
// ロググループ
const logGroup = ...
// KeycloakのFargateタスク作成
const fargateTaskDefinition = new ecs.FargateTaskDefinition(
this,
"keycloak-task",
{
family: "keycloak",
taskRole: ecsTaskRole,
executionRole: ecsTaskExecutionRole,
...
}
);
// Keycloakのコンテナの定義
const containerDefinition = fargateTaskDefinition.addContainer("keycloak", {
essential: true,
...
});
// サイドカーとするCloudWatch Agentの定義
const cloudWatchAgent = fargateTaskDefinition.addContainer(
"cloudwatch-agent",
{
image: ecs.ContainerImage.fromRegistry(
"public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest"
),
environment: {
PROMETHEUS_CONFIG_CONTENT,
CW_CONFIG_CONTENT: getCWConfig(logGroup.logGroupName),
},
memoryReservationMiB: 50,
logging: new ecs.AwsLogDriver({
streamPrefix: "ecs",
logGroup,
}),
}
);
上記のコード内で利用しているPROMETHEUS_CONFIG_CONTENT
定数とtoCWConfig
関数は以下のような定義となる。ポイントは、サイドカーなので http://127.0.0.1:8080/metrics で同一ECSタスク定義内にあるKeycloakのPrometheusエンドポイントにアクセス可能なため、Service Discoveryは使わずに static_configs
の定義だけでOKとなる。
const PROMETHEUS_CONFIG_CONTENT = `
global:
scrape_interval: 1m
scrape_timeout: 10s
scrape_configs:
- job_name: cwagent-static-config
sample_limit: 10000
metrics_path: /metrics
static_configs:
- targets: ['127.0.0.1:8080']
`;
function getCWConfig(logGroupName: string) {
return JSON.stringify({
agent: {
debug: false,
},
logs: {
metrics_collected: {
prometheus: {
log_group_name: logGroupName,
prometheus_config_path: "env:PROMETHEUS_CONFIG_CONTENT",
emf_processor: {
metric_namespace: "CWAgent",
metric_declaration: [
{
source_labels: ["job"],
label_matcher: "^cwagent-static-config$",
dimensions: [["ClusterName"]],
metric_selectors: [
"^base_classloader_loadedClasses_count$",
"^base_classloader_loadedClasses_total$",
... // 取得したいメトリクス名を並べる
],
},
],
},
},
},
force_flush_interval: 5,
},
});
}
metric_declaration
の定義はちょっと工夫が必要。CloudWatch Logsの方に、実際に取得したメトリクス情報が書かれているので、それを参考にsource_labels
、label_matcher
、dimensions
、metric_selectors
を設定するとよい。
{
"ClusterName": "sample-cluster",
"Timestamp": "1677049866519",
"Version": "0",
"instance": "127.0.0.1:8080",
"job": "cwagent-static-config",
"name": "G1 Survivor Space",
"prom_metric_type": "gauge",
"vendor_memoryPool_usage_bytes": 1048576,
"vendor_memoryPool_usage_max_bytes": 6291456
}
うまく期待通りに動かないときには、CW_CONFIG_CONTENT
のJSON設定のdebug: false
をtrue
にしてデバッグログを有効化するとよい。
Discussion