Despliega un clúster de Kubernetes listo para producción en Azure con Terraform

Jeroen Bach

Jeroen Bach · Linkedin

11 min read ·

En esta guía, aprenderás a crear una solución de Terraform completamente modular y reutilizable, desplegando recursos en Azure, Kubernetes y Cloudflare.

En mi artículo anterior, aprendiste cómo configurar un clúster de Kubernetes y ejecutar Plausible Analytics usando una serie de comandos CLI. Si bien ese enfoque funciona, no es ideal. Una solución mejor y más sostenible es usar Terraform. Con Terraform, describes tu infraestructura en su estado deseado, y Terraform determina los pasos necesarios para llegar allí.

También he usado helmfile antes, que es excelente para gestionar lanzamientos de Helm. Pero lo que me gusta de Terraform es que va más allá. No se detiene en Kubernetes, te da una solución única para definir todos los recursos en tu stack.

Beneficios clave

  • Definir estado deseado - No más scripts CLI manuales o desviación de configuración (tu entorno siempre representa tu código)
  • Recuperable - Dado que has definido tu estado deseado, es fácil recuperar un entorno completo con todas sus configuraciones
  • Diseño modular - Crea módulos que pueden reutilizarse en diferentes entornos
  • Agnóstico de entorno - Despliega en Azure, Kubernetes, Cloudflare y muchas más plataformas
  • Control de versiones - Rastrea cambios de infraestructura en control de versiones, manteniendo un registro de auditoría completo

Configurar tu solución como se describe en este artículo extiende esos beneficios con:

  • HTTPS automático - Certificados Let's Encrypt para todas tus aplicaciones expuestas
  • Optimizado en costos - Solo una IP pública, una configuración de VM rentable y sin discos de VM adicionales
  • Sin desviación de configuración - estado de infraestructura almacenado centralmente
  • Seguridad de datos - Los discos de Azure almacenan tus datos, permitiendo respaldo y restauración fáciles a través de instantáneas o backup-vault

Requisitos previos

Asegúrate de que las siguientes herramientas estén instaladas en tu máquina: Azure CLI, Terraform y herramientas de Kubernetes. O alternativamente usa Cloud Shell en Azure Portal, donde todas las herramientas necesarias están preinstaladas. Cuando uses Cloud Shell, monta el clouddrive para persistir todo entre sesiones de shell: clouddrive mount y cd clouddrive.

Si estás en Windows, por favor usa Cloud Shell o WSL, ya que todos los scripts están escritos en bash.

Comenzando

Hay un pequeño paso manual que debemos tomar antes de poder sumergirnos en Terraform. Nuestra solución necesita un backend (almacenamiento) para el archivo de estado, por lo que primero necesitamos crear una cuenta de almacenamiento con un contenedor en Azure. Hay un script práctico en nuestro proyecto Terraform que hace esto por ti. Así que primero obtengamos nuestro proyecto Terraform.

Puedes hacer esto rápidamente ejecutando el siguiente script de shell:

download-terraform-project.sh
#!/usr/bin/env bash
# Descargar y extraer el proyecto terraform del repositorio
curl -L "https://github.com/jeroenbach/bach.software/archive/refs/heads/main.zip" -o "bach.software-terraform.zip"
unzip -q "bach.software-terraform.zip" "bach.software-main/src/app/examples/post4/terraform/*"

# Mover la carpeta extraída al directorio actual y eliminar el zip y la carpeta extraída
mv "bach.software-main/src/app/examples/post4/terraform" "./terraform"
rm -rf "bach.software-terraform.zip" "bach.software-main"

# Navegar al directorio terraform
cd "./terraform"

Antes de ejecutar Terraform o cualquiera de los siguientes scripts, asegúrate siempre de haber iniciado sesión en la suscripción correcta de Azure usando az login.

Creemos la cuenta de almacenamiento y el contenedor para nuestro estado de Terraform:

az login
./scripts/create-tfstate-storage.sh

Ejecutando Terraform

Al ejecutar terraform apply, Terraform solicitará algunas variables de entrada. Puedes encontrar los valores necesarios en el archivo input-output.tf en la misma carpeta. También puedes crear un archivo terraform.tfvars con los valores necesarios, para no tener que ingresarlos cada vez.

Nota: Asegúrate de no incluir tus archivos .tfvars en el control de versiones

terraform.tfvars
azure_subscription_id = "<azure-subscription-id>"
azure_cluster_name    = "aks-westeu-prod"
cloudflare_api_token  = "<cloudflare-api-token>"
cloudflare_zone_id    = "<cloudflare-zone-id>"
plausible_dns         = "plausible.example.com"
letsencrypt_email     = "[email protected]"

Antes de comenzar, asegúrate de tener la información requerida disponible. Puedes crear una cuenta gratuita de Cloudflare y vincularla a un DNS que poseas, o crear un nuevo DNS. Puedes crear un token API siguiendo estas instrucciones y usar la plantilla "Edit zone DNS". Puedes encontrar tu ID de zona siguiendo estas instrucciones. Puedes encontrar tu ID de suscripción de Azure siguiendo estas instrucciones.

Si no especificas las variables de Cloudflare, el DNS no se actualizará, pero todo lo demás seguirá funcionando y se te mostrará la dirección IP (para usar para acceder a Plausible) al final. Necesitas crear un registro DNS con esta IP tú mismo, ya que el emisor de certificados lo necesita para validar el registro DNS antes de poder emitir un certificado válido.

Ahora, desplieguemos tu entorno:

# Nombre del entorno: Azure Kubernetes Service - Western Europe - Production
cd aks-westeu-prod
terraform init
terraform apply

Terraform:

  • Desplegará el clúster AKS
  • Instalará Plausible a través de Helm
  • Actualizará el DNS de Cloudflare

Respaldo y Restauración (Opcional)

En tu grupo de recursos rg-nodes-aks-westeu-prod, encontrarás los dos discos de Azure que contienen todos los datos de la solución Plausible: pv-disk-plausible-analytics-v3-clickhouse-0 y pv-disk-plausible-analytics-v3-postgresql-0. Puedes crear respaldos por hora, diarios o semanales de esos discos usando Azure Backup Vault.

Para restaurar un respaldo, crea una instantánea del respaldo específico a un grupo de recursos y completa los ID de instantánea en las siguientes variables que se encuentran en el archivo aks-westeu-prod/input-output.tf: postgresql_restore_snapshot_id y clickhouse_restore_snapshot_id.

La próxima vez que ejecutes terraform apply, Plausible se restaurará con los respaldos.

Destruyendo el entorno

Para destruir el entorno y todos los recursos asociados, puedes ejecutar el siguiente comando:

terraform destroy

Estructura de la solución

Para hacer que la solución funcione de principio a fin, hubo algunos obstáculos que superar. En este capítulo, examinaré esos obstáculos y cómo los resolví, pero primero déjame proporcionar una descripción general de la solución.

terraform/
├── aks-westeu-prod/
   ├── app-plausible.tf
   ├── aks-cluster.tf
   └── provider.tf
├── helm-charts/
   ├── letsencrypt-cert-issuer/
   ├── templates/
   ├── letsencrypt-cluster-issuer-staging.yaml
   └── letsencrypt-cluster-issuer.yaml
   ├── Chart.yaml
   └── values.yaml
├── modules/
   ├── aks-cluster/
   ├── aks-cluster.tf
   ├── ingress-and-certificates.tf
   └── input-output.tf
   ├── persistent-azure-disk-volume/
   ├── input.tf
   └── persistent-azure-disk-volume.tf
   └── plausible/
   ├── disks.tf
   ├── input.tf
   ├── namespace.tf
   └── plausible.tf
├── scripts/
   ├── create-tfstate-storage.sh
   └── download-terraform-project.sh
  • aks-westeu-prod: Una configuración de entorno de producción para desplegar en Azure West Europe. Puedes usar esta carpeta como plantilla para crear más entornos. Los archivos con prefijo app- muestran las diferentes aplicaciones instaladas en el clúster.
  • helm-charts: Charts de Helm personalizados
    • letsencrypt-cert-issuer: En lugar de desplegar los recursos ClusterIssuer por separado, los empaqué en un chart de Helm
  • modules: Cada módulo encapsula una responsabilidad específica
    • aks-cluster: Despliega un clúster AKS con emisor de certificados Let's Encrypt, nginx ingress como balanceador de carga, y espera a que la IP pública esté disponible
    • persistent-azure-disk-volume: Crea un disco de Azure o restaura uno usando una instantánea y luego crea un volumen persistente y reclamo de volumen persistente en Kubernetes
    • plausible: Instala Plausible y sus dependencias a través de Helm

Obstáculo: Detalles de conexión del nuevo clúster aún no disponibles

Después de crear el clúster de Kubernetes, queremos poder desplegar recursos en él. Pero en la etapa de planificación de Terraform, la información sobre cómo conectarse a este nuevo entorno aún no está disponible. Por lo tanto, tuvimos que tomar dos pasos para crear un despliegue sin problemas.

  • Configuración dinámica del proveedor: La información del clúster AKS se establece dinámicamente para los proveedores Helm y Kubernetes recuperando la información de conexión del clúster recién creado:
