Skip to content

Commit

Permalink
fix: improve support for GuUserData in EC2 App pattern (#446)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobwinch authored Apr 15, 2021
1 parent a3fb1c0 commit 55bb708
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 9 deletions.
6 changes: 3 additions & 3 deletions src/constructs/autoscaling/user-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -19,7 +19,7 @@ describe("GuUserData", () => {
const stack = simpleGuStackForTesting();
const app = "testing";

const props: GuUserDataProps = {
const props: GuUserDataPropsWithApp = {
app,
distributable: {
bucket: GuDistributionBucketParameter.getInstance(stack),
Expand Down Expand Up @@ -70,7 +70,7 @@ describe("GuUserData", () => {
const stack = simpleGuStackForTesting();
const app = "testing";

const props: GuUserDataProps = {
const props: GuUserDataPropsWithApp = {
app,
distributable: {
bucket: GuDistributionBucketParameter.getInstance(stack),
Expand Down
5 changes: 3 additions & 2 deletions src/constructs/autoscaling/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -65,7 +66,7 @@ export class GuUserData {
});
}

constructor(scope: GuStack, props: GuUserDataProps) {
constructor(scope: GuStack, props: GuUserDataPropsWithApp) {
this._userData = UserData.forLinux();

if (props.configuration) {
Expand Down
59 changes: 59 additions & 0 deletions src/patterns/ec2-app.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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, {
Expand Down
17 changes: 13 additions & 4 deletions src/patterns/ec2-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ 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,
GuApplicationTargetGroup,
} 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;
}
Expand Down Expand Up @@ -50,16 +51,24 @@ 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,
stageDependentProps: {
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 },
});

Expand Down

0 comments on commit 55bb708

Please sign in to comment.