Непрерывная интеграция и развертывание Docker в GitLab CI

В этом руководстве рассмотрим вариант настройки непрерывной интеграции и развертывания Flask приложения на Docker Swarm через GitLab CI.

Сначала мы рассмотрим настройку рабочей среды, включая создание серверов для нодов Docker Swarm. Затем создадим простое приложение Flask с Redis и подготовим GitLab CI для непрерывной доставки.

Управление контейнерами и управление нагрузкой — нетривиальная тема и требует значительной подготовки, особенно если планируется самостоятельная настройка системы оркестрации (например, с помощью Kubernetes). Однако, существуют такие инструменты как Docker Swarm или Rancher, которые берут на себя управление контейнерами, внутренними сетями и распределение нагрузок и дают возможность развернуть масштабируемую систему на собственных серверах.

Кроме того, GitLab хорошо поддерживает Docker и позволяет подключать собственные системы хранения образов (GitLab Registry) в несколько простых шагов. А также отслеживать статус выполнения работ по проекту и управлять развернутыми версиями приложения, позволяя при необходимости откатиться на прошлую в один клик.

Предварительные требования


Перед началом необходимо удостовериться в выполнении следующих условий:

  • установлен и доступен сервер с GitLab;
  • GitLab защищен с помощью SSL-сертификата;
  • в GitLab добавлен SSH-ключ.

Официальное руководство по настройке HTTPS в GitLab.

Установка серверов


Для настройки и запуска приложения в режиме Swarm нам понадобится 2 типа серверов — менеджер и подчиненный. Кроме того, дополнительно один сервер будет выделен под GitLab Runner для выполнения задачи сборки и запуска контейнеров.

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

Создадим три новых сервера из образа Docker:

  • Runner — для выполнения работ GitLab Runner;
  • Manager — управляющий нод Docker Swarm;
  • Node1 — подчиненный нод Docker Swarm.

Созданные виртуальные машины
Полученные IP-адреса мы будем использовать далее.

Защита сервиса докера на управляющем сервере самоподписанными сертификатом


Примечание. Использование TLS и управление центром сертификации — тема, требующая значительной подготовки. Желательно ознакомиться с технологиями OpenSSL, x509 и TLS перед использованием их в реальных проектах.

На финальном этапе развертывания приложения в рабочей Swarm-среде необходимо защищенное соединение между GitLab Runner и сервисом Docker, запущенном на сервере Manager, что видно на схеме ниже:

Диаграмма серверов
Процесс развертывания приложения в рабочей среде.

Для этого необходимо использовать единый сертификационный центр для клиента (GitLab Runner) и сервиса Docker (Manager). После создания сертификата и ключа для клиента, их можно использовать для удаленного подключения к сервису Docker и выполнения различных операций. Следует быть максимально осторожными с хранением клиентских сертификатов и ключей, поскольку они дают полное управление надо сервисом Docker.

Обновление сервиса Docker на машине Manager


Подключимся по SSH к серверу Manager и обновим Docker, поскольку в дальнейшем нам потребуются дополнительные возможности, доступные в старшей версии Docker API. Добавим репозиторий от разработчиков Docker для получения последней версии:

$ apt-get update
$ apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
$ add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ apt-get update

Установим последнюю версию Docker:

$ apt-get install docker-ce

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

$ docker version

Вывод команды может отличаться, главное, чтобы обе версии API клиента и сервера были больше 1.24:

Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:42:18 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.09.0-ce
 API version:  1.32 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:56 2017
 OS/Arch:      linux/amd64
 Experimental: false

Создание сертификата и ключей


Оставаясь в сервере Manager, приступим к созданию сертификационного центра (CA). Перейдем в новую директорию:

$ mkdir certificates
$ cd certificates

Для начала нужно создать приватный и публичный RSA-ключ для CA (потребуется придумать кодовое слово длиной не меньше 4 символов):

$ openssl genrsa -aes256 -out ca-key.pem 4096

Generating RSA private key, 4096 bit long modulus
...............................................................................................................................................................................................................................++
..................................................++
e is 65537 (0x10001)
Enter pass phrase for ca-key.pem:
Verifying - Enter pass phrase for ca-key.pem:

