Skip to content

Commit

Permalink
Add first attempt at Mappings implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobwinch committed Feb 16, 2021
1 parent 390a734 commit 76237c3
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 2 deletions.
78 changes: 78 additions & 0 deletions src/constructs/autoscaling/asg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ describe("The GuAutoScalingGroup", () => {
const defaultProps: GuAutoScalingGroupProps = {
vpc,
userData: "user data",
capacity: {
minimumCodeInstances: 1,
minimumProdInstances: 3,
},
};

test("adds the AMI parameter if no imageId prop provided", () => {
Expand Down Expand Up @@ -184,4 +188,78 @@ describe("The GuAutoScalingGroup", () => {

expect(Object.keys(json.Resources)).not.toContain("AutoscalingGroup");
});

test("adds the correct mappings when provided with minimal capacity config", () => {
const stack = simpleGuStackForTesting();
new GuAutoScalingGroup(stack, "AutoscalingGroup", defaultProps);

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;

expect(json.Mappings).toEqual({
stagemapping: {
CODE: {
minInstances: 1,
maxInstances: 2,
},
PROD: {
minInstances: 3,
maxInstances: 6,
},
},
});
});

test("uses custom max capacities (if provided)", () => {
const stack = simpleGuStackForTesting();
new GuAutoScalingGroup(stack, "AutoscalingGroup", {
...defaultProps,
capacity: {
minimumCodeInstances: 1,
minimumProdInstances: 3,
maximumCodeInstances: 5,
maximumProdInstances: 7,
},
});

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;

expect(json.Mappings).toEqual({
stagemapping: {
CODE: {
minInstances: 1,
maxInstances: 5,
},
PROD: {
minInstances: 3,
maxInstances: 7,
},
},
});
});

test("Uses Find In Map correctly to reference capacity mappings", () => {
const stack = simpleGuStackForTesting();
new GuAutoScalingGroup(stack, "AutoscalingGroup", defaultProps);

expect(stack).toHaveResource("AWS::AutoScaling::AutoScalingGroup", {
MinSize: {
"Fn::FindInMap": [
"stagemapping",
{
Ref: "Stage",
},
"minInstances",
],
},
MaxSize: {
"Fn::FindInMap": [
"stagemapping",
{
Ref: "Stage",
},
"maxInstances",
],
},
});
});
});
60 changes: 58 additions & 2 deletions src/constructs/autoscaling/asg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ import { AutoScalingGroup } from "@aws-cdk/aws-autoscaling";
import type { ISecurityGroup, MachineImage, MachineImageConfig } from "@aws-cdk/aws-ec2";
import { InstanceType, OperatingSystemType, UserData } from "@aws-cdk/aws-ec2";
import type { ApplicationTargetGroup } from "@aws-cdk/aws-elasticloadbalancingv2";
import type { GuStack } from "../core";
import type { GuStack, GuStageVariable } from "../core";
import { GuAmiParameter, GuInstanceTypeParameter } from "../core";

// Since we want to override the types of what gets passed in for the below props,
// we need to use Omit<T, U> to remove them from the interface this extends
// https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys
export interface GuAutoScalingGroupProps
extends Omit<AutoScalingGroupProps, "imageId" | "osType" | "machineImage" | "instanceType" | "userData"> {
extends Omit<
AutoScalingGroupProps,
| "imageId"
| "osType"
| "machineImage"
| "instanceType"
| "userData"
| "minCapacity"
| "maxCapacity"
| "desiredCapacity"
> {
capacity: GuAsgCapacityProps;
instanceType?: InstanceType;
imageId?: GuAmiParameter;
osType?: OperatingSystemType;
Expand All @@ -21,6 +32,50 @@ export interface GuAutoScalingGroupProps
overrideId?: boolean;
}

/**
* `minimumCodeInstances` and `minimumProdInstances` determine the number of ec2 instances running
* in each stage under normal circumstances (i.e. when there are no deployment or scaling events in progress).
*
* `maximumCodeInstances` and `maximumProdInstances` are both optional. If omitted, these
* will be set to `minimumCodeInstances * 2` and `minimumProdInstances * 2`, respectively.
* This allows us to support Riff-Raff's autoscaling deployment type by default.
*
* The maximum capacity values can be set manually if you need to scale beyond the default limit (e.g. due to heavy traffic)
* or restrict scaling for a specific reason.
*/
export interface GuAsgCapacityProps {
minimumCodeInstances: number;
minimumProdInstances: number;
maximumCodeInstances?: number;
maximumProdInstances?: number;
}

interface AwsAsgCapacityProps {
minCapacity: number;
maxCapacity: number;
}

function wireCapacityProps(scope: GuStack, capacity: GuAsgCapacityProps): AwsAsgCapacityProps {
const minInstancesKey = "minInstances";
const maxInstancesKey = "maxInstances";
const minInstances: GuStageVariable = {
variableName: minInstancesKey,
codeValue: capacity.minimumCodeInstances,
prodValue: capacity.minimumProdInstances,
};
const maxInstances: GuStageVariable = {
variableName: maxInstancesKey,
codeValue: capacity.maximumCodeInstances ?? capacity.minimumCodeInstances * 2,
prodValue: capacity.maximumProdInstances ?? capacity.minimumProdInstances * 2,
};
scope.mappings.addStageVariable(minInstances);
scope.mappings.addStageVariable(maxInstances);
return {
minCapacity: (scope.mappings.findInMap(scope.stage, minInstancesKey) as unknown) as number,
maxCapacity: (scope.mappings.findInMap(scope.stage, maxInstancesKey) as unknown) as number,
};
}

export class GuAutoScalingGroup extends AutoScalingGroup {
constructor(scope: GuStack, id: string, props: GuAutoScalingGroupProps) {
// We need to override getImage() so that we can pass in the AMI as a parameter
Expand All @@ -40,6 +95,7 @@ export class GuAutoScalingGroup extends AutoScalingGroup {

const mergedProps = {
...props,
...wireCapacityProps(scope, props.capacity),
machineImage: { getImage: getImage },
instanceType: props.instanceType ?? new InstanceType(new GuInstanceTypeParameter(scope).valueAsString),
userData: UserData.custom(props.userData),
Expand Down
1 change: 1 addition & 0 deletions src/constructs/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./mappings";
export * from "./parameters";
export * from "./stack";
18 changes: 18 additions & 0 deletions src/constructs/core/mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CfnMapping } from "@aws-cdk/core";
import type { GuStack } from "./stack";

export interface GuStageVariable {
variableName: string;
codeValue: unknown;
prodValue: unknown;
}

export class GuStageMapping extends CfnMapping {
addStageVariable(stageVariable: GuStageVariable): void {
this.setValue("CODE", stageVariable.variableName, stageVariable.codeValue);
this.setValue("PROD", stageVariable.variableName, stageVariable.prodValue);
}
constructor(scope: GuStack, id: string = "stage-mapping") {
super(scope, id);
}
}
7 changes: 7 additions & 0 deletions src/constructs/core/stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { App, StackProps } from "@aws-cdk/core";
import { Stack, Tags } from "@aws-cdk/core";
import { TrackingTag } from "../../constants/library-info";
import { GuStageMapping } from "./mappings";
import { GuStackParameter, GuStageParameter } from "./parameters";

export interface GuStackProps extends StackProps {
Expand Down Expand Up @@ -40,6 +41,7 @@ export interface GuStackProps extends StackProps {
export class GuStack extends Stack {
private readonly _stage: GuStageParameter;
private readonly _stack: GuStackParameter;
private _mappings: undefined | GuStageMapping;
private readonly _app: string;

public readonly migratedFromCloudFormation: boolean;
Expand All @@ -56,6 +58,11 @@ export class GuStack extends Stack {
return this._app;
}

// Use lazy initialisation for GuStageMapping so that Mappings block is only created when necessary
get mappings(): GuStageMapping {
return this._mappings ?? (this._mappings = new GuStageMapping(this));
}

/**
* A helper function to add a tag to all resources in a stack.
*
Expand Down
1 change: 1 addition & 0 deletions test/utils/synthed-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface SynthedStack {
Parameters: Record<string, { Properties: Record<string, unknown> }>;
Mappings: Record<string, unknown>;
Resources: Record<string, { Properties: Record<string, unknown> }>;
}

0 comments on commit 76237c3

Please sign in to comment.