Docker y todo, todo, todo

TL;DR: artículo guía de comparación sobre entornos para ejecutar aplicaciones en contenedores. Se abordarán las funcionalidades de Docker y otros sistemas similares.

Docker y todo, todo, todo

Un poco de historia, de dónde proviene todo esto

Historia

La primera forma conocida de aislamiento de aplicaciones es chroot. La llamada llamada del sistema permite cambiar el directorio raíz, proporcionando así a un programa que lo invoca acceso solo a los archivos dentro de ese directorio. Sin embargo, si se le dan privilegios de superusuario, potencialmente puede "escapar" de chroot y obtener acceso al sistema operativo principal. Además, la simple modificación del directorio raíz no limita otros recursos (memoria RAM, CPU), ni el acceso a la red.

La siguiente forma es ejecutar un sistema operativo completo dentro de un contenedor, gracias a los mecanismos del núcleo del sistema operativo. En diferentes sistemas operativos, este método se llama de diversas maneras, pero la esencia es la misma: ejecutar varios sistemas operativos independientes, cada uno de los cuales funciona con el mismo núcleo que el sistema operativo principal. Esto incluye FreeBSD Jails, Solaris Zones, OpenVZ y LXC para Linux. Se asegura el aislamiento no solo por espacio en disco, sino también por otros recursos; en particular, cada contenedor puede tener restricciones en el tiempo de CPU, la memoria RAM y la ancho de banda de red. En comparación con chroot, salir del contenedor es más difícil, ya que el superusuario dentro del contenedor solo tiene acceso a los componentes del contenedor. Sin embargo, debido a la necesidad de mantener el sistema operativo dentro del contenedor actualizado y al uso de versiones antiguas de núcleos (lo cual es relevante para Linux, en menor medida para FreeBSD), existe una probabilidad no nula de "romper" el sistema de aislamiento del núcleo y obtener acceso al sistema operativo principal.

En lugar de iniciar un sistema operativo completo en un contenedor (con un sistema de inicialización, gestor de paquetes, etc.), se pueden lanzar las aplicaciones directamente, siempre que se les brinde esa posibilidad (disponibilidad de las bibliotecas requeridas y otros archivos). Esta idea sirvió como base para la virtualización de aplicaciones en contenedores, cuyo representante más destacado y conocido es Docker. Comparado con los sistemas anteriores, los mecanismos de aislamiento más flexibles, junto con el soporte incorporado para redes virtuales entre contenedores y el seguimiento del estado de la aplicación dentro del contenedor, permitieron construir un entorno cohesivo a partir de un gran número de servidores físicos para ejecutar contenedores, sin necesidad de gestión manual de recursos.

Docker

Docker es el software más conocido para la contenerización de aplicaciones. Está escrito en el lenguaje Go, utiliza las capacidades nativas del núcleo de Linux: cgroups, namespaces, capabilities, etc., así como sistemas de archivos Aufs y otros similares para ahorrar espacio en disco.

Docker y todo, todo, todo
Fuente: wikimedia

Arquitectura

Hasta la versión 1.11, Docker funcionaba como un solo servicio que realizaba todas las operaciones con contenedores: descarga de imágenes de contenedores, lanzamiento de contenedores, procesamiento de solicitudes API. A partir de la versión 1.11, Docker se fragmentó en varias partes que interactúan entre sí: containerd, para manejar todo el ciclo de vida de los contenedores (asignación de espacio en disco, descarga de imágenes, trabajo con redes, lanzamiento, instalación y supervisión del estado de los contenedores) y runC, un entorno de ejecución de contenedores basado en el uso de cgroups y otras capacidades del núcleo de Linux. El servicio Docker se mantuvo, pero ahora solo se usa para procesar las solicitudes API que se transmiten a containerd.

Docker y todo, todo, todo

Instalación y configuración

Mi forma favorita de instalar Docker es usando docker-machine, que además de la instalación y configuración de Docker en servidores remotos (incluyendo diversas nubes), permite trabajar con los sistemas de archivos de esos servidores remotos y también puede ejecutar varios comandos.

Sin embargo, desde 2018 el proyecto casi no ha evolucionado, así que realizaremos la instalación de la manera estándar para la mayoría de las distribuciones de Linux: añadiendo el repositorio e instalando los paquetes necesarios.

Este método también se aplica durante la instalación automatizada, por ejemplo, utilizando Ansible u otros sistemas similares, pero no lo abordaré en este artículo.