Приступим к созданию локального сертификационного центра. Будут запрошены данные связанные с идентификацией центра. На этапе ввода полного квалифицированного доменного имени (FQDN) требуется ввести доменное имя хоста, по которому доступен сервер Manager, но в целях примера (не используйте подобный метод в рабочих машинах!) используем слово manager для обозначения сервера:

$ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem

Enter pass phrase for ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:manager
Email Address []:your@email.com

Далее создадим приватный ключ для сервера:

$ openssl genrsa -out server-key.pem 4096

Теперь у нас есть сертификационный центр и мы можем создать запрос на подпись SSL сертификата (CSR). Поле CN (Common Name) должно совпадать с использованным на предыдущем шаге значением FQDN:

$ openssl req -subj "/CN=manager" -sha256 -new -key server-key.pem -out server.csr

Дополнительно требуется указать IP-адрес сервера Manager (машина, в которой мы сейчас работаем):

 $ echo subjectAltName = DNS:manager,IP:{ваш IP-адрес} >> extfile.cnf

Создадим подписанный ключ для сервера:

$ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf

Создадим клиентский ключ, который будем использовать для доступа к сервису Docker:

$ openssl genrsa -out key.pem 4096

Создадим запрос на подпись и дополнительно укажем тип использования ключа — для авторизации:

$ openssl req -subj '/CN=client' -new -key key.pem -out client.csr
$ echo extendedKeyUsage = clientAuth >> extfile.cnf

Получим подписанный клиентский ключ:

$ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out cert.pem -extfile extfile.cnf

Теперь можно удалить файлы запросов:

$ rm -v client.csr server.csr

В итоге, мы получили следующие файлы:

$ ls
ca-key.pem  ca.srl    extfile.cnf  server-cert.pem
ca.pem      cert.pem  key.pem      server-key.pem

Оставим терминал с сессией на сервере Manager открытым, поскольку он понадобится нам далее.

Сейчас у нас есть всё необходимое для настройки защищенного доступа между GitLab Runner и рабочей Swarm-средой.

Настройка секретных переменных в GitLab CI


Мы не будем хранить данные клиентских ключей на машине Runner из соображений безопасности. Для таких задач в GitLab CI реализована функция секретных переменных среды.
Создадим новый проект в GitLab:

Новый проект в GitLab

После создания проекта перейдем в настройки CI / CD:

CI / CD

Откроем область секретных переменных (Secret variables) и будем работать с ней:

Secret Variables

Нам необходимо добавить и сохранить три переменные со следующими названиями и значениями файлов, которые мы создали на предыдущем шаге:

  • переменная TLSCACERT значение файла ca.pem;
  • переменная TLSCERT значение файла cert.pem;
  • переменная TLSKEY значение файла key.pem.

Вернёмся к терминалу с сессией на сервере Manager и выполним команду:

$ cat ca.pem
(Сокращенный вывод)
-----BEGIN CERTIFICATE-----
MIIFgTCCA2mgAwIBAgIJAMzFvrYTSMoxMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
...
bI9XGs39F+r8Si5y6oHqkZHMpRX631i2KRA6k4jBPrZrS0MH3OwsCobuat5T1ONH
Kx7TFZSuFO25XIut1WucVn5yPWLTKRniMV7dVws9i9x9Sp2Iamk+w2x1GPO6bHtr
BWqdORkUEWMs+DTgX2J989AFh7gnYwHZ2Bo7HKlC6IbOlol7b2E/5p7hWrpe7sf+
oQDn1bhgoauhq2AL4BysJfA3uHoA
-----END CERTIFICATE-----

Скопируем это значение и добавим новую секретную переменную в GitLab:

image

Теперь по этому примеру добавим оставшиеся значения cert.pem и key.pem.

Настройка сервиса Docker на управляющем сервере Manager


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

Мы создадим отдельный конфигурационный файл явно прописывающий хосты, по которым будет доступен Docker, поэтому для начала нам нужно удалить стандартный параметр -H, прописывающий хост. Для этого создадим новую директорию docker.service.d, в которой переопределим параметры запуска сервиса:

$ mkdir -p /etc/systemd/system/docker.service.d

Создадим файл настройки:

$ nano /etc/systemd/system/docker.service.d/exec-start.conf

Добавим следующую секцию, для параметра ExecStart требуется сначала очистить предыдущие значения и затем указать новые:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

Создадим новый конфигурационный файл:

$ nano  /etc/docker/daemon.json

И запишем следующий текст, определяющим использование протокола TLS для доступа к сервису Docker, а также расположение серверного ключа и сертификата:

{
  "hosts": ["tcp://0.0.0.0:2376","fd://"],
  "tlsverify": true,
  "tlscacert": "/root/certificates/ca.pem",
  "tlscert": "/root/certificates/server-cert.pem",
  "tlskey": "/root/certificates/server-key.pem"
}

Для вступления изменений в силу, перезапустим сервис Docker:

$ systemctl daemon-reload
$ service docker restart

Теперь мы можем подключиться по TLS к нашему сервису Docker по адресу

[IP адрес сервера]:2376

Активация режима Swarm


Для начала рекомендуем изучить материалы на тему режима Swarm, например, на официальном сайте Docker. Swarm — это кластер сервисов Docker, расположенных на различных физических или виртуальных машинах и ведущих себя как единое целое.
Распределение запросов между имеющимися сервисами Docker осуществляется по схеме ingress load balancing, суть которой в том, что любой запрос проходит через внутренний механизм балансировки, а затем перенаправляется на тот сервис, который в данный момент может обслужить запрос.
Масштабирование осуществляется за счет указания количества реплик внутренних сервисов, с которыми мы столкнемся позднее.
Мы активируем режим Docker Swarm на сервере Manager, на котором будет располагаться менеджер этого кластера. Затем мы добавим подчиненный сервис Docker с машины Node1.

В терминале с открытой сессией на сервере Manager выполним команду:

$ docker swarm init
Swarm initialized: current node (r1mbxr2dyuf48zpm5ss0kvwv7) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-5ihkl37kbs13po7htnj9dzzg3gex4i6iuvjho7910crd0hv895-36jw5epwcw3xwpzmqf1mqgod2 {ваш IP-адрес}:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Как видно из сообщения, текущий сервис Docker стал менеджером и готов к добавлению подчиненных хостов через выполнение указанной команды. Скопируем эту команду и подключимся к серверу Node1 по SSH, для добавления его добавления в Swarm:

$ docker swarm join --token SWMTKN-1-1lhmuomvb060rnom4jqj8gxc565f4wgwadjs9ucvqx6huwvbfc-6vt1ljdhldxtetjv2hnct7sh4  {ваш IP-адрес}:2377

Результатом успешного выполнения команды должно стать сообщение:

This node joined a swarm as a worker.

Следующем шагом станет настройка рабочего сервера, который будет выполнять все работы от GitLab CI.

Настройка Gitlab Runner


Финальным этапом настройки среды непрерывной интеграции и развертывания с Docker является подключение рабочего сервиса GitLab CI, на котором будут выполняться все работы по сборке и тестированию приложения.
Можно использовать совместные сервисы выполнения работ, но в данном руководстве рассмотрим создание собственного сервиса на созданном ранее сервере Runner.

Подключимся по SSH к серверу Runner. Сперва необходимо установить GitLab Runner и соединить этот сервер с GitLab.
Добавим репозиторий разработчиков GitLab:

$ apt update
$ apt install curl 
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash
$ apt-get install gitlab-runner

Вернемся к веб-интерфейсу GitLab для получения URL и registration-token. Снова зайдем в настройки проекта в раздел CI / CD, где мы остановились в прошлый раз. Откроем секцию Runner settings:

Runner Settings
В разделе Runner settings есть информация по подключенным рабочим хостам: приватным и открытым:

Настройки Runner

В блоке Specific Runners есть необходимые значения URL и registration token.

Вернемся в терминал с сессией на сервере Runner и заменив значения на свои выполним команду:

$ gitlab-runner register -n \
  --url http:// {ваш IP-адрес}/ \
  --registration-token _Kof1SxCHzVNcwuZZEwx \
  --executor docker \
  --description "Docker Prod Runner" \
  --docker-image "docker:latest" \
  --docker-privileged \
  --tag-list docker

Registering runner... succeeded runner=_Kof1SxC
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 

Теперь у нас зарегистрирован рабочий хост, который будет выполнять все работы, посылаемые GitLab CI.
Осталось настроить SSH доступ для Git-репозитория проекта на GitLab.
Создадим приватный и публичный ключи, заменив свой email-адрес и оставив все запрашиваемые значения по-умолчанию:

