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

15 KiB

배포 가이드 (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 생성:

# 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 생성:

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:

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 생성:

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 선택 후 아래 내용 입력:

    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에 복사 후 빌드:

# 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)

# 로컬 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:

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 라우팅 설정

cloudflared tunnel route dns sr-system-tunnel api.sr.example.com

5.4 TrueNAS에 cloudflared 컨테이너 배포

방법 A: Docker Compose에 포함

docker/infra/docker-compose.yml에 cloudflared 추가:

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 발급

cloudflared tunnel token sr-system-tunnel > tunnel-token.txt
# 출력된 token을 CLOUDFLARE_TUNNEL_TOKEN 환경변수로 등록

5.6 동작 확인

# 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 설정

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:

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:

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:

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 헬스체크

# 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 로그 확인

# 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 (수동):

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 추가:

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 연결 안 됨

# cloudflared 로그 확인
docker logs sr-tunnel

# DNS 확인
cloudflared tunnel info sr-system-tunnel

# 재시작
docker restart sr-tunnel

Spring Boot 컨테이너 Crash

# 로그 확인
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) 확인