diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md index 8664303213f26..1e45d14d5a2f7 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md @@ -32,9 +32,12 @@ are as follows: To get started, update your CDK App with a new `defaultStackSynthesizer`: ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', // put a unique id here + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); ``` @@ -94,9 +97,12 @@ synthesizer will create a new Staging Stack in each environment the CDK App is d its staging resources. To use this kind of synthesizer, use `AppStagingSynthesizer.defaultResources()`. ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, // The following line is optional. By default it is assumed you have bootstrapped in the same // region(s) as the stack(s) you are deploying. @@ -117,8 +123,13 @@ source code. As part of the `DefaultStagingStack`, an S3 bucket and IAM role wil used to upload the asset to S3. ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id' }), + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, + }), }); const stack = new Stack(app, 'my-stack'); @@ -138,9 +149,12 @@ You can customize some or all of the roles you'd like to use in the synthesizer if all you need is to supply custom roles (and not change anything else in the `DefaultStagingStack`): ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, deploymentIdentities: DeploymentIdentities.specifyRoles({ cloudFormationExecutionRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), deploymentRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Deploy'), @@ -158,9 +172,12 @@ and `CloudFormationExecutionRole` in the [bootstrap template](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml). ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, deploymentIdentities: DeploymentIdentities.cliCredentials(), }), }); @@ -171,9 +188,12 @@ assumable by the deployment role. You can also specify an existing IAM role for `fileAssetPublishingRole` or `imageAssetPublishingRole`: ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), imageAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/ECRAccess'), }), @@ -223,9 +243,12 @@ to a previous version of an application just by doing a CloudFormation deploymen template, without rebuilding and republishing assets. ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, deployTimeFileAssetLifetime: Duration.days(100), }), }); @@ -241,9 +264,12 @@ purged. To change the number of revisions stored, use `imageAssetVersionCount`: ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, imageAssetVersionCount: 10, }), }); @@ -257,9 +283,12 @@ or `emptyOnDelete` turned on. This creates custom resources under the hood to fa cleanup. To turn this off, specify `autoDeleteStagingAssets: false`. ```ts +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, autoDeleteStagingAssets: false, }), }); @@ -267,19 +296,20 @@ const app = new App({ ### Staging Bucket Encryption -By default, the staging resources will be stored in an S3 Bucket with KMS encryption. To use -SSE-S3, set `stagingBucketEncryption` to `BucketEncryption.S3_MANAGED`. +You must explicitly specify the encryption type for the staging bucket via the `stagingBucketEncryption` property. In +future versions of this package, the default will be `BucketEncryption.S3_MANAGED`. -```ts -import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; +In previous versions of this package, the default was to use KMS encryption for the staging bucket. KMS keys cost +$1/month, which could result in unexpected costs for users who are not aware of this. As we stabilize this module +we intend to make the default S3-managed encryption, which is free. However, the migration path from KMS to S3 +managed encryption for existing buckets is not straightforward. Therefore, for now, this property is required. -const app = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ - appId: 'my-app-id', - stagingBucketEncryption: BucketEncryption.S3_MANAGED, - }), -}); -``` +If you have an existing staging bucket encrypted with a KMS key, you will likely want to set this property to +`BucketEncryption.KMS`. If you are creating a new staging bucket, you can set this property to +`BucketEncryption.S3_MANAGED` to avoid the cost of a KMS key. + +You can learn more about choosing a bucket encryption type in the +[S3 documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html). ## Using a Custom Staging Stack per Environment diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts index 70d5cfd65fbe3..bd7f07936e0a9 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts @@ -64,9 +64,18 @@ export interface DefaultStagingStackOptions { /** * Encryption type for staging bucket * - * @default - s3.BucketEncryption.KMS + * In future versions of this package, the default will be BucketEncryption.S3_MANAGED. + * + * In previous versions of this package, the default was to use KMS encryption for the staging bucket. KMS keys cost + * $1/month, which could result in unexpected costs for users who are not aware of this. As we stabilize this module + * we intend to make the default S3-managed encryption, which is free. However, the migration path from KMS to S3 + * managed encryption for existing buckets is not straightforward. Therefore, for now, this property is required. + * + * If you have an existing staging bucket encrypted with a KMS key, you will likely want to set this property to + * BucketEncryption.KMS. If you are creating a new staging bucket, you can set this property to + * BucketEncryption.S3_MANAGED to avoid the cost of a KMS key. */ - readonly stagingBucketEncryption?: s3.BucketEncryption; + readonly stagingBucketEncryption: s3.BucketEncryption; /** * Pass in an existing role to be used as the file publishing role. @@ -155,7 +164,8 @@ export interface DefaultStagingStackProps extends DefaultStagingStackOptions, St * A default Staging Stack that implements IStagingResources. * * @example - * const defaultStagingStack = DefaultStagingStack.factory({ appId: 'my-app-id' }); + * import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; + * const defaultStagingStack = DefaultStagingStack.factory({ appId: 'my-app-id', stagingBucketEncryption: BucketEncryption.S3_MANAGED }); */ export class DefaultStagingStack extends Stack implements IStagingResources { /** @@ -226,7 +236,7 @@ export class DefaultStagingStack extends Stack implements IStagingResources { private readonly appId: string; private readonly stagingBucketName?: string; - private stagingBucketEncryption?: s3.BucketEncryption; + private stagingBucketEncryption: s3.BucketEncryption; /** * File publish role ARN in asset manifest format @@ -267,7 +277,11 @@ export class DefaultStagingStack extends Stack implements IStagingResources { this.deployRoleArn = props.deployRoleArn; this.stagingBucketName = props.stagingBucketName; + + // FIXME: when stabilizing this module, we should make `stagingBucketEncryption` optional, defaulting to S3_MANAGED. + // See https://github.com/aws/aws-cdk/pull/28978#issuecomment-1930007176 for details on this decision. this.stagingBucketEncryption = props.stagingBucketEncryption; + const specializer = new StringSpecializer(this, props.qualifier); this.providedFileRole = props.fileAssetPublishingRole?._specialize(specializer); @@ -369,11 +383,7 @@ export class DefaultStagingStack extends Stack implements IStagingResources { this.ensureFileRole(); let key = undefined; - if (this.stagingBucketEncryption === s3.BucketEncryption.KMS || this.stagingBucketEncryption === undefined) { - if (this.stagingBucketEncryption === undefined) { - // default is KMS as an AWS best practice, and for backwards compatibility - this.stagingBucketEncryption = s3.BucketEncryption.KMS; - } + if (this.stagingBucketEncryption === s3.BucketEncryption.KMS) { key = this.createBucketKey(); } diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts index 9b0b502a967b1..8e9f389b2c191 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts @@ -16,7 +16,7 @@ describe(AppStagingSynthesizer, () => { beforeEach(() => { app = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID }), + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, stagingBucketEncryption: BucketEncryption.S3_MANAGED }), }); stack = new Stack(app, 'Stack', { env: { @@ -63,7 +63,7 @@ describe(AppStagingSynthesizer, () => { test('stack template is in the asset manifest - environment tokens', () => { const app2 = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID }), + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, stagingBucketEncryption: BucketEncryption.S3_MANAGED }), }); const accountToken = Token.asString('111111111111'); const regionToken = Token.asString('us-east-2'); @@ -253,6 +253,7 @@ describe(AppStagingSynthesizer, () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, deployTimeFileAssetLifetime: Duration.days(1), + stagingBucketEncryption: BucketEncryption.KMS, }), }); stack = new Stack(app, 'Stack', { @@ -277,7 +278,6 @@ describe(AppStagingSynthesizer, () => { Status: 'Enabled', }]), }, - // When stagingBucketEncryption is not specified, it should be KMS for backwards compatibility BucketEncryption: { ServerSideEncryptionConfiguration: [ { @@ -470,6 +470,7 @@ describe(AppStagingSynthesizer, () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, imageAssetVersionCount: 1, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); stack = new Stack(app, 'Stack', { @@ -513,6 +514,7 @@ describe(AppStagingSynthesizer, () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, autoDeleteStagingAssets: false, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); stack = new Stack(app, 'Stack', { @@ -544,6 +546,7 @@ describe(AppStagingSynthesizer, () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, stagingStackNamePrefix: prefix, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); stack = new Stack(app, 'Stack', { @@ -573,6 +576,7 @@ describe(AppStagingSynthesizer, () => { expect(() => new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: Lazy.string({ produce: () => 'appId' }), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), })).toThrowError(/AppStagingSynthesizer property 'appId' may not contain tokens;/); }); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts index 63ff52e77e3fe..022e4a2cb603a 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import { App, Stack, CfnResource } from 'aws-cdk-lib'; +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema'; import { APP_ID, isAssetManifest } from './util'; import { AppStagingSynthesizer, BootstrapRole, DeploymentIdentities } from '../lib'; @@ -14,6 +15,7 @@ describe('Boostrap Roles', () => { const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'super long app id that needs to be cut', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); const stack = new Stack(app, 'Stack', { @@ -47,6 +49,7 @@ describe('Boostrap Roles', () => { lookupRole: BootstrapRole.fromRoleArn(LOOKUP_ROLE), deploymentRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE), }), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); const stack = new Stack(app, 'Stack', { @@ -79,6 +82,7 @@ describe('Boostrap Roles', () => { deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({ bootstrapRegion: 'us-west-2', }), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); @@ -100,6 +104,7 @@ describe('Boostrap Roles', () => { deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({ bootstrapRegion: 'us-west-2', }), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); @@ -118,6 +123,7 @@ describe('Boostrap Roles', () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); const stack = new Stack(app, 'Stack', { @@ -148,6 +154,7 @@ describe('Boostrap Roles', () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, imageAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/ECRAccess'), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); const stack = new Stack(app, 'Stack', { @@ -180,6 +187,7 @@ describe('Boostrap Roles', () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, deploymentIdentities: DeploymentIdentities.cliCredentials(), + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); const stack = new Stack(app, 'Stack', { @@ -209,6 +217,7 @@ describe('Boostrap Roles', () => { defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ bootstrapQualifier: 'abcdef', appId: APP_ID, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); new Stack(app, 'Stack', { @@ -245,4 +254,4 @@ function synthStack(app: App) { // THEN return asm.getStackArtifact('Stack'); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts index ffb00dd7b1259..f2df4b2a671b4 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts @@ -1,4 +1,5 @@ import { App } from 'aws-cdk-lib'; +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; import { testWithXRepos } from './util'; import { DefaultStagingStack } from '../lib'; @@ -16,6 +17,7 @@ describe('default staging stack', () => { expect(() => new DefaultStagingStack(app, 'stack', { appId: 'a'.repeat(21), qualifier: 'qualifier', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, })).toThrowError(/appId expected no more than 20 characters but got 21 characters./); }); @@ -24,6 +26,7 @@ describe('default staging stack', () => { expect(() => new DefaultStagingStack(app, 'stack', { appId: 'ABCDEF', qualifier: 'qualifier', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, })).toThrowError(/appId only accepts lowercase characters./); }); @@ -32,6 +35,7 @@ describe('default staging stack', () => { expect(() => new DefaultStagingStack(app, 'stack', { appId: 'ca$h', qualifier: 'qualifier', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, })).toThrowError(/appId expects only letters, numbers, and dashes \('-'\)/); }); @@ -41,6 +45,7 @@ describe('default staging stack', () => { expect(() => new DefaultStagingStack(app, 'stack', { appId, qualifier: 'qualifier', + stagingBucketEncryption: BucketEncryption.S3_MANAGED, })).toThrowError([ `appId ${appId} has errors:`, 'appId expected no more than 20 characters but got 40 characters.', @@ -49,4 +54,4 @@ describe('default staging stack', () => { ].join('\n')); }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts index 37bfa697f2794..5e21a52cf1464 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as integ from '@aws-cdk/integ-tests-alpha'; import { App, Stack } from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; import { APP_ID_MAX } from './util'; import { AppStagingSynthesizer } from '../lib'; @@ -17,6 +18,7 @@ const app = new App({ const stack = new Stack(app, 'synthesize-default-resources', { synthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID_MAX, // this has implications on the overall template size + stagingBucketEncryption: BucketEncryption.KMS, }), }); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts index a5001aaa9f2f4..d865f86637b00 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts @@ -1,4 +1,5 @@ import { App, Stack } from 'aws-cdk-lib'; +import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; import { APP_ID } from './util'; import { AppStagingSynthesizer } from '../lib'; @@ -8,6 +9,7 @@ describe('per environment cache', () => { const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); new Stack(app, 'Stack1', { @@ -36,6 +38,7 @@ describe('per environment cache', () => { const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); new Stack(app, 'Stack1', { @@ -64,6 +67,7 @@ describe('per environment cache', () => { const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID, + stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), }); new Stack(app, 'Stack1', {