大型組織應(yīng)用GitOps難免會(huì)遇到在多環(huán)境中部署的問(wèn)題,本文分析了應(yīng)用環(huán)境分支策略會(huì)遇到到問(wèn)題,介紹了應(yīng)用文件夾策略解決這些問(wèn)題的方案。原文:Stop Using Branches for Deploying to Different GitOps Environments[1], How to Model Your Gitops Environments and Promote Releases between Them[2]

在關(guān)于GitOps問(wèn)題的指南中,我們簡(jiǎn)要解釋了(參見(jiàn)第3和第4點(diǎn))當(dāng)前GitOps工具在支持不同環(huán)境部署以及多集群配置建模時(shí)的問(wèn)題。

“如何將發(fā)布部署到下一個(gè)環(huán)境?”的問(wèn)題在希望采用GitOps的組織中越來(lái)越受到重視[4],并且有幾種可能的答案。但在這篇文章中,我們將重點(diǎn)討論在這一過(guò)程中不應(yīng)該做什么。
我們不應(yīng)該使用Git分支來(lái)建模不同的環(huán)境。如果保存配置的Git存儲(chǔ)庫(kù)(在Kubernetes的例子中是manifests/templates)有名為“預(yù)發(fā)”、“QA”、“生產(chǎn)”等分支,那就掉進(jìn)了陷阱。

重要的事情說(shuō)三遍:
使用Git分支來(lái)建模不同的環(huán)境是一種反模式,不要這樣做!
使用Git分支來(lái)建模不同的環(huán)境是一種反模式,不要這樣做!
使用Git分支來(lái)建模不同的環(huán)境是一種反模式,不要這樣做!
我們將從以下幾點(diǎn)探討為什么這個(gè)實(shí)踐是反模式:
- 在部署環(huán)境中使用不同的Git分支是過(guò)去的遺留問(wèn)題。
- 不同分支之間的pull request和合并是有問(wèn)題的。
- 人們傾向于包含特定于環(huán)境的代碼并創(chuàng)建不同的配置。
- 一旦環(huán)境數(shù)量增多,環(huán)境的維護(hù)就會(huì)變得難以控制。
- 每個(gè)環(huán)境的分支模型違背了現(xiàn)有的Kubernetes生態(tài)系統(tǒng)。
在不同環(huán)境中采用分支應(yīng)該只應(yīng)用于遺留應(yīng)用程序。
當(dāng)問(wèn)到為什么選擇Git分支來(lái)建模不同的環(huán)境時(shí),回答幾乎總是“我們一直都是這樣做的”,“感覺(jué)很自然”,“這是開(kāi)發(fā)人員知道的”等等。
這沒(méi)有錯(cuò),大多數(shù)人都熟悉在不同環(huán)境中使用分支。這一實(shí)踐是由古老的Git-Flow模型[3]大力推廣的。但自從引入這種模式以來(lái),情況發(fā)生了很大的變化,甚至最初的作者也從宏觀角度發(fā)出了嚴(yán)重警告,建議人們不要在不了解后果的情況下采用這種模式。
事實(shí)上,Git-flow模型……
- 專注于應(yīng)用程序源代碼,而不是環(huán)境配置(更不用說(shuō)Kubernetes manifest了)。
- 如果需要在生產(chǎn)環(huán)境中支持多個(gè)應(yīng)用版本,這一模型很合適,通常沒(méi)有這種場(chǎng)景,但也時(shí)有發(fā)生。
因?yàn)楸疚氖顷P(guān)于GitOps環(huán)境而不是應(yīng)用程序源代碼的,因此不打算在這里過(guò)多討論Git-flow及其缺點(diǎn),總而言之,如果需要為不同的環(huán)境支持不同的特性,那么應(yīng)該遵循基于主干的開(kāi)發(fā)[5]并使用特性標(biāo)志[6]。
在GitOps上下文中,應(yīng)用程序源代碼和配置也應(yīng)該在不同的Git存儲(chǔ)庫(kù)中(一個(gè)存儲(chǔ)庫(kù)只有應(yīng)用程序代碼,一個(gè)存儲(chǔ)庫(kù)有Kubernetes manifests/templates)。這意味著應(yīng)用程序源代碼分支不應(yīng)該影響環(huán)境存儲(chǔ)庫(kù)中的分支。

當(dāng)我們?cè)陧?xiàng)目中采用GitOps時(shí),應(yīng)用程序開(kāi)發(fā)人員可以為源代碼選擇想要的任何分支策略(甚至使用Git-flow),但是環(huán)境配置Git存儲(chǔ)庫(kù)(包含所有Kubernetes manifests/templates)不應(yīng)該遵循每個(gè)環(huán)境一個(gè)分支的模型。
部署升級(jí)絕不是簡(jiǎn)單的Git合并
既然我們已經(jīng)了解了在部署中使用按環(huán)境區(qū)分分支的方法的歷史,就可以討論其缺點(diǎn)了。
這種方法的主要優(yōu)點(diǎn)是“部署升級(jí)是一個(gè)簡(jiǎn)單的git合并”。理論上,如果想要將一個(gè)版本從QA環(huán)境升級(jí)部署到預(yù)發(fā)環(huán)境,只需將QA分支合并到預(yù)發(fā)分支即可。當(dāng)我們準(zhǔn)備好生產(chǎn)環(huán)境時(shí),再次將預(yù)發(fā)分支合并到生產(chǎn)分支,就可以確定來(lái)自預(yù)發(fā)的所有變更已經(jīng)部署到了生產(chǎn)環(huán)境中。
想知道生產(chǎn)環(huán)境和預(yù)發(fā)環(huán)境之間有什么不同嗎?只需要在兩個(gè)分支之間做一個(gè)標(biāo)準(zhǔn)的git diff[7]就可以了。想要將配置變更從預(yù)發(fā)環(huán)境反向移植到QA環(huán)境?從預(yù)發(fā)分支到QA分支的一個(gè)簡(jiǎn)單的Git合并就可以做到這一點(diǎn)。
如果想對(duì)部署升級(jí)施加額外的限制,可以使用Pull Requests。一方面任何人都可以觸發(fā)從QA到預(yù)發(fā)的合并,另一方面如果想在生產(chǎn)分支中合入一些東西,可以觸發(fā)Pull Request并要求所有利益相關(guān)者手動(dòng)批準(zhǔn)。
這在理論上聽(tīng)起來(lái)很棒,一些瑣碎的場(chǎng)景實(shí)際上可以像這樣工作。但在實(shí)踐中,情況并非如此。通過(guò)Git合并來(lái)升級(jí)一個(gè)版本可能會(huì)遇到合并沖突、引入不想要的變更,甚至觸發(fā)錯(cuò)誤的變更順序。
下面我們以Kubernetes部署為例看一個(gè)簡(jiǎn)單的例子,當(dāng)前部署位于預(yù)發(fā)分支中:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
spec:
replicas: 15
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: backend
image: my-app:2.2
ports:
- containerPort: 80
QA團(tuán)隊(duì)已經(jīng)通知我們說(shuō)版本2.3(位于QA分支中)看起來(lái)已經(jīng)準(zhǔn)備好了,可以轉(zhuǎn)移到交付階段。我們將QA分支合并到預(yù)發(fā)分支,部署應(yīng)用程序,并認(rèn)為一切都很好。
但我們不知道,由于某些資源限制,有人將QA分支中的副本數(shù)量更改為2。使用Git合并,不僅將2.3部署到了預(yù)發(fā)環(huán)境,而且還將副本改成了2個(gè)(而不是15個(gè)),這可能并不是我們想要的。
你可能會(huì)說(shuō),在合并之前查看副本個(gè)數(shù)很容易,但請(qǐng)記住,在實(shí)際場(chǎng)景中,有大量的應(yīng)用程序,其中有大量的manifests被模板化(通過(guò)Helm或Kustomize)。因此,理解想要帶來(lái)什么變化,留下什么變化并不是一件小事情。
即使我們確實(shí)發(fā)現(xiàn)了不應(yīng)該被合并的變更,也需要使用git cherry-pick[8]或其他非標(biāo)準(zhǔn)方法手動(dòng)選擇“好的”部分,這與最初的“簡(jiǎn)單的”git合并相去甚遠(yuǎn)。
但是,即使我們知道了所有可以合并的變更,也會(huì)出現(xiàn)合并的順序與提交的順序不同的情況。例如,QA環(huán)境上有以下4個(gè)更改。
- 更新了應(yīng)用ingress[9]的主機(jī)名。
- 版本2.5被部署到QA環(huán)境,所有QA人員開(kāi)始測(cè)試。
- 在2.5版本中發(fā)現(xiàn)了一個(gè)問(wèn)題,并修復(fù)了Kubernetes的configmap。
- 資源限制[10]進(jìn)行了微調(diào),并提交到QA分支。
然后我們決定ingress設(shè)置和資源限制應(yīng)該部署到下一個(gè)環(huán)境(預(yù)發(fā)),但是QA團(tuán)隊(duì)還沒(méi)有完成2.5版本的測(cè)試。
如果我們盲目的將QA分支合并到預(yù)發(fā)分支,就將同時(shí)合并所有4個(gè)變更,包括2.5的升級(jí)。
為了解決這個(gè)問(wèn)題,需要再次使用git cherry-pick或其他手動(dòng)方法。
在更復(fù)雜的情況下,提交之間存在依賴關(guān)系,因此即使是cherry-pick也幫不上忙。

在上面的示例中,版本1.24必須部署到生產(chǎn)環(huán)境。問(wèn)題是其中一個(gè)提交(hotfix)包含了大量的變更,而其中某些變更又依賴于另一個(gè)提交(ingress配置變更),而后者本身無(wú)法部署到生產(chǎn)環(huán)境(因?yàn)橹贿m用于預(yù)發(fā)環(huán)境)。因此,即使是精心挑選,也不可能只將所需的變更從準(zhǔn)備階段引入到生產(chǎn)階段。
最終的結(jié)果是,部署升級(jí)絕不是簡(jiǎn)單的Git合并。大多數(shù)組織還擁有大量應(yīng)用,這些應(yīng)用位于大量集群中,由大量manifests組成,手動(dòng)選擇變更將是一場(chǎng)失敗的戰(zhàn)斗。
特定于環(huán)境的變更更容易造成配置漂移
理論上,配置漂移不應(yīng)該成為Git合并的問(wèn)題。如果在預(yù)發(fā)環(huán)境中進(jìn)行了變更,然后將該分支合并到生產(chǎn)環(huán)境,那么所有變更都應(yīng)該遷移到新環(huán)境中。
然而在實(shí)踐中,事情是不一樣的,因?yàn)榇蠖鄶?shù)組織只向一個(gè)方向合并,團(tuán)隊(duì)成員很容易改變上游環(huán)境,而從不將這些改變遷移到下游環(huán)境。
在QA、預(yù)發(fā)和生產(chǎn)三個(gè)環(huán)境的經(jīng)典例子中,Git合并的方向只有一個(gè)。人們將QA分支合并到預(yù)發(fā),將預(yù)發(fā)分支合并到生產(chǎn),這意味著變化只會(huì)向上流動(dòng)。
QA -> 預(yù)發(fā)(Staging) -> 生產(chǎn)(Production).
典型場(chǎng)景是,在生產(chǎn)環(huán)境中需要對(duì)配置進(jìn)行快速變更(一個(gè)hotfix),然后有人部署了該修復(fù)程序。在Kubernetes的情況下,這個(gè)修補(bǔ)程序可以是任何東西,比如對(duì)現(xiàn)有manifest的更改,甚至是一個(gè)全新的manifest。
現(xiàn)在生產(chǎn)環(huán)境有了一個(gè)與預(yù)發(fā)完全不同的配置。下次一個(gè)版本從臨時(shí)版本升級(jí)到生產(chǎn)版本時(shí),Git只會(huì)通知我們將從臨時(shí)版本升級(jí)到生產(chǎn)版本。生產(chǎn)上的臨時(shí)變更永遠(yuǎn)不會(huì)出現(xiàn)在Pull Request中的任何地方。

因?yàn)楝F(xiàn)在生產(chǎn)中有一個(gè)沒(méi)有文檔化的變更,這意味著所有后續(xù)部署都可能失敗,而這個(gè)變更永遠(yuǎn)不會(huì)被任何后續(xù)升級(jí)檢測(cè)到。
理論上,我們可以反向遷移這些變更,并周期性的將所有提交從生產(chǎn)階段合并到交付階段(以及交付階段合并到QA階段)。實(shí)際上,由于前面提到的原因,這種情況從未發(fā)生過(guò)。
可以想象,如果有很多環(huán)境,就會(huì)進(jìn)一步放大這個(gè)問(wèn)題。
總而言之,通過(guò)Git合并來(lái)部署發(fā)布版本并不能解決配置漂移問(wèn)題,而且實(shí)際上團(tuán)隊(duì)會(huì)試圖做出一些不按順序合并的特殊變更,因此會(huì)使問(wèn)題更加嚴(yán)重。
在大量環(huán)境中管理不同的Git分支是一場(chǎng)注定失敗的戰(zhàn)斗
在前面的所有示例中,我只使用了3個(gè)環(huán)境(QA環(huán)境->預(yù)發(fā)環(huán)境->生產(chǎn)環(huán)境)來(lái)說(shuō)明基于分支的環(huán)境部署的缺點(diǎn)。
根據(jù)組織的大小,也許有更多的環(huán)境,如果考慮地理位置等其他因素,那么環(huán)境的數(shù)量就會(huì)迅速增加。
我們以某個(gè)公司為例,它有5個(gè)工作環(huán)境:
- 負(fù)載測(cè)試
- 集成測(cè)試
- QA
- 預(yù)發(fā)
- 生產(chǎn)
我們假設(shè)最后3個(gè)環(huán)境也部署在歐洲、美國(guó)和亞洲,而前2個(gè)環(huán)境也有GPU和非GPU變體,這意味著該公司共有13個(gè)環(huán)境,而這只是針對(duì)單個(gè)應(yīng)用的。
如果使用基于分支的方法:
- 在任何時(shí)候都需要有13個(gè)長(zhǎng)期Git分支。
- 需要13個(gè)pull requests才能跨所有環(huán)境部署一個(gè)變更。
- 有一個(gè)二維的部署升級(jí)矩陣,縱向5步,橫向2-3步。
- 錯(cuò)誤合并、配置漂移和特別變更的可能性在所有環(huán)境組合中都有可能出現(xiàn)。
在這個(gè)示例組織的上下文中,所有以前的問(wèn)題現(xiàn)在都更加普遍了。
branch-per-environment模型與Helm/Kustomize背道而馳
描述應(yīng)用程序的兩個(gè)最流行的Kubernetes工具是Helm和Kustomize,我們看看這兩種工具如何對(duì)不同環(huán)境進(jìn)行建模。
對(duì)于Helm,需要?jiǎng)?chuàng)建一個(gè)通用chart,該chart本身接受values.yaml形式的參數(shù),如果希望擁有不同的環(huán)境,則需要多個(gè)values文件[11]。

對(duì)于Kustomize,需要?jiǎng)?chuàng)建一個(gè)“base”配置,然后每個(gè)環(huán)境被建模為一個(gè)overlay,有自己的文件夾:

在這兩種情況下,不同的環(huán)境使用不同的文件夾/文件進(jìn)行建模。Helm和Kustomize對(duì)Git分支、Git merge或Pull Requests一無(wú)所知,只使用普通文件。
再重復(fù)一遍:Helm和Kustomize在不同的環(huán)境下使用普通文件,而不是Git分支。這是一個(gè)很好的提示,說(shuō)明如何使用這兩種工具建模不同的Kubernetes配置。
如果引入Git分支,不僅會(huì)引入額外的復(fù)雜性,還會(huì)違背自己的工具。
在GitOps環(huán)境中部署發(fā)布的推薦方法
建模不同的Kubernetes環(huán)境,并在環(huán)境之間部署發(fā)布,對(duì)于所有采用GitOps的團(tuán)隊(duì)來(lái)說(shuō)都是非常普遍的問(wèn)題。盡管非常流行的方法是在每個(gè)環(huán)境中使用Git分支,并假設(shè)每次部署都是一個(gè)“簡(jiǎn)單的”Git合并,但在本文中已經(jīng)看到,這是一個(gè)反模式。
下面我們將介紹一種更好的方法來(lái)為不同的環(huán)境建模,從而在不同的Kubernetes集群上部署發(fā)布,之前的介紹(關(guān)于Helm/Kustomize)應(yīng)該已經(jīng)給了你一點(diǎn)關(guān)于這種方案的提示。
下面我會(huì)解釋如何在同一個(gè)Git分支上使用不同的文件夾對(duì)GitOps環(huán)境進(jìn)行建模,以及如何通過(guò)簡(jiǎn)單的文件復(fù)制操作來(lái)處理環(huán)境升級(jí)(簡(jiǎn)單的和復(fù)雜的)。

