レイヤー 3 コンストラクト - AWS 規範ガイダンス

レイヤー 3 コンストラクト

L1 コンストラクトでは CloudFormation リソースをプログラムコードにそのまま変換し、L2 コンストラクトでは冗長な CloudFormation 構文の多くをヘルパーメソッドやカスタムロジックに置換しました。では、L3 コンストラクトではどのような処理を行うのでしょうか。それは、ユーザーの想像力によって決まります。レイヤー 3 は、どのような特定のユースケースにも合うよう作成できるのです。例えば、プロジェクトで特定のプロパティサブセットを持つリソースが必要な場合は、再利用可能な L3 コンストラクトを作成して、そうしたニーズを満たすことができます。

AWS CDK 内では、L3 コンストラクトをパターンと呼びます。パターンとは、AWS CDK の Construct クラスを継承する (または Construct クラスを継承するクラスを継承する) オブジェクトであり、レイヤー 2 を超える抽象化が行われたロジックを実行するものです。AWS CDK CLI で cdk init を実行して、新しい AWS CDK プロジェクトを開始する場合、AWS CDK アプリケーションタイプを applibsample-app の 3 つから選択する必要があります。

AWS CDK のアプリケーションタイプ

appsample-app はどちらも、従来の AWS CDK アプリケーションを表すもので、これによって、CloudFormation スタックを構築し AWS 環境にデプロイします。lib は、まったく新しい L3 コンストラクトを構築する場合に選択します。appsample-app では、AWS CDK のサポート対象言語を任意に選択できますが、lib で選択できるのは TypeScript のみです。なぜなら、AWS CDK は、当初から TypeScript で開発されており、JSii というオープンソースシステムを使用して元のコードを他のサポート対象言語に翻訳するからです。AWS CDK の拡張機能を構築する場合は、lib を選択してプロジェクトを開始します。

Construct クラスを継承するクラスはすべて L3 コンストラクトとして定義できますが、レイヤー 3 の最も一般的なユースケースは、リソースインタラクション、リソース拡張、カスタムリソースです。ほとんどの L3 コンストラクトでは、AWS CDK の機能を拡張するために、これら 3 つのケースの 1 つ以上を使用します。

リソースインタラクション

ソリューションでは、通常、連携して動作する複数の AWS サービスを利用します。例えば、Amazon CloudFront ディストリビューションでは、多くの場合、そのオリジンに S3 バケットを使用し、一般的なエクスプロイトからの保護には AWS WAF を使用します。また、AWS AppSync と Amazon API Gateway では、API のデータソースに Amazon DynamoDB テーブルを使用することが多く、AWS CodePipeline のパイプラインでは、Amazon S3 をソースにし、ビルドステージに AWS CodeBuild を使用することがよくあります。こうした場合に有用なのは、単一の L3 コンストラクトを作成し、これによって、相互接続した 2 つ以上の L2 コンストラクトのプロビジョニングを処理することです。

以下に、L3​​ コンストラクトの例を示します。ここでは次の要素をプロビジョニングします: CloudFront ディストリビューション、その S3 オリジン、ディストリビューションの前に配置する AWS WAF、Amazon Route 53 レコード、AWS Certificate Manager (ACM) 証明書 (これにより、暗号化を備えたカスタムエンドポイントを追加する)。これらすべてが 1 つの再利用可能なコンストラクトにまとめられプロビジョニングされます。

// Define the properties passed to the L3 construct export interface CloudFrontWebsiteProps { distributionProps: DistributionProps bucketProps: BucketProps wafProps: CfnWebAclProps zone: IHostedZone } // Define the L3 construct export class CloudFrontWebsite extends Construct { public distribution: Distribution constructor( scope: Construct, id: string, props: CloudFrontWebsiteProps ) { super(scope, id); const certificate = new Certificate(this, "Certificate", { domainName: props.zone.zoneName, validation: CertificateValidation.fromDns(props.zone) }); const defaultBehavior = { origin: new S3Origin(new Bucket(this, "bucket", props.bucketProps)) } const waf = new CfnWebACL(this, "waf", props.wafProps); this.distribution = new Distribution(this, id, { ...props.distributionProps, defaultBehavior, certificate, domainNames: [this.domainName], webAclId: waf.attrArn, }); } }

