Die Twelve-Factor App – nur etwas für die Cloud?

2011 wurde von den Entwicklern beim PaaS-Betreiber Heroku ein Regelwerk entwickelt, an das sich Applikationen halten sollen, wenn sie für den Betrieb in der Cloud optimal vorbereitet sein wollen: Die Twelve-Factor App Methode. Es handelt sich um 12 Richtlinien, die gesammelte Best Practices und Wissen aus jahrelangem Betrieb ihrer eigenen Plattform zusammenfassen. Als Ziele wurde sich gesteckt, die Kommunikation mit dem Betriebssystem zu standardisieren und die Cloud als Plattform nutzbar zu machen. Dadurch kann gleichzeitig auch ein ganzes Stück Skalierbarkeit erreicht werden. Diese Regeln zur Twelve-Factor App veröffentlichten sie unter der Adresse https://12factor.net.
Regeln, die von einem Cloud-Anbieter für Applikationen entworfen wurden, damit diese besonders gut auf Cloud-Plattformen funktionieren? Als Entwickler klassischer On-Premise-Lösungen braucht einen das ja nicht zu interessieren, oder? Dem kann man ganz gezielt widersprechen: Bei diesen Regeln haben sich erfahrene Entwickler zusammengesetzt und ihre geteilte Erfahrung verschriftlicht. Wir haben es hier mit destilliertem Wissen zu tun, welches sich agnostisch gegenüber eingesetzten Technologien und Hostinglösungen positioniert. Es wurde vielmehr Wert auf eine größere Flughöhe und allgemeine Struktur der Applikationsarchitektur und Herangehensweise bei der Softwareentwicklung gelegt, sodass dieses Regelwerk auch noch in weiteren zehn Jahren nichts an seiner Gültigkeit verlieren wird.

Was haben denn Monolithen damit zu tun?

Gestehen wir uns ein: die klassische On-Premise-Applikation ist ein Monolith, besteht aus einem Artefakt und wird auf einem, manchmal auch mehreren Servern ausgerollt und mittels Load Balancer oder Reverse Proxy mit dem Netzwerk verbunden. Daran ist auch nichts falsch, funktioniert diese Vorgehensweise doch schon seit Jahrzehnten hervorragend. Der Unterschied nur zur Cloud-Applikation ist, dass die Laufzeitumgebung eines Monolithen oft „handgestrickt“, also, oftmals manuell, genau so hergerichtet ist, dass die Applikation betrieben werden kann. Das kann dann von Serverkonfigurationen über extra eingerichtete Firewallkonfigurationen bis hin zu korrekt eingebunden Netzlaufwerken reichen. Diese exakte Konfiguration der Laufzeitumgebung wird von der Applikation eingefordert, sonst ist sie nicht lauffähig. Cloudanwendungen hingegen müssen da deutlich abgekapselter sein, kann es doch sein, dass sie morgen in einem komplett anderen Rechenzentrum ausgeführt werden, oder mehrfach nebeneinander gestartet werden. Zusätzlich werden sie oftmals aus mehreren unterschiedlichen Artefakten konstruiert, die untereinander verbunden sind und unabhängig voneinander ausgerollt werden. Doch konzentriert man sich auf das Wesentliche, werden Cloudanwendungen aus mehreren „Monolithen“ aufgebaut, sind grob gesehen also genau das Gleiche. Gelten somit die für Cloudanwendungen gedachten Faktoren auch uneingeschränkt für klassiche Applikationen? Schauen wir mal:

I: Codebase

One codebase tracked in revision control, many deploys

Twelve Factor App - Codebase

Die Vorzüge von Versionskontrollsystemen sind bekannt, sodass der Einsatz eben solcher niemanden mehr verschreckt. Entwicklerteams, verteilt oder nicht, können sich ihre Arbeit ohne Git, SVN oder Mercurial wahrscheinlich gar nicht vorstellen. Auch werden die meisten zustimmen können, dass aus ein und demselben Repository auf alle Umgebungen ausgerollt wird. Schließlich soll auf allen Umgebungen dieselbe Software laufen, warum nicht dieselbe Quelle nehmen?
In der Beschreibung dieses Faktors steht aber auch, dass eine Applikation aus genau einem Repository bestehen soll. Selbstgebaute Abhängigkeiten sollen also entweder so archiviert sein, dass sie durch den Dependency Manager automatisiert aufgelöst werden können, oder schlichtweg im selben Repository vorhanden sein.
Außerdem besagt dieser Faktor, dass derselbe Quelltext auf alle Umgebungen ausgerollt werden soll. Daraus folgt, dass für die Produktivumgebung dasselbe Artefakt benutzt werden soll, wie für alle anderen Umgebungen. Spätestens hier kann es zu Problemen in manch einem Projekt kommen. Doch wie diese behandelt werden können, wird in den folgenden Faktoren diskutiert.
Faktor 1, die Codebase, sollte also kein Problem für Monolithen darstellen.

