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 извне, но тут я более подробно не буду останавливаться.
Другие возможности
Кроме вышеназванной 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 initSwarm 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:2377To 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:2377This 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 и применяем его:
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 listade526d28b1f92f7: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://etcd1:2379 isLeader=falsebd388e7810915853: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://etcd3:2379 isLeader=falsed282ac2ce600c1ce: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://etcd2:2379 isLeader=true# docker exec $(docker ps | awk '/etcd/ {print $1}') etcdctl cluster-healthmember ade526d28b1f92f7 is healthy: got healthy result from http://etcd1:2379member bd388e7810915853 is healthy: got healthy result from http://etcd3:2379member d282ac2ce600c1ce is healthy: got healthy result from http://etcd2:2379cluster is healthy
Создаем каталоги для Postgresql, команду выполняем на всех серверах:
# mkdir -p /srv/pgsql
Далее создаем файл для настройки Postgresql:
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: rollbackvolumes: 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 LEADER26baa11d false74e98768 falsea8cb002b true=== Active proxies ===ID4d2338269f562f3bb0c79ff1=== Keepers ===UID HEALTHY PG LISTENADDRESS PG HEALTHY PG WANTEDGENERATION PG CURRENTGENERATIONpgkeeper1 true pgkeeper1:5432 true 2 2pgkeeper2 true pgkeeper2:5432 true 2 2pgkeeper3 true pgkeeper3:5432 true 3 3=== Cluster Info ===Master Keeper: pgkeeper3===== Keepers/DB tree =====pgkeeper3 (master)pgkeeper2pgkeeper1
Настраиваем traefik для открытия доступа к контейнерам извне:
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=traefiknetworks: traefik: external: true
# docker stack deploy --compose-file 03traefik.yml traefik
Запускаем Redis Cluster, для этого создаем на всех узлах каталог для хранения:
# mkdir -p /srv/redis
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: anyvolumes: 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:
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=traefikvolumes: 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:
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' 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 == managervolumes: 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 lsID NAME MODE REPLICAS IMAGE PORTSlef9n3m92buq etcd_etcd1 replicated 1/1 quay.io/coreos/etcd:latestij6uyyo792x5 etcd_etcd2 replicated 1/1 quay.io/coreos/etcd:latestfqttqpjgp6pp etcd_etcd3 replicated 1/1 quay.io/coreos/etcd:latesthq5iyga28w33 gitlab_gitlab replicated 1/1 gitlab/gitlab-ce:latest *:22222->22/tcpdt7s6vs0q4qc gitlab_gitlab-runner replicated 1/1 gitlab/gitlab-runner:latestk7uoezno0h9n pgsql_pgkeeper1 replicated 1/1 sorintlab/stolon:master-pg10cnrwul4r4nse pgsql_pgkeeper2 replicated 1/1 sorintlab/stolon:master-pg10frflfnpty7tr pgsql_pgkeeper3 replicated 1/1 sorintlab/stolon:master-pg10x7pqqchi52kq pgsql_pgsentinel replicated 3/3 sorintlab/stolon:master-pg10mwu2wl8fti4r pgsql_postgresql replicated 3/3 sorintlab/stolon:master-pg109hkbe2vksbzb redis_redis-master global 3/3 bitnami/redis:latest *:6379->6379/tcpl88zn8cla7dc redis_redis-replica replicated 3/3 bitnami/redis:latest *:30003->6379/tcp1utp309xfmsy redis_redis-sentinel global 3/3 bitnami/redis:latest *:30002->16379/tcpoteb824ylhyp registry_registry replicated 1/1 registry:2.6qovrah8nzzu8 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 напрямую в секцию с сервисами.
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: - gitlabnetworks: gitlab:
# podman-compose -f gitlab-runner.yml -d up
Результат работы:
# podman psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESda53da946c01 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_1781c0103c94a 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 lsPOD ID NAME STATUS CREATED # OF CONTAINERS INFRA ID71fc2b2a5c63 root Running 11 minutes ago 3 db40ab8bf84b
# 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.4apiVersion: v1kind: Podmetadata: creationTimestamp: "2020-07-29T19:22:40Z" labels: app: root name: rootspec: 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-confstatus: {}
# 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.serviceDocumentation=man:podman-generate-systemd(1)Requires=container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.serviceBefore=container-781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3.service container-da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864.service[Service]Restart=on-failureExecStart=/usr/bin/podman start db40ab8bf84bf35141159c26cb6e256b889c7a98c0418eee3c4aa683c14fccaaExecStop=/usr/bin/podman stop -t 10 db40ab8bf84bf35141159c26cb6e256b889c7a98c0418eee3c4aa683c14fccaaKillMode=noneType=forkingPIDFile=/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.serviceDocumentation=man:podman-generate-systemd(1)RefuseManualStart=yesRefuseManualStop=yesBindsTo=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.serviceAfter=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service[Service]Restart=on-failureExecStart=/usr/bin/podman start da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864ExecStop=/usr/bin/podman stop -t 10 da53da946c01449f500aa5296d9ea6376f751948b17ca164df438b7df6607864KillMode=noneType=forkingPIDFile=/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.serviceDocumentation=man:podman-generate-systemd(1)RefuseManualStart=yesRefuseManualStop=yesBindsTo=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.serviceAfter=pod-71fc2b2a5c6346f0c1c86a2dc45dbe78fa192ea02aac001eb8347ccb8c043c26.service[Service]Restart=on-failureExecStart=/usr/bin/podman start 781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3ExecStop=/usr/bin/podman stop -t 10 781c0103c94aaa113c17c58d05ddabf8df4bf39707b664abcf17ed2ceff467d3KillMode=noneType=forkingPIDFile=/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 можно ознакомиться на странице курса.