Windows 10 で WSL2 と Docker をセットアップ

雑誌「Software Design 2021年12月号」の Docker 特集記事を読んで、Docker を試してみたくなりました。自宅の Windows 10 の PC に Docker 環境をインストールして遊んでみることに。せっかくなので、Docker のバックエンドには、Hyper-V ではなくて WSL2 (Windows Subsystem for Linux 2) を選びたいところ。最終的には、Docker Desktop for Windows はあきらめて、Rancher Desktop 1.0.0 を使うことにしました。

まえがき

Docker も WSL2 も以前から興味があって、いろいろ調べてはいたのですが、実際に使ったことはありませんでした。 WSL2 は「Windows 上で bash とか使えて便利!」とか聞きますが、わたしはもともと Git for Windows という MSYS ベースの環境を入れてて、 bash とか perl とかふつうに使えていたので、特に必要性は感じていなかったのです。本題には関係ないんですが、FFmpeg とか ImageMagick とかをシェルスクリプトで走らせるの、すごく便利ですよね。複数ファイルのバッチ処理も xargs -P で並列化するだけで簡単にマルチコアを有効活用できちゃいます。

Docker は、プライベートの独自ドメインを運用している VPS サーバで使いたいとは思っていて、本を買ったけど読んでなかったりして、次にサーバを乗り替えるタイミングで導入するつもりでした。実は、いま借りている VPS はカーネルが古くて Docker 使えないのです。そこで、手元の Windows PC で Docker 動かせばいろいろ遊べるのでは? というお話になるわけです。

まず、そもそも Windows 上で Docker を動かすってどういう意味? というところから調べてみる:

とりあえず、WSL2 をバックエンドにして Docker を動かすのが、一番オーバーヘッドが少なそう。 WSL2 に入れたディストリビューション上で Docker を動かしてもいいのですが、やっぱり Windows のコマンドライン上で docker コマンドが使えたほうが便利そうな気がする。 Docker Desktop for Windows の新しいライセンス形態について話題になっていますが、とりあえず個人的な利用だから OK でしょう。

というわけで、つぎに WSL2 について調べてみる:

だいたい理解できた。

実際の作業

環境:

OS: Windows 10 Pro 64bit 21H2 (build 19044)
CPU: AMD Ryzen Threadripper 2950X (16core/32thread, 3.5GHz)
MEM: 32GB

Hyper-V 利用中(いろいろ実験用の Windows 10 をゲストとして動かしている)
Windows Defender 以外のセキュリティソフトは無し

まず、Windows の機能を有効化します。(Windows のコマンドラインで wsl --install とやればよろしくやってくれるような話もありますが、今回は手動で有効化しました)

上図で赤線を引いてある「Linux 用 Windows サブシステム」と「仮想マシン プラットフォーム」にチェックマークを付けます。なお、この設定ウインドウは、
「設定」⇒「アプリ」⇒「オプション機能」⇒「Windows のその他の機能」
で開けます。

で、本来はここで WSL2 にてきとうなディストリビューション(Ubuntu-20.04 とか)を入れるのでしょうが、 Docker 環境以外に余計なモノを入れたくなかったので、スキップ。

いきなり Docker Desktop for Windows をインストールします。 Docker の公式サイトからインストーラーをダウンロードして実行。

バージョンは 4.4.4 (73704) でした。今回は Docker のバックエンドを WSL2 にしたいので、「Install required Windows components for WSL2」のチェックボックスを ON にしておきます。なお、ダウンローダーの .exe ファイルをダブルクリックしても起動せず、右クリックメニューから「管理者として実行」しないとダメでした。

インストーラーは無事終了。

デスクトップに作られたアイコンをダブルクリックして起動してみると、エラーダイアログが出てしまいました。なにやら内部エラーで例外をスローしている模様。「Failed to deploy distro docker-desktop to」とか言ってますが、よくわからない。

ググってみると、「Docker Desktop for Windows の WSL2 バックエンドは Hyper-V とは共存できない」みたいな話が出てきます。 Docker Desktop for Windows の Dashboard 画面は開いたので、設定画面で WSL2 バックエンドを無効化してみると、エラーにはならなくなりました。でも、これでは当初のもくろみから外れてしまいます・・・。

