webサービス開発における良い開発環境

最近、「webサービス開発における良い開発環境とは何か？」を自問する機会があったため、その内容を記事としてまとめました。
筆者が考える良い開発環境の条件と、それを実現するための具体的な方法を交えて記載していきます。
（もちろん、記載している条件に合致していることが全てのケース/シーンで「良い」ものであるとは限りませんが）

誰かが新規に開発環境を作る際、また、既存の開発環境を改善する際の参考になれば幸いです。

# 前提
下記のようなサービス開発を前提とします。

- webサービスの開発
- OSSではない[^1]
- VCS[^2]によって管理されている
- 開発環境とは以下の2つを内包する
  - ローカルマシンまたは仮想環境上でソースコードを編集し、テスト/静的解析を実行する
  - ローカルマシンまたは仮想環境上でサービスを起動する

また、技術的な開発環境についてのみ言及します。
開発フロー、コーディング規約、はてはオフィス環境まで広義では開発環境に含まれますが、今回はそこには触れません。

# 良い開発環境
それでは、良い開発環境の条件や具体例を紹介していきます。

## 単一のドキュメントとして構築手順が記載されている
筆者は開発環境の構築手順がドキュメントとして記載されていることが最も大事なことの1つであると考えます。
開発環境を構築しようと思ったが、知っている人に聞かないと手順の場所もわからない、という状態は**ドキュメントが無いものと等しい**と言っても過言ではないでしょう。

サービスのソースコードであればREADME.mdに記載し、ソースコードへのアクセス時に自然と目に入るようにするのがベストでしょう。
（ライブラリの場合は、開発環境よりも利用方法等を記載したいため別ドキュメントにすることがしばしばあります）

また、**単一の**ドキュメントとしてまとまっていることが重要であると考えます。
例えば、以下のようなドキュメントに出会ったことはないでしょうか？

- 依存ミドルウェアのインストール方法が複雑なため、別ドキュメントに分かれている
- 「構築時にハマった場合は」などの補助FQAドキュメントがある

これらはそもそも、「単一のドキュメントに記載するには適さないほど多量の情報が必要である」という場合に起こりがちです。
ハマるポイントが多い、手順が複雑すぎるといった点を見直し改善すべき事案だと考えます。


## 何時構築しても、同じ環境が構築される
**「久々に開発環境を構築し直したら、エラーが出た」**という経験をされた方は多いのではないでしょうか？
これは主に2通りの原因に大別できます。

1. インストールするミドルウェア、依存パッケージのバージョンを固定していないため
2. バージョンを固定しているが、配信側から削除されていたため

1に関しては、インストールするミドルウェア/依存パッケージのバージョンを固定することで対応できます。
最新をインストールする場合だと、互換性のあるうちは問題ないのですが、メジャーバージョンアップ等で互換性が切れたタイミングでエラーが発生するようになってしまいます。

例えばnginxのインストールであれば、下記のようにバージョンを固定するのが良いでしょう。

```sh
# Debian系の場合
sudo apt install nginx=1.14.0-0ubuntu1.6
# Macの場合
brew install nginx@1.14
```

