Docker×FastAPI×React(TypeScript) on AWS ECS【backend】
はい、やってみた系です。
最近ずーっとInputばっかりだったのでやったこと書くか。。となった次第です。
ECSとか使ったことなかったので。(高いし。。)
※お金は知らんうちに全然可愛くない金額になってるのでご利用は計画的に(RDSとかも使ってたけど1週で3000円弱くらいになってた。。)
最終構築のイメージ
何章かに分けて作成していきますが、全部終わったらこうなるイメージ
(今回はbackend編)
動作環境
- BigSur ver11.4
- MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)
- 1.4 GHz クアッドコアIntel Core i5
- 16 GB 2133 MHz LPDDR3
- Docker version 20.10.7
- docker-compose version 1.29.2
言語・FW
- Backend:FastAPI(Python) ← 今回はこれ
- Frontend:React(TypeScript)
document
backend編での最終構成
階層絡みでおかしいなとなったらこちら参考にしてみて下さい。
本章でのbackendの構成は最終的にこうなります。
.
├── docker-compose.yml
└── project
└── backend
├── Dockerfile
├── app
│ ├── __init__.py
│ ├── api
│ │ └── crud.py
│ ├── db.py
│ ├── main.pyいてr
│ └── models
│ ├── __init__.py
│ ├── pydantic.py
│ └── tortoise.py
├── db
│ ├── Dockerfile
│ └── create.sql
├── entrypoint.sh
└── requirements.txt
FastAPIのセットアップ
1.まずはプロジェクトを掘っていく
mkdir project && cd project
mkdir backend && cd backend
2.backendの階層にDockerfileを作成
# project/backend/Dockerfile
FROM python:3.9.6-slim-buster
#working directroy
WORKDIR /usr/src/
#environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
#install system depedencies
RUN apt-get update \
&& apt-get -y install netcat gcc postgresql \
&& apt-get clean
#install python depedencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
#add app
COPY . .
3.pythonはrequirements.txtでパッケージ類を管理できるので使う予定のものを適当に用意しておく。これもDockerfileと同階層に作成
# project/backend/requirements.txt
asyncpg==0.23.0
fastapi==0.65.3
gunicorn==20.1.0
requests==2.25.1
tortoise-orm==0.17.4
uvicorn==0.14.0
4.続けて同階層にappディレクトリを作成。さらに__init__.pyとmain.pyを作成後に以下のコードを記述しておく。(init.pyは作成しておくだけで何も記載しないでOK)
# project/backend/app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def pong():
return {"Hello": "world!"}
5.docker-compose.ymlはprojectの階層に作成
# docker-compose.yml
version: '3.8'
services:
web:
build:
context: ./project/backend
dockerfile: Dockerfile
command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
volumes:
- ./project/backend:/usr/src/
ports:
- 8004:8000
6.docker-compose.ymlの階層まで移動して以下のコマンドを実行する
cd ..
docker-compose up -d --build
7.コンテナが立ち上がっていることを確認したら、http://localhost:8004/ping を開いて、
{"Hello:"world!"}
が表示されていることも確認する
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# a280a1a11240 article-todo_web "uvicorn app.main:ap…" 2 minutes ago Up 2 minutes 0.0.0.0:8004->8000/tcp, :::8004->8000/tcp todo_web_1
確認できたら一旦downさせる
docker-compose down
ここまでのプロジェクト配下
.
├── docker-compose.yml
└── project
└── backend
├── Dockerfile
├── app
│ ├── __init__.py
│ └── main.py
└── requirements.txt
次はDBをセットアップしていく
Postgresのセットアップ
8.backend以下の階層にdbディレクトリ、さらにcreate.sqlを作成する
mkdir project/backend/db
ちなみにsqlの中身はデータベースを任意の名前で作成するだけ
# project/backend/db/create.sql
CREATE DATABASE develop;
9.PostgresのBaseImageでDockerfileを作成。補足としてはコンテナ内のdocker-entrypoint-initdb.d
ディレクトリにcreate.sql
を追加することで、公式のPostgresイメージ(Alpineベースのイメージ)を拡張することができるそうな。
# project/backend/db/Dockerfile
FROM postgres:13-alpine
# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d
10.先程作成したdocker-compose.ymlを編集する
version: '3.8'
services:
web:
build:
context: ./project/backend
dockerfile: Dockerfile
command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
volumes:
- ./project/backend:/usr/src/
environment:
- DATABASE_URL=postgres://postgres:postgres@web-db:5432/develop
ports:
- 8004:8000
depends_on:
- web-db
web-db:
build:
context: ./project/backend/db
dockerfile: Dockerfile
expose:
- 5432
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
11.Poastgresインスタンスが起動しているかを参照するためにentrypoint.shを作成する
#!/bin/bash
echo "waiting for postgres server"
while ! nc -z web-db 5432; do
sleep 0.5
done
echo "Connection Successfully"
exec "$@"
12.2で作成した、backendのDockerfileを編集する
#official base-image python
FROM python:3.9.6-slim-buster
#working directroy
WORKDIR /usr/src/
#environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
#install system depedencies
RUN apt-get update \
&& apt-get -y install netcat gcc postgresql \
&& apt-get clean
#install python depedencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
#add app
COPY . .
#add entrypoint.sh
COPY ./entrypoint.sh .
RUN chmod +x /usr/src/entrypoint.sh
#entrypoint.sh
ENTRYPOINT ["/usr/src/entrypoint.sh"]
13.編集したら、再びコンテナを起動させる
その前にentrypoint.shの実行権限を与えとくのを忘れずに。。
chmod +x project/backend/entrypoint.sh
docker-compose up -d --build
14.entrypoint.shの挙動を確認しておく
docker-compose logs web
# Attaching to article-todo_web_1
# web_1 | waiting for postgres server
# web_1 | Connection Successfully
# web_1 | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# web_1 | INFO: Started reloader process [1] using statreload
# web_1 | INFO: Started server process [14]
# web_1 | INFO: Waiting for application startup.
# web_1 | INFO: Application startup complete.
ここまでのプロジェクト配下
.
├── docker-compose.yml
└── project
└── backend
├── Dockerfile
├── app
│ ├── __init__.py
│ └── main.py
├── db
│ ├── Dockerfile
│ └── create.sql
├── entrypoint.sh
└── requirements.txt
確認できたのでまたdownさせとく
docker-compose down
TortoiseORMの実装
Python製ORMの代表選手といえばSQLAlchemyでしょう。公式チュートリアルもSQLAlchemy使ってますよね。
自分は業務でDjango使ってるので似たような雰囲気の非同期ORMとかいいんじゃね?と言う理由でTortoiseORMを採用。
15.appの配下にORM用のdb.pyを作成する。
# project/backend/app/db.py
import os
from fastapi import FastAPI
from tortoise import Tortoise, run_async
from tortoise.contrib.fastapi import register_tortoise
TORTOISE_ORM = {
"connections": {"default": os.environ.get("DATABASE_URL")},
"apps": {
"models": {
"models": ["app.models.tortoise"],
"default_connection": "default",
},
},
}
def init_db(app: FastAPI) -> None:
register_tortoise(
app,
db_url=os.environ.get("DATABASE_URL"),
modules={"models": ["app.models.tortoise"]},
generate_schemas=False,
add_exception_handlers=True,
)
async def generate_schema() -> None:
await Tortoise.init(
db_url=os.environ.get("DATABASE_URL"),
modules={"models": ["models.tortoise"]},
)
await Tortoise.generate_schemas()
await Tortoise.close_connections()
if __name__ == "__main__":
run_async(generate_schema())
16.appと同じ階層にmodelsディレクトリ作成後、その配下にmodel
とするtortoise.py、ResponseとOutputの型定義としてpydantic.pyを作成する.
(init/pyもmain.py作成したとき同様に空っぽで配置する)
mkdir project/backend/app/modelss
# project/backend/app/models/tortoise.py
from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator
class TextSummary(models.Model):
summary = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return self.summary
SummarySchema = pydantic_model_creator(TextSummary)
# project/backend/app/models/pydantic.py
from pydantic import BaseModel
class SummaryPayloadSchema(BaseModel):
summary: str
class SummaryResponseSchema(SummaryPayloadSchema):
id: int
17.appと同じ階層にapiディレクトリ作成後、apiディレクトリ配下にエンドポイント用のcrud.pyを作成する。(全部書くのだるかっったのでとりあえずPOSTとGETだけ。。。あとから修正します。。。)
# project/backend/app/api/crud.py
from app.models.pydantic import SummaryPayloadSchema
from app.models.tortoise import TextSummary
from typing import List
async def post(payload: SummaryPayloadSchema) -> int:
summary = TextSummary(
summary=payload.summary,
)
await summary.save()
return summary.id
async def get_all() -> List:
summarys = await TextSummary.all().values()
return summarys
18.最後にmain.pyを以下のように編集して一旦終了
import logging
import os
from fastapi import FastAPI
from typing import List
from app.api import crud
from app.db import init_db
from app.models.tortoise import SummarySchema
from app.models.pydantic import SummaryPayloadSchema, SummaryResponseSchema
log = logging.getLogger("uvicorn")
app = FastAPI()
@app.on_event("startup")
async def startup_event():
log.info("Starting up...")
init_db(app)
@app.on_event("shutdown")
async def shutdown_event():
log.info("Shutting down...")
@app.post("/", response_model=SummaryResponseSchema, status_code=201)
async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema:
summary_id = await crud.post(payload)
response_object = {
"id": summary_id,
"summary": payload.summary
}
return response_object
@app.get("/", response_model=List[SummarySchema])
async def read_all_todo() -> List[SummarySchema]:
return await crud.get_all()
19.docker-composeを起動させます。
docker-compose up -d --build
20.正常に起動されていることを確認できたらテーブルを作成するため以下のコマンドを実行します
docker-compose exec web python app/db.py
ここも正常に実行されたら以下を実行して、確認しましょう。
docker-compose exec web-db psql -U postgres
# db psql -U postgres
# psql (13.3)
# Type "help" for help.
postgres=\c develop
# You are now connected to database "develop" as user "postgres".
develop=\dt
# List of relations
# Schema | Name | Type | Owner
# --------+-------------+-------+----------
# public | textsummary | table | postgres
(1 row)
21.最後にPOSTとGETの挙動を確認します。http://localhost:8004/docs
Swagger UIがあると楽ちんですねェ。。
※一応curlだとこんな感じになる
- POST
curl -X 'POST' \
'http://localhost:8004/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"summary": "Test"
}'
- GET
curl -X 'GET' \
'http://localhost:8004/' \
-H 'accept: application/json'
[
{
"id": 1,
"summary": "Test",
"created_at": "2021-08-02T00:26:28.040690+00:00"
}
]
お疲れさまでした!
お次はfrontend編です!
Discussion