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 !


Note sur l’article

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

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
    }
  }
}
provider.tf

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
}
variables.tf

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
test.tfvars

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}"
}
main.tf

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
}
main.tf

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 '."
  }
}
variables.tf

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"
  }
}
main.tf

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… 😉
}
variables.tf

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
}
outputs.tf
C’est parti !

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 :

Attention, danger

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"