Aller au contenu

Lifecycle hooks ASG bien faits — drainage, désenregistrement et le motif heartbeat

Par défaut l'ASG termine une instance Spot deux minutes après l'avertissement. Deux minutes suffisent si votre hook draine le LB, vide les logs et désenregistre du service discovery. Voici le motif qui fait les trois de manière fiable.

La flotte roule Consul pour le service discovery, expédie les logs par Fluent Bit vers une destination CloudWatch, et sert le trafic derrière un ALB. Quand l’ASG termine une instance, le comportement par défaut est « arrête et continue » — AWS tue l’instance, l’ALB remarque trente secondes plus tard que la cible est en mauvaise santé et arrête d’y envoyer du trafic, Consul expire la vérification du nœud après une minute et retire l’entrée, et les dernières secondes de logs ne sortent jamais du tampon Fluent Bit. La plupart du temps ça ne pose pas problème. Le 1 % du temps où ça le pose, vous regardez des entrées Consul orphelines qui prennent six heures à se nettoyer, une fenêtre de 30 secondes où l’ALB envoie à une machine morte et retourne des 502, et un fragment de log manquant qui contient justement la ligne qu’il vous fallait pour un débogage. Les lifecycle hooks sont la manière de rendre la terminaison ordonnée.

Ce qui se passe

Un lifecycle hook est une pause dans la machine à états de l’ASG. Quand l’ASG décide de lancer ou terminer une instance, il peut suspendre la transition et demander au monde extérieur de faire du travail avant de continuer. Il y a deux transitions accrochables :

  • autoscaling:EC2_INSTANCE_LAUNCHING — se déclenche après le démarrage de l’instance mais avant qu’elle soit marquée InService. Votre hook peut tirer la configuration d’un serveur de config, s’enregistrer au service discovery, préchauffer des caches, exécuter un contrôle de cohérence. L’instance n’est pas encore dans le load balancer, donc pas d’inquiétude trafic.
  • autoscaling:EC2_INSTANCE_TERMINATING — se déclenche après que l’ASG décide de terminer mais avant qu’AWS arrête vraiment l’instance. Votre hook peut drainer les connexions, vider les logs, désenregistrer du service discovery, signaler aux systèmes externes d’arrêter d’envoyer du travail.

Le hook fonctionne par passage de messages. Quand l’ASG entre dans l’état suspendu, il publie un message dans une file SQS ou un bus EventBridge que vous avez spécifié. Quelque chose sur l’instance (ou hors d’elle) lit le message, fait le travail, et appelle complete-lifecycle-action pour relâcher la suspension. Si le travail n’est pas complété avant le heartbeat-timeout (par défaut 1 heure, max 1 heure), l’ASG reprend la transition avec le default-result que vous avez configuré — CONTINUE (procéder) ou ABANDON (marquer l’instance comme échouée et réessayer).

Le motif spécifique pour les hooks de terminaison :

  1. L’instance reçoit le message du hook — habituellement via un petit démon qui écoute SQS, ou via un document SSM déclenché par EventBridge.
  2. Le démon désenregistre du service discovery en premier. consul agent leave, retirant l’instance de tout DNS round-robin, la désenregistrant de tout load balancer interne. C’est l’étape la plus longue à refroidir parce que les clients avec des endpoints en cache ont besoin de temps pour expirer.
  3. Drainer le load balancer. Pour un ALB, c’est automatique une fois que le deregistration_delay.timeout_seconds du target group se déclenche — mais le hook devrait appeler deregister-targets explicitement pour démarrer le minuteur immédiatement plutôt que d’attendre le comportement de détachement par défaut de l’ASG.
  4. Attendre que les requêtes en vol terminent. Un endpoint de santé sur l’instance peut servir « draining » tout en retournant encore 200 pour garder les connexions existantes vivantes. Les nouvelles connexions depuis l’ALB s’arrêtent parce que la cible est désenregistrée.
  5. Vider les logs. pkill -USR1 fluent-bit ou équivalent force un vidage du tampon. Attendre assez longtemps pour que les enregistrements tamponnés soient confirmés par la destination.
  6. Compléter le hook. Le démon appelle complete-lifecycle-action avec CONTINUE. L’ASG reprend et AWS termine l’instance.

Le piège du timeout d’une heure mord les équipes qui essaient de faire entrer du travail lent dans le hook. Les hooks ont un timeout heartbeat maximum fixe. Si votre travail demande plus de temps, vous devez appeler record-lifecycle-action-heartbeat périodiquement pendant le travail, chaque appel réinitialisant le minuteur pour heartbeat-timeout secondes de plus. Oubliez les heartbeats pendant un drainage lent, le hook expire, l’ASG exécute le default-result (habituellement CONTINUE), et l’instance est tuée en plein drainage.

Les interruptions Spot sont différentes. AWS vous donne deux minutes avant de terminer une instance Spot, et le lifecycle hook se déclenche dans cette fenêtre. Le hook a au maximum deux minutes, pas une heure. Toute la logique de drainage sur une flotte Spot doit terminer bien en deçà — typiquement 90 secondes, laissant une marge pour que l’appel complete-lifecycle-action atterrisse avant qu’AWS termine de force.

Comment vérifier

Lister les hooks sur un ASG :

aws autoscaling describe-lifecycle-hooks \
  --auto-scaling-group-name my-app-prod-asg \
  --output table

La sortie montre le nom du hook, la transition (LAUNCHING ou TERMINATING), le heartbeat timeout, et la cible de notification. Chaque hook devrait avoir un timeout sensé — sous 5 minutes pour les hooks de terminaison (plus long est une odeur), plus long est bien pour les hooks de lancement.

Vérifier que le hook se déclenche et complète :

aws autoscaling describe-scaling-activities \
  --auto-scaling-group-name my-app-prod-asg \
  --max-items 50 \
  --query 'Activities[].[StartTime,StatusCode,Description,Cause]' \
  --output table

Un cycle de hook propre montre WaitingForInstanceWarmup ou WaitingForInstanceTermination, puis Successful. Un hook qui a expiré montre la description terminant en « due to lifecycle action result ». Quand vous voyez des résultats ABANDON sans cause évidente, le démon est le suspect — habituellement il a planté, perdu le message SQS, ou complété contre la mauvaise instance.

Chronométrer une vraie terminaison bout à bout :

# Choisir une instance saine et déclencher la terminaison via l'ASG
aws autoscaling terminate-instance-in-auto-scaling-group \
  --instance-id i-0123456789abcdef0 \
  --no-should-decrement-desired-capacity

Regarder le log d’activités de scaling et les logs propres de l’application en parallèle. Une terminaison saine montre : le hook se déclenche → l’ALB reçoit deregister-targets → les connexions drainent sur le délai de désenregistrement → les logs se vident → le hook complète → AWS arrête l’instance. Le temps entre terminate-instance-in-auto-scaling-group et « instance arrêtée » devrait être le temps réel de drainage du hook (typiquement 60 à 120 secondes pour une instance derrière LB avec logs).

Si le hook se déclenche mais ne complète pas en un temps utile, se brancher au démon et vérifier si le message SQS est récupéré :

aws sqs receive-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/asg-lifecycle-events \
  --max-number-of-messages 1

Un message coincé en vol pendant des dizaines de secondes veut dire que le démon l’a mais ne complète pas l’action. Les messages perdus veulent dire que le démon ne les a jamais récupérés — habituellement un problème de permissions ou un démon planté.

La correction

Le motif complet de hook de terminaison, pour une flotte derrière ALB utilisant Consul pour le service discovery :

  1. Créer le hook. C’est la mise en place unique au provisionnement de l’ASG :

    aws autoscaling put-lifecycle-hook \
      --auto-scaling-group-name my-app-prod-asg \
      --lifecycle-hook-name terminate-drain \
      --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING \
      --heartbeat-timeout 300 \
      --default-result CONTINUE \
      --notification-target-arn arn:aws:sqs:us-east-1:123456789012:asg-lifecycle-events \
      --role-arn arn:aws:iam::123456789012:role/asg-lifecycle-role

    Cinq minutes de heartbeat suffisent comme marge pour un drainage propre sur une instance occupée. Un default-result CONTINUE veut dire « si le hook expire, terminer quand même » — l’alternative ABANDON laisse l’instance en Terminating:Wait indéfiniment, ce qui est rarement souhaité.

  2. Le démon sur chaque instance. Un petit service systemd qui exécute awscli et lit SQS :

    while true; do
      msg=$(aws sqs receive-message \
        --queue-url "${QUEUE_URL}" \
        --wait-time-seconds 20 \
        --max-number-of-messages 1)
    
      # Parser le message, vérifier si cette instance est la cible
      # Si oui, exécuter les étapes de drainage et appeler complete-lifecycle-action
      # Toujours appeler delete-message après complete, quel que soit le résultat
    done

    En pratique c’est plus souvent un Lambda déclenché par la file SQS qui utilise SSM pour exécuter les commandes de drainage sur l’instance, parce que ce chemin survit au fait que l’instance ne puisse pas joindre les API AWS pendant le drainage.

  3. Les étapes de drainage, dans l’ordre, avec un heartbeat entre chaque :

    # 1. Marquer en drainage dans l'endpoint de santé
    echo "draining" > /var/run/app/health-state
    
    aws autoscaling record-lifecycle-action-heartbeat \
      --auto-scaling-group-name my-app-prod-asg \
      --lifecycle-hook-name terminate-drain \
      --instance-id "${INSTANCE_ID}" \
      --lifecycle-action-token "${TOKEN}"
    
    # 2. Désenregistrer de Consul
    consul leave
    sleep 5
    
    # 3. Désenregistrer de l'ALB explicitement
    aws elbv2 deregister-targets \
      --target-group-arn "${TG_ARN}" \
      --targets Id="${INSTANCE_ID}"
    
    # 4. Attendre le drainage des connexions en vol (délai de désenregistrement du target group)
    sleep 30
    
    # 5. Vider les logs
    systemctl reload fluent-bit
    sleep 10  # donner à Fluent Bit le temps de confirmer le vidage
    
    # 6. Compléter le hook
    aws autoscaling complete-lifecycle-action \
      --auto-scaling-group-name my-app-prod-asg \
      --lifecycle-hook-name terminate-drain \
      --instance-id "${INSTANCE_ID}" \
      --lifecycle-action-token "${TOKEN}" \
      --lifecycle-action-result CONTINUE
  4. Le pendant pour le hook de lancement, pour être complet, a la forme inverse : l’instance démarre → le démon reçoit le message EC2_INSTANCE_LAUNCHING → tire la dernière configuration d’un serveur de config → s’enregistre dans Consul → complète le hook → l’ASG marque l’instance InService → l’ALB démarre les health checks. Le hook de lancement protège contre l’entrée d’une instance en InService avant que ses dépendances soient prêtes, qui est la cause profonde de la moitié des échecs de déploiement vus sur les services sur ASG.

Trois règles à chaque conception de hook :

Les timeouts de hook doivent correspondre au pire-cas de drainage, avec marge. Un drainage de 60 secondes sur une instance P95 doit avoir un timeout de hook de 180 secondes, pas 60. Si P99 est 90 secondes, régler le timeout à 300. La marge protège contre le cas lent sans gonfler le cas commun (vous complétez quand prêt, pas quand le timeout frappe).

Default-result CONTINUE pour les hooks de terminaison, ABANDON pour les hooks de lancement. Un hook de terminaison qui expire doit quand même laisser l’instance terminer — la laisser en Terminating:Wait bloque l’ASG. Un hook de lancement qui expire ne doit pas laisser une instance à moitié démarrée entrer dans le LB — ABANDON retente le lancement, ce qui est le défaut le plus sûr.

Tester le mode d’échec. Tuer le démon en plein drainage. Retirer les permissions de la file SQS. S’assurer que le timeout du hook se déclenche quand il devrait, et que le comportement de default-result est ce que vous attendiez. Les bugs de hook sont silencieux jusqu’au jour où la charge les rencontre en production.

Le motif de lifecycle hook fait quarante lignes de script et une file SQS. Bien fait, les terminaisons sont inobservables de l’extérieur du cluster. Mal fait, vous en entendez parler quand l’alerte sur les 502 pendant les déploiements devient le matin de quelqu’un.


Stack Harbor opère les paliers applicatifs sur ASG et les politiques d’auto-scaling à coût plafonné dans le cadre de Gestion AWS et de la couche Auto-scaling. Si vos événements de scaling sont encore des surprises, commencez par une consultation d’infrastructure.