Grovarc 개발기 #3 — Phase 1 인프라 전체 세팅
앱 코드가 없어도 인프라는 먼저 잡아둘 수 있다. 실제 서비스를 배포하기 전에 틀을 만들어두는 작업이다. Dockerfile부터 Prometheus 알럿까지, Phase 1 전체를 오늘 완성했다.
목표
- 4개 앱(web, api, ai, mcp) Dockerfile 작성
- 로컬 개발 환경 docker-compose 구성
- GitHub Actions CI 파이프라인 구축
- Terraform AWS 인프라 코드 작성
- Kubernetes 매니페스트 작성
- Prometheus + Grafana 모니터링 설정
핵심 구현 포인트
1) 멀티 스테이지 Dockerfile — 4개 앱
각 앱 특성에 맞게 베이스 이미지를 선택했다.
| 앱 | 빌드 이미지 | 런타임 이미지 |
|---|---|---|
| web | node:22-alpine | node:22-alpine |
| api | gradle:8.7-jdk21 | eclipse-temurin:21-jre-alpine |
| ai | python:3.12-slim | python:3.12-slim |
| mcp | oven/bun:1 | oven/bun:1-slim |
전 앱에 비루트 유저 실행을 적용했다. 컨테이너가 root로 실행되면 호스트 시스템에 대한 잠재적 권한 상승 위험이 있기 때문이다.
# api/Dockerfile — 비루트 유저 패턴
RUN addgroup --system --gid 1001 spring && \
adduser --system --uid 1001 --ingroup spring spring
USER spring
2) docker-compose — 로컬 개발 인프라
앱 서비스는 주석 처리하고 인프라 서비스만 올려두는 방식을 택했다. 앱 구현 전에 컨테이너를 올려도 의미가 없기 때문이다.
# 인프라만 바로 실행 가능
docker compose up -d
# → PostgreSQL(pgvector), Redis, MongoDB, Kafka, Zookeeper 실행
# 앱 구현 후엔 주석 해제
# api, ai, web 서비스 주석 → 해제 후 통합 테스트
각 서비스에 healthcheck를 달아서 의존성 순서를 보장했다.
3) GitHub Actions CI — 소스 없어도 실패 안 나게
아직 앱 소스가 없는 상태에서 CI를 세팅하면 Dockerfile 빌드가 실패한다. package.json, build.gradle.kts, requirements.txt 같은 sentinel 파일 존재 여부로 스킵 처리했다.
- name: Check if app source exists
id: check
run: |
if [ -f "apps/${{ matrix.app }}/${{ matrix.sentinel }}" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Build Docker image
if: steps.check.outputs.exists == 'true'
uses: docker/build-push-action@v6
초기엔 Dockerfile 존재 여부로만 체크했는데, Dockerfile은 있고 소스가 없어서 COPY 단계에서 실패했다. sentinel 파일 방식으로 수정해서 해결.
4) Terraform — 모듈 기반 구조
환경(dev/prod)별로 같은 모듈을 재사용하는 구조로 설계했다.
infra/terraform/
├── environments/
│ ├── dev/main.tf ← 모듈 조합
│ └── prod/main.tf ← 동일 모듈, 다른 변수
└── modules/
├── vpc / eks / rds
├── redis / kafka
└── mongodb / s3
RDS 파라미터 그룹에 shared_preload_libraries = vector를 추가해 pgvector를 활성화했다.
tfstate는 S3 + DynamoDB 백엔드로 구성. bootstrap.sh로 최초 1회 생성 후 사용한다.
5) Prometheus + Grafana — 배포 전 알럿 설계
실제 배포 전이지만 알럿 기준을 미리 잡아둔다. 나중에 수치 조정은 쉽지만, 어떤 알럿이 필요한지 결정하는 게 어렵기 때문이다.
# 현재 알럿 3종
- APIHighErrorRate → 5xx 에러율 5% 초과 (2분 지속)
- APIHighLatency → p95 응답시간 1초 초과 (5분 지속)
- APIPodDown → 가용 Pod 0개 (1분 지속)
Grafana 대시보드는 ConfigMap으로 코드화해서 관리한다. UI에서 손으로 만든 대시보드는 재배포 시 날아가기 때문이다.
트러블슈팅 / 고민 포인트
Docker Build CI 실패 — COPY 대상 파일 없음
- 원인: Dockerfile은 있지만 소스가 없어서
COPY requirements.txt ./등이 실패 - 해결: sentinel 파일(package.json, build.gradle.kts 등) 존재 여부로 빌드 스킵 처리
Grafana 대시보드 ConfigMap 누락
- 원인:
kube-prometheus-stack.yaml에서grovarc-dashboardsConfigMap을 참조하는데 해당 매니페스트가 없었음 - 해결: 대시보드 JSON을 포함한 ConfigMap 매니페스트 별도 생성
Ingress deprecated 어노테이션
- 원인:
kubernetes.io/ingress.class: alb어노테이션은 구버전 방식 - 해결:
spec.ingressClassName: alb필드 방식으로 수정
결과
apps/{web,api,ai,mcp}/Dockerfile— 멀티 스테이지 + 비루트 유저docker-compose.yml— 로컬 인프라 5종 + healthcheck.github/workflows/ci.yml— PR 시 commitlint + 빌드 + Docker 검증infra/terraform/— AWS 인프라 모듈 7종infra/k8s/— Deployment, Service, Ingress, HPA, ConfigMap, Secretinfra/monitoring/— Prometheus 수집 설정, 알럿 룰, Grafana 대시보드- Phase 1 이슈 #8~#13 전부 완료 및 닫힘
다음 개선 아이디어
- Phase 2: Kotlin Spring Boot 프로젝트 세팅 (#20)
- JWT 인증 API 구현 (#21)
- 작업 로그 CRUD API + Kafka 이벤트 발행 (#22)