Une CI/CD prête à l'emploi pour Terraform avec GitLab CI

Une CI/CD prête à l'emploi pour Terraform avec GitLab CI

Une CI/CD, qu'est-ce que c'est ?

Le terme CI/CD représente une chaîne entièrement automatisée (ensemble d'étapes) qui se compose de deux concepts.

Tout d'abord, la première partie CI signifie Continuous Integration qui permet dès lors qu'un changement est apporté au code d'une application, d'exécuter un certain nombre d'actions (construction du paquet ou phase de build, lancement des tests unitaires, etc.) de manière automatisée dans le but que ce nouveau code soit entièrement vérifié et fusionné au plus vite sur la branche principale du dépôt de code de cette application.

D'autre part, la partie CD qui représente Continuous Deployment ou Continuous Delivery permettent de déployer une application compilée et testée dans un référentiel (Nexus, Artifactory, etc.) dans le but d'être, par la suite, déployée de manière automatisée sur des environnements pouvant aller jusqu'à la production.

Ici, je vous ai parlé d'une chaîne d'intégration et de déploiement pour la partie logicielle, du moins, quand on souhaite gérer le cycle de vie d'une application.

Les choses sont différentes quand on parle d'infrastructure as code notamment avec l'outil Terraform, il n'y a pas de phase de build à proprement parlé. Le but de la partie CI sera de vérifier le respect des bonnes pratiques en matière de syntaxe, de nomenclature et que le code est conforme aux pratiques de sécurité préalablement définies.

Par exemple, on souhaitera vérifier que le code est bien indenté avec la commande terraform fmt, que les variables définies au sein de Terraform sont toutes utilisées, possèdent une description et sont correctement typées.

Pour la partie sécurité, on souhaitera éviter de déployer des ressources de manière publique ou d'ouvrir des accès trop important au niveau du pare-feu (port SSH, par exemple) dès lors que l'on souhaite déployer sur le Cloud.

Maintenant, que le concept de CI/CD a été défini, qu'est-ce que GitLab CI et pourquoi utiliser ce type d'outil ?

GitLab CI

GitLab CI est une fonctionnalité propre à GitLab qui est la plateforme de développement collaborative open source ayant comme principale caractéristique de stocker votre code Git. Celle-ci permet de créer une chaîne CI/CD associée au cycle de vie de votre code source.

C'est une fonctionnalité de base et gratuite que vous pouvez utiliser directement sur le site GitLab.com ou, en hébergeant votre propre instance de GitLab.

Pourquoi GitLab CI et pas un autre outil de CI/CD ?

Personnellement, j'ai toujours beaucoup travaillé avec GitLab, quel que soit le projet sur lequel je suis. Je trouve que l'outil dispose de nombreuses fonctionnalités avancées comme par exemple les pipelines enfants qui peuvent être générées à la volée.

De plus, l'outil est très facile à prendre en main, avec très peu de code YAML, on est capable de faire des chaînes automatisées très rapidement.

GitLab CI se base sur des images conteneurisées pour exécuter les étapes de votre chaîne CI/CD ce qui permet d'avoir un large choix en matière de langages et d'outils comme par exemple Terraform qui est disponible sans aucune installation.

Dès que vous allez exécuter votre chaîne d'intégration et de déploiement continus, GitLab CI s'appuie sur un mécanisme de runners qui sont des machines qui vont exécuter les différentes étapes de votre CI/CD.

Vous avez deux possibilités sur GitLab.com : utiliser les runners partagés (shared runners) ou installer les vôtre et les associer à vos projets.

Terraform et la CI/CD

Comme évoqué plus haut, Terraform est un outil d'infrastructure as code qui permet de déployer des infrastructures, principalement dans le Cloud, de manière automatisée en proposant une approche déclarative via du code contenu dans des fichiers avec une extension .tf

Pour déployer ce code Terraform, il est recommandé de passer par une approche automatisée de type CI/CD composée de plusieurs étapes :

iac-cicd

Ces différentes étapes permettront de valider le code et de déployer l'infrastructure de manière centralisée.

Ici, l'infrastructure Terraform correspond à des ressources à déployer qui sont éligibles au niveau gratuit de Google Cloud que je vous avais présenté lors d'un article précédent. C'est donc un cas simple, mais très courant.

Voici ce à quoi pourrait ressembler une chaîne CI/CD pour Terraform :

terraform-cicd

Comme vous pouvez le constater, plusieurs outils ont été utilisés en fonction des étapes définies dans le schéma du dessus.

Je vais vous détailler ces différentes étapes...

Terraform fmt

Le formatage inclus dans le binaire Terraform est une vérification syntaxique de base qui permet de vérifier que le code est bien indenté selon les bonnes pratiques définies par Terraform.

La commande terraform fmt -check -recursive -write=false -diff permettra de vérifier globalement votre code et de vous notifier des erreurs.

Voici un exemple avec un problème d'indentation sur le fichier prod.tfvars :

$ terraform fmt -check -recursive -write=false -diff
terraform/configurations/prod.tfvars
--- old/terraform/configurations/prod.tfvars
+++ new/terraform/configurations/prod.tfvars
@@ -1 +1 @@
-project_id                = "xxx"
+project_id = "xxx"

TFLint

TFLint est ce que l'on appelle communément un Linter, il permet de détecter les possibles erreurs de syntaxe en fonction d'un ensemble de règles définies dans le fichier tflint.hcl.

De plus, il est possible d'ajouter des règles personnalisées en Rego en fonction de vos besoins.

Il se base sur trois catégories de fonctionnalité :

  • Les erreurs de configuration, comme des types d'instance invalides que ce soit sur AWS, Azure ou Google Cloud ;
  • La vérification de la syntaxe dépréciée ou de variables non utilisées ;
  • Le contrôle de la convention de nommage des ressources ainsi que les bonnes pratiques Terraform (spécifier une description pour les variables ou les outputs)

Ici, vous avez un exemple avec deux erreurs remontées par TFLint, il manque le champ required_version pour la version minimale de Terraform et le fichier main.tf au sein du code Terraform :

6 issue(s) found:

Warning: terraform "required_version" attribute is required (terraform_required_version)

  on  line 0:
   (source code not available)

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.2.2/docs/rules/terraform_required_version.md

Warning: Module should include a main.tf file as the primary entrypoint (terraform_standard_module_structure)

  on terraform/main.tf line 1:
   (source code not available)

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.2.2/docs/rules/terraform_standard_module_structure.md
[...]

Checkov

Checkov est un outil de Policy-as-code de l'entreprise Bridgecrew permettant d'identifier les erreurs de configuration sur des outils d'infrastructure as code, notamment Terraform.

Avec cet outil, vous allez être en mesure d'identifier si le code que vous allez déployer ne dispose pas de failles de sécurité importantes : permission donnée à un utilisateur trop large, adresse IP externe sur une machine virtuelle, etc.

Tout comme TFLint, il se base sur un fichier de configuration .checkov.yaml et il permet de créer des règles personnalisées sous différents formats.

Je trouve cet outil plus intéressant que son concurrent tfsec dans la mesure où Checkov permet de réaliser une analyse statique du code, mais aussi d'analyser le plan généré par Terraform.

Enfin, il est tout à fait possible de désactiver certaines règles de Checkov en fonction de vos besoins. Par exemple, si vous avez besoin qu'une machine virtuelle dispose d'une adresse IP externe (c'est le cas du code sur le niveau gratuit de Google Cloud), vous avez deux façons de faire :

  • Soit pour une seule ressource, en indiquant dans la ressoure concernée le commentaire #checkov:skip=CKV_GCP_40:Use Google Cloud Free Tier VM with external IP composé de #checkov:skip=<Numéro de la règle concernée>:Commentaire additionnel ;
  • Soit pour tout le code, en indiquant dans le fichier de configuration .checkov.yaml le bloc :
skip-check:
- CKV_GCP_40

La documentation de Checkov est très riche et permet de vous indiquer comment résoudre les différentes erreurs de configuration à la suite de votre analyse de code.

Par exemple, ici avec l'erreur suivante :

Check: CKV_GCP_32: "Ensure 'Block Project-wide SSH keys' is enabled for VM instances"
	FAILED for resource: google_compute_instance.external_services
	File: /compute.tf:9-51
	Guide: https://docs.bridgecrew.io/docs/bc_gcp_networking_8

		9  | resource "google_compute_instance" "external_services" {
		10 |   project = module.external_services.project_id
		11 | 
		12 |   name         = var.instance_name
		13 |   machine_type = var.machine_type
		14 |   zone         = var.zone
		15 | 
[...]

Le lien Guide inclus dans l'erreur indique qu'il faut ajouter ce bloc dans la ressource pour éviter d'ajouter les clés SSH du projet au sein de la machine virtuelle :

  metadata = {
    block-project-ssh-keys = true
  }

Terratest

Terratest de Gruntwork.io est une librairie en langage Go permettant de réaliser des tests de création d'infrastructure pour Terraform, mais pas seulement, d'autres langages sont pris en charge.

Cet outil permet de valider que l'ensemble de vos ressources se déploie correctement sans erreur, quelles que soient les modifications apportées au code Terraform.

Le test se réalise en trois étapes :

  • Création de l'infrastructure avec Terratest et Terraform ;
  • Validation de l'infrastructure déployée en vérifiant les ressources, leurs noms, leurs attributs, etc. ;
  • Suppression de l'infrastructure une fois le test terminé.

Terratest est livré avec plusieurs modules permettant de récupérer des valeurs sur des ressources et de faire des vérifications.

Voici un exemple de test qui va créer des ressources dans un projet Google Cloud, tester les caractéristiques d'une machine virtuelle en ayant récupéré préalablement les informations de celle-ci et supprimer les ressources en fin de test :

package test

import (
	"testing"

	"github.com/gruntwork-io/terratest/modules/gcp"
	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

func TestTerraformEndToEnd(t *testing.T) {

	terraformOptionsGoogleCloudFreeTier := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		TerraformDir: "../terraform",
		VarFiles: []string{
			"./configurations/test.tfvars",
		},
		BackendConfig: map[string]interface{}{
			"bucket": "xxx", // Update bucket name
			"prefix": "google-cloud-free-tier/test",
		},
		Reconfigure: true,
	})

	defer terraform.Destroy(t, terraformOptionsGoogleCloudFreeTier)

	terraform.InitAndApply(t, terraformOptionsGoogleCloudFreeTier)

	// Get project ID from outputs
	projectId := terraform.Output(t, terraformOptionsGoogleCloudFreeTier, "project_id")

	/* 
		Do some checks
	*/
	expectedInstanceName := "ci-external-services"
	expectedMachineType := "e2-micro"
	expectedVMStatus := "RUNNING"

	instance := gcp.FetchInstance(t, projectId, expectedInstanceName)

	// Check machine type
	assert.Contains(t, instance.Instance.MachineType, expectedMachineType)
	// Check status
	assert.Equal(t, expectedVMStatus, instance.Instance.Status)
}

À noter que l'instruction avec le mot clé defer s'exécutera à la fin de la fonction de test.

Terraform plan et apply

Une fois l'ensemble des vérifications syntaxiques, de sécurité et les tests d'intégration exécutés avec succès, on peut générer le plan avec Terraform et proposer la création des ressources avec celui-ci. À noter que dans mon cas, j'autorise l'apply uniquement si le code est sur la branche principale à savoir main.

Création de la CI/CD sur Gitlab CI

Avant de commencer, l'ensemble du code que je vais vous exposer dans cette partie est disponible sur GitLab via le lien suivant :

https://gitlab.com/filador-public/terraform-with-gitlabci

Ce dépôt de code contient l'ensemble des étapes pour créer une chaîne CI/CD pour Terraform en utilisant GitLab CI. Comme dit au début de l'article, le code Terraform se base sur un précédent article que j'avais réalisé sur la création de ressources permettant de profiter du niveau gratuit de Google Cloud.

Le point d'entrée de GitLab CI est le fichier .gitlab-ci.yml à la racine de votre dépôt de code. Il est divisé en plusieurs parties :

  • include permet de diviser la chaîne automatisée en plusieurs sous-fichiers, ce qui améliore la visibilité plutôt que de tout faire tenir dans un seul et même fichier ;
  • variables, liste l'ensemble des variables communes sur l'ensemble des étapes exécutées par GitLab CI ;
  • stages énumère l'ensemble des étapes de la chaîne CI/CD.

Par la suite, dans les fichiers check.gitlab-ci.yml, test.gitlab-ci.yml et terraform.gitlab-ci.yml on vient déclarer les jobs pour chaque étape (stage).

Par exemple, avec le job "format" :

format:
  stage: check
  image:
    name: hashicorp/terraform:${TERRAFORM_IMAGE_VERSION}
    entrypoint:
      - "/usr/bin/env"
      - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  script:
    - terraform fmt -check -recursive -write=false -diff

Celui-ci est rattaché à l'étape check avec une image conteneurisée hashicorp/terraform permettant d'exécuter le script de vérification du bon formatage du code Terraform.

Le mot clé entrypoint permet de surcharger le point d'entrée de l'image et de réaliser une suite de commande définie dans script.

Pour les deux jobs tflint et checkov de l'étape check, la partie script permet d'itérer sur l'ensemble des dossiers Terraform du dépôt de code et d'exécuter les commandes tflint ou checkov dans chaque dossier.

Le deuxième fichier test.gitlab-ci.yml contient le job terratest permettant de réaliser le test de création de l'infrastructure de bout en bout.

Deux choses sont à noter :

  • Le bloc before_script permet d'initialiser la clé du compte de service à travers la variable GOOGLE_APPLICATION_CREDENTIALS permettant de créer l'infrastructure sur Google Cloud dans le cas où on utilise les runners partagés de GitLab CI ;
  • Enfin, le bloc after_script vient supprimer les informations préalablement définies afin de supprimer toutes les informations sensibles contenues dans le job.

N'oubliez pas de configurer la variable GOOGLE_CLOUD_SERVICE_ACCOUNT contenant votre compte de service au format JSON dans les variables de la CI/CD (Settings > CI/CD > Variables > Add variable).

On retrouvera également ces deux instructions dans le fichier terraform.gitlab-ci.yml pour créer l'infrastructure.

Dans le dernier fichier terraform.gitlab-ci.yml, les deux dernières étapes plan et deploy sont présentes.

Un squelette de job est aussi visible : .default_terraform_job, il sert à regrouper les caractéristiques communes aux deux jobs Terraform terraform-plan et terraform-apply.

Pour réaliser la phase d'apply Terraform, on viendra utiliser le fichier googlecloudfreetier.tfplan contenant le plan de l'étape précédente via la fonctionnalité artifacts de GitLab CI.

Petite spécificité, le job terraform-apply ne s'exécutera que sur la branche principale main avec le mot clé only. De plus, il demandera à l'utilisateur d'être déclenché de manière manuelle avec l'instruction when: manual. Cela évite les comportements non désirés si le plan de l'étape précédente ne correspond pas à vos attentes.

Enfin, un système de cache est également configuré dans ces deux étapes, il permet d'éviter de retélécharger les providers et modules.

L'analyse du code de GitLab CI étant terminé, il me reste à conclure...

Pour finir...

Comme vous avez pu le constater, initialiser sa première chaîne CI/CD est relativement facile avec GitLab CI.

C'est d'autant plus rapide avec le dépôt de code que je vous ai fourni regroupant plusieurs étapes que j'ai l'habitude d'utiliser dans un contexte basé sur Terraform.

N'hésitez pas à l'adapter en fonction de vos besoins et de vos différents outils d'analyse de code, de sécurité, mais aussi permettant d'effectuer des tests de déploiement.

Pour ceux qui souhaitent en apprendre davantage sur GitLab CI, deux articles viendront prendre la suite de celui-ci, je vous parlerai de l'installation des runners sur des machines virtuelles et un cas avancé avec l'utilisation des pipelines enfants dans le cas d'un déploiement d'une infrastructure sur plusieurs couches avec GitLab CI.