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 !