Skip to content

Commit

Permalink
feat: support multiple GuInstanceRoles in a stack (#332)
Browse files Browse the repository at this point in the history
A `GuInstanceRole` provides the basic permissions needed by EC2, it grants access to:
* SSM
* Describing tags
* Writing to kinesis for log shipping

#326 started the work to support the declaration of multiple apps in a stack, where an app is a collection of ASG, LB etc. However, when actually attempting to use the updated constructs in reality, the synth step failed due to multiple constructs using the same ID.

This change follows #341 and #337 and converts `GuSSMRunCommandPolicy` and `GuDescribeEC2Policy` to singletons. This means these policies will only get created once regardless of how many roles use them - there will be one policy with multiple roles attached to it. This helps keep the stack DRY and also reduces the chance of reaching the max file size limits of cloudformation.

The commits have been crafted to be standalone, reviewing them one by one might make the overall review easier.

Requires #341.

BREAKING CHANGE:
* `GuSSMRunCommandPolicy` and `GuDescribeEC2Policy` can no longer be directly instantiated
  • Loading branch information
akash1810 authored Mar 24, 2021
1 parent 8c7a9f3 commit bff9009
Show file tree
Hide file tree
Showing 7 changed files with 451 additions and 61 deletions.
6 changes: 3 additions & 3 deletions src/constructs/iam/policies/describe-ec2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { attachPolicyToTestRole, simpleGuStackForTesting } from "../../../../tes
import { GuDescribeEC2Policy } from "./describe-ec2";

describe("DescribeEC2Policy", () => {
it("can accept a custom policy name", () => {
it("creates the correct policy", () => {
const stack = simpleGuStackForTesting();

const policy = new GuDescribeEC2Policy(stack, "DescribeEC2Policy", { policyName: "my-awesome-policy" });
const policy = GuDescribeEC2Policy.getInstance(stack);

attachPolicyToTestRole(stack, policy);

expect(stack).toHaveResource("AWS::IAM::Policy", {
PolicyName: "my-awesome-policy",
PolicyName: "describe-ec2-policy",
PolicyDocument: {
Version: "2012-10-17",
Statement: [
Expand Down
23 changes: 16 additions & 7 deletions src/constructs/iam/policies/describe-ec2.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { PolicyProps } from "@aws-cdk/aws-iam";
import { Effect, PolicyStatement } from "@aws-cdk/aws-iam";
import type { GuStack } from "../../core";
import type { GuPolicyProps } from "./base-policy";
import { GuPolicy } from "./base-policy";

export class GuDescribeEC2Policy extends GuPolicy {
private static getDefaultProps(): PolicyProps {
return {
private static instance: GuPolicy | undefined;

// eslint-disable-next-line custom-rules/valid-constructors -- TODO be better
private constructor(scope: GuStack) {
super(scope, "DescribeEC2Policy", {
policyName: "describe-ec2-policy",
statements: [
new PolicyStatement({
Expand All @@ -20,10 +21,18 @@ export class GuDescribeEC2Policy extends GuPolicy {
resources: ["*"],
}),
],
};
});
}

constructor(scope: GuStack, id: string = "DescribeEC2Policy", props?: GuPolicyProps) {
super(scope, id, { ...GuDescribeEC2Policy.getDefaultProps(), ...props });
public static getInstance(stack: GuStack): GuDescribeEC2Policy {
// Resources can only live in the same App so return a new instance where necessary.
// See https://github.com/aws/aws-cdk/blob/0ea4b19afd639541e5f1d7c1783032ee480c307e/packages/%40aws-cdk/core/lib/private/refs.ts#L47-L50
const isSameStack = this.instance?.node.root === stack.node.root;

if (!this.instance || !isSameStack) {
this.instance = new GuDescribeEC2Policy(stack);
}

return this.instance;
}
}
41 changes: 1 addition & 40 deletions src/constructs/iam/policies/ssm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("The GuSSMRunCommandPolicy class", () => {
it("sets default props", () => {
const stack = simpleGuStackForTesting();

const ssmPolicy = new GuSSMRunCommandPolicy(stack);
const ssmPolicy = GuSSMRunCommandPolicy.getInstance(stack);

attachPolicyToTestRole(stack, ssmPolicy);

Expand Down Expand Up @@ -39,43 +39,4 @@ describe("The GuSSMRunCommandPolicy class", () => {
},
});
});

it("merges defaults and passed in props", () => {
const stack = simpleGuStackForTesting();

const ssmPolicy = new GuSSMRunCommandPolicy(stack, "SSMRunCommandPolicy", {
policyName: "test",
});

attachPolicyToTestRole(stack, ssmPolicy);

expect(stack).toHaveResource("AWS::IAM::Policy", {
PolicyName: "test",
PolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Resource: "*",
Action: [
"ec2messages:AcknowledgeMessage",
"ec2messages:DeleteMessage",
"ec2messages:FailMessage",
"ec2messages:GetEndpoint",
"ec2messages:GetMessages",
"ec2messages:SendReply",
"ssm:UpdateInstanceInformation",
"ssm:ListInstanceAssociations",
"ssm:DescribeInstanceProperties",
"ssm:DescribeDocumentParameters",
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
],
},
],
},
});
});
});
21 changes: 17 additions & 4 deletions src/constructs/iam/policies/ssm.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { GuStack } from "../../core";
import type { GuNoStatementsPolicyProps } from "./base-policy";
import { GuAllowPolicy } from "./base-policy";

export class GuSSMRunCommandPolicy extends GuAllowPolicy {
constructor(scope: GuStack, id: string = "SSMRunCommandPolicy", props?: GuNoStatementsPolicyProps) {
super(scope, id, {
private static instance: GuSSMRunCommandPolicy | undefined;

// eslint-disable-next-line custom-rules/valid-constructors -- WIP
private constructor(scope: GuStack) {
super(scope, "SSMRunCommandPolicy", {
policyName: "ssm-run-command-policy",
resources: ["*"],
actions: [
Expand All @@ -23,7 +25,18 @@ export class GuSSMRunCommandPolicy extends GuAllowPolicy {
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
],
...props,
});
}

public static getInstance(stack: GuStack): GuSSMRunCommandPolicy {
// Resources can only live in the same App so return a new `GuSSMRunCommandPolicy` where necessary.
// See https://github.com/aws/aws-cdk/blob/0ea4b19afd639541e5f1d7c1783032ee480c307e/packages/%40aws-cdk/core/lib/private/refs.ts#L47-L50
const isSameStack = this.instance?.node.root === stack.node.root;

if (!this.instance || !isSameStack) {
this.instance = new GuSSMRunCommandPolicy(stack);
}

return this.instance;
}
}
Loading

0 comments on commit bff9009

Please sign in to comment.