Tutoriel pour apprendre l'autoscaling d'un environnement AWS avec SaltStack

Séven Le Mesle

Comment utiliser un outil de gestion de configuration dans le cadre d'un autoscaling group ? Les instances qui se lancent doivent pouvoir être provisionnées, qu'elles soient en mode golden image (image préprovisionnée) ou non. Plus encore, lorsqu'une instance est détruite, il faut pouvoir la nettoyer, voire la supprimer complètement de la gestion de configuration. Dans le cadre de SaltStack il s'agit d'une part d'appliquer le highstate au démarrage et d'a minima supprimer ses clefs du salt master. Dans cet article, je vous propose de découvrir une solution reposant sur les lifecycle hooks pour traiter ces problématiques.

Pour toute remarque ou suggestion, vous pouvez réagir sur l'espace de discussion créé sur le forum. Commentez

- Titre : Tutoriel pour apprendre l'autoscaling d'un environnement AWS avec SaltStack

- Auteur : Séven Le Mesle

- Parution : 6 octobre 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Séven Le Mesle. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Infrastructure

- II. API de traitement

- III. Pour conclure

- Note de la rédaction Developpez.com

I - Infrastructure

Pour mettre en place les notifications hooks sur un autoscaling group, il faut tout d'abord disposer d'une gateway de notification SNS capable de pousser ses notifications dans une file SQS. Notre infrastructure doit donc ressembler au schéma ci-dessous :

Pour créer la gateway SNS :

1. aws sns create-topic --name salt-master-notifications

Pour créer la file SQS :

1. aws sqs create-queue --queue-name salt-master-queue

Souscrire la file SQS aux notifications SNS :

1. aws sns subscribe --topic-arn arn:aws:sns:us-west-2:0123456789012:salt-master-notification --protocol sqs --notification-endpoint arn:aws:sqs:us-west-2:0123456789013:salt-master-queue

3. aws sns confirm-subscription --topic-arn arn:aws:sns:us-west-2:0123456789012:salt-master-notification --token 2336412f37fb687f5d51e6e241d7700ae02f7124d8268910b858cb4db727ceeb2474bb937929d3bdd7ce5d0cce19325d036bc858d3c217426bcafa9c501a2cace93b83f1dd3797627467553dc438a8c974119496fc3eff026eaa5d14472ded6f9a5c43aec62d83ef5f49109da7176391

Un rôle IAM doit être affecté au service d'autoscaling pour lui permettre de pousser ses notifications vers la gateway SNS.

Contenu de la policy :

1. "Version": "2012-10-17",

2. "Statement": [{

3. "Effect": "Allow",

4. "Resource": "*",

5. "Action": [

6. "sns:Publish"

7. ]

8. }

9. ]

10. }

Création du rôle :

aws iam create-role --role-name salt-autoscale --assume-role-policy-document file://policy.json

Nous considérons disposer déjà d'un autoscaling group et de l'instance salt-master.

Pour déclarer un lifecycle hook de terminaison sur un autoscaling group existant vous pouvez lancer :

1. aws autoscaling put-lifecycle-hook --lifecycle-hook-name **salt-hook** --auto-scaling-group-name **my-asg** --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING --notification-target-arn **arn:aws:sns:us-west-2:0123456789012:salt-master-notification** \

2. --role-arn **arn:aws:iam::123456789012:role/salt-autoscale**

Pour en faire de même au moment de la création des instances, il suffit de remplacer le paramètre -lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING par -lifecycle-transition autoscaling:EC2_INSTANCE_LAUNCHING.

Attention, à partir du moment ou vous avez ajouté les lifecycle hooks, lorsque l'autoscaling group crée ou détruit une instance, cette dernière est placée en attente de traitement avant d'entrer dans le group ou d'être détruite. Par défaut, il faudra attendre une erreur sans action de votre part pour que l'instance soit détruite.

Voici un schéma résumant le cycle de vie d'une instance :

Pour finir nous allons ajouter une notification envoyée à la terminaison finale des instances, afin de pouvoir supprimer la clé du minion correspondant à l'instance terminée :

1. aws autoscaling put-notification-configuration --auto-scaling-group-name my-asg --topic-arn arn --notification-types  "autoscaling:EC2_INSTANCE_TERMINATE"

II - API de traitement

Maintenant que notre infrastructure est en place, nous pouvons procéder à la création d'une API qui se chargera de récupérer les événements dans la file SQS et de lancer le ou les traitement(s) adéquat(s) :

- instance launching : accepter la clef dans le master et lancer le highstate s'il n'est pas lancé par le script de bootstrap de ladite instance puis notifier le groupe qu'il peut terminer l'action ;

- instance terminating : appliquer le nettoyage de l'instance, et notifier le group qu'il peut terminer l'action.

Pour ces besoins nous avons choisi de développer une API en Python en utilisant la bibliothèque Boto qui implémente l'ensemble des API AWS.

Notre API Python est un simple script se connectant à la file SQS et récupérant en permanence les notifications. Un event handler spécifique est défini pour chacune des notifications. Ce dernier se chargera de lancer via subprocess les commandes salt qui assureront le provisionning complet (ou highstate), le nettoyage de l'instance (state clean) puis la suppression de la clé du minion cible.

Connexion à SQS et consommation des messages :

1. conn = boto.sqs.connect_to_region(self.region)

2. auto_queue = conn.get_queue(self.queue_name)

3. auto_queue.set_message_class(boto.sqs.message.RawMessage)

4. messages = auto_queue.get_messages(num_messages=5, wait_time_seconds=20)

5. if messages:

6. ## Process messages

Ce bloc de code boto vous permet de récupérer les notifications et hooks dans la file sqs.

Chargement du message dans un dictionnaire :

- avant de parser les messages, il convient de voir ce qu'ils contiennent.

Notification

{
"Body": '{
"AutoScalingGroupName": "exampleAutoScalingGroup",
"Service": "AWS Auto Scaling",
"Time": "2015-01-07T19:13:22.375Z",
"AccountId": "356438515751",
"LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING",
"RequestId": "876eac1c-2aaa-407d-98d7-ce9afe597663",
"LifecycleActionToken": "4889fcc7-adc6-43ff-a415-46240e2f57dc",
"EC2InstanceId": "i-883a3a42",
"LifecycleHookName": "do-some-work"
}',
"ReceiptHandle": "AQEBAjam9pe3ZxzD+w3A==",
"MD5OfBody": "d872dc653bcd5d1cc981b2eae64d3827",
"MessageId": "b3308afb-dad3-4eef-abb9-1d99aa9dd50f"
}

Hook

{
"Type": "Notification",
"MessageId": "4bd78f34-3162-5d9a-9fd3-c03de213b664",
"TopicArn": "arn: aws:sns: eu-west-1: 123456789111: topic",
"Subject": "Auto Scaling: launch for group  asg",
"Message": {"StatusCode": "InProgress",
"Service": "AWS Auto Scaling",
"AutoScalingGroupName": " asg",
"Description": "Launching a new EC2 instance: i-2d125da0",
"ActivityId": "b2446012-9583-43e9-96b9-3341b1edebc5",
"Event": "autoscaling: EC2_INSTANCE_LAUNCH",
"Details": {"Availability Zone": "eu-west-1a",
"Subnet ID": "subnet-c76bf99e"},
"AutoScalingGroupARN": "arn: aws: autoscaling: eu-west-1: 123456789111: autoScalingGroup: a4993b91-8827-47fc-8135-5a845c3f58b1: autoScalingGroupName/ asg",
"Progress": 50,
"Time": "2016-01-08T15: 09: 37.851Z",
"AccountId": "123456789111",
"RequestId": "b2446012-9583-43e9-96b9-3341b1edebc5",
"StatusMessage": "",
"EndTime": "2016-01-08T15: 09: 37.851Z",
"EC2InstanceId": "i-2d125da0",
"StartTime": "2016-01-08T15: 09: 03.980Z",
"Cause": "At 2016-01-08T15: 08: 45Z a user request executed policy Test changing the desired capacity from 1 to 3. At 2016-01-08T15: 09: 02Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 1 to 3."
},
"Timestamp": "2016-01-08T15: 09: 37.890Z"
}

