持續改進和軟體自動化

10 年之前,我們在 Amazon 承辦一個專案,以了解我們的團隊將想法化為高品質生產系統的速度有多快。這引致我們測量軟體輸送量,以便改進執行速度。我們發現,從程式碼簽入到生產平均花了 16 天。在 Amazon,團隊先從想法開始,接著一般會用一天半的時間撰寫程式碼,為想法賦予生命。我們會花不到一小時建立並部署新程式碼。其餘的時間將近有 14 天,我們等候團隊成員開始建立、執行部署,和執行測試。到了專案結尾,我們會建議將簽入後的程序自動化,以改善執行速度。其目標在於去除延遲,同時維護甚至改善品質。

此項建議的核心為一套增加執行速度的持續提升計劃。我們改善執行速度的決策是以「堅持最高標準」領導原則為依據。此項原則崇尚堅持不懈地維持高品標準,持續突破標竿,及交付高品質的產品、服務和程序。我們的領導原則描述 Amazon 如何經營事業、主管如何領導統御,以及我們如何在決策時始終以客戶為重。

Amazon 已建立軟體開發工具,提高軟體工程師的生產力。我們建立自己的集中託管建立系統 Brazil,在伺服器上執行一系列命令,以產生能夠部署的成品。在這階段,Brazil 並未聽取原始程式碼的變更。必須以人為方式起始建立。我們也自有部署系統 Apollo,其要求在起始部署的過程中,需有建立成品上傳進去。鑑於業界對於持續交付的興趣,我們建立自有系統 Pipelines,以將 Brazil 和 Apollo 之間的軟體交付程序加以自動化。

Pipelines:我們的持續部署工具

我們展開試用計畫,將少數團隊的軟體交付程序自動化。在我們完成之前,標題測試團隊從簽入到生產,對於所花的全部時間已達到減少 90%。
 
該專案所驗證的概念是,以管道作為團隊定義將軟體發佈至客戶所需全部步驟的方式。管道中的第一個步驟是建立成品。管道接著經由一系列步驟執行該建立成品,至成品發佈至所有客戶為止。我們使用管道來降低新的程式碼變更可能對客戶帶來負面影響的風險。管道中的每一個步驟應能增加我們的信心,認為建立成品不含缺陷。若是缺陷不幸進入生產,我們想讓生產儘速恢復良好狀態。
 
當初我們啟動 Pipelines 時,只能就每個應用程式建立單一發佈程序的模型。該限制帶動團隊的發佈程序實現一致性、標準化和簡單化,繼而減少缺陷。我們改為使用管道之前,團隊經常為錯誤修正和重大功能發佈使用不同的發佈程序。隨著其他團隊見到採取試用自動化交付的團隊成功,便開始將手動管理的發佈程序移轉到管道內,以便也能改善一致性。以往有各種發佈程序的團隊,這時採行大家都用的一種標準化程序。此外,他們將發佈程序移入工具中後,團隊成員經常重新審視作法,找到簡化程序的方法。
 
Pipelines 團隊設有逐年目標,要以「誘因下的採用」提高使用率。 換言之,必須讓產品好到眾人要求使用。我們測量了使用管道將軟體部署進入生產的團隊數量,並且依照其自動化程度將管道分類。我們觀察到團隊設立目標,要以管道發佈軟體,並且朝向全自動發佈邁進。然而,我們也注意到在部分組織,我們度量品質的方式會導致團隊未提供任何測試,逕將發佈程序自動化。

「多少測試為足夠?」這個問題的答案見仁見智。這需要團隊瞭解本身操作所處的環境。為了因應此一情形,我們採取另一項領導原則「主事者精神」。此項原則旨在做長遠思考,不為短期結果犧牲長期的價值。Amazon 的軟體團隊對於測試懷有高標準,為此付出大量心力,畢竟只要對產品負責,表示也為產品內任何缺陷帶來的後果負責。若問題對客戶有所影響,即會由小型單一執行緒軟體團隊成員處理該問題,並且即時修復。增加執行速度與回應生產問題之間存有緊張關係,隨之激發團隊進行充分的測試。然而,若對測試過度投資,卻可能由於他人的腳步更快而導致我們無法成功。我們始終在尋求不致於對業務形成阻礙之下改善軟體發佈程序。

我們面臨的另一個難題是,團隊未相互學習軟體發佈的最佳實務。單一執行緒團隊受到自主運作的鼓舞,因此工程師會獨立解決遇到的部署問題。找到能夠滿足軟體發佈之所需的解決方案時,他們會經由郵寄清單、營運會議和其他溝通管道,將技術向其他工程師推廣。這種溝通風格有兩個問題。首先,這類管道為最佳溝通管道,因此並非人人都能得知這些新技術。第二,主管雖鼓舞團隊採行新的最佳實務,卻無以瞭解團隊是否已經完成實際採行該項最佳實務所需的工作。我們體認到,需要協助所有工程師能夠取用我們學到的最佳實務,並且讓主管能夠識別所需關注的管道。
 
