はじめに
関数型PG言語でWebアプリを作り、AWSのLightsailにデプロイするまでの手順を解説します。
解説すること
- バックエンドをHaskellで作る
- フロントエンドをElmで作る
- それらをAWSを使ってデプロイ、公開する
成果物
プロジェクト構成
プロジェクト直下に以下のディレクトリを作成し、それぞれの製造物を格納します。
- frontend:Elmを始めとするフロントエンドの製造物を置く
- backend:Haskellをはじめとするバックエンドの製造物を置く ※デプロイ対象はここの成果物
このような構成なので、frontendの成果物をbackendの下に出力することで、デプロイ対象に含めることが出来ます。
フロントエンド
※Elmの詳しい説明は省きます。
※パッケージマネージャとしてyarnを使っている前提です。
※認証にはAWS Congnitoを使っている前提です。
AWS Cognitoによる認証をElmとどう連携させるか
AWS CognitoはElmライブラリを提供していません。
基本的にはJavaScript(or TypeScript)でコーディングする必要があります。
ElmとJSが連携するには、以下の方法が考えられます。
- Portを使う:動的な処理に向く
- ElmをJS上でinitする際のflagsで情報を渡す:静的な情報を渡すのに向く
認証に関する情報は、基本的にはページが表示されたら変わることはないので、Elmをinitする際のflagsとして渡すのが適当です。
ビルドに関する工夫
フロントエンドでの工夫は、ビルドの成果物をbackendフォルダに配置するよう設定すること。
これをparcelというバンドラで実現するには以下のようなコマンドを実行します。
yarn parcel build src/index.html --public-url /public --dist-dir ../backend/public/
これで、backend/public
の下に静的ファイルが配置されることになります。
なおparcelは、デフォルトではハッシュ値をファイルに付与してくれます。これにより効率的なキャッシュを実現できます。
(jsやcssに変更が入ったらハッシュ値が変わり、HTML内のパスが変わる)
バックエンド
※Webアプリを実装するフレームワークとして、Servantを使っています。
ServantでWebサーバのルーティングを組み立てる
Servantは型安全にWebアプリを作れる素晴らしいフレームワークです。
基本的にはREST APIを提供するのに便利な機能が多いのですが、もちろん静的ファイルを配信するのにも使えます。
特徴的なのは、型プログラミングを使ってルーティングを組み立てるところです。
以下、メインページや認証で必要なHTMLを返すエンドポイントや、APIとしてのエンドポイントを構成しています。
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)
)
6行目に"public" :> Raw
という記述がありますが、これは/public
というパスがきた場合は、ファイルの内容そのものを返す、という意味です。
要はHTML/JS/CSS/画像、などの静的ファイルを返すための定義ですね。
今回はここに一工夫加えていますので、次から解説します。
必須ではない工夫:静的ファイルのHaskellコードへの埋め込み
開発中はフロントエンドで変更の入ったファイルを毎回動的に読み込むことで変更を即時反映させるのが良いのですが、プロダクション環境ではこれはうまくありません。
また、Dockerでデプロイすることを考慮しても、デプロイ対象のファイルは少ない方が取り回しが良い。
というわけで、静的ファイルの内容をHaskellのコード内に展開する方法を考えます。
こういう時はTemplate Haskellの出番です。
今回、この機能を実現するためにservant-toolsというツールを書きました。
servant-toolsでやっていること
Template Haskellというのは、ビルド時に動作する関数を定義し、その中でHaskellのコードを生成する仕組みのことです。
次に示すのは、HTMLページを作る関数を作るコードです。
{- | 指定のパスの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のデメリットとして、生成されるコードが見えない、という点があります。
これは、ビルド時にオプションを付けることで確認することが出来ます。
stack build --ghc-options="-ddump-splices"
また、生成されるコードをしっかりとドキュメント化しておくのも重要でしょう。
プロダクション環境と開発環境とで生成されるコードを分岐することのデメリット
それぞれの環境で必要な機能をオーバーヘッドなく実現するという点では素晴らしい手法なのですが、一方で、それぞれの環境で生成されるHaskellプログラムが異なるため、
「テストをしっかりやる」必要があると思います。
APIのテストを自動化しておくなど、工夫のしどころだと思います。
Lightsailへのデプロイ
LightsailはライトなWebアプリをデプロイするにはとても良い環境です。
ただ、Dockerイメージをビルドするときにいくつか落とし穴があります。
(詳しくはこちらの記事にまとめておきました)
Lightsailはlinux/x86_64向けのイメージしか動作させられない
このため、Dockerをビルドするときのコマンドは次のようになります
docker build . --platform linux/x86_64 -t servant-elm-study
aws lightsail push-container-imageコマンドのエラー
LightsailにてDockerを使ってWebアプリをホスティングする際、イメージをどこかにアップする必要があります。
Lightsail自体もそのためのリポジトリが持っており、そこにイメージをプッシュするためのコマンドが見出しのものです。
ところが、Macでこのコマンドを実行した時にエラーが起きることがあります。
これに対処するには、以下の2つの対処が必要なようです。
- Docker Desktopの設定を変更する
- 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アプリをデプロイするには
- 事前に、Docker Desktopの設定を変更しておく(
Use containerd for pulling and storing images
を無効にする) -
--platform linux/x86_64
を付けてdocker build
する -
docker context ls
コマンドでdesktop-linuxのエンドポイントを調べる -
aws lightsail push-container-image
を実行するときに環境変数DOCKER_HOST
を前項で調べた値に設定する - Lightsailのコンソールで、プッシュしたイメージからコンテナを起動する
Lightsailを構成する
Dockerイメージのプッシュに成功したら、いよいよLightsailへのデプロイを実行します。
デプロイする際のポイントは次のようなところでしょう。
- 実行コマンド:stackを使っているので
stack exec <package.yamlのexecutableのところで設定している実行ファイル名>
- 環境変数の設定をお忘れなく
- ポート開けるのを忘れない:アプリで使用するポートを決め打ちで指定します
html2elm-webというコンテナをデプロイした時の設定を載せておきます。
この記事を書くことにした経緯(本編と無関係、スルーして問題ありません)
2024年のHaskell Advent Calendarに
大好きな関数型PG言語で作ったWebアプリをAWSにデプロイして動かすまでに何をしたか(あるいはしているか)という記事を書いたのですが、悪戦苦闘の末、目標を完了することができました。
目標とはすなわち「関数型PG言語でWebアプリを作ってデプロイする」というものです。
もう少し噛み砕くと、
- バックエンドをHaskellで作って
- フロントエンドをElmで作り
- それをAWSを使ってデプロイ、公開する
となります。
あちらの記事は取り組み過程のメモとなってしまい、編集履歴がわけが分からなくなってしまうので、改めて新しい記事に整理しました。