Docker at lahat, lahat, lahat

TL;DR: Isang pangkalahatang-ideya na gabay sa paghahambing ng mga framework para sa pagpapatakbo ng mga application sa mga container. Isasaalang-alang ang mga kakayahan ng Docker at iba pang katulad na mga sistema.

Docker at lahat, lahat, lahat

Isang maliit na kasaysayan kung saan nanggaling ang lahat

Kuwento

Ang unang kilalang paraan upang ihiwalay ang isang application ay chroot. Ang system call na may parehong pangalan ay nagbibigay ng pagbabago sa root directory - kaya nagbibigay ng access sa program na tumawag dito, access lang sa mga file sa loob ng directory na ito. Ngunit kung ang programa ay bibigyan ng mga karapatan ng superuser sa loob, maaari itong potensyal na "makatakas" mula sa chroot at makakuha ng access sa pangunahing operating system. Gayundin, bilang karagdagan sa pagbabago ng direktoryo ng ugat, ang iba pang mga mapagkukunan (RAM, processor), pati na rin ang pag-access sa network, ay hindi limitado.

Ang susunod na paraan ay ang paglunsad ng isang ganap na operating system sa loob ng isang lalagyan, gamit ang mga mekanismo ng kernel ng operating system. Ang pamamaraang ito ay tinatawag na naiiba sa iba't ibang mga operating system, ngunit ang kakanyahan ay pareho - ang paglulunsad ng ilang mga independiyenteng operating system, na ang bawat isa ay tumatakbo sa parehong kernel kung saan tumatakbo ang pangunahing operating system. Kabilang dito ang FreeBSD Jails, Solaris Zones, OpenVZ at LXC para sa Linux. Tinitiyak ang paghihiwalay hindi lamang ng espasyo sa disk, kundi pati na rin ng iba pang mapagkukunan; sa partikular, ang bawat lalagyan ay maaaring may mga limitasyon sa oras ng processor, RAM, at bandwidth ng network. Kung ikukumpara sa chroot, ang pag-iwan sa container ay mas mahirap, dahil ang superuser sa container ay may access lang sa mga nilalaman ng container, gayunpaman, dahil sa pangangailangang panatilihing napapanahon ang operating system sa loob ng container at ang paggamit ng mga mas lumang bersyon. ng mga kernels (may-katuturan para sa Linux, sa mas mababang lawak ng FreeBSD), mayroong isang non-zero ang posibilidad na "masira" ang kernel isolation system at makakuha ng access sa pangunahing operating system.

Sa halip na maglunsad ng isang ganap na operating system sa isang lalagyan (na may isang initialization system, package manager, atbp.), Maaari mong ilunsad kaagad ang mga application, ang pangunahing bagay ay upang bigyan ang mga application ng ganoong pagkakataon (ang pagkakaroon ng mga kinakailangang aklatan at iba pang mga file). Ang ideyang ito ay nagsilbing batayan para sa containerized application virtualization, ang pinakakilala at kilalang kinatawan kung saan ay ang Docker. Kung ikukumpara sa mga nakaraang system, ang mas nababaluktot na mga mekanismo ng paghihiwalay, kasama ng built-in na suporta para sa mga virtual na network sa pagitan ng mga container at pagsubaybay sa estado ng aplikasyon sa loob ng container, ay nagresulta sa kakayahang bumuo ng isang magkakaugnay na kapaligiran mula sa isang malaking bilang ng mga pisikal na server para sa pagpapatakbo ng mga container - nang hindi nangangailangan ng manu-manong pamamahala ng mapagkukunan.

Manggagawa sa pantalan

Ang Docker ay ang pinakasikat na application containerization software. Nakasulat sa wikang Go, ginagamit nito ang mga karaniwang tampok ng kernel ng Linux - mga cgroup, namespace, kakayahan, atbp., pati na rin ang mga Aufs file system at iba pang katulad nito upang makatipid ng espasyo sa disk.

Docker at lahat, lahat, lahat
Pinagmulan: wikimedia

arkitektura

Bago ang bersyon 1.11, nagtrabaho ang Docker bilang isang solong serbisyo na nagsagawa ng lahat ng mga operasyon na may mga lalagyan: pag-download ng mga larawan para sa mga lalagyan, paglulunsad ng mga lalagyan, pagproseso ng mga kahilingan sa API. Simula sa bersyon 1.11, ang Docker ay nahahati sa ilang bahagi na nakikipag-ugnayan sa isa't isa: containerd, para sa pagproseso ng buong cycle ng buhay ng mga lalagyan (paglalaan ng espasyo sa disk, pag-download ng mga imahe, pagtatrabaho sa network, paglulunsad, pag-install at pagsubaybay sa estado ng mga lalagyan) at runC, ang container execution environment, batay sa paggamit ng mga cgroup at iba pang feature ng Linux kernel. Ang serbisyo ng docker mismo ay nananatili, ngunit ngayon ay nagsisilbi lamang itong iproseso ang mga kahilingan ng API na isinalin sa containerd.

Docker at lahat, lahat, lahat

Pag-install at pagsasaayos

Ang aking paboritong paraan ng pag-install ng docker ay ang docker-machine, na, bilang karagdagan sa direktang pag-install at pag-configure ng docker sa mga malalayong server (kabilang ang iba't ibang mga ulap), ay nagbibigay-daan sa iyo upang gumana sa mga file system ng mga malalayong server, at maaari ring magpatakbo ng iba't ibang mga utos.

Gayunpaman, mula noong 2018, ang proyekto ay halos hindi na binuo, kaya i-install namin ito sa karaniwang paraan para sa karamihan ng mga pamamahagi ng Linux - pagdaragdag ng isang repositoryo at pag-install ng mga kinakailangang pakete.

Ginagamit din ang paraang ito para sa awtomatikong pag-install, halimbawa, gamit ang Ansible o iba pang katulad na mga sistema, ngunit hindi ko ito isasaalang-alang sa artikulong ito.

Ang pag-install ay isasagawa sa Centos 7, gagamit ako ng isang virtual machine bilang isang server, upang mai-install, patakbuhin lamang ang mga utos sa ibaba:

# 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

Pagkatapos ng pag-install, kailangan mong simulan ang serbisyo, ilagay ito sa autoload:

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

Bukod pa rito, maaari kang lumikha ng pangkat ng docker, na ang mga user ay makakapagtrabaho sa docker nang walang sudo, mag-set up ng pag-log, paganahin ang access sa API mula sa labas, at huwag kalimutang i-configure ang firewall nang mas tumpak (lahat ng bagay na hindi pinapayagan ay ipinagbabawal sa mga halimbawa sa itaas at sa ibaba - Inalis ko ito para sa pagiging simple at visualization), ngunit hindi na ako magdetalye dito.

Iba pang mga tampok

Bilang karagdagan sa docker machine sa itaas, mayroon ding docker registry, isang tool para sa pag-iimbak ng mga imahe para sa mga container, pati na rin ang docker compose - isang tool para sa pag-automate ng pag-deploy ng mga application sa mga container, ang mga YAML file ay ginagamit upang bumuo at mag-configure ng mga container at iba pang mga kaugnay na bagay (halimbawa, mga network, paulit-ulit na file system para sa pag-iimbak ng data).

Maaari rin itong magamit upang ayusin ang mga conveyor para sa CICD. Ang isa pang kawili-wiling tampok ay gumagana sa cluster mode, ang tinatawag na swarm mode (bago ang bersyon 1.12 ito ay kilala bilang docker swarm), na nagbibigay-daan sa iyong mag-assemble ng isang solong imprastraktura mula sa ilang mga server para sa pagpapatakbo ng mga lalagyan. Mayroong suporta para sa isang virtual na network sa itaas ng lahat ng mga server, mayroong built-in na load balancer, pati na rin ang suporta para sa mga lihim para sa mga lalagyan.

Ang mga file ng YAML mula sa docker compose ay maaaring gamitin para sa mga naturang cluster na may maliliit na pagbabago, ganap na awtomatiko ang pagpapanatili ng maliliit at katamtamang mga cluster para sa iba't ibang layunin. Para sa malalaking cluster, mas mainam ang Kubernetes dahil ang mga gastos sa pagpapanatili ng swarm mode ay maaaring mas malaki kaysa sa mga gastos sa Kubernetes. Bilang karagdagan sa runC, bilang isang kapaligiran ng pagpapatupad para sa mga lalagyan, maaari mong i-install, halimbawa Mga lalagyan ng kata

Nagtatrabaho sa Docker

Pagkatapos ng pag-install at pagsasaayos, susubukan naming mag-assemble ng isang cluster kung saan kami ay magde-deploy ng GitLab at Docker Registry para sa development team. Gagamit ako ng tatlong virtual machine bilang mga server, kung saan ipapakalat ko rin ang ipinamahagi na FS GlusterFS; Gagamitin ko ito bilang isang docker volume storage, halimbawa, para magpatakbo ng fault-tolerant na bersyon ng docker registry. Mga pangunahing bahagi na tatakbo: Docker Registry, Postgresql, Redis, GitLab na may suporta para sa GitLab Runner sa ibabaw ng Swarm. Ilulunsad namin ang Postgresql na may clustering Stolon, kaya hindi mo kailangang gumamit ng GlusterFS upang mag-imbak ng data ng Postgresql. Ang natitirang kritikal na data ay maiimbak sa GlusterFS.

Upang i-deploy ang GlusterFS sa lahat ng mga server (tinatawag silang node1, node2, node3), kailangan mong mag-install ng mga pakete, paganahin ang firewall, at lumikha ng mga kinakailangang direktoryo:

# 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

Pagkatapos ng pag-install, ang trabaho sa pag-configure ng GlusterFS ay dapat ipagpatuloy mula sa isang node, halimbawa 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

Pagkatapos ay kailangan mong i-mount ang nagresultang dami (ang utos ay dapat isagawa sa lahat ng mga server):

# mount /srv/docker

Ang swarm mode ay naka-configure sa isa sa mga server, na siyang magiging Leader, ang iba ay kailangang sumali sa cluster, kaya ang resulta ng pagpapatupad ng command sa unang server ay kailangang kopyahin at isagawa sa iba.

Paunang pag-setup ng kumpol, pinapatakbo ko ang utos sa 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

Kopyahin ang resulta ng pangalawang utos, isagawa sa node2 at node3:

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

Nakumpleto nito ang paunang pagsasaayos ng mga server, simulan natin ang pag-configure ng mga serbisyo, ang mga utos na isasagawa ay ilulunsad mula sa node1, maliban kung tinukoy.

Una sa lahat, gumawa tayo ng mga network para sa mga container:

# 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

Pagkatapos ay markahan namin ang mga server, ito ay kinakailangan upang itali ang ilang mga serbisyo sa mga server:

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

Susunod, gumawa kami ng mga direktoryo para sa pag-iimbak ng etcd data, ang KV storage na kailangan ni Traefik at Stolon. Katulad ng Postgresql, ang mga ito ay mga lalagyan na nakatali sa mga server, kaya ipapatupad namin ang utos na ito sa lahat ng mga server:

# mkdir -p /srv/etcd

Susunod, lumikha ng isang file upang i-configure ang etcd at ilapat ito:

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

Pagkaraan ng ilang oras, tinitingnan namin na ang etcd cluster ay pataas:

# 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

Lumilikha kami ng mga direktoryo para sa Postgresql, isagawa ang utos sa lahat ng mga server:

# mkdir -p /srv/pgsql

Susunod, lumikha ng isang file upang i-configure ang 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

Bumubuo kami ng mga lihim at ginagamit ang file:

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

Pagkaraan ng ilang oras (tingnan ang output ng command serbisyo ng docker lsna ang lahat ng mga serbisyo ay tumaas) ay nagpasimula ng kumpol ng 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

Sinusuri ang kahandaan ng kumpol ng 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

Kino-configure namin ang traefik upang buksan ang access sa mga container mula sa labas:

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

Sinimulan namin ang Redis Cluster, para dito lumikha kami ng isang direktoryo ng imbakan sa lahat ng mga node:

# 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

Magdagdag ng 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

At sa wakas - GitLab:

08gitlab-runner.yml

version: '3.7'

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    networks:
      - pgsql
      - redis
      - traefik
      - gitlab
    ports:
      - 22222:22
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        postgresql['enable'] = false
        redis['enable'] = false
        gitlab_rails['registry_enabled'] = false
        gitlab_rails['db_username'] = "gitlab"
        gitlab_rails['db_password'] = "XXXXXXXXXXX"
        gitlab_rails['db_host'] = "postgresql"
        gitlab_rails['db_port'] = "5432"
        gitlab_rails['db_database'] = "gitlab"
        gitlab_rails['db_adapter'] = 'postgresql'
        gitlab_rails['db_encoding'] = 'utf8'
        gitlab_rails['redis_host'] = 'redis-master'
        gitlab_rails['redis_port'] = '6379'
        gitlab_rails['redis_password'] = 'xxxxxxxxxxx'
        gitlab_rails['smtp_enable'] = true
        gitlab_rails['smtp_address'] = "smtp.yandex.ru"
        gitlab_rails['smtp_port'] = 465
        gitlab_rails['smtp_user_name'] = "[email protected]"
        gitlab_rails['smtp_password'] = "xxxxxxxxx"
        gitlab_rails['smtp_domain'] = "example.com"
        gitlab_rails['gitlab_email_from'] = '[email protected]'
        gitlab_rails['smtp_authentication'] = "login"
        gitlab_rails['smtp_tls'] = true
        gitlab_rails['smtp_enable_starttls_auto'] = true
        gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
        external_url 'http://gitlab.example.com/'
        gitlab_rails['gitlab_shell_ssh_port'] = 22222
    volumes:
      - gitlab_conf:/etc/gitlab
      - gitlab_logs:/var/log/gitlab
      - gitlab_data:/var/opt/gitlab
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager
      labels:
        - traefik.enable=true
        - traefik.http.routers.gitlab.rule=Host(`gitlab.example.com`)
        - traefik.http.services.gitlab.loadbalancer.server.port=80
        - traefik.docker.network=traefik
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    networks:
      - gitlab
    volumes:
      - gitlab_runner_conf:/etc/gitlab
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
        - node.role == manager

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

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

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

Ang huling estado ng cluster at mga serbisyo:

# 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

Ano pa ang maaaring pagbutihin? Tiyaking i-configure ang Traefik upang gumana sa mga container ng https, magdagdag ng tls encryption para sa Postgresql at Redis. Ngunit sa pangkalahatan, maaari mo na itong ibigay sa mga developer bilang PoC. Tingnan natin ngayon ang mga alternatibo sa Docker.

podman

Isa pang medyo kilalang makina para sa pagpapatakbo ng mga lalagyan na nakapangkat ayon sa mga pod (mga pod, mga pangkat ng mga lalagyan na pinagsama-sama). Hindi tulad ng Docker, hindi ito nangangailangan ng anumang serbisyo upang magpatakbo ng mga lalagyan; lahat ng gawain ay ginagawa sa pamamagitan ng library ng libpod. Nakasulat din sa Go, nangangailangan ng runtime na katugma sa OCI para magpatakbo ng mga container, gaya ng runC.

Docker at lahat, lahat, lahat

Ang pagtatrabaho sa Podman sa pangkalahatan ay kahawig ng sa Docker, hanggang sa magagawa mo ito tulad nito (inaangkin ng marami na sumubok nito, kasama ang may-akda ng artikulong ito):

$ alias docker=podman

at maaari kang magpatuloy sa pagtatrabaho. Sa pangkalahatan, ang sitwasyon sa Podman ay napaka-interesante, dahil kung ang mga unang bersyon ng Kubernetes ay nagtrabaho sa Docker, pagkatapos noong mga 2015, pagkatapos i-standardize ang mundo ng container (OCI - Open Container Initiative) at hatiin ang Docker sa containerd at runC, isang alternatibo sa Ang Docker ay binuo upang tumakbo sa Kubernetes: CRI-O. Ang Podman sa bagay na ito ay isang alternatibo sa Docker, na binuo sa mga prinsipyo ng Kubernetes, kabilang ang pagpapangkat ng container, ngunit ang pangunahing layunin ng proyekto ay ang magpatakbo ng mga container na may istilong Docker nang walang mga karagdagang serbisyo. Para sa mga malinaw na kadahilanan, walang swarm mode, dahil malinaw na sinasabi ng mga developer na kung kailangan mo ng isang kumpol, kunin ang Kubernetes.

Instalasyon

Upang mai-install sa Centos 7, i-activate lang ang Extras repository, at pagkatapos ay i-install ang lahat gamit ang command:

# yum -y install podman

Iba pang mga tampok

Ang Podman ay maaaring makabuo ng mga yunit para sa systemd, kaya malulutas ang problema sa pagsisimula ng mga lalagyan pagkatapos ng pag-reboot ng server. Bilang karagdagan, ang systemd ay idineklara na gumagana nang tama bilang pid 1 sa lalagyan. Upang bumuo ng mga lalagyan, mayroong isang hiwalay na tool sa buildah, mayroon ding mga tool ng third-party - mga analogue ng docker-compose, na bumubuo rin ng mga file ng pagsasaayos na katugma sa Kubernetes, kaya ang paglipat mula sa Podman patungo sa Kubernetes ay kasing simple hangga't maaari.

Nagtatrabaho sa Podman

Dahil walang swarm mode (ito ay dapat na lumipat sa Kubernetes kung kinakailangan ang isang cluster), isasama namin ito sa magkakahiwalay na mga lalagyan.

I-install ang podman-compose:

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

Ang nagreresultang config file para sa podman ay bahagyang naiiba, bilang halimbawa kailangan naming ilipat ang isang hiwalay na seksyon ng volume nang direkta sa seksyon ng mga serbisyo.

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

Resulta ng trabaho:

# 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

Tingnan natin kung ano ang bubuo nito para sa systemd at kubernetes, para dito kailangan nating malaman ang pangalan o id ng 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

Sa kasamaang palad, bukod sa pagpapatakbo ng mga lalagyan, ang nabuong unit para sa systemd ay walang ibang ginagawa (halimbawa, paglilinis ng mga lumang lalagyan kapag ang naturang serbisyo ay na-restart), kaya ikaw mismo ang magdadagdag ng mga ganoong bagay.

Sa prinsipyo, sapat na ang Podman upang subukan kung ano ang mga container, ilipat ang mga lumang configuration para sa docker-compose, at pagkatapos ay pumunta sa Kubernetes, kung kinakailangan, sa isang cluster, o kumuha ng mas madaling gamitin na alternatibo sa Docker.

rkt

Proyekto napunta sa archive mga anim na buwan na ang nakalilipas dahil sa katotohanang binili ito ng RedHat, kaya hindi ko na ito pag-uusapan nang mas detalyado. Sa pangkalahatan, nag-iwan ito ng napakagandang impression, ngunit kumpara sa Docker, at higit pa sa Podman, mukhang isang pinagsama. Mayroon ding pamamahagi ng CoreOS na binuo sa ibabaw ng rkt (bagaman mayroon silang orihinal na Docker), ngunit natapos din iyon pagkatapos ng pagbili ng RedHat.

Flash

Pa isang proyekto, ang may-akda nito ay gusto lang bumuo at magpatakbo ng mga lalagyan. Sa paghusga sa dokumentasyon at code, hindi sinunod ng may-akda ang mga pamantayan, ngunit nagpasya lamang na isulat ang kanyang sariling pagpapatupad, na, sa prinsipyo, ginawa niya.

Natuklasan

Ang sitwasyon sa Kubernetes ay medyo kawili-wili: sa isang banda, sa Docker maaari kang bumuo ng isang kumpol (sa swarm mode), kung saan maaari ka ring magpatakbo ng mga kapaligiran ng produkto para sa mga kliyente, ito ay totoo lalo na para sa mga maliliit na koponan (3-5 tao) , o may maliit na kabuuang load , o kawalan ng pagnanais na maunawaan ang mga sali-salimuot ng pag-set up ng Kubernetes, kabilang ang para sa matataas na load.

Ang Podman ay hindi nagbibigay ng ganap na compatibility, ngunit mayroon itong isang mahalagang bentahe - compatibility sa Kubernetes, kabilang ang mga karagdagang tool (buildah at iba pa). Samakatuwid, lalapit ako sa pagpili ng isang tool para sa trabaho tulad ng sumusunod: para sa mga maliliit na koponan, o may limitadong badyet - Docker (na may posibleng swarm mode), para sa pagbuo para sa aking sarili sa isang personal na localhost - Podman comrades, at para sa lahat. - Kubernetes.

Hindi ako sigurado na ang sitwasyon sa Docker ay hindi magbabago sa hinaharap, pagkatapos ng lahat, sila ay mga pioneer, at dahan-dahan ding nag-standardize ng hakbang-hakbang, ngunit ang Podman, kasama ang lahat ng mga pagkukulang nito (gumagana lamang sa Linux, walang clustering, assembly. at ang iba pang mga aksyon ay mga desisyon ng third-party) mas malinaw ang hinaharap, kaya iniimbitahan ko ang lahat na talakayin ang mga natuklasang ito sa mga komento.

PS Sa Agosto 3 inilunsad namin ang "Docker video coursekung saan maaari kang matuto nang higit pa tungkol sa kanyang trabaho. Susuriin namin ang lahat ng mga tool nito: mula sa mga pangunahing abstraction hanggang sa mga parameter ng network, mga nuances ng pagtatrabaho sa iba't ibang mga operating system at programming language. Makikilala mo ang teknolohiya at mauunawaan mo kung saan at kung paano pinakamahusay na gamitin ang Docker. Magbabahagi din kami ng mga kaso ng pinakamahusay na kasanayan.

Presyo ng pre-order bago ilabas: RUB 5000. Maaari mong tingnan ang programa ng Docker Video Course sa pahina ng kurso.

Pinagmulan: www.habr.com

Magdagdag ng komento