in src/experimental/patterns/ec2-app.ts [291:385]
constructor(scope: GuStack, props: GuEc2AppExperimentalProps) {
const { minimumInstances, maximumInstances = minimumInstances * 2 } = props.scaling;
const { applicationPort, buildIdentifier } = props;
const { region, stackId } = scope;
super(scope, {
...props,
updatePolicy: UpdatePolicy.rollingUpdate({
maxBatchSize: maximumInstances,
minInstancesInService: minimumInstances,
minSuccessPercentage: 100,
waitOnResourceSignals: true,
/*
If a scale-in event fires during an `AutoScalingRollingUpdate` operation, the update could fail and rollback.
For this reason, we suspend the `AlarmNotification` process, else availability of a service cannot be guaranteed.
Consequently, services cannot scale-out during deployments.
If AWS ever supports suspending scale-out and scale-in independently, we should allow scale-out.
*/
suspendProcesses: [ScalingProcess.ALARM_NOTIFICATION],
}),
});
const { autoScalingGroup, targetGroup } = this;
const { userData, role } = autoScalingGroup;
const cfnAutoScalingGroup = autoScalingGroup.node.defaultChild as CfnAutoScalingGroup;
cfnAutoScalingGroup.desiredCapacity = minimumInstances.toString();
cfnAutoScalingGroup.cfnOptions.creationPolicy = {
autoScalingCreationPolicy: {
minSuccessfulInstancesPercent: 100,
},
resourceSignal: {
count: minimumInstances,
},
};
const policy = AsgRollingUpdatePolicy.getInstance(scope);
policy.attachToRole(role);
// Create the Policy with necessary permissions first.
// Then create the ASG that requires the permissions.
const cfnPolicy = policy.node.defaultChild as CfnPolicy;
cfnAutoScalingGroup.addDependency(cfnPolicy);
/*
`aws` is available via AMIgo baked AMIs.
See https://github.com/guardian/amigo/tree/main/roles/aws-tools.
*/
userData.addCommands(
`# ${GuEc2AppExperimental.name} UserData Start`,
`
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" "http://169.254.169.254/latest/meta-data/instance-id")
STATE=$(aws elbv2 describe-target-health \
--target-group-arn ${targetGroup.targetGroupArn} \
--region ${region} \
--targets Id=$INSTANCE_ID,Port=${applicationPort} \
--query "TargetHealthDescriptions[0].TargetHealth.State")
until [ "$STATE" == "\\"healthy\\"" ]; do
echo "Instance running build ${buildIdentifier} not yet healthy within target group. Current state $STATE. Sleeping..."
sleep ${RollingUpdateDurations.sleep.toSeconds()}
STATE=$(aws elbv2 describe-target-health \
--target-group-arn ${targetGroup.targetGroupArn} \
--region ${region} \
--targets Id=$INSTANCE_ID,Port=${applicationPort} \
--query "TargetHealthDescriptions[0].TargetHealth.State")
done
echo "Instance running build ${buildIdentifier} is healthy in target group."
`,
`# ${GuEc2AppExperimental.name} UserData End`,
);
userData.addOnExitCommands(
`
cfn-signal --stack ${stackId} \
--resource ${cfnAutoScalingGroup.logicalId} \
--region ${region} \
--exit-code $exitCode || echo 'Failed to send Cloudformation Signal'
`,
);
// If https://github.com/guardian/devx-logs is used, this tag will be added as a marker to logs in Central ELK.
Tags.of(autoScalingGroup.instanceLaunchTemplate).add(MetadataKeys.BUILD_IDENTIFIER, buildIdentifier, {
applyToLaunchedInstances: true,
});
// TODO Once out of experimental, instantiate these `Aspect`s directly in `GuStack`.
Aspects.of(scope).add(AutoScalingRollingUpdateTimeout.getInstance(scope));
Aspects.of(scope).add(HorizontallyScalingDeploymentProperties.getInstance(scope));
}