M3 ‐ Docker Compose 기반 CI CD 파이프라인 구성 - 100-hours-a-week/KTB4-4th-wiki GitHub Wiki

목차

1. CI/CD 설계 목표

Need U V1의 CI/CD는 Frontend, Backend, AI Server 세 Application을 반복 가능하게 검증하고 배포하는 것을 목표로 한다.

User
  │ HTTPS
  ▼
Nginx
├─ /      → Frontend (Next.js) :3000
└─ /api   → Backend :8080
                │
                ├─ MySQL
                │
                ▼
          AI Server (FastAPI)
                ├─ Redis
                ├─ PostgreSQL + pgvector
                ├─ External LLM API
                └─ Embedding API

EC2 내부 구조:

EC2 t3.medium
├─ Nginx (Host)
├─ Docker Compose
│  ├─ Frontend (Next.js :3000)
│  ├─ Backend (:8080)
│  ├─ AI Server (:8000)
│  └─ Redis (:6379)
├─ MySQL (Host Process)
└─ PostgreSQL + pgvector (Host Process)

전체 흐름:

코드 변경
↓
CI
↓
검증된 Production Artifact 생성
├─ Frontend → Docker Image → ECR
├─ Backend → Docker Image → ECR
└─ AI Server → Docker Image → ECR
↓
Production 대기
↓
담당자가 workflow_dispatch 실행
↓
AWS OIDC
↓
SSM
↓
EC2 반영
↓
Health / Smoke Check
↓
실패 시 이전 Version 복구

1.1 CI와 CD 역할

구분 CI CD
목적 Code 검증 및 Production Artifact 생성 검증된 Artifact를 Production에 반영
Trigger PR / main Push workflow_dispatch
Backend Test + Docker Image → ECR Backend Container 교체
AI Server Unit Test + Lint + Docker Image → ECR AI Container 교체
Frontend Type/Lint/Build/E2E + Docker Image → ECR Frontend Container 교체
EC2 접근 없음 AWS SSM
실패 Merge 차단 / Artifact 생성 중단 Rollback

1.2 Build와 Deploy 분리

Production 배포 시 Source Code를 다시 Build하지 않는다.

CI
→ Production Artifact 생성

CD
→ Artifact 선택
→ Production 반영

PR의 Docker Build Check는 Production Artifact가 아니라 Dockerfile이 정상적으로 Build되는지 확인하기 위한 검증 Build다.

실제 Production Artifact는 main Push CI에서 생성하고 이후 CD에서는 다시 Build하지 않는다.

2. V1 배포 대상과 Repository 구조

Need U V1은 세 Repository를 기준으로 운영한다.

2.1 Backend Repository

Backend Repository
├─ Dockerfile
├─ .dockerignore
├─ build.gradle
├─ settings.gradle
├─ gradlew
├─ gradle/
├─ src/
└─ .github/workflows/
   ├─ ci.yml
   └─ cd.yml

Production Artifact:

needu-backend:<Git Commit SHA>

ECR Repository: needu-backend

2.2 AI Repository

AI Repository
├─ Dockerfile
├─ .dockerignore
├─ pyproject.toml
├─ uv.lock
├─ Application Source
└─ .github/workflows/
   ├─ ci.yml
   └─ cd.yml
항목 V1 값
Project needu-ai 0.1.0
Python 3.12.11
Package Manager uv
uv 0.8.4
Framework FastAPI
Runtime Uvicorn
Production Image needu-ai:<SHA>
Registry Amazon ECR Private

2.3 Frontend Repository

Frontend Repository
├─ Dockerfile
├─ .dockerignore
├─ next.config.*
├─ app/
├─ public/
├─ package.json
├─ package-lock.json
└─ .github/workflows/
   ├─ ci.yml
   └─ cd.yml

현재 Frontend 실행 환경:

항목 값
Framework Next.js 16.3.5
Router App Router
Rendering CSR + Server Component + 향후 SSR / 요청 기반 Server 기능
React 19.2.8
TypeScript 5.x
Node.js 24.10.0 이상
현재 Local Node.js 24.19.0
Package Manager npm
현재 Local npm 11.17.0
Lock File package-lock.json
Production Build next build
Docker Runtime output: "standalone"
Runtime Command node server.js
Application Port 3000
Production Image needu-frontend:<SHA>
Registry Amazon ECR Private

Frontend는 output: "export" 기반 정적 배포를 사용하지 않는다.

Production에서는 Next.js Server를 Docker Container로 실행하며 Nginx가 / 요청을 localhost:3000으로 Reverse Proxy한다.

User
↓
Nginx :443
↓
Frontend Container :3000
↓
Next.js Server

3. Branch 및 Workflow Trigger

세 Repository 모두 동일한 Branch 전략을 사용한다.

feature/*
↓
PR
↓
dev
↓
PR
↓
main

dev는 통합 개발 Branch이고 main은 Production Release Candidate Branch다.

3.1 CI Trigger

feature/* → dev PR
dev → main PR
main Push

긴급 수정:

main
↓
hotfix/*
↓
PR
↓
main

Hotfix 반영 후 동일 수정사항을 dev에도 반영한다.

3.2 CD Trigger

Production CD는 main Merge만으로 자동 실행하지 않는다.

on:
  workflow_dispatch:
main Merge
≠
Production Deploy

기본 운영 기준:

주 2회
새벽 04:00
20~23시 계획 배포 지양
Critical Bug는 예외 배포

4. CI 공통 원칙

4.1 PR 단계

feature → dev
→ 빠른 기본 검증

dev → main
→ Release Candidate 검증

4.2 main 단계

main Push
↓
최종 CI
↓
Version = Git Commit SHA
↓
Production Artifact 생성
↓
CD 대기

4.3 외부 서비스

일반 CI에서는 실제 외부 LLM / Embedding API 호출에 의존하지 않는다.

  • API 비용 발생
  • 외부 장애로 CI 실패 가능
  • 응답 비결정성
  • Rate Limit
  • Secret 범위 증가

따라서 외부 LLM / Embedding / Kakao API는 필요한 범위에서 Mock / Fixture / Contract Test로 검증한다.

5. Backend CI

항목 값
Framework Spring Boot 4.1.x
Java 25 LTS
JDK Eclipse Temurin
Build Gradle Wrapper
Port 8080
Health /actuator/health
Docker Image needu-backend
Production Tag Git Commit SHA

Backend는 V1에서 다음 책임을 가진다.

Backend
├─ 인증
├─ 사용자 / 친구
├─ 일반 Service Business Logic
├─ MySQL
└─ AI Server API 호출

Backend Release CI에서는 다음 항목을 검증한다.

  • MySQL 연결 및 주요 Repository 동작
  • Backend ↔ AI Server Request / Response Contract

Backend의 AI 연동 검증에서는 AI Server 내부 Logic을 다시 테스트하지 않고, Backend가 AI Server와 주고받는 Request / Response Contract를 중심으로 확인한다.

Backend는 외부 LLM / Embedding API와 PostgreSQL + pgvector를 직접 처리하지 않는다.

5.1 feature → dev PR

Checkout
↓
Java 25
↓
Gradle Cache
↓
Unit Test
↓
Build
↓
Merge 가능
  • ECR Push는 하지 않는다.

5.2 dev → main PR

기본 검증
↓
MySQL Integration Test
↓
AI API Contract / Mock Test
↓
Docker Build Check
↓
main Merge 가능

AI 기능 연동은 Backend ↔ AI Server Request / Response Contract를 검증한다.

5.3 main Push

main Push
↓
Unit Test / Build
↓
Docker Image Build
↓
Git Commit SHA Tag
↓
AWS OIDC
↓
ECR Push
↓
CD 대기

예: needu-backend:a82f3c1

5.4 Backend CI Workflow 예시

name: Backend CI

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [main]

permissions:
  contents: read

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: needu-backend

jobs:
  basic-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "25"
          cache: gradle
      - run: chmod +x gradlew
      - name: Unit Test
        run: ./gradlew test --no-daemon
      - name: Build
        run: ./gradlew build --no-daemon

  release-check:
    needs: basic-check
    if: github.event_name == 'pull_request' && github.base_ref == 'main'
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.4
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: needu_test
          MYSQL_USER: test
          MYSQL_PASSWORD: test
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h localhost -uroot -proot"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=10

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "25"
          cache: gradle
      - run: chmod +x gradlew
      - name: Integration Test
        run: ./gradlew integrationTest --no-daemon
      - name: Docker Build Check
        run: docker build -t needu-backend:ci-check .

  ecr-push:
    needs: basic-check
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.BACKEND_CI_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
      - id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: Build and Push
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

실제 Project에서 integrationTest, Static Check, Test Profile은 구현 상태에 맞춰 확정한다.

6. AI Server CI

AI Server는 별도 FastAPI Application이므로 독립적인 CI가 필요하다.

현재 기준:

Python 3.12.11
uv 0.8.4
pyproject.toml
uv.lock
FastAPI
Uvicorn

6.1 feature → dev PR

Checkout
↓
Python 3.12.11
↓
uv 0.8.4
↓
uv sync --frozen --dev
↓
Unit Test
↓
Ruff
↓
Merge 가능
uv run pytest tests/unit
uv run ruff check .

6.2 dev → main PR

fast-check
↓
Docker Build Check
↓
main Merge 가능

Redis / PostgreSQL + pgvector Integration Test는 Adapter와 통합 테스트가 실제 구현된 이후 추가한다.

6.3 main Push

fast-check
↓
Production Docker Image Build
↓
Git Commit SHA Tag
↓
ECR Push
↓
CD 대기

예: needu-ai:4e91bd2

6.4 AI CI Workflow

name: AI CI

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [main]

permissions:
  contents: read

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: needu-ai

jobs:
  fast-check:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12.11"

      - uses: astral-sh/setup-uv@v6
        with:
          version: "0.8.4"
          enable-cache: true

      - name: Install dependencies
        run: uv sync --frozen --dev

      - name: Unit Test
        run: uv run pytest tests/unit

      - name: Lint
        run: uv run ruff check .

  release-check:
    needs: fast-check
    if: github.event_name == 'pull_request' && github.base_ref == 'main'
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - name: Docker Build Check
        run: docker build -t needu-ai:ci-check .

  ecr-push:
    needs: fast-check
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    timeout-minutes: 20

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AI_CI_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and Push
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

일반 CI에서 실제 LLM / Embedding API를 필수 호출하지 않는다.

7. Frontend CI

Frontend는 Next.js App Router 기반이며 CSR, Server Component, 향후 SSR 및 요청 기반 Server 기능을 사용한다.

현재 CI 기준 실행 환경:

항목 값
Next.js 16.3.5
React 19.2.8
TypeScript 5.x
Node.js 24.10.0 이상
Package Manager npm
npm 11.17.0
Lock File package-lock.json
Production Build next build
Docker Runtime output: "standalone"
Runtime Command node server.js
Port 3000
Docker Image needu-frontend
Production Tag Git Commit SHA

7.1 feature → dev PR

개별 Frontend 기능이 dev에 통합될 수 있는 상태인지 빠르게 검증한다.

Checkout
↓
Node.js 설정
↓
npm ci
↓
Type Check
↓
Lint / Format
↓
next build
↓
Merge 가능

이 단계에서는 ECR Push를 하지 않는다.

7.2 dev → main PR

dev에 통합된 Frontend가 Production 후보로 사용할 수 있는지 검증한다.

기본 검증
↓
Type Check
↓
Lint 
↓
next build
↓
Docker Build Check
↓
main Merge 가능

Docker Build Check에서는 M1에서 정의한 Frontend Dockerfile이 정상적으로 Build되고 .next/standalone 기반 Runtime Image를 생성할 수 있는지 확인한다.

PR 단계의 Docker Build는 Production Artifact가 아니며 ECR에 Push하지 않는다.

7.3 main Push

main에서는 실제 Production 배포에 사용할 Frontend Docker Image를 생성한다.

main Push
↓
Type Check / Lint
↓
next build
↓
Playwright E2E
↓
Docker Image Build
↓
Git Commit SHA Tag
↓
AWS OIDC
↓
ECR Push
↓
CD 대기

Production Image:

needu-frontend:<Git Commit SHA>

ECR Repository:

needu-frontend

7.4 Frontend CI Workflow 기준

name: Frontend CI

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [main]

permissions:
  contents: read

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: needu-frontend

jobs:
  check:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - name: Install Dependencies
        run: npm ci

      - name: Type Check
        run: npm run type-check

      - name: Lint
        run: npm run lint

      - name: Production Build
        run: npm run build

  docker-build-check:
    needs: check
    if: github.event_name == 'pull_request' && github.base_ref == 'main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Docker Build Check
        run: docker build -t needu-frontend:ci-check .

  e2e:
    needs: check
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - name: Install Dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Playwright E2E
        run: npx playwright test

  ecr-push:
    needs: [check, e2e]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.FRONTEND_CI_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and Push
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

npm run type-check, npm run lint Script 이름과 E2E 실행 방식은 실제 Frontend Repository의 package.json 및 Playwright 설정을 기준으로 확정한다.

8. E2E Test 및 Branch Protection

8.1 E2E Test 범위

카카오 로그인
↓
친구 조회
↓
AI 대화
↓
추천 결과
↓
외부 상품 이동

초기에는 구현된 흐름부터 점진적으로 확장한다.

8.2 Branch Protection

dev, main에 Required Status Check를 적용한다.

Backend main PR:

Unit / Build
+
MySQL Integration
+
AI API Contract / Mock
+
Docker Build Check

AI main PR:

Unit Test
+
Ruff
+
Docker Build Check

Frontend:

Type Check
+
Lint
+
Build

8.3 Redis가 CI/CD에 미치는 영향

Redis 자체는 AI Server의 Application Artifact와 함께 매번 새로 배포하는 대상이 아니다.

AI 배포
→ AI Container만 교체
→ Redis 유지

AI Server의 Redis Adapter / Integration Test가 실제 구현된 이후 Release CI에 Redis Service Test를 추가한다.

V1 Production Redis는 AI 대화 상태, 취향 분석 중간 상태 및 AI 관련 Cache를 저장하는 용도로 사용하며 Redis Container 자체의 재시작은 일반 Application 배포와 분리한다.

운영 Monitoring에서도 AI Server의 Redis 연결 상태를 확인할 수 있도록 구성한다.

9. Artifact 및 Version 관리

구분 Backend AI Server Frontend
Artifact Docker Image Docker Image Docker Image
저장소 Amazon ECR Amazon ECR Amazon ECR
Repository needu-backend needu-ai needu-frontend
Version Git SHA Git SHA Git SHA
CD 재Build X X X

예:

needu-backend:a82f3c1
needu-ai:4e91bd2
needu-frontend:93fbc21

세 Application 모두 main Push CI에서 Production Image를 Build하고 ECR에 Push한다.

CD에서는 해당 SHA Image를 선택하여 EC2에 반영하며 Source Code를 다시 Build하지 않는다.

10. GitHub Actions와 AWS 인증

장기 AWS Access Key를 GitHub Repository에 저장하지 않는다.

GitHub Actions
↓
OIDC
↓
AWS IAM Role
↓
AWS

CI Role:

BACKEND_CI_ROLE_ARN
AI_CI_ROLE_ARN

CD Role:

BACKEND_CD_ROLE_ARN
AI_CD_ROLE_ARN
FRONTEND_CD_ROLE_ARN

EC2 Role:

needu-ec2-role

주요 권한:

  • CI → ECR Push
  • CD → SSM SendCommand / 결과 조회
  • EC2 → ECR Pull / SSM Managed Instance

11. CD 공통 원칙

Production CD는 모두 수동 workflow_dispatch로 실행한다.

배포 단위:

Frontend
Backend
AI Server

Frontend, Backend, AI Server는 각각 독립적으로 Recreate한다.

Redis / MySQL / PostgreSQL / Nginx는 일반 Application 배포 시 재시작하지 않는다. Frontend 배포에서도 Frontend Container만 교체한다.

11.1 Health Check 방식

Backend / AI Runtime Image에는 Health Probe만을 위한 curl / wget을 추가하지 않는다.

배포 Script는 EC2 Host에서 HTTP Endpoint를 확인한다.

Backend
<http://localhost:8080/actuator/health>

AI
<http://localhost>:<AI_PORT>/health

12. Backend CD

workflow_dispatch
↓
Backend SHA 입력
↓
ECR Image 확인
↓
AWS OIDC
↓
SSM
↓
EC2
↓
deploy-backend.sh
↓
Host /actuator/health
↓
성공 / Rollback

배포 Script 기준:

#!/bin/bash
set -Eeuo pipefail

NEW_TAG="$1"
WORKDIR="/home/ec2-user/needu"
LAST_TAG_FILE="$WORKDIR/.last_deployed_backend_tag"
LOCK_FILE="$WORKDIR/.production-deploy.lock"

cd "$WORKDIR"

exec 9>"$LOCK_FILE"
flock -n 9 || {
  echo "Another production deployment is running"
  exit 1
}

PREV_TAG=$(cat "$LAST_TAG_FILE" 2>/dev/null || true)
export BACKEND_IMAGE_TAG="$NEW_TAG"

docker compose pull backend
docker compose up -d --no-deps backend

HEALTHY=false
for i in $(seq 1 12); do
  if curl --fail --silent <http://localhost:8080/actuator/health> >/dev/null; then
    HEALTHY=true
    break
  fi
  sleep 5
done

if [ "$HEALTHY" = true ]; then
  echo "$NEW_TAG" > "$LAST_TAG_FILE"
  exit 0
fi

if [ -z "$PREV_TAG" ]; then
  exit 1
fi

export BACKEND_IMAGE_TAG="$PREV_TAG"
docker compose pull backend
docker compose up -d --no-deps backend

for i in $(seq 1 12); do
  if curl --fail --silent <http://localhost:8080/actuator/health> >/dev/null; then
    echo "Backend rollback success"
    exit 1
  fi
  sleep 5
done

exit 1

curl은 Backend Container가 아니라 EC2 Host에서 사용한다.

13. AI Server CD

workflow_dispatch
↓
AI SHA 입력
↓
ECR needu-ai 확인
↓
AWS OIDC
↓
SSM
↓
EC2
↓
deploy-ai.sh
↓
AI /health
↓
성공 / Rollback

배포 Script 기준:

#!/bin/bash
set -Eeuo pipefail

NEW_TAG="$1"
WORKDIR="/home/ec2-user/needu"
LAST_TAG_FILE="$WORKDIR/.last_deployed_ai_tag"
LOCK_FILE="$WORKDIR/.production-deploy.lock"
AI_PORT="${AI_PORT:?AI_PORT is required}"

cd "$WORKDIR"

exec 9>"$LOCK_FILE"
flock -n 9 || {
  echo "Another production deployment is running"
  exit 1
}

PREV_TAG=$(cat "$LAST_TAG_FILE" 2>/dev/null || true)
export AI_IMAGE_TAG="$NEW_TAG"

docker compose pull ai-server
docker compose up -d --no-deps ai-server

HEALTHY=false
for i in $(seq 1 12); do
  if curl --fail --silent "<http://localhost>:${AI_PORT}/health" >/dev/null; then
    HEALTHY=true
    break
  fi
  sleep 5
done

if [ "$HEALTHY" = true ]; then
  echo "$NEW_TAG" > "$LAST_TAG_FILE"
  exit 0
fi

if [ -z "$PREV_TAG" ]; then
  exit 1
fi

export AI_IMAGE_TAG="$PREV_TAG"
docker compose pull ai-server
docker compose up -d --no-deps ai-server

for i in $(seq 1 12); do
  if curl --fail --silent "<http://localhost>:${AI_PORT}/health" >/dev/null; then
    echo "AI rollback success"
    exit 1
  fi
  sleep 5
done

exit 1

AI /health는 FastAPI Process 자체의 기동 확인용 가벼운 Endpoint로 두고 외부 LLM 일시 장애와 강하게 결합하지 않는다.

14. Frontend CD

Frontend는 Next.js Server를 Docker Container로 실행한다.

Production Runtime은 M1에서 정의한 output: "standalone" Image를 사용하며 Container 내부에서는 node server.js를 실행한다.

workflow_dispatch
↓
Frontend SHA 입력
↓
ECR needu-frontend Image 확인
↓
AWS OIDC
↓
SSM
↓
EC2
↓
deploy-frontend.sh
↓
docker compose pull frontend
↓
docker compose up -d --no-deps frontend
↓
localhost:3000 Smoke Check
↓
성공 / Rollback

14.1 Production 요청 흐름

User
 │ HTTPS
 ▼
Nginx :443
 │
 ▼
Frontend Container :3000
 │
 ▼
Next.js Server

Nginx는 / 요청을 localhost:3000으로 Reverse Proxy한다.

Backend API 요청은 /api Routing을 통해 Backend :8080으로 전달한다.

14.2 Frontend Image

Production Image:

needu-frontend:<Git Commit SHA>

Registry:

Amazon ECR Private

CD에서는 Source Code를 다시 Build하지 않는다.

CI
→ Docker Image Build
→ ECR Push

CD
→ ECR Image Pull
→ Container Recreate

14.3 Frontend 배포 Script 기준

#!/bin/bash
set -Eeuo pipefail

NEW_TAG="$1"
WORKDIR="/home/ec2-user/needu"
LAST_TAG_FILE="$WORKDIR/.last_deployed_frontend_tag"
LOCK_FILE="$WORKDIR/.production-deploy.lock"

cd "$WORKDIR"

exec 9>"$LOCK_FILE"
flock -n 9 || {
  echo "Another production deployment is running"
  exit 1
}

PREV_TAG=$(cat "$LAST_TAG_FILE" 2>/dev/null || true)
export FRONTEND_IMAGE_TAG="$NEW_TAG"

docker compose pull frontend
docker compose up -d --no-deps frontend

HEALTHY=false
for i in $(seq 1 12); do
  if curl --fail --silent <http://localhost:3000/> >/dev/null; then
    HEALTHY=true
    break
  fi
  sleep 5
done

if [ "$HEALTHY" = true ]; then
  echo "$NEW_TAG" > "$LAST_TAG_FILE"
  exit 0
fi

if [ -z "$PREV_TAG" ]; then
  exit 1
fi

export FRONTEND_IMAGE_TAG="$PREV_TAG"
docker compose pull frontend
docker compose up -d --no-deps frontend

for i in $(seq 1 12); do
  if curl --fail --silent <http://localhost:3000/> >/dev/null; then
    echo "Frontend rollback success"
    exit 1
  fi
  sleep 5
done

exit 1

curl은 Frontend Container 내부가 아니라 EC2 Host에서 실행한다.

14.4 Frontend 배포 성공 기준

지정한 ECR Image 존재
↓
Frontend Container Recreate 성공
↓
Next.js Server 기동
↓
localhost:3000 HTTP 응답 성공

Frontend Dockerfile 자체의 실행 명령은 다음과 같다.

CMD ["node", "server.js"]

CD에서 별도로 npm ci, next build, next start를 실행하지 않는다.

14.5 Nginx 설정

기본 Routing:

/
→ <http://localhost:3000>

/api
→ <http://localhost:8080>

일반 Frontend Code 배포에서는 Nginx 설정을 변경하지 않는다.

Nginx 설정 자체가 변경되는 경우에만:

nginx -t
sudo systemctl reload nginx

을 수행한다.

15. Production 환경변수 및 Secret

V1에서는 EC2 env file을 사용한다.

/home/ec2-user/needu/
├─ compose.env
├─ frontend.env
├─ backend.env
└─ ai.env

권한:

chmod 600 /home/ec2-user/needu/*.env

Frontend 예:

NODE_ENV=production
PORT=3000
HOSTNAME=0.0.0.0

실제 Frontend Runtime 환경변수는 Frontend 구현 기준에 맞춰 frontend.env에 추가한다.

NEXT_PUBLIC_* 값은 Client Bundle에 포함될 수 있으므로 Secret을 저장하지 않는다.

Backend 예:

SPRING_PROFILES_ACTIVE=prod

DB_NAME=
DB_USERNAME=
DB_PASSWORD=

KAKAO_CLIENT_ID=
KAKAO_CLIENT_SECRET=

AI_SERVER_URL=http://localhost:<AI_PORT>
JWT_SECRET=

AI Server 예:

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
AI_DB_NAME=
AI_DB_USERNAME=
AI_DB_PASSWORD=

REDIS_HOST=localhost
REDIS_PORT=6379

LLM_API_KEY=
EMBEDDING_API_KEY=
Backend
→ needu:prod:auth:...
→ needu:prod:cache:...
→ needu:prod:<domain>:idempotency:...
→ needu:prod:<domain>:ratelimit:...

AI Server
→ needu:prod:aichat:...
→ needu:prod:ai:...

Backend에는 LLM_API_KEY, EMBEDDING_API_KEY, PostgreSQL Credential을 두지 않는다.

16. 동시 Production 배포 제어

GitHub Actions concurrency.group은 Repository 범위에서 동작한다.

따라서 서로 다른 Backend / AI / Frontend Repository에 같은 문자열을 사용해도 Production 전체가 자동 직렬화되지는 않는다.

V1에서는 다음 두 단계로 제어한다.

Repository 내부
→ GitHub Actions concurrency

EC2 전체
→ 공통 flock File Lock

공통 Lock:

/home/ec2-user/needu/.production-deploy.lock

각 배포 Script 시작 시:

exec 9>"$LOCK_FILE"
flock -n 9 || {
  echo "Another production deployment is running"
  exit 1
}

17. 배포 실패 및 Rollback

실패 처리
Backend Unit / Build 실패 Merge 차단
Backend Integration 실패 main Merge 차단
AI Unit Test / Ruff 실패 Merge 차단
AI Docker Build 실패 main Merge 차단
Frontend Type / Lint / Build 실패 Merge 차단
Frontend E2E 실패 Artifact 생성 중단
Backend SHA 없음 EC2 변경 전 중단
AI SHA 없음 EC2 변경 전 중단
Frontend SHA / ECR Image 없음 EC2 변경 전 중단
Backend Health 실패 이전 Backend SHA
AI Health 실패 이전 AI SHA
Frontend Smoke Check 실패 이전 Frontend SHA Image로 Rollback

Version 파일:

.last_deployed_backend_tag
.last_deployed_ai_tag
.last_deployed_frontend_tag

18. Database Migration 주의사항

Application Rollback과 Database Schema Rollback은 별개다.

V1에서는 Column 즉시 삭제, Rename, 비호환 Type 변경 같은 파괴적 Migration을 Application 배포와 동시에 적용하지 않는다.

가능하면:

1. 새 Schema 추가
2. 구버전 / 신버전 모두 호환
3. Application 전환
4. 사용하지 않는 Schema는 이후 제거

형태로 진행한다.

MySQL과 PostgreSQL + pgvector 모두 같은 원칙을 적용한다.

19. 구현 순서 및 검증 시나리오

19.1 구현 순서

1. Frontend Dockerfile
2. Backend Dockerfile
3. AI Dockerfile
4. Frontend / Backend / AI PR CI
5. Branch Protection
6. Frontend ECR Push
7. Backend ECR Push
8. AI ECR Push
9. GitHub OIDC / IAM
10. Frontend CD
11. Backend CD
12. AI CD
13. Production Lock
14. 전체 통합 검증

19.2 실패 검증

Backend:

  • Unit Test 실패
  • Integration Test 실패
  • Docker Build 실패
  • 존재하지 않는 SHA
  • Health 실패 Image

AI:

  • pytest 실패
  • Ruff 실패
  • AI Dockerfile Build 실패
  • 존재하지 않는 SHA
  • /health 실패

Frontend:

  • TypeScript / Lint Error
  • next build 실패
  • Docker Build 실패
  • ECR Image Push 실패
  • 존재하지 않는 SHA
  • Frontend Container 기동 실패
  • http://localhost:3000 Smoke Check 실패
  • Rollback 실패
  • nginx -t 실패

동시 배포:

첫 번째 배포 → Lock 획득 → 진행
두 번째 배포 → Lock 실패 → EC2 변경 없이 종료

20. V1 CI/CD 최종 기준

20.1 Backend

feature → dev
→ Unit / Build / Static

dev → main
→ 위 검사
→ MySQL Integration
→ AI API Contract / Mock
→ Docker Build Check

main
→ needu-backend:<SHA>
→ ECR

20.2 AI Server

feature → dev
→ Python 3.12.11
→ uv 0.8.4
→ uv sync --frozen --dev
→ pytest tests/unit
→ ruff check .

dev → main
→ 위 검사
→ Docker Build Check

main
→ needu-ai:<SHA>
→ ECR

Redis / PostgreSQL Integration Test는 Adapter와 Test 구현 이후 추가한다.

20.3 Frontend

feature → dev
→ Type Check
→ Lint
→ next build

dev → main
→ 위 검사
→ Docker Build Check

main
→ Type Check / Lint
→ next build
→ Playwright E2E
→ Docker Image Build
→ needu-frontend:<SHA>
→ ECR

Production:

Nginx :443
↓
Frontend Container :3000
↓
Next.js Server

실행 환경:

Next.js 16.3.5
App Router
CSR + Server Component + 향후 SSR / 요청 기반 Server 기능
React 19.2.8
TypeScript 5.x
Node.js 24.10.0 이상
npm 11.17.0
output: "standalone"
node server.js
Port 3000

20.4 CD

main 성공
↓
Production Artifact 대기
↓
담당자 workflow_dispatch
↓
AWS OIDC
↓
SSM
↓
EC2 공통 Production Lock
↓
선택한 Component만 배포
↓
Health / Validation
├─ 성공 → Version 기록
└─ 실패 → Rollback

Frontend는 Production에서 output: "standalone" 기반 Docker Container로 Next.js Server를 실행한다.

Frontend CD에서는 ECR Image Pull → Frontend Container Recreate → localhost:3000 Smoke Check를 수행한다.

⚠️ **GitHub.com Fallback** ⚠️