ECS Service Connectに移行して6ヶ月、App Meshの複雑さから解放された話
App Mesh + Cloud Mapで疲弊した後、ECS Service Connectへ移行して半年。インシデントが減って設定量も激減した実感と、移行で気づいた落とし穴やトレードオフを正直に書きます。
ECS Service Connectを本番導入して6ヶ月、サービスメッシュの複雑さから解放された話
App Mesh + Cloud Mapの組み合わせで1年間ひどい目に遭った後、ECS Service Connectへ移行してちょうど6ヶ月が経った。App Mesh + Cloud Mapを1年本番運用して分かった「しんどかった話」でも触れたんだけど、あの構成は設定ファイルの量と理解コストが高すぎて、チームが疲弊していた。
移行後、「サービスメッシュ関連のインシデント」がSlackに流れてくる頻度が体感でかなり下がった。正直最初は「AWS独自のサービス、どうせ制約多いでしょ」と懐疑的だったけど、6ヶ月運用してみると思ったより「普通に使えるじゃないか」という印象に変わってきた。
実際に移行して気づいた設定の勘どころ、落とし穴、App Meshとのトレードオフをまとめていく。完璧な答えを書けるほど自信があるわけじゃないけど、同じ悩みを持ってる人の参考になれば。
そもそもService Connectって何が違うのか
ECS Service Connectは2022年末にGAして、2024年〜2026年にかけて継続的にアップデートされてきた機能だ。一言で言うと「ECS が管理するサービスディスカバリ + サイドカープロキシ」を、設定ほぼゼロで使えるようにしたもの。
従来のApp Meshは、Envoyのサイドカー設定・VirtualNode・VirtualRouter・VirtualServiceといったリソースを手動で定義する必要があり、構成が大きくなるにつれてTerraformのコードが爆発的に増えた。対してService Connectは、ECSタスク定義にちょっと設定を足すだけで動く。その「ちょっと」が本当にちょっとで済むのが最初は信じられなかった。
flowchart TB
subgraph VPC[VPC: 10.0.0.0/16]
subgraph AZ_A[AZ: ap-northeast-1a]
subgraph PrivSubA[Private Subnet A: 10.0.1.0/24]
TaskA1[ECS Task\napi-service\n+ SC Proxy]
TaskA2[ECS Task\norder-service\n+ SC Proxy]
end
end
subgraph AZ_B[AZ: ap-northeast-1b]
subgraph PrivSubB[Private Subnet B: 10.0.2.0/24]
TaskB1[ECS Task\napi-service\n+ SC Proxy]
TaskB2[ECS Task\norder-service\n+ SC Proxy]
end
end
subgraph Namespace[Cloud Map Namespace: myapp.internal]
NS1[api-service\nDNS auto-registered]
NS2[order-service\nDNS auto-registered]
end
ALB[Application Load Balancer\n外部トラフィック]
subgraph PublicSubnet[Public Subnet]
ALB
end
end
ALB --> TaskA1
ALB --> TaskB1
TaskA1 <-->|Service Connect\nProxy-to-Proxy| TaskA2
TaskA1 <-->|Service Connect\nProxy-to-Proxy| TaskB2
TaskB1 <-->|Service Connect\nProxy-to-Proxy| TaskA2
TaskB1 <-->|Service Connect\nProxy-to-Proxy| TaskB2
TaskA1 -.->|auto-register| NS1
TaskB1 -.->|auto-register| NS1
TaskA2 -.->|auto-register| NS2
TaskB2 -.->|auto-register| NS2
アーキテクチャ的には、各タスクに自動的にサイドカープロキシ(AWS管理のEnvoyベース)がインジェクトされ、サービス間通信はこのプロキシを経由する。Cloud Map Namespaceは自動的に使われるが、ユーザーが個別のVirtualNodeを定義する必要はない。
App Mesh との比較
個人的に一番刺さったのは「サイドカー管理」の行だ。App Meshの頃はEnvoyのバージョンアップのたびにタスク定義を更新していたので、それが丸ごと消えたのは地味に嬉しかった。
| 項目 | App Mesh | Service Connect |
|---|---|---|
| サイドカー管理 | 手動(ECR Pull必要) | AWS管理(自動更新) |
| 設定量 | 多い(VirtualNode等) | 少ない(タスク定義のみ) |
| 対応プロトコル | HTTP/HTTP2/gRPC/TCP | HTTP/HTTP2/gRPC(2026時点) |
| メトリクス | CloudWatch + X-Ray | CloudWatch(標準) |
| カスタムポリシー | Envoy設定で柔軟に | 制限あり |
| マルチクラスター | 対応 | ECSクラスター内のみ |
| EKS対応 | 対応 | 非対応 |
| 運用コスト | 高い | 低い |
App Meshはまだ廃止されていないけど、AWSの雰囲気的にService Connectへの誘導が強くなっている印象がある(正直まだ公式の廃止予告はないけど)。カスタムEnvoyフィルターが必要なケースや、EKSと混在する環境ではApp Meshの選択が現実的だと思う。うちはECS-onlyだったので移行を選んだ。
実際の設定と落とし穴
移行時にまず困ったのが、Service Connectの設定項目の「意味」が直感的にわかりにくいこと。公式ドキュメントを読んでも、clientAlias と portName の関係が最初よくわからなかった。
実際に動かしたタスク定義の抜粋を書く。タスク定義側では portMappings に name と appProtocol を追加するだけ。
{
"family": "order-service",
"containerDefinitions": [
{
"name": "order-service",
"image": "123456789.dkr.ecr.ap-northeast-1.amazonaws.com/order-service:latest",
"portMappings": [
{
"name": "order-http",
"containerPort": 8080,
"protocol": "tcp",
"appProtocol": "http"
}
]
}
],
"networkMode": "awsvpc"
}
次にECSサービス側の設定:
{
"serviceConnectConfiguration": {
"enabled": true,
"namespace": "myapp.internal",
"services": [
{
"portName": "order-http",
"clientAliases": [
{
"port": 8080,
"dnsName": "order-service"
}
],
"discoveryName": "order-service"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/service-connect",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "sc"
}
}
}
}
これで、api-service から http://order-service:8080 というホスト名でHTTPアクセスできるようになる。App Meshでこれをやるには何十行ものTerraformが必要だったことを思うと、正直笑えてくる。
落とし穴1: タスク起動順序
Service Connect有効化後、タスクの起動ログに見慣れないコンテナが1つ追加されていることに気づく。ecs-service-connect-agent という名前のサイドカーだ。問題は、このサイドカーの起動が終わる前にアプリコンテナが接続しようとするとエラーになること。
[ORDER-SERVICE] Caused by: Connection refused to order-service:8080
移行直後にたまに発生したエラーの原因はこれだった。対処としては、アプリ側にリトライロジックを入れるか、コンテナの dependsOn を設定する:
"dependsOn": [
{
"containerName": "ecs-service-connect-agent",
"condition": "HEALTHY"
}
]
ただしサイドカー名は固定ではなくAWSが管理するため、ecs-service-connect-agent という名前でdependsOnを書くと実際には動かないケースがある(2026年5月時点で確認)。現実的な解決策はアプリ側のリトライだと思う。
落とし穴2: ヘルスチェックが厳しくなる
Service Connect経由の通信はプロキシが健全性をチェックするため、タスク定義のヘルスチェックが設定されていないサービスへのアクセスが不安定になった。具体的には、新しいタスクが起動してすぐにトラフィックを受け取るものの、コンテナが完全に準備できていないタイミングでエラーが出る。
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 10,
"timeout": 5,
"retries": 3,
"startPeriod": 30
}
startPeriod を少し長めに設定することで解消した。インシデント対応のベストプラクティスの観点からも、起動時の不安定さは事前に潰しておくのが大事だと改めて実感した。
落とし穴3: Namespaceは事前作成が必要
「Service Connect使えばCloud Mapを意識しなくていい」と思っていたが、Namespace自体は事前にCloud Mapで作成しておく必要がある。Terraformだと:
resource "aws_service_discovery_http_namespace" "app" {
name = "myapp.internal"
description = "Service Connect namespace for myapp"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
ここで aws_service_discovery_http_namespace と aws_service_discovery_private_dns_namespace を間違えると動かない。Service ConnectにはHTTP Namespaceが必要で、Private DNS Namespaceではない。うちは最初ここで詰まって、30分くらい溶かした。ドキュメントをちゃんと読めという話なんだけど。
6ヶ月運用してわかった、メトリクスの使い方
Service Connectの地味に便利なポイントは、サイドカーが自動でCloudWatchメトリクスを送ってくれること。追加のX-Ray設定や、Datadogエージェントを別途立てなくても、最低限のサービス間通信の状態が見える。これが「ゼロ設定で動く」という意味の本体だと思う。
取得できる主なメトリクスは以下の3種類だ。
| メトリクス名 | 内容 |
|---|---|
TargetResponseTime | サービス間のレスポンスタイム |
RequestCount | リクエスト数 |
HTTPCode_Target_2XX_Count / 4XX / 5XX | ステータスコード別カウント |
xychart-beta
title "Service Connect: order-service レスポンスタイム推移(ms)"
x-axis [Jan, Feb, Mar, Apr, May]
y-axis "Response Time (ms)" 0 --> 500
line [320, 290, 270, 240, 210]
bar [320, 290, 270, 240, 210]
移行直後の1月がやや高めなのはチューニング前だからで、その後は地道に改善できている。App Mesh時代はここまで簡単にメトリクスが取れなかったので、これは本当に助かった。
CloudWatchのInsights Queryでサービス別のエラー率をダッシュボード化した例:
fields @timestamp, @message
| filter @logStream like /sc/
| filter @message like /5XX/
| stats count() as error_count by bin(5m)
| sort @timestamp desc
SLO設計の観点でも、この可視性があるだけでかなり楽になった。SLO設計の話で書いた「測定できないものはSLOにできない」という教訓が、ここで活きてくる。
App MeshからService Connectへの移行手順
移行は「一気に切り替え」ではなく、サービスごとに段階的に行った。ダウンタイムなしで移行できたのはこの順序のおかげだと思う。
sequenceDiagram
participant Ops as 運用チーム
participant ECS as ECS Cluster
participant AppMesh as App Mesh
participant SC as Service Connect
Ops->>ECS: 新Namespaceを作成(HTTP)
Ops->>ECS: 非クリティカルサービスをSCで再デプロイ
ECS->>SC: サイドカーが自動インジェクト
Ops->>ECS: 動作確認・メトリクス検証(1週間)
Ops->>ECS: 残りサービスを順次SC移行
Ops->>AppMesh: VirtualNode等のリソース削除
AppMesh-->>Ops: 削除完了
Note over Ops,SC: 移行期間: 約3週間
移行期間中は、App MeshとService Connectが共存する状態になる。App Mesh側のリソースを残しつつ、Service Connect経由での通信が安定しているか確認した。共存期間が3週間になったのは想定より長かったけど、段階移行でインシデントが0だったのは結果オーライだったと思っている。
移行チェックリスト
移行前に必ず確認してほしい項目をまとめた。特にIAMとNamespace種別の2点は、ここで詰まるチームが多い印象がある。
□ Cloud Map HTTP Namespaceの事前作成
□ タスク定義のportMappingsにname・appProtocolを追加
□ ヘルスチェックの設定(startPeriod含む)
□ アプリ側のリトライロジック確認
□ Service Connectのログ設定(CloudWatch)
□ IAMロールに CloudMapFullAccess OR 最小権限ポリシーを付与
□ タスク実行ロールに以下のアクションを追加:
- servicediscovery:RegisterInstance
- servicediscovery:DeregisterInstance
- route53:GetHealthCheck
IAMは意外と詰まりポイントで、特に route53:GetHealthCheck が抜けると謎のエラーが出ることがある。エラーメッセージが分かりにくいので、事前に設定しておくのが無難だ。
正直、向いていないケースも話す
Service Connectを推しすぎるのも良くないので、実際に「向いていない」と感じた部分も書いておく。
1. ECSとEKSが混在する環境
うちはECS-onlyだから問題なかったけど、EKSとECSが混在していてサービス間通信が必要な場合、Service Connectは使えない。その場合はApp MeshかIstioを使うことになる。EKSのコスト最適化記事でも触れたけど、EKSとECSの混在はそれ自体がアーキテクチャ的な課題を生みやすい。
2. カスタムEnvoyフィルターが必要なケース
Rate Limitingをサービスメッシュレベルで実装したい場合、Service ConnectのプロキシはAWS管理なのでカスタムフィルターを追加できない。アプリ側でレート制限を実装するか、API Gatewayで対応する必要がある。API Rate Limitingの実装パターンの話とも繋がるけど、レイヤーの選択は要件次第だと思う。
3. gRPCのストリーミング
2026年5月時点でも、gRPCの双方向ストリーミングはService Connectで完全にはサポートされていない(Unary RPCとServer Streaming RPCは動作確認済み)。gRPCをヘビーに使うサービスはまだ検証が必要だと感じている。
pie title Service Connect 適用サービス内訳(自社ケース)
"HTTP REST(問題なし)" : 65
"gRPC Unary(問題なし)" : 20
"gRPC Streaming(App Mesh継続)" : 10
"TCP(Service Connect外)" : 5
うちのケースでは、gRPCストリーミングを使う1サービスだけApp Meshに残して、残りをService Connectに移行した。完全移行できなかったのは少し悔しいけど、無理に統一するより現実的な落とし所だと思っている。
まとめ
6ヶ月運用してみた感想として、ECS Service Connectは「サービスメッシュの複雑さを受け入れたくないチーム」にとってかなり良い選択肢だ。移行コストは1ヶ月かかったけど、その後の運用が楽になったのでトータルでは間違いなく正解だったと思ってる。
| ポイント | 内容 |
|---|---|
| 設定量が劇的に少ない | タスク定義とサービス定義の変更だけで動く。App Meshの1/10くらいのコード量 |
| サイドカー管理が不要 | AWSがバージョンアップを管理してくれるので、Envoyの脆弱性対応が楽になった |
| メトリクスが自動で取れる | CloudWatch統合で、最低限のサービス間通信の可視化がすぐできる |
| 落とし穴は事前に潰せる | 起動順序・ヘルスチェック・IAM・Namespace種別の4点を事前確認すれば詰まらない |
| 限界もある | gRPCストリーミング・カスタムEnvoyフィルター・EKS混在は非対応 |
次のアクション:
- まずは開発環境の1サービスでService Connectを有効化して動作確認
serviceConnectConfigurationのlogConfigurationを必ず設定(これがないとデバッグがつらい)- CloudWatchに
TargetResponseTimeのアラームを設定して、移行前後のベースラインを比較する
皆さんのチームはサービス間通信どうやって解決してますか?まだApp Mesh使ってるという人、移行を検討しているなら参考にしてもらえると嬉しい。