aks-westeu-prod/provider.tf
provider "helm" {
  kubernetes = {
    # Usar configuración dinámica de proveedor para usar el clúster recién creado directamente
    host                   = module.aks_cluster.kube_config.host
    client_certificate     = base64decode(module.aks_cluster.kube_config.client_certificate)
    client_key             = base64decode(module.aks_cluster.kube_config.client_key)
    cluster_ca_certificate = base64decode(module.aks_cluster.kube_config.cluster_ca_certificate)
  }
}

provider "kubernetes" {
  # Usar configuración dinámica de proveedor para usar el clúster recién creado directamente
  host                   = module.aks_cluster.kube_config.host
  client_certificate     = base64decode(module.aks_cluster.kube_config.client_certificate)
  client_key             = base64decode(module.aks_cluster.kube_config.client_key)
  cluster_ca_certificate = base64decode(module.aks_cluster.kube_config.cluster_ca_certificate)
}
  • Establecer el contexto kubectl local: Después de crear el clúster AKS, escribimos la nueva configuración kube y establecemos el contexto kubectl en la máquina local, de esta manera los comandos local-exec pueden conectarse inmediatamente al nuevo clúster.
modules/aks-cluster/aks-cluster.tf
resource "null_resource" "set_kube_context" {
  provisioner "local-exec" {
    command = <<EOT
      # Lo obtenemos del estado de Terraform y lo agregamos al kubeconfig
      echo '${azurerm_kubernetes_cluster.aks_cluster.kube_config_raw}' > ~/.kube/config
      export KUBECONFIG=~/.kube/config
      kubectl config use-context ${azurerm_kubernetes_cluster.aks_cluster.name}
    EOT
  }

  // Siempre establecer el contexto kube al ejecutar apply, incluso si no se realizaron cambios en el clúster
  triggers = {
    always_run = "${timestamp()}"
  }

  depends_on = [azurerm_kubernetes_cluster.aks_cluster]
}

Obstáculo: IP del Load Balancer aún no disponible

Al desplegar un lanzamiento de helm, terraform termina antes de que el lanzamiento se despliegue completamente. Tampoco proporciona la información de IP del balanceador de carga. Por lo tanto, implementé dos scripts locales que esperan el despliegue del ingress nginx y recopilan la IP del balanceador de carga, que se necesita para actualizar tu DNS.

modules/aks-cluster/ingress-and-certificates.tf
# Esperar a que el lanzamiento de helm ingress-nginx se despliegue
resource "null_resource" "wait_for_ingress_nginx" {
  provisioner "local-exec" {
    command = <<EOT
      for i in {1..30}; do
        kubectl get svc -n ingress-nginx ${helm_release.ingress_nginx.name}-controller && sleep 30 && break || sleep 30;
      done
    EOT
  }

  depends_on = [helm_release.ingress_nginx]
}