La instalación se llevará a cabo en CentOS 7, utilizando una máquina virtual como servidor; para la instalación, basta con ejecutar los comandos a continuación:

# 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

Después de la instalación, es necesario iniciar el servicio y configurarlo para el arranque automático:

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

Adicionalmente, se puede crear un grupo docker, cuyas cuentas de usuario podrán trabajar con docker sin sudo, configurar el registro, habilitar el acceso a la API desde el exterior, y no olvidar ajustar más precisamente el firewall (en los ejemplos arriba y abajo lo he omitido por simplicidad y claridad), pero aquí no profundizaré más.

Otras capacidades

Además de la máquina docker mencionada, también existe el registro de docker, una herramienta para almacenar imágenes de contenedores, así como docker compose, una herramienta para automatizar la implementación de aplicaciones en contenedores, que utiliza archivos YAML para la creación y configuración de contenedores y otros componentes relacionados (por ejemplo, redes, sistemas de archivos persistentes para almacenamiento de datos).

También se puede organizar flujos de trabajo para CICD. Otra capacidad interesante es el modo clúster, conocido como swarm mode (hasta la versión 1.12 se conocía como docker swarm), que permite construir una infraestructura unificada a partir de varios servidores para ejecutar contenedores. Hay soporte para una red virtual sobre todos los servidores, balanceador de carga incorporado, así como soporte para secretos de contenedores.

Los archivos YAML de docker compose, con ligeras modificaciones, pueden ser utilizados para tales clústeres, automatizando completamente el mantenimiento de clústeres pequeños y medianos para diversos propósitos. Para clústeres grandes, es preferible utilizar Kubernetes, ya que los costos de mantenimiento del modo swarm pueden superar los de Kubernetes. Además de runC, como entorno de ejecución de contenedores, se puede instalar por ejemplo Kata containers

Adecuado para carga alta

Después de instalar y configurar, intentaremos crear un clúster donde desplegaremos GitLab y Docker Registry para el equipo de desarrollo. Utilizaré tres máquinas virtuales como servidores, en las que además implementaré un sistema de archivos distribuido GlusterFS, que usaré como almacenamiento para los volúmenes de Docker, por ejemplo, para ejecutar una versión tolerante a fallos del docker registry. Los componentes clave para el despliegue son: Docker Registry, Postgresql, Redis, GitLab con soporte para GitLab Runner sobre Swarm. Ejecutaremos Postgresql con clustering. Stolon, por lo que no es necesario usar GlusterFS para almacenar los datos de Postgresql. Los demás datos críticos se almacenarán en GlusterFS.

Para desplegar GlusterFS en todos los servidores (que se denominan node1, node2, node3), es necesario instalar los paquetes, permitir la actividad del firewall, y crear los directorios necesarios:

# 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

Después de la instalación, el trabajo de configuración de GlusterFS debe continuar desde un nodo, por ejemplo, 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

Luego hay que montar el volumen resultante (deben ejecutar el comando en todos los servidores):

# mount /srv/docker

La configuración del modo swarm se realiza en uno de los servidores, que será el Leader, los demás deben unirse al clúster, por lo que el resultado de la ejecución del comando en el primer servidor debe copiarse y ejecutarse en los demás.

Configuración inicial del clúster, ejecutando el comando en 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

Copiamos el resultado del segundo comando y lo ejecutamos en node2 y node3:

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

Con esto, la configuración preliminar de los servidores ha terminado, procedemos a la configuración de los servicios, los comandos se ejecutarán desde node1, a menos que se indique lo contrario.

Primero crearemos redes para los contenedores:

# 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

Luego etiquetamos los servidores, esto es necesario para vincular algunos servicios a los servidores:

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

A continuación, creamos directorios para el almacenamiento de datos etcd, un almacén KV necesario para Traefik y Stolon. Al igual que Postgresql, estos serán contenedores vinculados a los servidores, por lo que este comando se ejecuta en todos los servidores:

# mkdir -p /srv/etcd

A continuación, creamos un archivo para la configuración de etcd y lo aplicamos:

00etcd.yml

versión: '3.7'

servicios:
  etcd1:
    imagen: quay.io/coreos/etcd:latest
    nombre de host: etcd1
    comando:
      - 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
    redes:
      - etcd
    volúmenes:
      - etcd1vol:/data.etcd
    implementación:
      réplicas: 1
      colocación:
        restricciones: [node.labels.nodename == node1]
  etcd2:
    imagen: quay.io/coreos/etcd:latest
    nombre de host: etcd2
    comando:
      - 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
    redes:
      - etcd
    volúmenes:
      - etcd2vol:/data.etcd
    implementación:
      réplicas: 1
      colocación:
        restricciones: [node.labels.nodename == node2]
  etcd3:
    imagen: quay.io/coreos/etcd:latest
    nombre de host: etcd3
    comando:
      - 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
    redes:
      - etcd
    volúmenes:
      - etcd3vol:/data.etcd
    implementación:
      réplicas: 1
      colocación:
        restricciones: [node.labels.nodename == node3]

volúmenes:
  etcd1vol:
    controlador: local
    opciones_controlador:
      tipo: ninguno
      o: bind
      dispositivo: "/srv/etcd"
  etcd2vol:
    controlador: local
    opciones_controlador:
      tipo: ninguno
      o: bind
      dispositivo: "/srv/etcd"
  etcd3vol:
    controlador: local
    opciones_controlador:
      tipo: ninguno
      o: bind
      dispositivo: "/srv/etcd"

redes:
  etcd:
    externo: verdadero

# docker stack deploy --compose-file 00etcd.yml etcd

Después de un tiempo, verificamos que el clúster etcd se ha levantado:

# 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

Creamos los directorios para Postgresql, ejecutamos el comando en todos los servidores:

# mkdir -p /srv/pgsql

A continuación, creamos un archivo para la configuración de Postgresql:

01pgsql.yml

versión: '3.7'

servicios:
  pgsentinel:
    imagen: sorintlab/stolon:master-pg10
    comando:
      - 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
    redes:
      - etcd
      - pgsql
    despliegue:
      réplicas: 3
      configuración_actualización:
        paralelismo: 1
        retraso: 30s
        orden: stop-first
        acción_fallo: pause
  pgkeeper1:
    imagen: sorintlab/stolon:master-pg10
    nombre_host: pgkeeper1
    comando:
      - 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
    redes:
      - etcd
      - pgsql
    entorno:
      - PGDATA=/var/lib/postgresql/data
    volúmenes:
      - pgkeeper1:/var/lib/postgresql/data
    secretos:
      - pgsql
      - pgsql_repl
    despliegue:
      réplicas: 1
      ubicación:
        restricciones: [node.labels.nodename == node1]
  pgkeeper2:
    imagen: sorintlab/stolon:master-pg10
    nombre_host: pgkeeper2
    comando:
      - 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
    redes:
      - etcd
      - pgsql
    entorno:
      - PGDATA=/var/lib/postgresql/data
    volúmenes:
      - pgkeeper2:/var/lib/postgresql/data
    secretos:
      - pgsql
      - pgsql_repl
    despliegue:
      réplicas: 1
      ubicación:
        restricciones: [node.labels.nodename == node2]
  pgkeeper3:
    imagen: sorintlab/stolon:master-pg10
    nombre_host: pgkeeper3
    comando:
      - 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
    redes:
      - etcd
      - pgsql
    entorno:
      - PGDATA=/var/lib/postgresql/data
    volúmenes:
      - pgkeeper3:/var/lib/postgresql/data
    secretos:
      - pgsql
      - pgsql_repl
    despliegue:
      réplicas: 1
      ubicación:
        restricciones: [node.labels.nodename == node3]
  postgresql:
    imagen: sorintlab/stolon:master-pg10
    comando: 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
    redes:
      - etcd
      - pgsql
    despliegue:
      réplicas: 3
      configuración_actualización:
        paralelismo: 1
        retraso: 30s
        orden: stop-first
        acción_fallo: rollback

volúmenes:
  pgkeeper1:
    controlador: local
    opciones_controlador:
      tipo: none
      o: bind
      dispositivo: "/srv/pgsql"
  pgkeeper2:
    controlador: local
    opciones_controlador:
      tipo: none
      o: bind
      dispositivo: "/srv/pgsql"
  pgkeeper3:
    controlador: local
    opciones_controlador:
      tipo: none
      o: bind
      dispositivo: "/srv/pgsql"

secretos:
  pgsql:
    archivo: "/srv/docker/postgres"
  pgsql_repl:
    archivo: "/srv/docker/replica"

redes:
  etcd:
    externo: true
  pgsql:
    externo: true

Generando secretos, aplicando archivo:

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

Después de un tiempo (mira la salida del comando docker service ls, que todos los servicios se estén ejecutando) inicializamos el clúster 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

Verificamos la disponibilidad del clúster 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

Configuramos traefik para abrir el acceso a los contenedores desde el exterior:

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

Iniciamos el clúster de Redis, para ello creamos en todos los nodos un directorio para almacenamiento:

# mkdir -p /srv/redis

05redis.yml

version: '3.7'

services:
  redis-master:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '6379:6379'
    environment:
      - REDIS_REPLICATION_MODE=master
      - REDIS_PASSWORD=xxxxxxxxxxx
    deploy:
      mode: global
      restart_policy:
        condition: any
    volumes:
      - 'redis:/opt/bitnami/redis/etc/'

  redis-replica:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '6379'
    depends_on:
      - redis-master
    environment:
      - REDIS_REPLICATION_MODE=slave
      - REDIS_MASTER_HOST=redis-master
      - REDIS_MASTER_PORT_NUMBER=6379
      - REDIS_MASTER_PASSWORD=xxxxxxxxxxx
      - REDIS_PASSWORD=xxxxxxxxxxx
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: any

  redis-sentinel:
    image: 'bitnami/redis:latest'
    networks:
      - redis
    ports:
      - '16379'
    depends_on:
      - redis-master
      - redis-replica
    entrypoint: |
      bash -c 'bash -s <<EOF
      "/bin/bash" -c "cat < /opt/bitnami/redis/etc/sentinel.conf
      port 16379
      dir /tmp
      sentinel monitor master-node redis-master 6379 2
      sentinel down-after-milliseconds master-node 5000
      sentinel parallel-syncs master-node 1
      sentinel failover-timeout master-node 5000
      sentinel auth-pass master-node xxxxxxxxxxx
      sentinel announce-ip redis-sentinel
      sentinel announce-port 16379
      EOF"
      "/bin/bash" -c "redis-sentinel /opt/bitnami/redis/etc/sentinel.conf"
      EOF'
    deploy:
      mode: global
      restart_policy:
        condition: any

volumes:
  redis:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: "/srv/redis"

networks:
  redis:
    external: true

# docker stack deploy --compose-file 05redis.yml redis

Añadimos Docker Registry:

06registry.yml

versión: '3.7'

servicios:
  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.ejemplo.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

Y por último, GitLab:

08gitlab-runner.yml

versión: '3.7'

servicios:
  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@ejemplo.com"
        gitlab_rails['smtp_password'] = "xxxxxxxxx"
        gitlab_rails['smtp_domain'] = "ejemplo.com"
        gitlab_rails['gitlab_email_from'] = 'noreply@ejemplo.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.ejemplo.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.ejemplo.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

Estado final del clúster y servicios:

# 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

¿Qué más se puede mejorar? Asegúrese de configurar Traefik para que los contenedores funcionen a través de https, y agregar cifrado tls para Postgresql y Redis. Pero en general ya se puede entregar a los desarrolladores como PoC. Ahora, veamos las alternativas a Docker.

Podman

Otro motor bastante conocido para ejecutar contenedores, agrupados en pods (pods, grupos de contenedores desplegados juntos). A diferencia de Docker, no requiere ningún servicio para ejecutar contenedores, todo se gestiona a través de la biblioteca libpod. También está escrito en Go y necesita un runtime compatible con OCI para ejecutar contenedores, como runC.

Docker y todo, todo, todo

El trabajo con Podman recuerda en general al de Docker, hasta el punto de que se puede hacer así (lo han declarado muchos que lo han probado, incluido el autor de este artículo):

$ alias docker=podman

y se puede seguir trabajando. En general, la situación con Podman es bastante interesante, ya que si las versiones anteriores de Kubernetes trabajaban con Docker, desde aproximadamente 2015, después de la estandarización del mundo de los contenedores (OCI — Open Container Initiative) y la división de Docker en containerd y runC, se está desarrollando una alternativa a Docker para ejecutar en Kubernetes: CRI-O. Podman, en este sentido, es una alternativa a Docker construida según los principios de Kubernetes, incluida la agrupación de contenedores, pero el objetivo principal de este proyecto es ejecutar contenedores al estilo Docker sin servicios adicionales. Por razones comprensibles, no hay modo swarm, ya que los desarrolladores dejan claro que si se necesita un clúster — tome Kubernetes.

Instalación

Para instalar en Centos 7, solo necesita activar el repositorio Extras, después de lo cual puede instalar todo con el comando:

# yum -y install podman

Otras capacidades

Podman puede generar unidades para systemd, resolviendo así el problema de iniciar contenedores después del reinicio del servidor. Además, se ha declarado un funcionamiento correcto de systemd como pid 1 en el contenedor. Para construir contenedores, existe una herramienta separada llamada buildah, y también hay herramientas de terceros — análogos de docker-compose, que generan, además, archivos de configuración compatibles con Kubernetes, de modo que la transición de Podman a Kubernetes se simplifica tanto como sea posible.

