背景
コスト削減の一環として、非本番環境のVPCを毎日自動で削除・再作成する運用を行っています。
ただ、この運用にはひとつ厄介な点があります。VPCを作り直すたびにVPC IDが変わるため、アカウントをまたぐVPC Peeringも毎回作り直さなければならないことです。
同一アカウント内であれば比較的単純ですが、アカウントをまたぐPeeringでは、次の作業を分けて行う必要があります。
- リクエスターアカウント: VPC Peering Connectionを作成
- アクセプターアカウント: VPC Peering Connectionを承認
- 両アカウント: それぞれのルートテーブルに相手側VPCへの経路を追加
マネジメントコンソールを行き来しながら都度手作業で行うのは手間が大きく、作業漏れも起きやすくなります。そこで、CodeBuildを用いてワンクリックで安全に実行できる仕組みを構築しました。
本記事の手順は、リクエスター・アクセプター双方が同一リージョン(例: ap-northeast-1)に存在することを前提としています。
構成の概要
基本の考え方はシンプルです。リクエスター側アカウントでCodeBuildを動かし、そのジョブがアクセプター側アカウントのロールを一時的に借りることで、1回の実行で両アカウントの操作を完結させます。
[リクエスター側アカウント] [アクセプター側アカウント]
CodeBuild (DeployerRole)
│
├─ ① STS AssumeRole ──────────────────────────> AcceptorRole
├─ ② リクエスター側のVPC情報を取得
├─ ③ アクセプター側のVPC情報を取得 (--profile)
├─ ④ VPC Peeringを作成(リクエスト)
├─ ⑤ VPC Peeringを承認 (--profile) ──────────> AcceptVpcPeeringConnection
├─ ⑥ リクエスター側のルートテーブルに経路追加
└─ ⑦ アクセプター側のルートテーブルに経路追加 (--profile)
IAMロールの設計
必要以上の権限付与を避け、役割に応じた制限を意識してクロスアカウント用のIAMロールを設計します。
(※リクエスター側は動的に変わるVPC/ルートテーブルを操作する都合上、一部リソース制限をワイルドカードにしていますが、実運用に合わせて適宜タグ制限などを追加してください)
1. リクエスター側: VpcPeeringDeployerRole
CodeBuildのサービスロールです。自アカウントのVPC・ルートテーブルの操作権限に加え、アクセプター側の VpcPeeringAcceptorRole への sts:AssumeRole 権限を付与します。
インラインポリシー:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeVpcs",
"ec2:DescribeRouteTables",
"ec2:CreateVpcPeeringConnection",
"ec2:CreateRoute"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<アクセプター側アカウントID>:role/VpcPeeringAcceptorRole"
}
]
}
2. アクセプター側: VpcPeeringAcceptorRole
リクエスター側の DeployerRole からの AssumeRole のみを受け付けるロールです。不用意な操作を防ぐため、ec2:CreateRoute には Condition を設定し、特定のNameタグを持つルートテーブルのみを操作できるよう制限しています。
インラインポリシー:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AcceptVpcPeeringConnection",
"ec2:DescribeVpcs",
"ec2:DescribeRouteTables",
"ec2:CreateTags"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:CreateRoute"
],
"Resource": "arn:aws:ec2:*:*:route-table/*",
"Condition": {
"StringLike": {
"ec2:ResourceTag/Name": [
"example-project/vpc/private-subnet-*"
]
}
}
}
]
}
信頼関係 (Trust Relationship):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<リクエスター側アカウントID>:role/VpcPeeringDeployerRole"
},
"Action": "sts:AssumeRole"
}
]
}
事前準備 (CodeBuildの環境変数)
CodeBuildプロジェクトを作成する際は、以下の環境変数(Environment variables)を事前に設定してください。
| 変数名 | 説明 | 例 |
|---|---|---|
| ACCEPTOR_ACCOUNT_ID | アクセプター側のAWSアカウントID | 123456789012 |
| PEERING_NAME | 作成するVPC Peering Connectionに付与するNameタグ | example-peering |
| REQUESTER_VPC_NAME | リクエスター側VPCのNameタグ | example-requester-project/vpc |
| ACCEPTOR_VPC_NAME | アクセプター側VPCのNameタグ | example-acceptor-project/vpc |
CodeBuildの構築スクリプト(buildspec.yml)
CodeBuildで実行する処理の全容です。すでに存在する場合は処理をスキップし、何度実行してもエラーにならない安全な設計にしています。
version: 0.2
phases:
pre_build:
commands:
# AWS CLIのデフォルトリージョンを設定(環境に合わせて変更してください)
- export AWS_DEFAULT_REGION="ap-northeast-1"
# 1. アクセプター側のロールを引き受けて一時認証情報を取得
- ROLE_ARN="arn:aws:iam::${ACCEPTOR_ACCOUNT_ID}:role/VpcPeeringAcceptorRole"
- CRED=$(aws sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "CrossAccountPeeringSession")
# 2. アクセプター側操作用のAWS CLIプロファイル(acceptor-profile)を作成
- aws configure set aws_access_key_id $(echo $CRED | jq -r '.Credentials.AccessKeyId') --profile acceptor-profile
- aws configure set aws_secret_access_key $(echo $CRED | jq -r '.Credentials.SecretAccessKey') --profile acceptor-profile
- aws configure set aws_session_token $(echo $CRED | jq -r '.Credentials.SessionToken') --profile acceptor-profile
# 3. 意図したアカウントに切り替わっているかダブルチェック
- CALLER_ACCOUNT_ID=$(aws sts get-caller-identity --profile acceptor-profile --query 'Account' --output text)
- |
if [ "$CALLER_ACCOUNT_ID" = "$ACCEPTOR_ACCOUNT_ID" ]; then
echo "Success: acceptor-profile points to expected account: $CALLER_ACCOUNT_ID"
else
echo "Error: Account mismatch. expected=$ACCEPTOR_ACCOUNT_ID actual=$CALLER_ACCOUNT_ID"
exit 1
fi
build:
commands:
# 二重実行の防止: 既存のPeeringが存在すれば終了
- |
active_peering_ids=$(aws ec2 describe-vpc-peering-connections \
--filters "Name=tag:Name,Values=${PEERING_NAME}" \
--query "VpcPeeringConnections[?contains(['active','provisioning','pending-acceptance','initiating-request'], Status.Code)].VpcPeeringConnectionId" \
--output text)
if [ -n "$active_peering_ids" ]; then
echo "VPC peering already exists: $active_peering_ids. Exiting."
exit 1
fi
# 両アカウントのVPC情報(IDとCIDR)を取得
- REQUESTER_VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=${REQUESTER_VPC_NAME}" --query "Vpcs[0].VpcId" --output text)
- REQUESTER_VPC_CIDR=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=${REQUESTER_VPC_NAME}" --query "Vpcs[0].CidrBlock" --output text)
- ACCEPTOR_VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=${ACCEPTOR_VPC_NAME}" --query "Vpcs[0].VpcId" --output text --profile acceptor-profile)
- ACCEPTOR_VPC_CIDR=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=${ACCEPTOR_VPC_NAME}" --query "Vpcs[0].CidrBlock" --output text --profile acceptor-profile)
# VPC Peeringの作成(リクエスト)
- |
PEERING_ID=$(aws ec2 create-vpc-peering-connection \
--vpc-id ${REQUESTER_VPC_ID} \
--peer-vpc-id ${ACCEPTOR_VPC_ID} \
--peer-owner-id ${ACCEPTOR_ACCOUNT_ID} \
--tag-specifications "ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=${PEERING_NAME}}]" \
--query 'VpcPeeringConnection.VpcPeeringConnectionId' \
--output text)
# VPC Peeringの承認(アクセプター側でAccept)
- aws ec2 accept-vpc-peering-connection --vpc-peering-connection-id ${PEERING_ID} --profile acceptor-profile
# リクエスター側のルートテーブル(プライベートサブネット)に対象経路を追加
- |
REQUESTER_ROUTE_TABLE_IDS=$(aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=${REQUESTER_VPC_ID}" "Name=tag:Name,Values=${REQUESTER_VPC_NAME}/private-subnet-*" \
--query "RouteTables[].RouteTableId" --output text)
for rt_id in $REQUESTER_ROUTE_TABLE_IDS; do
aws ec2 create-route --route-table-id $rt_id --destination-cidr-block $ACCEPTOR_VPC_CIDR --vpc-peering-connection-id $PEERING_ID
echo "Route created in Requester route table $rt_id to Acceptor VPC CIDR $ACCEPTOR_VPC_CIDR via peering connection $PEERING_ID"
done
# アクセプター側のルートテーブル(プライベートサブネット)に対象経路を追加
- |
ACCEPTOR_ROUTE_TABLE_IDS=$(aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=${ACCEPTOR_VPC_ID}" "Name=tag:Name,Values=${ACCEPTOR_VPC_NAME}/private-subnet-*" \
--query "RouteTables[].RouteTableId" --output text --profile acceptor-profile)
for rt_id in $ACCEPTOR_ROUTE_TABLE_IDS; do
aws ec2 create-route --route-table-id $rt_id --destination-cidr-block $REQUESTER_VPC_CIDR --vpc-peering-connection-id $PEERING_ID --profile acceptor-profile
echo "Route created in Acceptor route table $rt_id to Requester VPC CIDR $REQUESTER_VPC_CIDR via peering connection $PEERING_ID"
done
まとめ
本仕組みを導入したことで、毎日VPCがスクラップ&ビルドされる環境であっても、開発者はCodeBuildの 「ビルドを開始」ボタンを押すだけ で、安全かつ瞬時にアカウント間のネットワーク経路を復旧できるようになりました。
今回の運用のポイントは以下の通りです。
- クロスアカウントの自動化: aws sts assume-role で取得した一時認証情報をAWS CLIのプロファイルに切り替えることで、単一のスクリプトから複数アカウントを安全に操作可能
- セキュリティの考慮: アクセプター側の権限は Condition タグを用いて操作可能なルートテーブルを厳密に絞り込み、不用意なルート追加を防ぐ(※リクエスター側も運用の要件に応じて、同様のタグ制限を入れるとより安全です)
- 安全な設計: 事前の存在チェックを入れることで、意図しない二重実行によるエラーやルートの重複登録を防ぐ
- シンプルなデプロイ構成: リクエスター側にCodeBuild、アクセプター側にIAMロールをそれぞれ通常のCDKスタックとして個別にデプロイするだけで実現可能
マルチアカウント環境でのネットワーク自動化を検討している方の参考になれば幸いです。