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相关的几个点
此时我们已看到 jenkins 已经初始化了
特别说明:至于jenkins初始化配置 这里笔者不做过多说明 那些网上都有 这里笔者直接跳过
前面有提到过 我们的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拥有这个权限 是十分危险的一件事情
Build With Parameters 输入框式的参数(使用参数化构建需要用到此选项)
Persistent Parameter 下拉框格式参数(使用参数化构建需要用到此选项)
http request http请求插件 用于后期 实现服务注册 服务发现 平滑下线使用
Config File Provider 用于统一化构建配置文件使用
Git Parameter git 参数插件
docker 相关插件 用于 构建docker镜像 登录docker仓库 push docker镜像的时候使用
比如我这边参数 有 端口号 git仓库地址 docker仓库地址 统一的密码 健康检查url 等参数
完成的配置文件如下: 远端的.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 实现服务注册 服务发现 平滑下线的功能说明