/ #CDK #custom resources 

【CDK/Python】Stack削除と同時にアイテム入りのS3バケットを削除する。カスタムリソースを使って

もくじ

あけましておめでとうございます! sisiiです。

何がしたいの?

今回はカスタムリソースを使ってS3バケットをdeployして削除する方法を書きます。

そもそもなぜそんなことが必要になるかというと、CloudFormationあるあるでStack削除時に削除が失敗!理由はバケットにアイテムが入っているからです!とうのがありまして、その場合にいちいちバケットを空にするのが面倒なのでそこもCDKにやらせたい、とうのが目論見です。

今回作ったものをcdk-diaで図示にするとこんな感じっぽいです。

毎回図をdraw.ioで描くのは大変なので、以降cdk-diaを使っていこうかなと思います。ちょっと見づらいですが、だいたい分かると思うので。

とか思っていたのですが、今回は分かりづらい。なので保続説明します。

手動で作っているLambdaはCustomResourcesLambdaただ1つです。それ以外のLambdaはCDK側が良しなに用意してくれたものです。

あとStackがたくさんあるように見えますが、一番外のStackを除くとStackは2つです。(命名も末尾に〜Stackにしたはずなのですが、微妙に反映されていない、、、cdk-diaを私が使いこなせてないのが原因ですかねー汗)

前提

  • CDK v2

では 作っていきましょう👻

全体の流れですが、まずバケットをdeployしてアイテムを入れるStackを作ります。次にCustomResourcesで削除時にバケットのアイテムをすべて削除するStackを作ります。最後に2つのStackの依存関係を定義します。

ではでは

0. プロジェクトの用意

$ mkdir cdk-empty-bucket-on-delete && cd $_
$ cdk init --language python

# 仮想環境とかはよしなに!

$ pip install -r requirements.txt

以上! CDKv2だとすごくさっぱりして良いですね!

1. バケットとアイテムをdeployするStack

プロジェクト直下に下記な感じのディレクトリを作ります

.
├── sample_bucket
│   ├── __init__.py # 中身空
│   ├── infrastructure.py
│   └── sample_data
│       └── item.txt
...

sample_data配下はバケットに入れるSampleデータなので何でも良いです!

infrastructure.pyはこんな感じ↓

import pathlib

from aws_cdk import RemovalPolicy
from aws_cdk.aws_s3 import Bucket
from aws_cdk.aws_s3_deployment import BucketDeployment, Source
from constructs import Construct


class SampleBucket(Construct):
    @property
    def bucket(self):
        return self._bucket

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id)

        bucket = Bucket(
            self,
            "Bucket",
            removal_policy=RemovalPolicy.DESTROY,
        )

        BucketDeployment(
            self,
            "BucketDeployment",
            sources=[Source.asset(str(pathlib.Path(__file__).resolve().parent.joinpath("sample_data")))],
            destination_bucket=bucket,
        )
        self._bucket = bucket

バケットを作って、sample_data配下をdeployしてるだけです!

後々バケット自体は他のStackで使いたかったのでpropertyをもたせています。

2. 削除時にバケットのアイテムをすべて削除するCustomResourcesのStack

こんな感じの構成

.
├── empty_bucket_on_delete_custom_resources
│   ├── __init__.py # 中身空
│   ├── infrastructure.py
│   └── runtime
│       └── index.py
...

infrastructure.pyindex.pyの中身はこんな感じ

まずインフラ

import pathlib

from aws_cdk import CustomResource, Duration
from aws_cdk.aws_lambda import Code, Function, Runtime
from aws_cdk.aws_s3 import Bucket
from aws_cdk.custom_resources import Provider
from constructs import Construct


class EmptyBucketOnDeleteCustomResources(Construct):
    def __init__(self, scope: Construct, construct_id: str, *, bucket: Bucket) -> None:
        super().__init__(scope, construct_id)

        on_event = Function(
            self,
            "CustomResourcesLambda",
            code=Code.from_asset(str(pathlib.Path(__file__).resolve().parent.joinpath("runtime"))),
            runtime=Runtime.PYTHON_3_9,
            timeout=Duration.seconds(60),
            handler="index.on_event",
            environment={"BUCKET_NAME": bucket.bucket_name},
        )
        bucket.grant_read(on_event)  # `ListObjects`するために必要
        bucket.grant_delete(on_event)

        provider = Provider(self, "Provider", on_event_handler=on_event)
        CustomResource(
            self,
            "CustomResource",
            service_token=provider.service_token,
        )

CustomResources用のLambdaを作成して権限を付与しているだけです。

そしてLambdaの中身はこちら

import os

import boto3


def on_event(event, context):
    if event["RequestType"] == "Delete":
        if bucket_name := os.getenv("BUCKET_NAME"):
            s3 = boto3.resource("s3")
            bucket = s3.Bucket(bucket_name)
            bucket.objects.all().delete()

RequestTypeが削除だった場合のみ、環境変数からバケット名を取得してObject全部削除します!そのまんまですね

3. Stackの依存関係を定義する

ここはプロジェクトを作ったときに作られるStackにベタベタ書いていきます

from aws_cdk import Stack  # Duration,; aws_sqs as sqs,
from constructs import Construct

from empty_bucket_on_delete_custom_resources.infrastructure import EmptyBucketOnDeleteCustomResources
from sample_bucket.infrastructure import SampleBucket


class CdkEmptyBucketOnDeleteStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Sampleバケットを作る
        sample_bucket_stack = Stack(self, "SampleBucketStack")
        sample_bucket = SampleBucket(sample_bucket_stack, "SampleBucket")

        # stack削除時にバケットを空にするカスタムポリシーを作る
        empty_bucket_on_delete_custom_resources_stack = Stack(self, "EmptyBucketOnDeleteCustomResourcesStack")
        EmptyBucketOnDeleteCustomResources(
            empty_bucket_on_delete_custom_resources_stack,
            "EmptyBucketOnDeleteCustomResources",
            bucket=sample_bucket.bucket,
        )

        # Stackの依存関係を定義する
        empty_bucket_on_delete_custom_resources_stack.add_dependency(sample_bucket_stack)

Stackを各々作って依存関係を入れて、バケットより先にCustomResourcesのStackが動くようにします。

動作確認

# デプロイ!!
cdk deploy --all

# 削除!!
cdk destroy --all
CdkEmptyBucketOnDeleteStackEmptyBucketOnDeleteCustomResourcesStack8C2CD9B9: destroying...
 ✅  CdkEmptyBucketOnDeleteStackEmptyBucketOnDeleteCustomResourcesStack8C2CD9B9: destroyed
 ✅  CdkEmptyBucketOnDeleteStackSampleBucketStack7E8EF32F: destroyed
 ✅  CdkEmptyBucketOnDeleteStack: destroyed

はい。無事削除できました!

余談で、CustomResourcesないとどうなるのか?というのも一応やっておきます!

このへん↓コメントアウトします


...

class CdkEmptyBucketOnDeleteStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:

        ...

        # # stack削除時にバケットを空にするカスタムポリシーを作る
        # empty_bucket_on_delete_custom_resources_stack = Stack(self, "EmptyBucketOnDeleteCustomResourcesStack")
        # EmptyBucketOnDeleteCustomResources(
        #     empty_bucket_on_delete_custom_resources_stack,
        #     "EmptyBucketOnDeleteCustomResources",
        #     bucket=sample_bucket.bucket,
        # )

        # # Stackの依存関係を定義する
        # empty_bucket_on_delete_custom_resources_stack.add_dependency(sample_bucket_stack)

からの

# デプロイ!!
cdk deploy --all

# 削除!!
cdk destroy --all
 ❌  CdkEmptyBucketOnDeleteStackSampleBucketStack7E8EF32F: destroy failed Error: The stack named CdkEmptyBucketOnDeleteStackSampleBucketStack7E8EF32F is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [SampleBucketD42019E7]. )
 ...
The stack named CdkEmptyBucketOnDeleteStackSampleBucketStack7E8EF32F is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [SampleBucketD42019E7]. )

はい、無事削除に失敗しました。

なので面倒ですが、手動で空にしてバケット削除してきます!

ではでは今回はここまでです。読んでいただいてありがとうございました!

今回のリポジトリはこちら

https://github.com/sisi100/cdk-empty-bucket-on-delete

Author

Sisii

インフラが好きなエンジニアぶってるなにか