ikasama over technology

忘れたくないことを忘れないために

docker-compose で複数環境を構築するときの設定をなるべく DRY に書く

概要

  • docker-compose-f, --file オプションを複数使って、共通の Composeファイル と環境ごとの Compose ファイルを読み込むようにします
    • こうすることで、共通の設定を DRY に書けます
  • -p, --project-name オプションと networks で環境を分離します
  • 以下のリファレンスの内容が理解できていればこの記事を読まなくても大丈夫です

docs.docker.com

docs.docker.com

背景

docker-compose, 便利ですよね。 Docker 完全に理解した *1 くらいのレベルで複数コンテナの環境を作るなら手軽でよいです。

その docker-compose が開発環境だけならまだいいんですが、 テスト用の環境も同じ仕組みで作るようになると、 環境差分をどうするかが課題になります。 例えば、以下のような環境差分が考えられます。

  • コンテナに渡す環境変数が違う
    • Web サーバの Virtual Host
    • 外接システムのエンドポイント
  • 開発環境とテスト環境で必要なサービスが違う
  • TLS の通信だったら証明書が違う

まだまだありそうです。 実際に差分が発生するかはアプリケーションの設計や環境にもよるんですが、 いったんこういう差分があり得るという前提で話を進めます。

アンチパターン

1. Compose ファイルは 1 つで、環境ごとに branch を切る

  • 修羅の道です
  • Compose ファイルがブランチごとに成長して、目も当てられなくなります
  • 唯一のメリットは、「環境ごとの起動コマンドが一緒」
    • でも各環境にそれぞれ 1 step で起動できるジョブを用意しておけばいいですよね?

2. 環境ごとに Compose ファイルを作る

  • 修羅の道パート 2 です
  • やっぱり Compose ファイルがファイルごとに成長していきます
  • プロジェクト名が同じだから同じサービス名が使えなくなり、かなりつらいです

解決策

$ docker-compose -f docker-compose.yml -f <your_env>.yml -p <your_env> up -d

実装例はこちら。

github.com

解説

-f, --file オプション

共通の Compose ファイル ( docker-compose.yml ) と環境依存の Compose ファイル ( <your_env>.yml ) を読み込みます

共通

$ cat docker-compose.yml
version: "3.5"
services:
  web:
    build:
      context: ./web
    volumes:
      - ./web/proxy-to-back.conf:/etc/nginx/conf.d/proxy-to-back.conf:ro
    environment:
      VIRTUAL_HOST: "*.web.local,*.back.local"
    networks:
      - default
      - front
  back:
    build:
      context: ./back
    depends_on:
      - web

networks:
  front:
    external: true

環境依存

$ cat env1.yml
version: "3.5"
services:
  web:
    environment:
      VIRTUAL_HOST: "env1.web.local,env1.back.local"
      APP_ENV: env1
    volumes:
      - ./web/env1.html:/usr/share/nginx/html/index.html:ro
  back:
    volumes:
      - ./back/env1.html:/usr/share/nginx/html/index.html:ro

networks:
  default:
    name: env1
$ diff env1.yml env2.yml
5c5
<       VIRTUAL_HOST: "env1.web.local,env1.back.local"
---
>       VIRTUAL_HOST: "env2.web.local,env2.back.local"
7c7
<       - ./web/env1.html:/usr/share/nginx/html/index.html:ro
---
>       - ./web/env2.html:/usr/share/nginx/html/index.html:ro
10c10
<       - ./back/env1.html:/usr/share/nginx/html/index.html:ro
---
>       - ./back/env2.html:/usr/share/nginx/html/index.html:ro
14c14
<     name: env1
---
>     name: env2
  • 前のファイルで定義した同じフィールドの項目が後のファイルにあれば、それを上書きします。
  • 新しい値があれば追加します。

例えば、

  • environment の同じキー ( VIRTUAL_HOST ) は上書きされます
  • environment の異なるキー ( APP_ENV ) は追加されます
  • volumes は追加されます

最終的にどんな設定になるのかは、 docker-compose config コマンドを使うと見れます。

$ docker-compose -f docker-compose.yml -f env1.yml -p env1 config
networks:
  default:
    name: env1
  front:
    external: true
    name: front
services:
  back:
    build:
      context: /home/ikasamak/work/dc-multi-env/back
    depends_on:
    - web
    volumes:
    - /home/ikasamak/work/dc-multi-env/back/env1.html:/usr/share/nginx/html/index.html:ro
  web:
    build:
      context: /home/ikasamak/work/dc-multi-env/web
    environment:
      APP_ENV: env1
      VIRTUAL_HOST: env1.web.local,env1.back.local
    networks:
      default: null
      front: null
    volumes:
    - /home/ikasamak/work/dc-multi-env/web/proxy-to-back.conf:/etc/nginx/conf.d/proxy-to-backi.conf:ro
    - /home/ikasamak/work/dc-multi-env/web/env1.html:/usr/share/nginx/html/index.html:ro
version: '3.5'

-p, --project-name オプション

プロジェクト名を指定します。 デフォルトは compose ファイルのあるディレクトリ名です。

~/work/dc-multi-env master* $ docker-compose up -d
Creating network "dc-multi-env_default" with the default driver

プロジェクト名を指定せずに同じディレクトリで別環境を立ち上げると、 同プロジェクトの同サービスと見なされ、既存のコンテナがかき消されてしまいます。

$ docker-compose -f docker-compose.yml -f env1.yml up -d
Creating network "env1" with the default driver
Creating dc-multi-env_web_1 ... done
Creating dc-multi-env_back_1 ... done
$ docker-compose -f docker-compose.yml -f env2.yml up -d
Creating network "env2" with the default driver
Recreating dc-multi-env_web_1 ... done
Recreating dc-multi-env_back_1 ... done
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                            NAMES
ffd5b9f636be        dc-multi-env_back   "nginx -g 'daemon of…"   5 minutes ago       Up 5 minutes        80/tcp                            dc-multi-env_back_1
6dcafba12120        dc-multi-env_web    "nginx -g 'daemon of…"   5 minutes ago       Up 43 seconds       80/tcp                            dc-multi-env_web_1

-p でプロジェクト名を指定し、別環境として立ち上げます。

$ docker-compose -f docker-compose.yml -f env1.yml -p env1 up -d
Creating network "env1" with the default driver
Creating env1_back_1 ... done
Creating env1_web_1  ... done
$ docker-compose -f docker-compose.yml -f env2.yml -p env2 up -d
Creating network "env2" with the default driver
Creating env2_back_1 ... done
Creating env2_web_1  ... done
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                            NAMES
2496c6858e0c        env2_web            "nginx -g 'daemon of…"   4 seconds ago       Up 3 seconds        80/tcp                            env2_web_1
fa5bcec301ad        env2_back           "nginx -g 'daemon of…"   5 seconds ago       Up 4 seconds        80/tcp                            env2_back_1
0aca011671e7        env1_web            "nginx -g 'daemon of…"   12 seconds ago      Up 11 seconds       80/tcp                            env1_web_1
ec885d01fa73        env1_back           "nginx -g 'daemon of…"   13 seconds ago      Up 12 seconds       80/tcp                            env1_back_1

プロジェクト名を指定する方法

  • -p, --project-name オプションを使う
  • COMPOSE_PROJECT_NAME を使う

Compose ファイルにプロジェクト名を指定できれば楽なんですけど、そういう仕様にはならなかったようです。 まあ普通は環境ごとにディレクトリ分けるから、 .env で何とかしなさいということなんでしょう。

github.com

networks

適切にネットワークを設定しないと、コンテナ名で名前解決していると別環境にトラフィックが飛んでしまうことがあります。

例えば、面倒なんで全部 front のプロキシのいるネットワークにつないでしまえ! ということをすると

$ cat docker-compose.yml
version: "3.5"
services:
  web:
    build:
      context: ./web
    volumes:
      - ./web/proxy-to-back.conf:/etc/nginx/conf.d/proxy-to-back.conf:ro
    environment:
      VIRTUAL_HOST: "*.web.local,*.back.local"
    networks:
      - front
    depends_on:
      - back
  back:
    build:
      context: ./back
    networks:
      - front

networks:
  front:
    external: true

env1webenv1, env2 両方の back とつながるので、 back へのアクセスがロードバランシングされてしまいます。

$ curl -H "Host: env1.back.local" localhost
here is env2.back!
$ curl -H "Host: env1.back.local" localhost
here is env1.back!
$ curl -H "Host: env1.back.local" localhost
here is env2.back!
$ curl -H "Host: env1.back.local" localhost
here is env1.back!

なので、適切にネットワークを設定しましょう。 プロジェクト名を分けているのであれば、 default で通信するようにしましょう。 *2

おまけ

extends を使えば、設定をモジュール化して再利用できるようです。

docs.docker.com

2019/03/08 追記

extends は compose file format v3 で使えなくなってました。

docs.docker.com

まとめ

  • 共通部分、環境依存部分に分けることで Compose ファイルを DRY に書けます
  • -p オプションでプロジェクト名を分け、同じサービス名を別環境で同時に動かせるようにします
  • networks を適切に設定して別環境にトラフィックが迷い込まないようにします

参考

*1:https://twitter.com/ito_yusaku/status/1042604780718157824

*2:サービス名だけの名前解決 ( http://back とか ) は、default ネットワークから行われるようです