Docker і ўсё, усё, усё

TL;DR: аглядная артыкул-кіраўніцтва ў параўнанні асяроддзяў для запуску прыкладанняў у кантэйнерах. Будуць разгледжаны магчымасці Docker і іншых падобных сістэм.

Docker і ўсё, усё, усё

Крыху гісторыі, адкуль усё ўзялося

Гісторыя

Першым агульнавядомым спосабам ізаляцыі прыкладання з'яўляецца chroot. Аднайменны сістэмны выклік забяспечвае змену каранёвага каталога - такім чынам забяспечваючы доступ праграме, яго якая выклікала, доступ толькі да файлаў усярэдзіне гэтага каталога. Але калі праграме ўнутры даць правы суперкарыстальніка, патэнцыйна яна можа "ўцячы" з chroot і атрымаць доступ да асноўнай аперацыйнай сістэмы. Таксама акрамя змены каранёвага каталога не абмяжоўваюцца іншыя рэсурсы (аператыўная памяць, працэсар), а таксама доступ да сеткі.

Наступны спосаб - запуск паўнавартаснай аперацыйнай сістэмы ўнутры кантэйнера, за кошт механізмаў ядра аперацыйнай сістэмы. У розных аперацыйных сістэмах гэты спосаб называюць па-рознаму, але сутнасць адна і тая ж - запуск некалькіх незалежных аперацыйных сістэм, кожная з якіх працуе з тым жа ядром, на якім працуе і асноўная аперацыйная сістэма. Сюды адносяцца FreeBSD Jails, Solaris Zones, OpenVZ і LXC для Linux. Забяспечваецца ізаляцыя не толькі па дыскавай прасторы, але і іншым рэсурсам, у прыватнасці кожны кантэйнер можа мець абмежаванні па працэсарным часе, аператыўнай памяці, паласе прапускання сеткі. У параўнанні з chroot выйсці з кантэйнера складаней, паколькі суперкарыстальнік у кантэйнеры валодае доступам толькі да начыння кантэйнера, аднак з-за неабходнасці падтрымліваць аперацыйную сістэму ўсярэдзіне кантэйнера ў актуальным стане і выкарыстанні старых версій ядраў (актуальна для Linux, у меншай меры FreeBSD) ёсць ненулявая верагоднасць «прабіцця» сістэмы ізаляцыі ядра і атрыманні доступу да асноўнай аперацыйнай сістэмы.

Замест запуску паўнавартаснай аперацыйнай сістэмы ў кантэйнеры (з сістэмай ініцыялізацыі, пакетным мэнэджарам і да т.п.) можна запускаць адразу ж прыкладанні, галоўнае - забяспечыць прыкладанням такую ​​магчымасць (наяўнасць неабходных бібліятэк і іншых файлаў). Гэта ідэя і паслужыла асновай для кантэйнернай віртуалізацыі прыкладанняў, найболей яркім і агульнавядомым прадстаўніком якой з'яўляецца Docker. У параўнанні з папярэднімі сістэмамі больш гнуткія механізмы ізаляцыі сумесна з убудаванай падтрымкай віртуальных сетак паміж кантэйнерамі і з адсочваннем стану прыкладання ўнутры кантэйнера далі ў выніку магчымасць пабудовы адзінага цэласнага асяроддзя з вялікай колькасці фізічных сервераў для запуску кантэйнераў – без неабходнасці ручнога кіравання рэсурсамі.

Докер

Docker гэта найбольш вядомае ПЗ для кантэйнерызацыі прыкладанняў. Напісаны на мове Go, выкарыстоўвае штатныя магчымасці ядра Linux - cgroups, namespaces, capabilities і да т.п., а таксама файлавыя сістэмы Aufs і іншыя падобныя для эканоміі дыскавай прасторы.

Docker і ўсё, усё, усё
Крыніца: wikimedia

Архітэктура

Да версіі 1.11 Docker працаваў у выглядзе адзінага сэрвісу, які ажыццяўляў усе аперацыі з кантэйнерамі: спампоўка вобразаў для кантэйнераў, запуск кантэйнераў, апрацоўку запытаў па API. Пачынальна з версіі 1.11 Docker разбілі на некалькі частак, якія ўзаемадзейнічаюць паміж сабой: containerd, для апрацоўкі ўсяго жыццёвага цыклу кантэйнераў (вылучэнне дыскавай прасторы, запампоўка выяў, праца з сеткай, запуск, усталёўка і назіранне за станам кантэйнераў) і runC, асяроддзі выканання кантэйнераў, заснаванай на выкарыстанні cgroups і іншых магчымасцяў ядра Linux. Сам сэрвіс docker застаўся, але зараз ён служыць толькі для апрацоўкі запытаў па API, якія транслююцца ў containerd.

