Skip to content

Commit

Permalink
chore(cli): implementation of the new bootstrap logic (aws#5856)
Browse files Browse the repository at this point in the history
This adds a new `cdk bootstrap` mode,
hidden behind the environment variable CDK_NEW_BOOTSTRAP,
that adds the necessary resources (ECR repository, IAM roles, etc.)
for our "CI/CD for CDK apps" epic,
as well as 2 new (hidden for now)
options: --trust (for specifying the accounts that are trusted to deploy into this environment)
and --cloudformation-execution-policies
(for specifying the ARNs of the Managed Policis that will be attached to the CFN deploying role).
  • Loading branch information
skinny85 authored Feb 17, 2020
1 parent 7fcbc48 commit de48222
Show file tree
Hide file tree
Showing 19 changed files with 863 additions and 342 deletions.
312 changes: 2 additions & 310 deletions design/cdk-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,313 +337,5 @@ This should make sure the CFN update succeeds.

## Bootstrap template

Here is the JSON of the bootstrap CloudFormation template:

```json
{
"Description": "The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.",
"Parameters": {
"TrustedPrincipals": {
"Description": "List of AWS principals that the publish and action roles should trust to be assumed from",
"Default": "",
"Type": "CommaDelimitedList"
},
"CloudFormationExecutionPolicies": {
"Description": "List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role",
"Default": "",
"Type": "CommaDelimitedList"
}
},
"Conditions": {
"HasTrustedPrincipals": {
"Fn::Not": [
{
"Fn::Equals": [
"",
{
"Fn::Join": [
"",
{
"Ref": "TrustedPrincipals"
}
]
}
]
}
]
}
},
"Resources": {
"FileAssetsBucketEncryptionKey": {
"Type": "AWS::KMS::Key",
"Properties": {
"KeyPolicy": {
"Statement": [
{
"Action": [
"kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*",
"kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*",
"kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", "kms:GenerateDataKey"
],
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
}
},
"Resource": "*"
},
{
"Action": [
"kms:Decrypt", "kms:DescribeKey", "kms:Encrypt",
"kms:ReEncrypt*", "kms:GenerateDataKey*"
],
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Sub": "${PublishingRole.Arn}"
}
},
"Resource": "*"
}
]
}
}
},
"StagingBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
},
"AccessControl": "Private",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": {
"Fn::Sub": "${FileAssetsBucketEncryptionKey.Arn}"
}
}
}]
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
}
},
"UpdateReplacePolicy": "Retain"
},
"ContainerAssetsRepository": {
"Type": "AWS::ECR::Repository",
"Properties": {
"RepositoryName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}"
}
}
},
"PublishingRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Ref": "AWS::AccountId"
}
}
},
{
"Fn::If": [
"HasTrustedPrincipals",
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Ref": "TrustedPrincipals"
}
}
},
{
"Ref": "AWS::NoValue"
}
]
}
]
},
"RoleName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
},
"PublishingRoleDefaultPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*", "s3:GetBucket*", "s3:List*",
"s3:DeleteObject*", "s3:PutObject*", "s3:Abort*"
],
"Resource": [
{
"Fn::Sub": "${StagingBucket.Arn}"
},
{
"Fn::Sub": "${StagingBucket.Arn}/*"
}
]
},
{
"Action": [
"kms:Decrypt", "kms:DescribeKey", "kms:Encrypt",
"kms:ReEncrypt*", "kms:GenerateDataKey*"
],
"Effect": "Allow",
"Resource": {
"Fn::Sub": "${FileAssetsBucketEncryptionKey.Arn}"
}
},
{
"Action": [
"ecr:PutImage", "ecr:InitiateLayerUpload",
"ecr:UploadLayerPart", "ecr:CompleteLayerUpload"
],
"Resource": {
"Fn::Sub": "${ContainerAssetsRepository.Arn}"
}
}
],
"Version": "2012-10-17"
},
"Roles": [{
"Ref": "PublishingRole"
}],
"PolicyName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}"
}
}
},
"DeploymentActionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Ref": "AWS::AccountId"
}
}
},
{
"Fn::If": [
"HasTrustedPrincipals",
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Ref": "TrustedPrincipals"
}
}
},
{
"Ref": "AWS::NoValue"
}
]
}
]
},
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"cloudformation:CreateChangeSet", "cloudformation:DeleteChangeSet",
"cloudformation:DescribeChangeSet", "cloudformation:DescribeStacks",
"cloudformation:ExecuteChangeSet",
"s3:GetObject*", "s3:GetBucket*",
"s3:List*", "s3:Abort*",
"s3:DeleteObject*", "s3:PutObject*",
"kms:Decrypt", "kms:DescribeKey"
],
"Resource": "*"
},
{
"Action": "iam:PassRole",
"Resource": {
"Fn::Sub": "${CloudFormationExecutionRole.Arn}"
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "default"
}
],
"RoleName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-deployment-action-role-${AWS::AccountId}-${AWS::Region}"
},
"Condition": "HasTrustedPrincipals"
}
},
"CloudFormationExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "cloudformation.amazonaws.com"
}
}
]
},
"ManagedPolicyArns": {
"Ref": "CloudFormationExecutionPolicies"
},
"RoleName": {
"Fn::Sub": "cdk-bootstrap-hnb659fds-cloudformation-execution-role-${AWS::AccountId}-${AWS::Region}"
},
"Condition": "HasTrustedPrincipals"
}
}
},
"Outputs": {
"BucketName": {
"Description": "The name of the S3 bucket owned by the CDK toolkit stack",
"Value": { "Fn::Sub": "${StagingBucket.Arn}" },
"Export": {
"Name": { "Fn::Sub": "${AWS::StackName}:BucketName" }
}
},
"BucketDomainName": {
"Description": "The domain name of the S3 bucket owned by the CDK toolkit stack",
"Value": { "Fn::Sub": "${StagingBucket.RegionalDomainName}" },
"Export": {
"Name": { "Fn::Sub": "${AWS::StackName}:BucketDomainName" }
}
},
"BootstrapVersion": {
"Description": "The version of the bootstrap resources that are currently mastered in this stack",
"Value": "1",
"Export": {
"Name": { "Fn::Sub": "AwsCdkBootstrapVersion" }
}
}
}
}
```
The bootstrap template used by the CLI command can be found in the
[aws-cdk package](../packages/aws-cdk/lib/api/bootstrap/bootstrap-template.json).
16 changes: 12 additions & 4 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as path from 'path';
import * as yargs from 'yargs';

import { bootstrapEnvironment, BootstrapEnvironmentProps, SDK } from '../lib';
import { bootstrapEnvironment2 } from '../lib/api/bootstrap/bootstrap-environment2';
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 @@ -56,6 +57,8 @@ async function parseCommandLineArguments() {
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined })
.option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] })
.option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true})
.option('trust', { type: 'array', desc: 'The (space-separated) list of AWS account IDs that should be trusted to perform deployments into this environment', default: [], hidden: true })
.option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true })
)
.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: [] })
Expand Down Expand Up @@ -201,11 +204,13 @@ async function initCommandLine() {
});

case 'bootstrap':
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, {
return await cliBootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, !!process.env.CDK_NEW_BOOTSTRAP, {
bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']),
kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']),
tags: configuration.settings.get(['tags']),
execute: args.execute
execute: args.execute,
trustedAccounts: args.trust,
cloudFormationExecutionPolicies: args.cloudformationExecutionPolicies,
});

case 'deploy':
Expand Down Expand Up @@ -266,7 +271,8 @@ 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, props: BootstrapEnvironmentProps): Promise<void> {
async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined,
useNewBootstrapping: boolean, 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 @@ -279,7 +285,9 @@ async function initCommandLine() {
await Promise.all(environments.map(async (environment) => {
success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name));
try {
const result = await bootstrapEnvironment(environment, aws, toolkitStackName, roleArn, props);
const result = useNewBootstrapping
? await bootstrapEnvironment2(environment, aws, toolkitStackName, roleArn, props)
: 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
Loading

0 comments on commit de48222

Please sign in to comment.