一種更好的 Google Cloud 持續部署方法

已發表: 2022-03-11

持續部署 (CD) 是將新代碼自動部署到生產環境的做法。 大多數持續部署系統通過運行單元和功能測試來驗證要部署的代碼是否可行,如果一切看起來都不錯,部署就會展開。 推出本身通常分階段進行,以便在代碼未按預期運行時能夠回滾。

不乏關於如何使用各種工具(如 AWS 堆棧、Google Cloud 堆棧、Bitbucket 管道等)實現您自己的 CD 管道的博客文章。但我發現其中大多數不符合我對好的 CD 管道的想法應該看起來像:一個首先構建,並且僅測試和部署該單個構建文件。

在本文中,我將構建一個事件驅動的持續部署管道,它首先構建,然後在我們的部署最終工件上運行測試。 這不僅使我們的測試結果更加可靠,而且使 CD 流水線易於擴展。 它看起來像這樣:

  1. 對我們的源存儲庫進行了提交。
  2. 這會觸發相關圖像的構建。
  3. 測試在構建的工件上運行。
  4. 如果一切看起來都不錯,則將映像部署到生產環境。

本文假設您至少熟悉 Kubernetes 和容器技術,但如果您不熟悉或需要復習,請參閱什麼是 Kubernetes? 容器化和部署指南。

大多數 CD 設置的問題

這是我對大多數 CD 管道的問題:它們通常在構建文件中執行所有操作。 我讀過的大多數博客文章在他們擁有的任何構建文件中都會有以下順序的一些變化( cloudbuild.yaml用於 Google Cloud Build, bitbucket-pipeline.yaml用於 Bitbucket)。

  1. 運行測試
  2. 構建鏡像
  3. 將圖像推送到容器 repo
  4. 使用新圖像更新環境

您沒有在最終工件上運行測試。

通過按此順序執行操作,您可以運行測試。 如果他們成功了,您將構建映像並繼續進行其餘的管道。 如果構建過程以某種方式更改了您的圖像,導致測試不再通過,會發生什麼? 在我看來,您應該首先生成一個工件(最終的容器映像),並且該工件不應該在構建和部署到生產之間發生變化。 這可確保您擁有的關於所述工件的數據(測試結果、大小等)始終有效。

您的構建環境擁有“通往王國的鑰匙”。

通過使用您的構建環境將您的映像部署到您的生產堆棧,您實際上是允許它更改您的生產環境。 我認為這是一件非常糟糕的事情,因為任何對您的源存儲庫具有寫入權限的人現在都可以對您的生產環境做任何他們想做的事情。

如果最後一步失敗,您必須重新運行整個管道。

如果最後一步失敗(例如,由於憑證問題),您必須重新運行整個管道,佔用本可以更好地用於其他事情的時間和其他資源。

這引出了我的最後一點:

你的步驟不是獨立的。

在更一般的意義上,擁有獨立的步驟可以讓您在管道中擁有更大的靈活性。 假設您想向管道添加功能測試。 通過將步驟放在一個構建文件中,您需要讓構建環境啟動一個功能測試環境並在其中運行測試(很可能是按順序運行)。 如果您的步驟是獨立的,您可以通過“圖像構建”事件啟動單元測試和功能測試。 然後它們將在自己的環境中並行運行。

我理想的 CD 設置

在我看來,解決這個問題的更好方法是讓一系列獨立的步驟通過事件機制鏈接在一起。

與以前的方法相比,這有幾個優點:

您可以對不同的事件採取幾個獨立的操作。

如上所述,成功構建新映像只會發布“成功構建”事件。 反過來,當這個事件被觸發時,我們可以運行幾件事情。 在我們的例子中,我們將開始單元和功能測試。 您還可以考慮在觸發構建失敗事件或測試未通過時提醒開發人員等事情。

每個環境都有自己的一組權限。

通過讓每一步都發生在自己的環境中,我們不再需要一個單一的環境來擁有所有的權利。 現在構建環境只能構建,測試環境只能測試,部署環境只能部署。 這使您可以確信,一旦您的圖像構建完成,它就不會改變。 生成的工件最終會出現在您的生產堆棧中。 它還允許更輕鬆地審核管道的哪個步驟正在做什麼,因為您可以將一組憑據鏈接到一個步驟。

有更多的靈活性。

想要在每次成功構建時向某人發送電子郵件? 只需添加一些對該事件作出反應並發送電子郵件的內容。 這很容易——您不必更改構建代碼,也不必在源存儲庫中硬編碼某人的電子郵件。

重試更容易。

擁有獨立的步驟也意味著如果一個步驟失敗,您不必重新啟動整個管道。 如果失敗條件是暫時的或已手動修復,您可以重試失敗的步驟。 這允許更有效的管道。 如果構建步驟需要幾分鐘時間,最好不要因為忘記授予部署環境對集群的寫入權限而重新構建映像。

實施 Google Cloud 持續部署

谷歌云平台擁有在短時間內構建這樣一個系統所需的所有工具,並且只需要很少的代碼。

我們的測試應用程序是一個簡單的 Flask 應用程序,它只提供一段靜態文本。 此應用程序部署到 Kubernetes 集群,該集群為更廣泛的互聯網提供服務。

