Skip to content

Commit

Permalink
feat(codepipeline): add support for CloudFormation StackSet actions (a…
Browse files Browse the repository at this point in the history
…ws#14225)

Adds support for CloudFormationStackSet CodePipeline actions.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
ndchelsea authored and TikiTDO committed Feb 21, 2022
1 parent 4e8b73b commit 394bcf0
Show file tree
Hide file tree
Showing 18 changed files with 2,248 additions and 177 deletions.
55 changes: 53 additions & 2 deletions packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ directly from a CodeCommit repository, with a manual approval step in between to
See [the AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline.html)
for more details about using CloudFormation in CodePipeline.

#### Actions defined by this package
#### Actions for updating individual CloudFormation Stacks

This package contains the following CloudFormation actions:

Expand All @@ -620,6 +620,57 @@ This package contains the following CloudFormation actions:
changes from the people (or system) applying the changes.
* **CloudFormationExecuteChangeSetAction** - Execute a change set prepared previously.

#### Actions for deploying CloudFormation StackSets to multiple accounts

You can use CloudFormation StackSets to deploy the same CloudFormation template to multiple
accounts in a managed way. If you use AWS Organizations, StackSets can be deployed to
all accounts in a particular Organizational Unit (OU), and even automatically to new
accounts as soon as they are added to a particular OU. For more information, see
the [Working with StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html)
section of the CloudFormation developer guide.

The actions available for updating StackSets are:

* **CloudFormationDeployStackSetAction** - Create or update a CloudFormation StackSet directly from the pipeline, optionally
immediately create and update Stack Instances as well.
* **CloudFormationDeployStackInstancesAction** - Update outdated Stack Instaces using the current version of the StackSet.

Here's an example of using both of these actions:

```ts
declare const pipeline: codepipeline.Pipeline;
declare const sourceOutput: codepipeline.Artifact;

pipeline.addStage({
stageName: 'DeployStackSets',
actions: [
// First, update the StackSet itself with the newest template
new codepipeline_actions.CloudFormationDeployStackSetAction({
actionName: 'UpdateStackSet',
runOrder: 1,
stackSetName: 'MyStackSet',
template: codepipeline_actions.StackSetTemplate.fromArtifactPath(sourceOutput.atPath('template.yaml')),

// Change this to 'StackSetDeploymentModel.organizations()' if you want to deploy to OUs
deploymentModel: codepipeline_actions.StackSetDeploymentModel.selfManaged(),
// This deploys to a set of accounts
stackInstances: codepipeline_actions.StackInstances.inAccounts(['111111111111'], ['us-east-1', 'eu-west-1']),
}),

// Afterwards, update/create additional instances in other accounts
new codepipeline_actions.CloudFormationDeployStackInstancesAction({
actionName: 'AddMoreInstances',
runOrder: 2,
stackSetName: 'MyStackSet',
stackInstances: codepipeline_actions.StackInstances.inAccounts(
['222222222222', '333333333333'],
['us-east-1', 'eu-west-1']
),
}),
],
});
```

#### Lambda deployed through CodePipeline

If you want to deploy your Lambda through CodePipeline,
Expand Down Expand Up @@ -792,7 +843,7 @@ const deployStage = pipeline.addStage({
```

When deploying across accounts, especially in a CDK Pipelines self-mutating pipeline,
it is recommended to provide the `role` property to the `EcsDeployAction`.
it is recommended to provide the `role` property to the `EcsDeployAction`.
The Role will need to have permissions assigned to it for ECS deployment.
See [the CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services)
for the permissions needed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './pipeline-actions';
export * from './stackset-action';
export * from './stackinstances-action';
export * from './stackset-types';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import { Action } from '../action';
import { parseCapabilities, SingletonPolicy } from './private/singleton-policy';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
Expand Down Expand Up @@ -512,148 +513,3 @@ export class CloudFormationDeleteStackAction extends CloudFormationDeployAction
};
}
}

/**
* 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
* using as few statements as possible (adding resources to existing compatible
* statements instead of adding new statements whenever possible).
*
* Statements created outside of this class are not considered when adding new
* permissions.
*/
class SingletonPolicy extends Construct implements iam.IGrantable {
/**
* Obtain a SingletonPolicy for a given role.
* @param role the Role this policy is bound to.
* @returns the SingletonPolicy for this role.
*/
public static forRole(role: iam.IRole): SingletonPolicy {
const found = role.node.tryFindChild(SingletonPolicy.UUID);
return (found as SingletonPolicy) || new SingletonPolicy(role);
}

private static readonly UUID = '8389e75f-0810-4838-bf64-d6f85a95cf83';

public readonly grantPrincipal: iam.IPrincipal;

private statements: { [key: string]: iam.PolicyStatement } = {};

private constructor(private readonly role: iam.IRole) {
super(role as unknown as cdk.Construct, SingletonPolicy.UUID);
this.grantPrincipal = role;
}

public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
this.statementFor({
actions: [
'cloudformation:DescribeStacks',
'cloudformation:DescribeChangeSet',
'cloudformation:ExecuteChangeSet',
],
conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } },
}).addResources(this.stackArnFromProps(props));
}

public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
this.statementFor({
actions: [
'cloudformation:CreateChangeSet',
'cloudformation:DeleteChangeSet',
'cloudformation:DescribeChangeSet',
'cloudformation:DescribeStacks',
],
conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } },
}).addResources(this.stackArnFromProps(props));
}

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 }).addResources(this.stackArnFromProps(props));
}

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

public grantPassRole(role: iam.IRole): void {
this.statementFor({ actions: ['iam:PassRole'] }).addResources(role.roleArn);
}

private statementFor(template: StatementTemplate): iam.PolicyStatement {
const key = keyFor(template);
if (!(key in this.statements)) {
this.statements[key] = new iam.PolicyStatement({ actions: template.actions });
if (template.conditions) {
this.statements[key].addConditions(template.conditions);
}
this.role.addToPolicy(this.statements[key]);
}
return this.statements[key];

function keyFor(props: StatementTemplate): string {
const actions = `${props.actions.sort().join('\x1F')}`;
const conditions = formatConditions(props.conditions);
return `${actions}\x1D${conditions}`;

function formatConditions(cond?: StatementCondition): string {
if (cond == null) { return ''; }
let result = '';
for (const op of Object.keys(cond).sort()) {
result += `${op}\x1E`;
const condition = cond[op];
for (const attribute of Object.keys(condition).sort()) {
const value = condition[attribute];
result += `${value}\x1F`;
}
}
return result;
}
}
}

private stackArnFromProps(props: { stackName: string, region?: string }): string {
return cdk.Stack.of(this).formatArn({
region: props.region,
service: 'cloudformation',
resource: 'stack',
resourceName: `${props.stackName}/*`,
});
}
}

interface StatementTemplate {
actions: string[];
conditions?: StatementCondition;
}

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

function parseCapabilities(capabilities: cdk.CfnCapabilities[] | undefined): string | undefined {
if (capabilities === undefined) {
return undefined;
} else if (capabilities.length === 1) {
const capability = capabilities.toString();
return (capability === '') ? undefined : capability;
} else if (capabilities.length > 1) {
return capabilities.join(',');
}

return undefined;
}
Loading

0 comments on commit 394bcf0

Please sign in to comment.