基礎設施即程式碼的過去和未來

Linksla發表於2023-05-18

基礎設施即程式碼(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= '/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。

我相信,在不久的將來,這個領域將會湧現出許多新的方法,這些方法將對我們編寫和釋出軟體的方式產生深遠的影響。

(版權歸原作者所有,侵刪)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70013542/viewspace-2952881/,如需轉載,請註明出處,否則將追究法律責任。

相關文章