From 822077fefd9929018bea480e2561015f9c65ffae Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Wed, 5 Feb 2020 09:04:15 +0100 Subject: [PATCH] Asg mixed instance types (#67045) * merge from origin pr 55067 * handle update existing asg with mixed-instance-policy * fix documentation and append output * update output documentation * update documentation version added * add integration test for mixed instance policy using launch template * add changelog fragment * Update lib/ansible/modules/cloud/amazon/ec2_asg.py Co-Authored-By: Mark Chappell * Update lib/ansible/modules/cloud/amazon/ec2_asg.py Co-Authored-By: Mark Chappell * Update lib/ansible/modules/cloud/amazon/ec2_asg.py Co-Authored-By: Jill R <4121322+jillr@users.noreply.github.com> * add warning about botocore version and add expand example documentation * Update changelogs/fragments/67045-ec2_asg_mixed_instance_policy.yml Co-Authored-By: Mark Chappell * Update lib/ansible/modules/cloud/amazon/ec2_asg.py Co-Authored-By: Mark Chappell * Update lib/ansible/modules/cloud/amazon/ec2_asg.py Co-Authored-By: Mark Chappell * remove useless line Co-authored-by: Yi-Tse Hong Co-authored-by: Mark Chappell Co-authored-by: Jill R <4121322+jillr@users.noreply.github.com> --- .../67045-ec2_asg_mixed_instance_policy.yml | 2 + lib/ansible/modules/cloud/amazon/ec2_asg.py | 83 ++++++++++++++++++- .../targets/ec2_asg/tasks/main.yml | 47 +++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/67045-ec2_asg_mixed_instance_policy.yml diff --git a/changelogs/fragments/67045-ec2_asg_mixed_instance_policy.yml b/changelogs/fragments/67045-ec2_asg_mixed_instance_policy.yml new file mode 100644 index 00000000000..bd57c2dc30a --- /dev/null +++ b/changelogs/fragments/67045-ec2_asg_mixed_instance_policy.yml @@ -0,0 +1,2 @@ +minor_changes: + - ec2_asg - Add the ability to use mixed_instance_policy in launch template driven autoscaling groups diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg.py b/lib/ansible/modules/cloud/amazon/ec2_asg.py index 6fce5638e1a..83e663313b7 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_asg.py +++ b/lib/ansible/modules/cloud/amazon/ec2_asg.py @@ -81,6 +81,20 @@ options: description: - Maximum number of instances in group, if unspecified then the current group value will be used. type: int + mixed_instances_policy: + description: + - A mixed instance policy to use for the ASG. + - Only used when the ASG is configured to use a Launch Template (I(launch_template)). + - 'See also U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-mixedinstancespolicy.html)' + required: false + version_added: "2.10" + suboptions: + instance_types: + description: + - A list of instance_types. + type: list + elements: str + type: dict placement_group: description: - Physical location of your cluster placement group created in Amazon EC2. @@ -329,6 +343,28 @@ EXAMPLES = ''' - environment: production propagate_at_launch: no +# Basic Configuration with Launch Template using mixed instance policy + +- ec2_asg: + name: special + load_balancers: [ 'lb1', 'lb2' ] + availability_zones: [ 'eu-west-1a', 'eu-west-1b' ] + launch_template: + version: '1' + launch_template_name: 'lt-example' + launch_template_id: 'lt-123456' + mixed_instances_policy: + instance_types: + - t3a.large + - t3.large + - t2.large + min_size: 1 + max_size: 10 + desired_capacity: 5 + vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] + tags: + - environment: production + propagate_at_launch: no ''' @@ -426,6 +462,11 @@ min_size: returned: success type: int sample: 1 +mixed_instance_policy: + description: Returns the list of instance types if a mixed instance policy is set. + returned: success + type: list + sample: ["t3.micro", "t3a.micro"] pending_instances: description: Number of instances in pending state returned: success @@ -511,6 +552,8 @@ try: except ImportError: pass # will be detected by imported HAS_BOTO3 +from ansible.module_utils.aws.core import AnsibleAWSModule + ASG_ATTRIBUTES = ('AvailabilityZones', 'DefaultCooldown', 'DesiredCapacity', 'HealthCheckGracePeriod', 'HealthCheckType', 'LaunchConfigurationName', 'LoadBalancerNames', 'MaxSize', 'MinSize', 'AutoScalingGroupName', 'PlacementGroup', @@ -711,6 +754,10 @@ def get_properties(autoscaling_group): properties['termination_policies'] = autoscaling_group.get('TerminationPolicies') properties['target_group_arns'] = autoscaling_group.get('TargetGroupARNs') properties['vpc_zone_identifier'] = autoscaling_group.get('VPCZoneIdentifier') + raw_mixed_instance_object = autoscaling_group.get('MixedInstancesPolicy') + if raw_mixed_instance_object: + properties['mixed_instances_policy'] = [x['InstanceType'] for x in raw_mixed_instance_object.get('LaunchTemplate').get('Overrides')] + metrics = autoscaling_group.get('EnabledMetrics') if metrics: metrics.sort(key=lambda x: x["Metric"]) @@ -738,6 +785,7 @@ def get_launch_object(connection, ec2_connection): launch_object = dict() launch_config_name = module.params.get('launch_config_name') launch_template = module.params.get('launch_template') + mixed_instances_policy = module.params.get('mixed_instances_policy') if launch_config_name is None and launch_template is None: return launch_object elif launch_config_name: @@ -756,6 +804,20 @@ def get_launch_object(connection, ec2_connection): launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": launch_template['version']}} else: launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": str(lt['LatestVersionNumber'])}} + + if mixed_instances_policy: + instance_types = mixed_instances_policy.get('instance_types', []) + policy = { + 'LaunchTemplate': { + 'LaunchTemplateSpecification': launch_object['LaunchTemplate'] + } + } + if instance_types: + policy['LaunchTemplate']['Overrides'] = [] + for instance_type in instance_types: + instance_type_dict = {'InstanceType': instance_type} + policy['LaunchTemplate']['Overrides'].append(instance_type_dict) + launch_object['MixedInstancesPolicy'] = policy return launch_object @@ -951,6 +1013,7 @@ def create_autoscaling_group(connection): availability_zones = module.params['availability_zones'] launch_config_name = module.params.get('launch_config_name') launch_template = module.params.get('launch_template') + mixed_instances_policy = module.params.get('mixed_instances_policy') min_size = module.params['min_size'] max_size = module.params['max_size'] placement_group = module.params.get('placement_group') @@ -1028,7 +1091,10 @@ def create_autoscaling_group(connection): if 'LaunchConfigurationName' in launch_object: ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName'] elif 'LaunchTemplate' in launch_object: - ag['LaunchTemplate'] = launch_object['LaunchTemplate'] + if 'MixedInstancesPolicy' in launch_object: + ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] + else: + ag['LaunchTemplate'] = launch_object['LaunchTemplate'] else: module.fail_json(msg="Missing LaunchConfigurationName or LaunchTemplate", exception=traceback.format_exc()) @@ -1201,7 +1267,10 @@ def create_autoscaling_group(connection): if 'LaunchConfigurationName' in launch_object: ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName'] elif 'LaunchTemplate' in launch_object: - ag['LaunchTemplate'] = launch_object['LaunchTemplate'] + if 'MixedInstancesPolicy' in launch_object: + ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] + else: + ag['LaunchTemplate'] = launch_object['LaunchTemplate'] else: try: ag['LaunchConfigurationName'] = as_group['LaunchConfigurationName'] @@ -1639,6 +1708,11 @@ def main(): launch_template_id=dict(type='str'), ), ), + mixed_instances_policy=dict(type='dict', + default=None, + options=dict( + instance_types=dict(type='list', elements='str'), + )), min_size=dict(type='int'), max_size=dict(type='int'), placement_group=dict(type='str'), @@ -1681,7 +1755,7 @@ def main(): ) global module - module = AnsibleModule( + module = AnsibleAWSModule( argument_spec=argument_spec, mutually_exclusive=[ ['replace_all_instances', 'replace_instances'], @@ -1691,6 +1765,9 @@ def main(): if not HAS_BOTO3: module.fail_json(msg='boto3 required for this module') + if module.params.get('mixed_instance_type') and not module.botocore_at_least('1.12.45'): + module.fail_json(msg="mixed_instance_type is only supported with botocore >= 1.12.45") + state = module.params.get('state') replace_instances = module.params.get('replace_instances') replace_all_instances = module.params.get('replace_all_instances') diff --git a/test/integration/targets/ec2_asg/tasks/main.yml b/test/integration/targets/ec2_asg/tasks/main.yml index 293e959c626..7c39f088d73 100644 --- a/test/integration/targets/ec2_asg/tasks/main.yml +++ b/test/integration/targets/ec2_asg/tasks/main.yml @@ -585,6 +585,44 @@ until: status is finished retries: 200 delay: 15 + + # we need a launch template, otherwise we cannot test the mixed instance policy + - name: create launch template for autoscaling group to test its mixed instance policy + ec2_launch_template: + template_name: "{{ resource_prefix }}-lt" + image_id: "{{ ec2_ami_image }}" + instance_type: t3.micro + credit_specification: + cpu_credits: standard + network_interfaces: + - associate_public_ip_address: yes + delete_on_termination: yes + device_index: 0 + groups: + - "{{ sg.group_id }}" + + - name: update autoscaling group with mixed-instance policy + ec2_asg: + name: "{{ resource_prefix }}-asg" + launch_template: + launch_template_name: "{{ resource_prefix }}-lt" + desired_capacity: 1 + min_size: 1 + max_size: 1 + vpc_zone_identifier: "{{ testing_subnet.subnet.id }}" + state: present + mixed_instances_policy: + instance_types: + - t3.micro + - t3a.micro + wait_for_instances: yes + register: output + + - assert: + that: + - "output.mixed_instances_policy | length == 2" + - "output.mixed_instances_policy[0] == 't3.micro'" + - "output.mixed_instances_policy[1] == 't3a.micro'" # ============================================================ @@ -639,6 +677,15 @@ - "{{ resource_prefix }}-lc" - "{{ resource_prefix }}-lc-2" + - name: delete launch template + ec2_launch_template: + name: "{{ resource_prefix }}-lt" + state: absent + register: del_lt + retries: 10 + until: del_lt is not failed + ignore_errors: true + - name: remove the security group ec2_group: name: "{{ resource_prefix }}-sg"