Quick Start KVM libvirt VMs with Terraform and Ansible – Part 1

Eine virtuelle Infrastruktur per Code zu managen erleichtert einem Administrator die tägliche Arbeit enorm. Gerade wenn es um die Bereitstellung von Testumgebungen, wie zum Beispiel Cluster Servern geht, die möglicherweise wieder komplett eingerissen und neu installiert werden sollen.

Unter Linux ist KVM (Kernel Virtual Machine) und libvirt ein Standard für Server-Virtualisierung, unter anderem weil es sehr ressourcenschonend arbeitet und voll integriert in allen Linux-Derivaten zur Verfügung steht. Mit den Tools Virt-Manager (GUI) oder virsh (commandline) lassen sich damit schnell einzelne VMs installieren und managen. Was aber, wenn eine Projektanforderung zum Beispiel lautet: Ein Kafka Cluster aus 8 VMs mit 3 x Broker, 3 x Zookeeper, 1 x Control Center und 1 x KSQL, mit unterschiedlicher RAM-, CPU- und Diskkonfiguration?
Puh, das macht ja richtig Arbeit, da geh ich lieber in die Cloud 😉

Hier spielen die beiden Tools Terraform und Ansible auch in On-Premise-Umgebungen ihre Stärken aus. Terraform von Hashicorp ist mittlerweile DAS Standard Werkzeug für Infrastructure as Code (IaC), weil es viele Plugins, sogenannte Provider für die unterschiedlichsten Devices wie VMs, aktive Netzwerkkomponentnen, Container und alle möglichen Cloud Umgebungen mitbringt, darunter auch einer für libvirt. Der Admin beschreibt dann quasi nur noch mit ein paar Zeilen Code, wie seine Umgebung (Server, Netzwerk) aussehen soll. Er legt z.B. die RAM/Disk Ausstattung die VMs fest, welche Linux-Distribution und Netzwerkkonfiguration er haben möchte und terraform kümmert sich um das eigentliche Deployment. So lässt sich die Umgebung auch schnell wieder einreißen und von neuem beginnen.

Ansible (OpenSource/RedHat) setzt dann noch eines oben drauf und installiert auf den vorbereiteten Systemen den eigentlichen Software Stack mit allen Konfigurationen, Gruppen, User etc. Beide Tools überlappen sich dabei an der einen oder anderen Stelle.

Genug der Vorrede! Ich zeige euch nun im ersten Schritt, wie man ein einfaches Terraform Modul selbst schreibt, um damit sehr elegant eine virtuelle Serverumgebung mit CentOS (oder einer Linux Distribution eurer Wahl) unter KVM/libvirt automatisiert zu installieren. Im zweiten Teil gehe ich dann auf die weitere Konfiguration per Ansible ein.

Es wird davon ausgegangen, dass bereits eine funktionierende KVM/libvirt-Umgebung mit einem default network (NAT) und mindestens einem Diskpool zur Verfügung steht.

Installation Terraform

Das Deployment per Terraform dauert nur wenige Sekunden.

Meine Installationsanleitung wurde unter Fedora 32 getestet, ist aber so ähnlich auch auf anderen Linux Distributionen durchführbar.
Bei der Installation von Terraform halten wir uns an die Installationsanleitung auf der Hashicorp Website, die sehr einfach von statten geht.

 juergen  ~  sudo dnf install -y dnf-plugins-core
[sudo] Passwort für juergen: 
Letzte Prüfung auf abgelaufene Metadaten: vor 0:43:34 am Mo 16 Nov 2020 10:43:23 CET.
Das Paket dnf-plugins-core-4.0.18-1.fc32.noarch ist bereits installiert.
Abhängigkeiten sind aufgelöst.
Nichts zu tun.
Fertig.

 juergen  ~  sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/fedora/hashicorp.repo
Paketquelle von https://rpm.releases.hashicorp.com/fedora/hashicorp.repo wird hinzugefügt

 juergen  ~  sudo dnf -y install terraform
Hashicorp Stable - x86_64                                                                                             1.6 MB/s | 363 kB     00:00    
Abhängigkeiten sind aufgelöst.
======================================================================================================================================================
 Package                             Architecture                     Version                               Repository                           Size
======================================================================================================================================================
Installieren:
 terraform                           x86_64                           0.13.5-1                              hashicorp                            27 M

Transaktionsübersicht
======================================================================================================================================================
Installieren  1 Paket

Gesamte Downloadgröße: 27 M
Installationsgröße: 82 M
Pakete werden heruntergeladen:
terraform-0.13.5-1.x86_64.rpm                                                                                         3.9 MB/s |  27 MB     00:06    
------------------------------------------------------------------------------------------------------------------------------------------------------
...                                                                                    

Fertig.
 
 juergen  ~  terraform -install-autocomplete

Terraform libvirt Provider installieren

Anschließend müssen wir nur noch den libvirt Provider für Terraform installieren. Das ist quasi das „Plugin“ und die Schnittstelle, was die Terraform Befehle in entsprechende libvirt Kommandos übersetzt.

Zu finden ist der Provider hier: https://github.com/dmacvicar/terraform-provider-libvirt.

Zuerst prüfen wir jedoch die Voraussetzungen auf dem System.

juergen  ~  virsh version --daemon
Kompiliert gegen die Bibliothek: libvirt 6.1.0
Verwende Bibliothek: libvirt 6.1.0

Verwende API: QEMU 6.1.0
Laufender Hypervisor: QEMU 4.2.1
Läuft gegen Dämon: 6.1.0

 juergen  ~  terraform --version
Terraform v0.13.5

Mit Terraform v0.13 hat sich die Art und Weise, wie Provider zur Verfügung gestellt werden geändert. Terraform benutzt jetzt standardmäßig ein Provider-Repository, worüber offiziell unterstützte Provider zur Verfügung gestellt werden. Allerdings gibt es den libvirt Provider dort nicht (Details dazu hier), so dass wir ihn manuell herunterladen und installieren müssen. Der Download der vorkompilierten Version erfolgt hier: https://github.com/dmacvicar/terraform-provider-libvirt/releases und die Installation orientiert sich daran: https://github.com/dmacvicar/terraform-provider-libvirt/blob/master/docs/migration-13.md


 juergen  ~  wget https://github.com/dmacvicar/terraform-provider-libvirt/releases/download/v0.6.3/terraform-provider-libvirt-0.6.3+git.1604843676.67f4f2aa.Fedora_32.x86_64.tar.gz

 juergen  ~  tar zxvf terraform-provider-libvirt-0.6.3+git.1604843676.67f4f2aa.Fedora_32.x86_64.tar.gz 
terraform-provider-libvirt

 juergen  ~  mkdir -p ~/.local/share/terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.3/linux_amd64

 juergen  ~  mv terraform-provider-libvirt ~/.local/share/terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.3/linux_amd64/

 juergen  ~  ll ~/.local/share/terraform/plugins/registry.terraform.io/dmacvicar/libvirt/0.6.3/linux_amd64
insgesamt 34440
-rwxr-xr-x. 1 juergen juergen 35265880 10. Nov 19:05 terraform-provider-libvirt
 juergen  ~  

Damit wäre die Installation abgeschlossen.

Erste Schritte mit Terraform libvirt Provider

Für einen ersten Test erstellen wir zuerst ein leeres Modul-Verzeichnis (test) und erstellen dort eine Datei libvirt.tf (.tf bezeichnet ein Terraform File).
Jedes Terraform Modul muss zuerst die benötigten Provider deklarieren, was folgend mit dem Block required_providers {} getan wird und in unserem Fall den libvirt Provider enthält. Weiterhin wird eine URI der virtuellen Server Ressource benötigt. Schließlich folgt die eigentliche Ressourcendefinition, die hier zum Test erst mal nur einen Namen beinhaltet.

juergen  ~  Scripts  terraform  mkdir test
juergen  ~  Scripts  terraform  cd test

juergen  ~  Scripts  terraform  test  vi libvirt.tf 

# Deklarieren des libvirt Providers (einmal pro Modul)
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.6.3"
    }
  }
}

# Definieren der virtuellen Server Resource (einmal pro Modul)
provider "libvirt" {
    uri = "qemu:///system"
}

# Die eigentliche Resourcenbeschreibung (was soll dieses Modul tun)
resource "libvirt_domain" "terraform_test" {
  name = "terraform_test"
}

Danach können wir dieses Terraform Modul initialisieren und uns den Terraform Plan ausgeben lassen.
Ein neues Terraform Modul wird immer mit dem Befehl „terraform init“ erstmalig initialisiert. Mit dem Befehl „terraform plan“ bekommen wir quasi den Bauplan unserer Infrastruktur angezeigt. Aber erst der Befehl „terraform apply“ setzt den Bauplan dann auch tatsächlich um. Wir wollen in unserem Beispiel jedoch nur sehen, ob Terraform und der libvirt Provider richtig installiert sind. Daher beschränken wir uns darauf den Bauplan auf der Console auszugeben.

 juergen  ~  Scripts  terraform  test  terraform init

Initializing the backend...

Initializing provider plugins...
- Finding dmacvicar/libvirt versions matching "0.6.3"...
- Installing dmacvicar/libvirt v0.6.3...
- Installed dmacvicar/libvirt v0.6.3 (unauthenticated)

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.


 juergen  ~  Scripts  terraform  test  terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # libvirt_domain.terraform_test will be created
  + resource "libvirt_domain" "terraform_test" {
      + arch        = (known after apply)
      + emulator    = (known after apply)
      + fw_cfg_name = "opt/com.coreos/config"
      + id          = (known after apply)
      + machine     = (known after apply)
      + memory      = 512
      + name        = "terraform_test"
      + qemu_agent  = false
      + running     = true
      + vcpu        = 1
    }

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Erstes konkretes Beispiel

Lasst uns jetzt aber endlich richtig anfangen und einen ersten virtuellen Server per Terraform Script bereitstellen.

Vorbereitung: Linux Cloud-Image

Libvirt-Provider verwendet zur Bereitstellung eines Betriebssystems speziell für den Cloud Einsatz vorbereitete Cloud-Images, die mit CloudInit initialisiert und konfiguriert werden. CloudInit ist der Standardweg, wie Cloud-Provider unterschiedlichste Distributionen schnell und in großer Anzahl zur Verfügung stellen können. Im Prinzip ist im zugrundeliegenden Betriebssystem-Image der cloud-init Agent installiert, der beim ersten Start das Betriebssystem mit allen nötigen Anpassungen, wie z.B. Hostname, SSH-Keys, User etc. vorbereitet, um so individuelle Umgebungen bereitzustellen.

In meinem Beispiel verwende ich ein CentOS 8 Cloud-Image, was ich von der CentOS Download-Seite herunterlade. Wir benötigen dort konkret das „GenericCloud“ Image. Alternativ könnte aber auch Terraform das Image während der Ausführung des Deployment-Plans zur Laufzeit beziehen. Wenn man öfter und viele VMs mit dem gleichen Basis-Image deployed, dann dauert dieser Prozess allerdings entsprechend länger bei der Ausführung des Terraform Moduls.

Terraform Modul

Dann erstellen wir wieder ein neues Modul-Verzeichnis z.B. „newsrv“ und wechseln dort hinein. Für unser Beispiel benötigen wir konkret drei Dateien: eine Terraform Datei (main.tf), eine Datei für globale Variablen (terraform.tfvars) und eine Datei Namens cloud_init.cfg.

Ich gehe nun auf die einzelnen Dateien genauer ein und erkläre ihren Zweck.

terraform.tfvars

Hierbei handelt es sich um eine zentrale Datei für die Deklaration von Variablen. Ich habe in meinem Beispiel hier z.B. alle Parameter reingepackt, die ich individuell konfigurieren möchte. Man kann hier z.B. weitere Host-Blöcke für weitere VM Instanzen hinzufügen und so sehr leicht eine ganze Serverfarm beschreiben.

Zuerst geben wir dem Projekt jedoch einen Namen. Der wird später in der main.tf verwendet, um Guest-Instanzen eines Projektes logisch zusammenzufassen und von anderen Projekten abzugrenzen. Dann folgt der Pfad bzw. die URL zum Cloud-Image und wir legen den KVM-Diskpool für das Cloud-Image fest. Man kann für die virtuellen VM-Disks (qcow2) und die Cloud-OS Images den selben Diskpool verwenden, aber auch getrennt halten z.B. einen SSD- und HDD-Diskpool.

# Projectname
projectname = "newsrv"

# OS Image
#sourceimage = "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.2.2004-20200611.2.x86_64.qcow2"
sourceimage = "/home/juergen/images/CentOS-8-GenericCloud-8.2.2004-20200611.2.x86_64.qcow2"

# The baseimage is the source diskimage for all VMs created from the sourceimage
baseimagediskpool = "default"

Das Cloud-Image stellt immer das Basis-Betriebssystem-Image (Base-Image) dar und die eigentlichen VMs (Guests) sind damit logisch verknüpft. Im libvirt Umfeld spricht man auch von Disk image chains oder Overlay-Image. Die virtuelle Disk der VM liegt quasi als zusätzliche virtuelle Schicht über dem BaseOS-Image. Das hat den Vorteil, dass das Grundgerüst des Betriebssystems mit den enthaltenen Dateien nicht für jede VM dupliziert werden müssen. Man kann das also als eine Art Deduplizierung auf VM Ebene verstehen. Der libvirt_provider arbeitet immer nach diesem Prinzip. Deshalb wird auch immer ein baseimagediskpool benötigt. Da wir mit dem qcow2 Format für die virtuellen Disks arbeiten, ist die Ausgangsgröße auf der Platte sehr gering, da dort nur die Veränderungen (Zuwachs) zum BaseOS-Image gespeichert werden müssen.

Hier sieht man schön die Dateigrößen einer neuen VM nach dem Ausführen des Terraform Plans (commoninit_newsrv.iso wird zusätzlich von Cloud-Init erzeugt)

 juergen  ~  sudo ls -alh /var/lib/libvirt/images
insgesamt 1,2G
drwx--x--x. 2 root root 4,0K 18. Nov 09:29 .
drwxr-xr-x. 9 root root 4,0K  2. Jun 19:54 ..
-rw-r--r--. 1 qemu qemu 1,1G 18. Nov 09:29 baseosimage_newsrv
-rw-r--r--. 1 qemu qemu 366K 18. Nov 09:29 commoninit_newsrv.iso
-rw-r--r--. 1 qemu qemu 111M 18. Nov 09:31 newsrv.qcow2

Nun folgen in der Datei die Netzwerk- und Domain-Settings. Wir verwenden hier das libvirt default Network, bei dem es sich um das Standard NAT Subnetz handelt. Die Einrichtung eines Bridged-Networks (gleiches Subnetz wie der Host), erkläre ich im Nachgang am Schluss dieses Artikels.



# Domain and network settings
domainname = "mydomain.vm"  
networkname = "default"    # Virtual Networks: default (=NAT)


Dann folgen noch die spezifischen Parameter für den oder die virtuellen Hosts. Innerhalb von hosts = {} können weitere Server-Blöcke hinzugefügt werden, die jeweils die Ausstattung einer virtuellen Server-Instanz beschreiben. Man kann hier bei Bedarf auch weitere Parameter, die man individuell konfigurieren möchte hinzufügen. Für meine Projekte waren die folgenden aber ausreichend.

Ein Hinweis noch: Weil die virtuelle Disk vom BaseOS-Image abhängig ist, muss die Kapazität (disksize in Bytes) der VM entsprechend größer gewählt werden. Wie man die Gröse des Base-Image ermittelt steht hier im Code.

# Host specific settings
# RAM size in bytes
# Disksize in bytes (disksize must be bigger than sourceimage virtual size)
# Example:
#    qemu-img info debian-10.3.4-20200429-openstack-amd64.qcow2
#         virtual size: 2 GiB (2147483648 bytes)
hosts = {
   "newsrv" = {
      name     = "newsrv",
      vcpu     = 1,
      memory   = "1024",
      diskpool = "default",
      disksize = 12000000000,
      mac      = "52:54:00:11:11:33",
   },
}

main.tf

Die main.tf ist der eigentliche Bauplan beziehungsweise das Rezept für unsere neu zu erstellende Umgebung.

In der main.tf wird zuerst die minimal benötigte Terraform Version festgelegt und der libvirt Provider mit folgenden Codezeilen aktiviert:

# Declare libvirt provider for this project
terraform {
  required_version = ">= 0.13"
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.6.3"
    }
  }
}
# Provider URI for libvirt
provider "libvirt" {
  uri = "qemu:///system"
}

Anschließend werden die Variablen deklariert, die wir zuvor in der terraform.tfvars definiert haben (auch mit Default-Werten), die wir zur Erzeugung der virtuellen Maschine benötigen.

Falls in der Datei terraform.tfvars nichts definiert ist, zieht hier der Fall-Back-Wert, der in default = {} hinterlegt ist, wie z.B. die URL für das Source-Image.

# Use terraform.tfvars to define the settings of your servers
# the variables here are the defaults if no terraform.tfvars setting is found
variable "projectname" {
 type   = string
 default = "myproject"
}
variable "hosts" {
  default = {
    "srv1" = {
       name = "srv1",
       vcpu     = 1,
       memory   = "1536",
       diskpool = "default",
       disksize = "4000000000",
       mac      = "52:54:00:11:11:11",
     },
  }
}
variable "baseimagediskpool" {
  type    = string
  default = "default"
}
variable "domainname" {
  type    = string
  default = "domain.local"
}
variable "networkname" {
  type    = string
  default = "default"
}
variable "sourceimage" {
  type    = string
  default = "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2"
}

Der nächste Block definiert den Ablageort des Base-Image innerhalb von KVM. Hier wird ein Cloud Image im qcow2 Format erwartet und ein KVM-Diskpool. Jede VM innerhalb unseres Projektes ist dann mit diesem Base-Image verknüpft und benutzt es als Basis-Layer für sein eigenes Disk-Image.

# Base OS image
resource "libvirt_volume" "baseosimage" {
  name   = "baseosimage_${var.projectname}"
  source = var.sourceimage
  pool   = var.baseimagediskpool
}

Als nächstes wird eine virtuelle Disk für jeden virtuellen Host in einer for_each Schleife erzeugt.

# Create a virtual disk per host based on the Base OS Image
resource "libvirt_volume" "qcow2_volume" {
  for_each = var.hosts
  name           = "${each.value.name}.qcow2"
  base_volume_id = libvirt_volume.baseosimage.id
  pool           = each.value.diskpool
  format         = "qcow2"
  size           = each.value.disksize
}

Anschließend werden einige Variablen an CloudInit übergeben, die später in der Datei cloudinit.cfg Verwendung finden. Hier werden zum Beispiel der Hostname und die Domain definiert. Man könnte noch weitere Settings per Variable definieren, die bei mir jedoch aktuell zum Teil noch „hard-coded“ in der cloudinit.cfg direkt konfiguriert werden.

# Use cloudinit config file and forward some variables to cloud_init.cfg
data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.cfg")
  for_each   = var.hosts
  vars     = {
    hostname   = each.value.name
    domainname = var.domainname
  }
}

# Use CloudInit to add the instance
resource "libvirt_cloudinit_disk" "commoninit" {
  for_each   = var.hosts
  name      = "commoninit_${each.value.name}.iso"
  user_data = data.template_file.user_data[each.key].rendered
}

Nun folgt die Erstellung der VM (Guest/Domain). Mit Hilfe einer Schleife (for_each) können so sehr bequem mehrere VMs (libvirt_domain) erzeugt werden, inklusive Netzwerk Interface Konfiguration und virtueller Disk. Weiterhin wird der cloudinit Variablen die commoninit.iso libvirt_cloudinit_disk zugewiesen, mit der der Betriebssystem Installationprozess eingeleitet wird.

# Define KVM-Guest/Domain
resource "libvirt_domain" "newvm" {
  for_each   = var.hosts
  name   = each.value.name 
  memory = each.value.memory
  vcpu   = each.value.vcpu

  network_interface {
    network_name   = var.networkname
    mac            = each.value.mac
    # If networkname is host-bridge do not wait for a lease
    wait_for_lease = var.networkname == "host-bridge" ? false : true
  }

  disk {
    volume_id = element(libvirt_volume.qcow2_volume[each.key].*.id, 1 )
  }

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

}
## END OF KVM DOMAIN CONFIG

Zuguterletzt wird noch ein Output des Ergebnisses der soeben erzeugten virtuellen Maschine auf der Console ausgegeben.



# Output results to console
output "hostnames" {
  value = [libvirt_domain.newvm.*]
}

cloudinit.cfg

Wie bereits eingangs erwähnt, kümmert sich CloudInit um die initiale Konfiguration des neuen Server-Images. Das CentOS GenericCloud image, was wir zur Vorbereitung heruntergeladen haben oder per Terraform zur Laufzeit laden, ist die Basis, um ein Betriebssystem angepasst zu installieren.

In diesem Zusammenhang bereiten wir den neuen Server auch gleich für Ansible vor, indem wir die SSH Public Keys für den Ansible-User hinterlegen.

Um ein neues SSH Keypair zu erzeugen gibt man folgendes an der Commandline ein (folgendes Beispiel bezieht sich auf meinen Benutzer „juergen“. Für den Benutzer „ansible“ ist die Vorgehensweise entsprechend):

juergen  ~  ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/juergen/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/juergen/.ssh/id_rsa
Your public key has been saved in /home/juergen/.ssh/id_rsa.pub
The key fingerprint is:
SHA256: juergen@localhost.localdomain
The key's randomart image is:
+---[RSA 3072]----+
|                 |
+----[SHA256]-----+

 juergen  ~  ll .ssh
insgesamt 12
-rw-------. 1 juergen juergen 2622 17. Nov 16:12 id_rsa
-rw-r--r--. 1 juergen juergen  583 17. Nov 16:12 id_rsa.pub
-rw-r--r--. 1 juergen juergen  372 17. Nov 12:52 known_hosts
 juergen  ~  

Den Public Key, der in id_rsa.pub gespeichert ist, den hinterlegen wir in der cloudinit.cfg.

In der cloudinit.cfg konfigurieren wir außerdem alle Einstellungen für unser angepasstes Setup, wie z.B. Hostname, weitere User, SSH-Keys, zu installierende Pakete etc.

Die Datei ist im yaml Format. Hier ist es wichtig auf die korrekte Form, insbesondere die richtigen Einrückungen zu achten!

Ich aktiviere dann auch gleich den ssh Zugriff per Passwort Authentifizierung, da das bei den Cloud-Images per default deaktiviert ist, ich es aber für sehr praktisch empfinde, mich per Virtual Manager notfalls auch mal an der Console anzumelden. Anschließend setzen wir ein neues Passwort für den User „root“.

#cloud-config
# vim: syntax=yaml
#
# ***********************
# 	---- for more examples look at: ------
# ---> https://cloudinit.readthedocs.io/en/latest/topics/examples.html
# ******************************
#
# This is the configuration syntax that the write_files module
# will know how to understand. encoding can be given b64 or gzip or (gz+b64).
# The content will be decoded accordingly and then written to the path that is
# provided.
#
# Note: Content strings here are truncated for example purposes.
ssh_pwauth: true
chpasswd:
  list: |
     root:Geheim1234
  expire: false

Dann folgen ein paar neue Linux User, die gleich initial auf allen VMs angelegt werden sollen, mit dem jeweiligen ssh-rsa Public-Key (hier nur den Key einsetzen), den wir weiter oben erzeugt haben, um sich vom KVM-Host aus entsprechend auch ohne Passwort an der VM anmelden zu können.

# User 'ansible' is used for ansible
users:
  - name: juergen
    ssh_authorized_keys:
      - ssh-rsa AAAAB3Nza...
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    groups: wheel
  - name: root
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1...
  - name: ansible
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2E...
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    groups: wheel

Als nächstes folgt die Festlegung des Hostnamen und des FQDN (die Variablen werden in terraform.tfvars befüllt und über die main.tf dann an cloudinit.cfg weitergereicht).

# Set hostname based on main.tf variables 
preserve_hostname: false 
fqdn: ${hostname}.${domainname}
hostname: ${hostname}

Anschließend folgt noch ein Reboot, um den neuen Hostnamen im DHCP und DNS bekannt zu machen. Wir installieren auch gleich noch zusätzlich python36, was wir später für ansible benötigen. Das generelle Systemupdate aller Pakete habe ich hier jedoch auskommentiert, weil das später Ansible für mich macht.







# Initiate a reboot after setting the fqdn. It's necessary to update the DNS/DHCP information in libwirt dnsmasq
power_state:
 delay: "+1" 
 mode: reboot
 condition: true

# Install python for ansible
packages:
  - python36

#package_update: true
#package_upgrade: true
#package_reboot_if_required: true

3, 2, 1, Server!

Jetzt haben wir alles zusammen, um einen ersten Deployment Versuch per terraform zu starten. Der Rest sind nur noch eine Hand voll Befehle, die unseren Plan ausführen.

  • terraform init (einmalig zur Initialisierung des Projektes)
  • terraform plan (prüft den Plan auf Code-Fehler und zeigt das zu erwartende Ergebnis auf der Console an)
  • terraform apply (setzt den Plan tatsächlich um)
  • terraform destroy (reißt alles wieder ein, entfernt alle VMs, virtuellen Disks und Netzwerke zu diesem Projekt)

Wenn der Befehl „terraform plan“ ohne Fehlermeldungen durchläuft, dann zeigt er an der Console an, was er alles tun würde. Hier kann man sein Setup nochmals kontrollieren. Anschließend führt der Befehl „terraform apply“ den Plan tatsächlich aus und erstellt hoffentlich erfolgreich die virtuelle Machine mit allen Konfigurationen, wie wir es uns vorgestellt haben. Dieser Vorgang dauert, je nach Komplexität und ob das BaseOS-Image schon lokal liegt, normalerweise weniger als eine Minute. Wenn nicht gleich alles auf Anhieb funktioniert, dann liefert Terraform im Console Output schon sehr informative Hilfestellungen zur Fehleranalyse. Darüberhnaus hilft auch Google bestimmt weiter. Es gibt mittlerweile für die meisten Fragestellungen Antworten im Netz.

Schließlich lässt sich mit „terraform destroy“ das ganze Projekt auch sehr schnell wieder löschen und alles auf den Ausgangspunkt zurücksetzen. Wer sich mit dieser Systematik einmal angefreundet hat, wird sicher viel Spaß damit haben seine Umgebungen per Terraform Code zu deployen.

Der zweite Teil meines Quickstart Guides baut auf diesen Teil auf und zeigt, wie man per Ansible die weitere Konfiguration seiner VM vornimmt.

Optional: Bridge statt NAT Network

Im oben gezeigten Beispiel erstellen wir eine Virtual Machine mit dem default NAT Network von libvirt. Manchmal ist es aber sinnvoll oder notwendig die VM im gleichen Subnetz zu betreiben, wie den KVM Host. Dazu müssen wir die VM über ein sogenanntes Bridge Network Device betreiben.

Hierfür müssen wir zuerst unter Linux eine Network Bridge erzeugen.

 juergen  ~  sudo brctl addbr br1

 juergen  ~  sudo brctl show
bridge name	bridge id		STP enabled	interfaces
br1		8000.f6e545eb05ba	no		
docker0		8000.02420865003e	no		
virbr0		8000.525400b83e08	yes		virbr0-nic
							vnet0

 juergen  ~  sudo brctl addif br1 enp0s31f6

 juergen  ~  brctl show
bridge name	bridge id		STP enabled	interfaces
br1		8000.f6e545eb05ba	no		enp0s31f6
							vnet0
docker0		8000.02420865003e	no		
virbr0		8000.525400b83e08	yes		virbr0-nic

Dieses virtuelle Bridge Device (br1) können wir nun mit dem libvirt Provider in unserem Terraform Modul verwenden, um ein libvirt Network zu erstellen. Beispiele zu libvirt_network finden sich hier: https://github.com/dmacvicar/terraform-provider-libvirt/blob/master/website/docs/r/network.markdown

resource "libvirt_network" "vmbridge" {
  # the name used by libvirt
  name = "vmbridge"

  # mode can be: "nat" (default), "none", "route", "bridge"
  mode = "bridge"

  # (optional) the bridge device defines the name of a bridge device
  # which will be used to construct the virtual network.
  # (only necessary in "bridge" mode)
  bridge = "br1"
  autostart = true
}