はじめに

この蚘事は AWS Advent Calendar 2021 の 5 日目の蚘事です。

Fargate で Node.js アプリのメトリクスを Prometheus Agent をサむドカヌコンテナずしお動かしお、Amazon Managed Service for Prometheus (AMP) に送信しお Grafana で芋られるようにしおみたした。

ちなみに Promethus Agent はただ 実隓的な機胜 なため、実務での利甚は掚奚したせん。

本蚘事の環境構築には AWS CDK を利甚しおいたす。

動䜜環境

  • Node.js v16.13.0
  • AWS CDK 2.0.0 (build 4b6ce31)
  • Prometheus 2.32.1

環境構築

早速環境構築を進めおいきたす。ただ AMP に぀いおは CDK から操䜜できないようでしたので、ワヌクスペヌスの䜜成に぀いおは AWS コン゜ヌルから手動で行いたす。(2021/12/06)

aws-aps を利甚するこずで AWS CDK からでも Amazon Managed Service for Prometheus のワヌクスペヌスを䜜成するこず確認できたしたので、そちらの利甚を掚奚いたしたす… 🙇🙇

lib/prometheus-agent-test-stack.ts のコヌドも修正枈みで AWS CDK で Amazon Managed Service for Prometheus のワヌクスペヌスを䜜成するように線集したした。(2021/12/18 远蚘)

手動で AMP のワヌクスペヌスを䜜成する手順

たず、AMP のコン゜ヌル画面 に遷移しおワヌクスペヌスを䜜成したす。

AMP のコン゜ヌル画面からワヌクスペヌスを䜜成する 1. AMP のコン゜ヌル画面 からワヌクスペヌス䜜成画面に遷移する

2. AMP のワヌクスペヌスを䜜成する 2. AMP のワヌクスペヌスを䜜成する

3. AMP のワヌクスペヌス䜜成完了ず同時に蚭定倀を控えおおく 3. AMP のワヌクスペヌス䜜成完了を確認するず同時に蚭定倀を控えおおく

AMP ワヌクスペヌスの ゚ンドポむント - リモヌト曞き蟌み URL は、Prometheus Agent で AMP にデヌタ送信する際や、Grafana でデヌタ゜ヌスを登録する際などに必芁ずなるため控えおおきたす。

AWS CDK で環境構築する

CDK で構築䜜業を進めたす。たずは䞋蚘コマンドで CDK プロゞェクトを䜜成したす。䜿甚蚀語は TypeScript を遞択したす。

mkdir prometheus-agent-test && cd prometheus-agent-test
cdk init --language typescript

たず CDK でむンフラ構築を進めおいく前に、メトリクス収集テスト甚の Node.js アプリを準備したす。

ECS Fargate で動かす Node.js アプリを準備する

prom-client を利甚しお、Node.js のメトリクスが取埗できるだけの Node.js アプリを準備したす。prometheus-agent-test フォルダで䞋蚘コマンドを実行したす。

mkdir metrics-app && cd metrics-app
npm init -y
npm install --save prom-client

次に metrics-app フォルダ内に index.js を䜜成しお䞋蚘を線集したす。

// metrics-app/index.js
"use strict";

const http = require("http");
const server = http.createServer();

const client = require("prom-client");
const register = new client.Registry();

// 5秒間隔でメトリクスを取埗する
client.collectDefaultMetrics({ register, timeout: 5 * 1000 });

server.on("request", async function (req, res) {
  // /metrics にアクセスしたら、Prometheus のレポヌトを返す
  if (req.url === "/metrics") {
    res.setHeader("Content-Type", register.contentType);

    const metrics = await register.metrics();
    return res.end(metrics);
  } else {
    return res.writeHead(404, { "Content-Type": "text/plain" });
  }
});

server.listen(8080);

node index.js コマンドを実行しお http://localhost:8080/metrics にアクセスしおみたす。䞋蚘のように各皮メトリクスが出力されおいる様子が確認できれば OK です。

Prometheus のレポヌトが正垞に出力されおいる様子 Prometheus のレポヌトが正垞に出力されおいる様子

今回は ECS 䞊で Node.js アプリを動䜜させるため、Dockerfile も䜜成したす。

