Categories
程式開發

我在雲上試了下流行的十二要素開發方法論


開發人員正在把應用程序遷移到雲上,於是他們在設計及部署雲原生應用程序方面也積累了越來越多的經驗。從這種經驗中出現了一組最佳實踐(俗稱十二要素)。考慮這些要素來設計應用程序,可以讓我們在把應用程序部署到雲上時,能有更高的可移植性和彈性,相比之下,部署到內部環境中的應用程序則需要花更長的時間來提供新資源。

我在雲上試了下流行的十二要素開發方法論 1

本文描述了流行的十二要素APP方法論,以及開發運行於谷歌云平台(Google Cloud Platform,簡稱GCP)上的應用程序時如何應用它。我們使用這種方法可以開發出可擴展和具有彈性的應用程序,這些應用程序能夠以最大靈活性持續地部署。本文旨在供那些熟悉GCP、版本控制、持續集成和容器技術的開發人員參考。

十二要素

十二要素設計還有助於解耦應用程序的組件,從而可以輕鬆地替換每個組件,或無縫地上下擴展。因為這些要素是獨立於任何編程語言或軟件堆棧的,所以十二要素設計可以應用於各種各樣的應用程序。

1. 代碼庫(Codebase)

應該在版本控制系統中(如Git或Mercurial)跟踪應用程序的代碼。處理應用時,可以在本地開發環境中確認代碼。在版本控制系統中存儲代碼可以提供對代碼更改的審計跟踪、解決合併衝突的系統方法及將代碼回滾到以前版本的能力,從而讓團隊合作開發。它還提供了一個進行持續集成(continuous integration,簡稱CI)和持續部署(continuous deployment,簡稱CD)的地方。

儘管開發人員可能在他們的開發環境中處理不同版本的代碼,但在任何給定的時間,事實來源都是版本控制系統中的代碼。存儲庫中的這些代碼是構建、測試和部署的內容,並且,存儲庫的數量與環境的數量無關。存儲庫中的代碼用於產生單個構建,和特定於環境的配置結合在一起用於產生一個不可變的版本(無法修改,包括對配置的修改),然後,它可以被部署到一個環境中(這個版本所需的任何更改都將導致一個新版本的產生。)

雲源代碼存儲庫讓我們能夠在一個功能齊全、可擴展的私有Git存儲庫中協作和管理我們的代碼。它具有跨所有存儲庫的代碼搜索功能。我們還可以連接到其他GCP產品,例如雲構建(Cloud Build)應用引擎(App Engine)Starkdriver雲發布/訂閱(Cloud Pub/Sub)

2. 依賴項

在談到十二要素APP的依賴項時,有兩個注意事項:依賴項聲明和依賴項隔離。

十二要素APP應該永遠沒有隱式依賴性。我們應該顯式聲明任何依賴項並讓它們進入版本控制中。這使我們能夠用可重複的方式快速地開始使用代碼,並更容易跟踪依賴項的更改。很多編程語言提供一種顯式聲明依賴項的方式,比如Python的pip和Ruby的Bundler。

我們還應該把應用程序及其依賴項封裝到容器中,從而把它們隔離開來。容器使我們能夠把應用程序及其依賴項與其環境隔離開來,這樣無論開發和運行環境有任何不同,都能確保應用程序一致地工作。

容器註冊表(Container Registry )是供團隊管理映像及進行漏洞分析的唯一去處。它還提供了對容器映像的細粒度訪問,讓我們決定誰可以訪問什麼。由於容器註冊表使用雲存儲桶作為服務容器映像的後端,因此,我們可以通過調整該雲存儲桶的權限,控制誰可以訪問容器註冊表映像。

現有的CI/CD集成還讓我們設置全自動管道,以獲得快速反饋。我們可以推送映像到它們的註冊表,然後使用HTTP端點從任何機器上來拉取映像,無論這些機器是計算引擎實例( Compute Engine instance)還是我們自己的硬件。接著,容器分析可以為容器註冊表中的映像提供漏洞信息。

