Eenvoudig instances aanmaken in de BIT VDC met Terraform en Ansible

Eenvoudig instances aanmaken in de BIT VDC met Terraform en Ansible

08-06-2022 08:22:56

2022.06.08 Terraform_Ansible_VDC.jpg
Bij BIT is het mogelijk om een Virtual Datacenter af te nemen. Hiermee beschik je over een bepaalde hoeveelheid dedicated resources zoals processorkracht, geheugen en opslag. Op deze manier hoef je zelf niet te investeren in kostbare hardware. Het is mogelijk om deze digitale omgeving te beheren via een beheeromgeving, waar je eenvoudig kan op- en afschalen door de resources te verdelen over het aantal virtuele servers dat je hebt. Hiermee heb je maximale grip op en flexibiliteit over de inrichting en kosten van je omgeving.

Naast het beheren van de omgeving via een beheeromgeving, is het ook mogelijk om de omgeving te beheren via Terraform. Door hierbij ook gebruik te maken van Ansible is bijvoorbeeld het toevoegen van extra webservers een fluitje van een cent. In dit blog zullen we aan de hand van voorbeelden laten zien hoe je gebruik kan maken van Terraform en Ansible wanneer je een BIT Virtual Datacenter hebt.

Wat houdt Infrastructure as Code (IaC) in?

Waarbij vroeger veel platformen direct draaiden op fysieke hardware, zien we dat er tegenwoordig steeds meer gevirtualiseerd wordt. Dit zorgt er niet alleen voor dat er veel meer (soorten) platformen tegelijk kunnen draaien op fysieke hardware, maar het biedt ook mogelijkheden voor het automatisch provisionen door het op te schrijven als code, ook wel Infrastructure as Code (IaC) genoemd. In een bepaalde programmeertaal wordt dan beschreven welke componenten er zijn, wat hun eigenschappen zijn, en wat het gewenste eindresultaat moet zijn.
 
Doordat de code eenvoudig opnieuw is uit te voeren, kunnen wijzigingen ook snel en gemakkelijk worden getest. Daarnaast heeft het ook als groot voordeel dat de kans op fouten geminimaliseerd wordt, omdat het proces is geautomatiseerd.

Terraform: de beheerder van jouw infrastructuur

Terraform is een open source project van HashiCorp dat ondersteuning biedt voor meer dan honderd providers. De IaC wordt geschreven in een taal die door HashiCorp zelf is bedacht: HashiCorp Configuration Language (HCL).

Het aantal providers dat Terraform ondersteunt is erg uitgebreid, wat te danken is aan de grote community die Terraform heeft. Op de Providers pagina van Terraform zijn alle beschikbare providers te vinden. Dit zijn niet alleen de bekende public cloud leveranciers, maar bijvoorbeeld ook netwerk devices, monitoring applicaties en communicatiediensten. Je zou kunnen stellen dat alles wat een API heeft gezien kan worden als provider.

Wanneer de infrastructuur is geschreven, kan deze middels één commando worden uitgerold. Naast het aanmaken van de infrastructuur is het ook mogelijk om de omgeving (gedeeltelijk) af te breken. En omdat de gehele omgeving in code geschreven is, heb je ook de mogelijkheid om versiebeheer toe te passen.

Een instance altijd hetzelfde geconfigureerd met Ansible

Nu we het kort hebben gehad over wat IaC en Terraform precies inhouden, moeten we het hebben over Ansible. Ook Ansible is een open-source project. Waar Terraform gericht is op het aanmaken van bijvoorbeeld virtual machines, is Ansible bedoeld voor onder andere het beheer van de omgeving binnen een virtual machine. Met Ansible is het daarnaast ook mogelijk om netwerkapparatuur van bijvoorbeeld F5, Juniper en Fortigate te beheren. Het is dus een tool die op vele plekken kan worden ingezet, dankzij de vele modules die beschikbaar zijn.

Ansible heeft als groot voordeel dat het agentless is. Alleen op de host vanaf waar je Ansible wilt draaien moet de software beschikbaar zijn. Bij een Linux of Unix remote host maakt Ansible verbinding via SSH, en bij een Windows remote host via WinRM.

Om in Ansible aan te geven wat er moet gebeuren op de remote host, wordt er gewerkt met een playbook. In dit playbook staan verwijzingen naar rollen of losse taken. Naast dat het mogelijk is om zelf rollen te schrijven, bestaat er ook de Ansible Galaxy. Hier worden rollen gedeeld die geschreven zijn door de community. Meer informatie over Ansible rollen is te vinden in de Ansible documentatie.

Door een remote host volledig te beheren via Ansible, zal elke uitrol (als het goed is) resulteren in hetzelfde resultaat!

Je werkomgeving klaar maken voor Terraform en Ansible

Op de officiële Terraform website is er een uitleg beschikbaar over hoe Terraform geïnstalleerd kan worden op verschillende besturingssystemen. Hetzelfde geldt voor Ansible, in de officiële documentatie zijn de nodige stappen voor de verschillende besturingssystemen te vinden.

Nadat de installatie voltooid is, zijn de commando's terraform en ansible beschikbaar.

$ ansible --version
ansible [core 2.12.2]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /user/lib/python3/dist-packages/ansible
ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.10.4 (main, Apr 2 2022, 09:04:19) [GCC 11.2.0]
jinja version = 3.0.3
libyaml = True

$ terraform --version
Terraform v1.1.9
on linux_amd64

De eerste stappen met Terraform

Nu Terraform en Ansible geïnstalleerd zijn, kan er een Terraform project worden gemaakt. We beginnen met het aanmaken van een locatie waar de projectbestanden in opgeslagen worden. In dit voorbeeld is dat in ‘home’, in de folder ‘terraform-blog’.

mkdir ~/terraform-blog
cd ~/terraform-blog

Maak met je favoriete editor het bestand main.tf aan en zet hier onderstaande code in:

terraform {
required_providers {
opennebula = {
source = "OpenNebula/opennebula"
version = "0.4.1"
}
}
}

Hiermee geven we aan dat de provider OpenNebula gebruikt moet worden en dan specifiek versie 0.4.1 - op het moment van schrijven de laatst beschikbare versie. Het is mogelijk om meerdere providers op te geven, maar in dit voorbeeld wordt alleen OpenNebula behandeld.

Wanneer de benodigde code voor de provider aan het bestand main.tf is toegevoegd kan deze worden opgeslagen. Het Terraform project kan nu worden geïnitialiseerd, wat ervoor zorgt dat de benodigde OpenNebula bestanden worden gedownload.

Het initialiseren doen we door het commando terraform init uit te voeren.

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding opennebula/opennebula versions matching "0.4.1"...
- Installing opennebula/opennebula v0.4.1...
- Installed opennebula/opennebula v0.4.1 (signed by a HashiCorp partner, key ID 3BB1E8904FC9C908)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.

Als we nu kijken naar de bestanden in onze project folder, dan zal daar de folder .terraform bij zijn gekomen, en het bestand .terraform.lock.hcl. Het is af te raden deze bestanden aan te passen, aangezien deze mogelijk weer worden overschreven door Terraform.

Nu de benodigde bestanden voor de koppeling met OpenNebula aanwezig zijn, gaan we de authenticatie met de OpenNebula-omgeving configureren. Open het bestand main.tf weer met de editor en voeg onderaan het bestand het volgende toe.

provider "opennebula" {
endpoint      = "https://api.example.tld/RPC2"
username      = "USERNAME"
password      = "PASSWORD"
}

Wanneer je een Virtal Datacenter bij BIT afneemt, worden de juiste inloggegevens aan je doorgegeven. Naast het gebruik van een wachtwoord is het ook mogelijk om gebruik te maken van een login token. Dit token kan je zelf genereren in de OpenNebula webinterface. Als de juiste inloggegevens zijn ingevoegd, kan het bestand worden opgeslagen. De basis is nu klaar, er is een provider geconfigureerd en de authenticatie naar die provider is ingesteld.

We gaan nu de configuratie zo maken dat er daadwerkelijk een instance wordt aangemaakt in OpenNebula.

Maak het bestand server.tf aan en zet de onderstaande code erin:

resource "opennebula_image" "clone_image" {
clone_from_image = 424
name             = "server1"
datastore_id     = 100
persistent       = true
}

In dit voorbeeld beginnen we met het klonen van een image dat al bestaat in onze OpenNebula omgeving. Dit is een basis Ubintu-installatie en het image id hiervan is 424. Wanneer je bij BIT een Virtual Datacenter afneemt kan je zelf images aanmaken. Het is ook mogelijk dat wij een basis-image voor je klaarzetten. Voor meer informatie hierover kan je terecht bij onze supportafdeling.

De naam van het gekloonde image moet server1 worden, en deze moet ook persistent zijn. Het datastore_id is in dit geval 100 maar kan per omgeving verschillen.

In hetzelfde bestand, onder het clone_image gedeelte, maken we het gedeelte waar de instance wordt aangemaakt. Per blok zal hierover de nodige informatie worden gegeven.

resource "opennebula_virtual_machine" "create_servers" {
name       = "server1"
cpu        = 3
vcpu       = 3
memory     = 1024
group      = "EXAMPLE"

context = {
NETWORK      = "YES"
SET_HOSTNAME = "$NAME"
SSH_PUBLIC_KEY = file("/home/user/.ssh/id_rsa.pub")
}

De instance zal server1 worden genoemd, net als het image. De instance krijgt 3 CPUs, 3 vCPUs, en 1024 MiB werkgeheugen. De instance wordt ook aangemaakt in de group EXAMPLE. Als je wil weten wat de group is in jouw geval, neem dan contact op met onze supportafdeling.

Vanuit OpenNebula is het mogelijk om bepaalde instellingen binnen het OS van de instance te configureren. Hiervoor moet one-context geïnstalleerd zijn in het OS. Maar omdat dit al is geïnstalleerd in het image dat eerder is gekloond, kunnen we hier deze functionaliteit gebruiken. In dit voorbeeld geven we aan dat het netwerk van het OS geconfigureerd moet worden, de hostname ingesteld moet worden, en deze gelijk moet zijn aan het veld name. Ook moet er een public SSH key worden geïmporteerd. Welk netwerk precies gebruikt moet worden, wordt later aangegeven.

Het volgende stuk dat we gaan toevoegen ziet er als volgt uit:

graphics {
type   = "VNC"
listen = "0.0.0.0"
}

os {
arch = "x86_64"
boot = "disk0"
}

Het eerste deel heeft te maken met de mogelijkheid om een console beschikbaar te hebben via de webinterface. Dit kan handig zijn in het geval dat een instance niet meer te benaderen is via SSH. In het tweede deel specificeren we de architectuur en vanaf welke disk er gestart moet worden. In dit geval is er maar één disk (of image), dus kiezen we disk0.

In het volgende stuk dat we moeten toevoegen zullen we de disk koppelen en een netwerk configureren.

disk {
image_id = opennebula_image.clone_image.id
target   = "sda"
}

nic {
model           = "virtio"
network_id      = 31
}

Omdat we eerder hebben geconfigureerd dat er een bestaand image moet worden gekloond, geven we met opennebula_image.clone_image.id aan dat deze gebruikt moet worden. Terraform zal dan de juiste image ID pakken die hoort bij het gekloonde image.

Wij hebben al een aantal netwerken gedefinieerd binnen OpenNebula. In dit geval krijgt de instance een IP-adres van het netwerk met ID31. Als je wil weten welk netwerk ID je kan gebruiken, neem dan contact op met onze supportafdeling.

Het laatste wat we gaan toevoegen is dat wanneer de instance voor de eerste keer wordt gestart, het OS ook direct wordt geüpdatet.

provisioner "remote-exec" {
inline = ["sudo apt update", "sudo apt upgrade -y"]

connection {
host        = "${self.ip}"
type        = "ssh"
user        = "root"
}
}
}

Als alles samengevoegd is ziet het bestand server.tf er ongeveer zo uit:

resource "opennebula_image" "clone_image" {
    clone_from_image = 424
    name             = "server1"
    datastore_id     = 100
    persistent       = true
}

resource "opennebula_virtual_machine" "create_servers" {
  name        = "server1"
  cpu         = 3
  vcpu        = 3
  memory      = 1024
  group       = "EXAMPLE"

  context = {
    NETWORK      = "YES"
    SET_HOSTNAME = "$NAME"
    SSH_PUBLIC_KEY = file("/home/user/.ssh/id_rsa.pub")
  }

  graphics {
  type   = "VNC"
  listen = "0.0.0.0"
  }

  os {
    arch = "x86_64"
    boot = "disk0"
  }

  disk {
    image_id = opennebula_image.clone_image.id
    target   = "sda"
  }

  nic {
    model           = "virtio"
    network_id      = 31
  }

  provisioner "remote-exec" {
    inline = ["sudo apt update", "sudo apt upgrade -y"]

    connection {
      host        = "${self.ip}"
      type        = "ssh"
      user        = "root"
    }
  }
}

Sla de wijzigingen op, controleer of er geen fouten in de configuratie zitten, en hoeveel instances Terraform zal aanmaken. Dit doen we met het commando terraform plan. Dit vertelt precies wat er zal wijzigen binnen OpenNebula.

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create

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

Als dit geen fouten of andere meldingen oplevert, kan de wijziging daadwerkelijk worden doorgevoerd. Dit doen we door het commando terraform apply uit te voeren. Er verschijnt weer een overzicht wat er gaat veranderen, en als je akkoord gaat, typ je het woord yes.

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create

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

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

Na het drukken op enter zal Terraform beginnen met het aanmaken van de instance. Afhankelijk van wat er allemaal moet gebeuren kan dit even duren.

opennebula_image.clone_image: Creating...
opennebula_image.clone_image: Still creating... [10s elapsed]
opennebula_image.clone_image: Creation complete after 11s [id=437]
opennebula_virtual_machine.create_servers: Creating...
...
opennebula_virtual_machine.create_servers: Creation complete after 5m55s [id=341]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Als de Terraform-run klaar is en er geen problemen zijn gemeld, is de instance aangemaakt en kan hij gebruikt worden. Als je nu naar de bestanden in de project folder kijkt, zul je zien dat één daarvan terraform.tfstate is. In dit bestand slaat Terraform op hoe de omgeving bij OpenNebula is aangemaakt. Als dit bestand verwijderd wordt, zal Terraform proberen alles opnieuw aan te maken. Echter, omdat de instances al bestaan, zal dit niet lukken en krijg je een foutmelding. Het is daarom belangrijk om nooit zelf aanpassingen in terraform.tfstate te maken.

De kracht van Ansible toevoegen

Nu het gelukt is om een instance te maken, is het ook handig om Ansible meteen aan te spreken. In dit voorbeeld zullen we een eenvoudig playbook maken dat Apache2 installeert op de instance en tevens een vhost configureert. We maken hier geen gebruik van rollen, maar alleen van losse taken in een playbook.

Maak binnen de bestaande project folder een nieuwe folder genaamd ansible.

mkdir ansible && cd ansible

Maak nu het bestand webserver.yaml aan met een editor. Dit bestand wordt het playbook. We beginnen met 3 streepjes om aan te geven dat dit het begin van het document is. Daaronder beginnen we met - name: om het playbook een naam te geven. In dit geval kiezen we voor ‘Configure webservers’. Direct daaronder definiëren we op welke hosts/hostgroups het playbook moet worden uitgevoerd. In dit geval zeggen we dat het playbook op alle hosts uitgevoerd moet worden. Als laatste zetten we tasks: neer. Hiermee geven we aan dat de blokken die eronder komen afzonderlijke taken zijn die moeten worden uitgevoerd. Deze regels samen zien er dan als volgt uit:

---

  - name: Configure webserver
    hosts: all
    tasks:

Nu gaan we de taken eronder definiëren. In dit voorbeeld gaan we Apache2 installeren, een web folder aanmaken, en zorgen dat Apache2 wordt gestart. Ook hier beginnen we met de taken een naam te geven, bijvoorbeeld - name: Install Apache. Daaronder geven we aan welke module Ansible moet gaan gebruiken. Omdat we een package willen installeren, zullen we de package module gebruiken. Het is ook mogelijk om de apt of yum module te gebruiken om een package te installeren, maar het voordeel van de package module is dat deze werkt op zowel Debian als RedHat systemen. Onder package geven we aan welk pakket geïnstalleerd moet worden en dat het de laatste versie moet zijn die op dit moment beschikbaar is in de repository. Dit ziet er als volgt uit:

---

  - name: Configure webserver
    hosts: all
    tasks:
      - name: Install Apache2
        package:
          name: apache2
          state: latest

Nu voegen we een nieuwe - name: toe, alleen dit keer met de tekst Create web directory. Direct daaronder roepen we de file module op. Met deze module is het mogelijk on onder andere files, folders of symlinks aan te maken. In dit geval vaan we een folder aanmaken, met path geven we de locatie op waar de folder moet worden aangemaakt. Met state geven we aan dat het een folder moet zijn en moet behoren tot de user/group www-data. Met mode stellen we de schrijfrechten in voor de betreffende folder.

---

  - name: Configure webserver
    hosts: all
    tasks:
      - name: Install Apache2
        package:
          name: apache2
          state: latest
      - name: Create web directory
        file:
          path: "/opt/www"
          state: directory
          owner: www-data
          group: www-data
          mode: 0755

Met het laatste blok zorgen we ervoor dat Apache2 wordt gestart, en stellen we het ook zo in dat bij een reboot van de server Apache2 altijd wordt gestart. We beginnen weer met - name: en nu met de tekst Start and enable Apache2. Daaronder roepen we de systemd module aan en geven aan dat Apache2 gestart moet zijn. Door enabled op true te zetten zorgen we ervoor dat bij een reboot Apache2 ook wordt gestart.

---

  - name: Configure webserver
    hosts: all
    tasks:
      - name: Install Apache2
        package:
          name: apache2
          state: latest
      - name: Create web directory
        file:
          path: "/opt/www"
          state: directory
          owner: www-data
          group: www-data
          mode: 0755
      - name: Start and enable Apache2
        systemd:
          name: apache2
          state: started
          enabled: true

Met dit eenvoudige playbook zorgen we ervoor dat Apache2 altijd geïnstalleerd wordt, dat er een folder wordt aangemaakt, en dat Apache2 wordt gestart. Uiteraard kan hier van alles staan. Zo zou je een playbook kunnen maken dat een MySQL cluster inricht, of een email server configureert. The sky’s the limit!

We nemen met Terraform script dat we eerder gemaakt hebben, en we maken een paar aanpassingen. We willen dat er 2 servers worden aangemaakt, en we voegen een extra provisioner toe die het Ansible playbook zal uitvoeren.

Dit ziet er als volgt uit:

resource "opennebula_image" "clone_image" {
    count            = 2
    clone_from_image = 424
    name             = "webserver${count.index + 1}"
    datastore_id     = 100
    persistent       = true
}

resource "opennebula_virtual_machine" "create_servers" {
  count       = 2
  name        = "webserver${count.index + 1}"
  cpu         = 3
  vcpu        = 3
  memory      = 1024
  group       = "EXAMPLE"

  context = {
    NETWORK      = "YES"
    SET_HOSTNAME = "$NAME"
    SSH_PUBLIC_KEY = file("/home/user/.ssh/id_rsa.pub")
  }

  graphics {
    type   = "VNC"
    listen = "0.0.0.0"
  }

  os {
    arch = "x86_64"
    boot = "disk0"
  }

  disk {
    image_id = opennebula_image.clone_image[count.index].id
    target   = "sda"
  }

  nic {
    model           = "virtio"
    network_id      = 31
  }

 provisioner "remote-exec" {
    inline = ["sudo apt update", "sudo apt upgrade -y", "sudo apt install python3 -y"]

    connection {
      host        = "${self.ip}"
      type        = "ssh"
      user        = "root"
    }
  }

  provisioner "local-exec" {
    command = "ansible-playbook -u root -i '${self.ip}', ansible/webserver.yaml"
  }
}

Wanneer we nu een Terraform apply uitvoeren, zullen de nieuwe servers worden aangemaakt en zal ook het Ansible playbook worden uitgevoerd. Na een paar minuten zijn deze servers dan klaar voor gebruik!

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create

....

opennebula_virtual_machine.create_servers[1] (local-exec): PLAY RECAP *********************************************************************
opennebula_virtual_machine.create_servers[1] (local-exec): XXX.XXX.XXX.1 : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

opennebula_virtual_machine.create_servers[1]: Creation complete after 6m1s [id=343]


opennebula_virtual_machine.create_servers[0] (local-exec): PLAY RECAP *********************************************************************
opennebula_virtual_machine.create_servers[0] (local-exec): XXX.XXX.XXX.2 : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

opennebula_virtual_machine.create_servers[0]: Creation complete after 6m4s [id=342]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.