首先了解應(yīng)用程序
在創(chuàng)建文件夾結(jié)構(gòu)之前,需要先做一些研究,了解應(yīng)用程序的“設(shè)置”。盡管一些人以通用的方式討論應(yīng)用程序配置,但實(shí)際上并不是所有的配置設(shè)置都同樣重要。
在Kubernetes應(yīng)用的上下文中,我們有以下幾類“環(huán)境配置”:
- 容器tag形式的應(yīng)用版本。這可能是Kubernetes manifest中最重要的設(shè)置(就環(huán)境升級(jí)而言)。根據(jù)不同的用例,只需更改容器鏡像版本即可。不過(guò)有可能源代碼中的新變更也需要更改部署環(huán)境。
- 應(yīng)用相關(guān)的Kubernetes特定配置。包括應(yīng)用的副本和其他Kubernetes相關(guān)信息,如資源限制、運(yùn)行狀況檢查、持久卷、親和性規(guī)則等。
- 基本靜態(tài)業(yè)務(wù)配置。這是一組與Kubernetes無(wú)關(guān)的設(shè)置,但與應(yīng)用業(yè)務(wù)有關(guān)。可能是外部url、內(nèi)部隊(duì)列大小、UI默認(rèn)值、身份驗(yàn)證配置文件等。所謂“基本靜態(tài)”,我指的是為每個(gè)環(huán)境定義一次的設(shè)置,然后永遠(yuǎn)不會(huì)更改。例如,我們總是希望生產(chǎn)環(huán)境使用production.paypal.com,而非生產(chǎn)環(huán)境使用staging.paypal.com。在不同的環(huán)境中,這是一個(gè)我們永遠(yuǎn)不希望遷移的設(shè)置。
- 非靜態(tài)業(yè)務(wù)配置。和上一點(diǎn)一樣,但包含了希望在不同環(huán)境之間遷移的設(shè)置,可以是全球VAT設(shè)置、推薦引擎參數(shù)、可用的比特率編碼,以及任何其他特定于業(yè)務(wù)的配置。
必須了解所有不同的設(shè)置是什么,更重要的是,哪些屬于第4類,因?yàn)檫@些是我們希望隨應(yīng)用程序版本一起推廣的設(shè)置。
這樣就可以覆蓋所有可能的部署場(chǎng)景:
- 應(yīng)用在QA中從版本1.34升級(jí)到1.35,這是一個(gè)簡(jiǎn)單的源代碼變更,因此只需要在QA環(huán)境中更改容器鏡像屬性。
- 應(yīng)用在預(yù)發(fā)環(huán)境中從版本3.23升級(jí)到3.24,這不是一個(gè)簡(jiǎn)單的源代碼變更,不但需要更新容器鏡像屬性,而且從QA環(huán)境帶來(lái)了新的設(shè)置“recommender.batch_size”。
我看到很多團(tuán)隊(duì)不理解不同配置參數(shù)之間的區(qū)別,而只使用一個(gè)配置文件(或機(jī)制)來(lái)設(shè)置不同域的值(即運(yùn)行時(shí)和應(yīng)用業(yè)務(wù)配置)。
有了配置列表以及所屬區(qū)域之后,就可以創(chuàng)建環(huán)境結(jié)構(gòu)并優(yōu)化需要經(jīng)常變更并且需要在不同環(huán)境之間遷移的文件復(fù)制操作。
5個(gè)GitOps環(huán)境及其變更示例
我們來(lái)看一個(gè)實(shí)際的例子。
我們將對(duì)之前提到的環(huán)境進(jìn)行建模,該公司有5個(gè)不同的環(huán)境:
- 負(fù)載測(cè)試
- 集成測(cè)試
- QA
- 預(yù)發(fā)
- 生產(chǎn)
我們假設(shè)最后兩個(gè)環(huán)境也部署在歐洲、美國(guó)和亞洲,而前兩個(gè)環(huán)境也有GPU和非GPU變體,這意味著該公司共有11個(gè)環(huán)境。
可以在 https://github.com/kostis-codefresh/gitops-environment-promotion 找到建議的文件夾結(jié)構(gòu),所有環(huán)境都是同一分支中的不同文件夾,對(duì)于不同的環(huán)境沒(méi)有分支。如果想知道在一個(gè)環(huán)境中部署了什么,只需查看repo的主分支中的envs/。
在解釋結(jié)構(gòu)之前,有一些免責(zé)聲明:
免責(zé)聲明1: 寫這篇文章花了我很長(zhǎng)時(shí)間,因?yàn)椴淮_定應(yīng)該討論Kustomize[12]、Helm[13]還是普通的manifests。我選擇了Kustomize,因?yàn)樗?jiǎn)單(在文章的最后我也提到了Helm)。但是請(qǐng)注意,示例repo中的Kustomize模板只是為了演示目的。本文不是Kustomize教程。在實(shí)際應(yīng)用中,你可能有Configmap生成器[14]、定制補(bǔ)丁[15],并采用和這里展示的完全不同的“組件”結(jié)構(gòu)。如果你不熟悉Kustomize,請(qǐng)先花些時(shí)間理解它的功能,然后再回來(lái)。
免責(zé)聲明2: 我用于部署的應(yīng)用[16]完全只是為了演示,它的配置由于簡(jiǎn)潔和簡(jiǎn)單的原因而忽略了幾個(gè)最佳實(shí)踐。例如,某些部署缺少運(yùn)行狀況檢查[17],所有部署都缺少資源限制[18]。同樣,本文不會(huì)討論如何創(chuàng)建Kubernetes部署,你應(yīng)該已經(jīng)知道正確的部署manifests是什么樣子的。如果想了解更多關(guān)于生產(chǎn)級(jí)最佳實(shí)踐的信息,請(qǐng)參閱另一篇文章 https://codefresh.io/kubernetes-tutorial/kubernetes-antipatterns-1/
拋開(kāi)免責(zé)聲明不說(shuō),下面是存儲(chǔ)庫(kù)的結(jié)構(gòu):

base目錄保存對(duì)所有環(huán)境通用的配置,不會(huì)經(jīng)常改變。如果同時(shí)對(duì)多個(gè)環(huán)境進(jìn)行更改,最好使用“variants”文件夾。
variants文件夾(或者叫mixins、components)保存不同環(huán)境之間的共同特征。在研究上一節(jié)討論的應(yīng)用程序之后,可以自行定義你認(rèn)為的環(huán)境之間的“共同之處”。
在示例應(yīng)用中,我們?yōu)樗衟rod和非prod環(huán)境以及地區(qū)提供了variants。下面是一個(gè)適用于所有生產(chǎn)環(huán)境的prod variant[19]示例。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-deployment
spec:
template:
spec:
containers:
- name: webserver-simple
env:
- name: ENV_TYPE
value: "production"
- name: PAYPAL_URL
value: "production.paypal.com"
- name: DB_USER
value: "prod_username"
- name: DB_PASSWORD
value: "prod_password"
livenessProbe:
httpGet:
path: /health
port: 8080
在上面的示例中,我們確保所有生產(chǎn)環(huán)境都使用了生產(chǎn)DB憑證、生產(chǎn)支付網(wǎng)關(guān)和活動(dòng)探針(這是一個(gè)精心設(shè)計(jì)的示例,請(qǐng)參閱本節(jié)開(kāi)頭的免責(zé)聲明2)。這些設(shè)置屬于我們不希望在不同環(huán)境之間遷移的配置集,我們假設(shè)在整個(gè)應(yīng)用生命周期中這些都是靜態(tài)的。
準(zhǔn)備好base和variants之后,可以用這些屬性的組合來(lái)定義每個(gè)最終環(huán)境。
下面是一個(gè)ASIA環(huán)境的示例[20]:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
namePrefix: staging-asia-
resources:
- ../../base
components:
- ../../variants/non-prod
- ../../variants/asia
patchesStrategicMerge:
- deployment.yml
- version.yml
- replicas.yml
- settings.yml
首先定義一些公共屬性,從base環(huán)境、非prod環(huán)境和asia的所有環(huán)境中繼承所有配置。
這里的關(guān)鍵點(diǎn)是我們應(yīng)用的補(bǔ)丁。version.yml[21]和replicas.yml[22]是自解釋的,只定義自己的鏡像和副本,其他什么都沒(méi)有。
version.yml文件(這是環(huán)境間最重要的東西)只定義了應(yīng)用的鏡像,其他什么都沒(méi)有。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-deployment
spec:
template:
spec:
containers:
- name: webserver-simple
image: docker.io/kostiscodefresh/simple-env-app:2.0
我們希望在不同環(huán)境之間部署的每個(gè)版本的相關(guān)設(shè)置也在settings.yml[23]中定義。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-deployment
spec:
template:
spec:
containers:
- name: webserver-simple
env:
- name: UI_THEME
value: "dark"
- name: CACHE_SIZE
value: "1024kb"
- name: PAGE_LIMIT
value: "25"
- name: SORTING
value: "ascending"
- name: N_BUCKETS
value: "42"
請(qǐng)隨意查看整個(gè)存儲(chǔ)庫(kù)[16],以理解所有kustomizations的構(gòu)造方式。
通過(guò)GitOps執(zhí)行初始部署
要將應(yīng)用程序部署到相關(guān)的環(huán)境中,只需將GitOps控制器指向相應(yīng)的“env”文件夾,kustomize將創(chuàng)建完整的settings和values層次結(jié)構(gòu)。
下面是在Staging/Asia中運(yùn)行的示例應(yīng)用程序[16]。

可以在命令行上使用Kustomize預(yù)覽將為每個(gè)環(huán)境部署的內(nèi)容,例如:
kustomize build envs/staging-asia
kustomize build envs/qa
kustomize build envs/integration-gpu
當(dāng)然,也可以將上述命令的輸出通過(guò)管道輸出到kubectl來(lái)部署每個(gè)環(huán)境,但在GitOps的上下文中,應(yīng)該始終讓GitOps控制器部署環(huán)境,避免手動(dòng)kubectl操作。
比較兩個(gè)環(huán)境的配置
對(duì)于軟件團(tuán)隊(duì)來(lái)說(shuō),一個(gè)非常普遍的需求是理解兩個(gè)環(huán)境之間的不同之處。我看到一些團(tuán)隊(duì)有這樣的誤解,他們認(rèn)為只有使用分支才能很容易的發(fā)現(xiàn)不同環(huán)境之間的差異。
這與事實(shí)相去甚遠(yuǎn)。通過(guò)比較文件和文件夾,可以很容易的使用成熟的文件diff工具來(lái)查找環(huán)境之間的不同之處。
最簡(jiǎn)單的方法是只區(qū)分對(duì)應(yīng)用程序至關(guān)重要的設(shè)置。
vimdiff envs/integration-gpu/settings.yml envs/integration-non-gpu/settings.yml

在kustomize的幫助下,還可以比較任意數(shù)量的環(huán)境,獲取整體的概念:
kustomize build envs/qa/> /tmp/qa.yml
kustomize build envs/staging-us/ > /tmp/staging-us.yml
kustomize build envs/prod-us/ > /tmp/prod-us.yml
vimdiff /tmp/staging-us.yml /tmp/qa.yml /tmp/prod-us.yml

個(gè)人認(rèn)為這種方式和在環(huán)境分支之間執(zhí)行“git diff”沒(méi)有什么差別。
如何在GitOps環(huán)境中進(jìn)行部署升級(jí)
現(xiàn)在文件結(jié)構(gòu)已經(jīng)很清楚了,終于可以回答這個(gè)古老的問(wèn)題:“我如何用GitOps部署發(fā)布”?
讓我們看看下面一些部署場(chǎng)景。如果你有關(guān)注文件結(jié)構(gòu),應(yīng)該已經(jīng)了解到所有部署升級(jí)都可以解析為簡(jiǎn)單的文件復(fù)制操作。
場(chǎng)景: 在美國(guó)將應(yīng)用版本從QA升級(jí)到預(yù)發(fā)環(huán)境:
- cp envs/qa/version.yml envs/staging-us/version.yml
- commit/push變更
場(chǎng)景: 從GPU集成測(cè)試到GPU負(fù)載測(cè)試,再到QA的應(yīng)用版本升級(jí)。這是一個(gè)兩步的過(guò)程:
- cp envs/integration-gpu/version.yml envs/load-gpu/version.yml
- commit/push變更
- cp envs/load-gpu/version.yml envs/qa/version.yml
- commit/push變更
場(chǎng)景: 通過(guò)額外配置,將應(yīng)用程序從prod-eu升級(jí)到prod-us。這里我們還復(fù)制了settings文件。
- cp envs/prod-eu/version.yml envs/prod-us/version.yml
- cp envs/prod-eu/settings.yml envs/prod-us/settings.yml
- commit/push變更
場(chǎng)景: 確保QA擁有與staging-asia相同的副本數(shù)量
- cp envs/staging-asia/replicas.yml envs/qa/replicas.yml
- commit/push變更
場(chǎng)景: 從QA到移植所有配置到集成測(cè)試(非gpu版本)
- cp envs/qa/settings.yml envs/integration-non-gpu/settings.yml
- commit/push變更
場(chǎng)景: 一次性對(duì)所有非prod環(huán)境進(jìn)行全局更改(但請(qǐng)參閱下一節(jié),以了解關(guān)于此操作的一些討論)
- 在variants/non-prod/non-prod.yml中做出變更
- commit/push變更
場(chǎng)景: 向所有美國(guó)環(huán)境(包括生產(chǎn)環(huán)境和預(yù)發(fā)環(huán)境)添加新的配置文件。
- 在variants/us文件夾中添加新的manifest
- 修改variants/us/kustomization.yml引入新的manifest
- commit/push變更
一般來(lái)說(shuō),所有的部署升級(jí)只是復(fù)制操作。與branch-per-environment方法不同,現(xiàn)在可以自由的將任何東西從任何環(huán)境推廣到其他環(huán)境,不必?fù)?dān)心進(jìn)行錯(cuò)誤的變更。特別是當(dāng)涉及到反向移植配置時(shí),environment-per-folder確實(shí)很出色,因?yàn)榭梢院?jiǎn)單地“向上”或“向后”移動(dòng)配置,甚至可以在不相關(guān)的環(huán)境之間移動(dòng)配置。
注意,我使用cp操作只是為了演示。在實(shí)際的應(yīng)用程序中,此操作將由CI系統(tǒng)或其他編排工具自動(dòng)執(zhí)行。根據(jù)環(huán)境的不同,你可能想先創(chuàng)建一個(gè)Pull Request,而不是直接在主分支中編輯文件夾。
一次對(duì)多個(gè)環(huán)境進(jìn)行更改
首先,我們需要定義“多重”環(huán)境的確切含義,假設(shè)以下兩種情況。
- 同時(shí)更改同一“級(jí)別”上的多個(gè)環(huán)境。例如,想要同時(shí)變更“prod-us”、“prod-eu”和“prod-asia”。
- 同時(shí)更改不在同一級(jí)別上的多個(gè)環(huán)境。例如,想同時(shí)更改“integration”和“staging-eu”。
第一種情況是有效場(chǎng)景,我們將在下面討論。但是,我認(rèn)為第二個(gè)場(chǎng)景是反模式,擁有不同環(huán)境的關(guān)鍵在于能夠以一種漸進(jìn)的方式發(fā)布內(nèi)容,并推動(dòng)從一個(gè)環(huán)境到下一個(gè)環(huán)境的變化。因此,如果你發(fā)現(xiàn)自己在不同的環(huán)境中部署了相同的變化,問(wèn)問(wèn)自己是否真的需要這樣做以及為什么。
對(duì)于部署單個(gè)更改到多個(gè)“類似”環(huán)境的有效場(chǎng)景,有兩種策略:
- 如果你確定更改是絕對(duì)“安全的”,并且希望立即應(yīng)用到所有環(huán)境,那么可以在適當(dāng)?shù)膙ariant(或各自的文件夾)中進(jìn)行更改。例如,如果你在variants/non-prod文件夾中提交/推送一個(gè)更改,那么所有非生產(chǎn)環(huán)境都會(huì)同時(shí)應(yīng)用這個(gè)更改。我個(gè)人反對(duì)這種方法,因?yàn)橛行└脑诶碚撋峡雌饋?lái)是“安全的”,但在實(shí)踐中可能會(huì)有問(wèn)題。
- 更可取的方法是將更改應(yīng)用于每個(gè)單獨(dú)的文件夾,然后將其移動(dòng)到“父”variant(當(dāng)它在所有環(huán)境中都存在時(shí))。
讓我們舉個(gè)例子。我們想做一個(gè)影響所有EU環(huán)境的改變(例如GDPR功能[24])。簡(jiǎn)單的方法是將配置更改直接提交/推送到variants/eu文件夾。這確實(shí)會(huì)影響到所有的EU環(huán)境(prod-eu和staging-eu)。但是,這有一點(diǎn)風(fēng)險(xiǎn),因?yàn)槿绻渴鹗。蜁?huì)導(dǎo)致生產(chǎn)環(huán)境崩潰。
建議采用如下方法:
- 首先在envs/staging-eu中做出變更
- 然后對(duì)envs/prod-eu做同樣的修改
- 最后,從兩個(gè)環(huán)境中刪除更改,并將其添加到variants/eu中(通過(guò)一個(gè)commit/push操作)。

你可以從漸進(jìn)的數(shù)據(jù)庫(kù)重構(gòu)[25]中認(rèn)識(shí)到這種模式。最后的提交是“過(guò)渡性的”,不會(huì)以任何方式影響任何環(huán)境。Kustomize將在這兩種情況下創(chuàng)建完全相同的定義,GitOps控制器應(yīng)該不會(huì)發(fā)現(xiàn)任何差異。
這種方法的優(yōu)點(diǎn)是,當(dāng)我們?cè)诃h(huán)境中移動(dòng)更改時(shí),可以輕松回滾/恢復(fù)更改。缺點(diǎn)是需要增加工作(和提交)將更改推廣到所有環(huán)境,但是我相信這些工作的好處大于風(fēng)險(xiǎn)。
如果采用這種方法,意味著永遠(yuǎn)不會(huì)直接對(duì)base文件夾應(yīng)用新的更改。如果希望對(duì)所有環(huán)境進(jìn)行更改,則首先將更改應(yīng)用于單個(gè)環(huán)境和/或variants,然后將其反向移植到base文件夾,同時(shí)將其從所有下游文件夾中刪除。
“environment-per-folder”方法的優(yōu)點(diǎn)
既然我們已經(jīng)分析了“environment-per-folder”方法的所有內(nèi)部工作原理,現(xiàn)在就該解釋為什么它比“branch-per-environment”方法更好了。如果你已經(jīng)看過(guò)前面的部分,那么應(yīng)該已經(jīng)理解了“environment-per-folder”方法是如何避免之前分析的所有問(wèn)題的。
環(huán)境分支最突出的問(wèn)題是提交的順序,以及從一個(gè)環(huán)境合并到另一個(gè)環(huán)境時(shí)帶來(lái)不必要更改的風(fēng)險(xiǎn)。使用文件夾方法,這個(gè)問(wèn)題就完全消除了:
- 提交順序現(xiàn)在已經(jīng)無(wú)關(guān)緊要了。當(dāng)你將一個(gè)文件從一個(gè)文件夾復(fù)制到下一個(gè)文件夾時(shí),不需要關(guān)心它的提交歷史,只需要關(guān)心它的內(nèi)容。
- 通過(guò)只復(fù)制周圍的文件,只拿需要的東西,而不拿其他東西。當(dāng)你復(fù)制envs/qa/version.yml到env/staging-asia/version.yml中,可以確定只升級(jí)了容器鏡像,沒(méi)有其他東西。如果其他人在QA環(huán)境中改變了副本,并不會(huì)影響升級(jí)流程。
- 不需要使用git cherry-picks或任何其他高級(jí)的git方法來(lái)升級(jí)版本,只需要復(fù)制文件,并且可以訪問(wèn)用于文件處理的實(shí)用程序的成熟生態(tài)系統(tǒng)。
- 可以自由的從任何環(huán)境對(duì)上游或下游環(huán)境進(jìn)行任何更改,而不受環(huán)境正確“順序”的任何限制。例如,如果想將設(shè)置從prod-us反向移植到staging-us,可以簡(jiǎn)單的將env/prod-us/settings.yml拷貝到env/staging-us/settings.yml,而不用擔(dān)心可能會(huì)無(wú)意中部署了不相關(guān)的只應(yīng)在生產(chǎn)環(huán)境中應(yīng)用的修補(bǔ)程序。
- 可以容易的使用文件diff操作來(lái)了解各個(gè)環(huán)境之間的不同之處(源環(huán)境和目標(biāo)環(huán)境,反之亦然)
我認(rèn)為這些優(yōu)勢(shì)對(duì)于任何重要的應(yīng)用程序都是非常重要的,我敢打賭大型組織中總會(huì)有幾個(gè)“失敗的部署”可以直接或間接歸因于有問(wèn)題的environment-per-branch模型。
之前我們提到的第二個(gè)問(wèn)題是,將一個(gè)分支合并到下一個(gè)環(huán)境時(shí),會(huì)出現(xiàn)配置漂移。這樣做的原因是,當(dāng)你執(zhí)行“git merge”時(shí),git只會(huì)通知你它將帶來(lái)的更改,而不會(huì)告訴你目標(biāo)分支中已經(jīng)發(fā)生了什么更改。
同樣,文件夾方案完全消除了這個(gè)問(wèn)題。正如前面說(shuō)的,文件diff操作沒(méi)有“方向”的概念,可以從任何環(huán)境向上或向下復(fù)制任何設(shè)置,如果對(duì)文件執(zhí)行diff操作,可以看到環(huán)境之間的所有更改,而不管它們的上游/下游位置如何。
關(guān)于環(huán)境分支的最后一點(diǎn)是隨著環(huán)境數(shù)量的增長(zhǎng),分支復(fù)雜性將會(huì)線性增加。對(duì)于5個(gè)環(huán)境,需要在5個(gè)分支之間切換更改,而對(duì)于20個(gè)環(huán)境,需要處理20個(gè)分支。在大量的分支之間正確遷移發(fā)布版本是一個(gè)繁瑣的過(guò)程,在生產(chǎn)環(huán)境中,這是一場(chǎng)災(zāi)難。
使用文件夾方法,分支的數(shù)量不僅是靜態(tài)的,而且只有一個(gè)。如果有5個(gè)環(huán)境,可以用“主”分支來(lái)管理,如果需要更多的環(huán)境,你只需要添加額外的文件夾。如果20個(gè)環(huán)境,仍然只需要一個(gè)Git分支。當(dāng)只有一個(gè)分支時(shí),獲得部署的集中視圖是很簡(jiǎn)單的。
在GitOps環(huán)境中使用Helm
如果你不使用Kustomize而是更喜歡Helm,也可以創(chuàng)建一個(gè)文件夾層次結(jié)構(gòu),其中包含所有環(huán)境的“通用”設(shè)置,特定的特性/mixins/組件,以及特定于每個(gè)環(huán)境的最終文件夾。
下面是文件夾結(jié)構(gòu)的樣子:
chart/
[...chart files here..]
common/
values-common.yml
variants/
prod/
values-prod.yml
non-prod/
Values-non-prod.yml
[...other variants…]
envs/
prod-eu/
values-env-default.yaml
values-replicas.yaml
values-version.yaml
values-settings.yaml
[..other environments…]
同樣,你需要花一些時(shí)間來(lái)檢查應(yīng)用屬性,并決定如何將它們分割成不同的values文件,以獲得最佳的升級(jí)速度。
除此之外,在環(huán)境升級(jí)方面,大多數(shù)過(guò)程都是一樣的。
場(chǎng)景: 在US將應(yīng)用版本從QA提升到預(yù)發(fā)環(huán)境:
- cp envs/qa/values-version.yml envs/staging-us/values-version.yml
- commit/push變更
場(chǎng)景: 從GPU集成測(cè)試到GPU負(fù)載測(cè)試,再到QA的應(yīng)用版本升級(jí)。這是一個(gè)兩步的過(guò)程:
- cp envs/integration-gpu/values-version.yml envs/load-gpu/values-version.yml
- commit/push變更
- cp envs/load-gpu/values-version.yml envs/qa/values-version.yml
- commit/push變更
場(chǎng)景: 通過(guò)額外配置,將應(yīng)用從prod-eu提升到prod-us。這里我們還復(fù)制了settings文件。
- cp envs/prod-eu/values-version.yml envs/prod-us/values-version.yml
- cp envs/prod-eu/values-settings.yml envs/prod-us/values-settings.yml
- commit/push變更
理解Helm(或者你的GitOps代理處理Helm)如何處理多個(gè)values文件以及它們相互覆蓋的順序也是非常重要的。
如果希望預(yù)覽某個(gè)環(huán)境,可以使用以下命令,而不是“kustomize build”:
helm template chart/ --values common/values-common.yaml --values variants/prod/values-prod.yaml –values envs/prod-eu/values-env-default.yml –values envs/prod-eu/values-replicas.yml –values envs/prod-eu/values-version.yml –values envs/prod-eu/values-settings.yml
可以看到,如果在每個(gè)環(huán)境文件夾中都有大量的variants或文件,那么Helm比Kustomize更麻煩一些。
environment-per-git-repo方法
當(dāng)我與大型組織討論文件夾方法時(shí),聽(tīng)到的第一個(gè)反對(duì)意見(jiàn)是,人們(尤其是安全團(tuán)隊(duì))不喜歡看到單個(gè)Git存儲(chǔ)庫(kù)中的單個(gè)分支同時(shí)包含產(chǎn)品化和非產(chǎn)品化環(huán)境。
這是一個(gè)可以理解的反對(duì)意見(jiàn),可以說(shuō)是文件夾方法相對(duì)于“environment-per-branch”范式的唯一弱點(diǎn)。畢竟,在Git存儲(chǔ)庫(kù)中保護(hù)各個(gè)分支比在單個(gè)分支中保護(hù)文件夾要容易得多。
這個(gè)問(wèn)題可以很容易的通過(guò)自動(dòng)化、驗(yàn)證檢查甚至手工批準(zhǔn)(如果這對(duì)你的組織至關(guān)重要的話)來(lái)解決。我想再次強(qiáng)調(diào),在文件操作中使用“cp”來(lái)升級(jí)發(fā)布版本,只是為了演示的目的,并不意味著當(dāng)升級(jí)發(fā)生時(shí),需要在交互式終端中手動(dòng)運(yùn)行cp。
理想情況下,應(yīng)該有一個(gè)自動(dòng)化系統(tǒng)來(lái)復(fù)制文件并commit/push它們,可以是持續(xù)集成(CI)系統(tǒng)或處理軟件生命周期的其他平臺(tái)。如果仍然有人自己做出改變,不應(yīng)該直接commit “main”目錄,而是應(yīng)該發(fā)起一個(gè)Pull Request,然后通過(guò)適當(dāng)?shù)牧鞒?,在合并之前檢查Pull Request。
然而,我意識(shí)到有些組織對(duì)安全問(wèn)題特別敏感,當(dāng)涉及到Git保護(hù)時(shí),他們更喜歡完全隔離的方法。對(duì)于這些組織,可以使用2個(gè)Git存儲(chǔ)庫(kù),一個(gè)保存base配置、所有生產(chǎn)variants和所有生產(chǎn)環(huán)境(以及所有與生產(chǎn)相關(guān)的東西),而第二個(gè)Git存儲(chǔ)庫(kù)保存所有非生產(chǎn)的東西。
這種方法讓升級(jí)變得有點(diǎn)困難,因?yàn)楝F(xiàn)在需要在做任何升級(jí)之前簽出2個(gè)git倉(cāng)庫(kù)。另一方面,它允許安全團(tuán)隊(duì)向“生產(chǎn)”Git存儲(chǔ)庫(kù)放置額外的安全約束,并且無(wú)論部署到多少環(huán)境中,仍然擁有靜態(tài)數(shù)量的Git存儲(chǔ)庫(kù)(只有2個(gè))。
個(gè)人認(rèn)為這種方法有些過(guò)分,至少在我看來(lái),它顯示出開(kāi)發(fā)和運(yùn)維缺乏信任。關(guān)于人們是否應(yīng)該直接訪問(wèn)生產(chǎn)環(huán)境的討論是一個(gè)復(fù)雜的問(wèn)題,可能需要單獨(dú)討論。
擁抱文件夾,忘記分支
希望通過(guò)這篇文章,可以解決在多環(huán)境中部署的問(wèn)題,現(xiàn)在你已經(jīng)很好的理解了文件夾方法的好處以及應(yīng)該使用它的原因。
GitOps部署快樂(lè)!
References:
[1] Stop Using Branches for Deploying to Different GitOps Environments: https://medium.com/containers-101/stop-using-branches-for-deploying-to-different-gitops-environments-7111d0632402
[2] How to Model Your Gitops Environments and Promote Releases between Them: https://codefresh.io/about-gitops/how-to-model-your-gitops-environments-and-promote-releases-between-them/
[3] Multiple environments(dev, stage, ..,. prod ) example: https://github.com/argoproj/argocd-example-apps/issues/57
[4] A successful git branching model: https://nvie.com/posts/a-successful-git-branching-model/
[5] Trunk based development: https://trunkbaseddevelopment.com/
[6] Feature flags: https://trunkbaseddevelopment.com/feature-flags/
[7] git diff: https://git-scm.com/docs/git-diff
[8] git cherry-pick: https://git-scm.com/docs/git-cherry-pick
[9] Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
[10] Manage resources containers: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
[11] Helm deployment evnironments: https://codefresh.io/helm-tutorial/helm-deployment-environments/
[12] Kustomize: https://kustomize.io/
[13] Helm: https://helm.sh/
[14] Configmap generator: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/
[15] Patches strategic merge: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patchesstrategicmerge/
[16] GitOps promotion source code: https://github.com/kostis-codefresh/gitops-promotion-source-code
[17] Configure liveness readiness startup probes: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
[18] Manage resources containers: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
[19] Sample prod: https://raw.githubusercontent.com/kostis-codefresh/gitops-environment-promotion/main/variants/prod/prod.yml
[20] Sample staging-asia: https://github.com/kostis-codefresh/gitops-environment-promotion/tree/main/envs/staging-asia
[21] Sample version.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/version.yml
[22] Sample replicas.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/replicas.yml
[23] Sample settings.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/settings.yml
[24] GDPR: https://gdpr-info.eu/
[25] Database refactoring: https://databaserefactoring.com/
你好,我是俞凡,在Motorola做過(guò)研發(fā),現(xiàn)在在Mavenir做技術(shù)工作,對(duì)通信、網(wǎng)絡(luò)、后端架構(gòu)、云原生、DevOps、CICD、區(qū)塊鏈、AI等技術(shù)始終保持著濃厚的興趣,平時(shí)喜歡閱讀、思考,相信持續(xù)學(xué)習(xí)、終身成長(zhǎng),歡迎一起交流學(xué)習(xí)。
微信公眾號(hào):DeepNoMind