Jenkins combined Docker achieve low version of CI & CD for the .NET Core Project

With the growing number of projects, most single project began manually execute docker buildcommands manually publish the project is no longer applicable. One or two projects may also be able to stand, more than 10 projects let you build one or unbearable every day. Even small your project, spend time each release above all add up to enough for you to change a few of the BUG.

So we need to automate this process, let publish and test items no longer so tedious. Here I used the Jenkins CI / CD Pipeline tool as a basis, on specific Jenkins introduced not repeat them here. In version management, build the project, unit testing, integration testing, deployment environment I were used to Gogs , Docker , Docker the Swarm (already integrated with Docker) these software work together.

I refer to the following steps Continuous Integration with Jenkins and Docker article, and use the file and groovy authors provide slave.pydocuments.

About Docker-CE installation, see separate my blog "Docker installation and use under Linux" .

A, Jenkins deployment

Since they took Docker, I do not want to install a bunch of environment in the entity upon it, so I used a form Docker to deploy Jenkins of Master and Slave, saving time and effort. Master host pipeline is scheduled tasks, and it is the only UI for users to operate. The Slave is a specific worker nodes for performing specific tasks pipe.

Construction of Mirror Master 1.1

The first step, we have established a master on the host folder and use vito create two groovy files, these two files in the back Dockerfile will be used to, the following is a default-user.groovycode file:

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()

Then again vicreate a new executors.groovyfile, and enter the following:

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

After the above action is completed, the master folder should have the following two groovy files.

Two master needed groovy file has been prepared, following written master mirrored Dockerfile documents, the role of each step I have been labeled in Chinese.

# 使用官方的 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

Then we build a command Mirror Master.

docker build -t jenkins-master .

1.2 Construction of the mirror Slave

Slave is a mirror image of the core slave.pyof a python script, it is mainly the implementation of the action is to run slave.jarand to establish communication and Master, so you will be able to Slave pipeline task execution. The workflow script does the following:

We then create a slave folder, and use the vicopy into the python script.

slave.py Content:

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.'

Work above the script consistent with the flow chart, as Jenkins for Python provides SDK, so use Python to write the original author of "proxy" program. But Jenkins also has a RESTful API, you can also use .NET Core write similar "agent" program.

Then we have to prepare Slave mirrored Dockerfile file because domestic source Ubuntu server access is slow, often fails because it times out building, where the handover source became Ali cloud, which reads as follows:

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" ]

Continue to use docker buildto build Slave Mirroring:

docker build -t jenkins-slave .

1.3 write Docker Compose file

Here Docker Compose file, I named the docker-compose.jenkins.yamlmain work is to start Master and Slave container.

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

After performing Docker Compose, we adopted 宿主机 IP:8080will have access to the internal Jenkins, as shown below.

Two, Gogs deployment

Our in-house developed using Git repository is to be built using Gogs, Gogs official Docker provides a mirror, then we can write directly to a Docker Compose rapid deployment Gogs.

docker-compose.gogs.yaml Document reads as follows:

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

After executing the following command to start the Gogs program, visit 宿主机 IP:3000follow the configuration instructions to install Gogs, then you can create a remote repository.

Three, Gogs integration with Jenkins

Although most of Gogs Webhook recommend Jenkins plugin, but this plugin does not update a long time, and it does not support the release event. For to this problem, although there are official PR # 62 , but has not been consolidated, until the time of the merger all of these years. Here it is recommended to use the Generic WebHook the Trigger , with this plug-in to trigger Jenkins pipeline tasks.

3.1 Creating pipeline project

First, find the center Jenkins plugin, search Generic Webhook Trigger plug-in and install it.

20190924210101.gif

Continue to create a new pipeline task, named called the TestProject, select the type of Pipeline.

First, the configuration data source project, select SCM, and configuration address Git remote repository, if it is a private warehouse is also required to set the user name and password.

3.2 Jenkins configuration of Webhook

After the establishment of the pipeline project is completed, we can begin to set some parameters Generic WebHook Trigger in order to allow remote Gogs can trigger a build task.

我们为 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 来触发流水线任务。

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

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

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

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

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

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

四、完整的项目示例

In the previous section we have completed the remote repository push notifications by Jenkins plugin, when we merge code, Jenkins will automatically trigger the execution of our pipeline tasks. Next, I will build a .NET Core project, which has a Controller, output "Hello World" after receiving the request. Then establish a xUnit testing program for the project, for unit testing.

The project structure shown below:

We need to write a UnitTest.Dockerfilemirror for the implementation of xUnit unit testing.

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

# 还原 NuGet 包。
WORKDIR /home/app
COPY ./ ./
RUN dotnet restore

ENTRYPOINT ["dotnet", "test" , "--verbosity=normal"]

Followed by the deployment of a write operation Deploy.Dockerfile, first of all restore this Dockerfile NuGet package, and then dotnet publishpublish our Web command.

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"]

Two Dockerfile After completion, it will be stored in the root directory of the project to build Slave.

Dockerfile write well, then we have to write Docker Compose each image file into two, used to perform unit testing and deployment behavior for the file name of the deployment is called docker-compose.Deploy.yaml, as follows:

version: '3.1'

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

Then we need to write unit tests run Docker Compose file, called docker-compose.UnitTest.yaml, as follows:

version: '3.1'

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

Fifth, write 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'
}

Sixth, final results

After the above operation is completed, put these files in the root directory of the project.

Back to Jenkins, you can manually perform what task, then the project has been successfully executed.

At this point, our "low version" CI, CD environment to build success.

Guess you like

Origin www.cnblogs.com/myzony/p/11583702.html