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.

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.

Ź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.

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.ioPo 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 --permanentDodatkowo 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ć
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ą. , 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/fstabPo 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 dockerNastępnie należy zamontować otrzymany wolumin (komenda musi być wykonana na wszystkich serwerach):
# mount /srv/dockerKonfiguracja 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 managerKopiujemy 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 gitlabNastę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 node3Nastę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/etcdNastę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 etcdPo 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 healthyTworzymy katalogi dla Postgresql, komendę wykonujemy na wszystkich serwerach:
# mkdir -p /srv/pgsqlNastę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: trueGenerujemy 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 pgsqlPo 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 initSprawdzamy 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 traefikUruchamiamy klaster Redis, tworząc katalog na wszystkich węzłach do przechowywania:
# mkdir -p /srv/redis05redis.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 redisDodajemy 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 registryA 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 gitlabStan 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/tcpCo 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.

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=podmani 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 podmanInne 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-composeWynikowy 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 upWynik 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_1Zobaczmy, 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 db40ab8bf84bKubernetes:
# 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.targetNiestety, 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 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 , 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 „”, 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ę .
Źródło: habr.com