Windows のコマンドラインで wsl -l -v を実行すると、docker-desktopdocker-desktop-data が確認できました。

しかし、タスクトレイのアイコンをクリックしても右クリックしても反応しなかったり、すごく遅れて右クリックメニューが開いたんだけど項目がクリックできなかったり、すごく不安定

もしかして WSL2 がちゃんとセットアップできていないのかも? と疑って、てきとうなディストリビューションを入れてみることに。

Windows のコマンドラインで wsl --install -d Ubuntu-20.04 と実行すると、あっさり成功。しかし、wsl -l -v で確認すると、WSL のバージョンが 2 ではなく 1 になってしまっていました。謎。

よくわからないので、いったん Docker Desktop for Windows をアンインストール。 WSL2 に入れた Ubuntu-20.04 もアンインストール(wsl --unregister Ubuntu-20.04)。

もう一度、Docker Desktop for Windows をインストール。しかし、症状は変わらず。

WSL2 にももう一度 Ubuntu-20.04 を入れようとしたら、開いたコンソールウインドウにエラーメッセージが出てインストールできず。エラーコード 0x8007000e との表示。ググってみると、これは WSL2 がメモリ不足だと言っているらしく、WSL2 の設定ファイル .wslconfig を作ってあげれば回避できるらしい。

Windows のユーザープロファイルのフォルダ(C:\Users\ユーザ名 あるいは %UserProfile%)に、以下のような内容のテキストファイル .wslconfig を置きました。(いちおう念のため改行コードは CR+LF にしておきました)

[wsl2]
memory=4GB
processors=16
swap=8GB

無事に Ubunto-20.04 のインストールができるようになりました。

しかし、Docker Desktop for Windows の不安定さは変わらず。

ひとまずあきらめて、Docker Desktop for Windows をアンインストール。

代わりに、最近リリースされたばかりの Rancher Desktop 1.0.0 を試してみることにしました。

公式サイトからインストーラーをダウンロードして、実行。問題なくインストールできました。 WSL2 にちゃんと入っているようです。

> wsl -l -v
  NAME                    STATE           VERSION
* Ubuntu-20.04_dev        Stopped         2
  rancher-desktop         Running         2
  rancher-desktop-data    Stopped         2
>

Windows のコマンドラインで docker コマンドが使えるようになりました。

デフォルトでは Kubernates のコンテナが動いているらしいのですが、使う予定もないですし、Rancher Desktop の公式 FAQ にしたがって停止させることにしました。

> kubectl config use-context rancher-desktop
> kubectl delete node lima-rancher-desktop

ここで、削除するノード名(?)が違っているらしく、delete がエラーに。 kubectl get nodes コマンドでノード名を確認して、再度実行。無事成功。

> kubectl get nodes
> kubectl delete node seaoak-pc
> kubectl get nodes

docker コマンドで hello-world コンテナをダウンロードして実行することもできました。OK。

最後に、Rancher Desktop を含めて WSL2 にインストールされているディストリビューションを、Cドライブから別のドライブに移動させます。 C ドライブはすでに容量不足ですし、Docker イメージとか大きなモノは別のドライブに入れるようにしたいのです。

Windows のコマンドラインで、一度 tar ファイルに export して、それを import すれば OK です。簡単!

> E:
> cd \
> mkdir WSL
> cd WSL
> mkdir images
> mkdir archives
> cd archives
> wsl --export rancher-desktop rancher-desktop_20220202a.tar
> wsl --export rancher-desktop-data rancher-desktop-data_20220202a.tar
> wsl --unregister rancher-desktop-data
> wsl --unregister rancher-desktop
> wsl -l -v
> wsl --import rancher-desktop E:\WSL\images\rancher-desktop .\rancher-desktop_20220202a.tar --version 2
> wsl -l -v
> wsl --import rancher-desktop-data E:\WSL\images\rancher-desktop-data .\rancher-desktop-data_20220202a.tar --version 2
> wsl -l -v

