基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程

声明:部分截图来自于网络

背景

预期目标

通过Jenkins Pipeline + github + docker,实现代码提交以后自动触发环境搭建和测试流程,包括拉取代码、构建镜像、测试镜像、发布镜像、远程部署镜像、回归测试等自动化流程。可以实现开发同学在提交代码后的自动化测试流程,并且测试环境的可移植性较强,受人为因素影响程度较低,整个流程的自动化程度较高,且环境稳定性较高。在此基础上,未来目标是可以实现测试环境可配置化,提升各个测试环境的定制化程度和灵活性。
展示一张最终效果截图:
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第1张图片

软硬件依赖环境

1.硬件环境

硬件环境 用途 描述
服务器 1 jenkins服务的宿主机 centos 7以上,用于安装、运行Jenkins相关服务和插件
服务器 2 测试环境的宿主机 centos 7以上,用于模拟在远程机器部署测试环境

2.软件环境

软件环境 用途 描述
docker 运行Jenkins服务的容器 基于Docker环境的Jenkins服务的搭建,对系统依赖程度较低,对系统的污染最低
Jenkins 配置基于Jenkins Pipeline的流水线任务 基于Jenkins Pipeline,完成对github、docker插件的集成

3.插件环境

插件环境 用途 描述
Pipeline Jenkins流水线,使用脚本实现整个应用的下载、编译、测试、发布等流程 基于Jenkinsfile,定义若干stage和steps来完成代码拉取、编译、构建、测试、发布、远程部署等自动化流程
Pipeline: Stage View 构建复杂流水线的可视化工具 方便查看每个版本,每个阶段的执行状态和日志
Blue Ocean 构建复杂流水线的可视化工具 重新设计的Jenkins Pipeline,快速只管的查看每个阶段的执行状态和日志
SSH Plugin 使用SSH协议执行远程shell命令
docker build step 自动化管理docker
github 管理github代码 提供github代码库管理功能;在Jenkins服务端提供github-webhook接口,用于实现代码提交以后的通知机制

4.外部其他环境

其他环境 用途 描述
github.com 整个Pipeline的触发起点 当有代码提交动作和分支合并动作时,实时推送给Jenkins Pipeline,触发Pipeline任务的构建
hub.docker.com 用于维护镜像管理 基于Dockerfile的镜像构建、拉取和推送

搭建步骤及过程

安装Jenkins & docker

初始化管理员用户

添加admin用户

adduser admin
passwd admin

给admin用户设置sudo权限

vi /etc/sudoers
# 添加下面一行配置
admin   ALL=(ALL)       ALL

切换到admin用户,后续操作都在admin用户下执行

sudo su admin

安装docker-ce(基于centos的安装过程)

安装必要的系统工具

​sudo yum install -y yum-utils device-mapper-persistent-data lvm2

添加软件源

​sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

执行安装

sudo yum install docker-ce docker-ce-cli containerd.io

启动docker

​sudo service docker start

配置docker

修改/etc/docker/daemon.json,添加国内镜像地址,修改本地默认docker graph地址

​{
    "registry-mirrors": ["https://xisih51q.mirror.aliyuncs.com"],
    "graph": "/home/admin/tools/docker"
}

重启docker

​sudo service docker restart

查看配置是否生效(需要使用root账号执行)

​docker info

配置docker使用非root账号

添加docker组

​sudo groupadd docker

在docker组中添加当前用户

​sudo usermod -aG docker $USER

更新docker组

newgrp docker 

查看配置生效(非root用户执行)

docker info

拉取并启动jenkins镜像

通过docker run命令拉取并启动jenkins镜像

​docker run -d -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean --name jenkins-blueocean
​docker exec -it jenkins-blueocean /bin/bash

访问jenkins以及初始化

访问jenkins主页

http://47.93.193.48:8080/

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第2张图片

安装推荐插件
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第3张图片
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第4张图片

配置Pipeline

Pipeline相关概念

什么是Pipeline?

按照官方给出的解释是:将持续集成实现和实施集成到jenkins中;流水线的定义被写在一个文本文件中(Jenkinsfile),该文件被提交到github仓库中(这是流水线即代码的基础),将CD流水线作为应用程序的一部分

一个标准CD场景的流程图

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第5张图片

Pipeline语法:

参考https://www.jenkins.io/zh/doc/book/pipeline/syntax/

创建Jenkinsfile并提交到源代码中的优点:

  • 自动为所有分支创建流水线构建过程,并拉取请求
  • ​在流水线上代码复查、迭代 ​
  • 对流水线进行审计跟踪
  • 该流水线的代码,可以被项目多个成员查看和编辑
Jenkinsfile相关介绍
  • 概念:​用户定义的一个CD流水线模型,流水线的代码定义了整个构建过程,包括构建、测试和交付等阶段

  • node(节点):是一个机器,它是jenkins还击的一部分

  • stage(阶段):stage块定义了整个流水线的执行任务的不同的子集(比如Build、Test、Deploy等)

  • steps(步骤):​一个单一的任务,一个step告诉jenkins在特定的时间点要做什么。例如:要执行shell命令,使用sh步骤:sh ‘make’

定义一个Jenkinsfile:

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第6张图片

Pipeline配置过程

点击Jenkins中的New Item菜单
为新工程起一个名字(例如mypipeline),选择Mulitibranch Pipeline
点击Add Source按钮,添加github仓库地址
github配置,生成token

​进入github --> setting --> Developer settings --> Personal Access Token --> Generate new token

​Note:finalbattle

​勾选repo和admin:repo_hook两个选项

​点击保存,会生成一个token
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第7张图片

github webhook配置(如果部署的Jenkins在外网可以访问,此步骤可忽略)

下载ngrok

wget ​https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip

启动ngrok

​./ngrok http 8080(注:指定的8080端口是jenkins启动所占用的端口)

​生成xxxxxxxx.ngrok.io

​Session Status online 
Session Expires 7 hours, 31 minutes 
Version 2.3.35 
Region United States (us) 
Web Interface http://127.0.0.1:4040 
Forwarding http://406282713900.ngrok.io -> http://localhost:8080 
Forwarding https://406282713900.ngrok.io -> http://localhost:8080 

Connections ttl opn rt1 rt5 p50 p90 
1 0 0.00 0.00 7.63 7.63 

HTTP Requests 
------------- 

POST /github-webhook/ 200 OK                                                                         

​进入GitHub上指定的项目 --> setting --> WebHooks --> add webhook

​# 填写生成好的ngrok地址:
http://406282713900.ngrok.io/github-webhook/

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第8张图片

在jenkins中配置github plugin

​jenkins系统管理 --> 系统设置 --> GitHub --> Add GitHub Sever

​找到github选项,Name和API URL都输入https://api.github.com

​添加Credentials(凭证提供者)配置

​Domain:Global credentials(unrestricted)​
kind:Secret text
Secret:​c2***********************************ba
​ID: github_token
​Description: github_token

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第9张图片

点击高级配置 →勾选【覆盖 HOOK URL】

点击连接测试,出现Credentials verified for user xxxxx, rate limit: xxxx,说明连接测试成功
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第10张图片

配置mypipeline中的分支源

在github代码库中添加一个Jenkinsfile文件

pipeline {
    agent {
        docker {
            image 'python:3.5.2'
            args '-v $HOME/tools/docker'
        }
    }
    stages {
        stage('build') {
            steps {
                sh 'python --version'
            }
        }
        stage('Test') {
            steps {
                sh 'python --version'
            }
        }
    }
}

​添加一个token(注:多分支里的token由于只支持用户名和密码,其他方式的token会被过滤掉)

用户名:finalbattle
密码:*********
ID:github_token_user_passwd
Description:github_token_user_passwd

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第11张图片

添加github地址

​https://github.com/finalbattle/templates.git

注:确保https://github.com/finalbattle/templates.git已经配置了webhook
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第12张图片

至此,一个简单的Jenkins Pipeline已经搭建起来,可以正常运行

修改代码库中的Jenkinsfile

node {
    // 定义环境变量
    def app
    def myRepo = checkout scm
    def gitCommit = myRepo.GIT_COMMIT
    def gitBranch = myRepo.GIT_BRANCH
    def shortGitCommit = "${gitCommit[0..10]}"
    def previousGitCommit = sh(script: "git rev-parse ${gitCommit}~", returnStdout: true)

    env.VERSION = "0.0.1"
    env.server_credentialsId = '47.93.193.48_user_admin_password_xxxxx'
    env.host = '47.93.193.48'
    env.PRO_ENV = 'test'
    env.docker_credentialsId = 'hub_docker_user_finalbattle_password_peng0351atbj'
    env.git_credentialsId = 'username_finalbattle_password_Peng0351atbj'
    def imageName = 'finalbattle/templates_demo1'
    def WORKSPACE_HOME = '/home/admin/projects'
    def LOG_HOME = '/home/admin/logs'
    def CONF_HOME = '/home/admin/conf'
    def SUPERVISOR_HOME = '/home/admin/supervisor'
    def SUPERVISOR_CONF_HOME = '/home/admin/supervisor/conf'
    def tagName = "${env.VERSION}_${env.PRO_ENV}_${BUILD_NUMBER}"
    def imageTagName = "${imageName}:${env.VERSION}_${env.PRO_ENV}_${BUILD_NUMBER}"

    stage('Build') {
        app = docker.build("finalbattle/templates_demo1", "--build-arg PRO_ENV=${env.PRO_ENV} --build-arg WORKSPACE=${WORKSPACE} . -f Dockerfile.templates_demo1")
        app.inside('-u root -itd  -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock') {
            sh "cp -r ${WORKSPACE}/* /home/admin/projects"
        }
    }

    stage('Test') {
        app.inside {
            sh 'echo "Test passed"'
        }
    }

    stage('Publish') {
        docker.withRegistry('https://registry.hub.docker.com', env.docker_credentialsId) {
            echo "Pushing ${tagName}"
            // Push tagged version
            app.push("${tagName}")
            echo "Pushed!"
        }
    }

    stage('Deploy') {
        // 部署的目标服务器
        withCredentials([usernamePassword(credentialsId: env.server_credentialsId, usernameVariable: 'USER', passwordVariable: 'PWD')]) {
            def otherArgs // 区分不同环境的启动参数
            def remote = [:]
            remote.name = 'ssh-deploy'
            remote.allowAnyHosts = true
            remote.host = env.host
            remote.user = USER
            remote.password = PWD

            if(env.PRO_ENV == "pro") {
                otherArgs = '-p 8000:8000'
            }else{
                otherArgs = '-p 7001:7001'

            }

            try {
                sshCommand remote: remote, command: "docker rm -f demo"
            } catch (err) {

            }
            echo 'imageName: ${imageName}'
            echo 'PRO_ENV: ${env.PRO_ENV}'
            sshCommand remote: remote, command: "docker run -itd --name demo -v /etc/localtime:/etc/localtime -e PRO_ENV='${env.PRO_ENV}' ${otherArgs} ${imageTagName} /bin/bash"
        }
        // 删除旧的镜像
        sh "docker rmi -f ${imageName.replaceAll("_${BUILD_NUMBER}", "_${BUILD_NUMBER - 1}")}"
    }

    stage('Regression Test') {
        sh 'echo "Test passed"'
    }

}

代码库中添加APP_META

在代码库中添加如下目录和文件:

APP_META/
└── environment
    └── common
        ├── bin
        │   └── appctl.sh
        └── conf
            └── supervisor.ini

appctl.sh

#!/bin/bash
. /etc/rc.d/init.d/functions
ARGV="$1"
ERROR=0

if [ "x$ARGV" = "x" ] ; then
    echo "$0 {start|stop|restart|status}"
    exit 0
fi

before_start () {
    check_supervisor
    update_supervisor
}

check_supervisor() {
    if [ ! -f "/home/admin/conf/supervisor.ini" ]; then
        echo "/home/admin/conf/supervisor.ini does not exist!"
        ERROR=1
        exit 0
    fi
    if [ ! -f "/home/admin/supervisor/conf/supervisor.ini" ]; then
        ln -s /home/admin/conf/supervisor.ini /home/admin/supervisor/conf/supervisor.ini
    fi
    ERROR=0
}

update_supervisor() {
    /opt/app-root/bin/supervisorctl -c /home/admin/supervisor/supervisord.conf update
    ERROR=0
}

start() {
    before_start
    /opt/app-root/bin/supervisorctl -c /home/admin/supervisor/supervisord.conf start all
}

restart() {
    before_start
    /opt/app-root/bin/supervisorctl -c /home/admin/supervisor/supervisord.conf status all
}

status() {
    /opt/app-root/bin/supervisorctl -c /home/admin/supervisor/supervisord.conf status all
}

stop() {
    /opt/app-root/bin/supervisorctl -c /home/admin/supervisor/supervisord.conf stop all
}


case "$ARGV" in
    start)
        start
        ;;
    restart)
        restart
        ;;
    stop)
        stop
        ;;
    status)
        status
        ;;
esac

if [ "x$ERROR" != "x0" ] ; then
echo_failure
else
echo_success
fi

exit $ERROR

supervisor.ini

[program:appone]
directory=/home/admin/projects/appone
command=python /home/admin/projects/appone/start_server.py --settings=/home/admin/projects/appone/settings/testing.yaml --port=7001
autostart=true
autorestart=true
stdout_logfile=/home/admin/logs/appone.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10


[program:celery_app]
directory=/home/admin/projects/appone
command=celery -A celery_app worker --loglevel=info -B
autostart=true
autorestart=true
stdout_logfile=/home/admin/logs/celery_app.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10

代码库中添加Dockerfile和Dockerfile.templates_demo1

为什么要两份Dockerfile?

默认的Dockerfile为基础镜像,包含各个环境共同的基础依赖

Dockerfile.templates_demo1为测试环境镜像,继承自Dockerfile,主要提供测试环境的一些个性化配置,方便与其他环境的Dockerfile做区分,而不会污染基础镜像。除非整个应用依赖包需要进行扩展,需要在基础镜像中进行修改,否则不需要对基础镜像进行变动。

Dockerfile

FROM centos/python-38-centos7:latest

MAINTAINER admin [email protected]

USER root

ENV LANG en_US.UTF-8

RUN ln -s -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

RUN yum install -y wget net-tools openssl-devel libffi-devel

RUN echo "[global]" > /etc/pip.conf
RUN echo "index-url = http://pypi.douban.com/simple" >> /etc/pip.conf
RUN echo "trusted-host = pypi.douban.com" >> /etc/pip.conf
RUN echo "disable-pip-version-check = true" >> /etc/pip.conf
RUN pip install celery==4.4.6 docopt==0.6.2 future==0.18.2 gevent==20.6.2 hbmqtt==0.9.6 ipython==7.15.0 ipython-genutils==0.2.0 Jinja2==2.11.2 \
kombu==4.6.11 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 numpy==1.19.0 pickleshare==0.7.5 pika==0.11.2 prompt-toolkit==3.0.5 \
ptyprocess==0.6.0 Pygments==2.6.1 pylint==2.5.3 python3-pika==0.9.14 pytz==2020.1 PyYAML==5.3.1 pyzmq==19.0.1 redis==3.5.1 six==1.15.0 \
toml==0.10.1 tornado==4.5.3 tornado-celery==0.3.5 traitlets==4.3.3 transitions==0.8.1 ujson==3.0.0 vine==1.3.0 wcwidth==0.2.4 websockets==8.1 \
wrapt==1.12.1 zope.event==4.4 zope.interface==5.1.0 voluptuous==0.11.7 supervisor

ENV WORKSPACE_HOME /home/admin/projects
ENV LOG_HOME /home/admin/logs
ENV CONF_HOME /home/admin/conf
ENV SUPERVISOR_HOME /home/admin/supervisor
ENV SUPERVISOR_CONF_HOME /home/admin/supervisor/conf
RUN mkdir -p $SUPERVISOR_HOME && echo_supervisord_conf > $SUPERVISOR_HOME/supervisord.conf 
RUN sed -i "s/\/tmp/\/home\/admin\/supervisor/g" $SUPERVISOR_HOME/supervisord.conf
RUN sed -i "s/;\[include\]/\[include\]/g" $SUPERVISOR_HOME/supervisord.conf
RUN echo "files = /home/admin/supervisor/conf/*.ini" >> $SUPERVISOR_HOME/supervisord.conf

Dockerfile.templates_demo1

FROM finalbattle/templates:latest

MAINTAINER admin [email protected]

ENV WORKSPACE_HOME /home/admin/projects
ENV LOG_HOME /home/admin/logs
ENV BIN_HOME /home/admin/bin
ENV CONF_HOME /home/admin/conf
ENV SUPERVISOR_CONF_HOME /home/admin/supervisor/conf
RUN mkdir -p ${WORKSPACE_HOME} && mkdir -p ${LOG_HOME} && mkdir -p ${BIN_HOME} && mkdir -p ${CONF_HOME} && mkdir -p ${SUPERVISOR_CONF_HOME}
RUN echo "#!/bin/bash" > /home/admin/start.sh && chmod a+x /home/admin/start.sh
RUN echo "echo 'start'" >> /home/admin/start.sh
RUN echo "nohup /opt/app-root/bin/supervisord -c /home/admin/supervisor/supervisord.conf &" >> /home/admin/start.sh
RUN echo "/home/admin/bin/appctl.sh start" >> /home/admin/start.sh

COPY ${WORKSPACE}/appone /home/admin/projects/appone
COPY ${WORKSPACE}/APP_META /home/admin/projects/APP_META
COPY ${WORKSPACE}/APP_META/environment/common/bin /home/admin/bin
COPY ${WORKSPACE}/APP_META/environment/common/conf /home/admin/conf

