Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 393be6f

Browse files
authoredOct 17, 2018
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.
1 parent 2d3661c commit 393be6f

File tree

4 files changed

+106
-30
lines changed

4 files changed

+106
-30
lines changed
 

‎packages/aws-cdk/bin/cdk.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async function parseCommandLineArguments() {
5656
.option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified.' })
5757
.option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status.' })
5858
.option('version-reporting', { type: 'boolean', desc: 'Disable insersion of the CDKMetadata resource in synthesized templates', default: undefined })
59+
.option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined })
5960
.command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs
6061
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' }))
6162
.command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs
@@ -188,13 +189,13 @@ async function initCommandLine() {
188189
return await diffStack(await findStack(args.STACK), args.template);
189190

190191
case 'bootstrap':
191-
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName);
192+
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn);
192193

193194
case 'deploy':
194-
return await cliDeploy(args.STACKS, toolkitStackName);
195+
return await cliDeploy(args.STACKS, toolkitStackName, args.roleArn);
195196

196197
case 'destroy':
197-
return await cliDestroy(args.STACKS, args.force);
198+
return await cliDestroy(args.STACKS, args.force, args.roleArn);
198199

199200
case 'synthesize':
200201
case 'synth':
@@ -266,7 +267,7 @@ async function initCommandLine() {
266267
* all stacks are implicitly selected.
267268
* @param toolkitStackName the name to be used for the CDK Toolkit stack.
268269
*/
269-
async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string): Promise<void> {
270+
async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined): Promise<void> {
270271
if (environmentGlobs.length === 0) {
271272
environmentGlobs = [ '**' ]; // default to ALL
272273
}
@@ -282,7 +283,7 @@ async function initCommandLine() {
282283
await Promise.all(environments.map(async (environment) => {
283284
success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name));
284285
try {
285-
const result = await bootstrapEnvironment(environment, aws, toolkitStackName);
286+
const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn);
286287
const message = result.noOp ? ' ✅ Environment %s was already fully bootstrapped!'
287288
: ' ✅ Successfully bootstraped environment %s!';
288289
success(message, colors.blue(environment.name));
@@ -575,7 +576,7 @@ async function initCommandLine() {
575576
return response.stacks;
576577
}
577578
578-
async function cliDeploy(stackNames: string[], toolkitStackName: string) {
579+
async function cliDeploy(stackNames: string[], toolkitStackName: string, roleArn: string | undefined) {
579580
const stacks = await selectStacks(...stackNames);
580581
renames.validateSelectedStacks(stacks);
581582
@@ -595,7 +596,7 @@ async function initCommandLine() {
595596
}
596597
597598
try {
598-
const result = await deployStack(stack, aws, toolkitInfo, deployName);
599+
const result = await deployStack({ stack, sdk: aws, toolkitInfo, deployName, roleArn });
599600
const message = result.noOp ? ` Stack was already up-to-date, it has ARN ${colors.blue(result.stackArn)}`
600601
: ` Deployment of stack %s completed successfully, it has ARN ${colors.blue(result.stackArn)}`;
601602
data(result.stackArn);
@@ -611,7 +612,7 @@ async function initCommandLine() {
611612
}
612613
}
613614
614-
async function cliDestroy(stackNames: string[], force: boolean) {
615+
async function cliDestroy(stackNames: string[], force: boolean, roleArn: string | undefined) {
615616
const stacks = await selectStacks(...stackNames);
616617
renames.validateSelectedStacks(stacks);
617618
@@ -628,7 +629,7 @@ async function initCommandLine() {
628629
629630
success(' ⏳ Starting destruction of stack %s...', colors.blue(deployName));
630631
try {
631-
await destroyStack(stack, aws, deployName);
632+
await destroyStack({ stack, sdk: aws, deployName, roleArn });
632633
success(' ✅ Stack %s successfully destroyed.', colors.blue(deployName));
633634
} catch (e) {
634635
error(' ❌ Destruction failed: %s', colors.blue(deployName), e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
role_name=cdk-integ-test-role
8+
delete_role() {
9+
for policy_name in $(aws iam list-role-policies --role-name $role_name --output text --query PolicyNames); do
10+
aws iam delete-role-policy --role-name $role_name --policy-name $policy_name
11+
done
12+
aws iam delete-role --role-name $role_name
13+
}
14+
15+
delete_role || echo 'Role does not exist yet'
16+
17+
role_arn=$(aws iam create-role \
18+
--output text --query Role.Arn \
19+
--role-name $role_name \
20+
--assume-role-policy-document file://<(echo '{
21+
"Version": "2012-10-17",
22+
"Statement": [{
23+
"Action": "sts:AssumeRole",
24+
"Principal": { "Service": "cloudformation.amazonaws.com" },
25+
"Effect": "Allow"
26+
}]
27+
}'))
28+
trap delete_role EXIT
29+
aws iam put-role-policy \
30+
--role-name $role_name \
31+
--policy-name DefaultPolicy \
32+
--policy-document file://<(echo '{
33+
"Version": "2012-10-17",
34+
"Statement": [{
35+
"Action": "*",
36+
"Resource": "*",
37+
"Effect": "Allow"
38+
}]
39+
}')
40+
41+
setup
42+
43+
stack_arn=$(cdk --role-arn $role_arn deploy cdk-toolkit-integration-test-2)
44+
echo "Stack deployed successfully"
45+
46+
# verify that we only deployed a single stack (there's a single ARN in the output)
47+
assert_lines "${stack_arn}" 1
48+
49+
# verify the number of resources in the stack
50+
response_json=$(mktemp).json
51+
aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json}
52+
resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)")
53+
if [ "${resource_count}" -ne 2 ]; then
54+
fail "stack has ${resource_count} resources, and we expected two"
55+
fi
56+
57+
# destroy
58+
cdk destroy --role-arn $role_arn -f cdk-toolkit-integration-test-2
59+
60+
echo "✅ success"

‎packages/aws-cdk/lib/api/bootstrap-environment.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SDK } from './util/sdk';
77
export const BUCKET_NAME_OUTPUT = 'BucketName';
88
export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName';
99

10-
export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string): Promise<DeployStackResult> {
10+
export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string, roleArn: string | undefined): Promise<DeployStackResult> {
1111
const synthesizedStack: SynthesizedStack = {
1212
environment,
1313
metadata: { },
@@ -37,5 +37,5 @@ export async function bootstrapEnvironment(environment: Environment, aws: SDK, t
3737
},
3838
name: toolkitStackName,
3939
};
40-
return await deployStack(synthesizedStack, aws);
40+
return await deployStack({ stack: synthesizedStack, sdk: aws, roleArn });
4141
}

‎packages/aws-cdk/lib/api/deploy-stack.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,30 @@ export interface DeployStackResult {
2323
readonly stackArn: string;
2424
}
2525

26+
export interface DeployStackOptions {
27+
stack: cxapi.SynthesizedStack;
28+
sdk: SDK;
29+
toolkitInfo?: ToolkitInfo;
30+
roleArn?: string;
31+
deployName?: string;
32+
quiet?: boolean;
33+
}
34+
2635
const LARGE_TEMPLATE_SIZE_KB = 50;
2736

28-
export async function deployStack(stack: cxapi.SynthesizedStack,
29-
sdk: SDK,
30-
toolkitInfo?: ToolkitInfo,
31-
deployName?: string,
32-
quiet: boolean = false): Promise<DeployStackResult> {
33-
if (!stack.environment) {
34-
throw new Error(`The stack ${stack.name} does not have an environment`);
37+
export async function deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
38+
if (!options.stack.environment) {
39+
throw new Error(`The stack ${options.stack.name} does not have an environment`);
3540
}
3641

37-
const params = await prepareAssets(stack, toolkitInfo);
42+
const params = await prepareAssets(options.stack, options.toolkitInfo);
3843

39-
deployName = deployName || stack.name;
44+
const deployName = options.deployName || options.stack.name;
4045

4146
const executionId = uuid.v4();
4247

43-
const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting);
44-
const bodyParameter = await makeBodyParameter(stack, toolkitInfo);
48+
const cfn = await options.sdk.cloudFormation(options.stack.environment, Mode.ForWriting);
49+
const bodyParameter = await makeBodyParameter(options.stack, options.toolkitInfo);
4550

4651
if (await stackFailedCreating(cfn, deployName)) {
4752
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,
6469
TemplateBody: bodyParameter.TemplateBody,
6570
TemplateURL: bodyParameter.TemplateURL,
6671
Parameters: params,
72+
RoleARN: options.roleArn,
6773
Capabilities: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ]
6874
}).promise();
6975
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,
7682

7783
debug('Initiating execution of changeset %s on stack %s', changeSetName, deployName);
7884
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
79-
const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName, stack.metadata, changeSetDescription.Changes.length).start();
85+
// tslint:disable-next-line:max-line-length
86+
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack.metadata, changeSetDescription.Changes.length).start();
8087
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSetName, deployName);
8188
await waitForStack(cfn, deployName);
8289
if (monitor) { await monitor.stop(); }
@@ -128,18 +135,26 @@ async function makeBodyParameter(stack: cxapi.SynthesizedStack, toolkitInfo?: To
128135
}
129136
}
130137

