Von 0 auf Kubernetes - Aufbau einer Kubernetes-CI/CD-Infrastruktur per GitLab-Pipeline - Teil 2 - Docker Services

titelbild-rancher-docker

Ansible, Terraform, GitLab und etwas Magie: Wie ich eine ganze Infrastruktur auf Knopfdruck erstelle.

In dieser Serie möchte ich euch zeigen, wie ihr automatisiert ein Kubernetes-Cluster aufbaut, ein GitLab mit GitLab-Runner installiert und eine eigene Docker-Registry zur Verfügung stellt. Das Ziel wird sein, dass ihr eure Anwendungen direkt aus dem GitLab heraus in eurem Cluster bauen und starten könnt.

Im zweiten Teil dieser Serie zeige ich euch, wie wir mit Docker ein GitLab, eine Docker-Registry und Traefik als Reverse Proxy und LoadBalancer erstellen können. 

Rückblick

Im ersten Teil haben wir eine Ansible-Rolle geschrieben, die unsere Zielsysteme auf ihre bevorstehenden Aufgaben vorbereitet. In dieser Rolle konfigurieren wir "apt" und installieren Docker. Letzteres nutzen wir nun, um einige Services starten zu können.

Unser Ziel für diesen Teil

Unserer Zielsetzung folgend müssen wir ein neues GitLab und eine eigene Docker-Registry zur Verfügung stellen. Für das GitLab werden wir zusätzlich noch eine Datenbank benötigen. Damit wir unsere Services auch erreichen können, brauchen wir zudem noch einen Reverse Proxy. Diese Aufgabe wird Traefik erledigen und zusätzlich auch als LoadBalancer für das noch zu installierende Kubernetes-Cluster dienen. Diese vier Services wollen wir in diesem Teil innerhalb einer docker-compose.yml definieren.

Die docker-compose.yml

Zuerst erstellen wir uns eine docker-compose.yml unter folgendem Pfad: ansible-setup/roles/gitlab/docker-compose.yml. Der aufmerksame Leser wird anhand der Ordnerstruktur schon vermuten, dass diese docker-compose.yml später Teil einer neuen Ansible-Rolle namens gitlab sein wird. Der Name der Rolle ist hier natürlich wieder frei austauschbar.

Im Folgenden werde ich die einzelnen Services der docker-compose.yml beschreiben und diese von oben nach unten mit euch durchgehen.

Traefik

version: '3'
services:
  traefik: 
    container_name: traefik
    restart: unless-stopped
    networks:
        - traefik_net 
    image: traefik:v2.3.5 
    ports:
        - "443:443" 
        - "80:80" 
    volumes:
        - /conf:/conf 
        - /var/run/docker.sock:/var/run/docker.sock 
    command: 
        - "--api.insecure=true" 
        - "--providers.docker=true" 
        - "--providers.docker.exposedbydefault=false" 
        - "--entrypoints.web.address=:80" 
        - "--entrypoints.websecure.address=:443" 
        - "--providers.file.directory=/conf" 
        - "--serverstransport.insecureskipverify=true" 
#       - "--log.level=DEBUG" 
#       - "--accesslog=true" 
    labels: 
        - "traefik.enable=true" 
        - "traefik.http.routers.traefik_http.rule=Host(`traefik.example.come`)" 
        - "traefik.http.routers.traefik_http.entrypoints=web" 
        - "traefik.http.routers.traefik_https.rule=Host(`traefik.example.com`)" 
        - "traefik.http.routers.traefik_https.entrypoints=websecure" 
        - "traefik.http.routers.traefik_https.tls=true"
        - "traefik.http.services.traefik.loadbalancer.server.port=8080" 
        - "traefik.docker.network=traefik_net" 

Unser erster Service ist Traefik. Traefik ist ein sehr einfach zu benutzender Reverse Proxy mit vielen weiteren Funktionen. Beispielsweise ist Traefik ebenfalls cloud-native, d.h. es könnte auch in unserem späteren Kubernetes-Cluster gestartet werden. Der Einfachheit halber starten und nutzen wir Traefik aber als Docker-Container.

Zum Zeitpunkt der Erstellung dieser Anleitung ist Traefik in der Version 2.3.5 eine valide Option. Beim Anwenden dieser Anleitung solltet ihr aber prüfen, ob es nicht mittlerweile eine neuere Version von Traefik gibt. Ich kann natürlich nicht garantieren, dass die Konfiguration Traefiks dann genauso funktioniert.

Wir lassen Traefik die Ports 80 (für HTTP) und 443 (für HTTPS) des Hosts belegen und mounten einige Docker Volumes. Zum einen mounten wir ein Verzeichnis /conf, in das wir später eine Konfigurationsdatei legen können. Diese kann Traefik dynamisch im laufenden Betrieb lesen und benötigt somit bei Änderungen keinen Neustart. Das andere Volume stellt Traefik das Docker-Socket zur Verfügung, durch welchen Traefik das Starten und Stoppen anderer Container registrieren kann. Sind diese Container mit entsprechenden Labels ausgestattet, so kann Treafik das Routing für diese Container automatisch durchführen.

Wir starten Traefik mit einigen Parametern (siehe den command Teil). Mit --api.insecure=true schalten wir den Zugriff auf das Traefik-Dashboard frei. Ob ihr das in einem Produktivsystem erlauben wollt, solltet ihr euch natürlich sehr gut überlegen. Für die Zwecke dieser Anleitung ist das aber vollkommen ausreichend. Wie man das Dashboard absichert, wird in der zugehörigen Dokumentation erklärt. Wir aktivieren den Docker-Provider (dieser nutzt dann das Docker-Socket zum Erkennen der Container) und legen fest, dass nur Container freigegeben werden, wenn wir es explizit konfigurieren. Anschließend erstellen wir einen Entrypoint namens web auf Port 80 für HTTP und einen namens websecure auf Port 443 für HTTPS. Nun teilen wir Traefik mit, dass er seine Konfigurationen für den File-Provider, also die dynamisch eingelesenen Konfigurationsdateien, im Verzeichnis /conf findet. Mit der letzten Option --serverstransport.insecureskipverify=true legen wir fest, dass Traefik auch auf Ressourcen ohne gültige SSL-Zertifikate weiterleiten darf. Für unser Testsystem haben wir kein solches Zertifikat, weshalb Traefik und später auch unsere Kubernetes-Distribution Rancher mit einem selbst signierten Zertifikat arbeiten wird. Wir benötigen diese Option also, damit die Weiterleitung von Traefik auf das Kubernetes-Cluster nicht blockiert wird. Selbst in einer Produktivumgebung könnte man diese Option aktiviert lassen, da hier die SSL-Verbindung meist an Traefik terminiert wird. Die zwei folgenden Optionen sind auskommentiert und damit wirkungslos. Mit diesen lässt sich Treafiks Log-Level erhöhen und das Loggen von Zugriffen aktivieren. Falls ihr dies benötigt, könnt ihr die zwei Teile einfach wieder einkommentieren.

Wie bereits beschrieben, können wir Traefik direkt über Container-Labels konfigurieren, da dieser Zugriff auf die Docker-Sockets besitzt. Für dieses Testsystem wollen wir das Traefik-Dashboard unter traefik.example.com freigeben. Innerhalb der Container-Labels geben wir diesen Container per traefik.enable=true für Traefik frei. Wir erstellen uns dann zwei Router namens traefik_http zum Entrypoint web (und damit auf Port 80) und traefik_https zum Entrypoint websecure (und damit auf Port 443). Bei beiden Routern legen wir den Host auf traefik.example.com fest und aktivieren TLS für den traefik_https Router. Wir teilen Traefik mit, dass er diesen Service unter dem Port 8080 findet (beide Router leiten also auf den Port 8080 dieses Containers weiter), unter welchem das Traefik-Dashboard antwortet. Zu guter Letzt legen wir noch das Docker-Netzwerk fest, auf das der Docker Provider hören soll.

Wichtig ist hier, die Funktionsweise von Traefik zu verstehen. Durch den Docker-Provider gibt Traefik alle Container im angegebenen Netzwerk frei, die es mit den gerade besprochenen Labels findet (Namen der Router und Hostnames werden sich natürlich unterscheiden). Aus der Sicht Traefiks ist es selbst auch nur ein Container, der die entsprechenden Labels aufweist, und somit gibt Traefik das Traefik-Dashboard unter dem angegebenen Host frei. Auf diese Weise werden wir auch GitLab und die Docker-Registry freigeben.

Tipp: Für ein Produktivsystem könnte Traefik HTTP Anfragen auch automatisch auf HTTPS umleiten. Nähere Infos dazu findet ihr hier.

PostgreSQL Datenbank für GitLab

database:
    image: postgres:13.0-alpine
    container_name: database
    restart: unless-stopped
    networks:
      - traefik_net
    environment:
      POSTGRES_PASSWORD: "safe_sql_password"
      POSTGRES_DB: gitlab
    volumes:
      - /home/ubuntu/database:/var/lib/postgresql/data
 

Hier starten wir eine PostgreSQL Datenbank. Durch das Volume werden die Daten extern persistiert und der Container liegt im gleichen Docker-Netzwerk wie Traefik. In den Umgebungsvariablen sollte natürlich ein sicheres Passwort für die Datenbank gewählt werden.

GitLab

gitlab:
    image: 'gitlab/gitlab-ee:13.6.3-ee.0'
    container_name: gitlab
    restart: unless-stopped
    hostname: 'gitlab.example.com'
    depends_on:
      - traefik
      - database
    environment:
      GITLAB_ROOT_PASSWORD: safe_admin_password
      GITLAB_ROOT_EMAIL: Diese E-Mail-Adresse ist vor Spambots geschützt! Zur Anzeige muss JavaScript eingeschaltet sein!
      GITLAB_SIGNUP_ENABLED: "false"
      GITLAB_OMNIBUS_CONFIG: |


        gitlab_rails['initial_root_password'] = "safe_admin_password"
        gitlab_rails['initial_shared_runners_registration_token'] = "safe_runner_reg_token"
        gitlab_rails['trusted_proxies'] = ['traefik']

        nginx['listen_port'] = 80
        nginx['listen_https'] = false
        nginx['redirect_http_to_https'] = false

        nginx['proxy_set_headers'] = {
          "X-Forwarded-Proto" => "http",
          "X-Forwarded-Ssl" => "off"
        }

        # DATABASE CONNECTION SETTINGS: in our case we use postgresql as database
        gitlab_rails['db_adapter'] = "postgresql"
        gitlab_rails['db_database'] = "gitlab"
        gitlab_rails['db_username'] = "postgres"
        gitlab_rails['db_password'] = "safe_sql_password"
        gitlab_rails['db_host'] = "database"

        # GITLAB DOCKER IMAGE REGISTRY: so that we can use our docker image registry with gitlab
        registry['enable'] = false # we do not activate this option because we provide our own registry
        gitlab_rails['registry_enabled'] = true
        gitlab_rails['registry_host'] = "registry.example.com"
        gitlab_rails['registry_api_url'] = "http://registry.example.com"
        gitlab_rails['registry_issuer'] = "gitlab-issuer"

        # SMTP SETTINGS: So that gitlab can send emails. In our case we send via google mail.
        gitlab_rails['smtp_enable'] = false
    networks:
      - traefik_net
    volumes:
      - /home/ubuntu/gitlab/config:/etc/gitlab
      - /home/ubuntu/gitlab/logs:/var/log/gitlab
      - /home/ubuntu/gitlab/data:/var/opt/gitlab
      - /home/ubuntu/registry/certs:/certs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitlab_http.rule=Host(`gitlab.example.com`)"
      - "traefik.http.routers.gitlab_http.entrypoints=web"
      - "traefik.http.routers.gitlab_https.rule=Host(`gitlab.example.com`)"
      - "traefik.http.routers.gitlab_https.entrypoints=websecure"
      - "traefik.http.routers.gitlab_https.tls=true"
      - "traefik.http.services.gitlab.loadbalancer.server.port=80"
      - "traefik.docker.network=traefik_net"
 

Hiermit definieren wir ein GitLab in der Version 13.6.3 (es gibt bereits neuere Versionen). Zuerst legen wir unseren Hostname und das Passwort und die E-Mail des Admin-Accounts fest. Für die spätere Pipeline, die unsere gesamte Infrastruktur erstellen wird, ist wichtig, dass das GitLab-Runner-Token an dieser Stelle per gitlab_rails['initial_shared_runners_registration_token'] = "safe_runner_reg_token" statisch gesetzt wird, da es sonst zufällig festgelegt werden würde. Dieses Token ist sozusagen das Passwort, mit dem sich der GitLab-Runner beim GitLab anmelden kann. Wir legen ein paar Optionen fest, damit GitLab auf Port 80 antwortet und nicht auf Port 443 weiterleitet (nur für dieses Testsystem relevant) und konfigurieren die Datenbank mit ihrem zuvor festgelegten Passwort. Als Nächstes geben wir auch schon unsere noch folgende Docker-Registry an, die wir unter registry.example.com erreichen werden. Da wir für das Testsystem keinen E-Mail-Server haben, wird dieser deaktiviert. Wir definieren anschließend einige Volumes, um die Daten GitLabs in der VM zu persistieren, wobei das Volume /certs erst relevant wird, wenn HTTPS verwendet werden soll. In den folgenden Labels konfigurieren wir Traefik so, dass unser GitLab unter gitlab.example.com erreichbar ist (analog zum Traefik Container).

Docker-Registry

registry:
    restart: unless-stopped
    image: registry:2.7.1
    container_name: registry
    depends_on:
      gitlab:
        condition: service_healthy
    volumes:
      - /home/ubuntu/registry/data:/registry
      - /home/ubuntu/registry/certs:/certs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitlab_registry_http.rule=Host(`registry.example.com`)"
      - "traefik.http.routers.gitlab_registry_http.entrypoints=web"
      - "traefik.http.routers.gitlab_registry_https.rule=Host(`registry.example.com`)"
      - "traefik.http.routers.gitlab_registry_https.entrypoints=websecure"
      - "traefik.http.routers.gitlab_registry_https.tls=true"
      - "traefik.http.services.gitlab_registry.loadbalancer.server.port=5000"
      - "traefik.docker.network=traefik_net"
    networks:
      - traefik_net
    environment:
      REGISTRY_LOG_LEVEL: debug
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /registry
      REGISTRY_STORAGE_DELETE_ENABLED: 'true'

networks:
  traefik_net:
    name: "traefik_net"
 

Unser vierter und letzter Service ist die Docker-Registry. Zur Vorbereitung für unsere spätere GitLab-CI-Pipeline, die unsere Infrastruktur erstellen wird, verwenden wir hier einen Trick mit depends_on. Der Startvorgang von GitLab benötigt leider sehr viel Zeit. Alle Versuche, GitLab zu konfigurieren, würden somit fehlschlagen, wenn GitLab noch nicht bereit ist. Deshalb möchten wir, dass unser zukünftiger Aufruf des Befehls docker-compose up -d so lange blockiert, bis alle Services erfolgreich gestartet wurden. Um dies zu erreichen, lassen wir die Docker-Registry erst starten, wenn GitLab den Status "healthy" erreicht. Dafür nutzen wir den im Docker-Image von GitLab definierten Healthcheck aus. Mit diesem Trick können wir sicher sein, dass GitLab am Ende des Aufrufs von docker-compose up -d vollständig gestartet wurde und für weitere Konfiguration bereit ist. Anschließend definieren wir zwei Volumes (/certs ist auch hier nur beim Gebrauch von HTTPS nötig) und konfigurieren Traefik so, dass unsere Registry unter registry.example.com erreichbar ist. Per Umgebungsvariablen legen wir dann noch das Log-Level der Registry und dessen Stammverzeichnis fest.
Zu guter Letzt definieren wir noch das Netzwerk traefik_net, welches wir bei allen Services verwendet haben.

Zusammenfassung

In diesem Teil haben wir eine docker-compose.yml erstellt, die für uns Traefik, GitLab und eine Docker Registry starten kann. Wir haben bereits mehrere Hostnames mit unseren Services verbunden und per depends_on-Trick einen wichtigen Grundstein für unsere GitLab-CI-Pipeline gelegt.
Möchtet ihr an dieser Stelle eure Ergebnisse überprüfen, so könnt ihr die docker-compose.yml einfach auf die VM1 kopieren und dort per docker-compose up -d starten. Ihr solltet dann beispielsweise das Traefik-Dashboard unter traefik.example.com erreichen. Voraussetzung ist natürlich, dass bereits Docker und Docker-Compose installiert sind. Dies müsstet ihr manuell erledigen, da dies erst durch unsere spätere Pipeline durchgeführt wird.
Im dritten Teil dieser Anleitung erstellen wir eine weitere Ansible-Rolle, die Docker-Compose installieren und diese docker-compose.yml auf den Server kopieren und starten kann.

By accepting you will be accessing a service provided by a third-party external to https://blog.ordix.de/