diff --git a/src/experimental/patterns/ec2-app.ts b/src/experimental/patterns/ec2-app.ts index 62950519e..1a072c49d 100644 --- a/src/experimental/patterns/ec2-app.ts +++ b/src/experimental/patterns/ec2-app.ts @@ -111,7 +111,7 @@ class AutoScalingRollingUpdateTimeout implements IAspect { */ export class HorizontallyScalingDeploymentPropertiesExperimental implements IAspect { public readonly stack: GuStack; - private readonly asgToParamMap: Map; + public readonly asgToParamMap: Map; private static instance: HorizontallyScalingDeploymentPropertiesExperimental | undefined; private constructor(scope: GuStack) { diff --git a/src/riff-raff-yaml-file/deployments/cloudformation.ts b/src/riff-raff-yaml-file/deployments/cloudformation.ts index 7483b5385..e9c9fdeba 100644 --- a/src/riff-raff-yaml-file/deployments/cloudformation.ts +++ b/src/riff-raff-yaml-file/deployments/cloudformation.ts @@ -1,4 +1,5 @@ import type { GuAutoScalingGroup } from "../../constructs/autoscaling"; +import { HorizontallyScalingDeploymentPropertiesExperimental } from "../../experimental/patterns/ec2-app"; import type { CdkStacksDifferingOnlyByStage, RiffRaffDeployment, RiffRaffDeploymentParameters } from "../types"; export function cloudFormationDeployment( @@ -72,3 +73,18 @@ export function getAmiParameters(autoScalingGroups: GuAutoScalingGroup[]): RiffR }; }, {}); } + +export function getMinInstancesInServiceParameters( + autoScalingGroups: GuAutoScalingGroup[], +): RiffRaffDeploymentParameters { + return autoScalingGroups.reduce((acc, asg) => { + const { app } = asg; + const cfnParameter = HorizontallyScalingDeploymentPropertiesExperimental.getCfnParameterName(asg); + return { + ...acc, + [cfnParameter]: { + App: app, + }, + }; + }, {}); +} diff --git a/src/riff-raff-yaml-file/index.test.ts b/src/riff-raff-yaml-file/index.test.ts index cee641bdd..8bcdb615f 100644 --- a/src/riff-raff-yaml-file/index.test.ts +++ b/src/riff-raff-yaml-file/index.test.ts @@ -1,14 +1,19 @@ import { App, Duration } from "aws-cdk-lib"; -import { UpdatePolicy } from "aws-cdk-lib/aws-autoscaling"; +import { CfnScalingPolicy, UpdatePolicy } from "aws-cdk-lib/aws-autoscaling"; import { InstanceClass, InstanceSize, InstanceType } from "aws-cdk-lib/aws-ec2"; import { Schedule } from "aws-cdk-lib/aws-events"; import { Runtime } from "aws-cdk-lib/aws-lambda"; import { AccessScope } from "../constants"; +import type { GuAutoScalingGroup } from "../constructs/autoscaling"; import type { GuStackProps } from "../constructs/core"; import { GuStack } from "../constructs/core"; import { GuLambdaFunction } from "../constructs/lambda"; -import { GuEc2AppExperimental } from "../experimental/patterns/ec2-app"; +import { + GuEc2AppExperimental, + HorizontallyScalingDeploymentPropertiesExperimental, +} from "../experimental/patterns/ec2-app"; import { GuEc2App, GuNodeApp, GuPlayApp, GuScheduledLambda } from "../patterns"; +import { getTemplateAfterAspectInvocation } from "../utils/test"; import { RiffRaffYamlFile } from "./index"; describe("The RiffRaffYamlFile class", () => { @@ -1413,4 +1418,106 @@ describe("The RiffRaffYamlFile class", () => { " `); }); + + it("Should include minInstancesInServiceParameters when GuEc2AppExperimental has a scaling policy", () => { + const app = new App({ outdir: "/tmp/cdk.out" }); + + class MyApplicationStack extends GuStack { + public readonly asg: GuAutoScalingGroup; + + // eslint-disable-next-line custom-rules/valid-constructors -- unit testing + constructor(app: App, id: string, props: GuStackProps) { + super(app, id, props); + + const appName = "my-app"; + + const { autoScalingGroup } = new GuEc2AppExperimental(this, { + app: appName, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MICRO), + access: { scope: AccessScope.PUBLIC }, + userData: { + distributable: { + fileName: `${appName}.deb`, + executionStatement: `dpkg -i /${appName}/${appName}.deb`, + }, + }, + certificateProps: { + domainName: "rip.gu.com", + }, + monitoringConfiguration: { noMonitoring: true }, + scaling: { + minimumInstances: 1, + }, + applicationPort: 9000, + imageRecipe: "arm64-bionic-java11-deploy-infrastructure", + buildIdentifier: "TEST", + }); + + new CfnScalingPolicy(autoScalingGroup, "ScaleOut", { + autoScalingGroupName: autoScalingGroup.autoScalingGroupName, + policyType: "SimpleScaling", + adjustmentType: "ChangeInCapacity", + scalingAdjustment: 1, + }); + + this.asg = autoScalingGroup; + } + } + + const guStack = new MyApplicationStack(app, "test-stack", { + stack: "test", + stage: "TEST", + env: { region: "eu-west-1" }, + }); + + // Ensure the Aspects are invoked... + getTemplateAfterAspectInvocation(guStack); + + // ...so that the CFN Parameters are added to the template, to then be processed by the `RiffRaffYamlFile` + const actual = new RiffRaffYamlFile(app).toYAML(); + + const cfnParameterName = HorizontallyScalingDeploymentPropertiesExperimental.getCfnParameterName(guStack.asg); + + expect(actual).toMatchInlineSnapshot(` + "allowedStages: + - TEST + deployments: + asg-upload-eu-west-1-test-my-app: + type: autoscaling + actions: + - uploadArtifacts + regions: + - eu-west-1 + stacks: + - test + app: my-app + parameters: + bucketSsmLookup: true + prefixApp: true + contentDirectory: my-app + cfn-eu-west-1-test-my-application-stack: + type: cloud-formation + regions: + - eu-west-1 + stacks: + - test + app: my-application-stack + contentDirectory: /tmp/cdk.out + parameters: + templateStagePaths: + TEST: test-stack.template.json + amiParametersToTags: + AMIMyapp: + BuiltBy: amigo + AmigoStage: PROD + Recipe: arm64-bionic-java11-deploy-infrastructure + Encrypted: 'true' + minInstancesInServiceParameters: + ${cfnParameterName}: + App: my-app + dependencies: + - asg-upload-eu-west-1-test-my-app + " + `); + }); }); diff --git a/src/riff-raff-yaml-file/index.ts b/src/riff-raff-yaml-file/index.ts index 2c5eba421..c26edfc3c 100644 --- a/src/riff-raff-yaml-file/index.ts +++ b/src/riff-raff-yaml-file/index.ts @@ -7,8 +7,13 @@ import { dump } from "js-yaml"; import { GuAutoScalingGroup } from "../constructs/autoscaling"; import { GuStack } from "../constructs/core"; import { GuLambdaFunction } from "../constructs/lambda"; +import { HorizontallyScalingDeploymentPropertiesExperimental } from "../experimental/patterns/ec2-app"; import { autoscalingDeployment, uploadAutoscalingArtifact } from "./deployments/autoscaling"; -import { cloudFormationDeployment, getAmiParameters } from "./deployments/cloudformation"; +import { + cloudFormationDeployment, + getAmiParameters, + getMinInstancesInServiceParameters, +} from "./deployments/cloudformation"; import { updateLambdaDeployment, uploadLambdaArtifact } from "./deployments/lambda"; import { groupByClassNameStackRegionStage } from "./group-by"; import type { @@ -271,6 +276,11 @@ export class RiffRaffYamlFile { const amiParametersToTags = getAmiParameters(autoscalingGroups); + const minInServiceParamMap = + HorizontallyScalingDeploymentPropertiesExperimental.getInstance(stack).asgToParamMap; + const minInServiceAsgs = autoscalingGroups.filter((asg) => minInServiceParamMap.has(asg.node.id)); + const minInstancesInServiceParameters = getMinInstancesInServiceParameters(minInServiceAsgs); + deployments.set(cfnDeployment.name, { ...cfnDeployment.props, parameters: { @@ -278,6 +288,9 @@ export class RiffRaffYamlFile { // only add the `amiParametersToTags` property if there are some ...(autoscalingGroups.length > 0 && { amiParametersToTags }), + + // only add the `minInstancesInServiceParameters` property if there are some + ...(minInServiceAsgs.length > 0 && { minInstancesInServiceParameters }), }, }); });