CI/CD是一個常常與其餘術語(例如DevOps,Agile,Scrum和看板,自動化等)一塊兒聽到的術語。有時,它只是工做流的一部分而沒有真正瞭解它是什麼或爲何採用它。對於年輕的DevOps工程師來講,將CI/CD視爲理所固然的事情很常見,他們可能尚未看到軟件發佈週期的「傳統」方式,所以沒法欣賞CI/CD。html
CI/CD表明持續集成/持續交付或部署。未實現CI/CD的團隊在建立新軟件產品時必須通過如下階段:node
上述工做流程有許多缺點:python
CI/CD經過引入自動化解決了上述問題。每次將代碼更改推送到版本控制系統後,都將進行測試,而後進一步部署到生產/UAT環境中,以進行進一步測試,而後再將其部署到生產環境中供用戶使用。自動化可確保整個過程快速,可靠,可重複,而且不易出錯。linux
咱們老是更喜歡較少的理論,更多的實踐。話雖如此,如下是對一旦執行代碼更改即應執行的自動化步驟的簡要說明:git
Pipeline是一個很是簡單的概念的幻想。當您須要以必定順序執行多個腳本以實現共同目標時,這些腳本統稱爲「Pipeline」。例如,在Jenkins中,Pipeline可能包含一個或多個階段,必須所有完成才能使構建成功。使用階段有助於可視化整個過程,瞭解每一個階段須要花費多長時間,並肯定構建在何處失敗。github
在此實驗中,咱們正在構建連續交付(CD)Pipeline。咱們正在使用一個用Go編寫的很是簡單的應用程序。爲了簡單起見,咱們將僅對代碼運行一種類型的測試。該實驗的前提條件以下:golang
Pipeline能夠描述以下:docker
咱們的示例應用程序將對任何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文件來鏈接到集羣(稍後會詳細介紹)。讓咱們快速討論一下該手冊的重要部分:
讓咱們安裝Ansible並使用它自動部署Jenkins服務器和Docker運行時環境。咱們還須要安裝openshift Python模塊以啓用與Kubernetes的Ansible鏈接。
Ansible的安裝很是簡單;只需安裝Python並使用pip安裝Ansible:
安裝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
- hosts: localhost become: yes vars: jenkins_hostname: 35.238.224.64 docker_users: - jenkins roles: - role: geerlingguy.jenkins - role: geerlingguy.docker
您須要作的最後一件事是安裝如下將在咱們的實驗中使用的插件:
如前所述,本實驗假設您已經有一個Kubernetes集羣啓動並正在運行。爲了使Jenkins鏈接到該集羣,咱們須要添加必要的kubeconfig文件。在此特定實驗中,咱們使用的是託管在Google Cloud上的Kubernetes集羣,所以咱們使用的是gcloud命令。您的具體狀況可能有所不一樣。可是在全部狀況下,咱們都必須按照如下步驟將kubeconfig文件複製到Jenkins的用戶目錄中:
$ sudo cp ~/.kube/config ~jenkins/.kube/ $ sudo chown -R jenkins: ~jenkins/.kube/
請注意,您將在此處使用的賬戶必須具備建立和管理「部署和服務」的必要權限。
建立一個新的Jenkins做業,而後選擇Pipeline類型。做業設置應以下所示:
咱們更改的設置是:
轉到 /credentials/store/system/domain/_/newCredentials 並將憑據添加到兩個目標。確保爲每個都提供有意義的ID和說明,由於稍後會引用它們:
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基本上包含四個階段:
如今,讓咱們討論這個Jenkinsfile的重要部分:
本文的最後一部分是咱們實際對咱們的工做進行測試的地方。咱們將代碼提交到GitHub,並確保咱們的代碼在pipeline中移動到達集羣:
獲取節點的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,咱們能夠看到上一次構建失敗了:
經過單擊失敗的做業,咱們能夠看到其失敗的緣由:
咱們的錯誤代碼將永遠不會進入目標環境。