Главная » Хабрахабр » CI/CD-пайплайн на примере одного небольшого проекта Уральской Дирекции ИТ

CI/CD-пайплайн на примере одного небольшого проекта Уральской Дирекции ИТ

Действующие лица (Команда): разработчиков – 2 человека, админ – 1 человек.

Статья повествует об использовании таких технологий, как Ansible, Docker Swarm, Jenkins и Portainer для реализации CI/CD-пайплайна с возможностью контроля за ним с помощью красивого веб-интерфейса.

Вступление

Чего обычно хочет разработчик? Он хочет творить, не думая о деньгах, и максимально быстро видеть результаты собственного творчества.

Другими словами, бизнес мечтает об ускорении получения MVP (a.k.a. С другой стороны, есть бизнес, который хочет денег, да побольше, и поэтому постоянно думает о снижении времени вывода продукта на рынок. Minimum Viable Product) в новых продуктах или при обновлении существующих.

А админ – человек простой, он хочет, чтобы сервис не падал и не мешал играть в Кваку Танки и чтобы его пореже дергали разработчики и бизнес.
Поскольку для реализации желаний админа, как показывает правда жизни, его силами должны реализоваться и мечты других героев, представители ИТ-тусовки много работали над этим. Ну а чего же хочет админ? Часто получалось достичь желаемого, придерживаясь методологии DevOps и реализуя принципы CI/CD (Continuous Integration and Delivery).

Так получилось в одном небольшом новом проекте в Уральской Дирекции ИТ, в которой удалось в весьма сжатые сроки реализовать полный пайплайн от публикации изменений исходников в системе контроля версии разработчиком до автоматического запуска новой версии приложения в тестовой среде.

Часть 0. Описание задачи

Архитектура системы

После некоторых обсуждений командой была выбрана следующая двухуровневая архитектура:

  • Бекэнд-часть на Java, реализованная на фреймворке Spring Boot, общающаяся с различными БД и другими корпоративными системами (потому что легко, быстро и понятно как писать).
  • Фронтэнд-часть на NodeJS (и ReactJS – интерфейс в браузере), потому что очень быстро работает.

В свою очередь, к этим компонентам был добавлен еще NGINX-сервер, являющийся фронтэндом для NodeJS-приложения. Его роль заключалась в распределении запросов между самим приложением и другими инфраструктурными компонентами системы, речь о которых пойдет ниже.

Чего хотелось команде

Как только новому проекту был дан зеленый свет, появилась первая техническая задача, а именно – подготовка «оборудования» для запуска нового проекта. Поскольку всем участникам было очевидно, что без максимальной оперативности выкатки новых версий на серверы развитие проекта будет весьма непростым, сразу же было решено идти по пути полного CI/CD, т.е. хотелось достичь следующего пайплайна:

  • Разработчик публикует изменения (коммит) в систему контроля версий (гит);
  • гит проводит минимально необходимое тестирование содержимого коммита на наличие необходимой атрибутики (например, правильный формат commit message), соответствие стилю оформления Банка и другой бюрократии;
  • гит-сервер посредством механизма web-hook-ов дергает сервер непрерывной интеграции Jenkins;
  • Jenkins запускает операции скачивания актуальной версии исходников из гит-а и выполнения CI/CD-пайплайна:

  1. компиляция исходников и первоначальное тестирование;
  2. сборки новой версии Docker-образов (неприлично же что-то деплоить в 2018-м на голое железо или виртуалку, не поймут);
  3. публикация образов в Artifactory (система хранения и управления бинарными артефактами, рекомендую!);
  4. перезапуск новой версии приложения (или всего «стека» приложений) на сервере с «откатом» к предыдущей версии в случае не самого успешного обновления.

Рамки

У людей в теме наверняка уже возник вопрос: «А чего это они какими-то костылями пользуются, а не 'production-ready'-решениями а-ля Kubernetes или Mesos / Marathon ?». Подобный вопрос вполне разумен, поэтому сразу же скажем, что описываемое решение было использовано по целому ряду причин, в том числе:

  • Оно проще (сильно-сильно проще);
  • Его было легче понять всей команде и развернуть админу.

Однако, мы не забываем, что выбранное нами решение относится к богатому семейству костылей, и надеемся в недалеком будущем переехать на более стандартный стек OpenShift + Bamboo.

Кроме того, эта статья касается только веб-ориентированных приложений, а также рассказывает об идеальном случае stateless-архитектуры, когда данные наверняка где-то есть, но они далеко, и мы об их хранении не задумываемся.

Часть 1. Установка и базовая настройка ПО на хост-системе

С целью максимальной автоматизации и высокой воспроизводимости всей цепочки хост-систему (виртуальная машина на базe VMWare / qemu KVM / облако / что-то еще) было решено настраивать с использованием системы управления конфигурацией Ansible.

свойства, при котором при повторных запусках итоговое состояние системы не изменяется. Стоит добавить, что в дополнение к легкой повторяемости и воспроизводимости, использование подобных систем (кроме Ansible, существуют также системы Puppet и Chef) обладает огромным преимуществом перед использованием разнообразных shell- или python- скриптов в виде наличия идемпотентности, т.е.

Данное преимущество вытекает из того факта, что при использовании систем управления конфигурациями описывается не процесс достижения желаемого состояния, а само желаемое состояние в декларативной форме.

1.1 ssh HostKeyChecking

По умолчанию, Ansible уважает безопасность и проверяет ssh-отпечаток удаленного конфигурируемого хоста. Т.к. в данном режиме отключена возможность авторизации по паролю, то при первоначальной настройке сервера требуется либо отключить HostKeyChecking, либо предварительно добавить отпечаток в локальный кеш. Последнего можно достичь целыми двумя путями:

или так:

Определить специальную переменную окружения:

$ export ANSIBLE_HOST_KEY_CHECKING=False

или по-другому:

Добавить в локальный конфигурационный файл ansible.cfg параметр host_key_checking:

[defaults] host_key_checking = False

При первом способе проверка отключается только пока существует такая переменная окружения, а при втором – полностью для этого хоста.

1.2 Inventory

Inventory – это сущность в системе Ansible, с помощью которой описываются хосты и их группы, конфигурацией которых необходимо управлять.

В данном проекте был выбран последний. Inventory можно описывать в формате ini или yaml.

Пример файла hosts.yml:

#_ Группа all существует всегда
all: hosts: # имя хоста удаленной системы, к которому будет подключаться Ansible some-cool-vm-host vars: # имя пользователя, кем будет авторизоваться ansible_user: 'root' # очень плохо такое делать, но тут записан пароль открытым текстом 🙁 ansible_password: '12345678' # Это сертификат банковского СА corp_ca_crt: "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"

Для впервые столкнувшихся с форматом yaml хотелось бы отметить, что все отступы в данном формате необходимо оформлять пробелами.

1.3 Playbook

Playbook – это еще одна сущность в Ansible, в которой непосредственно декларативно описывается желаемое конечное состояние хостов и групп из Inventory. Так же, как и почти все в Ansible, playbook описывается в файле (ах) в yaml-формате.

Для того чтобы запустить на выполнение playbook-файл, необходимо выполнить команду вида:

ansible-playbook -i ./hosts.yml tasks.yml

В данном playbook-е описывалась полная настройка базовой системы с созданием необходимых пользователей и установкой Docker-а:

