33
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AnsibleでKVMも構成管理

Last updated at Posted at 2016-12-11

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

33
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?