Build awareness and adoption for your software startup with Circuit.

Autoscaling using Spot Instances with AWS CDK + TS

Suppose you're a developer (like me xD). In that case, you know the value of running applications in the cloud --- the ability to scale up and down quickly, test your app anywhere, and pay for resources as needed.

AWS EC2 Spot Instances are the best of both worlds --- they're just like regular EC2 instances, except you don't need to pay for them upfront, and they're cheaper than regular instances.

The main advantage of spot instances is that you only pay when you use them (rather than paying an upfront fee). But with spot instances, you also get more flexibility --- you can move them around pretty easily, which means they work out as an excellent option for short-term projects or testing. In general, if your project isn't going to last longer than a few weeks or months, and you're not concerned with having access to all the features of your regular instance type (such as the ability to scale it up), then this is probably the best way to go!

If you want to know more about spot instances, please review this awesome blog post.

Let's get your hands dirty!

someone typing

Our Architecture:

VPC

Let's start creating our VPC, piece of cake, public and private subnets.

import { IVpc, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";

export default class DefaultVpc extends Construct {
  public readonly vpc: IVpc;
  constructor(scope: Construct, id: string) {
    super(scope, id);
    this.vpc = new Vpc(this, "my-vpc", {
      cidr: "10.0.0.1/24",
      subnetConfiguration: [
        {
          cidrMask: 28,
          name: "public subnet",
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 28,
          name: "private subnet",
          subnetType: SubnetType.PRIVATE_WITH_NAT,
        },
      ],
    });
  }
}

Application Load Balancer

In this step, we need to create an internet-facing load balancer

import { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
import { IVpc } from "aws-cdk-lib/aws-ec2";
import { ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";

export default class InternetFacingApplicationLoadBalancer extends Construct {
  constructor(
    scope: Construct,
    id: string,
    resources: { vpc: IVpc; ec2AutoScalingGroup: AutoScalingGroup }
  ) {
    super(scope, id);

    const loadBalancer = new ApplicationLoadBalancer(this, "appLoadBalancer", {
      vpc: resources.vpc,
      internetFacing: true,
    });

    const httpListener = loadBalancer.addListener("httpListener", {
      port: 80,
      open: true,
    });

    httpListener.addTargets("ApplicationSpotFleet", {
      port: 8080,
      targets: [resources.ec2AutoScalingGroup],
    });
  }
}

AutoScaling Group

Now we create the autoscaling, ec2 instances and the remarkable decision to use spot instances.

We are using t4g.micro instances with the latest Amazon Linux images. We are allowing all outbound traffic; the desired number of instances is one, while the maximum is 2.

The maximum spot price is set to 0.007, if the sport price goes up, for example to* 0.008*, then we have no instances. There are ways to address this, but for this example, I'll only use it in this way.

import { Duration } from "aws-cdk-lib";
import { AutoScalingGroup, HealthCheck } from "aws-cdk-lib/aws-autoscaling";
import {
  AmazonLinuxGeneration,
  AmazonLinuxImage,
  InstanceClass,
  InstanceSize,
  InstanceType,
  IVpc,
} from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";

export default class ApplicationAutoScalingGroup extends Construct {
  public readonly autoScalingGroup: AutoScalingGroup;
  constructor(scope: Construct, id: string, resources: { vpc: IVpc }) {
    super(scope, id);

    const applicationAutoScalingGroup = new AutoScalingGroup(
      this,
      "AutoScalingGroup",
      {
        vpc: resources.vpc,
        instanceType: InstanceType.of(
          InstanceClass.BURSTABLE4_GRAVITON,
          InstanceSize.MICRO
        ),
        machineImage: new AmazonLinuxImage({
          generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
        }),
        allowAllOutbound: true,
        maxCapacity: 2,
        minCapacity: 1,
        desiredCapacity: 1,
        spotPrice: "0.007", // $0.0032 per Hour when writing, $0.0084 per Hour on-demand
        healthCheck: HealthCheck.ec2(),
      }
    );

    applicationAutoScalingGroup.scaleOnCpuUtilization("CpuScaling", {
      targetUtilizationPercent: 50,
      cooldown: Duration.minutes(1),
      estimatedInstanceWarmup: Duration.minutes(1),
    });

    this.autoScalingGroup = applicationAutoScalingGroup;
  }
}

Stack

Finally, our stack will be something like this:

import { StackProps, Environment, Stack } from "aws-cdk-lib";
import { Construct } from "constructs";
import InternetFacingApplicationLoadBalancer from "../lib/application-load-balancer/internet-facing-application-load-balancer";
import ApplicationAutoScalingGroup from "../lib/ec2/auto-scaling-group";
import DefaultVpc from "../lib/vpc/default-vpc";

export interface IStackProps extends StackProps {
  variables?: any;
  env: Environment;
}

export class ApplicationIntegrationStack extends Stack {
  constructor(scope: Construct, id: string, props: IStackProps) {
    super(scope, id, props);

    const { vpc } = new DefaultVpc(this, "DefaultVpc");
    const { autoScalingGroup } = new ApplicationAutoScalingGroup(
      this,
      "ApplicationAutoScalingGroup",
      { vpc }
    );
    new InternetFacingApplicationLoadBalancer(
      this,
      "InternetFacingApplicationLoadBalancer",
      { vpc, ec2AutoScalingGroup: autoScalingGroup }
    );
  }
}

If you want to dive deeply into this topic I strongly recommend this re:invent video about spot instances.

That's all folks!




Continue Learning