おそらく誰しも一度は、なんでこんな感じになってしまったんだろう?みたいなインフラ構成に出くわしたことがあるんじゃないでしょうか。
自分の場合は、2つのアプリケーションを別のVPC上のECSで動かしていて、ECS間の通信をインターネット経由で行っているという構成が放置されていました。VPCをまたいでいるため、ECSタスク同士の通信がNAT GatewayやALBを経由しており、パフォーマンス、セキュリティ、コスト、どれをとってもあまりよくない状態でした。
結論:Service Connectを断念してService Discoveryで構成した
AWSのドキュメントやECSの設定画面では、"Service Connect推奨"と書かれていて、Cloud Mapを使ったService Discovery(サービス検出)構成はなんとなく古いやり方に思えたのですが、結果的にService Connectを使わず、Service Discoveryだけで構成しました。

Blue/GreenデプロイとService Connectは共存できない
構成としては、接続元のECS(以降A)から補助的な機能をもつ接続先のECS(B)に通信する形です。AのタスクはALB経由で外部公開しており、デプロイにはCodeDeploy
によるBlue/Green方式を採用しています。
トラフィックの切り替え制御でもしものときのロールバックが簡単だったり、最小ダウンタイムでのリリースができるのでBlue/Greenデプロイを選択したのですが、ECSのサービス定義でService Connectを有効にしてデプロイを試みたところ、以下のようなエラーが返ってきてしまいました。
InvalidParameterException: DeploymentController#type CODE_DEPLOY is not supported by ECS Service Connect.
なんと、Service ConnectはCodeDeploy
ベースのBlue/Greenデプロイと併用できませんでした。
この時点で、自分の構成ではService Connectを採用できないと判断しました。
そもそものService Connectのよさ
Service Connectを有効にすると、Envoyというサイドカーのプロキシを各タスクに自動で入れてくれます。
- 内部DNSの抽象化(clientAliases)
- トラフィックの可視化(Service Graph)
- mTLSやセキュリティポリシー強化
など、サービス間通信をモダンにしてくれます。
でも、Envoyが裏で動いているので、その分のリソース(メモリ、CPU)が必要になるというデメリットもあります。例えば、FargateだとvCPUとメモリサイズで料金が変わってくるので、リソース増でコストも上がってしまいます。
今回は接続先(B)が補助的なマイクロサービスで
、Service Graphでの可視化までは必要なしと考え、Service Discoveryだけでシンプルにいこうと判断しました。
Service ConnectとService Discoveryの簡単な比較
項目 | Service Connect | Service Discovery |
---|---|---|
サイドカー | 必要(Envoy) | 不要 |
通信パス | ECS A → Envoy A → Envoy B → ECS B | ECS A → ECS B |
CPU・メモリ負荷 | 増える(サイドカーあり) | 軽い(直接通信) |
Blue/Green対応 | 非対応 | 対応可能 |
DNS名の簡潔さ | clientAliasesで短縮可能 |
service.service.local など長め |
より詳細に知りたい方は、以下の記事がとても参考になります。
Service Discovery(Cloud Map)の導入手順
今回採用したService Discovery構成を組む手順を紹介します。Cloud Mapを使って、接続先のECSサービス(B)を名前解決できるようにします。
1. 名前空間(Cloud Map)の作成
まずはCloud Mapの名前空間を作成します。名前空間はサービスのドメインのようなもので、たとえば service.local
のように使います。
Terraformで作成する場合は以下のようになります。
resource "aws_service_discovery_private_dns_namespace" "service_local" {
name = "service.local"
description = "Private DNS namespace"
vpc = <vpc_id>
}
2. サービスの作成
この名前空間に対して、ECSサービスB用の「サービス」を作成します。
resource "aws_service_discovery_service" "service_b" {
name = "service-b"
namespace_id = aws_service_discovery_private_dns_namespace.service_local.id
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.service_local.id
dns_records {
type = "A"
ttl = 30
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 3
}
}
3. セキュリティグループの作成
接続元(ECS A)
通信元のECSタスクが、ECS Bにアクセスできるように双方向のルールを設定します。
resource "aws_security_group" "service_a_ecs_sg" {
name = "service-a-ecs-sg"
description = "Security group for ECS A (caller)"
vpc_id = <vpc-id>
}
resource "aws_security_group_rule" "service_a_to_b_egress" {
type = "egress"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_group_id = aws_security_group.service_a_ecs_sg.id
source_security_group_id = aws_security_group.service_b_ecs_sg.id
}
接続元(ECS B)
resource "aws_security_group" "service_b_ecs_sg" {
name = "service-b-ecs-sg"
description = "Security group for ECS B (callee)"
vpc_id = <vpc-id>
}
resource "aws_security_group_rule" "service_b_from_a_ingress" {
type = "ingress"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_group_id = aws_security_group.service_b_ecs_sg.id
source_security_group_id = aws_security_group.service_a_ecs_sg.id
}
# 必要であれば外部通信も許可
resource "aws_security_group_rule" "service_b_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.service_b_ecs_sg.id
cidr_blocks = ["0.0.0.0/0"]
}
4. アプリケーションの接続先を変更
ECS(A)のアプリケーションから、ECS(B)をhttp://service-b.service.local:8080
のようにアクセスできるようになります。ここで:8080
のようにポート番号を明示する必要がある点に注意が必要です。
5. 接続先(ECS B)のサービス/タスク定義を更新
サービス定義
接続先(ECS B)のサービス定義で、Service Discoveryに作成したCloud Mapの名前空間とサービスを登録します。
ecspressoでサービス定義を管理している場合は以下を追加します。
"serviceRegistries": [
{
"registryArn": "arn:aws:servicediscovery:ap-northeast-1:*********:service/srv-*********"
}
]
タスク定義
接続先ECS Bの前段にALBがなくなるので、ヘルスチェックをECS自身でできるようにタスク定義へhealthCheck
を追加します。以下はコンテナポート8080
を使用している例です。
"containerDefinitions": [
{
(略)
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp",
}
],
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost:8080/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 10
}
}
ヘルスチェックでcurl
を叩くので、コンテナイメージにcurl
が含まれていない場合は、Dockerfileにcurl
をインストールする記述(ex. RUN apk add --no-cache curl
)を追加します。
Service Connectの導入手順
今回はBlue/Greenデプロイとの併用ができないため採用しませんでしたが、参考までにService Connectの導入手順も紹介します。
1. 名前空間(Cloud Map)の作成
Service Discoveryと同様、service.local
などのプライベートDNS名前空間をCloud Map上に作成します。Terraformも同様です。
補足:Service Connectでは「サービス登録」を手動で行う必要はありません。ECSのデプロイ時に自動でCloud Mapに登録されます。
2. セキュリティグループの作成
Service Connectも内部ではEnvoyを経由したTCP通信のため、基本的には通常のECS間通信と同様にポート指定のセキュリティルールが必要です。
接続元(ECS A)
通信元のECSタスクが、ECS Bにアクセスできるように双方向のルールを設定します。
resource "aws_security_group" "service_a_ecs_sg" {
name = "service-a-ecs-sg"
description = "Security group for ECS A (Service Connect caller)"
vpc_id = <vpc-id>
}
resource "aws_security_group_rule" "service_a_to_b_egress" {
type = "egress"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_group_id = aws_security_group.service_a_ecs_sg.id
source_security_group_id = aws_security_group.service_b_ecs_sg.id
}
接続元(ECS B)
resource "aws_security_group" "service_b_ecs_sg" {
name = "service-b-ecs-sg"
description = "Security group for ECS B (Service Connect callee)"
vpc_id = <vpc-id>
}
resource "aws_security_group_rule" "service_b_from_a_ingress" {
type = "ingress"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_group_id = aws_security_group.service_b_ecs_sg.id
source_security_group_id = aws_security_group.service_a_ecs_sg.id
}
# 必要であれば外部通信も許可
resource "aws_security_group_rule" "service_b_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.service_b_ecs_sg.id
cidr_blocks = ["0.0.0.0/0"]
}
3. サービス定義の更新(ECS A)
コンソール画面からService Connectの設定を行います。
ecspressoでサービス定義を管理している場合は、serviceConnectConfiguration
を追加します。
"serviceConnectConfiguration": {
"enabled": true,
"namespace": "service.local",
"services": [
{
"portName": "http",
"discoveryName": "service",
"clientAliases": [
{
"port": 80,
"dnsName": "service"
}
]
}
]
},
注意:CodeDeploy(Blue/Greenデプロイ)とService Connectは併用できません。
DeploymentController#type CODE_DEPLOY is not supported
というエラーになります。
アプリケーション内の向き先変更
アプリケーション側では、以下のように clientAliases.dnsName
にあわせて通信先を指定します。
http://service
ポート番号の指定は不要です。
4. サービス定義の更新(ECS B)
ECS B側も同様にService Connect設定を行います。
ecspressoの記述も同様です。
"serviceConnectConfiguration": {
"enabled": true,
"namespace": "service.local",
"services": [
{
"portName": "http",
"discoveryName": "service",
"clientAliases": [
{
"port": 80,
"dnsName": "service"
}
]
}
]
},
5. タスク定義の更新(ECS B)
ALB を使わない構成になるため、タスク自身がヘルスチェックを返すように構成します。
"containerDefinitions": [
{
(略)
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp",
}
],
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost:8080/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 10
}
}
ヘルスチェックでcurl
を叩くので、コンテナイメージにcurl
が含まれていない場合は、Dockerfileにcurl
をインストールする記述(ex. RUN apk add --no-cache curl
)を追加します。
まとめ
Service ConnectはBlue/Greenデプロイとの相性に制限があるものの、DNS名のシンプルさや将来的な拡張性、トラフィック可視化など多くの利点があります。
今回のように小規模でBlue/Greenが必要なケースではService Discoveryが適していますが、中〜大規模なマイクロサービス環境やセキュリティ要件が高い環境では、Service Connectを選ぶ価値があると思います。