📌 結論ファースト
mTLSでクライアント・サーバー間の双方向認証が必要な場合、NLB+TCPリスナーが唯一の正解です。
TCPリスナーはパケットを復号化せずにパススルーするため、TLSハンドシェイクがクライアント〜Pod間で直接行われ、
相互認証と暗号化が途切れません。
🔒
NLB + TCPリスナー
レイヤー4で動作。暗号化パケットを開封せずそのままPodへ転送。mTLSが正しく機能する
☸️
AWS LB Controller + IP mode
Pod IPに直接ルーティングすることで、kube-proxyを経由せず低レイテンシーな通信を実現
📈
HPA + Cluster Autoscaler
gRPC接続数やCPU使用率に応じてPod→ノードと二段階で自動スケール
🏛️ たとえ話:「外交機密便」の仕組み
mTLS + NLB の仕組みを「国家間の外交機密便ネットワーク」で理解しよう
👨💼
クライアント(gRPCアプリ)
↕ たとえると
外交官 — 封印付き外交パスポートで身元を証明
🏛️
バックエンドPod(gRPCサーバー)
↕ たとえると
大使館 — 国章の入った公印で大使館の正当性を証明
📦
TLS暗号化パケット
↕ たとえると
封蝋付き外交行嚢(ポーチ)— 中身は開封不可
🚏
NLB(TCPリスナー)
↕ たとえると
国際郵便の仕分けゲート — 宛先を見て転送するだけで中身は開けない
🎛️
AWS LB Controller
↕ たとえると
郵便システムの管理局 — ゲートのルール設定と大使館の住所登録を担当
📋
CA(認証局)
↕ たとえると
外務省 — 外交パスポートと公印を発行する信頼の根拠
📬 外交機密便の配達フロー = mTLS通信フロー
👨💼
外交官
外交パスポートを提示して身元を証明(=クライアント証明書)
🤝
相互確認
外交官と大使館が互いの証明書を確認(=mTLSハンドシェイク)
🚏
仕分けゲート
宛先だけ見て転送。封蝋は開けない(=NLB TCPパススルー)
🗺️
直接配達
大使館の部屋に直行(=IP modeでPodへ直接ルーティング)
🏛️
大使館
公印で自身を証明し封蝋を開封(=Podがサーバー証明書を返す)
🎯 5つの要件 → 各技術がどう解決するか
🔒
E2E暗号化
復号化なしで通信を維持
→ NLB TCPリスナー
📡
gRPC on 443
HTTP/2ベースのRPC通信
→ TCP:443 パススルー
🔥
数千の同時接続
高スループット処理
→ NLBの高性能L4処理
🤝
mTLS認証
双方向の証明書認証
→ TCPパススルー + Pod
📈
自動スケーリング
負荷に応じた伸縮
→ HPA + Cluster AS
✅ すべてを満たす構成 = NLB + TCPリスナー + AWS LB Controller(IP mode)+ HPA + Cluster Autoscaler
この組み合わせだけが5つの要件をすべて同時に満たすことができる
🔄 mTLS 7ステップ・ハンドシェイク
1
ClientHello を送信(対応する暗号スイート一覧を含む)
→
2
←
ServerHello + サーバー証明書を返送
3
クライアントがサーバー証明書を検証(CAチェーンを確認)
🔍
4
📋
サーバーがCertificateRequestを送信(クライアント証明書を要求)
6
🔍
サーバーがクライアント証明書を検証(CAチェーンを確認)
7
✅ 双方の検証成功 → 暗号化通信チャネルが確立
🔐
⚠️
重要ポイント:mTLSのハンドシェイクはクライアントとサーバー間で直接行われる必要があります。
途中のロードバランサーがTLSを終端してしまうと、サーバーが受け取る証明書はLBのものになり、
クライアント証明書の検証ができなくなります。だからこそTCPパススルーが必要です。
📡 gRPC = Google が開発した高速 RPC フレームワーク
gRPC は Remote Procedure Call(遠隔手続き呼び出し)の仕組みです。
離れた場所にあるサーバーの関数を、まるでローカルの関数を呼ぶように実行できます。
📻
外交機密便のたとえでは、gRPC は「外交官と大使館が使う共通言語・暗号通信回線」にあたります。
普通の手紙(REST API / HTTP/1.1)は1通ずつ送って返事を待ちますが、
gRPC の暗号通信回線(HTTP/2)は1本の回線で複数の会話を同時進行でき、
さらにバイナリ形式(protobuf)で高速にデータをやり取りします。
HTTP/2 の多重化(Multiplexing)により、1つの TCP 接続で複数のリクエスト・レスポンスを同時にやり取りできます。
これが「数千の同時接続」に強い理由です。
1本の接続 → 同時に複数の RPC を処理
= 接続のオーバーヘッドが劇的に減少
JSON のようなテキスト形式ではなく、バイナリ形式でデータをシリアライズします。
データサイズが小さく、シリアライズ/デシリアライズが高速で、低レイテンシーの通信に最適です。
JSON: {"name":"Alice","age":30} → 約30バイト
protobuf: 同じデータ → 約10バイト 🚀
クライアント→サーバー、サーバー→クライアントの両方向で連続的にデータをストリーム送受信できます。
リアルタイム通信やマイクロサービス間連携に最適です。
📱 ← ストリーム → 🏛️
リアルタイムで双方向にデータ交換
📮 REST API(HTTP/1.1)
データ形式
JSON(テキスト)— 人間が読みやすいが重い
通信方式
リクエスト→レスポンスの1往復ずつ
接続管理
リクエストごとに接続 or Keep-Alive
ストリーム
❌ 非対応(WebSocket は別途必要)
向いている
ブラウザ向け API、シンプルな CRUD
VS
📡 gRPC(HTTP/2)
データ形式
Protocol Buffers(バイナリ)— 小さくて速い
通信方式
1接続で複数のRPCを同時多重化
接続管理
長時間接続を維持(Long-lived)
ストリーム
✅ 双方向ストリーミング標準サポート
向いている
マイクロサービス間通信、低レイテンシー
🔌 HTTP/1.1 vs HTTP/2 — 接続の違いを図で理解する
HTTP/1.1(REST)— 順番待ち
⏳ 1つずつ順番に処理 → 待ち時間が発生
HTTP/2(gRPC)— 同時並行
⚡ すべて同時に処理 → 待ち時間なし
⚠️
今回の構成で重要なポイント:gRPC は HTTP/2 の長時間接続(Long-lived connection)を使います。
NLB の TCP リスナーは接続単位で負荷分散するため、一度確立された gRPC 接続は同じ Pod に固定されます。
これがトラブルシューティング(セクション8)で触れる「gRPC 接続の偏り」問題の根本原因であり、
クライアント側で定期的に再接続する設計が必要になる理由です。
🔒
TLS終端しない — パケットをそのまま転送
🤝
mTLS対応 — クライアント⇔Pod間で直接ハンドシェイク
⚡
超低レイテンシー — 復号化/再暗号化なし
📡
gRPC対応 — TCP上でHTTP/2をそのまま通過
🔥
数百万接続 — 高スループットに最適化
✅ mTLSに最適
🔓
NLBでTLS終端 — 一旦復号化される
❌
mTLS不可 — クライアント証明書がNLBで途切れる
🔄
再暗号化可能 — バックエンドへの再暗号化は可能
📜
ACM統合 — AWS Certificate Managerで証明書管理
⚙️
TLSオフロードでバックエンドの負荷軽減
❌ mTLS不可
🔓
HTTPS終端必須 — コンテンツ検査のため復号化
❌
mTLS不可 — クライアント証明書が届かない
🛣️
高度なルーティング — パスベース/ホストベース
📡
gRPC対応 — ALBはgRPCをネイティブサポート
🔄
WebSocket、HTTP/2もサポート
❌ mTLS不可
💡
判定基準のまとめ:TLSリスナーやHTTPSリスナーは、ロードバランサー側でTLSを「終端」します。
すると、クライアント証明書の情報がPodまで届かないため、mTLSが成り立ちません。
TCPリスナーだけが「触らずに転送」できるため、mTLSの唯一の選択肢です。
🎛️ そもそも何をしてくれるもの?
AWS Load Balancer Controller は、Kubernetes 上で動作するコントローラーで、
YAML を書くだけで NLB/ALB の作成・設定・ターゲット管理をすべて自動化してくれます。
🏛️
外交機密便のたとえでは「郵便システムの管理局」にあたります。
仕分けゲート(NLB)のルール設定、大使館(Pod)の住所登録・削除を自動で行う管理者です。
管理局がなければ、ゲートの設置も大使館の住所変更もすべて手作業になってしまいます。
Kubernetes の Service マニフェスト(YAML) にアノテーションを書くだけで、
AWS 側で NLB のプロビジョニング、リスナー設定、セキュリティグループの構成まですべて自動実行されます。
手動で AWS コンソールや CLI を操作する必要はありません。
📝 YAML を kubectl apply
→ 🎛️ Controller が検知
→ 🚏 NLB が自動作成される
HPA で Pod が増えたら即座に NLB のターゲットグループに追加し、
Pod が消えたら自動で除外します。kube-proxy を経由せず Pod に直接トラフィックが届くため、
低レイテンシーを実現できます。
📈 HPA が Pod を追加
→ 🎛️ Controller が検知
→ 🎯 NLB ターゲットに自動登録
❌ Controller がない場合
3
Pod が増減するたびに手動でターゲット登録・削除
4
HPA / CA と連携できず、スケール時にダウンタイム発生
5
Kubernetes と AWS の設定が乖離するリスク
✅ Controller がある場合
1
YAML にアノテーションを書いて kubectl apply するだけ
2
NLB / リスナー / ターゲットグループが自動作成
4
HPA / CA と完全連携し、シームレスにスケール
5
Kubernetes マニフェストが「唯一の信頼源」(Single Source of Truth)
🔄 Controller が裏側で行う自動連携フロー
📝
YAML apply
Service マニフェストを
クラスターに適用
🎛️
Controller 検知
アノテーションを読み取り
NLB 設定を決定
🚏
NLB 自動作成
AWS API を呼び出し
NLB + TCPリスナー構築
🎯
ターゲット登録
Pod IP を
ターゲットグループに登録
🔒
トラフィック開始
mTLS 暗号化トラフィックが
Pod へ直接到達
💡
mTLS 構成での重要性:Controller がいるからこそ「Pod が増える → 自動で NLB に登録 → すぐトラフィック受信」
という流れがシームレスに動きます。オートスケーリングとの連携が実現するのは、この Controller のおかげです。
🏗️ NLB + TCPリスナー + AWS LB Controller + EKS 構成図
外部
💻
gRPCクライアント
クライアント証明書を保持
⬇️
TCP:443 / TLS暗号化パケット(封蝋付き外交ポーチ)
AWS
ネットワーク
🚏
Network Load Balancer
TCPリスナー(ポート443)
パケットを復号化しない = パススルー
⬇️
暗号化パケットそのまま転送(IP mode: Pod IPへ直接)
EKSクラスター
🎛️
AWS LB Controller
NLBのプロビジョニング
& TargetGroup管理
🏛️
gRPC Pod A
サーバー証明書 + mTLS処理
🏛️
gRPC Pod B
サーバー証明書 + mTLS処理
🖥️
Cluster Autoscaler
ノード数を自動調整
🔐 暗号化状態の可視化:パケットはどこで復号化される?
🔒 クライアント → NLB → Pod:全経路で暗号化が維持される(エンドツーエンド暗号化) 🔒
apiVersion: v1
kind: Service
metadata:
name: grpc-mtls-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "TCP"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "443"
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: grpc-server
ports:
- port: 443
targetPort: 8443
protocol: TCP
❶ NLBタイプの指定
"external" を指定するとAWS LB Controllerが NLB を作成。"nlb-ip" 旧構文も可
❷ IP モード
Pod IPを直接ターゲットに登録。kube-proxyを経由せずPodに到達。低レイテンシーの鍵
❸ TCPプロトコル = パススルー
最重要設定。これによりNLBはTLSパケットを開封せずそのまま転送。mTLSが機能する根拠
❹ ヘルスチェック
TCPヘルスチェックはポートの応答のみ確認。TLS終端不要なのでHTTPSヘルスチェックは使わない
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: grpc-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: grpc-server
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
CPU使用率 70% 超過 → Pod を追加
↓ スケールアウト ↓
監視メトリクス:CPU使用率、メモリ使用率、カスタムメトリクス(gRPC接続数など)。
閾値を超えるとPod数を増加し、負荷低下時は自動で縮小します。
AWS LB Controllerが新Podを検知してNLBのターゲットグループに自動登録します。
Podがスケジュール不可 → ノードを追加
🖥️ Node 1(満杯)
🖥️ Node 2(満杯)
↓ ノード追加 ↓
🖥️ Node 1
🖥️ Node 2
🖥️ +Node 3
トリガー条件:HPAが新しいPodを要求したが、既存ノードにリソース不足でスケジュールできない場合。
CAがAuto Scaling Groupを通じてEC2インスタンスを追加します。
低負荷時はノードを安全に削除してコストを最適化します。
💡 連携の流れ:
負荷増加 → HPA が Pod 追加を決定 → ノードの空きリソース不足を検知 → CA がノードを追加 →
新ノードで Pod がスケジュール → AWS LB Controller が NLB ターゲットに登録 → トラフィック受信開始
原因:TLSリスナーを使用しているため、NLBがTLSを終端してしまい、クライアント証明書がPodに届かない
解決:TCPリスナーに変更。aws-load-balancer-backend-protocol: "tcp" を設定
原因:AWS Load Balancer Controllerがインストールされていないか、IAMロールの権限不足
解決:Controller のデプロイ状況を確認し、IRSA(IAM Roles for Service Accounts)を正しく設定
原因:ターゲット登録のタイムラグ。NLBのヘルスチェックがまだ通過していない
解決:ヘルスチェック間隔を短縮(10秒)し、deregistration delay を適切に設定
原因:gRPCはHTTP/2の長時間接続。NLBがコネクションレベルで分散するため既存接続は再分散されない
解決:クライアント側でコネクションプールを実装し、定期的に再接続する仕組みを導入
✅ 運用チェックリスト
🔐
証明書のローテーション:cert-manager等を活用し、クライアント/サーバー証明書の自動更新を設定。有効期限切れはサービス停止に直結
🌐
クロスゾーン分散:NLBのクロスゾーンロードバランシングを有効化し、AZ間の負荷偏りを防止
📊
カスタムメトリクス:gRPC接続数やリクエストレートをPrometheusで収集し、HPAのスケール指標に活用
🔄
Graceful Shutdown:Pod終了時にpreStopフックで接続をドレインし、クライアントの接続断を最小化
🛡️
SecurityGroupの適切な設定:NLBからのトラフィックのみPodに到達させ、不正アクセスをネットワークレベルで遮断
🧪
mTLS接続テスト:grpcurl に --cert/--key オプションを付けて定期的にmTLS接続を検証