Goで作ったアプリケーションをHerokuで動かす

Goで作ったアプリケーションをWebサーバーに上げて活用したいなと思ったときに
無料だし、ある程度使っていていて慣れてたしという理由で、Herokuを使おうと思ったわけです。

ただ、いざやってみると色んな方法がありすぎて、いい感じの情報を見つけるのが難しく
デプロイするのに色々詰まったので、備忘録として残しておきます。

よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

# 事前準備

## １．Herokuアカウントの作成

Herokuの[公式サイト](https://dashboard.heroku.com)から、アカウントを作成してください。

## ２．Heroku CLIのインストール

次にターミナルからHerokuを操るための `Heroku CLI` を
[こちら](https://devcenter.heroku.com/articles/heroku-cli)のページから、ダウンロード・インストールしてください。

下記のように入力して、結果が返ってくれば完了です。

```zsh
% heroku -v
heroku/7.47.5 darwin-x64 node-v12.16.2
```

## ３．動作環境

その他、下記環境で動作しています。
適宜必要なものをインストールしてあるとスムーズです。

```zsh
% go version       
go version go1.15.5 darwin/amd64

% docker -v
Docker version 19.03.13, build 4484c46d9d

$ docker-compose -v
docker-compose version 1.27.4, build 40524192
```

# ローカルで動かす

Herokuにデプロイする前に、まずはローカルで動かせるように準備します。

## １．簡単なリクエストを受け付けるコードの作成

まずは簡単なリクエストを受け付ける
以下のようなコードを含むファイルを作成します。

※今回はフレームワークに [Gin](https://github.com/gin-gonic/gin) を使用しています。

```go:./cmd/main.go
package main

import (
  "net/http"
  "os"

  "github.com/gin-gonic/gin"
)

func main() {
  r:= gin.Default()
  r.GET("/", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "hello world",
    })
  })

  port := os.Getenv("PORT")
  if port == "" {
    port = "8080"
  }

  if err := r.Run(":" + port); err != nil {
    panic(err)
  }
}
```

HerokuではWeb worker processにポートが紐付けられるので、`os.Getenv("PORT")` でPORT番号を取ってきます。 
Herokuの[こちら](https://devcenter.heroku.com/articles/runtime-principles#web-servers)のドキュメントに記載があります。


## ２．Docker関連の準備

### ２−１．docker-compose.yml

ローカル開発では、Dockerイメージのビルドや各コンテナの起動・停止などをより簡単に行える `docker-compose` を使用します。

`dockers` ディレクトリを作成し、その中に下記のような `docker-compose.yml` を作成します。

```yml:./dockers/docker-compose.yml
version: "3.5"

services:
  app:
    build:
      context: ..
      target: builder
    ports:
      - 8080:8080
    volumes:
      - ../:/app
    command: air
```

### ２−２．Dockerfile

上述の docker-compose.yml を実行するのに必要な Dockerfile を用意します。
アプリのルートディレクトリに下記をコピーして作成してください。

```dockerfile:./Dockerfile
FROM golang:1.15.6-alpine3.12 as builder

RUN apk update \
  && apk add --no-cache git \
  && go get -u github.com/cosmtrek/air \
  && chmod +x ${GOPATH}/bin/air

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .
```

ファイルの中身を詳細に見ていきます。

#### 各種ライブラリのインストール

```dockerfile
RUN apk update \
  && apk add --no-cache git \
  && go get -u github.com/cosmtrek/air \
  && chmod +x ${GOPATH}/bin/air
```

それぞれ細かく見ていきます。

```dockerfile
&& apk add --no-cache git \
```

今回は、軽量な Alpine Linux の Docker image を採用しているため、
Go Modules のインストールに必要な `git` をインストールします。

※使用する Docker image がデフォルトのイメージであれば、インストール済みのため不要です。

```dockerfile
&& go get -u github.com/cosmtrek/air \
```

開発時にホットリロードを有効にしたいので、[Air](https://github.com/cosmtrek/air) をインストールします。

※有名なライブラリで [Realize](https://github.com/oxequa/realize) がありますが、 2020年12月23日現在メンテナンスが活発でないため、
メンテナンスが継続されており、GoModuleにも対応している [Air](https://github.com/cosmtrek/air) を使用します。

```dockerfile
&& chmod +x ${GOPATH}/bin/air
```

上述でインスールした [Air](https://github.com/cosmtrek/air) の実行権限を付与します。 

#### 各種ファイルのビルド/コピー
 
```dockerfile
COPY go.mod go.sum ./

RUN go mod download

COPY . .
```

アプリのルートディレクトリをコンテナ内にコピーする前に
`go.mod` と `go.sum` をコピーして、`go mod download` でライブラリをインストールしています。

こうすることで、次回以降、ソースコードの変更のみ（`go.mod` と `go.sum` に変更がない場合）であれば
ソースコード全体をコピーする `COPY . .` の行から実行されるようになります。
それより前のステップはキャッシュが活用されスキップされ、ビルド速度が向上します。

## ３．Makefileの作成

`docker-compose.yml` 起動のためのコマンドが長くなるので、Makefileを設定して立ち上げやすくします。

```yaml:./Makefile
up:
	docker-compose -f dockers/docker-compose.yml build && docker-compose -f dockers/docker-compose.yml up -d
down:
	docker-compose -f dockers/docker-compose.yml down
logs:
	docker-compose -f dockers/docker-compose.yml logs -f app
```

こうすることで下記コマンドを利用できます。

```zsh
# コンテナを作成、起動
% make up

# コンテナを停止
% make sown

# ログを出力
% make logs
```

## ４．ホットリロード設定

ホットリロードには [Air](https://github.com/cosmtrek/air) を使用します。
公式の [サンプル](https://github.com/cosmtrek/air/blob/master/air_example.toml) を基に、設定ファイルを下記のように作成します。

```toml:./.air.toml
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "build"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./build/main ./cmd"
# Binary file yields from `cmd`.
bin = "build/main"
# Customize binary.
full_bin = "./build/main web"
# Watch these filename extensions.
include_ext = ["go"]
# Ignore these filename extensions or directories.
exclude_dir = ["build"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop to run old binary when build errors occur.
stop_on_error = true
# This log file places in your tmp_dir.
log = "air_errors.log"

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true
```

## ５．サーバー立ち上げ準備

docker-compose 起動時に必要な `go.mod` と `go.sum` を作成するため
下記コマンドを事前に実行しておきます。

```zsh
# モジュール管理ファイルを用意
% go mod init {モジュール名}

# ビルド && 依存するモジュールをダウンロード
% go build
```

## ６．サーバーの立ち上げ

実際に下記コマンドでサーバーを立ち上げましょう。

```zsh
% make up
...
Creating dockers_app_1 ... done
```

http://localhost:8080 にアクセスして、下記の様に返り値が取れれば完了です。

```zsh
% curl 'http://localhost:8080' 
{"message":"hello world"}    
```

# Herokuにデプロイする

いよいよ作成したソースをHerokuにデプロイします。

## １．Herokuアプリの準備

今回は `go-heroku` という名前でアプリを作成します。

別の名前で作成したい場合は、`go-heroku` となっている箇所を
作成したいアプリ名に変えてもらえればOKです。

```zsh
# アプリのルートディレクトリへ移動
% cd /hoge

# herokuログイン
% heroku container:login

# herokuアプリの作成
% heroku create -a go-heroku

# herokuリポジトリをgit登録
% heroku git:remote -a go-heroku
```

## ２．必要なファイルを準備

### ２−１．heroku.yml

Herokuに自分でビルドしたDockerイメージをデプロイするために
アプリのルートディレクトリに下記のような `heroku.yml` を作成します。 

```yml:./heroku.yml
build:
  docker:
    web: Dockerfile
run:
  web: /main
```

詳細な設定については、[こちら](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml)のドキュメントに記載があります。

### ２−２．Dockerfile

上述の `heroku.yml` で実行するため、
ローカル環境用に作成した `Dockerfile` を下記のように修正します。

```diff:./Dockerfile
FROM golang:1.15.6-alpine3.12 as builder

RUN apk update \
-  && apk add --no-cache git \
+  && apk add --no-cache git curl \
  && go get -u github.com/cosmtrek/air \
  && chmod +x ${GOPATH}/bin/air

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .
+  
+ RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o /main ./cmd
+ 
+ FROM alpine:3.12
+ 
+ COPY --from=builder /main .
+ 
+ ENV PORT=${PORT}
+ ENTRYPOINT ["/main web"]
```

#### 各種ライブラリのインストール

```dockerfile
RUN apk update \
  && apk add --no-cache git curl \
  && go get -u github.com/cosmtrek/air \
  && chmod +x /go/bin/air
```

HerokuのUI上でログを残すために、インストールするライブラリに `curl` を追加します。
入れないとリリースが途中で死んでも何故落ちたかが追えなくなるので、入れたほうがいいです。

公式でも以下のようにアナウンスしています。

>
If you would like to see streaming logs as release phase executes, your Docker image is required to have `curl`.
>
出典：[Building Docker Images with heroku.yml | Heroku Dev Center](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#release-configuring-release-phase)

#### ビルド

```dockerfile
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o /main ./cmd
```

クロスコンパイル用の環境変数を指定し、-ldflags="-w -s" でバイナリを削減しています。

#### アプリの実行

```dockerfile
FROM alpine:3.12
```

```dockerfile
COPY --from=builder /main .
```

前のステージでビルドしたアプリケーションを次のステージにコピーしています。
`--from=[NAME]` をコピーするファイルの前に宣言することでコピー元のステージが指定できます。

```dockerfile
ENV PORT=${PORT}
```

先程もHerokuではWeb worker processにポートが紐付けられるので、


```dockerfile
ENTRYPOINT ["/main web"]
```

最後に ENTRYPOINT でアプリケーションを実行しています。


## ３．リリースする

herokuリポジトリをgit登録済みなので、下記コマンドのみでリリースできます。

※サンプルではデフォルトブランチを `main` にしていますが、必要あれば適宜変更してもらってもリリースできます。

```zsh
% git add .
% git commit -m "make it better"
% git push heroku main
```

上記コマンドで `buiｌｄ` から `deploy` までやってくれるので、
あとは完了するのを待つだけです。

完了したら下記コマンドを叩くことで画面で反映確認ができます。

```zsh
% heroku open
```

# 終わりに



今回のサンプルは下記で公開していますので、ご参考までにどうぞ
https://github.com/genki-sano/go-heroku

# 参考記事

- [Heroku](https://dashboard.heroku.com)
- [The Heroku CLI | Heroku Dev Center](https://devcenter.heroku.com/articles/heroku-cli)
- [Runtime Principles | Heroku Dev Center](https://devcenter.heroku.com/articles/runtime-principles)
- [Building Docker Images with heroku.yml | Heroku Dev Center](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml)
- [gin-gonic/gin | GitHub](https://github.com/gin-gonic/gin)
- [cosmtrek/air | GitHub](https://github.com/cosmtrek/air)
- [oxequa/realize | GitHub](https://github.com/oxequa/realize)