3. 配置

每個現代應用程序都需要某種形式的配置。通常,我們對每個環境(如開發環境、測試環境和生產環境)都有不同的配置。這些配置經常包括服務賬戶憑據和數據庫等支持服務的資源句柄。

每個環境的配置都應該在代碼的外部,並且不應該進入版本控制。每個人只處理代碼的一個版本,但我們有多個配置。部署環境決定使用哪個配置。這可以讓二進制代碼的一個版本部署到每個環境中,其中唯一的不同是運行時配置。一個檢查配置是否已經正確地外部化的簡單方法是,檢查是否可以在不洩露任何憑據的情況下公開代碼。

配置外部化的方法之一是創建配置文件。然而,配置文件通常特定於編程語言或開發框架。

一個更好的方法是在環境變量中存儲配置。這些環境變量容易在運行時針對每個環境進行更改,它們不太可能進入版本控制,並且,它們與編程語言和開發框架無關。在谷歌Kubernetes引擎(Google Kubernetes Engine,簡稱GKE)中,我們可以使用ConfigMaps。這讓我們可以在運行時將環境變量、端口號、配置文件、命令行參數以及其他配置工件綁定到pod容器和系統組件 。

4. 支持系統

應用程序正常操作時使用的每個服務(如文件系統、數據庫、緩衝系統和消息隊列)都應該作為服務被訪問並在配置中外部化。我們應該考慮把這些支持服務作為底層資源的抽象。比如,當應用程序把數據寫入存儲時,把存儲作為支持服務對待,可以讓我們無縫地更改底層存儲類型,這是因為它已與應用程序分離。這樣一來,我們就可以執行一個更改,如從本地PostgreSQL數據庫切換到Cloud SQL的PostgreSQL,而無需更改應用程序的代碼。

5. 構建、發布、運行

重要的是要把軟件部署過程分成三個截然不同的階段:構建、發布及運行。每個階段都應該形成一個唯一可識別的工件。每個部署都應該鏈接到一個特定版本,它是環境配置和內部版本結合形成的結果。這使回滾變得更加容易,每個產品部署歷史都有一個可見的審計踪跡。

我們可以手動觸發構建階段,但在我們提交通過了所有要求的測試的代碼時,通常會自動觸發該階段。構建階段獲取代碼、獲取所需的庫和資源,並把這些打包到一個自包含的二進製文件或容器中。構建階段的結果就是構建工件。

構建階段完成時,發布階段把構建工件和特定環境的配置結合在一起。這會生成一個版本。該版本可以通過持續部署應用程序自動地部署到環境中。或者可以通過同一個持續部署應用程序觸發該版本。

最後,運行階段推出該版本並啟動之。比如,如果我們要部署到GKE,那麼雲構建(Cloud Build)可以調用gke-deploy構建步驟以部署到我們的GKE集群中。雲構建可以使用YAML或JSON格式的構建配置文件,跨多種編程語言和環境,管理並自動化構建、發布和運行階段。

6. 進程

可以把十二要素APP作為一個或更多進程運行在環境中。這些進程應該是無狀態的,相互之間不應該共享數據。這使得這些應用可以通過複製其進程來擴展。創建無狀態應用還使進程可跨計算基礎設施移植。

如果我們已習慣“粘性”會話的概念,那麼就需要我們改變對處理和持久化數據的看法。這是因為進程可以隨時消失,而我們無法依賴本地存儲的可用內容,否則任何後續請求將由同一進程處理。因而,我們必須明確保留所有需要在外部支持服務(如數據庫)中重用的數據。

如果需要持久化數據,那麼可以使用Cloud Memorystore,把它作為支持服務以緩存我們應用程序的狀態,並在進程之間共享公共數據,以鼓勵鬆散耦合。

7. 端口綁定

在非雲環境中,web應用程序常常被編寫成在應用程序容器中運行,這些容器包括GlassFish、Apache Tomcat和Apache HTTP Server。與之相反,十二要素APP不依賴外部應用程序容器,而是綁定webserver庫,使之成為該應用程序本身的一部分。

