您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關怎樣使用 Kubernetes和Jenkins創建一個CI/CD流水線,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
CI/CD 同 DevOps、Agile、Scrum、Kanban、自動化以及其他術語一樣,是一個一起被經常提及的專用術語。有時候,它被當做工作流的一部分,但是并沒有搞清楚這是什么或者為什么它會被采用。對于年輕的 DevOps 工程師來說,使用 CI/CD 理所當然已經成為了常態,可能他們并沒有看到“傳統”的軟件發布流程而因此不欣賞 CI/CD。
CI/CD 表示持續集成/持續交付和/或部署。如果一個團隊不接入 CI/CD 流程就必須要在產生一個新的軟件產品時經歷如下的階段:
產品經理(代表了客戶利益)提供了產品需要有的功能以及產品需要遵從的行為。文檔必須要越詳實越好。
具有業務分析能力的開發人員開始對應用進行編碼,執行單元測試,然后將結果提交到版本控制系統(例如 git)。
一旦開發階段完成,項目移交到 QA。對產品進行多輪測試,比如用戶驗收測試,集成測試,性能測試。在此期間,直到 QA 階段完成之前都不會有任何代碼上的改動。如果有任何 bug 被發現,需要回退給開發人員做修改,然后再將產品移交給 QA。
一旦 QA 完成,操作團隊會將代碼部署到生產環境中。
上述工作流存在一些弊端:
首先,從產品經理提出需求到產品具備開發條件中間會消耗太多時間。
對開發人員來說,從寫了一個月甚至更長時間的代碼中去定位問題真的很困難。請記住,bug 只能是在開發階段完成 QA 階段開始后被發現。
當有一個緊急的代碼修復比如像一個嚴重的 bug 需要熱修復時,QA 階段可能會因為需要盡快部署而被縮短。
不同的團隊之間很少會有協作,當 bug 出現的時候,人們就開始互相甩鍋互相指責。每個人從一開始只是關心項目中自己的那部分工作而忽視了共同的目標。
CI/CD 通過引入自動化來解決上述的問題。代碼中的每次改動一旦推送至版本控制系統,進行測試,然后在部署到用戶使用的生產環境之前部署至預生產/UAT 環境進行進一步的測試。自動化確保了整體流程的快速,可信賴,可重復,以及不容易出錯。
關于這個主題已經有著作撰寫完畢。如何,為什么,以及什么時候在你的架構中使用。然而,我們總是傾向于輕理論重實踐。話雖如此,下文簡單介紹了一下一旦修改的代碼被提交后會執行哪些自動化步驟:
持續集成(CI):第一步不包括 QA。換句話說,它不關注代碼是否提供了用戶需要的功能。相反,它確保了代碼的質量。通過單元測試,集成測試,開發人員能很快的就會發現代碼質量中的缺陷。我們可以增加代碼覆蓋率的檢查以及靜態分析讓代碼質量保證做的更加完善。
用戶驗收測試:這是 CD 流程的第一部分。這個階段,會對代碼執行自動化測試從而確保代碼符合用戶的期望。比如說,一個 web 應用沒有任何報錯產生能正常運行,但是客戶想讓訪問者在導航到主頁之前先進入到登錄頁面。但是當前的代碼直接讓訪問者導航到了主頁面,這與客戶的需求不相符。這種問題會在 UAT 測試時被指出。而在非 CD 環境,就成了人工的 QA 測試人員的工作。
Deployment:這是 CD 流程的第二部分。它包括在托管應用的服務器/ pods /容器上面執行更改來應用更新的版本。這會在自動化的方法下完成,最好通過一個配置管理工具來做這些事情,比如 Ansible、Chef 或者 Puppet。
流水線是一個有著簡單的概念的花哨術語;當你有一些需要按照順序依次執行的腳本用來實現一個共同的目標時,這些組合起來可以稱為“流水線”。比如說,在 Jenkins 里,一個流水線包含了一個或多個一次構建需要成功必須全部執行的_stages_。使用 stages 能夠可視化整個流程,能夠看到每個階段使用了多長時間,然后能夠準確得出構建過程的哪個地方是失敗的。
在這個實驗中,我們構建一個持續交付(CD)的流水線。我們使用一個用 Go 語言編寫的簡單的小程序。為了簡單起見,我們只對代碼運行一種類型的測試。實驗的前期工作如下:
一個運行的 Jenkins 實例。它可以是一個云實例,一個虛擬機,一個裸機或者是一個 docker 容器。必須是從互聯網上可獲得的,這樣倉庫才可以通過 web-hooks 連上 Jenkins。
鏡像系統:你可以使用 Docker Registry,一款基于云的產品它提供了ECR,GCR, 甚至一個定制的系統。
一個 GitHub 賬號。盡管我們在這個例子中使用 GitHub,程序使用其他倉庫同樣可以,例如少量修改后的 Bitbucket。
流水線可以用下圖做一個說明:
我們的實驗程序會對任意的 GET 請求回復 ‘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)) }
當我們構建一個 CD 流水線時,我們應該進行一些測試。我們代碼是如此的簡單以至于它僅僅只需要一個測試用例;能夠確保我們在輸入根 URL 時得到正確的字符串。在同目錄下創建名為 main_test.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)) }
我們同樣有一些其他用來幫助我們部署應用程序的文件,稱為:
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" ]
Dcokerfile 是一個多階段的文件能讓鏡像保持的越小越好。它從基于 golang:alpine 構建鏡像開始。生成的二進制文件在第二個鏡像中使用,它僅僅是一個臨時的鏡像,這個鏡像沒有依賴或者庫文件,只有用來啟動應用的二進制文件。
Service
由于我們使用 Kubernetes 作為托管該應用程序的平臺,我們需要至少一個 service 和一個 deployment。我們的 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 作為其類型的 Service。它會監聽任何 IP 地址的集群節點上的 32000 端口。傳入的連接將中繼到 8080 端口上。而作為內部通信,這個服務在 80 端口上進行監聽。
deployment
應用程序本身,一旦容器化了,就可以通過一個 Deployment 資源部署到 Kubernetes。deployment.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
這個部署文件里的定義最有意思的地方就是 image 部分。不同于硬編碼鏡像名稱和標簽的方式,我們使用了一個變量。后面的內容,我們會看到怎樣將該變量用作 Ansible 的模板以及通過命令替換鏡像名稱(以及部署用的其他參數)。
Playbook
這個實驗中,我們使用 Ansible 作為部署工具。還有許多其他的方式用來部署 Kubernetes 資源包括Helm Charts,但是我認為 Ansible 是一個相對簡單一些的選擇。Ansible 使用 playbooks 來組織它的操作。我們的 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 已經包括了k8s 模塊用來處理和 Kubernetes API 服務器的通信。所以我們不需要安裝kubectl但是我們需要一個有效的 kubeconfig 文件來連接到集群(后面會詳細介紹)。讓我們快速討論一下這個 playbook 重要的部分:
這個 playbook 用來部署服務以及部署資源到集群上。
當我們需要在動態執行的過程中向定義文件中注入數據時,我們需要使用定義文件作為模板這樣變量可以應用到外部環境。
為此,Ansible 具有查找功能,你可以在其中傳遞一個有效的 YAML 文件作為模板。Ansible 支持許多將變量注入模板的方法。在這個實驗中,我們使用命令行的方法。
學習怎樣持續優化您的 k8s 集群
讓我們開始安裝 Ansible 然后使用它自動部署一個 Jenkins 服務器以及 Docker 運行環境。我們同樣需要安裝 openshift Python 模塊用來將 Ansible 連接到 Kubernetes。 Ansible 的安裝非常簡單;只需要安裝 Python 然后使用 pip 安裝 Ansible:
登錄 Jenkins 實例。
安裝 Python3,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
安裝必要的 Ansible 角色用來部署一個 Jenkins 實例。
ansible-galaxy install geerlingguy.jenkins
安裝 Dcoker 角色:
ansible-galaxy install geerlingguy.docker
創建一個 playbook.yml 添加下面的代碼:
- 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訪問到 Jenkins:
點擊登錄鏈接使用 “admin” 作為用戶名,“admin” 作為登錄密碼。這些都是通過 Ansible 角色創建的默認憑據。當 Jenkins 在生產環境中使用時,你可以(應該)修改這些默認值。這個可以通過設置角色變量來進行設置。你可以參考角色官方頁面。
你需要做的最后一件事情就是安裝下面這些我們這個實驗中用到的插件:
git
pipeline
CloudBees Docker Build and Publish
GitHub
之前我們提到了,這個實驗假設你已經有一個啟動的 Kubernetes 集群。為了讓 Jenkins 連接到這個集群上,我們需要添加必要的 kubeconfig 文件。在這個特定的實驗中,我們使用主機在 Google Cloud 的 Kubernetes 集群所以我們可以使用 gcloud command。因環境而異。但是不管什么情況,我們都必須拷貝 kubeconfig 文件到 Jenkins 的用戶目錄下,如下所示:
$ sudo cp ~/.kube/config ~jenkins/.kube/ $ sudo chown -R jenkins: ~jenkins/.kube/
需要記住的是你使用的賬號必須要有必要的權限用來創建管理 Deployment 和 Service。
創建一個新的 Jenkins 任務選擇流水線類型的任務。任務的設置如下圖所示:
我們修改的配置有:
我們使用 Poll SCM 作為構建觸發器;設置這個選項來讓 Jenkins 定期檢查 Git 倉庫(按 * * * * 指示的每分鐘進行檢查)。如果倉庫從上次輪詢后做了修改,任務就會被觸發。
從流水線本身來說,我們指定了倉庫的 URL 以及憑據。分支是 master 分支。
這個實驗中,我們在一個 Jenkinsfile 中添加了所有的任務的代碼,Jenkinsfile 跟代碼一樣存放在同一個倉庫當中。Jenkinsfile 我們會在后面的文章中進行討論。
轉到/credentials/store/system/domain/_/newCredentials鏈接下然后添加目標憑據。請確認你每個憑據均提供一個有意義的 ID 和描述信息因為你會在后面使用到它們。
Jenkinsfile 是用來指導 Jenkins 如何構建,測試,容器化,發布以及交付我們的應用程序的文件。我們的 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}\"" } } } } }
這個文件比它本身看起來要簡單的多。基本上,這個流水線包括了 4 個階段:
在哪里構建我們的 Go 二進制文件從而確保構建過程中無錯誤出現。
在哪里進行一個簡單的 UAT 測試能確保應用程序如預期運行。
發布,在哪里構建 Docker 鏡像然后推送到倉庫。在這之后,任何環境都可以使用它。
部署,這是流水線的最后一步, Ansible 會與 Kubernetes 通信然后應用這些定義文件。
現在,讓我們討論下這個 Jenkinsfile 中重要的部分:
一開始的兩個階段大致差不多。它們都是使用 golang Docker 鏡像來構建/測試應用程序。讓階段在所有構建和測試均已準備就緒的容器中運行始終是一個很好的實踐。另外的選擇就是安裝這些工具到 master 服務器上或者是其中一個節點上。當你需要測試不同版本的工具時問題容易顯現出來。例如,或許你想使用 Go 1.9 構建測試你的代碼,但是我們的應用尚未準備好支持最新版本的 Golang。所有的東西都放在鏡像中的話修改版本號或者甚至是鏡像類型會和修改字符串一樣簡單。
在發布階段(從42行開始)開頭定義了一個環境變量,這個環境變量會在后面的步驟中使用到。這個變量指向的是我們先前步驟在 Jenkins 中添加的 Docker Hub 憑據。
48 行:我們使用 docker 插件來構建鏡像。它默認使用我們 registry 中的 Dockerfile 然后添加構建號作為鏡像的 tag。后面,當你需要決定哪次 Jenkins 構建作為當前運行容器的來源時這會非常的重要。
49-51行:鏡像構建成功后,我們使用構建號將其推送到 Docker Hub。另外,我們在鏡像上添加了 “latest” 的標簽(一個第二標簽)因此我們允許用戶不需要指定構建號即可拉取鏡像
56-60行:在部署階段,我們將部署和服務定義文件應用到集群上。我們使用之前討論過的 Ansible 的 playbook。記住,我們傳遞 image_id 作為命令行的參數。該值將自動替換部署文件中的鏡像名稱。
了解更多關于配置模式的知識
這部分是真正將我們的工作進行測試的內容。我們將會提交代碼到 GitHub 上確保我們的代碼直到到達集群之前都是經過流水線操作的。
添加我們的文件:git add *
提交我們的改動:git commit -m “Initial commit”
推送到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"}
OK,我們可以看到應用程序工作正常。讓我們在代碼中故意制造一個錯誤以確保流水線不會將錯誤的代碼應用到目標環境中:
將應顯示的信息修改為“Hello World!”,注意到我們將每個單詞的首字母大寫并在末尾添加了一個感嘆號。然而客戶或許不想讓信息這樣顯示,流水線應該在 Test 階段停止。
首先,讓我們做一些改動。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,我們可以看到最后一次構建失敗了:
點擊失敗的任務,我們可以看到這個任務失敗的原因:
這樣我們錯誤的代碼永遠不會進入到目標環境上。
CI/CD 是一個遵循敏捷方法論的任何現代環境的一部分。
通過流水線,你可以確保從版本控制系統到目標環境(測試/預生產/生產/等等)的平穩過渡,同時應用所有必要的測試以及質量控制實踐。
這篇文章中,我們有一個實踐性的實驗來構建一個持續交付的流水線來部署一個 Golang 應用程序。
通過 Jenkins,我們可以從倉庫拉取代碼,構建以及使用一個相關聯的 Docker 鏡像進行測試。
下一步,我們進行容器化進而將已通過我們的測試的應用程序推送到 Docker Hub。
最后,我們使用 Ansible 將應用程序部署到運行在 Kubernetes 上的目標環境當中。
使用 Jenkins 流水線和 Ansible 可以非常輕松靈活地修改工作流。例如,我們可以在 Test 階段增加更多的測試,我們可以修改用來構建和測試代碼的 Go 的版本號,另外我們還可以使用更多的變量在其他諸如部署和服務定義的地方進行修改。
最好的部分是我們使用 Kubernetes 部署,這能夠確保當我們在零停機時間的情況下改變容器鏡像。因為在默認情況下部署使用滾動更新的方式來一次性終止和重建容器。只有在新的容器啟動和健康后舊的容器才會終止。
上述就是小編為大家分享的怎樣使用 Kubernetes和Jenkins創建一個CI/CD流水線了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。