BETA

Windows 10 で Vagrant と Ansible_local、Hyper-V を使って Infrastructure as Code をする方法

投稿日:2020-01-22
最終更新:2020-02-28

★追記(2020/02/28)
1/27 にリリースされた Vagrant 2.2.7 では、VirtualBox 6.1.x がサポートされたみたいです。(まだ試していません)
https://github.com/hashicorp/vagrant/blob/v2.2.7/CHANGELOG.md

VirtualBox 6.1 から、WSL2 と VirtualBox が同居できるようになったので、今回の Vagrant のアップデートによって、WSL2 を使用している環境でも VirtualBox と Vagrant を使った開発ができるようになりました。

なお、同居した場合、同居していない環境よりもパフォーマンスが劣化するようです・・・
https://arohajiro.qrunch.io/entries/eqsXCIJGVNtmvBsw

<< 追記ここまで

仕事で Windows 10 を使っていますが、Infrastructure as Code(IaC) をしたいなと思ったときに、一般的に IaC をするときに使われているこれらのツールのいくつかがインストールできなかったり、開発環境で利用するまでに苦労する場合があります。

  • Vagrant
  • Ansible
  • Serverspec
  • VirtualBox

例えば、Ansible は Windows ホストにはインストールできませんし、WSL / WSL2を使っている環境では VirtualBox と Vagrant を使うために、結構な手順を踏む必要があったり、バージョンに気をつけなければ行けないなど、開発を行うまでにかなりのハードルを超える必要があります。

WSL 2 に関する FAQ

Hyper-v が使用されている場合、一部のサードパーティアプリケーションが動作しないことを意味します。これは、WSL 2 が有効になっている場合、これらのアプリケーションを実行できないことを意味します。 残念なことに、これには VMware と、VirtualBox 6 より前の VirtualBox のバージョンが含まれます

VirtualBox 6.1 を使用すると、WSL2 と同居できますが、Vagrant が 2020/01/22 現在 VirtualBox 6.1 に対応していなかったりなど、僕自身相当試行錯誤しました。

そして、結果的に行き着いた構成が次のような構成です。

  • Vagrant
  • Ansible_Local(Vagrant の Provisioner)
  • Serverspec
  • Hyper-V

この構成は各ツールをかなりシンプルにインストールできます。
なお、WSL2 をインストールしている環境でも動作しますが、今回の IaC では WSL2 は使いません。

この構成のポイントは、Windows で Ansible の Playbook を作成できることです。
今回は、これらの構成を取る方法と、実際に IaC をする方法について紹介したいと思います。

なお、今回使用したツール、OS のバージョンは次のとおりです。

OS/ツール バージョン
OS Windows 10 バージョン 2004
VSCode 1.41.1
Vagrant 2.2.6
Ruby 2.6.5p114 (2019-10-01 revision 67812) [x64-mingw32]
Bundler 2.1.4
Serverspec 2.41.5
Rake 13.0.1
ゲストOS CentOS 7.6.1811 (Box のバージョンは v1905.1)

Hyper-V のインストール

Hyper-V のインストールは、管理者で PowerShell を起動して次のコマンドレットを実行します。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All  

インストール後は再起動します。

参考: Windows 10 上に Hyper-V をインストールする

Vagrant のインストール

Vagrantの公式サイトからインストーラをダウンロードしてインストールします。
Chocolatey の場合は次のコマンドでインストールできます。

choco install vagrant  

ワークスペースの作成

ワークスペースは次のような構成で作成しました。

/  
├─ ansible (Ansible 関連のファイルのルートフォルダ)  
├─ serverspec (Serverspec 関連のファイルのルートフォルダ)  
└ .gitignore  

Vagrantfile の作成

次にワークスペースのルート直下に Vagrantfile を作成します。

vagrant init  

Vagrant の Provider として Hyper-V を使用するように Vagrantfile を修正します。
この Vagrantfile では、WebサーバーとDBサーバーの2つのインスタンスを生成します。

# -*- mode: ruby -*-  
# vi: set ft=ruby :  

# All Vagrant configuration is done below. The "2" in Vagrant.configure  
# configures the configuration version (we support older styles for  
# backwards compatibility). Please don't change it unless you know what  
# you're doing.  
Vagrant.configure("2") do |config|  
  config.vm.synced_folder './ansible', '/home/vagrant/ansible', create: true, owner: 'vagrant', group: 'vagrant'  
  config.vm.box = "centos/7"  

  config.vm.define "web" do |w|  
    w.vm.hostname = "web-sv"  
    w.vm.provider "hyperv" do |h|  
      h.vmname = "web-sv"  
      h.memory = "4096"  
      h.cpus = 2  
    end  
    w.vm.provision "ansible_local" do |ansible|  
      ansible.compatibility_mode = "2.0"  
      ansible.provisioning_path = "/home/vagrant/ansible/"  
      ansible.playbook = "web_setup.yml"  
      ansible.verbose        = true  
      ansible.inventory_path = "inventory.ini"  
      ansible.limit          = "all"  
    end  
  end  

  config.vm.define "db" do |db|  
    db.vm.hostname = "db-sv"  
    db.vm.provider "hyperv" do |h|  
      h.vmname = "db-sv"  
      h.memory = "4096"  
      h.cpus = 2  
    end  
    db.vm.provision "ansible_local" do |ansible|  
      ansible.compatibility_mode = "2.0"  
      ansible.provisioning_path = "/home/vagrant/ansible/"  
      ansible.playbook = "db_setup.yml"  
      ansible.verbose        = true  
      ansible.inventory_path = "inventory.ini"  
      ansible.limit          = "all"  
    end  
  end  
end  

Vagrantfile の解説

Vagrant で作成した VM に共有フォルダとして、ワークスペースの ansible フォルダを、VM の /home/vagrant/ansible ディレクトリとしてマウントします。
後述しますが、VM にマウントされた ansible フォルダ内の Playbook をローカルでプロビジョニングします。

config.vm.synced_folder './ansible', '/home/vagrant/ansible', create: true, owner: 'vagrant', group: 'vagrant'  

provider には Hyper-V を使用するので、次のように記述します。
ブロックの内部で Hyper-V で表示されるインスタンス名やメモリ容量、CPU数などを指定します。

w.vm.provider "hyperv" do |h|  
  h.vmname = "server1"  
  h.memory = "4096"  
  h.cpus = 2  
end  

Provisioner には Ansible_local を指定します。
provisioning_path には前述した VM にマウントされた /home/vagrant/ansible/ を指定します。
ここで指定したパスがプロビジョニングを行うためのルートディレクトリとなります。
また、playbook ではプロビジョニングをする際に使用する Playbook の名前を指定します。

それぞれのインスタンスの define のブロック内に provison のブロックを記載しているので、
インスタンス別にプロビジョニングの設定を変えることができます。

    w.vm.provision "ansible_local" do |ansible|  
      ansible.compatibility_mode = "2.0"  
      ansible.provisioning_path = "/home/vagrant/ansible/"  
      ansible.playbook = "web_setup.yml"  
      ansible.verbose        = true  
      ansible.inventory_path = "inventory.ini"  
      ansible.limit          = "all"  
    end  

インスタンスの起動

この時点で一旦インスタンスを起動してみます。
ワークスペース内の ansible フォルダは空なので、--no-provision オプションを指定して、一旦、Vagrant でのプロビジョニングをスキップします。

vagrant up --provider hyperv --no-provision  

vagrant up 後に usernamepassword を聞かれます。
その際に、Windows にログインしているアカウント名とパスワードを入力します。

PS C:\Users\XXX> vagrant up --provider hyperv --no-provision  
Bringing machine 'web' up with 'hyperv' provider...  
Bringing machine 'db' up with 'hyperv' provider...  
==> web: Verifying Hyper-V is enabled...  
==> web: Verifying Hyper-V is accessible...  
    web: Configuring the VM...  
==> web: Starting the machine...  
==> web: Waiting for the machine to report its IP address...  
    web: Timeout: 120 seconds  
    web: IP: XXX.XXX.XXX.XXX  
==> web: Waiting for machine to boot. This may take a few minutes...  
    web: SSH address: XXX.XXX.XXX.XXX:22  
    web: SSH username: vagrant  
    web: SSH auth method: private key  
==> web: Machine booted and ready!  

Vagrant requires administrator access for pruning SMB shares and  
may request access to complete removal of stale shares.  
==> web: Preparing SMB shared folders...  
    web: You will be asked for the username and password to use for the SMB  
    web: folders shortly. Please use the proper username/password of your  
    web: account.  
    web:  
    web: Username: <windows username> # Windows アカウント名を指定する  
    web: Password (will be hidden):  # Windows アカウント名のパスワードを入力  

Vagrant requires administrator access to create SMB shares and  
may request access to complete setup of configured shares.  
==> web: Setting hostname...  
==> web: Mounting SMB shared folders...  
    web: C:/XXX/iac_sample/ansible => /home/vagrant/ansible  
==> web: Machine not provisioned because `--no-provision` is specified.  

これで Hyper-V に Vagrant を使ってインスタンスを生成できました。

Serverspec のインストール

Ruby のインストール

RubyInstaller でインストールする場合は、こちら からダウンロードできます。
Chocolatey でインストールする場合は次のコマンドを実行します。

choco install ruby  

僕の場合は、Bundler をローカルにインストールしました。

gem install bundler  

Serverspec フォルダに移動して、Gemfile を作成します。

bundle init  

次のように Gemfile を編集します。

# frozen_string_literal: true  

source "https://rubygems.org"  

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }  

gem "serverspec"  
gem "rake"  

その後、インストールします。

bundle install --path vendor/bundle  

Serverspec の初期設定

次のコマンドで Serverspec の初期設定をします。

PS C:\Users\XXX> bundle exec serverspec-init  
Select OS type:  

  1) UN*X  
  2) Windows  

Select number: 1  

Select a backend type:  

  1) SSH  
  2) Exec (local)  

Select number: 1  

Vagrant instance y/n: y  
Auto-configure Vagrant from Vagrantfile? y/n: y  
Choose a VM from the Vagrantfile: default  
 + spec/  
 + spec//sample_spec.rb  
 + spec/spec_helper.rb  
 + Rakefile  
 + .rspec  

spec フォルダの下に、Vagrantfile で定義したインスタンス名のフォルダを作成します。

├─spec  
│  ├─db  
│  └─web  
:  
:  

Serverspec を初期設定すると、Apache のインストールをテストする sample_spec.rb が自動で作成されます。

require 'spec_helper'  

describe package('httpd'), :if => os[:family] == 'redhat' do  
  it { should be_installed }  
end  

describe package('apache2'), :if => os[:family] == 'ubuntu' do  
  it { should be_installed }  
end  

describe service('httpd'), :if => os[:family] == 'redhat' do  
  it { should be_enabled }  
  it { should be_running }  
end  

describe service('apache2'), :if => os[:family] == 'ubuntu' do  
  it { should be_enabled }  
  it { should be_running }  
end  

describe service('org.apache.httpd'), :if => os[:family] == 'darwin' do  
  it { should be_enabled }  
  it { should be_running }  
end  

describe port(80) do  
  it { should be_listening }  
end  

sample_spec.rbspec フォルダ配下の web フォルダに移動します。
テストを実行する際は、次のコマンドを実行します。

bundle exec rake spec  

まずはテストが失敗することを確認してみます。

PS C:\Users\XXX> bundle exec rake spec  

Package "httpd"  
  is expected to be installed (FAILED - 1)  

Service "httpd"  
  is expected to be enabled (FAILED - 2)  
  is expected to be running (FAILED - 3)  

Port "80"  
  is expected to be listening (FAILED - 4)  

Failures:  

  1) Package "httpd" is expected to be installed  
     On host `web'  
     Failure/Error: it { should be_installed }  
       expected Package "httpd" to be installed  
       sudo -p 'Password: ' /bin/sh -c rpm\ -q\ httpd  
       package httpd is not installed  

     # ./spec/web/sample_spec.rb:4:in `block (2 levels) in <top (required)>'  

  2) Service "httpd" is expected to be enabled  
     On host `web'  
     Failure/Error: it { should be_enabled }  
       expected Service "httpd" to be enabled  
       sudo -p 'Password: ' /bin/sh -c systemctl\ --quiet\ is-enabled\ httpd  

     # ./spec/web/sample_spec.rb:12:in `block (2 levels) in <top (required)>'  

  3) Service "httpd" is expected to be running  
     On host `web'  
     Failure/Error: it { should be_running }  
       expected Service "httpd" to be running  
       sudo -p 'Password: ' /bin/sh -c systemctl\ is-active\ httpd  
       unknown  

     # ./spec/web/sample_spec.rb:13:in `block (2 levels) in <top (required)>'  

  4) Port "80" is expected to be listening  
     On host `web'  
     Failure/Error: it { should be_listening }  
       expected Port "80" to be listening  
       sudo -p 'Password: ' /bin/sh -c ss\ -tunl\ \|\ grep\ -E\ --\ :80\\\   

     # ./spec/web/sample_spec.rb:27:in `block (2 levels) in <top (required)>'  

Finished in 1.24 seconds (files took 21.78 seconds to load)  
4 examples, 4 failures  

Failed examples:  

rspec ./spec/web/sample_spec.rb:4 # Package "httpd" is expected to be installed  
rspec ./spec/web/sample_spec.rb:12 # Service "httpd" is expected to be enabled  
rspec ./spec/web/sample_spec.rb:13 # Service "httpd" is expected to be running  
rspec ./spec/web/sample_spec.rb:27 # Port "80" is expected to be listening  

