はじめに
開発環境のコスト削減のため、毎日深夜に環境を自動削除する運用をしています。
削除ジョブはCodeBuildから実行し、CloudFormationスタックを順番に落としていく構成です。
当初は「サービス基盤のスタックを消せばまるごとVPCまで消える」と考えていました。
しかし実際には、VPC削除を阻害する依存リソースが残っており、スタックにVPCが残った状態で失敗していました。
この記事では、CodeBuildで順序制御を行い、スタック削除を阻む依存リソースを確実に削除する実装について解説します。
背景
- マルチアカウントでそれぞれサービスを稼働
- サービス間でVPC Peeringを構成
- 開発環境は毎日削除し、必要時に再作成する運用
- インフラ定義はCloudFormationをCDKで管理
- サービス基盤のスタックでは以下が管理されている
- VPC本体
- subnet
- NAT Gateway
- サービスで使用するVPC Endpoint
- 上に必要なセキュリティグループ
今回紹介する自動削除スクリプトは、 「社内の命名規則が厳格に運用されていること」 を前提としています。
セキュリティグループ名や各種リソースのNameタグなどが特定のルール(VPC IDなどの環境変数を含む形)で一貫して命名されているため、シェルスクリプト側から名前ベースでピンポイントに狙い撃ちして削除するアプローチが可能になっています。
何が起きたか
VPCを管理しているスタックを削除しても、次のような状態で失敗しました。
- VPCにアタッチされたリソースが残っている
- 結果としてVPCが削除できず、スタック全体が
DELETE_FAILEDになる
見えていた原因はVPC Peeringでしたが、実際にはそれだけではありませんでした。
削除を阻んでいた依存リソース
今回、私の環境で削除前に明示的に処理が必要だったのは次の4つです。
(※環境によっては、ECSタスクやELB、Lambda用のENIなどが原因になることもあります。適宜ご自身の環境に合わせて読み替えてください)
- アカウント間VPC Peering Connection
- GuardDutyのVPC Endpoint
- GuardDutyのセキュリティグループ
- 定期実行するAWS Batchジョブ用セキュリティグループ
ポイントは、CloudFormationスタック外での作成や運用中に追加された関連リソースです。これらが残ると、最終的なVPC削除が失敗します。
対応方針
方針はシンプルで、「VPCにぶら下がる依存を先に外してからserviceスタックを削除する」です。
実行順序は次のようにしました。
- 対象VPCの特定
- VPC Peeringの削除
- GuardDuty VPC Endpointの削除
- GuardDuty関連セキュリティグループの削除
- 定期実行するAWS Batchジョブ用セキュリティグループの削除
- サービス基盤のスタック削除
実装の要点
以下のシェルスクリプト(Bash)は、CodeBuildの環境変数や前段の処理によって、AWSリージョンを示す $REGION が既に定義されている前提で動かしています。
0. 最初に削除対象の名前を変数で揃える
後続で同じ値を何度か使うため、最初に削除対象の名前を変数としてまとめておきます。
実運用では同名SGの誤削除を避けるため、名前だけでなく vpc-id や環境識別タグでも絞り込むことを強く推奨します。
target_vpc_name="my_vpc"
target_peering_name="my_peering"
batch_rule_name="my_batch_job-rule"
batch_sg_name="my_batch_job"
1. まず削除対象のVPCを特定する
対応方針の1つ目どおり、最初に削除対象の VPC ID を確定させます。
target_vpc_id=$(aws ec2 describe-vpcs \
--filters "Name=tag:Name,Values=$target_vpc_name" \
--query "Vpcs[0].VpcId" \
--output text)
if [ "$target_vpc_id" = "None" ] || [ -z "$target_vpc_id" ]; then
echo "Target VPC not found: $target_vpc_name"
exit 1
fi
2. VPC Peeringを先に削除
Nameタグで対象のPeeringを絞り込み、active系ステータスのみ削除対象にします。
target_peering_ids=$(aws ec2 describe-vpc-peering-connections \
--filters "Name=tag:Name,Values=$target_peering_name" \
--query "VpcPeeringConnections[?contains(['active','provisioning','pending-acceptance','initiating-request'], Status.Code)].VpcPeeringConnectionId" \
--output text)
if [ -n "$target_peering_ids" ]; then
for target_peering_id in $target_peering_ids; do
aws ec2 delete-vpc-peering-connection --vpc-peering-connection-id "$target_peering_id"
done
fi
3. GuardDuty VPC Endpointは削除完了を待つ
削除APIを実行した直後はまだ裏で処理(依存)が残っているため、状態確認ループで完全に消滅した(deleted または does not exist になった)のを確認してから、関連するセキュリティグループの削除に進みます。
target_service="com.amazonaws.${REGION}.guardduty-data"
target_vpce_id=$(aws ec2 describe-vpc-endpoints \
--filters "Name=vpc-id,Values=$target_vpc_id" \
"Name=vpc-endpoint-type,Values=Interface" \
"Name=service-name,Values=$target_service" \
--query "VpcEndpoints[0].VpcEndpointId" \
--output text)
if [ "$target_vpce_id" != "None" ]; then
aws ec2 delete-vpc-endpoints --vpc-endpoint-ids "$target_vpce_id"
success=false
for i in {1..36}; do
status=$(aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids "$target_vpce_id" \
--query "VpcEndpoints[0].State" \
--output text 2>&1)
if [[ "$status" == *"does not exist"* ]] || [[ "$status" == "deleted" ]]; then
success=true
break
fi
sleep 5
done
if [ "$success" = false ]; then
exit 1
fi
guardduty_sg_name="GuardDutyManagedSecurityGroup-${target_vpc_id}"
guardduty_sg_ids=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=$guardduty_sg_name" \
--query "SecurityGroups[*].GroupId" \
--output text)
if [ -n "$guardduty_sg_ids" ]; then
for guardduty_sg_id in $guardduty_sg_ids; do
aws ec2 delete-security-group --group-id "$guardduty_sg_id"
done
fi
fi
4. 定期実行するAWS Batchジョブ関連も明示削除
セキュリティグループの削除に加えて、EventBridge(CloudWatch Events)のルールをあらかじめ無効化して後続のジョブ動作を止め、リソースが再生成されるのを防ぎます。
aws events disable-rule --name "$batch_rule_name"
batch_sg_ids=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=$batch_sg_name" \
--query "SecurityGroups[*].GroupId" \
--output text)
if [ -n "$batch_sg_ids" ]; then
for batch_sg_id in $batch_sg_ids; do
aws ec2 delete-security-group --group-id "$batch_sg_id"
done
fi
5. 最後にサービス基盤のスタックを削除
こうしてVPCに紐づく動的な依存リソースをすべて剥がし終えたら、最後に本丸であるサービス基盤のCloudFormationスタック削除(aws cloudformation delete-stack)を実行します。これでVPCも含めてきれいに削除されるようになります。
うまくいった理由
- 依存リソースを「種類ごと」に先に処理した
- 非同期削除(特にVPCEndpointの削除)を待ってから次に進んだ
- スタック削除に期待しすぎず、運用で生まれる依存をCodeBuild側で吸収した
運用して得た学び
- CloudFormationスタック削除だけで環境が消えるとは限らない
- VPC削除の失敗は、VPC自体よりも「いつの間にか作られていた周辺の依存リソース」の見落としで起こる
- 自動化の削除処理は「何を消すか」より「どの順序、どのタイミング(同期・非同期)で消すか」が重要
おわりに
今回の改善で、毎日の非本番環境削除が失敗することなく安定しました。
VPC Peeringだけでなく、GuardDuty関連や常駐タスク関連の依存を、スクリプト側で順序立てて先に外すことが決め手でした。