Convertir le message en dict :

1. # Load message from JSON to dict

2. message_dict = json.loads(msg.get_body())

3. message_body = json.loads(message_dict['Message'])

4. message_dict['Message'] = message_body

Récupération du type d'évènement :

1. def get_message_evt_type(self,message_dict):

2. if 'Message' in message_dict:

3. msg_body = message_dict['Message']

4. if 'Event'in msg_body:

5. # This is a simple notification event

6. return msg_body['Event']

7. if 'LifecycleTransition' in msg_body:

8. # This is lifecycle hook

9. return msg_body['LifecycleTransition']

10. return None

Cette fonction permet de retourner le type d'évènement associé au message transformé en dict. S'il s'agit d'une notification simple avec présence du champ 'Event' ou d'un hook avec présence du champ LifecycleTransition. La méthode retourne la valeur associée au dit champ :

- autoscaling:EC2_INSTANCE_TERMINATING pour un hook de terminaison ;

- autoscaling:EC2_INSTANCE_TERMINATE pour une notification d'instance terminée ;

- autoscaling:EC2_INSTANCE_LAUNCH pour une notification de démarrage d'instance.

Il suffit maintenant de lancer les traitements attendus en fonction du type d'évènement reçu.

Attention pour pouvoir appliquer les commandes salt sur le minion cible, il faut définir une norme de nommage de ces derniers. Dans notre cas, nous utiliserons le nom de l'autoscaling group suivi de l'id de l'instance AWS séparé par un '-'. Cette logique sera utilisée pour construire le pattern permettant de cibler le minion.

Cibler le minion :

1. group = message_dict.get('AutoScalingGroupName', None)

2. instance_id = message_dict.get('EC2InstanceId', None)

3. minion_glob = "{}-{}".format(group, instance_id)

Appliquer un state de nettoyage de l'instance :

1. cmd = "salt '%s' state.sls clean --force-color --out json" % minion_glob

2. logger.info('executing command: %s', cmd)

3. p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

4. out, err = p.communicate()

Notifier le group pour terminer l'action :

1. cmd = "aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE " \

2. "--lifecycle-hook-name {hook_name} " \

3. "--auto-scaling-group-name {group_name} " \

4. "--lifecycle-action-token {action_token} --region {region}".format( hook_name=message_dict['LifecycleHookName'], group_name=message_dict['AutoScalingGroupName'], action_token=message_dict['LifecycleActionToken'], region=self.region)

5. p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate()

Le même principe est applicable pour accepter la clé du minion, supprimer la clé, ou appliquer toute sorte de traitement salt sur le minion à sa création ou à sa suppression de l'autoscaling group.

III - Pour conclure

Avec cette solution, nous avons un moyen d'utiliser notre gestion de configuration pour traiter les instances gérées par un autoscaling group. Cela ne doit pas vous empêcher de créer des golden AMI pour obtenir un temps de démarrage optimisé. Des solutions reposant directement sur SNS et l'API Salt existent ; dans notre cas l'utilisation de SQS nous permet de ne pas exposer nos commandes salt via une API publique. Bien que le système soit très orienté salt, le même principe est applicable à d'autres outils de gestion de configuration comme Ansible, par exemple.

Note de la rédaction Developpez.com

Tous nos remerciements à Wescale pour l'autorisation de publication de ce tutoriel.

Nos remerciements également à Winjerome pour la mise au gabarit Developpez.com et Jacques Jean pour la relecture orthographique.