diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4817f86101efa..21469506518de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,6 +142,9 @@ Sometimes, the GitHub issue is sufficient for such discussions, and can be suffi clarity on what you plan to do. Sometimes, a design document would work better, so people can provide iterative feedback. +Before starting on a design, read through the [design guidelines](DESIGN_GUIDELINES.md) for general +patterns and tips. + In such cases, use the GitHub issue description to collect **requirements** and **use cases** for your feature. diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md new file mode 100644 index 0000000000000..822965e702bf4 --- /dev/null +++ b/DESIGN_GUIDELINES.md @@ -0,0 +1,1314 @@ +# AWS Construct Library Design Guidelines + +The AWS Construct Library is a rich class library of CDK constructs which +represent all resources offered by the AWS Cloud and higher-level constructs for +achieving common tasks. + +The purpose of this document is to provide guidelines for designing the APIs in +the AWS Construct Library in order to ensure a consistent and integrated +experience across the entire AWS surface area. + +As much as possible, the guidelines in this document are enforced using the +**awslint** tool which reflects on the APIs and verifies that the APIs adhere to +the guidelines. When a guideline is backed by a linter rule, the rule name will +be referenced like this: _[awslint:resource-class-is-construct]_. + +For the purpose of this document we will use "Foo" to denote the official name +of the resource as defined in the AWS CloudFormation resource specification +(i.e. "Bucket", "Queue", "Topic", etc). This notation allows deriving names from +the official name. For example, `FooProps` would be `BucketProps`, `TopicProps`, +etc, `IFoo` would be `IBucket`, `ITopic` and so forth. + +The guidelines in this document use TypeScript (and npm package names) since +this is the source programming language used to author the library, which is +later packaged and published to all programming languages through +[jsii](https://github.com/awslabs/jsii). + +When designing APIs for the AWS Construct Library (and these guidelines), we +follow the tenets of the AWS CDK: + +* **Meet developers where they are**: our APIs are based on the mental model of +the user, and not the mental model of the service APIs, which are normally +designed against the constraints of the backend system and the fact that these +APIs are used through network requests. It's okay to enable multiple ways to +achieve the same thing, in order to make it more natural for users who come from +different mental models. +* **Full coverage**: the AWS Construct Library exposes the full surface area of +AWS. It is not opinionated about which parts of the service API should be +used. However, it offers sensible defaults to allow users to get started quickly +with best practices, but allows them to fully customize this behavior. We use a +layered architecture so that users can choose the level of abstraction that fits +their needs. +* **Designed for the CDK**: the AWS Construct Library is primarily optimized for +AWS customers who use the CDK idiomatically and natively. As much as possible, +the APIs are non-leaky and do not require that users understand how AWS +CloudFormation works. If users wish to “escape” from the abstraction, the APIs +offer explicit ways to do that, so that users won't be blocked by missing +capabilities or issues. +* **Open**: the AWS Construct Library is an open and extensible framework. It is +also open source. It heavily relies on interfaces to allow developers to extend +its behavior and provide their own custom implementations. Anyone should be able +to publish constructs that look & feel exactly like any construct in the AWS +Construct Library. +* **Designed for jsii**: the AWS Construct Library is built with jsii. This +allows the library to be used from all supported programming languages. jsii +poses restrictions on language features that cannot be idiomatically represented +in target languages. + +## API Design + +### Modules + +AWS resources are organized into modules based on their AWS service. For +example, the "Bucket" resource, which is offered by the Amazon S3 service will +be available under the **@aws-cdk/aws-s3** module. We will use the “aws-” prefix +for all AWS services, regardless of whether their marketing name uses an +“Amazon” prefix (e.g. “Amazon S3”). Non-AWS services supported by AWS +CloudFormation (like the Alexa::ASK namespace) will be **@aws-cdk/alexa-ask**. + +The name of the module is based on the AWS namespace of this service, which is +consistent with the AWS SDKs and AWS CloudFormation _[awslint:module-name]_. + +All major versions of an AWS namespace will be mastered in the AWS Construct +Library under the root namespace. For example resources of the **ApiGatewayV2** +namespace will be available under the **@aws-cdk/aws-apigateway** module (and +not under “v2) _[awslint:module-v2]_. + +In some cases, it makes sense to introduce secondary modules for a certain +service (e.g. aws-s3-notifications, aws-lambda-event-sources, etc). The name of +the secondary module will be +**@aws-cdk/aws-xxx-\**_[awslint:module-secondary]_. + +Documentation for how to use secondary modules should be in the main module. The +README file should refer users to the central module +_[awslint:module-secondary-readme-redirect]_. + +### Construct Class + +Constructs are the basic building block of CDK applications. They represent +abstract cloud components of any complexity. Constructs in the AWS Construct +Library normally represent physical AWS resources (such as an SQS queue) but +they can also represent abstract composition of other constructs (such as +**LoadBalancedFargateService**). + +Most of the guidelines in this document apply to all constructs in the AWS +Construct Library, regardless of whether they represent concrete AWS resources +or abstractions. However, you will notice that some sections explicitly call out +guidelines that apply only to AWS resources (and in many cases +enforced/implemented by the **Resource** base class). + +AWS services are modeled around the concept of *resources*. Service normally +expose through their APIs one or more resources, which can be provisioned +through the APIs control plane or through AWS CloudFormation. + +Every resource available in the AWS platform will have a corresponding resource +construct class to represents it. For example, the **s3.Bucket** construct +represents Amazon S3 Buckets, the **dynamodb.Table** construct represents an +Amazon DynamoDB table. The name of resource constructs must be identical to the +name of the resource in the AWS API, which should be consistent with the +resource name in the AWS CloudFormation spec _[awslint:resource-class]_. + +> The _awslint:resource-class_ rule is a **warning** (instead of an error). This + allows us to gradually expand the coverage of the library. + +Classes which represent AWS resources are constructs and they must extend the +**cdk.Resource** class directly or indirectly +_[awslint:resource-class-extends-resource]_. + +> Resource constructs are normally implemented using low-level CloudFormation + (“CFN”) constructs, which are automatically generated from the AWS + CloudFormation resource specification. + +The signature (both argument names and types) of all construct initializers +(constructors) must be as follows _[awslint:construct-ctor]_: + +```ts +constructor(scope: cdk.Construct, id: string, props: FooProps) +``` + +The **props** argument must be of type FooProps +[_awslint:construct-ctor-props-type_]. + +If all props are optional, the `props` argument must also be optional +_[awslint:construct-ctor-props-optional]_. + +```ts +constructor(scope: cdk.Construct, id: string, props: FooProps = { }) +``` + +> Using `={}` as a default value is preferable to using an optional qualifier + (`?`) since it will ensure that props will never be `undefined` and therefore + easier to parse in the method body. + +As a rule of thumb, most constructs should directly extend the **Construct** or +**Resource** instead of another construct. Prefer representing polymorphic +behavior through interfaces and not through inheritance. + +Construct classes should extend only one of the following classes +[_awslint:construct-inheritence_]: + +* The **Resource** class (if it represents an AWS resource) The **Construct** +* class (if it represents an abstract component) The **XxxBase** class (which, +* in turn extends **Resource**) + +All constructs must define a static type check method called **isFoo** with the +following implementation [_awslint:static-type-check_]: + +```ts +const IS_FOO = Symbol.for('@aws-cdk/aws-foo.Foo'); + +export class Foo { + public static isFoo(x: any): x is Foo { + return IS_FOO in x; + } + + constructor(scope: Construct, id: string, props: FooProps) { + super(scope, id); + + Object.defineProperty(this, IS_FOO, { value: true }); + } +} +``` + +### Construct Interface + +One of the important tenets of the AWS Construct Library is to use strong-types +when referencing resources across the library. This is in contrast to how AWS +backend APIs (and, consequently, AWS CloudFormation) model reference via one of +their *runtime attributes* (such as the resource's ARN). Since the AWS CDK is a +client-side abstraction, we can offer developers a much richer experience by +using *object references* instead of *attribute references*. + +Using object references instead of attribute references allows consumers of +these objects to have a richer interaction with the consumed object. They can +reference runtime attributes such as the resource's ARN, but also utilize logic +encapsulated by the target object. + +Here's an example: when a user defines an S3 bucket, they can pass in a KMS key +that will be used for bucket encryption: + +```ts +new s3.Bucket(this, 'MyBucket', { encryptionKey: key }); +``` + +The **Bucket** class can now use **key.keyArn** to obtain the ARN for the key, +but it can also call the **key.grantEncrypt** method as a result of a call to +**bucket.grantWrite**. Separation of concerns is a basic OO design principle: +the fact that the Bucket class needs the ARN or that it needs to request +encryption permissions are not the user's concern, and the API of the Bucket +class should not “leak” these implementation details. In the future, the Bucket +class can decide to interact differently with the **key** and this won't require +expanding it's surface area. It also allows the **Key** class to change it's +behavior (i.e. add an IAM action to enable encryption of certain types of keys) +without affecting the API of the consumer. + +#### owned vs. unowned constructs + +Using object references instead of attribute references provides a richer API, +but also introduces an inherent challenge: how do we reference constructs that +are not defined inside the same app (“**owned**” by the app). These could be +resources that were created by some other AWS CDK app, via the AWS console, +etc. We call these “**unowned**”**constructs.** + +In order to model this concept of owned and unowned constructs, all constructs +in the AWS Construct Library should always have a corresponding **construct +interface**. This interface includes the API of the construct +_[awslint:construct-interface]_. + +Therefore, when constructs are referenced ***anywhere*** in the API (e.g. in +properties or methods of other resources or higher-level constructs), the +resource interface (`IFoo`) should be used over concrete resource classes +(`Foo`). This will allow users to supply either internal or external resources +_[awslint:ref-via-interface]_. + +Construct interfaces must extend the **IConstruct** interface in order to allow +consumers to take advantage of common resource capabilities such as unique IDs, +paths, scopes, etc _[awslint:construct-interface-extends-iconstruct]_. + +Constructs that directly represent AWS resources (most of the constructs in the +AWS Construct Library) should extend **IResource** (which, transitively, extends +**IConstruct**) _[awslint:resource-interface-extends-resource]_. + +#### Abstract Base + +It is recommended to implement an abstract base class **FooBase** for each +resource **Foo****. **The base class would normally implement the entire +construct interface and leave attributes as abstract properties. + +```ts +abstract class FooBase extends Resource implements IFoo { + public abstract fooName: string; + public abstract fooArn: string; + + // .. concrete implementation of IFoo (grants, metrics, factories), + // should only rely on "fooName" and "fooArn" theoretically +} +``` + +The construct base class can then be used to implement the various +deserialization and import methods by defining an ad-hoc local class which +simply provides an implementation for the attributes (see “Serialization” below +for an example). + +The abstract base class should be internal and not exported in the module's API +_[awslint:construct-base-is-private]_. This is only a recommended (linter +warning). + +### Props + +Constructs are defined by creating a new instance and passing it a set of +**props** to the constructor. Throughout this document, we will refer to these +as “props” (to distinguish them from JavaScript object properties). + + The props argument for the **Foo** construct should be a struct (interface with + only readonly properties) named **FooProps** _[awslint:props-struct-name]_. + +> Even if a construct props simply extends from some other Props struct and does + not add any new properties, you should still define it, so it will be + extensible in the future without breaking users in languages like Java where + the props struct name is explicitly named. + +Props are the most important aspect of designing a construct. Props are the +entry point of the construct. They should reflect the entire surface area of the +service through semantics that are intuitive to how developers perceive the +service and it's capabilities. + +When designing the props of an AWS resource, consult the AWS Console experience +for creating this resource. Service teams spend a lot of energy thinking about +this experience. This is a great resource for learning about the mental model of +the user. Aligning with the console also makes it easier for users to jump back +and forth between the AWS Console (the web frontend of AWS) and the CDK (the +“programmatic frontend” of AWS). + +AWS constructs should *not* have a “props” property +[_awslint:props-no-property_]. + +Construct props should expose the *full set* of capabilities of the AWS service +through a declarative interface [_awslint:props-coverage_]. + +This section describes guidelines for construct props. + +#### types + +Use **strong types** (and specifically, construct interfaces) instead of +physical attributes when referencing other resources. For example, instead of +**keyArn**, use **kms.IKey** [_awslint:props-no-arn-refs_]. + +Do not “leak” the details or types of the CFN layer when defining your construct +API. In almost all cases, a richer object-oriented API can be exposed to +encapsulate the low-level surface [_awslint:props-no-cfn-types_]. + +Do not use the **Token** type. It provides zero type safety, and is a functional +interface that may not translate cleanly in other JSII runtimes: ergo it should +be avoided wherever possible [_awslint:props-no-tokens_]. + +**deCDK** allows users to synthesize CDK stacks through a CloudFormation-like + template, similar to SAM. CDK constructs are represented in deCDK templates + like CloudFormation resources. Technically, this means that when a construct + is defined, users supply an ID, type and a set of properties.****In order to + allow users to instantiate all AWS Construct Library constructs through the + deCDK syntax, we pose restrictions on prop types _[awslint:props-decdk]_: + +* Primitives (string, number, boolean, date) Collections (list, map) Structs +* Enums Enum-like classes Union-like classes References to other constructs +* (through their construct interface) Integration interfaces (interfaces that +* have a “**bind**” method) + +#### Defaults + +A prop should be *required* only if there is no possible sensible default value +that can be provided *or calculated*. + +Sensible defaults have a tremendous impact on the developer experience. They +offer a quick way to get started with minimal cognitive, but do not limit users +from harnessing the full power of the resource, and customizing its behavior. + +> A good way to determine what's the right sensible default is to refer to the + AWS Console resource creation experience. In many cases, there will be + alignment. + +The **@default** documentation tag must be included on all optional properties +of interfaces. Since there are cases where the default behavior is not a +specific value but rather depends on circumstances/context, the default +documentation tag must always begin with a **“**-"**** and then include a +description of the default behavior _[awslint:props-default-doc]_. + +For example: + +```ts +/** + * External KMS key to use for bucket encryption. + * + * @default - if encryption is set to "Kms" and this property is undefined, a + * new KMS key will be created and associated with this bucket. + */ +encryptionKey?: kms.IEncryptionKey; +``` + +#### Flat + +Do not introduce artificial nesting for props. It hinders discoverability and +makes it cumbersome to use in some languages (like Java) [_awslint:props-flat_]. + +You can use a shared prefix for related properties to make them appear next to +each other in documentation and code completion: + +For example, instead of: + +```ts +new Bucket(this, 'MyBucket', { + bucketWebSiteConfiguration: { + errorDocument: '404.html', + indexDocument: 'index.html', + } +}); +``` + +Prefer: + +```ts +new Bucket(this, 'MyBucket', { + websiteErrorDocument: '404.html', + websiteIndexDocument: 'index.html' +}); +``` + +#### Concise + +Property names should be short and concise as possible and take into +consideration the ample context in which the property is used. Being concise +doesn't mean inventing new semantics. It just means that you can remove +redundant context from the property names. + +Being concise doesn't mean you should invent new service semantics (see next +item). It just means that you can remove redundant context from the property +names. For example, there is no need to repeat the resource type, the property +type or indicate that this is a "configuration". + +For example prefer “readCapacity” versus “readCapacityUnits”. + +#### Naming + +We prefer the terminology used by the official AWS service documentation over +new terminology, even if you think it's not ideal. It helps users diagnose +issues and map the mental model of the construct to the service APIs, +documentation and examples. For example don't be tempted to change SQS's +**dataKeyReusePeriod** with **keyRotation** because it will be hard for people +to diagnose problems. They won't be able to just search for “sqs dataKeyReuse” +and find topics on it. + +> We can relax this guidelines when this is about generic terms (like + `httpStatus` instead of `statusCode`). The important semantics to preserve are + for *service features*: I wouldn't want to rename "lambda layers" to "lambda + dependencies" just because it makes more sense because then users won't be + able to bind these terms to the service documentation. + +Indicate units of measurement in property names that don't use a strong +type. Use “milli”, “sec”, “min”, “hr”, “Bytes”, “KiB”, “MiB”, “GiB” (KiB=1024 +bytes, while KB=1000 bytes). + +#### Property Documentation + +Every prop must have detailed documentation. It is recommended to **copy** from +the official AWS documentation in English if possible so that language and style +will match the service. + +#### Enums + +When relevant, use enums to represent multiple choices. + +```ts +export enum MyEnum { + OPTION1 = 'op21', + OPTION2 = 'opt2', +} +``` + +A common pattern in AWS is to allow users to select from a predefined set of +common options, but also allow the user to provide their own customized values. + +A pattern for an "Enum-like Class" should be used in such cases: + +```ts +export interface MyProps { + option: MyOption; +} + +export class MyOption { + public static COMMON_OPTION_1 = new MyOption('common.option-1'); + public static COMMON_OPTION_2 = new MyOption('common.option-2'); + + public MyOption(public readonly customValue: string) { } +} +``` + +Then usage would be: + +```ts +new BoomBoom(this, 'Boom', { + option: MyOption.COMMON_OPTION_1 +}); +``` + +Suggestion for alternative syntax for custom options? Motivation: if we make +everything go through static factories, it will look more regular (I'm fine not +pursuing this, just popped into my head): + +```ts +export class MyOption { + public static COMMON_OPTION_1 = new MyOption('common.option-1'); + public static COMMON_OPTION_2 = new MyOption('common.option-2'); + + public static custom(value: string) { + return new MyOption(value); + } + + // 'protected' iso. 'private' so that someone that really wants to can still + // do subclassing. But maybe might as well be private. + protected MyOption(public readonly value: string) { } +} + +// Usage +new BoomBoom(this, 'Boom', { + option: MyOption.COMMON_OPTION_1 +}); + +new BoomBoom(this, 'Boom', { + option: MyOption.custom('my-value') +}); +``` + +#### Unions + +Do not use TypeScript union types in construct APIs (`string | number`) since +many of the target languages supported by the CDK cannot strongly-model such +types _[awslint:props-no-unions]_. + +Instead, use a class with static methods: + +```ts +new lambda.Function(this, 'MyFunction', { + code: lambda.Code.asset('/asset/path'), // or + code: lambda.Code.bucket(myBucket, 'bundle.zip'), // or + code: lambda.Code.inline('code') + // etc +} +``` + +### Attributes + +Every AWS resource has a set of "physical" runtime attributes such as ARN, +physical names, URLs, etc. These attributes are commonly late-bound, which means +they can only be resolved during deployment, when AWS CloudFormation actually +provisions the resource. + +AWS constructs must expose all resource attributes defined in the underlying +CloudFormation resource as readonly properties of the class +_[awslint:resource-attribute]_. + +All properties that represent resource attributes must include the JSDoc tag +**@attribute** _[awslint:attribute-tag]_. + +All attribute names must begin with the type name as a prefix +(e.g. ***bucket*Arn** instead of just **arn**) _[awslint:attribute-name]_. This +implies that if a property begins with the type name, it must have an +**@attribute** tag. + +All resource attributes must be represented as readonly properties of the +resource interface _[awslint:attribute-readonly]_. + +Resource attributes should use a type that corresponds to the resolved AWS +CloudFormation type (e.g. **string**, **string[]**) _[awslint:attribute-type]_. + +> Resource attributes almost always represent string values (URL, ARN, + name). Sometimes they might also represent a list of strings. Since attribute + values can either be late-bound ("a promise to a string") or concrete ("a + string"), the AWS CDK has a mechanism called "tokens" which allows codifying + late-bound values into strings or string arrays. This approach was chosen in + order to dramatically simplify the type-system and ergonomics of CDK code. As + long as users treat these attributes as opaque values (e.g. not try to parse + them or manipulate them), they can be used interchangeably. + +If needed, you can query whether an object includes unresolved tokens by using +the **Token.unresolved(x)** method. + +To ensure users are aware that the value returned by attribute properties should +be treated as an opaque token, the JSDoc “@returns” annotation should begin with +“**@returns a $token representing the xxxxx**” +[_awslint:attribute-doc-returns-token_]. + +### Configuration + +When an app defines a construct or resource, it specifies its provisioning +configuration upon initialization. For example, when an SQS queue is defined, +it's visibility timeout can be configured. + +Naturally, when constructs are imported (unowned), the importing app does not +have control over its configuration (e.g. you cannot change the visibility +timeout of an SQS queue that you didn't create). Therefore, construct interfaces +cannot include methods that require access/mutation of configuration. + +One of the problems with configuration mutation is that there could be a race +condition between two parts of the app, trying to set contradicting values. + +There are a number use cases where you'd want to provide APIs which expose or +mutate the construct's configuration. For example, +**lambda.Function.addEnvironment** is a useful method that adds an environment +variable to the function's runtime environment, and used occasionally to inject +dependencies. + +> Note that there are APIs that look like they mutate the construct, but in fact + they are **factories** (i.e. they define resources on the user's stack). Those + APIs _should_ be exposed on the construct interface and not on the construct + class. + +To help avoid the common mistake of exposing non-configuration APIs on the +construct class (versus the construct interface), we require that configuration +APIs (methods/properties) defined on the construct class will be annotated with +the **@config** jsdoc tag [_awslint:config-explicit_]. + +```ts +interface IFoo extends IConstruct { + bar(): void; +} + +class Foo extends Construct implements IFoo { + public bar() { } + + /** @mutating */ + public goo() { } + + public mutateMe() { } // ERROR! missing "@mutating" or missing on IFoo +} +``` + +#### Prefer Additions + +As a rule of thumb, “adding” items to configuration props of type unordered +array is normally considered safe as it will unlikely cause race conditions. If +the prop is a map (like in **addEnvironment**), write defensive code that will +throw if two values are assigned to the same key. + +#### Dropped Mutations + +Since all references across the library are done through a construct's +interface, methods that are only available on the concrete construct class will +not be accessible by code that uses the interface type. For example, code that +accepts a **lambda.IFunction** will not see the **addEnvironment** method. + +In most cases, this is desirable, as it ensures that only the code the owns the +construct (instantiated it), will be able to mutate its configuration. + +However, there are certain areas in the library, where, for the sake of +consistency and interoperability, we allow mutating methods to be exposed on the +interface. For example, **grant** methods are exposed on the construct interface +and not on the concrete class. In most cases, when you grant a permission on an +AWS resource, the *principal's* policy needs to be updated, which mutates the +consumer . However, there are certain cases where a *resource policy* must be +updated. However, if the resource is unowned, it doesn't make sense (or even +impossible) to update its policy (there is usually a 1:1 relationship between a +resource and a resource policy). In such a case, we decided that grant methods +will simply skip any changes to resource policies, but will issue attach a +**permission notice** to the app, which will be printed when the stack is +synthesized by the toolkit. + +### Factories + +In most AWS services, there's a one or more resource which can be referred to as +“primary resources” (normally one), while other resources exposed by the service +can be considered “secondary resources”. + +For example, the AWS Lambda service exposes the **Function** resource, which can +be considered the primary resource while **Layer**, **Permission**, **Alias** +are considered secondary. For API Gateway, the primary resource is **RestApi**, +and there are many secondary resources such as **Method**, **Resource**, +**Deployment**, **Authorizer**. + +Secondary resources are normally associated with the primary resource (i.e. a +reference to the primary resource must be supplied upon initialization). + +Users should be able to define secondary resources either by directly +instantiating their construct class (like any other construct), and passing in a +reference to the primary resource's construct interface *or* it is recommended +to implement convenience methods on the primary resource that will facilitate +defining secondary resources. This improves discoverability and ergonomics +_[awslint:factory-method]_. + +For example, **lambda.Function.addLayer** can be used to add a layer to the +function, **apigw.RestApi.addResource** can be used to add to an API. + +Methods for defining a secondary resource “Bar” associated with a primary +resource “Foo” should have the following signature: + +```ts +export interface IFoo { + addBar(...): Bar; +} +``` + +Notice that: + +* The method has an “add” prefix. It implies that users are adding something to +* their stack. The method is implemented on the construct interface (to allow +* adding secondary resources to unowned constructs). The method returns a “Bar” +* instance (owned). + +In order to reuse the set of props used to configure the secondary resource, +define a base interface for **FooProps** called **FooOptions** to allow +secondary resource factory methods to reuse props +_[awslint:factory-method-options]_: + +```ts +export interface LogStreamOptions { + logStreamName?: string; +} + +export interface LogStreamProps extends LogStreamOptions { + logGroup: ILogGroup; +} + +export interface ILogGroup { + addLogStream(id: string, options?: LogStreamOptions): LogStream; +} +``` + +### Imports + +Construct classes should expose a set of static factory methods with a +“**from**” prefix that will allow users to import *unowned* constructs into +their app. + +The signature of all “from” methods should adhere to the following rules +_[awslint:from-signature]_: + +* First argument must be **scope** of type **Construct** Second argument is a +* **string**. This string will be used to determine the ID of the new +* construct. If the import method uses some value that is promised to be unique +* within the stack scope (such as ARN, export name), this value can be reused as +* the construct ID. Returns an object that implements the construct interface +* (**IFoo**). + +#### “from” Methods + +Resource constructs should export static “from” methods for importing unowned +resources given one more of it's physical attributes such as ARN, name, etc. All +constructs should have at least one fromXxx method _[awslint:from-method]_: + +```ts +static fromFooArn(scope: Construct, id: string, bucketArn: string): IFoo; +static fromFooName(scope: Construct, id: string, bucketName: string): IFoo; +``` + +> Since AWS constructs usually export all resource attributes, the logic behind + the various “from\” methods would normally need to convert one + attribute to another. For example, given a name, it would need to render the + ARN of the resource. Therefore, if **from\** methods expect to be + able to parse their input, they must verify that the input (e.g. ARN, name) + doesn't have unresolved tokens (using **Token.unresolved**). Preferably, they + can use **Stack.parseArn** to achieve this purpose. + +If a resource has an ARN attribute it should implement at least a **fromFooArn** +import method [_awslint:from-arn_]. + +To implement **fromAttribute** methods, use the abstract base class construct as +follows: + + +```ts +class Foo { + static fromArn(scope: Construct, fooArn: string): IFoo { + class _Foo extends FooBase { + public get fooArn() { return fooArn; } + public get fooName() { return this.node.stack.parseArn(fooArn).resourceName; } + } + + return new _Foo(scope, fooArn); + } +} +``` + + +#### From-attributes + +If a resource has more than a single attribute (“ARN” and “name” are usually +considered a single attribute since it's usually possible to convert one to the +other), then the resource should provide a static **fromAttributes** method to +allow users to explicitly supply values to all resource attributes when they +import an external (unowned) resource [_awslint:from-attributes_]. + +```ts +static fromFooAttributes(scope: Construct, id: string, attrs: FooAttributes): IFoo; +``` + +### Roles + +If a CloudFormation resource has a **Role** property, it normally represents the +IAM role that will be used by the resource to perform operations on behalf of +the user. + +Constructs that represent such resources should conform to the following +guidelines. + +An optional prop called **role** of type **iam.IRole**should be exposed to allow +users to "bring their own role", and use either an owned or unowned role +_[awslint:role-config-prop]_. + +```ts +interface FooProps { + /** + * The role to associate with foo. + * @default - a role will be automatically created + */ + role?: iam.IRole; +} +``` + +The construct interface should expose a **role**property, and extends +**iam.IGrantable** _[awslint:role-property]_: + +```ts +interface IFoo extends iam.IGrantable { + /** + * The role associated with foo. If foo is imported, no role will be available. + */ + readonly role?: iam.IRole; +} +``` + +This property will be `undefined` if this is an unowned construct (e.g. was not +defined within the current app). + +An **addToRolePolicy** method must be exposed on the construct interface to +allow adding statements to the role's policy _[awslint:role-add-to-policy]_: + +```ts +interface IFoo { + addToRolePolicy(statement: iam.Statement): void; +} +``` + +If the construct is unowned this method should no-op and issue a **permissions +notice** (TODO) to the user indicating that they should ensure that the role of +this resource should have the specified permission. + +TODO: add a few sentences on grantable + +Implementing **IGrantable** brings an implementation burden of **grantPrincipal: +IPrincipal**. This property must be set to the **role** if available, or to a +new **iam.ImportedResourcePrincipal** if the resource is imported and the role +is not available. + +### Resource Policies + +Resource policies are IAM policies defined on the side of the resource (as +oppose to policies attached to the IAM principal). Different resources expose +different APIs for controlling their resource policy. For example, ECR +repositories have a **RepositoryPolicyText** prop, SQS queues offer a +**QueuePolicy** resource, etc. + +Constructs that represents resources with a resource policy should encapsulate +the details of how resource policies are created behind a uniform API as +described in this section. + +When a construct represents an AWS resource that supports a resource policy, it +should expose an optional prop that will allow initializing resource with a +specified policy _[awslint:resource-policy-prop]_: + +```ts +resourcePolicy?: iam.PolicyStatement[] +``` + +Furthermore, the construct *interface* should include a method that allows users +to add statements to the resource policy +_[awslint:resource-policy-add-to-policy]_: + +```ts +interface IFoo extends iam.IResourceWithPolicy { + addToResourcePolicy(statement: iam.PolicyStatement): void; +} +``` + +For some resources, such as ECR repositories, it is impossible (in +CloudFormation) to add a resource policy if the resource is unowned (the policy +is coupled with the resource creation). In such cases, the implementation of +`addToResourcePolicy` should add a **permission** **notice** to the construct +(using `node.addInfo`) indicating to the user that they must ensure that the +resource policy of that specified resource should include the specified +statement. + +### VPC + +Compute resources such as AWS Lambda functions, Amazon ECS clusters, AWS +CodeBuild projects normally allow users to specify the VPC configuration in +which they will be placed. The underlying CFN resources would normally have a +property or a set of properties that accept the set of subnets in which to place +the resources. + +In most cases, the preferred default behavior is to place the resource in all +private subnets available within the VPC. + +Props of such constructs should include the following properties +_[awslint:vpc-props]_: + +```ts +/** + * The VPC in which to run your CodeBuild project. + */ +vpc: ec2.IVpc; // usually this is required + +/** + * Which VPC subnets to use for your CodeBuild project. + * + * @default - uses all private subnets in your VPC + */ +vpcSubnetSelection?: ec2.SubnetSelection; +``` + +### Grants + +Grants are one of the most powerful concept in the AWS Construct Library. They +offer a higher level, intent-based, API for managing IAM permissions for AWS +resources. + +**Despite the fact that they may be mutating**, grants should be exposed on the + construct interface, and not on the concrete class + _[awslint:grants-on-interface]_. See discussion above about mutability for + reasoning. + +Grants are represented as a set of methods with the “**grant**” prefix. + +All constructs that represent AWS resources must have at least one grant method +called “**grant**” which can be used to grant a grantee (such as an IAM +principal) permission to perform a set of actions on the resource with the +following signature. This method is defined as an abstract method on the +**Resource** base class (and the **IResource** interface) +_[awslint:grants-grant-method]_: + +```ts +grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; +``` + +The **iam.Grant** class has a rich API for implementing grants which implements +the desired behavior. + +Furthermore, resources should also include a set of grant methods for common use +cases. For example, **dynamodb.Table.grantPutItem**, +**s3.Bucket.grantReadWrite**, etc. In such cases, the signature of the grant +method should adhere to the following rules _[awslint:grant-signature]_: + +1. Name should have a “grant” prefix 2. Returns an **iam.Grant** object 3. First +argument must be **grantee: iam.IGrantable** + +```ts +grantXxx(grantee: iam.IGrantable): iam.Grant; +``` + +It makes sense for some AWS resources to also expose grant methods on all +resources in the account. To support such use cases, expose a set of static +grant methods on the construct class. For example, +**dynamodb.Table.grantAllListStreams**. The signature of static grants should be +similar _[awslint:grants-static-all]_. + +```ts +export class Table { + public static grantAll(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + public static grantAllListStreams(grantee: iam.IGrantable): iam.Grant; +} +``` + +### Metrics + +Almost all AWS resources emit CloudWatch metrics, which can be used with alarms +and dashboards. + +AWS construct interfaces should include a set of “metric” methods which +represent the CloudWatch metrics emitted from this resource +_[awslint:metrics-on-interface]_. + +At a minimum (and enforced by IResource), all resources should have a single +method called **metric**, which returns a **cloudwatch.Metric** object +associated with this instance (usually this method will simply set the right +metrics namespace and dimensions [_awslint:metrics-generic-method_]: + +```ts +metric(metricName: string, options?: cloudwatch.MetricOptions): cloudwatch.Metric; +``` + +> **Exclusion**: If a resource does not emit CloudWatch metrics, this rule may + be excluded + +Additional metric methods should be exposed with the official metric name as a +suffix and adhere to the following rules _[awslint:metrics-method-signature]:_ + +* Name should be “metricXxx” where “Xxx” is the official metric name Accepts a +* single “options” argument of type **MetricOptions** Returns a **Metric** +* object. + +```ts +interface IFunction { + metricDuration(options?: cloudwatch.MetricOptions): cloudwatch.Metric; + metricInvocations(options?: cloudwatch.MetricOptions): cloudwatch.Metric; + metricThrottles(options?: cloudwatch.MetricOptions): cloudwatch.Metric; +} +``` + +It is sometimes desirable to use a metric that applies to all resources of a +certain type within the account. To facilitate this, resources should expose a +static method called **metricAll** _[awslint:metrics-static-all]_. Additional +**metricAll** static methods can also be exposed +_[awslint:metrics-all-methods]_. + + +```ts +class Function extends Resource implements IFunction { + public static metricAll(metricName: string, options?: cloudwatch.MetricOptions): cloudwatch.Metric; + public static metricAllErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} +``` + + +### Events + +Many AWS resource emit events to the CloudWatch event bus. Such resources should +have a set of “onXxx” methods available on their construct interface +_[awslint:events-in-interface]_. + +All “on” methods should have the following signature +[_awslint:events-method-signature_]: + +```ts +onXxx(id: string, target: events.IEventRuleTarget, options?: XxxOptions): cloudwatch.EventRule; +``` + +When a resource emits CloudWatch events, it should at least have a single +generic **onEvent** method to allow users to specify the event name +[_awslint:events-generic_]: + +```ts +onEvent(event: string, id: string, target: events.IEventRuleTarget): cloudwatch.EventRule +``` + +### Connections + +AWS resources that use EC2 security groups to manage network security should +implement the **connections API** interface by having the construct interface +extend **ec2.IConnectable** _[awslint:connectable-interface]_. + +### Integrations + +Many AWS services offer “integrations” to other services. For example, AWS +CodePipeline has actions that can trigger AWS Lambda functions, ECS tasks, +CodeBuild projects and more. AWS Lambda can be triggered by a variety of event +sources, AWS CloudWatch event rules can trigger many types of targets, SNS can +publish to SQS and Lambda, etc, etc. + +> See [aws-cdk#1743](https://github.com/awslabs/aws-cdk/issues/1743) for a + discussion on the various design options. + +AWS integrations normally have a single **central** service and a set of +**consumed** services. For example, AWS CodePipeline is the central service and +consumes multiple services that can be used as pipeline actions. AWS Lambda is +the central service and can be triggered by multiple event sources. + +Integrations are an abstract concept, not necessarily a specific mechanism. For +example, each AWS Lambda event source is implemented in a different way (SNS, +Bucket notifications, CloudWatch events, etc), but conceptually, *some*users +like to think about AWS Lambda as the “center”. It is also completely legitimate +to have multiple ways to connect two services on AWS. To trigger an AWS Lambda +function from an SNS topic, you could either use the integration or the SNS APIs +directly. + +Integrations should be modeled using an **interface** (i.e. **IEventSource**) +exported in the API of the central module (e.g. “aws-lambda”) and implemented by +classes in the integrations module (“aws-lambda-event-sources”) +[_awslint:integrations-interface_]. + +```ts +// aws-lambda +interface IEventSource { + bind(fn: IFunction): void; +} +``` + +A method “addXxx” should be defined on the construct interface and adhere to the +following rules _[awslint:integrations-add-method]:_ + +* Should accept any object that implements the integrations interface Should not +* return anything (void) Implementation should call “bind” on the integration +* object + +```ts +interface IFunction extends IResource { + public addEventSource(eventSource: IEventSource) { + eventSource.bind(this); + } +} +``` + +An optional array prop should allow declaratively applying integrations (sugar +to calling “addXxx”): + +```ts +interface FunctionProps { + eventSources?: IEventSource[]; +} +``` + +Lastly, to ease discoverability and maintain a sane dependency graphs, all +integrations for a certain service should be mastered in a single secondary +module named aws-*xxx*-*yyy* (where *xxx* is the service name and *yyy* is the +integration name). For example, **aws-s3-notifications**, +**aws-lambda-event-sources**, **aws-codepipeline-actions**. All implementations +of the integration interface should reside in a single module +_[awslint:integrations-in-single-module]_. + +```ts +// aws-lambda-event-sources +class DynamoEventSource implements IEventSource { + constructor(table: dynamodb.ITable, options?: DynamoEventSourceOptions) { ... } + + public bind(fn: IFunction) { + // ...do your magic + } +} +``` + +When integration classes define new constructs in **bind**, they should be aware +that they are adding into a scope they don't fully control. This means they +should find a way to ensure that construct IDs do not conflict. This is a +domain-specific problem. + +### State + +Persistent resources are AWS resource which hold persistent state, such as +databases, tables, buckets, etc. + +To make sure stateful resources can be easily identified, all resource +constructs must include the **@stateful** or **@stateless** JSDoc annotations at +the class level _[awslint:state-annotation]_. + +This annotation enables the following linting rules. + +```ts +/** + * @stateful + */ +export class Table { } +``` + +Persistent resources must have a **removalPolicy** prop, defaults to +**Orphan**_[awslint:state-removal-policy-prop]_: + +```ts +import { RemovalPolicy } from '@aws-cdk/cdk'; + +export interface TableProps { + /** + * @default ORPHAN + */ + readonly removalPolicy?: RemovalPolicy; +} +``` + +Removal policy is applied at the CFN resource level using the +**RemovalPolicy.apply(resource)**: + +```ts +RemovalPolicy.apply(cfnTable, props.removalPolicy); +``` + +The **IResource** interface requires that all resource constructs implement a +property **stateful** which returns **true** or **false** to allow runtime +checks query whether a resource is persistent +_[awslint:state-stateful-property]_. + +### Physical Names (NEW) - TODO + +See + +### Tags (NEW) + +The AWS platform has a powerful tagging system that can be used to tag resources +with key/values. The AWS CDK exposes this capability through the **Tag** +“aspect”, which can seamlessly tag all resources within a subtree: + +```ts +// add a tag to all taggable resource under "myConstruct" +myConstruct.node.apply(new cdk.Tag("myKey", "myValue")); +``` + +Constructs for AWS resources that can be tagged must have an optional **tags** +hash in their props [_awslint:tags-prop_]. + +### Secrets (NEW) + +If you expect a secret in your API (such as passwords, tokens), use the +**cdk.SecretValue** class to signal to users that they should not include +secrets in their CDK code or templates. + +If a property is named “password” it must use the **SecretValue** type +[_awslint:secret-password_]. If a property has the word “token” in it, it must +use the SecretValue type [_awslint:secret-token_]. + +## Project Structure + +### Code Organization + +* Code should be under `lib/` Entry point should be `lib/index.ts` and should +* only contain “imports” for other files. No need to put every class in a +* separate file. Try to think of a reader-friendly organization of your source +* files. + +## Implementation + +The following guidelines and recommendations apply are related to the +implementation of AWS constructs. + +### General Principles + +* Do not future proof No fluent APIs Good APIs “speak” in the language of the +* user. The terminology your API uses should be intuitive and represent the +* mental model your user brings over, not one that you made up and you force +* them to learn. Multiple ways of achieving the same thing is legitimate +* Constantly maintain the invariants Fewer “if statements” the better + +### Construct IDs + +Construct IDs (the second argument passed to all constructs when they are +defined) are used to formulate resource logical IDs which must be **stable** +across updates. The logical ID of a resource is calculated based on the **full +path** of it's construct in the construct scope hierarchy. This means that any +change to a logical ID in this path will invalidate all the logical IDs within +this scope. This will result in **replacements of all underlying resources** +within the next update, which is extremely undesirable. + +As described above, use the ID “**Resource**” for the primary resource of an AWS +construct. + +Therefore, when implementing constructs, you should treat the construct +hierarchy and all construct IDs as part of the **external contract** of the +construct. Any chance to either should be considered and called out as a +breaking change. + +There is no need to concatenate logical IDs. If you find yourself needing to +that to ensure uniqueness, it's an indication that you may be able to create +another abstraction, or even just a **Construct** instance to serve as a +namespace: + +```ts +const privateSubnets = new Construct(this, 'PrivateSubnets'); +const publicSubnets = new Construct(this, 'PublishSubnets'); + +for (const az of availabilityZones) { + new Subnet(privateSubnets, az); + new Subnet(publicSubnets, az, { public: true }); +} +``` + +### Errors + +#### input validation + +Prefer to validate input as early as it is passed into your code (ctor, methods, +etc) and bail out by throwing an **Error** (no need to create subclasses of +Error since all errors in the CDK are unrecoverable): + +* All lowercase sentences (usually they are printed after “Error: \”) +* Include a descriptive message Include the value provided Include the +* expected/allowed values No need to include information that can be obtained +* from the stack trace. No need to add a period at the end of error messages. + +#### avoid errors if possible + +Always prefer to do the right thing for the user instead of raising an +error. Only fail if the user has explicitly specified bad configuration. For +example, VPC has **enableDnsHostnames** and **enableDnsSupport**. DNS hostnames +*require* DNS support, so only fail if the user enabled DNS hostnames but +explicitly disabled DNS support. Otherwise, auto-enable DNS support for them. + +#### Never catch exceptions + +All CDK errors are unrecoverable. If a method wishes to signal a recoverable +error, this should be modeled in a return value and not through exceptions. + +#### Post Validation + +In the rare case where the integrity of your construct can only be checked right +before synthesis, override the **Construct.validate()** method and return +meaningful errors. Always prefer early input validation over post-validation. + +#### attached Errors/warnings + +You can also “attach” an error or a warning to a construct via +**node.addWarning(s)** or **node.addError(s)**. These methods will attach CDK +metadata to your construct, which will be displayed to the user by the toolchain +when the stack is deployed. + +Errors will not allow deployment and warnings will only be displayed in +highlight (unless **--strict** mode is used). + +### Tokens + +* Do not use FnSub + +## Documentation + +Documentation style (copy from official AWS docs) No need to Capitalize Resource +Names Like Buckets after they've been defined Stability (@stable, @experimental) +Use the word “define” to describe resources/constructs in your stack (instead of +“~~create~~”, “configure”, “provision”). Use a summary line and separate the +doc body from the summary line using an empty line. + +### Inline Documentation + +All public APIs must be documented when first introduced +[_awslint:docs-public-apis_]. + +Do not add documentation on overrides/implementations. The public reference +documentation will automatically copy the base documentation to the derived +APIs, so it's better to avoid the confusion [_awslint:docs-no-duplicates_]. + +Use the following JSDoc tags: **@param**, **@returns**, **@default**, **@see**, +**@example.** + +### Readme + +* Header should include maturity level Example for the simple use case should be +* almost the first thing If there are multiple common use cases, provide an +* example for each one and describe what happens under the hood at a high level +* (e.g. which resources are created). Reference docs are not needed Use +* literate (`.lit.ts`) integration tests into README file (see example in + +## Testing + +### Unit tests + +* Unit test utility functions and object models separately from constructs. If +* you want them to be “package-private”, just put them in a separate file and +* import `../lib/my-util` from your unit test code. Failing tests should be +* prefixed with “fails” + +### Integration tests + +* Integration tests should be under `test/integ.xxx.ts` and should basically + just be CDK apps that can be deployed using “cdk deploy” (in the meantime). + +### Versioning + +* Semantic versioning Construct ID changes or scope hierarchy + +## Naming & Style + +### Naming Conventions + +* **Class names**: PascalCase Properties**: camelCase Methods (static and +* **non-static)**: camelCase Interfaces** (“behavioral interface”) : +* **IMyInterface Structs** (“data interfaces”): MyDataStruct Enums**: +* **PascalCase,**Members**: SNAKE_UPPER + +### Coding Style + +* **Indentation**: 2 spaces Line length**: 150 String literals**: use +* **single-quotes (`'`) or backticks (```) Semicolons**: at the end of each code +* **statement and declaration (incl. properties and imports). Comments**: start +* **with lower-case, end with a period. diff --git a/design/aws-guidelines.md b/design/aws-guidelines.md deleted file mode 100644 index 85082cae278a3..0000000000000 --- a/design/aws-guidelines.md +++ /dev/null @@ -1,443 +0,0 @@ -# AWS Construct Library Design Guidelines - -The AWS Construct Library is a rich class library of CDK constructs which -represent all resources offered by the AWS Cloud. - -The purpose of this document is to describe common guidelines for designing the -APIs in the AWS Construct Library in order to ensure a consistent and integrated -experience across the entire AWS surface area. - -As much as possible, the guidelines in this document are enforced using the -__awslint__ tool which reflects on the APIs and verifies that the APIs adhere to -the guidelines. - -When a guideline is backed by a linter rule, the rule name will be referenced -like this: `awslint|resource-class-is-construct` -and anchored with the rule name. - -For the purpose of this document we will use "**Foo**" to denote the official -name of the resource as defined in the AWS CloudFormation resource specification -(i.e. "Bucket", "Queue", "Topic", etc). This notation allows deriving names from -the official name. For example, `FooProps` would be `BucketProps`, `TopicProps`, -etc, `IFoo` would be `IBucket`, `ITopic` and so forth. - -The guidelines in this document use TypeScript (and npm package names) since -this is the source programming language used to author the library, which is -later packaged and published to all programming languages through -[jsii](https://github.com/aws/jsii). - -## Modules - -> awslint: module-name - -AWS resources are organized into modules based on their AWS service. For -example, the "Bucket" resource, which is offered by the Amazon S3 service will -be available under the **@aws-cdk/aws-s3** module. - -## Constructs - -Constructs are the basic building block of CDK applications. They represent -abstract cloud resources of any complexity. - -> awslint: construct-ctor - -Construct initializer (constructor) signature should always be: - -```ts -constructor(scope: cdk.Construct, id: string, props: FooProps) -``` - -> __TODO__: awslint: construct-ctor-optional-props - -If all initialization properties are optional, the `props` argument must also be -optional. - -> NOTE: This rule breaks down for the case where there are two (or more) -> potential arguments, and exactly one (or at least one) of them is required. -> Then the props will be marked as optional, but the whole object is not -> optional since there is no valid use of not specifying anything. -> -> Ideally we rather not design APIs such as this, but there might be cases where -> this is the best approach. In those cases, it is okay to "exclude" this role:scope -> in `package.json`. - -```ts -constructor(scope: cdk.Construct, id: string, props: FooProps = { }) -``` - -Each module in the AWS Construct Library includes generated constructs which -represent the "raw" CloudFormation resources. - -1. These classes are named `CfnFoo` -2. They extend `cdk.Resource` (which extends `cdk.Construct`) -3. Their constructor accepts a `props` parameter of type `CfnFooProps` -4. `CfnFooProps` represents the exact set of properties as defined in the - AWS CloudFormation resource specification. -5. They have readonly properties which represent all the runtime attributes of - the resource. - -> NOTE: there are no linting rules against this section because this layer -> is entirely generated by the __cfn2ts__ tool according to these guidelines -> so there is no need to lint against it. - -## Resource Interface - -> awslint: resource-interface - -Every AWS resource should have a resource interface `IFoo`. - -This interface represents both resources defined within the same stack (aka -"internal" or "owned") or resources that are defined in different stack/app (aka -"imported", "existing", "external" or "unowned"). Throughout this document we -shall refer to these two types of resources as **"internal"** and -**"external"**. - -> awslint: resource-interface-extends-construct - -Resource interfaces should extend `cdk.IConstruct` in order to allow consumers -to take advantage of construct capabilities such as unique IDs, paths, scopes, etc. - -> TODO: awslint: resource-ref-interface - -When resources are referenced anywhere in the API (e.g. in properties or methods -of other resources or higher level constructs), the resource interface (`IFoo`) -should be preferred over the concrete resource class (`Foo`). This will allow -users to supply either internal or external resources. - -## Resource Attributes - -Every AWS resource has a set of "physical" runtime attributes such as ARN, -physical names, URLs, etc. These attributes are commonly late-bound, which means -they can only be resolved when AWS CloudFormation actually provisions the -resource. - -Resource attributes almost always represent **string** values (URL, ARN, name). -Sometimes they might also represent a **list of strings**. - -> awslint: resource-attribute
-> awslint: resource-attribute-immutable - -All resource attributes must be represented as readonly properties of the -resource interface. The names of the attributes must correspond to the -CloudFormation resource attribute name. - -> TODO: awslint: resource-attribute-type - -Since attribute values can either be late-bound ("a promise to a string") or -concrete ("a string"), the AWS CDK has a mechanism called "tokens" which allows -codifying late-bound values into strings or string arrays. This approach was -chosen in order to dramatically simplify the type-system and ergonomics of CDK -code. As long as users treat these attributes as **opaque values** (e.g. not try -to parse them or manipulate them), they can be used interchangeably. - -As long as attribute values are not manipulated, they can still be concatenated -idiomatically. For example: - -```ts -`This is my bucket name: ${bucket.bucketName} and bucket ARN: ${bucket.bucketArn}` -``` - -Even though `bucketName` and `bucketArn` will only be resolved during -deployment, the CDK will identify those as tokens and will convert this string -into an `{ "Fn::Join" }` expression which includes the relevant intrinsic -functions. - -If needed, you can query whether an object includes unresolved tokens by using -the `cdk.isToken(x)` function. - -Resource attributes should use a type that corresponds to the __resolved__ AWS -CloudFormation type (e.g. `string`, `string[]`). - -At the moment, attributes that represent strings, are represented as `string` in -the `CfnFoo` resource. However, other types of tokens (string arrays, numbers) -are still represented as `Token`. You can use `token.toList()` to represent a token as a -string array, and soon we will also have `toNumber()`. - -## Resource Class - -> awslint: resource-class - -Each `CfnFoo` resource must have a corresponding `Foo` high-level (L2) -class. - -> awslint: resource-class-is-construct - -Classes which represent AWS resources are constructs (they must extend the `cdk.Construct` class -directly or indirectly). - -## Resource Props - -> awslint: resource-props - -Resource constructs are initialized with a set of properties defined in an interface `FooProps`. - -Initialization properties should enable developers to define the resource in -their application. Generally, they should expose most of the surface -area of the resource. - -Initialization properties should be _required_ only if there is no sane default -that can be provided or calculated. By providing sensible and safe defaults (or -"smart defaults"), developers can get started quickly. - -## Imports - -In order to allow users to work with resources that are either internal or -external to their stack, AWS resources should provide an "import/export" -mechanism as described in this section. - -> awslint: import - -Every AWS resource class must include a static method called `import` with the -following signature: - -```ts -static import(scope: cdk.Construct, id: string, props: XxxImportProps): IXxx -``` - -This method returns an object that implements the resource interface (`IXxx`) -and represents an "imported resource". - -> awslint: import-props-interface - -The "props" argument is `XxxImportProps`, which is an interface that declares -properties that allow the user to specify an external resource identity, usually -by providing one or more resource attributes such as ARN, physical name, etc. - -The import interface should have the minimum required properties, that is: if it -is possible to parse the resource name from the ARN (using `cdk.Stack.parseArn`), -then only the ARN should be required. In cases where it -is not possible to parse the ARN (e.g. if it is a token and the resource name -might have use "/" characters), both the ARN and the name should be optional and -runtime-checks should be performed to require that at least one will be defined. -See `ecr.RepositoryAttributes` for an example. - -The recommended way to implement the `import` method is as follows: - -1. A public abstract base class called `XxxBase` which implements `IXxx` and - extends `cdk.Construct`. -2. The base class should provide as much of the implementation of `IXxx` as possible given the - context it has. In most cases, `grant` methods, `metric` methods, etc. can be implemented at - at that level. -5. A private class called `ImportedXxx` which extends `XxxBase` and implements - any remaining abstract members. -4. The `import` static method should be have the following implementation: - -```ts -public static import(scope: cdk.Construct, id: string, props: XxxImportProps): IXxx { - return new ImportedXxx(scope, id, props); -} -``` - -## Exports - -> awslint: export - -All resource interfaces (`IXxx`) must declare a method called `export` with the -following signature: - -```ts -export(): XxxImportProps -``` - -This method can be used to export this resource from the current stack and use -it in another stack through an `Xxx.import` call. - -The implementation of `export` is different between internal resources (`Xxx`) and -external imported resource (`ImportedXxx` as recommended above): - -For internal resources, the `export` method should produce a CloudFormation Output -for each resource attribute, and return a set of `{ "Fn::ImportValue" }` tokens -so they can be imported to another stack. - -```ts -class Xxx extends XxxBase { - public export(): XxxImportProps { - return { - attr1: new cdk.CfnOutput(this, 'Attr1', { value: this.attr1 }).makeImportValue().toString(), - attr2: new cdk.CfnOutput(this, 'Attr2', { value: this.attr2 }).makeImportValue().toString(), - } - } -} -``` - -For external resources, we know the actual values, so basically you would want to reflect -your `props` as is: - -```ts -class ImportedXxx extends XxxBase { - constructor(scope: cdk.Construct, id: string, private readonly props: XxxImportProps) { - // ... - } - - public export() { - return this.props; - } -} -``` - -The reason we are defining `export` on the resource interface and not on the resource -class is in order to allow "composite export" scenarios, where a higher-level construct -wants to implement `export` by composing the exports of multiple resources: - -```ts -interface MyCompositeProps { - bucket: s3.IBucket; - topic: sns.ITopic; -} - -class MyComposite extends cdk.Construct { - public export(): MyCompositeImportProps { - return { - bucket: this.bucket.export(), - topic: this.topic.export() - } - } -} -``` - -In this scenario, you don't know if `bucket` or `topic` are internal or external resources, -but you still want export to work. - -## General Guidelines - -### Defaults must be documented on optional interface properties - -> TODO: awslint: interface-defaults-docs - -The `@default` documentation tag must be included on all optional properties of -interfaces. - -## Complete Example - -Here's a complete "template" for the types required when defining a resource in the -AWS construct library: - -```ts -// must extend cdk.IConstruct -// extends all interfaces that are applicable for both internal -// and external resources of this type -export interface IFoo extends cdk.IConstruct, ISomething { - - // attributes - readonly fooArn: string; - readonly fooBoo: string[]; - - // security group connections (if applicable) - readonly connections: ec2.Connections; - - // permission grants (adds statements to the principal's policy) - grant(grantee?: iam.IGrantable, ...actions: string[]): void; - grantFoo(grantee?: iam.IGrantable): void; - grantBar(grantee?: iam.IGrantable): void; - - // resource policy (if applicable) - addToResourcePolicy(statement: iam.PolicyStatement): void; - - // role (if applicable) - addToRolePolicy(statement: iam.PolicyStatement): void; - - // pipeline (if applicable) - addToPipeline(stage: pipelineapi.IStage, name: string, props?: FooActionProps): FooAction; - - // metrics - metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; - metricFoo(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - metricBar(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - // export - export(): FooImportProps; - - // any other methods/properties that are applicable for both internal - // and external resources of this type. - // ... -} - -// base class to share implementation between internal/external resources -// it has to be public sadly. -export abstract class FooBase extends cdk.Construct implements IFoo { - - // attributes are usually still abstract at this level - public abstract readonly fooArn: string; - public abstract readonly fooBoo: string[]; - - // the "export" method is also still abstract - public abstract export(): FooAttributes; - - // grants can usually be shared - public grantYyy(grantee?: iam.IGrantable) { - // ... - } - - // metrics can usually be shared - public metricFoo(...) { ... } -} - -// extends the abstract base class and implement any interfaces that are not applicable -// for imported resources. This is quite rare usually, but can happen. -export class Foo extends FooBase implements IAnotherInterface { - - // the import method is always going to look like this. - public static import(scope: cdk.Construct, id: string, props: FooImportProps): IFoo { - return new ImportedFoo(scope, id, props); - } - - // implement resource attributes as readonly properties - public readonly fooArn: string; - public readonly fooBoo: string[]; - - // ctor's 3rd argument is always FooProps. It should be optional (`= { }`) in case - // there are no required properties. - constructor(scope: cdk.Construct, id: string, props: FooProps) { - super(scope, id); - - // you would usually add a `CfnFoo` resource at this point. - const resource = new CfnFoo(this, 'Resource', { - // ... - }); - - // proxy resource properties - this.fooArn = resource.fooArn; - this.fooBoo = resource.fooBoo; - } - - // this is how export() should be implemented on internal resources - // they would produce a stack export and return the "Fn::ImportValue" token - // for them so they can be imported to another stack. - public export(): FooAttributes { - return { - fooArn: new cdk.CfnOutput(this, 'Arn', { value: this.fooArn }).makeImportValue().toString(), // represent Fn::ImportValue as a string - fooBoo: new cdk.CfnOutput(this, 'Boo', { value: this.fooBoo }).makeImportValue().toList() // represent as string[] - // ... - } - } -} - -// an internal class (don't export it) representing the external (imported) resource -class ImportedFoo extends FooBase { - public readonly string fooArn; - public readonly string[] fooBoo; - - constructor(scope: cdk.Construct, id: string, private readonly props: FooImportProps) { - super(scope, id); - - this.fooArn = props.fooArn; - this.fooBoo = props.fooBoo; - } - - public export() { - return this.props; // just reflect props back - } -} -``` - -## Roadmap - -- [ ] IAM (`role`, `addToRolePolicy`, `addToResourcePolicy`) -- [ ] Grants (`grantXxx`) -- [ ] Metrics (`metricXxx`) -- [ ] Events (`onXxx`) -- [ ] Security Groups (`connections`) -- [ ] Pipeline Actions (`addToPipeline`) -- [ ] SNS Targets -- [ ] `_asFooTarget` -- [ ] TODO: other cross AWS patterns