VagrantでスタンダードなDev環境を構築する — 「自分のマシンでは動く」問題よ、さらば

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

深夜2時、dev環境が燃え上がる

深夜2時17分。stagingへのデプロイが完了し、すべて正常に動作していた。翌朝、developerからバグレポートが届く:「自分のマシンでは再現できない。」5分間画面を眺めて、ようやく原因に気づいた — そのdevはUbuntu 20.04を使っていて、サーバーはUbuntu 22.04で動いていて、自分はmacOS上でデバッグしていた。三つの異なる環境、一つのcodebase。

Vagrantはまさにその痛みを解決してくれる。「最高」でも「最先端」でもないが、理由はシンプルだ:VagrantfileをgitにコミットしておけGe ば、Mac、WindowsどのマシンでcloneしてもGeGe vagrant upするだけで同じ環境が手に入る。

アプローチの比較:手動VM、Docker、Vagrant

3つのツール、3つの設計思想。選ぶ前にそれぞれを正しく理解しよう — 間違ったツールを選んで痛い目を見たのは、これが初めてではない:

Approach 1: 手動VM (VirtualBox/VMware)

VMを手動で作成し、OSをインストールし、dependenciesをセットアップして、snapshotを取る。自分のhomelabではこの方法を使っていた — 現在はProxmox VEで12台のVMとcontainerを管理し、productionに上げる前に何でも試せるplaygroundとして活用している。homelabならいいが、dev teamでやると悪夢になる。

  • configをcodeとして共有できない
  • 各devが手動でsetup → 環境が統一されない
  • 新メンバーのonboardingに丸一日かかる
  • 環境をversion controlできない

Approach 2: Docker + Docker Compose

Container-based、軽量、portable。microservicesで最もよく使うアプローチだ。ただし、「本番サーバーに近い環境」が必要な場合にはDockerにも弱点がある:

  • ContainerはhostとkernelをShareする — full OS isolationではない
  • systemd、kernel module、または実際のnetwork stackのテストが必要なツールもある
  • Mac/WindowsでDocker Desktopを動かすと、Linux nativeと比べてoverheadが生じ、挙動も異なる — bind mountの処理方法の違いで、Linuxでしか再現できないpermissionのバグを2時間かけてデバッグしたことがある。Mac上ではfile 777でも何も問題が起きなかった
  • firewall設定、disk partitioning、kernel tuningのテストには向かない

Approach 3: Vagrant

VagrantはVMの作成と管理を一つのconfig file — Vagrantfile — でラップしてくれる。Dockerの代替ではなく、full VMが必要なケースでDockerを補完するものだ。

  • VagrantfileをgitにcommitできるためGeGe、環境がcodeになりversion historyが残る
  • Full VM isolation、kernel levelの検証も可能
  • 複数のproviderをサポート:VirtualBox、VMware、Hyper-V、libvirt
  • Vagrant VMの中でDockerを動かすことも可能

Vagrantのメリット・デメリット分析

メリット

  • Infrastructure as Code:VagrantfileはRuby DSLで記述され、読みやすく編集しやすく、gitにcommitできる
  • Reproduciblevagrant destroy && vagrant upで、まっさらな環境を再現できる
  • Multi-machine:一つのVagrantfileで複数のVMを定義できる(web + db + cache)
  • Provisioning統合:Shell script、Ansible、Chef、Puppetがすぐに使える
  • Shared folders:codeはhostに置き、VMの中で実行 — ファイルをコピーせずに使い慣れたIDEで編集できる

デメリット

  • Dockerより重い:Full VMはcontainerより遥かにRAMとdiskを消費する
  • 起動が遅い:VMの起動に30〜60秒かかるが、containerなら数秒で済む
  • Hypervisorのオーバーヘッド:VirtualBox(デフォルト)が必要 — WindowsではHyper-Vの競合によりDocker Desktopとconflictする可能性がある
  • Microservicesには不向き:serviceが20個あるようなprojectには、Docker Composeの方が遥かに合理的

VagrantかDockerか:どちらをいつ使う?

間違ったツール選択を何度も繰り返した末に辿り着いた、シンプルなルール:

  • Dockerを使う:web service、microservices、素早いspin up/down、CI/CDパイプライン
  • Vagrantを使う:system levelの検証、multi-serverのシミュレーション、チームにMac/WindowsユーザーがいるがサーバーはLinux、またはkernelとnetworkレベルでproductionと100%同一の環境が必要な場合

