現在参画させていただいているプロジェクトで React + Python + Fast APIを使用しており、今回は同じではないが最小の構成で構築を行うこととした。
目的としては以下のように設定した。
1.自身の学習環境を構築する
2.Nginxのリバースプロキシ機能を理解すること
リバースプロキシとは
クライアントからのリクエストを受け取り、リクエストに応じられるサーバに転送し、そのサーバからのレスポンスをクライアントに返します。
1.ユーザのアクセスをリバースプロキシが受けるので、直接バックエンドサーバの情報を見ることが難しくなることから比較的攻撃に晒されにくくなる。
2.状況によって流すサーバーを変更すれば負荷が分散できる。
環境構築
完成形は以下に置いています。
https://github.com/KouheiWatanabe-CS/react_python_fastapi
環境のイメージ図としては以下のようになる。
クライアントからNginxにアクセスした場合に、URLによってリバースプロキシを使用して切り替えるようにしています。
ディレクトリ構成は以下になります。
まずはnode.jsのDockerfileを作成していきます。
FROM node:latest
WORKDIR /src
次にPython + FastapiのDockerfileの設定を行います。
まず、Pythonにインストールするライブラリをrequirements.txtに記入します。
fastapi
uvicorn
sqlalchemy
pymysql
Pythonの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の設定ファイルは以下のようにします。
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を以下のように作成します。
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情報をコマンドラインに表示するようにしています。
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名に書き換わっている。