golang,docker,mysqlの環境をherokuにデプロイする

# はじめに
言語: golang
コンテナ: docker-compose
RDB: mysql
ORM: gorm
マイグレーション: migrate

な環境をherokuにデプロイするまで結構ハマったので残す。

# コード
https://github.com/pokotyan/study-slack

# 実行したherokuのコマンド一覧

```terminal
$ cd /hoge/huga                    # アプリケーションのコードがあるところに移動
$ heroku container:login           # ログイン
$ heroku create -a app_name        # herokuアプリの作成
$ heroku git:remote -a app_name    # herokuリポジトリをgit登録
$ heroku addons:add cleardb:ignite # mysqlのアドオンを追加
$ heroku config                    # CLEARDB_DATABASE_URLが登録されていることを確認
$ heroku config:set DATABASE_URL="<ユーザー名>:<password>@tcp(<ホスト名>:3306)/<DB名>?parseTime=true" # CLEARDB_DATABASE_URLの値を元にsql.Open()に渡す用の文字列に整形
$ heroku config                    # DATABASE_URLが登録されていることを確認
$ heroku stack:set container       # heroku.ymlを使う時はこれがいるぽい
$ git push heroku master           # リリース

```

# [heroku.yml](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml)
heroku.ymlを使うとCI/CDみたいなことができる。アプリのルートディレクトリに置いて使う。
buildにはdockerのビルドの指定ができる。
releaseにはリリースする際に挟みたい処理があれば書くことができる。ここではマイグレーションの実行をしている。
runはプロセスタイプ[^1]ごとに実行するコマンドを指定する。


```yml:./heroku.yml
build:
  docker:
    web: Dockerfile
    worker:
      dockerfile: Dockerfile
      target: builder 
release:
  image: worker
  command:
    - make up_migrate_prod
run:
  web: /main
```

# Dockerfile

上述のheroku.ymlが参照するDockerfile。アプリのルートディレクトリに置く。
いくつかポイントがある。

```Dockerfile:./Dockerfile
FROM golang:alpine as builder

RUN apk update \
  && apk add --no-cache git curl make gcc g++ \
  && go get github.com/oxequa/realize

WORKDIR /app
COPY go.mod .
COPY go.sum .

RUN go mod download
COPY . .

RUN GOOS=linux GOARCH=amd64 go build -o /main

FROM alpine:3.9

COPY --from=builder /main .

ENV PORT=${PORT}
ENTRYPOINT ["/main"]
```

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

```Dockerfile:./Dockerfile
RUN apk update \
  && apk add --no-cache git curl make gcc g++ \
  && go get github.com/oxequa/realize
```

realizeは開発時のホットリロードのため。
make、gcc、g++はheroku.ymlのreleaseフェーズにてmakeコマンドでマイグレーションを流せるようにするため
curlはherokuのUI上でログを残すため（※）。

※ curlを入れていないとこんな感じで何も表示されない。リリースが途中で死んでもなんで落ちたかが追えなくなるので入れておいた方がいいと思う。[ログを出すためにcurlが必要なことは公式にも記載されている。](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#release-configuring-release-phase)
<img width="1241" alt="スクリーンショット 2020-03-15 0.59.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/6ad632a5-f381-1080-c100-7e611ec93701.png">




## ビルド

builderのイメージはheroku.ymlでイメージのビルドをする際に使ったり、マイグレーションを実行する時のイメージとして利用している。

```Dockerfile:./Dockerfile
FROM golang:alpine as builder
```

```yml:./heroku.yml
build:
  docker:
    web: Dockerfile
    worker:
      dockerfile: Dockerfile
      target: builder # builderのイメージをbuildする際に使う
release:
  image: worker # 上記のworkerのイメージをreleaseフェーズでも使う
  command:
    - make up_migrate_prod # マイグレーションを流す
```

## アプリの実行

`RUN GOOS=linux GOARCH=amd64 go build -o /main` でビルドしたファイルを実行する

```Dockerfile:./Dockerfile
FROM alpine:3.9

COPY --from=builder /main .

ENV PORT=${PORT}
ENTRYPOINT ["/main"]
```

```yml:./heroku.yml
run:
  web: /main
```

# docker-compose.yml
ローカルで開発する時のみに利用するdocker-compose.yml。
realizeを使ってホットリロードするようにしている。
また、mysqlのコンテナが立ち上がる際に `docker-entrypoint-initdb.d` を利用して `CREATE DATABASE` をするようにしている。

herokuの本番環境ではdatabaseは `heroku addons:add cleardb:ignite` で用意されたものを利用する。
そのため、本番環境ではこのdocker-compose.ymlは利用しない。

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

services:
  mysql:
    container_name: push_study_db
    image: mysql:5.7.22
    volumes:
      - ./mysql/:/docker-entrypoint-initdb.d/
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 4306:3306
  app:
    build:
      context: ..
      target: builder
    volumes:
      - ../:/app
    command: realize start --server
    environment:
      - API_VERSION=development
    ports:
      - 7777:7777
    depends_on:
      - mysql
```

# 起動するポート
herokuはアプリが起動するたびにポートが変わるらしい。$PORTを指定して起動するようにする。

```golang
	router.Run(":" + os.Getenv("PORT"))
```


# CLEARDB_DATABASE_URL
mysqlのアドオンを追加するとCLEARDB_DATABASE_URLという環境変数が自動で設定される。
heroku.ymlのreleaseフェーズで流れるようにしたマイグレーションだが、そのコードでは以下のようにしてdbと接続していた。

```golang
	dbURL := os.Getenv("CLEARDB_DATABASE_URL")
	db, _ := sql.Open("mysql", dbURL)
```

リリースを実行すると `invalid memory address or nil pointer dereference` のエラーが出る。

<img width="1233" alt="スクリーンショット_2020-03-15_1_17_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/3401bab5-6427-7e43-9a37-90801a2feaa2.png">

結果として、herokuが自動で作成してくれるCLEARDB_DATABASE_URLの書式をsql.Openが求めている書式に変換する必要があった。[こちらの記事を参考にさせていただきました。](https://qiita.com/touyu/items/87eaa5b2342c0cf7e854#databasego%E3%81%AE%E4%BD%9C%E6%88%90)
冒頭のherokuのコマンド一覧のところでも記載しているが、`"<ユーザー名>:<password>@tcp(<ホスト名>:3306)/<DB名>?parseTime=true"` の形にしてあげる必要があった。

# 最後に
herokuのリリース方法、色々ありすぎてまとまった情報を見つけるのが難しい。

[^1]: ここで使ってるのはwebのプロセスタイプ。[herokuが動くコンテナであるdynoのプロセスタイプは他にworker、one-offの計３つのプロセスタイプがあるぽい](https://jp.heroku.com/dynos/configure)