使用Kubernetes和Jenkins建立CI/CD pipeline

CI/CD试图解决什么?

CI/CD是一个常常与其余术语(例如DevOps,Agile,Scrum和看板,自动化等)一块儿听到的术语。有时,它只是工做流的一部分而没有真正了解它是什么或为何采用它。对于年轻的DevOps工程师来讲,将CI/CD视为理所固然的事情很常见,他们可能尚未看到软件发布周期的“传统”方式,所以没法欣赏CI/CD。html

CI/CD表明持续集成/持续交付或部署。未实现CI/CD的团队在建立新软件产品时必须通过如下阶段:node

  • 产品经理(表明客户的利益)提供了产品应具有的必要功能以及产品应遵循的行为。该文档必须尽量详尽和具体。
  • 具备业务分析师的开发人员经过编写代码,运行单元测试并将结果提交到版本控制系统(例如git)来开始处理应用程序。
  • 开发阶段完成后,该项目将移至质量检查。针对产品运行了一些测试,例如用户接受测试,集成测试,性能测试。在此期间,在QA阶段完成以前,不得对代码库进行任何更改。若是有任何错误,则会将其传给开发人员进行修复,而后将产品交给质量检查人员。
  • 完成质量检查后,操做团队会将代码部署到生产中。

上述工做流程有许多缺点:python

  • 首先,从产品经理提出请求到产品准备生产为止,要花费很长时间。
  • 对于开发人员来讲,解决一个月或更长时间以来已经编写的代码中的错误很困难。请记住,仅在开发阶段结束且质量检查阶段开始后才发现错误。
  • 当hotfix(例如须要修复程序的严重错误)时,因为须要尽快部署,所以QA阶段一般会缩短。
  • 因为不一样团队之间几乎没有协做,所以人们会在出现错误时开始指责并互相指责。每一个人开始只关心本身的项目部分,而忽略了共同的目标。

CI/CD经过引入自动化解决了上述问题。每次将代码更改推送到版本控制系统后,都将进行测试,而后进一步部署到生产/UAT环境中,以进行进一步测试,而后再将其部署到生产环境中供用户使用。自动化可确保整个过程快速,可靠,可重复,而且不易出错。linux

那么,什么是CI/CD?

咱们老是更喜欢较少的理论,更多的实践。话虽如此,如下是对一旦执行代码更改即应执行的自动化步骤的简要说明:git

  • 持续集成(CI):第一步不包括质量检查。换句话说,它不关注代码是否提供了客户端请求的功能。相反,它能够确保代码的质量。经过单元测试,集成测试,能够将任何代码质量问题迅速通知开发人员。咱们能够经过代码覆盖率和静态分析来进一步扩展测试,从而进一步保证质量。
  • 用户验收测试:这是CD流程的第一部分。在此阶段,将对代码执行自动测试,以确保其知足客户的指望。例如,一个Web应用程序能够正常运行而不会引起任何错误,可是客户但愿访问者在导航到主页以前,先找到登录页面。当前代码将访问者直接带到主页,这与客户的需求有所不一样。 UAT测试指出了此类问题。在非CD环境中,这是人工QA测试人员的工做。
  • 部署:这是CD流程的第二部分。它涉及对将托管应用程序的服务器/Pod/容器进行更改,以使其反映更新的版本。这应该以自动化方式完成,最好经过诸如Ansible,Chef或Puppet之类的配置管理工具来完成。

那什么是一个Pipeline?

Pipeline是一个很是简单的概念的幻想。当您须要以必定顺序执行多个脚本以实现共同目标时,这些脚本统称为“Pipeline”。例如,在Jenkins中,Pipeline可能包含一个或多个阶段,必须所有完成才能使构建成功。使用阶段有助于可视化整个过程,了解每一个阶段须要花费多长时间,并肯定构建在何处失败。github

为Golang应用程序建立Pipeline

在此实验中,咱们正在构建连续交付(CD)Pipeline。咱们正在使用一个用Go编写的很是简单的应用程序。为了简单起见,咱们将仅对代码运行一种类型的测试。该实验的前提条件以下:golang

  • 正在运行的Jenkins实例。这多是云实例,虚拟机,裸机或Docker容器。它必须能够从Internet公开访问,以便存储库能够经过Webhook链接到Jenkins。
  • 镜像注册表:您可使用Docker Registry,基于云的产品(如ECR或GCR),甚至可使用自定义注册表。
  • GitHub上的账户。尽管在此示例中咱们使用GitHub,可是该过程能够与其余存储库(如Bitbucket)同样进行较小的更改。

Pipeline能够描述以下:docker

z01.jpg

Step 01: 应用文件

咱们的示例应用程序将对任何GET请求作出“ Hello World”响应。建立一个名为main.go的新文件,并添加如下行:json

package main

import (
   "log"
   "net/http"
)

type Server struct{}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   w.WriteHeader(http.StatusOK)
   w.Header().Set("Content-Type", "application/json")
   w.Write([]byte(`{"message": "hello world"}`))
}

func main() {
   s := &Server{}
   http.Handle("/", s)
   log.Fatal(http.ListenAndServe(":8080", nil))
}

因为咱们正在构建CD pipeline,所以咱们应该进行一些测试。咱们的代码很是简单,只须要一个测试用例便可。确保在点击根URL时收到正确的字符串。在同一目录中建立一个名为main_test.go的新文件,并添加如下行:api

package main

import (
   "log"
   "net/http"
)

type Server struct{}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   w.WriteHeader(http.StatusOK)
   w.Header().Set("Content-Type", "application/json")
   w.Write([]byte(`{"message": "hello world"}`))
}

func main() {
   s := &Server{}
   http.Handle("/", s)
   log.Fatal(http.ListenAndServe(":8080", nil))
}

咱们还有其余一些文件能够帮助咱们部署应用程序,这些文件名为:

Dockerfile:

FROM golang:alpine AS build-env
RUN mkdir /go/src/app && apk update && apk add git
ADD main.go /go/src/app/
WORKDIR /go/src/app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o app .

FROM scratch
WORKDIR /app
COPY --from=build-env /go/src/app/app .
ENTRYPOINT [ "./app" ]

Dockerfile是一个多阶段的程序,用于保持镜像大小尽量小。它从基于golang:alpine的构建镜像开始。生成的二进制文件将用于第二个镜像,这只是一个临时镜像。暂存镜像不包含依赖项或库,仅包含启动应用程序的二进制文件。

Service:
因为咱们使用Kubernetes做为托管此应用程序的平台,所以咱们至少须要一项服务和一个部署。咱们的service.yml文件以下所示:

apiVersion: v1
kind: Service
metadata:
  name: hello-svc
spec:
  selector:
    role: app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 32000
  type: NodePort

这个定义没有什么特别的。只是使用NodePort做为其类型的服务。它将在任何群集节点的IP地址上的端口32000上进行侦听。传入的链接将中继到端口8080上的Pod。对于内部通讯,服务将侦听端口80。

Deployment:

应用程序自己一旦进行了docker化,就能够经过Deployment资源部署到Kubernetes。 deploy.yml文件以下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deployment
  labels:
    role: app
spec:
  replicas: 2
  selector:
    matchLabels:
      role: app
  template:
    metadata:
      labels:
        role: app
    spec:
      containers:
      - name: app
        image: ""
        resources:
          requests:
            cpu: 10m

关于此部署定义,最有趣的是镜像部分。咱们不是使用硬编码镜像名称和标签,而是使用一个变量。稍后,咱们将看到如何将该定义用做Ansible的模板,并经过命令行参数替换镜像名称(以及部署的任何其余参数)。

Playbook:

在本实验中,咱们使用Ansible做为部署工具。还有许多其余方式来部署Kubernetes资源,包括Helm Charts,但我认为Ansible是一个更容易的选择。 Ansible使用playbook来组织其说明。咱们的playbook.yml文件以下所示:

- hosts: localhost
  tasks:
  - name: Deploy the service
    k8s:
      state: present
      definition: ""
      validate_certs: no
      namespace: default
  - name: Deploy the application
    k8s:
      state: present
      validate_certs: no
      namespace: default
      definition: ""

Ansible已经包含用于处理与Kubernetes API服务器通讯的k8s模块。所以,咱们不须要安装kubectl,可是咱们须要一个有效的kubeconfig文件来链接到集群(稍后会详细介绍)。让咱们快速讨论一下该手册的重要部分:

  • 该playbook用于将服务和资源部署到群集。
  • 因为咱们须要在执行时动态地将数据注入到定义文件中,所以咱们须要将定义文件用做模板,从那里能够从外部提供变量。
  • 为此,Ansible具备查找功能,您能够在其中传递有效的YAML文件做为模板。 Ansible支持多种将变量注入模板的方法。在这个特定的实验中,咱们使用命令行方法。

Step 02: 安装 Jenkins, Ansible, 和 Docker

让咱们安装Ansible并使用它自动部署Jenkins服务器和Docker运行时环境。咱们还须要安装openshift Python模块以启用与Kubernetes的Ansible链接。

Ansible的安装很是简单;只需安装Python并使用pip安装Ansible:

  • 登陆Jenkins
  • 安装Python 3,Ansible和openshift模块

    sudo apt update && sudo apt install -y python3 && sudo apt install -y python3-pip && sudo pip3 install ansible && sudo pip3 install openshift
  • 默认状况下,pip将二进制文件安装在用户主文件夹中的隐藏目录下。咱们须要将此目录添加到$PATH变量中,以便咱们能够轻松地调用如下命令:

    echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc
  • 安装部署Jenkins实例所需的Ansible:

    ansible-galaxy install geerlingguy.jenkins
  • 安装docker

    ansible-galaxy install geerlingguy.docker
  • 建立一个playbook.yaml文件并添加如下行:
- hosts: localhost
  become: yes
  vars:
    jenkins_hostname: 35.238.224.64
    docker_users:
    - jenkins
  roles:
    - role: geerlingguy.jenkins
    - role: geerlingguy.docker
  • 经过如下命令运行playbook:ansible-playbook playbook.yaml。请注意,咱们使用实例的公共IP地址做为Jenkins将使用的主机名。若是使用的是DNS,则可能须要用实例的DNS名称替换它。另外,请注意,在运行playbook以前,必须在防火墙上启用端口8080(若是有)。
  • 几分钟后,应安装Jenkins。您能够经过导航到计算机的IP地址(或DNS名称)并指定端口8080进行检查:

z02.jpg

  • 单击“登陆”连接,并提供“ admin”做为用户名和“ admin”做为密码。请注意,这些是咱们使用的Ansible角色设置的默认凭据。在生产环境中使用Jenkins时,您能够(而且应该)更改这些默认值。这能够经过设置角色变量来完成。您能够参考角色官方页面。
  • 您须要作的最后一件事是安装如下将在咱们的实验中使用的插件:

    • git
    • pipeline
    • CloudBees Docker Build and Publish
    • GitHub

Step 03: 配置Jenkins用户链接到集群

如前所述,本实验假设您已经有一个Kubernetes集群启动并正在运行。为了使Jenkins链接到该集群,咱们须要添加必要的kubeconfig文件。在此特定实验中,咱们使用的是托管在Google Cloud上的Kubernetes集群,所以咱们使用的是gcloud命令。您的具体状况可能有所不一样。可是在全部状况下,咱们都必须按照如下步骤将kubeconfig文件复制到Jenkins的用户目录中:

$ sudo cp ~/.kube/config ~jenkins/.kube/
$ sudo chown -R jenkins: ~jenkins/.kube/

请注意,您将在此处使用的账户必须具备建立和管理“部署和服务”的必要权限。

Step 04: 建立Jenkins Pipeline 做业

z03.jpg

建立一个新的Jenkins做业,而后选择Pipeline类型。做业设置应以下所示:

z04.jpg

z05.jpg

咱们更改的设置是:

  • 咱们使用Poll SCM做为构建触发器;设置此选项将指示Jenkins按期(按 *指示的每一分钟)检查Git存储库。若是自上次轮询以来仓库已更改,则将触发做业。
  • 在Pipeline自己中,咱们指定了存储库URL和凭据。分支是master。
  • 在本实验中,咱们将做业的全部代码添加到Jenkins文件中,该文件与代码存储在同一存储库中。本文稍后将讨论Jenkinsfile。

Step 05: 为GitHub和Docker Hub配置Jenkins凭据

转到 /credentials/store/system/domain/_/newCredentials 并将凭据添加到两个目标。确保为每个都提供有意义的ID和说明,由于稍后会引用它们:

z06.jpg

z07.jpg

Step 06: 建立JenkinsFile

Jenkinsfile指导Jenkins如何构建,测试,docker化,发布和交付咱们的应用程序。咱们的Jenkinsfile看起来像这样:

pipeline {
   agent any
   environment {
       registry = "magalixcorp/k8scicd"
       GOCACHE = "/tmp"
   }
   stages {
       stage('Build') {
           agent {
               docker {
                   image 'golang'
               }
           }
           steps {
               // Create our project directory.
               sh 'cd ${GOPATH}/src'
               sh 'mkdir -p ${GOPATH}/src/hello-world'
               // Copy all files in our Jenkins workspace to our project directory.               
               sh 'cp -r ${WORKSPACE}/* ${GOPATH}/src/hello-world'
               // Build the app.
               sh 'go build'              
           }    
       }
       stage('Test') {
           agent {
               docker {
                   image 'golang'
               }
           }
           steps {                
               // Create our project directory.
               sh 'cd ${GOPATH}/src'
               sh 'mkdir -p ${GOPATH}/src/hello-world'
               // Copy all files in our Jenkins workspace to our project directory.               
               sh 'cp -r ${WORKSPACE}/* ${GOPATH}/src/hello-world'
               // Remove cached test results.
               sh 'go clean -cache'
               // Run Unit Tests.
               sh 'go test ./... -v -short'           
           }
       }
       stage('Publish') {
           environment {
               registryCredential = 'dockerhub'
           }
           steps{
               script {
                   def appimage = docker.build registry + ":$BUILD_NUMBER"
                   docker.withRegistry( '', registryCredential ) {
                       appimage.push()
                       appimage.push('latest')
                   }
               }
           }
       }
       stage ('Deploy') {
           steps {
               script{
                   def image_id = registry + ":$BUILD_NUMBER"
                   sh "ansible-playbook  playbook.yml --extra-vars \"image_id=${image_id}\""
               }
           }
       }
   }
}

该文件比看起来容易。pipeline基本上包含四个阶段:

  • 构建是咱们构建Go二进制文件的地方,并确保在构建过程当中没有错误。
  • 测试是咱们应用简单的UAT测试以确保应用程序按预期工做的地方。
  • 发布,构建Docker镜像并将其推送到注册表。以后,任何环境均可以使用它。
  • 部署,这是调用Ansible与Kubernetes联系并应用定义文件的最后一步。

如今,让咱们讨论这个Jenkinsfile的重要部分:

  • 前两个阶段大体类似。他们俩都使用golang Docker镜像来构建/测试应用程序。让阶段在已包含全部必要构建和测试工具的Docker容器中运行始终是一个好习惯。另外一种选择是在主服务器或从服务器之一上安装这些工具。当您须要针对不一样的工具版本进行测试时,就会出现问题。例如,也许咱们想使用Go 1.9来构建和测试代码,由于咱们的应用程序还没有准备好使用最新的Golang版本。镜像中包含全部内容,所以更改版本甚至镜像类型就像更改字符串同样简单。
  • Publish阶段(从第42行开始)首先指定一个环境变量,该变量将在之后的步骤中使用。该变量指向咱们在先前步骤中添加到Jenkins的Docker Hub凭据的ID。
  • 第48行:咱们使用docker插件构建镜像。默认状况下,它在咱们的注册表中使用Dockerfile,并将内部版本号添加为镜像标签。稍后,当您须要肯定哪一个Jenkins构建是当前运行的容器的来源时,这将很是重要。
  • 第49-51行:成功构建镜像后,咱们使用内部版本号将其推送到Docker Hub。此外,咱们在镜像上添加了“最新”标签(第二个标签),以便咱们容许用户在须要的状况下无需指定内部版本号便可拉取镜像。
  • 第56-60行:在部署阶段,咱们将部署和服务定义文件应用到集群。咱们使用前面讨论的剧本调用Ansible。请注意,咱们将image_id做为命令行变量传递。该值将自动替换部署文件中的镜像名称。

测试咱们的CD Pipeline

本文的最后一部分是咱们实际对咱们的工做进行测试的地方。咱们将代码提交到GitHub,并确保咱们的代码在pipeline中移动到达集群:

  • 添加咱们的文件:git add *
  • 提交更改:git commit -m“初始提交”
  • 推送到GitHub:git push
  • 在Jenkins上,咱们能够等待做业自动触发,也能够单击“当即构建”
  • 若是做业成功,咱们可使用如下命令检查已部署的应用程序:

获取节点的IP地址:

kubectl get nodes -o wide
NAME                                          STATUS   ROLES    AGE   VERSION          INTERNAL-IP   EXTERNAL-IP     OS-IMAGE                             KERNEL-VERSION   CONTAINER-RUNTIME
gke-security-lab-default-pool-46f98c95-qsdj   Ready       7d    v1.13.11-gke.9   10.128.0.59   35.193.211.74   Container-Optimized OS from Google   4.14.145+        docker://18.9.7

如今,让咱们向应用程序发起HTTP请求:

$ curl 35.193.211.74:32000
{"message": "hello world"}

好的,咱们能够看到咱们的应用程序运行正常。让咱们故意在代码中犯一个错误,并确保管道不会将错误的代码发送到目标环境:

将应显示的消息更改成“ Hello World!”,请注意,咱们将每一个单词的首字母大写,并在末尾添加了感叹号。因为咱们的客户可能不但愿该消息以这种方式显示,所以管道应在测试阶段中止。

首先,让咱们进行更改。如今,main.go文件应以下所示:

package main

import (
   "log"
   "net/http"
)

type Server struct{}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   w.WriteHeader(http.StatusOK)
   w.Header().Set("Content-Type", "application/json")
   w.Write([]byte(`{"message": "Hello World!"}`))
}

func main() {
   s := &Server{}
   http.Handle("/", s)
   log.Fatal(http.ListenAndServe(":8080", nil))
}

接下来,提交并推送咱们的代码:

$ git add main.go
$ git commit -m "Changes the greeting message"                                                                                                       
[master 24a310e] Changes the greeting message
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 319 bytes | 319.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/MagalixCorp/k8scicd.git
   7954e03..24a310e  master -> master

回到Jenkins,咱们能够看到上一次构建失败了:

z08.jpg

经过单击失败的做业,咱们能够看到其失败的缘由:

z09.jpg

咱们的错误代码将永远不会进入目标环境。

相关文章
相关标签/搜索