Docker und alles, alles, alles

TL;DR: Ein Überblicksartikel-Leitfaden zum Vergleich von Umgebungen für das Starten von Anwendungen in Containern. Es werden die Möglichkeiten von Docker und anderen ähnlichen Systemen behandelt.

Docker und alles, alles, alles

Ein bisschen Geschichte, woher das alles kommt

Geschichte

Die erste allgemein bekannte Methode zur Isolation von Anwendungen ist chroot. Der gleichnamige Systemaufruf ermöglicht die Änderung des Wurzelverzeichnisses — so hat das aufrufende Programm nur Zugriff auf die Dateien innerhalb dieses Verzeichnisses. Wenn das Programm jedoch mit Superuser-Rechten ausgestattet wird, kann es potenziell "aus dem chroot entkommen" und Zugriff auf das zugrunde liegende Betriebssystem erhalten. Darüber hinaus sind andere Ressourcen (Arbeitsspeicher, Prozessor) und der Netzwerkzugang nicht beschränkt.

Die nächste Methode besteht darin, ein vollständiges Betriebssystem innerhalb eines Containers zu starten, basierend auf den Mechanismen des Betriebssystemkerns. In verschiedenen Betriebssystemen wird diese Methode unterschiedlich bezeichnet, aber das Prinzip bleibt das gleiche — mehrere unabhängige Betriebssysteme zu starten, die alle mit demselben Kern arbeiten wie das zugrunde liegende Betriebssystem. Dazu gehören FreeBSD Jails, Solaris Zones, OpenVZ und LXC für Linux. Hierbei wird nicht nur die Isolation des Speicherplatzes, sondern auch anderer Ressourcen sichergestellt, sodass jeder Container Einschränkungen hinsichtlich CPU-Zeit, Arbeitsspeicher und Netzwerkbandbreite haben kann. Im Vergleich zu chroot ist es schwieriger, aus dem Container auszubrechen, da der Superuser im Container nur Zugriff auf die Inhalte des Containers hat. Dennoch gibt es eine nicht unerhebliche Wahrscheinlichkeit für ein "Durchbrechen" des Isolationstrenns des Kerns und damit für den Zugriff auf das zugrunde liegende Betriebssystem, insbesondere aufgrund der Notwendigkeit, das Betriebssystem im Container aktuell zu halten und der Verwendung älterer Kernversionen (insbesondere relevant für Linux, weniger für FreeBSD).

Anstatt ein vollwertiges Betriebssystem in einem Container (mit Initialisierungssystem, Paketmanager usw.) zu starten, kann man direkt Anwendungen ausführen, wobei es wichtig ist, den Anwendungen diese Möglichkeit zu geben (Verfügbarkeit der benötigten Bibliotheken und anderer Dateien). Diese Idee diente als Grundlage für die containerbasierte Virtualisierung von Anwendungen, deren bekanntester Vertreter Docker ist. Im Vergleich zu früheren Systemen ermöglichten flexiblere Isolationsmechanismen zusammen mit der integrierten Unterstützung virtueller Netzwerke zwischen Containern und der Überwachung des Anwendungsstatus innerhalb des Containers letztendlich den Aufbau einer einheitlichen Umgebung aus einer großen Anzahl physischer Server für die Ausführung von Containern – ohne die Notwendigkeit einer manuellen Ressourcenverwaltung.

Docker

Docker ist die bekannteste Software zur Containerisierung von Anwendungen. Es ist in der Programmiersprache Go geschrieben und nutzt die Standardfunktionen des Linux-Kernels – cgroups, Namespaces, Capabilities usw. – sowie Dateisysteme wie Auffs und andere ähnliche, um Speicherplatz zu sparen.

Docker und alles, alles, alles
Quelle: wikimedia

Architektur

Vor Version 1.11 arbeitete Docker als ein einzelner Dienst, der alle Operationen mit Containern durchführte: Herunterladen von Container-Images, Starten von Containern, Verarbeiten von API-Anfragen. Ab Version 1.11 wurde Docker in mehrere Teile aufgeteilt, die miteinander interagieren: containerd zur Verwaltung des gesamten Lebenszyklus von Containern (Zuteilung von Speicherplatz, Herunterladen von Images, Netzwerkmanagement, Start, Installation und Überwachung des Containerstatus) und runC, eine Ausführungsumgebung für Container, die auf der Nutzung von cgroups und anderen Möglichkeiten des Linux-Kernels basiert. Der Dienst docker selbst blieb bestehen, dient jedoch jetzt nur noch der Verarbeitung von API-Anfragen, die an containerd weitergeleitet werden.

Docker und alles, alles, alles

Installation und Konfiguration

Meine bevorzugte Methode zur Installation von Docker ist docker-machine, das nicht nur die Installation und Konfiguration von Docker auf entfernten Servern (einschließlich verschiedener Clouds) ermöglicht, sondern auch die Arbeit mit den Dateisystemen entfernter Server erlaubt und verschiedene Befehle ausführen kann.

Seit 2018 hat sich das Projekt jedoch kaum weiterentwickelt, daher werden wir die Installation auf die gängige Weise für die meisten Linux-Distributionen durchführen – durch Hinzufügen des Repositories und der Installation der erforderlichen Pakete.

Diese Methode wird auch bei automatisierten Installationen angewendet, beispielsweise mit Ansible oder anderen ähnlichen Systemen, aber in diesem Artikel werde ich sie nicht behandeln.

Die Installation erfolgt auf Centos 7. Als Server verwende ich eine virtuelle Maschine; zur Installation reichen die untenstehenden Befehle aus:

# yum install -y yum-utils
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yum install docker-ce docker-ce-cli containerd.io

Nach der Installation muss der Dienst gestartet und beim Systemstart aktiviert werden:

# systemctl enable docker
# systemctl start docker
# firewall-cmd --zone=public --add-port=2377/tcp --permanent

Zusätzlich kann eine Gruppe namens docker erstellt werden, deren Benutzer mit docker ohne sudo arbeiten können, das Logging konfiguriert werden, der Zugriff auf die API von außen aktiviert und die Firewall präziser eingestellt werden (alles ist verboten, was nicht erlaubt ist; in den obigen und folgenden Beispielen habe ich dies der Einfachheit halber weggelassen), aber ich werde hier nicht näher darauf eingehen.

Weitere Möglichkeiten

Neben der bereits genannten Docker-Maschine gibt es auch eine Docker-Registry, ein Tool zur Speicherung von Container-Images, sowie Docker Compose — ein Tool zur Automatisierung der Bereitstellung von Anwendungen in Containern, bei dem YAML-Dateien zur Erstellung und Konfiguration von Containern und anderen damit verbundenen Aspekten (wie Netzwerke, dauerhafte Dateisysteme zur Datenspeicherung) verwendet werden.

Darüber hinaus können mit dieser Methode Pipeline für CICD organisiert werden. Eine andere interessante Möglichkeit ist der Clusterbetrieb, der sogenannte Swarm-Modus (bis Version 1.12 als Docker Swarm bekannt), der es ermöglicht, aus mehreren Servern eine einheitliche Infrastruktur für den Betrieb von Containern zu schaffen. Es gibt Unterstützung für virtuelle Netzwerke über alle Server hinweg, einen integrierten Lastenausgleich sowie Unterstützung für Geheimnisse für Container.

Die YAML-Dateien von Docker Compose können mit kleinen Änderungen für solche Cluster verwendet werden, wodurch die Verwaltung kleinerer und mittlerer Cluster für verschiedene Zwecke vollständig automatisiert wird. Für große Cluster ist es jedoch besser, Kubernetes zu verwenden, da die Betriebskosten im Swarm-Modus die von Kubernetes überschreiten können. Neben runC kann beispielsweise auch Kata Containers

Arbeiten mit Docker

Nach der Installation und Konfiguration werden wir versuchen, ein Cluster aufzubauen, in dem wir GitLab und Docker Registry für das Entwicklerteam bereitstellen werden. Ich werde dafür drei virtuelle Maschinen als Server verwenden, auf denen ich zusätzlich das verteilte Dateisystem GlusterFS einrichten werde, das ich als Speicher für die Docker-Volumes verwenden werde, beispielsweise für den Start einer ausfallsicheren Version der Docker Registry. Die Schlüsselkomponenten für den Betrieb sind: Docker Registry, Postgresql, Redis, GitLab mit Unterstützung für GitLab Runner über Swarm. Postgresql werden wir mit Clusterbildung starten. Stolon, daher sollte GlusterFS nicht zur Speicherung der Postgresql-Daten verwendet werden. Die anderen kritischen Daten werden auf GlusterFS gespeichert.

Um GlusterFS auf allen Servern (die als node1, node2, node3 bezeichnet werden) bereitzustellen, müssen die Pakete installiert, die Firewall konfiguriert und die benötigten Verzeichnisse erstellt werden:

# yum -y install centos-release-gluster7
# yum -y install glusterfs-server
# systemctl enable glusterd
# systemctl start glusterd
# firewall-cmd --add-service=glusterfs --permanent
# firewall-cmd --reload
# mkdir -p /srv/gluster
# mkdir -p /srv/docker
# echo "$(hostname):/docker /srv/docker glusterfs defaults,_netdev 0 0" >> /etc/fstab

Nach der Installation muss die Konfiguration von GlusterFS von einem Knoten aus fortgesetzt werden, zum Beispiel node1:

# gluster peer probe node2
# gluster peer probe node3
# gluster volume create docker replica 3 node1:/srv/gluster node2:/srv/gluster node3:/srv/gluster force
# gluster volume start docker

Dann muss das erhaltene Volume (der Befehl muss auf allen Servern ausgeführt werden) eingebunden werden:

# mount /srv/docker

Die Konfiguration des Swarm-Modus erfolgt auf einem der Server, der der Leader sein wird, die anderen müssen sich dem Cluster anschließen. Daher muss das Ergebnis des Befehls auf dem ersten Server kopiert und auf den anderen ausgeführt werden.

Die erste Konfiguration des Clusters, ich führe den Startbefehl auf node1 aus:

# docker swarm init
Swarm initialized: current node (a5jpfrh5uvo7svzz1ajduokyq) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-0c5mf7mvzc7o7vjk0wngno2dy70xs95tovfxbv4tqt9280toku-863hyosdlzvd76trfptd4xnzd xx.xx.xx.xx:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
# docker swarm join-token manager

Wir kopieren das Ergebnis des zweiten Befehls und führen es auf node2 und node3 aus:

# docker swarm join --token SWMTKN-x-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxx xx.xx.xx.xx:2377
This node joined a swarm as a manager.

Damit ist die vorläufige Konfiguration der Server abgeschlossen, wir beginnen mit der Einrichtung der Dienste. Die Befehle zur Ausführung werden von node1 ausgeführt, sofern nicht anders angegeben.

Zuerst erstellen wir Netzwerke für die Container:

# docker network create --driver=overlay etcd
# docker network create --driver=overlay pgsql
# docker network create --driver=overlay redis
# docker network create --driver=overlay traefik
# docker network create --driver=overlay gitlab

Dann kennzeichnen wir die Server, dies ist notwendig, um einige Dienste an die Server zu binden:

# docker node update --label-add nodename=node1 node1
# docker node update --label-add nodename=node2 node2
# docker node update --label-add nodename=node3 node3

Danach erstellen wir Verzeichnisse zum Speichern von etcd-Daten, einem KV-Speicher, der für Traefik und Stolon benötigt wird. Ähnlich wie bei Postgresql werden dies Container sein, die an die Server gebunden sind, daher führen wir diesen Befehl auf allen Servern aus:

# mkdir -p /srv/etcd

Als nächstes erstellen wir eine Datei zur Konfiguration von etcd und wenden sie an:

00etcd.yml

version: '3.7'

services:
  etcd1:
    image: quay.io/coreos/etcd:latest
    hostname: etcd1
    command:
      - etcd
      - --name=etcd1
      - --data-dir=/data.etcd
      - --advertise-client-urls=http://etcd1:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --initial-advertise-peer-urls=http://etcd1:2380
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster
    networks:
      - etcd
    volumes:
      - etcd1vol:/data.etcd
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node1]
  etcd2:
    image: quay.io/coreos/etcd:latest
    hostname: etcd2
    command:
      - etcd
      - --name=etcd2
      - --data-dir=/data.etcd
      - --advertise-client-urls=http://etcd2:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --initial-advertise-peer-urls=http://etcd2:2380
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster
    networks:
      - etcd
    volumes:
      - etcd2vol:/data.etcd
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node2]
  etcd3:
    image: quay.io/coreos/etcd:latest
    hostname: etcd3
    command:
      - etcd
      - --name=etcd3
      - --data-dir=/data.etcd
      - --advertise-client-urls=http://etcd3:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --initial-advertise-peer-urls=http://etcd3:2380
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
      - --initial-cluster-token=etcd-cluster
    networks:
      - etcd
    volumes:
      - etcd3vol:/data.etcd
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node3]

volumes:
  etcd1vol:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/etcd"
  etcd2vol:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/etcd"
  etcd3vol:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/etcd"

networks:
  etcd:
    external: true

# docker stack deploy --compose-file 00etcd.yml etcd

Nach einiger Zeit überprüfen wir, ob der etcd-Cluster gestartet ist:

# docker exec $(docker ps | awk '/etcd/ {print $1}')  etcdctl member list
ade526d28b1f92f7: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://etcd1:2379 isLeader=false
bd388e7810915853: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://etcd3:2379 isLeader=false
d282ac2ce600c1ce: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://etcd2:2379 isLeader=true
# docker exec $(docker ps | awk '/etcd/ {print $1}')  etcdctl cluster-health
member ade526d28b1f92f7 is healthy: got healthy result from http://etcd1:2379
member bd388e7810915853 is healthy: got healthy result from http://etcd3:2379
member d282ac2ce600c1ce is healthy: got healthy result from http://etcd2:2379
cluster is healthy

Wir erstellen Verzeichnisse für PostgreSQL; der Befehl wird auf allen Servern ausgeführt:

# mkdir -p /srv/pgsql

Als nächstes erstellen wir eine Datei zur Konfiguration von PostgreSQL:

01pgsql.yml

version: '3.7'

services:
  pgsentinel:
    image: sorintlab/stolon:master-pg10
    command:
      - gosu
      - stolon
      - stolon-sentinel
      - --cluster-name=stolon-cluster
      - --store-backend=etcdv3
      - --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
      - --log-level=debug
    networks:
      - etcd
      - pgsql
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 30s
        order: stop-first
        failure_action: pause
  pgkeeper1:
    image: sorintlab/stolon:master-pg10
    hostname: pgkeeper1
    command:
      - gosu
      - stolon
      - stolon-keeper
      - --pg-listen-address=pgkeeper1
      - --pg-repl-username=replica
      - --uid=pgkeeper1
      - --pg-su-username=postgres
      - --pg-su-passwordfile=/run/secrets/pgsql
      - --pg-repl-passwordfile=/run/secrets/pgsql_repl
      - --data-dir=/var/lib/postgresql/data
      - --cluster-name=stolon-cluster
      - --store-backend=etcdv3
      - --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
    networks:
      - etcd
      - pgsql
    environment:
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - pgkeeper1:/var/lib/postgresql/data
    secrets:
      - pgsql
      - pgsql_repl
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node1]
  pgkeeper2:
    image: sorintlab/stolon:master-pg10
    hostname: pgkeeper2
    command:
      - gosu
      - stolon
      - stolon-keeper
      - --pg-listen-address=pgkeeper2
      - --pg-repl-username=replica
      - --uid=pgkeeper2
      - --pg-su-username=postgres
      - --pg-su-passwordfile=/run/secrets/pgsql
      - --pg-repl-passwordfile=/run/secrets/pgsql_repl
      - --data-dir=/var/lib/postgresql/data
      - --cluster-name=stolon-cluster
      - --store-backend=etcdv3
      - --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
    networks:
      - etcd
      - pgsql
    environment:
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - pgkeeper2:/var/lib/postgresql/data
    secrets:
      - pgsql
      - pgsql_repl
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node2]
  pgkeeper3:
    image: sorintlab/stolon:master-pg10
    hostname: pgkeeper3
    command:
      - gosu
      - stolon
      - stolon-keeper
      - --pg-listen-address=pgkeeper3
      - --pg-repl-username=replica
      - --uid=pgkeeper3
      - --pg-su-username=postgres
      - --pg-su-passwordfile=/run/secrets/pgsql
      - --pg-repl-passwordfile=/run/secrets/pgsql_repl
      - --data-dir=/var/lib/postgresql/data
      - --cluster-name=stolon-cluster
      - --store-backend=etcdv3
      - --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
    networks:
      - etcd
      - pgsql
    environment:
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - pgkeeper3:/var/lib/postgresql/data
    secrets:
      - pgsql
      - pgsql_repl
    deploy:
      replicas: 1
      placement:
        constraints: [node.labels.nodename == node3]
  postgresql:
    image: sorintlab/stolon:master-pg10
    command: gosu stolon stolon-proxy --listen-address 0.0.0.0 --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
    networks:
      - etcd
      - pgsql
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 30s
        order: stop-first
        failure_action: rollback

volumes:
  pgkeeper1:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/pgsql"
  pgkeeper2:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/pgsql"
  pgkeeper3:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/pgsql"

secrets:
  pgsql:
    file: "/srv/docker/postgres"
  pgsql_repl:
    file: "/srv/docker/replica"

networks:
  etcd:
    external: true
  pgsql:
    external: true

Generieren von Geheimnissen, Datei anwenden:

# </dev/urandom tr -dc 234567890qwertyuopasdfghjkzxcvbnmQWERTYUPASDFGHKLZXCVBNM | head -c $(((RANDOM%3)+15)) > /srv/docker/replica
# </dev/urandom tr -dc 234567890qwertyuopasdfghjkzxcvbnmQWERTYUPASDFGHKLZXCVBNM | head -c $(((RANDOM%3)+15)) > /srv/docker/postgres
# docker stack deploy --compose-file 01pgsql.yml pgsql

Nach einiger Zeit (wir sehen uns die Ausgabe des Befehls an docker service ls, dass alle Dienste hochgefahren sind) initialisieren wir den Postgresql-Cluster:

# docker exec $(docker ps | awk '/pgkeeper/ {print $1}') stolonctl --cluster-name=stolon-cluster --store-backend=etcdv3 --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 init

Wir überprüfen die Bereitschaft des Postgresql-Clusters:

# docker exec $(docker ps | awk '/pgkeeper/ {print $1}') stolonctl --cluster-name=stolon-cluster --store-backend=etcdv3 --store-endpoints=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 status
=== Active sentinels ===

ID      LEADER
26baa11d    false
74e98768    false
a8cb002b    true

=== Active proxies ===

ID
4d233826
9f562f3b
b0c79ff1

=== Keepers ===

UID     HEALTHY PG LISTENADDRESS    PG HEALTHY  PG WANTEDGENERATION PG CURRENTGENERATION
pgkeeper1   true    pgkeeper1:5432         true     2           2
pgkeeper2   true    pgkeeper2:5432          true            2                   2
pgkeeper3   true    pgkeeper3:5432          true            3                   3

=== Cluster Info ===

Master Keeper: pgkeeper3

===== Keepers/DB tree =====

pgkeeper3 (master)
├─pgkeeper2
└─pgkeeper1

Wir konfigurieren Traefik, um den Zugriff auf die Container von außen zu öffnen:

03traefik.yml

version: '3.7'

services:
  traefik:
    image: traefik:latest
    command: >
      --log.level=INFO
      --providers.docker=true
      --entryPoints.web.address=:80
      --providers.providersThrottleDuration=2
      --providers.docker.watch=true
      --providers.docker.swarmMode=true
      --providers.docker.swarmModeRefreshSeconds=15s
      --providers.docker.exposedbydefault=false
      --accessLog.bufferingSize=0
      --api=true
      --api.dashboard=true
      --api.insecure=true
    networks:
      - traefik
    ports:
      - 80:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      replicas: 3
      placement:
        constraints:
          - node.role == manager
        preferences:
          - spread: node.id
      labels:
        - traefik.enable=true
        - traefik.http.routers.traefik.rule=Host(`traefik.example.com`)
        - traefik.http.services.traefik.loadbalancer.server.port=8080
        - traefik.docker.network=traefik