# Obtener IP externa usando kubectl
data "external" "ingress_external_ip" {
  program = ["bash", "-c", <<EOT
    EXTERNAL_IP=$(kubectl get svc -n ingress-nginx ${helm_release.ingress_nginx.name}-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
    echo "{\"ip\":\"$EXTERNAL_IP\"}"
  EOT
  ]

  depends_on = [null_resource.wait_for_ingress_nginx]
}

Obstáculo: Los datos en nuestra solución no están seguros

Al usar el chart de helm de plausible, crea dos bases de datos: PostgreSQL y ClickHouse. Por defecto, estas bases de datos usan almacenamiento efímero, lo que significa que cuando el pod se elimina o reprograma, se pierden todos los datos. Para asegurarnos de que nuestros datos estén seguros, necesitamos usar almacenamiento persistente. En un entorno en la nube como Azure, podemos usar discos de Azure para esto.

He creado un módulo para crear o restaurar un disco de Azure y conectarlo en Kubernetes creando un volumen persistente y un reclamo de volumen persistente.

Así es como puedes usar el módulo y vincularlo en tu despliegue de Helm de Plausible.

module "create_pv_postgresql" {
  source                    = "../persistent-azure-disk-volume"
  snapshot_id               = var.postgresql_restore_snapshot_id
  azure_location            = var.azure_disk_location
  pvc_namespace             = var.namespace
  pv_name                   = "pv-disk-${var.name}-postgresql-0"
  pvc_name                  = "pvc-disk-${var.name}-postgresql-0"
  azure_resource_group_name = var.azure_disk_resource_group_name
  disk_size_gb              = var.plausible_config_disk_size # Mantener esto igual al tamaño definido en el chart de helm de plausible

  depends_on = [kubernetes_namespace.plausible_analytics]
}
# el existingClaim se establece en el pvc_name tanto para postgresql como para clickhouse
postgresql:
  primary:
    persistence:
      enabled: true
      existingClaim: pvc-disk-${var.name}-postgresql-0
      size: ${var.plausible_config_disk_size}Gi # Esta base de datos solo se usa para configuraciones y datos de usuario, por lo que no necesita ser muy grande

...

clickhouse:
  persistence:
    enabled: true
    existingClaim: pvc-disk-${var.name}-clickhouse-0
    size: ${var.plausible_data_disk_size}Gi # Esta base de datos se usa para almacenar todos los datos de análisis, por lo que necesita ser más grande

Obstáculo: El lanzamiento de Helm de Plausible no está expuesto

Al desplegar Plausible a través de Helm, no expone el servicio por defecto. Para hacerlo accesible desde Internet, necesitamos configurar un recurso de ingress. Al configurar el ingress, también podemos especificar la anotación cert-manager para asegurar que se cree el certificado.

modules/plausible/plausible.tf
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-production"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
  className: nginx
  hosts:
    - ${var.plausible_dns}
  path: /
  pathType: Prefix
  tls:
    - secretName: letsencrypt-production
      hosts:
        - ${var.plausible_dns}

Obstáculo: No se puede restaurar plausible desde un respaldo cuando el entorno ya existe

Al cambiar los ID de instantánea en el archivo terraform.tfvars, Terraform no recrea el lanzamiento de helm de plausible ni los reclamos de volumen persistente, porque no ve ningún cambio en ellos. Esto evita la eliminación y recreación de los discos de Azure y volúmenes persistentes, porque nunca se desvinculan. Por lo tanto, agregué un null_resource que activa un reemplazo del lanzamiento de plausible y los reclamos de volumen persistente cuando cambian los ID de instantánea. De esta manera puedes especificar nuevos ID de instantánea y hacer que los recursos se recreen sin intervención manual.

resource "null_resource" "snapshot_trigger" {
  triggers = {
    postgresql_snapshot = var.postgresql_restore_snapshot_id
    clickhouse_snapshot = var.clickhouse_restore_snapshot_id
  }
}
...

  lifecycle {
    replace_triggered_by = [
      null_resource.snapshot_trigger
    ]
  }
resource "null_resource" "snapshot_trigger" {
  triggers = {
    snapshot = var.snapshot_id
  }
}
...

  lifecycle {
    replace_triggered_by = [
      null_resource.snapshot_trigger
    ]
  }

Obstáculo: costos innecesarios en nuestro clúster AKS

En mi artículo anterior Eliminar los banners de cookies: Ejecuta Plausible Analytics en Azure Kubernetes, aprendiste algunos trucos para reducir los costos de tu clúster AKS. Estos también están incorporados en esta solución.

  • Usar discos efímeros: Estos se almacenan directamente en el almacenamiento local de la VM y vienen sin costo adicional.
  • Configuración Standard_B2s: La configuración de VM más rentable disponible
  • Aumentar el número de pods por nodo: Para permitir más cargas de trabajo en la instancia Standard_B2s

Reflexiones finales

Hemos transformado exitosamente los scripts bash de nuestro artículo anterior en un despliegue de Kubernetes de grado de producción en Azure. Al aprovechar el enfoque declarativo de Terraform y la infraestructura gestionada de AKS, ahora tienes una instancia de Plausible Analytics que no solo está funcionando, sino que es escalable, mantenible y lista para tráfico del mundo real.

La belleza de este enfoque de Infraestructura como Código está en su repetibilidad. ¿Necesitas un entorno de staging? Solo duplica la carpeta aks-westeu-prod con diferentes variables. ¿Quieres desplegar en otra región? Cambia un solo parámetro. Cada decisión de infraestructura está documentada en código, revisada a través de pull requests, y puede revertirse si es necesario.

Si bien esta configuración puede parecer exagerada para una simple herramienta de análisis, los patrones que has aprendido aquí (Terraform modularizado, integración cert-manager, gestión adecuada de secretos) te servirán bien para cualquier carga de trabajo de Kubernetes en producción.

Acerca de Jeroen Bach

Soy Ingeniero de Software y Líder de Equipo con más de 15 años de experiencia profesional. Me apasiona resolver problemas complejos a través de soluciones simples y elegantes. Este blog es donde comparto técnicas y perspectivas para construir gran software, inspirado en proyectos del mundo real.

Jeroen Bach

Diseñado en Figma y construido con Vue.js, Nuxt.js y Tailwind CSS. Desplegado vía Azure Static Web App y Azure Functions. Los análisis del sitio web están impulsados por Plausible Analytics, desplegado usando Azure Kubernetes Service.