19

I have an AWS Auto-Scaling Group, a Launch Configuration, and an Auto-Scaling Group Policy defined in Terraform like this:

resource "aws_autoscaling_group" "default" {
  name = "..."

  health_check_type = "EC2"
  vpc_zone_identifier = ["${...}"]

  min_size = "${var.asg_capacity}"
  max_size = "${var.asg_capacity * 2}"
  desired_capacity = "${var.asg_capacity}"

  launch_configuration = "${aws_launch_configuration.default.id}"

  termination_policies = ["OldestInstance"]
}

resource "aws_autoscaling_policy" "default" {
  name = "..."
  autoscaling_group_name = "${aws_autoscaling_group.default.name}"

  scaling_adjustment = "${var.asg_capacity}"
  adjustment_type = "ChangeInCapacity"
  cooldown = 300
}

resource "aws_launch_configuration" "default" {
  name_prefix = "..._"

  image_id = "${var.coreos_ami_id}"
  instance_type = "${var.ec2_instance_type}"
  iam_instance_profile = "${aws_iam_instance_profile.default.arn}"
  key_name = "..."

  security_groups = ["${aws_security_group.default.id}"]

  user_data = "${data.template_file.cloud_init.rendered}"

  lifecycle {
    create_before_destroy = true
  }
}

When I change my user data, a new launch configuration is created and then attached to the auto-scaling group. I would assume that this would cause the auto-scaling group to scale up by var.asg_capacity instances, wait 300 seconds, and then tear down the old ones as per OldestInstance.

When I have done similar things in CloudFormation, I have used the following configuration options:

ASG:
  Type: AWS::AutoScaling::AutoScalingGroup
  UpdatePolicy:
    AutoScaleRollingUpdate:
      # during a scale, 6 instances in service
      MaxBatchSize: 3
      MinInstancesInService: 3
      PauseTime: PT5M
  Properties:
    ...

Is there an analog for this in Terraform? I would really like my auto-scaling groups to change when I change the launch configuration.

Naftuli Kay
  • 87,710
  • 93
  • 269
  • 411

2 Answers2

25

I would assume that this would cause the auto-scaling group to scale up by var.asg_capacity instances, wait 300 seconds, and then tear down the old ones as per OldestInstance.

This assumption, unfortunately, is incorrect. When you change the launch configuration, the only thing that happens is a new launch configuration is created in your AWS account and associated with the Auto Scaling Group (ASG). That means all future Instances in that ASG will launch with the new launch configuration. However, merely changing the launch configuration does not trigger the launch of any instances, so you won't see your changes.

To force new instances to launch, you have two options:

Updated answer from 2022

In 2020, AWS introduced AWS instance refresh, which is a native way to roll out changes to Auto Scaling Groups. If you configure the instance_refresh block in your aws_autoscaling_group resource, then when you make a change to the launch configuration and run apply, AWS will kick off the instance refresh process automatically (note: this process runs in the background, so apply will complete very quickly, but the refresh will happen after, often taking 5-30 min).

resource "aws_launch_configuration" "example" {
  image_id        = var.ami
  instance_type   = var.instance_type

  user_data = data.template_file.user_data.rendered

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "example" {
  name                 = "example-asg"
  launch_configuration = aws_launch_configuration.example.id
  availability_zones   = data.aws_availability_zones.all.names

  min_size         = var.min_size
  max_size         = var.max_size

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 50
    }
  }
}

Original answer from 2016

You can use some native Terraform functionality to do a rolling deploy:

  1. Configure the name parameter of the ASG to depend directly on the name of the launch configuration. That way, each time the launch configuration changes (which it will when you update the AMI or User Data), Terraform will try to replace the ASG.
  2. Set the create_before_destroy parameter of the ASG to true, so each time Terraform tries to replace it, it will create the replacement before destroying the original.
  3. Set the min_elb_capacity parameter of the ASG to the min_size of the cluster so that Terraform will wait for at least that many servers from the new ASG to register in the ELB before it'll start destroying the original ASG.

Here's a rough idea of what the Terraform code would look like:

resource "aws_launch_configuration" "example" {
  image_id        = "${var.ami}"
  instance_type   = "${var.instance_type}"

  user_data = "${data.template_file.user_data.rendered}"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "example" {
  name                 = "${var.cluster_name}-${aws_launch_configuration.example.name}"
  launch_configuration = "${aws_launch_configuration.example.id}"
  availability_zones   = ["${data.aws_availability_zones.all.names}"]

  min_size         = "${var.min_size}"
  max_size         = "${var.max_size}"
  min_elb_capacity = "${var.min_size}"

  lifecycle {
    create_before_destroy = true
  }
}

For a fully working example, check out the zero-downtime deployment example code from the book Terraform: Up & Running.

Yevgeniy Brikman
  • 8,711
  • 6
  • 46
  • 60
  • Thank you for your answer. It is astounding that Terraform still does not provide a built-in way to do this without spinning up and tearing down an auto-scaling group on every provision. – Naftuli Kay Dec 06 '16 at 01:36
  • 2
    @NaftuliKay Yes, unfortunately, this is still an open issue in the Terraform community: https://github.com/hashicorp/terraform/issues/1552. Using the `create_before_destroy` approach above is, for now, your best solution with purely Terraform code. It's close to a blue/green deployment, so it's not a bad option, but it doesn't work for everyone and, critically, it does NOT work with dynamically sized ASGs. – Yevgeniy Brikman Dec 06 '16 at 15:13
  • The other possible approach is to put the ASG into a CloudFormation template wrapped within an overarching Terraform configuration, and allow it to handle the rolling update to each host since Terraform isn't capable of doing it natively. @Yevgeniy Brikman suggested this approach in https://github.com/hashicorp/terraform/issues/1552#issuecomment-190864512. – Tim Sep 03 '18 at 17:46
  • As a bare minimum, wouldn't it be enough to update the autoscaling group's name with the launch configuration's name to trigger the entire redeploy flow? – Patrik Iselind Nov 12 '18 at 13:07
  • 1
    @PatrikIselind: Sure, but then the original ASG would be deleted _before_ the new one is created, so you'd have downtime in between. – Yevgeniy Brikman Nov 13 '18 at 00:14
  • 1
    If `create_before_destroy = true` is set, the original ASG is not deleted before. You could even add `wait_for_elb_capacity = 1` to ensure a new instance is found healthy in the ELB before destroying the old ASG. – user2707671 Mar 03 '19 at 00:40
  • I'm new to this and a little confused with regard to "min_elb_capacity". What if you are using an autoscaling group with no ELB? – openCivilisation Jun 12 '22 at 08:34
  • If you weren't using an ELB, then the rolling deploy approach above doesn't work very well, as you don't have a proper "health check" mechanism to rely on. However, in 2020, AWS added support for [instance refresh](https://aws.amazon.com/blogs/compute/introducing-instance-refresh-for-ec2-auto-scaling/), which is a native way to roll out changes to ASGs. I've updated my answer above with this new approach, which works both with and without ELBs. – Yevgeniy Brikman Jun 13 '22 at 09:10
4

As of version 3.22.0 of the AWS provisioner, you can add an instance_refresh configuration block to your aws_autoscaling_group resource. The simplest config that can possibly work is:

instance_refresh {
  strategy = "Rolling"
}
James_pic
  • 3,240
  • 19
  • 24