Docker で Haskell アプリケーションを作成 - MongoDB 利用

MongoDB へ接続する Haskell アプリケーションを Docker で作成してみました。

以下の Docker イメージを使用します。

ビルドツールは stack を使って、MongoDB への接続には以下のライブラリを使います。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20170529/

(1) Docker コンテナの実行

Docker で MongoDB と Haskell のコンテナを実行します。

コンテナ間の連携には、deprecated となっている Link 機能(–link)は使わずにユーザー定義ネットワークを使う事にします。

(1.1) bridge ネットワークの作成

bridge ネットワークを新規作成します。

$ docker network create --driver bridge br1

(1.2) MongoDB コンテナの実行

作成したネットワークへ参加するように --net=<ネットワーク名> を使って MongoDB を docker run します。

$ docker run -d --name mongo1 --net=br1 mongo

(1.3) Haskell コンテナの実行

Haskell も同様に docker run します。

ここでは Docker ホスト側の /vagrant/work をコンテナの /work へマウントしています。

$ docker run -it --name hs1 --net=br1 -v /vagrant/work:/work haskell /bin/bash
root@・・・# 

確認のため Haskell コンテナ内から MongoDB のコンテナへ ping してみます。

ping
root@・・・# ping mongo1
PING mongo1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.640 ms
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.257 ms
・・・

(2) Haskell アプリケーションの作成

Haskell のコンテナで MongoDB へ接続するアプリケーションを作成します。 (以降の処理は (1.3) で起動した Haskell コンテナ内で実施します)

(2.1) プロジェクトのひな型作成

stack new <プロジェクト名> [<テンプレート名>] でプロジェクトのひな型を作ります。

プロジェクト作成
root@・・・# cd /work

root@・・・# stack new sample1
Downloading template "new-template" to create project "sample1" in sample1/ ...
・・・

プロジェクト名のディレクトリが作られ、各種ファイルが生成されます。

(2.2) 依存ライブラリの取得

MongoDB driver for Haskell を使用するため、<プロジェクト名>.cabal ファイルの library/build-depends へ mongoDB を追記します。

今回は Control.Monad.Trans も使うので mtl も追記しています。

sample1/sample1.cabal
・・・
library
  ・・・
  build-depends:       base >= 4.7 && < 5
                     , mongoDB
                     , mtl
  default-language:    Haskell2010
・・・

stack build でビルドすると未取得の依存ライブラリをダウンロードします。

プロジェクトのビルド(依存ライブラリの取得)
root@・・・# cd sample1

root@・・・# stack build --allow-different-user
・・・
mtl-2.2.1: download
mtl-2.2.1: configure
mtl-2.2.1: build
mtl-2.2.1: copy/register
・・・

ここで --allow-different-user オプションを付けていますが、今回のケースではこのオプションを付ける必要がありました。(-v でマウントしたディレクトリを使用した事が原因だと思われる)

なお、–allow-different-user 無しで stack build すると以下のようになりました。

root@・・・# stack build
You are not the owner of '/work/sample1/'. Aborting to protect file permissions.Retry with '--allow-different-user' to disable this precaution.

(2.3) MongoDB 接続アプリケーションの実装

とりあえず someFunc という関数名をそのまま使う事にします。(変更する場合は app/Main.hs も変更します)

OverloadedStringsExtendedDefaultRules の言語拡張が最低限必要となるようです。

MongoDB driver for Haskell では、MongoDB に対する処理を Action m a (MonadIO m) で定義し access 関数で処理すればよさそうです。

ここで Actiontype Action = ReaderT MongoContext と定義されています。

MongoDB のコレクションへ複数ドキュメントを追加するには insertManyinsertMany_ 関数が使えます。(ドキュメント追加の用途では insertinsertAll 関数等もあります)

関数名の最後の _ の有無は、作成したドキュメントの _id の値を返すかどうかの違いのようです。(_ の付いている方は返さない)

今回は _id の値は不要なので insertMany_ の方を使いました。

MongoDB へ登録するドキュメントは [<項目名> =: <値>, ・・・] という形式で定義できます。

find 関数の結果は Action m Cursor なので、rest 関数(Cursor -> Action m [Document]) をバインド(>>=)して Action m [Document] を取得しています。

接続先の MongoDB ホスト名は MONGO_HOST 環境変数から取得するようにしてみました。

sample1/src/Lib.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExtendedDefaultRules #-}

module Lib
    ( someFunc
    ) where

import System.Environment
import Control.Monad.Trans (lift)
import Database.MongoDB 

someFunc :: IO ()
someFunc = do
    mongo <- mongoHost
    -- MongoDB 接続
    pipe <- connect (host mongo)

    access pipe master "sample" proc

    close pipe

mongoHost :: IO String
mongoHost = getEnv "MONGO_HOST"

proc :: Action IO ()
proc = do
    -- items コレクションへドキュメント追加
    insertMany_ "items" [
        ["name" =: "item1", "value" =: 1],
        ["name" =: "item2", "value" =: 2] ]

    allItems >>= printDocs

-- items コレクションの全ドキュメント取得
allItems :: Action IO [Document]
allItems = rest =<< find ( select [] "items" )

-- ドキュメントの出力
printDocs :: [Document] -> Action IO ()
printDocs docs = lift $ mapM_ print docs

(2.4) ビルドと実行

それでは、ビルドして実行します。

ビルド
root@・・・# stack build --allow-different-user
・・・
/work/sample1/.stack-work/install/x86_64-linux/lts-8.15/8.0.2/bin
Registering sample1-0.1.0.0...

.stack-work ディレクトリへ(ビルドの)成果物が生成されます。

実行するには stack exec <実行ファイル名> で実行するか、実行ファイル(例 .stack-work/install/x86_64-linux/lts-8.15/8.0.2/bin/sample1-exe)を直接実行します。

実行ファイル名はデフォルトで <プロジェクト名>-exe となるようです。

今回は環境変数から MongoDB のホスト名を取得するようにしたので、MONGO_HOST 環境変数へ MongoDB のコンテナ名を設定してから実行します。

実行
root@・・・# export MONGO_HOST=mongo1

root@・・・# stack exec sample1-exe --allow-different-user
[ _id: 592a7ffb2139612699000000, name: "item1", value: 1]
[ _id: 592a7ffb2139612699000001, name: "item2", value: 2]