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名を入れるだけで名前解決してくれます。
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をデータソースとして追加
-
Grafanaにアクセス:
http://<grafana-public-ip>:3000 -
デフォルトログイン: ID:
admin/ PASS:adminでログイン

-
Connections → Connections → Add new connection
-
Prometheusを選択して Connection
http://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