Пример создания Continuous Integration/Continuous Delivery процесса для проекта, написанного на Java, c использованием Gradle, в качестве инструмента сборки Java, Docker, Docker-compose в качестве контейнеризации приложения, Ansible в качестве системы управления/настройки staging-сервера и запуска docker-compose-файла
Реализация этой связки описана в книге Сontinuous delivery with Docker and Jenkins by Rafal Leszko
Алгоритм действий:
1.Создание Java-проекта, который будет собираться Gradle-ом
2.Создание Unit-теста
3.Анализ покрытия кода тестами и публикация результатов отчета
4.Создание Docker-образа c созданным jar-файлом и загрузка его на Docker-репозитарий(на основе Nexus Sonatype)
5.Создание простого acceptance-теста для тестирования приложения на staging-сервере
6.Добавление использования Docker-compose в Jenkins-pipeline для запуска нескольких контейнеров
a) выполнение/запуск acceptance-тестирования непосредственно с staging-сервера
b) выполнение/запуск acceptance-тестирования с отдельного docker-контейнера
7.Добавление использования Ansible в Jenkins-pipeline для автоматической установки docker,docker-compose и запуска docker-compose.yaml файла на staging-сервере
8.Версионирование собранных Docker-образов
1.Создание Java-проекта, который будет собираться Gradle-ом
https://start.spring.io/
Скачиваем полученный zip-архив и распаковываем его содержимое в репозитарий
https://bitbucket.org/mybitbucketuser/calculator/src/master/
Также сделаем копию каталога-репозитария и удалим оттуда .git и .gitignore для ручного выполнения gradle-команд с целью проверки корректности их выполнения
1 |
# cp -pr /root/git/calculator /root/git/calculator-cli |
1 |
# cd /root/git/calculator-cli && rm -rf .git .gitignore |
Проверяем корректность компилляции
1 |
# ./gradlew compileJava |
2.Создание Unit-теста
А) добавление/создание бизнес-логики
1 |
# nano src/main/java/com/kamaok/calculator/Calculator.java |
1 2 3 4 5 6 7 8 9 |
package com.kamaok.calculator; import org.springframework.stereotype.Service; @Service public class Calculator { int sum(int a, int b) { return a + b; } } |
1 |
# nano src/main/java/com/kamaok/calculator/CalculatorController.java |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.kamaok.calculator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController class CalculatorController { @Autowired private Calculator calculator; @RequestMapping("/sum") String sum(@RequestParam("a") Integer a, @RequestParam("b") Integer b) { return String.valueOf(calculator.sum(a, b)); } } |
Проверяем работу бизнес-логики
1 |
# ./gradlew bootRun |
1 |
# netstat -nlptu | grep 8080 |
1 |
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 24473/java |
В браузере набираем
1 |
http://myjenkinsslaveservername:8080/sum?a=1&b=2 |
Либо
1 |
# curl http://localhost:8080/sum?a=1\&b=2 |
Страница должна отдать 3
Б) Создание Unit-теста
1 |
# nano src/test/java/com/kamaok/calculator/CalculatorTest.java |
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.kamaok.calculator; import org.junit.Test; import static org.junit.Assert.assertEquals; public class CalculatorTest { private Calculator calculator = new Calculator(); @Test public void testSum() { assertEquals(5, calculator.sum(2, 3)); } } |
Копируем созданные нами файлы с тестового каталога(/root/git/calculator-cli) в каталог, который версионирован(под контролем) git(/root/git/calculator)
1 |
# cp -p /root/git/calculator-cli/src/main/java/com/kamaok/calculator/Calculator.java /root/git/calculator/src/main/java/com/kamaok/calculator/Calculator.java |
1 |
# cp -p /root/git/calculator-cli/src/main/java/com/kamaok/calculator/CalculatorController.java /root/git/calculator/src/main/java/com/kamaok/calculator/CalculatorController.java |
1 |
# cp -p /root/git/calculator-cli/src/test/java/com/kamaok/calculator/CalculatorTest.java /root/git/calculator/src/test/java/com/kamaok/calculator/CalculatorTest.java |
Коммитим изменения в репозитарий и запускам pipeline-сборку
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
pipeline { agent { label 'linux-slave1' } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/calculator.git', branch: 'master' } } stage("Compile") { steps { sh "./gradlew compileJava" } } stage("Unit test") { steps { sh "./gradlew test" } } } } |
3.Анализ покрытия тестами кода и публикация результатов отчета
Например, добавим требование на минимальное покрытие тестами кода — не менее 20%, чтобы сборка не считалась проваленной.
1 |
# cd /root/git/calculator-cli |
1 |
# nano build.gradle |
1 2 3 4 5 6 7 8 9 10 |
apply plugin: "jacoco" jacocoTestCoverageVerification { violationRules { rule { limit { minimum = 0.2 } } } } |
Проверим уровень покрытия тестами кода
1 |
# ./gradlew test jacocoTestCoverageVerification |
Генерация отчета о покрытия тестами
1 |
# ./gradlew test jacocoTestReport |
Отчеты генерируются в файл
1 |
build/reports/jacoco/test/html/index.html |
Копируем созданный нами файл с тестового каталога(/root/git/calculator-cli) в каталог, который версионирован(под контролем) git(/root/git/calculator)
1 |
# cp -p /root/git/calculator-cli/build.gradle /root/git/calculator/build.gradle |
Добавляем покрытие кода в pipeline
1 2 3 4 5 6 |
stage("Code coverage") { steps { sh "./gradlew jacocoTestReport" sh "./gradlew jacocoTestCoverageVerification" } } |
Публикация результатов отчета покрытия кода(требуется предварительная установка плагина HTML Publisher)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
stage("Code coverage") { steps { sh "./gradlew jacocoTestReport" publishHTML (target: [ reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: "JaCoCo Report" ]) sh "./gradlew jacocoTestCoverageVerification" } } |
1 |
# cd /root/git/calculator/ |
Коммитим изменения в репозитарий и запускам pipeline-сборку
4.Создание Docker-образа c созданным jar-файлом и загрузка его на Docker-репозитарий(на основе Nexus Sonatype)
Создание Credentials с логином/паролем для доступа к удаленному Docker-репозитарию
Сборка Docker-образа и его загрузка в Docker-репозитарий(на основе Nexus Sonatype) будет производиться на основе Jenkins-slave/agent, в качестве которого выступает standalone Linux сервер(на нем предварительно необходимо установить Docker)
Для того,чтобы jenkins-пользователь, под которым Jenkins-мастер подключается к Jenkins-slave и выполняет весь цикл pipeline имел доступ к Docker-сокету, необходимо включить пользователя jenkins в группу docker на jenkins-slave сервере
Проверка прав и владельца/группы docker-сокета
1 |
# ls -l /var/run/docker.sock |
1 |
srw-rw---- 1 root docker 0 Oct 9 14:35 /var/run/docker.sock |
Добавление пользователя jenkins в группу docker на Jenkins-slave сервере
1 |
# usermod -aG docker jenkins |
1 |
# grep jenkins /etc/group |
1 2 |
docker:x:999:jenkins jenkins:x:1002: |
Перед добавление команд в pipeline проверим их через командную строку вручную
1 |
# cd /root/git/calculator-cli |
Создние jar-пакета
1 |
# ./gradlew build |
1 |
# ls -l build/libs/ |
1 2 |
total 15856 -rw-r--r-- 1 root root 16236422 Nov 5 18:11 calculator-0.0.1-SNAPSHOT.jar |
Создание Docker-файла, с помощью которого собирается Docker-образ, в котором запускается созданный на предыдущем шаге jar-файл
1 |
# nano Dockerfile |
1 2 3 |
FROM frolvlad/alpine-oraclejdk8:slim COPY build/libs/calculator-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] |
Сборка Docker-образа
1 |
# docker build -t calculator . |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Sending build context to Docker daemon 16.67MB Step 1/3 : FROM frolvlad/alpine-oraclejdk8:slim slim: Pulling from frolvlad/alpine-oraclejdk8 4fe2ade4980c: Pull complete a0290d5a7317: Pull complete 1d8a043e07b3: Pull complete Digest: sha256:a51161fd28d21add32482e3852c6fa2344ff64bcc6472aaccf02a047cfcc1171 Status: Downloaded newer image for frolvlad/alpine-oraclejdk8:slim ---> 3ee5e1ce00fc Step 2/3 : COPY build/libs/calculator-0.0.1-SNAPSHOT.jar app.jar ---> acbb9049b9a4 Step 3/3 : ENTRYPOINT ["java", "-jar", "app.jar"] ---> Running in 0e39c244a30b Removing intermediate container 0e39c244a30b ---> a62746401887 Successfully built a62746401887 Successfully tagged calculator:latest |
Запускаем docker-контейнер из созданного на предыдущем шаге образа и проверка в браузере корректного ответа http://myjenkinsslaveservername:8765/sum?a=1&b=2
1 |
# docker run -p 8765:8080 --name calculator calculator |
Если в браузере получаем ответ 3, значит приложение корректно запустилось в контейнере
Копируем в основной репозитарий в корень файл Dockerfile
1 |
# cp -p /root/git/calculator-cli/Dockerfile /root/git/calculator/ |
Коммитим изменения и загружаем их в удаленный репозитарий
1 |
# cd /root/git/calculator/ && git add Dockerfile && git commit -m "Added Dockerfile" && git push |
Добавляем в pipeline команды по сборке jar-пакета(stage «Package») создания docker-образа(«Docker build»), загрузки собранного docker-образа на удаленный Docker-репозитарий(stage»Docker push»), запуск контейнера из собранного образа (stage «Deploy to staging»)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
stage("Package") { steps { sh "./gradlew build" } } stage("Docker build") { steps { sh "docker build -t mydocker.repo.servername/calculator ." } } stage("Docker push") { steps { withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker login --username ${DOCKER_USER} --password ${DOCKER_PASSWORD} https://mydocker.repo.servername" sh "docker push mydocker.repo.servername/calculator" } } } stage("Deploy to staging") { steps { sh "docker run -d --rm -p 8765:8080 --name calculator mydocker.repo.servername/calculator" } } |
В нашем примере staging-сервер(на котором выполняются стадии Continuous Delivery) и сервер, на котором собирается проект и создается контейнер(т.е. выполняются стадии Continuous Integration) один и тот же(например, это standalone Linux-сервер на котором установлен Docker, этот сервер выступает в качестве Jenkins-slave ноды/агента
Если CI выполняется на одном сервере, а CD – на другом(как показано на изображении ниже, то перед стадией stage(«Deploy to staging») добавляем ноду/агента, на котором необходимо выполнять стадии CD
Например, где ‘jenkins-slave-linux’ – метка Jenkins-slave-сервера, на котором необходимо выполнить все последующие стадии CD начиная со стадии stage(«Deploy to staging»)
1 2 3 |
agent { node { label 'jenkins-slave-linux' } } stage("Deploy to staging") { … |
Запускаем pipeline-сборку и проверяем доступность раздеплоенного приложения
1 |
# netstat -nlptu | grep 8765 |
1 |
tcp6 0 0 :::8765 :::* LISTEN 10799/docker-proxy |
1 |
# docker ps |
1 2 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d3bd376acf45 mydocker.repo.servername/calculator "java -jar app.jar" 4 minutes ago Up 4 minutes 0.0.0.0:8765->8080/tcp calculator |
5.Создание простого acceptance-теста для тестирования приложения на staging-сервере
В корне репозитария создаем файл acceptance_test.sh, который будет выполнять acceptance-тест
Например, проверка вывода команды сложения двух чисел(1 и 2)
1 |
# nano acceptance_test.sh |
1 2 |
#!/bin/bash [ $(curl localhost:8765/sum?a=1\&b=2) -eq 3 ] |
Выставляем бит исполнения на скрипт
1 |
# chmod +x acceptance_test.sh |
Коммитим изменения и загружаем их в удаленный репозитарий
1 |
# git add acceptance_test.sh && git commit -m "Added acceptance_test.sh" && git push |
Добавляем в pipeline команды, которые выполняют простой acceptance test(stage «Acceptance test»), и очищают stage-окружение путем остановки докер-контейнера, который использовался для acceptance-тестов (стадия post)
Т.к. на стадии Deploy to staging Docker-контейнер был запущен с опцией —rm, то при остановке Docker-контейнера, он будет автоматически удален.
Это позволит не хранить остановленые докер-контейнеры на staging-окружении и тем самым не занимать дисковое пространство staging-сервера
Т.к. в секции post используется опция always, то команда(по остановке docker-контейнера) будет выполнена всегда, независимо от того, успешно ли завершилась ранее запущенный stage
На стадии Acceptance test выдерживается пауза в 15 секунд(в виде опции sleep), необходимая для запуска контейнера, на котором будет запущен acceptance-тест
1 2 3 4 5 6 7 8 9 10 11 12 |
stage("Acceptance test") { steps { sleep 15 sh "./acceptance_test.sh" } } post { always { sh "docker stop calculator" } } |
Общий pipeline имеет вид
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
pipeline { agent { label 'linux-slave1' } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/calculator.git', branch: 'master' } } stage("Compile") { steps { sh "./gradlew compileJava" } } stage("Unit test") { steps { sh "./gradlew test" } } stage("Code coverage") { steps { sh "./gradlew jacocoTestReport" publishHTML (target: [ reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: "JaCoCo Report" ]) sh "./gradlew jacocoTestCoverageVerification" } } stage("Package") { steps { sh "./gradlew build" } } stage("Docker build") { steps { sh "docker build -t mydocker.repo.servername/calculator ." } } stage("Docker push") { steps { withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker login --username ${DOCKER_USER} --password ${DOCKER_PASSWORD} https://mydocker.repo.servername" sh "docker push mydocker.repo.servername/calculator" } } } stage("Deploy to staging") { steps { sh "docker run -d --rm -p 8765:8080 --name calculator mydocker.repo.servername/calculator" } } stage("Acceptance test") { steps { sleep 15 sh "./acceptance_test.sh" } } } post { always { sh "docker stop calculator" } } } |
Выполняем сборку и проверяем успешность выполнения всех стадий pipeline
6.Добавление Docker-compose в Jenkins-pipeline
В реальной жизни приложения часто использует несколько зависимостей для своей работы(базы данных, кеширование, сервера очередей и т.д.)
Здесь на помощь приходит Docker Compose, управляющий много-контейнерными приложениями.
Docker Compose обеспечивает зависимости между контейнерам т.е. он связывает
один контейнер с другим благодаря тому, что контейнеры находятся в одной и той же сети и видны друг для друга по сети.
Установим docker-compose на staging-сервер( на котором будут запускаться несколько контейнеров и производиться acceptance-тестирование)
В данном случае в качестве staging-сервера используется все тот же Jenkins-slave-сервер, на котором и происходит все стадии CI/CD
Установка docker-compose
Последняя версия доступна здесь
https://github.com/docker/compose/releases
1 |
# curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose |
1 |
# chmod +x /usr/local/bin/docker-compose |
Установка docker-compose автодополнения команд
1 |
# curl -L https://raw.githubusercontent.com/docker/compose/1.23.1/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose |
https://docs.docker.com/compose/completion/#install-command-completion
1 |
# source /etc/bash_completion.d/docker-compose |
1 |
# docker-compose --version |
1 |
docker-compose version 1.23.1, build b02f1306 |
Например, приложения calculator использует Redis для кеширования, Redis — отдельный контейнер который будет запускаться через docker-compose
a) выполнение/запуск acceptance-тестирования непосредственно с staging-сервера
Acceptance-тестирование можно провести двумя способами
Bash-скрипт, с помощью которого выполняется acceptance-тестирование можно запускать непосредственно с сервера, на котором запускаются контейнеры с приложением(например, это может быть Jenkins-slave(или Jenkins-master,если не используются Jenkins slave/agent))
Аналогично тому, как мы запускали acceptance-тестирования, когда использовали только один контейнер приложением (без зависимостей типа Redis), только вместо doсker-команд для работы с docker-контейнерами будет использоваться docker-compose
Создадим docker-compose.yaml файл в корне репозитария
1 |
# nano docker-compose.yaml |
1 2 3 4 5 6 7 8 |
version: "3" services: calculator: image: mydocker.repo.servername/calculator:latest ports: - "8080" redis: image: redis:latest |
Чтобы иметь возможность масштабировать приложения путем запуска несколько экземпляров docker-контейнеров с приложением мы преднамеренно не указывем порт открываемый на хосте, который должен выставляться/прослушиваться для проброса на порт в контейнере(8080)
Docker самостоятельно выберет случайный свободный порт на хосте для этого и при необходимости запуска более одного экземпляра Docker-контейнера с приложением мы не получим ошибку, что порт на хосту уже занят(первым контейнером)
Поэтому отредактируем скрипт, в котором происходит acceptance-тестирование
Определим номер динамически выбранного Docker-ом порта для приложения и используем этот порт в проверке curl
1 |
# nano acceptance_test.sh |
1 2 3 4 |
#!/bin/bash CALCULATOR_PORT=$(docker-compose port calculator 8080 | cut -d: -f2) [ $(curl localhost:${CALCULATOR_PORT}/sum?a=1\&b=2) -eq 3 ] |
Коммитим изменения в репозитарий
1 |
# git add docker-compose.yaml acceptance_test.sh && git commit -m "Added docker-compose.yaml and changed acceptance_test.sh" && git push |
Сделаем изменения в pipeline
Используем docker-compose вместо docker в двух местах
При деплои приложения на stage-сервер вместо
1 2 3 4 5 6 7 8 9 10 11 12 |
/*stage("Deploy to staging") { steps { sh "docker run -d --rm -p 8765:8080 --name calculator mydocker.repo.servername/calculator" } } */ stage("Deploy to staging") { steps { sh "docker-compose up -d" } } |
Для остановки и удаления всех контейнеров, запущенных для acceptance-тестирования
1 2 3 4 5 6 |
post { always { /*sh "docker stop calculator"*/ sh "docker-compose down" } } |
и запускаем pipeline-сборку и проверяем корректность отработки команд касательно docker-compose и успешного acceptance-тестирования
b) выполнение/запуск acceptance-тестирования с отдельного docker-контейнера
Вторым способом запуска acceptance-тестирования может быть запуск bash-скрипта, с помощью которого выполняется acceptance-тестирование, ВНТУРИ ОТДЕЛЬНОГО docker-контейнера
Т.е. на Docker-хосте создается отдельный docker-контейнер, на котором будет запущен bash-скрипт для acceptance-тестирования приложения
При таком подходе не требуется вычисление/определение порта, на котором будет запущено приложение т.к. этот порт внутри контейнера постоянный (в нашем случае 8080), а контейнер со скриптом тестирования и контейнер с приложением будут запущены в одной подсети и внутренним механизмом docker-compose доступны один одному. В скрипте с тестом в качестве имени сервера/ip-адреса сервера будет использоваться имя сервиса, указанным в корневом docker-compose.yaml файле ( в нашем случае calculator)
Корневые Dockerfile и docker-compose.yaml файлы остаются неизменными
1 |
# cat Dockerfile |
1 2 3 |
FROM frolvlad/alpine-oraclejdk8:slim COPY build/libs/calculator-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] |
1 |
# cat docker-compose.yaml |
1 2 3 4 5 6 7 8 |
version: "3" services: calculator: image: mydocker.repo.servername/calculator:latest ports: - "8080" redis: image: redis:latest |
Для создания отдельного контейнера с acceptance-тест создадим отдельный каталог acceptance внутри котрого создадим файлы Dockerfile и docker-compose-acceptance.yaml
Для начала выполним это в в тестовом окружении (/root/git/calculator-cli)(который не под контролем git), после успешного запуска перенесем созданные файлы/каталоги в локальную рабочую копию репозитария(/root/git/calculator).
1 |
# cd /root/git/calculator-cli |
1 |
# mkdir acceptance |
Создание Dockerfile для образа использумого в acceptance-тесте
Сборка образа на основе ubuntu, установка curl, копирование и запуск тестового скрипта
1 |
# nano acceptance/Dockerfile |
1 2 3 4 |
FROM ubuntu:trusty RUN apt-get update && apt-get install -yq curl COPY test.sh . CMD ["bash", "test.sh"] |
Создание docker-compose.yml файла для acceptance-теста
Сборка и запуск контейнера с именем test, созданного из образа, собранного из Dockerfile в каталоге acceptance
1 |
# nano acceptance/docker-compose-acceptance.yaml |
1 2 3 4 |
version: "3" services: test: build: ./acceptance |
Создание скрипта для acceptance-тест
1 |
# nano acceptance/test.sh |
1 2 3 |
#!/bin/bash sleep 15 [ $(curl calculator:8080/sum?a=1\&b=2) -eq 3 ] |
Запуск acceptance-теста
1 |
# docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance up -d --build |
Проверка запущенных контейнеров
1 |
# docker ps |
1 2 3 4 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES eeea86e3225d redis:latest "docker-entrypoint.s…" 18 seconds ago Up 14 seconds 6379/tcp acceptance_redis_1_4b6015a0ae90 4eaed615f017 acceptance_test "bash test.sh" 18 seconds ago Up 15 seconds acceptance_test_1_b4dbf9cf6f60 d06b50f3e3f4 mydocker.repo.servername/calculator:latest "java -jar app.jar" 18 seconds ago Up 15 seconds 0.0.0.0:32772->8080/tcp acceptance_calculator_1_66d377fc5cde |
Нас интересует контейнер с тестами, в данном случае он имеет имя
acceptance_test_1_b4dbf9cf6f60
Лог контейнера сообщает о успешном вызове команды curl
1 |
# docker logs acceptance_test_1_b4dbf9cf6f60 |
1 2 3 |
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1 100 1 0 0 1 0 0:00:01 --:--:-- 0:00:01 1 |
Для просмотра результата выполнения команды curl определяем код выхода контейнера
1 |
# docker wait acceptance_test_1_b4dbf9cf6f60 |
1 |
"0" |
Код выхода контейнера 0(ноль) означает, что тест прошел успешно
Также с Docker-хоста/ноды можно проверить корректность работы приложения, запросив порт, который используется на Docker-хосте для проброса запроса на контейнер с приложением
1 |
# docker port acceptance_calculator_1_66d377fc5cde |
1 |
8080/tcp -> 0.0.0.0:32772 |
1 |
# curl localhost:32772/sum?a=1\&b=2 |
1 |
3 |
Удаляем созданные docker-compose-ом контейнеры
1 |
# docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance down |
Копируем созданный в тестовом окружении (/root/git/calculator-cli) в локальную рабочую копию репозитария(/root/git/calculator).
1 |
# cp -pr /root/git/calculator-cli/acceptance /root/git/calculator/ |
Коммитим изменения в репозитарий
1 |
# git add acceptance && git commit -m "Added directory acceptance" && git push |
Изменяем pipeline
Вместо стадий
1 2 |
stage("Deploy to staging") stage("Acceptance test") |
используем одну новую стадию
Сначало мы собирем образ для тестового контейнера (служба test). Затем запускаем все три контейнера (calculator,redis,test), после чего проверяем код выхода контейнера с тестом acceptance_test_1_*)
1 2 3 4 5 6 7 |
stage("Acceptance test") { steps { sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml build test" sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance up -d" sh '[ $(docker wait $(docker ps -a --format {{.Names}} | grep acceptance_test)) -eq 0 ]' } } |
А также изменим опции post, в которой остановим и удалим все запущенные на предыдущем шаге контейнеры
1 2 3 4 5 6 7 |
post { always { /*sh "docker stop calculator"*/ /*sh "docker-compose down"*/ sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance down" } } |
Общий итоговый pipeline имеет вид
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
pipeline { agent { label 'linux-slave1' } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/calculator.git', branch: 'master' } } stage("Compile") { steps { sh "./gradlew compileJava" } } stage("Unit test") { steps { sh "./gradlew test" } } stage("Code coverage") { steps { sh "./gradlew jacocoTestReport" publishHTML (target: [ reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: "JaCoCo Report" ]) sh "./gradlew jacocoTestCoverageVerification" } } stage("Package") { steps { sh "./gradlew build" } } stage("Docker build") { steps { sh "docker build -t mydocker.repo.servername/calculator ." } } stage("Docker push") { steps { withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker login --username ${DOCKER_USER} --password ${DOCKER_PASSWORD} https://mydocker.repo.servername" sh "docker push mydocker.repo.servername/calculator" } } } stage("Acceptance test") { steps { sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml build test" sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance up -d" sh '[ $(docker wait $(docker ps -a --format {{.Names}} | grep acceptance_test)) -eq 0 ]' } } } post { always { sh "docker-compose -f docker-compose.yaml -f acceptance/docker-compose-acceptance.yaml -p acceptance down" } } } |
Запускаем pipeline и проверяем корректность отработки шагов на stage(«Acceptance test»)
7.Добавление использования Ansible для установки docker,docker-compose и выполнения docker-compose.yaml файла на staging-сервере
Установка ansible на Jenkins-slave, на котором выполняется сборка
1 |
# apt-get update |
1 |
# apt-get install software-properties-common |
1 |
# apt-add-repository --yes --update ppa:ansible/ansible |
1 |
# apt-get install ansible |
1 |
# ansible --version |
1 2 3 4 5 6 |
ansible 2.7.1 config file = /etc/ansible/ansible.cfg configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python2.7/dist-packages/ansible executable location = /usr/bin/ansible python version = 2.7.12 (default, Dec 4 2017, 14:50:18) [GCC 5.4.0 20160609] |
На staging-сервере(у нас это Jenkins-slave) желательно предварительно проверить уже установленные модули python относительно docker
1 |
# pip list | grep -i docker |
1 2 3 4 |
docker 3.5.1 docker-compose 1.23.1 docker-pycreds 0.3.0 dockerpty 0.4.1 |
Удаляем 3 указанных модуля(docker, docker-compose, docker-py),если они уже установлены в системе(ansible сам установить нужные модули соглаcно своего playbook-а)
1 |
# pip uninstall docker docker-compose docker-py |
Проверяем, что указанные модули удалились
1 |
# pip list | grep -i docker |
1 2 |
docker-pycreds 0.3.0 dockerpty 0.4.1 |
Создадим playbook, который будет устанавливать на staging-сервере docker,docker-compose, копировать файл docker-compose.yaml на staging-сервер и выполнять его там(полагается,что staging-сервер работает под Ubuntu16.04)
1 |
# nano redis-calculator-playbook.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
--- - hosts: staging become: yes become_method: sudo tasks: - name: Add Docker GPG key apt_key: url=https://download.docker.com/linux/ubuntu/gpg - name: Add Docker APT repository apt_repository: repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ansible_distribution_release}} stable - name: Install list of packages apt: name: "{{ item }}" state: present update_cache: yes with_items: - apt-transport-https - ca-certificates - curl - software-properties-common - docker-ce - name: install python-pip apt: name: python-pip update_cache: yes state: present - name: install Docker-compose pip: name: docker-compose version: 1.23.1 - name: copy docker-compose.yaml copy: src: ./docker-compose.yaml dest: ./docker-compose.yaml - name: run docker-compose docker_service: project_src: . state: present |
Inventory-файл имеет вид
1 |
# nano inventory/staging |
1 2 |
[staging] localhost ansible_host=127.0.0.1 ansible_port=2222 ansible_user=jenkins |
Также нужно предоставить беспарольный(по SSH-ключам) доступ под пользователем jenkins на staging-сервер, на котором ансиблом запускается docker-compose( в даном случае jenkins-slave выполняет роль также staging-сервера) + добавить пользователя jenkins в /etc/sudoers
1 |
jenkins ALL=(ALL) NOPASSWD:ALL |
Чтобы после подключения под пользователем jenkins повысить привиллегии до пользователя root без запроса пароля при отработки ansible-плейбука
После чего проверить с командной строки Jenkins-slave-сервера
А) переключиться на jenkins-пользователя и проверить подключение под ним на staging-сервер по SSH-ключам
1 |
# su -l jenkins |
1 |
jenkins@jenkins-slave:~$ ssh -p2222 localhost |
1 |
exit (выходим c staging-сервера) |
1 |
exit (переключаемся обратно с jenkins на root) |
Отключение в Ansible предложения по добавлению ключей SSH в файл known_hosts
1 |
# nano /etc/ansible/ansible.cfg |
1 |
host_key_checking = False |
Альтернативным вариантом является установка переменной
1 |
ANSIBLE_HOST_KEY_CHECKING=False |
Docker-compose.yaml имеет вид
1 |
# cat docker-compose.yaml |
1 2 3 4 5 6 7 8 |
version: "3" services: calculator: image: mydocker.repo.servername/calculator:latest ports: - "8080" redis: image: redis:latest |
Проверяем синтаксис playbook-а
1 |
# ansible-playbook redis-calculator-playbook.yaml --syntax-check |
Выполняем playbook
1 |
# ansible-playbook redis-calculator-playbook.yaml |
Проверяем наличие запущенных контейнеров
1 2 3 4 |
# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 046e0ffa50ea mydocker.repo.servername/calculator:latest "java -jar app.jar" 47 seconds ago Up 44 seconds 0.0.0.0:32776->8080/tcp jenkins_calculator_1_b01f14b1b8ec 64f3754c1821 redis:latest "docker-entrypoint.s…" 47 seconds ago Up 45 seconds 6379/tcp jenkins_redis_1_ef587f11b3f8 |
Проверяем с Jenkins-slave-сервера, на котором выполнили playbook
1 |
# curl localhost:32776/sum?a=1\&b=2 |
1 |
3 |
Список установленных ansible-ом python модулей
1 |
# pip list | grep -i docker |
1 2 3 4 |
docker 3.5.1 docker-compose 1.23.1 docker-pycreds 0.3.0 dockerpty 0.4.1 |
Pipeline по разворачивание приложения на staging-сервере будет иметь вид
1 2 3 4 5 |
stage("Deploy to staging") { steps { sh "ansible-playbook redis-calculator-playbook.yaml -i inventory/staging" } } |
Скрипт по тестированию доступности приложения имеет вид
1 |
# cat acceptance_test.sh |
1 2 3 4 5 |
#!/bin/bash CALCULATOR_PORT=$(docker port $(docker ps --format {{.Names}} | grep calculator) | cut -d : -f2) [ $(curl localhost:${CALCULATOR_PORT}/sum?a=1\&b=2) -eq 3 ] |
Общий pipeline будет иметь вид
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
pipeline { agent { label 'linux-slave1' } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/calculator.git', branch: 'master' } } stage("Compile") { steps { sh "./gradlew compileJava" } } stage("Unit test") { steps { sh "./gradlew test" } } stage("Code coverage") { steps { sh "./gradlew jacocoTestReport" publishHTML (target: [ reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: "JaCoCo Report" ]) sh "./gradlew jacocoTestCoverageVerification" } } stage("Package") { steps { sh "./gradlew build" } } stage("Docker build") { steps { sh "docker build -t mydocker.repo.servername/calculator:latest ." } } stage("Docker push") { steps { withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker login --username ${DOCKER_USER} --password ${DOCKER_PASSWORD} https://mydocker.repo.servername" sh "docker push mydocker.repo.servername/calculator:latest" } } } stage("Deploy to staging") { steps { sh "ansible-playbook redis-calculator-playbook.yaml -i inventory/staging" } } stage("Acceptance test") { steps { sleep 15 sh "./acceptance_test.sh" } } } } |
8.Версионирование собранных Docker-образов
Добавим временную метку, например в формате yyyy-MM-dd-HH-mm, в качестве тега Docker-образа
Установим плагин Build Timestamp Plugin
Установим формат даты в Jenkins-настройках
Например, в формате
1 |
yyyy-MM-dd-HH-mm |
1 |
Jenkins->Configure Jenkins->Configure system |
Во всех местах,где используется Docker-образ добавим соответствующий тег в виде переменной ${BUILD_TIMESTAMP}
1 |
sh "docker build -t mydocker.repo.servername/calculator:${BUILD_TIMESTAMP} ." |
1 |
sh "docker push mydocker.repo.servername/calculator:${BUILD_TIMESTAMP}" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
stage("Docker build") { steps { sh "docker build -t mydocker.repo.servername/calculator:${BUILD_TIMESTAMP} ." } } stage("Docker push") { steps { withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker login --username ${DOCKER_USER} --password ${DOCKER_PASSWORD} https://mydocker.repo.servername" sh "docker push mydocker.repo.servername/calculator:${BUILD_TIMESTAMP}" } } } ... |
Docker-compose.yaml файл приводим к виду:
1 |
# cat docker-compose.yaml |
1 2 3 4 5 6 7 8 |
version: "3" services: calculator: image: mydocker.repo.servername/calculator:${BUILD_TIMESTAMP} ports: - "8080" redis: image: redis:latest |
Скрипт для проверки доступности приложения остается неизменным
1 |
# cat acceptance_test.sh |
1 2 3 4 |
#!/bin/bash CALCULATOR_PORT=$(docker port $(docker ps --format {{.Names}} | grep calculator) | cut -d : -f 2) [ $(curl localhost:${CALCULATOR_PORT}/sum?a=1\&b=2) -eq 3 ] |
Ansible плейбук приводим к виду:
1 |
# cat redis-calculator-playbook.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
--- - hosts: staging become: yes become_method: sudo tasks: - name: Add Docker GPG key apt_key: url=https://download.docker.com/linux/ubuntu/gpg - name: Add Docker APT repository apt_repository: repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ansible_distribution_release}} stable - name: Install list of packages apt: name: "{{ item }}" state: present update_cache: yes with_items: - apt-transport-https - ca-certificates - curl - software-properties-common - docker-ce - name: install python-pip apt: name: python-pip update_cache: yes state: present - name: install Docker-compose pip: name: docker-compose version: 1.23.1 - name: copy docker-compose.yaml copy: src: ./docker-compose.yaml dest: ./docker-compose.yaml - name: run docker-compose environment: BUILD_TIMESTAMP: "{{ lookup('env', 'BUILD_TIMESTAMP') }}" docker_service: project_src: . state: present |
Коммитим изменения в репозитарий и запускаем pipeline-сборку
1 |
# git add . && git commit -m "Changed redis-calculator-playbook.yaml and docker-compose.yaml" && git push |
Проверяем наличие запущенных контейнеров
1 |
# docker ps |
1 2 3 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5231a52d4448 mydocker.repo.servername/calculator:2018-11-21-11-28 "java -jar app.jar" 12 minutes ago Up 12 minutes 0.0.0.0:32769->8080/tcp jenkins_calculator_1_43ef678e567c d14d39b7cef0 redis:latest "docker-entrypoint.s…" 23 hours ago Up 23 hours 6379/tcp jenkins_redis_1_2fffa09b18db |
Источник: Книга Сontinuous delivery with Docker and Jenkins by Rafal Leszko