FastAPI, PostgreSQL, Nginx를 활용한 무중단 배포 학습 프로젝트입니다.
- FastAPI: 백엔드 API 서버 (2개 인스턴스)
- PostgreSQL: 데이터베이스
- Nginx: 로드 밸런서
- Alembic: 데이터베이스 마이그레이션 도구
- Locust: 부하 테스트 도구
- FastAPI 서버 2대를 순차적으로 재시작
- Graceful Shutdown으로 진행 중인 요청 보호
- Nginx 로드 밸런서로 트래픽 분산
- Health Check 기반 배포 검증
데이터베이스 스키마 변경을 안전하게 관리하는 방법을 학습했습니다.
이 프로젝트의 Alembic 학습 과정을 직접 따라해볼 수 있습니다.
📌 커밋 1: 초기 마이그레이션 설정 (ce69d14)
목표: Alembic 설정 및 첫 마이그레이션 생성
1단계: Dockerfile 수정
# Dockerfile에 추가
COPY alembic.ini .
COPY alembic ./alembic💡 Docker 컨테이너에서 alembic 명령어를 실행하려면 alembic 파일들이 이미지에 포함되어야 합니다.
2단계: 초기 마이그레이션 생성
# 로컬에서 실행 (개발 환경)
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic revision --autogenerate -m "Initial migration: create items table"명령어 분석:
alembic revision: 새 마이그레이션 파일 생성--autogenerate: SQLAlchemy 모델과 DB를 비교해서 자동으로 코드 생성-m "메시지": 마이그레이션 설명
결과:
Generating alembic/versions/0fee3ec0c79f_initial_migration_create_items_table.py ... done
INFO [alembic.autogenerate.compare] Detected added table 'items'
INFO [alembic.autogenerate.compare] Detected added index 'ix_items_id'
INFO [alembic.autogenerate.compare] Detected added index 'ix_items_name'
생성된 파일 확인:
# alembic/versions/0fee3ec0c79f_initial_migration_create_items_table.py
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ...
⚠️ 중요:--autogenerate는 자동 생성이지만 항상 검토 필요! 주석에 "please adjust!"라고 써있는 이유입니다.
3단계: 마이그레이션 적용
# DB에 마이그레이션 적용
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic upgrade head출력:
INFO [alembic.runtime.migration] Running upgrade -> 0fee3ec0c79f, Initial migration: create items table
4단계: 적용 확인
# 현재 마이그레이션 상태 확인
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic current출력:
0fee3ec0c79f (head)
📌 커밋 2: 스키마 변경 실습 (e35dc3b)
목표: 컬럼 추가 및 마이그레이션 자동 생성 학습
1단계: 모델 수정
# app/models.py
from sqlalchemy import Column, Integer, String, DateTime, Numeric # Numeric 추가
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(String)
price = Column(Numeric(10, 2), nullable=True) # 👈 추가
stock = Column(Integer, default=0) # 👈 추가
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())2단계: 스키마도 함께 수정
# app/schemas.py
from decimal import Decimal
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
price: Optional[Decimal] = None # 👈 추가
stock: int = 0 # 👈 추가3단계: 마이그레이션 생성 (변경 감지)
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic revision --autogenerate -m "Add price and stock columns to items table"출력:
INFO [alembic.autogenerate.compare] Detected added column 'items.price'
INFO [alembic.autogenerate.compare] Detected added column 'items.stock'
Generating alembic/versions/211d1fe6d912_add_price_and_stock_columns_to_items_.py ... done
💡 Alembic의 마법: 모델 변경을 자동으로 감지해서 마이그레이션 코드 생성!
생성된 파일:
# alembic/versions/211d1fe6d912_add_price_and_stock_columns_to_items_.py
def upgrade() -> None:
op.add_column('items', sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True))
op.add_column('items', sa.Column('stock', sa.Integer(), nullable=True))
def downgrade() -> None:
op.drop_column('items', 'stock')
op.drop_column('items', 'price')4단계: 마이그레이션 적용
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic upgrade head출력:
INFO [alembic.runtime.migration] Running upgrade 0fee3ec0c79f -> 211d1fe6d912, Add price and stock columns
5단계: 롤백 테스트
# 한 단계 되돌리기
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic downgrade -1출력:
INFO [alembic.runtime.migration] Running downgrade 211d1fe6d912 -> 0fee3ec0c79f
6단계: 다시 최신으로
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic upgrade head✅ 학습 포인트: upgrade/downgrade가 정확히 반대 동작을 수행합니다!
7단계: 히스토리 확인
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic history출력:
0fee3ec0c79f -> 211d1fe6d912 (head), Add price and stock columns to items table
<base> -> 0fee3ec0c79f, Initial migration: create items table
📌 커밋 3: 무중단 배포 통합 (2186626)
목표: 배포 스크립트에 마이그레이션 자동화
1단계: deploy.sh 수정
# deploy.sh
# 새 이미지 빌드
docker-compose build
# 👇 마이그레이션 추가
echo "2. 데이터베이스 마이그레이션 실행..."
docker-compose run --rm app1 alembic current
if docker-compose run --rm app1 alembic upgrade head; then
echo " - 마이그레이션 완료!"
else
echo " - 오류: 마이그레이션 실패! 배포를 중단합니다."
exit 1
fi
docker-compose run --rm app1 alembic current
# 이후 app1, app2 순차 재시작...왜 docker-compose run을 사용할까?
# ❌ 로컬 실행의 문제점
alembic upgrade head
# - 로컬 Python 환경 필요
# - localhost:5433으로 DB 접속 (외부 포트)
# - 배포 환경과 다를 수 있음
# ✅ docker-compose run 장점
docker-compose run --rm app1 alembic upgrade head
# - 실제 배포 환경과 동일한 컨테이너에서 실행
# - Docker 네트워크 내부에서 db:5432 접속 (내부 통신)
# - --rm: 실행 후 컨테이너 자동 삭제 (정리)명령어 분석:
docker-compose run --rm app1 alembic upgrade head
↓ ↓ ↓ ↓
도구명 옵션 서비스명 실행할 명령어
- run: 일회성 명령어 실행 (새 컨테이너 생성)
- --rm: 실행 후 컨테이너 자동 삭제
- app1: docker-compose.yml의 서비스명
- alembic upgrade head: 컨테이너 내부에서 실행할 명령💡 중요: app1이 이미 실행 중이어도 괜찮습니다!
docker-compose run은 새로운 독립적인 컨테이너를 생성하기 때문입니다.
2단계: 배포 실행
chmod +x deploy.sh
./deploy.sh배포 순서:
1. 이미지 빌드
2. 마이그레이션 실행 (DB 스키마 변경) ← 새로 추가!
3. app1 재시작 (새 코드)
4. app2 재시작 (새 코드)
📌 커밋 4: 데이터 마이그레이션 (a4ff441)
목표: SQL을 직접 실행하는 데이터 마이그레이션 학습
스키마 마이그레이션 vs 데이터 마이그레이션:
| 구분 | 스키마 마이그레이션 | 데이터 마이그레이션 |
|---|---|---|
| 목적 | 테이블/컬럼 구조 변경 | 데이터 변환/정제 |
| 생성 | --autogenerate |
수동 작성 |
| 예시 | ADD COLUMN | UPDATE 기존 데이터 |
1단계: 빈 마이그레이션 생성
# --autogenerate 없이 생성 (수동 작성용)
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic revision -m "Set default values for existing items"출력:
Generating alembic/versions/f25407aec7a2_set_default_values_for_existing_items.py ... done
생성된 파일 (초기 상태):
def upgrade() -> None:
pass # 👈 비어있음! 직접 작성해야 함
def downgrade() -> None:
pass2단계: 데이터 마이그레이션 코드 작성
# alembic/versions/f25407aec7a2_set_default_values_for_existing_items.py
def upgrade() -> None:
# op.execute()로 직접 SQL 실행
op.execute("""
UPDATE items
SET stock = 0
WHERE stock IS NULL
""")
op.execute("""
UPDATE items
SET price = 1000.00
WHERE price IS NULL
""")
def downgrade() -> None:
# 데이터 마이그레이션은 롤백이 어려움
pass💡 핵심:
op.execute()로 어떤 SQL이든 실행 가능!
3단계: 적용
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" \
python3 -m alembic upgrade head출력:
INFO [alembic.runtime.migration] Running upgrade 211d1fe6d912 -> f25407aec7a2, Set default values for existing items
📌 커밋 5: 문서화 (bd8242a)
README에 실전 가이드 추가 (이 섹션!)
# 1️⃣ 모델 변경
vim app/models.py # 컬럼 추가/수정
# 2️⃣ 마이그레이션 생성 (자동)
alembic revision --autogenerate -m "설명"
# 3️⃣ 생성된 파일 검토
vim alembic/versions/xxxxx_설명.py
# ⚠️ 항상 검토 필수!
# 4️⃣ 적용
alembic upgrade head
# 5️⃣ 확인
alembic current
alembic history
# 6️⃣ (필요시) 롤백
alembic downgrade -1
# 7️⃣ 배포
./deploy.sh # 자동으로 마이그레이션 실행됨- ✅ 스키마 버전 관리로 팀 협업 효율성 향상
- ✅ 자동 생성 + 수동 검토로 안전한 마이그레이션
- ✅ 배포 전 마이그레이션으로 다운타임 최소화
- ✅ nullable 컬럼으로 무중단 배포 호환성 유지
- ✅ 트랜잭션 기반 실행으로 실패 시 자동 롤백
더 자세한 학습 내용은 plan.md의 "6단계: Alembic 데이터베이스 마이그레이션" 섹션을 참고하세요.
GET /health- 서버 상태 확인
GET /- 기본 응답 (버전 정보 포함) 응답시간을 의도적으로 늘리기 위해 sleep(10)를 넣어놓음.
# Docker Compose로 모든 서비스 실행
docker-compose up -d
# 로그 확인
docker-compose logs -f실행되는 서비스:
db: PostgreSQL 데이터베이스 (포트 5432)app1: FastAPI 서버 1 (포트 8001)app2: FastAPI 서버 2 (포트 8002)nginx: 로드 밸런서 (포트 80)
무중단 배포를 위해서는 다음 요소들이 필요합니다:
로드 밸런서와 배포 스크립트가 서버 상태를 확인하기 위해 필요합니다.
@app.get("/health")
async def health_check():
return {"status": "healthy"}애플리케이션 시작/종료 시 리소스 정리를 위해 사용합니다.
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: 데이터베이스 연결, 리소스 초기화
Base.metadata.create_all(bind=engine)
print("Application started")
yield
# Shutdown: 연결 종료, 리소스 정리
print("Shutting down gracefully...")
await asyncio.sleep(0.1) # 진행 중인 요청 대기
app = FastAPI(lifespan=lifespan)주의: 커스텀 signal handler를 추가하면 Uvicorn의 graceful shutdown을 방해할 수 있으므로 제거하는 것이 좋습니다.
Dockerfile CMD에서 설정:
CMD ["uvicorn", "app.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--timeout-graceful-shutdown", "30"]- SIGTERM 수신 시 새 요청 거부
- 진행 중인 요청은 최대 30초 대기
- 요청 완료 시 즉시 종료
Dockerfile에 명시:
STOPSIGNAL SIGTERM각 서비스의 상태를 모니터링:
app1:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s주의: 컨테이너에 curl이 설치되어 있어야 합니다 (Dockerfile에 추가).
운영 환경에서는 자동 재시작 설정:
app1:
restart: unless-stopped서비스 시작 순서 및 의존성 관리:
app1:
depends_on:
db:
condition: service_healthy백엔드 서버 상태 자동 감지:
upstream backend {
least_conn; # 연결 수가 적은 서버로 요청 분산
server app1:8000 max_fails=3 fail_timeout=30s;
server app2:8000 max_fails=3 fail_timeout=30s;
}긴 요청 처리를 위한 타임아웃 설정:
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;# 1. SIGTERM으로 graceful shutdown 시작
docker-compose stop -t 30 app1
# 2. 새 컨테이너 시작
docker-compose up -d app1
# 3. Health check 확인
until curl -f http://localhost:8001/health; do
sleep 1
donestop -t 30: SIGTERM → 30초 대기 → SIGKILL (graceful)kill: 즉시 강제 종료 (진행 중인 요청 손실)
| 요소 | 필수 여부 | 이유 |
|---|---|---|
/health 엔드포인트 |
필수 | 서버 상태 확인용 |
| Uvicorn graceful shutdown timeout | 필수 | 진행 중인 요청 보호 |
| Docker health check | 필수 | 서비스 준비 상태 확인 |
| Lifespan 이벤트 | 권장 | 리소스 정리 |
| Nginx 로드 밸런서 | 권장 | 무중단 전환 |
| 커스텀 signal handler | ❌ 불필요 | Uvicorn이 자동 처리 |
# 배포 스크립트에 실행 권한 부여
chmod +x deploy.sh
# 무중단 배포 실행
./deploy.sh- 새 이미지 빌드: 최신 코드로 Docker 이미지 생성
- app1 배포:
- SIGTERM 신호 전송 → Graceful Shutdown
- 기존 요청 완료 대기 (최대 30초)
- 컨테이너 재시작
- Health check 통과 확인
- 안정화 대기: 5초간 대기
- app2 배포: app1과 동일한 과정 반복
FastAPI 애플리케이션은 SIGTERM 신호를 받으면:
- 새로운 요청 수락 중지
- 현재 처리 중인 요청 완료 대기 (최대 30초)
- 모든 연결 정리 후 종료
참고: app/main.py, Dockerfile
pip install locust배포 중 서비스 연속성 확인:
# 터미널 1: Locust 실행
locust -f locustfile.py --users 20 --spawn-rate 2 --host http://localhost DeploymentTestUser
localhost:8089 에 접속해서 http://localhost 에 요청 보내기 시작.
# 터미널 2: 배포 실행
./deploy.shAlembic은 SQLAlchemy를 위한 데이터베이스 마이그레이션 도구입니다. 데이터베이스 스키마 변경을 버전 관리하고, 여러 환경에서 일관되게 적용할 수 있습니다.
# 로컬에서 실행 (개발 시)
DATABASE_URL="postgresql://user:password@localhost:5433/testdb" python3 -m alembic <command>
# Docker 컨테이너에서 실행 (배포 시)
docker-compose run --rm app1 alembic <command># 스키마 변경 자동 감지하여 마이그레이션 생성
alembic revision --autogenerate -m "설명"
# 수동 마이그레이션 생성 (데이터 변환 등)
alembic revision -m "설명"# 최신 버전으로 업그레이드
alembic upgrade head
# 특정 리비전으로 업그레이드
alembic upgrade <revision_id>
# 한 단계 롤백
alembic downgrade -1
# 특정 리비전으로 다운그레이드
alembic downgrade <revision_id># 현재 적용된 마이그레이션 확인
alembic current
# 마이그레이션 히스토리 확인
alembic history
# 적용되지 않은 마이그레이션 확인
alembic headsalembic/
├── env.py # Alembic 환경 설정
├── script.py.mako # 마이그레이션 템플릿
└── versions/ # 마이그레이션 파일들
├── 0fee3ec0c79f_initial_migration_create_items_table.py
├── 211d1fe6d912_add_price_and_stock_columns_to_items_.py
└── f25407aec7a2_set_default_values_for_existing_items.py
# alembic/versions/211d1fe6d912_add_price_and_stock_columns_to_items_.py
def upgrade() -> None:
op.add_column('items', sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True))
op.add_column('items', sa.Column('stock', sa.Integer(), nullable=True))
def downgrade() -> None:
op.drop_column('items', 'stock')
op.drop_column('items', 'price')# alembic/versions/f25407aec7a2_set_default_values_for_existing_items.py
def upgrade() -> None:
op.execute("UPDATE items SET stock = 0 WHERE stock IS NULL")
op.execute("UPDATE items SET price = 1000.00 WHERE price IS NULL")
def downgrade() -> None:
pass # 데이터 마이그레이션은 롤백이 어려움배포 스크립트(deploy.sh)는 자동으로 마이그레이션을 실행합니다:
1. 이미지 빌드
2. 데이터베이스 마이그레이션 실행 ← alembic upgrade head
3. app1 배포
4. app2 배포
하위 호환 마이그레이션 원칙:
- ✅ 컬럼 추가는
nullable=True로 설정 - ✅ 인덱스 추가는 안전
⚠️ 컬럼 삭제, NOT NULL 제약은 2단계 배포 필요
# 현재 상태 확인
alembic current
# 마이그레이션 히스토리 확인
alembic history
# 특정 리비전으로 강제 설정 (주의!)
alembic stamp <revision_id>마이그레이션은 트랜잭션 내에서 실행되므로 실패 시 자동 롤백됩니다. 하지만 배포는 중단되므로:
- 오류 로그 확인
- 마이그레이션 파일 수정
- 다시 배포 실행
더 자세한 학습 내용은 plan.md의 "6단계: Alembic 데이터베이스 마이그레이션" 섹션을 참고하세요.