terraform-provider-libvirt を使ってみる

先日 vagrant-libvirt を使ってみました が、libvirt も terraform-provider-libvirt を使えば Terraform で管理できるので試してみました。

インストール

Terraform オフィシャルのプロバイダであれば terraform init で自動的にバイナリがダウンロードされますが、サードパーティのプロバイダは ~/.terraform.d/plugins にあらかじめバイナリをインストールしておく必要があります。

一部のディストリビューションではパッケージが用意されています。

ローカルが Fedora 31 だったので Fedora のパッケージをインストールしようとしたところ、依存関係で Terraform も必要になりました。Terraform は Linuxbrew で入れていたので依存関係が解決できずインストールでコケます。

sudo dnf install terraform-provider-libvirt
# Error:
#  Problem: conflicting requests
#   - nothing provides terraform >= 0.12.0 needed by terraform-provider-libvirt-0.6.2+git.1590253051.d1cf93cd-1.1.x86_64
#   - nothing provides mkisofs needed by terraform-provider-libvirt-0.6.2+git.1590253051.d1cf93cd-1.1.x86_64

また、Fedora 31 に mkisofs というパッケージは無く、代わりに mkisofs コマンドは genisoimage とか xorriso とかのパッケージからインストールされるため、mkisofs への依存も解決出来ていません。

rpm ファイルをダウンロードして rpm コマンドの --nodeps オプションを使えば依存関係を無視して無理やりインストールすることも可能です。

sudo dnf download terraform-provider-libvirt
sudo rpm -ivh --nodeps terraform-provider-libvirt-0.6.2+git.1590253051.d1cf93cd-1.1.x86_64.rpm

インストールすると /usr/bin/terraform-provider-libvirt にプラグインのバイナリがインストールされますが、ここにあるだけでは使えません。ユーザーの ~/.terraform.d/plugins/ にバイナリをコピーなりリンクなりする必要があります。

mkdir -p ~/.terraform.d/plugins/
ln -sfn /usr/bin/terraform-provider-libvirt ~/.terraform.d/plugins/

あるいは、GitHub Releases にバイナリがあるのでそれをダウンロードしてきても OK です。Fedora/openSUSE/SUSE Linux Enterprise Server/Ubuntu で別のバイナリが用意されています。Go 製ですが ldd で見た感じ 大量の so とリンクされていたので、ディストリビューションによって異なるバイナリが必要なようです。

wget https://github.com/dmacvicar/terraform-provider-libvirt/releases/download/v0.6.2/terraform-provider-libvirt-0.6.2+git.1585292411.8cbe9ad0.Fedora_28.x86_64.tar.gz
tar xvf terraform-provider-libvirt-0.6.2+git.1585292411.8cbe9ad0.Fedora_28.x86_64.tar.gz
mkdir -p ~/.terraform.d/plugins/
mv terraform-provider-libvirt ~/.terraform.d/plugins/

もしくは、Linuxbrew にもあったので Linuxbrew でも入れられそうです。

brew install terraform-provider-libvirt

ただ Linux 用の bottle が無いためビルドが実行されます。さらに formula に libvirt への依存もあるため libvirt を dnf でインストールしていたとしても Linuxbrew からもインストールされてしまいます。

バイナリいっこで済むほうが手っ取り早いので、GitHub Releases から落としてきてインストールしました。dnf や Linuxbrew と比べるとバージョンアップしにくい問題はありますけど。

必要最小限の構成

次のようなテンプレートでゲストが立ち上げられます。

provider libvirt {
  uri = "qemu+ssh://root@192.0.2.123/system"
}

resource libvirt_cloudinit_disk cloudinit {
  name = "cloudinit.iso"
  pool = "default"

  user_data = <<-EOS
    #cloud-config
    ssh_pwauth: true
    chpasswd:
      list: root:password
      expire: false
  EOS
}

resource libvirt_volume sv01 {
  name   = "sv01.qcow2"
  pool   = "default"
  source = "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2003.qcow2"
}

resource libvirt_domain sv01 {
  name = "sv01"

  network_interface {
    network_name = "default"
  }

  disk {
    volume_id = libvirt_volume.sv01.id
  }

  cloudinit = libvirt_cloudinit_disk.cloudinit.id

  console {
    type        = "pty"
    target_type = "serial"
    target_port = "0"
  }
}

コンソールや ssh で root:password でログインできます。

以降は補足的なメモです。

ディスクイメージ

