Skip to content

Commit

Permalink
feat(aws-codepipeline, aws-cloudformation): support cross-region Clou…
Browse files Browse the repository at this point in the history
…dFormation pipeline action (#1152)

Allows CloudFormation CodePipeline actions to be executed across regions.

https://aws.amazon.com/about-aws/whats-new/2018/11/aws-codepipeline-now-supports-cross-region-actions
  • Loading branch information
skinny85 authored and Elad Ben-Israel committed Nov 14, 2018
1 parent bdbeb7c commit 8e701ad
Show file tree
Hide file tree
Showing 11 changed files with 707 additions and 34 deletions.
75 changes: 44 additions & 31 deletions packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ export interface PipelineCloudFormationActionProps extends codepipeline.CommonAc
* @default Automatically generated artifact name.
*/
outputArtifactName?: string;

/**
* The AWS region the given Action resides in.
* Note that a cross-region Pipeline requires replication buckets to function correctly.
* You can provide their names with the {@link PipelineProps#crossRegionReplicationBuckets} property.
* If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets,
* that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack.
*
* @default the Action resides in the same region as the Pipeline
*/
region?: string;
}

/**
Expand All @@ -50,6 +61,7 @@ export abstract class PipelineCloudFormationAction extends codepipeline.Action {
super(parent, id, {
stage: props.stage,
runOrder: props.runOrder,
region: props.region,
artifactBounds: {
minInputs: 0,
maxInputs: 10,
Expand Down Expand Up @@ -358,14 +370,6 @@ export enum CloudFormationCapabilities {
NamedIAM = 'CAPABILITY_NAMED_IAM'
}

function stackArnFromName(stackName: string): string {
return cdk.ArnUtils.fromComponents({
service: 'cloudformation',
resource: 'stack',
resourceName: `${stackName}/*`
});
}

/**
* Manages a bunch of singleton-y statements on the policy of an IAM Role.
* Dedicated methods can be used to add specific permissions to the role policy
Expand Down Expand Up @@ -394,23 +398,14 @@ class SingletonPolicy extends cdk.Construct {
super(role, SingletonPolicy.UUID);
}

public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean }): void {
const actions = [
'cloudformation:DescribeStack*',
'cloudformation:CreateStack',
'cloudformation:UpdateStack',
'cloudformation:GetTemplate*',
'cloudformation:ValidateTemplate',
'cloudformation:GetStackPolicy',
'cloudformation:SetStackPolicy',
];
if (props.replaceOnFailure) {
actions.push('cloudformation:DeleteStack');
}
this.statementFor({ actions }).addResource(stackArnFromName(props.stackName));
public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
this.statementFor({
actions: ['cloudformation:ExecuteChangeSet'],
conditions: { StringEquals: { 'cloudformation:ChangeSetName': props.changeSetName } },
}).addResource(stackArnFromProps(props));
}

public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string }): void {
public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
this.statementFor({
actions: [
'cloudformation:CreateChangeSet',
Expand All @@ -419,23 +414,32 @@ class SingletonPolicy extends cdk.Construct {
'cloudformation:DescribeStacks',
],
conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } },
}).addResource(stackArnFromName(props.stackName));
}).addResource(stackArnFromProps(props));
}

public grantExecuteChangeSet(props: { stackName: string, changeSetName: string }): void {
this.statementFor({
actions: ['cloudformation:ExecuteChangeSet'],
conditions: { StringEquals: { 'cloudformation:ChangeSetName': props.changeSetName } },
}).addResource(stackArnFromName(props.stackName));
public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void {
const actions = [
'cloudformation:DescribeStack*',
'cloudformation:CreateStack',
'cloudformation:UpdateStack',
'cloudformation:GetTemplate*',
'cloudformation:ValidateTemplate',
'cloudformation:GetStackPolicy',
'cloudformation:SetStackPolicy',
];
if (props.replaceOnFailure) {
actions.push('cloudformation:DeleteStack');
}
this.statementFor({ actions }).addResource(stackArnFromProps(props));
}

public grantDeleteStack(props: { stackName: string }): void {
public grantDeleteStack(props: { stackName: string, region?: string }): void {
this.statementFor({
actions: [
'cloudformation:DescribeStack*',
'cloudformation:DeleteStack',
]
}).addResource(stackArnFromName(props.stackName));
}).addResource(stackArnFromProps(props));
}

public grantPassRole(role: iam.Role): void {
Expand Down Expand Up @@ -481,3 +485,12 @@ interface StatementTemplate {
}

type StatementCondition = { [op: string]: { [attribute: string]: string } };

function stackArnFromProps(props: { stackName: string, region?: string }): string {
return cdk.ArnUtils.fromComponents({
region: props.region,
service: 'cloudformation',
resource: 'stack',
resourceName: `${props.stackName}/*`
});
}
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export interface CommonActionConstructProps {
export interface ActionProps extends CommonActionProps, CommonActionConstructProps {
category: ActionCategory;
provider: string;

/**
* The region this Action resides in.
*
* @default the Action resides in the same region as the Pipeline
*/
region?: string;

