やりたいこと
haskellで実装したdb依存のアプリケーションをdockerコンテナ化したい。
haskellのdbライブラリにhaskell-relational-record (HRR)がある。そこそこ柔軟にクエリを組み立てられる上、コンパイル時にdbからテーブル情報を取得して自動的にデータ型を生成する機能がとても便利である。
アプリをコンテナ化する際、アプリとdbを同じコンテナに共存させることは避けたい。
本記事では、アプリのdockerイメージビルド時に外部のdbコンテナに接続することで、アプリ単体のコンテナを作る方法を示す。
できたもの
dockerコンテナを起動して、
http://localhost:8080
にアクセスするとdbから会社一覧を取得してテキスト表示するだけのhaskellアプリ。
メモ
Dockerイメージ作成の流れ
以下のコマンドは、build.shの一部。
I.
コンテナ間通信をするためにdockerのネットワーク(bridge)を作成。
docker network create --driver=bridge $NETWORK
II.
スキーマとテーブルのみを定義したdbコンテナを起動。
本例ではpostgresを利用する。
/docker-entrypoint-initdb.dにsqlファイルをマウントするとコンテナ起動時に実行される。
docker run --net $NETWORK --name $DB_CONTAINER_NAME --expose 5432 -v $(pwd)/schema.sql:/docker-entrypoint-initdb.d/schema.sql -e POSTGRES_USER=$POSTGRES_USER -e POSTGRES_DB=$POSTGRES_DB -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD -d postgres:11.5
III.
アプリケーションをビルド。
Dockerfileでは、ARGでdb接続に必要な情報を要求し、マルチステージビルドで最終的なイメージ容量を削減している。
本例では約5GB(GHCや依存パッケージなど含む)から100MB程(主にバイナリ)になった。
docker build --network $NETWORK -t $APP_CONTAINER_NAME --build-arg DB_HOST=$DB_CONTAINER_NAME --build-arg POSTGRES_USER=$POSTGRES_USER --build-arg POSTGRES_DB=$POSTGRES_DB --build-arg POSTGRES_PASSWORD=$POSTGRES_PASSWORD .
Dockerコンテナの起動
本例ではdocker-composeを用いて起動する。
appコンテナとdbコンテナに与える環境変数(POSTGRES_USER、POSTGRES_DB、POSTGRES_PASSWORD)は一致させる必要がある。
イメージビルド時に与えた値と異なっていても問題ない。
docker-composeではコンテナ間のネットワークが自動的に作られる。
dbコンテナへはホスト名"db"で接続できる。
そのためappコンテナの環境変数DB_HOSTに"db"を与えている。
dbコンテナにはアプリイメージ作成時に用いたものと同じsqlファイルをマウントする。
これに加えて、本例では初期データとしてseed.sqlもマウントしている。
Haskellのコード
環境変数を読んでpostgresへの接続を作成する関数。
connect :: IO Connection
connect = do
host <- getEnv "DB_HOST"
user <- getEnv "POSTGRES_USER"
db <- getEnv "POSTGRES_DB"
password <- getEnv "POSTGRES_PASSWORD"
let conninfo = unwords
[ "host=" <> host
, "user=" <> user
, "dbname=" <> db
, "password=" <> password
]
connectPostgreSQL conninfo
そのほかhrrの記述や表示用データ型の記述などは、直接リポジトリを参照されたい。
コード量は少ない。
まとめ
本記事ではdb接続のあるhaskellアプリケーションのdockerコンテナを単体で作る方法を一つを示した。
hrrはpostgresの他にもmysql,sqlserver,oracleなども対応しているようなので、dbありきのシステム上にhaskellプログラムを実装するということもやり易い。
haskellプログラムを100MB程度のコンテナにできたことで、例えばkubernetesで構築された既存マイクロサービスの一部をhaskellで置き換えたり新しく作ったり割と気軽にできるような気がしてくる。
本例ではwaiを使った最小限のwebアプリを作成したが、servantを使えば例えばspaのバックエンド、spockやlucidを使えば例えば小規模のwebサービスなども作れる。
何となく重たいイメージのあるhaskellプログラムでも、コンテナ化さえしてしまえば、サーバー運用においてはよく使われる他の言語で作ったプログラムと同じような気持ちで扱えるのではないだろうか。
補足
I.
macosやwindowsの場合、dockerネットワークを作成しなくてもhost.docker.internalでホストosにアクセスすることができるので、ホストos経由でdbコンテナに接続することでもアプリのdockerイメージを作成できる。
しかしlinuxの場合、今のところhost.docker.internalが使えないようなので、ci環境でビルドしたりテストしたりすることも考慮して本例ではdockerネットワークを作成する形にした。
II.
開発時はdocker上ではなくホストosでコンパイルするのが吉と思われる。
依存パッケージを追加したりreplで色々試したりするのをdocker上でやろうとすると多分面倒。
III.
なぜそこまでしてhaskellを使いたいか?
- 純粋な関数と副作用ありの関数を明確に区別してバグ少
- 型でうまく制約をかければもっとバグ少
- コンパイル通ったらしょうもないバグは多分ない!
構文の好みや圏論にあやかりたい気持ちもあるが、今のところバグの入り込む余地が少ないと感じるのが説得力ありそうな理由。
IV.
本例はmacosで実行した。
linuxやwindowsで実行する場合はbuild.shの微修正がいるかもしれない。
参考にしたQiita記事
docker multi-stage build