【Go 1.12】Goの開発環境テンプレート【DDD/Docker/Makefile/CircleCI】

# はじめに

Go 1.12を使ってAPIサーバを開発するに当たって、開発環境のテンプレートのようなものを作ってみたので紹介します。
APIサーバとMySQLのみの簡易的な構成になっているので、サービスの要件やチームの技術スタックによって適宜アレンジすると良さげです。

# まとめ

こんな感じのことをやりました。

1. ドメイン駆動設計を参考にしたレイヤ設計をアーキテクチャとして導入
1. Dockerを開発環境と本番環境で利用できるように
1. Makefileに開発で使うコマンドをまとめる
1. CircleCIでテストおよびマイグレーション・デプロイを自動化

# 解説

それでは今回作った開発環境の簡単な解説をしていきたいと思います。
コードは[GitHub](https://github.com/snowman-mh/go-sample)に置いているので適宜参照してください。

## 1. ドメイン駆動設計を参考にしたレイヤ設計をアーキテクチャとして導入

まずは、APIサーバのソフトウェアアーキテクチャにどのようなものを採用したかについて話します。
今回使った設計は[pospomeさんのDevFestでのセッション内容](https://www.pospome.work/entry/2017/10/11/023848)を大いに参考にしています。
ですので、さっき話しますと言ったばかりですが、本記事では詳しい説明は割愛します。
僕のDDDやレイヤ設計への理解を深めてくれたセッションで、本当に感謝しています。
少しアレンジしていたり、詳しい実装が紹介されていないところは僕が勝手に補完して実装していたりしますが、大枠は同じだと思うので細かいところは[GitHub](https://github.com/snowman-mh/go-sample)を見ていただければと思います。

## 2. Dockerを開発環境と本番環境で利用できるように

次に、ローカル開発環境と本番環境で使うDockerまわりの解説をしていきます。

### ディレクトリ構成

まずはDockerまわりの設定ファイルをどのように管理しているかについて説明します。
プロジェクトルートに[`docker`](https://github.com/snowman-mh/go-sample/tree/master/docker)ディレクトリを作り、この中にDocker関連のファイルを置いています。
`docker`ディレクトリ中のファイル構成は↓のような感じです。

```bash
docker/
├── docker-compose.yml       # ローカル開発用コンテナサービス群を立ち上げる
├── docker-compose.test.yml  # テスト用コンテナサービス群を立ち上げる
├── .env.default             # .envにコピーして使う
├── development
│   └── Dockerfile           # 開発用のAPIサーバコンテナ
├── migration
│   └── Dockerfile           # MySQLをマイグレーションするコンテナ
├── mysql
│   ├── Dockerfile           # MySQLコンテナ
│   ├── conf.d
│   │   └── my.cnf
│   └── data
└── production
    └── Dockerfile           # 本番用のAPIサーバコンテナ
```

作りたいDockerコンテナごとにディレクトリを作成し、その中に`Dockerfile`を置くようにしています。
こうすることで`Dockerfile`のファイル名を守れる（`Dockerfile.prd`とかしなくていい）ようになってディレクトリ構成がキレイになるので、良いかなと思っています。

プロジェクトルートに`docker-compose.yml`や`Dockerfile`を置くプロジェクトが多いと思いますが、それと比べて本記事のディレクトリ構成を取るデメリットとしては、`docker-compose up`したりするときのコマンドが複雑になる（ディレクトリを移動したり`-f`オプションでファイル名を指定したり）ことが挙げられます。
その点については、後述の`Makefile`で改善したいと思います。

### 各コンテナの簡単な解説

ここでは、`Dockerfile`で定義した各コンテナの用途と使い分けについて説明していきます。
等幅フォントになっているところにGitHubへのリンクが貼られていたりするので、適宜参照してください。

#### [`development`](https://github.com/snowman-mh/go-sample/blob/master/docker/development/Dockerfile)と[`production`](https://github.com/snowman-mh/go-sample/blob/master/docker/production/Dockerfile)

[`development`](https://github.com/snowman-mh/go-sample/blob/master/docker/development/Dockerfile)はローカル開発環境で用いるAPIサーバ用のコンテナで、テストでも用いられ、`docker-compose.yml`と`docker-compose.test.yml`で立ち上げます。
[`production`](https://github.com/snowman-mh/go-sample/blob/master/docker/production/Dockerfile)は本番環境で用いるAPIサーバ用のコンテナで、基本的にCIでイメージをビルドしてデプロイするときに用いられます。

分けた理由としては、主にローカル開発環境と本番環境では求められるコンテナの特性が異なるためです。
例えばローカル開発では、ファイルの変更に追従してもらうためにローカルのファイル群がコンテナにマウントされることが多いため、`Dockerfile`内でライブラリの依存を解決する必要はありません。
また、ファイルの変更をホットリロードしてローカルサーバを立ち上げ直してほしいため、ホットリロードできるライブラリを使いたかったりもします。
逆に本番環境では、ファイル群をマウントすることはないため、`Dockerfile`内で依存を解決する必要があったり、シングルバイナリで動作するというGoの特性を活かすためにビルドしたバイナリを起動するだけのコンテナで良かったりもします（`go`がインストールされてなくても問題ない）。
そのため、マルチステージビルドを使ってバイナリをビルドするステージと、そのバイナリを起動するステージに分けています。

#### [`mysql`](https://github.com/snowman-mh/go-sample/blob/master/docker/mysql/Dockerfile)と[`migration`](https://github.com/snowman-mh/go-sample/blob/master/docker/migration/Dockerfile)

[`mysql`](https://github.com/snowman-mh/go-sample/blob/master/docker/mysql/Dockerfile)はローカル開発環境で用いるMySQLサーバ用のコンテナで、テストでも用いられ、`docker-compose.yml`と`docker-compose.test.yml`で立ち上げます。
文字コードに`utf8mb4`を使うために、公式のMySQLイメージそのままではなく、別途設定ファイルを読み込む`Dockerfile`を作っています。

[`migration`](https://github.com/snowman-mh/go-sample/blob/master/docker/migration/Dockerfile)はローカル開発環境に立ち上がっているMySQLサーバのDBスキーママイグレーションを行うコンテナで、`docker-compose.yml`で立ち上げます。
また、`migration`は、本番で稼働しているAmazon RDSなどのDBサーバをマイグレーションするECS Taskのコンテナとしても使われる想定をしているため、`Dockerfile`を分けています。
本番環境のDBを異なる方法でマイグレーションする場合はマイグレーションの機能自体をローカル開発用のAPIサーバコンテナに入れてしまっても良いかもしれません。

## 3. [`Makefile`](https://github.com/snowman-mh/go-sample/blob/master/Makefile)に開発で使うコマンドをまとめる

続いて、前節で作ったDockerまわりを利用するためのコマンド群を[`Makefile`](https://github.com/snowman-mh/go-sample/blob/master/Makefile)にまとめることで、開発効率化を図りつつ、`docker`ディレクトリにまとめることで煩雑になってしまったコマンドを簡略化したいと思います。
コマンドの使い方はファイル内に書いています。
ちなみに、↓のような`Makefile`の特性(?)を利用して作っています。

- `make local start`のようにジョブを複数指定すると順番に実行してくれる特性を利用して、1つ目のジョブでは実行するコマンドを変数に代入し、2つ目のジョブでそのコマンドにサブコマンドをつけて実行する、ということを実現
- `test`でしか代入しない変数などを使ってローカル開発(`local`)とテスト(`test`)のコマンドを抽象化
- `cd docker; ...`のように書くと`docker`ディレクトリに移動してからコマンドを実行してくれる

## 4. CircleCIでテストおよびマイグレーション・デプロイを自動化

[このような設定ファイル](https://github.com/snowman-mh/go-sample/blob/master/.circleci/config.yml)を使うことで、テストやマイグレーション・デプロイを自動化します。

### 各ジョブの簡単な解説

ここでは、設定ファイルに定義した各ジョブの用途を簡単に説明していきます。

#### `cache`と`test`

このあたりは特に説明する必要はない気がしますが、よくあるテストを実行するジョブと、テストに用いるコンテナイメージのビルドをキャッシュしているジョブです。
イメージキャッシュのキーに何を使うかが悩みどころになりそうですが、今回はテストに使うコンテナを定義する`Dockerfile`とテスト用コンテナを立ち上げる`docker-compose.test.yml`、そして依存関係をlockしている`go.sum`の`checksum`を取れば十分だと思います。
MySQLコンテナはコンテナが立ち上がってもMySQLサーバがConnection readyになっていない場合があるので、[jwilder/dockerize](https://github.com/jwilder/dockerize)を使って待機しています。

#### `migrate`と`deploy`

今回はECS Taskを用いて本番環境で稼働しているAmazon RDSなどをスキーママイグレーションすることを想定しているため、`migrate`ジョブではマイグレーション用のイメージをビルドしてTaskを実行したりします。
そのとき、直前のマージコミットの中にマイグレーションファイルの変更が含まれていたらマイグレーションを実行するようにしています。
また、マイグレーションとAPI変更を同じPull Requestで行っても問題ないように、この`migrate`ジョブは`deploy`ジョブよりも先に実行します。

`deploy`ジョブでは、APIサーバの本番用のイメージをビルドし、Amazon ECSのServiceなどにデプロイします。
また、先にマイグレーションが行われているはずですが、`migrate`ジョブのECS Taskが成功していることを確認してからAPIサーバのデプロイを行いたいため、`migrate`ジョブと`deploy`ジョブの間に`approve`ジョブを挟んでいます。
CircleCIでは、Workflowのジョブに`type: approval`を指定すると、CircleCIのワークフロー画面からApproveしないとWorkflowを先に進めない、というジョブを作ることができます。
これを利用して、成功を確認してからデプロイを進める、ということを実現しています。
Approveするのを忘れることが想定されるため、Slackなどにリマインダーなどを送ることを検討してもいいかもしれません。

# おわりに

**いかがでしたでしたか？参考になれば幸いです。**

最初はこのくらいの最小構成から始めて、制作物の要件変更やチーム開発の状況に合わせて最適化していくのが良いのではないでしょうか？
こうしたほうがいいんじゃないか、とかあればコメントとかPull Requestをいただけると嬉しいです。