作者 | 朱燁

關(guān)于最佳實(shí)踐
本系列內(nèi)容是我們?cè)诓煌?xiàng)目的維護(hù)過(guò)程中總結(jié)的關(guān)于DevOps/SRE方面的優(yōu)秀實(shí)踐,我們將致力于在項(xiàng)目上盡最大的努力來(lái)推行這些優(yōu)秀實(shí)踐。我們希望這些最佳實(shí)踐能對(duì)項(xiàng)目的穩(wěn)定運(yùn)營(yíng)提供幫助,也希望剛接觸DevOps/SRE的新人能通過(guò)學(xué)習(xí)這些優(yōu)秀實(shí)踐來(lái)提升自己在這方面的水平。
因?yàn)镈evOps/SRE涉及到的方方面面比較多,一次性完成的工作量太大,所以我們決定分階段來(lái)完成,這一次發(fā)布的是“應(yīng)用開(kāi)發(fā)和部署”這個(gè)部分的內(nèi)容,后續(xù)我們將逐步發(fā)布“云平臺(tái)與網(wǎng)絡(luò)”,“操作系統(tǒng)和服務(wù)”,“用戶與權(quán)限”,“監(jiān)控與可視化”,“數(shù)據(jù)與備份”,“敏感數(shù)據(jù)”,“故障與應(yīng)急響應(yīng)”這幾部分的內(nèi)容。
所謂“最佳實(shí)踐”應(yīng)該是最適合自己的實(shí)踐,而不一定是最先進(jìn)的,而且每一種實(shí)踐本身也存在一定的局限性,所以我們?cè)诿枋隽藢?duì)應(yīng)實(shí)踐的優(yōu)點(diǎn)的同時(shí),也把可能存在的缺點(diǎn)寫了出來(lái),就是希望大家在看到它的好處的時(shí)候,也能知道可能存在的風(fēng)險(xiǎn)在那里,理性地去評(píng)估到底是不是要采用相應(yīng)的實(shí)踐,所以這里總結(jié)的最佳實(shí)踐請(qǐng)適度取用,不要為了“最佳”而實(shí)踐。
我們深知自己在諸多方面存在一定的局限性,相關(guān)的內(nèi)容可能存在一些不足,而且優(yōu)秀實(shí)踐本身會(huì)隨著技術(shù)更新等因素不停地變化,我們將會(huì)把藍(lán)皮書(shū)內(nèi)容同步發(fā)布在Github上(https://github.com/toc-lib/DevOps-SRE-best-practice) ,希望引發(fā)更廣范圍的傳播和討論。也請(qǐng)使用PR或Issue的方式來(lái)提出你的不同的觀點(diǎn)和更好的建議,謝謝。
應(yīng)用開(kāi)發(fā)和部署
使用牲口模式
在傳統(tǒng)的運(yùn)維環(huán)境中,由于條件的限制無(wú)法快速的提供新的基礎(chǔ)設(shè)施和環(huán)境,所以通常在業(yè)務(wù)的依賴環(huán)境如操作系統(tǒng)內(nèi)核,服務(wù),類庫(kù),運(yùn)行時(shí)版本等需要變化時(shí),我們會(huì)根據(jù)需要在現(xiàn)有的環(huán)境上做持續(xù)性變更。而且我們還可能會(huì)在機(jī)器上運(yùn)行一些臨時(shí)任務(wù),做調(diào)試和排錯(cuò)等,很多的時(shí)候,這些操作對(duì)應(yīng)的變化并不具有可追溯性,甚至不可以恢復(fù)到之前的狀態(tài)。這樣,剛開(kāi)始統(tǒng)一配置的無(wú)差別的一批機(jī)器隨著時(shí)間的推移慢慢的就會(huì)變得各自具有一些獨(dú)有的特性。另外還有一些類型的服務(wù),比如數(shù)據(jù)庫(kù),存儲(chǔ)等,其業(yè)務(wù)本質(zhì)就導(dǎo)致了集群中的每一臺(tái)機(jī)器具有獨(dú)特的屬性。當(dāng)我們?cè)诰S護(hù)這些服務(wù)的時(shí)候,需要根據(jù)每臺(tái)機(jī)器的特性來(lái)做不同的管理和配置,而且一旦機(jī)器出現(xiàn)故障的時(shí)候,也很難去創(chuàng)建出一樣的機(jī)器來(lái)替代。因?yàn)檫@種情形和養(yǎng)寵物類似,比如我們會(huì)給寵物起一個(gè)名字,它也需要悉心照料,生病的時(shí)候要帶去看病,所以我們稱這種服務(wù)模式為寵物模式。
而在具有云原生能力的平臺(tái)上,我們可以按需定制基礎(chǔ)鏡像,也能快速的從這個(gè)基礎(chǔ)鏡像中創(chuàng)建出運(yùn)行環(huán)境,我們的變更就可以基于基礎(chǔ)鏡像來(lái)做更新和版本迭代。這樣當(dāng)某一臺(tái)機(jī)器發(fā)生了故障,我們可以快速的復(fù)制出一臺(tái)一模一樣的機(jī)器來(lái)替代。如果需要做一些臨行性的操作和變化,在任務(wù)結(jié)束之后,也可以銷毀這臺(tái)已經(jīng)發(fā)生了變化的機(jī)器,使用一臺(tái)新的機(jī)器來(lái)替代,使整個(gè)集群恢復(fù)到一個(gè)最初的收斂狀態(tài)。這個(gè)場(chǎng)景和我們現(xiàn)實(shí)生活中的規(guī)?;陴B(yǎng)殖類似,對(duì)應(yīng)的我們稱這種服務(wù)模式為牲口模式。
大家所熟知的無(wú)狀態(tài)應(yīng)用,就是牲口模式的最常用的一種實(shí)現(xiàn)方式。在業(yè)務(wù)的設(shè)計(jì)和實(shí)施過(guò)程中,我們建議把邏輯和數(shù)據(jù)分離,在邏輯運(yùn)行環(huán)境不要兼顧數(shù)據(jù)存儲(chǔ)工作,比如請(qǐng)求的session相關(guān)的數(shù)據(jù),不要保存在本地,而是把它放在一個(gè)共享數(shù)據(jù)服務(wù)中,從而達(dá)到無(wú)狀態(tài)的目的,這樣就可以對(duì)邏輯運(yùn)行環(huán)境進(jìn)行牲口化的管理方式。
優(yōu)點(diǎn):
- 可隨時(shí)被銷毀或替換,結(jié)合自動(dòng)化基礎(chǔ)設(shè)施和監(jiān)控,自動(dòng)完成對(duì)故障機(jī)器或節(jié)點(diǎn)的替換。
 - 配合自動(dòng)化基礎(chǔ)設(shè)施和監(jiān)控,可實(shí)現(xiàn)自動(dòng)水平伸縮,從容應(yīng)對(duì)業(yè)務(wù)峰谷,節(jié)約成本。
 - 在不影響服務(wù)穩(wěn)定性的前提下可部署所需要版本的應(yīng)用、進(jìn)行系統(tǒng)升級(jí)或者打補(bǔ)丁。
 - 監(jiān)控和管理的重心不再是具體的單一資源的使用率,而是整體的承載能力和更深層次的性能關(guān)注點(diǎn)。
 
