Docker開発環境(6): docker-buildenvによるアプリケーション実行環境
前回の続きです。
これまで docker-buildenv を用いた組み込みファームウェアのビルド環境をメインに解説してきましたが、今回はアプリケーション実行環境として利用する応用を見ていきます。
組み込みファームウェア向けのビルド環境の目的は
- Docker イメージを知っていればファームウェアのビルドができる
というところにありましたが、アプリケーション実行環境の目的は
- Docker イメージを知っていればアプリケーションの実行ができる
というところにあります。
また、ファームウェアのビルドでは、誰でも、同じ環境で、すぐに、ビルドが始められるというところに力点がありましたが、今回のアプリケーション実行環境ではこれに加えて開発環境とデプロイ環境の一致という観点も増えるのがポイントです。
早速見ていきましょう。
docker-poetry-builder
まずは poetry を使った Python アプリケーションの実行環境を作っていきます。こちらも Yocto のときと同様に、既に作成済みのものが存在します。
まずは使ってみる
今回も完成イメージを掴むために、まずは完成品を使ってみるところから始めてみます。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
があります。
extract
が git clone
に対応し、build
が poetry 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 を作ります。
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
でソースコードを取得します。
Get source code from the repository.
$ git clone ${GIT_BRANCH:+-b $GIT_BRANCH} ${GIT_REPO}
次に setup
ですが、clone したリポジトリに cd
するだけ(ただし再実行しても無害なように)ですね。
Change into the project directory.
$ [[ -e pyproject.toml ]] || cd $(basename -s .git "${GIT_REPO}")
意味的には「pyproject.toml がなければ cd する」なんですが、途中に false で評価される文があると実行エラーになってしまうので、&&
ではなく ||
でつなげる書き方をしています。このあたりの書き方はもう少し改善の余地があるかもしれません。
build
では poetry install
で必要なパッケージをインストールします。
Run setup before build.
$ . <(buildenv setup)
Install dependencies.
$ poetry install
最後に run
は Yocto などの組み込みファームウェア構築環境ではなかったものですが、poetry run
で何かコマンドを走らせます。何を走らせるのかは $DEFAULT_SCRIPT
で指定できるものとします。
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
としてすべての依存関係をホストからマップされるカレントディレクトリに閉じるようにしている以外は、これまでとあまり大きな差はありません。
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 は以下に作成済みのものがあります。
コンテナに入って操作することもできるのですが、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 がセットアップされています。
docker-npm-builder は同様の考え方で npm がセットアップされたアプリケーション実行環境です。poetry install
の代わりに npm install
になるくらいのイメージで間違ってないです。
docker-yarn-builder は docker-npm-builder の亜種で、npm ではなく yarn を使います。
実際に docker-yarn-builder を使ってセットアップされた環境として、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