AnsibleでKVMも構成管理

この記事は [2016年 Livesense Advent Calendar](http://qiita.com/advent-calendar/2016/livesense_1) 12日目の記事です
本日はインフラストラクチャーグループの@nashioxがお送りします

# はじめに
みなさんAnsible使ってますか？
サーバの構成管理にAnsibleを使うとすごく便利ですよね
リブセンスでも以前はサーバの構築やインフラのオペレーションがあると手順書を書いてやっていたのですが、最近ではAnsibleで書いて適用してしまうというケースも増えてきました

Ansibleで構成管理するものが増えるに連れて、最近こう考えるようになってきました
「あとAnsibleで構成管理できていないものは何だ。。。そうかKVMゲストか」

libvirt等を使ってKVMゲストを構築、その後ansibleでプロビジョニングというのが普段のVM構築の流れなのですが、
このKVMゲスト構築までAnsibleで管理できれば1アクションのみでVMの構築が終わってしまうんです
なんて素晴らしい世界でしょうか

ということで、AnsibleでKVMも構成管理してみました

# まずは情報を整理
まずはAnsibleでKVMを構成管理ために必要な条件等を整理してみます

- マシン構成はAnsibleサーバ1台とKVMホストN台、ISOやkickstartファイルを配信するWebサーバの構成
- Ansibleサーバからplaybookを実行するとKVMホストにKVMゲストが構築される
    - この時、プロビジョニングで inventory_hostname や gather_facts の情報を利用するため、inventoryファイルには構築されるKVMゲストの情報が記載されるものとする
- 複数台、異なるスペック等にも可能な限り対応できる構成にする

上記のような構成でやっていきます
なお、検証環境にはCentOS7を用います

# いざ実装
もともと下記のようなVMのプロビジョニング用playbookがある前提で進めていきます

```yaml
- hosts: all
  become: yes
  gather_facts: yes
  roles:
    - install_packages
```

## 構築するKVMゲストの情報を記載する
KVMゲストを構築する際には、概ね以下のような情報が必要となります

- KVMゲストのIPアドレス
- KVMホストのIPアドレス
- CPUのコア数
- メモリサイズ
- ディスク容量

> 複数台、異なるスペック等にも可能な限り対応できる構成にする

KVMゲストのIPアドレスはinventoryファイルに記載するのでいいとして、上記のような制約もあるため残りの情報をどう保持しようか悩みました
そんなときは`host_vars`を使いましょう

```
$ tree host_vars
host_vars
└── 192.168.10.1

$ cat host_vars/192.168.10.1
cpu: 1
memory: 4
disk: 40
host_ip: '192.168.1.1'
```

上記のように host_vars ディレクトリ内に inventoryファイルに記載するKVMゲストのIPでファイルを作成、その中に構成情報を記載します
こうすることで、ホストごとのvarsを設定することが出来ます

## setupの実行を遅らせる
KVMゲストのIPをinventoryファイルに記載することで上記の問題は解決しそうでしたが、今度は inventory_hostname や gather_facts が問題となりました

> この時、プロビジョニングで inventory_hostname や gather_facts の情報を利用するため、inventoryファイルには構築されるKVMゲストの情報が記載されるものとする

上記のような制約があるため、gather_factsをyesにしたいのですが、playbook実行時にはまだKVMゲストは存在しません
そのため、SSHできないよと言われて実行が失敗してしまいます

SSHの判定を行っているのが`setup`というタスクなので、これの実行を遅らせられないかと考えました
そして以下のようにplaybookを修正しまいた

```yaml
- hosts: all
  become: yes
  gather_facts: no
  pre_tasks:
    ~~~~ KVMゲスト作成 ~~~~~

    ### setupを遅らせる
    - name: gather facts
      action: setup
  roles:
    - install_packages
```

gather_factsをnoにしてpre_tasksの中でKVMゲストの構築を行うようにしています
そして、pre_tasksの最後に`action: setup`をすることで`gather_facts: yes`としたときと同様かつsetupの実行が遅れるため、SSHの接続確認を遅らせることができます

あとはKVMゲスト構築の部分を作成して、setup実行前にSSHできる状態にしておけば良いわけです

## SSHが出来るようになるまで待つ
`action: setup`を実行するためにはSSHができるようになっていることを確認しないといけません
そのためには`wait_for`を使いましよう
http://docs.ansible.com/ansible/wait_for_module.html

```yaml
- hosts: all
  become: yes
  gather_facts: no
  pre_tasks:
    ~~~~ KVMゲスト作成 ~~~~~

    ### SSHが出来るようになるまで待つ
    - name: wait for vm to become available
      local_action: wait_for host={{ inventory_hostname }} port={{ ansible_ssh_port }} delay=10 state=started timeout=300

    ### setupを遅らせる
    - name: gather facts
      action: setup
  roles:
    - install_packages
```

`local_action`を指定することでansibleサーバからみてSSHが利用可能かを確認しています
`wait_for`ではhost、portを指定してSSHの状態が`started`になったかを確認しています
本設定では、`delay`を10、`timeout`を300に設定しています

## inventoryファイルに記載されていないホストに対してタスクを実行する
inventoryファイルにはKVMゲストの情報が記載されているため、playbookのタスク実行対象はKVMゲストとなってしまい、KVMホストに対してどうやってタスクを実行しようか悩みました

そんなときは`delegate_to`を使いましょう
http://docs.ansible.com/ansible/playbooks_delegation.html#delegation

`delegate_to`を利用するとタスク実行対象を別のホストにすることができます
先程使ったlocal_actionは`delegate_to: 127.0.0.1`と同様な条件となります
この機能を用いてplaybookを下記のように変更しました

```yaml
- hosts: all
  become: yes
  gather_facts: no
  pre_tasks:
    - name: KVMゲストの作成
      ~~~~~ KVMゲスト作成 ~~~~~
      delegate_to: "{{ host_ip }}"

    ### SSHが出来るようになるまで待つ
    - name: wait for vm to become available
      local_action: wait_for host={{ inventory_hostname }} port={{ ansible_ssh_port }} delay=10 state=started timeout=300

    ### setupを遅らせる
    - name: gather facts
      action: setup
  roles:
    - install_packages
```

`delegate_to: "{{ host_ip }}"`とすることで、`host_vars`に設定した`host_ip`を使ってKVMホストに対してタスクを実行します
これで、KVMホストに対してタスク実行可能な状態となりました

## KVMゲストの作成
さて、ここからは一気にKVMゲストの構築をしていきましょう

`shell`モジュールで`virt-install`コマンドを使ってしまうのが簡単でいいんですが、
せっかくなので`virt`モジュールを使ってlibvirtの操作をAnsibleで行います
http://docs.ansible.com/ansible/virt_module.html

最終的には以下のようになりました

```yaml
- hosts: all
  become: yes
  gather_facts: no
  pre_tasks:
    ### ここからKVMゲスト作成
    ### qemu-imgでKVMゲスト用のイメージを作成します
    - name: create qemu disk images
      shell: "qemu-img create -f raw /var/lib/libvirt/images/{{ inventory_hostname }}.img {{ disk }}G"
      delegate_to: "{{ host_ip }}"

    ### ブートに必要なものをWebサーバから取得
    - name: get vmlinuz
      get_url:
        url: "http://192.168.1.2/centos72/images/pxeboot/vmlinuz"
        dest: "/tmp/vmlinuz"
      delegate_to: "{{ host_ip }}"

    - name: get initrd.img
      get_url:
        url: "http://192.168.1.2/centos72/images/pxeboot/initrd.img"
        dest: "/tmp/initrd.img"
      delegate_to: "{{ host_ip }}"

    ### UUIDの所得
    - name: uuidgen
      shell: uuidgen
      register: result
      delegate_to: "{{ host_ip }}"

    - name: set uuid
      set_fact:
        uuid: "{{ result.stdout }}"

    ### templateで変数を埋めたxmlでVMをdefine(kickstart読み込み版)
    - name: define vm
      virt:
        name: "{{ inventory_hostname }}"
        command: define
        xml: "{{ lookup('template', 'vm_install.xml.j2') }}"
      delegate_to: "{{ host_ip }}"

    ### kickstartを読み込む状態でVMを起動
    - name: start vm
      virt:
        name: "{{ inventory_hostname }}"
        state: running
        uri: "qemu:///system"
      delegate_to: "{{ host_ip }}"

    ### kickstartでのVM作成が完了するのを待つ
    - name: wait vm create
      virt:
        name: "{{ inventory_hostname }}"
        command: status
      register: result
      until: result.status == 'shutdown'
      retries: 50
      delay: 60
      delegate_to: "{{ host_ip }}"

    ### 一度VMの定義を削除
    - name: undefine for change settings
      virt:
        name: "{{ inventory_hostname }}"
        command: undefine
      delegate_to: "{{ host_ip }}"

    ### templateで変数を埋めたxmlでVMをdefine(kickstartなし版)
    - name: define for change settings
      virt:
        name: "{{ inventory_hostname }}"
        command: define
        xml: "{{ lookup('template', 'vm.xml.j2') }}"
      delegate_to: "{{ host_ip }}"

    ### VMを起動
    - name: restart vm
      virt:
        name: "{{ inventory_hostname }}"
        state: running
        uri: "qemu:///system"
      delegate_to: "{{ host_ip }}"

    ### SSHが出来るようになるまで待つ
    - name: wait for vm to become available
      local_action: wait_for host={{ inventory_hostname }} port={{ ansible_ssh_port }} delay=10 state=started timeout=300

    ### setupを遅らせる
    - name: gather facts
      action: setup
  roles:
    - install_packages
```

ポイントはOSインストールのために、一度kickstartを読み込むパターンのxmlでdefineし、OSインストールが終わったら通常起動用のxmlを再度defineして起動するところです

libvirtで扱うxmlのテンプレート(kickstart利用時)は以下のようになりました

```xml:vm_install.xml.j2
<domain type='kvm'>
  <name>{{ inventory_hostname }}</name>
  <uuid>{{ uuid }}</uuid>
  <memory unit='KiB'>{{ (memory * 1024 * 1024) }}</memory>
  <currentMemory unit='KiB'>{{ (memory * 1024 * 1024) }}</currentMemory>
  <vcpu placement='auto'>{{ cpu }}</vcpu>
  <numatune>
    <memory mode='strict' placement='auto'/>
  </numatune>
  <os>
    <type arch='x86_64' machine='pc'>hvm</type>
    <kernel>/tmp/vmlinuz</kernel>
    <initrd>/tmp/initrd.img</initrd>
    <cmdline>method=http://192.168.1.2/centos72 ksdevice=eth0 ks=http://192.168.1.2/kickstart.ks ip={{ invenstory_hostname }} netmask=255.255.255.0 console=tty0 console=ttyS0,115200n8</cmdline>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <pae/>
  </features>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>destroy</on_reboot>
  <on_crash>destroy</on_crash>
  <devices>
    <emulator>/usr/libexec/qemu-kvm</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' cache='none'/>
      <source file='/var/lib/libvirt/images/{{ inventory_hostname }}.img'/>
      <target dev='vda' bus='virtio'/>
      <alias name='virtio-disk0'/>
    </disk>
    <controller type='usb' index='0'>
      <alias name='usb0'/>
    </controller>
    <interface type='bridge'>
      {% set octet = inventory_hostname.split('.') %}
      <mac address='00:00:0a:{{ '%02x'|format(octet[1]|int) }}:{{ '%02x'|format(octet[2]|int) }}:{{ '%02x'|format(octet[3]|int) }}'/>
      <source bridge='br0'/>
      <target dev='vnet0'/>
      <model type='virtio'/>
      <alias name='net0'/>
    </interface>
    <serial type='pty'>
      <source path='/dev/pts/1'/>
      <target port='0'/>
      <alias name='serial0'/>
    </serial>
    <console type='pty' tty='/dev/pts/1'>
      <source path='/dev/pts/1'/>
      <target type='serial' port='0'/>
      <alias name='serial0'/>
    </console>
    <input type='tablet' bus='usb'>
      <alias name='input0'/>
    </input>
    <watchdog model='i6300esb' action='reset'/>
    <memballoon model='virtio'>
      <alias name='balloon0'/>
    </memballoon>
  </devices>
  <seclabel type='none'/>
</domain>
```
libvirtで扱うxmlのテンプレート(通常起動時)は以下のようになりました

```xml:vm.xml.j2
<domain type='kvm'>
  <name>{{ inventory_hostname }}</name>
  <uuid>{{ uuid }}</uuid>
  <memory unit='KiB'>{{ (memory * 1024 * 1024) }}</memory>
  <currentMemory unit='KiB'>{{ (memory * 1024 * 1024) }}</currentMemory>
  <vcpu placement='auto'>{{ cpu }}</vcpu>
  <numatune>
    <memory mode='strict' placement='auto'/>
  </numatune>
  <os>
    <type arch='x86_64' machine='pc'>hvm</type>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <pae/>
  </features>
  <clock offset='utc'/>
  <on_poweroff>restart</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>restart</on_crash>
  <devices>
    <emulator>/usr/libexec/qemu-kvm</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' cache='none'/>
      <source file='/var/lib/libvirt/images/{{ inventory_hostname }}.img'/>
      <target dev='vda' bus='virtio'/>
      <alias name='virtio-disk0'/>
    </disk>
    <controller type='usb' index='0'>
      <alias name='usb0'/>
    </controller>
    <interface type='bridge'>
      {% set octet = inventory_hostname.split('.') %}
      <mac address='00:00:0a:{{ '%02x'|format(octet[1]|int) }}:{{ '%02x'|format(octet[2]|int) }}:{{ '%02x'|format(octet[3]|int) }}'/>
      <source bridge='br0'/>
      <target dev='vnet0'/>
      <model type='virtio'/>
      <alias name='net0'/>
    </interface>
    <serial type='pty'>
      <source path='/dev/pts/1'/>
      <target port='0'/>
      <alias name='serial0'/>
    </serial>
    <console type='pty' tty='/dev/pts/1'>
      <source path='/dev/pts/1'/>
      <target type='serial' port='0'/>
      <alias name='serial0'/>
    </console>
    <input type='tablet' bus='usb'>
      <alias name='input0'/>
    </input>
    <watchdog model='i6300esb' action='reset'/>
    <memballoon model='virtio'>
      <alias name='balloon0'/>
    </memballoon>
  </devices>
  <seclabel type='none'/>
</domain>

```

今回扱ったkickstartファイルは以下の様なものです

```text:kickstart.ks
auth  --useshadow  --enablemd5
authconfig --enableshadow --enablemd5

zerombr

bootloader --location=mbr

clearpart --all --initlabel
part /boot --fstype=xfs --size=200 --asprimary
part swap --size=1024
part pv.2 --size=1 --grow
volgroup VolGroup00 --pesize=32768 pv.2
logvol / --fstype xfs --name=lv_root --vgname=VolGroup00 --size=1024 --grow --fsoptions=defaults

text

firewall --enabled --ssh  --http
firstboot --disable
keyboard --vckeymap=us --xlayouts='us'
lang en_US.UTF-8
logging --level=debug

url --url="http://192.168.1.2/centos72/"
repo --name="CentOS" --baseurl="http://192.168.1.2/centos72"

reboot
rootpw --iscrypted [パスワード]
selinux --disabled
skipx
timezone Asia/Tokyo --isUtc
install

%packages
@core
%end
```

これでKVMゲストの作成、OSのインストール、構築までが一つのplaybookで完結するようになりました

# おわりに
AnsibleでKVMも構成管理いかがだったでしょうか？
今回の構成ではやや黒魔術的にがんばる結果になってしまいましたが、Ansibleは様々なモジュールがあるのでもっといろんなことが出来ます
いくつも手順、ステップがある作業はAnsibleに落とし込むと管理がしやすく、テンプレートの機能もあるため変数が扱えるというのがすごく良いと思います

1コマンドに落とし込めるようになったので、将来的にはチャットでボットに対してVMのスペックを送ったらVM構築が実行されるみたいなことが出来たら良いです(というか出来ます)