In modernen Softwareentwicklungsprojekten ist der Einsatz von Continuous Integration (CI) und Continuous Deployment (CD) unverzichtbar. Um jedoch sicherzustellen, dass Änderungen an der Software stabil und von hoher Qualität sind, ist es oft erforderlich, dass Codeänderungen vor der Bereitstellung in der Produktionsumgebung überprüft werden. CircleCI bietet eine effiziente Möglichkeit, solche Prozesse durch sogenannte "gated deployments" zu steuern, bei denen ein Entscheidungsträger die Bereitstellung genehmigen muss, bevor sie auf die Produktion angewendet wird. Ein solcher Prozess lässt sich über Workflows und "Approval Jobs" in CircleCI realisieren.

Im folgenden Abschnitt wird erklärt, wie man diesen Prozess mit CircleCI umsetzt und gleichzeitig Code-Coverage-Reports integriert, um die Qualität des Codes zu überwachen. Außerdem wird gezeigt, wie man einen automatisierten Code-Überprüfungsprozess mit der Coveralls-Plattform einrichtet.

Zunächst einmal lässt sich in CircleCI ein Workflow definieren, um zu kontrollieren, wann und wie die einzelnen Jobs ausgeführt werden. Dies ist besonders wichtig in Umgebungen, in denen die Produktion nur nach expliziter Genehmigung durch eine verantwortliche Person erreicht werden soll. Ein einfaches Beispiel für eine solche Workflow-Konfiguration ist die folgende:

yaml
.circleci/config.yml
workflows: version: 2 build-and-deploy: jobs: - build - hold: type: approval requires: - build - deploy: requires: - hold

In diesem Beispiel wird zunächst der "build"-Job ausgeführt. Danach folgt der "hold"-Job, der vom Typ „Approval“ ist. Dieser Job erfordert, dass der „build“-Job erfolgreich abgeschlossen wurde, bevor die Pipeline angehalten wird. Wenn ein Entscheidungsträger den „hold“-Job genehmigt, wird der „deploy“-Job ausgeführt und die Änderungen in die Produktionsumgebung bereitgestellt. Dies stellt sicher, dass keine unkontrollierten Änderungen an die Produktion gelangen.

Für komplexere Workflows kann man den Build- und Testprozess in separate Jobs unterteilen, die parallel ausgeführt werden. So könnte eine erweiterte Konfiguration aussehen:

yaml
.circleci/config.yml workflows: version: 2 build-test-and-approval-deploy: jobs: - build - test - hold: type: approval requires: - build - test filters: branches: only: main - deploy: requires: - hold

In dieser erweiterten Konfiguration werden die „build“- und „test“-Jobs parallel ausgeführt. Sobald der Code in den Hauptbranch (main) gemergt wird, stoppt die Pipeline und wartet auf die Genehmigung für den „deploy“-Job. Dies stellt sicher, dass nur der Code, der bereits in den Hauptbranch gemergt wurde, zur Produktion gelangt, was dem GitHub-Flow-Modell entspricht.

Neben der Bereitstellung und Genehmigung von Deployments ist es ebenfalls entscheidend, die Qualität des Codes ständig zu überwachen. Eine der gängigsten Methoden zur Überwachung der Codequalität ist die Code Coverage. Dies gibt Aufschluss darüber, welche Teile des Codes durch Tests abgedeckt sind und wo noch Lücken bestehen. Für Angular-Projekte lässt sich ein Code Coverage-Report wie folgt erzeugen:

bash
$ npx ng test --watch=false --code-coverage

Der resultierende Bericht wird in einem Ordner namens „coverage“ gespeichert, und über einen HTTP-Server kann dieser Bericht im Browser angezeigt werden:

bash
$ npx http-server -c-1 -o -p 9875 ./coverage

Ein solches Report-Tool ermöglicht es, die Codeabdeckung auf verschiedenen Ebenen (Ordner-, Dateiformat-, Zeilenebene) zu betrachten und zu analysieren, wie gut der Code durch Tests abgesichert ist. Besonders nützlich ist es, die Zeilen des Codes zu identifizieren, die nicht getestet werden, um gezielt Tests zu ergänzen. Ein solches Tool fördert die TDD-Mentalität (Test-Driven Development) und verbessert langfristig die Stabilität des Codes.

Die Integration der Code Coverage in den CI/CD-Workflow ist ebenfalls von großer Bedeutung. Es empfiehlt sich, die Code Coverage-Berichte direkt im CI-Prozess zu integrieren, sodass jedes Mal, wenn Tests ausgeführt werden, auch die Codeabdeckung überprüft wird. Dies kann helfen, Pull Requests zu verhindern, die den Code Coverage-Prozentsatz verschlechtern, und stellt sicher, dass nur qualitativ hochwertiger Code in den Hauptbranch gemergt wird.

Ein Beispiel, wie dies in CircleCI umgesetzt wird, sieht folgendermaßen aus:

yaml
.circleci/config.yml
version: 2.1 orbs: coveralls: coveralls/coveralls@2 jobs: build_and_test: ... - run: npm test -- --watch=false --code-coverage - store_test_results: path: ./test_results - store_artifacts: path: ./coverage - coveralls/upload

Nachdem der Code Coverage-Report generiert und gespeichert wurde, kann der „coveralls/upload“-Befehl verwendet werden, um die Ergebnisse bei Coveralls hochzuladen. Dadurch wird sichergestellt, dass der Code Coverage-Stand direkt in den Pull Requests angezeigt wird, was eine zusätzliche Schicht der Qualitätssicherung darstellt.

Die Integration von Code Coverage in den CI/CD-Prozess hat weitreichende Vorteile: Sie fördert nicht nur die Qualität des Codes, sondern sorgt auch dafür, dass neue Funktionen immer gut getestet sind, bevor sie in die Produktion gelangen. Über Tools wie Coveralls können spezifische Coverage-Schwellenwerte gesetzt werden, die sicherstellen, dass die Codeabdeckung immer einem bestimmten Standard entspricht.

Es ist auch wichtig zu verstehen, dass Code Coverage allein keine Garantie für gute Softwarequalität ist. Sie hilft zwar, ungetestete Codeabschnitte zu identifizieren, ersetzt jedoch nicht das kritische Denken und die sorgfältige Planung von Tests. Gute Tests sollten auf den tatsächlichen Anforderungen der Software basieren und nicht nur darauf abzielen, die Codeabdeckung zu maximieren.

Wie Docker die Bereitstellung von Webanwendungen vereinfacht und sicherer macht

Docker ist ein Open-Source-Tool zur Entwicklung, Bereitstellung und Ausführung von Anwendungen, das eine Reihe von Vorteilen gegenüber traditionellen Virtualisierungsansätzen bietet. Während virtuelle Maschinen (VMs) oft mehrere Gigabyte an Speicher und Festplattenspeicher benötigen, um eine vollständige virtuelle Instanz eines Betriebssystems zu betreiben, kommen Docker-Container mit wesentlich geringeren Anforderungen aus – in der Regel im Bereich von Megabytes. Diese Container bieten zudem eine isolierte Umgebung, die es ermöglicht, Anwendungen in einer konsistenten Art und Weise zu entwickeln, zu testen und bereitzustellen, ohne sich um die Variablen und Probleme der zugrunde liegenden Systemkonfiguration kümmern zu müssen.

Die wichtigste Eigenschaft von Docker ist seine Fähigkeit zur Containerisierung von Anwendungen. Ein Container ist ein leichtgewichtiger, in sich geschlossener Arbeitsbereich, der alles enthält, was benötigt wird, um eine Anwendung auszuführen – vom Code über Bibliotheken bis hin zu Abhängigkeiten. Diese Art der Virtualisierung ist besonders vorteilhaft, da sie eine schnelle Bereitstellung und die Möglichkeit zur Wiederverwendbarkeit von Komponenten auf verschiedenen Systemen ermöglicht. Docker abstrahiert die Betriebssystemkonfiguration, sodass jede Anwendung auf jedem Hostsystem mit dem gleichen Verhalten läuft, unabhängig davon, wo sie ausgeführt wird.

Ein Docker-Container wird durch die sogenannte Dockerfile beschrieben – eine Textdatei, die die Anweisungen enthält, wie der Container erstellt werden soll. Eine typische Dockerfile umfasst mehrere grundlegende Anweisungen:

  • FROM: Hiermit wird das Basismodell des Containers definiert. In den meisten Fällen beginnt der Docker-Container mit einem minimalen Linux-Image, das nur die nötigsten Betriebssystembestandteile enthält.

  • SETUP: In diesem Schritt werden alle notwendigen Softwareabhängigkeiten konfiguriert. Oft wird hierfür ein weiteres Docker-Image verwendet, das alle grundlegenden Programme und Bibliotheken enthält, die für die Ausführung der Anwendung erforderlich sind.

  • COPY: Mit dieser Anweisung wird der eigentliche Anwendungscode von der Entwicklungsmaschine in den Container übertragen. Diese Kopie kann dabei aus einem lokalen Verzeichnis oder einer entfernten Quelle stammen.

  • CMD: Hier wird festgelegt, welche Befehle beim Starten des Containers ausgeführt werden. Dies könnte beispielsweise der Start eines Webservers wie nginx sein, der den kopierten Anwendungscode bereitstellt.

