Einen produktionsreifen Kubernetes-Cluster auf Azure mit Terraform bereitstellen

Jeroen Bach

Jeroen Bach · Linkedin

10 min read ·

In diesem Leitfaden erfahren Sie, wie Sie eine vollständig modulare und wiederverwendbare Terraform-Lösung erstellen, die Ressourcen über Azure, Kubernetes und Cloudflare hinweg bereitstellt.

In meinem vorherigen Artikel haben Sie gelernt, wie Sie einen Kubernetes-Cluster einrichten und Plausible Analytics mit einer Reihe von CLI-Befehlen ausführen. Dieser Ansatz funktioniert zwar, ist aber nicht ideal. Eine bessere, nachhaltigere Lösung ist die Verwendung von Terraform. Mit Terraform beschreiben Sie Ihre Infrastruktur in ihrem gewünschten Zustand, und Terraform findet heraus, welche Schritte erforderlich sind, um dorthin zu gelangen.

Ich habe früher auch helmfile verwendet, das sich hervorragend für die Verwaltung von Helm-Releases eignet. Was mir an Terraform gefällt, ist jedoch, dass es noch weiter geht. Es hört nicht bei Kubernetes auf, sondern bietet Ihnen eine einheitliche Lösung zur Definition aller Ressourcen in Ihrem gesamten Stack.

Hauptvorteile

  • Gewünschten Zustand definieren - Keine manuellen CLI-Skripte oder Konfigurationsabweichungen mehr (Ihre Umgebung repräsentiert immer Ihren Code)
  • Wiederherstellbar - Da Sie Ihren gewünschten Zustand definiert haben, ist es einfach, eine gesamte Umgebung mit allen Einstellungen wiederherzustellen
  • Modulares Design - Erstellen Sie Module, die in verschiedenen Umgebungen wiederverwendet werden können
  • Umgebungsunabhängig - Bereitstellung auf Azure, Kubernetes, Cloudflare und vielen weiteren Plattformen
  • Versionskontrolle - Verfolgen Sie Infrastrukturänderungen in der Versionskontrolle und pflegen Sie einen vollständigen Prüfpfad

Die Einrichtung Ihrer Lösung wie in diesem Artikel beschrieben erweitert diese Vorteile um:

  • Automatisches HTTPS - Let's Encrypt-Zertifikate für alle Ihre exponierten Anwendungen
  • Kostenoptimiert - Nur eine einzige öffentliche IP, eine kosteneffiziente VM-Konfiguration und keine zusätzlichen VM-Festplatten
  • Keine Konfigurationsabweichungen - Infrastrukturzustand zentral gespeichert
  • Datensicherheit - Azure-Festplatten speichern Ihre Daten und ermöglichen einfaches Backup und Wiederherstellung über Snapshots oder Backup-Vault

Voraussetzungen

Stellen Sie sicher, dass die folgenden Tools auf Ihrem Rechner installiert sind: Azure CLI, Terraform & Kubernetes-Tools. Alternativ können Sie die Cloud Shell im Azure-Portal verwenden, wo alle benötigten Tools vorinstalliert sind. Bei Verwendung der Cloud Shell sollten Sie das clouddrive mounten, um alles über Shell-Sitzungen hinweg zu persistieren: clouddrive mount & cd clouddrive.

Wenn Sie Windows verwenden, nutzen Sie bitte die Cloud Shell oder WSL, da alle Skripte in Bash geschrieben sind.

Erste Schritte

Es gibt einen kleinen manuellen Schritt, bevor wir in Terraform eintauchen können. Unsere Lösung benötigt ein Backend (Speicher) für die State-Datei, daher müssen wir zunächst ein Storage Account mit einem Container in Azure erstellen. Es gibt ein praktisches Skript in unserem Terraform-Projekt, das dies für Sie erledigt. Lassen Sie uns also zuerst unser Terraform-Projekt holen.

Sie können dies schnell durchführen, indem Sie das folgende Shell-Skript ausführen:

