Server IP : 85.214.239.14 / Your IP : 3.148.115.43 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/lib/python3/dist-packages/ansible_collections/amazon/aws/plugins/modules/ |
Upload File : |
#!/usr/bin/python # This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: autoscaling_group version_added: 5.0.0 short_description: Create or delete AWS AutoScaling Groups (ASGs) description: - Can create or delete AWS AutoScaling Groups. - Can be used with the M(community.aws.autoscaling_launch_config) module to manage Launch Configurations. - Prior to release 5.0.0 this module was called C(community.aws.ec2_asg). The usage did not change. - This module was originally added to C(community.aws) in release 1.0.0. author: - "Gareth Rushgrove (@garethr)" options: state: description: - Register or deregister the instance. choices: ['present', 'absent'] default: present type: str name: description: - Unique name for group to be created or deleted. required: true type: str load_balancers: description: - List of ELB names to use for the group. Use for classic load balancers. type: list elements: str target_group_arns: description: - List of target group ARNs to use for the group. Use for application load balancers. type: list elements: str availability_zones: description: - List of availability zone names in which to create the group. - Defaults to all the availability zones in the region if I(vpc_zone_identifier) is not set. type: list elements: str launch_config_name: description: - Name of the Launch configuration to use for the group. See the community.aws.autoscaling_launch_config) module for managing these. - If unspecified then the current group value will be used. One of I(launch_config_name) or I(launch_template) must be provided. type: str 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. type: str launch_template_name: description: - The name of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required. type: str launch_template_id: description: - The id of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required. type: str type: dict min_size: description: - Minimum number of instances in group, if unspecified then the current group value will be used. type: int max_size: description: - Maximum number of instances in group, if unspecified then the current group value will be used. type: int max_instance_lifetime: description: - The maximum amount of time, in seconds, that an instance can be in service. - Maximum instance lifetime must be equal to 0, between 604800 and 31536000 seconds (inclusive), or not specified. - Value of 0 removes lifetime restriction. 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 suboptions: instance_types: description: - A list of instance_types. type: list elements: str required: false instances_distribution: description: - >- Specifies the distribution of On-Demand Instances and Spot Instances, the maximum price to pay for Spot Instances, and how the Auto Scaling group allocates instance types to fulfill On-Demand and Spot capacity. - 'See also U(https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html)' required: false type: dict version_added: 1.5.0 version_added_collection: community.aws suboptions: on_demand_allocation_strategy: description: - Indicates how to allocate instance types to fulfill On-Demand capacity. type: str required: false version_added: 1.5.0 version_added_collection: community.aws on_demand_base_capacity: description: - >- The minimum amount of the Auto Scaling group's capacity that must be fulfilled by On-Demand Instances. This base portion is provisioned first as your group scales. - >- Default if not set is 0. If you leave it set to 0, On-Demand Instances are launched as a percentage of the Auto Scaling group's desired capacity, per the OnDemandPercentageAboveBaseCapacity setting. type: int required: false version_added: 1.5.0 version_added_collection: community.aws on_demand_percentage_above_base_capacity: description: - Controls the percentages of On-Demand Instances and Spot Instances for your additional capacity beyond OnDemandBaseCapacity. - Default if not set is 100. If you leave it set to 100, the percentages are 100% for On-Demand Instances and 0% for Spot Instances. - 'Valid range: 0 to 100' type: int required: false version_added: 1.5.0 version_added_collection: community.aws spot_allocation_strategy: description: - Indicates how to allocate instances across Spot Instance pools. type: str required: false version_added: 1.5.0 version_added_collection: community.aws spot_instance_pools: description: - >- The number of Spot Instance pools across which to allocate your Spot Instances. The Spot pools are determined from the different instance types in the Overrides array of LaunchTemplate. Default if not set is 2. - Used only when the Spot allocation strategy is lowest-price. - 'Valid Range: Minimum value of 1. Maximum value of 20.' type: int required: false version_added: 1.5.0 version_added_collection: community.aws spot_max_price: description: - The maximum price per unit hour that you are willing to pay for a Spot Instance. - If you leave the value of this parameter blank (which is the default), the maximum Spot price is set at the On-Demand price. - To remove a value that you previously set, include the parameter but leave the value blank. type: str required: false version_added: 1.5.0 version_added_collection: community.aws type: dict placement_group: description: - Physical location of your cluster placement group created in Amazon EC2. type: str desired_capacity: description: - Desired number of instances in group, if unspecified then the current group value will be used. type: int replace_all_instances: description: - In a rolling fashion, replace all instances that used the old launch configuration with one from the new launch configuration. It increases the ASG size by I(replace_batch_size), waits for the new instances to be up and running. After that, it terminates a batch of old instances, waits for the replacements, and repeats, until all old instances are replaced. Once that's done the ASG size is reduced back to the expected size. default: false type: bool replace_batch_size: description: - Number of instances you'd like to replace at a time. Used with I(replace_all_instances). required: false default: 1 type: int replace_instances: description: - List of I(instance_ids) belonging to the named AutoScalingGroup that you would like to terminate and be replaced with instances matching the current launch configuration. type: list elements: str default: [] detach_instances: description: - Removes one or more instances from the specified AutoScalingGroup. - If I(decrement_desired_capacity) flag is not set, new instance(s) are launched to replace the detached instance(s). - If a Classic Load Balancer is attached to the AutoScalingGroup, the instances are also deregistered from the load balancer. - If there are target groups attached to the AutoScalingGroup, the instances are also deregistered from the target groups. type: list elements: str default: [] version_added: 3.2.0 version_added_collection: community.aws decrement_desired_capacity: description: - Indicates whether the AutoScalingGroup decrements the desired capacity value by the number of instances detached. default: false type: bool version_added: 3.2.0 version_added_collection: community.aws lc_check: description: - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current I(launch_config). default: true type: bool lt_check: description: - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current I(launch_template or I(launch_template) I(version). default: true type: bool vpc_zone_identifier: description: - List of VPC subnets to use type: list elements: str tags: description: - A list of tags to add to the Auto Scale Group. - Optional key is I(propagate_at_launch), which defaults to true. - When I(propagate_at_launch) is true the tags will be propagated to the Instances created. type: list elements: dict default: [] purge_tags: description: - If C(true), existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. - If the I(tags) parameter is not set then tags will not be modified. default: false type: bool version_added: 3.2.0 version_added_collection: community.aws health_check_period: description: - Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. required: false default: 300 type: int health_check_type: description: - The service you want the health status from, Amazon EC2 or Elastic Load Balancer. required: false default: EC2 choices: ['EC2', 'ELB'] type: str default_cooldown: description: - The number of seconds after a scaling activity completes before another can begin. default: 300 type: int wait_timeout: description: - How long to wait for instances to become viable when replaced. If you experience the error "Waited too long for ELB instances to be healthy", try increasing this value. default: 300 type: int wait_for_instances: description: - Wait for the ASG instances to be in a ready state before exiting. If instances are behind an ELB, it will wait until the ELB determines all instances have a lifecycle_state of "InService" and a health_status of "Healthy". default: true type: bool termination_policies: description: - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity. - Using I(termination_policies=Default) when modifying an existing AutoScalingGroup will result in the existing policy being retained instead of changed to C(Default). - 'Valid values include: C(Default), C(OldestInstance), C(NewestInstance), C(OldestLaunchConfiguration), C(ClosestToNextInstanceHour)' - 'Full documentation of valid values can be found in the AWS documentation:' - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html#custom-termination-policy)' default: Default type: list elements: str notification_topic: description: - A SNS topic ARN to send auto scaling notifications to. type: str notification_types: description: - A list of auto scaling events to trigger notifications on. default: - 'autoscaling:EC2_INSTANCE_LAUNCH' - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' - 'autoscaling:EC2_INSTANCE_TERMINATE' - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' required: false type: list elements: str suspend_processes: description: - A list of scaling processes to suspend. - 'Valid values include:' - C(Launch), C(Terminate), C(HealthCheck), C(ReplaceUnhealthy), C(AZRebalance), C(AlarmNotification), C(ScheduledActions), C(AddToLoadBalancer) - 'Full documentation of valid values can be found in the AWS documentation:' - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html)' default: [] type: list elements: str metrics_collection: description: - Enable ASG metrics collection. type: bool default: false metrics_granularity: description: - When I(metrics_collection=true) this will determine the granularity of metrics collected by CloudWatch. default: "1Minute" type: str metrics_list: description: - List of autoscaling metrics to collect when I(metrics_collection=true). default: - 'GroupMinSize' - 'GroupMaxSize' - 'GroupDesiredCapacity' - 'GroupInServiceInstances' - 'GroupPendingInstances' - 'GroupStandbyInstances' - 'GroupTerminatingInstances' - 'GroupTotalInstances' type: list elements: str extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 - amazon.aws.boto3 ''' EXAMPLES = r''' # Basic configuration with Launch Configuration - amazon.aws.autoscaling_group: name: special load_balancers: [ 'lb1', 'lb2' ] availability_zones: [ 'eu-west-1a', 'eu-west-1b' ] launch_config_name: 'lc-1' min_size: 1 max_size: 10 desired_capacity: 5 vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] tags: - environment: production propagate_at_launch: false # Rolling ASG Updates # Below is an example of how to assign a new launch config to an ASG and terminate old instances. # # All instances in "myasg" that do not have the launch configuration named "my_new_lc" will be terminated in # a rolling fashion with instances using the current launch configuration, "my_new_lc". # # This could also be considered a rolling deploy of a pre-baked AMI. # # If this is a newly created group, the instances will not be replaced since all instances # will have the current launch configuration. - name: create launch config community.aws.autoscaling_launch_config: name: my_new_lc image_id: ami-lkajsf key_name: mykey region: us-east-1 security_groups: sg-23423 instance_type: m1.small assign_public_ip: true - amazon.aws.autoscaling_group: name: myasg launch_config_name: my_new_lc health_check_period: 60 health_check_type: ELB replace_all_instances: true min_size: 5 max_size: 5 desired_capacity: 5 region: us-east-1 # To only replace a couple of instances instead of all of them, supply a list # to "replace_instances": - amazon.aws.autoscaling_group: name: myasg launch_config_name: my_new_lc health_check_period: 60 health_check_type: ELB replace_instances: - i-b345231 - i-24c2931 min_size: 5 max_size: 5 desired_capacity: 5 region: us-east-1 # Basic Configuration with Launch Template - amazon.aws.autoscaling_group: 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: false # Basic Configuration with Launch Template using mixed instance policy - amazon.aws.autoscaling_group: 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 instances_distribution: on_demand_percentage_above_base_capacity: 0 spot_allocation_strategy: capacity-optimized min_size: 1 max_size: 10 desired_capacity: 5 vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ] tags: - environment: production propagate_at_launch: false ''' RETURN = r''' --- auto_scaling_group_name: description: The unique name of the auto scaling group returned: success type: str sample: "myasg" auto_scaling_group_arn: description: The unique ARN of the autoscaling group returned: success type: str sample: "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:6a09ad6d-eeee-1234-b987-ee123ced01ad:autoScalingGroupName/myasg" availability_zones: description: The availability zones for the auto scaling group returned: success type: list sample: [ "us-east-1d" ] created_time: description: Timestamp of create time of the auto scaling group returned: success type: str sample: "2017-11-08T14:41:48.272000+00:00" default_cooldown: description: The default cooldown time in seconds. returned: success type: int sample: 300 desired_capacity: description: The number of EC2 instances that should be running in this group. returned: success type: int sample: 3 healthcheck_period: description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. returned: success type: int sample: 30 healthcheck_type: description: The service you want the health status from, one of "EC2" or "ELB". returned: success type: str sample: "ELB" healthy_instances: description: Number of instances in a healthy state returned: success type: int sample: 5 in_service_instances: description: Number of instances in service returned: success type: int sample: 3 instance_facts: description: Dictionary of EC2 instances and their status as it relates to the ASG. returned: success type: dict sample: { "i-0123456789012": { "health_status": "Healthy", "launch_config_name": "public-webapp-production-1", "lifecycle_state": "InService" } } instances: description: list of instance IDs in the ASG returned: success type: list sample: [ "i-0123456789012" ] launch_config_name: description: > Name of launch configuration associated with the ASG. Same as launch_configuration_name, provided for compatibility with M(amazon.aws.autoscaling_group) module. returned: success type: str sample: "public-webapp-production-1" load_balancers: description: List of load balancers names attached to the ASG. returned: success type: list sample: ["elb-webapp-prod"] max_instance_lifetime: description: The maximum amount of time, in seconds, that an instance can be in service. returned: success type: int sample: 604800 max_size: description: Maximum size of group returned: success type: int sample: 3 min_size: description: Minimum size of group returned: success type: int sample: 1 mixed_instances_policy: description: Returns the list of instance types if a mixed instances policy is set. returned: success type: list sample: ["t3.micro", "t3a.micro"] mixed_instances_policy_full: description: Returns the full dictionary representation of the mixed instances policy if a mixed instances policy is set. returned: success type: dict sample: { "instances_distribution": { "on_demand_allocation_strategy": "prioritized", "on_demand_base_capacity": 0, "on_demand_percentage_above_base_capacity": 0, "spot_allocation_strategy": "capacity-optimized" }, "launch_template": { "launch_template_specification": { "launch_template_id": "lt-53c2425cffa544c23", "launch_template_name": "random-LaunchTemplate", "version": "2" }, "overrides": [ { "instance_type": "m5.xlarge" }, { "instance_type": "m5a.xlarge" }, ] } } pending_instances: description: Number of instances in pending state returned: success type: int sample: 1 tags: description: List of tags for the ASG, and whether or not each tag propagates to instances at launch. returned: success type: list sample: [ { "key": "Name", "value": "public-webapp-production-1", "resource_id": "public-webapp-production-1", "resource_type": "auto-scaling-group", "propagate_at_launch": "true" }, { "key": "env", "value": "production", "resource_id": "public-webapp-production-1", "resource_type": "auto-scaling-group", "propagate_at_launch": "true" } ] target_group_arns: description: List of ARNs of the target groups that the ASG populates returned: success type: list sample: [ "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b", "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234" ] target_group_names: description: List of names of the target groups that the ASG populates returned: success type: list sample: [ "target-group-host-hello", "target-group-path-world" ] termination_policies: description: A list of termination policies for the group. returned: success type: list sample: ["Default"] unhealthy_instances: description: Number of instances in an unhealthy state returned: success type: int sample: 0 viable_instances: description: Number of instances in a viable state returned: success type: int sample: 1 vpc_zone_identifier: description: VPC zone ID / subnet id for the auto scaling group returned: success type: str sample: "subnet-a31ef45f" metrics_collection: description: List of enabled AutosSalingGroup metrics returned: success type: list sample: [ { "Granularity": "1Minute", "Metric": "GroupInServiceInstances" } ] ''' import time try: import botocore except ImportError: pass # Handled by AnsibleAWSModule from ansible.module_utils._text import to_native from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict ASG_ATTRIBUTES = ('AvailabilityZones', 'DefaultCooldown', 'DesiredCapacity', 'HealthCheckGracePeriod', 'HealthCheckType', 'LaunchConfigurationName', 'LoadBalancerNames', 'MaxInstanceLifetime', 'MaxSize', 'MinSize', 'AutoScalingGroupName', 'PlacementGroup', 'TerminationPolicies', 'VPCZoneIdentifier') INSTANCE_ATTRIBUTES = ('instance_id', 'health_status', 'lifecycle_state', 'launch_config_name') backoff_params = dict(retries=10, delay=3, backoff=1.5) @AWSRetry.jittered_backoff(**backoff_params) def describe_autoscaling_groups(connection, group_name): pg = connection.get_paginator('describe_auto_scaling_groups') return pg.paginate(AutoScalingGroupNames=[group_name]).build_full_result().get('AutoScalingGroups', []) @AWSRetry.jittered_backoff(**backoff_params) def deregister_lb_instances(connection, lb_name, instance_id): connection.deregister_instances_from_load_balancer(LoadBalancerName=lb_name, Instances=[dict(InstanceId=instance_id)]) @AWSRetry.jittered_backoff(**backoff_params) def describe_instance_health(connection, lb_name, instances): params = dict(LoadBalancerName=lb_name) if instances: params.update(Instances=instances) return connection.describe_instance_health(**params) @AWSRetry.jittered_backoff(**backoff_params) def describe_target_health(connection, target_group_arn, instances): return connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=instances) @AWSRetry.jittered_backoff(**backoff_params) def suspend_asg_processes(connection, asg_name, processes): connection.suspend_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) @AWSRetry.jittered_backoff(**backoff_params) def resume_asg_processes(connection, asg_name, processes): connection.resume_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) @AWSRetry.jittered_backoff(**backoff_params) def describe_launch_configurations(connection, launch_config_name): pg = connection.get_paginator('describe_launch_configurations') return pg.paginate(LaunchConfigurationNames=[launch_config_name]).build_full_result() @AWSRetry.jittered_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 is_boto3_error_code('InvalidLaunchTemplateName.NotFoundException'): 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 is_boto3_error_code('InvalidLaunchTemplateName.NotFoundException'): module.fail_json(msg="No launch template found matching: %s" % launch_template) @AWSRetry.jittered_backoff(**backoff_params) def create_asg(connection, **params): connection.create_auto_scaling_group(**params) @AWSRetry.jittered_backoff(**backoff_params) def put_notification_config(connection, asg_name, topic_arn, notification_types): connection.put_notification_configuration( AutoScalingGroupName=asg_name, TopicARN=topic_arn, NotificationTypes=notification_types ) @AWSRetry.jittered_backoff(**backoff_params) def del_notification_config(connection, asg_name, topic_arn): connection.delete_notification_configuration( AutoScalingGroupName=asg_name, TopicARN=topic_arn ) @AWSRetry.jittered_backoff(**backoff_params) def attach_load_balancers(connection, asg_name, load_balancers): connection.attach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) @AWSRetry.jittered_backoff(**backoff_params) def detach_load_balancers(connection, asg_name, load_balancers): connection.detach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) @AWSRetry.jittered_backoff(**backoff_params) def attach_lb_target_groups(connection, asg_name, target_group_arns): connection.attach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) @AWSRetry.jittered_backoff(**backoff_params) def detach_lb_target_groups(connection, asg_name, target_group_arns): connection.detach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) @AWSRetry.jittered_backoff(**backoff_params) def update_asg(connection, **params): connection.update_auto_scaling_group(**params) @AWSRetry.jittered_backoff(catch_extra_error_codes=['ScalingActivityInProgress'], **backoff_params) def delete_asg(connection, asg_name, force_delete): connection.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=force_delete) @AWSRetry.jittered_backoff(**backoff_params) def terminate_asg_instance(connection, instance_id, decrement_capacity): connection.terminate_instance_in_auto_scaling_group(InstanceId=instance_id, ShouldDecrementDesiredCapacity=decrement_capacity) @AWSRetry.jittered_backoff(**backoff_params) def detach_asg_instances(connection, instance_ids, as_group_name, decrement_capacity): connection.detach_instances(InstanceIds=instance_ids, AutoScalingGroupName=as_group_name, ShouldDecrementDesiredCapacity=decrement_capacity) 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 = [] 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: %s" % ",".join(missing_args)) def get_properties(autoscaling_group): properties = dict( healthy_instances=0, in_service_instances=0, unhealthy_instances=0, pending_instances=0, viable_instances=0, terminating_instances=0 ) 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'] } if 'LaunchConfigurationName' in i: instance_facts[i['InstanceId']]['launch_config_name'] = i['LaunchConfigurationName'] elif 'LaunchTemplate' in i: instance_facts[i['InstanceId']]['launch_template'] = i['LaunchTemplate'] if i['HealthStatus'] == 'Healthy' and i['LifecycleState'] == 'InService': properties['viable_instances'] += 1 if i['HealthStatus'] == 'Healthy': properties['healthy_instances'] += 1 else: properties['unhealthy_instances'] += 1 if i['LifecycleState'] == 'InService': properties['in_service_instances'] += 1 if i['LifecycleState'] == 'Terminating': properties['terminating_instances'] += 1 if i['LifecycleState'] == 'Pending': properties['pending_instances'] += 1 else: properties['instances'] = [] properties['auto_scaling_group_name'] = autoscaling_group.get('AutoScalingGroupName') properties['auto_scaling_group_arn'] = autoscaling_group.get('AutoScalingGroupARN') properties['availability_zones'] = autoscaling_group.get('AvailabilityZones') properties['created_time'] = autoscaling_group.get('CreatedTime') properties['instance_facts'] = instance_facts properties['load_balancers'] = autoscaling_group.get('LoadBalancerNames') if 'LaunchConfigurationName' in autoscaling_group: properties['launch_config_name'] = autoscaling_group.get('LaunchConfigurationName') else: properties['launch_template'] = autoscaling_group.get('LaunchTemplate') properties['tags'] = autoscaling_group.get('Tags') properties['max_instance_lifetime'] = autoscaling_group.get('MaxInstanceLifetime') properties['min_size'] = autoscaling_group.get('MinSize') properties['max_size'] = autoscaling_group.get('MaxSize') properties['desired_capacity'] = autoscaling_group.get('DesiredCapacity') properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown') properties['healthcheck_grace_period'] = autoscaling_group.get('HealthCheckGracePeriod') properties['healthcheck_type'] = autoscaling_group.get('HealthCheckType') properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown') 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_full'] = camel_dict_to_snake_dict(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"]) properties['metrics_collection'] = metrics if properties["target_group_arns"]: elbv2_connection = module.client("elbv2") tg_paginator = elbv2_connection.get_paginator("describe_target_groups") # Limit of 20 similar to https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html tg_chunk_size = 20 properties["target_group_names"] = [] tg_chunks = [ properties["target_group_arns"][i: i + tg_chunk_size] for i in range(0, len(properties["target_group_arns"]), tg_chunk_size) ] for chunk in tg_chunks: tg_result = tg_paginator.paginate(TargetGroupArns=chunk).build_full_result() properties["target_group_names"].extend([tg["TargetGroupName"] for tg in tg_result["TargetGroups"]]) else: properties["target_group_names"] = [] 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') 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: try: launch_configs = describe_launch_configurations(connection, launch_config_name) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to describe launch configurations") 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'])}} if mixed_instances_policy: instance_types = mixed_instances_policy.get('instance_types', []) instances_distribution = mixed_instances_policy.get('instances_distribution', {}) 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) if instances_distribution: instances_distribution_params = scrub_none_parameters(instances_distribution) policy['InstancesDistribution'] = snake_dict_to_camel_dict(instances_distribution_params, capitalize_first=True) launch_object['MixedInstancesPolicy'] = policy return launch_object def elb_dreg(asg_connection, group_name, instance_id): as_group = describe_autoscaling_groups(asg_connection, group_name)[0] wait_timeout = module.params.get('wait_timeout') count = 1 if as_group['LoadBalancerNames'] and as_group['HealthCheckType'] == 'ELB': elb_connection = module.client('elb') else: return for lb in as_group['LoadBalancerNames']: deregister_lb_instances(elb_connection, lb, instance_id) module.debug("De-registering %s from ELB %s" % (instance_id, lb)) wait_timeout = time.time() + wait_timeout while wait_timeout > time.time() and count > 0: count = 0 for lb in as_group['LoadBalancerNames']: lb_instances = describe_instance_health(elb_connection, lb, []) for i in lb_instances['InstanceStates']: if i['InstanceId'] == instance_id and i['State'] == "InService": count += 1 module.debug("%s: %s, %s" % (i['InstanceId'], i['State'], i['Description'])) time.sleep(10) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for instance to deregister. {0}".format(time.asctime())) def elb_healthy(asg_connection, elb_connection, group_name): healthy_instances = set() as_group = describe_autoscaling_groups(asg_connection, group_name)[0] props = get_properties(as_group) # get healthy, inservice instances from ASG instances = [] for instance, settings in props['instance_facts'].items(): if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy': instances.append(dict(InstanceId=instance)) module.debug("ASG considers the following instances InService and Healthy: %s" % instances) module.debug("ELB instance status:") lb_instances = list() for lb in as_group.get('LoadBalancerNames'): # we catch a race condition that sometimes happens if the instance exists in the ASG # but has not yet show up in the ELB try: lb_instances = describe_instance_health(elb_connection, lb, instances) except is_boto3_error_code('InvalidInstance'): return None except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except module.fail_json_aws(e, msg="Failed to get load balancer.") for i in lb_instances.get('InstanceStates'): if i['State'] == "InService": healthy_instances.add(i['InstanceId']) module.debug("ELB Health State %s: %s" % (i['InstanceId'], i['State'])) return len(healthy_instances) def tg_healthy(asg_connection, elbv2_connection, group_name): healthy_instances = set() as_group = describe_autoscaling_groups(asg_connection, group_name)[0] props = get_properties(as_group) # get healthy, inservice instances from ASG instances = [] for instance, settings in props['instance_facts'].items(): if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy': instances.append(dict(Id=instance)) module.debug("ASG considers the following instances InService and Healthy: %s" % instances) module.debug("Target Group instance status:") tg_instances = list() for tg in as_group.get('TargetGroupARNs'): # we catch a race condition that sometimes happens if the instance exists in the ASG # but has not yet show up in the ELB try: tg_instances = describe_target_health(elbv2_connection, tg, instances) except is_boto3_error_code('InvalidInstance'): return None except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except module.fail_json_aws(e, msg="Failed to get target group.") for i in tg_instances.get('TargetHealthDescriptions'): if i['TargetHealth']['State'] == "healthy": healthy_instances.add(i['Target']['Id']) module.debug("Target Group Health State %s: %s" % (i['Target']['Id'], i['TargetHealth']['State'])) return len(healthy_instances) def wait_for_elb(asg_connection, group_name): wait_timeout = module.params.get('wait_timeout') # if the health_check_type is ELB, we want to query the ELBs directly for instance # status as to avoid health_check_grace period that is awarded to ASG instances as_group = describe_autoscaling_groups(asg_connection, group_name)[0] if as_group.get('LoadBalancerNames') and as_group.get('HealthCheckType') == 'ELB': module.debug("Waiting for ELB to consider instances healthy.") elb_connection = module.client('elb') wait_timeout = time.time() + wait_timeout healthy_instances = elb_healthy(asg_connection, elb_connection, group_name) while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time(): healthy_instances = elb_healthy(asg_connection, elb_connection, group_name) module.debug("ELB thinks %s instances are healthy." % healthy_instances) time.sleep(10) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime()) module.debug("Waiting complete. ELB thinks %s instances are healthy." % healthy_instances) def wait_for_target_group(asg_connection, group_name): wait_timeout = module.params.get('wait_timeout') # if the health_check_type is ELB, we want to query the ELBs directly for instance # status as to avoid health_check_grace period that is awarded to ASG instances as_group = describe_autoscaling_groups(asg_connection, group_name)[0] if as_group.get('TargetGroupARNs') and as_group.get('HealthCheckType') == 'ELB': module.debug("Waiting for Target Group to consider instances healthy.") elbv2_connection = module.client('elbv2') wait_timeout = time.time() + wait_timeout healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name) while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time(): healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name) module.debug("Target Group thinks %s instances are healthy." % healthy_instances) time.sleep(10) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime()) module.debug("Waiting complete. Target Group thinks %s instances are healthy." % healthy_instances) def suspend_processes(ec2_connection, as_group): suspend_processes = set(module.params.get('suspend_processes')) try: suspended_processes = set([p['ProcessName'] for p in as_group['SuspendedProcesses']]) except AttributeError: # New ASG being created, no suspended_processes defined yet suspended_processes = set() if suspend_processes == suspended_processes: return False resume_processes = list(suspended_processes - suspend_processes) if resume_processes: resume_asg_processes(ec2_connection, module.params.get('name'), resume_processes) if suspend_processes: suspend_asg_processes(ec2_connection, module.params.get('name'), list(suspend_processes)) return True def create_autoscaling_group(connection): group_name = module.params.get('name') load_balancers = module.params['load_balancers'] 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') mixed_instances_policy = module.params.get('mixed_instances_policy') min_size = module.params['min_size'] max_size = module.params['max_size'] max_instance_lifetime = module.params.get('max_instance_lifetime') placement_group = module.params.get('placement_group') desired_capacity = module.params.get('desired_capacity') vpc_zone_identifier = module.params.get('vpc_zone_identifier') set_tags = module.params.get('tags') purge_tags = module.params.get('purge_tags') health_check_period = module.params.get('health_check_period') health_check_type = module.params.get('health_check_type') default_cooldown = module.params.get('default_cooldown') wait_for_instances = module.params.get('wait_for_instances') wait_timeout = module.params.get('wait_timeout') termination_policies = module.params.get('termination_policies') notification_topic = module.params.get('notification_topic') notification_types = module.params.get('notification_types') metrics_collection = module.params.get('metrics_collection') metrics_granularity = module.params.get('metrics_granularity') metrics_list = module.params.get('metrics_list') try: as_groups = describe_autoscaling_groups(connection, group_name) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to describe auto scaling groups.") ec2_connection = module.client('ec2') if vpc_zone_identifier: vpc_zone_identifier = ','.join(vpc_zone_identifier) asg_tags = [] for tag in set_tags: for k, v in tag.items(): if k != 'propagate_at_launch': asg_tags.append(dict(Key=k, Value=to_native(v), PropagateAtLaunch=bool(tag.get('propagate_at_launch', True)), ResourceType='auto-scaling-group', ResourceId=group_name)) if not as_groups: if module.check_mode: module.exit_json(changed=True, msg="Would have created AutoScalingGroup if not in check_mode.") 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_for_create() if desired_capacity is None: desired_capacity = min_size ag = dict( AutoScalingGroupName=group_name, MinSize=min_size, MaxSize=max_size, DesiredCapacity=desired_capacity, Tags=asg_tags, HealthCheckGracePeriod=health_check_period, HealthCheckType=health_check_type, DefaultCooldown=default_cooldown, TerminationPolicies=termination_policies) if vpc_zone_identifier: ag['VPCZoneIdentifier'] = vpc_zone_identifier if availability_zones: ag['AvailabilityZones'] = availability_zones if placement_group: ag['PlacementGroup'] = placement_group if load_balancers: ag['LoadBalancerNames'] = load_balancers if target_group_arns: ag['TargetGroupARNs'] = target_group_arns if max_instance_lifetime: ag['MaxInstanceLifetime'] = max_instance_lifetime launch_object = get_launch_object(connection, ec2_connection) if 'LaunchConfigurationName' in launch_object: ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName'] elif 'LaunchTemplate' in launch_object: if 'MixedInstancesPolicy' in launch_object: ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] else: ag['LaunchTemplate'] = launch_object['LaunchTemplate'] else: module.fail_json_aws(e, msg="Missing LaunchConfigurationName or LaunchTemplate") try: create_asg(connection, **ag) if metrics_collection: connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list) all_ag = describe_autoscaling_groups(connection, group_name) if len(all_ag) == 0: module.fail_json(msg="No auto scaling group found with the name %s" % group_name) as_group = all_ag[0] suspend_processes(connection, as_group) if wait_for_instances: wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances') if load_balancers: wait_for_elb(connection, group_name) # Wait for target group health if target group(s)defined if target_group_arns: wait_for_target_group(connection, group_name) if notification_topic: put_notification_config(connection, group_name, notification_topic, notification_types) as_group = describe_autoscaling_groups(connection, group_name)[0] asg_properties = get_properties(as_group) changed = True return changed, asg_properties except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to create Autoscaling Group.") else: if module.check_mode: module.exit_json(changed=True, msg="Would have modified AutoScalingGroup if required if not in check_mode.") as_group = as_groups[0] initial_asg_properties = get_properties(as_group) changed = False if suspend_processes(connection, as_group): changed = True # process tag changes have_tags = as_group.get('Tags') want_tags = asg_tags if purge_tags and not want_tags and have_tags: connection.delete_tags(Tags=list(have_tags)) if len(set_tags) > 0: if have_tags: have_tags.sort(key=lambda x: x["Key"]) if want_tags: want_tags.sort(key=lambda x: x["Key"]) dead_tags = [] have_tag_keyvals = [x['Key'] for x in have_tags] want_tag_keyvals = [x['Key'] for x in want_tags] for dead_tag in set(have_tag_keyvals).difference(want_tag_keyvals): changed = True if purge_tags: dead_tags.append(dict( ResourceId=as_group['AutoScalingGroupName'], ResourceType='auto-scaling-group', Key=dead_tag)) have_tags = [have_tag for have_tag in have_tags if have_tag['Key'] != dead_tag] if dead_tags: connection.delete_tags(Tags=dead_tags) zipped = zip(have_tags, want_tags) if len(have_tags) != len(want_tags) or not all(x == y for x, y in zipped): changed = True connection.create_or_update_tags(Tags=asg_tags) # Handle load balancer attachments/detachments # Attach load balancers if they are specified but none currently exist if load_balancers and not as_group['LoadBalancerNames']: changed = True try: attach_load_balancers(connection, group_name, load_balancers) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to update Autoscaling Group.") # Update load balancers if they are specified and one or more already exists elif as_group['LoadBalancerNames']: change_load_balancers = load_balancers is not None # Get differences if not load_balancers: load_balancers = list() wanted_elbs = set(load_balancers) has_elbs = set(as_group['LoadBalancerNames']) # check if all requested are already existing if has_elbs - wanted_elbs and change_load_balancers: # if wanted contains less than existing, then we need to delete some elbs_to_detach = has_elbs.difference(wanted_elbs) if elbs_to_detach: changed = True try: detach_load_balancers(connection, group_name, list(elbs_to_detach)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to detach load balancers {0}".format(elbs_to_detach)) if wanted_elbs - has_elbs: # if has contains less than wanted, then we need to add some elbs_to_attach = wanted_elbs.difference(has_elbs) if elbs_to_attach: changed = True try: attach_load_balancers(connection, group_name, list(elbs_to_attach)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to attach load balancers {0}".format(elbs_to_attach)) # Handle target group attachments/detachments # Attach target groups if they are specified but none currently exist if target_group_arns and not as_group['TargetGroupARNs']: changed = True try: attach_lb_target_groups(connection, group_name, target_group_arns) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to update Autoscaling Group.") # Update target groups if they are specified and one or more already exists elif target_group_arns is not None and as_group['TargetGroupARNs']: # Get differences wanted_tgs = set(target_group_arns) has_tgs = set(as_group['TargetGroupARNs']) tgs_to_detach = has_tgs.difference(wanted_tgs) if tgs_to_detach: changed = True try: detach_lb_target_groups(connection, group_name, list(tgs_to_detach)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to detach load balancer target groups {0}".format(tgs_to_detach)) tgs_to_attach = wanted_tgs.difference(has_tgs) if tgs_to_attach: changed = True try: attach_lb_target_groups(connection, group_name, list(tgs_to_attach)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json(msg="Failed to attach load balancer target groups {0}".format(tgs_to_attach)) # check for attributes that aren't required for updating an existing ASG # check if min_size/max_size/desired capacity have been specified and if not use ASG values if min_size is None: min_size = as_group['MinSize'] if max_size is None: max_size = as_group['MaxSize'] if desired_capacity is None: desired_capacity = as_group['DesiredCapacity'] ag = dict( AutoScalingGroupName=group_name, MinSize=min_size, MaxSize=max_size, DesiredCapacity=desired_capacity, HealthCheckGracePeriod=health_check_period, 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: if 'MixedInstancesPolicy' in launch_object: ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy'] else: ag['LaunchTemplate'] = launch_object['LaunchTemplate'] else: try: ag['LaunchConfigurationName'] = as_group['LaunchConfigurationName'] except Exception: 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: ag['VPCZoneIdentifier'] = vpc_zone_identifier if max_instance_lifetime is not None: ag['MaxInstanceLifetime'] = max_instance_lifetime try: update_asg(connection, **ag) if metrics_collection: connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list) else: connection.disable_metrics_collection(AutoScalingGroupName=group_name, Metrics=metrics_list) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to update autoscaling group") if notification_topic: try: put_notification_config(connection, group_name, notification_topic, notification_types) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to update Autoscaling Group notifications.") if wait_for_instances: wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances') # Wait for ELB health if ELB(s)defined if load_balancers: module.debug('\tWAITING FOR ELB HEALTH') wait_for_elb(connection, group_name) # Wait for target group health if target group(s)defined if target_group_arns: module.debug('\tWAITING FOR TG HEALTH') wait_for_target_group(connection, group_name) try: as_group = describe_autoscaling_groups(connection, group_name)[0] asg_properties = get_properties(as_group) if asg_properties != initial_asg_properties: changed = True except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to read existing Autoscaling Groups.") return changed, asg_properties def delete_autoscaling_group(connection): group_name = module.params.get('name') notification_topic = module.params.get('notification_topic') wait_for_instances = module.params.get('wait_for_instances') wait_timeout = module.params.get('wait_timeout') if notification_topic: del_notification_config(connection, group_name, notification_topic) groups = describe_autoscaling_groups(connection, group_name) if groups: if module.check_mode: module.exit_json(changed=True, msg="Would have deleted AutoScalingGroup if not in check_mode.") wait_timeout = time.time() + wait_timeout if not wait_for_instances: delete_asg(connection, group_name, force_delete=True) else: updated_params = dict(AutoScalingGroupName=group_name, MinSize=0, MaxSize=0, DesiredCapacity=0) update_asg(connection, **updated_params) instances = True while instances and wait_for_instances and wait_timeout >= time.time(): tmp_groups = describe_autoscaling_groups(connection, group_name) if tmp_groups: tmp_group = tmp_groups[0] if not tmp_group.get('Instances'): instances = False time.sleep(10) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime()) delete_asg(connection, group_name, force_delete=False) while describe_autoscaling_groups(connection, group_name) and wait_timeout >= time.time(): time.sleep(5) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for ASG to delete. %s" % time.asctime()) return True return False def get_chunks(l, n): for i in range(0, len(l), n): yield l[i:i + n] def update_size(connection, group, max_size, min_size, dc): module.debug("setting ASG sizes") module.debug("minimum size: %s, desired_capacity: %s, max size: %s" % (min_size, dc, max_size)) updated_group = dict() updated_group['AutoScalingGroupName'] = group['AutoScalingGroupName'] updated_group['MinSize'] = min_size updated_group['MaxSize'] = max_size updated_group['DesiredCapacity'] = dc update_asg(connection, **updated_group) def replace(connection): batch_size = module.params.get('replace_batch_size') wait_timeout = module.params.get('wait_timeout') wait_for_instances = module.params.get('wait_for_instances') group_name = module.params.get('name') max_size = module.params.get('max_size') min_size = module.params.get('min_size') desired_capacity = module.params.get('desired_capacity') 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 behavior 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') as_group = describe_autoscaling_groups(connection, group_name)[0] if desired_capacity is None: desired_capacity = as_group['DesiredCapacity'] if wait_for_instances: wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'], 'viable_instances') props = get_properties(as_group) instances = props['instances'] if replace_all_instances: # If replacing all instances, then set replace_instances to current set # This allows replace_instances and replace_all_instances to behave same 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) num_new_inst_needed = desired_capacity - len(new_instances) 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) as_group = describe_autoscaling_groups(connection, group_name)[0] props = get_properties(as_group) changed = True return changed, props # we don't want to spin up extra instances if not necessary if num_new_inst_needed < batch_size: module.debug("Overriding batch size to %s" % num_new_inst_needed) batch_size = num_new_inst_needed if not old_instances: changed = False return changed, props # check if min_size/max_size/desired capacity have been specified and if not use ASG values if min_size is None: min_size = as_group['MinSize'] if max_size is None: max_size = as_group['MaxSize'] # set temporary settings and wait for them to be reached # This should get overwritten if the number of instances left is less than the batch size. as_group = describe_autoscaling_groups(connection, group_name)[0] update_size(connection, as_group, max_size + batch_size, min_size + batch_size, desired_capacity + batch_size) if wait_for_instances: wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'] + batch_size, 'viable_instances') wait_for_elb(connection, group_name) wait_for_target_group(connection, group_name) as_group = describe_autoscaling_groups(connection, group_name)[0] props = get_properties(as_group) instances = props['instances'] if replace_instances: instances = replace_instances module.debug("beginning main loop") for i in get_chunks(instances, batch_size): # break out of this loop if we have enough new instances break_early, desired_size, term_instances = terminate_batch(connection, i, instances, False) if wait_for_instances: wait_for_term_inst(connection, term_instances) wait_for_new_inst(connection, group_name, wait_timeout, desired_size, 'viable_instances') wait_for_elb(connection, group_name) wait_for_target_group(connection, group_name) if break_early: module.debug("breaking loop") break update_size(connection, as_group, max_size, min_size, desired_capacity) as_group = describe_autoscaling_groups(connection, group_name)[0] asg_properties = get_properties(as_group) module.debug("Rolling update complete.") changed = True return changed, asg_properties def detach(connection): group_name = module.params.get('name') detach_instances = module.params.get('detach_instances') as_group = describe_autoscaling_groups(connection, group_name)[0] decrement_desired_capacity = module.params.get('decrement_desired_capacity') min_size = module.params.get('min_size') props = get_properties(as_group) instances = props['instances'] # check if provided instance exists in asg, create list of instances to detach which exist in asg instances_to_detach = [] for instance_id in detach_instances: if instance_id in instances: instances_to_detach.append(instance_id) # check if setting decrement_desired_capacity will make desired_capacity smaller # than the currently set minimum size in ASG configuration if decrement_desired_capacity: decremented_desired_capacity = len(instances) - len(instances_to_detach) if min_size and min_size > decremented_desired_capacity: module.fail_json( msg="Detaching instance(s) with 'decrement_desired_capacity' flag set reduces number of instances to {0}\ which is below current min_size {1}, please update AutoScalingGroup Sizes properly.".format(decremented_desired_capacity, min_size)) if instances_to_detach: try: detach_asg_instances(connection, instances_to_detach, group_name, decrement_desired_capacity) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to detach instances from AutoScaling Group") asg_properties = get_properties(as_group) return True, asg_properties 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']: # 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].get('launch_config_name') == props['launch_config_name']: 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 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 template 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].get('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 'launch_config_name' in module.params: if lc_check: for i in instances: if ( 'launch_template' in props['instance_facts'][i] or 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 'launch_template' in module.params: if lt_check: for i in instances: if ( 'launch_config_name' in props['instance_facts'][i] or 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 def terminate_batch(connection, replace_instances, initial_instances, leftovers=False): batch_size = module.params.get('replace_batch_size') min_size = module.params.get('min_size') 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 as_group = describe_autoscaling_groups(connection, group_name)[0] if desired_capacity is None: desired_capacity = as_group['DesiredCapacity'] props = get_properties(as_group) desired_size = as_group['MinSize'] 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, lt_check, replace_instances, initial_instances) module.debug("new instances needed: %s" % num_new_inst_needed) module.debug("new instances: %s" % new_instances) module.debug("old instances: %s" % old_instances) module.debug("batch instances: %s" % ",".join(instances_to_terminate)) if num_new_inst_needed == 0: decrement_capacity = True if as_group['MinSize'] != min_size: if min_size is None: min_size = as_group['MinSize'] updated_params = dict(AutoScalingGroupName=as_group['AutoScalingGroupName'], MinSize=min_size) update_asg(connection, **updated_params) module.debug("Updating minimum size back to original of %s" % min_size) # if are some leftover old instances, but we are already at capacity with new ones # we don't want to decrement capacity if leftovers: decrement_capacity = False break_loop = True instances_to_terminate = old_instances desired_size = min_size module.debug("No new instances needed") if num_new_inst_needed < batch_size and num_new_inst_needed != 0: instances_to_terminate = instances_to_terminate[:num_new_inst_needed] decrement_capacity = False break_loop = False module.debug("%s new instances needed" % num_new_inst_needed) module.debug("decrementing capacity: %s" % decrement_capacity) for instance_id in instances_to_terminate: elb_dreg(connection, group_name, instance_id) module.debug("terminating instance: %s" % instance_id) terminate_asg_instance(connection, instance_id, decrement_capacity) # we wait to make sure the machines we marked as Unhealthy are # no longer in the list return break_loop, desired_size, instances_to_terminate def wait_for_term_inst(connection, term_instances): wait_timeout = module.params.get('wait_timeout') group_name = module.params.get('name') as_group = describe_autoscaling_groups(connection, group_name)[0] count = 1 wait_timeout = time.time() + wait_timeout while wait_timeout > time.time() and count > 0: module.debug("waiting for instances to terminate") count = 0 as_group = describe_autoscaling_groups(connection, group_name)[0] props = get_properties(as_group) instance_facts = props['instance_facts'] instances = (i for i in instance_facts if i in term_instances) for i in instances: lifecycle = instance_facts[i]['lifecycle_state'] health = instance_facts[i]['health_status'] module.debug("Instance %s has state of %s,%s" % (i, lifecycle, health)) if lifecycle.startswith('Terminating') or health == 'Unhealthy': count += 1 time.sleep(10) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime()) def wait_for_new_inst(connection, group_name, wait_timeout, desired_size, prop): # make sure we have the latest stats after that last loop. as_group = describe_autoscaling_groups(connection, group_name)[0] props = get_properties(as_group) module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop])) # now we make sure that we have enough instances in a viable state wait_timeout = time.time() + wait_timeout while wait_timeout > time.time() and desired_size > props[prop]: module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop])) time.sleep(10) as_group = describe_autoscaling_groups(connection, group_name)[0] props = get_properties(as_group) if wait_timeout <= time.time(): # waiting took too long module.fail_json(msg="Waited too long for new instances to become viable. %s" % time.asctime()) module.debug("Reached %s: %s" % (prop, desired_size)) return props def asg_exists(connection): group_name = module.params.get('name') as_group = describe_autoscaling_groups(connection, group_name) return bool(len(as_group)) def main(): argument_spec = dict( name=dict(required=True, type='str'), load_balancers=dict(type='list', elements='str'), target_group_arns=dict(type='list', elements='str'), availability_zones=dict(type='list', elements='str'), 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'), max_instance_lifetime=dict(type='int'), mixed_instances_policy=dict( type='dict', default=None, options=dict( instance_types=dict( type='list', elements='str' ), instances_distribution=dict( type='dict', default=None, options=dict( on_demand_allocation_strategy=dict(type='str'), on_demand_base_capacity=dict(type='int'), on_demand_percentage_above_base_capacity=dict(type='int'), spot_allocation_strategy=dict(type='str'), spot_instance_pools=dict(type='int'), spot_max_price=dict(type='str'), ) ) ) ), placement_group=dict(type='str'), desired_capacity=dict(type='int'), vpc_zone_identifier=dict(type='list', elements='str'), replace_batch_size=dict(type='int', default=1), replace_all_instances=dict(type='bool', default=False), replace_instances=dict(type='list', default=[], elements='str'), detach_instances=dict(type='list', default=[], elements='str'), decrement_desired_capacity=dict(type='bool', default=False), 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=[], elements='dict'), purge_tags=dict(type='bool', default=False), health_check_period=dict(type='int', default=300), health_check_type=dict(default='EC2', choices=['EC2', 'ELB']), default_cooldown=dict(type='int', default=300), wait_for_instances=dict(type='bool', default=True), termination_policies=dict(type='list', default='Default', elements='str'), notification_topic=dict(type='str', default=None), notification_types=dict( type='list', default=[ 'autoscaling:EC2_INSTANCE_LAUNCH', 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', 'autoscaling:EC2_INSTANCE_TERMINATE', 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' ], elements='str' ), suspend_processes=dict(type='list', default=[], elements='str'), metrics_collection=dict(type='bool', default=False), metrics_granularity=dict(type='str', default='1Minute'), metrics_list=dict( type='list', default=[ 'GroupMinSize', 'GroupMaxSize', 'GroupDesiredCapacity', 'GroupInServiceInstances', 'GroupPendingInstances', 'GroupStandbyInstances', 'GroupTerminatingInstances', 'GroupTotalInstances' ], elements='str' ) ) global module module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True, mutually_exclusive=[ ['replace_all_instances', 'replace_instances'], ['replace_all_instances', 'detach_instances'], ['launch_config_name', 'launch_template'], ] ) state = module.params.get('state') replace_instances = module.params.get('replace_instances') replace_all_instances = module.params.get('replace_all_instances') detach_instances = module.params.get('detach_instances') connection = module.client('autoscaling') changed = create_changed = replace_changed = detach_changed = False exists = asg_exists(connection) if state == 'present': create_changed, asg_properties = create_autoscaling_group(connection) elif state == 'absent': changed = delete_autoscaling_group(connection) module.exit_json(changed=changed) # Only replace instances if asg existed at start of call 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) # Only detach instances if asg existed at start of call if ( exists and (detach_instances) and (module.params.get('launch_config_name') or module.params.get('launch_template')) ): detach_changed, asg_properties = detach(connection) if create_changed or replace_changed or detach_changed: changed = True module.exit_json(changed=changed, **asg_properties) if __name__ == '__main__': main()