はじめに
こんにちは、モンハンワイルズに向けてサンブレイクでガンランス練習中のアプリケーションサービス部ディベロップメントサービス1課の北出です。
AWS CDKを使っているお客様からCloudFormationでサポートされていないリソースをIaCで管理したいという要望があり、対象箇所のみカスタムリソースで作成することになりました。 その際、AWS CDKとカスタムリソースについて少し勉強しましたので備忘録も含めて書かせていただきます。
AWS CDK とは
AWS Cloud Development Kit(AWS CDK)は、TypeScript、Python、Javaなどの一般的なプログラミング言語を使用して、AWSリソースをコードとして定義し、AWS CloudFormationを通じてプロビジョニングするためのオープンソースのソフトウェア開発フレームワークです。 AWS CDKを使用することで、開発者は慣れ親しんだプログラミング言語でインフラストラクチャを定義でき、コードの再利用性や保守性が向上します。 また、AWS CloudFormationとの連携により、インフラストラクチャのデプロイや管理が効率化されます。
AWS CDK は以下のような特徴を持っています。
- プログラミング言語のサポート
- TypeScript、JavaScript、Python、Java、C#などの言語をサポートしており、開発者は馴染みのある言語でインフラストラクチャを定義できます。
- AWS CloudFormationとの統合
- AWS CloudFormationを通じてリソースをプロビジョニングするため、エラー時のロールバックやデプロイの予測可能性などの利点を享受できます
カスタムリソースとは
カスタムリソースとは、CloudFormationの機能で、標準のリソースタイプでは対応できない特定のプロビジョニングロジックや操作を、ユーザーが独自に定義して実行できる機能です。これにより、CloudFormationがサポートしていないリソースや、複雑な設定を含むリソースをテンプレート内で管理することが可能になります。 公式ドキュメント
AWS CDKでは、CustomResource
クラスを使用してカスタムリソースを定義することができます。
ただし、カスタムリソースの利用はエラーハンドリングや結果の出力などで独自のロジックを含むため、適切に設計・実装する必要があります。
一般的にカスタムリソースではLambda関数を使うことが多いですが、エラーハンドリングやCloudFormationへのレスポンスの定義など慣れていないと実装が難しいものとなっています。 今回はLambdaを使わずに、AwsSdkCallでカスタムリソースを作ってみます。(内部ではLambda関数が作成されていますが)
今回の目的
今回はAWS CDK とカスタムリソースをチュートリアルということで、以下のようなテンプレートを作成します。
- 2つのバケットを作成する
- 1つのバケットに初期データファイルをアップロードする(cdkにサポートされたカスタムリソース)
- 1つ目のバケットから2つ目のバケットに初期データファイルをコピーする(cdkにサポートされていないカスタムリソース)
チュートリアルということで、実用性は低いですが、手順やコードの書き方などで参考になれば幸いです。
やってみる
AWS CDK のインストール
AWS CDK をインストールする前に、Node.jsをインストールします。
Linuxの場合
# nvm のインストール curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash # nvm を有効化 source ~/.bashrc # または `source ~/.zshrc` # Node.js のインストール nvm install --lts # バージョン確認 node -v npm -v
Node.jsがインストールされたら、以下のコマンドでAWS CDKをインストールします。
npm install -g aws-cdk
以下のコマンドでCDKが正しくインストールされたか確認します
cdk --version
AWS CDK プロジェクトの初期化
以下のコマンドでCDK プロジェクトを初期化します。
mkdir cdk-s3-example cd cdk-s3-example cdk init app --language python
初期化に成功すると以下のファイルが作成されます
. ├── README.md ├── app.py ├── cdk.json ├── cdk_s3_example │ ├── __init__.py │ └── cdk_s3_example_stack.py ├── requirements-dev.txt ├── requirements.txt ├── source.bat └── tests ├── __init__.py └── unit ├── __init__.py └── test_cdk_s3_example_stack.py
S3バケットを作成するCDKを作成する
cdk_s3_example/cdk_s3_example_stack.py
を以下のように記述します
from aws_cdk import ( Stack, aws_s3 as s3, RemovalPolicy, ) from constructs import Construct class CdkS3ExampleStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # ソースバケットを作成 source_bucket = s3.Bucket( self, "SourceBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 ) # デスティネーションバケットを作成 destination_bucket = s3.Bucket( self, "DestinationBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 )
他のファイルは変更せずに以下のコマンドを実行します
cdk synth
このコマンドでCloudFormationテンプレートが作成されます
次に以下のコマンドを実行します
cdk deploy
このコマンドでCloudFormationスタックが作成されます。
2つのS3バケットが作成されることを確認してください。
初期データファイルをアップロードする
プロジェクトのルートディレクトリにinitial-data/
フォルダを作成し、cdk-s3-example/initial-data/file.txt
ファイルを作成します。
このファイルをsource_bucketに追加します
aws_cdk.aws_s3_deployment
を使用することで、作成したS3バケットにファイルをアップロードする処理を追加することができます。
このモジュールもカスタムリソースを使用しており、内部ではLambda関数を作成しています。 CDKではLambda関数を作成せずに簡易にカスタムリソースを作成することもできます。
cdk-s3-example/cdk_s3_example/cdk_s3_example_stack.py
を以下のように記述します
from aws_cdk import ( Stack, aws_s3 as s3, aws_s3_deployment as s3_deployment, RemovalPolicy, ) from constructs import Construct class CdkS3ExampleStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # ソースバケットを作成 source_bucket = s3.Bucket( self, "SourceBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 ) # デスティネーションバケットを作成 destination_bucket = s3.Bucket( self, "DestinationBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 ) # 初期データをアップロード initial_data_deployment = s3_deployment.BucketDeployment( self, "DeployInitialData", sources=[ s3_deployment.Source.asset("initial-data") ], # initial-dataディレクトリ内のファイル destination_bucket=source_bucket, ) # 明示的な依存関係を追加 initial_data_deployment.node.add_dependency(source_bucket)
バケット作成後にデータアップロード処理を行う必要があるため、明示的な依存関係を追加しています。
S3_deployment.BucketDeploymentの公式ドキュメント
S3バケット内のファイルをコピー
aws_cdk.Custom_resource.AwsCustomResource
とaws_cdk.Custom_resource.AwsSdkCall
を使用することで、AWS SDKで可能な操作をLambda関数を作成せずにカスタムリソースで定義することができます。
今回はCopyObjectをしていますが、幅広く応用できそうです。
AwsCustomResource
では実行ロールを定義する必要があるので忘れないようにしてください。
データアップロード処理の後に行う必要があるため、明示的な依存関係を追加しています。
cdk-s3-example/cdk_s3_example/cdk_s3_example_stack.py
を以下のように記述します
from aws_cdk import ( Stack, aws_s3 as s3, aws_s3_deployment as s3_deployment, RemovalPolicy, aws_iam as iam, custom_resources as cr, Duration, ) from constructs import Construct class CdkS3ExampleStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # ソースバケットを作成 source_bucket = s3.Bucket( self, "SourceBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 ) # デスティネーションバケットを作成 destination_bucket = s3.Bucket( self, "DestinationBucket", versioned=False, # バージョニングを無効化 removal_policy=RemovalPolicy.DESTROY, # スタック削除時にバケットも削除 auto_delete_objects=True, # バケットの中身も削除 ) # 初期データをアップロード initial_data_deployment = s3_deployment.BucketDeployment( self, "DeployInitialData", sources=[ s3_deployment.Source.asset("initial-data") ], # initial-dataディレクトリ内のファイル destination_bucket=source_bucket, ) # カスタムリソースのIAMロールを作成 custom_resource_role = iam.Role( self, "CustomResourceRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), ) # IAMロールにS3の権限を付与 custom_resource_role.add_to_policy( iam.PolicyStatement( actions=[ "s3:ListBucket", # バケットのリスト操作 "s3:GetObject", # オブジェクトの読み取り "s3:CopyObject", # オブジェクトのコピー "s3:PutObject", # オブジェクトの書き込み ], resources=[ source_bucket.bucket_arn, # ソースバケットへの操作 f"{source_bucket.bucket_arn}/*", # ソースバケット内のすべてのオブジェクト destination_bucket.bucket_arn, # デスティネーションバケットへの操作 f"{destination_bucket.bucket_arn}/*", # デスティネーションバケット内のすべてのオブジェクト ], ) ) # ソースバケットからデスティネーションバケットへのコピー custom_provider = cr.AwsCustomResource( self, "CopyObjects", on_create=cr.AwsSdkCall( action="copyObject", service="S3", parameters={ "Bucket": destination_bucket.bucket_name, "CopySource": f"{source_bucket.bucket_name}/file.txt", "Key": "file.txt", }, physical_resource_id=cr.PhysicalResourceId.of("CopyObjects"), ), role=custom_resource_role, timeout=Duration.seconds(30), ) # 明示的な依存関係を追加 initial_data_deployment.node.add_dependency(source_bucket) custom_provider.node.add_dependency(initial_data_deployment)
実装完了
ここまでできたら、cdk synth
とcdk deploy
を実行すると、2つのバケットにファイルがあることが確認できると思います。
まとめ
今回は、AWS CDKとカスタムリソースを使ってみました。CloudFormationでサポートされていないリソースをIaCで管理したい場合などでカスタムリソースを使う必要があるケースもあるかと思いますので参考になれば幸いです。 ちなみに、CloudFormationでサポートされていなくても、Terraformではサポートされているリソースもあります。(AWS StorageGateway など) カスタムリソースはエラーハンドリングや作成、更新、削除ごとに挙動を定義するといった手間もかかりますので、使用の際はしっかりと検討することをお勧めします。