k8s之cicd搭建

在Kubernetes中进行CI/CD的过程,一般的步骤如下:

  • 在GitLab中创建对应的项目
  • 配 置 Jenkins 集成 Kubernetes 集 群 , 后期Jenkins的Slave在Kubernetes中动态创建的Slave
  • Jenkins 创建对应的任务(Job),集成该项目的Git地 址 和Kubernetes集群
  • 开发者将代码提交到GitLab
  • 若配置了钩子,则推送(Push)代码会自动触发Jenkins构建,若没有配置钩子,则需要手动构建
  • Jenkins控制Kubernetes(使用的是Kubernetes插件)创建JenkinsSlave(Pod形式)
  • Jenkins Slave根据流水线定义的步骤执行构建
  • 通过Dockerfile生成镜像
  • 将镜像提送到私有Harbor(或者其他的镜像仓库)
  • Jenkins再次控制Kubernetes进行最新的镜像部署
  • 流水线结束,删除Jenkins Slave

上述为比较常用的步骤,中间还可能涉及自动化测试等其他步骤,可自行根据业务场景添加或删除。上述流水线步骤一般写在Jenkinsfile中,Jenkins会自动读取该文件,同时Jenkinsfile和Dockerfile可一并和代码放置于GitLab中,可以和代码一样实现版本控制,单独配置也可以

一、安装jenkins(v2.392)

本例子中Jenkins 、 GitLab 、 Harbor 都不是安装在Kubernetes集群中,而是选择直接在单独的机器上部署

1、通过docker部署jenkins

创建Jenkins的数据目录,防止容器重启后数据丢失:

mkdir /data/jenkins_data

创建jenkins,8080端口为Jenkins Web界面的端口,50000是jnlp使用的端口,Jenkins Slave需要使用50000端口和Jenkins主节点通信,命令如下:

docker run -d --name jenkins -u root\
--restart=always -e JENKINS_PASSWORD=admin123 \
-v /etc/localtime:/etc/localtime \
-e JENKINS_USERNAME=admin -e JENKINS_HTTP_PORT_NUMBER=8080 \
-p 8080:8080 -p 50000:50000 -v /data/jenkins_data:/var/jenkins_home \
jenkins/jenkins

2、通过IP:8080端口打开jenkins,可以看到获取密码教程,输入并登录,如图:

登录后单击Manage Jenkins → Manage Plugins-Available plugins,搜索安装中文插件,如图:

3、其他CI/CD相关插件安装,先配置国内的插件源,点击插件管理-高级设置,如图:

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

本例子中需要插件如下:

 Git
 Git Parameter
 Git Pipeline for Blue Ocean
 GitLab
 Credentials
 Credentials Binding
 Blue Ocean
 Blue Ocean Pipeline Editor
 Blue Ocean Core JS
 Pipeline SCM API for Blue Ocean
 Dashboard for Blue Ocean
 Build With Parameters
 Dynamic Extended Choice Parameter Plug-In
 Dynamic Parameter Plug-in
 Extended Choice Parameter
 List Git Branches Parameter
 Pipeline
 Pipeline: Declarative
 Kubernetes
 Kubernetes CLI
 Kubernetes Credentials Provider
 Image Tag Parameter
 Active Choices
 Docker

至此,Jenkins和Jenkins插件就安装完成

二、安装Gitlab

GitLab在企业内经常用于代码的版本控制,是DevOps平台中尤为重要的一个工具

首先在国内镜像源中下载安装包:

https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/

1、执行命令安装gitlab

yum -y install gitlab-ce-15.9.0-ce.0.el7.x86_64.rpm

2、修改配置文件

vim /etc/gitlab/gitlab.rb

gitlab里面会自带prometheus和一些exporter,默认都是安装的,我们需要禁止安装prometheus和相关exporter,修改配置,取消注释,改为false,如图:

还有其余的exporter,都要改为false,修改完成后执行命令加载配置文件

 gitlab-ctl reconfigure

3、通过IP访问gitlab,账号root,密码位置/etc/gitlab/initial_root_password,如图:

4、创建一个测试项目,点击左侧的Groups,如图:

点击Create group,如图:

输入组名称后,勾选私有的,如图:

点击右侧的New Project,如图:

点击Create blank project,如图:

输入项目名称后,点击Create project,如图:

5、将jenkins服务器的key导入Gitlab中,首先生成秘钥:

ssh-keygen -t rsa -C "abcdefg@qq.com"

将公钥内容放在Gitlab中,查找公钥位置

cat /root/.ssh/id_rsa.pub

首先点击Gitlab用户菜单中的Edit profile,如图:

点击左侧的SSH keys,如图:

将公钥内容粘贴进入后,点击Add key,如图:

6、测试拉取与推送代码,首先将项目拉取到本地:

git clone git@10.9.2.247:test/test-project.git

进入项目目录,创建一个新文件hello.txt,如图:

提交文件到Gitlab,执行命令如下:

git add .                        #添加到暂存区
git commit -am "first commit"    #提交修改
git push origin main             #推送到仓库

查看gitlab代码,发现已经推送成功,main为默认分支,如图:

三、安装Harbor

参考文章Harbor离线部署

四、配置Jenkins凭证Credentials

使用Jenkins、GitLab、Harbor、Kubernetes打造DevOps平台实现CI/CD时,需要涉及很多证书、账号和密码、公钥和私钥的配置,这些凭证需要放在一个统一的地方管理,并且能在整个流水线中使用,而Jenkins提供的Credentials插件可以满足以上要求,并且在流水线中也不会泄露相关的加密信息。所以Harbor的账号和密码、GitLab的私钥、Kubernetes的证书均使用Jenkins的Credentials管理

1、配置kubernetes证书

只要将kubeconfig(/root/.kube/config)文件配置到jenkins的Credentials中,就可以通过jenkins的kubernetes插件管理集群

打开jenkins,点击系统管理–Manage Credentials,如图:

点击全局下的添加凭据,如图:

选择凭据种类,上传文件,并且自定义设置ID和描述信息,如图:

jenkins添加k8s凭据完成,通过Jenkins语法即可引用该凭证,如果修改了凭据,需要重启jenkins才可生效

2、配置Harbor账号密码

添加方式与第一步类似,只是在选择类型的时候,选择Username with password,如图:

配置harbor认证完成,在流水线中即可通过类似environment的指令引用该凭证

3、配置Gitlab Key

在搭建GitLab时,将Jenkins服务器的公钥放在GitLab中(ssh keys),之后Jenkins就可以通过自己的私钥登录GitLab,然后下载和上传代码,在流水线的执行过程中,往往第一个过程就是下载代码,所以Jenkins的流水线需要有下载代码的权限。此时可以将Jenkins所在服务器(如果是docker部署的就要去容器里面获取私钥)的私钥(/root/.ssh/id_rsa)保存至Jenkins凭证中,之后在流水线中引用即可

点击添加凭据(Add Credentials),类型选择如图:

Username可不写或自定义内容,粘贴私钥内容,如图:

注:配置完成后如何Jenkins项目页面还是提示连接gitlab异常,此时可以去Jenkins所在机器(docker部署的就去容器里)使用git clone测试拉取一次,一般测试成功后jenkins界面就可以连接了

五、配置jenkins Agent

通常情况下,Jenkins Slave会通过Jenkins Master节点的50000端口与之通信,所以需要开启Agent的50000端口

点击系统管理–全局安全配置,如图:

在代理–指定端口,输入50000,点击save,如图:

1·、Jenkins配置kubernetes多集群

Pipeline采用的Agent为在Kubernetes集群中创建的Pod,因此Jenkins要控制Kubernetes集群创建Pod充当Jenkins的Slave,之前添加了Kubernetes的证书至Jenkins凭证,接下来在Jenkins中配置Kubernetes直接引用该证书

打开jenkins,点击系统管理–节点管理–Configure Clouds,如图:

在Add a new cloud中,选择Kubernetes,如图:

根据需要自定义集群名字,如图:

在凭据位置,选择之前添加的连接k8s集群的凭据,并测试连接,如图:

注:添加多个集群可重复上述步骤即可,实际使用时,没有必要把整个Kubernetes集群的节点都充当创建Jenkins Slave Pod的节点,可以选择任意一个或多个节点作为创建Slave Pod的节点

六、实战

本例子中以Java项目eureka为例子,首先将代码推送到gitlab,此处不演示

1、首先在命名空间test-project中定义pod,如图:

  • java-deploy:deployment的名称
  • Java-eureka:pod中的容器名字,本利中镜像先使用nginx,构建后会覆盖,实际项目中根据需要修改

2、定义Jenkinsfile

Jenkinsfile定义流水线的步骤包括拉取代码、执行构建、生成镜像、更新Kubernetes资源等

本例子中将Jenkinsfile放置于项目源代码一并管理,也可以单独放置于一个Git仓库进行管理

在GitLab的源代码中添加Jenkinsfile,首先点击代码首页的”+”,点击New file,如图:

输入名字jenkinsfile,如图:

jenkinsfile内容如下:

pipeline {
    agent {
        kubernetes {
            cloud 'kubernetes-test'
            slaveConnectTimeout 1200
            workspaceVolume hostPathWorkspaceVolume(hostPath: "/data/jenkins/agent/workspace",readOnly:false)
            yaml '''
                apiVersion: v1
                kind: Pod
                metadata:
                  name: k8s-test-project
                spec:
                  restartPolicy: "Never"
                  nodeSelector:
                    build: "true"
                  securityContext:
                    runAsUser: 0
                  containers:
                  - name: "jnlp"
                    args: [\'$(JENKINS_SECRET)\',\'$(JENKINS_NAME)\']
                    image: 'jenkins/jnlp-agent-jdk11'
                    imagePullPolicy: IfNotPresent
                    volumeMounts:
                    - mountPath: "/etc/localtime"
                      name: "localtime"
                      readOnly: false
                    - mountPath: "/home/jenkins/agent"
                      name: jenkinsagent
                      readOnly: false
                    workingDir: "/home/jenkins/agent"
                  - name: "mvnbuildjar"           #build容器,用maven镜像
                    image: "registry.cn-beijing.aliyuncs.com/citools/maven:3.5.3"
                    imagePullPolicy: IfNotPresent
                    command:
                    - "cat"
                    tty: true
                    volumeMounts:
                    - mountPath: "/etc/localtime"
                      name: "localtime"
                    - mountPath: "/root/.m2/"
                      name: "cachedir"
                      readOnly: false
                    - mountPath: "/home/jenkins/agent"  #将jenkins-agent工作目录挂载进来,即可执行mvn构建
                      name: jenkinsagent
                      readOnly: false
                    workingDir: "/home/jenkins/agent"
                    env:
                    - name: "LANGUAGE"
                      value: "en_US:en"
                    - name: "LC_ALL"
                      value: "en_US.UTF-8"
                    - name: "LANG"
                      value: "en_US.UTF-8"
                  - name: "docker-kubectl"           #根据Dockerfile构建镜像推送到仓库
                    image: "docker_20.10.8_kubectl_1.23.5:latest"  #此docker镜像版本一定要与宿主机一致,否则挂载docker.sock可能失败
                    imagePullPolicy: "IfNotPresent"
                    command:
                    - "cat"
                    env:
                    - name: "LANGUAGE"
                      value: "en_US:en"
                    - name: "LC_ALL"
                      value: "en_US.UTF-8"
                    - name: "LANG"
                      value: "en_US.UTF-8"
                    tty: true
                    volumeMounts:
                    - mountPath: "/etc/localtime"
                      name: "localtime"
                      readOnly: false
                    - mountPath: "/var/run/docker.sock" #将宿主机的docker.sock挂载到容器中,容器内部就可以直接登录harbor仓库并推送镜像
                      name: "dockersock"
                      readOnly: false
                    - mountPath: "/home/jenkins/agent"    #同样将jenkins-agent的workspace挂载进来即可执行docker相关命令
                      name: jenkinsagent
                      readOnly: false
                    workingDir: "/home/jenkins/agent"
                  volumes:                #定义存储卷,给里面的容器挂载
                  - hostPath:
                      path: "/var/run/docker.sock"
                    name: "dockersock"
                  - hostPath:
                      path: "/usr/share/zoneinfo/Asia/Shanghai"
                    name: "localtime"
                  - name: "cachedir"
                    hostPath:
                      path: "/data/m2"
                  - name: jenkinsagent
                    hostPath:
                      path: "/data/jenkins/agent"
              '''
        }
    }
    environment {
      COMMIT_ID = ''
      HARBOR_ADDRESS = 'harbor.centos.com'   //harbor地址
      NAMESPACE = "test-project"                     //此应用对应的k8s中命名空间
      TAG = ""
      REGISTRY_DIR = "java-project"                  //Harbor的项目目录
      IMAGE_NAME = "java-eureka"                     //项目目录下的镜像名称
      REPO = "git@10.9.2.247:test/test-project.git"  //git项目地址
      GIT_AUTH = 'gitlab-key'
      DEPLOY_NAME = 'java-deploy'
    }
    parameters {
      gitParameter(                //gitParameter字段会在jenkins页面生成一个选择分支的选项
        branch: '',
        branchFilter: 'origin/(.*)',
        defaultValue: '',
        description: 'Branch for build and deploy',
        name: 'BRANCH',
        quickFilterEnabled: false,
        selectedValue: 'NONE',
        sortMode: 'NONE',
        tagFilter: '*',
        type: 'PT_BRANCH'
      )
    }
    //拉取代码阶段
    stages {
        //手动触发
        stage('pull code by jenkins') {
          when {
            expression {
              env.gitlabBranch == null
            }
          }
          steps {
            sh """
              echo '===========开始手动拉取代码==========='
              
            """
            git(
              url: "${env.REPO}",
              changelog: true,
              poll: true,
              branch: "${BRANCH}",
              credentialsId: "${env.GIT_AUTH}"
            )
            script {
              COMMIT_ID = sh(
                returnStdout: true,
                script: "git log -n 1 --pretty=format:'%h'"
              ).trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
            }
          }
        }
        //自动触发
        stage('Pulling code by trigger') {
          when {
            expression {
              env.gitlabBranch != null
            }
          }
          steps {
            sh """
              echo '===========开始自动拉取代码==========='
              echo "${BRANCH}"
            """
            git(
              url: "${env.REPO}",
              changelog: true,
              poll: true,
              branch: env.gitlabBranch,
              credentialsId: "${env.GIT_AUTH}"
            )
            script {
              COMMIT_ID = sh(
                returnStdout: true,
                script: "git log -n 1 --pretty=format:'%h'"  //获取最近一次提交
              ).trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
            }
          }  
        }
    //构建jar包,本实验为Java,因此需要使用mvn命令
    stage('build jar by mvn'){
      steps {  
        //使用pod模板里的build容器进行构建
          container('mvnbuildjar') {
            dir("/home/jenkins/agent/workspace/test-project"){
               sh """
                echo "COPY设置文件,修改为国内阿里云maven仓库"
                cp -a settings.xml /usr/share/maven/conf/
                echo "开始执行mvn命令"
                mvn clean install -DskipTests
                echo "mvn命令执行结束"
               """
            }
          }
      }
    }
    stage('Docker build image') {
      environment {
        HARBOR_USER = credentials('Harbor-ID')
        my_kubeconfig = credentials('k8s-config')
      }
      steps {
        container(name: 'docker-kubectl'){
          dir("/home/jenkins/agent/workspace/test-project"){
            sh """
              echo "===========开始构建镜像============"
              docker build -t ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} .
              echo "===========登录Harbor镜像仓库======"
              docker login -u ${HARBOR_USER_USR} -p ${HARBOR_USER_PSW} https://${HARBOR_ADDRESS}
              echo "===========推送镜像到Harbor仓库====="
              docker push ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG}
              echo "使用kubectl set image执行发布命令"
              /usr/local/bin/kubectl --kubeconfig ${my_kubeconfig} set image deploy ${DEPLOY_NAME} \
              ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n ${NAMESPACE}
            """
          }
        }
      }
    }
  }     
}

jenkinsfile中的镜像可从阿里云仓库拉取:

registry.cn-hangzhou.aliyuncs.com/k8s_ry/docker_20.10.8_kubectl_1.23.5 #此镜像构建可参考harbor部署章节
registry.cn-hangzhou.aliyuncs.com/k8s_ry/maven:3.5.3
registry.cn-hangzhou.aliyuncs.com/k8s_ry/jnlp-agent-jdk11

3、定义Dockerfile,点击gitlab中Newfile,输入名字Dockerfile,内容如下:

#基础镜像
FROM registry.cn-beijing.aliyuncs.com/dotbalo/jre:8u211-data
#COPY jar包到镜像中
COPY target/spring-cloud-eureka-0.0.1-SNAPSHOT.jar ./
#启动Jar包
CMD  java -jar spring-cloud-eureka-0.0.1-SNAPSHOT.jar

除了源代码src和pom.xml外,完整的gitlab项目文件结构如下:

完整的构建流程为:Jenkins启动构建后,会根据jenkinsfile在kubenets中根据标签节点启动一个Pod , Pod中有三个容器,jnlp负责与jenkins的master通信,maven负责构建jar包,docker-kubectl负责打包为镜像推送到仓库并发布到k8s集群,maven和docker-kubectl容器都挂载了jnlp的工作目录/home/jenkins/agent目录,这样就可以在maven容器中构建jar,并且docker执行相关命令,最外层也挂载到了宿主机的/data/jenkins/agent,因此构建后的jar可以直接获取到

注意:k8s的每个节点都要能登录到harbor仓库才可以,否则可能出现拉取镜像失败

4、给运行jenkins-slave的节点打上标签,如下:

kubectl label node k8s-node01 build=true

5、创建Jenkins任务,选择流水线,如图:

6、在流水线位置,选择git,输入地址和凭据以及分支,点击保存,如图:

注意:这个脚本路径对应的jenkinsfile一定要与gitlab中定义的jenkinsfile名字一致否则构建失败

7、点击构建,由于jenkins参数是由jenkinsfile生成,因此第一次会构建失败,第一次失败后可以看到Build Now变为了击Build with Parameters,可选择gitlab上的分支进行构建,如图:

(1)、构建后,pod中的jnlp容器报错如下:

此报错路径为slave容器内部的路径,对应的挂载路径为外部的/data/jenkins/agent(jenkinsfile中定义),因此给外层目录授权即可

chmod -R 777 /data/jenkins/agent

(2)、jnlp容器再次报错如下:

Caused by: java.lang.UnsupportedClassVersionError: hudson/slaves/SlaveComputer$SlaveVersion has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

上面错误的原因是jenkins运行所用的jdk版本,高于了jnlp容器中jenkins-agent运行的jdk版本,因此调整版本,要么将jenkins运行版本降低,要么将jnlp容器中的jdk版本调高即可(本例子中jenkins版本为2.392对应的jdk为11),因此将jenkinsfile中的jnlp容器改为openjdk11即可

(3)如果是容器运行的jenkins(本地root运行的不会报错),在构建后会提示如图:

原因:容器安装的jenkins在执行流水线时用的是jenkins用户,此用户没有足够权限执行,可先关闭ssh验证,如下:

(4)、在maven构建环境如果报错如下:

Failed to execute goal org.apache.maven.plugins:maven-clean-plugin:3.1.0:clean (default-clean) 

可能原因:默认的maven仓库为官方仓库会连接超时,可改为阿里云仓库,可在gitlab中定义配置文件settings.xml,,然后跟代码一起拉取下来后,复制到maven容器的/usr/share/maven/conf目录下替换默认文件,settings.xml文件内容如下:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">
    <localRepository/>
    <interactiveMode/>
    <usePluginRegistry/>
    <offline/>
    <pluginGroups/>
    <servers/>
    <mirrors>
        <mirror>
            <id>aliyunmaven</id>
            <mirrorOf>central</mirrorOf>
            <name>阿里云公共仓库</name>
            <url>https://maven.aliyun.com/repository/central</url>
        </mirror>
        <mirror>
            <id>repo1</id>
            <mirrorOf>central</mirrorOf>
            <name>central repo</name>
            <url>http://repo1.maven.org/maven2/</url>
        </mirror>
        <mirror>
            <id>aliyunmaven</id>
            <mirrorOf>apache snapshots</mirrorOf>
            <name>阿里云阿帕奇仓库</name>
            <url>https://maven.aliyun.com/repository/apache-snapshots</url>
        </mirror>
    </mirrors>
    <proxies/>
    <activeProfiles/>
    <profiles>
        <profile>
            <repositories>
                <repository>
                    <id>aliyunmaven</id>
                    <name>aliyunmaven</name>
                    <url>https://maven.aliyun.com/repository/public</url>
                    <layout>default</layout>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
                <repository>
                    <id>MavenCentral</id>
                    <url>http://repo1.maven.org/maven2/</url>
                </repository>
                <repository>
                    <id>aliyunmavenApache</id>
                    <url>https://maven.aliyun.com/repository/apache-snapshots</url>
                </repository>
            </repositories>
        </profile>
    </profiles>
</settings>

再次构建即可成功

8、定义service,通过NodePort方式访问,如图:

9、通过集群中任意节点:30212端口访问,如图:

自动触发构建

上述构建都是手动选择分支进行构建,也可按需配置自动构建,即提交代码后自动触发jenkins构建任务

还是以上述java项目为例子

1、打开jenkins,找到项目,点击配置,勾选Build when a change is pushed,如图:

注意:Build when a change后面的url就是webhook地址,要写在gitlab中的

点击下方的高级,如果要允许所有分支都使用自动构建,就选择Allow all branches to trigger this job,如图:

如果不想任何分支都可以触发该流水线,可以选择Filter进行条件匹配,然后点击Generate生成Secret token,然后点击保存,如图:

2、接下来配置gitlab,点击Menu-Admin,如图:

点击Settings-Network,如图:

选择允许访问外部的请求,点击储存改变,如图:

扎到java项目,点击Settings-Webhooks,如图:

输入webhook地址以及上面生成的token,点击储存改变,如图:

在Push events位置,根据需要选择全部分支还是指定分支来触发自动构建,如图:

下方会出现新添加的webhook,点击Test–Push events,如图:

点击后在jenkins中可以看到已经触发了构建,如图:

当通过git命令或者直接在gitlab中修改代码并提交后即可自动触发jenkins构建,也可以点击Blue Ocean查看流水线过程,如图:

标签