接続元IPアドレスを絞りたいと言われた時用に特定のプライベートIPアドレスにSNATする仕組みを自作してみた

接続元IPアドレスを絞りたいと言われた時用に特定のプライベートIPアドレスにSNATする仕組みを自作してみた

単一のプライベートIPアドレスにSNATしたい場合に
2026.06.24

接続先から「閉域接続で送信元IPアドレスとして登録できるのは1IPまで」と言われたのだが

こんにちは、のんピ(@non____97)です。

皆さんは接続先から「閉域接続で送信元IPアドレスとして登録できるのは1IPまで」と言われたこと思ったことはありますか? 私はあります。

接続先から以下のような制約事項が提示されることがあります。

  1. 送信元IPアドレスは動的に変化してはならない
  2. 送信元IPアドレスとして登録できるのは1つまで

1つ目の「送信元IPアドレスは動的に変化してはならない」については、シンプルに複数台あるだけでIPアドレスが固定なのであれば良いですが、送信元となるリソースをAuto Scalingさせる必要があるのであれば非常に大変です。私はもうやりたくありません。

https://dev.classmethod.jp/articles/autoscaling-ec2-instance-mapping-private-ip-address/

2つ目の「送信元IPアドレスとして登録できるのは1つまで」についてはMulti-AZ構成とする場合に辛いです。

Single-AZで良いのであれば、Private NAT GatewayでSNATをすることで複数のEC2インスタンスからの通信の送信元IPアドレスを単一のIPアドレスとすることができます。

https://dev.classmethod.jp/articles/nat-gateway-supports-private-nat-gateway/

一方、Multi-AZとする場合は一筋縄ではありません。Multi-AZとする場合サブネットを複数に分ける必要があります。Multi-AZでActive/StandbyのHAクラスターを組みたい場合サブネットを跨ぐ形になるため、AZ障害が発生した際にはIPアドレスが変わってしまいます。

ENIはサブネットに紐づけられるので、ENIを障害時にフェイルオーバー先のリソースに付与することもできません。

「閉域接続で」という縛りがある場合はEIPの付け外しも採用できません。EIPはパブリックIPv4アドレスだからです。

現状Regional NAT GatewayはプライベートIPv4へのSNATをサポートしていません。

ということで、特定のプライベートIPアドレスにSNATする仕組みを自作してみました。

検証環境

検証環境は以下のとおりです。

検証環境構成図.png

要するにSNATインスタンスをMulti-AZでデプロイしています。仕組みについては後述します。

全てのリソースはAWS CDKでデプロイしています。使用したコードは以下GitHubリポジトリに保存しています。

https://github.com/non-97/aws-cdk-multi-az-fixed-egress-ip

ポイント

ポイントついて説明します。

各ポイントを図示すると以下のとおりです。

仕組み.png

① 戻りの通信についてActive な SNATインスタンスのENIがネクストホップとなるよう設定

SNATをするためにクライアントからSNATインスタンスにルーティングすることにのみフォーカスしてしまいがちですが、戻りの通信についても意識する必要があります。

今回はSite-to-Site VPNとVGWを用いて接続しているため、VGWのエッジルートテーブルで制御する形になります。

こちらでActiveなSNATインスタンスのENIにルーティングするように設定をしてあげます。

② VPC CIDR内でフローティングIPを実現するためには、フローティングIPが属するサブネットを作成する必要がある

VPC CIDR内でフローティングIPを実現するためには、フローティングIPが属するサブネットを作成する必要があります。

VPC CIDRが10.10.0.0/24のところ、フローティングIPアドレスが10.10.0.116/32の場合として、ルートテーブルで10.10.0.116/32のターゲットをSNATインスタンスのENIを指定して更新します。

1.10.10.0.116:32のルート追加.png

すると、Route destination doesn't match any subnet CIDR blocksとなりました。要するにどのサブネットのCIDRにも属していないが為にエラーになっています。

2.Route destination doesn't match any subnet CIDR blocks.png

では、ルートテーブルで追加するルートのプレフィックスを何をしたら良いのでしょうか。

VPC CIDRが10.10.0.0/24のところ、フローティングIPアドレスが10.10.0.85/32の場合として、10.10.0.80/28のCIDRブロックを持つサブネットがある状態で、ルートテーブルで10.10.0.85/32のターゲットをSNATインスタンスのENIを指定して更新します。

3.10.10.0.85:32.png

はい、こちらもRoute destination doesn't match any subnet CIDR blocksとなりました。

4.Route destination doesn't match any subnet CIDR blocks.png

つまりはVPC CIDR内でフローティングIPを実現する為には以下が必要です。

  • フローティングIPが属するサブネットを作成する
  • ルートテーブルでは「フローティングIPアドレスが属するサブネットCIDR」への通信の際に「SNAT ENIへルーティングする」というルートを追加する

なお、フローティングIPをVPC外とする場合はTGWで通信を捻じ曲げる必要があります。詳細は以下記事をご覧ください。

https://dev.classmethod.jp/articles/fsxn-floating-ip-vgw-gateway-routing/

https://dev.classmethod.jp/articles/amazon-fsx-for-netapp-ontap-access-via-network-load-balancer-endpoint-ip-address-range-outside-vpc-cidr/

③ SNATインスタンスは起動しているが、実際に適切なNATルールや疎通ができていない場合に備えて30秒間隔で疎通確認をしてカスタムメトリクスをPUT

SNATインスタンスは動作しているがSNATするルールがない場合や、ネットワークのコネクテビティがない場合もフェイルオーバーさせたいところです。

そのような場合に備えて、以下ヘルスチェックのスクリプトをsystemd-timerで30秒間隔で実行しています。

$ sudo cat /usr/local/bin/snat-healthcheck.sh
#!/bin/bash
set -uo pipefail
# SNAT インスタンスが「送信元を固定した転送経路」として正しく機能しているかを複数観点で検査し、
# 結果を 1(正常)/0(異常)として CloudWatch メトリクスに発行する。systemd timer が一定間隔で
# 実行し、このメトリクスを AZ ごとのアラームが監視する。EC2 ステータスチェック等の浅いヘルスでは
# 検知できない、転送経路に固有の故障 (IP 転送が無効 / SNAT ルールが消失 / 上流へ到達できない) を検出する。
# conntrack 枯渇のような負荷・容量の問題は対象外: 待機系へ切り替えても同じ負荷で再発するため、
# フェイルオーバーでは直らない (容量はインスタンスサイズ / nf_conntrack_max の調整で対処する)。

# 自分の AZ を取得 (IMDSv2)。最後の発行で次元 Az= に使い、AZ ごとに値を分けてアラームを AZ 単位にする。
TOKEN="$(curl -sS -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 60')"
AZ="$(curl -sS -H "X-aws-ec2-metadata-token: ${TOKEN}" http://169.254.169.254/latest/meta-data/placement/availability-zone)"

# 3 つの検査。いずれもインスタンス固有の故障 (片方だけ壊れる) なのでフェイルオーバーで直る。
# 1 つでも失敗したら healthy を 0 にする。
healthy=1
# (1) IP フォワーディングが有効か。SNAT は転送を行うので必須。無効だと一切転送できない。
[ "$(cat /proc/sys/net/ipv4/ip_forward)" = "1" ] || healthy=0
# (2) 送信元を固定 IP へ書き換える SNAT (POSTROUTING) ルールが存在するか。消えると送信元が固定されない。
iptables -t nat -S POSTROUTING | grep -q -- "--to-source 10.10.0.85" || healthy=0
# (3) 上流 (VPN ルーター) への到達性。VPN トンネルと SNAT サブネット → VGW → VPN の経路が通っているかを end-to-end で確認する。
ping -c 1 -W 2 "192.168.100.5" >/dev/null 2>&1 || healthy=0

# 検査結果を高解像度メトリクス (storage-resolution 1) として発行する。高解像度なので 30 秒周期の
# アラームが使え、短い発行間隔とあわせて約 1 分以内に異常を検知できる。インスタンスが停止や終了で
# 発行できなくなった場合は、アラーム側の「欠損 = breaching」設定が異常とみなす。
aws cloudwatch put-metric-data \
  --region "ap-northeast-1" \
  --namespace "FixedEgress" \
  --metric-name "SnatPathHealthy" \
  --unit Count \
  --value "${healthy}" \
  --storage-resolution 1 \
  --dimensions Az="${AZ}"

PUTされたカスタムメトリクスを監視するCloudWatchアラームをAZ毎に作成しています。こちらのCloudWatchアラームではカスタムメトリクスが存在しない場合もアラーム状態としているため、カスタムメトリクスのPUTがそもそも正常に行えない場合も異常と判定し、フェイルオーバーできるようにしています。

④ ダウンタイムを極力短くするためにAuto Scaling で常時2台起動

Auto Scalingを使用して、常時一台構成にすることでコストを削減することは可能です。

しかし、ダウンしたことをトリガーにAuto Scalingに従いEC2インスタンスを起動し始めてはダウンタイムが長くなってしまいます。

今回はダウンタイムを短くするため、コストを受容して常時2台起動するようにしています。

⑤ 指定したイベントをトリガーに対象ルートテーブルのルートを設定

指定したイベントをトリガーに対象ルートテーブルのルートを切り替えています。

対象ルートテーブルというのは以下の2つです。

  1. クライアントが属するサブネットのルートテーブル
  2. VGWのエッジルートテーブル

ルートの切り替えを行うイベントと具体的にどのような場合に対してのケアをしているのかを整理したものは以下のとおりです。

# トリガーイベント (EventBridge) 検知する状況(具体例) ルート対応 補足 / あわせて行う処理
1 EC2 Instance Launch Successful
(aws.autoscaling、ASG 名で絞り込み)
SNATインスタンスが起動した
(初回デプロイ、または置換や復旧で新しいインスタンスが起動)
初回はルートを作成。現用 AZなら向け、それ以外は現用を維持する(切り替えない) 起動直後は正常判定を待たず優先 AZへ暫定で向け、初回の経路未設定を防ぐ。初回ルートと障害復旧を担う
2 EC2 Instance State-change Notification
(state = stopping / shutting-down)
SNATインスタンスの停止や終了が始まった
(手動停止、ASG による置換、stopping を経ない直接終了)
停止または終了したインスタンスはdescribe-instances に現れなくなり、待機 AZ へただちにフェイルオーバー アラームの欠損確定(1〜2分)を待たず、いち早く切り替える。全インスタンスのイベントが届くので、自 ASG 以外はすぐに処理を終える。起点の状態だけを購読する
3 CloudWatch Alarm State Change
(詳細なヘルスチェックのアラーム ×2)
稼働中だが転送経路が破損
(ip_forward 無効 /SNAT ルール消失 / 上流到達不可)、メトリクスの欠損、またはアラーム復旧(OK 遷移)
異常な AZ から待機 AZ へフェイルオーバー 正常な対向 AZ があるとき、異常なインスタンスを SetInstanceHealth で Unhealthyにして入れ替える(両系がアラーム状態の場合は入れ替えない)

ルートの切り替えはLambda関数で行っています。

行っている処理は以下のとおりです。

動作確認

通常時

実際に動作確認を行います。

フローティングIPは10.10.0.85です。

デプロイ後ap-northeast-1aとap-northeast-1cのクライアントEC2インスタンスの両方からターゲットのEC2インスタンスに対してアクセスしてみます。

ap-northeast-1aのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.16

$ curl 192.168.100.20
ok: source=10.10.0.85
ap-northeast-1cのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.45

$ curl 192.168.100.20
ok: source=10.10.0.85
ターゲットのEC2インスタンス
$ cat /usr/local/bin/echo-server.py
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        src = self.client_address[0]
        # アクセスログ: どの送信元 IP から来たかを journal へ (SNAT 後は 10.10.0.85)
        print(f"access source={src} \"{self.requestline}\"", flush=True)
        body = f"ok: source={src}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass

HTTPServer(("0.0.0.0", 80), Handler).serve_forever()

$ sudo journalctl -u echo-server
Jun 24 07:16:14 ip-192-168-100-20.ap-northeast-1.compute.internal systemd[1]: Started echo-server.service - Echo source IP HTTP server.
Jun 24 07:42:48 ip-192-168-100-20.ap-northeast-1.compute.internal python3[3253]: access source=10.10.0.85 "GET / HTTP/1.1"
Jun 24 07:43:17 ip-192-168-100-20.ap-northeast-1.compute.internal python3[3253]: access source=10.10.0.85 "GET / HTTP/1.1"

意図したとおり、10.10.0.85にSNATされていることが分かります。

ちなみに現在はap-northeast-1aのSNATインスタンスを使用しています。

5.現在のVGW RT.png
6.現在のVGW RT_2.png

SNATインスタンスが停止した場合

それでは、現在ActiveとなっているSNATインスタンスが停止した場合の挙動を確認します。

マネジメントコンソールからEC2インスタンスを停止させます。その際常にターゲットのEC2インスタンスにpingを打ち続けます。

ap-northeast-1aのクライアントEC2インスタンスからのアクセス
$ ping 192.168.100.20
PING 192.168.100.20 (192.168.100.20) 56(84) bytes of data.
64 bytes from 192.168.100.20: icmp_seq=1 ttl=125 time=1.30 ms
64 bytes from 192.168.100.20: icmp_seq=2 ttl=125 time=1.36 ms
64 bytes from 192.168.100.20: icmp_seq=3 ttl=125 time=1.26 ms
64 bytes from 192.168.100.20: icmp_seq=4 ttl=125 time=1.22 ms
64 bytes from 192.168.100.20: icmp_seq=5 ttl=125 time=1.27 ms
.
.
(中略)
.
.
64 bytes from 192.168.100.20: icmp_seq=15 ttl=125 time=31.0 ms
64 bytes from 192.168.100.20: icmp_seq=16 ttl=125 time=1.79 ms
64 bytes from 192.168.100.20: icmp_seq=17 ttl=125 time=1.34 ms
64 bytes from 192.168.100.20: icmp_seq=18 ttl=125 time=1.50 ms
64 bytes from 192.168.100.20: icmp_seq=22 ttl=125 time=4.77 ms
64 bytes from 192.168.100.20: icmp_seq=23 ttl=125 time=4.43 ms
64 bytes from 192.168.100.20: icmp_seq=24 ttl=125 time=4.40 ms
64 bytes from 192.168.100.20: icmp_seq=25 ttl=125 time=4.72 ms
64 bytes from 192.168.100.20: icmp_seq=26 ttl=125 time=4.43 ms
64 bytes from 192.168.100.20: icmp_seq=27 ttl=125 time=4.54 ms
64 bytes from 192.168.100.20: icmp_seq=28 ttl=125 time=4.88 ms
64 bytes from 192.168.100.20: icmp_seq=29 ttl=125 time=4.81 ms
ap-northeast-1cのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.45

$ ping 192.168.100.20
PING 192.168.100.20 (192.168.100.20) 56(84) bytes of data.
64 bytes from 192.168.100.20: icmp_seq=1 ttl=125 time=2.91 ms
64 bytes from 192.168.100.20: icmp_seq=2 ttl=125 time=2.66 ms
64 bytes from 192.168.100.20: icmp_seq=3 ttl=125 time=2.94 ms
64 bytes from 192.168.100.20: icmp_seq=4 ttl=125 time=2.69 ms
64 bytes from 192.168.100.20: icmp_seq=5 ttl=125 time=2.69 ms
64 bytes from 192.168.100.20: icmp_seq=6 ttl=125 time=2.86 ms
64 bytes from 192.168.100.20: icmp_seq=7 ttl=125 time=2.90 ms
64 bytes from 192.168.100.20: icmp_seq=8 ttl=125 time=2.96 ms
.
.
(中略)
.
.
64 bytes from 192.168.100.20: icmp_seq=22 ttl=125 time=2.78 ms
64 bytes from 192.168.100.20: icmp_seq=23 ttl=125 time=2.83 ms
64 bytes from 192.168.100.20: icmp_seq=24 ttl=125 time=2.67 ms
64 bytes from 192.168.100.20: icmp_seq=27 ttl=125 time=2.86 ms
64 bytes from 192.168.100.20: icmp_seq=28 ttl=125 time=2.73 ms
64 bytes from 192.168.100.20: icmp_seq=29 ttl=125 time=2.97 ms
64 bytes from 192.168.100.20: icmp_seq=30 ttl=125 time=2.68 ms

はい、SNATインスタンスが停止をして、不通になってから2~3秒後に疎通できるようになりました。ap-northeast-1aのEC2インスタンスからのpingについてはクロスAZの通信となるので若干レイテンシーが増加していますね。

ルートテーブルを確認すると、ap-northeast-1cのSNATインスタンスのENIにルーティングをしていることが分かります。

8.VGW RT切り替え後.png
9.VGW RT切り替え後2.png

curlでもアクセスします。

ap-northeast-1aのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.16

$ curl 192.168.100.20
ok: source=10.10.0.85
ap-northeast-1cのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.45

$ curl 192.168.100.20
ok: source=10.10.0.85

はい、ターゲットのEC2インスタンスからは変わらずSNAT先の10.10.0.85のIPアドレスから通信が来ていること分かります。

なお、しばらくすると、Auto Scalingによりap-northeast-1aのEC2インスタンスが立ち上がってきました。

7.EC2インスタンスの停止.png

参考までにLambda関数では以下ログが出力されていました。

{
    "timestamp": "2026-06-24T07:50:03Z",
    "level": "INFO",
    "message": "Found credentials in environment variables.",
    "logger": "botocore.credentials",
    "requestId": ""
}

{
    "timestamp": "2026-06-24T07:50:03Z",
    "level": "INFO",
    "message": "trigger",
    "logger": "root",
    "requestId": "52ee1237-1297-4d28-a7f9-28e765bac356",
    "detailType": "EC2 Instance State-change Notification"
}

{
    "timestamp": "2026-06-24T07:50:04Z",
    "level": "INFO",
    "message": "eni discovered",
    "logger": "root",
    "requestId": "52ee1237-1297-4d28-a7f9-28e765bac356",
    "eniByAz": {
        "ap-northeast-1c": "eni-0ed1c0af1a3b2ca1f"
    }
}

{
    "timestamp": "2026-06-24T07:50:04Z",
    "level": "INFO",
    "message": "active az decided",
    "logger": "root",
    "requestId": "52ee1237-1297-4d28-a7f9-28e765bac356",
    "activeAz": "ap-northeast-1c",
    "healthy": {
        "ap-northeast-1a": true,
        "ap-northeast-1c": true
    }
}

{
    "timestamp": "2026-06-24T07:50:06Z",
    "level": "INFO",
    "message": "routes synced",
    "logger": "root",
    "requestId": "52ee1237-1297-4d28-a7f9-28e765bac356",
    "target": "eni-0ed1c0af1a3b2ca1f",
    "activeAz": "ap-northeast-1c",
    "prevGatewayEni": "eni-0f9e4f967f146a8ff"
}

{
    "timestamp": "2026-06-24T07:51:48Z",
    "level": "INFO",
    "message": "trigger",
    "logger": "root",
    "requestId": "d2c2c2e2-a302-4d7b-bfc4-15120889df9e",
    "detailType": "EC2 Instance State-change Notification"
}

{
    "timestamp": "2026-06-24T07:51:48Z",
    "level": "INFO",
    "message": "eni discovered",
    "logger": "root",
    "requestId": "d2c2c2e2-a302-4d7b-bfc4-15120889df9e",
    "eniByAz": {
        "ap-northeast-1c": "eni-0ed1c0af1a3b2ca1f"
    }
}

{
    "timestamp": "2026-06-24T07:51:48Z",
    "level": "INFO",
    "message": "active az decided",
    "logger": "root",
    "requestId": "d2c2c2e2-a302-4d7b-bfc4-15120889df9e",
    "currentAz": "ap-northeast-1c",
    "activeAz": "ap-northeast-1c",
    "healthy": {
        "ap-northeast-1a": true,
        "ap-northeast-1c": true
    }
}

{
    "timestamp": "2026-06-24T07:51:49Z",
    "level": "INFO",
    "message": "routes synced",
    "logger": "root",
    "requestId": "d2c2c2e2-a302-4d7b-bfc4-15120889df9e",
    "target": "eni-0ed1c0af1a3b2ca1f",
    "activeAz": "ap-northeast-1c",
    "prevGatewayEni": "eni-0ed1c0af1a3b2ca1f"
}

{
    "timestamp": "2026-06-24T07:51:53Z",
    "level": "INFO",
    "message": "trigger",
    "logger": "root",
    "requestId": "3ffd1b2c-78ca-4b1b-8c5f-0f6b9deffc40",
    "detailType": "EC2 Instance Launch Successful"
}

{
    "timestamp": "2026-06-24T07:51:53Z",
    "level": "INFO",
    "message": "eni discovered",
    "logger": "root",
    "requestId": "3ffd1b2c-78ca-4b1b-8c5f-0f6b9deffc40",
    "eniByAz": {
        "ap-northeast-1a": "eni-0867e700a5e931690",
        "ap-northeast-1c": "eni-0ed1c0af1a3b2ca1f"
    }
}

{
    "timestamp": "2026-06-24T07:51:53Z",
    "level": "INFO",
    "message": "active az decided",
    "logger": "root",
    "requestId": "3ffd1b2c-78ca-4b1b-8c5f-0f6b9deffc40",
    "currentAz": "ap-northeast-1c",
    "activeAz": "ap-northeast-1c",
    "healthy": {
        "ap-northeast-1a": true,
        "ap-northeast-1c": true
    }
}

{
    "timestamp": "2026-06-24T07:51:54Z",
    "level": "INFO",
    "message": "routes synced",
    "logger": "root",
    "requestId": "3ffd1b2c-78ca-4b1b-8c5f-0f6b9deffc40",
    "target": "eni-0ed1c0af1a3b2ca1f",
    "activeAz": "ap-northeast-1c",
    "prevGatewayEni": "eni-0ed1c0af1a3b2ca1f"
}

{
    "timestamp": "2026-06-24T07:52:11Z",
    "level": "INFO",
    "message": "trigger",
    "logger": "root",
    "requestId": "d41c660d-a70d-4f3d-9fe1-097fd319a0a0",
    "detailType": "CloudWatch Alarm State Change"
}

{
    "timestamp": "2026-06-24T07:52:11Z",
    "level": "INFO",
    "message": "eni discovered",
    "logger": "root",
    "requestId": "d41c660d-a70d-4f3d-9fe1-097fd319a0a0",
    "eniByAz": {
        "ap-northeast-1a": "eni-0867e700a5e931690",
        "ap-northeast-1c": "eni-0ed1c0af1a3b2ca1f"
    }
}

{
    "timestamp": "2026-06-24T07:52:11Z",
    "level": "INFO",
    "message": "active az decided",
    "logger": "root",
    "requestId": "d41c660d-a70d-4f3d-9fe1-097fd319a0a0",
    "currentAz": "ap-northeast-1c",
    "activeAz": "ap-northeast-1c",
    "healthy": {
        "ap-northeast-1a": false,
        "ap-northeast-1c": true
    }
}

{
    "timestamp": "2026-06-24T07:52:12Z",
    "level": "INFO",
    "message": "routes synced",
    "logger": "root",
    "requestId": "d41c660d-a70d-4f3d-9fe1-097fd319a0a0",
    "target": "eni-0ed1c0af1a3b2ca1f",
    "activeAz": "ap-northeast-1c",
    "prevGatewayEni": "eni-0ed1c0af1a3b2ca1f"
}

{
    "timestamp": "2026-06-24T07:52:12Z",
    "level": "INFO",
    "message": "skip replace (within cooldown)",
    "logger": "root",
    "requestId": "d41c660d-a70d-4f3d-9fe1-097fd319a0a0",
    "az": "ap-northeast-1a",
    "instanceId": "i-03eba2ad858a1967d"
}

{
    "timestamp": "2026-06-24T07:52:41Z",
    "level": "INFO",
    "message": "trigger",
    "logger": "root",
    "requestId": "71dae08d-c7a0-439a-a513-d6dabcf9d62c",
    "detailType": "CloudWatch Alarm State Change"
}

{
    "timestamp": "2026-06-24T07:52:41Z",
    "level": "INFO",
    "message": "eni discovered",
    "logger": "root",
    "requestId": "71dae08d-c7a0-439a-a513-d6dabcf9d62c",
    "eniByAz": {
        "ap-northeast-1a": "eni-0867e700a5e931690",
        "ap-northeast-1c": "eni-0ed1c0af1a3b2ca1f"
    }
}

{
    "timestamp": "2026-06-24T07:52:41Z",
    "level": "INFO",
    "message": "active az decided",
    "logger": "root",
    "requestId": "71dae08d-c7a0-439a-a513-d6dabcf9d62c",
    "currentAz": "ap-northeast-1c",
    "activeAz": "ap-northeast-1c",
    "healthy": {
        "ap-northeast-1a": true,
        "ap-northeast-1c": true
    }
}

{
    "timestamp": "2026-06-24T07:52:42Z",
    "level": "INFO",
    "message": "routes synced",
    "logger": "root",
    "requestId": "71dae08d-c7a0-439a-a513-d6dabcf9d62c",
    "target": "eni-0ed1c0af1a3b2ca1f",
    "activeAz": "ap-northeast-1c",
    "prevGatewayEni": "eni-0ed1c0af1a3b2ca1f"
}

SNATインスタンスのiptablesのSNATエントリーを削除した場合

それでは、SNATインスタンスが生きているが、何かしらの原因でルーティングができなくなったシナリオでトライをします。

現在Activeなap-northeast-1cのSNATインスタンスでiptablesのSNATエントリーを削除します。

$ sudo grep 192.168.100.20 /proc/net/nf_conntrack
ipv4     2 icmp     1 29 src=10.10.0.16 dst=192.168.100.20 type=8 code=0 id=3 src=192.168.100.20 dst=10.10.0.85 type=0 code=0 id=3 mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2
ipv4     2 icmp     1 29 src=10.10.0.45 dst=192.168.100.20 type=8 code=0 id=4 src=192.168.100.20 dst=10.10.0.85 type=0 code=0 id=4 mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2

$ sudo iptables -t nat -S POSTROUTING
-P POSTROUTING ACCEPT
-A POSTROUTING -s 10.10.0.0/27 -d 192.168.100.0/24 -j SNAT --to-source 10.10.0.85
-A POSTROUTING -s 10.10.0.32/27 -d 192.168.100.0/24 -j SNAT --to-source 10.10.0.85

$ sudo iptables -t nat -F POSTROUTING
$ sudo iptables -t nat -S POSTROUTING
-P POSTROUTING ACCEPT

裏側でpingを流していたのですが、このタイミングで疎通できなくなりました。

しばらくするとCloudWatchアラームがアラーム状態となり、クライアントが属するサブネットのルートテーブルおよびVGWのルートテーブルにて、Standby側のENI = ap-northeast-1aのSNATインスタンスのENIに向くようになりました。

ap-northeast-1aのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.16

$ ping 192.168.100.20
PING 192.168.100.20 (192.168.100.20) 56(84) bytes of data.
64 bytes from 192.168.100.20: icmp_seq=1 ttl=125 time=4.52 ms
64 bytes from 192.168.100.20: icmp_seq=2 ttl=125 time=4.27 ms
64 bytes from 192.168.100.20: icmp_seq=3 ttl=125 time=4.28 ms
64 bytes from 192.168.100.20: icmp_seq=4 ttl=125 time=4.28 ms
64 bytes from 192.168.100.20: icmp_seq=5 ttl=125 time=4.25 ms
.
.
(中略)
.
.
64 bytes from 192.168.100.20: icmp_seq=381 ttl=125 time=4.20 ms
64 bytes from 192.168.100.20: icmp_seq=382 ttl=125 time=4.35 ms
64 bytes from 192.168.100.20: icmp_seq=383 ttl=125 time=4.17 ms
64 bytes from 192.168.100.20: icmp_seq=384 ttl=125 time=4.26 ms
64 bytes from 192.168.100.20: icmp_seq=385 ttl=125 time=4.13 ms






64 bytes from 192.168.100.20: icmp_seq=448 ttl=125 time=1.44 ms
64 bytes from 192.168.100.20: icmp_seq=449 ttl=125 time=1.37 ms
64 bytes from 192.168.100.20: icmp_seq=450 ttl=125 time=1.38 ms
64 bytes from 192.168.100.20: icmp_seq=451 ttl=125 time=1.30 ms
64 bytes from 192.168.100.20: icmp_seq=452 ttl=125 time=1.31 ms
64 bytes from 192.168.100.20: icmp_seq=453 ttl=125 time=1.29 ms
64 bytes from 192.168.100.20: icmp_seq=454 ttl=125 time=1.53 ms
ap-northeast-1cのクライアントEC2インスタンスからのアクセス
$ hostname -i
10.10.0.45

$ ping 192.168.100.20
PING 192.168.100.20 (192.168.100.20) 56(84) bytes of data.
64 bytes from 192.168.100.20: icmp_seq=1 ttl=125 time=2.82 ms
64 bytes from 192.168.100.20: icmp_seq=2 ttl=125 time=2.73 ms
64 bytes from 192.168.100.20: icmp_seq=3 ttl=125 time=2.73 ms
64 bytes from 192.168.100.20: icmp_seq=4 ttl=125 time=2.73 ms
64 bytes from 192.168.100.20: icmp_seq=5 ttl=125 time=2.82 ms
64 bytes from 192.168.100.20: icmp_seq=6 ttl=125 time=2.76 ms
.
.
(中略)
.
.
64 bytes from 192.168.100.20: icmp_seq=87 ttl=125 time=3.11 ms
64 bytes from 192.168.100.20: icmp_seq=88 ttl=125 time=2.89 ms
64 bytes from 192.168.100.20: icmp_seq=89 ttl=125 time=2.85 ms
64 bytes from 192.168.100.20: icmp_seq=90 ttl=125 time=2.81 ms
64 bytes from 192.168.100.20: icmp_seq=91 ttl=125 time=2.85 ms
64 bytes from 192.168.100.20: icmp_seq=92 ttl=125 time=2.79 ms
64 bytes from 192.168.100.20: icmp_seq=93 ttl=125 time=2.78 ms



64 bytes from 192.168.100.20: icmp_seq=156 ttl=125 time=2.87 ms
64 bytes from 192.168.100.20: icmp_seq=157 ttl=125 time=3.02 ms
64 bytes from 192.168.100.20: icmp_seq=158 ttl=125 time=2.96 ms
64 bytes from 192.168.100.20: icmp_seq=159 ttl=125 time=2.87 ms
64 bytes from 192.168.100.20: icmp_seq=160 ttl=125 time=2.89 ms
64 bytes from 192.168.100.20: icmp_seq=161 ttl=125 time=3.04 ms
64 bytes from 192.168.100.20: icmp_seq=162 ttl=125 time=3.00 ms
64 bytes from 192.168.100.20: icmp_seq=163 ttl=125 time=2.92 ms
64 bytes from 192.168.100.20: icmp_seq=164 ttl=125 time=4.41 ms
64 bytes from 192.168.100.20: icmp_seq=165 ttl=125 time=3.04 ms
64 bytes from 192.168.100.20: icmp_seq=166 ttl=125 time=3.20 ms
64 bytes from 192.168.100.20: icmp_seq=167 ttl=125 time=2.86 ms
64 bytes from 192.168.100.20: icmp_seq=168 ttl=125 time=6.38 ms

10.NAT停止後.png
11.CloudWatchアラーム.png

成功ですね。

単一のプライベートIPアドレスにSNATしたい場合に

接続元IPアドレスを絞りたいと言われた時用に特定のプライベートIPアドレスにSNATする仕組みを自作してみました。

単一のプライベートIPアドレスにSNATしたい場合に参考にしてみてください。

個人的には早くRegional Private NAT GatewayがGAされることを期待しています。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事