501 초기 컨테이너 환경 구축 - 100-hours-a-week/KTB4-3rd-CLOUD GitHub Wiki

목차

[M1] 애플리케이션 Docker Image 구성

1. 시스템 범위와 전제

1.1. V1 실행 구성

사용자
  │ HTTPS :443 / HTTP :80 redirect
  ▼
Nginx
  ├── /       ──> Next.js :3000
  ├── /api    ──> Spring Boot :8080
  └── /ws     ──> Spring Boot WebSocket :8080

Next.js ────────> Spring Boot :8080   (SSR 호출이 있을 때)
Spring Boot ────> MySQL :3306

Next.js, Spring Boot, FastAPI는 각 애플리케이션 저장소에서 독립적으로 이미지를 빌드한다.
Nginx는 공식 이미지를 기반으로 설정 파일만 포함한 이미지로 관리하고, MySQL은 직접 빌드하지 않고 버전이 고정된 공식 이미지를 사용한다.
애플리케이션 이미지는 빌드 도구를 포함하는 단계와 실제 실행 단계를 나누는 멀티 스테이지 방식으로 구성한다. 운영 이미지는 실행에 필요한 파일만 포함하며, 소스 코드·테스트 결과·패키지 캐시·Secret은 포함하지 않는다.
AI 추론 서버는 V1 배포 범위에서 제외한다.

 

1.2. Docker Image을 선택한 이유

현재 한 EC2에서 Node.js, JVM, Python, Nginx, MySQL을 함께 실행한다. 각 서비스를 호스트에 직접 설치하면 런타임 버전과 시스템 패키지가 서로 영향을 주고, 서버에서 직접 빌드할 때마다 결과가 달라질 수 있다.
이미지 단위로 실행 환경을 고정하면 다음 효과를 얻는다.

  • 개발자가 검증한 실행 환경과 Dev·Prod의 실행 환경을 동일하게 유지한다.
  • 한 서비스의 런타임 또는 라이브러리 변경이 다른 서비스의 의존성에 영향을 주지 않는다.
  • 서버에서 소스 코드를 다시 빌드하지 않고 검증된 이미지를 내려받아 실행한다.
  • 장애 발생 시 이전 이미지 버전으로 애플리케이션을 되돌릴 수 있다.

 

따라서 Frontend·Backend·Nginx를 각각 독립 이미지로 빌드한다. 동일한 커밋에서 생성된 이미지가 Dev·Prod 인스턴스에 재현되고, 실행에 필요한 값은 이미지가 아니라 배포 환경에 주입되도록 구성한다.

 

1.3. M1 단계 산출물

<repository>/
├── Dockerfile                         # Frontend·Backend·AI 파트별 주 산출물
└── .dockerignore

infra/                                  # Cloud repository 또는 배포 repository
├── nginx/conf.d/default.conf
├── compose/docker-compose.yml          # M3 주 산출물
├── env/.env.dev.example                # Dev·Prod 환경 변수 분리
└── env/.env.prod.example

 

저장소가 분리되어 있다면 각 파트 repository의 Dockerfile을 해당 파트가 소유하고, compose·Nginx·환경 변수 연결은 Cloud repository에서 소유한다.

 

2. 애플리케이션 서버별 이미지 의존성

Dockerfile 작성 전 각 파트별 아래 파일을 전달받은 후 적용하려 한다.

FrontEnd 서버의 의존성 파일 (healthcheck, smoke test url 경로 없음)

의존성 실제 파일/값
package.json install, build, start, lint, typecheck, test script
packageManager : [email protected]
lock 파일 pnpm-lock.yaml
Node 버전 파일 .node-version
next.config.* output: standalone
image/asset 구성
tsconfig.json production build에 필요한 TypeScript 설정
.env.example NEXT_PUBLIC_API_BASE_URL=http://localhost:8080<br>NEXT_PUBLIC_GA_ID=<br>NEXT_PUBLIC_SENTRY_DSN=<br>SENTRY_DSN=<br>SENTRY_ENVIRONMENT=development<br>NEXT_PUBLIC_SENTRY_ENVIRONMENT=development<br>SENTRY_TRACES_SAMPLE_RATE=0.1<br>NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0.1<br>SENTRY_AUTH_TOKEN=<br>SENTRY_ORG=<br>SENTRY_PROJECT=
README.md 또는 docs/runtime.md production 실행 명령, 내부 포트(Cloud 기준 3000), health 경로와 smoke test URL

 

Backend 서버의 의존성 파일 (실제 값 추가)

의존성 실제 파일/값
build.gradle·settings.gradle·gradlew·gradlew.bat 또는 pom.xml·mvnw build tool과 wrapper
gradle.properties 또는 Maven 설정 JVM/build 옵션(Secret 제외)
src/main/resources/application*.yml 또는 application*.properties profile·port·DB 변수명 템플릿(실제 값 제외)
migration 디렉터리 Flyway/Liquibase/SQL 등 실제 migration 파일과 실행 방법
.env.example DB URL·사용자·비밀번호, JWT·외부 연동 변수명만 기록
README.md 또는 docs/runtime.md JDK 버전, build/run 명령, jar 경로, 내부 포트(Cloud 기준 8080), active profile
docs/health.md 또는 Actuator 설정 파일 startup·liveness·readiness 경로와 응답 기준
docs/deploy.md migration 소유 주체, 하위 호환성, JVM heap/GC, graceful shutdown, 업로드 경로

Nginx 및 MySQL 의존성

파일 내용
Nginx conf.d/default.conf 또는 nginx.conf /→Next.js, /api·/ws→Spring Boot upstream, WebSocket Upgrade, timeout
docs/nginx-runtime.md Nginx image 버전, TLS 인증서 외부 마운트 경로, health 경로, access/error log 형식
mysql image/version 문서 major/minor, charset/collation, health command
docs/database.md database/user 이름, volume·backup 경로, migration과 운영·개발 데이터 분리 방식

MySQL은 공식 이미지를 사용하고 별도 애플리케이션 Dockerfile을 만들지 않는 것을 기본으로 한다. 버전·초기화·백업은 Backend와 Cloud가 공동 검토한다.

 

3. M1 이미지 설계 (dockerfile 설계 고민 추가로 진행)

공통 Dockerfile 규칙

  1. BuildKit multi-stage로 build stage와 runtime stage를 분리한다.
  2. Base image tag를 major만 지정하지 않고 승인된 버전 또는 digest로 고정한다.
  3. Runtime stage에는 소스 전체·컴파일러·package manager cache·test fixture를 복사하지 않는다.
  4. Secret, .env, 개인 키, 로컬 캐시, 모델 실험 결과는 .dockerignore로 차단한다.

 

Multi-Stage Build 목적

  1. 캐시를 통해 빌드 속도 최적화
  2. 이미지 크기 감소

 

1. Frontend Dockerfile

# Multi-stage build

# 1. Base Image
FROM node:<NODE_VERSION>-bookworm-slim AS deps
WORKDIR /app
RUN corepack enable && corepack prepare <PACKAGE_MANAGER>@<PACKAGE_MANAGER_VERSION> --activate

# 2. 의존성 설치
COPY package.json <LOCK_FILE> ./
RUN <PACKAGE_MANAGER> install --frozen-lockfile

# 3. Next.js 빌드
FROM deps AS builder
COPY . .
ARG BUILD_SHA
ARG RELEASE_VERSION
ARG BUILD_DATE
ENV NEXT_TELEMETRY_DISABLED=1
RUN <PACKAGE_MANAGER> run build

# 4. Next.js 실행
FROM node:<NODE_VERSION>-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000
RUN addgroup --system app && adduser --system --ingroup app app

# next.config.*에서 output: standalone을 설정하면
# 빌드에 필요한 최소 파일이 .next/standalone에 생성된다.
# standalone 결과물에 포함되지 않는 static·public은 별도로 복사한다.
COPY --from=builder --chown=app:app <STANDALONE_ARTIFACT> ./
COPY --from=builder --chown=app:app <STATIC_ARTIFACT> ./
COPY --from=builder --chown=app:app public ./public
USER app
EXPOSE 3000
CMD ["<PRODUCTION_START_COMMAND>"]

 

2. Backend Dockerfile

# Multi-stage build

# 1. Build Base Image
FROM eclipse-temurin:<JAVA_VERSION>-jdk-jammy AS dependencies
WORKDIR /workspace

# 2. 빌드 의존성 파일 복사
COPY <WRAPPER_FILES> ./
COPY <BUILD_DESCRIPTOR_FILES> ./

# 3. 의존성 레이어 구성
RUN <BUILD_TOOL> <DEPENDENCY_CACHE_COMMAND>

# 4. 애플리케이션 빌드
FROM dependencies AS builder
COPY . .
RUN <BUILD_TOOL> <PACKAGE_COMMAND>

# 5. Runtime Base Image
FROM eclipse-temurin:<JAVA_VERSION>-jre-jammy AS runner
WORKDIR /app
RUN addgroup --system app && adduser --system --ingroup app app

# 빌드 결과물인 실행 jar만 runtime image로 복사한다.
COPY --from=builder --chown=app:app <JAR_PATH> /app/app.jar
USER app
EXPOSE 8080
ENV JAVA_OPTS=""
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

 

3. Nginx Dockerfile


# 1. Base Image
FROM nginx:<NGINX_VERSION>-alpine

# 2. Nginx 설정 복사
COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d/default.conf /etc/nginx/conf.d/default.conf

# 3. Nginx 실행
# TLS 인증서와 private key는 이미지에 포함하지 않고 Compose volume으로 주입한다.
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]

 

4. MySQL Dockerfile 제외

MySQL은 애플리케이션 Dockerfile을 작성하지 않고 Compose에서 공식 이미지를 사용한다.

# docker-compose.yml

# 1. Base Image
services:
  mysql:
    image: mysql:<MYSQL_VERSION>

    # 2. 초기 실행 환경 주입
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}

    # 3. 데이터와 초기화 파일 연결
    volumes:
      - mysql-data:/var/lib/mysql
      - ./db/init:/docker-entrypoint-initdb.d:ro

 

 

 


[M2] Docker Compose 기반 서비스 실행 환경 구성

AWS 구성요소 설정 초기값

VPC

항목 값
Region ap-northeast-2
VPC 수 1
VPC CIDR 10.20.0.0/16
DNS support / hostname true / true
Instance tenancy moyeota
IPv6 CIDR 사용하지 않음
Availability Zone 수 1
Availability Zone ap-northeast-2a
Public Subnet 수 1
Prod Subnet 10.20.10.0/24, ap-northeast-2a
Dev Subnet 10.20.20.0/24, ap-northeast-2a
map_public_ip_on_launch false — EC2별 EIP 사용
Internet Gateway 1개
NAT Gateway 0개
Elastic IP 2개 — Dev 1, Prod 1
Route Table 2개 — Dev 1, Prod 1
각 Route Table local route 10.20.0.0/16 -> local
각 Route Table public route 0.0.0.0/0 -> Internet Gateway
Custom Network ACL 0개 — VPC default NACL 사용

EC2

항목 Dev Prod
인스턴스 수 1 1
Instance type t3.medium t3.medium
vCPU / Memory 2 vCPU / 4 GiB 2 vCPU / 4 GiB
Architecture x86_64 x86_64
AMI AL2023 최신 x86_64 SSM parameter 동일
AMI parameter /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 동일
Root EBS gp3, 30 GiB, 3,000 IOPS, 125 MiB/s 동일
Data EBS gp3, 30 GiB, 3,000 IOPS, 125 MiB/s gp3, 30 GiB, 3,000 IOPS, 125 MiB/s
EBS encryption true true
Root delete on termination true true
Data delete on termination false false
Data mount /mnt/docker-data /mnt/docker-data
Elastic IP 1 1
Detailed monitoring false — 기본 5분 metric false — 기본 5분 metric
IMDS endpoint enabled, token required, hop limit 2 동일
EC2 key pair 기존 Dev 운영자 key 이름 null
운영 접속 제한된 SSH SSM only

보안 그룹

Prod EC2

방향 Protocol Port Source/Destination 설명
Ingress TCP 80 0.0.0.0/0 HTTPS redirect 또는 인증서 발급
Ingress TCP 443 0.0.0.0/0 Nginx HTTPS
Egress TCP 443 0.0.0.0/0 ECR, S3, SSM, CloudWatch, 외부 API

Dev EC2

방향 Protocol Port Source/Destination 설명
Ingress TCP 80 dev_qa_cidrs 통합 테스트, HTTPS redirect
Ingress TCP 443 dev_qa_cidrs 통합 테스트 HTTPS
Ingress TCP 22 admin_cidrs 임시 운영 SSH
Egress TCP 443 0.0.0.0/0 ECR, S3, SSM, CloudWatch, 외부 API

IAM

EC2 instance role

목적 최소 action 범위
SSM 접속 AWS managed policy AmazonSSMManagedInstanceCore
ECR 인증 ecr:GetAuthorizationToken (Resource = *)
ECR pull ecr:BatchCheckLayerAvailability, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage를 4개 repository ARN으로 제한
DB 백업 업로드 s3:PutObject, 필요한 multipart action을 자기 환경의 backup prefix로 제한
배포 bundle 조회 s3:GetObject를 자기 환경의 deployment prefix로 제한
로그 전송 logs:CreateLogStream, logs:DescribeLogStreams, logs:PutLogEvents를 자기 환경 log group으로 제한

GitHub Actions OIDC roles

Role 사용 주체 권한
moyeota-ci-ecr-push FE/BE/AI/CLOUD 이미지 workflow ECR 인증과 대상 repository push
moyeota-cd-dev CLOUD Dev environment Dev deployment prefix upload, Dev EC2에 SSM command 실행·결과 조회
moyeota-cd-prod CLOUD Prod environment Prod deployment prefix upload, Prod EC2에 SSM command 실행·결과 조회

ECR Repository

moyeota/nginx, moyeota/fe, moyeota/be, moyeota/ai
이미지는 환경-<sha 태그> 형식으로 푸시 진행한다.

 

S3 buckets

논리 이름 이름 예시 Prefix 초기 lifecycle
Deploy moyeota-<account-id>-deployments dev/, prod/ 7일 후 삭제
DB_backup moyeota-<account-id>-backups dev/mysql/, prod/mysql/ Dev 7일, Prod 30일
Image moyeota-<account-id>-uploads dev/, prod/ 자동 삭제 없음

CloudWatch log groups

/moyeota/dev/nginx
/moyeota/dev/frontend
/moyeota/dev/backend
/moyeota/dev/mysql
/moyeota/dev/ai
/moyeota/prod/nginx
/moyeota/prod/frontend
/moyeota/prod/backend
/moyeota/prod/mysql
/moyeota/prod/ai

 

Github Actions OIDC 연결

서비스 도메인 등록

SSL 설정 (예정)

 


[M3] Docker Compose 기반 CI/CD 파이프라인 구성

Dev,Prod 배포 환경

  • Dev 개발자들의 공동 테스트환경으로 정기 배포 전 새로운 기능을 테스트 하는 용도나 여러 팀에서 개발한 기능을 통합 테스트한다.

  • Prod V1 개발 기간에는 주 1회 정기 배포를 하는 환경으로 최종적으로 기능 테스트가 완료되었다면, main에 머지하고 QA이후 수동 승인을 통해 배포한다.

     

