Skip to content

Commit

Permalink
test(experimental-ec2-pattern): Ensure only a horizontally scaling AS…
Browse files Browse the repository at this point in the history
…G is adjusted
  • Loading branch information
akash1810 committed Sep 13, 2024
1 parent 2d56327 commit 11e2fb6
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/experimental/patterns/__snapshots__/ec2-app.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ exports[`The GuEc2AppExperimental pattern matches the snapshot 1`] = `
},
},
"Resources": {
"AsgReplacingUpdatePolicy78CF34D5": {
"AsgRollingUpdatePolicy2A1DDC6F": {
"Properties": {
"PolicyDocument": {
"Statement": [
Expand All @@ -90,7 +90,7 @@ exports[`The GuEc2AppExperimental pattern matches the snapshot 1`] = `
],
"Version": "2012-10-17",
},
"PolicyName": "AsgReplacingUpdatePolicy78CF34D5",
"PolicyName": "AsgRollingUpdatePolicy2A1DDC6F",
"Roles": [
{
"Ref": "InstanceRoleTestguec2appC325BE42",
Expand Down
93 changes: 93 additions & 0 deletions src/experimental/patterns/ec2-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,97 @@ describe("The GuEc2AppExperimental pattern", () => {
},
});
});

it("should only adjust properties of a horizontally scaling service", () => {
const cdkApp = new App();
const stack = new GuStack(cdkApp, "test", {
stack: "test-stack",
stage: "TEST",
});

const scalingApp = "my-scaling-app";
const { autoScalingGroup } = new GuEc2AppExperimental(stack, {
...initialProps(stack, scalingApp),
scaling: {
minimumInstances: 5,
},
});
autoScalingGroup.scaleOnRequestCount("ScaleOnRequests", {
targetRequestsPerMinute: 100,
});

const staticApp = "my-static-app";
new GuEc2AppExperimental(stack, initialProps(stack, staticApp));

/*
We're ultimately testing an `Aspect`, which appear to run only at synth time.
As a work-around, synth the `App`, then perform assertions on the resulting template.
See also: https://github.com/aws/aws-cdk/issues/29047.
*/
const { artifacts } = cdkApp.synth();
const cfnStack = artifacts.find((_): _ is CloudFormationStackArtifact => _ instanceof CloudFormationStackArtifact);

if (!cfnStack) {
throw new Error("Unable to locate a CloudFormationStackArtifact");
}

const template = Template.fromJSON(cfnStack.template as Record<string, unknown>);

/*
The scaling ASG should:
- Not have `DesiredCapacity` set
- Have `MinInstancesInService` set via a CFN Parameter
*/
const parameterName = `MinInstancesInServiceFor${scalingApp.replaceAll("-", "")}`;
template.hasParameter(parameterName, {
Type: "Number",
Default: 5,
MaxValue: 9, // (min * 2) - 1
});
template.hasResource("AWS::AutoScaling::AutoScalingGroup", {
Properties: {
MinSize: "5",
MaxSize: "10",
DesiredCapacity: Match.absent(),
Tags: Match.arrayWith([{ Key: "App", Value: scalingApp, PropagateAtLaunch: true }]),
},
UpdatePolicy: {
AutoScalingRollingUpdate: {
MaxBatchSize: 10,
SuspendProcesses: ["AlarmNotification"],
MinSuccessfulInstancesPercent: 100,
WaitOnResourceSignals: true,
PauseTime: "PT5M",
MinInstancesInService: {
Ref: parameterName,
},
},
},
});

/*
The static ASG should:
- Have `DesiredCapacity` set explicitly
- Have `MinInstancesInService` set explicitly
*/
template.hasResource("AWS::AutoScaling::AutoScalingGroup", {
Properties: {
MinSize: "1",
MaxSize: "2",
DesiredCapacity: "1",
Tags: Match.arrayWith([{ Key: "App", Value: staticApp, PropagateAtLaunch: true }]),
},
UpdatePolicy: {
AutoScalingRollingUpdate: {
MaxBatchSize: 2,
SuspendProcesses: ["AlarmNotification"],
MinSuccessfulInstancesPercent: 100,
WaitOnResourceSignals: true,
PauseTime: "PT5M",
MinInstancesInService: 1,
},
},
});
});
});
80 changes: 57 additions & 23 deletions src/experimental/patterns/ec2-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IAspect } from "aws-cdk-lib";
import type { IAspect, Stack } from "aws-cdk-lib";
import { Aspects, CfnParameter, Duration } from "aws-cdk-lib";
import type { CfnAutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
import { CfnScalingPolicy, ScalingProcess, UpdatePolicy } from "aws-cdk-lib/aws-autoscaling";
Expand All @@ -8,8 +8,24 @@ import { GuAutoScalingGroup } from "../../constructs/autoscaling";
import { GuStack } from "../../constructs/core";
import type { GuEc2AppProps } from "../../patterns";
import { GuEc2App } from "../../patterns";
import { isSingletonPresentInStack } from "../../utils/singleton";

class HorizontallyScalingDeploymentProperties implements IAspect {
public readonly stack: Stack;
private static instance: HorizontallyScalingDeploymentProperties | undefined;

private constructor(scope: GuStack) {
this.stack = scope;
}

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

return this.instance;
}

public visit(construct: IConstruct) {
if (construct instanceof CfnScalingPolicy) {
const { node } = construct;
Expand Down Expand Up @@ -60,6 +76,44 @@ class HorizontallyScalingDeploymentProperties implements IAspect {
}
}

class AsgRollingUpdatePolicy extends Policy {
private static instance: AsgRollingUpdatePolicy | undefined;

private constructor(scope: GuStack) {
const { stackId } = scope;

super(scope, "AsgRollingUpdatePolicy", {
statements: [
// Allow usage of command `cfn-signal`.
new PolicyStatement({
actions: ["cloudformation:SignalResource"],
effect: Effect.ALLOW,
resources: [stackId],
}),

/*
Allow usage of command `aws elbv2 describe-target-health`.
AWS Elastic Load Balancing does not support resource based policies, so the resource has to be `*` (any) here.
See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html
*/
new PolicyStatement({
actions: ["elasticloadbalancing:DescribeTargetHealth"],
effect: Effect.ALLOW,
resources: ["*"],
}),
],
});
}

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

return this.instance;
}
}

export interface GuEc2AppExperimentalProps extends Omit<GuEc2AppProps, "updatePolicy"> {}

/**
Expand Down Expand Up @@ -158,27 +212,7 @@ export class GuEc2AppExperimental extends GuEc2App {
},
};

new Policy(scope, "AsgReplacingUpdatePolicy", {
statements: [
// Allow usage of command `cfn-signal`.
new PolicyStatement({
actions: ["cloudformation:SignalResource"],
effect: Effect.ALLOW,
resources: [stackId],
}),

/*
Allow usage of command `aws elbv2 describe-target-health`.
AWS Elastic Load Balancing does not support resource based policies, so the resource has to be `*` (any) here.
See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html
*/
new PolicyStatement({
actions: ["elasticloadbalancing:DescribeTargetHealth"],
effect: Effect.ALLOW,
resources: ["*"],
}),
],
}).attachToRole(role);
AsgRollingUpdatePolicy.getInstance(scope).attachToRole(role);

/*
`ec2metadata` is available via `cloud-utils` installed on all Canonical Ubuntu AMIs.
Expand Down Expand Up @@ -222,6 +256,6 @@ export class GuEc2AppExperimental extends GuEc2App {
`,
);

Aspects.of(scope).add(new HorizontallyScalingDeploymentProperties());
Aspects.of(scope).add(HorizontallyScalingDeploymentProperties.getInstance(scope));
}
}

0 comments on commit 11e2fb6

Please sign in to comment.