【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.py
とindex.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]. )
はい、無事削除に失敗しました。
なので面倒ですが、手動で空にしてバケット削除してきます!
ではでは今回はここまでです。読んでいただいてありがとうございました!