Blog | WeScale

AWS Autoscaling avec SaltStack

Rédigé par Séven Le Mesle | 16/06/2016

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 :

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

Pour créer la file SQS :

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

Souscrire la file SQS aux notifications SNS :

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

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

Un role IAM doit être affecté au service d’autoscaling pour lui permettre de pousser ses notifications vers la gateway SNS.

contenu de la policy :

{
    "Version": "2012-10-17",
    "Statement": [{
         "Effect": "Allow",
         "Resource": "*",
         "Action": [ 
             "sns:Publish"
          ]
        }
      ] 
 }

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:

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** \
--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’auto-scaling 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 :

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

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 librairie 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 :

conn = boto.sqs.connect_to_region(self.region)
auto_queue = conn.get_queue(self.queue_name)
auto_queue.set_message_class(boto.sqs.message.RawMessage)
messages = auto_queue.get_messages(num_messages=5, wait_time_seconds=20)
if messages:
    ## 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 Hook ``` {     "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" } ``` ``` {     "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" }

</td></tr></tbody></table>

Convertir le message en dict :

Load message from JSON to dict

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


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

def get_message_evt_type(self,message_dict):
     if 'Message' in message_dict:
         msg_body = message_dict['Message']
         if 'Event'in msg_body:
             # This is a simple notification event
             return msg_body['Event']
         if 'LifecycleTransition' in msg_body:
            # This is lifecycle hook
             return msg_body['LifecycleTransition']
     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 :

group = message_dict.get('AutoScalingGroupName', None)
instance_id = message_dict.get('EC2InstanceId', None)
minion_glob = "{}-{}".format(group, instance_id)



Appliquer un state de nettoyage de l’instance :

cmd = "salt '%s' state.sls clean --force-color --out json" % minion_glob
logger.info('executing command: %s', cmd)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()


Notifier le group pour terminer l’action :

cmd = "aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE "
"--lifecycle-hook-name {hook_name} "
"--auto-scaling-group-name {group_name} "
"--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)
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.


## 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 AMIs 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.