lundi , 10 décembre 2018
Home » Tutoriel » Comment augmenter la vitesse de vos builds de déploiement continu AWS Lambda ?

Comment augmenter la vitesse de vos builds de déploiement continu AWS Lambda ?

Parallélisez vos builds Maven, rendez-les reproductibles, et lancez le volume à 11 en utilisant Docker pour la résolution des dépendances.

Les avantages des pratiques de déploiement continu débloquent un monde de promesses. L’automatisation des workflows dans un pipeline unifié réduit considérablement les risques tout en augmentant la productivité globale de l’équipe.

Par rapport aux déploiements de production de Bad Old Days™, un temps de déploiement automatisé de bout en bout de 10 à 20 minutes est sûrement assez rapide, n’est-ce pas ? C’est faux.

De nos jours, l’équipe de développement hautement performante utilise des workflows automatisés pour déployer le code beaucoup plus fréquemment et avec des délais plus courts.

Les ingénieurs d’Amazon déploient le code toutes les 11,7 secondes, en moyenne, réduisant à la fois le nombre et la durée des interruptions. Les ingénieurs Netflix déploient le code des milliers de fois par jour.

La rapidité de mise sur le marché constitue un avantage concurrentiel majeur et chaque seconde compte. Même une petite réduction du temps de cycle d’un workflow automatisé peut se cumuler en économies massives – en particulier lorsqu’il est appliqué à des centaines de déploiements quotidiens.

Le défi des systèmes de build éphémères

Dans les Bad Old Days™ de déploiement continu, les systèmes de build ont été conçus comme une ressource partagée. Ils présentaient tous les avantages de la gestion de la mise en cache partagée et des dépendances, ainsi que de tous les inconvénients inhérents à une configuration incohérente, à un état accumulé et à des processus mal isolés.

Les workflows automatisés d’aujourd’hui utiliseront probablement un système de build qui va tordre et détruire l’infrastructure à la volée – en utilisant des services tels que Travis CI ou CodeBuild d’AWS. Cette approche reflète étroitement les caractéristiques immuables et éphémères des déploiements d’applications natives cloud.

Puisque les systèmes de build modernes traitent l’infrastructure et l’environnement comme immuables, le processus de build s’amorce à partir de rien pour chaque build – sans les avantages de l’installation ou de l’optimisation. Ce n’est pas une pénalité énorme pour les builds simples, mais cela peut considérablement ralentir le cycle de feedback pour les builds complexes avec de nombreuses dépendances de bibliothèques externes à télécharger.

Si vous avez déjà utilisé Maven (un outil de build Java commun), l’observation sur le « téléchargement de l’Internet entier » devrait vous être familière. Les développeurs Java ajoutent simplement leurs bibliothèques externes à un fichier pom.xml, puis laissent Maven les télécharger avec toutes leurs dépendances dans un cache.

Étant donné que les fichiers téléchargés sont mis en cache par Maven dans un référentiel local, les systèmes de build traditionnels de Bad Old Days™ ne subissent que le processus de téléchargement pénible lors de la configuration initiale. Pour les systèmes de build éphémères tels que CodeBuild, Maven subit le processus de téléchargement pénible chaque fois que le build s’exécute, car le cache ne peut pas être accédé d’une génération à l’autre.

Puisque chaque seconde compte, il est essentiel que nous apprenions comment accélérer le processus de déploiement continu des systèmes de build modernes. Dans cet article, nous allons examiner quelques techniques utilisant Docker et Amazon EC2 Container Registry (ECR) – ainsi que quelques astuces avec Maven.

Les bases d’un pipeline de déploiement continu

Commençons rapidement par examiner un pipeline de déploiement continu de base pour une application sans serveur sur AWS, en utilisant certaines fonctions et composants Lambda Java comme API Gateway et DynamoDB.

Tout le code de cet article peut être trouvé sur le site de Symphonia Github.

Les actions de CodePipeline

Source

Le pipeline de déploiement continu est lancé depuis notre référentiel de code source. Chaque fois qu’une nouvelle validation (ou un ensemble de validations) est effectuée sur la branche maître, elle déclenche AWS CodePipeline pour démarrer une nouvelle exécution. En termes de CodePipeline, il s’agit de l’action « Source ».

Build

L’action « Source » déclenche ensuite une action « Build », qui dans ce cas est un travail AWS CodeBuild. CodeBuild place un conteneur de build, charge la nouvelle version de notre code source et exécute les étapes de build définies dans le fichier buildspec.yml à la racine de notre projet.

Dans notre cas, le fichier buildspec.yml contient un appel Maven qui exécute la commande mvn package. Cela compile notre code, exécute des tests unitaires, puis crée un uberjar pour chaque AWS Lambda. Chaque uberjar contient du code Lambda et toutes les dépendances répertoriées dans le fichier pom.xml correspondant.

Après que Maven ait compilé les fichiers uberjar et toujours dans l’action « Build » du pipeline, la commande aws cloudformation package utilise notre fichier SAM (Serverless Application Model) pour déterminer quels uberjars doivent être téléchargés sur S3 afin de mettre à jour le code dans notre diverses fonctions Lambda.

Déployer

Après l’action « Build », deux actions connexes créent, puis appliquent des modifications à notre infrastructure à l’aide de CloudFormation. Si vous avez déjà utilisé le modèle d’application sans serveur, cela équivaut à la commande aws cloudformation deploy.

Et après ces quatre étapes, notre pipeline de déploiement continu est terminé ! Notre application sans serveur est maintenant mise à jour avec un nouveau code, une nouvelle configuration et potentiellement une nouvelle infrastructure.

 

 

Pas si vite

Pour une application sans serveur simple avec seulement quelques fonctions Lambda et des changements d’infrastructure minimes, une durée d’exécution de pipeline normale peut ne prendre que quelques minutes. Environ 25 à 30% de ce temps de cycle est passé à l’étape de build – sur laquelle nous avons le plus de contrôle.

Pour une application sans serveur plus grande ou plus complexe, seule l’étape de build peut prendre plusieurs minutes, en particulier s’il existe plusieurs dizaines de modules Maven avec des tests unitaires. Chacun de ces modules doit passer par le processus de résolution des dépendances, ce qui peut impliquer le téléchargement de bibliothèques à partir de Maven Central ou d’autres référentiels.

Une fois que chaque module est compilé, les tests sont exécutés, et enfin, le module est potentiellement regroupé dans un uberjar. Tous les uberjars ayant des sommes de contrôle MD5 différentes provenant du dernier déploiement Lambda sont chargés dans S3, de sorte que les fonctions Lambda peuvent être mises à jour.

Même code, signature différente

Un problème compliqué et souvent négligé avec ce schéma de contrôle est que les builds ne sont pas strictement reproductibles dans un processus de build Maven normal. Les fichiers JAR sont simplement des archives zip avec des informations supplémentaires spécifiques à Java, et ces informations incluent des horodatages et d’autres sorties non déterministes.

Cela signifie que même avec le même fichier pom.xml et le même code source, les fichiers JAR produits face-à-face auront des sommes de contrôle MD5 différentes. Du point de vue de SAM, cela signifie que ces fichiers JAR sont nouveaux et doivent être téléchargés sur S3.

Les opportunités d’amélioration

Au cours de l’aperçu de notre pipeline de déploiement continu, nous avons déjà identifié trois domaines d’amélioration.

Tout d’abord, parce que notre build Maven s’exécute dans un tout nouveau conteneur CodeBuild chaque fois qu’une nouvelle build est exécutée, il doit télécharger toutes les dépendances du projet à partir de zéro. Non seulement cela n’est pas nécessaire, mais ces appels HTTP vers des référentiels externes sont lents et sujets aux erreurs.

Deuxièmement, même le conteneur CodeBuild le plus bas possède deux cœurs de processeur. Maven n’en profite pas, donc les modules sont compilés, testés et emballés les uns après les autres, en série.

Enfin, étant donné que notre build Maven n’est pas strictement reproductible, la commande aws cloudformation package télécharge toujours les fichiers uberjar, même si cela n’est pas nécessaire.

Alors allons voir plus de détails et découvrons comment accélérer la partie de build de notre pipeline de déploiement continu avec deux corrections simples et un changement complexe – mais valable -.

Deux corrections simples

Paralléliser un build Maven est assez simple. Ajoutez simplement l’indicateur -T, et soit une valeur numérique pour le nombre de threads que Maven doit utiliser, soit un argument comme `1C` pour indiquer à Maven de connaître le nombre de cœurs de CPU disponibles et assigner un seul thread au cœur.

Vous pouvez en savoir plus sur cette fonctionnalité Maven ici.

La deuxième solution rapide de Maven consiste à configurer des builds reproductibles. Le plugin-reproducible-build-maven (reproducible-build-maven-plugin) peut être ajouté à votre fichier pom.xml à côté de tous les autres plugins de build. Il supprime les informations non déterministes ou non répétables des artefacts produits par Maven.

Cela signifie que pour les mêmes dépendances et le code source, vous obtiendrez toujours exactement le même fichier JAR, avec la même somme de contrôle MD5. Par conséquent, la commande aws cloudformation package ne télécharge pas inutilement les fichiers JAR.

Augmenter jusqu’à 11

Maintenant, nous allons vraiment augmenter le volume, à la fois en termes d’impact au temps et, malheureusement, à la complexité de build.

À ce stade, nous avons déjà optimisé la construction et le déploiement de code avec des versions parallèles et reproductibles. Mais, nous n’avons pas traité la question de la résolution de la dépendance. Pour résoudre ce problème, nous aurons besoin de l’aide d’un véritable ami sans serveur – Docker !

CodeBuild et la Baleine

Comme base pour les processus de buid sous-jacents, CodeBuild utilise des conteneurs Docker. Par défaut, il utilise des conteneurs fournis par AWS et ne nécessite aucune configuration ou autorisation supplémentaire.

Avec un peu de travail supplémentaire, nous pouvons construire notre propre image Docker qui contient non seulement Java SDK et Maven, mais aussi toutes les dépendances pour notre projet. De cette façon, lorsque CodeBuild lance le conteneur, il a déjà tout ce qu’il faut pour construire notre projet.

En outre, il est apparent (mais pas nécessairement documenté) que CodeBuild met en cache les images Docker qu’il utilise. Cela signifie qu’il télécharge notre image spéciale, plus grande de temps en temps – et la réutilise souvent et rapidement.

Un autre avantage de l’utilisation de notre propre image Docker est la possibilité de mettre à jour d’autres logiciels dans le conteneur. Par exemple, l’image actuelle utilisée par CodeBuild pour les projets Maven contient une version obsolète de l’interface de ligne de commande AWS. Pour prendre en charge correctement le modèle d’application sans serveur, l’AWS CLI doit être mise à jour. Avant, nous effectuions la mise à jour pendant l’exécution de notre processus de build – ce qui ralentissait les choses. Maintenant, nous pouvons simplement mettre à jour AWS CLI dans l’image du conteneur elle-même !

CloudFormation à la rescousse !

Notre première approche des problèmes épineux d’infrastructure consiste à demander : « CloudFormation peut-il le faire ? » Dans ce cas, la réponse est un oui retentissant.

Nous avons déjà codé tout notre pipeline de livraison continue dans un modèle CloudFormation, nous allons donc simplement ajouter les composants nécessaires pour que nous puissions utiliser une image Docker personnalisée avec CodeBuild.

Tout d’abord, nous allons ajouter un ECR, ou EC2 Container Repository. C’est essentiellement la version AWS du hub Docker. Il n’y a pas grand-chose à configurer ici, mais les permissions sont importantes – et faciles à corriger.

Le service CodeBuild tirera lui-même des images à partir du référentiel, nous avons donc besoin d’une politique à cet effet. Notez qu’il s’agit d’une stratégie de ressources – nous définissons ces autorisations sur la ressource AWS elle-même, et non sur un rôle ou un utilisateur IAM.

CodeBuildImageRepository:
  Type: AWS::ECR::Repository
  Properties:
    RepositoryPolicyText:
      Version: "2012-10-17"
      Statement:
      - Sid: AllowPull
        Effect: Allow
        Principal:
          Service: "codebuild.amazonaws.com"
        Action:
        - "ecr:GetDownloadUrlForLayer"
        - "ecr:BatchGetImage"
        - "ecr:BatchCheckLayerAvailability"
        - ...

Ensuite, nous allons reconfigurer la section CodeBuild de notre modèle CloudFormation pour utiliser une image Docker personnalisée, en remplaçant la valeur aws/codebuild/java:openjdk-8 qui était auparavant.

Nous utilisons ici la fonction intrinsèque CloudFormation !Sub pour générer dynamiquement le nom de l’image Docker. Nous allons également exporter cette valeur afin que nous puissions y accéder plus facilement depuis l’extérieur de CloudFormation.

CodeBuildProject:
  Type: AWS::CodeBuild::Project
  DependsOn: CodeBuildRole
  Properties:
    Artifacts:
      Type: CODEPIPELINE
    Environment:
      ComputeType: BUILD_GENERAL1_SMALL
      Image: aws/codebuild/docker:1.12.1
      Type: LINUX_CONTAINER
      EnvironmentVariables:
      - Name: ECR_ACCOUNT_ID
        Value: !Ref AWS::AccountId
      - Name: ECR_IMAGE_TAG
        Value: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ImageRepository}:${ApplicationStackName}"
      ServiceRole: !Ref CodeBuildRole
      TimeoutInMinutes: 10
      Source:
        Type: CODEPIPELINE
        BuildSpec: infrastructure/image-buildspec.yml

Avec ces deux modifications apportées, nous pouvons mettre à jour notre pile de pipeline de build :

$ aws cloudformation update-stack \
    --capabilities CAPABILITY_IAM \
    --stack-name serverless-build \
    --template-body file://build-pipeline.yml

Et maintenant, nous devons construire notre image Docker personnalisée, contenant une AWS CLI mise à jour, le Java SDK, Maven, et bien sûr, toutes les dépendances Maven pour notre projet.

Voici le Dockerfile que nous utiliserons pour construire notre image personnalisée :

FROM openjdk:8-jdk
RUN apt-get -y update && apt-get -y upgrade
RUN apt-get -y install python-pip python-setuptools python-wheel — no-install-recommends
RUN pip install — upgrade awscli
ARG MAVEN_VERSION=3.5.0
ARG USER_HOME_DIR=”/root”
ARG SHA=beb91419245395bd69a4a6edad5ca3ec1a8b64e41457672dc687c173a495f034
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
 && curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
 && echo “${SHA} /tmp/apache-maven.tar.gz” | sha256sum -c — \
 && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven — strip-components=1 \
 && rm -f /tmp/apache-maven.tar.gz \
 && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG “$USER_HOME_DIR/.m2”
RUN mkdir — parents /usr/src/app
WORKDIR /usr/src/app
ADD . /usr/src/app
RUN mvn verify clean — fail-never

Il se passe beaucoup de choses ici, alors commençons au début et expliquons chaque élément.

  1. Premièrement, nous utilisons l’image openjdk:8-jdk comme base. Cela nous donnera la version la plus récente d’OpenJDK 8 (aussi connue comme Java 8).
  2. Ensuite, nous utilisons apt pour mettre à jour le système et installer la dernière version de l’AWS CLI.
  3. Les lignes suivantes téléchargent et installent Maven v3.5.0.
  4. Enfin, et surtout, nous AJOUTONS notre code de projet au conteneur et exécutons mvn verify clean. Ce processus télécharge toutes les dépendances Maven dont notre projet a besoin et les stocke dans l’image Docker elle-même. Cela s’oppose à l’utilisation d’un VOLUME externe, comme le font la plupart des images Docker/Maven pré-emballées.

Grâce à Dockerfile, nous pouvons construire une image localement à l’aide de la commande docker build . Lorsque l’image se termine, nous devons l’étiqueter avec le même nom d’image que celui utilisé dans la section de configuration CodeBuild de notre fichier CloudFormation.

Tout d’abord, récupérons le nom de l’image CodeBuild à partir de CloudFormation :

$ aws cloudformation list-exports \
    --query ‘Exports[?Name==`ServerlessWeatherCodeBuildImage`].Value’ \
    --output text
1234567890.dkr.ecr.us-west-2.amazonaws.com/serve-codeb-kfh86yr5we9w:serverless-weather-build

Maintenant, vu la sortie qui ressemble à ceci depuis la commande `docker build .`

$ docker build .
…
Successfully built b0448fb85c04

Nous pouvons marquer la nouvelle image comme ceci :

$ docker tag b0448fb85c04 \
    1234567890.dkr.ecr.us-west-2.amazonaws.com/serve-codeb-kfh86yr5we9w:serverless-weather-build

Avant que nous puissions pousser l’image vers ECR, nous devons authentifier notre Docker local dans le dépôt distant. Nous pouvons le faire en utilisant la commande AWS CLI, qui génère elle-même une commande Docker :

$ aws ecr get-login — registry-ids 1234567890
docker login -u AWS -p VERYLONGENCRYPTEDSTRING \
-e none https://1234567890.dkr.ecr.us-west-2.amazonaws.com

Copiez et collez la commande Docker retournée, pour vous authentifier auprès d’ECR :

$ docker login -u AWS -p VERYLONGENCRYPTEDSTRING \
    -e none https://1234567890.dkr.ecr.us-west-2.amazonaws.com
…
Login Succeeded

Et maintenant, nous pouvons pousser notre image Docker personnalisée vers le dépôt, afin qu’elle puisse être utilisée par CodeBuild (cela peut prendre un certain temps, étant donné la taille de l’image Docker) :

$ docker push 1234567890.dkr.ecr.us-west-2.amazonaws.com/serve-codeb-kfh86yr5we9w:serverless-weather-build

Nous avons effectué ces étapes manuellement, mais sans surprise cela peut être configuré comme un pipeline CodePipeline séparé, pour construire automatiquement des images Docker pour une utilisation par CodeBuild. Nous avons un modèle CloudFormation qui vous permet de le faire ici.

Récolter les récompenses

Une fois ces étapes terminées, nous pouvons lancer notre pipeline de déploiement continu avec une validation dans notre référentiel source. Maintenant, bien sûr, la première exécution de notre pipeline prendra plus de temps. C’est parce que CodeBuild télécharge notre image Docker personnalisée, qui est plus grande que l’image fournie par AWS standard.

Cependant, les exécutions ultérieures de notre pipeline seront plus rapides, car la portion CodeBuild du pipeline sera de 50 à 60% plus rapide ! Pour notre projet simple de deux Lambda Maven, cela signifie une amélioration de 30 à 40 secondes.

Pour un projet plus complexe, cela peut prendre quelques minutes, chaque fois que votre pipeline de déploiement continu s’exécute. Avec quelques déploiements par jour sur quelques projets différents, les économies s’accumulent – et cela signifie moins de temps à attendre que les builds se terminent et un cycle de rétroaction plus court !

À lire aussi

Les compétences en machine learning que les ingénieurs logiciels doivent avoir

Vous n’avez pas besoin d’être un scientifique des données pour faire de machine learning (apprentissage …