Packer 能夠有效簡化 Docker 映像檔的建置流程,尤其在跨平台佈署時更能展現其優勢。相較於 Dockerfile 的逐行指令建置,Packer 將所有變更整合至單一層級,大幅減少了映像檔體積。本文以 Moby Counter 應用程式為例,示範瞭如何使用 Packer 建置 Docker 映像檔,並透過選用更精簡的基礎映像檔 russmckendrick/base 和清除快取等步驟,進一步縮減了映像檔大小。實測結果顯示,使用 Packer 建置的映像檔大小明顯小於使用 Dockerfile 建置的映像檔,最多可減少約 20% 的空間。雖然 Packer 無法直接將映像檔推播到 Docker Hub 並設定自動建置,也無法直接設定映像檔後設資料,但它在映像檔大小最佳化和建置流程簡化方面的優勢,使其成為 Docker 映像檔建置流程中一個值得考慮的工具。

擴充套件你的基礎架構

正如你從以下終端輸出所看到的,你可以使用 vagrant ssh 命令連線到你的 Vagrant 主機:

你可能注意到的另一件事是,安裝的 Docker 版本不是最新的;這是因為 Vagrant 安裝的是作業系統預設儲存函式庫中可用的版本,而不是 Docker 提供的最新版本。

Vagrant Docker 提供者

正如我所提到的,有兩種方式可以在 Vagrant 中使用 Docker:我們剛才看過的方式是作為一個 provisioner,第二種方式是作為一個提供者。

那麼,什麼是提供者?在本章中,我們已經使用了提供者兩次,當我們啟動了我們的 Docker 主機。提供者是一個虛擬機器程式、管理器或 API,Vagrant 可以連線到它並啟動虛擬機器。Vagrant 內建了以下提供者:

  • VirtualBox
  • Docker
  • Hyper-V

此外,作者還提供了一個商業外掛,增加了以下提供者:

  • VMware Fusion 和 Workstation

最後,Vagrant 支援自定義提供者,例如 Amazon Web Services、libvirt,甚至 LXC。完整的自定義提供者和 Vagrant 外掛列表可以在 http://vagrant-lists.github.io/ 找到。

顯然,如果你使用的是 OS X,那麼你無法原生使用 Docker 提供者;然而,Vagrant 會處理這個問題。讓我們來看看如何使用 Docker 提供者而不是 provisioner 來啟動一個 NGINX 容器。

Vagrantfile 組態範例

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.define "boot2docker", autostart: false do |dockerhost|
    dockerhost.vm.box = "russmckendrick/boot2docker"
    dockerhost.nfs.functional = false
    dockerhost.vm.network :forwarded_port, guest: 80, host: 9999
    dockerhost.ssh.shell = "sh"
    dockerhost.ssh.username = "docker"
    dockerhost.ssh.password = "tcuser"
    dockerhost.ssh.insert_key = false
  end

  config.vm.define "nginx", primary: true do |v|
    v.vm.provider "docker" do |d|
      d.vagrant_vagrantfile = "./Vagrantfile"
      d.vagrant_machine = "boot2docker"
      d.image = "russmckendrick/nginx"
      d.name = "nginx"
      d.ports = ["80:80"]
    end
  end
end

內容解密:

Vagrantfile 被分成兩個部分:第一部分定義了一個 Boot2Docker 虛擬機器,第二部分定義了 NGINX 容器。

  • config.vm.define "boot2docker" 定義了一個名為 “boot2docker” 的虛擬機器,使用 russmckendrick/boot2docker 映象,並設定了埠轉發和 SSH 連線資訊。
  • config.vm.define "nginx" 定義了一個名為 “nginx” 的容器,使用 russmckendrick/nginx 映象,並將容器的 80 埠對映到主機的 80 埠。

使用 Docker 提供者啟動 NGINX 容器

當你執行 vagrant up 時,你會看到 Vagrant 首先下載並啟動 Boot2Docker 虛擬機器,然後在該虛擬機器中啟動 NGINX 容器。

終端輸出解說:

在 OS X 上,Vagrant 會啟動一個 Boot2Docker 虛擬機器,並將虛擬機器的 22 埠對映到本地主機的 2222 埠,以便進行 SSH 連線。同時,將虛擬機器的 80 埠對映到本地主機的 9999 埠。 如果是在 Linux 主機上,並且已經安裝了 Docker,那麼 Vagrant 將直接使用本地的 Docker 安裝,而跳過啟動 Boot2Docker 的步驟。

登入 Boot2Docker 虛擬機器

你可以使用以下命令登入 Boot2Docker 虛擬機器:

ssh docker@localhost -p2222

密碼是 tcuser

終止容器和虛擬機器

要終止 NGINX 容器和 Boot2Docker 虛擬機器,可以執行以下命令:

vagrant destroy

這將提示你確認是否刪除容器和虛擬機器。

為何不建議使用 Docker 提供者

Docker 提供者的功能現在已經顯得有些過時,尤其是它有一些限制,例如只能使用埠對映,而不能為虛擬機器分配 IP 地址。使用 provisioner 或其他工具可以輕易克服這些限制。因此,建議使用 provisioner 而不是提供者來利用 Vagrant。

封裝映像檔

到目前為止,我們一直愉快地下載來自 Docker Hub 的預建映像檔進行測試。接下來,我們將著眼於建立自己的映像檔。在使用第三方工具建立映像檔之前,我們應該先了解一下如何在 Docker 中構建映像檔。

一個應用程式

在開始構建自己的映像檔之前,我們應該要有一個應用程式可以「烘焙」到映像檔中。我猜你可能已經對重複安裝 WordPress 感到厭煩了,我們將會看一些完全不同的東西。

因此,我們將構建一個包含 Moby Counter 的映像檔。Moby Counter 是由 Kai Davenport 編寫的應用程式,他這樣描述它: 「一個小應用程式,用於演示如何在 docker-compose 應用程式中保持狀態。」

該應用程式在瀏覽器中執行,並在你點選頁面時新增 Docker 標誌,其理念是使用 Redis 或 Postgres 後端來儲存 Docker 標誌的數量和位置,這演示了資料如何在像我們在第 3 章《卷外掛》中看到的捲上持久化。你可以在 https://github.com/binocarlos/moby-counter/ 找到該應用程式的 GitHub 儲存函式庫。

Docker 的方式

既然我們對要啟動的應用程式有了一些瞭解,那麼就讓我們來看看如何使用 Docker 本身構建映像檔。

基本的 Dockerfile 相當簡單:

FROM russmckendrick/nodejs
ADD . /srv/app
WORKDIR /srv/app
RUN npm install
EXPOSE 80
ENTRYPOINT ["node", "index.js"]

內容解密:

  1. FROM russmckendrick/nodejs:使用 russmckendrick/nodejs 作為基礎映像檔,該映像檔已安裝 NodeJS。
  2. ADD . /srv/app:將當前工作目錄的內容新增到容器中的 /srv/app 目錄。
  3. WORKDIR /srv/app:將工作目錄更改為 /srv/app
  4. RUN npm install:安裝執行應用程式所需的依賴項。
  5. EXPOSE 80:暴露容器的 80 埠。
  6. ENTRYPOINT ["node", "index.js"]:設定容器的入口點為執行 node index.js

伴隨 Dockerfile 的是一個 Docker Compose 檔案,它啟動了 Moby Counter 映像檔的構建,下載了官方的 Redis 映像檔,然後啟動了兩個容器並將它們連線起來。

在執行此操作之前,我們需要啟動一台機器來執行構建;為此,請執行以下命令以啟動根據 VirtualBox 的本地 Docker 主機:

docker-machine create --driver "VirtualBox" chapter06

現在 Docker 主機已經啟動,執行以下命令以組態本地 Docker 使用者端直接與其通訊:

eval $(docker-machine env chapter06)

現在您已經準備好了主機並組態了使用者端,執行以下命令以構建映像檔並啟動應用程式:

docker-compose up -d

當您執行該命令時,您應該在終端中看到類別似以下的輸出。

現在應用程式已經啟動,您應該能夠透過執行以下命令在瀏覽器中開啟它:

open http://$(docker-machine ip chapter06)/

您將看到一個頁面,上面寫著「Click to add logos」,如果您點選頁面,Docker 標誌就會開始出現。如果您點選重新整理,您新增的標誌將保持不變,因為標誌的數量和位置儲存在 Redis 資料函式庫中。

要停止容器並刪除它們,請執行以下命令:

docker-compose stop
docker-compose rm

使用 Packer 構建

Packer 是由 Hashicorp 的 Mitchell Hashimoto 編寫的,與 Vagrant 的作者相同。因此,我們將使用的術語有很多相似之處。

Packer 網站對該工具有著最好的描述: 「Packer 是一個開源工具,用於從單一源組態為多個平台建立相同的機器映像檔。Packer 輕量級,執行在每個主要的作業系統上,並且效能極高,能夠平行建立多個平台的機器映像檔。Packer 不會替換像 Chef 或 Puppet 這樣的組態管理工具。事實上,在構建映像檔時,Packer 能夠使用像 Chef 或 Puppet 這樣的工具在映像檔上安裝軟體。」

我從 Packer 的第一個版本開始就一直在使用它來為 Vagrant 和公共雲構建映像檔。

您可以從 https://www.packer.io/downloads.html 下載 Packer,或者,如果您安裝了 Homebrew,您可以執行以下命令:

brew install packer

現在您已經安裝了 Packer,讓我們來看看組態檔案。Packer 組態檔案全部定義在 JSON 中。

JSON(JavaScript 物件表示法)是一種輕量級的資料交換格式。它易於人類閱讀和編寫,也易於機器解析和生成。

以下檔案與我們的 Dockerfile 所做的事情幾乎完全相同:

{
  "builders":[{
    "type": "docker",
    "image": "russmckendrick/nodejs",
    "export_path": "mobycounter.tar"
  }],
  "provisioners":[
    {
      "type": "file",
      "source": "app",
      "destination": "/srv"
    },
    {
      "type": "file",
      "source": "npmrc",
      "destination": "/etc/npmrc"
    },
    {
      "type": "shell",
      "inline": [
        "cd /srv/app",
        "npm install"
      ]
    }
  ]
}

內容解密:

  1. builders:定義了用於構建映像檔的構建器。在這裡,我們使用了 docker 型別的構建器,使用 russmckendrick/nodejs 作為基礎映像檔,並將映像檔匯出到 mobycounter.tar 檔案。
  2. provisioners:定義了用於組態映像檔的組態器。在這裡,我們使用了三個組態器:
    • 第一個組態器將 app 目錄複製到容器中的 /srv 目錄。
    • 第二個組態器將 npmrc 檔案複製到容器中的 /etc/npmrc 檔案。
    • 第三個組態器在容器中執行 shell 命令,進入 /srv/app 目錄並執行 npm install

與使用 Docker Compose 檔案構建映像檔不同,我們將不得不執行 Packer 然後匯入映像檔檔案。要啟動構建,請執行以下命令:

packer build docker.json

您應該在終端中看到類別似以下的輸出。

使用 Packer 最佳化 Docker 映像檔建置

在前面的章節中,我們已經瞭解如何使用 Docker 以及其相關工具來建置和管理容器化的應用程式。本章節將探討如何使用 Packer 來最佳化 Docker 映像檔的建置過程,並比較其與傳統 Docker 建置方法的差異。

Packer 建置 Docker 映像檔

當使用 Packer 建置 Docker 映像檔時,它會將映像檔儲存到你執行 Packer 建置命令的資料夾中。假設我們的映像檔名為 mobycounter.tar,可以使用以下命令將其匯入 Docker:

docker import mobycounter.tar mobycounter

匯入後,可以使用 docker images 命令來確認映像檔是否已成功匯入:

docker images

確認映像檔存在後,可以使用 docker-compose up -d 命令來啟動容器:

docker-compose up -d

接著,開啟瀏覽器並存取 http://$(docker-machine ip chapter06)/,即可看到應用程式正在執行。

Packer 與 Docker Build 的比較

為了比較 Packer 和 Docker Build 的差異,我們首先使用 Packer 再次建置映像檔。這次,我們嘗試減少映像檔的大小,使用 russmckendrick/base 這個基礎映像檔來取代 russmckendrick/nodejs

Packer 設定檔範例

{
  "builders": [
    {
      "type": "docker",
      "image": "russmckendrick/base",
      "export_path": "mobycounter-small.tar"
    }
  ],
  "provisioners": [
    {
      "type": "file",
      "source": "app",
      "destination": "/srv"
    },
    {
      "type": "file",
      "source": "npmrc",
      "destination": "/etc/npmrc"
    },
    {
      "type": "shell",
      "inline": [
        "apk update",
        "apk add --update nodejs",
        "npm -g install npm",
        "cd /srv/app",
        "npm install",
        "rm -rf /var/cache/apk/**/",
        "npm cache clean"
      ]
    }
  ]
}

內容解密:

  1. builders 部分:指定使用 Docker 建置器,並使用 russmckendrick/base 作為基礎映像檔,將最終映像檔匯出為 mobycounter-small.tar
  2. provisioners 部分
    • 檔案複製:將本地的 app 資料夾複製到映像檔中的 /srv 目錄。
    • 組態 npm:將 .npmrc 組態檔案複製到 /etc/npmrc
    • 安裝 NodeJS 和應用程式依賴:使用 Alpine Linux 的套件管理器安裝 NodeJS,並組態應用程式,最後清除快取以減少映像檔大小。

執行 Packer 建置

執行以下命令來建置映像檔:

packer build docker-small.json

建置完成後,我們比較三個不同方法建置出的映像檔大小:

  • 使用 Dockerfile 建置(根據 russmckendrick/nodejs):52 MB
  • 使用 Packer 建置(根據 russmckendrick/nodejs):47 MB
  • 使用 Packer 建置並安裝完整堆積疊(根據 russmckendrick/base):40 MB

可以看出,使用 Packer 建置並安裝完整堆積疊的映像檔大小最小,節省了約 12 MB 的空間。對於一個僅有 52 MB 的映像檔來說,這是一個不錯的最佳化。

Docker 映像檔層級結構解析

Docker 映像檔是由多層檔案系統疊加而成,每一層代表著對前一層的變更。當我們使用 Dockerfile 建置映像檔時,每一行指令都會建立一個新的層級。例如:

FROM russmckendrick/nodejs
ADD . /srv/app
WORKDIR /srv/app
RUN npm install
EXPOSE 80
ENTRYPOINT ["node", "index.js"]

每一層都是獨立的檔案系統存檔,這導致了額外的儲存開銷。相反,Packer 只建立兩層:基礎映像檔層和包含所有變更的第二層。這樣可以減少層級數量,從而節省空間。

圖表說明:Docker 映像檔層級結構

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Packer 最佳化 Docker 映像檔建置

package "Docker 架構" {
    actor "開發者" as dev

    package "Docker Engine" {
        component [Docker Daemon] as daemon
        component [Docker CLI] as cli
        component [REST API] as api
    }

    package "容器運行時" {
        component [containerd] as containerd
        component [runc] as runc
    }

    package "儲存" {
        database [Images] as images
        database [Volumes] as volumes
        database [Networks] as networks
    }

    cloud "Registry" as registry
}

dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置

@enduml

圖表翻譯: 此圖示展示了 Docker 映像檔的多層結構,每一層代表 Dockerfile 中的一條指令,從基礎映像檔層逐步疊加變更,直到最終的執行組態。

Packer 的優勢與限制

優勢:

  1. 減少映像檔大小:透過減少層級數量,Packer 可以有效縮小 Docker 映像檔的大小。
  2. 指令碼重用性:Packer 的指令碼可以在不同環境中重複使用,例如從本地開發環境到生產環境,無論是容器還是虛擬機器。

限制:

  1. 無法自動推播到 Docker Hub 並標記為自動建置:這可能會影響映像檔的可信度,因為使用者無法直接檢視建置過程。
  2. 不支援後設資料組態:例如,暴露埠號和預設執行命令等需要在 Dockerfile 中定義的內容,Packer 不直接支援,但可以透過 Docker Compose 或 docker run 命令來實作。

總的來說,Packer 提供了一個靈活且高效的方式來建置和最佳化 Docker 映像檔,尤其是在需要跨不同環境佈署相同組態時。儘管有一些限制,但透過合理的搭配使用其他工具,這些限制是可以被克服的。