규칙

  • 통합 개발 기준 브랜치는 develop으로 통일한다. dev는 환경 이름으로만 사용한다.
  • 일반 개발은 feat/*, fix/*, refactor/*, chore/*에서 시작해 develop으로 PR을 보낸다.
  • 정기 출시와 QA 승인 구간에는 develop/vMAJOR.MINOR.PATCH 브랜치를 사용한다.
    • 프론트엔드는 별도의 release/* 브랜치를 사용하지 않는다.
    • develop 브랜치에 병합된 커밋을 Dev EC2에 자동 배포하고, 해당 이미지에 대해 E2E 테스트와 수동 QA를 진행한다.
    • QA가 완료되고 팀의 확인이 끝난 경우 develop을 main에 병합한다.
    • main Push 이후 Prod CD가 실행되며, Production Environment 승인 후 동일한 이미지 Digest를 Prod EC2에 배포한다.
  • 운영 긴급 수정은 hotfix/*를 main에서 생성한다.
  • Dev 배포는 develop 병합 시 자동 수행한다.
  • Prod 배포는 main에 버전 Tag가 생성되고 GitHub Environment 승인이 완료된 경우에만 수행한다.
  • 이미지 Tag는 식별용으로 사용하고, 실제 실행은 ECR 이미지 Digest(@sha256:...)를 사용한다.

브랜치 구조

브랜치 생성 기준 목적 병합 대상 배포
main 보호 브랜치 운영에 배포된 코드와 출시 이력 release, hotfix PR만 허용 버전 Tag 생성 후 Prod
develop main에서 최초 생성 후 유지 개발 기능 통합 및 Dev 기준 코드 일반 작업 PR 수신 병합 시 Dev 자동 배포
feat/* develop 신규 기능 개발 develop PR 검증만 수행
fix/* develop 또는 현재 QA 중인 release Dev 또는 QA에서 발견된 일반 버그 수정 생성 기준 브랜치 기준 브랜치의 파이프라인 수행
chore/* develop 의존성, 설정, 빌드, 문서 작업 develop PR 검증만 수행
hotfix/* main 운영 긴급 장애 수정 main, 이후 develop 및 활성 release 승인 후 Prod 긴급 배포

 

develop/* 브랜치는 사용하지 않는다. develop은 고정된 통합 브랜치 이름이고, dev는 AWS Dev 환경 이름으로만 사용한다.

 

브랜치 흐름도

 

QA 작업 진행 중 새로운 develop 배포 중지

별도의 release 브랜치를 사용하지 않고, QA 대상은 브랜치가 아니라 특정 이미지 Digest로 고정한다.
develop에 병합된 커밋은 CI를 통과한 뒤 Dev 이미지로 빌드되어 Dev EC2에 자동 배포된다. Dev EC2는 실제 운영 환경과 동일한 Docker Compose 구조를 사용하되, 개발용 데이터와 환경변수를 사용한다.
QA를 시작하면 다음 정보를 기록한다.

  • Git commit SHA
  • ECR image tag
  • ECR image digest
  • CI 실행 ID
  • QA 시작 시각


QA가 진행되는 동안에는 새로운 develop 배포를 일시 중지한다. QA에서 문제가 발견되면 develop에 수정사항을 반영하고 새 이미지를 생성한 뒤 QA를 다시 시작한다. QA가 완료되고 팀원 확인이 끝나면 develop을 main에 병합한다.

Ticket 및 Git 작업 정책

Ticket 기준

  • 모든 작업은 Ticket을 먼저 생성한다.
  • 하나의 Ticket은 하나의 목적만 다룬다.
  • 하나의 Ticket에서 여러 기능을 함께 개발하지 않는다.
  • PR 제목, 브랜치명, 커밋 메시지에 Ticket ID를 포함한다.
  • PR 본문에는 변경 내용, 테스트 결과, 배포 영향, 설정 또는 Secret 변경 여부를 기록한다.
  • QA 버그는 기존 출시 Ticket의 하위 버그 Ticket으로 연결한다.

 

브랜치 생성 규칙

feat/123-add-release-workflow
fix/124-fix-health-check
refactor/125-simplify-deploy-script
chore/126-update-compose-version
release/v1.0.0
hotfix/127-fix-prod-startup
  • 영문 소문자와 하이픈을 사용한다.
  • 브랜치는 짧게 유지하고 병합 후 삭제한다.
  • main, develop, release/*에 직접 Push하지 않는다.
  • 모든 병합은 PR과 CI 통과를 필수로 한다.
  • Prod 배포 권한과 main 병합 권한은 제한한다.

 

Commit 규칙

Conventional Commits 형식을 적용해 작성한다.

Conventional Commits은 가장 보편적으로 쓰는 방식으로 기본 구조가 <type>(optional scope): <subject> 와 같다.

feat(M3-123): add release deployment workflow
fix(M3-124): correct health check retry
refactor(M3-125): simplify release manifest generation
chore(M3-126): update compose version

 

PR 병합 방식

  • 작업 브랜치 → develop: Squash merge, 여러 개의 커밋을 하나의 커밋으로 합친 새로운 커밋을 develop 브랜치에 추가하는 방식
  • release → main: Merge commit 또는 PR 병합으로 출시 경계 보존
  • release → develop: QA 수정사항 동기화
  • hotfix → main: Merge 후 Patch 버전 Tag 생성
  • hotfix → develop: 반드시 병합하여 수정사항 유실 방지

 

이미지 Tag 및 버전 정책

Git : Semantic Versioning

vMAJOR.MINOR.PATCH
v1.0.0
v1.0.1
v1.1.0
v2.0.0
  • MAJOR: 호환성이 깨지는 변경
  • MINOR: 하위 호환 신규 기능
  • PATCH: 버그 수정 및 긴급 수정
  • 운영 배포 Tag는 main의 병합 커밋에 생성한다.
  • Tag는 삭제하거나 같은 이름으로 덮어쓰지 않는다.

 

ECR 이미지 Tag

상황 이미지 Tag 예시 용도
develop 병합 dev-a1b2c3d Dev 자동 배포
정식 출시 prod-v1.0.0-a1b2c3d Prod 배포 이력
Hotfix 출시 prod-v1.0.1-f9e8d7c 긴급 Patch 배포

ECR은 Immutable Tag 정책을 사용하므로 이미 사용한 Tag를 재사용하지 않는다. latest, dev, prod처럼 이동하는 Tag는 운영 배포에 사용하지 않는다.
운영 실행은 Tag가 아니라 다음과 같은 Digest를 사용한다.

123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/moyeota/fe@sha256:<digest>
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/moyeota/be@sha256:<digest>
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/moyeota/nginx@sha256:<digest>
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/moyeota/ai@sha256:<digest>

Dev에서 검증한 이미지는 새로 빌드하지 않고 기존 Dev 이미지의 Digest를 Prod Tag로 승격한다.

 

CI/CD 반영 정책

Pull Request

대상 브랜치가 develop, release/*, main이면 다음을 수행한다.

  • 단위 테스트
  • Lint 및 Type Check
  • Docker Build
  • 이미지 취약점 스캔
  • Compose 설정 검증
  • Docker build 검증

PR 단계에서는 EC2 배포를 수행하지 않는다.

develop 병합

  1. FE, BE, Nginx, AI 이미지를 빌드한다.
  2. 이미지에 dev-<sha7> Tag를 부여한다.
  3. ECR에 Push한다.
  4. Push 결과에서 각 이미지 Digest를 취득한다.
  5. Dev Release Manifest에 Digest를 기록한다.
  6. EC2에 배포한다.
  7. Health Check와 Smoke Test를 실행한다.
  8. 실패하면 직전 성공 Digest로 Rollback한다.

 

Prod 배포

Prod는 다음 조건을 모두 만족해야 한다.

  • main에 정식 버전 Tag 존재
  • GitHub Actions Prod Environment 승인 완료
  • Dev 또는 QA Smoke Test 성공
  • 배포할 이미지 Digest 확정
  • Prod 배포 전 MySQL 백업 성공
  • Release Manifest 검증 성공

 

Hotfix 정책

  1. 운영 장애 Ticket을 생성한다.
  2. main에서 hotfix/*를 생성한다.
  3. 수정 범위를 장애 해결에 필요한 최소 범위로 제한한다.
  4. PR과 CI를 통과시킨다.
  5. 승인 후 main에 병합한다.
  6. Patch 버전 Tag를 생성한다. 예: v1.0.1
  7. Prod에 동일 Digest를 배포한다.
  8. hotfix를 develop과 활성 release에 병합한다.
  9. 장애 원인, 영향 범위, 복구 방법을 Ticket에 기록한다.

 

배포 흐름

작업 브랜치
  → develop
  → Dev 자동 배포
  → Dev EC2에서 수동 QA 진행
  → QA 승인 후 main 머지
  → Dev 이미지의 digest 유지 
  → prod-vX.Y.Z Tag로 승격
  → 승인 후 Prod 배포

 

[M3-1] CI 파이프라인

V1 기간 동안 CI 파이프라인 구축 목표는 다음 두가지를 생각해서 구성하려고 한다.

  • 빠른 CICD 환경 설정
  • 개발자들 간의 코드 병합 문제를 최소화하기 위해 세부적인 브랜치 정책 도입
  • 머지데이 발생하지 않도록 작은 PR 단위의 통합 과정
  • CI 이후 개발자의 판단 과정 없이 브랜치별 자동 배포 정책

 

이외에 더 고민해야하는 상황은 우선적으로 진행하지 않고 운영자의 다음 단계의 과제로 남겨 둔다.
프론트와 백엔드, AI는 각각 테스트 단계가 다르기 때문에 CICD 파이프라인이 다르다. 따라서 먼저 공통으로 적용되는 단계를 정의하고, 각 파트별 V1에서 진행하는 단계를 정의한다.

 

CI 실패 처리 정책

[2단계 CI 파이프라인설계] 에서 정의한 실패 유형별 처리에서 각 단계별로 실패 처리와 재시도 정책을 구체화해 정의한다.
실패 처리와 재시도 정책을 통해 통합과 배포 과정을 자동화하는 과정에서 사람이 판단하고 개입하는 지점을 줄이기 위함에 있다.

 

  • API 계약 테스트 실패 : PR merge 차단 후 문제 조합 기록 후 담당자에게 디스코드 웹 훅으로 전달 프론트, 백엔드, AI에 적용한다.
  • 통합 테스트 실패 : PR merge 차단 후 문제 조합 기록 후 담당자에게 디스코드 웹 훅으로 전달 백엔드와 AI에 적용한다.
  • 빌드 실패 : PR merge와 Dev 배포 실행 차단

 

CI/CD 실패 시 Discord Webhook Issue

name: Opend Issue Notification # 1
on: # 2
  issues:
    types:
      - opened 

jobs: 
  create-issue:
    name: Discord notification 
    runs-on: ubuntu-latest 
    steps:
      - name: Send Issue
        uses: Ilshidur/[email protected] # 3
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} # 4
          DISCORD_USERNAME: WAPP_BOT # 5
          DISCORD_AVATAR: https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true # 6
          DISCORD_EMBEDS: |  # 7
            [
                { 
                  "title": "🚨 긴급 이슈 알림\n${{ github.event.issue.title }}",
                  "color": 10478271,
                  "description": "${{ github.event.issue.html_url }}",
                  "fields": [
                    {
                      "name": "Issue Number",
                      "value": "#${{ github.event.issue.number }}",
                      "inline": true
                    },
                    {
                      "name": "Author",
                      "value": "${{ github.event.issue.user.login }}",
                      "inline": true
                    }
                  ]
                }
              ]

출처: Android CI/CD : Github Action을 통해, Discord + Jira 연동하기
디스코드 웹훅(Webhook)으로 자동 알림 보내는 법 (feat. GitHub Actions)

공통 단계

1) 각 브랜치 별 도커 이미지 태그 자동 생성

# next build 는 아래 Docker 빌드에서 수행되므로 별도 build 스텝을 두지 않는다.
#
# FE CI 의 책임은 dev-/rc- 이미지 생성까지다.
# 정식 출시 태그(prod-vX.Y.Z-<sha7>)는 "재빌드 없이 동일 Digest" 정책에 따라
# Cloud(CD) 레포에서 생성한다. CD 는 rc-vX.Y.Z- prefix 로 ECR 을 조회해
# 해당 버전의 rc 이미지를 찾은 뒤 prod- 태그로 재태깅한다.
- name: Resolve image tag
  id: meta
  run: |
    set -Eeuo pipefail

    if [[ "$GITHUB_REF_NAME" == develop ]]; then
      image_tag="dev-${GITHUB_SHA:0:7}"
      release_version=""
      push_image=true
    elif [[ "$GITHUB_REF_NAME" == release/* ]]; then
      version="${GITHUB_REF_NAME#release/}"
      [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
      image_tag="rc-${version}-${GITHUB_SHA:0:7}"
      release_version="$version"
      push_image=true
    elif [[ "$GITHUB_REF_NAME" == hotfix/* ]]; then
      image_tag="hf-${GITHUB_SHA:0:7}"
      release_version=""
      push_image=false
    else
      image_tag="pr-${GITHUB_SHA:0:7}"
      release_version=""
      push_image=false
    fi

    {
      echo "image_tag=$image_tag"
      echo "release_version=$release_version"
      echo "push_image=$push_image"
      echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    } >> "$GITHUB_OUTPUT"

develop 브랜치인 경우, Dev 인스턴스에 올라가는 이미지이기 때문에 이미지 태그는 [이미지 Tag 및 버전 정책] 에서 정의한대로 dev-[SHA태그] 로 생성된다.
release 브랜치인 경우, Prod 인스턴스에 올라가기 전 QA 테스트하는 이미지이기 때문에 rv-[시멘틱 버저닝] 로 생성된다.
이외 다른 브랜치에서 생성되는 이미지는 pr-[SHA태그] 로 생성해 이미지 push 하지 않는다.

2) Github Actions와 AWS ECR 연결 → 이미지 업로드

- name: Configure AWS credentials
  if: steps.meta.outputs.push_image == 'true'
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-region: ${{ env.AWS_REGION }}
    role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
    role-session-name: fe-ci-${{ github.run_id }}

- name: Login to ECR
  if: steps.meta.outputs.push_image == 'true'
  uses: aws-actions/amazon-ecr-login@v2

- name: Push image
  if: steps.meta.outputs.push_image == 'true'
  env:
    IMAGE_TAG: ${{ steps.meta.outputs.image_tag }}
  run: |
    set -Eeuo pipefail

    remote_image="${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}"

    docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "$remote_image"
    docker push "$remote_image"

 

 

 

Frontend CI 파이프라인

1. Frontend CI 트리거 정책

name: FE CI

on:
  # PR 은 검증만 수행하고 ECR push 와 배포는 하지 않는다.
  # hotfix/* 는 main 으로 PR 을 보내므로 아래 main 대상 PR 에 포함된다.
  pull_request:
    branches:
      - develop
      - main

  # develop, main 로의 직접 push 는 branch protection 으로 차단되므로
  # 이 이벤트는 사실상 "PR 이 병합되어 ref 가 전진했을 때"의 push 를 의미한다.
  push:
    branches:
      - develop
      - main

  workflow_dispatch:

 

프론트엔드의 CI가 실행되는 트리거는 다음으로 정의한다.

  • develop 와 main, release 브랜치에서 Pull_request 발생했을 경우
  • develop, main 브랜치에서 Push 가 발생했을 경우

인프라 운영자의 실행 테스트를 위하여 수동 테스트를 할 수 있게 workflow_dispatch를 추가한다.

2. 프론트엔드 의존성 설치

  - name: Install dependencies
    run: pnpm install --frozen-lockfile

프론트의 패키지 매니저는 pnpm이기 때문에 pnpm으로 의존성 파일을 설치한다.

3. 환경 변수 관리

CICD 정책에서 이미지 빌드 시 환경변수를 포함하지 않고 배포 시 런타임 환경변수를 주입하는 구조를 선택한다. Github Actions가 CI 실행에 필요한 환경변수는 레포지토리에 변수로 등록한다.

  • AWS_REGION
  • ECR_REGISTRY
  • ECR_REPOSITORY
  • AWS_ROLE_TO_ASSUME : secret

 

이외 NEXT_PUBLIC_API_BASE_URL 등 환경별 값은 이미지 빌드 시 주입하지 않는다. 컨테이너 시작 시 EC2의 런타임 환경변수 또는 runtime-config 파일로 주입한다.

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
  ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
  IMAGE_NAME: moyeota-fe
  NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }}
  NEXT_PUBLIC_GA_ID: ${{ vars.NEXT_PUBLIC_GA_ID }}
  NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
  NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
  NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE }}

환경변수 관리 방법에 대해서 다른 방법이 있는지 추가 과제로 진행한다.

 

4. 테스트

프론트엔드 CI 파이프라인에서 테스트 단계는 다음과 같이 정의한다.

  1. lint 테스트
  2. Unit 테스트
  3. Chromatic 테스트
  4. API 계약 테스트

추가적으로 E2E 테스트는 다음 단계의 과제로 진행한다.
프론트엔드에서는 통합 테스트를 진행하지 않는다. 프론트와 백엔드는 다른 레포에서 진행되며, 통합 테스트를 추가하게 되면 CI 파이프라인의 빌드 시간이 늘어나 대기 시간이 발생된다. 또한 프론트엔드의 경우 E2E 테스트로 충분히 오류를 감지할 수 있기 때문에 통합 테스트는 따로 추가하지 않을 예정이다.

 

1) Lint Test(oxlint)

.oxlintrc.json 에 oxlint 규칙을 지정하고 package.json에 "lint": "oxlint ." 을 정의한다.

  - name: Run oxlint
    run: pnpm lint

참고자료: Rust로 다시 쓴 JavaScript 도구 — OXC가 ESLint보다 100배 빠른 이유와 그 의미

 

2) Unit Test(vitest)

vitest.config.ts 에 tests/unit/**/*.test.tsx 파일 12개를 대상으로 단위테스트를 진행한다.

  - name: Unit test
    run: pnpm test

참고자료: techblog.woowahan.com

 

3) UI Test(chromatic)

현재 프론트엔드 팀은 1명으로 디자인 시스템 기반의 컴포넌트를 개발하고 있다. 컴포넌트 하나의 변경이 여러 화면에 영향을 줄 수 있기 때문에, 변경 후 모든 UI를 수동으로 확인하는 과정은 반복적이며 QA 병목으로 이어질 수 있다.

- name: Run Chromatic
  if: github.event_name == 'merge'
  run: pnpm run chromatic
  env:
    CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Chromatic은 Storybook에 정의된 컴포넌트 상태를 기준으로 이전 기준 화면과 현재 화면을 비교하는 시각적 회귀 테스트 도구로 아래 항목을 자동으로 검증할 수 있다.

  • 컴포넌트의 레이아웃, 색상, 간격, 크기 변경 감지
  • 공통 컴포넌트 변경으로 인한 영향 범위 확인
  • 반복적인 수동 UI 검수 감소
  • 디자인 시스템 컴포넌트의 시각적 품질 유지

 

Chromatic 테스트에 대해 CI 파이프라인에서 workflow 트리거는 다음과 같이 정의한다.

이벤트 CI 실행 Chromatic 실행
develop , main 브랜치 merge O O
develop, main, release/** 대상 PR O X

즉, 현재 Chromatic은 develop 또는 release/** 브랜치에 push될 때만 자동 실행된다. PR에서는 Lint, Format, Type check, Unit test까지만 실행되고 Chromatic은 실행되지 않는다.

 

위에서 PR 트리거에는 실행 시간과 파이프라인 단순화를 위해 Chromatic 테스트를 제외했다. 하지만 얼만큼 CI 파이프라인 전체 실행시간에 영향을 주는지에 측정하는 것은 추가 과제로 진행한다. (작업의 우선도에 따라 진행하지 않을 수 있다.)
참고 자료: Chromatic으로 시각적 회귀 테스트 구축하기
크로마틱(Chromatic)을 사용한 UI 테스트

 

5. 이미지 빌드

- name: Build Docker image
  run: |
    set -Eeuo pipefail

    docker_build_args=()
    if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then
      docker_build_args+=(--secret "id=sentry_auth_token,env=SENTRY_AUTH_TOKEN")
    fi

    DOCKER_BUILDKIT=1 docker build \
      --file Dockerfile \
      --tag "${IMAGE_NAME}:${IMAGE_TAG}" \
      --build-arg "BUILD_SHA=${GITHUB_SHA}" \
      --build-arg "RELEASE_VERSION=${{ steps.meta.outputs.release_version }}" \
      --build-arg "BUILD_DATE=${{ steps.meta.outputs.build_date }}" \
      --build-arg "SENTRY_ORG=${SENTRY_ORG:-}" \
      --build-arg "SENTRY_PROJECT=${SENTRY_PROJECT:-}" \
      --build-arg "NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-}" \
      --build-arg "NEXT_PUBLIC_GA_ID=${NEXT_PUBLIC_GA_ID:-}" \
      --build-arg "NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN:-}" \
      --build-arg "NEXT_PUBLIC_SENTRY_ENVIRONMENT=${NEXT_PUBLIC_SENTRY_ENVIRONMENT:-}" \
      --build-arg "NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=${NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE:-}" \
      "${docker_build_args[@]}" \

빌드 실행 시간 최적화는 이후 추가 과제로 빌드 시간을 측정 후 최적화하는 작업을 진행한다.

 

6. CI 실패 시 웹 훅으로 이슈 전송

각 단계에서 CI를 실패하는 경우 [CI 실패 처리 정책] 에 따라 진행한다.

name: CI Failure Notification

on:
  workflow_run:
    workflows:
      - FE CI
    types:
      - completed

permissions:
  actions: read

jobs:
  notify:
    name: Discord notification
    if: ${{ github.event.workflow_run.conclusion == 'failure' }}
    runs-on: ubuntu-latest
    steps:
      - name: Build CI failure embed
        uses: actions/github-script@v7
        with:
          script: |
            const run = context.payload.workflow_run;

            const embeds = [
              {
                title: `❌ CI 실패 알림\n${run.name}`,
                color: 15158332,
                description: run.html_url,
                fields: [
                  {
                    name: 'Branch',
                    value: run.head_branch || '-',
                    inline: true,
                  },
                  {
                    name: 'Event',
                    value: run.event || '-',
                    inline: true,
                  },
                  {
                    name: 'Actor',
                    value: run.actor?.login || '-',
                    inline: true,
                  },
                  {
                    name: 'Commit',
                    value: run.head_sha ? run.head_sha.slice(0, 7) : '-',
                    inline: true,
                  },
                ],
              },
            ];

            core.exportVariable('DISCORD_EMBEDS', JSON.stringify(embeds));

      - name: Send CI failure notification
        uses: Ilshidur/[email protected]
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
          DISCORD_USERNAME: WAPP_BOT
          DISCORD_AVATAR: https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true

 

Backend CI 파이프라인

1. Backend CI 트리거 정책

on:
  # PR 은 검증만 수행하고 ECR push 와 배포는 하지 않는다.
  # hotfix/* 는 main 으로 PR 을 보내므로 아래 main 대상 PR 에 포함된다.
  pull_request:
    branches:
      - develop
      - main
      - 'release/**'

  # develop, release/* 로의 직접 push 는 branch protection 으로 차단되므로
  # 이 이벤트는 사실상 "PR 이 병합되어 ref 가 전진했을 때"의 push 를 의미한다.
  push:
    branches:
      - develop
      - 'release/**'
  • develop 와 main, release 브랜치에서 Pull_request 발생했을 경우
  • develop, main 브랜치에서 Push 가 발생했을 경우

인프라 운영자의 실행 테스트를 위하여 수동 테스트를 할 수 있게 workflow_dispatch를 추가한다.

2. 백엔드 의존성 설치 및 단위 테스트

- name:  Install dependencies and run unit tests
  run: ./gradlew --no-daemon test

3. 환경변수 관리

CICD 정책에서 이미지 빌드 시 환경변수를 포함하지 않고 배포 시 런타임 환경변수를 주입하는 구조를 선택한다. Github Actions가 CI 실행에 필요한 환경변수는 레포지토리에 변수로 등록한다.

  • AWS_REGION
  • ECR_REGISTRY
  • ECR_REPOSITORY
  • JAVA_VERSION
  • AWS_ROLE_TO_ASSUME : secret
env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
  ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}

 

4. 이미지 빌드

- name: Build Docker image
  run: |
    set -Eeuo pipefail

    DOCKER_BUILDKIT=1 docker build \
      --file Dockerfile \
      --tag "moyeota-be:${IMAGE_TAG}" \
      --build-arg "BUILD_SHA=${GITHUB_SHA}" \
      --build-arg "RELEASE_VERSION=${RELEASE_VERSION}" \
      --build-arg "BUILD_DATE=${BUILD_DATE}" \
      --label "org.opencontainers.image.revision=${GITHUB_SHA}" \
      --cache-from "type=gha,scope=be-${GITHUB_REF_NAME}" \
      --cache-to "type=gha,mode=max,scope=be-${GITHUB_REF_NAME}" \
      --load \
      .

 

 

[M3-2] CD 파이프라인

배포 실패 처리 정책

컨테이너 기동 실패 환경변수·Secret 누락, 포트 충돌, 애플리케이션 초기화 오류 외부 트래픽 차단, 로그 확인 후 재배포 또는 직전 이미지 Rollback
Startup Probe 실패 애플리케이션 기동 지연, DB 연결 실패, 초기화 로직 오류 기동 상태 확인, 설정 수정 또는 이전 정상 Release로 Rollback
Readiness Probe 실패 필수 DB 연결 실패, Migration 미완료, 필수 의존성 장애 트래픽 전달 중지, 원인 확인 후 재기동·재배포
Health Check 실패 프로세스 중단, 애플리케이션 포트 미응답, 컨테이너 비정상 상태 컨테이너 재시작, 반복 실패 시 배포 중단 및 Rollback

ECR 이미지를 EC2로 자동 배포

 

 


[M4] IaC를 활용한 기본 인프라(VPC, EC2, SG 등) 구성

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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