В этом руководстве рассмотрим вариант настройки непрерывной интеграции и развертывания 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:
После создания проекта перейдем в настройки CI / CD:
Откроем область секретных переменных (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:
Теперь по этому примеру добавим оставшиеся значения cert.pem и key.pem.
Настройка сервиса Docker на управляющем сервере Manager
По-умолчанию доступ к сервису Docker имеет только пользователь-владелец процесса. Для выполнения операций по развертыванию приложения с удаленного хоста, нам необходимо разрешить подключение извне, при этом использовать TLS-протокол. Мы уже получили необходимые сертификаты и ключи, осталось настроить Docker для работы с ними.
Мы создадим отдельный конфигурационный файл явно прописывающий хосты, по которым будет доступен Docker, поэтому для начала нам нужно удалить стандартный параметр -H, прописывающий хост. Для этого откроем файл docker.service:
$ nano /etc/systemd/system/multi-user.target.wants/docker.service
Найдем строку:
ExecStart=/usr/bin/dockerd -H fd://
Удалим флаг и значение, приведя строку к следующему виду:
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 есть информация по подключенным рабочим хостам: приватным и открытым:
В блоке 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:
Перейдем к созданию и докеризации простого 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:
Для дальнейшей работы с внутренним хранилищем образов нам понадобится использовать связку логин-пароль для подключения к нему. Используем возможность добавления секретных переменных в 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 и откроем последний:
Если ваш результат отличается от приведенного, перейдите в раздел Jobs и посмотрите логи по неудавшейся операции, часто это бывают опечатки или неверные адреса серверов.
Перейдем в раздел 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.
Автор: nzavyalov