501 초기 컨테이너 환경 구축 - 100-hours-a-week/KTB4-3rd-CLOUD GitHub Wiki
목차
- [M1] 애플리케이션 Docker Image 구성
- [M2] Docker Compose 기반 서비스 실행 환경 구성
- [M3] Docker Compose 기반 CI/CD 파이프라인 구성
- [M3-1] CI 파이프라인
- [M3-2] CD 파이프라인
- [M4] IaC를 활용한 기본 인프라(VPC, EC2, SG 등) 구성
사용자
│ 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 배포 범위에서 제외한다.
현재 한 EC2에서 Node.js, JVM, Python, Nginx, MySQL을 함께 실행한다. 각 서비스를 호스트에 직접 설치하면 런타임 버전과 시스템 패키지가 서로 영향을 주고, 서버에서 직접 빌드할 때마다 결과가 달라질 수 있다.
이미지 단위로 실행 환경을 고정하면 다음 효과를 얻는다.
- 개발자가 검증한 실행 환경과 Dev·Prod의 실행 환경을 동일하게 유지한다.
- 한 서비스의 런타임 또는 라이브러리 변경이 다른 서비스의 의존성에 영향을 주지 않는다.
- 서버에서 소스 코드를 다시 빌드하지 않고 검증된 이미지를 내려받아 실행한다.
- 장애 발생 시 이전 이미지 버전으로 애플리케이션을 되돌릴 수 있다.
따라서 Frontend·Backend·Nginx를 각각 독립 이미지로 빌드한다. 동일한 커밋에서 생성된 이미지가 Dev·Prod 인스턴스에 재현되고, 실행에 필요한 값은 이미지가 아니라 배포 환경에 주입되도록 구성한다.
<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에서 소유한다.
Dockerfile 작성 전 각 파트별 아래 파일을 전달받은 후 적용하려 한다.
| 의존성 | 실제 파일/값 |
|---|---|
| package.json |
install, build, start, lint, typecheck, test scriptpackageManager : [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 |
| 의존성 | 실제 파일/값 |
|---|---|
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 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가 공동 검토한다.
- BuildKit multi-stage로 build stage와 runtime stage를 분리한다.
- Base image tag를 major만 지정하지 않고 승인된 버전 또는 digest로 고정한다.
- Runtime stage에는 소스 전체·컴파일러·package manager cache·test fixture를 복사하지 않는다.
- Secret,
.env, 개인 키, 로컬 캐시, 모델 실험 결과는.dockerignore로 차단한다.
- 캐시를 통해 빌드 속도 최적화
- 이미지 크기 감소
# 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>"]
# 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"]
# 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;"]
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
| 항목 | 값 |
|---|---|
| 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 사용 |
| 항목 | 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 |
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 실행·결과 조회 |
moyeota/nginx, moyeota/fe, moyeota/be, moyeota/ai
이미지는 환경-<sha 태그> 형식으로 푸시 진행한다.
| 논리 이름 | 이름 예시 | 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/
|
자동 삭제 없음 |
/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
-
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 환경 이름으로만 사용한다.
별도의 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을 먼저 생성한다.
- 하나의 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병합 권한은 제한한다.
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
- 작업 브랜치 →
develop: Squash merge, 여러 개의 커밋을 하나의 커밋으로 합친 새로운 커밋을develop브랜치에 추가하는 방식 - release →
main: Merge commit 또는 PR 병합으로 출시 경계 보존 - release →
develop: QA 수정사항 동기화 - hotfix →
main: Merge 후 Patch 버전 Tag 생성 - hotfix →
develop: 반드시 병합하여 수정사항 유실 방지
vMAJOR.MINOR.PATCH
v1.0.0
v1.0.1
v1.1.0
v2.0.0
- MAJOR: 호환성이 깨지는 변경
- MINOR: 하위 호환 신규 기능
- PATCH: 버그 수정 및 긴급 수정
- 운영 배포 Tag는
main의 병합 커밋에 생성한다. - 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로 승격한다.
대상 브랜치가 develop, release/*, main이면 다음을 수행한다.
- 단위 테스트
- Lint 및 Type Check
- Docker Build
- 이미지 취약점 스캔
- Compose 설정 검증
- Docker build 검증
PR 단계에서는 EC2 배포를 수행하지 않는다.
- FE, BE, Nginx, AI 이미지를 빌드한다.
- 이미지에
dev-<sha7>Tag를 부여한다. - ECR에 Push한다.
- Push 결과에서 각 이미지 Digest를 취득한다.
- Dev Release Manifest에 Digest를 기록한다.
- EC2에 배포한다.
- Health Check와 Smoke Test를 실행한다.
- 실패하면 직전 성공 Digest로 Rollback한다.
Prod는 다음 조건을 모두 만족해야 한다.
-
main에 정식 버전 Tag 존재 - GitHub Actions Prod Environment 승인 완료
- Dev 또는 QA Smoke Test 성공
- 배포할 이미지 Digest 확정
- Prod 배포 전 MySQL 백업 성공
- Release Manifest 검증 성공
- 운영 장애 Ticket을 생성한다.
-
main에서hotfix/*를 생성한다. - 수정 범위를 장애 해결에 필요한 최소 범위로 제한한다.
- PR과 CI를 통과시킨다.
- 승인 후
main에 병합한다. - Patch 버전 Tag를 생성한다. 예:
v1.0.1 - Prod에 동일 Digest를 배포한다.
- hotfix를
develop과 활성 release에 병합한다. - 장애 원인, 영향 범위, 복구 방법을 Ticket에 기록한다.
작업 브랜치
→ develop
→ Dev 자동 배포
→ Dev EC2에서 수동 QA 진행
→ QA 승인 후 main 머지
→ Dev 이미지의 digest 유지
→ prod-vX.Y.Z Tag로 승격
→ 승인 후 Prod 배포
V1 기간 동안 CI 파이프라인 구축 목표는 다음 두가지를 생각해서 구성하려고 한다.
- 빠른 CICD 환경 설정
- 개발자들 간의 코드 병합 문제를 최소화하기 위해 세부적인 브랜치 정책 도입
- 머지데이 발생하지 않도록 작은 PR 단위의 통합 과정
- CI 이후 개발자의 판단 과정 없이 브랜치별 자동 배포 정책
이외에 더 고민해야하는 상황은 우선적으로 진행하지 않고 운영자의 다음 단계의 과제로 남겨 둔다.
프론트와 백엔드, AI는 각각 테스트 단계가 다르기 때문에 CICD 파이프라인이 다르다. 따라서 먼저 공통으로 적용되는 단계를 정의하고, 각 파트별 V1에서 진행하는 단계를 정의한다.
[2단계 CI 파이프라인설계] 에서 정의한 실패 유형별 처리에서 각 단계별로 실패 처리와 재시도 정책을 구체화해 정의한다.
실패 처리와 재시도 정책을 통해 통합과 배포 과정을 자동화하는 과정에서 사람이 판단하고 개입하는 지점을 줄이기 위함에 있다.
- API 계약 테스트 실패 : PR merge 차단 후 문제 조합 기록 후 담당자에게 디스코드 웹 훅으로 전달 프론트, 백엔드, AI에 적용한다.
- 통합 테스트 실패 : PR merge 차단 후 문제 조합 기록 후 담당자에게 디스코드 웹 훅으로 전달 백엔드와 AI에 적용한다.
- 빌드 실패 : PR merge와 Dev 배포 실행 차단
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)
# 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 하지 않는다.
- 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"
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를 추가한다.
- name: Install dependencies
run: pnpm install --frozen-lockfile프론트의 패키지 매니저는 pnpm이기 때문에 pnpm으로 의존성 파일을 설치한다.
CICD 정책에서 이미지 빌드 시 환경변수를 포함하지 않고 배포 시 런타임 환경변수를 주입하는 구조를 선택한다. Github Actions가 CI 실행에 필요한 환경변수는 레포지토리에 변수로 등록한다.
AWS_REGIONECR_REGISTRYECR_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 }}
환경변수 관리 방법에 대해서 다른 방법이 있는지 추가 과제로 진행한다.
프론트엔드 CI 파이프라인에서 테스트 단계는 다음과 같이 정의한다.
- lint 테스트
- Unit 테스트
- Chromatic 테스트
- API 계약 테스트
추가적으로 E2E 테스트는 다음 단계의 과제로 진행한다.
프론트엔드에서는 통합 테스트를 진행하지 않는다. 프론트와 백엔드는 다른 레포에서 진행되며, 통합 테스트를 추가하게 되면 CI 파이프라인의 빌드 시간이 늘어나 대기 시간이 발생된다. 또한 프론트엔드의 경우 E2E 테스트로 충분히 오류를 감지할 수 있기 때문에 통합 테스트는 따로 추가하지 않을 예정이다.
.oxlintrc.json 에 oxlint 규칙을 지정하고 package.json에 "lint": "oxlint ." 을 정의한다.
- name: Run oxlint
run: pnpm lint참고자료: Rust로 다시 쓴 JavaScript 도구 — OXC가 ESLint보다 100배 빠른 이유와 그 의미
vitest.config.ts 에 tests/unit/**/*.test.tsx 파일 12개를 대상으로 단위테스트를 진행한다.
- name: Unit test
run: pnpm test참고자료: techblog.woowahan.com
현재 프론트엔드 팀은 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 테스트
- 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[@]}" \빌드 실행 시간 최적화는 이후 추가 과제로 빌드 시간을 측정 후 최적화하는 작업을 진행한다.
각 단계에서 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
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를 추가한다.
- name: Install dependencies and run unit tests
run: ./gradlew --no-daemon testCICD 정책에서 이미지 빌드 시 환경변수를 포함하지 않고 배포 시 런타임 환경변수를 주입하는 구조를 선택한다. Github Actions가 CI 실행에 필요한 환경변수는 레포지토리에 변수로 등록한다.
AWS_REGIONECR_REGISTRYECR_REPOSITORYJAVA_VERSION-
AWS_ROLE_TO_ASSUME: secret
env:
AWS_REGION: ${{ vars.AWS_REGION }}
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
- 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 \
.
| 컨테이너 기동 실패 | 환경변수·Secret 누락, 포트 충돌, 애플리케이션 초기화 오류 | 외부 트래픽 차단, 로그 확인 후 재배포 또는 직전 이미지 Rollback |
| Startup Probe 실패 | 애플리케이션 기동 지연, DB 연결 실패, 초기화 로직 오류 | 기동 상태 확인, 설정 수정 또는 이전 정상 Release로 Rollback |
| Readiness Probe 실패 | 필수 DB 연결 실패, Migration 미완료, 필수 의존성 장애 | 트래픽 전달 중지, 원인 확인 후 재기동·재배포 |
| Health Check 실패 | 프로세스 중단, 애플리케이션 포트 미응답, 컨테이너 비정상 상태 | 컨테이너 재시작, 반복 실패 시 배포 중단 및 Rollback |
