2021-12-19に投稿

📔 ECS Fargate のメトリクスを Prometheus Agent 使って AMP に送って Grafana で監視する

読了目安:18分

はじめに

この記事は 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

環境構築

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 つとなれたら幸いです。

参考リンク

Originally published at zenn.dev
ツイッターでシェア
みんなに共有、忘れないようにメモ

nikaera

xR企業でエンジニアやってます!主にプログラミングとゲームと音楽の話題を投稿します。サーバーサイドや Web 周りが主戦場ですが、最近は Standalone VR や WebXR に興味津々です。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント