AWSのVCS(Virtual Private Cloud)設計において、プライベートサブネットからインターネットへのアウトバウンド通信を確保するための王道といえば、マネージドな NAT Gateway です。高可用性、フルマネージド、スケールはお任せ――非の打ち所がないサービスに見えます。
しかし、シニアなインフラエンジニアであれば、一度はこんな壁にぶぶかったことがあるはずです。
「マルチAZで各AZに NAT Gateway を置いたら、時間あたりの料金がバカにならない」
「特定の国やプロバイダ宛てのトラフィックで、特定の固定IP(EIP)のプールを厳密に制御しつつ、コストを極限まで削りたい」
マネージドな快適さを捨ててでも、我々が「自前で NATインスタンス を組む」のには、それなりの理由とロマンがあります。そして、自前でEC2インスタンスをルーターとして立てる以上、避けて通れないのが「インスタンス障害時のフェイルオーバー」という宿命的な課題です。
今回は、マネージドの甘い汁を断ち、LambdaとCloudWatch Events(現EventBridge)、そしてカスタムルートテーブルを駆使して、泥臭くも堅牢な「NATインスタンス高可用性&自動フェイルオーバー機構」を構築する実務のノウハウを、余すところなくお伝えします。
—
1. なぜマネージドではなく、あえて「NATインスタンス」なのか?
AWSの設計原則において、単一障害点(SPOF)の排除は絶対正義です。マネージドの NAT Gateway はAWS側が裏側で冗長化してくれていますが、EC2ベースの NATインスタンス を使う場合、インスタンスが1台ダウンすれば、プライベートサブネットからの外向き通信は一瞬でブラックホールに吸い込まれます。
ここで「じゃあAuto Scaling Group(ASG)に入れればいいじゃないか」と思うかもしれませんが、話はそう単純ではありません。
AWSのVPCルーティングにおいて、カスタムルートテーブルのターゲット(宛先)に指定できるのは、NAT Gateway や Internet Gateway、そして「特定のEC2インスタンスのID(例: i-0123456789abcdef0)」そのものです。
通常のASG構成のように、ロードバランサー(ALB)の裏でインスタンスが入れ替わったり、Auto ScalingによってインスタンスIDが変わるたびに、VPCのルートテーブルを手動で書き換える――そんな運用をしていたら、深夜の障害対応で精神を病むこと間違いなしです。
だからこそ、「インスタンスが死んだ検知」と「ルートテーブルのターゲット動的書き換え」をプログラムで結合する仕組みが必要になります。
—
2. アーキテクチャの全体像とパケットの旅
今回構築するアーキテクチャのコンポーネントは以下の通りです。
1. Private Subnet: アプリケーションサーバー(EC2やECS)が常駐。デフォルトルート(0.0.0.0/0)の向き先として、後述の動的ルートテーブルを設定。
2. Public Subnet: アクティブな NATインスタンス(ENA / SourceDestCheck 無効化済み)が常駐。
3. Amazon EventBridge (旧CloudWatch Events): NATインスタンス のハードウェア障害やインスタンスステータスチェック失敗を検知。
4. AWS Lambda: EventBridgeからのトリガーを受け取り、Boto3を使ってVPCのルートテーブルを書き換える主役。
障害発生から復旧までのシーケンス
[Private Subnet (EC2)]
│
▼ (通信要求: 0.0.0.0/0)
[Route Table] ──(ターゲット変更)──> [死活監視: EventBridge]
│ │
├─(旧: 障害発覚 i-aaa) ▼
└─(新: 復活 i-bbb) [AWS Lambda (Boto3)]
│
▼
ルートテーブルの動的更新
(`replace_route`)
1. 異常検知: アクティブなNATインスタンス(例: i-0aaa)でOSハングやハードウェア障害が発生。CloudWatchの StatusCheckFailed_System アラームが ALARM 状態になる。
2. イベント発火: EventBridgeがこの状態変化をキャッチし、ターゲットに指定されたLambda関数を起動。
3. ルート書き換え: LambdaがVPCのAPIを叩き、プライベートサブネット用のルートテーブルにある 0.0.0.0/0 のターゲットを、スタンバイ側のNATインスタンス(例: i-bbb)のインスタンスIDへアトミックに置き換える。
4. 通信回復: 数十秒のダウンタイムを挟み、プライベートインスタンスからの外向き通信が新しいNATインスタンス経由で再開される。
—
3. 実装のキモ:Lambdaによるルートテーブルの動的制御
現場で最もハマりやすいのが、AWS SDK(Boto3等)を用いたルートの置換処理です。単に create_route を叩くだけでは「ルートが既に存在する」というエラー(ClientError)に阻まれます。既存のルートを安全に差し替えるには、replace_route メソッドを使用するのが正解です。
以下に、フェイルオーバーを担うLambda関数(Python 3.9以降)の実装例を示します。
import os
import boto3
from botocore.exceptions import ClientError
# 環境変数から対象のルートテーブルIDと、新しいNATインスタンスのIDを取得
ROUTE_TABLE_ID = os.environ.get('ROUTE_TABLE_ID')
NEW_NAT_INSTANCE_ID = os.environ.get('NEW_NAT_INSTANCE_ID')
DESTINATION_CIDR = '0.0.0.0/0'
ec2 = boto3.client('ec2')
def lambda_handler(event, context):
"""
CloudWatch Events (EventBridge) からのトリガーを受け取り、
指定されたルートテーブルのデフォルトゲートウェイを新しいNATインスタンスに切り替える。
"""
print(f"Received event: {event}")
print(f"Target Route Table: {ROUTE_TABLE_ID}")
print(f"New NAT Instance ID: {NEW_NAT_INSTANCE_ID}")
try:
# replace_route を使用して、既存の 0.0.0.0/0 のターゲットを強制的かつアトミックに置き換える
response = ec2.replace_route(
RouteTableId=ROUTE_TABLE_ID,
DestinationCidrBlock=DESTINATION_CIDR,
InstanceId=NEW_NAT_INSTANCE_ID
)
print(f"Successfully replaced route. Response: {response}")
return {
'statusCode': 200,
'body': f'Successfully failed over to NAT instance: {NEW_NAT_INSTANCE_ID}'
}
except ClientError as e:
print(f"Error occurred while replacing route: {e}")
raise e
パラメータ設定の重要な注意点
SourceDestCheckの無効化: NATインスタンスとして動作するEC2インスタンスは、自分宛てではないパケット(プライベートIPからの外向きパケット)をルーティングするため、必ずインスタンスの「送信元/宛先チェック(Source/Destination Check)」を無効化しておかなければなりません。これを忘れると、AWSの仮想ネットワーク層でパケットが問答無用でドロップされます。- IAM権限(Least Privilege): LambdaにアタッチするIAMロールには、過剰な権限を与えず、必要最小限の
ec2:ReplaceRouteおよびログ出力用のlogs:CreateLogStream,logs:PutLogEventsのみを許可してください。
—
4. リカバリタイム(RTO)の実測値と現場の泥臭い現実
「で、結局障害から復旧するまでに何秒かかるんだ?」という疑問が湧くはずです。実運用におけるリカバリタイム(RTO: Recovery Time Objective)の分解能を見てみましょう。
| プロセス | 所要時間の目安 | 備考 |
| :— | :— | :— |
| ハードウェア障害発生 〜 CloudWatch検知 | 約 1 〜 2分 | システムステータスチェックのデフォルトポーリング間隔に依存 |
| EventBridge検知 〜 Lambda起動 | 数秒以内 | サーバーレスのコールドスタートを含む |
| Lambda実行(replace_route) | 0.5秒未満 | VPC APIの応答速度は非常に高速 |
| TCPコネクションの収束(クライアント側) | アプリケーション依存 | 既存のセッションは切断されるため、再送やリトライが必須 |
トータルでおよそ 1分半〜2分程度 のダウンタイムが発生します。これを「短い」と見るか「致命的」と見るかはシステム要件によりますが、もしミリ秒単位の可用性が必要であれば、素直にマネージドな NAT Gateway をマルチAZで採用するべきです。
逆に、バッチ処理や重要度の低いバックグラウンド連携、あるいはコスト削減が至上命題である開発環境であれば、この構成は十分すぎるほどのコストパフォーマンスを発揮します。
—
5. 運用時のトラブルシューティングTips
最後に、実際にこの構成を本番稼働させたシニアエンジニアが直面しがちな「ハマりどころ」とデバッグ手順を授けます。
1. パケットがNATインスタンスに届いているのに外に出ていかない
- 原因: LinuxカーネルのIPフォワーディングが有効になっていない可能性が高いです。
- 対策: NATインスタンスのOS側で
/etc/sysctl.confを確認し、net.ipv4.ip_forward = 1が設定されているか、およびiptables(またはnftables)のMASQUERADE設定(iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE)が正しく入っているかをsysctl -pやiptables -L -t natで確認してください。
2. Lambdaが ClientError を吐いて失敗する
- 原因: LambdaにアタッチされているIAMロールが、対象のルートテーブルに対する変更権限を持っていません。
- 対策: CloudWatch Logsでエラーコード(
UnauthorizedOperationやInvalidRouteTableID.NotFound)を確認し、リソースARNの指定ミスや権限不足を修正します。
—
まとめ
クラウドネイティブ全盛の時代にあっても、ネットワークの根幹にある「ルーティング」の仕組みと、APIによる動的制御の組み合わせを知ることは、エンジニアの強力な武器になります。
マネージドサービスの裏側で何が起きているのか、もしそれを自前でコード化するならどう設計すべきか――その泥臭いプロセスを理解しているかどうかが、いざという時のトラブルシューティングのスピードを大きく左右します。コストと可用性のトレードオフに悩む現場で、ぜひこのカスタムルート制御によるNATインスタンス構成を検討してみてください。
コメント