@ -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 = '''
@ -476,6 +515,22 @@ def describe_launch_configurations(connection, launch_config_name):
return pg . paginate ( LaunchConfigurationNames = [ launch_config_name ] ) . build_full_result ( )
@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 )
el if 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_l c( props , lc_check , initial_instances ) :
def get_instances_by_l aun ch_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