Stack Labs Blog moves to Dev.to | Le Blog Stack Labs déménage sur Dev.to 🚀

19 février 2020 | Infrastructure | Xavier Goffin

Terraform: Comment déployer facilement une infrastructure en quelques commandes

Temps de lecture estimé : 12 minutes

Aujourd’hui au menu, Terraform : un outil sympathique qui permet de gérer de l’infrastructure-as-code indépendamment du Cloud que vous utilisez !

Déployer une infrastructure à la main, ça va de “pénible et long” à “carrément diabolique”.

Admettons, votre architecte Cloud a mis ses trois dernières semaines de travail à faire une architecture aux petits oignons et vous tend un magnifique schéma. “Tu peux me déployer ça, s’il-te-plaît?”. Vous regardez le schéma… Des VPC dans tous les coins, assez d’instances EC2 pour gagner sa vie en minant de la cryptomonnaie et ne parlons même pas des lambdas !

Non, définitivement, déployer une architecture à la main, c’est inefficace, pas très agréable pour la personne qui le fait et source d’erreur humaine.

Chacun des grands cloud providers fournit une solution de déploiement automatisé, c’est-à-dire d’infrastructure-as-code. L’intérêt est simple : écrire une série de fichiers descriptifs des différents éléments de l’infrastructure, dits templates, la donner à manger à la solution de déploiement automatisé qui déploie alors une infrastructure respectant exactement les spécifications données dans les templates.

Cela dit, que vous utilisiez CloudFormation (AWS), Deployment Manager (GCP), ou Azure Resource Manager (Azure), vous devez développer un template particulier pour chacun de ces outils. Heureusement, Terraform est là pour proposer une solution de déploiement unifiée !

Infrastructure-as-code indépendante du cloud

Mais si je n’utilise qu’un seul provider, pourquoi est-ce que je prendrais Terraform?

Il y a, à mes yeux, une myriade de raisons d’utiliser Terraform. La première et une des plus importante est qu’il répond à un besoin réel, et y répond bien. Terraform est un outil multi-cloud : passer d’un déploiement sur AWS à un déploiement sur, par exemple GCP, est faisable en s’inspirant des templates version AWS, réduisant ainsi la charge de travail comparée avec une réécriture ex-nihilo. L’infrastructure ainsi décrite peut être portée sur d’autres plateformes, un peu à la manière du lift-and-shift, en reprenant et en s’inspirant des templates et en les “traduisant”.

De surcroît, une fois le déploiement terminé, Terraform établit un fichier .tfstate. Ce dernier permet à Terraform de facilement reprendre l’état actuel de l’infrastructure et le mettre à jour pour lancer d’autres opérations dessus. Contrairement aux services d’infrastructure-as-code des grandes plateformes de cloud computing, il est requis d’avoir un dossier Terraform en local sur un ordinateur pour exécuter des créations et destructions d’infrastructures, ainsi que des clés d’accès à un utilisateur avec suffisamment de permissions. Après, il est tout à fait possible de stocker les templates et le .tfstate dans le cloud, lorsqu’ils ne servent pas, ou juste pour avoir un backup. On peut également recommander de push le .tfstate avec l’intégralité des fichiers Terraform sur un repo git pour permettre à tout le monde de faire des mises à jour et des modifications sur l’infrastructure déployée.

Un autre avantage est celui de pouvoir voir un rapport prévisionnel du déploiement avant de lancer le vrai déploiement et pendant ce dernier, Terraform affiche également sa progression de manière répétée et détaillée dans le terminal.

Enfin, la documentation fournie par HashiCorp, l’éditeur de Terraform, est très complète, sans compter la quantité impressionnante de fournisseurs de services disponibles. Bref, Terraform est un petit couteau suisse pratique.

Les commandes de base

Initialiser un dossier Terraform

Terraform s’initialise dans un dossier, au choix. C’est le dossier dans lequel il faudra placer l’intégralité des templates avec lesquelles Terraform va travailler. La commande qui effectue cette action est :

$ terraform init

xg@debian:~/terraform$ terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.49.0...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.aws: version = "~> 2.49"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Si Terraform trouve des fichiers au bon format, à savoir .tf pour les templates Terraform, il va initialiser un sous-dossier masqué .terraform dans lequel seront téléchargés les plugins pour le provider sélectionné. Nous allons maintenant voir comment choisir ce provider.

Il est tout à fait possible, avec Terraform, de mettre toute son infrastructure dans un seul fichier .tf. Cependant, pour plus de lisibilité, il est agréable de les trier par type ou par service. Créons donc un ficher provider.tf :

provider "aws" {
profile = "altaccount"
region = "eu-west-1"
}
view raw provider.tf hosted with ❤ by GitHub

Ce fichier sert à dire à Terraform ce que l’on utilise. La syntaxe Terraform ressemble à un croisement entre JSON et YAML, ce qui la rend assez facile à lire et écrire par des humains. L’on notera que Terraform accepte aussi le JSON.

Ici, on demande à Terraform de télécharger le plugin pour gérer des ressources AWS. La variable “profile” sert à indiquer le profil nommé (voir named profiles sur l’AWS CLI) qui sera utilisé pour lancer les commandes (Si vous comptez déployer quoi que ce soit au cours de cet article, pensez à mettre un nom de profil valide). La région spécifiée correspond à la région dans laquelle sera déployée l’infrastructure.

Terraform télécharge donc le plugin et le place dans .terraform. Comme le plugin est assez lourd, il est recommandé d’éviter de le télécharger à répétition et d’éviter de le mettre sur un repo git.

Cette étape peut être effectuée avec ou sans d’autres fichiers .tf. Seuls les blocs “provider” sont lus pour l’initialisation. Il est possible d’en mettre plusieurs à la suite pour interagir avec différentes plateformes.

Notre première infrastructure : un bucket s3

Créons maintenant notre premier objet AWS avec Terraform : un bucket s3 et mettons-y une photo de mon chat ! Pour cela, créons un fichier s3.tf. Une fois de plus, rien (mis à part l’esthétique et la lisibilité) ne nous empêche de le mettre dans le même fichier que le bloc “provider”, mais mieux vaut ranger son espace de travail.

mila my cat

resource "aws_s3_bucket" "my-bucket-example" {
bucket = "my-bucket-example-23413426676567"
acl = "public-read"
}
resource "aws_s3_bucket_object" "picture-of-cat" {
bucket = aws_s3_bucket.my-bucket-example.id
key = "cat.jpg"
acl = "public-read"
source = "./cat.jpg"
}
view raw s3.tf hosted with ❤ by GitHub

Décomposons un peu ce fichier. Deux ressources sont crées avec le mot-clé “resource”. La première est un “aws_s3_bucket”, donc le bucket que nous allons créer. Son nom, pour Terraform, est “my-bucket-example”. Cependant, son paramètre “bucket” est déclaré comme étant “my-bucket-example-23413426676567”. Ceci est dû au fait que les noms des buckets sont uniques. Ceci est le nom du bucket qui sera créé sur notre compte AWS. Le nom donné à la ressource pour Terraform n’est utilisé que par Terraform. Le paramètre “acl” ou “access control list” nous permet de choisir qui peut accéder à ce bucket. Par défaut, le bucket est privé, mais comme j’adore mon chat, nous allons le rendre lisible au public.

La deuxième ressource définie correspond à l’image de chat que l’on veut placer dans le bucket, au préalable téléchargée et placée dans le même dossier. Évidemment, il est possible de la placer ailleurs et de modifier le paramètre “source” en conséquence. Prêtons une attention toute particulière au paramètre “bucket”. En effet, nous aurions pu mettre “my-bucket-example-23413426676567”. Cependant, dans le cas où le nom du bucket est variable, il est possible d’y faire référence par son nom Terraform, dans ce cas, sans quotes, ce qui permettra à Terraform de trouver le bon nom du bucket pendant le déploiement. Cette résolution de nom s’appelle une référence (voir partie “Références”).

Il est maintenant temps de déployer cette infrastructure ! Commençons par demander à Terraform de nous faire un petit rapport sur ce qu’il va faire. Pour cela, exécutons :

$ terraform plan

xg-debian@debian:~/terraform-example$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.my-bucket-example will be created
+ resource "aws_s3_bucket" "my-bucket-example" {
+ acceleration_status = (known after apply)
+ acl = "private"
+ arn = (known after apply)
+ bucket = "my-bucket-example-23413426676567"
+ bucket_domain_name = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
+ versioning {
+ enabled = (known after apply)
+ mfa_delete = (known after apply)
}
}
# aws_s3_bucket_object.picture-of-cat will be created
+ resource "aws_s3_bucket_object" "picture-of-cat" {
+ acl = "private"
+ bucket = (known after apply)
+ content_type = (known after apply)
+ etag = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ key = "cat.jpg"
+ server_side_encryption = (known after apply)
+ source = "./cat.jpg"
+ storage_class = (known after apply)
+ version_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Terraform nous propose donc une liste ordonnée de tout ce qu’il veut faire. Ceci peut être pratique pour nous rappeler à quoi servaient ces fichiers sans avoir besoin de relire les templates. La commande qui nous intéresse est la suivante :

$ terraform apply

Cette dernière génère le même plan que terraform plan, mais nous demande ensuite confirmation pour déployer l’infrastructure. Lorsqu’on la lui donne, Terraform crée alors les ressources et nous donne le temps requis pour les mettre en place.

Il est ensuite possible de voir notre photo de chat sur le bucket, soit en visitant l’adresse donnée, soit en allant voir directement sur la console AWS.

Lors de la création des ressources, Terraform a créé un fichier terraform.tfstate. Ce fichier traque l’intégralité des ressources gérées par Terraform. Avant chaque opération, Terraform va actualiser le .tfstate. Il est aussi possible de lui faire actualiser le .tfstate manuellement, ainsi que de faire rentrer des ressources créées autrement dans le .tfstate, ce qui permettra à Terraform de les gérer, bien qu’elles n’aient pas été créées avec un template.

Relancer la commande terraform apply permet de comparer l’état actuel de l’infrastructure (mis à jour et enregistré dans le .tfstate) à celui décrit dans le template. Si les deux sont identiques, Terraform se contentera de ne rien faire. Sinon, il proposera de faire correspondre l’infrastructure existente à celle décrite par le template.

Enfin, pour détruire ce bucket (je me doute que vous aimez aussi mon chat, mais vous n’allez pas garder la photo dessus quand-même), il nous reste simplement à lancer :

$ terraform destroy

Une fois le .tfstate mis à jour, Terraform demande confirmation et, le cas échéant, détruit l’infrastructure enregistrée. Vous connaissez désormais la base de l’utilisation de Terraform !

Un exemple complet sur AWS

L’exemple précédent illustrait une utilisation simple des templates de Terraform pour déployer un objet quelconque. Nous allons maintenant nous plonger dans une infrastructure plus complexe afin de couvrir un cas applicable.

Template de base

Considérons la demande d’infrastructure suivante :

example schema

On veut donc créer un VPC et un premier subnet dans lequel on placera deux instances : un “bastion” avec une adresse IP externe dans lequel on pourra se connecter en SSH et un serveur privé auquel on accède depuis le bastion.

Commençons par créer les composants du plus général au plus fin. Les templates vont être assez longues, pour le coup, parce qu’elles créent tout ce qu’il faut pour. En langage Terraform, un commentaire se marque avec #. Créons donc vpc.tf avec des commentaires explicatifs. Jusqu’à ce que l’on se mette à parler d’un autre fichier, tous ces blocs vont ensemble dans vpc.tf.

#Création d'une ressource "my-vpc" qui utilise des adresses ip en 192.168.0.0/24, donc de
#192.168.0.0 à 192.168.0.254 (l'adresse 192.168.0.255 étant celle de broadcast).
resource "aws_vpc" "my-vpc" {
cidr_block = "192.168.0.0/24"
#Desactiver les noms d'hôtes DNS (false par défaut, redondant).
enable_dns_hostnames = false
#Activer le support DNS (true par défaut, redondant).
enable_dns_support = true
#Gestion de l'instance, par défaut. Possible d'avoir un VPC "dedicated", mais nettement plus cher.
instance_tenancy = "default"
#Tags, un système clé:valeur au choix pour trier ses ressources.
tags = {
Name = "my-vpc"
}
}
#Création d'une internet gateway pour garantir l'accès internet depuis ce VPC
resource "aws_internet_gateway" "my-vpc-iwg" {
#Référence au nom du VPC
vpc_id = aws_vpc.my-vpc.id
tags = {
Name = "my-vpc-iwg"
}
}
#Ajout du subnet désiré
resource "aws_subnet" "my-vpc-subnet" {
#A nouveau, le VPC dans lequel on crée le subnet. On préfère une référence à hard-coder le
#nom du VPC, ce qui rend les changements faciles.
vpc_id = aws_vpc.my-vpc.id
#Ce subnet prend tout l'espace CIDR du VPC
cidr_block = "192.168.0.0/24"
#Permet de fixer l'availability zone utilisée, facultatif.
availability_zone = "eu-west-1a"
#Ce paramètre permet de faire en sorte qu'une instance lancée dans ce subnet n'a, par défaut, pas
#d'IP publique sauf si autrement spécifié.
map_public_ip_on_launch = false
tags = {
Name = "my-vpc-subnet"
}
}
#On crée une table de routage afin de garantir que les paquets soient envoyés sur internet
resource "aws_route_table" "my-vpc-routing-table" {
#Il est bien sûr possible de créer plusieurs routes
route {
#Tous les packets
cidr_block = "0.0.0.0/0"
#Redirigés vers l'internet gateway
gateway_id = aws_internet_gateway.my-vpc-iwg.id
}
vpc_id = aws_vpc.my-vpc.id
tags = {
Name = "my-vpc-routing-table"
}
}
#Une particularité de terraform. L'association se fait automatiquement par la console AWS
#généralement, mais il est requis de la déclarer sur terraform.
resource "aws_route_table_association" "my-vpc-routing-table-association" {
#On associe le subnet créé...
subnet_id = aws_subnet.my-vpc-subnet.id
#Avec la table de routage précédente.
route_table_id = aws_route_table.my-vpc-routing-table.id
}
#On crée une access control list pour le VPC (elle sera peu utile ici)
resource "aws_network_acl" "my-vpc-acl" {
#Il est nécessaire d'y assigner le subnet et le VPC désiré
vpc_id = aws_vpc.my-vpc.id
subnet_ids = [aws_subnet.my-vpc-subnet.id]
#Permet de contrôler les entrées: Autorisées de tout port vers tout port, tout protocole, toute IP
ingress {
from_port = 0
to_port = 0
rule_no = 100
action = "allow"
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
#Idem pour les sorties: Tout port, tout protocole, toute IP
egress {
from_port = 0
to_port = 0
rule_no = 100
action = "allow"
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
tags = {
Name = "my-vpc-acl"
}
}
view raw vpc.tf hosted with ❤ by GitHub

Ouf ! C’était fastidieux ! Heureusement, on ne crée pas des VPC tous les jours. Et généralement, les paramètres changent peu. Attaquons-nous maintenant aux instances EC2. Créons donc ec2.tf :

#Créer une paire de clés dans terraform requiert d'utiliser sa clé publique comme matériel. Il est
#recommandé d'utiliser `$ ssh-keygen -t rsa` pour en créer une et la remplacer ici.
resource "aws_key_pair" "my-key" {
key_name = "my-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUNl0PHsE9is7/PB0OilYBt3gUuQOlS4DM1jtN+WVP8DsgufiUWdbIFtKX0Q4P/tvkez5r7XVHOSDPliswatHisDK5aNCjW20VGI1Tf0lljoLVoIHSZgHPgj7L+3v40vLCkrMojpc9c6S5KH2fRh8qKDhMe9Qnw197WSFwItRNq4PsKHZvQQuZIkoolzfp4OwM7rkcazLfD3GW2JWo95Ut/Pw+L0tjx478+7KIsHoz4uyT1N7Lc90rb6EY/d3J1grvgWm82/mAZnFmPVEurnNsMIOAmh+Bv3yhYJQdNFX7mHln6/JFo3ei2tFVna3Yv0sOpnn2+2wlatUMwD8t7+TP example@example"
}
#Permet de déclarer un group de sécurité pour EC2, sans quoi il ne sera pas possible d'y accéder
resource "aws_security_group" "my-security-group" {
#Il est nécessaire de nommer le security group, mais optionnel de lui donner une description
name = "my-security-group"
description = "Allow whitelisted IP in"
#On rattache le security group au VPC créé précédemment
vpc_id = aws_vpc.my-vpc.id
#Sert à définir une règle d'entrée: L'accès à tous les ports, vers tous les ports, tous
#protocoles, est possible depuis un certain bloc CIDR
ingress {
from_port = 0
to_port = 0
protocol = "-1"
#Ceci est une variable! C'est un type particulier de référence que nous aborderons ensuite
cidr_blocks = ["${var.ip}/32"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = []
self = true
}
#Sert à définir une règle de sortie: Il est possible de sortir vers n'importe où, tous protocoles
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
#Créons maintenant notre première instance EC2: le bastion
resource "aws_instance" "my-ec2-bastion" {
#Cette AMI (amazon machine image) correspond à Amazon Linux version 2 pour eu-west-1
ami = "ami-028188d9b49b32a80"
availability_zone = "eu-west-1a"
ebs_optimized = false
#Les t2.micro sont les plus petites instances disponibles. L'avantage: elles rentrent dans
#le free tier de AWS.
instance_type = "t2.micro"
#Inutile de surveiller en détail l'instance (et cela sort du free tier)
monitoring = false
#On utilise la clé générée précédemment
key_name = aws_key_pair.my-key.id
#L'instance est placée dans le subnet créé précédemment
subnet_id = aws_subnet.my-vpc-subnet.id
#Et on utilise le security group qu'on a créé
vpc_security_group_ids = [aws_security_group.my-security-group.id]
#Comme on veut SSH dans l'instance depuis l'internet, il est nécessaire d'associer une IP publique
associate_public_ip_address = true
#Il faut un disque de démarrage pour l'instance. On choisit un disque de la taille minimale (8GB),
#effacé ors de l'arrêt de l'instance, et de type gp2, c'est à dire "general purpose SSD". La
#valeur par défaut étant "standard", ce qui correspond à un disque magnétique, on préfère
#utiliser un SSD.
root_block_device {
volume_type = "gp2"
volume_size = 8
delete_on_termination = true
}
}
#Dernièrement, créons l'autre instance EC2 à laquelle on veut accéder à travers le bastion
resource "aws_instance" "my-ec2-server" {
ami = "ami-028188d9b49b32a80"
availability_zone = "eu-west-1a"
ebs_optimized = false
instance_type = "t2.micro"
monitoring = false
key_name = aws_key_pair.my-key.id
subnet_id = aws_subnet.my-vpc-subnet.id
vpc_security_group_ids = [aws_security_group.my-security-group.id]
#Puisque cette instance doit rester privée, on n'assigne pas d'IP publique
associate_public_ip_address = false
#Enfin, on peut également assigner une IP privée fixée de manière à SSH plus simplement.
private_ip = "192.168.0.28"
root_block_device {
volume_type = "gp2"
volume_size = 8
delete_on_termination = true
}
#Permet de spécifier un script de démarrage. Ici, pour l'exemple, on peut écrire un fichier
#dans /home/ec2-user
user_data = <<-EOF
#!/bin/bash
echo "Hello!" > hello.txt
EOF
}
view raw ec2.tf hosted with ❤ by GitHub

Ouf ! Voilà, l’intégralité de l’infrastructure est déployée. Mais il reste certaines choses qui ne sont pas réglées : il nous faut parler plus en détails des références, aborder les variables (rappelez-vous le ${var.ip} de tout à l’heure) et enfin, récupérer certaines valeurs à la fin du déploiement.

Références, variables, outputs

Références

Nous avons déjà parlé précédemment des références. En quelques mots, une référence est la valeur d’un paramètre fixe d’une autre ressource. Elle s’écrit selon une structure un peu “orientée-objet”, c’est-à-dire qu’on écrit <type de la ressource>.<nom Terraform de la ressource>.<valeur recherchée>. On peut écrire une référence seule sans guillemets, directement dans le template, comme vu précédemment. Par exemple, écrire subnet_id = "aws_subnet.my-vpc-subnet.id" est redondant dans Terraform v0.12.20.

Cependant, ceci n’est vrai que pour les références “seules”. Si une référence est mixée à d’autres caractères, il faut utiliser la structure de variable de Terraform, c’est-à-dire la suivante :

"Resource": "${aws_s3_bucket.my-bucket.arn}/*"

On trouverait ce type de strucure dans une policy IAM, par exemple, où l’on voudrait restreindre l’accès en lecture d’un utilisateur à “my-bucket”.

Puisque la ligne ne se compose pas exclusivement d’une référence, on encadre la référence telle une variable, c’est-à-dire que toute la chaîne de caractères doit être entre guillemets et la référence doit être encadrée par ${}, comme les variables, dont nous allons parler maintenant.

Variables

Supposons que, pour savoir à qui est quelle ressource, nous voulons créer un tag “Nom” attaché à chaque ressource qui est la concaténation du nom Terraform de la ressource et du profil AWS utilisé pour le déployer. Pour éviter de devoir modifier les fichiers à chaque fois, optons pour l’utilisation d’une variable !

Une variable se déclare généralement dans variables.tf. Il est possible de lui attribuer une valeur par défaut et d’en passer une en paramètre à terraform plan, terraform apply ou terraform destroy.

#Nom de profil AWS
variable "profile" {
#Valeur par défaut
default = "default"
}
variable "ip" {
#Sans valeur par défaut
}
view raw variables.tf hosted with ❤ by GitHub

Inclure ce fichier permet à Terraform d’interpréter, lors de terraform plan, terraform apply ou terraform destroy, l’instruction "${var.profile}" comme une valeur passée en paramètre, ou sinon, comme “default”. Pour passer une valeur en paramètres, on utilise :

$ terraform apply -var="profile=my-profile"

On peut alors ajouter le bloc suivant à n’importe quelle ressource :

tags = {
Name = "my-resource-name-${var.profile}"
}
view raw tag.tf hosted with ❤ by GitHub

La variable est ainsi concaténée avec le nom de notre choix et peut être changée pour chaque déploiement. Évidemment, il est possible d’écrire autant de blocs variable "" {} que voulu. On dispose également d’une variable “ip” qui sert à écrire le security group et s’assurer que nous seuls pouvons accéder à notre instance.

Outputs

Il est également intéressant de récupérer certaines valeurs attribuées lors du processus de déploiement. Tout ceci est faisable sans avoir besoin de faire des appels à l’API de AWS. En effet, c’est ce à quoi sert un fichier output.tf ! Dans notre cas, nous voulons récupérer l’adresse IP publique du bastion que nous avons créé. Voyons donc sa structure de base :

output "bastion-ip" {
value = aws_instance.my-ec2-bastion.public_ip
}
view raw output.tf hosted with ❤ by GitHub

Une fois l’output déclaré, Terraform l’affichera de lui-même après un terraform apply. Pour cela, on peut utiliser la commande $ terraform output bastion-ip pour obtenir directement la valeur dans notre shell, voire pour l’assigner à une variable locale. Évidemment, il est possible d’écrire autant de blocs output "" {} que voulu.

Voilà une sacrée aventure. Nous pouvons donc placer tous ces fichiers Terraform (provider.tf, vpc.tf, ec2.tf, variables.tf, output.tf) dans un même dossier, avec la clé SSH générée précédemment. A partir de là :

$ terraform apply -var="profile=$YOUR_PROFILE" -var="ip=$YOUR_IP"

Il vous sera demandé de confirmer le déploiement. Une fois que celui-ci est terminé, l’IP publique du bastion sera affichée. A partir de là :

Cliquer pour copier
scp -i key ./key ec2-user@$BASTION_IP:/home/ec2-user ssh -i key ec2-user@$BASTION_IP ssh -i key ec2-user@192.168.0.28 cat hello.txt

On peut donc bien accéder à nos deux instances et l’on retrouve bien le fichier que l’on a créé à l’aide du script de lancement.

Voilà ! Vous avez maintenant une bonne idée de comment créer une infrastructure de base avec Terraform, que cela soit dans un VPC créé pour l’occasion, ou en utilisant votre propre VPC.

La dimension multi-cloud

Intéressons-nous maintenant à la possibilité de changer de fournisseur en “adaptant” un template existant en un autre. Évidemment, l’adaptation d’un template à un autre fournisseur est une opération unique à chaque infrastructure et dépendant fortement des deux fournisseurs de services impliqués. Nous allons donc aborder un cas général.

Abordons donc un petit exemple de ce qui rend Terraform si intéressant pour le multi-cloud. Intéressons-nous à l’aide de Terraform pour les bucket S3 de AWS et pour les bucket Cloud Storage de GCP.

Terraform supporte une variété de blocs pour ces deux objets. Ces blocs sont souvent identiques, par exemple website, versioning, cors, lifecycle_rule, logging. Prenons l’exemple d’un bucket sensé servir de site web statique.

resource "aws_s3_bucket" "bucket" {
bucket = "my-bucket-on-aws"
acl = "public-read"
policy = "${file("bucketpolicy.json")}"
website {
index_document = "index.html"
error_document = "error.html"
}
}
view raw s3_aws.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "bucket" {
name = "my-bucket-on-gcp"
location = "EU"
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
}
view raw s3_gcp.tf hosted with ❤ by GitHub

Pour des fonctionnalités simples, les deux templates sont largement similaires. Évidemment, dès lors que l’on rentre dans des fonctionnalités plus complexes, les descriptions divergent. Cela dit, pour une bonne partie du template, il est facile de créer un script ou parseur pour “traduire” un template AWS en template GCP, ou vice versa, même si finalement, le changement reste non-trivial.

Conclusion

Vous avez pu voir au cours de cet article que finalement, Terraform s’utilise relativement facilement et permet de déployer des copies d’une infrastructure à l’identique de manière répétable. Les templates sont faits pour être lisibles et faciles à éditer et la documentation de HashiCorp sur Terraform est complète et détaillée. En somme, Terraform est une fantastique corde à avoir à son arc pour toute problématique de déploiement d’infrastructure !