なお、import した tar ファイルは、削除してしまって問題ありません。

これで、今後、Docker イメージをたくさん作っても安心です。

以上、Windows 10 上での WSL2 と Docker のセットアップでした。

Rust の HTML テンプレートエンジンを選ぶ

Rust で使える HTML テンプレートエンジンを探してみました。

「HTML テンプレートエンジン」というのは、ブログサイトとかの生成ツールが使っている便利なしくみです。 HTML ファイルを生成するときに全部プログラムで生成するのは大変なので、あらかじめテキストファイルで HTML ファイルの「ひな形」(テンプレート)を作っておいて、その中に、プログラムから値を埋め込むための印(プレースホルダー)を書いておきます。テンプレートエンジンは、そのひな形を読み込んで、プログラムから渡されたデータでプレースホルダーを書き替えます。

テンプレートの書式もいろいろあって、基本的にはテンプレートエンジンによって異なります。もちろん、複数のプログラミング言語でのエンジン実装が作られているテンプレート書式もあります。基本的な書式は一緒だけど、エンジン実装ごとに微妙に仕様が違う、というのもあります。

ぱっと思いつくだけでも、テンプレートエンジンはたくさんあります:

  • Pug – 旧称「Jade」。JavaScript がメインっぽいけど、いろんな言語での実装がある。
  • Nunjucks – JavaScript 製エンジン。
  • EJS – これも JavaScript 製エンジン。
  • Jinja – Python 製エンジン。
  • Django – これも Python 製エンジン。
  • Handlebars – JavaScript と Rust の実装があるみたい。Rust の公式ドキュメントとかでも使われているらしい。
  • Tera – Rust 製エンジン。Jinja2 と Django からの派生とのこと。

今回は、Rust で作っているプログラムで使いたいので、Rust 製エンジンを選びます。

とりあえず、crate.io で人気のあるクレートを探してみる。
https://crates.io/search?q=template html&sort=downloads

tinytemplate とか handlebars とか tera とか Askama とか liquid とか出てきますね。ほかにも、Seilfish とか Dojang とかありました。

今回は、可能であれば、JavaScript 製エンジンもあるテンプレート書式を選びたいと思っています。というのも、もともと Node.js で動くブログ構築アプリケーション Hexo を利用しているので、テンプレートファイルも共通化できたらうれしいのです。

ついでに言うと、

  • オリジナルのヘルパー関数を定義して使いたい(できればクロージャーが使えるとうれしい)
  • テンプレートはコンパイル時ではなく実行時に読み込みたい(テンプレートの作成が楽になるので)

とかいうリクエストもあります。

今回は、Tera と Handlebars を試してみることにしました。

Tera を試す

とりあえず、テンプレートエンジンにデータを渡す方法と、オリジナルのヘルパー関数を定義して使う方法を調べました。

テンプレートファイルを書いてみると、こんなかんじ:

This is a sample for {{ person.first_name }}.

list:
{% for x in people -%}
  {{loop.index}}. {{x.first_name}} / {{x.last_name}}
{% endfor %}

helper:
{{ my_helper(name="Alice") }}
{{ my_helper(name="Bob") }}

んで、書いてみた Rust コードが以下のもの:

use std::collections::HashMap;

use serde::Serialize;
use tera::{Context, Tera};

#[derive(Debug, Clone, Serialize)]
struct Person {
    first_name: String,
    last_name: String,
}

#[derive(Debug, Clone, Serialize)]
struct Locals {
    people: Vec<Person>,
}