我們的解決方案是將檢查最佳實務加入至我們用以建立和發佈軟體的工具中,以將所學機械化。我們對一件事實頗為敏感,那就是一個組織的最佳實務,不見得是另一組織的良好實務,因此我們允許這些檢查依照各組織分別設定。組織層級的最佳實務檢查讓主管有能力將發佈程序量身訂製,以符合其業務需要。想要鼓舞或落實採行新最佳實務的主管,能夠首先在工程師日常使用的工具內提出警語。藉由在工具內放置訊息,幾乎可以保證團隊成員能夠得知該最佳實務,也知道該最佳實務何時會生效。我們發現只要給團隊時間學習新的最佳實務以及進行辯論,組織就有機會推陳出新以及改善其最佳實務檢查。這最終能使得最佳實務的品質提升,工程社群也更能接受。
 
我們有系統地識別所要應用的最佳實務。我們有一群最資深的工程師,為效果不彰的發佈建立常見原因的型錄。他們識別出原本能使發佈奏效的步驟。接著,我們利用該份清單,建立我們的一套最佳實務檢查。透過此程序,我們瞭解,雖然我們想讓新軟體修訂版立即送入客戶手中,同時不費心力、不使可用性降低,不過我們是以可用性為第一優先,速度次之,再來才是讓工程師更方便。

降低缺陷到達客戶處的風險

我們的發佈程序,包括管道和部署系統在內,必須能夠盡快識別這些缺陷,防止對客戶有所影響。我們需要確定將發佈程序設定正確,並且確定建立成品如所預期地運作。

部署的清潔:部署測試的最基本形式能確保新部署的成品能夠啟動,並能回應工作。部署後的工作流程中,我們會執行快速檢查,確保新部署的成品已經啟動並且為流量服務。例如,我們使用 AWS CodeDeploy AppSpec 檔案中的生命週期事件勾點來觸發簡單指令碼中對部署執行停止、開始和驗證。我們也會檢查確實有足夠容量能服務客戶的流量。我們也在 CodeDeploy 中建立例如最低運作狀態良好主機數等技術,以驗證始終有足夠的容量能夠服務客戶。最後,如果部署引擎能偵測到失敗,應該回復變更,使客戶發現缺陷的時間盡量縮短。

生產之前測試:Amazon 的最佳實務之一即為將單位、整合及前期生產測試加以自動化,並將這些測試加入我們的管道之中。我們堅持執行負載及安全測試,偏向將這些測試加入至我們的管道中。我們所說的單位測試是指您可能會想在建立機器上執行的所有測試,包括風格檢查、程式碼涵蓋範圍、程式碼複雜性等。我們所認為的整合測試包括所有離線測試,例如失敗注入、自動化瀏覽器測試等。討論單位和整合測試的優質文章很多,在此不多贅述。

我們的單位和整合測試旨在驗證我們的建立成品行為在運作上正確。執行的驗證越多,對客戶暴露出缺陷的風險越低。為縮短產品送入客戶手中之前所花的時間,我們嘗試在發佈程序中盡早偵測出缺陷。一般而言,這意味著若測試較小且較快,變更上有任何問題,就能更快收到回饋。

在 Amazon,我們也使用內部稱為前期生產測試的技術。前期生產環境是我們將變更部署進入生產之前,最後測試之處。一項前期生產環境測試採用系統的生產組態,以便行為完全如同生產系統。這種方式有兩大優點。首先,前期生產環境會測試生產組態,以確定服務能正確連線至所有生產資源,包括生產資料存放區。第二,會確保系統與其所依存之生產服務的 API 能夠正確互動。前期生產環境僅限由負責該項服務的團隊使用,並且從未接收出自客戶的流量。執行前期生產測試能提高我們的信心,確信相同的程式碼和組態在生產時能夠運作。

生產中驗證:我們將程式碼發佈給客戶時,不會全部一次完成。缺陷同時對所有客戶釋出的影響範圍太大。我們則是部署至儲存格 — 完全獨立的服務執行個體。我們將變更部署至第一個儲存格中的第一組客戶時,會極度地謹慎。我們只讓少數客戶看見新的變更,並且收集新程式碼是否有效的回饋。我們部署 Canary 之後,會監控服務發出的錯誤量。如果錯誤率上升,我們會自動將變更回復。例如,我們可能會等到有 3,000 個正資料點,並且無負資料點時,再繼續部署。

要是自動化測試漏失一個使用案例,複雜度就會提高。我們以結構化並且可重複的測試盡力捕捉所有錯誤,無論為自動化或手動式。然而,儘管我們盡最大努力,缺陷仍有可能溜過。為了測試我們的測試,我們將新作的變更留在生產中一段固定的時間,觀察是否有非團隊成員發現問題。我們已花了許多時間辯論是否應該讓變更直接留在生產中,或是在 Canary 部署完成之後應該等多久,再部署至部署群組的其餘部分。我們有許多團隊決定除了收集正資料點之外又再等候一段固定時間,才繼續部署例行程序的進展。管道等候的時間量,高度依存於團隊。一些團隊等候數小時,也有的等候數分鐘。影響越大、補救問題所花的時間越長,發佈程序越慢。

我們從第一個儲存格獲得信心之後,就會將新的程式碼變更向越來越多客戶公開,至完全發佈為止。如同對待 Canary 部署的方式,我們等候部署至第一個新儲存格獲得信心之後,再繼續往下一個儲存格進展。隨著我們從建立成品增添信心,在驗證程式碼變更之上所花的時間也跟著縮短。結果形成一種模式,我們容易把目的放在能夠從簽入盡快達到第一位生產客戶。不過,經過進入生產之後,我們慢慢將新程式碼發佈給客戶,隨著在部署的其餘部分時遞增速度,努力增加更多信心。

為了確定我們的生產系統能持續符合客戶的請求,我們在系統上生成綜合流量。我們希望要是服務無法正確運作,能迅速得到回饋,因此至少每分鐘執行一次綜合測試。我們之所以設計出綜合測試,是為了確保執行程序的運作狀態良好,並且所有相依性皆得到測試,這經常要求測試所有面向公眾的 API。

控制軟體發佈時機:為了控制軟體發佈的安全性,我們建立了一些機制以便控制變更在管道中移動的速度。我們使用指標、窗期和安全檢查來控制軟體發佈的時機。

可設定管道,使得根據指標變化發出警示時可禁止部署。我們普遍採用指標,對於系統的運作狀態、儲存格的運作狀態、可用區域和區域,以及能想到的一切幾乎都設有警示。我們將管道設定成為有重要指標觸發警示時會停止程式碼的部署。然而,有時候團隊需要部署修正程式,以處理系統警示。在此情況下,我們會讓團隊覆寫警示,防止變更在管道中移動。

我們的管道能指定窗期,在其中允許變更在管道中繼續前進。團隊能找到本身的窗期,限制變更到達客戶處的時機。AWS 的團隊偏好選擇有許多人可迅速回應,並能補救部署所造成的問題時發佈軟體。為實現此情形,團隊一般將窗期設定成為限於營業時間部署。Amazon 也有些團隊偏好於客戶流量小時發佈軟體。這些窗期可視需要加以覆寫。

我們也有能力根據建立成品的內容停止管道運行。例如,我們能將已知含有不良套件或有特定 Git 參考的建立成品加以封鎖。我們發現對套件所作的變更中含有效能回歸時,便曾使用這項功能。如果我們只將套件從套件儲存庫中移除,則已含有缺失套件的管道仍能將那項不良的變更部署至客戶處。

我們對於執行速度的作法

我們發現團隊相當急於擁抱自動化。我們都非常希望能為客戶建立和發佈可提高生活品質的功能,而持續交付可永續達成這個理念。我們見到自動化可去除令人心生挫折、易出錯誤和勞力密集的手動工作,繼而將時間交回給工程師。我們已經展現,持續部署對品質有正面影響。我們已經看出,自動化讓團隊能頻繁發佈,以及一次發佈一項變更,因此更容易識別回歸。

系統尚新時,測試的表面範圍通常能為大多數的團隊成員所瞭解,因此部分手動測試還易於處理。不過,隨著系統越來越複雜,團隊成員也改變,自動化的價值也提高了。我們樂意將系統自動化,以便專心為客戶提高價值,而非手動管理將變更從客戶處移除的程序。

多年來,Amazon 以向客戶發佈軟體的速度和這些發佈的安全為焦點,執行著持續提升計劃。我們並未從本人於此文章所述的全部風險檢查和測試著手。歷經一段時日,我們發明許多方式以識別和降低風險。

持續提升計劃是由組織不同層級的業務主管所執行。這允許各業務主管配合其業務所受的風險和影響,調整軟體發佈程序。我們有一些持續提升計劃是跨 Amazon 大型部門所執行,部分較小型組織的主管則執行本身的計劃。我們知道,規則總有例外。我們的系統有選擇退出機制,因此不致於拖慢需要永久或暫時豁免的團隊。最終,我們的團隊能掌控其軟體的行為,並對其軟體發佈程序的投資得當負起責任。

我們首先測量痛點在何處,予以解決,並且反覆處理。為使此項工作能夠永續,我們需要以遞增方式進行,逐漸享受提升的好處。Amazon 首次開始使用管道時,許多團隊不確定持續部署是否有效。為使團隊開始動作,我們鼓勵團隊將目前的發佈程序、手動程序等全部編碼至一個管道中。對許多團隊而言,其管道是作為發佈程序的視覺介面,而非經由發佈程序將建立成品自動推進之用。隨著他們的信心提高,便逐漸在管道中的不同階段開啟自動化,最終不再需要手動觸發管道的任何步驟。

時間快轉到現今。Amazon 現已來到團隊撰寫新程式碼時即以全自動為目標的時點。對我們而言,這是我們業務持續成長的唯一方式。
 


作者簡介

Mark Mansour 是 Amazon Web Services 的資深軟體開發經理。其於 2014 年加入 Amazon,並從事軟體部署的各種內部和外部服務,以及持續大規模的軟體交付。工作之餘,Mark 喜歡修理鐘錶、玩桌遊和打高爾夫球。

在部署期間確保轉返安全