| FOTO | AUTO | EDV | AUDIO |

Dies ist eine alte Version des Dokuments!


Lebenszyklus: Geburt, Betrieb und Ende einer Herde

Ein Haustier wird einmal angeschafft und dann über Jahre gepflegt. Nutzvieh hat einen geplanten, kurzen und vollständig automatisierten Lebenszyklus. Diese Seite beschreibt jede Phase und zeigt, wie sie sich auf Azure mit Terraform umsetzen lässt – aufbauend auf dem IaC-Lab und der IaC-Pipeline.

Zurück zur IaC-Übersicht

Überblick

  ┌──────────┐   ┌───────────┐   ┌───────────┐   ┌──────────┐
  │  Build   │──►│ Provision │──►│ Bootstrap │──►│ Betrieb  │
  │ (Packer) │   │(Terraform)│   │(cloud-init)│  │ Health,  │
  └──────────┘   └───────────┘   └───────────┘   │ Repair,  │
       ▲                                         │ Scaling  │
       │           ┌──────────┐                  └────┬─────┘
       │           │ Rückbau  │◄──── Scale-in ────────┤
       │           │          │◄──── Update/Rollout ──┘
       │           └──────────┘
       └──── neue Image-Version (Patch, Feature)
Phase Auslöser Werkzeug Menschlicher Eingriff
Build Codeänderung oder Patch-Zyklus Packer + Ansible Review im Merge Request
Provision Merge + Apply Terraform Apply-Freigabe
Bootstrap Start einer Instanz cloud-init keiner
Betrieb laufend Health Probe, Automatic Repair keiner
Skalierung Last Autoscale keiner
Update neue Image-Version Rolling Upgrade oder Blue/Green Apply-Freigabe
Rückbau Scale-in, Update, Service-Ende Scale Set, Terraform nur bei Service-Ende

Voraussetzung: zustandslose Instanzen

Nutzvieh funktioniert nur, wenn eine Instanz jederzeit ohne Verlust gelöscht werden kann. Alles, was überdauern muss, lebt außerhalb der Instanz.

Zustand Pet-Welt Cattle-Welt
Anwendungsdaten Lokale Platte Managed Database, Storage Account
Sitzungen Im Webserver-Speicher Redis-Cache oder zustandslose Tokens
Logs /var/log Zentral gesammelt (Azure Monitor Agent, Splunk-Forwarder im Image)
Konfiguration Handgepflegt auf dem Host Im Image, instanzspezifisch per cloud-init
Secrets Dateien auf dem Host Key Vault, Abruf per Managed Identity zur Laufzeit
Uploads/Dateien Lokales Verzeichnis Blob Storage oder Azure Files

Die zustandsbehafteten Ressourcen sind die eigentlichen „Haustiere“ der Architektur – und werden entsprechend geschützt:

resource "azurerm_postgresql_flexible_server" "db" {
  # ...
  lifecycle {
    prevent_destroy = true   # Terraform verweigert jedes Löschen oder Ersetzen
  }
}

In der Change-Klassifizierung ist jedes Ersetzen ohnehin ein Normal Change. prevent_destroy ist die zweite, technische Sicherung.

Build: Image-Versionen planen

Versionsschema

Die Compute Gallery verlangt Versionen im Format Major.Minor.Patch mit ganzzahligen Teilen. Ein praktisches Schema:

Teil Bedeutung Beispiel
Major Grundlegender Wechsel, z. B. neues Betriebssystem 2 = Ubuntu 26.04
Minor Funktionale Änderung im Playbook 2.3
Patch Build-Datum für reine Patch-Builds 2.3.20261001

So ist an jeder Instanz ablesbar, welchen Funktionsstand und welchen Patchstand sie hat.

Patch-Zyklus: monatliches Image

Sicherheitsupdates werden nicht auf laufenden Instanzen eingespielt, sondern in ein neues Image gebaut. Ein monatlicher Zeitplan in GitLab baut das Image und öffnet automatisch einen Merge Request für den Rollout. Der Rollout selbst bleibt ein geprüfter Standard Change.

