Créer un module scalable avec Terraform

Je profite de la fin du confinement qui n’en finit plus de finir pour continuer de jouer avec ma récente découverte : Terraform. Comme j’en parlais dans de précédents articles, la force de Terraform vient d’une part de sa capacité à pouvoir « dialoguer » avec différents fournisseurs de Cloud, d’autre part de rendre trivial la « scalabilité » de l’infra. On peut parfaitement en cas de montée de charge sur une application, décider de créer par exemple, un serveur supplémentaire dédié au cache (un slave Redis).

L’ancien code

Je prends pour exemple mon précédent code Terraform, on peut y voir différents blocs qui ont pourtant la même fonction (pas bien…) :

resource "libvirt_domain" "domain_nc" {
  name   = var.hostname[0]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true
...

resource "libvirt_domain" "domain_rd" {
  name   = var.hostname[2]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true
...

resource "libvirt_domain" "domain_db" {
  name   = var.hostname[1]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true
...

Les blocs « resource » vont créer des machines virtuelles, en l’occurrence dans mon cas des serveurs Fedora avec des fonctions différentes (appli web, base de donnée, serveur cache). On pourrait tout à fait « factoriser », pour faire cela je vais utiliser une feinte : les « locals ».

Les « locals »

Les variables locales sont basées sur les variables globales, elles vont nous permettre de créer un objet variable : la machine. Je m’explique, voici mes variables globales :

variable "machine_name" {
}
variable "image_url" {
  type = string
  default = "http://nexus.lan:8081/repository/qcow2/Fedora-Cloud-Base-31-1.9.x86_64.qcow2"
}
variable "tld" {
  type = string
  default = "lan"
}
variable "net" {
  type = string
  default = "LAN"
}
variable "ansible_grp" {
  type = string
  default = "lan_rd"
}
variable "user" {
  type = string
  default = "matt"
}
variable "ssh_key_path" {
  type = string
  default = "/home/matt/.ssh/id_rsa"
}
variable "ssh_public_key" {
  type = string
  default = "ssh-rsa AAAAB3...45DAAZ"
}
variable "server_count" {
}
variable "server_name" {
  type = string
  default = "fed"
}
variable "disks_enabled" {
  type =  list
}
variable "memoryMB" {
  type = number
  default = 4096
}
variable "cpu" {
  type = number
  default = 4
}
variable "rootdiskBytes" {
  type = number
  default = 1024*1024*1024*16
}
variable "diskBytes" {
  type = number
  default = 1024*1024*1024*32
}
variable "first_packages" {
  type    = list
  default = ["python3-httplib2", "qemu-guest-agent"]
}

Et maintenant je créé un bloc « locals » dans mon code Terraform (par ex dans main.tf), qui fait appel à ces variables :

locals {
  servers = {
    for s in range(var.server_count) : "${var.server_name}-${var.machine_name}-${s}" => {
        dd     = var.disks_enabled["${s}"]
        ddsize = var.rootdiskBytes
        cpu    = var.cpu
        mem    = var.memoryMB
        name   = "${var.server_name}-${var.machine_name}-${s}"
        fqdn   = "${var.server_name}-${var.machine_name}-${s}.${var.tld}"
        osdisk = "${var.server_name}-${var.machine_name}-${s}-os.qcow2"
        pname  = var.server_name
        cinit  = "${var.server_name}-${var.machine_name}-${s}-commoninit.iso"
        sshkey  = var.ssh_key_path
        user = var.user
        index  = s
        }
  }
}

Factorisation

J’ai donc une boucle for qui est en fait un catalogue de serveurs avec différentes clés/valeurs : le nombre de vCPU, la taille des disques, le nom de la machine… Tout ceci incrémenté par un compteur que j’appelle « server_count ». Cela va me permettre de factoriser les blocs qui se répétaient dans mon précédent code :

data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.tpl")
  for_each = local.servers
  vars     = {
            hostname       = lookup(each.value, "name", "*")
            fqdn           = lookup(each.value, "fqdn", "*")
            user           = var.user
            first_packages = jsonencode(var.first_packages)
            ssh_public_key = var.ssh_public_key
            dd_enabled     = lookup(each.value, "dd", "*")
  }
}

data "template_file" "network_config" {
  for_each = local.servers
  template = file("${path.module}/network_config_dhcp.cfg")
}

resource "libvirt_cloudinit_disk" "commoninit" {
  for_each = local.servers
  name           = lookup(each.value, "cinit", "*")
  pool           = "default"
  user_data      = data.template_file.user_data[each.key].rendered
  network_config = data.template_file.network_config[each.key].rendered
}

resource "libvirt_volume" "server-os" {
  for_each = local.servers
  name     = lookup(each.value, "osdisk", "*")
  pool     = "default"
  source   = var.image_url
  format   = "qcow2"
}

resource "libvirt_volume" "data_disk" {
  name           = "data-disk-xfs"
  pool           = "default"
  size           = var.diskBytes
  format         = "qcow2"
}

resource "libvirt_domain" "redis_vms" {
for_each = local.servers
  name       = each.key
  memory     = each.value["mem"]
  vcpu       = each.value["cpu"]
  qemu_agent = true
  network_interface {
    network_name   = var.net
    wait_for_lease = true
  }

  disk {
    volume_id = libvirt_volume.server-os[each.key].id
  }
  dynamic "disk" {
    for_each = each.value["dd"] ? [libvirt_volume.data_disk.*.id] : []
    content {
        volume_id = "/var/lib/libvirt/images/data-disk-xfs"
    }
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[each.key].id

  console {
  type        = "pty"
  target_type = "serial"
  target_port = "0"
  }

  graphics {
    type        = "spice"
    listen_type = "address"
    autoport    = true
  }
 depends_on = [ libvirt_volume.server-os ]
}
###
terraform {
  required_version = ">= 0.12"
}

Et voilà le tour est joué, je suis passé de 150 lignes de code à 50 lignes, avec la possibilité de faire évoluer facilement le nombre de serveurs puisqu’il dépend de ma variable « server_count ». Vous noterez au passage qu’avec Terraform, il n’est pas facile de créer un disque supplémentaire pour un seul serveur au sein d’une boucle. Pour y parvenir, c’est une autre feinte que j’utilise, dans un bloc dynamique pour les disques (utilisée ici avec le module libvirt pour qemu-KVM, cette astuce est également valable avec le module vsphere) :

  dynamic "disk" {
    for_each = each.value["dd"] ? [libvirt_volume.data_disk.*.id] : []
    content {
        volume_id = "/var/lib/libvirt/images/data-disk-xfs"
    }
  }

C’est basé sur une liste de booléens : ma variable disks_enabled. Dans une loop si c’est Vrai : la valeur est créée, soit ici un disque supplémentaire.

Le module

Pour terminer, on va mettre tout cela dans un module qui ressemble à cela :

[matt@m4800 redis]$ tree
.
├── main.tf
├── modules
│   └── redis-module
│       ├── ansible
│       │   └── hosts
│       ├── cloud_init.tpl
│       ├── main.tf
│       ├── network_config_dhcp.cfg
│       ├── outputs.tf
│       └── variables.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── variables.tf

3 directories, 10 files

Dans mon fichier main.tf principal, je donne mes variables qui me permettront de facilement faire évoluer le nombre de serveurs :

module "redis-cluster" {
   # Source du module
   source = "./modules/redis-module/"
   # Renseignement des variables
   machine_name = "rd"
   server_count = "3"
   disks_enabled = ["true", "false", "false"]
}
output "IPs" {
  value = module.redis-cluster.output_data.addresses
}

Une dernière explication sur le fichier outputs.tf du module qui est appelé par le bloc « output » du point d’entrée main.tf ci-dessus, il faut interroger les valeurs du domaine que l’on vient de créer sinon ça ne fonctionne pas, ce n’est pas forcément trivial à comprendre :

output "output_data" {
  value = {
    addresses = values(libvirt_domain.redis_vms).*.network_interface.0.addresses
  }
}

Et voilà pour cet article qui permet de comprendre une manière de rendre du code Terraform « scalable » au sein d’un module que l’on peut appeler depuis un code plus vaste.

Bon Hacking !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

× 3 = 3

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.