Trabajo con Podman

Dado que no hay modo swarm (se supone que se debe cambiar a Kubernetes si se necesita un clúster) — agruparemos contenedores individuales.

Instalamos podman-compose:

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

El archivo de configuración resultante para podman es un poco diferente, por ejemplo, se tuvo que mover una sección separada de volumes directamente a la sección de servicios.

gitlab-podman.yml

versión: '3.7'

servicios:
  gitlab:
    imagen: gitlab/gitlab-ce:latest
    nombre_anfitrión: gitlab.ejemplo.com
    reiniciar: a menos que se detenga
    entorno:
      GITLAB_OMNIBUS_CONFIG: |
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    puertos:
      - "80:80"
      - "22222:22"
    volúmenes:
      - /srv/podman/gitlab/conf:/etc/gitlab
      - /srv/podman/gitlab/data:/var/opt/gitlab
      - /srv/podman/gitlab/logs:/var/log/gitlab
    redes:
      - gitlab

  gitlab-runner:
    imagen: gitlab/gitlab-runner:alpine
    reiniciar: a menos que se detenga
    depende_de:
      - gitlab
    volúmenes:
      - /srv/podman/gitlab/runner:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
    redes:
      - gitlab

redes:
  gitlab:

# podman-compose -f gitlab-runner.yml -d up

Resultado de la operación:

# 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

Veamos qué generará para systemd y kubernetes, para esto necesitamos saber el nombre o id del pod:

# 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

Desafortunadamente, además de iniciar contenedores, la unidad generada para systemd no hace nada más (por ejemplo, no limpia contenedores antiguos al reiniciar este servicio), por lo que estas cosas deberán añadirse manualmente.

En principio, Podman es suficiente para probar qué son los contenedores, trasladar configuraciones antiguas de docker-compose, después de lo cual se puede optar por Kubernetes, si se necesita un clúster, o tener una alternativa más simple a Docker.

rkt

Proyecto ha quedado archivado hace aproximadamente seis meses porque fue comprado por RedHat, así que no me detendré en él más a fondo. En general, dejaba una impresión bastante buena, sin embargo, en comparación con Docker y aún más con Podman parece un monstruo. También existió una distribución de CoreOS, construida sobre rkt (aunque originalmente tenían Docker), sin embargo, su soporte también terminó después de la compra de RedHat.

Plash

Más un proyecto, cuyo autor simplemente quería compilar y ejecutar contenedores. Según la documentación y el código, el autor no siguió los estándares, sino que decidió escribir su propia implementación, lo que de hecho hizo.

Conclusiones

La situación con Kubernetes es bastante interesante: por un lado, con Docker se puede construir un clúster (en modo swarm), con el que incluso se pueden ejecutar entornos de producción para los clientes, lo cual es especialmente relevante para equipos pequeños (3-5 personas), o cuando hay una carga general baja, o simplemente cuando no se desea lidiar con las complejidades de la configuración de Kubernetes, incluso para cargas altas.

Podman no proporciona compatibilidad total, pero tiene una ventaja importante: es compatible con Kubernetes y también con herramientas adicionales (buildah y otras). Por lo tanto, mi enfoque para elegir una herramienta será el siguiente: para equipos pequeños o con presupuesto limitado, Docker (con posible modo swarm), para desarrollo personal en localhost, Podman con amigos, y para todos los demás, Kubernetes.

No estoy seguro de que la situación con Docker no cambie en el futuro, ya que son pioneros y están estandarizándose poco a poco. Sin embargo, a pesar de todas sus desventajas (funciona solo en Linux, no tiene clustering, construcciones y otras acciones deben hacerse con soluciones externas), el futuro de Podman parece más claro. Por ello, invito a todos los interesados a discutir estas conclusiones en los comentarios.

P.D. El 3 de agosto lanzamos "Curso de video sobre Docker" donde podrás aprender más sobre su funcionamiento. Analizaremos todas sus herramientas: desde las abstracciones básicas hasta los parámetros de red, matices de trabajo con diferentes sistemas operativos y lenguajes de programación. Te familiarizarás con la tecnología y entenderás dónde y cómo utilizar mejor Docker. También compartiremos casos de buenas prácticas.

El precio de la preorden hasta el lanzamiento es: 5000 r. Puedes consultar el programa del "Curso de video sobre Docker" en la página del curso.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster