大好きな関数型PG言語で作ったWebアプリをAWSにデプロイして動かすまでに何をしたか（あるいはしているか）

# はじめに
Haskellで関数型PG言語に出会い、挫折を繰り返し、Elmに出会って満足していた自分ですが、やっぱりHaskellでバックエンドを作りたくなるのがFuncational Programmerの性ってものです（たぶん）

Haskellを知ってから15年くらい経ち、その界隈の技術と私の知識もだいぶ進んだだろう、ということで、改めてMade by Full Functionarl Programming LanguageなWebアプリを作り、かつAWSで公開する取り組みを開始しました。

# 具体的な目標

* Webアプリを作る
* バックエンドをHaskellで作って
* フロントエンドをElmで作り
* それをAWSを使ってデプロイ、公開する

# 構成
* バックエンドはServantで作成
* 可能ならDBアクセスも実装する
* フロントエンドはElmで作る
* これらをDockerにまとめてAWSにデプロイする

実は、ここまでDockerにほぼノータッチで過ごしてきたので、その学習も兼ねています。

# 免責事項

この記事は完成しない可能性があります！！

# ここからが、やったこと

## 開発PC
* MacBook Air M1, 2020 Sequoia 15.2

## Gitリポジトリ
https://github.com/jabaraster/servant-elm-study

## 何はともあれ、ローカル環境で動作する最小のWebアプリを作成
Haskellと言えばstack。
これを使ってローカルで起動する最低限のWebアプリを作ります。

ここでの成果物はこれ。
https://github.com/jabaraster/servant-elm-study/blob/f062c63a9c362b643111cd789a693b5bf40d8cd5

今回は静的ファイルの配信も必要なのですがその実現にかなり遠回り。
終わってみればServantが提供している```serveDirectoryFileServer```などの関数を使えばいいのですが、そこに気付くのに時間がかかった！

```hs
server :: Server API
server =
  indexHandler
    :<|> serveDirectoryFileServer "./static" -- ここ！
    :<|> usersHandler
```

全体的な構成としては
* フロントエンドでビルドした成果物のフォルダを
* Servantの```serverDirectioryFileServer```にて公開

となります。

ただ、何やら静的リソースをバイナリに含める方法があるらしく。

[ここ](https://hackage.haskell.org/package/servant-server-0.20.2/docs/Servant-Server-StaticFiles.html)serveDirectoryEmbedded辺りを使うような気がしています。

## Dockerで公開するためのビルド環境の構築

ここからが難しい。
こちらの記事を足がかりにして進めます。
https://qiita.com/lambda_funtaro/items/5ac47f83616f8c07d4db

大雑把な目標は

1. ~~まずmuslを使ってシングルバイナリを作って~~
   →　上手くいかないのであきらめました。
1. それを動かすDockerfileを書き
1. Dockerをビルドし
1. ~~AWS ECR/ECSを使ってデプロイする~~
  →　ECSは結構料金が高いし設定が面倒なので、まずはLightsailを試してみることにします。
   
というものになります。

2025.1.26追記
さらに
* 静的ファイルをバイナリに埋め込む
という仕掛けを追加します。


## 静的ファイルをバイナリに埋め込む
シングルバイナリを作るのはかなり苦労しそうなので、先にこのテーマをメモ。
なお、この機能を組み込んだ直後の状態は以下のコミットをご覧ください（長くなるので先にご案内）。
https://github.com/jabaraster/servant-elm-study/commit/0058854667a7d297fff813009e0d451bf1c3558b

### 動機
なぜ静的ファイルをバイナリに埋め込みたいか、その理由は**シングルバイナリにしたい**からです。
作ったバイナリをサーバに配置するときに、静的ファイルを別途コピーするのは面倒ですから。

### TemplateHaskellを使って静的ファイルを埋め込む
Servantが依存しているWAIというライブラリのドキュメントで、[静的ファイルをTemplateHaskellを使ってバイナリに埋め込む方法](https://hackage.haskell.org/package/wai-app-static-3.1.9/docs/WaiAppStatic-Storage-Embedded.html#v:embeddedSettings)が紹介されていました。

これを使うと次のようにして静的ファイルを配信するハンドラが書けます。

```App.hs
serveDirectoryWith $(mkSettings Embedded.staticFiles)
```

Embedded.staticFilesというのは自作の関数です。
コンパイル時に特定のフォルダ下のファイルの内容をソースコードに埋め込むためのコードを書いています。

ただ、実際にTemplateHaskellとして展開するのはWAIが提供している```WaiAppStatic.Storage.Embedded.mkSettings```という関数がやってくれるので、自作するのはmkSettings関数に渡す引数を作成することです。

```Embedded.hs
staticFiles :: IO [EmbeddableEntry]
staticFiles =
  listStaticFileNames
    >>= mapM fileToEmb

listStaticFileNames :: IO [FilePath]
listStaticFileNames =
  getDirectoryContents "public"
    >>= return . filter (\x -> x /= "." && x /= ".." && x /= "index.html")

fileToEmb :: FilePath -> IO EmbeddableEntry
fileToEmb fileName = do
  let path = "public" ++ [pathSeparator] ++ fileName
  cnt <- BL.readFile path
  return
    EmbeddableEntry
      { eLocation = T.pack fileName
      , eMimeType = defaultMimeLookup $ T.pack path
      , eContent = Left (hash cnt, cnt)
      }
```

### index.htmlの扱い
今回想定しているのはSPAな作りのWebアプリであり、その場合エントリポイントとなるHTMLを配信しなければなりません。
ただ、Servantのルーティングの仕様なのか、前節で書いた方法でindex.htmlをバイナリに埋め込武のがどうしてもうまく行きませんでした。

例えば、次のように書いてもうまく動作しないのです（トップページがFile Not Foundになる）。

```App.hs
type API =
  Raw
    :<|> "public" :> Raw
    :<|> "api" :> "users" :> Get '[JSON] [User]

server :: Server API
server =
  serveDirectoryWebApp "public"
    :<|> serveDirectoryWebApp "public"
    :<|> liftIO usersHandler
```

なのでindex.htmlには別の方法を考えなければなりません。
２つの機能の合わせ技で実現します。

1. Servantの機能を使って、HTML文字列をクライアントに返すハンドラ？を作成
1. 1で作った関数を使ってTemplateHaskellにてソースコードに埋め込む仕組みを作成

#### Servantの機能を使って、HTML文字列をクライアントに返すハンドラ？を作成

正直ここはよく理解できていないですが、とりあえず以下のソースで実現できます。

```Embedded.hs
newtype FileContent = FileContent {unRaw :: ByteString}

data HTML = HTML
instance Accept HTML where
  contentType _ = "text" // "html" /: ("charset", "utf-8")
instance MimeRender HTML FileContent where
  mimeRender _ = unRaw

makeHandlerFromHtml :: ByteString -> Handler FileContent
makeHandlerFromHtml = return . FileContent
```

#### 1で作った関数を使ってTemplateHaskellにてソースコードに埋め込む仕組みを作成

今回はお手軽にTemplateHaskellを実装できる、QuasiQuoterという機能を使って実現しました。

```Embedded.hs
genIndexHandler :: QuasiQuoter
genIndexHandler =
  QuasiQuoter
    { quoteExp = undefined :: String -> Q Exp
    , quotePat = undefined :: String -> Q Pat
    , quoteType = undefined :: String -> Q Type
    , quoteDec = genIndexHandlerCore :: String -> Q [Dec]
    }

genIndexHandlerCore :: FilePath -> Q [Dec]
genIndexHandlerCore pathWithSpace = do
  let name = mkName "indexHandler"
  let path = strip pathWithSpace
  html <- liftIO $ Prelude.readFile path
  return
    [ SigD name (AppT (ConT ''Handler) (ConT ''FileContent)) -- 関数宣言部
    , FunD
        name
        [ Clause
            []
            ( NormalB $ AppE (VarE 'makeHandlerFromHtml) (LitE $ StringL html)
            )
            [] -- 実装定義部
        ]
    ]
```

```genIndexHandlerCore```が心臓部です。
Haskellのソースコードを組み立てています。

一方、これを使ってindex.htmlのハンドラを作るには、以下のようにします。

```App.hs
[Embedded.genIndexHandler| ./public/index.html |]
```

### 工夫・開発中は静的ファイルの変更を検出したい
さてここまで静的ファイルをバイナリに埋め込む方法を追求してきましたが、開発中はこれでは不便です。
JSやCSSを更新したら毎回Haskell側をビルドしないといけないのは手間ですよね。

なので、リクエストがある度にファイルから内容を読み込んで返す機能を追加して、環境変数で切り替えられるようにします。

これを反映した、静的ファイルのハンドラは以下のようになります。

まずはpublicディレクトリ下のファイルを配信する箇所。
こちらは単純。
```App.hs
server :: Config -> Server API
server config =
  indexHandler
    :<|> ( if Config.runtimeEnv config == Dev
            then (serveDirectoryWebApp "public")
            else serveDirectoryWith $(mkSettings Embedded.staticFiles) -- index.html以外の静的ファイルもバイナリに埋め込む
         )
    :<|> liftIO usersHandler
```

もう１つ、index.htmlのハンドラ。ここは、QuasiQuoterを使ってHaskellのソースコードを作成するので、ちょっと複雑です。
dynamicHandlerという関数を追加した上で、環境変数によって呼び出す関数を切り替えます。
```Embedded.hs
newtype FileContent = FileContent {unRaw :: ByteString}

data HTML = HTML
instance Accept HTML where
  contentType _ = "text" // "html" /: ("charset", "utf-8")
instance MimeRender HTML FileContent where
  mimeRender _ = unRaw

makeHandlerFromHtml :: ByteString -> Handler FileContent
makeHandlerFromHtml = return . FileContent

dynamicHandler :: FilePath -> Handler FileContent
dynamicHandler path = do
  html <- liftIO $ Lazy.readFile $ strip path
  return $ FileContent $ html

genIndexHandler :: QuasiQuoter
genIndexHandler =
  QuasiQuoter
    { quoteExp = undefined :: String -> Q Exp
    , quotePat = undefined :: String -> Q Pat
    , quoteType = undefined :: String -> Q Type
    , quoteDec = genIndexHandlerCore :: String -> Q [Dec]
    }

genIndexHandlerCore :: FilePath -> Q [Dec]
genIndexHandlerCore pathWithSpace = do
  let name = mkName "indexHandler"
  let path = strip pathWithSpace
  config <- liftIO $ Config.loadConfigWithDefault
  if Config.runtimeEnv config == Dev
    then do
      -- 開発中は毎回ファイルを読み込む
      return
        [ SigD name (AppT (ConT ''Handler) (ConT ''FileContent)) -- 関数宣言部
        , FunD
            name
            [ Clause
                []
                ( NormalB $ AppE (VarE 'dynamicHandler) (LitE $ StringL path)
                )
                [] -- 実装定義部
            ]
        ]
    else do
      -- 開発以外ではコンパイル時にファイルを埋め込む
      html <- liftIO $ Prelude.readFile path
      return
        [ SigD name (AppT (ConT ''Handler) (ConT ''FileContent)) -- 関数宣言部
        , FunD
            name
            [ Clause
                []
                ( NormalB $ AppE (VarE 'makeHandlerFromHtml) (LitE $ StringL html)
                )
                [] -- 実装定義部
            ]
        ]
```

これで開発スピードを落とさず、かつビルドの成果物が最小になる環境が出来上がりました！

## Lightsailへのデプロイ
AWSのLightsailにデプロイするのに苦労しています。
AWSが用意しているサンプルのWebアプリはデプロイに成功するのですが、そのソースを使って自前でビルドしたDockerイメージを使ったデプロイが上手くいかないのです。

このようなログが出ているので、Dockerコンテナがいつまでも起動していないか、あるいは所定のポートが開いていないのか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10225/2ea8a494-0b0b-421c-bef5-89f9ae635467.png)

いずれにしろ、これ以上のログが出ないので調べようがないんですよね・・・


## シングルバイナリを作る設定

・・・2025年2月の時点で、シングルバイナリ化は一旦諦めました。
先に、AWSにDockerイメージをデプロイしてWebアプリとして公開する手順を先に確立することにしました。

一応、何をやったかは残しておきます。

stackにはDockerを使ってビルドする機能があります。
stack.yamlに書く方法もあるのですが、まずは```stack build```コマンドでDockerイメージを指定する方法を使います。

```sh
stack build \
  --ghc-options ' -static -optl-static -optl-pthread -fPIC' \
  --docker --docker-image "utdemir/ghc-musl:v25-ghc944" \
  --no-nix
```

ここで困ったことが発生。
ghcをダウンロードするときに、コンテント長が想定と異なる、と言って怒られます。

```
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Preparing to install GHC (musl) to an isolated location. This will not interfere with any system-level installation.
Preparing to download ghc-musl-9.6.6 ...
Download expectation failure: ContentLength header
Expected: 224131196
Actual:   198860724
For: https://downloads.haskell.org/ghc/9.6.6/ghc-9.6.6-x86_64-alpine3_12-linux.tar.xz
```

GHC9.6.6に関するバイナリの一覧は[こちら](https://downloads.haskell.org/ghc/9.6.6/)
にあります。ここで確認したところ、

- ダウンロードしようとしているファイルはghc-9.6.6-x86_64-alpine3_12-linux.tar.xzなのに
- コンテント長はghc-9.6.6-x86_64-alpine3_12-linux-static.tar.xzのものを見ている

ということが分かります。

が、これがなぜ起こるのか、どうやって解消させるのかが分からない。

stack.yamlには```skip-ghc-check```というオプションもあり、これをtrueにしてみましたが、

```
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
wai-app-static   > configure
wai-app-static   > Configuring wai-app-static-3.1.9...
wai-app-static   > build with ghc-9.4.4
wai-app-static   > Preprocessing library for wai-app-static-3.1.9..
wai-app-static   > Building library for wai-app-static-3.1.9..
wai-app-static   > [1 of 9] Compiling WaiAppStatic.Types
wai-app-static   > [2 of 9] Compiling Util
wai-app-static   > [3 of 9] Compiling WaiAppStatic.Listing
wai-app-static   > [4 of 9] Compiling WaiAppStatic.Storage.Filesystem
wai-app-static   > [5 of 9] Compiling WaiAppStatic.Storage.Embedded.TH
wai-app-static   > [6 of 9] Compiling WaiAppStatic.Storage.Embedded.Runtime
wai-app-static   > [7 of 9] Compiling WaiAppStatic.Storage.Embedded
wai-app-static   > [8 of 9] Compiling Network.Wai.Application.Static
Progress 1/3

Error: [S-7282]
       Stack failed to execute the build plan.
       
       While executing the build plan, Stack encountered the error:
       
       [S-7011]
       While building package wai-app-static-3.1.9 (scroll up to its section to see the error) using:
       /xxxxxxxxxx/.stack/setup-exe-cache/x86_64-linux-dk85569e7f830e7dc606115fd702e078fb/Cabal-simple_DY68M0FN_3.8.1.0_ghc-9.4.4 --verbose=1 --builddir=.stack-work/dist/x86_64-linux-dk85569e7f830e7dc606115fd702e078fb/ghc-9.4.4 build --ghc-options " -fdiagnostics-color=always"
       Process exited with code: ExitFailure (-11) 
```

2024/12/19時点ではここまで。

進展したら追記します。
追記がなかったら力尽きたと察してください。


# 2024/12/23追記

このような記事に出合いました。

https://qiita.com/t10471/items/4afa598e1be5d6c7cc1f#追記-実は本当のシングルバイナリにはならないかも

HaskellでWebアプリを作る際の共通的なインターフェイスを定義するWAIがシングルバイナリを作る際の障壁となっているのでは？という内容。

WAIが依存するNetworkがCのFFIに依存している、とあります

確かに、ビルドがうまく行っていないエラーメッセージに

```
While building package wai-app-static-3.1.9 (scroll up to its section to see the error) using:
```

とあります。
これは怪しい！

もしかすると、シングルバイナリを諦める必要があるかもしれません。

今回の目標はHaskell+Elmで作ったWebアプリケーションをAWSで公開することです。
シングルバイナリを作ることではありません。

この辺を見直してみます。

# Lightsailへのデプロイ

[別の記事](https://qiita.com/jabaraster/items/d941b951aeb20219aa7b)で言及したようにWebアプリをAWSにDocker使ってデプロイする場合はLightsailを使うのが楽チンです。

ただ、落とし穴が２つほどあって

1. Lightsailで動作するプラットフォームは```linux/x86_64```
1. ```aws lightsail push-container-image```コマンドで```Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?```というエラーが出る

順に対処法を示します。

## Lightsailで動作するプラットフォームは```linux/x86_64```

これは単純な話で、docker buildするときにプラットフォームを明示的に指定します。

```
docker build . --platform linux/x86_64 -t ...
```

## aws lightsail push-container-imageコマンドのエラー

このエラーは、どうもMacで実行したときに生じるエラーのようです。

さて、このコマンドが今回やりたいことに絶対に必要かというとそうではありません。

というのは、このコマンドをローカルで作成したDockerイメージをLightsailの特定のリポジトリにプッシュするものであって、このリポジトリを使わない場合は、このコマンドを使う必要はないのです。

ただ、じゃあこのリポジトリを使わない場合にどのリポジトリを使うの？と言ったらそれは迷います。
Docker Hubみたいな全世界に公開されるリポジトリは論外として、じゃあAWSの[ECR](https://aws.amazon.com/jp/ecr/)を使うのは大袈裟だし、そもそもWebアプリをデプロイしたいだけなのに、そこから離れたサービスに別途リポジトリを作るのは、なんか回り道をしている気がします。

というわけで、Lightsailにデプロイしたいイメージは、Lightsailの中で管理するのが便利であり、そこにイメージをプッシュするコマンドが```aws lightsail push-container-image```なわけですね。

というわけで、このエラーはなんとか解消したい。

調べたところ、

1. Docker Desktopの設定を変更する
1. DOCKER_HOSTを明示的に指定する

という２つが必要なようです。

Docker Desktopの設定については、以下の記事に詳しいです。

https://zenn.dev/kuritify/articles/docker-descktop-setting-from-ecr-400-bad-request


DOCKER_HOSTについては、以下の記事に詳しいです。
https://koko206.hatenablog.com/entry/2024/02/12/195159

さて、ここまでの情報を総合すると、LightsailにHaskell+Elmで作ったWebアプリをデプロイするには

1. 事前に、Docker Desktopの設定を変更しておく（```Use containerd for pulling and storing images```を無効にする）
1. ```--platform linux/x86_64```を付けて```docker build```する
1. ```docker context ls```コマンドでdesktop-linuxのエンドポイントを調べる
1. ```aws lightsail push-container-image```を実行するときに環境変数```DOCKER_HOST```を前項で調べた値に設定する
1. Lightsailのコンソールで、プッシュしたイメージからコンテナを起動する

これで、やっとHaskell+ElmのWebアプリがデプロイできました！

やったーーーー！