fn main() {
    println!("Hello, world!");

    let mut tera = match Tera::new("templates/**/*.html") {
        Ok(t) => t,
        Err(e) => {
            eprintln!("Tera parsing error: {:?}", e);
            std::process::exit(1);
        }
    };
    tera.autoescape_on(vec![]); // disable auto-escaping

    let locals = Locals {
        people: vec![
            Person {
                first_name: "Alice".into(),
                last_name: "1990".into(),
            },
            Person {
                first_name: "Bob".into(),
                last_name: "1980".into(),
            },
        ],
    };
    let mut context = Context::new();
    context.insert("person", &locals.people[0]);
    context.insert("people", &locals.people);

    {
        let target = locals.people[0].clone();
        let unknown = Person {
            first_name: "Unknown".into(),
            last_name: "0".into(),
        };
        tera.register_function("my_helper", Box::new(move |args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
            let arg = match args.get("name") {
                Some(v) => match tera::from_value::<String>(v.clone()) {
                    Ok(v) => v,
                    Err(_) => return Err("my_helper: invalid argument".into()),
                },
                None => return Err("my_helper: one argument should be specified".into()),
            };
            let person = if arg == target.first_name {
                &target
            } else {
                &unknown
            };
            Ok(tera::to_value(format!("{:?}", person)).unwrap())
        }));
    }

    let output = tera.render("index.html", &context).expect("Tera render error");
    println!("{}", output);
}

ちなみに、Rust の Cargo.toml には以下の依存クレート指定が必要です:

serde = { version = "1.0.134", features = ["derive"] }
serde_json = "1.0.75"
tera = "1"

やりたかったことは、ひととおりできそうな感じです。ただ、ヘルパー関数を呼び出すときの書式がちょっと好きじゃないかも。 my_helper(name="Bob") という「名前付き引数」指定が必須なのが、ちょっと冗長な気がする。

Handlebars を試す

テンプレートファイルを書いてみる:

This is a sample for {{ person.first_name }}.

list:
{{#each people }}
  {{@index}}. {{this.first_name}} / {{this.last_name}}
{{/each }}

helper:
{{my_helper "Alice" }}
{{my_helper "Bob" }}

Rust のコード:

use anyhow::Result;
use handlebars::{Context, Handlebars, Helper, JsonRender, HelperResult, RenderContext, RenderError, Output};
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
struct Person {
    first_name: String,
    last_name: String,
}

#[derive(Debug, Clone, Serialize)]
struct Locals {
    person: Person,
    people: Vec<Person>,
}

fn main() -> Result<()> {
    println!("Hello, world!");

    let mut handlebars = Handlebars::new();
    handlebars.set_strict_mode(true);
    handlebars.set_dev_mode(true);
    handlebars.register_escape_fn(handlebars::no_escape); // disable HTML escaping

    handlebars.register_templates_directory(".html", "templates/")?;

    let locals = Locals {
        person:
            Person {
                first_name: "Charlie".into(),
                last_name: "2000".into(),
            },
        people: vec![
            Person {
                first_name: "Alice".into(),
                last_name: "1990".into(),
            },
            Person {
                first_name: "Bob".into(),
                last_name: "1980".into(),
            },
        ],
    };

    {
        let target = locals.people[0].clone();
        let unknown = Person {
            first_name: "Unknown".into(),
            last_name: "0".into(),
        };
        handlebars.register_helper("my_helper", Box::new(move |h: &Helper, _: &Handlebars, context: &Context, _: &mut RenderContext, out: &mut dyn Output| -> HelperResult {
            let arg = h.param(0).ok_or(RenderError::new("my_helper: one argument should be specified"))?;
            let arg = arg.value().render();
            let person = if arg == target.first_name {
                &target
            } else {
                &unknown
            };
            let val = context.data().get("person").unwrap().get("last_name").unwrap().render().parse::<usize>()?; // check a variable in the context
            out.write(&format!("{:?} and {:?}", person, val))?;
            Ok(())
        }));
    }

    let output = handlebars.render("index", &locals)?;
    println!("{}", output);
    Ok(())
}

Rust の Cargo.toml に書く依存クレートの指定:

[dependencies]
anyhow = "1.0.52"
handlebars = { version = "4.2.1", features = ["dir_source"] }
serde = { version = "1.0.134", features = ["derive"] }
serde_json = "1.0.75"

こちらも、とりあえずやりたかったことはできました。独自のヘルパー関数に渡される情報(引数)が多いですね。テンプレートの書式はちょっと Lisp っぽいところがありますが、キライではない。

まとめ

今回は Handlebars を使ってみようと思います。