Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Node & TypeScript
node_modules
dist
npm-debug.log
yarn-debug.log
yarn-error.log

# Environment
.env
.env.*
!env.example # 예시 파일 허용

# Git / Actions
.git
.gitignore
.github

# Prisma
src/prisma/*.db
src/prisma/migrations/dev.db-journal
src/prisma/dev.db

# Docker
Dockerfile
docker-compose.yaml
docker-compose.prod.yaml

# OS / IDE
.DS_Store
.vscode
.idea
*.swp
*.swo
*.bak
Thumbs.db
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# PostgreSQL DB
DATABASE_URL=postgresql://username:password@your-rds-endpoint:5432/panda_market

# BACKEND PORT
# App
PORT=3000
BASE_URL=https://pandamarket.mimu.live

# JWT Secrets
ACCESS_TOKEN_SECRET=
REFRESH_TOKEN_SECRET=

# BASE URL (API ORIGIN)
BASE_URL=https://pandamarket.mimu.live
# Uploads
UPLOAD_STRATEGY=local # local | s3
UPLOAD_DIR=/app/uploads

# AWS S3
AWS_REGION=ap-northeast-2
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CD

on:
push:
branches:
- main

jobs:
deploy:
name: Deploy to EC2
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

- name: Deploy to EC2
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
# 작업 디렉토리로 이동 또는 생성
mkdir -p ~/app
cd ~/app

# docker-compose.prod.yaml 생성
cat > docker-compose.prod.yaml << 'YML'
version: "3.9"
services:
web:
image: ${{ secrets.IMAGE }}
env_file:
- .env.prod
ports:
- "3000:3000"
restart: unless-stopped
YML

# .env.prod 생성
cat > .env.prod << 'ENV'
DATABASE_URL=${{ secrets.DATABASE_URL }}
ACCESS_TOKEN_SECRET=${{ secrets.ACCESS_TOKEN_SECRET }}
REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }}
BASE_URL=${{ secrets.BASE_URL }}
AWS_REGION=${{ secrets.AWS_REGION }}
AWS_S3_BUCKET=${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
NODE_ENV=production
PORT=3000
UPLOAD_STRATEGY=s3
ENV

# Docker compose 재시작
docker compose -f docker-compose.prod.yaml pull
docker compose -f docker-compose.prod.yaml up -d
EOF
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI (PR)

on:
pull_request:
branches: ["**"]

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: panda_market_test
POSTGRES_USER: app
POSTGRES_PASSWORD: app
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U app -d panda_market_test"
--health-interval=5s --health-timeout=3s --health-retries=10

env:
NODE_ENV: test
DATABASE_URL: postgres://app:app@localhost:5432/panda_market_test
ACCESS_TOKEN_SECRET: test-access
REFRESH_TOKEN_SECRET: test-refresh

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

- name: Prisma generate & migrate
run: |
npx prisma generate --schema=src/prisma/schema.prisma
npx prisma migrate deploy --schema=src/prisma/schema.prisma

- name: Test
run: CI=true npm test
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# deps / build
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npx prisma generate --schema=src/prisma/schema.prisma
RUN npm run build

# runtime
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV UPLOAD_DIR=/app/uploads

# Prisma 및 bcrypt 등 네이티브 모듈 호환성을 위한 보강
RUN apk add --no-cache openssl libc6-compat

# non-root
RUN addgroup -S app && adduser -S app -G app
RUN mkdir -p /app/uploads && chown -R app:app /app
USER app

COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/dist ./dist
COPY --from=deps /app/src/prisma ./src/prisma
COPY package*.json ./

EXPOSE 3000
# Prisma 마이그레이션 후 서버 기동
CMD sh -c "npx prisma migrate deploy --schema=src/prisma/schema.prisma && node dist/app.js"
69 changes: 33 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,47 @@
## 미션 목표
- 판다마켓 서비스를 AWS로 배포하기
- AWS S3 적용
- AWS RDS 적용
- AWS EC2에 Express 서버 배포하기
- (심화) 프로세스 매니저 적용
- (심화) 리버스 프록시 적용
- Github Actions로 테스트, 배포 자동화
- Docker 이미지 만들기

## 요구사항

### 기본 요구사항
- [x] 프로젝트에 프로덕션 배포를 위한 환경 변수 설정을 해 주세요.
### Github Actions 활용
- [x] 브랜치에 pull request가 발생하면 테스트를 실행하는 액션을 구현해 주세요.
- [x] `main` 브랜치에 push가 발생하면 AWS 배포를 진행하는 액션을 구현해 주세요.
- [x] 개인 Github 리포지터리에서 Actions 동작을 확인해 보세요.

### AWS S3 적용
- [x] AWS S3 버킷을 생성하고, 퍼블릭 액세스를 허용해 주세요.
- [x] 일반 사용자가 S3 업로드된 파일에 접근할 수 있도록 S3 버킷 정책을 설정해 주세요.
- [x] AWS EC2에서 AWS S3를 사용하기 위한 액세스 키를 AWS IAM에서 발급하세요.
- [x] 프로덕션 환경에서는 파일 업로드에 AWS S3를 사용하도록 구현을 수정해 주세요.
- [x] 데이터베이스는 Postgres 이미지를 사용해 연결하도록 구현해 주세요.
- [x] 실행된 Express 서버 컨테이너는 호스트 머신에서 3000번 포트로 접근 가능하도록 구현해 주세요.

### AWS RDS 적용
- [x] AWS RDS 프리티어에 해당하는 인스턴스를 생성합니다.
- [x] RDS 인스턴스에 대한 보안 그룹을 설정합니다.
- [x] 프로덕션 환경에서는 Prisma에 프로젝트 데이터베이스와 연결하도록 합니다.

### AWS EC2에 Express 서버 배포하기
- [x] AWS EC2 프리티어에 해당하는 인스턴스를 생성합니다.
- [x] SSH를 사용해 EC2 인스턴스에 접속해 Express 서버를 배포해 주세요.
### Docker 이미지 만들기
다음을 만족하는 Dockerfile과 docker-compose.yaml을 작성해 주세요.

### 심화 요구사항
- [x] EC2 인스턴스에서 pm2 프로세스 매니저를 사용하여 애플리케이션을 실행해 주세요.
- [x] EC2 인스턴스에서 nginx 리버스 프록시를 설정해 서버를 80번 포트로 서비스합니다.
- [x] Express 서버를 실행하는 Dockerfile을 작성해 주세요.
- [x] Express 서버가 파일 업로드를 처리하는 폴더는 Docker의 Volume을 활용하도록 구현해 주세요.
- [x] 프로덕션 환경에서는 Prisma에 프로젝트 데이터베이스와 연결하도록 합니다.

---

## 멘토에게
- AWS 프리티어 사용이 불가하여, 비용 문제로 다른 서버에 배포하였습니다.
- AWS 관련 설정은 캡쳐한 후 인스턴스 및 스토리지 삭제하는 방식으로 미션 진행하였습니다.
- 제출한 이미지 업로드 코드는 S3 기반으로 작성하였지만, 실제 배포 환경에서는 이미지 파일을 로컬 스토리지에 업로드합니다.
- 엔드포인트는 https://pandamarket.mimu.live 입니다.
- 요구사항에는 없지만, PM2 및 Nginx 환경 설정을 일부 보강하여 적용하였습니다.
- PM2
- 프로세스 안정성을 고려하여, 메모리 300MB 초과 시에 자동 재시작하도록 설정했습니다.
중급 프로젝트 시에 메모리 문제로 서비스가 종료된 적이 있어 추가 설정하였습니다.
- TS 기반으로 작성되었으며, 배포 환경에서 불필요한 재시작 루프를 방지하기 위해
watch 옵션을 명시적으로 비활성화했습니다.
- Nginx
- 파일 업로드 용량 제한을 2MB로 명시적으로 설정했습니다.
- `real_ip_header CF-Connecting-IP` 및 Cloudflare IP 대역을 지정하여,
프록시 환경에서도 실제 클라이언트 IP를 확인할 수 있도록 구성했습니다.
- 단순 IP가 아닌 실제 도메인으로 접근 가능하도록 설정하여 구현했습니다.
- 기본 요구사항은 모두 충족하였으며, 추가 구현한 내용을 아래에 기재합니다.

### 헬스체크
- `docker-compose.yaml` 내에서 Postgres 컨테이너의 상태를 `pg_isready`로 주기적으로 확인하도록 설정했습니다.
- 애플리케이션 컨테이너는 DB 서비스가 정상 상태(`healthy`)일 때만 기동되도록 의존성을 설정했습니다.

### Volume 구성 강화
- 업로드 파일을 컨테이너 외부에서도 보존할 수 있도록 `/app/uploads` 경로를 호스트와 Volume으로 연결했습니다.
- 애플리케이션 실행 시 업로드 폴더가 자동 생성되도록 Dockerfile 내에 디렉터리 생성 및 권한 설정을 추가했습니다.

### Prisma 마이그레이션 자동화
- 배포 시 Prisma 마이그레이션이 자동으로 반영되도록 Dockerfile의 CMD 단계에 `migrate deploy` 명령어를 포함했습니다.

### 보안 및 권한 관리
- 컨테이너 내에서 애플리케이션이 root 권한으로 실행되지 않도록 `USER app`을 설정했습니다.
- 이미지 빌드 시 불필요한 파일 및 비공개 설정이 포함되지 않도록 `.dockerignore`를 구성했습니다.

### upload 미들웨어
- `.env`의 실행 환경 설정에 따라 로컬 스토리지 또는 AWS S3 중 하나를 선택하여 파일 업로드를 처리하도록 수정했습니다.
- 이에 따라 `.env.example`에 `UPLOAD_STRATEGY` 항목을 추가했습니다.
35 changes: 35 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: '3.9'

services:
# PostgreSQL 데이터베이스 서비스
db:
image: postgres
restart: always
ports:
- '5432:5432'
volumes:
- db:/var/lib/postgresql/data
env_file:
- .env
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 3s
retries: 10

# Node.js 애플리케이션 서비스
web:
build: .
restart: always
ports:
- '3000:3000'
depends_on:
db:
condition: service_healthy
env_file:
- .env
volumes:
- ./uploads:/app/uploads
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bind mounts보다는 named volumes를 사용하는 것을 좀 더 권장합니다.


volumes:
db:
Loading