🙆

Docker×FastAPI×React(TypeScript) on AWS ECS【backend】

2021/08/03に公開

はい、やってみた系です。

最近ずーっと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編です!

GitHubで編集を提案

Discussion