27 月 2019 日在 DevOpsConf XNUMX 會議主廳舉行,作為節日的一部分
自演示以來,我們的實用程式(以前稱為 dapp)已經達到了歷史性的里程碑 GitHub 上有 1000 顆星 - 我們希望其不斷成長的用戶社群將使許多 DevOps 工程師的生活變得更輕鬆。
所以,我們提出
向 Kubernetes 交付程式碼
演講將不再是關於 werf,而是關於 Kubernetes 中的 CI/CD,這意味著我們的軟體是打包在 Docker 容器中的 (我在
Kubernetes 中的交付是什麼樣的?
- 有一個 Git 儲存庫,其中包含建置它的程式碼和說明。 該應用程式內建到 Docker 映像中並發佈在 Docker 註冊表中。
- 同一存儲庫還包含有關如何部署和運行應用程式的說明。 在部署階段,這些指令會傳送到 Kubernetes,後者從註冊表接收所需的映像並啟動它。
- 另外,通常還有測試。 其中一些可以在發布圖像時完成。 您還可以(按照相同的說明)部署應用程式的副本(在單獨的 K8s 命名空間或單獨的叢集中)並在那裡執行測試。
- 最後,您需要一個 CI 系統來接收來自 Git 的事件(或按鈕點擊)並呼叫所有指定的階段:建置、發布、部署、測試。
這裡有一些重要的注意事項:
- 因為我們有一個不可變的基礎設施 (不可變的基礎設施),在所有階段(暫存、生產等)使用的應用程式映像, 一定有一個. 我透過範例更詳細地討論了這一點。
這裡 . - 因為我們遵循基礎設施即程式碼方法 (IAC),應用程式程式碼,組裝和啟動它的說明應該是 完全在一個儲存庫中. 有關這方面的更多信息,請參閱
同一份報告 . - 配送鏈 (送貨) 我們通常會看到這樣的情況:應用程式被組裝、測試、發布 (發布階段) 就是這樣 - 交付已經發生。 但實際上,用戶得到的是你推出的東西, 沒有 然後當你將其交付生產時,當他能夠去那裡並且該生產工作成功時。 所以我相信交付鏈結束了 僅在營運階段 (跑),或者更準確地說,即使在程式碼從生產中刪除(用新程式碼替換)的那一刻。
讓我們回到上面的 Kubernetes 交付方案:它不僅是我們發明的,而且是每個處理這個問題的人發明的。 事實上,這種模式現在被稱為 GitOps (您可以閱讀有關該術語及其背後的想法的更多信息
建構階段
看來你可以在 2019 年談論建立 Docker 映像了,那時每個人都知道如何編寫 Dockerfile 並運行 docker build
?...以下是我想注意的細微差別:
- 影像權重 很重要,所以使用
多級 僅在圖像中留下操作真正需要的應用程式。 - 層數 必須透過組合鏈來最小化
RUN
- 根據含義發出命令。 - 然而,這增加了問題 偵錯,因為當程式集崩潰時,你必須從導致問題的鏈結中找到正確的命令。
- 組裝速度 很重要,因為我們希望快速推出變更並查看結果。 例如,您不希望每次建置應用程式時都重建語言庫中的依賴項。
- 通常來自您需要的一個 Git 儲存庫 許多影像,這可以透過一組 Dockerfile(或一個檔案中的命名階段)和一個 Bash 腳本及其順序組裝來解決。
這只是每個人面臨的冰山一角。 但還有其他問題,特別是:
- 通常在組裝階段我們需要一些東西 山 (例如,將 apt 等指令的結果快取在第三方目錄中)。
- 我們想要 Ansible 而不是在 shell 中編寫。
- 我們想要 不使用 Docker 構建 (當我們已經有一個可以運行容器的 Kubernetes 叢集時,為什麼我們還需要一個額外的虛擬機器來設定所有內容?)。
- 平行裝配,可以用不同的方式來理解:來自 Dockerfile 的不同命令(如果使用多階段)、同一儲存庫的多次提交、多個 Dockerfile。
- 分散式組裝:我們希望在 pod 中收集「短暫」的東西,因為它們的快取消失了,這意味著它需要單獨儲存在某個地方。
- 最後,我命名了慾望的頂峰 自動魔法:理想的做法是存取儲存庫,輸入一些命令並取得現成的映像,並了解如何正確執行操作和執行哪些操作。 然而,我個人不確定是否可以透過這種方式預見所有細微差別。
以下是這些項目:
-
莫比/buildkit — Docker Inc 的一個建置器(已經整合到目前版本的 Docker 中),正在嘗試解決所有這些問題; -
卡尼科 — 來自 Google 的建構器,可讓您在沒有 Docker 的情況下進行建置; -
Buildpacks.io — CNCF 嘗試創造自動魔法,特別是一個有趣的層變基解決方案; - 以及許多其他實用程序,例如
建造者 ,正版工具/img ...
……看看他們在 GitHub 上有多少顆星。 也就是說,一方面, docker build
存在並且可以做某事,但實際上 問題還沒有完全解決 - 證明這一點的是替代收集器的並行開發,每個收集器都解決了部分問題。
在碼頭組裝
所以我們必須
藍色陰影的問題已經實施,並行建置是在同一主機內完成的,黃色突出顯示的問題計劃在夏末完成。
在註冊表中發布的階段(發布)
我們撥打了 docker push
... - 將圖像上傳到註冊表可能會遇到什麼困難? 然後問題出現了:“我應該在圖像上放置什麼標籤?” 它的出現是因為我們有 gitflow (或其他 Git 策略)和 Kubernetes,業界正在努力確保 Kubernetes 中發生的事情遵循 Git 中發生的事情。 畢竟,Git 是我們唯一的真理來源。
這有什麼難的呢? 確保重現性:來自 Git 中的提交,本質上是不可變的 (不可變),到 Docker 映像,該映像應保持不變。
這對我們也很重要 確定原產地,因為我們想了解 Kubernetes 中運行的應用程式是從哪個提交構建的(然後我們可以進行差異和類似的操作)。
標記策略
第一個很簡單 git標籤。 我們有一個註冊表,其中的圖像標記為 1.0
。 Kubernetes 有階段和生產,上傳此圖像的地方。 在 Git 中,我們進行提交,並在某個時刻進行標記 2.0
。 我們根據存儲庫的說明收集它,並將其放置在帶有標籤的註冊表中 2.0
。 我們將其推向舞台,如果一切順利,然後投入生產。
這種方法的問題在於我們首先放置標籤,然後再進行測試並推出它。 為什麼? 首先,這根本不合邏輯:我們正在發布一個我們甚至還沒有測試過的軟體版本(我們不能這樣做,因為為了檢查,我們需要放置一個標籤)。 其次,這條路徑與Gitflow不相容。
第二種選擇 - git提交+標籤。 master分支有一個標籤 1.0
; 在登錄中 - 部署到生產環境的映像。 此外,Kubernetes 叢集還具有預覽和暫存輪廓。 接下來我們依照Gitflow:在主分支中進行開發(develop
)我們建立新功能,導致使用識別碼進行提交 #c1
。 我們收集它並使用此標識符將其發佈在註冊表中(#c1
)。 使用相同的標識符,我們推出預覽。 我們對提交做同樣的事情 #c2
и #c3
.
當我們意識到有足夠的功能時,我們開始穩定一切。 在 Git 中建立分支 release_1.1
(在底座上 #c3
的 develop
)。 沒有必要收集這個版本,因為...... 這是在上一步完成的。 因此,我們可以簡單地將其推出到暫存階段。 我們修復了以下錯誤 #c4
並類似地推廣到分期。 同時,開發工作正在進行中 develop
,其中定期進行更改 release_1.1
。 在某個時候,我們得到了編譯並上傳到暫存的提交,我們對此感到滿意(#c25
).
然後我們合併(快轉)發布分支(release_1.1
)在主控中。 我們在此提交上放置了帶有新版本的標籤(1.1
)。 但是這個鏡像已經在註冊表中收集了,所以為了不再收集它,我們只需在現有鏡像上添加第二個標籤(現在它在註冊表中有標籤 #c25
и 1.1
)。 之後,我們將其投入生產。
有一個缺點,即只有一張圖像上傳到暫存(#c25
),而在生產上則有點不同(1.1
),但我們知道「物理上」這些是註冊表中的相同圖像。
真正的缺點是不支援合併提交,你必須快轉。
我們可以更進一步,做一個技巧...讓我們來看一個簡單的 Dockerfile 的範例:
FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb
FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public
讓我們根據以下原則從中建立一個文件:
- 來自所用影像識別碼的 SHA256 (
ruby:2.3
иnginx:alpine
),這是其內容的校驗和; - 所有球隊(
RUN
,CMD
等等。); - 來自新增的文件的 SHA256。
……並從這樣的文件中取得校驗和(同樣是 SHA256)。 這 簽名 定義 Docker 映像內容的所有內容。
讓我們回到圖表 我們將使用這樣的簽名來代替提交, IE。 用簽名標記影像。
現在,例如,當有必要將更改從發布版本合併到主版本時,我們可以進行真正的合併提交:它將具有不同的標識符,但具有相同的簽名。 使用相同的標識符,我們將把鏡像投入生產。
缺點是現在無法確定哪種提交被推送到生產環境 - 校驗和僅在一個方向上運作。 這個問題可以透過使用元資料的附加層來解決 - 我稍後會告訴你更多。
在 werf 中標記
在 werf 中,我們走得更遠,準備使用不儲存在一台機器上的快取進行分散式建置......因此,我們正在建立兩種類型的 Docker 映像,我們稱之為 階段 и 圖片.
werf Git 儲存庫儲存特定於建置的指令,這些指令描述了建置的不同階段(安裝前, 安裝, 安裝前, 格局)。 我們收集第一階段影像,其簽名定義為第一步的校驗和。 然後我們添加原始程式碼,對於新的階段圖像,我們計算其校驗和...對所有階段重複這些操作,結果我們得到一組階段圖像。 然後我們製作最終圖像,其中還包含有關其來源的元資料。 我們用不同的方式標記這個圖像(稍後將詳細介紹)。
假設此後出現一個新的提交,其中僅更改了應用程式程式碼。 會發生什麼事? 對於程式碼更改,將建立補丁並準備新的階段映像。 其簽名將被確定為舊階段影像和新補丁的校驗和。 將從該影像形成新的最終影像。 其他階段的變化也會發生類似的行為。
因此,階段鏡像是一個可以分散式儲存的緩存,已經建立的映像會上傳到 Docker 註冊表。
清理註冊表
我們不是在談論刪除刪除標籤後仍然掛起的圖層 - 這是 Docker 註冊表本身的標準功能。 我們討論的是這樣一種情況:大量 Docker 標籤累積起來,我們知道我們不再需要其中一些標籤,但它們佔用了空間(和/或我們為此付費)。
清潔策略有哪些?
- 你可以什麼都不做 不清潔。 有時,花一點錢購買額外的空間確實比解開一大堆標籤更容易。 但這僅在一定程度上有效。
- 完全重置。 如果刪除所有映像並僅重建 CI 系統中的目前映像,可能會出現問題。 如果容器在生產中重新啟動,則會為其加載新映像 - 尚未經過任何人測試的映像。 這扼殺了不可變基礎設施的想法。
- 藍綠。 一個註冊表開始溢出 - 我們將圖像上傳到另一個註冊表。 與上一種方法相同的問題:什麼時候可以清除已經開始溢出的註冊表?
- 按時間。 刪除所有超過 1 個月的影像? 但一定會有一個月沒有更新的服務…
- 手工 確定哪些內容已經可以刪除。
有兩個真正可行的選擇:不清洗或藍綠色+手動的組合。 在後一種情況下,我們討論的是:當您知道需要清理註冊表時,您可以建立一個新註冊表,並在例如一個月的時間內向其中添加所有新映像。 一個月後,請查看 Kubernetes 中哪些 Pod 仍在使用舊註冊表,並將它們也轉移到新註冊表。
我們到了什麼程度 韋爾夫? 我們收集:
- Git head:所有標籤,所有分支 - 假設我們需要圖像中 Git 中標記的所有內容(如果沒有,那麼我們需要在 Git 本身中刪除它);
- 目前輸出到 Kubernetes 的所有 Pod;
- 舊的 ReplicaSet(最近發布的),我們還計劃掃描 Helm 版本並選擇其中的最新映像。
....並從該集合中創建一個白名單 - 我們不會刪除的圖像列表。 我們清理其他所有內容,然後找到孤立的舞台圖像並將其刪除。
部署階段
可靠的聲明性
在部署中我想提請注意的第一點是以聲明方式聲明的更新的資源配置的推出。 描述 Kubernetes 資源的原始 YAML 文件總是與叢集中實際運行的結果有很大差異。 因為 Kubernetes 新增了配置:
- 身份識別;
- 服務資訊;
- 許多預設值;
- 當前狀態的部分;
- 作為准入 Webhook 的一部分進行的變更;
- 各種控制器(和調度程序)的工作結果。
因此,當出現新的資源配置時(新),我們不能只用它來覆蓋當前的“實時”配置(生活)。 為此,我們必須比較 新 與最後套用的配置(最後申請的)並滾到 生活 收到補丁。
這種方法稱為 2路合併。 例如,它被用在 Helm 中。
還有 3路合併,其不同之處在於:
- 比較 最後申請的 и 新,我們看看刪除了什麼;
- 比較 新 и 生活,我們查看添加或更改的內容;
- 求和補丁應用於 生活.
我們使用 Helm 部署了 1000 多個應用程序,因此我們實際上採用的是 2 路合併。 不過,它有一些問題,我們已經透過補丁解決了,這有助於 Helm 正常工作。
真實推出狀態
我們的 CI 系統根據下一個事件為 Kubernetes 產生新的配置後,將其傳輸以供使用 (申請) 到叢集 - 使用 Helm 或 kubectl apply
。 接下來,發生已經描述的 N 路合併,Kubernetes API 向 CI 系統及其使用者做出滿意的回應。
然而,有一個很大的問題:畢竟 申請成功不代表上線成功。 如果 Kubernetes 了解需要應用哪些變更並套用它,我們仍然不知道結果會是什麼。 例如,在前端更新和重新啟動 Pod 可能會成功,但在後端則不會成功,並且我們將獲得正在運行的應用程式映像的不同版本。
為了正確地完成所有工作,該方案需要一個額外的連結 - 一個特殊的追蹤器,它將從 Kubernetes API 接收狀態資訊並將其傳輸以進一步分析事物的真實狀態。 我們用 Go 創建了一個開源庫 -
此追蹤器在 werf 層級的行為是使用放置在 Deployments 或 StatefulSet 上的註解進行配置的。 主要註釋- fail-mode
- 理解以下含義:
-
IgnoreAndContinueDeployProcess
— 我們忽略推出該元件的問題並繼續部署; -
FailWholeDeployProcessImmediately
— 該元件中的錯誤會停止部署程序; -
HopeUntilEndOfDeployProcess
- 我們希望該元件能夠在部署結束時正常運作。
例如,資源和註釋值的這種組合 fail-mode
:
當我們第一次部署時,資料庫(MongoDB)可能還沒準備好——部署將會失敗。 但您可以等待它啟動,部署仍然會進行。
werf 中 kubedog 的註解還有兩個:
-
failures-allowed-per-replica
— 每份副本允許的跌落次數; -
show-logs-until
— 調節 werf 顯示(在標準輸出中)所有已推出 pod 的日誌的時間。 預設為PodIsReady
(當流量開始進入 Pod 時忽略我們並不真正想要的訊息),但值也是有效的:ControllerIsReady
иEndOfDeploy
.
我們還想從部署中得到什麼?
除了已經描述的兩點之外,我們還希望:
- 看 日誌 - 只有必要的,而不是連續的所有;
- 奧特斯勒克瓦塔特 進度,因為如果作業「無聲地」掛起幾分鐘,了解那裡發生的情況很重要;
- иметь 自動復原 以防出現問題(因此了解部署的真實狀態至關重要)。 推出必須是原子的:要嘛進行到最後,要嘛一切都回到之前的狀態。
結果
對於我們公司來說,要在交付的不同階段(建置、發布、部署)實現所有描述的細微差別,CI 系統和實用程式就足夠了
而不是結論:
在 werf 的幫助下,我們在為 DevOps 工程師解決大量問題方面取得了良好進展,如果更廣泛的社區至少嘗試這個實用程序,我們將很高興。 在一起的話會比較容易取得好的結果。
影片和幻燈片
表演影片(約 47 分鐘):
報告介紹:
聚苯乙烯
我們部落格上有關 Kubernetes 的其他報導:
- «
Kubernetes 中的自動縮放和資源管理 » (德米特里·斯托利亞羅夫;27 年 2019 月 XNUMX 日,「罷工」); - «
擴充和補充 Kubernetes » (Andrey Polovov;8 年 2019 月 XNUMX 日,Saint HighLoad++); - «
資料庫和 Kubernetes » (Dmitry Stolyarov;8 年 2018 月 XNUMX 日,HighLoad++); - «
監控和 Kubernetes » (德米特里·斯托利亞羅夫;28 年 2018 月 XNUMX 日在 RootConf); - «
Kubernetes 和 GitLab 的 CI/CD 最佳實踐 » (Dmitry Stolyarov;7 年 2017 月 XNUMX 日,HighLoad++); - «
我們在小型項目中使用 Kubernetes 的經驗 » (德米特里·斯托利亞羅夫;6 年 2017 月 XNUMX 日,RootConf).
來源: www.habr.com