Terraform + libvirt + Open vSwitch

# はじめに

ネットワークのテストを実環境でやるのは大変なので、仮想環境で基本の確認をやるという方はそれなりにいらっしゃると思います。

クラウドの世界では割と有名と思われるIaC(Infrastrucure as Code)を提供するTerraformをローカル環境に適用してテスト環境を構築していたのですが、いろいろあちこち罠に引っかかったので備忘録を兼ねて解説したいと思います。

# Terraform + libvirt

Terraformは作成したいリソースを`resource`として定義しておき、`terraform apply`で記述したとおりにリソースを作成あるいは変更してくれます。通常はAWSやAzure, GCPといったクラウドのリソースを指定しますが、libvirtを操作することでVMを作ったり消したりもできます。

libvirtを操作するlibvirt-provider-libvirtですが、現在はTerraform registryに登録されてるので、それほどハマることなく導入できます。ディスクやネットワークなど指定なしなのでOSをブートできませんが、めっちゃシンプルな記述例は下記です。

```hcl:example.tf
resource "libvirt_domain" "example" {
  name = "example0"
  memory = 2048
  vcpu = 2
}
```
# 複数のVMをつなげる

`libvirt_network`でネットワークリソース(ブリッジ)を定義して、`libvirt_domain`の`network_interface`に指定することで、そのドメイン(VM)がネットワークに参加します。複数のドメインが同じネットワークに参加すれば、VM同士での通信が可能になります。下記は、やっぱりOS起動などできませんが、`VM1`と`VM2`のあいだを`test_bridge`でつなぐ記述です。

```hcl:test.tf
resource "libvirt_network" "test_bridge" {
  mode = "none"
  dns { local_only = true }
}

resource "libvirt_domain" "VM1" {
  name = "VM1"
  memory = 2048
  vcpu = 2
  network_interface {
    network_name = "test_bridge"
  }
}

resource "libvirt_domain" "VM2" {
  name = "VM2"
  memory = 2048
  vcpu = 2
  network_interface {
    network_name = "test_bridge"
  }
}
```

だいたいはこれでうまくいきます。

# うまく通信できない?

仮想環境あるあるの話です。

昔(といっていいでしょう)流行っていたOpenFlowのようなものでは、どんなMACアドレスのパケットも処理できて、自由にMACアドレスを変更してパケット送信できます。

しかし、仮想環境における通常の仮想ネットワークは外部からMACアドレスの情報などわかることから、無駄な通信をさせないために、想定外のMACアドレスのパケットは全部はじくような動作をすることが多いのです。そのためOpenFlowのテストを仮想環境で実施してみると、うまくいかないことがありました。

実はSONiCのVM版 (SONiC-VS)が、この問題に引っかかってうまく他のホストとBGPセッションを張ることができなくて、しばらく原因がわからず頭を抱えていました。GNS3で組んだらあっさりセッションが張れて、もしかして……と。

# 通信の問題の解決方法

うまく通信させるにはどうすればいいか。

VMとVMをつなぐものがパケットを素通しにするブリッジ、物理機器で言えば昔あったバカHUB相当の動作をしてくれればいいのですが、このような動作をさせるのによく使われるのがOpen vSwitchです。2つのVMをつなぐとケーブルのように動作します。

# libvirt + Open vSwitch

Open vSwitchの公式ドキュメントがあります。
https://docs.openvswitch.org/en/latest/howto/libvirt/

サマリーだけ書くと

- `ovs-vsctl`でOVS bridgeを作成する
- libvirt domain作成用のXML内の`<interface>`の中に`<virtualport type="openvswitch"/>`を入れる

となります。ん? libvirtでXML書いてOVS bridge作ったりできないの?

# Terraform + libvirt + open vSwitch

libvirtからOVS bridgeを作成することは、できません。あらかじめOVS bridgeを作成しておいて、それをlibvirtで指定することになります。

## `libvirt_network` でなんとかならないか?

結論から言えば、失敗。

OVS公式で書かれていたように手動で`ovs-vsctl`でブリッジを作っておいて、`mode = "bridge"`で指定する方法を試してみましたが、うまく動きませんでした。

## `provisioner` だッ!

`libvirt_network`を使うのはあきらめます。

`terraform apply`と`terraform destroy`でリソース操作する際に、任意のコマンドを実行させることができます。provisionerです。

`provisioner "local-exec"`で、ローカル環境でコマンドを実行できます。`command=`で実行するコマンドラインを指定します。他に何も指定しなければ、リソース作成時に実行されます。`when = destroy`を書いておくと、その中のコマンドラインはリソース削除時に実行されます。

通常、provisionerはなんらかのリソース(たとえば`libvirt_network`)の作成・削除にともなって実行されますが、とくに関連リソースがない場合は `resource "null_resource"`を指定する方法が公式から案内されています。

```hcl:net.tf
resource "null_resource" "net" {
  provisioner "local-exec" {
    command = "sudo ovs-vsctl add-br br0"
  }
  provisioner "local-exec" {
    when = destroy
    command = "sudo ovs-vsctl del-br br0"
  }
}
```

## XSLT!

実はふつうに記述するだけでは`<virtualport type="openswitch"/>`を指定することができません。ではどうやって指定するのか? XMLを加工することができるプログラミング言語XSLT (XSL Transform) を使います。

```xml:ovs-port.xsl
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="/domain/devices/interface[@type='bridge']">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
      <virtualport type="openvswitch"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>
```

ややこしいので解説しませんが、ざっくりいうと、`<domain><devices><interface type="bridge">'`の内側、最後に`<virtualport type="openvswitch"/>`を追加する処理になります。

処理を実行するのに `xsltproc` というプログラムが必要です。`apt-get install`しておきましょう。

## `libvirt_domain`での指定方法

XSLTで書かれた処理を、`libvirt_domain`の中で指定します。

```hcl:main.tf
resource "libvirt_domain" "VM1" {
  name = "VM1"
  network_interface {
    bridge = "br0"
  }
  xml {
    xslt = file("ovs-port.xsl")
  }
}

resource "libvirt_domain" "VM2" {
  name = "VM2"
  network_interface {
    bridge = "br0"
  }
  xml {
    xslt = file("ovs-port.xsl")
  }
}
```

上記の`main.tf`と、`ovs-port.xsl`、`net.tf`を用意して`terraform apply`することで、`VM1`と`VM2`の間をOVS bridge `br0`でつなぐことができます。(ネットワーク以外の設定はいろいろ足してください)

# おわりに

いろいろ調べてみたのですが、「OVS bridge作れるようにしてほしい。例えば`libvirt_network`で`mode="ovs"`」「XSLTで`<virtualport type="openvswitch"/>`指定できるようになったからこのissueはclose」と微妙に噛み合わない上に具体例がなくて、しばらく頭を抱えていました。

けっきょくどこにも答えがなくて自力でゴリゴリやったので、もしかするともっとスマートな解決方法があるかも知れません。