Edited at
LivesenseDay 12

AnsibleでKVMも構成管理

More than 1 year has passed since last update.

この記事は 2016年 Livesense Advent Calendar 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がある前提で進めていきます

- 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を修正しまいた

- 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

- 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を下記のように変更しました

- 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

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

- 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利用時)は以下のようになりました


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のテンプレート(通常起動時)は以下のようになりました


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ファイルは以下の様なものです


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構築が実行されるみたいなことが出来たら良いです(というか出来ます)