お知らせ フロントエンド バックエンド インフラ 品質保証 セキュリティ 製品 興味・関心 その他

2023.02.13

インフラ

FreeBSDのカスタムディスクイメージ作成

OSのバージョンアップをする際、皆さんはどのようにしているでしょうか? 私は上書きインストールやそのまま上書き更新が嫌いで、メジャーアップデートの際は必ずディスクフォーマットを行って、クリーンインストールします。どんどんゴミが溜まっていきますし。

普段使用しているPCもそんな感じでしょっちゅうクリーンインストールするため、OSの設定はほとんどデフォルト、ツール類もなるべくインストールせず、OSに最初から入っているものをなるべく使う、デフォルト主義を貫いています。

このため、普段使いのPCでも2時間もあればディスクフォーマット、OSクリーンインストール、必要最小限のツールのインストールと設定が完了します。2時間というのもほとんどOSのインストール時間とデータコピーの時間なので、手作業の時間は合計で10分もないと思います。

そんなクリーンインストール好きな私ですので、サーバについてもOSのメジャーアップデートの際はクリーンインストールを必ず行っています。サーバに何もアプリケーションをインストールせず、設定も行わないというのは現実的ではないため、それなりに手間がかかります。その作業を省力化/自動化する方法をご紹介します。

今ならDockerという便利なものがありますので同様のことを簡単に実現できますが、これから紹介する方法はDockerがまだ存在しない頃に開発した手法です。以前のマーケライズのシステムはVPSサーバや仮想ホスト型のクラウドサーバ上に構築していましたので、この方法でOSなどの更新を行っていました。今のシステムは仮想ホスト型のサービスを使用していないためこの手法は本番環境では使用していませんが、開発環境のディスクイメージの作成に今でも活躍しています。

手順の概要

FreeBSDのカスタムディスクイメージの作成は、以下の手順で行います。自動化の前に、手動で行う場合の手順として説明します。

  • ディスクイメージ作成環境(以下、「ホスト」と呼びます)の用意
    • ターゲットとするバージョンと同じOSバージョンの環境を用意します
      • ABIが同一であれば厳密に同じバージョンでなくても良いとは思いますが、私は念のため同じバージョンを用意しています
    • ビルドに十分な空きディスク容量を用意します(1つのイメージ作成につき、40GB以上)
  • FreeBSD構成ファイルを取得、展開
    • OS配布FTPサイトから目的のバージョンのbase.txzとkernel.txzを取得
    • ホストにひとつ作業ディレクトリ(ここでは./fsとします)を作成し、上記の二つのファイルの中身を展開
