この記事は 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利用時)は以下のようになりました
<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のテンプレート(通常起動時)は以下のようになりました
<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ファイルは以下の様なものです
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構築が実行されるみたいなことが出来たら良いです(というか出来ます)