# Lab 05: Containerization In diesem Lab lernen Sie die Tool-Chain kennen, um Applikationen als standardisierte Deployment Units (Container-Images) bereitstellen zu können. Dies funktioniert sowohl lokal auf einem Entwicklungsrechner (z.B. Ihrem Laptop) als auch im Rahmen von Deployment Pipelines (d.h. in Remote Building Environments wie bspw. Gitlab CI/CD). Die Standardisierung von Deployment Units in Form von Containern ist ein zentrales Merkmal der Entwicklung Cloud-nativer Anwendungen. Die hier vorgestellten Prinzipien können Sie daher problemlos auf weitere Cloud-native Projekte übertragen. ## Inhalt - [Lab 05: Containerization](#lab-05-containerization) - [Inhalt](#inhalt) - [Vorbereitung](#vorbereitung) - [Übung 01: Erstellung von einem Image](#übung-01-erstellung-von-einem-image) - [Aufgabe 01.1 HTTP-Service mittels eines NGINX-Basisimages](#aufgabe-011-http-service-mittels-eines-nginx-basisimages) - [Aufgabe 01.2: HTTP-Service mittels eines generellen Basis-Images bauen](#aufgabe-012-http-service-mittels-eines-generellen-basis-images-bauen) - [Übung 02: Image-Shrinking](#übung-02-image-shrinking) - [Aufgabe 02.1: Unnötige Dateien löschen](#aufgabe-021-unnötige-dateien-löschen) - [Aufgabe 02.2: Image-Layer einsparen](#aufgabe-022-image-layer-einsparen) - [Aufgabe 02.3: Kleinere Basis-Images nutzen](#aufgabe-023-kleinere-basis-images-nutzen) - [Übung 03: Images in Registries bereitstellen](#übung-03-images-in-registries-bereitstellen) - [Übung 04: Deployment-Pipeline zum Bau und Test eines Images](#übung-04-deployment-pipeline-zum-bau-und-test-eines-images) - [Links](#links) - [Was sollten Sie mitnehmen](#was-sollten-sie-mitnehmen) ## Vorbereitung - [Installieren](https://docs.docker.com/engine/install/) Sie gem. den verlinkten Anweisungen Docker für Ihr System: - [Mac](https://www.docker.com/products/docker-desktop) - [Windows](https://www.docker.com/products/docker-desktop) - [Linux](https://docs.docker.com/engine/install/) - Prüfen Sie in Ihrer Konsole, ob die Installation erfolgreich war: ``` docker --version ``` Sie sollten eine Ausgabe mit der Versionsnummer und build id bekommen, z.B.: ``` Docker version 19.03.8, build afacb8b ``` - Klonen Sie sich bitte ferner als Vorbereitung dieses Repository mittels: ``` git clone https://git.mylab.th-luebeck.de/cloud-native/lab-containerization.git cd lab-containerization ``` ## Übung 01: Erstellung von einem Image Sie werden in diesem Teil sehen, wie man Images baut. Häufig benötigt man Images für spezfische Dienste, wie bspw. Datenbanken, Webserver, usw. Hierfür bieten die Hersteller meist vorkonfigurierte Images an, die man nur noch mit einer kleinen Konfiguration (z.B. Access Credentials, Dateipfade, etc.) auf die spezifischen Bedürfnisse anpassen muss. Es lohnt sich hierzu öffentliche Image Repositories wie [Dockerhub](https://hub.docker.com) zu durchsuchen. Man kann aber auch Images auf Basis einer Standard Linux-Distribution aufsetzen. Sie werden beides am Beispiel eines kleinen Webservers sehen. ### Aufgabe 01.1 HTTP-Service mittels eines NGINX-Basisimages 1. Öffnen Sie nun bitte die Datei `Dockerfile.nginx` (diese ist recht übersichtlich, versuchen Sie diese zu verstehen und nachzuvollziehen) 2. Erzeugen Sie aus dieser nun das für Docker erforderliche Dockerfile mittels `cp Dockerfile.nginx Dockerfile`. 3. Bauen Sie nun ein Container-Image mittels `docker build -t web:nginx .` (vergessen Sie nicht die Punkt am Ende, der gibt das Current Directory an und ist wichtig!). 4. Prüfen Sie mittels `docker image list web*`, ob Ihr Image gebaut wurde. Sie sollten eine Ausgabe wie folgt (o. ähnl.) erhalten. ``` REPOSITORY TAG IMAGE ID CREATED SIZE web nginx ca0547d1d208 seconds ago 133MB ``` 5. Starten Sie dieses Image nun bitte mittels `docker run -p 8080:80 web:nginx` (Sie binden dadurch Ihren lokalen Port 8080 an den Port 80 des Containers). 6. Prüfen Sie, ob Ihre Website ausgeliefert wird, in dem Sie [http://localhost:8080](http://localhost:8080) aufrufen. Sie sollten dann folgende Webpage sehen. ![screenshot](index.html.png) 7. Beenden Sie den HTTP-Service in dem Sie in Ihrer Konsole CTRL-C drücken (Sie senden damit das SIGTERM Signal an den NGINX Server-Prozess und der Container wird terminiert). Das war ja erstaunlich einfach. ### Aufgabe 01.2: HTTP-Service mittels eines generellen Basis-Images bauen In Fällen von weit verbreiteten Diensten und Produkten wie NGINX, Apache, Redis, Memcached, CouchDB, MySQL, usw. finden sich häufig solche Basisimages der entsprechenden Projekte. Diese kann man meist sehr schnell und unkompliziert aufsetzen und nutzen. Sie verlieren aber häufig auch etwas Kontrolle und Konfigurationsmöglichkeiten. In Fällen spezifischerer Produkte oder selbst geschriebener Software sind Sie sogar ggf. gezwungen selber ein Image zu erstellen und die entsprechende Software darauf zu installieren. Man geht in diesen Fällen üblicherweise von einem Distributions Basis-Image aus (wie bspw. dem Ubuntu 18.04 LTS Image). Wir wollen nun denselben Service mit einem anderen Image bauen, um diesen generelleren Ansatz zu demonstrieren. 1. `cp Dockerfile.ubuntu Dockerfile` 2. Öffen Sie das `Dockerfile` in einem Editor und versuchen Sie es zu verstehen. Sie sehen, es ist etwas länger, als das vorherige Image, aber Sie sollten die Wirkungsweise mittels der Kommentare gut nachvollziehen können. 3. Bauen Sie nun das Image zu diesem Ubuntu-basierten `Dockerfile` mittels `docker build -t web:ubuntu .`. 4. Prüfen Sie wieder mittels `docker image list web*`, ob Ihr Image gebaut wurde. Sie sollten eine Ausgabe wie folgt (o. ähnl.) erhalten. ``` REPOSITORY TAG IMAGE ID CREATED SIZE web ubuntu 8032bb56c82d seconds ago 154MB web nginx ca0547d1d208 minutes ago 133MB ``` 5. Starten Sie dieses Image nun bitte mittels `docker run -p 8080:80 web:ubuntu`. 6. Prüfen Sie, ob Ihre Website ausgeliefert wird, in dem Sie [http://localhost:8080](http://localhost:8080) aufrufen. Sie sollten wieder dieselbe Webpage sehen, wie bereits im vorherigen Teil. ## Übung 02: Image-Shrinking Sie haben in Übung 02 einen einfachen Webserver samt Inhalt als Docker-Image erzeugt. - Einmal haben Sie auf dem von NGINX selbst bereitgestellten Image nur Ihren Inhalt (`web`-Verzeichnis) hinzugefügt. - Im zweiten Fall haben Sie auf Basis eines Ubuntu 18.04 LTS Basis Images auch den Webserver NGINX installiert, den Inhalt hinzugefügt und den Entrypoint für den Container konfiguriert. Das war zwar etwas aufwändiger, Sie hatten aber dadurch letztlich mehr Kontrolle über die Konfiguration. Sie sehen also, dass bereitgestellte Community-Images häufig komfortabler sind. Doch wie sieht es mit dem Größenbedarf der resultierenden Images aus? Geben Sie dazu bitte folgendes in Ihrer Shell ein. ``` docker image list web* ``` Dies sollte folgende (o. ähnl.) Ausgabe erzeugen: ``` REPOSITORY TAG IMAGE ID CREATED SIZE web ubuntu 8032bb56c82d 57 minutes ago 154MB web nginx ca0547d1d208 14 hours ago 133MB ``` Sie sehen, Ihr selbst gebautes Ubuntu Image ist etwas größer. Das ist nicht wirklich erstaunlich, denn die NGINX Macher wissen vermutlich besser als Sie und ich, wie man eine NGINX-Basisinstallation effizient konfiguriert. Doch Images lassen sich auch "shrinken". Sie sollten sich dazu ins Bewusstsein rufen, dass Container Overlay-Dateisysteme nutzen, und jede Anweisungszeile eines Dockerfiles ein Image-Layer (mit Platzbedarf) erzeugt. Man kann sich das zu Nutze machen und Images auf mehrere Arten "kleiner" machen: 1. Indem man für den Betrieb unnötige Dateien nicht in den Layer mit aufnimmt. 2. Indem man unnötige Image-Layer einspart. 3. Und indem man kleinere Basis-Images nutzt. Die folgenden Aufgaben dienen dazu, Ihnen zu zeigen, was für Effekte diese drei Möglichkeiten auf die resultierenden Imagegrößen haben. ### Aufgabe 02.1: Unnötige Dateien löschen Wir gehen von unserem `web:ubuntu` Image aus. 1. `cp Dockerfile.ubuntu Dockerfile` 2. Löschen Sie nun unnötige Package Manager Dateien mittels `apt-get clean` und `rm -rf /var/lib/apt/lists/*` in dem Sie die im Dockerfile dafür vorgesehenen Zeilen einkommentieren. 3. Bauen Sie nun ein neues Package mittels `docker build -t web:ubuntu-cleaned .` 4. Lassen Sie sich nun die Größen aller drei Packages mittels `docker image list web*` anzeigen. Sie sehen, dass dadurch Ihr Image bereits etwas kleiner geworden ist, aber noch größer als das NGINX-Image ist. ### Aufgabe 02.2: Image-Layer einsparen In Linux/UNIX Shells kann man, um zwei Kommandos nacheinander auszuführen, entweder dies ``` cat /file/exists echo "success" ``` oder dies ``` cat /file/exists && echo "success" ``` schreiben. "Success" wird im zweiten Fall sogar nur dann ausgegeben, wenn das erste Kommando `cat /file/exists` mit einem Exit-Code von 0 (also erfolgreich) beendet werden konnte. Das entspricht exakt der Ausführungsweise sequentiell aufeinander folgender `RUN`-Anweisungen. Mittels des `&&` Operators lassen sich also mehrere Shell-Kommandos in einer `RUN`-Anweisung eines Dockerfiles unterbringen. Anstatt mehrere `RUN`-Anweisungen (also mehrere Image-Layers) benötigt man so nur eine `RUN`-Anweisung (und erzeugt nur einen Image-Layer). Dieses __RUN-Chaining__ genannte Prinzip werden wir anwenden und sehen welchen Effekt dies auf Image-Größen haben kann. 1. `cp Dockerfile.ubuntu Dockerfile` 2. Kommentieren Sie nun alle `RUN`-Anweisungen im Dockerfile aus. 3. Kommentieren Sie nun bitte die einzelne `RUN`-Anweisung unter dem `Image Shrinking Effect`-Kommentar ein. 4. Bauen Sie nun ein neues Package mittels `docker build -t web:ubuntu-shrinked .` 5. Lassen Sie sich nun die Größen aller vier Packages mittels `docker image list web*` anzeigen. Sie sollten etwa folgende (o. ähnl.) Ausgabe erhalten: ``` REPOSITORY TAG IMAGE ID CREATED SIZE web ubuntu-shrinked f5f33804dcf2 38 minutes ago 117MB web ubuntu-cleaned 4f345b6cac34 48 minutes ago 147MB web ubuntu 8032bb56c82d 57 minutes ago 154MB web nginx ca0547d1d208 14 hours ago 133MB ``` ### Aufgabe 02.3: Kleinere Basis-Images nutzen Alle Images haben bislang Größen, die deutlich die 100 MB sprengen. Das ist zwar kleiner als viele VM-Images, aber immer noch recht groß, um ein paar HTML-Seiten von wenigen KBs auszuliefern. Aus diesem Grund gibt es Linux-Distributionen (Basis-Images), die deutlich kleiner sind, als die Standard-Distributionen. Diese beinhalten nur das absolut Wesentliche (quasi die POSIX-Schnittstelle) eines Linux-Betriebssystems. Hier gibt es mehrere Distributionen, die häufig verwendet werden. Bspw.: - [Alpine Linux](https://alpinelinux.org) - [Atomic](https://www.projectatomic.io) - [Busybox](https://busybox.net) - [RancherOS](https://rancher.com/rancher-os) - [Photon](https://vmware.github.io/photon) Wir werden die Übung 02 nun mit dem Alpine Linux Basis-Image wiederholen und sehen, wie sich diese Distribution auf die Image-Größen auswirkt. 1. `cp Dockerfile.alpine Dockerfile` 2. `docker build -t web:alpline .` 3. Kommentieren Sie dann folgende Zeile im Dockerfile ein: `RUN rm -rf /var/cache/apk/*` 4. `docker build -t web:alpine-cleaned .` 5. Kommentieren Sie nun alle `RUN` Commands im Dockerfile aus. 6. Kommenteren Sie dann das `RUN`-Chaining im Dockerfile ein. 7. `docker build -t web:alpine-shrinked .` 8. Lassen Sie sich nun mittels `docker image list web*` alle Image-Größen im Überblick anzeigen. Sie sollten nun folgende (o. ähnl.) Ausgabe erhalten: ``` REPOSITORY TAG IMAGE ID CREATED SIZE web alpine-shrinked 647de3450afe 2 seconds ago ??.??MB web alpine-cleaned 9a30127f52b1 About a minute ago ??.??MB web alpine b9f1decf5a22 2 minutes ago ??.??MB web ubuntu-shrinked f5f33804dcf2 38 minutes ago 117MB web ubuntu-cleaned 4f345b6cac34 48 minutes ago 147MB web ubuntu 8032bb56c82d 57 minutes ago 154MB web nginx ca0547d1d208 14 hours ago 133MB ``` __Beantworten Sie nun die Frage, um wieviel Prozent der Image-Wechsel das resultierende Container-Image reduziert hat?__ Sie können auch gerne prüfen, dass die Images alle laufen, indem Sie diese jeweils mit den folgenden Kommandos starten ``` docker run -p 8080:80 web:alpine docker run -p 8080:80 web:alpine-cleaned docker run -p 8080:80 web:alpine-shrinked ``` und danach [http://localhost:8080](http://localhost:8080) aufrufen. Der erbrachte Dienst ist in allen Fällen derselbe. ## Übung 03: Images in Registries bereitstellen Bislang haben Sie Images lokal auf Ihrem Host erzeugt. Das ist für lokale Entwicklungszwecke meist vollkommen ausreichend. Aber Sie haben auch bereits gesehen, dass Images in Public oder Private Registries bereitgestellt werden können. Die bereits in diesem Lab von Ihnen genutzten NGINX, Ubuntu und Alpine Images sind bspw. aus dem öffentlichen [Dockerhub](https://hub.docker.com) bezogen worden (ohne das Sie dies vermutlich recht zur Kenntnis genommen haben). In der normalen Entwicklung stellt man Images normalerweise in Privaten Registries zur Verfügung, damit nicht alle Zugriff auf diese haben. Hierzu können Image Registries private gehostet werden. Bspw. kann dies mit dem [THL Gitlab Service](https://git.mylab.th-luebeck.de) in jedem Projekt gemacht werden. Dies wollen wir nun demonstrieren: __Hinweis:__ Dieser Teil kann auch mit dem Public [Gitlab.com](https://gitlab.com) Service analog nachvollzogen werden, falls Sie sich dort registriert haben. 1. Loggen Sie sich hierzu in den [THL Gitlab Service](https://git.mylab.th-luebeck.de) ein. 2. Legen Sie in der Weboberfläche mittels "Neues Projekt" ein Test Repository namens `image-test` mit der Visibility `private` an. 3. Klicken Sie in diesem Repository auf "Packages & Registries -> Container Registry". Sie sollten dann eine (noch) leere Registry sehen. ![Registry](registry.png) 4. Ihnen sind ferner ein paar Kommandos vorgegeben, um sich mit der Registry von Docker aus zu verbinden (diese lassen sich aus der Web-Oberfläche kopieren). 5. Loggen Sie sich mittels Ihrer Konsole hierzu in die Registry ein (geben Sie Ihre Zugangsdaten ein). 6. Arbeiten Sie nun mit dem NGINX-Image `cp Dockerfile.nginx Dockerfile`. 7. Bauen Sie nun dieses Image für Ihre Registry mittels `docker build -t git.mylab.th-luebeck.de:4181/./image-test/web:nginx .`. 8. Pushen Sie das Image anschließend in diese Registry mittels `docker push git.mylab.th-luebeck.de:4181/./image-test/web:nginx`. Ersetzen Sie dabei `.` mit Ihrem Namen (oder kopieren Sie diesen aus der GitLab-Web-Oberfläche). Der Upload kann ggf. etwas länger dauern. 9. Navigieren Sie erneut zu Ihrer Registry. Sie sollten Ihr Image dort nun sehen. ![Registry-filled](registry-filled.png) 10. Mittels `docker pull git.mylab.th-luebeck.de:4181/./image-test/web:nginx` können Sie es von der Registry herunterladen. 11. Unterschiedliche Versionen von Images können Sie mit Tags kennzeichnen. ``` cp Dockerfile.alpine Dockerfile docker build -t git.mylab.th-luebeck.de:4181/./image-test/web:alpine . docker push git.mylab.th-luebeck.de:4181/./image-test/web:alpine ``` würde das Alpine-basierte Image erzeugen. ![registry-tag](registry-tags.png) Dieses könnten Sie mittels `docker pull git.mylab.th-luebeck.de:4181/./image-test/web:alpine` auf Ihren lokalen Host herunterladen. Gebräuchlich ist dies, um unterschiedliche Versionen von Images mittels Versionsnummern wie `image:0.1.0` zu kennzeichnen. Mittels dieser Tool-Chain lassen sich also Images in Registries zentral bereitstellen und für Deployments nutzen. ## Übung 04: Deployment-Pipeline zum Bau und Test eines Images Dieser Teil soll Ihnen zeigen, wie Sie mittels einer Deployment Pipeline Images automatisiert bauen können. Gitlab Deployment Pipelines haben Sie ja bereits in einem vorherigen Lab kennengelernt. 1. Arbeiten Sie hierzu mit dem NGINX-Image: `cp Dockerfile.nginx Dockerfile` 2. Öffnen Sie die `.gitlab-ci.yml` Datei in einem Editor und versuchen Sie diese nachzuvollziehen. - Dort sind zwei Stages `test` und `build` mit jeweils einem Job definiert. - In der `test`-Stage wird Ihr Container Image gebaut, gestartet und anschließend mittels `curl` abgerufen. Die herunter geladene Datei wird mit der `web/index.html` verglichen. Sind diese identisch ist der Container richtig konfiguriert. - In der `build`-Stage wird das Container-Image für die Registry gebaut, entsprechend getagged und in die Registry gepushed. Gitlab CI blendet hierfür über mehrere Umgebungsvariablen wie `$CI_REGISTRY_USER`, `$CI_REGISTRY_PASSWORD` (Access Credentials), `$CI_REGISTRY` (URL der Registry) und `$CI_REGISTRY_NAME` (Repository) die erforderlichen Daten für Docker ein. 3. Kopieren Sie hierzu - den `web`-Ordner samt Inhalt, - das `Dockerfile`, - und die `.gitlab-ci.yml` in Ihr `image-test` Repository (z.B. mittels der WebIDE in Gitlab). 4. Committen Sie dann diese Änderungen (z.B. mittels der WebIDE in Gitlab). Dadurch wird die Pipeline automatisch angestoßen. Öffnen Sie dann in GitLab über __CI/CD -> Pipelines__ die Pipeline und verfolgen Sie deren Fortschritt. Die Pipeline sollte erfolgreich durchlaufen. 5. Öffnen Sie dann in GitLab über __Packages & Registries -> Container Registry__ die Registry Ihres Projekts. Dort sollten Sie nun auch einen `automatic` Tag sehen. Dieses Image wird nun immer dann aktualisiert, wenn Sie einen neuen Commit in das Repo pushen und die `test`-Stage erfolgreich bestanden wird. Auf diese Art und Weise können Sie nun auch automatisiert im Rahmen von Deployment Pipelines Images bauen und bereitstellen. ## Links - [Docker Playground](https://labs.play-with-docker.com/) (Online Lab, Registration erforderlich) - [Docker Get Started](https://docs.docker.com/get-started/) - [Docker Tutorials and Community Trainings](https://www.docker.com/play-with-docker) - [Best Practices for Writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) - [Building Docker images with GitLab CI/CD](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html) ## Was sollten Sie mitnehmen 1. Mittels der den meisten Distributionen beiliegenden Package Managern ist es über `RUN`-Commands möglich in Basis-Images beliebige Software komfortabel und "non-interactive" zu installieren. 1. Eigener Code, Content oder Konfigurationen können mittels `ADD`-Commands dem Container-Image hinzugefügt werden (und danach mittels `RUN`-Commands auch kompiliert und installiert werden). 2. Sie können Server-Dienste mittels TCP-Ports nach außen `EXPOSE`n. 3. Über `ENTRYPOINT` können Sie hierzu den Prozess starten, der durch den Container bereitgestellt werden soll (vermeiden Sie dabei Prozesse als Daemons zu starten). 4. Image-Größen lassen sich mittels **RUN-Chaining** und der Wahl kleiner Basis-Images signifikant reduzieren. 5. Mittels `docker login` kann man sich auf entfernten Image-Registries einloggen um dann mittels `docker push` und `docker pull` Images auf privaten Registries hochladen bzw. herunterladen zu können. 6. Mittels GitLab Deployment Pipelines können Sie Images automatisiert testen und erstellen.