artifactBounds: ActionArtifactBounds;
configuration?: any;
version?: string;
Expand All @@ -178,6 +186,17 @@ export abstract class Action extends cdk.Construct {
*/
public readonly provider: string;

/**
* The AWS region the given Action resides in.
* Note that a cross-region Pipeline requires replication buckets to function correctly.
* You can provide their names with the {@link PipelineProps#crossRegionReplicationBuckets} property.
* If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets,
* that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack.
*
* @default the Action resides in the same region as the Pipeline
*/
public readonly region?: string;

/**
* The action's configuration. These are key-value pairs that specify input values for an action.
* For more information, see the AWS CodePipeline User Guide.
Expand Down Expand Up @@ -211,6 +230,7 @@ export abstract class Action extends cdk.Construct {
this.version = props.version || '1';
this.category = props.category;
this.provider = props.provider;
this.region = props.region;
this.configuration = props.configuration;
this.artifactBounds = props.artifactBounds;
this.runOrder = props.runOrder === undefined ? 1 : props.runOrder;
Expand Down
47 changes: 47 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,53 @@ new codepipeline.JenkinsBuildAction(this, 'Jenkins_Build', {
});
```

### Cross-region CodePipelines

You can also use the cross-region feature to deploy resources
(currently, only CloudFormation Stacks are supported)
into a different region than your Pipeline is in.

It works like this:

```ts
const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', {
// ...
crossRegionReplicationBuckets: {
'us-west-1': 'my-us-west-1-replication-bucket',
},
});

// later in the code...
new cloudformation.PipelineCreateUpdateStackAction(this, 'CFN_US_West_1', {
// ...
region: 'us-west-1',
});
```

This way, the `CFN_US_West_1` Action will operate in the `us-west-1` region,
regardless of which region your Pipeline is in.

If you don't provide a bucket name for a region (other than the Pipeline's region)
that you're using for an Action with the `crossRegionReplicationBuckets` property,
there will be a new Stack, named `aws-cdk-codepipeline-cross-region-scaffolding-<region>`,
defined for you, containing a replication Bucket.
Note that you have to make sure to `cdk deploy` all of these automatically created Stacks
before you can deploy your main Stack (the one containing your Pipeline).
Use the `cdk ls` command to see all of the Stacks comprising your CDK application.
Example:

```bash
$ cdk ls
MyMainStack
aws-cdk-codepipeline-cross-region-scaffolding-us-west-1
$ cdk deploy aws-cdk-codepipeline-cross-region-scaffolding-us-west-1
# output of cdk deploy here...
$ cdk deploy MyMainStack
```

See [the AWS docs here](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-create-cross-region.html)
for more information on cross-region CodePipelines.

### Events

#### Using a pipeline as an event target
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import crypto = require('crypto');

/**
* Construction properties for {@link CrossRegionScaffoldStack}.
*/
export interface CrossRegionScaffoldStackProps {
/**
* The AWS region this Stack resides in.
*/
region: string;

/**
* The AWS account ID this Stack belongs to.
*
* @example '012345678901'
*/
account: string;
}

/**
* A Stack containing resources required for the cross-region CodePipeline functionality to work.
*/
export class CrossRegionScaffoldStack extends cdk.Stack {
/**
* The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region.
*/
public readonly replicationBucketName: string;

constructor(parent?: cdk.App, props: CrossRegionScaffoldStackProps = defaultCrossRegionScaffoldStackProps()) {
super(parent, generateStackName(props), {
env: {
region: props.region,
account: props.account,
},
});

const replicationBucketName = generateUniqueName('cdk-cross-region-codepipeline-replication-bucket-',
props.region, props.account, false, 12);

new s3.Bucket(this, 'CrossRegionCodePipelineReplicationBucket', {
bucketName: replicationBucketName,
});
this.replicationBucketName = replicationBucketName;
}
}

function generateStackName(props: CrossRegionScaffoldStackProps): string {
return `aws-cdk-codepipeline-cross-region-scaffolding-${props.region}`;
}

function generateUniqueName(baseName: string, region: string, account: string,
toUpperCase: boolean, hashPartLen: number = 8): string {
const sha256 = crypto.createHash('sha256')
.update(baseName)
.update(region)
.update(account);

const hash = sha256.digest('hex').slice(0, hashPartLen);

return baseName + (toUpperCase ? hash.toUpperCase() : hash.toLowerCase());
}

// purely to defeat the limitation that a required argument cannot follow an optional one
function defaultCrossRegionScaffoldStackProps(): CrossRegionScaffoldStackProps {
throw new Error('The props argument when creating a CrossRegionScaffoldStack is required');
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './cross-region-scaffold-stack';
export * from './github-source-action';
export * from './manual-approval-action';
export * from './pipeline';
Expand Down
Loading

0 comments on commit 8e701ad

Please sign in to comment.