ENTRYPOINT /home/admin/start.sh > /home/admin/start.log && tail -f /home/admin/start.log

添加好相应的文件和目录以后,当提交代码到github库的时候,github会将提交信息同步到Jenkins服务(webhook),Jenkins Pipeline收到消息通知后,自动触发执行job任务中的Build、Test、Publish、Deploy、Regression Test各个阶段。

  • Build:触发构建Dockerfile.templates_demo1镜像
  • Test:执行相关容器检查和测试
  • Publish:发布相关的Tag镜像到hub.docker.com Deploy:在远程机器拉取对应Tag的镜像,并启动容器
  • Regression Test:在远程机器自动执行回归测试

后续扩展:

基于当前实现的自动化Pipeline流程,可以做一些更高级的扩展,比如在部署测试环境阶段之前,进行人工干预确认;可以在构建时增加其他的集成测试环境、预发环境、灰度环境以及生产环境。

遇到的问题和解决方案

jenkins初始化插件时,提示处于离线状态,需要配置HTTP代理:

基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第13张图片

解决上述问题方法:

  1. 修改/var/lib/jenkins/updates/default.json

jenkins在下载插件之前会先检查网络连接,其会读取这个文件中的网址。默认是:

访问谷歌,这就很坑了,服务器网络又不能,肯定监测失败,所以将图下的google改为www.baidu.com即可,更改完重启服务。

  1. 修改/var/lib/jenkins/hudson.model.UpdateCenter.xml

该文件为jenkins下载插件的源地址,改地址默认jenkins默认为:https://updates.jenkins.io/update-center.json,就是因为https的问题,此处我们将其改为http即可,之后重启jenkins服务即可。

其他国内备用地址(也可以选择使用):

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

http://mirror.esuni.jp/jenkins/updates/update-center.json

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

解决上述问题方法:

确保当前用户和jenkins用户拥有docker组权限:

sudo groupadd docker

sudo usermod -aG docker $USER

sudo usermod -aG docker jenkins

newgrp docker

sudo service jenkins restart

Jenkins Pipeline任务结束,Docker容器被自动删除

解决问题方法:

使用远程部署方式,避免在本地的主任务中执行docker环境部署

withCredentials([usernamePassword(credentialsId: env.server_credentialsId, usernameVariable: ‘USER’, passwordVariable: ‘PWD’)]) {

}

Docker镜像在发布过程中出现认证失败:

现象:
基于Jenkins Pipeline + github + docker持续集成CI/CD环境搭建过程_第14张图片

源代码:

docker.withRegistry('https://registry.hub.docker.com/', docker_credentialsId) {
    registry_url = "http://registry.hub.docker.com/"
    sh "docker push ${imageTagName}"
}

解决问题方法:

    app = docker.build("finalbattle/templates_demo1", "--build-arg PRO_ENV=${env.PRO_ENV} --build-arg WORKSPACE=${WORKSPACE} . -f Dockerfile.templates_demo1")
    docker.withRegistry('https://registry.hub.docker.com', env.docker_credentialsId) {
        echo "Pushing ${tagName}"
        // Push tagged version
        app.push("${tagName}")
        echo "Pushed!"
    }

注:不能使用docker push的命令来推送镜像,而是需要使用Jenkins Pipeline中的customImage.push()函数,参考https://www.jenkins.io/zh/doc/book/pipeline/docker/#custom-registry

Docker容器内部报docker命令找不到

解决问题方法:

启动docker容器时,增加如下参数:

app.inside('-u root -itd  -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock') {
    ......
}

Jenkins服务访问https://api.github.com报连接被拒绝

java.net.ConnectException: Connection refused (Connection refused)
Caused: org.kohsuke.github.HttpException: Server returned HTTP response code: -1, message: 'null' for URL: https://api.github.com/
org.jenkinsci.plugins.github_branch_source.GitHubSCMSource.checkApiUrlValidity(GitHubSCMSource.java:1528)
Caused: java.io.IOException: It seems https://api.github.com is unreachable

原因是国内网络访问github,CDN域名遭到DNS污染,导致我们无法连接使用github的加速服务,因此访问速度缓慢,参考文档:https://zhuanlan.zhihu.com/p/107334179
解决问题方法:
修改/etc/hosts文件:

140.82.114.5 api.github.com

你可能感兴趣的:(Jenkins,Pipeline,docker,github,docker,jenkins,devops)