131-
export async function destroyStack(stack: cxapi.SynthesizedStack, sdk: SDK, deployName?: string, quiet: boolean = false) {
132-
if (!stack.environment) {
133-
throw new Error(`The stack ${stack.name} does not have an environment`);
138+
export interface DestroyStackOptions {
139+
stack: cxapi.SynthesizedStack;
140+
sdk: SDK;
141+
roleArn?: string;
142+
deployName?: string;
143+
quiet?: boolean;
144+
}
145+
146+
export async function destroyStack(options: DestroyStackOptions) {
147+
if (!options.stack.environment) {
148+
throw new Error(`The stack ${options.stack.name} does not have an environment`);
134149
}
135150

136-
deployName = deployName || stack.name;
137-
const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting);
151+
const deployName = options.deployName || options.stack.name;
152+
const cfn = await options.sdk.cloudFormation(options.stack.environment, Mode.ForWriting);
138153
if (!await stackExists(cfn, deployName)) {
139154
return;
140155
}
141-
const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName).start();
142-
await cfn.deleteStack({ StackName: deployName }).promise().catch(e => { throw e; });
156+
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName).start();
157+
await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }).promise().catch(e => { throw e; });
143158
const destroyedStack = await waitForStack(cfn, deployName, false);
144159
if (monitor) { await monitor.stop(); }
145160
if (destroyedStack && destroyedStack.StackStatus !== 'DELETE_COMPLETE') {

0 commit comments

Comments
 (0)
Failed to load comments.