依存ライブラリであれば言語/パッケージ管理システムによってバージョンを固定する方法があるので、それらのlockファイル等をリポジトリで管理しましょう。
詳しくは後述の[依存パッケージのバージョンが管理されている](#依存パッケージのバージョンが管理されている)にて紹介します。

2のパッケージ配信サーバー側から削除された場合に関しては、どう対応すべきでしょうか？
該当バージョンが永遠にホスティングされるという保証はありません。

削除される場合の理由としては大抵、「脆弱性が見つかった」「相当古くなった」のどちらかでしょう。
どちらのケースだとしても、本番環境と開発環境とで利用するミドルウェア、パッケージのバージョンを統一していれば問題にはならないでしょう。
なぜなら、本番環境のバージョンを更新する際に、同時に開発環境のバージョンも更新されるためです。


## ミドルウェアのバージョンが管理されている
本番環境と開発環境の関係はどうあるべきでしょうか？

筆者の考える理想は下記です。

- ミドルウェア/パッケージの管理は全てコード化されている
- 本番環境、テスト環境、開発環境も全て同じコードから構築される
- 環境の差異は環境変数で制御する

さて、ここでどのレイヤーまで各環境間で同一に保つべきかという疑問が生まれます。
OSレイヤーまで同一に構築すべきでしょうか？
もしくはミドルウェアのレイヤーまでで十分でしょうか？

これはサービスの特性に依存すると思われます。
例えば、処理内容や言語によってはOSが提供するカーネル関数によって処理方法が変わってくる場合もあるでしょう。このような場合はOSレベルで本番と同等の環境を構築するのが望ましいです。

しかし、当然ながらOSレベルまで本番環境と同一の環境を開発環境として用意しようとした場合、仮想化の関係上、消費リソースや構築/起動時間が増加してしまいます。
そこで、OSの差異がクリティカルでないようなサービスや言語の場合はミドルウェアレベルまで本番と開発環境で統一するといった、トレードオフを考える必要があるでしょう。
最近の大抵の言語は、特殊な外部コマンドに依存するようなパッケージを利用しない限り、LinuxとMac程度の差異であれば吸収してくれるように設計されているものがほとんどです。
なので、大抵の場合は本番環境と開発環境はミドルウェアレイヤーまで統一しておくのが最もコストパフォーマンスに優れた選択であると思われます。

開発環境を構築する際に依存するミドルウェアがDB1つとか、少ない場合はインストール手順をドキュメントに記載しておく程度でよいでしょう。

インストールすべきミドルウェアがいくつもある場合は、インストールスクリプトや構成管理ツール等を用いてコード化しておくのも良いでしょう。

ミドルウェアレイヤーをコード化して管理/構築する具体例としてここでは2つ紹介します。

### Dockerを使い、ミドルウェアレイヤーを構築する
本番環境がKuernetesやECS/GCEといったコンテナ環境で構成されている場合は、特に悩む必要もないでしょう。
本番環境と同じイメージを利用すればよいだけです。

または、本番環境のイメージには極力不要なパッケージはインストールせず、軽量なイメージを保つという場合は、本番環境のイメージ作成と同時に、開発環境で必要なパッケージ(例：vim, bash, curl等)をインストールしたイメージを作成するのも良いでしょう。

### 構成管理ツールを使い、ミドルウェアレイヤーを構築する
本番環境が非コンテナ環境である場合は、ミドルウェアレイヤーの構築やOSの設定には構成管理ツールを利用するのが良いでしょう。
代表的な構成管理ツールとしては、[Ansible](https://www.ansible.com/)、[Chef](https://www.chef.io/)、[Puppet](https://puppet.com/#)等が有名です。

開発マシンが本番環境と同一OS（たいていの場合はLinux）であれば、ほとんど同じスクリプトを開発マシンに対しても実行することで、本番環境と同等の環境が構築できるでしょう。
しかし、複数のサービスを開発するケースや、本番環境と開発マシンのOSが異なる場合はそうもいきません。
この場合は[Vagrant](https://www.vagrantup.com/)等の仮想マシン管理ツールを利用し、仮想マシンを開発マシン上に構築し、その仮想マシンに対して構成管理ツールで環境構築を実行する構成をとることが多いです。

この方法であれば、OSを問わず、本番環境と同等の開発環境を構築することが可能です。
ですが、ご存知の通り仮想マシンはDockerコンテナに比べて遥かにオーバーヘッドが大きく、起動に時間が必要で、リソース消費も大きいです。
これらのことから、本番環境をコンテナ化するべき理由の1つとしても挙げられています。

## 依存パッケージのバージョンが管理されている
最近の開発環境では当たり前に実施されているかと思います。
各言語、各パッケージ管理システムにはバージョンを指定する機能やlockファイルのような依存パッケージまで含めたバージョン固定機能が実装されています。
例：Ruby/gemのGemfile.lock、Node.js/npmのpckage-lock.json等


## CIの環境も本番環境、開発環境と同じである
CI(継続的インテグレーション)の環境に関しても重要です。
ただテストを実行できる環境を用意するだけでは、CI環境でだけ失敗するテストが発生する等、問題が発生するケースがあります。
ミドルウェアや外部コマンド等、極力本番環境/開発環境と同一のものを用意するのが望ましいです。

一方でCircleCI等、SaaS系のCI環境では、SaaS側で用意したイメージをベースに使用することで、キャッシュが利用され起動速度が向上するケースもあります。
本番環境/開発環境と同等のイメージを用意するためには自身で作成したカスタムイメージを利用するのが一番良いですが、セットアップのスピードとはトレードオフになるでしょう。


## 依存システムがMock化されている
複数のサービスが協調して動作するサービスの場合について考えます。

### リリースタイミング/開発チームが同じ複数のサービスの場合
1つのチームで開発する複数のサブシステムの場合が該当します。
こちらは1つの開発環境として提供する形でも問題ないでしょう。
環境のメンテナンスも同じチーム内なので自由にできますし、システム間の連携も密である場合が多いので、開発環境を分割するデメリットも多くなってしまいます。

### リリースタイミング/開発チームが異なる複数のサービスの場合
認証機能とメイン機能がサービスとして別れており、開発チームも別であるケース等が例としてあげられます。
このようなケースでは、下記理由からメイン機能の開発環境では、認証機能はMockを利用するのが良いと筆者は考えます。

- Mockのほうが認証サービスを起動するより省リソースである
- 認証サービスをメインサービスの開発環境に組み込むと、追従のコストが発生する
- サービス間のインターフェースは頻繁に変更されることはない

OpenAPIやgRPC等のドキュメントの定義、Mockの作成が用意なフレームワークを活用することで、より容易になるはずです。

## サービスの起動が早い
開発環境でサービスを実際に起動し、挙動を確かめるシーンは少なくありません。
この時に毎回データの生成や、外部システムへの通信等が発生すると、起動が遅くなり生産性が下がってしまいます。

筆者は下記観点でサービスの起動の高速化を図ることがあります。

- 依存サービスのMockを活用し、起動を早める
- テストデータ等の永続化のライフサイクルを工夫する

## 省リソースである
残念なことに開発者のマシンのCPU/メモリは無限ではありません。
また、複数の開発を並行で行わなければならない場合も存在します。

1つの開発環境がメモリを数GBも必要としてしまうような状況はあまり好ましくありません。
もし、現在このような開発環境になってしまっている場合は、依存サービスのMock化や不要なミドルウェアのオプション化等で見直していくのがよいでしょう。

## テスト、静的解析の実行が容易で早い
テストコードの実行やLintツールなどの静的解析が気軽にかつ高速に実行できることも重要です。
これらの実行スピードは生産性に直結します。

MacOSの場合、下手にDockerコンテナ内でこれらを実行しようとすると、「Docker for Mac遅い問題」によりテストの実行に異常に時間がかかってしまいます。テストや静的解析はサービスを実際に動作させるデバッグに比べて依存ミドルウェアが少なくすむはずなので、Macの場合はローカル環境で実行できるように整備しておくのが良いでしょう。

最近ではエディタからテストを実行したり、自動で静的解析をかけるプライグインも豊富なため、これらを活用して生産性を高めていきたいところです。


## 適用が任意の設定に関しては、オプションとして提供されている
以下のようなシーンに遭遇した方は多いのではないでしょうか？

- ある機能の開発/検証時のみ利用したいミドルウェア(コンテナ)がある
  - しかし、全員の環境に導入するとリソース消費が増えるだけだ
- 開発環境の動作に影響のある、とある設定値を変更したい
  - しかし、変更したい派と変更したくない派でわかれてしまっている

サービスが成長し大きくなってくると、機能の複雑化、開発メンバーの増加等で上記のようにある種の意見の対立が発生することもあるでしょう。これらの対処としては、筆者としては（筋が通っていれば）どちらの意見も尊重し、共存できる開発環境にすることが望ましいと思います。
環境をガチガチに固定することで、安定を優先することも可能ですが、開発メンバーのDXが損なわれてしまうのは良くないことだと考えます。

こういったケースに関しては、環境変数や設定ファイルで個人ごとに制御できるように構築するのが望ましいと考えます。
筆者の場合は最近は、[direnv](https://github.com/direnv/direnv)を利用して個人のローカルに環境変数の設定ファイル(`.envrc`ファイル)を用意し、そこで構成や設定を切り替えられるように構築する場合が多いです。

`docker-compose.yml`に加え、特定の機能開発時のみで追加したいコンテナの設定を記載した`docker-compose.xxx-dev.yml`等を用意し、下記のような`.envrc`を用意します。

```shell:.envrc
# .envrcは.gitignoreに登録し、リポジトリには含めません
export COMPOSE_FILE=docker-compose.yml:docker-compose.xxx-dev.yml
```

このように定義し、direnvの設定を有効化することで、`docker-compose`コマンドからは`docker-compose.yml`と`docker-compose.xxx-dev.yml`がマージされた設定として扱われます。

参考記事：https://qiita.com/reireias/items/ecb81e248314253eb156#multiple-compose-files

# まとめ
良いwebサービスの開発環境の条件をいくつかご紹介しました。
サービスによっては当てはまらない例もあるものの、誰かのサービス開発環境の改善の一助となれば幸いです。

# 余談
開発環境に関するブログや記事は多くないため、みなさんが普段どのような環境で開発しているか、どこに課題があるか等けっこう気になります。

[^1]: OSSの場合はコントリビューターを増やしたいか、どこまで安定したリポジトリにしたいか等で事情が変わってくるため、今回は対象外としています。
[^2]: バージョン管理システム（Git、Subversion等）。