Auf dem Weg von Continuous Integration zu Continuous Delivery

Nicht jedes Projekt startet auf der grünen Wiese und kann Continuous Delivery nach Lehrbuch umsetzen. Daher ist es wichtig, die Ideen hinter den Prinzipien von Continuous Delivery zu verstehen, um selbst beantworten zu können, mit welchen Fragen sich Unternehmen bei der Einführung von Continuous Delivery beschäftigen müssen. In diesem Artikel werden einzelne Aspekte beleuchtet, die für den Weg von Continuous Integration zu Continuous Delivery besonders wichtig sind. Dazu gehören Fragen wie: Mit welchen Themen sollten sich Unternehmen in diesem Kontext befassen und welche technischen Möglichkeiten stehen für eine konkrete Umsetzung zur Verfügung?
Der Einsatz eines Continuous Integration-Systems ist heute in den meisten Unternehmen zum Standard geworden. Nach Änderungen am Source Code erhält der Entwickler mit der Ausführung von automatisierten Tests durch das Continuous Integration-System und dem Feedback der Testergebnisse ein Mindestmaß an Sicherheit. Das Vorgehen ist dabei immer gleich (Abb.1).
Nach einem Commit in das Source Control-System wird automatisch ein neuer Build angestoßen. Daraufhin werden die automatisierten Tests ausgeführt und am Ende des Builds erhält der Entwickler einen Report über die Testergebnisse. Das Ergebnis der Tests fließt dann wiederum in die Entwicklung mit ein. Continuous Integration bietet damit ein schnelles Feedback bei Source Code-Änderungen. Wenn man das Prinzip „Integrate early and often“ anwendet und mindestens einmal am Tag seine Source Code-Änderungen eincheckt, kann man mit diesem Vorgehen die Softwarequalität nachhaltig verbessern.
Abgrenzung von Continuous Integration zu Continuous Delivery
Der Continuous Integration-Prozess ist nach den Änderungen am Source Code und der Ausführung der Tests abgeschlossen und beginnt daraufhin wieder von vorn. Continuous Delivery setzt an dieser Stelle an und erweitert den Feedback-Zyklus bis in die Produktion. Erst wenn die Anwendung in die Produktion deployt wurde und dem Kunden zur Verfügung steht, ist die "Definition of Done" erfüllt. Vor diesem Hintergrund wird Continuous Delivery auch als finale Stage oder "letzte Meile" von Continuous Integration bezeichnet.
Ziele von Continuous Delivery
Das originäre Ziel von Continuous Delivery ist bereits im agilen Manifest begründet. Dort heißt es im ersten Prinzip: "Unsere höchste Priorität ist es, den Kunden durch frühe und kontinuierliche Auslieferung wertvoller Software zufriedenzustellen." [1]. Continuous Delivery adressiert dieses Ziel, indem es agile Entwicklungspraktiken bis in die Produktion fortführt.
Continuous Delivery ist dabei im Grunde nicht mehr als eine Sammlung von Techniken, Prozessen und Werkzeugen, die den Prozess der Softwareauslieferung verbessern. Der Schwerpunkt liegt auf den Werkzeugen und dem Auslieferungsprozess. Der Auslieferungsprozess lässt sich wiederum unter zeitlichen und qualitativen Aspekten differenzieren.
Beim zeitlichen Aspekt versucht man den Auslieferungsprozess zu beschleunigen, um die Time-To-Market zu reduzieren [2]. Besonders das Management und zunehmend auch die Fachbereiche wollen häufiger neue Produkte oder Änderungen am Markt platzieren.
Neben dem zeitlichen Aspekt spielt aber ebenso der qualitative Aspekt eine bedeutende Rolle. Bei einem eher konservativen Release-Zyklus von mehreren Wochen oder Monaten kommt es im Auslieferungsprozess meist zu Problemen und Fehlern. Das liegt daran, dass viele manuelle Schritte von einzelnen Personen getätigt werden müssen und sich die notwendigen Tätigkeiten zwischen den einzelnen Auslieferungszeitpunkten ändern können. Continuous Delivery versucht hier, einen wiederholbaren und zuverlässigen Prozess zu etablieren – überwiegend durch die Automatisierung der einzelnen manuellen Schritte.
Prinzipien und Praktiken von Continuous Delivery
Jez Humble und David Farley gehen erstmals in ihrem Buch "Continuous Delivery" [3] auf Prinzipien und Praktiken im Kontext von Continuous Delivery ein. Unter den Prinzipien und Praktiken verstehen sie im Wesentlichen konkrete Leitsätze oder Empfehlungen, an die man sich bei der Anwendung von Continuous Delivery halten sollte. Der Artikel von James Betteley "8 Principles of Continuous Delivery" [4] bietet einen guten Überblick über die wichtigsten Prinzipien. Ergänzt man diese Prinzipien mit ein paar weiteren und gruppiert sie inhaltlich, so bilden sich die Themenschwerpunkte, die in Abb.2 dargestellt sind.
Die Zuordnung zu den Themen ist nicht immer eindeutig und trennscharf, sodass an dieser Stelle auch kein Anspruch auf Vollständigkeit erhoben wird. Die Gruppierung gibt einen Überblick darüber, mit welchen Themen sich Unternehmen beschäftigen müssen, wenn es um die Einführung von Continuous Delivery geht.
Themenschwerpunkte im Umfeld von Continuous Delivery
Diese Themenschwerpunkte spielen für die Prinzipien des Continuous Delivery eine wesentliche Rolle:
Automatisierung
Das mit Sicherheit wichtigste Thema im Kontext von Continuous Delivery ist Automatisierung. Ein wiederholbarer und zuverlässiger Release- und Deployment-Prozess kann nur erreicht werden, wenn viele manuelle Arbeitsschritte automatisiert werden. Manuelle Schritte, die evtl. nur in einer Checkliste dokumentiert sind, werden mit Hilfe von Tools und Shell-Skripten so automatisiert, dass letztendlich nur ein "Button-Klick" notwendig ist, um ein Deployment der Anwendung auf das Zielsystem auszulösen.
Um keine bösen Überraschungen beim Deployment in unterschiedliche Umgebungen zu erhalten, ist es wichtig, alle Umgebungen möglichst gleich zu halten. Mittlerweile gibt es gute Tools, um die Umgebung selbst automatisiert einzurichten (z. B. Ansible, Chef, Puppet, Saltstack) [5]. Im Vergleich zu der Verwendung einfacher Shell-Skripte haben diese Tools den Vorteil, dass die Ausführung der Installationsanweisungen idempotent vorgenommen wird. Auf meist deklarative Weise wird der Zielzustand definiert (z. B. "Paket X muss installiert sein und der Service Y muss sich im Zustand "gestartet" befinden") und das Tool findet dann heraus, welche Aktionen auf dem System durchgeführt werden müssen, um den Zielzustand zu erreichen. Diese Vorgehensweise ist auch als "Infrastructure as Code" bekannt, da sich die Definition der Installationsanweisungen der herkömmlicher Programmierung ähnelt.
Deployment-Pipeline
Der "Klebstoff" der einzelnen Automatisierungsschritte wird durch die sogenannte Deployment-Pipeline abgebildet. Alle Schritte, die von einem einzelnen Commit bis zum Deployment der Anwendung in Produktion notwendig sind, werden durch die Pipeline sichergestellt. Abb.3 zeigt die schematische Darstellung einer Deployment-Pipeline.
Den Übergang von einer Stage zu nächsten stellt das sogenannte "Quality Gate" dar. Sobald es zu einem Fehler in der vorangegangenen Stage gekommen ist, wird an dieser Stelle der gesamte Pipeline Build abgebrochen (Continuous Delivery-Praktik: "If anything fails, stop the line"). Damit wird sichergestellt, dass keine fehlerhaften Artefakte in die Produktion gelangen. Und nicht nur das: Mit jedem Durchlaufen der weiteren Stages erreicht das Auslieferungsartefakt auch ein immer höheres Qualitätsniveau.
Die Reihenfolge der Testausführung orientiert sich an der Testpyramide von Martin Fowler [6]. Auch hier ist das Ziel, auf ein möglichst frühzeitiges Feedback im Fehlerfall zurückzugreifen. Entsprechend ihrer Ausführungsgeschwindigkeit werden in der Commit-Stage Unit- und Integrationstests ausgeführt. In den weiteren Stages folgen Tests von längerer Dauer (z. B. Akzeptanz-, Kapazitäts- und Lasttests). Die Pipeline ermöglicht dabei parallele Vorgänge, die die Gesamtdurchlaufzeit verkürzen.
Durch die Pipeline wird vor allem der gesamte Release- und Deployment-Prozess transparent veranschaulicht. Mit der Automatisierung und Verkettung der einzelnen Schritte ist man in der Lage, die Anwendung wesentlich häufiger in die Produktion auszuliefern. Abgesehen von der Tatsache, dass die Änderungen dem Kunden früher zur Verfügung stehen, reduziert dieses Vorgehen durch die häufigeren Auslieferungen die Anzahl der Änderungen je Deployment. Damit sinkt auch das Risiko potenzieller Fehler pro Auslieferung. Und wenn es doch mal zu einem Fehler gekommen ist, kann leicht ein Bugfix-Release nachgeschoben werden (Roll-Forward) und es muss nicht der Versuch unternommen werden, die alte Version wieder lauffähig zu bekommen (Roll-Backward).
Für die Realisierung einer Deployment Pipeline bietet sich der weit verbreitete Continuous Integration-Server Jenkins [7] an – mit entsprechenden Plugins – oder aber der mittlerweile ebenfalls als Open Source veröffentlichte Continuous Delivery-Server GO von Thoughtworks [8]. Letzterer bietet deutlich mehr Abstraktionsebenen für die Definition der einzelnen Arbeitsschritte und ist besonders für komplexere Pipelines eine gute Wahl. Kommerzielle Anbieter haben ebenfalls Continuous Delivery-Funktionen in ihre CI-Systeme integriert (z. B. Bamboo [9] oder Teamcity [10]).
Konfigurationsmanagement
Das Konfigurationsmanagement beschäftigt sich mit der Frage, welche Änderungen an einer Anwendung vorgenommen werden müssen, um die Entwicklung möglichst einfach in eine Continuous Delivery-Umgebung integrieren zu können.
Zum einen sollte sichergestellt werden, dass nur ein Build-Artefakt für alle Umgebungen (DEV, TEST, PROD) erstellt wird. Die jeweils aktive Umgebung wird dann "von außen" beim Start der Anwendung mitgegeben, z. B. als JVM-Parameter. Die Anwendung weiß dann, welche umgebungsspezifischen Einstellungen sie laden soll. Im Spring-Framework wird dies beispielsweise über sogenannte Spring-Profile gelöst [11].
Um den Transport auf das Zielsystem zu vereinfachen, sollte die Anwendung möglichst wenige Anforderungen an ihre Umgebung stellen. Das kann erreicht werden, indem sie ihre Umgebung selbst mitbringt und mitsamt ihrer Laufzeitumgebung ausgerollt wird. Beispielsweise wird bei einer Webanwendung mit Spring Boot die gesamte Anwendung inklusive ihrer Abhängigkeiten und einem Servlet-Container in eine JAR-Datei verpackt (Fat-Jar oder Uber-Jar). Der Transport auf das Zielsystem ist dann nichts weiter als das Kopieren einer Datei. Noch einfacher wird es, wenn man diese Datei in einen Docker-Container verpackt. In der Deployment-Pipeline wird das Image erstellt und in ein Repository (Docker-Registry) hochgeladen. Es kann dann vom Zielsystem wieder heruntergeladen und gestartet werden. Auf dem Zielsystem muss lediglich Docker installiert sein. Alle Entscheidungen über weitere Vorbedingungen (z. B. Java-Version, Betriebssystem-Patches etc.) werden bereits im Docker-Image getroffen. Alternativ ist aber auch die Verwendung von betriebssystemnahen Package-Managern denkbar, welche die Anwendung z. B. in ein RPM- oder DEB-Paket verpacken. Da sich Betriebsmitarbeiter mit diesen Tools meist gut auskennen, können Reibungsverluste bei der Inbetriebnahme vermieden werden.
Jeder Commit führt im besten Fall dazu, dass am Ende der Deployment-Pipeline ein auslieferungsfähiges Artefakt zur Verfügung steht. Für die Versionierung der Software braucht das Artefakt eine feste Versionsnummer. Denn nur mit einer festen Versionsnummer kann der Bezug zum jeweiligen Softwarestand hergestellt werden. Üblicherweise wird die Pipeline-Build-Nummer als Teil der Versionsnummer mit aufgenommen und in der Source Code-Verwaltung getaggt. So kann im Falle eines Fehlers in Produktion ein Rückbezug hergestellt werden.
Damit am Ende einer Deployment-Pipeline ein lauffähiger Softwarestand entsteht, muss die Anwendung jederzeit in einem auslieferungsfähigen Zustand sein. Bei einer ständigen Weiterentwicklung der Anwendung ist das gar nicht so einfach. Unfertige Features sollen keine Seiteneffekte auf den Rest des Systems verursachen. Abhilfe können sogenannte Feature Toggles bzw. Feature Flags schaffen, die es ermöglichen, diese Teile selektiv ein- oder auszuschalten. Damit bleibt die Anwendung in einem deploybaren Zustand, unfertige Features werden deaktiviert. Ein weiterer positiver Nebeneffekt bei der Nutzung von Feature Toggles ist, dass neue Features auf Wunsch zunächst nur für einen begrenzten Anwenderbereich zur Verfügung gestellt werden. Ein Beispiel für ein toolgestütztes Handling dieser Flags aus dem Java-Bereich ist togglz [12].
Änderungen an der Anwendung können auch Änderungen in der verwendeten Datenbank bzw. im Datenbankschema nach sich ziehen. Beim Deployment der Anwendung muss geprüft werden, ob sich die Datenbank im gewünschten Zielzustand befindet. Auch hier können Tools wie Flyway, Liquibase oder Orcas helfen, eine Schemamigration automatisch durchzuführen [13]. Etwas einfacher wird es mit der Verwendung einer dokumentenorientierten Datenbank, die ohne ein festes Datenschema auskommt (z. B. MongoDB etc.).
Self-Service-Operations
In einer Continuous Delivery-Umgebung ist das Release und Deployment nur "einen Button weit" entfernt. Doch wo befindet sich dieser Button? Hier kommen Self-Service-Operations-Systeme ins Spiel, die es ermöglichen, diese automatisierten Schritte unter einer Oberfläche zu vereinen. Typische Features sind die Ausführung von manuell oder automatisch ausgelösten Tasks (lokal oder remote), einem Rechtemanagement und einer Historie der ausgeführten Aktivitäten. Die Funktionen sind meist über ein Dashboard vereint und ermöglichen dem Benutzer schnell festzustellen, wer welche Version der Software wann deployt hat. Neben dem Release- und Deploymentmanagement lassen sich aber auch Aufgaben wie das Neustarten eines Applikationsservers oder die Integration von Smoke-Tests realisieren. Beispiele für diese Art von Tools sind Rundeck oder Ansible Tower [14].
Wer kein separates Tool einsetzen will, kann die meisten dieser Aufgaben auch in der Deployment-Pipeline erledigen (z. B. durch den manuellen Trigger einer Pipeline Stage).
Log-Management und Monitoring
Das Feedback über die laufende Anwendung in Produktion ist nicht nur für den Betriebsmitarbeiter von Interesse, sondern liefert auch für die Entwicklung wichtige Informationen. Dafür ist es notwendig, dass Log-Informationen über ein geeignetes Portal jedem Entwickler zur Verfügung stehen. Durch die zunehmende Virtualisierung und "Containerisierung" und die damit verbundene horizontale Skalierung entstehen immer mehr Log-Informationen auf unterschiedlichen Servern, für die es einen zentralen Zugang und eine zentrale Haltung geben muss. Bekannte Vertreter dieser Systemgattung sind der sogenannte ELK-Stack (Elasticsearch, Logstash, Kibana) [15] oder Graylog [16]. Mit diesen wird es möglich, Produktiv-Logs nicht nur zu überwachen, sondern auch zu durchsuchen (Indexierung) und unterschiedliche Arten von Log-Informationen (z. B. auch von Routern, Loadbalancern, Syslog usw.) miteinander in Beziehung zu setzen. Die gewonnenen Informationen können in einem Dashboard aggregiert dargestellt werden oder auch automatische Benachrichtigungen auslösen, wenn z. B. eine bestimmte Menge an Fehlern in einem definierten Zeitraum aufgetreten sind. Für eine einfache Verarbeitung der Log-Informationen ist es allerdings wichtig, dass diese in einem einheitlichen Format an das Log-Management-System gesendet werden. Das Graylog Extended Log Format (GELF) ist ein Beispiel für solch ein vereinheitlichtes Format. Das im Java-Bereich häufig eingesetzte Logging-Framework Log4J [17] bietet ab Version 2 einen fertigen GELF-Appender, um Log-Events in diesem Format zu versenden.
DevOps
Im Vergleich zu Continuous Delivery versteht man unter DevOps eher eine Philosophie oder Kultur im Unternehmen. Als Kunstwort aus Development und Operations wird hier das von Konflikten behaftete Verhältnis dieser beiden Abteilungen beleuchtet. Wenn man sich die Zielsetzung von Development und Operations näher anschaut, erscheint es nicht verwunderlich, dass es zwischen beiden Abteilungen zu Konflikten kommen kann. Schließlich muss sich die Entwicklung darum kümmern, möglichst viele Anforderungen und damit auch Änderungen umzusetzen, wohingegen der Betrieb vorrangig die Stabilität des Systems zum Ziel hat.
Genau diese eher konträre Zielsetzung zeigt, dass bei der Softwareentwicklung nicht die Interessen einer einzelnen Abteilung im Vordergrund stehen dürfen, sondern dass der Fokus auf das gesamte System die Ziele definiert. Jeder, der am Auslieferungsprozess direkt oder indirekt beteiligt ist, ist gleichermaßen für eine erfolgreiche Produktivsetzung verantwortlich. Damit bezieht DevOps auch andere Abteilungen wie QA, Fachbereiche etc. mit ein. Im Kontext von Microservices werden Querschnitt-Teams gebildet, die für einen Themenkomplex komplett zuständig sind. Nach dem Prinzip "You build it, you run it" [18] wird die Gesamtverantwortung auf ein Team verlagert und damit Reibungsverluste an Abteilungsgrenzen vermieden.
Ein hilfreicher Schritt in Richtung interdisziplinärer Teams kann die Beteiligung eines Betriebsmitarbeiters in der Entwicklung sein (oder umgekehrt). Das vereinfacht Abstimmung und Koordination für anstehende Produktsetzungen aufgrund der räumlichen Nähe zum Entwicklerteam.
Cloud und Virtualisierung
Das Thema Cloud und Virtualisierung ist nicht notwendigerweise eine Voraussetzung für Continuous Delivery. Dennoch vereinfacht es die Bereitstellung der Anwendung enorm, da Test- oder Produktivumgebungen in kürzester Zeit "on demand" in der Cloud zur Verfügung gestellt werden können. Zudem kann je nach Situation eine einfache und dynamische Skalierung vorgenommen werden. Diese Flexibilität macht es möglich, dass die Software nicht an ihre Umgebung angepasst werden muss (im Sinne von "Anwendung A muss auf Server B in Applikations-Server C lauffähig sein"), sondern, dass eine Umgebung speziell für die jeweilige Anwendung erstellt wird. Diese Denkweise gewinnt bei der Containerisierung (aka Docker) zunehmend an Bedeutung und eignet sich besonders auch im Umfeld einer Microservices-Architektur.
Ein anderer Anwendungsfall ist die Bereitstellung eines Build Agents für die Deployment Pipeline in einem Docker Container. Damit werden die vorhandenen Systemressourcen auf einem Server deutlich effizienter ausgenutzt und die Parallelisierung der Build-Ausführung erhöht.
Die Toolauswahl in diesem Bereich ist sehr groß und wächst stetig. Für das Management von Docker Containern (Starten, Stoppen, Orchestrierung, Überwachung etc.) bietet sich die gehostete Lösung Docker Cloud (ehemals Tutum) an [19]. Wer eine solche Cloud-Umgebung im eigenen Unternehmen betreiben möchte, der sollte einen Blick auf Rancher werfen [20]. Die Ausführung der Container erfolgt anschließend auf physikalischen oder virtuellen Nodes, die entweder angemietete Cloud-Instanzen oder eigene Server sein können. Bekannte Vertreter für diese Aufgaben sind z. B. Amazon Web Services (AWS), Microsoft Azure oder Digital Ocean [21].
Fazit
Continuous Delivery ist der nächste logische Schritt, um als Fortführung von Continuous Integration den Prozess der Softwareauslieferung bis in die Produktion zu unterstützen. Die Prinzipien und Praktiken des Continuous Delivery betreffen wichtige Themenfelder, die auf dem Weg von Continuous Integration nach Continuous Delivery beachtet werden müssen. Der Schwerpunkt liegt dabei auf der Automatisierung von manuellen Ausführungsschritten.
Für die meisten Unternehmen wird sicherlich nicht die Häufigkeit der Softwareauslieferung das alleinige Zielkriterium sein, sondern vielmehr die zuverlässige und fehlerfreie Auslieferung. Unter diesem Aspekt wird Continuous Delivery für jedes Unternehmen wichtig, dass Softwareentwicklung betreibt. Leider gibt es nicht das eine und einzige Tool, das dabei unterstützt. Die Vielzahl der Möglichkeiten zeigt aber, dass sich viel in diesem Kontext bewegt und dass der Weg dorthin zunehmend einfacher wird.
- Agiles Manifest
- Wikipedia: Time to Market
- J. Humble, D. Farley, 2010: Continuous Delivery: Reliable Software Releases Through Build, Addison-Wesley
- J. Betteley: 8 Principles of Continuous Delivery
- Ansible, Chef, Puppet, Saltstack,
- Martin Fowler: Testpyramide
- Jenkins
- Go CD Server
- Bamboo
- TeamCity
- Spring Profiles
- Togglz
- Flyway, Liquibase, Orcas
- Rundeck, Ansible Tower
- ELK-Stack
- Graylog
- Log4J
- A Conversation with Werner Vogels
- Docker Cloud
- Rancher
- Amazon AWS, Microsoft Azure, Ocean Cloud