CloudFront、Amazon S3、Route 53、ACM ではいずれも、L2 コンストラクトを使用していますが、Web ACL (Web リクエストの処理ルールを定義する) では L1 コンストラクトを使用していることに注意してください。これは、AWS CDK が進歩し続けるオープンソースパッケージであり、完成していないことと、WebAcl の L2 コンストラクトがまだ存在しないことによります。ただし、誰もが、新しい L2 コンストラクトを作成して AWS CDK の進歩に貢献できます。そのような理由で、AWS CDK に WebAcl の L2 コンストラクトが用意されるまでは、L1 コンストラクトを使用しなければなりません。CloudFrontWebsite L3 コンストラクトを使用して新しいウェブサイトを作成するには、次のコードを使用します。

const siteADotCom = new CloudFrontWebsite(stack, "siteA", siteAProps); const siteBDotCom = new CloudFrontWebsite(stack, "siteB", siteBProps); const siteCDotCom = new CloudFrontWebsite(stack, "siteC", siteCProps);

この例では、CloudFront Distribution の L2 コンストラクトを L3 コンストラクトのパブリックプロパティとして公開しています。このように、場合によっては、L3 プロパティの公開が必要なケースも依然として存在します。実際に、以降の「カスタムリソース」セクションでも、Distribution を再度取り上げます。

AWS CDK 内には、このようなリソース相互作用パターンの例が複数見られます。例えば、Amazon Elastic Container Service (Amazon ECS) の L2 コンストラクトが含まれる aws-ecs パッケージのほか、AWS CDK には、aws-ecs-patterns というパッケージもあります。このパッケージ内には、Amazon ECS を Application Load Balancer、Network Load Balancer、ターゲットグループと組み合わせる複数の L3 コンストラクトに加え、Amazon Elastic Compute Cloud (Amazon EC2) と AWS Fargate 向けにプリセットされた異なるバージョンが用意されています。多くのサーバーレスアプリケーションでは Amazon ECS を Fargate とのみ組み合わせて使用するため、こうした L3 コンストラクトを使用すると、開発側の時間と顧客側のコストをともに節約できます。

リソース拡張

一部のユースケースのリソースには、L2 コンストラクトでネイティブではない特定のデフォルト設定が必要です。スタックレベルであれば、アスペクトを使用してこれに対応できますが、L2 コンストラクトに新しいデフォルトを設定するもう 1 つの便利な方法は、レイヤー 2 を継承することです。どのコンストラクトも Construct クラスを継承したコンストラクトであり、L2 コンストラクトはそのクラスを継承するため、L2 コンストラクトを直接継承することでも L3 コンストラクトを作成できます。

これが特に有用なのは、顧客の特定のニーズに対応したカスタムビジネスロジックを実行するときです。AWS Lambda 関数のコードが src/lambda という単一のディレクトリにすべて保存されるリポジトリがあり、ほとんどの Lambda 関数で毎回同じランタイム名とハンドラー名が再利用されているとします。その場合は、Lambda 関数の新規設定のたびにコードパスを設定せず、新しい L3 コンストラクトを作成すると良いでしょう。

export class MyCompanyLambdaFunction extends Function { constructor( scope: Construct, id: string, props: Partial<FunctionProps> = {} ) { super(scope, id, { handler: 'index.handler', runtime: Runtime.NODEJS_LATEST, code: Code.fromAsset(`src/lambda/${props.functionName || id}`), ...props }); }

そうすることにより、リポジトリ内の任意の場所で L2 Function のコンストラクトを次のように置き換えることができます。

new MyCompanyLambdaFunction(this, "MyFunction"); new MyCompanyLambdaFunction(this, "MyOtherFunction"); new MyCompanyLambdaFunction(this, "MyThirdFunction", { runtime: Runtime.PYTHON_3_11 });

デフォルト設定を使用すると、新しい Lambda 関数を 1 行で作成でき、必要に応じてデフォルトのプロパティをオーバーライドできるように L3 コンストラクトを設定することも可能です。

既存の L2 コンストラクトに新しいデフォルト設定を追加するだけの場合に最良なのは、L2 コンストラクトを直接継承することです。他のカスタムロジックも必要な場合は、Construct クラスを継承すると良いでしょう。その理由は、コンストラクタ内で呼び出す super メソッドにあります。他のクラスを継承したクラスでは、super メソッドを使用して親クラスのコンストラクタを呼び出します。これは、コンストラクタ内で最初に実行する必要があります。つまり、渡す引数やその他のカスタムロジックの操作は、元の L2 コンストラクトが作成されたにのみ実行できます。L2 コンストラクトのインスタンス化前にこうしたカスタムロジックを実行する必要がある場合は、「リソースインタラクション」セクションで前述したパターンに従うことをお勧めします。

カスタムリソース

カスタムリソースは、CloudFormation の強力な機能であり、これによって、スタックのデプロイ中にアクティブ化した Lambda 関数からカスタムロジックを実行できます。CloudFormation で直接サポートされていないプロセスがデプロイ中に必要な場合は、カスタムリソースを使用して処理すると良いでしょう。AWS CDK には、カスタムリソースをプログラムで作成可能なクラスも用意されています。L3 コンストラクタ内でカスタムリソースを使用すると、ほぼあらゆるリソースからコンストラクトを作成できます。

Amazon CloudFront を使用する利点の 1 つは、強力なグローバルキャッシュ機能です。キャッシュを手動でリセットしてオリジンへの新しい変更をウェブサイトにすぐに反映したい場合は、CloudFront の無効化を利用できます。ただし、無効化は、CloudFront ディストリビューション上で実行されるプロセスであり、CloudFront ディストリビューションのプロパティではありません。既存のディストリビューションにいつでも作成し適用できるため、プロビジョニングおよびデプロイプロセスに、ネイティブには組み込まれていません。

このシナリオでは、ディストリビューションのオリジンを更新するたびに無効化を作成し実行するという要件が考えられます。カスタムリソースを使用すると、次のような L3 コンストラクトを作成できます。

export interface CloudFrontInvalidationProps { distribution: Distribution region?: string paths?: string[] } export class CloudFrontInvalidation extends Construct { constructor( scope: Construct, id: string, props: CloudFrontInvalidationProps ) { super(scope, id); const policy = AwsCustomResourcePolicy.fromSdkCalls({ resources:AwsCustomResourcePolicy.ANY_RESOURCE }); new AwsCustomResource(scope, `${id}Invalidation`, { policy, onUpdate: { service: 'CloudFront', action: 'createInvalidation', region: props.region || 'us-east-1', physicalResourceId: PhysicalResourceId.fromResponse('Invalidation.Id'), parameters: { DistributionId: props.distribution.distributionId, InvalidationBatch: { Paths: { Quantity: props.paths?.length || 1, Items: props.paths || ['/*'] }, CallerReference: crypto.randomBytes(5).toString('hex') } } } } } }

先ほど CloudFrontWebsite L3 コンストラクトで作成したディストリビューションを使用すると、これを非常に簡単に実行できます。

new CloudFrontInvalidation(this, 'MyInvalidation', { distribution: siteADotCom.distribution });

この L3 コンストラクトでは、AwsCustomResource という AWS CDK L3 コンストラクトを使用して、カスタムロジックを実行するカスタムリソースを作成します。Lambda コードの記述が不要という点を考えると、AwsCustomResource は、AWS SDK 呼び出しを 1 回だけ実行する必要がある場合に非常に便利です。より複雑な要件があり、独自ロジックの実装が必要な場合は、基本的な CustomResource クラスを直接使用すると良いでしょう。

AWS CDK でカスタムリソース L3 コンストラクトを使用するもう 1 つの良い例は、S3 バケットのデプロイです。カスタムリソースによってこの L3 コンストラクトのコンストラクタ内に作成された Lambda 関数は、CloudFormation では処理できない機能、つまり S3 バケットにオブジェクトを追加し更新する機能を備えるようになります。S3 バケットのデプロイ機能がないと、スタックの一部として作成した S3 バケットにコンテンツを追加できないため、非常に不便です。

AWS CDK では、膨大な CloudFormation 構文が不要になります。この基本的な S3BucketDeployment の例を見ると、それが非常によくわかるでしょう。

new BucketDeployment(this, 'BucketObjects', { sources: [Source.asset('./path/to/amzn-s3-demo-bucket')], destinationBucket: amzn-s3-demo-bucket });

これと同じ目的を達成するために、CloudFormation ではどれくらいの量のコードが必要かを比較してみましょう。

"lambdapolicyA5E98E09": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:UpdateFunctionCode", "Effect": "Allow", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function" } ], "Version": "2012-10-17" }, "PolicyName": "lambdaPolicy", "Roles": [ { "Ref": "myiamroleF09C7974" } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/lambda-policy/Resource" } }, "BucketObjectsAwsCliLayer8C081206": { "Type": "AWS::Lambda::LayerVersion", "Properties": { "Content": { "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "S3Key": "e2277687077a2abf9ae1af1cc9565e6715e2ebb62f79ec53aa75a1af9298f642.zip" }, "Description": "/opt/awscli/aws" }, "Metadata": { "aws:cdk:path": "CdkScratchStack/BucketObjects/AwsCliLayer/Resource", "aws:asset:path": "asset.e2277687077a2abf9ae1af1cc9565e6715e2ebb62f79ec53aa75a1af9298f642.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } }, "BucketObjectsCustomResourceB12E6837": { "Type": "Custom::CDKBucketDeployment", "Properties": { "ServiceToken": { "Fn::GetAtt": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", "Arn" ] }, "SourceBucketNames": [ { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" } ], "SourceObjectKeys": [ "f888a9d977f0b5bdbc04a1f8f07520ede6e00d4051b9a6a250860a1700924f26.zip" ], "DestinationBucketName": { "Ref": "amzn-s3-demo-bucket77F80CC0" }, "Prune": true }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { "aws:cdk:path": "CdkScratchStack/BucketObjects/CustomResource/Default" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" } } ], "Version": "2012-10-17" }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ] ] } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "s3:GetBucket*", "s3:GetObject*", "s3:List*" ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":s3:::", { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "/*" ] ] }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition" }, ":s3:::", { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" } ] ] } ] }, { "Action": [ "s3:Abort*", "s3:DeleteObject*", "s3:GetBucket*", "s3:GetObject*", "s3:List*", "s3:PutObject", "s3:PutObjectLegalHold", "s3:PutObjectRetention", "s3:PutObjectTagging", "s3:PutObjectVersionTagging" ], "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "amzns3demobucket77F80CC0", "Arn" ] }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "amzns3demobucket77F80CC0", "Arn" ] }, "/*" ] ] } ] } ], "Version": "2012-10-17" }, "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", "Roles": [ { "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" } ] }, "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource" } }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "S3Key": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip" }, "Role": { "Fn::GetAtt": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", "Arn" ] }, "Environment": { "Variables": { "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" } }, "Handler": "index.handler", "Layers": [ { "Ref": "BucketObjectsAwsCliLayer8C081206" } ], "Runtime": "python3.9", "Timeout": 900 }, "DependsOn": [ "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" ], "Metadata": { "aws:cdk:path": "CdkScratchStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource", "aws:asset:path": "asset.9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd", "aws:asset:is-bundled": false, "aws:asset:property": "Code" } }

4 行に対し 241 行とは、かなりの差です。しかし、これは、レイヤー 3 を活用してスタックをカスタマイズする場合の可能性を示す一例にすぎません。