はじめに
Go 1.12を使ってAPIサーバを開発するに当たって、開発環境のテンプレートのようなものを作ってみたので紹介します。
APIサーバとMySQLのみの簡易的な構成になっているので、サービスの要件やチームの技術スタックによって適宜アレンジすると良さげです。
まとめ
こんな感じのことをやりました。
- ドメイン駆動設計を参考にしたレイヤ設計をアーキテクチャとして導入
- Dockerを開発環境と本番環境で利用できるように
- Makefileに開発で使うコマンドをまとめる
- CircleCIでテストおよびマイグレーション・デプロイを自動化
解説
それでは今回作った開発環境の簡単な解説をしていきたいと思います。
コードはGitHubに置いているので適宜参照してください。
1. ドメイン駆動設計を参考にしたレイヤ設計をアーキテクチャとして導入
まずは、APIサーバのソフトウェアアーキテクチャにどのようなものを採用したかについて話します。
今回使った設計はpospomeさんのDevFestでのセッション内容を大いに参考にしています。
ですので、さっき話しますと言ったばかりですが、本記事では詳しい説明は割愛します。
僕のDDDやレイヤ設計への理解を深めてくれたセッションで、本当に感謝しています。
少しアレンジしていたり、詳しい実装が紹介されていないところは僕が勝手に補完して実装していたりしますが、大枠は同じだと思うので細かいところはGitHubを見ていただければと思います。
2. Dockerを開発環境と本番環境で利用できるように
次に、ローカル開発環境と本番環境で使うDockerまわりの解説をしていきます。
ディレクトリ構成
まずはDockerまわりの設定ファイルをどのように管理しているかについて説明します。
プロジェクトルートにdocker
ディレクトリを作り、この中にDocker関連のファイルを置いています。
docker
ディレクトリ中のファイル構成は↓のような感じです。
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
とproduction
development
はローカル開発環境で用いるAPIサーバ用のコンテナで、テストでも用いられ、docker-compose.yml
とdocker-compose.test.yml
で立ち上げます。
production
は本番環境で用いるAPIサーバ用のコンテナで、基本的にCIでイメージをビルドしてデプロイするときに用いられます。
分けた理由としては、主にローカル開発環境と本番環境では求められるコンテナの特性が異なるためです。
例えばローカル開発では、ファイルの変更に追従してもらうためにローカルのファイル群がコンテナにマウントされることが多いため、Dockerfile
内でライブラリの依存を解決する必要はありません。
また、ファイルの変更をホットリロードしてローカルサーバを立ち上げ直してほしいため、ホットリロードできるライブラリを使いたかったりもします。
逆に本番環境では、ファイル群をマウントすることはないため、Dockerfile
内で依存を解決する必要があったり、シングルバイナリで動作するというGoの特性を活かすためにビルドしたバイナリを起動するだけのコンテナで良かったりもします(go
がインストールされてなくても問題ない)。
そのため、マルチステージビルドを使ってバイナリをビルドするステージと、そのバイナリを起動するステージに分けています。
mysql
とmigration
mysql
はローカル開発環境で用いるMySQLサーバ用のコンテナで、テストでも用いられ、docker-compose.yml
とdocker-compose.test.yml
で立ち上げます。
文字コードにutf8mb4
を使うために、公式のMySQLイメージそのままではなく、別途設定ファイルを読み込むDockerfile
を作っています。
migration
はローカル開発環境に立ち上がっているMySQLサーバのDBスキーママイグレーションを行うコンテナで、docker-compose.yml
で立ち上げます。
また、migration
は、本番で稼働しているAmazon RDSなどのDBサーバをマイグレーションするECS Taskのコンテナとしても使われる想定をしているため、Dockerfile
を分けています。
本番環境のDBを異なる方法でマイグレーションする場合はマイグレーションの機能自体をローカル開発用のAPIサーバコンテナに入れてしまっても良いかもしれません。
3. Makefile
に開発で使うコマンドをまとめる
続いて、前節で作ったDockerまわりを利用するためのコマンド群をMakefile
にまとめることで、開発効率化を図りつつ、docker
ディレクトリにまとめることで煩雑になってしまったコマンドを簡略化したいと思います。
コマンドの使い方はファイル内に書いています。
ちなみに、↓のようなMakefile
の特性(?)を利用して作っています。
-
make local start
のようにジョブを複数指定すると順番に実行してくれる特性を利用して、1つ目のジョブでは実行するコマンドを変数に代入し、2つ目のジョブでそのコマンドにサブコマンドをつけて実行する、ということを実現 -
test
でしか代入しない変数などを使ってローカル開発(local
)とテスト(test
)のコマンドを抽象化 -
cd docker; ...
のように書くとdocker
ディレクトリに移動してからコマンドを実行してくれる
4. CircleCIでテストおよびマイグレーション・デプロイを自動化
このような設定ファイルを使うことで、テストやマイグレーション・デプロイを自動化します。
各ジョブの簡単な解説
ここでは、設定ファイルに定義した各ジョブの用途を簡単に説明していきます。
cache
とtest
このあたりは特に説明する必要はない気がしますが、よくあるテストを実行するジョブと、テストに用いるコンテナイメージのビルドをキャッシュしているジョブです。
イメージキャッシュのキーに何を使うかが悩みどころになりそうですが、今回はテストに使うコンテナを定義するDockerfile
とテスト用コンテナを立ち上げるdocker-compose.test.yml
、そして依存関係をlockしているgo.sum
のchecksum
を取れば十分だと思います。
MySQLコンテナはコンテナが立ち上がってもMySQLサーバがConnection readyになっていない場合があるので、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をいただけると嬉しいです。