📊

ECS Service Connect で 複数 ECS サービスからのメトリクスを Prometheus + Grafana で監視する

に公開

この記事でやること

複数のメトリクス出力アプリ/Prometheus/Grafana をそれぞれ ECS サービスとして起動して、ECS Service Connect を使用して Grafana 上でメトリクス可視化を実現します。

きっかけ

Prometheus メトリクスを出力するアプリがあり、そのメトリクスを Grafana で可視化したいというユースケースはよくあると思います。

ローカル環境であれば、docker-compose.yaml を使用し、アプリコンテナ/設定ファイルを一緒にビルドした Prometheus コンテナ/Grafana コンテナを同時に起動してあげれば、比較的簡単に Grafana でメトリクスを可視化させることが可能です。

ある日、検証において ECS 上でそれらを実現する必要が出てきました。

それも、メトリクスを出力するアプリは複数あり、それぞれ独立しているアプリです。
さらに、アプリが今後も追加されていく可能性もあります。

そこで、ServiceConnect でサービス間通信を行い、独立したアプリ(ECS サービス)から Prometheus -> Grafana と連携してメトリクスを出力できるような構成を作っておけば便利かなと思い、今回の検証を行ってみます。

アーキテクチャ

1つの ECS クラスター/同じ Namespace の中に、アプリケーション3つ、Prometheus、Grafana の各 ECS サービスが存在します。

それぞれは Service Connect によりサービス間通信を行います。

アプリケーション

Prometheus メトリクスを出力するアプリについて、今回は検証のためパブリックなイメージを使用しています。

App1: node-exporter

  • イメージ: prom/node-exporter:latest
  • ポート: 9100
  • Service Connect DNS: app1:9100

App2: cAdvisor

  • イメージ: gcr.io/cadvisor/cadvisor:latest
  • ポート: 8080
  • Service Connect DNS: app2:8080

App3: prometheus-example-app

  • イメージ: quay.io/brancz/prometheus-example-app:v0.3.0
  • ポート: 8080
  • Service Connect DNS: app3:8080

Prometheus

  • イメージ: prom/prometheus:latest
  • ポート: 9090
  • Service Connect DNS: prometheus:9090
  • 設定: SSM Parameter Store (/ecs/prometheus/config)

設定ファイルは以下のように記載します。targets には、Service Connect DNS名を入れるだけで名前解決してくれます。

prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'app1-node-exporter'
    static_configs:
      - targets: ['app1:9100']
  
  - job_name: 'app2-cadvisor'
    static_configs:
      - targets: ['app2:8080']
  
  - job_name: 'app3-example-app'
    static_configs:
      - targets: ['app3:8080']

Grafana

  • イメージ: grafana/grafana:latest
  • ポート: 3000
  • Service Connect DNS: grafana:3000

デプロイ手順

1. CloudFormationスタックのデプロイ

使用したスタックテンプレートは以下です。VPC、サブネットなどのネットワークリソースは個人のものを指定してください。

スタックテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECS Service Connect with Prometheus metrics exporters'

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id  
  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
  SecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  ClusterName:
    Type: String
    Default: default