# xz -d < base.txz | tar xf - -C ./fs
# xz -d < kernel.txz | tar xf - -C ./fs
  • OSのマイナーバージョンアップを適用します
    • 展開したファイルに対して適用するため、作業ディレクトリにchrootしてからfreebsd-updateを実行します
      • DNSを引く必要があるため、一時的にホストの/etc/resolv.confをコピーします
      • デバイスファイル(/dev/*)が必要なので、devfsをマウントします
# cp /etc/resolv.conf fs/etc/resolv.conf
# mount -t devfs devfs fs/dev
# chroot fs
# freebsd-update fetch
# freebsd-update install
# exit (chrootを抜ける)
  • portsやpackageで必要なアプリケーションをインストールします
    • 今回はPHPのアプリケーションサーバを想定してApacheとPHP8.2をインストールします
    • PHP Extensionの依存関係で自動でcurlがインストールされますが、今回はcurlのコンパイルオプションをカスタマイズします
    • カスタマイズするcurlはportsでコンパイル/インストールを行い、それ以外はビルド時間の時短のためpackagesでインストールします
# vi fs/etc/make.conf (以下の内容を記述)
.if ${.CURDIR} == "/usr/ports/ftp/curl"
OPTIONS_SET=            CARES
OPTIONS_UNSET=          THREADED_RESOLVER
.endif

# chroot fs
# portsnap fetch
# portsnap extract
# make -C /usr/ports/ftp/curl BATCH=yes install
# pkg install apache24 mod_php82 php82 php82-extensions
# exit (chrootを抜ける)
  • 設定ファイル等を作成、修正をします
    • OSの設定ファイル(/etc/fstab, /etc/rc.confなど)を作成
    • OSの設定ファイル(/etc/master.passwd, /etc/groupなど)を修正
    • ApacheやPHPの設定ファイルを作成/修正

最後の設定ファイル等の作成、修正が作業省力化のミソとなりますので、次のセクションで改めて説明します。

設定ファイルの作成、修正

ディスクイメージを作成するたびに毎回設定ファイルを作成したり修正したりするのは手間ですので、省力化します。

  • 新規作成のファイルは、単純にファイルを用意しておいてコピーします
  • 修正が必要なファイルは、修正内容をdiffの形で残しておいてpatchコマンドで適用します
    • /etc/master.passwdを修正してユーザの追加やパスワードの設定を行った場合は、pwd_mkdbの実行が必要です

ファイルの修正をパッチ形式で行うのは、OSのバージョンアップでオリジナルのファイルに変更が入った場合にその変更を修正後のファイルにも残すためです。ファイルを用意しておいて単純にコピーする方法だと、OSバージョンアップによる変更が上書きされて消されてしまいます。

パッチ当てに失敗してしまう場合も発生しますが、その際にはパッチが当たるようにdiffファイルをメンテナンスしていきます。

ディスクイメージ作成

ここまででOSの構成ファイルを用意できましたので、いよいよディスクイメージを作成します。私は古い人間ですので、昔の流儀でパーティションは細かく分けるスタイルでやっています。以下のようなパーティション分けをします。

ボリューム名サイズマウントポイント
rootfs1GB/
2GB(swap)
varfs2GB/var
usrfs1152MB/usr
localfs2G/usr/local
portsfs1536MB/usr/ports
homefs9472MB/home
tmpfs1G/tmp

さくらのクラウドの20GBディスクに収まるように、パーティションサイズの合計+ブートコードのサイズが20GB未満となるようにしています。

各パーティションサイズの空のファイルを作成、それをmdconfigコマンドでファイルベースのメモリディスクとしてマウントします。

(rootfsの例)
# truncate -s 1G root.img
# mdconfig -a -t vnode -f root.img
# newfs -Uj -L rootfs /dev/md0

上の作業を全てのパーティションについて繰り返します(/dev/md0 – /dev/md6まで作成されます)。作成できたメモリディスクをマウントします。

# mkdir img
# mount /dev/md0 img
# mkdir img/{home,var,usr,tmp}
# mount /dev/md1 img/var
# mount /dev/md2 img/usr
# mkdir img/usr/{local,ports}
# mount /dev/md3 img/usr/local
# mount /dev/md4 img/usr/ports
# mount /dev/md5 img/home
# mount /dev/md6 img/tmp

imgの下に各パーティションがマウントされましたので、ファイルをコピーします。「階層コピーはcp -rを使ってはいけない」と厳しく教育された古い人間ですので、コピーはtarを使用します。

# ( cd fs; tar cf - . ) | ( cd img; tar xpf - )

各パーティションをアンマウントし、メモリディスクを解除します。

# umount img/tmp
# mdconfig -d -u 6
(上の操作を全てのパーティションに対して繰り返し)

root.imgのようなファイルがパーティションの数の7個できましたので、これを一つにまとめ、ディスクイメージとします。今回はMBRブートで、ディスクイメージフォーマットはrawとします。ここでは紹介しませんが、EFIブートのディスクイメージの作成も可能です。

# mkimg -f raw -s gpt -b /boot/pmbr \
                -p freebsd-boot/bootfs:=/boot/gptboot \
                -p freebsd-ufs/rootfs:=root.img \
                -p freebsd-swap/swap::2G \
                -p freebsd-ufs/varfs:=var.img \
                -p freebsd-ufs/usrfs:=usr.img \
                -p freebsd-ufs/localfs:=local.img \
                -p freebsd-ufs/portsfs:=ports.img \
                -p freebsd-ufs/homefs:=home.img \
                -p freebsd-ufs/tmpfs:=tmp.img -o image.raw

この結果、image.rawというファイルが作成されますので、クラウドにアップロードし、クラウドサーバのディスクとしてそのまま利用します。

-f rawとしている部分を-f vmdkとすればVMware用のディスクイメージが作成できますので、これをそのままVMware仮想マシンのディスクとして利用することもできます。

自動化

ここまで、コマンドを手動で打ってディスクイメージを作成する手順を説明してきました。毎回手動でやるのは面倒なので、全自動化します。

今回はMakefileを作成します。make (BSD make)を採用したのは、複数種類のサーバディスクイメージの構築処理のうち、共通化できる部分をまとめてしまいたいということと、OS配布ファイル(base.txz, kernel.txz)をネットワークから取得してローカルに保存し、複数種類のディスクイメージ作成で使いまわしたい(makeのファイル存在チェック機能で実現)という理由です。

これから紹介するファイルを用意すれば、

# make fs

でfsディレクトリの下にファイル群が自動で作成され、さらに

# make image FORMAT=raw

を実行すればrawフォーマットでディスクイメージが作成されます。

まず用途によらないMakefileの共通部分(Makefile.common)です。

FREEBSD_VERSION?=       13.1
DISTSITE?=              ftp2.jp.freebsd.org
FORMAT?=                raw

DISTDIR=                dist
DISTFILES=              base.txz kernel.txz
FSDIR=                  fs
IMGDIR=                 img

clean:
        @echo "Cleaning working directory..."
        if [ -d ${FSDIR} ]; then \
                /bin/chflags -R noschg ${FSDIR}; \
                /bin/rm -rf ${FSDIR}; \
        fi
        /bin/rm -f ${NAME}.vmdk ${NAME}.raw

.for _file in ${DISTFILES}
fetch: ${DISTDIR}/${_file}
${DISTDIR}/${_file}:
        if [ ! -d ${DISTDIR} ]; then \
                /bin/mkdir ${DISTDIR}; \
        fi
        /usr/bin/fetch ftp://${DISTSITE}/pub/FreeBSD/releases/amd64/amd64/${FREEBSD_VERSION}-RELEASE/${_file} -o ${DISTDIR}/${_file}
.endfor

cleancache:
        @echo "Cleaning package cache files..."
        /bin/rm -rf ${FSDIR}/var/cache/pkg
        /bin/rm -rf ${FSDIR}/home/ports/distfiles

extract: clean
        /bin/mkdir ${FSDIR}
        @echo "Extracting distribution packages..."
.for _file in ${DISTFILES}
        /usr/bin/xz -d < ${DISTDIR}/${_file} | /usr/bin/tar xf - -C ${FSDIR}
.endfor

freebsd-update:
        @echo "Applying OS security patches..."
        /sbin/mount -t devfs devfs ${FSDIR}/dev
        /bin/cp /etc/resolv.conf ${FSDIR}/etc/resolv.conf
        /usr/sbin/chroot ${FSDIR} /usr/sbin/freebsd-update fetch
        /usr/sbin/chroot ${FSDIR} /usr/sbin/freebsd-update install || /usr/bin/true
        /bin/rm -f ${FSDIR}/etc/resolv.conf
        /sbin/umount ${FSDIR}/dev

次に、Apache + PHP8.2サーバ用のMakefileです。

curlの依存関係でインストールされるcmake等はコンパイルに時間がかかるため、portsでcurlをコンパイルする前に、cmake等をpackagesでインストールして時短しています。

NAME=           example
IMAGENAME=      Example Server (FreeBSD 13.1 + PHP 8.2)

# DiskSize
ROOTSIZE=       1G
SWAPSIZE=       2G
VARSIZE=        2G
USRSIZE=        1152M
LOCALSIZE=      2G
PORTSSIZE=      1536M
HOMESIZE=       9472M
TMPSIZE=        1G

PACKAGES=       apache24 mod_php82 php82 php82-extensions

all: fs

.include "Makefile.common"

fs: fetch extract freebsd-update ports customize cleancache

ports:
        @echo "Installing ports..."
        /sbin/mount -t devfs devfs ${FSDIR}/dev
        /bin/cp /etc/resolv.conf ${FSDIR}/etc/resolv.conf
        /bin/cp files/make.conf ${FSDIR}/etc/make.conf
        /usr/sbin/chroot ${FSDIR} /usr/bin/env ASSUME_ALWAYS_YES=yes /usr/sbin/pkg install pkg cmake perl5 python39
        /usr/sbin/chroot ${FSDIR} /usr/sbin/portsnap fetch
        /usr/sbin/chroot ${FSDIR} /usr/sbin/portsnap extract
        /usr/sbin/chroot ${FSDIR} /usr/bin/make -C /usr/ports/ftp/curl BATCH=yes install
        /usr/sbin/chroot ${FSDIR} /usr/bin/env ASSUME_ALWAYS_YES=yes /usr/sbin/pkg install ${PACKAGES}
        /usr/sbin/chroot ${FSDIR} /usr/bin/env ASSUME_ALWAYS_YES=yes /usr/sbin/pkg autoremove
        /bin/rm -rf ${FSDIR}/home/ports/usr
        /bin/rm -f ${FSDIR}/etc/resolv.conf
        /sbin/umount ${FSDIR}/dev

customize: files/fs.diff
        @echo "Customizing..."
        /sbin/mount -t devfs devfs ${FSDIR}/dev
        /usr/bin/patch -d ${FSDIR} -p 1 -s < files/fs.diff
        /bin/cp files/fstab ${FSDIR}/etc/
        /bin/cp files/rc.conf ${FSDIR}/etc/
        /bin/cp files/main.conf ${FSDIR}/usr/local/etc/apache24/Includes/
        /bin/ln -s php.ini-production ${FSDIR}/usr/local/etc/php.ini
        /usr/bin/bzcat files/home.tar.bz2 | ( cd ${FSDIR}; /usr/bin/tar xpf - )
        /bin/ln -s /usr/share/zoneinfo/Asia/Tokyo ${FSDIR}/etc/localtime
        /bin/rm -f ${FSDIR}/etc/master.passwd.orig
        /usr/sbin/chroot ${FSDIR} /usr/sbin/pwd_mkdb -p /etc/master.passwd
        /sbin/umount ${FSDIR}/dev

image:
        @echo "Creating disk image..."
        /bin/rm -f root.img var.img usr.img local.img home.img tmp.img ${NAME}.raw
        /usr/bin/truncate -s ${ROOTSIZE} root.img
        /usr/bin/truncate -s ${VARSIZE} var.img
        /usr/bin/truncate -s ${USRSIZE} usr.img
        /usr/bin/truncate -s ${LOCALSIZE} local.img
        /usr/bin/truncate -s ${PORTSSIZE} ports.img
        /usr/bin/truncate -s ${HOMESIZE} home.img
        /usr/bin/truncate -s ${TMPSIZE} tmp.img
        /sbin/mdconfig -a -t vnode -f root.img
        /sbin/mdconfig -a -t vnode -f var.img
        /sbin/mdconfig -a -t vnode -f usr.img
        /sbin/mdconfig -a -t vnode -f local.img
        /sbin/mdconfig -a -t vnode -f ports.img
        /sbin/mdconfig -a -t vnode -f home.img
        /sbin/mdconfig -a -t vnode -f tmp.img
        /sbin/newfs -Uj -L rootfs /dev/md0
        /sbin/newfs -Uj -L varfs /dev/md1
        /sbin/newfs -Uj -L usrfs /dev/md2
        /sbin/newfs -Uj -L localfs /dev/md3
        /sbin/newfs -Uj -L portsfs /dev/md4
        /sbin/newfs -Uj -L homefs /dev/md5
        /sbin/newfs -Uj -L tmpfs /dev/md6
        if [ ! -d ${IMGDIR} ]; then \
                /bin/mkdir ${IMGDIR}; \
        fi
        /sbin/mount /dev/md0 ${IMGDIR}
        /bin/mkdir ${IMGDIR}/home
        /bin/mkdir ${IMGDIR}/var
        /bin/mkdir ${IMGDIR}/usr
        /bin/mkdir ${IMGDIR}/tmp
        /sbin/mount /dev/md1 ${IMGDIR}/var
        /sbin/mount /dev/md2 ${IMGDIR}/usr
        /bin/mkdir ${IMGDIR}/usr/local
        /bin/mkdir ${IMGDIR}/usr/ports
        /sbin/mount /dev/md3 ${IMGDIR}/usr/local
        /sbin/mount /dev/md4 ${IMGDIR}/usr/ports
        /sbin/mount /dev/md5 ${IMGDIR}/home
        /sbin/mount /dev/md6 ${IMGDIR}/tmp
        ( cd ${FSDIR} ; /usr/bin/tar cf - . ) | ( cd ${IMGDIR}; /usr/bin/tar xpf - )
        /sbin/umount ${IMGDIR}/tmp
        /sbin/umount ${IMGDIR}/home
        /sbin/umount ${IMGDIR}/usr/ports
        /sbin/umount ${IMGDIR}/usr/local
        /sbin/umount ${IMGDIR}/usr
        /sbin/umount ${IMGDIR}/var
        /sbin/umount ${IMGDIR}
.for _i in 0 1 2 3 4 5 6
        /sbin/mdconfig -d -u ${_i}
.endfor
        /usr/bin/env TMPDIR=/var/tmp mkimg -f ${FORMAT} -s gpt -b /boot/pmbr \
                -p freebsd-boot/bootfs:=/boot/gptboot \
                -p freebsd-ufs/rootfs:=root.img \
                -p freebsd-swap/swap::${SWAPSIZE} \
                -p freebsd-ufs/varfs:=var.img \
                -p freebsd-ufs/usrfs:=usr.img \
                -p freebsd-ufs/localfs:=local.img \
                -p freebsd-ufs/portsfs:=ports.img \
                -p freebsd-ufs/homefs:=home.img \
                -p freebsd-ufs/tmpfs:=tmp.img -o ${NAME}.${FORMAT}
        /bin/rm -f root.img var.img usr.img local.img home.img ports.img tmp.img

設定ファイルの例を示します。

# Device                Mountpoint      FStype  Options Dump    Pass#
/dev/gpt/rootfs         /               ufs     rw      1       1
/dev/gpt/swap           none            swap    sw      0       0
/dev/gpt/varfs          /var            ufs     rw      2       2
/dev/gpt/usrfs          /usr            ufs     rw      2       2
/dev/gpt/localfs        /usr/local      ufs     rw      2       2
/dev/gpt/portsfs        /usr/ports      ufs     rw      2       2
/dev/gpt/homefs         /home           ufs     rw      2       2
/dev/gpt/tmpfs          /tmp            ufs     rw      2       2
DISTDIR=                /home/ports/distfiles
WRKDIRPREFIX=           /home/ports
PACKAGES=               /home/ports/packages
OPTIONS_UNSET=          X11
DEFAULT_VERSIONS=       php=8.2 pgsql=11 python=3.9

.if ${.CURDIR} == "/usr/ports/ftp/curl"
OPTIONS_SET=            CARES
OPTIONS_UNSET=          THREADED_RESOLVER
.endif
hostname="test.example.com"
ifconfig_DEFAULT="DHCP"
sshd_enable="YES"
ntpd_enable="YES"
apache24_enable="YES"
DirectoryIndex index.php index.cgi index.html

<FilesMatch \.php$>
    SetHandler application/x-httpd-php
</FilesMatch>
AddType text/html .php

ServerTokens ProductOnly
diff -urN fs.org/etc/group fs/etc/group
--- fs.org/etc/group    2022-11-16 17:43:02.371250000 +0000
+++ fs/etc/group        2022-11-16 18:03:01.495575000 +0000
@@ -1,6 +1,6 @@
 # $FreeBSD$
 #
-wheel:*:0:root
+wheel:*:0:root,example
 daemon:*:1:
 kmem:*:2:
 sys:*:3:
@@ -37,3 +37,4 @@
 tests:*:977:
 nogroup:*:65533:
 nobody:*:65534:
+user:*:1000:
diff -urN fs.org/etc/hosts fs/etc/hosts
--- fs.org/etc/hosts    2022-11-16 17:43:02.371250000 +0000
+++ fs/etc/hosts        2022-11-16 18:03:01.495575000 +0000
@@ -11,7 +11,7 @@
 #
 #
 ::1                    localhost localhost.my.domain
-127.0.0.1              localhost localhost.my.domain
+127.0.0.1              localhost localhost.my.domain test.example.com
 #
 # Imaginary network.
 #10.0.0.2              myname.my.domain myname
diff -urN fs.org/etc/master.passwd fs/etc/master.passwd
--- fs.org/etc/master.passwd    2022-11-16 17:43:02.380484000 +0000
+++ fs/etc/master.passwd        2022-11-16 18:03:01.500335000 +0000
@@ -1,6 +1,6 @@
 # $FreeBSD$
 #
-root::0:0::0:0:Charlie &:/root:/bin/csh
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
 toor:*:0:0::0:0:Bourne-again Superuser:/root:
 daemon:*:1:1::0:0:Owner of many system processes:/root:/usr/sbin/nologin
 operator:*:2:5::0:0:System &:/:/usr/sbin/nologin
@@ -29,0 +29,4 @@
+#
+# Example User
+#
+example:*:1000:1000::0:0:Example User:/home/example:/bin/tcsh
diff -urN fs.org/etc/newsyslog.conf fs/etc/newsyslog.conf
--- fs.org/etc/newsyslog.conf   2022-05-12 08:31:21.000000000 +0000
+++ fs/etc/newsyslog.conf       2022-11-16 18:03:01.500910000 +0000
@@ -31,6 +31,10 @@
 /var/log/utx.log                       644  3     *    @01T05 B
 /var/log/weekly.log                    640  5     *    $W6D0 JN
 /var/log/daemon.log                    644  5     1000 @0101T JC
+
+/var/log/httpd-access.log              644  20    *    $W0D4 JC    /var/run/httpd.pid
+/var/log/httpd-error.log               644  9     *    $M1D4 JC    /var/run/httpd.pid
+/var/log/httpd-ssl-access.log          644  20    *    $W0D4 JC    /var/run/httpd.pid
 
 <include> /etc/newsyslog.conf.d/[!.]*.conf
 <include> /usr/local/etc/newsyslog.conf.d/[!.]*.conf
diff -urN fs.org/etc/ntp.conf fs/etc/ntp.conf
--- fs.org/etc/ntp.conf 2022-05-12 08:32:06.000000000 +0000
+++ fs/etc/ntp.conf     2022-11-16 18:03:01.501485000 +0000
@@ -29,7 +29,7 @@
 #
 # The option `iburst' is used for faster initial synchronization.
 #
-pool 0.freebsd.pool.ntp.org iburst
+# pool 0.freebsd.pool.ntp.org iburst
 
 #
 # If you want to pick yourself which country's public NTP server
@@ -37,7 +37,7 @@
 # the next one, and replace CC with the country's abbreviation.
 # Make sure that the hostname resolves to a proper IP address!
 #
-# pool 0.CC.pool.ntp.org iburst
+pool 0.jp.pool.ntp.org iburst
 
 #
 # To configure a specific server, such as an organization-wide local
diff -urN fs.org/etc/passwd fs/etc/passwd
--- fs.org/etc/passwd   2022-11-16 17:43:02.385360000 +0000
+++ fs/etc/passwd       2022-11-16 18:03:01.502001000 +0000
@@ -29,0 +29,4 @@
+#
+# Example User
+#
+example:*:1000:1000:Example User:/home/example:/bin/tcsh

さらに、files/home.tar.bz2にホームディレクトリのファイルをtar + bzip2したものを置いています。

AWS EC2 AMIの作成

AWS EC2でディスクイメージを利用する場合はAMIを作成する必要があります。FreeBSD packageにAMIを作成するツール(GitHub)がありますので、これを利用します。

実行例

# pkg install bsdec2-image-upload
# bsdec2-image-upload --sriov --ena example.raw "Example Image `/bin/date +%Y%m%d%H%M%S`" "FreeBSD `/usr/bin/uname -r`" ap-northeast-1 s3bucket_name ~/.awskey
工作クラブ

工作クラブ

記事一覧

「マーケライズ工作クラブ」で役に立つものを作り、その過程で新しい技術を習得してスキルアップしていきましょう。