4. August 2026 · 6 Min. Lesezeit
Der Fehler, den ich dem Provider angelastet habe
Wochenlang hatte ein Stack einen terraform plan, den ich zu ignorieren gelernt hatte. Bei jedem Lauf, in jeder Umgebung, wollte er die Server-Side-Encryption-Regel von einem S3-Bucket entfernen und durch eine leere ersetzen. Der laufende Bucket war korrekt verschlüsselt. In AWS war nichts kaputt. Der Plan konvergierte einfach nicht, und egal welche Werte ich deklarierte, der nächste Plan verlangte dieselbe Änderung erneut.
Ich hakte es gedanklich unter “AWS-Provider-Eigenheit, nicht der Mühe wert” ab und machte weiter. Das war der Fehler. Der Provider tat genau das, was ihm gesagt wurde. Die Lüge lag komplett vor Terraform, in meinem eigenen Code, und sie lag offen sichtbar in einer Datei, die ich nie geöffnet hatte.
Der Reflex, und warum er falsch ist
Der Stack ist CDKTF in Python. Man schreibt Ressourcen als typisierte Klassen oder als einfache Dicts, cdktf synth rendert sie in eine cdk.tf.json-Datei, und Terraform plant gegen diese. Wenn ein Plan sich seltsam verhält, ist der Reflex, die Schicht zu beschuldigen, in die man nicht hineinsehen kann: die Diff-Logik des Providers, eine bekannt-fragile Ressource, eine State-Eigenheit. S3-Encryption-Ressourcen haben einen Ruf für Drift-Rauschen, also passte das gut zu der Geschichte, die ich glauben wollte.
Der Reflex ist falsch, weil CDKTF ein Artefakt hat, das man lesen kann. cdk.tf.json ist exakt das JSON, gegen das Terraform plant. Es ist keine Blackbox; es ist die kompilierte Ausgabe, die nach jedem Synth auf der Platte liegt. Ich hatte Wochen damit verbracht, über das Verhalten des Providers nachzudenken, und null Sekunden damit, mir anzuschauen, was ich ihm tatsächlich übergab.
Als ich die Datei endlich öffnete, war die Regel da. Sie war nur leer:
"rule": [
{}
]
Terraform driftete nicht. Es versuchte getreulich, einen Bucket mit einer echten Encryption-Regel gegen eine deklarierte Konfiguration abzugleichen, die besagte, die Regel solle keinen Inhalt haben. Ein leerer Block ist eine positive Anweisung, keine Abwesenheit. Der Provider war unschuldig.
Die eigentliche Ursache
Hier ist der Code, der diesen leeren Block erzeugt hat. Er sieht völlig gewöhnlich aus:
S3BucketServerSideEncryptionConfiguration(
self, "encryption",
bucket=bucket.id,
rule=[{
"apply_server_side_encryption_by_default": {
"sse_algorithm": "aws:kms",
"kms_master_key_id": key.arn,
},
"bucket_key_enabled": True,
}],
)
Das rule-Argument nimmt eine Liste. Jedes Element ist ein verschachteltes Dict: ein Block, der einen weiteren Block enthält. Diese Verschachtelung ist das ganze Problem.
Die Python-Bindings von CDKTF sitzen auf jsii auf, das Python-Dicts automatisch in die typisierten Structs umwandelt, die das darunterliegende TypeScript erwartet. Diese Umwandlung steigt nicht rekursiv in Listenelemente ab, die selbst verschachtelte Dicts sind. Wenn jsii ein Sequence[Union[SomeTypedClass, Dict]] durchläuft und ein Dict findet, dessen Werte weitere Dicts sind, gelingt es ihm nicht, die innere Struktur abzubilden, und es gibt still ein leeres Objekt aus. Keine Exception. Keine Warnung. Synth endet mit Exit-Code null.
Was das wirklich fies macht: Dasselbe Muster funktioniert überall sonst. Eine Liste von Dicts mit flachen Skalarwerten wird sauber umgewandelt:
# Das rendert korrekt. Die Listenelemente sind flach.
taint=[{
"key": "dedicated",
"value": "gpu",
"effect": "NO_SCHEDULE",
}]
Der fehlerhafte Fall und der funktionierende Fall sind also visuell identisch: eine Liste von Dicts, die einem Ressourcenargument übergeben wird. Der einzige Unterschied ist, ob die Werte des Dicts Skalare oder weitere Dicts sind, und nichts im Code, in der Synth-Ausgabe oder in deinen Assertions unterscheidet sie. Man kann es nicht durch Lesen des Python-Codes erkennen. Man kann es nur durch Lesen des JSON erkennen.
Die Falle in der Falle
Der leere Block ist schlimmer als ein No-Op. Er ist eine positive Deklaration von “diese Regel hat keine Konfiguration”, was bedeutet, dass der Plan niemals konvergieren kann. Terraform sieht eine laufende Regel, sieht eine deklariert-leere Regel und schlägt das Delta vor. Man bearbeitet das Python, um mehr Werte hinzuzufügen, und jsii lässt sie auf dieselbe Weise fallen, also ist das JSON weiterhin [{}] und der nächste Plan ist identisch. Die Konfigurationsänderung, die es beheben sollte, kann es nicht beheben, weil deine Werte den Synth nie überleben, um überhaupt zu Terraform zu gelangen. Deshalb las es sich wie unbehebbare Drift. In gewissem Sinne war es das auch, solange ich versuchte, es in der falschen Schicht zu beheben.
Die Lösung
Hör auf, jsii ein verschachteltes Dict zum Raten zu übergeben. Konstruiere die typisierte Klasse explizit, und die Umwandlung hat nichts zu erschließen:
S3BucketServerSideEncryptionConfiguration(
self, "encryption",
bucket=bucket.id,
rule=[S3BucketServerSideEncryptionConfigurationRuleA(
apply_server_side_encryption_by_default=(
S3BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultA(
sse_algorithm="aws:kms",
kms_master_key_id=key.arn,
)
),
bucket_key_enabled=True,
)],
)
Die Klassennamen sind wortreich und das angehängte A ist ein Artefakt daraus, wie die Bindings generiert werden, aber der Punkt ist, dass jsii nun ein voll typisiertes Objekt erhält statt eines Dicts, das es introspizieren muss. Die verschachtelte Struktur überlebt, das JSON rendert jeden Schlüssel, und der Plan konvergiert beim ersten Versuch. Die Faustregel: Jede Ressource, deren Listenelemente selbst einen verschachtelten Block enthalten, will die typisierte Klasse, nicht ein rohes Dict.
Die Lektion, die bleibt
Ich hatte diesen Fehler tatsächlich schon einmal gesehen und die Verbindung nicht hergestellt. Beim Kubernetes-Provider hatte das Bauen von Ressourcen aus Dicts still volumeMounts fallengelassen, einem StatefulSet ein leeres Passwort-Env hinterlassen und Readiness-Probes entfernt, alles ohne Synth-Fehler. Ich hatte das als Kubernetes-Provider-Merkwürdigkeit abgetan. Ist es nicht. Es ist eine Eigenschaft der CDKTF-Python-Bindings, und es taucht bei jedem Provider auf, sobald deine Ressource einen verschachtelten Block innerhalb einer Liste hat.
Die bleibende Lektion handelt nicht von S3 oder Kubernetes oder irgendeiner bestimmten Ressource. Sie lautet: Ein erfolgreicher Synth bedeutet, dass dein Code lief, nicht, dass er das renderte, was du meintest. Die beiden sind nicht dasselbe, und die Lücke zwischen ihnen ist genau da, wo dieser Fehler lebt. Grep-basierte Assertions bestehen, weil die Felder, die fallengelassen werden, die tief verschachtelten sind, für die niemand ein Grep schreibt. Dass Synth besteht, sagt dir nichts, denn die Felder fallenzulassen ist das, was Synth tut.
Wenn also ein Plan nicht konvergiert und die Ressource in der Konsole korrekt aussieht, widerstehe dem Drang, den Provider zu beschuldigen. Öffne das kompilierte Artefakt. Lies die cdk.tf.json, gegen die Terraform tatsächlich plant, nicht das Python, das du dir wünschst. Das Werkzeug tut fast immer genau das, was du ihm gesagt hast. Die interessante Frage ist, was du ihm tatsächlich gesagt hast, und diese Antwort steht geschrieben, einen Synth entfernt, in einer Datei, die die meisten Leute nie öffnen.