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 docker
  • image qui corresponse à une référence de ressource docker_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.tfqui 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 port external et intercom
  • Un block volumes avec un mapping de volume depuis container_path vers host_path qui est un chemin local. chemin qui doit être absolu d’où l’utilisation de la fonction abspath qui transform les chemins relatifs en absolu.
  • Une liste env pour définir les variables d’environnements du container. Ici POSTGRES_USER et POSTGRES_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_onqui 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 à travers docker_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.tfavec 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 avec var.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