Auto devops avec Gitlab CI - Part 3

Introduction

Dans la section précédente, nous avons découvert les concepts de :

  • Variables d'environnement prédéfinies
  • L'utilisation de cache
  • Des déploiements par environnement et comment créer un flux de production

Dans cette section, nous allons essayer de reproduire le schéma suivant :

processus-de-deploiement

Il s'agit d'un processus plus complexe, mais nous sommes proches de réaliser un processus proche de la réalité. Pour cela, nous allons changer de langage de programmation pour créer une application plus complexe que nous déploierons sur AWS. Êtes-vous prêt pour ce défi ?

Installation

Pour commencer, voici les étapes pour installer IntelliJ, Java et Gradle :

  • Pour installer IntelliJ, il suffit de télécharger le fichier d'installation correspondant à votre système d'exploitation depuis le site officiel et de suivre les instructions d'installation : jetbrains
  • Pour installer Java, il est recommandé de télécharger et d'installer la version correspondant à votre système d'exploitation depuis le site officiel : java
  • Pour installer Gradle, vous pouvez suivre les instructions d'installation disponibles sur le site officiel : gradle

Une fois ces étapes terminées, nous pouvons commencer.

Contexte de l'application

Gitlab branch: Java-AWS/init

Pour accéder au projet Java, vous pouvez vous rendre sur la branche Java-AWS/init. Je vous recommande de "forker" le projet pour plus de confort. Une fois cela fait, vous pouvez lancer l'application directement depuis IntelliJ. En cas d'erreur, vous pouvez exécuter la commande suivante :

 mvn clean install

Ce projet est une API, donc il n'y a pas d'interface utilisateur. Pour tester notre API, vous pouvez utiliser Postman. Tous les appels possibles sont disponibles dans le repository. En important cette collection, vous devriez trouver l'ensemble des endpoints que notre API permet d'exposer.

Dans cette section, nous allons nous concentrer sur la création du job de build pour notre application Java. Cette étape est essentielle, car elle permet de transformer le code source en langage machine.

Étape de build

GitLab Branch: Java-AWS/build

Le résultat du job de build est un fichier "JAR", qui sera ensuite interprété par le serveur. Pour cette formation, nous allons utiliser Gradle, mais vous pouvez également utiliser Maven si vous préférez.

Voici les étapes nécessaires pour la création du job de build ::

  • Une image Docker pour exécuter Java
  • Lancer un build avec Gradle
  • Sauvegarder notre artifact dans GitLab
 stages: 
  - build
build:
  stage: build
  image: openjdk:12-alpin
  script:
    - ./gradlew build
  artifacts: 
    paths:
      - ./build/libs/

Test stage

Gitlab branch: Java-AWS/test

Pour s'assurer que l'application fonctionne correctement, nous allons créer des tests de fumée. Dans un premier temps, nous allons effectuer des appels pour vérifier que l'application répond correctement.

Étape de tests de fumée

GitLab Branch: Java-AWS/smoke-tests

Dans cette étape, nous devons :

  • Créer un nouveau stage
  • Utiliser l'image Alpine par défaut
  • Créer un script qui lancera le fichier JAR créé dans l'artifact précédent avec Java
  • Attendre quelques instants pour que l'application se lance
  • Appeler l'endpoint health pour s'assurer que tout fonctionne correctement
  • Créer un before_script pour installer la dépendance permettant d'utiliser "curl"
 stages:
  - build
  - test

build:
  stage: build
  image: openjdk:12-alpine
  script:
    - ./gradlew build
  artifacts:
    paths:
      - ./build/libs/

smoke test:
  stage: test
  image: openjdk:12-alpine
  before_script:
    - apk --no-cache add curl
  script:
    - java -jar ./build/libs/cars-api.jar &
    - sleep 30
    - curl http://localhost:5000/actuator/health | grep "UP"

Deploy to AWS

Gitlab branch: Java-AWS/deploy

Je vous propose dans un premier temps, si vous n'avez pas de compte, de créer votre compte sur AWS. Une fois connecter rendez vous dans "AWS Management Console" :

All Services -> Compute -> Elastic Beanstalk -> Create new Application -> ajouter le nom de l'application et créer cette application Nous allons avoir besoin d'un environnement mais que cela fonctionne : Dans l'application créé -> Environments -> Create one now -> Web server environment -> Environment name (production) && platform (Java) && Sample application Si l'on veut déployer manuellement, nous pouvons directement le faire dans le dashboard et sélectionner notre projet (jar file). Nous allons maintenant faire ce déploiement de manière automatique ! AWS fournit des outils qui permet de nous faciliter la vie, comme "AWS Command Line Interface". C'est ce que l'on va utiliser pour intéragir avec AWS. Un autre soucis c'est que l'on ne peut pas directement intérargir avec Elastic Beanstack, nous allons devoir passé par AWS S3. C'est comme un dropbox pour nos fichiers AWS qui nous permettra de déposer notre fichier compilé.

Pour déployer notre application sur AWS S3, nous devons d'abord créer un bucket S3 sur la console AWS Management :

  1. Accédez à la console AWS Management.
  2. Sélectionnez "All Services".
  3. Accédez à la section "Storage" et sélectionnez "S3".
  4. Créez un bucket en spécifiant un nom (ex : cars-api-deployments), et cliquez sur "Next" puis sur "Create bucket".

Ensuite, pour configurer GitLab :

  1. Accédez à "Settings" puis "CI/CD".
  2. Dans la section "Variables", ajoutez une variable nommée "S3_BUCKET" avec la valeur "cars-api-deployment".

Enfin, pour interagir avec AWS, nous allons utiliser le système de ligne de commande et une image Docker. Voici les étapes à suivre :

  1. Créez un nouveau stage dans votre fichier de configuration CI/CD.
  2. Puller l'image Docker AWS.
  3. Configurez l'image pour qu'elle n'ait pas de point d'entrée.
  4. Écrivez un script pour déployer l'artifact créé vers le bucket S3 que vous avez créé précédemment.
 stages:
  - build
  - test
  - deploy

build:
  stage: build
  image: openjdk:12-alpine
  script:
    - ./gradlew build
  artifacts:
    paths:
      - ./build/libs/

smoke test:
  stage: test
  image: openjdk:12-alpine
  before_script:
    - apk --no-cache add curl
  script:
    - java -jar ./build/libs/cars-api.jar &
    - sleep 30
    - curl http://localhost:5000/actuator/health | grep "UP"

deploy:
  stage: deploy
  image:
    name: amazon/aws-cli
    entrypoint: [""]
  script:
    - aws configure set region us-east-1
    - aws s3 cp ./build/libs/cars-api.jar s3://$S3_BUCKET/cars-api.jar

Pour accéder à notre bucket S3, nous devons générer des credentials (identifiants) :

  1. Accédez à la console AWS Management.
  2. Sélectionnez "All Services".
  3. Accédez à la section "IAM" et sélectionnez "Users".
  4. Cliquez sur "Add user".
  5. Donnez un nom à l'utilisateur (ex : gitlabci) et sélectionnez "Programmatic access".
  6. Dans la section "Permissions", cliquez sur "Attach existing policies directly".
  7. Cherchez "S3" et sélectionnez "AmazonS3FullAccess".
  8. Cliquez sur "Attach policy".
  9. Dans la section "Permissions", cliquez à nouveau sur "Attach existing policies directly".
  10. Cherchez "S3" et sélectionnez "AdminstratorAccess-AWSElasticBeanStalk".
  11. Cliquez sur "Attach policy".
  12. Ne pas ajouter de tags et cliquez sur "Create user".
  13. Enregistrez les informations d'identification (access key ID et secret access key).

Maintenant, nous pouvons ajouter ces credentials dans les variables GitLab :

  1. Accédez à "Settings" puis "CI/CD".
  2. Dans la section "Variables", ajoutez une variable nommée "AWS_ACCESS_KEY_ID" avec la valeur de l'access key ID enregistré précédemment.
  3. Ajoutez une autre variable nommée "AWS_SECRET_ACCESS_KEY" avec la valeur du secret access key enregistré précédemment.

Maintenant que nous avons créé notre pipeline pour déployer notre application sur S3, nous avons besoin de la déployer sur notre serveur en utilisant une application-version dans AWS. Cela garantira la cohérence de nos déploiements et nous permettra de disposer d'un historique des versions déployées.

Voici les étapes à suivre :

  1. Créez des variables locales pour le nom de l'artifact et le nom de l'application.
  2. Utilisez la variable prédéfinie $CI_PIPELINE_IID pour créer un identifiant unique pour chaque version.
  3. Créez une application-version en utilisant le nom de l'application et l'identifiant unique créé précédemment.
  4. Mettez à jour l'environnement en précisant la version que vous venez de créer.

Ces étapes vous permettront de déployer votre application de manière cohérente et traçable sur AWS.

 variables:
  ARTIFACT_NAME: cars-api-v$CI_PIPELINE_IID.jar
  APP_NAME: cars-api
  
deploy:
  stage: deploy
  image:
    name: amazon/aws-cli
    entrypoint: [""]
  script:
    - aws configure set region us-east-1
    - aws s3 cp ./build/libs/$ARTIFACT_NAME s3://$S3_BUCKET/$ARTIFACT_NAME
    - aws elasticbeanstalk create-application-version --application-name $APP_NAME --version-label $CI_PIPELINE_IID --source-bundle S3Bucket=$S3_BUCKET,S3Key=$ARTIFACT_NAME
    - aws elasticbeanstalk update-environment --application-name $APP_NAME --environment-name "production" --version-label=$CI_PIPELINE_IID

Maintenant que tout fonctionne comme prévu, vous pouvez améliorer votre code pour qu'il fournisse des informations utiles telles que :

  • L'ID de la pipeline
  • L'ID du commit
  • Le nom de la branche

Ces informations vous aideront à mieux comprendre et à tracer vos déploiements. Voici comment les ajouter :

  1. Utilisez la variable prédéfinie $CI_PIPELINE_IID pour obtenir l'ID de la pipeline.
  2. Utilisez la variable prédéfinie $CI_COMMIT_SHA pour obtenir l'ID du commit.
  3. Utilisez la variable prédéfinie $CI_COMMIT_REF_NAME pour obtenir le nom de la branche.

Vous pouvez ensuite afficher ces informations dans les journaux de votre pipeline pour les rendre plus facilement accessibles. En ajoutant ces informations, vous pourrez mieux suivre vos déploiements et identifier les problèmes plus rapidement.

 build:
  stage: build
  image: openjdk:12-alpine
  script:
    - sed -i "s/CI_PIPELINE_IID/$CI_PIPELINE_IID/" ./src/main/resources/application.yml
    - sed -i "s/CI_COMMIT_SHORT_SHA/$CI_COMMIT_SHORT_SHA/" ./src/main/resources/application.yml
    - sed -i "s/CI_COMMIT_BRANCH/$CI_COMMIT_BRANCH/" ./src/main/resources/application.yml
    - ./gradlew build
    - mv ./build/libs/cars-api.jar ./build/libs/$ARTIFACT_NAME
  artifacts:
    paths:
      - ./build/libs/

Maintenant que votre application est déployée sur AWS, vous pouvez utiliser GitLab pour vérifier que la version déployée est bien celle que vous avez choisie. Voici les étapes à suivre :

  1. Installez la dépendance CURL pour effectuer des appels API.
  2. Installez la dépendance JQ pour extraire facilement des informations d'un fichier JSON.
  3. Utilisez CURL pour effectuer un appel à l'application AWS et vérifier que la version déployée est bien celle que vous avez choisie.
  4. Utilisez JQ pour extraire les données de CNAME du résultat JSON.
  5. Vérifiez que l'application est bien en train de tourner sur AWS.
 deploy:
  stage: deploy
  image:
    name: amazon/aws-cli
    entrypoint: [""]
  before_script:
    - apk --no-cache add curl
    - apk --no-cache add jq
  script:
    - aws configure set region us-east-1
    - aws s3 cp ./build/libs/$ARTIFACT_NAME s3://$S3_BUCKET/$ARTIFACT_NAME
    - aws elasticbeanstalk create-application-version --application-name $APP_NAME --version-label $CI_PIPELINE_IID --source-bundle S3Bucket=$S3_BUCKET,S3Key=$ARTIFACT_NAME
    - CNAME=$(aws elasticbeanstalk update-environment --application-name $APP_NAME --environment-name "production" --version-label=$CI_PIPELINE_IID | jq '.CNAME' --raw-output)
    - sleep 45
    - curl http://$CNAME/actuator/info | grep $CI_PIPELINE_IID

Code quality

Gitlab branch: Java-AWS/quality

Lorsque plusieurs développeurs travaillent ensemble sur un projet, il est important de s'assurer que les bonnes pratiques et les guidelines sont respectées, ainsi que le formattage du code. Pour cela, nous allons utiliser PMD, un outil d'analyse de code.

Si PMD n'est pas déjà installé dans votre projet, vous pouvez l'installer en suivant les instructions de votre système de gestion de paquets ou en le téléchargeant directement depuis le site web de PMD.

Une fois PMD installé, voici comment l'utiliser :

  1. Ouvrez un terminal de commande dans le projet.
  2. Exécutez la commande PMD pour lancer l'analyse de code.
  3. Analysez les résultats de PMD pour identifier les erreurs et les violations de guidelines.
  4. Corrigez les erreurs et les violations de guidelines pour améliorer la qualité de votre code.

En utilisant PMD, vous pourrez vous assurer que votre code respecte les bonnes pratiques et les guidelines, ce qui facilitera la collaboration entre les développeurs et améliorera la qualité du code de votre projet.

 ./gradlew pmdMain pmdTest

Pour utiliser PMD et appliquer des règles de qualité de code à votre projet, vous pouvez ajouter les règles dans le fichier "pmd-ruleset.xml" qui se trouve à la racine du projet. Pour automatiser cette vérification, vous pouvez créer un nouveau job dans votre pipeline.

Voici comment procéder :

  1. Créez un nouveau job dans votre fichier de configuration CI/CD.
  2. Liez-le au stage de test pour vérifier la qualité du code avant le déploiement.
  3. Écrivez un script qui exécute la commande PMD que vous avez utilisée précédemment.
  4. Enregistrez le résultat de PMD sous forme d'artifact pour pouvoir le consulter facilement en cas d'erreur de code.

En ajoutant ce job à votre pipeline, vous pourrez automatiser la vérification de la qualité de votre code à chaque fois que vous lancez un déploiement. Cela vous permettra de détecter rapidement les erreurs et les violations de guidelines, et de les corriger avant de déployer votre application.

 code quality:
  stage: test
  image: openjdk:12-alpine
  script:
    - ./gradlew pmdMain pmdTest
  artifacts:
    when: always
    paths:
      - build/reports/pmd

Unit test stage

Gitlab branch: Java-AWS/unit-test

Dans votre application, vous avez déjà des tests unitaires qui garantissent que de nouvelles implémentations ne cassent pas les anciennes fonctionnalités. Pour intégrer cette contrainte dans votre pipeline de déploiement, vous pouvez ajouter un nouveau job pour exécuter les tests unitaires.

Voici comment procéder :

  1. Créez un nouveau job dans votre fichier de configuration CI/CD.
  2. Liez-le au stage de test pour exécuter les tests avant le déploiement.
  3. Utilisez Gradle (ou un autre outil) pour exécuter les tests unitaires.
  4. Enregistrez le rapport de tests sous forme d'artifact pour pouvoir le consulter facilement.
 unit tests:
  stage: test
  image: openjdk:12-alpine
  script:
    - ./gradlew test
  artifacts:
    when: always
    paths:
      - build/reports/tests
    reports:
      junit: build/test-results/test/*.xml

API test stage

Gitlab branch: Java-AWS/api-test-stage

Pour assurer le bon fonctionnement de vos endpoints, vous pouvez ajouter des tests fonctionnels à votre pipeline de déploiement. Voici comment procéder :

  1. Créez un fichier JSON avec les données que vous souhaitez utiliser pour tester vos endpoints.
  2. Ajoutez ce fichier à votre projet, par exemple dans le dossier de tests.
  3. Créez un nouveau job dans votre fichier de configuration CI/CD.
  4. Liez-le au stage de test pour exécuter les tests avant le déploiement.
  5. Dans ce job, lancez votre application et utilisez Postman (ou un autre outil) pour envoyer des requêtes à vos endpoints.
  6. Vérifiez que les résultats retournés par vos endpoints sont bien ceux que vous attendez.
  7. Enregistrez les résultats des tests sous forme d'artifact pour pouvoir les consulter facilement.
 {
   "info":{
      "_postman_id":"e4a42f44-a58f-4bfa-a5d0-5557a0a36247",
      "name":"Cars API",
      "schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
   },
   "item":[
      {
         "name":"CRUD",
         "item":[
            {
               "name":"Get all cars",
               "event":[
                  {
                     "listen":"test",
                     "script":{
                        "id":"838290a6-f7c6-417d-a387-7df2d2c719f4",
                        "exec":[
                           "pm.test(\"Status code is 200\", function () {",
                           "    pm.response.to.have.status(200);",
                           "});"
                        ],
                        "type":"text/javascript"
                     }
                  }
               ],
               "request":{
                  "method":"GET",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/cars",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "cars"
                     ]
                  }
               },
               "response":[
                  
               ]
            },
            {
               "name":"Add car",
               "request":{
                  "method":"POST",
                  "header":[
                     {
                        "key":"Content-Type",
                        "name":"Content-Type",
                        "value":"application/json",
                        "type":"text"
                     }
                  ],
                  "body":{
                     "mode":"raw",
                     "raw":"{\n    \"manufacturer\": \"Dacia\",\n    \"model\": \"Logan\",\n    \"build\": 2000\n}",
                     "options":{
                        "raw":{
                           "language":"json"
                        }
                     }
                  },
                  "url":{
                     "raw":"{{baseUrl}}/cars",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "cars"
                     ]
                  }
               },
               "response":[
                  
               ]
            },
            {
               "name":"Get single car",
               "event":[
                  {
                     "listen":"test",
                     "script":{
                        "id":"907cccdc-8da6-426d-9bef-2a618a44e8e2",
                        "exec":[
                           "pm.test(\"Status code is 200\", function () {",
                           "    pm.response.to.have.status(200);",
                           "});"
                        ],
                        "type":"text/javascript"
                     }
                  }
               ],
               "request":{
                  "method":"GET",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/cars/4",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "cars",
                        "4"
                     ]
                  }
               },
               "response":[
                  
               ]
            },
            {
               "name":"Delete car",
               "request":{
                  "method":"DELETE",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/cars/1",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "cars",
                        "1"
                     ]
                  }
               },
               "response":[
                  
               ]
            }
         ],
         "protocolProfileBehavior":{
            
         }
      },
      {
         "name":"Statistics",
         "item":[
            {
               "name":"Average fleet age",
               "event":[
                  {
                     "listen":"test",
                     "script":{
                        "id":"79b19feb-1129-451d-8d19-f8a1a512e5dd",
                        "exec":[
                           "pm.test(\"Status code is 200\", function () {",
                           "    pm.response.to.have.status(200);",
                           "});"
                        ],
                        "type":"text/javascript"
                     }
                  }
               ],
               "request":{
                  "method":"GET",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/statistics/age",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "statistics",
                        "age"
                     ]
                  }
               },
               "response":[
                  
               ]
            }
         ],
         "protocolProfileBehavior":{
            
         }
      },
      {
         "name":"Health",
         "item":[
            {
               "name":"Health",
               "event":[
                  {
                     "listen":"test",
                     "script":{
                        "id":"1743a75d-932a-4e2d-b5d4-cf5b98c3c7f1",
                        "exec":[
                           "pm.test(\"Status code is 200\", function () {",
                           "    pm.response.to.have.status(200);",
                           "});"
                        ],
                        "type":"text/javascript"
                     }
                  }
               ],
               "request":{
                  "method":"GET",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/actuator/health",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "actuator",
                        "health"
                     ]
                  }
               },
               "response":[
                  
               ]
            },
            {
               "name":"Info",
               "event":[
                  {
                     "listen":"test",
                     "script":{
                        "id":"72e253db-70d1-4fce-9d7f-a3647081b16f",
                        "exec":[
                           "pm.test(\"Status code is 200\", function () {",
                           "    pm.response.to.have.status(200);",
                           "});"
                        ],
                        "type":"text/javascript"
                     }
                  }
               ],
               "request":{
                  "method":"GET",
                  "header":[
                     
                  ],
                  "url":{
                     "raw":"{{baseUrl}}/actuator/info",
                     "host":[
                        "{{baseUrl}}"
                     ],
                     "path":[
                        "actuator",
                        "info"
                     ]
                  }
               },
               "response":[
                  
               ]
            }
         ],
         "protocolProfileBehavior":{
            
         }
      }
   ],
   "protocolProfileBehavior":{
      
   }
}

Maintenant que vous avez créé des tests fonctionnels pour vos endpoints, vous pouvez ajouter un nouveau stage à votre pipeline pour les exécuter. Voici comment procéder :

  1. Ajoutez un nouveau stage à votre fichier de configuration CI/CD, par exemple "api testing".
  2. Créez un nouveau job dans ce stage pour exécuter les tests fonctionnels.
  3. Importez Newman (ou un autre outil) pour lancer Postman.
  4. Fixez la version de Newman que vous voulez utiliser pour vous assurer de ne pas être contraint par de nouvelles images.
  5. Lancez votre collection de tests à l'aide de Newman.
  6. Enregistrez les résultats de Newman sous forme d'artifact pour pouvoir les consulter facilement.
 stages:
  - build
  - test
  - deploy
  - api testing

api testing:
  stage: post deploy
  image:
    name: vdespa/newman
    entrypoint: [""]
  script:
    - newman --version
    - newman run "Cars API.postman_collection.json" --environment Production.postman_environment.json --reporters cli,htmlextra,junit --reporter-htmlextra-export "newman/report.html" --reporter-junit-export "newman/report.xml"
  artifacts:
    when: always
    paths:
      - newman/
    reports:
      junit: newman/report.xml

Developpeur et architecte passionné, qui souhaite partagé son univers et ses découvertes afin de rendre les choses plus simple pour chacun

URLs

Check les divers liens pour cet article