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개 앱

각 앱 특성에 맞게 베이스 이미지를 선택했다.

빌드 이미지런타임 이미지
webnode:22-alpinenode:22-alpine
apigradle:8.7-jdk21eclipse-temurin:21-jre-alpine
aipython:3.12-slimpython:3.12-slim
mcpoven/bun:1oven/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-dashboards ConfigMap을 참조하는데 해당 매니페스트가 없었음
  • 해결: 대시보드 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, Secret
  • infra/monitoring/ — Prometheus 수집 설정, 알럿 룰, Grafana 대시보드
  • Phase 1 이슈 #8~#13 전부 완료 및 닫힘

다음 개선 아이디어

  • Phase 2: Kotlin Spring Boot 프로젝트 세팅 (#20)
  • JWT 인증 API 구현 (#21)
  • 작업 로그 CRUD API + Kafka 이벤트 발행 (#22)