Files
SR-SYSTEM/docs/08-deployment-guide.md
2026-06-02 13:50:39 +09:00

644 lines
15 KiB
Markdown

# 배포 가이드 (Deployment Guide)
## 1. 전체 아키텍처
### 시스템 구성도
```
[User Browser]
│
├── https://sr.example.com ──────────────────────┐
│ │
▼ ▼
Cloudflare DNS ─── Cloudflare Pages ──── Cloudflare Tunnel
(도메인 관리) (Next.js 정적 호스팅) (outbound 전용)
│
│ http://localhost:8080
▼
TrueNAS Scale
├── Docker: Spring Boot (:8080)
├── Docker: PostgreSQL (:5432)
└── Dataset: 첨부파일 저장소
```
### 데이터 흐름
```
1. User → https://sr.example.com → Cloudflare Pages (Next.js SPA)
2. SPA → GET https://api.sr.example.com/v1/tickets
│
3. Cloudflare DNS → Cloudflare Tunnel (443) → cloudflared 컨테이너
4. cloudflared → http://spring-boot:8080/api/v1/tickets
5. Spring Boot → PostgreSQL (:5432) / Dataset (/data/attachments)
```
### 포트 오픈 현황
| 방향 | 프로토콜 | 포트 | 설명 |
|------|---------|------|------|
| **아웃바운드** | TCP | 443 | cloudflared → Cloudflare Edge |
| 인바운드 | **없음** | - | 포트포워딩 불필요 |
TrueNAS에 **열린 인바운드 포트가 0개**. cloudflared가 아웃바운드 연결만 생성하여 Cloudflare와 통신.
---
## 2. 사전 준비
### 2.1 Cloudflare
- Cloudflare 계정 (https://dash.cloudflare.com)
- 도메인 Cloudflare DNS 위임 완료
- Cloudflare Pages 가입 (무료 티어 가능)
### 2.2 TrueNAS Scale
- TrueNAS Scale 24.04+ (Linux 기반)
- Apps 기능 활성화: **Apps → Settings → Choose Pool** (앱 전용 Dataset 선택)
- Docker 네트워크 확인: `docker network ls`
### 2.3 GitHub
- Frontend Repository (`sr-frontend`)
- Backend Repository (`sr-backend`)
- GitHub Secrets 등록 준비
---
## 3. 1단계: Backend Docker화
### 3.1 Dockerfile
Spring Boot 프로젝트 루트에 `Dockerfile` 생성:
```dockerfile
# Build stage
FROM gradle:8.7-jdk17 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src
RUN gradle build -x test --no-daemon
# Runtime stage
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
```
### 3.2 application-prod.yml
`src/main/resources/application-prod.yml` 생성:
```yaml
spring:
datasource:
url: jdbc:postgresql://postgres:5432/sr_system
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
app:
file:
upload-dir: /data/attachments
jwt:
secret: ${JWT_SECRET}
server:
port: 8080
logging:
level:
com.hq.support.portal: INFO
```
### 3.3 docker-compose.yml
최상위 `docker/infra/docker-compose.yml`:
```yaml
version: "3.8"
services:
postgres:
image: postgres:16
container_name: sr-postgres
restart: unless-stopped
environment:
POSTGRES_DB: sr_system
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- /mnt/apps/sr-system/postgres:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d sr_system"]
interval: 10s
timeout: 5s
retries: 5
spring-boot:
build:
context: .
dockerfile: Dockerfile
container_name: sr-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: prod
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
volumes:
- /mnt/apps/sr-system/attachments:/data/attachments
ports:
- "127.0.0.1:8080:8080"
```
> **중요:** `ports`가 `127.0.0.1:8080:8080`으로 설정됨. 외부 직접 접근 불가, cloudflared를 통해서만 접근 가능.
---
## 4. 2단계: TrueNAS Scale 배포
### 4.1 Dataset 생성
TrueNAS GUI → Storage → Create Dataset:
| 항목 | 경로 | 용도 |
|------|------|------|
| App Config | `/mnt/apps/sr-system` | docker-compose, 환경변수 |
| PostgreSQL Data | `/mnt/apps/sr-system/postgres` | DB 데이터 |
| Attachments | `/mnt/apps/sr-system/attachments` | 첨부파일 저장소 |
### 4.2 환경변수 파일
`/mnt/apps/sr-system/.env` 생성:
```bash
DB_USERNAME=sr_admin
DB_PASSWORD=<복잡한_비밀번호>
JWT_SECRET=<openssl rand -base64 64>
```
### 4.3 Custom App 등록
TrueNAS GUI → Apps → Launch Docker Image 대신 **Custom App** 사용:
1. **App Name:** `sr-system`
2. **Image:** (docker-compose 방식 사용)
3. **Add Container → docker-compose:**
TrueNAS Apps → Discover Apps → Custom App 선택 후 아래 내용 입력:
```yaml
services:
postgres:
image: postgres:16
container_name: sr-postgres
restart: unless-stopped
env_file: /mnt/apps/sr-system/.env
volumes:
- /mnt/apps/sr-system/postgres:/var/lib/postgresql/data
networks:
- sr-network
spring-boot:
image: sr-backend:latest
container_name: sr-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file: /mnt/apps/sr-system/.env
environment:
SPRING_PROFILES_ACTIVE: prod
volumes:
- /mnt/apps/sr-system/attachments:/data/attachments
networks:
- sr-network
networks:
sr-network:
driver: bridge
```
> **참고:** docker-compose 방식 대신 TrueNAS Apps GUI의 **Custom App → Container Images**에서 각 이미지를 개별 등록하고 `Network`를 동일한 bridge 네트워크로 연결해도 됨.
### 4.4 수동 빌드 (초기)
백엔드 코드를 TrueNAS에 복사 후 빌드:
```bash
# TrueNAS SSH 접속
cd /mnt/apps/sr-system
git clone https://github.com/your-org/sr-backend.git
cd sr-backend
docker build -t sr-backend:latest .
docker compose up -d
```
---
## 5. 3단계: Cloudflare Tunnel 설치
### 5.1 Cloudflare Tunnel 생성 (로컬 CLI)
```bash
# 로컬 PC에서 cloudflared 설치
brew install cloudflare/cloudflare/cloudflared
# Cloudflare 로그인
cloudflared tunnel login
# Tunnel 생성
cloudflared tunnel create sr-system-tunnel
# 출력 예시: Tunnel ID: abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 인증 파일: ~/.cloudflared/abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
```
### 5.2 Config 파일 생성
`~/.cloudflared/config.yml`:
```yaml
tunnel: abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /home/.cloudflared/abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress:
- hostname: api.sr.example.com
service: http://localhost:8080
- service: http_status:404
```
### 5.3 DNS 라우팅 설정
```bash
cloudflared tunnel route dns sr-system-tunnel api.sr.example.com
```
### 5.4 TrueNAS에 cloudflared 컨테이너 배포
**방법 A: Docker Compose에 포함**
`docker/infra/docker-compose.yml`에 cloudflared 추가:
```yaml
services:
# ... postgres, spring-boot ...
cloudflared:
image: cloudflare/cloudflared:latest
container_name: sr-tunnel
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- sr-network
```
**방법 B: TrueNAS Apps에서 개별 배포**
1. TrueNAS → Apps → Discover Apps → Custom App
2. **App Name:** `sr-tunnel`
3. **Image:** `cloudflare/cloudflared:latest`
4. **Entrypoint:** `tunnel --no-autoupdate run`
5. **Environment Variables:**
- `TUNNEL_TOKEN`: `<Tunnel Token>`
### 5.5 Tunnel Token 발급
```bash
cloudflared tunnel token sr-system-tunnel > tunnel-token.txt
# 출력된 token을 CLOUDFLARE_TUNNEL_TOKEN 환경변수로 등록
```
### 5.6 동작 확인
```bash
# Tunnel 로그 확인
docker logs sr-tunnel
# 정상 출력 예시:
# 2026/05/21 10:00:00 INF Connection abc12345 registered
# 2026/05/21 10:00:00 INF Route created hostname=api.sr.example.com
# 2026/05/21 10:00:00 INF tunnelId=abc12345
# Spring Boot 헬스체크
curl https://api.sr.example.com/api/v1/actuator/health
# {"status":"UP"}
```
---
## 6. 4단계: Frontend Cloudflare Pages 배포
### 6.1 next.config.ts 설정
```typescript
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export", // 정적 사이트 내보내기
images: {
unoptimized: true, // 정적 export 시 필수
},
trailingSlash: true,
distDir: "out",
};
export default nextConfig;
```
> **참고:** API Routes나 Server Components가 필요하면 `output: 'export'` 대신 `@cloudflare/next-on-pages` 사용.
### 6.2 환경변수
`.env.production`:
```bash
NEXT_PUBLIC_API_URL=https://api.sr.example.com/api/v1
```
### 6.3 Cloudflare Pages 연결
1. Cloudflare Dashboard → **Workers & Pages → Pages**
2. **Connect to Git** → GitHub 저장소 선택
3. **Build settings:**
- Framework preset: **Next.js**
- Build command: `npm run build && npm run export`
- Build output: `out`
4. **Environment Variables (Advanced):**
- `NEXT_PUBLIC_API_URL`: `https://api.sr.example.com/api/v1`
5. **Deploy**
### 6.4 Custom Domain 설정
Deploy 완료 후:
1. Pages 프로젝트 → **Custom domains** → **Set up custom domain**
2. `sr.example.com` 입력
3. Cloudflare가 자동으로 DNS 레코드 생성 (Proxy ON = 주황 구름)
---
## 7. 5단계: CI/CD
### 7.1 Backend (GitHub Actions)
`.github/workflows/deploy-backend.yml`:
```yaml
name: Deploy Backend
on:
push:
branches: [main]
paths:
- "backend/**"
- "docker/**"
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"
- name: Build with Gradle
run: |
cd backend
./gradlew build -x test
- name: Build Docker Image
run: |
cd backend
docker build -t ghcr.io/${{ github.repository }}/sr-backend:latest .
docker tag ghcr.io/${{ github.repository }}/sr-backend:latest \
ghcr.io/${{ github.repository }}/sr-backend:${{ github.sha }}
- name: Push to GitHub Container Registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}/sr-backend --all-tags
- name: Deploy to TrueNAS
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.TRUENAS_HOST }}
username: ${{ secrets.TRUENAS_SSH_USER }}
key: ${{ secrets.TRUENAS_SSH_KEY }}
script: |
cd /mnt/apps/sr-system
docker compose pull spring-boot
docker compose up -d spring-boot
docker image prune -f
```
### 7.2 Frontend (GitHub Actions)
`.github/workflows/deploy-frontend.yml`:
```yaml
name: Deploy Frontend
on:
push:
branches: [main]
paths:
- "frontend/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install & Build
run: |
cd frontend
npm ci
npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: sr-frontend
directory: frontend/out
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
```
### 7.3 GitHub Secrets 등록
| Secret | 값 |
|--------|-----|
| `TRUENAS_HOST` | TrueNAS IP 주소 (내부망) |
| `TRUENAS_SSH_USER` | SSH 사용자 |
| `TRUENAS_SSH_KEY` | SSH Private Key |
| `CLOUDFLARE_API_TOKEN` | Cloudflare API Token (Pages 권한) |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare Account ID |
| `TUNNEL_TOKEN` | Cloudflare Tunnel Token |
---
## 8. 보안 설정
### 8.1 Cloudflare WAF 규칙
Cloudflare Dashboard → Security → WAF:
**규칙 1: API 경로만 허용**
```
Field: Hostname
Operator: equals
Value: api.sr.example.com
Then:
URI Path starts with /api/v1 OR /actuator/health
→ Allow
Else → Block
```
**규칙 2: Rate Limiting**
```
When: requests match api.sr.example.com
Rate: 1000 requests per 5 minutes
Action: Block
```
### 8.2 Cloudflare Access (선택 - VPN 대체)
Zero Trust → Access → Applications:
- **App Domain:** `api.sr.example.com`
- **Policy:** 특정 이메일/Google/OAuth 로그인 필수
- **Session Duration:** 24h
> Cloudflare Access를 활성화하면 API도 인증된 사용자만 접근 가능. 단, 프론트엔드 SPA에서 API 호출 시 Access Token 처리 로직이 추가로 필요.
### 8.3 SSL 설정
Cloudflare Dashboard → SSL/TLS:
- **Full (Strict)** 선택
- TrueNAS 측에서는 별도 SSL 인증서 불필요 (cloudflared가 종단 암호화)
### 8.4 DB 접근 제한
PostgreSQL이 `127.0.0.1:5432`에서만 리스닝. 외부 접근 차단.
Docker 네트워크 내에서만 `postgres:5432`로 접근 가능.
---
## 9. 운영
### 9.1 헬스체크
```bash
# Spring Boot Actuator
curl https://api.sr.example.com/api/v1/actuator/health
# DB 연결 확인
curl https://api.sr.example.com/api/v1/actuator/health/db
```
### 9.2 로그 확인
```bash
# TrueNAS SSH 접속 후
docker logs sr-backend # Spring Boot 로그
docker logs sr-postgres # PostgreSQL 로그
docker logs sr-tunnel # Tunnel 로그
docker compose logs -f # 실시간 로그 스트림
```
### 9.3 DB 백업
**ZFS Snapshot (자동):**
TrueNAS GUI → Storage → `/mnt/apps/sr-system/postgres` → Snapshot:
- Schedule: Daily at 03:00
- Retention: 30일
**pg_dump (수동):**
```bash
docker exec sr-postgres pg_dump -U sr_admin sr_system > \
/mnt/apps/sr-system/backups/sr_system_$(date +%Y%m%d).sql
```
### 9.4 Watchtower (자동 업데이트)
docker-compose에 Watchtower 추가:
```yaml
services:
watchtower:
image: containrrr/watchtower
container_name: sr-watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 3600 --cleanup
```
매시간 이미지 체크 → 새 이미지 있으면 자동 재배포.
---
## 10. 문제 해결
### Tunnel 연결 안 됨
```bash
# cloudflared 로그 확인
docker logs sr-tunnel
# DNS 확인
cloudflared tunnel info sr-system-tunnel
# 재시작
docker restart sr-tunnel
```
### Spring Boot 컨테이너 Crash
```bash
# 로그 확인
docker logs sr-backend --tail 50
# DB 연결 테스트
docker exec sr-postgres pg_isready -U sr_admin -d sr_system
# 환경변수 확인
docker exec sr-backend env | grep -E "DB_|JWT_|SPRING_"
```
### Cloudflare Pages 502 Error
- Cloudflare Tunnel이 실행 중인지 확인
- `api.sr.example.com` DNS가 Tunnel UUID를 가리키는지 확인
- Spring Boot가 정상 기동했는지 확인 (`http://localhost:8080/actuator/health`)
- Cloudflare Dashboard → SSL/TLS → **Full (Strict)** 확인