Docker i wszystko, wszystko, wszystko

TL;DR: przeglądowy artykuł-przewodnik porównujący środowiska uruchamiania aplikacji w kontenerach. Rozważone zostaną możliwości Dockera i innych podobnych systemów.

Docker i wszystko, wszystko, wszystko

Trochę historii, skąd to wszystko się wzięło

Historia

Pierwszym powszechnie znanym sposobem izolacji aplikacji jest chroot. Odpowiedni systemowy wywołanie zapewnia zmianę katalogu głównego — tym samym zapewniając programowi, który go wywołuje, dostęp tylko do plików wewnątrz tego katalogu. Ale jeśli program wewnątrz otrzyma prawa superużytkownika, teoretycznie może "uciec" z chroot i uzyskać dostęp do podstawowego systemu operacyjnego. Dodatkowo, oprócz zmiany katalogu głównego, nie są ograniczane inne zasoby (pamięć operacyjna, procesor), a także dostęp do sieci.

Kolejnym sposobem jest uruchomienie pełnego systemu operacyjnego wewnątrz kontenera, dzięki mechanizmom jądra systemu operacyjnego. W różnych systemach operacyjnych ten sposób nazywa się różnie, ale istota jest ta sama — uruchomienie kilku niezależnych systemów operacyjnych, z których każdy działa na tym samym jądrze, na którym pracuje także podstawowy system operacyjny. Należą tutaj FreeBSD Jails, Solaris Zones, OpenVZ i LXC dla Linuksa. Zapewnia to izolację nie tylko pod względem przestrzeni dyskowej, ale również innych zasobów, w szczególności każdy kontener może mieć ograniczenia dotyczące czasu procesora, pamięci operacyjnej i przepustowości sieci. W porównaniu do chroota, wyjście z kontenera jest trudniejsze, ponieważ superużytkownik w kontenerze ma dostęp tylko do komponentów kontenera, jednak z powodu konieczności utrzymania systemu operacyjnego wewnątrz kontenera w aktualnym stanie oraz wykorzystania starszych wersji jąder (aktualne dla Linuksa, w mniejszym stopniu FreeBSD) istnieje niezerowa możliwość "przebicia" systemu izolacji jądra i uzyskania dostępu do podstawowego systemu operacyjnego.

Zamiast uruchamiać pełnoprawny system operacyjny w kontenerze (z systemem inicjalizacji, menedżerem pakietów itd.), można od razu uruchamiać aplikacje, a kluczowe jest zapewnienie aplikacjom odpowiednich warunków (dostępność niezbędnych bibliotek i innych plików). Ta idea stanowiła podstawę dla kontenerowej wirtualizacji aplikacji, której najbardziej znanym i powszechnie docenianym przedstawicielem jest Docker. W porównaniu do wcześniejszych systemów, bardziej elastyczne mechanizmy izolacji, w połączeniu z wbudowaną obsługą wirtualnych sieci między kontenerami oraz monitorowaniem stanu aplikacji wewnątrz kontenera, umożliwiły stworzenie jednolitego środowiska z wielu fizycznych serwerów do uruchamiania kontenerów — bez konieczności ręcznego zarządzania zasobami.

Docker

Docker to najpopularniejsze oprogramowanie do konteneryzacji aplikacji. Napisany w języku Go, wykorzystuje natywne możliwości jądra Linux — cgroups, namespaces, capabilities itd., a także systemy plików Aufs i inne podobne, aby zaoszczędzić miejsce na dysku.

Docker i wszystko, wszystko, wszystko
Źródło: wikimedia

Architektura

Do wersji 1.11 Docker działał jako pojedyncza usługa, która realizowała wszystkie operacje związane z kontenerami: pobieranie obrazów dla kontenerów, uruchamianie kontenerów, przetwarzanie żądań przez API. Począwszy od wersji 1.11 Docker został podzielony na kilka części, które współpracują ze sobą: containerd, do zarządzania całym cyklem życia kontenerów (przydzielanie miejsca na dysku, pobieranie obrazów, obsługa sieci, uruchamianie, instalacja i monitorowanie stanu kontenerów) oraz runC, środowisko uruchomieniowe kontenerów, oparte na wykorzystaniu cgroups i innych możliwości jądra Linux. Sama usługa docker pozostała, ale teraz służy wyłącznie do przetwarzania żądań przez API, które są przekazywane do containerd.

Docker i wszystko, wszystko, wszystko

Instalacja i konfiguracja

Moim ulubionym sposobem instalacji Dockera jest docker-machine, która oprócz samej instalacji i konfiguracji Dockera na zdalnych serwerach (w tym różnych chmurach) umożliwia dostęp do systemów plików zdalnych serwerów oraz może uruchamiać różnorodne polecenia.

Jednak od 2018 roku projekt rozwija się w niewielkim stopniu, dlatego instalację przeprowadzimy standardową metodą dla większości dystrybucji Linux — polegając na dodaniu repozytoriów i instalacji niezbędnych pakietów.

Metoda ta jest również stosowana przy zautomatyzowanej instalacji, na przykład za pomocą Ansible lub innych podobnych systemów, ale w tym artykule nie będę jej omawiać.

Instalacja będzie przeprowadzana na CentOS 7, a jako serwer wykorzystam maszynę wirtualną; wystarczy wykonać poniższe polecenia.

# 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

Po instalacji należy uruchomić usługę i dodać ją do autostartu:

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

Dodatkowo można utworzyć grupę docker, której użytkownicy będą mogli pracować z dockerem bez sudo, skonfigurować logowanie, włączyć dostęp do API zewnętrznie, nie zapominając o dokładniejszym skonfigurowaniu firewalla (wszystko, co nie jest dozwolone, jest zabronione; w powyższych i poniższych przykładach pominąłem to dla uproszczenia i przejrzystości), ale tutaj nie będę się tym bardziej zajmować.

Inne możliwości

Oprócz wymienionej docker machine istnieje również docker registry, narzędzie do przechowywania obrazów kontenerów, a także docker compose — narzędzie do automatyzacji wdrażania aplikacji w kontenerach, które wykorzystuje pliki YAML do budowy i konfiguracji kontenerów oraz innych powiązanych rzeczy (na przykład sieci, trwałe systemy plików do przechowywania danych).

Dzięki niemu można również organizować potoki dla CI/CD. Inną interesującą funkcjonalnością jest tryb klastrowy, zwany swarm mode (do wersji 1.12 znany jako docker swarm), który pozwala na zbudowanie jednolitej infrastruktury do uruchamiania kontenerów z kilku serwerów. Obsługuje wirtualną sieć na wszystkich serwerach, ma wbudowany balancer obciążenia, a także obsługę sekretów dla kontenerów.

Pliki YAML z docker compose z drobnymi zmianami mogą być używane w takich klastrach, automatyzując w pełni zarządzanie małymi i średnimi klastrami do różnych celów. Dla dużych klastrów lepiej jest używać Kubernetes, ponieważ koszty utrzymania swarm mode mogą przewyższyć te związane z Kubernetes. Oprócz runC jako środowiska uruchomieniowego kontenerów można na przykład zainstalować Kata containers

Praca z Dockerem

Po zainstalowaniu i skonfigurowaniu spróbujemy utworzyć klaster, na którym uruchomimy GitLab i Docker Registry dla zespołu deweloperów. Jako serwery wykorzystam trzy maszyny wirtualne, na których dodatkowo zainstaluję rozproszony system plików GlusterFS, który posłuży mi jako miejsce do przechowywania woluminów dockera, na przykład do uruchomienia wersji docker registry odpornej na awarie. Kluczowe komponenty do uruchomienia: Docker Registry, Postgresql, Redis, GitLab z obsługą GitLab Runner na Swarmie. PostgreSQL uruchomimy z klasteryzacją. Stolon, dlatego do przechowywania danych PostgreSQL nie trzeba używać GlusterFS. Pozostałe krytyczne dane będą przechowywane na GlusterFS.

Aby zainstalować GlusterFS na wszystkich serwerach (nazywanych node1, node2, node3), należy zainstalować pakiety, zezwolić na działanie zapory i stworzyć potrzebne katalogi:

# 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

Po zainstalowaniu należy kontynuować konfigurację GlusterFS z jednego węzła, na przykład 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

Następnie należy zamontować otrzymany wolumin (komenda musi być wykonana na wszystkich serwerach):

# mount /srv/docker

Konfiguracja trybu swarm odbywa się na jednym z serwerów, który będzie Liderem, pozostałe serwery będą musiały dołączyć do klastra, dlatego wynik wykonania polecenia na pierwszym serwerze należy skopiować i wykonać na pozostałych.

Początkowa konfiguracja klastra, uruchamiam polecenie na node1:

# 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

Kopiujemy wynik drugiego polecenia, wykonujemy je na node2 i node3:

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

Na tym wstępna konfiguracja serwerów się kończy, przechodzimy do konfiguracji usług, polecenia będą uruchamiane z node1, chyba że zaznaczone inaczej.

Najpierw stworzymy sieci dla kontenerów:

# 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

Następnie oznaczamy serwery, co jest potrzebne do przypisania niektórych usług do serwerów:

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

Następnie tworzymy katalogi do przechowywania danych etcd, magazynu KV, który jest potrzebny dla Traefik i Stolon. Podobnie jak PostgreSQL będą to kontenery przypisane do serwerów, dlatego tę komendę wykonujemy na wszystkich serwerach:

# mkdir -p /srv/etcd

Następnie tworzymy plik do konfiguracji etcd i stosujemy go:

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

Po pewnym czasie sprawdzamy, czy klaster etcd został uruchomiony:

# 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

Tworzymy katalogi dla Postgresql, komendę wykonujemy na wszystkich serwerach:

# mkdir -p /srv/pgsql

Następnie tworzymy plik do konfiguracji Postgresql:

01pgsql.yml

wersja: '3.7'

usługi:
  pgsentinel:
    obraz: sorintlab/stolon:master-pg10
    polecenie:
      - 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
    sieci:
      - etcd
      - pgsql
    wdrażanie:
      repliki: 3
      konfiguracja_aktualizacji:
        równoległość: 1
        opóźnienie: 30s
        kolejność: zatrzymaj-najpierw
        akcja_na_błąd: wstrzymaj
  pgkeeper1:
    obraz: sorintlab/stolon:master-pg10
    nazwa_hosta: pgkeeper1
    polecenie:
      - 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
    sieci:
      - etcd
      - pgsql
    środowisko:
      - PGDATA=/var/lib/postgresql/data
    wolumeny:
      - pgkeeper1:/var/lib/postgresql/data
    sekrety:
      - pgsql
      - pgsql_repl
    wdrażanie:
      repliki: 1
      umiejscowienie:
        ograniczenia: [node.labels.nodename == node1]
  pgkeeper2:
    obraz: sorintlab/stolon:master-pg10
    nazwa_hosta: pgkeeper2
    polecenie:
      - 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
    sieci:
      - etcd
      - pgsql
    środowisko:
      - PGDATA=/var/lib/postgresql/data
    wolumeny:
      - pgkeeper2:/var/lib/postgresql/data
    sekrety:
      - pgsql
      - pgsql_repl
    wdrażanie:
      repliki: 1
      umiejscowienie:
        ograniczenia: [node.labels.nodename == node2]
  pgkeeper3:
    obraz: sorintlab/stolon:master-pg10
    nazwa_hosta: pgkeeper3
    polecenie:
      - 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
    sieci:
      - etcd
      - pgsql
    środowisko:
      - PGDATA=/var/lib/postgresql/data
    wolumeny:
      - pgkeeper3:/var/lib/postgresql/data
    sekrety:
      - pgsql
      - pgsql_repl
    wdrażanie:
      repliki: 1
      umiejscowienie:
        ograniczenia: [node.labels.nodename == node3]
  postgresql:
    obraz: sorintlab/stolon:master-pg10
    polecenie: 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
    sieci:
      - etcd
      - pgsql
    wdrażanie:
      repliki: 3
      konfiguracja_aktualizacji:
        równoległość: 1
        opóźnienie: 30s
        kolejność: zatrzymaj-najpierw
        akcja_na_błąd: cofnięcie

wolumeny:
  pgkeeper1:
    sterownik: lokalny
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/pgsql"
  pgkeeper2:
    sterownik: lokalny
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/pgsql"
  pgkeeper3:
    sterownik: lokalny
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/pgsql"

sekrety:
  pgsql:
    plik: "/srv/docker/postgres"
  pgsql_repl:
    plik: "/srv/docker/replica"

sieci:
  etcd:
    zewnętrzny: true
  pgsql:
    zewnętrzny: true

Generujemy sekrety, używając pliku:

# </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

Po pewnym czasie (sprawdzamy wynik komendy docker service ls, co oznacza, że wszystkie usługi zostały uruchomione) inicjujemy klaster Postgresql:

# 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

Sprawdzamy gotowość klastra Postgresql:

# 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

Konfigurujemy traefik, aby otworzyć dostęp do kontenerów z zewnątrz:

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

Uruchamiamy klaster Redis, tworząc katalog na wszystkich węzłach do przechowywania:

# 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

Dodajemy rejestr Docker:

06registry.yml

wersja: '3.7'

usługi:
  rejestr:
    obraz: registry:2.6
    sieci:
      - traefik
    wolumeny:
      - registry_data: /var/lib/registry
    wdrożenie:
      repliki: 1
      lokalizacja:
        ograniczenia: [node.role == manager]
      polityka_ponownego_uruchomienia:
        warunek: on-failure
      etykiety:
        - traefik.enable=true
        - traefik.http.routers.registry.rule=Host(`registry.example.com`)
        - traefik.http.services.registry.loadbalancer.server.port=5000
        - traefik.docker.network=traefik

wolumeny:
  registry_data:
    sterownik: local
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/docker/registry"

sieci:
  traefik:
    zewnętrzna: true

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

A na koniec — GitLab:

08gitlab-runner.yml

wersja: '3.7'

usługi:
  gitlab:
    obraz: gitlab/gitlab-ce:latest
    sieci:
      - pgsql
      - redis
      - traefik
      - gitlab
    porty:
      - 22222:22
    środowisko:
      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
    wolumeny:
      - gitlab_conf:/etc/gitlab
      - gitlab_logs:/var/log/gitlab
      - gitlab_data:/var/opt/gitlab
    wdrożenie:
      tryb: replicated
      repliki: 1
      lokalizacja:
        ograniczenia:
        - node.role == manager
      etykiety:
        - 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:
    obraz: gitlab/gitlab-runner:latest
    sieci:
      - gitlab
    wolumeny:
      - gitlab_runner_conf:/etc/gitlab
      - /var/run/docker.sock:/var/run/docker.sock
    wdrożenie:
      tryb: replicated
      repliki: 1
      lokalizacja:
        ograniczenia:
        - node.role == manager

wolumeny:
  gitlab_conf:
    sterownik: local
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/docker/gitlab/conf"
  gitlab_logs:
    sterownik: local
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/docker/gitlab/logs"
  gitlab_data:
    sterownik: local
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/docker/gitlab/data"
  gitlab_runner_conf:
    sterownik: local
    opcje_sterownika:
      typ: none
      o: bind
      urządzenie: "/srv/docker/gitlab/runner"

sieci:
  pgsql:
    zewnętrzna: true
  redis:
    zewnętrzna: true
  traefik:
    zewnętrzna: true
  gitlab:
    zewnętrzna: 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

Stan końcowy klastra i usług:

# 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

Co jeszcze można poprawić? Należy skonfigurować w Traefik działanie kontenerów przez https, dodać szyfrowanie tls dla Postgresql i Redis. Generalnie jednak możemy już przekazać to programistom jako PoC. Przyjrzyjmy się teraz alternatywom dla Dockera.

Podman

Kolejny dość znany silnik do uruchamiania kontenerów, pogrupowanych w podach (pods, grupy kontenerów uruchamianych wspólnie). W przeciwieństwie do Dockera, nie wymaga żadnej usługi do uruchamiania kontenerów, cała praca odbywa się za pomocą biblioteki libpod. Również napisany w Go, potrzebuje zgodnego z OCI runtime do uruchamiania kontenerów, na przykład runC.

Docker i wszystko, wszystko, wszystko

Praca z Podmanem w zasadzie przypomina pracę z Dockerem, aż do tego, że można to zrobić (zgodnie z deklaracjami wielu próbujących, w tym autora tego artykułu):

$ alias docker=podman

i można kontynuować pracę. Ogólnie sytuacja z Podmanem jest dość interesująca, ponieważ jeśli wcześniejsze wersje Kubernetes działały z Dockerem, to mniej więcej od 2015 roku, po ustandaryzowaniu świata kontenerów (OCI - Open Container Initiative) i podziale Dockera na containerd i runC, rozwija się alternatywa Dockera do uruchamiania w Kubernetes: CRI-O. W tym kontekście Podman jest alternatywą dla Dockera, zbudowaną zgodnie z zasadami Kubernetes, w tym grupowaniem kontenerów, ale głównym celem istnienia projektu jest uruchamianie kontenerów w stylu Dockera bez dodatkowych usług. Z oczywistych powodów brak trybu swarm, ponieważ deweloperzy wyraźnie mówią, że jeśli potrzebny jest klaster — należy brać Kubernetes.

Instalacja

Aby zainstalować w Centos 7, wystarczy aktywować repozytorium Extras, a następnie zainstalować wszystko poleceniem:

# yum -y install podman

Inne możliwości

Podman może generować jednostki dla systemd, tym samym rozwiązując problem uruchamiania kontenerów po ponownym uruchomieniu serwera. Dodatkowo zadeklarowano poprawne działanie systemd jako pid 1 w kontenerze. Do budowy kontenerów używa się osobnego narzędzia buildah, są też zewnętrzne narzędzia — odpowiedniki docker-compose, które generują również pliki konfiguracyjne, zgodne z Kubernetes, co znacznie ułatwia przejście z Podmana na Kubernetes.

Praca z Podmanem

Ponieważ nie ma trybu swarm (zakłada się przejście na Kubernetes, jeśli potrzebny jest klaster) — będziemy zbierać osobnymi kontenerami.

Instalujemy podman-compose:

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

Wynikowy plik konfiguracyjny dla podmana nieco się różni, tak przynajmniej musiałem przenieść osobną sekcję volumes bezpośrednio do sekcji z usługami.

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

Wynik działania:

# 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

Zobaczmy, co wygeneruje dla systemd i kubernetes. W tym celu musimy poznać nazwę lub id podu:

# 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

Niestety, poza uruchamianiem kontenerów, wygenerowana jednostka dla systemd nie robi nic więcej (na przykład nie czyści starych kontenerów przy ponownym uruchomieniu takiej usługi), dlatego takie rzeczy trzeba będzie dopisać samodzielnie.

W zasadzie Podman wystarczy, aby spróbować, czym są kontenery, przenieść stare konfiguracje dla docker-compose, a następnie przejść do Kubernetes, jeśli potrzebujemy klastra, lub uzyskać prostszą w obsłudze alternatywę dla Dockera.

rkt

Projekt został zarchiwizowany około pół roku temu, ponieważ został zakupiony przez RedHat, dlatego nie będę się na nim zatrzymywał dłużej. W całości pozostawiał dość dobre wrażenie, jednak w porównaniu do Dockera, a tym bardziej do Podmana, wygląda na kombajn. Istniał również dystrybucja CoreOS, oparta na rkt (chociaż pierwotnie mieli Dockera), jednak jego wsparcie również zakończyło się po zakupie przez RedHat.

Plash

Więcej jeden projekt, którego autor chciał po prostu zbierać i uruchamiać kontenery. Sądząc po dokumentacji i kodzie — autor nie przestrzegał standardów, a po prostu postanowił napisać swoją implementację, co w zasadzie zrobił.

Wnioski

Sytuacja przy posiadaniu Kubernetes jest bardzo interesująca: z jednej strony z Dockerem można zbudować klaster (w trybie swarm), z którym nawet można uruchamiać środowiska produkcyjne dla klientów, co jest szczególnie istotne dla małych zespołów (3-5 osób) lub przy niewielkim ogólnym obciążeniu, albo braku chęci zagłębiania się w szczegóły konfiguracji Kubernetes, w tym dla wysokich obciążeń.

Podman nie zapewnia pełnej kompatybilności, ale ma jedną istotną zaletę — kompatybilność z Kubernetes, w tym dodatkowe narzędzia (buildah i inne). Dlatego do wyboru narzędzia do pracy podchodzę w ten sposób: dla małych zespołów lub przy ograniczonym budżecie — Docker (z możliwym trybem swarm), do osobistego rozwoju na lokalnym localhost — Podman z kolegami, a dla wszystkich innych — Kubernetes.

Nie jestem pewien, czy sytuacja z Dockerem w przyszłości się nie zmieni, w końcu są pionierami, a także krok po kroku się standardyzują, ale przyszłość Podmana, przy wszystkich jego wadach (działa tylko na Linuxie, brak klastrowania, budowa i inne działania — zewnętrznymi rozwiązaniami), jest bardziej klarowna, dlatego zapraszam wszystkich chętnych do dyskusji na ten temat w komentarzach.

P.S. 3 sierpnia uruchamiamy „Kurs wideo po Dockerze”, gdzie będzie można bliżej poznać jego działanie. Przeanalizujemy wszystkie jego narzędzia: od podstawowych abstrakcji po parametry sieci, niuanse pracy z różnymi systemami operacyjnymi i językami programowania. Poznasz technologię i zrozumiesz, gdzie i jak najlepiej używać Dockera. Podzielimy się również najlepszymi praktykami.

Cena przedsprzedaży przed wydaniem: 5000 zł. Z programem „Kursu wideo o Dockerze” można zapoznać się na stronie kursu.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster