Jenkins发布打包好的镜像到K8s

1,278 阅读6分钟

使用Docker创建Jenkins容器

# 主要是在容器中挂载docker程序

docker run -d -u 0 -p 8099:8080 \
-p 50099:50000 \
-v /usr/local/jenkins:/var/jenkins_home \
-v /usr/bin/docker:/usr/bin/docker \
-v /var/run/docker.sock:/var/run/docker.sock \
--restart always \
--name jenkins jenkins/jenkins:centos7-jdk11

K8s创建ServiceAccount并获取token

首先创建一个命名空间

# 创建namespace.yaml文件,后面的操作在改名称空间下
cat > namespace.yaml << EOF
apiVersion: v1
kind: Namespace
metadata:
   name: demoapp
   labels:
        name: demoapp
EOF

# 应用yaml文件
kubectl apply -f namespace.yaml

创建ServiceAccount获取token

创建yaml文件

# serviceAccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: jenkins
  name: jenkins
  namespace: demoapp

---
# 注意apiGroups要写apps,写""默认为核心apiGroup
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: jenkins
rules:
 - apiGroups: ["apps"]
   resources: ["namespaces","deployments"]
   verbs: ["create","delete","get","list","patch","update","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
 name: jenkins
 namespace: demoapp
roleRef:
 apiGroup: rbac.authorization.k8s.io
 kind: ClusterRole
 name: jenkins
subjects:
 - kind: ServiceAccount
   name: jenkins
   namespace: demoapp
---
# k8s v1.24.0之后创建ServiceAccount之后不会自动创建Secret,需要手动创建
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
     name: jenkins
     namespace: demoapp
     annotations:
         kubernetes.io/service-account.name: "jenkins"

应用yaml文件

[root@master demoapp]# kubectl apply -f serviceAccount.yaml 
serviceaccount/jenkins created
role.rbac.authorization.k8s.io/jenkins created
rolebinding.rbac.authorization.k8s.io/jenkins created
secret/jenkins created

获取token

# 获取k8s token
kubectl get secret jenkins -n demoapp -o jsonpath={".data.token"} | base64 -d

将刚才获取的K8s的token添加到Jenkins中,该token用于后面向k8s发送http请求时候使用

类型选择Secret-text

image.png

在Jenkins中创建Gitlab的token

首先获取Gitlab的token

image.png

将获取的Gitlab的token添加到Jenkins中,该token用于后面向Gitlab发送http请求使用

image.png

同时在Jenkins中创建gitlab的用户密码类型的凭据,用于后面拉取代码

image.png

Jenkins安装需要用到的插件

首先先实现自动打包Docker镜像并推送到Harbor

在jenkins创建一个名称为k8s-devops流水线的项目

image.png

配置webhook

配置Jenkins中的webhook

选择Generic Webhook Trigger

image.png

设置该Trigger

image.png

在Gitlab设置对应的webhook,当有代码提交的时候会自动触发jenkins构建

image.png

添加变量

image.png

结果测试

  • 在该pipeline中添加脚本,内容如下

image.png

  • 往Gitlab推送代码后,可以看到Jenkins的构建控制台打印输出了该变量

image.png

编译代码,并生成docker镜像推送到Harbor

再添加一个变量,用于获取gitlab项目请求地址

image.png

编写pipeline脚本自动拉取代码

pipeline脚本如下

pipeline {
  agent any
  stages {
    // 拉取代码
    stage('pullCode') {
      steps {
        script {
          branchName = ref - 'refs/heads/'   // 获取分支名
          // 拉取分支的语法可以用jenkins自动生成,然后修改下就可以
          checkout scmGit(branches: [[name: branchName]], extensions: [], userRemoteConfigs: [[credentialsId: 'gitlab-auth', url: gitUrl]])
        }
      }
    }
  }
}

结果测试

image.png

打包构建镜像推送到Harbor

首先在Jenkins中添加Harbor的凭证

image.png

修改pipeline的脚本如下

pipeline {
  agent any
  stages {
    // 拉取代码
    stage('pullCode') {
      steps {
        script {
          branchName = ref - 'refs/heads/'
          checkout scmGit(branches: [[name: "${branchName}"]], extensions: [], userRemoteConfigs: [[credentialsId: 'gitlab-auth', url: "${gitUrl}"]])
        }
      }
    }
    
    // 打包
    stage('goBuild') {
      steps {
        script {
          docker.image('golang:bullseye').inside {
            sh """
              ls -al
              go version
              go build -o main
            """
          }

        }
      }
    }
    
    // 构建镜像
    stage('BuildImage') {
      steps {
        script {
          withCredentials([usernamePassword(credentialsId: 'harbor', passwordVariable: 'harborPass', usernameVariable: 'harborUser')]) {
            sh """
              # 登陆harbor
              docker login -u ${harborUser} -p ${harborPass} www.mysite.top
              # 构建镜像,docker会使用go项目中写好的docerfile文件构建镜像
              docker build -t www.mysite.top/web/demo-go-app:${branchName} .
              sleep 1
              # 上传镜像
              docker push www.mysite.top/web/demo-go-app:${branchName}
              sleep 1
              docker rmi www.mysite.top/web/demo-go-app:${branchName}
            """
          }

        }
      }
    }
  }
}

查看结果

Harbor中已推送成功镜像

image.png

编写Jenkins的library用于请求

jenkins的library作用是将一些通用的代码封装在一起,以后在不同的流水线中都可以调用

项目目录结构

➜  jenkinslib git:(main) ✗ tree                 
.
├── README.md
└── src
    └── org
        └── devops
            ├── gitlab.groovy
            └── kubernetes.groovy

3 directories, 3 files

创建K8s请求服务使用的library

k8s接口请求文档

// kubernetes.groovy文件
package org.devops

// 封装http请求
def httpReq(reqType, reqUrl, reqBody) {
  def result
  // k8s的api可以参考https://kubernetes.io/zh-cn/docs/reference/using-api/api-concepts/
  def apiServer = 'https://192.168.2.24:6443/apis/apps/v1'
  withCredentials([string(credentialsId: 'k8s-token', variable: 'k8stoken')]) {
    result = httpRequest consoleLogResponseBody: true,
                        customHeaders: [[maskValue: true, name: 'Authorization', value: "Bearer ${k8stoken}"], [maskValue: false, name: 'Content-Type', value: 'application/yaml'], [maskValue: false, name: 'Accept', value: 'application/yaml']],
                        httpMode: reqType,
                        ignoreSslErrors: true,
                        requestBody: reqBody,
                        url: "${apiServer}/${reqUrl}",
                        wrapAsMultipart: false
  }

  return result
}

// 获取某一个deployment
def getDeployment(nameSpace, deployName) {
  apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"
  response = httpReq('GET', apiUrl, '')
  return response
}

// 更新deployment
def updateDeployment(nameSpace, deployName, deployBody) {
  apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"

  response = httpReq('PATCH', apiUrl, deployBody)
  println(response)
}

// 获取所有的deployments
def getAllDeployment(nameSpace) {
  apiUrl = "namespaces/${nameSpace}/deployments"
  response = httpReq('GET', apiUrl, '')
  return response.content
}

// 创建一个deployment
def createDeployment(nameSpace, deployBody) {
  apiUrl = "namespaces/${nameSpace}/deployments"
  response = httpReq('POST', apiUrl, deployBody)
  return response.content
}

// 删除一个deployment
def deleteDeployment(nameSpace, deployName) {
    apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"
  response = httpReq('DELETE', apiUrl, '')
  return response.content
}

创建Gitlab请求所使用的library

// gitlab.groovy文件
package org.devops

// 封装http请求
def httpReq(reqType, reqUrl, reqBody) {
  def apiServer = 'http://172.157.38.2/api/v4'
  withCredentials([string(credentialsId: 'gitlab-token', variable: 'gitlabtoken')]) {
    result = httpRequest consoleLogResponseBody: true,
                        contentType: 'APPLICATION_JSON',
                        customHeaders: [[maskValue: true, name: 'PRIVATE-TOKEN', value: "${gitlabtoken}"]],
                        httpMode: reqType,
                        ignoreSslErrors: true,
                        requestBody: reqBody,
                        url: "${apiServer}/${reqUrl}",
                        wrapAsMultipart: false
  }

  return result
}

// 获取文件内容
def  getRepoFile(projectId, filePath) {
  apiUrl = "projects/${projectId}/repository/files/${filePath}/raw"

  response = httpReq('GET', apiUrl, '')
  return response.content
}
// 更新文件内容
def updateRepoFile(projectId, filePath, fileContent) {
  apiUrl = "projects/${projectId}/repository/files/${filePath}"
  reqBody = """
    {"branch": "main", "encoding": "base64", "content": "${fileContent}", "commit_message": "Update a new file"}
  """
  response = httpReq('PUT', apiUrl, reqBody)
  println(response.content)
}

使用Gitlab的api,获取yaml文件文件

gitlab api参考链接

在Jenkins中添加上面创建的共享库

image.png

使用K8s部署项目

K8s上创建一个Docker登陆使用的Secret

kubectl -n demoapp create secret docker-registry registry-key \ 
  --docker-server=www.mysite.top \ 
  --docker-username=root \ 
  --docker-password=Harbor12345 

创建一个仓库,用于单独保存K8s部署使用的yaml文件

以后有新的代码提交的时候,都会先从gitlab中拉取该yaml文件,然后将该yaml文件中的镜像替换为新的镜像,从而得到一个新的yaml文件

# 目录结构如下,仓库名称为k8s-info-service

➜  k8s-info-service git:(main) tree 
.
├── README.md
└── uat
    └── deploy.yaml

# deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-go-app
spec:
  selector:
    matchLabels:
      app: demo-go-app
  template:
    metadata:
      labels:
        app: demo-go-app
    spec:
      imagePullSecrets:
        - name: registry-key  # 拉取镜像时候使用指定的key
      containers:
        - name: demo-go-app
          image: www.mysite.top/web/demo-go-app:release-v0.0.1
          imagePullPolicy: Always

K8s自动部署的pipeline脚本文件

// 引用共享库
@Library('jenkinslib@main') _
// 使用java包里面的Base64,因为groovy兼容java
import java.util.Base64

def gitlab = new org.devops.gitlab()
def k8s = new org.devops.kubernetes()

pipeline {
  agent any
  stages {
    // 拉取代码
    stage('pullCode') {
      steps {
        script {
          branchName = ref - 'refs/heads/'
          checkout scmGit(branches: [[name: "${branchName}"]], extensions: [], userRemoteConfigs: [[credentialsId: 'gitlab-auth', url: "${gitUrl}"]])
        }
      }
    }
    
    // 构建代码
    stage('goBuild') {
      steps {
        script {
          docker.image('golang:bullseye').inside {
            sh """
              ls -al
              go version
              go build -o main
            """
          }

        }
      }
    }
    
    // 生成镜像
    stage('BuildImage') {
      steps {
        script {
          withCredentials([usernamePassword(credentialsId: 'harbor', passwordVariable: 'harborPass', usernameVariable: 'harborUser')]) {
            sh """
              # 登陆harbor
              docker login -u ${harborUser} -p ${harborPass} www.mysite.top
              # 构建镜像
              docker build -t www.mysite.top/web/demo-go-app:${branchName} .
              sleep 1
              # 上传镜像
              docker push www.mysite.top/web/demo-go-app:${branchName}
              sleep 1
              docker rmi www.mysite.top/web/demo-go-app:${branchName}
            """
          }

        }
      }
    }
    
    // 部署到k8s
    stage('Deployment') {
      steps {
        script {
          dockerImage = "www.mysite.top/web/demo-go-app:${branchName}"
          // 下载版本库文件
          // 使用前面创建的k8s-info-service项目,查看gitlab该项目的id为5,所以第一个参数传5
          response = gitlab.getRepoFile(5, "uat%2fdeploy.yaml")

          // 替换文件的内容(替换镜像)
          fileData = readYaml text: """${response}"""
          fileData["spec"]["template"]["spec"]["containers"][0]["image"] = dockerImage

          String ret = writeYaml returnText: true, data: fileData  // 将yaml转换为字符串
          
          String base64encodedString = Base64.getEncoder().encodeToString(ret.getBytes("utf-8"));  // 将字符串进行base64编码
          println(base64encodedString)
          // 更新gitlab中的内容
          gitlab.updateRepoFile(5, 'uat%2fdeploy.yaml', base64encodedString)

          //获取部署的deployment状态
          deployment = k8s.getAllDeployment("demoapp")
          
          // 如果demo-go-app不存在,部署
          yamlData = readYaml text: deployment

          // 定义一个包含所有的deployemt的数组
          deploymentList = []
          
          // 遍历,将demoapp名称空间下的deployment都取出来
          for (item in yamlData.items) {
            deploymentList.add(item.metadata.name)
          }
          
          if (deploymentList.contains('demo-go-app')) {
            // 如果已经包含了这个deployment,则先删除,再创建
            println("如果已经包含了这个deployment")
            k8s.deleteDeployment("demoapp", "demo-go-app")
            sleep 1
            k8s.createDeployment("demoapp",  ret)
          } else {
            // 没有包含这个deployment,创建
            println("没有包含这个deployment,创建")
            k8s.createDeployment("demoapp",  ret)
          }

        }
      }
    }
  }
}

创建一个新的分支

git push origin release-v0.0.2:release-v0.0.2

可以看到jenkins开始打包构建了

查看结果

[root@master demoapp]# kubectl get pods -n demoapp
NAME                           READY   STATUS              RESTARTS   AGE
demo-go-app-54fc9b7596-pv747   0/1     ContainerCreating   0          18s

创建一个Service

apiVersion: v1
kind: Service
metadata:
  name: demo-go-app
  namespace: demoapp
spec:
  selector:
    app: demo-go-app
  type: NodePort
  ports:
     - protocol: TCP
       port: 9090
       targetPort: 9090
       nodePort: 31111

应用后访问测试,可以正常访问

➜  ~ curl 192.168.3.192:31111/hello
{"code":200,"success":true}%   

其他内容

本次测试使用的dockerfile

FROM ubuntu:latest

COPY main /main
RUN chmod +x /main
EXPOSE 9090
ENTRYPOINT ["/main"]