〽️

Prometheus形式のメトリクスを収集するCloudWatch Agent on FargateをCDKでデプロイする

2023/02/27に公開

はじめに

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_CONTENTCW_CONFIG_CONTENTの設定内容については、以下の記事も参考になる。

    // 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_labelslabel_matcherdimensionsmetric_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: falsetrueにしてデバッグログを有効化するとよい。

脚注
  1. https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Setup-ECS.html ↩︎

Discussion