Terraform Libvirt Provider で Q35 タイプの KVM を作成する

### 前置き
Terraform libvirt provider で Q35 type の KVM を作ろうとすると、エラーになる場合がある。同 provider は、[Q35 machine type の指定そのものはサポートしている](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/domain#machine-1) が、CD-ROM をマウントするようなコードが共存する場合、失敗する。原因は、同 provider が CD-ROM に自動的に IDE bus を割り当てるためである。この形式はレガシーであり、Q35 machine にマウントできない。したがって、次のような典型的なユースケースにおいて、VM 作成時にエラーが発生してしまう：
- インストール用の Bootable デバイスを 仮想 CD-ROM としてマウントするケース
- cloudinit でインストールするケース (ISO イメージのマウントが発生)

この問題については [Issue #1018](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1018
) が報告されているが、2025年1月現在まで修正されておらず、現状は [XSLT パッチ](https://github.com/kubealex/terraform-libvirt-libvirt-resources/blob/main/modules/terraform-libvirt-instance/linux-patch.xsl
) を用いて IDE bus を SATA bus に編集する対処が勧められている。

本記事では、Terraform Libvirt Provider で Q35 マシンを作成するための XSLT パッチの当て方を紹介する。

## 準備
Terraform 実行ホストに次のソフトウェアをインストールする（Terraform Libvirt Provider が XSLT を解釈する際に必要となる）。

Ubuntu 系の場合：
```
$ sudo apt install xsltproc
```

RHEL 系の場合：
```
$ sudo dnf install libxslt
```

テスト用 OS image として、Rocky Linux の `qcow2` image をダウンロードしておく：
```
$ sudo curl -o /var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 -LO https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
```

## プロビジョニングファイルの作成

### XSLT パッチの用意

次の `pathc.xsl` を用意する：
```xml:patch.xsl
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <!-- Identity transform to copy everything as is -->
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <!-- Use SATA CD-ROM for Linux setup -->
  <xsl:template match="target[@bus='ide']">
    <xsl:copy>
      <xsl:apply-templates select="@*[name()!='bus']"/>
      <xsl:attribute name="bus">sata</xsl:attribute>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
```

`<!-- Use SATA CD-ROM for Linux setup -->` のブロック内で、IDE bus の指定を SATA bus に修正する処理を行っている：
1. `<target>` 要素にマッチする要素を探す
2. 属性名が `bus` 以外の場合、元の属性値をそのままコピーする
3. 属性名が `bus` の場合、属性値を `sata` に変更する。

それ以外の部分は、基本的な宣言や、もとの domain xml の内容をそのままコピーする処理が書かれている、いわゆる「おまじない部分」である。

## Terraform file の作成
VM 構築テスト用に、次の `main.tf` を作成する。

```main.tf
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.8.1"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_cloudinit_disk" "commoninit" {
  name      = "commoninit_q35-test.iso"
  user_data = templatefile("cloud_init.cfg", {})
  pool      = "default"
}

resource "libvirt_domain" "vm" {
  name    = "q35-test"
  vcpu    = 2
  memory  = 8000
  machine = "q35"

  disk {
    volume_id = libvirt_volume.volume.id
    scsi      = true
  }

  cloudinit = libvirt_cloudinit_disk.commoninit.id
  autostart = true

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

  cpu {
    mode = "host-passthrough"
  }

  graphics {
    type        = "vnc"
    listen_type = "address"
  }

  # Makes the tty0 available via `virsh console`
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  xml {
    xslt = file("${path.module}/patch.xsl")
  }
}

# Download from https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
resource "libvirt_volume" "volume" {
  name           = "q35-test.qcow2"
  pool           = "default"
  format         = "qcow2"
  base_volume_id = "/var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2"
  size           = 100 * 1024 * 1024 * 1024
}
```

ポイントは libvirt_domain リソース内の次の2箇所：
- `machine` に `q35` を指定する
- `xml` ブロックで xslt パッチを渡す

:::note
TIPS: 利用可能な qemu machine type の一覧は `/usr/libexec/qemu-kvm -machine help` コマンド (RHEL 系以外の OS では `/usr/bin/qemu-system-x86_64 -machine help` コマンド) で確認することができる：
```
# /usr/libexec/qemu-kvm -machine help
Supported machines are:
pc                   RHEL 7.6.0 PC (i440FX + PIIX, 1996) (alias of pc-i440fx-rhel7.6.0)
pc-i440fx-rhel7.6.0  RHEL 7.6.0 PC (i440FX + PIIX, 1996) (default) (deprecated)
q35                  RHEL-9.4.0 PC (Q35 + ICH9, 2009) (alias of pc-q35-rhel9.4.0)
pc-q35-rhel9.4.0     RHEL-9.4.0 PC (Q35 + ICH9, 2009)
pc-q35-rhel9.2.0     RHEL-9.2.0 PC (Q35 + ICH9, 2009)
pc-q35-rhel9.0.0     RHEL-9.0.0 PC (Q35 + ICH9, 2009)
pc-q35-rhel8.6.0     RHEL-8.6.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.5.0     RHEL-8.5.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.4.0     RHEL-8.4.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.3.0     RHEL-8.3.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.2.0     RHEL-8.2.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.1.0     RHEL-8.1.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel8.0.0     RHEL-8.0.0 PC (Q35 + ICH9, 2009) (deprecated)
pc-q35-rhel7.6.0     RHEL-7.6.0 PC (Q35 + ICH9, 2009) (deprecated)
```

なお、__Terraform libvirt provider は、デフォルトで `pc-i440fx` machine type の VM を作成する__。このタイプはレガシーであり、例えば PCIe device の passthrough といったモダンな機能をサポートしていない。
:::


cloudinit 設定ファイル `cloud_init.cfg` を作成する：

```cloud_init.cfg
#cloud-config
users:
  - name: root
    ssh-authorized-keys:
      - "<SSH_KEY>"
```


## VM の作成
次のコマンドを実行し、VM を作成する。
```
$ terraform init
$ terraform apply
...
libvirt_cloudinit_disk.commoninit: Creating...
libvirt_volume.volume: Creating...
libvirt_volume.volume: Creation complete after 0s [id=/var/lib/libvirt/images/q35-test.qcow2]
libvirt_cloudinit_disk.commoninit: Creation complete after 0s [id=/var/lib/libvirt/images/commoninit_q35-test.iso;0a9b23c1-c711-469f-968b-e8adc566ea79]
libvirt_domain.vm: Creating...
libvirt_domain.vm: Creation complete after 1s [id=81cf19ea-54cb-4544-aaae-fbc8222fde98]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
```

cloudinit を用いながらも、`q35` type によるマシン作成が成功した。

## VM の作成 (失敗例)

成功例だけ見ても、ああそう、という感じなので、前置きで述べた失敗例も示す。`main.tf` XSLT patch の部分だけコメントアウトしてみる：
```text:main.tf
...
  #xml {
  #  xslt = file("${path.module}/patch.xsl")
  #}
...
```

再度 VM の作成を試みると、今度はエラーになる：
```
$ terraform destroy   # 先程の VM を削除
$ terraform apply
...
libvirt_cloudinit_disk.commoninit: Creating...
libvirt_volume.volume: Creating...
libvirt_volume.volume: Creation complete after 0s [id=/var/lib/libvirt/images/q35-test.qcow2]
libvirt_cloudinit_disk.commoninit: Creation complete after 0s [id=/var/lib/libvirt/images/commoninit_q35-test.iso;548e73d8-1fb5-4141-9a0e-efd49d8e078f]
libvirt_domain.vm: Creating...
╷
│ Error: error defining libvirt domain: unsupported configuration: IDE controllers are unsupported for this QEMU binary or machine type
│ 
│   with libvirt_domain.vm,
│   on main.tf line 20, in resource "libvirt_domain" "vm":
│   20: resource "libvirt_domain" "vm" {
│ 
╵
```

Q35 type がサポートしない IDE bus が使われたことでエラーになっている。