Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add first draft of EC2 App Pattern #399

Merged
merged 4 commits into from
Apr 14, 2021
Merged

Conversation

jacobwinch
Copy link
Contributor

@jacobwinch jacobwinch commented Apr 8, 2021

What does this change?

This PR adds a first attempt at our EC2 App pattern. It's still a work in progress, but we have confirmed that this minimal version of the pattern can be deployed successfully so it feels like a good time to get something merged.

Does this change require changes to existing projects or CDK CLI?

No.

How to test

We've added a basic unit test for this. More unit tests (and proper docs) will be added as the pattern develops.

We've also manually tested deploying a stack with the following code:

import type { App } from "@aws-cdk/core";
import type { GuStackProps } from "@guardian/cdk/lib/constructs/core";
import { GuStack } from "@guardian/cdk/lib/constructs/core";
import { GuEc2App } from "@guardian/cdk/lib/patterns/ec2-app";

export class GuCDKEC2Pattern extends GuStack {
  constructor(scope: App, id: string, props: GuStackProps) {
    super(scope, id, props);

    const port = 8000;

    new GuEc2App(this, {
      app: "TestApp",
      applicationPort: port,
      publicFacing: false,
      userData: `#!/usr/bin/env bash
        mkdir test
        cd test
        echo OK >> healthcheck
        python -m SimpleHTTPServer ${port}
      `,
    });
  }
}

This just allows us to serve a static file via /healthcheck, so that we can ensure that requests can be served correctly.

How can we measure success?

It should be much faster/easier to create a new EC2 app from scratch using this pattern.

Have we considered potential risks?

Although this pattern is now functional, we don't want 'real' users to start experimenting with it at this stage. To mitigate against this:

  1. We've added a comment to warn people that this is still under development
  2. The pattern is not mentioned in https://github.com/guardian/cdk/blob/main/src/patterns/index.ts yet.

} from "../constructs/loadbalancing";

interface GuEc2AppProps extends AppIdentity {
userData: GuUserData | string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user needs to download private config then we still need to wire up these permissions (for more details on that, see: #312)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacobwinch jacobwinch marked this pull request as ready for review April 8, 2021 12:50
@jacobwinch jacobwinch requested a review from a team April 8, 2021 12:50
},
role: new GuInstanceRole(scope, { app: props.app }),
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nicer for userData in GuAutoScalingGroup's props to be of type GuUserData | string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is already, what would look better here do you think?

src/patterns/ec2-app.ts Show resolved Hide resolved
src/patterns/ec2-app.ts Outdated Show resolved Hide resolved
src/patterns/ec2-app.ts Show resolved Hide resolved
src/patterns/ec2-app.ts Show resolved Hide resolved
Copy link
Member

@akash1810 akash1810 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fantastic, great work!

Should we add a test for a single GuStack with multiple GuEc2Apps too?

@stephengeller
Copy link
Contributor

I made a change to how the IDs of VPC constructs are declared, as we ran into issues with declaring multiple GuEC2Apps in the same stack, mainly due to the IDs of VPCs and subnets constructs being hardcoed. Would be keen to get opinions on the workaround implemented here.

Copy link
Member

@akash1810 akash1810 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a couple of comments, mainly on code reuse.

src/constructs/ec2/vpc.ts Show resolved Hide resolved
src/constructs/ec2/vpc.ts Show resolved Hide resolved
Comment on lines 41 to 45
{
Key: "gu:cdk:version",
PropagateAtLaunch: true,
Value: "TEST",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use the mock here.

This would become:

import { TrackingTag } from "../../constants/library-info";

...

Tags: alphabeticalTags([
  TrackingTag,
  { Key: "App", ... },
  ...
]);

That is, this can be replaced:

{
  Key: "gu:cdk:version",
  PropagateAtLaunch: true,
  Value: "TEST",
},

With:

TrackingTag

Using the constant means we can update the naming of the tag in one place (should we want to).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout. This doesn't currently work due to the missing PropagateAtLaunch keypair, but I've created a new constant that handles this called TrackingTagWithPropagate.

src/patterns/ec2-app.test.ts Outdated Show resolved Hide resolved
constructor(scope: GuStack, props: GuEc2AppProps) {
const id = (id: string) => `${props.app}${id}`;

scope.tags.setTag(id("App"), props.app);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AppIdentity.taggedConstruct helper should be used here for consistency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout, done.

*/
export class GuEc2App {
constructor(scope: GuStack, props: GuEc2AppProps) {
const id = (id: string) => `${props.app}${id}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppIdentity.suffixText in other places that are app aware, can this be reused here for consistency?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done!


const asg = new GuAutoScalingGroup(scope, id("AutoScalingGroup"), {
vpc,
app: props.app,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: we've destructured app above, so this can become:

Suggested change
app: props.app,
app,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, done.

*/
export class GuPlayApp extends GuEc2App {
constructor(scope: GuStack, props: GuMaybePortProps) {
super(scope, { ...props, applicationPort: props.applicationPort ?? GuApplicationPorts.Play });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd favour being strongly opinionated here and say Play always runs on 9000. That is, don't accept applicationPort in the props.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout, done.

*/
export class GuNodeApp extends GuEc2App {
constructor(scope: GuStack, props: GuMaybePortProps) {
super(scope, { ...props, applicationPort: props.applicationPort ?? GuApplicationPorts.Node });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, I'd favour being strongly opinionated here and say Node always runs on 3000. That is, don't accept applicationPort in the props.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout, done.

expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot();
});

it("can handle multiple EC2 apps in a single stack", function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 lovely!

@akash1810
Copy link
Member

The commit history on this PR is 👌🏽 .

@stephengeller stephengeller force-pushed the sg-jw/ec2-pattern branch 6 times, most recently from b06fcf3 to 7ab36ad Compare April 13, 2021 14:06
@stephengeller
Copy link
Contributor

This should now be ready for review again @akash1810 @jacobwinch !

@@ -10,3 +10,8 @@ export const TrackingTag = {
Key: "gu:cdk:version",
Value: LibraryInfo.VERSION,
};

export const TrackingTagWithPropagate = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct, as I'm not sure we want to publicly export this variant as it's specifically for the tests and not for "daily use"? In the past we've done something like this.

I think there's an opportunity to add a custom matcher here (similar to toHaveResourceOfTypeAndLogicalId) as it looks like we want to assert a construct has the identity tags. That is, a "toHaveAnIdentityTaggedResource" matcher that'll take a stack, a resource type string and assert that it's tags contain all the identity keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing this via Chat, we decided to go with the simpler option for now, in order to get this merged. We'll look at implementing the custom matcher when everyone is around to work on it together.

Copy link
Member

@akash1810 akash1810 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

👍🏽 to this too.

@jacobwinch jacobwinch merged commit e84cc0d into main Apr 14, 2021
@jacobwinch jacobwinch deleted the sg-jw/ec2-pattern branch April 14, 2021 10:10
@github-actions
Copy link
Contributor

🎉 This PR is included in version 8.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants