# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ Purpose Shows how to use the AWS SDK for Python (Boto3) with Amazon EC2 Auto Scaling to manage groups and instances. """ import logging from typing import Any, Dict, List, Optional from botocore.exceptions import ClientError logger = logging.getLogger(__name__) # snippet-start:[python.example_code.auto-scaling.AutoScalingWrapper.decl] class AutoScalingWrapper: """Encapsulates Amazon EC2 Auto Scaling actions.""" def __init__(self, autoscaling_client): """ :param autoscaling_client: A Boto3 Amazon EC2 Auto Scaling client. """ self.autoscaling_client = autoscaling_client # snippet-end:[python.example_code.auto-scaling.AutoScalingWrapper.decl] # snippet-start:[python.example_code.auto-scaling.CreateAutoScalingGroup] def create_group( self, group_name: str, group_zones: List[str], launch_template_name: str, min_size: int, max_size: int, ) -> None: """ Creates an Auto Scaling group. :param group_name: The name to give to the group. :param group_zones: The Availability Zones in which instances can be created. :param launch_template_name: The name of an existing Amazon EC2 launch template. The launch template specifies the configuration of instances that are created by auto scaling activities. :param min_size: The minimum number of active instances in the group. :param max_size: The maximum number of active instances in the group. :return: None :raises ClientError: If there is an error creating the Auto Scaling group. """ try: self.autoscaling_client.create_auto_scaling_group( AutoScalingGroupName=group_name, AvailabilityZones=group_zones, LaunchTemplate={ "LaunchTemplateName": launch_template_name, "Version": "$Default", }, MinSize=min_size, MaxSize=max_size, ) # Wait for the group to exist. waiter = self.autoscaling_client.get_waiter("group_exists") waiter.wait(AutoScalingGroupNames=[group_name]) logger.info(f"Successfully created Auto Scaling group {group_name}.") except ClientError as err: error_code = err.response["Error"]["Code"] logger.error(f"Failed to create Auto Scaling group {group_name}.") if error_code == "AlreadyExistsFault": logger.error( f"An Auto Scaling group with the name '{group_name}' already exists. " "Please use a different name or update the existing group.", ) elif error_code == "LimitExceededFault": logger.error( "The request failed because you have reached the limit " "on the number of Auto Scaling groups or launch configurations. " "Consider deleting unused resources or request a limit increase. " "\nSee Auto Scaling Service Quota documentation here:" "\n\thttps://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-quotas.html" ) logger.error(f"Full error:\n\t{err}") raise # snippet-end:[python.example_code.auto-scaling.CreateAutoScalingGroup] # snippet-start:[python.example_code.auto-scaling.UpdateAutoScalingGroup] def update_group(self, group_name: str, **kwargs: Any) -> None: """ Updates an Auto Scaling group. :param group_name: The name of the group to update. :param kwargs: Keyword arguments to pass through to the service. :return: None :raises ClientError: If there is an error updating the Auto Scaling group. """ try: self.autoscaling_client.update_auto_scaling_group( AutoScalingGroupName=group_name, **kwargs ) logger.info(f"Successfully updated Auto Scaling group {group_name}.") except ClientError as err: error_code = err.response["Error"]["Code"] logger.error(f"Failed to update Auto Scaling group {group_name}.") if error_code == "ResourceInUse": logger.error( "The Auto Scaling group '%s' is currently in use and cannot be modified. Please try again later.", group_name, ) elif error_code == "ScalingActivityInProgress": logger.error( f"A scaling activity is currently in progress for the Auto Scaling group '{group_name}'." "Please wait for the activity to complete before attempting to update the group." ) logger.error(f"Full error:\n\t{err}") raise # snippet-end:[python.example_code.auto-scaling.UpdateAutoScalingGroup] # snippet-start:[python.example_code.auto-scaling.DeleteAutoScalingGroup] def delete_group(self, group_name: str) -> None: """ Deletes an Auto Scaling group. All instances must be stopped before the group can be deleted. :param group_name: The name of the group to delete. :return: None :raises ClientError: If there is an error deleting the Auto Scaling group. """ try: self.autoscaling_client.delete_auto_scaling_group( AutoScalingGroupName=group_name ) # Wait for the group to be deleted. waiter = self.autoscaling_client.get_waiter("group_not_exists") waiter.wait(AutoScalingGroupNames=[group_name]) logger.info(f"Successfully deleted Auto Scaling group {group_name}.") except ClientError as err: error_code = err.response["Error"]["Code"] logger.error(f"Failed to delete Auto Scaling group {group_name}.") if error_code == "ScalingActivityInProgress": logger.error( "A scaling activity is currently in progress for the Auto Scaling group '%s'. " "Please wait for the activity to complete before attempting to delete the group.", group_name, ) elif error_code == "ResourceInUse": logger.error( "The Auto Scaling group '%s' or one of its associated resources is currently in use and cannot be deleted. " "Ensure all instances are stopped and no other operations are pending before retrying.", group_name, ) logger.error(f"Full error:\n\t{err}") raise # snippet-end:[python.example_code.auto-scaling.DeleteAutoScalingGroup] # snippet-start:[python.example_code.auto-scaling.DescribeAutoScalingGroups] def describe_group(self, group_name: str) -> Optional[Dict[str, Any]]: """ Gets information about an Auto Scaling group. :param group_name: The name of the group to look up. :return: A dictionary with information about the group if found, otherwise None. :raises ClientError: If there is an error describing the Auto Scaling group. """ try: paginator = self.autoscaling_client.get_paginator( "describe_auto_scaling_groups" ) response_iterator = paginator.paginate(AutoScalingGroupNames=[group_name]) groups = [] for response in response_iterator: groups.extend(response.get("AutoScalingGroups", [])) logger.info( f"Successfully retrieved information for Auto Scaling group {group_name}." ) except ClientError as err: error_code = err.response["Error"]["Code"] logger.error(f"Failed to describe Auto Scaling group {group_name}.") if error_code == "ResourceContentionFault": logger.error( "There is a conflict with another operation that is modifying the " f"Auto Scaling group '{group_name}' Please try again later." ) logger.error(f"Full error:\n\t{err}") raise else: return groups[0] if len(groups) > 0 else None # snippet-end:[python.example_code.auto-scaling.DescribeAutoScalingGroups] # snippet-start:[python.example_code.auto-scaling.TerminateInstanceInAutoScalingGroup] def terminate_instance( self, instance_id: str, decrease_capacity: bool ) -> Dict[str, Any]: """ Stops an instance. :param instance_id: The ID of the instance to stop. :param decrease_capacity: Specifies whether to decrease the desired capacity of the group. When passing True for this parameter, you can stop an instance without having a replacement instance start when the desired capacity threshold is crossed. :return: A dictionary containing details of the scaling activity that occurs in response to this action. :raises ClientError: If there is an error terminating the instance. """ try: response = self.autoscaling_client.terminate_instance_in_auto_scaling_group( InstanceId=instance_id, ShouldDecrementDesiredCapacity=decrease_capacity ) logger.info(f"Successfully terminated instance {instance_id}.") return response["Activity"] except ClientError as err: error_code = err.response["Error"]["Code"] logger.error(f"Failed to terminate instance {instance_id}.") if error_code == "ScalingActivityInProgress": logger.error( "A scaling activity is currently in progress for the Auto Scaling group " f"associated with instance '{instance_id}'. " "Please wait for the activity to complete before attempting to terminate the instance." ) elif error_code == "ResourceInUse": logger.error( f"The instance '{instance_id}' or an associated resource is currently in use " "and cannot be terminated. " "Ensure the instance is not involved in any ongoing processes and try again." ) logger.error(f"Full error:\n\t{err}") raise # snippet-end:[python.example_code.auto-scaling.TerminateInstanceInAutoScalingGroup] # snippet-start:[python.example_code.auto-scaling.SetDesiredCapacity] def set_desired_capacity(self, group_name: str, capacity: int) -> None: """ Sets the desired capacity of the group. Amazon EC2 Auto Scaling tries to keep the number of running instances equal to the desired capacity. :param group_name: The name of the group to update. :param capacity: The desired number of running instances. :return: None :raises ClientError: If there is an error setting the desired capacity. """ try: self.autoscaling_client.set_desired_capacity( AutoScalingGroupName=group_name, DesiredCapacity=capacity, HonorCooldown=False, ) logger.info( f"Successfully set desired capacity of {capacity} for Auto Scaling group '{group_name}'." ) except ClientError as err: error_code = err.response["Error"]["Code"] logger.error( f"Failed to set desired capacity for Auto Scaling group '{group_name}'." ) if error_code == "ScalingActivityInProgress": logger.error( f"A scaling activity is currently in progress for the Auto Scaling group '{group_name}'. " "Please wait for the activity to complete before attempting to set the desired capacity." ) logger.error(f"Full error:\n\t{err}") raise # snippet-end:[python.example_code.auto-scaling.SetDesiredCapacity] # snippet-start:[python.example_code.auto-scaling.DescribeAutoScalingInstances] def describe_instances(self, instance_ids: List[str]) -> List[Dict[str, Any]]: """ Gets information about instances. :param instance_ids: A list of instance IDs to look up. :return: A list of dictionaries with information about each instance, or an empty list if none are found. :raises ClientError: If there is an error describing the instances. """ try: paginator = self.autoscaling_client.get_paginator( "describe_auto_scaling_instances" ) response_iterator = paginator.paginate(InstanceIds=instance_ids) instances = [] for response in response_iterator: instances.extend(response.get("AutoScalingInstances", [])) logger.info(f"Successfully described instances: {instance_ids}") except ClientError as err: error_code = err.response["Error"]["Code"] logger.error( f"Couldn't describe instances {instance_ids}. Error code: {error_code}, Message: {err.response['Error']['Message']}" ) raise else: return instances # snippet-end:[python.example_code.auto-scaling.DescribeAutoScalingInstances] # snippet-start:[python.example_code.auto-scaling.DescribeScalingActivities] def describe_scaling_activities(self, group_name: str) -> List[Dict[str, Any]]: """ Gets information about scaling activities for the group. Scaling activities are things like instances stopping or starting in response to user requests or capacity changes. :param group_name: The name of the group to look up. :return: A list of dictionaries representing the scaling activities for the group, ordered with the most recent activity first. :raises ClientError: If there is an error describing the scaling activities. """ try: paginator = self.autoscaling_client.get_paginator( "describe_scaling_activities" ) response_iterator = paginator.paginate(AutoScalingGroupName=group_name) activities = [] for response in response_iterator: activities.extend(response.get("Activities", [])) logger.info( f"Successfully described scaling activities for group '{group_name}'." ) except ClientError as err: error_code = err.response["Error"]["Code"] logger.error( f"Couldn't describe scaling activities for group '{group_name}'. Error code: {error_code}, Message: {err.response['Error']['Message']}" ) if error_code == "ResourceContentionFault": logger.error( f"There is a conflict with another operation that is modifying the Auto Scaling group '{group_name}'. " "Please try again later." ) raise else: return activities # snippet-end:[python.example_code.auto-scaling.DescribeScalingActivities] # snippet-start:[python.example_code.auto-scaling.EnableMetricsCollection] def enable_metrics(self, group_name: str, metrics: List[str]) -> Dict[str, Any]: """ Enables CloudWatch metric collection for Amazon EC2 Auto Scaling activities. :param group_name: The name of the group to enable. :param metrics: A list of metrics to collect. :return: A dictionary with the response from enabling the metrics collection. :raises ClientError: If there is an error enabling metrics collection. """ try: response = self.autoscaling_client.enable_metrics_collection( AutoScalingGroupName=group_name, Metrics=metrics, Granularity="1Minute" ) logger.info( f"Successfully enabled metrics for Auto Scaling group '{group_name}'." ) except ClientError as err: error_code = err.response["Error"]["Code"] logger.error( f"Couldn't enable metrics on '{group_name}'. Error code: {error_code}, Message: {err.response['Error']['Message']}" ) if error_code == "ResourceContentionFault": logger.error( f"There is a conflict with another operation that is modifying the Auto Scaling group '{group_name}'. " "Please try again later." ) elif error_code == "InvalidParameterCombination": logger.error( f"The combination of parameters provided for enabling metrics on '{group_name}' is not valid. " "Please check the parameters and try again." ) raise else: return response # snippet-end:[python.example_code.auto-scaling.EnableMetricsCollection] # snippet-start:[python.example_code.auto-scaling.DisableMetricsCollection] def disable_metrics(self, group_name: str) -> Dict[str, Any]: """ Stops CloudWatch metric collection for the Auto Scaling group. :param group_name: The name of the group. :return: A dictionary with the response from disabling the metrics collection. :raises ClientError: If there is an error disabling metrics collection. """ try: response = self.autoscaling_client.disable_metrics_collection( AutoScalingGroupName=group_name ) logger.info( f"Successfully disabled metrics collection for group '{group_name}'." ) return response except ClientError as err: error_code = err.response["Error"]["Code"] logger.error( f"Couldn't disable metrics for group '{group_name}'. Error code: {error_code}, Message: {err.response['Error']['Message']}" ) if error_code == "ResourceContentionFault": logger.error( f"There is a conflict with another operation that is modifying the Auto Scaling group '{group_name}'. " "Please try again later." ) raise # snippet-end:[python.example_code.auto-scaling.DisableMetricsCollection]