返回文章列表

Azure 映像自動化:Packer 模板與 Ansible 整合實踐

本文深入探討如何運用 Packer 自動化建構 Azure 虛擬機映像。內容從準備 Azure 服務主體開始,詳細解析 Packer 模板中的 builders、variables 與 provisioners 區塊,並展示如何使用 shell 配置器安裝軟體及執行通用化。

DevOps 基礎架構即程式碼

description: “本文深入探討如何運用 Packer 自動化建構 Azure 虛擬機映像。內容從準備 Azure 服務主體開始,詳細解析 Packer 模板中的 builders、variables 與 provisioners 區塊,並展示如何使用 shell 配置器安裝軟體及執行通用化。進一步地,文章闡述了整合 Ansible…” 在現代雲端維運與 DevOps 實踐中,標準化且不可變的「黃金映像檔」(Golden Image)是確保環境一致性與加速部署的關鍵基石。傳統手動建構流程不僅耗時,且容易導致組態漂移(Configuration Drift)。本文旨在闡述如何透過基礎架構即程式碼(IaC)工具 Packer,建立一套可重複的映像建構管線。文章從基礎的 Packer 模板結構切入,說明如何定義 Azure 平台上的建構器與變數,並透過 Shell 腳本進行基礎配置。接著,將探討如何整合 Ansible 這類成熟的組態管理工具,將既有的維運腳本無縫融入映像檔的生命週期管理,實現從程式碼到可部署映像的端到端自動化。

Azure 映像構建實踐:Packer 模板詳解

在掌握了 Packer 模板的各個組件後,我們現在將實際構建一個用於在 Azure 環境中創建 Linux 虛擬機映像的模板。這個過程需要先準備 Azure 服務主體 (Service Principal),然後編寫包含 buildersvariablesprovisioners 的 Packer 模板文件。

步驟一:準備 Azure 服務主體 (Service Principal)

為了讓 Packer 能夠在您的 Azure 訂閱中創建資源(如虛擬機、映像),需要一個具有相應權限的服務主體。這個過程與為 Terraform 配置 Azure 環境類似。

  1. 創建服務主體: 使用 Azure CLI 或 Azure 入口網站創建一個服務主體。建議為其分配最低必要的權限,例如「參與者」(Contributor) 角色,並將其作用域限定在特定的資源組或訂閱。
  2. 獲取認證資訊: 創建服務主體後,您需要獲取以下關鍵資訊,這些資訊將用於 Packer 模板中的變數:
    • client_id (應用程式 ID)
    • client_secret (密碼)
    • subscription_id (訂閱 ID)
    • tenant_id (租戶 ID)

步驟二:創建 Packer 模板文件 (JSON 格式)

我們將創建一個名為 azure_linux.json 的文件,並開始編寫 Packer 模板。

1. builders 區塊:定義 Azure 構建目標

此區塊指定了映像構建的目標平台為 Azure,並配置了連接和映像屬性。

{
  "builders": [
    {
      "type": "azure-arm",
      "client_id": "{{user 'clientid'}}",
      "client_secret": "{{user 'clientsecret'}}",
      "subscription_id": "{{user 'subscriptionid'}}",
      "tenant_id": "{{user 'tenantid'}}",
      "os_type": "Linux",
      "image_publisher": "Canonical",
      "image_offer": "UbuntuServer",
      "image_sku": "18.04-LTS",
      "location": "West Europe",
      "vm_size": "Standard_DS2_v3",
      "managed_image_resource_group_name": "{{user 'resource_group'}}",
      "managed_image_name": "{{user 'image_name'}}-{{user 'image_version'}}",
      "azure_tags": {
        "version": "{{user 'image_version'}}",
        "role": "WebServer"
      }
    }
  ],
  // ... variables 和 provisioners 區塊將在此處添加 ...
}

區塊解析:

  • type: "azure-arm": 指定使用 Azure Resource Manager 的構建器。
  • 認證變數: client_id, client_secret, subscription_id, tenant_id 都通過 {{user '...'}} 引用變數,這些變數將在執行時提供,以確保安全。
  • 基礎映像: os_type, image_publisher, image_offer, image_sku 定義了基礎的 Ubuntu 18.04 LTS 映像。
  • 部署設定: location 指定了映像創建的 Azure 區域,vm_size 設定了臨時虛擬機的大小。
  • 映像命名與位置: managed_image_resource_group_name 指定了最終映像將被存儲的資源組,managed_image_name 則結合了映像名稱和版本變數來生成唯一的映像名稱。
  • 標籤 (azure_tags): 為創建的映像附加了元數據標籤,便於管理和識別。
2. variables 區塊:定義動態參數

此區塊定義了模板中使用的變數及其預設值或來源。

{
  // ... builders 區塊 ...
  "variables": {
    "subscriptionid": "{{env 'AZURE_SUBSCRIPTION_ID'}}",
    "clientid": "{{env 'AZURE_CLIENT_ID'}}",
    "clientsecret": "{{env 'AZURE_CLIENT_SECRET'}}",
    "tenantid": "{{env 'AZURE_TENANT_ID'}}",
    "resource_group": "rg_images",
    "image_name": "linuxWeb",
    "image_version": "0.0.1"
  },
  // ... provisioners 區塊 ...
}

變數說明:

  • 認證變數: subscriptionid, clientid, clientsecret, tenantid 通過 {{env '...'}} 引用環境變數。這意味著您需要在運行 Packer 前設置這些環境變數。
  • 映像屬性變數:
    • resource_group: 指定了映像將被創建的 Azure 資源組名稱。
    • image_name: 設定了映像的基本名稱。
    • image_version: 設定了映像的版本號。 這些變數的組合(image_name-image_version)用於動態生成最終的映像名稱,方便版本管理。
3. provisioners 區塊:配置和加固映像

此區塊定義了在臨時虛擬機上執行的腳本,用於安裝必要的軟體並進行系統配置。

{
  // ... builders 和 variables 區塊 ...
  "provisioners": [
    {
      "type": "shell",
      "execute_command": "sudo sh -c '{{ .Vars }} {{ .Path }}'",
      "inline": [
        "apt-get update",
        "apt-get -y install nginx"
      ]
    },
    {
      "type": "shell",
      "execute_command": "sudo sh -c '{{ .Vars }} {{ .Path }}'",
      "inline": [
        "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
      ]
    }
  ]
}

配置器說明:

  • 第一個 shell 配置器:
    • apt-get update: 更新 Ubuntu 的套件列表。
    • apt-get -y install nginx: 自動安裝 Nginx Web 伺服器。
  • 第二個 shell 配置器:
    • 執行 Linux 映像的通用化步驟,包括運行 waagent 進行去配置和用戶移除,清空命令歷史記錄,並確保數據同步。

執行 Packer 構建

在完成 azure_linux.json 文件的編寫後,您需要設置必要的環境變數,然後執行 Packer 命令來構建映像。

  1. 設置環境變數:

    export AZURE_SUBSCRIPTION_ID="your_subscription_id"
    export AZURE_CLIENT_ID="your_client_id"
    export AZURE_CLIENT_SECRET="your_client_secret"
    export AZURE_TENANT_ID="your_tenant_id"
    
  2. 執行 Packer 命令:

    packer init azure_linux.json
    packer build azure_linux.json
    

    packer init 會下載所需的插件(例如 azure-arm 插件)。packer build 命令將啟動構建過程。

Packer 會按照模板中的定義,在 Azure 中創建一個臨時虛擬機,執行配置腳本,然後將其轉換為一個名為 linuxWeb-0.0.1(或您指定的其他名稱)的託管映像,並存儲在 rg_images 資源組中。

視覺化模板結構

以下圖示展示了 Packer 模板的主要結構及其關聯性。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:Packer 模板 (JSON/HCL);  :builders 區塊;
  :定義目標平台 (Azure);
  :配置認證 (Service Principal);
  :指定基礎映像 (Ubuntu);
  :設定映像屬性 (位置, 大小, 名稱);

  :variables 區塊;
  :定義動態參數;
  :連結至環境變數或預設值;
  :用於 builders 和 provisioners;

  :provisioners 區塊;
  :執行配置腳本;
  :安裝軟體 (Nginx);
  :執行通用化 (waagent);

:Packer 執行構建;
:創建臨時 VM;
:應用 provisioners;
:創建最終映像;

stop

@enduml

看圖說話:

此圖示描繪了 Packer 模板的核心結構及其在映像構建過程中的作用。頂部代表了 Packer 模板本身,它被劃分為三個主要部分:buildersvariablesprovisionersbuilders 區塊負責定義映像構建的目標環境,例如 Azure 雲端,並配置連接到該平台的必要資訊,同時指定基礎映像的來源和最終映像的屬性。variables 區塊則扮演著參數化的角色,它允許我們定義可變的值,這些值可以從環境變數或命令列參數中獲取,從而使模板更具彈性,並用於動態配置 buildersprovisioners 中的參數。provisioners 區塊是執行實際配置操作的地方,它包含一系列腳本或命令,用於在臨時虛擬機上安裝軟體、進行系統設置,並執行關鍵的通用化步驟,以確保映像的可重用性。當 Packer 執行時,它會依序讀取這些配置,創建一個臨時的虛擬機,應用 provisioners 中的所有步驟,然後將其轉換為一個最終的、可部署的映像。

Packer 模板整合 Ansible:實現自動化映像構建

在之前的探討中,我們了解了如何使用 Packer 的 shellwindows-shell 配置器來執行腳本,從而完成映像的配置和通用化。然而,對於更複雜的配置管理任務,直接使用腳本可能會變得繁瑣。Packer 提供了與 Ansible 等配置管理工具整合的能力,這能極大地簡化映像構建過程,並提高一致性。

為什麼選擇 Ansible?

  • 重用現有腳本: 如果您已經在使用 Ansible 來配置虛擬機,那麼您可以直接重用現有的 Ansible Playbook 來創建映像,無需重新編寫腳本,節省了大量時間和精力。
  • 聲明式配置: Ansible 以其聲明式的語法,使得配置過程更易於理解和維護。
  • 強大的生態系統: Ansible 擁有豐富的模組和龐大的社群支持,能夠處理各種複雜的配置任務。

步驟一:編寫 Ansible Playbook

首先,我們需要一個 Ansible Playbook 來定義映像的配置。這個 Playbook 的目標是安裝 Nginx 並啟動其服務。

Ansible Playbook 範例 (nginx_playbook.yml):

---
- hosts: 127.0.0.1
  become: true
  connection: local
  tasks:
    - name: Installing Nginx latest version
      apt:
        name: nginx
        state: latest

    - name: Starting Nginx service
      service:
        name: nginx
        state: started

Playbook 說明:

  • hosts: 127.0.0.1: 指定 Playbook 在本地主機上運行。在 Packer 的上下文中,這意味著 Playbook 將在 Packer 構建過程中創建的臨時虛擬機上執行。
  • become: true: 啟用特權提升(類似於 sudo),以便執行需要 root 權限的任務。
  • connection: local: 指示 Ansible 使用本地連接模式。
  • tasks:
    • 第一個任務使用 apt 模組安裝最新版本的 Nginx。
    • 第二個任務使用 service 模組確保 Nginx 服務正在運行。

與獨立 VM 配置的差異:

與為獨立虛擬機編寫的 Playbook 相比,用於 Packer 映像構建的 Playbook 通常會有以下變化:

  • 無 Inventory: Packer 負責管理臨時虛擬機的生命週期,因此不需要單獨的 Ansible Inventory 文件。
  • hosts: 127.0.0.1: 如上所述,直接指向本地執行。
  • 任務精簡: 通常只包含映像構建所需的配置任務,例如軟體安裝和基本配置,而排除與特定部署相關的任務(如數據庫安裝、用戶創建等)。

步驟二:整合 Ansible Playbook 到 Packer 模板

現在,我們將修改 Packer 模板的 provisioners 區塊,使其能夠調用我們編寫的 Ansible Playbook。

Packer 模板 (azure_linux_ansible.json) 的 provisioners 區塊:

{
  // ... builders 和 variables 區塊 (與之前 JSON 範例類似) ...
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "echo 'Waiting for cloud-init to finish...'",
        "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting...'; sleep 1; done;",
        "echo 'Cloud-init finished.'"
      ]
    },
    {
      "type": "ansible",
      "playbook_file": "./nginx_playbook.yml",
      "user": "packer", // 指定連接臨時 VM 的用戶
      "extra_arguments": [
        "--extra-vars", "ANSIBLE_HOST_KEY_CHECKING=False"
      ]
    },
    {
      "type": "shell",
      "inline": [
        "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
      ]
    }
  ]
}

provisioners 區塊解析:

  1. shell (等待 cloud-init): 在某些雲端環境中,cloud-init 會在首次啟動時執行一些初始化任務。在運行 Ansible 之前,最好確保 cloud-init 已完成,以避免潛在的衝突。
  2. ansible 配置器:
    • type: "ansible": 指定使用 Ansible 配置器。
    • playbook_file: "./nginx_playbook.yml": 指向我們編寫的 Ansible Playbook 文件。Packer 會將此 Playbook 上傳到臨時虛擬機並執行。
    • user: "packer": 指定用於 SSH 連接到臨時虛擬機的用戶名。
    • extra_arguments: 可以傳遞額外的參數給 Ansible 命令,例如禁用主機密鑰檢查 (ANSIBLE_HOST_KEY_CHECKING=False),這在自動化構建中很常見。
  3. shell (通用化): 在 Ansible 配置完成後,執行 Linux 映像的通用化步驟,與之前使用純 shell 配置器時的步驟相同。

HCL 格式的整合

在 HCL 格式的模板中,整合 Ansible 的方式也非常直觀。

# ... source 區塊 ...

build {
  sources = ["source.azure-rm.azurevm"]

  provisioner "shell" {
    inline = [
      "echo 'Waiting for cloud-init to finish...'",
      "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting...'; sleep 1; done;",
      "echo 'Cloud-init finished.'"
    ]
  }

  provisioner "ansible" {
    playbook_file = "./nginx_playbook.yml"
    user          = "packer"
    extra_arguments = [
      "--extra-vars", "ANSIBLE_HOST_KEY_CHECKING=False"
    ]
  }

  provisioner "shell" {
    inline = [
      "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
    ]
  }
}

執行與驗證

完成 Packer 模板和 Ansible Playbook 的編寫後,執行步驟與之前類似:

  1. 設置環境變數: 確保 Azure 認證的環境變數已設置。
  2. 執行 Packer 命令:
    packer init azure_linux_ansible.json
    packer build azure_linux.json
    

Packer 會啟動構建流程,創建臨時虛擬機,運行 cloud-init 等待腳本,然後調用 Ansible Playbook 來安裝和配置 Nginx,最後執行通用化步驟並生成最終的 Azure 映像。

視覺化 Ansible 整合流程

以下圖示展示了 Packer 如何調用 Ansible Playbook 來配置映像。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:Packer 啟動構建;
:創建臨時 Azure VM;

:執行 Provisioner 1 (Shell - 等待 cloud-init);

:執行 Provisioner 2 (Ansible);
:Packer 傳遞 Playbook 路徑;
:Ansible 在臨時 VM 上執行;
:安裝 Nginx;
:啟動 Nginx 服務;

:執行 Provisioner 3 (Shell - 通用化);
:移除用戶資訊;
:清空歷史記錄;
:同步磁碟;

:Packer 將 VM 轉換為映像;
:映像部署至 Azure 資源組;

stop

@enduml

看圖說話:

此圖示清晰地描繪了使用 Packer 整合 Ansible 來構建 Azure 映像的流程。整個過程始於 Packer 啟動,並在 Azure 中創建一個臨時虛擬機。接著,Packer 依序執行定義在 provisioners 區塊中的三個步驟。第一步是一個 shell 腳本,用於等待雲端初始化完成,確保系統準備就緒。第二步是核心的 Ansible 配置階段,Packer 將指定的 Ansible Playbook(例如 nginx_playbook.yml)傳遞給 Ansible 執行器,Ansible 在臨時虛擬機上運行,根據 Playbook 的指示安裝 Nginx 並啟動其服務。完成配置後,第三步是另一個 shell 腳本,執行映像的通用化操作,清除臨時用戶資訊和配置,為映像的部署做好準備。最後,Packer 將完成配置的臨時虛擬機轉換為一個可在 Azure 中部署的映像,並將其存儲在指定的資源組中。

縱觀現代管理者的多元挑戰,將映像檔自動化建構的演進路徑,從單純的 shell 腳本提升至整合 Ansible 的模式,其核心價值不僅在於技術效率的提升,更深刻地反映了團隊從「單點執行」邁向「系統化治理」的思維躍遷。相較於 shell 腳本在初期部署的直觀便利,其在複雜環境下容易形成難以維護的技術債;而整合 Ansible 則代表一種策略性突破,成功將「建構機制」與「配置邏輯」解耦。這種分離不僅大幅提升了配置腳本的重用性與可維護性,更實現了基礎設施即程式碼(IaC)實踐的深度整合,將既有的配置管理投資轉化為可持續的競爭優勢。

展望未來 2-3 年,這種以 Packer 為核心、整合專業配置管理工具的「智慧工具鏈」,將成為成熟維運團隊的標準作業模式,並推動基礎設施生命週期管理的典範轉移。

綜合評估後,對於追求長期維運穩定性與規模化效益的組織而言,將 Ansible 整合至 Packer 流程,已是從「能用」邁向「卓越」的關鍵投資,值得納入核心技術戰略。