hugo-teek is loading...

K8s环境持续部署

最后更新于:

K8s环境持续部署

image-20230709095816871

目录

[toc]

推荐文章

https://www.yuque.com/xyy-onlyone/aevhhf?# 《玩转Typora》

image-20230624094222589

1、Kubectl 发布流水线

image-20230709101119001

CI流水线:

  • 下载代码
  • 构建单元测试
  • 代码扫描
  • 构建镜像
  • 更新发布文件

image-20230709101204102

image-20230709101441396

CD流水线:

  • 输入版本,从git下载部署文件;
  • 使用kubectl发布;
  • 使用kubectl回滚;

image-20230709101219592

image-20230709101518246

环境准备

jenkins

gitlab

sonarQube

harbor

image-20230703072539490

使用之前的npm项目

  • 使用之前的npm项目devops6-npm-service

如何构建,请看npm构建项目部分。

image-20230703121000171

  • 新建Dockerfile
1FROM nginx:1.17.0
2
3COPY index.html /usr/share/nginx/html/

image-20230703121318027

  • 编辑index.html文件
1<h1>VERSION: main</h1>

image-20230703121454410

  • 配置好sonar参数

创建sonar-project.properties文件

image-20230703121748214

 1# 定义唯一的关键字
 2sonar.projectKey=devops6-npm-service
 3
 4# 定义项目名称
 5sonar.projectName=devops6-npm-service
 6
 7# 定义项目的版本信息
 8sonar.projectVersion=1.0
 9 
10# 指定扫描代码的目录位置(多个逗号分隔)
11sonar.sources=src
12 
13# 执行项目编码
14sonar.sourceEncoding=UTF-8
15
16# 指定sonar Server
17sonar.host.url=http://172.29.9.101:9000
18
19# 认证信息
20sonar.login=admin
21sonar.password=Admin@123

然后提交项目代码。

创建一条新pipeline

  • 以原devops6-maven-service_CI为基础拷贝一条新流水线devops6-npm-service_K8SCI

image-20230703124225860

⚠️ 注意:但是这里的Git选项参数一直没效果,测试不出来,很奇怪。。。因此,就直接用默认的选项参数就好。

image-20230703124106609

  • 开始进行构建

image-20230703124309393

可以看待,前面几个过程都是ok的,这里上传制品过程,不需要。

创建harbor仓库

  • 镜像命名规范:

image-20230704055658325

  • 创建仓库

image-20230704055308745

image-20230704055323738

  • 修改本地 docker 服务使用 http 协议和私有仓库通信(否则会报错)
1#配置可信任(如果仓库是HTTPS访问不用配置)
2#在 daemon.json 中添加以下参数
3[root@harbor ~]# vim /etc/docker/daemon.json #创建此文件,并写入以下内容
4{
5"insecure-registries": ["172.29.9.120"] 
6}
7
8#重启docker 服务
9[root@harbor ~]# systemctl daemon-reload && systemctl restart docker

image-20230704061032271

image-20230704061012431

编写CI pipeline

  • 创建k8sci.jenkinsfile
 1@Library("devops06@main") _
 2
 3//import src/org/devops/xxx.groovy
 4def checkout = new org.devops.CheckOut()
 5def build = new org.devops.Build()
 6def sonar = new org.devops.Sonar()
 7
 8
 9//使用git 参数需要格式化
10env.branchName = "${env.branchName}" - "origin/"
11println(env.branchName)
12
13pipeline {
14    agent {label "build"}
15
16    //跳过默认的代码检出功能
17    options {
18        skipDefaultCheckout true
19    }    
20    
21
22    stages{
23        stage("CheckOut"){
24            steps{
25                script{
26                    checkout.CheckOut()
27                    
28                    //获取commitID
29                    env.commitID = checkout.GetCommitID()
30                    println("commitID: ${env.commitID}")
31
32                    // Jenkins构建显示信息
33                    currentBuild.displayName = "第${BUILD_NUMBER}次构建-${env.commitID}"
34                    currentBuild.description = "构建分支名称:${env.branchName}"
35                    //currentBuild.description = "Trigger by user jenkins \n branch: ${env.branchName}"
36                }
37            }
38        }
39
40        stage("Build"){
41            steps{
42                script{
43                    build.Build()
44                }
45            }
46
47        }        
48
49        stage("CodeScan"){
50            // 是否跳过代码扫描?
51            when {
52                environment name: 'skipSonar', value: 'false'
53            }
54            
55            steps{
56                script{
57                    sonar.SonarScannerByPlugin()        
58
59                }
60            }
61        }
62
63         stage("ImageBuild"){
64            steps{
65                script{
66                    //PushArtifactByPlugin()
67                    //PushArtifactByPluginPOM()
68
69                    // init package info
70                    appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
71                    repoName = appName.split('-')[0]   //devops6
72  
73                    imageName = "${repoName}/${appName}"
74                    imageTag = "${env.branchName}-${env.commitID}"
75                    sh """
76                        #登录镜像仓库
77                        docker login -u admin -p Harbor12345 172.29.9.120
78
79                        # 构建镜像
80                        docker build -t 172.29.9.120/${imageName}:${imageTag} .
81
82                        # 上传镜像
83                        docker push 172.29.9.120/${imageName}:${imageTag}
84
85                        # 删除镜像
86                        sleep 2
87                        docker rmi 172.29.9.120/${imageName}:${imageTag}
88                    """
89                }
90            }
91    
92        }       
93
94    }
95}
  • 在回放里运行

image-20230704061305147

image-20230704061326320

image-20230704061342760

  • 然后将代码提交到共享库里。

==准备k8s环境==

  • 启动好k8s环境

image-20230704220639938

  • 在devops06机器上安装kubectl工具
 1cat > /etc/yum.repos.d/kubernetes.repo << EOF
 2[kubernetes]
 3name=Kubernetes
 4baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
 5enabled=1
 6gpgcheck=0
 7repo_gpgcheck=0
 8gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
 9EOF
10
11[root@Devops6 ~]#yum install -y kubectl-1.20.0 --disableexcludes=kubernetes
  • 然后把k8s集群master节点下的~/.kube/config文件拷贝到devops6机器~/.kube/目录下

image-20230704221711066

  • 然后在k8s的2个节点先配置好 修改本地 docker 服务使用 http 协议和私有仓库通信(否则会报错)
1#配置可信任(如果仓库是HTTPS访问不用配置)
2#在 daemon.json 中添加以下参数
3[root@harbor ~]# vim /etc/docker/daemon.json #创建此文件,并写入以下内容
4{
5"insecure-registries": ["172.29.9.120"] 
6}
7
8#重启docker 服务
9[root@harbor ~]# systemctl daemon-reload && systemctl restart docker

记得:只需要在node1 node2上配置就行。

  • 自己k8s集群需不是一个ingress-controller。

image-20230704222438286

这里之前已经部署好ingress-controller了。

  • 配置下kubectl的自动补全
1#安装软件包
2yum install -y epel-release bash-completion
3
4#执行命令
5source /usr/share/bash-completion/bash_completion
6
7source <(kubectl completion bash)
8echo "source <(kubectl completion bash)" >> ~/.bashrc
9source ~/.bashrc

==创建一个devops6-deploy-repo仓库==

image-20230704223054699

  • 创建Deployment.yaml
 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: devops6-npm-service
 5spec:
 6  replicas: 3
 7  revisionHistoryLimit: 3
 8  selector:
 9    matchLabels:
10      app: devops6-npm-service
11  template:
12    metadata:
13      labels:
14        app: devops6-npm-service
15    spec:
16      containers:
17      - image: 172.29.9.120/devops6/devops6-npm-service:main-ed12ce10
18        name: devops6-npm-service
19        ports:
20        - containerPort: 80
21---
22apiVersion: v1
23kind: Service
24metadata:
25  name: devops6-npm-service
26spec:
27  type: ClusterIP
28  selector:
29    app: devops6-npm-service
30  ports:
31    - name: http
32      protocol: TCP
33      port: 80
34      targetPort: 80
35---
36apiVersion: networking.k8s.io/v1
37kind: Ingress
38metadata:
39  name: devops6-npm-service
40  annotations:
41    kubernetes.io/ingress.class: nginx
42spec:
43  rules:
44  - host: devops.test.com
45    http:
46      paths:
47      - path: /
48        pathType: Prefix
49        backend:
50          service:
51            name: devops6-npm-service
52            port: 
53              name: http

image-20230704224325940

  • 自己部署应用测试

创建新命名空间devops6

1[root@Devops6 ~]#kubectl create ns devops6

部署应用:

1[root@Devops6 ~]#kubectl  -n  devops6 apply -f Deployment.yaml 
2deployment.apps/devops6-npm-service created
3service/devops6-npm-service created
4ingress.networking.k8s.io/devops6-npm-service created

注意:这里直接加上命名空间后,应用就会直接被部署到此命名空间了。

1[root@Devops6 ~]#kubectl get po -ndevops6
2NAME                                  READY   STATUS    RESTARTS   AGE
3devops6-npm-service-bd4978ff9-27bpp   1/1     Running   0          32s
4devops6-npm-service-bd4978ff9-clkhm   1/1     Running   0          32s
5devops6-npm-service-bd4978ff9-x2sw8   1/1     Running   0          32s

配置ingress域名解析:

1[root@Devops6 ~]#vim /etc/hosts
2172.29.9.31 devops.test.com
3
4[root@Devops6 ~]#kubectl get ingress -ndevops6
5NAME                  CLASS    HOSTS             ADDRESS       PORTS   AGE
6devops6-npm-service   <none>   devops.test.com   172.29.9.31   80      8m11s

测试效果:

 1[root@Devops6 ~]#curl devops.test.com
 2<!DOCTYPE html>
 3<html>
 4  <head>
 5    <meta charset="utf-8">
 6    <meta name="viewport" content="width=device-width,initial-scale=1.0">
 7    <title>vuedemo</title>
 8  </head>
 9  <body>
10    <div id="app">
11      <h1>VERSION: main</h1>
12    </div>
13    <!-- built files will be auto injected -->
14  </body>
15</html>

==新建devops6-npm-service版本分支,特性分支==

  • 以main为基础新建devops6-npm-service版本分支RELEASE-1.1.1

修改index.html的内容为RELEASE-1.1.1

image-20230705133649522

  • 运行一次CI流水线

image-20230705133823641

要记得改下这里的jenkinsfile文件:

image-20230705134025853

成功生成镜像:

image-20230705134050399

image-20230705134124852

  • 我们再来更新一个版本的应用程序看下
 1[root@Devops6 ~]#vim Deployment.yaml
 2 3- image: 172.29.9.120/devops6/devops6-npm-service:main-ed12ce10
 4替换为
 5172.29.9.120/devops6/devops6-npm-service:RELEASE-1.1.1-7d906f68
 6
 7#然后部署应用:
 8[root@Devops6 ~]#kubectl apply -f Deployment.yaml -ndevops6
 9
10#验证
11[root@Devops6 ~]#curl devops.test.com
12<!DOCTYPE html>
13<html>
14  <head>
15    <meta charset="utf-8">
16    <meta name="viewport" content="width=device-width,initial-scale=1.0">
17    <title>vuedemo</title>
18  </head>
19  <body>
20    <div id="app">
21      <h1>VERSION: RELEASE-1.1.1</h1>
22    </div>
23    <!-- built files will be auto injected -->
24  </body>
25</html>
26#可以看到,已经更新成功了。
  • 我们来测试下回滚

回滚命令:

 1## 查看历史
 2kubectl rollout history deployment/anyops-devopsdocker-ui
 3
 4## 查看具体某一个历史版本信息
 5kubectl rollout history deployment/anyops-devopsdocker-ui --revision=2
 6
 7## 回滚上个版本 
 8kubectl rollout undo deployment/anyops-devopsdocker-ui -n anyops
 9
10## 回滚指定版本
11kubectl rollout undo deployment/nginx --to-revision=2

查看当前应用版本:

1[root@Devops6 ~]#kubectl rollout history deployment devops6-npm-service -ndevops6
2deployment.apps/devops6-npm-service 
3REVISION  CHANGE-CAUSE
41         <none>
52         <none>
6#可以看到有2个历史版本

我们打算回滚到上个历史版本:

先来查看下当前应用版本:

1watch -n 1 "curl devops.test.com"

image-20230706200642766

1watch -n 1 "curl -s devops.test.com"

开始回滚:

1[root@Devops6 ~]#kubectl rollout undo deployment devops6-npm-service -ndevops6
2deployment.apps/devops6-npm-service rolled back

回滚结果:

image-20230706200839138

image-20230706200908325

回滚成功。

  • 这里调的是gitlab的api

image-20230706201314056

调用gitlab api自动更新配置文件。

  • jenkins装一个插件HTTP Request

image-20230706201142874

  • devops6-npm-service项目ProjectID为11。

image-20230707070500615

  • 创建devops6-npm-service目录

image-20230707060343353

Deployment.yaml里的image改为__IMAGE_NAME__

image-20230707060513745

  • pipeline代码

Gitlab.groovy

 1package org.devops
 2
 3//获取文件内容
 4def GetRepoFile(projectId,filePath,branchName){
 5   //GET /projects/:id/repository/files/:file_path/raw
 6   apiUrl = "/projects/${projectId}/repository/files/${filePath}/raw?ref=${branchName}"
 7   response = HttpReq('GET', apiUrl)
 8   return response
 9}
10
11//更新文件内容
12def UpdateRepoFile(projectId,filePath,fileContent, branchName){
13    apiUrl = "projects/${projectId}/repository/files/${filePath}"
14    reqBody = """{"branch": "${branchName}","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
15    response = HttpReqByPlugin('PUT',apiUrl,reqBody)
16    println(response)
17
18}
19
20//创建文件
21def CreateRepoFile(projectId,filePath,fileContent, branchName){
22    apiUrl = "projects/${projectId}/repository/files/${filePath}"
23    reqBody = """{"branch": "${branchName}","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
24    response = HttpReqByPlugin('POST',apiUrl,reqBody)
25    println(response)
26
27}
28
29// 封装HTTP
30def HttpReqByPlugin(reqType, reqUrl,reqBody ){
31    def gitServer = "http://172.29.9.101:8076/api/v4"
32    withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', 
33                            variable: 'GITLABTOKEN')]) {
34        response = httpRequest acceptType: 'APPLICATION_JSON_UTF8', 
35                    consoleLogResponseBody: true, 
36                    contentType: 'APPLICATION_JSON_UTF8', 
37                    customHeaders: [[maskValue: false, name: 'PRIVATE-TOKEN', value: "${GITLABTOKEN}"]], 
38                    httpMode: "${reqType}", 
39                    url: "${gitServer}/${reqUrl}", 
40                    wrapAsMultipart: false,
41                    requestBody: "${reqBody}"
42
43    }
44    return response
45}
46
47
48
49//发起HTTP请求
50//调用gitlab api
51def HttpReq(method, apiUrl){
52
53    withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
54        response = sh  returnStdout: true, 
55        script: """ 
56        curl --location --request ${method} \
57        http://172.29.9.101:8076/api/v4/${apiUrl} \
58        --header "PRIVATE-TOKEN: ${gitlabtoken}"
59        """
60    }
61
62    //新增这段代码
63    try {
64        response = readJSON text: response - "\n" //json数据的读取方式
65    } catch(e){
66        println(e)
67    }
68    return response    
69}
70
71//获取ProjectID
72def GetProjectIDByName(projectName, groupName){
73    apiUrl = "projects?search=${projectName}"
74    response = HttpReq("GET", apiUrl)
75    if (response != []){
76        for (p in response) {
77            if (p["namespace"]["name"] == groupName){
78                return response[0]["id"]
79            }
80        }
81    }
82}
83
84//获取分支CommitID
85def GetBranchCommitID(projectID, branchName){
86    apiUrl = "projects/${projectID}/repository/branches/${branchName}"
87    response = HttpReq("GET", apiUrl)
88    return response.commit.short_id
89}

k8sci.jenkinsfile

  1@Library("devops06@main") _
  2
  3//import src/org/devops/xxx.groovy
  4def checkout = new org.devops.CheckOut()
  5def build = new org.devops.Build()
  6def sonar = new org.devops.Sonar()
  7def mygit = new org.devops.Gitlab()
  8
  9
 10//使用git 参数需要格式化
 11env.branchName = "${env.branchName}" - "origin/"
 12println(env.branchName)
 13
 14pipeline {
 15    agent {label "build"}
 16
 17    //跳过默认的代码检出功能
 18    options {
 19        skipDefaultCheckout true
 20    }    
 21    
 22
 23    stages{
 24        stage("CheckOut"){
 25            steps{
 26                script{
 27                    checkout.CheckOut()
 28                    
 29                    //获取commitID
 30                    env.commitID = checkout.GetCommitID()
 31                    println("commitID: ${env.commitID}")
 32
 33                    // Jenkins构建显示信息
 34                    currentBuild.displayName = "第${BUILD_NUMBER}次构建-${env.commitID}"
 35                    currentBuild.description = "构建分支名称:${env.branchName}"
 36                    //currentBuild.description = "Trigger by user jenkins \n branch: ${env.branchName}"
 37                }
 38            }
 39        }
 40
 41        stage("Build"){
 42            steps{
 43                script{
 44                    build.Build()
 45                }
 46            }
 47
 48        }        
 49
 50        stage("CodeScan"){
 51            // 是否跳过代码扫描?
 52            when {
 53                environment name: 'skipSonar', value: 'false'
 54            }
 55            
 56            steps{
 57                script{
 58                    sonar.SonarScannerByPlugin()        
 59
 60                }
 61            }
 62        }
 63
 64        stage("ImageBuild"){
 65            steps{
 66                script{
 67                    //PushArtifactByPlugin()
 68                    //PushArtifactByPluginPOM()
 69
 70                    // init package info
 71                    appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
 72                    repoName = appName.split('-')[0]   //devops6
 73  
 74                    imageName = "${repoName}/${appName}"
 75                    imageTag = "${env.branchName}-${env.commitID}"
 76                    env.fullImageName = "172.29.9.120/${imageName}:${imageTag}"
 77                    sh """
 78                        #登录镜像仓库
 79                        docker login -u admin -p Harbor12345 172.29.9.120
 80
 81                        # 构建镜像
 82                        docker build -t ${env.fullImageName} .
 83
 84                        # 上传镜像
 85                        docker push ${env.fullImageName}
 86
 87                        # 删除镜像
 88                        sleep 2
 89                        docker rmi ${env.fullImageName}
 90                    """
 91                }
 92            }
 93    
 94        }
 95        
 96        stage("UpdateEnvFile"){
 97            steps{
 98                script {
 99                    // 更新部署文件
100
101                    projectId = 11
102                    fileName = "Deployment.yaml"  //模板文件
103                    branchName = "main"
104
105                    //下载模板文件
106                    fileData = mygit.GetRepoFile(projectId,fileName,branchName)
107                    sh "rm -fr ${fileName}"
108
109                    //模板文件内容保存到本地
110                    writeFile file: fileName , text: fileData
111                    env.deployFile = fileName
112                    //替换镜像
113                    sh "sed -i 's#__IMAGE_NAME__#${env.fullImageName}#g' ${env.deployFile} "
114                    sh "ls -l ; cat ${fileName}"
115
116                    //创建/更新发布文件
117                    newYaml = sh returnStdout: true, script: "cat ${env.deployFile}"
118                    println(newYaml)
119                   
120                    //更新gitlab文件内容
121                    base64Content = newYaml.bytes.encodeBase64().toString()
122                    
123                    appName = "${JOB_NAME}".split('_')[0] //devops6-npm-service
124                    env.groupName = appName.split('-')[0]   //devops6
125                    env.projectName = appName
126
127                    // 会有并行问题,同时更新报错
128                    try {
129                        mygit.UpdateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
130                    } catch(e){
131                        mygit.CreateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
132                    }
133                }
134            }
135        }              
136
137    }
138}
  • 再次新建版本分支RELEASE-2.1.1,jenkins里记得配置下该分支名。

image-20230707070739603

  • 运行

image-20230707070829381

点击approve

image-20230707070146129

image-20230707070202014

image-20230707070928439

image-20230707070945375

image-20230707071022438

符合预期。

【GitOps最重要的一个特性: ==自动更新部署文件==】

更新生成版本文件

  • 步骤: 拿到 env仓库中的deployments.yaml模板文件, 然后替换内容,更新到版本库。

image-20230709102231404

  • 更新对象:

    • RELEASE_VERSION(镜像TAG)
  •  1       stage("UpdateEnvFile"){
     2            steps{
     3                script {
     4                    // 更新部署文件
     5
     6                    projectId = 11
     7                    fileName = "Deployment.yaml"  //模板文件
     8                    branchName = "main"
     9
    10                    //下载模板文件
    11                    fileData = mygit.GetRepoFile(projectId,fileName,branchName)
    12                    sh "rm -fr ${fileName}"
    13
    14                    //模板文件内容保存到本地
    15                    writeFile file: fileName , text: fileData
    16                    env.deployFile = fileName
    17                    //替换镜像
    18                    sh "sed -i 's#__IMAGE_NAME__#${env.fullImageName}#g' ${env.deployFile} "
    19                    sh "ls -l ; cat ${fileName}"
    20
    21                    //创建/更新发布文件
    22                    newYaml = sh returnStdout: true, script: "cat ${env.deployFile}"
    23                    println(newYaml)
    24
    25                    //更新gitlab文件内容
    26                    base64Content = newYaml.bytes.encodeBase64().toString()
    27
    28                    appName = "${JOB_NAME}".split('_')[0] //devops6-npm-service
    29                    env.groupName = appName.split('-')[0]   //devops6
    30                    env.projectName = appName
    31
    32                    // 会有并行问题,同时更新报错
    33                    try {
    34                        mygit.UpdateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
    35                    } catch(e){
    36                        mygit.CreateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
    37                    }
    38                }
    39            }
    40        }              
    
  • 更新后的版本文件

image-20230709102321743

编写CD pipeline

image-20230707073206225

  • 创建一个CD的job devops6-npm-service_K8SCD

image-20230707073334556

配置git仓库:

image-20230707074103792

image-20230707073918410

  • 编写pipeline

k8scd.jenkinsfile文件

 1@Library("devops06@main") _ 
 2
 3def mygit = new org.devops.Gitlab()
 4
 5//使用git 参数需要格式化
 6env.branchName = "${env.branchName}" - "origin/"
 7println(env.branchName)
 8
 9pipeline {
10    agent { label "build"}
11    options {
12        skipDefaultCheckout true
13    }
14    stages{
15        stage("GetManifests"){
16            steps{
17                script{
18                   
19                   //下载发布文件
20                    projectId = 11
21                    env.deployFile = "${env.branchName}.yaml"  //版本分支RELEASE-2.1.1.yaml
22                    env.appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
23                    filePath = "${env.appName}%2f${env.deployFile}" //devops6-npm-service/RELEASE-2.1.1.yaml
24                    branchName = "main"
25                    fileData = mygit.GetRepoFile(projectId,filePath,branchName)
26                    sh "rm -fr ${env.deployFile}"
27                    writeFile file: env.deployFile , text: fileData
28                    sh "ls -l ; cat ${env.deployFile}"
29                }
30            }
31        }
32
33        stage("Deploy"){
34            steps{
35                script{
36                    env.namespace = "${env.appName}".split('-')[0]   //devops6
37
38                    sh """
39                        ## 发布应用
40                        kubectl apply -f ${env.deployFile} -n ${env.namespace}
41
42                    """
43
44                    // 获取应用状态
45                    5.times{
46                        sh "sleep 2; kubectl -n ${env.namespace} get pod | grep ${env.appName}"
47                    }
48                }
49            }
50        }
51
52        stage("RollBack"){
53            input {
54                message "是否进行回滚"
55                ok "提交"
56                submitter ""
57                parameters {
58                    choice(choices: ['yes', 'no'], name: 'opts')
59                }
60            }
61            steps{
62                script{
63                    switch("${opts}") {
64                        case "yes":
65                            sh "kubectl rollout undo deployment/${env.appName} -n ${env.namespace} "
66                            break
67                        case "no":
68                            break
69                    }
70                }
71            }
72        }
73    }
74}
  • 运行流水线

我们来提前观察下此时版本:

image-20230707074010838

image-20230707074017724

image-20230707074135231

image-20230707074148261

image-20230707074204527

image-20230707074223776

符合预期。

完整代码

完整代码如下:

链接:https://pan.baidu.com/s/1XFtZ0epIwgVwu0jzQHyJkA?pwd=0820 提取码:0820

2023.7.9-day9-k8s-ci-cd (kubectl和helm ci-cd)

CI/CD共享库源码 前端项目 (Dockerfile) K8s清单文件代码仓库 helm chart仓库

环境:

1gitlab-ce:15.0.3-ce.0
2jenkins:2.346.3-2-lts-jdk11
3sonarqube:9.9.0-community
4harbor v2.6.2

image-20230709084502758

image-20230709084520218

image-20230709083713029

k8sci.jenkinsfile文件

  1@Library("devops06@main") _
  2
  3//import src/org/devops/xxx.groovy
  4def checkout = new org.devops.CheckOut()
  5def build = new org.devops.Build()
  6def sonar = new org.devops.Sonar()
  7def mygit = new org.devops.Gitlab()
  8
  9
 10//使用git 参数需要格式化
 11env.branchName = "${env.branchName}" - "origin/"
 12println(env.branchName)
 13
 14pipeline {
 15    agent {label "build"}
 16
 17    //跳过默认的代码检出功能
 18    options {
 19        skipDefaultCheckout true
 20    }    
 21    
 22
 23    stages{
 24        stage("CheckOut"){
 25            steps{
 26                script{
 27                    checkout.CheckOut()
 28                    
 29                    //获取commitID
 30                    env.commitID = checkout.GetCommitID()
 31                    println("commitID: ${env.commitID}")
 32
 33                    // Jenkins构建显示信息
 34                    currentBuild.displayName = "第${BUILD_NUMBER}次构建-${env.commitID}"
 35                    currentBuild.description = "构建分支名称:${env.branchName}"
 36                    //currentBuild.description = "Trigger by user jenkins \n branch: ${env.branchName}"
 37                }
 38            }
 39        }
 40
 41        stage("Build"){
 42            steps{
 43                script{
 44                    build.Build()
 45                }
 46            }
 47
 48        }        
 49
 50        stage("CodeScan"){
 51            // 是否跳过代码扫描?
 52            when {
 53                environment name: 'skipSonar', value: 'false'
 54            }
 55            
 56            steps{
 57                script{
 58                    sonar.SonarScannerByPlugin()        
 59
 60                }
 61            }
 62        }
 63
 64        stage("ImageBuild"){
 65            steps{
 66                script{
 67                    //PushArtifactByPlugin()
 68                    //PushArtifactByPluginPOM()
 69
 70                    // init package info
 71                    appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
 72                    repoName = appName.split('-')[0]   //devops6
 73  
 74                    imageName = "${repoName}/${appName}"
 75                    imageTag = "${env.branchName}-${env.commitID}"
 76                    env.fullImageName = "172.29.9.120/${imageName}:${imageTag}"
 77                    sh """
 78                        #登录镜像仓库
 79                        docker login -u admin -p Harbor12345 172.29.9.120
 80
 81                        # 构建镜像
 82                        docker build -t ${env.fullImageName} .
 83
 84                        # 上传镜像
 85                        docker push ${env.fullImageName}
 86
 87                        # 删除镜像
 88                        sleep 2
 89                        docker rmi ${env.fullImageName}
 90                    """
 91                }
 92            }
 93    
 94        }
 95        
 96        stage("UpdateEnvFile"){
 97            steps{
 98                script {
 99                    // 更新部署文件
100
101                    projectId = 11
102                    fileName = "Deployment.yaml"  //模板文件
103                    branchName = "main"
104
105                    //下载模板文件
106                    fileData = mygit.GetRepoFile(projectId,fileName,branchName)
107                    sh "rm -fr ${fileName}"
108
109                    //模板文件内容保存到本地
110                    writeFile file: fileName , text: fileData
111                    env.deployFile = fileName
112                    //替换镜像
113                    sh "sed -i 's#__IMAGE_NAME__#${env.fullImageName}#g' ${env.deployFile} "
114                    sh "ls -l ; cat ${fileName}"
115
116                    //创建/更新发布文件
117                    newYaml = sh returnStdout: true, script: "cat ${env.deployFile}"
118                    println(newYaml)
119                   
120                    //更新gitlab文件内容
121                    base64Content = newYaml.bytes.encodeBase64().toString()
122                    
123                    appName = "${JOB_NAME}".split('_')[0] //devops6-npm-service
124                    env.groupName = appName.split('-')[0]   //devops6
125                    env.projectName = appName
126
127                    // 会有并行问题,同时更新报错
128                    try {
129                        mygit.UpdateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
130                    } catch(e){
131                        mygit.CreateRepoFile(projectId,"${env.projectName}%2f${env.branchName}.yaml",base64Content, "main")
132                    }
133                }
134            }
135        }              
136
137    }
138}

k8scd.jenkinsfile文件:

 1@Library("devops06@main") _ 
 2
 3def mygit = new org.devops.Gitlab()
 4
 5//使用git 参数需要格式化
 6env.branchName = "${env.branchName}" - "origin/"
 7println(env.branchName)
 8
 9pipeline {
10    agent { label "build"}
11    options {
12        skipDefaultCheckout true
13    }
14    stages{
15        stage("GetManifests"){
16            steps{
17                script{
18                   
19                   //下载发布文件
20                    projectId = 11
21                    env.deployFile = "${env.branchName}.yaml"  //版本分支RELEASE-2.1.1.yaml
22                    env.appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
23                    filePath = "${env.appName}%2f${env.deployFile}" //devops6-npm-service/RELEASE-2.1.1.yaml
24                    branchName = "main"
25                    fileData = mygit.GetRepoFile(projectId,filePath,branchName)
26                    sh "rm -fr ${env.deployFile}"
27                    writeFile file: env.deployFile , text: fileData
28                    sh "ls -l ; cat ${env.deployFile}"
29                }
30            }
31        }
32
33        stage("Deploy"){
34            steps{
35                script{
36                    env.namespace = "${env.appName}".split('-')[0]   //devops6
37
38                    sh """
39                        ## 发布应用
40                        kubectl apply -f ${env.deployFile} -n ${env.namespace}
41
42                    """
43
44                    // 获取应用状态
45                    5.times{
46                        sh "sleep 2; kubectl -n ${env.namespace} get pod | grep ${env.appName}"
47                    }
48                }
49            }
50        }
51
52        stage("RollBack"){
53            input {
54                message "是否进行回滚"
55                ok "提交"
56                submitter ""
57                parameters {
58                    choice(choices: ['yes', 'no'], name: 'opts')
59                }
60            }
61            steps{
62                script{
63                    switch("${opts}") {
64                        case "yes":
65                            sh "kubectl rollout undo deployment/${env.appName} -n ${env.namespace} "
66                            break
67                        case "no":
68                            break
69                    }
70                }
71            }
72        }
73    }
74}

2、Helm CI/CD流水线

tstmp_20230709102436

image-20230709102517407

image-20230709102602647

image-20230709102618699

环境准备

删除devops6命名空间

1[root@Devops6 ~]#kubectl delete ns devops6

集群安装helm

 1[root@Devops6 ~]#tar xf helm-v3.7.2-linux-amd64.tar.gz
 2[root@Devops6 ~]#cd linux-amd64/
 3[root@Devops6 linux-amd64]#cp helm /usr/bin/
 4[root@Devops6 linux-amd64]#chmod +x  /usr/bin/helm
 5[root@Devops6 linux-amd64]#helm version
 6WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
 7WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
 8version.BuildInfo{Version:"v3.7.2", GitCommit:"663a896f4a815053445eec4153677ddc24a0a361", GitTreeState:"clean", GoVersion:"go1.16.10"}
 9[root@Devops6 linux-amd64]#helm repo add stable http://mirror.azure.cn/kubernetes/charts/
10WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
11WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
12"stable" has been added to your repositories
13[root@Devops6 linux-amd64]# helm repo list
14WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
15WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
16NAME  	URL                                      
17stable	http://mirror.azure.cn/kubernetes/charts/
18[root@Devops6 linux-amd64]#echo "source <(helm completion bash)" >> ~/.bashrc
19[root@Devops6 linux-amd64]#source ~/.bashrc
20WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
21WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
22[root@Devops6 linux-amd64]#helm list 
23WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
24WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
25NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION
26[root@Devops6 linux-amd64]#
  • 具体安装文档参考链接:

https://blog.csdn.net/weixin_39246554/article/details/123955289?ops_request_misc=&request_id=5c6a4510d72e40eabbcc39450945e6e4&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~koosearch~default-2-123955289-null-null.268^v1^control&utm_term=helm&spm=1018.2226.3001.4450

image-20230708102933431

创建helm仓库

devops6-helm-repo

image-20230708103100028

helm手动安装包

  • 生成helm chart
1[root@Devops6 ~]#helm create devops6-npm-service
2WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
3WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
4Creating devops6-npm-service
5[root@Devops6 ~]#cd devops6-npm-service/
6[root@Devops6 devops6-npm-service]#ls
7charts  Chart.yaml  templates  values.yaml
  • 编辑values.yaml

修改如下2处地方:

 1[root@Devops6 devops6-npm-service]#vim values.yaml
 2image:
 3  repository: 172.29.9.120/devops6/devops6-npm-service
 4  pullPolicy: IfNotPresent
 5  # Overrides the image tag whose default is the chart appVersion.
 6  tag: "RELEASE-2.1.1-8a398eee"
 7
 8……
 9ingress:
10  enabled: true
11  className: ""
12  annotations:
13    kubernetes.io/ingress.class: nginx
14    # kubernetes.io/tls-acme: "true"
15  hosts:
16    - host: devops.test.com
17      paths:
18        - path: /
19          pathType: Prefix

image-20230708104625493

image-20230708105629563

  • 生成部署文件
1[root@Devops6 devops6-npm-service]#helm template --output-dir manifests .
2WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
3WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
4wrote manifests/devops6-npm-service/templates/serviceaccount.yaml
5wrote manifests/devops6-npm-service/templates/service.yaml
6wrote manifests/devops6-npm-service/templates/deployment.yaml
7wrote manifests/devops6-npm-service/templates/ingress.yaml
8wrote manifests/devops6-npm-service/templates/tests/test-connection.yaml
  • 部署
 1[root@Devops6 devops6-npm-service]#helm install devops6-npm-service . -ndevops6 --create-namespace 
 2WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
 3WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
 4NAME: devops6-npm-service
 5LAST DEPLOYED: Sat Jul  8 10:57:40 2023
 6NAMESPACE: devops6
 7STATUS: deployed
 8REVISION: 1
 9NOTES:
101. Get the application URL by running these commands:
11  http://devops.test.com/
  • 验证
 1[root@Devops6 devops6-npm-service]#kubectl get po -ndevops6
 2NAME                                   READY   STATUS    RESTARTS   AGE
 3devops6-npm-service-7bcb6c49b5-rls6f   1/1     Running   0          28s
 4
 5[root@Devops6 devops6-npm-service]#watch -n 1 "curl -s devops.test.com"
 6Every 1.0s: curl -s devops.test.com             Sat Jul  8 10:58:46 2023                                                                                             
 7<!DOCTYPE html>
 8<html>
 9  <head>
10    <meta charset="utf-8">
11    <meta name="viewport" content="width=device-width,initial-scale=1.0">
12    <title>vuedemo</title>
13  </head>
14  <body>
15    <div id="app">
16      <h1>VERSION: RELEASE-2.1.1</h1>
17    </div>
18    <!-- built files will be auto injected -->
19  </body>
20</html>

部署成功。

  • 将次helm chart推送到刚才创建的helm仓库
 1#先删除manifests目录
 2[root@Devops6 devops6-npm-service]#pwd
 3/root/devops6-npm-service
 4[root@Devops6 devops6-npm-service]#ls
 5charts  Chart.yaml  manifests  templates  values.yaml
 6[root@Devops6 devops6-npm-service]#rm -rf manifests/
 7
 8#推送 (注意:这次试验这里是master分支)
 9cd existing_folder
10git init
11git remote add origin http://172.29.9.101:8076/devops6/devops6-helm-repo.git
12git add .
13git commit -m "Initial commit"
14git push -u origin master

image-20230708110348017

harbor上开启helm chart

  • 默认harbor没有helm chart功能,需要另外配置下才行

image-20230708111712317

  • 配置
1开启helm charts
2./install.sh  --with-chartmuseum
3helm repo add devops6repo http://172.29.9.120/chartrepo/devops6/  --username=admin --password=Harbor12345

image-20230708111953115

image-20230708112138351

配置完后,harbor界面就出现了helm chart选项了:

image-20230708112212739

添加过程:

 1[root@Devops6 devops6-npm-service]#helm repo list
 2WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
 3WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
 4NAME  	URL                                      
 5stable	http://mirror.azure.cn/kubernetes/charts/
 6[root@Devops6 devops6-npm-service]#helm repo add devops6repo http://172.29.9.120/chartrepo/devops6/  --username=admin --password=Harbor12345
 7WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
 8WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
 9"devops6repo" has been added to your repositories
10[root@Devops6 devops6-npm-service]#helm repo list
11WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /root/.kube/config
12WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /root/.kube/config
13NAME       	URL                                      
14stable     	http://mirror.azure.cn/kubernetes/charts/
15devops6repo	http://172.29.9.120/chartrepo/devops6/   

helm安装cm-push软件

image-20230708223328263

CI pipeline

pipeline代码

创建CI 流水线

  • devops6-npm-service_K8SCI为基础新创建devops6-npm-service_HELMCI项目

image-20230708110659948

修改为helmci.jenkinsfile

image-20230708110755571

  • 最终

image-20230709083045417

CI 测试

image-20230709083102471

image-20230709083152825

image-20230709083212264

CD pipeline

pipeline代码

创建CD流水线

  • devops6-npm-service_K8SCD为基础新创建devops6-npm-service_HELMCD项目

image-20230708111046803

修改为helmcd.jenkinsfile

image-20230708111355792

修改选线参数:

image-20230709082758598

最终:

image-20230709082857194

CD测试

image-20230709083003688

image-20230709082930825

image-20230709082950852

完整代码

完整代码如下:

链接:https://pan.baidu.com/s/1XFtZ0epIwgVwu0jzQHyJkA?pwd=0820 提取码:0820

2023.7.9-day9-k8s-ci-cd (kubectl和helm ci-cd)

CI/CD共享库源码 前端项目 (Dockerfile) K8s清单文件代码仓库 helm chart仓库

环境:

1gitlab-ce:15.0.3-ce.0
2jenkins:2.346.3-2-lts-jdk11
3sonarqube:9.9.0-community
4harbor v2.6.2  

image-20230709084502758

image-20230709084520218

image-20230709083713029

helmci.jenkinsfile文件

  1@Library("devops06@main") _ 
  2
  3//import  src/org/devops/Build.groovy
  4def build = new org.devops.Build()
  5def sonar = new org.devops.Sonar()
  6def checkouts = new org.devops.CheckOut()
  7def mygit = new org.devops.Gitlab()
  8
  9//使用git 参数需要格式化
 10env.branchName = "${env.branchName}" - "origin/"
 11println(env.branchName)
 12
 13pipeline {
 14    agent { label "build"}
 15    options {
 16        skipDefaultCheckout true
 17    }
 18    stages{
 19        stage("CheckOut"){
 20            steps{
 21                script{
 22                   checkouts.CheckOut()
 23                }
 24            }
 25        }
 26        stage("Build"){
 27            steps{
 28                script{
 29                    build.Build()
 30                }
 31            }
 32        }
 33
 34        stage("CodeScan"){
 35            steps{
 36                script{
 37                    sonar.SonarScannerByPlugin()
 38                }
 39            }
 40        }
 41        stage("ImageBuild"){
 42            steps{
 43                script{
 44                    //PushArtifactByPlugin()
 45                    //PushArtifactByPluginPOM()
 46                    // init package info
 47                    appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
 48                    repoName = appName.split('-')[0]   //devops6
 49                    
 50                    //commitID
 51                    commitID = checkouts.GetCommitID()
 52                    println(commitID)
 53
 54                    // Jenkins构建显示信息
 55                    currentBuild.displayName = "第${BUILD_NUMBER}次构建-${commitID}"
 56                    currentBuild.description = "构建分支名称:${env.branchName}"
 57
 58                    imageName = "${repoName}/${appName}"
 59                    env.imageTag = "${env.branchName}-${commitID}"
 60                    env.fullImageName = "172.29.9.120/${imageName}:${env.imageTag}"
 61                    sh """
 62                        #登录镜像仓库
 63                        docker login -u admin -p Harbor12345 172.29.9.120
 64
 65                        # 构建镜像
 66                        docker build -t ${env.fullImageName} .
 67
 68                        # 上传镜像
 69                        docker push ${env.fullImageName}
 70
 71                        # 删除镜像
 72                        sleep 2
 73                        docker rmi ${env.fullImageName}
 74                    """ 
 75                }
 76            }
 77        }
 78        stage("UpdateEnvFile"){
 79            steps{
 80                script {
 81                    // 更新部署文件
 82
 83                    projectId = 12 //helm repo id
 84                    fileName = "values.yaml"  //模板文件
 85                    branchName = "master"
 86                    //下载模板文件
 87                    fileData = mygit.GetRepoFile(projectId,fileName,branchName)
 88                    sh "rm -fr ${fileName}"
 89
 90                    //修改镜像tag
 91                    yamlData = readYaml text: fileData
 92                    yamlData.image.tag = "${env.imageTag}"
 93
 94                    //模板文件内容保存到本地
 95                    writeYaml file: "${fileName}" , data: yamlData
 96
 97                
 98                    //创建/更新发布文件
 99                    newYaml = sh returnStdout: true, script: "cat ${fileName}"
100                    println(newYaml)
101                    //更新gitlab文件内容
102                    base64Content = newYaml.bytes.encodeBase64().toString()
103                    
104
105                    // 会有并行问题,同时更新报错
106                    try {
107                        mygit.UpdateRepoFile(projectId,"${fileName}",base64Content, "master")
108                    } catch(e){
109                        mygit.CreateRepoFile(projectId,"${fileName}",base64Content, "master")
110                    }
111                }
112            }
113        }
114        stage("HelmPackage"){
115            steps{
116                script{
117                    // helm pakcage & push harbor
118                    appName = "${JOB_NAME}".split('_')[0]
119                    sh "pwd && ls -l"
120                    sh "mkdir -p ${appName} && cd ${appName}"
121                    ws("${workspace}/${appName}"){
122                        checkout([$class: 'GitSCM', branches: [[name: '*/master']], 
123                            extensions: [], 
124                            userRemoteConfigs: [[credentialsId: 'gitlab-root', 
125                            url: 'http://172.29.9.101:8076/devops6/devops6-helm-repo.git']]])
126
127                        sh "ls -l"
128                    }
129                    //helm package
130                    chartVersion = "${env.branchName}".split("-")[-1]  //1.1.1
131                    sh """ 
132                        helm package ${appName} --version ${chartVersion}
133                        helm-cm-push ${appName}-${chartVersion}.tgz devops6repo
134                    """
135
136                }
137            }
138        }
139    }
140}

helmcd.jenkinsfile文件

 1@Library("devops06@main") _ 
 2
 3def checkout = new org.devops.CheckOut()
 4def build = new org.devops.Build()
 5
 6//使用git 参数需要格式化
 7env.branchName = "${env.branchName}" - "origin/"
 8println(env.branchName)
 9
10//commitID
11env.commitID = checkouts.GetCommitID()
12println(commitID)
13
14// Jenkins构建显示信息
15currentBuild.displayName = "第${BUILD_NUMBER}次构建-${env.commitID}"
16currentBuild.description = "构建分支名称:${env.branchName}"
17
18pipeline {
19    agent { label "build"}
20    options {
21        skipDefaultCheckout true
22    }
23    stages{
24        stage("GetHelmChart"){
25            steps{
26                script{
27                    //下载helm chart
28                    env.chartVersion = "${env.branchName}".split("-")[-1]
29                    env.appName = "${JOB_NAME}".split('_')[0]
30                    sh """
31                        helm repo update devops6repo
32                        helm pull devops6repo/${env.appName} --version ${env.chartVersion}
33                    """
34                }
35            }
36        }
37
38        stage("Deploy"){
39            steps{
40                script{
41                    env.namespace = "${env.appName}".split('-')[0]   //devops6
42
43                    sh """
44                        ## 发布应用
45                        helm upgrade --install --create-namespace  "${env.appName}" ./"${env.appName}"-${env.chartVersion}.tgz -n ${env.namespace}
46                        helm history "${env.appName}"  -n ${env.namespace}
47                    """
48
49                    // 获取应用状态
50                    5.times{
51                        sh "sleep 2; kubectl -n ${env.namespace} get pod | grep ${env.appName}"
52                    }
53
54                    //收集history
55                    env.revision = sh returnStdout: true, 
56                                      script: """helm history ${env.appName} -n ${env.namespace} | grep -v 'REVISION' | awk '{print \$1}' """
57                    println("${env.revision}")
58                    println("${env.revision.split('\n').toString()}")
59                    env.REVISION = "${env.revision.split('\n').toString()}"
60                    println("${env.REVISION}")
61                }
62            }
63        }
64
65
66        stage("RollOut"){
67            input {
68                message "是否进行回滚"
69                ok "提交"
70                submitter ""
71                parameters {
72                    choice(choices: ['yes', 'no'], name: 'opts')
73                }
74            }
75
76            steps{
77                script{
78
79                   switch("${opts}") {
80                        case "yes":
81                        def result = input  message: "选择回滚版本?", 
82                            parameters: [choice(choices: env.REVISION, name: 'rversion')]
83
84                        println("${result}")
85                        sh "helm rollback ${env.appName} ${result} -n ${env.namespace} "
86                        break
87
88                        case "no":
89                        break
90                    }
91                }
92            }
93        }
94    }
95}

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 微信二维码 x2675263825 (舍得), qq:2675263825。

image-20230107215114763

🍀 微信公众号 《云原生架构师实战》

image-20230107215126971

🍀 语雀

https://www.yuque.com/xyy-onlyone

image-20230624093747671

🍀 csdn https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

image-20230107215149885

🍀 知乎 https://www.zhihu.com/people/foryouone

image-20230107215203185

最后

好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

推荐使用微信支付
微信支付二维码
推荐使用支付宝
支付宝二维码
最新文章

文档导航