缺點(diǎn):
- 需要基礎(chǔ)設(shè)施平臺(tái)具有相應(yīng)的能力支撐,否則很難實(shí)現(xiàn)。
 - 不是所有的業(yè)務(wù)類型都能做牲口模式設(shè)計(jì),比如數(shù)據(jù)庫(kù)。
 
實(shí)施要點(diǎn):
- 除計(jì)算和業(yè)務(wù)處理過(guò)程中的臨時(shí)產(chǎn)生的數(shù)據(jù),數(shù)據(jù)的來(lái)源和最終的持久化應(yīng)由外部服務(wù)來(lái)提供,如獨(dú)立的內(nèi)存型數(shù)據(jù)庫(kù)或者關(guān)系型數(shù)據(jù)庫(kù)。
 - 可以使用客戶端Cookie、cache取代外部數(shù)據(jù)服務(wù)。如果有敏感數(shù)據(jù),服務(wù)器端可以加密后交由客戶端存儲(chǔ),在之后的請(qǐng)求時(shí)發(fā)回服務(wù)器解密使用。
 - 通過(guò)鎖或者冪等性設(shè)計(jì),使得應(yīng)用能正確、快速、自動(dòng)地解決對(duì)同一份數(shù)據(jù)的競(jìng)爭(zhēng)而導(dǎo)致的流程異常、數(shù)據(jù)不一致等問(wèn)題。例如,多個(gè)定時(shí)任務(wù)同時(shí)處理一批數(shù)據(jù)。
 
使業(yè)務(wù)升級(jí)向前兼容
向前兼容指低版本的系統(tǒng)、程序或技術(shù)能優(yōu)雅處理(例如:忽略其不理解的部分)高版本的系統(tǒng)、程序或技術(shù)。向前兼容技術(shù)的目標(biāo)是讓舊系統(tǒng)能夠識(shí)別為新系統(tǒng)生成的數(shù)據(jù),簡(jiǎn)單的說(shuō)就是舊版本的系統(tǒng)可以接受新版本的數(shù)據(jù),是舊版本對(duì)新版本的兼容。
我們建議在做業(yè)務(wù)升級(jí)時(shí)候,設(shè)計(jì)你的業(yè)務(wù)具有向前兼容的能力,以應(yīng)對(duì)升級(jí)失敗時(shí)某一功能模塊或者依賴無(wú)法隨之回滾的風(fēng)險(xiǎn)。比如說(shuō)在有數(shù)據(jù)庫(kù)字段變化的升級(jí)中,在正式對(duì)數(shù)據(jù)庫(kù)做變動(dòng)之前,基于舊的業(yè)務(wù)流程做代碼層面更新,使其可以兼容數(shù)據(jù)庫(kù)將要發(fā)生的改動(dòng)并加以部署。在數(shù)據(jù)庫(kù)升級(jí)完成之后,如果新的業(yè)務(wù)流程上線后不幸出現(xiàn)重大的問(wèn)題等情況需要回滾時(shí),回滾之后的代碼仍然可以兼容數(shù)據(jù)庫(kù)的變化,而不用對(duì)數(shù)據(jù)庫(kù)也進(jìn)行回滾,畢竟數(shù)據(jù)庫(kù)的回滾成本非常高。
優(yōu)點(diǎn):
- 可以在新版本出現(xiàn)不容易修復(fù)和存在重大的風(fēng)險(xiǎn)的時(shí)候快速地回滾到舊的版本,業(yè)務(wù)中斷的可能性會(huì)大大降低。
 - 即使整個(gè)系統(tǒng)中存在不可回滾的部分,但我們不用花費(fèi)很多的精力去考慮和解決完全不可回滾的問(wèn)題。
 
缺點(diǎn):
- 設(shè)計(jì)成本:要做到兼容未來(lái)的變化。這聽(tīng)起來(lái)就很難。一開(kāi)始很難獲知所有用例、極端案例和業(yè)務(wù)理解?;仡欉^(guò)去并說(shuō)這是一個(gè)錯(cuò)誤的決定很容易,今天做出明天不會(huì)后悔的決定要困難得多。
 - 為了同時(shí)兼容兩種數(shù)據(jù)格式,需要在代碼中增加額外的處理邏輯,增加復(fù)雜度和投入的成本。
 
實(shí)施要點(diǎn):
- select語(yǔ)句只獲取需要的字段,避免使用select * from語(yǔ)句,有效防止新增字段對(duì)應(yīng)用邏輯的影響,還能減少對(duì)性能的影響。
 - 對(duì)數(shù)據(jù)庫(kù)表結(jié)構(gòu)變更通過(guò)新增字段實(shí)現(xiàn)。
 - 盡量新增接口,避免對(duì)現(xiàn)有接口做修改,如需要修改現(xiàn)有接口,可嘗試在接口上增加版本標(biāo)識(shí)。
 
使用唯一性標(biāo)識(shí)給鏡像打標(biāo)簽
當(dāng)生成容器鏡像時(shí),應(yīng)當(dāng)使用唯一性標(biāo)識(shí)來(lái)給容器鏡像打標(biāo)簽,唯一標(biāo)識(shí)可以更好的標(biāo)記當(dāng)次生成的鏡像,避免出現(xiàn)多個(gè)同名標(biāo)簽但不同的版本鏡像被使用的情況。例如多次部署都使用了latest標(biāo)簽的鏡像,可能因?yàn)槔『途彺娌呗詫?dǎo)致不同節(jié)點(diǎn)使用了不同版本的鏡像,從而導(dǎo)致功能上的不一致,在這種情況下,并不能很方便地判斷出某個(gè)節(jié)點(diǎn)部署的是哪一個(gè)版本。
唯一標(biāo)識(shí)最好有一定的含義,不僅可以用來(lái)區(qū)分產(chǎn)物,還可以獲取到本次構(gòu)建的關(guān)鍵信息。比如git提交哈希等關(guān)聯(lián)性比較強(qiáng)的標(biāo)識(shí)。雖然時(shí)間戳也是一個(gè)唯一性比較強(qiáng)的標(biāo)識(shí),但是關(guān)聯(lián)性相對(duì)較差,如果長(zhǎng)度不足,也有一定的幾率產(chǎn)生碰撞??梢钥紤]使用組合型標(biāo)簽,比如使用時(shí)間戳,build號(hào),版本號(hào)等根據(jù)自己的需求來(lái)組合生成唯一標(biāo)識(shí),這樣的標(biāo)簽本身就包含了很豐富的信息。
不建議單純使用pipeline的build序號(hào)來(lái)作為鏡像的標(biāo)簽,如果需要更換CI工具或者重建pipeline時(shí),這個(gè)序號(hào)將會(huì)被重置而可能產(chǎn)生重復(fù),除非在構(gòu)建腳本中加入偏移量。而且不同的CI工具獲取這個(gè)序號(hào)的方法也有所不同,對(duì)于遷移并不友好。雖然它的可追溯性看起來(lái)較好,但是單純的Build序號(hào)和代碼之間并沒(méi)有直接的關(guān)聯(lián)。
如果不是需要對(duì)外公開(kāi)發(fā)布的鏡像,并不建議對(duì)同一鏡像打上多個(gè)不同標(biāo)簽。因?yàn)榻^大部分的情況下,我們只會(huì)選用其中一個(gè)標(biāo)簽在所有的地方使用,多個(gè)標(biāo)簽的實(shí)際意義并不會(huì)很大。
如果制品庫(kù)支持immutable特性,強(qiáng)烈建議開(kāi)啟這個(gè)功能,防止因?yàn)橐馔馇闆r導(dǎo)致對(duì)已上傳的鏡像的覆蓋。
優(yōu)點(diǎn):
- 可以準(zhǔn)確對(duì)應(yīng)的到源代碼具體版本,在溯源時(shí)可以對(duì)應(yīng)到特定的提交而不是可能存在的多個(gè)提交。
 - 不需要使用SHA256等額外的信息來(lái)區(qū)分同一標(biāo)簽的不同版本。
 
缺點(diǎn):
- 一些類型的唯一性標(biāo)識(shí)可讀性不是很高,比如git提交哈希。
 - 一些類型的標(biāo)識(shí)受時(shí)間影響,不能使用同一命令獲得一致結(jié)果,需要使用其他的方式來(lái)傳遞給后續(xù)階段,比如時(shí)間戳。
 - 制品庫(kù)immutable功能開(kāi)啟之后,重跑已完成構(gòu)建鏡像的pipeline會(huì)發(fā)生上傳鏡像失敗的錯(cuò)誤,有可能會(huì)導(dǎo)致后續(xù)任務(wù)不能繼續(xù)。
 
實(shí)施示例:

在所有環(huán)境中使用同一個(gè)構(gòu)建產(chǎn)物
應(yīng)該在不同環(huán)境中使用相同的構(gòu)建產(chǎn)物來(lái)部署,避免對(duì)不同的環(huán)境生成不同的構(gòu)建產(chǎn)物,以確保環(huán)境的一致性,同時(shí)也保證部署在不同環(huán)境中的業(yè)務(wù)代碼是測(cè)試和驗(yàn)證通過(guò)的。比如某次的構(gòu)建產(chǎn)物,在測(cè)試環(huán)境部署后經(jīng)由測(cè)試人員和相關(guān)的自動(dòng)化測(cè)試工具完成相關(guān)的測(cè)試驗(yàn)證,如果沒(méi)有問(wèn)題才會(huì)繼續(xù)部署到后續(xù)環(huán)境中,應(yīng)繼續(xù)使用該產(chǎn)物部署后面的環(huán)境,不建議重新構(gòu)建新的產(chǎn)物來(lái)做后續(xù)環(huán)境的部署,也不建議覆蓋之前的構(gòu)建產(chǎn)物標(biāo)識(shí)。因?yàn)樵诂F(xiàn)有流行的語(yǔ)言和框架中,普遍存在大量的第三方依賴,即便是同一份源代碼,由于其依賴以及構(gòu)建環(huán)境的不同,會(huì)有一定幾率出現(xiàn)由于外部依賴的更新導(dǎo)致構(gòu)建產(chǎn)物存在差異,從而產(chǎn)生非預(yù)期的情況出現(xiàn)。
優(yōu)點(diǎn):
- 確保所有的環(huán)境部署的構(gòu)建產(chǎn)物是一樣的,盡可能的保證環(huán)境的一致性。
 - 確保部署到生產(chǎn)環(huán)境的產(chǎn)物是測(cè)試驗(yàn)證之后并無(wú)變化的,避免出現(xiàn)非預(yù)期的差異。
 
缺點(diǎn):
- 對(duì)于如前端這類純靜態(tài)資源的應(yīng)用,由于不同的環(huán)境需要連接不同的后端服務(wù)地址,因此無(wú)法直接使用唯一的構(gòu)建產(chǎn)物??梢钥紤]在業(yè)務(wù)啟動(dòng)階段,用一些額外的啟動(dòng)腳本或命令配合傳入環(huán)境變量或參數(shù)來(lái)修改配置文件,從而達(dá)到所有環(huán)境使用同一個(gè)構(gòu)建產(chǎn)物的目的。
 
下面例子展示了在使用nginx的容器鏡像里,通過(guò)在CMD指令里面先執(zhí)行一段腳本來(lái)對(duì)配置進(jìn)行修改,來(lái)達(dá)到在容器運(yùn)行時(shí)根據(jù)傳入的環(huán)境變量WEB_ENV的值來(lái)訪問(wèn)對(duì)應(yīng)環(huán)境的后端服務(wù)的目的。

- 對(duì)于移動(dòng)端app,也存在與前端應(yīng)用類似的問(wèn)題,需要開(kāi)發(fā)人員做額外設(shè)計(jì)和開(kāi)發(fā),在app啟動(dòng)時(shí)判斷需要進(jìn)入什么樣的運(yùn)行模式。
 
實(shí)施要點(diǎn):
- 在設(shè)計(jì)CICD流水線時(shí),將構(gòu)建產(chǎn)物同步到制品庫(kù)時(shí),給該產(chǎn)物打上唯一標(biāo)識(shí)。
 - 如制品庫(kù)支持,開(kāi)啟制品庫(kù)的immutable特性。
 - 將該唯一標(biāo)識(shí)傳遞到在后面所有的部署流水線任務(wù)中,所有的部署任務(wù)都使用該唯一標(biāo)識(shí)所指向的構(gòu)建產(chǎn)物。
 - 如果需要在多個(gè)制品庫(kù)保存同一個(gè)構(gòu)建產(chǎn)物,建議在上傳成功之后對(duì)構(gòu)建產(chǎn)物做完整性檢查。
 
減少腳本/工具對(duì)環(huán)境的依賴
一般情況下,腳本都會(huì)或多或少的使用到一些外部工具。而我們的腳本很有可能會(huì)運(yùn)行在不同的環(huán)境中,不同環(huán)境中提供的工具也會(huì)有版本和用法的差異。如果需要在環(huán)境中維護(hù)某一工具的多個(gè)版本的,工具本身的版本管理,以及多個(gè)工具之間的依賴沖突和升級(jí)更新也會(huì)產(chǎn)生較高的管理和維護(hù)成本。
我們建議盡可能的減少所使用的工具對(duì)環(huán)境的依賴,尤其是系統(tǒng)不會(huì)默認(rèn)安裝的工具。另外在編寫腳本的時(shí)候,也盡量避免使用只有某些版本特有的語(yǔ)法特性。這些情況都會(huì)導(dǎo)致腳本有可能出現(xiàn)一些不可預(yù)期的結(jié)果。我們建議使用容器化工具或者容器化環(huán)境管理工具如Batect來(lái)替代對(duì)應(yīng)的需求。
優(yōu)點(diǎn):
- CI/CD agent中只需要安裝容器運(yùn)行時(shí)即可,可以減小agent的體積。
 - 容器化的工具因?yàn)閷?duì)環(huán)境的依賴非常低,所以不論是工具升級(jí)還是降級(jí)都非常簡(jiǎn)單,同時(shí)也解耦了對(duì)agent特性的依賴,提高agent利用率。
 - 最大化的保證環(huán)境一致性,使用容器化的工具消除了環(huán)境差異可能導(dǎo)致的非預(yù)期異常。
 - 新人友好,新加入的團(tuán)隊(duì)成員可以快速的配置好可運(yùn)行的環(huán)境,無(wú)需過(guò)多的考慮具體工具的安裝,配置等。
 
解耦對(duì)CI/CD工具的依賴,雖然在實(shí)際項(xiàng)目中很少會(huì)有更換CI/CD工具的情況,但是如果需要遷移,我們也只需在新的工具環(huán)境中構(gòu)建出容器運(yùn)行環(huán)境即可,大大減少了切換工具工作量,提高遷移的速度。
缺點(diǎn):
- 因?yàn)闆](méi)有預(yù)裝構(gòu)建所需要的各種軟件,如果本地沒(méi)有鏡像緩存,在運(yùn)行容器化的工具時(shí)都需要去容器倉(cāng)庫(kù)中獲取對(duì)應(yīng)的工具鏡像,會(huì)有額外的帶寬壓力。
 - 因?yàn)樾枰@取工具鏡像,容器啟動(dòng)也比二進(jìn)制的程序要慢,所以整個(gè)任務(wù)運(yùn)行過(guò)程需要的時(shí)間會(huì)更長(zhǎng)。
 - 理論上來(lái)講,容器化技術(shù)性能損耗很小,工具的性能和二進(jìn)制程序的差別不會(huì)很大,但是在實(shí)際的使用中,我們發(fā)現(xiàn)因?yàn)槿萜饕媾渲貌划?dāng)?shù)仍驎?huì)導(dǎo)致一些工具性能變差甚至無(wú)響應(yīng)的情況出現(xiàn)。
 
實(shí)施示例:
在使用 terraform 時(shí),不同版本之間的 terraform 并不兼容,那么如何保證所有人與 CI 都使用相同的 terraform 版本就是一個(gè)非常麻煩的事情。那么如果我們無(wú)論在 CI 還是本地都基于 docker 去運(yùn)行 terraform 就可以解決這個(gè)問(wèn)題。

使用auto/ACTION模式來(lái)維護(hù)管理腳本
auto/ACTION是我們?cè)陧?xiàng)目實(shí)踐中總結(jié)并希望可以廣泛推廣的一個(gè)經(jīng)驗(yàn)總結(jié),在和客戶合作過(guò)程中,尤其是有很多團(tuán)隊(duì)的大型項(xiàng)目上,我們從這個(gè)模式中受益匪淺。auto/ACTION模式的核心是使用統(tǒng)一語(yǔ)義能表明腳本目的的ACTION來(lái)命名管理腳本,如應(yīng)用的測(cè)試(test),驗(yàn)證(validate),打包(build),發(fā)布(deploy)等相關(guān)任務(wù),統(tǒng)一把這些管理腳本歸放在auto目錄下來(lái)維護(hù)。
因?yàn)轭恥nix系統(tǒng)在運(yùn)行的時(shí)候并不真正使用文件后綴來(lái)識(shí)別文件的類型,我們建議腳本名字不要加后綴。這個(gè)建議是基于管理腳本有可能會(huì)在多個(gè)地方被使用,而不同的開(kāi)發(fā)和維護(hù)人員對(duì)于語(yǔ)言的偏好不同,如果在需要使用另外一種語(yǔ)言重寫腳本的時(shí)候,使用這個(gè)腳本的地方就不需要做更新,消除了因?yàn)槲募兓赡軐?dǎo)致的自動(dòng)化任務(wù)的錯(cuò)誤和中斷。雖然沒(méi)有后綴可能會(huì)帶來(lái)一些不便,比如編輯器的語(yǔ)言類型識(shí)別錯(cuò)誤等,但是相對(duì)于它帶來(lái)的優(yōu)點(diǎn),還是非常值得的。
優(yōu)點(diǎn):
- 管理代碼和業(yè)務(wù)代碼放在同一代碼庫(kù),使用版本控制,便于進(jìn)行更新,回退。
 - 每個(gè)腳本只做一件事,職責(zé)單一,同時(shí)便于理解和管理。
 - 可以方便的知道所有可用的腳本。如:
 

- 如果跨團(tuán)隊(duì)合作,或者團(tuán)隊(duì)成員有輪換的時(shí)候,可以更快速的掌握業(yè)務(wù)管理的上下文。
 
- 每個(gè)項(xiàng)目都有一套自己的auto腳本,如果有基礎(chǔ)性變化,改動(dòng)成本較高,可以考慮使用git submodule等模式來(lái)管理。
 - 沒(méi)有后綴的文件名會(huì)帶來(lái)一些管理上的不便。
 
實(shí)施要點(diǎn):
- 腳本滿足既可在本地執(zhí)行,又能在CI流水線上執(zhí)行,便于驗(yàn)證。
 - 腳本中的變量?jī)?nèi)容盡可能從環(huán)境變量中讀取,避免向腳本中傳入?yún)?shù),方便運(yùn)行。
 - 專屬于CI/CD平臺(tái)的腳本不要放在auto根目錄下,建議創(chuàng)建一個(gè)對(duì)應(yīng)的子目錄,例如 .buildkite, .github, .travis來(lái)做管理。
 - 可根據(jù)團(tuán)隊(duì)的需求適當(dāng)?shù)臄U(kuò)展腳本的名字使之更容易理解,建議使用-而非_ 來(lái)分隔單詞, 如auto/upload-image-to-ecr。
 
管理腳本和業(yè)務(wù)腳本分離
我們的應(yīng)用中一般都會(huì)有一些腳本來(lái)做一些輔助性的工作。這些腳本通常會(huì)和業(yè)務(wù)代碼放在同一個(gè)代碼倉(cāng)庫(kù),使用版本控制來(lái)進(jìn)行管理。這些腳本大致分為兩種:管理腳本和業(yè)務(wù)腳本。
管理腳本是用來(lái)做應(yīng)用打包,部署等管理相關(guān)工作工作,這種類型的腳本是無(wú)需打包進(jìn)業(yè)務(wù)運(yùn)行所需的產(chǎn)出物中的;業(yè)務(wù)腳本是輔助業(yè)務(wù)運(yùn)行,比如說(shuō)初始化環(huán)境和配置,結(jié)束時(shí)的清理工作等,這些腳本需要打包到業(yè)務(wù)運(yùn)行的產(chǎn)出物中。
我們建議除了一些有特殊要求的腳本外,不要把腳本放在根目錄。并且把這兩種不同類型的腳本存放在不同的目錄中。
優(yōu)點(diǎn):
- 在封裝鏡像時(shí),業(yè)務(wù)腳本和業(yè)務(wù)代碼同等重要,需要封裝在鏡像中。將管理腳本和業(yè)務(wù)腳本分離可以減少鏡像中的文件數(shù)量。
 - 在軟件開(kāi)發(fā)過(guò)程中,針對(duì)業(yè)務(wù)運(yùn)行和自動(dòng)化管理關(guān)注信息不一樣,將管理腳本和業(yè)務(wù)腳本分離,讓團(tuán)隊(duì)成員更加清楚腳本的類型和目的。
 
缺點(diǎn):
- 將管理腳本和業(yè)務(wù)腳本分離,會(huì)增加倉(cāng)庫(kù)的層次結(jié)構(gòu)。
 
實(shí)施要點(diǎn):
- 推薦將管理腳本放置在auto目錄,將業(yè)務(wù)腳本放置在scripts目錄。
 - 腳本中的變量采用從環(huán)境變量中讀取,避免向腳本中傳入?yún)?shù),方便運(yùn)行。
 - 推薦腳本名稱即表明腳本的作用,不建議使用auto/script這樣不表意的腳本命名。
 - 不在文件名中使用文件類型后綴。
 
及時(shí)更新容器的基礎(chǔ)鏡像
基礎(chǔ)鏡像是業(yè)務(wù)鏡像的地基,其包含了我們業(yè)務(wù)和應(yīng)用所必需的基礎(chǔ)庫(kù)、二進(jìn)制文件和配置文件等。一個(gè)良好維護(hù)的基礎(chǔ)鏡像通常會(huì)根據(jù)需要做更新,這些更新通常包含安全補(bǔ)丁,新功能或?qū)Σ僮飨到y(tǒng)或框架的改進(jìn)等,我們建議及時(shí)的更新容器的基礎(chǔ)鏡像來(lái)保障業(yè)務(wù)的安全性。除非有特定原因需要繼續(xù)使用舊版本鏡像,否則應(yīng)及時(shí)跟進(jìn)使用經(jīng)過(guò)充分評(píng)估和測(cè)試的最新版本鏡像。
在Dockerfile和compose等文件中,可以通過(guò)指定鏡像中的標(biāo)識(shí)和sha256值組合來(lái)指定基礎(chǔ)鏡像的版本。當(dāng)鏡像有了更新之后,及時(shí)沿用了如latest或大版本號(hào)這類通用性比較高的標(biāo)簽時(shí),其sha256的值也會(huì)發(fā)生變化,通過(guò)更新這個(gè)組合可以更新使用最新版本的基礎(chǔ)鏡像。
優(yōu)點(diǎn):
- 最新的鏡像通常帶有可以增強(qiáng)應(yīng)用程序安全性的補(bǔ)丁修復(fù),降低安全風(fēng)險(xiǎn)。
 - 最新的鏡像通常包括可以提高應(yīng)用程序性能的新功能或改進(jìn)功能。
 
缺點(diǎn):
- 新功能可能存在不可預(yù)期的bug。
 - 新的功能有非常小的概率存在未知的安全漏洞,如果有特殊的安全需求,請(qǐng)?jiān)诎踩块T的指導(dǎo)下升級(jí)。
 
實(shí)施示例:
- 可以使用dfresh或者類似的工具來(lái)檢查和更新基礎(chǔ)鏡像。
 - 檢查基礎(chǔ)鏡像是否有更新
 

- 更新基礎(chǔ)鏡像
 

- 回退方法 在需要回退基礎(chǔ)鏡像版本時(shí),可從代碼庫(kù)的提交找到上一個(gè)可用版本的相應(yīng)信息。
 
定期檢查和升級(jí)依賴包
隨著 Bug 修復(fù)、新功能的開(kāi)發(fā)或者其他更新,我們應(yīng)用的依賴包可能會(huì)過(guò)時(shí)。此時(shí)應(yīng)用的依賴項(xiàng)越多,就越難跟上這些更新。過(guò)時(shí)的依賴包可能對(duì)安全構(gòu)成威脅,并對(duì)性能產(chǎn)生負(fù)面影響。最新的軟件包可防止漏洞,這意味著定期的依賴性檢查和更新很重要。我們建議定期的對(duì)應(yīng)用的依賴包做更新和安全檢查,并升級(jí)到一個(gè)合適的版本。并且我們建議在應(yīng)用的 pipeline 中加入這些檢查任務(wù),并在常規(guī)的開(kāi)發(fā)過(guò)程中及時(shí)發(fā)現(xiàn)和升級(jí)。如果應(yīng)用已經(jīng)處于維護(hù)階段,我們也建議定期執(zhí)行這些檢查并在需要的時(shí)候加以升級(jí)。
優(yōu)點(diǎn):
- 定期升級(jí)依賴可以讓應(yīng)用的安全性和代碼的可用性都有保障。
 - 定期升級(jí)依賴會(huì)讓解決依賴版本沖突和代碼兼容性變得容易。
 - 更新依賴項(xiàng)可以獲得新的依賴項(xiàng)版本提供的所有性能改進(jìn)。這些改進(jìn)可以有多種形式,例如修復(fù)以前的性能問(wèn)題、改進(jìn)了實(shí)現(xiàn)和算法等。
 - 升級(jí)依賴項(xiàng)不僅可以改進(jìn)現(xiàn)有功能,還可以使用到以前不存在的新功能。這些新功能最終可能讓我們更好的實(shí)現(xiàn)自己應(yīng)用的新功能。
 
缺點(diǎn):
- 如果不及時(shí)更新依賴,將會(huì)使得產(chǎn)品難以維護(hù),并可能導(dǎo)致開(kāi)發(fā)人員的時(shí)間被常規(guī)的、無(wú)意義的工作占用。
 - 如果長(zhǎng)期不更新依賴,會(huì)使應(yīng)用面臨無(wú)人問(wèn)津的風(fēng)險(xiǎn),之后在某一天需要進(jìn)行改動(dòng)的時(shí)候,面臨大量的依賴包過(guò)期無(wú)法獲取和版本升級(jí)造成的接口變化。這時(shí)就需要投入非常高的成本來(lái)讓代碼重新變得可用,甚至完全無(wú)法更新而變成遺留系統(tǒng)。
 - 當(dāng)進(jìn)行大的版本升級(jí)時(shí),需要對(duì)應(yīng)用程序進(jìn)行更多的更改才能與較新的庫(kù)兼容。這使得付出代價(jià)比及時(shí)更新依賴大得多。
 - 如果忽略升級(jí)依賴項(xiàng),那么會(huì)面臨無(wú)法在自己喜歡的平臺(tái)上運(yùn)行軟件的可能。例如,如果停止升級(jí)軟件中的數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序,那么將無(wú)法使用舊版本的數(shù)據(jù)庫(kù)系統(tǒng)。這不僅會(huì)使應(yīng)用變得過(guò)時(shí)且易受攻擊,而且甚至可能無(wú)法從該數(shù)據(jù)庫(kù)系統(tǒng)提供商處獲得任何支持。
 - 如果應(yīng)用依賴于過(guò)時(shí)的依賴項(xiàng)而導(dǎo)致升級(jí)困難變得很難維護(hù),會(huì)使得項(xiàng)目很難找到對(duì)這些舊技術(shù)有經(jīng)驗(yàn)的人,甚至失去現(xiàn)有的維護(hù)者。
 
實(shí)施示例:
(1) 手動(dòng)檢查
JS 篇:
- npm-outdated & npm-update
 
- npm outdated:可以使用 npm outdated 獲取當(dāng)前需要升級(jí)的包的信息。
 - npm update: 會(huì)把所有的包升級(jí)到我們定義的需要的版本號(hào)。如果需要升級(jí)到最新的則需要使用@latest eg: npm update cypress@latest。
 
- npm-check-updates: 是一種更高級(jí)的檢查工具
 首先需要全局安裝 npm-check-updates: npm install -g npm-check-updates ;
ncu: 檢查需要升級(jí)的包信息,這里類似 npm outdated;
ncu --upgrade/ncu -u: 將所有的包升級(jí)到最新版本,即便是包含重大更改,也會(huì)進(jìn)行更新。注意:更新完成后不會(huì)自動(dòng)運(yùn)行 npm install,所以還需要再手動(dòng)執(zhí)行來(lái)更新 package-lock.json。
ncu --interactive/ncu -i : interactive mode 安裝某個(gè)包。
小結(jié):npm-outdated 和 npm-check-updates都可以用來(lái)做 JS項(xiàng)目的包檢查、升級(jí)。
Java 篇:
- 在 build.gradle中配置 owasp.dependency-check
 - 執(zhí)行./gradlew dependencyCheckAnalyze
 
查看報(bào)告:項(xiàng)目根目錄>build>reports>dependency-check-report.html
(2) CI Pipeline 集成
- npm-check-updates 與 Buildkite Pipeline 的集成由于 buildkite 沒(méi)有官方插件支持 dependency-check。所以對(duì)于buildkite 推薦兩種方式:
 
- 自己開(kāi)發(fā)對(duì)應(yīng)功能的插件,然后集成到 pipeline 的 step 中;
 - 通過(guò) docker-compose 的方式去運(yùn)行對(duì)應(yīng)的檢查,將其在 pipeline 的 step 中去運(yùn)行(如果需要可以添加 block 來(lái)強(qiáng)制檢查 npm-check-updates 的結(jié)果)。

 
- jenkins pipeline 的集成:需要安裝 dependency-check Plugin。步驟如下:
 
- 在 Jenkins Global Tool Configuration 安裝 dependency-check;
 - 在 Jenkins builder 配置已經(jīng)安裝好的 dependency-check;
 - 在 Jenkins Publish 里配置讀取 dependency-check 的 report ,通過(guò)對(duì)相關(guān)指標(biāo)進(jìn)行讀取,設(shè)置閾值,配置構(gòu)建失敗或者警告等設(shè)置。
 
如果項(xiàng)目 code 托管在 Github,我們可以使用 Dependabot 和 Renovate 工具和 Github 集成來(lái)做依賴檢查。這兩個(gè)工具都會(huì)做定期掃描,創(chuàng)建依賴版本升級(jí)的 PR。
配置 Dependabot 進(jìn)行版本更新:
- 在 GitHub 的代碼倉(cāng)庫(kù)的主頁(yè),找到代碼倉(cāng)庫(kù)名稱下的 setting;
 - 在邊欄的安全性部分中,單擊代碼安全性和分析;
 - 在代碼安全和分析下,在Dependabot version updates右側(cè),單擊啟用以打開(kāi)存儲(chǔ)庫(kù) .github 目錄中的基本 dependabot.yml 配置文件;
 - 添加version;
 - 添加 updates 部分,并輸入希望 Dependabot 監(jiān)視的每個(gè)包管理器的條目;
 - 對(duì)于每個(gè)包管理器,可使用:
 
- package-ecosystem 指定包管理器。
 - directory 指定清單或其他定義文件的位置。
 - schedule.interval 指定檢查新版本的頻率。
 
- 在代碼倉(cāng)庫(kù)的根目錄創(chuàng)建.github目錄;
 - 創(chuàng)建 dependabot.yml文件并且存儲(chǔ)到.github目錄下。
 
示例 dependabot.yml:

配置 Renovate:
- 在 Github 的 App 里面安裝 Renovate app https://github.com/apps/renovate;
 - 安裝并配置完成后可以在PR中看到一個(gè)自動(dòng)生成的PR Configure Renovate,這個(gè)PR中包含一個(gè) renovate.json 文件,這個(gè)文件中包含了 renovate 的一些默認(rèn)設(shè)定;
 - 可以根據(jù)文檔 (https://docs.renovatebot.com/configuration-options/) 添加或者修改適合自身項(xiàng)目的具體配置項(xiàng);
 - merge 此 PR;
 - Renovate 會(huì)根據(jù)你配置的 schedule 時(shí)間去自動(dòng)的掃描并生成包升級(jí) PR 提醒
 
定期的重新部署維護(hù)階段的應(yīng)用
在應(yīng)用處于維護(hù)階段,如果業(yè)務(wù)不再會(huì)增加新的功能,抑或因?yàn)槟承┰驘o(wú)法做定期的應(yīng)用依賴升級(jí),我們也建議你定期的重新部署這個(gè)應(yīng)用,以應(yīng)對(duì)平臺(tái)等更底層的變化帶來(lái)的部署失敗的風(fēng)險(xiǎn)。定期部署可以確保你的應(yīng)用在新的平臺(tái)環(huán)境中也可以正常的部署,如果在周期性的部署過(guò)程中發(fā)現(xiàn)應(yīng)用無(wú)法在新的環(huán)境部署,你也會(huì)有一個(gè)緩沖期來(lái)制訂應(yīng)對(duì)策略,而不是在平臺(tái)完成升級(jí)之后的某一天,應(yīng)用發(fā)生了問(wèn)題才發(fā)現(xiàn)已經(jīng)無(wú)法部署。
優(yōu)點(diǎn):
- 定期部署應(yīng)用是對(duì)部署工具和流程的有效驗(yàn)證,CI/CD Agent的一些升級(jí)有可能會(huì)導(dǎo)致我們?cè)诓渴鹆鞒讨惺褂霉ぞ甙l(fā)生兼容性問(wèn)題,定期部署可以及早的發(fā)現(xiàn)這些問(wèn)題。
 - 定期部署應(yīng)用也能夠有效縮短我們的依賴獲取未驗(yàn)證的窗口期。雖然我們的應(yīng)用依賴可以鎖定版本,也可以將依賴保存到私有倉(cāng)庫(kù),但長(zhǎng)時(shí)間沒(méi)有運(yùn)行相關(guān)部署流程,我們無(wú)法保證應(yīng)用的依賴能夠在需要的時(shí)候可正常獲取且可用。
 - 現(xiàn)在的應(yīng)用的基礎(chǔ)設(shè)施很多都基于各類云平臺(tái),服務(wù)提供商會(huì)定期的對(duì)自己的基礎(chǔ)設(shè)施做升級(jí)和換代,定期部署應(yīng)用可以讓我們及早的獲知基礎(chǔ)設(shè)施變化帶來(lái)的兼容性風(fēng)險(xiǎn)。
 
缺點(diǎn):
- 定期部署會(huì)對(duì)系統(tǒng)的穩(wěn)定運(yùn)行造成一些影響,變化本身就會(huì)帶來(lái)一定的未知風(fēng)險(xiǎn)。
 - 自動(dòng)化的發(fā)布一般情況下都需要配有完善的回歸測(cè)試流程來(lái)確保業(yè)務(wù)的可用性,會(huì)帶來(lái)成本的增加
 















 
 
 












 
 
 
 