Ubuntu Weekly Recipe

第869回WSLでもcloud-initを活用する

Windows Subsystem for Linux(WSL)が登場して久しいです。WSL登場以前は、WindowsマシンでUbuntuを使おうとすると、⁠仮想マシンソフトウェアを利用してUbuntuの仮想マシンを作成する」⁠UbuntuとWindowsのデュアルブートする」⁠もう戻らないという覚悟でWindowsの領域を消し飛ばしてUbuntuマシンに変えてしまう」といった対応が必要でした。

しかし、WSLを使えるようになったことで、Windowsマシンでも手軽にUbuntuを利用できるようになりました。開発環境や作業環境としてUbuntuをWSL上で使っているという方も多数いることでしょう。

さらに、WSLを使えば、Ubuntu環境を作ったり、壊したりも手軽にできます。

PS> wsl --install ubuntu-24.04      # 作ったり
PS> wsl --unregister ubuntu-24.04   # 壊したり

でも、作ったり、壊したりまでは簡単でも、そのあとの環境の再構築・再設定が億劫で案外「作り直してない」なんてことはよくあると思います(筆者がそうです⁠⁠。

そうだ、cloud-initを使おう

Ubuntu 24.04 LTS以降および22.04 LTSでは、WSL環境でもcloud-initが使えます第561回ではローカルインストール時にcloud-initを活用する方法は紹介していますが、同じようなことをWSL環境で(しかも、より簡単に)実現できます[1]。そのため、WSL上のUbuntuの環境構築をcloud-initに任せれば、作ったり壊したりも気軽にできるようになります。

ユーザーデータの配置場所と優先順位

WSLで稼働するUbuntuのcloud-initは初回起動時にユーザーデータを次の順番で探し、合致するユーザーデータが見つかった時点で検索は終了、見つけたそのユーザーデータを読み込み・実行します。

  1. %USERPROFILE%\.cloud-init\<インスタンス名>.user-data
  2. %USERPROFILE%\.cloud-init\<ID>-<VERSION_ID>.user-data
  3. %USERPROFILE%\.cloud-init\<ID>-all.user-data
  4. %USERPROFILE%\.cloud-init\default.user-data

ちなみに、デフォルトでは大文字・小文字は区別しません。

これらのパスについて解説します。

まず、4つ全部に共通する%USERPROFILE%という部分ですが、これはWindowsの環境変数で、現在のWindowsユーザーフォルダーC:\Users\<ユーザー名>\など)が該当します[2]

続いて個別のパスの解説へと移るのですが、その前に1にある「インスタンス」という言葉について解説します。この「インスタンス」wslコマンド側で「ディストリビューション」と表現されているものです。例えば、次のコマンドでUbuntu-24.04とあるものがインスタンスでありその名前です。

PS> wsl -l
Linux 用 Windows サブシステム ディストリビューション:
Ubuntu-24.04 (既定値)

ただ、WSL側がいうところの「ディストリビューション」「名前」はいくらでもつけようがあり、その中で利用されているLinuxディストリビューション=「実体」とは何ら関係がありません。また、WSL上でのcloud-init利用においては、この「名前」「実体」の違いが重要です。そのため、本稿ではcloud-initのドキュメントに習い以後、wslコマンド側で「ディストリビューション」と表現されているものを「⁠⁠WSL)インスタンス」とします。

これを踏またうえで1から解説しますと、1は特定の「名前」を持つインスタンスにのみ適用されます。そのため、⁠この名前を持つインスタンスは特定の用途で使用するので、その前提処理を初回起動時に実行する」といったユースケースで威力を発揮します。

続いて2ですが、2に含まれる<ID><VERSION_ID>には、それぞれ、WSLインスタンス内部の/etc/os-releaseに記載されるIDVERSION_IDを代入したパスになります。例えば、Ubuntu 24.04の場合、/etc/os-releaseに記載されたIDubuntuVERSION_ID24.04であるため、対応するパスは%USERPROFILE%\.cloud-init\ubuntu-24.04.user-dataとなります。

$ grep -e ID= -e VERSION_ID= /etc/os-release
VERSION_ID="24.04"
ID=ubuntu

先ほどの「名前」「実体」でいうと、1が「名前」で見分けてるのに対し、2は「実体」で見分けて対応しているといえます。この2は「特定のディストリビューションとバージョンに依存する処理をしておきたい」という場面で活きてくるでしょう。

3は2と考え方は同じですが、2と違って<VERSION_ID>の部分がallとなっています。これはディストリビューション名/etc/os-releaseIDだけを見て、バージョン(リリース)は任意であることを意味します。この3はWSL上で複数のディストリビューションを稼働させている場合に、特定のディストリビューション(例:Ubuntu)に固有の処理をさせるときに便利でしょう。

4は1~3のどれにも該当しなかった場合に読み込まれるユーザーデータです。4はディストリビューションに依存しない処理[3]をおこなう場合に便利でしょう。

このように優先順位が高いパスほど適用範囲が狭まります。

ユーザーデータを作成する

早速ですが、ユーザーデータを作成します。より広範なユーザーデータのサンプルはcloud-initのドキュメントを見るなどしていただくとして、今回は次のサンプルを使います。

#cloud-config
locale: ja_JP.UTF-8
users:
- name: wsl-user
  groups: users,sudo,netdev,audio,adm
  sudo: ALL=(ALL) NOPASSWD:ALL
  shell: /bin/bash
  lock_passwd: true

write_files:
- path: /etc/wsl.conf
  append: true
  content: |
    [user]
    default=wsl-user

packages:
- unzip

この時点で他にユーザーデータは作成していないため、Ubuntuを使用するすべてのWSLインスタンスでこのユーザーデータが実行されます。

簡単ですが部分ごとに内容を解説しておきましょう。

locale: ja_JP.UTF-8

localeではロケールを日本語ja_JP.UTF-8に設定しています。設定しない場合のロケールは英語C.UTF-8です。

users:
- name: wsl-user
  groups: users,sudo,netdev,audio,adm
  sudo: ALL=(ALL) NOPASSWD:ALL
  shell: /bin/bash
  lock_passwd: true

このusersではwsl-userという名前のユーザーを作成して、グループへの追加やシェルの設定、sudoの設定[4]をします。

write_files:
- path: /etc/wsl.conf
  append: true
  content: |
    [user]
    default=wsl-user

ここでは/etc/wsl.confに追記をおこない、WSLインスタンスを起動したときに、上で作成するよう指定したwsl-userとしてログインするよう設定しています[5]

packages:
- unzip

最後にpackagesに書かれたパッケージをインストールするよう指定しています。

WSLインスタンスを作成する

ユーザーデータが準備できたのでこのファイルを3の%USERPROFILE%\.cloud-init\ubuntu-all.user-dataとして保存しておきます。先程見たように、このパスはすべてのUbuntuのWSLインスタンスが対象となりうるパスで読み込み順位は3位です。ユーザーデータが準備できたので実際にWSLインスタンスを作成してみましょう。先ほど見たように、ユーザーデータの置き場所=ユーザーデータの適用パターンは複数あるので、それぞれ解説します。

すべてのUbuntuのWSLインスタンスに適用

まずは、作成したユーザーデータのファイルを3の%USERPROFILE%\.cloud-init\ubuntu-all.user-dataとして保存しておきます。このパスはすべてのUbuntuのWSLインスタンスが対象となりうるパスで読み込み順位は3位です。

それでは、WSLインスタンスを作成します。次のコマンドを実行します。

PS> wsl --install -d Ubuntu-24.04 --name Test01

今回はUbuntu 24.04のWSLインスタンスを(既存のインスタンス名と重複しないように)Test01というインスタンス名で作成します。なお、--nameオプションが使えない場合はお使いのWSLのバージョンが古い可能性があるため、wsl --updateを実行して更新してください。

ダウンロード・インストールが完了すると、作成したインスタンスが起動し、wsl-userとしてログインした状態となっているはずです。また、パスワードの入力なしで、sudoも使え、ロケールも日本語になっているはずです。

$ id
uid=1000(wsl-user) gid=1000(wsl-user) groups=1000(wsl-user),4(adm),27(sudo),29(audio),100(users),107(netdev)

$ sudo id
uid=0(root) gid=0(root) groups=0(root)

$ locale
LANG=ja_JP.UTF-8
LANGUAGE=
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_PAPER="ja_JP.UTF-8"
LC_NAME="ja_JP.UTF-8"
LC_ADDRESS="ja_JP.UTF-8"
LC_TELEPHONE="ja_JP.UTF-8"
LC_MEASUREMENT="ja_JP.UTF-8"
LC_IDENTIFICATION="ja_JP.UTF-8"
LC_ALL=

