随着项目的不断增多,最开始单体项目手动执行 docker build 命令,手动发布项目就不再适用了。一两个项目可能还吃得消,10 多个项目每天让你构建一次还是够呛。即便你的项目少,每次花费在发布上面的时间累计起来都够你改几个 BUG 了。

所以我们需要自动化这个流程,让项目的发布和测试不再这么繁琐。在这里我使用了 Jenkins 作为基础的 CI/CD Pipeline 工具,关于 Jenkins 的具体介绍这里就不再赘述。在版本管理、构建项目、单元测试、集成测试、环境部署我分别使用到了 GogsDockerDocker Swarm(已与 Docker 整合) 这几个软件协同工作。

以下步骤我参考了 Continuous Integration with Jenkins and Docker 一文,并使用了作者提供的 groovy 文件和 slave.py 文件。

关于 Docker-CE 的安装,请参考我的另一篇博文 《Linux 下的 Docker 安装与使用》

一、Jenkins 的部署

既然都用了 Docker,我是不想在实体机上面安装一堆环境,所以我使用了 Docker 的形式来部署 Jenkins 的 Master 和 Slave,省时省力。Master 就是调度管道任务的主机,也是唯一有 UI 供用户操作的。而 Slave 就是具体的工作节点,用于执行具体的管道任务。

1.1 构建 Master 镜像

第一步,我们在主机上建立一个 master 文件夹,并使用 vi 创建两个 groovy 文件,这两个文件在后面的 Dockerfile 会被使用到,下面是 default-user.groovy 文件的代码:

import jenkins.model.*
import hudson.security.* def env = System.getenv() def jenkins = Jenkins.getInstance()
jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy()) def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS)
user.save() jenkins.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER)
jenkins.save()

接着再用 vi 创建一个新的 executors.groovy 文件,并输入以下内容:

import jenkins.model.*
Jenkins.instance.setNumExecutors(0)

以上动作完成之后,在 master 文件夹下面应该有两个 groovy 文件。

两个 master 所需要的 groovy 文件已经编写完成,下面来编写 master 镜像的 Dockerfile 文件,每一步的作用我已经用中文进行了标注。

# 使用官方的 Jenkins 镜像作为基础镜像。
FROM jenkins/jenkins:latest # 使用内置的 install-plugins.sh 来安装插件。
RUN /usr/local/bin/install-plugins.sh git matrix-auth workflow-aggregator docker-workflow blueocean credentials-binding # 设置 Jenkins 的管理员账户和密码。
ENV JENKINS_USER admin
ENV JENKINS_PASS admin # 跳过初始化安装向导。
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false # 将刚刚编写的两个 groovy 脚本复制到初始化文件夹内。
COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/
COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/ # 挂载 jenkins_home 目录到 Docker 卷。
VOLUME /var/jenkins_home

接着我们通过命令构建出 Master 镜像。

docker build -t jenkins-master .

1.2 构建 Slave 镜像

Slave 镜像的核心是一个 slave.py 的 python 脚本,它主要执行的动作是运行 slave.jar 并和 Master 建立通信,这样你的管道任务就能够交给 Slave 进行执行。这个脚本所做的工作流程如下:

我们再建立一个 slave 文件夹,并使用 vi 将 python 脚本复制进去。

slave.py 的内容:

