Skip to content

Otus-DevOps-2022-05/r2d2k_microservices

Repository files navigation

Домашние задания по микросервисам

Оглавление:


01 - Технология контейнеризации. Введение в Docker

Задание №01-1:

  • Создать docker host
  • Создать свой образ
  • Изучить работу с Docker Hub

Решение №01-1: Работаем в каталоге docker-monolith. Устанавливаем Docker по официальной документации. Проверяем версии установленного ПО:

> docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:46 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:00:51 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.7
  GitCommit:        0197261a30bf81f1ee8e6a4dd2dea0ef95d67ccb
 runc:
  Version:          1.1.3
  GitCommit:        v1.1.3-0-g6724737
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

> docker compose version
Docker Compose version v2.6.0

Проверяем работу окружения:

> docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:7d246653d0511db2a6b2e0436cfd0e52ac8c066000264b3ce63331ac66dca625
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Образ был скачан, запущен в контейнере, вывел сообщение и завершил работу. Вот контейнер в выключенном состоянии:

> docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED         STATUS                     PORTS     NAMES
7209dc11e9c9   hello-world   "/hello"   6 seconds ago   Exited (0) 3 seconds ago             ecstatic_tu

Видим, что образ присутствует в локальной системе:

> docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
hello-world   latest    feb5d9fea6a5   13 months ago   13.3kB

При каждом запуске создаётся новый контейнер, это видно по имени хоста:

> docker run -it ubuntu:18.04 /bin/bash
Unable to find image 'ubuntu:18.04' locally
18.04: Pulling from library/ubuntu
e706e0a9f423: Pull complete
Digest: sha256:40b84b75884ff39e4cac4bf62cb9678227b1fbf9dbe3f67ef2a6b073aa4bb529
Status: Downloaded newer image for ubuntu:18.04
root@e0c1b582acce:/# echo 'Hello world!' > /tmp/file
root@e0c1b582acce:/# exit

> docker run -it ubuntu:18.04 /bin/bash
root@4143404d8c92:/# cat /tmp/file
cat: /tmp/file: No such file or directory
root@4143404d8c92:/# exit

Запустим контейнер, в котором мы создали временный файл:

> docker start e0c1b582acce
e0c1b582acce

Проверим, на месте ли файл:

> docker attach e0c1b582acce
root@e0c1b582acce:/# cat /tmp/file
Hello world!
root@e0c1b582acce:/# exit
exit

При использовании ключа -d контейнер будет запущен в фоне:

> docker run -dt nginx:latest
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
bd159e379b3b: Pull complete
6659684f075c: Pull complete
679576c0baac: Pull complete
22ca44aeb873: Pull complete
b45acafbea93: Pull complete
bcbbe1cb4836: Pull complete
Digest: sha256:5ffb682b98b0362b66754387e86b0cd31a5cb7123e49e7f6f6617690900d20b2
Status: Downloaded newer image for nginx:latest
f79a4795e75acae7ada6e8ce518ea6ca2da5634c1f5024e7e8b22b1df7ea597c

> docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS     NAMES
f79a4795e75a   nginx:latest   "/docker-entrypoint.…"   6 seconds ago   Up 4 seconds   80/tcp    heuristic_elgamal

Из запущенного контейнера, при необходимости, можно создать образ:

> docker commit e0c1b582acce r2d2k/ubuntu-tmp-file
sha256:dba315d073020223be142943876a4681aab37280aed53e105d79869c6269ac37

> docker images
REPOSITORY              TAG       IMAGE ID       CREATED         SIZE
r2d2k/ubuntu-tmp-file   latest    dba315d07302   3 seconds ago   63.1MB
nginx                   latest    5d58c024174d   4 days ago      142MB
ubuntu                  18.04     71cb16d32be4   2 weeks ago     63.1MB
test_alpine             latest    fd8e1b7c15b7   2 months ago    5.54MB
alpine                  latest    9c6f07244728   2 months ago    5.54MB
ubuntu                  latest    df5de72bdb3b   2 months ago    77.8MB
hello-world             latest    feb5d9fea6a5   13 months ago   13.3kB

Остановка контейнера:

> docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS     NAMES
f79a4795e75a   nginx:latest   "/docker-entrypoint.…"   40 minutes ago   Up 40 minutes   80/tcp    heuristic_elgamal

> docker stop f79a4795e75a
f79a4795e75a

> docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Проверка использования докером пространства на диске:

> docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         3         288.3MB   146.5MB (50%)
Containers      5         0         1.227kB   1.227kB (100%)
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Удалим все образы незапущенных контейнеров:

> docker rm $(docker ps -a -q)
f79a4795e75a
ba3de6d4e888
4143404d8c92
e0c1b582acce
7209dc11e9c9

> docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         0         288.3MB   288.3MB (100%)
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Пришло время собрать собственный образ, для этого подготовим четыре файла:

  • Dockerfile - инструкции для сборки нашего образа
  • mongod.conf - подготовленный конфиг для mongodb
  • db_config - содержит переменную окружения со ссылкой на mongodb
  • start.sh - скрипт запуска приложения

Содержимое: mongod.conf:

# Where and how to store data.
storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1

Содержимое: start.sh:

#!/bin/bash

/usr/bin/mongod --fork --logpath /var/log/mongod.log --config /etc/mongodb.conf

source /reddit/db_config

cd /reddit && puma || exit

Содержимое: db_config:

DATABASE_URL=127.0.0.1

Собирать образ будем на базе Ubuntu 18.04. Обновляем списки пакетов, устанавливаем всё необходимое для работы. Клонируем репозиторий с приложением. Копируем конфигурационные файлы приложения и скрипт для его запуска. Устанавливаем необходимые пакеты ruby и запускаем приложение.

Содержимое: Dockerfile:

FROM ubuntu:18.04

RUN apt-get update
RUN apt-get install -y mongodb-server ruby-full ruby-dev build-essential git
RUN gem install bundler
RUN git clone -b monolith https://github.com/express42/reddit.git

COPY mongod.conf /etc/mongod.conf
COPY db_config /reddit/db_config
COPY start.sh /start.sh

RUN cd /reddit && rm Gemfile.lock && bundle install
RUN chmod 0777 /start.sh

CMD ["/start.sh"]

После подготовки файла соберём образ:

> docker build -t reddit:latest .

Sending build context to Docker daemon   21.5kB
Step 1/11 : FROM ubuntu:18.04
 ---> 71cb16d32be4
Step 2/11 : RUN apt-get update
 ---> Running in 8b3c8d475755
Get:1 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
...
...
...
Reading package lists...
Removing intermediate container 8b3c8d475755
 ---> 8bad6f048d49
Step 3/11 : RUN apt-get install -y mongodb-server ruby-full ruby-dev build-essential git
 ---> Running in a1ad589b7de0
Reading package lists...
Building dependency tree...
...
...
...
Running hooks in /etc/ca-certificates/update.d...
done.
Removing intermediate container a1ad589b7de0
 ---> 89e6a0741d60
Step 4/11 : RUN gem install bundler
 ---> Running in ab67503c8084
Successfully installed bundler-2.3.24
Parsing documentation for bundler-2.3.24
Installing ri documentation for bundler-2.3.24
Done installing documentation for bundler after 0 seconds
1 gem installed
Removing intermediate container ab67503c8084
 ---> 75364dae8491
Step 5/11 : RUN git clone -b monolith https://github.com/express42/reddit.git
 ---> Running in 495eeebba071
Cloning into 'reddit'...
Removing intermediate container 495eeebba071
 ---> 3ca3debf38c9
Step 6/11 : COPY mongod.conf /etc/mongod.conf
 ---> 0c986fd9970a
Step 7/11 : COPY db_config /reddit/db_config
 ---> 181a55e05130
Step 8/11 : COPY start.sh /start.sh
 ---> b26b3b10a35b
Step 9/11 : RUN cd /reddit && rm Gemfile.lock && bundle install
 ---> Running in 924c75a3aeae
Your RubyGems version (2.7.6) has a bug that prevents `required_ruby_version` from working for Bundler. Any scripts that use `gem install bundler` will break as soon as Bundler drops support for your Ruby version. Please upgrade RubyGems to avoid future breakage and silence this warning by running `gem update --system 3.2.3`
...
...
...
Bundle complete! 11 Gemfile dependencies, 28 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Removing intermediate container 924c75a3aeae
 ---> a7d6da9b92cf
Step 10/11 : RUN chmod 0777 /start.sh
 ---> Running in 9fc7dd6d1119
Removing intermediate container 9fc7dd6d1119
 ---> f3d656badaeb
Step 11/11 : CMD ["/start.sh"]
 ---> Running in 29091bf59c0a
Removing intermediate container 29091bf59c0a
 ---> 4a45f764506c
Successfully built 4a45f764506c
Successfully tagged reddit:latest

Вот, что у нас получилось после сборки образа:

> docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
reddit       latest    4a45f764506c   6 minutes ago   668MB
ubuntu       18.04     71cb16d32be4   2 weeks ago     63.1MB

> docker images -a
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
<none>       <none>    f3d656badaeb   6 minutes ago    668MB
reddit       latest    4a45f764506c   6 minutes ago    668MB
<none>       <none>    a7d6da9b92cf   6 minutes ago    668MB
<none>       <none>    b26b3b10a35b   7 minutes ago    626MB
<none>       <none>    181a55e05130   7 minutes ago    626MB
<none>       <none>    0c986fd9970a   7 minutes ago    626MB
<none>       <none>    3ca3debf38c9   7 minutes ago    626MB
<none>       <none>    75364dae8491   7 minutes ago    626MB
<none>       <none>    89e6a0741d60   7 minutes ago    624MB
<none>       <none>    8bad6f048d49   10 minutes ago   106MB
ubuntu       18.04     71cb16d32be4   2 weeks ago      63.1MB

Запустим контейнер с нашим образом и проверим статус:

> docker run --name reddit -d --network=host reddit:latest
082020743e90c4038b58d9a7a20bc287c3f0796aea183b2e9b036f66af70553d

> docker ps
CONTAINER ID   IMAGE           COMMAND       CREATED         STATUS         PORTS     NAMES
082020743e90   reddit:latest   "/start.sh"   9 seconds ago   Up 8 seconds

Проверим, что порт прослушивается:

> sudo ss -nlp sport 9292
Netid  State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Process
tcp    LISTEN  0       1024           0.0.0.0:9292        0.0.0.0:*      users:(("ruby2.5",pid=50812,fd=8))

Убедимся, что наше приложение в контейнере корректно отвечает на запросы из внешнего мира:

> lynx -dump http://127.0.0.1:9292

   (BUTTON) [1]Monolith Reddit
     * [2]Sign up
     * [3]Login

Menu

     * [4]All posts
     * [5]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/signup
   3. http://127.0.0.1:9292/login
   4. http://127.0.0.1:9292/
   5. http://127.0.0.1:9292/new

Загрузим наш образ на hub.docker.com:

> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: r2d2k
Password:

Login Succeeded

> docker tag reddit:latest r2d2k/otus-reddit:1.0

> docker push r2d2k/otus-reddit:1.0
The push refers to repository [docker.io/r2d2k/otus-reddit]
587e3001e1e1: Pushed
b99c80d72085: Pushed
55f4cad4000c: Pushed
0882c02ecaec: Pushed
85d6da4ee5fe: Pushed
9293ad396c99: Pushed
bdf1ab466a1b: Pushed
bd6347d5f95e: Pushed
4fd68b6da872: Pushed
b9b23e654574: Mounted from library/ubuntu
1.0: digest: sha256:e5022562c09c608db4c507582546b3ac28d81ad81327dcbcd8bd18ceab25089b size: 2413

Попробуем запустить контейнер с образом из hub.docker.com. Для начала зачистим систему от старых контейненеров и образов:

> docker ps
CONTAINER ID   IMAGE           COMMAND       CREATED          STATUS          PORTS     NAMES
082020743e90   reddit:latest   "/start.sh"   20 minutes ago   Up 20 minutes             reddit

> docker kill reddit
reddit

> docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

> docker images
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
reddit              latest    4a45f764506c   30 minutes ago   668MB
r2d2k/otus-reddit   1.0       4a45f764506c   30 minutes ago   668MB
ubuntu              18.04     71cb16d32be4   2 weeks ago      63.1MB

> docker rmi 4a45f764506c 71cb16d32be4 -f
Untagged: reddit:latest
Untagged: r2d2k/otus-reddit:1.0
Untagged: r2d2k/otus-reddit@sha256:e5022562c09c608db4c507582546b3ac28d81ad81327dcbcd8bd18ceab25089b
Deleted: sha256:4a45f764506c918f161c9e75ef0daf24c679db3ec74c3b7ff7f35bb190a96ed3
Deleted: sha256:f3d656badaebb39b425f464e267f27bf9af30c685cbbd93b0b8ef4520ff808f5
Deleted: sha256:99738d77a76ffff8e0f0ac6e9fdf3f20020a214b957dba3bbc0dbe44f6df086f
Deleted: sha256:a7d6da9b92cfa530b711b48dea28d7400bcb320736af7f804eba027a9cf18043
Deleted: sha256:fc283d09aba733709ab27d9169f6fe436ea6c1e201bcf8d2d391863345857f96
Deleted: sha256:b26b3b10a35bafbbaefd19169ccbfab22bd107df939b9098ef433392dbc37c63
Deleted: sha256:727f8d218b5cf9ad258fe1d55c4b8f2c9b5d3ea2e4c6e8a3c6edaf55ec671422
Deleted: sha256:181a55e051303e103cde2331605d9c934b42624437ec3f51544dae6c3c502bd1
Deleted: sha256:a80f1b3658d2af3c6fd0d006008e63777c376d9f862301f81cc1a5fc3058aad6
Deleted: sha256:0c986fd9970a7a01857c608fdbdab78abe398caeba8902722cf54bf46641b1f0
Deleted: sha256:2eb7ef424d76481b63fb1c1a04eb8c6d940f764e80afd659a8f200f1adcae602
Deleted: sha256:3ca3debf38c903e568611c9ce068b81cf7e364cef15b386ab8b52debacb4e9b7
Deleted: sha256:bc776045cce17acc374b0c3e7774fced1b5d2e85429ee8555c082e4ada5c1b2b
Deleted: sha256:75364dae8491a9536e559d1c4a34a249ad380db0ffe8954bac30bb349fba7e2b
Deleted: sha256:360d3102d598364913d33eb568efef5b1f20b79a5132095e6061127629be8a1b
Deleted: sha256:89e6a0741d6089e9d8bef0f98750bba57ce3acac05cd43b82d0623e80c26ec22
Deleted: sha256:d349964087d4f79bdd2394a1e76bcb7a7d731303e46e964ffe3bd17073e245bd
Deleted: sha256:8bad6f048d49edc2de6c0640edaa0df5e14b54f9fc42a668c38852f2ae96bc1b
Deleted: sha256:d6f285e235418eee01c4a50dc29ba2f07261371cf20f5ba2fde03abfe4dbf0f6
Untagged: ubuntu:18.04
Untagged: ubuntu@sha256:40b84b75884ff39e4cac4bf62cb9678227b1fbf9dbe3f67ef2a6b073aa4bb529
Deleted: sha256:71cb16d32be4a95065b4fa1c8841a6f4c0098de7be0a90e14519098412d48356
Deleted: sha256:b9b23e6545749dab77233e9c3ce2237e6705cbd30de01e11f529b0e49c155cd5

> docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

Загружаем образ и запускаем контейнер:

> docker run --name reddit -d -p 9292:9292 r2d2k/otus-reddit:1.0
Unable to find image 'r2d2k/otus-reddit:1.0' locally
1.0: Pulling from r2d2k/otus-reddit
e706e0a9f423: Pull complete
234b6057aae1: Pull complete
99f90bb6380e: Pull complete
b011cdbb40ca: Pull complete
a4962faa405a: Pull complete
6470925595eb: Pull complete
b7634723ba9b: Pull complete
ac43f38048c7: Pull complete
15c75bc10aab: Pull complete
c094e48923bf: Pull complete
Digest: sha256:e5022562c09c608db4c507582546b3ac28d81ad81327dcbcd8bd18ceab25089b
Status: Downloaded newer image for r2d2k/otus-reddit:1.0
cc1ca78072ae5db744a64fe68a29c064375b0bc4a461ca003e9efa5259b47d87

> docker ps
CONTAINER ID   IMAGE                   COMMAND       CREATED              STATUS              PORTS                                       NAMES
cc1ca78072ae   r2d2k/otus-reddit:1.0   "/start.sh"   About a minute ago   Up About a minute   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   reddit

Проверяем приложение:

> lynx -dump http://127.0.0.1:9292

   (BUTTON) [1]Monolith Reddit
     * [2]Sign up
     * [3]Login

Menu

     * [4]All posts
     * [5]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/signup
   3. http://127.0.0.1:9292/login
   4. http://127.0.0.1:9292/
   5. http://127.0.0.1:9292/new

Результат №01-1:

  • Создана машина с docker
  • Проверена работа тестовых образов
  • Собран собственный образ с приложением
  • Образ загружен на Docker Hub

Задание №01-2: Теперь, когда есть готовый образ с приложением, можно автоматизировать поднятие нескольких инстансов в Yandex Cloud, установку на них докера и запуск там образа /otus-reddit:1.0

  • Нужно реализовать в виде прототипа в директории /docker-monolith/infra/
  • Шаблон пакера, который делает образ с уже установленным Docker;
  • Поднятие инстансов с помощью Terraform, их количество задается переменной;
  • Несколько плейбуков Ansible с использованием динамического инвентори для установки докера и запуска там образа приложения;

Решение №01-2: При помощи packer подготовим образ виртуальной машины с установленным docker. Шаблон ubuntu-docker.json будет выглядеть так:

{
    "variables": {
        "mv_service_account_key_file": "",
        "mv_folder_id": "",
        "mv_source_image_family": ""
    },
    "builders": [
        {
            "type": "yandex",
            "service_account_key_file": "{{user `mv_service_account_key_file`}}",
            "folder_id": "{{user `mv_folder_id`}}",
            "source_image_family": "{{user `mv_source_image_family`}}",
            "image_name": "{{user `mv_image_family`}}-{{timestamp}}",
            "image_family": "{{user `mv_image_family`}}",
            "ssh_username": "ubuntu",
            "platform_id": "standard-v1",
            "use_ipv4_nat": "true"
        }
    ],
    "provisioners": [
        {
            "type": "shell",
            "pause_before": "60s",
            "script": "scripts/install_docker.sh",
            "execute_command": "sudo {{.Path}}"
        },
        {
            "type": "shell",
            "script": "scripts/cleanup.sh",
            "execute_command": "sudo {{.Path}}"
        }
    ]
}

Перед выполнением скрипта установки делаем паузу в 60 секунд, чтобы завершились все процессы при старте новой машины. В противном случае у нас возникнут проблемы с apt-get. Работой будет заниматься скрипт scripts\install_docker.sh:

#!/bin/sh

# Official guide: https://docs.docker.com/engine/install/ubuntu/

apt-get update
apt-get -y upgrade
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

# Setup repo
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Update repo
sudo apt-get update

# Install latest docker
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Второй скрипт scripts/cleanup.sh используем для зачистки будущего образа от мусора:

#!/bin/sh

apt-get autoclean
apt-get autoremove

Переменные для шаблона сохраним в variables.json:

{
    "mv_service_account_key_file": "./key.json",
    "mv_folder_id": "WYDIWYG",
    "mv_source_image_family": "ubuntu-1804-lts",
    "mv_image_family": "reddit-docker"
}

Проверим шаблон на ошибки:

> packer validate -var-file=variables.json ubuntu-docker.json
The configuration is valid.

Запускаем сборку образа:

> packer build -var-file=variables.json ubuntu-docker.json
yandex: output will be in this color.

==> yandex: Creating temporary RSA SSH key for instance...
==> yandex: Using as source image: fd8hvlnfb66dgavf0e1a (name: "ubuntu-18-04-lts-v20220815", family: "ubuntu-1804-lts")
==> yandex: Creating network...
==> yandex: Creating subnet in zone "ru-central1-a"...
==> yandex: Creating disk...
==> yandex: Creating instance...
==> yandex: Waiting for instance with id fhm7hfb88n2e76kg8vl3 to become active...
    yandex: Detected instance IP: 84.201.129.65
==> yandex: Using SSH communicator to connect: 84.201.129.65
==> yandex: Waiting for SSH to become available...
==> yandex: Connected to SSH!
==> yandex: Pausing 1m0s before the next provisioner...
==> yandex: Provisioning with shell script: scripts/install_docker.sh
    yandex: Hit:1 http://mirror.yandex.ru/ubuntu bionic InRelease
...
...
...
    yandex: 0 upgraded, 0 newly installed, 0 to remove and 5 not upgraded.
==> yandex: Stopping instance...
==> yandex: Deleting instance...
    yandex: Instance has been deleted!
==> yandex: Creating image: reddit-docker-1666723129
==> yandex: Waiting for image to complete...
==> yandex: Success image create...
==> yandex: Destroying subnet...
    yandex: Subnet has been deleted!
==> yandex: Destroying network...
    yandex: Network has been deleted!
==> yandex: Destroying boot disk...
    yandex: Disk has been deleted!
Build 'yandex' finished after 5 minutes 40 seconds.

==> Wait completed after 5 minutes 40 seconds

==> Builds finished. The artifacts of successful builds are:
--> yandex: A disk image was created: reddit-docker-1666723129 (id: fd8mkch2tdig66qg0edu) with family name reddit-docker

В результате имеем образ виртуальной машины на базе Ubuntu 18.04 с предустановленным docker. Запомним реквизиты образа reddit-docker-1666723129 (id: fd8mkch2tdig66qg0edu).

Поднимать виртуальные машины будем при помощи terraform.

Для начала опишем провайдер в файле provider.tf:

terraform {
  required_providers {
    yandex = {
      source = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
}

Формируем описания переменных в файле variables.tf:

variable "service_account_key_file" {
  description = "Path to service account key file"
}

variable "cloud_id" {
  description = "Cloud"
}

variable "folder_id" {
  description = "Folder"
}

variable "zone" {
  description = "Zone"
  default     = "ru-central1-a"
}

variable "image_id" {
  description = "Image id for VM"
}

variable "subnet_id" {
  description = "ID for subnet"
}

variable "public_key_path" {
  description = "Path to the public key used for ssh access"
}

variable "servers_count" {
  description = "Number of servers to create"
}

Значения переменных задаём в terraform.tfvars:

service_account_key_file    = "key.json"
cloud_id                    = "00000000000000000000"
folder_id                   = "00000000000000000000"
zone                        = "00-00000000-0"
image_id                    = "fd8mkch2tdig66qg0edu"
subnet_id                   = "00000000000000000000"
public_key_path             = "~/.ssh/ubuntu.pub"
servers_count               = 2

Теперь опишем конфигурацию виртуальных машин main.tf:

provider "yandex" {
  service_account_key_file = var.service_account_key_file
  cloud_id                 = var.cloud_id
  folder_id                = var.folder_id
  zone                     = var.zone
}

resource "yandex_compute_instance" "docker" {
  name = "reddit-docker-${count.index}"
  zone = var.zone

  count = var.servers_count

  resources {
    cores  = 2
    memory = 2
  }

  boot_disk {
    initialize_params {
      image_id = var.image_id
    }
  }

  network_interface {
    subnet_id = var.subnet_id
    nat       = true
  }

  metadata = {
    ssh-keys = "ubuntu:${file(var.public_key_path)}"
  }
}

Вывод адресов создаваемых машин опишем в output.tf:

output "external_ip_address_docker" {
  value = yandex_compute_instance.docker[*].network_interface.0.nat_ip_address
}

После формирования файлов инициализируем окружение:

> terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of yandex-cloud/yandex...
- Installing yandex-cloud/yandex v0.81.0...
- Installed yandex-cloud/yandex v0.81.0 (unauthenticated)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Создадим пару виртуальных машин:

> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # yandex_compute_instance.docker[0] will be created
  + resource "yandex_compute_instance" "docker" {
      + created_at                = (known after apply)
...
...
...
      + scheduling_policy {
          + preemptible = (known after apply)
        }
    }

  # yandex_compute_instance.docker[1] will be created
  + resource "yandex_compute_instance" "docker" {
      + created_at                = (known after apply)
...
...
...
      + scheduling_policy {
          + preemptible = (known after apply)
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  ~ external_ip_address_docker = [
      + (known after apply),
      + (known after apply),
    ]

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

yandex_compute_instance.docker[1]: Creating...
yandex_compute_instance.docker[0]: Creating...
yandex_compute_instance.docker[1]: Still creating... [10s elapsed]
yandex_compute_instance.docker[0]: Still creating... [10s elapsed]
yandex_compute_instance.docker[1]: Still creating... [20s elapsed]
yandex_compute_instance.docker[0]: Still creating... [20s elapsed]
yandex_compute_instance.docker[1]: Still creating... [30s elapsed]
yandex_compute_instance.docker[0]: Still creating... [30s elapsed]
yandex_compute_instance.docker[1]: Still creating... [40s elapsed]
yandex_compute_instance.docker[0]: Still creating... [40s elapsed]
yandex_compute_instance.docker[0]: Creation complete after 50s [id=fhmiqrg1f5k8tk7sc8v6]
yandex_compute_instance.docker[1]: Still creating... [50s elapsed]
yandex_compute_instance.docker[1]: Creation complete after 59s [id=fhmt60vocfo2mt9hkvgl]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

external_ip_address_docker = [
  "84.201.133.40",
  "84.201.134.246",
]

Машины созданы, адреса получены, далее запустим на этих машинах контейнер с нашим приложением. Просят использовать ansible, так используем его. Подробное описание подключения динамического инвентори я делал тут.

Если кратко, то:

  • Клонируем репозиторий: git clone --branch yc_compute https://github.com/st8f/community.general.git
  • Ищем community.general/plugins/inventory/yc_compute.py
  • Копируем в каталог /home/ubuntu/.ansible/plugins/inventory
  • Читаем документацию: ansible-doc -t inventory yc_compute
  • Готовим yc.yml и ansible.cfg

Содержимое yc.yml:

plugin: yc_compute

folders:
  - ******************pp

auth_kind: serviceaccountfile

service_account_file: ./ansible-key.json

hostnames:
  - fqdn

compose:
  ansible_host: network_interfaces[0].primary_v4_address.one_to_one_nat.address

keyed_groups:
  - key: labels['group']
    prefix: ''
    separator: ''

Содержимое ansible.cfg:

[defaults]
inventory = ./yc.yml
remote_user = ubuntu
private_key_file = ~/.ssh/ubuntu
host_key_checking = False
retry_files_enabled = False

[inventory]
enable_plugins = yc_compute

Проверим, как всё это отработает:

> ansible-inventory --list
{
    "_meta": {
        "hostvars": {
            "fhmiqrg1f5k8tk7sc8v6.auto.internal": {
                "ansible_host": "84.201.133.40"
            },
            "fhmt60vocfo2mt9hkvgl.auto.internal": {
                "ansible_host": "84.201.134.246"
            }
        }
    },
    "all": {
        "children": [
            "ungrouped"
        ]
    },
    "ungrouped": {
        "hosts": [
            "fhmiqrg1f5k8tk7sc8v6.auto.internal",
            "fhmt60vocfo2mt9hkvgl.auto.internal"
        ]
    }
}

Для запуска контейнера возьмём community.docker.docker_container.

Готовим простой плэйбук deploy_docker_app.yml:

- name: Run reddit-docker
  hosts: all
  become: true
  tasks:
    - name: Run app in container
      community.docker.docker_container:
        name: reddit-docker
        image: r2d2k/otus-reddit:1.0
        state: started
        ports:
          - "80:9292"

Вот, что бывает, если плохо читаем документацию:

> ansible-playbook deploy_docker_app.yml

PLAY [Run reddit-docker] ********************************************************

TASK [Gathering Facts] **********************************************************
ok: [fhmt60vocfo2mt9hkvgl.auto.internal]
ok: [fhmiqrg1f5k8tk7sc8v6.auto.internal]

TASK [Run app in container] *****************************************************
fatal: [fhmt60vocfo2mt9hkvgl.auto.internal]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (Docker SDK for Python: docker (Python >= 2.7) or docker-py (Python 2.6)) on fhmt60vocfo2mt9hkvgl's Python /usr/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter, for example via `pip install docker` or `pip install docker-py` (Python 2.6). The error was: No module named 'docker'"}
fatal: [fhmiqrg1f5k8tk7sc8v6.auto.internal]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (Docker SDK for Python: docker (Python >= 2.7) or docker-py (Python 2.6)) on fhmiqrg1f5k8tk7sc8v6's Python /usr/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter, for example via `pip install docker` or `pip install docker-py` (Python 2.6). The error was: No module named 'docker'"}

PLAY RECAP **********************************************************************
fhmiqrg1f5k8tk7sc8v6.auto.internal : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
fhmt60vocfo2mt9hkvgl.auto.internal : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Добавляем задачу по установке Docker SDK for Python, пробуем снова:

- name: Run reddit-docker
  hosts: all
  gather_facts: no
  become: yes
  tasks:

    - name: Install PIP
      apt:
        name: python3-pip
        state: present

    - name: Install Docker SDK for Python
      pip:
        name: docker
        state: present

    - name: Run app in container
      community.docker.docker_container:
        name: reddit-docker
        image: r2d2k/otus-reddit:1.0
        state: started
        ports:
          - "80:9292"

Внешне всё хорошо:

> ansible-playbook deploy_docker_app.yml

PLAY [Run reddit-docker] ********************************************************

TASK [Install PIP] **************************************************************

changed: [fhmiqrg1f5k8tk7sc8v6.auto.internal]
changed: [fhmt60vocfo2mt9hkvgl.auto.internal]

TASK [Install Docker SDK for Python] ********************************************
changed: [fhmt60vocfo2mt9hkvgl.auto.internal]
changed: [fhmiqrg1f5k8tk7sc8v6.auto.internal]

TASK [Run app in container] *****************************************************
changed: [fhmt60vocfo2mt9hkvgl.auto.internal]
changed: [fhmiqrg1f5k8tk7sc8v6.auto.internal]

PLAY RECAP **********************************************************************
fhmiqrg1f5k8tk7sc8v6.auto.internal : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
fhmt60vocfo2mt9hkvgl.auto.internal : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Проверяем приложение на двух хостах:

> lynx -dump http://84.201.133.40
   (BUTTON) [1]Monolith Reddit
     * [2]Sign up
     * [3]Login

Menu

     * [4]All posts
     * [5]New post

References

   1. http://84.201.133.40/
   2. http://84.201.133.40/signup
   3. http://84.201.133.40/login
   4. http://84.201.133.40/
   5. http://84.201.133.40/new

> lynx -dump http://84.201.134.246
   (BUTTON) [1]Monolith Reddit
     * [2]Sign up
     * [3]Login

Menu

     * [4]All posts
     * [5]New post

References

   1. http://84.201.134.246/
   2. http://84.201.134.246/signup
   3. http://84.201.134.246/login
   4. http://84.201.134.246/
   5. http://84.201.134.246/new

Всё работает. Прибираем за собой, т.е. обнуляем переменную servers_count, применяем конфигурацию terraform:

>terraform apply
yandex_compute_instance.docker[0]: Refreshing state... [id=fhmiqrg1f5k8tk7sc8v6]
yandex_compute_instance.docker[1]: Refreshing state... [id=fhmt60vocfo2mt9hkvgl]
...
...
...
Plan: 0 to add, 0 to change, 2 to destroy.

Changes to Outputs:
  ~ external_ip_address_docker = [
      - "84.201.133.40",
      - "84.201.134.246",
    ]

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

yandex_compute_instance.docker[1]: Destroying... [id=fhmt60vocfo2mt9hkvgl]
yandex_compute_instance.docker[0]: Destroying... [id=fhmiqrg1f5k8tk7sc8v6]
yandex_compute_instance.docker[1]: Still destroying... [id=fhmt60vocfo2mt9hkvgl, 10s elapsed]
yandex_compute_instance.docker[0]: Still destroying... [id=fhmiqrg1f5k8tk7sc8v6, 10s elapsed]
yandex_compute_instance.docker[1]: Still destroying... [id=fhmt60vocfo2mt9hkvgl, 20s elapsed]
yandex_compute_instance.docker[0]: Still destroying... [id=fhmiqrg1f5k8tk7sc8v6, 20s elapsed]
yandex_compute_instance.docker[1]: Destruction complete after 28s
yandex_compute_instance.docker[0]: Still destroying... [id=fhmiqrg1f5k8tk7sc8v6, 30s elapsed]
yandex_compute_instance.docker[0]: Destruction complete after 33s

Apply complete! Resources: 0 added, 0 changed, 2 destroyed.

Outputs:

external_ip_address_docker = []

Результат №01-2:

  • Подготовили при помощи packer образ ВМ с установленным docker
  • Используя terraform развернули несколько ВМ из этого образа
  • Написали простой плэйбук для установки и запуска приложения в docker из созданного ранее контейнера

02 - Docker-образы. Микросервисы

Задание №02-1:

  • Научиться описывать и собирать Docker-образы для сервисного приложения
  • Научиться оптимизировать работу с Docker-образами
  • Запуск и работа приложения на основе Docker-образов, оценка удобства запуска контейнеров при помощи docker run

Решение №02-1:

Создаём новую ветку:

> git checkout -b docker-3
Switched to a new branch 'docker-3'

> git push --set-upstream origin docker-3
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'docker-3' on GitHub by visiting:
remote:      https://github.com/Otus-DevOps-2022-05/r2d2k_microservices/pull/new/docker-3
remote:
To https://github.com/Otus-DevOps-2022-05/r2d2k_microservices.git
 * [new branch]      docker-3 -> docker-3
branch 'docker-3' set up to track 'origin/docker-3'.

Качаем архив, распаковываем в каталог src:

> tree src/
src/
├── comment
│             ├── comment_app.rb
│             ├── config.ru
│             ├── docker_build.sh
│             ├── Gemfile
│             ├── Gemfile.lock
│             ├── helpers.rb
│             └── VERSION
├── post-py
│             ├── docker_build.sh
│             ├── helpers.py
│             ├── post_app.py
│             ├── requirements.txt
│             └── VERSION
├── README.md
└── ui
    ├── config.ru
    ├── docker_build.sh
    ├── Gemfile
    ├── Gemfile.lock
    ├── helpers.rb
    ├── middleware.rb
    ├── ui_app.rb
    ├── VERSION
    └── views
        ├── create.haml
        ├── index.haml
        ├── layout.haml
        └── show.haml

4 directories, 25 files

Каталог src теперь основной каталог этого домашнего задания и наше приложение состоит из трех компонентов:

  • post-py - сервис отвечающий за написание постов
  • comment - сервис отвечающий за написание комментариев
  • ui - веб-интерфейс, работающий с другими сервисами

Создаём Dockerfile для приложения post-py:

FROM python:3.6.0-alpine

WORKDIR /app
ADD . /app

RUN apk --no-cache --update add build-base && \
    pip install -r /app/requirements.txt && \
    apk del build-base

ENV POST_DATABASE_HOST post_db
ENV POST_DATABASE posts

ENTRYPOINT ["python3", "post_app.py"]

Создаём Dockerfile для приложения comment:

FROM ruby:2.2
RUN apt-get update -qq && apt-get install -y build-essential

ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

ADD Gemfile* $APP_HOME/
RUN bundle install
ADD . $APP_HOME

ENV COMMENT_DATABASE_HOST comment_db
ENV COMMENT_DATABASE comments

CMD ["puma"]

Создаём Dockerfile для приложения ui:

FROM ruby:2.2
RUN apt-get update -qq && apt-get install -y build-essential

ENV APP_HOME /app
RUN mkdir $APP_HOME

WORKDIR $APP_HOME
ADD Gemfile* $APP_HOME/
RUN bundle install
ADD . $APP_HOME

ENV POST_SERVICE_HOST post
ENV POST_SERVICE_PORT 5000
ENV COMMENT_SERVICE_HOST comment
ENV COMMENT_SERVICE_PORT 9292

CMD ["puma"]

Для нормальной работы приложений нужна mongodb:

> docker pull mongo:latest
latest: Pulling from library/mongo
eaead16dc43b: Pull complete
8a00eb9f68a0: Pull complete
f683956749c5: Pull complete
b33b2f05ea20: Pull complete
3a342bea915a: Pull complete
fa956ab1c2f0: Pull complete
138a8542a624: Pull complete
acab179a7f07: Pull complete
f88335710e84: Pull complete
Digest: sha256:3b9bfc35335710340afe1e98c870491b2a969fd93b62505b4617eab73d97cec6
Status: Downloaded newer image for mongo:latest
docker.io/library/mongo:latest

Далее начинаем собирать наши образы:

> docker build -t r2d2k/post:1.0 ./post-py
Sending build context to Docker daemon   16.9kB
Step 1/7 : FROM python:3.6.0-alpine
3.6.0-alpine: Pulling from library/python
709515475419: Pull complete
7f8ede2d2484: Pull complete
3c752c95ebfb: Pull complete
39c204c94887: Pull complete
Digest: sha256:142fc3f338b10569d08c3f3855c492c2a176b0c45af099f9ebe87f0fededb210
Status: Downloaded newer image for python:3.6.0-alpine
 ---> cb178ebbf0f2
Step 2/7 : WORKDIR /app
 ---> Running in ac54899d5936
Removing intermediate container ac54899d5936
 ---> 0bba10393d99
Step 3/7 : ADD . /app
 ---> 6a92f2a283a7
Step 4/7 : RUN apk --no-cache --update add build-base &&     pip install -r /app/requirements.txt &&     apk del build-base
 ---> Running in 186b45b7fe5f
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
(1/21) Upgrading musl (1.1.14-r14 -> 1.1.14-r16)
(2/21) Installing binutils-libs (2.26-r1)
(3/21) Installing binutils (2.26-r1)
(4/21) Installing gmp (6.1.0-r0)
(5/21) Installing isl (0.14.1-r0)
(6/21) Installing libgomp (5.3.0-r0)
(7/21) Installing libatomic (5.3.0-r0)
(8/21) Installing libgcc (5.3.0-r0)
(9/21) Installing pkgconf (0.9.12-r0)
(10/21) Installing pkgconfig (0.25-r1)
(11/21) Installing mpfr3 (3.1.2-r0)
(12/21) Installing mpc1 (1.0.3-r0)
(13/21) Installing libstdc++ (5.3.0-r0)
(14/21) Installing gcc (5.3.0-r0)
(15/21) Installing make (4.1-r1)
(16/21) Installing musl-dev (1.1.14-r16)
(17/21) Installing libc-dev (0.7-r0)
(18/21) Installing fortify-headers (0.8-r0)
(19/21) Installing g++ (5.3.0-r0)
(20/21) Installing build-base (0.4-r1)
(21/21) Upgrading musl-utils (1.1.14-r14 -> 1.1.14-r16)
Executing busybox-1.24.2-r13.trigger
OK: 183 MiB in 52 packages
Collecting prometheus_client==0.0.21 (from -r /app/requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/45/6d/0cc171ce82a8f284133faf12a50018501abdc4b0742e4f8f471b6b2dc81f/prometheus_client-0.0.21.tar.gz
Collecting flask==0.12.3 (from -r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/24/3e/1b6aa496fa9bb119f6b22263ca5ca9e826aaa132431fd78f413c8bcc18e3/Flask-0.12.3-py2.py3-none-any.whl (88kB)
Collecting pymongo==3.5.1 (from -r /app/requirements.txt (line 3))
  Downloading https://files.pythonhosted.org/packages/a8/f6/f324f5c669478644ac64594b9d746a34e185d9c34d3f05a4a6a6dab5467b/pymongo-3.5.1.tar.gz (1.3MB)
Collecting structlog==17.2.0 (from -r /app/requirements.txt (line 4))
  Downloading https://files.pythonhosted.org/packages/9c/10/6f9ba14af65179d0e1a5f281064525a45a68763736188a5a6472846e3359/structlog-17.2.0-py2.py3-none-any.whl
Collecting py-zipkin==0.13.0 (from -r /app/requirements.txt (line 5))
  Downloading https://files.pythonhosted.org/packages/85/b7/c4b46b7437f8a7dfc255169a1df46e4a1c3456926be04c26a476c93e8893/py_zipkin-0.13.0.tar.gz
Collecting requests==2.18.4 (from -r /app/requirements.txt (line 6))
  Downloading https://files.pythonhosted.org/packages/49/df/50aa1999ab9bde74656c2919d9c0c085fd2b3775fd3eca826012bef76d8c/requests-2.18.4-py2.py3-none-any.whl (88kB)
Collecting itsdangerous>=0.21 (from flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/9c/96/26f935afba9cd6140216da5add223a0c465b99d0f112b68a4ca426441019/itsdangerous-2.0.1-py3-none-any.whl
Collecting Jinja2>=2.4 (from flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl (133kB)
Collecting Werkzeug>=0.7 (from flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/f4/f3/22afbdb20cc4654b10c98043414a14057cd27fdba9d4ae61cea596000ba2/Werkzeug-2.0.3-py3-none-any.whl (289kB)
Collecting click>=2.0 (from flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/4a/a8/0b2ced25639fb20cc1c9784de90a8c25f9504a7f18cd8b5397bd61696d7d/click-8.0.4-py3-none-any.whl (97kB)
Collecting six (from structlog==17.2.0->-r /app/requirements.txt (line 4))
  Downloading https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl
Collecting thriftpy (from py-zipkin==0.13.0->-r /app/requirements.txt (line 5))
  Downloading https://files.pythonhosted.org/packages/f4/19/cca118cf7d2087310dbc8bd70dc7df0c1320f2652873a93d06d7ba356d4a/thriftpy-0.3.9.tar.gz (208kB)
Collecting idna<2.7,>=2.5 (from requests==2.18.4->-r /app/requirements.txt (line 6))
  Downloading https://files.pythonhosted.org/packages/27/cc/6dd9a3869f15c2edfab863b992838277279ce92663d334df9ecf5106f5c6/idna-2.6-py2.py3-none-any.whl (56kB)
Collecting certifi>=2017.4.17 (from requests==2.18.4->-r /app/requirements.txt (line 6))
  Downloading https://files.pythonhosted.org/packages/1d/38/fa96a426e0c0e68aabc68e896584b83ad1eec779265a028e156ce509630e/certifi-2022.9.24-py3-none-any.whl (161kB)
Collecting chardet<3.1.0,>=3.0.2 (from requests==2.18.4->-r /app/requirements.txt (line 6))
  Downloading https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl (133kB)
Collecting urllib3<1.23,>=1.21.1 (from requests==2.18.4->-r /app/requirements.txt (line 6))
  Downloading https://files.pythonhosted.org/packages/63/cb/6965947c13a94236f6d4b8223e21beb4d576dc72e8130bd7880f600839b8/urllib3-1.22-py2.py3-none-any.whl (132kB)
Collecting MarkupSafe>=2.0 (from Jinja2>=2.4->flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz
  Requested MarkupSafe>=2.0 from https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz#sha256=594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a (from Jinja2>=2.4->flask==0.12.3->-r /app/requirements.txt (line 2)), but installing version None
Collecting dataclasses; python_version < "3.7" (from Werkzeug>=0.7->flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl
Collecting importlib-metadata; python_version < "3.8" (from click>=2.0->flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/a0/a1/b153a0a4caf7a7e3f15c2cd56c7702e2cf3d89b1b359d1f1c5e59d68f4ce/importlib_metadata-4.8.3-py3-none-any.whl
Collecting ply<4.0,>=3.4 (from thriftpy->py-zipkin==0.13.0->-r /app/requirements.txt (line 5))
  Downloading https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl (49kB)
Collecting zipp>=0.5 (from importlib-metadata; python_version < "3.8"->click>=2.0->flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/bd/df/d4a4974a3e3957fd1c1fa3082366d7fff6e428ddb55f074bf64876f8e8ad/zipp-3.6.0-py3-none-any.whl
Collecting typing-extensions>=3.6.4; python_version < "3.8" (from importlib-metadata; python_version < "3.8"->click>=2.0->flask==0.12.3->-r /app/requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/45/6b/44f7f8f1e110027cf88956b59f2fad776cca7e1704396d043f89effd3a0e/typing_extensions-4.1.1-py3-none-any.whl
Installing collected packages: prometheus-client, itsdangerous, MarkupSafe, Jinja2, dataclasses, Werkzeug, zipp, typing-extensions, importlib-metadata, click, flask, pymongo, six, structlog, ply, thriftpy, py-zipkin, idna, certifi, chardet, urllib3, requests
  Running setup.py install for prometheus-client: started
    Running setup.py install for prometheus-client: finished with status 'done'
  Running setup.py install for MarkupSafe: started
    Running setup.py install for MarkupSafe: finished with status 'done'
  Running setup.py install for pymongo: started
    Running setup.py install for pymongo: finished with status 'done'
  Running setup.py install for thriftpy: started
    Running setup.py install for thriftpy: finished with status 'done'
  Running setup.py install for py-zipkin: started
    Running setup.py install for py-zipkin: finished with status 'done'
Successfully installed Jinja2-3.0.3 MarkupSafe-0.0.0 Werkzeug-2.0.3 certifi-2022.9.24 chardet-3.0.4 click-8.0.4 dataclasses-0.8 flask-0.12.3 idna-2.6 importlib-metadata-4.8.3 itsdangerous-2.0.1 ply-3.11 prometheus-client-0.0.21 py-zipkin-0.13.0 pymongo-3.5.1 requests-2.18.4 six-1.16.0 structlog-17.2.0 thriftpy-0.3.9 typing-extensions-4.1.1 urllib3-1.22 zipp-3.6.0
You are using pip version 9.0.1, however version 22.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(1/19) Purging build-base (0.4-r1)
(2/19) Purging make (4.1-r1)
(3/19) Purging fortify-headers (0.8-r0)
(4/19) Purging g++ (5.3.0-r0)
(5/19) Purging gcc (5.3.0-r0)
(6/19) Purging binutils (2.26-r1)
(7/19) Purging isl (0.14.1-r0)
(8/19) Purging libatomic (5.3.0-r0)
(9/19) Purging pkgconfig (0.25-r1)
(10/19) Purging pkgconf (0.9.12-r0)
(11/19) Purging libc-dev (0.7-r0)
(12/19) Purging musl-dev (1.1.14-r16)
(13/19) Purging libstdc++ (5.3.0-r0)
(14/19) Purging binutils-libs (2.26-r1)
(15/19) Purging mpc1 (1.0.3-r0)
(16/19) Purging mpfr3 (3.1.2-r0)
(17/19) Purging gmp (6.1.0-r0)
(18/19) Purging libgomp (5.3.0-r0)
(19/19) Purging libgcc (5.3.0-r0)
Executing busybox-1.24.2-r13.trigger
OK: 31 MiB in 33 packages
Removing intermediate container 186b45b7fe5f
 ---> e24df85a06fe
Step 5/7 : ENV POST_DATABASE_HOST post_db
 ---> Running in 9911da2ec3d2
Removing intermediate container 9911da2ec3d2
 ---> 33a60c62bd72
Step 6/7 : ENV POST_DATABASE posts
 ---> Running in 4cfe432333f0
Removing intermediate container 4cfe432333f0
 ---> 71898b376dfa
Step 7/7 : ENTRYPOINT ["python3", "post_app.py"]
 ---> Running in 09604c0835a3
Removing intermediate container 09604c0835a3
 ---> b438f345df77
Successfully built b438f345df77
Successfully tagged r2d2k/post:1.0
> docker build -t r2d2k/comment:1.0 ./comment
Sending build context to Docker daemon  14.85kB
Step 1/11 : FROM ruby:2.2
2.2: Pulling from library/ruby
3d77ce4481b1: Pull complete
534514c83d69: Pull complete
d562b1c3ac3f: Pull complete
4b85e68dc01d: Pull complete
52134d825d3e: Pull complete
b2262ff3b75c: Pull complete
4d1332abe17f: Pull complete
Digest: sha256:4987b5e2f03b7086c493208ef616b711fe73228391a80faf451975f9e0195236
Status: Downloaded newer image for ruby:2.2
 ---> 6c8e6f9667b2
Step 2/11 : RUN apt-get update -qq && apt-get install -y build-essential
 ---> Running in 4efd9c42effc
Reading package lists...
Building dependency tree...
Reading state information...
The following extra packages will be installed:
  dpkg-dev fakeroot libalgorithm-diff-perl libalgorithm-diff-xs-perl
  libalgorithm-merge-perl libdpkg-perl libfakeroot libfile-fcntllock-perl
  libtimedate-perl
Suggested packages:
  debian-keyring
The following NEW packages will be installed:
  build-essential dpkg-dev fakeroot libalgorithm-diff-perl
  libalgorithm-diff-xs-perl libalgorithm-merge-perl libdpkg-perl libfakeroot
  libfile-fcntllock-perl libtimedate-perl
0 upgraded, 10 newly installed, 0 to remove and 162 not upgraded.
Need to get 2916 kB of archives.
After this operation, 5051 kB of additional disk space will be used.
Get:1 http://deb.debian.org/debian/ jessie/main libtimedate-perl all 2.3000-2 [42.2 kB]
Get:2 http://deb.debian.org/debian/ jessie/main libdpkg-perl all 1.17.27 [1075 kB]
Get:3 http://deb.debian.org/debian/ jessie/main dpkg-dev all 1.17.27 [1548 kB]
Get:4 http://deb.debian.org/debian/ jessie/main build-essential amd64 11.7 [7114 B]
Get:5 http://deb.debian.org/debian/ jessie/main libfakeroot amd64 1.20.2-1 [44.7 kB]
Get:6 http://deb.debian.org/debian/ jessie/main fakeroot amd64 1.20.2-1 [84.7 kB]
Get:7 http://deb.debian.org/debian/ jessie/main libalgorithm-diff-perl all 1.19.02-3 [51.7 kB]
Get:8 http://deb.debian.org/debian/ jessie/main libalgorithm-diff-xs-perl amd64 0.04-3+b1 [12.2 kB]
Get:9 http://deb.debian.org/debian/ jessie/main libalgorithm-merge-perl all 0.08-2 [13.5 kB]
Get:10 http://deb.debian.org/debian/ jessie/main libfile-fcntllock-perl amd64 0.22-1+b1 [36.4 kB]
debconf: delaying package configuration, since apt-utils is not installed
Fetched 2916 kB in 3s (914 kB/s)
Selecting previously unselected package libtimedate-perl.
(Reading database ... 21206 files and directories currently installed.)
Preparing to unpack .../libtimedate-perl_2.3000-2_all.deb ...
Unpacking libtimedate-perl (2.3000-2) ...
Selecting previously unselected package libdpkg-perl.
Preparing to unpack .../libdpkg-perl_1.17.27_all.deb ...
Unpacking libdpkg-perl (1.17.27) ...
Selecting previously unselected package dpkg-dev.
Preparing to unpack .../dpkg-dev_1.17.27_all.deb ...
Unpacking dpkg-dev (1.17.27) ...
Selecting previously unselected package build-essential.
Preparing to unpack .../build-essential_11.7_amd64.deb ...
Unpacking build-essential (11.7) ...
Selecting previously unselected package libfakeroot:amd64.
Preparing to unpack .../libfakeroot_1.20.2-1_amd64.deb ...
Unpacking libfakeroot:amd64 (1.20.2-1) ...
Selecting previously unselected package fakeroot.
Preparing to unpack .../fakeroot_1.20.2-1_amd64.deb ...
Unpacking fakeroot (1.20.2-1) ...
Selecting previously unselected package libalgorithm-diff-perl.
Preparing to unpack .../libalgorithm-diff-perl_1.19.02-3_all.deb ...
Unpacking libalgorithm-diff-perl (1.19.02-3) ...
Selecting previously unselected package libalgorithm-diff-xs-perl.
Preparing to unpack .../libalgorithm-diff-xs-perl_0.04-3+b1_amd64.deb ...
Unpacking libalgorithm-diff-xs-perl (0.04-3+b1) ...
Selecting previously unselected package libalgorithm-merge-perl.
Preparing to unpack .../libalgorithm-merge-perl_0.08-2_all.deb ...
Unpacking libalgorithm-merge-perl (0.08-2) ...
Selecting previously unselected package libfile-fcntllock-perl.
Preparing to unpack .../libfile-fcntllock-perl_0.22-1+b1_amd64.deb ...
Unpacking libfile-fcntllock-perl (0.22-1+b1) ...
Setting up libtimedate-perl (2.3000-2) ...
Setting up libdpkg-perl (1.17.27) ...
Setting up dpkg-dev (1.17.27) ...
Setting up build-essential (11.7) ...
Setting up libfakeroot:amd64 (1.20.2-1) ...
Setting up fakeroot (1.20.2-1) ...
update-alternatives: using /usr/bin/fakeroot-sysv to provide /usr/bin/fakeroot (fakeroot) in auto mode
Setting up libalgorithm-diff-perl (1.19.02-3) ...
Setting up libalgorithm-diff-xs-perl (0.04-3+b1) ...
Setting up libalgorithm-merge-perl (0.08-2) ...
Setting up libfile-fcntllock-perl (0.22-1+b1) ...
Removing intermediate container 4efd9c42effc
 ---> ffccdcac3a64
Step 3/11 : ENV APP_HOME /app
 ---> Running in 75e8523e1665
Removing intermediate container 75e8523e1665
 ---> 942515889cc2
Step 4/11 : RUN mkdir $APP_HOME
 ---> Running in 2b68bf830694
Removing intermediate container 2b68bf830694
 ---> 2bbd4fd7966f
Step 5/11 : WORKDIR $APP_HOME
 ---> Running in 45f52bff20e1
Removing intermediate container 45f52bff20e1
 ---> 43c09de77149
Step 6/11 : ADD Gemfile* $APP_HOME/
 ---> f70a60676ec8
Step 7/11 : RUN bundle install
 ---> Running in 1b9caed9872a
Warning: the running version of Bundler (1.16.1) is older than the version that created the lockfile (1.17.2). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
Fetching gem metadata from https://rubygems.org/........
Fetching bson 4.3.0
Installing bson 4.3.0 with native extensions
Fetching bson_ext 1.5.1
Installing bson_ext 1.5.1 with native extensions
Using bundler 1.16.1
Fetching thread_safe 0.3.6
Installing thread_safe 0.3.6
Fetching tzinfo 1.2.5
Installing tzinfo 1.2.5
Fetching et-orbi 1.1.6
Installing et-orbi 1.1.6
Fetching raabro 1.1.6
Installing raabro 1.1.6
Fetching fugit 1.1.6
Installing fugit 1.1.6
Fetching mongo 2.6.2
Installing mongo 2.6.2
Fetching mustermann 1.0.3
Installing mustermann 1.0.3
Fetching quantile 0.2.1
Installing quantile 0.2.1
Fetching prometheus-client 0.8.0
Installing prometheus-client 0.8.0
Fetching puma 3.12.0
Installing puma 3.12.0 with native extensions
Fetching rack 2.0.6
Installing rack 2.0.6
Fetching rack-protection 2.0.4
Installing rack-protection 2.0.4
Fetching rufus-scheduler 3.5.2
Installing rufus-scheduler 3.5.2
Fetching tilt 2.0.9
Installing tilt 2.0.9
Fetching sinatra 2.0.4
Installing sinatra 2.0.4
Fetching tzinfo-data 1.2018.7
Installing tzinfo-data 1.2018.7
Bundle complete! 8 Gemfile dependencies, 19 gems now installed.
Bundled gems are installed into `/usr/local/bundle`
Removing intermediate container 1b9caed9872a
 ---> 05874bc28333
Step 8/11 : ADD . $APP_HOME
 ---> e8dd7d8edc8a
Step 9/11 : ENV COMMENT_DATABASE_HOST comment_db
 ---> Running in a1b8521ac359
Removing intermediate container a1b8521ac359
 ---> be3334b9f2b5
Step 10/11 : ENV COMMENT_DATABASE comments
 ---> Running in f9b0888174fa
Removing intermediate container f9b0888174fa
 ---> 818315914101
Step 11/11 : CMD ["puma"]
 ---> Running in 46ecd35fc8d8
Removing intermediate container 46ecd35fc8d8
 ---> 5ed27e0339a7
Successfully built 5ed27e0339a7
Successfully tagged r2d2k/comment:1.0
> docker build -t r2d2k/ui:1.0 ./ui
Sending build context to Docker daemon  30.72kB
Step 1/13 : FROM ruby:2.2
 ---> 6c8e6f9667b2
Step 2/13 : RUN apt-get update -qq && apt-get install -y build-essential
 ---> Using cache
 ---> ffccdcac3a64
Step 3/13 : ENV APP_HOME /app
 ---> Using cache
 ---> 942515889cc2
Step 4/13 : RUN mkdir $APP_HOME
 ---> Using cache
 ---> 2bbd4fd7966f
Step 5/13 : WORKDIR $APP_HOME
 ---> Using cache
 ---> 43c09de77149
Step 6/13 : ADD Gemfile* $APP_HOME/
 ---> 7370fda28023
Step 7/13 : RUN bundle install
 ---> Running in 70de0b26eeb7
Warning: the running version of Bundler (1.16.1) is older than the version that created the lockfile (1.17.2). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
Fetching gem metadata from https://rubygems.org/............
Fetching concurrent-ruby 1.1.4
Installing concurrent-ruby 1.1.4
Fetching i18n 1.3.0
Installing i18n 1.3.0
Fetching minitest 5.11.3
Installing minitest 5.11.3
Fetching thread_safe 0.3.6
Installing thread_safe 0.3.6
Fetching tzinfo 1.2.5
Installing tzinfo 1.2.5
Fetching activesupport 5.2.2
Installing activesupport 5.2.2
Fetching backports 3.11.4
Installing backports 3.11.4
Fetching bson 1.12.5
Installing bson 1.12.5
Fetching bson_ext 1.12.5
Installing bson_ext 1.12.5 with native extensions
Using bundler 1.16.1
Fetching et-orbi 1.1.6
Installing et-orbi 1.1.6
Fetching multipart-post 2.0.0
Installing multipart-post 2.0.0
Fetching faraday 0.15.4
Installing faraday 0.15.4
Fetching thrift 0.9.3.0
Installing thrift 0.9.3.0 with native extensions
Fetching finagle-thrift 1.4.2
Installing finagle-thrift 1.4.2
Fetching raabro 1.1.6
Installing raabro 1.1.6
Fetching fugit 1.1.6
Installing fugit 1.1.6
Fetching temple 0.8.0
Installing temple 0.8.0
Fetching tilt 2.0.9
Installing tilt 2.0.9
Fetching haml 5.0.4
Installing haml 5.0.4
Fetching multi_json 1.13.1
Installing multi_json 1.13.1
Fetching mustermann 1.0.3
Installing mustermann 1.0.3
Fetching quantile 0.2.1
Installing quantile 0.2.1
Fetching prometheus-client 0.8.0
Installing prometheus-client 0.8.0
Fetching puma 3.12.0
Installing puma 3.12.0 with native extensions
Fetching rack 2.0.6
Installing rack 2.0.6
Fetching rack-protection 2.0.4
Installing rack-protection 2.0.4
Fetching rufus-scheduler 3.5.2
Installing rufus-scheduler 3.5.2
Fetching sinatra 2.0.4
Installing sinatra 2.0.4
Fetching sinatra-contrib 2.0.4
Installing sinatra-contrib 2.0.4
Fetching sucker_punch 2.1.1
Installing sucker_punch 2.1.1
Fetching tzinfo-data 1.2018.7
Installing tzinfo-data 1.2018.7
Fetching zipkin-tracer 0.30.0
Installing zipkin-tracer 0.30.0
Bundle complete! 11 Gemfile dependencies, 33 gems now installed.
Bundled gems are installed into `/usr/local/bundle`
Post-install message from i18n:

HEADS UP! i18n 1.1 changed fallbacks to exclude default locale.
But that may break your application.

Please check your Rails app for 'config.i18n.fallbacks = true'.
If you're using I18n (>= 1.1.0) and Rails (< 5.2.2), this should be
'config.i18n.fallbacks = [I18n.default_locale]'.
If not, fallbacks will be broken in your app by I18n 1.1.x.

For more info see:
https://github.com/svenfuchs/i18n/releases/tag/v1.1.0

Post-install message from sucker_punch:
Sucker Punch v2.0 introduces backwards-incompatible changes.
Please see https://github.com/brandonhilkert/sucker_punch/blob/master/CHANGES.md#200 for details.
Removing intermediate container 70de0b26eeb7
 ---> 1371b93c5c26
Step 8/13 : ADD . $APP_HOME
 ---> ada96900535d
Step 9/13 : ENV POST_SERVICE_HOST post
 ---> Running in 8c0aa092dde7
Removing intermediate container 8c0aa092dde7
 ---> f914ee4f9740
Step 10/13 : ENV POST_SERVICE_PORT 5000
 ---> Running in d25fa37668fb
Removing intermediate container d25fa37668fb
 ---> d7c371a31dbb
Step 11/13 : ENV COMMENT_SERVICE_HOST comment
 ---> Running in fd864a53bd0a
Removing intermediate container fd864a53bd0a
 ---> 7537ff8dec53
Step 12/13 : ENV COMMENT_SERVICE_PORT 9292
 ---> Running in 30470579b4be
Removing intermediate container 30470579b4be
 ---> 78ca5da620ac
Step 13/13 : CMD ["puma"]
 ---> Running in 64a9d394f5a6
Removing intermediate container 64a9d394f5a6
 ---> dede21880119
Successfully built dede21880119
Successfully tagged r2d2k/ui:1.0

Статистика по собранным образам:

> docker images
REPOSITORY      TAG            IMAGE ID       CREATED          SIZE
r2d2k/ui        1.0            dede21880119   3 minutes ago    772MB
r2d2k/comment   1.0            5ed27e0339a7   5 minutes ago    769MB
r2d2k/post      1.0            b438f345df77   11 minutes ago   111MB
mongo           latest         b70536aeb250   24 hours ago     695MB
ruby            2.2            6c8e6f9667b2   4 years ago      715MB
python          3.6.0-alpine   cb178ebbf0f2   5 years ago      88.6MB

Сборка ui началась не с первого шага, так как у нас уже есть слои, подготовленные при сборке предыдущего приложения. Они совпали и были использованы в работе.

Создаём отдельную сеть под приложения:

> docker network create reddit
da565863ffc207b4b5096a410a689b378388a34221fe582587a422ea4989ac18

> docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
318eb18767d9   bridge    bridge    local
f9d28c74ca97   host      host      local
6a9dc35c3605   none      null      local
da565863ffc2   reddit    bridge    local

Создадим и запустим контейнеры с приложениями:

docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:latest
docker run -d --network=reddit --network-alias=post r2d2k/post:1.0
docker run -d --network=reddit --network-alias=comment r2d2k/comment:1.0
docker run -d --network=reddit -p 9292:9292 r2d2k/ui:1.0

> docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS         PORTS                                       NAMES
f4d33854842a   r2d2k/ui:1.0        "puma"                   2 minutes ago   Up 2 minutes   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   flamboyant_carver
6cce41e5d607   r2d2k/comment:1.0   "puma"                   2 minutes ago   Up 2 minutes                                               distracted_haslett
b2881bbc8ea9   mongo:latest        "docker-entrypoint.s…"   3 minutes ago   Up 2 minutes   27017/tcp                                   zealous_rubin

Мы запускали четыре контейнера, а в работе всего три. Ищем четвёртый:

> docker ps -a
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS                     PORTS                                       NAMES
f4d33854842a   r2d2k/ui:1.0        "puma"                   4 minutes ago   Up 4 minutes               0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   flamboyant_carver
6cce41e5d607   r2d2k/comment:1.0   "puma"                   4 minutes ago   Up 4 minutes                                                           distracted_haslett
4aed397da8ce   r2d2k/post:1.0      "python3 post_app.py"    4 minutes ago   Exited (1) 4 minutes ago                                               suspicious_ishizaka
b2881bbc8ea9   mongo:latest        "docker-entrypoint.s…"   4 minutes ago   Up 4 minutes               27017/tcp                                   zealous_rubin

Смотрим, логи:

> docker logs suspicious_ishizaka
Traceback (most recent call last):
  File "post_app.py", line 7, in <module>
    from flask import Flask, request, Response, abort, logging
  File "/usr/local/lib/python3.6/site-packages/flask/__init__.py", line 19, in <module>
    from jinja2 import Markup, escape
  File "/usr/local/lib/python3.6/site-packages/jinja2/__init__.py", line 8, in <module>
    from .environment import Environment as Environment
  File "/usr/local/lib/python3.6/site-packages/jinja2/environment.py", line 15, in <module>
    from markupsafe import Markup
ImportError: cannot import name 'Markup'

Не найден один из модулей. Изучаем лог сборки и видим, что пакет MarkupSafe не смог установиться. Виной всему устаревшая версия pip. Добавляем в Dockerfile шаг обновления pip.

FROM python:3.6.0-alpine

WORKDIR /app
ADD . /app

RUN apk --no-cache --update add build-base && \
    pip install --upgrade pip && \
    pip install -r /app/requirements.txt && \
    apk del build-base

ENV POST_DATABASE_HOST post_db
ENV POST_DATABASE posts

ENTRYPOINT ["python3", "post_app.py"]

После сборки образа запускаем, проверяем, что все контейнеры на месте:

> docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                                       NAMES
f65870a45dd4   r2d2k/post:1.0      "python3 post_app.py"    4 seconds ago    Up 2 seconds                                                objective_darwin
f4d33854842a   r2d2k/ui:1.0        "puma"                   30 minutes ago   Up 30 minutes   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   flamboyant_carver
6cce41e5d607   r2d2k/comment:1.0   "puma"                   31 minutes ago   Up 30 minutes                                               distracted_haslett
b2881bbc8ea9   mongo:latest        "docker-entrypoint.s…"   31 minutes ago   Up 31 minutes   27017/tcp                                   zealous_rubin

Статистика по собранным образам:

> docker images
REPOSITORY      TAG            IMAGE ID       CREATED          SIZE
r2d2k/post      1.0            f5721bc9c435   3 minutes ago    121MB
r2d2k/ui        1.0            dede21880119   42 minutes ago   772MB
r2d2k/comment   1.0            5ed27e0339a7   44 minutes ago   769MB
mongo           latest         b70536aeb250   25 hours ago     695MB
ruby            2.2            6c8e6f9667b2   4 years ago      715MB
python          3.6.0-alpine   cb178ebbf0f2   5 years ago      88.6MB

Проверяем, что приложение отвечает на запросы.

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in f4d33854842a container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/
   4. http://127.0.0.1:9292/new

Выше видно ошибку "Can't show blog posts, some problems with the post service". Вызвана она тем, что мы используем слишком старый драйвер БД. Подробности можно узнать на официальном сайте MongoDB. Попробуем запустить БД версии ниже шестой.

> docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                                       NAMES
9cbb73073be2   r2d2k/ui:1.0        "puma"                   11 minutes ago   Up 11 minutes   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   cool_greider
88e8909ad1cc   r2d2k/comment:1.0   "puma"                   11 minutes ago   Up 11 minutes                                               charming_tereshkova
944b15ca7947   r2d2k/post:1.0      "python3 post_app.py"    11 minutes ago   Up 11 minutes                                               pedantic_banzai
b449cbff86b3   mongo:latest        "docker-entrypoint.s…"   11 minutes ago   Up 11 minutes   27017/tcp                                   competent_satoshi

> docker rm -f b449cbff86b3
b449cbff86b3

> docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:5.0
Unable to find image 'mongo:5.0' locally
5.0: Pulling from library/mongo
eaead16dc43b: Already exists
8a00eb9f68a0: Already exists
f683956749c5: Already exists
b33b2f05ea20: Already exists
3a342bea915a: Already exists
ff038722853e: Pull complete
a1c21bd7f89a: Pull complete
1266ae82d139: Pull complete
b95aca638f91: Pull complete
Digest: sha256:26b6f0ef7e3e0dccf5c3687c5bf61ead6a60b59cc613668eabf2ca80a6fd7c75
Status: Downloaded newer image for mongo:5.0
782a6951a7363f3920a9901ec57f4ff66bbbe950e626ecfd07c4bcbbcf0c00ab

Прекрасно, записи добавляются без ошибок.

Результат №02-1: Собраны три образа, запущены в одной сети с базой данных. Исправлены ошибки в работе приложения


Задание №02-2:

  • Остановите контейнеры docker kill $(docker ps -q)
  • Запустите контейнеры с другими сетевыми алиасами
  • Адреса для взаимодействия контейнеров задаются через ENV-переменные внутри Dockerfile
  • При запуске контейнеров docker run задайте им переменные окружения соответствующие новым сетевым алиасам, не пересоздавая образ
  • Проверьте работоспособность сервиса

Решение №02-2: Соберём данные по переменным окружения контейнеров.

Сервис Значение по умолчанию Новое значение
ui POST_SERVICE_HOST post POST_SERVICE_HOST wow_post
ui COMMENT_SERVICE_HOST comment COMMENT_SERVICE_HOST wow_comment
post-py POST_DATABASE_HOST post_db POST_DATABASE_HOST wow_post_db
comment COMMENT_DATABASE_HOST comment_db COMMENT_DATABASE_HOST wow_comment_db

Переменные передаём в контейнеры через ключ -e:

> docker run -d --network=reddit --network-alias=wow_post_db --network-alias=wow_comment_db mongo:5.0
> docker run -d --network=reddit --network-alias=wow_post -e POST_DATABASE_HOST=wow_post_db r2d2k/post:1.0
> docker run -d --network=reddit --network-alias=wow_comment -e COMMENT_DATABASE_HOST=wow_comment_db r2d2k/comment:1.0
> docker run -d --network=reddit -p 9292:9292 -e POST_SERVICE_HOST=wow_post -e COMMENT_SERVICE_HOST=wow_comment r2d2k/ui:1.0

Приложение работает:

> docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS         PORTS                                       NAMES
1825f9cd5485   r2d2k/ui:1.0        "puma"                   9 seconds ago    Up 8 seconds   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   inspiring_satoshi
02c2bc5b5865   r2d2k/comment:1.0   "puma"                   9 seconds ago    Up 8 seconds                                               tender_colden
9305ac7e7bd7   r2d2k/post:1.0      "python3 post_app.py"    10 seconds ago   Up 9 seconds                                               silly_ishizaka
7d4429630943   mongo:5.0           "docker-entrypoint.s…"   10 seconds ago   Up 9 seconds   27017/tcp                                   zen_boyd

На запросы отвечает:

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 1825f9cd5485 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Результат №02-2: Контейнеры запущены с другими сетевыми алиасами без пересборки контейнеров. Имена соседних контейнеров передаём через переменные окружения.


Результат №02-1: Собраны три образа, запущены в одной сети с базой данных. Исправлены ошибки в работе приложения


Задание №02-3: Оптимизация размера образов.

  • Попробуйте собрать образ на основе Alpine Linux
  • Придумайте еще способы уменьшить размер образа
  • Можете реализовать как только для ui, так и для post, comment
  • Все оптимизации проводите в Dockerfile сервиса. Дополнительные варианты решения уменьшения размера образов можете оформить в виде файла Dockerfile.<цифра> в папке сервиса

Решение №02-3: Исходные данные:

> docker images
REPOSITORY      TAG            IMAGE ID       CREATED             SIZE
r2d2k/post      1.0            f5721bc9c435   About an hour ago   121MB
r2d2k/ui        1.0            dede21880119   2 hours ago         772MB
r2d2k/comment   1.0            5ed27e0339a7   2 hours ago         769MB
mongo           5.0            cb51c58bc695   26 hours ago        698MB
ruby            2.2            6c8e6f9667b2   4 years ago         715MB
python          3.6.0-alpine   cb178ebbf0f2   5 years ago         88.6MB

Обновим ui/Dockerfile:

--- a/src/ui/Dockerfile
+++ b/src/ui/Dockerfile
@@ -1,5 +1,7 @@
-FROM ruby:2.2
-RUN apt-get update -qq && apt-get install -y build-essential
+FROM ubuntu:16.04
+RUN apt-get update \
+    && apt-get install -y ruby-full ruby-dev build-essential \
+    && gem install bundler --no-ri --no-rdoc

 ENV APP_HOME /app
 RUN mkdir $APP_HOME

Собираем образ заново, смотрим, как изменился размер:

> docker build -t r2d2k/ui:2.0 ./ui
...
...
...

> docker images r2d2k/ui
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
r2d2k/ui     2.0       538bcf31c707   3 minutes ago   464MB
r2d2k/ui     1.0       dede21880119   2 hours ago     772MB

Переведём образ ui на Alpine Linux. Так, как наше приложение скончается в мучениях, если мы попробуем запустить его на новой версии ruby, то придётся заняться археологией. Идём изучать пакеты Alpine Linux на предмет ruby2.3. Подходящую версию находим в Alpine Linux v3.4.

Готовим Dockerfile.1:

--- ./ui/Dockerfile     2022-10-26 19:43:40.061221284 +0000
+++ ./ui/Dockerfile.1   2022-10-26 20:24:50.443688442 +0000
@@ -1,7 +1,5 @@
-FROM ubuntu:16.04
-RUN apt-get update \
-    && apt-get install -y ruby-full ruby-dev build-essential \
-    && gem install bundler --no-ri --no-rdoc
+FROM alpine:3.4
+RUN apk update && apk add ruby-bundler ruby-json ruby-dev build-base

 ENV APP_HOME /app
 RUN mkdir $APP_HOME

Так как версия очень старая, то она испытывает проблемы с проверкой сертификатов. Переводим Gemfile с https на http. Смотрим, что получается по размеру образов:

> docker images r2d2k/ui
REPOSITORY   TAG          IMAGE ID       CREATED             SIZE
r2d2k/ui     3.0-alpine   59201283bad6   18 seconds ago      192MB
r2d2k/ui     2.0          538bcf31c707   About an hour ago   464MB
r2d2k/ui     1.0          dede21880119   3 hours ago         772MB

Можно выкинуть одну команду:

--- ./ui/Dockerfile.1   2022-10-26 20:48:18.903089747 +0000
+++ ./ui/Dockerfile.2   2022-10-26 20:51:59.482139742 +0000
@@ -2,7 +2,6 @@
 RUN apk update && apk add ruby-bundler ruby-json ruby-dev build-base

 ENV APP_HOME /app
-RUN mkdir $APP_HOME

 WORKDIR $APP_HOME
 ADD Gemfile* $APP_HOME/

После сборки образа размер не изменился:

> docker images r2d2k/ui
REPOSITORY   TAG          IMAGE ID       CREATED             SIZE
r2d2k/ui     3.1-alpine   735b5108b497   2 minutes ago       192MB
r2d2k/ui     3.0-alpine   59201283bad6   5 minutes ago       192MB
r2d2k/ui     2.0          538bcf31c707   About an hour ago   464MB
r2d2k/ui     1.0          dede21880119   3 hours ago         772MB

Переведём comment на Alpine Linux:

--- ./comment/Dockerfile        2022-10-26 05:42:22.019148857 +0000
+++ ./comment/Dockerfile.1      2022-10-26 21:13:11.927702796 +0000
@@ -1,5 +1,5 @@
-FROM ruby:2.2
-RUN apt-get update -qq && apt-get install -y build-essential
+FROM alpine:3.4
+RUN apk update && apk add ruby-bundler ruby-json ruby-dev build-base

 ENV APP_HOME /app
 RUN mkdir $APP_HOME

Размер образа значительно уменьшился:

> docker images r2d2k/comment
REPOSITORY      TAG          IMAGE ID       CREATED              SIZE
r2d2k/comment   2.2-alpine   e3c05b3dada6   44 seconds ago       189MB
r2d2k/comment   2.0-alpine   e0b20e2aad8e   About a minute ago   189MB
r2d2k/comment   1.0          5ed27e0339a7   4 hours ago          769MB

Запустим приложение:

> docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:5.0
> docker run -d --network=reddit --network-alias=post r2d2k/post:1.0
> docker run -d --network=reddit --network-alias=comment r2d2k/comment:2.2-alpine
> docker run -d --network=reddit -p 9292:9292 r2d2k/ui:3.1-alpine

> docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                                       NAMES
8711fb05d8a0   r2d2k/ui:3.1-alpine        "puma"                   9 seconds ago    Up 7 seconds    0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   quirky_archimedes
5fe2f10847be   r2d2k/comment:2.2-alpine   "puma"                   11 seconds ago   Up 9 seconds                                                focused_matsumoto
cb5cbe4136ff   r2d2k/post:1.0             "python3 post_app.py"    13 seconds ago   Up 11 seconds                                               reverent_franklin
d34d689c91c1   mongo:5.0                  "docker-entrypoint.s…"   18 seconds ago   Up 13 seconds   27017/tcp                                   blissful_chandrasekhar

Проверим, что всё работает:

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 8711fb05d8a0 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Всё работает, но список постов пуст. При перезапуске контейнера с базой данных все записи пропали. Для постоянного хранения данных контейнера можно использовать Docker volume.

Создаём Docker volume, перезапускаем контейнер базы с ним:

> docker volume create reddit_db
reddit_db

> docker rm -f d34d689c91c1
d34d689c91c1

> docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db -v reddit_db:/data/db mongo:5.0

> docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                                       NAMES
fa116790680b   mongo:5.0                  "docker-entrypoint.s…"   20 seconds ago   Up 18 seconds   27017/tcp                                   epic_galois
8711fb05d8a0   r2d2k/ui:3.1-alpine        "puma"                   8 minutes ago    Up 8 minutes    0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   quirky_archimedes
5fe2f10847be   r2d2k/comment:2.2-alpine   "puma"                   8 minutes ago    Up 8 minutes                                                focused_matsumoto
cb5cbe4136ff   r2d2k/post:1.0             "python3 post_app.py"    8 minutes ago    Up 8 minutes                                                reverent_franklin

Создадим запись в приложении через браузер, проверим, что запись на месте:

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 8711fb05d8a0 container
   (BUTTON)
0
   (BUTTON)

[2]Yandex
26-10-2022
21:38

   [3]Go to the link

Menu

     * [4]All posts
     * [5]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/post/6359a8c9c4c60c000e04e36e
   3. http://yandex.ru/
   4. http://127.0.0.1:9292/
   5. http://127.0.0.1:9292/new

Перезапустим контейнер с базой, проверим, что запись не исчезла:

> docker stop fa116790680b
fa116790680b

> docker start fa116790680b
fa116790680b

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 8711fb05d8a0 container
   (BUTTON)
0
   (BUTTON)

[2]Yandex
26-10-2022
21:38

   [3]Go to the link

Menu

     * [4]All posts
     * [5]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/post/6359a8c9c4c60c000e04e36e
   3. http://yandex.ru/
   4. http://127.0.0.1:9292/
   5. http://127.0.0.1:9292/new

Всё работает.

Результат №02-3:

  • Мы уменьшили размер образа за счёт использования более компактного базового образа
  • Мы сохранили данные контейнера между перезапусками, за счёт использования Docker volume

03 - Docker: сети, docker-compose

Задание №03-1:

  • Работа с сетями в Docker: none, host, bridge
  • Использование docker-compose

Решение №03-1:

Создаём новую ветку docker-4, работаем в каталоге src. За базу берём образ joffotron/docker-net-tools, так как он содержит все необходимые сетевые утилиты. Первый запуск, при этом укажем сеть в none:

> docker run -it --rm --network none joffotron/docker-net-tools -c ifconfig
Unable to find image 'joffotron/docker-net-tools:latest' locally
latest: Pulling from joffotron/docker-net-tools
3690ec4760f9: Pull complete
0905b79e95dc: Pull complete
Digest: sha256:5752abdc4351a75e9daec681c1a6babfec03b317b273fc56f953592e6218d5b5
Status: Downloaded newer image for joffotron/docker-net-tools:latest

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

Видно, что loopback это единственный интерфейс, доступный контейнеру. Попробуем запустить контейнер, подключив его к сети хоста:

> docker run -ti --rm --network host joffotron/docker-net-tools -c ifconfig
br-da565863ffc2 Link encap:Ethernet  HWaddr 02:42:63:4F:AD:03
          inet addr:172.18.0.1  Bcast:172.18.255.255  Mask:255.255.0.0
          inet6 addr: fe80::42:63ff:fe4f:ad03%32722/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:7289 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7270 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:475691 (464.5 KiB)  TX bytes:812092 (793.0 KiB)

docker0   Link encap:Ethernet  HWaddr 02:42:6F:C8:2F:52
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          inet6 addr: fe80::42:6fff:fec8:2f52%32722/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:370721 errors:0 dropped:0 overruns:0 frame:0
          TX packets:833566 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:21309396 (20.3 MiB)  TX bytes:1496060294 (1.3 GiB)

eth0      Link encap:Ethernet  HWaddr B6:70:98:38:A4:3F
          inet addr:192.168.10.96  Bcast:192.168.10.255  Mask:255.255.255.0
          inet6 addr: fe80::b470:98ff:fe38:a43f%32722/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5056995 errors:0 dropped:161 overruns:0 frame:0
          TX packets:877421 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:3424447104 (3.1 GiB)  TX bytes:327494245 (312.3 MiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1%32722/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:2048 errors:0 dropped:0 overruns:0 frame:0
          TX packets:2048 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:329008 (321.2 KiB)  TX bytes:329008 (321.2 KiB)

Сравним с интерфейсами на локальном хосте:

> ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether b6:70:98:38:a4:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:6f:c8:2f:52 brd ff:ff:ff:ff:ff:ff
46: br-da565863ffc2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:63:4f:ad:03 brd ff:ff:ff:ff:ff:ff

Видим, что все локальные интерфейсы доступны и внутри контейнера. Попробуем несколько раз запустить контейнер с использованием сети хоста:

> docker run --network host -d nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
e9995326b091: Pull complete
71689475aec2: Pull complete
f88a23025338: Pull complete
0df440342e26: Pull complete
eef26ceb3309: Pull complete
8e3ed6a9e43a: Pull complete
Digest: sha256:47a8d86548c232e44625d813b45fd92e81d07c639092cd1f9a49d98e1fb5f737
Status: Downloaded newer image for nginx:latest
f75b0c9f9db9500e859873fc80caeb12b2135dfce7b539f0f625f3529a3d1e56

> docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
f75b0c9f9db9   nginx     "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds             interesting_spence

> docker run --network host -d nginx
5e7cfb2e810accd7cc7c73ee27f4cd129c5e4d49a36feb6eacf118007a692c40

> docker run --network host -d nginx
b77474b4f4560acc3508a17d5507115f4efed447c58689f9c8c2a40310a84ca6

> docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                      PORTS     NAMES
b77474b4f456   nginx     "/docker-entrypoint.…"   20 seconds ago   Exited (1) 17 seconds ago             confident_black
5e7cfb2e810a   nginx     "/docker-entrypoint.…"   22 seconds ago   Exited (1) 19 seconds ago             affectionate_keller
f75b0c9f9db9   nginx     "/docker-entrypoint.…"   29 seconds ago   Up 28 seconds                         interesting_spence

В работе остаётся только контейнер, который был запущен первым. Почему?

> docker logs b77474b4f456
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/13/27 04:40:46 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/13/27 04:40:46 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2022/13/27 04:40:46 [notice] 1#1: try again to bind() after 500ms
2022/13/27 04:40:46 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/13/27 04:40:46 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2022/13/27 04:40:46 [notice] 1#1: try again to bind() after 500ms
2022/13/27 04:40:46 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/13/27 04:40:46 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2022/13/27 04:40:46 [notice] 1#1: try again to bind() after 500ms
2022/13/27 04:40:46 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/13/27 04:40:46 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2022/13/27 04:40:46 [notice] 1#1: try again to bind() after 500ms
2022/13/27 04:40:46 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2022/13/27 04:40:46 [emerg] 1#1: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2022/13/27 04:40:46 [notice] 1#1: try again to bind() after 500ms
2022/13/27 04:40:46 [emerg] 1#1: still could not bind()
nginx: [emerg] still could not bind()

Всё правильно, порт уже занят первым контейнером. Сеть хоста одна, нужно следить, чтобы порты контейнеров не пересекались. При запуске контейнера с сетью none каждый раз создаётся отдельное пространство имён, чего не происходит при использовании сети host.

Создадим сеть с драйвером bridge:

> docker network create reddit --driver bridge
1e340640165944261656d5af9ccc5661d98de29dd56749f197f2497d3985726e

> docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
318eb18767d9   bridge    bridge    local
f9d28c74ca97   host      host      local
6a9dc35c3605   none      null      local
1e3406401659   reddit    bridge    local

Образов у нас много, поэтому, чтобы не путаться, присвоим текущим образам тег latest:

> docker images
REPOSITORY                   TAG            IMAGE ID       CREATED         SIZE
r2d2k/comment                2.2-alpine     e3c05b3dada6   8 hours ago     189MB
r2d2k/comment                2.0-alpine     e0b20e2aad8e   8 hours ago     189MB
r2d2k/ui                     3.1-alpine     735b5108b497   8 hours ago     192MB
r2d2k/ui                     3.0-alpine     59201283bad6   8 hours ago     192MB
r2d2k/ui                     2.0            538bcf31c707   9 hours ago     464MB
r2d2k/post                   1.0            f5721bc9c435   11 hours ago    121MB
r2d2k/ui                     1.0            dede21880119   11 hours ago    772MB
r2d2k/comment                1.0            5ed27e0339a7   11 hours ago    769MB
mongo                        5.0            cb51c58bc695   35 hours ago    698MB
nginx                        latest         76c69feac34e   43 hours ago    142MB
ubuntu                       18.04          71eaf13299f4   2 days ago      63.1MB
ubuntu                       16.04          b6f507652425   14 months ago   135MB
alpine                       3.4            b7c5ffe56db7   3 years ago     4.81MB
ruby                         2.2            6c8e6f9667b2   4 years ago     715MB
python                       3.6.0-alpine   cb178ebbf0f2   5 years ago     88.6MB
joffotron/docker-net-tools   latest         b97158e38a06   5 years ago     10.6MB

> docker images r2d2k/comment
REPOSITORY      TAG          IMAGE ID       CREATED        SIZE
r2d2k/comment   2.2-alpine   e3c05b3dada6   8 hours ago    189MB
r2d2k/comment   2.0-alpine   e0b20e2aad8e   8 hours ago    189MB
r2d2k/comment   1.0          5ed27e0339a7   11 hours ago   769MB

> docker tag r2d2k/comment:2.2-alpine r2d2k/comment:latest

> docker images r2d2k/ui
REPOSITORY   TAG          IMAGE ID       CREATED        SIZE
r2d2k/ui     3.1-alpine   735b5108b497   8 hours ago    192MB
r2d2k/ui     3.0-alpine   59201283bad6   8 hours ago    192MB
r2d2k/ui     2.0          538bcf31c707   9 hours ago    464MB
r2d2k/ui     1.0          dede21880119   11 hours ago   772MB

> docker tag r2d2k/ui:3.1-alpine r2d2k/ui:latest

> docker images r2d2k/post
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
r2d2k/post   1.0       f5721bc9c435   11 hours ago   121MB

> docker tag r2d2k/post:1.0 r2d2k/post:latest

Запустим наше приложение с использованием созданной сети:

> docker run -d --network=reddit mongo:5.0
> docker run -d --network=reddit r2d2k/post:latest
> docker run -d --network=reddit r2d2k/comment:latest
> docker run -d --network=reddit -p 9292:9292 r2d2k/ui:latest

Проверим, что вышло:

> docker ps -a
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS                                       NAMES
6dd3cdd87d1a   r2d2k/ui:latest        "puma"                   5 seconds ago    Up 3 seconds    0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   optimistic_dijkstra
4684755c0cda   r2d2k/comment:latest   "puma"                   11 seconds ago   Up 9 seconds                                                boring_bhabha
13e02ac3b982   r2d2k/post:latest      "python3 post_app.py"    17 seconds ago   Up 16 seconds                                               compassionate_albattani
8154487d77e9   mongo:5.0              "docker-entrypoint.s…"   25 seconds ago   Up 24 seconds   27017/tcp                                   magical_antonelli

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 6dd3cdd87d1a container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/
   4. http://127.0.0.1:9292/new

Контейнеры запущены, но приложение не работает. Контейнеры обращаются к соседям по определённым именам, о которых процессу docker ничего неизвестно.

Перезапустим контейнеры с указанием имён:

> bash -c 'docker rm -f $(docker ps -q -a)'

> docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:5.0
> docker run -d --network=reddit --network-alias=post r2d2k/post:latest
> docker run -d --network=reddit --network-alias=comment r2d2k/comment:latest
> docker run -d --network=reddit -p 9292:9292 r2d2k/ui:latest
> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in fa81d2be7c57 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Теперь всё в порядке, контейнеры видят соседей. Давайте сделаем так, чтобы ui не видели друг друга db, разнесём их по разным сетям.

                   ┌──────────────────────────────────┐
                   │                       back_net   │
┌──────────────────┼──────────────┐                   │
│    front_net     │              │                   │
│                  │  ┌─────────┐ │                   │
│                  │  │ comment │ │                   │
│                  │  └─────────┘ │                   │
│                  │              │                   │
│   ┌────────┐     │              │        ┌──────┐   │
│   │   ui   │     │              │        │  db  │   │
│   └────────┘     │              │        └──────┘   │
│                  │   ┌──────┐   │                   │
│                  │   │ post │   │                   │
│                  │   └──────┘   │                   │
│                  │              │                   │
└──────────────────┼──────────────┘                   │
                   │                                  │
                   └──────────────────────────────────┘

Останавливаем все контейнеры и создаём две сети:

> bash -c 'docker rm -f $(docker ps -q -a)'
> docker network create back_net --subnet=10.0.2.0/24
> docker network create front_net --subnet=10.0.1.0/24
> docker network ls
NETWORK ID     NAME        DRIVER    SCOPE
182b52db77ae   back_net    bridge    local
318eb18767d9   bridge      bridge    local
19428cd51adb   front_net   bridge    local
f9d28c74ca97   host        host      local
6a9dc35c3605   none        null      local
5e20b3a67720   reddit      bridge    local

Запускаем контейнеры с указанием сетей:

> docker run -d --network=back_net --name=mongo_db --network-alias=post_db --network-alias=comment_db mongo:5.0
> docker run -d --network=back_net --name=post r2d2k/post:latest
> docker run -d --network=back_net --name=comment r2d2k/comment:latest
> docker run -d --network=front_net --name ui -p 9292:9292 r2d2k/ui:latest

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in a99ed4847758 container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/
   4. http://127.0.0.1:9292/new

При инициализации контейнера к нему можно подключить только одну сеть. Остальные сети подключаются после старта.

> docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS                                       NAMES
a99ed4847758   r2d2k/ui:latest        "puma"                   29 seconds ago   Up 28 seconds   0.0.0.0:9292->9292/tcp, :::9292->9292/tcp   ui
393791b22e4f   r2d2k/comment:latest   "puma"                   37 seconds ago   Up 36 seconds                                               comment
4a210b2c1f85   r2d2k/post:latest      "python3 post_app.py"    44 seconds ago   Up 43 seconds                                               post
761672a22f37   mongo:5.0              "docker-entrypoint.s…"   53 seconds ago   Up 52 seconds   27017/tcp                                   mongo_db

> docker network connect front_net 305bfd5b67a2
> docker network connect front_net 80343670ae2b
> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in a99ed4847758 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Результат получен, пара контейнеров работает в двух сетях, а ui и db разнесены. Посмотрим поближе на сеть контейнеров, для этого нам нужен инструмент: sudo apt install bridge-utils.

Вот список сетей docker с драйвером bridge:

)> docker network ls --filter driver=bridge
NETWORK ID     NAME        DRIVER    SCOPE
182b52db77ae   back_net    bridge    local
318eb18767d9   bridge      bridge    local
19428cd51adb   front_net   bridge    local
5e20b3a67720   reddit      bridge    local

А вот список бриджей локальной системы:

> ip link | grep ": br-" | cut -d":" -f2
 br-5e20b3a67720
 br-182b52db77ae
 br-19428cd51adb

Посмотрим на эти интерфейсы более подробно:

> brctl show
bridge name             bridge id               STP enabled     interfaces
br-182b52db77ae         8000.0242a04a32f1       no              vethc13f63f
                                                                vethcf98cab
                                                                vethd753d3f
br-19428cd51adb         8000.024237fcf040       no              veth7727a7b
                                                                vethbc08305
                                                                vethf8df15d
br-5e20b3a67720         8000.0242d7c4ae2a       no
docker0                 8000.02426fc82f52       no

Видно, что у каждого бриджа есть по три интерфейса. Один смотрит в сеть хоста, остальные в контейнеры.

Поднимемся на уровень повыше:

> sudo iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  10.0.1.0/24          0.0.0.0/0
MASQUERADE  all  --  10.0.2.0/24          0.0.0.0/0
MASQUERADE  all  --  172.20.0.0/16        0.0.0.0/0
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0
MASQUERADE  tcp  --  10.0.1.2             10.0.1.2             tcp dpt:9292

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:9292 to:10.0.1.2:9292

В цепочке POSTROUTING видим правила для наших бриджей. В цепочке DOCKER видим правила, перенаправляющие трафик внутрь контейнера. В этом также участвуют docker-proxy:

> ps ax | grep docker-proxy
 482048 ?        Sl     0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9292 -container-ip 10.0.1.2 -container-port 9292
 482055 ?        Sl     0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 9292 -container-ip 10.0.1.2 -container-port 9292

Результат №03-1:

  • Рассмотрели различные виды сетей
  • Использовали несколько способов организации связей между контейнерами
  • Увидели, как сети контейнеров связаны с реальными сетями хоста

Задание №03-2

  • Установить docker-compose на локальную машину
  • Собрать образы приложения reddit с помощью docker-compose
  • Запустить приложение reddit с помощью docker-compose

Решение №03-2

Для начала удалим следы от предыдущих экспериментов:

> bash -c 'docker rm -f $(docker ps -q -a)'
a99ed4847758
393791b22e4f
4a210b2c1f85
761672a22f37

> bash -c 'docker volume rm $(docker volume ls -q)'
16b9634e7c9e63015fae0334498af00b269f9e0b69e75a4852b561a11828fb2e
793a977e33aedc9caa4706010bd16169bb8156a8f6e2b2a35f15d4a397e2d45d
74873ffc81d15f16ee620329a8991d04fee6e63ea9fb9e6e7be6525e881d64ff
a04f681d79f6049fd2385b57acd741d00275bc8240ea45dad0e4a5328cafe2f7
cd233e3af859ef9532e80968e66e5fae6eda80a17792ecb3e2ceb3a566a33aa9
da63807876f436a5c5e730f3e38c115f1642547b5a143c8cbeef3efc0a60c311
e9176854722a6e2e75bcb8fa12e72bdcffb403914cf7a167b1c515fc6d1ba1be
efb6333e7649a436a6125d47b3f0ab0207e722ad7023f44df42e84e3f64880ad

Проверим, что docker-compose у нас установлен:

> docker compose version
Docker Compose version v2.6.0

В каталоге src создаём docker-compose.yml:

version: '3.3'
services:
  post_db:
    image: mongo:3.2
    volumes:
      - post_db:/data/db
    networks:
      - reddit
  ui:
    build: ./ui
    image: ${USERNAME}/ui:1.0
    ports:
      - 9292:9292/tcp
    networks:
      - reddit
  post:
    build: ./post-py
    image: ${USERNAME}/post:1.0
    networks:
      - reddit
  comment:
    build: ./comment
    image: ${USERNAME}/comment:1.0
    networks:
      - reddit

volumes:
  post_db:

networks:
  reddit:

Экспортируем переменную окружения с нашим логином, запускаем приложение:

> export USERNAME=r2d2k
> docker compose up -d
[+] Running 12/12
 ⠿ post_db Pulled                                                                                                                                               14.4s
   ⠿ a92a4af0fb9c Pull complete                                                                                                                                  2.9s
   ⠿ 74a2c7f3849e Pull complete                                                                                                                                  5.4s
   ⠿ 927b52ab29bb Pull complete                                                                                                                                  6.2s
   ⠿ e941def14025 Pull complete                                                                                                                                  6.4s
   ⠿ be6fce289e32 Pull complete                                                                                                                                  6.5s
   ⠿ f6d82baac946 Pull complete                                                                                                                                  6.7s
   ⠿ 7c1a640b9ded Pull complete                                                                                                                                  6.8s
   ⠿ e8b2fc34c941 Pull complete                                                                                                                                 12.0s
   ⠿ 1fd822faa46a Pull complete                                                                                                                                 12.1s
   ⠿ 61ba5f01559c Pull complete                                                                                                                                 12.3s
   ⠿ db344da27f9a Pull complete                                                                                                                                 12.3s
[+] Running 6/6
 ⠿ Network src_reddit       Created                                                                                                                              0.0s
 ⠿ Volume "src_post_db"     Created                                                                                                                              0.0s
 ⠿ Container src-post-1     Started                                                                                                                              2.9s
 ⠿ Container src-post_db-1  Started                                                                                                                              2.9s
 ⠿ Container src-comment-1  Started                                                                                                                              2.7s
 ⠿ Container src-ui-1       Started                                                                                                                              2.7s

> docker compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
src-comment-1       "puma"                   comment             running
src-post-1          "python3 post_app.py"    post                running
src-post_db-1       "docker-entrypoint.s…"   post_db             running             27017/tcp
src-ui-1            "puma"                   ui                  running             0.0.0.0:9292->9292/tcp, :::9292->9292/tcp

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in 8284fef7b90b container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Образы были скачаны, все необходимые объекты созданы, контейнеры запущены и отвечают на запросы.

Чтобы организовать разделение ui и db по сетям, мы приводим docker-compose.yml к следующему виду:

version: '3.3'
services:
  post_db:
    image: mongo:3.2
    volumes:
      - post_db:/data/db
    networks:
      - back_net

  ui:
    build: ./ui
    image: ${USERNAME}/ui:1.0
    ports:
      - 9292:9292/tcp
    networks:
      - front_net

  post:
    build: ./post-py
    image: ${USERNAME}/post:1.0
    networks:
      - back_net
      - front_net

  comment:
    build: ./comment
    image: ${USERNAME}/comment:1.0
    networks:
      - back_net
      - front_net

volumes:
  post_db:

networks:
  front_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.1.0/24

  back_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.2.0/24

Запускаем, проверяем:

> docker compose up -d
[+] Running 6/6
 ⠿ Network src_back_net     Created                                        0.0s
 ⠿ Network src_front_net    Created                                        0.1s
 ⠿ Container src-ui-1       Started                                        1.0s
 ⠿ Container src-post-1     Started                                        1.2s
 ⠿ Container src-comment-1  Started                                        1.4s
 ⠿ Container src-post_db-1  Started                                        1.1s

> docker compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
src-comment-1       "puma"                   comment             running
src-post-1          "python3 post_app.py"    post                running
src-post_db-1       "docker-entrypoint.s…"   post_db             running             27017/tcp
src-ui-1            "puma"                   ui                  running             0.0.0.0:9292->9292/tcp, :::9292->9292/tcp

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in fb4caa51ed88 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Для того, чтобы не задавать переменные окружения перед запуском приложений мы можем поместить их в файл .env. Вынесем все используемые переменные:

USERNAME=r2d2k

# From comment image
COMMENT_DATABASE_HOST=comment_db
COMMENT_DATABASE=comments

# From post image
POST_DATABASE_HOST=post_db
POST_DATABASE=posts

# From ui image
POST_SERVICE_HOST=post
POST_SERVICE_PORT=5000
COMMENT_SERVICE_HOST=comment
COMMENT_SERVICE_PORT=9292

# Web UI Port
UI_PORT=3333

Для изменения порта веб-приложения введём новую переменную UI_PORT, внесём изменения в docker-compose.yml:

--- a/src/docker-compose.yml
+++ b/src/docker-compose.yml
@@ -11,7 +11,7 @@ services:
     build: ./ui
     image: ${USERNAME}/ui:1.0
     ports:
-      - 9292:9292/tcp
+      - ${UI_PORT}:9292/tcp
     networks:
       - front_net

Проверяем, что приложение перезапустится и будет отвечать на новом порту:

> docker compose down
[+] Running 6/6
 ⠿ Container src-ui-1       Removed                                                                                                                              0.5s
 ⠿ Container src-comment-1  Removed                                                                                                                              1.0s
 ⠿ Container src-post-1     Removed                                                                                                                              1.1s
 ⠿ Container src-post_db-1  Removed                                                                                                                              0.8s
 ⠿ Network src_back_net     Removed                                                                                                                              0.1s
 ⠿ Network src_front_net    Removed                                                                                                                              0.2s

> docker compose up -d
[+] Running 6/6
 ⠿ Network src_back_net     Created                                                                                                                              0.0s
 ⠿ Network src_front_net    Created                                                                                                                              0.1s
 ⠿ Container src-comment-1  Started                                                                                                                              1.2s
 ⠿ Container src-ui-1       Started                                                                                                                              0.9s
 ⠿ Container src-post_db-1  Started                                                                                                                              1.1s
 ⠿ Container src-post-1     Started                                                                                                                              1.3s

> docker compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
src-comment-1       "puma"                   comment             running
src-post-1          "python3 post_app.py"    post                running
src-post_db-1       "docker-entrypoint.s…"   post_db             running             27017/tcp
src-ui-1            "puma"                   ui                  running             0.0.0.0:3333->9292/tcp, :::3333->9292/tcp

> lynx -dump http://127.0.0.1:3333
   (BUTTON) [1]Microservices Reddit in ff1a7ab760a4 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:3333/
   2. http://127.0.0.1:3333/
   3. http://127.0.0.1:3333/new
☝️ Базовое имя образа формируется из названия папки и названия контейнера. Его можно изменить при помощи переменной окружения COMPOSE_PROJECT_NAME, либо указать в параметре ключа -p при запуске docker compose.

Для переопределения параметров основного файла docker-compose.yml можно использовать docker-compose.override.yml, который считывается при запуске конфигурации. Все указанные в нём параметры объединяются с параметрами основого файла. Подготовим его:

version: '3.3'
services:
  ui:
    command: puma --debug -w 2
    volumes:
      - ./ui:/app

  post:
    volumes:
      - ./post-py:/app

  comment:
    command: puma --debug -w 2
    volumes:
      - ./comment:/app
  • Мы переопределили параметры запуска конейнеров с ruby.
  • Мы также примонтировали каталоги с кодом внутрь контейнера, заменив установленное там приложение, получили вожможность менять код без пересборки контейнеров.

Результат №03-2

  • Получен опыт работы с docker-compose

04 - Устройство Gitlab CI. Построение процесса непрерывной поставки

Задание №04-1:

  • Подготовить инсталляцию Gitlab CI
  • Подготовить репозиторий с кодом приложения
  • Описать для приложения этапы пайплайна
  • Определить окружения

Решение №04-1: Создаём новую ветку git checkout -b gitlab-ci-1.

Проводить изыскания будем на Gitlab CE (community edition). Идём на офциальный сайт Gitlab, изучаем системные требования.

Минимальная конфигурация на 500 пользователей:

  • 4 core CPU
  • 4 GB RAM
  • 2.5 GB HDD (только Gitlab)

Для себя примем конфигурацию в 2 core СPU, 4 GB RAM, 50 GB HDD. Машину создаём в облаке Яндекс, используем terraform. Сначала подготовим образ машины при помощи packer, за основу возьмём Ubuntu 22.04 LTS.

Содержимое ubuntu-docker.json:

{
    "variables": {
        "mv_service_account_key_file": "",
        "mv_folder_id": "",
        "mv_source_image_family": ""
    },
    "builders": [
        {
            "type": "yandex",
            "service_account_key_file": "{{user `mv_service_account_key_file`}}",
            "folder_id": "{{user `mv_folder_id`}}",
            "source_image_family": "{{user `mv_source_image_family`}}",
            "image_name": "{{user `mv_image_family`}}-{{timestamp}}",
            "image_family": "{{user `mv_image_family`}}",
            "ssh_username": "ubuntu",
            "platform_id": "standard-v1",
            "use_ipv4_nat": "true"
        }
    ],
    "provisioners": [
        {
            "type": "shell",
            "pause_before": "60s",
            "script": "scripts/install_docker.sh",
            "execute_command": "sudo {{.Path}}"
        },
        {
            "type": "shell",
            "script": "scripts/cleanup.sh",
            "execute_command": "sudo {{.Path}}"
        }
    ]
}

Содержимое variables.json:

{
    "mv_service_account_key_file": "./key.json",
    "mv_folder_id": "WYDIWYG",
    "mv_source_image_family": "ubuntu-2204-lts",
    "mv_image_family": "ubuntu-docker"
}

Проверяем шаблон:

> packer validate -var-file=variables.json ubuntu-docker.json
The configuration is valid.

Я тут внезапно подумал, а почему не проверить, как работает конфиг в HCL? Попробуем преобразовать шаблон в HCL средствами packer.

> packer hcl2_upgrade -with-annotations ubuntu-docker.json
Successfully created ubuntu-docker.json.pkr.hcl. Exit 0

Что получили в итоге:

# This file was autogenerated by the 'packer hcl2_upgrade' command. We
# recommend double checking that everything is correct before going forward. We
# also recommend treating this file as disposable. The HCL2 blocks in this
# file can be moved to other files. For example, the variable blocks could be
# moved to their own 'variables.pkr.hcl' file, etc. Those files need to be
# suffixed with '.pkr.hcl' to be visible to Packer. To use multiple files at
# once they also need to be in the same folder. 'packer inspect folder/'
# will describe to you what is in that folder.

# Avoid mixing go templating calls ( for example ```{{ upper(`string`) }}``` )
# and HCL2 calls (for example '${ var.string_value_example }' ). They won't be
# executed together and the outcome will be unknown.

# All generated input variables will be of 'string' type as this is how Packer JSON
# views them; you can change their type later on. Read the variables type
# constraints documentation
# https://www.packer.io/docs/templates/hcl_templates/variables#type-constraints for more info.
variable "mv_folder_id" {
  type    = string
  default = ""
}

variable "mv_service_account_key_file" {
  type    = string
  default = ""
}

variable "mv_source_image_family" {
  type    = string
  default = ""
}

# "timestamp" template function replacement
locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }

# source blocks are generated from your builders; a source can be referenced in
# build blocks. A build block runs provisioner and post-processors on a
# source. Read the documentation for source blocks here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/source
source "yandex" "autogenerated_1" {
  folder_id                = "${var.mv_folder_id}"
  image_family             = "${var.mv_image_family}"
  image_name               = "${var.mv_image_family}-${local.timestamp}"
  platform_id              = "standard-v1"
  service_account_key_file = "${var.mv_service_account_key_file}"
  source_image_family      = "${var.mv_source_image_family}"
  ssh_username             = "ubuntu"
  use_ipv4_nat             = "true"
}

# a build block invokes sources and runs provisioning steps on them. The
# documentation for build blocks can be found here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/build
build {
  sources = ["source.yandex.autogenerated_1"]

  provisioner "shell" {
    environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
    execute_command  = "sudo {{ .Path }}"
    pause_before     = "1m0s"
    script           = "scripts/install_docker.sh"
  }

  provisioner "shell" {
    environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
    execute_command  = "sudo {{ .Path }}"
    script           = "scripts/cleanup.sh"
  }

}

Причешем немного шаблон, впишем в него скрипты, которые лежали у нас в отдельных файлах. В итоге получаем такой файл:

# Define variables
variable "mv_folder_id" {
  type    = string
  default = ""
}

variable "mv_service_account_key_file" {
  type    = string
  default = ""
}

variable "mv_image_family" {
  type    = string
  default = ""
}

variable "mv_source_image_family" {
  type    = string
  default = ""
}

variable "mv_username" {
  type    = string
  default = ""
}

# Define functions
locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }

source "yandex" "image" {
  folder_id                = "${var.mv_folder_id}"
  image_family             = "${var.mv_image_family}"
  image_name               = "${var.mv_image_family}-${local.timestamp}"
  platform_id              = "standard-v1"
  service_account_key_file = "${path.root}/${var.mv_service_account_key_file}"
  source_image_family      = "${var.mv_source_image_family}"
  ssh_username             = "${var.mv_username}"
  use_ipv4_nat             = true
}

build {
  sources = ["source.yandex.image"]

  provisioner "shell" {
    environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
    pause_before     = "30s"
    inline = [
        # Set noninteractive mode
        "echo debconf debconf/frontend select Noninteractive | sudo debconf-set-selections",
        # Official guide: https://docs.docker.com/engine/install/ubuntu/
        "sudo apt-get -y update",
        "sudo apt-get -y upgrade",
        "sudo apt-get -y install apt-transport-https ca-certificates curl gnupg lsb-release",
        # Setup repo
        "sudo mkdir -p /etc/apt/keyrings",
        "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg",
        "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null",
        # Update repo
        "sudo apt-get -y update",
        # Install latest docker
        "sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin mc htop tmux",
        # Cleanup
        "sudo apt-get -y autoremove",
        "sudo apt-get -y clean"
    ]
  }
}

Переменные выносим в отдельный файл yandex.pkrvars.hcl:

mv_service_account_key_file = "key.json"
mv_folder_id                = "WYDIWYG"
mv_source_image_family      = "ubuntu-2204-lts"
mv_image_family             = "ubuntu-docker"
mv_username                 = "ubuntu"

Проверим шаблон на ошибки:

> packer validate -var-file=yandex.pkrvars.hcl ubuntu-docker.pkr.hcl
The configuration is valid.

Всё прекрасно, собираем:

> packer build -var-file=yandex.pkrvars.hcl ubuntu-docker.pkr.hcl
yandex.image: output will be in this color.

==> yandex.image: Creating temporary RSA SSH key for instance...
==> yandex.image: Using as source image: fd8egv6phshj1f64q94n (name: "ubuntu-22-04-lts-v20221024", family: "ubuntu-2204-lts")
==> yandex.image: Creating network...
==> yandex.image: Creating subnet in zone "ru-central1-a"...
==> yandex.image: Creating disk...
==> yandex.image: Creating instance...
==> yandex.image: Waiting for instance with id fhmic1hvch2stnurr4ib to become active...
    yandex.image: Detected instance IP: 178.154.207.47
==> yandex.image: Using SSH communicator to connect: 178.154.207.47
==> yandex.image: Waiting for SSH to become available...
==> yandex.image: Connected to SSH!
==> yandex.image: Pausing 30s before the next provisioner...
==> yandex.image: Provisioning with shell script: /tmp/packer-shell1309385743
    yandex.image: Hit:1 http://ru.archive.ubuntu.com/ubuntu jammy InRelease
...
...
...
    yandex.image: Removing libftdi1-2:amd64 (1.5-5build3) ...
    yandex.image: Processing triggers for libc-bin (2.35-0ubuntu3.1) ...
==> yandex.image: Stopping instance...
==> yandex.image: Deleting instance...
    yandex.image: Instance has been deleted!
==> yandex.image: Creating image: ubuntu-docker-20221028185210
==> yandex.image: Waiting for image to complete...
==> yandex.image: Success image create...
==> yandex.image: Destroying subnet...
    yandex.image: Subnet has been deleted!
==> yandex.image: Destroying network...
    yandex.image: Network has been deleted!
==> yandex.image: Destroying boot disk...
    yandex.image: Disk has been deleted!
Build 'yandex.image' finished after 9 minutes 26 seconds.

==> Wait completed after 9 minutes 26 seconds

==> Builds finished. The artifacts of successful builds are:
--> yandex.image: A disk image was created: ubuntu-docker-20221028185210 (id: fd8h8umi4qqh0ovrgchl) with family name ubuntu-docker

С образом виртуальной машины закончили, поднимаем саму машину. Описываем конфигурацию:

# provider.tf

terraform {
  required_providers {
    yandex = {
      source = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
}

provider "yandex" {
  service_account_key_file = var.service_account_key_file
  cloud_id                 = var.cloud_id
  folder_id                = var.folder_id
  zone                     = var.zone
}
# variables.tf

variable "service_account_key_file" {
  description = "Path to service account key file"
}

variable "cloud_id" {
  description = "Cloud"
}

variable "folder_id" {
  description = "Folder"
}

variable "zone" {
  description = "Zone"
  default     = "ru-central1-a"
}

variable "image_id" {
  description = "Image id for VM"
}

variable "subnet_id" {
  description = "ID for subnet"
}

variable "public_key_path" {
  description = "Path to the public key used for ssh access"
}

variable "server_name" {
  description = "VM name"
}

variable "disk_size" {
  description = "VM disk size"
}
# terraform.tfvars

service_account_key_file    = "key.json"
cloud_id                    = "00000000000000000000"
folder_id                   = "00000000000000000000"
zone                        = "00-00000000-0"
image_id                    = "fd8mkch2tdig66qg0edu"
subnet_id                   = "00000000000000000000"
public_key_path             = "~/.ssh/ubuntu.pub"
server_name                 = "Nemo"
disk_size                   = 50
#

resource "yandex_compute_instance" "server" {
  name = var.server_name
  zone = var.zone

  resources {
    cores  = 2
    memory = 4
  }

  boot_disk {
    initialize_params {
      image_id = var.image_id
      size     = var.disk_size
    }
  }

  network_interface {
    subnet_id = var.subnet_id
    nat       = true
  }

  metadata = {
    ssh-keys = "ubuntu:${file(var.public_key_path)}"
  }
}

Проверяем:

> terraform validate

│ Error: Missing required provider

│ This configuration requires provider registry.terraform.io/yandex-cloud/yandex, but that provider isn't available. You may be able to install it automatically by
│ running:
│   terraform init

А, точно:

> terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of yandex-cloud/yandex...
- Installing yandex-cloud/yandex v0.81.0...
- Installed yandex-cloud/yandex v0.81.0 (self-signed, key ID E40F590B50BB8E40)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

> terraform validate
Success! The configuration is valid.

Применяем конфигурацию:

> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # yandex_compute_instance.server will be created
  + resource "yandex_compute_instance" "server" {
      + created_at                = (known after apply)
...
...
...
Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

yandex_compute_instance.server: Creating...
yandex_compute_instance.server: Still creating... [10s elapsed]
yandex_compute_instance.server: Still creating... [20s elapsed]
yandex_compute_instance.server: Still creating... [30s elapsed]
yandex_compute_instance.server: Still creating... [40s elapsed]
yandex_compute_instance.server: Still creating... [50s elapsed]
yandex_compute_instance.server: Creation complete after 1m0s [id=fhmtcvphatodu6nrmj7t]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Результат №04-1:

  • Подготовлен образ виртуальной машины с предустановленным docker
  • Из этого образа создана виртуальная машина в облаке

Задание №04-2:

Написать плейбук или небольшую роль по поднятию Gitlab в контейнере.

Решение №04-2:

В предыдущих занятиях мы уже настраивали инвентори, просто заберём конфиг и проверим его работу:

> ansible-inventory --list
{
    "_meta": {
        "hostvars": {
            "fhmtqhi1g294dgsialot.auto.internal": {
                "ansible_host": "84.252.128.119"
            }
        }
    },
    "all": {
        "children": [
            "ungrouped"
        ]
    },
    "ungrouped": {
        "hosts": [
            "fhmtqhi1g294dgsialot.auto.internal"
        ]
    }
}

Готовим плейбук для установки Gitlab. Используем модуль для работы с контейнерами docker_container_module. Читаем описание, выдаём результат:

- name: Run Gitlab in Docker
  hosts: all
  become: yes
  tasks:

    - name: Create dirs for volumes
      file:
        name: "{{ item }}"
        state: directory
        owner: root
        group: root
      with_items:
        - "/srv/gitlab/config"
        - "/srv/gitlab/logs"
        - "/srv/gitlab/data"

    - name: Install PIP
      apt:
        name: python3-pip
        state: present

    - name: Install Docker SDK for Python
      pip:
        name: docker
        state: present

    - name: Run Gitlab in Docker
      community.docker.docker_container:
        name: gitlab
        hostname: gitlab
        image: gitlab/gitlab-ce:15.4.0-ce.0
        state: started
        restart_policy: unless-stopped
        container_default_behavior: "no_defaults"
        env:
          GITLAB_OMNIBUS_CONFIG: "external_url \"http://{{ ansible_host }}\""
        ports:
          - "80:80"
          - "443:443"
          - "2222:22"
        volumes:
          - "/srv/gitlab/config:/etc/gitlab"
          - "/srv/gitlab/logs:/var/log/gitlab"
          - "/srv/gitlab/data:/var/opt/gitlab"

Применяем:

ansible-playbook gitlab_in_docker.yml

PLAY [Run Gitlab in Docker] *****************************************************

TASK [Gathering Facts] **********************************************************
ok: [fhmtqhi1g294dgsialot.auto.internal]

TASK [Create dirs for volumes] **************************************************
changed: [fhmtqhi1g294dgsialot.auto.internal] => (item=/srv/gitlab/config)
changed: [fhmtqhi1g294dgsialot.auto.internal] => (item=/srv/gitlab/logs)
changed: [fhmtqhi1g294dgsialot.auto.internal] => (item=/srv/gitlab/data)

TASK [Install PIP] **************************************************************
changed: [fhmtqhi1g294dgsialot.auto.internal]

TASK [Install Docker SDK for Python] ********************************************
changed: [fhmtqhi1g294dgsialot.auto.internal]

TASK [Run Gitlab in Docker] *****************************************************
changed: [fhmtqhi1g294dgsialot.auto.internal]

PLAY RECAP **********************************************************************
fhmtqhi1g294dgsialot.auto.internal : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Даём серверу пять минут на разворачивание конфигурации внутри контейнера, проверяем:

> lynx -dump http://84.252.128.119
   #[1]Search GitLab

   GitLab Logo

GitLab

   Username or email ____________________
   Password ____________________
   [ ] Remember me
   [2]Forgot your password?
   (BUTTON) Sign in

   By signing in you accept the [3]Terms of Use and acknowledge the
   Privacy Policy and Cookie Policy.

   Don't have an account yet? [4]Register now
     __________________________________________________________________

   [5]Explore [6]Help [7]About GitLab [8]Community forum

References

   1. http://84.252.128.119/search/opensearch.xml
   2. http://84.252.128.119/users/password/new
   3. http://84.252.128.119/-/users/terms
   4. http://84.252.128.119/users/sign_up
   5. http://84.252.128.119/explore
   6. http://84.252.128.119/help
   7. https://about.gitlab.com/
   8. https://forum.gitlab.com/

Видим ответ от Gitlab.

Результат №04-2:

  • Gitlab развёрнут автоматически, при помощи ansible.

Задание №04-2: Настраиваем Gitlab.

Решение №04-2:

Открываем браузер, идём на http://84.252.128.119. После первой установки для root генерируется случайный пароль, лежит в каталоге с конфигурацией. Идём на сервер, смотрим пароль:

ubuntu@fhmtqhi1g294dgsialot:~$ sudo cat /srv/gitlab/config/initial_root_password
# WARNING: This value is valid only in the following conditions
#          1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run).
#          2. Password hasn't been changed manually, either via UI or via command line.
#
#          If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.

Password: 9/oj46i2FqN2rlXutHq/nUewaNoCqz3BBu5qp7bMIXo=

# NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.

После ввода пароля попадаем в Gitlab. Пока сидел в консоли, заметил, что процессы отъели почти 80% памяти. В официальной документации есть советы по снижению аппетитов. После быстрой оптимизации Gitlab занимает уже 30% памяти.

Через веб-интерфейс создаём группу homework, в ней проект example. Клонируем репозиторий на локальную машину:

> git clone http://84.252.128.119/homework/example.git
Cloning into 'example'...
Username for 'http://84.252.128.119': root
Password for 'http://root@84.252.128.119':
warning: You appear to have cloned an empty repository.

В корне репозитория создадим описание пайплайна .gitlab-ci.yml:

stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - echo 'Building'

test_unit_job:
  stage: test
  script:
    - echo 'Testing 1'

test_integration_job:
  stage: test
  script:
    - echo 'Testing 2'

deploy_job:
  stage: deploy
  script:
    - echo 'Deploy'

Пушим изменения в удалённый репозиторий:

> git add .gitlab-ci.yml

> git commit -m "Add pipeline definition"
[main a116fa6] Add pipeline definition
 1 file changed, 24 insertions(+)
 create mode 100644 .gitlab-ci.yml

> git push
Username for 'http://84.252.128.119': root
Password for 'http://root@84.252.128.119':
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 408 bytes | 408.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To http://84.252.128.119/homework/example.git
   08fcbf7..a116fa6  main -> main

Идём в CI/CD -> Pipelines, видим, что наш пайплайн застрял в состоянии pending. Это произошло из-за отсуствия раннеров, которые могут выполнять работу. Раннер запустим на том же сервере, где установлен Gitlab. В консоли выполняем:

> sudo mkdir -p /srv/gitlab-runnner/config

> sudo docker run -d --name gitlab-runner --restart always -v /srv/gitlab-runnner/config:/etc/gitlab-runnner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:alpine-v15.5.0

Unable to find image 'gitlab/gitlab-runner:alpine-v15.5.0' locally
alpine-v15.5.0: Pulling from gitlab/gitlab-runner
9621f1afde84: Pull complete
4facbddabc98: Pull complete
12db4ccead3b: Pull complete
Digest: sha256:31ca964ad8f227c7dee37f5eda5df216b82ce0ba5cba291df26a6179f02ef234
Status: Downloaded newer image for gitlab/gitlab-runner:alpine-v15.5.0
27a5b705a3d50caa49309d8e70acab494c09f238b7755b2478d3ceede6f037ee

Теперь этот раннер нужно зарегистрировать, то есть привязать к нашему Gitlab. В настройках CI/CD находим токен, с его помощью регистрируем раннер:

> sudo docker exec -it gitlab-runner register --url http://84.252.128.119/ --non-interactive --locked=false --name=DockerRunner --executor docker --docker-image alpine:latest --registration-token GR13489414ApxCsr63cLavyuDh384 --tag-list="linux,xenial,ubuntu,docker" --run-untagged

Runtime platform arch=amd64 os=linux pid=37 revision=0d4137b8 version=15.5.0
Running in system-mode.
Registering runner... succeeded runner=GR13489414ApxCsr6
Runner registered successfully.
Feel free to start it, but if it's running already the config should be automatically reloaded!
Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"

В настройках CI/CD находим новый раннер. Пайплайн вышел из застрявшего состояния, отработал без ошибок.

Добавим приложение reddit в наш проект:

> git clone https://github.com/express42/reddit.git
Cloning into 'reddit'...
remote: Enumerating objects: 376, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 376 (delta 0), reused 0 (delta 0), pack-reused 371
Receiving objects: 100% (376/376), 67.42 KiB | 945.00 KiB/s, done.
Resolving deltas: 100% (201/201), done.

> rm -rf ./reddit/.git
> git add reddit/

> git commit -m "Add reddit app"
[main d084e8b] Add reddit app
 17 files changed, 656 insertions(+)
 create mode 100644 reddit/.gitignore
 create mode 100644 reddit/Capfile
 create mode 100644 reddit/Gemfile
 create mode 100644 reddit/Gemfile.lock
 create mode 100644 reddit/README.md
 create mode 100644 reddit/app.rb
 create mode 100644 reddit/config.ru
 create mode 100644 reddit/config/deploy.rb
 create mode 100644 reddit/config/deploy/production.rb
 create mode 100644 reddit/config/deploy/staging.rb
 create mode 100644 reddit/helpers.rb
 create mode 100644 reddit/views/create.haml
 create mode 100644 reddit/views/index.haml
 create mode 100644 reddit/views/layout.haml
 create mode 100644 reddit/views/login.haml
 create mode 100644 reddit/views/show.haml
 create mode 100644 reddit/views/signup.haml

> git push
Username for 'http://84.252.128.119': root
Password for 'http://root@84.252.128.119':
Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 2 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (22/22), 7.99 KiB | 4.00 MiB/s, done.
Total 22 (delta 2), reused 0 (delta 0), pack-reused 0
To http://84.252.128.119/homework/example.git
   a116fa6..d084e8b  main -> main

Добавим запуск тетов в наш пайплайн:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cf9e693..bd8cbaa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,17 @@
+image: ruby:2.4.2
+
 stages:
   - build
   - test
   - deploy

+variables:
+  DATABASE_URL: 'mongo'
+
+before_script:
+  - cd reddit
+  - bundle install
+
 build_job:
   stage: build
   script:
@@ -10,8 +19,10 @@ build_job:

 test_unit_job:
   stage: test
+  services:
+    - mongo:3.2
   script:
-    - echo 'Testing 1'
+    - ruby simpletest.rb

 test_integration_job:
   stage: test

Создадим сам тест reddit/simpletest.rb:

require_relative './app'
require 'test/unit'
require 'rack/test'

set :environment, :test

class MyAppTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    Sinatra::Application
  end

  def test_get_request
    get '/'
    assert last_response.ok?
  end
end

В reddit/Gemfile добавим библиотеку для тестирования:

diff --git a/reddit/Gemfile b/reddit/Gemfile
index 3ce71c0..4735172 100644
--- a/reddit/Gemfile
+++ b/reddit/Gemfile
@@ -7,6 +7,7 @@ gem 'bcrypt'
 gem 'puma'
 gem 'mongo'
 gem 'json'
+gem 'rack-test'

 group :development do
     gem 'capistrano',         require: false

Пушим изменения, видим, что тесты отработали:

Running with gitlab-runner 15.5.0 (0d4137b8)
  on DockerRunner so4_tQcx
Preparing the "docker" executor 00:08
Using Docker executor with image ruby:2.4.2 ...
Starting service mongo:3.2 ...
Pulling docker image mongo:3.2 ...
Using docker image sha256:fb885d89ea5c35ac02acf79a398b793555cbb3216900f03f4b5f7dc31e595e31 for mongo:3.2 with digest mongo@sha256:0463a91d8eff189747348c154507afc7aba045baa40e8d58d8a4c798e71001f3 ...
Waiting for services to be up and running (timeout 30 seconds)...
Pulling docker image ruby:2.4.2 ...
Using docker image sha256:2a867526d4724cf92c845c70b67e53d61e0a76e324a6051262c3731ddcc2f568 for ruby:2.4.2 with digest ruby@sha256:7271d0cd55da37b6f28924c9452871d77e828c4d38ef3438cfc179388209e51f ...
Preparing environment 00:01
Running on runner-so4tqcx-project-3-concurrent-0 via dd3122804efe...
Getting source from Git repository 00:01
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /builds/homework/example/.git/
Checking out 65ce6b80 as main...
Skipping Git submodules setup
Executing "step_script" stage of the job script 00:18
Using docker image sha256:2a867526d4724cf92c845c70b67e53d61e0a76e324a6051262c3731ddcc2f568 for ruby:2.4.2 with digest ruby@sha256:7271d0cd55da37b6f28924c9452871d77e828c4d38ef3438cfc179388209e51f ...
$ cd reddit
$ bundle install
Warning: the running version of Bundler (1.16.0) is older than the version that created the lockfile (1.16.1). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
Fetching gem metadata from https://rubygems.org/.......
Resolving dependencies...
Fetching rake 12.0.0
Installing rake 12.0.0
Fetching net-ssh 4.1.0
Installing net-ssh 4.1.0
Fetching net-scp 1.2.1
Installing net-scp 1.2.1
Fetching sshkit 1.14.0
Installing sshkit 1.14.0
Fetching airbrussh 1.3.0
Installing airbrussh 1.3.0
Fetching bcrypt 3.1.11
Installing bcrypt 3.1.11 with native extensions
Fetching bson 4.2.2
Installing bson 4.2.2 with native extensions
Fetching bson_ext 1.5.1
Installing bson_ext 1.5.1 with native extensions
Using bundler 1.16.0
Fetching i18n 0.8.6
Installing i18n 0.8.6
Fetching capistrano 3.9.0
Installing capistrano 3.9.0
Fetching capistrano-bundler 1.2.0
Installing capistrano-bundler 1.2.0
Fetching capistrano-rvm 0.1.2
Installing capistrano-rvm 0.1.2
Fetching puma 3.10.0
Installing puma 3.10.0 with native extensions
Fetching capistrano3-puma 3.1.1
Installing capistrano3-puma 3.1.1
Fetching temple 0.8.0
Installing temple 0.8.0
Fetching tilt 2.0.8
Installing tilt 2.0.8
Fetching haml 5.0.2
Installing haml 5.0.2
Fetching json 2.1.0
Installing json 2.1.0 with native extensions
Fetching mongo 2.4.3
Installing mongo 2.4.3
Fetching mustermann 1.0.2
Installing mustermann 1.0.2
Fetching rack 2.0.5
Installing rack 2.0.5
Fetching rack-protection 2.0.2
Installing rack-protection 2.0.2
Fetching rack-test 2.0.2
Installing rack-test 2.0.2
Fetching sinatra 2.0.2
Installing sinatra 2.0.2
Bundle complete! 12 Gemfile dependencies, 25 gems now installed.
Bundled gems are installed into `/usr/local/bundle`
Post-install message from capistrano3-puma:
    All plugins need to be explicitly installed with install_plugin.
    Please see README.md
  $ ruby simpletest.rb
/builds/homework/example/reddit/helpers.rb:4: warning: redefining `object_id' may cause serious problems
D, [2022-10-30T13:51:51.841902 #329] DEBUG -- : MONGODB | Topology type 'unknown' initializing.
D, [2022-10-30T13:51:51.842065 #329] DEBUG -- : MONGODB | Server mongo:27017 initializing.
D, [2022-10-30T13:51:51.845826 #329] DEBUG -- : MONGODB | Topology type 'unknown' changed to type 'single'.
D, [2022-10-30T13:51:51.845922 #329] DEBUG -- : MONGODB | Server description for mongo:27017 changed from 'unknown' to 'standalone'.
D, [2022-10-30T13:51:51.845981 #329] DEBUG -- : MONGODB | There was a change in the members of the 'single' topology.
Loaded suite simpletest
Started
D, [2022-10-30T13:51:51.957565 #329] DEBUG -- : MONGODB | mongo:27017 | user_posts.find | STARTED | {"find"=>"posts", "filter"=>{}, "sort"=>{"timestamp"=>-1}}
D, [2022-10-30T13:51:51.959650 #329] DEBUG -- : MONGODB | mongo:27017 | user_posts.find | SUCCEEDED | 0.0019516730000000001s
.
Finished in 0.058503711 seconds.
------
1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
------
17.09 tests/s, 17.09 assertions/s
Job succeeded

Добавим в пайплайн dev окружение:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a9ff7cf..83e0432 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,7 +3,7 @@ image: ruby:2.4.2
 stages:
   - build
   - test
-  - deploy
+  - review

 variables:
   DATABASE_URL: 'mongo'
@@ -29,7 +29,11 @@ test_integration_job:
   script:
     - echo 'Testing 2'

-deploy_job:
-  stage: deploy
+deploy_dev_job:
+  stage: review
   script:
     - echo 'Deploy'
+  environment:
+    name: dev
+    url: http://dev.example.com

Пушим изменения, видим, что у нас в Deployments -> Environments появилось окружение dev.

Добавим ещё два окружения в пайплайн:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 83e0432..d565580 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,6 +4,8 @@ stages:
   - build
   - test
   - review
+  - stage
+  - production

 variables:
   DATABASE_URL: 'mongo'
@@ -37,3 +39,20 @@ deploy_dev_job:
     name: dev
     url: http://dev.example.com

+staging:
+  stage: stage
+  when: manual
+  script:
+    - echo 'Deploy'
+  environment:
+    name: beta
+    url: http://beta.example.com
+
+production:
+  stage: production
+  when: manual
+  script:
+    - echo 'Deploy'
+  environment:
+    name: production
+    url: http://example.com

После завершения работы пайплайна мы видим новые окружения. Дбавим условие, при котором на stage и production пойдут только те ветки, которые отмечены тегом:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fd92f6e..882d818 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,6 +42,8 @@ deploy_dev_job:
 staging:
   stage: stage
   when: manual
+  only:
+    - tags
   script:
     - echo 'Deploy'
   environment:
@@ -51,6 +53,8 @@ staging:
 production:
   stage: production
   when: manual
+  only:
+    - tags
   script:
     - echo 'Deploy'
   environment:

Добавим тег к коммиту:

> git commit -m "Remove unused files"
[main e15c2dc] Remove unused files
 1 file changed, 1 deletion(-)
 delete mode 100644 reddit/.gitignore

> git tag 2.4.10

> git push --tags
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 280 bytes | 280.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
To http://84.252.128.119/homework/example.git
 * [new tag]         2.4.10 -> 2.4.10

Видим, что при пуше без тега паплайн состоит из трёх стадий, а при установке тега у нас появляется пять стадий.

В документации пишут: "Defining image, services, cache, before_script, and after_script globally is deprecated. Support could be removed from a future release.". Поправим свой пайплайн:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3800749..df7f8ad 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,8 @@
-image: ruby:2.4.2
+default:
+  image: ruby:2.4.2
+  before_script:
+    - cd reddit
+    - bundle install

 stages:
   - build
@@ -10,10 +14,6 @@ stages:
 variables:
   DATABASE_URL: 'mongo'

-before_script:
-  - cd reddit
-  - bundle install
-
 build_job:
   stage: build
   script:

Есть возможность созлавать динамические окружения. Сделаем это для всех веток, исключая главную ветку:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index df7f8ad..96c17ef 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,6 +39,17 @@ deploy_dev_job:
     name: dev
     url: http://dev.example.com

+branch_review:
+  stage: review
+  script: echo "Deploy to $CI_ENVIRONMENT_SLUG"
+  environment:
+    name: branch/$CI_COMMIT_REF_NAME
+    url: http://$CI_ENVIRONMENT_SLUG.example.com
+  only:
+    - branches
+  except:
+    - main
+
 staging:
   stage: stage
   when: manual

Создаём несколько веток, пушим изменения, видим, что окружения создаются динамически.

Результат №04-3:

  • Отработаны приёмы настройки пайплайна в Gitlab
  • Научились создавать окружения, в том числе и динамические

Задание №04-4: Запуск reddit в контейнере (по желанию). В этап пайплайна build добавьте запуск контейнера с приложением reddit. Сделайте так, чтобы контейнер с reddit деплоился на окружение, динамически создаваемое для каждой ветки в Gitlab.

Решение №04-4: Какая-то невнятная постановка задачи. Какое ТЗ, такой результат) В своё время мы загружали на dockerhub образ приложения, добавим его в этап build:

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 90138c8..c7ed2e6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,6 +18,10 @@ build_job:
   stage: build
   script:
     - echo 'Building'
+  environment:
+    name: branch/$CI_COMMIT_REF_NAME
+    url: http://$CI_ENVIRONMENT_SLUG.example.com
+  image: r2d2k/otus-reddit:1.0
+  before_script:
+    - echo 'Prepare to run reddit'

 test_unit_job:
   stage: test

Результат №04-4: Во время запуска шага build был выкачан образ и запущен контейнер с reddit. Параллельно создалось окружение под эту ветку.


Задание №04-5: Автоматизация развёртывания GitLab Runner (по желанию)

Решение №04-5: Создадим под эту задачу отдельный плейбук. По умолчанию раннер будет устанавливаться на тот же хост, на котором работает Gitlab, то есть используем хост из динамического инвентори. Заполняем переменные окружения и запускаем плейбук. Если не указать никаких тегов, то контейнер будет запущен, но не зарегистрирован. Регистрация произойдёт только при запуске плейбука с тегом register:

- name: Got runner
  hosts: all
  become: yes
  vars:
    url: "http://84.252.128.119/"
    name: "DockerRunner"
    docker_image: "alpine:latest"
    token: "GR13489414ApxCsr63cLavyuDh384"
    tag_list: "linux,xenial,ubuntu,docker"
  tasks:

    - name: Create dirs for volumes
      file:
        name: "/srv/gitlab-runnner/config"
        state: directory
        owner: root
        group: root

    - name: Install PIP
      apt:
        name: python3-pip
        state: present

    - name: Install Docker SDK for Python
      pip:
        name: docker
        state: present

    - name: Run gitlab-runner
      community.docker.docker_container:
        name: gitlab-runner
        hostname: gitlab-runner
        image: gitlab/gitlab-runner:alpine-v15.5.0
        state: started
        restart_policy: unless-stopped
        container_default_behavior: "no_defaults"
        volumes:
          - "/srv/gitlab-runnner/config:/etc/gitlab-runnner"
          - "/var/run/docker.sock:/var/run/docker.sock"

    - name: Register runner
      community.docker.docker_container_exec:
        container: gitlab-runner
        command: gitlab-runner register --url {{ url }} --non-interactive --locked=false --name={{ name }} --executor docker --docker-image {{ docker_image }} --registration-token {{ token }} --tag-list={{ tag_list }} --run-untagged
      register: result
      tags: [ never, register ]

    - name: Print result
      debug:
        var: result.stderr_lines
      tags: [ never, register ]

Результат №04-5:

  • Автоматизирован процесс разворачивания и регистрации раннера
  • Переменные намеренно оставлены в плейбуке, для упрощения восприятия

Задание №04-6: Настройка оповещений в Slack (пожеланию). Настройте интеграцию вашего пайплайна с тестовым Slack-чатом, который вы использовали ранее. Для этого перейдите в Settings -> Integrations -> Slack notifications. Нужно включить эту интеграцию, выбрать события и заполнить поля с URL вашего Slack webhook. Добавьте ссылку на канал в слаке, в котором можно проверить работу оповещений, в файл README.md.

Решение №04-6: В слаке идём в Browse Apps -> Custom Integrations -> Incoming WebHooks -> Edit configuration, добавляем входящий вебхук. В Gitlab идём в Settings -> Integrations -> Slack notifications, вносим имя своего канала, адрес полученного на предыдущем шаге вебхука.

Результат №04-6:

  • Работу оповещений можно увидеть по этой ссылке.

05 - Введение в мониторинг. Системы мониторинга.

Задание №05-1:

  • Prometheus: запуск, конфигурация, знакомство с Web UI
  • Мониторинг состояния микросервисов
  • Сбор метрик хоста с использованием экспортера

Решение №05-1:

Создаём новую ветку git checkout -b monitoring-1. Готовим окружение, нам нужен сервер с установленным docker. Возьмём описание сервера из предыдущего задания, поднимем его при помощи terraform. После запуска сервера заходим на него по ssh, запускаем prometheus:

> sudo docker run --rm -p 9090:9090 -d --name prometheus prom/prometheus
Unable to find image 'prom/prometheus:latest' locally
latest: Pulling from prom/prometheus
50783e0dfb64: Pull complete
daafb1bca260: Pull complete
72d3569fdc6f: Pull complete
13afa930da33: Pull complete
6ef28183cda8: Pull complete
4ad7245dbb40: Pull complete
26e6063b72b5: Pull complete
d859dd8f8ba9: Pull complete
583221d3597c: Pull complete
b4e477a4eb49: Pull complete
0b0ad5fc938d: Pull complete
53ddffa5a7d1: Pull complete
Digest: sha256:4748e26f9369ee7270a7cd3fb9385c1adb441c05792ce2bce2f6dd622fd91d38
Status: Downloaded newer image for prom/prometheus:latest
b7a22bf74cb765380384fcc1689dd96f47ab36583d408be64480bfef68e0b73f

Сервер слушает на порту 9090, подключаемся, видим веб-интерфейс prometheus. По умолчанию уже собираются метрики самой системы мониторинга. Можем посмотреть информацию о версии продукта:

prometheus_build_info{branch="HEAD", goversion="go1.19.2", instance="localhost:9090", job="prometheus", revision="dcd6af9e0d56165c6f5c64ebbc1fae798d24933a", version="2.39.1"}

Создадим простой prometheus/Dockerfile:

FROM prom/prometheus:v2.1.0
ADD prometheus.yml /etc/prometheus/

Сам файл конфигурации prometheus.yml будет выглядеть так:

global:
  scrape_interval: '5s'

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets:
        - 'localhost:9090'

  - job_name: 'ui'
    static_configs:
      - targets:
        - 'ui:9292'

  - job_name: 'comment'
    static_configs:
      - targets:
        - 'comment:9292'

Собираем образ:

> docker build -t r2d2k/prometheus .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM prom/prometheus:v2.1.0
v2.1.0: Pulling from prom/prometheus
Image docker.io/prom/prometheus:v2.1.0 uses outdated schema1 manifest format. Please upgrade to a schema2 image for better future compatibility. More information at https://docs.docker.com/registry/spec/deprecated-schema-v1/
aab39f0bc16d: Pull complete
a3ed95caeb02: Pull complete
2cd9e239cea6: Pull complete
48afad9e6cdd: Pull complete
8fb7aa0e1c16: Pull complete
3b9d4fd63760: Pull complete
57a87cf4a659: Pull complete
9a31588e38ae: Pull complete
7a0ac0080f04: Pull complete
659e24e6d37f: Pull complete
Digest: sha256:7b987901dbc44d17a88e7bda42dbbbb743c161e3152662959acd9f35aeefb9a3
Status: Downloaded newer image for prom/prometheus:v2.1.0
 ---> c8ecf7c719c1
Step 2/2 : ADD prometheus.yml /etc/prometheus/
 ---> fca39e0c1f75
Successfully built fca39e0c1f75
Successfully tagged r2d2k/prometheus:latest

Собираем образы всех приложений:

/src/ui $ bash docker_build.sh
/src/post-py $ bash docker_build.sh
/src/comment $ bash docker_build.sh

После сборки получаем такой результат:

> docker images
REPOSITORY      TAG            IMAGE ID       CREATED              SIZE
r2d2k/comment   latest         eb80e04347b7   11 seconds ago       769MB
r2d2k/post      latest         44874e613120   About a minute ago   121MB
r2d2k/ui        latest         ca343ac78a70   3 minutes ago        464MB
ubuntu          16.04          b6f507652425   14 months ago        135MB
ruby            2.2            6c8e6f9667b2   4 years ago          715MB
python          3.6.0-alpine   cb178ebbf0f2   5 years ago          88.6MB

Обновляем docker-compose.yml, добавив в него сервис prometheus:

services:
  post_db:
    image: mongo:3.2
    volumes:
      - post_db:/data/db
    networks:
      - back_net

  ui:
    image: ${USERNAME}/ui:latest
    ports:
      - ${UI_PORT}:9292/tcp
    networks:
      - front_net

  post:
    image: ${USERNAME}/post:latest
    networks:
      - back_net
      - front_net

  comment:
    image: ${USERNAME}/comment:latest
    networks:
      - back_net
      - front_net

  prometheus:
    image: ${USERNAME}/prometheus:latest
    ports:
      - '9090:9090'
    volumes:
      - prometheus_data:/prometheus
    command: # Передаем доп параметры в командной строке
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention=1d' # Задаем время хранения метрик в 1 день
    networks:
      - front_net
      - back_net

volumes:
  post_db:
  prometheus_data:

networks:
  front_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.1.0/24

  back_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.2.0/24

Поднимаем связку:

> docker compose up -d
[+] Running 9/9
 ⠿ Network r2d2k_back_net          Created                                  0.2s
 ⠿ Network r2d2k_front_net         Created                                  0.1s
 ⠿ Volume "r2d2k_prometheus_data"  Crea...                                  0.0s
 ⠿ Volume "r2d2k_post_db"          Created                                  0.0s
 ⠿ Container r2d2k-comment-1       Started                                  2.0s
 ⠿ Container r2d2k-prometheus-1    Starte...                                1.8s
 ⠿ Container r2d2k-post_db-1       Started                                  1.6s
 ⠿ Container r2d2k-ui-1            Started                                  1.0s
 ⠿ Container r2d2k-post-1          Started                                  1.4s

Проверим приложение:

> lynx -dump http://127.0.0.1:9292
   (BUTTON) [1]Microservices Reddit in d8a6f992662c container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://127.0.0.1:9292/
   2. http://127.0.0.1:9292/
   3. http://127.0.0.1:9292/new

Проверяем доступность метрик в интерфейсе prometheus. Для сбора информации о других сущностях можно использовать экспортёры prometheus, которых написано великое множество. Добавим в наш docker-compose.yml node-exporter, который позволит собирать данные о хосте:

+  node-exporter:
+    image: prom/node-exporter:v0.15.2
+    user: root
+    volumes:
+      - /proc:/host/proc:ro
+      - /sys:/host/sys:ro
+      - /:/rootfs:ro
+    command:
+      - '--path.procfs=/host/proc'
+      - '--path.sysfs=/host/sys'
+      - '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"'
+

В конфиг prometheus добавляем новую секцию:

+
+  - job_name: 'node'
+    static_configs:
+      - targets:
+        - 'node-exporter:9100'

Собираем образ с prometheus, перезапускаем наши сервисы, проверяем, что в prometheus появились новые данные.

Все собранные образы пушим в registry:

> docker login
Login Succeeded

> docker push $USER_NAME/ui
> docker push $USER_NAME/comment
> docker push $USER_NAME/post
> docker push $USER_NAME/prometheus

Образы доступны по ссылке.

Результат №05-1:

  • Собраны образы с экспортом метрик
  • Собран образ prometheus с преднастроенным конфигом
  • Подготовлен конфигурационный файл для docker-compose, позволяющий запустить все сервисы и мониторинг к ним

Задание №05-2:

  • Добавьте в Prometheus мониторинг MongoDB с использованием необходимого экспортера
  • Версию образа экспортера нужно фиксировать на последнюю стабильную версию
  • Если будете добавлять для него Dockerfile, он должен быть в директории monitoring, а не в корне репозитория

Решение №05-2:

Попробуем использовать mongodb_exporter. Есть вариант в виде образа docker, проверим его. После чтения документации по ссылке выше внесём изменения в наш docker-compose.yml:

+  mongo-exporter:
+    image: percona/mongodb_exporter:0.35
+    command:
+      - '--mongodb.uri=mongodb://post_db:27017'
+      - '--collect-all'
+      - '--log.level=debug'
+    ports:
+      - '9216:9216'
+    networks:
+      - back_net
+

В строке с образом фиксируем версию экспортёра. В параметрах команды указываем адрес сервера mongodb, список собираемых параметров. В файл конфигурации prometheus также добавим опрос новых параметров:

+
+  - job_name: 'mongodb'
+    static_configs:
+      - targets:
+        - 'mongo-exporter:9216'

Собираем образ и поднимаем весь комплект при помощи docker compose up -d. Проверим, отдаёт ли экспортёр метрики:

> lynx -dump http://127.0.0.1:9216/metrics | grep mongodb | wc
   2300    6961  167231

Видим 2300 параметров с упоминанием mongodb. В интерфейсе prometheus появился mongo-exporter в статусе up.

Результат №05-2: Для сбора метрик mongodb был использован отдельный контейнер, вмешательства в образ mongodb не потребовалось.


Задание №05-3:

  • Добавьте в Prometheus мониторинг сервисов comment, post, ui с помощью blackbox экспортера
  • Версию образа экспортера нужно фиксировать на последнюю стабильную версию
  • Если будете добавлять для него Dockerfile, он должен быть в директории monitoring, а не в корне репозитория

Решение №05-3:

Идём на страницу разработчика и качаем последний релиз продукта. Создаём папку blackbox-exporter, распаковываем в неё содержимое архива. Из файла конфигурации blackbox.yml вырезаем всё лишнее:

modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: []
      method: GET

Подготовим Dockerfile для сборки контейнера:

FROM scratch
COPY blackbox_exporter  /bin/blackbox_exporter
COPY blackbox.yml       /etc/blackbox_exporter/config.yml

EXPOSE      9115
ENTRYPOINT  [ "/bin/blackbox_exporter" ]
CMD         [ "--config.file=/etc/blackbox_exporter/config.yml" ]

Собираем образ:

> docker build -t r2d2k/blackbox-exporter .
Sending build context to Docker daemon  20.75MB
Step 1/6 : FROM scratch
 --->
Step 2/6 : COPY blackbox_exporter  /bin/blackbox_exporter
 ---> e30c5d8b7c84
Step 3/6 : COPY blackbox.yml       /etc/blackbox_exporter/config.yml
 ---> 5480d381f219
Step 4/6 : EXPOSE      9115
 ---> Running in 050e4b77aeb6
Removing intermediate container 050e4b77aeb6
 ---> 0f2622c25a07
Step 5/6 : ENTRYPOINT  [ "/bin/blackbox_exporter" ]
 ---> Running in c6026a8e647e
Removing intermediate container c6026a8e647e
 ---> 79327d6d92df
Step 6/6 : CMD         [ "--config.file=/etc/blackbox_exporter/config.yml" ]
 ---> Running in d45bf82883b3
Removing intermediate container d45bf82883b3
 ---> 8d2fdebb3ecf
Successfully built 8d2fdebb3ecf
Successfully tagged r2d2k/blackbox-exporter:latest

> docker tag r2d2k/blackbox-exporter:latest r2d2k/blackbox-exporter:1.0

У нас получился образ размером в 20 МБ:

> docker images
REPOSITORY                 TAG            IMAGE ID       CREATED                 SIZE
r2d2k/blackbox-exporter    1.0            8d2fdebb3ecf   over 9000 seconds ago   20.7MB
r2d2k/blackbox-exporter    latest         8d2fdebb3ecf   over 9000 seconds ago   20.7MB

Добавляем ещё один контейнер в наш docker-compose.yml:

+  blackbox-exporter:
+    image: r2d2k/blackbox-exporter:1.0
+    ports:
+      - '9115:9115'
+    networks:
+      - front_net
+

В файл конфигурации prometheus добавим опрос контейнеров через blackbox-exporter:

+  - job_name: 'blackbox-exporter'
+    metrics_path: /probe
+    params:
+      module: [http_2xx]
+    static_configs:
+      - targets:
+        - http://comment:9292/healthcheck
+        - http://post:5000/healthcheck
+        - http://ui:9292/healthcheck
+    relabel_configs:
+      - source_labels: [__address__]
+        target_label: __param_target
+      - source_labels: [__param_target]
+        target_label: instance
+      - target_label: __address__
+        replacement: blackbox-exporter:9115

По исходникам видно, что у каждого из сервисов есть endpoint для проверки здоровья. Пересобираем образ, запускаем весь стек и видим, что новые цели мониторинга добавлены и опрашиваются. По запросу probe_success в веб-интерфейсе prometheus видим следующий результат:

probe_success{instance="http://comment:9292/healthcheck",job="blackbox-exporter"}   1
probe_success{instance="http://post:5000/healthcheck",job="blackbox-exporter"}      1
probe_success{instance="http://ui:9292/healthcheck",job="blackbox-exporter"}        1

Результат №05-3: Собрали отдельный образ с blackbox-exporter, запустили в отдельном контейнере и мониторим состояние трёх сервисов.


Задание №05-4:

Написать Makefile, который умеет:

  • Билдить любой или все образы, которые сейчас используются
  • Умеет пушить их в докер хаб

Решение №05-4: Идём, читаем документацию. Она довольно старая, но для нашего случая это не имеет значения. Структура каталога src у нас следующая:

.
├── comment
│   ├── build_info.txt
│   ├── comment_app.rb
│   ├── config.ru
│   ├── docker_build.sh
│   ├── Dockerfile
│   ├── Dockerfile.1
│   ├── Dockerfile.2
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── helpers.rb
│   ├── Makefile
│   └── VERSION
├── post-py
│   ├── build_info.txt
│   ├── docker_build.sh
│   ├── Dockerfile
│   ├── helpers.py
│   ├── Makefile
│   ├── post_app.py
│   ├── requirements.txt
│   └── VERSION
├── ui
│   ├── views
│   ├── build_info.txt
│   ├── config.ru
│   ├── docker_build.sh
│   ├── Dockerfile
│   ├── Dockerfile.1
│   ├── Dockerfile.2
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── helpers.rb
│   ├── Makefile
│   ├── middleware.rb
│   ├── ui_app.rb
│   └── VERSION
├── docker-compose.override.yml
├── docker-compose.yml
├── Makefile
└── README.md

В каждом дочернем каталоге созадим Makefile следующего содержания (с учётом имени сервиса):

.PHONY: build push

build:
        docker build -t r2d2k/comment .

push:
        docker push r2d2k/comment

В корне родительского каталога создаём общий Makefile:

.PHONY: build push

build:
        $(MAKE) -C comment
        $(MAKE) -C post-py
        $(MAKE) -C ui

push:
        $(MAKE) push -C comment
        $(MAKE) push -C post-py
        $(MAKE) push -C ui

Файлы без особых излишеств, просто делают свою работу) Для сборки отдельного образа выполняем make в соответствующем каталоге. Для пуша в докер хаб говорим make push. Эти же команды в родительском каталоге будут действовать на все три сервиса.

Результат №05-4:

  • Автоматизировали процесс сборки докер образов при помощи make

06 - Логирование и распределенная трассировка

Задание №06-1:

  • Подготовка окружения
  • Логирование Docker-контейнеров
  • Сбор неструктурированных логов
  • Визуализация логов
  • Сбор структурированных логов
  • Распределенный трейсинг

Решение №06-1: Работаем в новой ветке logging-1.

Забираем обновлённые исходники наших сервисов:

> git clone https://github.com/express42/reddit.git
Cloning into 'reddit'...
remote: Enumerating objects: 376, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 376 (delta 0), reused 0 (delta 0), pack-reused 371
Receiving objects: 100% (376/376), 67.42 KiB | 821.00 KiB/s, done.
Resolving deltas: 100% (201/201), done.

> cd reddit/

> git branch -a
* monolith
  remotes/origin/HEAD -> origin/monolith
  remotes/origin/bugged
  remotes/origin/dependabot/bundler/json-2.3.0
  remotes/origin/dependabot/bundler/puma-3.12.6
  remotes/origin/dependabot/bundler/rack-2.2.3
  remotes/origin/dependabot/bundler/rake-13.0.1
  remotes/origin/dependabot/bundler/sinatra-2.2.0
  remotes/origin/logging
  remotes/origin/microservices
  remotes/origin/microservices-docker
  remotes/origin/microservices_deprecated
  remotes/origin/microservices_for_mc
  remotes/origin/monolith
  remotes/origin/revert-6-microservices

> git checkout logging
Branch 'logging' set up to track remote branch 'logging' from 'origin'.
Switched to a new branch 'logging'

> tree -L 2 --dirsfirst
.
├── comment
│   ├── comment_app.rb
│   ├── config.ru
│   ├── docker_build.sh
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── helpers.rb
│   └── VERSION
├── post-py
│   ├── docker_build.sh
│   ├── helpers.py
│   ├── post_app.py
│   ├── requirements.txt
│   └── VERSION
├── ui
│   ├── views
│   ├── config.ru
│   ├── docker_build.sh
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── helpers.rb
│   ├── middleware.rb
│   ├── ui_app.rb
│   └── VERSION
└── README.md

Копируем эти файлы в наш каталог src. Собираем образы и загружаем их на докер хаб.

> export USER_NAME=r2d2k
> cd ./src/ui && bash docker_build.sh && docker push $USER_NAME/ui
...

> cd ../post-py && bash docker_build.sh && docker push $USER_NAME/post
...

> cd ../comment && bash docker_build.sh && docker push $USER_NAME/comment
...

> docker images
REPOSITORY      TAG            IMAGE ID       CREATED          SIZE
r2d2k/post      logging        3637ea2f7e34   37 seconds ago   210MB
r2d2k/comment   logging        9db45c86146f   2 minutes ago    187MB
r2d2k/ui        logging        ec99272cec4c   4 minutes ago    188MB
alpine          3.4            b7c5ffe56db7   3 years ago      4.81MB
python          3.6.0-alpine   cb178ebbf0f2   5 years ago      88.6MB

Образы подготовлены, теперь займёмся системой логирования. Будем использовать классическую связку ELK (Elasticsearch, Kibana, Logstash), только вместо Logstash возьмём Fluentd, он полегче. Получится EFK.

Для запуска системы создадим отдельный docker/docker-compose-logging.yml:

version: '3'
services:
  fluentd:
    image: ${USERNAME}/fluentd
    ports:
      - "24224:24224"
      - "24224:24224/udp"

  elasticsearch:
    image: elasticsearch:7.4.0
    environment:
      - ELASTIC_CLUSTER=false
      - CLUSTER_NODE_MASTER=true
      - CLUSTER_MASTER_NODE_NAME=es01
      - discovery.type=single-node
    expose:
      - 9200
    ports:
      - "9200:9200"

  kibana:
    image: kibana:7.4.0
    ports:
      - "5601:5601"

Для запуска Fluentd подготовим образ с нужной нам конфигурацией.

Dockerfile создаём в каталоге logging/fluentd:

FROM fluent/fluentd:v0.12
RUN gem install --no-rdoc --no-ri --version 2.1.0 faraday-net_http
RUN gem install --no-rdoc --no-ri --version 1.10.2 faraday
RUN gem install --no-rdoc --no-ri --version 1.9.5 fluent-plugin-elasticsearch
RUN gem install --no-rdoc --no-ri --version 1.0.0 fluent-plugin-grok-parser
ADD fluent.conf /fluentd/etc

Рядом создаём файл конфигурации:

<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<match *.**>
  @type copy
  <store>
    @type elasticsearch
    host elasticsearch
    port 9200
    logstash_format true
    logstash_prefix fluentd
    logstash_dateformat %Y%m%d
    include_tag_key true
    type_name access_log
    tag_key @log_name
    flush_interval 1s
  </store>
  <store>
    @type stdout
  </store>
</match>

Собираем образ:

> docker build -t r2d2k/fluentd .
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM fluent/fluentd:v0.12
 ---> 5ad80e121366
Step 2/6 : RUN gem install --no-rdoc --no-ri --version 2.1.0 faraday-net_http
 ---> Running in e5f75bee3acc
Successfully installed faraday-net_http-2.1.0
1 gem installed
Removing intermediate container e5f75bee3acc
 ---> ad7785093b14
Step 3/6 : RUN gem install --no-rdoc --no-ri --version 1.10.2 faraday
 ---> Running in f9491d9f32b5
Successfully installed faraday-em_http-1.0.0
Successfully installed faraday-em_synchrony-1.0.0
Successfully installed faraday-excon-1.1.0
Successfully installed faraday-httpclient-1.0.1
Successfully installed multipart-post-2.2.3
Successfully installed faraday-multipart-1.0.4
Successfully installed faraday-net_http-1.0.1
Successfully installed faraday-net_http_persistent-1.2.0
Successfully installed faraday-patron-1.0.0
Successfully installed faraday-rack-1.0.0
Successfully installed faraday-retry-1.0.3
Successfully installed ruby2_keywords-0.0.5
Successfully installed faraday-1.10.2
13 gems installed
Removing intermediate container f9491d9f32b5
 ---> a8adad624361
Step 4/6 : RUN gem install --no-rdoc --no-ri --version 1.9.5 fluent-plugin-elasticsearch
 ---> Running in f2c42909dcbb
Successfully installed multi_json-1.15.0
Successfully installed elastic-transport-8.1.0
Successfully installed elasticsearch-api-8.5.0
Successfully installed elasticsearch-8.5.0
Successfully installed excon-0.94.0
Successfully installed fluent-plugin-elasticsearch-1.9.5
6 gems installed
Removing intermediate container f2c42909dcbb
 ---> 1e8267e11380
Step 5/6 : RUN gem install --no-rdoc --no-ri --version 1.0.0 fluent-plugin-grok-parser
 ---> Running in 60c68c694e6a
Successfully installed fluent-plugin-grok-parser-1.0.0
1 gem installed
Removing intermediate container 60c68c694e6a
 ---> d9f3026219f8
Step 6/6 : ADD fluent.conf /fluentd/etc
 ---> cd1dddbc6ca7
Successfully built cd1dddbc6ca7
Successfully tagged r2d2k/fluentd:latest

Меняем наш docker-compose для запуска сервисов с логированием:

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index d03c9f9..003e501 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -10,20 +10,20 @@ services:
       - back_net

   ui:
-    image: ${USERNAME}/ui:latest
+    image: ${USERNAME}/ui:logging
     ports:
       - ${UI_PORT}:9292/tcp
     networks:
       - front_net

   post:
-    image: ${USERNAME}/post:latest
+    image: ${USERNAME}/post:logging
     networks:
       - back_net
       - front_net

   comment:
-    image: ${USERNAME}/comment:latest
+    image: ${USERNAME}/comment:logging
     networks:
       - back_net
       - front_net
-  prometheus:
-    image: ${USERNAME}/prometheus:latest
-    ports:
-      - '9090:9090'
-    volumes:
-      - prometheus_data:/prometheus
-    command: # Передаем доп параметры в командной строке
-      - '--config.file=/etc/prometheus/prometheus.yml'
-      - '--storage.tsdb.path=/prometheus'
-      - '--storage.tsdb.retention=1d' # Задаем время хранения метрик в 1 день
-    networks:
-      - front_net
-      - back_net
-
-  node-exporter:
-    image: prom/node-exporter:v0.15.2
-    user: root
-    volumes:
-      - /proc:/host/proc:ro
-      - /sys:/host/sys:ro
-      - /:/rootfs:ro
-    command:
-      - '--path.procfs=/host/proc'
-      - '--path.sysfs=/host/sys'
-      - '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"'
-    networks:
-      - front_net
-      - back_net
-
-  mongo-exporter:
-    image: percona/mongodb_exporter:0.35
-    command:
-      - '--mongodb.uri=mongodb://post_db:27017'
-      - '--collect-all'
-      - '--log.level=debug'
-    ports:
-      - '9216:9216'
-    networks:
-      - back_net
-
-  blackbox-exporter:
-    image: r2d2k/blackbox-exporter:1.0
-    ports:
-      - '9115:9115'
-    networks:
-      - front_net
-
 volumes:
   post_db:
-  prometheus_data:

 networks:
   front_net:

Стек запущен:

> docker compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
r2d2k-comment-1     "puma"                   comment             running
r2d2k-post-1        "python3 post_app.py"    post                running
r2d2k-post_db-1     "docker-entrypoint.s…"   post_db             running             27017/tcp
r2d2k-ui-1          "puma"                   ui                  running             0.0.0.0:9292->9292/tcp, :::9292->9292/tcp

Смотрим, что с логами docker logs -f r2d2k-post-1:

{"event": "find_all_posts", "level": "info", "message": "Successfully retrieved all posts from the database", "params": {}, "request_id": "d8f9c2d9-88e8-4674-b40f-a875877266b3", "service": "post", "timestamp": "2022-11-10 18:32:45"}
{"addr": "10.0.1.3", "event": "request", "level": "info", "method": "GET", "path": "/posts?", "request_id": "d8f9c2d9-88e8-4674-b40f-a875877266b3", "response_status": 200, "service": "post", "timestamp": "2022-11-10 18:32:45"}
{"event": "find_all_posts", "level": "info", "message": "Successfully retrieved all posts from the database", "params": {}, "request_id": "0d704b11-5434-448f-bd83-9296bf965c68", "service": "post", "timestamp": "2022-11-10 18:32:47"}
{"addr": "10.0.1.3", "event": "request", "level": "info", "method": "GET", "path": "/posts?", "request_id": "0d704b11-5434-448f-bd83-9296bf965c68", "response_status": 200, "service": "post", "timestamp": "2022-11-10 18:32:47"}

Логи выглядят довольно структурированно. Настроим драйвер логирования для сервиса post:

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 7db7f7d..bb38d2e 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -21,6 +21,11 @@ services:
     networks:
       - back_net
       - front_net
+    logging:
+      driver: "fluentd"
+      options:
+        fluentd-address: localhost:24224
+        tag: service.post

   comment:
     image: ${USERNAME}/comment:logging

Сначала запустим стек для приёма логов:

> docker compose -f docker-compose-logging.yml up -d

> lynx -dump http://127.0.0.1:9200
{
  "name" : "4c627b97aef0",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "jU1XzzAaRSitHuIUOysHnA",
  "version" : {
    "number" : "7.4.0",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "22e1767283e61a198cb4db791ea66e3f11ab9910",
    "build_date" : "2019-09-27T08:36:48.569419Z",
    "build_snapshot" : false,
    "lucene_version" : "8.2.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

> lynx -dump http://127.0.0.1:5601
   Loading Kibana

Please upgrade your browser

   This Kibana installation has strict security requirements enabled that
   your current browser does not meet.

Видим, что elasticsearch и kibana поднялись и отвечают на запросы. Перезапустим стек нашего приложения docker compose down, затем docker compose up -d. Создадим несколько постов и проверим, что видно в kibana.

Ничего не видно, создать индексы не можем, нет данных. Идём в логи fluentd, видим, что он не может найти транспорт elasticsearch.Говорят, что должен быть gem elasticsearch.

Для нормальной работы версии пакетов должны быть примерно одного времени выпуска. За ориентир возьмём fluerntd-plugin-elasticsearch.

OMG, версия 1.9.5 была выпущена 11 мая 2017 года. Ищем версию gem elasticsearch того же времени, это версия 6.2.0 выглядит подходящей.

Обновляем Dockerfile fluentd, собираем образ и запускаем стек. После запуска идём в kibana, создаём индекс, данные идут!

Записи мы получаем, но они выглядят, как строки, но в формате JSON:

{"addr": "10.0.1.3", "event": "request", "level": "info", "method": "GET", "path": "/healthcheck?", "request_id": null, "response_status": 200, "service": "post", "timestamp": "2022-11-10 19:31:41"}

Можем разбить эту строку на поля, для этого используем фильры fluentd. Изменим конфиг:

index fedd386..9d2cb56 100644
--- a/logging/fluentd/fluent.conf
+++ b/logging/fluentd/fluent.conf
@@ -4,6 +4,12 @@
   bind 0.0.0.0
 </source>

+<filter service.post>
+  @type parser
+  format json
+  key_name log
+</filter>
+
 <match *.**>
   @type copy
   <store>
@@ -22,3 +28,4 @@
     @type stdout
   </store>
 </match>

Cоберём образ, перезапустим его и что мы видим? Видим, что строка разобрана на поля. Можем фильтровать, искать, что нам нужно.

Теперь разбираемся с неструктурированными логами. Сервис ui как раз занимается оправкой таких логов. Добавим к контейнеру драйвер для логирования:

index bb38d2e..fd85fe4 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -15,6 +15,11 @@ services:
       - ${UI_PORT}:9292/tcp
     networks:
       - front_net
+    logging:
+      driver: "fluentd"
+      options:
+        fluentd-address: localhost:24224
+        tag: service.ui

   post:
     image: ${USERNAME}/post:logging

Перезапускаем контейнер, видим логи сервиса ui следующего формата:

I, [2022-11-10T19:52:07.401598 #1]  INFO -- : service=ui | event=request | path=/post/636d4abf9185ef0015c1116a/vote/1 | request_id=6378a322-909c-4c4e-907d-4fca282088af | remote_addr=192.168.10.97 | method= POST | response_status=303

Разобрать такие строки можно при помощи регулярных выражений. Добавим фильтр в конфиг fluentd:

index 3ea4e43..93cead1 100644
--- a/logging/fluentd/fluent.conf
+++ b/logging/fluentd/fluent.conf
@@ -10,6 +10,12 @@
   key_name log
 </filter>

+<filter service.ui>
+  @type parser
+  format /\[(?<time>[^\]]*)\]  (?<level>\S+) (?<user>\S+)[\W]*service=(?<service>\S+)[\W]*event=(?<event>\S+)[\W]*(?:path=(?<path>\S+)[\W]*)?request_id=(?<request_id>\S+)[\W]*(?:remote_addr=(?<remote_addr>\S+)[\W]*)?(?:method= (?<method>\S+)[\W]*)?(?:response_status=(?<response_status>\S+)[\W]*)?(?:message='(?<message>[^\']*)[\W]*)?/
+  key_name log
+</filter>
+
 <match *.**>
   @type copy
   <store>

После пересборки и перезапуска контейнера fluentd видим, что строки разобраны по полям и доступны для поиска. Чтобы не усложнять себе жизнь регулядными выражениями, можно использовать grok шаблоны. Используем такой вместо фильтра на регулярных выражениях:

index 93cead1..0ef1c57 100644
--- a/logging/fluentd/fluent.conf
+++ b/logging/fluentd/fluent.conf
@@ -12,7 +12,8 @@

 <filter service.ui>
   @type parser
-  format /\[(?<time>[^\]]*)\]  (?<level>\S+) (?<user>\S+)[\W]*service=(?<service>\S+)[\W]*event=(?<event>\S+)[\W]*(?:path=(?<path>\S+)[\W]*)?request_id=(?<request_id>\S+)[\W]*(?:remote_addr=(?<remote_addr>\S+)[\W]*)?(?:method= (?<method>\S+)[\W]*)?(?:response_status=(?<response_status>\S+)[\W]*)?(?:message='(?<message>[^\']*)[\W]*)?/
+  format grok
+  grok_pattern %{RUBY_LOGGER}
   key_name log
 </filter>

После использования такого фильтра некоторые сообщения остаются в исходном виде.

service=ui | event=show_all_posts | request_id=5a1d3778-88eb-4bce-827b-0998b559abe8 | message='Successfully showed the home page with posts' | params: "{}"

Для их разбора можно применить несколько фильтров последовательно. Применим ещё один фильтр.

index 0ef1c57..ed56fb4 100644
--- a/logging/fluentd/fluent.conf
+++ b/logging/fluentd/fluent.conf
@@ -17,6 +17,14 @@
   key_name log
 </filter>

+<filter service.ui>
+  @type parser
+  format grok
+  grok_pattern service=%{WORD:service} \| event=%{WORD:event} \| request_id=%{GREEDYDATA:request_id} \| message='%{GREEDYDATA:message}'
+  key_name message
+  reserve_data true
+</filter>
+
 <match *.**>
   @type copy
   <store>

Результат №06-1:

  • Подготовили окружение для сбора логов на базе EFK
  • Настроили передачу логов контейнера в базу
  • Настроили разбор структурированных логов при помощи стандартных фильтров
  • Настроили grok для обработки неструктурированных логов
  • Визуализировали логи с помощью kibana

Задание №06-2:

  • Разобрать осталные неструктурированные логи

Решение №06-2:

Часть записей остаётся неразобранной, напиример такая строка:

service=ui | event=request | path=/ | request_id=47af35ff-46be-4dbd-83f8-6c544f3a0234 | remote_addr=192.168.10.97 | method= GET | response_status=200

Напишем для неё фильтр:

index 0ef1c57..7897957 100644
--- a/logging/fluentd/fluent.conf
+++ b/logging/fluentd/fluent.conf
@@ -17,6 +17,22 @@
   key_name log
 </filter>

+<filter service.ui>
+  @type parser
+  format grok
+  grok_pattern service=%{WORD:service} \| event=%{WORD:event} \| request_id=%{GREEDYDATA:request_id} \| message='%{GREEDYDATA:message}'
+  key_name message
+  reserve_data true
+</filter>
+
+<filter service.ui>
+  @type parser
+  format grok
+  grok_pattern service=%{WORD:service} \| event=%{WORD:event} \| path=%{URIPATH:path} \| request_id=%{UUID:request_id} \| remote_addr=%{IP:remote_addr} \| method= %{WORD:method} \| response_status=%{NUMBER:response_status}
+  key_name message
+</filter>
+
+

Для отладки шаблонов можно пользоваться одним из онлайн дебаггеров grok.

Результат №06-2:

  • После двух последовательных фильтров все сообщения сервиса ui начинают разбираться на составные части.

Задание №06-3:

  • Распределенный трейсинг

Решение №06-3:

Будем использовать zipkin. Для этого добавим новый сервис в стек логирования:

index 5bd05c7..a003de5 100644
--- a/docker/docker-compose-logging.yml
+++ b/docker/docker-compose-logging.yml
@@ -22,3 +22,8 @@ services:
     image: kibana:7.4.0
     ports:
       - "5601:5601"
+
+  zipkin:
+    image: openzipkin/zipkin:2.21.0
+    ports:
+      - "9411:9411"

Для его включения каждому сервису добавим переменную окружения:

index fd85fe4..c930eb9 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -20,6 +20,8 @@ services:
       options:
         fluentd-address: localhost:24224
         tag: service.ui
+    environment:
+      - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}

   post:
     image: ${USERNAME}/post:logging
@@ -31,12 +33,16 @@ services:
       options:
         fluentd-address: localhost:24224
         tag: service.post
+    environment:
+      - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}

   comment:
     image: ${USERNAME}/comment:logging
     networks:
       - back_net
       - front_net
+    environment:
+      - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}

 volumes:
   post_db:

Перезапускаем стек и открываем веб-интерфейс zipkin, порт 9411. Ходим по веб-интерфейсу нашего приложения, но в zipkin ничего не появляется. Правильно, мы не подключили zipkin к сетям нашего стека. Исправляем:

index a003de5..644012e 100644
--- a/docker/docker-compose-logging.yml
+++ b/docker/docker-compose-logging.yml
@@ -27,3 +27,19 @@ services:
     image: openzipkin/zipkin:2.21.0
     ports:
       - "9411:9411"
+    networks:
+      - front_net
+      - back_net
+
+networks:
+  front_net:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 10.0.1.0/24
+
+  back_net:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 10.0.2.0/24

Перезапускаем стек логирования, теперь данные сервисов собираются нормально.

Результат №06-3:

  • Мы настроили сбор данных о внутренних процессах сервисов при помощи zipkin

Задание №06-4:

  • Траблшутинг UI-экспириенса

С нашим приложением происходит что-то странное. Пользователи жалуются, что при нажатии на пост они вынуждены долго ждать, пока у них загрузится страница с постом. Жалоб на загрузку других страниц не поступало. Нужно выяснить, в чем проблема, используя Zipkin. Код приложения с багом отличается от используемого ранее в этом ДЗ и доступен в репозитории со сломанным кодом приложения. Т.е. необходимо сбилдить багнутую версию приложения и запустить Zipkin для неё.

Решение №06-4:

  • Качаем код по ссылке
  • Размещаем в корне нашего репозитория, каталог bugged-code
  • Удаляем подкаталог .git в каталоге bugged-code
  • Правим Gemfile, меняем https на http. Контейнеры старые, все сертификаты давно протухли
  • Собираем образы при помощи docker_build.sh
  • Навешиваем на каждый образ тег bug
  • Делаем копию docker-compose.yml в docker-compose-bug.yml, меняя теги logging на bug
  • Останавливаем старый стек, поднимаем новый

Открываем главную страницу приложения, видим, что нам прямым текстом говорят:

Can't show blog posts, some problems with the post service. Refresh?

Открываем kibana, в глаза бросается такая строка:

Failed to read from Post service. Reason: Failed to open TCP connection to 127.0.0.1:4567 (Connection refused - connect(2) for "127.0.0.1" port 4567)

Проверяем, видим, что в docker-compose-bug.yml забыли указать переменные окружения для контейнеров и они не могут найти друг друга. Исправим это:

--- docker/docker-compose.yml   2022-11-12 05:42:00.630286092 +0000
+++ docker/docker-compose-bug.yml       2022-11-12 12:06:08.709982815 +0000
@@ -10,7 +10,7 @@
       - back_net

   ui:
-    image: ${USERNAME}/ui:logging
+    image: ${USERNAME}/ui:bug
     ports:
       - ${UI_PORT}:9292/tcp
     networks:
@@ -21,10 +21,15 @@
         fluentd-address: localhost:24224
         tag: service.ui
     environment:
-      - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
+      - ZIPKIN_ENABLED=${ZIPKIN_ENABLED:-false}
+      - POST_SERVICE_HOST=${POST_SERVICE_HOST:-localhost}
+      - POST_SERVICE_PORT=${POST_SERVICE_PORT:-5000}
+      - COMMENT_SERVICE_HOST=${COMMENT_SERVICE_HOST:-localhost}
+      - COMMENT_SERVICE_PORT=${COMMENT_SERVICE_PORT:-9292}
+

   post:
-    image: ${USERNAME}/post:logging
+    image: ${USERNAME}/post:bug
     networks:
       - back_net
       - front_net
@@ -35,14 +40,20 @@
         tag: service.post
     environment:
       - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
+      - POST_DATABASE_HOST=${POST_DATABASE_HOST:-localhost}
+      - POST_DATABASE=${POST_DATABASE:-posts}
+

   comment:
-    image: ${USERNAME}/comment:logging
+    image: ${USERNAME}/comment:bug
     networks:
       - back_net
       - front_net
     environment:
       - ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
+      - COMMENT_DATABASE_HOST=${COMMENT_DATABASE_HOST:-localhost}
+      - COMMENT_DATABASE=${COMMENT_DATABASE:-comments}
+

 volumes:
   post_db:

Перезапускаем стек с багами, теперь всё работает. При попытке открыть пост получаем задержку около 3 секунд.

В zipkin видим, что функция db_find_single_post сервиса post отрабатывает за 3 секунды. Идём в исходники, ищем эту процедуру. Видим странную задержку, комментируем:

index 1441173..da062f0 100644
--- a/bugged-code/post-py/post_app.py
+++ b/bugged-code/post-py/post_app.py
@@ -164,7 +164,7 @@ def find_post(id):
         stop_time = time.time()  # + 0.3
         resp_time = stop_time - start_time
         app.post_read_db_seconds.observe(resp_time)
-        time.sleep(3)
+#        time.sleep(3)
         log_event('info', 'post_find',
                   'Successfully found the post information',
                   {'post_id': id})

Пересобираем образ, перезапускаем стек, проверяем работу. Процедура стала выполняться за 3 мс.

Прекрасно, ускорили сервис post в 1000 раз.

Результат №06-4:

  • Задержка при открытии поста вызвана time.sleep(3) в функции db_find_single_post сервиса post.

07 - Введение в kubernetes

Задание №07-1:

  • Разобрать на практике все компоненты Kubernetes, развернуть их вручную используя kubeadm
  • Ознакомиться с описанием основных примитивов нашего приложения и его дальнейшим запуском в Kubernetes

Решение №07-1:

Работу ведём в новой ветке kubernetes-1.

Опишем приложение в контексте Kubernetes с помощью манифестов в формате yaml. Для каждого из четырёх сервисов создаём Deployment манифесты.

Содержимое post-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: post-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: post
  template:
    metadata:
      name: post
      labels:
        app: post
    spec:
      containers:
      - image: r2d2k/post
        name: post

Содержимое ui-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ui-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ui
  template:
    metadata:
      name: ui
      labels:
        app: ui
    spec:
      containers:
      - image: r2d2k/ui
        name: ui

Содержимое comment-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: comment-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: comment
  template:
    metadata:
      name: comment
      labels:
        app: comment
    spec:
      containers:
      - image: r2d2k/comment
        name: comment

Содержимое mongo-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      name: mongo
      labels:
        app: mongo
    spec:
      containers:
      - image: mongo:3.2
        name: mongo

Развернуть k8s предложено при помощи kubeadm на двух нодах следующей конфигурации:

  • 4 GB RAM
  • 4 vCPU
  • 40 GB SSD

Виртуальные машины создадим в облаке Яндекс при помощи terraform. У нас уже подготовлен образ с docker, его и возьмём за основу. Процедура неоднократно проводилась в предыдущих заданиях, поэтому не буду приводить подробности.

На данный момент у нас есть две виртуальные машины с установленным docker. Разворачивать кластер будем при помощи ansible. Заготовку с динамическим инвентори возьмём из предыдущих заданий и убедимся, что всё функционирует как положено:

> ansible all -m ping
fhmugpmt4brksfmqcfnf.auto.internal | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
fhmct74c8394a74ghbu1.auto.internal | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Читаем официальное руководство по установке kubeadm, пишем плейбук.

- name: k8s by kubeadm
  hosts: all
  become: true
  tasks:

  - name: Add k8s apt-key
    apt_key:
      url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
      state: present

  - name: Add k8s repository
    apt_repository:
      repo: deb https://apt.kubernetes.io/ kubernetes-xenial main
      state: present

  - name: Install k8s utils
    apt:
      name: [kubelet, kubeadm, kubectl]
      state: present
      update_cache: true

  - name: Install cri-dockerd
    apt:
      deb: https://github.com/Mirantis/cri-dockerd/releases/download/v0.2.6/cri-dockerd_0.2.6.3-0.ubuntu-jammy_amd64.deb
      state: present


- name: Main node tasks
  hosts: fhmct74c8394a74ghbu1.auto.internal
  become: true
  tasks:

  - name: Init master node
    shell: kubeadm init --pod-network-cidr=10.244.0.0/16 --cri-socket unix://var/run/cri-dockerd.sock >> kubeadm_init.txt
    args:
      chdir: $HOME
      creates: kubeadm_init.txt

  - name: Create kubectl config dir
    file:
      path: /home/ubuntu/.kube
      state: directory
      owner: ubuntu
      group: ubuntu
      mode: 0755

  - name: Copy config for kubectl
    copy:
      src: /etc/kubernetes/admin.conf
      dest: /home/ubuntu/.kube/config
      remote_src: true
      owner: ubuntu
      group: ubuntu
      mode: 0600

  - name: Get join command
    shell: kubeadm token create --print-join-command
    register: join_command_out

  - name: Set join command
    set_fact:
      join_command: "{{ join_command_out.stdout_lines[0] }}"


- name: Worker node tasks
  hosts: fhmugpmt4brksfmqcfnf.auto.internal
  become: true
  tasks:

  - name: Join cluster
    shell: "{{ hostvars['fhmct74c8394a74ghbu1.auto.internal'].join_command }} --cri-socket unix://var/run/cri-dockerd.sock >> node_joined.txt"
    args:
      chdir: $HOME
      creates: hode_joined.txt

После запуска плейбука проверим состояние кластера на основной ноде:

$ kubectl get nodes
NAME                   STATUS     ROLES           AGE   VERSION
fhmct74c8394a74ghbu1   NotReady   control-plane   10m   v1.25.4
fhmugpmt4brksfmqcfnf   NotReady   <none>          10s   v1.25.4

Проверяем состояние ноды при помощи kubectl describe node ..., видим такую ошибку:

container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

Как и описано в документации, устанавливаем calico:

> kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
poddisruptionbudget.policy/calico-kube-controllers created
serviceaccount/calico-kube-controllers created
serviceaccount/calico-node created
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
clusterrole.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrole.rbac.authorization.k8s.io/calico-node created
clusterrolebinding.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrolebinding.rbac.authorization.k8s.io/calico-node created
daemonset.apps/calico-node created
deployment.apps/calico-kube-controllers created

Смотрим:

> kubectl get nodes
NAME                   STATUS   ROLES           AGE   VERSION
fhmct74c8394a74ghbu1   Ready    control-plane   13m   v1.25.4
fhmugpmt4brksfmqcfnf   Ready    <none>          13s   v1.25.4

Применяем созданные ранее манифесты при помощи kubectl apply -f <manifest_name>.yml.

Проверяем результат:

> kubectl get pods
NAME                                 READY   STATUS    RESTARTS   AGE
comment-deployment-7db9f6d87-47mqc   1/1     Running   0          38s
mongo-deployment-797dcbffd4-dpr7k    1/1     Running   0          2m9s
post-deployment-766cd985c7-r6whl     1/1     Running   0          42s
ui-deployment-75c5849b5c-tsn4h       1/1     Running   0          34s

Просто замечательно.

Результат №07-1:

  • При помощи terraform созданы виртуальные машины для нод кластера
  • При помощи ansible подготовлено окружение для кластера
  • Кластер инициализирован при помощи kubeadm
  • Ранее созданные манифесты применены к кластеру
  • Поды запущены

08 - Kubernetes. Запуск кластера и приложения. Модель безопасности

Задание №08-1:

  • Развернуть локальное окружение для работы с Kubernetes
  • Развернуть Kubernetes в Yandex Cloud
  • Запустить reddit в Kubernetes

Решение №08-1:

Работу ведём в новой ветке kubernetes-2.

Готовим окружение:

  • kubectl - главная утилита для работы с Kubernets API (все, что делает kubectl, можно сделать с помощью HTTP-запросов к API k8s)
  • minikube - утилита для разворачивания локальной инсталяции Kubernetes
  • ~/.kube - каталог, который содержит служебную информацию для kubectl (конфиги, кеши, схемы API)

kubectl будем ставить на Ubuntu "22.04.1 LTS (Jammy Jellyfish)". Ставим по официальной инструкции.

Проверяем результат:

> kubectl version
Client Version: version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.4", GitCommit:"872a965c6c6526caa949f0c6ac028ef7aff3fb78", GitTreeState:"clean", BuildDate:"2022-11-09T13:36:36Z", GoVersion:"go1.19.3", Compiler:"gc", Platform:"linux/amd64"}
Kustomize Version: v4.5.7

minikube устанавливаем подглядывая в документацию.

Проверяем:

> minikube version
minikube version: v1.28.0
commit: 986b1ebd987211ed16f8cc10aed7d2c42fc8392f

Поднимаем кластер:

> minikube start
* minikube v1.28.0 on Ubuntu 22.04 (kvm/amd64)
* Using the docker driver based on existing profile
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* docker "minikube" container is missing, will recreate.
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.25.3 on Docker 20.10.20 ...
  - Generating certificates and keys ...
  - Booting up control plane ...
  - Configuring RBAC rules ...
* Verifying Kubernetes components...
  - Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Enabled addons: default-storageclass, storage-provisioner
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

В процессе поднятия кластера автоматически настраивается kubectl, проверим:

> kubectl get nodes
NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   10m   v1.25.3

Обновляем наши манифесты.

Содержимое ui-deployment.yml:

apiVersion: apps/v1
kind: Deployment        # Deploy metadata
metadata:
  name: ui
  labels:
    app: reddit
    component: ui
spec:                   # Deploy specification
  replicas: 3
  selector:
    matchLabels:
      app: reddit
      component: ui
  template:             # Pod description
    metadata:
      name: ui-pod
      labels:
        app: reddit
        component: ui
    spec:
      containers:
      - image: r2d2k/ui
        name: ui

Проверяем:

> kubectl apply -f ui-deployment.yml
deployment.apps/ui created

> kubectl get deployment
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
ui     3/3     3            3           80s

> kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
ui-65475c5d46-ggrds   1/1     Running   0          31s
ui-65475c5d46-m4mdp   1/1     Running   0          31s
ui-65475c5d46-vrzvf   1/1     Running   0          31s

Можно пробросить порт пода на локальную машину:

> kubectl get pods --selector component=ui
NAME                  READY   STATUS    RESTARTS   AGE
ui-65475c5d46-ggrds   1/1     Running   0          3m44s
ui-65475c5d46-m4mdp   1/1     Running   0          3m44s
ui-65475c5d46-vrzvf   1/1     Running   0          3m44s

> kubectl port-forward ui-65475c5d46-ggrds 8080:9292
Forwarding from 127.0.0.1:8080 -> 9292
Forwarding from [::1]:8080 -> 9292
Handling connection for 8080

Проверим в соседней консоли:

> lynx -dump http://127.0.0.1:8080
   (BUTTON) [1]Microservices Reddit in ui-65475c5d46-ggrds container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://127.0.0.1:8080/
   2. http://127.0.0.1:8080/
   3. http://127.0.0.1:8080/
   4. http://127.0.0.1:8080/new

Настроим остальные сервисы.

Содержимое component-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: comment
  labels:
    app: reddit
    component: comment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: reddit
      component: comment
  template:
    metadata:
      name: comment
      labels:
        app: reddit
        component: comment
    spec:
      containers:
      - image: r2d2k/comment
        name: comment

Проверяем:

> kubectl apply -f comment-deployment.yml
deployment.apps/comment created

> kubectl get deployment
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
comment   3/3     3            3           10s
ui        3/3     3            3           10m

> kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
comment-f8fb4fdb-bb94z   1/1     Running   0          2m
comment-f8fb4fdb-krpwp   1/1     Running   0          2m
comment-f8fb4fdb-qcssw   1/1     Running   0          2m
ui-65475c5d46-ggrds      1/1     Running   0          10m
ui-65475c5d46-m4mdp      1/1     Running   0          10m
ui-65475c5d46-vrzvf      1/1     Running   0          10m

> kubectl port-forward comment-f8fb4fdb-bb94z 8080:9292
Forwarding from 127.0.0.1:8080 -> 9292
Forwarding from [::1]:8080 -> 9292
Handling connection for 8080
Handling connection for 8080

В соседней консоли проверим сервис:

> lynx -dump http://127.0.0.1:8080/healthcheck
   {"status":0,"dependent_services":{"commentdb":0},"version":"0.0.3"}

Содержимое post-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: post
  labels:
    app: reddit
    component: post
spec:
  replicas: 3
  selector:
    matchLabels:
      app: reddit
      component: post
  template:
    metadata:
      name: post
      labels:
        app: reddit
        component: post
    spec:
      containers:
      - image: r2d2k/post
        name: post

Проверяем:

> kubectl apply -f post-deployment.yml
deployment.apps/post created

> kubectl get deployment
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
comment   3/3     3            3           10m
post      3/3     3            3           20s
ui        3/3     3            3           20m

> kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
comment-f8fb4fdb-bb94z   1/1     Running   0          10m
comment-f8fb4fdb-krpwp   1/1     Running   0          10m
comment-f8fb4fdb-qcssw   1/1     Running   0          10m
post-86dd946b76-45nmr    1/1     Running   0          20s
post-86dd946b76-fjnrt    1/1     Running   0          20s
post-86dd946b76-tpc7h    1/1     Running   0          20s
ui-65475c5d46-ggrds      1/1     Running   0          20m
ui-65475c5d46-m4mdp      1/1     Running   0          20m
ui-65475c5d46-vrzvf      1/1     Running   0          20m

> kubectl port-forward post-86dd946b76-45nmr 8080:5000
Forwarding from 127.0.0.1:8080 -> 5000
Forwarding from [::1]:8080 -> 5000

В соседней консоли:

> lynx -dump http://127.0.0.1:8080/healthcheck
   {"status": 0, "dependent_services": {"postdb": 0}, "version": "0.0.2"}

У базы данных появляется том для хранения данных.

Содержимое mongo-deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongo
  labels:
    app: reddit
    component: mongo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reddit
      component: mongo
  template:
    metadata:
      name: mongo
      labels:
        app: reddit
        component: mongo
    spec:
      containers:
      - image: mongo:3.2
        name: mongo
        volumeMounts:                       # Mountpoint in container
        - name: mongo-persistent-storage
          mountPath: /data/db
      volumes:
      - name: mongo-persistent-storage
        emptyDir: {}

Смотрим результат:

> kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
comment-f8fb4fdb-bb94z   1/1     Running   0          20m
comment-f8fb4fdb-krpwp   1/1     Running   0          20m
comment-f8fb4fdb-qcssw   1/1     Running   0          20m
mongo-78fdcb9c65-7dntp   1/1     Running   0          20s
post-86dd946b76-45nmr    1/1     Running   0          10m
post-86dd946b76-fjnrt    1/1     Running   0          10m
post-86dd946b76-tpc7h    1/1     Running   0          10m
ui-65475c5d46-ggrds      1/1     Running   0          30m
ui-65475c5d46-m4mdp      1/1     Running   0          30m
ui-65475c5d46-vrzvf      1/1     Running   0          30m

> kubectl get deployment
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
comment   3/3     3            3           20m
mongo     1/1     1            1           20s
post      3/3     3            3           10m
ui        3/3     3            3           30m

В текущем состоянии приложение не будет работать, так его компоненты ещё не знают, как найти друг друга.

Для связи компонентов между собой и с внешним миром используется объект Service - абстракция, которая определяет набор POD-ов (Endpoints) и способ доступа к ним.

Создаём манифест сервиса для comment:

apiVersion: v1
kind: Service
metadata:
  name: comment
  labels:
    app: reddit
    component: comment
spec:
  ports:
  - port: 9292
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: comment

Проверяем:

> kubectl apply -f comment-service.yml
service/comment created

> kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
comment      ClusterIP   10.111.231.173   <none>        9292/TCP   10s
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP    90m

> kubectl describe service comment
Name:              comment
Namespace:         default
Labels:            app=reddit
                   component=comment
Annotations:       <none>
Selector:          app=reddit,component=comment
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.111.231.173
IPs:               10.111.231.173
Port:              <unset>  9292/TCP
TargetPort:        9292/TCP
Endpoints:         172.17.0.6:9292,172.17.0.7:9292,172.17.0.8:9292
Session Affinity:  None
Events:            <none>

> kubectl exec post-86dd946b76-45nmr -- nslookup comment

Name:      comment
Address 1: 10.111.231.173 comment.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve

Манифест для сервиса post:

apiVersion: v1
kind: Service
metadata:
  name: post
  labels:
    app: reddit
    component: post
spec:
  ports:
  - port: 5000
    protocol: TCP
    targetPort: 5000
  selector:
    app: reddit
    component: post

Проверяем:

> kubectl apply -f post-service.yml
service/post created

> kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
comment      ClusterIP   10.111.231.173   <none>        9292/TCP   25m
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP    120m
post         ClusterIP   10.109.193.148   <none>        5000/TCP   10s

> kubectl describe service post
Name:              post
Namespace:         default
Labels:            app=reddit
                   component=post
Annotations:       <none>
Selector:          app=reddit,component=post
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.109.193.148
IPs:               10.109.193.148
Port:              <unset>  5000/TCP
TargetPort:        9292/TCP
Endpoints:         172.17.0.10:9292,172.17.0.11:9292,172.17.0.9:9292
Session Affinity:  None
Events:            <none>

> kubectl exec post-86dd946b76-45nmr -- nslookup post
nslookup: can't resolve '(null)': Name does not resolve

Name:      post
Address 1: 10.109.193.148 post.default.svc.cluster.local

Так, как с базой данных также нужно общаться по сети, то готовим mongodb-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: mongodb
  labels:
    app: reddit
    component: mongo
spec:
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: reddit
    component: mongo

Проверяем:

> kubectl apply -f mongodb-service.yml
service/mongodb created

> kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
comment      ClusterIP   10.111.231.173   <none>        9292/TCP    50m
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP     130m
mongodb      ClusterIP   10.101.215.91    <none>        27017/TCP   20s
post         ClusterIP   10.109.193.148   <none>        5000/TCP    20m

> kubectl describe service mongodb
Name:              mongodb
Namespace:         default
Labels:            app=reddit
                   component=mongo
Annotations:       <none>
Selector:          app=reddit,component=mongo
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.101.215.91
IPs:               10.101.215.91
Port:              <unset>  27017/TCP
TargetPort:        27017/TCP
Endpoints:         172.17.0.12:27017
Session Affinity:  None
Events:            <none>

> kubectl exec post-86dd946b76-45nmr -- nslookup mongodb
nslookup: can't resolve '(null)': Name does not resolve

Name:      mongodb
Address 1: 10.101.215.91 mongodb.default.svc.cluster.local

Если пробросить порт сервиса ui наружу, попытаться подключиться к нему, то мы увидим ошибку. Сервис ui ищет совсем другой адрес: comment_db, а не mongodb, как и сервис comment ищет post_db. Эти адреса заданы в их Dockerfile в виде переменных окружения: POST_DATABASE_HOST=post_db и COMMENT_DATABASE_HOST=comment_db.

Пропишем в deployment этих сервисов переменные окружения, указывающие на сервис mongodb.

diff --git a/kubernetes/reddit/comment-deployment.yml b/kubernetes/reddit/comment-deployment.yml
index 4cecdab..380a34a 100644
--- a/kubernetes/reddit/comment-deployment.yml
+++ b/kubernetes/reddit/comment-deployment.yml
@@ -21,3 +21,6 @@ spec:
       containers:
       - image: r2d2k/comment
         name: comment
+        env:
+        - name: COMMENT_DATABASE_HOST
+          value: mongodb
diff --git a/kubernetes/reddit/post-deployment.yml b/kubernetes/reddit/post-deployment.yml
index 285ccb7..5a4499d 100644
--- a/kubernetes/reddit/post-deployment.yml
+++ b/kubernetes/reddit/post-deployment.yml
@@ -21,3 +21,6 @@ spec:
       containers:
       - image: r2d2k/post
         name: post
+        env:
+        - name: POST_DATABASE_HOST
+          value: mongodb

Применяем, проверяем:

> kubectl apply -f kubernetes/reddit
deployment.apps/comment configured
service/comment unchanged
deployment.apps/mongo unchanged
service/mongodb unchanged
deployment.apps/post configured
service/post unchanged
deployment.apps/ui unchanged

> kubectl port-forward ui-65475c5d46-ggrds 8080:9292
Forwarding from 127.0.0.1:8080 -> 9292
Forwarding from [::1]:8080 -> 9292
Handling connection for 8080

> lynx -dump http://127.0.0.1:8080
   (BUTTON) [1]Microservices Reddit in ui-65475c5d46-ggrds container

   (BUTTON)

0

   (BUTTON)

[4]test

13-11-2022
12:10

   [5]Go to the link

Menu

     * [6]All posts
     * [7]New post

References

   1. http://127.0.0.1:8080/
   2. http://127.0.0.1:8080/post/6370ded06cf4fe000f485bf4
   3. http://test2.com/
   4. http://127.0.0.1:8080/post/6370de9ba04c21000f6482ab
   5. http://test.com/
   6. http://127.0.0.1:8080/
   7. http://127.0.0.1:8080/new

Видим сохранённые посты в базе, ошибок нет.

В методичке написано, что нужно сделать по отдельному сервису для post и comment. Причина мне пока непонятна, но раз просят, значит надо) Создаём ещё два манифеста.

Содержимое comment-mongodb-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: comment-db
  labels:
    app: reddit
    component: mongo
    comment-db: "true"
spec:
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: reddit
    component: mongo
    comment-db: "true"

Содержимое post-mongodb-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: post-db
  labels:
    app: reddit
    component: mongo
    post-db: "true"
spec:
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: reddit
    component: mongo
    post-db: "true"

Ниже отражены изменения в существующих манифестах:

diff --git a/kubernetes/reddit/comment-deployment.yml b/kubernetes/reddit/comment-deployment.yml
index 380a34a..1c682bf 100644
--- a/kubernetes/reddit/comment-deployment.yml
+++ b/kubernetes/reddit/comment-deployment.yml
@@ -23,4 +23,4 @@ spec:
         name: comment
         env:
         - name: COMMENT_DATABASE_HOST
-          value: mongodb
+          value: comment-db
diff --git a/kubernetes/reddit/mongo-deployment.yml b/kubernetes/reddit/mongo-deployment.yml
index 50f3a11..f553bd7 100644
--- a/kubernetes/reddit/mongo-deployment.yml
+++ b/kubernetes/reddit/mongo-deployment.yml
@@ -5,6 +5,8 @@ metadata:
   labels:
     app: reddit
     component: mongo
+    comment-db: "true"
+    post-db: "true"
 spec:
   replicas: 1
   selector:
@@ -17,6 +19,8 @@ spec:
       labels:
         app: reddit
         component: mongo
+        comment-db: "true"
+        post-db: "true"
     spec:
       containers:
       - image: mongo:3.2
diff --git a/kubernetes/reddit/post-deployment.yml b/kubernetes/reddit/post-deployment.yml
index 5a4499d..75ffc08 100644
--- a/kubernetes/reddit/post-deployment.yml
+++ b/kubernetes/reddit/post-deployment.yml
@@ -23,4 +23,4 @@ spec:
         name: post
         env:
         - name: POST_DATABASE_HOST
-          value: mongodb
+          value: post-db

Применяем все манифесты в каталоге:

> kubectl apply -f kubernetes/reddit/
deployment.apps/comment configured
service/comment-db created
service/comment unchanged
deployment.apps/mongo configured
service/mongodb unchanged
deployment.apps/post configured
service/post-db created
service/post unchanged
deployment.apps/ui unchanged

> kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
comment      ClusterIP   10.111.231.173   <none>        9292/TCP    5h
comment-db   ClusterIP   10.108.201.61    <none>        27017/TCP   15s
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP     6h
mongodb      ClusterIP   10.101.215.91    <none>        27017/TCP   4h
post         ClusterIP   10.109.193.148   <none>        5000/TCP    4h
post-db      ClusterIP   10.99.236.22     <none>        27017/TCP   15s

Нам нужно как-то обеспечить доступ к ui снаружи, для этого нам понадобится Service для ui. Создаём ui-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: ui
  labels:
    app: reddit
    component: ui
spec:
  type: NodePort
  ports:
  - port: 9292
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui

Применяем:

> kubectl apply -f kubernetes/reddit/
deployment.apps/comment unchanged
service/comment-db unchanged
service/comment unchanged
deployment.apps/mongo unchanged
service/mongodb unchanged
deployment.apps/post unchanged
service/post-db unchanged
service/post unchanged
deployment.apps/ui unchanged
service/ui created

> kubectl get services
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
comment      ClusterIP   10.111.231.173   <none>        9292/TCP         5h30m
comment-db   ClusterIP   10.108.201.61    <none>        27017/TCP        50m
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP          7h
mongodb      ClusterIP   10.96.201.234    <none>        27017/TCP        30m
post         ClusterIP   10.109.193.148   <none>        5000/TCP         5h
post-db      ClusterIP   10.99.236.22     <none>        27017/TCP        40m
ui           NodePort    10.111.205.235   <none>        9292:32145/TCP   10s

При работе с minikube можно получить доступ к NodePort, выглядит это так:

> minikube service ui
|-----------|------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT |            URL            |
|-----------|------|-------------|---------------------------|
| default   | ui   |        9292 | http://192.168.49.2:32145 |
|-----------|------|-------------|---------------------------|
* Opening service default/ui in default browser...

После чего в браузере открывается наше приложение. Ходим по ссылкам, создаём заметки, всё работает.

Можно посмотреть список сервисов, которые доступны извне:

> minikube service list
|-------------|------------|--------------|---------------------------|
|  NAMESPACE  |    NAME    | TARGET PORT  |            URL            |
|-------------|------------|--------------|---------------------------|
| default     | comment    | No node port |                           |
| default     | comment-db | No node port |                           |
| default     | kubernetes | No node port |                           |
| default     | mongodb    | No node port |                           |
| default     | post       | No node port |                           |
| default     | post-db    | No node port |                           |
| default     | ui         |         9292 | http://192.168.49.2:32145 |
| kube-system | kube-dns   | No node port |                           |
|-------------|------------|--------------|---------------------------|

В комплекте с minikube идёт достаточно большое количество дополнений:

> minikube addons list
|-----------------------------|----------|--------------|--------------------------------|
|         ADDON NAME          | PROFILE  |    STATUS    |           MAINTAINER           |
|-----------------------------|----------|--------------|--------------------------------|
| ambassador                  | minikube | disabled     | 3rd party (Ambassador)         |
| auto-pause                  | minikube | disabled     | Google                         |
| cloud-spanner               | minikube | disabled     | Google                         |
| csi-hostpath-driver         | minikube | disabled     | Kubernetes                     |
| dashboard                   | minikube | disabled     | Kubernetes                     |
| default-storageclass        | minikube | enabled      | Kubernetes                     |
| efk                         | minikube | disabled     | 3rd party (Elastic)            |
| freshpod                    | minikube | disabled     | Google                         |
| gcp-auth                    | minikube | disabled     | Google                         |
| gvisor                      | minikube | disabled     | Google                         |
| headlamp                    | minikube | disabled     | 3rd party (kinvolk.io)         |
| helm-tiller                 | minikube | disabled     | 3rd party (Helm)               |
| inaccel                     | minikube | disabled     | 3rd party (InAccel             |
|                             |          |              | [info@inaccel.com])            |
| ingress                     | minikube | disabled     | Kubernetes                     |
| ingress-dns                 | minikube | disabled     | Google                         |
| istio                       | minikube | disabled     | 3rd party (Istio)              |
| istio-provisioner           | minikube | disabled     | 3rd party (Istio)              |
| kong                        | minikube | disabled     | 3rd party (Kong HQ)            |
| kubevirt                    | minikube | disabled     | 3rd party (KubeVirt)           |
| logviewer                   | minikube | disabled     | 3rd party (unknown)            |
| metallb                     | minikube | disabled     | 3rd party (MetalLB)            |
| metrics-server              | minikube | disabled     | Kubernetes                     |
| nvidia-driver-installer     | minikube | disabled     | Google                         |
| nvidia-gpu-device-plugin    | minikube | disabled     | 3rd party (Nvidia)             |
| olm                         | minikube | disabled     | 3rd party (Operator Framework) |
| pod-security-policy         | minikube | disabled     | 3rd party (unknown)            |
| portainer                   | minikube | disabled     | 3rd party (Portainer.io)       |
| registry                    | minikube | disabled     | Google                         |
| registry-aliases            | minikube | disabled     | 3rd party (unknown)            |
| registry-creds              | minikube | disabled     | 3rd party (UPMC Enterprises)   |
| storage-provisioner         | minikube | enabled      | Google                         |
| storage-provisioner-gluster | minikube | disabled     | 3rd party (Gluster)            |
| volumesnapshots             | minikube | disabled     | Kubernetes                     |
|-----------------------------|----------|--------------|--------------------------------|

При старте Kubernetes кластер имеет 3 namespace:

  • default - для объектов для которых не определен другой Namespace (в нём мы работали все это время)
  • kube-system - для объектов созданных Kubernetes и для управления им
  • kube-public - для объектов к которым нужен доступ из любой точки кластера

Для того, чтобы выбрать конкретное пространство имен, нужно указать флаг -n или –namespace при запуске kubectl.

Посмотрим, что у нас запущено для управления k8s:

> kubectl get all -n kube-system
NAME                                   READY   STATUS    RESTARTS        AGE
pod/coredns-565d847f94-t8tjd           1/1     Running   0               7h
pod/etcd-minikube                      1/1     Running   0               7h
pod/kube-apiserver-minikube            1/1     Running   0               7h
pod/kube-controller-manager-minikube   1/1     Running   0               7h
pod/kube-proxy-95jk6                   1/1     Running   0               7h
pod/kube-scheduler-minikube            1/1     Running   0               7h
pod/storage-provisioner                1/1     Running   1 (7h57m ago)   7h

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   7h

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-proxy   1         1         1       1            1           kubernetes.io/os=linux   7h

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   1/1     1            1           7h

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-565d847f94   1         1         1       7h

Включим dashboard:

> minikube dashboard
* Enabling dashboard ...
  - Using image docker.io/kubernetesui/dashboard:v2.7.0
  - Using image docker.io/kubernetesui/metrics-scraper:v1.0.8
* Some dashboard features require the metrics-server addon. To enable all features please run:

        minikube addons enable metrics-server


* Verifying dashboard health ...
* Launching proxy ...
* Verifying proxy health ...
* Opening http://127.0.0.1:33067/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...

После активации dashboard она откроется в браузере. Можно посмотреть состояние кластера со всх сторон:

  • Отслеживать состояние кластера и рабочих нагрузок в нём
  • Создавать новые объекты (загружать YAML-файлы)
  • Удалять и изменять объекты (кол-во реплик, YAML-файлы)
  • Отслеживать логи в POD-ах
  • При включении Heapster-аддона смотреть нагрузку на POD-ах

Воспользуемся namespace для отделения среды разработки нашего приложения от всего остального. Готовим манифест dev-namespace.yml:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

Применяем:

> kubectl apply -f dev-namespace.yml
namespace/dev created

Запускаем наше приложение в dev:

> kubectl apply -n dev -f kubernetes/reddit/
deployment.apps/comment created
service/comment-db created
service/comment created
namespace/dev unchanged
deployment.apps/mongo created
service/mongodb created
deployment.apps/post created
service/post-db created
service/post created
deployment.apps/ui created
service/ui created

> kubectl get services -n dev
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
comment      ClusterIP   10.111.173.253   <none>        9292/TCP         99s
comment-db   ClusterIP   10.111.33.252    <none>        27017/TCP        99s
mongodb      ClusterIP   10.96.223.27     <none>        27017/TCP        99s
post         ClusterIP   10.105.189.255   <none>        5000/TCP         99s
post-db      ClusterIP   10.99.182.120    <none>        27017/TCP        99s
ui           NodePort    10.108.95.203    <none>        9292:30282/TCP   99s

Проверяем minikube service ui -n dev, всё работает.

Результат №08-1:

  • При помощи minikube был развёрнут локальный k8s
  • Подготовлены манифесты для запуска приложения reddit в k8s
  • Приложение запущено в локальном кластере и оно работает

Задание №08-2:

  • Мы подготовили наше приложение в локальном окружении.
  • Самое время запустить его на реальном кластере Kubernetes.
  • В качестве основной платформы будем использовать Yandex Cloud "Managed Service for kubernetes"

Решение №08-2:

  • Идём в Yandex Cloud, перейдите в "Managed Service for kubernetes"
  • Жмём "Создать Cluster"
  • Имя кластера может быть произвольным
  • Если нет сервис аккаунта его можно создать
  • Релизный канал *** Rapid ***
  • Версия k8s 1.19
  • Зона доступности - на ваше усмотрение (сети - аналогично)
  • Жмём "Создать"" и ждём, пока поднимется кластер
  • После создания кластера, вам нужно создать группу узлов, входящих в кластер
  • Версия k8s 1.19
  • Количество узлов - 2
  • vCPU - 4
  • RAM - 8
  • Disk - SSD 64ГБ (минимальное значение)
  • В поле "Доступ" добавьте свой логин и публичный ssh-ключ

После поднятия кластера настраиваем к нему доступ:

> yc managed-kubernetes cluster get-credentials test-cluster --external

> kubectl config get-contexts
CURRENT   NAME              CLUSTER                               AUTHINFO                              NAMESPACE
          minikube          minikube                              minikube                              default
*         yc-test-cluster   yc-managed-k8s-********************   yc-managed-k8s-********************

Запускаем наше приложение:

> kubectl apply -f kubernetes/reddit/dev-namespace.yml
namespace/dev created

> kubectl apply -f kubernetes/reddit/ -n dev
deployment.apps/comment created
service/comment-db created
service/comment created
namespace/dev unchanged
deployment.apps/mongo created
service/mongodb created
deployment.apps/post created
service/post-db created
service/post created
deployment.apps/ui created
service/ui created

> kubectl get services -n dev
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
comment      ClusterIP   10.96.205.17    <none>        9292/TCP         108s
comment-db   ClusterIP   10.96.184.40    <none>        27017/TCP        109s
mongodb      ClusterIP   10.96.166.9     <none>        27017/TCP        108s
post         ClusterIP   10.96.132.178   <none>        5000/TCP         108s
post-db      ClusterIP   10.96.184.43    <none>        27017/TCP        108s
ui           NodePort    10.96.204.142   <none>        9292:31706/TCP   107s

> kubectl get pods -n dev
NAME                       READY   STATUS    RESTARTS   AGE
comment-7799d6d7cf-f4csf   1/1     Running   0          2m
comment-7799d6d7cf-r8p6f   1/1     Running   0          2m
comment-7799d6d7cf-z5f6q   1/1     Running   0          2m
mongo-6b9fcfd49f-h9vkf     1/1     Running   0          119s
post-678cc6585-74794       1/1     Running   0          119s
post-678cc6585-j2895       1/1     Running   0          119s
post-678cc6585-rmkr8       1/1     Running   0          119s
ui-565b9d6499-56q9w        1/1     Running   0          118s
ui-565b9d6499-d2fvb        1/1     Running   0          118s
ui-565b9d6499-gtlsf        1/1     Running   0          118s

> kubectl get nodes -o wide
NAME                        STATUS   ROLES    AGE   VERSION   INTERNAL-IP   EXTERNAL-IP      OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
cl16c21m1fm9vhbf0rap-isyc   Ready    <none>   18m   v1.22.6   10.129.0.23   51.250.23.100    Ubuntu 20.04.4 LTS   5.4.0-124-generic   containerd://1.6.7
cl16c21m1fm9vhbf0rap-urer   Ready    <none>   18m   v1.22.6   10.129.0.10   84.201.140.137   Ubuntu 20.04.4 LTS   5.4.0-124-generic   containerd://1.6.7

Из вывода выше берём внешние адреса нод и порт приложения, проверяем:

> lynx -dump http://51.250.23.100:31706/
   (BUTTON) [1]Microservices Reddit in dev ui-565b9d6499-d2fvb container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://51.250.23.100:31706/
   2. http://51.250.23.100:31706/
   3. http://51.250.23.100:31706/new
amur@vm-minikube ~/r2d2k_microservices (kubernetes-2)> lynx -dump http://84.201.140.137:31706/
   (BUTTON) [1]Microservices Reddit in dev ui-565b9d6499-d2fvb container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://84.201.140.137:31706/
   2. http://84.201.140.137:31706/
   3. http://84.201.140.137:31706/new

Оба адреса в ответ возвращают наше приложение. Успех.

Результат №08-2:

  • В Yandex Cloud развёрнут кластер k8s
  • В этом кластере запущено приложение reddit

Задание №08-3

  • Разверните Kubernetes-кластер в Yandex cloud с помощью Terraform
  • Создайте YAML-манифесты для описания созданных сущностей для включения dashboard
  • Приложите конфигурацию к PR

Решение №08-3

Для создания кластера в облаке при помощи terraform придётся изучить документацию:

Что у нас получается в итоге:

resource "yandex_kubernetes_cluster" "yc_cluster" {

  name                    = var.cluster_name

  master {

    zonal {
      zone      = var.zone
      subnet_id = var.subnet_id
    }

    version               = var.k8s_version
    public_ip             = true
  }

  network_id              = var.network_id

  service_account_id      = var.service_account_id
  node_service_account_id = var.service_account_id

  release_channel         = "RAPID"
  network_policy_provider = "CALICO"
}

resource "yandex_kubernetes_node_group" "my_node_group" {

  cluster_id  = yandex_kubernetes_cluster.yc_cluster.id
  version     = var.k8s_version

  instance_template {
    platform_id = "standard-v2"

    network_interface {
      nat                = true
      subnet_ids         = [var.subnet_id]
    }

    resources {
      memory = var.node_memory_size
      cores  = var.node_cpu_count
    }

    boot_disk {
      type = "network-ssd"
      size = var.node_disk_size
    }

    scheduling_policy {
      preemptible = false
    }

    container_runtime {
      type = "containerd"
    }
  }

  scale_policy {
    fixed_scale {
      size = 1
    }
  }
}

Применяем конфигурацию, добавляем полученный кластер в локальную конфигурацию:

> yc managed-kubernetes cluster get-credentials test-cluster --external

Context 'yc-test-cluster' was added as default to kubeconfig '/home/.../.kube/config'.
Check connection to cluster using 'kubectl cluster-info --kubeconfig /home/.../.kube/config'.

Note, that authentication depends on 'yc' and its config profile 'default'.
To access clusters using the Kubernetes API, please use Kubernetes Service Account.

> kubectl config get-contexts
CURRENT   NAME              CLUSTER                               AUTHINFO                              NAMESPACE
          minikube          minikube                              minikube                              default
*         yc-test-cluster   yc-managed-k8s-********************   yc-managed-k8s-********************

> kubectl cluster-info
Kubernetes control plane is running at https://51.250.9.224
CoreDNS is running at https://51.250.9.224/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Для установки dashboard воспользуемся стандартным манифестом со страницы разработчика. Сохраним манифест в каталог kubernetes/dashboard.

Для того, чтобы полноценно управлять кластером, нужно создать пользователя с ролью cluster-admin. Подготовим манифест user-admin.yaml и сохраним его рядом с манифестом dashboard:

apiVersion: v1
kind: ServiceAccount
metadata:
 name: admin-user
 namespace: kubernetes-dashboard

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 name: admin-user
roleRef:
 apiGroup: rbac.authorization.k8s.io
 kind: ClusterRole
 name: cluster-admin
subjects:
- kind: ServiceAccount
 name: admin-user
 namespace: kubernetes-dashboard

Применим манифесты:

> kubectl apply -f dashboard/
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created
serviceaccount/admin-user created
clusterrolebinding.rbac.authorization.k8s.io/admin-user created

> kubectl get pods --all-namespaces
NAMESPACE              NAME                                                  READY   STATUS    RESTARTS      AGE
kube-system            calico-node-b58hl                                     1/1     Running   0             24m
kube-system            calico-typha-58f9b7574f-xwj9t                         1/1     Running   0             33m
kube-system            calico-typha-79cddf6bd8-grs5x                         0/1     Pending   0             22m
kube-system            calico-typha-horizontal-autoscaler-8495b957fc-hrxnn   1/1     Running   0             33m
kube-system            calico-typha-vertical-autoscaler-6cc57f94f4-wh4j7     1/1     Running   3 (23m ago)   33m
kube-system            coredns-5f8dbbff8f-74wlt                              1/1     Running   0             33m
kube-system            ip-masq-agent-qnlcr                                   1/1     Running   0             24m
kube-system            kube-dns-autoscaler-598db8ff9c-gp8jj                  1/1     Running   0             33m
kube-system            kube-proxy-5q2xz                                      1/1     Running   0             24m
kube-system            metrics-server-7574f55985-hjl2g                       2/2     Running   0             23m
kube-system            npd-v0.8.0-kfflp                                      1/1     Running   0             24m
kube-system            yc-disk-csi-node-v2-p8hkl                             6/6     Running   0             24m
kubernetes-dashboard   dashboard-metrics-scraper-7c857855d9-54nhw            1/1     Running   0             2m
kubernetes-dashboard   kubernetes-dashboard-6b79449649-r2cxq                 1/1     Running   0             2m

Для доступа к dashboard запускаем kubectl proxy, на этой же машине переходим по адресу http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/. У нас запрашивают токен, который можно получить следующим образом:

> kubectl get secret -n kubernetes-dashboard $(kubectl get serviceaccount admin-user -n kubernetes-dashboard -o jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64 --decode

Вводим токен для авторизации и попадаем в dashboard.

Результат №08-3

  • При помощи terraform поднят кластер k8s в Yandex Cloud
  • Подготовлены и проверенв а работе манифесты для запуска dashboard и создания пользователя с правами администратора

09 - Kubernetes. Networks, Storages

Задание №09-1:

  • Ingress Controller
  • Ingress
  • Secret
  • TLS
  • LoadBalancer Service
  • Network Policies
  • PersistentVolumes
  • PersistentVolumeClaims

Решение №09-1:

Работаем в новой ветке kubernetes-3.

При помощи terraform в Yandex Cloud поднимаем кластер, который готовили в предыдущем задании. Пока кластер поднимается (занимает минут десять), разбермся с балансировщиком.

Тип LoadBalancer позволяет нам использовать внешний облачный балансировщик нагрузки как единую точку входа в наши сервисы, а не полагаться на IPTables и не открывать наружу весь кластер. Настроим соответствующим образом сервис ui, правим ui-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: ui
  labels:
    app: reddit
    component: ui
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui

Регистрируем кластер в локальном окружении:

> yc managed-kubernetes cluster get-credentials test-cluster --external
...

> kubectl cluster-info
Kubernetes control plane is running at https://158.160.39.143
CoreDNS is running at https://158.160.39.143/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Применяем манифесты и проверяем результат:

> kubectl apply -f kubernetes/reddit/dev-namespace.yml
namespace/dev created

> kubectl apply -n dev -f kubernetes/redditdeployment.apps/comment created
service/comment-db created
service/comment created
namespace/dev unchanged
deployment.apps/mongo created
service/mongodb created
deployment.apps/post created
service/post-db created
service/post created
deployment.apps/ui created
service/ui created

> kubectl get pods -n dev
NAME                       READY   STATUS    RESTARTS   AGE
comment-7799d6d7cf-mg5lj   1/1     Running   0          60s
comment-7799d6d7cf-t5pzf   1/1     Running   0          60s
comment-7799d6d7cf-xn2bs   1/1     Running   0          60s
mongo-6b9fcfd49f-5rgwm     1/1     Running   0          60s
post-678cc6585-4ssr5       1/1     Running   0          60s
post-678cc6585-q6fpb       1/1     Running   0          60s
post-678cc6585-z8dwf       1/1     Running   0          60s
ui-565b9d6499-4n9ck        1/1     Running   0          60s
ui-565b9d6499-8cmg5        1/1     Running   0          60s
ui-565b9d6499-r2826        1/1     Running   0          60s

> kubectl get services -n dev
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
comment      ClusterIP      10.96.158.99    <none>           9292/TCP       100s
comment-db   ClusterIP      10.96.170.3     <none>           27017/TCP      100s
mongodb      ClusterIP      10.96.226.69    <none>           27017/TCP      100s
post         ClusterIP      10.96.176.224   <none>           5000/TCP       100s
post-db      ClusterIP      10.96.243.20    <none>           27017/TCP      100s
ui           LoadBalancer   10.96.146.125   158.160.37.149   80:31746/TCP   100s

> lynx -dump http://158.160.37.149/
   (BUTTON) [1]Microservices Reddit in dev ui-565b9d6499-8cmg5 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://158.160.37.149/
   2. http://158.160.37.149/
   3. http://158.160.37.149/new

Видим, что все поды поднялись, сервисы работают, а у сервиса LoadBalancer появился внешний адрес. По этому адресу и доступно наше приложение.

Балансировка с помощью Service типа LoadBalancer имеет ряд недостатков:

  • Нельзя управлять с помощью http URI (L7-балансировщика)
  • Используются только облачные балансировщики
  • Нет гибких правил работы с трафиком

Для более удобного управления входящим снаружи трафиком и решения недостатков LoadBalancer можно использовать другой объект Kubernetes - Ingress

Ingress - это набор правил внутри кластера Kuberntes, предназначенных для того, чтобы входящие подключения могли достичь сервисов. Сами по себе Ingress'ы это просто правила. Для их применения нужен Ingress Controller.

Ingress Controller - это скорее плагин (а значит и отдельный POD), который состоит из 2-х функциональных частей:

  • Приложение, которое отслеживает через k8s API новые объекты Ingress и обновляет конфигурацию балансировщика
  • Балансировщик (Nginx, haproxy, traefik, ...), который и занимается управлением сетевым трафиком

Основные задачи, решаемые с помощью Ingress'ов:

  • Организация единой точки входа в приложения снаружи
  • Обеспечение балансировки трафика
  • Терминация SSL
  • Виртуальный хостинг на основе имен

Установим ingress controller:

> kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.5.1/deploy/static/provider/cloud/deploy.yaml

namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created

Создадим манифест ingress для сервиса ui:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ui
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ui
            port:
              number: 9292

Применим:

> kubectl apply -n dev -f ui-ingress.yml
ingress.networking.k8s.io/ui configured

Проверим статус:

> kubectl get ingress ui -n dev
NAME   CLASS   HOSTS   ADDRESS        PORTS   AGE
ui     nginx   *       62.84.124.91   80      12m

> lynx -dump http://62.84.124.91/
   (BUTTON) [1]Microservices Reddit in dev ui-565b9d6499-8cmg5 container

Menu

     * [2]All posts
     * [3]New post

References

   1. http://62.84.124.91/
   2. http://62.84.124.91/
   3. http://62.84.124.91/new

Теперь давайте защитим наш сервис с помощью TLS. Генерируем сертификат:

> openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=62.84.124.91"
..+.....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*......+...+..+...+......+....+.................+.+.....+....+...+.....+...+............+.+..+...+.+...+.....+............+.............+...........+......+...............+.+..+.+..+....+........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+......+.........+.+..+............+.......+......+........+....+...+.....+............+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
...+.....+...+...+...+.........+.+..+.......+..+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+....+...+..+.........+....+......+..+.........+...+...+...+....+...+...+......+.....+...+.+.....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+....+........+......+..........+.....+......+..........+......+..............+...+.......+........+.+..+....+.........+..+............+......+.+.....+....+......+...+..+.........+....+........+...+............+....+.....+...+......+.......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-----

Создаём секрет с данным сертификатом:

> kubectl create secret tls ui-ingress --key tls.key --cert tls.crt -n dev
secret/ui-ingress created

> kubectl describe secret ui-ingress -n dev
Name:         ui-ingress
Namespace:    dev
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/tls

Data
====
tls.crt:  1123 bytes
tls.key:  1704 bytes

Обновим манифест ui-ingress.yml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ui
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - secretName: ui-ingress
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ui
            port:
              number: 9292

Применяем, проверяем:

> kubectl apply -n dev -f ui-ingress.yml
ingress.networking.k8s.io/ui configured

> kubectl get ingress ui -n dev
NAME   CLASS   HOSTS   ADDRESS        PORTS     AGE
ui     nginx   *       62.84.124.91   80, 443   26m

> kubectl describe ingress ui -n dev
Name:             ui
Labels:           <none>
Namespace:        dev
Address:          62.84.124.91
Ingress Class:    nginx
Default backend:  <default>
TLS:
  ui-ingress terminates
Rules:
  Host        Path  Backends
  ----        ----  --------
  *
              /   ui:9292 (10.112.128.23:9292,10.112.128.24:9292,10.112.128.25:9292)
Annotations:  nginx.ingress.kubernetes.io/force-ssl-redirect: true
Events:
  Type    Reason  Age                  From                      Message
  ----    ------  ----                 ----                      -------
  Normal  Sync    2m21s (x8 over 20m)  nginx-ingress-controller  Scheduled for sync

> lynx -dump http://62.84.124.91
                           308 Permanent Redirect
     __________________________________________________________________

                                    nginx

Наш сервис отвечает по https, а при обращении на http делает редирект.

Результат №09-1:

  • Создан ingress controller
  • Создан ingress
  • Настроено шифрование трафика при помощи сертификата (HTTPS)

Задание №09-2:

  • Опишите создаваемый объект Secret в виде Kubernetes-манифеста

Решение №09-2:

Предлагаю не мудрить. Готовый секрет у нас есть, поэтому добудем его содержимое:

> kubectl edit secret ui-ingress -n dev

apiVersion: v1
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREekNDQWZlZ0F3SUJBZ0lVU2dUaGxZeGFXejBSTVdBc1VPMnBTSHNhWXc4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0Z6RVZNQk1HQTFVRUF3d01Oakl1T0RRdU1USTBMamt4TUI0WERUSXlNVEV4TkRFM05UZ3pNVm9YRFRJegpNVEV4TkRFM05UZ3pNVm93RnpFVk1CTUdBMVVFQXd3TU5qSXVPRFF1TVRJMExqa3hNSUlCSWpBTkJna3Foa2lHCjl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6ZVBzMVYvT050MmNhK3k2OURFWnkzTENiZ3NKNmo2ZzJvelkKYkhyQlBWaGJmYnNEMGNtakZmdWVpY0J6eldOVkZZeGJPQWV3dXpwcDJmYnJPR3lvSzhUWUx4VXVQSVVucW1XZwp3eVpaMDlUN2pxak1STnFmQTBoekJDVGE3VW82ajBqUGZvMWQyZ1UwaXQ4MHl2UTVUbXhValdja2ZHSFlKRU9DCjBiRTdzSkRQZVVHcW9yU3c2RVRYWkUramlDTHIrRHU5d09rV01oekI0MlhvZU1NOHVrOVh3bnBTdUdGZHRheGMKNE1CVTN6djg2OGprdStYV1FKU2Z2NWMwdE5EMXlHZmFnaUJnMmNnTVloQW4waS9SZmVrVS9YSk5URlJkVThjZwpMMGJyWDgwOHdLNjA5c2R4c2tjTlVId253U1p2MWNqK3JkVlkvRGQ0eDBMeHRzS1YwUUlEQVFBQm8xTXdVVEFkCkJnTlZIUTRFRmdRVU5IYlhacExaejlSekVxY0lEaGthZnRpRUhYZ3dId1lEVlIwakJCZ3dGb0FVTkhiWFpwTFoKejlSekVxY0lEaGthZnRpRUhYZ3dEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQwpBUUVBWUw1UVRYRk5zU3htckJyNERmQ3lUUXBaNlA3eFdQN1NnVE9DenN5M3ZpalNjZ3pPUkt0cjJMSk5GYzdICnE3WDlGL1FVbzRpZ1VlVmlaaytDek80QURwa2FMWUlmWGM5K2NEQWE1ZjY0Q1dPYXV6Z2k2cCs5NUNRd2N0VzEKa1RsdzJaK3BPYkp1SEZEZ09MQ0lLbXA4cGIvVFlGU3VxMk9OdFBWMjk1V29Wd0dkSllBZHhBTkRGczdzOXBYego1S0dYRWNURlg3aFZOOUFSZFNzUEZiRW56M1YrRFRKQnVpWkNDNXJGZmc3MFdpVWErZkhvalh6WWlTbGg1OEh0CnVMcjlYNlM5bHJtcDF0YXdYcERXS1V1MStPU1pYMUJiOGpJM2Q5YmxIMXlTNlA3MlljKzV5QU9GOSs5VVBia2QKeng3dmNWMWF6eVVKdHREa0hLeklKT0g4elE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRE40K3pWWDg0MjNaeHIKN0xyME1Sbkxjc0p1Q3ducVBxRGFqTmhzZXNFOVdGdDl1d1BSeWFNVis1Nkp3SFBOWTFVVmpGczRCN0M3T21uWgo5dXM0YktncnhOZ3ZGUzQ4aFNlcVphRERKbG5UMVB1T3FNeEUycDhEU0hNRUpOcnRTanFQU005K2pWM2FCVFNLCjN6VEs5RGxPYkZTTlp5UjhZZGdrUTRMUnNUdXdrTTk1UWFxaXRMRG9STmRrVDZPSUl1djRPNzNBNlJZeUhNSGoKWmVoNHd6eTZUMWZDZWxLNFlWMjFyRnpnd0ZUZk8venJ5T1M3NWRaQWxKKy9selMwMFBYSVo5cUNJR0RaeUF4aQpFQ2ZTTDlGOTZSVDljazFNVkYxVHh5QXZSdXRmelR6QXJyVDJ4M0d5UncxUWZDZkJKbS9WeVA2dDFWajhOM2pIClF2RzJ3cFhSQWdNQkFBRUNnZ0VBREpSeUZDZmd0dndWTmJ0VHA4U0xEbTY5Z3hlUFg2RHUvYlVnWGlFL0xTc1MKWm1uemlSZWV4Z2t0cEZla3FJTkhoaGNIb3BqZ0dBVktramNWOEdKWE5JNDRKVWtjNnM2NStaMXdvUkpEdEprNgpJZnNFSDFGSEhDc2tYbEZadzNXdGd1YTI0dDYzd2YyU2JjbGdqMHdCWnlzdEhKTVZkYmlBVndyandFR2RGcjJCCmF5MWFMS2lpemdHNCtMdERLcnhOSzdhb0NVYzFWN2kwRzhWdDBoUGJzWlpkM1hHVFNrVkZSQ1B0OE5mMDh3ckkKTGNBSE9CYzRKbTdEWlowQXJhaFBDRnVBOFhWVjk0SmNWVit1Wjk5OEc0SVRYS1l5MDNGYWV6K2tzeTFGTElnZApPU0g2WUFhVlpYUE5EalV2ZEdFaTF2Z0tOaU5kM1p0RTc2bmFwUi9FandLQmdRRDN2blVOUHRMRE5wemY0eGsvCk1vWkhYb0lMVnRrb21zOXZBS3JtZW8xQVd0cE1ydUE0T25NcXI0N0hlb2JEcmplU1JLdkZ2WHdCaEZoZzNobGYKQkJvTjRnak85Q2xITldjZGxnckxCMGtIbjJTTUJGaWlLdWFVU1ZRa2k5QWhVR1psTEg1Rlo0WmJHMkNoTnBhNAoyUXFGQ0hiaUdJUjlwTDF1TUZWM2xPZlgvd0tCZ1FEVXdHaHN3Q1FlYnJTZXhXU2JvcFpRT1JrUkFUc3pxNnJuCmlrK25xRjhvendBcEo5UEdFdm5ZWWFWd1VpYU43L3YrMnpFVkZ2S2c1eU9GZ2RnUHZHMXhiTE9MQmUxZVExZU4KSldpaFB5bEF0SUp5U0VHUUxLejVNenExSWtMRmhWc09XaWloRitKcE4rUlpzNEJVRFNobTJQb0tpeE5RK2hwVgpZWE1qUFA0U0x3S0JnRmFWK1NEMCtRS0RQdGE4NzJENERwZzQvcWhwNVNIYzRXekJSZm1oa1dhUm1rUTh4bDdBCmh1bS9TOTZLQXptMjFQTkpEdVBnY3N1dzdwYUVhVWVkRG1JVnd0Qlo0MmRnMGJZMGIzZEFCNVVqYnlmRWlSbTgKZHJRUzROYVpDdGZwMnErM21qWTFsVzZZSmZDU1BLRkVNZm9HMkUzekZiTTM5WURpWWF5V25XVVBBb0dCQUxpOApkakJ3U3l5dHZtTGJUamdpWHRrOEt6cnIwY2RWT2lxaG0vY2VLYnNhdTY0QTZrL2xMRk9xdm1nZ3ZWK2tVakdECmpVUWQwQUxOa2JlYy9zcnpPQ2swVlZiVGg4REJRdVhKNU9lWEc3QVd6ZXFFT1lJQ2VSUk9XcHpzS2dTdmZsaWgKQ3dTTzQ4ZXZnN1lzT3JOQlZhS3dwN1c5KzhEbDJ6WG1UMzc2dURkN0FvR0FJbGtBb2tqVUJwc2RRbGZJWFJFOAp0K1J1Q1lhTFBmTGltKzQ2V2haZkIzcjY4QVUvZlM0eFc3VHZZcUJ2QS9SOGdYVnpzTDRNeWt2ZWsyLzlyd3lRCjNEMSt2NEh4MVFWNWNNcURxb1QrNUZSYTgrbUZxQkNWcU5jcXNwemRnSTdlOUlwZVREWm90dk9ldHJZYmJkR20KclRBeldGZWc3ZXZxQVplYmtmdml2ZFE9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
kind: Secret
metadata:
  creationTimestamp: "2022-11-14T17:59:54Z"
  name: ui-ingress
  namespace: dev
  resourceVersion: "17833"
  uid: 005cbbc1-2cce-4e95-b235-14b8b29153a7
type: kubernetes.io/tls

Сохраним это в файл ui-ingress-secret.yml.

Результат №09-2:

  • Для создания ui-ingress-secret.yml мы использовали команду редактирования секрета.

Задание №09-3:

    • ограничить трафик, поступающий на mongodb отовсюду, кроме сервисов post и comment

Решение №09-3:

В прошлых проектах мы договорились о том, что хотелось бы разнести сервисы базы данных и сервис фронтенда по разным сетям, сделав их недоступными друг для друга. В Kubernetes у нас так сделать не получится с помощью отдельных сетей, так как все POD-ы могут достучаться друг до друга по-умолчанию. Мы будем использовать NetworkPolicy - инструмент для декларативного описания потоков трафика.

Описываем правило в манифесте mongo-network-policy.yml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-db-traffic
  labels:
    app: reddit
spec:
  podSelector:
    matchLabels:
      app: reddit
      component: mongo
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: reddit
          component: comment

Применяем, проверяем:

> kubectl apply -n dev -f mongo-network-policy.yml
networkpolicy.networking.k8s.io/deny-db-traffic created

> kubectl get networkpolicy -n dev
NAME              POD-SELECTOR                 AGE
deny-db-traffic   app=reddit,component=mongo   21s

> kubectl describe networkpolicy -n dev
Name:         deny-db-traffic
Namespace:    dev
Created on:   2022-11-14 00:00:00 +0000 UTC
Labels:       app=reddit
Annotations:  <none>
Spec:
  PodSelector:     app=reddit,component=mongo
  Allowing ingress traffic:
    To Port: <any> (traffic allowed to all ports)
    From:
      PodSelector: app=reddit,component=comment
  Not affecting egress traffic
  Policy Types: Ingress

Чтобы post мог подключаться к базе, его нужно добавить в podSelector:

index 2417842..48ba4a5 100644
--- a/kubernetes/reddit/mongo-network-policy.yml
+++ b/kubernetes/reddit/mongo-network-policy.yml
@@ -17,3 +17,7 @@ spec:
         matchLabels:
           app: reddit
           component: comment
+    - podSelector:
+        matchLabels:
+          app: reddit
+          component: post

Применяем, проверяем:

> kubectl apply -n dev -f mongo-network-policy.yml
networkpolicy.networking.k8s.io/deny-db-traffic configured

> kubectl get networkpolicy -n dev
NAME              POD-SELECTOR                 AGE
deny-db-traffic   app=reddit,component=mongo   19m

> kubectl describe networkpolicy -n dev
Name:         deny-db-traffic
Namespace:    dev
Created on:   2022-11-14 00:00:00 +0000 UTC
Labels:       app=reddit
Annotations:  <none>
Spec:
  PodSelector:     app=reddit,component=mongo
  Allowing ingress traffic:
    To Port: <any> (traffic allowed to all ports)
    From:
      PodSelector: app=reddit,component=comment
    From:
      PodSelector: app=reddit,component=post
  Not affecting egress traffic
  Policy Types: Ingress

Результат №09-3:

  • Для контейнера с БД ограничен трафик, доступ разрешён только с post и comment, но это не точно.

Задание №09-4:

  • Рассмотрим вопросы хранения данных

Решение №09-4:

Основной Stateful сервис в нашем приложении - это базы данных MongoDB. В текущий момент она запускается в виде Deployment и хранит данные в стандартных Docker Volume-ах. Это имеет несколько проблем:

  • При удалении POD-а удаляется и Volume
  • Потерям Nod'ы с mongo грозит потерей данных
  • Запуск базы на другой ноде запускает новый экземпляр данных

Пробуем удалить deployment для mongo и создать его заново. После запуска пода база оказывается пустой.

Для постоянного хранения данных используется PersistentVolume.

Создадим диск в облаке:

> yc compute disk create --name k8s --size 4 --description "disk for k8s"

done (10s)
id: fhm1tve2dl47lp9lukfj
folder_id: b1******************
created_at: "2022-11-14T00:00:00Z"
name: k8s
description: disk for k8s
type_id: network-hdd
zone_id: ru-central1-a
size: "4294967296"
block_size: "4096"
status: READY
disk_placement_policy: {}

Описываем mongo-volume.yml:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongo-pv
spec:
  capacity:
    storage: 4Gi
  accessModes:
    - ReadWriteOnce
  csi:
    driver: disk-csi-driver.mks.ycloud.io
    fsType: ext4
    volumeHandle: fhm1tve2dl47lp9lukfj

Мы создали ресурс дискового хранилища, распространенный на весь кластер, в виде PersistentVolume. Чтобы выделить приложению часть такого ресурса - нужно создать запрос на выдачу - PersistentVolumeClain. Claim - это именно запрос, а не само хранилище. С помощью запроса можно выделить место как из конкретного PersistentVolume (тогда параметры accessModes и StorageClass должны соответствовать, а места должно хватать), так и просто создать отдельный PersistentVolume под конкретный запрос.

Описываем mongo-claim.yml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongo-pvc
spec:
  storageClassName: ""
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  volumeName: mongo-ya-pd-storage

Обновляем mongo-deployment.yml:

--- a/kubernetes/reddit/mongo-deployment.yml
+++ b/kubernetes/reddit/mongo-deployment.yml
@@ -30,4 +30,5 @@ spec:
           mountPath: /data/db
       volumes:
       - name: mongo-persistent-storage
-        emptyDir: {}
+        persistentVolumeClaim:
+          claimName: mongo-pvc

Применяем, проверяем:

> kubectl apply -f mongo-pv.yml
persistentvolume/mongo-pv created

> kubectl apply -f mongo-pvc.yml -n dev
persistentvolumeclaim/mongo-pvc created

> kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
mongo-pv   4Gi        RWO            Retain           Available                                   50s

> kubectl describe pv mongo-pv
Name:            mongo-pv
Labels:          <none>
Annotations:     <none>
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Available
Claim:
Reclaim Policy:  Retain
Access Modes:    RWO
VolumeMode:      Filesystem
Capacity:        4Gi
Node Affinity:   <none>
Message:
Source:
    Type:              CSI (a Container Storage Interface (CSI) volume source)
    Driver:            disk-csi-driver.mks.ycloud.io
    FSType:            ext4
    VolumeHandle:      fhm1tve2dl47lp9lukfj
    ReadOnly:          false
    VolumeAttributes:  <none>
Events:                <none>

> kubectl get pvc -n dev
NAME        STATUS    VOLUME                CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mongo-pvc   Pending   mongo-ya-pd-storage   0                                        5m

> kubectl describe pvc mongo-pvc -n dev
Name:          mongo-pvc
Namespace:     dev
StorageClass:
Status:        Bound
Volume:        mongo-pv
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      4Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Used By:       mongo-7dbb8b6f7b-fqhkg
Events:        <none>

Создаём пост, удаляем под с базой данных, запускаем его заново. Пост на месте.

Всё работает.

Результат №09-4:

  • При помощи PersistentVolume было создано постоянное хранилище даных для mongo

10 - CI/CD в Kubernetes

Задание №10-1:

  • Работа с Helm
  • Развертывание Gitlab в Kubernetes
  • Запуск CI/CD конвейера в Kubernetes

Решение №10-1:

Работу ведём в новой ветке kubernetes-4.

Helm - пакетный менеджер для Kubernetes.

С его помощью мы будем:

  • Стандартизировать поставку приложения в Kubernetes
  • Декларировать инфраструктуру
  • Деплоить новые версии приложения

Начнём с установки Helm. Так, как у нас Ubuntu, то делаем так:

> sudo snap install helm --classic
helm 3.7.0 from Snapcrafters installed

Helm читает конфигурацию kubectl ~/.kube/config и сам определяет текущий контекст (кластер, пользователь, неймспейс).

Chart - это пакет в Helm. Создаём подкаталог Charts в каталоге kubernetes со следующей структурой:

> mkdir -p kubernetes/Charts/{comment,post,reddit,ui}

> tree kubernetes/Charts/
kubernetes/Charts/
├── comment
├── post
├── reddit
└── ui

Создадим файл-описание чарта для компонента ui, сохраним его в ui/Chart.yaml:

name: ui
version: 1.0.0
description: OTUS reddit application UI
maintainers:
  - name: me
    email: me@me.me
appVersion: 1.0

Перенесём в каталог ui/templates все манифесты, разработанные для ui, сделаем косметические изменения:

> tree
.
├── comment
├── post
├── reddit
└── ui
    ├── Chart.yaml
    └── templates
        ├── deployment.yaml
        ├── ingress.yaml
        └── service.yaml

Для работы нужен кластер Kubernetes, поднимем его в Yandex Cloud, как делали в предыдущих заданиях и зарегистрируем кластер локально.

> /home/amur/yandex-cloud/bin/yc managed-kubernetes cluster get-credentials test-cluster --external

> kubectl cluster-info
Kubernetes control plane is running at https://84.252.130.138
CoreDNS is running at https://84.252.130.138/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Установим наш Chart:

> helm ls
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION

> helm install test-ui-1 ui/
NAME: test-ui-1
LAST DEPLOYED: Wed Nov 16 05:06:31 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

> helm ls
NAME            NAMESPACE       REVISION        UPDATED                                STATUS   CHART           APP VERSION
test-ui-1       default         1               2022-11-16 05:06:31.776316382 +0000 UTCdeployed ui-1.0.0        1

> kubectl get deployments
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
ui     3/3     3            3           30s

> kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
ui-565b9d6499-9rj7n   1/1     Running   0          34s
ui-565b9d6499-bwf5h   1/1     Running   0          34s
ui-565b9d6499-j65sm   1/1     Running   0          34s

Теперь шаблонизируем Chart, чтобы можно было запускать несколько релизов одновременно.

diff --git a/kubernetes/Charts/ui/templates/deployment.yaml b/kubernetes/Charts/ui/templates/deployment.yaml
index 83d77ac..f3ee482 100644
--- a/kubernetes/Charts/ui/templates/deployment.yaml
+++ b/kubernetes/Charts/ui/templates/deployment.yaml
@@ -1,22 +1,25 @@
 apiVersion: apps/v1
 kind: Deployment       # Deploy metadata
 metadata:
-  name: ui
+  name: {{ .Release.Name }}-{{ .Chart.Name }}
   labels:
     app: reddit
     component: ui
+    release: {{ .Release.Name }}
 spec:                  # Deploy specification
   replicas: 3
   selector:
     matchLabels:
       app: reddit
       component: ui
+      release: {{ .Release.Name }}
   template:            # Pod description
     metadata:
       name: ui-pod
       labels:
         app: reddit
         component: ui
+        release: {{ .Release.Name }}
     spec:
       containers:
       - image: r2d2k/ui
diff --git a/kubernetes/Charts/ui/templates/ingress.yaml b/kubernetes/Charts/ui/templates/ingress.yaml
index e149a98..62bd972 100644
--- a/kubernetes/Charts/ui/templates/ingress.yaml
+++ b/kubernetes/Charts/ui/templates/ingress.yaml
@@ -1,7 +1,7 @@
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
-  name: ui
+  name: {{ .Release.Name }}-{{ .Chart.Name }}
   annotations:
     nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
 spec:
@@ -15,6 +15,6 @@ spec:
         pathType: Prefix
         backend:
           service:
-            name: ui
+            name: {{ .Release.Name }}-{{ .Chart.Name }}
             port:
               number: 9292
diff --git a/kubernetes/Charts/ui/templates/service.yaml b/kubernetes/Charts/ui/templates/service.yaml
index 8797099..971c457 100644
--- a/kubernetes/Charts/ui/templates/service.yaml
+++ b/kubernetes/Charts/ui/templates/service.yaml
@@ -1,10 +1,11 @@
 apiVersion: v1
 kind: Service
 metadata:
-  name: ui
+  name: {{ .Release.Name }}-{{ .Chart.Name }}
   labels:
     app: reddit
     component: ui
+    release: {{ .Release.Name }}
 spec:
   type: LoadBalancer
   ports:
@@ -14,3 +15,4 @@ spec:
   selector:
     app: reddit
     component: ui
+    release: {{ .Release.Name }}

Определим значения переменных в ui/values.yaml:

service:
  internalPort: 9292
  externalPort: 9292
image:
  repository: r2d2k/ui
  tag: latest

Установим пару релизов:

> helm install test-ui-2 ui/
NAME: test-ui-2
LAST DEPLOYED: Thu Nov 17 03:55:08 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

> helm install test-ui-3 ui/
NAME: test-ui-3
LAST DEPLOYED: Thu Nov 17 03:55:18 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

> kubectl get ingress
NAME           CLASS   HOSTS   ADDRESS   PORTS     AGE
test-ui-2-ui   nginx   *                 80, 443   18s
test-ui-3-ui   nginx   *                 80, 443   9s
ui             nginx   *                 80, 443   22h

Видим, что внешний адрес не появляется.

Спросим у облака, что у нас с балансировщиками:

> yc lb nlb list
+----------------------+----------------------------------------------+-------------+----------+----------------+------------------------+--------+
|          ID          |                     NAME                     |  REGION ID  |   TYPE   | LISTENER COUNT | ATTACHED TARGET GROUPS | STATUS |
+----------------------+----------------------------------------------+-------------+----------+----------------+------------------------+--------+
| enp6t701tk97ge4k75oc | k8s-91ce5bff8ca6697cb9567ac809a71aaf48e9f093 | ru-central1 | EXTERNAL |              1 | enpdssprnrnadoslcdhb   | ACTIVE |
| enpn65s3tpitk5k2me8c | k8s-6b39764270d3383e462b282effdec09230a84fb6 | ru-central1 | EXTERNAL |              1 | enpdssprnrnadoslcdhb   | ACTIVE |
+----------------------+----------------------------------------------+-------------+----------+----------------+------------------------+--------+

> yc lb nlb get enp6t701tk97ge4k75oc
id: enp6t701tk97ge4k75oc
folder_id: b1gesodgpd94d12usupp
created_at: "2022-11-17T04:20:47Z"
name: k8s-91ce5bff8ca6697cb9567ac809a71aaf48e9f093
description: cluster cat2v91bt9lco01gdk4k, service default/test-ui-2-ui
labels:
  cluster-name: cat2v91bt9lco01gdk4k
  service-name: test-ui-2-ui
  service-namespace: default
  service-uid: 4de34cbe-0588-4065-ae1f-370fa8e3d81f
region_id: ru-central1
status: ACTIVE
type: EXTERNAL
listeners:
  - name: default
    address: 84.201.132.37
    port: "80"
    protocol: TCP
    target_port: "30769"
    ip_version: IPV4
attached_target_groups:
  - target_group_id: enpdssprnrnadoslcdhb
    health_checks:
      - name: default
        interval: 10s
        timeout: 5s
        unhealthy_threshold: "2"
        healthy_threshold: "2"
        http_options:
          port: "10256"
          path: /healthz

> /home/amur/yandex-cloud/bin/yc lb nlb get enpn65s3tpitk5k2me8c
id: enpn65s3tpitk5k2me8c
folder_id: b1gesodgpd94d12usupp
created_at: "2022-11-17T04:14:37Z"
name: k8s-6b39764270d3383e462b282effdec09230a84fb6
description: cluster cat2v91bt9lco01gdk4k, service default/test-ui-1-ui
labels:
  cluster-name: cat2v91bt9lco01gdk4k
  service-name: test-ui-1-ui
  service-namespace: default
  service-uid: 5a2fcd6a-1120-461d-af4b-c740a434fa05
region_id: ru-central1
status: ACTIVE
type: EXTERNAL
listeners:
  - name: default
    address: 84.201.175.243
    port: "80"
    protocol: TCP
    target_port: "32084"
    ip_version: IPV4
attached_target_groups:
  - target_group_id: enpdssprnrnadoslcdhb
    health_checks:
      - name: default
        interval: 10s
        timeout: 5s
        unhealthy_threshold: "2"
        healthy_threshold: "2"
        http_options:
          port: "10256"
          path: /healthz

Видим, что создано два балансировщика, третьего нет не будет по простой причине:

Error syncing load balancer: failed to ensure load balancer: failed to ensure cloud loadbalancer: failed to start cloud lb creation: request-id = e44410dd-c2df-41b3-b701-cdf668701dfa rpc error: code = ResourceExhausted desc = Quota limit ylb.networkLoadBalancers.count exceeded

Что самое интересное - по адресам, указанным в свойствах балансировщика, наше приложение отвечает:

> lynx -dump http://84.201.132.37
   (BUTTON) [1]Microservices Reddit in default
   test-ui-2-ui-6c87d9b8c4-vpd2j container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://84.201.132.37/
   2. http://84.201.132.37/
   3. http://84.201.132.37/
   4. http://84.201.132.37/new

> lynx -dump http://84.201.175.243
   (BUTTON) [1]Microservices Reddit in default
   test-ui-1-ui-f974d5575-6kkqp container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://84.201.175.243/
   2. http://84.201.175.243/
   3. http://84.201.175.243/
   4. http://84.201.175.243/new

Пройдём по шаблонам и заменим значения портов на переменные. После обновим релиз:

> helm upgrade test-ui-1 ui/
Release "test-ui-1" has been upgraded. Happy Helming!
NAME: test-ui-1
LAST DEPLOYED: Thu Nov 17 04:39:44 2022
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None

> helm upgrade test-ui-2 ui/
Release "test-ui-2" has been upgraded. Happy Helming!
NAME: test-ui-2
LAST DEPLOYED: Thu Nov 17 04:39:50 2022
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None

> kubectl get ingress --all-namespaces
NAMESPACE   NAME           CLASS   HOSTS   ADDRESS   PORTS     AGE
default     test-ui-1-ui   nginx   *                 80, 443   25m
default     test-ui-2-ui   nginx   *                 80, 443   19m

> helm ls
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
test-ui-1       default         2               2022-11-17 04:39:44.236811529 +0000 UTC deployed        ui-1.0.0        1
test-ui-2       default         2               2022-11-17 04:39:50.938181636 +0000 UTC deployed        ui-1.0.0        1

> lynx -dump http://84.201.132.37:9292
   (BUTTON) [1]Microservices Reddit in default
   test-ui-2-ui-5b96678f8d-9r47v container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://84.201.132.37:9292/
   2. http://84.201.132.37:9292/
   3. http://84.201.132.37:9292/
   4. http://84.201.132.37:9292/new

> lynx -dump http://84.201.175.243:9292
   (BUTTON) [1]Microservices Reddit in default
   test-ui-1-ui-585676fb7-rgfvl container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://84.201.175.243:9292/
   2. http://84.201.175.243:9292/
   3. http://84.201.175.243:9292/
   4. http://84.201.175.243:9292/new

Приложение отвечает на портах, указанных в переменных.

Структура чарта приобрела следующий вид:

> tree
.
├── comment
├── post
└── ui
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── ingress.yaml
    │   └── service.yaml
    └── values.yaml

Подготовим чарты для остальных приложений, структура будет такая:

> tree
.
├── comment
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   └── values.yaml
├── post
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   └── values.yaml
└── ui
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── ingress.yaml
    │   └── service.yaml
    └── values.yaml

В каждую папку с шаблонами добавим _helpers.tpl:

{{- define "comment.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name }}
{{- end -}}

Все ссылки на имена заменим функцией {{ template "comment.fullname" . }}.

Для запуска приложения целиком создадим общий чарт в каталоге reddit: Содержимое requirements.yaml:

dependencies:
  - name: ui
    version: "1.0.0"
    repository: "file://../ui"

  - name: post
    version: "1.0.0"
    repository: "file://../post"

  - name: comment
    version: "1.0.0"
    repository: "file://../comment"

Сожержимое Chart.yaml:

name: reddit
version: 0.1.0
description: OTUS reddit application
maintainers:
  - name: me
    email: me@me.me
appVersion: 1.0

Загружаем зависимости:

> helm dep update
Saving 3 charts
Deleting outdated charts

> tree
.
├── Chart.yaml
├── charts
│   ├── comment-1.0.0.tgz
│   ├── post-1.0.0.tgz
│   └── ui-1.0.0.tgz
├── requirements.lock
├── requirements.yaml
└── values.yaml

Для полной картины нам не хватает базы данных, поищем готовый чарт:

> helm repo list
Error: no repositories to show

> helm repo add google https://kubernetes-charts.storage.googleapis.com
Error: repo "https://kubernetes-charts.storage.googleapis.com" is no longer available; try "https://charts.helm.sh/stable" instead

> helm repo add google https://charts.helm.sh/stable
"google" has been added to your repositories

> helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

> helm search repo mongo
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
bitnami/mongodb                         13.4.4          6.0.3           MongoDB(R) is a relational open source NoSQL da...
bitnami/mongodb-sharded                 6.1.13          6.0.3           MongoDB(R) is an open source NoSQL database tha...
google/mongodb                          7.8.10          4.2.4           DEPRECATED NoSQL document-oriented database tha...
google/mongodb-replicaset               3.17.2          3.6             DEPRECATED - NoSQL document-oriented database t...
google/prometheus-mongodb-exporter      2.8.1           v0.10.0         DEPRECATED A Prometheus exporter for MongoDB me...
google/unifi                            0.10.2          5.12.35         DEPRECATED - Ubiquiti Network's Unifi Controller

Попробуем установить БД так, как указано в методичке:

index 0d2aa1f..2477c52 100644
--- a/kubernetes/Charts/reddit/requirements.yaml
+++ b/kubernetes/Charts/reddit/requirements.yaml
@@ -10,3 +10,7 @@ dependencies:
   - name: comment
     version: "1.0.0"
     repository: "file://../comment"
+
+  - name: mongodb
+    version: 0.4.18
+    repository: https://charts.helm.sh/stable
> helm dep update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "google" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 4 charts
Downloading mongodb from repo https://charts.helm.sh/stable
Deleting outdated charts

У нас проблема:

> helm install reddit-test reddit
Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: unable to recognize "": no matches for kind "Deployment" in version "extensions/v1beta1"

Извлечём Chart БД из архива и оформим его отдельным каталогом. Поправим всё, что не нравится helm3.

Плохая идея, слишком много проблем, поэтому напишем свой чарт для БД.

Запускаем:

> helm install reddit-test reddit
NAME: reddit-test
LAST DEPLOYED: Thu Nov 17 17:54:55 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

> kubectl get pods
NAME                                   READY   STATUS         RESTARTS   AGE
reddit-test-comment-7986ffbdd4-lmpp5   1/1     Running        0          48s
reddit-test-mongodb-856884f4d4-znzpb   0/1     ErrImagePull   0          48s
reddit-test-post-5fbbcb9499-fscwv      1/1     Running        0          48s
reddit-test-ui-7b8ccdd5b7-7l4mr        1/1     Running        0          48s
reddit-test-ui-7b8ccdd5b7-8f4jg        1/1     Running        0          48s
reddit-test-ui-7b8ccdd5b7-lrkng        1/1     Running        0          48s

> kubectl describe pod reddit-test-mongodb-856884f4d4-znzpb
Name:             reddit-test-mongodb-856884f4d4-znzpb
Namespace:        default
Priority:         0
Service Account:  default
Node:             cl1mdightd6q302tsooo-utaq/10.128.0.30
Start Time:       Thu, 17 Nov 2022 17:54:57 +0000
Labels:           app=reddit
                  component=mongodb
                  pod-template-hash=856884f4d4
                  release=reddit-test
Annotations:      cni.projectcalico.org/containerID: 16dc3515fe1866fe02091a83f0a4156008975b9fb8ff6ace49260cdbf72eb31f
                  cni.projectcalico.org/podIP: 10.112.129.16/32
                  cni.projectcalico.org/podIPs: 10.112.129.16/32
Status:           Pending
IP:               10.112.129.16
IPs:
  IP:           10.112.129.16
Controlled By:  ReplicaSet/reddit-test-mongodb-856884f4d4
Containers:
  mongodb:
    Container ID:
    Image:          mongodb:3.2
    Image ID:
    Port:           27017/TCP
    Host Port:      0/TCP
    State:          Waiting
      Reason:       ImagePullBackOff
    Ready:          False
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-ssdks (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             False
  ContainersReady   False
  PodScheduled      True
Volumes:
  kube-api-access-ssdks:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  2m48s                default-scheduler  Successfully assigned default/reddit-test-mongodb-856884f4d4-znzpb to cl1mdightd6q302tsooo-utaq
  Normal   Pulling    80s (x4 over 2m48s)  kubelet            Pulling image "mongodb:3.2"
  Warning  Failed     79s (x4 over 2m37s)  kubelet            Failed to pull image "mongodb:3.2": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/mongodb:3.2": failed to resolve reference "docker.io/library/mongodb:3.2": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
  Warning  Failed     79s (x4 over 2m37s)  kubelet            Error: ErrImagePull
  Warning  Failed     53s (x6 over 2m37s)  kubelet            Error: ImagePullBackOff
  Normal   BackOff    39s (x7 over 2m37s)  kubelet            Back-off pulling image "mongodb:3.2"

Ошибка в имени образа, исправляем, повторяем:

> helm dep update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "google" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 4 charts
Deleting outdated charts

> helm upgrade reddit-test reddit
Release "reddit-test" has been upgraded. Happy Helming!
NAME: reddit-test
LAST DEPLOYED: Thu Nov 17 18:00:50 2022
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None

> kubectl get pods
NAME                                   READY   STATUS    RESTARTS   AGE
reddit-test-comment-7986ffbdd4-lmpp5   1/1     Running   0          6m12s
reddit-test-mongodb-5d789f678-pvlzx    1/1     Running   0          17s
reddit-test-post-5fbbcb9499-fscwv      1/1     Running   0          6m12s
reddit-test-ui-7b8ccdd5b7-7l4mr        1/1     Running   0          6m12s
reddit-test-ui-7b8ccdd5b7-8f4jg        1/1     Running   0          6m12s
reddit-test-ui-7b8ccdd5b7-lrkng        1/1     Running   0          6m12s

По старой схеме (через Yandex Cloud) выясняем адрес балансировщика, проверяем:

> lynx -dump http://51.250.10.12:9292
   (BUTTON) [1]Microservices Reddit in default
   reddit-test-ui-7b8ccdd5b7-8f4jg container

   Can't show blog posts, some problems with the post service. [2]Refresh?

Menu

     * [3]All posts
     * [4]New post

References

   1. http://51.250.10.12:9292/
   2. http://51.250.10.12:9292/
   3. http://51.250.10.12:9292/
   4. http://51.250.10.12:9292/new

Мы не указали переменыне окружения для поиска хостов, исправим:

index 4e29a92..8d443ac 100644
--- a/kubernetes/Charts/ui/templates/deployment.yaml
+++ b/kubernetes/Charts/ui/templates/deployment.yaml
@@ -25,9 +25,18 @@ spec:                        # Deploy specification
       - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
         name: ui
         ports:
-          - containerPort: {{ .Values.service.internalPort }}
+        - containerPort: {{ .Values.service.internalPort }}
+          name: ui
         env:
+        - name: POST_SERVICE_HOST
+          value: {{  .Values.postHost | default (printf "%s-post" .Release.Name) }}
+        - name: POST_SERVICE_PORT
+          value: {{  .Values.postPort | default "5000" | quote }}
+        - name: COMMENT_SERVICE_HOST
+          value: {{  .Values.commentHost | default (printf "%s-comment" .Release.Name) }}
+        - name: COMMENT_SERVICE_PORT
+          value: {{  .Values.commentPort | default "9292" | quote }}
         - name: ENV
           valueFrom:
             fieldRef:
-              fieldPath: metadata.namespace
+              fieldPath: metadata.namespace

Обновим чарты и релиз:

> helm dep update ./reddit
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "google" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 4 charts
Deleting outdated charts

> helm upgrade reddit-test reddit
Release "reddit-test" has been upgraded. Happy Helming!
NAME: reddit-test
LAST DEPLOYED: Thu Nov 17 18:49:18 2022
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None

> lynx -dump http://51.250.75.182:9292
   (BUTTON) [1]Microservices Reddit in default
   reddit-test-ui-5f778f9b95-48pvf container

   (BUTTON)

7

   (BUTTON)

[2]1

17-11-2022
18:50

   [3]Go to the link

Menu

     * [4]All posts
     * [5]New post

References

   1. http://51.250.75.182:9292/
   2. http://51.250.75.182:9292/post/637682859fd337000f32f9e8
   3. http://test.com/
   4. http://51.250.75.182:9292/
   5. http://51.250.75.182:9292/new

Посты сохраняются, приложение работает.

Понятно, почему у нас не было внешнего адреса. Кое что забыли поставить:

> kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.5.1/deploy/static/provider/cloud/deploy.yaml

ud/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created

> kubectl get ingress reddit-test-ui
NAME             CLASS   HOSTS   ADDRESS          PORTS   AGE
reddit-test-ui   nginx   *       84.201.159.206   80      23m

> lynx -dump http://84.201.159.206
   (BUTTON) [1]Microservices Reddit in default
   reddit-test-ui-5f778f9b95-48pvf container

   (BUTTON)

7

   (BUTTON)

[2]1

17-11-2022
18:50

   [3]Go to the link

Menu

     * [4]All posts
     * [5]New post

References

   1. http://84.201.159.206/
   2. http://84.201.159.206/post/637682859fd337000f32f9e8
   3. http://test.com/
   4. http://84.201.159.206/
   5. http://84.201.159.206/new

Так то лучше)

Результат №10-1:

  • Подготовлены чарты для приложения reddit
  • Приложение запущено в кластере облака при помощи Helm

Задание №10-2:

  • GitLab + Kubernetes

Решение №10-2:

Первая задача - подготовить кластер для установки Gitlab, думаю пары нод с 4 CPU и 8 RAM хватит. Поднимаем кластер при помощи terraform, как делали в предыдущих занятиях.

> terraform apply
...
...
Plan: 3 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

yandex_kubernetes_cluster.yc_cluster: Creating...
yandex_kubernetes_cluster.yc_cluster: Still creating... [10s elapsed]
...
...
yandex_kubernetes_node_group.my_node_group[0]: Still creating... [2m20s elapsed]
yandex_kubernetes_node_group.my_node_group[1]: Creation complete after 2m26s [id=cathm2m36lmp45h2b7et]
yandex_kubernetes_node_group.my_node_group[0]: Still creating... [2m30s elapsed]
yandex_kubernetes_node_group.my_node_group[0]: Creation complete after 2m38s [id=cat3o1mdmt24aumc3ubt]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Регистрируем кластер локально:

> yc managed-kubernetes cluster get-credentials test-cluster --external --force

Context 'yc-test-cluster' was added as default to kubeconfig '/home/amur/.kube/config'.
Check connection to cluster using 'kubectl cluster-info --kubeconfig /home/amur/.kube/config'.

Note, that authentication depends on 'yc' and its config profile 'default'.
To access clusters using the Kubernetes API, please use Kubernetes Service Account.

> kubectl cluster-info
Kubernetes control plane is running at https://51.250.94.80
CoreDNS is running at https://51.250.94.80/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Gitlab будем ставить с помощью Helm Chart’а.

Добавим репозиторий Gitlab и скачаем чарт:

> helm repo remove bitnami
"bitnami" has been removed from your repositories

> helm repo add gitlab https://charts.gitlab.io/
"gitlab" has been added to your repositories

> helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "gitlab" chart repository
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "google" chart repository
Update Complete. ⎈Happy Helming!⎈

> helm pull gitlab/gitlab --untar

Вносим минимальные измененения в настройки чарта:

diff --git a/kubernetes/Charts/gitlab/values.yaml b/kubernetes/Charts/gitlab/values.yaml
index 448897b..73178af 100644
--- a/kubernetes/Charts/gitlab/values.yaml
+++ b/kubernetes/Charts/gitlab/values.yaml
@@ -36,7 +36,7 @@ global:
     labels: {}

   ## https://docs.gitlab.com/charts/installation/deployment#deploy-the-community-edition
-  edition: ee
+  edition: ce

   ## https://docs.gitlab.com/charts/charts/globals#gitlab-version
   # gitlabVersion:
@@ -664,7 +664,7 @@ global:
       certName: "tls.crt"

   ## Timezone for containers.
-  time_zone: UTC
+  time_zone: Europe/Moscow

   ## Global Service Annotations and Labels
   service:
@@ -805,10 +805,10 @@ upgradeCheck:
   priorityClassName: ""

 ## Settings to for the Let's Encrypt ACME Issuer
-# certmanager-issuer:
+certmanager-issuer:
 #   # The email address to register certificates requested from Let's Encrypt.
 #   # Required if using Let's Encrypt.
-#   email: email@example.com
+  email: ****@******.com

 ## Installation & configuration of jetstack/cert-manager
 ## See requirements.yaml for current version
@@ -884,7 +884,7 @@ nginx-ingress:
 ## Installation & configuration of stable/prometheus
 ## See requirements.yaml for current version
 prometheus:
-  install: true
+  install: false
   rbac:
     create: true
   alertmanager:

Ставим по официальной документации:

> helm install gitlab ./gitlab

NAME: gitlab
LAST DEPLOYED: Wed Nov 23 05:10:13 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
=== NOTICE
The minimum required version of PostgreSQL is now 12. See https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/doc/installation/upgrade.md for more details.

=== NOTICE
You've installed GitLab Runner without the ability to use 'docker in docker'.
The GitLab Runner chart (gitlab/gitlab-runner) is deployed without the `privileged` flag by default for security purposes. This can be changed by setting `gitlab-runner.runners.privileged` to `true`. Before doing so, please read the GitLab Runner chart's documentation on why we
chose not to enable this by default. See https://docs.gitlab.com/runner/install/kubernetes.html#running-docker-in-docker-containers-with-gitlab-runners
Help us improve the installation experience, let us know how we did with a 1 minute survey:https://gitlab.fra1.qualtrics.com/jfe/form/SV_6kVqZANThUQ1bZb?installation=helm&release=15-6

=== NOTICE
The in-chart NGINX Ingress Controller has the following requirements:
    - Kubernetes version must be 1.19 or newer.
    - Ingress objects must be in group/version `networking.k8s.io/v1`.

Ждём несколько минут, проверяем:

> kubectl get ingress
NAME                        CLASS          HOSTS                  ADDRESS          PORTS     AGE
gitlab-kas                  gitlab-nginx   kas.example.com        158.160.37.223   80, 443   2m15s
gitlab-minio                gitlab-nginx   minio.example.com      158.160.37.223   80, 443   2m15s
gitlab-registry             gitlab-nginx   registry.example.com   158.160.37.223   80, 443   2m15s
gitlab-webservice-default   gitlab-nginx   gitlab.example.com     158.160.37.223   80, 443   2m15s

Вносим изменения в чарт:

diff --git a/kubernetes/Charts/gitlab/values.yaml b/kubernetes/Charts/gitlab/values.yaml
index 73178af..5c425d2 100644
--- a/kubernetes/Charts/gitlab/values.yaml
+++ b/kubernetes/Charts/gitlab/values.yaml
@@ -48,10 +48,10 @@ global:
     allowClusterRoles: true
   ## https://docs.gitlab.com/charts/charts/globals#configure-host-settings
   hosts:
-    domain: example.com
+    domain: 158.160.37.223.sslip.io
     hostSuffix:
     https: true
-    externalIP:
+    externalIP: 158.160.37.223
     ssh: ~
     gitlab: {}
     minio: {}

Применяем изменения:

> helm upgrade gitlab ./gitlab
Release "gitlab" has been upgraded. Happy Helming!
NAME: gitlab
LAST DEPLOYED: Wed Nov 23 05:17:45 2022
NAMESPACE: default
STATUS: deployed
REVISION: 2
NOTES:
=== NOTICE
The minimum required version of PostgreSQL is now 12. See https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/doc/installation/upgrade.md for more details.

=== NOTICE
You've installed GitLab Runner without the ability to use 'docker in docker'.
The GitLab Runner chart (gitlab/gitlab-runner) is deployed without the `privileged` flag by default for security purposes. This can be changed by setting `gitlab-runner.runners.privileged` to `true`. Before doing so, please read the GitLab Runner chart's documentation on why we
chose not to enable this by default. See https://docs.gitlab.com/runner/install/kubernetes.html#running-docker-in-docker-containers-with-gitlab-runners

=== NOTICE
The in-chart NGINX Ingress Controller has the following requirements:
    - Kubernetes version must be 1.19 or newer.
    - Ingress objects must be in group/version `networking.k8s.io/v1`.

> kubectl get ingress
NAME                        CLASS          HOSTS                              ADDRESS          PORTS     AGE
gitlab-kas                  gitlab-nginx   kas.158.160.37.223.sslip.io        158.160.37.223   80, 443   7m53s
gitlab-minio                gitlab-nginx   minio.158.160.37.223.sslip.io      158.160.37.223   80, 443   7m53s
gitlab-registry             gitlab-nginx   registry.158.160.37.223.sslip.io   158.160.37.223   80, 443   7m53s
gitlab-webservice-default   gitlab-nginx   gitlab.158.160.37.223.sslip.io     158.160.37.223   80, 443   7m53s

Получаем первоначальный пароль:

> kubectl get secret gitlab-gitlab-initial-root-password -ojsonpath='{.data.password}' | base64 --decode ; echo
sGXV3JkYPtGmNrMB3jsvsx6pMzUMDnJM1Ruqdr9lB8vBGX9QDajrZ9GDHUHQrgV6

Заходим по адресу https://gitlab.158.160.37.223.sslip.io, логин root, пароль мы добыли выше.

Создаём публичную группу, в качестве имени выбираем логин пользователя, который выполянет работы.

В свойствах группы находим CI/CD, добавляем две переменные - CI_REGISTRY_USER - логин в DockerHub и CI_REGISTRY_PASSWORD - пароль от DockerHub.

В группе создаём четыре публичных проекта: reddit-deploy, comment, post, ui.

Локально создаём каталоги под каждый проект, в comment, post, ui переносим исходный код сервисов, пушим всё в Gitlab.

В reddit-deploy добавляем все чарты, которые мы создавали ранее и аналогично пушим всё в Gitlab.

В репозиторий ui добавим конфигурацию CI/CD, файл .gitlab-ci.yml:

image: alpine:latest

stages:
  - build
  - test
  - release
  - cleanup

build:
  stage: build
  image: docker:git
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - build
  variables:
    DOCKER_DRIVER: overlay2
  only:
    - branches

test:
  stage: test
  script:
    - exit 0
  only:
    - branches

release:
  stage: release
  image: docker
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - release
  variables:
    DOCKER_TLS_CERTDIR: ""
  only:
    - main

.auto_devops: &auto_devops |
  [[ "$TRACE" ]] && set -x
  export CI_REGISTRY="index.docker.io"
  export CI_APPLICATION_REPOSITORY=$CI_REGISTRY/$CI_PROJECT_PATH
  export CI_APPLICATION_TAG=$CI_COMMIT_REF_SLUG
  export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
  export TILLER_NAMESPACE="kube-system"

  function setup_docker() {
    if ! docker info &>/dev/null; then
      if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
        export DOCKER_HOST='tcp://localhost:2375'
      fi
    fi
  }

  function release() {

    echo "Updating docker images ..."

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    docker pull "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    docker tag "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    docker push "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    echo ""
  }

  function build() {

    echo "Building Dockerfile-based application..."
    echo `git show --format="%h" HEAD | head -1` > build_info.txt
    echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
    docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    echo "Pushing to GitLab Container Registry..."
    docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    echo ""
  }

before_script:
  - *auto_devops

Аналогичные файлы добавляем в comment и post.

При запуске пайплайна получили проблемы с докером, лечим:

> helm upgrade gitlab ./gitlab --set gitlab-runner.runners.privileged=true
Release "gitlab" has been upgraded. Happy Helming!
NAME: gitlab
LAST DEPLOYED: Wed Nov 23 18:20:51 2022
NAMESPACE: default
STATUS: deployed
REVISION: 4
NOTES:
=== NOTICE
The minimum required version of PostgreSQL is now 12. See https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/doc/installation/upgrade.md for more details.

=== NOTICE
The in-chart NGINX Ingress Controller has the following requirements:
    - Kubernetes version must be 1.19 or newer.
    - Ingress objects must be in group/version `networking.k8s.io/v1`.

Вносим изменения в ui:

diff --git a/ui/templates/ingress.yaml b/ui/templates/ingress.yaml
index 8e5ff84..e1a2af0 100644
--- a/ui/templates/ingress.yaml
+++ b/ui/templates/ingress.yaml
@@ -2,10 +2,13 @@ apiVersion: networking.k8s.io/v1
 kind: Ingress
 metadata:
   name: {{ template "ui.fullname" . }}
+  annotations:
+    kubernetes.io/ingress.class: {{ .Values.ingress.class }}
 spec:
   ingressClassName: {{ .Values.ingress.class }}
   rules:
-  - http:
+  - host: {{ .Values.ingress.host | default .Release.Name }}
+    http:
       paths:
       - path: /
         pathType: Prefix

Настройки CI/CD тоже меняем (с учётом настоящего времени):

image: alpine:latest

stages:
  - build
  - test
  - review
  - release

build:
  stage: build
  image: docker:git
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - build
  variables:
    DOCKER_DRIVER: overlay2
  only:
    - branches

test:
  stage: test
  script:
    - exit 0
  only:
    - branches

release:
  stage: release
  image: docker
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - release
  only:
    - main

review:
  stage: review
  script:
    - install_dependencies
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    - ensure_namespace
    - deploy
  variables:
    KUBE_NAMESPACE: review
    host: $CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    url: http://$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
  only:
    refs:
      - branches
  except:
    - main

.auto_devops: &auto_devops |
  [[ "$TRACE" ]] && set -x
  export CI_REGISTRY="index.docker.io"
  export CI_APPLICATION_REPOSITORY=$CI_REGISTRY/$CI_PROJECT_PATH
  export CI_APPLICATION_TAG=$CI_COMMIT_REF_SLUG
  export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
  export TILLER_NAMESPACE="kube-system"

  function deploy() {
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"

    if [[ "$track" != "stable" ]]; then
      name="$name-$track"
    fi

    echo "Clone deploy repository..."
    git clone $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/reddit-deploy.git

    echo "Download helm dependencies..."
    helm dep update reddit-deploy/reddit

    echo "Deploy helm release $name to $KUBE_NAMESPACE"
    helm upgrade --install \
      --wait \
      --set ui.ingress.host="$host" \
      --set $CI_PROJECT_NAME.image.tag=$CI_APPLICATION_TAG \
      --namespace="$KUBE_NAMESPACE" \
      --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
      "$name" \
      reddit-deploy/reddit/
  }

  function install_dependencies() {

    apk add -U openssl curl tar gzip bash ca-certificates git
    wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    apk add --force-overwrite glibc-2.35-r0.apk
    apk fix --force-overwrite alpine-baselayout-data
    rm glibc-2.35-r0.apk

    curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    export PATH=${PATH}:$HOME/gsutil

    curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx

    mv linux-amd64/helm /usr/bin/
    helm version --client

    curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    chmod a+x /usr/bin/sync-repo.sh

    curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    chmod +x /usr/bin/kubectl
    kubectl version --client
  }

  function setup_docker() {
    if ! docker info &>/dev/null; then
      if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
        export DOCKER_HOST='tcp://localhost:2375'
      fi
    fi
  }

  function ensure_namespace() {
    kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  }

  function release() {

    echo "Updating docker images ..."

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    docker pull "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    docker tag "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    docker push "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    echo ""
  }

  function build() {

    echo "Building Dockerfile-based application..."
    echo `git show --format="%h" HEAD | head -1` > build_info.txt
    echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
    docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    echo "Pushing to GitLab Container Registry..."
    docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    echo ""
  }

before_script:
  - *auto_devops

Пайплайн ломается на функции ensure_namespace шага review, судя по всему раннеру не хватает прав.

Чтобы работать с Kubernetes, нужно настроить Gitlab. Процедура описана тут. Установка самого агента описана в этом разделе.

Конфигурация агента должна лежать в основной ветке репозитория, полный путь: .gitlab/agents/<agent-name>/config.yaml. Авторизуем агента для проектов нашей группы.

ci_access:
  groups:
    - id: r2d2k

Пушим изменения в Gitlab.

Идём в проект, в котором создан файл конфигурации, Infastucture/Kubernetes cluster, Connect a cluster, выбираем имя агента и жмём Register. Gitlab генерирует команды для установки и подключения агента:

> helm repo add gitlab https://charts.gitlab.io
> helm repo update
> helm upgrade --install reddit-agent gitlab/gitlab-agent \
    --namespace gitlab-agent \
    --create-namespace \
    --set image.tag=v15.6.0 \
    --set config.token=szv2u9_uHzy85zpdoTtdP9u15QKf93m2jui18DMDJbfDBfzL2w \
    --set config.kasAddress=wss://kas.158.160.37.223.sslip.io

Release "reddit-agent" does not exist. Installing it now.
NAME: reddit-agent
LAST DEPLOYED: Thu Nov 24 16:56:54 2022
NAMESPACE: gitlab-agent
STATUS: deployed
REVISION: 1
TEST SUITE: None

Через некоторое время агент выйдет на связь.

После нескольких часов правок багов приходим к такому рабочему конфигу:

image: alpine:latest

stages:
  - build
  - test
  - review
  - release
  - cleanup

build:
  stage: build
  image: docker:git
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - build
  variables:
    DOCKER_DRIVER: overlay2
  only:
    - branches

test:
  stage: test
  script:
    - exit 0
  only:
    - branches

release:
  stage: release
  image: docker
  services:
    - docker:18.09.7-dind
  script:
    - setup_docker
    - release
  only:
    - main

review:
  stage: review
  script:
    - install_dependencies
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    - ensure_namespace
    - deploy
  variables:
    KUBE_NAMESPACE: review
    host: $CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    url: http://$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
    on_stop: stop_review
  only:
    refs:
      - branches
  except:
    - main

stop_review:
  stage: cleanup
  variables:
    GIT_STRATEGY: none
    KUBE_NAMESPACE: review
  script:
    - install_dependencies
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    - delete
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    action: stop
  when: manual
  allow_failure: true
  only:
    refs:
      - branches
  except:
    - main

.auto_devops: &auto_devops |
  [[ "$TRACE" ]] && set -x
  export CI_REGISTRY="index.docker.io"
  export CI_APPLICATION_REPOSITORY=$CI_REGISTRY/$CI_PROJECT_PATH
  export CI_APPLICATION_TAG=$CI_COMMIT_REF_SLUG
  export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
  export TILLER_NAMESPACE="kube-system"

  function delete() {
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"
    helm delete "$name" --namespace="$KUBE_NAMESPACE" || true
  }

  function deploy() {
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"

    if [[ "$track" != "stable" ]]; then
      name="$name-$track"
    fi

    echo "Clone deploy repository..."
    git clone $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/reddit-deploy.git

    echo "Download helm dependencies..."
    helm dep update reddit-deploy/reddit

    echo "Deploy helm release $name to $KUBE_NAMESPACE"
    helm upgrade --install \
      --wait \
      --set ui.ingress.host="$host" \
      --set $CI_PROJECT_NAME.image.tag=$CI_APPLICATION_TAG \
      --namespace="$KUBE_NAMESPACE" \
      --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
      "$name" \
      reddit-deploy/reddit/
  }

  function install_dependencies() {

    apk add -U openssl curl tar gzip bash ca-certificates git
    wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    apk add --force-overwrite glibc-2.35-r0.apk
    apk fix --force-overwrite alpine-baselayout-data
    rm glibc-2.35-r0.apk

    curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    export PATH=${PATH}:$HOME/gsutil

    curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx

    mv linux-amd64/helm /usr/bin/
    helm version --client

    curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    chmod a+x /usr/bin/sync-repo.sh

    curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    chmod +x /usr/bin/kubectl
    kubectl version --client
  }

  function setup_docker() {
    if ! docker info &>/dev/null; then
      if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
        export DOCKER_HOST='tcp://localhost:2375'
      fi
    fi
  }

  function ensure_namespace() {
    kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  }

  function release() {

    echo "Updating docker images ..."

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    docker pull "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    docker tag "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    docker push "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    echo ""
  }

  function build() {

    echo "Building Dockerfile-based application..."
    echo `git show --format="%h" HEAD | head -1` > build_info.txt
    echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
    docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .

    if [[ -n "$CI_REGISTRY_USER" ]]; then
      echo "Logging to GitLab Container Registry with CI credentials..."
      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      echo ""
    fi

    echo "Pushing to GitLab Container Registry..."
    docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    echo ""
  }

before_script:
  - *auto_devops

Копируем этот конфиг в post и comment, проверяем работу. Для этого создаём ветку, что-то меняем в ней, пушим. Должны отработать пайплайны с review. Исходное состояние:

> helm ls --namespace review
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION

После изменений в ветках post:

> helm ls --namespace review
NAME                            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
review-r2d2k-post-vtboig        review          1               2022-11-24 19:37:05.806289118 +0000 UTC deployed        reddit-0.1.0    1

После ручной остановки пайплайна в ветках post:

> helm ls --namespace review
NAME    NAMESPACE       REVISION        UPDATED STATUS  CHART   APP VERSION

Для создания staging и production среды используем следующий конфиг CI\CD для репозитория reddit-deploy:

image: alpine:latest

stages:
  - test
  - staging
  - production

test:
  stage: test
  script:
    - exit 0
  only:
    - triggers
    - branches

staging:
  stage: staging
  script:
  - install_dependencies
  - kubectl config get-contexts
  - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
  - kubectl get pods --namespace "$KUBE_NAMESPACE"
  - ensure_namespace
  - deploy
  variables:
    KUBE_NAMESPACE: staging
  environment:
    name: staging
    url: http://staging
  only:
    refs:
      - main

production:
  stage: production
  script:
    - install_dependencies
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods --namespace "$KUBE_NAMESPACE"
    - ensure_namespace
    - deploy
  variables:
    KUBE_NAMESPACE: production
  environment:
    name: production
    url: http://production
  when: manual
  only:
    refs:
      - main

.auto_devops: &auto_devops |
  # Auto DevOps variables and functions
  [[ "$TRACE" ]] && set -x
  export CI_REGISTRY="index.docker.io"
  export CI_APPLICATION_REPOSITORY=$CI_REGISTRY/$CI_PROJECT_PATH
  export CI_APPLICATION_TAG=$CI_COMMIT_REF_SLUG
  export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
  export TILLER_NAMESPACE="kube-system"

  function deploy() {
    echo $KUBE_NAMESPACE
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"
    helm dep build reddit

    helm upgrade --install \
      --debug \
      --wait \
      --set ui.ingress.host="$host" \
      --set ui.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/ui/-/raw/main/VERSION)" \
      --set post.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/post/-/raw/main/VERSION)" \
      --set comment.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/comment/-/raw/main/VERSION)" \
      --namespace="$KUBE_NAMESPACE" \
      --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
      "$name" \
      reddit
  }

  function install_dependencies() {

    apk add -U openssl curl tar gzip bash ca-certificates git
    wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    apk add --force-overwrite glibc-2.35-r0.apk
    apk fix --force-overwrite alpine-baselayout-data
    rm glibc-2.35-r0.apk

    curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    export PATH=${PATH}:$HOME/gsutil

    curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx

    mv linux-amd64/helm /usr/bin/
    helm version --client

    curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    chmod a+x /usr/bin/sync-repo.sh

    curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    chmod +x /usr/bin/kubectl
    kubectl version --client
  }

  function ensure_namespace() {
    kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  }

  function delete() {
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"
    helm delete "$name" --namespace="$KUBE_NAMESPACE" || true
  }

before_script:
  - *auto_devops

Этот конфиг отличается от предыдущего тем, что:

  • Не собирает docker-образы
  • Деплоит на статичные окружения (staging и production)
  • Не удаляет окружения

После успешного завершения staging прописываем в host соответствие имени среды и IP, который можно увидеть у облачного балансировщика нагрузки. Проверяем - приложение работает. Т.к. количество внешних балансировщиков ограничено, то для запуска production нужно удалить балансировщик staging. Удаляем, запускаем этап production руками из интерфейса Gitlab, смотрим адрес балансировщика, прописываем в hosts - работает.

Можно избавиться от AutoDevOps, перенеся все процедуры в соотвествующте шаги. Для ui получим следущий вариант:

image: alpine:latest

stages:
  - build
  - test
  - review
  - release
  - cleanup

build:
  stage: build
  only:
    - branches
  image: docker:git
  services:
    - docker:18.09.7-dind
  variables:
    DOCKER_DRIVER: overlay2
    CI_REGISTRY: 'index.docker.io'
    CI_APPLICATION_REPOSITORY: $CI_REGISTRY/$CI_PROJECT_PATH
    CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG
    CI_CONTAINER_NAME: ci_job_build_${CI_JOB_ID}
  before_script:
    - >
      if ! docker info &>/dev/null; then
        if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
          export DOCKER_HOST='tcp://localhost:2375'
        fi
      fi
  script:
    # Building
    - echo "Building Dockerfile-based application..."
    - echo `git show --format="%h" HEAD | head -1` > build_info.txt
    - echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
    - docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .
    - >
      if [[ -n "$CI_REGISTRY_USER" ]]; then
        echo "Logging to GitLab Container Registry with CI credentials...for build"
        docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      fi
    - echo "Pushing to GitLab Container Registry..."
    - docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"

test:
  stage: test
  script:
    - exit 0
  only:
    - branches

release:
  stage: release
  image: docker
  services:
    - docker:18.09.7-dind
  variables:
    CI_REGISTRY: 'index.docker.io'
    CI_APPLICATION_REPOSITORY: $CI_REGISTRY/$CI_PROJECT_PATH
    CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG
    CI_CONTAINER_NAME: ci_job_build_${CI_JOB_ID}
  before_script:
    - >
      if ! docker info &>/dev/null; then
        if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
          export DOCKER_HOST='tcp://localhost:2375'
        fi
      fi
  script:
    # Releasing
    - echo "Updating docker images ..."
    - >
      if [[ -n "$CI_REGISTRY_USER" ]]; then
        echo "Logging to GitLab Container Registry with CI credentials for release..."
        docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
      fi
    - docker pull "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
    - docker tag "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    - docker push "$CI_APPLICATION_REPOSITORY:$(cat VERSION)"
    # latest is neede for feature flags
    - docker tag "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" "$CI_APPLICATION_REPOSITORY:latest"
    - docker push "$CI_APPLICATION_REPOSITORY:latest"
  only:
    - main

review:
  stage: review
  variables:
    KUBE_NAMESPACE: review
    host: $CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
    CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG
    name: $CI_ENVIRONMENT_SLUG
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    url: http://$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
    on_stop: stop_review
  only:
    refs:
      - branches
    kubernetes: active
  except:
    - main
  before_script:
    # installing dependencies
    - apk add -U openssl curl tar gzip bash ca-certificates git
    - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    - apk add --force-overwrite glibc-2.35-r0.apk
    - apk fix --force-overwrite alpine-baselayout-data
    - rm glibc-2.35-r0.apk
    - curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    - export PATH=${PATH}:$HOME/gsutil
    - curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx
    - mv linux-amd64/helm /usr/bin/
    - helm version --client
    - curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    - chmod a+x /usr/bin/sync-repo.sh
    - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x /usr/bin/kubectl
    - kubectl version --client
    # Set context
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    # ensuring namespace
    - kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  script:
    - export track="${1-stable}"
    - >
      if [[ "$track" != "stable" ]]; then
        name="$name-$track"
      fi
    - echo "Clone deploy repository..."
    - git clone $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/reddit-deploy.git
    - echo "Download helm dependencies..."
    - helm dep update reddit-deploy/reddit
    - echo "Deploy helm release $name to $KUBE_NAMESPACE"
    - echo "Upgrading existing release..."
    - >
      helm upgrade \
        --install \
        --wait \
        --set ui.ingress.host="$host" \
        --set $CI_PROJECT_NAME.image.tag="$CI_APPLICATION_TAG" \
        --namespace="$KUBE_NAMESPACE" \
        --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
        "$name" \
        reddit-deploy/reddit/

stop_review:
  stage: cleanup
  variables:
    KUBE_NAMESPACE: review
    GIT_STRATEGY: none
    name: $CI_ENVIRONMENT_SLUG
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    action: stop
  when: manual
  allow_failure: true
  only:
    refs:
      - branches
    kubernetes: active
  except:
    - main
  before_script:
    # installing dependencies
    - apk add -U openssl curl tar gzip bash ca-certificates git
    - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    - apk add --force-overwrite glibc-2.35-r0.apk
    - apk fix --force-overwrite alpine-baselayout-data
    - rm glibc-2.35-r0.apk
    - curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    - export PATH=${PATH}:$HOME/gsutil
    - curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx
    - mv linux-amd64/helm /usr/bin/
    - helm version --client
    - curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    - chmod a+x /usr/bin/sync-repo.sh
    - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x /usr/bin/kubectl
    - kubectl version --client
    # Set context
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
  script:
    - helm delete "$name" --namespace="$KUBE_NAMESPACE" || true

Аналогично меняем конфиги для post и comment.

Так как мы изначально использовали helm3, то нужда в переделке отпадает.

Для reddit-deploy также избавимся от AutoDevOps:

image: alpine:latest

stages:
  - test
  - staging
  - production

test:
  stage: test
  script:
    - exit 0
  only:
    - triggers
    - branches

staging:
  stage: staging
  variables:
    KUBE_NAMESPACE: staging
    CI_REGISTRY: "index.docker.io"
    CI_APPLICATION_REPOSITORY: $CI_REGISTRY/$CI_PROJECT_PATH
    CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG
    CI_CONTAINER_NAME: ci_job_build_${CI_JOB_ID}
  environment:
    name: staging
    url: http://staging
  only:
    refs:
      - main
  before_script:
    # installing dependencies
    - apk add -U openssl curl tar gzip bash ca-certificates git
    - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    - apk add --force-overwrite glibc-2.35-r0.apk
    - apk fix --force-overwrite alpine-baselayout-data
    - rm glibc-2.35-r0.apk
    - curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    - export PATH=${PATH}:$HOME/gsutil
    - curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx
    - mv linux-amd64/helm /usr/bin/
    - helm version --client
    - curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    - chmod a+x /usr/bin/sync-repo.sh
    - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x /usr/bin/kubectl
    - kubectl version --client
    # Set context
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    # ensuring namespace
    - kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  script:
    - export track="${1-stable}"
    - export name="$CI_ENVIRONMENT_SLUG"
    - echo "Release name - $name"
    - helm dep build reddit
    - >
       helm upgrade --install \
        --debug \
        --wait \
        --set ui.ingress.host="$host" \
        --set ui.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/ui/-/raw/main/VERSION)" \
        --set post.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/post/-/raw/main/VERSION)" \
        --set comment.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/comment/-/raw/main/VERSION)" \
        --namespace="$KUBE_NAMESPACE" \
        --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
        "$name" \
        reddit

production:
  stage: production
  variables:
    KUBE_NAMESPACE: production
    CI_REGISTRY: "index.docker.io"
    CI_APPLICATION_REPOSITORY: $CI_REGISTRY/$CI_PROJECT_PATH
    CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG
    CI_CONTAINER_NAME: ci_job_build_${CI_JOB_ID}
  environment:
    name: production
    url: http://production
  when: manual
  only:
    refs:
      - main
  before_script:
    # installing dependencies
    - apk add -U openssl curl tar gzip bash ca-certificates git
    - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
    - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk
    - apk add --force-overwrite glibc-2.35-r0.apk
    - apk fix --force-overwrite alpine-baselayout-data
    - rm glibc-2.35-r0.apk
    - curl https://storage.googleapis.com/pub/gsutil.tar.gz | tar -xz -C $HOME
    - export PATH=${PATH}:$HOME/gsutil
    - curl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz | tar zx
    - mv linux-amd64/helm /usr/bin/
    - helm version --client
    - curl  -o /usr/bin/sync-repo.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/sync-repo.sh
    - chmod a+x /usr/bin/sync-repo.sh
    - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x /usr/bin/kubectl
    - kubectl version --client
    # Set context
    - kubectl config get-contexts
    - kubectl config use-context r2d2k/reddit-deploy:reddit-agent
    - kubectl get pods
    # ensuring namespace
    - kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
  script:
    - export track="${1-stable}"
    - export name="$CI_ENVIRONMENT_SLUG"
    - echo "Release name - $name"
    - helm dep build reddit
    - >
       helm upgrade --install \
        --debug \
        --wait \
        --set ui.ingress.host="$host" \
        --set ui.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/ui/-/raw/main/VERSION)" \
        --set post.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/post/-/raw/main/VERSION)" \
        --set comment.image.tag="$(curl $CI_SERVER_URL/$CI_PROJECT_NAMESPACE/comment/-/raw/main/VERSION)" \
        --namespace="$KUBE_NAMESPACE" \
        --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
        "$name" \
        reddit

Для автоматического запуска деплоя после сборки образов можно использовать триггеры. Добавим следующий блок в конфигурацию CI сервисов:

deploy-app:
  stage: deploy-app
  trigger:
    project: r2d2k/reddit-deploy
    branch: main
  only:
    - main

Результат №10-2:

  • При помощи Terraform поднят кластер в Yandex Cloud
  • При помощи helm3 раззвёрнуто тестовое приложение
  • В кластере запущен Gitlab
  • Настроены процессы сборки и деплоя приложения в различные среды

FIN