ALB/NLB選定で3年失敗し続けた話|本番で詰まった3つのパターンと対策
「WebアプリはALB」くらいの雑な理解で本番障害を起こした経験、ありませんか?gRPCのタイムアウト沼、Preserve Client IPの罠など実際に踏んだ失敗をもとに選定基準を整理しました。
先日チームのポストモーテムで「ALBとNLBの選定ミスが原因で本番障害が起きた」という話が出て、改めて自分のここ3年の失敗を棚卸しした。最初はなんとなく「Webアプリ=ALB、パフォーマンス重視=NLB」くらいの雑な理解で運用してたけど、それが本番でボロボロに崩れる経験を何度もした。今回はその実体験を元に、2026年時点での選定基準と高度な設定のコツを整理する。
教科書的な比較表はすでにAWSの公式に山ほどある。ここでは「なぜ失敗したか」「何を見落としてたか」に絞って書いていく。皆さんも似たような罠にハマったことありませんか?
ALBとNLBの選定基準、本当に大事なのはここだった
最初に結論から言うと、「HTTP/HTTPSを扱うか」「L4/L7のどちらで制御したいか」という軸だけで選ぶと後悔する。自分が実際に後悔したのはこの3パターンだ。
パターン1:gRPCにALBを選んで詰まった
gRPCサービスを作ったときに「HTTPだからALB」と即断したら、HTTP/2のストリーミング挙動がALBのタイムアウト設定と相性が悪くて長時間接続が切れまくった。ALBはデフォルトのアイドルタイムアウトが60秒で、gRPCの双方向ストリーミングと組み合わせると詰む。後述するけど、これはチューニングで回避できる。
パターン2:NLBのPreserve Client IPでDB接続プールが壊れた
TCPの低レイテンシが必要なMySQLクラスターの前にNLBを置いた際、Preserve Client IP(クライアントIPの保持)がオンになっていたせいでDB側のファイアウォールルールと競合した。地味にハマった。
パターン3:NLBのCross-Zone Load Balancingを後から有効化した
2024年末からNLBはCross-Zone Load Balancingがデフォルト有効になったが、既存のNLBはオフのままで、後から有効化したときにAZ間通信費用が想定外に増えた。これは月80万円のデータ転送費をVPCエンドポイント導入で削減した実装記録でも触れたデータ転送コストの落とし穴と根っこが同じ問題だ。
3パターンとも「デフォルトのまま深く考えずに進めた」という共通点がある。個人的には、この手の設定ミスは調査に時間がかかって精神的にもきついやつなので、選定時点で一度立ち止まる習慣が本当に大事だと思っている。
2026年時点での選定基準を整理するとこうなる:
| 観点 | ALB向き | NLB向き |
|---|---|---|
| プロトコル | HTTP/HTTPS/gRPC/WebSocket | TCP/UDP/TLS |
| レイテンシ要件 | 〜数ms許容(L7処理あり) | サブms(L4スルー) |
| IPアドレス固定 | 不可(DNS) | 可(静的IP/Elastic IP) |
| VPC外からの接続 | ALB + PrivateLink制限あり | NLB + PrivateLink推奨 |
| WebSocket | ネイティブ対応 | TCP経由で対応 |
| コスト(目安) | LCU課金(リクエスト数依存) | NLCU課金(帯域幅依存) |
| mTLS | 2024年後半対応 | TLS終端あり |
| ヘルスチェック粒度 | HTTP/HTTPS詳細設定可 | TCP/HTTP/HTTPSシンプル |
ここで「VPC Latticeを使えばNLBとService Discoveryが不要になるケースが増えた」という話も忘れてはいけない。マイクロサービス間通信ならVPC Lattice で本番マイクロサービス通信が2倍高速化した理由を先に読むと選択肢が広がる。
AWS構成図:実際に本番で動かしている構成
以下は現在うちのチームで運用しているマルチAZ構成だ。フロントにALB、バックエンドの一部サービスにNLBを使い分けている。
graph TB
subgraph Internet
Users["👤 ユーザー"]
Partners["🏢 パートナーシステム"]
end
subgraph AWS_Cloud["AWS Cloud"]
R53["Route 53\nDNS + Health Check"]
WAF["AWS WAF v2"]
CF["CloudFront\nCDN + Edge Cache"]
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph PublicSubnets["Public Subnets"]
ALB["ALB\nHTTPS/gRPC\nL7ルーティング"]
NLB["NLB\nTCP/TLS\n静的IP"]
end
subgraph AZ_A["AZ: ap-northeast-1a"]
ECS_A1["ECS Fargate\nWebアプリ"]
ECS_A2["ECS Fargate\ngRPCサービス"]
DB_Primary["Aurora PostgreSQL\nPrimary"]
end
subgraph AZ_C["AZ: ap-northeast-1c"]
ECS_C1["ECS Fargate\nWebアプリ"]
ECS_C2["ECS Fargate\ngRPCサービス"]
DB_Replica["Aurora PostgreSQL\nReplica"]
end
subgraph PrivateSubnets["Private Subnets"]
TGW["Target Group\nWebアプリ"]
TGW_GRPC["Target Group\ngRPC"]
TGW_TCP["Target Group\nTCP"]
end
SG_ALB["Security Group\nALB"]
SG_ECS["Security Group\nECS"]
end
CW["CloudWatch\nメトリクス・アラーム"]
end
Users --> CF --> WAF --> ALB
Partners --> R53 --> NLB
ALB --> SG_ALB --> TGW
ALB --> SG_ALB --> TGW_GRPC
NLB --> TGW_TCP
TGW --> ECS_A1
TGW --> ECS_C1
TGW_GRPC --> ECS_A2
TGW_GRPC --> ECS_C2
TGW_TCP --> DB_Primary
ECS_A1 --> DB_Primary
ECS_C1 --> DB_Replica
CW -.監視.-> ALB
CW -.監視.-> NLB
パートナーシステムへの固定IP要件があったのでNLBを残しているが、新規のマイクロサービス間通信はほぼVPC Latticeに移行しつつある。
ALBの高度な設定、実際にやって効いたもの・効かなかったもの
リスナールールの重み付けルーティング(Weighted Target Groups)
これは地味に便利だ。カナリアリリースのときに、旧バージョンに90%・新バージョンに10%とか流すのを、デプロイパイプライン外でALBだけで制御できる。コードを一切変えずにトラフィック比率をいじれるのがシンプルに助かる。
# Terraform例
resource "aws_lb_listener_rule" "canary" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.v1.arn
weight = 90
}
target_group {
arn = aws_lb_target_group.v2.arn
weight = 10
}
stickiness {
enabled = true
duration = 300 # 5分間スティッキー
}
}
}
condition {
path_pattern {
values = ["/api/*"]
}
}
}
注意点として、stickiness(スティッキーセッション)を有効にしないとユーザーが同一リクエスト内でバージョンを行き来するケースがあって混乱した。5分くらいの短いdurationにしておくのが無難。
gRPCのヘルスチェックとタイムアウト設定
前述のgRPC問題の解決策として、タイムアウト系の設定を全部見直した。ここが一番ハマりやすい箇所でもあるので、コードごとそのまま流用してもらえると助かると思う:
resource "aws_lb_target_group" "grpc" {
name = "grpc-tg"
port = 9090
protocol = "HTTP"
protocol_version = "GRPC"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
interval = 10
path = "/grpc.health.v1.Health/Check"
matcher = "0" # gRPC status code 0 = OK
port = "traffic-port"
protocol = "HTTP"
}
# gRPCストリーミング用にstickiness設定
stickiness {
type = "lb_cookie"
cookie_duration = 86400
enabled = false # gRPCはステートレスに保つ
}
}
そして忘れがちなのがALBのidle_timeout。デフォルト60秒をgRPCの双方向ストリーミングに合わせて延ばす必要がある:
resource "aws_lb" "main" {
# ...
idle_timeout = 3600 # 1時間。gRPC長時間ストリーム対応
# 2025年から利用可能なALB capacity unitsの予約設定
# 急激なトラフィック増加時のウォームアップ問題を回避
client_keep_alive = 3600
}
mTLS対応(2024年後半〜2026年の変化)
2024年後半にALBがmTLSに対応した。これが意外と設定ハードルが高くて最初詰まった。ACMのCA証明書をALBのリスナーに紐付けるパターンと、トラストストアを使うパターンがある:
resource "aws_lb_trust_store" "client_cert" {
name = "client-cert-store"
ca_certificates_bundle_s3_bucket = aws_s3_bucket.certs.bucket
ca_certificates_bundle_s3_key = "ca-bundle.pem"
}
resource "aws_lb_listener" "https_mtls" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.main.arn
mutual_authentication {
mode = "verify" # "off"|"passthrough"|"verify"
trust_store_arn = aws_lb_trust_store.client_cert.arn
}
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
}
passthroughモードはALBがmTLSの検証をせずにバックエンドに渡す。verifyはALBが検証する。B2B APIで使う分にはverifyで十分だけど、証明書ローテーションの運用設計をちゃんとやっておかないと後で地獄を見る。正直ここの運用設計を後回しにして一度痛い目に遭っているので、後から追加するよりも初期設計で決めておくことを強くすすめる。
NLBの落とし穴と2026年の設定最適化
Connection Draining(接続ドレイニング)の挙動差
ALBとNLBでデプロイ時の挙動が違う。ALBはderegistration_delayで既存接続を保持しながら新規接続を止める。NLBはconnection_terminationをtrueにしないと登録解除してもTCPコネクションが即切断されないことがある:
resource "aws_lb_target_group" "nlb_tcp" {
name = "nlb-tcp-tg"
port = 3306
protocol = "TCP"
vpc_id = var.vpc_id
deregistration_delay = 60
# NLB特有:登録解除時に既存接続を強制終了するか
connection_termination = true
# Preserve Client IPの設定(デフォルトtrue)
# DB等でクライアントIPが必要ない場合はfalseに
preserve_client_ip = false
health_check {
enabled = true
healthy_threshold = 3
unhealthy_threshold = 3
interval = 10
protocol = "TCP"
}
}
preserve_client_ip = falseにしたら前述のDB接続プール問題が解消された。ただし、アプリ側でクライアントIPのロギングが必要な場合はProxyプロトコルv2を使う必要がある。ここは好み分かれるかもしれない。
NLBのCross-Zone Load Balancingとコスト
2024年末以降のNLBはCross-Zone Load Balancingがデフォルト有効になった。これ自体は良いことだが、AZ間データ転送費用が発生することを忘れがちだ。うちが実際に喰らったコスト増加がこれで、グラフにすると一目瞭然だった:
xychart-beta
title "NLB Cross-Zone有効化前後のAZ間転送コスト(月次・万円)"
x-axis ["2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"]
y-axis "コスト(万円)" 0 --> 40
bar [8, 9, 8, 31, 33, 32]
line [8, 9, 8, 31, 33, 32]
2026年1月にクロスAZ転送が突然3倍以上に跳ね上がったのがNLBのデフォルト変更直後だった。実際、AZ間でトラフィックをバランシングすること自体はレイテンシ改善に効くので、コストと相談しながら判断する必要がある。トラフィックが1AZに集中する設計が取れるなら無効化する選択肢もある。
TLSターミネーションの選択肢
NLBのTLSターミネーションは「NLBで終端する」か「バックエンドまで通過させる(TLS Passthrough)」かで設計が変わる:
# TLSターミネーションをNLBでやる場合
resource "aws_lb_listener" "nlb_tls" {
load_balancer_arn = aws_lb.nlb.arn
port = 443
protocol = "TLS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.main.arn
# アルファティック証明書の自動ローテーション
alpn_policy = "HTTP2Preferred"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.nlb_tcp.arn
}
}
TLS Passthroughはprotocol = "TCP"のまま443を通すだけ。ACM証明書が使えずバックエンドで証明書管理が必要になるが、エンドツーエンド暗号化の監査要件がある場合はこちらを選ぶ。SOC2やコンプライアンス対応についてはSOC2対応2026年版|AI検証とZTA実装で完全コンプライアンスも参考になる。
本番監視・アラーム設定の実務パターン
ロードバランサー選定と同じくらい大事なのが監視設計だ。正直これを後回しにして何度も痛い目を見た。「後でやろう」がいちばん危ない。
# ALBの重要メトリクスアラーム
resource "aws_cloudwatch_metric_alarm" "alb_5xx_rate" {
alarm_name = "alb-5xx-rate-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
threshold = 5 # 5%超えたら
alarm_description = "ALB 5XXエラーレートが5%を超えました"
metric_query {
id = "error_rate"
expression = "(m2/m1)*100"
label = "5XX Error Rate"
return_data = true
}
metric_query {
id = "m1"
metric {
metric_name = "RequestCount"
namespace = "AWS/ApplicationELB"
period = 60
stat = "Sum"
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
}
}
metric_query {
id = "m2"
metric {
metric_name = "HTTPCode_ELB_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
stat = "Sum"
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
}
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
# NLB特有:TLSネゴシエーションエラー監視
resource "aws_cloudwatch_metric_alarm" "nlb_tls_error" {
alarm_name = "nlb-tls-negotiation-error"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ClientTLSNegotiationErrorCount"
namespace = "AWS/NetworkELB"
period = 60
statistic = "Sum"
threshold = 10
alarm_description = "NLB TLSネゴシエーションエラーが急増"
dimensions = {
LoadBalancer = aws_lb.nlb.arn_suffix
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
監視で「これだけは必ず設定しろ」と思っているメトリクスをまとめる。特にUnHealthyHostCount > 0は即日設定してほしい。これを入れていなかったせいで深夜アラートに気付くのが遅れたことが2回ある:
| メトリクス | LBタイプ | 閾値目安 | 意味 |
|---|---|---|---|
| HTTPCode_ELB_5XX_Count | ALB | 5%/分 | LB自体のエラー(バックエンド起因ではない) |
| TargetResponseTime | ALB | p99 > 2s | バックエンドのレイテンシ劣化 |
| UnHealthyHostCount | ALB/NLB | > 0 | 異常ホスト数 |
| ActiveFlowCount | NLB | 容量の80% | TCP接続数上限接近 |
| ClientTLSNegotiationErrorCount | NLB | > 10/分 | 証明書問題・クライアント設定ミス |
| ConsumedLCUs | ALB | コスト監視 | LCU消費量(課金直結) |
インシデント発生時の対応フローについてはインシデント対応の最新ベストプラクティス2026が詳しい。特にALBの5XXエラー急増時のエスカレーション判断フローは参考になった。
flowchart TD
A["アラート発火"] --> B{"UnHealthyHostCount > 0?"}
B -->|Yes| C["ECSタスクログ確認\nヘルスチェック失敗原因調査"]
B -->|No| D{"HTTPCode_ELB_5XX?"}
D -->|Yes| E["ALB自体の問題\nSecurity Group/ACL確認"]
D -->|No| F{"TargetResponseTime増加?"}
F -->|Yes| G["バックエンド性能問題\nDB接続・メモリ確認"]
F -->|No| H["RequestCount急増\nオートスケール・WAF確認"]
C --> I["ローリング再デプロイまたは\nターゲット手動解除"]
E --> J["設定修正\n必要に応じてLB再作成"]
G --> K["オートスケール確認\n接続プール設定見直し"]
H --> L["WAF・Shield確認\nDDoS攻撃の可能性"]
まとめ
3年間のALB/NLB運用で学んだことを一言で言うと「選定より設定の失敗のほうが多い」だ。どちらを選ぶかよりも、選んだ後の細かい設定値とデフォルト値の罠に何度もやられた。
要点:
- プロトコルと固定IP要件で一次判断する — HTTPSアプリはALB、固定IP必須・超低レイテンシ・TCP/UDPはNLB。gRPCはALBで動くが
protocol_version = "GRPC"とidle_timeoutの設定が必須 - NLBのpreserve_client_ipとCross-Zone転送コストを必ず確認する — デフォルト値のまま本番投入するとコストと接続問題のどちらかが待っている
- mTLSはALBのTrust Storeで対応できる — ただし証明書ローテーション運用を先に設計しておくこと
- 監視は5XXレートとUnHealthyHostCountの2つを最低限即日設定する — これを後回しにするたびに夜中に起こされた
- マイクロサービス間通信はVPC Latticeへの移行を検討する — NLB + Service DiscoveryはVPC Latticeで置き換えられるケースが増えている
次のアクション:
- 既存NLBのCross-Zone Load Balancingの設定状態とデータ転送コストを確認する
- ALBのgRPCターゲットグループのヘルスチェックパスが
/grpc.health.v1.Health/Checkになっているか確認する - CloudWatchで
UnHealthyHostCount > 0のアラームがなければ今日中に追加する
正直まだALBのCapacity Unit Reservation(新規追加された事前プロビジョニング機能)は検証途中なので、またまとまったら続きを書く。皆さんのNLB/ALBのハマりポイントがあればコメントかX/Twitterで教えてもらえると嬉しい。