.gitlab-ci.yml
image:monthly:
  stage: image
  extends: .id_apply
  script:
    - VERSION="$(cut -d. -f1-2 packer/VERSION).$(date +%Y%m%d)"
    - az login --service-principal -u "$ARM_CLIENT_ID" -t "$ARM_TENANT_ID"
        --federated-token "$AZURE_ID_TOKEN" -o none
    - az account set -s "$ARM_SUBSCRIPTION_ID"
    - (cd packer && packer init . && packer build -color=false -var image_version="$VERSION" .)
    - git switch -c "patch-image-$VERSION"
    - sed -i -E "s/(image_version *= *\")[^\"]+/\1$VERSION/" workload/terraform.tfvars
    - git -c user.name="IaC-Pipeline" -c user.email="ci@localhost"
        commit -am "Rollout Patch-Image $VERSION"
    - git push "https://oauth2:${CI_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD
        -o merge_request.create
        -o merge_request.target="$CI_DEFAULT_BRANCH"
        -o merge_request.title="Patch-Image $VERSION"
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE == "monthly-image"

Hinweise:

  • CI_PUSH_TOKEN ist ein Projekt-Zugriffstoken mit Schreibrecht auf das Repository, als maskierte und geschützte Variable hinterlegt. Es ist ein GitLab-Geheimnis, kein Azure-Zugang.
  • Die Zeitpläne bekommen eine Variable SCHEDULE (monthly-image bzw. drift); die Regel des Drift-Jobs wird entsprechend auf $SCHEDULE == „drift“ erweitert.
  • Das Snippet setzt die einfache Rolling-Variante aus dem Lab voraus. Bei Blue/Green (unten) erzeugt der Job stattdessen eine neue Farbe.

Alternative: Automatische OS-Image-Upgrades

Scale Sets können neue Image-Versionen aus der Compute Gallery auch selbstständig ausrollen. Das ist bequem, umgeht aber Merge Request, Plan und Freigabe. Für nachvollziehbare Changes ist die explizite Version im Code die bessere Wahl.

Betrieb: Selbstheilung und würdiges Ende

Automatic Repair

Im Lab bereits aktiv: Schlägt die Health Probe einer Instanz über die Grace Period hinaus fehl, ersetzt das Scale Set sie. Siehe IaC-Lab, Schritt 5.

Scale-in-Reihenfolge und geordnetes Herunterfahren

Beim Verkleinern soll das Scale Set die ältesten Instanzen zuerst entfernen – sie sind am längsten gelaufen und am weitesten vom frischen Zustand entfernt. Außerdem bekommt jede Instanz vor dem Löschen eine Vorwarnung, um laufende Anfragen sauber abzuschließen.

Ergänzung im Scale Set des Labs:

resource "azurerm_linux_virtual_machine_scale_set" "web" {
  # ... wie im Lab
 
  scale_in {
    rule = "OldestVM"
  }
 
  termination_notification {
    enabled = true
    timeout = "PT5M"
  }
}

Die Instanz erfährt vom bevorstehenden Ende über den Scheduled Events-Endpunkt des Azure Instance Metadata Service. Ein kleiner Dienst im Image fragt ihn regelmäßig ab, lässt die Health Probe bei einem Terminate-Ereignis fehlschlagen (der Load Balancer schickt dann keine neuen Anfragen mehr), wartet laufende Verbindungen ab und bestätigt das Ereignis. Dieser Dienst gehört ins Ansible-Playbook.

Autoscaling

Das Scale Set passt die Anzahl der Instanzen selbst an die Last an:

resource "azurerm_monitor_autoscale_setting" "web" {
  name                = "as-${var.prefix}-web"
  resource_group_name = azurerm_resource_group.web.name
  location            = azurerm_resource_group.web.location
  target_resource_id  = azurerm_linux_virtual_machine_scale_set.web.id
 
  profile {
    name = "cpu"
 
    capacity {
      default = 3
      minimum = 3
      maximum = 9
    }
 
    rule {
      metric_trigger {
        metric_name        = "Percentage CPU"
        metric_resource_id = azurerm_linux_virtual_machine_scale_set.web.id
        time_grain         = "PT1M"
        statistic          = "Average"
        time_window        = "PT5M"
        time_aggregation   = "Average"
        operator           = "GreaterThan"
        threshold          = 70
      }
      scale_action {
        direction = "Increase"
        type      = "ChangeCount"
        value     = "1"
        cooldown  = "PT5M"
      }
    }
 
    rule {
      metric_trigger {
        metric_name        = "Percentage CPU"
        metric_resource_id = azurerm_linux_virtual_machine_scale_set.web.id
        time_grain         = "PT1M"
        statistic          = "Average"
        time_window        = "PT10M"
        time_aggregation   = "Average"
        operator           = "LessThan"
        threshold          = 30
      }
      scale_action {
        direction = "Decrease"
        type      = "ChangeCount"
        value     = "1"
        cooldown  = "PT10M"
      }
    }
  }
}

