Serverless Framework × AWS Lambda で始めるサーバサイド Elm

## はじめに

この記事は、クライアントサイドの実装に特化した言語仕様、アーキテクチャを持っている[Elm](http://elm-lang.org/)で**あえてサーバサイド実装をしてみた**まとめの記事になります。インフラのことは考えずサーバレスで作っていきたいので[AWS Lambda](https://aws.amazon.com/jp/lambda/)と[Serverless Framework](https://github.com/serverless/serverless)を使ってみました。かなり荒削りなので質問があればコメント欄、もしくはTwitterで気軽にお声がけいただけるとうれしいです。

## なぜElmでサーバサイド？

モチベーションは非常にシンプルです。**フロントエンドとバックエンドの両方でElmが使える**、それに尽きます。メリットとしては以下が考えられると思います。

- モデルの構造（データ型）に差異が出ない
- APIのインターフェースの定義をフロントエンド側からも参照できる
- 全てがElmである（？）

## 登場人物

### [Elm](http://elm-lang.org/)

言わずと知れたHaskellライクな文法を持った言語、アーキテクチャです。

### [AWS Lambda](https://aws.amazon.com/jp/lambda/)

AWSが提供する、書いたコードをzipにかためてアップロードするだけでそのコードをクラウド上で実行することが可能になるサービスです。今回はElmが吐き出すJavaScriptのコードを実行するためのインフラとして利用します。サーバレスやAWS Lambdaについて気になる方は以下の記事が分かりやすいので見てみると良いかもしれません。

- [アプリ開発者もインフラ管理者も知っておきたいサーバレスとAWS Lambdaの基礎知識 - @IT](http://www.atmarkit.co.jp/ait/articles/1702/22/news016.html)
- [サーバレスアーキテクチャとは何か？ - CodeZine](https://codezine.jp/article/detail/10332)

### [Serverless Framework](https://github.com/serverless/serverless)

例えばAWS Lambdaである程度の規模のアプリケーションを開発する場合、デプロイやアプリケーションの設定などが複雑になってきます。それを用意にしてくれるアプリケーションフレームワークが[Serverless Framework](https://github.com/serverless/serverless)です。

### [elm-serverless](https://github.com/ktonon/elm-serverless)

ElmでAWS Lambdaを動かすためのライブラリです。以下を内包しています。

- AWS LambdaのNode.jsのコードで受け取ったHTTPリクエストをElmに流し込み、レスポンスをElmから受け取るためのNode.jsのパッケージ
- Node.js側から受け取ったリクエストをElm側で処理し、レスポンスを構築するためのElmパッケージ

## elm-serverless の仕組み

今回は非常にシンプルな、Todoの閲覧しかできないアプリ（登録/更新はできません...）の一部を雑に実装してみたので、それを作った流れを追いながらelm-serverlessの仕組みをご紹介します。扱うTodoの型はこんな感じ。今回はDBとの連携が試せてないのでアプリに静的なデータを埋め込んでそれを扱います:bow:

```elm
dummyTodos : List Todo
dummyTodos =
    [ { id = "1", title = "This is todo1", finished = False }
    , { id = "2", title = "This is todo2", finished = True }
    , { id = "3", title = "This is todo3", finished = False }
    ]


type alias Todo =
    { id : String
    , title : String
    , finished : Bool
    }
```

以下がGitHubのリポジトリになります。
https://github.com/otofu-square/serverless-elm-todo-api

### 環境構築

elm-serverless を始めるには以下のものが必要です。

- Node.js
- yarn(or npm)
- AWSアカウント

それではまずServerless Frameworkをインストールして、プロジェクトを作りましょう。Serverless Frameworkでプロジェクトのボイラープレートを作成する際は[create](https://serverless.com/framework/docs/providers/aws/cli-reference/create/)コマンドを使います。

```shell
$ yarn global add serverless
$ serverless create --template aws-nodejs --path serverless-elm-todo-api
$ cd serverless-elm-todo-api
```

プロジェクトのディレクトリに移動して、必要なライブラリ群をインストールします。ElmのコンパイルやライブラリのバンドリングはWebpack先生頼りなので、必要なライブラリ

```shell
$ yarn add --dev \
  elm elm-serverless　\ # Elm
  serverless-webpack webpack uglifyjs-webpack-plugin elm-webpack-loader # Webpack関連 
```

Elmで使うためのライブラリもインストールしておきます。HTTPリクエストやレスポンスではJSONを簡単に扱いたいので`elm-decode-pipeline`、またURLをパースして取り扱いたいので`url-parser`を導入しています。
（`elm-package`は一度に複数インストールできないのがちょっと面倒ですよね...）

```shell
$ yarn elm package install elm-lang/core && \
  yarn elm package install elm-lang/http && \
  yarn elm package install ktonon/elm-serverless && \
  yarn elm package install ktonon/url-parser && \
  yarn elm package install NoRedInk/elm-decode-pipeline
```

だいたい必要なものは以上です。それでは実際にアプリを書いていきましょう。

### serverless.yml, webpack.config.jsを書く

今回は横道にそれそうなので割愛します:bow:
気になる方は自分のリポジトリにある[serverless.yml](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/serverless.yml), [webpack.config.js](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/webpack.config.js)を見ていただけるともしかしたら参考になるかもしれません。


### エントリーポイントとなるjsファイルを書く

まずはじめに、AWS Lambdaがリクエストを受け付けるエントリーポイントとなるNode.jsのコードを実装します。
https://github.com/otofu-square/serverless-elm-todo-api/blob/master/src/api.js

```js
const elmServerless = require('elm-serverless');
const elm = require('./API.elm');

module.exports.handler = elmServerless.httpApi({
  handler: elm.Hello.API,
  requestPort: 'requestPort',
  responsePort: 'responsePort',
});
```

このコードでは、どのElmファイルをハンドラとして使用するかを設定し、必要があればElm側にJavaScriptの関数を渡すことができます。渡した関数はElm内で実行することが可能です。今回は特に使用しません。

### Elmのハンドラを書く

Elmのハンドラを書いていきます。ここが先程のエントリーポイントのjsとElmを繋ぎこむコードとなるため、jsから受け取ったHTTPリクエストを処理してレスポンスを構築する部分を実装していくことになります。

```elm
port module Hello.API exposing (main)

import Serverless
import Conn exposing (..)
import Router exposing (router, urlParser)


main : Serverless.Program () () Route () ()
main =
    Serverless.httpApi
        { configDecoder = Serverless.noConfig
        , initialModel = ()
        , parseRoute = urlParser
        , update = Serverless.noSideEffects
        , interop = Serverless.noInterop
        , requestPort = requestPort
        , responsePort = responsePort
        , endpoint = router
        }


port requestPort : Serverless.RequestPort msg


port responsePort : Serverless.ResponsePort msg
```

main関数は`Serverless.Program`型になっており、これはElmのcoreの[`Program`](http://package.elm-lang.org/packages/elm-lang/core/latest/Platform)が元になっています。具体的には`Serverless.httpApi`に対して8つのメンバを持つレコードを渡してあげることで処理を書くことができます。全ての説明は割愛しますが、今回主に実装したのはAWS Lambdaが受け取ったURLをパースする`parseRoute`と、レスポンスを構築する`endpoint`の部分になります。他のパラメータについては公式のAPI Docに簡単な説明があります。
http://package.elm-lang.org/packages/ktonon/elm-serverless/latest/Serverless

### parseRouteの実装

ルーティングの型と、それに対するパーサを書いてあげることでURLのハンドリングが行えるようになります。今回実装したルーティングは`GET /todos`と`GET /todos/:id`の２つのエンドポイントです。

- [ルーティングの型](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/src/Conn.elm#L6-L8)

```elm
type Route
    = TodoIndex -- GET /todos
    | TodoShow String -- GET /todos/:id
```

- [URLパーサ](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/src/Router.elm)

```elm
...
urlParser : String -> Maybe Route
urlParser =
    UrlParser.parseString <|
        oneOf
            [ map TodoIndex (s "todos") -- GET /todos
            , map TodoShow (s "todos" </> string) -- GET /todos/:id
            ]
...
```

### endpointの実装

endpointではURLをパースした結果をパターンマッチで抽出して、それぞれのエンドポイントに対する処理とレスポンスの組み立てを行います。

```elm
router : Conn -> ( Conn, Cmd msg )
router conn =
    case
        ( method conn
        , route conn
        )
    of
        ( GET, TodoIndex ) ->
            respond ( 200, jsonBody <| todosEncoder dummyTodos ) conn

        ( GET, TodoShow id ) ->
            case (getTodoById id dummyTodos) of
                Just todo ->
                    respond ( 200, jsonBody <| todoEncoder <| todo ) conn

                Nothing ->
                    respond ( 404, jsonBody <| errorEncoder <| "Not Found id: " ++ id ) conn

        _ ->
            respond ( 405, jsonBody <| errorEncoder "Method not allowed" ) conn
```

JSONの組み立てには[エンコーダ](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/src/Todo.elm#L31-L42)を用意してそちらを使っています。

## デプロイ

ここまでで大体実装が終わり、デプロイ出来るようになります。[AWSアカウントのクレデンシャル](https://github.com/otofu-square/serverless-elm-todo-api/blob/master/.envrc.skeleton)を環境変数にセットしてあげた状態で

```shell
$ NODE_ENV=production serverless deploy
```

を実行してあげるとAWS Lambdaに実装したアプリがデプロイされます。お手軽。
デプロイが完了するとエンドポイントのURLが表示されていると思うので、そちらにリクエストすると結果が返ってくるはずです。

## さいごに

Elmでサーバサイドがまさか書けるとは思っていませんでしたが、小さなアプリケーションであればElmでフロントエンドもバックエンドも両方書くのはそこまで非現実的ではないなと少し希望を持てました。しかしながら実際にアプリ内からDBを扱ったりHTTPリクエストを飛ばしたりといったことはまだ試せていないため、そこの部分がうまく解決できるかで命運が分かれそうです（elmのaws-sdkのラッパーもありそうだったが使い勝手が微妙そうだった）。あとこれは自分の熟練度の問題ですがファイル分割が結構難しいですね...気を抜くと循環参照になっちゃったりしてました。
時間不足感が溢れ中途半端なところで記事にしてしまったのが少し悔しいので、今後も継続して`elm-serverless`は追っていきたいと思います:muscle: