Vagrant Tutorial(4)虛擬機,若即若離的國中之國 << 前情
上回我們示範了如何在 guest OS 裡面,手動設定好 Redis server,以及一份「Docker 化 (Dockerized)」的 Redis server。辛辛苦苦把軟體設定好了,下一步,就是研究該如何存檔、散佈、自動化管理,讓這些心血不會因意外就消失不見,還得整個從頭來過。
讓我們先手動將虛擬機組態設定、儲存、複製增生虛擬機等流程完整走過一遍,之後再設法自動化整個流程。
我們會依序演練以下任務:
- 手動設定 Guest OS 組態
- 打包,存檔
- 加進本機端 Vagrant repository
- 複製增生虛擬機執行個體
- 散佈 box 檔
- 加進 Vagrant Cloud repository
- 自動設定 Guest OS 組態 (Provisioning)
手動設定 Guest OS 組態
與上一篇文章一樣,我們仍然以「在 guest OS 中安裝 Redis server」當例子。
首先,換一個新的工作目錄 demo-redis-1 :
$ mkdir demo-redis-1
$ cd demo-redis-1
下一步,要準備一份 Vagrant 定義檔 Vagrantfile 。
以前我們都是藉由 vagrant init 指令產生一份 Vagrantfile 供我們差遣,不過,本系列文章都已經連載到第五集了,不能再當懶人了。這一回,讓我們當個硬漢吧!自己硬刻,手動將以下內容寫進 Vagrantfile 檔案裡面去:
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/trusty64"
end
接著,啟動、登入這台 guest OS:
$ vagrant up
$ vagrant ssh
vagrant$
最後,用以下指令安裝 Redis server 進去:
vagrant$ # 安裝 Redis...
vagrant$ sudo apt-get install redis-server -y
[略]
vagrant$
vagrant$ # 允許 Redis bind 至全部 network interface...
vagrant$ sudo sed -i -e 's/^bind/#bind/' /etc/redis/redis.conf
vagrant$ # 重啟 Redis,讓新設定生效。
vagrant$ sudo service redis-server restart
到目前為止,都是上一回我們演練過的步驟。好戲還在後面⋯⋯
打包,存檔
我們可用 vagrant package 指令,叫 Vagrant 將目前這個虛擬機執行個體打包起來,整個儲存到外部檔案。依照 Vagrant 慣例,儲存的檔案,副檔名會是 .box 。如果不特別下 --output 參數,預設的輸出檔名會是 package.box 。
假設我們想存成 redis.box 檔:
vagrant$ # 先登出 guest OS
vagrant$ exit
$
$ # 回到 host OS 了。
$ # 打包,存成 redis.box 檔
$ vagrant package --output redis.box
==> default: Attempting graceful shutdown of VM...
==> default: Clearing any previously set forwarded ports...
==> default: Exporting VM...
==> default: Compressing package to: /private/tmp/demo-redis-1/redis.box
$
$ # 看看存檔的情況
$ ls -al
total 673168
drwxr-xr-x 5 william wheel 170 9 23 23:00 .
drwxrwxrwt 17 root wheel 578 9 23 22:47 ..
drwxr-xr-x 3 william wheel 102 9 23 22:47 .vagrant
-rw-r--r-- 1 william wheel 191 9 23 22:47 Vagrantfile
-rw-r--r-- 1 william wheel 344656169 9 23 23:01 redis.box
$
有了這個 box 檔母體,就可據以複製增生新的虛擬機執行個體;在本機端,甚至讓別的電腦下載回去安裝。
據 Vagrant 官方文件 “Box File Format” 的說法,box 檔案會是一個壓縮檔(tar 、tar.gz 或 zip )。以此例而言,壓縮檔是 tar.gz 格式:
$ # 看看這個壓縮檔裡面到底塞了哪些東西...
$ tar ztvf redis.box
-rw------- 0 william staff 352730624 9 23 23:00 ./box-disk1.vmdk
-rw------- 0 william staff 10849 9 23 22:59 ./box.ovf
-rw-r--r-- 0 william staff 505 9 23 23:00 ./Vagrantfile
$
這 box 壓縮檔的內容,是不是似曾相識呢?
沒錯!解壓縮後的內容,其實就是本系列文章第三集提過的這些東西:
Vagrant box 檔案本體被 Vagrant 藏在哪裡?
如果你沒有改變 VAGRANT_HOME 環境變數的話,預設目錄會是:
- 若 host OS 是 Linux & Mac,會擺在
$HOME/.vagrant.d 目錄。
- 若 host OS 是 Windows,會擺在
%USERPROFILE%\.vagrant.d 目錄。
可見,本機端的 Vagrant repository,其實擺的就是 box 檔案解壓縮之後的東西,再加上一些輔助資訊。
加進本機端 Vagrant repository
我們可用 vagrant box add 指令,叫 Vagrant 將這個虛擬機 box 檔案,以指定的名字,添加、登記到本機端的 Vagrant repository。
假設我們想把它取名為 my-redis-1 :
$ # 先看看目前本機端的 Vagrant repository 已有哪些 box:
$ vagrant box list
ubuntu/trusty64 (virtualbox, 14.04)
$
$
$ # 把 redis.box 這個虛擬機檔案,添加到本機端的 Vagrant repository,
$ # 並取名為 my-redis-1
$ vagrant box add --name my-redis-1 redis.box
==> box: Adding box 'my-redis-1' (v0) for provider:
box: Downloading: file:///private/tmp/demo-redis-1/redis.box
box: Progress: 0% (Rate: 0/s, Estimated time remaining: --:--
box: Progress: 70% (Rate: 267M/s, Estimated time remaining: 0
==> box: Successfully added box 'my-redis-1' (v0) for 'virtualbox'!
$
$
$ # 現在,再看看是否真的已經有一個 `my-redis-1` box 生出來了:
$ vagrant box list
my-redis-1 (virtualbox, 0)
ubuntu/trusty64 (virtualbox, 14.04)
$
$
$ # 或是,更深入一點看看本機端的 Vagrant repository 內容:
$ ls -al ~/.vagrant.d/boxes/my-redis-1/0/virtualbox/
total 688968
drwxr-xr-x 6 william staff 204 9 23 23:09 .
drwxr-xr-x 3 william staff 102 9 23 23:09 ..
-rw-r--r-- 1 william staff 505 9 23 23:09 Vagrantfile
-rw------- 1 william staff 352730624 9 23 23:09 box-disk1.vmdk
-rw------- 1 william staff 10849 9 23 23:09 box.ovf
-rw-r--r-- 1 william staff 25 9 23 23:09 metadata.json
$
$
$ # 與前面的 redis.box 壓縮檔內容對照看看...
$ tar ztvf redis.box
-rw------- 0 william staff 352730624 9 23 23:00 ./box-disk1.vmdk
-rw------- 0 william staff 10849 9 23 22:59 ./box.ovf
-rw-r--r-- 0 william staff 505 9 23 23:00 ./Vagrantfile
$
兩相比對,顯而易見的,本機端的 Vagrant repository,其實擺的就是 box 檔案解壓縮之後的東西,再加上一些輔助資訊(即此例的 metadata.json )。
複製增生虛擬機執行個體
只要 box 檔案有添加登記到本機端的 Vagrant repository,日後就可用這個登記過的名字去 vagrant init 及 vagrant up 相同組態的虛擬機執行個體。
首先,換一個乾淨的工作目錄:
$ cd ..
$ mkdir demo-redis-2
$ cd demo-redis-2
再根據 my-redis-1 這個登記在案的虛擬機母體,複製增生一份虛擬機執行個體:
$ vagrant init my-redis-1
$ vagrant up
你可以 vagrant ssh 進去看一看,是否一切正常。
小測驗:打包、安裝一份 Ubuntu + Docker box
中場休息一下,來一段小測驗。
在上一集文章中,我們已經手動在 Ubuntu 這個 guest OS 裡面,安裝設定出一份 Ubuntu + Docker 系統。現在,請你根據本篇文章學的新把戲,從頭開始一步一步做出:
- 以 Ubuntu 做為 guest OS,啟動,登入。
- 手動設定此 guest OS 組態(可參考上次的錄影,複習一下 Ubuntu + Docker 安裝過程: http://www.screencast.com/t/7XqIo9Cepb4 )。
- 打包,存檔。
- 加進本機端 Vagrant repository。
- 複製增生虛擬機執行個體。
參考解答: http://asciinema.org/a/11924 。
散佈 box 檔
有了 box 檔,不只可以安裝在本機端的 Vagrant repository。只要把它擺在一個可供他人下載的網路空間,甚至可讓別的電腦下載回去,安裝在其他電腦自己的本機端 Vagrant repository。
這個所謂「可供他人下載的網路空間」,簡單一點的,通常會是像 Dropbox、Google Drive 之類的網路硬碟、雲端硬碟;高檔一點的,可能會是 Amazon S3 或 CDN;有隱私要求的,則可放在私有檔案伺服器。
來演練一次吧!
首先,上傳 box 檔案到某一個可供他人下載的網路空間,並記下它的 URL。以我的例子,網址是 http://bit.ly/vagrantbox-redis 。
接著,換一個乾淨的工作目錄:
$ cd ..
$ mkdir demo-redis-3
$ cd demo-redis-3
接著,編輯 Vagrantfile 內容,加上一行 config.vm.box_url 設定。正如變數名稱所暗示的,這裡要填上該 box 檔案的 URL:
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# 這次,改取名為 "my-redis-2"
config.vm.box = "my-redis-2"
# 填入該 box 檔案所在網址
config.vm.box_url = "http://bit.ly/vagrantbox-redis"
end
一切就緒,讓我們啟動虛擬機吧!
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'my-redis-2' could not be found. Attempting to find and install...
default: Box Provider: virtualbox
default: Box Version: >= 0
==> default: Adding box 'my-redis-2' (v0) for provider: virtualbox
default: Downloading: http://bit.ly/vagrantbox-redis
default: Progress: 0% (Rate: 0/s, Estimated time remaining: -
[略]
default: Progress: 99% (Rate: 723k/s, Estimated time remainin
==> default: Successfully added box 'my-redis-2' (v0) for 'virtualbox'!
==> default: Importing base box 'my-redis-2'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: demo-redis-3_default_1411486799399_97764
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: Forwarding ports...
default: 22 => 2222 (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Connection timeout. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
default: /vagrant => /private/tmp/demo-redis-3
$
輸出畫面中有這麼一行:
==> default: Adding box 'my-redis-2' (v0) for provider: virtualbox
default: Downloading: http://bit.ly/vagrantbox-redis
表示 Vagrant 真的有試著去指定網址下載 box 檔案。
再往下看,看到這一行:
==> default: Successfully added box 'my-redis-2' (v0) for 'virtualbox'!
我們就來看一看,本機端的 Vagrant repository 是否真的已經新增了 my-redis-2 這個 box:
$ vagrant box list
my-redis-1 (virtualbox, 0)
my-redis-2 (virtualbox, 0)
ubuntu/trusty64 (virtualbox, 14.04)
$
同樣的,你可以 vagrant ssh 進去看一看,是否一切正常。
從這個例子可以看出,只要別人知道 box 檔案的網址,就可以自己依樣畫葫蘆寫一份 Vagrantfile 檔,以將它下載安裝到他們自己的本機端 Vagrant repository;至於他們想替這個 box 取什麼名字,則由他們自己決定。
現在你應該知道,該怎麼善用像 Vagrantbox.es 網站所列的 box 檔案母體了。
加進 Vagrant Cloud repository
到目前為止,我們所示範的,都是把 box 登記到本機端的 repository(或其他電腦自己的本機端 repository),取的 box 名字也都是相對於本機端。
所謂的「相對於本機端」的名字,意思是:前面我們示範出來的 my-redis-1 、my-redis-2 這類的 box 名字,他們的有效能見範圍(就像程式語言的 scope 概念),只限於該機器本身。換句話說,我電腦裡面的 my-redis-1 ,不見得和你電腦裡面的 my-redis-1 一樣。
那麼,像 ubuntu/trusty64 、hashicorp/precise64 、chef/centos-6.5 這種全域式的 box 名字,是怎麼設定出來的?
答案是,要向 Vagrant 發明人 Mitchell Hashimoto 所創辦的 HashiCorp 公司所經營的 Vagrant Cloud 服務辦理登記手續,Vagrand Cloud 網站才會配置 ubuntu/trusty64 、hashicorp/precise64 、chef/centos-6.5 這樣專屬的全域名稱,供世界各地的 Vagrant 用戶引用。
所以,Vagrant Cloud 就相當於 Vagrant 世界的中央 repository。
把 box 送進這個中央 repository 的步驟是:
- 如前一節所示範的,先將 box 檔案上傳到一個可供他人下載的網路空間。
- 登入 Vagrant Cloud 網站,替 box 取一個名字:
- 填入該 box 檔案所在網址:
- 發佈!
發佈完畢,其他人就可在 Vagrant Cloud 搜尋到它。如果你希望這是個只限特定人士才搜得到、用得到的 box,就得升級成付費會員。
來驗收成果吧。假設我們在 Vagrant Cloud 將它取名為 williamyeh/redis 的話,以後全世界各地用戶,都可以這樣引用它:
$ vagrant init williamyeh/redis
$ vagrant up
照例,你可以 vagrant ssh 進去看一看是否一切正常。整段操作細節也都錄影起來了: http://www.youtube.com/watch?v=dUwoUPqQQPM 。
這種方法的好處是,用戶不必知道 box 檔案實際擺放的網址,不必手寫 Vagrantfile 的 config.vm.box_url 設定值,還享有 Vagrant Cloud 網站提供的瀏覽搜尋服務協助你曝光成果。缺點則是,如果你不是付費會員,你的成果會被迫公開在全世界人類眼前。
自動設定 Guest OS 組態:Provisioning
剛剛我們已經先手動將虛擬機組態設定、儲存、複製增生虛擬機等流程完整走過一遍,現在該設法自動化整個流程了。
回顧一下,本文到目前為止,依序手動演練過哪些任務:
- 手動設定 Guest OS 組態
- 打包,存檔
- 加進本機端 Vagrant repository
- 複製增生虛擬機執行個體
- 散佈 box 檔
- 加進 Vagrant Cloud repository
該怎麼自動化這些任務呢?
其中,(2)~(5) 都只是在 host OS 裡面串接流程而已,很容易用腳本語言搞定。「(6) 加進 Vagrant Cloud repository」稍微麻煩一點,需要透過 Vagrant Cloud API,但這份 API 是帶有 REST 風格的,用腳本語言就能串起來,厲害一點的人,甚至用 bash + curl 組合技就能搞定。
剩下「(1) 手動設定 Guest OS 組態」這一點,還沒介紹該如何自動化。
這段自動化環節,有一個正規的術語,叫做「provisioning」。據 Wikipedia 的解釋:
Server provisioning is a set of actions to prepare a server with appropriate systems, data and software, and make it ready for network operation.
Typical tasks when provisioning a server are: select a server from a pool of available servers, load the appropriate software (operating system, device drivers, middleware, and applications), appropriately customize and configure the system and the software to create or change a boot image for this server, and then change its parameters […] to find associated network and storage resources […] to audit the system.
[…] This makes the system ready for operation.
節錄自 http://en.wikipedia.org/wiki/Provisioning#Server_provisioning
如果懶得讀那麼長的說明,只要抓住頭尾兩句重點:
- “Server provisioning is a set of actions to prepare a server” 告訴我們,provisioning 往往是一連串的連續動作。
- “This makes the system ready for operation” 告訴我們,provisioning 的目的是讓系統組態設定到可上線運作的 “I’m READY!” 狀態。
Vagrant 提供三種自動化 provisioning 機制。由淺入深,依序是:
- inline script(內嵌腳本)
- external script file(外部腳本檔)
- configuration management software(組態管理軟體)
只要依照各自的語法去修改 Vagrantfile ,爾後,在以下幾種情況,Vagrant 都會自動執行我們準備好的一系列自動化 provisioning 指令:
vagrant up 第一次執行時。
- 每次執行
vagrant up --provision 時。
- 每次執行
vagrant provision 時。
- 每次執行
vagrant reload --provision 時。
讓我們逐一嘗試這三種做法。
Inline script 內嵌腳本
內嵌腳本的做法是,將我們要 Vagrant 自動執行的一系列自動化 provisioning 指令,以 shell script 語法,塞入 Vagrantfile 的 config.vm.provision 設定值。譬如說,如果寫成這樣:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "shell", inline: "echo Hello, World"
end
那麼,當你在此工作目錄下,第一次執行 vagrant up 時,會看到輸出畫面的最後面,在 guest OS 啟動完畢後,在 port forwarding 及 shared folder 也都配置完畢後,還多執行了一行 echo Hello, World 指令:
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/trusty64'...
[略]
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
default: /vagrant => /private/tmp/z
==> default: Running provisioner: shell...
default: Running: inline script
==> default: stdin: is not a tty
==> default: Hello, World
當然啦,實務上,我們要塞的自動化指令,不會只有 echo Hello, World 這麼一行而已。萬一我們想塞多行指令進去呢?
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "shell",
inline: "echo Hello, World\necho World Peace\necho I love Vagrant"
end
寫法有點蠢。
漂亮一點的寫法是,利用「Vagrantfile 本身也就是一份遵循 Ruby DSL 規則的 Ruby 源碼文件」此一事實(本系列文章第三集介紹過),再借用 Ruby 的 heredoc 語法,將 Vagrantfile 改寫成:
$script = <<SCRIPT
# 安裝 Redis...
sudo apt-get install redis-server -y
# 允許 Redis bind 至全部 network interface...
sudo sed -i -e 's/^bind/#bind/' /etc/redis/redis.conf
# 重啟 Redis,讓新設定生效。
sudo service redis-server restart
SCRIPT
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "shell", inline: $script
end
如此一來,程序式的 provisioning 指令擺在前面,宣告式的虛擬機屬性內容擺在後面,一邊一國明顯區隔成兩塊,比較好維護。
External script 外部腳本檔
上述 script 內容,還可更進一步抽離到外部檔案。
譬如說,如果我們在工作目錄裡,寫一個 install.sh 檔:
#!/bin/bash
# 安裝 Redis...
sudo apt-get install redis-server -y
# 允許 Redis bind 至全部 network interface...
sudo sed -i -e 's/^bind/#bind/' /etc/redis/redis.conf
# 重啟 Redis,讓新設定生效。
sudo service redis-server restart
然後,修改 Vagrantfile 的 config.vm.provision 設定值,讓它指涉到外部腳本檔 install.sh :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "shell", path: "install.sh"
end
如此一來,Vagrant 便會在 provisioning 階段,載入並執行這個 install.sh 檔。
這種寫法,適合有潔癖的人。
Configuration management 組態管理軟體
如果上線主機早已導入組態管理機制,也早已運用像 Chef、Puppet、Ansible、Salt 這類比 shell script 更合身的軟體,是否也能套用在 Vagrant 的 provisioning 環節?如此一來,就能 “infrastructure as code” 一以貫之了。
譬如說,以下這份 Ansible 檔案,可用來將 Redis 安裝在 Ubuntu 上:
---
# file: playbook.yml
# Ansible playbook for installing Redis on Ubuntu
- hosts: all
sudo: True
tasks:
- name: Install the Redis package
apt: name={{ item }} state=present update_cache=yes
with_items: redis-server
- name: let Redis bind all network interfaces, if necessary.
lineinfile: dest=/etc/redis/redis.conf regexp="^bind 127.0.0.1" line="#bind 127.0.0.1" insertafter="^bind"
- name: restart redis-server
service: name=redis-server state=restarted enabled=yes
該怎樣將它套用在 Vagrant 身上呢?
有兩種做法,硬漢法及懶人法。
硬漢法:叫 Ansible 直接對已經 vagrant up 之後的 guest OS 進行 SSH 連線,透過此 SSH 通道設定 guest OS 組態。當然啦,本系列文章第四集就提醒過了,想當嘴硬的硬漢,就得手動輸入該 guest OS 對外開放的 IP 位址 (預設為 127.0.0.1 )、TCP 通訊埠 (預設為 2222 ),甚至登入的帳號 (vagrant ) 及密碼 (vagrant ):
$ # 先啟動 guest OS
$ vagrant up
$
$ # 將這次 Vagrant 告訴我們的 SSH host:port 填入 Ansible inventory file
$ cat <<EOF > inventory
[vagrant]
127.0.0.1 ansible_ssh_port=2222
EOF
$
$ # 叫 Ansible 透過 SSH 連線,將 "playbook.yml" 組態套用進去
$ ansible-playbook -c paramiko -u vagrant -k -vvv -i inventory playbook.yml
整個經過,請見錄影: http://asciinema.org/a/12556 。
懶人法:首先,編輯 Vagrantfile 內容,加入 config.vm.provision 一系列設定值:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "ansible" do |ansible|
ansible.playbook = "playbook.yml"
ansible.sudo = true
end
end
接著,直接 vagrant up (或 vagrant up --provision 或 vagrant provision )即可,不必準備 inventory 檔案,也不必手動執行 ansible-playbook 程式、夾帶一堆命令列參數,Vagrant 會替你打理好這堆瑣事。整個過程,請見錄影:http://asciinema.org/a/12554 。
懶人法的確簡單多了。不過,你還是必須知道硬漢法的原理。
這裡只是簡單示範如何將組態管理軟體融入 Vagrant provisioning 機制中,實務上組態管理遠比此例複雜,請參考 Vagrant 官方文件的 Provisioning 章節。
Docker, again!
前面的小測驗,我們已經手動打包一份「Ubuntu + Docker engine」box。現在,如果想將某個 Docker image 自動裝進去,該怎麼做?
你可能會利用剛剛學到的 inline script 來安裝:
Vagrant.configure("2") do |config|
config.vm.box = "williamyeh/ubuntu-trusty64-docker"
config.vm.provision "shell",
inline: "docker pull redis:latest"
end
或者,利用 Vagrant 1.6 新增的 “docker” provisioner 語法:
Vagrant.configure("2") do |config|
# any base box with Docker engine installed is OK;
# e.g, "yungsang/coreos", "yungsang/coreos-beta"
config.vm.box = "williamyeh/ubuntu-trusty64-docker"
# pull images from the Docker registry
# see http://docs.vagrantup.com/v2/provisioning/docker.html
config.vm.provision "docker", images: ["redis:latest"]
end
Vagrant 1.6 對於 Docker 的支援還不僅如此,有興趣的,可參考以下文章:
回顧:虛擬機 Wish List
本系列文章第二集曾經分析過,軟體研發者需要以下這些「可程式化介面」,才能將虛擬機納入軟體開發工具鏈中:
- 自動安裝一台或多台指定版本的 guest OS 虛擬機。
- 自動設定好這些虛擬機叢集的組態,與上線主機共用同一套組態管理機制。
- 儲存虛擬機現有狀態,可日後回復或快速複製。
- 虛擬機層次的 repository 中央儲存庫。
到目前為止,我們已經用 Vagrant 逐一實現各般需求,只剩下「多台虛擬機叢集」這一招。請待下回分解。
|