$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Далее нужно добавить информацию о сервере GitLab, на котором расположен Git-репозиторий в список известных хостов для предотвращения ошибок во время подключения (IP адрес нужно заменить на IP адрес сервера GitLab, доступный в панели управления):

$ ssh-keyscan -t rsa  {ваш IP-адрес} >> .ssh/known_hosts

Затем необходимо добавить публичный ключ в GitLab, чтобы разрешить подключение с сервера Runner. Скопируем значение ключа:

$ cat .ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMY+G6rmx+AZ6Ow1lVr+4ox0HaAaV4xwthCS3ucyI3KsXVV+ltLU0zkFOP8WZoTXCHo38Fpcu5KwYe3V6L/hZ26fNse6WhJ6EvRmVx7wVHyixhpzKj6Jp9zzAf24SbtrjGgNtN4ASKyouU///a3+gtM+eWQYdxavz6wlJ0xgm8MnDqpbCUv1M7IRWsKhejA9vLSXjfdkxQnxVSCOT/FXb/eDsGRTs7WMXYapqeQ0msTXDCFlNDVQaRWZagXpHLRDkeOhRE2rJ6daj8YNKKx0jaatRKIsICqwUljvPgsrnpF9FiUg8n8PTWyYbz3VpwUoIPnFiFXvbgIn8xLb2/4QkFDoZUgyLI+VgrmZmd0HZPWvW5QbMLZ8vwb/Izi0TG/+qoMm8jas0RaUUp18rQAc4GmCLRsFbzN3DsnME31xFa0y/pwA3LK9ptIRivYq82uP5twq0jXpMSji8w+No7kBI5O9VUHmbRYYYWpn+jeKTxmoVORsrCHpAT7Cub0+Ynyq1M7Em0RMqZgdzLsP9rlLwRkc6ZEgqpVQHDZgwJsnQ5qo/6lr18bD9QHSe5t+SSnUbnkmXkp0xb0ivC4XayxCjYVIOoZV2cqyGa+45s7LY+ngPk0Cg+vSMHV8/enEwu1ABdpoGVjaELJOtw1UBr4y9GCyQ0OhKnrzWmqL6+HnEMDQ== your_email@example.com

И добавим его в GitLab:

Добавление ключа в GitLab

Перейдем к созданию и докеризации простого Flask приложения, использующего два дополнительных сервиса: Nginx для маршрутизации запросов и Redis для хранения счетчика посещений страницы.

Подключение Docker Registry в GitLab


Перед созданием приложения нам необходимо активировать функцию хранения образов Docker в нашем приложении GitLab для эффективного управления развернутыми версиями приложения и обеспечения возможности отката на предыдущие версии.

Подключимся по SSH к серверу GitLab и откроем конфигурационный файл:

$ nano /etc/gitlab/gitlab.rb

Далее добавим с новой строки адрес, по которому будет доступно хранилище Docker, заменив example.com на имя своего хоста:

registry_external_url 'https://gitlab.example.com:4567'

Добавим ещё две строки с указанием места хранения сертификата и ключа. Поскольку мы используем такое же доменное имя, что и для основного приложения GitLab (если вы ещё не настроили HTTPS для GitLab, можно это сделать сейчас):

registry_nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.example.com/fullchain.pem"
registry_nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.example.com/privkey.pem"

Перезагрузим сервис GitLab для активации хранилища:

$ gitlab-ctl reconfigure

Перейдем к созданному ранее проекту flask-docker-swarm в GitLab. В случае успешной активации хранилища, в меню проекта станет доступен раздел Registry:

Проверка Registry

Для дальнейшей работы с внутренним хранилищем образов нам понадобится использовать связку логин-пароль для подключения к нему. Используем возможность добавления секретных переменных в GitLab для добавления пароля. Для этого перейдем в раздел Settings и выберем блок CI / CD:

Добавление секретных переменных

Раскроем раздел Runner settings и добавим новую переменную HUB_REGISTRY_PASSWORD, значением которой является пароль от учетной записи пользователя GitLab:

Настройка пароля

Создание приложения


Программа будет представлять собой простое веб-приложение, считающее количество посещений и отображающее информацию о контейнере, в котором оно запущено. Для этого нам необходимо создать несколько Dockerfile (файлов конфигурации образа Docker), для каждого используемого сервиса (Nginx, Redis, Flask) и указать, как они должны взаимодействовать между собой.

