Déployer un cluster Kubernetes prêt pour la production sur Azure avec Terraform

Jeroen Bach

Jeroen Bach · Linkedin

11 min read ·

Dans ce guide, vous apprendrez à créer une solution Terraform entièrement modulaire et réutilisable, déployant des ressources sur Azure, Kubernetes et Cloudflare.

Dans mon article précédent, vous avez appris à configurer un cluster Kubernetes et à exécuter Plausible Analytics en utilisant une série de commandes CLI. Bien que cette approche fonctionne, elle n'est pas idéale. Une meilleure solution plus durable est d'utiliser Terraform. Avec Terraform, vous décrivez votre infrastructure dans son état désiré, et Terraform détermine les étapes nécessaires pour y parvenir.

J'ai également utilisé helmfile auparavant, qui est excellent pour gérer les versions Helm. Mais ce que j'aime dans Terraform, c'est qu'il va plus loin. Il ne s'arrête pas à Kubernetes, il vous donne une solution unique pour définir toutes les ressources de votre pile.

Avantages clés

  • Définir l'état désiré - Plus de scripts CLI manuels ou de dérive de configuration (votre environnement représente toujours votre code)
  • Récupérable - Comme vous avez défini votre état désiré, il est facile de récupérer un environnement entier avec tous ses paramètres
  • Conception modulaire - Créer des modules réutilisables dans différents environnements
  • Agnostique de l'environnement - Déployer sur Azure, Kubernetes, Cloudflare et bien d'autres plateformes
  • Contrôle de version - Suivre les changements d'infrastructure dans le contrôle de version, en maintenant une piste d'audit complète

Configurer votre solution comme décrit dans cet article étend ces avantages avec:

  • HTTPS automatique - Certificats Let's Encrypt pour toutes vos applications exposées
  • Optimisé en coûts - Une seule IP publique, une configuration VM rentable et pas de disques VM supplémentaires
  • Pas de dérive de configuration - État d'infrastructure stocké de manière centralisée
  • Sécurité des données - Les disques Azure stockent vos données, permettant une sauvegarde et une restauration faciles via des instantanés ou un coffre de sauvegarde

Prérequis

Assurez-vous que les outils suivants sont installés sur votre machine: Azure CLI, Terraform & Outils Kubernetes. Ou alternativement, utilisez Cloud Shell dans Azure Portal, où tous les outils nécessaires sont pré-installés. Lors de l'utilisation de Cloud Shell, montez le clouddrive pour persister tout entre les sessions shell: clouddrive mount & cd clouddrive.

Si vous êtes sous Windows, veuillez utiliser Cloud Shell ou WSL, car tous les scripts sont écrits en bash.

Commencer

Il y a une petite étape manuelle à faire avant de plonger dans Terraform. Notre solution a besoin d'un backend (stockage) pour le fichier d'état, nous devons donc d'abord créer un compte de stockage avec un conteneur dans Azure. Il y a un script pratique dans notre projet Terraform qui fait cela pour vous. Obtenons d'abord notre projet Terraform.

Vous pouvez le faire rapidement en exécutant le script shell suivant:

