constructor()

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));
  }