IngressのURIパスルーティングをdocker-composeで簡単に再現する

TIPS,Vue.jsDocker,docker-compose,Ingress

コロナの影響で日本でも様々な変化がありましたね。
私は父方の実家がハワイ州のミリラニという街にあるのですが当面は帰ってきてくれるなと釘を打たれております。
アメリカの方が深刻そうなのですがアジアの方が危険という意識は未だに強いみたいですね。
さて、ご無沙汰な投稿となりますが今回はKubernetesのIngressをdocker-composeを使いローカルでURIルーティングを簡単に実装してみたいと思います。

現在業務でKubernetesを用いた開発を行っているのですがIngressはとても便利ですよね。
L7スイッチでSSL終端を担うことができますし、マイクロサービスで複数コンテナに構築されたAPI群へのアクセスをURIベースでルーティングでき、カオスになりがちなフロントエンドの負担を軽くすることができます。

k8sでの開発環境構築については以前紹介したDevSpaceを用いるとホットローディングも効くのでイケてるのですが、業務で使おうとするとガバナンスが効いていてすぐに使用できないなんてこともありますよね。


まさに私もこの理由で今回はdocker-composeを使いローカル開発しているのですが、モノレポなマイクロサービスでやっているためAPI群へのアクセスを管理するのが少し面倒でした。
ローカルでも面倒なconf設定をせずにサクッと楽で依存のない再現をしたかったのです。

そこで今回は簡単にnginxでプロキシできる jwilder/nginx-proxy を使用することにしました。


jwilder/nginx-proxy の設定

nginx-proxy をリバプロで使う場合、設定方法はとても簡単です。
例えば以下のように docker-compose.yml 内でリバプロしたいサービスに VIRTUAL_HOST でホスト名、VIRTUAL_PORT でコンテナのEXPOSEしたいポート番号を定義してやるだけです。

proxy:
  image: jwilder/nginx-proxy:latest
  privileged: true
  ports:
    - 80:80
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock:ro
  network_mode: "bridge"
  restart: always

a-api:
  image: node:14-alpine
    environment:
      - VIRTUAL_HOST: localhost
      - VIRTUAL_PORT: 80

しかし今回はURIベースでの仮想ホスティングを行いたいのでこの設定だけでは実現できません。
まずはそれぞれのルーティング先を整理してみます。

  • APIその1
    URI: localhost/api/a-api
    Container: a-api
  • APIその2
    URI: localhost/api/b-api
    Container: b-api
  • APIその3
    URI: localhost/api/c-api
    Container: c-api

このように3つのAPIサービスがそれぞれ独立したコンテナで存在しているとします。
localhost ドメインでエンドポイントを構築し、/api/ URIでそれぞれのコンテナにルーティングしてみましょう。

confファイルを用意する

開発でしか使わないしなるべく設定に時間は使いたいたくない!!
と嘆きつつももこれは必要でした。
とはいえ、単一のファイルを用意するだけで済みます。

まず、公式のドキュメントによると VIRTUAL_HOST ごとに個別の設定を行うには /etc/nginx/vhost.d 配下にホスト名をつけた設定ファイルを格納するように記載されています。

Per-VIRTUAL_HOST

To add settings on a per-VIRTUAL_HOST basis, add your configuration file under /etc/nginx/vhost.d. Unlike in the proxy-wide case, which allows multiple config files with any name ending in .conf, the per-VIRTUAL_HOST file must be named exactly after the VIRTUAL_HOST.

https://hub.docker.com/r/jwilder/nginx-proxy/


よって今回は localhost を使用するので設定ファイル名は [localhost] とします。
中にはlocationディレクティブでパス毎の設定を記述します。

location /api/a-api/ {
    proxy_pass http://a-api/;
}
location /api/b-api/ {
    proxy_pass http://b-api/;
}
location /api/c-api/ {
    proxy_pass http://c-api/;
}
location /static/ {
    alias /usr/src/app/static/;
}

proxy_pass に記載している a-api / b-api / c-api はそれぞれ docker-compose.yml で定義するサービス名です、詳細は後述します。
/static/ はこのあとDRFで作成するAPIを叩く際にブラウザから動作確認をするため追加しました。

docker-compose.yml を記述する

改めて docker-compose.yml を書いていきましょう。
先ほどリバプロで使う場合に記載したものと大きくは変わりませんが、各サービス毎に解説し最後に全文を掲載します。

services: nginx-proxy

まず nginx-proxy 本体です。

nginx-proxy:
  image: jwilder/nginx-proxy:alpine
  ports:
    - "80:80"
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock:ro
    - ./api/00_ingress:/etc/nginx/vhost.d
    - static_volume:/usr/src/app/static
  depends_on:
    - a-api
    - b-api
    - c-api

volumes
- /var/run/docker.sock:/tmp/docker.sock:ro
Dockerのソケットパスです。これはソケット通信を行うために必須となります。

- ./api/00_ingress:/etc/nginx/vhost.d
上述した設定ファイルのマウント部分です。

- static_volume:/usr/src/app/static
Djangoでcollectstaticしたファイルをマウントしています。

depends_on:
初回の docker-compose up では問題ないのですが -v オプションをつけずに down したあと、再度コンテナを立ち上げるとAPIサーバよりも早く立ち上がってしまい名前解決できず existed code 1 が発生することがあります。
そのため起動順対策のために記述したおまじないです。

services: termination

続いてTLS終端でありゲートウェイとして扱うサービスを定義します。
今回はOS情報とHTTPリクエスト内容を出力するだけのGo言語で書かれた containous/whoami を使っています。

termination:
  image: containous/whoami
  environment:
    - VIRTUAL_HOST=localhost

environment:
VIRTUAL_HOST=localhost
今回設定する localhost ドメインを定義しています。
先ほど作成した設定ファイルのファイル名と一致させる必要があります。

services: api servers

APIサーバとして containous/whoami を再度使用し定義していきます。

a-api:
  build: ./api/01_a-api
  expose:
    - 80
  command: sh -c "python manage.py runserver 0.0.0.0:80"
  volumes:
    - ./api/01_a-api:/usr/src/app
    - static_volume:/usr/src/app/static

# APIその2
b-api:
  build: ./api/02_b-api
  expose:
    - 80
  command: sh -c "python manage.py runserver 0.0.0.0:80"
  volumes:
    - ./api/02_b-api:/usr/src/app
    - static_volume:/usr/src/app/static

# APIその3
c-api:
  build: ./api/03_c-api
  expose:
    - 80
  command: sh -c "python manage.py runserver 0.0.0.0:80"
  volumes:
    - ./api/03_c-api:/usr/src/app
    - static_volume:/usr/src/app/static

Django RESR framwork で作成したAPIを定義します。
特筆すべき点はないですね、今回はURIルーティングの検証をするため単純にEXPOSEしています。

@api_view(['GET'])
def callback(request):
    data = 'this is a-api response'
    return Response(json.dumps(data), status=200)

ちなみに各APIはこのように実装しています。

services: client

最後にAPIを叩く側としてレスポンスを表示するだけの簡単なVueアプリを用意してみました。

client:
  build:
    context: ./client
    target: dev
  volumes:
    - ./client:/app
    - /app/node_modules
  ports:
    - "8080:8080"
  environment:
    - HOST=0.0.0.0

volumes: static_volume

volumes:
  static_volume:

Djangoでcollectstaticしたファイルをマウントします。

これで docker-compose.yml 全文はこのようになります。

version: '3.7'
services:
  #############################################################################
  # frontend
  #############################################################################
  client:
    build:
      context: ./client
      target: dev
    volumes:
      - ./client:/app
      - /app/node_modules
    ports:
      - "8080:8080"
    environment:
      - HOST=0.0.0.0

  #############################################################################
  # backend Ingress-controller
  #############################################################################
  # nginx-proxy本体
  nginx-proxy:
    image: jwilder/nginx-proxy:alpine
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./api/00_ingress:/etc/nginx/vhost.d
      - static_volume:/usr/src/app/static
    depends_on:
      - a-api
      - b-api
      - c-api

  # gateway
  termination:
    image: containous/whoami
    environment:
      - VIRTUAL_HOST=localhost

  #############################################################################
  # backend API containers
  #############################################################################
  # APIその1
  a-api:
    build: ./api/01_a-api
    expose:
      - 80
    command: sh -c "python manage.py runserver 0.0.0.0:80"
    volumes:
      - ./api/01_a-api:/usr/src/app
      - static_volume:/usr/src/app/static

  # APIその2
  b-api:
    build: ./api/02_b-api
    expose:
      - 80
    command: sh -c "python manage.py runserver 0.0.0.0:80"
    volumes:
      - ./api/02_b-api:/usr/src/app
      - static_volume:/usr/src/app/static

  # APIその3
  c-api:
    build: ./api/03_c-api
    expose:
      - 80
    command: sh -c "python manage.py runserver 0.0.0.0:80"
    volumes:
      - ./api/03_c-api:/usr/src/app
      - static_volume:/usr/src/app/static

#############################################################################
# volumes
#############################################################################
volumes:
  static_volume:

動かしてみる

設定が完了したので早速動作確認してみましょう。
プロジェクトディレクトリは次のように構成されています。

local-ingress
├── api
│   ├── 00_ingress/
│   ├── 01_a-api/
│   ├── 02_b-api/
│   └── 03_c-api/
├── client
│   ├── Dockerfile
│   ├── README.md
│   ├── babel.config.js
│   ├── node_modules/
│   ├── package-lock.json
│   ├── package.json
│   ├── public/
│   └── src/
└── docker-compose.yml

それでは早速 docker-compose up --build といきたいところですが1つだけ注意点があります。

jwilder/nginx-proxy は起動時に他に立ち上がっているコンテナを一覧で取得しdefault.confを生成しようとしてくれます。
この時、Docker Desktop for Mac 等でKubernetesの local cluster を有効にしていると正常に読み込めず503が返るようになるため事前にDisableしておいてください。

準備ができたら docker-compose up --buildhttp://localhost:8080/ へアクセスしましょう。

各APIのエンドポイントは80番でExposeされており、URLディスパッチャは
path( '', views.callback, name='callback' )
と定義されていますが http://localhost/ ではアクセスすることが出来ません。

このように containous/whoami を使用した termination コンテナからレスポンスが戻ります。
これは先ほど作成した設定ファイルで定義した location ディレクティブに一致するルールがないためです。

次に設定ファイルで定義していた http://localhost/api/a-api/ でブラウザからアクセスしてみます。

無事に見慣れたDRFの画面が出てきました!
Vueアプリに戻り、各APIの呼び出しボタンを押下すると適切なコンテナへ振り分けられているのが確認出来ます。


まとめ

設定ファイルを1つ作成し docker-compose.yml に何点かの追記をするだけで面倒な設定不要のまま実装することが出来ました。
私は今回ローカル開発環境でのIngress検証用に導入していますが、しっかりと設定をしていけばSSL終端も担えますしprodでのリバプロ利用も難しくなさそうです。

今回作成したサンプルコードはGitHubにアップしてあります。