使用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

咱們的錯誤代碼將永遠不會進入目標環境。

相關文章
相關標籤/搜索