From 393be6f5400ef697a0e672043f5a408e9be45196 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 17 Oct 2018 09:51:57 +0200 Subject: [PATCH] feat(aws-cdk): deploy supports CloudFormation Role (#940) Add --role-arn parameter to the CDK toolkit to allow passing a custom role when invoking CloudFormation. Fixes #735. --- packages/aws-cdk/bin/cdk.ts | 19 +++--- .../integ-tests/test-cdk-deploy-with-role.sh | 60 +++++++++++++++++++ .../aws-cdk/lib/api/bootstrap-environment.ts | 4 +- packages/aws-cdk/lib/api/deploy-stack.ts | 53 ++++++++++------ 4 files changed, 106 insertions(+), 30 deletions(-) create mode 100755 packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index e990910be1e38..1c57b2d122f99 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -56,6 +56,7 @@ async function parseCommandLineArguments() { .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified.' }) .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status.' }) .option('version-reporting', { type: 'boolean', desc: 'Disable insersion of the CDKMetadata resource in synthesized templates', default: undefined }) + .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined }) .command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' })) .command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs @@ -188,13 +189,13 @@ async function initCommandLine() { return await diffStack(await findStack(args.STACK), args.template); case 'bootstrap': - return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName); + return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn); case 'deploy': - return await cliDeploy(args.STACKS, toolkitStackName); + return await cliDeploy(args.STACKS, toolkitStackName, args.roleArn); case 'destroy': - return await cliDestroy(args.STACKS, args.force); + return await cliDestroy(args.STACKS, args.force, args.roleArn); case 'synthesize': case 'synth': @@ -266,7 +267,7 @@ async function initCommandLine() { * all stacks are implicitly selected. * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ - async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string): Promise { + async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined): Promise { if (environmentGlobs.length === 0) { environmentGlobs = [ '**' ]; // default to ALL } @@ -282,7 +283,7 @@ async function initCommandLine() { await Promise.all(environments.map(async (environment) => { success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name)); try { - const result = await bootstrapEnvironment(environment, aws, toolkitStackName); + const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn); const message = result.noOp ? ' ✅ Environment %s was already fully bootstrapped!' : ' ✅ Successfully bootstraped environment %s!'; success(message, colors.blue(environment.name)); @@ -575,7 +576,7 @@ async function initCommandLine() { return response.stacks; } - async function cliDeploy(stackNames: string[], toolkitStackName: string) { + async function cliDeploy(stackNames: string[], toolkitStackName: string, roleArn: string | undefined) { const stacks = await selectStacks(...stackNames); renames.validateSelectedStacks(stacks); @@ -595,7 +596,7 @@ async function initCommandLine() { } try { - const result = await deployStack(stack, aws, toolkitInfo, deployName); + const result = await deployStack({ stack, sdk: aws, toolkitInfo, deployName, roleArn }); const message = result.noOp ? ` ✅ Stack was already up-to-date, it has ARN ${colors.blue(result.stackArn)}` : ` ✅ Deployment of stack %s completed successfully, it has ARN ${colors.blue(result.stackArn)}`; data(result.stackArn); @@ -611,7 +612,7 @@ async function initCommandLine() { } } - async function cliDestroy(stackNames: string[], force: boolean) { + async function cliDestroy(stackNames: string[], force: boolean, roleArn: string | undefined) { const stacks = await selectStacks(...stackNames); renames.validateSelectedStacks(stacks); @@ -628,7 +629,7 @@ async function initCommandLine() { success(' ⏳ Starting destruction of stack %s...', colors.blue(deployName)); try { - await destroyStack(stack, aws, deployName); + await destroyStack({ stack, sdk: aws, deployName, roleArn }); success(' ✅ Stack %s successfully destroyed.', colors.blue(deployName)); } catch (e) { error(' ❌ Destruction failed: %s', colors.blue(deployName), e); diff --git a/packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh b/packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh new file mode 100755 index 0000000000000..e184324c26b9c --- /dev/null +++ b/packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +role_name=cdk-integ-test-role +delete_role() { + for policy_name in $(aws iam list-role-policies --role-name $role_name --output text --query PolicyNames); do + aws iam delete-role-policy --role-name $role_name --policy-name $policy_name + done + aws iam delete-role --role-name $role_name +} + +delete_role || echo 'Role does not exist yet' + +role_arn=$(aws iam create-role \ + --output text --query Role.Arn \ + --role-name $role_name \ + --assume-role-policy-document file://<(echo '{ + "Version": "2012-10-17", + "Statement": [{ + "Action": "sts:AssumeRole", + "Principal": { "Service": "cloudformation.amazonaws.com" }, + "Effect": "Allow" + }] + }')) +trap delete_role EXIT +aws iam put-role-policy \ + --role-name $role_name \ + --policy-name DefaultPolicy \ + --policy-document file://<(echo '{ + "Version": "2012-10-17", + "Statement": [{ + "Action": "*", + "Resource": "*", + "Effect": "Allow" + }] + }') + +setup + +stack_arn=$(cdk --role-arn $role_arn deploy cdk-toolkit-integration-test-2) +echo "Stack deployed successfully" + +# verify that we only deployed a single stack (there's a single ARN in the output) +assert_lines "${stack_arn}" 1 + +# verify the number of resources in the stack +response_json=$(mktemp).json +aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json} +resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)") +if [ "${resource_count}" -ne 2 ]; then + fail "stack has ${resource_count} resources, and we expected two" +fi + +# destroy +cdk destroy --role-arn $role_arn -f cdk-toolkit-integration-test-2 + +echo "✅ success" diff --git a/packages/aws-cdk/lib/api/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap-environment.ts index 6f6d12378792c..9755769caa4a0 100644 --- a/packages/aws-cdk/lib/api/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap-environment.ts @@ -7,7 +7,7 @@ import { SDK } from './util/sdk'; export const BUCKET_NAME_OUTPUT = 'BucketName'; export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; -export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string): Promise { +export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string, roleArn: string | undefined): Promise { const synthesizedStack: SynthesizedStack = { environment, metadata: { }, @@ -37,5 +37,5 @@ export async function bootstrapEnvironment(environment: Environment, aws: SDK, t }, name: toolkitStackName, }; - return await deployStack(synthesizedStack, aws); + return await deployStack({ stack: synthesizedStack, sdk: aws, roleArn }); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index b7b9fe7ce849b..81511a9530355 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -23,25 +23,30 @@ export interface DeployStackResult { readonly stackArn: string; } +export interface DeployStackOptions { + stack: cxapi.SynthesizedStack; + sdk: SDK; + toolkitInfo?: ToolkitInfo; + roleArn?: string; + deployName?: string; + quiet?: boolean; +} + const LARGE_TEMPLATE_SIZE_KB = 50; -export async function deployStack(stack: cxapi.SynthesizedStack, - sdk: SDK, - toolkitInfo?: ToolkitInfo, - deployName?: string, - quiet: boolean = false): Promise { - if (!stack.environment) { - throw new Error(`The stack ${stack.name} does not have an environment`); +export async function deployStack(options: DeployStackOptions): Promise { + if (!options.stack.environment) { + throw new Error(`The stack ${options.stack.name} does not have an environment`); } - const params = await prepareAssets(stack, toolkitInfo); + const params = await prepareAssets(options.stack, options.toolkitInfo); - deployName = deployName || stack.name; + const deployName = options.deployName || options.stack.name; const executionId = uuid.v4(); - const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting); - const bodyParameter = await makeBodyParameter(stack, toolkitInfo); + const cfn = await options.sdk.cloudFormation(options.stack.environment, Mode.ForWriting); + const bodyParameter = await makeBodyParameter(options.stack, options.toolkitInfo); if (await stackFailedCreating(cfn, deployName)) { debug(`Found existing stack ${deployName} that had previously failed creation. Deleting it before attempting to re-create it.`); @@ -64,6 +69,7 @@ export async function deployStack(stack: cxapi.SynthesizedStack, TemplateBody: bodyParameter.TemplateBody, TemplateURL: bodyParameter.TemplateURL, Parameters: params, + RoleARN: options.roleArn, Capabilities: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ] }).promise(); debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id); @@ -76,7 +82,8 @@ export async function deployStack(stack: cxapi.SynthesizedStack, debug('Initiating execution of changeset %s on stack %s', changeSetName, deployName); await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise(); - const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName, stack.metadata, changeSetDescription.Changes.length).start(); + // tslint:disable-next-line:max-line-length + const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack.metadata, changeSetDescription.Changes.length).start(); debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSetName, deployName); await waitForStack(cfn, deployName); if (monitor) { await monitor.stop(); } @@ -128,18 +135,26 @@ async function makeBodyParameter(stack: cxapi.SynthesizedStack, toolkitInfo?: To } } -export async function destroyStack(stack: cxapi.SynthesizedStack, sdk: SDK, deployName?: string, quiet: boolean = false) { - if (!stack.environment) { - throw new Error(`The stack ${stack.name} does not have an environment`); +export interface DestroyStackOptions { + stack: cxapi.SynthesizedStack; + sdk: SDK; + roleArn?: string; + deployName?: string; + quiet?: boolean; +} + +export async function destroyStack(options: DestroyStackOptions) { + if (!options.stack.environment) { + throw new Error(`The stack ${options.stack.name} does not have an environment`); } - deployName = deployName || stack.name; - const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting); + const deployName = options.deployName || options.stack.name; + const cfn = await options.sdk.cloudFormation(options.stack.environment, Mode.ForWriting); if (!await stackExists(cfn, deployName)) { return; } - const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName).start(); - await cfn.deleteStack({ StackName: deployName }).promise().catch(e => { throw e; }); + const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName).start(); + await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }).promise().catch(e => { throw e; }); const destroyedStack = await waitForStack(cfn, deployName, false); if (monitor) { await monitor.stop(); } if (destroyedStack && destroyedStack.StackStatus !== 'DELETE_COMPLETE') {