diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index c6146379dea85..75448eabfe8e8 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -158,9 +158,67 @@ Example of overwriting the topic name from a previous deployment. $ cdk deploy --parameters "ParametersStack:TopicNameParam=blahagain" --force ``` -⚠️ Parameters will be applied to all stacks if a stack name is not specified or `*` is provided. Parameters provided to Stacks that do not make use of the parameter will not successfully deploy. +⚠️ Parameters will be applied to all stacks if a stack name is not specified or `*` is provided. +Parameters provided to Stacks that do not make use of the parameter will not successfully deploy. -⚠️ Parameters do not propagate to NestedStacks. These must be sent with the constructor. See Nested Stack [documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudformation.NestedStack.html) +⚠️ Parameters do not propagate to NestedStacks. These must be sent with the constructor. +See Nested Stack [documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudformation.NestedStack.html) + +##### Outputs + +Write stack outputs from deployments into a file. When your stack finishes deploying, all stack outputs +will be written to the output file as JSON. + +Usage of output in a CDK stack +```typescript +const fn = new lambda.Function(this, "fn", { + handler: "index.handler", + code: lambda.Code.fromInline(`exports.handler = \${handler.toString()}`), + runtime: lambda.Runtime.NODEJS_10_X +}); + +new cdk.CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, +}); +``` + +Specify an outputs file to write to by supplying the `--outputs-file` parameter + +```console +$ cdk deploy --outputs-file outputs.json +``` + +When the stack finishes deployment, `outputs.json` would look like this: +```json +{ + "MyStack": { + "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:MyStack-fn5FF616E3-G632ITHSP5HK" + } +} +``` + +⚠️ The `key` of the outputs corresponds to the logical ID of the `CfnOutput`. +Read more about identifiers in the CDK [here](https://docs.aws.amazon.com/cdk/latest/guide/identifiers.html) + +If multiple stacks are being deployed or the wild card `*` is used to deploy all stacks, all outputs +are written to the same output file where each stack artifact ID is a key in the JSON file + + +```console +$ cdk deploy '*' --outputs-file "/Users/code/myproject/outputs.json" +``` + +Example `outputs.json` after deployment of multiple stacks +```json +{ + "MyStack": { + "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:MyStack-fn5FF616E3-G632ITHSP5HK" + }, + "AnotherStack": { + "VPCId": "vpc-z0mg270fee16693f" + } +} +``` #### `cdk destroy` Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 672d676b0d455..0fca5f06b3c8a 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -71,6 +71,7 @@ async function parseCommandLineArguments() { .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) + .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) ) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }) @@ -238,7 +239,8 @@ async function initCommandLine() { sdk: aws, execute: args.execute, force: args.force, - parameters: parameterMap + parameters: parameterMap, + outputsFile: args.outputsFile }); case 'destroy': diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 1bfa81a6eedd0..dea29b910345f 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -100,6 +100,9 @@ export class CdkToolkit { } } + const stackOutputs: { [key: string]: any } = { }; + const outputsFile = options.outputsFile; + for (const stack of stacks) { if (stacks.length !== 1) { highlight(stack.displayName); } if (!stack.environment) { @@ -170,6 +173,8 @@ export class CdkToolkit { if (Object.keys(result.outputs).length > 0) { print('\nOutputs:'); + + stackOutputs[stack.stackName] = result.outputs; } for (const name of Object.keys(result.outputs)) { @@ -183,6 +188,17 @@ export class CdkToolkit { } catch (e) { error('\n ❌ %s failed: %s', colors.bold(stack.displayName), e); throw e; + } finally { + // If an outputs file has been specified, create the file path and write stack outputs to it once. + // Outputs are written after all stacks have been deployed. If a stack deployment fails, + // all of the outputs from successfully deployed stacks before the failure will still be written. + if (outputsFile) { + fs.ensureFileSync(outputsFile); + fs.writeJson(outputsFile, stackOutputs, { + spaces: 2, + encoding: 'utf8' + }); + } } } } @@ -337,6 +353,12 @@ export interface DeployOptions { * @default {} */ parameters?: { [name: string]: string | undefined }; + + /** + * Path to file where stack outputs will be written after a successful deploy as JSON + * @default - Outputs are not written to any file + */ + outputsFile?: string; } export interface DestroyOptions { diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index e4ca9ce7f39c5..b1b1294cc9d81 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -61,6 +61,34 @@ class OtherParameterStack extends cdk.Stack { } } +class OutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOutput', { + topicName: 'MyTopic' + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }) + } +} + +class AnotherOutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOtherOutput', { + topicName: 'MyOtherTopic' + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }); + } +} + class IamStack extends cdk.Stack { constructor(parent, id, props) { super(parent, id, props); @@ -208,6 +236,9 @@ new YourStack(app, `${stackPrefix}-test-2`); // Deploy wildcard with parameters does ${stackPrefix}-param-test-* new ParameterStack(app, `${stackPrefix}-param-test-1`); new OtherParameterStack(app, `${stackPrefix}-param-test-2`); +// Deploy stack with outputs does ${stackPrefix}-outputs-test-* +new OutputsStack(app, `${stackPrefix}-outputs-test-1`); +new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); // Not included in wildcard new IamStack(app, `${stackPrefix}-iam-test`); const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`); diff --git a/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.json b/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.json new file mode 100644 index 0000000000000..66a2ec3ed0071 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.json @@ -0,0 +1,8 @@ +{ + "cdk-toolkit-integration-outputs-test-1": { + "TopicName": "MyTopic" + }, + "cdk-toolkit-integration-outputs-test-2": { + "TopicName": "MyOtherTopic" + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.json b/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.json new file mode 100644 index 0000000000000..7e5a38d1d975f --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.json @@ -0,0 +1,5 @@ +{ + "cdk-toolkit-integration-outputs-test-1": { + "TopicName": "MyTopic" + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/common.bash b/packages/aws-cdk/test/integ/cli/common.bash index 690ef418f6ca5..ebf653fe4c9b9 100644 --- a/packages/aws-cdk/test/integ/cli/common.bash +++ b/packages/aws-cdk/test/integ/cli/common.bash @@ -93,6 +93,8 @@ function cleanup() { cleanup_stack ${STACK_NAME_PREFIX}-test-2 cleanup_stack ${STACK_NAME_PREFIX}-iam-test cleanup_stack ${STACK_NAME_PREFIX}-with-nested-stack + cleanup_stack ${STACK_NAME_PREFIX}-outputs-test-1 + cleanup_stack ${STACK_NAME_PREFIX}-outputs-test-2 } function setup() { diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh new file mode 100755 index 0000000000000..09e22f2015140 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +setup + +outputs_file=${integ_test_dir}/outputs/outputs.json +expected_outputs=${scriptdir}/cdk-deploy-wildcard-with-outputs-expected.json + +# deploy all outputs stacks +cdk deploy ${STACK_NAME_PREFIX}-outputs-test-\* --outputs-file ${outputs_file} +echo "Stacks deployed successfully" + +# verify generated outputs file +generated_outputs_file="$(cat ${outputs_file})" +expected_outputs_file="$(cat ${expected_outputs})" +if [[ "${generated_outputs_file}" != "${expected_outputs_file}" ]]; then + fail "unexpected outputs. Expected: ${expected_outputs_file} Actual: ${generated_outputs_file}" +fi + +# destroy +rm ${outputs_file} +cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-1 +cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-2 + +echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh new file mode 100755 index 0000000000000..da90c0e14c43d --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +setup + +outputs_file=${integ_test_dir}/outputs/outputs.json +expected_outputs=${scriptdir}/cdk-deploy-with-outputs-expected.json + +cdk deploy -v ${STACK_NAME_PREFIX}-outputs-test-1 --outputs-file ${outputs_file} +echo "Stack deployed successfully" + +# verify generated outputs file +generated_outputs_file="$(cat ${outputs_file})" +expected_outputs_file="$(cat ${expected_outputs})" +if [[ "${generated_outputs_file}" != "${expected_outputs_file}" ]]; then + fail "unexpected outputs. Expected: ${expected_outputs_file} Actual: ${generated_outputs_file}" +fi + +# destroy +rm ${outputs_file} +cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-1 + +echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh b/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh index b37e99bd2825c..05476f3b8d854 100755 --- a/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh +++ b/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh @@ -15,6 +15,8 @@ ${STACK_NAME_PREFIX}-iam-test ${STACK_NAME_PREFIX}-lambda ${STACK_NAME_PREFIX}-missing-ssm-parameter ${STACK_NAME_PREFIX}-order-providing +${STACK_NAME_PREFIX}-outputs-test-1 +${STACK_NAME_PREFIX}-outputs-test-2 ${STACK_NAME_PREFIX}-param-test-1 ${STACK_NAME_PREFIX}-param-test-2 ${STACK_NAME_PREFIX}-test-1