FastAPI + nginx on ECS Fargate

# 0. はじめに
以前投稿した[本番稼働を意識したECS Fargateを用いたFastAPIの環境構築（ALB、SSM、BlueGreenデプロイ）](https://qiita.com/yuyasat/items/04a083a57f5691f1ed3c)という記事では、FastAPIをECS Fargateで動かすための流れについて説明しました。しかしnginxを前段に噛ませる構成ではありませんでした。今回、仕事にてFastAPIを用いかつnginxをかませる形でFargateに構成しましたので、備忘録も兼ねて設定内容を記述したいと思います。

# 1. 簡単な構成説明
簡単な構成説明ですが、
ALB - nginxコンテナ - FastAPIコンテナ
という形でALBがnginxにリクエストを分散し、nginxとFastAPIはFargate内でそれぞれコンテナとして稼働するという構成になります。

FastAPIをFargateで動かすにあたり、必ずしもnginxはかませる必要はないのですが、uvicornの公式によると、nginxとの連携については、以下の様な記述があります。[^1]
> Using Nginx as a proxy in front of your Uvicorn processes may not be neccessary, but is recommended for additional resiliance. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.

お作法的にもALBから直接アプリケーションサーバーにアクセスしに行くよりも、nginxを噛ませた方が丁寧で、アプリケーションサーバーの負荷も下げられるかと思います。[^2]
[^1]: https://www.uvicorn.org/deployment/#running-behind-nginx
[^2]: 私が社会人１年目にSIerの研修では「Web３層構造」について学びました。今もこれが一般的なのかなと思っていますが、最近は違ってきているかもしれません。

# 2. ファイル構成
ファイル構成は以下になります。DockerfileはFargate用にDockerfile-ecs-fargateを作っています。また、今の構成では不要なのですが、後々の拡張性のためにdocker-composeを用いており、そのためのファイルとしてdocker-compose.ecs_fargate.ymlを用意しています。
また、ecs_fargate/nginxディレクトリ内にはECS Fargate向けのnginxのDockerファイルやFastAPI向けのnginxのconfを置いています。

```
.
├── Dockerfile-ecs-fargate
├── app
│   └── main.py
├── docker-compose.ecs_fargate.yml
├── ecs_fargate
│   └── nginx
│       ├── Dockerfile
│       └── fastapi.conf
├── poetry.lock
└── pyproject.toml
```
それではそれぞれのファイルの中身について見ていきましょう。

## Dockerfile-ecs-fargate
```Dockerfile
FROM python:3.9-buster
ENV PYTHONUNBUFFERED=1

WORKDIR /src

RUN mkdir -p tmp

RUN pip install poetry

COPY pyproject.toml* poetry.lock* ./

RUN poetry config virtualenvs.in-project true
RUN poetry install

COPY . .

VOLUME /src
VOLUME /src/tmp

CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--uds", "tmp/uvicorn.sock"]
```
- `COPY pyproject.toml* poetry.lock* ./`をしたあとに`COPY . .`しているのは、`poetry install`でキャッシュを効かせるためになります。
- ポイントとしては、最後の行のCMDで、`"--uds", "tmp/uvicorn.sock"`としているところになります。nginxとの連携をsocketで行うためです。

## ecs_fargate/nginx/Dockerfile
```Dockerfile
FROM nginx:1.21.5
RUN apt-get update && \
    apt-get install -y apt-utils curl \
    locales && \
    echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8
ADD ./ecs_fargate/nginx/fastapi.conf /etc/nginx/conf.d
EXPOSE 80
```
- ポイントは`ADD ./ecs_fargate/nginx/fastapi.conf /etc/nginx/conf.d`でFastAPI用のconfを読み込ませているところです。


## ecs_fargate/nginx/fastapi.conf
```nginx
server {
    listen 80 default_server;

    location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_redirect off;
      proxy_buffering off;
      proxy_pass http://uvicorn;
    }
}

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

upstream uvicorn {
  server unix:///src/tmp/uvicorn.sock;
}
```
- `upstream`で`uvicorn`を定義し`uvicorn.sock`の場所を指定しています。

## app/main.py
```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/healthcheck")
async def healthcheck():
    return {"message": "healthcheck"}
```
- 基本的なHello Worldおよび、ALBからのヘルスチェック用の`/healthcheck`のみエンドポイントを用意しています。

## pyproject.toml
```toml
[tool.poetry]
name = "fastapi-sample"
version = "0.1.0"
description = ""
authors = ["someone <someone@example.com>"]

[tool.poetry.dependencies]
python = "^3.9"
uvicorn = "^0.17.6"
pydantic = "^1.9.1"
fastapi = "^0.78.0"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```

# 2. ビルドおよびECSのタスク定義について
以上の様なファイル構成およびファイル内容であとはお好きな形でイメージをビルドしていけば良いのですが、私はdocker-composeを利用しました。[^3]
[^3]: docker-composeを利用している理由としてはビルド時に環境変数をビルド環境に合わせて外から指定してあげたかったからです。dockerコマンドではコマンドごとに指定する必要があり、prod, stg, demoなどと複数環境ごとにイメージをビルドしたい場合に困ったので、docker-composeを用いています。

```
docker-compose -f docker-compose.ecs_fargate.yml build
```
このコマンドを打つと`ディレクトリ名_app`、`ディレクトリ名_nginx`といったイメージが作成されます。`docker images`コマンドで確認してみてください。
その後、イメージのタグづけ、ECRへのpushなどを行っていきます。ここでは割愛します。詳細は、[ECRへの登録](https://qiita.com/yuyasat/items/04a083a57f5691f1ed3c#2-ecr%E3%81%B8%E3%81%AE%E7%99%BB%E9%8C%B2)の「docker imageのタグ付け」以降をご覧ください。

タスク定義のコンテナの定義のところですが、今回は`app`と`nginx`というコンテナ名で登録しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121304/2c79fb6b-d8ca-3ece-fb5b-c83076c198d5.png)
タスク定義の設定に関しては、`nginx`コンテナのポートマッピングのコンテナポートに80を、ボリュームソースのソースコンテナにappを設定します。この辺りは例えばdocker-compose.ymlでの設定内容をECSではここで設定してあげるということになるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121304/2f6323c3-1df1-a96e-e259-15241848af89.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121304/81a82ed4-2cbb-4c6a-4a73-e0fc986b9a66.png)

# 3. 最後に
最終的にはまっていたのは`uvicorn.sock`のところでした。今思えばRailsで環境構築した際には`puma.sock`で`nginx`と連携していたので、当然のことでした。また公式[^1]にも`server unix:/tmp/uvicorn.sock;`の記載があり、きちんと公式読めば解決した話でした（アクセス権限の問題で`uvicorn.sock`の置き場所は公式とは変更しています）。普段はRailsで開発しているのですが、この度Pythonで開発する必要があり、Railsでの知識が役立った良い例でした。引き続き精進していきます。