# metrics-app/Dockerfile
FROM public.ecr.aws/docker/library/node:16-alpine3.12 AS builder

EXPOSE 8080
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install --max-old-space-size=4096

COPY . .

CMD [ "node", "index.js" ]

䞊蚘 Dockerfile 䜜成埌、再び動䜜怜蚌のため䞋蚘コマンドを実行しおから、http://localhost:8080/metrics にアクセスしおみたす。

docker build -t prometheus-agent-test/metrics-app .
docker run -p 8080:8080 prometheus-agent-test/metrics-app:latest

先ほどず同様に http://localhost:8080/metrics アクセス時に各皮メトリクスが出力されおいる様子を確認できれば OK です。

Node.js アプリを監芖する Prometheus Agent を準備する

たずは Prometheus 関連ファむルを配眮するためのフォルダを䜜成したす。prometheus-agent-test フォルダ内で䞋蚘コマンドを実行したす。

mkdir prometheus-agent && cd prometheus-agent

次に Prometheus の蚭定テンプレヌトファむルを䜜成したす。テンプレヌトファむルは sed を利甚しお䞭身の __TASK_ID__ および __REMOTE_WRITE_URL__ を曞き換えお利甚したす。

# prometheus-agent/prometheus.tmpl.yml
global:
  scrape_interval: 5s
  external_labels:
    monitor: "prometheus"

scrape_configs:
  - job_name: "prometheus-agent-test"
    static_configs:
      - targets: ["localhost:8080"]
        labels:
          # デフォルトの localhost:8080 がむンスタンスずしお利甚されるず、
          # メトリクスの刀別がしづらくなるため ECS Task の ID を利甚する
          instance: "__TASK_ID__"

remote_write:
  # AMP ワヌクスペヌス䜜成時に控えおおいた、
  # `゚ンドポむント - リモヌト曞き蟌み URL` を蚭定する箇所
  - url: "__REMOTE_WRITE_URL__"
    sigv4:
      region: ap-northeast-1
    queue_config:
      max_samples_per_send: 1000
      max_shards: 200
      capacity: 2500

蚭定ファむルの䜜成が完了したら、テンプレヌトファむルを利甚しお Prometheus の蚭定ファむルを䜜成し、Prometheus Agent を起動させるためのシェルスクリプトを䜜成したす。

# prometheus-agent/docker-entrypoint.sh
#!/bin/sh

while [ -z "$taskId" ]
do
  # ECS Fargate で起動したタスク ID を取埗する
  taskId=$(curl --silent ${ECS_CONTAINER_METADATA_URI}/task | jq -r '.TaskARN | split("/") | .[-1]')

  echo "waiting..."
  sleep 1
done

echo "taskId: ${taskId}"
echo "remoteWriteUrl: ${REMOTE_WRITE_URL}"

# タスク ID `taskId` および、環境倉数 `REMOTE_WRITE_URL` で、
# Prometheus のテンプレヌトファむル `prometheus.tmpl.yml` の内容を曞き換え、
# その結果を `/etc/prometheus/prometheus.yml` に出力する
cat /etc/prometheus/prometheus.tmpl.yml | \
    sed "s/__TASK_ID__/${taskId}/g" | \
    sed "s>__REMOTE_WRITE_URL__>${REMOTE_WRITE_URL}>g" > /etc/prometheus/prometheus.yml

# --enable-feature=agent で Prometheus を Agent モヌドで起動する
# Prometheus のコンフィグファむルには䞊蚘で出力した `/etc/prometheus/prometheus.yml` を利甚する
/usr/local/bin/prometheus \
    --enable-feature=agent \
    --config.file=/etc/prometheus/prometheus.yml \
    --web.console.libraries=/etc/prometheus/console_libraries \
    --web.console.templates=/etc/prometheus/consoles

これで Prometheus Agent 起動のための準備は敎ったため、最埌に Dockerfile を準備したす。ちなみに Prometheus Agent は v2.32.0 以降で利甚可胜です。本蚘事では v2.32.1 を利甚したす。

# prometheus-agent/Dockerfile
FROM --platform=arm64 alpine:3.15

ADD prometheus.tmpl.yml /etc/prometheus/

RUN apk add --update --no-cache jq sed curl

# ARM64 で動䜜する Prometheus v2.32.1 を curl でダりンロヌド展開する
RUN curl -sL -O https://github.com/prometheus/prometheus/releases/download/v2.32.1/prometheus-2.32.1.linux-arm64.tar.gz
RUN tar -zxvf prometheus-2.32.1.linux-arm64.tar.gz && rm prometheus-2.32.1.linux-arm64.tar.gz

# `prometheus` コマンドを `/usr/local/bin/prometheus` に移動する
RUN mv prometheus-2.32.1.linux-arm64/prometheus /usr/local/bin/prometheus

COPY ./docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
CMD ["/docker-entrypoint.sh"]

ここたでで CDK でむンフラ敎備を進めおいくための䞋準備は完了です。

ECS Fargate 䞊で Node.js アプリおよび Prometheus Agent を動䜜させる

あずは CDK で ECS Fargate 䞊で Node.js アプリおよび Prometheus Agent、Grafana を動䜜させるための環境を敎備しおいきたす。

lib/prometheus-agent-test-stack.ts の内容を曞き換えたす。

// lib/prometheus-agent-test-stack.ts
import { Construct } from "constructs";
import {
  Stack,
  StackProps,
  aws_ecs as ecs,
  aws_logs as logs,
  aws_aps as aps,
  aws_ecs_patterns as ecs_patterns,
  aws_iam as iam,
  aws_elasticloadbalancingv2 as elbv2,
  Duration,
  CfnOutput,
} from "aws-cdk-lib";

export class PrometheusAgentTestStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Node.js アプリに ecs_patterns.ApplicationLoadBalancedFargateService を利甚しお ALB 経由でアクセス可胜にする
    const projectName = "prometheus-agent-test";
    const fargateService =
      new ecs_patterns.ApplicationLoadBalancedFargateService(
        this,
        `${projectName}-fargate-service`,
        {
          serviceName: `${projectName}-fargate-service`,
          cpu: 256,
          desiredCount: 3,
          listenerPort: 80,
          taskImageOptions: {
            family: `${projectName}-taskdef`,
            image: ecs.ContainerImage.fromAsset("metrics-app"),
            containerPort: 8080,
            logDriver: ecs.LogDrivers.awsLogs({
              streamPrefix: `/${projectName}/metrics-app`,
              logRetention: logs.RetentionDays.ONE_DAY,
            }),
          },
          cluster: new ecs.Cluster(this, `${projectName}-cluster`, {
            clusterName: `${projectName}-cluster`,
          }),
          memoryLimitMiB: 512,
        }
      );
    fargateService.targetGroup.configureHealthCheck({
      path: "/metrics",
      timeout: Duration.seconds(8),
      interval: Duration.seconds(10),
      healthyThresholdCount: 2,
      unhealthyThresholdCount: 4,
      healthyHttpCodes: "200",
    });

    // 本質ではないが、Gravition2 で動䜜させたいために RuntimePlatform のプロパティを䞊曞きしおいる
    const fargateServiceTaskdef = fargateService.taskDefinition.node
      .defaultChild as ecs.CfnTaskDefinition;
    fargateServiceTaskdef.addPropertyOverride("RuntimePlatform", {
      CpuArchitecture: "ARM64",
      OperatingSystemFamily: "LINUX",
    });

    // AMP ぞの曞き蟌み暩限を付䞎する
    fargateService.taskDefinition.taskRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName(
        "AmazonPrometheusRemoteWriteAccess"
      )
    );

    // (2021/12/18) Amazon Managed Service for Prometheus のワヌクスペヌスを䜜成しお、Prometheus の remote-write URL を取埗する
    const apsWorkspace = new aps.CfnWorkspace(
      this,
      `${projectName}-prom-workspace`,
      {
        alias: `${projectName}-prom-workspace`,
      }
    );
    const apsWorkspaceRemoteUrl = `${apsWorkspace.attrPrometheusEndpoint}api/v1/remote_write`;

    // (2021/12/18) 本蚘事で頻出する "゚ンドポむント - リモヌト曞き蟌み URL" をコン゜ヌルに出力する
    new CfnOutput(this, "prom-remote-write-url", {
      value: apsWorkspaceRemoteUrl,
      description: "Prometheus Workspace の remote-write URL",
      exportName: "PromRemoteWriteURL",
    });

    // AMP ぞメトリクス情報を送信するための Prometheus Agent コンテナを远加する
    const containerName = `${projectName}-prometheus-agent`;
    fargateService.taskDefinition.addContainer(containerName, {
      containerName,
      image: ecs.ContainerImage.fromAsset("prometheus-agent"),
      memoryReservationMiB: 128,
      environment: {
        // (2021/12/18) CDK 経由で䜜成した Prometheus の remote-write URL を蚭定する
        REMOTE_WRITE_URL: apsWorkspaceRemoteUrl,
      },
      logging: new ecs.AwsLogDriver({
        streamPrefix: `/${projectName}/prometheus-agent`,
        logRetention: logs.RetentionDays.ONE_DAY,
      }),
    });

    // Grafana のタスク定矩を䜜成する
    const grafanaDashboardTaskDefinition = new ecs.FargateTaskDefinition(
      this,
      `${projectName}-grafana-taskdef`,
      {
        family: `${projectName}-grafana-taskdef`,
      }
    );
    // Grafana のタスクが Prometheus Query を叩けるように暩限付䞎する
    grafanaDashboardTaskDefinition.taskRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonPrometheusQueryAccess")
    );

    // Grafana のコンテナを远加する。パスプレフィクスには dashboard を蚭定する
    const grafanaDashboardContainerName = `${projectName}-grafana-dashboard`;
    grafanaDashboardTaskDefinition.addContainer(grafanaDashboardContainerName, {
      containerName: grafanaDashboardContainerName,
      image: ecs.ContainerImage.fromRegistry("public.ecr.aws/ubuntu/grafana"),
      environment: {
        AWS_SDK_LOAD_CONFIG: "true",
        GF_AUTH_SIGV4_AUTH_ENABLED: "true",
        GF_SERVER_SERVE_FROM_SUB_PATH: "true",
        GF_SERVER_ROOT_URL: "%(protocol)s://%(domain)s/dashboard",
      },
      portMappings: [{ containerPort: 3000 }],
      memoryLimitMiB: 512,
      logging: new ecs.AwsLogDriver({
        streamPrefix: `/${projectName}/grafana-dashboard`,
        logRetention: logs.RetentionDays.ONE_DAY,
      }),
    });

    const grafanaDashboardServiceName = `${projectName}-grafana-dashboard-service`;
    const grafanaDashboardService = new ecs.FargateService(
      this,
      grafanaDashboardServiceName,
      {
        serviceName: grafanaDashboardServiceName,
        cluster: fargateService.cluster,
        taskDefinition: grafanaDashboardTaskDefinition,
        desiredCount: 1,
      }
    );

    // Grafana のタスクを ALB のタヌゲットグルヌプに玐づける
    fargateService.listener.addTargets(
      `${projectName}-grafana-dashboard-target`,
      {
        priority: 1,
        conditions: [elbv2.ListenerCondition.pathPatterns(["/dashboard/*"])],
        healthCheck: {
          path: "/dashboard/login",
          interval: Duration.seconds(10),
          timeout: Duration.seconds(8),
          healthyThresholdCount: 2,
          unhealthyThresholdCount: 3,
          healthyHttpCodes: "200",
        },
        port: 3000,
        protocol: elbv2.ApplicationProtocol.HTTP,
        targets: [grafanaDashboardService],
      }
    );
  }
}

その埌、cdk deploy でむンフラを構築したす。

CDK によるむンフラ構築が正垞に実行された時の様子 CDK によるむンフラ構築が正垞に実行された時の様子

デプロむが正垞に完了したのを確認したら、Outputs に出力されおいる PrometheusAgentTestStack.prometheusagenttestfargateserviceServiceURL<識別子> の URL 末尟に /metrics を付䞎しおアクセスしおみたす。 出力されおいる URL のフォヌマットは http://<識別子>.ap-northeast-1.elb.amazonaws.com になりたす。

぀たり、http://<識別子>.ap-northeast-1.elb.amazonaws.com/metrics にアクセスしたす。

ALB 経由で Node.js アプリにアクセス可胜なこずを確認する ALB 経由で Node.js アプリにアクセス可胜なこずを確認する

たた、Outputs に出力されおいる PrometheusAgentTestStack.promremotewriteurl は埌に利甚する ゚ンドポむント - リモヌト曞き蟌み URL で䜿甚するので控えおおきたす。

ここたでで AWS CDK でのむンフラ構築䜜業は完了したした。最埌に Grafana で AMP のメトリクスを可芖化するための䜜業を進めおいきたす。

Grafana で Prometheus (AMP) のメトリクスを可芖化する

先ほどの /metrics パスぞのアクセス同様、Outputs に出力されおいる URL の末尟に /dashboard/login を付䞎しおアクセスしたす。Grafana の初期ナヌザおよびパスワヌドは admin ずなりたす。

぀たり、http://<識別子>.ap-northeast-1.elb.amazonaws.com/dashboard/login にアクセスしおみたす。

ログむンを行う

ログむン情報が正しければ、新しいパスワヌドを蚭定する画面に遷移するので新たなパスワヌドを入力しおログむンを終えたす。ログむン埌は、Prometheus (AMP) をデヌタ゜ヌスずしお远加するために䞋蚘の操䜜を行いたす。

1. 歯車アむコンをクリックしお <code>Data sources</code> をクリックする 1. 歯車アむコンをクリックしお Data sources をクリックする

2. <code>Add data source</code> ボタンをクリックする 2. Add data source ボタンをクリックする

3. デヌタ゜ヌスずしお Prometheus を遞択する 3. デヌタ゜ヌスずしお Prometheus を遞択する

4. Prometheus をデヌタ゜ヌスずしお远加する

4. Prometheus をデヌタ゜ヌスずしお远加する

4. Prometheus をデヌタ゜ヌスずしお远加する 4. Prometheus をデヌタ゜ヌスずしお远加する

Prometheus (AMP) に送信したメトリクスを Grafana で可芖化するための準備が敎ったので、実際に Grafana のダッシュボヌドでメトリクスを可芖化しおみたす。手っ取り早くメトリクスを可芖化するため、ダッシュボヌドには NodeJS Application Dashboard を利甚したす。

1. + アむコンをクリックしお、<code>Import</code> をクリックする 1. + アむコンをクリックしお、Import をクリックする

2. <code>NodeJS Application Dashboard</code> の ID を入力しお <code>Load</code> ボタンをクリックする 2. NodeJS Application Dashboard の ID を入力しお Load ボタンをクリックする

3. 必芁な情報を入力しお <code>NodeJS Application Dashboard</code> のむンポヌトを完了する 3. 必芁な情報を入力しお NodeJS Application Dashboard のむンポヌトを完了する

4. ダッシュボヌドから Prometheus のメトリクスが確認できる 4. ダッシュボヌドから Node.js アプリのメトリクスが確認できる

ここたでの手順でメトリクスの可芖化は完了したしたが、負荷に応じお実際にメトリクスが倉化する様子も確認しおみたす。Vegeta を利甚しお、実際に負荷をかけおみたす。䞋蚘コマンドを実行したす。

echo 'GET http://<識別子>.ap-northeast-1.elb.amazonaws.com/metrics' | vegeta attack -duration=5s | vegeta report

その埌、再び Grafana のダッシュボヌドを芋にいきたす。負荷をかけた時間垯のみグラフに倉化があるこずを確認できるはずです。

ダッシュボヌドの CPU 䜿甚率のグラフに倉化があったこずを確認できる ダッシュボヌドの CPU 䜿甚率のグラフに倉化があったこずを確認できる

おわりに

今回は ECS Fargate のメトリクスを Prometheus Agent で Amazon Managed Service for Prometheus (AMP) に送信し、それを Grafana で可芖化する方法に぀いお玹介したした。

ECS のサヌビスでタスクを実行する堎合は サヌビスディスカバリ の利甚が可胜なため、Prometheus の サヌビスディスカバリの蚭定 を行うこずで、単䞀の Prometheus で党おのコンテナのメトリクスを扱うこずも可胜です。

たた Node.js アプリを䜜成する際に利甚した prom-client で カスタムメトリクス を䜜成するこずで、監芖したい項目を自由に増やすこずも可胜です。

本蚘事が ECS Fargate を監芖する際の怜蚎材料の 1 ぀ずなれたら幞いです。

参考リンク