Automatisez vos déploiements CodePipeline à intervalles réguliers


Un petit peu de contexte

Cet article découle d’un besoin client que nous avons régulièrement : ils souhaitent réaliser leurs déploiements via AWS CodePipeline de manière pré-programmée. Par exemple, une mise en production uniquement les lundis à 9H.

Seul petit soucis : cette fonctionnalité n’est pas proposée de manière native sur AWS CodePipeline ! Nous avons donc mis au point un petit stratagème que nous partageons avec plaisir avec vous.

C’est parti !


L’architecture retenue

Bonne nouvelle : l'architecture retenue est assez basique ! Il nous suffit d’ajouter une étape dans notre pipeline juste avant le déploiement de la production. Cette étape est proposée de manière native sur AWS CodePipeline : il s’agit tout simplement de l’étape d’approbation manuelle, qui est bien documentée par AWS et pour laquelle nous ne reviendrons pas en détails dans cet article.

Notre seul conseil : n’ajoutez pas de notification SNS à votre approbation manuelle pour limiter le “spam”, puisque cette approbation manuelle ne le sera bientôt plus.

Pour transformer cette approbation manuelle en approbation programmée, nous allons avoir recours à AWS Lambda et CloudWatch Events. AWS CloudFormation sera également de la partie pour automatiser au maximum le déploiement de cette solution au niveau infrastructure.

Architecture


Déploiement de notre petit trick

Comme nos clients, nous adorons tout automatiser chez Osones. Nous avons donc fait un template AWS CloudFormation disponible directement sur notre repo GitHub. Passons le en revue ensemble :


  LambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        ZipFile:
          !Join
            - ' '
            - - 'var AWS = require("aws-sdk");'
              - 'exports.handler = (event, context, callback) => {'
              - '  var paramsGet = {'
              - '    name: process.env.PIPELINE_NAME'
              - '  };'
              - '  var codepipeline = new AWS.CodePipeline();'
              - '  codepipeline.getPipelineState(paramsGet, function(err, data) {'
              - '    if (err) console.log(err, err.stack);'
              - '    else {'
              - '      if (data.stageStates.length > 0) {'
              - '        var token = null;'
              - '        for (let stage of data.stageStates) {'
              - '          var stageName = stage.stageName;'
              - '          if (stageName === process.env.STAGE_NAME) {'
              - '            var actionStates = stage.actionStates;'
              - '            for (let actionState of actionStates) {'
              - '              var actionName = actionState.actionName;'
              - '              if (actionName === process.env.APPROVAL_NAME) {'
              - '                token = actionState.latestExecution.token;'
              - '                break;'
              - '              }'
              - '            }'
              - '          }'
              - '        }'
              - '        var paramsPut = {'
              - '          actionName: process.env.APPROVAL_NAME,'
              - '          pipelineName: process.env.PIPELINE_NAME,'
              - '          result: {'
              - '            status: "Approved",'
              - '            summary: "Scheduled approval by Lambda."'
              - '          },'
              - '          stageName: process.env.STAGE_NAME,'
              - '          token: token'
              - '        };'
              - '        if (token != null) {'
              - '          codepipeline.putApprovalResult(paramsPut, function(err, data) {'
              - '            if (err) console.log(err, err.stack);'
              - '            else console.log(data);'
              - '          });'
              - '        }'
              - '      }'
              - '    }'
              - '    callback(null, "Approved");'
              - '  })'
              - '};'
      Description: Automatic CodePipeline Approval
      Environment:
        Variables:
          PIPELINE_NAME: !Ref PipelineName
          STAGE_NAME: !Ref StageName
          APPROVAL_NAME: !Ref ApprovalName
      FunctionName: AutomaticCodePipelineApproval
      Handler: "index.handler"
      Role: !GetAtt LambdaAutoApprovalServiceRole.Arn
      Runtime: nodejs6.10

Avant tout nous déclarons notre code avec un Fn::Join et un délimiteur ‘espace’ de manière à pouvoir injecter de manière lisible notre script JavaScript dans ce template, et qu’il soit exécutable sur la Lambda (le script sera sur une seule ligne dans l’éditeur en ligne).

Petite libertée : si normalement, la best practice consiste à placer ce script Javascript dans un bucket Amazon S3 puis créer la Lambda en faisant récupérer le code par CloudFormation dans ce bucket, nous nous permettons ici de le laisser directement dans le template étant donné sa taille modeste.

Nous déclarons ensuite des variables d’environnement afin de pouvoir réutiliser cette fonction AWS Lambda. A la fin, nous pouvons voir que nous lions la Lambda à un rôle. Ce rôle est assez important :

  LambdaAutoApprovalServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument: |
        {
            "Statement": [{
                "Effect": "Allow",
                "Principal": { "Service": [ "lambda.amazonaws.com" ]},
                "Action": [ "sts:AssumeRole" ]
            }]
        }
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - arn:aws:logs:*:*:*
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Resource: "*"
                Effect: Allow
                Action:
                  - codepipeline:GetPipeline
                  - codepipeline:GetPipelineState
                  - codepipeline:GetPipelineExecution
                  - codepipeline:ListPipelineExecutions
                  - codepipeline:ListPipelines
                  - codepipeline:PutApprovalResult

Ce rôle consiste à donner des droits d'écriture de logs dans CloudWatch Logs et à valider les approbations dans CodePipeline. Notez que si vous avez des politiques de sécurité strictes, vous devriez restreindre les accès au minimum dans le champs Resource.

Maintenant que la Lambda est créée, nous devons définir la “CloudWatch Event Rule” :

  ScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: Scheduled CodePipeline approval
      ScheduleExpression: "cron(0 9 ? * MON *)"
      State: ENABLED
      Targets:
        -
          Arn: !GetAtt LambdaFunction.Arn
          Id: LambdaAutomaticApproval

Cette définition est courte, il s'agit juste du planning (expression cron) et de la cible sur laquelle lier, notre Lambda en effet.

Pour rendre la règle d'événement CloudWatch capable d'invoquer le Lambda, nous devons lui donner la permission avec une déclaration AWS::Lambda::Permission :

  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt ScheduledRule.Arn

Et voilà ! Vous pouvez désormais utiliser AWS CodePipeline avec des mises en production régulières tous les lundi matins !

Alexandre KERVADEC

La discussion continue !

Nous attendons vos questions, remarques & mots doux sur notre Twitter :