PackerからzerofreeによりOVAイメージのサイズを小さくする方法
日頃仮想マシンやハイパーバイザ周辺を見ていることが多く、 仮想マシンイメージの自動ビルドを整備するような機会も少なくない。
仮想マシンのイメージをビルドする際、そのサイズを小さく抑えることは重要で、様々な恩恵がある。 (これは、コンテナの長所の一つにその軽量さが挙げられることからも想像できると思う。)
少々ニッチなケースではあるが、仮想マシンイメージのサイズを縮める部分で工夫を要する状況に遭遇したので、 その時の対処をメモしておく。 なお、ビルドの実行環境はvSphereを前提にしている。 それ以外の環境でこの手順が必要になるケースがあるかどうかは定かでないが、 vSphereに強く依存した手順ではないため、必要になった場合は応用できると思われる。
予備知識
HashiCorp Packer
仮想マシンイメージやコンテナイメージのビルド手順を自動化する際によく使われるソフトウェア。 ビルド環境として各種IaaSやvSphere等仮想化基盤をサポートしている。
OVF/OVA
仮想マシンの仮想ハードウェアとしての仕様を記述するOVF(Open Virtualization Format)というフォーマットがあり、vSphereでも仮想マシンのインポート・エクスポートに使われている。
実際にインポート・エクスポートを行う際は、 OVFファイルに加えてディスクイメージ(vSphereだとVMDK形式)が使用される。 そのため、これらディスクイメージやそのハッシュ値を収めたファイル等とOVFファイルをまとめて TAR形式でアーカイブし、使い勝手を良くした状態で扱うこともあり、このアーカイブファイルをOVAと呼ぶ。
zerofree
OVF+ディスクイメージをエクスポートする際、vSphereではディスクイメージ中に存在する NULLブロック (0x00
のみのブロック) の記録を省略し
ファイルサイズを小さくするように動く。
(それを示す記述として探した中で一番近いものは
このドキュメントの
“The disk files are stored in a compressed, sparse format.” であり、詳細は記載されていないが、
実際の結果などを見る限りNULLブロックの記録が省かれるようである。)
Thin provisioning で仮想ディスクを利用している場合、仮想マシンがブロックを利用するまで実際にはブロックを確保しないため、 未使用の領域はディスクイメージに含まれることはない。 しかしながら、仮想マシン内でファイル作成後に削除した場合など、一度使ったが解放したブロックというものが通常は存在する。 そして、それらは通常再利用され新しいデータで上書きされることになるため、ファイルシステムはわざわざコストをかけてゼロ埋めしない。 結果として、すでに削除したデータが残っているだけの不要なブロックが、エクスポート後のディスクイメージに含まれてしまう。
そのため、仮想マシンのディスクイメージをエクスポートする前に、データを保持していないブロックをゼロ埋めしておくことで、 エクスポート後のサイズを縮めることが可能になる。これを実現するツールが zerofreeコマンドになる。
ただし、利用中のブロックをどう管理しているかはファイルシステムに依存しており、 zerofreeはext2, ext3, ext4しかサポートしていないようなので、その点に注意が必要である。 また、対象のファイルシステムがread-onlyでマウントされていないと利用できないという制限もある。
ここまで、仮想マシンのエクスポートに際しては事前にzerofreeを実行するとイメージのサイズを縮められるという話をしたが、 実際にはもっとスマートな方法が存在する。
fstrim
HDDのような記録媒体であれば、前述のようにわざわざブロックをゼロ埋めする必要はなく、そういった処理は通常されない。 しかし、HDDとは記憶の仕組みが異なるSSDにおいては、データを上書きするような場合に、物理的には記憶素子に対し一度初期化を 行ってからデータを書き込むような手順が必要であるらしい。 (それも、ある程度まとまった単位で初期化する必要があり、変更がない部分も書き戻す必要が生じて相応のコストがかかるらしい。) そのせいで、長期間SSDを利用した場合に書き込み速度が低下するようなことが起こりうる。
それに対し、不要になったデータを保持している素子について、予め非同期的に初期化しておけば、 その箇所を再利用する際の初期化の手間を省き、書き込み速度の低下を軽減できると考えられる。 これを実現するために、SSDのような記録媒体に対し、データが不要となったことを通知するための方法が存在する。 ATAのデバイスにおいてはTRIM, SCSIデバイスにおいてはUNMAPと呼ばれる命令がそれである。 (余談: コンシューマにSSDが流行りだした当時、自作PC関連の情報をよく追っていたが、 TRIMの対応に関しては自作PC界隈でもよく取り上げられた話題だったと記憶している。)
このコマンドはthin provisionedな仮想ディスクについても有用で、TRIM/UNMAPをゲストOSが発行すれば、 不要なブロックをハイパーバイザが知ることが可能となる。 したがって、エクスポート時にNULLブロックが省略されることに期待せずとも、不要なブロックは最初からエクスポートされないようにできる。
ただし動作には一定の条件があり、 ドキュメント にあるようにvSphere環境については
- VMFS6の一部の例外を除いた通常のケースと、VMFS5で一定の条件を満たしたケースにおいて、ゲストOS上で発行されたTRIM/UNMAPが自動的にバックエンドの物理ストレージデバイスにまで渡される
- VMFS6の一部の例外ケースと、VMFS5の通常のケースにおいて、ESXiで
esxcli storage vmfs unmap
を実行することでバックエンドの物理ストレージデバイスにUNMAPが発行される
という挙動になる。また、NFSのデータストアでは対応していない。
OS側の対応としては、10年以上前から存在するコマンドではあるので、大抵はサポートされているようだが、
ファイル削除時などに自動でTRIM/UNMAPを発行するのかは設定による部分もあり、
例えばext4ファイルシステムの場合マウントオプション discard
の指定が必要になる。
自動でTRIM/UNMAPが発行されなくても、手動で発行する方法もあり、例えばfstrimコマンドがその一例となる。
つまるところ、VMFS5以降を利用していれば、zerofreeを使わずともfstrimでも不要なブロックのエクスポートの抑止は実現できる。 (条件によってはfstrimの実行すら不要である。)
問題点
エクスポート後のイメージのサイズを削減するためには、不要なファイルを削除した上で、通常はfstrimを利用すれば良い。 しかしながら、NFSデータストアを利用している場合など、zerofreeに頼る他ないケースも存在する。
しかし、zerofreeを実行するには対象のファイルシステムがread-onlyである必要があるため、 動作中のシステムでroot filesystemに対して実行することは難しい。 通常は、single user modeでログインして操作するような手順が一般的であると思う。
packerを使った自動ビルドをしている場合、エクスポート前にsingle user modeでの操作を差し挟むのは非常に困難になる。
( vmware-iso
builderなどはvSphereの機能としてのコンソール接続を利用できるため、不可能ということはないが、
大抵はSSH接続可能になる段階からはSSH接続を経由してプロビジョニングを実施するようにしてしまい、それが終わってから
single user modeで操作することはおそらくできないと思われる。)
以下では、この問題を回避してpackerのprovisionerの中でzerofreeを実行する方法を紹介する。
解決方法
概要
- 起動最初期にはroot filesystemはread-onlyマウントされている
- これはLinuxのブートシーケンスについて調べると分かるが、起動時にはread-onlyでマウントされ、すぐにrwで再マウントされる
- その僅かなタイミングにzerofreeを流せるようsystemdのserviceを仕立てる
systemd-remount-fs.service
というサービスが再マウントをしているので、その前に実行できれば良い
手順
packerとしては、以下のような流れをshell provisionerなどを使って実現する。
-
(仮想マシンイメージのプロビジョニング処理を一通り済ませる)
-
zerofreeをインストールする
-
キャッシュ等のイメージに不要なファイルを削除する
-
以下のようなunit fileを書いてserviceを用意し(
zerofree-root.service
とする)、systemctl daemon-reload
してsystemctl enable zerofree-root.service
で有効化[Unit] DefaultDependencies=no Before=systemd-remount-fs.service [Service] Type=oneshot ExecStart=/bin/sh -c 'zerofree $(grep "^/" /etc/fstab | awk "\\$2 == \\"/\\" { print \\$1 }")' RemainAfrterExit=yes TimeoutSec=infinity [Install] WantedBy=local-fs-pre.target
-
reboot
- 再起動するので、shell provisionerの
expect_disconnect
skip_clean
は trueにしておくなどの工夫は必要 - この起動直後のタイミングで、先程のサービスが動きだし、zerofreeしてくれる
- 再起動するので、shell provisionerの
-
起動後、
systemctl disable zerofree-root.service
で無効化しておく- 再起動を待つためにshell provisionerの
pause_before
などをうまく使うと良い
- 再起動を待つためにshell provisionerの
まとめ
- packerとかでOVAイメージを作っているようなときに、そのサイズを縮める方法
- fstrimを使う (VMFS5以上を使っているとき)
- 工夫してzerofreeを使う (NFSデータストアなどの場合)
- root filesystemをpackerからzerofree実行するには
systemd-remount-fs.service
より前にzerofreeを実行するserviceを書いて実行させよう