4 examples, 4 failures という結果となりました。
4つのテストを実行し、4つの失敗が検知されたことを意味します。
次に、Ansible を使って、このテストをパスする Playbook を作成します。

Ansible の設定

ワークスペースの ansible フォルダに移動し、まずは inventory.ini を作成します。
Playbook が実行されるのは Vagrant で作成したインスタンス上なので、inventory.ini には ansible_connection=local を指定します。

[web]  
127.0.0.1 ansible_connection=local  

[db]  
127.0.0.1 ansible_connection=local  

[web_servers:children]  
web  

[db_servers:children]  
db  

続いてメインとなる Playbook を作成します。
Vagrantfile の ansible.playbook で指定したファイル名 web_setup.yml で Playbook を作成します。
この Playbook には実行する Ansible のロールのみを記述します。

---  
- name: Setup web Server  
  hosts: web_servers  
  become: true  
  gather_facts: no  

  roles:  
    - { role: common, tags: common }  
    - { role: web, tags: web}  

続いて roles フォルダを作成し、次のような構成にします。
ここでは webサーバー、db サーバーに共通する処理をまとめた common フォルダと、web サーバーのインストール処理をまとめた web フォルダを作成しました。

roles  
├─common  
│  └─tasks  
│          main.yml  
│  
└─web  
    └─tasks  
            main.yml  

common フォルダのタスクでは次の処理を行うように記載しました。

  • SELinux の無効化
  • yum パッケージのアップデート
---  
- name: Disable SELinux  
  selinux:  
    state: disabled  

- name: update yum packages  
  yum:  
    name: '*'  
    state: latest  
    update_cache: yes  

web フォルダのタスクでは次の処理を行うように記載しました。

  • Apache のインストール
  • 80 番ポートの開放
  • サービスの start、enable

Vagrant で生成したインスタンスはデフォルトで Firewalld が停止しているっぽいです。
その状態で- name: Allow ports for firewalldを実行するとエラーになるので、Firewalld のステータスを確認し、開始していれば処理を行い、それ以外の場合はスキップするようにしました。

---  
- name: Install Apache  
  yum:  
    name: "httpd"  
    state: present  

- name: Check firewalld status  
  service_facts:  
  no_log: true  
  register: service_state  

- name: Allow ports for firewalld  
  firewalld:  
    port: 80  
    permanent: yes  
    immediate: yes  
    state: enabled  
  when: service_state.ansible_facts.services['firewalld.service'].state == "running"  

- name: Start and enable httpd service  
  systemd:  
    name: httpd  
    enabled: yes  
    state: started  

Playbook の実行

Ansible_local を実行するには、vagrant provison コマンドを実行します。
初回の実行ではインスタンスに Ansible がインストールされるため結構時間がかかります。
2回目以降は即時 Playbook の処理が行われます。

2つのインスタンスのうち、web だけをプロビジョニングしたい場合は vagrant provison web のようにインスタンス名を指定します。

PS C:\Users\XXX\ansible> vagrant provison web  
==> web: Running provisioner: ansible_local...  
    web: Installing Ansible...  
    web: Running ansible-playbook...  

(省略)  

PLAY [Setup web Server] ********************************************************  

TASK [common : Disable SELinux] ************************************************  
[WARNING]: SELinux state temporarily changed from 'enforcing' to 'permissive'.  
State change will take effect next reboot.  
changed: [127.0.0.1] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "configfile": "/etc/selinux/config", "msg": "Config SELinux state changed from 'enforcing' to 'disabled'", "policy": "targeted", "reboot_required": true, "state": "disabled"}  

TASK [common : update yum packages] ********************************************  

(省略)  

PLAY RECAP *********************************************************************  
127.0.0.1                  : ok=5    changed=4    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0  

すべて OK になっていることを確認します。
failed があれば内容を確認して、Playbook を修正します。

再テスト

Ansible でのプロビジョニングが完了したら、再度 Serverspec でテストを行います。
初回の実行ではすべてのテストが失敗していましたが、プロビジョニング後は正常にテストがパスしていることが確認できます。

PS C:\Users\XXX> bundle exec rake spec  

Package "httpd"  
  is expected to be installed  

Service "httpd"  
  is expected to be enabled  
  is expected to be running  

Port "80"  
  is expected to be listening  

Finished in 0.98338 seconds (files took 22.78 seconds to load)  
4 examples, 0 failures  

まとめ

Hyper-V を使っているので、Vagrantfile が Windows に依存した内容になってしまっていることがネックにはなりますが、Windows PC でもなんとか Vagrant や Ansible を使った IaC ができるようになりました。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@toyocyの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう