Terraform et le provider Libvirt
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 = true
qui indique qu’il faut utiliser l’agent qemu pour récupérer l’adresse IP - le paramètre
wait_for_lease = true
dans 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.local
Attendre 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
.