【初心者向け】AWS CDK (Python) の始め方とカスタムリソースの作成方法

記事タイトルとURLをコピーする

はじめに

こんにちは、モンハンワイルズに向けてサンブレイクでガンランス練習中のアプリケーションサービス部ディベロップメントサービス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 とカスタムリソースをチュートリアルということで、以下のようなテンプレートを作成します。

  1. 2つのバケットを作成する
  2. 1つのバケットに初期データファイルをアップロードする(cdkにサポートされたカスタムリソース)
  3. 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の公式ドキュメント

AwsSdkCallの公式ドキュメント

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 など) カスタムリソースはエラーハンドリングや作成、更新、削除ごとに挙動を定義するといった手間もかかりますので、使用の際はしっかりと検討することをお勧めします。

北出 宏紀(執筆記事の一覧)

アプリケーションサービス部ディベロップメントサービス1課

2024年9月中途入社です。 毎朝1時間資格勉強継続中です。

"; doc.innerHTML = entry_notice + doc.innerHTML; }
' } }) e.innerHTML = codeBlock; });