HaskellでもGoみたいにシングルバイナリでアプリケーションをデプロイしたい

More than 1 year has passed since last update.

Haskell (その2) Advent Calendar 2017の(9日目)の記事です。


モチベーション

業務でもHaskell使いたい!


前置き

業務ではGKEで主にScalaプログラムの開発を行なっています。

Kubernetes(Docker)を使う場合、imageサイズが大きいとデプロイに時間がかかり、リリースやCIが遅くなってしまいます。

プロジェクトでは、ベースにapline(14MB)を使い、JRE8/JDK8を入れると190MBくらいになり、アプリケーションを入れると350MBくらいになります。さらに4アプリケーションあるので全部で1.4GBくらいになります。

ScalaだとJVMが必要なのと、jarが大きくなりやすいのでimageサイズが大きくなりやすいです。

そうなってくると、Goのようにシングルバイナリだけで実行できるのはとても魅力的です。

最近はKubernetesとGoで開発するプロジェクトも社内で増えてきました。

Haskellでも同じことができれば、実務でも使えるのでは...


webアプリケーションを作ってみる

最初はこの記事をみてDockerをstack.yamlに指定してstack build --ghc-options='-optl-static -optl-pthread'を実行してみたのですが、うまくコンパイルできず、色々原因を調べていたのですが、ghc,gccあたりの知識が足らず、困っていました。

そしたら、@algas さんのHaskell on Docker で Portable CLI を作ろうで、pandocのDockerfileでやってるじゃないですか...

今回はWebアプリケーションとしてSpockのtutorialのサンプルをそのまま使います。

まず、下記でstackアプリケーションを作成します。

$ stack new spock-app

そして、app/Main.hsを下に書き換えます。

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Web.Spock
import Web.Spock.Config

import Control.Monad.Trans
import Data.Monoid
import Data.IORef
import qualified Data.Text as T

data MySession = EmptySession
data MyAppState = DummyAppState (IORef Int)

main :: IO ()
main =
do ref <- newIORef 0
spockCfg <- defaultSpockCfg EmptySession PCNoDatabase (DummyAppState ref)
runSpock 8080 (spock spockCfg app)

app :: SpockM () MySession MyAppState ()
app =
do get root $
text "Hello World!"
get ("hello" <//> var) $ \name ->
do (DummyAppState ref) <- getState
visitorNumber <- liftIO $ atomicModifyIORef' ref $ \i -> (i+1, i+1)
text ("Hello " <> name <> ", you are visitor number " <> T.pack (show visitorNumber))

Dockerfileとして下記を用意します。

FROM fpco/stack-build:lts-9.14

WORKDIR /usr/lib/gcc/x86_64-linux-gnu/5.4.0
RUN cp crtbeginT.o crtbeginT.o.orig
RUN cp crtbeginS.o crtbeginT.o
ADD ./ /work
WORKDIR /work
RUN stack setup
RUN stack --system-ghc --local-bin-path /sbin build --ghc-options '-optl-static -fPIC -optc-Os'

FROM alpine:latest
RUN mkdir /work
COPY --from=0 /work/.stack-work/install/x86_64-linux/lts-9.14/8.0.2/bin/spock-app /sbin/
CMD ["/sbin/spock-app"]

最後にcabalファイルの依存関係を直します。

プロジェクト全体はここにあります。

あとは下記コマンドでimageを作成します。

$ docker build --rm -t spock-app .

imageのサイズが23.6MBで結構小さくできました!

$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
spock-app latest 814e8c86669d 12 minutes ago 23.6MB

試しに下記のコマンドで実行して、 https://localhost:8080/ にアクセスするとちゃんとレスポンスが返ってくると思います。



docker run -d --name spock-app -d -p 8080:8080 spock-app


課題



  • ghcのダウンロードとフルコンパイルが毎回走るのでビルドがものすごく遅い...


    • コメントでもらった方法で大丈夫そう (初回のコンパイルはspockの依存が多いせいかちょっと遅いけど...)




【追記】 実は本当のシングルバイナリにはならないかも?

動いたから安心してすっかり忘れてたのですが、コンパイルログには下記のwarningが出てて、spockが依存しているwaiが依存しているnetworkにcのFFIがあり...

FFIがあるとそこはスタティックリンクにならないような...?

goだとこの記事のように意識して頑張っている?

Linking .stack-work/dist/x86_64-linux/Cabal-1.24.2.0/build/spock-app/spock-app ...

/root/.stack/snapshots/x86_64-linux/lts-9.14/8.0.2/lib/x86_64-linux-ghc-8.0.2/network-2.6.3.2-IsLM4TXcLoRI0fmmBYVyQz/libHSnetwork-2.6.3.2-IsLM4TXcLoRI0fmmBYVyQz.a(HsNet.o):function hsnet_getaddrinfo: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/opt/ghc/8.0.2/lib/ghc-8.0.2/rts/libHSrts_thr.a(Linker.thr_o):function internal_dlopen: warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking