Непрерывная интеграция и развертывание на примере GitLab CI + Flask + Docker + Docker Swarm. Часть 2. Создание приложения
Вторую часть нашего знакомства с GitLab CI в связке с Docker мы посвятим созданию и докеризации простого Flask приложения, использующего два дополнительных сервиса: Nginx для маршрутизации запросов и Redis для хранения счетчика посещений страницы.
Для начала необходимо убедиться, что GitLab и вспомогательные сервера настроены и доступны. Если нет, то рекомендуем сначала пройти первую часть руководства. Кроме того, для использования Docker Registry нам понадобится защитить GitLab SSL-сертификатом, это можно сделать с помощью бесплатного сервиса Let’s Encrypt, что рассмотрено в статье “Настройка HTTPS в GitLab с помощью Let’s Encrypt”
В данном руководстве мы будем использовать локальную машину с ОС Ubuntu 16.04, но это проявляется лишь в процессе создания файлов и навигации между директориями, поэтому пользователи других операционных систем смогут без проблем воспользоваться данной статьей.
Подключение Docker Registry в GitLab
Перед созданием приложения нам необходимо активировать функцию хранения образов Docker в нашем приложении GitLab для эффективного управления развернутыми версиями приложения и обеспечения возможности отката на предыдущие версии.
Подключимся по SSH к серверу GitLab и откроем конфигурационный файл:
$ nano /etc/gitlab/gitlab.rb
Далее добавим с новой строки адрес, по которому будет доступно хранилище Docker, заменив example.com на имя своего хоста:
registry_external_url 'https://gitlab.example.com:4567'
Добавим ещё две строки с указанием места хранения сертификата и ключа. Поскольку мы используем такое же доменное имя, что и для основного приложения 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"
Сохраним изменения и выйдем из редактора сочетанием клавиш ‘Ctrl’+’X’.
Перезагрузим сервис GitLab для активации хранилища:
$ gitlab-ctl reconfigure
Перейдем к созданному ранее проекту flask-docker-swarm в GitLab. В случае успешной активации хранилища, в меню проекта станет доступен раздел Registry:
Для дальнейшей работы с внутренним хранилищем образов нам понадобится использовать связку логин-пароль для подключения к нему. Используем возможность добавления секретных переменных в GitLab для добавления пароля. Для этого перейдем в раздел Settings и выберем блок CI / CD:
Раскроем раздел Runner settings и добавим новую переменную HUB_REGISTRY_PASSWORD, значением которой является пароль от учетной записи пользователя GitLab:
Создание приложения
Программа будет представлять собой простое веб-приложение, считающее количество посещений и отображающее информацию о контейнере, в котором оно запущено. Для этого нам необходимо создать несколько Dockerfile (файлов конфигурации образа Docker), для каждого используемого сервиса (Nginx, Redis, Flask) и указать, как они должны взаимодействовать между собой.
Откроем страницу проекта, созданного в первой части данного руководства:
На локальной машине выполним предлагаемые команды в терминале (если этого не было сделано ранее) для настройки пользователя, публикующего изменения в репозитории (для Windows воспользуйтесь Git Bash):
$ git config --global user.name "Administrator"
$ git config --global user.email "admin@example.com"
Выполним следующую команду для клонирования репозитория и перехода в рабочую директорию, заменив доменное имя:
$ 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 = "cannot connect to Redis, counter disabled"
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
Запишем следующий текст:
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.
Войдите в службу, чтобы оставить комментарий.
Комментарии
0 комментариев