Aller au contenu

Déploiements canari sur Lambda — les hooks pré/post-trafic qui attrapent réellement les régressions

CodeDeployDefault.LambdaCanary10Percent5Minutes ne fonctionne que si le hook de validation fait quelque chose de réel. Voici le hook qui exécute une sonde de fumée et vérifie le taux d'erreur CloudWatch avant la promotion.

Une fonction Lambda exposée par API Gateway gère les livraisons de webhooks depuis un SaaS tiers — environ 80 requêtes par seconde au pic. L’équipe expédie un refactor un vendredi après-midi. Le déploiement utilise CodeDeployDefault.LambdaCanary10Percent5Minutes et les hooks pré/post-trafic sortent tous deux 0 sans aucune vérification. Cinq minutes plus tard, la bascule à 100 % est terminée, la cloche d’alarme sonne quarante minutes plus tard, et tout un après-midi de webhooks a été perdu parce que le nouveau code retourne 502 sur une forme de charge utile que la suite de tests n’a jamais exercée. Les déploiements canari sur Lambda ne valent que le hook qui valide le canari. Voici le modèle qui attrape la régression au lieu d’attendre l’alarme.

Ce qui se passe

Le canari Lambda via CodeDeploy fonctionne par bascule de trafic basée sur un alias. L’alias (typiquement live) pointe vers la version N. Le déploiement publie la version N+1 et bascule une fraction des invocations vers elle selon la configuration de déploiement. CodeDeployDefault.LambdaCanary10Percent5Minutes bascule 10 % du trafic pendant 5 minutes, puis 100 %. LambdaCanary10Percent10Minutes maintient plus longtemps. LambdaLinear10PercentEvery1Minute monte progressivement. LambdaAllAtOnce est l’option sans canari.

Deux hooks encadrent la bascule :

  • BeforeAllowTraffic s’exécute une fois, avant qu’aucun trafic de production n’atteigne la nouvelle version. La nouvelle version est publiée mais l’alias dirige toujours 100 % vers l’ancienne. C’est la fenêtre de tests synthétiques — invoquez la nouvelle version directement par ARN, exercez les chemins critiques, sortez non-zéro pour avorter.
  • AfterAllowTraffic s’exécute une fois, après que la bascule à 100 % soit terminée. La nouvelle version est en production. C’est la fenêtre de validation — vérifiez les métriques CloudWatch sur la période canari, sortez non-zéro pour déclencher l’annulation.

Les hooks par défaut ne font rien. CodeDeploy livre des modèles avec return {'hookStatus': 'Succeeded'} et les développeurs les laissent tels quels. Les hooks doivent faire un vrai travail, et la configuration d’annulation doit respecter le code de sortie du hook.

L’autre mode de défaillance est le câblage des alarmes. Si automaticRollbackConfiguration ne contient pas DEPLOYMENT_STOP_ON_ALARM, une alarme CloudWatch qui se déclenche pendant le canari apparaîtra dans la console mais ne déclenchera pas réellement l’annulation. La liste des événements a besoin de DEPLOYMENT_FAILURE, DEPLOYMENT_STOP_ON_ALARM, et DEPLOYMENT_STOP_ON_REQUEST pour une couverture complète.

Comment vérifier

Trois vérifications avant de promouvoir un groupe de déploiement canari Lambda en production : l’alias est réel, les hooks ont les permissions, et les alarmes existent.

La vérification de l’alias importe parce que aws lambda publish-version incrémente le numéro de version à chaque déploiement, et l’alias doit pointer vers une version spécifique (pas $LATEST). Vérifiez :

aws lambda get-alias \
  --function-name webhook-processor \
  --name live

La sortie doit contenir "FunctionVersion": "47" (ou la version de production courante) et non "FunctionVersion": "$LATEST". Un alias pointant vers $LATEST ne peut pas être déployé en canari — CodeDeploy refuse de créer un déploiement.

La vérification des permissions de hook importe parce que les hooks pré/post-trafic sont eux-mêmes des fonctions Lambda, et ils ont besoin de codedeploy:PutLifecycleEventHookExecutionStatus pour rapporter leur résultat. Le rôle d’exécution de la fonction hook a besoin de cette politique :

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "codedeploy:PutLifecycleEventHookExecutionStatus",
    "Resource": "*"
  }, {
    "Effect": "Allow",
    "Action": ["lambda:InvokeFunction", "cloudwatch:GetMetricStatistics"],
    "Resource": "*"
  }]
}

Sans la première action, le hook s’exécutera, fera son travail, puis échouera à rapporter — et CodeDeploy mettra l’événement de cycle de vie en timeout après une heure.

La vérification des alarmes est la même que pour les déploiements EC2. Listez la configuration d’alarmes du groupe de déploiement :

aws deploy get-deployment-group \
  --application-name webhook-processor \
  --deployment-group-name webhook-prod \
  --query 'deploymentGroupInfo.alarmConfiguration'

Pour Lambda, les alarmes qui comptent sont au niveau fonction : Errors sur la période canari, Duration p95 ou p99, et Throttles. L’alarme de taux d’erreur doit se déclencher si les erreurs dépassent un compte fixe (pas un pourcentage) sur une fenêtre d’une minute avec deux périodes d’évaluation — une alarme basée sur un pourcentage est bruyante à faible trafic.

La correction

Le hook AfterAllowTraffic qui valide réellement. Voici une Lambda Python qui exécute une sonde de fumée contre la nouvelle version, puis vérifie le taux d’erreur CloudWatch sur la fenêtre canari :

import boto3
import json
import os
import time
from datetime import datetime, timedelta, timezone

cloudwatch = boto3.client('cloudwatch')
lambda_client = boto3.client('lambda')
codedeploy = boto3.client('codedeploy')

FUNCTION_NAME = 'webhook-processor'
CANARY_WINDOW_MINUTES = 5
MAX_ERRORS = 3

def handler(event, context):
    deployment_id = event['DeploymentId']
    hook_execution_id = event['LifecycleEventHookExecutionId']
    status = 'Succeeded'

    try:
        smoke_test()
        check_error_rate()
    except Exception as e:
        print(f'Validation failed: {e}')
        status = 'Failed'

    codedeploy.put_lifecycle_event_hook_execution_status(
        deploymentId=deployment_id,
        lifecycleEventHookExecutionId=hook_execution_id,
        status=status
    )
    return {'hookStatus': status}

def smoke_test():
    payload = {'webhook_id': 'smoke-test', 'event': 'ping'}
    response = lambda_client.invoke(
        FunctionName=FUNCTION_NAME,
        Qualifier='live',
        Payload=json.dumps(payload).encode()
    )
    body = json.loads(response['Payload'].read())
    if response.get('FunctionError') or body.get('statusCode') != 200:
        raise RuntimeError(f'smoke test failed: {body}')

def check_error_rate():
    end = datetime.now(timezone.utc)
    start = end - timedelta(minutes=CANARY_WINDOW_MINUTES)
    response = cloudwatch.get_metric_statistics(
        Namespace='AWS/Lambda',
        MetricName='Errors',
        Dimensions=[{'Name': 'FunctionName', 'Value': FUNCTION_NAME}],
        StartTime=start,
        EndTime=end,
        Period=60,
        Statistics=['Sum']
    )
    total = sum(d['Sum'] for d in response['Datapoints'])
    if total > MAX_ERRORS:
        raise RuntimeError(f'{total} errors in {CANARY_WINDOW_MINUTES}m, threshold {MAX_ERRORS}')

Le hook BeforeAllowTraffic est une version plus mince du même — il invoque la nouvelle version directement par ARN qualifié de version (pas l’alias live) et exécute uniquement la sonde de fumée. La vérification du taux d’erreur n’a pas encore de données à ce moment-là.

La configuration complète du groupe de déploiement avec les deux hooks et le câblage des alarmes :

aws deploy create-deployment-group \
  --application-name webhook-processor \
  --deployment-group-name webhook-prod \
  --service-role-arn arn:aws:iam::111122223333:role/CodeDeployServiceRoleLambda \
  --deployment-config-name CodeDeployDefault.LambdaCanary10Percent5Minutes \
  --auto-rollback-configuration '{
    "enabled": true,
    "events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_STOP_ON_REQUEST"]
  }' \
  --alarm-configuration '{
    "enabled": true,
    "alarms": [
      {"name": "webhook-processor-errors"},
      {"name": "webhook-processor-duration-p99"}
    ]
  }'

Le appspec.yaml pour la révision Lambda fait référence aux ARN des hooks :

version: 0.0
Resources:
  - webhook-processor:
      Type: AWS::Lambda::Function
      Properties:
        Name: webhook-processor
        Alias: live
        CurrentVersion: 47
        TargetVersion: 48
Hooks:
  - BeforeAllowTraffic: arn:aws:lambda:ca-central-1:111122223333:function:webhook-before-traffic
  - AfterAllowTraffic: arn:aws:lambda:ca-central-1:111122223333:function:webhook-after-traffic

Trois règles que nous tenons pour les déploiements canari Lambda :

Le hook de validation doit invoquer la version cible directement, pas via l’alias. Pendant le canari, seulement 10 % du trafic dirigé par l’alias atteint la nouvelle version — une sonde de fumée via l’alias a 90 % de chances de tester l’ancien code. Invoquez par ARN de version.

Utilisez un compte d’erreurs fixe, pas un pourcentage. Une fonction faisant 80 RPS verra des centaines d’invocations pendant un canari de 5 minutes; une fonction faisant 0,5 RPS en verra 150. Un seuil de 1 % signifie 1,5 erreur dans le second cas — arrondi à 2 et vous acceptez un taux de défaillance réel de 1,3 %. Les seuils fixes sont raisonnables.

Calibrez la fenêtre canari pour au moins deux périodes d’évaluation CloudWatch. Un canari de 5 minutes contre une alarme d’erreur d’une minute, 2 périodes a juste assez de marge pour que l’alarme se déclenche pendant le canari. Un canari de 5 minutes contre une alarme de 5 minutes, 2 périodes ne peut absolument pas se déclencher à temps — l’alarme a besoin de 10 minutes de mauvaises données pour basculer.

Le modèle canari est la forme de déploiement Lambda la plus sûre. Référez-vous au runbook d’annulation automatique pour les détails du câblage des alarmes.


Stack Harbor opère CodeDeploy + CodePipeline sur les flottes EC2 / ECS / Lambda de production dans le cadre de Gestion AWS et des Environnements infogérés en continu. Si vos déploiements demandent encore une fenêtre de maintenance, commencez par une consultation d’infrastructure.