linux+docker+jenkins 流水线形式发布.net core 项目

2021/8/30 7:07:57

本文主要是介绍linux+docker+jenkins 流水线形式发布.net core 项目,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

背景:

         1.当前CI/CD是企业级运维发布体系的核心组成部分。特别是当前微服务化理念越来越重,服务拆分的情况越来越多,会有很多的业务程序需要部署,发布,迭代至生产环境。这对运维人员,开发人员的维护是及其困难的。

         2.jenkins的出现允许开发人员对需要服务进行持续的CI/CD操作  CI 持续集成 CD 持续部署。 但是,网络上大部分的文章都是针对java的jenkins流水线自动化部署 。 .net core下寥寥无几 ,因此笔者开立博客,算是为.net生态贡献一下自己的力量!

 

      整个CI/CD 单机的流程如下:  笔者先演示单机实现CI/CD

 

 

环境准备如下:

                         docker-ce  最新版本(笔者推荐使用docker高版本  因为docker低版本有一些特殊的bug 有兴趣的童鞋可以看看docker的官方说明)

                         linux CentOS 8.4 64位  2台 服务器 (笔者需要演示 微服务 分布式架构下的发布 及平滑下线  因此需要至少2台服务器. 一台足够我们搭建jenkins的CI/CD环境)

                         git仓库  (笔者此篇使用git方式做代码仓库  此方式配置连接就行  git源无所谓  码云  coding github  gitlab  都行  服务器能访问到就行)

                         jenkins 客户端  只需要其中一台安装jenkins就行了 就像实际生产上jenkins的客户端也需要一台服务器就足够, 当然如果是多节点的jenkins多终端发布 那么也只需要配置一下即可

 

正片开始:

          如何在linux安装docker-ce  这里就不多说了 重点说明Jenkins相关的几个点

         1.开始在docker中安装jenkins,执行命令如下:

          docker run -tid -m 1G --memory-swap=1G --restart=always -v /data/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker --net=host --name jenkins docker.io/jenkins/jenkins                    笔者简单说一下这条命令核心在于  -v /data/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker   这三句 这三句主要是把dockers容器挂载在物理机的硬盘上 让jenking产生的记录能写入物理机硬盘 后面那句是挂载到docker中 因为我们是docker去跑的jenkins  对docker有一些基础的同学都知道 docker容器本身不挂载目录 需要指定 而且本篇主要讲的是 在docker中部署jenkins  然后使用jenkins部署 ,net core 项目已docker容器的方式运行。 所以jenkins需要有能执行docker命令的权限,因此需要挂载到docker目录下         

 

        此时我们已看到 jenkins 已经初始化了

        特别说明:至于jenkins初始化配置 这里笔者不做过多说明 那些网上都有 这里笔者直接跳过  

       2.jenkins中执行docker命令

         前面有提到过 我们的jenkins跟.net的应用程序 都是docker跑 因此jenkins必须要有能执行docker命令的权限

        所以  cd 到上一个步骤中 docker的挂载目录下 执行命令    chown -R 1000:1000 /data/jenkins  运行jenkins执行docker命令的权限 

    

       当然想在docker容器中运行docker命令的方式有很多  上面只是赋予权限的其中一种  官方有一种 docker  in docker的方式有兴趣的童靴可以尝试一下  或者更简单的方式 docker run 容器的时候 带上指定账户root  比如 docker run  -u root  的方式拥有权限 

       但是 笔者不推荐这样做 因为root的权限实在太大了  如果让jenkins拥有这个权限 是十分危险的一件事情

       3.创建流水线项目 已发布.net core 

        3.1 笔者新建一个流水线项目 如图所示:

       

 

  

 

 

 

    3.2 我们安装流程需要的jenkins插件可能会有点多.请逐个安装确认 插件列表如下:

            Build With Parameters  输入框式的参数(使用参数化构建需要用到此选项)

            Persistent Parameter  下拉框格式参数(使用参数化构建需要用到此选项)

            http request   http请求插件 用于后期 实现服务注册 服务发现 平滑下线使用

            Config File Provider  用于统一化构建配置文件使用  

            Git Parameter   git 参数插件

            docker 相关插件 用于 构建docker镜像 登录docker仓库 push docker镜像的时候使用

          3.3 然后 到项目中配置具体参数 这个根据各位童靴实际的业务场景来

           比如我这边参数 有 端口号 git仓库地址  docker仓库地址 统一的密码 健康检查url  等参数 

          3.4 推荐新建2个配置文件  一个是dockefile 因为作为运维当然希望dockerfile配置文件的统一规范化  因此把dockerfile 统一配置存放在jenkins中 如图所示

    

 

 

 

 

 

完成的配置文件如下:  远端的.net5.0运行时 跟SDK 笔者用XXX来替代了。 通常在dockerhub总会有很多的镜像用来打包 推荐各位童靴寻找合适的包然后push到自己私有仓库  dockerfile中用私有仓库的地址进行打包操作

FROM XXXXXXX AS base

WORKDIR /app
EXPOSE #PORT

FROM XXXXX AS build

WORKDIR /src
COPY ["#MODULE/#MODULE.csproj", "#MODULE/"]
RUN dotnet restore "#MODULE/#MODULE.csproj"
COPY . .
WORKDIR "/src/#MODULE"
RUN dotnet build "#MODULE.csproj" --configuration Release -o /app/build

FROM build AS publish
RUN dotnet publish "#MODULE.csproj" --configuration Release -o /app/publish

FROM base AS final

WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "#MODULE.dll"]

           

 同理  新建一个这样的配置

 

 

 

       

#!/bin/bash

JOB_NAME=$1
PORT=$2
dockerImageName=$3
HOST_IP=$4
ENV=$5
GRAY_VERSION=$6

# 获取容器的ID
containerID=`docker -H ${HOST_IP}:2375 ps -a |grep -w ${JOB_NAME} | awk '{print $1}'`

echo "${containerID}"

# 获取容器的imageID
imageID=`docker -H ${HOST_IP}:2375 images |grep -w ${JOB_NAME}| awk '{print $3}'`

echo "${imageID}"


if [ "${containerID}" != "" ] ; then
#删除容器
docker -H ${HOST_IP}:2375 ps -a |grep -w ${JOB_NAME} | awk '{print $1}'| xargs docker -H ${HOST_IP}:2375 rm -f
echo "成功删除容器"
fi

# 删除本地镜像

if [ "${imageID}" != "" ] ; then
#删除镜像
docker -H ${HOST_IP}:2375 images |grep -w ${JOB_NAME}| awk '{print $3}'| xargs docker -H ${HOST_IP}:2375 rmi -f
echo "成功删除镜像"
fi

# ENV=${ENV^}

#二次登录dcoker仓库
docker -H ${HOST_IP}:2375 login -u XXXXX-p XXXXX registry.cn-hangzhou.aliyuncs.com

echo ${ENV}
echo ${GRAY_VERSION}
# 运行镜像
docker -H ${HOST_IP}:2375 run -d \
-m 1G \
--memory-swap=1G \
-e ASPNETCORE_ENVIRONMENT=${ENV} \
-e ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=SkyAPM.Agent.AspNetCore \
-e grayscale_version=${GRAY_VERSION} \
--restart=always \
--net=host \
--name ${JOB_NAME} \
-p ${PORT}:${PORT} \
${dockerImageName}

docker -H ${HOST_IP}:2375 ps

# 应用健康检查
containerID=`docker -H ${HOST_IP}:2375 ps -a |grep -w ${JOB_NAME} | awk '{print $1}'`
echo $containerID

if [ $? = 0 ] ; then
echo "**应用${JOB_NAME}已经运行**"
else
echo "**应用${JOB_NAME}启动失败**"
fi

 

上面配置的意思 就是传入多个参数 用来去判断容器是否存在 如果存在 停止并删除容器  然后删除本地对应镜像  然后通过docker 2375的端口去执行docekr run的命令  最后去监听对应端口是否有挂载服务 如果有 输出 应用XXX 已经运行

特别说明: docker 具有开放端口允许外部调用的方式. 比如开启2375端口 可以允许远端来调用当前ip下的docker  执行对应的docker命令  实现jenkins远端发布的场景。 但是 因为笔者实际生产跑的都是docker环境 因此docker上面的容器会非常多,docker的安全性非常重要 针对类似于2375的端口允许外部调用的,请一定将2375 设置安全组规则  并且设置对应的白名单 具体到ip 此项非常重要

 最后就是笔者的流水线语法了:

// def tools = new org.devops.tools()

def AppName = "${JOB_NAME}"
def HOST_IP = ['ip1','ip2']
def createVersion() {
return new Date().format('yyyyMMddHHmmss') + "_${env.BUILD_ID}"
}
def deployment(HOSTIP){
timestamps {
script {
NacosRequestUrl = "http://${HOSTIP}:${PORT}/nacos/getStatus"
try {
result = httpRequest "${NacosRequestUrl}"
print("输出状态码")
print("${result.status}")
// 判断是否返回 200
if ("${result.status}" == "200") {
print "Http 请求成功"
sh """
echo "======== 执行 nacos 服务优雅下线 ========"
curl http://${HOSTIP}:${PORT}/nacos/deregister
sleep 10
sh ./docker_deploy.sh ${AppName} ${PORT} ${dockerImageName} ${HOSTIP} ${ENVIRONMENT}
"""
}
}
catch(Exception e){
sh """
echo ""======== 服务已下线,不需要执行优雅下线命令 "========"
sh ./docker_deploy.sh ${AppName} ${PORT} ${dockerImageName} ${HOSTIP} ${ENVIRONMENT}
"""
}
}
}

}

def healthcheck(HOSTIP){
timestamps {
script {
// 设置检测延迟时间 10s,10s 后再开始检测
sleep 30
// 健康检查地址
httpRequestUrl = "http://${HOSTIP}:${PORT}/${params.HTTP_REQUEST_URL}"
// 循环使用 httpRequest 请求,检测服务是否启动
for(n = 1; n <= "${params.HTTP_REQUEST_NUMBER}".toInteger(); n++){
try{
// 输出请求信息和请求次数
print "访问服务:${AppName} \n" +
"访问地址:${httpRequestUrl} \n" +
"访问次数:${n}"
// 如果非第一次检测,就睡眠一段时间,等待再次执行 httpRequest 请求
if(n > 1){
sleep "${params.HTTP_REQUEST_INTERVAL}".toInteger()
}
// 使用 HttpRequest 插件的 httpRequest 方法检测对应地址
result = httpRequest "${httpRequestUrl}"
// 判断是否返回 200
if ("${result.status}" == "200") {
print "Http 请求成功,流水线结束"
break
}
}
catch(Exception e){
print "监控检测失败,将在 ${params.HTTP_REQUEST_INTERVAL} 秒后将再次检测。"
// 判断检测次数是否为最后一次检测,如果是最后一次检测,并且还失败了,就对整个 Jenkins 任务标记为失败
if (n == "${params.HTTP_REQUEST_NUMBER}".toInteger()) {
currentBuild.result = "FAILURE"
}
}
}
}
}

}
pipeline {
agent { label 'master' }
environment {
version = createVersion()
AppName = "${JOB_NAME}"
}
//清理空间
stages {
stage('Clean阶段') {
steps {
timestamps {
cleanWs(
cleanWhenAborted: true,
cleanWhenFailure: true,
cleanWhenNotBuilt: true,
cleanWhenSuccess: true,
cleanWhenUnstable: true,
cleanupMatrixParent: true,
disableDeferredWipeout: true,
deleteDirs: true
)
}
}
}
stage('Git 阶段') {
when {
environment name: 'mode',value:'Deploy'
}
steps {
echo "start fetch code from git ${GIT_PROJECT_URL}"
buildDescription "发布机器:${HOST_IP} 构建模块: ${MODULE} 构建构建分支:${GIT_BRANCH}"
deleteDir()

checkout([$class: 'GitSCM',
branches: [[name: '*/master']],
extensions: [],
userRemoteConfigs: [[credentialsId: '4738804f-6a89-4149-9efa-a7cfa3d94536',
url: "${GIT_PROJECT_URL}"
]]])
script {
BUILD_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${BUILD_TAG}"
}
}

stage('Docker构建阶段') {
when {
environment name: 'mode',value:'Deploy'
}
steps {
timestamps {
script {
// 创建 Dockerfile 文件,但只能在方法块内使用
configFileProvider([configFile(fileId: "${params.DOCKER_DOCKERFILE_ID}", targetLocation: "Dockerfile-Template")]){
// 设置 Docker 镜像名称
dockerImageName = "${params.HARBOR_URL}/${params.ENVIRONMENT}:${BUILD_TAG}"
// 读取 Dockerfile 文件
dockerfile = readFile encoding: "UTF-8", file: "Dockerfile-Template"
// 替换 Dockerfile 文件中的变量,生成新的 NewDockerfile 文件
NewDockerfile = dockerfile.replaceAll("#PORT","${params.PORT}")
.replaceAll("#MODULE","${params.MODULE}")
writeFile encoding: 'UTF-8', file: './Dockerfile', text: "${NewDockerfile}"
// 输出新的Dockerfile 文件内容
sh "cat Dockerfile"
echo "${dockerImageName}"
// 判断 DOCKER_HUB_GROUP 是否为空,有些仓库是不设置仓库组的
if ("${params.ENV}" == '') {
dockerImageName = "${params.HARBOR_URL}:${BUILD_TAG}"
}
// 提供 Docker 环境,使用 Docker 工具来进行 Docker 镜像构建与推送
docker.withRegistry("http://${params.HARBOR_URL}", "${params.HARBOR_CREADENTIAL}") {
def customImage = docker.build("${dockerImageName}")
customImage.push()
}
}
configFileProvider([configFile(fileId: "docker_deploy", targetLocation: "docker_deploy.sh")]){
sh "cat docker_deploy.sh"
sh "chmod 755 docker_deploy.sh"
}

}
}
}
}
stage('Docker xxxxxx 发布阶段'){
when {
environment name: 'MODE',value:'Deploy'
}
steps{
deployment("${HOST_IP[0]}")
}
}
stage('Docker xxxxxx 健康检查阶段'){
steps {
healthcheck("${HOST_IP[0]}")
}
}
stage('Docker xxxxxx 发布阶段'){
when {
environment name: 'MODE',value:'Deploy'
}
steps{
deployment("${HOST_IP[1]}")
}
}
stage('Docker xxxxxxx 健康检查阶段'){
steps {
healthcheck("${HOST_IP[1]}")
}
}
}
//构建后操作
post{
success{
script{
if(params.MODE == 'Deploy'){
//tools.PrintMes("========pipeline executed successfully========",'green')

} else {
// tools.PrintMes("========pipeline executed successfully========",'green')

}
}
}
failure{
script{
if(params.MODE == 'Deploy'){
//tools.PrintMes("========pipeline execution failed========",'red')

} else {
//tools.PrintMes("========pipeline execution failed========",'red')

}
}
}
unstable{
script{
if(params.MODE == 'Deploy'){
//tools.PrintMes("========pipeline execution unstable========",'red')

} else {
//tools.PrintMes("========pipeline execution unstable========",'red')

}
}
}
aborted{
script{
if(params.MODE == 'Deploy'){
//tools.PrintMes("========pipeline execution aborted========",'blue')

} else {
// tools.PrintMes("========pipeline execution aborted========",'blue')

}
}
}
}
}

流水线语法中有几个点 笔者这里说明一下:  

1.checkout   流水线中的checkout 是jenkins的统一的流水线生成的语法 可直接在jenkins中生成  主要是为了 git相关的操作

2.docker.withRegistry  这也是流水线生成的语法  是统一封装好的 去登录docker仓库  打包当前的docker镜像  push镜像到远端

 好了 最后我们来构建一次!

此时我们可以看到已经构建成功 那么这一次的CI/CD 就成功结束了

 

 

 

笔者下一篇 将配合nacos 实现服务注册  服务发现  平滑下线的功能说明 



这篇关于linux+docker+jenkins 流水线形式发布.net core 项目的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程