投稿日:
更新日:

GCE Nested V12n で VT-x on KVM を利用する

Authors

目次

banner.png

はじめに

サーバ仮想化技術は、物理サーバのコンピュートリソースを最大限活用するために、従来から利用されてきました。 サーバ仮想化を実現する代表的なハイパバイザに 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

gce-nested-virtualization.png

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 Cloud が保有する L0 の物理マシンには、Intel VT-x が仮想化支援機能として搭載されており、GCE の Nested V12n 機能を有効化することで L1 VM から利用できるようになります。 なお、現時点では L0 のチップは AMD-v には対応していないようです。

GCE で Nested V12n を利用するには、VM インスタンスを作成する際に --enable-nested-virtualization フラグを渡します。

パフォーマンスに関する考慮事項

ハードウェア支援による Nested V12n の場合でも、ネストされた L2 VM では、

  1. CPU にバインドされたワークロードのパフォーマンスが 10% 以上低下する可能性がある
  2. 入出力にバインドされたワークロードでは 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 を介して通信します。

kvm-default-network-architecture.png

この場合、ホストマシンが属する 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)に割り当て直すことができます。参考

libvirt で起動した仮想マシンは、TAP として macvtap を生成することでゲストマシンが独自の MAC アドレスと IP アドレスを持ち、L2 レベルでホストマシンと同一 LAN に参加できるようになります。

ホストマシン

NICMAC アドレス接続先
lo00:00:00:00:00:00無し
enp3s0a8:a1:59:85:11:4a無し
(ホストマシンの物理 NIC)
virbr052:54:00:48:72:53ホストマシンのデフォルトゲートウェイに NAT 接続
(libvirt のデフォルトネットワーク)
virbr152:54:00:40:cd:ebホストマシンのデフォルトゲートウェイに NAT 接続
(ゲスト VM 用の仮想ネットワーク)
macvlan0f6:a4:58:20:4e:08デフォルトゲートウェイ
enp3s0 をブリッジ)
macvtap052:54:00:13:c7:c0macvlan0
vnet0fe:54:00:29:6c:6bvirbr1
macvtap152:54:00:cb:66:17macvlan0
vnet1fe:54:00:ac:a3:37virbr1

ゲストマシン

  • ゲストマシン 1
NICMAC アドレス接続先
lo00:00:00:00:00:00無し
eth052:54:00:29:6c:6bvnet0
eth152:54:00:13:c7:c0macvtap0
macvlan0 の TAP)
  • ゲストマシン 2
NICMAC アドレス接続先
lo00:00:00:00:00:00無し
eth052:54:00:ac:a3:37vnet1
eth152:54:00:cb:66:17macvtap1
macvlan0 の TAP)

ネットワーク構成

上の接続関係を図にまとめると以下のようになります。

kvm-on-premises-network.png
参考

赤系統のネットワークは NetworkManager が管理していて、青系統のネットワークは libvirt が管理するデフォルトの仮想ネットワークです。

スクリプトで作成される macvlan0 はホストマシンの物理 NIC(enp3s0)をブリッジする形で LAN 内の他のマシンと同一のネットワークに接続されます。

また、ゲストマシンは TAP デバイス(macvtap0 / macvtap1)を通じてホストマシンの macvlan0 に接続し、最終的に enp3s0 をブリッジすることで、独立した個別のマシンとして LAN に直接参加することができます。

疎通確認

  • ICMP
  • マッピング情報

では、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 の eth010.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)するという仕組みで管理します。

まとめると、

  1. L2 VM にはローカル IP(例:172.15.7.0/24)を設定
  2. L1 VM に Alias IP を追加
  3. L1 VM の netfilter(iptables) で DNAT して L2 VM へパケットを転送(後方通信の場合は SNAT)

という流れになります。

ip-alias-fowarding.png

つまり、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
vpc-config-01.png

L1 VM の構築

サーバ

次に VPC 上に Nested V12n が有効化された L1 VM、つまり GCE インスタンスそのものを構築します。

IP アドレスは次のように割り当てることにします。

  • ホスト名:vagrant-playground-instance-01
割り当てIP アドレス概要
Primary IP10.10.10.10/32GCE 本体(L1 VM)の IP アドレス
Alias IP10.10.10.110/32L2 VM 1(server01)の IP アドレス
Alias IP10.10.10.111/32L2 VM 2(server02)の IP アドレス
Terraform
gce-config-01.png

Nested V12n が有効化されているかを確認します。

L1 VM の Alias IP が正しく設定されているかを確認します。

以下のコマンドで実際に L1 VM がハードウェア仮想化支援機能をサポートしているかを確認します。 Google Cloud の L0 VM は Intel VT-x のみのサポートとなるため、CPU フラグの vmx を確認します。

コマンド出力から、L1 VM の論理 CPU 64 コア分に VT-x が搭載されており、カーネルがそれを認識していることが分かります。

また、KVM を利用するためのカーネルモジュールは以下のコマンドで確認できます。

クライアント

上で構築した、L2 VM に対するネットワーク疎通を検証するために、VPC サブネット内に別の GCE を配置しておきます。

  • ホスト名:vagrant-playground-instance-02
割り当てIP アドレス概要
Primary IP10.10.10.15/32GCE 本体(L1 VM)の IP アドレス
Terraform
gce-config-02.png

スペックの確認

L1 VM のマシンスペックを確認してみます。

今回使用している n2-standard-32 タイプは 16 コア 32 スレッド 128GB RAM なので、問題無く起動していることが分かります。

ストレージには 500 GB の pd-balanced タイプを使用しています。

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.png

Vagrant はハイパバイザ(KVM)で仮想環境を構築するためのツールで、Vagrantfile による IaC でゲストマシンを管理できます。 Vagrant はそれ単体で動作するわけではなく、libvirt API 等の抽象化レイヤを挟み、最終的に QEMU を呼び出して仮想マシンを制御します。

kvm-architecture.png

クライアントが Vagrant、バックエンドが libvirt、インフラが QEMU といったイメージです。

Vagrant の開発元は HashiCorp ですが、Terraform のようにクラウドプロバイダを抽象化しているわけではなく、あくまでローカルの仮想環境を構築するためのツールとなっています。

  1. L2 VM の構築に必要なパッケージをインストール
  1. libvirt グループに一般ユーザを追加

インストール時点では libvirt がルート権限でシステムサービスとして動作しており、仮想マシンの起動・停止、NIC の管理等を担っています。 そのため、一般ユーザが libvirt を操作するためには libvirt グループに追加してあげる必要があります。

  1. プラグインのインストール

vagrant-libvirt プラグインを使用することで、Vagrant から libvirt を呼び出せるようになります。

  1. 仮想マシンの起動

以下の Vagrantfile から L2 VM を起動します。

Vagrantfile

今回はゲストマシンに Ubuntu 22.04 を使用します。

また、L2 VM と L1 VM 間のプライベートネットワークは 172.15.7.0/24 としています。

L2 VMIP アドレス
server01172.15.7.11
server02172.15.7.12

5 分程度で構築が完了しました。

構成確認

各マシンのネットワークインターフェースを確認してみます。

  • ホストマシン
NICMAC アドレス接続先
lo00:00:00:00:00:00無し
ens442:01:0a:0a:0a:0a無し
(ホストマシンの仮想 NIC)
virbr052:54:00:01:8a:56ホストマシンのデフォルトゲートウェイに NAT 接続
(libvirt のデフォルトネットワーク)
virbr152:54:00:4a:fd:84ホストマシンのデフォルトゲートウェイに NAT 接続
(ゲスト VM 用の仮想ネットワーク)
virbr252:54:00:30:90:11ホストマシンのデフォルトゲートウェイに NAT 接続
(ゲスト VM 用の仮想ネットワーク)
vnet2fe:54:00:ad:eb:39virbr1
vnet3fe:54:00:53:ac:f2virbr2
vnet0fe:54:00:72:ff:31virbr1
vnet1fe:54:00:8a:e8:35virbr2
  • L2 VM(server01)
NICMAC アドレス接続先
lo00:00:00:00:00:00無し
eth052:54:00:ad:eb:39vnet2
eth152:54:00:53:ac:f2vnet3
  • L2 VM(server02)
NICMAC アドレス接続先
lo00:00:00:00:00:00無し
eth052:54:00:72:ff:31vnet0
eth152:54:00:8a:e8:35vnet1

上の接続関係を図にまとめると以下のようになります。

kvm-vpc-network-phase-01.png

赤系統のネットワークはホストマシンとゲストマシン間のプライベートネットワークとなっていて、青系統のネットワークは 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 が処理しているわけではありません。

これは 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

ここで、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 へ転送します。

  1. IP Forwarding の有効化

sysctl.d にカスタムルールを追加します。

  1. iptables Chain Rule を初期化

デフォルトの iptables Chain Rule を一度クリアします。

  1. フィルタルールの追加

基本的な INPUT / FORWARD / OUTPUT の許可フィルタを追加します。

  1. NAT ルールの追加

L2 VM に対する PREROUTING / POSTROUTING の NAT フィルタを追加します。

  1. 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 ですが、このルールが適用されるとカウンタがインクリメントされます。

  1. 再度、疎通確認

vagrant-playground-instance-02 から L2 VM(10.10.10.110)への ping が成功しており、応答パケットを返送しているのが L2 VM(server01)であることが分かります。

  1. 再度、NAT テーブルの確認

iptables の NAT テーブルを確認してみると、L2 VM(10.10.10.110)への PREROUTING カウンタが増加しており、DNAT でパケットが転送されたことが分かります。

これで L2 VM を VPC サブネットと同一のネットワーク上にぶら下げることができました。

kvm-vpc-network-phase-02.png

GCE Nested V12n で KVM を利用した場合、最終的にこのような構成になります。

L1 VM の処理

L2 VM に対するパケットを依然として L1 VM が受け取るためパケットキャプチャでは観測されます。

これは L1 VM の仮想 NIC(ens4)に着信したパケットに対して、iptables の PREROUTING Chain を適用する前にキャプチャツール(libcap)が観測するためです。

packet-flow-within-l1-vm.png

nftables への移行とルールの永続化

最後に iptables のルールを永続化しておきます。

Linux カーネルは、パケットフィルタリングやマングル処理のフレームワークとして netfilter を利用しています。 従来 netfilter の制御は iptables が主流でしたが、Linux 3.13 以降、後継である nftables が徐々に浸透し始め、2023 年現在では大半の Linux ディストリビューションでデフォルトになっています。

iptables と比較して、nftables はルールの記述が簡潔化されており、パフォーマンスも良いとされています。

今回使用している L1 VM(GCE)も nftables がデフォルトで有効になっているため、こちらを利用して Chain を永続化します。

  1. nftables をアクティベート

nftables が有効になっていない場合は、以下のコマンドでアクティベートします。

  1. デフォルトの nftables.conf を退避

nftables の設定ファイルは /etc/nftables.conf にあります。 既存の設定ファイルを安全のため退避させておきます。

  1. nftables の設定ファイルを追加

以下の内容を追加します。

  1. nftables の設定を適用

以下のコマンドで設定ファイルをシステムに反映します。

  1. nftables の設定を確認

先ほど設定したルールが正しく適用されているか確認します。

この状態になれば先ほど手動で適用した iptables のルールは不要となるので削除して構いません。

これで L1 VM が再起動した場合でも Chain が保存されているため、起動時に nftables を呼び出すことで、常に L2 VM へのパケット転送が可能になります。

まとめ

今回のブログでは Nested V12n を活用してクラウドプロバイダが提供する VM 内で KVM 環境を構築する方法について紹介しました。 物理サーバと比較すると、クラウドサービスでは特有の制約があるため、特にホストマシンがどのようなネットワーク環境下で稼働しているかを理解することが重要です。

大半のクラウドサービスはネットワークを SDN で運用しているため、L3 より下のレイヤで用いられるプロトコルや Linux カーネルの機能が制限されています。 このため、マイグレーション先のクラウドプロバイダがどのように VM を運用しているのかを把握することで、KVM 環境を適切に構築することができます。

今回は気になって検証しただけですが、実際に Nested V12 を利用して VM in VM の運用をする場合は、パフォーマンスにも注意が必要になると思います。

参考・引用