Docker і ўсё, усё, усё

Ўстаноўка і настройка

Маім каханым спосабам усталёўкі docker з'яўляецца docker-machine, які акрамя непасрэдна ўсталёўкі і налады docker на выдаленыя серверы (уключаючы розныя аблокі) дае магчымасць працы з файлавымі сістэмамі выдаленых сервераў, а таксама можа вырабляць запуск розных каманд.

Аднак з 2018 года праект амаль не развіваецца, таму ўстаноўку будзем вырабляць штатным для большасці дыстрыбутываў Linux спосабам - даданнем рэпазітара і ўстаноўкай неабходных пакетаў.

Таксама гэты спосаб ужываецца і пры аўтаматызаванай усталёўцы, напрыклад з дапамогай Ansible ці іншых падобных сістэм, але ў гэтым артыкуле я яго разглядаць не буду.

Устаноўка будзе вырабляцца на Centos 7, у якасці сервера я буду выкарыстоўваць віртуальную машыну, для ўстаноўкі дастаткова выканаць каманды ніжэй:

# 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

Пасля ўстаноўкі трэба запусціць сэрвіс, паставіць яго ў аўтазагрузку:

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

Дадаткова можна стварыць групу docker, карыстачы якой змогуць працаваць з docker без sudo, наладзіць часопісаванне, уключыць доступ да API звонку, не забыцца больш дакладна наладзіць firewall (забаронена ўсё, што не дазволена, у прыкладах вышэй і ніжэй - я гэта апусціў для прастаты і нагляднасці), але тут я больш падрабязна не буду спыняцца.

іншыя магчымасці

Акрамя вышэйназванай docker machine яшчэ ёсць docker registry, сродак для захоўвання выяў для кантэйнераў, а таксама docker compose - сродак для аўтаматызацыі разгортвання прыкладанняў у кантэйнерах, выкарыстоўваюцца файлы YAML для зборкі і налады кантэйнераў і іншых звязаных рэчаў (напрыклад сеткі, пастаянныя файлавыя сістэмы для захоўвання дадзеных).

Таксама з яго дапамогай можна арганізаваць канвееры для CICD. Іншая цікавая магчымасць – праца ў кластарным рэжыме, так званы swarm mode (да версіі 1.12 быў вядомы як docker swarm), якая дазваляе сабраць з некалькіх сервераў адзіную інфраструктуру для запуску кантэйнераў. Маецца падтрымка віртуальнай сеткі па-над усімі серверамі, ёсць наяўнасць убудаванага балансавальніка нагрузкі, а таксама падтрымка сакрэтаў для кантэйнераў.

Файлы YAML ад docker compose з невялікімі зменамі могуць быць скарыстаны для такіх кластараў, цалкам аўтаматызуючы абслугоўванне малых і сярэдніх кластараў для розных мэт. Для вялікіх кластараў пераважней выкарыстоўваць Kubernetes, паколькі выдаткі на абслугоўванне swarm mode могуць перасягнуць такія з Kubernetes. Акрамя runC у якасці асяроддзя выканання кантэйнераў можна ўсталяваць напрыклад Кантэйнеры для ката

Праца з Docker

Пасля ўсталёўкі і налады паспрабуем сабраць кластар, у якім разгарнем GitLab і Docker Registry для каманды распрацоўнікаў. У якасці сервераў я буду выкарыстоўваць тры віртуальныя машыны, на якіх дадаткова разгарну размеркаваную ФС GlusterFS, яе я буду выкарыстоўваць у якасці сховішчы docker volumes, напрыклад для запуску адмоваўладкавальнай версіі docker registry. Ключавыя кампаненты для запуску: Docker Registry, Postgresql, Redis, GitLab з падтрымкай GitLab Runner па-над Swarm. Postgresql будзем запускаць з кластэрызацыяй Stolon, таму для захоўвання дадзеных Postgresql не трэба выкарыстоўваць GlusterFS. Астатнія крытычныя дадзеныя будуць захоўвацца на GlusterFS.

Для разгортвання GlusterFS на ўсіх серверах (яны завуцца node1, node2, node3) трэба ўсталяваць пакеты, дазволіць працу firewall, стварыць патрэбныя каталогі:

# 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

Пасля ўсталёўкі працу па наладзе GlusterFS трэба працягваць з аднаго вузла, напрыклад 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

Затым трэба змантаваць атрыманы volume (каманду трэба выканаць на ўсіх серверах):

# mount /srv/docker

Настройка swarm mode робіцца на адным з сервераў, які будзе Leader, астатнія павінны будуць далучацца да кластара, таму вынік выканання каманды на першым серверы трэба будзе скапіяваць і выканаць на астатніх.

Першасная налада кластара, каманду запускаю на 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

Капіяваны вынік другой каманды, выконваем на node2 і node3:

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

На гэтым папярэдняя настройка сервераў скончана, прыступаем да налады сэрвісаў, каманды для выканання будуць запускацца з node1, калі не пазначана інакш.

Перш за ўсё створым сеткі для кантэйнераў:

# 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

Затым пазначаем сервера, гэта трэба для прывязкі некаторых сэрвісаў да сервераў:

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

Далей ствараем каталогі для захоўвання дадзеных etcd, KV-сховішчы, якое трэба для Traefik і Stolon. Аналагічна Postgresql гэта будуць прывязаныя да сервераў кантэйнеры, таму гэтую каманду выконваем на ўсіх серверах:

# mkdir -p /srv/etcd

Далей ствараем файл для налады etcd і ўжываем яго:

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

Праз некаторы час правяраем, што падняўся etcd кластар:

# 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

Ствараем каталогі для Postgresql, каманду выконваем на ўсіх серверах:

# mkdir -p /srv/pgsql

Далей ствараем файл для налады 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

Генеруем сакрэты, ужывальны файл:

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

Праз некаторы час (глядзім выснову каманды docker service ls, што падняліся ўсе сэрвісы) ініцыялізуем кластар 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

Правяраем гатоўнасць кластара 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

Наладжваем traefik для адкрыцця доступу да кантэйнераў звонку:

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

Запускаем Redis Cluster, для гэтага ствараем на ўсіх вузлах каталог для захоўвання:

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

Дадаем Docker Registry:

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

Ну і нарэшце – 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'] = "[email protected]"
        gitlab_rails['smtp_password'] = "xxxxxxxxx"
        gitlab_rails['smtp_domain'] = "example.com"
        gitlab_rails['gitlab_email_from'] = '[email protected]'
        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

Выніковы стан кластара і сэрвісаў:

# 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

Што яшчэ можна палепшыць? Абавязкова наладзіць у Traefik працу кантэйнераў па https, дадаць шыфраванне tls для Postgresql і Redis. Але ў цэлым ужо можна аддаваць распрацоўшчыкам у якасці PoC. Паглядзім зараз альтэрнатывы Docker.

Падман

Яшчэ адзін дастаткова вядомы engine для запуску кантэйнераў, згрупаваныя па падах (pods, групы кантэйнераў, разгорнутых сумесна). У адрозненне ад Docker не патрабуе які-небудзь сэрвісу для запуску кантэйнераў, уся праца вырабляецца праз бібліятэку libpod. Таксама напісаны на Go, мае патрэбу ў OCI-сумяшчальным runtime для запуску кантэйнераў, напрыклад runC.

Docker і ўсё, усё, усё

Праца з Podman у цэлым нагадвае такую ​​для Docker, аж да таго, што можна зрабіць так (заяўлена ў шматлікіх якія паспрабавалі, у тым ліку і аўтарам гэтага артыкула):

$ alias docker=podman

і можна працягваць працаваць. У цэлым сітуацыя з Podman вельмі цікавая, бо калі раннія версіі Kubernetes працавалі з Docker, то прыкладна з 2015 года, пасля стандартызацыі свету кантэйнераў (OCI – Open Container Initiative) і падзелу Docker на containerd і runC, развіваецца альтэрнатыва Docker для запуску ў Kubernetes: CRI-O. Podman у гэтым плане з'яўляецца альтэрнатывай Docker, пабудаванай па прынцыпах Kubernetes, у тым ліку і па групоўцы кантэйнераў, але асноўная мэта існавання праекта – запуск кантэйнераў у стылі Docker без дадатковых сэрвісаў. Па зразумелых прычынах няма наяўнасці swarm mode, так як распрацоўшчыкі відавочна кажуць аб тым, што калі трэба кластар – бярыце Kubernetes.

Ўстаноўка

Для ўстаноўкі ў Centos 7 дастаткова актываваць рэпазітар Extras, пасля чаго ўсталяваць усе камандай:

# yum -y install podman

іншыя магчымасці

