Fin 2019 on m’a demandé d’aider à la migration d’un serveur dédié vers une infrastructure managée AWS pour faciliter la scalabilité d’une plateforme médicale.
La société qui allait s’occuper de l’infogérance (nécessaire pour la certification HDS) ne maitraisant pas Terraform, j’ai utilisé la console AWS pour configurer les différents services. Les pipelines de déploiement, le cluster MongoDB, la configuration réseau… tout a été monté à la main. Les besoins de l’époque étant relativement basiques, ce n’était pas tellement un problème : j’ai pu répliquer intégralement l’infrastructure d’un environnement à l’autre en une grosse journée ou deux.
Mais le temps passant, les besoins changent, la complexité augmente et les risques aussi. Plusieurs acteurs intervenant pour modifier des configurations ou installer de nouveaux outils, le partage d’informations devient compliqué et des effets de bords peuvent apparaître. Risquant également de ne pas toujours être disponible (j’aimerais bien prendre des vacances de temps en temps, quand même 😊), que se passe-t-il s’il faut modifier un élément précis sans risque, ou dupliquer un pipeline pour un nouvel environnement ?
J’ai donc proposé spontanément à mon client de mettre en place des outils pour centraliser la configuration de son infrastructure entière, mais aussi permettre l’automatisation de celle-ci : un simple push sur un dépôt Git pourrait permettre de mettre à jour ce qui a besoin de l’être, sans même se connecter à la console AWS ou lancer un tunnel SSH.
Convaincu, la mission est validée, je me lance donc dans un projet… alors que je n’ai jamais touché à Terraform avant ! Tout juste ai-je joué avec Ansible. Le client me fait confiance malgré tout, c’est parti pour apprendre en pratiquant !
Cet article a vocation d’être une introduction à Terraform. D’autres articles sont prévus pour aborder des aspects plus poussés. Il s’agit ici de poser les bases pour créer des ressources simples avec une organisation de fichiers propre mais linéaire.
- Démarrage en douceur
- Organiser ses fichiers
- Définir un fournisseur
- Utiliser des variables
- Les Locales
- Importer des ressources existantes
- Créer des ressources dépendantes
- Faire sortir des infos
- Tout supprimer et partir en retraite
Démarrage en douceur
Pour faire simple je vous suggère de suivre les tutoriels Get Started officiels de Hashicorp (qui produit Terraform) : j’ai suivi celui pour AWS et ça aide à prendre l’outil en main et assimiler les commandes de base.
On y voit ainsi la syntaxe des fichiers .tf
(qui contiennent la configuration de notre infra) et .tfvars
(qui permettent d’injecter des variables via un fichier, en remplacement ou complément des variables d’environnement), les commandes terraform init
pour installer les dépendances, terraform plan
pour générer un plan d’exécution avant d’appliquer nos modifications et terraform apply
pour déployer une nouvelle version de l’infra.
Les tutoriels sont très bien faits et vous permettront de voir les bases de Terraform appliquées à votre fournisseur préféré.
Organiser ses fichiers
Pour commencer on peut tout mettre dans un même dossier, qui servira de racine à notre projet Terraform. On verra dans un prochain article comment organiser un projet plus complexe avec des modules.
Les fichiers .tf
seront chargés automatiquement, peu importe leur nom : les noms utilisés dans cet article ne sont qu’une convention ou des suggestions. Libre à vous d’en utiliser d’autres si besoin, tant que vous vous y retrouvez.
Les fichiers .tfvars
ne sont pas lus automatiquement, c’est à vous de les inclure lors de certaines commandes (plan
ou apply
par exemple).
Les fichiers .tfstate
sont générés par Terraform automatiquement : évitez d’y toucher, c’est un risque à perdre des données importantes.
Définir un fournisseur
Avant toute chose, dans un projet tout neuf, on indique à Terraform quel fournisseur utiliser :
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.27"
}
}
required_version = ">= 1.0"
}
provider "aws" {
profile = var.aws_profile
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
}
}
}
Il s’agit ici d’indiquer à Terraform que l’on souhaite utiliser la version 1.0
ou plus de l’outil, avec le provider
aws
(version 3.27
ou version mineure supérieure).
En lançant terraform init
il sera alors installé et prêt à l’usage.
On définit ensuite des informations pour initialiser le provider : le profil AWS (défini dans la configuration de l’outil en ligne de commande d’AWS) à utiliser, la région dans laquelle on veut travailler par défaut… et quelques étiquettes (tags
) à appliquer par défaut aux ressources.
Utiliser des variables
Les plus attentifs ont pu remarquer la syntaxe var.xxxx
pour faire appel à une variable.
Définition
On commence donc par les définir :
variable "project_name" {
description = "Nom du projet"
type = string
default = "tutoriel"
validation {
condition = length(var.project_name) <= 12 && length(regexall("[^a-zA-Z0-9-]", var.project_name)) == 0
error_message = "La variable 'project_name' doit faire moins de 12 caractères, et contenir uniquement des lettres, chiffres et tirets."
}
}
variable "environment" {
description = "Nom de l'environment actuel"
type = string
default = "test"
validation {
condition = length(var.environment) <= 12 && length(regexall("[^a-zA-Z0-9-]", var.environment)) == 0
error_message = "La variable 'environment' doit faire moins de 12 caractères, et contenir uniquement des lettres, chiffres et tirets."
}
}
variable "aws_region" {
description = "Région AWS du déploiement"
type = string
default = "eu-west-3" # eu-west-3 pour Paris
}
variable "aws_profile" {
description = "Nom du compte AWS CLI (~/.aws/credentials)"
type = string
default = "default" # Pensez à renseigner le nom de votre compte pour la CLI AWS
}
Les blocs variable
permettent donc d’indiquer que l’on souhaite définir une nouvelle variable dont le nom est dans les guillemets qui suivent, avec une description
, un type
et une éventuelle valeur par défaut (default
).
On peut également y inclure un bloc validation
pour vérifier le format de la variable en question, par exemple sa longueur, les caractères contenus, ou une sous-chaîne… On indique alors une condition
ainsi qu’un message d’erreur (error_message
). Plus d’infos sur la validation de variables.
Injection
On peut alors créer un fichier test.tfvars
pour y définir les valeurs correspondantes spécifiques à cet environnement :
project_name = "tutoaws"
environment = "tests"
aws_region = "eu-west-3" # Pas obligé de re-définir si on veut garder la valeur par défaut
aws_profile = "mon-compte-aws"
aws_account_id = 1234567890
Pour utiliser ce fichier pensez à l’indiquer à Terraform : terraform plan -var-file="./test.tfvars"
ou terraform apply -var-file="./test.tfvars"
.
Les Locales
Un peu sur le même principe, le mot-clé local
permet de calculer une valeur et de l’utiliser comme une variable.
On peut par exemple calculer un préfixe pour les noms des ressources :
locals {
prefix = "${var.project_name}-${var.environment}"
}
On peut alors l’utiliser directement, par exemple en important une paire de clés de chiffrement :
resource "aws_key_pair" "main" {
key_name = local.prefix
public_key = var.ssh_public_key
}
Il faut donc s’assurer d’avoir une clé tutoaws-tests
sur le compte AWS et d’avoir extrait la partie publique (qui commence par ssh-rsa
) pour la mettre dans une nouvelle variable :
variable "ssh_public_key" {
description = "Clé publique pour la connexion SSH aux instances EC2 (normalement installé dans ~/.ssh/tutoaws-tests.pem)"
default = ""
validation {
condition = length(var.ssh_public_key) >= 380 && substr(var.ssh_public_key, 0, 8) == "ssh-rsa "
error_message = "La variable 'ssh_public_key' dit faire au moins 380 caractères et commencer par 'ssh-rsa '."
}
}
Importer des ressources existantes
Que se passe-t-il si la ressource existe déjà ?
Si la clé SSH en question a déjà été créée par AWS, vous pouvez demander à Terraform de l’importer pour éviter qu’il ne tente d’en créer une nouvelle qui pourrait rentrer en conflit :
terraform import -var-file="./test.tfvars" aws_key_pair.main tutoaws-tests
Ici aws_key_pair.main
correspond à la ressource Terraform à importer, et tutoaws-tests
au nom de la clé sur la console AWS. Terraform saura maintenant que la clé existe déjà, vous évitant ainsi des erreurs à l’application de la configuration.
Créer des ressources dépendantes
Jusqu’ici rien de bien difficile, on a créé une simple clé de chiffrement. Et si on voulait créer plusieurs ressources… avec des dépendances entre elles ?
Eh bien on peut faire référence à un attribut d’une ressource comme on utiliserait une variable !
Prenons le cas d’une instance EC2 (un genre de VPS) qui doit faire partie d’un groupe de sécurité :
resource "aws_security_group" "default" {
name = "${local.prefix}-default"
description = "Groupe de sécurité par défaut"
egress { # On autorise toutes les connexions sortantes
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
ingress { # On autorise les connexions SSH entrantes
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Vous pouvez indiquer votre IP ici pour restreindre l'accès
}
}
resource "aws_instance" "test" {
instance_type = "t2.micro"
ami = var.ec2_instance_ami
security_groups = [aws_security_group.default.id] # Sera déterminé une fois le groupe en question créé
key_name = aws_key_pair.main.key_name # On autorise notre clé SSH
tags = {
Name = "${local.prefix}-instance"
}
}
En n’oubliant pas de définir notre variable :
variable "ec2_instance_ami" {
description = "Identifiant de l'image AMI des instances MongoDB"
type = string
default = "ami-01f14919ba412de34"
# NOTE: on peut ajouter une validation ici… 😉
}
Faire sortir des infos
Une fois votre configuration appliquée, il peut être intéressant de récupérer des valeurs, notamment celles qui sont générées, pour pouvoir retrouver facilement certaines informations.
On peut alors utiliser un bloc output
:
output "environment" {
description = "Nom de l'environnement actif"
value = var.environment
}
output "ssh_key_arn" {
description = "ARN de la clé SSH"
value = ssh_public_key.arn
}
output "security_group_arn" {
description = "Identifiant du groupe de sécurité"
value = aws_security_group.default.arn
}
On peut alors appliquer tout ça :
terraform apply -var-file="./test.tfvars"
Et voilà, vous êtes prêts à pratiquer l'infra-as-code ou IaC !
Tout supprimer et partir en retraite
Enfin… vous êtes pas obligés non plus, mais ça peut être pratique de supprimer toutes ses ressources si c’était juste pour tester, histoire de ne pas payer pour rien :
Cette commande détruit toutes les ressources gérées par Terraform, sans exception. Y compris celles qui ont juste été importées.
Elle ne touche (heureusement) pas aux autres, mais ça reste destructeur.
terraform destroy -var-file="./test.tfvars"