Automatisierung und IT-Service-Management gelten oft als Gegensätze: Hier die schnelle Pipeline, dort das Change Advisory Board mit Wochenrhythmus. Richtig umgesetzt ergänzen sie sich. Die Pipeline liefert genau das, was ITIL verlangt – bewertete, genehmigte, dokumentierte und nachvollziehbare Änderungen –, nur automatisch, vollständig und ohne Medienbruch.
Zurück zur IaC-Übersicht · Grundlagen unter ITIL v4
ITIL v4 formuliert sieben Leitprinzipien. Jedes hat eine direkte Entsprechung in Infrastructure as Code.
| Leitprinzip | Umsetzung mit IaC |
|---|---|
| Focus on value | Server sind Mittel zum Zweck; der Wert ist der Service, nicht die Maschine |
| Start where you are | Bestehende Ressourcen per terraform import übernehmen statt neu bauen |
| Progress iteratively with feedback | Kleine Merge Requests, jeder mit Plan und Prüfung |
| Collaborate and promote visibility | Änderungen sind als Code für alle lesbar, Reviews finden im MR statt |
| Think and work holistically | Netz, Compute, Image und Konfiguration in einer durchgängigen Kette |
| Keep it simple and practical | Ein Weg für alle Änderungen: die Pipeline |
| Optimize and automate | Erst den Ablauf standardisieren, dann automatisieren |
| Praxis | Pet-Welt (manuell) | Cattle-Welt (IaC) |
|---|---|---|
| Change Enablement | Ticket, CAB-Termin, Admin setzt um | Merge Request, automatische Bewertung, Pipeline setzt um |
| Release Management | Wartungsfenster, Installationsanleitung | Versionierte Image-Version in der Compute Gallery |
| Deployment Management | Handarbeit auf jedem Server | Rolling Upgrade des Scale Sets |
| Service Configuration Management | CMDB wird von Hand gepflegt und veraltet | State, Tags und Resource Graph liefern den Ist-Zustand |
| Incident Management | Admin loggt sich ein, repariert | Health Probe erkennt, Automatic Repair ersetzt |
| Problem Management | Ursachen gehen im Einzelfall unter | Häufung automatischer Repairs zeigt Muster |
| Monitoring and Event Management | Überwachung einzelner Hosts | Überwachung des Service und der Soll-Ist-Abweichung (Drift) |
| Information Security Management | Wer hat wann was geändert? Oft unklar | Git-Historie, Pipeline-Protokoll, Anmeldeprotokolle der Identitäten |
ITIL v4 unterscheidet drei Arten von Changes. Die Pipeline bildet alle drei ab – aber auf einem technischen Weg.
| Change-Typ | ITIL-Definition | In der Pipeline |
|---|---|---|
| Standard | Vorab bewertet, geringes Risiko, festgelegtes Verfahren | Plan enthält nur unkritische Änderungen → MR-Freigabe genügt |
| Normal | Einzeln zu bewerten und zu genehmigen | Plan enthält Löschungen, Ersetzungen oder kritische Ressourcen → zusätzliche Change-Genehmigung |
| Emergency | Muss schnellstmöglich umgesetzt werden | Beschleunigter MR mit nachgelagerter Prüfung (siehe unten) |
Der entscheidende Schritt: Nicht der Mensch klassifiziert den Change, sondern der Plan. terraform plan weiß exakt, welche Ressourcen angelegt, geändert, ersetzt oder gelöscht werden. Daraus lässt sich der Change-Typ regelbasiert ableiten.
Ein Change ist ein Standard Change, wenn alle folgenden Bedingungen erfüllt sind:
Typische Standard Changes im Lab sind das Skalieren des Scale Sets und das Ausrollen einer neuen Image-Version – beides In-Place-Updates, die Azure batchweise und mit Health Probe absichert.
Diese Regeln einmal formal zu genehmigen, ist der eigentliche Change-Management-Akt. Danach wendet die Pipeline sie bei jedem Lauf gleich an.
Das Skript wertet den Plan im JSON-Format aus, bestimmt den Change-Typ und erzeugt einen Change-Datensatz als Markdown.
#!/usr/bin/env python3 """Klassifiziert einen Terraform-Plan als ITIL-Change-Typ. Aufruf: classify_change.py <plan.json> <stack> Erzeugt: change-record.md und change.env (für GitLab-dotenv-Reports) """ import datetime import json import os import sys # Änderungen an diesen Typen sind immer ein Normal Change CRITICAL_TYPES = { "azurerm_resource_group", "azurerm_virtual_network", "azurerm_subnet", "azurerm_network_security_group", "azurerm_subnet_network_security_group_association", "azurerm_shared_image_gallery", "azurerm_shared_image", } def classify(plan): changes, reasons = [], [] for rc in plan.get("resource_changes", []): actions = rc["change"]["actions"] if actions in (["no-op"], ["read"]): continue changes.append((rc["address"], rc["type"], actions)) if "delete" in actions: art = "wird ersetzt" if "create" in actions else "wird gelöscht" reasons.append(f"{rc['address']} {art}") elif rc["type"] in CRITICAL_TYPES: reasons.append(f"{rc['address']}: kritischer Ressourcentyp") if not changes: return "none", changes, reasons return ("normal" if reasons else "standard"), changes, reasons def main(): plan_file, stack = sys.argv[1], sys.argv[2] with open(plan_file, encoding="utf-8") as fh: plan = json.load(fh) change_type, changes, reasons = classify(plan) lines = [ f"# Change-Datensatz – Schicht {stack}", "", f"- Zeitpunkt: {datetime.datetime.now(datetime.timezone.utc):%Y-%m-%d %H:%M} UTC", f"- Change-Typ: **{change_type}**", f"- Commit: {os.environ.get('CI_COMMIT_SHA', 'lokal')}", f"- Merge Request: {os.environ.get('CI_MERGE_REQUEST_IID', '-')}", f"- Ausgelöst von: {os.environ.get('GITLAB_USER_LOGIN', 'lokal')}", "", "## Geplante Änderungen", ] lines += [f"- `{addr}`: {'/'.join(act)}" for addr, _, act in changes] or ["- keine"] if reasons: lines += ["", "## Gründe für Normal Change"] + [f"- {r}" for r in reasons] with open("change-record.md", "w", encoding="utf-8") as fh: fh.write("\n".join(lines) + "\n") with open("change.env", "w", encoding="utf-8") as fh: fh.write(f"CHANGE_TYPE_{stack.upper()}={change_type}\n") print(f"[{stack}] Change-Typ: {change_type}") for r in reasons: print(f" - {r}") if __name__ == "__main__": main()
Das Skript liegt als ci/classify_change.py im Repository. Die Liste CRITICAL_TYPES ist selbst eine Change-Management-Entscheidung und deshalb über CODEOWNERS geschützt.
Ergänzungen zur IaC-Pipeline:
stages: [validate, security, image, plan, change, apply, drift] .plan: # ... bisheriger Inhalt, am Ende des script-Blocks ergänzen: script: - *tf_init - terraform plan -lock=false -out=tfplan - terraform show -no-color tfplan > plan.txt - terraform show -json tfplan > plan-full.json - python3 ../ci/classify_change.py plan-full.json "$STACK" # ... jq-Zusammenfassung für das MR-Widget wie bisher artifacts: paths: ["$STACK/tfplan", "$STACK/plan.txt", "$STACK/change-record.md"] reports: terraform: "$STACK/plan.json" dotenv: "$STACK/change.env" expire_in: 1 week change:gate: stage: change needs: ["plan:base", "plan:workload"] script: - echo "Change-Typ base=$CHANGE_TYPE_BASE workload=$CHANGE_TYPE_WORKLOAD" - | if [ "$CHANGE_TYPE_BASE" = "normal" ] || [ "$CHANGE_TYPE_WORKLOAD" = "normal" ]; then case ",$CI_MERGE_REQUEST_LABELS," in *,change-approved,*) echo "Normal Change: Genehmigung liegt vor." ;; *) echo "Normal Change: Label 'change-approved' fehlt – Genehmigung erforderlich." exit 1 ;; esac else echo "Standard Change: MR-Freigabe genügt." fi rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
Zusammen mit der Projekteinstellung „Pipelines must succeed“ lässt sich ein Normal Change nicht mergen, bevor die Genehmigung vorliegt. Nach dem Setzen des Labels muss die MR-Pipeline neu gestartet werden.
Grenzen der Lab-Lösung: Ein Label kann jeder mit Reporter-Rechten setzen. Im Produktivbetrieb prüft der Gate-Job stattdessen per API im ITSM-Werkzeug (z. B. ServiceNow oder Jira Service Management), ob der im MR referenzierte Change genehmigt ist. Das Prinzip bleibt gleich: Die Pipeline fragt nach, der Mensch genehmigt im dafür vorgesehenen System.
Auch im Notfall gilt: Kein Handeingriff im Azure-Portal. Jeder manuelle Eingriff erzeugt Drift, die später niemand mehr erklären kann.
emergency im TitelFür den Fall, dass die Pipeline selbst ausfällt, gibt es einen Break-Glass-Zugang: ein Notfallkonto mit Schreibrechten, dessen Nutzung alarmiert wird. Danach wird die manuelle Änderung per Merge Request in den Code übernommen; der nächtliche Drift-Check stellt sicher, dass das nicht vergessen wird.
In der Pet-Welt ist jeder Server ein CI mit eigenem Lebenslauf. In der Cattle-Welt verschiebt sich die Granularität: Einzelne Instanzen leben Stunden oder Tage und sind austauschbar. Sie als CI zu pflegen, erzeugt nur Rauschen.
| Pet-Welt: CI | Cattle-Welt: CI |
|---|---|
Server zeus | Service „Webshop“, umgesetzt durch Scale Set vmss-iaclab-web |
| Installierte Softwarestände | Image-Version 1.1.0 (enthält den vollständigen Softwarestand) |
| Konfigurationsdateien | Commit im Repository |
| Handgepflegte Abhängigkeiten | Abhängigkeiten aus Terraform-Code und Resource Graph |
Damit Azure-Ressourcen einem Service zugeordnet werden können, bekommen alle Ressourcen einheitliche Tags. Das Schema wird in locals.tags beider Schichten ergänzt:
locals {
tags = {
service = "iac-lab-web" # Service-Zuordnung in der CMDB
owner = "plattform-team" # verantwortliche Organisationseinheit
criticality = "low" # Grundlage für Priorisierung von Incidents
managed_by = "terraform"
repo = "gitlab.com/<gruppe>/iac-lab-azure"
layer = "workload"
}
}
Eine Azure Policy prüft, ob die Pflicht-Tags vorhanden sind. Die Policy startet im Prüfmodus (enforce = false), damit sie nichts blockiert, bis alle Ressourcen konform sind:
data "azurerm_policy_definition" "require_tag" {
display_name = "Require a tag on resources"
}
resource "azurerm_resource_group_policy_assignment" "require_service_tag" {
name = "require-tag-service"
resource_group_id = azurerm_resource_group.web.id
policy_definition_id = data.azurerm_policy_definition.require_tag.id
enforce = false # erst prüfen, später erzwingen
parameters = jsonencode({
tagName = { value = "service" }
})
}
Damit ist auch die Regel selbst Code, versioniert und über die Pipeline geändert – Policy as Code.
Der Azure Resource Graph liefert den aktuellen Bestand aller Ressourcen eines Services in Sekunden:
az graph query -q " Resources | where tags.service == 'iac-lab-web' | project name, type, location, resourceGroup, owner=tostring(tags.owner), criticality=tostring(tags.criticality) " --output table
Als nächtlicher Pipeline-Job mit –output json lässt sich das Ergebnis in die CMDB importieren. Drei Quellen ergänzen sich:
| Quelle | Liefert |
|---|---|
| Terraform-Code | Soll-Zustand und Absicht (was sein soll) |
| Terraform-State | Was Terraform verwaltet und zuletzt gesehen hat |
| Resource Graph | Was in Azure tatsächlich existiert (was ist) |
Ressourcen, die im Resource Graph auftauchen, aber keinen Tag managed_by = terraform tragen, sind Kandidaten für Schatten-IT oder vergessene Handarbeit.
Für jede Änderung lässt sich lückenlos beantworten, was, wann, von wem, warum und mit wessen Genehmigung geändert wurde:
| Frage | Nachweis |
|---|---|
| Was? | Diff im Merge Request, plan.txt, change-record.md |
| Wann? | Zeitstempel von Merge und Apply-Job |
| Wer? | Autor des MR, Auslöser des Apply-Jobs |
| Warum? | Beschreibung des MR, verknüpftes Ticket |
| Genehmigt von? | MR-Freigaben, Change-Genehmigung |
| Wie in Azure? | Aktivitätsprotokoll, Anmeldeprotokolle der Workload-Identität |
| Vorheriger Stand? | Git-Historie, versionierter Terraform-State |
In regulierten Umfeldern, etwa im Finanzsektor unter DORA, verlangen die Aufsichtsvorgaben ein dokumentiertes, geprüftes und genehmigtes IKT-Änderungsmanagement. Eine Pipeline erfüllt diese Anforderungen strukturell besser als ein manueller Prozess, weil sie keinen Schritt auslassen kann. Wichtig sind passende Aufbewahrungsfristen für Pipeline-Protokolle und Artefakte.
Die vier Kennzahlen aus der DevOps-Forschung (DORA – DevOps Research and Assessment, nicht zu verwechseln mit der EU-Verordnung DORA) passen direkt zur ITIL-Praxis Continual Improvement:
| Kennzahl | Frage | Quelle in der Pipeline |
|---|---|---|
| Deployment Frequency | Wie oft wird erfolgreich ausgerollt? | Anzahl erfolgreicher Apply-Jobs |
| Lead Time for Changes | Wie lange vom Commit bis zur Produktion? | Zeit zwischen erstem Commit und Apply |
| Change Failure Rate | Welcher Anteil der Changes führt zu Störungen? | Rollbacks, Hotfixes je Apply |
| Time to Restore Service | Wie schnell ist der Service nach einer Störung wieder da? | Dauer bis zum erfolgreichen Rollback/Repair |
Ergänzend aus ITIL-Sicht: Anteil Standard Changes an allen Changes (Ziel: steigend) und Anzahl Drift-Meldungen (Ziel: null).