aws-nukeで作るマルチアカウントのお掃除機能
はじめに
みなさんこんにちは。仕事のお供はお煎餅なhiropyです。
今回は、ControlTowerで構築したマルチアカウント環境で、aws-nukeを使って、自動、かつ定期的にリソースのクリーンアップを行う方法について書いてみようと思います。
これを導入することで、社内の検証環境の不要なリソースの手動削除の手間を減らせるかも
前提
今回は、以下の前提で進めさせていただきます。
- ControlTowerがデプロイされていること
- ControlTowerのルートアカウントが使用できること
- ControlTowerのルートアカウントから各アカウントに対してAssumeRoleができること
- リソース削除の対象となるアカウントにエイリアスが付与されていること
エイリアスの付与に関しては見落としがちな点だと思うので、注意してください。
アカウントのエイリアス付与の方法についてはこちらをご参照ください。
aws-nukeとは?
本題に入る前に、「そもそもaws-nukeってなんなの?」と思われる方が多いと思うのでその説明をさせていただきます。
そもそもaws-nukeとは、アカウント内のリソースを強制的に削除するコマンドラインツールです。(Githubリポジトリ)
設定ファイルを用意し、コマンドを実行するだけで強制的にリソースを削除できます。
かなり強力なツールなので使用する際は細心の注意を払う必要がありますが、適切な使い方をすればとても便利ツールです。
利用するための料金はかかりません。
今回のゴール
aws-nukeのご紹介もさせていただいたところで、早速本題に入ろうと思います。
今回ご紹介させていただくリソースのアーキテクチャは以下の通りです。
やっていることとしては、
- EventBridgeスケジュールで定期的にStep Functionsステートマシンを呼び出す
- Step Functionsステートマシン内で
- Lambda関数によりOU内にあるアカウントIDのリストを取得
- アカウントIDのリストをCodeBuildプロジェクトに渡す
- CodeBuildプロジェクト内で
aws-nuke
をアカウントIDの分だけ実行しリソースを削除する
となっています。
今回は、
- アカウントIDを取得するLambda関数を作成する
- CodeBuildプロジェクトを作成する
- Step Functionsのステートマシンを作成する
- EventBridgeスケジュールを作成する
の順番でご紹介させていただきます。
以降の作業は、すべてControlTowerの管理アカウントで行ってください
やってみよう
1. アカウントIDを取得するLambda関数を作成する
まずはOU内のアカウントIDを取得するLambda関数を作成します。
以下のコードをコピーして、Lambdaで新しい関数を作成してください。
- アカウントIDを取得するLambdaのコード
-
import boto3 client = boto3.client('organizations') # 異なるAWSアカウント/ロールのクレデンシャル取得を実行する def sts_assume_role(account_id): role_arn = "arn:aws:iam::%s:role/AWSControlTowerExecution" % account_id session_name = "create-alias" client = boto3.client('sts') # AssumeRoleで一時クレデンシャルを取得 response = client.assume_role( RoleArn=role_arn, RoleSessionName=session_name ) iam_client = boto3.client( 'iam', aws_access_key_id=response['Credentials']['AccessKeyId'], aws_secret_access_key=response['Credentials']['SecretAccessKey'], aws_session_token=response['Credentials']['SessionToken'], ) return iam_client # アカウントのエイリアスを取得する def get_account_aliases(account_id): iam = sts_assume_role(account_id) res = [] # List account aliases through the pagination interface paginator = iam.get_paginator('list_account_aliases') for response in paginator.paginate(): res = response["AccountAliases"] return res # エイリアスが存在するアカウントIDのリストを取得する def get_account_ids(): # 全組織のアカウント情報を取得する pagenator = client.get_paginator('list_accounts') response_iterator = pagenator.paginate() row_value = [] for response in response_iterator: for acct in response['Accounts']: # 取得するアカウント情報のリスト化 row_value.append(acct['Id']) res = "" # アカウントIDのリストを空白区切りの文字列にする for index,value in enumerate(row_value): # 先頭の要素はスキップする if index == 0: res = value continue res = res + " " + value return res def lambda_handler(event, context): account_list = get_account_ids() return {"accounts": account_list}
任意のテストケースを作成して、OU内のアカウントIDが取得できることを確認しましょう。
2. nukeを実行するためのCodeBuildプロジェクトを作成する
次にaws-nukeの実行体である、CodeBuildプロジェクトの作成を行います。
前準備として、aws-nukeの設定ファイルとログを置くためのS3 Bucketを作成します。
任意の名前のS3Bucketを2つ作成し、どちらかにnuke-config.yaml
という名前で以下のファイルを配置してください。
- nuke-config.yamlのコード
-
regions: # add `global` here to include IAM entities to be nuked # nukeでリソースを削除する対象のリージョンをリストアップします - ap-northeast-1 - us-east-1 account-blocklist: - 123456789012 # 削除対象から外すアカウントをリストアップします # optional: restrict nuking to these resources resource-types: excludes: # 削除の対象外とするリソースをリストアップします - IAMUser - IAMUserPolicyAttachment - IAMUserAccessKey - IAMLoginProfile - IAMGroup - IAMRolePolicy - IAMRolePolicyAttachment - IAMPolicy - IAMSAMLProvider - CloudTrailTrail - SNSTopic - LambdaFunction - CloudFormationStack - Budget targets: # 削除対象とするサービスをリストアップします - S3Bucket - S3Object - SQSQueue - RDSInstance - EC2Instance - EC2Image - EC2NATGateway - DynamoDBTableItem - DynamoDBTable accounts: ACCOUNT: # ACCOUNTという文字列は後ほどCodeBuild内で実際のアカウントIDに書き換えられます filters: # 特定のタグがついたリソースを削除対象外にできます EC2Instance: - type: exact property: tag:DoNotNuke value: "true" S3Bucket: - type: exact property: tag:DoNotNuke value: "true" S3Object: - type: exact property: tag:DoNotNuke value: "true" SQSQueue: - type: exact property: tag:DoNotNuke value: "true" RDSInstance: - type: exact property: tag:DoNotNuke value: "true" EC2Image: - type: exact property: tag:DoNotNuke value: "true" EC2InternetGateway: - type: exact property: tag:DoNotNuke value: "true" EC2NATGateway: - type: exact property: tag:DoNotNuke value: "true" DynamoDBTableItem: - type: exact property: tag:DoNotNuke value: "true" DynamoDBTable: - type: exact property: tag:DoNotNuke value: "true" feature-flags: disable-deletion-protection: RDSInstance: true EC2Instance: true
yamlファイルを構成する各要素について解説します。
- regions: nukeでリソースを削除する際、どのリージョンのリソースを削除するかを指定することができます。
- account-blocklist:指定したアカウントをnukeの削除対象から外すことができます。
- resource-types
- excludes: 削除の対象外とするサービスを指定することができます。
- targets: 削除の対象とするサービスを明示的に指定することができます。
- accounts: アカウント別に削除するリソースを絞り込むことができます。今回は
DoNotNuke: true
のタグがついたリソースを削除の対象外としています。 - feature-flags: その他のオプションです。今回は、削除保護がついているRDSとEC2も強制的に削除するようにしています。
このようにaws-nukeでは、削除対象の指定などを柔軟に行うことができます。
皆様が利用されているAWS環境のニーズに合わせて、こちらのルールは変更してください。
続いて、nukeを実行するためのCodeBuildプロジェクトをCloudFormationのStackでデプロイします。
以下のコードを手元にコピーして新規ファイルを作成し、CloudFormationでStackを作成してください。
なお、以下の箇所は対応する値に置き換えてください。
<管理アカウントのアカウントID>
:CloudFormationをデプロイするアカウント(=ControlTowerの管理アカウント)のアカウントID<設定ファイル用のバケット名>
:nuke-config.yaml
を配置したバケットの名前<ログ用のバケット名>
:先ほど作成したバケットのうち、何も配置していないバケットの名前
- CodeBuildデプロイ用のCloudFormation Stack
-
Parameters: AWSNukeDryRunFlag: Description: The dry run flag to run for the aws-nuke. By default it is set to True which will not delete any resources. Type: String Default: "true" AWSNukeVersion: Description: The aws-nuke latest version to be used from internal artifactory/S3. Make sure to check the latest releases and any resource additions added. As you update the version, you will have to handle filtering any new resources that gets updated with that new version. Type: String Default: 2.21.2 AccountId: Description: The AWS account ID Type: String Resources: NukeCodeBuildProject: Type: AWS::CodeBuild::Project Properties: Artifacts: Type: NO_ARTIFACTS BadgeEnabled: false Description: Builds a container to run AWS-Nuke for all accounts within the specified account/regions Environment: ComputeType: BUILD_GENERAL1_2XLARGE Image: aws/codebuild/docker:18.09.0 ImagePullCredentialsType: CODEBUILD PrivilegedMode: true Type: LINUX_CONTAINER EnvironmentVariables: - Name: AWS_NukeDryRun Type: PLAINTEXT Value: !Ref AWSNukeDryRunFlag - Name: AWS_NukeVersion Type: PLAINTEXT Value: !Ref AWSNukeVersion - Name: AccountId Type: PLAINTEXT Value: !Ref AccountId - Name: NukeCodeBuildProjectName Type: PLAINTEXT Value: !Sub "AccountNuker-codeBuild" LogsConfig: CloudWatchLogs: GroupName: !Sub "AccountNuker-logs" Status: ENABLED Name: !Sub "AccountNuker-${AWS::StackName}" ServiceRole: arn:aws:iam::<管理アカウントのアカウントID>:role/CodeBuildExecNukeRole TimeoutInMinutes: 120 Source: BuildSpec: | version: 0.2 phases: pre_build: commands: - export AWS_NUKE_VERSION=$AWS_NukeVersion - apt-get install -y wget - apt-get install jq - wget https://github.com/rebuy-de/aws-nuke/releases/download/v$AWS_NUKE_VERSION/aws-nuke-v$AWS_NUKE_VERSION-linux-amd64.tar.gz --no-check-certificate - tar xvf aws-nuke-v$AWS_NUKE_VERSION-linux-amd64.tar.gz - chmod +x aws-nuke-v$AWS_NUKE_VERSION-linux-amd64 - mv aws-nuke-v$AWS_NUKE_VERSION-linux-amd64 /usr/local/bin/aws-nuke - aws-nuke version - echo "Setting aws cli profile with config file for role assumption using metadata" - aws configure set profile.nuke.credential_source "EcsContainer" - export AWS_PROFILE=nuke - export AWS_DEFAULT_PROFILE=nuke - export AWS_SDK_LOAD_CONFIG=1 build: commands: - echo "Getting nuke generic config file from S3"; - aws s3 cp s3://<設定ファイル用のバケット名>/nuke_config.yaml . - echo "Configured nuke_config.yaml"; - echo " ------------------------------------------------ " >> error_log.txt - echo "Running Nuke on Account"; - | for i in $AccountId; do aws configure set profile.nuke.role_arn arn:aws:iam::$i:role/AWSControlTowerExecution touch nuke_config_$i.yaml cp nuke_config.yaml nuke_config_$i.yaml sed -i "s/ACCOUNT/$i/g" nuke_config_$i.yaml log_file=aws-nuke-$(date +%Y%m%d%I%M%S).log if [ "$AWS_NukeDryRun" = "true" ]; then aws-nuke -c nuke_config_$i.yaml --quiet --force --profile nuke 2>&1 |tee -a $log_file; elif [ "$AWS_NukeDryRun" = "false" ]; then aws-nuke -c nuke_config_$i.yaml --quiet --force --no-dry-run --profile nuke 2>&1 |tee -a $log_file; else echo "Couldn't determine Dryrun flag...exiting" exit 1 fi; aws s3 cp $log_file s3://<ログ用のバケット名>/$i/ nuke_pid=$!; wait $nuke_pid; echo "Checking if Nuke Process completed for account" if cat $log_file | grep -F "Error:"; then echo "Nuke errored due to no AWS account alias set up - exiting" cat $log_file >> error_log.txt aws s3 cp error_log.txt s3://<ログ用のバケット名>/$i/error/ exit 1 else echo "Nuke completed Successfully - Continuing" fi done Type: NO_SOURCE
3. Step Functionsのステートマシンを作成する
次に、アカウントIDを取得するLambda関数とCodeBuildを呼び出すためのStep Functionsステートマシンを作成します。
Step Functionsコンソールを表示し、「ステートマシンを定義する」ボタンをクリックしたら、画像のように「コードでワークフローを記述」を選択してください。
下にスクロールするとワークフローを定義できる箇所が出てくるので、
以下のコードを貼り付けてください。
なお、以下の箇所は該当の値に置き換えてください。
- <アカウントID取得Lambdaの関数名>: 1.で作成したLambda関数名
- <CodeBuildプロジェクトのARN>: 2.で作成したCodeBuildプロジェクトのARN
- Step Functionsのステートマシン定義
-
{ "Comment": "AWS Nuke Account Cleanser", "StartAt": "GetAccountIds", "States": { "GetAccountIds": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "<アカウントID取得Lambdaの関数名>" }, "ResultPath": "$.accountList", "Next": "NukeCodeBuildJob" }, "NukeCodeBuildJob": { "Type": "Task", "Resource": "arn:aws:states:::codebuild:startBuild.sync", "Parameters": { "ProjectName": "<CodeBuildプロジェクトのARN>", "EnvironmentVariablesOverride": [ { "Name": "AccountId", "Type": "PLAINTEXT", "Value.$": "$.accountList.Payload.accounts" }, { "Name": "AWS_NukeDryRun", "Type": "PLAINTEXT", "Value.$": "$.InputPayLoad.nuke_dry_run" }, { "Name": "AWS_NukeVersion", "Type": "PLAINTEXT", "Value.$": "$.InputPayLoad.nuke_version" } ] }, "Next": "NukeStatusCheck", "ResultSelector": { "NukeBuildOutput.$": "$.Build" }, "ResultPath": "$.AccountCleanserRegionOutput", "Retry": [ { "ErrorEquals": [ "States.TaskFailed" ], "BackoffRate": 1, "IntervalSeconds": 1, "MaxAttempts": 0 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "NukeFailed", "ResultPath": "$.AccountCleanserRegionOutput" } ] }, "NukeStatusCheck":{ "Type": "Choice", "Choices": [ { "Variable": "$.AccountCleanserRegionOutput.NukeBuildOutput.BuildStatus", "StringEquals": "SUCCEEDED", "Next": "NukeSuccess" }, { "Variable": "$.AccountCleanserRegionOutput.NukeBuildOutput.BuildStatus", "StringEquals": "FAILED", "Next": "NukeFailed" } ], "Default": "NukeSuccess" }, "NukeSuccess":{ "Type": "Succeed", }, "NukeFailed":{ "Type": "Fail", "Cause": "nukeの実行に失敗しました。", } } }
このステートマシンにより、
- アカウントIDのリストの取得
- CodeBuildプロジェクトの実行
を行っています。
4. EventBridgeスケジュールを作成する
最後に、定義したステートマシンを定期実行するEventBridgeのスケジュールを作成します。
EventBridgeコンソールからスケジュールタブに進み、新規作成ボタンを押します。定期的なスケジュール
を選択し、cron式で以下のように入力します。
(以下のcronで設定すると、毎週金曜日の19:00にnukeが実行されます。)
0 19 ? * 6 *
ターゲットの選択ではStep FunctionsのStart Execution
を選択し、
3で作成したステートマシンを選択してください。
なお、その下のインプットには、以下のように入力してください。
{
"Comment": "Insert your JSON here",
"InputPayLoad":{
"nuke_dry_run":"false",
"nuke_version":"2.21.2"
}
}
これで、ステートマシンに対し強制削除するためのパラメータを渡していることになります。
そのほかは特に変更せずに、確認とレビューの画面で「作成」ボタンを押します。
これで「マルチアカウントにおけるリソースの定期削除機能」の実装は完了です!
最後に
いかがでしたでしょうか?
マルチアカウントを管理する際、こちらの記事が参考になれば幸いです!