Ubuntu Weekly Recipe

第561回ローカルインストール時もcloud-initを活用する

Ubuntuのサーバー版イメージには最初からcloud-initがインストールされています。このcloud-initはクラウド上のインスタンスを立ち上げる際、もろもろの初期設定を行ってくれる強力なツールです。今回はそのcloud-init先生に、ローカルのマシンにおいてもがんばってもらいましょう。

cloud-initの位置づけ

まずはcloud-initの位置づけから説明します。cloud-initそのものを知っている・使っているのであれば、次の項目まで読み飛ばしてください。

今世紀に入って「Linuxのインストール」は劇的に簡単になりました。インストールするだけでも一苦労、まともに動けばラッキーだった時代も昔の話。今はUSBデバイスから起動して、いくつかの質問に回答するだけでインストール完了です。さらにWindowsマシンがあるならば3クリックでデスクトップLinuxが立ち上がりますし、極端な話、Webブラウザーとクレジットカードだけ準備すればマウス操作だけでLinuxサーバーを用立てられる時代です。

ただし、インストールが簡単になったとは言え、インストール時の設定が不要になったわけではありません。たしかにハードウェアの標準化が進んだ結果、自動的に設定される項目が増えたのは事実です。それでもユーザー名やパスワード、インストール先のデバイスなど、利用者固有の設定は必要になります。

従来、これらの設定はインストーラーが担っていました。つまり利用者はインストーラーの内容が表示されるディスプレイの前に座って、インストーラーの質問に対してマウスやキーボードで答える必要があったのです。しかしながら「クラウド」の場合、ディスプレイやキーボード・マウスをどこにどうやって繋ぐのか、という問題が発生します。さらには「たくさんのインスタンスをデプロイできる」クラウドにおいて、デプロイのたびにインストーラーの質問に答えなくてはならないのも煩雑です。

「自動化できるものは自動化する」の観点から作られたのがcloud-initです。と言っても、もともとのパッケージ名が「ec2-init」だったことからかもわかるように、Amazon EC2専用のツールでした[1]⁠。初期のec2-initのパッケージは次のような説明されていたのです。

Description: Init scripts for EC2 instances
 EC2 instances need special scripts to run during initialisation
 to get hold of ssh keys and to let the user run various scripts.

EC2にはインスタンスメタデータとユーザーデータというインスタンスを設定・管理する仕組みが存在します。これはインスタンスに外部から任意のデータを渡すと、インスタンス内部からIPv4のリンクローカルアドレス(169.254.169.254)で取得できる仕組みです。一番最初のec2-initは、この仕組みを用いて、Upstartのジョブから次の2つの設定を実現していました。

  • メタデータのpublic-keysを/root/.ssh/authorized_keysに追加し、インスタンスが起動したらrootでsshログインできるようにする[2]⁠。
  • ユーザーデータをそのまま/tmp以下にコピーして実行する。

その後、Ubuntu 10.04 LTSに向けて大幅に改修・機能追加し、名前を「cloud-init」に変更した上で、今に至ります。

当初はAmazon EC2専用のツールでしたが、その後はさまざまなクラウドサービスの方式(データストア)に対応しました。またアカウントだけではなくリポジトリの設定やパッケージのインストールから、RHELのサブスクリプション登録まで、多種多様な機能をサポートしています。もちろん任意のスクリプトの実行も可能です。

サーバーの構成管理と言えばAnsibleが定番です。cloud-initのできることはAnsibleとかぶる部分もあるものの、どちらかと言うと構成管理よりは「初期セットアップ」が目的です。たとえばSSHログインできるようにするための設定のように、⁠Ansibleを動かす前にやっておきたいことはcloud-initで実行する」などと使い分けると良いでしょう[3]⁠。

VagrantfileやDockerfileも機能的に近い存在です。設定済みのイメージファイルを作りたいならVagrantfile/Dockerfileを使い、作成済みのイメージファイルを起動する際にセットアップしたいならcloud-initを使うような分担が一般的なようです[4]⁠。

cloud-initの仕組み

cloud-initは「メタデータとユーザーデータ」「何らかの方法」で受け取って、それを元に初期セットアップを行う仕組みです。

