F# SuaveをHerokuデプロイ

最近F#を触り始め、HerokuにWebアプリをデプロイしてみた。
色々調べたので手順をまとめておく。

# 完成物

https://github.com/dyoshikawa/heroku-suave-getting-started

# 環境
OSX Mojave
.NET Core 3.0
Heroku CLI 7.33.3

# .NET Coreのインストール

https://dotnet.microsoft.com/download

インストーラで簡単に導入できる。
dotnetコマンドも自動で入る。

# ソリューション・プロジェクト作成

## ソリューション作成

```
dotnet new sln -o heroku-suave-getting-started
```

## プロジェクト作成

```
cd heroku-suave-getting-started
dotnet new console -lang F#
dotnet sln add heroku-suave-getting-started.fsproj
```

## Hello World

```
dotnet run
Hello World from F#!
```

# Suave導入

https://github.com/SuaveIO/suave

SuaveはF#における (たぶん) デファクトのWEBフレームワーク。
Expressのようなマイクロフレームワーク。

## パッケージインストール

サードパーティのパッケージマネージャなど不要で、dotnetコマンドでパッケージインストールが行える。

```
dotnet add package Suave --version 2.5.6
```

# サーバを起動してみる

先程のHello Worldの記述がされているProgram.fsというファイルがあるので下のように書き換える。

```fsharp:Program.fs
open Suave

[<EntryPoint>]
let main _argv =
    startWebServer defaultConfig (Successful.OK "Hello World!")
    0
```

`dotnet run` でビルドと実行を行える。

```
dotnet run
[20:00:56 INF] Smooth! Suave listener started in 45.809ms with binding 127.0.0.1:8080
```

この状態でブラウザを開き http://localhost:8080/ を見ると 「Hello World!」が確認できる。

# Herokuで動作するコードに変更

Herokuの仕様でポートは環境変数 `$PORT` から読み取るようにしなければいけない。
そのため下記のように変更する。

```fsharp:Program.fs
open System
open Suave
open Suave.Sockets
open YoLo

[<EntryPoint>]
let main _argv =
    let portByEnv =
        match Env.var ("PORT") with
        | Some(x) -> x
        | None -> "8080"

    let ip : Net.IPAddress = Net.IPAddress.Parse("0.0.0.0")
    let port : Port = uint16 portByEnv

    let socketBinding : Sockets.SocketBinding =
        { ip = ip
          port = port }

    let httpBinding : Http.HttpBinding =
        { scheme = Protocol.HTTP
          socketBinding = socketBinding }

    let conf = { defaultConfig with bindings = [ httpBinding ] }
    startWebServer conf (Successful.OK "Hello World")
    0

```

ずらずら書いているが環境変数 `$PORT` を読み取ってそれを起動ポートとしているだけ。
`$PORT` 変数がない場合は8080となる。

# 実行ファイルを生成して実行 （Mac OSX）

.NET Core3.0にはスタンドアロンな実行ファイルを生成するという機能がある。

https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0#single-file-executables

> dotnet publish コマンドは、プラットフォーム固有の単一ファイルの実行可能ファイルにアプリをパッケージ化することをサポートします。 実行可能ファイルは自己展開型であり、アプリの実行に必要なすべての依存関係 (ネイティブを含む) が含まれます。 

> 自己完結型アプリには、コードの実行に必要なものがすべて含まれるので、ホスト コンピューターに .NET をインストールする必要はありません。

スタンドアロンというだけあってデプロイ先の本番環境に.NETがなくても動作する。

ではまずはローカル (OSX) で試す。

ビルド時にどのOS向けかを指定する必要がある。

https://docs.microsoft.com/ja-jp/dotnet/core/rid-catalog

RIDカタログによるとOSXはosx-x64となるので `-r` オプションの引数に指定。

```
dotnet publish -c Release -r osx-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true
```

少し待つとビルドが終わりbin/Release/netcoreapp3.0/osx-x64/publishの下にheroku-suave-getting-startedとheroku-suave-getting-started.pdbというファイルができる。
本番にはこの2つのファイルを設置しheroku-suave-getting-startedを実行すれば動作する。

実行してみる。

```
 ./bin/Release/netcoreapp3.0/osx-x64/publish/heroku-suave-getting-started
[20:09:07 INF] Smooth! Suave listener started in 52.388ms with binding 127.0.0.1:8080
```

このようにサーバ起動できた。

# デプロイ準備

Herokuでは.NETの公式ビルドパックがないため、

- サードパーティのビルドパックを使う
- Dockerデプロイする

かのどちらかになる。
今回は後者でいく。

最も、上述のシングル実行ファイル生成機能のおかげで環境構築は非常に簡単。

## デプロイ用バイナリを生成 （Alpine Linux）

https://docs.microsoft.com/ja-jp/dotnet/core/rid-catalog

RIDカタログを参照。
Alpineなので `linux-musl-x64` を指定。

```
dotnet publish -c Release -r linux-musl-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true
```

## Dockerfile

ソリューション直下にDockerfileを用意する。
動かすのはスタンドアロンな実行ファイルなので、.NET入りのDockerイメージを用意する必要はなく、素のAlpineで構わない。
しかし多少の依存はあるらしく、最低限以下のパッケージを入れれば動くようだ。

```bash
apk add gcc libintl icu
```

Dockerfileの内容は下記。

```dockerfile:Dockerfile
FROM alpine:3.10

RUN apk update && \
    apk add gcc libintl icu

ADD ./bin/Release/netcoreapp3.0/linux-musl-x64/publish /app

CMD /app/heroku-suave-getting-started
```

# Herokuデプロイ

## CLIインストール

https://devcenter.heroku.com/articles/heroku-cli

```
brew tap heroku/brew && brew install heroku
```

で導入できる。

## Herokuログイン

```bash
heroku login
heroku container:login
```

## Herokuアプリ作成

```bash
heroku create heroku-suave-getting-started
```

heroku-suave-getting-startedは任意のアプリ名。

## デプロイ

```bash
heroku container:push web -a heroku-suave-getting-started
heroku container:release web -a heroku-suave-getting-started
```

## アプリをブラウザで開く

```bash
heroku open
```

「Hello World!」が出ていればOK。

# 参考
https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet-new?tabs=netcore22
http://maxfie1d.hatenablog.com/entry/2018/01/23/141755
https://docs.microsoft.com/ja-jp/dotnet/core/rid-catalog
https://dev.to/pluralsight/creating-trimmed-self-contained-executables-in-net-core-4m08
https://hub.docker.com/_/microsoft-dotnet-core
https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli