Автоматичне розгортання Django з GitLab

У цій статті я опишу налаштування автоматичного розгортання веб-додатки на стеку Django + uWSGI + PostgreSQL + Nginx з репозиторію на сервісі GitLab.com. Викладене також стосується кастомних інсталяції GitLab. Передбачається, що читач має досвід у створенні веб-додатків на Django, а так само досвід адміністрування Linux-систем.
Розгортання реалізуємо за допомогою Fabric, Docker і docker-compose, а здійснювати його буде сервіс безперервної інтеграції, вбудований в GitLab, під назвою GitLab CI.
Механізм автоматичного розгортання
Розгортання буде відбуватися наступним чином:
  1. При push'e нових комітів в репозиторій буде автоматично запускатися GitLab CI.
  2. GitLab CI буде збирати Docker-образ готовим до запуску Django-додатком.
  3. Потім GitLab CI відправить (push) зібраний Docker-образ в GitLab container registry. Зверніть увагу, налаштування приватності в registry ті ж, що і у репозиторію, тобто для публічних репозиторіїв GitLab registry відкритий для всіх.
  4. Gitlab CI запустить юніт-тести.
  5. У разі, якщо коміти або merge request'и проводилися в головну гілку (
    master
    ), то після успішного складання та тестування Gitlab CI з допомогою Fabric розгорне зібраний Docker-образ на сервер з зазначеним нами IP-адресою.
Приватні дані, необхідні для розгортання — закриті ключі,
SECRET_KEY
для Django, токени сторонніх сервісів і т. д. — зберігати відкритим текстом в репозиторії безумовно не стіт, тому для їх зберігання скористаємося механізмом GitLab Secret Variables:
image
При такому підході конфіденційні дані доступні відкритим текстом лише у двох місцях: у настроюваннях проекту на GitLab.com і на сервері, на який здійснюється розгортання. У свою чергу, на сервері конфіденційні дані будуть зберігатися в змінних оточення (читай: будуть видні кожному, хто може на нього зайти по SSH).
Наступні змінні необхідні для роботи механізму розгортання:
  • DEPLOY_KEY
    — приватна частина SSH-ключа, який використовується для входу на сервер;
  • DEPLOY_ADDR
    — його IP-адресу;
  • SECRET_KEY
    відповідна налаштування Django.
Крім того, у файлі
settings.py
Django-проекту визначимо
SECRET_KEY
наступним чином:
SECRET_KEY = os.getenv('SECRET_KEY') or sys.exit('SECRET_KEY environment variable is not set.')

Крок 1: Docker
В першу чергу, створимо
Dockerfile
для запуску Django і uWSGI на основі легковажного способу Alpine Linux:
web/Dockerfile
FROM python:3.5-alpine

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

RUN apk add --no-cache --virtual .build-deps gcc musl-dev linux-headers pkgconf \
autoconf automake libtool make postgresql-dev postgresql-client openssl-dev && \
apk add postgresql-libs postgresql-client && \
# Запобігаємо невдалу компіляцію uWSGI всередині Docker, див. https://git.io/v1ve3
(while true; do pip --no-cache-dir install uwsgi==2.0.14 && break; done)

COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /usr/src/app
RUN SECRET_KEY=build ./manage.py collectstatic --noinput && \
./manage.py makemessages && \
apk del .build-deps

Передбачається, що залежно нашого веб-додатки, як це прийнято у світі Python, зберігаються у файлі
requirements.txt
.
Крок 2: docker-compose
Далі, для оркестрации Docker-контейнерів стека нам знадобиться
docker-compose
.
Теоретично, можна було б обійтися і без нього, але тоді файл з інструкціями для CI став би роздутим і нечитабельним (див. для прикладу здесь).
Отже, в кореневій директорії репозиторію створимо файл
docker-compose.yml
наступного змісту:
docker-compose.yml
version: '2'
services:
web:
# TODO: Змініть username і project на підходящі вам значення.
image: registry.gitlab.com/username/project:${CI_BUILD_REF_NAME}
build: ./web
ports:
# відкриті назовні порти
- "8000:8000"
environment:
# змінні, значення яких пробрасываются
# в контейнер з сервера
- SECRET_KEY
command: uwsgi /usr/src/app/uwsgi.ini
volumes:
- static:/srv/static
restart: unless-stopped

test:
# TODO: Змініть username і project на підходящі вам значення.
image: registry.gitlab.com/username/project:${CI_BUILD_REF_NAME}
command: python manage.py test
restart: "ні"

postgres:
image: postgres:9.6
ports:
# відкриті назовні порти
- "5432:5432"
environment:
# змінні оточення: користувач і база даних
- POSTGRES_USER=root
- POSTGRES_DB=database
volumes:
# сховище даних
- data:/var/lib/postgresql/data
restart: unless-stopped

nginx:
image: nginx:mainline
ports:
# відкриті назовні порти
- "80:80"
- "443:443"
volumes:
# сховища конфіги і статичних файлів
- ./nginx:/etc/nginx:ro
- static:/srv/static:ro
depends_on:
- web
restart: unless-stopped

Наведений файл відповідає наступній структурі проекту:
repository
├── nginx
│ ├── mime.types
│ ├── nginx.conf
│ ├── ssl_params
│ └── uwsgi_params
├── web
│ ├── project
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── app
│ │ ├── migrations
│ │ │ └── ...
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── Dockerfile
│ ├── manage.py
│ ├── requirements.txt
│ └── uwsgi.ini
├── docker-compose.yml
└── fabfile.py

Тепер весь стек запускається однією командою
docker-compose up
, а всередині Docker-контейнерів стека доступ до інших запущеним контейнерів відбувається за DNS-імен, відповідних записів у файлі
docker-compose.yml
. Так, релевантна частина конфига Nginx буде виглядати наступним чином:
nginx.conf
upstream django {
server web:8000;
}

… а налаштування доступу Django до БД наступним:
settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'database',
'HOST': 'postgres',
}
}

Завдяки налаштуванню
restart: unless-stopped
перезавантаження сервера всі контейнери в нашому стеку автоматично перезапускається з тими параметрами, з якими вони були запущені спочатку, тобто ніяких додаткових дій при перезапуску сервера здійснювати не потрібно.
Крок 3: GitLab CI
Створимо в корені репозиторію файл
.gitlab-ci.yml
з інструкціями для GitLab CI:
.gitlab-ci.yml
# Повідомляємо Gitlab CI, що ми будемо використовувати Docker при збірці.
image:docker:latest
services:
- docker:dind

# Описуємо, з яких ступенів складатиметься наша безперервна інтеграція:
# - збірка Docker-образу,
# - прогін тестів Django,
# - викотити на бойовий сервер.
stages:
- build
- test
- deploy

# Описуємо инициализационные команди, які необхідно запускати
# перед запуском кожного ступеня.
# Зміни, внесені на кожному ступені, не переносяться на інші, так як запуск
# ступенів здійснюється в чистому Docker-контейнері, який пересоздается кожен раз.
before_script:
# установка pip
- apk add --no-cache py-pip
# установка docker-compose
- pip install docker-compose==1.9.0
# логін в Gitlab Docker registry
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY

# Збірка Docker-образу
build:
stage: build
script:
# власне збірка
- docker-compose build
# відправка зібраного в registry
- docker-compose push

# Прогін тестів
test:
stage: test
script:
# замість повторної зборки, забираємо зібраний на попередній щаблі
# готовий образ з registry
- docker-compose pull test
# запускаємо тести
- docker-compose run test

# Викотити на сервер
deploy:
stage: deploy
# викочуємо тільки гілку master
only:
- master
# для цього ступеня інші команди ініціалізації
before_script:
# встановлюємо залежності Fabric, bash і rsync
- apk add --no-cache openssh-client py-pip py-crypto bash rsync
# встановлюємо Fabric
- pip install fabric==1.12.0
# додаємо приватний ключ для викочування
- eval $(ssh-agent -s)
- bash -c 'ssh-add <(echo "$DEPLOY_KEY")'
- mkdir -p ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
script:
- fab -H $DEPLOY_ADDR deploy

Варто відзначити, що Docker-runner'и GitLab CI, які ми використовуємо, в якості основи використовують все той же образ Alpine Linux, що створює ряд труднощів — з коробки немає bash, незвичний пакетний менеджер апк, незвична стандартна бібліотека musl-libc та ін. Труднощі компенсуються тим, що образи на основі Apline Linux виходять дійсно легковажними; так, офіційний образ
python:3.5.2-alpine
важить всього 27.6 MB.
Крок 4: Fabric
Для викочування програми на сервер потрібно в кореневій же директорії репозиторію створити файл
fabfile.py
, як мінімум містить наступне:
fabfile.py
#!/usr/bin/env python2

from fabric.api import hide, env, settings, abort, run, cd, shell_env
from fabric.colors import magenta, red
from fabric.contrib.files import append
from fabric.contrib.project import rsync_project
import os

env.user = 'root'
env.abort_on_prompts = True
# TODO: Замініть шлях на сервері, з якого будуть скопійовані файли додатки
PATH = '/srv/mywebapp'
ENV_FILE = '/etc/profile.d/variables.sh'
VARIABLES = ('SECRET_KEY', )

def deploy():
def rsync():
exclusions = ('.git*', '.env', '*.sock*', '*.lock', '*.pyc', '*cache*',
'*.log', 'log/', 'id_rsa*', 'maintenance')
rsync_project(PATH, './', exclude=exclusions, delete=True)

def docker_compose(command):
with cd(PATH):
with shell_env(CI_BUILD_REF_NAME=os.getenv(
'CI_BUILD_REF_NAME', 'master')):
# ховаємо прогрес-бар, див. https://git.io/vXH8a
run('set -o pipefail; docker-compose %s | tee' % command)

# Зберігаємо змінні на сервері
variables_set = True
for var in VARIABLES + ('CI_BUILD_TOKEN', ):
if os.getenv(var) is None:
variables_set = False
print(red('ERROR: environment variable' + var + ' is not set.'))
if not variables_set:
abort (Missing required parameters')
hide with('commands'):
run('rm -f "%s"' % ENV_FILE)
append(ENV_FILE,
['export %s="%s"' % (var, val) for var, val in zip(
VARIABLES, map(os.getenv, VARIABLES))])
# Fabric перечитує змінні з профілю при кожному виклику run(),
# тому немає сенсу робити це явно. див. http://stackoverflow.com/q/38024726/1336774

# Логинимся в registry
run('docker login -u %s -p %s %s' % (os.getenv('REGISTRY_USER',
'gitlab-ci-token'),
os.getenv('CI_BUILD_TOKEN'),
os.getenv('CI_REGISTRY',
'registry.gitlab.com')))

# Виконуємо початкову установку, якщо потрібно
with settings(warn_only=True):
hide with('warnings'):
need_bootstrap = run('docker ps | grep -q web').return_code != 0
if need_bootstrap:
print(magenta('No previous installation found, bootstrapping'))
rsync()
docker_compose('up -d')

# Включаємо заглушку "технічні роботи", див. https://habr.ru/post/139968
run ("touch %s/nginx/maintenance && docker kill -s HUP nginx_1' % PATH)
rsync()
docker_compose('pull')
docker_compose('up -d')
# Прибираємо заглушку
run('rm -f %s/nginx/maintenance && docker kill -s HUP nginx_1' % PATH)

Взагалі кажучи, копіювати
rsync
'ом весь репозиторій необов'язково, для запуску було б достатньо файлу
docker-compose.yml
та вмісту директорії
nginx
.
Код додатка зберігається на сервері на випадок, якщо раптом знадобиться внести термінові зміни "наживую". На безкоштовних акаунтів gitlab.com для запуску CI використовується порівняно слабке виртуализированное залізо, тому збірка, тести і викот, як правило, відбуваються за 5-10 хвилин.
image
(правда, буває, що вони до цього ще в черзі стирчать цілу вічність)
Однак бувають випадки, коли кожна секунда на рахунку — для таких випадків ми залишаємо лазівку у вигляді повних джерел додатки. Для застосування змін, внесених "наживую", достатньо перейти в теку
/srv/mywebapp
і сказати в консолі
docker-compose build
docker-compose up -d

Висновок
Таким чином, ми реалізували безперервну інтеграцію веб-додатки за допомогою сервісу GitLab.
image
Тепер всі зміни будуть прогоняться через батарею автоматичних тестів (які, зрозуміло, теж потрібно написати), а зміни в головній гілці будуть автоматично розгортатиметься на бойовий сервер з майже нульовим часом простою.
За рамками статті залишилися наступні питання:
  • налаштування файрвола на цільовому сервері для заборони доступу до PostgreSQL і uWSGI ззовні;
  • налаштування ротації журналів Nginx;
  • налаштування бекапів PostgreSQL.
Залишимо їх допитливому читачеві в якості самостійного вправи.
Посилання
» GitLab CI: Вчимося деплоить
» GitLab CI Quick Start
» GitLab Container Registry
» Django на production. uWSGI + nginx. Докладне керівництво
» Fabric documentation
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.