download-terraform-project.sh
#!/usr/bin/env bash
# Télécharger et extraire le projet terraform du dépôt
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/*"

# Déplacer le dossier extrait vers le répertoire actuel et supprimer le zip et le dossier extrait
mv "bach.software-main/src/app/examples/post4/terraform" "./terraform"
rm -rf "bach.software-terraform.zip" "bach.software-main"

# Naviguer vers le répertoire terraform
cd "./terraform"

Avant d'exécuter Terraform ou l'un des prochains scripts, assurez-vous toujours d'être connecté à l'abonnement Azure correct en utilisant az login.

Créons le compte de stockage et le conteneur pour notre état Terraform:

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

Exécuter Terraform

Lors de l'exécution de terraform apply, Terraform demandera certaines variables d'entrée. Vous pouvez trouver les valeurs nécessaires dans le fichier input-output.tf du même dossier. Vous pouvez également créer un fichier terraform.tfvars avec les valeurs nécessaires, donc vous n'avez pas à les saisir à chaque fois.

Note: Assurez-vous de ne pas enregistrer vos fichiers .tfvars dans le contrôle de version

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

Avant de commencer, assurez-vous d'avoir les informations requises disponibles. Vous pouvez créer un compte Cloudflare gratuit et le lier à un DNS que vous possédez, ou créer un nouveau DNS. Vous pouvez créer un jeton API en suivant ces instructions et utiliser le modèle "Edit zone DNS". Vous pouvez trouver votre ID de zone en suivant ces instructions. Vous pouvez trouver votre ID d'abonnement Azure en suivant ces instructions.

Si vous ne spécifiez pas les variables Cloudflare, le DNS ne sera pas mis à jour, mais tout le reste fonctionnera toujours et vous verrez l'adresse IP (à utiliser pour accéder à Plausible) à la fin. Vous devez créer un enregistrement DNS avec cette adresse IP vous-même, car l'émetteur de certificat en a besoin pour valider l'enregistrement DNS avant de pouvoir émettre un certificat valide.

Maintenant, déployons votre environnement:

# Nom de l'environnement: Azure Kubernetes Service - Europe Occidentale - Production
cd aks-westeu-prod
terraform init
terraform apply

Terraform va:

  • Déployer le cluster AKS
  • Installer Plausible via Helm
  • Mettre à jour le DNS Cloudflare

Sauvegarde & Restauration (Optionnel)

Dans votre groupe de ressources rg-nodes-aks-westeu-prod, vous trouverez les deux disques Azure qui contiennent toutes les données de la solution Plausible: pv-disk-plausible-analytics-v3-clickhouse-0 & pv-disk-plausible-analytics-v3-postgresql-0. Vous pouvez créer des sauvegardes horaires, quotidiennes ou hebdomadaires de ces disques en utilisant Azure Backup Vault.

Pour restaurer une sauvegarde, créez un instantané de la sauvegarde spécifique vers un groupe de ressources et remplissez les ID d'instantané dans les variables suivantes trouvées dans le fichier aks-westeu-prod/input-output.tf: postgresql_restore_snapshot_id et clickhouse_restore_snapshot_id.

La prochaine fois que vous exécuterez terraform apply, Plausible sera restauré avec les sauvegardes.

Détruire l'environnement

Pour détruire l'environnement et toutes les ressources associées, vous pouvez exécuter la commande suivante:

terraform destroy

Structure de la solution

Pour faire fonctionner la solution du début à la fin, il y avait quelques obstacles à surmonter. Dans ce chapitre, j'examinerai ces obstacles et comment je les ai résolus, mais laissez-moi d'abord fournir un aperçu général de la solution.

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: Une configuration d'environnement de production pour le déploiement vers Azure West Europe. Vous pouvez utiliser ce dossier comme modèle pour créer plus d'environnements. Les fichiers préfixés avec app- montrent les différentes applications installées dans le cluster.
  • helm-charts: Graphiques Helm personnalisés
    • letsencrypt-cert-issuer: Au lieu de déployer les ressources ClusterIssuer séparément, je les ai empaquetées dans un graphique Helm
  • modules: Chaque module encapsule une responsabilité spécifique
    • aks-cluster: Déploie un cluster AKS avec un émetteur de certificat Let's Encrypt, nginx ingress comme équilibreur de charge, et attend que l'IP publique soit disponible
    • persistent-azure-disk-volume: Crée un disque Azure ou en restaure un en utilisant un instantané et crée ensuite un volume persistant et une réclamation de volume persistant dans Kubernetes
    • plausible: Installe Plausible et ses dépendances via Helm

Obstacle: Détails de connexion du nouveau cluster pas encore disponibles

Après avoir créé le cluster Kubernetes, nous voulons pouvoir déployer des ressources dessus. Mais à l'étape du plan Terraform, les informations sur la façon de se connecter à ce nouvel environnement ne sont pas encore disponibles. Par conséquent, nous avons dû prendre deux mesures pour créer un déploiement transparent.

  • Configuration dynamique du fournisseur: Les informations du cluster AKS sont définies dynamiquement pour les fournisseurs Helm et Kubernetes en récupérant les informations de connexion du cluster nouvellement créé:
aks-westeu-prod/provider.tf
provider "helm" {
  kubernetes = {
    # Utiliser la configuration dynamique du fournisseur pour utiliser le cluster nouvellement créé directement
    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" {
  # Utiliser la configuration dynamique du fournisseur pour utiliser le cluster nouvellement créé directement
  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)
}
  • Définir le contexte kubectl local: Après la création du cluster AKS, nous écrivons la nouvelle configuration kube et définissons le contexte kubectl sur la machine locale, de cette façon les commandes local-exec peuvent immédiatement se connecter au nouveau cluster.
modules/aks-cluster/aks-cluster.tf
resource "null_resource" "set_kube_context" {
  provisioner "local-exec" {
    command = <<EOT
      # Nous l'obtenons de l'état Terraform et l'ajoutons au 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
  }

  // Toujours définir le contexte kube lors de l'exécution d'apply, même si aucun changement n'a été apporté au cluster
  triggers = {
    always_run = "${timestamp()}"
  }

  depends_on = [azurerm_kubernetes_cluster.aks_cluster]
}

Obstacle: IP de l'équilibreur de charge pas encore disponible

Lors du déploiement d'une version helm, terraform se termine avant que la version ne soit complètement déployée. Il ne fournit pas non plus les informations IP de l'équilibreur de charge. Par conséquent, j'ai implémenté deux scripts locaux qui attendent le déploiement de nginx ingress et collectent l'IP de l'équilibreur de charge, qui est nécessaire pour mettre à jour votre DNS.

modules/aks-cluster/ingress-and-certificates.tf
# Attendre que la version helm ingress-nginx soit déployée
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]
}

# Obtenir l'IP externe en utilisant 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]
}

Obstacle: Les données dans notre solution ne sont pas sûres

Lors de l'utilisation du graphique helm plausible, il crée deux bases de données: PostgreSQL et ClickHouse. Par défaut, ces bases de données utilisent un stockage éphémère, ce qui signifie que lorsque le pod est supprimé ou replanifié, toutes les données sont perdues. Pour s'assurer que nos données sont en sécurité, nous devons utiliser un stockage persistant. Dans un environnement cloud comme Azure, nous pouvons utiliser les disques Azure pour cela.

J'ai créé un module pour créer ou restaurer un disque Azure et le connecter à Kubernetes en créant un volume persistant et une réclamation de volume persistant.

Voici comment vous pouvez utiliser le module et le lier à votre déploiement Helm 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 # Gardez ceci égal à la taille définie dans le graphique helm plausible

  depends_on = [kubernetes_namespace.plausible_analytics]
}
# l'existingClaim est défini sur le pvc_name pour postgresql et clickhouse
postgresql:
  primary:
    persistence:
      enabled: true
      existingClaim: pvc-disk-${var.name}-postgresql-0
      size: ${var.plausible_config_disk_size}Gi # Cette base de données n'est utilisée que pour les paramètres et les données utilisateur, elle n'a donc pas besoin d'être très grande

...

clickhouse:
  persistence:
    enabled: true
    existingClaim: pvc-disk-${var.name}-clickhouse-0
    size: ${var.plausible_data_disk_size}Gi # Cette base de données est utilisée pour stocker toutes les données d'analyse, elle doit donc être plus grande

Obstacle: La version Helm de Plausible n'est pas exposée

Lors du déploiement de Plausible via Helm, il n'expose pas le service par défaut. Pour le rendre accessible depuis Internet, nous devons configurer une ressource ingress. Lors de la configuration de l'ingress, nous pouvons également spécifier l'annotation cert-manager pour garantir la création du certificat.

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}

Obstacle: Impossible de restaurer plausible à partir d'une sauvegarde lorsque l'environnement existe déjà

Lors du changement des ID d'instantané dans le fichier terraform.tfvars, Terraform ne recrée pas la version helm de plausible ou les réclamations de volume persistant, car il ne voit aucun changement en eux. Cela empêche la suppression et la recréation des disques azure et des volumes persistants, car ils ne sont jamais déliés. J'ai donc ajouté une null_resource qui déclenche un remplacement de la version plausible et des réclamations de volume persistant lorsque les ID d'instantané changent. De cette façon, vous pouvez spécifier de nouveaux ID d'instantané et faire recréer les ressources sans intervention manuelle.

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

Obstacle: coûts inutiles dans notre cluster AKS

Dans mon article précédent Éliminer les bannières de cookies: Exécutez Plausible Analytics sur Azure Kubernetes, vous avez appris quelques astuces pour réduire les coûts de votre cluster AKS. Celles-ci sont également intégrées dans cette solution.

  • Utiliser des disques éphémères: Ceux-ci sont stockés directement sur le stockage local de la VM et sont fournis sans coût supplémentaire.
  • Configuration Standard_B2s: La configuration VM la plus rentable disponible
  • Augmenter le nombre de pods par nœud: Pour permettre plus de charges de travail sur l'instance Standard_B2s

Réflexions finales

Nous avons réussi à transformer les scripts bash de notre article précédent en un déploiement Kubernetes de qualité production sur Azure. En exploitant l'approche déclarative de Terraform et l'infrastructure gérée d'AKS, vous avez maintenant une instance Plausible Analytics qui ne se contente pas de fonctionner—elle est évolutive, maintenable et prête pour le trafic du monde réel.

La beauté de cette approche Infrastructure as Code réside dans sa répétabilité. Besoin d'un environnement de staging? Dupliquez simplement le dossier aks-westeu-prod avec des variables différentes. Vous voulez déployer dans une autre région? Changez un seul paramètre. Chaque décision d'infrastructure est documentée dans le code, examinée via des pull requests, et peut être annulée si nécessaire.

Bien que cette configuration puisse sembler excessive pour un simple outil d'analyse, les modèles que vous avez appris ici (Terraform modularisé, intégration cert-manager, gestion appropriée des secrets) vous serviront bien pour toute charge de travail Kubernetes en production.

À propos de Jeroen Bach

Je suis un ingénieur logiciel et chef d'équipe avec plus de 15 ans d'expérience professionnelle. Je suis passionné par la résolution de problèmes complexes à travers des solutions simples et élégantes. Ce blog est l'endroit où je partage des techniques et des perspectives pour créer d'excellents logiciels, inspirés par des projets réels.

Jeroen Bach

Conçu dans Figma et construit avec Vue.js, Nuxt.js et Tailwind CSS. Déployé via Azure Static Web App et Azure Functions. Les analyses du site web sont alimentées par Plausible Analytics, déployé en utilisant Azure Kubernetes Service.