第 3 层构造 - AWS 规范指引

第 3 层构造

如果 L1 构造将 CloudFormation 资源直译为编程代码,而 L2 构造用帮助程序方法和自定义逻辑替换了大部分冗长的 CloudFormation 语法,那么 L3 构造会做什么? 答案只受您的想象力的限制。您可以创建第 3 层以适应任何特定的使用案例。如果您的项目需要具有特定属性子集的资源,则可以创建可重复使用的 L3 构造来满足该需求。

L3 构造在 AWS CDK 内称为模式。模式是指任何可扩展 AWS CDK 中的 Construct 类别(或扩展可扩展 Construct 类别的类别)的对象,用于执行超出第 2 层的任何抽象逻辑。使用 AWS CDK CLI 运行 cdk init 启动新的 AWS CDK 项目时,必须从以下三种 AWS CDK 应用程序类型中选择:applibsample-app

AWS CDK 应用程序类型

appsample-app 都代表经典 AWS CDK 应用程序,您可以在其中构建 CloudFormation 堆栈并将其部署到 AWS 环境。选择 lib 时,您就选择了构建一个全新的 L3 构造。appsample-app 允许您选择 AWS CDK 支持的任何语言,但您只能选择带有 lib 的 TypeScript。这是因为 AWS CDK 本身是用 TypeScript 编写的,并且使用名为 JSii 的开源系统将原始代码翻译成其他受支持的语言。选择 lib 启动项目时,您就选择了构建 AWS CDK 的扩展。

任何可扩展 Construct 类别的类别都可以是 L3 构造,但第 3 层最常见的使用案例是资源交互、资源扩展和自定义资源。大多数 L3 构造使用这三种情况中的一种或多种来扩展 AWS CDK 功能。

资源交互

一个解决方案通常使用多个协同工作的 AWS 服务。例如,Amazon CloudFront 分发通常使用 S3 存储桶作为来源,并使用 AWS WAF 来防范常见漏洞攻击。AWS AppSync 和 Amazon API Gateway 通常使用 Amazon DynamoDB 表作为其 API 的数据来源。AWS CodePipeline 中的管线通常使用 Amazon S3 作为来源,并使用 AWS CodeBuild 作为构建阶段。在这些情况下,创建一个 L3 构造来处理两个或多个互连的 L2 构造的预调配通常很有用。

以下是一个 L3 构造示例,该构造预调配 CloudFront 分发及其 S3 源、部署在其前面的 AWS WAF、Amazon Route 53 记录以及 AWS Certificate Manager(ACM)证书,以添加带有传输加密的自定义端点 – 所有这些都在一个可重复使用的构造中:

// 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 构造。要使用 L3 构造 CloudFrontWebsite 创建新网站,请使用以下代码:

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 的软件包。此软件包包含多个 L3 构造,它们将 Amazon ECS 与应用程序负载均衡器、网络负载均衡器和目标组相结合,同时提供针对 Amazon Elastic Compute Cloud(Amazon EC2)和 AWS Fargate 预设的不同版本。由于许多无服务器应用程序仅将 Amazon ECS 与 Fargate 配合使用,因此这些 L3 构造提供了便利,可以为开发者节省时间,为客户节省金钱。

资源扩展

某些使用案例要求资源具有特定的默认设置,而这些设置不是 L2 构造的原生设置。在堆栈级别,这可以通过使用方面来处理,但为 L2 构造赋予新默认值的另一种便捷方法是扩展第 2 层。由于构造是继承了 Construct 类别的任何类别,而 L2 构造又扩展了该类别,因此您也可以通过直接扩展 L2 构造来创建 L3 构造。

这对于支持客户单一需求的自定义业务逻辑特别有用。假设一家公司有一个存储库,其中存储了其所有 AWS Lambda 函数代码,这些代码位于名为 src/lambda 的单个目录中,并且大多数 Lambda 函数每次都重复使用相同的运行时和处理程序名称。您可以创建一个新的 L3 构造,而不必在每次配置新的 Lambda 函数时都配置代码路径:

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 函数,且 L3 构造已设置,因此您仍然可以在需要时覆盖默认属性。

当您只想为现有 L2 构造添加新的默认值时,直接扩展 L2 构造效果最佳。如果您还需要其他自定义逻辑,最好扩展 Construct 类别。其原因源于在构造函数内调用的 super 方法。在可扩展其他类别的类别中,super 方法用于调用父类别的构造函数,并且这必须是构造函数内发生的第一件事。这意味着只有在创建了原始 L2 构造之后,才能对传递的参数或其他自定义逻辑进行任何操作。如果您需要在实例化 L2 构造之前执行任何自定义逻辑,则最好遵循之前在资源交互部分中概述的模式。

自定义资源

自定义资源是 CloudFormation 中的一项强大功能,可让您通过堆栈部署期间激活的 Lambda 函数运行自定义逻辑。当您在部署期间需要任何不受 CloudFormation 直接支持的流程时,都可以使用自定义资源来实现。AWS CDK 提供的类别还允许您以编程方式创建自定义资源。通过在 L3 构造函数内使用自定义资源,您几乎可以用任何东西制作一个构造。

使用 Amazon CloudFront 的优势之一是其强大的全球缓存功能。如果您想手动重置该缓存,以便您的网站立即反映对您的来源所做的新更改,则可以使用 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 构造来创建执行自定义逻辑的自定义资源。当您只需要进行一次 AWS SDK 调用时,AwsCustomResource 非常方便,因为它允许您无需编写任何 Lambda 代码即可执行此操作。如果您有更复杂的要求并想要实现自己的逻辑,则可以直接使用基本的 CustomResource 类别。

AWS CDK 使用自定义资源 L3 构造的另一个很好的示例是 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 层自定义堆栈所能实现的功能的一个示例。