diff --git a/.changelog/23187.txt b/.changelog/23187.txt new file mode 100644 index 000000000000..08ebac55a08a --- /dev/null +++ b/.changelog/23187.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_group: Disable scale-in protection before draining instances +``` \ No newline at end of file diff --git a/internal/service/autoscaling/group.go b/internal/service/autoscaling/group.go index 2789552436f3..6091a18687fc 100644 --- a/internal/service/autoscaling/group.go +++ b/internal/service/autoscaling/group.go @@ -1654,6 +1654,18 @@ func resourceGroupDrain(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting capacity to zero to drain: %s", err) } + // Next, ensure that instances are not prevented from scaling in. + // + // The ASG's own scale-in protection setting doesn't make a difference here, + // as it only affects new instances, which won't be launched now that the + // desired capacity is set to 0. There is also the possibility that this ASG + // no longer applies scale-in protection to new instances, but there's still + // old ones that have it. + log.Printf("[DEBUG] Disabling scale-in protection for all instances in the group") + if err := disableASGScaleInProtections(d, conn); err != nil { + return fmt.Errorf("Error disabling scale-in protection for all instances: %s", err) + } + // Next, wait for the Auto Scaling Group to drain log.Printf("[DEBUG] Waiting for group to have zero instances") var g *autoscaling.Group @@ -2365,3 +2377,40 @@ func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecificat return result } + +// disableASGScaleInProtections disables scale-in protection for all instances +// in the given Auto-Scaling Group. +func disableASGScaleInProtections(d *schema.ResourceData, conn *autoscaling.AutoScaling) error { + g, err := getGroup(d.Id(), conn) + if err != nil { + return fmt.Errorf("Error getting group %s: %s", d.Id(), err) + } + + var instanceIds []string + for _, instance := range g.Instances { + if aws.BoolValue(instance.ProtectedFromScaleIn) { + instanceIds = append(instanceIds, aws.StringValue(instance.InstanceId)) + } + } + + const chunkSize = 50 // API limit + + for i := 0; i < len(instanceIds); i += chunkSize { + j := i + chunkSize + if j > len(instanceIds) { + j = len(instanceIds) + } + + input := autoscaling.SetInstanceProtectionInput{ + AutoScalingGroupName: aws.String(d.Id()), + InstanceIds: aws.StringSlice(instanceIds[i:j]), + ProtectedFromScaleIn: aws.Bool(false), + } + + if _, err := conn.SetInstanceProtection(&input); err != nil { + return fmt.Errorf("Error disabling scale-in protections: %s", err) + } + } + + return nil +} diff --git a/internal/service/autoscaling/group_test.go b/internal/service/autoscaling/group_test.go index 9a73980219a0..e347e1249527 100644 --- a/internal/service/autoscaling/group_test.go +++ b/internal/service/autoscaling/group_test.go @@ -2306,6 +2306,33 @@ func TestAccAutoScalingGroup_launchTempPartitionNum(t *testing.T) { }) } +func TestAccAutoScalingGroup_Destroy_whenProtectedFromScaleIn(t *testing.T) { + var group autoscaling.Group + rName := fmt.Sprintf("terraform-test-%s", sdkacctest.RandString(10)) + resourceName := "aws_autoscaling_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, autoscaling.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGroupConfig_DestroyWhenProtectedFromScaleIn_beforeDestroy(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(resourceName, &group), + testAccCheckGroupHealthyCapacity(&group, 2), + resource.TestCheckResourceAttr(resourceName, "protect_from_scale_in", "true"), + ), + }, + { + Config: testAccGroupConfig_DestroyWhenProtectedFromScaleIn_afterDestroy(), + // Reaching this step is good enough, as it indicates the ASG was destroyed successfully. + }, + }, + }) +} + func testAccGroupNameGeneratedConfig() string { return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptInDefaultExclude(), @@ -4776,6 +4803,56 @@ resource "aws_autoscaling_group" "test" { ` } +func testAccGroupConfig_DestroyWhenProtectedFromScaleIn_beforeDestroy(name string) string { + return acctest.ConfigAvailableAZsNoOptInDefaultExclude() + + fmt.Sprintf(` +data "aws_ami" "test" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn-ami-hvm-*-x86_64-gp2"] + } +} + +resource "aws_launch_configuration" "test" { + image_id = data.aws_ami.test.id + instance_type = "t3.micro" +} + +resource "aws_autoscaling_group" "test" { + availability_zones = [data.aws_availability_zones.available.names[0]] + name = %[1]q + max_size = 2 + min_size = 2 + desired_capacity = 2 + protect_from_scale_in = true + launch_configuration = aws_launch_configuration.test.name +} +`, name) +} + +func testAccGroupConfig_DestroyWhenProtectedFromScaleIn_afterDestroy() string { + return acctest.ConfigAvailableAZsNoOptInDefaultExclude() + + ` +data "aws_ami" "test" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn-ami-hvm-*-x86_64-gp2"] + } +} + +resource "aws_launch_configuration" "test" { + image_id = data.aws_ami.test.id + instance_type = "t3.micro" +} +` +} + func testAccCheckAutoScalingInstanceRefreshCount(group *autoscaling.Group, expected int) resource.TestCheckFunc { return func(state *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).AutoScalingConn