Resources:
  ServiceConnectNamespace:
    Type: AWS::ServiceDiscovery::HttpNamespace
    Properties:
      Name: monitoring-namespace

  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: SSMAccess
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameters
                  - ssm:GetParameter
                Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/ecs/prometheus/*'

  App1TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: metrics-app1
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: '256'
      Memory: '512'
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: exporter
          Image: prom/node-exporter:latest
          PortMappings:
            - Name: app1-metrics
              ContainerPort: 9100
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref App1LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: app1

  App1LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/metrics-app1
      RetentionInDays: 7

  App1Service:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: metrics-app1
      Cluster: !Ref ClusterName
      TaskDefinition: !Ref App1TaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: !Ref SubnetIds
          SecurityGroups: [!Ref SecurityGroupId]
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt ServiceConnectNamespace.Arn
        Services:
          - PortName: app1-metrics
            ClientAliases:
              - Port: 9100
                DnsName: app1

  App2TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: metrics-app2
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: '256'
      Memory: '512'
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: cadvisor
          Image: gcr.io/cadvisor/cadvisor:latest
          PortMappings:
            - Name: app2-metrics
              ContainerPort: 8080
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref App2LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: app2

  App2LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/metrics-app2
      RetentionInDays: 7

  App2Service:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: metrics-app2
      Cluster: !Ref ClusterName
      TaskDefinition: !Ref App2TaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: !Ref SubnetIds
          SecurityGroups: [!Ref SecurityGroupId]
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt ServiceConnectNamespace.Arn
        Services:
          - PortName: app2-metrics
            ClientAliases:
              - Port: 8080
                DnsName: app2

  App3TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: metrics-app3
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: '256'
      Memory: '512'
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: exporter
          Image: quay.io/brancz/prometheus-example-app:v0.3.0
          PortMappings:
            - Name: app3-metrics
              ContainerPort: 8080
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref App3LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: app3

  App3LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/metrics-app3
      RetentionInDays: 7

  App3Service:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: metrics-app3
      Cluster: !Ref ClusterName
      TaskDefinition: !Ref App3TaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: !Ref SubnetIds
          SecurityGroups: [!Ref SecurityGroupId]
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt ServiceConnectNamespace.Arn
        Services:
          - PortName: app3-metrics
            ClientAliases:
              - Port: 8080
                DnsName: app3

  PrometheusTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: prometheus
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: '512'
      Memory: '1024'
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: prometheus
          Image: prom/prometheus:latest
          PortMappings:
            - Name: prometheus
              ContainerPort: 9090
          Secrets:
            - Name: PROMETHEUS_CONFIG
              ValueFrom: /ecs/prometheus/config
          EntryPoint:
            - /bin/sh
            - -c
          Command:
            - |
              echo "$PROMETHEUS_CONFIG" > /etc/prometheus/prometheus.yml
              /bin/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref PrometheusLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: prometheus

  PrometheusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/prometheus
      RetentionInDays: 7

  PrometheusService:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: prometheus
      Cluster: !Ref ClusterName
      TaskDefinition: !Ref PrometheusTaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: !Ref SubnetIds
          SecurityGroups: [!Ref SecurityGroupId]
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt ServiceConnectNamespace.Arn
        Services:
          - PortName: prometheus
            ClientAliases:
              - Port: 9090
                DnsName: prometheus

  GrafanaTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: grafana
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: '512'
      Memory: '1024'
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: grafana
          Image: grafana/grafana:latest
          PortMappings:
            - Name: grafana
              ContainerPort: 3000
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref GrafanaLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: grafana

  GrafanaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/grafana
      RetentionInDays: 7

  GrafanaService:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: grafana
      Cluster: !Ref ClusterName
      TaskDefinition: !Ref GrafanaTaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: !Ref SubnetIds
          SecurityGroups: [!Ref SecurityGroupId]
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt ServiceConnectNamespace.Arn
        Services:
          - PortName: grafana
            ClientAliases:
              - Port: 3000
                DnsName: grafana

Outputs:
  NamespaceArn:
    Value: !GetAtt ServiceConnectNamespace.Arn
  App1ServiceName:
    Value: !GetAtt App1Service.Name
  App2ServiceName:
    Value: !GetAtt App2Service.Name
  App3ServiceName:
    Value: !GetAtt App3Service.Name
  PrometheusServiceName:
    Value: !GetAtt PrometheusService.Name
  GrafanaServiceName:
    Value: !GetAtt GrafanaService.Name

2. Prometheus設定の登録

Prometheus の設定ファイルは SSM パラメーターに格納し、コンテナ起動時に変数 PROMETHEUS_CONFIG として設定ファイルに読み込ませます。

aws ssm put-parameter \
  --name /ecs/prometheus/config \
  --value "$(cat prometheus.yml)" \
  --type String \
  --overwrite \
  --region ap-northeast-1

3. Grafanaの設定

1. Prometheusをデータソースとして追加

  1. Grafanaにアクセス: http://<grafana-public-ip>:3000

  2. デフォルトログイン: ID: admin / PASS: admin でログイン
    1. Grafanaにアクセス:

  3. Connections → Connections → Add new connection

  4. Prometheusを選択して Connectionhttp://prometheus:9090 を入力して Save & Test




2. ダッシュボード作成

  • Dashboards -> Add visualization で新しくダッシュボードを作成

  • データソースには先ほど設定した Prometheus を選択

  • クエリを作成。 job でフィルタリングすると、各 ECS サービスに限定してメトリクスを確認できます。Run queries を押下することでクエリが走ります。

  • それぞれのアプリ(ECS サービス)のメトリクスが確認できます。


注意事項

  • 今回は検証用に Grafana で各サービスごとのメトリクスを確認することを優先しており、冗長性などを考慮していません。特に Prometheus, Grafana では取得したメトリクスや保存された設定内容は、タスクが停止すると削除されます。実際の運用では、データの永続化/高可用性を検討する必要があります。

  • 新たに ECS サービスとしてアプリを追加しメトリクスを取得したい場合、Prometheus の設定を更新する必要があります。設定ファイルを保存している SSM Parameter Store を更新後、Prometheus タスクをデプロイし直す必要があります。

  • Service Connect の仕様上、同じ名前空間上で ECS サービスを起動させる必要があります。

まとめ

ECS Service Connectを活用することで、Prometheus + Grafana の監視環境を簡単に構築できます。固定 DNS 名による接続ができて便利ですね。

Service Connect の使い道として、このような使い方がどなたかの参考になれば嬉しいです🎄

Discussion