Blog Ingénierie Déployer en continu dans de multiples environnements avec les pipelines enfants
Mise à jour : January 9, 2025
Lecture : 32 min

Déployer en continu dans de multiples environnements avec les pipelines enfants

Découvrez comment créer un workflow rationalisé dans GitLab pour gérer le déploiement continu dans de multiples environnements.

pipeline-abstract-cover

Les équipes DevSecOps doivent parfois coordonner le déploiement continu dans des environnements différents, tout en préservant leurs workflows. La plateforme DevSecOps de GitLab répond à ce besoin, de manière simple et efficace, y compris avec des environnements sandbox temporaires créés à la demande. Découvrez dans cet article un exemple de mise en œuvre de ce processus en déployant une architecture avec Terraform sur plusieurs environnements cibles.

Cette stratégie s'adapte facilement qu'il s'agisse d'un projet d'Infrastructure as Code (IaC) utilisant une autre technologie comme Pulumi ou Ansible, d'un projet de code source ou d'un projet de dépôt monolithique combinant plusieurs langages.

À la fin de ce tutoriel, le pipeline que vous aurez créé permettra de déployer :

  • Un environnement temporaire de développement pour chaque branche de fonctionnalité.
  • Un environnement d'intégration, qu'il est facile de supprimer et redéployer à partir de la branche principale.
  • Un environnement d'assurance qualité (QA), également déployé à partir de la branche principale, pour exécuter les étapes de QA.
  • Un environnement de préproduction pour chaque tag, dernière étape avant la phase de production.
  • Un environnement de production qui dans cet exemple sera déployé manuellement, mais qui peut également être déployé en continu.

Légende des schémas figurant dans cet article :

  • Les encarts aux angles arrondis représentent les branches de GitLab.
  • Les encarts rectangulaires représentent les environnements.
  • Le texte sur les flèches représente les actions requises pour passer d'un encart à l'autre.
  • Les encarts carrés représentent une prise de décision.
flowchart LR
    A(main) -->|new feature| B(feature_X)

    B -->|auto deploy| C[review/feature_X]
    B -->|merge| D(main)
    C -->|destroy| D

    D -->|auto deploy| E[integration]
    E -->|manual| F[qa]

    D -->|tag| G(X.Y.Z)
    F -->|validate| G

    G -->|auto deploy| H[staging]
    H -->|manual| I{plan}
    I -->|manual| J[production]

Nous allons vous expliquer les raisons des actions présentées dans le flowchart ci-dessus, ainsi que les étapes à suivre pour chacune d'elles. Ce tutoriel sera ainsi plus facile à suivre et vous pourrez le reproduire sans difficulté.

Raisons

  • L'intégration continue (CI) constitue quasiment une norme établie. La plupart des entreprises implémentent des pipelines CI ou cherchent à standardiser leurs pratiques.

  • La livraison continue (CD), qui consiste à effectuer la publication des artefacts vers un dépôt ou un registre à la fin du pipeline CI, est également courante.

  • L'étape suivante, le déploiement continu, qui automatise le déploiement de ces artefacts, est en revanche moins répandu. Il est essentiellement implémenté dans le domaine des applications. Le déploiement continu d'une infrastructure est plus compliqué et implique la gestion de plusieurs environnements. Tester, sécuriser et vérifier le code de l'infrastructure constitue un défi supplémentaire et c'est un domaine où le processus DevOps n'a pas encore atteint sa pleine maturité. L'intégration de la sécurité en amont, qui nécessite l'implication des équipes de sécurité, représente également une difficulté. Et il est très important de prendre en compte les problèmes de sécurité dès les premières étapes du développement, afin de passer d'une approche DevOps à un processus DevSecOps.

Ce tutoriel vous invite à tester une méthode simple et efficace pour adopter une approche DevSecOps pour votre infrastructure. Nous prendrons l'exemple du déploiement de ressources dans cinq environnements, du développement à la production.

Remarque : même si je préconise l'adoption d'une approche FinOps et la réduction du nombre d'environnements, il existe parfois d'excellentes raisons de ne pas se limiter aux simples étapes de développement, préproduction et production. N'hésitez pas à adapter les exemples en fonction de vos besoins.

Actions

L’avènement du cloud a boosté l'utilisation de l'IaC. Ansible et Terraform ont ouvert la voie, suivis par OpenTofu, Pulumi, AWS CDK, Google Deploy Manager et bien d'autres.

Une Infrastructure as Code est la solution parfaite pour déployer une infrastructure de manière sécurisée. Vous pouvez la tester, la déployer et la réappliquer autant de fois que nécessaire pour atteindre votre objectif.

Malheureusement, les entreprises maintiennent souvent plusieurs branches, voire de multiples dépôts, pour chacun de leurs environnements cibles, ce qui crée des problèmes. Elles ne respectent plus un processus rigoureux. Elles ne s'assurent plus que chaque modification du code en production a été soigneusement testée dans les environnements précédents. Par conséquent, des décalages apparaissent peu à peu d'un environnement à l'autre.

J'ai réalisé que ce tutoriel était nécessaire lors d'une conférence à laquelle j'ai assisté : tous les participants ont déclaré que leur workflow n'imposait des tests rigoureux de l'infrastructure qu'avant le déploiement en production. Et ils ont tous convenu qu'ils appliquaient parfois des correctifs directement en production. Bien sûr, cette démarche permet d'aller vite, mais est-elle sûre ? Comment reporter les correctifs sur les environnements précédents ? Comment vérifier qu'il n'y a pas d'effets de bord ? Comment limiter les risques auxquels votre entreprise est exposée lorsque vous déployez votre code trop rapidement en production ?

La question essentielle est de savoir pourquoi les équipes DevOps déploient directement en production. Le pipeline devrait-il être plus efficace ou plus rapide ? N'est-il pas possible d'automatiser le processus ? Ou, pire encore, n'y a-t-il aucun moyen de tester le code en dehors de l'environnement de production ?

Dans la section suivante, vous apprendrez à automatiser votre infrastructure et à garantir que votre équipe DevOps mène des tests efficaces avant d'effectuer un push vers un environnement qui affectera le reste du processus. Vous verrez comment sécuriser votre code et contrôler son déploiement de bout en bout.

Les étapes à suivre

Comme mentionné précédemment, de nombreux langages permettent actuellement de gérer l'IaC et nous ne pouvons pas tous les aborder ici. Je vais m'appuyer sur un code Terraform version 1.4. Ne prêtez pas attention au langage utilisé pour gérer l'IaC, mais plutôt au processus transposable à votre écosystème.

Le code Terraform

Commençons par un code Terraform de base.

Nous allons déployer sur AWS, un cloud privé virtuel (VPC), qui est un réseau virtuel. Dans ce VPC, nous déploierons un sous-réseau public et un sous-réseau privé. Comme leur nom l'indique, il s'agit de sous-réseaux du VPC principal. Enfin, nous ajouterons une instance Elastic Cloud Compute (EC2) (une machine virtuelle) dans le sous-réseau public.

Nous allons ainsi déployer quatre ressources de manière relativement simple. L'idée est de se concentrer sur le pipeline, et non sur le code.

Voici la cible que nous voulons atteindre pour votre dépôt.

cible du dépôt

Décomposons le processus.

Tout d'abord, nous déclarons toutes les ressources dans un fichier terraform/main.tf :

provider "aws" {
  region = var.aws_default_region
}

resource "aws_vpc" "main" {
  cidr_block = var.aws_vpc_cidr

  tags = {
    Name     = var.aws_resources_name
  }
}

resource "aws_subnet" "public_subnet" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.aws_public_subnet_cidr

  tags = {
    Name = "Public Subnet"
  }
}
resource "aws_subnet" "private_subnet" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.aws_private_subnet_cidr

  tags = {
    Name = "Private Subnet"
  }
}

resource "aws_instance" "sandbox" {
  ami           = var.aws_ami_id
  instance_type = var.aws_instance_type

  subnet_id = aws_subnet.public_subnet.id

  tags = {
    Name     = var.aws_resources_name
  }
}

Comme vous pouvez le constater, ce code nécessite plusieurs variables. Nous les déclarons dans un fichier terraform/variables.tf :

variable "aws_ami_id" {
  description = "The AMI ID of the image being deployed."
  type        = string
}

variable "aws_instance_type" {
  description = "The instance type of the VM being deployed."
  type        = string
  default     = "t2.micro"
}

variable "aws_vpc_cidr" {
  description = "The CIDR of the VPC."
  type        = string
  default     = "10.0.0.0/16"
}

variable "aws_public_subnet_cidr" {
  description = "The CIDR of the public subnet."
  type        = string
  default     = "10.0.1.0/24"
}

variable "aws_private_subnet_cidr" {
  description = "The CIDR of the private subnet."
  type        = string
  default     = "10.0.2.0/24"
}

variable "aws_default_region" {
  description = "Default region where resources are deployed."
  type        = string
  default     = "eu-west-3"
}

variable "aws_resources_name" {
  description = "Default name for the resources."
  type        = string
  default     = "demo"
}

À ce stade, nous avons presque terminé la partie IaC. Il nous manque simplement une méthode pour partager les états Terraform. Si vous l'ignorez, Terraform fonctionne schématiquement comme suit :

  • La commande plan vérifie les différences entre l'infrastructure actuelle et celle définie dans le code. Elle génère ensuite un rapport des différences.
  • La commande apply exécute les modifications en fonction du rapport plan et met à jour l'état.

Lors du premier passage, l'état est vide. Il comporte ensuite les détails (ID, etc.) des ressources appliquées par Terraform.

Le problème est le suivant : où cet état est-il stocké ? Comment le partager pour permettre à plusieurs développeurs et développeuses de collaborer sur le code ?

La solution est assez simple : stockez et partagez l'état dans GitLab via un backend HTTP Terraform.

Lorsque vous utilisez ce backend, la première étape consiste à créer le fichier terraform/backend.tf le plus simple qui soit. La deuxième étape est prise en charge dans le pipeline.

terraform {
  backend "http" {
  }
}

Et voilà ! Nous disposons maintenant d'un code Terraform minimaliste pour déployer ces quatre ressources. Nous renseignerons les valeurs des variables lors de l'exécution à une étape ultérieure.

Le workflow

Mettons en œuvre le workflow suivant :

flowchart LR
    A(main) -->|new feature| B(feature_X)

    B -->|auto deploy| C[review/feature_X]
    B -->|merge| D(main)
    C -->|destroy| D

    D -->|auto deploy| E[integration]
    E -->|manual| F[qa]

    D -->|tag| G(X.Y.Z)
    F -->|validate| G

    G -->|auto deploy| H[staging]
    H -->|manual| I{plan}
    I -->|manual| J[production]
  1. Créez une branche de fonctionnalité. Elle exécute tous les scanners en continu sur le code pour s'assurer qu'il est toujours conforme et sécurisé. Ce code est déployé en continu dans un environnement temporaire review/feature_branch portant le nom de la branche actuelle. Il s'agit d'un environnement sûr où les équipes de développement et d'opérations peuvent tester leur code sans impact sur le reste du Système d’Information (SI). Le processus, comme les revues de code et l'exécution de scanners, est imposé à cette étape pour assurer que la qualité et la sécurité du code sont suffisantes et ne mettent pas votre SI en danger. L'infrastructure déployée par cette branche est automatiquement détruite lorsque la branche est fermée. Vous pouvez ainsi contrôler votre budget.
flowchart LR
    A(main) -->|new feature| B(feature_X)

    B -->|auto deploy| C[review/feature_X]
    B -->|merge| D(main)
    C -->|destroy| D
  1. Une fois approuvée, la branche de fonctionnalité est fusionnée dans la branche principale. Il s'agit d'une branche protégée où aucun push ne peut être effectué directement. Elle est nécessaire pour veiller à ce que chaque demande de modification de l'environnement de production soit minutieusement testée. Cette branche est également déployée en continu. La cible ici est l'environnement integration. La suppression de cet environnement n'est pas automatisée pour des questions de stabilité, mais elle peut être déclenchée manuellement.
flowchart LR
    D(main) -->|auto deploy| E[integration]
  1. Une approbation manuelle est ensuite nécessaire pour déclencher le déploiement suivant. La branche principale sera déployée dans l'environnement qa. J'ai défini une règle ici pour empêcher la suppression depuis le pipeline. Cet environnement devrait être assez stable (après tout, c'est déjà le troisième) et je souhaite éviter une suppression accidentelle. N'hésitez pas à adapter les règles à vos processus.
flowchart LR
    D(main)-->|auto deploy| E[integration]
    E -->|manual| F[qa]
  1. Pour continuer, nous devons ajouter un tag au code. Nous utilisons les tags protégés pour que seul un ensemble spécifique d'utilisateurs ait l'autorisation de déployer dans ces deux derniers environnements. Ce tag va immédiatement déclencher un déploiement dans l'environnement staging.
flowchart LR
    D(main) -->|tag| G(X.Y.Z)
    F[qa] -->|validate| G

    G -->|auto deploy| H[staging]
  1. Nous arrivons enfin à l'environnement production. Il est souvent difficile de déployer l'infrastructure progressivement (10 %, 25 %, etc.). Nous la déployons donc dans son intégralité. Nous contrôlons toutefois ce déploiement à l'aide d'un déclencheur manuel intégré dans cette dernière étape. Afin de garder un contrôle maximal sur cet environnement hautement critique, nous le contrôlons en tant qu'environnement protégé.
flowchart LR
    H[staging] -->|manual| I{plan}
    I -->|manual| J[production]

Le pipeline

Pour mettre en œuvre le workflow ci-dessus, nous allons maintenant implémenter un pipeline comportant deux pipelines enfants.

Le pipeline principal

Commençons par le pipeline principal. Il est déclenché automatiquement par un push effectué vers une branche de fonctionnalité, une fusion vers la branche par défaut ou un tag. Il effectue un vrai déploiement continu dans les environnements suivants : dev, integration et staging. Il est déclaré dans le fichier .gitlab-ci.yml à la racine de votre projet.

la cible du dépôt

stages:
  - test
  - environments

.environment:
  stage: environments
  variables:
    TF_ROOT: terraform
    TF_CLI_ARGS_plan: "-var-file=../vars/$variables_file.tfvars"
  trigger:
    include: .gitlab-ci/.first-layer.gitlab-ci.yml
    strategy: depend            # Wait for the triggered pipeline to successfully complete
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

review:
  extends: .environment
  variables:
    environment: review/$CI_COMMIT_REF_SLUG
    TF_STATE_NAME: $CI_COMMIT_REF_SLUG
    variables_file: review
    TF_VAR_aws_resources_name: $CI_COMMIT_REF_SLUG  # Used in the tag Name of the resources deployed, to easily differenciate them
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH

integration:
  extends: .environment
  variables:
    environment: integration
    TF_STATE_NAME: $environment
    variables_file: $environment
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

staging:
  extends: .environment
  variables:
    environment: staging
    TF_STATE_NAME: $environment
    variables_file: $environment
  rules:
    - if: $CI_COMMIT_TAG

#### TWEAK
# This tweak is needed to display vulnerability results in the merge widgets.
# As soon as this issue https://gitlab.com/gitlab-org/gitlab/-/issues/439700 is resolved, the `include` instruction below can be removed.
# Until then, the SAST IaC scanners will run in the downstream pipelines, but their results will not be available directly in the merge request widget, making it harder to track them.
# Note: This workaround is perfectly safe and will not slow down your pipeline.
include:
  - template: Security/SAST-IaC.gitlab-ci.yml
#### END TWEAK

Ce pipeline n'exécute que deux étapes : test et environments. La première est nécessaire pour que le TWEAK exécute les scanners. La seconde déclenche un pipeline enfant contenant un ensemble de variables différent pour chaque cas défini ci-dessus (push vers la branche, fusion dans la branche par défaut ou tag).

Nous ajoutons ici une dépendance avec le mot-clé strategy:depend sur notre pipeline enfant afin que la vue du pipeline dans GitLab ne soit mise à jour qu'une fois le déploiement terminé.

Comme vous le voyez, nous définissons un job de base masqué, puis nous ajoutons des variables et des règles spécifiques afin de déclencher un seul déploiement pour chaque environnement cible.

Outre les variables prédéfinies, nous utilisons deux nouveaux éléments que nous devons définir :

  1. Les variables spécifiques à chaque environnement : ../vars/$variables_file.tfvars
  2. Le pipeline enfant, défini dans .gitlab-ci/.first-layer.gitlab-ci.yml

Commençons par le plus rapide, les définitions des variables.

Les définitions des variables

Nous allons ici mélanger deux solutions pour fournir des variables à Terraform :

  • La première utilise des fichiers .tfvars pour tous les intrants ne contenant pas de données sensibles, qui doivent être stockées dans GitLab.

solution 1 pour fournir des variables à Terraform

  • La seconde utilise des variables d'environnement avec le préfixe TF_VAR. Combinée à la capacité de GitLab à masquer les variables, à les protéger et à les rendre accessibles uniquement pour certains environnements, cette deuxième façon d'injecter des variables est une solution puissante pour empêcher les fuites d'informations contenant des données sensibles. Par exemple, si vous considérez que le routage CIDR privé de votre environnement de production est une donnée sensible, vous pouvez le protéger de cette manière. Veillez à ce qu'il ne soit disponible que pour l'environnement production, pour les pipelines fonctionnant avec des branches et des tags protégés, et que sa valeur soit masquée dans les journaux du job.

solution 2 pour fournir des variables à Terraform

De plus, chaque fichier de variables doit être contrôlé via un fichier CODEOWNERS où sont définies les personnes ayant l'autorisation d'apporter des modifications.

[Production owners] 
vars/production.tfvars @operations-group

[Staging owners]
vars/staging.tfvars @odupre @operations-group

[CodeOwners owners]
CODEOWNERS @odupre

Cet article n'a pas pour but d'expliquer Terraform, nous allons donc simplement montrer le fichier vars/review.tfvars. Les fichiers d'environnement suivants sont, bien sûr, très similaires. Il suffit de définir les variables ne contenant pas de données sensibles et leurs valeurs ici.

aws_vpc_cidr = "10.1.0.0/16"
aws_public_subnet_cidr = "10.1.1.0/24"
aws_private_subnet_cidr = "10.1.2.0/24"

Le pipeline enfant

C'est dans ce pipeline que le travail concret est effectué. Il est donc un peu plus complexe que le premier. Mais rien qu'on ne puisse surmonter ensemble !

Comme nous l'avons vu dans la définition du pipeline principal, ce pipeline enfant est déclaré dans le fichier .gitlab-ci/.first-layer.gitlab-ci.yml.

Pipeline downstream déclaré dans le fichier

Décomposons-le en petites étapes avant de revenir à une vue d'ensemble.

Exécution des commandes Terraform et sécurisation du code

Nous allons d'abord mettre en place un pipeline pour Terraform. GitLab est une plateforme open source tout comme notre template de pipeline pour Terraform. Il vous suffit de l'inclure, en utilisant l'extrait de code suivant :

include:
  - template: Terraform.gitlab-ci.yml

Ce template exécute les vérifications Terraform sur le formatage et valide votre code, avant de le planifier et de l'appliquer. Il vous permet également de détruire ce que vous avez déployé.

En tant que plateforme DevSecOps unifiée, GitLab intègre deux scanners de sécurité directement dans ce template afin de détecter les menaces potentielles dans votre code et de vous avertir avant tout déploiement dans les environnements suivants.

Maintenant que nous avons vérifié, sécurisé, compilé et déployé notre code, explorons quelques astuces supplémentaires.

Partage du cache entre les jobs

Pour réutiliser les résultats des jobs dans les étapes suivantes du pipeline, nous allons activer la mise en cache. Il suffit d'ajouter le code suivant :

default:
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: cache-$CI_COMMIT_REF_SLUG
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .

Nous définissons ici un cache différent pour chaque commit, en revenant au nom de la branche principale si nécessaire.

En regardant de près les templates utilisés, on observe qu’ils contiennent des règles contrôlant l’exécution des jobs. Nous voulons exécuter tous les contrôles (assurance qualité et sécurité) sur toutes les branches. Nous allons donc personnaliser ces paramètres.

Exécution des contrôles sur toutes les branches

Les templates GitLab offrent une fonctionnalité puissante permettant de modifier uniquement certaines parties d’un template. Nous souhaitons seulement remplacer les règles de certains jobs afin de toujours exécuter des contrôles d'assurance qualité et de sécurité. Les autres paramètres de ces jobs resteront conformes au template.

fmt:
  rules:
    - when: always

validate:
  rules:
    - when: always

kics-iac-sast:
  rules:
    - when: always

iac-sast:
  rules:
    - when: always

Maintenant que nous avons appliqué les contrôles d'assurance qualité et de sécurité, nous voulons différencier le comportement des environnements principaux (intégration et préproduction) dans le workflow par rapport aux environnements de revue. Commençons par définir le comportement des environnements principaux. Nous modifierons ensuite cette configuration pour les environnements de revue.

Pipeline CD pour l'intégration et la préproduction

Comme indiqué, nous voulons déployer la branche principale et les tags dans ces deux environnements. Nous ajoutons des règles pour contrôler ce déploiement sur les jobs build et deploy. Ensuite, nous activons la fonction destroy uniquement pour integration, car l'environnement staging est trop critique pour être supprimé en un seul clic. Les erreurs sont possibles et nous souhaitons les éviter.

Enfin, nous relions le job deploy au job destroy, afin de pouvoir déclencher un stop sur l'environnement directement à partir de l'interface utilisateur graphique de GitLab.

GIT_STRATEGY positionné à none empêche la récupération du code de la branche source dans le runner lors de la destruction. L'opération échouerait si la branche avait été supprimée manuellement. Nous comptons donc sur le cache pour obtenir tout ce dont nous avons besoin pour exécuter les instructions Terraform.

build:  # terraform plan
  environment:
    name: $TF_STATE_NAME
    action: prepare
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

deploy: # terraform apply --> automatically deploy on corresponding env (integration or staging) when merging to default branch or tagging. Second layer environments (qa and production) will be controlled manually
  environment: 
    name: $TF_STATE_NAME
    action: start
    on_stop: destroy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

destroy:
  extends: .terraform:destroy
  variables:
    GIT_STRATEGY: none
  dependencies:
    - build
  environment:
    name: $TF_STATE_NAME
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $TF_DESTROY == "true" # Manually destroy integration env.
      when: manual

Comme indiqué, cela nous permet de déployer sur les environnements integration et staging. Mais il manque toujours un environnement temporaire où les développeurs et développeuses peuvent expérimenter et valider leur code sans affecter le travail des autres. C'est tout l'intérêt du déploiement dans l'environnement review.

Pipeline CD pour les environnements de revue

Le déploiement dans l'environnement de revue n'est pas très différent du déploiement dans les environnements integration et staging. Nous allons une fois de plus tirer parti de la capacité de GitLab à remplacer uniquement des éléments de définition de job.

Tout d'abord, nous définissons des règles pour exécuter ces jobs uniquement sur les branches de fonctionnalités.

Ensuite, nous relions le job deploy_review à destroy_review. Nous pouvons ainsi arrêter l'environnement manuellement à partir de l'interface utilisateur de GitLab. Plus important encore, ce job déclenche automatiquement la destruction de l'environnement lorsque la branche de fonctionnalité est fermée. Cette bonne pratique FinOps vous aide à contrôler vos dépenses opérationnelles.

Puisque Terraform a besoin d'un fichier de plan pour la destruction d'une infrastructure, comme pour la compilation, nous ajoutons une dépendance de destroy_review à build_review afin de récupérer ses artefacts.

Enfin, nous voyons ici que le nom de l'environnement est $environment. Il a été défini sur review/$CI_COMMIT_REF_SLUG dans le pipeline principal et transmis à ce pipeline enfant avec l'instruction trigger:forward:yaml_variables:true.

build_review:
  extends: build
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

deploy_review:
  extends: deploy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: start
    on_stop: destroy_review
    # url: https://$CI_ENVIRONMENT_SLUG.example.com
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

destroy_review:
  extends: destroy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # Do not destroy staging
      when: never
    - when: manual

Pour récapituler, nous avons maintenant un pipeline qui peut :

  • Déployer des environnements de revue temporaires, qui sont automatiquement détruits lorsque la branche de fonctionnalité est fermée
  • Déployer en continu la branche par défaut sur integration
  • Déployer en continu les tags sur l'environnement staging

Ajoutons maintenant un niveau supplémentaire, où nous allons déployer sur les environnements qa et production avec un déclencheur manuel.

Pipeline CD pour l'assurance qualité et la production

Comme tout le monde n'est pas prêt à effectuer des déploiements continus en production, nous ajoutons une validation manuelle pour les deux prochains déploiements. À strictement parler, nous ne devrions pas ajouter ce déclencheur dans un processus CD, mais profitons de cette occasion pour vous apprendre à exécuter des jobs à partir d'autres déclencheurs.

Jusqu'à présent, nous avons lancé un pipeline enfant à partir du pipeline principal pour exécuter tous les déploiements.

Comme nous voulons exécuter d'autres déploiements à partir de la branche par défaut et des tags, nous ajoutons un nouveau niveau pour ces étapes supplémentaires. Rien de bien nouveau. Nous allons répéter le processus utilisé pour le pipeline principal. En procédant de cette façon, vous pouvez manipuler autant de niveaux que vous le souhaitez. J'ai déjà vu jusqu'à neuf environnements.

Sans revenir sur les avantages d'un nombre limité d'environnements, le processus que nous utilisons ici permet d'implémenter très facilement le même pipeline, de la phase initiale jusqu’à la livraison finale, tout en gardant la définition de votre pipeline simple et divisée en petits segments que vous pouvez maintenir facilement.

Pour éviter les conflits de variables, nous utilisons simplement de nouveaux noms pour identifier l'état Terraform et le fichier d'intrant.

.2nd_layer:
  stage: 2nd_layer
  variables:
    TF_ROOT: terraform
  trigger:
    include: .gitlab-ci/.second-layer.gitlab-ci.yml
    # strategy: depend            # Do NOT wait for the downstream pipeline to finish to mark upstream pipeline as successful. Otherwise, all pipelines will fail when reaching the pipeline timeout before deployment to 2nd layer.
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

qa:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: qa
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

production:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: production
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_TAG

Un point important ici est la stratégie utilisée pour le nouveau pipeline enfant. Nous maintenons la valeur par défaut du déclencheur trigger:strategy. Dans le cas contraire, le pipeline principal attend la fin de votre pipeline de niveau « petit-enfant ». Si vous utilisez un déclencheur manuel, cette opération peut prendre beaucoup de temps et rendre votre tableau de bord de pipeline plus difficile à lire et à comprendre.

Vous vous demandez probablement ce que contient le fichier .gitlab-ci/.second-layer.gitlab-ci.yml qui est inclus ici. Nous aborderons cette question dans la section suivante.

Le premier niveau complet de définition de pipeline

Si vous recherchez une vue complète de ce premier niveau (stocké dans .gitlab-ci/.first-layer.gitlab-ci.yml), consultez la section suivante.

variables:
  TF_VAR_aws_ami_id: $AWS_AMI_ID
  TF_VAR_aws_instance_type: $AWS_INSTANCE_TYPE
  TF_VAR_aws_default_region: $AWS_DEFAULT_REGION

include:
  - template: Terraform.gitlab-ci.yml

default:
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: cache-$CI_COMMIT_REF_SLUG
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .

stages:
  - validate
  - test
  - build
  - deploy
  - cleanup
  - 2nd_layer       # Use to deploy a 2nd environment on both the main branch and on the tags

fmt:
  rules:
    - when: always

validate:
  rules:
    - when: always

kics-iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: on_success

iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: on_success

###########################################################################################################
## Integration env. and Staging. env
##  * Auto-deploy to Integration on merge to main.
##  * Auto-deploy to Staging on tag.
##  * Integration can be manually destroyed if TF_DESTROY is set to true.
##  * Destroy of next env. is not automated to prevent errors.
###########################################################################################################
build:  # terraform plan
  environment:
    name: $TF_STATE_NAME
    action: prepare
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

deploy: # terraform apply --> automatically deploy on corresponding env (integration or staging) when merging to default branch or tagging. Second layer environments (qa and production) will be controlled manually
  environment: 
    name: $TF_STATE_NAME
    action: start
    on_stop: destroy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

destroy:
  extends: .terraform:destroy
  variables:
    GIT_STRATEGY: none
  dependencies:
    - build
  environment:
    name: $TF_STATE_NAME
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $TF_DESTROY == "true" # Manually destroy integration env.
      when: manual
###########################################################################################################

###########################################################################################################
## Dev env.
##  * Temporary environment. Lives and dies with the Merge Request.
##  * Auto-deploy on push to feature branch.
##  * Auto-destroy on when Merge Request is closed.
###########################################################################################################
build_review:
  extends: build
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

deploy_review:
  extends: deploy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: start
    on_stop: destroy_review
    # url: https://$CI_ENVIRONMENT_SLUG.example.com
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

destroy_review:
  extends: destroy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # Do not destroy staging
      when: never
    - when: manual
###########################################################################################################

###########################################################################################################
## Second layer
##  * Deploys from main branch to qa env.
##  * Deploys from tag to production.
###########################################################################################################
.2nd_layer:
  stage: 2nd_layer
  variables:
    TF_ROOT: terraform
  trigger:
    include: .gitlab-ci/.second-layer.gitlab-ci.yml
    # strategy: depend            # Do NOT wait for the downstream pipeline to finish to mark upstream pipeline as successful. Otherwise, all pipelines will fail when reaching the pipeline timeout before deployment to 2nd layer.
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

qa:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: qa
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

production:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: production
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_TAG
###########################################################################################################

À cette étape, nous avons déjà effectué des déploiements vers trois environnements en toute sécurité. Je trouve personnellement que cette démarche est idéale. Cependant, si vous avez besoin d'autres environnements, ajoutez-les à votre pipeline CD.

Vous avez sûrement remarqué que nous incluons un pipeline enfant avec le mot-clé trigger:include. Il inclut le fichier .gitlab-ci/.second-layer.gitlab-ci.yml. Nous souhaitons exécuter un pipeline très similaire, son contenu ressemble donc évidemment beaucoup à celui présenté ci-dessus. Le principal avantage de ce pipeline de niveau « petit-enfant » est qu'il existe par lui-même, ce qui facilite la définition des variables et des règles.

Le pipeline de niveau « petit-enfant »

Ce pipeline de deuxième couche est tout nouveau. Par conséquent, il doit imiter la définition de la première couche, à savoir :

Comme expliqué ci-dessus, les variables TF_STATE_NAME et TF_CLI_ARGS_plan sont copiées du pipeline principal au pipeline enfant. Nous avions besoin d'un nom de variable différent pour transférer ces valeurs du pipeline enfant au pipeline « petit-enfant ». C'est pourquoi, dans le pipeline enfant, les noms de ces variables incluent le suffixe _2. La valeur est ensuite copiée dans la variable correspondante appropriée lors de l'exécution de la section before_script.

Comme nous avons déjà décomposé chaque étape, nous pouvons passer directement à la vue d'ensemble du deuxième niveau (codé dans .gitlab-ci/.second-layer.gitlab-ci.yml).

# Use to deploy a second environment on both the default branch and the tags.

include:
  template: Terraform.gitlab-ci.yml

stages:
  - validate
  - test
  - build
  - deploy

fmt:
  rules:
    - when: never

validate:
  rules:
    - when: never

kics-iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: always

###########################################################################################################
## QA env. and Prod. env
##  * Manually trigger build and auto-deploy in QA
##  * Manually trigger both build and deploy in Production
##  * Destroy of these env. is not automated to prevent errors.
###########################################################################################################
build:  # terraform plan
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: $TF_STATE_NAME_2
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .
  environment:
    name: $TF_STATE_NAME_2
    action: prepare
  before_script:  # Hack to set new variable values on the second layer, while still using the same variable names. Otherwise, due to variable precedence order, setting new value in the trigger job, does not cascade these new values to the downstream pipeline
    - TF_STATE_NAME=$TF_STATE_NAME_2
    - TF_CLI_ARGS_plan=$TF_CLI_ARGS_plan_2
  rules:
    - when: manual

deploy: # terraform apply
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: $TF_STATE_NAME_2
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .
  environment: 
    name: $TF_STATE_NAME_2
    action: start
  before_script:  # Hack to set new variable values on the second layer, while still using the same variable names. Otherwise, due to variable precedence order, setting new value in the trigger job, does not cascade these new values to the downstream pipeline
    - TF_STATE_NAME=$TF_STATE_NAME_2
    - TF_CLI_ARGS_plan=$TF_CLI_ARGS_plan_2
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG && $TF_AUTO_DEPLOY == "true"
    - if: $CI_COMMIT_TAG
      when: manual
###########################################################################################################

Voilà qui est fait. Tout est prêt. N'hésitez pas à changer la façon dont vous contrôlez l'exécution de vos jobs, en tirant parti, par exemple, de la capacité de GitLab à retarder un job avant de le déployer en production.

Essayez par vous-même

Ce tutoriel est maintenant terminé. Nous savons désormais comment contrôler les déploiements vers cinq environnements différents en utilisant uniquement les branches de fonctionnalités, la branche principale et les tags.

  • Nous réutilisons intensivement les templates open source GitLab pour assurer la productivité et la sécurité de nos pipelines.
  • Nous tirons parti des capacités du template GitLab pour remplacer uniquement les blocs nécessitant un contrôle personnalisé.
  • Nous avons divisé le pipeline en petits segments et contrôlons les pipelines enfants afin qu'ils correspondent exactement à nos besoins.

À vous de jouer maintenant. Vous pouvez, par exemple, facilement mettre à jour le pipeline principal afin de déclencher des pipelines enfants, pour votre code source logiciel, avec le mot-clé trigger:rules:changes. Vous pouvez utiliser un autre template en fonction des changements qui se sont produits. Mais c'est une autre histoire.

Votre avis nous intéresse

Cet article de blog vous a plu ou vous avez des questions ou des commentaires ? Partagez vos réflexions en créant un nouveau sujet dans le forum de la communauté GitLab. Partager votre expérience

Lancez-vous dès maintenant

Découvrez comment la plateforme DevSecOps unifiée de GitLab peut aider votre équipe.

Commencer un essai gratuit

Découvrez le forfait qui convient le mieux à votre équipe

En savoir plus sur la tarification

Découvrez ce que GitLab peut offrir à votre équipe

Échanger avec un expert