理系学生日記

おまえはいつまで学生気分なのか

dev containerを使って開発環境をコンテナに封じ込める

気になっていたのに触っていなかった技術としてdev containerがあった。

Dev Containerはその名前の通り、開発環境をコンテナ化する技術。 そうすると「開発環境とは何か」という話になるわけだけど、ここでいう開発環境とは「コードを書くための環境」であり、それは「コードを書くためのツールやライブラリ」を含む環境でもある。

アーキテクチャ

そうはいっても結局どういうことなのよという疑念は拭えないわけなので、より具体的なアーキテクチャを示す必要がある。以下はDeveloping inside a Container using Visual Studio Code Remote Developmentから引用したもの。

Dev Container概要

コンテナの中に、VS Code Serverがあり、ソースコードはローカルPCのものをコンテナにVolume Mountする。VS Codeの(UI周りを除く)拡張もコンテナ側で動作させる。これによって、ソフトウェア開発者がプロジェクトごとに必要なツールやライブラリを含んだ環境を簡単に構築、共有、再現できるようにしている。

もちろん、VS Codeだけあっても開発環境としては不十分なので、NodeやPythonといった言語のランタイムや、デバッガ等のツールも含めてコンテナに閉じ込める。 このアプローチにより、以下の問題が解決される。少なくとも解決に近づく。

  • "私の環境では動作するがあなたの環境では動作しない"という問題の抑止
  • 同一チームの中での開発環境構築をコード管理でき、構築作業も簡略化できる
  • ローカルPCにツールを逐一インストールする必要がなくなり、PC環境を汚さなくて済む

設定ファイル

もちろん、この環境をチマチマと手で作っていたのでは結局環境構築の手間が大きくなってしまうし、コード管理できない。そこで、Dev Container metadataの仕様が決まっており、これに従って設定ファイルを作成することで、環境を構築できる。具体的には、.devcotainerディレクトリ配下の.devcontainer.jsonへ記述することになる。

実際にブログ記述環境に使ってみる

このブログを記述する環境も、実は以下のようなランタイムやツールを必要としていた。

というわけで、手始めにこのブログ記述環境をDev Container化してみる過程で、Dev Container化というのはどういうことかを理解することにする。

コンテナをどう作成するか

Dev Containerの一般的なテンプレートはTemplatesあたりに用意されているため、直接これを使っても良い。 他のオプションとしてはDockerfileを指定する、あるいは、Docker composeのファイルを指定するという方法もある。今回はDB等のサービスは使わないので、Dockerfileを指定する方法で進める。

Dockerfileを作成しツールをインストールする

nodeのRuntimeが必要だったので、Node.js Development Container Imagesを使うことにした。

dev containerに使うといっても、Dockerfileを使う以上dev containerを特別に意識することはない。 途中でhadolintとblogsyncをインストールする必要があったので、最終的に以下のようなDockerfileが爆誕した。

FROM mcr.microsoft.com/devcontainers/javascript-node:dev-20-bookworm

ENV BLOGSYNC_VERSION v0.20.1
ENV HADOLINT_VERSION v2.12.0

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
    # Install hadolint
    && curl -SL "https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-arm64" -o /usr/local/bin/hadolint \
    && chmod +x /usr/local/bin/hadolint \
    # Install blogsync
    && curl -SL "https://github.com/x-motemen/blogsync/releases/download/${BLOGSYNC_VERSION}/blogsync_${BLOGSYNC_VERSION}_linux_arm64.tar.gz" | tar -xzC /tmp/ \
    && mv /tmp/blogsync_${BLOGSYNC_VERSION}_linux_arm64/blogsync /usr/local/bin/ \
    && apt-get autoremove -y \
    && rm -rf /var/lib/apt/lists/*

VS Codeの拡張をインストールする

VS Codeの拡張をコンテナ側にインストールするには、.devcontainer.jsoncustomizations.vscode.extensionsを追加する。

devcontainer.jsonは一応IDEに依存しない仕様となっているが、customizations配下はプロダクト固有の設定を書いて良いことになっている。costomizations.vscodeはVS Code用の設定として、インストールすべき拡張が指定できる。 この辺りの詳細はSupporting tools and servicesを参照。

    "customizations": {
        "vscode": {
            "extensions": [
                "GitHub.copilot-chat",
                "GitHub.copilot",
                (snip)
            ]
        }
    }

逐一、必要な拡張を記述していくのは骨が折れるが、VS Codeの拡張機能メニューのコンテキストメニューから直接.devcontainer.jsonに追加できる。

拡張を.devcontainer.jsonに追加

Workspaceに依存するツールを導入する

単純にDev containerを定義するシンプルなmetadataは次のようになった。 Dockerfileを指定するためにはbuild.dockerfileを指定すれば良い。

{
    "name": "はてなブログエディタ",
    "build": {
        "dockerfile": "Dockerfile"
    },
    "forwardPorts": [
        3000
    ],
    "postCreateCommand": "npm install",
}

導入対象ツールであるtextlintはWorkspaceにあるpackage.jsonで定義されている。これをコンテナに導入するためにはpostCreateCommandnpm installを実行する。

DockerfileでRUN npm installすれば良いじゃねーかという話はあるが、これはうまくいかない。 これは、VS CodeのWorkspaceにあるファイル(ここではpackage.json)はコンテナが作成された後でマウントされるため。

the Dockerfile runs before the dev container is created and the workspace folder is mounted and therefore does not have access to the files in the workspace folder. A Dockerfile is most suitable for installing packages and tools independent of your workspace files.

Create a development container using Visual Studio Code Remote Development

このためにpostCreateCommandというフックが用意されている。

Workspace外にある設定ファイルをコンテナに持ち込む

"開発環境"はVS CodeのWorkspaceに閉じない。$HOME/.config (See: XDG Base Directory Specification)など、Workspace外にある設定ファイルをコンテナに持ち込まなければならないケースがある。実際、blogsyncは$HOME/.config/blogsync`に設定ファイルを置くことが想定されている。

これを実現するためには、.devcontainer.jsonの中でmountsを使う。mountsはホスト側のファイルをコンテナにマウントするための設定。Dockerのマウント設定そのもの。

    "mounts": [
        {
            "source": "${localEnv:HOME}/.config/blogsync",
            "target": "/home/node/.config/blogsync",
            "type": "bind"
        }
    ]

あとはコンテナをビルドして、Workspaceをコンテナ側で開けば良い。

コンテナで開く

Workspaceを開いた後、blogsyncの設定ファイルがコンテナにマウントされていることが確認できる。

$ whoami; ls ~/.config/blogsync
node
config.yaml

使ってみて

コンテナの中で動作しているとは思えないほど軽快に動く。ローカルで開発をしている体験と何ら変わらない。 全員が同じ環境で開発を進められるというのは、プロジェクトにとってはありがたい。 このエントリもDev Containerを使って書いた。まったく違和感がない。

VS Code派とIntelliJ派が混在する環境ではどうなのだろう。