logo le blog invivoo blanc

Déployer une infrastructure Terraform avec Jenkins & XL Deploy

18 février 2021 | Deliver & Run, DevOps | 0 comments

Introduction

Garder une infrastructure cohérente, voir identique entre les différents environnements de test, pré-production ou production est un enjeu majeur pour toutes les équipes qui en ont la charge. A mesure que la complexité de cette infrastructure augmente, il devient de plus en plus difficile de garantir l’homogénéité de l’ensemble.
C’est ici qu’intervient Terraform et le principe de “l’Infrastructure as Code” (laC) : il n’y a plus de configuration manuelle à faire sur la console du cloud provider et toutes les opérations se font au travers d’un éditeur de texte (l’un des plus populaire étant Visual Studio Code et son ensemble d’extensions) et d’un ensemble de variables.

Une infrastructure minimale de Terraform ressemblerait à ça :

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
  required_providers {
    aws = {
        source  = "hashicorp/aws"
        version = "~> 3.0"
    }
  }
}

provider "aws" {
    region  = "eu-west-1"
    access_key = "{{access_key}}"
    secret_key = "{{secret_key}}"
}

resource "aws_instance" "web_server" {
    ami = "ami-0bb3fad3c0286ebd5"
    instance_type = "t2.micro"
    count = 2
    tags = {
        Name = "web"
        Description = "Deployed w/ Terraform"
    }
}

Les deux premières sections “terraform” et “provider” permettent de configurer terraform pour AWS. La troisième permet de créer les ressources utilisées par notre infrastructure, à savoir deux EC2 (Elastic Compute Cache, le service de VM sur AWS). Le déploiement de code sur AWS crée automatiquement deux ec2 de type t2.micro avec les tags donnés, comme visible sur la capture d’écran ci-dessous.

Plugins nécessaires

Pour réaliser une pipeline Cl/CD Terraform Jenkins/XL Deploy, quelques plugins sont nécessaires :

Présentation de la pipeline

Nous allons revenir sur chaque élément de cette pipeline, en attendant voici celle qui vous est proposée :

  1. L’utilisateur publie son code sur le repo git
  2. Un build est créé par Jenkins, soit par une action humaine soit automatique (se référencer à la section “Hook” de l’outil que vous utilisez pour gérer votre repo)
  3. Jenkins via le plugin XL Deploy va créer un package qui pourra être déployé directement
  4. XL Deploy via le plugin Terraform exécutera la commande qui va déployer l’infrastructure telle que codée dans Terraform

Terraform

Cet article n’a pas vocation à présenter en détail le fonctionnement de terraform, certains éléments sont toutefois à préciser. Néanmoins si vous avez besoin d’un accompagnement particulier, vous pouvez vous renseigner sur notre l’expertise DevOps.

L’usage le plus simple de Terraform consiste à écrire le code et à l’exécuter par une commande terraform apply afin de créer l’infrastructure. Pour une structure importante qui peut avoir plusieurs dizaines d’applications et trois ou quatre environnements par lesquels doivent passer chacune d’entre elles avant le déploiement en production, la complexité de l’écosystème requiert plus de suivi, de visibilité et même de procédure qui demandent l’usage d’outils de déploiement en remplacement du lancement d’une simple commande à la main.

L’idée sera donc ici de créer des packages versionnés facilement identifiables qui seront déployés dans chaque environnement, soit via un glisser-déposer (par exemple dans le cas de la mise en production) soit automatiquement dès la création du package.

Pour effectuer ces opérations, Terraform se base sur l’API fourni par la plateforme sur laquelle l’infrastructure sera déployée : dans l’exemple plus haut, le provider (fournisseur) est donc AWS. Terraform est donc restreint par ce que l’API du provider autorise, cela n’a généralement pas d’impact dans l’usage mais il est important d’avoir en tête les quotas de requête API : certains provider imposent une limite sur le nombre d’appels API par laps de temps et lors d’un déploiement trop important d’un coup, il est possible d’être bloqué temporairement, le quota ayant été atteint.

Terraform se veut “cloud agnostic”, c’est à qu’il peut fonctionner sur n’importe quel type de cloud. Derrière ce terme se cache plus un vœu pieu qu’une réalité puisque, à service équivalent, chaque provider ne le gère pas de la même manière et que d’autres services n’ont pas d’équivalence chez la concurrence. Terraform ne trivialise donc pas la migration de service entre providers.

Git

Introduction à Git

Git est un logiciel de gestion de versions décentralisé (en anglais Version Control System, VCS). Il existe beaucoup de VCS, comme SVN ou Mercurial mais Git est le plus populaire d’entre-deux c’est pourquoi nous allons nous baser sur celui-ci.

Un VCS va permettre à une équipe de travailler sur un même projet en faisant de son côté des modifications avant de push ces modifications sur un repository (aussi appelé par apocope repo ou dépot en Français), un serveur qui permettra d’une part de centraliser toutes les modifications faites et d’autre part de s’assurer que les utilisateurs travaillent bien sur la dernière version du code disponible. Il est possible de créer le repository de toute pièce avec des commandes git mais la plupart du temps, on utilisera plutôt un outil comme GitHub, GitLab ou Bitbucket. Ces outils possédant un lot de fonctions, de configurations et d’intégrations utiles.

Plus d’informations sur Git sont disponibles sur le site officiel de Git ou en Français sur celui d’Atlassian (éditeur de Bitbucket).

Workflow

Comme à chaque fois qu’il s’agit d’organisation, le meilleur workflow pour Git est celui qui répond au mieux aux besoin d’une équipe. Quelques-uns de ces workflow sont particulièrement populaires, comme Git Flow, assez complet ; ou GitHub Flow, plus léger.

L’idée générale derrière ces workflows reste la même : une branche master qui contient un code prêt pour la production et un certain nombre de branche temporaire ou non qui vont permettre de développer les nouveaux pans de l’infrastructure avant d’être fusionnés dans la branche master.

XL Deploy

XL Deploy est un ARA (Application Release Automation) qui permet de déployer des paquets vers un ou plusieurs serveurs et d’exécuter des scripts pour leur installation. Ces paquets prennent la forme d’un “Deployment ARchive” ou DAR. Un .dar est dans les faits un fichier zip qui comprend l’ensemble des éléments à déposer (scripts, assets, etc) ainsi qu’un fichier deployit-manifest.xml. Ce fichier xml contient les instructions pour XL Deploy dont :

  • L’ordre des opérations
  • Les modules XLD à utiliser
  • Les commandes à exécuter

Des informations sur la rédaction de deployit-manifest.xml peuvent être trouvées dans la documentation de XL Deploy. Il est possible de passer des variables d’environnement Jenkins dans ce fichier.

Au moment du déploiement, XLD scan les fichiers du .dar pour y trouver les placeholders (le nom des variables XL Deploy) pour les ajouter à deployit-manifest.xml en vu d’être complétés par les dictionnaires XLD ; le dictionnaire étant une table où se trouve la liste des placeholders et la valeur qu’on leur a attribué.

deployit-manifest.xml

Présent dans chaque .dar de XLD, le deployit-manifest.xml donne à XLD le déroulé du déploiement.

<?xml version="1.0" encoding="UTF-8"?>
<udm.DeploymentPackage version="$BRANCH_NAME-$BUILD_NUMBER" application="{{application}}">
  <application />
  <satisfiesDbaValidation>false</satisfiesDbaValidation>
  <deployables>
    <terraform.Module name="/infra-terraform" file="package-terraform.zip">
      <scanPlaceholders>true</scanPlaceholders>
      <preScannedPlaceholders>true</preScannedPlaceholders>
      <placeholders>
      </placeholders>
      <targets />
      <inputVariables>
        <entry key="environment">$BRANCH_NAME</entry>
      </inputVariables>
    </terraform.Module>
  </deployables>
  <applicationDependencies />
  <dependencyResolution>LATEST</dependencyResolution>
  <undeployDependencies>false</undeployDependencies>
  <templates />
  <boundTemplates />
</udm.DeploymentPackage>