服務公開由PORT環境變量指定的端口號是一種體系結構的最佳實踐。

使用平台即服務模型時,導出端口綁定的應用程序能夠在外部使用端口綁定信息(作為環境變量)。在GCP中,可以在平台服務上部署應用程序,這些平台服務包括計算引擎(Compute Engine)、GKE、應用引擎(App Engine)或云運行(Cloud Run)。

在這些服務中,路由層把請求從面向公共的主機名路由到端口綁定的web進程。比如,在把應用程序部署到應用引擎時,需要聲明給應用程序添加webserver庫的依賴項,如Express(用於Node.js)、Flask和Gunicorn(用於Python)或Jetty(用於Java)。我們不應該在代碼中硬編碼端口號。相反,我們應該在環境中提供端口號,比如在一個環境變量中提供。這使得我們的應用程序在GCP上運行時具有可移植性。

由於Kubernetes具有內置的服務發現(service discovery)功能,在Kubernetes中,我們可以把服務端口映射到容器來抽象端口綁定。服務發現使用內部DNS名來完成。

與硬編碼webserver偵聽的端口相反,配置使用了環境變量。以下所示的代碼截自一個應用引擎應用程序,顯示瞭如何接收在環境變量中傳遞的端口值。

const express = require('express')
const request = require('got')
const app = express()
app.enable('trust proxy')
const PORT = process.env.PORT || 8080
app.listen(PORT, () => {
  console.log('App listening on port ${PORT}')
  console.log('Press Ctrl+C to quit.')
})

8. 並發性

我們應該基於進程類型(如後台、web、工作進程),把應用程序分解成獨立的進程。這使我們的應用程序根據單個工作負載要求進行上下擴展。大多數雲原生的應用程序允許我們按需擴展。我們應該把應用程序設計為多個分佈式進程,這些進程能夠獨立地執行工作塊,並通過添加更多的進程進行擴展。

以下內容描述了一些結構,以便應用程序擴展變得可行。用可處置性和無狀態性的原則為核心構建的應用程序可以很好地從這些水平擴展的結構中受益。

使用應用引擎

我們可以使用應用引擎把我們的應用程序託管到GCP的託管基礎設施上。實例是計算單元,這些計算單元是應用引擎用來自動擴展應用程序的。在任何給定的時間,我們的應用程序可以運行在一個實例或多個實例上,而請求則被分散到所有這些實例上。

應用引擎調度程序決定如何處理每個新的請求。該調度程序可能使用一個現有的實例(或者是空閒的,或者是接受並發請求的),把該請求放到一個待處理的請求隊列中,或為該請求啟動一個新的實例。該決定要考慮可用實例的數量、我們的應用程序處理請求的速度(延遲),以及啟動一個新的實例所需的時間。

如果我們使用自動擴展,那麼,我們可以通過設置目標CPU利用率、目標吞吐量和最大並發請求,在性能和成本之間進行平衡。

我們可以在app.yaml文件中指定擴展的類型,app.yaml文件是我們為獲取服務版本上傳的文件。根據這個配置輸入,該應用引擎基礎設施將使用動態或常駐實例。關於擴展類型的更多信息,請參考應用引擎文檔

使用計算引擎

或者,我們可以在計算引擎上部署和管理應用程序。在這種情況下,我們可以根據CPU利用率、正在處理的請求或其他來自應用程序的遙測信號,使用託管實例組(managed instance groups,簡稱MIG)擴展應用程序來響應可變負載

下圖說明了託管實例組提供的關鍵功能。

我在雲上試了下流行的十二要素開發方法論 2

使用託管實例組可以讓我們的應用程序擴展到傳入的需求並具有高可用性。這個概念對於無狀態應用程序(如web前端)和基於批處理、高性能的工作負載非常適用。

使用雲函數(Cloud Function )

