Skip to content

Commit bedd4c0

Browse files
authored
fix(bootstrap): no longer creates KMS master key by default (#10365)
The modern bootstrap stack used to unconditionally create a KMS Customer Master Key (CMK) for users. This incurs a $1/month charge for every user of the CDK for every region and account they want to deploy in, which is not acceptable if we're going to make this the default bootstrapping experience in the future. This PR switches off the creation of the CMK by default for new bootstrap stacks. Bootstrap stacks that already exist can remove the existing CMK by running: ``` cdk bootstrap --bootstrap-customer-key=false [aws://...] ``` This change is backwards compatible: updates to existing (modern) bootstrap stacks will leave the current KMS key in place. To achieve this, the new default is encoded into the CLI, not into the template. Fixes #10115. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent cb6fef8 commit bedd4c0

8 files changed

+194
-72
lines changed

packages/aws-cdk/bin/cdk.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ async function parseCommandLineArguments() {
6969
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }))
7070
.command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs
7171
.option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined })
72-
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined })
72+
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' })
73+
.option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' })
7374
.option('qualifier', { type: 'string', desc: 'Unique string to distinguish multiple bootstrap stacks', default: undefined })
7475
.option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined })
7576
.option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] })
@@ -271,6 +272,7 @@ async function initCommandLine() {
271272
parameters: {
272273
bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']),
273274
kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']),
275+
createCustomerMasterKey: args.bootstrapCustomerKey,
274276
qualifier: args.qualifier,
275277
publicAccessBlockConfiguration: args.publicAccessBlockConfiguration,
276278
trustedAccounts: arrayFromYargs(args.trust),

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

+38-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Bootstrapper {
2525
case 'legacy':
2626
return this.legacyBootstrap(environment, sdkProvider, options);
2727
case 'default':
28-
return this.defaultBootstrap(environment, sdkProvider, options);
28+
return this.modernBootstrap(environment, sdkProvider, options);
2929
case 'custom':
3030
return this.customBootstrap(environment, sdkProvider, options);
3131
}
@@ -45,13 +45,16 @@ export class Bootstrapper {
4545
const params = options.parameters ?? {};
4646

4747
if (params.trustedAccounts?.length) {
48-
throw new Error('--trust can only be passed for the new bootstrap experience.');
48+
throw new Error('--trust can only be passed for the modern bootstrap experience.');
4949
}
5050
if (params.cloudFormationExecutionPolicies?.length) {
51-
throw new Error('--cloudformation-execution-policies can only be passed for the new bootstrap experience.');
51+
throw new Error('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.');
52+
}
53+
if (params.createCustomerMasterKey !== undefined) {
54+
throw new Error('--bootstrap-customer-key can only be passed for the modern bootstrap experience.');
5255
}
5356
if (params.qualifier) {
54-
throw new Error('--qualifier can only be passed for the new bootstrap experience.');
57+
throw new Error('--qualifier can only be passed for the modern bootstrap experience.');
5558
}
5659

5760
const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName);
@@ -66,7 +69,7 @@ export class Bootstrapper {
6669
*
6770
* @experimental
6871
*/
69-
private async defaultBootstrap(
72+
private async modernBootstrap(
7073
environment: cxapi.Environment,
7174
sdkProvider: SdkProvider,
7275
options: BootstrapEnvironmentOptions = {}): Promise<DeployStackResult> {
@@ -77,6 +80,10 @@ export class Bootstrapper {
7780

7881
const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName);
7982

83+
if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) {
84+
throw new Error('You cannot pass \'--bootstrap-kms-key-id\' and \'--bootstrap-customer-key\' together. Specify one or the other');
85+
}
86+
8087
// If people re-bootstrap, existing parameter values are reused so that people don't accidentally change the configuration
8188
// on their bootstrap stack (this happens automatically in deployStack). However, to do proper validation on the
8289
// combined arguments (such that if --trust has been given, --cloudformation-execution-policies is necessary as well)
@@ -93,15 +100,28 @@ export class Bootstrapper {
93100
if (cloudFormationExecutionPolicies.length === 0) {
94101
throw new Error('Please pass \'--cloudformation-execution-policies\' to specify deployment permissions. Try a managed policy of the form \'arn:aws:iam::aws:policy/<PolicyName>\'.');
95102
}
96-
// Remind people what the current settings are
103+
104+
// * If an ARN is given, that ARN. Otherwise:
105+
// * '-' if customerKey = false
106+
// * '' if customerKey = true
107+
// * if customerKey is also not given
108+
// * undefined if we already had a value in place (reusing what we had)
109+
// * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap)
110+
const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId;
111+
const kmsKeyId = params.kmsKeyId ??
112+
(params.createCustomerMasterKey === true ? CREATE_NEW_KEY :
113+
params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY :
114+
undefined);
115+
116+
// Remind people what we settled on
97117
info(`Trusted accounts: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`);
98118
info(`Execution policies: ${cloudFormationExecutionPolicies.join(', ')}`);
99119

100120
return current.update(
101121
bootstrapTemplate,
102122
{
103123
FileAssetsBucketName: params.bucketName,
104-
FileAssetsBucketKmsKeyId: params.kmsKeyId,
124+
FileAssetsBucketKmsKeyId: kmsKeyId,
105125
// Empty array becomes empty string
106126
TrustedAccounts: trustedAccounts.join(','),
107127
CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','),
@@ -124,7 +144,7 @@ export class Bootstrapper {
124144
if (version === 0) {
125145
return this.legacyBootstrap(environment, sdkProvider, options);
126146
} else {
127-
return this.defaultBootstrap(environment, sdkProvider, options);
147+
return this.modernBootstrap(environment, sdkProvider, options);
128148
}
129149
}
130150

@@ -139,3 +159,13 @@ export class Bootstrapper {
139159
}
140160
}
141161
}
162+
163+
/**
164+
* Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default keyo
165+
*/
166+
const USE_AWS_MANAGED_KEY = 'AWS_MANAGED_KEY';
167+
168+
/**
169+
* Magic parameter value that will cause the bootstrap-template.yml to create a CMK
170+
*/
171+
const CREATE_NEW_KEY = '';

packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,20 @@ export interface BootstrappingParameters {
5555
/**
5656
* The ID of an existing KMS key to be used for encrypting items in the bucket.
5757
*
58-
* @default - the default KMS key for S3 will be used.
58+
* @default - use the default KMS key or create a custom one
5959
*/
6060
readonly kmsKeyId?: string;
6161

62+
/**
63+
* Whether or not to create a new customer master key (CMK)
64+
*
65+
* Only applies to modern bootstrapping. Legacy bootstrapping will never create
66+
* a CMK, only use the default S3 key.
67+
*
68+
* @default false
69+
*/
70+
readonly createCustomerMasterKey?: boolean;
71+
6272
/**
6373
* The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped.
6474
*

packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml

+10-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Parameters:
1616
Default: ''
1717
Type: String
1818
FileAssetsBucketKmsKeyId:
19-
Description: Custom KMS key ID to use for encrypting file assets (by default a
20-
KMS key will be automatically defined)
19+
Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed
20+
S3 key, or the ID/ARN of an existing key.
2121
Default: ''
2222
Type: String
2323
ContainerAssetsRepositoryName:
@@ -61,6 +61,10 @@ Conditions:
6161
Fn::Equals:
6262
- ''
6363
- Ref: FileAssetsBucketKmsKeyId
64+
UseAwsManagedKey:
65+
Fn::Equals:
66+
- 'AWS_MANAGED_KEY'
67+
- Ref: FileAssetsBucketKmsKeyId
6468
HasCustomContainerAssetsRepositoryName:
6569
Fn::Not:
6670
- Fn::Equals:
@@ -150,7 +154,10 @@ Resources:
150154
Fn::If:
151155
- CreateNewKey
152156
- Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}"
153-
- Fn::Sub: "${FileAssetsBucketKmsKeyId}"
157+
- Fn::If:
158+
- UseAwsManagedKey
159+
- Ref: AWS::NoValue
160+
- Fn::Sub: "${FileAssetsBucketKmsKeyId}"
154161
PublicAccessBlockConfiguration:
155162
Fn::If:
156163
- UsePublicAccessBlockConfiguration

packages/aws-cdk/lib/api/util/cloudformation.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export class StackParameters {
377377
this._changes = true;
378378
}
379379

380-
if (key in updates && updates[key]) {
380+
if (key in updates && updates[key] !== undefined) {
381381
this.apiParameters.push({ ParameterKey: key, ParameterValue: updates[key] });
382382

383383
// If the updated value is different than the current value, this will lead to a change

packages/aws-cdk/test/api/bootstrap.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ test('passing trusted accounts to the old bootstrapping results in an error', as
158158
},
159159
}))
160160
.rejects
161-
.toThrow('--trust can only be passed for the new bootstrap experience.');
161+
.toThrow('--trust can only be passed for the modern bootstrap experience.');
162162
});
163163

164164
test('passing CFN execution policies to the old bootstrapping results in an error', async () => {
@@ -169,7 +169,7 @@ test('passing CFN execution policies to the old bootstrapping results in an erro
169169
},
170170
}))
171171
.rejects
172-
.toThrow('--cloudformation-execution-policies can only be passed for the new bootstrap experience.');
172+
.toThrow('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.');
173173
});
174174

175175
test('even if the bootstrap stack is in a rollback state, can still retry bootstrapping it', async () => {

0 commit comments

Comments
 (0)