download-terraform-project.sh
#!/usr/bin/env bash
# Terraform-Projekt aus dem Repository herunterladen und extrahieren
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/*"

# Extrahierten Ordner ins aktuelle Verzeichnis verschieben und Zip sowie extrahierten Ordner entfernen
mv "bach.software-main/src/app/examples/post4/terraform" "./terraform"
rm -rf "bach.software-terraform.zip" "bach.software-main"

# Zum Terraform-Verzeichnis navigieren
cd "./terraform"

Bevor Sie Terraform oder eines der nächsten Skripte ausführen, stellen Sie immer sicher, dass Sie bei der richtigen Azure-Subscription angemeldet sind, indem Sie az login verwenden.

Lassen Sie uns das Storage Account und den Container für unseren Terraform-State erstellen:

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

Terraform ausführen

Bei der Ausführung von terraform apply fordert Terraform einige Eingabevariablen an. Die benötigten Werte finden Sie in der Datei input-output.tf im selben Ordner. Sie können auch eine Datei terraform.tfvars mit den benötigten Werten erstellen, sodass Sie diese nicht jedes Mal eingeben müssen.

Hinweis: Stellen Sie sicher, dass Sie Ihre .tfvars-Dateien nicht in die Versionskontrolle einchecken

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]"

Bevor Sie beginnen, stellen Sie sicher, dass Sie die erforderlichen Informationen zur Verfügung haben. Sie können ein kostenloses Cloudflare-Konto erstellen und es mit einer DNS verknüpfen, die Sie besitzen, oder eine neue DNS erstellen. Sie können ein API-Token erstellen, indem Sie diesen Anweisungen folgen und die Vorlage "Edit zone DNS" verwenden. Ihre Zone-ID finden Sie, indem Sie diesen Anweisungen folgen. Ihre Azure-Subscription-ID finden Sie, indem Sie diesen Anweisungen folgen.

Wenn Sie die Cloudflare-Variablen nicht angeben, wird das DNS nicht aktualisiert, aber alles andere funktioniert weiterhin und Sie erhalten am Ende die IP-Adresse angezeigt (um auf Plausible zuzugreifen). Sie müssen selbst einen DNS-Eintrag mit dieser IP-Adresse erstellen, da der Zertifikatsaussteller diesen benötigt, um den DNS-Eintrag zu validieren, bevor er ein gültiges Zertifikat ausstellen kann.

Lassen Sie uns nun Ihre Umgebung bereitstellen:

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

Terraform wird:

  • Den AKS-Cluster bereitstellen
  • Plausible über Helm installieren
  • Cloudflare DNS aktualisieren

Backup & Wiederherstellung (Optional)

In Ihrer Ressourcengruppe rg-nodes-aks-westeu-prod finden Sie die beiden Azure-Festplatten, die alle Daten der Plausible-Lösung enthalten: pv-disk-plausible-analytics-v3-clickhouse-0 & pv-disk-plausible-analytics-v3-postgresql-0. Sie können stündliche, tägliche oder wöchentliche Backups dieser Festplatten mit Azure Backup Vault erstellen.

Um ein Backup wiederherzustellen, erstellen Sie einen Snapshot des spezifischen Backups in einer Ressourcengruppe und tragen Sie die Snapshot-IDs in die folgenden Variablen in der Datei aks-westeu-prod/input-output.tf ein: postgresql_restore_snapshot_id und clickhouse_restore_snapshot_id.

Beim nächsten Ausführen von terraform apply wird Plausible mit den Backups wiederhergestellt.

Die Umgebung zerstören

Um die Umgebung und alle zugehörigen Ressourcen zu zerstören, können Sie den folgenden Befehl ausführen:

terraform destroy

Lösungsstruktur

Um die Lösung von Anfang bis Ende zum Laufen zu bringen, gab es einige Hürden zu überwinden. In diesem Kapitel werde ich diese Hürden und deren Lösung untersuchen, aber lassen Sie mich zunächst einen allgemeinen Überblick über die Lösung geben.

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: Eine Produktionsumgebungskonfiguration für die Bereitstellung in Azure West Europe. Sie können diesen Ordner als Vorlage verwenden, um weitere Umgebungen zu erstellen. Die mit app- präfixierten Dateien zeigen die verschiedenen im Cluster installierten Anwendungen.
  • helm-charts: Benutzerdefinierte Helm-Charts
    • letsencrypt-cert-issuer: Anstatt die ClusterIssuer-Ressourcen separat bereitzustellen, habe ich sie in einem Helm-Chart verpackt
  • modules: Jedes Modul kapselt eine spezifische Verantwortlichkeit
    • aks-cluster: Stellt einen AKS-Cluster mit Let's Encrypt-Zertifikatsaussteller, nginx ingress als Load Balancer bereit und wartet darauf, dass die öffentliche IP verfügbar ist
    • persistent-azure-disk-volume: Erstellt eine Azure-Festplatte oder stellt eine mit einem Snapshot wieder her und erstellt dann ein persistent volume und persistent volume claim in Kubernetes
    • plausible: Installiert Plausible und seine Abhängigkeiten über Helm

Hürde: Verbindungsdetails des neuen Clusters noch nicht verfügbar

Nach dem Erstellen des Kubernetes-Clusters möchten wir Ressourcen darin bereitstellen können. Aber in der Terraform-Planungsphase sind die Informationen zur Verbindung mit dieser neuen Umgebung noch nicht verfügbar. Daher mussten wir zwei Schritte unternehmen, um eine nahtlose Bereitstellung zu ermöglichen.

  • Dynamische Provider-Konfiguration: Die Informationen des AKS-Clusters werden für die Helm- und Kubernetes-Provider dynamisch festgelegt, indem die Verbindungsinformationen vom neu erstellten Cluster abgerufen werden:
aks-westeu-prod/provider.tf
provider "helm" {
  kubernetes = {
    # Dynamische Provider-Konfiguration verwenden, um den neu erstellten Cluster direkt zu nutzen
    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" {
  # Dynamische Provider-Konfiguration verwenden, um den neu erstellten Cluster direkt zu nutzen
  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)
}
  • Lokalen kubectl-Kontext setzen: Nach dem Erstellen des AKS-Clusters schreiben wir die neue kube config und setzen den kubectl-Kontext auf dem lokalen Rechner, sodass local-exec-Befehle sich sofort mit dem neuen Cluster verbinden können.
modules/aks-cluster/aks-cluster.tf
resource "null_resource" "set_kube_context" {
  provisioner "local-exec" {
    command = <<EOT
      # Wir holen es aus dem Terraform-State und fügen es zur kubeconfig hinzu
      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
  }

  // Kube-Kontext immer beim Ausführen von apply setzen, auch wenn keine Änderungen am Cluster vorgenommen wurden
  triggers = {
    always_run = "${timestamp()}"
  }

  depends_on = [azurerm_kubernetes_cluster.aks_cluster]
}

Hürde: Load Balancer IP noch nicht verfügbar

Beim Bereitstellen eines Helm-Release ist Terraform fertig, bevor das Release vollständig bereitgestellt ist. Es liefert auch keine Load Balancer IP-Informationen. Daher habe ich zwei lokale Skripte implementiert, die auf die nginx ingress-Bereitstellung warten und die Load Balancer IP sammeln, die für die Aktualisierung Ihres DNS benötigt wird.

modules/aks-cluster/ingress-and-certificates.tf
# Warten, bis das ingress-nginx Helm-Release bereitgestellt ist
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]
}

# Externe IP mit kubectl abrufen
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]
}

Hürde: Die Daten in unserer Lösung sind nicht sicher

Bei Verwendung des Plausible Helm-Charts werden zwei Datenbanken erstellt: PostgreSQL und ClickHouse. Standardmäßig verwenden diese Datenbanken flüchtigen Speicher, was bedeutet, dass alle Daten verloren gehen, wenn der Pod gelöscht oder neu geplant wird. Um sicherzustellen, dass unsere Daten sicher sind, müssen wir persistenten Speicher verwenden. In einer Cloud-Umgebung wie Azure können wir dafür Azure-Festplatten verwenden.

Ich habe ein Modul erstellt, um eine Azure-Festplatte zu erstellen oder wiederherzustellen und sie in Kubernetes zu integrieren, indem ein persistent volume und ein persistent volume claim erstellt werden.

So können Sie das Modul verwenden und es in Ihre Plausible Helm-Bereitstellung einbinden.

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 # Diese Größe muss gleich der im Plausible Helm-Chart definierten Größe sein

  depends_on = [kubernetes_namespace.plausible_analytics]
}
# der existingClaim wird auf den pvc_name sowohl für postgresql als auch für clickhouse gesetzt
postgresql:
  primary:
    persistence:
      enabled: true
      existingClaim: pvc-disk-${var.name}-postgresql-0
      size: ${var.plausible_config_disk_size}Gi # Diese Datenbank wird nur für Einstellungen und Benutzerdaten verwendet, muss also nicht sehr groß sein

...

clickhouse:
  persistence:
    enabled: true
    existingClaim: pvc-disk-${var.name}-clickhouse-0
    size: ${var.plausible_data_disk_size}Gi # Diese Datenbank wird zum Speichern aller Analysedaten verwendet, muss also größer sein

Hürde: Das Plausible Helm Release ist nicht exponiert

Bei der Bereitstellung von Plausible über Helm wird der Service standardmäßig nicht exponiert. Um ihn aus dem Internet zugänglich zu machen, müssen wir eine Ingress-Ressource konfigurieren. Bei der Konfiguration des Ingress können wir auch die cert-manager-Annotation angeben, um sicherzustellen, dass das Zertifikat erstellt wird.

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}

Hürde: Kann Plausible nicht aus einem Backup wiederherstellen, wenn die Umgebung bereits existiert

Wenn Sie die Snapshot-IDs in der Datei terraform.tfvars ändern, erstellt Terraform das Plausible Helm-Release oder die persistent volume claims nicht neu, da es keine Änderungen darin sieht. Dies verhindert das Löschen und Neuerstellen der Azure-Festplatten und persistent volumes, da sie niemals ungebunden werden. Ich habe daher eine null_resource hinzugefügt, die einen Austausch des Plausible-Release und der persistent volume claims auslöst, wenn sich die Snapshot-IDs ändern. Auf diese Weise können Sie neue Snapshot-IDs angeben und die Ressourcen ohne manuelle Eingriffe neu erstellen lassen.

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
    ]
  }

Hürde: Unnötige Kosten in unserem AKS-Cluster

In meinem vorherigen Artikel Ditching the Cookie Banners: Run Plausible Analytics on Azure Kubernetes haben Sie einige Tricks gelernt, um die Kosten Ihres AKS-Clusters zu reduzieren. Diese sind auch in dieser Lösung integriert.

  • Ephemere Festplatten verwenden: Diese werden direkt auf dem lokalen Speicher der VM gespeichert und verursachen keine zusätzlichen Kosten.
  • Standard_B2s-Konfiguration: Die kostengünstigste verfügbare VM-Konfiguration
  • Anzahl der Pods pro Node erhöhen: Um mehr Workloads auf der Standard_B2s-Instanz zu ermöglichen

Abschließende Gedanken

Wir haben die Bash-Skripte aus unserem vorherigen Artikel erfolgreich in eine produktionsreife Kubernetes-Bereitstellung auf Azure transformiert. Durch die Nutzung von Terraforms deklarativem Ansatz und der verwalteten Infrastruktur von AKS verfügen Sie nun über eine Plausible Analytics-Instanz, die nicht nur läuft – sie ist skalierbar, wartbar und bereit für den Einsatz unter realen Bedingungen.

Das Schöne an diesem Infrastructure as Code-Ansatz liegt in seiner Wiederholbarkeit. Benötigen Sie eine Staging-Umgebung? Duplizieren Sie einfach den Ordner aks-westeu-prod mit unterschiedlichen Variablen. Möchten Sie in einer anderen Region bereitstellen? Ändern Sie einen einzigen Parameter. Jede Infrastrukturentscheidung ist im Code dokumentiert, wird durch Pull Requests überprüft und kann bei Bedarf zurückgesetzt werden.

Obwohl diese Einrichtung für ein einfaches Analyse-Tool übertrieben erscheinen mag, werden Ihnen die hier erlernten Muster (modularisiertes Terraform, cert-manager-Integration, ordnungsgemäße Secret-Verwaltung) bei jeder produktiven Kubernetes-Workload gute Dienste leisten.

Über Jeroen Bach

Ich bin Software-Ingenieur und Team Lead mit über 15 Jahren Berufserfahrung. Ich bin leidenschaftlich daran interessiert, komplexe Probleme durch einfache, elegante Lösungen zu lösen. Dieser Blog ist der Ort, an dem ich Techniken und Erkenntnisse zum Erstellen großartiger Software teile, inspiriert von realen Projekten.

Jeroen Bach

Entworfen in Figma und erstellt mit Vue.js, Nuxt.js und Tailwind CSS. Bereitgestellt über Azure Static Web App und Azure Functions. Website-Analysen werden von Plausible Analytics betrieben, bereitgestellt mit Azure Kubernetes Service.