Skip to content

Commit d8bc0d0

Browse files
authored
feat(codepipeline): add support for CloudFormation StackSet actions (#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*
1 parent 0e19f12 commit d8bc0d0

18 files changed

+2248
-177
lines changed

packages/@aws-cdk/aws-codepipeline-actions/README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ directly from a CodeCommit repository, with a manual approval step in between to
607607
See [the AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline.html)
608608
for more details about using CloudFormation in CodePipeline.
609609

610-
#### Actions defined by this package
610+
#### Actions for updating individual CloudFormation Stacks
611611

612612
This package contains the following CloudFormation actions:
613613

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

623+
#### Actions for deploying CloudFormation StackSets to multiple accounts
624+
625+
You can use CloudFormation StackSets to deploy the same CloudFormation template to multiple
626+
accounts in a managed way. If you use AWS Organizations, StackSets can be deployed to
627+
all accounts in a particular Organizational Unit (OU), and even automatically to new
628+
accounts as soon as they are added to a particular OU. For more information, see
629+
the [Working with StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html)
630+
section of the CloudFormation developer guide.
631+
632+
The actions available for updating StackSets are:
633+
634+
* **CloudFormationDeployStackSetAction** - Create or update a CloudFormation StackSet directly from the pipeline, optionally
635+
immediately create and update Stack Instances as well.
636+
* **CloudFormationDeployStackInstancesAction** - Update outdated Stack Instaces using the current version of the StackSet.
637+
638+
Here's an example of using both of these actions:
639+
640+
```ts
641+
declare const pipeline: codepipeline.Pipeline;
642+
declare const sourceOutput: codepipeline.Artifact;
643+
644+
pipeline.addStage({
645+
stageName: 'DeployStackSets',
646+
actions: [
647+
// First, update the StackSet itself with the newest template
648+
new codepipeline_actions.CloudFormationDeployStackSetAction({
649+
actionName: 'UpdateStackSet',
650+
runOrder: 1,
651+
stackSetName: 'MyStackSet',
652+
template: codepipeline_actions.StackSetTemplate.fromArtifactPath(sourceOutput.atPath('template.yaml')),
653+
654+
// Change this to 'StackSetDeploymentModel.organizations()' if you want to deploy to OUs
655+
deploymentModel: codepipeline_actions.StackSetDeploymentModel.selfManaged(),
656+
// This deploys to a set of accounts
657+
stackInstances: codepipeline_actions.StackInstances.inAccounts(['111111111111'], ['us-east-1', 'eu-west-1']),
658+
}),
659+
660+
// Afterwards, update/create additional instances in other accounts
661+
new codepipeline_actions.CloudFormationDeployStackInstancesAction({
662+
actionName: 'AddMoreInstances',
663+
runOrder: 2,
664+
stackSetName: 'MyStackSet',
665+
stackInstances: codepipeline_actions.StackInstances.inAccounts(
666+
['222222222222', '333333333333'],
667+
['us-east-1', 'eu-west-1']
668+
),
669+
}),
670+
],
671+
});
672+
```
673+
623674
#### Lambda deployed through CodePipeline
624675

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

794845
When deploying across accounts, especially in a CDK Pipelines self-mutating pipeline,
795-
it is recommended to provide the `role` property to the `EcsDeployAction`.
846+
it is recommended to provide the `role` property to the `EcsDeployAction`.
796847
The Role will need to have permissions assigned to it for ECS deployment.
797848
See [the CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services)
798849
for the permissions needed.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './pipeline-actions';
2+
export * from './stackset-action';
3+
export * from './stackinstances-action';
4+
export * from './stackset-types';

packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts

Lines changed: 1 addition & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline';
33
import * as iam from '@aws-cdk/aws-iam';
44
import * as cdk from '@aws-cdk/core';
55
import { Action } from '../action';
6+
import { parseCapabilities, SingletonPolicy } from './private/singleton-policy';
67

78
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
89
// eslint-disable-next-line no-duplicate-imports, import/order
@@ -512,148 +513,3 @@ export class CloudFormationDeleteStackAction extends CloudFormationDeployAction
512513
};
513514
}
514515
}
515-
516-
/**
517-
* Manages a bunch of singleton-y statements on the policy of an IAM Role.
518-
* Dedicated methods can be used to add specific permissions to the role policy
519-
* using as few statements as possible (adding resources to existing compatible
520-
* statements instead of adding new statements whenever possible).
521-
*
522-
* Statements created outside of this class are not considered when adding new
523-
* permissions.
524-
*/
525-
class SingletonPolicy extends Construct implements iam.IGrantable {
526-
/**
527-
* Obtain a SingletonPolicy for a given role.
528-
* @param role the Role this policy is bound to.
529-
* @returns the SingletonPolicy for this role.
530-
*/
531-
public static forRole(role: iam.IRole): SingletonPolicy {
532-
const found = role.node.tryFindChild(SingletonPolicy.UUID);
533-
return (found as SingletonPolicy) || new SingletonPolicy(role);
534-
}
535-
536-
private static readonly UUID = '8389e75f-0810-4838-bf64-d6f85a95cf83';
537-
538-
public readonly grantPrincipal: iam.IPrincipal;
539-
540-
private statements: { [key: string]: iam.PolicyStatement } = {};
541-
542-
private constructor(private readonly role: iam.IRole) {
543-
super(role as unknown as cdk.Construct, SingletonPolicy.UUID);
544-
this.grantPrincipal = role;
545-
}
546-
547-
public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
548-
this.statementFor({
549-
actions: [
550-
'cloudformation:DescribeStacks',
551-
'cloudformation:DescribeChangeSet',
552-
'cloudformation:ExecuteChangeSet',
553-
],
554-
conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } },
555-
}).addResources(this.stackArnFromProps(props));
556-
}
557-
558-
public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void {
559-
this.statementFor({
560-
actions: [
561-
'cloudformation:CreateChangeSet',
562-
'cloudformation:DeleteChangeSet',
563-
'cloudformation:DescribeChangeSet',
564-
'cloudformation:DescribeStacks',
565-
],
566-
conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } },
567-
}).addResources(this.stackArnFromProps(props));
568-
}
569-
570-
public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void {
571-
const actions = [
572-
'cloudformation:DescribeStack*',
573-
'cloudformation:CreateStack',
574-
'cloudformation:UpdateStack',
575-
'cloudformation:GetTemplate*',
576-
'cloudformation:ValidateTemplate',
577-
'cloudformation:GetStackPolicy',
578-
'cloudformation:SetStackPolicy',
579-
];
580-
if (props.replaceOnFailure) {
581-
actions.push('cloudformation:DeleteStack');
582-
}
583-
this.statementFor({ actions }).addResources(this.stackArnFromProps(props));
584-
}
585-
586-
public grantDeleteStack(props: { stackName: string, region?: string }): void {
587-
this.statementFor({
588-
actions: [
589-
'cloudformation:DescribeStack*',
590-
'cloudformation:DeleteStack',
591-
],
592-
}).addResources(this.stackArnFromProps(props));
593-
}
594-
595-
public grantPassRole(role: iam.IRole): void {
596-
this.statementFor({ actions: ['iam:PassRole'] }).addResources(role.roleArn);
597-
}
598-
599-
private statementFor(template: StatementTemplate): iam.PolicyStatement {
600-
const key = keyFor(template);
601-
if (!(key in this.statements)) {
602-
this.statements[key] = new iam.PolicyStatement({ actions: template.actions });
603-
if (template.conditions) {
604-
this.statements[key].addConditions(template.conditions);
605-
}
606-
this.role.addToPolicy(this.statements[key]);
607-
}
608-
return this.statements[key];
609-
610-
function keyFor(props: StatementTemplate): string {
611-
const actions = `${props.actions.sort().join('\x1F')}`;
612-
const conditions = formatConditions(props.conditions);
613-
return `${actions}\x1D${conditions}`;
614-
615-
function formatConditions(cond?: StatementCondition): string {
616-
if (cond == null) { return ''; }
617-
let result = '';
618-
for (const op of Object.keys(cond).sort()) {
619-
result += `${op}\x1E`;
620-
const condition = cond[op];
621-
for (const attribute of Object.keys(condition).sort()) {
622-
const value = condition[attribute];
623-
result += `${value}\x1F`;
624-
}
625-
}
626-
return result;
627-
}
628-
}
629-
}
630-
631-
private stackArnFromProps(props: { stackName: string, region?: string }): string {
632-
return cdk.Stack.of(this).formatArn({
633-
region: props.region,
634-
service: 'cloudformation',
635-
resource: 'stack',
636-
resourceName: `${props.stackName}/*`,
637-
});
638-
}
639-
}
640-
641-
interface StatementTemplate {
642-
actions: string[];
643-
conditions?: StatementCondition;
644-
}
645-
646-
type StatementCondition = { [op: string]: { [attribute: string]: string } };
647-
648-
function parseCapabilities(capabilities: cdk.CfnCapabilities[] | undefined): string | undefined {
649-
if (capabilities === undefined) {
650-
return undefined;
651-
} else if (capabilities.length === 1) {
652-
const capability = capabilities.toString();
653-
return (capability === '') ? undefined : capability;
654-
} else if (capabilities.length > 1) {
655-
return capabilities.join(',');
656-
}
657-
658-
return undefined;
659-
}

0 commit comments

Comments
 (0)