Installer Nextcloud sur Fedora Servers Cloud grâce à Terraform et Ansible, part1

Aujourd’hui, je vais vous exposer un de mes POC. Je voulais installer Nexcloud sur Fedora server et pour plus de sécurité et plus d’efficacité, séparer les middlewares et les applications sur ces différents serveurs. Je pars sur 3 serveurs, on peut faire évoluer le code pour en ajouter plus, notamment Redis en master-slave, Mariadb en Master/Slave et un loadbalancer nginx. Je mets en ligne un de mes premiers codes Terraform fonctionnel. Il est plus simple à comprendre mais pas super optimisé/industrialisé. Dans des articles ultérieurs, je mettrai des codes Terraform pour de l’infra scalable, c’est plus sexy. J’ai essayé dans les playbooks ansible d’adapter les règles SElinux pour que l’application Nextcloud fonctionne en mode enforcing, et ça passe: c’est toujours mieux avec. Un truc sympa à faire également au niveau sécu, serait de relier le chiffrement de Nextcloud à la puce TPM émulée du serveur, dans le même registre je n’ai pas mis de rôle pour le durcissement des souches Fedora. Tout un programme…

Pré requis

Sur votre hyperviseur Fedora, CentOS ou RedHat, il faut avoir installé Ansible, qemu-KVM, Terraform avec le module libvirt. Dans mon cas de figure, j’ai installé Openvswitch et pluggé dessus un firewall Opnsense avec deux interfaces “WAN” et “LAN”, les serveurs créés par Terraform arrivent dans le réseau LAN. Cela me permet de pouvoir plus facilement gérer les flux vers les serveurs web. Dans la vrai vie, il faudrait une zone DMZ sur le firewall, je coderai cela plus tard, il n’est pas évident pour moi d’automatiser l’installation et la configuration d’Opnsense.

Pour installer les outils de virtualisation sur l’hyperviseur (adaptez en fonction de vos besoin):

[matt@m4800 ~]$ sudo dnf -y install bridge-utils libvirt libvirt-devel virt-install qemu-kvm virt-top libguestfs-tools virt-manager virt-viewer virt-xml virt-clone genisoimage

lorsque l’installation est terminée, il est toujours intéressant de connaître l’analyse du hardware de votre serveur pour la virtualisation avec la commande :

[root@m4800 ~]# virt-host-validate
QEMU : Vérification for hardware virtualization  : PASS
...

Pour Ansible :

[matt@m4800 ~]$ sudo dnf install ansible

Installer Terraform dans sa version 12:

[matt@m4800 tmp]$ wget https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip && sudo unzip ./terraform_0.12.24_linux_amd64.zip -d /usr/local/bin/

ainsi que le module libvirt KVM pour Terraform. Ce dernier demande d’avoir go sur le système.

[matt@m4800 ~]$ sudo dnf install go

Ajouter dans votre bash_profile

[matt@m4800 ~]$ export GOPATH=$HOME/go

On va ensuite utiliser la commande go pour installer le module libvirt pour terraform:

[matt@m4800 ~]$ go get github.com/dmacvicar/terraform-provider-libvirt
[matt@m4800 ~]$ go install github.com/dmacvicar/terraform-provider-libvirt

Si tout c’est bien passé, vous devriez avoir le binaire dans ~/go/bin/terraform-provider-libvirt. Il faut à présent initialiser terraform, pour cela créez un répertoire vide, par exemple “projects” dans votre home et exécutez les commandes suivantes:

[matt@m4800]$ mkdir ~/projects
[matt@m4800]$ cd projects
[matt@m4800 projects]$ terraform init
[matt@m4800 projects]$ mkdir ~/.terraform.d/plugins
[matt@m4800 projects]$ ln -s /home/$USER/go/bin/terraform-provider-libvirt ~/.terraform.d/plugins/

YOU’RE READY NOW

Le code de Terraform

J’ai séparé le code en 8 fichiers dans mon répertoire ~/projects/nextcloud :

  • network_config_dhcp.cfg,
  • cloud_init.tpl,
  • hosts.tpl
  • libvirt_mariadb_master.tf,
  • connections.tf,
  • libvirt_nextcloud.tf,
  • libvirt_redis_master.tf,
  • variables.tf.

Créons notre premier fichier pour le nouveau projet:

[matt@m4800 ~]$ mkdir -p ~/projects/nextcloud
[matt@m4800 ~]$ touch ~/projects/nextcloud/connections.tf

Connections.tf

#instance the provider
provider "libvirt" {
  uri = "qemu:///system"
}

Rien de bien compliqué ici, on prévient Terraform que le provider est libvirt pour KVM sur le serveur local. Remarque : si vous travaillez à distance par ssh ou tcp (paramètres tcp à configurer dans /etc/libvirt/libvirtd.conf) il faudrait un uri de la forme :

provider "libvirt" {
  uri = "qemu+(ssh|tcp)://<user>@<machine>/system"
}

Variables.tf

Comme son nom l’indique on y met toutes nos variables… dans cette version je n’ai pas utilisé les locales, donc c’est un peu fastidieux… mais bon ça fonctionne. Pensez à adapter le nombre de vCPU et la RAM, là j’avais de la marge. Ne mettez pas “LAN”, ça ne fonctionnera pas, pour tester mettez “default”, le réseau bridgé par défaut :

#les variables peuvent être surchargées
variable "image_url" {
  type = string
  default = "https://download.fedoraproject.org/pub/fedora/linux/releases/31/Cloud/x86_64/images/Fedora-Cloud-Base-31-1.9.x86_64.qcow2"
}
variable "tld" {
  type = string
  default = "lan"
}
variable "net" {
  type = string
  default = "LAN"
}
variable "dev_host_label" {
  type = list
  default = ["lan_nc", "lan_db", "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 = "mettre ici votre empreinte de clé"
}
variable "hostname" {
  type = list
  default = ["fed-nc", "fed-db", "fed-rd"]
}
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"]
}

Libvirt_nextcloud.tf

Dans le bloc de code suivant, j’ai découvert la “scalabilité” dans Terraform… je laisse donc cette première version qui fonctionne mais qui est un peu moins élégante. J’ai décidé de séparer les datas de Nextcloud sur un deuxième disque. A la fin du bloc, on lance les actions avec Ansible, trois playbooks qui font appel chacun à différents rôles, c’est pareil, dans la dernière version j’ai créé un template pour créer un inventaire ansible dynamique… j’apprends en codant…

#############################
## je prends la dernière image cloud de fedora server
#############################
resource "libvirt_volume" "os_image" {
  name   = "fedora-os_image"
  pool   = "default"
  source = var.image_url
  format = "qcow2"
}

resource "libvirt_volume" "os_image_resized" {
  name               = "disk-nc-${count.index}"
  pool               = "default"
  base_volume_id     = libvirt_volume.os_image.id
  size               = var.rootdiskBytes
  count              = length(var.hostname)
}

#############################
# Créer un disque supplémentaire en xfs pour les données de nextcloud
#############################
resource "libvirt_volume" "disk_data1" {
  name           = "${var.hostname[0]}-disk-xfs"
  pool           = "default"
  size           = var.diskBytes
  format         = "qcow2"
}

#############################
## Data pour CloudInit
#############################
data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.tpl")
  vars = {
        hostname       = "${var.hostname[count.index]}"
        fqdn           = "${var.hostname[count.index]}.${var.tld}"
        user           = var.user
        first_packages = jsonencode(var.first_packages)
        ssh_public_key = var.ssh_public_key
  }
  count = length(var.hostname)
}

data "template_file" "network_config" {
  template = file("${path.module}/network_config_dhcp.cfg")
}

#############################
## CloudInit ISO pour préparer les serveurs
#############################
resource "libvirt_cloudinit_disk" "commoninit" {
  name           = "fedora-commoninit-${count.index}.iso"
  pool           = "default"
  user_data      = element(data.template_file.user_data.*.rendered, count.index)
  network_config = data.template_file.network_config.rendered
  count = length(var.hostname)
}

#############################
## Creation de la machine pour Nextcloud
#############################
resource "libvirt_domain" "domain_nc" {
  name   = var.hostname[0]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true

  disk = [
    {
      block_device = "null"
      file = "null"
      scsi = false
      url = "null"
      volume_id = libvirt_volume.os_image_resized[0].id
      wwn = "null"
    },
    {
      block_device = "null"
      file = "null"
      scsi = false
      url = "null"
      volume_id = libvirt_volume.disk_data1.id
      wwn = "null"
    }
  ]

  network_interface {
    network_name = var.net
    wait_for_lease = true
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[0].id
 
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  graphics {
    type        = "spice"
    listen_type = "address"
    autoport    = "true"
  }

}
#################################
data "template_file" "playbook_hosts" {
    template = file("${path.module}/hosts.tpl")
    vars = {
        group_0       = var.dev_host_label[0]
        group_1       = var.dev_host_label[1]
        group_2       = var.dev_host_label[2]
        private_ips_0 = libvirt_domain.domain_nc.network_interface.0.addresses[0]
        private_ips_1 = libvirt_domain.domain_db.network_interface.0.addresses[0]
        private_ips_2 = libvirt_domain.domain_rd.network_interface.0.addresses[0]
        ssh_file      = var.ssh_key_path
        user          = var.user
    }
    depends_on = [ libvirt_domain.domain_nc, libvirt_domain.domain_rd, libvirt_domain.domain_db ]
}

resource "local_file" "ansible_inventory" {
        content     =  data.template_file.playbook_hosts.rendered
        filename = "${path.module}/ansible/hosts"
        depends_on = [ libvirt_domain.domain_nc, libvirt_domain.domain_rd, libvirt_domain.domain_db ]
}

resource "null_resource" "ansible_rd" {
    provisioner "local-exec" {
      command = "sed -i -e '/hosts:/ s/: .*/: ${var.dev_host_label[2]}/' /etc/ansible/playbooks/rd.yml"
    }
depends_on = [ local_file.ansible_inventory ]
}

resource "null_resource" "ansible_db" {
    provisioner "local-exec" {
      command = "sed -i -e '/hosts:/ s/: .*/: ${var.dev_host_label[1]}/' /etc/ansible/playbooks/db.yml"
    }
depends_on = [ local_file.ansible_inventory ]
}

resource "null_resource" "ansible_nc" {
    provisioner "local-exec" {
      command = "sed -i -e '/hosts:/ s/: .*/: ${var.dev_host_label[0]}/' /etc/ansible/playbooks/nc.yml"
    }
    provisioner "local-exec" {
      command = "sleep 10; ansible-playbook -i ${path.module}/ansible/hosts /etc/ansible/playbooks/main.yml"
    }
depends_on = [ null_resource.ansible_db, null_resource.ansible_rd ]
}

resource "null_resource" "destroy_ansible_hosts" {
  provisioner "local-exec" {
    when = destroy
    command = "rm -rf ${path.module}/ansible/hosts"
  }
}
# Sorties
terraform {
  required_version = ">= 0.12"
}
output "ip_nextcloud" {
  value = libvirt_domain.domain_nc.network_interface.0.addresses
}
output "ip_redis" {
  value = libvirt_domain.domain_rd.network_interface.0.addresses
}
output "ip_sql" {
  value = libvirt_domain.domain_db.network_interface.0.addresses
}

Libvirt_redis_master.tf

#############################
## Création du serveur cache redis
#############################
resource "libvirt_domain" "domain_rd" {
  name   = var.hostname[2]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true

  disk {
    volume_id = libvirt_volume.os_image_resized[2].id
  }

  network_interface {
    network_name = var.net
    wait_for_lease = true
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[2].id

  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  graphics {
    type        = "spice"
    listen_type = "address"
    autoport    = "true"
  }
}

Libvirt_mariadb_master.tf

#############################
## Creation du serveur de base de donnée
#############################
resource "libvirt_domain" "domain_db" {
  name   = var.hostname[1]
  memory = var.memoryMB
  vcpu   = var.cpu
  qemu_agent = true

  disk { volume_id = libvirt_volume.os_image_resized[1].id }

  network_interface {
    network_name = var.net
    wait_for_lease = true
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[1].id

  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  graphics {
    type        = "spice"
    listen_type = "address"
    autoport    = "true"
  }
}

Cloud_init.tpl

On passe mainitenant au template de cloud_init qui va personnaliser l’installation et le premier run des serveurs :

#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.
hostname: ${hostname}
fqdn: ${fqdn}
#package_upgrade: true
packages: ${first_packages}
ssh_pwauth: true
manage_etc_hosts: true
chpasswd:
  list: |
     root: StrongPassword
  expire: False
users:
  - name: ${user}
    ssh_authorized_keys:
      - ${ssh_public_key}
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    groups: wheel
runcmd:
  - sudo systemctl start qemu-guest-agent.service
  - sudo timedatectl set-timezone Europe/Paris
  - sudo hostnamectl set-hostname ${fqdn}
growpart:
  mode: auto
  devices: ['/']
%{ if hostname == "fed-nc" }
disk_setup:
  /dev/vdb:
    table_type: gpt
    layout: True
    overwrite: False
fs_setup:
  - label: DATA_XFS
    filesystem: xfs
    device: '/dev/vdb'
    partition: auto
mounts:
  - [ LABEL=DATA_XFS, /var/nc_data, xfs ]
%{ endif }
output:
    init:
        output: "> /var/log/cloud-init.out"
        error: "> /var/log/cloud-init.err"
    config: "tee -a /var/log/cloud-config.log"
    final:
        - ">> /var/log/cloud-final.out"
        - "/var/log/cloud-final.err"
final_message: "The system is finall up, after $UPTIME seconds"

Hosts.tpl

C’est un template qui va générer le fichier hosts d’ansible en fonction des données des serveurs envoyées par Terraform/qemu-kvm. J’ai pas fait de loop for dans cette version, c’est venu après… mais ça fonctionne impec :

[all:vars]
ansible_python_interpreter="/usr/bin/python3"
[${group_0}]
${private_ips_0} ansible_user=${user} ansible_ssh_private_key_file=${ssh_file}
[${group_1}]
${private_ips_1} ansible_user=${user} ansible_ssh_private_key_file=${ssh_file}
[${group_2}]
${private_ips_2} ansible_user=${user} ansible_ssh_private_key_file=${ssh_file}

Network_config_dhcp.cfg

version: 2
ethernets:
  eth0:
  dhcp4: true

C’est terminé pour la partie Terraform, dans le prochain article, on verra les playbooks et rôles Ansible.

Bon hacking !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.