Introduction à Terraform
Terraform, développé par HashiCorp et lancé en 2014, a révolutionné le concept d'Infrastructure as Code (IAC). Son émergence a été motivée par la complexité croissante des architectures informatiques et la nécessité d'une gestion plus agile et efficace des ressources dans le cloud.
Avant Terraform, la gestion de l'infrastructure était souvent fragmentée et dépendante de multiples outils spécifiques à chaque fournisseur de cloud ou à chaque technologie. Cette approche cloisonnée générait des défis significatifs en termes de portabilité et d'efficacité opérationnelle. Les administrateurs systèmes faisaient face à des processus manuels sujets aux erreurs, entraînant des déploiements lents et des inconsistances.
Terraform qu'est-ce que c'est ?
Mitchell Hashimoto et Armon Dadgar, les fondateurs de HashiCorp, ont perçu ces défis comme une opportunité pour créer un outil unifié qui simplifierait la gestion de l'infrastructure. Terraform a été conçu pour permettre aux utilisateurs de définir et de provisionner l'infrastructure à l'aide d'un langage de configuration simple et déclaratif. Cette approche visait à rendre les configurations reproductibles et à faciliter la gestion de versions, un élément indispensable.
L'Évolution de Terraform
L'histoire de Terraform est une histoire de croissance, d'adaptation et d'innovation constantes depuis sa première introduction par HashiCorp en 2014. Dans ce chapitre, nous explorerons en détail l'évolution de Terraform, en mettant en lumière les principales étapes de son développement.
Versions et Mises à Jour Majeures
Terraform a connu de nombreuses versions au fil des ans, chacune introduisant de nouvelles fonctionnalités et améliorations significatives. Parmi les versions les plus notables :
Terraform 0.6 : Cette version a introduit le support des modules, permettant aux utilisateurs de réutiliser et de partager des morceaux de code d'infrastructure. Cela a favorisé la création d'une bibliothèque de modules Terraform, facilitant la mise en place de bonnes pratiques et la réduction de la duplication de code.
Terraform 0.12 : L'une des mises à jour les plus importantes, Terraform 0.12 a amélioré la syntaxe HCL (HashiCorp Configuration Language) et a introduit des fonctionnalités telles que les boucles, les conditions, et les expressions plus puissantes. Cela a rendu les fichiers de configuration Terraform plus lisibles et plus flexibles.
Terraform 0.13 : Cette version a amélioré la gestion des modules et a introduit le concept de "providers" pour une meilleure extensibilité. Elle a également contribué à rendre Terraform plus efficace et plus évolutif.
- Terraform 1.0 : Cette version 1.0 de Terraform a marqué une étape importante dans l'histoire de cet outil d'infrastructure en tant que code (IaC). Elle a été annoncée par HashiCorp en juin 2021 et a apporté une stabilité et une maturité significatives à Terraform.
La Contribution de la Communauté
L'une des forces de Terraform réside dans sa communauté open source dynamique. Des milliers de contributeurs du monde entier ont participé à son développement en créant des modules, en signalant des problèmes, en proposant des améliorations et en partageant leurs connaissances. La communauté a joué un rôle essentiel dans l'expansion de l'écosystème Terraform, en ajoutant de nouveaux fournisseurs de cloud, en créant des modules réutilisables, et en aidant les nouveaux utilisateurs à maîtriser l'outil.
HCL le langage de Terraform
Dans le cas de Terraform, le langage utilisé pour définir l’infrastructure est connu sous le nom de HashiCorp Configuration Langage (HCL).
L'objectif principal de Terraform est de déclarer des ressources, qui représentent des objets d'infrastructure. Toutes les autres fonctionnalités de son langage permettent de rendre la définition des ressources plus flexible et pratique.
Pour le moment nous allons nous concentrer sur la description du workflow de Terraform et l'écriture des fichiers de déclarations.
Le workflow de Terraform
Le workflow de Terraform reposent sur cinq étapes clés : Write
, Init
, Plan
,
Apply
et Destroy
et sur l'enregistrement du state
:
- Write: Vous commencez à écrire votre configuration.
- Init: Vous initialisez votre configuration pour installer les dépendances nécessaires.
- Plan: Vous auditez les changements et validez simplement si vous les acceptez.
- Apply: Vous appliquez les changements à l'infrastructure réelle.
- Destroy: Vous décommissionnez toute votre infrastructure.
Ensuite, vous modifiez votre code, vous rebouclez par plan, apply
et ainsi de
suite. La phase de destroy
n'intervenant que lorsque vous n'avez plus besoin de
ces ressources.
Le state de Terraform
La gestion de l'état est un aspect important du workflow de Terraform. L'état de l'infrastructure est stocké dans un fichier d'état, généralement stocké localement ou de manière centralisée via un backend distant. Il est important de gérer cet état avec soin pour assurer la cohérence de l'infrastructure et éviter les conflits.
Par défaut cet état est stocké dans un fichier local nommé "terraform.tfstate", mais il peut également être stocké à distance, mais pas dans votre repository git, parce qu'il peut contenir des secrets.
Terraform utilise cet état pour créer des plans et apporter des modifications à votre infrastructure. Avant toute opération, Terraform effectue un rafraîchissement pour mettre à jour l'état avec celle de l'infrastructure réelle.
Pour gérer ce state, il est possible d'utiliser les workspace et les backends
Installer et configurer Terraform
Terraform est un outil polyvalent qui peut être installé sur différentes plateformes, notamment Linux, macOS et Windows. Dans cette section, nous allons examiner comment installer Terraform en utilisant les dépôts officiels d'HashiCorp pour chaque plateforme.
Pour Linux
Pour les distributions Linux, HashiCorp fournit un dépôt officiel permettant
d'installer Terraform avec des gestionnaires de paquets tels que apt
(pour
Ubuntu/Debian) et dnf
(pour CentOS/RHEL).
Pour Ubuntu/Debian
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install terraform
Pour RHEL family: Centos, Oracle Linux, Alma Linux, Rocky Linux
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform
Pour MacOs
Pour macOS, vous pouvez utiliser le gestionnaire de paquets Homebrew pour installer Terraform. Assurez-vous que vous avez Homebrew installé. Si ce n'est pas le cas, installez-le depuis https://brew.sh.
Dans une fenêtre Terminal :
brew install terraform
Pour Windows
Sur Windows, vous pouvez utiliser le gestionnaire de paquets Chocolatey pour installer Terraform. Assurez-vous que vous avez Chocolatey installé. Si ce n'est pas le cas, installez-le depuis https://chocolatey.org.
Ouvrez une invite de commandes ou PowerShell en tant qu'administrateur.
choco install terraform
Après avoir suivi ces étapes d'installation, vous pouvez vérifier que Terraform
est correctement installé en exécutant la commande terraform version
. Elle
affichera la version de Terraform installée sur votre système.
La CLI terraform
La CLI de Terraform possède les commandes suivantes :
init
: La commande init va initialiser le répertoire de travail et vérifier que les plugins utilisés sont correctement installés.plan
: La commande plan permet de créer un plan d'exécution. Terraform va indiquer quelles actions il doit effectuer pour arriver au résultat décrit dans le fichier de configuration.apply
: L'infrastructure voulue est mise en place.console
: Cette commande permet de faire du débogage, avant de créer un plan ou de l'appliquer.destroy
: L'infrastructure est détruite
terraform init
terraform plan
terraform apply
terraform destroy
Le registre Terraform ou registry
Comme la plupart des langages de programmation Terraform encourage la réutilisation du code via des artefacts réutilisables. Pour les utilisateurs de Terraform, la registry Terraform permet la distribution des modules Terraform, qui sont en fait des configurations réutilisables et de se fournir en providers. Comme avec docker, vous pouvez créer une registry locale pour y stocker votre propre code.
Les fichiers de déclarations de configurations
Les fichiers Terraform porte l'extension .tf
dont le principal se nomme
main.tf
. Comme la plupart des produits HashiCorp, Terraform utilise le
langage
HCL.
Le langage de configuration HashiCorp (HCL) permet de décrire rapidement des ressources à l'aide de blocs, d'arguments et d'expressions.
- Les blocs sont des conteneurs pour d'autres contenus et représentent généralement la configuration d'un type d'objet, comme une ressource, un provider, ... Les blocs ont un type de bloc, peuvent avoir zéro ou plusieurs étiquettes et avoir un corps qui contient un nombre quelconque d'arguments et de blocs imbriqués.
- Les arguments attribuent une valeur à un nom. Ils apparaissent dans les blocs.
- Les expressions représentent une valeur, soit littéralement, soit en référençant et en combinant d'autres valeurs. Ils apparaissent en tant que valeurs d'arguments ou dans d'autres expressions.
Pour vous aider à écrire du code rapidement et ce de manière sûr je vous conseille d'utiliser l'extension Visual Code. En plus elle est écrite par HashiCorp !
Les providers
Terraform s'appuie sur des plugins appelés « providers » pour interagir avec les fournisseurs de cloud, les fournisseurs SaaS et d'autres API. Les configurations Terraform doivent déclarer les fournisseurs dont elles ont besoin pour que Terraform puisse les installer et les utiliser. De plus, certains fournisseurs nécessitent une configuration (comme des URL de point de terminaison ou des régions cloud) avant de pouvoir être utilisés.
provider "libvirt" {
uri = "qemu:///system"
}
Ici, nous utiliserons le fournisseur libvirt
. Nous utiliserons ici une ressource
de type volume du provider libvirt qui porte l'étiquette os_image.
Les providers sont distribués séparément de Terraform lui-même, mais ils sont
installés automatiquement lors de l'initialisation de votre configuration.
Terraform détermine généralement automatiquement le fournisseur à utiliser en
fonction des types de ressource. Pour les providers, comme celui de libvirt
,
vous devez utiliser un bloc required_providers
:
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "0.6.14"
}
}
}
Les principaux providers d'infrastructure :
Les ressources ou resource
Les ressources sont les éléments les plus importants du langage Terraform. Une ressource est une entité d'un service (cloud ou pas). De multiples ressources forment ainsi une infrastructure.
Chaque type de ressource est fournie par un provider. Chaque ressource est associée à un type de ressource unique, qui détermine le type d'objet d'infrastructure qu'elle manipule.
resource "libvirt_volume" "os_image" {
name = "${var.hostname}-os_image"
pool = "default"
source = "bionic-server-cloudimg-amd64.img"
format = "qcow2"
}
Les variables d'entrée
Les Input variables
ou variables d'entrée
pour nous français, sont définies
en indiquant un nom, un type et une valeur par défaut (default = "valeur"). Le
type est optionnel, car Terraform les déduit automatiquement. On peut
ajouter une description.
variable "nom" {
type = "string"
default = "valeur"
description : "Un texte de description."
validation {
condition = condition
error_message = "message d'erreur."
}
}
L'étiquette se trouvant après le mot-clé variable est le nom de celle-ci, il doit être unique parmi toutes les variables d'un même module. Ce nom est utilisé pour lui affecter une valeur ou utiliser sa valeur dans la configuration.
Une variable ne peut pas se nommer : source
, version
, providers
, count
,
for_each
, lifecycle
, depends_on
ou encore locals
.
Pour accéder à une variable dans les blocs, il suffit d'ajouter le préfixe var.
.
variable "hostname" {
type = string
default = "staticip"
}
resource "libvirt_volume" "os_image" {
name = "${var.hostname}-os_image"
pool = "default"
source = "bionic-server-cloudimg-amd64.img"
format = "qcow2"
}
Les types simples
Les chaines ou string
Le type string
est une série de caractères Unicode représentant un texte.
variable "nom_variable" {
type = "string"
default = "bonjour"
}
Les nombres ou number
Le type number
permet de spécifier une valeur numérique, qui peut être entière ou décimale (.)
variable "port" {
type = "number"
default = 80
}
Les booléens ou bool
Le type bool
qui peut prendre les valeurs true
ou false
:
variable "delete_after" {
type = "bool"
default = true
}
Les types de variables complexes
Les listes ou list
Le type list
est une séquence de valeurs :
variable "ma_liste" {
type = "list"
default = ["a", 15, true]
}
Pour accéder à un élément de la liste :
element = "$ {var.ma_liste [0]}"
Les maps ou map ou object
Le type map
est une structure de données composée de couple clé/valeur.
variable "user1" {
type = "map"
default = {
name = "John"
age = 52
}
}
Pour accéder à une clé de la map, deux écritures possibles :
nom = "$ {var.user1[var.name]}"
nom = "$ {var.user1["name"]}"
Les fichiers de variables
Pour de nombreuses variables, il est préférable de les déclarer dans un fichier
de définitions de variables. Un fichier se terminant par l'extension .tfvars
.
Par contre, ici pas besoin de spécifier qu'il s'agit d'un bloc de type variable.
On les spécifie directement sous la forme nom = valeur
.
port = 80
zones = [
"us-east-1a",
"us-west-1c",
]
Les fichiers .tfvars
doit être chargé explicitement lors de l'application du
plan. Pour le charger automatiquement, il suffit de l'appeler .auto.tfvars
Les variables de sortie ou output
Les variables de sortie permettent d'afficher les valeurs d'une variable à la
sortie de l'application du plan, sauf si elle est déclarée sensitive
.
output "ip" {
value = libvirt_domain.domain-ubuntu.*.network_interface.0.addresses
description : "The public IP address of the server instance."
sensitive = true
}
Les variables locals
Contrairement aux variables d'entrée, une variable locale n'est accessible que
dans les expressions du module où elle a été déclarée. Les valeurs locales
peuvent être utiles pour éviter de répéter plusieurs fois les mêmes valeurs ou
expressions dans une configuration, mais si elles sont trop utilisées, elles
peuvent également rendre la lecture d'une configuration difficile. On accède
à une variable locale en la préfixant par local.
.
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
resource "aws_instance" "example" {
# ...
tags = local.common_tags
}
Les data sources ou data
Les sources de données appelées data
permettent d'extraire ou de calculer des
données qui seront ensuite utilisées ailleurs dans votre configuration
Terraform.
data "google_compute_instance" "appserver" {
name = "primary-application-server"
zone = "us-central1-a"
}
Comme dis précédemment cela peut être aussi une donnée calculée, par exemple l'utilisation d'un template pour générer un fichier qui sera ensuite utilisé par votre configuration :
data "template_file" "user_data" {
template = file("${path.module}/cloud_init.cfg")
vars = {
hostname = "${var.hostname}"
fqdn = "${var.hostname}.${var.domain}"
public_key = "${file("./id_rsa.pub")}"
}
}
Le contenu (en partie) du template ou on voit l'utilisation des variables ${variable :
hostname: ${hostname}
fqdn: ${fqdn}
manage_etc_hosts: true
...
C'est comme cela qu'on arrive à créer par exemple un inventaire Ansible qui sera ensuite utilisé par un provisionner local. Mais je préfère utiliser l'inventaire Ansible du provider en question.
On peut aussi faire appel à des programmes
externes
ou external
:
data "external" "cars_count" {
program = ["python", "${path.module}/get_cool_data.py"]
query = {
thing_to_count = "cars"
}
}
output "cars_count" {
value = "${data.external.cars_count.result.cars}"
}
Vous commencez à voir le potentiel qu'offre Terraform, il est infini. On peut ainsi récupérer des données d'un vault Terraform. doc
Filtrer les data sources avec filter
Les filtres permettent de faire le tri et de récupérer les informations nécessaires, utiles pour les données externes.
data "aws_ami" "example" {
executable_users = ["self"]
most_recent = true
name_regex = "^myami-\\d{3}"
owners = ["self"]
filter {
name = "name"
values = ["myami-*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Ici, on récupère toutes les images ami
disponibles pour ne garder que celles :
- dont le nom contient 'myami-*'
- de root type ebs
- de virtualisation-type hvm
La documentation de cette source de données
Utilisation des data dans votre configuration
Comme pour les variables, il suffit d'ajouter le suffixe data
devant le nom de
celui :
resource "aws_instance" "app" {
ami = "${data.aws_ami.app_ami.id}"
instance_type = "t2.micro"
}
Les provisionneurs ou provisioner
Les provisioner
sont utilisés pour lancer des actions sur la machine locale ou
sur une machine distante afin de préparer des serveurs ou d'autres objets
d'infrastructure pour le service. Il existe des trois provisioner génériques :
local_exec
, remote_exec
et file
.
Il faut privilégier l'utilisation au maximum les provider plutôt que ces provisioner.
Le provisioner local_exec
Pour exécuter une action locale vous pouvez utiliser le provisioner local_exec
:
resource "null_resource" "example2" {
provisioner "local-exec" {
command = "Get-Date > completed.txt"
interpreter = ["PowerShell", "-Command"]
}
}
Du powershell :)
Le provisioner remote_exec
Pour exécuter une action sur la ressource distante il faut utiliser le provisioner
remote_exec
:
resource "aws_instance" "web" {
# ...
provisioner "remote-exec" {
inline = [
"dnf -y install epel-release",
"dnf -y install htop",
]
}
}
Le provisioner file
Le provisioner file est utilisé pour copier des fichiers ou des répertoires de la machine exécutant Terraform vers la ressource nouvellement créée.
resource "aws_instance" "web" {
# ...
# Copies the myapp.conf file to /etc/myapp.conf
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
Pour info : ce provisioner prend en charge les connexions de type ssh
ou et winrm
.
Modifier le comportement d'un provisioner
Par défaut, un provisioner s'exécute lorsque la ressource dans laquelle il est
défini est créée. Attention seulement pendant la création et non la mise à jour
! Cependant, on peut modifier ce comportement avec les arguments when =
destroy
et on_failure = continue
.
when = destroy
permet de lancer un provisioner au moment du décommissionnement
d'une ressource.
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
when = destroy
command = "echo 'Destroy-time provisioner'"
}
}
En cas d'échec d'un provisioner la ressource associé est déclarée comme vérolée.
On peut bypasser ce comportement en utilisant on_failure = continue
.
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo The server's IP address is ${self.private_ip}"
on_failure = continue
}
}
Structure d'un projet Terraform
Pour un projet très simple, vous pouvez regrouper tout dans un seul fichier
main.tf
. Cependant, je vous conseille d'adopter une structure pour vous y
retrouver facilement dans tous vos projets, voici la structure que j'ai adoptée :
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── resources.tf
├── provider.tf
├── terraform.tfvars
├── modules/
│ ├── module1/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
- Le fichier
main.tf
qui est le fichier principal d'un projet terraform - Le fichier
provider.tf
pour y définir les fournisseurs - Le fichier
variables.tf
pour les variables principales - Le fichier
terraform.tfvars
pour les variables secrètes qui ne sera pas stocké dans votre repository git - Le fichier de variables *.auto.tfvars variables qui sont lues automatiquement
- Le fichier
outputs.tf
pour y définir tout ce qui sera affiché - Les fichiers
resources.tf
pour un petit projet un simple fichier resources.tf suffira pour de plus prend vous pouvez en créer plusieurs avec des noms explicites. - Les modules
- Le fichier
.gitignore
dont voici le contenu :
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
#
*.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
L'ordre des blocs et des fichiers dans lesquels ils sont organisés ne sont généralement pas significatifs ; En effet Terraform utilise les relations implicites et explicites entre les ressources pour déterminer dans quel ordre les opérations vont s'exécuter.
Vous pouvez poursuivre la lecture sur les billets suivants :
- Les backends Terraform et les workspaces
- Meta-arguments : créer des boucles et modifier le comportement des ressources
Cas pratique
Je vous propose de suivre l'application dans plusieurs exemples :
En utilisant le provider libvirt:
dans Mon Home Lab Devops
Plus d'infos
- Site Officiel : terraform.io
- Documentation : terraform.io/docs
Livres
Sites
- Comprendre Terraform (infra-as-code) en 5 minutes
- Comprendre et manipuler ses fichiers d'état Terraform
- Infrastructure As Code sous AWS avec Terraform