Yuanchieh's Blog

Yuanchieh's Blog

生命是長期而持續的累積

12 Apr 2020

Vagrant 教學- 從本地端開發到 AWS 部署

大綱 Infrusture as Code 挑戰賽 - Hashicorp 工具鏈全教學

Vagrant 主要是建立與管理 VM 的工具,主要希望在工作流程中提供一致的環境,例如說有新人加入開發,需要在本地端設定 Runtime 環境 / 資料庫等,這時候如果用 Vagrant 使用 $vagrant up 一鍵啟動所有需要的環境,就非常的方便,也可以避免「明明在我的電腦就沒問題」的尷尬

Vagrant 主要是 VM-based 的工具,預設使用 Virtualbox,也可以用 VMWare / AWS 等虛擬化平台

本次教學目標為部署一個 nodejs api server + mongodb

github repo 在此 vagrant-getting-started

Install

下載 Vagrant
下載 Virtualbox

Terminology

在 Vagrant 有幾個名詞先介紹

  1. provider:
    提供 Vagrant 虛擬化的環境,預設是 virtualbox,也可以是其他的第三方 provider
  2. box:
    對比是 Docker 的 Image,也就是 Vagrant 虛擬化啟動的 Image,透過這個基礎再去客製化,以下用 ubuntu 示範
  3. provision:
    客製化環境的每一個步驟,可以用 shell執行 shell script、file 上傳檔案,或是搭配 chef/ansible 等 provisioning 工具

Get Started!

首先建立一個檔案夾 vagrant-demo,先拉下等等要用的 box,

[Host]
$ vagrant box add ubuntu/trusty64

可以在這邊找到你想要的 box,Discover Vagrant Boxes

接著建立 Vargrantfile 文件,裡頭寫入

Vagrant.configure("2") do |config|
    config.vm.box = "ubuntu/trusty64"
end

首先看一下語法,Vagrant 設定檔是用 Ruby 語法,當然不會 Ruby 也可以使用,Vagrant.configure("2") 定義 Vagrant 的語法版本,接著 do |config| ... end 定義 vm 的設定,config 是我們在這個 block 中的 vm 名稱;
config.vm.box 則是指定採用的 box,config.vm 底下有很多參數可以指定,請參考文件

接著執行

[Host]
$ vagrant up

如果安裝正確,vagrant 會直接啟動新的 vm,如果直接開 virtuabox 會看到一個新的 vm instance
$vagrant up 表示依序路徑讀取 Vagrantfile(當前路徑 –沒有找到再往–> ../當前路徑 -> ….),並啟動新的 vm instance,啟動後會發現路徑下多了 ./.vagrant 的資料夾,這主要是記錄 vagrant 執行狀態

接著讓我們 ssh 進入 vm 看一下

[Host]
$ vagrant ssh

ssh 用的 key 等等 vagrant 都處理好了,ssh 進入後,預設操作的 user 是 vagrant,先看一下 /vagrant

[VM]
$ ls /vagrant

你會意外發現 /vagrant 裡頭竟然有 Vagrantfile,主要是 vagrant 預設會把本地端 Vagrantfile 同在的資料夾一並同步到 vm 底下的 /vagrant 當中
依照共享的不同,可能是 rsync 一次性複製,也可以透過 SMB 雙向同步共享資料夾

provisioning

接著我們安裝上 nodejs,讓我們透過 shell script 安裝 nvm,並建立 server 資料夾放到 vm 中,接著透過 pm2 啟動
將 Vagrantfile 改成

Vagrant.configure("2") do |config|
    config.vm.box = "ubuntu/trusty64"
    
    config.vm.provision :shell, path: "init.sh", privileged: false
    config.vm.provision "file", source: "server", destination: "/home/vagrant/"
    config.vm.provision :shell, path: "start.sh", privileged: false

    config.vm.synced_folder ".", "/vagrant", 
        id: "sync", 
        type: "rsync", 
        rsync__exclude: [".git/", "server/", "start.sh"]
end

每一個 config.vm.provision 代表一個步驟,我們指定了

  1. 執行 init.sh:主要是安裝 nvm 與 pm2
  2. file 是用來複製檔案,將 server 路徑複製到 /home/vagrant/ 底下
  3. start.sh 主要是啟動 nodejs server

config.vm.synced_folder 則是顯示指定我們要同步到 /vagrant 底下的資料,也可以指定到其他資料夾下,不過要小心處理檔案路徑

接著執行

[Host]
$ vagrant provision

注意如果這時候跑 $ vagrant up 是沒有用的,因為 vm 已經啟動了,如果是 Vagrantfile 增加 provision,記得用 $ vagrant provision$ vagrant reload --provision,如果是 Vagrantfile 其他設定檔有更動,請用 $ vagrant reload

此時 ssh 登入後可以看到 nodejs server 已經在執行了

這時候好好說一下 shell 需要注意的地方,

....
source ~/.nvm/nvm.sh
nvm install 12
npm install -g pm2

可以看到我在 init.sh / start.sh 要呼叫 nvm/pm2 之前都要 $source ~/.nvm/nvm.sh 而非 $source ~/.bash.sh,主要是因為預設 vagrant 的 .bashrc 有這一段

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

而我們又將 nvm 的啟動 script 放在這一段之後才會導致 shell 找不到 nvm command,所以要改成$source ~/.nvm/nvm.sh,這部分是參考 stackoverflow Why is nvm command installed as root and also not found during vagrant bootstrap.sh?

其他的 provisioning 工具搭配請參考文件

destroy

如果開發過程中有誤,建議 Vagrantfile 改完砍到 vm 重來,使用 $ vagrant destroy,主要是每次執行 $ vagrant provision 會不斷在原本的 vm instance 操作,這樣會導致每次疊加結果而違反 immutable

network

在 Vagrant 中,網路大致有三種設定方式

  1. Port Forward
    讓 Host 環境可以用指定 port 對應 VM 中的 port
  2. Private Network
    指定 private ip 讓內網的機器都能透過 ip 溝通
  3. Public Network
    Vagrant 預設整合 Ngrok,可以用公開連結連線

此時 nodejs server 只能在 vm 內部使用,我們用 Port Forward 方式讓 host 也可以呼叫
修改 Vagrantfile

Vagrant.configure("2") do |config|
    ...
    config.vm.network "forwarded_port", guest: 80, host: 8080
end

接著用 $ vagrant reload 這時候在 Host 就可以發請求到 VM 上囉 $ curl localhost:8080

小結

在 Provisioning 部分,個人覺得用 shell script 有點不太方便,例如說切換 OS 時 script 就需要修改,而且很指令式而非宣告式,如果能用其他的 provisioning tool 去管理,又或是使用 image / container 去隔離對底層 OS 的相依,這勢必會方便很多

Vagrant 有提供 AWS provider,但因為是社群開發套件就暫且不試,專注在搭建本地端的開發環境

目前是只有一台 api server,接著要配置 DB 並放在同一個 Vagrantfile 中整理

Multi machile

如果要在 Vagrantfile 中定義多個 vm instance,可以透過 config.vm.define 區隔,預設會繼承全域的所有 provision,但是個別定義中可以複寫或是客製化,如以下

Vagrant.configure("2") do |config|
    config.vm.box = "ubuntu/trusty64"
    
    config.vm.define "api" do |api|
        api.vm.provision :shell, path: "init.sh", privileged: false
        api.vm.provision "file", source: "server", destination: "/home/vagrant/"
        api.vm.provision :shell, path: "start.sh", privileged: false

        api.vm.synced_folder ".", "/vagrant", 
            id: "sync", 
            type: "rsync", 
            rsync__exclude: [".git/", "server/", "start.sh"]

        api.vm.network "forwarded_port", guest: 3000, host: 8080
    end 

    config.vm.define "db" do |db|
        db.vm.provision :shell, path: "./mongodb/install.sh", privileged: false
    end
end

如果想要指定其中一台下指令,針對名稱就好如 $vagrant ssh db

結語

個人覺得開發不推薦用 Vagrant (那怎麼花了一個假日…),主要是社群看起來沒有很活躍,例如最多人下載的 box ubuntu/trusty64 還是在 14.04 的版本,想要直接找 Mongodb 的 box 也沒有(大多的 db 都沒有),且整個配置上沒有很方便,例如 shell script 切換 os 就要重寫,還不如用 docker + docker compose 來得快速
但如果你是一定要用 vm 那或許 Vagrant 還是個不錯的選擇

如果你有其他建議,又或是有覺得 Vagrant 有厲害獨特的應用場景再麻煩指教~