GCE Nested V12n で VT-x on KVM を利用する
- Authors
- Name
- ごとれん
- X
- @ren510dev
目次
- 目次
- はじめに
- GCE Nested Virtualization
- パフォーマンスに関する考慮事項
- 利用に関する制限事項
- 仮想マシンのネットワークトポロジ
- オンプレミスネットワーク
- Google Cloud VPC の制約
- Alias IP による L2 VM の透過性
- Nested V12n で L2 VM を構築する
- VPC の準備
- L1 VM の構築
- L2 VM の構築
- iptables Chain の追加
- nftables への移行とルールの永続化
- まとめ
- 参考・引用

はじめに
サーバ仮想化技術は、物理サーバのコンピュートリソースを最大限活用するために、従来から利用されてきました。 サーバ仮想化を実現する代表的なハイパバイザに KVM(Kernel-based Virtual Machine) があります。 KVM は、オープンソースでありながら高い性能と安定性を持ち、Linux に深く統合されていることから、多くのオンプレミス環境やプライベートクラウドで採用されてきた実績があります。
※ サーバ仮想化におけるホスト型仮想化方式とハイパバイザ型仮想化方式については こちら のブログでも紹介しています。
昨今ではパブリッククラウドが普及したことで、オンプレミスのサーバから GCE や EC2、AVM といったコンピュートマシンへのリフトアンドシフトが進んでいます。 大半の場合、パブリッククラウドが提供するコンピュートマシンは、それ自体が仮想化された VM として提供されます。
ここで、従来利用してきた「KVM によるサーバ仮想化はクラウドプロバイダが提供する VM でも利用できるのか」という疑問が生じました。
答え、クラウドプロバイダが提供する VM でも Nested Virtualization(Nested V12n) という仕組みを利用できます。 Nested V12n は、その名の通りネストされた仮想化を意味し、VM の中で別の VM を実行(VM in VM)することを指します。 Nested V12n を使用すると、クラウドプロバイダが提供する VM 内で KVM を利用することができます。
しかし、実際にはオンプレミスの物理マシンとクラウドプロバイダが提供する仮想マシンでは、KVM の構築に際して仕組み上異なる部分があり、後者の場合は特有の制約もあります。
今回のブログでは、Google Cloud の VM(GCE)で Nested V12n を利用し、libvirt を用いて Linux KVM を構築してみたので、クラウドサービスの VM で KVM を実行する方法や、VM ネットワークの違いについて紹介したいと思います。
GCE Nested Virtualization
公式ドキュメント:About nested virtualization

Google Cloud の Compute Engine(GCE)では Nested Virtualization(Nested V12n)によるネストされた VM が公式にサポートされています。
Google Cloud の仮想環境構成は次のような階層構造になっています。
- Level 0(L0)
- Google Cloud のデータセンターにある物理サーバ
- Google 独自のセキュリティ強化版 KVM ベースのハイパバイザが稼働する(これによって GCE 本体が払い出される)
- Level 1(L1)
- ユーザが作成した Compute Engine VM(普段利用している GCE そのもの)
- この VM 内部で KVM / QEMU を使用して L0 とは別のハイパバイザを利用できる
- Level 2(L2)
- ユーザが L1 VM 内で実行するさらに別の VM(L1 ホストのゲストマシン)
- L1 VM 内のハイパバイザで仮想マシンを実行する
[Google Physical Host] ← L0
└── [Compute Engine VM] ← L1
└── [Nested VM] ← L2
Google Cloud が保有する L0 の物理マシンには、Intel VT-x が仮想化支援機能として搭載されており、GCE の Nested V12n 機能を有効化することで L1 VM から利用できるようになります。 なお、現時点では L0 のチップは AMD-v には対応していないようです。
GCE で Nested V12n を利用するには、VM インスタンスを作成する際に --enable-nested-virtualization
フラグを渡します。
$ gcloud compute instances create [GCE_VM_NAME] --enable-nested-virtualization
パフォーマンスに関する考慮事項
ハードウェア支援による Nested V12n の場合でも、ネストされた L2 VM では、
- CPU にバインドされたワークロードのパフォーマンスが 10% 以上低下する可能性がある
- 入出力にバインドされたワークロードでは 10% を超える低下が発生する可能性がある
という点に注意が必要です。
利用に関する制限事項
L1 VM には次の制限があります。
L1 VM でサポートされるハイパバイザは Linux KVM のみで、Microsoft Hyper-V はサポートされていません。
また、L1 VM(GCE)本体のマシンタイプとして、以下のシリーズは Nested V12n を利用できません。
L2 VM には次の制限があります。
ライセンスが必要な OS を L2 VM で実行する場合は、独自のライセンスを用意する必要があり、BYOL(Bring Your Own License) となります。
例えば、Windows Server 2019 の場合、L1 VM では Google Cloud 経由でライセンス提供(課金込み)されますが、L2 VM で実行する場合は、独自にライセンスを取得する必要があります。
仮想マシンのネットワークトポロジ
オンプレミスネットワーク
KVM におけるデフォルトのネットワーク構成は NAT Mode となるため、ホストマシンとゲストマシンは NAT を介して通信します。

この場合、ホストマシンが属する LAN 内のプライベート IP アドレスを用いて直接通信することはできません。
ゲストマシンに LAN 内のプライベート IP アドレスを割り当てる場合、Macvlan という Linux カーネルの機能を用います。 Macvlan を使用するとホストマシンとゲストマシンの間で L2 レベルの疎通を取ることが可能となり、これによってゲストマシンはホストマシンと同一 LAN に接続することができます。
Macvlan の Bridge Mode は、ネットワークブリッジを利用して ホストマシンの物理 NIC に複数の MAC アドレスを独立して追加 します。 通常、NIC は 1 つの MAC アドレスしか持つことができませんが、macvlan インターフェースを作成することで、複数 MAC アドレスを同一の NIC で扱えるようになります。
また、ホストマシン上に構築されたゲストマシンは TAP(Terminal Access Point) と呼ばれる、イーサネットデバイスをシミュレートするトンネルインターフェースを通じて macvlan に接続します。
※ KVM の仮想ネットワーク構成の全体像ついては こちら の記事が参考になります。
Ubuntu の場合、以下のようなスクリプトで 物理 NIC(enp3s0
)に紐付く IP アドレスを macvlan(macvlan0
)に割り当て直すことができます。参考
#!/bin/sh
HOST_INTERFACE_NAME=enp3s0
HOST_IPV4_ADDR=10.10.0.105/20
DEFAULT_GATEWAY_ADDR=10.10.15.254
BROADCAST_ADDR=10.10.15.255
MACVLAN_NAME=macvlan0
### 1. macvlan I/F を追加して物理 NIC をブリッジ
ip link add dev ${MACVLAN_NAME} link ${HOST_INTERFACE_NAME} type macvlan mode bridge
### 2. 元の物理 NIC から IP アドレスを削除
ip addr del ${HOST_IPV4_ADDR} dev ${HOST_INTERFACE_NAME}
### 3. 物理 NIC に紐付いていた元の IP アドレスを macvlan I/F に付け替える
ip addr add ${HOST_IPV4_ADDR} broadcast ${BROADCAST_ADDR} dev ${MACVLAN_NAME}
### 4. macvlan I/F を有効化
ip link set ${MACVLAN_NAME} up
### 5. 物理 NIC のルート情報をすべて削除
ip route flush dev ${HOST_INTERFACE_NAME}
### 6. デフォルトルートを macvlan I/F に向ける
ip route add default via ${DEFAULT_GATEWAY_ADDR} dev ${MACVLAN_NAME} proto static
### 7. NetworkManager に反映
systemctl restart NetworkManager.service
libvirt で起動した仮想マシンは、TAP として macvtap を生成することでゲストマシンが独自の MAC アドレスと IP アドレスを持ち、L2 レベルでホストマシンと同一 LAN に参加できるようになります。
ホストマシン
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
### ホストマシンの物理 NIC
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether a8:a1:59:85:11:4a brd ff:ff:ff:ff:ff:ff
inet6 fe80::aaa1:59ff:fe85:114a/64 scope link
valid_lft forever preferred_lft forever
### libvirt のデフォルトの仮想ブリッジ
3: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 52:54:00:48:72:53 brd ff:ff:ff:ff:ff:ff
inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
valid_lft forever preferred_lft forever
### 今回作成されたゲスト VM 用の仮想ブリッジ(192.168.121.0/24 の VM 間ローカルネットワーク)
4: virbr1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 52:54:00:40:cd:eb brd ff:ff:ff:ff:ff:ff
inet 192.168.121.1/24 brd 192.168.121.255 scope global virbr1
valid_lft forever preferred_lft forever
### ホストマシンの macvlan I/F(enp3s0 をブリッジ)
5: macvlan0@enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether f6:a4:58:20:4e:08 brd ff:ff:ff:ff:ff:ff
inet 10.10.0.105/20 brd 10.10.15.255 scope global macvlan0
valid_lft forever preferred_lft forever
### ゲストマシン1 が利用する macvtap I/F
6: macvtap0@enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 500
link/ether 52:54:00:13:c7:c0 brd ff:ff:ff:ff:ff:ff
inet6 fe80::5054:ff:fe13:c7c0/64 scope link
valid_lft forever preferred_lft forever
### ゲストマシン1 が virbr1 に接続するための仮想ブリッジ
7: vnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr1 state UNKNOWN group default qlen 1000
link/ether fe:54:00:29:6c:6b brd ff:ff:ff:ff:ff:ff
### ゲストマシン2 が利用する macvtap I/F
8: macvtap1@enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 500
link/ether 52:54:00:cb:66:17 brd ff:ff:ff:ff:ff:ff
inet6 fe80::5054:ff:fecb:6617/64 scope link
valid_lft forever preferred_lft forever
### ゲストマシン2 が virbr1 に接続するための仮想ブリッジ
9: vnet1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr1 state UNKNOWN group default qlen 1000
link/ether fe:54:00:ac:a3:37 brd ff:ff:ff:ff:ff:ff
### ブリッジリスト
$ brctl show
bridge name bridge id STP enabled interfaces
virbr0 8000.525400487253 yes
virbr1 8000.52540040cdeb yes vnet0
vnet1
### server01 の仮想 NIC
$ virsh domiflist ren510dev_server01
Interface Type Source Model MAC
---------------------------------------------------------------------
vnet0 network vagrant-libvirt virtio 52:54:00:29:6c:6b
macvtap0 direct enp3s0 virtio 52:54:00:13:c7:c0
### server02 の仮想 NIC
$ virsh domiflist ren510dev_server02
Interface Type Source Model MAC
---------------------------------------------------------------------
vnet1 network vagrant-libvirt virtio 52:54:00:ac:a3:37
macvtap1 direct enp3s0 virtio 52:54:00:cb:66:17
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
enp3s0 | a8:a1:59:85:11:4a | 無し (ホストマシンの物理 NIC) |
virbr0 | 52:54:00:48:72:53 | ホストマシンのデフォルトゲートウェイに NAT 接続 (libvirt のデフォルトネットワーク) |
virbr1 | 52:54:00:40:cd:eb | ホストマシンのデフォルトゲートウェイに NAT 接続 (ゲスト VM 用の仮想ネットワーク) |
macvlan0 | f6:a4:58:20:4e:08 | デフォルトゲートウェイ ( enp3s0 をブリッジ) |
macvtap0 | 52:54:00:13:c7:c0 | macvlan0 |
vnet0 | fe:54:00:29:6c:6b | virbr1 |
macvtap1 | 52:54:00:cb:66:17 | macvlan0 |
vnet1 | fe:54:00:ac:a3:37 | virbr1 |
ゲストマシン
- ゲストマシン 1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:29:6c:6b brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
inet 192.168.121.83/24 metric 100 brd 192.168.121.255 scope global dynamic eth0
valid_lft 2436sec preferred_lft 2436sec
inet6 fe80::5054:ff:fe29:6c6b/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:13:c7:c0 brd ff:ff:ff:ff:ff:ff
altname enp0s6
altname ens6
inet 10.10.10.110/20 brd 10.10.15.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe13:c7c0/64 scope link
valid_lft forever preferred_lft forever
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
eth0 | 52:54:00:29:6c:6b | vnet0 |
eth1 | 52:54:00:13:c7:c0 | macvtap0 ( macvlan0 の TAP) |
- ゲストマシン 2
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:ac:a3:37 brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
inet 192.168.121.145/24 metric 100 brd 192.168.121.255 scope global dynamic eth0
valid_lft 2413sec preferred_lft 2413sec
inet6 fe80::5054:ff:feac:a337/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:cb:66:17 brd ff:ff:ff:ff:ff:ff
altname enp0s6
altname ens6
inet 10.10.10.111/20 brd 10.10.15.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fecb:6617/64 scope link
valid_lft forever preferred_lft forever
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
eth0 | 52:54:00:ac:a3:37 | vnet1 |
eth1 | 52:54:00:cb:66:17 | macvtap1 ( macvlan0 の TAP) |
ネットワーク構成
上の接続関係を図にまとめると以下のようになります。