II: Dependencies

Explicitly declare and isolate dependencies

Cloud-Applikationen werden mal auf dem einen, mal auf einem anderen Server ausgeführt. Dabei kann es auch zu Unterschieden im Betriebssystemen kommen. Daher sollen sie sich nicht auf irgendwelche vorinstallierten Bibliotheken oder anderweitige Begebenheiten des Betriebssystems verlassen. Stattdessen sollen Applikationen ihre Abhängigkeiten mithilfe eines „dependency declaration manifests“ deklarieren und sich mit einem „dependency isolation tool“ während der Ausführung von ihrer Umgebung isolieren, um nicht doch unbeabsichtigt Abhängigkeiten zu benutzen. Dies gilt auch für Betriebssystemwerkzeuge, beispielsweise ImageMagick oder curl. Sollten solche Werkzeuge zur Laufzeit benötigt werden, müssen sie mit der Applikation ausgeliefert werden.
Als Beispiele für solche Werkzeuge werden in der Ruby-Welt die Kombination aus `Gemfile` und Bundler, in der Python-Welt Pip und Virtualenv genannt. In der Java-Welt haben sich Werkzeuge wie Maven und Gradle durchgesetzt.
Die Vorteile, die die Befolgung dieses Faktors mitbringen, sollten auf der Hand liegen und sind genauso auf Monolithen anwendbar. Nicht vom konkreten Setup der zugrundeliegenden Maschine abhängig zu sein bedeutet einfacheres Onboarding neuer Kollegen in der Entwicklung, ein einfacheres Setup neuer Umgebung auf Kundenseite und ermöglicht überhaupt, auf anderen Betriebssystemen lauffähig zu sein. Gerade im Bereich der Webentwicklung kommt es häufig vor, dass Server Unix-basierte Systeme sind und die Entwickler auf Windows-Maschinen festsitzen.

III: Config

Store config in the environment

Twelve Factor App - Codebase

Die Konfiguration der Applikation besteht aus allem, was sich zwischen Deploys und Umgebungen ändern könnte. Dies beinhaltet unter Anderem Zugangsdaten zu Fremdsystemen, Zugriffsmöglichkeiten auf Drittdienste und Werte, die sich pro Deploy ändern. Um Standardwerte anzugeben, die sich unter Umständen nur auf bestimmten Umgebungen ändern müssen, können diese entweder mit in die Software einkompiliert werden, oder in Konfigurationsdateien angegeben und später über beispielsweise Umgebungsvariablen überschrieben werden.
Ein gutes Beispiel für solche Mechanismen kann der Konfigurationsmechanismus von Spring Boot Applikationen dienen. Im Kapitel „Externalized Configuration“ wird beschrieben, in welcher Reihenfolge Konfigurationswerte, die man üblicherweise in der `application.properties` oder `application.yml` anlegt, von außen überschrieben werden können.
Mithilfe dieses Mechanismus kann die Software so programmiert werden, dass alle Standardwerte, die man in der zentralen Konfigurationsdatei eingegeben hat, für die lokale Entwicklungsumgebung der Programmierer gültig sind. Sowie die Software dann ausgeliefert wird, kann über Umgebungsvariablen, Kommandozeilenparameter oder Java System Properties, um nur ein paar zu nennen, die Applikation für das jeweilige Einsatzgebiet passend konfiguriert werden.
Diese Vorgehensweise ist dabei vollkommen programmiersprachen- und betriebssystemunabhängig, bringt in Cloud-Applikationen kritische Vorteile, kann aber auch bei monolithischen Programmen sehr große Mehrwerte generieren.

IV: Backing services

Treat backing services as attached resources

Twelve Factor App - Codebase

Als Backing service bezeichnen die Autoren der twelve factor app alle Dienste, die eine Applikation über das Netzwerk konsumieren. Dabei unterscheiden sie zwischen Diensten, die dieselben Administratoren betreiben, die auch die Applikation betreuen, und Diensten, die von Dritten bereitgestellt werden. Als Beispiele können hier die applikationseigene Datenbank auf der einen Seite und Binärdatenspeicher wie Amazons S3 oder SMTP-Dienste auf der anderen Seite dienen.
Ein Softwareprojekt, das den zwölf Faktoren genügen möchte, behandelt diese Dienste, egal von wem sie betreut werden, als lose angebundene Ressource. Man soll dazu in der Lage sein eine lokal angelegte Datenbank, die auf einer benachbarten VM liegt, durch eine Datenbank eines Cloudanbieters austauschen zu können. Dies soll vor allem der dritte Faktor, Config, ermöglichen: sämtliche Zugangsdaten liegen in der Konfiguration vor und können von den Betreibern der Software ausgetauscht werden. Die Applikation kümmert sich lediglich um die technische Natur der Netzwerkverbindung und zieht die Konfigurationsdaten heran, um sich zum Service zu verbinden. Der Service wird nur noch als angeschlossene Ressource behandelt.
Dieser Faktor spielt im Cloudumfeld eine besondere Rolle, da hier viel schneller über Netzwerkgrenzen hinweg kommuniziert wird. Der Gedanke, dass ein zu konsumierender Dienst nur über eine nicht permanent verfügbare Schnittstelle erreichbar ist, die zusätzlich Latenzen in die Kommunikation einbringen kann, muss stets im Hinterkopf vorhanden sein.
Bei monolithisch aufgebauten Systemen ist dieser vorhergesagte Wandel im Betriebsumfeld nicht unbedingt gegeben – die Software muss schlicht nicht unbedingt das gleiche Maß an Flexibilität aufweisen. Nichtsdestotrotz kostet es keinen Mehraufwand,++ und ist als Best Practice angesehen, die Schnittstellen des Systems so aufzubauen, dass man sich lediglich auf die technisch garantierten Eigenschaften der Technologie verlässt, statt beispielsweise davon auszugehen, dass die Datenbank immer auf demselben Host liegt und somit Latenzen nahe null sind.

V: Build, release, run

Strictly separate build and run stages

Twelve Factor App - Codebase

Laut der 12 Factor App ist die Build Stage dafür zuständig, den Code eines Repositories in ein ausführbares Artefakt umzuwandeln, auch Build genannt. Die Release Stage kombiniert das Artefakt mit der Konfiguration eines Deployziels und erstellt damit ein Release. Die Run Stage schlussendlich führt das Release aus. Darüber hinaus wird definiert, dass ein Release eine einzigartige Bezeichnung besitzt, genannt Release ID. Ein Schema wird nicht vorgegeben, stattdessen werden Timestamps oder hochzählende Zahlen empfohlen.
Dieses Vorgehen ermöglicht, dass derselbe Stand eines Repositories auf allen Umgebungen eingesetzt wird und verdeutlicht nochmals die Wichtigkeit des dritten Faktors Konfiguration. Außerdem wird so bereits eine grobe Struktur einer Buildpipeline definiert und der Deployprozess angeschnitten. Spätestens hier kriegen wir einen ersten Geschmack davon, dass erfahrene Softwareentwickler an diesem Dokument gearbeitet haben. Jedes Kind kriegt einen Namen und eine klar umschnittene Aufgabe zugewiesen. Altbekannte Best Practices, die in sich bereits destilliertes Wissen darstellen, werden Prozessschritten zugeordnet und beweisen in ihrer Einfachheit Eleganz.
In manch einer Technologie oder bestehenden Legacysystemen wird sich die Funktion einer Release Stage unter Umständen nicht innerhalb kurzer Zeit umsetzen, doch auf lange Zeit wird sich eine solche Struktur positiv auf Faktoren wie die Stabilität der Releases auswirken – und das nicht nur im Cloudumfeld.

VI: Processes

Execute the app as one or more stateless processes

Twelve Factor App - Codebase

Die Applikation soll in der Run Stage als einer oder mehrere Prozesse ausgeführt werden. Im einfachsten Fall bedeutet dies, dass die Applikation ein simples Script ist, welches über die Kommandozeile ausgeführt wird.
Der wichtigste Aspekt jedoch ist, dass solche Prozesse zustandslos sind und keine Daten untereinander teilen.
Zustandslos zu sein bedeutet, dass Prozesse über mehrere Läufe hinweg immer wieder komplett terminieren und somit keine Daten „flüchtig“ im Arbeitsspeicher speichern. Sämtliche Daten, die zu persistieren sind, müssen in einem Backing Service, beispielsweise einer Datenbank, abgelegt werden.
Untereinander keine Daten zu teilen bedeutet, dass Prozesse voneinander unabhängig arbeiten können und davon ausgehen müssen, isoliert von anderen Prozessen ausgeführt zu werden. Die einzige Art, wie Prozesse Daten austauschen können, ist mit Backing Services zu kommunizieren.
Sind diese Kriterien erfüllt, hat man nicht nur eine klare Applikationsstruktur erreicht, sondern kann wichtige Verarbeitungsschritte, welche als Prozesse definiert sind, parallelisiert ausführen um horizontale Skalierbarkeit zu erreichen.

VII: Port binding

Export services via port binding

12 Factor Apps sind in sich abgeschlossene Applikationen, die selbstständig Ports öffnen um über das Netzwerk zu kommunizieren. Dies steht im starken Kontrast zum Gedanken, dass Applikationen innerhalb bereits installierter Container wie Apache-Modulen oder Java-Servlet-Containern ausgeführt werden.
Ein Vorteil dieser Vorgehensweise besteht darin, dass Serverbetreiber auf einer technisch sehr niedrigen Ebene die Applikation verwalten können, da sie auf Betriebssystemebene als Prozess auftaucht, der nicht von anderen Prozessen, wie extra gestarteten Webservern, abhängig ist. Dadurch werden viele Verwaltungsaufgaben, wie automatische Neustarts und Monitoring, einfacher umsetzbar.
Ein weiterer Vorteil besteht darin, dass durch die klare Abgrenzung und den Export der Applikationsdienste über Netzwerkschnittstellen jede 12 Factor App als Backing Service nach Faktor IV dienen kann. Somit ermöglichen sich neue Möglichkeiten der Vernetzung von unterschiedlichen Services.
Dies ist sicherlich einer der Punkte die man am ehesten aus der Perspektive eines Monolithen ausblenden kann, ist doch einer der Kernaspekte eines Monolithen, dass alle benötigten Fähigkeiten gebündelt vorliegen.
Es spricht jedoch nichts dagegen, auch einen Monolithen als eigenständige Applikation ohne umgebenden Webserver zu betreiben.

VIII: Concurrency

Scale out via the process model

Twelve Factor App - Codebase

Dieser Faktor verstärkt nochmals Faktor VI. Prozesse sollen in der Architektur der Applikation als Objekte erster Klasse angesehen werden. Sie bilden das primäre Bauteil der Anwendungsstruktur. Es wird vorgeschlagen, Prozesse nach Typ zu gruppieren, beispielsweise Web-Prozesse die HTTP-Anfragen verarbeiten oder Hintergrundprozesse die regelmäßige Aufgaben abarbeiten. Solche Prozesse können selber Kindprozesse starten, können so aber nur eine beschränkte Menge skalieren.
Der größte Vorteil liegt sicherlich darin, dass über dieses Prozessmodell horizontale Skalierbarkeit auf Applikationsebene erreicht werden kann. Wurden die vorgeschriebenen Bedingungen eingehalten, kann die Parallelität ohne Probleme erhöht werden, um mehr Aufgaben abzuarbeiten.
Im Cloudumfeld bedeutet dies meistens, dass mehr virtuelle Maschinen herangezogen werden oder mehr Container im Cluster hochgefahren werden. Beim Monolithen kann dies bedeuten, dass eine Konfiguration angepasst wird, sodass beispielsweise mehr Hintergrundprozesse gestartet werden. Hier ist die Skalierbarkeit über mehrere Maschinen vielleicht nicht unbedingt gegeben, jedoch kann dann einfacher mit einem Upgrade der Hardware gearbeitet werden.

IX: Disposability

Maximize robustness with fast startup and graceful shutdown

Twelve Factor App - Codebase

Prozesse, die den oben genannten Bedingungen genügen und Teil einer 12 Factor App sind, können nach Belieben gestartet und gestoppt werden, damit eine schnelle Skalierbarkeit und Anpassbarkeit, beispielsweise durch geänderte Konfiguration, erreicht werden kann. Außerdem sollen sie schnell, innerhalb weniger Sekunden, hoch- und wieder runterfahren können und das Prozessmodell soll einen abrupten Prozessstop, beispielsweise wenn der Host abstürzt, handhaben können.
Diese Liste an Anforderungen ist ein klares Eingeständnis an die Bedingungen im Cloudumfeld. Während ein schneller Applikationsstart auch einem Monolithen sehr gut zu Gesicht steht und Fehler in der unterliegenden Hardware des Hosts oder das sprichwörtliche Ziehen am Stromstecker im besten Fall nie zum katastrophalen Prozessschaden führen sollte, sind die Bedingungen im On-Premise-Bereich meistens als stabiler anzusehen.
Nichtsdestotrotz zeigt dieser Punkt einige, wenn auch selbstverständliche, erstrebenswerte Ziele auf.

X: Dev/prod parity

Keep development, staging, and production as similar as possbile

Die Autoren beschreiben, dass aus historischen Gründen zwischen den Umgebungen Lücken bestehen können. So kann es eine lange Zeit dauern, bis eine neue Version produktiv geht, unterschiedliche Abteilungen sind für die Entwicklung und den Deploy zuständig, und die eingesetzten Werkzeuge und Betriebsmittel unterscheiden sich zwischen Entwicklung und der produktiven Umgebung. Eine 12 Factor App strebt an via Continuous Deployment ausgerollt zu werden und somit diese Lücken zu schließen. Die zeitliche Lücke wird durch Automatisierung geschlossen, Entwickler werden beim Ausrollen der Applikationen mindestens mit eingebunden und Technologien werden nicht pro Umgebung eingesetzt, sondern im gesamten Projekt.
Dieser Faktor beschreibt eher modernes Denken und Best Practices, als ein Vorgehen, dass speziell für Microservices oder Cloudapplikationen reserviert ist. Die Denkrichtung DevOps ist klar erkennbar und wenn DevOps eins vorschreibt, dann, dass diese Philosophie technologieunabhängig ist.

XI: Logs

Treat logs as event streams

12 Factor Apps kümmern sich nicht darum, wie ihre Logs gespeichert oder verarbeitet werden. Stattdessen sollen sie schlicht ihr Output nach `stdout` senden, sodass die Umgebung sie verarbeiten kann.
Dieses Vorgehen vereinfacht für alle Beteiligten die Handhabung von Logs und sollte die erste Wahl für jede Form von Programm sein.

XII: Admin processes

Run admin/management tasks as one-off processes

Twelve Factor App - Codebase

Auch im bestgetesteten Projekt das die strengsten Qualitätsprozesse besitzt, ist es manchmal für Entwickler und Betreiber hilfreich, über mitgelieferte Tools der Applikation ins laufende System einzugreifen. Die Autoren empfehlen auch für diese administrativen Prozesse auf das Prozessmodell und alle weiteren Faktoren zu achten. So ist es zum Beispiel empfehlenswert für administrative Prozesse das Buildtool der jeweiligen Technologie zu nutzen, in Ruby beispielsweise rake oder rails, um die gleichen Vorteile in Hinblick auf Dependencies und Isolation zu erhalten, wie bei der Applikation selbst.
Als Alternative zum Buildtool werden explizit REPLs genannt. Sprachen wie Python oder Clojure unterstützen diese Art Technologie schon seit langer Zeit, Java auch schon seit einigen Versionen. Die Fähgikeiten unterscheiden sich aber relativ stark und die Nutzung ist sicherlich nicht für jede Person zugänglich.

Abschließende Gedanken

Nach Betrachtung aller Faktoren stellt sich ein Bild dar, welches auf der einen Seite klar für Microservices und Cloudumgebungen gestaltet wurde. Aber schon beim zweiten Nachdenken wird klar, dass hier Entwickler am Werk waren, die schon einiges gesehen haben und den ein oder anderen Fehler selber begangen haben. Dass der Adressat für die 12 Factor Apps nicht der klassische Monolith ist, ist dem Zeitgeist und dem Trend der technologischen Entwicklung geschuldet.
Wir haben hier also ein Dokument vorliegen, welches Best Practices sammelt und auf zwölf Ratschläge kondensiert. Bei einigen wird man sicherlich denken, dass es sich um Selbstverständlichkeiten handelt, doch ist es hilfreich die Ausformulierungen zu haben und bei Bedarf nachlesen zu können.
Befolgt man die Ratschläge die einem gegeben werden, wird man eine Applikation erhalten, die auf der Makroebene mit klaren Strukturen glänzen kann und einen simplen Ansatz im Hosting verfolgt. Das Gegenteil von Simplizität ist Komplexität und diese wurde bereits 1968 bei einer Konferenz identifiziert, die von einem Komitee in Garmisch abgehalten wurde.

Josha von Gizycki
Software Development
Ständiges Dazulernen und der Austausch mit Gleichgesinnten sind seiner Meinung nach zwei der wichtigsten Bausteine zur stetigen Verbesserung.