Goで書いたサーバーをHerokuにDocker Deployする

Goで書いたサーバーをデプロイする先を探していて、[Google App Engine(SE)](https://qiita.com/croquette0212/items/1e9df0f25f69b97d06e2)や[Arukas](https://qiita.com/croquette0212/items/b86d9f39b5c19c3e3de1)などを試してきたが、ふと気になって調べてみたらRailsアプリのホスティングで馴染みのあるHerokuもDocker Deployに対応していたので、試してみた。割と良さげなので、手順を残しておく。

# 前提: 必要なもの
* [Herokuアカウント](https://heroku.com)
* [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)
    * CLIのバージョンが古いと`container:release`でエラーが発生するので注意！
* [Docker](http://docker.com)

## Heroku CLI ログイン
```
heroku login
heroku container:login
```

HerokuへのデプロイはCLIを使ってシェルから行います。`heroku login`をして、CLIからログインして起きましょう。Docker Deployの場合は`heroku container:login`で[Container Registry](https://devcenter.heroku.com/articles/container-registry-and-runtime)へのログインも必要となります。

# プログラムの準備
まずはHerokuにDeployする、簡単なHTTPサーバーをGoで書いて、ローカル(+ローカルのDocker)で動作確認をします。

## シンプルなHTTPサーバーをGoで書く
```golang:main.go
package main

import (
	"fmt"
	"net/http"
	"os"
	"strconv"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello world\n")
}

func main() {
	port, _ := strconv.Atoi(os.Args[1])
	fmt.Printf("Starting server at Port %d", port)
	http.HandleFunc("/", handler)
	http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

```

HTTPリクエストがきたら`Hello world\n`を返す、シンプルなHTTPサーバーです。

重要なのは、Listenに使うポート番号をコマンドライン引数(`os.Args[1]`)で実行時に指定できるようにしていることです。これは、HerokuのDocker DeployでHTTPサーバーを動かしたい場合、Herokuから指定されたポート番号(`$PORT`)でListenをする必要があるためです。

参考: [Dockerfile commands and runtime](https://devcenter.heroku.com/articles/container-registry-and-runtime#dockerfile-commands-and-runtime)

> The web process must listen for HTTP traffic on $PORT, which is set by Heroku.


ローカルで実行して動作確認しましょう。

```sh

go run main.go 3000
```

`curl`で`localhost:3000`にアクセスして、`Hello world\n`が返ってきたらOKです。

## Dockerfileを定義する
```
FROM golang:latest as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go/src/github.com/yokoe/go-server-example
COPY . .
RUN go build main.go

# runtime image
FROM alpine
COPY --from=builder /go/src/github.com/yokoe/go-server-example /app

CMD /app/main $PORT
```

最新の`golang`でビルド、容量の小さい`Alpine Linux`上でサーバーを動かすDocker imageを作ります。[HerokuではEXPOSEコマンドは無視される](https://devcenter.heroku.com/articles/container-registry-and-runtime#dockerfile-commands-and-runtime)ので、記述する必要はありません。前述の通り、サーバーはHerokuから指定されるポート番号(`$PORT`)でListenする必要があるので、起動時の引数に渡しています。

```
docker build -t herokuexp .
docker run -e "PORT=3000" -p 3000:3000 -t herokuexp
```

ローカルでビルドして試してみましょう。先ほどと同じく`Hello world\n`が返ってきたらOKです。

# Herokuにデプロイする
## gitレポジトリの作成
```
git init
```

gitレポジトリを作成するのは必須ではありませんが、gitで管理しておくと以降の`heroku`コマンドでデプロイなどの作業をする際に`--appname`を指定しなくてよくなるので楽です。

## herokuアプリの作成
```sh
heroku create
```

新しいherokuアプリを作成します。

## pushする
```sh
heroku container:push web
```

デプロイの作業は`push`と`release`の二段階に分かれています。2018年5月までは`push`をするだけでデプロイが完了していましたが、今は明示的に`release`をするまでデプロイが完了しないので注意しましょう。

参考: https://devcenter.heroku.com/changelog-items/1426

まずは、Container RegistryにDocker imageをPushします。pushを実行するとローカルでDocker imageが作成され、Container Registryに登録されます。最後の引数はプロセスタイプです。今回はHTTPサーバーなので`web`を指定します。

なお、gitレポジトリを使用していない場合(gitのremoteにherokuが登録されていない状態)の場合は、コマンドを叩く際に`appname`を明示的に指定してあげる必要があります。

### unauthorized: authentication requiredが出る場合
Container Registryにログインしていない可能性があります。`heroku container:login`でContainer Registryにログインしてから試しましょう。

## releaseする
```sh
heroku container:release web
``` 

Container Registryにイメージをpushできたら、それを`release`します。`web`プロセス1個のみを使う場合は`push`と`release`を分けて叩くメリットがわからないかもしれませんが、複数のプロセスタイプのイメージを同時にデプロイしたい場合などに便利なようです。

`push`にはDocker Image作成のために多少時間がかかりますが、`release`は一瞬で完了します。二つの作業合わせても、Google App Engineのデプロイに比べると圧倒的に速いので快適ですね。

`push`と`release`をわける必要がない場合は、[Task](https://github.com/go-task/task)で下記のようなタスクを追加すると楽です。

```yaml:Taskfile.yml
version: '2'

tasks:
  release:
    cmds:
      - heroku container:push web
      - heroku container:release web
      - osascript -e 'display notification "Deploy Done" with title "Heroku Deploy" subtitle "My Go Server" sound name "Submarine"'

```

蛇足ですが、Macの場合は最後に`osascript`でNotificationを出すようにしておくと、作業完了までの間他の作業に集中しやすいのでおすすめです。
## 動作確認
```sh

heroku open
```

デプロイに成功したら、サーバーにアクセスして確認します。サーバーのURLは`heroku create`した際などに表示されるので、それを`curl`で叩いても良いですし、`heroku open`コマンドを叩くとそのURLをブラウザで起動してくれるので楽です。(APIサーバーの場合はcurlで確認したいですが)

# まとめ
GoアプリのHerokuへのデプロイはかなり簡単で速い上、値段も明瞭なのでおすすめです。CircleCIと連携させればGitレポジトリへPushしたタイミングで自動デプロイすることも可能なので、気が向いたら追記or新しいポストで投稿しようと思います。

# 参考リンク
* https://devcenter.heroku.com/articles/container-registry-and-runtime