AWS Auto Scaling Features to Optimize Cost and Ensure Availability
Tags: aws, autoscaling, loadbalancer, cloud
Published: 2022-08-18 07:24:00

Cloud Computing may be a solution for your on-premises problem but not everyone knows the cloud, right? Let's say you find a solution for your application that can be done using the cloud but a solution to "*persuade*" your boss may seem a bit harder :)

If you agree with me, please comment "Yes" so I can hear your voice :) *Just kidding!*

Are you the one who is confused with so many questions?

1. How about the cost? (we can't deny that it always be number one)
    
2. What are the benefits?
    
3. Many more ...
    

The following solutions can be your "*weapon*" when it comes to the cloud, but how?

![EC2 Auto Scaling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akjwtkd31g64sxobw704.png)

## Always Available

One of the biggest benefits of using the cloud is the provider is always able to meet our needs. Let's say you need a VM right now! You can directly access your AWS console and create an EC2 instance. It will be available and running as soon as possible after you create it. One problem is solved.

Then, what if we need more features? Let's say you want to use LightSail to run your WordPress instantly but it seems like it's not available in your region (for example Jakarta)! You can choose the nearest region where the service is already activated (for example Singapore). The second problem is solved and you can access your WordPress dashboard now.

Or maybe you want to have a backup for your application when there is a problem in one area? You can activate multi-AZ or region, third problem is solved.

But what if our product/application becomes viral and accessed from various countries? You can use CloudFront as CDN.

That's how cloud availability solves our problem "**in general**", I can't say it's the best AWS solution since AWS has so many services. So when we need the resources as soon as possible, we don't need to buy and look for the best brand for our devices.

## Ease of Scaling

Let's say we're newbies in the cloud and we're just moving from on-premises culture. So EC2 will bring you easily to get to know about cloud environment. We all know we can create as many EC2 instances as we need (20 per region but can be increased by request) but one thing that really important and we can't forget about is **cost**. It can be so high when you don't know how to optimize based on your need (especially when you use on-demand instances).

Alright! There are so many ways we can scale our applications and optimize the cost, but here I will take two services that can be a basis for how we do it with AWS.

In this post, I'll create AWS Auto Scaling Group and Application Load Balancer using CloudFormation. All operations will be done through CLI, so make sure you have installed [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [set up the credentials](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html). Note: You can do this through the web console if you want.

![Full ASG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwr5fh6thcioe1np3brp.png)

Network details (you can change based on your need):

```yaml
VPC CIDR: 172.16.0.0/16
Subnet 1: 172.16.1.0/28
Subnet 2: 172.16.15.0/28 
Subnet 3: 172.16.30.0/28
```

(Sorry for a little typo, subnet 2 should be `172.16.1.16/28` and subnet 3 should be `172.16.1.32/28` for `/28` usage but it's okay because the above networks still work)

Here's the whole content of the CloudFormation template named `cfn-asg.yaml`! I'll tell you the main points later.

```yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  AmazonLinux2LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
  EnvironmentName:
    Type: String
    Default: DhonaASGTemplate
  SpotInstanceType1:
    Type: String
    Default: t3.large
  SpotInstanceType2:
    Type: String
    Default: t3.medium
  SpotInstanceType3:
    Type: String
    Default: t3.small
  SpotInstanceType4:
    Type: String
    Default: t3.micro
  SpotInstanceType5:
    Type: String
    Default: t3.nano

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName}
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName}
  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: InternetGateway
      VpcId:
        Ref: VPC
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPC
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName} Public Routes
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      SubnetId:
        Ref: PublicSubnet1
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: ap-southeast-3a
      CidrBlock: 172.16.1.0/28
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName} Public Subnet (AZ1)
  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      SubnetId:
        Ref: PublicSubnet2
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: ap-southeast-3b
      CidrBlock: 172.16.15.0/28
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName} Public Subnet (AZ2)
  PublicSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      SubnetId:
        Ref: PublicSubnet3
  PublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: ap-southeast-3c
      CidrBlock: 172.16.30.0/28
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName} Public Subnet (AZ3)

  DhonaWebAppAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn: InternetGatewayAttachment
    Properties:
      CapacityRebalance: true
      HealthCheckType: ELB
      HealthCheckGracePeriod: 300
      MinSize: "3"
      MaxSize: "5"
      DesiredCapacity: "3"
      VPCZoneIdentifier:
        - Ref: PublicSubnet1
        - Ref: PublicSubnet2
        - Ref: PublicSubnet3
      MixedInstancesPolicy:
        InstancesDistribution:
          OnDemandAllocationStrategy: prioritized
          OnDemandPercentageAboveBaseCapacity: 30
          SpotAllocationStrategy: capacity-optimized
          SpotMaxPrice: 0.003
        LaunchTemplate:
          LaunchTemplateSpecification:
            LaunchTemplateId:
              Ref: DhonaWebAppLaunchTemplate
            Version:
              Fn::GetAtt:
                - DhonaWebAppLaunchTemplate
                - LatestVersionNumber
          Overrides:
            - InstanceType:
                Ref: SpotInstanceType1
            - InstanceType:
                Ref: SpotInstanceType2
            - InstanceType:
                Ref: SpotInstanceType3
            - InstanceType:
                Ref: SpotInstanceType4
            - InstanceType:
                Ref: SpotInstanceType5
      TargetGroupARNs:
        - Ref: DhonaWebAppTargetGroup
      Tags:
        - Key: Name
          Value:
            Fn::Sub: ${EnvironmentName}
          PropagateAtLaunch: true
  DhonaWebAppLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    DependsOn: InternetGatewayAttachment
    Properties:
      LaunchTemplateData:
        ImageId:
          Ref: AmazonLinux2LatestAmiId
        SecurityGroupIds:
          - Ref: DhonaWebAppEC2SecurityGroup
        UserData:
          Fn::Base64: >
            #!/bin/bash

            yum -y install httpd

            echo "hello world! 

            My instance-id is $(curl -s http://169.254.169.254/latest/meta-data/instance-id)

            My instance type is $(curl -s http://169.254.169.254/latest/meta-data/instance-type)

            I'm on Availability Zone $(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)" > /var/www/html/index.html

            service httpd start
  DhonaWebAppELBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ELB
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      VpcId:
        Ref: VPC
  DhonaWebAppEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EC2 ASG
      SecurityGroupIngress:
        - SourceSecurityGroupId:
            Ref: DhonaWebAppELBSecurityGroup
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      VpcId:
        Ref: VPC
  DhonaWebAppLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    DependsOn: InternetGatewayAttachment
    Properties:
      SecurityGroups:
        - Ref: DhonaWebAppELBSecurityGroup
      Subnets:
        - Ref: PublicSubnet1
        - Ref: PublicSubnet2
        - Ref: PublicSubnet3
  DhonaWebAppTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: "80"
      Protocol: HTTP
      VpcId:
        Ref: VPC
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn:
        Ref: DhonaWebAppLoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn:
            Ref: DhonaWebAppTargetGroup

Outputs:
  URL:
    Value: !Join
      - ''
      - - 'http://'
        - !GetAtt
          - DhonaWebAppLoadBalancer
          - DNSName
```

Let's create the CloudFormation stack!

```bash
$ aws cloudformation create-stack --stack-name cfn-asg --template-body file://cfn-asg.yaml
StackId: arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-asg/438781a0-1e7e-11ed-b1ba-06ff26b14cdc
$ aws cloudformation describe-stacks --stack-name cfn-asg | grep "OutputValue\|StackStatus"
    OutputValue: http://cfn-a-Dhona-1NM3JA9AKJ0BE-1240836435.ap-southeast-3.elb.amazonaws.com
  StackStatus: CREATE_COMPLETE
```

## Main Points

**1\. Use Mixed Instances (On-Demand + Spot) To Optimize Cost**

```yaml
      MixedInstancesPolicy:
        InstancesDistribution:
          OnDemandAllocationStrategy: prioritized
          OnDemandPercentageAboveBaseCapacity: 30
          SpotAllocationStrategy: capacity-optimized
          SpotMaxPrice: 0.003
```

If your workload can be interrupted (the process can be repeated when it fails), you can use spot instances. Why? Because you will get a big discount instead of using On-Demand. Here's the comparison:

![Pricing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mbkgsn1vhkw2s6i2ibt.png)

As you can see, it's almost 70% compared to the On-Demand instance but one thing you should know is it can be interrupted when your bid price is lower than the actual price. That's why you should know which workloads are suitable for this instance type.

Then here I use 5 instance types based on priority:

```yaml
          Overrides:
            - InstanceType:
                Ref: SpotInstanceType1 (t3.large)
            - InstanceType:
                Ref: SpotInstanceType2 (t3.medium)
            - InstanceType:
                Ref: SpotInstanceType3 (t3.small)
            - InstanceType:
                Ref: SpotInstanceType4 (t3.micro)
            - InstanceType:
                Ref: SpotInstanceType5 (t3.nano)
```

Pricing:

![Priority](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/labqgjkp9ii7c04mnpuj.png)

From the details above, I choose to use:

* On-demand instance type based on top priority (t3.large) with percentage 30 which means 30% on-demand and 70% for spot instances. So if I use desired and minimum capacity 3, 1 on-demand instance and 2 spot instances should be running.
    
* Spot instances with a maximum price is 0.003 and based on all instance types I used, t3.nano meets the criteria.
    

```bash
$ aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'Reservations[].Instances[].{ID:InstanceId}'
- ID: i-02323c2a2be9759f2
- ID: i-004b63ceec33e2bb5
- ID: i-0b11803cfd9e623bb
```

From the settings, the first creation of this architecture is going to be:

![ASG 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvxdiixyhjxl851at6sy.png)

![Output 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dl8qat51lpv5zzui43dd.png)

**2\. Activate Capacity Rebalance To Ensure Availability**

It helps you to create a new spot instance based on the EC2 instance rebalance recommendation two minutes before your current spot instance is interrupted.

```yaml
      CapacityRebalance: true
```

For more information, click [here](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-capacity-rebalancing.html)!

**3\. Use Launch Template Instead of Launch Configuration**

![Launch Template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ce2ocimmpj6rzcwcz0n5.png)

For more information, click [here](https://aws.amazon.com/blogs/compute/amazon-ec2-auto-scaling-will-no-longer-add-support-for-new-ec2-features-to-launch-configurations/).

Alright! Those are all the main points. Let's play some scenarios so we can understand them better!

**1\. Change Maximum Spot Price (Increase)**

On the first change, we will increase the maximum spot price and terminate one spot instance to see which instance type will be chosen.

Note: You can make a change to the same template directly. Here I create a new template to show you the difference between them.

```bash
$ diff cfn-asg.yaml cfn-asg-2.yaml 
145c145
<           SpotMaxPrice: 0.003
---
>           SpotMaxPrice: 0.01
```

Update the stack:

```bash
$ aws cloudformation update-stack --stack-name cfn-asg --template-body file://cfn-asg-2.yaml
StackId: arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-asg/438781a0-1e7e-11ed-b1ba-06ff26b14cdc
$ aws cloudformation describe-stacks --stack-name cfn-asg | grep "OutputValue\|StackStatus"
    OutputValue: http://cfn-a-Dhona-1NM3JA9AKJ0BE-1240836435.ap-southeast-3.elb.amazonaws.com
  StackStatus: UPDATE_COMPLETE
```

Terminate one spot instance:

```bash
$ aws ec2 describe-spot-instance-requests --query "SpotInstanceRequests[*].{ID:InstanceId}"
- ID: i-0b11803cfd9e623bb
- ID: i-02323c2a2be9759f2
$ aws ec2 terminate-instances --instance-ids i-02323c2a2be9759f2
TerminatingInstances:
- CurrentState:
    Code: 32
    Name: shutting-down
  InstanceId: i-02323c2a2be9759f2
  PreviousState:
    Code: 16
    Name: running
```

Here's the output when it's considered as UNHEALTHY and replaced with the new instance:

```yaml
- AutoScalingGroupName: cfn-asg-DhonaWebAppAutoScalingGroup-NQBH0P5D9DNF
  AvailabilityZone: ap-southeast-3a
  HealthStatus: HEALTHY
  InstanceId: i-00c3754aed2929ad9
  InstanceType: t3.small
  LaunchTemplate:
    LaunchTemplateId: lt-0ab4b34b94589f554
    LaunchTemplateName: DhonaWebAppLaunchTemplate_6Nl7i61kH5qJ
    Version: '1'
  LifecycleState: InService
  ProtectedFromScaleIn: false
- AutoScalingGroupName: cfn-asg-DhonaWebAppAutoScalingGroup-NQBH0P5D9DNF
  AvailabilityZone: ap-southeast-3a
  HealthStatus: UNHEALTHY
  InstanceId: i-02323c2a2be9759f2
  InstanceType: t3.nano
  LaunchTemplate:
    LaunchTemplateId: lt-0ab4b34b94589f554
    LaunchTemplateName: DhonaWebAppLaunchTemplate_6Nl7i61kH5qJ
    Version: '1'
  LifecycleState: Terminating
  ProtectedFromScaleIn: false
```

The instance type is t3.small based on the best match with the maximum spot price from the priority.

![ASG 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uhwskj6mwn0plxeo20s0.png)

![Output 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u0vh1c80p97nnhs0c9w5.png)

But what if we terminate the on-demand instance? Will the instance type be changed? The answer is No because the maximum spot price doesn't affect the on-demand setting.

```bash
$ aws ec2 terminate-instances --instance-ids i-004b63ceec33e2bb5
TerminatingInstances:
- CurrentState:
    Code: 32
    Name: shutting-down
  InstanceId: i-004b63ceec33e2bb5
  PreviousState:
    Code: 16
    Name: running
```

![ASG 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fop77a1g79wwjlokwxoj.png)

![Output 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xoiu2nqw9ynajct3siv.png)

**2\. Change The On-Demand Allocation Strategy**

By changing it to the `lowest-price`, it will ignore the priority and choose the lowest price of all available instance types. In this case, it's `t3.nano`.

```bash
$ diff cfn-asg-2.yaml cfn-asg-3.yaml
142c142
<           OnDemandAllocationStrategy: prioritized
---
>           OnDemandAllocationStrategy: lowest-price
```

Update the stack:

```bash
$ aws cloudformation update-stack --stack-name cfn-asg --template-body file://cfn-asg-3.yaml
StackId: arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-asg/438781a0-1e7e-11ed-b1ba-06ff26b14cdc
```

Terminate the on-demand instance:

```bash
$ aws ec2 terminate-instances --instance-ids i-018469de2746c9432
TerminatingInstances:
- CurrentState:
    Code: 32
    Name: shutting-down
  InstanceId: i-018469de2746c9432
  PreviousState:
    Code: 16
    Name: running
```

As we expected, the new on-demand instance type is `t3.nano`.

```yaml
- AutoScalingGroupName: cfn-asg-DhonaWebAppAutoScalingGroup-NQBH0P5D9DNF
  AvailabilityZone: ap-southeast-3b
  HealthStatus: HEALTHY
  InstanceId: i-0f3ab2d7fa671a597
  InstanceType: t3.nano
  LaunchTemplate:
    LaunchTemplateId: lt-0ab4b34b94589f554
    LaunchTemplateName: DhonaWebAppLaunchTemplate_6Nl7i61kH5qJ
    Version: '1'
  LifecycleState: InService
  ProtectedFromScaleIn: false
```

![Output 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fx2h0obrsvlbd7zly46i.png)

**3\. Make a Change To Launch Template (Create Version)**

We have set to use the latest version of the launch template when there is a change, right? So now we will try to prove it.

```yaml
        LaunchTemplate:
          LaunchTemplateSpecification:
            LaunchTemplateId:
              Ref: DhonaWebAppLaunchTemplate
            Version:
              Fn::GetAtt:
                - DhonaWebAppLaunchTemplate
                - LatestVersionNumber
```

```bash
$ diff cfn-asg-3.yaml cfn-asg-4.yaml
193c193,195
<             I'm on Availability Zone $(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)" > /var/www/html/index.html
---
>             I'm on Availability Zone $(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
> 
>             This is additional line for updating launch template" > /var/www/html/index.html
```

Update the stack:

```bash
$ aws cloudformation update-stack --stack-name cfn-asg --template-body file://cfn-asg-4.yaml
StackId: arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-asg/438781a0-1e7e-11ed-b1ba-06ff26b14cdc
```

Then, let's terminate one spot instance again!

```bash
$ aws ec2 describe-spot-instance-requests --filters Name=state,Values=active --query "SpotInstanceRequests[*].{ID:InstanceId}"
- ID: i-00c3754aed2929ad9
- ID: i-0b11803cfd9e623bb
$ aws ec2 terminate-instances --instance-ids i-0b11803cfd9e623bb
TerminatingInstances:
- CurrentState:
    Code: 32
    Name: shutting-down
  InstanceId: i-0b11803cfd9e623bb
  PreviousState:
    Code: 16
    Name: running
```

We're still using the same Auto Scaling Group with the right capacity but in a different version of the launch template. In this case, it's the latest. So that's why we are suggested to use a launch template instead of a launch configuration. By using a launch configuration, we have to create a new one when we need to make a change.

Now, the latest architecture is:

![ASG 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ow6ccg1l650fqyyfygub.png)

Here's the output!

![Output 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mq6yvo2d97dym3mnxgv3.png)

Alright! Those are all the changes we have made to the architecture. Please note that all the changes will be applied to the new instances, not to the current running instances. So for example when one instance is interrupted, the changes will be applied to the new instance created.

## Cleanup

If you already followed all the instructions above, you can remove all of them only by deleting the stack. That's why we use CloudFormation, just to make it simple :)

```bash
$ aws cloudformation delete-stack --stack-name cfn-asg
$ aws autoscaling describe-auto-scaling-groups
AutoScalingGroups: []
$ aws ec2 describe-spot-instance-requests --filters Name=state,Values=active
SpotInstanceRequests: []
$ aws ec2 describe-instances --filters Name=instance-state-name,Values=running
Reservations: []
$ aws cloudformation describe-stacks --stack-name cfn-asg

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id cfn-asg does not exist
$
```

In this post, I just show you some features that you can use with Auto Scaling. This is not the all features Auto Scaling has. You can mix it with Reserved Instance, use dynamic scaling, and many more. Here I'm not doing it all because this is just a "personal" demo, I don't need too big resources for very high workloads or long-term subscriptions.

That's it for now! Thank you for coming and I'm looking forward to your feedback. Follow me to get notified when my new post is published!