0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFormationスタックを消せない? VPCの削除を阻む依存リソースをCodeBuildで順番に剥がした

0
Last updated at Posted at 2026-07-10

はじめに

開発環境のコスト削減のため、毎日深夜に環境を自動削除する運用をしています。
削除ジョブは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スタックを削除する」です。

実行順序は次のようにしました。

  1. 対象VPCの特定
  2. VPC Peeringの削除
  3. GuardDuty VPC Endpointの削除
  4. GuardDuty関連セキュリティグループの削除
  5. 定期実行するAWS Batchジョブ用セキュリティグループの削除
  6. サービス基盤のスタック削除

実装の要点

以下のシェルスクリプト(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関連や常駐タスク関連の依存を、スクリプト側で順序立てて先に外すことが決め手でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?