IngressのURIパスルーティングをdocker-composeで簡単に再現する
コロナの影響で日本でも様々な変化がありましたね。
私は父方の実家がハワイ州のミリラニという街にあるのですが当面は帰ってきてくれるなと釘を打たれております。
アメリカの方が深刻そうなのですがアジアの方が危険という意識は未だに強いみたいですね。
さて、ご無沙汰な投稿となりますが今回は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
https://hub.docker.com/r/jwilder/nginx-proxy/
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 theVIRTUAL_HOST
.
よって今回は 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 --build
し http://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にアップしてあります。