Vagrant Tutorial(4)虛擬機,若即若離的國中之國 by William Yeh | CodeData
top

Vagrant Tutorial(4)虛擬機,若即若離的國中之國

分享:

Vagrant Tutorial(3)細說虛擬機生滅狀態 << 前情

國中之國

梵蒂岡這個內陸國,總面積 0.44 平方公里,整個國土都被義大利 360° 團團圍住。在地理學上,這種只被單獨一個國家完整包覆的國家,稱為「國中之國」。

國中之國通常都會與他的外覆國家維持著若即若離的關係,只要兩國沒有交惡。

  • 「若離」之處在於:他有自己的國旗,有自己的國家行政體系,有自己的貨幣,有自己的警政系統。單獨來看,他與其他的「非國中之國」並沒有什麼不同,都在聯合國裡享有一席,都是一個主權獨立的國家。
  • 「若即」之處在於:兩國關係通常都不會太差。像梵蒂岡,民生用水要靠義大利,因為自己沒有水源;民生用電要靠義大利,因為自己沒有發電廠。就連對外的陸上交通及航空,仍要仰賴義大利開放陸權及領空權,否則,裡面出不來,外面進不去,就形同遭受禁航禁運處分了。

某方面來說,虛擬機也是一種「國中之國」的概念:在 host OS 國度中,挖出一塊相對起來獨立自主的 guest OS 區域,且與外面的 host OS 維持著若即若離的關係。

  • 「若離」之處在於:藉由 hypervisor 之助,在這國中之國裡,可以有貌似真實且獨立的 CPU、記憶體、儲存空間、網路等資源,你可以放心在裡面胡搞瞎搞自由大膽實驗,不必擔心會污染到外面的 host OS。
  • 「若即」之處在於:畢竟這一切都是 hypervisor 的功勞。不管是全虛擬化還是半虛擬化技術,若 host OS 不釋出善意分享資源,guest OS 也只能含淚被禁航禁運了。

Vagrant 提供兩種管道,讓 guest OS 可與外界互通。本文就來探討 Vagrant 是怎麼處理這「國中之國」、「若即若離」的關係。

Guest OS 與外界溝通的兩個管道

Vagrant 執行時輸出的畫面,常常內含玄機,草草略過很可惜。像 vagrant up 指令執行時,有三處值得注意:

vagrant-4-up

畫面告訴我們,在用 vagrant up 啟動虛擬機的同時,Vagrant 已經偷偷幫我們建好兩條互通管道了,一個是透過 TCP/UDP 通訊埠轉接 (port forwarding),另一個是透過共享目錄 (shared folder)。畫面也告訴我們,當我們登入 guest OS 時,身份會是一個名為 “vagrant” 的帳號,登入至 127.0.0.1:2222

以下就帶你逐一細看這兩條互通管道。

共享管道之一:Port forwarding

vagrant up 所建立的第一條互通管道 port forwarding,請看這兩行輸出:

==> default: Forwarding ports...
    default: 22 => 2222 (adapter 1)

這兩行告訴我們,Vagrant 已將以下兩個 TCP port 連接起來:

  • Guest OS 的 22 port(也就是 SSH 服務預設的 TCP port)
  • Host OS 的 2222 port

再用白話文解釋幾遍此處的 port forwarding 有什麼意義:

  • Guest OS 裡面的 22 port 封包,會導至 host OS 的 2222 port;反之亦然。
  • Guest OS 裡面的程式,只要是與 22 port 連線,其實骨子裡就是在跟 host OS 裡的 2222 port 連線。
  • Host OS 裡面的程式,只要是與 2222 port 連線,其實骨子裡就是在跟 guest OS 裡的 22 port 連線。

當然啦,Vagrant 之所以能做到 port forwarding 這一點,也是仰賴底層的 VirtualBox;因此,這件事也可以在 VirtualBox 裡檢視。請點選該虛擬機的【設定值】→【網路】→【連接埠轉送】:

vagrant-4-ssh-port

由此可見,這台虛擬機,已經開放 127.0.0.1:2222 給虛擬機之外的「外界」透過 SSH 方式登入。

如果你想再新增其他的 port forwarding 規則,請在 Vagrantfile 裡,找到以下這一行:

  # config.vm.network "forwarded_port", guest: 80, host: 8080

把開頭的註解符號 # 刪掉,修改那兩個數字,再重新啟動此虛擬機 box 執行個體。

共享管道之二:共享目錄

vagrant up 所建立的第二條互通管道 shared folder(共享目錄),請見這兩行輸出畫面:

==> default: Mounting shared folders...
    default: /vagrant => /private/tmp/demo-1

你現在應該能看圖說故事依樣畫葫蘆了。這兩行告訴我們,Vagrant 已將以下兩個目錄連接起來:

  • Guest OS 的 /vagrant 目錄
  • Host OS 的工作目錄(即此例的 /private/tmp/demo-1

當然啦,Vagrant 之所以能做到「共享目錄」這一點,也是仰賴底層的 VirtualBox;因此,這件事也可以在 VirtualBox 裡檢視。操作步驟請見我的錄影: http://www.screencast.com/t/bwwP345Ewb5 ,本文就不贅述。

如果你想再新增其他的共享目錄,請在 Vagrantfile 裡,找到以下這一行:

  # config.vm.synced_folder "../data", "/vagrant_data"

把開頭的註解符號 # 刪掉,修改那兩個字串,再重新啟動此虛擬機 box 執行個體。

Port forwarding 實例:SSH

vagrant up 輸出畫面也告訴我們,預設情況下,被它啟動的虛擬機,會開放 127.0.0.1:2222 給外界,以 vagrant 的身份,透過 SSH 方式登入。

我們之前都是用 vagrant ssh 指令登入這台 guest OS:

$ # 懶人登入法
$ vagrant ssh
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-35-generic x86_64)
[略]
vagrant$

但其實這是 Vagrant 幫的忙,幫我們處理掉許多 SSH 登入的細節。

身為硬漢,就要嘗試用最原始的 ssh 方式登入!

不過,嘴硬的硬漢,少了庇蔭,就得手動輸入該 guest OS 對外開放的 IP 位址 (127.0.0.1)、TCP 通訊埠 (2222),甚至登入的帳號 (vagrant) 及密碼 (vagrant):

$ # 硬漢登入法
$ ssh  -p 2222  [email protected]
The authenticity of host '[127.0.0.1]:2222 ([127.0.0.1]:2222)' can't be established.
RSA key fingerprint is 42:f8:f0:d5:7b:8b:87:a4:34:d4:5b:7d:7f:1b:40:8a.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[127.0.0.1]:2222' (RSA) to the list of known hosts.
[email protected]'s password:
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-35-generic x86_64)
[略]

vagrant$

由此實驗可印證,host OS 裡面的程式,只要是與 2222 port 連線,其實骨子裡就是在跟 guest OS 裡的 22 port 連線。整個過程,請見以下錄影: http://www.screencast.com/t/KyY40sWK6X6

當然啦,平常我們不會這麼自找麻煩多打那麼多文字及數字。以上只是讓你確認 port forwarding 的作用,順便讓你知道 vagrant ssh 指令背後幫你做了哪些事情。知道這些隱藏在背後的細節,以後再遇到一些 Vagrant 內建工具沒辦法幫你節省力氣的場合(譬如說,用 Ansible、Capistrano 等外部程式動態操控 guest OS 的組態及行為),就知道該如何餵 SSH 帳號密碼給外部程式使用。

Port forwarding 實例:Redis

實務上,真實上線主機的 OS 裡面,不只會跑 SSH server 讓我們從遠端登入進去管理,還會跑許多像是 httpd、MongoDB、MySQL、Nginx、Redis 等 daemon,分別綁定 80、27017、3306、6379 等 port 作為對外服務端口,如下圖所示:

vagrant-4-typical-prod-setting

我們研發人員使用 Vagrant 管理虛擬機的初衷是,想在本機端就能模擬出真實上線主機的組態,越逼真越好。因此,我們也需要學習如何在單獨一台電腦裡,用 Vagrant 模擬出上述的組態配置。我們必須設法在 Vagrant 的遊戲規則中,妥善安排 host OS 與 guest OS 之間的相互關係,模擬出 client、server machine、server service、public port 等真實上線系統的組成元素。最直覺的做法如下圖:

vagrant-4-defective-dev-setting

不過,guest OS 是個獨立自主的國中之國,有自己獨立的內部網路,與外界網路隔絕。因此, guest OS 裡面的 service 原先所綁定的 port 服務端口,被 guest OS 完美地包覆起來,或者說,被完美地封印私藏起來,不讓外界直接觸碰,就像上圖的「禁止通行」交通標誌一樣。畢竟這是虛擬機底層 hypervisor 的重大責任:資源隔離。

因此,如果有需要把這些位於 guest OS 裡面的服務開放給 host OS 存取,就必須請 hypervisor 網開一面,個別放水 port forwarding,讓 guest OS 裡面的 port 能夠開放給外面相連。如下圖所示:

vagrant-4-typical-dev-setting

聽了一堆原理,現在,讓我們拿 Redis 當例子,親手實驗一下 port forwarding 設定前與設定後,到底有什麼不同。

實驗之前,為了方便講解起見,建議你把終端機畫面安排成這樣,以便自由切換 guest 及 host OS 兩種視角:

  1. 從 guest OS 角度,設定 Redis server 組態。
  2. 從 host OS 角度,設定 Vagrantfile,並試圖連線至 guest OS 裡面的 Redis server。

vagrant-4-two-pane-layout

Port forwarding 設定前

接下來,請在 guest OS 裡安裝好 Redis,並允許它 bind 至全部的 interface:

vagrant$  #---> 這裡是 guest OS。
vagrant$
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

安裝好了,我們先留在 guest OS 裡,試試看 Redis server 是否已經正常運作:

vagrant$  # 用 redis-cli 工具連線進入 Redis server,查看 Redis 資訊
vagrant$  redis-cli
127.0.0.1:6379>
127.0.0.1:6379>  info clients
# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
127.0.0.1:6379>
127.0.0.1:6379>  exit
vagrant$

連得進去。由此可見,在 guest OS 裡,Redis server 的確已經正常運作,也順利綁定在 Redis server 預設的 TCP port 6379 身上。

接著,切換到 host OS 視窗,看看從 host OS 裡,是否一樣能連得到位於 guest OS 裡面的 Redis server 呢?

$ #---> 這裡是 host OS。
$
$ # 在 host OS 裡,用同樣的 redis-cli 工具,
$ # 試試看是否連得進去 guest OS 裡面的 Redis...
$ redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected>

很可惜,這次連不進去了!由此可見,此刻,guest OS 裡面的 port 6379,並沒有開放給該虛擬機以外的「外界」存取。它已被虛擬機系統完美地保護起來,隔離起來。

Port forwarding 設定後

為了讓該虛擬機之外的「外界」能夠順利深入魔爪碰到裡面的這個 port,我們必須針對這個 port 設定 port forwarding 規則。步驟是:

  1. 登出 guest OS 並關機。
  2. 修改 Vagrantfile 裡面的 "forwarded_port" 規則:
    config.vm.network "forwarded_port", guest: 6379, host: 6379
  3. 將 guest OS 重開機。

做完後,guest OS 與 host OS 兩者的 6379 port,就會順利連接起來。

口說無憑,來印證一下吧。我們可以分別在 guest OS 及 host OS 兩處,重做前面的 redis-cli 連線查詢動作。如果你的步驟正確,這回,應該可以在 host OS 裡面看到 connected_clients:2 字眼,代表說,同時有兩個 redis-cli 正在連線到 guest OS 裡面的 Redis server:

$ #---> 這裡是 host OS。
$
$ # 在 host OS 裡,用同樣的 redis-cli 工具,
$ # 試試看是否連得進去 guest OS 裡面的 Redis...
$ redis-cli
127.0.0.1:6379>  info clients
# Clients
connected_clients:2
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
127.0.0.1:6379>

以上就是 port forwarding 的完整過程。如果你還是試不出來,請對照我的錄影 http://www.screencast.com/t/8NDUq3smsk 再試試看。

把 Redis 塞進 Docker 吧!

聽說最近 Docker 很夯,主流大廠紛紛表態支持。我們要不要試一試,把前面的 Redis 改安裝在 Docker 裡面?

一個典型的 Linux 雲端伺服主機,如果有支援所謂「Docker 化 (Dockerized)」的 Redis,那麼,它的軟體層,通常會由 Linux → Docker engine → Docker container → 「Docker 化的 Redis」層層堆疊上去,如下圖所示:

vagrant-4-defective-prod-docker-setting

某種程度上,Docker 號稱是輕量級的虛擬化技術,所以,從圖中可看出,Docker 這裡也會再次遇到跟前面 guest OS 類似的「國中之國」老問題,只不過換了個主場及主角:「Docker container 裡面的 port,並沒有開放給該 container 以外的『外界』存取。它已被 Docker engine 系統完美地保護起來,隔離起來。」解決方法也是一樣:port forwarding(在 Docker 世界裡,可能會用 binding、mapping、publishing 等術語來稱呼 port forwarding 這件事)。於是,典型的雲端伺服主機,應該會設定成這樣:

