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(codepipeline): add support for CloudFormation StackSet actions #14225

Merged
merged 33 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f607d47
feat(codepipelineactions): support for CloudFormationStackSet actions
Apr 16, 2021
613ed00
move StackSet tests to new file
Apr 21, 2021
78bb952
update comments
Apr 21, 2021
4094bf1
remove de-dupe
Apr 21, 2021
2625369
move CloudFormationStackSetAction into cfn section - not sure if it s…
Apr 21, 2021
481d207
move parameters (private) below bound (protected)
Apr 21, 2021
543aff7
add StackSetParameters, fix comments, update tests, remove union types
Apr 21, 2021
6a9af1e
Merge remote-tracking branch 'origin/master' into codepipeline_stacks…
Apr 21, 2021
8e0161b
arrtype[] instead of Array<arrtype>
Apr 21, 2021
938ee11
Merge remote-tracking branch 'origin/master' into pr/ndchelsea/14225
rix0rrr Feb 3, 2022
8bdd605
Add more modeling of parameters, add instances action
rix0rrr Feb 4, 2022
4c2906c
Update README
rix0rrr Feb 7, 2022
9b62b01
Fix unit test
rix0rrr Feb 7, 2022
4adf9c5
Remove `| undefined`
rix0rrr Feb 7, 2022
95babaa
Remove unused interfaces
rix0rrr Feb 7, 2022
d740d5d
Fix README examples
rix0rrr Feb 7, 2022
f5cdc96
Review comments
rix0rrr Feb 8, 2022
240f0f2
Change some more docs
rix0rrr Feb 8, 2022
69716f6
Update tests with renames
rix0rrr Feb 8, 2022
271964b
Update integ snapshot
rix0rrr Feb 8, 2022
ccd96e4
Move singleton
rix0rrr Feb 11, 2022
c647940
Rename actions
rix0rrr Feb 11, 2022
331d03b
inAccounts/inOrganizationalUnits
rix0rrr Feb 11, 2022
237fa9f
@internal
rix0rrr Feb 11, 2022
3c5c94c
region => stackSetRegion
rix0rrr Feb 11, 2022
8fd68f0
Common interface
rix0rrr Feb 11, 2022
d647664
Clarify integ test
rix0rrr Feb 11, 2022
9ac8358
Move an enum
rix0rrr Feb 11, 2022
ef99e65
Add index
rix0rrr Feb 11, 2022
b54e867
Add example
rix0rrr Feb 11, 2022
17e538c
Validate
rix0rrr Feb 11, 2022
551db25
Rename props
rix0rrr Feb 15, 2022
c64f039
Merge branch 'master' into codepipeline_stacksetaction
mergify[bot] Feb 15, 2022
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
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