Wichtig: Terraform und Autoscale dürfen nicht um die Instanzanzahl streiten. Sonst meldet der Drift-Check jede Skalierung als Abweichung, und jeder Apply setzt die Anzahl zurück. Deshalb im Scale Set:

  lifecycle {
    ignore_changes = [instances]
  }

Ab jetzt ist die Instanzanzahl Sache des Autoscalers. Terraform verwaltet nur noch die Grenzen (minimum/maximum).

Update-Strategien

Strategie Ablauf Ausfall Mehrkosten Rollback Umsetzung auf Azure
Recreate Alles löschen, neu bauen ja keine erneuter Neubau upgrade_mode = „Manual“ + Reimage
Rolling Instanzen batchweise austauschen nein gering Rolling zurück, Minuten upgrade_mode = „Rolling“ (Lab)
Blue/Green Zweite Herde aufbauen, Verkehr umschalten nein doppelt, kurz Umschalten, Sekunden Zwei Scale Sets, Load-Balancer-Regel schalten
Canary Kleinen Teil des Verkehrs auf neue Version leiten nein gering Anteil auf null setzen Gewichtete Verteilung per Application Gateway oder Front Door

Rolling ist der pragmatische Standard. Blue/Green lohnt sich, wenn ein Rollback in Sekunden nötig ist oder die neue Version vor dem Umschalten unter echten Bedingungen getestet werden soll. Der Azure Load Balancer kann Verkehr nicht gewichtet verteilen; für Canary braucht es eine Schicht-7-Komponente.

Blue/Green mit Terraform

Statt eines einzelnen Scale Sets beschreibt eine Map die aktiven „Farben“. Eine Variable legt fest, welche Farbe Produktivverkehr bekommt. Die jeweils andere Farbe ist über Port 8080 zum Testen erreichbar.

variable "deployments" {
  description = "Aktive Herden: Farbe => Image-Version und Größe"
  type = map(object({
    image_version = string
    instances     = number
  }))
}
 
variable "live" {
  description = "Farbe, die den Produktivverkehr erhält"
  type        = string
}
 
data "azurerm_shared_image_version" "web" {
  for_each            = var.deployments
  name                = each.value.image_version
  image_name          = "img-web-ubuntu2404"
  gallery_name        = "gal_${var.prefix}"
  resource_group_name = "rg-${var.prefix}-base"
}
 
resource "azurerm_lb_backend_address_pool" "web" {
  for_each        = var.deployments
  name            = "be-${each.key}"
  loadbalancer_id = azurerm_lb.web.id
}
 
# Produktivverkehr: Port 80 -> Farbe "live"
resource "azurerm_lb_rule" "live" {
  name                           = "http-live"
  loadbalancer_id                = azurerm_lb.web.id
  protocol                       = "Tcp"
  frontend_port                  = 80
  backend_port                   = 80
  frontend_ip_configuration_name = "fe"
  backend_address_pool_ids       = [azurerm_lb_backend_address_pool.web[var.live].id]
  probe_id                       = azurerm_lb_probe.http.id
  disable_outbound_snat          = true
 
  lifecycle {
    precondition {
      condition     = contains(keys(var.deployments), var.live)
      error_message = "Die Farbe in 'live' muss in 'deployments' enthalten sein."
    }
  }
}
 
# Testzugang: Port 8080 -> die jeweils andere Farbe
resource "azurerm_lb_rule" "preview" {
  for_each                       = { for k, v in var.deployments : k => v if k != var.live }
  name                           = "http-preview-${each.key}"
  loadbalancer_id                = azurerm_lb.web.id
  protocol                       = "Tcp"
  frontend_port                  = 8080
  backend_port                   = 80
  frontend_ip_configuration_name = "fe"
  backend_address_pool_ids       = [azurerm_lb_backend_address_pool.web[each.key].id]
  probe_id                       = azurerm_lb_probe.http.id
  disable_outbound_snat          = true
}
 
resource "azurerm_lb_outbound_rule" "egress" {
  for_each                = var.deployments
  name                    = "egress-${each.key}"
  loadbalancer_id         = azurerm_lb.web.id
  protocol                = "All"
  backend_address_pool_id = azurerm_lb_backend_address_pool.web[each.key].id
 
  frontend_ip_configuration {
    name = "fe"
  }
}
 
resource "azurerm_linux_virtual_machine_scale_set" "web" {
  for_each        = var.deployments
  name            = "vmss-${var.prefix}-web-${each.key}"
  instances       = each.value.instances
  source_image_id = data.azurerm_shared_image_version.web[each.key].id
  # ... restliche Einstellungen wie im Lab, außerdem:
 
  network_interface {
    name    = "nic"
    primary = true
    ip_configuration {
      name                                   = "ipcfg"
      primary                                = true
      subnet_id                              = data.azurerm_subnet.app.id
      load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.web[each.key].id]
    }
  }
}