from jenkins import Jenkins, JenkinsError, NodeLaunchMethod
import os
import signal
import sys
import urllib
import subprocess
import shutil
import requests
import time slave_jar = '/var/lib/jenkins/slave.jar'
slave_name = os.environ['SLAVE_NAME'] if os.environ['SLAVE_NAME'] != '' else 'docker-slave-' + os.environ['HOSTNAME']
jnlp_url = os.environ['JENKINS_URL'] + '/computer/' + slave_name + '/slave-agent.jnlp'
slave_jar_url = os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar'
print(slave_jar_url)
process = None def clean_dir(dir):
for root, dirs, files in os.walk(dir):
for f in files:
os.unlink(os.path.join(root, f))
for d in dirs:
shutil.rmtree(os.path.join(root, d)) def slave_create(node_name, working_dir, executors, labels):
j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS'])
j.node_create(node_name, working_dir, num_executors = int(executors), labels = labels, launcher = NodeLaunchMethod.JNLP) def slave_delete(node_name):
j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS'])
j.node_delete(node_name) def slave_download(target):
if os.path.isfile(slave_jar):
os.remove(slave_jar) loader = urllib.URLopener()
loader.retrieve(os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar', '/var/lib/jenkins/slave.jar') def slave_run(slave_jar, jnlp_url):
params = [ 'java', '-jar', slave_jar, '-jnlpUrl', jnlp_url ]
if os.environ['JENKINS_SLAVE_ADDRESS'] != '':
params.extend([ '-connectTo', os.environ['JENKINS_SLAVE_ADDRESS' ] ]) if os.environ['SLAVE_SECRET'] == '':
params.extend([ '-jnlpCredentials', os.environ['JENKINS_USER'] + ':' + os.environ['JENKINS_PASS'] ])
else:
params.extend([ '-secret', os.environ['SLAVE_SECRET'] ])
return subprocess.Popen(params, stdout=subprocess.PIPE) def signal_handler(sig, frame):
if process != None:
process.send_signal(signal.SIGINT) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) def master_ready(url):
try:
r = requests.head(url, verify=False, timeout=None)
return r.status_code == requests.codes.ok
except:
return False while not master_ready(slave_jar_url):
print("Master not ready yet, sleeping for 10sec!")
time.sleep(10) slave_download(slave_jar)
print 'Downloaded Jenkins slave jar.' if os.environ['SLAVE_WORING_DIR']:
os.setcwd(os.environ['SLAVE_WORING_DIR']) if os.environ['CLEAN_WORKING_DIR'] == 'true':
clean_dir(os.getcwd())
print "Cleaned up working directory." if os.environ['SLAVE_NAME'] == '':
slave_create(slave_name, os.getcwd(), os.environ['SLAVE_EXECUTORS'], os.environ['SLAVE_LABELS'])
print 'Created temporary Jenkins slave.' process = slave_run(slave_jar, jnlp_url)
print 'Started Jenkins slave with name "' + slave_name + '" and labels [' + os.environ['SLAVE_LABELS'] + '].'
process.wait() print 'Jenkins slave stopped.'
if os.environ['SLAVE_NAME'] == '':
slave_delete(slave_name)
print 'Removed temporary Jenkins slave.'

上述脚本的工作基本与流程图的一致,因为 Jenkins 针对 Python 提供了 SDK ,所以原作者使用 Python 来编写的 “代理” 程序。不过 Jenkins 也有 RESTful API,你也可以使用 .NET Core 编写类似的 “代理” 程序。

接着我们来编写 Slave 镜像的 Dockerfile 文件,因为国内服务器访问 Ubuntu 的源很慢,经常因为超时导致构建失败,这里切换成了阿里云的源,其内容如下:

FROM ubuntu:16.04

# 安装 Docker CLI。
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list && apt-get clean
RUN apt-get update --fix-missing && apt-get install -y apt-transport-https ca-certificates curl openjdk-8-jre python python-pip git # 使用阿里云的镜像源。
RUN curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | apt-key add -
RUN echo "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial stable" > /etc/apt/sources.list.d/docker.list RUN apt-get update --fix-missing && apt-get install -y docker-ce --allow-unauthenticated
RUN easy_install jenkins-webapi # 安装 Docker-Compose 工具。
RUN curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
RUN mkdir -p /home/jenkins
RUN mkdir -p /var/lib/jenkins # 将 slave.py 文件添加到容器。
ADD slave.py /var/lib/jenkins/slave.py WORKDIR /home/jenkins # 配置 Jenkins Master 的一些连接参数和 Slave 信息。
ENV JENKINS_URL "http://jenkins"
ENV JENKINS_SLAVE_ADDRESS ""
ENV JENKINS_USER "admin"
ENV JENKINS_PASS "admin"
ENV SLAVE_NAME ""
ENV SLAVE_SECRET ""
ENV SLAVE_EXECUTORS "1"
ENV SLAVE_LABELS "docker"
ENV SLAVE_WORING_DIR ""
ENV CLEAN_WORKING_DIR "true" CMD [ "python", "-u", "/var/lib/jenkins/slave.py" ]

继续使用 docker build 构建 Slave 镜像:

docker build -t jenkins-slave .

1.3 编写 Docker Compose 文件

这里的 Docker Compose 文件,我取名叫 docker-compose.jenkins.yaml ,主要工作是为了启动 Master 和 Slave 容器。

version: '3.1'
services:
jenkins:
container_name: jenkins
ports:
- '8080:8080'
- '50000:50000'
image: jenkins-master
jenkins-slave:
container_name: jenkins-slave
restart: always
environment:
- 'JENKINS_URL=http://jenkins:8080'
image: jenkins-slave
volumes:
- /var/run/docker.sock:/var/run/docker.sock # 将宿主机的 Docker Daemon 挂载到容器内部。
- /home/jenkins:/home/jenkins # 将数据挂载出来,方便后续进行释放。
depends_on:
- jenkins

执行 Docker Compose 之后,我们通过 宿主机 IP:8080 就可以访问到 Jenkins 内部了,如下图。

二、Gogs 的部署

我们内部开发使用的 Git 仓库是使用 Gogs 进行搭建的,Gogs 官方提供了 Docker 镜像,那我们可以直接编写一个 Docker Compose 快速部署 Gogs。

docker-compose.gogs.yaml 文件内容如下:

version: '3.1'
services:
gogs:
image: gogs/gogs
container_name: 'gogs'
expose:
- '3000:3000'
expose:
- 22
volumes:
- /var/lib/docker/Persistence/Gogs:/data # 挂载数据卷。
restart: always

执行以下命令后,即可启动 Gogs 程序,访问 宿主机 IP:3000 按照配置说明安装 Gogs 即可,之后你就可以创建远程仓库了。

三、Gogs 与 Jenkins 的集成

虽然大部分都推荐 Jenkins 的 Gogs Webhook 插件,不过这个插件很久不更新了,而且不支持 版本发布 事件。针对于该问题虽然官方有 PR #62,但一直没有合并,等到合并的时候都是猴年马月了。这里还是建议使用 Generic Webhook Trigger ,用这个插件来触发 Jenkins 的管道任务。

3.1 创建流水线项目

首先找到 Jenkins 的插件中心,搜索 Generic Webhook Trigger 插件,并进行安装。

继续新建一个管道任务,取名叫做 TestProject,类型选择 Pipeline 。

首先配置项目的数据来源,选择 SCM,并且配置 Git 远程仓库的地址,如果是私有仓库则还需要设置用户名和密码。

3.2 Jenkins 的 Webhook 配置

流水线项目建立完成后,我们就可以开始设置 Generic WebHook Trigger 的一些参数,以便让远程的 Gogs 能够触发构建任务。

我们为 TestProject 创建一个 Token,这个 Token 是跟流水线任务绑定了,说白了就是流水线任务的一个标识。建议使用随机 Guid 作为 Token,不然其他人都可以随便触发你的流水线任务进行构建了。

3.3 Gogs 的 Webhook 配置

接着来到刚刚我们建好的仓库,找到 仓库设置->管理 Web 钩子->添加 Web 钩子->Gogs

因为触发构建不可能每次提交都触发,一般来说都是创建了某个合并请求,或者发布新版本的时候就会触发流水线任务。因此这里你可以根据自己的情况来选择触发事件,这里我以合并请求为例,你可以在钩子设置页面点击 测试推送。这样就可以看到 Gogs 发送给 Jenkins 的 JSON 结构是怎样的,你就能够在 Jenkins 那边有条件的进行处理。

不过测试推送只能够针对普通的 push 事件进行测试,像 合并请求 或者 版本发布 这种事件只能自己模拟操作了。在这里我新建了一个用户,Fork 了另一个帐号建立的 TestProject 仓库。

在 Fork 的仓库里面,我新建了一个 Readme.md 文件,然后点击创建合并,这个时候你看 Gogs 的 WebHook 推送记录就有一条新的数据推送给 Jenkins,同时你也可以在 Jenkins 看到流水线任务被触发了。

3.4 限定任务触发条件

通过上面的步骤,我们已经将 Gogs 和 Jenkins 中的具体任务进行了绑定。不过还有一个比较尴尬的问题是,Gogs 的合并事件不仅仅包括创建合并,它的原始描述是这样说的。

合并请求事件包括合并被开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签、设置里程碑、取消设置里程碑或代码同步。

如果我们仅仅是依靠上面的配置,那么上述所有行为都会触发构建操作,这肯定不是我们想要的效果。还好 Generic Webhook 为我们提供了变量获取,以及 Webhook 过滤。

我们从 Gogs 发往 Jenkins 的请求中可以看到,在 JSON 内部包含了一个 action 字段,里面就是本次的操作标识。那么我们就可以想到通过判断 action 字段是否等于 opened 来触发流水线任务。

首先,我们增加 2 个 Post content parameters 参数,分别获取到 Gogs 传递过来的 action 和 PR 的 Id,这里我解释一下几个文本框的意思。

除了这两个 Post 参数以外,在请求头中,Gogs 还携带了具体事件,我们将其一起作为过滤条件。需要注意的是,针对于请求头的参数,在转换成变量时,插件会将字符转为小写,并会使用 "_" 代替 "-"。

最后我们编写一个 Optional filter ,它的 Expression 参数是正则表达式,下面的 Text 即是源字符串。实现很简单,当 Text 里面的内容满足正则表达式的时候,就会触发流水线任务。

所以我们的 Text 字符串就是由上面三个变量的值组成,然后和我们预期的值进行匹配即可。

当然,你还想整一些更加炫酷的功能,可以使用 Jenkins 提供的 Http Request 之类的插件。因为 Gogs 提供了 API 接口,你就可以在构建完成之后,回写给 Gogs,用于提示构建结果。

这样的话,这种功能就有点像 Github 上面的机器人帐号了。

四、完整的项目示例

在上一节我们通过 Jenkins 的插件完成了远程仓库推送通知,当我们合并代码时,Jenkins 会自动触发执行我们的管道任务。接下来我将建立一个 .NET Core 项目,该项目拥有一个 Controller,接收到请求之后输出 “Hello World”。随后为该项目建立一个 xUnit 的测试项目,用于执行单元测试。

整个项目的结构如下图:

我们需要编写一个 UnitTest.Dockerfile 镜像,用于执行 xUnit 单元测试。

FROM mcr.microsoft.com/dotnet/core/sdk:2.2

# 还原 NuGet 包。
WORKDIR /home/app
COPY ./ ./
RUN dotnet restore ENTRYPOINT ["dotnet", "test" , "--verbosity=normal"]

之后为部署操作编写一个 Deploy.Dockerfile ,这个 Dockerfile 首先还原了 NuGet 包,然后通过 dotnet publish 命令发布了我们的网站。

FROM mcr.microsoft.com/dotnet/core/sdk:2.2 as build-image

# 还原 NuGet 包。
WORKDIR /home/app
COPY ./ ./
RUN dotnet restore # 发布镜像。
COPY ./ ./
RUN dotnet publish ./TestProject.WebApi/TestProject.WebApi.csproj -o /publish/ FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /publish
COPY --from=build-image /publish . ENTRYPOINT ["dotnet", "TestProject.WebApi.dll"]

两个 Dockerfile 编写完成之后,将其存放在项目的根目录,以便 Slave 进行构建。

Dockerfile 编写好了,那么我们还要分别为两个镜像编写 Docker Compose 文件,用于执行单元测试和部署行为,用于部署的文件名称叫做 docker-compose.Deploy.yaml,内容如下:

version: '3.1'

services:
backend:
container_name: dev-test-backend
image: dev-test:B${BUILD_NUMBER}
ports:
- '5000:5000'
restart: always

然后我们需要编写运行单元测试的 Docker Compose 文件,名字叫做 docker-compose.UnitTest.yaml,内容如下:

version: '3.1'

services:
backend:
container_name: dev-test-unit-test
image: dev-test:TEST${BUILD_NUMBER}

五、编写 Jenkinsfile

node('docker') {

    stage '签出代码'
checkout scm
stage '单元测试'
sh "docker build -t dev-test:TEST${BUILD_NUMBER} -f UnitTest.Dockerfile ."
sh "docker-compose -f docker-compose.UnitTest.yaml up --force-recreate --abort-on-container-exit"
sh "docker-compose -f docker-compose.UnitTest.yaml down -v"
stage '部署项目'
sh "docker build -t dev-test:B${BUILD_NUMBER} -f Deploy.Dockerfile ."
sh 'docker-compose -f docker-compose.Deploy.yaml up -d'
}

六、最后的效果

上述操作完成之后,将这些文件放在项目根目录。

回到 Jenkins,你可以手动执行一下任务,然后项目就被成功执行了。

