Приложение js_example:
Источник: https://github.com/dahachm/student-exam2
Demonstrates how to post form data and process a JSON response using JavaScript. This allows making requests without navigating away from the page. Demonstrates using XMLHttpRequest, fetch, and jQuery.ajax. See the Flask docs about jQuery and Ajax.
Задача в этом проекте:
- Установить, настроить и запустить Jenkins (агент и мастер) с использованием docker образов
- Написать CI pipeline, который:
- клонирует файлы из репозитория с приложением и запускает python тесты
- собирает docker образ с ним
- пушит это образ в приватный репозиторий на dockerhub.
- Написать CD pipeline, который:
- использует ansible playbook с 3-мя ролями: docker (устанавливает docker на указанных хостах), nginx (устанавливает nginx для балансировки экземпляров приолжения
js_example
), web (запускает несколько docker контейнеров из ранее созданного образа сjs_example
на борту) - после того, как отработает playbook и все контейнеры запущены, проверяет доступность сервера
- использует ansible playbook с 3-мя ролями: docker (устанавливает docker на указанных хостах), nginx (устанавливает nginx для балансировки экземпляров приолжения
Образ dahachm/js_example:1.0 собран на базе python:3
.
Для того, чтобы удалось проверить доступность приложения и успешность запуска, необходимо пробросить порт 5000 контейнера на любой другой доступный порт хостовой ОС и запускать приложение (flask) с параметром --host=0.0.0.0
(т.е. указать ему: "открой, пожалуйста, доступ на всех сетевых интерфейсах с IPv4 адресом").
FROM python:3
WORKDIR /usr/local/js_example
COPY . /usr/local/js_example/
ENV FLASK_APP js_example
EXPOSE 5000
RUN pip install -e .
CMD flask run --host=0.0.0.0
Сборка образа:
$ git clone https://github.com/dahachm/student-exam2.git
$ docker build -t flask:1 .
Запуск образа:
$ docker run -d -p 5000:5000 --name flask_app flask:1
Результат:
Для того, чтобы в полной мерей выполнить следующие задания, сформировались следующие требования к агенту:
-
установить java8, ansible, docker, git, openssh, python3 и pip3, curl
-
при старте контейнера считывать из (передаваемой) переменной окружения публичный ключ мастера и запускать sshd (этим занимается скрипт setup-ssh.sh)
Образ dahachm/js_example:jenkins-agent-alpine собран на базе alpine:3.12
.
В нём уже установлены нужные пакеты, создан пользователь jenkins
, внутри содержится скрипт setup-ssh.sh, который создает каталог /home/jenkins/.ssh, добавляет ключ jenkins мастера, устанавливает необходимые права доступа на файлы ssh и запускает демон sshd.
Сам dockefile:
FROM alpine:3.12
RUN apk update \
&& apk add \
openrc \
openssh \
openssh-server \
python3 \
py3-pip \
openjdk8 \
git \
ansible \
docker \
curl
RUN adduser -D jenkins
ENV SSH_PUBLIC_KEY ''
COPY setup-ssh.sh /usr/local/setup-ssh.sh
CMD /bin/sh /usr/local/setup-ssh.sh && tail -f /dev/null
Сборка образа и пуш в репозиторий docker hub:
$ docker build -f jenkins_agent -t jenkins-agent_alpine:1 .
$ docker tag jenkins-agent_alpine:1 dahachm/js_example:jenkins-agent-alpine
$ docker push dahachm/js_example:jenkins-agent-alpine
Результат
3. Docker-compose файл, который будет поднимать оба контейнера (мастер и агент) вместе с нужными нам параметрами
Чтобы docker клиент имел доступ к docker демону, можно передать через вольюм docker.sock из хостовой ОС или повозиться с docker образом, установить и настроить службу инициализации (например, system.d или init.d) при запуске контейнера. Я выбрала первый вариант :)
В файле docker-compose устанавливаем вольюмы с указанием пути к каталогам для домашних директорий (чтобы иметь доступ к файлам jenkins контейнеров из хостовой ОС и хранить данные между их стартами) и пути в /var/run/docker.sock - сокету docker по умолчанию.
Для контейнера с мастером открываем порты 8080 и 50000, чтобы иметь доступ к webui из браузера хостовой ОС.
Для контейнера с агентом прописываем установку переменной окружения для хранения публичного SSH ключа - SSH_PUBLIC_KEY.
version: '3'
services:
jenkins_master:
image: jenkins/jenkins
volumes:
- ./jenkins_home_master:/var/jenkins_home:rw
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 9000:8080
- 50000:50000
jenkins_agent:
image: dahachm/js_example:jenkins-agent-alpine
volumes:
- ./jenkins_home_agent:/var/jenkins_home:rw
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SSH_PUBLIC_KEY=${SSH_PUBLIC_KEY}
tty: true
Перед запуском docker-compose нужно также:
-
создать ключи
$ ssh-keygen -P '' -m PEM -f id_rsa
Столкнулась с проблемой, что ключи, сгенерированные на хостовой или любой другой ОС кроме той, что на jenkins мастере, получают ошибку авторизации при попытке связать мастера и агента. Рабочий костыль: сначала запустить контейнер с jenkins мастером, сгенерировать на нём ключи, скопировать к себе в рабочую директорию на хостовой ОС (откуда будут запускаться контейнеры через docker copmose) и снова запустить docker-compose.
-
сохранить содержимое публичного ключа в переменную SSH_PUBLIC_KEY
$ export SSH_PUBLIC_KEY=$(cat id_rsa.pub)
-
создать домашние директории для jenkins мастера и jenkins агента
$ mkdir jenkins_home_master $ mkdir jenkins_home_agent
Запуск docker-compose:
$ docker-compose -f jenkins_up.yml up -d
Результат:
В моём docker-compose файле трафик с порта 8080 контейнера пробрасывается на 9000 на хостовой ОС (centos7, которая живет у меня в VM, в VBox установленном на Windows).
Так что WebUI jenkins мастера у меня доступен по 192.168.56.117:8080:
Так как домашний каталог мы закрепили в локальном каталоге ./jenkins_home_master, то прочитать пароль можно следующим вызовом (из рабочего каталога):
$ cat jenkins_home_master/secrets/initialAdminPassword
либо отправить запрос в сам контейнер:
$ docker exec alpine_jenkins_master_1 cat /var/jenkins_home/sercrets/initialAdminPassword
Далее предлагается установить плагины (рекомендуемые или выбрать вручную):
Можно создать первого пользователя или продолжить как админ. Я создам своего пользователя admin:
Подтверждение настроек и старт:
Создадим нового пользователя developer:
Заданим матрицу доступа для имеющихся пользователей:
-
admin – полные права на все
-
developer:
Overall: read
Job: build, cancel, discover, read, workspace
Agent: build
Нужно добавить криндешиалс для взаимодействия с другими сервисами (github, dockerhub) и установки связи с агентами.
Установка SSH закрытого ключа (предварительного скопировать его в буфер обмена, ключ должен быть из той же пары, из которой экземпляр публичного ключа был помещен в контейнер с агентом!):
Установка реквизитов для подключения к github, dockerhub происходит в том же разделе, режим Username with password
. Нужно добавить логин и пароль.
В результате имеем следующий набор:
Установка плагинов ansible, docker:
В разделе управления плагинами перейти во вкладку Доступные и найти нужный плагин:
Ansible:
Далее в разделе Global tool configuration добавить путь к бинарнику ansible, установленному на jenkins агенте:
Также нужно установить плагины для docker:
Создание:
Dashboard -> New Item -> [Ввести имя pipeline] и выбрать Pipeline
В разделе General
выбираем Github project
и добавляем url к репозиторию с приложением js_example
:
В разделе Build triggers
выбираем Poll SCM
(SCM - Source Control Management) и в Shedule
указать * * * * *
, что значит, что jenkins будет проверять наличие обновление в репозитории и, если недавно был коммит, то запустит новый билд.
В разделе Pipeline
в Definition
выбрать Pipeline script from SCM
, что указывает jenkins, что pipeline скрипт будет брать из удаленного репозитория.
Далее указываем параметры доступа к git репозиторию, указываем имя ветки, из которой брать файлы для билда и имя jenknsfil'а в репозитории (относительно корня репы).
pipeline {
environment {
imagename = "dahachm/js_example"
registryCredential = 'dockerhub_pass'
}
agent { label 'agent-1' }
stages {
stage('Checkout code') {
steps {
checkout scm
}
}
stage('Run Python tests') {
steps {
sh '''
python3 -m venv venv
. venv/bin/activate
pip install -e '.[test]'
coverage run -m pytest
coverage report
'''
}
}
stage('Building image') {
steps {
script {
dockerImage = docker.build imagename
}
}
}
stage('Deploy Image') {
steps {
script {
docker.withRegistry( '', registryCredential ) {
dockerImage.push("$BUILD_NUMBER")
}
}
}
}
stage('Remove Unused docker image') {
steps {
sh "docker rmi $imagename:$BUILD_NUMBER"
}
}
}
}
В секии environment указываем переменные со значением тега образа, с под которым будем загружать его на удаленный репозиторий в docker hub и с именем реквизитов для входа в dockerhub.
Dashboard -> js_example_CI -> Build Now
или автоматически после коммита в репозитории.
Stage Veiw - все этапы pipeline c указанием результата каждого стейджа (успех/провал, время работы):
Логи pipeline'a:
Образ добавлен в репозиторий docker hub:
Этот playbook будет применяться в localhost, то есть внутри контейнера jenkins агента. Образ jenkins агента основа на alpine, следовательно целевая ОС - alpine (ОС нашего агента).
Репозиторий с playbook'ом: https://github.com/dahachm/student-exam2-ansible
Роли:
-
docker
Устанавливает docker, python3 и Docker SDK for python (нужен для создания docker network через ansible playbook), запускает docker демон и создает docker network с именем {{ network_name }}, к которой будут подключены контейнеры с
js_example
, nginx и агентом (агент в ней добавляетя для того, чтобы позже смогли проверить доступность сервера nginx). -
nginx
Эта роль зависима от роли docker.
Устанавливает nginx и помещает nginx.conf из Jinja2 шаблона, в котором в секции upstream указываются переменные с именем хост и порта, на которых будут запущены приложения.
Режим балансировщика -
least_conn
, что значит, что nginx будет перенаправлять запросы на тот хост, к которому сейчас меньше всего подключений.Сам nginx поднимается на 80-м порту.
-
web
Эта роль зависима от роли docker.
Эта роль логинится в приватный репозиторий dockerhub (реквзиты для подключения хранятся в зашифрованных с помощью ansible-vault переменных), загружает образ с
js_example
и запускает контейнеры c открытием 5000-го порта на заданный порт и подключает их к ранее созданной docker сети.
В главном файле Playbook.yml указываются переменные web_servers
(структура - словарь) и network_name
, которые задают имена хостов и портов, на которых будет запускаться приложение js_example
, и имя создаваемой docker сети соответсвенно.
vars:
web_servers:
app_1: 5081
app_2: 5082
app_3: 5083
network_name: web
Также указываю параметр become: yes
, так как для некоторых операций (установка пакетов, например) нужны sudo права, а через интерфейс, доступный в jenkins плагине для ansible нет возможности указать параметр повышения привилегий (-b
).
Предварительные настройки агента:
Перейдём в контейнер и следующие команды будем выполнять внутри:
$ docker exec -it alpine_jenkins_agent_1 /bin/bash
Создание пользователя admin и задание пароля:
# adduser admin
# passwd admin
Создание и сохранение ssh ключе для соединения admin@localhost:
# ssh-keygen -P '' -f ~/.ssh/id_rsa
# ssh-copy-id -i ~/.ssh/id_rsa admin@localhost
Также нужно установить sudo и добавить пользователя admin в sudoers
:
# apk add sudo
# echo "admin ALL=(ALL) NOPASSWD:ALL"
Чтобы docker отработал без ошибок, нужно проверить права /var/run/docker.sock
были установлены в 660, а gid группы docker в контейнере совпадал c gid группы docker на хостовой ОС.
Также нужно добавить пользователей jenkins и admin в группу docker.
Также добавить еще две пары реквизитов в Manage credentials: vault пароль и закрытый SSH ключ для подключения к admin@localhost от имени jenkins:
Создание CD pipeline
Настройки при создании pipelin'a такие же, как в п.6, отличие только в том, что в качестве источника файла для билдов указаны новый репозиторий (с ansible playbook) и имя ветки alpine, а не master.
pipeline {
agent { label 'agent-1' }
stages {
stage('Checkout code') {
steps {
checkout scm
}
}
stage('Deploy with ansible playbook') {
steps {
ansiblePlaybook(
credentialsId: 'ssh_ansible',
vaultCredentialsId: 'vault_pass',
inventory: 'hosts',
playbook: 'Playbook.yml')
}
}
stage('Play Integration tests') {
steps {
sh '''
nginx_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx)
answer_code=$(curl -I $nginx_IP:80 2>/dev/null | head -n1 | awk '{print $2}')
if (( answer_code == 200 ));
then
echo "SUCCESS";
else
echo "FAILURE. Server return $answer_code";
fi
'''
}
}
}
}
В качестве интеграционного теста здесь с помощью curl
проверяем возвращаемый ответ сервера nginx. Запрос отправляем на адрес, который назначен контейнеру с nginx, на 80й порт.
Если возвращается 200, то выводим сообщение SUCCESS
, что значит, что все хорошо, сервер как минимум живой. Если возвращается другой код, то выводим сообщение FAILURE
и номер вернувшегося кода.
*Для этого предварительно пришлось добавить контейнер с jenkins агентом в ту же сеть, к которой уже прикреплены контейнеры app_ и nginx. Все из-за того, что все docker клиенты, которых мы создали в этом проекте, и "внешние", и "внутренние", обращаются к одному и тому же docker демону. Так что вот такой костыль. Но я уверена, что есть способ избежать и этого, возможно, исправлю это позже.
Результаты:
Состояние pipelin'а:
Запущенные контейнеры:
Сеть web_network
, которая была создана playbook'ом:
$ docker network inspect web_network
Так как все контейнеры у меня на одном сервере, то в своем браузере я могу открыть их обращаясь к порту 10000
(при создании контейнера nginx поставили правило перенаправления трафика из порта 80 контейнера на порт 10000 внешней системы).
Сейчас я создам сразу несколько сессий, открывая несколько новых вкладок в инкогнито режиме. Так как мы установили режим балансировки nginx least_conn, ожидается, что каждая вкладка будет перенаправлена на новый хост (всего три штуки).