自分が最もよくVagrantを使う実際のユースケース:productionに適用する前のAnsible playbookのテスト、複数VMでのnetwork topologyのシミュレーション、特定のkernel versionでしか再現しないバグの再現。

Vagrantをゼロからセットアップする手順

ステップ1:インストール

VagrantにはVM providerが必要だ。デフォルトではVirtualBoxを使う:

# Ubuntu/Debian — VirtualBoxをインストール
sudo apt update && sudo apt install -y virtualbox

# HashiCorpの公式リポジトリからVagrantをインストール
wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vagrant

# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/vagrant
# インストール確認
vagrant --version
# Vagrant 2.4.x

ステップ2:最初のVagrantfileを作成する

projectディレクトリを作成してVagrantfileを書く。vagrant initで生成するより手書きの方が多い — 生成されるfileはコメントで埋め尽くされていて、読むだけで半日かかる。自分で書いた方が短くてずっと分かりやすい。以下はシンプルなLAMP stackのconfig:

Vagrant.configure("2") do |config|
  # Base box — Ubuntu 22.04 LTS
  config.vm.box = "ubuntu/jammy64"
  config.vm.box_check_update = false

  # port 80と3306をhostに転送
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.network "forwarded_port", guest: 3306, host: 3306

  # 静的IPのprivate network(覚えやすい)
  config.vm.network "private_network", ip: "192.168.56.10"

  # Shared folder:hostのcodeをVM内の/var/wwwにマウント
  config.vm.synced_folder "./src", "/var/www/html"

  # VMリソース
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "2048"
    vb.cpus = 2
    vb.name = "dev-lamp"
  end

  # Provisioning:VM初回作成時にshell scriptを実行
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update -q
    apt-get install -y -q apache2 mysql-server php php-mysql
    systemctl enable apache2 mysql
    systemctl start apache2 mysql
    echo "完了!アクセス先: http://192.168.56.10"
  SHELL
end

ステップ3:VMを起動してSSH接続する

# VMを作成・起動(初回はboxのダウンロード ~500MB)
vagrant up

# VMにSSH接続
vagrant ssh

# VM内で確認
uname -a
# Linux ubuntu-jammy 5.15.0-xx-generic ...

# 終了
exit

ステップ4:日常のワークフロー

# VMを起動(作成済みのため初回より速い)
vagrant up

# 一時停止 — RAMの状態を保持(最速)
vagrant suspend
vagrant resume

# VMを完全にシャットダウン(RAMを解放)
vagrant halt

# VMを完全に削除して最初から再作成
vagrant destroy -f && vagrant up

ステップ5:Multi-machine — productionアーキテクチャをシミュレートする

これこそが手動セットアップと比べてVagrantが真に光る部分だ。web serverとdatabase serverを分離してシミュレートし、まさにproductionと同じ構成を再現する:

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

  # Webサーバー
  config.vm.define "web" do |web|
    web.vm.hostname = "web-server"
    web.vm.network "private_network", ip: "192.168.56.10"
    web.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
    web.vm.provision "shell", inline: "apt-get install -y -q nginx"
  end

  # データベースサーバー
  config.vm.define "db" do |db|
    db.vm.hostname = "db-server"
    db.vm.network "private_network", ip: "192.168.56.11"
    db.vm.provider "virtualbox" do |vb|
      vb.memory = "2048"
    end
    db.vm.provision "shell", inline: "apt-get install -y -q postgresql"
  end
end
# すべてのmachineを起動
vagrant up

# web serverのみ起動
vagrant up web

# 各machineにSSH接続
vagrant ssh web
vagrant ssh db

Tip:shell inlineの代わりにAnsible provisionerを使う

shell script inlineはさっと試すには十分だ。本格的なprojectでは、Ansible provisionerに切り替えることにしている:

config.vm.provision "ansible" do |ansible|
  ansible.playbook = "provision/setup.yml"
  ansible.verbose = "v"
end

一石二鳥の利点:同じplaybookを本番の実サーバーにそのままapplyできる — まさに「configの一行一行までproductionと同じdev環境」を実現できる。

実践的な結論

環境の不一致のせいで何度も深夜2時にデバッグを繰り返した末に至った結論:どちらかを選ぶ必要はない。VagrantでVMを動かし、そのVM内でDockerのcontainerを動かす — この組み合わせでほぼすべてのユースケースをカバーできる。

「サーバーでしか起きない」バグを再現したい?新メンバーをsetupで半日潰さずにonboardしたい?productionに適用する前にAnsible playbookをテストしたい?git repoのVagrantfileが答えだ。vagrant upで完了。

Share: