Armar un Datacenter. Tuqui.

Una cosa que quise hacer desde siempre. ¿Desde cuándo siempre? Desde mi tierna adolescencia: cuando comprendí que un par de compus en red podían sentar las bases infraestructurales para levantar servicios "de la interwebz". Lo interesante, es que en todas las iteraciones que lo intenté, a lo largo de todos estos años, siempre me faltó algo, mayormente tiempo y dinero. Usé computadoras variopintas para estos proyectos pero siempre terminaba todo en la nada ya sea por falta de hardware, conocimiento, tiempo o por pura paja.

También estoy escribiendo este artículo hace demasiado y en algún momento tengo que frenar. Lo último que estaba haciendo antes de cortar este artículo era configurar todo un entorno de desarrollo y deployment con Flux. Está muy bien, pero después de Flux: ¿Qué sigue? Y… todo sigue. Esta profesión tiene la particularidad que pareciera ser infinita y nunca terminara.

En algún momento del año pasado, se estaba cristalizando la idea de armar un pequeño datacenter (o HomeLab) para mi. En Hetzner, tenía varios servicios funcionando para mis cosas, pero algunas de estas aplicaciones requerían mucha capacidad de almacenamiento y estaba volviéndose muy caro mantener todo esto en "la nube".

A mitad del año pasado, reduzco la cantidad de servidores al mínimo: paso de 7 servidores para mis servicios a solamente 2, con moderado éxito. Tengo que remover y hacer backups de las aplicaciones que uso que requieren mucho almacenamiento y ponerlas offline para reducir costos.

Por otro lado, consigo un NAS Synology DS923+ con varios discos y es suficiente para almacenar muchas de mis otras cosas, pero en algún momento, necesito volver a tener mis servicios andando nuevamente y el almacenamiento ya no es un problema.

Entonces con el aguinaldo en mano, hago unos presupuestos e investigo que opciones hay en mercados de usados/refurbished para conseguir algunas computadoras, placas, cables, Switches y abandonar "la nube" completamente.

Antes de empezar, debo decir que hay una tendencia en la industria del software que está empezando a considerar cada vez más fuertemente alejarse de las soluciones corporativas de "la nube" y hostear el software on-premise. Todavía no entiendo bien si esta tendencia está dada para abaratar costos en el largo plazo o simplemente porque armar soluciones en la nube, hoy, es mucho más sencillo.

Y antaño es hace, no sé, ¿Dos, tres años? ;D

El Hardware

Empecemos por listar el hardware mínimo para correr mi Datacenter:

  • Synology DS923+, 4 Bay, 2 HDD 8TB y 2 HDD 16TB en SHR1. Todos los discos de tipo NAS menos uno que es un EXOS y que por ahora no esta integrado al volumen y lo tengo como hotspare. Capacidad total 14 TB. La configuración no es óptima asi como tampoco el precio de estos discos.
  • Cuatro Lenovo ThinkCentre (m720q y m920q) Tiny, ~i5-9500T, 16/32 GB, 240 GB SSD
  • Placa de red Intel i350-T4 4 port gigabit PCIe
  • 01AJ940 Raiser Para Lenovo Thinkcentre M920x
  • TP-Link Archer AX55 Wi-Fi 6 WLAN Router, 2402 Mbps 5 GHz, 574 Mbps 2.4 GHz, 4 × Gigabit LAN Ports, 1 × USB 3.0 Port
  • TP-Link TL-SG116E 16-Port Gigabit Switch
  • Cables Cat 5.e ó mejores
  • Netgear GS108E Managed Switch 8 Port Gigabit Ethernet LAN Switch Plus

Muchos de estos componentes son relativamente baratos. Los más baratos son los switches, los cables y el router wifi. Los más caros, son las computadoras y la placa de red Intel, que es usada para servidores. Lo interesante de esto, es que todos los componentes caros son usados y refurbisheados, lo que los hacen increiblemente accessibles, sin contar que estamos dándole vida a cosas que tiraron prácticamente a la basura previamente. En el país en el que vivo, hay empresas que se dedican a restaurar y arreglar hardware viejo y roto para la reventa a precios ridículos.

Los switches parecen una exageración, pero tengo muchos dispositivos conectados a una red una Pentium MMX, una Commodore 64 con WIC64 y otras cosas divertidas las cuales escriba quisá en algún otro post.

Dos ThinkCentre AbiertasDesarmando dos ThinkCentres y preparando la instalación de una placa

Lo interesante de estas máquinas Lenovo ThinkCentre M920q Tiny, es que son muy pequeñas, y el consumo en IDLE ronda alrededor de 10W asi que la factura de luz no se incrementa demasiado según mis cálculos y sigue siendo una opción más barata que rentar máquinas virtuales en cualquier proveedor de "la nube". También es interesante que estas máquinas, particularmente soportan IOMMU y SR-IOV para poder hacer PCIe Passthrough, que es precisamente algo importantísimo en mi setup, ya que necesito armar un firewall y un router para mi datacenter. Y es acá donde entra en acción la interacción entre las ThinkCentre y la placa de red Intel i350T4.

Intel i350T4La placa de red que va a tener una de las ThinkCentre

El primer problema de todos, es como metemos esta placa en una máquina con espacio tan reducido y además, con un slot PCIe que tiene una orientación que simplemente no permite la instalación de ninguna placa.

Slot PCIeOops! La placa de red no entra ni a palos acá en el único slot PCIe

Ahi es donde viene una parte que solo pude conseguir en China, que es el Riser 01AJ940, específicamente diseñado para cambiar la dirección vertical del slot a una horizontal.

Riser 01AJ940Riser 01AJ940 conectado a una de las ThinkCentre. Se puede ver como cambia la dirección del slot

Esta parte, debería haber venido con un adaptador para la parte del chasis de la ThinkCentre que lo que hace es sostener la placa en el aire para que los contactos de la parte trasera de la placa no toquen ningún componente del motherboard ó el chasis. Y no, solo me vino el Riser sin el adapador, asi que tuve que atarlo con alambre.

Riser 01AJ940 con CartonCortamos unos pedazos de cartón…

Riser 01AJ940 con Carton 2… y se lo chantamos ahi masomeno’

Riser 01AJ940 con Carton 3No cierra bien el chasis de este servidor, y quedaron unos huecos a los costados de la placa, pero bueno :D

Con esta máquina ensamblada de nuevo, ya podemos empezar a armar en uno de nuestros nodos del Datacenter, un router/firewall con pfSense, virtualizado obvio, pero permitiéndole a la VM capturar el hardware directamente vía PCIe Passthrough.

Es importante destacar que la elección de todo este hardware estuvo directamente basada en las funcionalidades descriptas para poder permitir que las VMs puedan usar la placa de red directamente, sin que el sistema operativo del Host estuviera involucrado.

Interesantemente, antes de comprar este hardware estuve jugando muchísimo con libvirt pensando inocentemente que podría emular todo el datacenter en mi PC de escritorio. En teoría es posible, pero el hardware de mi PC de escritorio es muy malo y no solo no me permite capturar componentes conectados en los slots PCIe, sino que también mi placa de red Realtek es una reverenda mierda. Pude hacer andar todo, si, pero sin buen hardware, las redes virtualizadas en placas de mierda, son tremendamente inestables.

Solo cuando me dí cuenta de esto, entendí que debía comprar hardware de todas maneras. Además, inutilizar mi PC para correr un cluster, tampoco era una opción.

Proxmox

Instalando ProxmoxInstalando Proxmox

Habiendo ganado mucha experiencia (y peleas) con libvirt tuve que tomar una decisión. ¿Uso libvirt o cambio a Proxmox?

Por detalles en los que no voy a ahondar, prácticamente Proxmox fue la decisión correcta. Integra el Hypervisor KVM, soporta Linux Containers (LXC) out-of-the-box y también funciona como cluster. Cada instalación de Proxmox puede funcionar como un nodo único o podés incluir el nodo en un cluster para organizar y distribuir el uso de recursos de containers o VMs.

Toda la explicación del hardware en este artículo converge exactamente en este mapeo de recursos. La VM "pfSense" tiene estos recursos asignados:

Proxmox Intel i350-IOMMUProxmox Intel i350-IOMMU - Dispositivo Mappeado a esta VM con PCI Passthrough

Ahora, al instalar pfSense en una VM hosteada por Proxmox,pfSense detecta los cuatro puertos ethernet como si fueran propios, interactuando directamente con el hardware, sin intermediarios.

pfSense InterfacesInterfases detectadas por pfSense via a Proxmox VM con PCI Passthrough

En este punto, ya tenemos un router andando. Como configurar un router/firewall como pfSense amerita un artículo completamente distinto y mi setup es más complejo que lo que quiero explicar en este. Pero brevemente, tengo 4 interfaces, de los cuales salen 4 cables a el switch de 16 puertos, segmentado o aislados por VLANs para separar las compus viejas, el wifi, la tele y varios dispositivos electrónicos, asi como también no permitir el acceso a mi red a invitados. ¿Porqué? Porque tengo conectado un Windows 3.11 en mi red con File Sharing a un Windows 10, una máquina MS-DOS también con Packet Driver y una 3COM y cualquiera que tuviera una PC conectada a mi red, podría hacer desmanes. Sin contar que la Commodore 64 también está conectada a la red y a la internet.

Mismo procedimiento luego para incluir la otra ThinkCentre en el cluster Proxmox, joinearlo como nodo y conectando un drive NFS para tener almacenamiento suficiente para las VMs.

Y ahi arrancó un nuevo problema.

Demasiado Acceso a Disco

Resulta que cuando empecé a conectar VMs al drive NFS del NAS, los discos empezaron a hacer ruido, y fuerte. Aunque tenga redundancia en los discos a través de SHR, tampoco es que por solamente tenerlo encendido para que las VMs estén disponibles, vayan desgastándolos lentamente acortando su vida útil, eventualmente fallando uno (o varios) de ellos, y tener que reemplazar el disco roto. Y los discos no son algo necesariamente barato. Asi que me puse a ver como hacer para minimizar la escritura a disco de las VMs de Linux que tenia corriendo.

Las distribuciones de Linux, o al menos las más comunes o importantes, tienen settings por default que necesitamos cambiar.

Habiendo cambiado algunas propiedades en el fstab y midiendo el las escrituras y lecturas pude minimizar las escrituras. Pero configurar VM por VM cuando tenés más de diez corriendo, es totalmente impracticable. Especialmente si por cada nueva instalación de una VM, tenés que pasar por los mismos tweaks una y otra vez.

Entonces, a la 3er VM que estaba configurando, pensé en armar un template "base" con todas las cosas configuradas y chantársela a las nuevas VMs en el momento de la creación. Asi, solo configuro una imagen con todos mis settings óptimos y me olvido de cualquier configuración adicional.

Con Packer de Hashicorp, preparar imágenes para VMs es "relativamente" sencillo.

Packer y cloud-init, un solo corazón

Con Packer lo que vamos a hacer es crear una imagen exclusiva para mi cluster (o el tuyo), para que cada nueva VM tenga todo configurado, ahorrándonos un montón de tiempo.

Cuando dije anteriormente que Packer es "relativamente" sencillo, es en principio verdad. Definís una VM para buildear la imágen, conectas un ISO con cloud-init y le decís a la imagen como construirse con subiquity si usas Ubuntu Server o cualquier Debian based Distro.

El tema escomo los layers de abstracción; el orden de ejecución de autoinstall de Ubuntu Server subiquity; y finalmente en que momento diferentes instancias de cloud-init son ejecutadas es increíblemente confuso. cloud-init se ejecuta en varias instancias dependiendo de el contexto de ejecución:

  • La instancia de la creación de la imagen.
  • La instancia del uso de esta imagen en la creación de la VM.

La documentación tampoco ayuda mucho y ni que hablar de las diferencias entre versiones. La parte menos importante de todo esto es el archivo de Packer en si. Asi que lo pasamos rápido, mostrando las partes más importantes:

packer {
  required_plugins {
    name = {
      version = "~> 1"
      source  = "github.com/hashicorp/proxmox"
    }
  }
}