現時点で、他にユーザーデータはなく、すべてのUbuntuリリースで実行されるユーザーデータのため、Ubuntu 22.04でも同じ結果が得られます。なお、Ubuntu 22.04は「レガシーディストリビューション」とされており、--nameオプションは使えませんので、次のように起動します。

PS> wsl --install -d Ubuntu-22.04

特定のリリースのUbuntuインスタンスに適用

次にユーザーデータを2の%USERPROFILE%\.cloud-init\ubuntu-24.04.user-dataに配置します。これはWSLインスタンスで使用されるディストリビューションとそのリリースが、Ubuntu 24.04の場合に使用されるパスで、読み込み順位は2位でした。

もっとも、%USERPROFILE%\.cloud-init\ubuntu-all.user-dataと同じ内容だとどちらが読み込まれたのかがわかりません。よって、次のような内容を末尾に追記します。

runcmd:
- echo "Hello, Ubuntu 24.04" > /output

このruncmdでコマンドを実行して、ファイルに文字列を出力させ、実行状況がわかるようにしようというわけです。

同様にWSLインスタンスを作成します。

PS> wsl --install -d Ubuntu-24.04 --name Test02

すると期待した通りにUbuntu 24.04向けのユーザーデータが実行されていることがわかります。

$ cat /output
Hello, Ubuntu 24.04

違いを確認するために、Ubuntu 25.04を利用してみます[6]。まずは、こちらからUbuntu 25.04のWSLイメージubuntu-25.04-wsl-amd64.wslをダウンロードします。そして、ダウンロードしたWSLイメージを使用して、インスタンスを作成します。

PS> wsl --install --from-file Downloads\ubuntu-25.04-wsl-amd64.wsl --name Test03

そして結果を確認してみます。

$ cat /output
cat: /output: No such file or directory

ファイルは存在しません。また、読み込まれているユーザーデータも、期待通り、ubuntu-all.user-dataであることがわかります。

$ sudo cat /var/lib/cloud/instance/user-data.txt
#cloud-config
locale: ja_JP.UTF-8
users:
- name: wsl-user
  groups: users,sudo,netdev,audio,adm
  sudo: ALL=(ALL) NOPASSWD:ALL
  shell: /bin/bash
  lock_passwd: true

write_files:
- path: /etc/wsl.conf
  append: true
  content: |
    [user]
    default=wsl-user

packages:
- unzip

特定の名前のインスタンスに適用

最後は、3の特定の名前を持つインスタンス専用の%USERPROFILE%\.cloud-init\<インスタンス名>.user-dataです。とはいえ、作業・確認方法自体は3や2と同じです。このままでは、おもしろみがないので、ユーザーデータの作成方法を工夫してみます。

ユーザーデータでのコマンド実行方法には、2で見たcloud-config形式のユーザーデータでruncmdを使う方法と、ユーザーデータをシェルスクリプト形式で書く方法と大きく2種類あります。両者は一長一短であるため、二者択一で考えると、

  • cloud-config形式はこれまでみてきたようにユーザーの設定などはやりやすいけど、複雑なシェルスクリプトは書きづらい
  • シェルスクリプト形式で書くと複雑なシェルスクリプトは書きやすいけど、それ以外も頑張ってシェルスクリプトに落とし込まないといけない

というジレンマがあります。

実際には二者択一ではなく、MIMEマルチパート形式を使うことで、これら両方の形式を併用できます。最後の3は、このMIMEマルチパート形式でユーザーデータを作成してみます。

MIMEマルチパート形式のユーザーデータはcloud-initコマンドで作成できます。今回、cloud-config形式のユーザーデータは%USERPROFILE%\.cloud-init\ubuntu-all.user-dataがすでにありますのでこれを再利用することにして、追加でシェルスクリプト側を作成します。以下の内容でシェルスクリプトを作成し、%USERPROFILE%\.cloud-init\test04.shとして配置しておきます[7]

#!/bin/bash

echo 'Hello, instance "Test04"' > /output

すでに作成したWSLインスタンス(いっぱい作ったはずです)のどれかを起動して、WSL側から%USERPROFILE%\.cloud-init\に相当するパスへ移動、次のコマンドを実行して、MIMEマルチパート形式のユーザーデータを作成します。

$ cd /mnt/c/Users/${your-windows-user-name}/.cloud-init
$ cloud-init devel make-mime -a ubuntu-all.user-data:cloud-config -a test04.sh:x-shellscript > test04.user-data

test04.user-dataは次のような内容になっているはずです。

Content-Type: multipart/mixed; boundary="===============3009917472491891126=="
MIME-Version: 1.0

--===============3009917472491891126==
Content-Type: text/cloud-config; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="ubuntu-all.user-data"

I2Nsb3VkLWNvbmZpZwpsb2NhbGU6IGphX0pQLlVURi04CnVzZXJzOgotIG5hbWU6IHdzbC11c2Vy
CiAgZ3JvdXBzOiB1c2VycyxzdWRvLG5ldGRldixhdWRpbyxhZG0KICBzdWRvOiBBTEw9KEFMTCkg
Tk9QQVNTV0Q6QUxMCiAgc2hlbGw6IC9iaW4vYmFzaAogIGxvY2tfcGFzc3dkOiB0cnVlCgp3cml0
ZV9maWxlczoKLSBwYXRoOiAvZXRjL3dzbC5jb25mCiAgYXBwZW5kOiB0cnVlCiAgY29udGVudDog
fAogICAgW3VzZXJdCiAgICBkZWZhdWx0PXdzbC11c2VyCgpwYWNrYWdlczoKLSB1bnppcA==

--===============3009917472491891126==
Content-Type: text/x-shellscript; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test04.sh"

IyEvYmluL2Jhc2gKCmVjaG8gJ0hlbGxvLCBpbnN0YW5jZSAiVGVzdDA0IicgPiAvb3V0cHV0

--===============3009917472491891126==--

前半部分がubuntu-all.user-dataを、後半部分がtest04.shをそれぞれ、base64でエンコードされたものになっています[8]

そして、"Test04"という名前でインスタンスを作成し、結果を確認すると、cloud-config形式およびシェルスクリプト形式の両方のユーザーデータの内容が実行されていることがわかります。

PS> wsl --install --from-file Downloads\ubuntu-25.04-wsl-amd64.wsl --name Test04
...

$ id
uid=1000(wsl-user) gid=1000(wsl-user) groups=1000(wsl-user),4(adm),27(sudo),29(audio),100(users),105(netdev)

$ cat /output
Hello, instance "Test04"

$ sudo cat /var/lib/cloud/instance/user-data.txt
Content-Type: multipart/mixed; boundary="===============3009917472491891126=="
MIME-Version: 1.0

--===============3009917472491891126==
Content-Type: text/cloud-config; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="ubuntu-all.user-data"

I2Nsb3VkLWNvbmZpZwpsb2NhbGU6IGphX0pQLlVURi04CnVzZXJzOgotIG5hbWU6IHdzbC11c2Vy
CiAgZ3JvdXBzOiB1c2VycyxzdWRvLG5ldGRldixhdWRpbyxhZG0KICBzdWRvOiBBTEw9KEFMTCkg
Tk9QQVNTV0Q6QUxMCiAgc2hlbGw6IC9iaW4vYmFzaAogIGxvY2tfcGFzc3dkOiB0cnVlCgp3cml0
ZV9maWxlczoKLSBwYXRoOiAvZXRjL3dzbC5jb25mCiAgYXBwZW5kOiB0cnVlCiAgY29udGVudDog
fAogICAgW3VzZXJdCiAgICBkZWZhdWx0PXdzbC11c2VyCgpwYWNrYWdlczoKLSB1bnppcA==

--===============3009917472491891126==
Content-Type: text/x-shellscript; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test04.sh"

IyEvYmluL2Jhc2gKCmVjaG8gJ0hlbGxvLCBpbnN0YW5jZSAiVGVzdDA0IicgPiAvb3V0cHV0

--===============3009917472491891126==--

お片付け

最後に片付けをします。不要なWSLインスタンスごとに次の様なコマンドを実行して、インスタンスを削除しておきましょう。

PS> wsl --unregister test01

また、今後、意図しない動作を防ぐために不要なユーザーデータがあればクリーンアップしておくとよいでしょう。


このように、WSL環境でもcloud-initを使うことで、自分向けにカスタマイズされた環境を安定的に再構成できます。手軽に作ったり壊したりがやりやすくなりますね。

おすすめ記事

記事・ニュース一覧