Repository: cloud-custodian/cloud-custodian

Issue:
aws.asg - allow setting MaxInstanceLifetime
**Is your feature request related to a problem? Please describe.**
I'd like to set the [MaxInstanceLifetime](https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-max-instance-lifetime.html) of ASGs.

**Describe the solution you'd like**
We should add a generic ASG action that allows us to update whatever ASG settings are needed.

**Describe alternatives you've considered**
Could create a specific action for this one setting, but that seems short-sighted.

**Additional context**
I'd propose we allow something like this (looking at what [`update_auto_scaling_groups`](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/autoscaling.html#AutoScaling.Client.update_auto_scaling_group) supports):
```
name: set-asg-instance-lifetime
resource: aws.asg
filter:
  - MaxInstanceLifetime: empty
action:
  - type: update
    max-instance-lifetime: 604800  # (7 days)
```
There are probably only a handful of ASG settings that _should_ be allowed to get updated here, but it's probably more than 1, so it seems useful to allow several in one action.  I could see allowing any of the following:
```
  - type: update
    default-cooldown: 600
    max-instance-lifetime: 604800
    new-instances-protected-from-scale-in: true
    capacity-rebalance: true
```


Discussion:
that sounds great, re using new native capability, looks like thats from nov 2020 reinvent. 
https://awsapichanges.info/archive/changes/3ace65-autoscaling.html

[update] actually thats a different replacement mechanism, the lifetime one is simpler and dates back to reinvent 2019
https://awsapichanges.info/archive/changes/d2cf1a-autoscaling.html


Resolving patch:
diff --git a/c7n/resources/asg.py b/c7n/resources/asg.py
--- a/c7n/resources/asg.py
+++ b/c7n/resources/asg.py
@@ -1659,6 +1659,93 @@ def process_asg(self, client, asg):
             raise
 
 
+@ASG.action_registry.register('update')
+class Update(Action):
+    """Action to update ASG configuration settings
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: set-asg-instance-lifetime
+                resource: asg
+                filters:
+                  - MaxInstanceLifetime: empty
+                actions:
+                  - type: update
+                    max-instance-lifetime: 604800  # (7 days)
+
+              - name: set-asg-by-policy
+                resource: asg
+                actions:
+                  - type: update
+                    default-cooldown: 600
+                    max-instance-lifetime: 0      # (clear it)
+                    new-instances-protected-from-scale-in: true
+                    capacity-rebalance: true
+    """
+
+    schema = type_schema(
+        'update',
+        **{
+            'default-cooldown': {'type': 'integer', 'minimum': 0},
+            'max-instance-lifetime': {
+                "anyOf": [
+                    {'enum': [0]},
+                    {'type': 'integer', 'minimum': 86400}
+                ]
+            },
+            'new-instances-protected-from-scale-in': {'type': 'boolean'},
+            'capacity-rebalance': {'type': 'boolean'},
+        }
+    )
+    permissions = ("autoscaling:UpdateAutoScalingGroup",)
+    settings_map = {
+        "default-cooldown": "DefaultCooldown",
+        "max-instance-lifetime": "MaxInstanceLifetime",
+        "new-instances-protected-from-scale-in": "NewInstancesProtectedFromScaleIn",
+        "capacity-rebalance": "CapacityRebalance"
+    }
+
+    def validate(self):
+        if not set(self.settings_map).intersection(set(self.data)):
+            raise PolicyValidationError(
+                "At least one setting must be specified from: " +
+                ", ".join(sorted(self.settings_map))
+            )
+        return self
+
+    def process(self, asgs):
+        client = local_session(self.manager.session_factory).client('autoscaling')
+
+        settings = {}
+        for k, v in self.settings_map.items():
+            if k in self.data:
+                settings[v] = self.data.get(k)
+
+        with self.executor_factory(max_workers=2) as w:
+            futures = {}
+            error = None
+            for a in asgs:
+                futures[w.submit(self.process_asg, client, a, settings)] = a
+            for f in as_completed(futures):
+                if f.exception():
+                    self.log.error("Error while updating asg:%s error:%s" % (
+                        futures[f]['AutoScalingGroupName'],
+                        f.exception()))
+                    error = f.exception()
+            if error:
+                # make sure we stop policy execution if there were errors
+                raise error
+
+    def process_asg(self, client, asg, settings):
+        self.manager.retry(
+            client.update_auto_scaling_group,
+            AutoScalingGroupName=asg['AutoScalingGroupName'],
+            **settings)
+
+
 @resources.register('launch-config')
 class LaunchConfig(query.QueryResourceManager):
 


Test patch:
diff --git a/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_1.json b/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_1.json
@@ -0,0 +1,84 @@
+{
+    "status_code": 200,
+    "data": {
+        "AutoScalingGroups": [
+            {
+                "AutoScalingGroupName": "tf-asg-20210406161729306300000003",
+                "AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:644160558196:autoScalingGroup:f92d4ea4-ba11-42ab-8c34-696ed8047010:autoScalingGroupName/tf-asg-20210406161729306300000003",
+                "LaunchTemplate": {
+                    "LaunchTemplateId": "lt-007e80764a20763ae",
+                    "LaunchTemplateName": "foobar20210406161728176600000001",
+                    "Version": "$Latest"
+                },
+                "MinSize": 1,
+                "MaxSize": 1,
+                "DesiredCapacity": 1,
+                "DefaultCooldown": 300,
+                "AvailabilityZones": [
+                    "us-west-2a"
+                ],
+                "LoadBalancerNames": [],
+                "TargetGroupARNs": [],
+                "HealthCheckType": "EC2",
+                "HealthCheckGracePeriod": 300,
+                "Instances": [
+                    {
+                        "InstanceId": "i-0843feb694ac159c7",
+                        "InstanceType": "t3.micro",
+                        "AvailabilityZone": "us-west-2a",
+                        "LifecycleState": "InService",
+                        "HealthStatus": "Healthy",
+                        "LaunchTemplate": {
+                            "LaunchTemplateId": "lt-007e80764a20763ae",
+                            "LaunchTemplateName": "foobar20210406161728176600000001",
+                            "Version": "1"
+                        },
+                        "ProtectedFromScaleIn": false
+                    }
+                ],
+                "CreatedTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 4,
+                    "day": 6,
+                    "hour": 16,
+                    "minute": 17,
+                    "second": 30,
+                    "microsecond": 696000
+                },
+                "SuspendedProcesses": [],
+                "VPCZoneIdentifier": "",
+                "EnabledMetrics": [],
+                "Tags": [
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "App",
+                        "Value": "Testing",
+                        "PropagateAtLaunch": true
+                    },
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "Creator",
+                        "Value": "tstansell",
+                        "PropagateAtLaunch": false
+                    },
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "c7n_status",
+                        "Value": "AutoScaleGroup does not meet org policy: suspend@2021/04/06 1717 UTC",
+                        "PropagateAtLaunch": false
+                    }
+                ],
+                "TerminationPolicies": [
+                    "Default"
+                ],
+                "NewInstancesProtectedFromScaleIn": false,
+                "ServiceLinkedRoleARN": "arn:aws:iam::644160558196:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_2.json b/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_aws_asg_update/autoscaling.DescribeAutoScalingGroups_2.json
@@ -0,0 +1,86 @@
+{
+    "status_code": 200,
+    "data": {
+        "AutoScalingGroups": [
+            {
+                "AutoScalingGroupName": "tf-asg-20210406161729306300000003",
+                "AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:644160558196:autoScalingGroup:f92d4ea4-ba11-42ab-8c34-696ed8047010:autoScalingGroupName/tf-asg-20210406161729306300000003",
+                "LaunchTemplate": {
+                    "LaunchTemplateId": "lt-007e80764a20763ae",
+                    "LaunchTemplateName": "foobar20210406161728176600000001",
+                    "Version": "$Latest"
+                },
+                "MinSize": 1,
+                "MaxSize": 1,
+                "DesiredCapacity": 1,
+                "DefaultCooldown": 600,
+                "AvailabilityZones": [
+                    "us-west-2a"
+                ],
+                "LoadBalancerNames": [],
+                "TargetGroupARNs": [],
+                "HealthCheckType": "EC2",
+                "HealthCheckGracePeriod": 300,
+                "Instances": [
+                    {
+                        "InstanceId": "i-0843feb694ac159c7",
+                        "InstanceType": "t3.micro",
+                        "AvailabilityZone": "us-west-2a",
+                        "LifecycleState": "InService",
+                        "HealthStatus": "Healthy",
+                        "LaunchTemplate": {
+                            "LaunchTemplateId": "lt-007e80764a20763ae",
+                            "LaunchTemplateName": "foobar20210406161728176600000001",
+                            "Version": "1"
+                        },
+                        "ProtectedFromScaleIn": false
+                    }
+                ],
+                "CreatedTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 4,
+                    "day": 6,
+                    "hour": 16,
+                    "minute": 17,
+                    "second": 30,
+                    "microsecond": 696000
+                },
+                "SuspendedProcesses": [],
+                "VPCZoneIdentifier": "",
+                "EnabledMetrics": [],
+                "Tags": [
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "App",
+                        "Value": "Testing",
+                        "PropagateAtLaunch": true
+                    },
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "Creator",
+                        "Value": "tstansell",
+                        "PropagateAtLaunch": false
+                    },
+                    {
+                        "ResourceId": "tf-asg-20210406161729306300000003",
+                        "ResourceType": "auto-scaling-group",
+                        "Key": "c7n_status",
+                        "Value": "AutoScaleGroup does not meet org policy: suspend@2021/04/06 1717 UTC",
+                        "PropagateAtLaunch": false
+                    }
+                ],
+                "TerminationPolicies": [
+                    "Default"
+                ],
+                "NewInstancesProtectedFromScaleIn": true,
+                "ServiceLinkedRoleARN": "arn:aws:iam::644160558196:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
+                "MaxInstanceLifetime": 604800,
+                "CapacityRebalance": true
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_aws_asg_update/autoscaling.UpdateAutoScalingGroup_1.json b/tests/data/placebo/test_aws_asg_update/autoscaling.UpdateAutoScalingGroup_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_aws_asg_update/autoscaling.UpdateAutoScalingGroup_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/terraform/aws_asg_update/main.tf b/tests/terraform/aws_asg_update/main.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/aws_asg_update/main.tf
@@ -0,0 +1,47 @@
+data "aws_ami" "ubuntu" {
+  most_recent = true
+
+  filter {
+    name   = "name"
+    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
+  }
+
+  filter {
+    name   = "virtualization-type"
+    values = ["hvm"]
+  }
+
+  owners = ["099720109477"] # Canonical
+}
+
+data "aws_availability_zones" "available" {
+  state = "available"
+}
+
+resource "aws_launch_template" "foobar" {
+  name_prefix   = "foobar"
+  image_id      = data.aws_ami.ubuntu.id
+  instance_type = "t3.micro"
+
+  metadata_options {
+    http_endpoint = "enabled"
+    http_tokens   = "required"
+  }
+}
+
+resource "aws_autoscaling_group" "bar" {
+  availability_zones = [data.aws_availability_zones.available.names[0]]
+  desired_capacity   = 1
+  max_size           = 1
+  min_size           = 1
+
+  launch_template {
+    id      = aws_launch_template.foobar.id
+    version = "$Latest"
+  }
+  tag {
+    key                 = "App"
+    value               = "Testing"
+    propagate_at_launch = true
+  }
+}
diff --git a/tests/terraform/aws_asg_update/tf_resources.json b/tests/terraform/aws_asg_update/tf_resources.json
new file mode 100644
--- /dev/null
+++ b/tests/terraform/aws_asg_update/tf_resources.json
@@ -0,0 +1,213 @@
+{
+    "pytest-terraform": 1,
+    "outputs": {},
+    "resources": {
+        "aws_ami": {
+            "ubuntu": {
+                "architecture": "x86_64",
+                "arn": "arn:aws:ec2:us-west-2::image/ami-0a62a78cfedc09d76",
+                "block_device_mappings": [
+                    {
+                        "device_name": "/dev/sda1",
+                        "ebs": {
+                            "delete_on_termination": "true",
+                            "encrypted": "false",
+                            "iops": "0",
+                            "snapshot_id": "snap-0eaef1b981c163e61",
+                            "throughput": "0",
+                            "volume_size": "8",
+                            "volume_type": "gp2"
+                        },
+                        "no_device": "",
+                        "virtual_name": ""
+                    },
+                    {
+                        "device_name": "/dev/sdb",
+                        "ebs": {},
+                        "no_device": "",
+                        "virtual_name": "ephemeral0"
+                    },
+                    {
+                        "device_name": "/dev/sdc",
+                        "ebs": {},
+                        "no_device": "",
+                        "virtual_name": "ephemeral1"
+                    }
+                ],
+                "creation_date": "2021-03-25T23:31:16.000Z",
+                "description": "Canonical, Ubuntu, 20.04 LTS, amd64 focal image build on 2021-03-25",
+                "ena_support": true,
+                "executable_users": null,
+                "filter": [
+                    {
+                        "name": "name",
+                        "values": [
+                            "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
+                        ]
+                    },
+                    {
+                        "name": "virtualization-type",
+                        "values": [
+                            "hvm"
+                        ]
+                    }
+                ],
+                "hypervisor": "xen",
+                "id": "ami-0a62a78cfedc09d76",
+                "image_id": "ami-0a62a78cfedc09d76",
+                "image_location": "644160558196/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210325",
+                "image_owner_alias": null,
+                "image_type": "machine",
+                "kernel_id": null,
+                "most_recent": true,
+                "name": "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210325",
+                "name_regex": null,
+                "owner_id": "644160558196",
+                "owners": [
+                    "644160558196"
+                ],
+                "platform": null,
+                "platform_details": "Linux/UNIX",
+                "product_codes": [],
+                "public": true,
+                "ramdisk_id": null,
+                "root_device_name": "/dev/sda1",
+                "root_device_type": "ebs",
+                "root_snapshot_id": "snap-0eaef1b981c163e61",
+                "sriov_net_support": "simple",
+                "state": "available",
+                "state_reason": {
+                    "code": "UNSET",
+                    "message": "UNSET"
+                },
+                "tags": {},
+                "usage_operation": "RunInstances",
+                "virtualization_type": "hvm"
+            }
+        },
+        "aws_availability_zones": {
+            "available": {
+                "all_availability_zones": null,
+                "exclude_names": null,
+                "exclude_zone_ids": null,
+                "filter": null,
+                "group_names": [
+                    "us-west-2"
+                ],
+                "id": "us-west-2",
+                "names": [
+                    "us-west-2a",
+                    "us-west-2b",
+                    "us-west-2c",
+                    "us-west-2d"
+                ],
+                "state": "available",
+                "zone_ids": [
+                    "usw2-az1",
+                    "usw2-az2",
+                    "usw2-az3",
+                    "usw2-az4"
+                ]
+            }
+        },
+        "aws_autoscaling_group": {
+            "bar": {
+                "arn": "arn:aws:autoscaling:us-west-2:644160558196:autoScalingGroup:f92d4ea4-ba11-42ab-8c34-696ed8047010:autoScalingGroupName/tf-asg-20210406161729306300000003",
+                "availability_zones": [
+                    "us-west-2a"
+                ],
+                "capacity_rebalance": false,
+                "default_cooldown": 300,
+                "desired_capacity": 1,
+                "enabled_metrics": null,
+                "force_delete": false,
+                "health_check_grace_period": 300,
+                "health_check_type": "EC2",
+                "id": "tf-asg-20210406161729306300000003",
+                "initial_lifecycle_hook": [],
+                "instance_refresh": [],
+                "launch_configuration": "",
+                "launch_template": [
+                    {
+                        "id": "lt-007e80764a20763ae",
+                        "name": "foobar20210406161728176600000001",
+                        "version": "$Latest"
+                    }
+                ],
+                "load_balancers": null,
+                "max_instance_lifetime": 0,
+                "max_size": 1,
+                "metrics_granularity": "1Minute",
+                "min_elb_capacity": null,
+                "min_size": 1,
+                "mixed_instances_policy": [],
+                "name": "tf-asg-20210406161729306300000003",
+                "name_prefix": null,
+                "placement_group": "",
+                "protect_from_scale_in": false,
+                "service_linked_role_arn": "arn:aws:iam::644160558196:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
+                "suspended_processes": null,
+                "tag": [
+                    {
+                        "key": "App",
+                        "propagate_at_launch": true,
+                        "value": "Testing"
+                    }
+                ],
+                "tags": null,
+                "target_group_arns": null,
+                "termination_policies": null,
+                "timeouts": null,
+                "vpc_zone_identifier": [],
+                "wait_for_capacity_timeout": "10m",
+                "wait_for_elb_capacity": null
+            }
+        },
+        "aws_launch_template": {
+            "foobar": {
+                "arn": "arn:aws:ec2:us-west-2:644160558196:launch-template/lt-007e80764a20763ae",
+                "block_device_mappings": [],
+                "capacity_reservation_specification": [],
+                "cpu_options": [],
+                "credit_specification": [],
+                "default_version": 1,
+                "description": "",
+                "disable_api_termination": false,
+                "ebs_optimized": "",
+                "elastic_gpu_specifications": [],
+                "elastic_inference_accelerator": [],
+                "enclave_options": [],
+                "hibernation_options": [],
+                "iam_instance_profile": [],
+                "id": "lt-007e80764a20763ae",
+                "image_id": "ami-0a62a78cfedc09d76",
+                "instance_initiated_shutdown_behavior": "",
+                "instance_market_options": [],
+                "instance_type": "t3.micro",
+                "kernel_id": "",
+                "key_name": "",
+                "latest_version": 1,
+                "license_specification": [],
+                "metadata_options": [
+                    {
+                        "http_endpoint": "enabled",
+                        "http_put_response_hop_limit": 0,
+                        "http_tokens": "required"
+                    }
+                ],
+                "monitoring": [],
+                "name": "foobar20210406161728176600000001",
+                "name_prefix": "foobar",
+                "network_interfaces": [],
+                "placement": [],
+                "ram_disk_id": "",
+                "security_group_names": null,
+                "tag_specifications": [],
+                "tags": null,
+                "update_default_version": null,
+                "user_data": "",
+                "vpc_security_group_ids": null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_asg.py b/tests/test_asg.py
--- a/tests/test_asg.py
+++ b/tests/test_asg.py
@@ -8,6 +8,7 @@
 
 from .common import BaseTest
 
+from c7n.exceptions import PolicyValidationError
 from c7n.resources.asg import LaunchInfo
 from c7n.resources.aws import shape_validate
 
@@ -131,6 +132,56 @@ def test_asg_propagate_tag_action(test, aws_asg):
     assert itags['Owner'] == 'Kapil'
 
 
+@terraform("aws_asg_update")
+def test_aws_asg_update(test, aws_asg_update):
+    factory = test.replay_flight_data("test_aws_asg_update")
+    p = test.load_policy(
+        {
+            "name": "asg-update",
+            "resource": "aws.asg",
+            "filters": [{
+                "AutoScalingGroupName": aws_asg_update["aws_autoscaling_group.bar.id"]
+            }],
+            "actions": [{
+                "type": "update",
+                "default-cooldown": 600,
+                "max-instance-lifetime": 604800,
+                "new-instances-protected-from-scale-in": True,
+                "capacity-rebalance": True,
+            }],
+        },
+        session_factory=factory,
+    )
+    resources = p.run()
+    test.assertEqual(len(resources), 1)
+
+    client = factory().client("autoscaling")
+    result = client.describe_auto_scaling_groups(
+        AutoScalingGroupNames=[resources[0]["AutoScalingGroupName"]]
+    )[
+        "AutoScalingGroups"
+    ].pop()
+    test.assertEqual(result["DefaultCooldown"], 600)
+    test.assertEqual(result["MaxInstanceLifetime"], 604800)
+    test.assertTrue(result["NewInstancesProtectedFromScaleIn"])
+    test.assertTrue(result["CapacityRebalance"])
+
+
+def test_aws_asg_update_no_settings(test):
+    factory = test.replay_flight_data("test_aws_asg_update")
+    with test.assertRaises(PolicyValidationError):
+        test.load_policy(
+            {
+                "name": "asg-update",
+                "resource": "aws.asg",
+                "actions": [{
+                    "type": "update",
+                }],
+            },
+            session_factory=factory,
+        )
+
+
 class AutoScalingTest(BaseTest):
 
     def get_ec2_tags(self, ec2, instance_id):