PetClinic on Amazon EKS Docker · Kubernetes 개인 프로젝트 · 2026.10 상태WEBWAS·DB PetClinic 열기 GitHub
PetClinic EKS 아키텍처 AWS Cloud · 서울 리전 (ap-northeast-2) 사용자 Route 53 ACM ECR CloudWatch Secrets Manager VPC 10.0.0.0/16 Public subnet × 2 (2a / 2c) ALBGateway API NAT Gateway Bastion Private subnet × 2 · EKS 노드 Amazon EKS 1.36 ng-system · 시스템 노드 2대 LB Controller External Secrets KEDA Karpenter Add-ons 6종 Karpenter 노드 (Spot) namespace: petclinic WEBNginx WASTomcat 11 DBMySQL 8.4 + log-agent KEDA 2~4 Probe 3종 HPA 2~4 StatefulSet ExternalSecret Seed Job CronJob Quota · PDB Kustomize Private subnet × 2 · DB RDS MySQL (prod)
이 단계에서 추가 이전 단계까지 구성 요청 흐름 연동 · 관리

소개

Spring PetClinic을 Amazon EKS에
단계적으로 배포한 프로젝트

Nginx(WEB)와 Tomcat(WAS)을 컨테이너 이미지로 만들고, MySQL과 함께 3-Tier로 EKS에 올렸습니다. 처음부터 전부 구성하지 않고 가장 단순한 형태를 먼저 띄운 다음, 하루에 한 주제씩 기능을 더했습니다.

기간2026년 10월
형태개인 프로젝트
주요 기술EKS, Gateway API, Kustomize, StatefulSet, RDS, HPA, KEDA, Karpenter, External Secrets, Route 53, ACM

진행 순서

Stage 13-Tier 기본 구성
Stage 2DB 영속성, 최소 권한, dev/prod 분리
Stage 3파드 안정성과 자원 관리
Stage 4오토스케일링 (HPA, KEDA, Karpenter)
Stage 5Secrets Manager, Route 53 + HTTPS

전체 구조

최종 아키텍처

사용자 요청은 Route 53 → ALB(HTTPS) → WEB → WAS → DB 순서로 흐릅니다.

네트워크VPC 1개, 가용 영역 2개. Public / Private(노드) / Private(DB) 서브넷 각 2개
노드시스템 컨트롤러는 관리형 노드그룹 ng-system, 애플리케이션은 Karpenter가 만든 Spot 노드
환경Kustomize dev(클러스터 내부 MySQL)와 prod(RDS). WAS 설정은 두 환경이 같음
확장WAS는 CPU(HPA), WEB은 ALB 요청 수(KEDA), 노드는 대기 중인 파드(Karpenter)
보안DB 계정 최소 권한, 비밀번호는 Secrets Manager, AWS 권한은 Pod Identity / IRSA로 컨트롤러별 분리

진행하면서 지킨 원칙

  • 한 번에 하나씩 추가하고, 추가할 때마다 동작을 확인한다.
  • 설정과 비밀번호는 이미지에 넣지 않고 실행 시점에 주입한다.
  • 권한은 필요한 만큼만 준다.

Stage 1

3-Tier 기본 구성

부가 기능 없이 ALB → WEB → WAS → DB가 연결되는 것부터 확인했습니다.

구성

  • 콘솔에서 VPC, 서브넷 6개, IGW, NAT Gateway, Bastion 생성 후 eksctl로 클러스터 생성
  • WAS 이미지: Maven(-P MySQL) 빌드 후 Tomcat 11 이미지로 복사하는 멀티 스테이지 빌드
  • 외부 노출: Ingress 대신 Gateway API (GatewayClass, Gateway, HTTPRoute)
  • ALB 설정은 LoadBalancerConfiguration, 대상 그룹 설정은 TargetGroupConfiguration으로 분리
문제 → 해결
WAS가 DB에 붙지 않았던 원인이 세 가지였습니다.
1) catalina.sh가 JAVA_OPTS를 셸에서 다시 해석해 &, #이 깨짐 → JDK_JAVA_OPTIONS 사용
2) 설정값을 바꿀 방법이 필요 → data-access.properties를 Secret으로 만들어 subPath로 교체
3) Maven 리소스 필터링이 XML의 ${jdbc.url}까지 빌드 시점 값으로 바꿔 둠 → 빌드 후 플레이스홀더 복원

Stage 2

DB 영속성, 최소 권한, 환경 분리

  • StatefulSet + EBS gp3: DB 파드를 지워도 같은 이름, 같은 볼륨으로 다시 붙습니다. 볼륨은 파드가 배치될 AZ에 만들어지도록 WaitForFirstConsumer 사용
  • Seed Job: 테이블 생성과 초기 데이터는 관리자 계정으로 한 번만 실행. 애플리케이션 계정은 SELECT, INSERT, UPDATE, DELETE만 허용
  • Kustomize: 공통은 base, DB만 overlays/dev(StatefulSet)와 overlays/prod(RDS)로 분리
  • ExternalName Service: 서비스 이름 petclinic-db는 그대로 두고 prod에서는 RDS 주소를 가리키게 함
확인한 것
DB 파드를 삭제해도 데이터 유지 · 애플리케이션 계정의 DROP TABLE 거부 · WAS 수정 없이 RDS로 전환

Stage 3

파드 안정성과 자원 관리

Probestartup, liveness는 DB를 거치지 않는 정적 리소스로, readiness는 매번 DB를 조회하는 /owners로 검사. /vets는 캐시 때문에 DB가 멈춰도 200을 반환해서 쓰지 않음
자원requests / limits, JVM 힙 비율(MaxRAMPercentage=75), LimitRange, ResourceQuota
가용성replicas 2, AZ·노드 분산, 롤링 업데이트 maxUnavailable: 0, PDB minAvailable: 1
로그WEB 파드에 네이티브 사이드카(initContainers + restartPolicy: Always)로 접속 로그 수집
배치 작업5분마다 DB 현황을 집계하는 CronJob (중복 실행 금지, 실행 시간 제한)
확인한 것
DB를 멈추면 WAS는 트래픽에서 빠지지만 재시작하지 않음 · 노드를 비우는 동안에도 응답 유지 · 파드가 1개일 때는 PDB가 drain을 막음

Stage 4

오토스케일링

WASHPA. CPU 사용률 50%(requests 기준), 2~4개
WEBKEDA. CloudWatch의 ALB 요청 수(파드당 분당 60건) + 평일 업무시간 최소 3개
노드Karpenter. 대기 중인 파드에 맞는 인스턴스를 Spot 우선으로 생성하고, 남는 노드는 한 대씩 정리

함께 바꾼 것

  • ng-system에 Taint를 걸어 시스템 컨트롤러와 애플리케이션 노드를 분리
  • HPA와 KEDA가 파드 수를 관리하므로 Deployment에서 replicas 제거
  • KEDA의 CloudWatch 권한은 EKS Pod Identity로 필요한 동작 3개만 허용
관찰
HPA는 수십 초 안에 반응했지만 KEDA는 CloudWatch 지표 지연 때문에 2~3분이 걸렸습니다. 예측 가능한 트래픽은 cron 트리거로 미리 늘려 두는 방식이 맞다고 판단했습니다.

Stage 5

Secrets Manager, Route 53 + HTTPS

Secrets Manager + External Secrets Operator

  • Git에 평문으로 있던 DB 비밀번호를 Secrets Manager(petclinic/dev/db)로 이동
  • External Secrets가 값을 읽어 기존과 같은 이름의 Secret을 만들어 주므로 파드 매니페스트는 그대로
  • WAS 설정 파일은 템플릿으로 조립하고 비밀번호 한 줄만 Secrets Manager 값으로 채움
  • 읽기 권한은 Pod Identity로 petclinic/* 비밀에만 허용, 이후 애플리케이션 비밀번호 교체까지 진행

Route 53 + ACM + HTTPS

  • 가비아에서 구입한 zzgg2.cloud의 네임서버를 Route 53으로 위임
  • ACM 인증서(zzgg2.cloud, *.zzgg2.cloud)를 DNS 검증으로 발급
  • Gateway에 HTTPS 리스너 추가, HTTP는 HTTPRoute 리다이렉트(301)로 HTTPS로 이동
  • Route 53 Alias 레코드로 도메인을 ALB에 연결. Gateway는 그대로 두어 ALB 주소 유지
문제 → 해결
Secret을 External Secrets로 옮길 때 잘못된 값이 들어가 새 WAS가 기동하지 못했습니다. 기존 WAS는 예전 값으로 계속 동작하고 있었고(maxUnavailable: 0), 실행 중인 파드에서 원래 값을 확인해 복구했습니다. subPath로 마운트한 파일은 Secret이 바뀌어도 갱신되지 않아 재시작이 필요하다는 것도 이때 확인했습니다.

정리

정리

배운 점

  • 파드가 Running이어도 서비스가 되는 상태는 아닐 수 있습니다. Probe를 어떻게 나누느냐에 따라 장애 범위가 달라졌습니다.
  • 설정을 실행 시점에 주입하려면 빌드 도구가 어떤 값을 미리 넣어 두는지 결과물을 직접 열어 봐야 했습니다.
  • 스케일링 기준은 지표가 들어오는 시점까지 고려해야 했습니다.

다음에 해 보고 싶은 것

  • Argo CD로 배포 자동화 (지금은 Bastion에서 직접 적용)
  • Flyway로 스키마 변경 이력 관리
  • Prometheus, Grafana로 지표 수집과 대시보드 구성

github.com/ZZGG2/petclinic-k8s-project

web v1