Blog DevOps

Améliorer la qualité de code IaC via les pre-commit hooks

Rédigé par Antoine Chapusot | 11 août 2022 12:00:00

Installer et configurer pre-commit

Tous les exemples ci-dessous sont repris dans ce repository Github.

Pourquoi pre-commit et comment le mettre en place localement

C’est ici notre point de départ. Pre-commit v2 à été introduit début 2020, nous sommes à l’heure de cet article à la v2.20.0. C’est un outil formidable à combiner avec le OnSave formating de votre IDE (si vous en avez un) afin de vous assurer de toujours commit du code propre (selon les règle que vous aurez choisi d’activer).

Premièrement, nous allons voir comment personnaliser votre environnement afin de lancer les scripts de pre-commit au lancement de la commande git commit.

Prérequis

  • Python (python3 de préférence) et pip (pip3 ;) ), et c’est tout

    apt update && apt install -y python3 python3-pip
    
    pip3 install pre-commit --upgrade

Le fichier config

  • Un exemple du fichier .pre-commit-config.yaml

# .pre-commit-config.yaml
default_stages: [commit]
repos:
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v2.29.5
    hooks:
      - id: commitizen
        stages:
          - commit-msg
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.74.1
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint
      - id: terraform_tfsec
      - id: checkov
      - id: terraform_docs
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.3.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

Dans ce fichier, vous allez choisir les hooks depuis un fichier .pre-commit-hooks.yaml défini dans différents git repos et les default_stages au moment desquels les executer.

Pour cet exemple, vous aurez besoin d’installer le hook commit-msg. Je vous conseille cette documentation pour en savoir plus.

pre-commit install --hook-type commit-msg

Executer les scripts

Dans un premier temps, créer un petit fichier terraform comme celui-ci :

# main.tf
terraform {
  required_version = ">= 1.0.0, < 1.0.4"
}

resource "random_password" "password" {
  length = 16
  special = true
  override_special = "_%@"
}

output "test" {
  value = random_password.password.result
  sensitive = true
}

Executer ensuite manuellement les scripts via la commande pre-commit run -a pour confirmer que les pre-commit sont fonctionnels (nous automatiserons dans un 2e temps).

root@655088f99f35:/test# pre-commit run -a
[INFO] Initializing environment for https://github.com/commitizen-tools/commitizen.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/antonbabenko/pre-commit-terraform.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Terraform fmt............................................................Failed
- hook id: terraform_fmt
- files were modified by this hook

main.tf
Terraform validate.......................................................Passed
Terraform validate with tflint...........................................Failed
- hook id: terraform_tflint
- exit code: 127

/root/.cache/pre-commit/reposnevwej6/terraform_tflint.sh: line 66: tflint: command not found

Terraform validate with tfsec............................................Failed
- hook id: terraform_tfsec
- exit code: 127

/root/.cache/pre-commit/reposnevwej6/terraform_tfsec.sh: line 25: tfsec: command not found

Checkov..................................................................Failed
- hook id: checkov
- exit code: 1

Executable `checkov` not found

Terraform docs (overwrite README.md).....................................Failed
- hook id: terraform_docs_replace
- exit code: 1

/bin/sh: 1: terraform-docs: not found
Command 'terraform-docs md ./ > .//README.md' returned non-zero exit status 127.

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed

On voit des retours des scripts, certains en failed d’autre en passed. Super ! Notre fichier .pre-commit-config.yaml est lu, reconnu et consistant. Pour corriger les failed, nous allons installer quelques outils liés aux scripts que l’on a choisi.

Maintenir les versions à jour

Pre-commit possède uen sous-commande de mise à jour des tags sur lesquels il pointe pour télécharger les .pre-commit-hooks.yaml distants. Pensez donc à lancer régulièrement pre-commit autoupdate.

Les hooks pour Terraform

Nous allons maintenant nous concentrer sur l’outil Terraform. Quels serait les hooks qui nous permettrait d’améliorer la qualité de notre code IaC ? Vous pouvez trouver quelques indices dans l’output plus haut ou depuis la section du fichier YAML. Une fois que nous les aurons configurés, je vous détaillerai leur utilité.

Quels pre-commit hooks installer pour Terraform ?

Installons les packages suivants (brew est aussi disponible pour linux, sinon, utilisez votre gestionnaire de package préféré)

brew install tfsec tflint checkov terraform-docs

Maintenant, vous pouvez créer un commit pour voir les scripts se lancer automatiquement

$ git add --all && git commit -m "chore: first commit"
Terraform fmt............................................................Passed
Terraform validate.......................................................Passed
Terraform validate with tflint...........................................Passed
Terraform validate with tfsec............................................Passed
Checkov..................................................................Passed
Terraform docs (overwrite README.md).....................................Failed
- hook id: terraform_docs_replace
- files were modified by this hook
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed

La majorité d’entre eux affichent Passed, ce qui est OK, mais terraform-docs affiche Failed en indiquant files were modified by this hook ce qui signifie qu’une documentation a été créée comme attendue mais qu’elle n’a pas encore été “staged” dans git. Pour la résoudre, vous pouvez ajouter le README.md avec git add README.md, puis relancer votre commande de commit. C’est tout pour les erreurs ! Passons au détails de chaque hook.

Les hook de linting

Dans la qualité de code, ils améliorent la lisibilité :

  • terraform fmt : The terraform fmt command is used to rewrite Terraform configuration files to a canonical format and style. Cette commande formate les fichiers en uniformisant les indentation entre autres. Elle est intégrée au binaire de terraform
  • terraform validate : The terraform validate command validates the configuration files in a directory, referring only to the configuration and not accessing any remote services such as remote state, provider APIs, etc. Cette commande, également intégrée au binaire de terraform, s”assure de la validité du code au regard des standards Hashicorp tel la déclaration de toute variable utilisée, le respect des syntaxes de chaque bloc, etc.
  • terraform lint : tflint trouve les erreurs possibles dans l’appel de certaines resources liées au provider, averti des syntaxes dépréciées et s’assure de quelques conventions de nommage de base.
  • trailing-whitespace & end-of-file-fixer : Comme son nom l’indique : s’assure de supprimer les caractères espace de fin de ligne et s’assure également de la présence d’un saut de ligne en fin de fichier.

Les bonnes pratiques liées aux Cloud Providers

C’est ici le coeur de cet article : nous allons voir en détail les pre-commit hooks qui apportent le plus à votre qualité de code mais qui demandent également une certaine adaptation de votre codebase. Basés sur des références de bonnes pratiques mis à jour par la communauté, les 2 hooks suivants remontent erreurs et avertissements en fonction de l’implémentation de certaines resources terraform.

  • tfsec
  • checkov

Ce sont tout les 2 des analyseurs de code statiques qui scannent les configurations de votre infrastructure et repèrent des défaut de configuration avant sa création.

Prenons l’exemple d’une clef AWS KMS aws_kms_key. La règle aws-kms-auto-rotate-keys de tfsec s”assure que l’instruction suivante, concernant la rotation de la clef, est activée

enable_key_rotation = true

C’est une bonne pratique de sécurité que d’activer la rotation automatique des clefs de chiffrement.

Cependant, il est possible que pour un besoin métier, vous ayez besoin de laisser cette rotation désactivée. Pour ignorer l’avertissement de tfsec à ce sujet, vous pouvez ajouter le commentaire suivant dans le bloc resource de la clef.

resource "aws_kms_key" "bad_example" {
	enable_key_rotation = false #tfsec:ignore:AWS004
}

Il est possible de faire la même chose pour checkov avec un commentaire de ce type #checkov:skip=CKV_AWS_7:The KMS key must not be rotated due to business needs.

Ces 2 outils ont des bases de règles similaires en grande partie. Vous pouvez choisir d’utiliser l’un ou l’autre. Je vous recommande checkov de BridgeCrew qui selon moi montre une plus grande base de règle qui compte actuellement 303 checks pour AWS, 117 pour GCP et 184 pour Azure.

Vous devez porter une attention particulière vis à vis de l’activation de ces hooks sur une codebase existante. En effet, si le nombre de resources que possède votre code terraform dépasse quelques dizaines, il se peut que vous soyez contraint de penser à dégager du temps pour adapter votre codebase voire à recréer certaines ressources qui ne respecteraient pas les bonnes pratiques.

Des hooks pour la collaboration

  • commitizen define a standard way of committing rules and communicating it. il permet de controller que le format du message associé au commit est sous la forme feat(scope): message. Cette pratique vient du conventionnal commit et s’avère très utile pour la génération automatique de CHANGELOG.
  • terraform-docs : génère une documentation selon différents formats. Cet outil vous permet de vous abstraire de détailler manuellement l’interface de vos modules et le fait à votre place. terraform-docs place dans plusieurs tables les inputs, outputs, providers, etc. liés à votre module. Il peut être configuré au travers d’un fichier .terraform-docs.yml ce que je recommande.

Les autres hooks

Je vous ai montré les essentiels, mais vous pouvez assurément parcourir les différents hooks disponible afin de trouver ceux qui conviennent le plus à votre codebase.

Executer les hooks dans une CI

Nous avons vu que les pre-commits hooks sont formidables pour le contrôle local de la qualité de code, mais il est probable que vous ne soyez pas seul à contribuer à votre codebase. Que se passe-t-il si les autres DevOps n’ont pas installé pre-commit ou bien qu’ils décident sciemment de les ignorer?

Et bien ces derniers peuvent publier du code qui ne respecte pas les règles que vous avez définies et ces changements peuvent vous bloquer si vous décider de récupérer ce code et d’ajouter des modifications par dessus. Le code “corrompu” sera confronté aux règles de pre-commit au moment ou vous ferez un nouveau commit sur votre poste en local.

Vous avez donc certainement envie de mettre en place une manière forte d’imposer ces standards de qualité de code à toute votre équipe. Et pour cela, une solution consiste à executer les pre-commit hooks dans une CI.

Comment s’assurer que les pre-commit sont exécutés avant git merge ?

Vous pouvez executer la commande pre-commit run -a dans la CI de votre choix. Celle-ci étant centrale, vous pouvez executer ce job sur chaque branche différente de celles ou celle sur lesquels vous souhaiter merger (selon votre gitflow), c’est à dire, sur vos branches de feature. Ajouté à cela une condition de merge qui requiert que les pipeline de CI soient success avant de pouvoir merge et vous avez une condition forte de contrôle de la qualité de vos commits.

Mon collègue et ami Stan a fait un repository GitHub avec une image Docker contenant pre-commit ainsi que les hooks présentés plus haut et accompagnée d’un exemple de job de CI pour GitlabCI. Une fois mis en place, vous pouvez voir votre CI bloquer sur des violation de règles de pre-commit de votre fichier .pre-commit-config.yaml.

Conclusion

Nous avons donc vu comment installer et configurer pre-commit, ses hooks utiles sur une codebase terraform et comment imposer ses standards à une collaboration. J’ai personnellement témoigné d’une amélioration conséquente de la qualité globale du code suite à la mise en place de ces pre-commit dans une équipe de quelques 10 contributeurs.

En tant que DevOps, quels peuvent être vos exigences en terme de qualité de code ? Quels autres languages pourriez-vous soumettre aux pre-commit hooks (comme Ansible ou Python) ? Qu’en pensez-vous ?