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.
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 rapportplan
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]
- 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
- 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]
- 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]
- 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]
- 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.
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 :
- Les variables spécifiques à chaque environnement :
../vars/$variables_file.tfvars
- 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.
- 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'environnementproduction
, 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.
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
.
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 :
- Il doit inclure le template Terraform.
- Il doit exécuter des contrôles de sécurité. La validation Terraform dupliquerait le premier niveau, mais les scanners de sécurité peuvent identifier des menaces qui n'existaient pas encore lors des précédents scans (par exemple, si vous déployez en production quelques jours après votre déploiement en préproduction).
- Il doit remplacer les jobs de compilation et de déploiement pour définir des règles spécifiques. Notez que l'étape
destroy
n'est plus automatisée pour éviter des suppressions accidentelles.
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.