본문 바로가기
내가 만든 실습🍪 완전 삽질했지

CloudType, PlanetScale, Docker을 이용한 FastAPI 웹 서버 구축하기

by 민휘 2023. 6. 29.

목차

1. 목표

2. 사용할 기술 소개

3. 계획

4. 구현

 

구현 목차

1. hello world api 만들고 도커로 배포

2. Plane Scale 세팅과 더미 데이터 추가

3. hello world 서버에 DB 연결

 

목표

도커 사용법을 배운 김에 프로젝트 배포에 활용해보려고 한다.

데이터베이스를 사용하는 간단한 API 서버를 개발하고 클라우드 환경에 배포한다.

개발 환경과 운영 환경의 통합을 위해 도커 컨테이너를 사용한다.

(사실 cloud type에 fast api 템플릿으로 배포를 했었는데, 데이터베이스 보안 파일 경로를 못 읽는 에러를 해결하지 못해서 도커로 다시 도전한다 )

 

사용할 기술

  • Fast API : 빠르고 가벼운 파이썬 웹 프레임워크. 간단한 API는 코드 몇줄이면 완성할 수 있다.
  • Planet Scale : MySQL과 호환되는 Serverless Database Platform. 무료로 이용해도 10GB 의 저장공간과 수많은 읽기/쓰기 횟수 등을 제공한다.
  • CloudType : 클라우드 애플리케이션 플랫폼. 카드를 등록하지 않아도 네개의 웹사이트를 배포할 수 있다.

클라우드 클럽 스터디장 분이 공유해주신 프리티어 제한이 낮은 기술 위주로 사용했다! 스터디 장님 짱 👍

 

계획

  1. hello world를 반환하는 간단한 FastAPI 프로젝트를 만들어 도커 파일을 작성하고, Cloud Type에 배포한다.
  2. Planet Scale에 데이터베이스를 만들고 User 테이블을 만들어 더미 데이터를 추가한다.
  3. hello world 프로젝트에 DB를 연결하고, User 삽입 조회 API를 만든다. 도커 파일을 수정해서 다시 Cloud Type에 배포한다.

 

1. hello world api 만들고 도커로 배포하기

조사를 하다보니 Fast API를 도커로 배포하는 작업에 대한 정보가 굉장히 많았다. 심지어 도커 파일과 도커 컴포즈 파일을 생성하는 Cookiecutter template도 발견했다! 마음 같아서는 이걸 바로 사용하고 싶었지만, 도커 파일 만드는걸 연습하는거니까 Fast API 공식 문서를 보면서 따라하기로 했다.

Step1. PyCharm에서 fast api 프로젝트 생성

fast api 프로젝트를 생성했더니, main.py에 hello world 찍어주는 api와 path variable을 받아 이름을 출력하는 api가 자동으로 만들어져있다.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

main.py가 루트 경로에 만들어져있는데, 나중에 다른 파일들도 생길테니 app 폴더를 만들고 그 안에 main.py를 옮겨주었다. 이렇게!

 

만든 김에 다른 파일 구경도 해보자. (저는 파이썬 프레임워크가 처음이라서요..)

test_main.http : 간단한 API 테스트를 해볼 수 있는 파일이다. 인텔리제이 ultimate 버전에서도 지원하는 기능이다. 커뮤니티 버전은 지원하지 않는 것으로 알고 있다.

# Test your FastAPI endpoints

GET <http://127.0.0.1:8000/>
Accept: application/json

###

GET <http://127.0.0.1:8000/hello/User>
Accept: application/json

###

 

requirements.txt : 파이썬 프로젝트의 패키지 관리 도구. 현재 가상환경에 설치된 python 패키지를 버전과 함께 작성한다. JVM의 gradle이나 maven이랑 같은 역할인 것 같다. 참고로 pip freeze > requirements.txt 명령어를 사용하면 자동으로 requirements.txt파일을 생성한다.

fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0

 

자자 이제 실행을 해보자~

 

오잉 그런데 main.py를 run해보니 이런 오류가 뜨면서 서버가 안 뜬다. >> Warning: FastAPI application has to be a valid .py file << 알고보니 main에서 uvicorn이라는 내장서버를 띄워줘야한다고. requirements.txt의 uvicorn이 내장서버인가보다. 파이참 설정으로 세팅해도 되고, main.py에 유비콘을 직접 코드로 띄워도 된다.

로컬에서 테스트할 용도라면 이 코드를 main에 넣어서 서버를 띄운다.

if __name__ == "__main__":
	    uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")

나는 파이참 설정으로 서버를 띄우기로 했다. 왜냐하면 컨테이너에 올라가는 서버는 CMD로 띄울거기 때문에 main에 있는 uvicorn run.. 이런 코드가 필요하지 않다.

스택 오버플로우 글을 보고 파이참 설정을 했다.

 

터미널에서 uvicorn app.main:app --reload --port 8000 를 실행하면 된다.

 

Step2. 도커 파일 작성

공식 문서에 나와있는 도커 파일을 그대로 사용했다. 친절하게 주석으로 부연설명 해주고 있다. 하나씩 살펴보자.

# Start from the official Python base image.
FROM python:3.9

# Set the current working directory to /code.
# This is where we'll put the requirements.txt file and the app directory.
WORKDIR /code

# Copy the file with the requirements to the /code directory.
# Copy only the file with the requirements first, not the rest of the code.
# As this file doesn't change often, Docker will detect it and use the cache for this step, enabling the cache for the next step too.
COPY ./requirements.txt /code/requirements.txt

# Install the package dependencies in the requirements file.
# The --no-cache-dir option tells pip to not save the downloaded packages locally,
# as that is only if pip was going to be run again to install the same packages,
# but that's not the case when working with containers.
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# Copy the ./app directory inside the /code directory.
# As this has all the code which is what changes most frequently the Docker cache won't be used for this or any following steps easily.
# So, it's important to put this near the end of the Dockerfile, to optimize the container image build times.
COPY ./app /code/app

# Set the command to run the uvicorn server.
# This command will be run from the current working directory, the same /code directory you set above with WORKDIR /code.
# Because the program will be started at /code and inside of it is the directory ./app with your code, Uvicorn will be able to see and import app from app.main.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  • FROM python:3.9 : 베이스 이미지로 파이썬 3.9 이미지를 사용하고 있다.
  • WORKDIR /code : 컨테이너 내의 /code 디렉터리로 이동한다.
  • COPY ./requirements.txt /code/requirements.txt : 이동한 /code 디렉터리에 requirements.txt(필요한 패키지)를 복사한다. 여기서 현재 디렉토리는 docker build 명령어를 실행하는 위치이다. 이 파일이 자주 변경되지 않는다면 도커가 캐시한다.
  • RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt : pip로 필요한 패키지들을 설치한다. -no-cache-dir 옵션은 pip가 로컬에 다운로드한 패키지를 캐시하지 않도록 한다. -upgrade 옵션은 이미 설치된 패키지를 업그레이드해서 도커가 캐시할 수 있게 한다.
  • COPY ./app /code/app : /code 디렉터리에 app 디렉터리(애플리케이션 코드)를 복사한다.
  • CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] : 컨테이너를 시작할 때 실행되는 명령어. FastAPI 앱을 실행하고 0.0.0.0 IP 주소와 80 포트로 바인딩하여 서버를 시작한다. uvicorn은 파이썬 3.6+ 에서 사용되는 웹 서버이다. (톰캣 같은거구만..)

 

 

Step3. 이미지 빌드하고 로컬에서 컨테이너 실행

  • 이미지 빌드 : minhwi@minhwiui-MacBookPro  ~/PycharmProjects/fastApiProject  docker build -t first-fastapi-image:0.0 .
  • 로컬에서 실행 : minhwi@minhwiui-MacBookPro  ~/PycharmProjects/fastApiProject  docker run -d --name mycontainer -p 80:80 first-fastapi-image:0.0

localhost:80 접속 결과! root로 요청했을 때 Hello World가 뜬다.

 

 

 

Fast API의 막강한 기능 중 하나인 Swagger 도큐먼트 자동 생성. 도메인/docs으로 접속하면 Fast API가 만들어준 Swagger 도큐먼트를 확인할 수 있다. 스프링 부트 쓸 때는 애노테이션 달면 코드 더러워지는게 너무 싫어서 Postman으로 한땀 한땀 작성했는데, 정말 편하고 좋은 기능인 듯.

 

 

 

존재하지 않는 컨트롤러 주소로 요청하면 이렇게 텍스트로 에러를 잡아준다. 예외를 어떻게 처리하는지는 프로젝트 발전시키면서 알아봐야겠다.

존재하지 않는 컨트롤러 주소로 요청하면 이렇게 텍스트로 에러를 잡아준다. 예외를 어떻게 처리하는지는 프로젝트 발전시키면서 알아봐야겠다.

 

 

Step4. 깃허브에 공유하기

지금까지 만든 Fast API 프로젝트를 깃허브에 공유하자.

선택사항. 태그 붙이고 도커 허브에 푸시

클라우드 타입에 배포할 때는 딱히 필요하진 않지만.. 이왕 실습하는 김에 배운거 따라해봅시닷.

도커 파일~로 만든 이미지~로 만든 컨테이너가 로컬에서 잘 동작하는 것을 확인했으니, 태그를 붙여서 내 도커 허브에 푸시했다. 참고로 푸시하기 전에 도커 허브에 레포지토리를 먼저 만들어주어야 한다.

  • 태그 : docker tag first-fastapi-image:0.0 mingadinga/first-fastapi-image:0.0
  • 푸시 : docker push mingadinga/first-fastapi-image:0.0

짠. 잘 올라갔습니다.

 

 

Step5. Cloud Type에 배포

클라우드 타입에서 제공하는 도커 파일을 이용한 배포 문서를 참고해서 배포를 진행했다.

 

깃허브 레포지토리를 공유한다. dockerfile config를 클릭하면 설정 정보를 넣을 수 있는데, 아직은 build나 run 명령어를 사용할 때 필요한 환경 변수는 없으므로 공란으로 남겨둔다. 도커 파일에서 설정한 80포트만 입력한다. Dockerfile path는 도커 파일의 이름을 작성한다. (Dockerfile.dev , Dockerfile.prod 등으로 변경 가능하다) Region은 서울을 선택하고 Deploy한다.

 

 

 

 

빌드 후에 런타임 로그를 찍어보니 ERROR: [Errno 13] error while attempting to bind on address ('0.0.0.0', 80): permission denied 이런 오류가 보였다. 도커 컨테이너 내에서 80번 포트를 사용하기 위해서는 루트 권한이 필요한데, 클라우드 타입은 보안 강화를 위해 non-root로 컨테이너를 실행하도록 도커 파일을 작성해야한다고 한다. 그래서 루트 권한이 필요하지 않은 8000번 포트로 변경했다.

 

도커 파일을 다음과 같이 수정하고 푸시했다.

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

 

클라우드 타입으로 돌아가 설정에서 포트 정보를 8000번으로 변경한다.

깃허브에 새로 커밋한 내용들을 받아서 재배포하려면 해당 문서를 참고해서 재배포한다.

 

재배포에 성공하면 배포 내역에 새로운 항목이 뜰 것이다.

이제 콘솔에 들어가 런타임 로그를 확인해보면 ~~~~

 

 

짠! 잘 됐다. 도메인으로 접속도 해보자.

 

 

성공적으로 배포가 되었다. Hello World 프로젝트를 도커로 클라우드 환경에 배포했다. 👏

 

2. Planet Scale에 데이터베이스 만들고 더미 데이터 추가

 

Step1. 데이터베이스 생성

문서를 보고 데이터베이스를 생성해보자.

 

다음과 같이 DB의 이름과 리전을 선택한다.

 

 

PlanetScale은 특이하게 깃처럼 브랜치를 제공한다. Prod 브랜치는 프로덕션 트래픽용 고가용성 데이터베이스이다. 기본적으로 스키마가 직접 변경되지 않도록 보호되며 매일 자동 백업된다. Dev 브랜치는 Prod DB 스키마의 복사본을 받아서 스키마를 변경할 수 있다. 더 자세한 내용은 아래 문서 참고!

 

 

Step2. main 브랜치에 스키마 생성하고 데이터 넣기

브랜치 탭에서 main을 확인해보니 production 브랜치라고 한다. 그렇다면 main에서 Dev 브랜치를 만들어서 User 테이블을 만들고 테스트용 데이터를 넣어보자. 어느 정도 필요한 테이블이 Dev에 다 모였다면, main에 푸시해서 운영 환경에서 안전하게 사용하면 된다.

 

 

d1 브랜치에서 콘솔을 연결하고, 아래 SQL을 입력해서 스키마를 생성하고 데이터를 추가한다.

 

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    age INT
);

INSERT INTO users (name, age) VALUES ('Bob', 30), ('Charlie', 35), ('Dave', 40);

간단한 유저 테이블을 만들고 데이터를 추가했다.

 

 

Note : Planet Scale은 FK 제약조건을 지원하지 않는다…?!

 

(아직은 FK 포함한 테이블이 필요하지 않아서 개념만 알아보겠습니다)

 

Operating without foreign key constraints — PlanetScale Documentation

 

Operating without foreign key constraints — PlanetScale Documentation

How to manage your relational data without formal foreign key constraints in your schema

planetscale.com

 

Planet Scale은 FK 제약조건을 지원하지 않는다. 온라인 DDL의 이점(지속적인 배포 및 스키마 변경, 스키마 변경 중 블로킹 방지)과 데이터베이스 서버 분할을 유지하기 위함이라고 한다. FK 제약조건을 사용하지 않더라도 동일한 방식으로 테이블을 조인할 수 있다.

 

기존 MySQL의 테이블 생성문

CREATE TABLE parent_table (
  id INT NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE child_table (
  id INT NOT NULL,
  parent_id INT,
  PRIMARY KEY (id),
  KEY parent_id_idx (parent_id), // FK 인덱스
  CONSTRAINT `child_parent_fk` FOREIGN KEY (parent_id) REFERENCES parent_table(id) ON DELETE NO ACTION
);

 

Planet Scale에서 사용하는 테이블 생성문

CREATE TABLE child_table (
  id INT NOT NULL,
  parent_id INT,
  PRIMARY KEY (id),
  KEY parent_id_idx (parent_id)
	// FK 제약조건을 따로 명시하지 않음
);

 

 

3. hello world 프로젝트에 DB 연결

애플리케이션에 데이터베이스를 연결하려면 DB 라이브러리를 설치하고, DB 커넥션 정보를 세팅해서 커넥션 객체를 주입 받아야 한다. Fast API에서 DB 연결하는 과정은 Fast API 공식 튜토리얼를 참고했다.

 

Planet Scale에 있는 설정 정보 값으로 Fast API를 연결해보자.

 

Step1. 로컬에서 DB 연결

Planet Scale에 들어가서 테이블을 만든 브랜치를 클릭해 탭으로 들어가면 Connect 버튼이 있다. 이걸 클릭하면 Planet Scale가 제공하는 MySQL 클라이언트 설치 명령어, 환경변수, main.py에서 커넥션을 받아오는 코드를 볼 수 있다. 하나씩 해보자.

 

1. MySQL 설치

pip install python-dotenv mysqlclient

 

2. 환경변수 파일 생성

루트 경로에 .env 파일을 만들고 git ignore에 추가한다. .env 파일은 스프링 properties나 yaml 파일처럼 코드로 올라가면 안되는 값들을 보관하는 파일이다. Planet Scale이 제공한 정보를 넣는다. 참고로 비밀번호는 처음 만들어졌을때만 보여주기 때문에 저렇게 ****** 가 뜬다면 다시 만들어야 한다. 유저 네임도 같이 만들어지니까 복붙하자.

 

3. main.py에 커넥션 받아오는 코드 추가

pem 키 경로는 .env 파일로 추출하고 여기서는 os.getenv로 주입받는다.

from dotenv import load_dotenv
load_dotenv()
import os
import MySQLdb

connection = MySQLdb.connect(
  host= os.getenv("HOST"),
  user=os.getenv("USERNAME"),
  passwd= os.getenv("PASSWORD"),
  db= os.getenv("DATABASE"),
  ssl_mode = "VERIFY_IDENTITY",
  ssl      = {
    'ca': os.getenv("SSL_CERT") # .env 파일 - SSL_CERT=/etc/ssl/cert.pem
  }
)

 

4. main.py에 DB 사용하는 API 코드 넣기

간단하게 유저를 조회하고 저장하는 코드를 (gpt가) 만들어서 넣어준다.

class User(BaseModel):
    name: str
    age: int

@app.post("/users/")
async def create_user(user: User):
    # MySQL 쿼리 실행
    cursor = connection.cursor()
    sql = "INSERT INTO users (name, age) VALUES (%s, %s)"
    val = (user.name, user.age)
    cursor.execute(sql, val)
    connection.commit()
    return {"message": "User created successfully"}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # MySQL 쿼리 실행
    cursor = connection.cursor()
    sql = "SELECT name, age FROM users WHERE id=%s"
    cursor.execute(sql, (user_id,))
    result = cursor.fetchone()
    if result:
        name, age = result
        return {"id": user_id, "name": name, "age": age}
    else:
        return {"message": "User not found"}

 

5. 로컬에서 테스트

localhost:8000/users/1로 유저 조회 api를 테스트해보면 .. 잘 된다.

 

유저 생성은 스웨거로 해보자. localhost:8000/docs로 스웨거에 들어가서 post 요청을 보낸다. 200 떴다 (ง ˘ω˘ )ว

 

 

Step2. 로컬에서 컨테이너로 실행

MySQL을 추가했으니 도커 파일도 수정해보자.

mysql 설치 명령어 RUN으로 추가하고 .env 파일 같이 복사하면 .. 되지 않을까?

# 도커 파일에 추가
RUN pip install python-dotenv mysqlclient
# COPY ./.env /code/.env # 이건 빼주세요 CloudType 배포할땐 .env 파일 못 써요

이제 docker run 명령어를 실행할 때 .env 파일을 옵션으로 같이 넘겨서 실행하면 된다. 이미지를 빌드하고 컨테이너를 실행했다.

  • 빌드 : docker build -t first-fastapi-image:0.2 ./
  • 컨테이너 : docker run -d --name mycontainer_with_planetscale -p 80:8000 --env-file .env first-fastapi-image:0.2

 

뭐지 😇

컨테이너를 띄우자마자 죽는다.. 종료 코드가 1이더라

File "/code/./app/main.py", line 9, in <module>
    connection = MySQLdb.connect(
  File "/usr/local/lib/python3.9/site-packages/MySQLdb/__init__.py", line 123, in Connect
    return Connection(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/MySQLdb/connections.py", line 185, in __init__
    super().__init__(*args, **kwargs2)
MySQLdb.NotSupportedError: MySQL client library does not support ssl_mode specification

내가 설치한 MySQl 드라이버에 문제가 있는 것 같다. Planet Scale이 설치하라고 알려준 라이브러리였는데 문제가 있나보다.

 

서치 하다가 나랑 같은 오류가 난 사람의 글을 발견!

How to use PlanetScale with Python

 

How to use PlanetScale with Python

Planetscale seems to be a pretty good database system but I was having problems connecting to it from Python, specifically from within a…

vfxger.medium.com

 

이 분도 나처럼 파이썬 애플리케이션을 패키징하는 도커에 Planet Scale DB를 연결하고 있었는데, 나랑 같은 오류가 났었다. mysql-connector-python으로 드라이버를 바꾸고 다른 인증키를 사용하니 정상적으로 작동한다고 한다. (이분은 파이썬 3.8 이미지를 사용하셨다고 한다 근데 3.9에서도 잘 된다!)

 

일단 로컬에서 설정 바꿔서 실행해보자.

  1. mysql-connector-python 설치 : pip install mysql-connector-python
  2. requirements.txt에 추가 : mysql-connector-python~=8.0.32
  3. main.py에서 커넥션 받아오는 부분 수정
  4. from mysql.connector import Error import mysql.connector app = FastAPI() load_dotenv() connection = mysql.connector.connect( host=os.getenv("HOST"), database=os.getenv("DATABASE"), user=os.getenv("USERNAME"), password=os.getenv("PASSWORD"), ssl_ca=os.getenv("SSL_CERT") )
  5. .env Certificate 경로 수정 : SSL_CERT=/etc/ssl/certs/ca-certificates.crt

돌려보니 잘 된다.

 

이제 도커 파일에도 적용해보자. mysql connector 설치 명령어만 변경한다.

RUN pip install mysql-connector-python

빌드하고 돌려보면 ~~~ 성공 ~~~~ 👍

 

 

 

Step3. CloudType에 배포

🙏 : (배포만 잘 되면 된다 제발 한번에 가자..)

 

위에서 작업한 코드는 깃허브에 푸시해서 마스터 브랜치에 반영한다. 이제 CloudType에서 재배포를 해야하는데, .env 파일은 깃허브에 올라가지 않기 때문에 docker run에 필요한 환경변수들을 CloudType에서 설정해야한다.

 

프로젝트 > 설정 > Configuration > Environment variables에서 Name-Value로 환경변수를 설정할 수 있다. .env 파일에 있던 변수들을 여기에 복붙하고 Deploy하자.

 

 

뭐임 🤔

 

아 바보인가 ~~~ 도커 파일에서 .env 복사하는 부분 빼서 커밋하고 다시 빌드!

 

 

조회 성공! (✌’ω’)✌

 

생성도 성공! ✌(‘ω’✌)

 

 

끝입니다.. ( ⌒ ͜ ⌒ )

도커파일 연습하려고 시작했다가

ssl에서 열심히 삽질했도다

이제 서비스 좀 불려봐야겠다

언제할진 모르겠지만..

09 : FastAPI Connecting to a Database

 

09 : FastAPI Connecting to a Database

09 : FastAPI Connecting to a Database Super, So, far we have done a lot. Now, its time to add persistence. We are going to connect a database to fastapi. I am opting for PostgreSQL. You might need to download Postgres and PgAdmin(for monitoring Postgres).

www.fastapitutorial.com