diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md index d2271bee90a82..44a6447b415e2 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md @@ -48,44 +48,106 @@ in your account. ## Bootstrap Model -In our default bootstrapping process, when you run `cdk bootstrap aws:///`, the following -resources are created: - -- It creates Roles to assume for cross-account deployments and for Pipeline deployments; -- It creates staging resources: a global S3 bucket and global ECR repository to hold CDK assets; -- It creates Roles to write to the S3 bucket and ECR repository; - -Because the bootstrapping resources include regional resources, you need to bootstrap -every region you plan to deploy to individually. All assets of all CDK apps deploying -to that account and region will be written to the single S3 Bucket and ECR repository. - -By using the synthesizer in this library, instead of the -`DefaultStackSynthesizer`, a different set of staging resources will be created -for every CDK application, and they will be created automatically as part of a -regular deployment, in a separate Stack that is deployed before your application -Stacks. The staging resources will be one S3 bucket, and *one ECR repository per -image*, and Roles necessary to access those buckets and ECR repositories. The -Roles from the default bootstrap stack are still used (though their use can be -turned off). - -This has the following advantages: - -- Because staging resources are now application-specific, they can be fully cleaned up when you clean up - the application. -- Because there is now one ECR repository per image instead of one ECR repository for all images, it is - possible to effectively use ECR life cycle rules (for example, retain only the most recent 5 images) - to cut down on storage costs. -- Resources between separate CDK Apps are separated so they can be cleaned up and lifecycle - controlled individually. -- Because the only shared bootstrapping resources required are Roles, which are global resources, - you now only need to bootstrap every account in one Region (instead of every Region). This makes it - easier to do with CloudFormation StackSets. +Our current bootstrap model looks like this, when you run `cdk bootstrap aws:///` : + +```text +┌───────────────────────────────────┐┌────────────────────────┐┌────────────────────────┐ +│ ││ ││ │ +│ ││ ││ │ +│ ┌───────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │ +│ │Bootstrap Stack│ ││ │ CDK App 1 │ ││ │ CDK App 2 │ │ +│ └───────────────┘ ││ └──────────────┘ ││ └──────────────┘ │ +│ ││ ││ │ +│ ││ ││ │ +│ ┌───────────────────────────┐ ││ ┌────────────┐ ││ │ +│ │IAM Role for CFN execution │ ││┌────│ S3 Asset │ ││ │ +│ │ IAM Role for lookup │ │││ └────────────┘ ││ │ +│ │ IAM Role for deployment │ │││ ││ │ +│ └───────────────────────────┘ │││ ││ ┌─────────────┐ │ +│ │││ ┌──────────┼┼─────│ S3 Asset │ │ +│ │││ │ ││ └─────────────┘ │ +│ ┌───────────────────────────────┐ │││ │ ││ │ +│ │ IAM Role for File Publishing │ │││ │ ││ │ +│ │ IAM Role for Image Publishing │ │││ │ ││ │ +│ └───────────────────────────────┘ │││ │ ││ │ +│ │││ │ ││ │ +│ ┌─────────────────────────────┐ │││ │ ││ │ +│ │S3 Bucket for Staging Assets │ │││ │ ││ │ +│ │ KMS Key encryption │◀─┼┼┴────────────┘ ││ ┌────────────┐ │ +│ └─────────────────────────────┘ ││ ┌──────────┼┼───── │ ECR Asset │ │ +│ ││ │ ││ └────────────┘ │ +│ ││ │ ││ │ +│┌─────────────────────────────────┐││ │ ││ │ +││ECR Repository for Staging Assets◀┼┼─────────────┘ ││ │ +│└─────────────────────────────────┘││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +└───────────────────────────────────┘└────────────────────────┘└────────────────────────┘ +``` + +Your CDK Application utilizes these resources when deploying. For example, if you have a file asset, +it gets uploaded to the S3 Staging Bucket using the File Publishing Role when you run `cdk deploy`. + +This library introduces an alternate model to bootstrapping, by splitting out essential CloudFormation IAM roles +and staging resources. There will still be a Bootstrap Stack, but this will only contain IAM roles necessary for +CloudFormation deployment. Each CDK App will instead be in charge of its own staging resources, including the +S3 Bucket, ECR Repositories, and associated IAM roles. It works like this: + +The Staging Stack will contain, on a per-need basis, + +- 1 S3 Bucket with KMS encryption for all file assets in the CDK App. +- An ECR Repository _per_ image (and its revisions). +- IAM roles with access to the Bucket and Repositories. + +```text +┌─────────────────────────────┐┌───────────────────────────────────────┐┌───────────────────────────────────────┐ +│ ││ ││ │ +│ ┌───────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │ +│ │Bootstrap Stack│ ││ │ CDK App 1 │ ││ │ CDK App 2 │ │ +│ └───────────────┘ ││ └──────────────┘ ││ └──────────────┘ │ +│ ││┌──────────────────┐ ││┌──────────────────┐ │ +│ │││ ┌──────────────┐ │ │││ ┌──────────────┐ │ │ +│ │││ │Staging Stack │ │ │││ │Staging Stack │ │ │ +│ │││ └──────────────┘ │ │││ └──────────────┘ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││┌────────────────┐│ ┌────────────┐│││┌────────────────┐│ ┌────────────┐│ +│ ││││ IAM Role for ││ ┌───│ S3 Asset │││││ IAM Role for ││ ┌───│ S3 Asset ││ +│ ││││File Publishing ││ │ └────────────┘││││File Publishing ││ │ └────────────┘│ +│ │││└────────────────┘│ │ ││││ IAM Role for ││ │ │ +│ │││ │ │ ││││Image Publishing││ │ │ +│┌───────────────────────────┐│││ │ │ │││└────────────────┘│ │ │ +││IAM Role for CFN execution ││││ │ │ │││ │ │ │ +││ IAM Role for lookup ││││ │ │ │││ │ │ │ +││ IAM Role for deployment ││││┌────────────────┐│ │ │││┌────────────────┐│ │ │ +│└───────────────────────────┘││││ S3 Bucket for ││ │ ││││ S3 Bucket for ││ │ │ +│ ││││ Staging Assets │◀─┘ ││││ Staging Assets │◀─┘ │ +│ │││└────────────────┘│ │││└────────────────┘│ ┌───────────┐│ +│ │││ │ │││ │ ┌───│ ECR Asset ││ +│ │││ │ │││┌────────────────┐│ │ └───────────┘│ +│ │││ │ ││││ ECR Repository ││ │ │ +│ │││ │ ││││ for Staging │◀──┘ │ +│ │││ │ ││││ Assets ││ │ +│ │││ │ │││└────────────────┘│ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ ││└──────────────────┘ ││└──────────────────┘ │ +└─────────────────────────────┘└───────────────────────────────────────┘└───────────────────────────────────────┘ +``` -For the deployment roles, this synthesizer still uses the Roles from the default -bootstrap stack, and nothing else. The staging resources from that bootstrap -stack will be unused. You can customize the template to remove those resources -if you prefer. In the future, we will provide a bootstrap stack template with -only those Roles, specifically for use with this synthesizer. +This allows staging resources to be created when needed next to the CDK App. It has the following +benefits: + +- Resources between separate CDK Apps are separated so they can be cleaned up and lifecycle +controlled individually. +- Users have a familiar way to customize staging resources in the CDK Application. ## Using the Default Staging Stack per Environment @@ -97,10 +159,6 @@ its staging resources. To use this kind of synthesizer, use `AppStagingSynthesiz const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', - - // 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. - deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({ bootstrapRegion: 'us-east-1' }), }), }); ``` @@ -174,7 +232,7 @@ assumable by the deployment role. You can also specify an existing IAM role for const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', - fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), + fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), imageAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/ECRAccess'), }), }); @@ -184,14 +242,14 @@ const app = new App({ There are two types of assets: -- Assets used only during deployment. These are used to hand off a large piece of data to another -service, that will make a private copy of that data. After deployment, the asset is only necessary for -a potential future rollback. +- Assets used only during deployment. These are used to hand off a large piece of data to another +service, that will make a private copy of that data. After deployment, the asset is only necessary for +a potential future rollback. - Assets accessed throughout the running life time of the application. Examples of assets that are only used at deploy time are CloudFormation Templates and Lambda Code -bundles. Examples of assets accessed throughout the life time of the application are script files -downloaded to run in a CodeBuild Project, or on EC2 instance startup. ECR images are always application +bundles. Examples of assets accessed throughout the life time of the application are script files +downloaded to run in a CodeBuild Project, or on EC2 instance startup. ECR images are always application life-time assets. S3 deploy time assets are stored with a `deploy-time/` prefix, and a lifecycle rule will collect them after a configurable number of days. Lambda assets are by default marked as deploy time assets: @@ -217,9 +275,9 @@ const asset = new Asset(stack, 'deploy-time-asset', { }); ``` -By default, we store deploy time assets for 30 days, but you can change this number by specifying +By default, we store deploy time assets for 30 days, but you can change this number by specifying `deployTimeFileAssetLifetime`. The number you specify here is how long you will be able to roll back -to a previous version of an application just by doing a CloudFormation deployment with the old +to a previous version of an application just by doing a CloudFormation deployment with the old template, without rebuilding and republishing assets. ```ts @@ -258,7 +316,7 @@ you can subclass `DefaultStagingStack`. ```ts interface CustomStagingStackOptions extends DefaultStagingStackOptions {} -class CustomStagingStack extends DefaultStagingStack { +class CustomStagingStack extends DefaultStagingStack { } ``` @@ -331,13 +389,13 @@ const app = new App({ Since this module is experimental, there are some known limitations: -- Currently this module does not support CDK Pipelines. You must deploy CDK Apps using this - synthesizer via `cdk deploy`. Please upvote [this issue](https://github.com/aws/aws-cdk/issues/26118) - to indicate you want this. +- Currently this module does not support CDK Pipelines. You must deploy CDK Apps using this + synthesizer via `cdk deploy`. - This synthesizer only needs a bootstrap stack with Roles, without staging resources. We haven't written such a bootstrap stack yet; at the moment you can use the existing modern - bootstrap stack, the staging resources in them will just go unused. You can customize the - template to remove them if desired. + bootstrap stack, the staging resources in them will just go unused. - Due to limitations on the CloudFormation template size, CDK Applications can have - at most 38 independent ECR images. Please upvote [this issue](https://github.com/aws/aws-cdk/issues/26119) - if you need more than this. + at most 38 independent ECR images. +- When you run `cdk destroy` (for example during testing), the staging bucket and ECR + repositories will be left behind because CloudFormation cannot clean up non-empty resources. + You must deploy those resources manually if you want to redeploy again using the same `appId`. diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts index 6511808a95395..d422e182bcb97 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts @@ -171,13 +171,13 @@ export class AppStagingSynthesizer extends StackSynthesizer implements IReusable private constructor(private readonly props: AppStagingSynthesizerProps) { super(); - const defaultIdentities = DeploymentIdentities.defaultBootstrapRoles(); - const identities = props.deploymentIdentities ?? DeploymentIdentities.defaultBootstrapRoles(); - this.roles = { - deploymentRole: identities.deploymentRole ?? defaultIdentities.deploymentRole!, - cloudFormationExecutionRole: identities.cloudFormationExecutionRole ?? defaultIdentities.cloudFormationExecutionRole!, - lookupRole: identities.lookupRole ?? identities.lookupRole!, + deploymentRole: props.deploymentIdentities?.roles.deploymentRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_DEPLOY_ROLE_ARN), + cloudFormationExecutionRole: props.deploymentIdentities?.roles.cloudFormationExecutionRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN), + lookupRole: this.props.deploymentIdentities?.roles.lookupRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_LOOKUP_ROLE_ARN), }; } diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts index a91311e4660f9..a5454ca51d021 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts @@ -1,5 +1,4 @@ import { StringSpecializer, translateAssetTokenToCfnToken, translateCfnTokenToAssetToken } from 'aws-cdk-lib/core/lib/helpers-internal'; -import { AppStagingSynthesizer } from './app-staging-synthesizer'; /** * Bootstrapped role specifier. These roles must exist already. @@ -78,62 +77,10 @@ export class DeploymentIdentities { return new DeploymentIdentities(roles); } - /** - * Use the Roles that have been created by the default bootstrap stack - */ - public static defaultBootstrapRoles(options: DefaultBootstrapRolesOptions = {}): DeploymentIdentities { - function replacePlaceholders(x: string) { - if (options.bootstrapRegion !== undefined) { - x = x.replace(/\$\{AWS::Region\}/g, options.bootstrapRegion); - } - return x; - } - - return new DeploymentIdentities({ - deploymentRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_DEPLOY_ROLE_ARN)), - cloudFormationExecutionRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN)), - lookupRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_LOOKUP_ROLE_ARN)), - }); - } - - /** - * CloudFormation Execution Role - */ - public readonly cloudFormationExecutionRole?: BootstrapRole; - - /** - * Deployment Action Role - */ - public readonly deploymentRole?: BootstrapRole; - - /** - * Lookup Role - @default - use bootstrapped role - */ - public readonly lookupRole?: BootstrapRole; - private constructor( /** roles that are bootstrapped to your account. */ - roles: BootstrapRoles, - ) { - this.cloudFormationExecutionRole = roles.cloudFormationExecutionRole; - this.deploymentRole = roles.deploymentRole; - this.lookupRole = roles.lookupRole; - } -} - -/** - * Options for `DeploymentIdentities.defaultBootstrappedRoles` - */ -export interface DefaultBootstrapRolesOptions { - /** - * The region where the default bootstrap roles have been created - * - * By default, the region in which the stack is deployed is used. - * - * @default - the stack's current region - */ - readonly bootstrapRegion?: string; + public readonly roles: BootstrapRoles, + ) {} } /** 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..a46e1807f8c97 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 @@ -71,47 +71,6 @@ describe('Boostrap Roles', () => { expect(stackArtifact.assumeRoleArn).toEqual(DEPLOY_ACTION_ROLE); }); - test('can request other bootstrap region', () => { - // GIVEN - const app = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ - appId: APP_ID, - deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({ - bootstrapRegion: 'us-west-2', - }), - }), - }); - - // WHEN - const stackArtifact = synthStack(app); - - // Bootstrapped roles are as advertised - expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-west-2'); - expect(stackArtifact.lookupRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-west-2' }); - expect(stackArtifact.assumeRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-west-2'); - }); - - test('can request other qualifier', () => { - // GIVEN - const app = new App({ - defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ - appId: APP_ID, - bootstrapQualifier: 'Q', - deploymentIdentities: DeploymentIdentities.defaultBootstrapRoles({ - bootstrapRegion: 'us-west-2', - }), - }), - }); - - // WHEN - const stackArtifact = synthStack(app); - - // Bootstrapped roles are as advertised - expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-cfn-exec-role-000000000000-us-west-2'); - expect(stackArtifact.lookupRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-lookup-role-000000000000-us-west-2' }); - expect(stackArtifact.assumeRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-deploy-role-000000000000-us-west-2'); - }); - test('can supply existing arn for bucket staging role', () => { // GIVEN const app = new App({ @@ -228,21 +187,3 @@ describe('Boostrap Roles', () => { expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-abcdef-cfn-exec-role-000000000000-us-east-1'); }); }); - -function synthStack(app: App) { - const stack = new Stack(app, 'Stack', { - env: { - account: '000000000000', - region: 'us-east-1', - }, - }); - new CfnResource(stack, 'Resource', { - type: 'Some::Resource', - }); - - // WHEN - const asm = app.synth(); - - // THEN - return asm.getStackArtifact('Stack'); -} \ No newline at end of file