🌟

Docker開発環境(6): docker-buildenvによるアプリケーション実行環境

2021/08/12に公開

前回の続きです。

https://zenn.dev/anyakichi/articles/2e2cfcb9af7728

これまで docker-buildenv を用いた組み込みファームウェアのビルド環境をメインに解説してきましたが、今回はアプリケーション実行環境として利用する応用を見ていきます。

組み込みファームウェア向けのビルド環境の目的は

  • Docker イメージを知っていればファームウェアのビルドができる

というところにありましたが、アプリケーション実行環境の目的は

  • Docker イメージを知っていればアプリケーションの実行ができる

というところにあります。

また、ファームウェアのビルドでは、誰でも、同じ環境で、すぐに、ビルドが始められるというところに力点がありましたが、今回のアプリケーション実行環境ではこれに加えて開発環境とデプロイ環境の一致という観点も増えるのがポイントです。

早速見ていきましょう。

docker-poetry-builder

まずは poetry を使った Python アプリケーションの実行環境を作っていきます。こちらも Yocto のときと同様に、既に作成済みのものが存在します。

https://github.com/anyakichi/docker-poetry-builder

まずは使ってみる

今回も完成イメージを掴むために、まずは完成品を使ってみるところから始めてみます。poetry を使って構築されたアプリケーションは、特別な外部の依存関係さえなければ概ね以下のように動作させることができると思います。

$ git clone poetry-app
$ cd poetry-app
$ poetry install
$ poetry run [何か動かす対象]

厳密に言うと、ここに動作対象となる Python のバージョンの想定も必要です。

これを、docker-poetry-builder を用いると以下のようになります。

$ mkdir poetry-app-1 && cd $_
$ din anyakichi/poetry-builder:3.9 extract -y
$ din anyakichi/poetry-builder:3.9 build -y
$ din anyakichi/poetry-builder:3.9 poetry run [何か動かす対象]

ここでは Python 3.9 の含まれる anyakichi/poetry-builder:3.9 を使っています。Python 3.8 を使いたい場合は anyakichi/poetry-builder:3.8 があります。

extractgit clone に対応し、buildpoetry install に対応しています。最後の動かす部分についてはどうしてもアプリケーション依存になってしまうので、docker-poetry-builder でも一般化することができません。

アプリケーションを構築してみる

run の部分を一般化するためには、そのようにアプリケーションも作っておく必要があります。docker-poetry-builder 本体の構成方法についてはもう少しあとに回して、一旦アプリケーションを作ってみます。ここでは FastAPI を使ったチュートリアルレベルのアプリケーションを作ってみましょう。

まずはコンテナを使ってアプリケーションの雛形を作ります。Python は 3.9 を使うことにしましょう。

$ din anyakichi/poetry-builder:3.9 poetry new fastapi-sample
$ cd fastapi-sample

fastapi, poethepoet, uvicorn を依存関係に追加します。poethepoet はタスクランナーとして使用します。

$ din anyakichi/poetry-builder:3.9 poetry add fastapi poethepoet uvicorn

せっかくなので dev-dependencies にもいろいろ足しておきましょう。

$ din anyakichi/poetry-builder:3.9 poetry add --dev \
    black flake8 flake8-black flake8-bugbear flake8-isort isort mypy

次に fastapi_sample/__init__.py をこのように編集しましょう。JSON で "Hello World" を返すだけのアプリケーションです。

$ vi fastapi_sample/__init__.py
$ cat fastapi_sample/__init__.py
__version__ = "0.1.0"

from typing import Any, Dict

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root() -> Dict[str, Any]:
    return {"message": "Hello World"}

好みもあるところだと思いますが、pyproject.toml にこれくらい書き足しておきます。

[tool.poe.tasks]
fmt = { shell = "isort fastapi_sample tests; black fastapi_sample tests" }
lint = { shell = "flake8 fastapi_sample tests && mypy fastapi_sample tests"}
start = "uvicorn --host 0.0.0.0 fastapi_sample:app"
test = "pytest"

[tool.isort]
profile = "black"

[tool.mypy]
python_version = "3.9"
strict = true
implicit_reexport = true
ignore_missing_imports = true

とりあえず black と isort でソースコードをフォーマットしてみましょう。

$ din anyakichi/poetry-builder:3.9 poetry run poe fmt

tests 以下がちょっと修正されるかもしれません。同様に lint。

$ din anyakichi/poetry-builder:3.9 poetry run poe lint

やはり同様に tests 以下が怒られているかもしれません。まあひとまず気にせずサンプルを起動してみましょう。-p オプションを使ってローカルホストにポートをマップします。

$ din -p 8000:8000 anyakichi/poetry-builder:3.9 poetry run poe start

別のウインドウから curl でアクセスしてみます。

$ curl http://localhost:8000/
{"message":"Hello World"}

うまく動いていそうです。

ここで最後の仕上げに、このアプリケーション用の Dockerfile を作ります。

Dockerfile
FROM anyakichi/poetry-builder:3.9

ENV DEFAULT_SCRIPT="poe start"

ビルドして使ってみましょう。

$ docker build -t builder .
$ din -p 8000:8000 builder run -y

docker-poetry-builder に組み込まれている run が動くようになりました。

docker-poetry-builder を作ってみる

それでは実際に docker-poetry-builder 本体を作ってみましょう。基本的には Yocto のときと大きくは変わりません。extract については git clone でソースコードを取得します。

buildenv.d/extract.40.md
Get source code from the repository.

    $ git clone ${GIT_BRANCH:+-b $GIT_BRANCH} ${GIT_REPO}

次に setup ですが、clone したリポジトリに cd するだけ(ただし再実行しても無害なように)ですね。

buildenv.d/setup.40.md
Change into the project directory.

    $ [[ -e pyproject.toml ]] || cd $(basename -s .git "${GIT_REPO}")

意味的には「pyproject.toml がなければ cd する」なんですが、途中に false で評価される文があると実行エラーになってしまうので、&& ではなく || でつなげる書き方をしています。このあたりの書き方はもう少し改善の余地があるかもしれません。

build では poetry install で必要なパッケージをインストールします。

buildenv.d/build.40.md
Run setup before build.

    $ . <(buildenv setup)

Install dependencies.

    $ poetry install

最後に run は Yocto などの組み込みファームウェア構築環境ではなかったものですが、poetry run で何かコマンドを走らせます。何を走らせるのかは $DEFAULT_SCRIPT で指定できるものとします。

buildenv.d/run.40.md
Run setup before run.

    $ . <(buildenv setup)

Run default script.

    $ poetry run ${DEFAULT_SCRIPT}

最後に Dockerfile ですが、今回は python の Docker イメージをそのまま流用することにします。将来的な拡張性を見て wait-for-it をインストールしていること、poetry config virtualenvs.in-project true としてすべての依存関係をホストからマップされるカレントディレクトリに閉じるようにしている以外は、これまでとあまり大きな差はありません。

Dockerfile
ARG pyversion=latest
FROM python:${pyversion}

RUN \
  apt-get update \
  && DEBIAN_FRONTEND=noninteractive apt-get install -y \
    git \
    gosu \
    sudo \
    wait-for-it \
  && rm -rf /var/lib/apt/lists/*

RUN pip3 install poetry

RUN \
  useradd -ms /bin/bash builder \
  && echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

USER builder
RUN \
  echo '. <(buildenv init)' >> ~/.bashrc \
  && git config --global user.email "builder@poetry" \
  && git config --global user.name "poetry builder" \
  && poetry config virtualenvs.in-project true

USER root
WORKDIR /home/builder

ENV \
  DEFAULT_SCRIPT=python \
  GIT_REPO="" \
  LANG=C.UTF-8

COPY buildenv/entrypoint.sh /buildenv-entrypoint.sh
COPY buildenv/buildenv.sh /usr/local/bin/buildenv

COPY buildenv/buildenv.conf /etc/
COPY buildenv.d/ /etc/buildenv.d/

RUN sed -i 's/^#DOTCMDS=.*/DOTCMDS=setup/' /etc/buildenv.conf

ENTRYPOINT ["/buildenv-entrypoint.sh"]
CMD ["/bin/bash"]

fastapi-sample まとめ

今回作成した fastapi-sample は以下に作成済みのものがあります。

https://github.com/anyakichi/fastapi-sample

コンテナに入って操作することもできるのですが、Python の場合は以下のように直接実行モードで使うのがおすすめです。

$ din anyakichi/fastapi-sample:main extract -yf
$ cd fastapi-sample
$ din anyakichi/fastapi-sample:main build -y
$ din -p 8000:8000 anyakichi/fastapi-sample:main run -y

Python の virtualenv は絶対パスベースでセットアップされてしまうので、上記のように cd fastapi-sample してから build した場合と、