我將實現我之前介紹的管道的簡化版本。 我基本上刪除了測試步驟,所以現在看起來像這樣:

  • 對源存儲庫進行新的提交
  • 這會觸發映像構建。 如果成功,則將其推送到容器存儲庫並將事件發佈到 Pub/Sub 主題
  • 訂閱該主題的小腳本並檢查圖像的參數 - 如果它們與我們要求的匹配,則將其部署到 Kubernetes 集群。

這是我們管道的圖形表示。

管道的圖形表示

流程如下:

  1. 有人提交到我們的存儲庫。
  2. 這會觸發基於源存儲庫構建 Docker 映像的雲構建。
  3. 雲構建將鏡像推送到容器存儲庫,並將消息發佈到雲 pub/sub。
  4. 這會觸發一個雲函數,該函數會檢查已發布消息的參數(構建狀態、構建的映像名稱等)。
  5. 如果參數正確,雲功能會使用新映像更新 Kubernetes 部署。
  6. Kubernetes 使用新鏡像部署新容器。

源代碼

我們的源代碼是一個非常簡單的 Flask 應用程序,它只提供一些靜態文本。 這是我們項目的結構:

 ├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py

Docker 目錄包含構建 Docker 映像所需的所有內容。 該映像基於 uWSGI 和 Nginx 映像,僅安裝依賴項並將應用程序複製到正確的路徑。

k8s 目錄包含 Kubernetes 配置。 它由一項服務和一項部署組成。 部署基於從Dockerfile構建的映像啟動一個容器。 然後該服務啟動具有公共 IP 地址的負載均衡器並重定向到應用程序容器。

雲構建

雲構建配置本身可以通過雲控制台或谷歌云命令行來完成。 我選擇使用雲控制台。

雲控制台截圖

在這裡,我們為任何分支上的任何提交構建一個映像,但例如,您可以為開發和生產創建不同的映像。

如果構建成功,雲構建會自行將鏡像發佈到容器註冊中心。 然後它將向 cloud-builds pub/sub 主題發布一條消息。

雲構建還會在構建進行中和構建失敗時發布消息,因此您也可以讓事物對這些消息做出反應。

雲構建的發布/訂閱通知的文檔在這裡,消息的格式可以在這裡找到

雲發布/訂閱

如果您查看雲控制台中的雲發布/訂閱選項卡,您會看到雲構建創建了一個名為雲構建的主題。 這是雲構建發布其狀態更新的地方。

Pub/Sub 項目截圖

雲功能

我們現在要做的是創建一個雲函數,該函數會在發佈到 cloud-builds 主題的任何消息上觸發。 同樣,您可以使用雲控制台或 Google Cloud 命令行實用程序。 在我的案例中,我所做的是,每次發生更改時,我都會使用雲構建來部署雲功能。

雲功能的源代碼在這裡。

我們先來看一下部署這個雲功能的代碼:

 steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']

在這裡,我們使用 Google Cloud Docker 映像。 這允許輕鬆運行 GCcloud 命令。 我們正在執行的操作相當於直接從終端運行以下命令:

 gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME

我們要求 Google Cloud 部署一個新的雲函數(或者如果該區域中已經存在該名稱的函數,則替換該函數),它將使用 Python 3.7 運行時,並將由 cloud-builds 主題中的新消息觸發。 我們還告訴 Google 在哪裡可以找到該函數的源代碼(這裡 PROJECT_ID 和 REPO_NAME 是由構建過程設置的環境變量)。 我們還告訴它調用哪個函數作為入口點。

附帶說明一下,為了使其工作,您需要為您的 cloudbuild 服務帳戶提供“雲功能開發人員”和“服務帳戶用戶”,以便它可以部署雲功能。

以下是雲函數代碼的一些註釋片段

入口點數據將包含在 pub/sub 主題上收到的消息。

 def onNewImage(data, context):

第一步是從環境中獲取特定部署的變量(我們通過修改雲控制台中的雲函數來定義這些變量。

 project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')

我們將跳過檢查消息結構是否符合我們預期的部分,並驗證構建是否成功並生成了一個圖像工件。

下一步是確保構建的映像是我們要部署的映像。

 image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return

現在,我們得到一個 Kubernetes 客戶端並檢索我們想要修改的部署

 v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return

最後,我們使用新映像修補部署; Kubernetes 將負責將其推出。

 for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)

結論

這是一個非常基本的示例,說明我喜歡如何在 CD 管道中構建事物。 您可以通過更改發布/訂閱事件觸發的內容來執行更多步驟。

例如,您可以運行一個容器,該容器在映像中運行測試,並在成功時發布一個事件,在失敗時發布另一個事件,並通過更新部署或根據結果發出警報來對這些事件做出反應。

我們構建的管道非常簡單,但您可以為其他部分編寫其他雲函數(例如,向提交破壞單元測試的代碼的開發人員發送電子郵件的雲函數)。

如您所見,我們的構建環境無法更改 Kubernetes 集群中的任何內容,我們的部署代碼(雲功能)無法修改已構建的映像。 我們的權限分離看起來不錯,而且我們知道流氓開發人員不會讓我們的生產集群崩潰,我們可以睡個安穩覺。 此外,我們可以讓更多面向運維的開發人員訪問云功能代碼,以便他們修復或改進它。

如果您有任何問題、意見或改進,請隨時在下面的評論中聯繫。