libvirt_volumesource でイメージの URL を指定すれば terraform apply で自動的にダウンロードされます。

resource libvirt_volume sv01 {
  name   = "sv01.qcow2"
  pool   = "default"
  source = "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2003.qcow2"
}

この例で指定しているイメージは CentOS がオフィシャルで提供しているイメージです。このイメージの構築に使用された kickstart ファイルは https://git.centos.org/centos/kickstarts/blob/master/f/CentOS-7-GenericCloud.ks で公開されています。

これは新しくボリュームを作るたびにダウンロードされるため、サイズがでかいと辛いです。あらかじめローカルにダウンロードしておいて次のようにローカル(Terraform を実行するホスト)のファイルを指定しても良いかもしれません。

# あらかじめローカルにダウンロードしておく
wget https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2003.qcow2
// ローカルのファイル名を指定する
resource libvirt_volume sv01 {
  name   = "sv01.qcow2"
  pool   = "default"
  source = "./CentOS-7-x86_64-GenericCloud-2003.qcow2"
}

ただ、この場合も「ローカル → KVMホスト」のイメージの転送はボリュームを新しく作るたびに必要になります。

KVM ホストのストレージプールにあらかじめイメージを保存しておいて、そのイメージをベースとした差分のボリュームを作成するのが良いかもしれません。

# ベースイメージのボリュームを作成
virsh -c qemu+ssh://root@192.0.2.123/system vol-create-as \
  --pool default \
  --name CentOS-7-x86_64-GenericCloud-2003.qcow2 \
  --capacity 0 \
  --format qcow2

# ベースイメージをローカルから転送
virsh -c qemu+ssh://root@192.0.2.123/system vol-upload \
  --pool default \
  --vol CentOS-7-x86_64-GenericCloud-2003.qcow2 \
  --file ./CentOS-7-x86_64-GenericCloud-2003.qcow2
// あらかじめ作成しておいたイメージをベースとしてボリュームを作る
resource libvirt_volume sv01 {
  name = "sv01.qcow2"
  pool = "default"

  base_volume_name = "CentOS-7-x86_64-GenericCloud-2003.qcow2"
  base_volume_pool = "default"
}

cloud-init

CentOS-7-x86_64-GenericCloud-2003.qcow2 のイメージは cloud-init が利用可能です。

terraform-provider-libvirt は cloud-init の NoCloud データソース 用の ISO ファイルをストレージプールに作成できるので、cloud-init のユーザーデータも Terraform で管理できます。

resource libvirt_cloudinit_disk cloudinit {
  name = "cloudinit.iso"
  pool = "default"

  user_data = <<-EOS
    #cloud-config
    timezone: Asia/Tokyo
    ssh_pwauth: true
    chpasswd:
      list: root:password
      expire: false
    users:
      - name: ore
        groups: wheel
        sudo: ALL=(ALL) NOPASSWD:ALL
        ssh_authorized_keys:
          - ${file("${path.module}/ssh_authorized_keys")}
  EOS
}


resource libvirt_domain sv01 {
  ...snip...

  cloudinit = libvirt_cloudinit_disk.cloudinit.id
}

複数のネットワーク

CentOS-7-x86_64-GenericCloud-2003.qcow2 のイメージの素のままだと、ゲストにネットワークを複数アタッチしてもゲストの開始時には最初のインタフェースしか有効になりません。

  // このインタフェースしか有効にならない
  network_interface {
    macvtap = "eth0"
  }

  // このインタフェースは開始時は無効
  network_interface {
    network_name = "default"
  }

/etc/sysconfig/network-scripts/ifcfg-eth1 が無いためです。

cloud-init で次のようにネットワーク構成を指定すれば cloud-init によってインタフェースが初期化されるので大丈夫です。

resource libvirt_cloudinit_disk cloudinit {
  name = "cloudinit.iso"
  pool = "default"

  network_config = <<-EOS
    version: 2
    ethernets:
      eth0:
        match:
          name: eth0
        dhcp4: true
      eth1:
        match:
          name: eth1
        dhcp4: true
  EOS
}

もしくは、cloud-init のユーザーデータの write_files で /etc/sysconfig/network-scripts/ifcfg-eth1 を作成して runcmd で systemctl restart network とか、あるいは runcmd でNetworkManager をインストール&開始する、などの方法も考えられます。

インタフェースにアドレスが付与されるのを待つ

network_interfacewait_for_lease = true を指定すると、ゲストの作成時にそのインタフェースにアドレスが付与されるのを待つようになります。

  network_interface {
    network_name   = "default"
    wait_for_lease = true
  }

未指定や false だとインタフェースにアドレスが付与されるのを待たずにゲストの作成が完了します。そのため、同じテンプレートでゲストのアドレスを別のリソースから参照しても空になってしまいます。wait_for_lease = true でアドレスの付与を待てば有効なアドレスが入った状態で参照できます。

なお、何らかの原因で wait_for_lease = true を指定したインタフェースにアドレスがいつまでも付与されない場合(前述の複数のネットワークにアタッチした場合とか)、ゲストの作成がいつまでも終わらずにタイムアウトで中断されてしまうので注意が必要です。

ネットワークのアドレス指定

ゲストのインタフェースに libvirt 管理下のネットワークを指定すると addresses でゲストに付与するアドレスも一緒に指定できます。

  network_interface {
    network_name   = "default"
    addresses      = ["192.168.122.101"]
  }

これはネットワークの定義に次のようにホストの MAC アドレスと IP アドレスの対応を追加することで実現されています。

<ip address='192.168.122.1' netmask='255.255.255.0'>
  <dhcp>
    <range start='192.168.122.2' end='192.168.122.254'/>
    <host mac='52:54:00:1A:5E:D9' name='sv01' ip='192.168.99.101'/>
    <host mac='52:54:00:be:23:da' name='sv02' ip='192.168.99.102'/>
  </dhcp>
</ip>

なので指定した libvirt 管理下のネットワークで DHCP が有効になっている必要があります。ネットワークの forward mode が bridge だと DHCP が libvirt の管理下に無いので(外のネットワークに DHCP サーバがあるので)、この方法でアドレスを指定することは出来ません。

なお、指定するネットワークが isolated network だと駄目なようです。次のように libvirt_network で mode を none にすると isolated network になります。

resource libvirt_network isolated {
  name      = "isolated"
  mode      = "none"
  addresses = ["192.168.99.0/24"]
  dhcp { enabled = true }
}

isolated network でも KVM ホスト~ゲスト間の通信は可能なので、同じ仕組みで IP アドレスを指定できるはずですが・・issue も登録されていました。たぶんバグっぽいので PR 出しておきました。そのうち修正されるかもしれません。

ちなみに、cloud-init でも次のように固定アドレスは付与できます。

resource libvirt_cloudinit_disk cloudinit {
  name = "cloudinit.iso"
  pool = "default"

  network_config = <<-EOS
    version: 2
    ethernets:
      eth0:
        match:
          name: eth0
        addresses:
          - 192.168.99.109/24
        gateway4: 192.168.99.1
        nameservers:
          addresses:
            - 192.168.99.1
  EOS
}

この方法はネットワークの forward mode が bridge で DHCP が libvirt の管理下にない場合でも使えます。

ただ、複数のゲストを作るときにこの方法で固定アドレスを付与しようとすると cloud-init の ISO を分ける必要があります。ゲストの network_interface に指定する方法なら cloud-init の ISO は共有できるため、無駄がありません。

CentOS-7-x86_64-GenericCloud-2003.qcow2c

CentOS のオフィシャルで提供されているイメージ、拡張子が qcow2c となってる少しサイズが小さいイメージもあります。

https://cloud.centos.org/centos/7/images/

これは拡張子 qcow2 のイメージと内容はまったく同じものですが圧縮されているためサイズが小さくなっています。この圧縮は qcow2 の内部のものなので直接イメージとして使えます。

resource libvirt_volume sv01 {
  name = "sv01.qcow2"
  pool = "default"

  base_volume_name = "CentOS-7-x86_64-GenericCloud-2003.qcow2c"
  base_volume_pool = "default"
}

パフォーマンスをあまり気にしない検証用途なら使えると思います。

なお、CentOS 8 用には qcow2c の形式のイメージは提供されていないようです。

https://cloud.centos.org/centos/8/x86_64/images/

さいごに

前回試してみた vagrant-libvirt と 比較すると terraform-provider-libvirt がやること自体は必要最低限で基本的には cloud-init で環境を整える必要があります。

vagrant-libvirt はプロビジョニングや Forwarded Port などでかなり独特なことをやっています。どっちもどっちだと思いますが、vagrant-libvirt 特有の問題に悩まされることがない分 terraform-provider-libvirt のほうが扱いやすいかもしれません。