大好きな関数型PG言語で作ったWebアプリをAWSにデプロイして動かすまでに何をしたか(完了版)

# はじめに

関数型PG言語でWebアプリを作り、AWSのLightsailにデプロイするまでの手順を解説します。

# 解説すること

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

# 成果物

https://github.com/jabaraster/servant-elm-study/tree/dev

# プロジェクト構成

プロジェクト直下に以下のディレクトリを作成し、それぞれの製造物を格納します。

* frontend：Elmを始めとするフロントエンドの製造物を置く
* backend：Haskellをはじめとするバックエンドの製造物を置く　※デプロイ対象はここの成果物

このような構成なので、frontendの成果物をbackendの下に出力することで、デプロイ対象に含めることが出来ます。

# フロントエンド

※Elmの詳しい説明は省きます。
※パッケージマネージャとしてyarnを使っている前提です。
※認証にはAWS Congnitoを使っている前提です。

## AWS Cognitoによる認証をElmとどう連携させるか

AWS CognitoはElmライブラリを提供していません。
基本的にはJavaScript(or TypeScript)でコーディングする必要があります。

ElmとJSが連携するには、以下の方法が考えられます。

1. Portを使う：動的な処理に向く
1. ElmをJS上でinitする際のflagsで情報を渡す：静的な情報を渡すのに向く

認証に関する情報は、基本的にはページが表示されたら変わることはないので、Elmをinitする際のflagsとして渡すのが適当です。



## ビルドに関する工夫

フロントエンドでの工夫は、ビルドの成果物をbackendフォルダに配置するよう設定すること。

これをparcelというバンドラで実現するには以下のようなコマンドを実行します。

```sh
yarn parcel build src/index.html --public-url /public --dist-dir ../backend/public/
```

これで、```backend/public```の下に静的ファイルが配置されることになります。
なおparcelは、デフォルトではハッシュ値をファイルに付与してくれます。これにより効率的なキャッシュを実現できます。
（jsやcssに変更が入ったらハッシュ値が変わり、HTML内のパスが変わる）

# バックエンド

※Webアプリを実装するフレームワークとして、[Servant](https://hackage.haskell.org/package/servant)を使っています。

## ServantでWebサーバのルーティングを組み立てる

Servantは型安全にWebアプリを作れる素晴らしいフレームワークです。
基本的にはREST APIを提供するのに便利な機能が多いのですが、もちろん静的ファイルを配信するのにも使えます。

特徴的なのは、型プログラミングを使ってルーティングを組み立てるところです。

以下、メインページや認証で必要なHTMLを返すエンドポイントや、APIとしてのエンドポイントを構成しています。

```App.hs
type API =
  ( Get '[HTML] FileContent
      :<|> "signin-callback" :> Get '[HTML] FileContent
      :<|> "signout-callback" :> Get '[HTML] FileContent
      :<|> "short" :> Get '[HTML] FileContent
      :<|> "public" :> Raw
  )
    :<|> "api"
      :> ( "authentication" :> "status" :> HeaderAuth :> Get '[JSON] Bool
            :<|> "users" :> Get '[JSON] [UserEntity]
            :<|> "authorities" :> Get '[JSON] [AuthorityEntity]
            :<|> "authorities" :> Capture "authorityId" Text :> Get '[JSON] (Maybe AuthorityEntity)
         )
```
６行目に```"public" :> Raw```という記述がありますが、これは```/public```というパスがきた場合は、ファイルの内容そのものを返す、という意味です。
要はHTML/JS/CSS/画像、などの静的ファイルを返すための定義ですね。

今回はここに一工夫加えていますので、次から解説します。

## 必須ではない工夫：静的ファイルのHaskellコードへの埋め込み

開発中はフロントエンドで変更の入ったファイルを毎回動的に読み込むことで変更を即時反映させるのが良いのですが、プロダクション環境ではこれはうまくありません。
また、Dockerでデプロイすることを考慮しても、デプロイ対象のファイルは少ない方が取り回しが良い。

というわけで、静的ファイルの内容をHaskellのコード内に展開する方法を考えます。
こういう時はTemplate Haskellの出番です。

今回、この機能を実現するために[servant-tools](https://github.com/jabaraster/servant-tools)というツールを書きました。

### servant-toolsでやっていること

Template Haskellというのは、ビルド時に動作する関数を定義し、その中でHaskellのコードを生成する仕組みのことです。

次に示すのは、HTMLページを作る関数を作るコードです。

```Embedded.hs
{- | 指定のパスのHTMLファイルの中身を返すHandlerの実装を埋め込む.
|
| 開発ビルド
> htmlHandlerDynamic "static/index.html"
|
| プロダクションビルド
> htmlHandlerEmbedded <htmlファイル内容のLazy.ByteString表現>
-}
genHtmlHandlerImpl :: FilePath -> Q Exp
genHtmlHandlerImpl htmlPath = do
  isProd <- liftIO $ isProductionEnv
  if not isProd
    then [|htmlHandlerDynamic htmlPath|]
    else do
      htmlExp <- embedTextContent htmlPath
      [|htmlHandlerEmbedded $(return htmlExp)|]
```

詳しい実装は省きますが、環境変数がDevの場合は

「リクエストがある度にファイルの内容を読み取ってレスポンスする関数」

を生成し、Prodの場合は

「毎回、Haskellコードに埋め込まれたデータをレスポンスする関数」

を生成しています。

### この手法のデメリット

#### Template Haskellのデメリット

Template Haskellのデメリットとして、生成されるコードが見えない、という点があります。
これは、ビルド時にオプションを付けることで確認することが出来ます。

```sh
stack build --ghc-options="-ddump-splices"
```
また、生成されるコードをしっかりとドキュメント化しておくのも重要でしょう。

#### プロダクション環境と開発環境とで生成されるコードを分岐することのデメリット

それぞれの環境で必要な機能をオーバーヘッドなく実現するという点では素晴らしい手法なのですが、一方で、それぞれの環境で生成されるHaskellプログラムが異なるため、

「**テストをしっかりやる**」必要があると思います。

APIのテストを自動化しておくなど、工夫のしどころだと思います。

# Lightsailへのデプロイ

LightsailはライトなWebアプリをデプロイするにはとても良い環境です。

ただ、Dockerイメージをビルドするときにいくつか落とし穴があります。
（詳しくは[こちら](https://qiita.com/jabaraster/items/d941b951aeb20219aa7b)の記事にまとめておきました）

## Lightsailはlinux/x86_64向けのイメージしか動作させられない

このため、Dockerをビルドするときのコマンドは次のようになります

```sh
docker build . --platform linux/x86_64 -t servant-elm-study
```

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

LightsailにてDockerを使ってWebアプリをホスティングする際、イメージをどこかにアップする必要があります。
Lightsail自体もそのためのリポジトリが持っており、そこにイメージをプッシュするためのコマンドが見出しのものです。

ところが、Macでこのコマンドを実行した時にエラーが起きることがあります。
これに対処するには、以下の２つの対処が必要なようです。

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のコンソールで、プッシュしたイメージからコンテナを起動する

## Lightsailを構成する

Dockerイメージのプッシュに成功したら、いよいよLightsailへのデプロイを実行します。
デプロイする際のポイントは次のようなところでしょう。

* 実行コマンド：stackを使っているので```stack exec <package.yamlのexecutableのところで設定している実行ファイル名>```
* 環境変数の設定をお忘れなく
* ポート開けるのを忘れない：アプリで使用するポートを決め打ちで指定します

html2elm-webというコンテナをデプロイした時の設定を載せておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10225/85cb4c10-56b7-4e25-9b8e-ea7740f83f7e.png)


# この記事を書くことにした経緯（本編と無関係、スルーして問題ありません）

2024年のHaskell Advent Calendarに
[大好きな関数型PG言語で作ったWebアプリをAWSにデプロイして動かすまでに何をしたか（あるいはしているか）](https://qiita.com/jabaraster/items/af636c8160c2579e94f1)という記事を書いたのですが、悪戦苦闘の末、目標を完了することができました。

目標とはすなわち「関数型PG言語でWebアプリを作ってデプロイする」というものです。
もう少し噛み砕くと、

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

となります。

あちらの記事は取り組み過程のメモとなってしまい、編集履歴がわけが分からなくなってしまうので、改めて新しい記事に整理しました。