Introduction à terraform avec docker
Installer terraform
Pour installer terraform, il vous suffit d’aller sur le site officiel et de télécharger le binaire en fonction de votre système et de votre architecture CPU.
Vous pouvez vérifier sa bonne installation avec la commande
$ terraform --version
Terraform v1.1.3
on darwin_amd64
Syntaxe et configuration
Assignation d’un argument ou d’une variable
image_id = "1234"
Definir un block de ressource
resource "container" "example" {
name = "1234"
network_interface {
# ...
}
}
Les commentaires
#
//
/* */
Les variables
"1234" // Chaine de caractère
1234 // Numéros
true / false // Booléens
[0,5,2,4,5,4] // list(type) simple liste d'un certain type
[0,2,4,5] // set(type) trie les éléments d'une liste
[0, 'string', false] // tuple([,...])
{ "key" = "value" } // map(type) paire clé - valeur
{ a = "value", b = 10 } // object({<attr_name>=<type>, ...}) paire clé - valeur
Configuration du provider docker
Architecture
|-- main.tf
|-- provider.tf
provider.tf
Contient la configuration des providers terraform à utiliser. Ici nous demandons à terraform d’utiliser le provider kreuzwerker/docker
dans sa version 2.19.0
sous alias docker
.
Ensuite, nous avons besoin de dire à ce provider d’utiliser le socket docker local unix:///var/run/docker.sock
. Nous pourrions très bien utiliser une autre méthode de connexion à docker comme définit dans la documentation du provider.
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "2.19.0"
}
}
}
provider "docker" {
host = "unix:///var/run/docker.sock"
}
Pour initialiser le provider terraform exécutez la commande ci-dessous. Cela va télécharger le provider docker.
$ terraform init
Installer un premier container
main.tf
resource "docker_image" "ubuntu" {
name = "ubuntu:precise"
}
resource "docker_container" "ubuntu" {
name = "foo"
image = docker_image.ubuntu.latest
}
Dans un premier temps nous devons définir une image docker avec la définition de block ressource docker_image
(documentation du block) en précisent le nom de l’image et sa version. Nous pouvons lui préciser l’argument keep_locally = true
si nous ne vous pas supprimer l’image dans le cas où nous voulons détruire l’infrastructure.
La définition du container se fait avec la définition d’un block docker_container
. Ce bloque prend plusieurs arguments:
name
qui sera le nom de votre container dockerimage
qui corresponse à une référence de ressourcedocker_image
Afin d’appliquer les changements apportés dans les fichiers de définition terraform, exécutez la commande puis confirmez avec yes.
$ terraform apply
yes
Si vous voulez juste avoir une idée des changements que ça apporter terraform sans appliquer de modification à son state:
$ terraform plan
le state
A chaque modification, terraform va référencer toutes les ressources créées avec leur état courant dans un fichier terraform.state. Il s’en sert pour tracker chaque ressource afin de savoir s’il doit la créer, la modifier ou la supprimer. Ne modifiez jamais ce fichier vous-même pour ne pas risquer de corrompre le state.
Il est aussi possible de stoquer ce fichier state autre part qu’en local pour son utilisation dans une CI par exemple. Pour plus d’informations voici la documentation sur la configuration backend de terraform.
Installer postgres et adminer
Dans cette parti, nous allons voir une utilisation plus concrète de terraform à travers des modules pour rendre notre code plus propre et réutilisable. Voici une évolution de la structure de dossier précédente.
|-- main.tf
|-- provider.tf
|-- modules
| |-- postgres
| | |-- provider.tf
| | |-- postgres.tf
| | |-- adminer.tf
Nous avons donc un dossier modules
qui contiendra tous nos modules puis un module postgres
avec ces fichiers de définitions. provider.tf
avec le même require_provider
que celui de base. postgres.tf
qui définira nos ressources liées à postres et adminer.tf
qui définira nos ressources liées à adminer.
main.ts
module "postgres" {
source = "./modules/postgres"
}
Nous initialisons le module postgres en lui fournissant la source du module avec l’argument source
. Chaque ajout de module nécessite un terraform init
.
posgtes.tf
resource "docker_image" "postgres" {
name = "postgres"
keep_locally = true
}
resource "docker_container" "postgres" {
name = "postgres"
image = docker_image.postgres.latest
shm_size = 4000
ports {
external = 5432
internal = 5432
}
volumes {
container_path = "/var/lib/postgresql/data"
host_path = abspath("tmp/postgres")
}
env = [
"POSTGRES_USER=user",
"POSTGRES_PASSWORD=password"
]
}
Nous retrouvons donc la définition de notre docker_image
et de docker_container
.
Des argument supplémentaires on été ajouté au container:
shm_size
qui définit le shym du container. obligatoire pour un fonctionnement de potgres en local- Un block
ports
avec in mapping de portexternal
etintercom
- Un block
volumes
avec un mapping de volume depuiscontainer_path
vershost_path
qui est un chemin local. chemin qui doit être absolu d’où l’utilisation de la fonctionabspath
qui transform les chemins relatifs en absolu. - Une liste
env
pour définir les variables d’environnements du container. IciPOSTGRES_USER
etPOSTGRES_PASSWORD
.
Si vous connaissez un peu docker, ces arguments vous seront surement familier.
adminer.tf
resource "docker_image" "adminer" {
name = "adminer"
keep_locally = true
}
resource "docker_container" "adminer" {
name = "adminer"
image = docker_image.adminer.latest
ports {
external = 8080
internal = 8080
}
env = [
"ADMINER_DEFAULT_SERVER=${docker_container.postgres.name}"
]
depends_on = [
docker_container.postgres
]
}
Pour adminer c’est sensiblement la même chose mais avec quelques arguments supplémentaires:
depends_on
qui nous permet de définir une dépendance à une autre ressource terraform afin que cette ressource ne se créer que si la dépendance a bien été créé aussi.- Nous retrouvons aussi dans les
env
, un appel à la ressource postgres à traversdocker_container.postgres.name
afin de récupérer le nom du container.
les outputs
Chaque ressource terraform (ressource, data ou modules) peut avoir des output
qui sont des références à des propriété internes de la ressource terraform. Ici nous avons la ressource docker_container.postgres
suivi d’un output name
.
Dans notre module nous pouvons très bien définir un fichier output.tf
avec le contenu suivant
output "postgres_container_name" {
value = docker_container.postgres.name
}
Récupérable à travers l’initialisation du module module.posgres.postgres_container_name
pour être utilisé dans une autre ressource terraform, mais nous n’en avons pas l’utilité ici.
Plus de flexibilité
Maintenant nous allons voir comment passer des arguments à notre module. Dans un premier temps nous allons créer un nouveau network docker afin de mettre tous nos containers dedans. Ensuite nous réattribueront les ports de postgres et adminer.
Le network
Ajoutez une nouvelle ressource docker_network
dans main.tf
que nous nommerons local
pour l’exemple. Ensuite passez le nom du network en argument du module postgres.
resource "docker_network" "default" {
name = "local"
}
module "postgres" {
source = "./modules/postgres"
network_name = docker_network.default.name
}
Afin que terraform reconnaisse cet argument au niveau du module postgres, nous allons devoir créer un nouveau fichier variables.tf
dans modules/postgres
qui contiendra la définition de network_name
de type string
non optionnel (nous pouvons le rendre optionnel si besoin en ajouten nullable = true
en argument de la variable).
variable "network_name" {
type = string
// nullable = true
}
Ensuite nous allons utiliser cette variable afin d’assigner nos containers dans le network créé plus haut et en profiter pour les renommer pour mieux les reconnaître. Rendez vous dans modules/postgres/posgres.tf
.
A l’intérieur d’un module, les variables sont accessibles seulement à l’intérieur de ce module avec le mot-clé var
. Pour sortir une valeur d’un module, vous devez utiliser les output
terraform comme vu globalement plus haut.
resource "docker_container" "postgres" {
name = "${var.network_name}-postgres"
image = docker_image.postgres.latest
shm_size = 4000
ports {
external = 5432
internal = 5432
}
volumes {
container_path = "/var/lib/postgresql/data"
host_path = abspath("tmp/postgres")
}
env = [
"POSTGRES_USER=user",
"POSTGRES_PASSWORD=password"
]
networks_advanced {
name = var.network_name
}
}
Les nouveautés sur le container:
- Nous avons modifié le
name
afin de lui include le nom du network avecvar.network_name
.${...}
concatène une chaine de caractère dans une autre chaine de caractère - La définition d’un nouvel argument
networks_advanced
de type block pour l’assignation du réseau définit dans la documentation