Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-codepipeline, aws-cloudformation): support cross-region Pipelines for CFN Actions #1152

Merged
merged 1 commit into from
Nov 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -153,6 +153,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
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
*/
region?: string;

artifactBounds: ActionArtifactBounds;
configuration?: any;
version?: string;
Expand All @@ -177,6 +185,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;
skinny85 marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 @@ -210,6 +229,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

skinny85 marked this conversation as resolved.
Show resolved Hide resolved
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.

skinny85 marked this conversation as resolved.
Show resolved Hide resolved
### 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;
skinny85 marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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', {
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why make the parent of CrossRegionScaffoldStack optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because parent is optional for Stack.

Copy link
Contributor

@eladb eladb Nov 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's required for CrossRegionScaffoldStack, so there is no problem making it a required argument.

Please follow up with a fix in a subsequent PR. I don't think it's blocking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be optional in CrossRegionScaffoldStack, because the parent is taken from the Stack the Pipeline itself is in. If parent is optional there, it needs to be optional here as well. See code: https://github.com/awslabs/aws-cdk/blob/master/packages/%40aws-cdk/aws-codepipeline/lib/pipeline.ts#L308

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