Исходный код был взят с этого репозитария
https://github.com/cirulls/hands-on-jenkins/
А именно отсюда
https://github.com/cirulls/hands-on-jenkins/tree/master/section_4/code/cd_pipeline
Этот репозитарий используется в видеокурсе
https://www.packtpub.com/virtualization-and-cloud/hands-continuous-integration-and-automation-jenkins-video
В первом примере Dev, Stage и Live–окружения будут запущены на одном сервере
Объянение шагов в pipeline
1.Загрузка кода из репозитария
1 |
stage("Checkout") |
2.Сборка Docker-образа с приложением
1 |
stage("Build") |
Имя образа имеет формат
1 |
mydocker.repo.servername/myapp:${BUILD_NUMBER} |
3.Загрузка собранного образа в удаленный Docker-репозитарий
1 |
stage("Push to Docker-repo") |
4.Разворачивание приложения из Docker-образа, собранного на шаге 2 на Dev-сервере
1 |
stage("Deploy - Dev") |
а) проверка существует ли уже запущенный контейнер с указанным именем и остановка такого контейнера, если он существует
б) удаление остановленного на предыдущем шаге контейнера
в) запуска контейнера на основе образа, собранного на шаге 2
Имя контейнера и номер порта, который выставляется наружу(открывается на прослушивание на docker-хосте) зависит от окружения
Окружение: имя контейнера, порта
1 2 3 |
Dev: app_dev, 8085 Stage: app_stage, 8086 Live: app_live, 8087 |
5.Запуск UAT-теста на Dev-сервере
1 |
stage("Test - UAT Dev") |
Запускается скрипт sh «tests/runUAT.sh с позиционным параметром ${port}, где вместо
номер порта подставляется номер порта соглаcно окружению (см. шаг 4)
6. Разворачивание приложения из Docker-образа, собранного на шаге 2 на Stage-сервере(аналогично тому, как это выполнено в шаге 4)
1 |
stage("Deploy - Stage") |
7. Запуск UAT-теста на Stage-сервере(аналогично тому, как это выполнено в шаге 5)
1 |
stage("Test - UAT Stage") |
8.Ручное подтверждение разворачивания приложения на Live-сервере
1 |
stage("Approve") |
9. Разворачивание приложения из Docker-образа, собранного на шаге 2 на Live-сервере(аналогично тому, как это выполнено в шаге 4)
1 |
stage("Deploy - Live") |
10. Запуск UAT-теста на Live-сервере(аналогично тому, как это выполнено в шаге 5)
1 |
stage("Test - UAT Live") |
Dockerfile для сборки Docker-образа с приложением имеет вид
1 |
# cat Dockerfile |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# base image FROM alpine:3.7 # Install python 3 and pip RUN apk add --update python3 # Install Python modules needed by the Python app COPY requirements.txt /usr/src/app/ RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt # Copy files required for the app to run COPY app.py /usr/src/app/ COPY templates/index.html /usr/src/app/templates/ # Tell the port number the container should expose EXPOSE 5000 # Run the application CMD ["python3", "/usr/src/app/app.py"] |
Файл для запуска User-acceptance теста имеет вид
1 |
# cat tests/runUAT.sh |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#!/bin/bash # set variables hostname='localhost' port=$1 # wait for the app to start sleep 5 # ping the app status_code=$(curl --write-out %{http_code} --out /dev/null --silent ${hostname}:${port}) if [ $status_code == 200 ]; then echo "PASS: ${hostname}:${port} is reachable" else echo "FAIL: ${hostname}:${port} is unreachable" exit 1 fi |
Индексная/главная/дефолтная страница имеет вид.
1 |
# cat templates/index.html |
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 |
<html> <head> <style type="text/css"> body { background: black; color: white; } div.container { max-width: 500px; margin: 100px auto; border: 20px solid white; padding: 10px; text-align: center; } h4 { text-transform: uppercase; } </style> </head> <body> <div class="container"> <h4>Cat Gif of the day</h4> <img src="{{url}}" /> <p><small>Courtesy: <a href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs">Buzzfeed</a></small></p> </div> </body> </html> |
В этом файле в исходном/оригинальном репозитарии указанны некорректные/устаревшие ссылки на изображения
Реально рабочие URL можно получить отсюда:
https://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs
Часть таких ссылок на изображения я и добавил в файл app.py(вмето имеющихся там неактуальных ссылок)
1 |
# cat app.py |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from flask import Flask, render_template import random app = Flask(__name__) # list of cat images images = [ "https://img.buzzfeed.com/buzzfeed-static/static/2013-10/enhanced/webdr05/15/9/anigif_enhanced-buzz-26390-1381844163-18.gif", "https://img.buzzfeed.com/buzzfeed-static/static/2013-10/enhanced/webdr06/15/10/anigif_enhanced-buzz-1376-1381846217-0.gif", "https://img.buzzfeed.com/buzzfeed-static/static/2013-10/enhanced/webdr03/15/9/anigif_enhanced-buzz-3391-1381844336-26.gif", "https://img.buzzfeed.com/buzzfeed-static/static/2013-10/enhanced/webdr03/15/9/anigif_enhanced-buzz-3409-1381844582-13.gif", "https://img.buzzfeed.com/buzzfeed-static/static/2013-10/enhanced/webdr02/15/9/anigif_enhanced-buzz-19667-1381844937-10.gif" ] @app.route('/') def index(): url = random.choice(images) return render_template('index.html', url=url) if __name__ == "__main__": app.run(host="0.0.0.0") |
Unit-тестирование отключено в pipeline
Скрипт, используемый для Unit-тестирования
1 |
# cat tests/test_flask_app.py |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#!/usr/bin/python import sys import os import unittest import app class BasicTests(unittest.TestCase): # execute before each test def setUp(self): self.app = app.app.test_client() # executed after each test def tearDown(self): pass # tests def test_main_page(self): response = self.app.get('/', follow_redirects=True) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main() |
1 |
# cat requirements.txt |
1 |
Flask==0.12.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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
pipeline { agent { label 'master' } options { disableConcurrentBuilds() } environment { PYTHONPATH = "${WORKSPACE}" } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/cats.git', branch: 'master' } } /*stage("Test - Unit tests") { steps { runUnittests() } }*/ stage("Build") { steps { buildApp() } } stage("Push to Docker-repo") { steps { pushImage() } } stage("Deploy - Dev") { steps { deploy('dev') } } stage("Test - UAT Dev") { steps { runUAT(8085) } } stage("Deploy - Stage") { steps { deploy('stage') } } stage("Test - UAT Stage") { steps { runUAT(8086) } } stage("Approve") { steps { approve() } } stage("Deploy - Live") { steps { deploy('live') } } stage("Test - UAT Live") { steps { runUAT(8087) } } } } // steps def approve() { timeout(time:1, unit:'DAYS') { input('Do you want to deploy to live?') } } def pushImage(){ 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/myapp:${BUILD_NUMBER}" } } def buildApp() { def appImage = docker.build("mydocker.repo.servername/myapp:${BUILD_NUMBER}") /*sh "docker build -t mydocker.repo.servername/myapp:${BUILD_NUMBER} ."*/ } def deploy(environment) { def containerName = '' def port = '' if ("${environment}" == 'dev') { containerName = "app_dev" port = "8085" } else if ("${environment}" == 'stage') { containerName = "app_stage" port = "8086" } else if ("${environment}" == 'live') { containerName = "app_live" port = "8087" } else { println "Environment not valid" System.exit(0) } sh "docker ps -f name=${containerName} -q | xargs --no-run-if-empty docker stop" sh "docker ps -a -f name=${containerName} -q | xargs --no-run-if-empty docker rm" sh "docker run -d -p ${port}:5000 --name ${containerName} mydocker.repo.servername/myapp:${BUILD_NUMBER}" } def runUnittests() { sh "python tests/test_flask_app.py" } def runUAT(port) { sh "tests/runUAT.sh ${port}" } |
Во-втором примере Dev, Stage и Live–окружения будут запущены на трех разных серверах
Изменим pipeline для реализации такой возможности:
1.Сборки образа и запуска приложения на Dev-окружении на одном сервере(например, Jenkins Master)
2.Запуска приложения для Stage-окружения на втором сервере (например, Jenkins Slave1)
3.Запуска приложения для Live-окружения на третьем сервере (например, Jenkins Slave2)
Порты я оставил неизменными, но при необходимости порты на Dev/Stage/Live-окружениях можно изменить на стандартные(80)
Изменений всего два по сравнение с предыдущим pipeline:
1.Для запуска приложения на Stage/Live-окружениях были добавлена опция agent с соответствующей меткой
1 2 |
agent { label 'linux-slave1' } agent { label 'linux-slave2' } |
2. Для Stage/Live-окружений добавлен сheckout-кода с репозитария для того, чтобы локально на Stage/Live-серверах появился файл tests/runUAT.sh, используемый для UAT-теста
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
pipeline { agent { label 'master' } options { disableConcurrentBuilds() } environment { PYTHONPATH = "${WORKSPACE}" } stages { stage("Checkout") { steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/cats.git', branch: 'master' } } /*stage("Test - Unit tests") { steps { runUnittests() } }*/ stage("Build") { steps { buildApp() } } stage("Push to Docker-repo") { steps { pushImage() } } stage("Deploy - Dev") { steps { deploy('dev') } } stage("Test - UAT Dev") { steps { runUAT(8085) } } stage("Deploy - Stage") { agent { label 'linux-slave1' } steps { deploy('stage') } } stage("Test - UAT Stage") { agent { label 'linux-slave1' } steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/cats.git', branch: 'master' runUAT(8086) } } stage("Approve") { agent { label 'linux-slave2' } steps { approve() } } stage("Deploy - Live") { agent { label 'linux-slave2' } steps { deploy('live') } } stage("Test - UAT Live") { agent { label 'linux-slave2' } steps { git credentialsId: 'jenkins-user-ssh-key', url: 'git@bitbucket.org:mybitbucketuser/cats.git', branch: 'master' runUAT(8087) } } } } // steps def approve() { timeout(time:1, unit:'DAYS') { input('Do you want to deploy to live?') } } def pushImage(){ 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/myapp:${BUILD_NUMBER}" } } def buildApp() { def appImage = docker.build("mydocker.repo.servername/myapp:${BUILD_NUMBER}") /*sh "docker build -t mydocker.repo.servername/myapp:${BUILD_NUMBER} ."*/ } def deploy(environment) { def containerName = '' def port = '' if ("${environment}" == 'dev') { containerName = "app_dev" port = "8085" } else if ("${environment}" == 'stage') { containerName = "app_stage" port = "8086" } else if ("${environment}" == 'live') { containerName = "app_live" port = "8087" } else { println "Environment not valid" System.exit(0) } withCredentials([usernamePassword(credentialsId: 'docker-login-password-authentification', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')]) { sh "docker ps -f name=${containerName} -q | xargs --no-run-if-empty docker stop" sh "docker ps -a -f name=${containerName} -q | xargs --no-run-if-empty docker rm" sh "docker run -d -p ${port}:5000 --name ${containerName} mydocker.repo.servername/myapp:${BUILD_NUMBER}" } } def runUnittests() { sh "python tests/test_flask_app.py" } def runUAT(port) { sh "tests/runUAT.sh ${port}" } |
Источник:
https://www.packtpub.com/virtualization-and-cloud/hands-continuous-integration-and-automation-jenkins-video
https://github.com/cirulls/hands-on-jenkins/tree/master/section_4/code/cd_pipeline