Skip to content

Commit

Permalink
feat(ec2): Require explicit UserData type instead of string in Pa…
Browse files Browse the repository at this point in the history
…ttern props.
  • Loading branch information
AshCorr committed Jul 22, 2024
1 parent d424632 commit e15d900
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 50 deletions.
32 changes: 32 additions & 0 deletions .changeset/thick-owls-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@guardian/cdk": major
---

GuCDK EC2 patterns now require an explicit `UserData` or `GuUserDataProps` input, instead of a string.

The UserData class comes with helpers that allow us to mutate the user data in our patterns which will be helpful with some of our upcoming work.
Unfortunately whenever a `string` is passed to our patterns we have to wrap it in a special `CustomUserData` class which disables most of these helpers.

For applications that were already using `GuUserDataProps` no change is required, however applications that used strings will have to make a small change.

```js
new GuEc2App({
userData: `#!/usr/bin/bash echo "hello world"`,
...
})
```

becomes

```js
const userData = UserData.forLinux();
userData.addCommands(`echo "hello world"`);

new GuEc2App({
userData,
...
})
```

Note that you no longer need to specify a shebang, by default `UserData` adds one for you. If you need to customize this behaviour you can look at the props accepted by `forLinux`.
You may also want to look at some of the other methods that UserData has to understand if it may be able to help you in other ways, for example `addS3DownloadCommand` the method helps you write commands to download from S3.
2 changes: 2 additions & 0 deletions src/constructs/autoscaling/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface GuUserDataProps {
*/
export class GuUserData {
private readonly _userData: UserData;
readonly configuration?: GuPrivateS3ConfigurationProps;

get userData(): UserData {
return this._userData;
Expand Down Expand Up @@ -64,6 +65,7 @@ export class GuUserData {

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

if (props.configuration) {
this.downloadConfiguration(scope, props.app, props.configuration);
Expand Down
4 changes: 2 additions & 2 deletions src/patterns/ec2-app/__snapshots__/base.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ exports[`the GuEC2App pattern can produce a restricted EC2 app locked to specifi
},
],
"UserData": {
"Fn::Base64": "#!/bin/dev foobarbaz",
"Fn::Base64": "#!/bin/bash",
},
},
"TagSpecifications": [
Expand Down Expand Up @@ -1861,7 +1861,7 @@ exports[`the GuEC2App pattern should produce a functional EC2 app with minimal a
},
],
"UserData": {
"Fn::Base64": "#!/bin/dev foobarbaz",
"Fn::Base64": "#!/bin/bash",
},
},
"TagSpecifications": [
Expand Down
78 changes: 39 additions & 39 deletions src/patterns/ec2-app/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Match, Template } from "aws-cdk-lib/assertions";
import { BlockDeviceVolume, EbsDeviceVolumeType, UpdatePolicy } from "aws-cdk-lib/aws-autoscaling";
import { InstanceClass, InstanceSize, InstanceType, Peer, Port, Vpc } from "aws-cdk-lib/aws-ec2";
import { InstanceClass, InstanceSize, InstanceType, Peer, Port, UserData, Vpc } from "aws-cdk-lib/aws-ec2";
import { type CfnLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { AccessScope, MetadataKeys } from "../../constants";
import { GuPrivateConfigBucketParameter } from "../../constructs/core";
Expand All @@ -18,7 +18,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand All @@ -37,7 +37,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.RESTRICTED, cidrRanges: [Peer.ipv4("1.2.3.4/5")] },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand All @@ -56,7 +56,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.INTERNAL, cidrRanges: [Peer.ipv4("10.0.0.0/8")] },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand Down Expand Up @@ -159,7 +159,7 @@ describe("the GuEC2App pattern", function () {
},
unhealthyInstancesAlarm: false,
},
userData: "",
userData: UserData.forLinux(),
});
//The shape of this alarm is tested at construct level
GuTemplate.fromStack(stack).hasResourceWithLogicalId("AWS::CloudWatch::Alarm", /^High5xxPercentageAlarm.+/);
Expand Down Expand Up @@ -187,7 +187,7 @@ describe("the GuEC2App pattern", function () {
},
unhealthyInstancesAlarm: false,
},
userData: "",
userData: UserData.forLinux(),
});
//The shape of this alarm is tested at construct level
GuTemplate.fromStack(stack).hasResourceWithLogicalId("AWS::CloudWatch::Alarm", /^High4xxPercentageAlarm.+/);
Expand All @@ -212,7 +212,7 @@ describe("the GuEC2App pattern", function () {
http5xxAlarm: false,
unhealthyInstancesAlarm: true,
},
userData: "",
userData: UserData.forLinux(),
});
//The shape of this alarm is tested at construct level
GuTemplate.fromStack(stack).hasResourceWithLogicalId("AWS::CloudWatch::Alarm", /^UnhealthyInstancesAlarm.+/);
Expand All @@ -237,7 +237,7 @@ describe("the GuEC2App pattern", function () {
http5xxAlarm: false,
unhealthyInstancesAlarm: false,
},
userData: "",
userData: UserData.forLinux(),
});
Template.fromStack(stack).resourceCountIs("AWS::CloudWatch::Alarm", 0);
});
Expand All @@ -257,7 +257,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
});

const template = Template.fromStack(stack);
Expand Down Expand Up @@ -306,7 +306,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
});

Template.fromStack(stack).hasResourceProperties("AWS::EC2::SecurityGroup", {
Expand Down Expand Up @@ -339,7 +339,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
}),
).toThrowError(
"Restricted apps cannot be globally accessible. Adjust CIDR ranges (0.0.0.0/0, 1.2.3.4/32) or use Public.",
Expand All @@ -363,7 +363,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
}),
).toThrowError(
"Internal apps should only be accessible on 10. ranges. Adjust CIDR ranges (93.1.2.3/12) or use Restricted.",
Expand All @@ -385,7 +385,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
roleConfiguration: {
withoutLogShipping: true,
additionalPolicies: [new GuDynamoDBWritePolicy(stack, "DynamoTable", { tableName: "my-dynamo-table" })],
Expand Down Expand Up @@ -490,16 +490,15 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "UserData from pattern declaration",
userData: UserData.forLinux({ shebang: "#!/user/data/from/pattern" }),
});

expect(pattern.autoScalingGroup.userData).toEqual({ lines: ["UserData from pattern declaration"] });
expect(pattern.autoScalingGroup.userData.render()).toEqual(`#!/user/data/from/pattern`);

pattern.autoScalingGroup.addUserData("UserData from accessed construct");

expect(pattern.autoScalingGroup.userData).toEqual({
lines: ["UserData from pattern declaration", "UserData from accessed construct"],
});
expect(pattern.autoScalingGroup.userData.render()).toEqual(`#!/user/data/from/pattern
UserData from accessed construct`);
});

it("users can optionally configure block devices", function () {
Expand All @@ -517,7 +516,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "UserData from pattern declaration",
userData: UserData.forLinux(),
blockDevices: [
{
deviceName: "/dev/sda1",
Expand Down Expand Up @@ -558,7 +557,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "UserData from pattern declaration",
userData: UserData.forLinux(),
});

const cfnLb = pattern.loadBalancer.node.defaultChild as CfnLoadBalancer;
Expand Down Expand Up @@ -597,7 +596,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "node-app.code.example.com",
},
Expand All @@ -612,7 +611,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "play-app.code.example.com",
},
Expand Down Expand Up @@ -651,7 +650,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: true, prefix: "access-logging-prefix" },
});

Expand Down Expand Up @@ -680,7 +679,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
});

Expand Down Expand Up @@ -709,7 +708,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
applicationLogging: { enabled: true },
});

Expand Down Expand Up @@ -738,7 +737,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
applicationLogging: { enabled: true, systemdUnitName: "not-my-app-name" },
});

Expand Down Expand Up @@ -768,7 +767,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
applicationLogging: { enabled: true },
roleConfiguration: {
withoutLogShipping: true,
Expand All @@ -795,7 +794,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
});

Expand Down Expand Up @@ -823,7 +822,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
googleAuth: {
enabled: true,
Expand Down Expand Up @@ -864,7 +863,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
googleAuth: {
enabled: true,
Expand Down Expand Up @@ -894,7 +893,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
googleAuth: {
enabled: true,
Expand Down Expand Up @@ -924,7 +923,7 @@ describe("the GuEC2App pattern", function () {
minimumInstances: 1,
},
monitoringConfiguration: { noMonitoring: true },
userData: "",
userData: UserData.forLinux(),
accessLogging: { enabled: false },
googleAuth: {
enabled: true,
Expand All @@ -943,7 +942,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand All @@ -968,7 +967,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand Down Expand Up @@ -996,7 +995,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand Down Expand Up @@ -1028,7 +1027,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand All @@ -1048,7 +1047,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand Down Expand Up @@ -1081,7 +1080,7 @@ describe("the GuEC2App pattern", function () {
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand All @@ -1098,13 +1097,14 @@ describe("the GuEC2App pattern", function () {

it("has a defined UpdatePolicy when provided with one", function () {
const stack = simpleGuStackForTesting();

new GuEc2App(stack, {
applicationPort: 3000,
app: "test-gu-ec2-app",
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
userData: UserData.forLinux(),
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
Expand Down
Loading

0 comments on commit e15d900

Please sign in to comment.