kubernetes(k8s) jenkins CI/CD(pipeline流水线)

Jenkins Pipeline 介绍

要实现在 Jenkins 中的构建工作,可以有多种方式,这里采用比较常用的 Pipeline 这种方式。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 命令一样。

那么如何创建 Jenkins Pipline 呢?

  • Pipeline 脚本是由 Groovy 语言实现的,但是没必要单独去学习 Groovy,当然你会的话最好
  • Pipeline 支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法
  • Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中
  • 一般我们都推荐在 Jenkins 中直接从源代码控制(SCMD)中直接载入 Jenkinsfile Pipeline 这种方法

创建一个简单的 Pipeline

创建一个简单的 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,可以看到如下输出信息:

console output

可以看到上面Pipeline 脚本中的4条输出语句都打印出来了,证明是符合预期的。

Pipeline 语法链接Pipeline Syntax中进行查看,这里有很多关于 Pipeline 语法的介绍,也可以自动帮我们生成一些脚本。

在 Slave 中构建任务

上面创建了一个简单的 Pipeline 任务,但是可以看到这个任务并没有在 Jenkins 的 Slave 中运行,那么如何让任务跑在 Slave 中呢?之前在添加 Slave Pod 的时候,需要用到这个 label,我们重新编辑上面创建的 Pipeline 脚本,给 node 添加一个 label 属性,如下:

node('hwzx-cmp') {
    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 添加了一个 hwzx-cmp 这样的一个label,然后保存,构建之前查看下 kubernetes 集群中的 Pod:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS              RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running             4          6d

然后重新触发立刻构建:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d
jnlp-0hrrz                 1/1       Running   0          23s

会发现多了一个名叫jnlp-0hrrz的 Pod 正在运行,隔一会儿这个 Pod 就不在了:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d

这也证明Job 构建完成了,同样回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:

pipeline demo2

是不是也证明当前的任务在跑在上面动态生成的这个 Pod 中,也符合预期。回到 Job 的主界面,也可以看到大家可能比较熟悉的 Stage View 界面:

pipeline demo3

部署 Kubernetes 应用

上面已经知道了如何在 Jenkins Slave 中构建任务了,那么如何来部署一个原生的 Kubernetes 应用呢? 要部署 Kubernetes 应用,就得对之前部署应用的流程要非常熟悉才行,之前的流程是怎样的:

  • 编写代码
  • 测试
  • 编写 Dockerfile
  • 构建打包 Docker 镜像
  • 推送 Docker 镜像到仓库
  • 编写 Kubernetes YAML 文件
  • 更改 YAML 文件中 Docker 镜像 TAG
  • 利用 kubectl 工具部署应用

之前在 Kubernetes 环境中部署一个原生应用的流程应该基本上是上面这些流程吧?现在需要把上面这些流程放入 Jenkins 中来自动帮我们完成(当然编码除外),从测试到更新 YAML 文件属于 CI 流程,后面部署属于 CD 的流程。如果按照上面的示例,现在要来编写一个 Pipeline 的脚本。

node('hwzx-cmp') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Docker Image Stage"
    }
    stage('Push') {
      echo "4.Push Docker Image Stage"
    }
    stage('YAML') {
      echo "5. Change YAML File Stage"
    }
    stage('Deploy') {
      echo "6. Deploy Stage"
    }
}

这里将一个简单 golang 程序部署到 kubernetes 环境中,代码链接:https://github.com/cnych/jenkins-demo。如果按照之前的示例,我们是不是应该像这样来编写 Pipeline 脚本:

  • 第一步,clone 代码,这个没得说吧
  • 第二步,进行测试,如果测试通过了才继续下面的任务
  • 第三步,由于 Dockerfile 基本上都是放入源码中进行管理的,所以我们这里就是直接构建 Docker 镜像了
  • 第四步,镜像打包完成,就应该推送到镜像仓库中吧
  • 第五步,镜像推送完成,是不是需要更改 YAML 文件中的镜像 TAG 为这次镜像的 TAG
  • 第六步,万事俱备,只差最后一步,使用 kubectl 命令行工具进行部署了

到这里整个 CI/CD 的流程是不是就都完成了。

接下来就来对每一步具体要做的事情进行详细描述就行了:

第一步,Clone 代码

stage('Clone') {
    echo "1.Clone Stage"
    git url: "https://github.com/cnych/jenkins-demo.git"
}

第二步,测试

由于我们这里比较简单,忽略该步骤即可

第三步,构建镜像

stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t qienda/jenkins-demo:${build_tag} ."
}

平时构建的时候是不是都是直接使用docker build命令进行构建就行了,那么这个地方呢?之前提供的 Slave Pod 的镜像里面是不是采用的 Docker In Docker 的方式,也就是说也可以直接在 Slave 中使用 docker build 命令,所以这里直接使用 sh 执行 docker 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/cnych/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 qienda/jenkins-demo:${build_tag} ."
}

第四步,推送镜像

镜像构建完成了,现在就需要将此处构建的镜像推送到镜像仓库中去,直接使用 docker hub 即可。

docker hub 是公共的镜像仓库,任何人都可以获取上面的镜像,但是要往上推送镜像就需要用到一个帐号了,所以需要提前注册一个 docker hub 的帐号,记住用户名和密码,。正常来说我们在本地推送 docker 镜像的时候,需要使用docker login命令,然后输入用户名和密码,认证通过后,就可以使用docker push命令来推送本地的镜像到 docker hub 上面去了,如果是这样的话,这里的 Pipeline 是不是就该这样写了:

stage('Push') {
    echo "4.Push Docker Image Stage"
    sh "docker login -u cnych -p xxxxx"
    sh "docker push cnych/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 类型的认证信息,如下:


2.jpg

输入 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 qienda/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: qienda/jenkins-demo:<BUILD_TAG>
        imagePullPolicy: IfNotPresent
        name: jenkins-demo
        env:
        - name: branch
          value: <BRANCH_NAME>

该 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"
}

上面的 sed 命令就是将 k8s.yaml 文件中的 标识给替换成变量 build_tag 的值。

第六步,部署

Kubernetes 应用的 YAML 文件已经更改完成了,之前手动部署的环境下,接使用 kubectl apply 命令就可以直接更新应用了,当然这里只是写入到了 Pipeline 里面,思路都是一样的:

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('hwzx-cmp') {
    stage('Clone') {
        echo "1.Clone Stage"
        git url: "https://github.com/cnych/jenkins-demo.git"
        script {
            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
        }
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
        echo "3.Build Docker Image Stage"
        sh "docker build -t qienda/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 qienda/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/<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 "kubectl apply -f k8s.yaml"
    }
}

现在 Jenkins Web UI 中重新配置 jenkins-demo 这个任务,将上面的脚本粘贴到 Script 区域,重新保存,然后点击左侧的 Build Now,触发构建,然后过一会儿就可以看到 Stage View 界面出现了暂停的情况:

pipeline demo5

这就是上面 Deploy 阶段加入了人工确认的步骤,所以这个时候构建暂停了,需要人为的确认下,比如这里选择 QA,然后点击 Proceed,就可以继续往下走了,然后构建就成功了,在 Stage View 的 Deploy 这个阶段可以看到如下的一些日志信息:

pipeline demo6

打印出来了 QA,和刚刚的选择是一致的,现在去 Kubernetes 集群中观察下部署的应用:

$ kubectl get deployment -n kube-ops
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
jenkins        1         1         1            1           7d
jenkins-demo   1         1         1            0           1m
$ kubectl get pods -n kube-ops
NAME                           READY     STATUS      RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv       1/1       Running     4          7d
jenkins-demo-f6f4f646b-2zdrq   0/1       Completed   4          1m
$ kubectl logs jenkins-demo-f6f4f646b-2zdrq -n kube-ops
Hello, Kubernetes!I'm from Jenkins CI!

可以看到应用已经正确的部署到了 Kubernetes 的集群环境中了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342