AWSでインスタンスに合わせてルートテーブルを自動変更したかった話
この記事は、「Unagi-Network Advent Calendar 2023」17日目の記事になります。
Unagi-Network Advent Calendar 2023 - Adventar
3年ほど記事を更新していませんでしたが、Advent Calenderにお誘いいただいたので、久々に更新しようと思います。
注意点
AWS初心者なので、そもそもの前提を含めて、もっと良い解決方法があるかもしれません。
要約
LambdaとEventBridgeを使い、検証用インスタンスが立ち上がる度にルートテーブルを自動で変更する仕組みを作成しました。また、今後同じ人がもし万が一存在していた場合のため、それまでの過程や失敗談をまとめました。
背景
とある日、検証のために1つのAWS VPC上で「インスタンスA←→検証用インスタンス←→インスタンスB」の通信を行う必要性が出たため、下記のVPC環境を用意しました。
この時、VPCのルートテーブルは、デフォルトでは「同一VPC内のインスタンス間の通信は、インスタンス間で直接通信が行われる」仕様になっているため、「インスタンスA←→インスタンスB」の通信になってしまいます。そこで、VPCのルートテーブルにおいて、「Public Subnet 2/3において、Public Subnet 1/2/3の通信は全て検証用インスタンスのネットワークインターフェイスを経由する」ように設定しました。
当時は検証用インスタンスが1つだったので問題なかったのですが、その後検証用インスタンスが2つ以上になったとき、下記の問題が生じ始めたため、何とかしないとなぁという状況になりました。
- 検証用インスタンス1を検証しようと思ったら、ルートテーブルが検証用インスタンス2用に設定されており、上手く検証ができない
- 検証用インスタンス1と検証用インスタンス2を数日ごとに変更しながら検証を行う際、ルートテーブルを変更するのに時間がかかる
この解決策として、EventBridgeで検証用インスタンスが立ち上がったことを検知し、Lambdaで立ち上がった検証用インスタンス用にルートテーブルを動的に変更しよう、となりました。例として、検証用インスタンス2を起動した際の挙動を下図に示します。
解決策の流れ
解決策を実施するにあたり、下記の流れで進めていきました。
- Lambdaを実行する際に用いるIAMロールの作成
- Lambdaの作成
- EventBridgeの作成
- 確認
IAMロールの作成
Lambdaでは、ルートテーブルの変更に加え、CloudWatchログへのロギングも実施します。それに合わせて、IAMロールAWSLambdaBasicExecutionRoleForNWPRoductsを作成します。今回は、それぞれ下記のIAMポリシーをアタッチしています。なお、最小権限で許可したほうが良いので、今回の権限は非常に悪い例です…
- ルートテーブルの変更:AmazonEC2FullAccess
- CloudWatchログへのロギング:下記IAMポリシー
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:*:*:log-group:/aws/lambda/*:*" ] } ] }
Lambdaの作成
下記の設定に従ってLambdaに関数Replace-Routesを作成しました。
設定項目 | 設定値 |
---|---|
関数の作成 | 一から作成 |
関数名 | Replace-Routes |
ランタイム | Python 3.12 |
アーキテクチャ | x86_64 |
実行ロール | 既存のロールを使用する |
既存のロール | AWSLambdaBasicExecutionRoleForNWPRoducts(IAMロールの作成で作成したIAMロール) |
Replace-Routes関数の作成後、一般設定から関数のタイムアウト時間を3秒から30秒に変更しました。下記に記載しているプログラムだと、4~5秒程度かかるようで、タイムアウト時間を延ばさないとタイムアウトによりエラーが発生します。
最後に、作成されたReplace-Routes関数のlambda_function.pyを、下記のように設定して「Deploy」ボタンを押下しました。この時、ルートテーブルIDやプライベートIPレンジ等は各自で修正して利用してください。*1
import os import sys import json import boto3 import pprint from datetime import date, datetime def lambda_handler(event, context): ec2 = boto3.client('ec2', region_name='ap-northeast-1') # get netowk interfaces instances = ec2.describe_instances(InstanceIds=[event['detail']['instance-id']]) instance = instances['Reservations'][0]['Instances'][0] interfaces = instance['NetworkInterfaces'] # get route tables routetables = ec2.describe_route_tables(RouteTableIds=['Public Subnet 2のルートテーブルID', 'Public Subnet 3のルートテーブルID']) # change routes for routetable in routetables['RouteTables']: # extract interface routesubnet = routetable['Associations'][0]['SubnetId'] interface = [i for i in interfaces if i['SubnetId'] == routesubnet] if len(interface) != 1: continue interface = interface[0] #replace routes routes = [i for i in routetable['Routes'] if 'DestinationCidrBlock' in i] for route in routes: if route['DestinationCidrBlock'] in ['10.0.0.0/24', '10.0.1.0/24', '10.0.2.0/24']: response = ec2.replace_route( DestinationCidrBlock=route['DestinationCidrBlock'], NetworkInterfaceId=interface['NetworkInterfaceId'], RouteTableId=routetable['Associations'][0]['RouteTableId'] ) print(json.dumps(response, default=json_serial)) return 0 def json_serial(obj): if isinstance(obj, (datetime, date)): return obj.isoformat() raise TypeError ('Type %s not serializable' % type(obj))
EventBridgeの作成
下記の設定に従ってEventBridgeにルールInstance-Runningを作成しました。なお、ステップ自体は5までありますが、ステップ4(タグを設定)とステップ5(レビューと作成)は何もせず進めています。
ステップ1 ルールの詳細を定義
設定項目 | 設定値 |
---|---|
名前 | Instance-Running |
イベントパス | default |
選択したイベントパスでルールを有効にする | 有効化 |
ルールタイプ | イベントパターンを持つルール |
ステップ2 イベントパターンを構築
設定項目 | 設定値 |
---|---|
イベントソース | AWSイベントまたはEventBridgeパートナーイベント |
作成のメソッド | パターンフォームを使用する |
イベントソース | AWSのサービス |
AWSのサービス | EC2 |
イベントタイプ | EC2 Instance State-change-Notification |
イベントタイプの仕様 1 | 特定の状態 |
特定の状態 | running |
イベントタイプの仕様 2 | 個別のインスタンスID |
個別のインスタンスID | 検証用インスタンスのインスタンスID |
ステップ3 ターゲットを選択
設定項目 | 設定値 |
---|---|
ターゲットタイプ | AWSのサービス |
ターゲットを選択 | Lambda 関数 |
関数 | Replace-Routes(Lambdaの作成で作成したLambda関数) |
設定の確認
検証用インスタンス1を経由して通信が行われるようにルートテーブルが設定されている状態で、検証用インスタンス2が立ち上がると、ルートテーブルが検証用インスタンス2を経由して通信が行われるように自動で設定されることを確認します。
現在、Public Subnet 2/3のルートテーブルは下記のように設定されています。
- Public Subnet 2のルートテーブル:Public Subnet 1/2/3のプライベートIPレンジが送信先である場合、「eni-07b7~」(検証用インスタンス1のネットワークインターフェイス)へ送付
- Public Subnet 3のルートテーブル:Public Subnet 1/2/3のプライベートIPレンジが送信先である場合、「eni-0299~」(検証用インスタンス1のネットワークインターフェイス)へ送付
この状態で、検証用インスタンス2を立ち上げてしばらくすると、Public Subnet 2/3のルートテーブルが下記のように設定されたため、正常に動作しています。
- Public Subnet 2のルートテーブル:Public Subnet 1/2/3のプライベートIPレンジが送信先である場合、「eni-0dda~」(検証用インスタンス2のネットワークインターフェイス)へ送付
- Public Subnet 3のルートテーブル:Public Subnet 1/2/3のプライベートIPレンジが送信先である場合、「eni-02be~」(検証用インスタンス2のネットワークインターフェイス)へ送付
解決策以外の没案
ここでは、上記の解決策に至るまでの没案を2つほど記載します。
マネージドプレフィックスリストの活用
AWS VPCには、下記のように複数のIPレンジを1つにまとめて定義可能なマネージドプレフィックスリストが存在します。
マネージドプレフィックスリストは、ルートテーブルの送信先としても定義することができる*2上、ルートがランダムに選択されるという記載があったため、下記等のように設定すれば、ランダム性を利用(悪用)してうまく通信できるようになるかと試行錯誤していました。
送信先 | ターゲット |
---|---|
Public Subnet 1/2/3のプライベートIPレンジを定義したマネージドプレフィックスリスト | 検証用インスタンス1のネットワークインターフェイス |
Public Subnet 1/2/3のプライベートIPレンジを定義したマネージドプレフィックスリスト | 検証用インスタンス2のネットワークインターフェイス |
・ルートテーブルで複数のプレフィックスリストが参照されていて、異なるターゲットへの CIDR ブロックが重複する場合、優先されるルートはランダムに選択されます。その後は、同じルートが常に優先されます。
しかし、最終的に下記のルールがあることから、どう頑張っても送信先「10.0.0.0/20」のルールが優先されるため、プライベートIPレンジ経由では使えないことが分かりました。
・ルートテーブルに、プレフィックスリストを持つ静的ルートと重複する送信先の CIDR ブロックを持つ静的ルートが含まれている場合、CIDR ブロックを持つ静的ルートが優先されます。
ルーティングプロトコルの活用
RIPやOSPF等のルーティングプロトコルを活用して、検証用インスタンス1が立ち上がっている場合は検証用インスタンス1、検証用インスタンス2が立ち上がっている場合は検証用インスタンス2、をそれぞれ経由する仕組みを作成できないか検討していました。結果として、できないことはないのですが、AWSの費用が高くなってしまうため諦めました。
さいごに
今回の記事のように、AWSでもこういった物理的なネットワーク構成をある程度模倣することが可能です。(物理で考えると、検証用の製品を変更する際にLANケーブルを抜き差しするのが面倒なので、自動でLANケーブルを抜き差しするようにした、みたいなのが今回の記事のイメージです)物理/仮想/クラウド等のどの環境で検証/実装するか、みたいな話は様々な側面で検討する必要があると思いますが、AWSだとAWS Marketplace Subscriptionに存在する各種ライセンスを時間単位の課金で検証できるという利点もあるので、今回の記事のような需要もあるのかな、と思っています。*3
今回の記事のような事例は非常に少ないと思いますが、もし同じような状況に陥った人は参考にしていただければ幸いです。ちなみに、こんなことやらなくても解決できる方法があれば教えてください…
余談
上みたいな感じで個人環境で検証用インスタンスの検証等を行っていたら、11月のAWS費が過去最高になっていました…AWSの使い過ぎには気を付けよう
*1:json_serialは下記リンク先の記事を参考にしています。
https://www.yoheim.net/blog.php?q=20170703
*2:https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_Route_Tables.html
*3:物理機器だと機器代+年月単位のライセンス代、VMだと年月単位のライセンス代、がそれぞれかかるため、インスタンス関連費+時間単位のライセンス代で済ませられるクラウドは検証も安く済ませられたりします。