AWSの心臓部を暴く:ENIの構造とマルチIPアドレス割り当ての深淵
こんにちは。SREチームでインフラ基盤の設計・運用を見ているシニアエンジニアです。
クラウド上でWebアプリケーションを運用していて、こんな壁にぶつかったことはありませんか?
「単一のEC2インスタンス(あるいはKubernetesのノード)で、外部公開用と内部通信用で完全にIPを分けたい」
「マイクロサービスのコネクション枯渇を防ぐために、発信元IPを動的に切り替えたい」
「KubernetesのContainer Network Interface (CNI) が、内部でどうやってポッドにIPを配っているのかリアルな仕組みを知りたい」
これらすべてのパズルを解く鍵が、今回解説する ENI(Elastic Network Interface) と、そのマルチIPアドレス割り当ての仕組みです。教科書通りの「仮想のNICです」という説明だけでは、深夜のトラブルシューティングを乗り切ることはできません。パケットがハイパーバイザーの境界をどう越え、どのMACアドレスを頼りにルーティングされているのか。その深部まで潜ってみましょう。
—
1. ENIとは何か?:ベアメタルと仮想化を繋ぐレイヤー2の幻想
AWSのEC2インスタンスを立ち上げると、必ず1つ以上のENI(プライマリENI)がアタッチされます。このENIは、単なるソフトウェア上の抽象化された設定ではありません。物理的なホストサーバー(Nitroシステムなど)の物理NICと、仮想マシン(VM)のゲストOSを繋ぐ、きわめてハードウェアに近い仮想インターフェースです。
実務上、ENIを理解する上で外せない要素は以下の3点です。
- MACアドレスのバインド: 各ENIには、AWSによってユニークに発行されたMACアドレス(
01:23:45:67:89:abのような形式)が1対1でハードコードされます。OSからは単なる物理NICのように見えます。 - セキュリティグループの直結: ENI単位でセキュリティグループ(SG)をアタッチできます。インスタンス単位ではなくENI単位であるため、複数のENIを持つインスタンスでは、インターフェースごとに全く異なるファイアウォールルールを適用可能です。
- ライフサイクルの分離: ENIはインスタンスのライフサイクルから切り離して独立して存在できます。インスタンスがクラッシュしても、ENIを別のインスタンスに「アタッチし直す」ことで、IPアドレスやSGの設定を維持したままトラフィックを即座に引き継ぐ(フェイルオーバー)ことができます。
—
2. マルチIPアドレスの構造:プライマリとセカンダリのリアル
1つのENIには、インスタンスタイプに応じた数のIPアドレスを割り当てることができます。ここで重要になるのが、プライマリプライベートIPv4アドレスと、セカンダリプライベートIPv4アドレスの概念です。
[ AWS VPC ネットワーク ]
│
▼
┌───────────────────────────────────────────────┐
│ EC2 Instance │
│ ┌─────────────────────────────────────────┐ │
│ │ ENI (MAC: 0a:1b:2c:3d:4e:5f) │ │
│ │ ├── プライマリIP: 10.0.1.10 │ │
│ │ ├── セカンダリIP: 10.0.1.11 (API用) │ │
│ │ └── セカンダリIP: 10.0.1.12 (DB用) │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
プライマリとセカンダリの挙動の違い
- プライマリIP: ENIを作成した際に自動(または手動)で割り当てられ、ENIが存在する限り削除できません。インスタンスのメインの識別子として使われます。
- セカンダリIP: 1つのENIに対して複数追加・削除が可能なIPアドレスです。OSのネットワークスタックからは、同じMACアドレス(同じENI)に複数のIPがぶら下がっている状態に見えます。
ここで現場でよくある勘違いが、「セカンダリIPを追加しただけで、OSが勝手にそれを認識してルーティングしてくれる」という思い込みです。Linuxのカーネルから見ると、OS側の設定(ip addr や netplan、NetworkManagerなど)で明示的にセカンダリIPをインターフェースにバインドしてやらないと、AWS側でIPを追加してもOSはトラフィックを受け取れません。
—
3. 実践:マルチIP環境の構築とトラフィック制御
では、実際にAWS CLIとLinuxのコマンドを使って、マルチIP環境をセットアップし、特定のIPからリクエストを送信する検証をしてみましょう。
ステップ1: AWS CLIでセカンダリIPを追加する
まずは、既存のENI(あるいはインスタンス)にセカンダリIPをアタッチします。ここではインスタンスIDからENIのIDを特定し、IPを割り当てる手順をシミュレートします。
# 1. インスタンスにアタッチされているENIのIDを確認する
aws ec2 describe-instances \
--instance-ids i-0123456789abcdef0 \
--query "Reservations[*].Instances[*].NetworkInterfaces[*].NetworkInterfaceId" \
--output text
# 2. 特定のENIにセカンダリIPアドレスを動的に追加する
aws ec2 assign-private-ip-addresses \
--network-interface-id eni-0123456789abcdef0 \
--private-ip-addresses 10.0.1.50 10.0.1.51
ステップ2: Linux OS側でのIPアドレスの有効化
AWS側でIPを追加したら、SSHでEC2にログインし、OS側でそのIPを認識させます。ここではモダンなLinuxで使われる ip コマンドを使用します(プライマリが eth0 だと仮定します)。
# OSのネットワークインターフェースにセカンダリIPをバインドする
sudo ip addr add 10.0.1.50/24 dev eth0 label eth0:0
sudo ip addr add 10.0.1.51/24 dev eth0 label eth0:1
# 設定が正しく反映されたか確認する
ip addr show dev eth0
ステップ3: アプリケーションから発信元IPをバインドしてリクエストを送る
Web APIや外部サービスと通信する際、複数のセカンダリIPのうち「特定のIPアドレス」を送信元(Source IP)として固定したい場合があります。Pythonの requests や urllib、あるいは低レイヤーのソケットプログラミングでは、バインドするローカルIPを指定可能です。
以下は、Pythonで特定のセカンダリIP(10.0.1.50)を送信元IPとして固定し、HTTPリクエストを送信する実用的なコードスニペットです。
import socket
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.connection import create_connection
# カスタムアダプターを作成し、特定の送信元IPアドレスにバインドするソケットを生成する
class SourceIPAdapter(HTTPAdapter):
def __init__(self, source_ip, *args, **kwargs):
self.source_ip = source_ip
super().__init__(*args, **kwargs)
def init_poolmanager(self, *args, **kwargs):
# ソケット作成時にバインド元IPを指定するコールバックを仕込む
class SourceIPConnectionPool(requests.packages.urllib3.poolmanager.HTTPConnectionPool):
def __init__(self, *args, source_ip=None, **kwargs):
self.source_ip = source_ip
super().__init__(*args, **kwargs)
def _new_conn(self):
conn = super()._new_conn()
return conn
# 実際の発信元IPバインド処理を伴うコネクション生成関数
old_init_poolmanager = super().init_poolmanager(*args, **kwargs)
# ソケットのバインドを強制するカスタム関数を定義
def patched_create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None, **kw):
# ここで明示的にセカンダリIPを発信元として指定
return create_connection(address, timeout, source_address=(self.source_ip, 0), **kw)
# urllib3の内部関数をモンキーパッチ的に置き換えて送信元IPを固定
import urllib3.util.connection
urllib3.util.connection.create_connection = patched_create_connection
return super().init_poolmanager(*args, **kwargs)
if __name__ == "__main__":
# 割り当てたセカンダリIPの1つを指定
SOURCE_IP = "10.0.1.50"
TARGET_URL = "https://httpbin.org/ip"
try:
session = requests.Session()
session.mount("https://", SourceIPAdapter(SOURCE_IP))
session.mount("http://", SourceIPAdapter(SOURCE_IP))
response = session.get(TARGET_URL, timeout=5)
response.raise_for_status()
print(f"[成功] 送信元IP固定リクエスト完了")
print(f"レスポンス内容: {response.json()}")
except Exception as e:
print(f"[エラー] 通信に失敗しました: {e}", file=sys.stderr)
このコードを実行すると、AWSのENIに紐づいた特定のセカンダリIP(10.0.1.50)がパケットのソースIPとしてパブリックインターネット(あるいはVPC内)に出ていくことになります。APIのレートリミット回避や、IPベースのホワイトリスト制限をかけるシステムとの連携において、極めて強力な武器になります。
—
4. トラブルシューティングの現場から:よくある罠と回避策
最後に、実務の現場で私が何度も遭遇した「ENIとマルチIPに関するハマりポイント」を共有します。
罠1: 非対称ルーティング(Asymmetric Routing)の悲劇
複数のENIを1つのインスタンスにアタッチし、それぞれに異なるサブネット(別々のルートテーブル)のIPを割り当てたときによく起こります。
- 症状: パケットは
eth1から入ってきたのに、OSのデフォルトルートがeth0を向いているため、レスポンスがeth0から出ていってしまい、セキュリティグループやVPCのルーターにパケットを捨てられる。 - 対策: ポリシーベースルーティング(
ip ruleとip route)を使い、受信したパケットのIPアドレスに応じて、どのインターフェースから応答を返すかを明示的にルーティングテーブルに書き込む必要があります。KubernetesのCNI(AWS VPC CNIなど)はこの複雑なルーティング制御を内部で自動化しています。
罠2: セキュリティグループの適用漏れ
「セカンダリIPを追加したのに、通信がブロックされる」という相談を受けます。
- 原因: セキュリティグループはENI単位で適用されます。追加したセカンダリIPは「既存のENI」に追加されるため、そのENIにアタッチされているセキュリティグループのルールがそのまま適用されます。もしセカンダリIPごとに異なるインバウンド/アウトバウンド制御をしたい場合は、同じENIに詰め込むのではなく、「新しいENIをもう一枚アタッチする」という設計アプローチをとるのが正解です。
—
まとめ
ENIの構造とマルチIPアドレスの割り当ては、一見すると地味な低レイヤーの機能に思えますが、クラウドネイティブなアーキテクチャの根幹を支える重要な技術です。
単に「IPが足りなくなったら増やす」のではなく、パケットのフロー、OSのルーティングテーブル、そしてアプリケーション層からのバインド制御までを一気通貫で理解しておくことで、複雑なクラウドネットワークのトラブルシューティングも怖くなくなります。
明日のインフラ設計やコンテナ基盤のチューニングに、ぜひこの知識を役立ててください。それでは、良きSREライフを!
コメント