post

AWS Parameter Store im Build

Parameter Store im Build macht Konfiguration explizit, wirft aber harte Fragen zu Secrets und Reproduzierbarkeit auf.

≈ 8 Min. Lesezeit

Parameter Store im Build macht Konfiguration explizit, wirft aber harte Fragen zu Secrets und Reproduzierbarkeit auf.

Warum ich das aufschreibe

Der konkrete Anlass ist ein kleines Gatsby-Plugin, das ich gebaut habe: gatsby-plugin-aws-parameter-store-environment. Es zieht beim Build Konfigurationswerte aus dem AWS Systems Manager Parameter Store und stellt sie als Environment-Variablen bereit. Fünfzig Zeilen Code, ein sehr überschaubares Problem – und trotzdem eine gute Gelegenheit, eine Architekturfrage sichtbar zu machen, die deutlich größer ist als das Plugin selbst.

Die Frage lautet nicht „Wie hole ich einen Wert aus einem AWS-Service?", sondern: „Wo lebt die Konfiguration eines statischen Frontends, wer besitzt sie, und was kostet mich die Antwort im Betrieb?" Genau an dieser Grenze entscheidet sich, ob ein Deployment reibungslos läuft oder ob jedes Release ein kleiner manueller Kraftakt bleibt. Ein winziges Plugin ist oft der ehrlichste Ort, um so eine Entscheidung durchzudenken, weil nichts von der eigentlichen Frage ablenkt.

Das Problem: Konfiguration in einem statischen Build

Gatsby erzeugt zur Build-Zeit statisches HTML, CSS und JavaScript. Das ist der Kern des JAMstack-Versprechens: schnelle, cache-freundliche Auslieferung ohne Server im Anfragepfad. Der Preis dafür ist eine Verschiebung, die viele unterschätzen – Konfiguration wird nicht zur Laufzeit gelesen, sondern zur Build-Zeit eingebacken. Was während gatsby build in process.env steht, landet im Ergebnis. Was fehlt, fehlt für immer, bis der nächste Build läuft.

In der Praxis wächst daraus schnell ein bekanntes Muster. Jede Umgebung – lokal, Staging, Produktion – bekommt ihre eigene .env-Datei. Manche Werte liegen im CI-System als Secret, andere im Git-Repository, ein paar nur im Kopf desjenigen, der zuletzt deployt hat. Es gibt keine einzelne Quelle, sondern eine Streuung. Und Streuung erzeugt Drift: Staging läuft gegen einen anderen API-Endpunkt als gedacht, ein Feature-Flag ist in Produktion anders gesetzt als dokumentiert, ein rotierter Zugangsschlüssel wurde an drei von vier Stellen aktualisiert.

Das ist kein exotisches Randproblem. Es ist die häufigste Reibungsquelle zwischen „Code ist fertig" und „Änderung ist live". Und es ist teuer, weil es nicht als Ausfall auftritt, sondern als schleichende Unsicherheit: Niemand weiß mehr mit Bestimmtheit, welche Konfiguration ein bestimmter Build wirklich gesehen hat.

Die Idee: eine einzige Quelle für Build-Konfiguration

Der Parameter Store dreht diese Streuung um. Statt viele .env-Dateien zu pflegen, definiere ich die Konfiguration hierarchisch an einer Stelle in AWS, geordnet nach Anwendung und Umgebung:

# One source of truth, organized by app and environment
aws ssm put-parameter \
  --name "/mikebild-dev/production/GATSBY_API_URL" \
  --value "https://api.mikebild.com/graphql" \
  --type "String"

aws ssm put-parameter \
  --name "/mikebild-dev/production/CONTENTFUL_DELIVERY_TOKEN" \
  --value "cda-xxxxxxxx" \
  --type "SecureString" \
  --key-id "alias/build-config"

Der Build zieht dann den ganzen Teilbaum einer Umgebung und exportiert ihn als Environment-Variablen. Im gatsby-config.js sieht das bewusst unspektakulär aus:

module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-aws-parameter-store-environment",
      options: {
        // Pull the whole subtree for this environment
        path: `/mikebild-dev/${process.env.DEPLOY_ENV}/`,
        withDecryption: true,
      },
    },
  ],
};

Die Hierarchie ist der eigentliche Gewinn, nicht der API-Aufruf. Ein Pfad wie /app/environment/KEY gibt der Konfiguration eine Struktur, die man per IAM-Policy schneiden kann: Der Produktions-Build darf /mikebild-dev/production/* lesen, sonst nichts. Werte vom Typ SecureString liegen mit KMS verschlüsselt, und jeder Zugriff hinterlässt in CloudTrail eine Spur. Aus „irgendwo liegt eine Datei" wird „ein benannter, versionierter, zugriffskontrollierter Wert mit Audit-Trail".

Architektur: Build-Zeit gegen Laufzeit

Bevor ich das Plugin einsetze, ziehe ich eine Linie. Sie ist die wichtigste Entscheidung des ganzen Themas, und das Plugin trifft sie nicht für mich.

flowchart LR
  PS["Parameter Store<br/>/app/env/*"] -->|build time| B["gatsby build"]
  B --> OUT["Static bundle<br/>HTML / JS / CSS"]
  OUT --> CDN["CDN / Browser"]
  RT["Runtime secrets<br/>never here"] -.->|not in bundle| OUT

Alles, was zur Build-Zeit in das statische Bundle wandert, ist öffentlich. Punkt. Ein API-Endpunkt, ein öffentlicher Content-Delivery-Key, ein Feature-Flag, eine Analytics-ID – das darf im ausgelieferten JavaScript stehen, weil es ohnehin für jeden Browser sichtbar ist. Gatsby macht das durch die GATSBY_-Präfixkonvention sogar explizit: Diese Variablen werden bewusst in den Client-Bundle eingebettet.

Alles, was ein echtes Geheimnis ist – ein schreibender Datenbank-Zugang, ein privater API-Key, ein Signaturschlüssel – gehört niemals in einen Build-Time-Wert für ein Frontend. Nicht, weil der Parameter Store es nicht schützen könnte, sondern weil das Ziel ein statisches, öffentlich ausgeliefertes Artefakt ist. Der sicherste Speicher der Welt hilft nichts, wenn ich den entschlüsselten Wert danach in eine Datei schreibe, die auf einem CDN landet.

Der Parameter Store verführt hier ein wenig, und das ist die harte Frage zu Secrets: Weil er SecureString und KMS anbietet, fühlt sich jeder Wert „sicher" an. Aber die Sicherheit endet an der Grenze des Builds. Meine Methodik ist deshalb simpel und unbestechlich – ich sortiere jeden Parameter vorab in genau eine von zwei Schubladen: „darf öffentlich sein" oder „ist ein Laufzeit-Geheimnis". Nur die erste Schublade darf das Plugin überhaupt anfassen. Echte Laufzeit-Secrets leben in einer Serverless-Funktion oder einem Backend, das sie zur Laufzeit selbst aus dem Parameter Store liest und nie an den Client gibt.

Der zweite Trade-off: Reproduzierbarkeit

Die zweite harte Frage ist subtiler und wird gern übersehen. In dem Moment, in dem der Build externe Konfiguration liest, ist er keine reine Funktion des Repositories mehr. Derselbe Git-Commit erzeugt nicht mehr zwangsläufig dasselbe Ergebnis – er erzeugt das Ergebnis, das zum externen Zustand des Parameter Store zum Build-Zeitpunkt passt.

Das ist ein realer Verlust. Ändert jemand einen Wert im Parameter Store und stößt kein Rebuild an, driften Deployment und deklarierte Konfiguration auseinander, ohne dass ein Commit das anzeigt. Ein Rollback auf einen alten Commit stellt womöglich nicht die alte Konfiguration wieder her, weil die Parameterwerte sich inzwischen verändert haben. „Funktioniert bei mir" bekommt eine neue, unangenehme Dimension: bei mir mit dem Parameter-Stand von gestern.

Der Parameter Store hat für dieses Problem ein Werkzeug, und ich halte es für den entscheidenden methodischen Hebel: Jeder Parameter ist versioniert. Ich kann eine konkrete Version referenzieren – /app/prod/KEY:4 – statt „den jeweils aktuellen Wert". Damit wird der Build wieder deterministischer: Ein Commit plus ein gepinnter Parameter-Stand ergibt ein reproduzierbares Artefakt. Der Preis ist eine bewusste Disziplin, denn jemand muss die Version anheben und damit die Konfigurationsänderung genauso behandeln wie eine Code-Änderung: sichtbar, nachvollziehbar, rollback-fähig. Genau das ist der Punkt, an dem Konfiguration aufhört, ein Nebenschauplatz zu sein, und Teil der Architektur wird.

Was das messbar besser macht

Der Aufwand lohnt sich, weil er an drei Stellen konkrete Wirkung zeigt, nicht nur Eleganz.

Kürzere Zyklen: Eine neue Umgebung ist ein neuer Pfad im Parameter Store, kein neues Ritual aus .env-Dateien, Copy-Paste und Nachfragen im Chat. Ein Deploy braucht keine manuelle Vorbereitung mehr, weil die Konfiguration dort liegt, wo der Build sie ohnehin abholt. Der Weg von „Wert ändern" zu „Wert ist live" verkürzt sich auf einen Parameter-Update plus einen Build.

Weniger Reibung: Der Zugriff ist über IAM an Umgebungen gebunden, nicht an Personen mit Dateizugriff. Ein Entwickler kann eine Staging-Konfiguration selbst setzen, ohne an ein produktives Geheimnis zu kommen. Self-Service innerhalb sauberer Grenzen ersetzt das Nadelöhr, an dem sonst eine einzelne Person sitzt, die „die .env kennt".

Höhere Qualität: Config-Drift wird strukturell unwahrscheinlicher, weil es nur noch eine Quelle gibt. Und wenn doch etwas schiefgeht, gibt es einen Audit-Trail. CloudTrail beantwortet die Frage „Wer hat wann welchen Wert geändert?" – die Frage, die bei einer Streuung aus Dateien und CI-Secrets prinzipiell unbeantwortbar bleibt.

Es gibt noch einen vierten Effekt, der leicht übersehen wird, aber im Alltag viel Reibung spart: Dieselbe Mechanik funktioniert lokal. Ein Entwickler, der die Staging-Werte lesen darf, startet seinen lokalen Build gegen exakt denselben Pfad im Parameter Store wie die Pipeline – kein manuelles Kopieren einer .env, kein veralteter lokaler Stand, keine Diskussion darüber, warum etwas lokal anders läuft als in Staging. Das schließt eine der ärgerlichsten Lücken im Entwicklungszyklus, nämlich die zwischen der Konfiguration auf dem Laptop und der Konfiguration im CI. „Works on my machine" verliert einen seiner häufigsten Gründe, weil die Maschine und die Pipeline aus derselben Quelle lesen. Wichtig bleibt auch hier die Grenze: lokal darf nur der öffentliche, build-taugliche Teilbaum sichtbar sein, niemals ein produktives Laufzeit-Geheimnis.

Worauf ich vor dem Einsatz prüfe

Eine Idee ist erst belastbar, wenn ihr Fehlerpfad beschrieben ist. Bevor ich das Plugin in eine Pipeline hänge, gehe ich ein paar Fragen durch, die im Demo-Modus alle unsichtbar bleiben und im Betrieb entscheiden.

Was passiert, wenn der Parameter Store beim Build nicht erreichbar ist oder ein erwarteter Wert fehlt? Ein Build, der stillschweigend mit einem leeren process.env.GATSBY_API_URL durchläuft, ist gefährlicher als einer, der laut abbricht – denn er produziert ein defektes, aber deploybares Artefakt. Ich will hier ein hartes Fail-Fast, kein undefined, das erst im Browser des Nutzers auffällt.

Ist der Zugriff eng genug geschnitten? Der Build-Runner sollte über eine IAM-Rolle genau den einen Umgebungspfad lesen dürfen und withDecryption nur für die Werte, die er wirklich braucht. Ein weit gefasster ssm:GetParametersByPath-Zugriff über die gesamte Hierarchie ist bequem und genau die Art von Bequemlichkeit, die ein Sicherheits-Review später teuer macht.

Und die entscheidende Frage: Liegt in dem Pfad, den der Build liest, versehentlich ein echtes Geheimnis? Das ist kein technischer, sondern ein disziplinärer Fehler – und der häufigste. Deshalb trenne ich die Hierarchie schon in der Benennung, sodass build-öffentliche Werte und Laufzeit-Secrets gar nicht erst im selben Teilbaum landen und ein zu weiter Zugriff nicht sofort ein Leak bedeutet.

Der typische Fehler

Der verführerischste Fehler bei diesem Thema ist, das Werkzeug mit der Lösung zu verwechseln. Der Parameter Store ist keine Konfigurationsarchitektur, so wenig wie ein verschlüsselter Speicher automatisch eine Secrets-Strategie ist. Er ist ein sehr guter Ort, um eine Grenze auszudrücken – die Grenze zwischen build-öffentlicher Konfiguration und Laufzeit-Geheimnis, zwischen Umgebungen, zwischen dem, was ein Build lesen darf, und dem, was er nie sehen sollte. Aber diese Grenze erzeuge ich, nicht der Service.

Genau darin liegt der eigentliche Wert dieses kleinen Plugins. Es zwingt mich, die Frage zu beantworten, die die .env-Streuung erfolgreich verdeckt hat: Was von der Konfiguration meines Frontends ist öffentlich, was ist geheim, und wer besitzt jeden dieser Werte über seinen Lebenszyklus hinweg? Sobald diese Antwort steht, ist der technische Rest – ein Pfad, ein Plugin, ein GetParametersByPath – fast trivial.

Schluss

Parameter Store im Build ist ein gutes Beispiel dafür, dass die interessante Entscheidung selten im Toolnamen steckt. Das Plugin holt Werte aus AWS – das ist die einfache Hälfte. Die tragende Hälfte ist die Methodik dahinter: Konfiguration explizit machen, sie an genau einer Stelle mit klaren Zugriffsgrenzen führen, die Trennung von öffentlich und geheim erzwingen und die Reproduzierbarkeit über gepinnte Versionen zurückholen.

Wer diese Linien zieht, gewinnt kürzere Deploy-Zyklen, weniger Reibung bei jeder Umgebung und weniger stille Fehler im Betrieb. Wer sie nicht zieht, hat am Ende einen sehr sicheren Speicher, aus dem trotzdem Geheimnisse in ein öffentliches Bundle sickern. Der Unterschied liegt nicht im Service, sondern in der Entscheidung, die er sichtbar macht.

Kommentare