kubernetes(34):持续集成(3)-JenkinsPipeline部署一个k8s应用
JenkinsPipeline部署一个Kubernetes 应用
https://www.qikqiak.com/k8s-book/docs/37.Jenkins%20Pipeline.html
1 Jenkins Pipeline 介绍
https://www.cnblogs.com/wangxu01/articles/11156603.html
Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。
Jenkins Pipeline 有几个核心概念:
- Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行环境,比如我们之前动态运行的 Jenkins Slave 就是一个 Node 节点
- Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念,可以跨多个 Node
- Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh 'make',就相当于我们平时 shell 终端中执行 make 命令一样。s
那么我们如何创建 Jenkins Pipline 呢?
- Pipeline 脚本是由 Groovy 语言实现的,但是我们没必要单独去学习 Groovy,当然你会的话最好
- Pipeline 支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法
- Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中
- 一般我们都推荐在 Jenkins 中直接从源代码控制(SCMD)中直接载入 Jenkinsfile Pipeline 这种方法
2 创建一个简单的 Pipeline
直接在 Jenkins 的 Web UI 界面中输入脚本运行
- 新建 Job:在 Web UI 中点击 New Item -> 输入名称:pipeline-demo -> 选择下面的 Pipeline -> 点击 OK
- 配置:在最下方的 Pipeline 区域输入如下 Script 脚本,然后点击保存。
node { stage('Clone') { echo "1.Clone Stage: 下代码" } stage('Test') { echo "2.Test Stage:测试" } stage('Build') { echo "3.Build Stage:构建" } stage('Deploy') { echo "4. Deploy Stage:发布" } }
- 构建:点击左侧区域的 Build Now,可以看到 Job 开始构建了
隔一会儿,构建完成,可以点击左侧区域的 Console Output,我们就可以看到如下输出信息:
3 在Slave中构建任务
编辑上面创建的 Pipeline 脚本,给 node 添加一个 label 属性,如下:
node('wx-jnlp') { stage('Clone') { echo "1.Clone Stage: 下代码" } stage('Test') { echo "2.Test Stage:测试" } stage('Build') { echo "3.Build Stage:构建" } stage('Deploy') { echo "4. Deploy Stage:发布" } }
给 node 添加了一个 wx-jnlp 这样的一个label,然后我们保存,构建之前查看下 kubernetes 集群中的 Pod:
[root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h42m [root@k8s-master ~]# #然后重新触发立刻构建: [root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h43m jnlp-3tcdd 1/1 Running 0 4s [root@k8s-master ~]# [root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h43m [root@k8s-master ~]#
#多了一个Pod 正在运行,隔一会儿这个 Pod 就不再了:
Job 构建完成了,同样回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:
4 部署Kubernetes 应用
部署应用的流程
- 编写代码
- 测试
- 编写 Dockerfile
- 构建打包 Docker 镜像
- 推送 Docker 镜像到仓库
- 编写 Kubernetes YAML 文件
- 更改 YAML 文件中 Docker 镜像 TAG
- 利用 kubectl 工具部署应用
这里我们来将一个简单 golang 程序,部署到 kubernetes 环境中,代码链接:https://github.com/cnych/jenkins-demo。如果按照之前的示例,我们是不是应该像这样来编写 Pipeline 脚本:
- 第一步,clone 代码,这个没得说吧
- 第二步,进行测试,如果测试通过了才继续下面的任务
- 第三步,由于 Dockerfile 基本上都是放入源码中进行管理的,所以我们这里就是直接构建 Docker 镜像了
- 第四步,镜像打包完成,就应该推送到镜像仓库中吧
- 第五步,镜像推送完成,是不是需要更改 YAML 文件中的镜像 TAG 为这次镜像的 TAG
- 第六步,万事俱备,只差最后一步,使用 kubectl 命令行工具进行部署了
项目地址https://github.com/cnych/jenkins-demo.git 我fork了一下
第一步,Clone 代码
stage('Clone') { echo "1.Clone Stage" git url: "https://github.com/wangxu01/jenkins-demo.git" }
第二步,测试
由于我们这里比较简单,忽略该步骤即可
第三步,构建镜像
stage('Build') { echo "3.Build Docker Image Stage" sh "docker build -t wangxu01/jenkins-demo:${build_tag} ." }
如果我们使用镜像 tag,则每次都是 latest 的 tag,这对于以后的排查或者回滚之类的工作会带来很大麻烦,我们这里采用和git commit的记录为镜像的 tag,这里有一个好处就是镜像的 tag 可以和 git 提交记录对应起来,也方便日后对应查看。但是由于这个 tag 不只是我们这一个 stage 需要使用,下一个推送镜像是不是也需要,所以这里我们把这个 tag 编写成一个公共的参数,把它放在 Clone 这个 stage 中,这样一来我们前两个 stage 就变成了下面这个样子:
stage('Clone') { echo "1.Clone Stage" git url: "https://github.com/wangxu01/jenkins-demo.git" script { build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() } } stage('Build') { echo "3.Build Docker Image Stage" sh "docker build -t wangxu01/jenkins-demo:${build_tag} ." }
第四步,推送镜像
镜像构建完成了,现在我们就需要将此处构建的镜像推送到镜像仓库中去,当然如果你有私有镜像仓库也可以,我们这里还没有自己搭建私有的仓库,所以直接使用 docker hub 即可,以后更改成 Harbor 仓库。
docker hub 是公共的镜像仓库,任何人都可以获取上面的镜像,但是要往上推送镜像我们就需要用到一个帐号了,所以我们需要提前注册一个 docker hub 的帐号,记住用户名和密码,我们这里需要使用。正常来说我们在本地推送 docker 镜像的时候,是不是需要使用docker login命令,然后输入用户名和密码,认证通过后,就可以使用docker push命令来推送本地的镜像到 docker hub 上面去了,如果是这样的话,我们这里的 Pipeline 是不是就该这样写了:
stage('Push') { echo "4.Push Docker Image Stage" sh "docker login -u wangxu01 -p xxxxx" sh "docker push wangxu01/jenkins-demo:${build_tag}" }
如果我们只是在 Jenkins 的 Web UI 界面中来完成这个任务的话,我们这里的 Pipeline 是可以这样写的,但是我们是不是推荐使用 Jenkinsfile 的形式放入源码中进行版本管理,这样的话我们直接把 docker 仓库的用户名和密码暴露给别人这样很显然是非常非常不安全的,更何况我们这里使用的是 github 的公共代码仓库,所有人都可以直接看到我们的源码,所以我们应该用一种方式来隐藏用户名和密码这种私密信息,幸运的是 Jenkins 为我们提供了解决方法。
在首页点击 Credentials -> Stores scoped to Jenkins 下面的 Jenkins -> Global credentials (unrestricted) -> 左侧的 Add Credentials:添加一个 Username with password 类型的认证信息,如下:
输入 docker
hub 的用户名和密码,ID 部分我们输入dockerHub,注意,这个值非常重要,在后面 Pipeline 的脚本中我们需要使用到这个 ID 值。
有了上面的 docker hub 的用户名和密码的认证信息,现在我们可以在 Pipeline 中使用这里的用户名和密码了:
stage('Push') { echo "4.Push Docker Image Stage" withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) { sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}" sh "docker push wangxu01/jenkins-demo:${build_tag}" } }
注意我们这里在 stage 中使用了一个新的函数withCredentials,其中有一个 credentialsId 值就是我们刚刚创建的 ID 值,而对应的用户名变量就是 ID 值加上 User,密码变量就是 ID 值加上 Password,然后我们就可以在脚本中直接使用这里两个变量值来直接替换掉之前的登录 docker hub 的用户名和密码,现在是不是就很安全了,我只是传递进去了两个变量而已,别人并不知道我的真正用户名和密码,只有我们自己的 Jenkins 平台上添加的才知道。
第五步,更改 YAML
上面我们已经完成了镜像的打包、推送的工作,接下来我们是不是应该更新 Kubernetes 系统中应用的镜像版本了,当然为了方便维护,我们都是用 YAML 文件的形式来编写应用部署规则,比如我们这里的 YAML 文件:(k8s.yaml)
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: jenkins-demo spec: template: metadata: labels: app: jenkins-demo spec: containers: - image: wangxu01/jenkins-demo:<BUILD_TAG> imagePullPolicy: IfNotPresent name: jenkins-demo env: - name: branch value: <BRANCH_NAME>
我们使用一个 Deployment 资源对象来管理 Pod,该 Pod 使用的就是我们上面推送的镜像,唯一不同的地方是 Docker 镜像的 tag 不是我们平常见的具体的 tag,而是一个 的标识,实际上如果我们将这个标识替换成上面的 Docker 镜像的 tag,是不是就是最终我们本次构建需要使用到的镜像?怎么替换呢?其实也很简单,我们使用一个sed命令就可以实现了:
stage('YAML') { echo "5. Change YAML File Stage" sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml" sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml" }
第六步,部署
stage('Deploy') { echo "6. Deploy Stage" sh "kubectl apply -f k8s.yaml" }
人工确认
理论上来说我们上面的6个步骤其实已经完成了,但是一般在我们的实际项目实践过程中,可能还需要一些人工干预的步骤,这是为什么呢?比如我们提交了一次代码,测试也通过了,镜像也打包上传了,但是这个版本并不一定就是要立刻上线到生产环境的,对吧,我们可能需要将该版本先发布到测试环境、QA 环境、或者预览环境之类的,总之直接就发布到线上环境去还是挺少见的,所以我们需要增加人工确认的环节,一般都是在 CD 的环节才需要人工干预,比如我们这里的最后两步,我们就可以在前面加上确认,比如:
stage('YAML') { echo "5. Change YAML File Stage" def userInput = input( id: 'userInput', message: 'Choose a deploy environment', parameters: [ [ $class: 'ChoiceParameterDefinition', choices: "Dev\nQA\nProd", name: 'Env' ] ] ) echo "This is a deploy step to ${userInput.Env}" sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml" sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml" }
这里使用了 input 关键字,里面使用一个 Choice 的列表来让用户进行选择,然后在我们选择了部署环境后,我们当然也可以针对不同的环境再做一些操作,比如可以给不同环境的 YAML 文件部署到不同的 namespace 下面去,增加不同的标签等等操作:
stage('Deploy') { echo "6. Deploy Stage" if (userInput.Env == "Dev") { // deploy dev stuff } else if (userInput.Env == "QA"){ // deploy qa stuff } else { // deploy prod stuff } sh "kubectl apply -f k8s.yaml" }
最终的 Pipeline 脚本
node('wx-jnlp') { stage('下代码Clone') { echo "1.Clone Stage" echo "env.BRANCH_NAME" git url: "https://github.com/cnych/jenkins-demo.git" script { build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() env.BRANCH_NAME = sh(returnStdout: true, script: 'git branch').trim() } } stage('测试Test') { echo "2.Test Stage" } stage('构建Build') { echo "3.Build Docker Image Stage" sh "docker build -t wangxu01/jenkins-demo:${build_tag} ." } stage('推仓库Push') { echo "4.Push Docker Image Stage" withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) { sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}" sh "docker push wangxu01/jenkins-demo:${build_tag}" } } stage('部署Deploy') { echo "5. Deploy Stage" def userInput = input( id: 'userInput', message: '选择一个环境Choose a deploy environment', parameters: [ [ $class: 'ChoiceParameterDefinition', choices: "Dev\nQA\nProd", name: 'Env' ] ] ) echo "This is a deploy step to ${userInput}" // sh "sed -i 's/cnych/wangxu01/' Jenkinsfile" // sh "sed -i 's/cnych/wangxu01/' k8s.yaml" sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml" // sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml" if (userInput == "Dev") { // deploy dev stuff } else if (userInput == "QA"){ // deploy qa stuff } else { // deploy prod stuff } // sh "pwd" // sh "cat k8s.yaml" sh "kubectl apply -f k8s.yaml" } }
执行
现在我们在 Jenkins Web UI 中重新配置 jenkins-demo 这个任务,将上面的脚本粘贴到 Script 区域,重新保存,然后点击左侧的 Build Now,触发构建,然后过一会儿我们就可以看到 Stage View 界面出现了暂停的情况:
这就是我们上面 Deploy 阶段加入了人工确认的步骤,所以这个时候构建暂停了,需要我们人为的确认下,比如我们这里选择 QA,然后点击 Proceed,就可以继续往下走了,然后构建就成功了,我们在 Stage View 的 Deploy 这个阶段可以看到如下的一些日志信息:
打印出来了 QA,和我们刚刚的选择是一致的,现在我们去 Kubernetes 集群中观察下部署的应用:
这个BRANCH_NAME为空,不过不影响
这个是部署在default命名空间的
Kubernetes 集群中观察下部署的应用:
[root@k8s-master jenkins-demo]# kubectl get deployments | grep jenkins jenkins-demo 0/1 1 0 8m34s [root@k8s-master jenkins-demo]# kubectl get pod | grep jenkins jenkins-demo-5fc7fd744-t72sv 0/1 Completed 3 56s [root@k8s-master jenkins-demo]# kubectl logs jenkins-demo-5fc7fd744-t72sv Hello, Kubernetes!I'm from Jenkins CI! BRANCH_NAME: <BRANCH_NAME> [root@k8s-master jenkins-demo]#
当然hub也上传成功了
配置Jenkinsfile
在实际的工作实践中,我们更多的是将 Pipeline 脚本写入到 Jenkinsfile 文件中,然后和代码一起提交到代码仓库中进行版本管理。现在我们将上面的 Pipeline 脚本拷贝到一个 Jenkinsfile 中,将该文件放入上面的 git 仓库中,但是要注意的是,现在既然我们已经在 git 仓库中了,是不是就不需要 git clone 这一步骤了,所以我们需要将第一步 Clone 操作中的 git clone 这一步去掉,可以参考:https://github.com/cnych/jenkins-demo/
然后我们更改上面的 jenkins-demo 这个任务,点击 Configure -> 最下方的 Pipeline 区域 -> 将之前的 Pipeline Script 更改成 Pipeline Script from SCM,然后根据我们的实际情况填写上对应的仓库配置,要注意 Jenkinsfile 脚本路径:
这里我在github上fork了一个分支,配置文件都改成我自己的,label标签、hub地址、github地址等等
最后附一个执行日志
Started by user admin Obtained Jenkinsfile from git https://github.com/wangxu01/jenkins-demo.git Running in Durability level: MAX_SURVIVABILITY [Pipeline] Start of Pipeline [Pipeline] node Still waiting to schedule task ‘jnlp-ps5dp’ is offline Agent jnlp-ps5dp is provisioned from template Kubernetes Pod Template --- apiVersion: "v1" kind: "Pod" metadata: annotations: {} labels: jenkins: "slave" jenkins/wx-jnlp: "true" name: "jnlp-ps5dp" spec: containers: - env: - name: "JENKINS_SECRET" value: "********" - name: "JENKINS_AGENT_NAME" value: "jnlp-ps5dp" - name: "JENKINS_NAME" value: "jnlp-ps5dp" - name: "JENKINS_AGENT_WORKDIR" value: "/home/jenkins/agent" - name: "JENKINS_URL" value: "http://jenkins2.kube-ops.svc.cluster.local:8080/" image: "cnych/jenkins:jnlp6" imagePullPolicy: "IfNotPresent" name: "jnlp" resources: limits: {} requests: {} securityContext: privileged: false tty: true volumeMounts: - mountPath: "/var/run/docker.sock" name: "volume-0" readOnly: false - mountPath: "/root/.kube" name: "volume-1" readOnly: false - mountPath: "/home/jenkins/agent" name: "workspace-volume" readOnly: false workingDir: "/home/jenkins/agent" nodeSelector: {} restartPolicy: "Never" serviceAccount: "jenkins2" volumes: - hostPath: path: "/var/run/docker.sock" name: "volume-0" - hostPath: path: "/root/.kube" name: "volume-1" - emptyDir: medium: "" name: "workspace-volume" Running on jnlp-ps5dp in /home/jenkins/agent/workspace/pipeline-demo [Pipeline] { [Pipeline] stage [Pipeline] { (Prepare) [Pipeline] echo 1.Prepare Stage [Pipeline] checkout No credentials specified Cloning the remote Git repository Cloning repository https://github.com/wangxu01/jenkins-demo.git > git init /home/jenkins/agent/workspace/pipeline-demo # timeout=10 Fetching upstream changes from https://github.com/wangxu01/jenkins-demo.git > git --version # timeout=10 > git fetch --tags --progress -- https://github.com/wangxu01/jenkins-demo.git +refs/heads/*:refs/remotes/origin/* > git config remote.origin.url https://github.com/wangxu01/jenkins-demo.git # timeout=10 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10 > git config remote.origin.url https://github.com/wangxu01/jenkins-demo.git # timeout=10 Fetching upstream changes from https://github.com/wangxu01/jenkins-demo.git > git fetch --tags --progress -- https://github.com/wangxu01/jenkins-demo.git +refs/heads/*:refs/remotes/origin/* Checking out Revision 9851bdf7dd63b690cd0a80814a312855c286dcd9 (refs/remotes/origin/master) Commit message: "Update Jenkinsfile" First time build. Skipping changelog. [Pipeline] script [Pipeline] { [Pipeline] sh > git rev-parse refs/remotes/origin/master^{commit} # timeout=10 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10 > git config core.sparsecheckout # timeout=10 > git checkout -f 9851bdf7dd63b690cd0a80814a312855c286dcd9 + git rev-parse --short HEAD [Pipeline] } [Pipeline] // script [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Test) [Pipeline] echo 2.Test Stage [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Build) [Pipeline] echo 3.Build Docker Image Stage [Pipeline] sh + docker build -t wangxu01/jenkins-demo:null-9851bdf . Sending build context to Docker daemon 140.3kB Step 1/5 : FROM golang:1.8.0-alpine ---> bcb935bbf1da Step 2/5 : ADD . /go/src/app ---> e12fdc91931c Step 3/5 : WORKDIR /go/src/app ---> Running in 106c10ce573e Removing intermediate container 106c10ce573e ---> a7d41cd09420 Step 4/5 : RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/jenkins-app ---> Running in a025f9bd3c51 [91mruntime/internal/sys [0m[91mruntime/internal/atomic [0m[91mruntime [0m[91merrors [0m[91minternal/race [0m[91mmath [0m[91msync/atomic [0m[91municode/utf8 [0m[91msync [0m[91mio syscall [0m[91mstrconv [0m[91mreflect [0m[91mtime [0m[91mos [0m[91mfmt [0m[91mapp [0mRemoving intermediate container a025f9bd3c51 ---> f9ba2f994f79 Step 5/5 : CMD ["./jenkins-app"] ---> Running in 51d271e4be69 Removing intermediate container 51d271e4be69 ---> 8155d613d6a5 Successfully built 8155d613d6a5 Successfully tagged wangxu01/jenkins-demo:null-9851bdf [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Push) [Pipeline] echo 4.Push Docker Image Stage [Pipeline] withCredentials Masking supported pattern matches of $dockerHubUser or $dockerHubPassword [Pipeline] { [Pipeline] sh + docker login -u **** -p **** WARNING! Using --password via the CLI is insecure. Use --password-stdin. WARNING! Your password will be stored unencrypted in /home/jenkins/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded [Pipeline] sh + docker push ****/jenkins-demo:null-9851bdf The push refers to repository [docker.io/****/jenkins-demo] 244b926aac60: Preparing eab6a59b1b75: Preparing a26f54127e87: Preparing d9ec36cf6713: Preparing 5fb532fed28c: Preparing 2d494005645b: Preparing dc364fd32a38: Preparing 23b9c7b43573: Preparing dc364fd32a38: Waiting 23b9c7b43573: Waiting 2d494005645b: Waiting 5fb532fed28c: Layer already exists d9ec36cf6713: Layer already exists a26f54127e87: Layer already exists 2d494005645b: Layer already exists dc364fd32a38: Layer already exists 23b9c7b43573: Layer already exists eab6a59b1b75: Pushed 244b926aac60: Pushed null-9851bdf: digest: sha256:2df27d31591b42274c88e955034336bc7c89c693da11dfa9a2ba27ad355ab6b9 size: 1991 [Pipeline] } [Pipeline] // withCredentials [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Deploy) [Pipeline] echo 5. Deploy Stage [Pipeline] sh + sed -i s/<BUILD_TAG>/null-9851bdf/ k8s.yaml [Pipeline] sh + sed -i s/<BRANCH_NAME>/null/ k8s.yaml [Pipeline] sh + kubectl apply -f k8s.yaml --record deployment.extensions/jenkins-demo configured [Pipeline] } [Pipeline] // stage [Pipeline] } [Pipeline] // node [Pipeline] End of Pipeline Finished: SUCCESS