至此,我们的 “低配版” CI、CD 环境就搭建成功了。

Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD的更多相关文章

  1. 搭建react项目(低配版)

    react项目低配版,可作为react相关测试的基础环境,方便快速进行测试. git clone git@github.com:whosMeya/simple-react-app.git git ch ...

  2. Docker + Jenkins 持续部署 ASP.NET Core 项目

    Docker 是个好东西,特别是用它来部署 ASP.NET Core Web 项目的时候,但是仅仅的让程序运行起来远远不能满足我的需求,如果能够像 DaoCloud 提供的持续集成服务那样,检测 gi ...

  3. Linux服务器使用Docker部署.net Core项目

    发布ASP.NET Core项目 和普通的项目发布一样,将项目发布到目标文件夹中 构建Dockerfile文件 在目标文件根目录新建Dockerfile文件(没有后缀) FROM microsoft/ ...

  4. TeamCity+Rancher+Docker实现.Net Core项目DevOps(目前成本最小的DevOps实践)

    1.准备项 1.1.服务器一台,1H4G(更小内存应该也可以,自行测试),系统:Ubuntu 16.04 64位 1.2.数据库一个,MYSQL,MSSQL都可以(还有其他的,自行配置),教程是MSS ...

  5. 使用Docker方式部署Gitlab,Gitlab-Runner并使用Gitlab提供的CI/CD功能自动化构建SpringBoot项目

    1.Docker安装Gitlab,地址:https://www.cnblogs.com/sanduzxcvbnm/p/13814730.html 2.Docker安装Gitlab-runner,地址: ...

  6. Linux上用Docker部署Net Core项目

    前提:本地配置好Docker环境1.构建Net Core镜像 docker pull microsoft/dotnet 2.新建一个DockerFile文件并填充内容 #基于 `microsoft/d ...

  7. jenkins +gitlab +docker 自动化部署tomcat 项目

    实验环境 实验设备 三台服务器 centos 7.X 以上 内存 2-3G左右 192.168.1.195 (jenkins最新+ git 2.8+maven 3.5 +tomcat 8+java1. ...

  8. ASP.Net Core2.1 秒杀项目一步一步实现CI/CD系列一

    前言:有一段时间没写博客了,那是因为博主菜,需要学习和准备,这不带来了本系列的文章.在这里我把学习的心得分享出来,有些点理解的也不是太到位,希望大佬们能多多给点建议和指导.下半年就把这个系列的文章写完 ...

  9. Go-Zero 短链项目 DevOps 实战,利用 Drone CI/CD 打通上云(Kubernetes)迭代流程

    Go-Zero 官方短链项目教程:快速构建高并发微服务 关于 go-zero,大家可以看文档.为少认为它是中国目前最好用的 golang 微服务框架. 完整的 Go-Zero ShortUrl Dev ...

随机推荐

  1. MySQL数据库的安装和配置

    MySQL数据库介绍 什么是数据库DB? DB的全称是database,即数据库的意思.数据库实际上就是一个文件集合,是一个存储数据的仓库,数据库是按照特定的格式把数据存储起来,用户可以对存储的数据进 ...

  2. 《Java 8 in Action》Chapter 4:引入流

    1. 流简介 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现).就现在来说,你可以把它们看成遍历数据集的高级迭代器.此外,流还可以透明地并行 ...

  3. EL表达式forEach中索引获取

    有的时候,不得不使用循环中的索引,比如label对应的单选多选: <c:forEach items="${lpalls }" var="pall" var ...

  4. 【原创】Linux cpu hotplug

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  5. 一 下载Java的JDK及配置环境变量

    1.下载JDK地址 https://www.oracle.com/technetwork/java/javase/downloads/index.html 2.点击download 3.安装JDK 我 ...

  6. TypeScript语法基础

    什么是TypeScript? TypeScript是微软开发的一门编程语言,它是JavaScript的超集,即它基于JavaScript,拓展了JavaScript的语法,遵循ECMAScript规范 ...

  7. CodeForces-714B-Filya and Homework+思路

    Filya and Homework 题意: 给定一串数字,任选一个数a,把若干个数加上a,把若干个数减去a,能否使得数列全部相同: 思路: 我开始就想找出平均数,以为只有和偶数的可以,结果wa在 1 ...

  8. Ryuji doesn't want to study 2018徐州icpc网络赛 树状数组

    Ryuji is not a good student, and he doesn't want to study. But there are n books he should learn, ea ...

  9. 51nod 1020 逆序排列(dp,递推)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1020 题意:是中文题. 题解:很显然要设dp[i][j]表示 ...

  10. C++多例模式下对Instance的使用

    最近工作中遇到这样一个问题: 之前N年,公司用的都是一块CPU对应一块物理板,也就是,一块物理板只要一个实例化就可以了----俗称单例模式. 现在突然要一块CPU对应多块物理板,妥妥的多例模式啊.但是 ...