Откроем страницу проекта, созданного ранее:

Страница проекта

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

$ git clone git@gitlab.example.ru:root/flask-docker-swarm.git
$ cd flask-docker-swarm

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

$ mkdir nginx
$ mkdir web
$ mkdir redis

Создание сервиса Nginx


Для сервиса Nginx нам понадобится создать три файла — Dockerfile и два файла настроек. Создадим и запишем общие настройки для всего веб-сервера:

$ nano nginx/nginx.conf

И запишем следующий текст:

# Укажем какой пользователь запускает и выполняет Nginx процесс
user  nginx;
# Укажем количество рабочих процессов, рекомендуемое число -
# число процессоров на сервере
worker_processes  1;

# Укажем расположение лога ошибок и уровень значимости записываемых сообщений
error_log  /var/log/nginx/error.log warn;
# Укажем имя файла, в котором будет хранится PID главного Nginx-процесса
pid        /var/run/nginx.pid;

events {
   # Укажем максимальное число одновременных соединений
   worker_connections  1024;
}


# http блок определяет как должен вести себя Nginx c http-трафиком
http {
   # Включение файла с перечислением всех поддерживаемых типов файлов
   include       /etc/nginx/mime.types;
   # Определение файла возвращаемого по-умолчанию
   default_type  text/html;

   # Определяет формат сообщений-логов
   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

   # Указывает путь сохранения логов запросов на доступ к Nginx
   access_log  /var/log/nginx/access.log  main;

   # Параметры для оптимизации доставки статических файлов
   sendfile        on;
   tcp_nopush     on;
   tcp_nodelay    on;

   # Определяет время жизни соединения с клиентом
   keepalive_timeout  65;

   # Параметр включает компрессирование gzip для экономии трафика
   #gzip  on;

   # Включение дополнительных параметров для виртуальных хостов
   include /etc/nginx/conf.d/*.conf;
}

Следующий файл определяет параметры для виртуального сервера, непосредственно здесь указывается связь на сервис web, в котором будет выполняться наше приложение:

$ nano nginx/flask.conf

И запишем текст:

# Блок server определяет параметры виртуального хоста/сервера
server {


   # Определение имени сервера, IP адреса и/или порта, на котором слушает сервер
   listen 80 default_server;
   # server_name xxx.yyy.zzz.aaa

   # Определение типа символов для поля “Content-Type” в заголовке ответа
   charset utf-8;

   # Настройка Nginx для доставки статических файлов через указанную директорию
   #location /static {
   #    alias /usr/src/app/web/static;
   #}

   # Настройка Nginx в качестве прокси-сервера на внутренний сервер выгрузки данных (Gunicorn (WSGI server))
   location / {
       # Адрес и порт сервера выгрузки данных
       proxy_pass http://web:5000;

       # Переопределение заголовков посылаемых серверу выгрузки данных
       proxy_set_header Host $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

       # Максимальный размер файлов для загрузки
       client_max_body_size 5M;
       client_body_buffer_size 5M;
   }
}

Блок со статическим контентом закомментирован, поскольку в нашем приложении не будет дополнительных файлов, но в дальнейшем можно добавить директорию static в сервисе web и открыть этот блок для эффективной передачи статических файлов.

Создадим Dockerfile, в котором используем готовый образ Nginx в DockerHub и модифицируем его для использования своих файлов настройки:

$ nano nginx/Dockerfile

И запишем следующий текст:

FROM nginx:1.13.6

RUN rm /etc/nginx/nginx.conf

COPY nginx.conf /etc/nginx/

RUN rm /etc/nginx/conf.d/default.conf

COPY flask.conf /etc/nginx/conf.d

Создание сервиса Redis


Для сервиса Redis создадим Dockerfile:

$ nano redis/Dockerfile

С простым содержанием:

FROM redis:3.2.11

Мы не вносим дополнительных изменений, но в будущем они вполне возможны, поэтому создадим отдельный сервис.

Создание сервиса с приложением Flask


Сервис с приложением Flask начнём с создания основного исполняемого файла:

$ nano web/main.py

Вставим следующий код:

from flask import Flask
from redis import Redis, RedisError
import os
import socket

# Connect to Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)

app = Flask(__name__)

@app.route("/")
def hello():
    try:
        visits = redis.incr("counter")
    except RedisError:
        visits = "<i>cannot connect to Redis, counter disabled</i>"

    html = "<h3>Hello {name}!</h3>" \
           "<b>Hostname:</b> {hostname}<br/>" \
           "<b>Visits:</b> {visits}"
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)

if __name__ == "__main__":
    app.run()

Создадим отдельный файл с указанием использованных зависимостей:

$ nano web/requirements.txt

И перечислим используемые программные пакеты:

Flask==0.12.2
Redis==2.10.6
Gunicorn==19.7.1
Nose2
Coverage

Настроим простой пример модульного тестирования для демонстрации, в котором проверим доступность страницы. Создадим файл теста:

$ nano web/test_smoke.py

Скопируем и вставим текст:

import os
import unittest

from main import app


class BasicTests(unittest.TestCase):

    # executed prior to each test
    def setUp(self):
        app.config['TESTING'] = True
        app.config['WTF_CSRF_ENABLED'] = False
        app.config['DEBUG'] = False
        self.app = app.test_client()
        self.assertEqual(app.debug, False)

    # executed after each test
    def tearDown(self):
        pass


    def test_main_page(self):
        response = self.app.get('/', follow_redirects=True)
        self.assertEqual(response.status_code, 200)



if __name__ == "__main__":
    unittest.main()

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

$ nano web/wsgi.py

В котором укажем имя импортируемого объекта, который будет использовать Gunicorn:

from main import app

if __name__ == "__main__":
    app.run(host='0.0.0.0')

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

$ nano web/Dockerfile

Со следующим содержанием:

FROM python:3.6.3

RUN groupadd flaskgroup && useradd -m -g flaskgroup -s /bin/bash flask

WORKDIR /app

ADD . /app

RUN pip install -r requirements.txt

Создание сервисов на основе контейнеров Docker


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

$ nano docker-compose.yml

И запишем такой текст, заменив доменное имя:

version: "3.4"

services:
  web:
    image: gitlab.example.ru:4567/root/flask-docker-swarm/web:${CI_COMMIT_SHA}
    deploy:
      replicas: 4
      restart_policy:
        condition: on-failure
    command: gunicorn -w 3 --bind 0.0.0.0:5000 wsgi:app

  nginx:
    image: gitlab.example.ru:4567/root/flask-docker-swarm/nginx:${CI_COMMIT_SHA}
    deploy:
      mode: global
      restart_policy:
        condition: on-failure
    ports:
      - "80:80"

  redis:
    image: gitlab.example.ru:4567/root/flask-docker-swarm/redis:latest
    deploy:
      replicas: 1
      placement:
          constraints: [node.role == manager]
      restart_policy:
        condition: on-failure
    ports:
      - "6379"

Рассмотрим подробнее структуру файла:

  • блок services включает в себя описание всех создаваемых сервисов;
  • в каждом сервисе есть раздел image, который определяет используемый образ для создания контейнеров на его основе. Подобный формат записи позволяет получать образы с хранилища, доступного по адресу gitlab.example.ru:4567. Последний аргумент ${CI_COMMIT_SHA} — переменная окружения, связанная с значением хеша текущего коммита, которую мы используем для различия сборок друг от друга в данном руководстве;
  • блок deploy используется только при использовании команды docker stack deploy. Мы используем три ключевых слова внутри данного блока:
    • replicas — количество копий контейнера;
    • placement — расположение контейнеров относительно рабочих нодов;
    • restart_policy — условия перезапуска контейнеров;

  • открытие портов для общения между сервисами и внешней средой осуществляется в разделе ports;
  • для выполнения дополнительных команд при старте сервиса используется блок command.

Для запуска на локальной машине нам понадобится создать дополнительный файл настройки docker-compose для упрощенного тестирования приложения без использования сервиса Nginx:

$ nano docker-compose.override.yml

Вставим следующий текст:

version: "3.4"

services:
    web:
        image: web
        environment:
            - FLASK_APP=wsgi.py
            - FLASK_DEBUG=1
        build:
            context: ./web
            dockerfile: Dockerfile
        command: 'flask run --host=0.0.0.0'
        links:
            - redis
        ports:
            - "5000:5000"
        volumes:
            - ./web/:/usr/src/app/web

    redis:
        image: redis
        build:
            context: ./redis
            dockerfile: Dockerfile
        ports:
          - "6379:6379"


По структуре файл напоминает основную версию, но не использует сервис Nginx и есть дополнительный раздел сборки контейнеров build.

Управление GitLab CI осуществляется через файл конфигурации:

$ nano .gitlab-ci.yml

Запишем следующий текст, поменяв example.ru на свое доменное имя:

image: docker:17.09.0-ce


services:
- docker:dind

before_script:
- apk add --update py-pip &&
      pip install docker-compose


stages:
  - test
  - build
  - deploy
  - stage


unittests:
  stage: test
  script:
    - cd web
    - pip install -q -r requirements.txt
    - nose2 -v --with-coverage
  tags:
    - docker

docker-build:
  stage: build
  script:
  - docker login -u root -p $HUB_REGISTRY_PASSWORD https://gitlab.example.ru:4567/
  - docker build -t gitlab.example.ru:4567/root/flask-docker-swarm/nginx:$CI_COMMIT_SHA ./nginx
  - docker push gitlab.example.ru:4567/root/flask-docker-swarm/nginx:$CI_COMMIT_SHA
  - docker build -t gitlab.example.ru:4567/root/flask-docker-swarm/web:$CI_COMMIT_SHA ./web
  - docker push gitlab.example.ru:4567/root/flask-docker-swarm/web:$CI_COMMIT_SHA
  - docker build -t gitlab.example.ru:4567/root/flask-docker-swarm/redis:latest ./redis
  - docker push gitlab.example.ru:4567/root/flask-docker-swarm/redis:latest
  tags:
  - docker


deploy-to-swarm:
  stage: deploy
  variables:
    DOCKER_HOST: tcp://{manager_ip_address}:2376
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "/certs"
  script:
    - mkdir -p $DOCKER_CERT_PATH
    - echo "$TLSCACERT" > $DOCKER_CERT_PATH/ca.pem
    - echo "$TLSCERT" > $DOCKER_CERT_PATH/cert.pem
    - echo "$TLSKEY" > $DOCKER_CERT_PATH/key.pem
    - docker login -u root -p $HUB_REGISTRY_PASSWORD $CI_REGISTRY
    - docker stack deploy -c docker-compose.yml env_name --with-registry-auth
    - rm -rf $DOCKER_CERT_PATH
  environment:
    name: master
    url:  http://{manager_ip_address}
  only:
    - master
  tags:
    - docker

Необходимо заменить значения полей DOCKER_HOST и URL на свои, а также внимательно изменить названия тегов образов Docker. Подробнее о доступных названиях образов можно посмотреть в Registry в GitLab.

В качестве тегов для образов мы использовали переменную окружения $CI_COMMIT_SHA для создания привязки между коммитом и образом. Для образа Redis мы указали тег latest в целях сохранения текущей базы данных.

Рассмотрим файл настроек подробнее:

  • вначале файла указывается блок image, который определяет на основе какого образа будет осуществляться сборка проекта;
  • в блоке services мы дополнительно подключаем образ с настроенным Docker, для запуска контейнеров Docker внутри Docker;
  • раздел before_script содержит команды, выполняемые перед запуском каждой стадии сборки проекта;
  • блок stages перечисляет в каком порядке будут исполняться различные стадии выполнения работ;
  • выполнение каждого раздела происходит в именованном блоке, например, unittests, который в свою очередь состоит из нескольких разделов:
    • поле stage указывает в какой стадии выполняется блок;
    • поле variables может содержать дополнительные переменные окружения, необходимые для выполнения операций;
    • поле script содержит список команд, которые будут выполнены в этом блоке;
    • поле tags определяет на каких GitLab Runners может выполняться даный блок.


Запуск проекта на локальной машине


Мы готовы запустить проект на локальной машине. Подобный метод запуска удобно использовать в целях разработки и тестирования в дальнейшем. Для начала установим Docker (мы приведем пример для Ubuntu 16.04, для Windows существует отдельный инсталлятор). Добавим репозиторий от разработчиков Docker для получения последней версии:

$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update

Установим последнюю версию Docker:

$ sudo apt-get install docker-ce

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

$ sudo docker-compose -f docker-compose.override.yml build

Если всё прошло без ошибок, запустим проект локально:
$ sudo docker-compose -f docker-compose.override.yml up

И перейдем в браузере по адресу:

localhost:5000

В случае успешного запуска всех сервисов, вывод будет примерно следующим:

Локально

Если попробовать перезагрузить страницу несколько раз, счётчик посещений будет увеличиваться, а hostname останется прежним, потому что сейчас используется только один контейнер, в котором выполняется сервис web, генерирующей веб-страницу.

Непрерывная интеграция и доставка


Пришло время самого интересного — запуска и обзора непрерывной интеграции. Для этого просто перейдем в директорию нашего проекта на локальной машине и произведем первый коммит и публикацию в удаленный репозиторий:

$ git add --all
$ git commit -m “init”
$ git push origin master

Если на предыдущих шагах не было совершенно ошибки, произойдет “пуш” в наш репозиторий GitLab, пройдут все тесты и сборка образов Docker, а затем развертывание в Docker Swarm.

Для отслеживания процессов перейдем в GitLab в раздел Pipelines и откроем последний:

Pipelines!

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

Перейдем в раздел Environments:

Environments

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

Среды

Для отката на предыдущую версию достаточно нажать кнопку Rollback, которая запустит стадию deploy для выбранного коммита.

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

Посмотрим на наше приложение. В браузере перейдите по адресу сервера Manager:

Реальное приложение
Если несколько раз перезагрузить страницу, то заметим, что значение hostname меняется в пределах 4 вариантов, что соответствует количеству реплик, которое мы указали для сервиса web.
Docker Swarm взял на себя контроль по размещению контейнеров между нодами в своей сети. При этом половина контейнеров сервиса web была автоматически размещена в подчиненном ноде. Для просмотра подробной информации можно выполнить следующую команду на сервере Manager, заменив название рабочей среды (если вы его меняли в команде docker stack deploy) на своё:

$ docker stack ps env_name

Мы рассмотрели один из способов применения GitLab CI для настройки непрерывной интеграции и доставки своих Docker-проектов. Следует отметить, что при использовании подобного решения для рабочих проектов следует уделить значительное внимание безопасности — использовать сертификаты доверенных центров, настроить сеть нодов в Docker Swarm для реагирования на перезагрузки серверов и контролировать количество хранимых и используемых образов Docker.
  • +21
  • 12,3k
  • 7
Поделиться публикацией
Ой, у вас баннер убежал!

Ну, и что?
Реклама
Комментарии 7
  • +4
    Хорошая обзорная статья, радует подробное описание Docker TLS auth.

    Пара замечаний:
    $ nano /etc/systemd/system/multi-user.target.wants/docker.service


    Не надо так делать, при следующем обновление пакета докер перестанет работать, нужно создать директорию:
    nano  /etc/systemd/system/multi-user.target.wants/docker.service.d/ 

    и в ней переопределить параметр запуска, создав файл с расширением .conf и параметром, который нужно добавить или переопределить.

    И image для develop/master по комитам я бы не тегал, лучше для этого feature_ бранчи использовать и git tag для релизов. Это больше от реализации git flow у вас зависит, конечно.
    • +2
      Большое спасибо за комментарий! Обновил раздел про переопределение параметра запуска.
      • 0
        Непонятно зачем это делать в скопе multi-user.target. /etc/systemd/system/docker.service.d/ так-же должен сработать.
        А на достаточно новых версиях systemd можно просто сказать systemctl edit docker
    • +2
      $HUB_REGISTRY_PASSWORD лишний, можно использовать $CI_REGISTRY_PASSWORD:
      echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
      
      • 0
        А как решаете, если во время бмлда нужен доступ к приватному репозиторию?
        • +2
          есть несколько вариантов:
          — прокидывать с хостовой машины ssh agent
           something:
              container_name: something
              volumes:
                  - $SSH_AUTH_SOCK:/ssh-agent # Forward local machine SSH key to docker
              environment:
                  SSH_AUTH_SOCK: /ssh-agent 

          — использовать secret variable с приватным ключом и делать что-то вида
          # add credentials on build
          ARG SSH_PRIVATE_KEY
          RUN mkdir /root/.ssh/
          RUN echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa
          
          # make sure your domain is accepted
          RUN touch /root/.ssh/known_hosts
          RUN ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts

          — использовать какую-либо обертку над сборкой докера, например habitus blog.cloud66.com/using-ssh-private-keys-securely-in-docker-build
          • 0

            Вот я второй способ использовал, собственно другого для стандартного docker container build не нашёл.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое