AWS CDKで別リージョンにスタックをデプロイしてパラメータをリージョン間で受け渡す方法 -AWS CDKカスタムリソースの実装例

小西秀和です。
これまで、次の記事のようなAWSの静的ウェブサイトホスティングをテーマにAWS CloudformationやAWS Amplifyの使用例を紹介してきました。

今回からはAWS Cloud Development Kit(AWS CDK)でこれまでの静的ウェブサイトホスティングと同様の構成を作成する方法について見ていきたいと思います。
これまでの記事で扱ったAWS CloudformationではAWS Lambdaカスタムリソースを使用してマルチリージョンにスタックをデプロイする方法を試してきました。
参考:AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする

一方、AWS CDKを使用するとマルチリージョンへのスタックのデプロイは簡単に指定できるようになっています。
また、AWS CDKで用意されているカスタムリソース(AwsCustomResource)ではAWS SDK呼び出しが簡単に記述できるため、リージョン間のパラメータの受け渡しにリージョンを指定したAWS CDKカスタムリソースを使用することが可能です。今回はこれらの実装例を紹介します。

なお、AWS CDK V1ではJavaScript、TypeScript、Python、Java、C#のプログラミング言語がサポートされています。
このうち、技術情報が多く、テストツールなど最新の機能の取り込みも充実して一般的に広く使われているのはTypeScriptです。
ただ、Pythonにも一定のニーズがある一方でコード例などの技術情報が少ないため、自主研究ベースの私の記事では敢えてPythonを使ってみたいと思います。

※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。

AWS CDKプロジェクトのスタック毎にリージョンを指定する

AWS CDKではプロジェクトでスタックをリージョン指定で作成することができます。
AWS CDKプロジェクト内のapp.pyでスタックにリージョンを指定した例を記載します。

■app.py

#!/usr/bin/env python3
import os
from aws_cdk import core as cdk
from aws_cdk import core

from s3cf_acm_edge_s3sec.certificate_stack import CertificateStack
from s3cf_acm_edge_s3sec.lambda_edge_stack import LambdaEdgeStack
from s3cf_acm_edge_s3sec.s3secondary_stack import S3secondaryStack
from s3cf_acm_edge_s3sec.s3cloudfront_stack import S3CloudfrontStack

app = core.App()
certificate_stk = CertificateStack(app, "CdkS3CfAllCertificateStack", env=core.Environment(region='us-east-1'))
lambda_edge_stk = LambdaEdgeStack(app, "CdkS3CfAllLambdaEdgeStack", env=core.Environment(region='us-east-1'))
s3secondary_stk = S3secondaryStack(app, "CdkS3CfAllS3secondaryStack", env=core.Environment(region='us-east-1'))
s3cloudfront_stk = S3CloudfrontStack(app, "CdkS3CfAllS3CloudfrontStack", env=core.Environment(region='ap-northeast-1'))

s3cloudfront_stk.add_dependency(certificate_stk)
s3cloudfront_stk.add_dependency(s3secondary_stk)
s3cloudfront_stk.add_dependency(lambda_edge_stk)

app.synth()

この例ではS3CloudfrontStackをap-northeast-1リージョンで作成し、その他のスタックをus-east-1で作成しています。
別リージョンで作成するスタックもadd_dependencyで依存関係を指定して、作成順序を指定することが可能です。

このようにAWS CDKでは簡単にスタックを作成するリージョンを指定できますが、スタック間でパラメータをやり取りするためにはAWS CDKカスタムリソースを使用するなど工夫が必要になります。

スタック間のパラメータの受け渡しはリージョン指定のAWS CDKカスタムリソースを使用する

スタック間のパラメータの受け渡しはAWS CDKのカスタムリソースでAWS SDK呼び出しをリージョンを指定して実行し、ストレージサービスなどにデータを保存するといった方法で実現できます。

今回はAWS Systems ManagerパラメータストアとAWS Secrets Managerに対してパラメータの保存と取得をする機能をAWS CDKカスタムリソース用のConstructを継承したクラスとしてまとめた例を記載します。

■x_region_param.py

from aws_cdk import (
    core,
    custom_resources as cr
)

class XRegionParam(core.Construct):
    def __init__(self, scope: core.Construct, id: str, region, service, action, key, val, description, **kwargs):
        super().__init__(scope, id, **kwargs)

        stack = core.Stack.of(self);
        param_region = region
        #リージョン指定がない場合は呼出元スタックのリージョンを使用する
        if not param_region:
            param_region = stack.region

        param_service = str(service).upper()
        param_action = str(action).upper()

        if param_service == 'SSM': 
            if param_action == 'GET':
                act = 'SsmGet'
                res = self.ssm_get_parameter(action + id, param_region, key)
            elif param_action == 'PUT':
                act = 'SsmPut'
                res = self.ssm_put_parameter(action + id, param_region, key, val, description)
            else:
                act = None
                res = None
        elif param_service == 'ASM' or param_service == 'SECRETSMANAGER': 
            if param_action == 'GET':
                act = 'AsmGet'
                res = self.asm_get_secret_string(action + id, param_region, key)
            elif param_action == 'PUT':
                act = 'AsmPut'
                res = self.asm_put_secret_string(action + id, param_region, key, val, description)
            else:
                act = None
                res = None
        else:
            act = None
            res = None

        self.action = act
        self.result = res
        
    def get_result(self):
        return {'action': self.action, 'result': self.result}
        
    #AWS Systems Managerパラメータストアからリージョン名、パラメータ名を指定してパラメータを取得する
    def ssm_get_parameter(self, id_name, region, parameter_name):
        stack = core.Stack.of(self);
        param_region = region
        #リージョン指定がない場合は呼出元スタックのリージョンを使用する
        if not param_region:
            param_region = stack.region

        #AWS CDKカスタムリソース内でAWS SDK呼出を実行してパラメータを取得する
        result_params = cr.AwsCustomResource(self, id_name, 
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ), 
            on_update=cr.AwsSdkCall(
                service='SSM',
                action='getParameter',
                parameters={
                    'Name': parameter_name
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            )
        )
        #レスポンス内の要素を指定して取得する:{"Parameter":{"Value":"<取得するパラメータ>"}}
        result = result_params.get_response_field('Parameter.Value')
        
        return result
        
    #AWS Systems Managerパラメータストアにリージョン名、パラメータ名を指定してパラメータを作成・更新・削除する
    def ssm_put_parameter(self, id_name, region, parameter_name, string_value, description):
        stack = core.Stack.of(self);
        param_region = region
        #リージョン指定がない場合は呼出元スタックのリージョンを使用する
        if not param_region:
            param_region = stack.region

        #AWS CDKカスタムリソース内でAWS SDK呼出を実行してパラメータを作成・更新する
        result = cr.AwsCustomResource(self, id_name, 
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ), 
            on_update=cr.AwsSdkCall(
                service='SSM',
                action='putParameter',
                parameters={
                    'Name': parameter_name,
                    'Value': string_value,
                    'Description': description, 
                    'Type': 'String',
                    'Overwrite': True
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            ),
            on_delete=cr.AwsSdkCall(
                service='SSM',
                action='deleteParameter',
                parameters={
                    'Name': parameter_name
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            )
        )

        return result

    #AWS Systems Managerパラメータストアからリージョン名、パラメータ名を指定してパラメータを取得する
    def asm_get_secret_string(self, id_name, region, secret_name):
        stack = core.Stack.of(self);
        param_region = region
        #リージョン指定がない場合は呼出元スタックのリージョンを使用する
        if not param_region:
            param_region = stack.region

        #リソースポリシーのリソースに「cr.AwsCustomResourcePolicy.ANY_RESOURCE」を指定するとactionに対してすべてのリソースのアクセス許可をする
        #AWS CDKカスタムリソース内でAWS SDK呼出を実行してシークレットを取得する
        result_params = cr.AwsCustomResource(self, id_name, 
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ), 
            on_update=cr.AwsSdkCall(
                service='SecretsManager',
                action='getSecretValue',
                parameters={
                    'SecretId': secret_name
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            )
        )
        #レスポンス内の要素を指定して取得する:{"SecretString":"<取得するパラメータ>"}
        result = result_params.get_response_field('SecretString')
        
        return result
        
    #AWS Systems Managerパラメータストアにリージョン名、パラメータ名を指定してパラメータを作成・更新・削除する
    def asm_put_secret_string(self, id_name, region, secret_name, secret_string, description):
        stack = core.Stack.of(self);
        param_region = region
        #リージョン指定がない場合は呼出元スタックのリージョンを使用する
        if not param_region:
            param_region = stack.region

        #AWS CDKカスタムリソース内でAWS SDK呼出を実行してパラメータを更新する
        result = cr.AwsCustomResource(self, id_name, 
            policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
                resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
            ), 
            on_create=cr.AwsSdkCall(
                service='SecretsManager',
                action='createSecret',
                parameters={
                    'Name': secret_name,
                    'SecretString': secret_string,
                    'Description': description
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            ), 
            on_update=cr.AwsSdkCall(
                service='SecretsManager',
                action='updateSecret',
                parameters={
                    'SecretId': secret_name,
                    'SecretString': secret_string,
                    'Description': description
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            ),
            on_delete=cr.AwsSdkCall(
                service='SecretsManager',
                action='deleteSecret',
                parameters={
                    'SecretId': secret_name,
                    'RecoveryWindowInDays': 7
                },
                region=param_region,
                physical_resource_id=cr.PhysicalResourceId.of(id_name)
            )
        )
        
        return result

■x_region_param.pyの呼び出し例

from aws_cdk import core as cdk
from x_region_param import XRegionParam
from aws_cdk import (
    core
)

class SampleCdkStack(cdk.Stack):

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

        #AWS Systems Managerパラメータストアのパラメータの作成・更新
        resut_put_param = XRegionParam(self, 'SsmPut', 
            region='us-east-1', 
            service='SSM', 
            action='PUT', 
            key='parameter_name', 
            val='Nobody', 
            description='SSM Param for Sample'
        )

        #AWS Secrets Managerシークレットの作成・更新
        resut_put_secret = XRegionParam(self, 'AsmPut', 
            region='us-east-1', 
            service='ASM', 
            action='PUT', 
            key='Sample/Secret/Value', 
            val='{"Password":"Nobody"}', 
            description='ASM Param for Sample'
        )

        #AWS Systems Managerパラメータストアのパラメータの取得
        resut_get_param = XRegionParam(self, 'SsmPut', 
            region='us-east-1', 
            service='SSM', 
            action='GET', 
            key='parameter_name', 
            val='', 
            description=''
        )
        ssm_parameter = resut_get_param.get_result()['result']

        #AWS Secrets Managerシークレットの取得
        resut_get_secret = XRegionParam(self, 'AsmPut', 
            region='us-east-1', 
            service='ASM', 
            action='GET', 
             key='Sample/Secret/Value', 
            val='', 
            description=''
        )
        asm_parameter = resut_get_secret.get_result()['result']

AWS CDKのカスタムリソースの特徴

AWS CDKのカスタムリソースはAWS CloudFormationのAWS Lambdaカスタムリソースのように一から開発する必要なく、AWS CDKのコードをCloudFormationに変換してデプロイするコマンドであるaws cdk deployを実行した際に自動的にAWS SDK呼び出しの内容を実行するAWS Lambda関数を作成してくれます。
ただ、開発が少なく済む一方で柔軟な細かい内部処理やエラーハンドリングができないことに配慮が必要です。

AWS CDKのカスタムリソースはaws cdk deployを実行すると各スタック毎にAWS Lambda関数が作成され、そのAWS Lambda関数内でAwsCustomResourceに記述した内容が実行されます。そのため、スタック毎に用意されたAWS Lambda関数にスタックで使用する各カスタムリソースのアクションやリソースポリシーのアクセス権限が追加されていくことを知っておいたほうが良いでしょう。
これを概念図にすると次のようになります。

AWS CDKカスタムリソース用AWS Lambda関数とスタックの関係
AWS CDKカスタムリソース用AWS Lambda関数とスタックの関係

例えば、あるスタックで前述のConstructを継承したクラスのssm_get_parameterでパラメータ取得、asm_put_secret_stringでシークレット作成・更新をする場合は、「AwsCustomResourcePolicy.ANY_RESOURCE」のリソースポリシーの指定によってSSMのgetParameterアクションに対するすべてのリソースへのアクセス権限、SecretsManagerのupdateSecretcreateSecretアクションに対するすべてのリソースへのアクセス権限が、そのスタックのために用意されたカスタムリソースのAWS Lambda関数に付与されます。
「AwsCustomResourcePolicy.ANY_RESOURCE」を使用せずにリソース毎に細かく権限を指定する場合は、その内容がカスタムリソースのAWS Lambda関数に適用されているIAMロールのインラインポリシーへリソース毎に追加されていくため、扱うリソース数が多数の場合はその点も知っておいたほうがよいでしょう。

aws cdk deployによってAWS CloudFormationのCreate、Update、Deleteのイベントが発生すると、AWS CDKのカスタムリソースではそれに対応してon_createon_updateon_deleteが実行されます(on_createの指定がない場合はCreateイベント発生時にon_updateが実行される)。
前述のConstructを継承したクラスでは各処理に対応するようにAWS CDKカスタムリソースが実行されるように記載しています。

AWS CDKのカスタムリソースの詳細な仕様については次のドキュメントで確認できます。
class AwsCustomResource (construct) · AWS CDK

また、AwsCustomResource内で使用するAwsSdkCallのサービス、アクション、パラメータの記述の仕様は次のAWS SDK for JavaScriptのドキュメントで確認できます。
AWS SDK for JavaScript

まとめ

今回はAWS CDKでスタックを複数のリージョンを指定してデプロイし、AWS CDKカスタムリソースのAWS SDK呼び出しでパラメータをリージョン間で受け渡す方法について記載しました。
次回からはこれらの方法を使用して「AWS LambdaカスタムリソースでSSL証明書・基本認証・CloudFrontオリジンフェイルオーバーを作成するAWS Cloudformationスタックを別リージョンにデプロイする」で紹介したようなAmazon S3+Amazon CluodFrontの静的ウェブサイトホスティングに別リージョンで作成したACM証明書、基本認証用Lambda@Edge、Amazon CloudFrontオリジンフェイルオーバーを追加する構成をAWS CDKで作成するとどのようになるかについて見ていきたいと思います。

Hidekazu Konishi, ALL AWS Certifications Engineer

執筆者小西秀和

ALL AWS Certifications Engineer(AWS認定全取得)の知識をベースにAWSクラウドの活用に取り組んでいます。