Skip to content

Commit

Permalink
feat: Support more than one EC2 app with load balancer access logs en…
Browse files Browse the repository at this point in the history
…abled
  • Loading branch information
marsavar committed Jan 29, 2024
1 parent 5df089e commit ac7354f
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-maps-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@guardian/cdk": minor
---

Support multiple EC2 apps with load balancer access logs enabled
24 changes: 24 additions & 0 deletions src/constructs/core/parameters/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,27 @@ export class GuPrivateConfigBucketParameter extends GuStringParameter {
});
}
}

/**
* Creates a CloudFormation parameter which references the bucket used to store load balancer access logs.
* By default, the bucket name is stored in an SSM Parameter called `/account/services/access-logging/bucket`.
*/
export class GuAccessLoggingBucketParameter extends GuStringParameter {
private static instance: GuAccessLoggingBucketParameter | undefined;

private constructor(scope: GuStack) {
super(scope, "AccessLoggingBucket", {
description: NAMED_SSM_PARAMETER_PATHS.AccessLoggingBucket.description,
default: NAMED_SSM_PARAMETER_PATHS.AccessLoggingBucket.path,
fromSSM: true,
});
}

public static getInstance(stack: GuStack): GuAccessLoggingBucketParameter {
if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {
this.instance = new GuAccessLoggingBucketParameter(stack);
}

return this.instance;
}
}
202 changes: 130 additions & 72 deletions src/patterns/ec2-app/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,84 +934,142 @@ describe("the GuEC2App pattern", function () {
}),
).toThrowError("googleAuth.allowedGroups must use the @guardian.co.uk domain.");
});
});

it("should provides a default healthcheck", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
});
Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", {
HealthCheckIntervalSeconds: 10,
HealthCheckPath: "/healthcheck",
HealthCheckProtocol: "HTTP",
HealthCheckTimeoutSeconds: 5,
HealthyThresholdCount: 5,
it("should provides a default healthcheck", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
});
Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", {
HealthCheckIntervalSeconds: 10,
HealthCheckPath: "/healthcheck",
HealthCheckProtocol: "HTTP",
HealthCheckTimeoutSeconds: 5,
HealthyThresholdCount: 5,
});
});
});

it("allows a custom healthcheck", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
healthcheck: {
path: "/custom-healthcheck",
},
});
Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", {
HealthCheckIntervalSeconds: 10,
HealthCheckPath: "/custom-healthcheck",
HealthCheckProtocol: "HTTP",
HealthCheckTimeoutSeconds: 5,
HealthyThresholdCount: 5,
it("allows a custom healthcheck", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
healthcheck: {
path: "/custom-healthcheck",
},
});
Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", {
HealthCheckIntervalSeconds: 10,
HealthCheckPath: "/custom-healthcheck",
HealthCheckProtocol: "HTTP",
HealthCheckTimeoutSeconds: 5,
HealthyThresholdCount: 5,
});
});
});

it("can specify instance metadata hop limit", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
instanceMetadataHopLimit: 2,
it("can specify instance metadata hop limit", 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",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
instanceMetadataHopLimit: 2,
});
Template.fromStack(stack).hasResourceProperties("AWS::EC2::LaunchTemplate", {
LaunchTemplateData: {
MetadataOptions: {
HttpPutResponseHopLimit: 2,
HttpTokens: "required",
},
},
});
});
Template.fromStack(stack).hasResourceProperties("AWS::EC2::LaunchTemplate", {
LaunchTemplateData: {
MetadataOptions: {
HttpPutResponseHopLimit: 2,
HttpTokens: "required",

it("supports more than one EC2 app with load balancer access logs enabled", () => {
const stack = simpleGuStackForTesting({
env: {
region: "test",
},
},
});

new GuEc2App(stack, {
applicationPort: 3000,
app: "test-gu-ec2-app-1",
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
instanceMetadataHopLimit: 2,
accessLogging: {
enabled: true,
prefix: "test-1",
},
});

new GuEc2App(stack, {
applicationPort: 3000,
app: "test-gu-ec2-app-2",
access: { scope: AccessScope.PUBLIC },
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
monitoringConfiguration: { noMonitoring: true },
userData: "#!/bin/dev foobarbaz",
certificateProps: {
domainName: "domain-name-for-your-application.example",
},
scaling: {
minimumInstances: 1,
},
instanceMetadataHopLimit: 2,
accessLogging: {
enabled: true,
prefix: "test-2",
},
});

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
Tags: Match.arrayWith([Match.objectLike({ Key: "App", Value: "test-gu-ec2-app-1" })]),
LoadBalancerAttributes: Match.arrayWith([Match.objectLike({ Key: "access_logs.s3.prefix", Value: "test-1" })]),
});

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
Tags: Match.arrayWith([Match.objectLike({ Key: "App", Value: "test-gu-ec2-app-2" })]),
LoadBalancerAttributes: Match.arrayWith([Match.objectLike({ Key: "access_logs.s3.prefix", Value: "test-2" })]),
});
});
});
8 changes: 2 additions & 6 deletions src/patterns/ec2-app/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
GuUnhealthyInstancesAlarm,
} from "../../constructs/cloudwatch";
import type { GuStack } from "../../constructs/core";
import { AppIdentity, GuLoggingStreamNameParameter, GuStringParameter } from "../../constructs/core";
import { AppIdentity, GuAccessLoggingBucketParameter, GuLoggingStreamNameParameter } from "../../constructs/core";
import { GuHttpsEgressSecurityGroup, GuSecurityGroup, GuVpc, SubnetType } from "../../constructs/ec2";
import type { GuInstanceRoleProps } from "../../constructs/iam";
import { GuGetPrivateConfigPolicy, GuInstanceRole } from "../../constructs/iam";
Expand Down Expand Up @@ -420,11 +420,7 @@ export class GuEc2App extends Construct {
});

if (accessLogging.enabled) {
const accessLoggingBucket = new GuStringParameter(scope, "AccessLoggingBucket", {
description: NAMED_SSM_PARAMETER_PATHS.AccessLoggingBucket.description,
default: NAMED_SSM_PARAMETER_PATHS.AccessLoggingBucket.path,
fromSSM: true,
});
const accessLoggingBucket = GuAccessLoggingBucketParameter.getInstance(scope);

loadBalancer.logAccessLogs(
Bucket.fromBucketName(
Expand Down

0 comments on commit ac7354f

Please sign in to comment.