From 55bb7086159f9579b6d29d01eb86ae57d2bb54c6 Mon Sep 17 00:00:00 2001 From: Jacob Winch Date: Thu, 15 Apr 2021 11:53:10 +0100 Subject: [PATCH] fix: improve support for GuUserData in EC2 App pattern (#446) --- src/constructs/autoscaling/user-data.test.ts | 6 +- src/constructs/autoscaling/user-data.ts | 5 +- src/patterns/ec2-app.test.ts | 59 ++++++++++++++++++++ src/patterns/ec2-app.ts | 17 ++++-- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/constructs/autoscaling/user-data.test.ts b/src/constructs/autoscaling/user-data.test.ts index 76a86526e9..4614928a19 100644 --- a/src/constructs/autoscaling/user-data.test.ts +++ b/src/constructs/autoscaling/user-data.test.ts @@ -5,7 +5,7 @@ import { Stage } from "../../constants"; import { simpleGuStackForTesting } from "../../utils/test"; import { GuDistributionBucketParameter, GuPrivateConfigBucketParameter } from "../core"; import { GuAutoScalingGroup } from "./asg"; -import type { GuUserDataProps } from "./user-data"; +import type { GuUserDataPropsWithApp } from "./user-data"; import { GuUserData } from "./user-data"; describe("GuUserData", () => { @@ -19,7 +19,7 @@ describe("GuUserData", () => { const stack = simpleGuStackForTesting(); const app = "testing"; - const props: GuUserDataProps = { + const props: GuUserDataPropsWithApp = { app, distributable: { bucket: GuDistributionBucketParameter.getInstance(stack), @@ -70,7 +70,7 @@ describe("GuUserData", () => { const stack = simpleGuStackForTesting(); const app = "testing"; - const props: GuUserDataProps = { + const props: GuUserDataPropsWithApp = { app, distributable: { bucket: GuDistributionBucketParameter.getInstance(stack), diff --git a/src/constructs/autoscaling/user-data.ts b/src/constructs/autoscaling/user-data.ts index 31a224046a..c59dbbf756 100644 --- a/src/constructs/autoscaling/user-data.ts +++ b/src/constructs/autoscaling/user-data.ts @@ -16,7 +16,8 @@ export interface GuUserDataS3DistributableProps { executionStatement: string; // TODO can we detect this and auto generate it? Maybe from the file extension? } -export interface GuUserDataProps extends AppIdentity { +export type GuUserDataPropsWithApp = GuUserDataProps & AppIdentity; +export interface GuUserDataProps { distributable: GuUserDataS3DistributableProps; configuration?: GuPrivateS3ConfigurationProps; } @@ -65,7 +66,7 @@ export class GuUserData { }); } - constructor(scope: GuStack, props: GuUserDataProps) { + constructor(scope: GuStack, props: GuUserDataPropsWithApp) { this._userData = UserData.forLinux(); if (props.configuration) { diff --git a/src/patterns/ec2-app.test.ts b/src/patterns/ec2-app.test.ts index 4446181a9d..3b4f0a353f 100644 --- a/src/patterns/ec2-app.test.ts +++ b/src/patterns/ec2-app.test.ts @@ -1,6 +1,7 @@ import "@aws-cdk/assert/jest"; import { SynthUtils } from "@aws-cdk/assert"; import { TrackingTag } from "../constants/library-info"; +import { GuDistributionBucketParameter, GuPrivateConfigBucketParameter } from "../constructs/core"; import { alphabeticalTags, simpleGuStackForTesting } from "../utils/test"; import { GuApplicationPorts, GuEc2App, GuNodeApp, GuPlayApp } from "./ec2-app"; @@ -16,6 +17,64 @@ describe("the GuEC2App pattern", function () { expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); + it("adds the correct permissions for apps which need to fetch private config from s3", function () { + const stack = simpleGuStackForTesting(); + const app = "test-gu-ec2-app"; + new GuEc2App(stack, { + applicationPort: GuApplicationPorts.Node, + app: app, + publicFacing: false, + userData: { + distributable: { + bucket: GuDistributionBucketParameter.getInstance(stack), + fileName: "my-app.deb", + executionStatement: `dpkg -i /${app}/my-app.deb`, + }, + configuration: { + bucket: new GuPrivateConfigBucketParameter(stack), + files: ["secrets.json", "application.conf"], + }, + }, + }); + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: "s3:GetObject", + Resource: [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "PrivateConfigBucketName", + }, + "/secrets.json", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "PrivateConfigBucketName", + }, + "/application.conf", + ], + ], + }, + ], + }, + ], + }, + }); + }); + it("can handle multiple EC2 apps in a single stack", function () { const stack = simpleGuStackForTesting(); new GuEc2App(stack, { diff --git a/src/patterns/ec2-app.ts b/src/patterns/ec2-app.ts index b6e15e4c22..0e176eb76c 100644 --- a/src/patterns/ec2-app.ts +++ b/src/patterns/ec2-app.ts @@ -2,12 +2,13 @@ import { HealthCheck } from "@aws-cdk/aws-autoscaling"; import { Certificate } from "@aws-cdk/aws-certificatemanager"; import { ApplicationProtocol, ListenerAction } from "@aws-cdk/aws-elasticloadbalancingv2"; import { Duration } from "@aws-cdk/core"; +import type { GuUserDataProps } from "../constructs/autoscaling"; import { GuAutoScalingGroup, GuUserData } from "../constructs/autoscaling"; import type { GuStack } from "../constructs/core"; import { GuArnParameter } from "../constructs/core"; import { AppIdentity } from "../constructs/core/identity"; import { GuVpc, SubnetType } from "../constructs/ec2"; -import { GuInstanceRole } from "../constructs/iam"; +import { GuGetPrivateConfigPolicy, GuInstanceRole } from "../constructs/iam"; import { GuApplicationListener, GuApplicationLoadBalancer, @@ -15,7 +16,7 @@ import { } from "../constructs/loadbalancing"; interface GuEc2AppProps extends AppIdentity { - userData: GuUserData | string; + userData: GuUserDataProps | string; publicFacing: boolean; // could also name it `internetFacing` to match GuApplicationLoadBalancer applicationPort: number; } @@ -50,6 +51,11 @@ export class GuEc2App { certificateArn.valueAsString ); + const maybePrivateConfigPolicy = + typeof props.userData !== "string" && props.userData.configuration + ? [new GuGetPrivateConfigPolicy(scope, "GetPrivateConfigFromS3Policy", props.userData.configuration)] + : []; + const asg = new GuAutoScalingGroup(scope, "AutoScalingGroup", { app, vpc, @@ -57,9 +63,12 @@ export class GuEc2App { CODE: { minimumInstances: 1 }, PROD: { minimumInstances: 3 }, }, - role: new GuInstanceRole(scope, { app: props.app }), + role: new GuInstanceRole(scope, { app: props.app, additionalPolicies: maybePrivateConfigPolicy }), healthCheck: HealthCheck.elb({ grace: Duration.minutes(2) }), // should this be defaulted at pattern or construct level? - userData: props.userData instanceof GuUserData ? props.userData.userData : props.userData, + userData: + typeof props.userData !== "string" + ? new GuUserData(scope, { app, ...props.userData }).userData + : props.userData, vpcSubnets: { subnets: privateSubnets }, });