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 !