什麼是持續整合(CI)/持續部署(CD)?
在軟體開發中經常會提到 持續整合(Continuous Integration)(CI)和 持續交付(Continuous Delivery)(CD)這幾個術語。但它們真正的意思是什麼呢?
在談論軟體開發時,經常會提到 持續整合(Continuous Integration)(CI)和 持續交付(Continuous Delivery)(CD)這幾個術語。但它們真正的意思是什麼呢?在本文中,我將解釋這些和相關術語背後的含義和意義,例如 持續測試(Continuous Testing)和 持續部署(Continuous Deployment)。
概覽
工廠裡的裝配線以快速、自動化、可重複的方式從原材料生產出消費品。同樣,軟體交付管道以快速、自動化和可重複的方式從原始碼生成釋出版本。如何完成這項工作的總體設計稱為“持續交付”(CD)。啟動裝配線的過程稱為“持續整合”(CI)。確保質量的過程稱為“持續測試”,將最終產品提供給使用者的過程稱為“持續部署”。一些專家讓這一切簡單、順暢、高效地執行,這些人被稱為 運維開發(DevOps)
“持續”是什麼意思?
“持續”用於描述遵循我在此提到的許多不同流程實踐。這並不意味著“一直在執行”,而是“隨時可執行”。在軟體開發領域,它還包括幾個核心概念/最佳實踐。這些是:
- 頻繁釋出:持續實踐背後的目標是能夠頻繁地交付高質量的軟體。此處的交付頻率是可變的,可由開發團隊或公司定義。對於某些產品,一季度、一個月、一週或一天交付一次可能已經足夠頻繁了。對於另一些來說,一天可能需要多次交付也是可行的。所謂持續也有“偶爾、按需”的方面。最終目標是相同的:在可重複、可靠的過程中為終端使用者提供高質量的軟體更新。通常,這可以通過很少甚至無需使用者的互動或掌握的知識來完成(想想裝置更新)。
- 自動化流程:實現此頻率的關鍵是用自動化流程來處理軟體生產中的方方面面。這包括構建、測試、分析、版本控制,以及在某些情況下的部署。
- 可重複:如果我們使用的自動化流程在給定相同輸入的情況下始終具有相同的行為,則這個過程應該是可重複的。也就是說,如果我們把某個歷史版本的程式碼作為輸入,我們應該得到對應相同的可交付產出。這也假設我們有相同版本的外部依賴項(即我們不建立該版本程式碼使用的其它交付物)。理想情況下,這也意味著可以對管道中的流程進行版本控制和重建(請參閱稍後的 DevOps 討論)。
- 快速迭代:“快速”在這裡是個相對術語,但無論軟體更新/釋出的頻率如何,預期的持續過程都會以高效的方式將原始碼轉換為交付物。自動化負責大部分工作,但自動化處理的過程可能仍然很慢。例如,對於每天需要多次釋出候選版更新的產品來說,一輪 整合測試(integrated testing)
什麼是“持續交付管道”?
將原始碼轉換為可釋出產品的多個不同的 任務(task)和 作業(job)通常串聯成一個軟體“管道”,一個自動流程成功完成後會啟動管道中的下一個流程。這些管道有許多不同的叫法,例如持續交付管道、部署管道和軟體開發管道。大體上講,程式管理者在管道執行時管理管道各部分的定義、執行、監控和報告。
持續交付管道是如何工作的?
軟體交付管道的實際實現可以有很大不同。有許多程式可用在管道中,用於原始碼跟蹤、構建、測試、指標採集,版本管理等各個方面。但整體工作流程通常是相同的。單個業務流程/工作流應用程式管理整個管道,每個流程作為獨立的作業執行或由該應用程式進行階段管理。通常,在業務流程中,這些獨立作業是以應用程式可理解並可作為工作流程管理的語法和結構定義的。
這些作業被用於一個或多個功能(構建、測試、部署等)。每個作業可能使用不同的技術或多種技術。關鍵是作業是自動化的、高效的,並且可重複的。如果作業成功,則工作流管理器將觸發管道中的下一個作業。如果作業失敗,工作流管理器會向開發人員、測試人員和其他人發出警報,以便他們儘快糾正問題。這個過程是自動化的,所以比手動執行一組過程可更快地找到錯誤。這種快速排錯稱為 快速失敗(fail fast),並且在抵達管道端點方面同樣有價值。
“快速失敗”是什麼意思?
管道的工作之一就是快速處理變更。另一個是監視建立釋出的不同任務/作業。由於編譯失敗或測試未通過的程式碼可以阻止管道繼續執行,因此快速通知使用者此類情況非常重要。快速失敗指的是在管道流程中儘快發現問題並快速通知使用者的方式,這樣可以及時修正問題並重新提交程式碼以便使管道再次執行。通常在管道流程中可通過檢視歷史記錄來確定是誰做了那次修改並通知此人及其團隊。
所有持續交付管道都應該被自動化嗎?
管道的幾乎所有部分都是應該自動化的。對於某些部分,有一些人為干預/互動的地方可能是有意義的。一個例子可能是 使用者驗收測試(user-acceptance testing)(讓終端使用者試用軟體並確保它能達到他們想要/期望的水平)。另一種情況可能是部署到生產環境時使用者希望擁有更多的人為控制。當然,如果程式碼不正確或不能執行,則需要人工干預。
有了對“持續”含義理解的背景,讓我們看看不同型別的持續流程以及它們在軟體管道上下文中的含義。
什麼是“持續整合”?
持續整合(CI)是在原始碼變更後自動檢測、拉取、構建和(在大多數情況下)進行單元測試的過程。持續整合是啟動管道的環節(儘管某些預驗證 —— 通常稱為 上線前檢查(pre-flight checks) —— 有時會被歸在持續整合之前)。
持續整合的目標是快速確保開發人員新提交的變更是好的,並且適合在程式碼庫中進一步使用。
持續整合是如何工作的?
持續整合的基本思想是讓一個自動化過程監測一個或多個原始碼倉庫是否有變更。當變更被推送到倉庫時,它會監測到更改、下載副本、構建並執行任何相關的單元測試。
持續整合如何監測變更?
目前,監測程式通常是像 Jenkins 這樣的應用程式,它還協調管道中執行的所有(或大多數)程序,監視變更是其功能之一。監測程式可以以幾種不同方式監測變更。這些包括:
- 輪詢:監測程式反覆詢問程式碼管理系統,“程式碼倉庫裡有什麼我感興趣的新東西嗎?”當代碼管理系統有新的變更時,監測程式會“喚醒”並完成其工作以獲取新程式碼並構建/測試它。
- 定期:監測程式配置為定期啟動構建,無論原始碼是否有變更。理想情況下,如果沒有變更,則不會構建任何新內容,因此這不會增加額外的成本。
- 推送:這與用於程式碼管理系統檢查的監測程式相反。在這種情況下,程式碼管理系統被配置為提交變更到倉庫時將“推送”一個通知到監測程式。最常見的是,這可以以 webhook 的形式完成 —— 在新程式碼被推送時一個 掛勾(hook)的程式通過網際網路向監測程式傳送通知。為此,監測程式必須具有可以通過網路接收 webhook 資訊的開放埠。
什麼是“預檢查”(又稱“上線前檢查”)?
在將程式碼引入倉庫並觸發持續整合之前,可以進行其它驗證。這遵循了最佳實踐,例如 測試構建(test build)和 程式碼審查(code review)。它們通常在程式碼引入管道之前構建到開發過程中。但是一些管道也可能將它們作為其監控流程或工作流的一部分。
例如,一個名為 Gerrit 的工具允許在開發人員推送程式碼之後但在允許進入(Git 遠端)倉庫之前進行正式的程式碼審查、驗證和測試構建。Gerrit 位於開發人員的工作區和 Git 遠端倉庫之間。它會“接收”來自開發人員的推送,並且可以執行通過/失敗驗證以確保它們在被允許進入倉庫之前的檢查是通過的。這可以包括檢測新變更並啟動構建測試(CI 的一種形式)。它還允許開發者在那時進行正式的程式碼審查。這種方式有一種額外的可信度評估機制,即當變更的程式碼被合併到程式碼庫中時不會破壞任何內容。
什麼是“單元測試”?
單元測試(也稱為“提交測試”),是由開發人員編寫的小型的專項測試,以確保新程式碼獨立工作。“獨立”這裡意味著不依賴或呼叫其它不可直接訪問的程式碼,也不依賴外部資料來源或其它模組。如果執行程式碼需要這樣的依賴關係,那麼這些資源可以用 模擬(mock)來表示。模擬是指使用看起來像資源的 程式碼存根(code stub),可以返回值,但不實現任何功能。
在大多陣列織中,開發人員負責建立單元測試以證明其程式碼正確。事實上,一種稱為 測試驅動開發(test-driven develop)(TDD)的模型要求將首先設計單元測試作為清楚地驗證程式碼功能的基礎。因為這樣的程式碼可以更改速度快且改動量大,所以它們也必須執行很快。
由於這與持續整合工作流有關,因此開發人員在本地工作環境中編寫或更新程式碼,並通單元測試來確保新開發的功能或方法正確。通常,這些測試採用斷言形式,即函式或方法的給定輸入集產生給定的輸出集。它們通常進行測試以確保正確標記和處理出錯條件。有很多單元測試框架都很有用,例如用於 Java 開發的 JUnit。
什麼是“持續測試”?
持續測試是指在程式碼通過持續交付管道時執行擴充套件範圍的自動化測試的實踐。單元測試通常與構建過程整合,作為持續整合階段的一部分,並專注於和其它與之互動的程式碼隔離的測試。
除此之外,可以有或者應該有各種形式的測試。這些可包括:
- 整合測試 驗證元件和服務組合在一起是否正常。
- 功能測試 驗證產品中執行功能的結果是否符合預期。
- 驗收測試 根據可接受的標準驗證產品的某些特徵。如效能、可伸縮性、抗壓能力和容量。
所有這些可能不存在於自動化的管道中,並且一些不同型別的測試分類界限也不是很清晰。但是,在交付管道中持續測試的目標始終是相同的:通過持續的測試級別證明程式碼的質量可以在正在進行的釋出中使用。在持續整合快速的原則基礎上,第二個目標是快速發現問題並提醒開發團隊。這通常被稱為快速失敗。
除了測試之外,還可以對管道中的程式碼進行哪些其它型別的驗證?
除了測試是否通過之外,還有一些應用程式可以告訴我們測試用例執行(覆蓋)的原始碼行數。這是一個可以衡量程式碼量指標的例子。這個指標稱為 程式碼覆蓋率(code-coverage),可以通過工具(例如用於 Java 的 JaCoCo)進行統計。
還有很多其它型別的指標統計,例如程式碼行數、複雜度以及程式碼結構對比分析等。諸如 SonarQube 之類的工具可以檢查原始碼並計算這些指標。此外,使用者還可以為他們可接受的“合格”範圍的指標設定閾值。然後可以在管道中針對這些閾值設定一個檢查,如果結果不在可接受範圍內,則流程終端上。SonarQube 等應用程式具有很高的可配置性,可以設定僅檢查團隊感興趣的內容。
什麼是“持續交付”?
持續交付(CD)通常是指整個流程鏈(管道),它自動監測原始碼變更並通過構建、測試、打包和相關操作執行它們以生成可部署的版本,基本上沒有任何人為干預。
持續交付在軟體開發過程中的目標是自動化、效率、可靠性、可重複性和質量保障(通過持續測試)。
持續交付包含持續整合(自動檢測原始碼變更、執行構建過程、執行單元測試以驗證變更),持續測試(對程式碼執行各種測試以保障程式碼質量),和(可選)持續部署(通過管道釋出版本自動提供給使用者)。
如何在管道中識別/跟蹤多個版本?
版本控制是持續交付和管道的關鍵概念。持續意味著能夠經常整合新程式碼並提供更新版本。但這並不意味著每個人都想要“最新、最好的”。對於想要開發或測試已知的穩定版本的內部團隊來說尤其如此。因此,管道建立並輕鬆儲存和訪問的這些版本化物件非常重要。
在管道中從原始碼建立的物件通常可以稱為 工件(artifact)。工件在構建時應該有應用於它們的版本。將版本號分配給工件的推薦策略稱為 語義化版本控制(semantic versioning)。(這也適用於從外部源引入的依賴工件的版本。)
語義版本號有三個部分: 主要版本(major)、 次要版本(minor) 和 補丁版本(patch)。(例如,1.4.3 反映了主要版本 1,次要版本 4 和補丁版本 3。)這個想法是,其中一個部分的更改表示工件中的更新級別。主要版本僅針對不相容的 API 更改而遞增。當以 向後相容(backward-compatible)的方式新增功能時,次要版本會增加。當進行向後相容的版本 bug 修復時,補丁版本會增加。這些是建議的指導方針,但只要團隊在整個組織內以一致且易於理解的方式這樣做,團隊就可以自由地改變這種方法。例如,每次為釋出完成構建時增加的數字可以放在補丁欄位中。
如何“分銷”工件?
團隊可以為工件分配 分銷(promotion)級別以指示適用於測試、生產等環境或用途。有很多方法。可以用 Jenkins 或 Artifactory 等應用程式進行分銷。或者一個簡單的方案可以在版本號字串的末尾新增標籤。例如,-snapshot
可以指示用於構建工件的程式碼的最新版本(快照)。可以使用各種分銷策略或工具將工件“提升”到其它級別,例如 -milestone
或 -production
,作為工件穩定性和完備性版本的標記。
如何儲存和訪問多個工件版本?
從原始碼構建的版本化工件可以通過管理 工件倉庫(artifact repository)的應用程式進行儲存。工件倉庫就像構建工件的版本控制工具一樣。像 Artifactory 或 Nexus 這類應用可以接受版本化工件,儲存和跟蹤它們,並提供檢索的方法。
管道使用者可以指定他們想要使用的版本,並在這些版本中使用管道。
什麼是“持續部署”?
持續部署(CD)是指能夠自動提供持續交付管道中釋出版本給終端使用者使用的想法。根據使用者的安裝方式,可能是在雲環境中自動部署、app 升級(如手機上的應用程式)、更新網站或只更新可用版本列表。
這裡的一個重點是,僅僅因為可以進行持續部署並不意味著始終部署來自管道的每組可交付成果。它實際上指,通過管道每套可交付成果都被證明是“可部署的”。這在很大程度上是由持續測試的連續級別完成的(參見本文中的持續測試部分)。
管道構建的釋出成果是否被部署可以通過人工決策,或利用在完全部署之前“試用”釋出的各種方法來進行控制。
在完全部署到所有使用者之前,有哪些方法可以測試部署?
由於必須回滾/撤消對所有使用者的部署可能是一種代價高昂的情況(無論是技術上還是使用者的感知),已經有許多技術允許“嘗試”部署新功能並在發現問題時輕鬆“撤消”它們。這些包括:
藍/綠測試/部署
在這種部署軟體的方法中,維護了兩個相同的主機環境 —— 一個“藍色” 和一個“綠色”。(顏色並不重要,僅作為標識。)對應來說,其中一個是“生產環境”,另一個是“預釋出環境”。
在這些例項的前面是排程系統,它們充當產品或應用程式的客戶“閘道器”。通過將排程系統指向藍色或綠色例項,可以將客戶流量引流到期望的部署環境。通過這種方式,切換指向哪個部署例項(藍色或綠色)對使用者來說是快速,簡單和透明的。
當新版本準備好進行測試時,可以將其部署到非生產環境中。在經過測試和批准後,可以更改排程系統設定以將傳入的線上流量指向它(因此它將成為新的生產站點)。現在,曾作為生產環境例項可供下一次候選釋出使用。
同理,如果在最新部署中發現問題並且之前的生產例項仍然可用,則簡單的更改可以將客戶流量引流回到之前的生產例項 —— 有效地將問題例項“下線”並且回滾到以前的版本。然後有問題的新例項可以在其它區域中修復。
金絲雀測試/部署
在某些情況下,通過藍/綠髮布切換整個部署可能不可行或不是期望的那樣。另一種方法是為 金絲雀(canary)測試/部署。在這種模型中,一部分客戶流量被重新引流到新的版本部署中。例如,新版本的搜尋服務可以與當前服務的生產版本一起部署。然後,可以將 10% 的搜尋查詢引流到新版本,以在生產環境中對其進行測試。
如果服務那些流量的新版本沒問題,那麼可能會有更多的流量會被逐漸引流過去。如果仍然沒有問題出現,那麼隨著時間的推移,可以對新版本增量部署,直到 100% 的流量都排程到新版本。這有效地“更替”了以前版本的服務,並讓新版本對所有客戶生效。
功能開關
對於可能需要輕鬆關掉的新功能(如果發現問題),開發人員可以新增 功能開關(feature toggles)。這是程式碼中的 if-then
軟體功能開關,僅在設定資料值時才啟用新程式碼。此資料值可以是全域性可訪問的位置,部署的應用程式將檢查該位置是否應執行新程式碼。如果設定了資料值,則執行程式碼;如果沒有,則不執行。
這為開發人員提供了一個遠端“終止開關”,以便在部署到生產環境後發現問題時關閉新功能。
暗箱釋出
在 暗箱釋出(dark launch)中,程式碼被逐步測試/部署到生產環境中,但是使用者不會看到更改(因此名稱中有 暗箱(dark)一詞)。例如,在生產版本中,網頁查詢的某些部分可能會重定向到查詢新資料來源的服務。開發人員可收集此資訊進行分析,而不會將有關介面,事務或結果的任何資訊暴露給使用者。
這個想法是想獲取候選版本在生產環境負載下如何執行的真實資訊,而不會影響使用者或改變他們的經驗。隨著時間的推移,可以排程更多負載,直到遇到問題或認為新功能已準備好供所有人使用。實際上功能開關標誌可用於這種暗箱釋出機制。
什麼是“運維開發”?
運維開發(DevOps) 是關於如何使開發和運維團隊更容易合作開發和釋出軟體的一系列想法和推薦的實踐。從歷史上看,開發團隊研發了產品,但沒有像客戶那樣以常規、可重複的方式安裝/部署它們。在整個週期中,這組安裝/部署任務(以及其它支援任務)留給運維團隊負責。這經常導致很多混亂和問題,因為運維團隊在後期才開始介入,並且必須在短時間內完成他們的工作。同樣,開發團隊經常處於不利地位 —— 因為他們沒有充分測試產品的安裝/部署功能,他們可能會對該過程中出現的問題感到驚訝。
這往往導致開發和運維團隊之間嚴重脫節和缺乏合作。DevOps 理念主張是貫穿整個開發週期的開發和運維綜合協作的工作方式,就像持續交付那樣。
持續交付如何與運維開發相交?
持續交付管道是幾個 DevOps 理念的實現。產品開發的後期階段(如打包和部署)始終可以在管道的每次執行中完成,而不是等待產品開發週期中的特定時間。同樣,從開發到部署過程中,開發和運維都可以清楚地看到事情何時起作用,何時不起作用。要使持續交付管道迴圈成功,不僅要通過與開發相關的流程,還要通過與運維相關的流程。
說得更遠一些,DevOps 建議實現管道的基礎架構也會被視為程式碼。也就是說,它應該自動配置、可跟蹤、易於修改,並在管道發生變化時觸發新一輪執行。這可以通過將管道實現為程式碼來完成。
什麼是“管道即程式碼”?
管道即程式碼(pipeline-as-code)是通過編寫程式碼建立管道作業/任務的通用術語,就像開發人員編寫程式碼一樣。它的目標是將管道實現表示為程式碼,以便它可以與程式碼一起儲存、評審、跟蹤,如果出現問題並且必須終止管道,則可以輕鬆地重建。有幾個工具允許這樣做,如 Jenkins 2。
DevOps 如何影響生產軟體的基礎設施?
傳統意義上,管道中使用的各個硬體系統都有配套的軟體(作業系統、應用程式、開發工具等)。在極端情況下,每個系統都是手工設定來定製的。這意味著當系統出現問題或需要更新時,這通常也是一項自定義任務。這種方法違背了持續交付的基本理念,即具有易於重現和可跟蹤的環境。
多年來,很多應用被開發用於標準化交付(安裝和配置)系統。同樣, 虛擬機器(virtual machine)被開發為模擬在其它計算機之上執行的計算機程式。這些 VM 要有管理程式才能在底層主機系統上執行,並且它們需要自己的作業系統副本才能執行。
後來有了 容器(container)。容器雖然在概念上與 VM 類似,但工作方式不同。它們只需使用一些現有的作業系統結構來劃分隔離空間,而不需要執行單獨的程式和作業系統的副本。因此,它們的行為類似於 VM 以提供隔離但不需要過多的開銷。
VM 和容器是根據配置定義建立的,因此可以輕易地銷燬和重建,而不會影響執行它們的主機系統。這允許執行管道的系統也可重建。此外,對於容器,我們可以跟蹤其構建定義檔案的更改 —— 就像對原始碼一樣。
因此,如果遇到 VM 或容器中的問題,我們可以更容易、更快速地銷燬和重建它們,而不是在當前環境嘗試除錯和修復。
這也意味著對管道程式碼的任何更改都可以觸發管道新一輪執行(通過 CI),就像對程式碼的更改一樣。這是 DevOps 關於基礎架構的核心理念之一。