Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-cdk): deploy supports CloudFormation Role #940

Merged
merged 2 commits into from
Oct 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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