基礎設施| 即代碼的過去和未來
基礎設施即代碼(IaC)是軟件開發中的一個令人著迷的領域。雖然作為一門學科它還相對年輕,但在其短暫的發展歷程中,它已經經歷了幾次具有劃時代意義的變化。我認為,它是當今軟件開發創新中最熱門的領域之一,參與者很多,從大型科技公司到年輕的初創企業,都在創造新的方法,如果完全實現,有可能徹底改變我們編寫和部署軟件的方式。
在本篇文章中,對 IaC 這一主題進行深入探討:它是什么,它能帶來什么好處,它已經經歷了哪些具有顛覆性的轉變,以及未來可能會發生什么樣的變化。
#01
什么是 IaC?
讓我們從解釋這個概念開始。基礎設施即代碼是一個涵蓋一系列實踐和工具的術語,旨在將應用程序開發中的嚴謹性和經驗應用到基礎設施供應和維護的領域。
這里的 “基礎設施” 是故意模糊的,但我們可以把它定義為在環境中運行一個特定的應用程序所需要的一切,但這并不是應用程序本身的一部分。一些常見的例子包括:服務器、配置、網絡、數據庫、存儲等等。在本文后面我們還會看到更多的例子。
IaC 的實踐與運行時代碼的實踐相呼應。這些實踐包括:使用源代碼控制進行版本管理、自動化測試、持續集成/持續交付(CI/CD)部署流程、快速反饋的本地開發等。
遵循 IaC 的實踐可以帶來以下好處:
性能:如果需要提供或更改大量基礎設施,IaC 將始終比人工手動執行相同操作更快。
可重復性:人類在可靠地重復執行相同任務方面往往表現不佳。如果我們需要重復進行一百次相同的操作,很可能會分心并在過程中出錯。IaC 不會受到這個問題的影響。
文檔化:你的 IaC 可以作為你系統結構的文檔。當維護系統的團隊規模擴大時,這就變得至關重要了 —— 你不希望依賴于部落知識,或者只有少數幾個團隊成員了解系統基礎設施的工作原理。最重要的是,與傳統文檔不同,這份文檔永遠不會過時。
審計歷史:有了 IaC,由于你對 IaC 的版本控制與你的應用代碼相同(有時被稱為 GitOps),它為你提供了歷史記錄,你可以查看你的基礎設施是如何隨時間變化的,如果任何變化導致問題,有辦法回滾到一個安全點。
可測試性:IaC 可以像應用程序代碼一樣進行測試。你可以對其進行單元測試、集成測試和端到端測試。
接下來,讓我們談談 IaC 工具在實踐開始以來所經歷的主要階段。
#02
第一代:聲明式,主機配置
代表:Chef、Puppet、Ansible
第一代 IaC 工具都是關于主機配置的。這很有意義,因為軟件系統的基礎設施,在其最低的抽象層次上,由單個機器組成。因此,這個領域的第一批工具集中在配置這些機器上。
這些工具管理的基礎設施資源是 Unix 中熟悉的概念:文件、來自 Apt 或 RPM 等軟件包管理器的 users、groups、permissions、init services 等等。
下面是一個創建 Java 服務的 Ansible playbook 例子:
- hosts: app
tasks:
- name: Update apt-get
apt: update_cache=yes- name: Install Apache
apt: name=apache2 state=present- name: Install Libapache-mod-jk
apt: name=libapache2-mod-jk state=present- name: Install Java
apt: name=default-jdk state=present- name: Create Tomcat node directories
file: path=/etc/tomcat state=directory mode=0777
- file: path=/etc/tomcat/server state=directory mode=0775- name: Download Tomcat 7 package
get_url: url=http://apache.mirror.digionline.de/tomcat/tomcat-7/v7.0.92/bin/apache-tomcat-7.0.92.tar.gz dest='/etc/tomcat'
- unarchive: src=/etc/tomcat/apache-tomcat-7.0.92.tar.gz dest=/etc/tomcat/server copy=no- name: Configuring Mod-Jk & Apache
replace: dest=/etc/apache2/sites-enabled/000-default.conf regexp='^</VirtualHost>' replace="JkMount /status status \n JkMount /* loadbalancer \n JkMountCopy On \n </VirtualHost>"- name: Download sample Tomcat application
get_url: url=https://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war dest='/etc/tomcat/server/apache-tomcat-7.0.92/webapps' validate_certs=no- name: Restart Apache
service: name=apache2 state=restarted- name: Start Tomcat nodes
command: nohup /etc/tomcat/server/apache-tomcat-7.0.92/bin/catalina.sh start
本操作手冊的抽象層次是一臺以 Linux 為操作系統的單一計算機。我們聲明我們想要安裝的 Apt 軟件包,我們想要創建的文件(創建它們的方式有多種:直接在給定的路徑下創建目錄,從給定的 URL 下載,從存檔中提取文件,或根據正則表達式替換編輯現有文件),我們想要運行的系統服務或命令等等。
實際上,如果你稍微看一下,你會發現這個 playbook 與 Bash 腳本非常相似。主要的區別是,playbook 是聲明式的 —— 它描述了它希望發生的事情,比如在機器上安裝給定的 Apt 軟件包。這與腳本不同,腳本包含要執行的命令。
雖然這個區別很小,但它很重要;它使 playbook 具有冪等性,這意味著,即使它在中間某個地方失敗了(也許 tomcat。apache。org 有暫時的故障,導致從它那里的下載失敗),你可以重新啟動它,之前成功執行的步驟會識別到這一點,并在不做任何事情的情況下通過,這通常不是 Bash 腳本的情況。
現在,這些工具對于推進軟件開發行業的發展起著至關重要的作用,不可忽視。但是,它們只能在單個主機層面上運行,這有巨大的局限性。這就意味著你不得不手動管理這些主機,這在很大程度上抵消了 IaC 所帶來的好處,或者你需要將這些工具與能夠管理主機的其他工具結合使用,比如用于本地開發的 Vagrant,或者用于管理共享環境(如生產環境)的 OpenStack。
舉個例子,如果你想創建一個經典的三層架構,你需要創建三種類型的虛擬機,每種類型的虛擬機都有自己的 Ansible playbook,根據它們在架構中的角色來配置這些主機。
IaC 工具的下一階段將擺脫這種限制。
#03
第二代:聲明式,云計算
代表:CloudFormation、Terraform、Azure Resource Manager
2000 年代中期,云計算的引入是軟件開發史上的一個里程碑事件。在許多方面,我認為我們仍在深度消化它所帶來的革命性影響。
突然間,主機管理的諸多問題得到了解決。你不需要運行和操作你自己的 OpenStack 集群來自動管理虛擬機;云供應商將為你處理這一切。
但更重要的是,云計算立即提高了我們設計系統的抽象水平。不再只是給主機分配不同的角色那么簡單。
如果你需要發布 - 訂閱資源,那么就沒有必要去配置一臺虛擬機,并在其上安裝 Apt 的 ZeroMQ 包;相反,你可以直接使用 Amazon SNS。如果你想存儲一些文件,你也不需要指定一堆主機作為你的存儲層;相反,你可以創建一個 S3 存儲桶。諸如此類,不一而足。
我們進入了配置管理服務的階段,而不再是將主機配置置于首要位置。由于上一代的工具被設計成只在單個主機的層面上工作,因此我們需要一種全新的方法。
為了解決這個問題,像 CloudFormation 和 Terraform 這樣的工具應運而生。它們和第一代工具一樣,都采用聲明式設計;但不同之處在于,它們操作的抽象層級不再是單一機器上的文件和軟件包,而是各種屬于不同托管服務的獨立資源,以及這些資源的屬性和它們之間的相互關系。
例如,這里有一個 CloudFormation 模板,定義了一個由 SQS 隊列觸發的 AWS Lambda 函數:
AWSTemplateFormatVersion : 2010-09-09
Resources:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: my-source-bucket
S3Key: lambda/my-java-app.zip
Handler: example.Handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: java17
Timeout: 60
MemorySize: 512
MyQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 120
LambdaFunctionEventSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
BatchSize: 10
Enabled: true
EventSourceArn: !GetAtt MyQueue.Arn
FunctionName: !GetAtt LambdaFunction.Arn
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: allowLambdaLogs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:*
Resource: arn:aws:logs:*:*:*
- PolicyName: allowSqs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
- sqs:ChangeMessageVisibility
Resource: !GetAtt MyQueue.Arn
這個 CloudFormation 模板與我們之前看到的 Ansible playbook 差別很大。它并未提及任何文件、程序包或初始化服務;而是使用了托管服務的語言。我們配置的資源類型是AWS::Lambda::Function和AWS::SQS::Queue。我們并未定義這些服務將在何處運行,也未定義如何配置這些主機 —— 我們所關心的是,云供應商所提供的托管服務能否被正確使用。
然而,它與 Ansible 的共同點在于其聲明性質。我們不需要編寫對 SQS API 的調用來創建一個隊列 —— 我們只需要聲明我們需要一個隊列,并將 VisibilityTimeout 屬性設置為 120,部署引擎(在這個例子中是 CloudFormation)會負責確定需要調用哪些 AWS API 來實現這個目標。如果我們后來決定修改隊列(比如我們想將超時時間設為 240,而不是 120),或者完全刪除它,我們只需修改模板,引擎便會自動找出需要的 API 調用來更新或者刪除隊列。
這些工具是 IaC 發展過程中的一個巨大的里程碑,這大大提升了前一代的抽象水平。然而,它們也存在一些缺陷。
第一個問題是,為了實現其聲明性質,這些工具使用了自定義的 DSL(領域特定語言),例如,在 CloudFormation 中,這種語言可能是 JSON 或 YAML 格式。這就意味著所有的通用編程語言功能,比如變量、函數、循環、if 語句、類等,在這種 DSL 中都無法使用。因此,沒有簡單的辦法來減少重復代碼。
舉個例子,如果我們想要在我們的應用中配置不止一個,而是三個具有相同設置的隊列,我們無法簡單地編寫一個循環來執行三次;我們必須把相同的定義復制和粘貼三次,這并不理想。同時,這也意味著我們無法將模板劃分為邏輯單元;我們無法將一部分資源指定為存儲層,另一部分資源指定為前端層等。所有的資源都屬于一個扁平的命名空間。
這些工具的另一個問題是,雖然它們肯定比第一代的主機配置更高級,但它們仍然需要你詳細指定在系統中使用的所有資源的所有細節。例如,你可能已經注意到,在上面的模板示例中,除了我們主要關注的 Lambda 和 SQS 資源,我們還有事件映射和 IAM 資源。這是連接 SQS 和 Lambda 所需的 “粘合劑”,而正確配置這些 “粘合劑” 資源并非易事。
舉例來說,你需要向執行函數的 IAM 角色授予一組非常特定的權限(sqs:ReceiveMessage、sqs:DeleteMessage、sqs:GetQueueAttributes 和 sqs:ChangeMessageVisibility),才能成功地從特定隊列觸發它。
從某種程度上來說,這是一個非常低級的問題;然而,由于 DSL 中缺乏抽象工具,我們實際上沒有任何工具可以隱藏這些實現細節。所以,每次你需要創建一個由 SQS 隊列觸發的新 Lambda 函數,你別無選擇,只能復制包含這四個權限的代碼段。因此,這些模板往往會很快變得冗長,并包含大量重復內容。
#04
第三代:命令式,云計算
代表:AWS CDK、Pulumi、SST
例如,讓我們看看相當于上述 CloudFormation 模板的云開發工具包程序(在這個例子中我將使用 TypeScript,但任何其他 CDK 支持的語言看起來都非常相似):
第二代工具的所有缺陷都可以追溯到它們使用了一種自定義的 DSL,這種語言缺乏我們在使用通用編程語言時習慣的抽象工具,如變量、函數、循環、類、方法等。
因此,第三代 IaC 工具的主要思想非常簡單:如果通用編程語言已經具備了這些功能,那么我們為什么不使用它們來定義基礎設施,而要使用自定義的 JSON 或 YAML DSL?
例如,讓我們看看相當于上述 CloudFormation 模板的云開發工具包程序(在這個例子中我將使用 TypeScript,但任何其他 CDK 支持的語言看起來都非常相似):
class LambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);const func = new lambda.Function(this, 'Function', {
code: lambda.Code.fromBucket(
s3.Bucket.fromBucketName(this, 'CodeBucket', 'my-source-bucket'),
'lambda/my-java-app.zip'),
handler: 'example.Handler',
runtime: lambda.Runtime.JAVA_17,
});const queue = new sqs.Queue(this, 'Queue', {
visibilityTimeout: cdk.Duration.minutes(2),
});func.addEventSource(new lambda_events.SqsEventSource(queue));
}
}const app = new cdk.App();
new LambdaStack(app, 'LambdaStack');
這個 CDK 代碼的第一個有趣之處在于,它比其對應的 CloudFormation 模板要短得多 —— 大約 20 行 TypeScript,而 YAML 大約有 60 行,所以大概是 3 比 1 的比例。這是一個非常簡單的例子;當你的基礎設施越來越復雜時,這個比例就會越來越大 —— 我見過有些情況下比例高達 30 比 1。
其次,CDK 代碼的級別比 CloudFormation 模板要高得多。請注意,如何從隊列中觸發函數的細節被 addEventSource() 方法和 SqsEventSource 類優雅地封裝了起來。這兩個 API 都是類型安全的 —— 你不能錯誤地將一個 SNS 主題傳遞給 SqsEventSource,因為編譯器不允許這樣。
還請注意,我們不必在代碼中的任何地方提到 IAM —— CDK 為我們處理了所有這些細節,所以我們不必知道需要哪 4 個確切的權限來允許一個函數被隊列觸發。
所有這些都是因為高級編程語言允許我們構建抽象概念。我可以把一段重復的或復雜的代碼,放在一個類或函數中,并為我的項目提供一個干凈、簡單的 API,這個 API 巧妙地封裝了所有混亂的實現細節,就像 CDK 團隊創建和維護的 SqsEventSource 類那樣。
如果這是其他項目可能受益的東西,我可以把我的抽象概念打包成它所使用的編程語言的庫,并通過我的語言的包管理器分發出去,比如 JavaScript/TypeScript 的 npmjs.com,或 Java 的 Maven Central,這樣其他人就可以依賴它,就像我們分發應用程序代碼的庫一樣。我甚至可以把它添加到 constructs.dev 的可用開源 CDK 庫目錄中,這樣就更容易找到它。
#05
第四代:Infrastructure from Code
代表:Wing、Dark、Eventual、Ampt、Klotho
雖然第三代 IaC 工具是一個巨大的飛躍,使云計算更容易被使用(我在這里可能有偏見,因為我是 AWS 的 CDK 團隊的前成員,但我認為這種說法很接近事實),但它們仍然有改進的空間。
他們的第一個缺點是,它們在很大程度上是在單個云服務的層面上運作的。因此,雖然他們使使用 Lambda 或 SQS 變得很容易,但你仍然需要知道這些服務是什么,以及為什么你會考慮使用它們。
現在是云計算時代,我們已經看到每個供應商提供的服務數量激增。僅 AWS 就有 200 多種。在如此多樣化的選擇中,選擇適合自己要求的服務變得越來越難。我應該在 AWS Lambda、AWS EKS 或 AWS AppRunner 上運行我的容器嗎?我應該使用 Google Cloud Functions 還是 Google Cloud Run?在什么情況下,這一個比那一個更適合?
大多數開發人員對每個云計算供應商的產品沒有特別詳細的了解,特別是由于這些產品往往經常變化,新的服務(或現有服務的新功能)不斷推出,舊的服務被淘汰。但他們確實對系統設計的基本原理有很好的理解。
因此,他們知道他們需要一個無狀態的 HTTP 服務,在負載均衡器后面進行水平擴展,一個 NoSQL 文檔存儲,一個緩存層,一個靜態網站前端,等等。第三代的工具對他們來說太低級了;理想情況下,他們希望用這些高級別的系統架構術語來描述他們的基礎設施,然后將如何在給定的云供應商上最好地實現這種架構的細節委托給他們的 IaC 工具。
第三代工具的第二個缺點是,它們將 IaC 與應用程序代碼完全分開。例如,在上面的 CDK 的例子中,Lambda 函數的代碼與它的基礎設施定義完全脫節。而且,雖然 CDK 有資產的概念,允許這兩種類型的代碼在同一個版本控制倉庫中存在,但它們仍然不能相互對接。從某種意義上說,這就是重復 —— 我的應用程序代碼使用了 SQS 隊列,這對我的 IaC 提出了一個隱含的要求,即正確配置該隊列。
但是,就像所有的重復和隱含要求一樣,當雙方意外地不同步時(例如,如果我從我的基礎設施代碼中刪除了隊列,但忘記更新我的應用程序代碼以不再使用它),這可能會導致問題,而且在我部署我的更改之前,我的語言的編譯器并不能幫助我捕獲這些錯誤,可能會引發問題。
第四代 IaC 工具的目標是解決上述兩個問題。它們的主要理念是,在云計算時代,基礎設施代碼和應用程序代碼之間的區別已經變得沒有太大意義。因為兩者都在使用托管服務的語言,我在應用程序代碼中想使用的任何資源,都需要在我的基礎設施代碼中存在,就像我們在 Lambda 和 SQS 的例子中看到的一樣。
因此,這些工具將兩者統一起來。它們不再是獨立的基礎設施和應用程序代碼,而是消除了前者,只保留了應用程序代碼,而基礎設施則完全來自應用程序代碼。由于這個原因,這種方法被稱為 Infrastructure from Code,而不是 Infrastructure as Code。
讓我們來看看 IfC 工具的兩個例子。
Eventual
第一個是 Eventual,一個 TypeScript 庫,它定義了現代云應用的幾個通用構建模塊:Service、API、Workflow、Task、Event 以及其他一些東西。你可以從這些通用構件中創建一個任意復雜的應用程序,把它們組合在一起,就像樂高積木一樣。
Eventual 部署引擎知道如何將這些構建模塊轉換為 AWS 資源,如 Lambda 函數、API 網關、StepFunction 狀態機、EventBridge 規則等。這種轉換的細節被庫的抽象所隱藏,因此,作為它的用戶,你無需關心這些細節 —— 你只需使用所提供的構件模塊,部署由庫處理。
下面是一個簡單的例子,顯示 Event、Subscription、Task、Workflow 和 API:
import { event, subscription, task, workflow, command } from "@eventual/core";// define an Event
export interface HelloEvent {
message: string;
}
export const helloEvent = event<HelloEvent>("HelloEvent");// get notified each time the event is emitted
export const onHelloEvent = subscription("onHelloEvent", {
events: [helloEvent],
}, async (event) => {
console.log("received event:", event);
});// a Task that formats the received message
export const helloTask = task("helloTask", async (name: string) => {
return `hello ${name}`;
});// an example Workflow that uses the above Task
export const helloWorkflow = workflow("helloWorkflow", async (name: string) => {
// call the Task to format the message
const message = await helloTask(name);// emit an Event, passing it some data
await helloEvent.emit({
message,
});return message;
});// create a REST API for POST /hello <name>
export const hello = command("hello", async (name: string) => {
// trigger the above Workflow
const { executionId } = await helloWorkflow.startExecution({
input: name,
});return { executionId };
});
Wing
另一種方法是創建一個全新的通用編程語言,該語言不僅僅在單臺機器上執行,而是從一開始就設計成在云上分布式運行。Wing 就是由 Monada 公司創建的一種這樣的語言,該公司的聯合創始人是 AWS CDK 的創建者 Elad Ben-Israel。
Wing 通過引入執行階段的概念成功地將基礎設施代碼和應用程序代碼合并在一起。默認情況下,Preflight 對應于 “構建時間”,在這個階段執行基礎設施代碼;Inflight 對應于 “運行時間”,應用程序代碼在云上運行。
通過 Wing 編譯器實現的復雜的引用機制,Inflight 代碼可以使用 Preflight 代碼中定義的對象。然而,Inflight 階段不能創建新的 Preflight 對象,只能使用這些對象明確標有修飾符的特定 API Inflight。Wing 編譯器會確保你的程序遵守這些規則,所以如果你試圖破壞這些規則,它就會編譯失敗,并為你快速提供關于應用程序正確性的反饋。
因此,我們上面看到的那個由隊列觸發的無服務器函數的例子,在 Wing 中看起來會是下面這樣的:
bring cloud;let queue = new cloud.Queue(timeout: 2m);
let bucket = new cloud.Bucket();queue.addConsumer(inflight (item: str): str => {
// get an item from the bucket with the name equal to the message
let object = bucket.get(item);
// do something with 'object'...
});
這段代碼相當高級 —— 我們甚至沒有在任何地方明確提到無服務器函數資源,我們只是在一個匿名函數中寫了我們的應用代碼,用 Inflight 修改器進行了注釋。該匿名函數被部署在無服務器函數中,并在云上執行(或在 Wing 附帶的本地模擬器中執行,以提供快速開發體驗)。
還要注意的是,我們不能在應用代碼中錯誤地使用錯誤的資源。例如,錯誤地使用 SNS 主題而不是 SQS 隊列,因為在 Preflight 的代碼中沒有定義 Topic 對象,所以我們沒有辦法在 Inflight 的代碼中引用它。同樣,你也不能在 Preflight 的代碼中使用 bucket.get() 方法,因為那是一個 Inflight 的專用 API。這樣一來,語言本身就可以防止我們犯很多錯誤,如果基礎設施和應用代碼是分開的,這些錯誤就不會被發現。
如果你想了解更多關于 Infrastructure from Code 的新趨勢,我推薦這篇來自 Ala Shiban 的文章,他是這個領域另一個工具 Klotho 的聯合創始人。
https://klo.dev/state-of-infrastructure-from-code-2023
#06
總結
這就是 IaC 領域的歷史和最新發展。小編認為這值得密切關注,因為它是當今軟件工程中最熱門的領域之一,甚至在一些產品中還將最新的人工智能進展納入其中,比如 EventualAI 和 Pulumi Insights。
相信在不久的將來,這個領域將會涌現出許多新的方法,這些方法將對我們編寫和發布軟件的方式產生深遠的影響。