2019-05-24 - ソフトウェア開発, Docker, 社内勉強会

Docker Composeによるローカル開発環境構築

ローカル開発環境の重要性と、ローカル開発環境構築のベストプラクティスのひとつであるDocker/Docker Composeについて述べる。

ローカル開発環境

アプリケーションの開発現場では、大まかに以下のような環境が構築される。

実際の開発の際には上記の他に、ローカル開発環境の構築が望ましい。特に効率の良い開発のために洗練されたローカル開発環境が求められる。

イテレーション

ソフトウェア開発で重要なのは高速なイテレーションである。ここでのイテレーションとは以下の作業を繰り返すことである。

  1. ソースコードの編集
  2. ソフトウェアのビルド
  3. 動作確認・テストの実行
  4. バグ・不具合の特定

高速なイテレーションを実現できれば、ソースコードの編集についての結果や問題がただちに分かり、次の開発作業の方向性を素早く決定していける。 よってイテレーションを高速化することは、開発者の生産性向上につながるといえる。

ローカル開発環境

イテレーション高速化に必要なのがローカル開発環境である。ローカル開発環境とは以下のような特徴を持つ環境である。

ソースコードの編集からビルド・実行・テストを手元のマシンに完結することが特徴である。つまりイテレーションの1~4をローカルマシンに完結して実施できる。 ネットワークを超えたソースコードの転送・反映など時間のかかる作業がイテレーションに入り込まない。そのため他のどの環境よりもイテレーションを高速化できる環境といえる。

ローカル開発環境構築時の問題

イテレーション高速化のために、ローカル開発環境が必要と述べた。しかし、問題はローカル開発環境の構築時にある。洗練されたローカル開発環境構築は意外と難しい。それは以下のようなローカル開発環境特有の問題があるからである。

すなわち、ローカル開発環境構築の問題は、開発者毎に構築される環境差異の問題に収束する。

Docker Composeによる開発環境の抽象化

昨今では上記のようなローカル開発環境構築の課題解消のために、Docker/Docker Composeの利用が一般的となってきた。

差異を吸収するための武器は抽象化である。Dockerはアプリケーション実行環境を抽象化するコンテナ技術を基礎としたツールであり、開発環境の抽象化にも極めて相性の良いツールである。

Docker/Docker Composeによるローカル開発環境の定義及び構築方法を示していく。

Dockerfile

まず開発対象のアプリケーションを動作させるためのDockerコンテナを定義する。プロジェクトルートにDockerfileを配置し、記述していく。ここでは一般的なDockerfileの記述方法は割愛し、ローカル開発環境定義に特有なDockerfile記述法を示す。

WORKDIR

ローカル開発環境のためのDockerfileには WORKDIR を指定すると便利である。このWORKDIRには後述するように、ソフトウェアプロジェクトのルートをボリュームマウントする。

// Dockerfile

WORKDIR /app

尚、WORKDIRの指定場所に決まりはないが、/ 付近としておくとタイプがしやすく地味によい。

CMD

メインアプリケーション実行のためのコマンドを CMD で指定する。開発しているアプリケーションの実行コマンドが自明でないことも多い、CMD に実行コマンドを明記すれば、他の開発者にも決まった方法でアプリケーションを実行してもらえる。

// Goアプリケーションの例
CMD ["go", "run", "./cmd/app"]

// Java/Spring Bootアプリケーションの例
CMD ["./mvnw", "spring-boot:run"]

注意点として、CMD にはビルド自体を内包したコマンドを指定するのがおすすめである。例えば以下のように、ビルドを CMD の前に指定するのは避けたい。

// CMDの前にビルドして、CMDではビルドしない
RUN go build ./cmd/app
CMD ["./app"]

上記の指定の場合、コンテナを再起動のみでは最新コードが反映されず、Dockerイメージの再ビルドが必要になってしまう。ビルド自体を CMD に含めることで、コンテナを再起動のみで最新コードを反映できる。また、確実に最新コードを反映できれば、誤って反映前のコードを動作確認・テストしてしまうミスも防止できる。

尚、CMD の他に ENTRYPOINT による実行コマンド指定も可能である。しかしローカル開発環境には CMD が適する。ENTRYPOINT は、コンテナの実行コマンドが固定化するため、開発中のデバッグ作業などで別の実行コマンドを試したい場合に対応し辛い。

docker-compose.yml

1つのDockerfileは1コンテナを定義するものである。またDockerでは1コンテナにつき1プロセスのみの稼働が推奨されている。よってDockerのみでは例えばDBと接続するアプリケーションなど、複数のプロセスから成るアプリケーションの開発には不十分である。

そこで、Docker Composeを用いる。Docker Composeは複数のコンテナを扱うDocker環境を定義するツールである。メインアプリ用のコンテナの他に、DB専用のコンテナなどを含めた定義が可能となる。 Docker Composeを用いるには、docker-compose.ymlをプロジェクトルートに配置する。

ローカル開発環境構築に有用な設定項目を述べる。

services.<service>.volumes

volumes は、ホストマシン側のディレクトリをDockerコンテナ側のファイルシステムにマウントするための項目である。docker run-v オプションに相当する。 以下のように、プロジェクトルートを指定することで、最新コードをコンテナに反映させられる。

// docker-compose.yml

services:
  app:
    volumes: .:/app

services.<service>.env_file

環境変数設定である。環境変数の設定もローカル開発環境構築時に問題の生じやすい箇所だ。開発者個別での環境変数設定は避けたい。 環境変数の設定自体を間違える可能性があり、ローカル開発環境構築につまづく原因となるからだ。 以下のように、dev/envrcファイルにローカル開発環境用の定番設定を記述しておくことで、このミスの可能性を減らせる。

// docker-compose.yml
services:
  app:
    env_file: dev/envrc
    
// dev/envrc
PORT=8080
DATABASE_URL=root:root@tcp(db:3306)/db

services.<service>.links

複数のコンテナ間を通信させる際に用いる。通常あるコンテナから別のコンテナのホストは見えないが、links に設定することで、設定したホスト名を通して通信可能となる。

特によくあるのはDBへの接続時である。Dockerであれば例えばMySQLのローカル環境を用意するのも容易い。 このとき、MySQLと通信するアプリケーションコンテナについて links を設定する。

services:
  app:
    :
    links:
    - db
    :
  db:
    image: "mysql:5.7"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      TZ: 'Asia/Tokyo'
    command: mysqld
    ports:
    - "3306:3306"

上記ではDB側コンテナ(db)のポート3306を開放している。アプリケーション側コンテナ(app)からは links で指定したホスト名 db でdbコンテナに接続できる。 つまり db:3306 としてMySQLインスタンスにアクセスできる。

Docker Composeによるローカル開発環境の作業フロー

おおよそ以下のような作業フローになる。原則としてdocker-compose.ymlを定義している場合は、生の docker コマンドよりも docker-compose コマンドを通してコンテナを操作すると良いだろう。

コンテナの作成・起動

-d オプションでデーモン起動させる方が、後述する再起動などがしやすいのでおすすめである。

$ docker-compose up -d

コンテナの停止・削除

$ docker-compose down

コンテナの再起動

restart を用いればコンテナの再作成を行わず高速に再起動できる。

$ docker-compose restart

コンテナ内で作業

デバッグ時など、コンテナ内で作業するときは以下コマンドでシェルを起動する。

$ docker-compose exec <service> sh

終わりに

ローカル開発環境の重要性を、イテレーション高速化の観点から述べた。しかしローカル開発環境の構築は、開発者間の環境差異のために、困難な作業となりうる。その解消のためのプラクティスとしてDocker Composeによる手法を紹介した。

周辺のプロダクトでDocker Compose化されていないものがあれば、積極的にDocker Compose化を推進し、開発環境の健全化と開発作業の効率化を図りたい。