はじめに
terraform-provider-libvirtを使ってみたの続きです。(やっぱりSONiCかよ! という感想をお持ちの方がいらっしゃいましたら、そのとおりでした! と開き直りますw)
module
の話とlibvirt_network
の話も交えて、解説します。
SONiC-VS
SONiCはホワイトボックススイッチ用のNOSであり、SONiC-VSはVirtual Switchの名の通り仮想マシンで動作するスイッチです。SONiC-VSの起動イメージであるsonic-vs.img
はqcow2フォーマットです。
$ file sonic-vs.img
sonic-vs.img: QEMU QCOW2 Image (v3), 17179869184 bytes
ですので、Terraformで扱う際もイメージファイルを置き換えるだけで、Debianのときと同様にSONiC-VS単体を起動させること自体は簡単です。vcpu = 1
、memory = 2048
で動きます。
ただし、SONiC-VSをそれ単体で動かすことにあまり意味はありません。スイッチなので複数のネットワークインタフェースを持っていて、他のルータ・スイッチ・VMと接続し、パケットを中継するのが役割となります。つまり、Terraformで複数VMを定義し、またVM間のネットワーク接続を定義することではじめてSONiC-VSをTerraformでデプロイする意味が出てきます。
複数のネットワークインタフェース
前回の記事でのDebian VMはネットワークインタフェースをひとつだけ持っていました。libvirt_domain
リソースの定義の中に下記のような記述がありました。
network_interface {
network_name = "default"
}
複数のネットワークインタフェースを定義するには、このnetwork_interface
ブロックを複数並べます。SONiC-VSでは一番上のnetwork_interface
がeth0
(マネジメントポート)となり、2番目以降がEthernet0
, Ethernet4
, という並びになります。
network_name
については後述します。
Terraform module
記事「terraform-provider-libvirtを使ってみた」ではDebianのVMをデプロイする例を紹介しました。この例では、デプロイするVMはひとつでした。では、複数のVMを起動するにはどうすればいいでしょうか。
同じ記述をコピペして名前を変えていけば、たしかに複数のVMをデプロイすることができます。しかしそれは非効率ですし変更が生じたときに漏れが発生しがちだったりします。ここで出てくるのがmodule
という概念です。
moduleとは
moduleは繰り返し使われるリソース定義の共通部分を切り出し、別のディレクトリ(別のtfファイル)て提供することで再利用するというものです。名前やIPアドレスなど個別に違う値を必要とする部分については変数を定義し、呼び出し元で指定することができます。
例
簡単な例です。ファイルを2つ用意します。呼び出し元とmoduleです。
test/main.tf
test/modules/sonic/sonic.tf
呼び出し元です。
source =
で利用するmoduleを指定します。hostname =
でmodule側で参照される変数の値を定義しています。ふつうのプログラミング言語の関数呼び出しのイメージです。
module "sonic-1" {
source = "modules/sonic"
hostname = "s1"
}
module "sonic-2" {
source = "modules/sonic"
hostname = "s2"
}
moduleの定義です。
変数を宣言して、文字列中であれば ${var.hostname}
で参照しています。VMごとにディスクイメージのファイル名を変える必要があるため、このようにします。(全部書くと長くなるので、前後は省いていますのでご注意ください)
${path.module}
は、このディレクトリ(modules/sonic
)を示します。
variable hostname { default = "sonic" }
provider "libvirt" {
uri = "qemu:///system"
}
resource "libvirt_volume" "os-image" {
name = "${var.hostname}-disk.qcow2"
source = "${path.module}/sonic-vs.img"
format = "qcow2"
}
module
の中で生成された値を呼び出し元に返すこともできますが、今回は利用していません。
上記例で、2つのSONiC-VSを用意することができます。
ハマりどころとしては、Terraform registryにないproviderを引き込むためのterraform
ブロックはmodule
側に書く必要があるというところです。正確には、libvirt_XXXXリソースを記述するファイルの先頭に書いておく必要があります。
ネットワークを定義する
L2あるいはL3のネットワークを定義して、network_interface
で指定することで、複数のネットワークインタフェースをそれぞれ別々のネットワークに接続することができます。ネットワークの定義はlibvirt_networkリソースで行います。公式の解説はこちら。
VM間をつなぐためにネットワークを定義するので、libvirt_networkリソースの定義はmodule側ではなく呼び出し側で定義することになります。つまりterraform
ブロックが呼び出し側のmain.tf
にも必要になりますので追記をお忘れなく。
libvirt_networkリソースですが、名前の他にmode
を指定する必要があります。
none
-
nat
(default) route
bridge
none
以外はホストネットワークと接続するので、仮想マシン間の接続だけに留める場合modeにnone
を指定したlibvirt_networkリソースを定義することになります。また、nat
ではホストから仮想マシンにアクセスできませんので、ホストからアクセスしたい場合はbridge
あるいはroute
を指定したlibvirt_networkリソースを定義します。
定義したlibvirt_networkリソースは、仮想マシンのnetwork_interface
で名前を指定することで接続します。
resource "libvirt_network" "s1-s2" {
name = "c1"
mode = "none"
dns { local_only = true }
}
ネットワークインタフェースをネットワークに接続する
こんなふうにつないでみます。
(default) (default)
eth0| |eth0
---+--- ---+---
| s1 | | s2 |
---+--- ---+---
|Ethernet0 Ethernet0|
---------------c1---------------
module側では2つ目のnetwork_interface
でネットワーク名を変数で指定できるようにします。今回はEthernet0
のみ利用しますが、今後ルーティングすることを想定してEthernet4
もコードを用意しておきました。関係ある部分を一部抜粋します。
variable eth0 { default = "default" }
variable Ethernet0 { default = "default" }
variable Ethernet4 { default = "default" }
resource "libvirt_domain" "sonic-domain" {
network_interface {
network_name = var.eth0
}
network_interface {
network_name = var.Ethernet0
}
network_interface {
network_name = var.Ethernet4
}
呼び出し側ではlibvirt_networkリソースを定義して、指定します。
resource "libvirt_network" "s1-s2" {
name = "c1"
mode = "none"
dns { local_only = true }
}
module "sonic-1" {
source = "./modules/sonic"
hostname = "s1"
Ethernet0 = "c1"
}
module "sonic-2" {
source = "./modules/sonic"
hostname = "s2"
Ethernet0 = "c1"
}
デプロイ
terraform init
terraform apply
できました。terraform state list
でリソースを確認できます。
$ terraform state list
libvirt_network.s1-s2
module.sonic-1.libvirt_domain.domain-sonic
module.sonic-1.libvirt_volume.os-image
module.sonic-2.libvirt_domain.domain-sonic
module.sonic-2.libvirt_volume.os-image
また、virsh list
やvirsh net-list
などでも作成できていることが確認できます。
$ virsh list
Id Name State
------------------------
41 s2 running
42 s1 running
$ virsh net-list
Name State Autostart Persistent
--------------------------------------------
c1 active no yes
default active yes yes
$ virsh net-port-list c1
UUID
---------------------------------------
3bebbe2b-ff78-49d5-9bea-cbf28385154d
55867b00-38cf-45da-909b-67eecaa7537b
$
動作確認
virt-viewer
でs1とs2のコンソールを開きます。
公式Jenkinsから拾ったイメージなら admin/YourPaSsWoRd
で、自分でビルドしたイメージならそこで指定したパスワードでログインします。
インタフェースはupしているか?
起動したとき、デフォルトのconfigが同一なためどちらのVMもIPアドレスが同じなので、/etc/sonic/config_db.json
を書き換えます。Ethernet0
以外は今回使わないので思い切って削除。編集がおわったらリブートします。(下記はINTERFACE
部分だけを引っ張り出してます)
{
INTERFACE {
"Ethernet0|10.0.0.0/31": {}
}
}
{
INTERFACE {
"Ethernet0|10.0.0.1/31": {}
}
}
再起動してきたら、それぞれのVMでshow ip interfaces
を実行し、upしているかを確認してみます。
admin@sonic:~$ show ip interfaces
Interface Master IPv4 address/mask Admin/Oper BGP Neighbor Neighbor IP
----------- -------- ------------------- ------------ -------------- -------------
Ethernet0 10.0.0.0/31 up/up N/A N/A
Loopback0 10.1.0.1/32 up/up N/A N/A
docker0 240.127.1.1/24 up/down N/A N/A
eth0 192.168.122.240/24 up/up N/A N/A
lo 127.0.0.1/16 up/up N/A N/A
admin@sonic:~$ show ip interfaces
Interface Master IPv4 address/mask Admin/Oper BGP Neighbor Neighbor IP
----------- -------- ------------------- ------------ -------------- -------------
Ethernet0 10.0.0.1/31 up/up N/A N/A
Loopback0 10.1.0.1/32 up/up N/A N/A
docker0 240.127.1.1/24 up/down N/A N/A
eth0 192.168.122.57/24 up/up N/A N/A
lo 127.0.0.1/16 up/up N/A N/A
大丈夫そうです。
pingは通るのか?
では、ping
を使って実際に通信できることを確認してみましょう。
admin@sonic:~$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(64) bytes of data.
PING 10.0.0.0 icmp_seq=1 Destination Host Unreachable
PING 10.0.0.0 icmp_seq=2 Destination Host Unreachable
PING 10.0.0.0 icmp_seq=3 Destination Host Unreachable
……返答がありません。あれ?
s2
から10.0.0.0
に向かってpingしても結果は同じ。なぜ?
そもそもパケットが届いていないのか? と思い、s2
でtcpdump
しておいてs1
からping
してみると
admin@sonic:~$ sudo tcpdump -i Ethernet0
tcpdump: verbose output suggested, use -v or -vv for full protocol decode.
listning on Ethernet0, link-type EM10MB (Ethernet), capture size 262144 bytes
02:13:06.079409 ARP, Request who-has 10.0.0.1 tell 10.0.0.0, length 20
02:13:06.079442 ARP, Reply 10.0.0.1 is-at 52.54:00:12:34:56 (oui Unknown), length 20
ここでMACアドレスをよく見ていればすぐ気づいたのですが、ARP届いてるし返事してるわーと思って別のところもいろいろ調べて首を傾げていました。
そいやパケットは来てたけどarp tableちゃんとしてる? と思い show arp
で確認して原因がわかりました。
admin@sonic:~$ show arp
Address MacAddress Iface Vlan
------------- ----------------- --------- ------
10.0.0.1 52:54:00:12:34:56 Ethernet0 -
192.168.122.1 52:54:00:31:dd:3a eth0 -
Total number of entries 2
admin@sonic:~$ show arp
Address MacAddress Iface Vlan
------------- ----------------- --------- ------
10.0.0.0 52:54:00:12:34:56 Ethernet0 -
192.168.122.1 52:54:00:31:dd:3a eth0 -
Total number of entries 2
192.168.122.1
は外部接続のgatewayなので同じでいいとして、Ethernet0
の……
この2つのVM、MACアドレスが同じ! ダメ……!!
そういえば、/etc/sonic/config_db.json
のDEVICE_METADATA
の中にMACアドレスの情報がベタっと書かれてるのを思い出しました。
{
"DEBICE_METADATA": {
"mac": "52:54:00:12:34:56"
}
}
これだ! というわけで、どちらか片方だけでいいので別の値に書き換えます。外に出ていくわけではないのでテキトーに。たとえば56
を57
に。書き換えた後は、sudo config reload
でもいい気がしますが、リブート。
再起動してきたら、あらためてping
してみます。
admin@sonic:~$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(64) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.550 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.354 ms
無事ping
がとおりました! めでたしめでたし。
しかし、ここ(MACアドレス設定のユニーク化)を自動化する方法は現時点では発見できていません。
- 起動前に設定?
virt-install
したあとのディスクイメージにアクセスできる? - 起動後に設定?
virsh console
が動かないので無理かも…… - イメージに細工?
config_db.json
からmac
を消したらどうなる? - ZTPを使う? 別サーバを別途建ててそちらに設定を仕込む必要が……
実機だとぶつかることはないので、無理に自動化しなくてもいいという考え方もあります。もっとも、実機だとTerraformでデプロイできませんが……
※2021/01/26追記: network_interface
に mac = "XX:Xx:XX:XX:XX:XX"
を加えることで明示的にMACアドレスを変更できるようでした。
おそらくDebianのVMで同じことをやれば、ハマることはなかったのではないかと思います。
どのくらいリソースを食うの?
DebianやSONiCのVMに2GBメモリを指定したら、ほぼそのまま2GB消費してました。2つ起動で4GB。4つ起動すれば8GB。手元では、32GBメモリ搭載マシンでなにもデプロイしてない状態で利用中メモリが7.5GB程度。つまりVMを12個起動するとアップアップということに。10個動かせれば十分なのかも知れませんが、メモリ増設したくなる瞬間でした。
おわりに
SONiCのVMを複数起動してインタフェースをつなぐことができました。もう少し規模を増やして、両端にホストVMをつなぐなどすれば、いろいろなテストが捗るのではないかと思います。
もっとも、SONiCの場合VMで動くが実機で動かないということがありうるので油断は禁物なのですが。実機で規模の大きなテスト環境を用意するのは時間も金額も人手も大変なので、仮想環境で少しでも楽できるといいなあと思っています。
今回作成したtfファイルをgithubで公開しました。
https://github.com/iMasaruOki/terraform-libvirt-sonic-example
参考になれば幸いです。