Podman можа генераваць юніты для systemd, такім чынам вырашаючы задачу запуску кантэйнераў пасля перазагрузкі сервера. Дадаткова заяўлена карэктная праца systemd у якасці pid 1 у кантэйнеры. Для зборкі кантэйнераў ідзе асобная прылада buildah, ёсць таксама іншыя прылады — аналогі docker-compose, генеравальны ў тым ліку канфігурацыйныя файлы, сумяшчальныя з Kubernetes, так што пераход з Podman на Kubernetes спрошчаны наколькі гэта магчыма.

Праца з Podman

Паколькі няма swarm mode (мяркуецца пераход на Kubernetes, калі трэба кластар) - збіраць будзем асобнымі кантэйнерамі.

Усталёўваны podman-compose:

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

Выніковы канфігурацыйны файл для podman крыху адрозніваецца, так да прыкладу прыйшлося перанесці асобную секцыю volumes напроста ў секцыю з сэрвісамі.

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

Вынік працы:

# 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

Давайце паглядзім, што ён згенеруе для systemd і kubernetes, для гэтага трэба пазнаць імя або id пода:

# 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

Нажаль акрамя запуску кантэйнераў згенераваны юніт для systemd больш нічога не робіць (напрыклад зачыстку старых кантэйнераў пры перазапуску такога сэрвісу), таму такія рэчы прыйдзецца дапісваць самастойна.

У прынцыпе Podman дастаткова для таго, каб паспрабаваць, што такое кантэйнеры, перанесці старыя канфігурацыі для docker-compose, пасля чаго сысці ў бок Kubernetes, калі трэба на кластар, альбо атрымаць прасцейшую ў працы альтэрнатыву Docker.

РКТ

праект сышоў у архіў прыкладна паўгода таму з-за таго, што яго купіў RedHat, таму не буду спыняцца на ім падрабязней. У цэлым ён пакідаў вельмі нядрэннае ўражанне, аднак у параўнанні з Docker і тым больш з Podman выглядае камбайнам. Існаваў таксама дыстрыбутыў CoreOS, пабудаваны на аснове rkt (хоць у іх першапачаткова быў Docker), аднак яго падтрымка таксама скончылася пасля пакупкі RedHat.

Плаш

Яшчэ адзін праект, аўтар якога хацеў проста збіраць і запускаць кантэйнеры. Мяркуючы па дакументацыі і кодзе - аўтар не прытрымліваўся стандартаў, а проста вырашыў напісаць сваю рэалізацыю, што ў прынцыпе і зрабіў.

Высновы

Сітуацыя пры наяўнасці Kubernetes складаецца вельмі цікавая: з аднаго боку з Docker можна сабраць кластар (у swarm mode), з якім нават можна запускаць прадуктовыя асяроддзі для кліентаў, гэта асабліва актуальна для невялікіх каманд (3-5 чалавек), або пры невялікай агульнай нагрузцы , ці ж адсутнасці жадання разбірацца ў тонкасцях налады Kubernetes у тым ліку і для высокіх нагрузак.

Podman не забяспечвае поўнай сумяшчальнасці, але ў яго ёсць адна важная перавага – сумяшчальнасць з Kubernetes, у тым ліку і па дадатковых прыладах (buildah і іншыя). Таму да выбару прылады для працы я буду падыходзіць так: для малых каманд, альбо пры абмежаваным бюджэце – Docker (з магчымым swarm mode), для распрацоўкі для сябе на асабістым localhost – Podman і іншыя, а ўсім астатнім – Kubernetes.

Я не ўпэўнены, што сітуацыя з Docker не памяняецца ў будучыні, усёткі яны з'яўляюцца піянерамі, а таксама крок за крокам паволі стандартуюцца, але ў Podman пры ўсіх яго недахопах (праца толькі на Linux, няма кластарызацыі, зборка і іншыя дзеянні - іншымі рашэннямі) будучыня больш ясная, таму я запрашаю ўсіх жадаючых абмеркаваць дадзеныя высновы ў каментарах.

PS 3 жніўня запускаем «Відэакурс па Docker», дзе можна будзе падрабязней даведацца пра яго працу. Мы разбяром усе яго прылады: ад асноўных абстракцый да параметраў сеткі, нюансаў працы з рознымі АС і мовамі праграмавання. Вы пазнаёміцеся з тэхналогіяй і зразумееце, дзе і як лепш выкарыстоўваць Docker. Таксама падзелімся best practice кейсамі.

Кошт папярэдняга заказу да рэлізу: 5000 р. З праграмай "Відэакурсу па Docker" можна азнаёміцца на старонцы курса.

Крыніца: habr.com

Дадаць каментар