歡迎光臨
每天分享高質量文章

容器環境下的持續集成最佳實踐

本文結合 Drone + GitFlow + Kubernetes 介紹一些容器環境下持續集成、持續發佈方面的實踐經驗。文章將展示從 Hello World 到單人單分支手動發佈到團隊多分支 GitFlow 工作流再到團隊多分支 semantic-release 語意化發佈最後到通知 Kubernetes 全自動發佈,如何從零開始一步一步搭建 CI 將團隊開發、測試、發佈的流程全部自動化的過程,最終能讓開發人員只需要認真提交代碼就可以完成日常的所有 DevOps 工作。
雲原生(Cloud Native)是伴隨的容器技術發展出現的的一個詞,最早出自 Pivotal 公司(即開發了 Spring 的公司)的一本技術小冊子 Migrating to Cloud-Native Application Architectures, 其中定義了雲原生應用應當具備的一些特質,如無狀態、可持續交付、微服務化等。隨後雲原生概念被廣為取用,並圍繞這一概念由數家大廠牽頭,成立了 CNCF 基金會來推進雲原生相關技術的發展,主要投資並孵化雲原生生態內的若干專案,包括瞭如 Kubernetes / etcd / CoreDNS 等耳熟能詳的重要專案。而這張大大的雲原生版圖仍然在不斷的擴展和完善。
從個人理解來說,傳統的應用由於年代久遠,更多會考慮單機部署、行程間通信等典型的“單機問題”,雖然也能工作在容器下,但由於歷史包袱等原因,架構上已經很難做出大的調整。而“雲原生”的應用大都是從一開始就為容器而準備,很少考慮在單機環境下使用,有些甚至無法脫離容器環境工作;考慮的場景少,從而更輕量,迭代更快。比如 etcd 之於 ZooKeeper, Traefik 之於 Nginx 等,相信只要在容器環境下實現一次同樣的功能,就能強烈的體會到雲原生應用所特有的便捷之處。
在 CNCF 的版圖下,持續集成與持續交付(Continuous Integration & Delivery)板塊一直缺少一個欽定的主角,雖然也不乏 Travis CI、GitLab、Jenkins 這樣的知名專案,但最能給人雲原生應用感覺的,應該還是 Drone 這個專案,本文將圍繞 Drone 結合 GitFlow 及 Kubernetes 介紹一些容器環境下持續集成、持續發佈(CI/CD)方面的實踐經驗。
主流 CI/CD 應用對比

 

之前我也介紹過基於 Travis CI 的一些持續集成實踐[1]。後來經過一些比較和調研,最終選擇了 Drone 作為主力 CI 工具。截止本文,團隊已經使用 Drone 有 2 年多的時間,從 v0.6 一路用到現在即將發佈的 v1.0,雖然也踩了不少坑,但總的來說 Drone 還是可以滿足大部分需求,並以不錯的勢頭在完善和發展的。
下麵這張表總結了主流的幾個 CI/CD 應用的特點: 
Travis CI 和 CircleCI 是目前占有率最高的兩個公有雲 CI,易用性上相差無幾,只是收費方式有差異。由於不支持私有部署,如果並行的任務量一大,按行程收費其實並不划算;而且由於服務器位置的原因,如果推送鏡像到國內,速度很不理想。
GitLab CI 雖然好用,但和 GitLab 是深度系結的,我們的代碼托管在 GitHub,整體遷移代碼庫的成本太大,放棄。
Jenkins 作為老牌勁旅,也是目前市場占有率最高的 CI,幾乎可以改寫所有 CI 的使用場景,由於使用 Java 編寫,配置檔案使用 Groovy 語法,非常適合 Java 為主語言的團隊。Jenkins 顯然是可以滿足我們需要的,只是團隊並非 Java 為主,又已經習慣了使用 YAML 書寫 CI 配置,抱著嘗鮮的心態,將 Jenkins 作為了保底的選擇。
綜上,最終選擇 Drone 的結論也就不難得出了,Drone 即開源,又可以私有化部署,同時作為雲原生應用,官方提供了針對 Docker、Docker Swarm、Kubernetes 等多種容器場景下的部署方案,針對不同容器場景還有特別優化,比如在 Docker Swarm 下 Drone 是以 Agent 方式運行 CI 任務的,而在 Kubernetes 下則通過創建 Kubernetes Job 來實現,顯然充分利用了容器的優勢所在,這也是 Drone 優於其他 CI 應用之處。個人還覺得 Drone 的語法是所有 CI 中最容易理解和掌握的,由於 Drone 每一個步驟都是運行一個 Docker 容器,本地模擬或除錯也非常容易。
一句話概況 Drone,可以將其看做是可以支持私有化部署的開源版 CircleCI,並且目前仍然沒有看到有其他主打這個定位的 CI 工具,因此個人認為 Drone 是 CI/CD 方面雲原生應用頭把交椅的有力競爭者。
容器環境下一次規範的發佈應該包含哪些內容

 

技術選型完成後,我想首先演示一下最終的成果,希望能直觀的體現出 CI 對自動化效率起到的提升,不過這就涉及到一個問題:在容器環境下,一次發佈應該包含哪些內容,其中有哪些部分是可以被 CI 自動化完成的。這個問題雖然每家公司各不相同,不過按經驗來說,容器環境下一次版本發佈通常包含這樣一個 Checklist:
  • 代碼的下載構建及編譯

  • 運行單元測試,生成單元測試報告及改寫率報告等

  • 在測試環境對當前版本進行測試

  • 為待發佈的代碼打上版本號

  • 編寫 ChangeLog 說明當前版本所涉及的修改

  • 構建 Docker 鏡像

  • 將 Docker 鏡像推送到鏡像倉庫

  • 在預發佈環境測試當前版本

  • 正式發佈到生產環境

 

看上去很繁瑣對嗎,如果每次發佈都需要人工去處理上述的所有內容,不僅容易出錯,而且也無法應對 DevOps 時代一天至少數次的發佈頻率,那麼下麵就來使用 CI 來解決所有問題吧。
CI 流程演示

 

為了對 CI 流程有最直觀的認識,我創建了一個精簡版的 GitHub 專案 AlloVince/drone-ci-demo[2] 來演示完整的流程,同時專案對應的 CI 地址是 cloud.drone.io/AlloVince/drone-ci-demo,專案自動構建的 Docker 鏡像會推送到 docker registry 的 allovince/drone-ci-demo。為了方便說明,假設這個專案的核心檔案只有 index.html 一個靜態頁面。
單人開發樣式
目前這個專案背後的 CI 都已經配置部署好,假設我是這個專案的唯一開發人員,如何開發一個新功能併發布新版本呢?
  • Clone 專案到本地, 修改專案代碼, 如將 Hello World 改為 Hello World V2。

  • git add .,然後書寫符合約定的 Commit 並提交代碼, git commit -m “feature: hello world v2”。

  • 推送代碼到代碼庫git push,等待數分鐘後,開發人員會看到單元測試結果,GitHub 倉庫會產生一次新版本的 Release,Release 內容為當前版本的 ChangeLog, 同時線上已經完成了新功能的發佈。

 

雖然在開發者看來,一次發佈簡單到只需 3 個指令,但背後經過瞭如下的若干次交互,這是一次發佈實際產生交互的時序圖,具體每個環節如何工作將在後文中詳細說明。 
多人開發樣式
一個專案一般不止一個開發人員,比如我是新加入這個專案的成員,在這個 Demo 中應該如何上線新功能呢?同樣非常簡單:
  1. Clone 專案到本地,創建一個分支來完成新功能的開發, git checkout -b feature/hello-world-v3。在這個分支修改一些代碼,比如將Hello World V2修改為Hello World V3

  2. git add .,書寫符合規範的 Commit 並提交代碼, git commit -m “feature: hello world v3”

  3. 將代碼推送到代碼庫的對應分支, git push origin feature/hello-world

  4. 如果功能已經開發完畢,可以向 Master 分支發起一個 Pull Request,並讓專案的負責人 Code Review

  5. Review 通過後,專案負責人將分支合併入主幹,GitHub 倉庫會產生一次新版本的 Release,同時線上已經完成了新功能的發佈。

這個流程相比單人開發來多了 2 個環節,很適用於小團隊合作,不僅強制加入了 Code Review 把控代碼質量,同時也避免新人的不規範行為對發佈帶來影響。實際專案中,可以在 GitHub 的設置界面對 Master 分支設置寫入保護,這樣就從根本上杜絕了誤操作的可能。當然如果團隊中都是熟手,就無需如此謹慎,每個人都可以負責 PR 的合併,從而進一步提升效率。
 
GitFlow 開發樣式
在更大的專案中,參與的角色更多,一般會有開發、測試、運維幾種角色的劃分;還會劃分出開發環境、測試環境、預發佈環境、生產環境等用於代碼的驗證和測試;同時還會有多個功能會在同一時間並行開發。可想而知 CI 的流程也會進一步複雜。
能比較好應對這種複雜性的,首選 GitFlow 工作流, 即通過並行兩個長期分支的方式規範代碼的提交。而如果使用了 GitHub,由於有非常好用的 Pull Request 功能,可以將 GitFlow 進行一定程度的簡化,最終有這樣的工作流: 
  • 以 dev 為主開發分支,Master 為發佈分支

  • 開發人員始終從 dev 創建自己的分支,如 feature-a

  • feature-a 開發完畢後創建 PR 到 dev 分支,併進行 code review

  • review 後 feature-a 的新功能被合併入 dev,如有多個並行功能亦然

  • 待當前開發周期內所有功能都合併入 dev 後,從 dev 創建 PR 到 master

  • dev 合併入 Master,並創建一個新的 Release

上述是從 Git 分支角度看代碼倉庫發生的變化,實際在開發人員視角里,工作流程是怎樣的呢。假設我是專案的一名開發人員,今天開始一期新功能的開發:
  1. Clone 專案到本地,git checkout dev。從 dev 創建一個分支來完成新功能的開發, git checkout -b feature/feature-a。在這個分支修改一些代碼,比如將Hello World V3修改為Hello World Feature A

  2. git add .,書寫符合規範的 Commit 並提交代碼, git commit -m “feature: hello world feature A”

  3. 將代碼推送到代碼庫的對應分支, git push origin feature/feature-a:feature/feature-a

  4. 由於分支是以 feature/ 命名的,因此 CI 會運行單元測試,並自動構建一個當前分支的鏡像,發佈到測試環境,並自動配置一個當前分支的域名如 test-featue-a.avnpc.com

  5. 聯繫產品及測試同學在測試環境驗證並完善新功能

  6. 功能通過驗收後發起 PR 到 dev 分支,由 Leader 進行 code review

  7. Code Review 通過後,Leader 合併當前 PR,此時 CI 會運行單元測試,構建鏡像,併發布到測試環境

  8. 此時 dev 分支有可能已經積累了若干個功能,可以訪問測試環境對應 dev 分支的域名,如 test.avnpc.com,進行集成測試。

  9. 集成測試完成後,由運維同學從 Dev 發起一個 PR 到 Master 分支,此時會 CI 會運行單元測試,構建鏡像,併發布到預發佈環境

  10. 測試人員在預發佈環境下再次驗證功能,團隊做上線前的其他準備工作

    運維同學合併 PR,CI 將為本次發佈的代碼及鏡像自動打上版本號並書寫 ChangeLog,同時發佈到生產環境。

  11. 由此就完成了上文中 Checklist 所需的所有工作。雖然描述起來看似冗長,但不難發現實際作為開發人員,並沒有任何複雜的操作,流程化的部分全部由 CI 完成,開發人員只需要關註自己的核心任務:按照工作流規範,寫好代碼,寫好 Commit,提交代碼即可。

接下來將介紹這個以 CI 為核心的工作流,是如何一步步搭建的。
Step by Step 構建 CI 工作流

 

Step.0:基於 Kubernetes 部署 Drone v1.0.0
以 GitHub 為例,截止本文完成時間(2019 年 3 月 28 日), Drone 剛剛發佈了第一個正式版本 v1.0.0。官方文件已經提供了分別基於 Docker、Kubernetes 的 Drone 部署說明,不過比較簡略,因此這裡給出一個相對完整的配置檔案。
首先需要在 GitHub 創建一個 Auth App,用於 repo 的訪問授權。應用創建好之後,會得到 Client ID 和 Client Secret 。同時 Authorization callback URL 應填寫 Drone 服務對應域名下的 /login,如 https://ci.avnpc.com/login。
Drone 支持 SQLite、MySQL、Postgres、S3 等多種後端儲存,主要用於記錄 build logs 等文本信息,這些信息並不是特別重要,且我們的 CI 有可能做遷移,因此個人更推薦使用 SQLite。
而在 Kubernetes 環境下,SQLite 更適合用掛載 NAS 的方式供節點使用,因此首先將儲存的部分獨立為檔案 drone-pvc.yml,可以根據實際情況配置 nfs.path 和 nfs.server:
  1. kubectl apply -f drone-pvc.yaml
Drone 的配置主要涉及兩個鏡像:
  • drone/kubernetes-secrets 加密資料服務,用於讀取 Kubernetes 的 secrets

  • drone/drone:1.0.0-rc.6 就是 Drone 的 server 端,由於在 Kubernetes 下 Drone 利用了 Job 機制,因此不需要部署 Agent。

這部分配置較長,可以直接參考示例 drone.yaml[3]。
 
主要涉及到的配置項包括:
 
  • Drone/kubernetes-secrets 鏡像中

    • SECRET_KEY:資料加密傳輸所用的 key,可以使用 openssl rand -hex 16 生成一個

  • Drone/Drone鏡像中

    • DRONEKUBERNETESENABLED:開啟 Kubernetes 樣式

    • DRONEKUBERNETESNAMESPACE:Drone 所使用的 Namespace, 這裡使用 default

    • DRONEGITHUBSERVER:GitHub 服務器地址,一般為 https://github.com

    • DRONEGITHUBCLIENT_ID:上文創建 GitHub Auth App 得到的 Client ID

    • DRONEGITHUBCLIENT_SECRET:上文創建 GitHub Auth App 得到的 Client Secret

    • DRONESERVERHOST:Drone 服務所使用的域名

    • DRONESERVERPROTO:http 或 https

    • DRONEDATABASEDRIVER:Drone 使用的資料庫型別,這裡為 SQLite3

    • DRONEDATABASEDATASOURCE:這裡為 SQLite 資料庫的存放路徑

    • DRONESECRETSECRET:對應上文的 SECRET_KEY

    • DRONESECRETENDPOINT:加密資料服務的地址,這裡通過 Kubernetes Service 暴露,無需修改

最後部署即可:
  1. kubectl apply -f drone.yaml
部署後首次登錄 Drone 就會跳轉到 GitHub Auth App 進行授權,授權完畢後可以看到所有能讀寫的 Repo,選擇需要開啟 CI 的 Repo,點擊 ACTIVATE 即可。 如果開啟成功,在 GitHub Repo 的 Settings > Webhooks 下可以看到 Drone 的回呼地址。
Step.1:Hello World for Drone
在正式開始搭建工作流之前,首先可以測試一下 Drone 是否可用。Drone 預設的配置檔案是 .drone.yml, 在需要 CI 的 repo 根目錄下創建.drone.yml, 內容如下,提交並git push到代碼倉庫即可觸發 Drone 執行 CI。
  1. kind: pipeline
  2. name: deploy
  3. steps:
  4. - name: hello-world
  5. image: docker
  6. commands:
  7. - echo "hello world"
Drone v1 的語法主要參考的 Kubernetes 的語法,非常直觀,無需閱讀文件也可以知道,我們首先定義了一個管道(Pipeline),管道由若干步驟(step)組成,Drone 的每個步驟是都基於容器實現的,因此 Step 的語法就回到了我們熟悉的 Docker,一個 Step 會拉取 image 定義的鏡像,然後運行該鏡像,並順序執行 commands 定義的指令。
在上例中,Drone 首先 clone git repo 代碼到本地,然後根據 .drone.yml 所定義的,拉取 Docker 的官方鏡像,然後運行該進行並掛載 git repo 的代碼到 /drone/src 目錄。
在 Drone 的界面中,也可以清楚的看到這一過程。
 
本階段對應:
  • 代碼部分:https://github.com/AlloVince/drone-ci-demo/tree/hello-world

  • Drone 構建記錄:https://cloud.drone.io/AlloVince/drone-ci-demo/1

  • Docker 鏡像:無

 

Step.2:單人工作流,自動化單元測試與 Docker 鏡像構建
有了 Hello World 的基礎,接下來我們嘗試將這個工作流進行擴充。
為了方便說明,這裡假設專案語言為 JavaScript,專案內新增了 test/index.js 檔案用於模擬單元測試,一般在 CI 中,只要程式的傳回值為 0,即代表運行成功。這個檔案中我們僅僅輸出一行 Log Unit test passed用於模擬單元測試通過。
我們希望將代碼打包成 Docker 鏡像,根目錄下增加了 Dockerfile 檔案,這裡直接使用 Nginx 的官方鏡像,構建過程只有 1 行 COPY index.html /usr/share/nginx/html/, 這樣鏡像運行後可以通過 http 請求看到 index.html 的內容。
至此我們可以將工作流改進為:
  • 當 Master 分支接收到 push 後,運行單元測試

  • 當 GitHub 發佈一次 Release, 構建 Docker 鏡像,並推送到鏡像倉庫

對應的 Drone 配置檔案如下:
  1. kind: pipeline
  2. name: deploy
  3. steps:
  4. - name: unit-test
  5. image: node:10
  6. commands:
  7. - node test/index.js
  8. when:
  9. branch: master
  10. event: push
  11. - name: build-image
  12. image: plugins/docker
  13. settings:
  14. repo: allovince/drone-ci-demo
  15. username: allovince
  16. password:
  17. from_secret: DOCKER_PASSWORD
  18. auto_tag: true
  19. when:
  20. event: tag
雖然比 Hello World 複雜了一些,但是可讀性仍然很好,配置檔案中出現了幾個新概念:
Step 運行條件, 即 when 部分,上例中展示了當代碼分支為 Master,且收到一個 push;以及當代碼被標記 tag 這兩種情況。Drone 還支持 repo、運行結果等很多其他條件,可以參考 Drone Conditions[4] 文件。
Plugin 插件,上例中用於構建和推送鏡像的是 plugins/docker 這個 Plugin, 一個 Plugin 本質上仍然是一個 Docker 鏡像,只是按照 Drone 的規範接受特定的輸入,並完成特定的操作。所以完全可以將 Plugin 看做一個無法更改 command 的 Docker 鏡像。
Docker 這個 Plugin 由 Drone 官方提供,用於 Docker 鏡像的構建和推送,具體的用法可以查看 Docker 插件的文件[5]。例子中演示的是將鏡像推送到私有倉庫,如果不做特殊配置,鏡像將被推送到 Docker 的官方倉庫。
此外 Docker 插件還有一個很方便的功能,如果設置 auto_tag: true,將根據代碼的版本號自動規劃 Docker 鏡像的標簽,如代碼版本為1.0.0,將為 Docker 鏡像打三個標簽 1, 1.0, 1.0.0。如果代碼版本號不能被解析,則鏡像標簽為 latest。
目前 Drone 的插件已經有很多,可以改寫主流的雲服務商和常見的工作流,並且自己製作插件的成本也不高。
Secret 加密資料,鏡像倉庫的用戶名和密碼都屬於敏感信息,因此可以使用 fromsecret 獲取加密資料。一條加密資料就是一個 key / value 對,如上例中的 DOCKERPASSWORD 就是我們自己定義的加密資料 key。即便加密資料在 log 中被打印,UI 也只能看到 *。加密資料的 value 需要提前儲存好,儲存的方式有 3 種:
  • 通過 Drone UI 界面中, repo -> Settings -> Secrets 添加,所添加的加密資料將儲存在 Drone 的資料庫中,僅能在當前 repo 中使用。

  • 通過 Drone cli 加密後儲存在 .drone.yml 檔案中, 使用範圍僅限 yaml 檔案內

  • 通過 Kubernetes 儲存為 Kubernetes Secret,稱為 External Secrets,所有的 repo 都可以共享。如果是團隊使用的話,這種儲存方式顯然是最方便的,但也要註意安全問題,因此 External Secrets 還支持 repo 級別的權限管理, 可以只讓有當前 repo 寫入權限的人才能使用對應 secret。

 

這個階段對應:
  • 代碼倉庫:https://github.com/AlloVince/drone-ci-demo/tree/single-person

  • push 時觸發的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/4

  • Release 時觸發的 Drone CI:https://cloud.drone.io/AlloVince/drone-ci-demo/5

  • Release 後 CI 構建的 Docker 鏡像:allovince/drone-ci-demo:latest

Step.3:GitFlow 多分支團隊工作流
上面的工作流已經基本可以應付單人的開發了,而在團隊開發時,這個工作流還需要一些擴展。不需要引入 Drone 的新功能,只需要在上文基礎上根據分支做一點調整即可。
首先保證單元測試位於 steps 的第一位,並且限定團隊工作的分支,在 push 和 pull_request 時,都能觸發單元測試。
  1. - name: unit-test
  2. image: node:10
  3. commands:
  4. - node test/index.js
  5. when:
  6. branch:
  7. include:
  8. - feature/*
  9. - master
  10. - dev
  11. event:
  12. include:
  13. - push
  14. - pull_request
然後根據 GitFlow 的流程對於不同的分支構建 Docker 鏡像並打上特定標簽,以 feature 分支為例,下麵的配置約定了當分支名滿足 feature/*,並收到 push 時,會構建 Docker 鏡像並打標簽,標簽名稱為當前分支名去掉 feature/。如分支 feature/readme, 對應 Docker 鏡像為 allovince/drone-ci-demo:readme,考慮到 feature 分支一般都出於開發階段,因此新的鏡像會改寫舊的。配置如下:
  1. - name: build-branch-image
  2. image: plugins/docker
  3. settings:
  4. repo: allovince/drone-ci-demo
  5. username: allovince
  6. password:
  7. from_secret: DOCKER_PASSWORD
  8. tag:
  9. - ${DRONE_BRANCH##feature/}
  10. when:
  11. branch: feature/*
  12. event: push
鏡像的 Tag 處不再使用自動方式,其中 DRONE_BRANCH 是 Drone 的內置環境變數(Environment),對應當前的分支名。##feature/ 是執行了一個字串的替換操作(Substitution)。更多的環境變數和字串操作都可以在文件中找到。
以此類推,可以查看這個階段的完整 .drone.yml ,此時我們的工作流示例如下:
  • 團隊成員從 dev 分支 checkout 自己的分支 feature/readme

  • 向feature/readme提交代碼並 push, CI 運行單元測試,構建鏡像allovince/drone-ci-demo:readme

  • 功能開發完成後,團隊成員向 dev 分支 發起 pull request , CI 運行單元測試

  • 團隊其他成員 merge pull request, CI 運行單元測試,構建鏡像allovince/drone-ci-demo:test

  • 運維人員從 dev 向 master 發起 pull request,CI 運行單元測試,並構建鏡像allovince/drone-ci-demo:latest

  • 運維人員 merge pull request, 並 Release 新版本 pre-0.0.2, CI 構建鏡像allovince/drone-ci-demo:pre-0.0.2

可能細心的同學會發現 dev -> Master 的 pull request 時,構建鏡像失敗了,這是由於 Drone 出於安全考慮限制了在 pull request 時預設無法讀取加密資料,因此無法得到 Docker Registry 密碼。如果是私有部署的話,可以在 Repo Settings 中勾選 Allow Pull Requests,此處就可以構建成功。
Step.4:語意化發佈
上面基本完成了一個支持團隊協作的半自動 CI 工作流,如果不是特別苛刻的話,完全可以用上面的工作流開始幹活了。
不過基於這個工作流工作一段時間,會發現仍然存在痛點,那就是每次發佈都要想一個版本號,寫 ChangeLog,並且人工去 Release。
標記版本號涉及到上線後的回滾,追溯等一系列問題,應該是一項嚴肅的工作,其實如何標記早已有比較好的方案,即語意化版本。在這個方案中,版本號一共有 3 位,形如 1.0.0,分別代表:
  • 主版本號:當你做了不兼容的 API 修改

  • 次版本號:當你做了向下兼容的功能性新增

  • 修訂號:當你做了向下兼容的問題修正

 

雖然有了這個指導意見,但並沒有很方便的解決實際問題,每次發佈要搞清楚代碼的修改到底是不是向下兼容的,有哪些新的功能等,仍然要花費很多時間。
而語意化發佈(Semantic Release)就能很好的解決這些問題。
語意化發佈的原理很簡單,就是讓每一次 Commit 所附帶的 Message 格式遵守一定規範,保證每次提交格式一致且都是可以被解析的,那麼進行 Release 時,只要統計一下距離上次 Release 所有的提交,就分析出本次提交做了何種程度的改動,並可以自動生成版本號、自動生成 ChangeLog 等。
語意化發佈中,Commit 所遵守的規範稱為約定式提交(Conventional Commits)。比如 Node.js、 Angular、Electron 等知名專案都在使用這套規範。
語意化發佈首先將 Commit 進行分類,常用的分類(Type)有:
  • feat:新功能

  • fix:BUG 修複

  • docs:文件變更

  • style:文字格式修改

  • refactor:代碼重構

  • perf:性能改進

  • test:測試代碼

  • chore:工具自動生成

每個 Commit 可以對應一個作用域(Scope),在一個專案中作用域一般可以指不同的模塊。
當 Commit 內容較多時,可以追加正文和腳註,如果正文起始為BREAKING CHANGE,代表這是一個破壞性變更。
以下都是符合規範的 Commit:
  1. feat:增加重置密碼功能
  1. fix(郵件模塊):修複郵件發送延遲BUG
  1. featAPI):API重構
  2. BREAKING CHANGEAPI v3上線,API v1停止支持
有了這些規範的 Commit,版本號如何變化就很容易確定了,目前語意化發佈預設的規則如下:
  1. Commit 版本號變更
  2. BREAKING CHANGE 主版本號
  3. feat 次版本號
  4. fix / perf 修訂號
因此在 CI 部署 semantic-release 之後,作為開發人員只需要按照規範書寫 Commit 即可,其他的都由 CI 完成。
具體如何將語意化發佈加入 CI 流程中呢, semantic-release 是 js 實現的,如果是 js 的專案,可以直接在 package.json 中增加配置項,而對於任意語言的專案,推薦像 Demo 中一樣,在根目錄下增加 配置檔案release.config.js。這個配置目的是為了禁用預設開啟的 npm 發佈機制,可以直接套用。
semantic-release 要執行 GitHub Release,因此我們需要在 CI 中配置自己的 Personal access tokens 讓 CI 有 GitHub Repo 的讀寫權限, 可以通過 GitHub 點擊自己頭像 -> Settings -> Developer settings -> Personal access tokens -> Generate new token 生成一個 Token。 然後在 Drone 的 repo 設置界面新增一個 Secret, key 為 GITHUB_TOKEN, value 填入剛生成的 Token。
最後在 .drone.yml 中增加這樣一段就可以了。
  1. - name: semantic-release
  2. image: gtramontina/semantic-release:15.13.3
  3. environment:
  4. GITHUB_TOKEN:
  5. from_secret: GITHUB_TOKEN
  6. entrypoint:
  7. - semantic-release
  8. when:
  9. branch: master
  10. event: push
來再次模擬一下流程,feature 分支部分與上文相同:
  • 從 dev 向 master 發起 pull request,CI 運行單元測試,並構建鏡像allovince/drone-ci-demo:latest

  • merge pull request,CI 會執行單元測試並運行 semantic-release , 運行成功的話能看到 GitHub 新增 Release v1.0.0

  • GitHub Release 再次觸發CI 構建生產環境用 Docker 鏡像allovince/drone-ci-demo:1.0.0

 

最終我們能得到這樣一個賞心悅目的 Release。 
Step.5:Kubernetes 自動發佈
Docker 鏡像推送到倉庫後,我們還剩最後一步就可以完成全自動發佈的閉環,即通知 Kubernetes 將鏡像發佈到生產環境。這一步實現比較靈活,因為很多雲服務商在容器服務都會提供 Trigger 機制,一般是提供一個 URL,只要請求這個 URL 就可以觸發容器服務的發佈。Demo 中我們使用更為通用的方法,就是將 kubectl 打包為容器,以客戶端呼叫 Kubernetes 集群 Master 節點 API(kube-apiserver)的形式完成發佈。
假設我們在生產環境下 drone-ci-demo 專案的 Kubernetes 發佈檔案如下:
  1. ---
  2. apiVersion: extensions/v1beta1
  3. kind: Deployment
  4. metadata:
  5. name: ci-demo-deployment
  6. namespace: default
  7. spec:
  8. replicas: 1
  9. template:
  10. spec:
  11. containers:
  12. - image: allovince/drone-ci-demo
  13. name: ci-demo
  14. restartPolicy: Always
對應 .drone.yml 中增加 step 如下。這裡使用的插件是 honestbee/drone-kubernetes, 插件中 kubectl 連接 API 使用的是證書 + Token 的方式鑒權,因此需要先獲得證書及 Token, 已經授權的 Token 儲存於 Kubernetes Secret,可以通過 kubectl get secret [ your default secret name ] -o yaml | egrep ‘ca.crt:|token:’ 獲得並配置到 Drone 中,註意插件要求 Token 是明文的,需要 base64 解碼一下:echo [ your token ] | base64 -d && echo ”。
  1. - name: k8s-deploy
  2. image: quay.io/honestbee/drone-kubernetes
  3. settings:
  4. kubernetes_server:
  5. from_secret: KUBERNETES_SERVER
  6. kubernetes_cert:
  7. from_secret: KUBERNETES_CERT
  8. kubernetes_token:
  9. from_secret: KUBERNETES_TOKEN
  10. namespace: default
  11. deployment: ci-demo-deployment
  12. repo: allovince/drone-ci-demo
  13. container: ci-demo
  14. tag:
  15. - ${DRONE_TAG}
  16. when:
  17. event: tag
在示例中,可以看到在語意化發佈之後 CI 會將新版本的 Docker 鏡像自動發佈到 Kubernetes,這裡為了演示僅打印了指令並未實際運行。相當於運行瞭如下的指令:
  1. kubectl -n default set image deployment/ci-demo-deployment ci-demo=allovince/drone-ci-demo:v1.0.2
由於自動發佈的環節勢必要接觸到生產服務器,需要格外註意安全問題,首推的方式當然是將 CI 和 Kubernetes 集群放於同一內網中,同時可以使用 Kubernetes 的 RBAC 權限控制,為自動發佈單獨創建一個用戶,並刪除不必要的權限。
後話

 

總結一下,本文展示了從 Hello World 到單人單分支手動發佈再到團隊多分支 GitFlow 工作流再到團隊多分支 semantic-release 語意化發佈最後到 通知 Kubernetes 全自動發佈,如何從零開始一步一步搭建 CI 將團隊開發、測試、發佈的流程全部自動化的過程,最終能讓開發人員只需要認真提交代碼就可以完成日常的所有 DevOps 工作。
最終 Step 的完成品可以適配之前的所有 Step,如果不太在意實現細節的話,可以在此基礎上稍作修改,直接使用。
然而寫好每一個 Commit 這個看似簡單的要求,其實對於大多數團隊來說並不容易做到,在實施過程中,經常會遇到團隊成員不理解為什麼要重視 Commit 規範,每個 Commit 都要深思熟慮是否過於吹毛求疵等等疑問。
以 Commit 作為 CI 的核心,個人認為主要會帶來以下幾方面的影響:
  1. 一個好的 Commit,代表著開發人員對當前改動之於整個系統的影響,有非常清楚的認識,代碼的修改到底算 feat 還是 fix ,什麼時候用 BREAKING CHANGE 等都是要仔細斟酌的,每個 Commit 都會在 ChangeLog 里“留底”,從而約束團隊不隨意提交未經思考的代碼,提高代碼質量。

  2. 一個好的 Commit 也代表開發人員有能力對所實現功能進行精細的劃分,一個分支做的事情不宜過多,一個提交也應該專註於只解決一個問題,每次提交(至少是每次 push)都應該保持系統可構建、可運行、可測試,如果能堅持做到這些,對於合併代碼時的衝突解決,以及集成測試都有很大幫助。

  3. 由於每次發佈能清楚的看到所有關聯的 Commit 以及 Commit 的重要程度,那麼線上事故的回滾也會非常輕鬆,回滾到哪個版本,回滾後哪些功能會受到影響,只要看 CI 自動生成的 Release 記錄就一目瞭然。如果沒有這些,回滾誤傷到預期外的功能從而引發連鎖反應的慘痛教訓,可能很多運維都有過類似經歷吧。

因此 CI 自動化其實是錦上添花而非雪中送炭,如果團隊原本就無視規範,Commit 全是空白或者沒有任何意義的單詞,分支管理混亂,發佈困難,奢望引入一套自動化 CI 來能解決所有這些問題,無疑是不現實的。而只有原本就重視代碼質量,有一定規範意識,再通過自動化 CI 來監督約束,團隊在 CI 的幫助下代碼質量提高,從而有機會進一步改進 CI 的效率,才能形成良心迴圈。
願天下不再有難發佈的版本。
Q&A;

 

Q:Kubernetes 上主流的 CI/CD 方案是啥?
A:其實這無關 Kubernetes,從市場占有率來看,前三名分別是:
  1. Jenkins

  2. JetBrains TeamCity

  3. CircleCI

來源:https://www.datanyze.com/market-share/ci
Q:GitLab 自帶的 CI 與 Jenkins 和 GitLab 結合的 CI,該如何選擇?想知道更深層次的理解。
A:還是要結合自己團隊的實際情況做選擇。從成熟度來說,肯定是 Jenkins 用戶最多,成熟度最高,缺點是側重 Java,配置相對繁瑣。GitLab 自帶的 CI 相對簡單,可以用 yaml,和 GitLab 結合的最好,但功能肯定沒有 Jenkins 全面。如果是小團隊新專案,GitLab CI 又已經可以滿足需求的話,並不需要上 Jenkins,如果是較大的團隊,又是偏 Java 的,個人更偏向 Jenkins。
Q:Jenkins 如果不想運行在 Kubernetes 裡面,該怎麼和 Kubernetes 集成?
A:從 CI 的流程來說,CI 應用是不是跑在 Kubernetes 的並不重要,CI 只要能訪問代碼庫,有權限在生產環境發佈,是不是跑在容器里從效果來說其實沒有區別,只是用 Kubernetes 部署 Jenkins 的話,運維的一致性比較好,運維團隊不用額外花時間維護一套物理機的部署方案。
Q:Kubernetes 的回滾方案是回滾代碼,重做鏡像,還是先切流量,後做修改?
A:代碼一定是打包到鏡像里的,鏡像的版本就是代碼的版本,所以一定是切鏡像。至於回滾操作本身,Kubernetes 已經內置了很多滾動發佈(Rolling update)的策略,無論是發新版本還是回滾版本,都可以做到用戶無感知。
Q:鏡像大到幾 G 的話如何更新部署,有什麼好的實踐呢,以及如何回滾?
A:幾個要點:
  1. 鏡像倉庫部署在內網,鏡像推拉走內網,幾個 G 的鏡像傳輸起來也只是秒級別的

  2. 鏡像構建時將不經常變動的部分劃分到 1 層,因為 Docker 鏡像是分層的,這樣每次拉鏡像可以利用到 Docker 的快取傳輸的只有鏡像發生變化的部分

  3. 選擇 alpine 這樣儘量小的鏡像

回滾如果發生在相鄰的版本,一般物理機上是有快取的,速度都很快。
Q:Drone 開放 API 服務嗎?這樣方便其他系統集成。
A:可以調整一下思路,直接把需要的功能做成鏡像在 Drone 里呼叫就好了。
Q:如果有 Drone 的 Server怎麼做高可用?
A:Drone serve r用 Kubernetes 部署的話本身只起到了一個任務調度的作用,很難會遇到性能瓶頸。真的有性能問題可以嘗試水平擴展 Drone server,共享同一資料庫。
相關鏈接:
  1. https://avnpc.com/pages/android-auto-deploy-workflow-on-travis-ci

  2. https://github.com/AlloVince/drone-ci-demo

  3. https://github.com/AlloVince/drone-ci-demo/blob/master/kubernetes/drone.yaml

  4. https://docs.drone.io/user-guide/pipeline/conditions/

  5. http://plugins.drone.io/drone-plugins/drone-docker/

作者博客:https://avnpc.com/pages/drone-gitflow-kubernetes-for-cloud-native-ci

赞(0)

分享創造快樂