Skip to content

Commit

Permalink
feat(aws-cdk): deploy supports CloudFormation Role (#940)
Browse files Browse the repository at this point in the history
Add --role-arn parameter to the CDK toolkit to allow passing a custom
role when invoking CloudFormation.

Fixes #735.
  • Loading branch information
rix0rrr committed Oct 17, 2018
1 parent 2d3661c commit 393be6f
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 30 deletions.
19 changes: 10 additions & 9 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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<void> {
async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined): Promise<void> {
if (environmentGlobs.length === 0) {
environmentGlobs = [ '**' ]; // default to ALL
}
Expand 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));
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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);
Expand Down
60 changes: 60 additions & 0 deletions packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/api/bootstrap-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeployStackResult> {
export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string, roleArn: string | undefined): Promise<DeployStackResult> {
const synthesizedStack: SynthesizedStack = {
environment,
metadata: { },
Expand Down Expand Up @@ -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 });
}
53 changes: 34 additions & 19 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeployStackResult> {
if (!stack.environment) {
throw new Error(`The stack ${stack.name} does not have an environment`);
export async function deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
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.`);
Expand All @@ -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);
Expand All @@ -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(); }
Expand Down Expand Up @@ -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') {
Expand Down

0 comments on commit 393be6f

Please sign in to comment.