K.Watanabeをフォローする

Docker + React + Python + Fast APIで簡単な開発環境をリバースプロキシを使用して構築する

開発環境・ツール

現在参画させていただいているプロジェクトで React + Python + Fast APIを使用しており、今回は同じではないが最小の構成で構築を行うこととした。
目的としては以下のように設定した。

1.自身の学習環境を構築する
2.Nginxのリバースプロキシ機能を理解すること

リバースプロキシとは

クライアントからのリクエストを受け取り、リクエストに応じられるサーバに転送し、そのサーバからのレスポンスをクライアントに返します。

1.ユーザのアクセスをリバースプロキシが受けるので、直接バックエンドサーバの情報を見ることが難しくなることから比較的攻撃に晒されにくくなる。
2.状況によって流すサーバーを変更すれば負荷が分散できる。

環境構築

完成形は以下に置いています。
https://github.com/KouheiWatanabe-CS/react_python_fastapi

環境のイメージ図としては以下のようになる。

クライアントからNginxにアクセスした場合に、URLによってリバースプロキシを使用して切り替えるようにしています。
ディレクトリ構成は以下になります。

まずはnode.jsのDockerfileを作成していきます。

/docker/nodejs/Dockerfile
FROM node:latest

WORKDIR /src

次にPython + FastapiのDockerfileの設定を行います。

まず、Pythonにインストールするライブラリをrequirements.txtに記入します。

/docker/python/requirements.txt
fastapi

uvicorn

sqlalchemy

pymysql

PythonのDockerfileは以下のようになります。

Dockerfile
FROM python:3.9-alpine

WORKDIR /app

COPY ./docker/python/requirements.txt .
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./backend/ .

CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]

nginxの設定ファイルは以下のようにします。

/docker/nginx/default.conf
server {
    listen 80;
    server_name localhost;
 
    # reactのアクセスはnodejsコンテナに流す
    location / {
        proxy_pass http://frontend:3000;
    }
 
    # /apiはバックエンドのpythonのコンテナに流す
    #リバースプロキシにheaderを付与する
    location /api {
        proxy_set_header Host               $host;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host   $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP          $remote_addr;
        proxy_set_header X-Forwarded-Proto  $scheme;
        proxy_pass http://backend:8080;
    }
    # リバースプロキシにheaderを付与しない
    location /no-proxy-header {
        proxy_pass http://backend:8080;
    }
}

docker-compose.ymlを以下のように作成します。

docker-compose.yml
version: "3.0"

services:
  backend:
    volumes:
      - ./backend:/app
    build:
      context: .
      dockerfile: docker/python/Dockerfile
    ports:
      - 8080:8080

  frontend:
    build:
      context: .
      dockerfile: docker/nodejs/Dockerfile
    volumes:
      - ./frontend:/src # ローカルをコンテナ内にマウント
    command: sh -c "cd react-project && yarn start" #コンテナを立ち上げたときに自動的にbuildする
    ports:
      - "3000:3000"
 
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf

バックエンドのmain.pyには以下のように書いておきます。
リバースプロキシの挙動を見たかったのでheader情報をコマンドラインに表示するようにしています。

main.py
from fastapi import FastAPI,Request

app = FastAPI()


@app.get("/api")
async def root(request:Request):
    print(request.headers)
    return {"message": "Hello World"}

@app.get("/no-proxy-header")
async def noProxyHeader(request:Request):
    print(request.headers)
    return {"message": "no proxy header"}

設定ファイルを作成したらDocker imageを作成する。

docker-compose build

create react appでreactのプロジェクトを作成します。

docker-compose run --rm frontend sh -c "npm install -g create-react-app && create-react-app react-project --template typescript"

最後にdocker-compose upをすれば終了です。

docker-compose up

reactにアクセスするにはlocalhostをURLに打ち込みます。
バックエンドサーバにアクセスする場合はlocalhost/api 以下で動作するようにしています。

リバースプロキシの挙動を見る

① nginxを通さずにバックエンドサーバにアクセスした場合

http://localhost:8080/apiをURLに打ち込む

Headers({'host': 'localhost:8080', 'connection': 'keep-alive', 'cache-control': 'max-age=0', 'sec-ch-ua': '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'ja,en-US;q=0.9,en;q=0.8', 'cookie': '_ga=GA1.1.1834239719.1652948776; _ga_8KZ44R9GPS=GS1.1.1656478108.36.0.1656478110.58'})

クライアントから直接つながっています。 

② リバースプロキシでつないだ場合
URLにhttp://localhost/no-proxy-headerを打ち込みます。

Headers({'host': 'backend:8080', 'connection': 'close', 'sec-ch-ua': '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'ja,en-US;q=0.9,en;q=0.8', 'cookie': '_ga=GA1.1.1834239719.1652948776; _ga_8KZ44R9GPS=GS1.1.1656478108.36.0.1656478110.58'})

nginxに設定したproxy_passのURLが入っています。nginxからリクエストが飛んでいることがわかります。

③ リバースプロキシでつないでかつheaderを設定した場合
URLにhttp://localhost/apiを打ち込む

Headers({'host': 'localhost', 'x-forwarded-for': '172.18.0.1', 'x-forwarded-host': 'localhost', 'x-forwarded-server': 'localhost', 'x-real-ip': '172.18.0.1', 'x-forwarded-proto': 'http', 'connection': 'close', 'sec-ch-ua': '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'purpose': 'prefetch', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'ja,en-US;q=0.9,en;q=0.8', 'cookie': '_ga=GA1.1.1834239719.1652948776; _ga_8KZ44R9GPS=GS1.1.1656478108.36.0.1656478110.58'})

hostがclientで打ったhost名に書き換わっている。