App Mesh + Cloud Mapを1年本番運用して分かった「しんどかった話」【EKS実録】
「App MeshとCloud Mapって両方いるの?」—社内でも聞かれた疑問、ありませんか。EKSで12本のマイクロサービスを1年以上回して気づいた、便利な点と正直しんどかった落とし穴を書きました。
先日、社内のポストモーテムで「App Meshの設定ミスによるトラフィックルーティング障害」を発表したら、「そもそもApp MeshとCloud Mapって何が違うの?」という質問が複数のエンジニアから来た。そうか、まだここを整理できていないチームが多いんだな、と思って記事にすることにした。
うちのチームは2025年初頭からEKS上でApp Mesh + Cloud Mapを本番運用しており、今ではマイクロサービス12本がこの構成で動いている。最初は「Istioでよくね?」という話もあったけど、AWSネイティブの運用コスト・IAM統合・CloudWatchとの親和性を考えたときに、App Meshを選んだ。1年以上運用した今、正直「ここは良かった」「ここはしんどかった」が両方ある。その実体験を共有したい。
App MeshとCloud Mapの役割分担を整理する
まず一番混乱しやすいところから片付けよう。「App MeshとCloud Mapって両方いるの?」という疑問、最初に自分も持っていた。
Cloud Map は純粋なサービスディスカバリ。「このサービスはどこにいるか(IPアドレス・ポート)」を解決するためのものだ。EKS Podが起動するとCloud Mapに自動登録され、他のサービスがDNSや直接API呼び出しで場所を解決できる。
App Mesh はサービスメッシュ。L7レベルのトラフィック制御(カナリアリリース・ヘッダーベースルーティング)、相互TLS、タイムアウト・リトライ設定、そしてEnvoyサイドカーによるオブザーバビリティを提供する。内部的にはCloud Mapと連携して「解決済みのエンドポイント」を受け取り、その上でトラフィックポリシーを適用する。
App Mesh(L7トラフィック制御) ← Cloud Map(サービス解決)
両者は補完関係にあって、App Meshだけでもサービスメッシュは成立するけど、Cloud Mapと統合することでDNSベースのサービスディスカバリが滑らかになる。EKS CoreDNS + Cloud Mapを組み合わせると、service-name.namespace.local という形でPod間通信が解決される。これが地味に便利で、導入前は手動で管理していたエンドポイント情報がほぼ意識しなくてよくなった。
実際の構成図:うちのチームのEKS + App Mesh環境
graph TB
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ_A["Availability Zone A"]
subgraph Private_A["Private Subnet A"]
PodA1["Pod: frontend-v1"]
PodA2["Pod: order-service"]
PodA3["Pod: user-service"]
end
end
subgraph AZ_B["Availability Zone B"]
subgraph Private_B["Private Subnet B"]
PodB1["Pod: frontend-v2 (canary)"]
PodB2["Pod: order-service"]
PodB3["Pod: inventory-service"]
end
end
subgraph Mesh_Control["App Mesh Control Plane"]
MeshCP["AWS App Mesh\n(Virtual Nodes / Routers)"]
end
subgraph ServiceDiscovery["Cloud Map"]
NS1["Namespace:\napp.local"]
SVC1["Service: frontend"]
SVC2["Service: order-service"]
SVC3["Service: user-service"]
SVC4["Service: inventory-service"]
end
subgraph Observability["Observability"]
CW["CloudWatch\n(Container Insights)"]
XRAY["AWS X-Ray"]
end
subgraph Ingress["Ingress"]
ALB["Application Load Balancer"]
APIGW["API Gateway"]
end
end
subgraph External["External"]
Client["Client"]
end
Client --> ALB
ALB --> PodA1
ALB --> PodB1
PodA1 -->|"Envoy Sidecar"| MeshCP
PodB1 -->|"Envoy Sidecar"| MeshCP
MeshCP -->|"Traffic Policy"| NS1
NS1 --> SVC1
NS1 --> SVC2
NS1 --> SVC3
NS1 --> SVC4
SVC2 --> PodA2
SVC2 --> PodB2
PodA1 --> XRAY
PodA2 --> XRAY
XRAY --> CW
MeshCP --> CW
この構成でポイントになるのが、Virtual NodeがCloud Mapのサービスに紐づいているという点。Virtual Nodeの定義でCloud Mapのnamespaceとservice nameを指定することで、Envoyが自動的にエンドポイントを解決してくれる。後述するが、ここの設定ミスが一番ハマりやすい。ポストモーテムのネタもまさにここだった。
実装:Virtual Node / Virtual Router / Virtual Serviceの設定例
実際にどう設定するか見ていこう。KubernetesのCRDとして宣言的に定義できるのが、個人的にはかなり気に入っているポイントだ。
Cloud Map名前空間の作成
aws servicediscovery create-private-dns-namespace \
--name app.local \
--vpc vpc-xxxxxxxxxx \
--region ap-northeast-1
Virtual Node(Cloud Map統合)
apiVersion: appmesh.k8s.aws/v1beta2
kind: VirtualNode
metadata:
name: order-service
namespace: production
spec:
meshRef:
name: production-mesh
listeners:
- portMapping:
port: 8080
protocol: http
healthCheck:
protocol: http
path: /health
healthyThreshold: 2
unhealthyThreshold: 3
timeoutMillis: 2000
intervalMillis: 5000
serviceDiscovery:
awsCloudMap:
namespaceName: app.local
serviceName: order-service
attributes:
- key: ECS_TASK_DEFINITION_FAMILY
value: order-service
backends:
- virtualService:
virtualServiceRef:
name: inventory-service
logging:
accessLog:
file:
path: /dev/stdout
Cloud Mapとの統合は serviceDiscovery.awsCloudMap ブロックで指定する。最初は dns ベースで試していたんだけど、Cloud Mapとの統合の方がヘルスチェックとの連携が断然安定していた。dns だとTTLの問題でPod入れ替えのタイミングにリクエストが落ちるケースが何度かあって、うちのチームはCloud Map統合に全面統一している。
Virtual Router(カナリアリリース設定)
apiVersion: appmesh.k8s.aws/v1beta2
kind: VirtualRouter
metadata:
name: frontend-router
namespace: production
spec:
meshRef:
name: production-mesh
listeners:
- portMapping:
port: 8080
protocol: http
routes:
- name: frontend-route
httpRoute:
match:
prefix: /
action:
weightedTargets:
- virtualNodeRef:
name: frontend-v1
weight: 90
- virtualNodeRef:
name: frontend-v2
weight: 10
retryPolicy:
httpRetryEvents:
- server-error
- gateway-error
maxRetries: 3
perRetryTimeout:
value: 2
unit: s
timeout:
perRequest:
value: 10
unit: s
このweight設定で90/10のカナリアリリースを実現している。Argo Rolloutsと組み合わせるとウェイトの段階的な移行を自動化できるんだけど、それはまた別の話。正直、このyamlを毎回手で書くのはつらいので、Helm Chartに切り出してパラメータ化している。
コントローラーのインストール(2026年現在)
# App Mesh Controller(v1.13以降でEKS 1.29+対応)
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm upgrade --install appmesh-controller eks/appmesh-controller \
--namespace appmesh-system \
--create-namespace \
--set region=ap-northeast-1 \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::ACCOUNT_ID:role/AppMeshControllerRole \
--set tracing.enabled=true \
--set tracing.provider=x-ray \
--version 1.13.1
2026年時点ではApp Mesh ControllerはEKS Add-onとしても提供されているけど、カスタム設定の柔軟性を考えてHelmで管理している。EKS Auto Modeとの組み合わせはEKS Auto Mode完全ガイド2026でも触れているので参考にしてほしい。
1年運用してわかったハマりポイントと対処法
ここからが本番の話。きれいな公式ドキュメントには書いていない、泥臭い実体験を共有したい。
ハマり1:Envoyサイドカーのメモリ消費
マイクロサービスが増えるにつれて、Envoyサイドカーが一番のメモリ圧迫要因になった。各Podにサイドカーが注入されるわけで、12サービス×平均3レプリカ=36コンテナ分のEnvoyが動く。最初はデフォルト設定のままで動かしていたら、Node全体のメモリが圧迫されてKarpenterが予想外のスケールアウトを始めた。コスト請求書を見たときのあの気持ち、思い出したくない。
# Envoyのリソース制限(実際に設定した値)
annotations:
appmesh.k8s.aws/sidecarMemory: "128Mi"
appmesh.k8s.aws/sidecarMemoryLimit: "256Mi"
appmesh.k8s.aws/sidecarCpu: "50m"
appmesh.k8s.aws/sidecarCpuLimit: "200m"
サービスのトラフィック量に応じてここを調整する必要がある。高トラフィックなAPIゲートウェイ側のPodは256Mi/512Mi、内部間通信しかしないバックエンドは64Mi/128Miくらいに落とした。EKSコスト最適化と組み合わせて考えると、ここの最適化は地味に効く。
ハマり2:Virtual NodeとCloud Mapの同期ズレ
Podが急スケールしたとき(Karpenterで30秒以内にPodが立つ)、Cloud MapへのエンドポイントのPropagateが遅れてEnvoyが古いエンドポイント情報を持ち続けるケースがあった。具体的には新しいPodへのルーティングが数秒〜10秒ほど遅延する。スケールアウトが完了しているはずなのに502が飛んでくる、あの不思議な現象の正体はこれだった。
# Cloud Mapの登録状況をリアルタイム確認
aws servicediscovery discover-instances \
--namespace-name app.local \
--service-name order-service \
--query 'Instances[*].{Id:InstanceId,IP:Attributes.AWS_INSTANCE_IPV4,Health:HealthStatus}' \
--output table
これを定期実行するスクリプトをデプロイパイプラインに組み込んで、「期待する台数がhealthyになるまで待つ」という待機処理を入れた。地味だけど、これでカナリアリリース中の502エラーが激減した。
ハマり3:相互TLS(mTLS)の証明書管理
最初にmTLSを全サービス間で有効化しようとしたら、ACM Private CAのコストが思ったより高くなった。証明書のローテーション間隔がデフォルトで24時間なのを変えずに、サービスが50本になったときのことを試算したら月数万円の追加コストになる計算だった。
| 設定項目 | デフォルト値 | うちの設定 | 理由 |
|---|---|---|---|
| 証明書有効期間 | 24時間 | 72時間 | ACMコスト削減 |
| ローテーション開始 | 残り12時間 | 残り24時間 | バッファ確保 |
| TLSモード | STRICT | PERMISSIVE→STRICT段階移行 | 移行リスク軽減 |
| CA階層 | Root CA | Subordinate CA | Root CAを守る |
本番の内部通信はPERMISSIVEで動かしながら、サービスごとに順番にSTRICTに切り替えていく段階移行を選んだ。一気にSTRICTにしたら証明書エラーで全サービスが死ぬリスクがあったので、慎重にいった。この辺のセキュリティ設定はコンテナセキュリティ完全ガイド2026も参考になる。
ハマり4:X-Rayトレーシングのサンプリングレート
X-Rayと統合してトレーシングを入れたのはいいものの、デフォルトの5%サンプリングでは問題のあるリクエストが「サンプリング対象外」になることが多くて、インシデント時に証跡が取れないことがあった。かといって100%にするとコストが跳ね上がる。結局こういう設定に落ち着いた。
// X-Ray サンプリングルール(コンソールまたはTerraformで設定)
{
"version": 2,
"rules": [
{
"description": "エラーレスポンスは必ずサンプリング",
"host": "*",
"http_method": "*",
"url_path": "*",
"fixed_target": 1,
"rate": 1.0,
"attributes": {
"http.response.status": "5*"
}
},
{
"description": "ヘルスチェックは除外",
"host": "*",
"http_method": "GET",
"url_path": "/health",
"fixed_target": 0,
"rate": 0.0
},
{
"description": "通常リクエスト 10%",
"host": "*",
"http_method": "*",
"url_path": "*",
"fixed_target": 1,
"rate": 0.1
}
]
}
5xxエラーは100%サンプリング、ヘルスチェックは除外、通常リクエストは10%。これでコストを抑えながら障害時の証跡も確保できるようになった。インシデント対応の観点ではインシデント対応の最新ベストプラクティス2026も合わせて読んでほしい。
パフォーマンスへの影響:実測値
「Envoyサイドカーが入るとレイテンシが上がるんじゃ?」という懸念、導入前も後も聞かれる。実際に測定した結果がこちら。
xychart-beta
title "サービス間通信レイテンシ比較(p99, ms)"
x-axis ["order→inventory", "user→auth", "frontend→order", "order→payment"]
y-axis "レイテンシ (ms)" 0 --> 60
bar [12, 8, 18, 22]
line [15, 10, 21, 26]
棒グラフがApp Mesh導入前、折れ線がApp Mesh導入後(Envoyサイドカーあり)。p99で見ると2〜4ms程度のオーバーヘッドが追加されている。正直このくらいであれば許容範囲で、得られるオブザーバビリティや制御性と天秤にかけると十分ペイすると思っている。
p50だともっと小さくて0.5〜1ms程度。問題が出るのは高頻度に呼ばれる内部API(1秒に1000回以上)くらいで、そういうサービスはサイドカーのCPUをちゃんと確保する必要がある。
2026年時点でのApp Mesh vs Istioの選択基準
「結局Istioじゃなくてよかったの?」という話。これは正直まだ答えが出ていない部分もある。
| 比較軸 | App Mesh | Istio |
|---|---|---|
| AWSサービス統合 | ◎ ネイティブ統合 | △ カスタム設定が必要 |
| IAM認証統合 | ◎ IRSA対応 | △ 別途管理が必要 |
| 運用コスト | ○ マネージドコントロールプレーン | △ 自前管理 |
| トラフィック制御の柔軟性 | ○ 基本機能は充実 | ◎ 細かい制御が可能 |
| マルチクラスタ対応 | △ 同一AWS内が前提 | ◎ ハイブリッド対応 |
| コミュニティ・情報量 | △ Istioに比べると少ない | ◎ 豊富 |
| Envoy設定の自由度 | △ 制限あり | ◎ フル制御 |
| AWSコスト | 無料(Envoyのコンピュート費のみ) | 無料(同様) |
うちのチームの場合、AWSシングルクラウドで運用していて、EKSのIRSA・CloudWatch・X-Rayとの統合を重視したのでApp Meshを選んだ。マルチクラウドやオンプレ連携が必要なケースはマルチクラウド戦略2026を参照してほしいけど、その場合はIstioの方が向いているかもしれない。
ただ、2026年時点での懸念点として、App MeshのロードマップのアップデートペースがIstioに比べて遅いという実感がある。特にGateway APIサポートはIstioの方が先を走っていて、うちのチームでも「将来的にIstioへ移行する可能性」はゼロではない状態だ。ここは好みが分かれるというか、組織の状況次第だと思う。皆さんのチームはどうしてます?
まとめ
1年以上App Mesh + Cloud Mapを本番運用してきた知見を整理するとこうなる。
良かった点
- AWSネイティブの統合(X-Ray・CloudWatch・IAM)が素直に動く
- Cloud Map + Virtual Nodeの組み合わせで、DNSのTTL問題なくサービスディスカバリが安定
- カナリアリリースやリトライポリシーをyamlで宣言的に管理できる
注意点
- Envoyサイドカーのリソース設計を最初に丁寧にやること
- Cloud MapへのPropagateにラグがあるのでデプロイパイプラインに待機処理を入れる
- mTLSはPERMISSIVEから段階的にSTRICTへ移行する
まだ検証中な部分
- Gateway API(Kubernetes SIG Network標準)への対応状況を継続ウォッチ中
- Istioへの将来的な移行コストの試算
次のアクション
- まず単一サービスでApp Meshのサイドカー注入とX-Rayトレーシングを試す
- Cloud Mapのヘルスチェック設定を念入りに確認してからVirtual Nodeを定義する
- Envoyのメモリ・CPUはデプロイ後1週間分のメトリクスを見て調整する
サービスメッシュは「入れればOK」じゃなくて、入れてからの運用設計が全てだと思っている。これから導入を検討しているチームの参考になれば嬉しい。