$ din anyakichi/fastapi-sample:main extract -yf
$ din anyakichi/fastapi-sample:main build -y

のように cd せずに build した場合とで、相互に動かない(run ができない)という欠点があります(前者は /build 以下に、後者は /build/fastapi-sample 以下にソースツリーが展開された形になる)。Python の場合は Yocto の場合のようにコンテナ内で違うコマンドが叩きたいということが少ないので、直接実行モードで編集しながら fmt, lint をかけていくスタイルのうほうが実際の開発がしやすいです。

ちなみに、上記の fastapi-sample は din による開発環境としてではなく、直接実行環境としても使えるようにセットアップしてあります。以下のように実行してもアプリケーションが起動できます(終了するときは docker stop してください)。

$ docker run --rm -p 8000:8000 anyakichi/fastapi-sample:main run -y

ソースコードはダウンロードしていないのにどうして…、と思うところですが、これは anyakichi/fastapi-sample:main の Docker イメージ内に動くようにセットアップされたソースツリーが同梱されているためで、din を使ってカレントディレクトリを /build に持っていかなくても /build にはソースツリーが展開されています。

$ docker run --rm anyakichi/fastapi-sample:main ls -a /build

このハイブリッド形式には以下の利点があります。

  • ビルドをしなくても動作だけは見ることができる。
  • 実際にデプロイする環境としてもそのまま使うことができる。

欠点としては Docker イメージが肥大化するのですが、もともとビルド環境として作っているもので、余計なものがそれなりに入っているのはそういう前提なので、ランタイム環境が入る程度の肥大化は許容範囲ではないか、という割り切りがあります。

どのみちイメージサイズを気にするのであれば python も slim をベースにするのでしょうし、そちらを :slim タグで作っておくくらいで良いのではないかと思っています。

fastapi-sample 自体は DB は使わないのですが、一応 postgres と連携するための docker-compose.yml のサンプルも同梱されています(wait-for-it コマンドを入れてあったのは、DB の起動待ちがしやすいようにでした)。

その他のアプリケーション実行環境

docker-pipenv-builder は poetry の代わりに pipenv がセットアップされています。

https://github.com/anyakichi/docker-pipenv-builder

docker-npm-builder は同様の考え方で npm がセットアップされたアプリケーション実行環境です。poetry install の代わりに npm install になるくらいのイメージで間違ってないです。

https://github.com/anyakichi/docker-npm-builder

docker-yarn-builder は docker-npm-builder の亜種で、npm ではなく yarn を使います。

https://github.com/anyakichi/docker-yarn-builder

実際に docker-yarn-builder を使ってセットアップされた環境として、webapp-template があります。

https://github.com/anyakichi/webapp-template

webapp-template は ReasonML/OCaml で React アプリケーション開発を始めるためのテンプレートなのですが(create-react-app のシンプル版で、一応 TypeScript も使えるようにセットアップしてある)、Python の fastapi-sample と同じような方法で使用できます。

ビルド・実行のハイブリッドイメージとして構成されているので、ひとまず実行したらどうなるのかを見たければ以下のように din は使わず直接 docker run します(ちょっと手抜きして Python の http.server を使っているので、プロダクション向きではないです)。

$ docker run -p 8080:8080 --rm anyakichi/webapp-template:main run -y

開発環境として使う場合は、din を使ってセットアップします。

$ din anyakichi/webapp-template:main extract -yf
$ cd webapp-template
$ din anyakichi/webapp-template:main build -y
$ din -p 8080:8080 anyakichi/webapp-template:main run -y

アプリケーション開発に docker-buildenv を使う意義

冒頭でも触れたとおり、アプリケーション実行環境として docker-buildenv を使うとデプロイ環境とまさに同じ環境で開発が行えるようになります。Python や Node のバージョンもなんとか env ではなく Docker イメージのレベルで固定されるので、開発者による環境の揺れもほぼ排除されます。

アプリケーションの実行環境として Docker を使うこと自体はかなり一般的だと思いますが、ここでの主要なメリットはターゲットのアプリケーションを動くとわかっている環境に固定できること、なおかつポータブルに持ち運べることではないかと思います。しかしこのメリットを運用時にだけしか使わないのはもったいなく、開発時にも拡張してしまおうというのが docker-buildenv 化した場合の強みになります。

次回は docker-buildenv の第3の活用形態、編集環境に docker-buildenv を使う応用を見ていきます。

Discussion