644 lines
15 KiB
Markdown
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)** 확인
|