雲函數是無狀態、單一目的的函數,它們運行在GCP上,其運行的底層架構是由谷歌為我們管理的。雲函數響應事件觸發器(如,一次到雲存儲桶的上傳或云發布/訂閱消息)。每個函數調用對單個事件或請求做出響應。

雲函數通過把傳入的請求分配給函數的實例進行處理。當入站請求量超過現有實例的數量時,雲函數可能啟動新的實例來處理請求。這種自動、完全託管的擴展行為允許雲函數並行處理很多請求,每個請求都使用函數的不同實例。

使用GKE自動擴展

有些關鍵Kubernetes結構適用於擴展進程:

  • 水平Pod自動擴展(Horizo​​ntal Pod Autoscaling,簡稱HPA)。根據標准或自定義指標,可以將Kubernetes配置為擴展運行於集群中pod的數量。當我們需要對GKE集群上的可變負載做出響應時,這很方便。以下的HPA YAML文件例子顯示瞭如何根據平均CPU利用率,通過設置最多10個pod,為部署配置擴展。
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: my-sample-web-app-hpa
  namespace: dev
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-sample-web-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  • 節點自動擴展。在需求增長的時候,我們可能需要擴展集群及容納更多pod。借助GKE,我們可以聲明性地配置我們的集群進行擴展。啟動了自動擴展後,在額外pod需要調度且現有節點無法容納它們時,GKE自動擴展節點。根據我們配置的閾值,當集群上的負載減少時,GKE還能夠縮減節點。
  • 作業。 GKE支持Kubernetes作業。作業可以廣義地定義為任務,需要運行一個或更多pod以執行該任務。該作業可能只運行一次或按時間表運行。當作業完成時,運行該作業的pod會被​​丟棄。配置該作業的YAML文件指定錯誤處理、並行處理、如何處理重啟等的細節。

9. 一次性

對於運行在雲基礎設施上的應用程序,我們應該把它們和底層基礎設施作為一次性資源來對待。我們的應用程序應該能夠處理底層基礎設施的暫時丟失,並應該能夠正常地關閉和重啟。

要考慮的主要原則包括:

  • 適用支持服務解耦功能,如狀態管理和事務數據的存儲。請參閱該文檔中前面的支持服務以獲得更多信息。
  • 在應用程序外部管理環境變量,以便在運行時使用它們。
  • 確保啟動時間最短。這意味著,我們必須決定,在使用虛擬機(如公共映像和自定義映像)時,在映像中要構建多少層。這個決定特定於每個應用程序,並且應該基於啟動腳本執行的任務。例如,如果我們在下載幾個軟件包或二進製文件,在啟動階段將它們初始化,啟動時間的很大一部分將致力於完成這些任務。
  • 使用GCP的原生功能以執行基礎設施任務。例如,我們可以在GKE中使用滾動更新,使用云密鑰管理服務(Cloud Key Management Service,簡稱Cloud KMS)來管理安全密鑰。
  • 使用SIGTERM信號(當其可用時)來啟動一個正常的關機。比如,當應用程序引擎Flex關閉一個實例時,它通常發送一個STOP(SIGTERM)信號給應用程序容器。我們的應用程序可以在容器關閉前使用該信號來執行任何清理操作。 (我們的應用程序不需要響應SIGTERM事件。)在正常情況下,系統最多等待30秒鐘以讓應用程序停止,然後發送一個KILL(SIGKILL)信號。

以下的代碼段來自一個應用引擎應用程序,展示了我們如何攔截SIGTERM信號以關閉打開的數據庫連接。

const express = require('express')
const dbConnection = require('./db')
// Other business logic related code
app.listen(PORT, () => {
  console.log('App listening on port ${PORT}')
  console.log('Press Ctrl+C to quit.')
})
process.on('SIGTERM', () => {
  console.log('App Shutting down')
  dbConnection.close()  // Other closing of database connection
})

10. 環境的等價性

企業應用程序在其開發生命週期中會跨不同的環境遷移。通常,這些環境是開發、測試和預發布(staging)以及生產環境。最好讓這些環境盡可能地相似。

環境的等價性是一個大多數開發人員認為已給定的特性。儘管如此,隨著企業的成長及其IT生態系統的發展,環境的等價性變得越來越難以維持。

由於這幾年來,開發人員採用了源碼控制、配置管理和模板化配置文件,因此,維持環境的等價性變得輕鬆了一些。這樣一來,把應用程序一致地部署到多個環境中就變得更輕鬆了。例如,使用Docker和Cocker Compose,我們可以確保應用程序堆棧跨環境保持其形狀和工具組合。

下表列出的GCP服務和工具,在我們設計運行於GCP的應用程序時可以使用它們。這些組件有不同的用途,它們共同幫助我們構建讓我們的環境更加一致的工作流。

GCP 組件 用途
雲資源存儲庫 供團隊存儲、管理和跟踪代碼的單一去處。
雲存儲、 雲資源存儲庫 存儲構建工件
雲KMS 在一個中心雲服務中存儲我們的加密密鑰,供其它雲資源和應用程序直接使用。
雲存儲 存儲自定義映像,這些映像是我們從資源磁盤、映像、截圖或存儲在雲存儲的映像中創建的。我們可以使用這些映像來創建為我們的應用程序量身製定的虛擬機實例
容器註冊表 存儲、管理和保護Docker容器映像。
部署管理器 編寫靈活的模板和配置文件,並使用它們來創建使用GCP產品變體的部署

11. 日誌

日誌讓我們能夠了解應用程序的健康狀況。對日誌的收集、處理和分析與應用程序核心邏輯的解耦是非常重要的。在我們的應用程序需要動態擴展並運行於公共雲上時,解耦日誌消除了管理日誌存儲位置和分佈式(通常是臨時性的)虛擬機進行聚合的開銷,因此特別有用。

GCP提供了一套工具,以幫助處理日誌的收集、處理和結構化分析。在我們的計算引擎虛擬機中安裝Starkdriver Logging Agent是個很好的方法。 (這個代理默認預安裝在應用程序引擎和GKE VM映像中。)該代理監控一組預先配置的日誌記錄位置。運行於虛擬機的應用程序生成的日誌將被收集並以流的形式被傳輸到Stackdriver Logging中。

當為GKE集群啟用日誌記錄時,日誌代理被部署到該集群的每個節點上。該代理收集日誌,用相關的元數據豐富日誌,並在數據存儲中將它們持久保存。可以使用Stackdriver Logging來查看這些日誌。我們可以使用Fluentd 守護程序集對記錄的內容進行更多的控制。請參閱使用Fluentd為谷歌Kubernetes引擎自定義Stackdriver日誌以獲得更多的信息。

12. 管理流程

管理流程通常包括一次性任務或定時的、可重複的任務,如生成報告、執行批處理腳本、啟動數據庫備份和遷移模式。十二要素宣言中的管理流程要素是考慮一次性任務而寫的。對於雲原生應用程序,在創建可重複的任務時,該要素變得越來越重要,本部分的指南針對的是類似任務。

定時觸發器常常是作為定時任務(cron job)構建的,並由應用程序本身來處理。該模型有用,但是,它引入了與應用程序緊密耦合的邏輯,需要維護和協調,尤其是當應用程序跨時區分佈時。

因此,在為管理流程進行設計時,應該把這些任務的管理與應用程序本身解耦。根據應用程序運行所需的工具和基礎設施,可以考慮以下建議:

  • 要在GKE上運行應用程序,請為管理任務啟動單獨的容器。我們可以利用GKE中的CronJobs。 CronJobs在臨時容器中運行,並且,我們可以控制時間、執行頻率,並且如果作業失敗或完成時間過長時,可以重試。

  • 對於託管在應用引擎或計算引擎上的應用程序,我們可以外部化觸發機制並為要調用的觸發器創建端點。與單一用途的端點相比,這種方法有助於定義應用程序的職責範圍。雲任務(Cloud Tasks)是全託管的、異步任務執行服務,我們可以用以使用應用引擎實施這個模式。我們還可以使用雲調度程序(Cloud Scheduler),這是GCP上一個企業級、全託管的調度程序,用以觸發定時操作。

十二要素以外

本文描述的十二要素為我們應該如何構建雲原生應用程序提供了指導。這些應用程序是企業的基礎構建塊。

一個典型的企業有很多這樣的應用程序,它們通常是由幾個團隊合作開發的,以交付業務功能。重要的是,在應用程序開發生命週期中建立一些其他原則(而不是事後才考慮),以解決應用程序相互之間如何通信和如何保護它們,以及如何進行訪問控制。

以下部分概述了在應用程序設計和開發過程中應考慮的一些其他問題。

API

應用程序使用API​​進行通信。當我們在構建應用程序時,要考慮應用程序的生態系統會怎樣使用該應用程序,從設計一個API策略開始。一個良好的API設計可以讓應用程序開發人員和外部利益相關者輕鬆地使用。在實現任何代碼前,使用OpenAPI規範記錄API是一個良好的習慣。

API抽象了底層的應用程序功能。一個設計良好的API端點應該把提供服務的應用程序基礎設施與在使用的應用程序隔離並解耦。這種解耦讓我們能夠獨立地改變底層服務及其基礎設施,而不會影響到應用程序的使用者。

對我們開發的API進行分類、記錄和發布是非常重要的,這樣API的使用者才能夠發現並使用這些API。理想情況下,我們希望API使用者自我服務。我們可以通過設置開發人員門戶網站來實現。開發人員門戶網站為所有API使用者作為入口點提供服務,這些API使用者包括企業內部的,或像來自合作夥伴生態系統的開發人員這樣的外部使用者。

谷歌的API管理產品套件Apigee有助於管理API的整個生命週期,從設計,構建一直到發布。

安全性

安全性的範疇很廣,包括操作系統、網絡和防火牆、數據及數據庫安全性、應用程序安全性、身份認證、訪問管理。在企業生態系統中,解決安全問題的所有方面是至關重要的。

從應用程序的角度來看,API提供對企業生態系統中應用程序的訪問。因此,我們應該確保在應用程序設計和構建過程中,這些構建塊可以解決安全問題。幫助保護對應用程序的訪問要考慮的問題如下所示:

  • 傳輸層安全性(Transport Layer Security,簡稱TLS)。使用TLS來幫助保護傳輸中的數據。我們可能想對業務應用程序使用雙向的TLS,如果我們使用服務網格(如在谷歌Kubernetes引擎上的Istio),這會更輕鬆。對於一些用例來說,基於IP地址創建允許列表和拒絕列表作為附加安全層也是常見的。傳輸安全還涉及保護我們的服務免受DDoS和機器人攻擊。
  • 應用程序和終端用戶安全性。傳輸安全性有助於為傳輸中的數據提供安全性並建立信任。但是,最佳實踐是添加應用程序級別安全性,以根據應用程序的使用者是誰來控制對應用程序的訪問。這些使用者可以是其它應用程序、員工、合作者或企業的終端用戶。我們可以使用API​​密鑰(針對消費類應用程序)、基於證書的身份驗證和授權、JSON Web令牌(JSON Web Tokens,簡稱JWTs)交換,或安全性聲明標記語言(Security Assertion Markup Language,簡稱SAML)來增強安全性。

安全環境在企業內持續地發展,使我們更難在應用程序中編寫安全性結構代碼。 API管理產品(如Apigee)有助於確保在本節中提到的所有層上的API安全

接下來要做的事

  • 查看採用了十二要素APP原則並使用GCP產品和服務構建的微服務演示應用程序
  • 審查用於日誌和監控的GCP產品套件,請參閱Stackdriver文檔
  • 嘗試谷歌云平台功能,可以參考教學視頻

原文鏈接:Twelve-factor app development on GCP