こんにちは、SREチームのシニアアーキテクトです。
クラウドのインフラ設計を長年やっていると、「1台のEC2インスタンスに複数のENI(Elastic Network Interface)をぶっ刺して、トラフィックを綺麗にルーティングしたい」という要件に必ずと言っていいほどぶつかります。例えば、社内セキュアネットワーク(閉域網)とパブリックインターネット向けのインターフェースを1台の踏み台サーバーやプロキシで綺麗に分離したい場合や、強烈なスループットを要求されるミドルウェアサーバーでトラフィックを分散させたいケースですね。
AWSのコンソールでポチポチとENIを追加し、OS側でIPアドレスを設定して「よし、これで通信できるはず!」とテストしてみる。しかし、現実はそう甘くありません。パブリック側から叩いたはずなのに応答が返ってこない、あるいはセカンダリENI宛ての通信がなぜかプライマリのゲートウェイに吸い込まれて捨てられる……。
夜中にそんな不可解なパケットの迷子に直面し、冷や汗をかいた経験はありませんか?
今回は、AWSの複数ENI構成(マルチホーム構成)におけるLinuxのルーティングの罠と、それを華麗に解決するポリシーベースルーティング(ip rule と ip route)の極意を、現場のリアルな知見と共にお伝えします。
—
なぜ複数ENIの構成でパケットが迷子になるのか?
まず、Linuxカーネルのデフォルトの挙動を理解することから始めましょう。
1台のEC2インスタンスにプライマリENI(eth0)とセカンダリENI(eth1)をアタッチしたとします。AWS側ではそれぞれのENIに対して異なるサブネット、異なるIPアドレス、そしてそれぞれのサブネット用のデフォルトゲートウェイが存在します。
しかし、Linux OS(Amazon Linux 2やUbuntuなど)が標準で持っているメインルーティングテーブル(ip route show で確認できるもの)は、原則として1つのデフォルトゲートウェイしか持ちません。
非対称ルーティング(Asymmetric Routing)の悲劇
ここに外部からセカンダリENI(eth1)のIPアドレス宛てにパケットが飛んできたとしましょう。
パケットは無事に eth1 に到着し、アプリケーション(例えばPythonのWebサーバーやAPI)に到達します。ここまでは順調です。
問題はここから。アプリケーションがその応答パケット(レスポンス)をクライアントビルダーに送り返そうとするとき、OSのカーネルはこう考えます。
「お、外向きのパケットだな。じゃあいつものようにメインテーブルのデフォルトゲートウェイ(つまり eth0 のゲートウェイ)経由で外に投げよう」
これが非対称ルーティングの罠です。
セカンダリENI(eth1)から入ってきたのに、応答がプライマリENI(eth0)から出ていこうとする。AWSのVPCネットワークはセキュリティ上、ENIに割り当てられたIPアドレスと異なる送信元IPアドレスを持つパケットや、不自然なルーティング経路を通るパケットを「スプーフィング(なりすまし)」とみなして容赦なくドロップ(破棄)します。
結果として、クライアント側にはタイムアウト(ETIMEDOUT)だけが冷たく返ってくるというわけです。
—
解決の切り札:ポリシーベースルーティング(PBR)
この問題を解決するのが、Linuxのポリシーベースルーティング(Policy-Based Routing: PBR)です。
「どのIPアドレス(送信元)から出てきたパケットか」によって、参照するルーティングテーブルを動的に切り替える仕組みを作ります。「eth1 のIPアドレスを持つパケットなら、eth1 専用のルーティングテーブルを見に行け」とカーネルに厳命するわけです。
これを実現するために使うのが、ip rule(ルールの定義)と ip route(カスタムルーティングテーブルの作成)という2つの強力なコマンドです。
—
実践:マルチホーム構成のルーティング設定手順
ここでは、Amazon Linux 2023を想定し、プライマリENI(eth0)に加えてセカンダリENI(eth1)を追加した環境を例に、具体的な設定手順をハンズオン形式で解説します。
1. 前提となるネットワーク環境のイメージ
- プライマリENI (
eth0): - IPアドレス:
10.0.1.10 - サブネットのデフォルトゲートウェイ:
10.0.1.1 - セカンダリENI (
eth1): - IPアドレス:
10.0.2.20 - サブネットのデフォルトゲートウェイ:
10.0.2.1
2. カスタムルーティングテーブルの定義
まずは、eth1 専用のルーティングテーブルを作成します。Linuxでは /etc/iproute2/rt_tables に独自のテーブル名(番号付き)を登録することで、名前でテーブルを管理できるようになります。
# /etc/iproute2/rt_tables にカスタムテーブルを追加する
sudo sh -c 'echo "200 custom_eth1" >> /etc/iproute2/rt_tables'
これで、custom_eth1 という名前のテーブル(ID: 200)が使用可能になりました。
3. カスタムテーブルへのルート追加
次に、custom_eth1 テーブルに対して、セカンダリENI用のローカルネットワーク経路と、デフォルトゲートウェイを設定します。
# セカンダリサブネット向けの直結ルートを追加
sudo ip route add 10.0.2.0/24 dev eth1 table custom_eth1
# セカンダリENI用のデフォルトゲートウェイを追加
sudo ip route add default via 10.0.2.1 dev eth1 table custom_eth1
4. ポリシー(ip rule)の設定
ここが肝心です。「10.0.2.20 という送信元IPアドレスを持つパケットは、先ほど作った custom_eth1 テーブルを参照しなさい」というルールを追加します。
# 送信元IPベースのルーティングルールを追加
sudo ip rule add from 10.0.2.20 table custom_eth1
# 受信パケットの逆引き用ルール(必要に応じて)
sudo ip rule add to 10.0.2.20 table custom_eth1
これで、OSはセカンダリENIのIPアドレスを送信元とするパケットを正しく eth1 のゲートウェイへとルーティングできるようになりました。
—
永続化の罠:再起動で消える設定にどう立ち向かうか?
ここまでで手動設定は完了ですが、SREとして最も恐ろしいのは「インスタンスの再起動やオートスケーリングによる再作成時に設定が吹き飛ぶこと」です。
Systemd-networkdやNetworkManagerが主流となったモダンなLinuxディストリビューションでは、ネットワークスクリプトの書き方も進化しています。Amazon Linux 2やUbuntuで確実に永続化させるためのアプローチを見ていきましょう。
NetworkManagerを利用する場合の設定例
多くのモダンな環境では NetworkManager が動いています。eth1 に対するディスパッチスクリプトや、接続プロファイル設定でルーティングを維持させます。
例えば、/etc/sysconfig/network-scripts/route-eth1(環境による)または NetworkManager の設定ファイルに以下のように記述します。
# /etc/sysconfig/network-scripts/route-eth1 の例
# (古いifcfgフォーマットをサポートしている環境の場合)
ADDRESS0=0.0.0.0
NETMASK0=0.0.0.0
GATEWAY0=10.0.2.1
TABLE=200
しかし、クラウドネイティブな環境においては、Cloud-init や起動時スクリプト(Userdata)、あるいは Configuration Management(Ansible等)を組み合わせて、インスタンス起動時に自動で上記の ip rule / ip route コマンドが流し込まれる仕組みを構築するのが最も確実でトラブルが少ないです。
以下は、Pythonやシェルスクリプトで死活監視と同時にルーティングを治す、あるいは起動時に実行するインフラ構成の鉄板スニペットです。
import subprocess
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def apply_policy_routing():
"""
マルチホーム構成におけるセカンダリENI用のポリシーベースルーティングを動的に適用する
"""
rules = [
"ip route add 10.0.2.0/24 dev eth1 table custom_eth1 2>/dev/null || true",
"ip route add default via 10.0.2.1 dev eth1 table custom_eth1 2>/dev/null || true",
"ip rule add from 10.0.2.20 table custom_eth1 2>/dev/null || true"
]
for cmd in rules:
logger.info(f"Executing: {cmd}")
subprocess.run(cmd, shell=True, check=False)
if __name__ == "__main__":
apply_policy_routing()
—
現場のトラブルシューティング:パケットはどこで消えているか?
もし上記の設定を行っても通信がうまくいかない場合、シニアエンジニアは次のような手順でデバッグを行います。
1. tcpdump でパケットの生死を確認する
プライマリとセカンダリ、両方のインターフェースでパケットがどのように流れているかを同時にキャプチャします。
# セカンダリENI側でICMPやHTTPリクエストが到達しているか、応答が出てい出ているか確認
sudo tcpdump -nn -i eth1 host 10.0.2.20
ここで、リクエストは来ているのに、パケットが eth0 側から出ていろうとしていないか(あるいは eth1 から出ているのに返事がないか)を確認します。
2. AWS側のセキュリティグループとルートテーブルの再確認
OS側の設定が完璧でも、AWS側で以下の見落としがよくあります。
- セカンダリENIにアタッチされたセキュリティグループが、インバウンド/アウトバウンドの通信をブロックしていないか。
- 送信元/宛先チェック(Source/Destination Check)が有効になっていないか。
- *Note*: インスタンスがルーターやNATとして振る舞う場合や、特定のENIでルーティングを行う場合、AWSのENI設定で「送信元/宛先チェックの無効化(Disable source/destination check)」をポチっとオフにしておく必要があります。これを忘れると、AWSのハイパーバイザーレイヤーでパケットが問答無用で捨てられます。
3. ルーティングテーブルの評価順序(優先度)の確認
ip rule show を実行し、ルールが意図した優先度(プレフィックスの数値)で評価されているか確認してください。
$ ip rule show
0: from all lookup local
32765: from 10.0.2.20 lookup custom_eth1
32766: from all lookup main
32767: from all lookup default
数値が小さいほど優先度が高くなります。カスタムルールが main テーブルよりも上にきていることが、正常なマルチホーム通信の絶対条件です。
—
まとめ
複数ENIを駆使したマルチホーム構成は、ネットワークの分離や可用性向上において非常に強力な武器になります。しかし、Linuxカーネルのデフォルトルーティングの思想とAWSの仮想ネットワークの仕様の狭間で、一筋縄ではいかないトラブルを生み出す原因にもなります。
「パケットはどこから来て、どこへ帰ろうとしているのか?」
この原点を常に頭に置き、ポリシーベースルーティング(ip rule / ip route)とAWS側の送信元/宛先チェックの無効化を正しく組み合わせることで、どんな複雑なネットワーク要件も確実にねじ伏せることができます。
現場のインフラ運用の参考になれば幸いです。それでは、また次回の技術ブログでお会いしましょう!
コメント