networks:
  traefik:
    external: true

# docker stack deploy --compose-file 03traefik.yml traefik

Wir starten den Redis-Cluster, dazu erstellen wir auf allen Knoten ein Verzeichnis für die Speicherung:

# mkdir -p /srv/redis

05redis.yml

version: '3.7'

services:
  redis-master:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '6379:6379'
    environment:
      - REDIS_REPLICATION_MODE=master
      - REDIS_PASSWORD=xxxxxxxxxxx
    deploy:
      mode: global
      restart_policy:
        condition: any
    volumes:
      - 'redis:/opt/bitnami/redis/etc/'

  redis-replica:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '6379'
    depends_on:
      - redis-master
    environment:
      - REDIS_REPLICATION_MODE=slave
      - REDIS_MASTER_HOST=redis-master
      - REDIS_MASTER_PORT_NUMBER=6379
      - REDIS_MASTER_PASSWORD=xxxxxxxxxxx
      - REDIS_PASSWORD=xxxxxxxxxxx
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: any

  redis-sentinel:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '16379'
    depends_on:
      - redis-master
      - redis-replica
    entrypoint: |
      bash -c 'bash -s <<EOF
      "/bin/bash" -c "cat < /opt/bitnami/redis/etc/sentinel.conf
      port 16379
      dir /tmp
      sentinel monitor master-node redis-master 6379 2
      sentinel down-after-milliseconds master-node 5000
      sentinel parallel-syncs master-node 1
      sentinel failover-timeout master-node 5000
      sentinel auth-pass master-node xxxxxxxxxxx
      sentinel announce-ip redis-sentinel
      sentinel announce-port 16379
      EOF"
      "/bin/bash" -c "redis-sentinel /opt/bitnami/redis/etc/sentinel.conf"
      EOF'
    deploy:
      mode: global
      restart_policy:
        condition: any

volumes:
  redis:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: "/srv/redis"

networks:
  redis:
    external: true

# docker stack deploy --compose-file 05redis.yml redis

Wir fügen das Docker-Registry hinzu:

06registry.yml

version: '3.7'

services:
  registry:
    image: registry:2.6
    networks:
      - traefik
    volumes:
      - registry_data:/var/lib/registry
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager]
      restart_policy:
        condition: on-failure
      labels:
        - traefik.enable=true
        - traefik.http.routers.registry.rule=Host(`registry.example.com`)
        - traefik.http.services.registry.loadbalancer.server.port=5000
        - traefik.docker.network=traefik

volumes:
  registry_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/docker/registry"

networks:
  traefik:
    external: true

# mkdir /srv/docker/registry
# docker stack deploy --compose-file 06registry.yml registry

Und schließlich — GitLab:

08gitlab-runner.yml

version: '3.7'

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    networks:
      - pgsql
      - redis
      - traefik
      - gitlab
    ports:
      - 22222:22
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        postgresql['enable'] = false
        redis['enable'] = false
        gitlab_rails['registry_enabled'] = false
        gitlab_rails['db_username'] = "gitlab"
        gitlab_rails['db_password'] = "XXXXXXXXXXX"
        gitlab_rails['db_host'] = "postgresql"
        gitlab_rails['db_port'] = "5432"
        gitlab_rails['db_database'] = "gitlab"
        gitlab_rails['db_adapter'] = 'postgresql'
        gitlab_rails['db_encoding'] = 'utf8'
        gitlab_rails['redis_host'] = 'redis-master'
        gitlab_rails['redis_port'] = '6379'
        gitlab_rails['redis_password'] = 'xxxxxxxxxxx'
        gitlab_rails['smtp_enable'] = true
        gitlab_rails['smtp_address'] = "smtp.yandex.ru"
        gitlab_rails['smtp_port'] = 465
        gitlab_rails['smtp_user_name'] = "noreply@example.com"
        gitlab_rails['smtp_password'] = "xxxxxxxxx"
        gitlab_rails['smtp_domain'] = "example.com"
        gitlab_rails['gitlab_email_from'] = 'noreply@example.com'
        gitlab_rails['smtp_authentication'] = "login"
        gitlab_rails['smtp_tls'] = true
        gitlab_rails['smtp_enable_starttls_auto'] = true
        gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
        external_url 'http://gitlab.example.com/'
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    volumes:
      - gitlab_conf:/etc/gitlab
      - gitlab_logs:/var/log/gitlab
      - gitlab_data:/var/opt/gitlab
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager
      labels:
        - traefik.enable=true
        - traefik.http.routers.gitlab.rule=Host(`gitlab.example.com`)
        - traefik.http.services.gitlab.loadbalancer.server.port=80
        - traefik.docker.network=traefik
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    networks:
      - gitlab
    volumes:
      - gitlab_runner_conf:/etc/gitlab
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager

volumes:
  gitlab_conf:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/docker/gitlab/conf"
  gitlab_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/docker/gitlab/logs"
  gitlab_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/docker/gitlab/data"
  gitlab_runner_conf:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: "/srv/docker/gitlab/runner"

networks:
  pgsql:
    external: true
  redis:
    external: true
  traefik:
    external: true
  gitlab:
    external: true

# mkdir -p /srv/docker/gitlab/conf
# mkdir -p /srv/docker/gitlab/logs
# mkdir -p /srv/docker/gitlab/data
# mkdir -p /srv/docker/gitlab/runner
# docker stack deploy --compose-file 08gitlab-runner.yml gitlab

Der aktuelle Status des Clusters und der Dienste:

# docker service ls
ID                  NAME                   MODE                REPLICAS            IMAGE                          PORTS
lef9n3m92buq        etcd_etcd1             replicated          1/1                 quay.io/coreos/etcd:latest
ij6uyyo792x5        etcd_etcd2             replicated          1/1                 quay.io/coreos/etcd:latest
fqttqpjgp6pp        etcd_etcd3             replicated          1/1                 quay.io/coreos/etcd:latest
hq5iyga28w33        gitlab_gitlab          replicated          1/1                 gitlab/gitlab-ce:latest        *:22222->22/tcp
dt7s6vs0q4qc        gitlab_gitlab-runner   replicated          1/1                 gitlab/gitlab-runner:latest
k7uoezno0h9n        pgsql_pgkeeper1        replicated          1/1                 sorintlab/stolon:master-pg10
cnrwul4r4nse        pgsql_pgkeeper2        replicated          1/1                 sorintlab/stolon:master-pg10
frflfnpty7tr        pgsql_pgkeeper3        replicated          1/1                 sorintlab/stolon:master-pg10
x7pqqchi52kq        pgsql_pgsentinel       replicated          3/3                 sorintlab/stolon:master-pg10
mwu2wl8fti4r        pgsql_postgresql       replicated          3/3                 sorintlab/stolon:master-pg10
9hkbe2vksbzb        redis_redis-master     global              3/3                 bitnami/redis:latest           *:6379->6379/tcp
l88zn8cla7dc        redis_redis-replica    replicated          3/3                 bitnami/redis:latest           *:30003->6379/tcp
1utp309xfmsy        redis_redis-sentinel   global              3/3                 bitnami/redis:latest           *:30002->16379/tcp
oteb824ylhyp        registry_registry      replicated          1/1                 registry:2.6
qovrah8nzzu8        traefik_traefik        replicated          3/3                 traefik:latest                 *:80->80/tcp, *:443->443/tcp

Was kann noch verbessert werden? Stellen Sie unbedingt die Arbeit der Container in Traefik über https ein, fügen Sie eine tls-Verschlüsselung für Postgresql und Redis hinzu. Aber insgesamt kann es bereits als PoC an die Entwickler übergeben werden. Schauen wir uns jetzt die Alternativen zu Docker an.

Podman

Eine weitere recht bekannte Engine zum Starten von Containern, gruppiert in Pods (Pods, Gruppen von gemeinsam bereitgestellten Containern). Im Gegensatz zu Docker benötigt es keinen Dienst zum Starten von Containern, die gesamte Arbeit erfolgt über die Bibliothek libpod. Sie ist ebenfalls in Go geschrieben und benötigt zur Ausführung von Containern eine OCI-kompatible Runtime, beispielsweise runC.

Docker und alles, alles, alles

Die Arbeit mit Podman ähnelt weitgehend der mit Docker, bis hin zu der Möglichkeit, folgendes zu tun (wie viele, einschließlich des Autors dieses Artikels, berichteten):

$ alias docker=podman

und man kann weiterarbeiten. Insgesamt ist die Situation mit Podman sehr interessant, denn während frühere Versionen von Kubernetes mit Docker arbeiteten, entwickelt sich seit etwa 2015, nach der Standardisierung der Containerwelt (OCI - Open Container Initiative) und der Trennung von Docker in containerd und runC, eine Alternative zu Docker für den Einsatz in Kubernetes: CRI-O. In diesem Sinne ist Podman eine Alternative zu Docker, die nach den Prinzipien von Kubernetes aufgebaut ist, einschließlich der Gruppierung von Containern, aber das Hauptziel des Projekts besteht darin, Container im Docker-Stil ohne zusätzliche Dienste zu starten. Aus verständlichen Gründen gibt es keinen Swarm-Modus, da die Entwickler deutlich darauf hinweisen, dass, wenn ein Cluster benötigt wird, Kubernetes verwendet werden sollte.

Installation

Für die Installation auf Centos 7 reicht es aus, das Extras-Repository zu aktivieren und danach alles mit dem Befehl zu installieren:

# yum -y install podman

Weitere Möglichkeiten

Podman kann Einheiten für systemd generieren, was das Problem der Ausführung von Containern nach einem Serverneustart löst. Zusätzlich wurde eine ordnungsgemäße Funktion von systemd als pid 1 im Container bestätigt. Zum Erstellen von Containern gibt es ein separates Werkzeug namens buildah, es gibt auch Drittanbieter-Tools - Alternativen zu docker-compose, die unter anderem Konfigurationsdateien generieren, die mit Kubernetes kompatibel sind, sodass der Übergang von Podman zu Kubernetes so reibungslos wie möglich gestaltet ist.

Arbeiten mit Podman

Da es keinen Swarm-Modus gibt (ein Wechsel zu Kubernetes wird vorausgesetzt, wenn ein Cluster benötigt wird) - werden wir separate Container erstellen.

Wir installieren podman-compose:

# yum -y install python3-pip
# pip3 install podman-compose

Die resultierende Konfigurationsdatei für podman unterscheidet sich ein wenig, so musste beispielsweise eine separate Sektion für Volumes direkt in die Sektion mit den Services verschoben werden.

gitlab-podman.yml

version: '3.7'

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    hostname: gitlab.example.com
    restart: unless-stopped
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    ports:
      - "80:80"
      - "22222:22"
    volumes:
      - /srv/podman/gitlab/conf:/etc/gitlab
      - /srv/podman/gitlab/data:/var/opt/gitlab
      - /srv/podman/gitlab/logs:/var/log/gitlab
    networks:
      - gitlab

  gitlab-runner:
    image: gitlab/gitlab-runner:alpine
    restart: unless-stopped
    depends_on:
      - gitlab
    volumes:
      - /srv/podman/gitlab/runner:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - gitlab

networks:
  gitlab:

# podman-compose -f gitlab-runner.yml -d up

Ergebnis:

# podman ps
CONTAINER ID  IMAGE                                  COMMAND               CREATED             STATUS                 PORTS                                      NAMES
da53da946c01  docker.io/gitlab/gitlab-runner:alpine  run --user=gitlab...  About a minute ago  Up About a minute ago  0.0.0.0:22222->22/tcp, 0.0.0.0:80->80/tcp  root_gitlab-runner_1
781c0103c94a  docker.io/gitlab/gitlab-ce:latest      /assets/wrapper       About a minute ago  Up About a minute ago  0.0.0.0:22222->22/tcp, 0.0.0.0:80->80/tcp  root_gitlab_1

Lass uns sehen, was es für systemd und kubernetes generieren wird. Dafür müssen wir den Namen oder die ID des Pods herausfinden:

# podman pod ls
POD ID         NAME   STATUS    CREATED          # OF CONTAINERS   INFRA ID
71fc2b2a5c63   root   Running   11 minutes ago   3                 db40ab8bf84b

Kubernetes:

# podman generate kube 71fc2b2a5c63
# Generation of Kubernetes YAML is still under development!
#
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-1.6.4
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2020-07-29T19:22:40Z"
  labels:
    app: root
  name: root
spec:
  containers:
  - command:
    - /assets/wrapper
    env:
    - name: PATH
      value: /opt/gitlab/embedded/bin:/opt/gitlab/bin:/assets:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    - name: TERM
      value: xterm
    - name: HOSTNAME
      value: gitlab.example.com
    - name: container
      value: podman
    - name: GITLAB_OMNIBUS_CONFIG
      value: |
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    - name: LANG
      value: C.UTF-8
    image: docker.io/gitlab/gitlab-ce:latest
    name: rootgitlab1
    ports:
    - containerPort: 22
      hostPort: 22222
      protocol: TCP
    - containerPort: 80
      hostPort: 80
      protocol: TCP
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
    volumeMounts:
    - mountPath: /var/opt/gitlab
      name: srv-podman-gitlab-data
    - mountPath: /var/log/gitlab
      name: srv-podman-gitlab-logs
    - mountPath: /etc/gitlab
      name: srv-podman-gitlab-conf
    workingDir: /
  - command:
    - run
    - --user=gitlab-runner
    - --working-directory=/home/gitlab-runner
    env:
    - name: PATH
      value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    - name: TERM
      value: xterm
    - name: HOSTNAME
    - name: container
      value: podman
    image: docker.io/gitlab/gitlab-runner:alpine
    name: rootgitlab-runner1
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
    volumeMounts:
    - mountPath: /etc/gitlab-runner
      name: srv-podman-gitlab-runner
    - mountPath: /var/run/docker.sock
      name: var-run-docker.sock
    workingDir: /
  volumes:
  - hostPath:
      path: /srv/podman/gitlab/runner
      type: Directory
    name: srv-podman-gitlab-runner
  - hostPath:
      path: /var/run/docker.sock
      type: File
    name: var-run-docker.sock
  - hostPath:
      path: /srv/podman/gitlab/data
      type: Directory
    name: srv-podman-gitlab-data
  - hostPath:
      path: /srv/podman/gitlab/logs
      type: Directory
    name: srv-podman-gitlab-logs
  - hostPath:
      path: /srv/podman/gitlab/conf
      type: Directory
    name: srv-podman-gitlab-conf
status: {}

Systemd:

# podman generate systemd 71fc2b2a5c63
# pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service
# autogenerated by Podman 1.6.4
# Thu Jul 29 15:23:28 EDT 2020

[Unit]
Description=Podman pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service
Documentation=man:podman-generate-systemd(1)
Requires=container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.service
Before=container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.service

[Service]
Restart=on-failure
ExecStart=/usr/bin/podman start db40ab8bf84bf35141159c26cb6e256b889c7a98c0418eee3c4aa683c14fccaa
ExecStop=/usr/bin/podman stop -t 10 db40ab8bf84bf35141159c26cb6e256b889c7a98c0418eee3c4aa683c14fccaa
KillMode=none
Type=forking
PIDFile=/var/run/containers/storage/overlay-containers/db40ab8bf84bf35141159c26cb6e256b889c7a98c0418eee3c4aa683c14fccaa/userdata/conmon.pid

[Install]
WantedBy=multi-user.target
# container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.service
# autogenerated by Podman 1.6.4
# Thu Jul 29 15:23:28 EDT 2020

[Unit]
Description=Podman container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.service
Documentation=man:podman-generate-systemd(1)
RefuseManualStart=yes
RefuseManualStop=yes
BindsTo=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service
After=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service

[Service]
Restart=on-failure
ExecStart=/usr/bin/podman start da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864
ExecStop=/usr/bin/podman stop -t 10 da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864
KillMode=none
Type=forking
PIDFile=/var/run/containers/storage/overlay-containers/da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864/userdata/conmon.pid

[Install]
WantedBy=multi-user.target
# container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service
# autogenerated by Podman 1.6.4
# Thu Jul 29 15:23:28 EDT 2020

[Unit]
Description=Podman container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service
Documentation=man:podman-generate-systemd(1)
RefuseManualStart=yes
RefuseManualStop=yes
BindsTo=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service
After=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service

[Service]
Restart=on-failure
ExecStart=/usr/bin/podman start 781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3
ExecStop=/usr/bin/podman stop -t 10 781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3
KillMode=none
Type=forking
PIDFile=/var/run/containers/storage/overlay-containers/781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3/userdata/conmon.pid

[Install]
WantedBy=multi-user.target

Leider macht die generierte Einheit für systemd außer dem Starten von Containern nichts weiter (zum Beispiel keine Bereinigung alter Container bei einem Neustart eines solchen Dienstes), daher müssen solche Dinge manuell hinzugefügt werden.

Prinzipiell ist Podman ausreichend, um zu testen, was Container sind, alte Konfigurationen für docker-compose zu migrieren, und danach auf Kubernetes umzusteigen, falls man einen Cluster benötigt, oder um eine einfachere Alternative zu Docker zu erhalten.

rkt

Projekt wurde archiviert vor ungefähr einem halben Jahr, nachdem es von RedHat gekauft wurde, weshalb ich nicht näher darauf eingehen werde. Insgesamt machte es einen ganz guten Eindruck, erschien aber im Vergleich zu Docker und insbesondere Podman wie ein Kombinat. Es gab auch eine Distribution namens CoreOS, die auf rkt basierte (obwohl sie ursprünglich Docker hatten), deren Unterstützung jedoch ebenfalls nach dem Kauf durch RedHat endete.

Plash

Weitere ein Projekt, dessen Autor einfach Container sammeln und starten wollte. Judging by the documentation and the code — der Autor hielt sich nicht an Standards, sondern entschloss sich einfach, seine eigene Implementierung zu schreiben, was er grundsätzlich auch tat.

Das DBMS Tarantool ist ein attraktives, zukunftsträchtiges Produkt zur Erstellung von hochbelasteten Anwendungen.

Die Situation mit Kubernetes gestaltet sich recht interessant: Einerseits kann man mit Docker einen Cluster (im Swarm-Modus) aufbauen, mit dem sogar produktive Umgebungen für Kunden gestartet werden können. Dies ist besonders relevant für kleine Teams (3-5 Personen) oder bei einer geringen Gesamtlast, oder auch wenn es keine Lust gibt, sich mit den Feinheiten der Kubernetes-Konfiguration, auch für hohe Lasten, auseinanderzusetzen.

Podman bietet keine vollständige Kompatibilität, hat jedoch einen wichtigen Vorteil – die Kompatibilität mit Kubernetes, einschließlich zusätzlicher Tools (buildah und andere). Daher werde ich bei der Auswahl des Werkzeugs so verfahren: für kleine Teams oder bei begrenztem Budget – Docker (mit möglichem Swarm-Modus), für die persönliche Entwicklung auf localhost – Podman mit Freunden, und für alle anderen – Kubernetes.

Ich bin mir nicht sicher, ob sich die Situation mit Docker in Zukunft ändern wird; schließlich sind sie Pioniere und standardisieren sich Schritt für Schritt. Dennoch hat Podman mit all seinen Nachteilen (nur auf Linux lauffähig, keine Clusterbildung, Erstellung und andere Aktionen – mit externen Lösungen) eine klarere Zukunft, weshalb ich alle Interessierten einlade, diese Schlussfolgerungen in den Kommentaren zu besprechen.

P.S. Am 3. August starten wir „Videokurs über Docker“, wo Sie mehr über seine Funktionsweise erfahren können. Wir werden alle seine Werkzeuge durchgehen: von den grundlegenden Abstraktionen bis hin zu Netzwerkeinstellungen, Feinheiten der Arbeit mit verschiedenen Betriebssystemen und Programmiersprachen. Sie werden mit der Technologie vertraut gemacht und verstehen, wo und wie man Docker am besten einsetzt. Außerdem teilen wir Best Practices.

Der Preis für die Vorbestellung bis zur Veröffentlichung: 5000 RUR. Das Programm „Videokurs über Docker“ kann eingesehen werden auf der Kursseite.

Quelle: habr.com

60GB SSD 8Gb DDR4