Die Einzelressourcen azurerm_lb_rule.http, azurerm_lb_outbound_rule.egress und azurerm_lb_backend_address_pool.web aus dem Lab werden durch diese Varianten ersetzt. Beim Umbau eines bestehenden Labs verschiebt terraform state mv das vorhandene Scale Set auf azurerm_linux_virtual_machine_scale_set.web[„blue“], statt es neu zu bauen. Port 8080 muss in der NSG freigegeben werden – sinnvollerweise nur für die eigene IP-Adresse.

Ablauf eines Blue/Green-Rollouts – jeder Schritt ist ein Merge Request:

terraform.tfvars
# 1. Ausgangslage: nur Blau
deployments = {
  blue = { image_version = "1.1.0", instances = 3 }
}
live = "blue"
 
# 2. Grün dazu – erreichbar unter http://<ip>:8080, Produktion unverändert
deployments = {
  blue  = { image_version = "1.1.0", instances = 3 }
  green = { image_version = "1.2.0", instances = 3 }
}
live = "blue"
 
# 3. Umschalten – Produktivverkehr geht auf Grün, Blau wird Testzugang
live = "green"
 
# 4. Beobachten. Rollback wäre: live = "blue"
 
# 5. Blau entfernen – Rückbau der alten Herde
deployments = {
  green = { image_version = "1.2.0", instances = 3 }
}
live = "green"

Der nächste Rollout läuft dann umgekehrt: Blau kommt mit der neuen Version zurück.

Rückbau

Instanzen

Das Ende einzelner Instanzen ist Routine und passiert ohne menschlichen Eingriff: beim Scale-in, beim Rolling Upgrade, beim Automatic Repair. Da keine Daten auf der Instanz liegen, gibt es nichts zu sichern.

Image-Versionen

Alte Image-Versionen kosten Speicher und sind ein Sicherheitsrisiko, wenn sie versehentlich wieder verwendet werden. Aufbewahrt werden die aktuell verwendeten Versionen und eine kleine Anzahl der jüngsten als Rollback-Reserve:

cleanup-images.sh
#!/usr/bin/env bash
set -euo pipefail
 
RG="rg-iaclab-base"; GAL="gal_iaclab"; IMG="img-web-ubuntu2404"
KEEP=3   # Rollback-Reserve: die jüngsten N Versionen bleiben immer erhalten
 
# Alle im Code referenzierten Versionen (auch bei Blue/Green)
IN_USE=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' workload/terraform.tfvars | sort -u)
 
az sig image-version list -g "$RG" -r "$GAL" -i "$IMG" --query "[].name" -o tsv \
  | sort -V | head -n -"$KEEP" \
  | while read -r v; do
      if grep -qx "$v" <<<"$IN_USE"; then
        echo "Behalte $v (in Verwendung)"
        continue
      fi
      echo "Lösche $v"
      az sig image-version delete -g "$RG" -r "$GAL" -i "$IMG" -e "$v"
    done

Das Skript eignet sich als monatlicher Pipeline-Job direkt nach dem Patch-Image-Build. Zum ersten Ausprobieren die delete-Zeile durch ein echo ersetzen.

Services

Das Ende eines ganzen Services ist ein bewusster Normal Change. Checkliste:

  1. Abhängige Services und Nutzer identifizieren (CMDB, Tags service/owner)
  2. Zustandsbehaftete Daten sichern oder gemäß Aufbewahrungsfristen archivieren
  3. prevent_destroy an Datenressourcen bewusst per Merge Request entfernen
  4. terraform destroy über die Pipeline, nicht vom Laptop – der Rückbau ist genauso nachvollziehbar wie der Aufbau
  5. DNS-Einträge, Zertifikate und externe Firewall-Freigaben entfernen
  6. Workload-Identitäten und Federated Credentials löschen (verwaiste Vertrauensstellungen sind ein Sicherheitsrisiko)
  7. Image-Definition und -Versionen in der Gallery entfernen
  8. Terraform-State archivieren und aus dem aktiven Backend entfernen
  9. CMDB-Eintrag auf „außer Betrieb“ setzen, Repository archivieren

Weiterführend

  • IaC-Lab – Rolling Upgrade und Automatic Repair in der Praxis
  • IaC-Pipeline – wie Änderungen ausgerollt werden
  • IaC und ITIL v4 – Change-Typen und Rückbau als Change
  • Packer – Image-Builds im Detail