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/WebSocketTCP/UDP/TLS
レイテンシ要件〜数ms許容(L7処理あり)サブms(L4スルー)
IPアドレス固定不可(DNS)可(静的IP/Elastic IP)
VPC外からの接続ALB + PrivateLink制限ありNLB + PrivateLink推奨
WebSocketネイティブ対応TCP経由で対応
コスト(目安)LCU課金(リクエスト数依存)NLCU課金(帯域幅依存)
mTLS2024年後半対応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_CountALB5%/分LB自体のエラー(バックエンド起因ではない)
TargetResponseTimeALBp99 > 2sバックエンドのレイテンシ劣化
UnHealthyHostCountALB/NLB> 0異常ホスト数
ActiveFlowCountNLB容量の80%TCP接続数上限接近
ClientTLSNegotiationErrorCountNLB> 10/分証明書問題・クライアント設定ミス
ConsumedLCUsALBコスト監視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運用で学んだことを一言で言うと「選定より設定の失敗のほうが多い」だ。どちらを選ぶかよりも、選んだ後の細かい設定値とデフォルト値の罠に何度もやられた。

要点:

  1. プロトコルと固定IP要件で一次判断する — HTTPSアプリはALB、固定IP必須・超低レイテンシ・TCP/UDPはNLB。gRPCはALBで動くがprotocol_version = "GRPC"とidle_timeoutの設定が必須
  2. NLBのpreserve_client_ipとCross-Zone転送コストを必ず確認する — デフォルト値のまま本番投入するとコストと接続問題のどちらかが待っている
  3. mTLSはALBのTrust Storeで対応できる — ただし証明書ローテーション運用を先に設計しておくこと
  4. 監視は5XXレートとUnHealthyHostCountの2つを最低限即日設定する — これを後回しにするたびに夜中に起こされた
  5. マイクロサービス間通信は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で教えてもらえると嬉しい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事