Aller au contenu principal

Ecrire du code Terraform

L'importance de maîtriser l'écriture de code Terraform réside dans sa capacité à simplifier la gestion de configurations complexes de ressources à travers divers fournisseurs de services cloud. Cela inclut non seulement la création et la suppression de ressources, mais aussi leur mise à jour et leur interconnexion de manière sécurisée et efficace. Dans ce guide Vous apprendrez à structurer un projet Terraform, à manipuler des variables et à appliquer les meilleures pratiques qui vous aideront à écrire des scripts robustes et maintenables.

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.

attention

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_ed25519.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
  }
}

Écriture de votre premier script Terraform

Après avoir installé et configuré Terraform et compris les prérequis nécessaires, il est temps de passer à l'action et d'écrire votre premier script Terraform. Dans cette section, je vais vous guider à travers la création d'une configuration simple pour déployer une instance de serveur virtuel sur un fournisseur de cloud courant, comme AWS.

Création du fichier principal

Tout d'abord, créez un fichier nommé main.tf. Ce fichier contiendra toute la configuration nécessaire pour déployer votre ressource. Ouvrez ce fichier dans votre éditeur de texte ou IDE préféré.

# main.tf
provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

Explication du code

  1. Déclaration du fournisseur : La première partie du script définit le fournisseur de cloud, ici AWS (provider "aws"). Vous devez spécifier la région où vous souhaitez déployer vos ressources, dans cet exemple, us-east-1.

  2. Ressource : La seconde partie du script crée une ressource, spécifiquement une instance EC2 (resource "aws_instance" "example"). Vous devez fournir un AMI (Amazon Machine Image) et un type d'instance. L'AMI sert de modèle pour l'instance, et le type d'instance détermine la puissance de calcul, la mémoire, et d'autres caractéristiques de l'instance.

Exécution du script

Avec votre script prêt, vous pouvez maintenant lancer les commandes Terraform pour déployer votre instance. Ouvrez un terminal, naviguez vers le répertoire contenant votre fichier main.tf, et exécutez les commandes suivantes :

terraform init
terraform plan
terraform apply
  • terraform init initialise le répertoire de travail de Terraform et télécharge le plugin nécessaire pour interagir avec le fournisseur spécifié, dans ce cas, AWS.
  • terraform plan affiche un plan d'exécution, montrant ce qui sera créé, modifié, ou détruit.
  • terraform apply applique les modifications nécessaires pour atteindre l'état désiré défini dans votre configuration.

Vérification

Après l'exécution de terraform apply, Terraform vous fournira un résumé des actions réalisées et des ressources déployées. Vous pouvez vous connecter à votre console AWS pour vérifier que l'instance est bien en cours d'exécution.

Félicitations, vous avez écrit et déployé votre premier script Terraform ! Cette expérience de base est un tremplin vers la création de configurations plus complexes et la gestion de diverses ressources cloud. Continuez à explorer et à expérimenter avec Terraform pour mieux comprendre et exploiter sa puissance et sa flexibilité.

Meilleures Pratiques

Lors de la rédaction de configurations Terraform, l'adoption de meilleures pratiques peut grandement améliorer la maintenabilité, la clarté et l'efficacité de votre code. Voici quelques conseils essentiels :

Organisation et Structure du Code

Une bonne organisation du code est cruciale. Séparez vos configurations en fichiers et modules logiques. Utilisez des fichiers distincts pour les variables, les outputs et les configurations de ressources principales. Les modules réutilisables permettent de simplifier les configurations complexes.

Nommer Clairement les Variables et Ressources

Choisissez des noms descriptifs et cohérents pour vos variables, ressources et modules. Cela facilite la compréhension du code par d'autres développeurs et par vous-même dans le futur. Par exemple, préférez instance_type_production à it_prod.

Utilisation Judicieuse des Variables et Outputs

Définissez des variables pour les éléments susceptibles de changer, comme les tailles d'instance, les régions ou les noms de balises. Utilisez des outputs pour exposer des informations clés de vos ressources, comme les adresses IP publiques ou les identifiants de ressources.

Commentaires et Documentation

Commentez votre code pour expliquer le "pourquoi" derrière certaines configurations complexes. Documentez également l'utilisation de vos modules et les prérequis de votre infrastructure. Cela rend votre code plus accessible et plus facile à maintenir.

Gestion des Versions et Modules

Utilisez le contrôle de version pour votre code Terraform. Cela permet de suivre les modifications, de collaborer avec d'autres développeurs et de revenir facilement à des versions antérieures si nécessaire. De même, si vous utilisez des modules externes, assurez-vous de verrouiller leur version pour éviter des changements inattendus.

Tests et Validation

Testez vos configurations pour éviter les surprises. Utilisez des outils comme terraform plan pour voir les modifications avant de les appliquer. Pensez également à des tests plus approfondis avec des outils comme Terratest pour valider la logique de vos configurations.

Sécurité et Conformité

Intégrez les considérations de sécurité dès la conception de votre infrastructure. Utilisez des politiques de mots de passe forts, chiffrez les données sensibles et respectez les meilleures pratiques de sécurité pour chaque service cloud utilisé.

Optimisation des Coûts

Soyez conscient des coûts associés aux ressources que vous déployez. Utilisez des tailles d'instance et des services adaptés à vos besoins réels et surveillez régulièrement vos coûts pour éviter des dépenses inutiles.

Conclusion

En suivant ce guide, vous avez pu découvrir comment Terraform permet de décrire l'infrastructure de manière déclarative, ce qui facilite la gestion et l'évolution de vos environnements cloud de manière contrôlée et prévisible.

Je vous encourage à continuer à explorer Terraform en expérimentant avec différentes configurations, en intégrant des modules, et en adoptant les bonnes pratiques que nous avons discutées. Plus vous pratiquez, plus vous serez à l'aise pour utiliser Terraform afin de créer des solutions robustes et évolutives.