Kai Ole Hartwig
15 Min. Lesezeit
Niedrig

tofu init ohne GitHub: OpenTofu-Provider aus meiner eigenen OCI-Registry statt von GitHub installieren

Manche Ausfälle passieren grundsätzlich nur der Infrastruktur anderer Leute — und ausgerechnet auf dem eigenen kritischen Pfad, im denkbar ungünstigsten Moment. Bei mir war das registry.opentofu.org, das mitten in einem tofu apply plötzlich 504er zurückgab und mein gesamtes Release gleich mit sich riss.

Kein Retry-Loop hat das gelöst, kein längeres Timeout. Sondern die Abhängigkeit komplett zu entfernen: die tatsächlich benötigten Provider als OCI-Artefakte neu verpacken, in die ohnehin schon betriebene Container-Registry pushen, und tofu init stattdessen dorthin zeigen lassen. Um das Warum geht es in diesem Beitrag, aber vor allem um das Wie — inklusive der Teile von OpenTofus OCI-Provider-Format, die sich erst erschließen, wenn man eines tatsächlich von Hand zusammengebaut hat.

01 — Der Aufbau: eine IPv6-only-Flotte, und der eine Ausreißer

Meine GitLab-CI-Worker sind kurzlebige AWS-Spot-Instanzen, und die Flotte läuft bewusst IPv6-only: Die Instanzen kommen ganz ohne öffentliche IPv4-Adresse hoch und gehen ausschließlich über IPv6 nach außen. Das ist gleichzeitig ein Kosten- und ein Sicherheitshebel — der Großteil der kurzlebigen Worker-Stunden trägt keine öffentliche v4-Adresse, und die Netzwerk-Abhängigkeiten, auf die es ankommt (docker.io, quay.io, die eigene Registry, die Sprach-CDNs), sind ohnehin alle dual-stack.

Eine Handvoll Jobs braucht trotzdem noch IPv4 — meist alles, was mit GitHub spricht. Die tragen einen eigenen Tag und landen auf einem kleinen IPv4-Pool. Das ganze Spiel besteht darin, diese Liste kurz zu halten und genau zu beobachten, was sich wieder hineinschleicht. Lange Zeit stand koh-infra darauf — das OpenTofu-Repo, das den AWS-Account selbst verwaltet. Der Grund dafür war deutlich banaler, als ich erwartet hätte.

02 — Das Problem: „Von OpenTofu installieren“ heißt „von GitHub herunterladen“

Schreibt man source = "hashicorp/aws", löst OpenTofu das über die öffentliche Registry unter registry.opentofu.org auf. Nur hostet diese Registry die Provider-Binaries gar nicht selbst — sie liefert lediglich einen Redirect auf die Release-Assets, und die liegen auf GitHub. Und GitHub veröffentlicht bis heute schlicht keinen AAAA-Eintrag: rein IPv4.

Damit pinnte jedes einzelne tofu init — auch das, das unmittelbar vor dem apply auf der Infrastruktur des Accounts selbst läuft — den jeweiligen Job auf den IPv4-Pool und schob nebenbei das Release-CDN eines Drittanbieters auf den kritischen Pfad eines Applys. Mitten in einer Spot-Reclaim-Welle im Sommer 2026 begann GitHubs Asset-Endpunkt unter der zusätzlichen Last zeitweise 504er auszuwerfen, und einige Infra-Pipelines wurden rot — nicht weil ich etwas geändert hätte, sondern weil irgendwo drei Redirects weiter ein Download getimeoutet war.

Der tatsächliche Fehler

 

tofu init → registry.opentofu.org → 307 → objects.githubusercontent.com → 504 Gateway Timeout

 

Weder mein Code noch mein Netzwerk, und auf keine Weise retrybar, die tatsächlich geholfen hätte. Exakt auf dem Pfad zwischen „push auf main“ und „Infrastruktur geändert“.

Zwei Dinge waren hier eigentlich falsch, und nur eines davon war IPv4. Das andere: Meine sensibelste Automatisierung — die, die IAM und Netzwerk ändern kann — hing zur Apply-Zeit von der Verfügbarkeit eines öffentlichen Release-CDNs ab, das ich nicht kontrolliere. Selbst auf dem IPv4-Pool war das eine Abhängigkeit, die ich loswerden wollte.

03 — Die Optionen: Woher sollten die Provider eigentlich kommen?

Für die Provider-Installation abseits der öffentlichen Registry bietet OpenTofu ein paar Wege an. Realistisch waren für mich zwei davon:

OptionMechanismusBewertung
A — Netzwerk-Mirror (Filesystem/S3)tofu providers mirror in ein Verzeichnis, das nach S3 synchronisiert wird, dazu ein network_mirror-BlockFunktioniert — aber ein zweiter Distributionskanal mit eigenem Auth-Modell, eigener Bucket-Policy und eigener Dual-Stack-Geschichte, die man richtig hinbekommen muss
B — OCI-Mirror in der Registry (gewählt)OpenTofu 1.10+ installiert Provider direkt aus einer OCI-Registry — derselben Registry, die bereits meine Images ausliefertDual-Stack, gecacht, Auth bereits gelöst. Ein Endpunkt, ein Credential-Modell, nichts Neues zu betreiben

Option B gewann nach einem einzigen Prinzip: kein zweites Ding aufbauen, wenn das erste den Job längst erledigt. Ich betreibe ohnehin eine Container-Registry, dual-stack, meine CI authentifiziert sich bei jeder Pipeline sowieso schon dagegen. Provider-Artefakte darüber auszuliefern fügt keine neue Angriffsfläche hinzu — sie werden einfach ein weiteres Set getaggter Artefakte, direkt neben den Container-Images. Der Preis dafür: Provider müssen ins OCI-Format neu verpackt werden. Und genau da wird es interessant.

04 — Das Format: Wie ein OpenTofu-Provider als OCI-Artefakt aussieht

Das ist der Teil, der in keinem Quickstart auftaucht. Eine Provider-Version ist kein einzelner Blob, sondern ein kleiner, zweistufiger Manifest-Baum — weil eine Provider-Version pro Betriebssystem und Architektur ein eigenes Binary mitbringt, und OpenTofu bei init das passende auswählen muss.

Bei init zieht tofu zunächst den Versions-Index, gleicht os/arch des Runners gegen die platform-Felder ab und lädt anschließend nur das Zip dieser einen Plattform. Beide meiner Spot-Architekturen — amd64 und arm64 — lösen sich dabei aus demselben Tag auf. Assembliert wird das Ganze lokal in einem OCI-Layout: pro Plattform push, dann manifest index create, um beide zu verknüpfen. Erst danach wandert der fertige Baum per oras cp in einem Rutsch in die Registry.

Eine Version bauen (mirror:tofu-providers)

 

# alles in einem LOKALEN oci-layout zusammenbauen — noch kein Registry-Kontakt
for plat in amd64 arm64; do
  oras push --artifact-type application/vnd.opentofu.provider-target \
    --artifact-platform "linux/${plat}" \
    --oci-layout "tmp-layout:linux_${plat}" "${zip}:archive/zip"
done

# der Versions-Index, lokal über die beiden Plattform-Manifeste gebaut
oras manifest index create --artifact-type application/vnd.opentofu.provider \
  --oci-layout "tmp-layout:${ver}" linux_amd64 linux_arm64

# den fertigen Baum ausliefern — nur der Index bekommt einen Tag, Plattformen gehen per Digest
oras cp --from-oci-layout "tmp-layout:${ver}" "${dest}:${ver}"

 

Weil alles zunächst lokal zusammengebaut und erst dann per oras cp als kompletter Baum verschoben wird, bekommt am Ende nur der Versions-Index einen Tag — die Plattform-Manifeste gehen per Digest hoch. So bleibt pro Version genau ein sauberer Tag in der Registry übrig.

05 — Der Producer: ein Job, dessen einziger Zweck es ist, GitHub genau einmal anzufassen

Das Neuverpacken lebt in meinem Mirroring-Repo als Job namens mirror:tofu-providers. Sein Input: ein winziges, backend-loses Tofu-Modul, das nur einen Zweck hat — festzulegen, welche Provider und Versionen gespiegelt werden. Renovate hält die Pins dabei automatisch aktuell.

Das gespiegelte Set (tofu-providers/versions.tf)

 

terraform {
  required_providers {
    aws  = { source = "hashicorp/aws",  version = "6.52.0" }
    http = { source = "hashicorp/http", version = "3.6.0" }
    tls  = { source = "hashicorp/tls",  version = "4.3.0" }
  }
}

 

Dieses Set muss immer eine Obermenge dessen bleiben, was der Consumer sperrt. Zu dieser Kopplung gleich mehr.

Der Job selbst führt tofu providers mirror -platform=linux_amd64 -platform=linux_arm64 aus, zieht damit jeden deklarierten Provider für beide Architekturen in einen Filesystem-Mirror, durchläuft anschließend die entstandenen Zips und übernimmt die oras-Assemblierung aus dem vorigen Abschnitt. Das ist die einzige Stelle im ganzen Setup, an der GitHub überhaupt berührt wird — und das passiert selten, ausgelöst nur durch Änderungen an versions.tf oder einen Zeitplan, nicht bei jeder Pipeline. Ein Job zahlt die IPv4-Steuer, damit sie sonst niemand zahlen muss.

06 — Der Consumer: zwei Blöcke, und eine bewusste Sackgasse

Auf koh-infra-Seite steckt die gesamte Umleitung in einer einzigen Config-Datei. oci_mirror mappt jede registry.opentofu.org-Source auf meinen Registry-Pfad, direct { exclude } entfernt danach die GitHub-Installationsmethode für exakt dieselben Sources.

Consumer-Config (koh-infra/.tofurc)

 

provider_installation {
  oci_mirror {
    repository_template = "registry.ole-hartwig.eu/devops/ci-mirrors/${namespace}/${type}"
    include             = ["registry.opentofu.org/*/*"]
  }
  direct {
    exclude = ["registry.opentofu.org/*/*"]   # kein GitHub-Fallback — mit Absicht
  }
}

 

Aktiv wird das Ganze nur, wenn CI TF_CLI_CONFIG_FILE setzt — lokale Entwicklung ohne diese Env-Variable läuft weiter über die normale Direct-Installation, auf meinem Laptop ändert sich also nichts.

Diese exclude-Zeile ist die eigentlich entscheidende, und man liest sie leicht als Versehen. Warum den Fallback kappen? Weil ein stiller Fallback auf GitHub genau die IPv4-Abhängigkeit und Flakiness unbemerkt zurückbringen würde, die gerade entfernt wurde — funktioniert einwandfrei, bis GitHub das nächste Mal wackelt, und dann steckt man wieder mitten im selben Ausfall, den man doch längst behoben glaubte. Lieber früh und laut scheitern: Fehlt eine gesperrte Version im Mirror, stoppt init sofort und nennt das fehlende Artefakt beim Namen. Aus einem seltenen Rätsel-Ausfall wird so ein langweiliger, sofort erkennbarer Fehler.

Der Preis für diese Klarheit: eine harte Kopplung, die ehrlich gehalten werden muss.

 

koh-infra/.terraform.lock.hcl  ⊆  ci-mirrors/tofu-providers/versions.tf

 

Warum das in der Praxis hält

Renovate pflegt beide Repos. Driften sie doch einmal auseinander, zeigt sich das sofort an einem roten tofu init, das das fehlende registry.opentofu.org/<ns>/<type>:<ver> beim Namen nennt — billig zu diagnostizieren, billig zu beheben: Version ergänzen, Mirror-Job laufen lassen, erneut versuchen. Eine laute Kopplung schlägt einen stillen Fallback jedes Mal.

07 — Die Falle: Reihenfolge beim ersten Aktivieren

Diese fallback-lose Kopplung hat einen einzigen wunden Punkt, und der sticht nur einmal zu. Weil der Consumer GitHub gar nicht mehr erreichen kann, muss der Mirror existieren, bevor der Consumer ihm überhaupt vertraut. Die einmalige Rollout-Reihenfolge ist deshalb strikt:

  1. Zuerst den Producer mergen und mirror:tofu-providers einmal laufen lassen, damit die Registry tatsächlich befüllt ist.
  2. Sicherstellen, dass der CI-Job-Token des Consumers aus der Mirror-Registry pullen darf.
  3. Erst dann die .tofurc des Consumers mergen und dessen IPv4-Tag fallen lassen.

Wird stattdessen zuerst der Consumer gemergt, werden ausnahmslos alle seine Pipelines rot — Mirror leer, oder Token noch nicht zugelassen. Nach dieser ersten Aktivierung spielt die Reihenfolge keine Rolle mehr, nur die Versions-Set-Invariante zählt dann noch.

08 — Das Ergebnis: eine Fehlerquelle weniger, die 504 werfen kann

Mit dem Mirror an Ort und Stelle ziehen tofu init, fmt und validate vollständig aus der eigenen dual-stack-Registry. Den IPv4-Tag konnte ich daraufhin von diesen Jobs entfernen — sie liefen fortan wieder auf der IPv6-Flotte, wo der Rest ohnehin schon läuft. GitHub ist vom Provider-Install-Pfad komplett verschwunden.

KennzahlBedeutung
0GitHub-Hops auf dem Apply-Pfad (vorher 2 Redirects tief)
IPv6init / fmt / validate zurück vom IPv4-Pool
1Job, der GitHub noch anfasst — selten, nur bei Lock-Bumps

Um genau zu sein, was das behoben hat und was nicht: Das eigentliche AWS-apply erreicht weiterhin nicht-dual-stack-AWS-Endpunkte, koh-infra ist also noch nicht vollständig IPv4-frei — ein separates Problem für einen anderen Tag. Aber die Provider-Install-Hälfte ist erledigt, und genau die war die instabile. Hat registry.opentofu.org das nächste Mal einen schlechten Nachmittag, bekommt meine Infra-Pipeline davon gar nichts mehr mit.

09 — Was sich gewehrt hat: drei Wege, wie der Entwurf falsch war, bevor er richtig war

Die Idee war simpel, der Rollout dagegen nicht. Jedes der folgenden drei Probleme wirkte zunächst, als wäre der Mirror kaputt — und entpuppte sich am Ende jedes Mal als Lücke zwischen dem, was ich über ein Tool angenommen hatte, und dem, was es tatsächlich tat. GitHub hat dabei kein einziges Mal geflackert: Download und Neuverpackung liefen jedes Mal sauber durch, der Fehler lag bei mir.

Ein Flag aus der Zukunft

Der erste Push starb mit oras push: unknown flag: --artifact-platform. Gepinnt hatte ich oras1.2.3 — ein plausibel wirkendes „aktuell stabil“, aus dem Gedächtnis gewählt. Nur landete das Flag, das die Plattform auf ein Artefakt stempelt, erst in 1.3.0. Ich hatte eine Version gepinnt und im selben Job ein Feature aus einer neueren genutzt. Die Lehre daraus lautet nicht „nimm die neueste Version“, sondern: verifizieren, dass die Flags, auf die man sich verlässt, tatsächlich in der gepinnten Version existieren — nicht nur, dass die Version selbst real ist.

Höchstens zwei Segmente tief

Als nächstes wurde jeder Push mit 401 abgelehnt — sogar ein direkter, obwohl derselbe Job-Token meine übrigen Mirror-Images ohne Probleme gepusht hatte. Der Pfad lautete ci-mirrors/tofu-providers/hashicorp/aws. Geklärt hat es ein Wegwerf-Probe-Job in einem einzigen Durchlauf: Ein GitLab-CI-Job-Token kann Registry-Images höchstens zwei Pfadsegmente unterhalb des Projekts pushen. probe/a/b funktionierte, probe/a/b/c bekam 401. Mein Gruppierungs-Präfix machte den Pfad eine Ebene zu tief. Gekürzt auf ci-mirrors/hashicorp/aws — ohnehin die kanonische OpenTofu-Form — war das Problem behoben. Mit Rechten zum Anlegen von Repos hatte die Tiefe nichts zu tun, das konnte der Token längst. Drei Ebenen tief kann er schlicht nicht.

Die Credentials, die tofu einfach nicht lesen wollte

Der Mirror war voll, der Pfad stimmte, und tofu init bekam trotzdem 403. Übergeben hatte ich eine ~/.docker/config.json mit username/password — genau so, wie man sie für die Docker-CLI schreiben würde. OpenTofu liest solche Dateien aber im containers-auth.json-Format, das ausschließlich das base64-auth-Feld honoriert. Verwertbares fand sich also nichts, tofu fiel auf anonym zurück, und die interne Registry lehnte höflich ab. Der verräterische Hinweis: Der Runner zog tofus eigenes Job-Image ganz ohne Stocken aus exakt derselben Registry — weil Image-Pulls die Job-Payload-Credentials nutzen, eine völlig andere Tür. Behoben war das Problem erst, als ich aufhörte, die Datei von Hand zu bauen, und tofu stattdessen einen expliziten oci_credentials-Block gab, in dem username/password unterstützt wird und der über ambiente Discovery gewinnt.

Das Muster hinter allen dreien

Jeder einzelne Fehler war eine selbstsichere Annahme über das Verhalten eines Tools — das Flag existiert, die Pfadtiefe passt, die Credential-Datei wird gelesen —, die sich immer erst durch einen tatsächlichen Lauf widerlegen ließ. Das günstigste Debugging-Werkzeug war dabei keine klügere Vermutung, sondern ein 40-Sekunden-Probe-Job, der die Registry direkt fragte, statt mich zu fragen.

Die weiterreichende Lehre lerne ich offenbar immer wieder neu: Die günstigste Supply-Chain-Abhängigkeit, die sich entfernen lässt, ist die, die man sich selbst aus Infrastruktur bedienen kann, die man ohnehin schon betreibt. Es fehlte mir kein Werkzeug — mir fehlte die Erkenntnis, dass eine Container-Registry und eine Provider-Registry auf Artefakt-Ebene ein und dasselbe sind.

Häufige Fragen

Muss meine gesamte CI-Flotte IPv6-only sein, damit dieser Ansatz funktioniert?+

Nein. Der IPv6-only-Aufbau war hier nur der Auslöser der Geschichte — der OCI-Provider-Mirror selbst nützt jedem, der tofu init von der Verfügbarkeit von registry.opentofu.org und GitHub entkoppeln möchte, unabhängig vom eigenen IP-Stack. Auch eine reine IPv4-Flotte profitiert davon, einen Single Point of Failure aus dem Apply-Pfad zu entfernen.

Funktioniert der OCI-Mirror auch für private oder interne Provider, nicht nur für hashicorp/aws & Co.?+

Ja. Das Verfahren aus Teil 04 — Provider-Zip pro Plattform in ein provider-target-Artefakt packen, per Versions-Index verknüpfen — ist völlig unabhängig davon, ob der Provider aus der öffentlichen OpenTofu-Registry stammt oder selbst gebaut wurde. Bei einem komplett privaten Provider entfällt lediglich der Producer-Schritt „von der öffentlichen Registry spiegeln“ — das eigene Build-Artefakt wandert direkt in dasselbe OCI-Format.

Was passiert, wenn ein Provider im Lock-File auftaucht, den der Mirror noch nicht kennt?+

Genau dafür ist die bewusste Sackgasse aus Teil 06 gedacht: tofu init stoppt sofort mit einer klaren Fehlermeldung, die das fehlende Artefakt (registry.opentofu.org/<namespace>/<type>:<version>) beim Namen nennt, statt still auf GitHub auszuweichen. Die Behebung ist rein mechanisch — fehlende Version in tofu-providers/versions.tf ergänzen, Mirror-Job laufen lassen, init erneut ausführen.

Brauche ich eine separate OCI-Registry, oder reicht die, die ich schon für Container-Images nutze?+

Die bestehende reicht völlig aus — genau darauf zielt Option B aus Teil 03 ab. Provider-Artefakte sind, genau wie Container-Images, einfach getaggte OCI-Manifeste in derselben Registry, nur unter einem eigenen Repository-Pfad (bei mir ci-mirrors/<namespace>/<type>). Eine zweite Registry nur für Provider zu betreiben, wäre genau die zusätzliche Betriebsfläche, die dieser Ansatz eigentlich vermeiden soll.

Warum den GitHub-Fallback per exclude ganz abschalten, statt ihn als Sicherheitsnetz zu behalten?+

Weil ein stiller Fallback genau die Abhängigkeit unbemerkt zurückbringt, die eigentlich entfernt werden sollte — er funktioniert einwandfrei, bis GitHub wieder wackelt, und dann steht man wieder im selben Rätsel-Ausfall wie zuvor, nur schwerer zu diagnostizieren, weil man ihn längst für behoben hielt. Ein sofortiger, klar benannter Fehler bei fehlendem Mirror-Eintrag ist bewusst die schlechtere Kurzfrist-Erfahrung — für die deutlich bessere Langfrist-Garantie.

Gilt die GitLab-Pfadtiefen-Begrenzung (max. zwei Segmente) auch für andere Registries?+

Das ist konkret eine Grenze der GitLab-Container-Registry für über den CI-Job-Token authentifizierte Pushes — andere OCI-Registries wie Harbor, ECR, Docker Hub oder GHCR haben eigene, teils großzügigere oder gar keine Pfadtiefen-Grenzen. Übertragbar ist nicht die konkrete Zahl zwei, sondern die Lehre dahinter: Vor einer tief verschachtelten Repository-Hierarchie lieber kurz mit einem Wegwerf-Probe-Push verifizieren, was die jeweilige Registry und der jeweilige Token tatsächlich erlauben.

Fazit

Kein Retry-Loop, kein längeres Timeout — einfach die Abhängigkeit entfernen. OpenTofus OCI-Provider-Format entpuppt sich als kleiner, sauber spezifizierter Manifest-Baum, sobald man einmal eines von Hand zusammengesetzt hat. Die eigentliche Arbeit steckte gar nicht im Format, sondern in drei stillen Annahmen über Tool-Verhalten, die sich erst durch einen echten Lauf widerlegen ließen. Die übergreifende Lehre bleibt: Die günstigste Supply-Chain-Abhängigkeit, die sich loswerden lässt, ist die, die man sich selbst aus Infrastruktur bedienen kann, die man ohnehin schon betreibt — eine Container-Registry und eine Provider-Registry sind auf Artefakt-Ebene ein und dasselbe.

Ich löse Ihre CI/CD-Pipeline von unnötigen Abhängigkeiten auf fremde Release-CDNs — OCI-Provider-Mirrors, IPv6-only-Fleet-Design und die Credential-Fallstricke, die in keiner Doku stehen.

OCI-Manifest-Assemblierung mit oras, GitLab-Registry-Grenzen, OpenTofu-Credential-Discovery — und die Rollout-Reihenfolge, auf die es beim ersten Aktivieren ankommt.

Plattform-Betrieb statt Beratung auf Papier: Ich richte Ihre CI/CD- und Provider-Supply-Chain ein, härte sie und betreibe sie laufend.

Termin buchen →

Über den Autor

[Translate to English:] Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.