Quelques informations :

  • Il y a deux types  de variables dans ce fichier :
    • Les variables sous la forme $variable sont remplacées par jenkins au moment de la création du package. Il s’agit ici uniquement de variable d’environnement.
    • Celles en {{variable}} sont des variables XL Deploy et sont remplacées au moment du déploiement d’après les valeurs inscrite dans le dictionnaire XL Deploy.
  • Les deux éléments <scanPlaceholders> et <preScannedPlaceholders> permettent d’autoriser l’analyse des fichiers présents dans le .dar pour y trouver les variables XLD.

Jenkins

Jenkins est l’outil de CI qui nous permet de créer les .dar à exporter sur XLD. Comme dit précédemment, ces .dar sont en fait des zip ce qui nous permet de les créer facilement. Afin d’avoir une bonne flexibilité dans la création des builds et la gestion des branches de notre dépôt GIT, nous allons utiliser la fonctionnalité Multibranch Pipeline plutôt qu’un projet free-style.

La Multribranch Pipeline va scanner la liste des branches du dépôt, chercher un fichier nommé Jenkinsfile et lorsqu’elle en trouve un, lancer un build d’après les instructions incluses dans ce fichier. S’il n’y pas de Jenkinsfile, Jenkins verra quand même la branche mais ne la prendra pas en compte. L’autre avantage du Jenkinsfile, c’est qu’il permet de déporter la configuration du pipeline depuis la console Jenkins vers un fichier avec le reste du code.

Le plugin Jenkins pour XL Deploy supporte trois commandes :

  1. xldCreatePackage Création du .dar
  2. xldPublishPackage Publication du .dar sur XLD
  3. xldDeploy  Déploiement vers l’environnement sélectionné

Voici un exemple simple de jenkinsfile pour un déploiement automatisé via le module XL Deploy pour Jenkins. Ce fichier nous emmène de la récupération de la dernière version disponible sur une branche git jusqu’à son exécution dans le cloud provider configuré pour notre Terraform.

 node {
        stage('Checkout') { 
            git url: 'https://url_depotgit/projet/terraform.git', 
            branch: env.BRANCH_NAME
        }
        stage('Compression') {
            sh ```
                zip -r package-terraform.zip *.tf *tfvars *.rc modules
            ``` 
        }
        stage('Create .dar') { 
            xldCreatePackage artifactsPath: '', manifestPath: 'deployit-manifest.xml', darPath: '${BUILD_NUMBER}.dar'
        } 
        stage('Publish to xld') { 
            xldPublishPackage serverCredentials: 'xldeploy-cred', darPath: '${BUILD_NUMBER}.dar'
        }
        stage('Deploy') {
            when {
                expression {
                    return env.BRANCH_NAME != 'master';
                }
            }
            steps {
                xldDeploy serverCredentials: 'xldeploy-cred', environmentId: 'Environments/${env.BRANCH_NAME}', packageId: 'Applications/<project_name>/${BUILD_NUMBER}'
            }
        }    
        stage('Cleaning workspace') { 
            cleanws() 
        }
    }

Etape 1 : Checkout

stage('Checkout') { 
            git url: 'https://url_depotgit/projet/terraform.git', 
            branch: env.BRANCH_NAME
        }

branch est une option facultative qui permet d’indiquer à Jenkins la branche à récupérer. En l’absence de cette option, il s’agira de la branche Master. Ici, il s’agit d’une variable d’environnement Jenkins qui prend pour valeur le nom de la branche que jenkins récupère. Par exemple :

  1. Jenkins voit une branche nommée develop dans le dépôt Git
  2. jenkins lit le fichier Jenkinsfile et ajoute la branche à la liste des branches surveillées

La variable d’environnement env.BRANCH_NAME prend la valeur develop lors des builds de cette branche.

Etape 2 : Compression

stage('Compression') {
            sh ```
                zip -r package-terraform.zip *.tf *tfvars *.rc modules
            ``` 
        }

Création d’un package avec les fichiers et dossiers présents à la racine du workspace Jenkins. On crée un fichier zip qui sera transformé en .dar à l’étape suivante. le nom du zip, package-terraform.zip est celle utilisée par terraform.Module dans le deployit-manifest.xml.

Etape 3 : création du package XL Deploy

stage('Create .dar') { 
            xldCreatePackage artifactsPath: '', manifestPath: 'deployit-manifest.xml', darPath: '${BUILD_NUMBER}.dar'
        }

  1. artifactsPath chemin vers le fichier zip créé précédemment. Comme il se situe à la racine du workspace jenkins, la valeur est vide. On pourrait penser par habitude qu’il faille écrire ‘.’ ou ‘/’ pour indiquer le dossier actuel, ce n’est pas le cas car XL Deploy concatène tous les caractères entre apostrophe pour le nom de l’artifact : si le fichier zip est à la racine du workspace, le path doit être vide.
  2. manifestPath chemin vers le manifest XLD. Comme il se trouve à la racine du workspace, le chemin relatif est simplement composé du nom du fichier
  3. darPath chemin et nom où doit être créé le .dar qui sera envoyé vers XLD. Ici on le nomme simplement d’après le numéro de build avec une variable d’environnement jenkins : le workspace sera nettoyé à la fin de l’exécution de la pipeline

Etape 4 : publication du package sur XL Deploy

stage('Publish to xld') { 
            xldPublishPackage serverCredentials: 'xldeploy-cred', darPath: '${BUILD_NUMBER}.dar'
        }

  1. serverCredentials les identifiants permettant d’accéder à XLD..
  2. darPath chemin vers le .dar créé à l’étape précédente

Etape 5 : déploiement du package par XL Deploy

  stage('Deploy') {
            when {
                expression {
                    return env.BRANCH_NAME != 'master';
                }
            }
            steps {
                xldDeploy serverCredentials: 'xldeploy-cred', environmentId: 'Environments/${env.BRANCH_NAME}', packageId: 'Applications/<project_name>/${BUILD_NUMBER}'
            }
        }    

Cette étape comporte plus de lignes que les précédentes mais sa lecture est en fait plus simple qu’il n’y parait. Nous avons à son début une condition :

  1. Si la branche récupérée au début de la pipeline Jenkins n’est pas la branche master, alors le package sera automatiquement déployé dans l’environnement qui le concerne : ‘Environments/${env.BRANCH_NAME}’.
  2. S’il s’agit de la branch master alors cette étape est ignorée.

L’idée derrière cette étape conditionnelle est de conserver un contrôle sur qui va en production. On peut considérer que la branche master est toujours censée avoir un code fonctionnel et prêt pour l’environnement final mais comme garde-fou, on laisse le déploiement final de l’infrastructure de production à un humain. Si votre branche master a un autre nom (“production” par exemple), la condition est à modifier pour le prendre en compte.

Etape 6 : Nettoyage du workspace Jenkins

stage('Cleaning workspace') { 
            cleanws() 
        }

Etape finale, nettoyage du workspace et suppression de tous éléments créés précédemment.

Conclusion

Dans cet article, nous avons vu comment l’usage d’une pipeline Intégration Continue / Livraison Continue (CI/CD en anglais) combiné à Terraform (Infrastructure as Code) permet de simplifier les déploiements d’une infrastructure AWS :

  1. Git :

    1. Versionner chaque changement et donc de garder un historique de l’infrastructure
    2. L’usage de branche permet de séparer le code de développement du code utilisé en production
  2. Jenkins (le CI) crée des packages contenant tout le le code et ses variables qui sont prêt pour les livraisons.
  3. XL Deploy (le CD) via son plugin Jenkins permet de livrer manuellement ces packages ou par le jenkinsfile de les livrer automatiquement
  4. Terraform via le plugin XL Deploy exécute le code pour son déploiement vers le cloud provider choisi.

L’objectif de cette pipeline est donc de limiter le plus possible les interactions humaines lors du déploiement et par conséquent le risque opérationnel des erreurs humaines.  Après le git push qui envoie le code sur le repository, la chaîne se met en route et quelques minutes plus tard, l’infrastructure sera déployée / mise à jour.