vagrant-4-typical-prod-docker-setting

因此,如果想在單獨一台本機電腦裡,用 Vagrant 模擬出上述的組態配置,整個軟體層次就會需要兩個層次的 port forwarding,一次是在內側的「Docker container ⇔ guest OS 之間」,一次是在外側的「guest OS ⇔ host OS 之間」:

vagrant-4-typical-docker-dev-setting

聽了一堆原理,現在,讓我們親手實驗一下吧!

首先,我們換一個全新的工作目錄 demo-2,啟動並登入 ubuntu/trusty64

$ mkdir demo-2
$ cd demo-2
$ vagrant init ubuntu/trusty64
$ vagrant ssh

然後,參考 Docker 官方文件,用以下指令替 Ubuntu 安裝 Docker engine:

# @see http://docs.docker.com/installation/ubuntulinux/

export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8

sudo apt-get update

# install Docker
curl -sL http://get.docker.io/ubuntu/ | sudo sh

# enable memory and swap accounting
sudo sed -i -e \
  's/^GRUB_CMDLINE_LINUX=.+/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"/' \
  /etc/default/grub
sudo update-grub

# enable UFW forwarding
sudo sed -i -e \
  's/^DEFAULT_FORWARD_POLICY=.+/DEFAULT_FORWARD_POLICY="ACCEPT"/' \
  /etc/default/ufw
#sudo ufw reload

# add 'vagrant' user to docker group
sudo usermod -aG docker vagrant

OK,我們已經有了一份 Ubuntu + Docker 系統。下一篇文章還會用到這段安裝過程,所以我也錄影起來了: http://www.screencast.com/t/7XqIo9Cepb4 ,以便日後重播。

現在,讓我們透過 Docker 的映像檔管理機制,下載、安裝一份「Docker 化 (Dockerized)」的 Redis server。我們選擇的是 Docker 官方製作的 “redis” 映像檔的最新版本 (redis:latest):

vagrant$  # NOW, pull redis image from official repository
vagrant$  docker pull redis:latest

安裝好了之後,可以開始啟動它。不過,如果你只是這樣執行:

vagrant$  # **DON'T** run it this way!
vagrant$  docker run  --name my-redis  -d  redis

就少設定了在內側的「Docker container ⇔ guest OS 之間」的 port forwarding。解決方法是,只要加上一個 -p 6379:6379 參數,就可以讓 container 外面存取得到這個 container 裡面的 6379 port:

vagrant$  # run it this way!
vagrant$  docker run  --name my-redis  -p 6379:6379  -d  redis

別忘了,還要再替外側的「guest OS ⇔ host OS 之間」也做一次 port forwarding。文章中間介紹過,這需要修改 Vagrantfile 定義檔裡面的 forwarded_port 設定值。

整個過程不難,只是繁瑣。建議你,先照著我的錄影做一遍:

如果試成功了,不妨再故意略掉一些 port forwarding 環節,看看 Redis 服務是否還能被 redis-cli 碰到。

恭喜你!你已經徹底學會了,怎麼樣從 host OS 連到 guest OS 裡面的 Docker 裡面的 Redis⋯⋯

乍聽之下有點饒舌。但整個概念其實頗一致的,就是一層一層地包覆保護,一層一層地 port forwarding 解除封印。以前行之有年的防火牆、NAT、reverse proxy 等技術,不也是一樣嗎?

現在,你應該看得懂 “Running Couchbase Cluster Under Docker” 這篇文章講的「從 Mac OS X 連到 VirtualBox 裡面的 CoreOS 裡面的 Docker 裡面的 Couchbase」是怎麼一回事了⋯⋯

辛辛苦苦把軟體設定好了,下一步,就是研究該如何讓這些心血不會因意外就消失不見,還得整個從頭來過。下一期,我將示範如何以 “infrastructure as code” 角度設定虛擬機組態,以及儲存、複製增生這些設定好的組態。

後續 >> Vagrant Tutorial(5)客製化虛擬機內容的幾種方法

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

Leon H.01/17

請問為什麼要建兩層虛擬化啊?用意何在?

關於作者

擔任過許多職場角色:程式設計師、技術團隊領班、技術作家及譯者、教授、顧問、技術佈道者,但目前最喜歡的身份,還是「軟體架構師」。

熱門論壇文章

熱門技術文章