「何らかの方法」は使用するクラウドサービスに依存します。たとえばAmazon EC2ならインスタンスの起動時にWebフォームやコマンドから渡せますし、OpenNebulaならマシンテンプレートとして設定したものがCD-ROMイメージとしてインスタンスに渡ってくるようです。大抵はネットワーク経由ですが、設定によってはrootfsの中にあるファイルを使うことも可能です。

cloud-initはデータの取得方法をサービスごとの「データストア」として実装しています。

メタデータについては大抵の場合クラウドサービスが自動的に付加します。cloud-initはメタデータ内部の情報(インスタンスIDやホスト名設定など)に合わせて、再コンフィグが可能かどうかやユーザーデータ適用前の事前設定を実施します。利用者はメタデータを意識することはないはずですが、そういう仕組みが存在することは覚えておくと良いでしょう。

cloud-initの本体はユーザーデータになります。cloud-initは#cloud-configで始まっているユーザーデータをcloud-init用のデータ(cloud-config)として解釈します。cloud-configの書き方はcloud-initのドキュメントを参照してください。

cloud-utilsでデータストア作成

さて、ローカルマシンでcloud-initを使う方法を考えてみましょう。一般的なクラウドサービスでは、そのサービスがメタデータとユーザーデータそのものを渡す方法やその場所を通知する方法(データストア)を実装しています。しかしながらローカルマシンでは、その方法が定義されていません。そこでcloud-initでは、ローカルのマシンやLXDのようなコンテナなどのクラウドサービス外のデータストアとしてNoCloudを実装しています。

まず最初に「どこにデータストアがあるか」を指定する方法が必要です。NoCloudには次の3つの方法が用意されています。

  1. SMBIOS(System Management BIOS)上のシリアル番号
  2. カーネルのコマンドラインパラメーター
  3. ラベルcidataが付けられたファイルシステム

複数指定された場合、上のほうが使われます[5]⁠。

SMBIOSはマシン固有のデータをOSなどのソフトウェアに渡す仕組みです。Linuxであれば/sys/class/dmi以下やdmidecodeコマンドで参照できます。このうち「シリアル番号」とは、Type 1のシステム情報に記録されているデータで、一般的なPCなら「製造番号」などが記録されるフィールドです。

仮想マシンなどで使われているQEMUには、起動時にSMBIOSに任意の値を設定できる仕組みが存在します。そこでシリアル番号をデータストアに関する情報を渡しておけば、cloud-initはそれを解釈するのです。

$ sudo cat /sys/class/dmi/id/product_serial
ds=nocloud-net;seedfrom=http://10.0.0.1:8000/

上記の例なら、⁠http://10.0.0.1:8000/」にデータストアがあるとして、ネットワーク越しに情報を取得します。

カーネルのコマンドラインパラメーターは、上記のシリアル番号の内容をカーネル起動時のパラメーターとして渡す方法です。GRUBなどのブートローダーで指定したい場合に使えるでしょう。

ファイルシステムのラベルとしてcidataが付いているパーティションが存在すれば、cloud-initはそれをデータストアとして使用します。QEMU/KVMベースのNoCloudでもっとも一般的に使う方法です。さらにNoCloud以外のデータストアでも使えることが多いので、今回はこの方法を紹介しましょう。

cidataタイプのデータは次の3つの条件が満たされている必要があります。

  • ファイルシステムのラベルがcidataであること
  • システムが解釈できるファイルシステムであること(ISO9660かFATが使われることが一般的です)
  • トップディレクトリに「meta-data」「user-data」の2つのファイルが存在すること

上記の条件を満たすのデータは手作業でも作れますが、cloud-utilsにあるcloud-image-utilsパッケージを使うとよりかんたんに作成できます。

$ sudo apt install cloud-image-utils

cloud-image-utilsパッケージは、クラウドイメージを操作するために便利なツールが含まれているパッケージです。たとえばパーティションのリサイズや最新のAMIのID取得などが行なえます。今回はこのうちcloud-localdsコマンドを使用します。

$ cloud-localds --help
Usage: cloud-localds [ options ] output user-data [meta-data]

   Create a disk for cloud-init to utilize nocloud

   options:
     -h | --help             show usage
     -d | --disk-format D    disk format to output. default: raw
                             can be anything supported by qemu-img or
                             tar, tar-seed-local, tar-seed-net
     -H | --hostname    H    set hostname in metadata to H
     -f | --filesystem  F    filesystem format (vfat or iso), default: iso9660

     -i | --interfaces  F    write network interfaces file into metadata
     -N | --network-config F write network config file to local datasource
     -m | --dsmode      M    add 'dsmode' ('local' or 'net') to the metadata
                             default in cloud-init is 'net', meaning network is
                             required.
     -V | --vendor-data F    vendor-data file
     -v | --verbose          increase verbosity

   Note, --dsmode, --hostname, and --interfaces are incompatible
   with metadata.

   Example:
    * cat my-user-data
      #cloud-config
      password: passw0rd
      chpasswd: { expire: False }
      ssh_pwauth: True
    * echo "instance-id: $(uuidgen || echo i-abcdefg)" > my-meta-data
    * cloud-localds my-seed.img my-user-data my-meta-data
    * kvm -net nic -net user,hostfwd=tcp::2222-:22 \
         -drive file=disk1.img,if=virtio -drive file=my-seed.img,if=virtio
    * ssh -p 2222 ubuntu@localhost

オプションからもわかるようにホストネームやネットワークインターフェースも指定可能です。これらはcloud-configではなく、meta-data側に保存されます。

あらかじめcloud-configデータを作っておきます。今回は初期状態で存在するアカウント(ubuntu)のパスワードを「ubuntu」に設定し、パスワードでsshログインできるように変更しているだけです。

$ cat >user-data <<EOF
#cloud-config
password: ubuntu
chpasswd: { expire: False }
ssh_pwauth: True
EOF

cloud-localdsでデータストアを作成します。設定は基本的に規定値で問題ありません。

$ cloud-localds user-data.img user-data

作られたデータストアを確認しましょう。まず、IS09660形式で、なおかつcidateのラベルが振られていることがわかります。

$ file user-data.img
user-data.img: ISO 9660 CD-ROM filesystem data 'cidata'

データストアをマウントして中身を確認してみましょう。

$ sudo mount -o ro user-data.img /mnt/

$ cat /mnt/meta-data
{
"instance-id": "iid-local01"
}

$ cat /mnt/user-data
#cloud-config
password: ubuntu
chpasswd: { expire: False }
ssh_pwauth: True

$ sudo umount /mnt

meta-dataにはインスタンスIDが、user-dataには作成したcloud-configが保存されていることがわかります。ちなみにインスタンスIDはその値を変えると、次回起動時にuser-dataの内容が再設定されます。インスタンスIDを変更したい場合は、cloud-localdsの第三引数にmeta-dataファイルを指定してください。

これでローカルマシン用のデータストアが作成されました。

QEMUでNoCloudを使用する

では作成したデータストアをQEMUで使ってみましょう。必要なパッケージとイメージファイルをダウンロードします。

$ sudo apt install qemu-kvm qemu-utils ovmf
$ wget https://cloud-images.ubuntu.com/releases/18.04/release/ubuntu-18.04-server-cloudimg-amd64.img
$ qemu-img resize ubuntu-18.04-server-cloudimg-amd64.img 10G

最後のコマンドでイメージサイズを2GiBから10GiBに変更しています。この時点ではディスクのサイズが変わっただけで、パーティションのサイズはオリジナルのままになっています。本来はパーティションのリサイズも必要ですが、実はcloud-initが自動的にルートパーティションのサイズを変更してくれますので、このままでかまいません[6]⁠。

さらに今回は第441回でも紹介したように、UEFI/OVMF版のファームウェアをインストールしていますが、もちろん従来のSeaBIOSでも可能です。

$ cp /usr/share/OVMF/OVMF_VARS.fd .

UEFI/OVMFを使うなら、変数領域を別途コピーしておくと良いでしょう。詳しいことは第441回を参照してください。

最後にルートファイルシステムに加えて、先程作ったデータストアもドライブとして指定して起動します。

$ sudo qemu-system-x86_64 \
    -m 2G -enable-kvm -nographic \
    -net user,hostfwd=tcp::2222-:22 \
    -drive if=pflash,format=raw,readonly,file=/usr/share/OVMF/OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=OVMF_VARS.fd \
    -drive file=ubuntu-18.04-server-cloudimg-amd64.img,format=qcow2 \
    -drive file=user-data.img,format=raw

データストアとして必要なのは最後の1行だけです。起動してしばらくすると、次のようなメッセージが表示されることでしょう。

[   20.396382] cloud-init[1131]: Cloud-init v. 18.5-21-g8ee294d5-0ubuntu1~18.04.1 finished at Sun, 17 Mar 2019 12:03:34 +0000. Datasource

これでcloud-initの設定完了ですので、設定したアカウントとパスワードでログインできるか確認してください。さらにhostfwdオプションも指定しているので、ホストマシンのポート2222からsshログインできるはずです。

$ ssh -p 2222 ubuntu@localhost

virt-managerでNoCloudを使用する

もう一つの例として、virtinstパッケージにあるvirt-installコマンドからデータストアを追加してみましょう。まずはあらかじめ、必要なパッケージをインストールしておきます。

$ sudo apt install virtinst

あとは「cloudtest」の名前で仮想マシンを作ってみましょう。

$ virt-install \
  --name cloudtest \
  --vcpus 1 --ram 2048 \
  --hvm --virt-type kvm \
  --os-type linux --os-variant ubuntu18.04 \
  --graphics none --serial pty --console pty \
  --import --noreboot \
  --disk path=ubuntu-18.04-server-cloudimg-amd64.img \
  --disk path="$PWD/user-data.img,bus=ide,device=cdrom"

こちらも渡しているオプションのうちデータストア関連は最後の1行だけです。

実際に仮想マシンを起動し、コンソールから設定したパスワードでログインできるか確認しておきましょう。

$ virsh start cloudtest
$ virsh console cloudtest

SMBIOSで指定する方法

最後にSMBIOSで設定する方法も紹介しておきます。この方法の最も便利な点は、任意のURL上にメタデータとユーザーデータを置けることです。

まずあらかじめ、仮想マシンからHTTP/HTTPS/FTPアクセスできる場所に、meta-dataファイルとuser-dataファイルを配置してください。今回は「https://example.com/cloud-init/」とします。つまり、⁠https://example.com/cloud-init/meta-data」でmeta-dataファイルそのものが、⁠https://example.com/cloud-init/user-data」でuser-dataファイルそのものが取得できる状態になっているはずです。

あとはuser-data.imgファイルの代わりにsmbiosオプションを指定して仮想マシンを起動します。

$ sudo qemu-system-x86_64 \
    -m 2G -enable-kvm -nographic \
    -net user,hostfwd=tcp::2222-:22 \
    -drive if=pflash,format=raw,readonly,file=/usr/share/OVMF/OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=OVMF_VARS.fd \
    -drive file=ubuntu-18.04-server-cloudimg-amd64.img,format=qcow2 \
    -smbios "type=1,serial=ds=nocloud-net;seedfrom=https://example.com/cloud-init/"

「type=1」でType 1の(つまりシステム情報の)設定を行い、⁠serial=」でシリアル番号の値を変更しています。シリアル番号の値はds=nocloud-net;seedfrom=https://example.com/cloud-init/です。中にセミコロンが含まれているのでダブルクオーテーションで括ってください。

「ds=nocloud-net」はネットワーク経由でデータストアを取得することを意味します。これはつまりインスタンスのネットワーク設定が完了するまでは、cloud-initは実施されません。⁠seedfrom=」で取得するデータのURLを指定します。

「ds=nocloud」と指定することも可能です。この場合はインスタンスのローカルファイルからデータを取得できます。⁠seedfrom=」「/」から「file://」で始まる絶対パスを指定してください。ルートファイルシステムイメージにcloud-configのデータまで入れておきたい場合に便利でしょう。

うまく設定できていれば、次のようにDMIの値が変わっているはずです。

$ sudo cat /sys/class/dmi/id/product_serial
ds=nocloud-net;seedfrom=https://examplecom/cloud-init/

cloud-initそのものの実行ログは/var/log/cloud-init.logに、cloud-initによる設定ログは/var/log/cloud-init-output.logに記録されます。うまく動かないようなら、そちらも確認しておくと良いでしょう。ただしクラウドイメージは、そのままだとログインできるアカウントが設定されていません。つまりcloud-initが失敗するとログインできません。トライアンドエラーを行うなら、デバッグ用のアカウントをあらかじめ作成済みのイメージを用意してそちらを使用してください。

おすすめ記事

記事・ニュース一覧