第 2 层构造 - AWS 规范指引

第 2 层构造

AWS CDK 开源存储库主要使用 TypeScript 编程语言编写,由许多包和模块组成。名为 aws-cdk-lib 的主软件包库大致分为每项 AWS 服务一个软件包,尽管情况并非总是如此。如前文所述,L1 构造是在构建过程中自动生成的,那么当您查看存储库内部时,您看到的全部代码是什么? 这些是 L2 构造,它们是 L1 构造的抽象。

这些软件包还包含 TypeScript 类型、枚举和接口的集合,以及添加更多功能的帮助程序类,但这些项目都提供 L2 构造。所有 L2 构造在实例化时都会在其构造函数中调用相应的 L1 构造,并且可以从第 2 层访问所创建的 L1 构造,如下所示:

const role = new Bucket(this, "amzn-s3-demo-bucket", {/*...BucketProps*/}); const cfnBucket = role.node.defaultChild;

L2 构造采用默认属性、便利方法及其他语法糖,并将其应用于 L1 构造。这消除了直接在 CloudFormation 中预调配资源所必需的大部分重复和冗长。

所有 L2 构造都是在后台构建相应的 L1 结构。但是,L2 构造实际上并没有扩展 L1 构造。L1 和 L2 构造都继承一个名为 Construct 的特殊类别。在 AWS CDK 的第 1 版中,Construct 类别已内置于开发工具包中,但在第 2 版中,它是一个单独的独立软件包。这样,其他软件包 [例如 Terraform 的云开发工具包(CDKTF)] 就可以将其作为依赖项包含在内。继承 Construct 类别的任何类别都是 L1、L2 或 L3 构造。L2 构造直接扩展此类别,而 L1 构造扩展名为 CfnResource 的类别,如下表所示。

L1 继承树

L2 继承树

L1 构造

→ 类别 CfnResource

→→ 抽象类别 CfnRefElement

→→→ 抽象类别 CfnElement

→→→→ 类别 Construct

L2 构造

→ 类别 Construct

如果 L1 和 L2 构造都继承 Construct 类别,为什么 L2 构造不直接扩展 L1? 嗯,这是因为 Construct 类别和 L1 层之间的类别将 L1 构造锁定为 CloudFormation 资源的镜像。它们包含抽象方法(下游类别必须包含的方法),例如 _toCloudFormation,这会强制构造直接输出 CloudFormation 语法。L2 构造会跳过这些类别,并直接扩展 Construct 类别。这使它们能够通过在构造函数中单独构建 L1 构造,来灵活地抽象 L1 构造所需的大部分代码。

上一节对 CloudFormation 模板中的 S3 存储桶和渲染为 L1 构造的相同 S3 存储桶进行了并排比较。该比较表明,属性和语法几乎相同,与 CloudFormation 构造相比,L1 构造仅节省了三四行。现在,让我们比较同一 S3 存储桶的 L1 构造和 L2 构造:

用于 S3 存储桶的 L1 构造

用于 S3 存储桶的 L2 构造

new CfnBucket(this, "amzns3demobucket", { bucketName: "amzn-s3-demo-bucket", bucketEncryption: { serverSideEncryptionConfiguration: [ { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } } ] }, metricsConfigurations: [ { id: "myConfig" } ], ownershipControls: { rules: [ { objectOwnership: "BucketOwnerPreferred" } ] }, publicAccessBlockConfiguration: { blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true }, versioningConfiguration: { status: "Enabled" } });
new Bucket(this, "amzns3demobucket", { bucketName: "amzn-s3-demo-bucket", encryption: BucketEncryption.S3_MANAGED, metrics: [ { id: "myConfig" }, ], objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, versioned: true });

如您所见,L2 构造的大小不到 L1 构造的一半。L2 构造使用多种技术来完成此整合。其中一些技术适用于单个 L2 构造,但其他技术可以在多个构造中重复使用,因此它们被分成自己的类别以实现可重用性。L2 构造通过多种方式整合 CloudFormation 语法,如以下各部分所述。

默认属性

整合预调配资源代码的最简单方法是将最常用的属性设置转换为默认值。AWS CDK 可以使用强大的编程语言,而 CloudFormation 则不能,因此这些默认值本质上通常是有条件的。有时,可以从 AWS CDK 代码中删除几行 CloudFormation 配置,因为这些设置可以从传递给构造的其他属性的值中推断出来。

结构、类型和接口

虽然 AWS CDK 提供多种编程语言版本,但它是使用 TypeScript 原生编写的,因此该语言的类型系统用于定义构成 L2 构造的类型。深入研究该类型系统超出了本指南的范围;有关详细信息,请参阅 TypeScript 文档。总而言之,TypeScript type 描述了特定变量所包含的数据类型。这可能是基本数据(例如 string),也可能是更复杂的数据(例如 object)。TypeScript interface 是表达 TypeScript 对象类型的另一种方式,而 struct 是接口的另一个名称。

TypeScript 不使用结构这个词,但是如果您查看 AWS CDK API 参考,您会发现一个结构实际上只是代码内的另一个 TypeScript 接口。API 参考也将某些接口称为接口。如果结构和接口是同一回事,为什么 AWS CDK 文档要区分它们呢?

AWS CDK 中所谓的结构是指代表 L2 构造所使用的任何对象的接口。其中包括在实例化期间传递给 L2 构造的属性参数的对象类型,例如 S3 存储桶构造的 BucketProps 和 DynamoDB 表构造的TableProps,以及 AWS CDK 中使用的其他 TypeScript 接口。简而言之,如果是 AWS CDK 中的 TypeScript 接口,并且其名称没有以字母 I 为前缀,则 AWS CDK 会将其称为结构

相反,AWS CDK 使用术语接口来表示普通对象需要具备的基本元素,才能视为特定构造或帮助程序类的正确表示。也就是说,接口描述了 L2 构造的公共属性必须是什么。所有 AWS CDK 接口名称都是以字母 I 为前缀的现有构造或帮助程序类的名称。所有 L2 构造都扩展了 Construct 类别,但它们也实现了相应的接口。因此,L2 构造 Bucket 实现了 IBucket 接口。

静态方法

L2 构造的每个实例也是其相应接口的实例,但反过来不成立。在浏览结构以查看需要哪些数据类型时,这一点很重要。如果结构具有名为 bucket 的属性,该属性需要数据类型 IBucket,则可以传递一个包含 IBucket 接口中列出的属性的对象,或传递一个 L2 Bucket 的实例。任何一种方法都可以。但是,如果该 bucket 属性调用 L2 Bucket,则只能在该字段中传递一个 Bucket 实例。

当您将预先存在的资源导入堆栈时,这种区别变得非常重要。您可以为堆栈原生的任何资源创建 L2 构造,但是如果您需要引用在堆栈之外创建的资源,则必须使用该 L2 构造的接口。这是因为,如果堆栈内尚不存在该资源,则创建 L2 构造会创建一个新资源。对现有资源的引用必须是符合该 L2 构造接口的普通对象。

为了方便实践,大多数 L2 构造都有一组与之关联的静态方法,这些方法返回该 L2 构造的接口。这些静态方法通常以单词 from 开头。传递给这些方法的前两个参数与标准 L2 构造所需的 scopeid 参数相同。然而,第三个参数不是 props,而是定义接口的一小部分属性(有时甚至只有一个属性)。因此,当您传递 L2 构造时,大多数情况下只需要接口的元素。这样,您也可以在可能的情况下使用导入的资源。

// Example of referencing an external S3 bucket const preExistingBucket = Bucket.fromBucketName(this, "external-bucket", "name-of-bucket-that-already-exists");

但是,您不应该过分依赖接口。只有在绝对必要时才应导入资源并直接使用接口,因为接口不提供使 L2 构造如此强大的许多属性,例如帮助程序方法。

帮助程序方法

L2 构造是一个编程类别,而不是一个简单的对象,因此它可以公开类别方法,允许您在实例化完成后操作资源配置。这方面的一个很好的示例是 AWS Identity and Access Management(IAM)L2 角色构造。以下片段显示了使用 L2 Role 构造创建相同 IAM 角色的两种方法。

不使用帮助程序方法:

const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com'), managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess") ], inlinePolicies: { lambdaPolicy: new PolicyDocument({ statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }) } });

使用帮助程序方法:

const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com') }); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")); role.attachInlinePolicy(new Policy(this, "lambda-policy", { policyName: "lambdaPolicy", statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }));

L2 构造能够使用实例方法在实例化后操作资源配置,这使得它比上一层具有更大的灵活性。L1 构造还会继承一些资源方法(例如 addPropertyOverride),但是直到第二层才获得专门为该资源及其属性设计的方法。

枚举

CloudFormation 语法通常要求您指定许多细节才能正确预调配资源。但是,大多数使用案例通常只能由少数几种配置覆盖。使用一系列枚举值来表示这些配置可以极大减少所需的代码量。

例如,在本节前面的 S3 存储桶 L2 代码示例中,您必须使用 CloudFormation 模板的 bucketEncryption 属性来提供所有详细信息,包括要使用的加密算法的名称。相反,AWS CDK 提供了 BucketEncryption 枚举,它采用了五种最常见的存储桶加密形式,并允许您使用单个变量名来表达每种形式。

枚举未涵盖的边缘情况该怎么办? L2 构造的目标之一是简化预调配第 1 层资源的任务,因此第 2 层可能不支持某些不太常用的边缘情况。为了支持这些边缘情况,AWS CDK 允许您使用 addPropertyOverride 方法直接操作底层 CloudFormation 资源属性。有关属性覆盖的更多信息,请参阅本指南的最佳实践部分以及 AWS CDK 文档中的抽象和逃生舱口部分。

帮助程序类

有时,枚举无法完成为给定使用案例配置资源所需的编程逻辑。在这些情况下,AWS CDK 通常会改为提供帮助程序类。枚举是一个提供一系列键值对的简单对象,而帮助程序类提供了 TypeScript 类别的全部功能。通过公开静态属性,帮助程序类仍然可以像枚举一样起作用,但是这些属性的值可以在帮助程序类构造函数或帮助程序方法中使用条件逻辑在内部设置。

因此,虽然 BucketEncryption 枚举可以减少在 S3 存储桶上设置加密算法所需的代码量,但同样的策略不适用于设置持续时间,因为有太多可能的值可供选择。为每个值创建枚举会麻烦得不值当。因此,将帮助程序类用于 S3 存储桶的默认 S3 对象锁定配置设置,如 ObjectLockRetention 类别所示。ObjectLockRetention 包含两种静态方法:一种用于合规性保留,另一种用于治理保留。这两种方法都将 Duration 帮助程序类的实例作为参数,来表示应配置锁的时间。

另一个示例是 AWS Lambda 帮助程序类运行时。乍一看,与此类别关联的静态属性似乎可以通过枚举来处理。但是,在后台,每个属性值都代表 Runtime 类别本身的一个实例,因此在类别的构造函数中执行的逻辑无法在枚举内实现。