Skip to content

Commit

Permalink
chore(core): Stages (#8423)
Browse files Browse the repository at this point in the history
Stages are self-contained application units that synthesize as a cloud assembly. This change centralizes prepare + synthesis logic into the stage level and changes `App` to extend `Stage`. 

Once `stage.synth()` is called, the stage becomes (practically) immutable. This means that subsequent synths will return the same output.

The cloud assembly produced by stages is nested as an artifact inside another cloud assembly (either the App's top-level assembly) or a child.

Authors: @rix0rrr, @eladb 

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Elad Ben-Israel authored Jun 8, 2020
1 parent 6407535 commit f5ebacd
Show file tree
Hide file tree
Showing 28 changed files with 1,279 additions and 153 deletions.
6 changes: 5 additions & 1 deletion allowed-breaking-changes.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Actually adding any artifact type will break the load() type signature because I could have written
# const x: A | B = Manifest.load();
# and that won't typecheck if Manifest.load() adds a union arm and now returns A | B | C.
change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load

removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN
removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN
removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId
removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingRoleArn

22 changes: 21 additions & 1 deletion packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,27 @@ export interface TreeArtifactProperties {
readonly file: string;
}

/**
* Artifact properties for nested cloud assemblies
*/
export interface NestedCloudAssemblyProperties {
/**
* Relative path to the nested cloud assembly
*/
readonly directoryName: string;

/**
* Display name for the cloud assembly
*
* @default - The artifact ID
*/
readonly displayName?: string;
}

/**
* Properties for manifest artifacts
*/
export type ArtifactProperties = AwsCloudFormationStackProperties | AssetManifestProperties | TreeArtifactProperties;
export type ArtifactProperties = AwsCloudFormationStackProperties
| AssetManifestProperties
| TreeArtifactProperties
| NestedCloudAssemblyProperties;
5 changes: 5 additions & 0 deletions packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export enum ArtifactType {
* Manifest for all assets in the Cloud Assembly
*/
ASSET_MANIFEST = 'cdk:asset-manifest',

/**
* Nested Cloud Assembly
*/
NESTED_CLOUD_ASSEMBLY = 'cdk:cloud-assembly',
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
},
{
"$ref": "#/definitions/TreeArtifactProperties"
},
{
"$ref": "#/definitions/NestedCloudAssemblyProperties"
}
]
}
Expand All @@ -85,6 +88,7 @@
"enum": [
"aws:cloudformation:stack",
"cdk:asset-manifest",
"cdk:cloud-assembly",
"cdk:tree",
"none"
],
Expand Down Expand Up @@ -331,6 +335,23 @@
"file"
]
},
"NestedCloudAssemblyProperties": {
"description": "Artifact properties for nested cloud assemblies",
"type": "object",
"properties": {
"directoryName": {
"description": "Relative path to the nested cloud assembly",
"type": "string"
},
"displayName": {
"description": "Display name for the cloud assembly (Default - The artifact ID)",
"type": "string"
}
},
"required": [
"directoryName"
]
},
"MissingContext": {
"description": "Represents a missing piece of context.",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"4.0.0"}
{"version":"5.0.0"}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
set -euo pipefail
scriptsdir=$(cd $(dirname $0) && pwd)
packagedir=$(realpath ${scriptsdir}/..)
packagedir=$(cd ${scriptsdir}/.. && pwd)

# Output
OUTPUT_DIR="${packagedir}/schema"
Expand Down
64 changes: 50 additions & 14 deletions packages/@aws-cdk/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,42 @@ Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html) for
information of most of the capabilities of this library. The rest of this
README will only cover topics not already covered in the Developer Guide.

## Stacks and Stages

A `Stack` is the smallest physical unit of deployment, and maps directly onto
a CloudFormation Stack. You define a Stack by defining a subclass of `Stack`
-- let's call it `MyStack` -- and instantiating the constructs that make up
your application in `MyStack`'s constructor. You then instantiate this stack
one or more times to define different instances of your application. For example,
you can instantiate it once using few and cheap EC2 instances for testing,
and once again using more and bigger EC2 instances for production.

When your application grows, you may decide that it makes more sense to split it
out across multiple `Stack` classes. This can happen for a number of reasons:

- You could be starting to reach the maximum number of resources allowed in a single
stack (this is currently 200).
- You could decide you want to separate out stateful resources and stateless resources
into separate stacks, so that it becomes easy to tear down and recreate the stacks
that don't have stateful resources.
- There could be a single stack with resources (like a VPC) that are shared
between multiple instances of other stacks containing your applications.

As soon as your conceptual application starts to encompass multiple stacks,
it is convenient to wrap them in another construct that represents your
logical application. You can then treat that new unit the same way you used
to be able to treat a single stack: by instantiating it multiple times
for different instances of your application.

You can define a custom subclass of `Construct`, holding one or more
`Stack`s, to represent a single logical instance of your application.

As a final note: `Stack`s are not a unit of reuse. They describe physical
deployment layouts, and as such are best left to application builders to
organize their deployments with. If you want to vend a reusable construct,
define it as a subclasses of `Construct`: the consumers of your construct
will decide where to place it in their own stacks.

## Nested Stacks

[Nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are stacks created as part of other stacks. You create a nested stack within another stack by using the `NestedStack` construct.
Expand All @@ -36,7 +72,7 @@ class MyNestedStack extends cfn.NestedStack {
constructor(scope: Construct, id: string, props?: cfn.NestedStackProps) {
super(scope, id, props);

new s3.Bucket(this, 'NestedBucket');
new s3.Bucket(this, 'NestedBucket');
}
}

Expand Down Expand Up @@ -236,7 +272,7 @@ new CustomResource(this, 'MyMagicalResource', {
Property2: 'bar'
},

// the ARN of the provider (SNS/Lambda) which handles
// the ARN of the provider (SNS/Lambda) which handles
// CREATE, UPDATE or DELETE events for this resource type
// see next section for details
serviceToken: 'ARN'
Expand Down Expand Up @@ -292,7 +328,7 @@ function getOrCreate(scope: Construct): sns.Topic {
Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification
is sent to the SNS topic. Users must process these notifications (e.g. through a
fleet of worker hosts) and submit success/failure responses to the
CloudFormation service.
CloudFormation service.

Set `serviceToken` to `topic.topicArn` in order to use this provider:

Expand All @@ -311,7 +347,7 @@ new CustomResource(this, 'MyResource', {

An AWS lambda function is called *directly* by CloudFormation for all resource
events. The handler must take care of explicitly submitting a success/failure
response to the CloudFormation service and handle various error cases.
response to the CloudFormation service and handle various error cases.

Set `serviceToken` to `lambda.functionArn` to use this provider:

Expand Down Expand Up @@ -361,7 +397,7 @@ exports.handler = async function(event) {
const id = event.PhysicalResourceId; // only for "Update" and "Delete"
const props = event.ResourceProperties;
const oldProps = event.OldResourceProperties; // only for "Update"s

switch (event.RequestType) {
case "Create":
// ...
Expand All @@ -371,7 +407,7 @@ exports.handler = async function(event) {

// if an error is thrown, a FAILED response will be submitted to CFN
throw new Error('Failed!');

case "Delete":
// ...
}
Expand Down Expand Up @@ -403,10 +439,10 @@ Here is an complete example of a custom resource that summarizes two numbers:

```js
exports.handler = async e => {
return {
Data: {
return {
Data: {
Result: e.ResourceProperties.lhs + e.ResourceProperties.rhs
}
}
};
};
```
Expand Down Expand Up @@ -463,7 +499,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be
implemented in any Lambda-supported runtime. Furthermore, this provider has an
asynchronous mode, which means that users can provide an `isComplete` lambda
function which is called periodically until the operation is complete. This
allows implementing providers that can take up to two hours to stabilize.
allows implementing providers that can take up to two hours to stabilize.

Set `serviceToken` to `provider.serviceToken` to use this type of provider:

Expand All @@ -487,7 +523,7 @@ See the [documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/custom-r
Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification
is sent to the SNS topic. Users must process these notifications (e.g. through a
fleet of worker hosts) and submit success/failure responses to the
CloudFormation service.
CloudFormation service.

Set `serviceToken` to `topic.topicArn` in order to use this provider:

Expand All @@ -506,7 +542,7 @@ new CustomResource(this, 'MyResource', {

An AWS lambda function is called *directly* by CloudFormation for all resource
events. The handler must take care of explicitly submitting a success/failure
response to the CloudFormation service and handle various error cases.
response to the CloudFormation service and handle various error cases.

Set `serviceToken` to `lambda.functionArn` to use this provider:

Expand All @@ -532,7 +568,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be
implemented in any Lambda-supported runtime. Furthermore, this provider has an
asynchronous mode, which means that users can provide an `isComplete` lambda
function which is called periodically until the operation is complete. This
allows implementing providers that can take up to two hours to stabilize.
allows implementing providers that can take up to two hours to stabilize.

Set `serviceToken` to `provider.serviceToken` to use this provider:

Expand Down Expand Up @@ -827,7 +863,7 @@ to use intrinsic functions in keys. Since JSON map keys must be strings, it is
impossible to use intrinsics in keys and `CfnJson` can help.

The following example defines an IAM role which can only be assumed by
principals that are tagged with a specific tag.
principals that are tagged with a specific tag.

```ts
const tagParam = new CfnParameter(this, 'TagName');
Expand Down
46 changes: 5 additions & 41 deletions packages/@aws-cdk/core/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import { Construct, ConstructNode } from './construct-compat';
import { prepareApp } from './private/prepare-app';
import { collectRuntimeInformation } from './private/runtime-info';
import { TreeMetadata } from './private/tree-metadata';
import { Stage } from './stage';

const APP_SYMBOL = Symbol.for('@aws-cdk/core.App');

Expand Down Expand Up @@ -76,8 +74,7 @@ export interface AppProps {
*
* @see https://docs.aws.amazon.com/cdk/latest/guide/apps.html
*/
export class App extends Construct {

export class App extends Stage {
/**
* Checks if an object is an instance of the `App` class.
* @returns `true` if `obj` is an `App`.
Expand All @@ -87,16 +84,14 @@ export class App extends Construct {
return APP_SYMBOL in obj;
}

private _assembly?: cxapi.CloudAssembly;
private readonly runtimeInfo: boolean;
private readonly outdir?: string;

/**
* Initializes a CDK application.
* @param props initialization properties
*/
constructor(props: AppProps = {}) {
super(undefined as any, '');
super(undefined as any, '', {
outdir: props.outdir ?? process.env[cxapi.OUTDIR_ENV],
});

Object.defineProperty(this, APP_SYMBOL, { value: true });

Expand All @@ -110,10 +105,6 @@ export class App extends Construct {
this.node.setContext(cxapi.DISABLE_VERSION_REPORTING, true);
}

// both are reverse logic
this.runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? false : true;
this.outdir = props.outdir || process.env[cxapi.OUTDIR_ENV];

const autoSynth = props.autoSynth !== undefined ? props.autoSynth : cxapi.OUTDIR_ENV in process.env;
if (autoSynth) {
// synth() guarantuees it will only execute once, so a default of 'true'
Expand All @@ -126,33 +117,6 @@ export class App extends Construct {
}
}

/**
* Synthesizes a cloud assembly for this app. Emits it to the directory
* specified by `outdir`.
*
* @returns a `CloudAssembly` which can be used to inspect synthesized
* artifacts such as CloudFormation templates and assets.
*/
public synth(): cxapi.CloudAssembly {
// we already have a cloud assembly, no-op for you
if (this._assembly) {
return this._assembly;
}

const assembly = ConstructNode.synth(this.node, {
outdir: this.outdir,
runtimeInfo: this.runtimeInfo ? collectRuntimeInformation() : undefined,
});

this._assembly = assembly;
return assembly;
}

protected prepare() {
super.prepare();
prepareApp(this);
}

private loadContext(defaults: { [key: string]: string } = { }) {
// prime with defaults passed through constructor
for (const [ k, v ] of Object.entries(defaults)) {
Expand Down
25 changes: 12 additions & 13 deletions packages/@aws-cdk/core/lib/construct-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export enum ConstructOrder {

/**
* Options for synthesis.
*
* @deprecated use `app.synth()` or `stage.synth()` instead
*/
export interface SynthesisOptions extends cxapi.AssemblyBuildOptions {
/**
Expand Down Expand Up @@ -222,28 +224,25 @@ export class ConstructNode {

/**
* Synthesizes a CloudAssembly from a construct tree.
* @param root The root of the construct tree.
* @param node The root of the construct tree.
* @param options Synthesis options.
* @deprecated Use `app.synth()` or `stage.synth()` instead
*/
public static synth(root: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly {
const builder = new cxapi.CloudAssemblyBuilder(options.outdir);

root._actualNode.synthesize({
outdir: builder.outdir,
skipValidation: options.skipValidation,
sessionContext: {
assembly: builder,
},
});

return builder.buildAssembly(options);
public static synth(node: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const a: typeof import('././private/synthesis') = require('./private/synthesis');
return a.synthesize(node.root, options);
}

/**
* Invokes "prepare" on all constructs (depth-first, post-order) in the tree under `node`.
* @param node The root node
* @deprecated Use `app.synth()` instead
*/
public static prepare(node: ConstructNode) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p: typeof import('./private/prepare-app') = require('./private/prepare-app');
p.prepareApp(node.root); // resolve cross refs and nested stack assets.
return node._actualNode.prepare();
}

Expand Down
Loading

0 comments on commit f5ebacd

Please sign in to comment.