Skip to content

Commit

Permalink
feat(app-staging-synthesizer-alpha): require passing `stagingBucketEn…
Browse files Browse the repository at this point in the history
…cryption` and note that we intend to default to `S3_MANAGED` in the future (#28978)

### Issue # (if applicable)

Relates to #28815

### Reason for this change

The App Staging Synthesizer is great - I've moved to using it for most of my stacks. However, the current default uses a Customer-Managed KMS key, which costs $1/month.

The default synthesizer bucket uses SSE-S3 encryption by default. This is nice because users do not incur additional fees for a KMS key.

In my opinion, SSE-S3 is good enough for most people. If folks need additional security, they should opt-in to SSE-KMS, which they can do via the `stagingBucketEncryption` property @msambol introduced with #28903.

### Description of changes

With guidance from @kaizencc [below](#28978 (comment)), this PR makes `stagingBucketEncryption` a required property, with a user-facing note that we intend to default to `S3_MANAGED` as the module is stablized.

### Description of how you validated changes

Updated unit tests.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)


BREAKING CHANGE: `stagingBucketEncryption` property is now required. For existing apps, specify `BucketEncryption.KMS` to retain existing behavior. For new apps, choose the bucket encryption that makes most sense for your use case. `BucketEncryption.S3_MANAGED` is available and is intended to be the default when this module is stabilized.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
blimmer committed Feb 13, 2024
1 parent 5a54ec5 commit fc8b955
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 26 deletions.
54 changes: 42 additions & 12 deletions packages/@aws-cdk/app-staging-synthesizer-alpha/README.md
Expand Up @@ -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,
}),
});
```
Expand Down Expand Up @@ -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.
Expand All @@ -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');
Expand All @@ -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'),
Expand All @@ -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(),
}),
});
Expand All @@ -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'),
}),
Expand Down Expand Up @@ -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),
}),
});
Expand All @@ -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,
}),
});
Expand All @@ -257,29 +283,33 @@ 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,
}),
});
```

### 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

Expand Down
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}

Expand Down
Expand Up @@ -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: {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -253,6 +253,7 @@ describe(AppStagingSynthesizer, () => {
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
deployTimeFileAssetLifetime: Duration.days(1),
stagingBucketEncryption: BucketEncryption.KMS,
}),
});
stack = new Stack(app, 'Stack', {
Expand All @@ -277,7 +278,6 @@ describe(AppStagingSynthesizer, () => {
Status: 'Enabled',
}]),
},
// When stagingBucketEncryption is not specified, it should be KMS for backwards compatibility
BucketEncryption: {
ServerSideEncryptionConfiguration: [
{
Expand Down Expand Up @@ -470,6 +470,7 @@ describe(AppStagingSynthesizer, () => {
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
imageAssetVersionCount: 1,
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});
stack = new Stack(app, 'Stack', {
Expand Down Expand Up @@ -513,6 +514,7 @@ describe(AppStagingSynthesizer, () => {
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
autoDeleteStagingAssets: false,
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});
stack = new Stack(app, 'Stack', {
Expand Down Expand Up @@ -544,6 +546,7 @@ describe(AppStagingSynthesizer, () => {
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
stagingStackNamePrefix: prefix,
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});
stack = new Stack(app, 'Stack', {
Expand Down Expand Up @@ -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;/);
});
Expand Down
@@ -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';
Expand All @@ -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', {
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -79,6 +82,7 @@ describe('Boostrap Roles', () => {
deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({
bootstrapRegion: 'us-west-2',
}),
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});

Expand All @@ -100,6 +104,7 @@ describe('Boostrap Roles', () => {
deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({
bootstrapRegion: 'us-west-2',
}),
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});

Expand All @@ -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', {
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -209,6 +217,7 @@ describe('Boostrap Roles', () => {
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
bootstrapQualifier: 'abcdef',
appId: APP_ID,
stagingBucketEncryption: BucketEncryption.S3_MANAGED,
}),
});
new Stack(app, 'Stack', {
Expand Down Expand Up @@ -245,4 +254,4 @@ function synthStack(app: App) {

// THEN
return asm.getStackArtifact('Stack');
}
}

0 comments on commit fc8b955

Please sign in to comment.