#_ указывается маска имен хостов и групп, для которых будут выполняться нижеописанные таски (задания)
- hosts: all tasks: # Список тасков - name: Удаляем все репозитории из системы shell: rm /etc/zypp/repos.d/* || exit 0 - name: Добавляем банковские SLES-репы на хост REPOs... zypper_repository: repo="}" name="{{ item.name }}" disable_gpg_check="{{ item.disable_gpg_check|default('no') }}" with_items: - { repo: "http://...", name: "SLE-DISTRO-X" } - name: Обновляем все пакеты в системе автоматически zypper: name: '*' state: latest - name: Помещаем сертификат банковского CA на сервер copy: # Берем сертификат из переменных Inventory content: '{{ corp_ca_crt }}' dest: /etc/pki/trust/anchors/сorpCA.crt owner: root group: root mode: 0644 - name: ... и перечитываем системное хранилище shell: update-ca-certificates - name: Создаем юзеров и группы group: name="{{ item.name }}" gid={{ item.gid }} state="present" with_items: - { name: "docker", gid: 1000 } - user: name: "{{ item.name }}" uid: "{{ item.uid }}" group: "{{ item.gid }}" state: "present" with_items: - { name: "dockeradm", uid: 1000, gid: "docker" } - name: Меняем пароли пользователям user: name: "{{ item }}" password: "$6$..." generate_ssh_key: yes with_items: - root - dockeradm - name: Добавляем публичный ssh-ключ текущего пользователя ЛОКАЛЬНОЙ машины указанным юзерам на УДАЛЕННОМ сервере authorized_key: user: "{{ item }}" key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" state: "present" with_items: - root - dockeradm - name: Создаем нужные VG в системе lvg: vg: 'vgAPP' pvs: '/dev/sdb' - name: ... и LV lvol: vg: "{{ item.vg }}" lv: "{{ item.lv }}" size: "{{ item.size }}" with_items: - { vg: 'vgAPP', lv: "lvData", size: "10G" } - { vg: 'vgAPP', lv: "lvDockerData", size: "5G" } - name: Создаем ФС на созданных LV-ах filesystem: dev="/dev/{{ item }}" fstype="btrfs" with_items: - 'vgAPP/lvData' - 'vgAPP/lvDockerData' - name: Прописываем информацию об ФС в /etc/fstab и монтируем их mount: path="{{ item.dst }}" src="/dev/{{ item.src }}" state="mounted" fstype="btrfs" opts="noatime" with_items: - { src: "vgAPP/lvData", dst: "/APP" } - { src: "vgAPP/lvDockerData", dst: "/var/lib/docker" } - name: Создаем всякие важные каталоги file: path: "{{ item.path }}" state: "directory" # внимание не дефолтные значения mode: "0{{ item.perms|default('755') }}" owner: "{{ item.user|default('dockeradm') }}" group: "{{ item.group|default('docker') }}" with_items: - { path: '/etc/docker', user: 'root', group: 'root' } - { path: '/APP' } - { path: '/APP/configs' } - { path: '/APP/configs/filebeat' } - { path: '/APP/logs' } - { path: '/APP/logs/nginx' } - { path: '/APP/jenkins' } - { path: '/APP/jenkins/master' } - { path: '/APP/jenkins/node' } - { path: '/APP/portainer_data' } - name: Ставим нужные пакеты в систему zypper: name: '{{ item }}' with_items: - docker - mc # Java необходима для запуска Jenkins-слейва - java-1_8_0-openjdk-headless # Помним о необходимости настроить подсети для Докера - name: Копируем конфиг-файл с "правильными сетями" template: src: daemon.json dest: /etc/docker/daemon.json owner: root group: root mode: 0644 - name: Активируем и запускаем сервисы... systemd: name: "{{ item }}" state: 'restarted' enabled: 'yes' with_items: - docker - sshd - name: Помещаем на целевой сервер docker-compose через скачивание на локальную get_url: url: "https://github.com/docker/compose/releases/download/1.18.0/docker-compose-Linux-x86_64" dest: "/tmp/docker-compose" delegate_to: 127.0.0.1 - copy: src: "/tmp/docker-compose" dest: "/usr/local/bin/docker-compose" mode: "u=rwx,g=rx,o=rx" - name: Копируем конфиг-файл NGINX, конструируя его из шаблона template: src: nginx.conf dest: /APP/configs/nginx.conf owner: dockeradm group: docker mode: '0644' - name: Копируем описание docker-compose - сервиса, конструируя его из шаблона template: src: docker-compose.yml dest: /APP/docker-compose.yml owner: dockeradm group: docker mode: '0644' - name: Запуск docker-compose - сервис с Jenkins, Portainer и NGINX перед ними shell: docker-compose -f /APP/docker-compose.yml up -d --force-recreate - name: Ожидаем окончания инициализации Jenkins-а wait_for: path: '/APP/jenkins/master/secrets/initialAdminPassword' - name: Получаем значение временного пароля Jenkins fetch: src: '/APP/jenkins/master/secrets/initialAdminPassword' dest: initialJenkinsAdminPassword.txt flat: yes

Часть 2. Сервисы проекта

2.1 CI-сервер и процесс

За процесс «непрерывной интеграции и деплоймента» в проекте отвечает хорошо многим известный сервер Jenkins CI.

Код Ansible playbook-и выше устроен так, что в конце его выполнения на сервере уже запущен свежеустановленный Jenkins (в Docker-контейнере), а его временный пароль сохранен на ЛОКАЛЬНОЙ машине в файле initialJenkinsAdminPassword.txt.

Поскольку всей команде хотелось максимально приблизиться к идеальному случаю Infrastructure as code (IaC), то в проекте таски были реализованы в виде декларативных и скриптованных пайплайнов Jenkins-а, когда задачи описываются на языке Groovy Script, а сам их код хранится рядом с исходниками проекте в системе контроля версий (git).

Пример пайплайна сборки backend-части приложения на Spring Boot показан ниже:

pipeline { agent { # указываем, что выполнять задачу хотим внутри # Docker-контейнера на базе указанного образа: docker { image 'java:8-jdk' } } stages { stage('Стягиваем код из ГИТа') { steps { checkout scm } } stage('Собираем') { steps { sh 'chmod +x ./gradlew' sh './gradlew build -x test' } } stage('Тестируем') { steps { script { sh './gradlew test' } } } }
} #_ Этап сборки нового Docker-образа и его загрузки с систему Artifactory:
node { stage('Собираем образ') { docker.withRegistry("https://repo.artifactory.bank", "LoginToArtifactory") { def dkrImg = docker.build("repo.artifactory.bank/dev-backend:${env.BUILD_ID}") dkrImg.push() dkrImg.push('latest') } } stage('Заливаем его в Artifactory') { docker.withRegistry("https://repo.artifactory.bank", "LoginToArtifactory") { sh "docker service update --image repo.artifactory.bank/dev-backend:${env.BUILD_ID} SMB_dev-backend" } }
}

Отдельно хотелось бы отметить, что при сборке образов каждая версия получает свой тег (метку), что значительно облегчает процесс авторестарта приложения.

2.2 Portainer

Для облегчения взаимодействия всех членов команды с Docker в проекте нами был использован простой веб-интерфейс для него – Portainer. Данное приложение, так же, как и сам Docker, написано на языке Go, а поэтому отличается высокой производительностью при чрезвычайно легком развертывании.

Например, в простейшем случае следующая команда приведет к запуску Портейнера на порту 9000 хост-системы:

docker run -d \ -p 9000:9000 \ -v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer

Однако, в текущем проекте было решено воспользоваться функциональностью средства «оркестрации» для одного хоста – Docker Compose.

2.3 Docker-контейнеры и сервисы

Все необходимые приложения и сервисы в данном проекте запускаются посредством простого файла docker-compose.yml.

Базовый набор «инфраструктурных» сервисов запускается посредством следующего описания:

version: '3.4' services: # Непосредственно общающийся с пользователями NGINX nginx: image: "nginx:1" container_name: fe-nginx restart: always volumes: - /APP/configs/nginx.conf:/etc/nginx/nginx.conf - /APP/logs/nginx:/var/log/nginx - /usr/share/zoneinfo/Europe/Moscow:/etc/localtime:ro networks: - int ports: - "80:80/tcp" - "8080:80/tcp" # Jenkins CI - сервер, который отвечает за CI/CD-процесс ci: image: "jenkins/jenkins:lts" container_name: ci-jenkins restart: always volumes: - /usr/share/zoneinfo/Europe/Moscow:/etc/localtime:ro - /APP/jenkins/master:/var/jenkins_home environment: JENKINS_OPTS: '--prefix=/jenkinsci' JAVA_OPTS: '-Xmx512m' networks: int: aliases: - srv-ci # Веб-интерфейс для Docker-а portainer: image: "portainer/portainer:latest" volumes: - type: bind source: /var/run/docker.sock target: /var/run/docker.sock - type: bind source: /APP/portainer_data target: /data networks: int: aliases: - srv-portainer command: -H 'unix:///var/run/docker.sock' networks: int: external: true

2.4 Docker Swarm-кластер без кластера

Как можно видеть в файле docker-compose.yml выше, во-первых, отсутствуют упоминания бэкенд и фронтенд- частей приложения, а также присутствует ссылка на «внешнюю» (external: true) сеть по имени int. Внешними являются любые ресурсы (сети, тома и другие существующие сущности), не объявленные в одном файле.

Данная возможность реализуется через возможность изменить требуемый образ у запущенного сервиса, и при наличии новой версии образа в репозитории перезапуск произойдет автоматически. Дело в том, что в проекте нам требовалась иметь возможность проводить рестарт «сервисов» при обновлении версии образа в Docker-репозитории Artifactory, а подобные функции присутствуют в сервисах Docker Swarm (встроенная в Docker multi-master (система оркестрации Docker-контейнеров) что называется «из коробки». В случае же, если версия не изменилась – сервисный контейнер продолжает штатно выполняться.

Это было достигнуто через создание на сервере кластерой Overlay-сети, в которую включались и базовые сервисы, описанные выше, и непосредственно компоненты приложения: Что касается сети, запуская приложение в виде сервиса Docker Swarm (yaml-описание представлено ниже), нам требовалось сохранить сетевую связность его компонентов и сервера NGINX, объявленного выше.

docker network create -d overlay --subnet 10.1.2.254/24 --attachable int

(ключ --attachable является необходимым, т.к. без него базовые сервисы не имеют доступа к кластерной сети)

Описание компонентов приложения c двумя сервисами:

version: '3.2' services: pre-live-backend: image:repo.artifactory.bank/dev-backend:latest deploy: mode: replicated replicas: 1 networks: - int pre-live-front: image: repo.artifactory.bank/dev-front:latest deploy: mode: replicated replicas: 1 networks: - int
networks: int: external: true

Заключение

Как было отмечено в начале, на старте проекта команде хотелось получить все преимущества DevOps-подхода, в частности организовать процесс непрерывной доставки кода от гит-репозитория до «боевого» сервера в виде запущенного на нем приложения. При этом на текущем этапе не хотелось полностью уходить от наработанных практик и перестраивать себя под жизнь в мире больших оркестраторов. Описанная архитектура системы, которая была продумана и реализована менее, чем за 2 недели (параллельно с другими проектами, над которыми трудились члены команды), в итоге позволила нам достичь желаемого. Мы полагаем, что данный материал должен быть интересным и полезным и другим командам, внедряющим DevOps-подходы в жизнь.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Что мы узнали о безопасности Intel ME за последние годы: 7 фактов о таинственной подсистеме

Изображение: Unsplash Эту технологию окружает ореол таинственности – несмотря на то, что она имеет доступ практически ко всем данным на компьютере и ее компрометация позволяет захватить полный контроль над машиной, официальной документации и руководств по работе с ней от производителя ...

Орден куколки

Прошу считать пятничным постом! Это как бы научная фантастика, но правда важный дисклеймер 1. Научная фантастика понятие очень широкое. Этот рассказ-эссе я смею предложить только тем, кто нормально относится к углу . Простите меня, если в вашем представлении научная фантастика ...