背景:

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

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

 

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

 

jenkins docker发版 jenkins docker slave_IP

 

环境准备如下:

                         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=2G --restart=always -v /data/jenkins:/bitnami/jenkins/home -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker --net=host --name jenkins    --env LC_ALL=C.UTF-8 bitnami/jenkins

          首先 bitnami是dockerhub上支持比较好的镜像制作商  推荐使用bitnami的镜像  比如jenkins  pgsql  redis mq等一切可有的镜像

           --env LC_ALL=C.UTF-8  可以设置字符集  为中文格式  如果不设置此字符集 那么会出现乱码

 

所以jenkins需要有能执行docker命令的权限,因此需要挂载到docker目录下

        

jenkins docker发版 jenkins docker slave_docker_02

 

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

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

 

如果进入jenkins直接到了登录页面,没有设置账号密码的地方,是因为binami官方把jenkins的镜像做了修改 ,关于密码的部分 参考 https://github.com/bitnami/bitnami-docker-jenkins。  如果在docker run的时候不设置默认密码 那么bitnami讲启用默认密码 user  bitnami

       2.jenkins中执行docker命令

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

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

        同时因为jenkins是在docker中运行的 因此需要在docker容器 也就是jenkins中 执行docker命令 

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

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

      这里笔者推荐一种赋予权限的方式 就是  chmod 777 docker.sock 命令  其实 docker本身具有挂载相应的目录,文件到容器之中的能力,我们直接将docker命令以及docker daemon的socket文件挂载进运行容器之中即可运行docker命令

     因此 cd到宿主机对应目录下执行 chmod 777 docker.sock  便会允许docker容器中执行docker命令  但是 本身docker.sock 是在docker重启之后才生成的 所以这种方案有个缺陷就是每次重启 都需要重新执行命令  解决办法当然是有的 就是add一个docker的用户  添加到docker组中   然后以此用户重新run一次 jenkins镜像  就可以一劳永逸了!!!

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

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

       

jenkins docker发版 jenkins docker slave_jenkins docker发版_03

 

  

 

 

 

    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中 如图所示

    

jenkins docker发版 jenkins docker slave_git_04

 

 

 

jenkins docker发版 jenkins docker slave_docker_05

 

 

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

FROM XXXXXXX AS base
WORKDIR /app
EXPOSE #PORTFROM 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/buildFROM build AS publish
RUN dotnet publish "#MODULE.csproj" --configuration Release -o /app/publishFROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "#MODULE.dll"]

           

 同理  新建一个这样的配置

jenkins docker发版 jenkins docker slave_git_06

 

 

 

       

#!/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 $containerIDif [ $? = 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 就成功结束了

 

jenkins docker发版 jenkins docker slave_IP_07

 

 

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

特别鸣谢:醉仙桃