Skip to content

Commit

Permalink
feat(bootstrap): add kms option to cdk bootstrap (#3634)
Browse files Browse the repository at this point in the history
* feat(bootstrap): add kms option to cdk bootstrap

* feat(bootstrap): PR review

* feat(bootstrap): more aliases

* fix(bootstrap): swap aliases
  • Loading branch information
hoegertn authored and mergify[bot] committed Aug 13, 2019
1 parent 0ca5d4a commit d915aac
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 13 deletions.
21 changes: 11 additions & 10 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import colors = require('colors/safe');
import path = require('path');
import yargs = require('yargs');

import { bootstrapEnvironment, destroyStack, SDK } from '../lib';
import { bootstrapEnvironment, BootstrapEnvironmentProps, destroyStack, SDK } from '../lib';
import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments';
import { execProgram } from '../lib/api/cxapp/exec';
import { AppStacks, DefaultSelection, ExtendedStackSelection } from '../lib/api/cxapp/stacks';
Expand Down Expand Up @@ -51,13 +51,14 @@ async function parseCommandLineArguments() {
.command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' }))
.command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs
.option('toolkit-bucket-name', { type: 'string', alias: 'b', desc: 'The name of the CDK toolkit bucket', default: undefined }))
.option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket', default: undefined })
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined }))
.command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs
.option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'do not rebuild asset with the given ID. Can be specified multiple times.', default: [] })
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' })
.option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' }))
.option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' })
.option('ci', { type: 'boolean', desc: 'Force CI detection. Use --no-ci to disable CI autodetection.', default: process.env.CI !== undefined })
.option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true })
.option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true }))
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependees' })
.option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' }))
Expand Down Expand Up @@ -186,7 +187,10 @@ async function initCommandLine() {
});

case 'bootstrap':
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, args.toolkitBucketName);
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, {
bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']),
kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']),
});

case 'deploy':
return await cli.deploy({
Expand Down Expand Up @@ -236,7 +240,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, roleArn: string | undefined, toolkitBucketName: string | undefined): Promise<void> {
async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps): Promise<void> {
// Two modes of operation.
//
// If there is an '--app' argument, we select the environments from the app. Otherwise we just take the user
Expand All @@ -246,13 +250,10 @@ async function initCommandLine() {

const environments = app ? await globEnvironmentsFromStacks(appStacks, environmentGlobs, aws) : environmentsFromDescriptors(environmentGlobs);

// Bucket name can be passed using --toolkit-bucket-name or set in cdk.json
const bucketName = configuration.settings.get(['toolkitBucketName']) || toolkitBucketName;

await Promise.all(environments.map(async (environment) => {
success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name));
try {
const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn, bucketName);
const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn, props);
const message = result.noOp ? ' ✅ Environment %s bootstrapped (no changes).'
: ' ✅ Environment %s bootstrapped.';
success(message, colors.blue(environment.name));
Expand Down
29 changes: 26 additions & 3 deletions packages/aws-cdk/lib/api/bootstrap-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,41 @@ export const BUCKET_NAME_OUTPUT = 'BucketName';
/** @experimental */
export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName';

export interface BootstrapEnvironmentProps {
/**
* The name to be given to the CDK Bootstrap bucket.
*
* @default - a name is generated by CloudFormation.
*/
readonly bucketName?: string;

/**
* The ID of an existing KMS key to be used for encrypting items in the bucket.
*
* @default - the default KMS key for S3 will be used.
*/
readonly kmsKeyId?: string;
}

/** @experimental */
export async function bootstrapEnvironment(environment: cxapi.Environment, aws: ISDK, toolkitStackName: string, roleArn: string | undefined, toolkitBucketName: string | undefined): Promise<DeployStackResult> {
export async function bootstrapEnvironment(environment: cxapi.Environment, aws: ISDK, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise<DeployStackResult> {

const template = {
Description: "The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.",
Resources: {
StagingBucket: {
Type: "AWS::S3::Bucket",
Properties: {
BucketName: toolkitBucketName,
BucketName: props.bucketName,
AccessControl: "Private",
BucketEncryption: { ServerSideEncryptionConfiguration: [{ ServerSideEncryptionByDefault: { SSEAlgorithm: "aws:kms" } }] }
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: "aws:kms",
KMSMasterKeyID: props.kmsKeyId,
},
}]
}
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export class Settings {
plugin: argv.plugin,
requireApproval: argv.requireApproval,
toolkitStackName: argv.toolkitStackName,
toolkitBucket: {
bucketName: argv.bootstrapBucketName,
kmsKeyId: argv.bootstrapKmsKeyId,
},
versionReporting: argv.versionReporting,
staging: argv.staging,
output: argv.output,
Expand Down
156 changes: 156 additions & 0 deletions packages/aws-cdk/test/api/test.bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { CreateChangeSetInput } from 'aws-sdk/clients/cloudformation';
import { Test } from 'nodeunit';
import { bootstrapEnvironment } from '../../lib';
import { fromYAML } from '../../lib/serialize';
import { MockSDK } from '../util/mock-sdk';

export = {
async 'do bootstrap'(test: Test) {
// GIVEN
const sdk = new MockSDK();

let executed = false;

sdk.stubCloudFormation({
describeStacks() {
return {
Stacks: []
};
},

createChangeSet(info: CreateChangeSetInput) {
const template = fromYAML(info.TemplateBody as string);
const bucketProperties = template.Resources.StagingBucket.Properties;
test.equals(bucketProperties.BucketName, undefined, 'Expected BucketName to be undefined');
test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID,
undefined, 'Expected KMSMasterKeyID to be undefined');
return {};
},

describeChangeSet() {
return {
Status: 'CREATE_COMPLETE',
Changes: [],
};
},

executeChangeSet() {
executed = true;
return {};
}
});

// WHEN
const ret = await bootstrapEnvironment({
account: '123456789012',
region: 'us-east-1',
name: 'mock',
}, sdk, 'mockStack', undefined);

// THEN
test.equals(ret.noOp, false);
test.equals(executed, true);

test.done();
},
async 'do bootstrap using custom bucket name'(test: Test) {
// GIVEN
const sdk = new MockSDK();

let executed = false;

sdk.stubCloudFormation({
describeStacks() {
return {
Stacks: []
};
},

createChangeSet(info: CreateChangeSetInput) {
const template = fromYAML(info.TemplateBody as string);
const bucketProperties = template.Resources.StagingBucket.Properties;
test.equals(bucketProperties.BucketName, 'foobar', 'Expected BucketName to be foobar');
test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID,
undefined, 'Expected KMSMasterKeyID to be undefined');
return {};
},

describeChangeSet() {
return {
Status: 'CREATE_COMPLETE',
Changes: [],
};
},

executeChangeSet() {
executed = true;
return {};
}
});

// WHEN
const ret = await bootstrapEnvironment({
account: '123456789012',
region: 'us-east-1',
name: 'mock',
}, sdk, 'mockStack', undefined, {
bucketName: 'foobar',
});

// THEN
test.equals(ret.noOp, false);
test.equals(executed, true);

test.done();
},
async 'do bootstrap using KMS CMK'(test: Test) {
// GIVEN
const sdk = new MockSDK();

let executed = false;

sdk.stubCloudFormation({
describeStacks() {
return {
Stacks: []
};
},

createChangeSet(info: CreateChangeSetInput) {
const template = fromYAML(info.TemplateBody as string);
const bucketProperties = template.Resources.StagingBucket.Properties;
test.equals(bucketProperties.BucketName, undefined, 'Expected BucketName to be undefined');
test.equals(bucketProperties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.KMSMasterKeyID,
'myKmsKey', 'Expected KMSMasterKeyID to be myKmsKey');
return {};
},

describeChangeSet() {
return {
Status: 'CREATE_COMPLETE',
Changes: [],
};
},

executeChangeSet() {
executed = true;
return {};
}
});

// WHEN
const ret = await bootstrapEnvironment({
account: '123456789012',
region: 'us-east-1',
name: 'mock',
}, sdk, 'mockStack', undefined, {
kmsKeyId: 'myKmsKey',
});

// THEN
test.equals(ret.noOp, false);
test.equals(executed, true);

test.done();
},
};

0 comments on commit d915aac

Please sign in to comment.