Files
tasteby/docs/deployment-guide.md
joungmin f2861b6b79 홈 탭 장르 카드 UI + Tabler Icons 적용 + 지역 필터 추가
- 홈 탭: 장르 가로 스크롤 카드 (Tabler Icons 픽토그램)
- 홈 탭: 가격/지역/내위치 필터 2줄 배치
- 리스트 탭: 기존 바텀시트 필터 UI 유지
- cuisine-icons: Tabler 아이콘 매핑 추가 (getTablerCuisineIcon)
- 드래그 스크롤 장르 카드에 적용
- 배포 가이드 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:52:42 +09:00

263 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasteby 배포 가이드
## 환경 요약
| 항목 | Dev (개발) | Prod (운영) |
|------|-----------|-------------|
| URL | dev.tasteby.net | www.tasteby.net |
| 호스트 | 로컬 Mac mini | OKE (Oracle Kubernetes Engine) |
| 프로세스 관리 | PM2 | Kubernetes Deployment |
| 프론트엔드 실행 | `npm run dev` (Next.js dev server) | `node server.js` (standalone 빌드) |
| 백엔드 실행 | `./gradlew bootRun` | `java -jar app.jar` (bootJar 빌드) |
| Redis | 로컬 Redis 서버 | K8s Pod (redis:7-alpine) |
| TLS | Nginx(192.168.0.147) + Certbot | cert-manager + Let's Encrypt |
| 리버스 프록시 | Nginx (192.168.0.147 → 192.168.0.208) | Nginx Ingress Controller (K8s) |
| 도메인 DNS | dev.tasteby.net → Mac mini IP | www.tasteby.net → OCI NLB 217.142.131.194 |
---
## 1. Dev 환경 (dev.tasteby.net)
### 구조
```
브라우저 → dev.tasteby.net (HTTPS)
Nginx (192.168.0.147) — Certbot Let's Encrypt TLS
├── /api/* → proxy_pass http://192.168.0.208:8000 (tasteby-api)
└── /* → proxy_pass http://192.168.0.208:3001 (tasteby-web)
Mac mini (192.168.0.208) — PM2 프로세스 매니저
├── tasteby-api → ./gradlew bootRun (:8000)
└── tasteby-web → npm run dev (:3001)
```
- **192.168.0.147**: Nginx 리버스 프록시 서버 (TLS 종료, Certbot 자동 갱신)
- **192.168.0.208**: Mac mini (실제 앱 서버, PM2 관리)
### PM2 프로세스 구성 (ecosystem.config.js)
```javascript
module.exports = {
apps: [
{
name: "tasteby-api",
cwd: "/Users/joungmin/workspaces/tasteby/backend-java",
script: "./start.sh", // gradlew bootRun 실행
interpreter: "/bin/bash",
},
{
name: "tasteby-web",
cwd: "/Users/joungmin/workspaces/tasteby/frontend",
script: "npm",
args: "run dev", // ⚠️ 절대 standalone으로 바꾸지 말 것!
},
],
};
```
### 백엔드 start.sh
```bash
#!/bin/bash
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
set -a
source /Users/joungmin/workspaces/tasteby/backend/.env # 환경변수 로드
set +a
exec ./gradlew bootRun
```
### 코드 수정 후 반영 방법
```bash
# 프론트엔드: npm run dev라서 코드 수정 시 자동 Hot Reload (재시작 불필요)
# 백엔드: 코드 수정 후 재시작 필요
pm2 restart tasteby-api
# 전체 재시작
pm2 restart tasteby-api tasteby-web
# PM2 상태 확인
pm2 status
# 로그 확인
pm2 logs tasteby-api --lines 50
pm2 logs tasteby-web --lines 50
```
### 주의사항
- `tasteby-web`은 반드시 `npm run dev`로 실행 (dev server)
- standalone 모드(`node .next/standalone/server.js`)로 바꾸면 static/public 파일을 못 찾아서 404 발생
- standalone은 prod(Docker/K8s) 전용
- dev 포트: 프론트 3001, 백엔드 8000 (3000은 Gitea가 사용 중)
- 환경변수는 `backend/.env`에서 로드
---
## 2. Prod 환경 (www.tasteby.net)
### 구조
```
브라우저 → www.tasteby.net (HTTPS)
OCI Network Load Balancer (217.142.131.194)
↓ 80→NodePort:32530, 443→NodePort:31437
Nginx Ingress Controller (K8s)
├── /api/* → backend Service (:8000)
└── /* → frontend Service (:3001)
```
### 클러스터 정보
- **OKE 클러스터**: tasteby-cluster-prod
- **노드**: ARM64 × 2 (2 CPU / 8GB)
- **네임스페이스**: tasteby
- **K8s context**: `context-c6ap7ecrdeq`
### Pod 구성
| Pod | Image | Port | 리소스 |
|-----|-------|------|--------|
| backend | `icn.ocir.io/idyhsdamac8c/tasteby/backend:TAG` | 8000 | 500m~1 CPU, 768Mi~1536Mi |
| frontend | `icn.ocir.io/idyhsdamac8c/tasteby/frontend:TAG` | 3001 | 200m~500m CPU, 256Mi~512Mi |
| redis | `docker.io/library/redis:7-alpine` | 6379 | 100m~200m CPU, 128Mi~256Mi |
### 배포 명령어 (deploy.sh)
```bash
# 전체 배포 (백엔드 + 프론트엔드)
./deploy.sh "배포 메시지"
# 백엔드만 배포
./deploy.sh --backend-only "백엔드 수정 사항"
# 프론트엔드만 배포
./deploy.sh --frontend-only "프론트 수정 사항"
# 드라이런 (실제 배포 없이 확인)
./deploy.sh --dry-run "테스트"
```
### deploy.sh 동작 순서
1. **버전 계산**: 최신 git tag에서 patch +1 (v0.1.9 → v0.1.10)
2. **Docker 빌드**: Colima로 `linux/arm64` 이미지 빌드 (로컬 Mac에서)
- 백엔드: `backend-java/Dockerfile` → multi-stage (JDK build → JRE runtime)
- 프론트: `frontend/Dockerfile` → multi-stage (node build → standalone runtime)
3. **OCIR Push**: `icn.ocir.io/idyhsdamac8c/tasteby/{backend,frontend}:TAG` + `:latest`
4. **K8s 배포**: `kubectl set image``kubectl rollout status` (롤링 업데이트)
5. **Git tag**: `vX.Y.Z` 태그 생성 후 origin push
### Docker 빌드 상세
**백엔드 Dockerfile** (multi-stage):
```dockerfile
# Build: eclipse-temurin:21-jdk에서 gradlew bootJar
# Runtime: eclipse-temurin:21-jre에서 java -jar app.jar
# JVM 옵션: -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC
```
**프론트엔드 Dockerfile** (multi-stage):
```dockerfile
# Build: node:22-alpine에서 npm ci + npm run build
# Runtime: node:22-alpine에서 standalone 출력물 복사 + node server.js
# ⚠️ standalone 모드는 Docker(prod) 전용. .next/static과 public을 직접 복사해야 함
```
### Ingress 설정
```yaml
# 주요 annotation
cert-manager.io/cluster-issuer: letsencrypt-prod # 자동 TLS 인증서
nginx.ingress.kubernetes.io/ssl-redirect: "true" # HTTP → HTTPS 리다이렉트
nginx.ingress.kubernetes.io/from-to-www-redirect: "true" # tasteby.net → www 리다이렉트
# 라우팅
www.tasteby.net/api/* → backend:8000
www.tasteby.net/* → frontend:3001
```
### TLS 인증서 (cert-manager)
- ClusterIssuer: `letsencrypt-prod`
- HTTP-01 challenge 방식 (포트 80 필수)
- Secret: `tasteby-tls`
- 인증서 상태 확인: `kubectl get certificate -n tasteby`
### 운영 확인 명령어
```bash
# Pod 상태
kubectl get pods -n tasteby
# 로그 확인
kubectl logs -f deployment/backend -n tasteby
kubectl logs -f deployment/frontend -n tasteby
# 인증서 상태
kubectl get certificate -n tasteby
# Ingress 상태
kubectl get ingress -n tasteby
# 롤백 (이전 이미지로)
kubectl rollout undo deployment/backend -n tasteby
kubectl rollout undo deployment/frontend -n tasteby
```
---
## 3. OCI 네트워크 구성
### VCN 서브넷
| 서브넷 | CIDR | 용도 |
|--------|------|------|
| oke-k8sApiEndpoint-subnet | 10.0.0.0/28 | K8s API 서버 |
| oke-nodesubnet | 10.0.10.0/24 | 워커 노드 |
| oke-svclbsubnet | 10.0.20.0/24 | NLB (로드밸런서) |
### 보안 리스트 (Security List)
**LB 서브넷** (oke-svclbsubnet):
- Ingress: `0.0.0.0/0` → TCP 80, 443
- Egress: `10.0.10.0/24` → all (노드 서브넷 전체 허용)
**노드 서브넷** (oke-nodesubnet):
- Ingress: `10.0.10.0/24` → all (노드 간 통신)
- Ingress: `10.0.0.0/28` → TCP all (API 서버)
- Ingress: `0.0.0.0/0` → TCP 22 (SSH)
- Ingress: `10.0.20.0/24` → TCP 30000-32767 (LB → NodePort)
- Ingress: `0.0.0.0/0` → TCP 30000-32767 (NLB preserve-source 대응)
> ⚠️ NLB `is-preserve-source: true` 설정으로 클라이언트 원본 IP가 보존됨.
> 따라서 노드 서브넷에 `0.0.0.0/0` → NodePort 인바운드가 반드시 필요.
---
## 4. OCIR (컨테이너 레지스트리) 인증
```bash
# 로그인
docker login icn.ocir.io -u idyhsdamac8c/oracleidentitycloudservice/<email> -p <auth-token>
```
- Registry: `icn.ocir.io/idyhsdamac8c/tasteby/`
- K8s imagePullSecret: `ocir-secret` (namespace: tasteby)
---
## 5. 자주 하는 실수 / 주의사항
| 실수 | 원인 | 해결 |
|------|------|------|
| dev에서 static 404 | PM2를 standalone 모드로 바꿈 | `npm run dev`로 원복 |
| prod HTTPS 타임아웃 | NLB 보안 리스트 NodePort 불일치 | egress를 노드 서브넷 all 허용 |
| 인증서 발급 실패 | 포트 80 방화벽 차단 | LB 서브넷 ingress 80 + 노드 서브넷 NodePort 허용 |
| OKE에서 이미지 pull 실패 | CRI-O short name 불가 | `docker.io/library/` 풀네임 사용 |
| NLB 헬스체크 실패 | preserve-source + 노드 보안 리스트 | 0.0.0.0/0 → NodePort 인바운드 추가 |