Terraform et le provider Libvirt
Publié le :

Dans le premier billet consacré au couple Terraform / Libvirt, nous avions vu comment créer des ressources, mais pas comment enchaîner automatiquement le provisioning. Par provisioning j’entends l’utilisation des fonctions remote-exec et local-exec qui permettent par exemple de lancer des playbooks ansible ou des scripts.
Je bloquais sur :
- Comment attendre que la machine provisionnée ait une adresse IP allouée par le serveur DHCP.
- Comment attendre la fin de la partie cloud-init
Depuis j’ai pas mal bossé sur mon projet de Home Lab Devops ou je cherche à tout automatiser et j’ai fini par trouver les solutions à ces problèmes.
Attendre que la machine ait une adresse IP
Si je reprends la création de la ressource libvirt_domain qui est la machine en
elle-même, telle que je la définissais auparavant.
Ce qu’il manquait, c’était trois choses pour que terraform comprenne que l’adresse IP était définie :
- le paramètre qemu_agent = truequi indique qu’il faut utiliser l’agent qemu pour récupérer l’adresse IP
- le paramètre wait_for_lease = truedans la partie network_interface
- et surtout dans le cloud-init le démarrage du qemu-guest-agent
Ce qui donne :
terraform {  required_providers {    libvirt = {      source = "dmacvicar/libvirt"    }  }}
// instance the providerprovider "libvirt" {  // uri = "qemu:///system"  uri = "qemu+ssh://admuser@devbox3/system"}
// variables that can be overridenvariable "hostname" { default = "test" }variable "domain" { default = "robert.local" }variable "ip_type" { default = "dhcp" } # dhcp is other valid typevariable "memoryMB" { default = 1024 * 1 }variable "cpu" { default = 1 }
// fetch the latest ubuntu release image from their mirrorsresource "libvirt_volume" "os_image" {  name   = "${var.hostname}-os_image"  pool   = "devbox"  source = "impish-server-cloudimg-amd64.img"  format = "qcow2"}
// Use CloudInit ISO to add ssh-key to the instanceresource "libvirt_cloudinit_disk" "commoninit" {  name           = "${var.hostname}-commoninit.iso"  pool           = "devbox"  user_data      = data.template_cloudinit_config.config.rendered  network_config = data.template_file.network_config.rendered}
data "template_file" "user_data" {  template = file("${path.module}/cloud_init.cfg")  vars = {    hostname   = var.hostname    fqdn       = "${var.hostname}.${var.domain}"    public_key = file("~/.ssh/id_ed25519.pub")  }}
data "template_cloudinit_config" "config" {  gzip          = false  base64_encode = false  part {    filename     = "init.cfg"    content_type = "text/cloud-config"    content      = data.template_file.user_data.rendered  }}
data "template_file" "network_config" {  template = file("${path.module}/network_config_${var.ip_type}.cfg")}
// Create the machineresource "libvirt_domain" "domain-alma" {  # domain name in libvirt, not hostname  name       = var.hostname  memory     = var.memoryMB  vcpu       = var.cpu  autostart  = true  qemu_agent = true
  disk {    volume_id = libvirt_volume.os_image.id  }  network_interface {    network_name = "bridged-network"    mac          = "52:54:00:36:14:e5"    wait_for_lease = true  }
  cloudinit = libvirt_cloudinit_disk.commoninit.id
  console {    type        = "pty"    target_port = "0"    target_type = "serial"  }
  graphics {    type        = "spice"    listen_type = "address"    autoport    = "true"  }
terraform {  required_version = ">= 0.12"}
output "ips" {  #value = libvirt_domain.domain-alma  #value = libvirt_domain.domain-alma.*.network_interface  # show IP, run 'terraform refresh' if not populated  value = libvirt_domain.domain-alma.*.network_interface}Et dans le cloud-init complet :
#cloud-config# https://cloudinit.readthedocs.io/en/latest/topics/modules.htmltimezone: Europe/Paris
fqdn: test.robert.localmanage_etc_hosts: trueresize_rootfs: true
users:  - name: admuser    sudo: ALL=(ALL) NOPASSWD:ALL    groups: users, wheel    home: /home/admuser    shell: /bin/bash    lock_passwd: false    ssh-authorized-keys:      - ${public_key}
# only cert auth via ssh (console access can still login)## debug - ssh_pwauth: truedisable_root: falsessh_pwauth: truechpasswd:  list: |    root:passwd    admuser:123456  expire: falsegrowpart:  mode: auto  devices: ['/']packages:  - qemu-guest-agentwrite_files:  - path: /etc/sysctl.d/10-disable-ipv6.conf    permissions: 0644    owner: root    content: |      net.ipv6.conf.all.disable_ipv6 = 1      net.ipv6.conf.default.disable_ipv6 = 1# every bootbootcmd:    - [ sh, -c, 'echo $(date) | sudo tee -a /root/bootcmd.log' ]# run once for setup
runcmd:  - sed -i 's/#UseDNS yes/UseDNS no/' /etc/ssh/sshd_config  - systemctl start qemu-guest-agent  - systemctl restart sshd  - sysctl --load /etc/sysctl.d/10-disable-ipv6.conf  - localectl set-keymap fr  - localectl set-locale LANG=fr_FR.UTF8  - domainname robert.localAttendre que le cloud-init soit terminé.
Maintenant que l’adresse IP est récupérée nous pouvons lancer un
remote-exec avec la commande cloud-init status --wait qui va se charger
d’attendre la fin de la configuration de la machine.
Donc dans le bloc de ressource de la machine nous pouvons ajouter ceci :
// Create the machineresource "libvirt_domain" "domain-alma" {  # domain name in libvirt, not hostname  name       = var.hostname  memory     = var.memoryMB  vcpu       = var.cpu  autostart  = true  qemu_agent = true
  disk {    volume_id = libvirt_volume.os_image.id  }  network_interface {    network_name = "bridged-network"    mac          = "52:54:00:36:14:e5"    wait_for_lease = true  }
  cloudinit = libvirt_cloudinit_disk.commoninit.id
  console {    type        = "pty"    target_port = "0"    target_type = "serial"  }
  graphics {    type        = "spice"    listen_type = "address"    autoport    = "true"  }
  provisioner "remote-exec" {    inline = [      "cloud-init status --wait",    ]
    connection {      host     = "${self.network_interface.0.addresses.0}"      type     = "ssh"      user     = "admuser"      private_key = "${file("~/.ssh/id_ed25519")}"    }  }}Vous remarquerez que dans la définition de la connection j’utilise :
- l’addresse IP de la machine
- la clé privé pour me connecter à la machine.
Lancer un playbook ansible
Pour lancer le playbook Ansible, il faut utiliser un local-exec. Il suffit
de l’ajouter à la suite du précédent remote-exec :
  provisioner "local-exec" {    command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -u admuser -i ${self.network_interface.0.addresses.0}, --private-key ${var.private_key_path} provision.yml"  }}On réutilise l’adresse IP et cette on charge la clé depuis une variable
définie dans le fichier variables.tf (pour montrer les deux façons de la
récupérer.)
Le fichier variables.tf :
variable "private_key_path" {  description = "Path to the private SSH key, used to access the instance."  default     = "~/.ssh/id_ed25519"}Pour cete exemple j’utilise un simple playbook dont le contenu est le suivant :
---- hosts: all  gather_facts: true  become: true  vars:    timezone: Europe/Paris
  tasks:    - name: install packages      ansible.builtin.package:        state: present        name:          - tar          - unzip          - python3-pip          - git          - htop          - net-tools    - name: set as default locale      ansible.builtin.command: localectl set-locale LANG=en_US.UTF-8    - name: Set timezone      community.general.timezone:        name: "{{ timezone }}"Mais on pourrait faire tout le provisonning d’une application comme je le faire sur les application de mon Home Lab: rundeck, powerdns et nexus.
Plus loin
Je vous propose de créer dans le projet home lab un template avec le contenu précédent. Comme ça vous pouvez l’utiliser comme base pour vos projets. Le lien ↗
Je dois encore trouver comment automatiser la récupération de l’image Ubuntu
et le changement de sa taille directement par terraform. En effet par défaut
elle ne fait que 2Go. On ne peut pas utiliser l’url avec le paramètre
size.