参考
# frozen_string_literal: true
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure('2') do |config|
config.vm.boot_timeout = 600
config.vm.define :server01 do |server01|
define_machine_spec server01, '8', '32768'
server01.vm.hostname = 'server01'
server01.vm.box = 'generic/ubuntu2204'
server01.vm.network :public_network,
ip: '10.10.10.110',
netmask: '255.255.240.0',
bridge: 'enp3s0',
dev: 'enp3s0'
end
config.vm.define :server02 do |server02|
define_machine_spec server02, '8', '24576'
server02.vm.hostname = 'server02'
server02.vm.box = 'generic/ubuntu2204'
server02.vm.network :public_network,
ip: '10.10.10.111',
netmask: '255.255.240.0',
bridge: 'enp3s0',
dev: 'enp3s0'
end
## Define macros
def define_machine_spec(name, use_cpu, use_memory)
name.vm.provider :libvirt do |libvirt|
libvirt.driver = 'qemu'
libvirt.cpus = use_cpu
libvirt.memory = use_memory
end
end
end
赤系統のネットワークは NetworkManager が管理していて、青系統のネットワークは libvirt が管理するデフォルトの仮想ネットワークです。
スクリプトで作成される macvlan0
はホストマシンの物理 NIC(enp3s0
)をブリッジする形で LAN 内の他のマシンと同一のネットワークに接続されます。
また、ゲストマシンは TAP デバイス(macvtap0
/ macvtap1
)を通じてホストマシンの macvlan0
に接続し、最終的に enp3s0
をブリッジすることで、独立した個別のマシンとして LAN に直接参加することができます。
疎通確認
- ICMP
### ゲストマシン1(10.10.10.110)→ ゲストマシン2(10.10.10.111)
$ ping -c 3 10.10.10.111
PING 10.10.10.111 (10.10.10.111) 56(84) bytes of data.
64 bytes from 10.10.10.111: icmp_seq=1 ttl=64 time=1.79 ms
64 bytes from 10.10.10.111: icmp_seq=2 ttl=64 time=1.53 ms
64 bytes from 10.10.10.111: icmp_seq=3 ttl=64 time=1.62 ms
--- 10.10.10.111 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.534/1.649/1.794/0.108 ms
### ゲストマシン1(10.10.10.110)→ ホストマシン(10.10.0.105)
$ ping -c 3 10.10.0.105
PING 10.10.0.105 (10.10.0.105) 56(84) bytes of data.
64 bytes from 10.10.0.105: icmp_seq=1 ttl=64 time=0.269 ms
64 bytes from 10.10.0.105: icmp_seq=2 ttl=64 time=0.799 ms
64 bytes from 10.10.0.105: icmp_seq=3 ttl=64 time=1.10 ms
--- 10.10.0.105 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 0.269/0.722/1.100/0.343 ms
### ゲストマシン1(10.10.10.110)→ LAN 内の他のマシン(10.10.0.106)
$ ping -c 3 10.10.0.106
PING 10.10.0.106 (10.10.0.106) 56(84) bytes of data.
64 bytes from 10.10.0.106: icmp_seq=1 ttl=64 time=0.496 ms
64 bytes from 10.10.0.106: icmp_seq=2 ttl=64 time=1.17 ms
64 bytes from 10.10.0.106: icmp_seq=3 ttl=64 time=1.18 ms
--- 10.10.0.106 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2006ms
rtt min/avg/max/mdev = 0.496/0.949/1.184/0.320 ms
- マッピング情報
### ゲストマシン1 の ARP テーブル
$ arp -na
? (10.10.0.105) at f6:a4:58:20:4e:08 [ether] on eth1 ## ホストマシン macvlan0
? (10.10.0.106) at a8:a1:59:85:13:4c [ether] on eth1 ## LAN 内の他のマシンの物値 NIC
? (10.10.10.111) at 52:54:00:cb:66:17 [ether] on eth1 ## ホストマシン macvtap1(ゲストマシン2 の eth1)
では、Google Cloud でも VPC サブネットの IP アドレスを直接、ゲストマシン(L2 VM)に割り当てることができるのか。
結論、できません。
これは Google Cloud VPC の仕様そのものに起因しており、GCE では基本的に L3 より下のレイヤで行われる通信をハンドリングすることができない ためです。
Google Cloud VPC の制約
Google Cloud の VPC は、物理的なネットワークではなく Google が独自に実装した Andromeda と呼ばれる SDN(Software Defined Networking) 上に構築されており、次のような制約があります。
L2 の非透過性
VPC networks do not support broadcast or multicast addresses within the network.
VPC は L3(IP レイヤの通信)ベースで設計されており、ARP や DHCP の L2 ブロードキャストリクエストが通りません。
そのため、L1 VM のハイパバイザが L2 VM に仮想 NIC(macvtap, bridge 等)をアタッチしても、VPC 内の DHCP サーバ(または相当の機能を担うもの)や、他のホストは L2 VM が持つ MAC アドレスを認識することができません。
IP アドレスの偽装防止
By default, IP forwarding is disabled, and Google Cloud performs strict source address checking.
The
next-hop-address
of a static route must match an IP address that is assigned to a VM in the route's VPC network.
The anti-spoofing checks verify that VMs do not send traffic with arbitrary source IP addresses.
Google Cloud では Source Address Spoofing Guard(送信元アドレス偽造防止)という仕組みによって、仮想マシンのトラフィックが VM の仮想 NIC に割り当てられた IP アドレスから送信されていることを常に検証しています。
つまり、その IP を使って良いのは、その NIC に紐付いたマシンのみ (1 IP アドレスにつき 1 MAC アドレス) という前提があるため、例えば、ある VM の eth0
に 10.10.10.110/32
というアドレスが割り当てられている場合、この VM がこれ以外の IP アドレスを送信元としてパケットを送信しようとすると VPC 内でドロップされます。
Alias IP による L2 VM の透過性
Google Cloud の VPC ネットワークには前述のような制約があるため、L2 VM に直接サブネット内のプライベート IP アドレスを割り当てることができません。
では、どのようにサブネットの IP アドレスを L2 VM に割り当てるかというと、Alias IP という Google Cloud が用意している独自の仕組み利用します。
Alias IP を使用すると、L1 VM が属するサブネット内のプライベート IP アドレスを L2 VM の NIC に 透過的に割り当てる ことができます。 ポイントは "透過的" という部分で、直接 L2 VM の NIC が L1 VM と同じサブネット帯域のプライベート IP アドレスを持つのではなく、L1 VM がその IP アドレスをエイリアスとして持ち、L2 VM への通信を透過的に転送(IP Fowarding)するという仕組みで管理します。
まとめると、
- L2 VM にはローカル IP(例:
172.15.7.0/24
)を設定 - L1 VM に Alias IP を追加
- L1 VM の netfilter(iptables) で DNAT して L2 VM へパケットを転送(後方通信の場合は SNAT)
という流れになります。

つまり、L2 VM は 直接 VPC サブネットのプライベート IP アドレスを持ちません。 Alias IP を用いて L2 VM に対する通信を L1 VM で受け取り、iptables でマスカレードすることでパケットを橋渡しします。
これによって VPC 内の他のマシンからは、あたかも L2 VM が直接 VPC サブネットにぶら下がっているように見える わけです。
Nested V12n で L2 VM を構築する
Google Cloud で KVM を扱う際の注意点を抑えたところで、実際に Nested V12n を利用して GCE 上に L2 VM を構築してみます。
VPC の準備
VM を作成する前に、まずは VPC ネットワークを構築します。
今回は、東京リージョンに VPC を用意し、サブネット帯域は 10.10.0.0/20
とします。
Terraform
module "vpc" {
source = "terraform-google-modules/network/google"
version = "11.0.0"
project_id = "ren510dev"
network_name = "vagrant-playground-vpc"
routing_mode = "GLOBAL"
subnets = [
{
subnet_name = "vagrant-playground-asia-northeast1-01"
subnet_ip = "10.10.0.0/20"
subnet_region = "asia-northeast1"
description = "VPC subnet for Vagrant Playground"
subnet_private_access = "true"
subnet_flow_logs = "true"
},
]
firewall_rules = [
{
name = "${module.vpc.network_name}-allow-internal"
description = "Allow internal traffic"
direction = "INGRESS"
allow = [
{
protocol = "icmp"
},
{
protocol = "tcp"
ports = ["1-65535"]
},
{
protocol = "udp"
ports = ["1-65535"]
}
]
ranges = module.vpc.subnets_ips
},
{
name = "${module.vpc.network_name}-allow-iap-ssh"
description = "Allow IAP SSH traffic"
direction = "INGRESS"
allow = [
{
protocol = "tcp"
ports = ["22", "3389"]
},
{
protocol = "icmp"
}
]
ranges = ["35.235.240.0/20"] # https://cloud.google.com/iap/docs/using-tcp-forwarding#create-firewall-rule
}
]
}

L1 VM の構築
サーバ
次に VPC 上に Nested V12n が有効化された L1 VM、つまり GCE インスタンスそのものを構築します。
IP アドレスは次のように割り当てることにします。
- ホスト名:
vagrant-playground-instance-01
割り当て | IP アドレス | 概要 |
---|---|---|
Primary IP | 10.10.10.10/32 | GCE 本体(L1 VM)の IP アドレス |
Alias IP | 10.10.10.110/32 | L2 VM 1(server01)の IP アドレス |
Alias IP | 10.10.10.111/32 | L2 VM 2(server02)の IP アドレス |
Terraform
resource "google_service_account" "server" {
account_id = "vagrant-playground-server"
display_name = "vagrant-playground-server"
description = "GCE Service Account for vagrant-playground-server"
}
resource "google_compute_instance" "server" {
name = "vagrant-playground-instance-01"
description = "Vagrant Playground Instance 01"
machine_type = "n2-standard-32" ## 16core / 32thread / 128 GB
zone = "asia-northeast1-a"
tags = [
"vagrant-playground-instance-01",
]
can_ip_forward = true ## IP フォワーディングを有効化
advanced_machine_features {
enable_nested_virtualization = true ## Nested virtualization を有効化
}
boot_disk {
auto_delete = true
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2204-lts" ## Ubuntu 22.04 LTS
size = 500 ## 500 GB
type = "pd-balanced"
}
}
network_interface {
network = "vagrant-playground-vpc"
subnetwork = "vagrant-playground-asia-northeast1-01"
network_ip = "10.10.10.10"
alias_ip_range {
ip_cidr_range = "10.10.10.110/32" ## server01 の IP アドレス
subnetwork_range_name = null
}
alias_ip_range {
ip_cidr_range = "10.10.10.111/32" ## server02 の IP アドレス
subnetwork_range_name = null
}
access_config {
nat_ip = null
public_ptr_domain_name = null
network_tier = "PREMIUM"
}
}
service_account {
email = google_service_account.server.email
scopes = [
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/servicecontrol",
"https://www.googleapis.com/auth/trace.append"
]
}
depends_on = [
google_service_account.server
]
}

Nested V12n が有効化されているかを確認します。
$ gcloud compute instances describe vagrant-playground-instance-01 \
--zone=asia-northeast1-a \
--format="yaml(advancedMachineFeatures.enableNestedVirtualization)"
advancedMachineFeatures:
enableNestedVirtualization: true
L1 VM の Alias IP が正しく設定されているかを確認します。
$ gcloud compute instances describe vagrant-playground-instance-01 \
--zone=asia-northeast1-a \
--format="yaml(networkInterfaces[].aliasIpRanges)"
networkInterfaces:
- aliasIpRanges:
- ipCidrRange: 10.10.10.110/32
- ipCidrRange: 10.10.10.111/32
以下のコマンドで実際に L1 VM がハードウェア仮想化支援機能をサポートしているかを確認します。 Google Cloud の L0 VM は Intel VT-x のみのサポートとなるため、CPU フラグの vmx
を確認します。
### CPU フラグが 1 以上であることを確認
$ egrep -c 'vmx' /proc/cpuinfo
64
コマンド出力から、L1 VM の論理 CPU 64 コア分に VT-x が搭載されており、カーネルがそれを認識していることが分かります。
また、KVM を利用するためのカーネルモジュールは以下のコマンドで確認できます。
$ ls -l /dev/kvm
crw-rw---- 1 root kvm 10, 232 Nov 4 06:05 /dev/kvm
クライアント
上で構築した、L2 VM に対するネットワーク疎通を検証するために、VPC サブネット内に別の GCE を配置しておきます。
- ホスト名:
vagrant-playground-instance-02
割り当て | IP アドレス | 概要 |
---|---|---|
Primary IP | 10.10.10.15/32 | GCE 本体(L1 VM)の IP アドレス |
Terraform
resource "google_service_account" "client" {
account_id = "vagrant-playground-client"
display_name = "vagrant-playground-client"
description = "GCE Service Account for vagrant-playground-client"
}
resource "google_compute_instance" "client" {
name = "vagrant-playground-instance-02"
description = "Vagrant Playground Instance 02"
machine_type = "e2-micro" # 安価なもので良い
zone = "asia-northeast1-a"
tags = [
"vagrant-playground-instance-02",
]
can_ip_forward = false
boot_disk {
auto_delete = true
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2204-lts"
size = 500
type = "pd-balanced"
}
}
network_interface {
network = "vagrant-playground-vpc"
subnetwork = "vagrant-playground-asia-northeast1-01"
network_ip = "10.10.10.15"
access_config {
nat_ip = null
public_ptr_domain_name = null
network_tier = "PREMIUM"
}
}
service_account {
email = google_service_account.client.email
scopes = [
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/servicecontrol",
"https://www.googleapis.com/auth/trace.append"
]
}
depends_on = [
google_service_account.client
]
}

スペックの確認
L1 VM のマシンスペックを確認してみます。
#!/bin/bash
P_CPU=$(fgrep 'physical id' /proc/cpuinfo | sort -u | wc -l)
CORES=$(fgrep 'cpu cores' /proc/cpuinfo | sort -u | sed 's/.*: //')
L_PRC=$(fgrep 'processor' /proc/cpuinfo | wc -l)
H_TRD=$((L_PRC / P_CPU / CORES))
echo -n "${L_PRC} processer"
[ "${L_PRC}" -ne 1 ] && echo -n "s"
echo -n " = ${P_CPU} socket"
[ "${P_CPU}" -ne 1 ] && echo -n "s"
echo -n " x ${CORES} core"
[ "${CORES}" -ne 1 ] && echo -n "s"
echo -n " x ${H_TRD} thread"
[ "${H_TRD}" -ne 1 ] && echo -n "s"
echo
32 processers = 2 sockets x 8 cores x 2 threads
$ free -h
total used free shared buff/cache available
Mem: 125Gi 788Mi 124Gi 1.0Mi 552Mi 124Gi
Swap: 0B 0B 0B
今回使用している n2-standard-32 タイプは 16 コア 32 スレッド 128GB RAM なので、問題無く起動していることが分かります。
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 485G 2.2G 483G 1% /
tmpfs 63G 0 63G 0% /dev/shm
tmpfs 26G 1.1M 26G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
efivarfs 56K 24K 27K 48% /sys/firmware/efi/efivars
/dev/sda15 105M 6.1M 99M 6% /boot/efi
tmpfs 13G 4.0K 13G 1% /run/user/1014
ストレージには 500 GB の pd-balanced タイプを使用しています。
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
link/ether 42:01:0a:0a:0a:0a brd ff:ff:ff:ff:ff:ff
inet 10.10.10.10/32 metric 100 scope global dynamic ens4
valid_lft 85657sec preferred_lft 85657sec
inet6 fe80::4001:aff:fe0a:a0a/64 scope link
valid_lft forever preferred_lft forever
L1 VM の NIC(ens4
)に 10.10.10.10/32
がプライマリ IP アドレスとして割り当てられていることが分かります。
一方で、Alias IP は L1 VM 内からは直接確認することができません。 これは、Alias IP が Google Cloud 側で管理する VPC ネットワークレイヤに SDN でルーティング可能な IP としてマッピングされているだけであり、OS 側には明示的に付与されていないためです。
L2 VM の構築
続いて、L1 VM 上に L2 VM を構築します。 L2 VM の構築には Vagrant を使用します。
Vagrant はハイパバイザ(KVM)で仮想環境を構築するためのツールで、Vagrantfile による IaC でゲストマシンを管理できます。 Vagrant はそれ単体で動作するわけではなく、libvirt API 等の抽象化レイヤを挟み、最終的に QEMU を呼び出して仮想マシンを制御します。

クライアントが Vagrant、バックエンドが libvirt、インフラが QEMU といったイメージです。
Vagrant の開発元は HashiCorp ですが、Terraform のようにクラウドプロバイダを抽象化しているわけではなく、あくまでローカルの仮想環境を構築するためのツールとなっています。
- L2 VM の構築に必要なパッケージをインストール
$ sudo apt update && sudo apt upgrade -y
$ sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst vagrant vagrant-libvirt uml-utilities net-tools
- libvirt グループに一般ユーザを追加
インストール時点では libvirt がルート権限でシステムサービスとして動作しており、仮想マシンの起動・停止、NIC の管理等を担っています。 そのため、一般ユーザが libvirt を操作するためには libvirt グループに追加してあげる必要があります。
### libvirt グループへユーザを追加
$ sudo usermod -aG libvirt $(whoami)
### グループ変更を反映
$ newgrp libvirt
- プラグインのインストール
vagrant-libvirt プラグインを使用することで、Vagrant から libvirt を呼び出せるようになります。
$ vagrant plugin install vagrant-libvirt
Installing the 'vagrant-libvirt' plugin. This can take a few minutes...
Installed the plugin 'vagrant-libvirt (0.7.0)'!
- 仮想マシンの起動
以下の Vagrantfile から L2 VM を起動します。
Vagrantfile
# frozen_string_literal: true
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure('2') do |config|
config.vm.boot_timeout = 600
config.vm.define :server01 do |server01|
define_machine_spec server01, '8', '32768'
server01.vm.hostname = 'server01'
server01.vm.box = 'generic/ubuntu2204'
server01.vm.network :private_network,
ip: '172.15.7.11',
netmask: '255.255.255.0',
auto_config: true
end
config.vm.define :server02 do |server02|
define_machine_spec server02, '8', '24576'
server02.vm.hostname = 'server02'
server02.vm.box = 'generic/ubuntu2204'
server02.vm.network :private_network,
ip: '172.15.7.12',
netmask: '255.255.255.0',
auto_config: true
end
## Define macros
def define_machine_spec(name, use_cpu, use_memory)
name.vm.provider :libvirt do |libvirt|
libvirt.driver = 'qemu'
libvirt.cpus = use_cpu
libvirt.memory = use_memory
end
end
end
今回はゲストマシンに Ubuntu 22.04 を使用します。
また、L2 VM と L1 VM 間のプライベートネットワークは 172.15.7.0/24
としています。
L2 VM | IP アドレス |
---|---|
server01 | 172.15.7.11 |
server02 | 172.15.7.12 |
$ vagrant up
Bringing machine 'server01' up with 'libvirt' provider...
Bringing machine 'server02' up with 'libvirt' provider...
==> server01: Box 'generic/ubuntu2204' could not be found. Attempting to find and install...
server01: Box Provider: libvirt
server01: Box Version: >= 0
==> server01: Loading metadata for box 'generic/ubuntu2204'
server01: URL: https://vagrantcloud.com/generic/ubuntu2204
==> server01: Adding box 'generic/ubuntu2204' (v4.3.12) for provider: libvirt
server01: Downloading: https://vagrantcloud.com/generic/boxes/ubuntu2204/versions/4.3.12/providers/libvirt/amd64/vagrant.box
server01: Calculating and comparing box checksum...
==> server01: Successfully added box 'generic/ubuntu2204' (v4.3.12) for 'libvirt'!
==> server02: Box 'generic/ubuntu2204' could not be found. Attempting to find and install...
server02: Box Provider: libvirt
server02: Box Version: >= 0
==> server01: Uploading base box image as volume into Libvirt storage...
==> server02: Loading metadata for box 'generic/ubuntu2204'
Progress: 41% server02: URL: https://vagrantcloud.com/generic/ubuntu2204
...
...
...
server01:
server01: Vagrant insecure key detected. Vagrant will automatically replace
server01: this with a newly generated keypair for better security.
server01:
server01: Inserting generated public key within guest...
server01: Removing insecure key from the guest if it's present...
server01: Key inserted! Disconnecting and reconnecting using new SSH key...
==> server01: Machine booted and ready!
==> server01: Setting hostname...
server02:
server02: Vagrant insecure key detected. Vagrant will automatically replace
server02: this with a newly generated keypair for better security.
==> server01: Configuring and enabling network interfaces...
server02:
server02: Inserting generated public key within guest...
server02: Removing insecure key from the guest if it's present...
server02: Key inserted! Disconnecting and reconnecting using new SSH key...
==> server02: Machine booted and ready!
==> server02: Setting hostname...
==> server02: Configuring and enabling network interfaces...
$ vagrant status
Current machine states:
server01 running (libvirt)
server02 running (libvirt)
This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.
5 分程度で構築が完了しました。
構成確認
各マシンのネットワークインターフェースを確認してみます。
- ホストマシン
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
### ホストマシンの仮想 NIC
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
link/ether 42:01:0a:0a:0a:0a brd ff:ff:ff:ff:ff:ff
inet 10.10.10.10/32 metric 100 scope global dynamic ens4
valid_lft 85657sec preferred_lft 85657sec
inet6 fe80::4001:aff:fe0a:a0a/64 scope link
valid_lft forever preferred_lft forever
### libvirt のデフォルトの仮想ブリッジ
3: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 52:54:00:01:8a:56 brd ff:ff:ff:ff:ff:ff
inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
valid_lft forever preferred_lft forever
### 今回作成されたゲスト VM 用の仮想ブリッジ(192.168.121.0/24 の VM 間ローカルネットワーク)
4: virbr1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 52:54:00:4a:fd:84 brd ff:ff:ff:ff:ff:ff
inet 192.168.121.1/24 brd 192.168.121.255 scope global virbr1
valid_lft forever preferred_lft forever
### 今回作成されたゲスト VM 用の仮想ブリッジ(172.15.7.0/24 の VM 間ローカルネットワーク)
5: virbr2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 52:54:00:30:90:11 brd ff:ff:ff:ff:ff:ff
inet 172.15.7.1/24 brd 172.15.7.255 scope global virbr2
valid_lft forever preferred_lft forever
### server01 が virbr1 に接続するための仮想ブリッジ
6: vnet2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr1 state UNKNOWN group default qlen 1000
link/ether fe:54:00:ad:eb:39 brd ff:ff:ff:ff:ff:ff
inet6 fe80::fc54:ff:fead:eb39/64 scope link
valid_lft forever preferred_lft forever
### server01 が virbr2 に接続するための仮想ブリッジ
7: vnet3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr2 state UNKNOWN group default qlen 1000
link/ether fe:54:00:53:ac:f2 brd ff:ff:ff:ff:ff:ff
inet6 fe80::fc54:ff:fe53:acf2/64 scope link
valid_lft forever preferred_lft forever
### server02 が virbr1 に接続するための仮想ブリッジ
8: vnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr1 state UNKNOWN group default qlen 1000
link/ether fe:54:00:72:ff:31 brd ff:ff:ff:ff:ff:ff
inet6 fe80::fc54:ff:fe72:ff31/64 scope link
valid_lft forever preferred_lft forever
### server02 が virbr2 に接続するための仮想ブリッジ
9: vnet1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master virbr2 state UNKNOWN group default qlen 1000
link/ether fe:54:00:8a:e8:35 brd ff:ff:ff:ff:ff:ff
inet6 fe80::fc54:ff:fe8a:e835/64 scope link
valid_lft forever preferred_lft forever
### ブリッジリスト
$ brctl show
bridge name bridge id STP enabled interfaces
virbr0 8000.525400018a56 yes
virbr1 8000.5254004afd84 yes vnet0
vnet2
virbr2 8000.525400309011 yes vnet1
vnet3
### server01 の仮想 NIC
$ virsh domiflist ren510dev_server01
Interface Type Source Model MAC
---------------------------------------------------------------------
vnet2 network vagrant-libvirt virtio 52:54:00:ad:eb:39
vnet3 network ren510dev0 virtio 52:54:00:53:ac:f2
### server02 の仮想 NIC
$ virsh domiflist ren510dev_server02
Interface Type Source Model MAC
---------------------------------------------------------------------
vnet0 network vagrant-libvirt virtio 52:54:00:72:ff:31
vnet1 network ren510dev0 virtio 52:54:00:8a:e8:35
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
ens4 | 42:01:0a:0a:0a:0a | 無し (ホストマシンの仮想 NIC) |
virbr0 | 52:54:00:01:8a:56 | ホストマシンのデフォルトゲートウェイに NAT 接続 (libvirt のデフォルトネットワーク) |
virbr1 | 52:54:00:4a:fd:84 | ホストマシンのデフォルトゲートウェイに NAT 接続 (ゲスト VM 用の仮想ネットワーク) |
virbr2 | 52:54:00:30:90:11 | ホストマシンのデフォルトゲートウェイに NAT 接続 (ゲスト VM 用の仮想ネットワーク) |
vnet2 | fe:54:00:ad:eb:39 | virbr1 |
vnet3 | fe:54:00:53:ac:f2 | virbr2 |
vnet0 | fe:54:00:72:ff:31 | virbr1 |
vnet1 | fe:54:00:8a:e8:35 | virbr2 |
- L2 VM(server01)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:ad:eb:39 brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
inet 192.168.121.46/24 metric 100 brd 192.168.121.255 scope global dynamic eth0
valid_lft 3214sec preferred_lft 3214sec
inet6 fe80::5054:ff:fead:eb39/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:53:ac:f2 brd ff:ff:ff:ff:ff:ff
altname enp0s6
altname ens6
inet 172.15.7.11/24 brd 172.15.7.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe53:acf2/64 scope link
valid_lft forever preferred_lft forever
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
eth0 | 52:54:00:ad:eb:39 | vnet2 |
eth1 | 52:54:00:53:ac:f2 | vnet3 |
- L2 VM(server02)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:72:ff:31 brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
inet 192.168.121.107/24 metric 100 brd 192.168.121.255 scope global dynamic eth0
valid_lft 3192sec preferred_lft 3192sec
inet6 fe80::5054:ff:fe72:ff31/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:8a:e8:35 brd ff:ff:ff:ff:ff:ff
altname enp0s6
altname ens6
inet 172.15.7.12/24 brd 172.15.7.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe8a:e835/64 scope link
valid_lft forever preferred_lft forever
NIC | MAC アドレス | 接続先 |
---|---|---|
lo | 00:00:00:00:00:00 | 無し |
eth0 | 52:54:00:72:ff:31 | vnet0 |
eth1 | 52:54:00:8a:e8:35 | vnet1 |
上の接続関係を図にまとめると以下のようになります。

赤系統のネットワークはホストマシンとゲストマシン間のプライベートネットワークとなっていて、青系統のネットワークは libvirt が管理するデフォルトの仮想ネットワークです。 いずれも L2 VM から外部ネットワークへの通信は L1 VM 内で NAT して送り出されます。
疎通確認
L2 VM の構築は完了しましたが、この時点では L2 VM はホストマシンとの通信が 172.15.7.0/24
プライベートネットワークに閉じた状態で、L2 VM は VPC サブネットには紐付いていません。
クライアントマシン(vagrant-playground-instance-02
)から L2 VM へ ICMP Request を送信すると応答が返ってきますが、これはあくまで L1 VM が返しているだけで、L2 VM が処理しているわけではありません。
### vagrant-playground-instance-02 から L2 VM(10.10.10.110)への ping は返ってくる
$ ping -c 3 10.10.10.110
PING 10.10.10.110 (10.10.10.110) 56(84) bytes of data.
64 bytes from 10.10.10.110: icmp_seq=1 ttl=64 time=2.72 ms
64 bytes from 10.10.10.110: icmp_seq=2 ttl=64 time=1.03 ms
64 bytes from 10.10.10.110: icmp_seq=3 ttl=64 time=1.08 ms
--- 10.10.10.110 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.026/1.606/2.717/0.785 ms
### L2 VM(10.10.10.110)のキャプチャ結果
$ sudo tcpdump -i eth1 icmp and src host 10.10.10.15
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
[何も受け取れていない]
これは Alias IP の仕組みに理由があります。
Alias IP を L1 VM に設定すると GCE Guest Agent が経路情報を自動的に追加します。
Setup or remove IP routes in the guest for IP forwarding and IP aliases
addresses.go
var registryEntries []string
for _, ip := range wantIPs {
// If the IP is not in toAdd, add to registry list and continue.
if !slices.Contains(toAdd, ip) {
registryEntries = append(registryEntries, ip)
continue
}
var err error
if runtime.GOOS == "windows" {
// Don't addAddress if this is already configured.
if !slices.Contains(configuredIPs, ip) {
// In case of forwardedIpv6 we get IPV6 CIDR and Parse IP will return nil.
netip := net.ParseIP(ip)
if netip != nil && !isIPv6(netip) {
// Retains existing behavior for ipv4 addresses.
err = addAddress(netip, net.IPv4Mask(255, 255, 255, 255), uint32(iface.Index))
} else {
err = addIpv6Address(ip, uint32(iface.Index))
}
}
} else {
err = addLocalRoute(ctx, config, ip, iface.Name)
}
if err == nil {
registryEntries = append(registryEntries, ip)
} else {
logger.Errorf("error adding route: %v", err)
}
}
### L1 VM の経路情報を確認
$ ip route get 10.10.10.110
local 10.10.10.110 dev lo src 10.10.10.110 uid 1015
cache <local>
ここで、10.10.10.110
という IP 宛のパケットをループバックインターフェース(lo
)で受け取るようにローカルの経路情報が構成されていることが分かります。
Alias IP に対するルーティングは Google Cloud 内の VPC ネットワークレイヤで自動的に構成されており、L1 VM がこの IP 宛のパケットをカーネルで処理するように設定されているため、ICMP Request に対して応答が返っているわけです。
When alias IP ranges are configured, Google Cloud automatically installs Virtual Private Cloud (VPC) network routes for primary and alias IP ranges for the subnet of the primary network interface.
You do need to perform in-guest configuration as described in Alias IP ranges key properties.
iptables Chain の追加
実際に L2 VM にパケットを転送するために、L1 VM に iptables の Chain Rule を追加します。
Alias IP と L1 VM が持つ経路情報により、L1 VM は同一 VPC サブネットの他のホストから L2 VM へのパケットを受け取ることができます。 これをマスカレードして最終的に L2 VM へ転送します。
- IP Forwarding の有効化
sysctl.d にカスタムルールを追加します。
### /etc/sysctl.d に IP Forwarding の設定を追加
$ echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ip-forward.conf
net.ipv4.ip_forward = 1
### 反映
$ sudo sysctl --system
- iptables Chain Rule を初期化
デフォルトの iptables Chain Rule を一度クリアします。
$ sudo iptables -F
$ sudo iptables -X
$ sudo iptables -t nat -F
$ sudo iptables -t nat -X
$ sudo iptables -t mangle -F
$ sudo iptables -t mangle -X
$ sudo iptables -P INPUT ACCEPT
$ sudo iptables -P FORWARD ACCEPT
$ sudo iptables -P OUTPUT ACCEPT
### フィルタテーブル(INPUT / FORWARD / OUTPUT)の確認
$ sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 46 packets, 4760 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 26 packets, 3352 bytes)
pkts bytes target prot opt in out source destination
### NAT テーブル(PREROUTING)の確認
$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
- フィルタルールの追加
基本的な INPUT / FORWARD / OUTPUT の許可フィルタを追加します。
### INPUT Chain(L2 VM へのパケット受信を許可)
sudo iptables -A INPUT -p icmp -j ACCEPT
sudo iptables -A INPUT -p tcp -j ACCEPT
sudo iptables -A INPUT -p udp -j ACCEPT
### OUTPUT Chain(L2 VM からのパケット送信を許可)
sudo iptables -A OUTPUT -p icmp -j ACCEPT
sudo iptables -A OUTPUT -p tcp -j ACCEPT
sudo iptables -A OUTPUT -p udp -j ACCEPT
### FORWARD Chain(L2 VM へのパケット転送の許可)
sudo iptables -A FORWARD -p icmp -j ACCEPT
sudo iptables -A FORWARD -p tcp -j ACCEPT
sudo iptables -A FORWARD -p udp -j ACCEPT
- NAT ルールの追加
L2 VM に対する PREROUTING / POSTROUTING の NAT フィルタを追加します。
### L2 VM(server01)への IP マスカレード
$ sudo iptables -t nat -A PREROUTING -d 10.10.10.110 -j DNAT --to-destination 172.15.7.11
$ sudo iptables -t nat -A POSTROUTING -s 172.15.7.11 -j MASQUERADE
### L2 VM(server02)への IP マスカレード
$ sudo iptables -t nat -A PREROUTING -d 10.10.10.111 -j DNAT --to-destination 172.15.7.12
$ sudo iptables -t nat -A POSTROUTING -s 172.15.7.12 -j MASQUERADE
- NAT テーブルの確認
送信元 0.0.0.0/0
から受け取った 10.10.10.110/32
宛のパケットを 172.15.7.11
に転送します。
送信元 0.0.0.0/0
から受け取った 10.10.10.111/32
宛のパケットを 172.15.7.12
に転送します。
作成した時点では、pkts
フィールド(処理したパケット数)は 0 ですが、このルールが適用されるとカウンタがインクリメントされます。
$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DNAT all -- * * 0.0.0.0/0 10.10.10.110 to:172.15.7.11
0 0 DNAT all -- * * 0.0.0.0/0 10.10.10.111 to:172.15.7.12
- 再度、疎通確認
### vagrant-playground-instance-02 から L2 VM(10.10.10.110)への ICMP Request
$ ping -c 3 10.10.10.110
PING 10.10.10.110 (10.10.10.110) 56(84) bytes of data.
64 bytes from 10.10.10.110: icmp_seq=1 ttl=63 time=4.58 ms
64 bytes from 10.10.10.110: icmp_seq=2 ttl=63 time=1.22 ms
64 bytes from 10.10.10.110: icmp_seq=3 ttl=63 time=1.29 ms
--- 10.10.10.110 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.221/2.364/4.579/1.566 ms
### L2 VM(10.10.10.110)のキャプチャ結果
$ sudo tcpdump -i eth1 icmp and src host 10.10.10.15
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
16:11:53.739666 IP 10.10.10.15 > 172-15-7-11.lightspeed.stlsmo.sbcglobal.net: ICMP echo request, id 4, seq 1, length 64
16:11:54.739586 IP 10.10.10.15 > 172-15-7-11.lightspeed.stlsmo.sbcglobal.net: ICMP echo request, id 4, seq 2, length 64
16:11:55.741059 IP 10.10.10.15 > 172-15-7-11.lightspeed.stlsmo.sbcglobal.net: ICMP echo request, id 4, seq 3, length 64
vagrant-playground-instance-02
から L2 VM(10.10.10.110
)への ping が成功しており、応答パケットを返送しているのが L2 VM(server01
)であることが分かります。
- 再度、NAT テーブルの確認
$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
1 84 DNAT all -- * * 0.0.0.0/0 10.10.10.110 to:172.15.7.11
0 0 DNAT all -- * * 0.0.0.0/0 10.10.10.111 to:172.15.7.12
iptables の NAT テーブルを確認してみると、L2 VM(10.10.10.110
)への PREROUTING カウンタが増加しており、DNAT でパケットが転送されたことが分かります。
これで L2 VM を VPC サブネットと同一のネットワーク上にぶら下げることができました。

GCE Nested V12n で KVM を利用した場合、最終的にこのような構成になります。
L1 VM の処理
L2 VM に対するパケットを依然として L1 VM が受け取るためパケットキャプチャでは観測されます。
### ### vagrant-playground-instance-02 から L2 VM(10.10.10.110)への ping
$ ping -c 3 10.10.10.110
PING 10.10.10.110 (10.10.10.110) 56(84) bytes of data.
64 bytes from 10.10.10.110: icmp_seq=1 ttl=63 time=2.57 ms
64 bytes from 10.10.10.110: icmp_seq=2 ttl=63 time=1.16 ms
64 bytes from 10.10.10.110: icmp_seq=3 ttl=63 time=1.15 ms
--- 10.10.10.110 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 1.151/1.626/2.567/0.665 ms
### L1 VM のキャプチャ結果
$ sudo tcpdump -i ens4 icmp and src host 10.10.10.15
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens4, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:45:25.703327 IP vagrant-playground-instance-02.c.ren510dev.internal > 10.10.10.110: ICMP echo request, id 10, seq 1, length 64
18:45:26.703923 IP vagrant-playground-instance-02.c.ren510dev.internal > 10.10.10.110: ICMP echo request, id 10, seq 2, length 64
18:45:27.704319 IP vagrant-playground-instance-02.c.ren510dev.internal > 10.10.10.110: ICMP echo request, id 10, seq 3, length 64
これは L1 VM の仮想 NIC(ens4
)に着信したパケットに対して、iptables の PREROUTING Chain を適用する前にキャプチャツール(libcap)が観測するためです。

nftables への移行とルールの永続化
最後に iptables のルールを永続化しておきます。
Linux カーネルは、パケットフィルタリングやマングル処理のフレームワークとして netfilter を利用しています。 従来 netfilter の制御は iptables が主流でしたが、Linux 3.13 以降、後継である nftables が徐々に浸透し始め、2023 年現在では大半の Linux ディストリビューションでデフォルトになっています。
iptables と比較して、nftables はルールの記述が簡潔化されており、パフォーマンスも良いとされています。
$ which iptables
/usr/sbin/iptables
$ iptables --version
iptables v1.8.7 (nf_tables)
今回使用している L1 VM(GCE)も nftables がデフォルトで有効になっているため、こちらを利用して Chain を永続化します。
- nftables をアクティベート
nftables が有効になっていない場合は、以下のコマンドでアクティベートします。
### nftables の有効化
$ sudo systemctl enable nftables
### nftables の起動
$ sudo systemctl start nftables
### 確認
$ sudo systemctl status nftables
● nftables.service - nftables
Loaded: loaded (/lib/systemd/system/nftables.service; enabled; vendor preset: enabled)
Active: active (exited) since Tue 2023-11-04 16:36:22 UTC; 4s ago
Docs: man:nft(8)
http://wiki.nftables.org
Process: 3180 ExecStart=/usr/sbin/nft -f /etc/nftables.conf (code=exited, status=0/SUCCESS)
Main PID: 3180 (code=exited, status=0/SUCCESS)
CPU: 11ms
Nov 04 16:36:22 vagrant-playground-instance-01 systemd[1]: Starting nftables...
Nov 04 16:36:22 vagrant-playground-instance-01 systemd[1]: Finished nftables.
- デフォルトの
nftables.conf
を退避
nftables の設定ファイルは /etc/nftables.conf
にあります。 既存の設定ファイルを安全のため退避させておきます。
$ sudo cp /etc/nftables.conf /etc/nftables.conf.bak
- nftables の設定ファイルを追加
$ sudo vim /etc/nftables.conf
以下の内容を追加します。
#!/usr/sbin/nft -f
flush ruleset
### INPUT / FORWARD / OUTPUT
table inet filter {
chain input {
type filter hook input priority 0;
policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
chain forward {
type filter hook forward priority 0;
policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
chain output {
type filter hook output priority 0;
policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
}
### PREROUTING / POSTROUTING)
table ip nat {
chain prerouting {
type nat hook prerouting priority 0;
ip daddr 10.10.10.110 dnat to 172.15.7.11
ip daddr 10.10.10.111 dnat to 172.15.7.12
}
chain postrouting {
type nat hook postrouting priority 100;
ip saddr 172.15.7.11 masquerade
ip saddr 172.15.7.12 masquerade
}
}
- nftables の設定を適用
以下のコマンドで設定ファイルをシステムに反映します。
$ sudo nft -f /etc/nftables.conf
- nftables の設定を確認
先ほど設定したルールが正しく適用されているか確認します。
$ sudo nft list ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
chain forward {
type filter hook forward priority filter; policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
chain output {
type filter hook output priority filter; policy accept;
ip protocol icmp accept
ip protocol tcp accept
ip protocol udp accept
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority filter; policy accept;
ip daddr 10.10.10.110 dnat to 172.15.7.11
ip daddr 10.10.10.111 dnat to 172.15.7.12
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 172.15.7.11 masquerade
ip saddr 172.15.7.12 masquerade
}
}
この状態になれば先ほど手動で適用した iptables のルールは不要となるので削除して構いません。
これで L1 VM が再起動した場合でも Chain が保存されているため、起動時に nftables を呼び出すことで、常に L2 VM へのパケット転送が可能になります。
まとめ
今回のブログでは Nested V12n を活用してクラウドプロバイダが提供する VM 内で KVM 環境を構築する方法について紹介しました。 物理サーバと比較すると、クラウドサービスでは特有の制約があるため、特にホストマシンがどのようなネットワーク環境下で稼働しているかを理解することが重要です。
大半のクラウドサービスはネットワークを SDN で運用しているため、L3 より下のレイヤで用いられるプロトコルや Linux カーネルの機能が制限されています。 このため、マイグレーション先のクラウドプロバイダがどのように VM を運用しているのかを把握することで、KVM 環境を適切に構築することができます。
今回は気になって検証しただけですが、実際に Nested V12 を利用して VM in VM の運用をする場合は、パフォーマンスにも注意が必要になると思います。