summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Webster <email@nathanwebster.me>2018-09-25 21:39:34 +0100
committerRyan Brown <sb@ryansb.com>2018-09-25 16:39:34 -0400
commit3b786acb86223f523099f47858ff84f239d01e3f (patch)
tree87296549816932412304f1a2a689508e5373f894
parent6069d09b9d7193a1896dfc2816e0e51833a53e6b (diff)
downloadansible-3b786acb86223f523099f47858ff84f239d01e3f.tar.gz
EC2_ASG: Enable support for launch_templates (#45647)
* Enable support for launch_templates in ec2_asg * Fix asg create with LT and no version number * Update mutually exclusive list * Better function names
-rw-r--r--lib/ansible/modules/cloud/amazon/ec2_asg.py291
1 files changed, 232 insertions, 59 deletions
diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg.py b/lib/ansible/modules/cloud/amazon/ec2_asg.py
index 2c39286c26..723a9a695f 100644
--- a/lib/ansible/modules/cloud/amazon/ec2_asg.py
+++ b/lib/ansible/modules/cloud/amazon/ec2_asg.py
@@ -24,7 +24,7 @@ module: ec2_asg
short_description: Create or delete AWS Autoscaling Groups
description:
- Can create or delete AWS Autoscaling Groups
- - Works with the ec2_lc module to manage Launch Configurations
+ - Can be used with the ec2_lc module to manage Launch Configurations
version_added: "1.6"
author: "Gareth Rushgrove (@garethr)"
requirements: [ "boto3", "botocore" ]
@@ -51,8 +51,22 @@ options:
launch_config_name:
description:
- Name of the Launch configuration to use for the group. See the ec2_lc module for managing these.
- If unspecified then the current group value will be used.
- required: true
+ If unspecified then the current group value will be used. One of launch_config_name or launch_template must be provided.
+ launch_template:
+ description:
+ - Dictionary describing the Launch Template to use
+ suboptions:
+ version:
+ description:
+ - The version number of the launch template to use. Defaults to latest version if not provided.
+ default: "latest"
+ launch_template_name:
+ description:
+ - The name of the launch template. Only one of launch_template_name or launch_template_id is required.
+ launch_template_id:
+ description:
+ - The id of the launch template. Only one of launch_template_name or launch_template_id is required.
+ version_added: "2.8"
min_size:
description:
- Minimum number of instances in group, if unspecified then the current group value will be used.
@@ -87,6 +101,11 @@ options:
- Check to make sure instances that are being replaced with replace_instances do not already have the current launch_config.
version_added: "1.8"
default: 'yes'
+ lt_check:
+ description:
+ - Check to make sure instances that are being replaced with replace_instances do not already have the current launch_template or launch_template version.
+ version_added: "2.8"
+ default: 'yes'
vpc_zone_identifier:
description:
- List of VPC subnets to use
@@ -182,7 +201,7 @@ extends_documentation_fragment:
"""
EXAMPLES = '''
-# Basic configuration
+# Basic configuration with Launch Configuration
- ec2_asg:
name: special
@@ -245,6 +264,26 @@ EXAMPLES = '''
max_size: 5
desired_capacity: 5
region: us-east-1
+
+# Basic Configuration with Launch Template
+
+- 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'
+ min_size: 1
+ max_size: 10
+ desired_capacity: 5
+ vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ]
+ tags:
+ - environment: production
+ propagate_at_launch: no
+
+
'''
RETURN = '''
@@ -477,6 +516,22 @@ def describe_launch_configurations(connection, launch_config_name):
@AWSRetry.backoff(**backoff_params)
+def describe_launch_templates(connection, launch_template):
+ if launch_template['launch_template_id'] is not None:
+ try:
+ lt = connection.describe_launch_templates(LaunchTemplateIds=[launch_template['launch_template_id']])
+ return lt
+ except (botocore.exceptions.ClientError) as e:
+ module.fail_json(msg="No launch template found matching: %s" % launch_template)
+ else:
+ try:
+ lt = connection.describe_launch_templates(LaunchTemplateNames=[launch_template['launch_template_name']])
+ return lt
+ except (botocore.exceptions.ClientError) as e:
+ module.fail_json(msg="No launch template found matching: %s" % launch_template)
+
+
+@AWSRetry.backoff(**backoff_params)
def create_asg(connection, **params):
connection.create_auto_scaling_group(**params)
@@ -534,16 +589,18 @@ def terminate_asg_instance(connection, instance_id, decrement_capacity):
ShouldDecrementDesiredCapacity=decrement_capacity)
-def enforce_required_arguments():
+def enforce_required_arguments_for_create():
''' As many arguments are not required for autoscale group deletion
they cannot be mandatory arguments for the module, so we enforce
them here '''
missing_args = []
- for arg in ('min_size', 'max_size', 'launch_config_name'):
+ if module.params.get('launch_config_name') is None and module.params.get('launch_template') is None:
+ module.fail_json(msg="Missing either launch_config_name or launch_template for autoscaling group create")
+ for arg in ('min_size', 'max_size'):
if module.params[arg] is None:
missing_args.append(arg)
if missing_args:
- module.fail_json(msg="Missing required arguments for autoscaling group create/update: %s" % ",".join(missing_args))
+ module.fail_json(msg="Missing required arguments for autoscaling group create: %s" % ",".join(missing_args))
def get_properties(autoscaling_group):
@@ -558,11 +615,17 @@ def get_properties(autoscaling_group):
instance_facts = dict()
autoscaling_group_instances = autoscaling_group.get('Instances')
if autoscaling_group_instances:
+
properties['instances'] = [i['InstanceId'] for i in autoscaling_group_instances]
for i in autoscaling_group_instances:
- instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
- 'lifecycle_state': i['LifecycleState'],
- 'launch_config_name': i.get('LaunchConfigurationName')}
+ if i.get('LaunchConfigurationName'):
+ instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
+ 'lifecycle_state': i['LifecycleState'],
+ 'launch_config_name': i['LaunchConfigurationName']}
+ else:
+ instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
+ 'lifecycle_state': i['LifecycleState'],
+ 'launch_template': i['LaunchTemplate']}
if i['HealthStatus'] == 'Healthy' and i['LifecycleState'] == 'InService':
properties['viable_instances'] += 1
if i['HealthStatus'] == 'Healthy':
@@ -584,7 +647,10 @@ def get_properties(autoscaling_group):
properties['created_time'] = autoscaling_group.get('CreatedTime')
properties['instance_facts'] = instance_facts
properties['load_balancers'] = autoscaling_group.get('LoadBalancerNames')
- properties['launch_config_name'] = autoscaling_group.get('LaunchConfigurationName')
+ if autoscaling_group.get('LaunchConfigurationName'):
+ properties['launch_config_name'] = autoscaling_group.get('LaunchConfigurationName')
+ else:
+ properties['launch_template'] = autoscaling_group.get('LaunchTemplate')
properties['tags'] = autoscaling_group.get('Tags')
properties['min_size'] = autoscaling_group.get('MinSize')
properties['max_size'] = autoscaling_group.get('MaxSize')
@@ -616,6 +682,31 @@ def get_properties(autoscaling_group):
return properties
+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')
+ if launch_config_name is None and launch_template is None:
+ return launch_object
+ elif launch_config_name:
+ try:
+ launch_configs = describe_launch_configurations(connection, launch_config_name)
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json(msg="Failed to describe launch configurations",
+ exception=traceback.format_exc())
+ if len(launch_configs['LaunchConfigurations']) == 0:
+ module.fail_json(msg="No launch config found with name %s" % launch_config_name)
+ launch_object = {"LaunchConfigurationName": launch_configs['LaunchConfigurations'][0]['LaunchConfigurationName']}
+ return launch_object
+ elif launch_template:
+ lt = describe_launch_templates(ec2_connection, launch_template)['LaunchTemplates'][0]
+ if launch_template['version'] is not None:
+ launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": launch_template['version']}}
+ else:
+ launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": str(lt['LatestVersionNumber'])}}
+ return launch_object
+
+
def elb_dreg(asg_connection, group_name, instance_id):
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
@@ -807,6 +898,7 @@ def create_autoscaling_group(connection):
target_group_arns = module.params['target_group_arns']
availability_zones = module.params['availability_zones']
launch_config_name = module.params.get('launch_config_name')
+ launch_template = module.params.get('launch_template')
min_size = module.params['min_size']
max_size = module.params['max_size']
placement_group = module.params.get('placement_group')
@@ -830,15 +922,15 @@ def create_autoscaling_group(connection):
module.fail_json(msg="Failed to describe auto scaling groups.",
exception=traceback.format_exc())
- if not vpc_zone_identifier and not availability_zones:
- region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
- ec2_connection = boto3_conn(module,
- conn_type='client',
- resource='ec2',
- region=region,
- endpoint=ec2_url,
- **aws_connect_params)
- elif vpc_zone_identifier:
+ region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
+ ec2_connection = boto3_conn(module,
+ conn_type='client',
+ resource='ec2',
+ region=region,
+ endpoint=ec2_url,
+ **aws_connect_params)
+
+ if vpc_zone_identifier:
vpc_zone_identifier = ','.join(vpc_zone_identifier)
asg_tags = []
@@ -854,19 +946,13 @@ def create_autoscaling_group(connection):
if not vpc_zone_identifier and not availability_zones:
availability_zones = module.params['availability_zones'] = [zone['ZoneName'] for
zone in ec2_connection.describe_availability_zones()['AvailabilityZones']]
- enforce_required_arguments()
- try:
- launch_configs = describe_launch_configurations(connection, launch_config_name)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json(msg="Failed to describe launch configurations",
- exception=traceback.format_exc())
- if len(launch_configs['LaunchConfigurations']) == 0:
- module.fail_json(msg="No launch config found with name %s" % launch_config_name)
+
+ enforce_required_arguments_for_create()
+
if desired_capacity is None:
desired_capacity = min_size
ag = dict(
AutoScalingGroupName=group_name,
- LaunchConfigurationName=launch_configs['LaunchConfigurations'][0]['LaunchConfigurationName'],
MinSize=min_size,
MaxSize=max_size,
DesiredCapacity=desired_capacity,
@@ -886,6 +972,15 @@ def create_autoscaling_group(connection):
if target_group_arns:
ag['TargetGroupARNs'] = target_group_arns
+ launch_object = get_launch_object(connection, ec2_connection)
+ if 'LaunchConfigurationName' in launch_object:
+ ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName']
+ elif 'LaunchTemplate' in launch_object:
+ ag['LaunchTemplate'] = launch_object['LaunchTemplate']
+ else:
+ module.fail_json(msg="Missing LaunchConfigurationName or LaunchTemplate",
+ exception=traceback.format_exc())
+
try:
create_asg(connection, **ag)
if metrics_collection:
@@ -1035,18 +1130,8 @@ def create_autoscaling_group(connection):
max_size = as_group['MaxSize']
if desired_capacity is None:
desired_capacity = as_group['DesiredCapacity']
- launch_config_name = launch_config_name or as_group['LaunchConfigurationName']
-
- try:
- launch_configs = describe_launch_configurations(connection, launch_config_name)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json(msg="Failed to describe launch configurations",
- exception=traceback.format_exc())
- if len(launch_configs['LaunchConfigurations']) == 0:
- module.fail_json(msg="No launch config found with name %s" % launch_config_name)
ag = dict(
AutoScalingGroupName=group_name,
- LaunchConfigurationName=launch_configs['LaunchConfigurations'][0]['LaunchConfigurationName'],
MinSize=min_size,
MaxSize=max_size,
DesiredCapacity=desired_capacity,
@@ -1054,6 +1139,21 @@ def create_autoscaling_group(connection):
HealthCheckType=health_check_type,
DefaultCooldown=default_cooldown,
TerminationPolicies=termination_policies)
+
+ # Get the launch object (config or template) if one is provided in args or use the existing one attached to ASG if not.
+ launch_object = get_launch_object(connection, ec2_connection)
+ if 'LaunchConfigurationName' in launch_object:
+ ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName']
+ elif 'LaunchTemplate' in launch_object:
+ ag['LaunchTemplate'] = launch_object['LaunchTemplate']
+ else:
+ try:
+ ag['LaunchConfigurationName'] = as_group['LaunchConfigurationName']
+ except:
+ launch_template = as_group['LaunchTemplate']
+ # Prefer LaunchTemplateId over Name as it's more specific. Only one can be used for update_asg.
+ ag['LaunchTemplate'] = {"LaunchTemplateId": launch_template['LaunchTemplateId'], "Version": launch_template['Version']}
+
if availability_zones:
ag['AvailabilityZones'] = availability_zones
if vpc_zone_identifier:
@@ -1168,7 +1268,18 @@ def replace(connection):
max_size = module.params.get('max_size')
min_size = module.params.get('min_size')
desired_capacity = module.params.get('desired_capacity')
- lc_check = module.params.get('lc_check')
+ launch_config_name = module.params.get('launch_config_name')
+ # Required to maintain the default value being set to 'true'
+ if launch_config_name:
+ lc_check = module.params.get('lc_check')
+ else:
+ lc_check = False
+ # Mirror above behaviour for Launch Templates
+ launch_template = module.params.get('launch_template')
+ if launch_template:
+ lt_check = module.params.get('lt_check')
+ else:
+ lt_check = False
replace_instances = module.params.get('replace_instances')
replace_all_instances = module.params.get('replace_all_instances')
@@ -1185,12 +1296,16 @@ def replace(connection):
replace_instances = instances
if replace_instances:
instances = replace_instances
+
# check to see if instances are replaceable if checking launch configs
+ if launch_config_name:
+ new_instances, old_instances = get_instances_by_launch_config(props, lc_check, instances)
+ elif launch_template:
+ new_instances, old_instances = get_instances_by_launch_template(props, lt_check, instances)
- new_instances, old_instances = get_instances_by_lc(props, lc_check, instances)
num_new_inst_needed = desired_capacity - len(new_instances)
- if lc_check:
+ if lc_check or lt_check:
if num_new_inst_needed == 0 and old_instances:
module.debug("No new instances needed, but old instances are present. Removing old instances")
terminate_batch(connection, old_instances, instances, True)
@@ -1247,14 +1362,17 @@ def replace(connection):
return(changed, asg_properties)
-def get_instances_by_lc(props, lc_check, initial_instances):
+def get_instances_by_launch_config(props, lc_check, initial_instances):
new_instances = []
old_instances = []
# old instances are those that have the old launch config
if lc_check:
for i in props['instances']:
- if props['instance_facts'][i]['launch_config_name'] == props['launch_config_name']:
+ # Check if migrating from launch_template to launch_config first
+ if 'launch_template' in props['instance_facts'][i]:
+ old_instances.append(i)
+ elif props['instance_facts'][i]['launch_config_name'] == props['launch_config_name']:
new_instances.append(i)
else:
old_instances.append(i)
@@ -1272,20 +1390,60 @@ def get_instances_by_lc(props, lc_check, initial_instances):
return new_instances, old_instances
-def list_purgeable_instances(props, lc_check, replace_instances, initial_instances):
+def get_instances_by_launch_template(props, lt_check, initial_instances):
+ new_instances = []
+ old_instances = []
+ # old instances are those that have the old launch template or version of the same launch templatec
+ if lt_check:
+ for i in props['instances']:
+ # Check if migrating from launch_config_name to launch_template_name first
+ if 'launch_config_name' in props['instance_facts'][i]:
+ old_instances.append(i)
+ elif props['instance_facts'][i]['launch_template'] == props['launch_template']:
+ new_instances.append(i)
+ else:
+ old_instances.append(i)
+ else:
+ module.debug("Comparing initial instances with current: %s" % initial_instances)
+ for i in props['instances']:
+ if i not in initial_instances:
+ new_instances.append(i)
+ else:
+ old_instances.append(i)
+ module.debug("New instances: %s, %s" % (len(new_instances), new_instances))
+ module.debug("Old instances: %s, %s" % (len(old_instances), old_instances))
+
+ return new_instances, old_instances
+
+
+def list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances):
instances_to_terminate = []
instances = (inst_id for inst_id in replace_instances if inst_id in props['instances'])
-
# check to make sure instances given are actually in the given ASG
# and they have a non-current launch config
- if lc_check:
- for i in instances:
- if props['instance_facts'][i]['launch_config_name'] != props['launch_config_name']:
- instances_to_terminate.append(i)
- else:
- for i in instances:
- if i in initial_instances:
- instances_to_terminate.append(i)
+ if module.params.get('launch_config_name'):
+ if lc_check:
+ for i in instances:
+ if 'launch_template' in props['instance_facts'][i]:
+ instances_to_terminate.append(i)
+ elif props['instance_facts'][i]['launch_config_name'] != props['launch_config_name']:
+ instances_to_terminate.append(i)
+ else:
+ for i in instances:
+ if i in initial_instances:
+ instances_to_terminate.append(i)
+ elif module.params.get('launch_template'):
+ if lt_check:
+ for i in instances:
+ if 'launch_config_name' in props['instance_facts'][i]:
+ instances_to_terminate.append(i)
+ elif props['instance_facts'][i]['launch_template'] != props['launch_template']:
+ instances_to_terminate.append(i)
+ else:
+ for i in instances:
+ if i in initial_instances:
+ instances_to_terminate.append(i)
+
return instances_to_terminate
@@ -1295,6 +1453,7 @@ def terminate_batch(connection, replace_instances, initial_instances, leftovers=
desired_capacity = module.params.get('desired_capacity')
group_name = module.params.get('name')
lc_check = module.params.get('lc_check')
+ lt_check = module.params.get('lt_check')
decrement_capacity = False
break_loop = False
@@ -1304,13 +1463,15 @@ def terminate_batch(connection, replace_instances, initial_instances, leftovers=
props = get_properties(as_group)
desired_size = as_group['MinSize']
-
- new_instances, old_instances = get_instances_by_lc(props, lc_check, initial_instances)
+ if module.params.get('launch_config_name'):
+ new_instances, old_instances = get_instances_by_launch_config(props, lc_check, initial_instances)
+ else:
+ new_instances, old_instances = get_instances_by_launch_template(props, lt_check, initial_instances)
num_new_inst_needed = desired_capacity - len(new_instances)
# check to make sure instances given are actually in the given ASG
# and they have a non-current launch config
- instances_to_terminate = list_purgeable_instances(props, lc_check, replace_instances, initial_instances)
+ instances_to_terminate = list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances)
module.debug("new instances needed: %s" % num_new_inst_needed)
module.debug("new instances: %s" % new_instances)
@@ -1412,6 +1573,14 @@ def main():
target_group_arns=dict(type='list'),
availability_zones=dict(type='list'),
launch_config_name=dict(type='str'),
+ launch_template=dict(type='dict',
+ default=None,
+ options=dict(
+ version=dict(type='str'),
+ launch_template_name=dict(type='str'),
+ launch_template_id=dict(type='str'),
+ ),
+ ),
min_size=dict(type='int'),
max_size=dict(type='int'),
placement_group=dict(type='str'),
@@ -1421,6 +1590,7 @@ def main():
replace_all_instances=dict(type='bool', default=False),
replace_instances=dict(type='list', default=[]),
lc_check=dict(type='bool', default=True),
+ lt_check=dict(type='bool', default=True),
wait_timeout=dict(type='int', default=300),
state=dict(default='present', choices=['present', 'absent']),
tags=dict(type='list', default=[]),
@@ -1455,7 +1625,9 @@ def main():
global module
module = AnsibleModule(
argument_spec=argument_spec,
- mutually_exclusive=[['replace_all_instances', 'replace_instances']]
+ mutually_exclusive=[
+ ['replace_all_instances', 'replace_instances'],
+ ['launch_config_name', 'launch_template']]
)
if not HAS_BOTO3:
@@ -1464,6 +1636,7 @@ def main():
state = module.params.get('state')
replace_instances = module.params.get('replace_instances')
replace_all_instances = module.params.get('replace_all_instances')
+
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module,
conn_type='client',
@@ -1481,7 +1654,7 @@ def main():
module.exit_json(changed=changed)
# Only replace instances if asg existed at start of call
- if exists and (replace_all_instances or replace_instances):
+ if exists and (replace_all_instances or replace_instances) and (module.params.get('launch_config_name') or module.params.get('launch_template')):
replace_changed, asg_properties = replace(connection)
if create_changed or replace_changed:
changed = True