TL;DR: обзорная статья-руководство по сравнению сред для запуска приложений в контейнерах. Будут рассмотрены возможности Docker и других схожих систем.



Немножко истории, откуда все взялось

История


Первым общеизвестным способом изоляции приложения является chroot. Одноименный системный вызов обеспечивает изменение корневого каталога — таким образом обеспечивая доступ программе, его вызвавшей, доступ только к файлам внутри этого каталога. Но если программе внутри дать права суперпользователя, потенциально она может «убежать» из chroot и получить доступ к основной операционной системе. Также кроме смены корневого каталога не ограничиваются другие ресурсы (оперативная память, процессор), а также доступ к сети.


Следующий способ — запуск полноценной операционной системы внутри контейнера, за счет механизмов ядра операционной системы. В различных операционных системах этот способ называют по-разному, но суть одна и та же — запуск нескольких независимых операционных систем, каждая из которых работает с тем же ядром, на котором работает и основная операционная система. Сюда относятся FreeBSD Jails, Solaris Zones, OpenVZ и LXC для Linux. Обеспечивается изоляция не только по дисковому пространству, но и другим ресурсам, в частности каждый контейнер может иметь ограничения по процессорному времени, оперативной памяти, полосе пропускания сети. По сравнению с chroot выйти из контейнера сложнее, поскольку суперпользователь в контейнере обладает доступом только к начинке контейнера, однако из-за необходимости поддерживать операционную систему внутри контейнера в актуальном состоянии и использования старых версий ядер (актуально для Linux, в меньшей мере FreeBSD) есть ненулевая вероятность «пробития» системы изоляции ядра и получения доступа к основной операционной системе.


Вместо запуска полноценной операционной системы в контейнере (с системой инициализации, пакетным менеджером и т.п.) можно запускать сразу же приложения, главное — обеспечить приложениям такую возможность (наличие необходимых библиотек и прочих файлов). Эта идея и послужила основой для контейнерной виртуализации приложений, наиболее ярким и общеизвестным представителем которой является Docker. По сравнению с предыдущими системами более гибкие механизмы изоляции совместно с встроенной поддержкой виртуальных сетей между контейнерами и с отслеживанием состояния приложения внутри контейнера дали в результате возможность построения единой целостной среды из большого числа физических серверов для запуска контейнеров — без необходимости ручного управления ресурсами.


Docker


Docker — это наиболее известное ПО для контейнеризации приложений. Написан на языке Go, использует штатные возможности ядра Linux — cgroups, namespaces, capabilities и т.п., а также файловые системы Aufs и другие подобные для экономии дискового пространства.



Источник: wikimedia


Архитектура


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



Установка и настройка


Моим любимым способом установки 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 для сборки и настройки контейнеров и прочих связанных вещей (например сети, постоянные файловые системы для хранения данных).


Также с его помощью можно организовать конвейеры для CI\CD. Другая интересная возможность — работа в кластерном режиме, так называемый swarm mode (до версии 1.12 был известен как docker swarm), позволяющая собрать из нескольких серверов единую инфраструктуру для запуска контейнеров. Имеется поддержка виртуальной сети поверх всех серверов, есть наличие встроенного балансировщика нагрузки, а также поддержка секретов для контейнеров.


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


Работа с 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
L-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'] = "noreply@example.com"
        gitlab_rails['smtp_password'] = "xxxxxxxxx"
        gitlab_rails['smtp_domain'] = "example.com"
        gitlab_rails['gitlab_email_from'] = 'noreply@example.com'
        gitlab_rails['smtp_authentication'] = "login"
        gitlab_rails['smtp_tls'] = true
        gitlab_rails['smtp_enable_starttls_auto'] = true
        gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
        external_url 'http://gitlab.example.com/'
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    volumes:
      - gitlab_conf:/etc/gitlab
      - gitlab_logs:/var/log/gitlab
      - gitlab_data:/var/opt/gitlab
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager
      labels:
        - traefik.enable=true
        - traefik.http.routers.gitlab.rule=Host(`gitlab.example.com`)
        - traefik.http.services.gitlab.loadbalancer.server.port=80
        - traefik.docker.network=traefik
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    networks:
      - gitlab
    volumes:
      - gitlab_runner_conf:/etc/gitlab
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager

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

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

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

Итоговое состояние кластера и сервисов:


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


Podman


Еще один достаточно известный engine для запуска контейнеров, сгруппированные по подам (pods, группы контейнеров, развернутых совместно). В отличие от Docker не требует какого-либо сервиса для запуска контейнеров, вся работа производится через библиотеку libpod. Также написан на Go, нуждается в OCI-совместимом runtime для запуска контейнеров, например runC.



Работа с 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.


rkt


Проект ушел в архив примерно полгода назад из-за того, что его купил RedHat, поэтому не буду останавливаться на нем детальнее. В целом он оставлял весьма неплохое впечатление, однако по сравнению с Docker и тем более с Podman выглядит комбайном. Существовал также дистрибутив CoreOS, построенный на основе rkt (хотя у них изначально был Docker), однако его поддержка также окончилась после покупки RedHat.


Plash


Еще один проект, автор которого хотел просто собирать и запускать контейнеры. Судя по документации и коду — автор не следовал стандартам, а просто решил написать свою реализацию, что в принципе и сделал.


Выводы


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


Podman не обеспечивает полной совместимости, но у него есть одно важное преимущество — совместимость с Kubernetes, в том числе и по дополнительным инструментам (buildah и прочие). Поэтому к выбору инструмента для работы я буду подходить так: для малых команд, либо при ограниченном бюджете — Docker (с возможным swarm mode), для разработки для себя на личном localhost — Podman сотоварищи, а всем остальным — Kubernetes.


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


P.S. 3 августа запускаем «Видеокурс по Docker», где можно будет подробнее узнать о его работе. Мы разберем все его инструменты: от основных абстракций до параметров сети, нюансов работы с различными ОС и языками программирования. Вы познакомитесь с технологией и поймете, где и как лучше использовать Docker. Также поделимся best practice кейсами.


Стоимость предзаказа до релиза: 5000 р. С программой «Видеокурса по Docker» можно ознакомиться на странице курса.