AWS CDK + AutoScale + Lifecycle Hooks + Lambda + SSM =💪
CDK — Kubernetes a veces es demasiado pesado o demasiado caro para resolver todos los problemas o una solución de “lift and shift” demasiado grande para los equipos de desarrollo.
Pero a medida que las soluciones se trasladan a la nube y, a menudo, enfrentan mayores cargas de trabajo, las soluciones aún deben poder escalar dinámicamente debido a razones de costo o utilización. No todas las aplicaciones o soluciones se pueden escalar horizontalmente de forma automática o sencilla.
Hay casos en los que es necesario tomar medidas cuando un componente de la solución aumenta o disminuye; por ejemplo, si el componente de trabajo aumenta o disminuye, es posible que deba hacer que un nodo de control sea consciente de esos cambios a través de actualizaciones de configuración.
Veamos un enfoque en Amazon Web Services donde un Autoscale Group (ASG) puede usar Lifecycle Hooks para activar operaciones durante el lanzamiento o la finalización de una instancia.
En este desglose, utilizaremos AWS Simple Notification Service (SNS) que entregará los mensajes relacionados con el ciclo de vida de ASG, AWS Lambda en el que una función sin servidor escuchará el tema del ciclo de vida de SNS para reaccionar a la changes, AWS Systems Manager, que se utilizará para invocar comandos de shell en el punto de enlace de destino, a fin de mostrar cómo estos ganchos pueden dar forma a los cambios en varios sistemas.
Una nota importante es que Systems Manager Agent (SMS) solo está instalado en imágenes de Amazon Linux de manera predeterminada; es posible que deba actualizar su imagen de máquina de Amazon para incluir la instalación y configuración de este elemento.
El repositorio de ejemplo que veremos a medida que analicemos estos conceptos utiliza Amazon Cloud Development Toolkit (CDK). El CDK le permite usar uno de los muchos lenguajes de código populares para definir las plantillas de formación de nubes resultantes que se pueden usar para implementaciones repetibles.
Este ejemplo básico configura una VPC y un grupo de escalado automático.
Paso a paso
Veamos el proyecto de ejemplo basado en CDK que:
1. Crear una red privada virtual (VPC)
2. Crear un grupo de escalado automático
3. Crea un tema SNS
4. Registre la función Lambda y suscríbase al tema SNS
5. Crea la función Lambda
Estos cinco elementos diferentes se capturan en el siguiente repositorio de GitHub de ejemplo:
cdk-autoscale-lambda-ssm-action
Creación de una VPC
Hay muchos ejemplos de la configuración básica de una VPC. Centraremos nuestra atención en el archivo net.py. A continuación puede ver la creación de una red privada virtual simple.
class ExampleNetworkStack(Stack): def __init__(self, scope: Construct, id: str, props, **kwargs) -> None: super().__init__(scope, id, **kwargs) # Subnet configurations for a public and private tier subnet1 = SubnetConfiguration(name="PublicNet", subnet_type=SubnetType.PUBLIC, cidr_mask=24) subnet2 = SubnetConfiguration( name="PrivateNet", subnet_type=SubnetType.PRIVATE_WITH_NAT, cidr_mask=24, ) vpc = Vpc( self, "ExampleVPC", cidr="10.0.0.0/16", enable_dns_hostnames=True, enable_dns_support=True, max_azs=2, nat_gateway_provider=NatProvider.gateway(), nat_gateways=1, subnet_configuration=[subnet1, subnet2], ) # This will export the VPC's ID in CloudFormation under the key # 'vpcid' CfnOutput(self, "vpcid", value=vpc.vpc_id) # Prepares output attributes to be passed into other stacks # In this case, it is our VPC, subnets and public_subnet_id. self.output_props = props.copy() self.output_props["vpc"] = vpc self.output_props["subnets"] = vpc.public_subnets self.output_props["public_subnet_id"] = vpc.public_subnets[0].subnet_id
Crear un grupo de escalado automático
La siguiente sección de código, systems.py
, proporciona un ejemplo de grupo de escalado automático que usaremos para realizar un seguimiento de los cambios del ciclo de vida con fines de automatización.
from aws_cdk.aws_ec2 import SubnetType from aws_cdk import ( BundlingOptions, Duration, aws_ec2 as ec2, aws_autoscaling as autoscaling, aws_elasticloadbalancingv2 as elbv2, aws_iam as iam, aws_sns as sns, aws_lambda as _lambda, aws_autoscaling_hooktargets as hooktargets, aws_sns_subscriptions as subscriptions, Stack, ) from constructs import Construct from constructs import Construct from . import AWS_AMI, AWS_REGION, AWS_KEYPAIR class ExampleSystemStack(Stack): def __init__(self, scope: Construct, id: str, props, **kwargs) -> None: super().__init__(scope, id, **kwargs) # Autoscale group autoscale_group = autoscaling.AutoScalingGroup( self, "example-autoscale-group", vpc=props["vpc"], instance_type=ec2.InstanceType.of(ec2.InstanceClass.MEMORY5, ec2.InstanceSize.XLARGE), machine_image=ec2.MachineImage.generic_linux({AWS_REGION: AWS_AMI}), key_name="{keypair}".format(keypair=AWS_KEYPAIR), vpc_subnets=ec2.SubnetSelection(subnet_type=SubnetType.PRIVATE_WITH_NAT), ) asg_dict = { "asg_name": autoscale_group.auto_scaling_group_name, } props.update(asg_dict) # Creates a security group for the autoscale group sg_example_asg = ec2.SecurityGroup(self, id="sg_example_asg", vpc=props["vpc"], security_group_name="sg_example_asg") # Creates a security group for the the autoscale group application load balancer sg_alb = ec2.SecurityGroup(self, id="sg_alb", vpc=props["vpc"], security_group_name="sg_example_asg_alb") # Allow all egress from ALB to ASG instance sg_alb.connections.allow_to(sg_example_asg, ec2.Port.all_tcp(), "To ASG") # Allows connections from ALB to ASG instances access port 80 # where application UI listens sg_example_asg.connections.allow_from(sg_alb, ec2.Port.tcp(80), "ALB Ingress") # Allow SSH connection between manager and auto-scale group sg_example_asg.connections.allow_from(sg_example_asg, ec2.Port.tcp(22), "SSH Ingress") sg_example_asg.add_ingress_rule( peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(22), description="ssh", ) # Adds the security group 'sg_example_asg' to the autoscaling group autoscale_group.add_security_group(sg_example_asg) # Creates an application load balance example_alb = elbv2.ApplicationLoadBalancer( self, "ExampleALB", vpc=props["vpc"], security_group=sg_alb, internet_facing=True, ) # Adds the autoscaling group's instance to be registered as targets on port 80 example_listener = example_alb.add_listener("Listener", port=80) example_listener.add_targets("Target", port=80, protocol=elbv2.ApplicationProtocol.HTTP, targets=[autoscale_group]) # This creates a "0.0.0.0/0" rule to allow every one to access the ALB example_listener.connections.allow_default_port_from_any_ipv4("Open to the world")
Crear un tema de SNS
Ahora que tenemos el grupo de escalado automático, podemos crear el tema de SNS y hacer que escuche los eventos del ciclo de vida del grupo de escalado automático.
topic_id = "system-autoscale-topic" topic = sns.Topic(self, topic_id, display_name="System autoscale topic", topic_name=topic_id) # Create role asg_topic_pub_role_name = "system-autoscale-topic-publisher-role-" asg_topic_pub_role = iam.Role( scope=self, id=asg_topic_pub_role_name, assumed_by=iam.ServicePrincipal("autoscaling.amazonaws.com"), role_name=asg_topic_pub_role_name, inline_policies=[ iam.PolicyDocument( statements=[ iam.PolicyStatement(resources=[topic.topic_arn], actions=["sns:Publish"]), ] ) ], ) topic_hook = hooktargets.TopicHook(topic) asg_lifecyclehook_name = "system-autoscale-lifecycle-hook" autoscale_group.add_lifecycle_hook( id=asg_lifecyclehook_name, lifecycle_transition=autoscaling.LifecycleTransition.INSTANCE_LAUNCHING, default_result=autoscaling.DefaultResult.ABANDON, heartbeat_timeout=Duration.minutes(15), lifecycle_hook_name=asg_lifecyclehook_name, notification_target=topic_hook, role=asg_topic_pub_role, )
Registre una función Lambda y suscríbase al tema SNS
Con el tema SNS en su lugar, podemos definir una función Lambda que escuchará las notificaciones emitidas por los eventos del ciclo de vida del grupo de escalado automático.
# Create role lambda_role_name = "system-autoscale-topic-lamda-role" lambda_role = iam.Role( scope=self, id=lambda_role_name, assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), role_name=lambda_role_name, inline_policies=[ iam.PolicyDocument( statements=[ iam.PolicyStatement(resources=["arn:aws:logs:*:*:*"], actions=["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]), iam.PolicyStatement(resources=["*"], actions=["autoscaling:CompleteLifecycleAction"]), ] ) ], managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name("AWSLambdaExecute"), iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaVPCAccessExecutionRole"), iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AmazonSSMAutomationRole"), ], ) lambda_sg_name = "system-autoscale-launching-lambda-sg" lambda_sg = ec2.SecurityGroup(self, lambda_sg_name, security_group_name=lambda_sg_name, vpc=props["vpc"], allow_all_outbound=True) # Defines an AWS Lambda resource lambda_name = "system-autoscale-launch-lambda" asg_launch = _lambda.Function( self, lambda_name, runtime=_lambda.Runtime.PYTHON_3_9, function_name=lambda_name, description="Lambda function deployed to handle launching Controller instances.", code=_lambda.Code.from_asset( "./scale_manager/lambda", bundling=BundlingOptions( image=_lambda.Runtime.PYTHON_3_9.bundling_image, command=["bash", "-c", "pip install -r requirements.txt -t /asset-output && cp -dR . /asset-output"], ), ), handler="asg_launching.handler", role=lambda_role, security_groups=[lambda_sg, sg_example_asg], timeout=Duration.minutes(10), vpc=props["vpc"], ) topic.add_subscription(subscriptions.LambdaSubscription(asg_launch))
Código detrás de la función Lambda
También echemos un vistazo a cómo se ve el código de la función Lambda. En este caso, solo vamos a hacer una llamada simple usando la API de Systems Manager para escribir un archivo en el sistema de archivos, pero dependiendo de su caso de uso específico, puede hacer muchos cambios más complejos.
import boto3 import logging import json import time if len(logging.getLogger().handlers) > 0: # The Lambda environment pre-configures a handler logging to stderr. If a handler is already configured, # `.basicConfig` does not execute. Thus we set the level directly. logging.getLogger().setLevel(logging.INFO) else: logging.basicConfig(level=logging.INFO) logger = logging.getLogger() def get_notification_sns_msg(notification): message_dict = {} records = notification.get("Records", []) for record in records: if record.get("EventSource") != "aws:sns": continue sns_data = record.get("Sns", {}) message = sns_data.get("Message", "{}") try: message_dict = json.loads(message) except: logger.error(f"Cannot parse Sns message. Details:{message}") if message_dict: break return message_dict def handler(notification, context): logger.info(f"Request receieved. Details:{notification}") client = boto3.client("ssm") message_dict = get_notification_sns_msg(notification) instance_id = message_dict.get("EC2InstanceId") if not instance_id: return "Error processing notification." command_call = time.time_ns() temp_file = f"/tmp/run-command-{command_call}.txt" response = client.send_command( InstanceIds=[instance_id], DocumentName="AWS-RunShellScript", Parameters={ "commands": [ f"echo 'This file is from a run command via SSM Agent for instance {instance_id}.' > {temp_file}", f"if [ -e {temp_file} ]; then echo -n True; else echo -n False; fi", ] }, ) command_id = response["Command"]["CommandId"] tries = 0 output = "False" while tries < 20: tries = tries + 1 try: time.sleep(2) result = client.get_command_invocation( CommandId=command_id, InstanceId=instance_id, ) if result["Status"] == "InProgress": continue output = result["StandardOutputContent"] break except client.exceptions.InvocationDoesNotExist: continue as_client = boto3.client("autoscaling") response = as_client.complete_lifecycle_action( LifecycleHookName=message_dict.get("LifecycleHookName"), LifecycleActionToken=message_dict.get("LifecycleActionToken"), AutoScalingGroupName=message_dict.get("AutoScalingGroupName"), LifecycleActionResult="CONTINUE", InstanceId=instance_id, ) return output == "True"
Sinopsis
Con los enfoques descritos anteriormente, tiene un poderoso conjunto de herramientas que le permiten crear una implementación dinámica y reactiva.
Puede aprovechar los beneficios de más capacidades nativas de la nube con un peso más ligero y menos reingeniería para las soluciones existentes.
Hemos capturado todos estos pasos con infraestructura como código, aprovechando el poder del CDK y simplemente superponiendo algunos servicios de AWS adicionales que abren una variedad de capacidades.
Si le interesa, puede echar un vistazo a algunos de los otros artículos que he escrito recientemente sobre Laravel: