Разворачиваем Jenkins для Django проекта с использованием docker-compose
В этой статье я хочу рассмотреть задачу развертывания jenkins для django-приложения, использующего такой стек технологий, как redis (для кэша) и postgres (в качестве СУБД).
Она может оказаться полезной тем, кто хочет познакомиться с docker-compose, а также тем, кто хочет развернуть jenkins, но не знает как правильно настроить его для работы с django.
Я предполагаю, что на машине уже установлен docker (пользователь находится в группе docker) и python3.
Статью я решил разбить на четыре части:
-
создадим простое джанго приложение и настроим среду для его работы, изолируем postgres и redis в докер-контейнеры и научимся к ним подключаться;
-
добавим простой функционал в наше приложение;
-
напишем тесты к этому функционалу;
-
развернем jenkins и настроим его для работы с нашим приложением.
Если у вас есть уже свое приложение и вас интересует только подключение django к jenkins, вы можете пропустить первые три части.
Часть 1. Развертывание django приложения в тандеме с docker-compose
Начнем с простого - создадим виртуальную среду для python приложения. Я использую virtualenvwrapper.
mkdir django_jenkins_test
cd django_jenkins_test
mkvirtualenv django_jenkins_test -p `which python3`
Создадим файл requirements.txt со следующим содержимым:
Django==1.11
django-redis==4.8.0 # для использования redis в качестве кэша
psycopg2==2.7.3 # для работы с БД
coverage==4.4.1 # для генерации тестов покрытия
Установим зависимости:
pip install -r requirements.txt
Создадим приложение:
django-admin startproject django_jenkins_test .
Теперь, когда у нас есть заготовка приложения, надо подготовить базу данных и redis для кэширования. Окружение python прекрасно изолируется через virtualenv, а вот для изоляции БД и redis я воспользуюсь докер контейнерами. Для удобства работы я буду использовать docker-compose.
Docker compose - это обертка для Docker написанная на python, упрощающая работу с несколькими контейнерами. Устанавливается через pip. Можно установить как в виртуальную среду, так и глобально:
pip install docker-compose
Создадим файл docker-compose.yml со следующим содержимым:
version: '2'
services:
db:
image: "postgres:9.6-alpine"
environment:
POSTGRES_PASSWORD: admin
POSTGRES_USER: admin
ports:
- 15432:5432
redis:
image: "redis:3.0-alpine"
ports:
- 16379:6379
Тут мы определили два сервиса:
-
Один для базы данных. Мы установили ему алиас db, в качестве образа указали: postgres:9.6-alpine, пробросили порт 5432 контейнера в 15432 порт хоста и указали пароль и юзера для суперюзера.
-
Второй сервис для redis. Мы указали ему алиас redis, в качестве образа используем redis:3.0-alpine и пробросили порт 6379 контейнера в 16379 порт хоста.
Теперь мы воспользуемся docker-compose, чтобы запустить эти сервисы. Для этого вызовем:
docker-compose up
Если ошибок не наблюдается, то тормозим контейнеры Ctrl+С и запускаем по новой:
docker compose start
Отличие start от up в том, что start просто запускает остановленные контейнеры, а up удаляет старые контейнеры, создает их по новой и запускает. Причем start, по умолчанию, запускает контейнеры в режиме демона, а up - в общем потоке, чтобы up запускался в detach состоянии, надо добавлять флаг -d.
Остановить контейнеры, запущенные через docker-compose, можно вызвав команду:
docker-compose stop
А удалить через:
docker-compose down
Теперь, когда redis и postgres у нас запущены, необходимо настроить django приложение, чтобы оно подключалось к сервисам, работающим в докер контейнерах. Для этого создадим файл django_jenkins_test/local_settings.py и пропишем в него наши настройки:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'default',
'USER': 'admin',
'PASSWORD': 'admin',
'HOST': '127.0.0.1',
'PORT': '15432', # тут используем порт, в который проборсили порт контейнера
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:16379/1", # тут используем порт, в который проборсили порт контейнера
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
В конец файла django_jenkins_test/settings.py добавим:
from .local_settings import *
После всех действий, содержимое нашей папки должно выглядеть так:
Теперь попробуем запустить django миграции:
python manage.py migrate
и получим ошибку:
django.db.utils.OperationalError: FATAL: database "default" does not exist
Причина ошибки в том, что мы не создали БД в контейнере. Есть несколько способов решения проблемы. Самый простой, наверное, это изменить имя базы данных в local_settings на admin, и подключиться через:
python manage.py dbshell
И создать базу через CREATE DATABASE default и вернуть настройки обратно. Но мы пойдем другим путем и попробуем зайти в докер контейнер, подключиться изнутри к postgres и создать там базу данных.
Для начала узнаем имена контейнеров, которые создал docker-compose (команду, как и все прочие, надо вызывать из папки, в которой находится docker-compose.yml файл), вызовем:
docker-compose ps
увидим что-то подобное:
Name Command State Ports
------------------------------------------------------------------------------
djangotest_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:15432->5432/tcp
djangotest_redis_1 docker-entrypoint.sh redis Up 0.0.0.0:16379->6379/tcp
То есть у нас два контейнера (как мы и ожидали). Имя контейнера состоит из названия папки, в котором лежит файл docker-compose.yml + алиас сервиса + порядковый номер запущенной копии контейнера.
Коннектимся к контейнеру базы данных, как это принято в обыкновенном docker:
docker exec -it djangotest_db_1 bash
Оказываемся внутри контейнера и создаем базу данных используя пользователя admin, указанного при создании сервиса. Любым способом, например, через psql:
echo “CREATE DATABASE "default";” | psql -Uadmin
или через утилиту postgres:
createdb -Uadmin “default”
Выходим из контейнера. Возвращаемся к миграциям, запускаем их:
python manage.py migrate
Теперь все должно быть зеленое.
Часть 2. Добавляем функционал
Для начала создадим приложение main:
python manage.py startapp main
Добавляем его в settings в INSTALLED_APPS:
INSTALLED_APPS = [
...
'main',
]
Наш проект выглядит вот так:
Теперь создаем простое апи для добавления и просмотра существующих пользователей. Для этого в main/views.py пишем:
from django.contrib.auth.models import User
from django.core.cache import cache
from django.http.response import JsonResponse
from django.http.response import HttpResponseNotFound
from django.http.response import HttpResponse
from django.views.generic.base import View
from django.conf import settings
class UserApi(View):
def post(self, request, *args, **kwargs):
"""
Создает пользователя
"""
user = User.objects.create_user(
username=request.POST.get('username'),
email=request.POST.get('email'),
password=request.POST.get('password')
)
return {
'id': user.id,
'username': user.username,
'email': user.email,
}
def get(self, request, *args, **kwargs):
"""
Возвращает список пользователей
"""
return {
'users': [{
'id': user.id,
'username': user.username,
'email': user.email,
} for user in User.objects.all().order_by("id")]
}
def dispatch(self, request, *args, **kwargs):
result = super(UserApi, self).dispatch(request, *args, **kwargs)
return JsonResponse(result)
В этот же файл main/views.py добавляем простое апи для трекинга пользователей:
class TrackerView(View):
def post(self, request, *args, **kwargs):
"""
Фиксирует пользователя как активного на settings.TRACKER_CACHE_TIMEOUT секунд
"""
if request.user:
cache.set("tracker_{}".format(request.user.pk), "active", settings.TRACKER_CACHE_TIMEOUT)
return HttpResponse()
def get(self, request, *args, **kwargs):
"""
Возвращает 200, если пользователь активный или 404 в обратном случае
"""
user_id = request.GET['id']
value = cache.get("tracker_{}".format(user_id))
if not value:
return HttpResponseNotFound()
return HttpResponse()
При POST запросе в кеше будет создаваться ключ для пользователя, который будет очищаться через TRACKER_CACHE_TIMEOUT, в GET запросе можно будет узнать статус пользователя; если с момента вызова POST произошло больше TRACKER_CACHE_TIMEOUT секунд, в ответе будет приходить статус 404; если же пользователь все еще активен, будет выдавать статус 200.
Не забудем добавить в settings.py новую настройку:
TRACKER_CACHE_TIMEOUT = 10
Часть 3. Добавляем тесты
Один на проверку добавления пользователя, второй для тестирования вывода списка пользователей и третий для тестирования трекера. Для этого в main/tests.py пишем следующий код:
from time import sleep
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
from django.urls.base import reverse
class TestMainViews(TestCase):
def test_create_user(self):
# создаем пользователя
data = {
'username': 'username',
'email': 'email@email.ru',
'password': 'password',
}
self.client.post(reverse("users"), data)
self.assertEqual(1, User.objects.count())
# проверяем, что он создался с корректными данными
user = User.objects.get()
self.assertEqual(user.username, 'username')
self.assertEqual(user.email, 'email@email.ru')
# проверяем, что можем залогиниться
logged = self.client.login(username='username', password='password')
self.assertTrue(logged)
self.client.logout()
def test_list_users(self):
# создаем двух пользователей
user1 = User.objects.create_user(
username='username1',
email='email1@email.ru',
password='password',
)
user2 = User.objects.create_user(
username='username2',
email='email2@email.ru',
password='password',
)
# проверяем, что они возвращаются в корректном порядке и количестве
r = self.client.get(reverse("users"))
data = r.json()
self.assertEqual(2, len(data['users']))
self.assertEqual(data['users'][0]['id'], user1.id)
self.assertEqual(data['users'][1]['id'], user2.id)
@override_settings(TRACKER_CACHE_TIMEOUT=1)
def test_tracker_works(self):
# создаем пользователя
user = User.objects.create_user(
username='username',
email='email@email.ru',
password='password',
)
# логинимся под ним
self.client.login(username='username', password='password')
# дергаем трекер
self.client.post(reverse("tracker"))
# проверяем, что при запросе статуса пользователя в трекере,
# пользователь значится как активный
r = self.client.get(reverse("tracker"), {
'id': user.pk
})
self.assertEqual(200, r.status_code)
# еще раз дергаем трекер
self.client.post(reverse("tracker"))
# проверяем, что через 2 секунды пользователь уже не значится залогиненым
sleep(2)
r = self.client.get(reverse("tracker"), {
'id': user.pk
})
self.assertEqual(404, r.status_code)
Часть 4. Разворачиваем Jenkins
Итак, у нас есть готовое приложение. Подключим git (или любую другую vcs, если вдруг еще не сделали) к приложению и отправим его во внешний репозиторий. Это нам потребуется в будущем, чтобы jenkins мог подтягивать приложение независимо от того, где мы его развернем.
Также убедитесь, что файл django_jenkins_test/local_settings.py исключен из репозитория.
Разворачивать мы будем по следующей схеме. Дженкинс мы поместим в отдельный докер контейнер + выделим два дополнительных контейнера под postgres и redis.
Приступим. Для начала создадим отдельную папку под дженкинс, назовем ее jenkins и в ней создадим файл docker-compose.yml со следующим содержимым:
version: '2'
services:
postgres:
image: "postgres:9.6-alpine"
environment:
POSTGRES_PASSWORD: admin
POSTGRES_USER: admin
redis:
image: "redis:3.0-alpine"
jenkins:
build:
context: .
dockerfile: Dockerfile_jenkins
ports:
- "9090:8080"
- "50000:50000"
depends_on:
- redis
- postgres
Как мы видим, первые два сервиса postgres и redis имеют практически идентичные настройки тем, что мы прописывали для приложения на основном приложении.
Главное отличие в том, что мы не пробрасываем порты, так как не планируем обращаться к этим контейнерам напрямую.
А вот с jenkins все сложнее, возможностей стандартного образа нам не хватит, поэтому придется его расширить. Для этого создадим файл jenkins/Dockerfile_jenkins и поместим в него следующее содержимое:
FROM jenkins/jenkins:2.75-alpine
USER root
RUN apk add --update\
python3 \
python3-dev \
build-base \
py3-pip \
postgresql-dev
USER jenkins
Тут мы дополнительно установим python3, а также dev-пакеты для разработки для python и postgresql.
Мы укажем имя этого файла для нашего сервиса в docker-compose.yml в настройке dockerfile: Dockerfile_jenkins
-
ports - мы пробросим 8080 из контейнера в 9090 порт хоста.
-
depends_on - означает, что мы не запустим этот контейнер пока не будут запущены указанные в списке сервисы.
Наш проект должен выглядеть примерно так:
А все действия мы будем теперь выполнять из папки jenkins, для этого перейдем в нее:
cd jenkins
Запускаем docker-compose из папки jenkins:
docker-compose up -d
В этот раз я запустил контейнеры в режиме демонов сразу, и поэтому их логи мне не видны. Чтобы увидеть, что там происходит внутри, запустим:
docker-compose logs
Причем, наверное, лучше даже так (чтоб листать удобнее было):
docker-compose logs | less
Открываем http://localhost:9090 там нас встречает:
Пароль можно подсмотреть в логах:
либо вызвать команду:
docker exec jenkins_jenkins_1 cat /var/jenkins_home/secrets/initialAdminPassword
В следующем окне можно выбрать плагины, но мы это сделаем чуть позже. Поэтому вместо выбора варианта, просто закрываем окно:
Попадаем на dashboard дженкинса:
Заходим в Manage Jenkins / Manage Plugins, выбираем вкладку Available и выбираем следующие плагины:
Config File Provider Plugin -- потребуется нам для создания/редактирования файла local_settings.py.
ShiningPanda Plugin -- потребуется чтобы работать в python окружении.
Git plugin -- потребуется для работы с git репозиторием.
Ждем пока дженкинс закончит установку, перегружаем его. Для перезагрузки можно поставить галочку внизу:
Но у меня, почему-то, не всегда корректно происходит перезагрузка. Так что я предпочитаю дождаться установки плагинов, и только после этого ставить галочку.
Убедитесь, что все плагины установлены, прежде чем перейти к следующему шагу. Их названия должны появиться в Manage Jenkins / Manage Plugins, во вкладке Installed.
Теперь давайте создадим конфиг для нашего приложения. Он должен будет уметь стягивать проект из внешнего репозитория, устанавливать необходимые зависимости, ну и, самое главное, прогонять тесты и генерировать соответствующие отчеты.
Сначала настроим ShiningPanda и укажем путь к python, для этого переходим в Manage Jenkins / Global Tool Configuration и в разделе Python (должен быть в самом низу) укажем следующие настройки:
Адрес python в контейнере можно узнать, вызвав команду:
docker exec jenkins_jenkins_1 which python3
Теперь создадим конфигурационный файл для local_settings.py. Для этого перейдем в раздел Manage Jenkins / Managed files:
Жмем Add a new config:
Выбираем custom file и жмем submit:
Заполняем данные, в поле Name указываем что-то вроде “DjangoJenkinsTestLocalSettings”(это псевдоним нашего файла), а в поле content вбиваем:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'default',
'USER': 'admin',
'PASSWORD': 'admin',
'HOST': 'postgres',
'PORT': '5432',
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://redis:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
Как видите, в качестве хостов для БД и redis, я указываю не адрес и даже не localhost, а имена сервисов, как они были записаны в docker-compose.yml. Docker compose сам заботится о создании внутренней сети между контейнерами и позволяет контейнерам общаться друг с другом, используя алиасы сервисов. В качестве портов я указываю стандартные порты для postgres и redis.
Подготовительные работы завершены. Создаем новый Freestyle project:
В настройках проекта в Source Code Management подразделе указываем путь к репозиторию с проектом и в случае необходимости добавляем юзеры/пароли для доступа. В итоге получаем вот такое:
Раздел Build Environment настраиваем следующим образом:
В File выбираем созданный ранее Configuration File, а в поле Target указываем куда он будет скопирован, то есть, в нашем случае, файл надо положить в папку django_jenkins_test и дать ему имя local_settings.py.
В разделе Build нажимаем Add build step и выбираем Virtualenv Builder:
А теперь просто прописываем все команды, которые мы будем вызывать:
Разберем по командам:
pip install -r requirements.txt # устанавливаем зависимости
coverage run --source='.' manage.py test # прогоняем тесты с отчетом по
покрытию
coverage html # генерируем html отчет покрытия
В качестве последнего шага, в раздел Post-build Actions добавляем “Publish coverage.py HTML reports”:
В качестве Report Directory, указывается путь к папке, куда генеритcя отчет по покрытию.
Отлично, все готово и можно запускать:
Наслаждаемся успешным результатом:
Послесловие
В данной статье я попытался описать процесс разворачивания дженкинса для джанго проекта с использованием докер-контейнеров. Как видите, конфиг дженкинса/докера, получился достаточно простой. В нашей компании мы используем похожую конфигурацию, и, хотя наш проект в сотни раз больше приведенного тестового проекта, сам конфиг, по существу, отличается только немного более усложненным шагом Virtualenv Builder. Плюс в post-build actions мы генерируем дополнительные отчеты, помимо отчета по покрытию.
Также вы, наверное, заметили, что наш конфиг не создает никакой информации по результатам тестирования: какие тесты упали, а какие прошли успешно. К сожалению, это один из минусов стандартного джанговского testrunner`а который ничего не умеет, кроме прогонки тестов. Для решения этой проблемы существует как минимум два решения: подключить к проекту пакет django-jenkins или unittest-xml-reporting или сменить систему тестирования, например, на куда более мощный py.test.
Подробнее данный вопрос поднимается в статье. Только не забудьте поставить плагин JUnit Plugin для работы с junits отчетами.
Дополнительно, для более глубокого ознакомления с docker`ом рекомендую статью.