source "proxmox-iso" "proxmox-ubuntu-server-jammy" {
  proxmox_url = var.proxmox_api_url
  username    = var.proxmox_api_token_id
  token       = var.proxmox_api_token_secret
  node        = "px-03"
  vm_id       = "9666"
  vm_name     = "packer-proxmox-ubuntu-server-jammy-1"
  iso_url     = "https://old-releases.ubuntu.com/releases/jammy/ubuntu-22.04.4-live-server-amd64.iso"


  # get the checksum from https://releases.ubuntu.com/22.04/SHA256SUMS
  iso_checksum= "45f873de9f8cb637345d6e66a583762730bbea30277ef7b32c9c3bd6700a32b2"
  iso_storage_pool = var.proxmox_storage_pool

  ssh_username = "ubuntu"
  ssh_password = "ubuntu"
  ssh_timeout  = "10m"

  cloud_init = false
  cloud_init_storage_pool = var.proxmox_storage_pool

  [...]

  template_name        = "packer-ubuntu-server-jammy"
  template_description = "Packer generated Ubuntu 22.04"
  unmount_iso          = true

  additional_iso_files {
    cd_files = [
      "./http/meta-data",
      "./http/user-data",
    ]
    cd_label         = "cidata"
    unmount          = true
    iso_storage_pool = var.proxmox_storage_pool
  }


  memory     = "4096"
  cores      = "4"
  sockets    = "1"
  os         = "l26"
  machine    = "q35"
  qemu_agent = true

  scsi_controller = "virtio-scsi-pci"

  disks {
    disk_size         = "8G"
    # qcow2 over raw, because NFS in my system
    # but it's not supported by terraform proxmox (???)
    # update: months after, it's supported. Todo piola!
    # https://forum.proxmox.com/threads/raw-vs-qcow2.34566/
    format            = "qcow2"
    storage_pool      = var.proxmox_disks_nas_storage_pool #Specify your storage pool
    #storage_pool_type = "zfs"   #Specify pool type
    type              = "virtio"
    # writeback increase of r/w speed
    # https://forum.proxmox.com/threads/virtio-vs-scsi.52893/
    cache_mode        = "writeback"
  }

  [...]
}

# Build Definition to create the VM Template
build {

  name    = "proxmox-ubuntu-server-jammy"
  sources = ["source.proxmox-iso.proxmox-ubuntu-server-jammy"]

  # Provisioning the VM Template for Cloud-Init Integration in Proxmox #1
  provisioner "shell" {
    inline = [
      "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting for cloud-init...'; sleep 1; done",
      "sudo rm /etc/ssh/ssh_host_*",
      "sudo truncate -s 0 /etc/machine-id",
      "sudo apt -y autoremove --purge",
      "sudo apt -y clean",
      "sudo apt -y autoclean",
      "sudo cloud-init clean",
      "sudo rm -f /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg",
      "sudo sync"
    ]
  }

  [...]

}



Filesystem Mount Options

Una de las cosas que queremos hacer para minimizar el acceso da disco, es que en momento de provisionar la imagen o el template que estamos armando con packer, se hacen configuraciones de algunos flags de varios componentes del sistema operativo. Como por ejemplo, en el fstab generalmente tenemos algunas líneas como la siguiente:

UUID=b22312e6-a495-4a5b-959f-4d73ec97e9bf /               ext4    errors=remount-ro 0       1

Como default, esta bien, pero nosotros necesitamos para nuestro escenario particular, donde queremos minimizar las escrituras a disco, un fstab que se vea más o menos así:

UUID=b22312e6-a495-4a5b-959f-4d73ec97e9bf /               ext4    discard,noatime,nodiratime,errors=remount-ro 0       1

Donde:

  • discard - le dice al device que descarte blocks. Si el dispositivo no lo soporta, no tiene efecto
  • noatime - no guardes información sobre el tiempo de acceso a archivos
  • noadate - no guardes información sobre la fecha de acceso a archivos
  • nodiratime- no guardes el tiempo de acceso a directorios

Reconfigurar journald

El journald en su configuración por defecto escribe una banda a disco, guardando todos los logs posibles. En años de experiencia, debo decir también que esta es solo mi opinión, que hay dos instancias donde se revisan los logs. La primera es cuando por colectarlos y procesarlos, salta alguna alerta para revisar. La segunda, es cuando ya es demasiado tarde. Si algún proceso no está funcionando más, esa instancia de logs de systemd/journald va a dejar de producir logs. Entonces es innecesario estar guardando logs sobre el estado de cada una de las aplicaciones a disco si son consumidos desde otros procesadores de logs. Por eso, desactivamos y configuramos algunas cosas de journald para que los logs queden en memoria. Para esto, en nuestro packer debemos lograr escribir /etc/systemd/journald.conf.d/50-local.conf con la siguiente configuración:

[Journal]
Storage=volatile
RuntimeMaxUse=128M
ForwardToSyslog=no

sysctl swappiness a 1

Hay un parámetro del Kernel de Linux que controla el swapping de la información de una aplicación (como páginas anónimas) de la memoria física a la memoria virtual al disco. Se llama vm.swappiness. Este parámetro va de 0 a 100.

Mientras más alto este parámetro, más posibilidades hay que procesos inactivos swappeen fuera de la memoria física; mientras más bajo el valor, se swappea menos, forzando los bufferes del filesystem que se vacíen.

Entonces, bajemos la probabilidad al mínimo seteando este valor a 1 para minimizar la escritura a disco de la siguiente manera:

echo "vm.swappiness = 1" >> /target/etc/sysctl.conf

Clavándola con autoinstall, user-data y Packer

Todas estas configuraciones están barbaras, pero las tenemos que cristalizar en un template de proxmox para que cuando empecemos a crear VMs con terraform, no solo algunos packetes esten en el template/imagen preconfiguradas, pero también algunos settings en el sistema operativo.

Recordemos, esta configuración es para mi cluster. Cada datacenter, arquitectura, cableado, discos, servidores tendrá sus propias particularidades. En mi caso, escritura a disco era un problema. Entonces, ¿Cómo armamos esa imagen para nuestro "cluster super ecológico" ? ¿Cómo lo automatizamos?

En nuestro archivo pkr donde definimos nuestra imagen, tenemos una sección donde podemos definir user-data donde declaramos los pasos que autoinstall va a ejecutar en la creación del template:

source "proxmox-iso" "proxmox-ubuntu-server-jammy" {
[...]
  additional_iso_files {
    cd_files = [
      "./http/meta-data",
      "./http/user-data",
    ]
    cd_label         = "cidata"
    unmount          = true
    iso_storage_pool = var.proxmox_storage_pool
  }
[...]
}

Las secciones relevantes de nuestro archivo user-data son las siguientes:

#cloud-config

# autoinstall reference
# https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html#user-data
autoinstall:
autoinstall:
  version: 1
  locale: en_US
  keyboard:
    layout: es
[...]
  storage:
    #layout:
    #  name: direct
    config:
      - id: disk0
        type: disk
        wipe: superblock-recursive
        ptable: msdos
        model: QEMU HARDDISK
        path: /dev/vda
        name: system-disk
        preserve: false
        grub_device: true
      - id: disk0-part1
        device: disk0
        type: partition
        #partition_type: EF02
        name: boot-partition
        number: 1
        #size: 512MB
        size: 1GB
        flag: boot
        preserve: false
      - id: disk0-part2
        device: disk0
        type: partition
        preserve: false
        size: 4GB
        resize: true
        name: root-partition
        number: 2
        #wipe: superblock
      - id: disk0-part1-format-boot
        type: format
        volume: disk0-part1
        fstype: ext4
        label: boot
      - id: disk0-part2-format-root
        volume: disk0-part2
        type: format
        fstype: ext4
        label: root
      - id: disk0-part1-mount-boot
        type: mount
        path: /boot
        device: disk0-part1-format-boot
        options: 'discard,noatime,nodiratime,errors=remount-ro'        
      - id: disk0-part1-mount-root
        type: mount
        path: /
        device: disk0-part2-format-root
        options: 'discard,noatime,nodiratime,errors=remount-ro'
    swap:
      size: 0

  late-commands:
  - |
    rm /target/etc/netplan/00-installer-config.yaml
    cat <<EOF > /target/etc/netplan/80-my-net.yaml
    network:
      version: 2
      #renderer: networkd
      ethernets:
        enp18:
          match:
            name: enp*
          set-name: eth0
          dhcp4: true    
    EOF
  # configure journald to minimize disk writes
  - |
    mkdir /target/etc/systemd/journald.conf.d
    cat <<EOF > /target/etc/systemd/journald.conf.d/50-local.conf
    [Journal]
    Storage=volatile
    RuntimeMaxUse=128M
    ForwardToSyslog=no    
    EOF
  - echo "vm.swappiness = 1" >> /target/etc/sysctl.conf
  


  user-data:
    package_upgrade: false
    hostname: packerimage
    manage_etc_hosts: true
    timezone: Europe/Vienna
    users:
      - name: ubuntu
        groups: [adm, sudo]
        lock-passwd: false
        sudo: ALL=(ALL) NOPASSWD:ALL
        shell: /bin/bash
        passwd: "$6$exDY1mhS4KUYCE/2$zmn9ToZwTKLhCw.b4/b.ZRTIZM30JZ4QrOQ2aOXJ8yk96xpcCof0kxKwuX1kqLG/ygbJ1f8wxED22bTL4F46P0" #ubuntu

En el id disk0-part1-mount-root y el id disk0-part1-mount-boot declaramos las opciones de montaje de la unidad como habíamos mencionado en la sección anterior de este artículo:

        options: 'discard,noatime,nodiratime,errors=remount-ro'
    swap:
      size: 0

De yapa, también decimos en la sección storage: que no vamos a tener swap.

  late-commands:
  - |
    rm /target/etc/netplan/00-installer-config.yaml
    cat <<EOF > /target/etc/netplan/80-my-net.yaml
    network:
      version: 2
      #renderer: networkd
      ethernets:
        enp18:
          match:
            name: enp*
          set-name: eth0
          dhcp4: true    
    EOF
  # configure journald to minimize disk writes
  - |
    mkdir /target/etc/systemd/journald.conf.d
    cat <<EOF > /target/etc/systemd/journald.conf.d/50-local.conf
    [Journal]
    Storage=volatile
    RuntimeMaxUse=128M
    ForwardToSyslog=no    
    EOF
  - echo "vm.swappiness = 1" >> /target/etc/sysctl.conf
  

Luego, además de cambiar el nombre de las interfaces de red de la que se usa en ubuntu (enp*) a la vieja y querida definición eth0. Lo que sigue luego es sencillo, configuramos journald con los settings que discutimos anteriormente y seteamos a 1 el swappiness en /etc/sysctl.conf. Es importante saber, que en esta fase de configuración, el disco donde se está instalando el sistema operativo esta montado en el mountpoint "/target"

Una ves que tenemos todas nuestras definiciones armadas, ejecutamos:

$ packer build -force -var-file=vars.pkrvars.hcl . 

Luego que termine de ejecutar, un template va a estar disponible en nuestro nodo proxmox para poder referenciarlo en terraform y usarlo como template base para instalar nuestra propia imagen Linux en cada nueva Virtual Machine.

Terraform con Proxmox

Primero lo primero. Definir nuestros providers:

terraform {

  required_version = ">= 1.7.0"

  required_providers {
    proxmox = {
      source = "bpg/proxmox"
      version = "0.66.2"
    }
    ssh = {
      source = "loafoe/ssh"
      version = "2.7.0"
    }
    tls = {
      source  = "hashicorp/tls"
      version = ">= 4.0"
    }
  }
}

provider "proxmox" {
  endpoint = var.proxmox_api_url

  username = "root@pam" # for SSH
  password = var.proxmox_ssh_password  # SSH password

  insecure = true

  ssh {
    agent = true
  }
}

Tener en cuenta que este ambiente no es apto para un ambiente de producción. En caso de querer autenticarnos con proxmox a través de terraform, debemos crear una API Key de manera segura con la lista de permisos permitidos y mínimos para que terraform pueda interactuar con el Proxmox cluster.

Después de configurar API Keys, usuarios, SSH y accesos de Proxmox para el provider de Terraform en mi máquina local y ya teniendo la imagen construida con Packer podemos empezar a levantar con nuestros scripts de Terraform, un pequeño cluster Kubernetes, como ejemplo.

Voy a crear con las siguientes definiciones dos VMs. Una con un controlplane y la otra con un agent de la siguiente manera:

variable k3s_control_plane_instances {
  description = "Proxmox/k3s number of control planes for this cluster"
  type        = number
  default     = 1
}

variable k3s_agent_instances {
  description = "Proxmox/k3s number of agents for this cluster"
  type        = number
  default     = 1
}

Como ejemplo para levantar un Kubernetes Control-Plane y un Kubernetes Agent. Todo parece muy complicado, pero escencialmente es armar unas VMs comunes y corrientes y correrles un cloudinit dependiendo si son control planes o agents.

Lo que si, hay un detalle. Para poder controlar todo el cluster Kubernetes es importante poder recuperar su archivo kubeconfig. Con todo esto dicho, la configuración del primer control-plane:

resource "proxmox_virtual_environment_file" "k3s_control_plane_cloudinit" {
    count        = var.k3s_control_plane_instances
    node_name    = var.proxmox_main_node_name
    content_type = "snippets"
    # it is needed to add a "snippet" storage in proxmox node
    # at Datacenter>Storage section
    datastore_id = "terraform_snippets"
    [...]
    source_raw {
        data = <<EOF
        [...]

        write_files: 
            - path: /root/control-plane-setup.sh
              permissions: 0744
              content: |
                #!/bin/bash
                if [[ `hostname` == "k3s-control-plane-0" ]]; then
                 echo "curl -fL https://get.k3s.io | sh -s - server --cluster-init --token \"secret\"  --write-kubeconfig-mode \"0644\" --disable traefik --disable servicelb --bind-address \`hostname -i\` --tls-san \"k3s-control-plane-${count.index}.tsh\" "
                else
                 echo "curl -fL https://get.k3s.io | sh -s - server --token \"secret\"  --write-kubeconfig-mode \"0644\" --disable traefik --disable servicelb --bind-address \`hostname -i\`  --server https://k3s-control-plane-0.tsh:6443 --tls-san \"k3s-control-plane-${count.index}.tsh\" "
                fi    
        [...]

        EOF
        file_name = "k3s-control-plane-${count.index}-cloud-config.yaml"
    }
}

Que lindo tener mi propio TLD (top level domain) llamado .tsh <3 Todo esto viene de un previamente configurado pfSense. Como se construye toda la parte de red amerita otro artículo.

IMPORTANTE: nótese el --token \"secret\". El token del cluster, no está generado. Esta hardcodeado como secret. Guarda con eso, ¿Eh?. Está mal y el token tiene que ser generado, secreto y fuera de cualquier repositorio y del alcance de los niños.

Cotinuamos con nuestra definición de Virtual Machine para el control-plane:

resource "proxmox_virtual_environment_vm" "k3s_control_plane" {
    count = var.k3s_control_plane_instances
    node_name = var.proxmox_main_node_name
    vm_id     = "505${count.index}" 

    name = "${var.k3s_hostname_prefix}-control-plane-${count.index}"
    description = "k3s cluster managed by Terraform"
    tags = ["terraform", "k3s", "control-plane"]

    agent {
        enabled = true
    }

    machine = "q35"
    cpu {
        architecture    = "x86_64"
        cores           = 2
        sockets         = 1
        type            = "x86-64-v2-AES"
    }

    memory {
        dedicated = 2048
        floating  = 2048 // set equal to dedicated to enable ballooning
    }

    initialization {
        ip_config {
            ipv4 {
                address = "dhcp"
            }
        }

        user_data_file_id = element(proxmox_virtual_environment_file.k3s_control_plane_cloudinit.*.id, count.index)
    }

    // fake mac-addr for pfSense identification
    network_device {
      bridge = "vmbr0"
      mac_address = "ee:ee:ee:00:00:1${count.index}"
    }

    operating_system {
      type = "l26"
    }

    vga {
        memory  = 16
        type    = "std"
    }

    //serial_device {}

    clone {
        vm_id = 9666
        node_name = var.proxmox_main_node_name
        datastore_id = var.proxmox_disks_nas_storage_pool
    }

}


output "control_plane_hostnames" {
    description = "Control Planes Hostnames"
    value = proxmox_virtual_environment_vm.k3s_control_plane.*.name
}

resource "terraform_data" "clean_ssh_known_hosts" {
    count = length(proxmox_virtual_environment_vm.k3s_control_plane) != 0 ? 1 : 0

    depends_on = [ proxmox_virtual_environment_vm.k3s_control_plane ]
    # clean up the first control plane local ssh known_hosts
    provisioner "local-exec" {
        command = "ssh-keygen -f \"/home/$USER/.ssh/known_hosts\" -R \"${proxmox_virtual_environment_vm.k3s_control_plane.0.name}.tsh\""
    }
}


resource "ssh_sensitive_resource" "grab_kubeconfig" {
  count = length(proxmox_virtual_environment_vm.k3s_control_plane) != 0 ? 1 : 0

  # The default behaviour is to run file blocks and commands at create time
  # You can also specify 'destroy' to run the commands at destroy time
  when = "create"

  host     = "${proxmox_virtual_environment_vm.k3s_control_plane.0.name}.tsh" 
  user         = "root"
  private_key = tls_private_key.ssh.private_key_pem

  agent        = false
  # An ssh-agent with your SSH private keys should be running
  # Use 'private_key' to set the SSH key otherwise

  # Try to complete in at most 15 minutes and wait 5 seconds between retries
  timeout     = "5m"
  retry_delay = "5s"

  commands = [
     "cat /etc/rancher/k3s/k3s.yaml"
  ]

  triggers = {
    always_run = "${timestamp()}"
  }

  depends_on = [ terraform_data.clean_ssh_known_hosts ]

}


output "kubeconfig" {
    # grab this output from terraform with the following command
    # terraform output -raw kubeconfig > ~/.kube/config
    value = length(ssh_sensitive_resource.grab_kubeconfig) != 0 ? ssh_sensitive_resource.grab_kubeconfig.0.result : 0
    sensitive = true
}

# finally write the content of kubeconfig to ~/.kube/config automatically
resource "local_sensitive_file" "local_user_kubeconfig" {
    count = length(proxmox_virtual_environment_vm.k3s_control_plane) != 0 ? 1 : 0

    content  = ssh_sensitive_resource.grab_kubeconfig.0.result
    filename = pathexpand("~/.kube/config")

    depends_on = [ ssh_sensitive_resource.grab_kubeconfig ]
}

Creemos el control plane con terraform:


[  2:10PM ]  [ toshi@turmalina:~/dev/alberinfra/terraform(master✗) ]
 $ terraform apply  -var-file="vars.tfvars"
tls_private_key.ssh: Refreshing state... [id=fc8e10ca14b36c8be3389e57006877903259b17f]
proxmox_virtual_environment_file.cloud_config: Refreshing state... [id=terraform_snippets:snippets/cloud-config.yaml]
proxmox_virtual_environment_file.k3s_etcd_cloudinit: Refreshing state... [id=terraform_snippets:snippets/k3s-etcd-cloud-config.yaml]

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

Terraform will perform the following actions:

  # local_sensitive_file.local_user_kubeconfig[0] will be created
  + resource "local_sensitive_file" "local_user_kubeconfig" {
      + content              = (sensitive value)
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0700"
      + file_permission      = "0700"
      + filename             = "/home/toshi/.kube/config"
      + id                   = (known after apply)
    }

[...]

  # proxmox_virtual_environment_vm.k3s_control_plane[0] will be created
  + resource "proxmox_virtual_environment_vm" "k3s_control_plane" {
      + acpi                    = true
      + bios                    = "seabios"
      + description             = "k3s cluster managed by Terraform"
      + id                      = (known after apply)
      + ipv4_addresses          = (known after apply)
      + ipv6_addresses          = (known after apply)
      + keyboard_layout         = "en-us"
      + mac_addresses           = (known after apply)
      + machine                 = "q35"
      + migrate                 = false
      + name                    = "k3s-control-plane-0"
      + network_interface_names = (known after apply)
      + node_name               = "px-03"
      + on_boot                 = true
      + protection              = false
      + reboot                  = false
      + scsi_hardware           = "virtio-scsi-pci"
      + started                 = true
      + stop_on_destroy         = false
      + tablet_device           = true
      + tags                    = [
          + "terraform",
          + "k3s",
          + "control-plane",
        ]
      + template                = false
      + timeout_clone           = 1800
      + timeout_create          = 1800
      + timeout_migrate         = 1800
      + timeout_move_disk       = 1800
      + timeout_reboot          = 1800
      + timeout_shutdown_vm     = 1800
      + timeout_start_vm        = 1800
      + timeout_stop_vm         = 300
      + vm_id                   = 5050

      + agent {
          + enabled = true
          + timeout = "15m"
          + trim    = false
          + type    = "virtio"
        }

      + clone {
          + datastore_id = "cluster_drives"
          + full         = true
          + node_name    = "px-03"
          + retries      = 1
          + vm_id        = 9666
        }

      + cpu {
          + architecture = "x86_64"
          + cores        = 2
          + hotplugged   = 0
          + limit        = 0
          + numa         = false
          + sockets      = 1
          + type         = "x86-64-v2-AES"
          + units        = 1024
        }

      + initialization {
          + datastore_id      = "local-lvm"
          + upgrade           = (known after apply)
          + user_data_file_id = (known after apply)

          + ip_config {
              + ipv4 {
                  + address = "dhcp"
                }
            }
        }

      + memory {
          + dedicated      = 2048
          + floating       = 2048
          + keep_hugepages = false
          + shared         = 0
        }

      + network_device {
          + bridge      = "vmbr0"
          + enabled     = true
          + firewall    = false
          + mac_address = "ee:ee:ee:00:00:10"
          + model       = "virtio"
          + mtu         = 0
          + queues      = 0
          + rate_limit  = 0
          + vlan_id     = 0
        }

      + operating_system {
          + type = "l26"
        }

      + vga {
          + memory = 16
          + type   = "std"
        }
    }

  # ssh_sensitive_resource.grab_kubeconfig[0] will be created
  + resource "ssh_sensitive_resource" "grab_kubeconfig" {
      + agent                              = false
      + bastion_port                       = "22"
      + commands                           = [
          + "cat /etc/rancher/k3s/k3s.yaml",
        ]
      + commands_after_file_changes        = true
      + host                               = "k3s-control-plane-0.tsh"
      + id                                 = (known after apply)
      + ignore_no_supported_methods_remain = false
      + port                               = "22"
      + private_key                        = (sensitive value)
      + result                             = (sensitive value)
      + retry_delay                        = "5s"
      + timeout                            = "5m"
      + triggers                           = (known after apply)
      + user                               = "root"
      + when                               = "create"
    }

  # terraform_data.clean_ssh_known_hosts[0] will be created
  + resource "terraform_data" "clean_ssh_known_hosts" {
      + id = (known after apply)
    }

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

Changes to Outputs:
  ~ control_plane_hostnames = [
      + "k3s-control-plane-0",
    ]
  ~ kubeconfig              = (sensitive value)

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!


proxmox_virtual_environment_file.k3s_control_plane_cloudinit[0]: Creating...
proxmox_virtual_environment_file.k3s_control_plane_cloudinit[0]: Creation complete after 0s [id=terraform_snippets:snippets/k3s-control-plane-0-cloud-config.yaml]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Creating...
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [10s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [20s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [30s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [40s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [50s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m0s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m10s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m20s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m30s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m40s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [1m50s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Still creating... [2m0s elapsed]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Creation complete after 2m5s [id=5050]
terraform_data.clean_ssh_known_hosts[0]: Creating...
terraform_data.clean_ssh_known_hosts[0]: Provisioning with 'local-exec'...
terraform_data.clean_ssh_known_hosts[0] (local-exec): Executing: ["/bin/sh" "-c" "ssh-keygen -f \"/home/$USER/.ssh/known_hosts\" -R \"k3s-control-plane-0.tsh\""]
terraform_data.clean_ssh_known_hosts[0] (local-exec): # Host k3s-control-plane-0.tsh found: line 47
terraform_data.clean_ssh_known_hosts[0] (local-exec): /home/toshi/.ssh/known_hosts updated.
terraform_data.clean_ssh_known_hosts[0] (local-exec): Original contents retained as /home/toshi/.ssh/known_hosts.old
terraform_data.clean_ssh_known_hosts[0]: Creation complete after 0s [id=31f50cec-46e2-b9c9-a343-b93ffce76b42]
ssh_sensitive_resource.grab_kubeconfig[0]: Creating...
ssh_sensitive_resource.grab_kubeconfig[0]: Still creating... [10s elapsed]
ssh_sensitive_resource.grab_kubeconfig[0]: Still creating... [20s elapsed]
ssh_sensitive_resource.grab_kubeconfig[0]: Still creating... [30s elapsed]
ssh_sensitive_resource.grab_kubeconfig[0]: Creation complete after 32s [id=7996163503264882213]
local_sensitive_file.local_user_kubeconfig[0]: Creating...
local_sensitive_file.local_user_kubeconfig[0]: Creation complete after 0s [id=4f1a874c917b0ba076bf53abab934f6105f5e266]

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

Outputs:

control_plane_hostnames = [
  "k3s-control-plane-0",
]
kubeconfig = <sensitive>
private_key = <sensitive>    

Tuqui. :)

Mientras ejecutramos terraform, esta preciousura empieza a ronronear:

Datacenter_1Vista frontal, con línea roja para urgencias conectada a la CCC EPVPN

Datacenter_2Vista d’cotè

Datacenter_3Switch principal

Datacenter_4Vista dorsal, la máquina inferior corre pfSense y distribuye la internet a todas las partes de la casa

Muy bueno el rack de madera ¿no? Heh, bueno. Seguimos.

Hacemos lo mismo, agregando un agente para que el control-plane no se coma toda la carga de lo que vamos a hacer más tarde:

resource "proxmox_virtual_environment_vm" "k3s_agent" {
    count = var.k3s_agent_instances
    node_name = var.proxmox_main_node_name
    vm_id     = "507${count.index}" 

    name = "${var.k3s_hostname_prefix}-agent-${count.index}"
    description = "k3s cluster managed by Terraform"
    tags = ["terraform", "k3s", "agent"]

    started = true

    agent {
        enabled = true
    }

    machine = "q35"
    cpu {
        architecture    = "x86_64"
        cores           = 2
        sockets         = 1
        type            = "x86-64-v2-AES"
    }

    memory {
        dedicated = 1024 * 4
        floating  = 1024 * 4 // set equal to dedicated to enable ballooning
    }

    // overlay the disk that comes with `packer`
    // this settings are the same as configured
    // in our ubuntu packer template for proxmox
    disk {
        interface    = "virtio0" # overlay the first disk
        datastore_id = var.proxmox_disks_nas_storage_pool
        size         = 8  # in gigabytes

        cache        = "writeback"
        file_format  = "qcow2"
        replicate    = false
    }


    // second disk -- this gives the agents more disk space 
    // than the other VMs
    disk {
        interface    = "virtio1" # attach it as a second disk
        datastore_id = "cluster_drives"
        size         = 200  # in gigabytes

        cache        = "writeback"
        file_format  = "qcow2"
        replicate    = false
    }

    initialization {
        ip_config {
            ipv4 {
                address = "dhcp"
            }
        }

        user_data_file_id = element(proxmox_virtual_environment_file.k3s_agent_cloudinit.*.id, count.index)
    }

    network_device {
      bridge = "vmbr0"
      mac_address = "ee:ee:ee:00:00:5${count.index}"
    }

    operating_system {
      type = "l26"
    }

    vga {
        memory  = 16
        type    = "std"
    }

    //serial_device {}

    clone {
        vm_id = 9666
        node_name = var.proxmox_main_node_name
        datastore_id = var.proxmox_disks_nas_storage_pool
    }

    depends_on = [ proxmox_virtual_environment_vm.k3s_control_plane ]

}

[  2:18PM ]  [ toshi@turmalina:~/dev/alberinfra/terraform(master✗) ]
 $ terraform apply  -var-file="vars.tfvars"
tls_private_key.ssh: Refreshing state... [id=fc8e10ca14b36c8be3389e57006877903259b17f]
proxmox_virtual_environment_file.cloud_config: Refreshing state... [id=terraform_snippets:snippets/cloud-config.yaml]
proxmox_virtual_environment_file.k3s_etcd_cloudinit: Refreshing state... [id=terraform_snippets:snippets/k3s-etcd-cloud-config.yaml]
proxmox_virtual_environment_file.k3s_control_plane_cloudinit[0]: Refreshing state... [id=terraform_snippets:snippets/k3s-control-plane-0-cloud-config.yaml]
proxmox_virtual_environment_vm.k3s_control_plane[0]: Refreshing state... [id=5050]
terraform_data.clean_ssh_known_hosts[0]: Refreshing state... [id=31f50cec-46e2-b9c9-a343-b93ffce76b42]
ssh_sensitive_resource.grab_kubeconfig[0]: Refreshing state... [id=7996163503264882213]
local_sensitive_file.local_user_kubeconfig[0]: Refreshing state... [id=4f1a874c917b0ba076bf53abab934f6105f5e266]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
-/+ destroy and then create replacement

Terraform will perform the following actions:
[ ... ]

  # proxmox_virtual_environment_vm.k3s_agent[0] will be created
  + resource "proxmox_virtual_environment_vm" "k3s_agent" {
      + acpi                    = true
      + bios                    = "seabios"
      + description             = "k3s cluster managed by Terraform"
      + id                      = (known after apply)
      + ipv4_addresses          = (known after apply)
      + ipv6_addresses          = (known after apply)
      + keyboard_layout         = "en-us"
      + mac_addresses           = (known after apply)
      + machine                 = "q35"
      + migrate                 = false
      + name                    = "k3s-agent-0"
      + network_interface_names = (known after apply)
      + node_name               = "px-03"
      + on_boot                 = true
      + protection              = false
      + reboot                  = false
      + scsi_hardware           = "virtio-scsi-pci"
      + started                 = true
      + stop_on_destroy         = false
      + tablet_device           = true
      + tags                    = [
          + "terraform",
          + "k3s",
          + "agent",
        ]
      + template                = false
      + timeout_clone           = 1800
      + timeout_create          = 1800
      + timeout_migrate         = 1800
      + timeout_move_disk       = 1800
      + timeout_reboot          = 1800
      + timeout_shutdown_vm     = 1800
      + timeout_start_vm        = 1800
      + timeout_stop_vm         = 300
      + vm_id                   = 5070

      + agent {
          + enabled = true
          + timeout = "15m"
          + trim    = false
          + type    = "virtio"
        }

      + clone {
          + datastore_id = "cluster_drives"
          + full         = true
          + node_name    = "px-03"
          + retries      = 1
          + vm_id        = 9666
        }

      + cpu {
          + architecture = "x86_64"
          + cores        = 2
          + hotplugged   = 0
          + limit        = 0
          + numa         = false
          + sockets      = 1
          + type         = "x86-64-v2-AES"
          + units        = 1024
        }

      + disk {
          + aio               = "io_uring"
          + backup            = true
          + cache             = "writeback"
          + datastore_id      = "cluster_drives"
          + discard           = "ignore"
          + file_format       = "qcow2"
          + interface         = "virtio0"
          + iothread          = false
          + path_in_datastore = (known after apply)
          + replicate         = false
          + size              = 8
          + ssd               = false
        }
      + disk {
          + aio               = "io_uring"
          + backup            = true
          + cache             = "writeback"
          + datastore_id      = "cluster_drives"
          + discard           = "ignore"
          + file_format       = "qcow2"
          + interface         = "virtio1"
          + iothread          = false
          + path_in_datastore = (known after apply)
          + replicate         = false
          + size              = 200
          + ssd               = false
        }

      + initialization {
          + datastore_id      = "local-lvm"
          + upgrade           = (known after apply)
          + user_data_file_id = (known after apply)

          + ip_config {
              + ipv4 {
                  + address = "dhcp"
                }
            }
        }

      + memory {
          + dedicated      = 4096
          + floating       = 4096
          + keep_hugepages = false
          + shared         = 0
        }

      + network_device {
          + bridge      = "vmbr0"
          + enabled     = true
          + firewall    = false
          + mac_address = "ee:ee:ee:00:00:50"
          + model       = "virtio"
          + mtu         = 0
          + queues      = 0
          + rate_limit  = 0
          + vlan_id     = 0
        }

      + operating_system {
          + type = "l26"
        }

      + vga {
          + memory = 16
          + type   = "std"
        }
    }

  # ssh_sensitive_resource.grab_kubeconfig[0] must be replaced
-/+ resource "ssh_sensitive_resource" "grab_kubeconfig" {
      ~ id                                 = "7996163503264882213" -> (known after apply)
      ~ result                             = (sensitive value)
      ~ triggers                           = { # forces replacement
          - "always_run" = "2024-12-17T13:14:11Z"
        } -> (known after apply) # forces replacement
        # (12 unchanged attributes hidden)
    }

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

Changes to Outputs:
  ~ kubeconfig              = (sensitive value)
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

local_sensitive_file.local_user_kubeconfig[0]: Destroying... [id=4f1a874c917b0ba076bf53abab934f6105f5e266]
local_sensitive_file.local_user_kubeconfig[0]: Destruction complete after 0s
proxmox_virtual_environment_file.k3s_agent_cloudinit[0]: Creating...
ssh_sensitive_resource.grab_kubeconfig[0]: Destroying... [id=7996163503264882213]
ssh_sensitive_resource.grab_kubeconfig[0]: Destruction complete after 0s
ssh_sensitive_resource.grab_kubeconfig[0]: Creating...
proxmox_virtual_environment_file.k3s_agent_cloudinit[0]: Creation complete after 0s [id=terraform_snippets:snippets/k3s-agent-0-cloud-config.yaml]
proxmox_virtual_environment_vm.k3s_agent[0]: Creating...
ssh_sensitive_resource.grab_kubeconfig[0]: Creation complete after 0s [id=7130346671968112612]
local_sensitive_file.local_user_kubeconfig[0]: Creating...
local_sensitive_file.local_user_kubeconfig[0]: Creation complete after 0s [id=4f1a874c917b0ba076bf53abab934f6105f5e266]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [10s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [20s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [30s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [40s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [50s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m0s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m10s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m20s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m30s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m40s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [1m50s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [2m0s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Still creating... [2m10s elapsed]
proxmox_virtual_environment_vm.k3s_agent[0]: Creation complete after 2m13s [id=5070]

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

Outputs:

control_plane_hostnames = [
  "k3s-control-plane-0",
]
kubeconfig = <sensitive>
private_key = <sensitive>

Próximos Pasos

Una vez que ya tenemos andando nuestro cluster Kubernetes, podemos hacer con él lo que queramos. Lo que hice en mi caso fue instalar GitLab y FluxCD para aplicar el framework GitOps y tener un Single Source of Truth.

Flux Bootstrap ReconcileFlux Bootstrap Reconcile

En mi caso, toda la infraestructura, kubernetes y las aplicaciones y containers que corren sobre el cluster, estan todas definidas en un único repositorio que Flux está mirando constantemente cuando hay nuevos pushes en el repositorio donde están todas las definiciones. Cualquier cambio que se haga en mi repo, modifica alguna parte del cluster.

Corriendo FluxCDFlux y podinfo corriendo en mi cluster k8s

Y en caso de querer mandar todo a la mierda y volar el cluster, solo basta con un dulce y tierno terraform destroy :D

Si llegaste hasta acá y te sirvió: ¡Salud!