Elixir
ポエム
docker
docker-compose

Elixir のチームでの開発環境について

この数ヶ月間は、社内で利用するための Elixir の Webフレームワークを作るのに注力していて、今も開発を続けています。
その開発で、Elixir の開発環境やルールをどうしているのかについて書きます。

開発環境について

開発環境は、各人でバージョン揃えるのが大変という問題があります。
例えば以下のアプリケーションのバージョンを考慮する必要があります。

  • Erlang のバージョン
  • Elixir のバージョン
  • NodeJS のバージョン
  • MySQL のバージョン
  • Redis のバージョン

これらを、新しい人が入る度に指定したバージョンでインストールしてもらうのも大変だし、全員でバージョンを揃えるのも大変です。
また、開発中も統一してバージョンを上げていきたいし、そのバージョンは出来る限り最新にしたいところです。

ローカル環境でこれをやり続けるのはかなり大変なので、Docker と Docker Compose を使うことにしました。

Erlang や Elixir 等の特定のバージョンを入れた Docker イメージを作り、それを全員に使ってもらうようにします。
MySQL や DynamoDB、Redis といった DB も全部 Docker で動かします。

こうすることで、各開発者はローカルに Docker と Docker Compose を入れるだけで良くなります。開発にあたって Elixir や MySQL 等をインストールする必要はありません。
あとは docker-compose 経由で実行すれば、指定したバージョンの MySQL や Redis 等の Docker イメージが無ければ勝手にダウンロードし、それらを起動してくれます。

また、docker-compose.yml で指定するイメージのタグ名には、必ずバージョンを含めるようにします。
新しいバージョンの Erlang や Elixir がリリースされたら、それを含んだ Docker イメージを作り、タグ名を変えて push し、docker-compose.yml のタグ名を新しいものに書き換えるだけです。
これで、開発者全員に新しいイメージが自動的に落ちてくるようになります。

こうすることで、誰か一人がアップデートの作業をすれば全員が新しい環境になります。そのため各アプリケーションの更新を素早く行えるようになります。
例えば Elixir 1.5.3 は 2017年12月20日にリリースされましたが、2日後の2017年12月22日に全員の開発環境が Elixir 1.5.3 になりました。
このように、Docker と Docker Compose を使うことで、チーム全体の開発環境をかなり早く、楽に更新できるようになりました。

latest タグ

Docker イメージの latest タグは使わない方針です。
同じバージョンで統一するために使っているのに、イメージを pull してきた時期によって利用するバージョンが異なるのは目的に反するためです。

Makefile

docker-compose コマンドを毎回書くのは面倒なので、やりたいことは全部 Makefile で実行するようにしています。
例えば make test を打つと docker-compose run --rm servicename mix test が実行されるようにしています。

正確には、依存してる MySQL や Redis がちゃんと起動するまで待つ必要があるので、Docker イメージの中に Dockerize を入れておき、ポートが開いていることを確認してから mix test を実行するとかもしているため、実際のコマンドはもっと長いです。

その他のマイクロサービス

社内で利用するゲーム用の認証サーバや課金サーバといったマイクロサービスも Docker イメージで提供していて、make test 時にこれらのイメージも一緒に起動するようにしています。

これによって、ローカルで気軽に本番と同等の認証や課金が試せるようになっています。

コンテナ起動しすぎじゃない?

現在、この Web フレームワークを利用して make test を打つと 12個 のコンテナが起動するようになっています。
正直起動しすぎだと思うのですが、今のところは特に問題なく管理できています。

各マイクロサービスは、依存ライブラリのバージョンを上げるために結構更新してますが、バージョン上げて Docker イメージを作って push して docker-compose.yml を書き換えるだけなので、そんなに手間では無いです。

また、コンテナの起動と終了には時間が掛かりますが、一度起動したら起動しっ放しにしているので、初回の実行以外はそんなに時間が掛かったりしません。

Mac で遅い問題

Performance issues, solutions, and roadmap に書いてあるように、Docker で osxfs のディレクトリをマウントすると、めちゃめちゃ遅くなります。
ただ、以下の理由があり、しばらくは我慢して遅いままやってました。

しかし、delegated はドキュメントに仕様だけ書かれて、中身が一向に実装される気配が無いので、仕方なく docker-sync を使うことにしました。

以下のような問題が起きたりしましが、今のところ何とかやっていけています。

  • ローカル上で消した後、コンテナ上に反映される前にコマンドを実行してしまって、変な動きになってしまった
  • ローカル上とコンテナ上で同じファイルを操作してしまい、コンフリクトが起きた
  • 同期するファイル(≒監視するファイル)が増えすぎて CPU を使いまくってしまった
  • docker-sync.yml の同期しないファイルの一覧を編集したら、必要なファイルまで同期しなくなってしまった
  • コンテナ上からローカル上に同期しなかったファイルの状態を確認するために手間が掛かるようになってしまった

ひとまず、docker-sync はいつでも外せるようにしたいので、docker-sync が無くても動作するようにしました。
あとは早く delegated が実装されるのを祈るだけです。

(2018/10/11 追記)諦めた

いつまで経ってもまともにならないので、諦めてチーム全体で asdf を使うようにしました。
ただし、各マイクロサービスは変わらず Docker で提供するので、Docker も必要になります。

インストール手順が増えるのと環境依存の問題が発生する確率が上がるのが難点ですが、かなり高速になったので割と快適になりました。

マルチプラットフォーム

Docker を導入した時は特に考えてなかったのですが、どうやら Windows でも動くようです。
Makefile があるので Windows Subsystem for Linux を入れる必要はあるみたいですけど、Windows でも割と簡単に導入できるならかなり良い感じです。

ルールについて

多人数1で開発しているので、いくつかルールを決めています。
基本的には「開発者を信用する」というスタンスです。

悪いことをする前提でルールを考えるとか地獄すぎるし、そのようなルールが守られる気もしないし、守らせるのも難しいでしょう。

コーディングルール

コーディングルールは、ほぼ口伝&既存のコードの空気を読め&レビューで指摘、という状態なので、どこかでルールを纏めたいと思っています。

ひとまず、このあたりを読むように言っておけば良さそうかなと思ってます。

インデントや空白といった、書き方のスタイルに関しては、今はしない という方針です。
Elixir 1.6 でフォーマッタが入るので、それが入ったら、基本的にはそれに合わせるだけでいいだろう、という感じでやっています。
まあフォーマッタを入れてなくても、開発者はみんな空気が読める人ばっかりなので、そこそこスタイルが揃ってます。

Lint

コードレビューはとても時間が掛かるので、可能な限り楽をするために各種 Lint を使っています。

まず、警告が出た時点でエラーとして扱う warnings_as_errors: true を入れて、警告を無視できないようにしています。

また、credo を使って問題のありそうなコードを検出しています。
基本的にはデフォルトの設定でやっていますが、TODO や FIXME の警告に関しては除けています。

TODO や FIXME は、それが必要だと思ったから書いているので、credo に警告されても直しようがありません。
そのため credo の警告を無視する習慣が付くか、あるいは TODO や FIXME の記述を避けるようになってしまいます。
なので、TODO や FIXME に関しては警告を出さないようにしています。

Dialyzer は、遅すぎるので使うのをやめました。
CI で実行する時にチェックすればいいやという方針にしました。

ただ、CI 環境はまだ作れていません。早く作りたい…。

テスト

テストは「自分が書いたコードが正しく動くかどうか不安になってるのを解消する」という目的で書きます。
コードの品質が、とかカバレッジが、とかはあまり考えなくていいやという方針です(カバレッジを取らないという意味ではないです)。

不安を解消するためなので、そのために必要だと思えば Elixir で private な関数をテストする で書いた方法を使って private な関数もテストします。

プルリクエスト

開発した機能は、プルリクエストを出してもらってレビューを行います。

プルリクエストを出す時には、結構詳しく書いてもらうようにしています。
「その機能を書いた人が世界で一番そのコードについて詳しいんだから、レビューして欲しいなら一番詳しいお前が説明を書け」という方針です。
なのでレビューする際には、コードの意図や説明が分からないなら、コードから意図を頑張ってエスパーするのではなく「分からない」と伝えるようにしています。

また、プルリクエストのコメントだけでコミュニケーションを頑張ったりしない方針です。
開発者同士の席は近くにあるので、書いた人の横に行って「これよく分からないんだけど」と言って説明して貰う方が20倍ぐらい早いです。
なので、レビューしてる人数が5人ぐらいまでだったら、一人ひとりに説明していっても十分に早くなるでしょう。

プルリクエストのマージについては、「プルリクエストを出した人がマージする」「いつでもマージしても構わない」 という方針です。
レビューを受けて、自分のコードが問題無さそうだと思った時点でマージしてもいいし、急ぎでそれが必要なら誰にもレビューを受けてない状態でマージしても構いません。

危険そうなルールに見えますが、普通に考えて、動くかどうか不安に思ってるコードを勝手にマージなんてしないし、すぐにマージしたいけど動くか不安なら直接レビュワーに今すぐレビューをやって欲しいってお願いしに行くし、レビュワーもそうなったらよっぽどのことが無い限り断ったりしないでしょう。

リリース

書いてるWebフレームワークは、アプリ開発をする各部署で使われるので、開発中のものを使ってもらうのではなく、確実に「リリース」という単位で区切って、それを使ってもらうようにしています。

その際に CHANGELOG を書いて、どんな機能が追加されたり変更されたのかを各部署に伝えるようにしています。

最低でも月に一度、依存ライブラリや Elixir 本体のバージョン更新のためにアップデートしていく方針でやっています。
(今はゴリゴリ書いてるので機能も追加されていってますが)。

これは、今まで自分達が何をしてきたのかもすぐに分かるし、成果として報告もしやすいので、そういう意味でも結構役立っています。

また、Elixir アプリケーションをパッケージ化しよう で書いたように、アプリケーションをデプロイする時にはパッケージ化をしています。

パッケージ化したら、そのファイルを S3 にアップロードし、CodeDeploy を使っていい感じにデプロイをする予定です。

まとめ

Elixir を複数人で開発するということで、新しく環境やルールを整備していきました。
今は一応回っているように見えるので、今後も、よりうまく回るように環境周りを整備していければと思います。

完走した感想

Elixir 一人 Advent Calendar 2017 を無事 25 日埋められました。
Elixir の日本語の情報を結構出せたんじゃないかなと思います。

また、記事を書くためにいろいろ調べたり、ネタを見つけるために Elixir のコードや issue やコミットログを見たりしてたこともあり、個人的にも大分勉強になりました。
自分はアウトプット駆動学習が向いているようです。

今回の Elixir 一人 Advent Calendar 2017 の記事は、11 月ぐらいからちょっとずつ記事を書いていました。
なので 去年 に比べれば大分書けたんじゃないかなと思います。

一人 Advent Calendar、1, 2ヶ月間、遊ぶ時間が大量に減ってしまうことを除けばオススメです。みんなやりましょう。


  1. 2〜5人ぐらい。1人ではないという程度の意味です。