Ein typisches Beispiel für eine Dockerfile könnte folgendermaßen aussehen:

dockerfile
FROM duluca/minimal-nginx-web-server:1-alpine
COPY /dist/local-weather-app /var/www CMD 'nginx'

In diesem Fall basiert der Container auf einem minimalen Nginx-Image, das von Docker Hub bereitgestellt wird. Der Anwendungscode wird in das /var/www-Verzeichnis des Containers kopiert, und der Webserver wird mit dem Befehl nginx gestartet, um die Anwendung bereitzustellen. Solch ein Setup stellt sicher, dass die Anwendung in einer sicheren, isolierten Umgebung läuft, was nicht nur die Konsistenz, sondern auch die Sicherheit erhöht.

Docker-Container bieten mehrere Sicherheitsvorteile. Die hierarchische Struktur von Docker-Images sorgt dafür, dass jede Schicht des Containers zusätzliche Sicherheitsvorkehrungen trifft. Vom Basisbetriebssystem über die Webserver-Konfiguration bis hin zum Anwendungscode – alle Teile des Containers sind voneinander isoliert, was es für Angreifer schwieriger macht, den Host-Server zu kompromittieren. Die Möglichkeit, Images von vertrauenswürdigen Quellen wie Docker Hub zu verwenden und regelmäßig zu aktualisieren, trägt ebenfalls dazu bei, Sicherheitslücken zu vermeiden.

Das Prinzip der "Containerisierung" sorgt dafür, dass die Softwareumgebung von der zugrunde liegenden Hardware unabhängig ist. So können Entwickler sicherstellen, dass ihre Anwendung unter identischen Bedingungen auf verschiedenen Maschinen läuft – sei es auf einer lokalen Entwicklerumgebung oder in einer Produktionsumgebung in der Cloud. Dies minimiert die typischen Probleme, die bei der Bereitstellung von Software auftreten, wie etwa die unterschiedlichen Versionen von Softwarepaketen oder Betriebssystemen.

Ein weiterer entscheidender Vorteil von Docker ist die Effizienz in der Verwaltung und Bereitstellung von Anwendungen. Docker-Images sind in der Regel deutlich kleiner als vollständige virtuelle Maschinen, was bedeutet, dass sie schneller geladen und über das Netzwerk verteilt werden können. Zudem kann Docker als Bestandteil eines größeren Continuous Integration (CI) und Continuous Deployment (CD) Workflows verwendet werden, was den gesamten Bereitstellungsprozess noch weiter automatisiert und beschleunigt. Docker-Container bieten somit nicht nur eine kostengünstige Möglichkeit zur Verwaltung von Entwicklungsumgebungen, sondern auch eine schnelle, sichere und skalierbare Lösung für den Produktionsbetrieb.

Die Wahl des richtigen Docker-Images ist von entscheidender Bedeutung für die Effektivität des Workflows. Viele vorgefertigte Docker-Images bieten bereits optimierte Setups, die für die meisten gängigen Anwendungen geeignet sind. Die Basisimages wie alpine oder nginx sind dabei besonders beliebt, da sie klein und gut dokumentiert sind. Es ist jedoch ratsam, vor der Verwendung eines Docker-Images dessen Herkunft und Inhalte zu prüfen, um sicherzustellen, dass keine unnötigen Sicherheitsrisiken mit eingeführt werden.

Die Docker-Community spielt dabei eine wichtige Rolle, da regelmäßig neue, verbesserte Versionen von Images veröffentlicht werden, die Fehlerbehebungen und Sicherheitsupdates beinhalten. Für den Entwickler bedeutet dies, dass er nicht ständig nach den neuesten Versionen suchen muss, sondern sicher sein kann, dass durch die Wahl von "evergreen" Basis-Images wie lts-alpine oder 1-alpine die aktuellsten Patches automatisch integriert werden.

Es ist ebenfalls wichtig, regelmäßig zu testen, ob eine neue Version eines Docker-Images zu unerwarteten Fehlern führt, bevor es in die Produktion überführt wird. Eine konsequente Wartung der Docker-Images und das Testen nach jeder neuen Version ist notwendig, um die Sicherheit und Stabilität der Anwendungen zu gewährleisten.