【Pythonスクリプト】アカウント内のCloudFormationスタックを全部一気に削除保護する

記事タイトルとURLをコピーする

こんにちは。3月より技術1課に正式配属となりました加藤和也です。
研修、OJTの1年間を経てついに配属です。頑張るしかねぇ。

さて今回は、アカウント内にあるCloudFormation(以後、CFn)スタック全ての削除保護を有効化するスクリプトを作成したので、気をつけるポイント含め共有していきます。

CFnスタックの削除保護とは

CFnにはスタックが誤って削除されることを防ぐために、削除保護という機能があります。
削除保護を有効化した状態でスタックを削除しようとすると、削除処理自体が失敗します。ステータスを含め、スタックの変更が行われないのです。
本番稼働するシステムなど、操作ミスなどによる誤った削除を起こしたくない場合には有効化しておくことが望ましいでしょう。

この設定はスタック作成時やスタック作成後にいつでも有効化できます。
もちろんマネージメントコンソールからぽちぽち設定することも可能なのですが、手間を省いたり漏れを防いだりするためにスクリプトで一気に設定してしまいましょう。

完成したスクリプト

import boto3
import json
import sys

args = sys.argv
enable = '--disable' not in args

client = boto3.client('cloudformation')
token = ''

while token is not None:
# Set params
  params = {
    'StackStatusFilter': ['CREATE_COMPLETE']
  }
  if token:
    params['NextToken'] = token
# Get list of CFn Stacks
  stacks = client.list_stacks(**params)['StackSummaries']

  if 'NextToken' in stacks:
    token = stacks['NextToken']
  else:
    token = None

# Get only root Stacks
  root_stacks = list(filter(lambda stack: 'ParentId' not in stack, stacks))
# Get root_stacks name
  stack_names = list(map(lambda stack: stack['StackName'], root_stacks))
# Enable Termination Protection
  for stack_name in stack_names:
    res = client.update_termination_protection(
      EnableTerminationProtection=enable,
      StackName=stack_name
    )
  message = 'protected: ' if enable else 'disabled: '
  print(message, res['StackId'])

enable/disableをどちらもできるようにした結果多少長めになりました。開発環境は、
- Python 3.7.2
- boto3 1.9.97
となっています。Python2系対応は行なっておりませんが、悪しからず。

使い方

クレデンシャル情報を設定

1. スタックに対し以下の権限を持つIAMユーザーを用意します。
- "cloudformation:ListStacks",
- "cloudformation:UpdateTerminationProtection"

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "cloudformation:ListStacks",
      "cloudformation:UpdateTerminationProtection"
    ],
    "Resource": "*"
  }]
}

2. 払い出したIAMユーザーのアクセスキーを、環境変数に登録します

# windows
set AWS_ACCESS_KEY_ID=xxx #(アクセスキーを記載)
set AWS_SECRET_ACCESS_KEY=xxxx #(シークレットアクセスキーを記載)
set AWS_DEFAULT_REGION=ap-northeast-1
# Unix系
export AWS_ACCESS_KEY_ID=xxx #(アクセスキーを記載)
export AWS_SECRET_ACCESS_KEY=xxxx #(シークレットアクセスキーを記載)
export AWS_DEFAULT_REGION=ap-northeast-1

 

削除保護を有効化

任意の名前で保存したスクリプトを実行します

python script_name.py

削除保護を無効化

引数 --disable を与えてスクリプトを実行します

python script_name.py --disable

スクリプト解説

いくつかハマりやすい箇所があるため、解説を加えておきます。

1. ネストしたスタックへの対応

ネストしたスタックへの削除保護の設定は、必ずルートスタックに対してのみ行います。
子スタックに対して設定を行おうとすると、エラーが発生してうまくスクリプトが動きません。スタック一覧からルートスタックと子スタックを判別する際には ParentId または RootId を保持しているかどうかをみます。今回のスクリプトでは、

root_stacks = list(filter(lambda stack: 'ParentId' not in stack, stacks))

この行で ParentId を保持していないスタックを抽出することで、ルートスタックのみを取得しています。
filterとlambda記法でささっと書いてしまいたい人なのでこんな書き方をしていますが、普通にfor文で

root_stacks = []
for stack in stacks:
  if 'ParentId' not in stack:
    root_stacks.append(stack)

と書いてもOKです。

2. NextTokenの処理

list_stacksはレスポンスのサイズが1MBを超える場合、NextTokenという値とともに1MB以下の値のみを返す仕様になっています。続きの値が欲しい場合は、取得したNextTokenをつけて改めてlist_stacksを実行します。

レスポンスが1MBを超えることはそうないかもしれませんが、今回のスクリプトでは念のため、NextTokenが返ってきた場合の処理も追加しました。
以下の部分で対応を行なっています。

token = ''

while token is not None: 
# Set params
  params = {
    'StackStatusFilter': ['CREATE_COMPLETE']
  }
  if token:
    params['NextToken'] = token

# Get list of CFn Stacks
  stacks = client.list_stacks(**params)['StackSummaries']

  if 'NextToken' in stacks:
    token = stacks['NextToken']
  else:
    token = None

注意しなければいけないのが、list_stacksへの引数の渡し方です。

最初にリクエストを投げる際には当然 NextToken の値は持っていません。とりあえず一回目のリクエストではNextTokenに空文字やNoneを指定しておきたいところです。
ですが、list_stacks関数の仕様上、それができません。NextTokenを与えた場合は型や文字長のチェックが行われるため、空文字やNoneではエラーが発生してしまいます。

これを踏まえると、
- 最初は引数に StackStatusFilter のみを渡す
- 2回目以降(NextTokenを受け取って以降)は NextToken も渡す
という形を守る必要があります。

今回は**で引数を展開しましたが、普通に1回目と2回目以降の処理を分けて書いてもいいと思います。
その辺りはお好みで。