aws-nukeとCodeシリーズを使って、AWSリソース一括削除ボタンのようなものを作ってみる

2023.01.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

「aws-nukeを使って好きなタイミングでリソースクリーンアップしたい。だけど、毎回CLIでコマンド打つのが面倒」

aws-nukeというAWSリソースを一括削除できるCLIツールがあります。

aws-nukeは全てのAWSリソースを削除するだけではなく、柔軟に除外するリソースを設定することもできて、とても便利なツールです。

毎回ローカルでaws-nukeのコマンド打って実行してもいいのですが、AWSコンソールからボタン一つで実行できたらもっと便利かと思いました。

以前GithubActions + aws-nukeで似たようなことをやってみました。

今回はCodeシリーズとaws-nukeでやってみます。

CodePipelineの手動実行をトリガーにAWSリソースを一括削除する仕組みとなっています。

この記事で使用するコードは、以下のリポジトリにあります。

msato0731/remove-aws-resource-pipeline-blog

今回作成するリソース

実行の流れとしては以下です。

Dry-Runの結果を見て、実際に削除するかどうか確認できるようにしたかったです。 そのため、いきなりRunするのではなく、Dry-Run>手動承認>Runという流れにしています。

  1. CodePipelineの手動実行をトリガーにパイプラインを起動
  2. Dry-Run用のCodeBuild実行(この時点では、リソース削除は行われない)
  3. Dry-Runの結果を確認して、CodePipelineの手動承認を承認
  4. Run用のCodeBuild実行(リソース削除が行われる)

やってみる

一部AWSリソースを除いて、AWS CDKを使ってリソースを作成します。

使用するリージョンは東京リージョン(ap-northeast-1)を想定しています。

CodeCommitリポジトリの作成・push

サンプルコードをForkして、ローカルにCloneします。

% git clone git@github.com:[Githubユーザー名]/cleanup-aws-resource.git

CodeCommitは手動で作成しました。

% aws codecommit create-repository --repository-name remove-aws-resource-pipeline

CodeCommitにPushします。GRCを使っていますが、Pushできれば任意の方法で構いません。

% export AWS_ACCESS_KEY_ID=<アクセスキー>
% export AWS_SECRET_ACCESS_KEY=<シークレットアクセスキー>
% export AWS_SESSION_TOKEN=<セッショントークン>
% git remote add codecommit codecommit::ap-northeast-1://remove-aws-resource-pipeline
% git push codecommit main

aws-nuke用のconfigを作成

aws-nuke用のconfigファイルを用意します。

サンプルコードから以下の部分を変更する必要があります。

  • accouts 部分のアカウントID変更
  • (オプション)必要に応じてFilter追加
    • 今回のCDKコードで作成されるリソースをfilterで削除除外される設定になっています
  • (オプション)IAM Role関連リソースの除外設定修正
    • サービスごと除外しているため、リソース単位で残すフィルターを追加

nuke-config.yml

regions:
  - ap-northeast-1
  - global

account-blocklist:
  - "000000000000" # dummy

resource-types:
  excludes:
    # 東京リージョンが対応しないサービスでERRORやWARNが出るサービスを除外(2022/12/23)
    # 実行結果のノイズになるため
    - FMSPolicy
    - FMSNotificationChannel
    - GlobalAccelerator
    - GlobalAcceleratorListener
    - GlobalAcceleratorEndpointGroup
    - WorkLinkFleet
    - MobileProject
    - SESReceiptRuleSet
    - SESReceiptFilter
    # スキャン時間が長くなるため除外
    - S3Object
    # ログインに使うリソースを一旦除外
    - IAMRole
    - IAMRolePolicy
    - IAMRolePolicyAttachment

accounts:
  "111111111111": # アカウントIDを書き換える
    presets:
      - "remove-aws-resource-pipeline"

presets:
  remove-aws-resource-pipeline:
      filters:
        CodeBuildProject:
        - type: exact
          property: tag:System
          value: "remove-aws-resource-pipeline"
        CodePipelinePipeline:
        - type: glob
          value: "RemoveAwResourcePipeline*"
        KMSAlias:
        - type: glob
          value: "alias/codepipeline-removeawresourcepipeline*"
        KMSKey:
        - type: exact
          property: tag:System
          value: "remove-aws-resource-pipeline"
        S3Bucket:
        - type: exact
          property: tag:System
          value: "remove-aws-resource-pipeline"
        CloudFormationStack:
        - type: exact
          property: tag:System
          value: "remove-aws-resource-pipeline"
        CloudWatchLogsLogGroup:
        - type: glob
          value: "/aws/codebuild/remove-resource-pipeline/*"
        IAMRole:
        - type: exact
          property: tag:System
          value: "remove-aws-resource-pipeline"
        IAMRolePolicy:
        - type: exact
          property: tag:role:System
          value: "remove-aws-resource-pipeline"
        IAMRolePolicyAttachment:
        - type: exact
          property: tag:role:System
          value: "remove-aws-resource-pipeline"

Dry-Runは、以下のコマンドで実行できます。 結果を確認して、必要に応じてconfigファイルを修正してください。

$ docker compose run aws-nuke

修正完了したら、CodeCommit上に変更をpushします。

$ git add . 
$ git commit -m "update: filter"
$ git push codecommit main

Docker Hubログイン用のSecrets Manager作成・Secretsの登録

CodeBuild上で、aws-nukeを実行するのですがその際にdocker-composeを使用して実行します。

今回の構成では、Docker Hubのレートリミットに引っかかる可能性があります。

そのため、CodeBuild上でDocekr Hubにログインして、レートリミットの上限を緩和します。

DockerHubのログイン情報を保存するための、Secrets Managerを用意しておきます。

  • シークレット名: docker_hub
  • キー/値:
    • username: [Docker Hubのユーザ名]
    • password: [Docker Hubのパスワード]

作成したSecrets ManagerのSecrets ARNにcdk.json内のdockerhubSecretsManagerArn を書き換えます。

deploy/cdk/cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/remove-aws-resource-pipeline.ts",
  "watch": {
    "include": ["**"],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "codecommitRepoName": "remove-aws-resource-pipeline",
    "dockerhubSecretsManagerArn": "arn:aws:secretsmanager:ap-northeast-1:00000000:secret:docker_hub-9YbE87"
  }
}

書き換えができたら、CodeCommitにPushします。

$ git add . 
$ git commit -m "update: secrets manager arn"
$ git push codecommit main

CDKでリソースを作成

事前準備が済んだら、残りのリソースはCDKで作成します。

$ cd cdploy/cdk
$ npm i
$ npm run cdk deploy

以下、コード等の説明です。

buildspec.yml

CodeBuild内で、docker-composeを使って、aws-nukeを実行しています。 Docker in Dockerの方法です。

他にも色々方法があるかと思いますが、ローカルと同様のコマンドを使えて実装もシンプルかと思い今回はこの方法を採用しました。

CodeBuild上では、以下の処理を行なっています。

  • aws-nukeコンテナに渡すAWS認証情報の取得
  • Docker Hubへのログイン
  • aws-nukeの実行

Dry-RunとRunで同じbuildspec.ymlを使用して、CodeBuildの環境変数の値によってRunコマンドかDry-Runコマンドかを切り分けています。

version: 0.2
env:
  secrets-manager:
    DOCKERHUB_USER: $DOCKER_HUB_SECRET_ARN:username
    DOCKERHUB_PASS: $DOCKER_HUB_SECRET_ARN:password
phases:
  pre_build:
    commands:
      # 一時的なAWS認証情報の取得
      - TEMP_ROLE=$(aws sts assume-role --role-arn $ASSUME_ROLE_ARN --role-session-name aws-nuke)
      - export TEMP_ROLE
      - export AWS_ACCESS_KEY_ID=$(echo "${TEMP_ROLE}" | jq -r '.Credentials.AccessKeyId')
      - export AWS_SECRET_ACCESS_KEY=$(echo "${TEMP_ROLE}" | jq -r '.Credentials.SecretAccessKey')
      - export AWS_SESSION_TOKEN=$(echo "${TEMP_ROLE}" | jq -r '.Credentials.SessionToken')
      # DockerHub へのログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin
  build:
    commands:
      - |
        if [ $AWS_NUKE_DRY_RUN = "false" ]; then
          docker-compose run aws-nuke --no-dry-run
        else
          docker-compose run aws-nuke
        fi

CDK

リソースに対して、Systemという共通のタグをつけています。

このタグをaws-nukeのconfigで指定して、このCDKを使用して作成されるリソースを除外しています。

bin/remove-aws-resource-pipeline.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { RemoveAwResourcePipelineStack } from '../lib/remove-aws-resource-pipeline-stack';

const app = new cdk.App();
new RemoveAwResourcePipelineStack(app, 'RemoveAwResourcePipeline', {
  codecommitRepoName: app.node.tryGetContext('codecommitRepoName'),
  dockerhubSecretsManagerArn: app.node.tryGetContext('dockerhubSecretsManagerArn'),
});

cdk.Tags.of(app).add('System', 'remove-aws-resource-pipeline');

実際のリソース定義は以下です。

基本的にはリソースの命名はデフォルトに任せていますが、一部のタグをつけれないリソースaws-nukeの除外の関係で名前をつけています。(ロググループなど)

deploy/cdk/lib/remove-aws-resource-pipeline-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as logs from 'aws-cdk-lib/aws-logs';

export interface RemoveAwResourcePipelineStackProps extends cdk.StackProps {
  codecommitRepoName: string;
  dockerhubSecretsManagerArn: string;
}

export class RemoveAwResourcePipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: RemoveAwResourcePipelineStackProps) {
    super(scope, id, props);

    // IAM Role
    const codebuildRole = new iam.Role(this, 'CodeBuildRole', {
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
    });

    const nukeRole = new iam.Role(this, 'AwsNukeRole', {
      assumedBy: new iam.ArnPrincipal(codebuildRole.roleArn),
    });

    const dockerhubSecrets = secretsmanager.Secret.fromSecretCompleteArn(
      this,
      'DockerhubSecrets',
      props.dockerhubSecretsManagerArn,
    );
    codebuildRole.addToPolicy(
      new iam.PolicyStatement({
        resources: [dockerhubSecrets.secretArn],
        actions: ['secretsmanager:GetSecretValue'],
      }),
    );

    // CodeBuild RoleでAwsNuke Roleを引き受けれるようにする
    nukeRole.grantAssumeRole(codebuildRole);
    // aws-nukeはリソース削除を行うため強い権限を付与
    nukeRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'));

    // Source
    const sourceOutput = new codepipeline.Artifact();
    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: 'CodeCommit_Source',
      repository: codecommit.Repository.fromRepositoryName(this, 'Repo', props.codecommitRepoName),
      branch: 'main',
      output: sourceOutput,
      // 手動トリガーでPipelineを起動させたいため
      trigger: codepipeline_actions.CodeCommitTrigger.NONE,
    });

    // Log Group
    // aws-nukeで除外時に名前が固定されていた方が都合が良いため(タグをつけれないため、タグで除外ができない)
    const dryRunProjectLogGroup = new logs.LogGroup(this, 'DryRunProjectLogGroup', {
      logGroupName: '/aws/codebuild/remove-resource-pipeline/dry-run-project',
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
    const runProjectLogGroup = new logs.LogGroup(this, 'RunProjectLogGroup', {
      logGroupName: '/aws/codebuild/remove-resource-pipeline/run-project',
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // CodeBuild
    const dryRunProject = new codebuild.PipelineProject(this, 'DryRunProject', {
      role: codebuildRole,
      logging: {
        cloudWatch: {
          logGroup: dryRunProjectLogGroup,
        },
      },
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
        privileged: true,
      },
      environmentVariables: {
        DOCKER_HUB_SECRET_ARN: {
          value: props.dockerhubSecretsManagerArn,
        },
        ASSUME_ROLE_ARN: {
          value: nukeRole.roleArn,
        },
        AWS_NUKE_DRY_RUN: {
          value: true,
        },
      },
    });
    const runProject = new codebuild.PipelineProject(this, 'RunProject', {
      role: codebuildRole,
      logging: {
        cloudWatch: {
          logGroup: runProjectLogGroup,
        },
      },
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
        privileged: true,
      },
      environmentVariables: {
        DOCKER_HUB_SECRET_ARN: {
          value: props.dockerhubSecretsManagerArn,
        },
        ASSUME_ROLE_ARN: {
          value: nukeRole.roleArn,
        },
        AWS_NUKE_DRY_RUN: {
          value: false,
        },
      },
    });

    // CodePipeline
    const dryRunAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'DryRun',
      project: dryRunProject,
      input: sourceOutput,
    });

    const runAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'Run',
      project: runProject,
      input: sourceOutput,
    });

    const approvalActions = new codepipeline_actions.ManualApprovalAction({
      actionName: 'Approval',
    });

    new codepipeline.Pipeline(this, 'Pipeline', {
      stages: [
        {
          stageName: 'Source',
          actions: [sourceAction],
        },
        {
          stageName: 'DryRun',
          actions: [dryRunAction],
        },
        {
          stageName: 'Approval',
          actions: [approvalActions],
        },
        {
          stageName: 'Run',
          actions: [runAction],
        },
      ],
    });
  }
}

実行してみる

リソースの作成ができたら、実行してみましょう。

マネジメントコンソール > CodePipeline > パイプライン > [作成したパイプライン]の順番で選択します。

変更をリリースする を選択すると、パイプラインが動作します。

以下はDry-Run用のCodeBuildのログです。Dry-Runの結果が確認できています。

結果が問題なければ、手動承認を行うことでリソースの削除用のCodeBuildが実行されます。

おわりに

Codeシリーズを使って、aws-nukeを実行してみました。

CodePipelineでは、簡単に手動承認ステップを入れることができていいですね。

以前紹介したGithub Actionsを使う方法では、Githubを使う必要がありましたが、CodeシリーズであればAWS内で完結する点も良いなと思いました。

ただ、この仕組みをマルチアカウントで導入する際には、CodeCommitやDockerhub用のSecretsManagerをどのアカウントに置くか検討が必要かもしれません。 (個別に作るのは中々面倒になりそうだから、クロスアカウントでアクセスできるようにするとか)

以上、AWS事業本部の佐藤(@chari7311)でした。