풀스택 앱 EKS 배포하기
6부 캡스톤 — 책의 마지막 챕터입니다. 리액트의 Next.js (App Router + RSC + Server Actions) 앱과 모던 파이썬의 FastAPI (SQLAlchemy 2.x + Pydantic v2) 앱을 같은 TODO 도메인 위에서 한 EKS 클러스터에 함께 배포합니다. Terraform + Karpenter + IRSA + ALB Controller + ExternalDNS + cert-manager의 클러스터 셋업부터, RDS + External Secrets + RDS IAM auth의 DB 연동, Helm + ArgoCD ApplicationSet의 환경별 배포, Prometheus + Grafana + Loki + OpenTelemetry의 옵저버빌리티, HPA + Karpenter의 오토스케일링, k6 부하 테스트 + OpenCost 비용 추정, 그리고 26장 + 30장 운영 사이클의 적용까지를 13개의 PR로 한 사이클로 다룹니다. 1~30장의 모든 도구가 한 시스템 안에서 어떻게 맞물리는지의 시야가 본 캡스톤에서 손에 들어옵니다.
본 책의 마지막 챕터입니다. 6부 캡스톤은 1~30장의 모든 도구가 한 시스템 안에서 어떻게 맞물리는지를 한 프로젝트로 묶는 종합 실습입니다. 가상의 회사가 아니라 본 시리즈의 다른 두 책의 결과물을 그대로 입력으로 가져옵니다 — 리액트 6부의 Next.js TODO 앱과 모던 파이썬 4부의 FastAPI TODO 백엔드가 같은 도메인 위에서 동작하고 있습니다. 본 챕터에서는 그 둘을 한 EKS 클러스터 위에 함께 배포해, K8s 트랙의 모든 결을 한 시스템 안에서 다시 만납니다.
이번 챕터의 목표는 다음과 같습니다.
https://todo.example.com에 Next.js가,https://api.todo.example.com에 FastAPI가 떠 있는 상태- RDS PostgreSQL이 백업 · Multi-AZ · External Secrets와 결합된 상태
- GitHub push → ECR → ArgoCD ApplicationSet 자동 sync의 한 사이클
- Prometheus + Grafana + Loki + OpenTelemetry의 옵저버빌리티 스택이 두 앱을 같은 방향으로 관측하는 상태
- HPA + Karpenter가 트래픽 변동에 자동 반응하는 상태
- 한 달 약 $80~$120의 운영 비용 가설이 OpenCost로 검증된 상태
진행은 13개의 PR 단위입니다. 각 PR이 다음 PR의 입력이 되는 누적 구조이고, 한 PR의 변경량은 의도적으로 작게 두어 리뷰 가능한 크기로 유지합니다.
목표의 아키텍처 #
[Browser]
|
| HTTPS (Route 53 + ACM)
v
[ALB] -- AWS Load Balancer Controller
|
|-- / -> [Next.js Pod x N] (SSR + RSC + Server Actions)
`-- /api/* -> [FastAPI Pod x M] (REST + Pydantic v2)
|
| PgBouncer
v
[RDS PostgreSQL] (Multi-AZ)
^
|
[External Secrets] <- [AWS Secrets Manager]
^
| IRSA
[ServiceAccount]이 그림이 본 챕터의 13 PR이 도달하는 최종 형태입니다. 그림의 각 화살표가 본 책의 한 챕터 이상에서 풀린 결입니다 — 이번 챕터는 그 결을 한 시스템으로 묶는 단계입니다.
PR #1 — 도메인과 아키텍처 결정 #
첫 PR은 코드 없이 ADR (Architecture Decision Record) 한 장입니다.
# ADR-0001: 풀스택 todo 시스템의 K8s 배포 아키텍처
## 컨텍스트
Next.js (App Router + RSC) + FastAPI + PostgreSQL 의 todo 시스템을
운영 환경에 배포해야 함.
## 옵션
1. ECS Fargate (매니지드 컨테이너)
2. EKS (Kubernetes)
3. Lambda + RDS (서버리스)
## 결정
EKS 채택.
## 근거
- 두 앱 (Next.js + FastAPI) 의 결이 다르고, 라이프사이클 격리 필요
- HPA · Karpenter 의 오토스케일링 결이 트래픽 패턴에 맞음
- GitOps (ArgoCD) 의 운영 표준 모델 활용
- 본 책의 1 ~ 30 장 모든 도구의 종합 검증
## 결과
- 한 달 $80 ~ $120 의 비용 가설 (28장 모델로 검증 예정)
- 운영 캘린더 (26장) 의 정기 사이클 적용
- AWS 책의 ECS Fargate 챕터와 비교 학습 가능AWS의 같은 캡스톤이 ECS Fargate 노선을 다루므로, 두 책을 비교하면 “K8s vs 매니지드 컨테이너"의 운영상 차이가 명확히 보입니다. 본 챕터는 K8s 선택 이후의 한 사이클을 본격 다룹니다.
PR #2 — EKS 클러스터 신규 셋업 #
21장 EKS 클러스터 셋업의 Terraform 매니페스트가 입력입니다. 본 캡스톤에서는 한 차이만 둡니다 — Karpenter를 처음부터 도입 합니다.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
# ... 21장 그대로
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "todo-${var.env}"
cluster_version = "1.32"
enable_irsa = true
cluster_addons = {
coredns = { most_recent = true }
kube-proxy = { most_recent = true }
vpc-cni = { most_recent = true }
aws-ebs-csi-driver = {
most_recent = true
service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
}
}
# 최소 노드만 ON_DEMAND 로 유지, 나머지는 Karpenter 가 즉석에서
eks_managed_node_groups = {
system = {
desired_size = 2
min_size = 2
max_size = 3
instance_types = ["t3.medium"]
capacity_type = "ON_DEMAND"
labels = { role = "system" }
taints = [{
key = "system"
value = "true"
effect = "NO_SCHEDULE"
}]
}
}
}
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
cluster_name = module.eks.cluster_name
irsa_oidc_provider_arn = module.eks.oidc_provider_arn
}system 노드 그룹은 Karpenter · CoreDNS · 모니터링 스택 같은 시스템 컴포넌트만 위치하고, 애플리케이션 (Next.js / FastAPI) 워크로드는 Karpenter가 띄우는 노드에 가는 패턴입니다. 13장 오토스케일링의 Karpenter 모델 + 28장 비용 최적화 §“Karpenter — Cluster Autoscaler와의 결정 트리"의 결합입니다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["t", "m", "c"]
- key: karpenter.k8s.aws/instance-cpu
operator: In
values: ["2", "4", "8"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
budgets:
- nodes: "10%"
duration: 10m
schedule: "0 9 * * mon-fri"disruption.budgets가 30장 업그레이드 전략의 blast radius 결입니다 — 평일 업무 시간에 한 번에 10% 이하의 노드만 교체합니다.
보조 컴포넌트 한 묶음 #
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system --set clusterName=todo-prod --set serviceAccount.create=false
helm install external-dns external-dns/external-dns \
-n external-dns --create-namespace \
--set provider=aws --set "domainFilters[0]=todo.example.com"
helm install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace --set installCRDs=true22장 앱 배포 골격 §“cert-manager와 external-dns"에서 짚었던 셋업 그대로입니다.
PR #3 — 네임스페이스 / RBAC / NetworkPolicy 골격 #
워크로드를 띄우기 전에 격리 골격을 잡아 둡니다.
---
apiVersion: v1
kind: Namespace
metadata:
name: todo-frontend
labels:
team: web
env: prod
role: frontend
---
apiVersion: v1
kind: Namespace
metadata:
name: todo-backend
labels:
team: backend
env: prod
role: backend
---
apiVersion: v1
kind: Namespace
metadata:
name: todo-data
labels:
team: backend
env: prod
role: data세 네임스페이스 분리 — frontend / backend / data — 가 본 캡스톤의 격리 단위입니다. 7장 Namespace와 라벨의 라벨 표준 (team / env / role)이 25장 모니터링 · 알람의 그루핑 키이자 28장의 비용 분배 키로 활용됩니다.
NetworkPolicy — 격리의 본격 #
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: todo-backend-ingress
namespace: todo-backend
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: todo-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
role: frontend
- namespaceSelector:
matchLabels:
role: backend # 같은 backend 의 다른 워크로드도 허용
ports:
- port: 800014장 RBAC / NetworkPolicy / ResourceQuota의 NetworkPolicy 모델이 본격적인 격리로 이어집니다. frontend가 직접 RDS에 못 가고, 반드시 backend를 거치는 강제 흐름입니다.
ResourceQuota — 팀별 한도 #
apiVersion: v1
kind: ResourceQuota
metadata:
name: todo-backend-quota
namespace: todo-backend
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
persistentvolumeclaims: "5"14장의 ResourceQuota가 멀티 팀 환경의 비용 격리의 첫 보호선입니다.
PR #4 — PostgreSQL RDS + External Secrets #
23장 DB 연동의 Terraform 매니페스트가 거의 그대로 들어옵니다. 차이는 dev에서 Aurora Serverless v2를 옵션으로 두는 결입니다.
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
identifier = "todo-${var.env}"
engine = "postgres"
engine_version = "16.3"
major_engine_version = "16"
instance_class = var.env == "prod" ? "db.t4g.small" : "db.t4g.micro"
allocated_storage = 20
manage_master_user_password = true
multi_az = var.env == "prod"
backup_retention_period = var.env == "prod" ? 30 : 7
performance_insights_enabled = true
deletion_protection = var.env == "prod"
}비용 가설을 작게 잡기 위해 인스턴스 클래스를 db.t4g.small로 둡니다 — 23장의 db.m6g.large보다 작은 옵션입니다. todo 도메인의 부하가 작아 충분합니다.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: todo-api-db
namespace: todo-backend
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: todo-api-db
template:
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@pgbouncer.todo-backend.svc:5432/todo?sslmode=disable"
data:
- secretKey: username
remoteRef:
key: rds!cluster-todo-prod
property: username
- secretKey: password
remoteRef:
key: rds!cluster-todo-prod
property: password23장의 매니페스트 그대로이고, 29장 시크릿 운영 §“비밀번호 0"의 RDS IAM auth는 본 캡스톤에서는 옵션으로 둡니다 — todo의 트래픽이 작아 PgBouncer + 비밀번호 모델로 충분합니다.
PR #5 — FastAPI 백엔드 배포 #
모던 파이썬 4부 캡스톤의 FastAPI todo 백엔드가 입력입니다. (modern-python은 구 파이썬 강좌와의 차별 의미를 살려 “모던” 접두어를 유지합니다.) 컨테이너화는 본 책의 범위 밖이지만, Dockerfile의 핵심을 짚어 둡니다.
FROM python:3.13-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev
FROM python:3.13-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ src/
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]Deployment #
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-api
namespace: todo-backend
labels:
app.kubernetes.io/name: todo-api
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: todo-api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app.kubernetes.io/name: todo-api
spec:
serviceAccountName: todo-api
containers:
- name: api
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/todo-api:1.0.0
ports:
- containerPort: 8000
name: http
envFrom:
- secretRef:
name: todo-api-db
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 30
periodSeconds: 10
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 6022장 앱 배포 골격의 표준 매니페스트에 30장의 graceful shutdown 결 (preStop + terminationGracePeriodSeconds)까지 결합한 모양입니다.
ServiceAccount + IRSA #
apiVersion: v1
kind: ServiceAccount
metadata:
name: todo-api
namespace: todo-backend
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/todo-prod-api
automountServiceAccountToken: false # 16장의 보안 결16장 RBAC / ServiceAccount 깊이의 IRSA + 29장 시크릿 운영 §“automountServiceAccountToken: false"의 보안 결이 한 매니페스트 안에 있습니다.
PR #6 — Next.js 프론트 배포 #
리액트 6부 캡스톤의 Next.js TODO 앱이 입력입니다. App Router + RSC + Server Actions의 모델이 K8s 안에서는 다음과 같이 동작합니다.
[Browser]
|
| HTTPS
v
[ALB]
|
v
[Next.js Pod] -- Node.js 서버 (next start)
|
| RSC 렌더링 시 fetch
v
[todo-api Service] -- ClusterIP, FastAPI 가리킴
|
v
[todo-api Pod]Server Actions는 Next.js Pod 안에서 그대로 실행됩니다. 외부 API 호출이 필요한 경우 같은 클러스터 안의 todo-api Service로 부릅니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-web
namespace: todo-frontend
spec:
replicas: 2
template:
spec:
serviceAccountName: todo-web
containers:
- name: web
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/todo-web:1.0.0
ports:
- containerPort: 3000
name: http
env:
- name: TODO_API_URL
value: "http://todo-api.todo-backend.svc.cluster.local:80"
- name: NODE_ENV
value: "production"
resources:
requests:
cpu: 200m
memory: 256Mi # SSR + RSC 의 메모리 가설
limits:
cpu: 1
memory: 512MiNext.js Pod의 메모리 가설은 11장 자원 요청과 한도의 측정 결로 잡습니다 — SSR + RSC의 한 요청당 메모리 점유가 일정 정도 누적되므로, requests를 256 Mi로 두는 게 보수적인 출발점입니다. 28장 비용 최적화의 VPA recommendation으로 한 달 뒤 적정값에 수렴시킵니다.
PR #7 — Ingress + ALB #
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: todo
namespace: todo-frontend
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
alb.ingress.kubernetes.io/group.name: todo
external-dns.alpha.kubernetes.io/hostname: "todo.example.com,api.todo.example.com"
spec:
rules:
- host: todo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todo-web
port:
number: 80
- host: api.todo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todo-api.todo-backend
port:
number: 80alb.ingress.kubernetes.io/group.name: todo가 결정적 — 두 호스트가 같은 ALB 한 대를 공유 합니다. 28장 비용 최적화 §“ALB의 LCU"에서 짚었던 비용 절감 패턴이 본 절에서 직접 적용됩니다.
external-dns가 두 호스트의 A 레코드를 Route 53에 자동 등록하고, ACM 인증서는 와일드카드 (*.todo.example.com)로 한 장이면 충분합니다. 22장의 Ingress 매니페스트가 멀티 호스트 패턴으로 확장된 모양입니다.
PR #8 — Helm 차트로 묶기 #
지금까지 적은 매니페스트를 Helm 차트 두 개로 묶습니다.
charts/
├── todo-web/
│ ├── Chart.yaml
│ ├── values.yaml
│ ├── values-dev.yaml
│ ├── values-prod.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── hpa.yaml
│ └── pdb.yaml
├── todo-api/
│ ├── Chart.yaml
│ ├── values.yaml
│ ├── values-dev.yaml
│ ├── values-prod.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ ├── externalsecret.yaml
│ ├── hpa.yaml
│ ├── pdb.yaml
│ └── servicemonitor.yaml
└── todo-infra/
├── Chart.yaml
└── templates/
├── namespaces.yaml
├── networkpolicy.yaml
├── resourcequota.yaml
└── ingress.yaml세 차트의 분리가 핵심입니다.
todo-infra— 네임스페이스 · NetworkPolicy · ResourceQuota · Ingress. 두 앱이 공유하는 인프라.todo-api— backend의 모든 매니페스트.todo-web— frontend의 모든 매니페스트.
22장 앱 배포 골격 §“Helm 차트로 묶기"의 패턴이 멀티 앱 환경에서 어떻게 갈라지는지의 본격 적용입니다. Chart.yaml의 dependencies로 묶는 옵션도 있지만, 단순함을 위해 본 캡스톤은 평면 구조로 두고 ArgoCD ApplicationSet으로 통합합니다.
PR #9 — GitOps: ArgoCD ApplicationSet #
20장 GitOps + 24장 CI / CD 파이프라인의 모델이 ApplicationSet 한 매니페스트로 정리됩니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: todo
namespace: argocd
spec:
generators:
- matrix:
generators:
- list:
elements:
- app: todo-infra
- app: todo-api
- app: todo-web
- list:
elements:
- env: dev
cluster: https://kubernetes.default.svc
- env: prod
cluster: https://kubernetes.default.svc
template:
metadata:
name: '{{`{{.app}}`}}-{{`{{.env}}`}}'
spec:
project: todo
source:
repoURL: https://github.com/myorg/todo-manifests.git
targetRevision: main
path: charts/{{`{{.app}}`}}
helm:
valueFiles:
- values.yaml
- values-{{`{{.env}}`}}.yaml
destination:
server: '{{`{{.cluster}}`}}'
namespace: todo-{{`{{.app}}`}}
syncPolicy:
automated:
prune: true
selfHeal: truematrix generator가 3 앱 × 2 환경 = 6개의 Application을 한 매니페스트로 자동 생성합니다. dev는 자동 sync, prod는 ApplicationSet의 별도 인스턴스로 수동 sync 모드로 분기하는 게 운영의 표준이지만, 본 캡스톤은 단순화를 위해 둘 다 automated로 둡니다.
24장의 GitHub Actions OIDC + ECR push + 매니페스트 repo 자동 commit 사이클이 본 매니페스트의 입력입니다 — 코드 push 한 번에 dev / prod의 양쪽 환경이 자동 sync 됩니다.
PR #10 — 옵저버빌리티 #
19장 옵저버빌리티 + 25장 모니터링 · 알람의 kube-prometheus-stack 그대로 들어옵니다. 차이는 OpenTelemetry Collector를 추가해 두 앱의 트레이스를 묶는 결입니다.
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel
namespace: monitoring
spec:
mode: daemonset
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
prometheus:
endpoint: 0.0.0.0:8889
otlp/tempo:
endpoint: tempo.monitoring.svc:4317
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
exporters: [prometheus]Next.js의 OpenTelemetry SDK와 FastAPI의 OTel instrumentation이 같은 endpoint로 트레이스를 보내면, 두 앱을 가로지르는 한 요청의 전체 경로가 Tempo에서 보입니다. RSC 렌더링 시 fetch 호출이 FastAPI의 어느 핸들러를 거쳐 RDS까지 도달했는지가 한 트레이스 화면에서 추적됩니다.
ServiceMonitor + PrometheusRule #
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: todo-api
namespace: todo-backend
labels:
release: prometheus
spec:
groups:
- name: todo-api.golden-signals
rules:
- alert: TodoApiHighErrorRate
expr: |
sum(rate(http_requests_total{app="todo-api",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{app="todo-api"}[5m])) > 0.05
for: 5m
labels:
severity: critical
# ... latency, traffic, saturation 도 동일25장의 매니페스트 그대로이고, 동일한 룰이 todo-web에도 적용됩니다. 알람의 severity 라우팅은 25장의 Alertmanager 매니페스트가 그대로 들어옵니다.
PR #11 — 오토스케일링 #
13장 오토스케일링의 HPA와 28장의 Karpenter NodePool이 결합되어 두 단계의 자동 반응을 만듭니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: todo-api
namespace: todo-backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: todo-api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
behavior:
scaleUp:
stabilizationWindowSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300트래픽 증가
|
v
HPA: 30 초 만에 todo-api Pod 2 -> 5 -> 10 -> 20
|
| 노드의 자원 부족
v
Karpenter: 30 초 ~ 1 분 만에 새 노드 (spot 우선) 프로비저닝
|
v
Pending 이었던 Pod 가 새 노드에 스케줄링됨이 두 단계가 한 사이클로 굴러가는 모양이 K8s 오토스케일링의 목표입니다. 부하 테스트로 그 모양을 다음 PR에서 측정합니다.
PR #12 — 부하 테스트와 비용 추정 #
import http from "k6/http";
import { check } from "k6";
export const options = {
stages: [
{ duration: "2m", target: 50 },
{ duration: "5m", target: 200 },
{ duration: "2m", target: 500 },
{ duration: "5m", target: 500 },
{ duration: "2m", target: 0 },
],
};
export default function () {
const res = http.get("https://todo.example.com/api/todos");
check(res, {
"status is 200": (r) => r.status === 200,
"duration < 500ms": (r) => r.timings.duration < 500,
});
}k6 run k6/script.js측정할 결입니다.
- HPA scale-up의 응답 시간 — 트래픽 50 → 200 사이에서 Pod가 몇 초 만에 늘어나는가
- Karpenter의 노드 추가 시간 — Pod가 Pending에 머무른 시간
- P95 latency — 부하 정점 (500 VUs)에서 latency가 어떻게 변하는가
- 5xx 비율 — 부하 중 에러율이 25장의 임계치 (5%)를 넘는지
비용 검증 #
helm install opencost opencost/opencost \
-n opencost --create-namespaceOpenCost의 출력에서 한 달 가설 비용을 검증합니다.
| 항목 | 예상 (월) |
|---|---|
| EKS 컨트롤 플레인 | $73 |
| 노드 (system t3.medium × 2 ON_DEMAND) | $60 |
| 노드 (애플리케이션 spot 평균 1.5대) | $20 |
| RDS db.t4g.small Multi-AZ | $30 |
| ALB (1대, LCU) | $20 |
| NAT Gateway + 데이터 전송 | $35 |
| ECR / Route 53 / 기타 | $10 |
| 합계 | 약 $248 |
28장 §“청구서 review 체크리스트"의 각 항목을 본 실측치와 비교합니다. prod의 목표가 책의 표준 가이드 ($200~$300) 안에 들어왔는지가 검증 지표입니다.
학습용 환경에서는 다음 조정으로 한 달 $40~$80까지 줄일 수 있습니다.
- prod의 Multi-AZ RDS를 dev의 단일 AZ로
- ALB 한 대 (이미 공유)
- system 노드 그룹도 spot으로
- NAT Gateway를 Single NAT로
PR #13 — 운영 체크리스트 적용 #
마지막 PR은 26장 운영 체크리스트의 정기 캘린더와 30장 업그레이드 전략의 업그레이드 체크리스트를 본 시스템에 적용합니다.
# todo 시스템 운영 캘린더
## 매일
- Grafana 의 todo 대시보드 5 패널 점검
- Alertmanager 의 활성 알람 검토
## 매주
- ECR Trivy 스캔 결과 (todo-api, todo-web 모두)
- 신규 보안 패치 검토
## 매월
- OpenCost 의 팀 / 워크로드별 비용 1, 2, 3 위
- VPA recommendation 의 미반영 워크로드
- ArgoCD 의 OutOfSync 상태 점검
## 분기
- EKS 마이너 업그레이드 (30장의 13 단계)
- RDS Performance Insights 의 right-sizing 신호
- RBAC audit
- 복구 훈련 (PITR 시뮬레이션)
- kube-bench CIS 점검
## 반기
- 외부 보안 감사
- DR 시뮬레이션 (Velero restore)
## 연
- 클러스터 아키텍처 리뷰
- 매니페스트 현대화이 매니페스트가 git에 들어가는 게 본 캡스톤의 마지막 PR입니다. 코드뿐 아니라 운영 절차도 git의 단일 소스에 두는 게 GitOps의 본질적 목표입니다.
사후 회고 — 30장이 어떻게 묶였는가 #
13 PR을 거치며 본 책의 챕터들이 한 시스템 안에서 어떻게 맞물렸는지를 정리합니다.
| 본 책 챕터 | 본 캡스톤에서의 역할 |
|---|---|
| 1~3장 | 매니페스트의 한 줄을 읽는 시야 |
| 4장 Deployment | todo-api / todo-web의 RollingUpdate 전략 |
| 5장 Service | todo-api ↔ todo-web의 cluster DNS 연결 |
| 6장 ConfigMap · Secret | 환경변수 주입의 표준 |
| 7장 Namespace와 라벨 | frontend / backend / data 분리 |
| 9장 PV / PVC / StorageClass | EBS CSI Driver (직접 PV는 안 씀 — RDS) |
| 10장 Ingress | ALB 한 대 + group.name으로 두 호스트 |
| 11장 자원 요청과 한도 | Next.js 256 Mi · FastAPI 128 Mi의 출발점 |
| 12장 헬스 체크 | 3 종 probe + graceful shutdown |
| 13장 오토스케일링 | HPA + Karpenter의 두 단계 자동 반응 |
| 14장 RBAC / NetworkPolicy / Quota | 네임스페이스 격리 + 팀별 한도 |
| 15장 CNI 깊이 | VPC CNI가 Pod에 직접 IP 부여 (배경) |
| 16장 IRSA | todo-api의 AWS 자격 증명 |
| 17장 Admission Controller | Kyverno 정책 (선택) |
| 18장 CRD와 Operator | ESO, Karpenter, ALB Controller, OTel |
| 19장 옵저버빌리티 | OpenTelemetry + Tempo의 트레이스 |
| 20장 GitOps | ArgoCD ApplicationSet의 한 매니페스트 |
| 21장 EKS 셋업 | Terraform의 출발점 |
| 22장 앱 배포 골격 | todo-api / todo-web의 표준 9 묶음 |
| 23장 DB 연동 | RDS + ESO + PgBouncer |
| 24장 CI / CD 파이프라인 | GitHub Actions OIDC → ECR → ApplicationSet |
| 25장 모니터링 · 알람 | PrometheusRule + Alertmanager 라우팅 |
| 26장 운영 체크리스트 | 매일 / 매주 / 매월 / 분기 / 반기 / 연 |
| 27장 kubectl 디버깅 | 사고 시 5분 표준 흐름 |
| 28장 비용 최적화 | OpenCost + Karpenter spot + ALB 공유 |
| 29장 시크릿 운영 | ESO + automountServiceAccountToken |
| 30장 업그레이드 전략 | preStop · PDB · Karpenter disruption budgets |
이 표가 본 캡스톤의 한 줄 요약입니다 — 30장이 한 시스템 안에서 한 위치씩 자리를 잡는 모양이 K8s 트랙의 목표입니다.
AWS 책과의 비교 #
AWS 책(출시 예정)의 6부 캡스톤이 같은 todo 시스템을 ECS Fargate 노선으로 다룹니다. 두 책을 비교 학습하면 같은 도메인을 두 플랫폼으로 구현했을 때의 운영 차이가 명확히 보입니다.
| 결 | 본 책 (EKS) | AWS (ECS Fargate) |
|---|---|---|
| 출발점 비용 | 월 $200~$300 | 월 $80~$150 |
| 운영 표면 | K8s의 풍부함 + 학습 곡선 | AWS 콘솔 + 적은 객체 |
| 자동화 도구 | Karpenter, HPA, ArgoCD | Service Auto Scaling, CodePipeline |
| 옵저버빌리티 | Prometheus + Grafana | CloudWatch Container Insights |
| 멀티 클라우드 가능성 | 가능 (K8s 표준) | AWS 종속 |
| 팀의 학습 비용 | 큼 | 작음 |
작은 팀 + 단일 도메인이라면 ECS Fargate가 더 효율적이고, 멀티 도메인 + GitOps + 풍부한 워크로드 패턴 + 멀티 클라우드 옵션이 필요하면 EKS가 적합합니다. 본 캡스톤의 결정 (EKS)은 학습 가치 + 본 책의 30장의 종합 검증의 결입니다.
정리 — 클러스터 삭제 #
학습용 클러스터는 캡스톤이 끝난 뒤 즉시 정리하는 게 비용 측면의 표준입니다.
# 1. ArgoCD Application 삭제 (워크로드 정리)
kubectl delete applicationset todo -n argocd
# 2. RDS deletion_protection 해제 후 terraform destroy
# (prod 의 경우 deletion_protection 이 켜져 있으므로 terraform 변수로 false 후 apply)
# 3. ALB / Route 53 의 자동 정리 확인
# external-dns 가 hostname 의 A 레코드를 자동 삭제
# 4. terraform destroy
terraform destroy
# 5. ECR repository 삭제 (이미지 잔재)
aws ecr delete-repository --repository-name todo-api --force
aws ecr delete-repository --repository-name todo-web --force이 순서가 안전한 정리의 표준입니다 — Application부터 정리하지 않으면 Terraform이 ALB 의존성에 막혀 destroy가 실패합니다.
연습문제 #
- 본 캡스톤의 13 PR을 실제로 본인 GitHub 조직에 적용해 보고, 마지막 부하 테스트의 결과를 OpenCost의 비용 출력과 함께 한 페이지로 정리합니다. 예상 비용 가설 ($248 정도)과 실측치의 격차가 어디서 발생했는지 (특히 NAT 데이터 전송 · ALB LCU · spot 비율)를 28장 비용 최적화의 §“청구서 review 체크리스트"와 매핑합니다.
- 본 캡스톤의 ApplicationSet 매니페스트를 분기해 dev와 prod의 sync 정책이 다르게 동작하도록 수정합니다 (dev는 automated + selfHeal, prod는 수동 sync). 일부러 dev의 매니페스트에 깨진 값 (예: 존재하지 않는 이미지 태그)을 적용해 selfHeal이 어떻게 보호하는지, prod의 수동 sync가 어떻게 사람의 게이트로 작동하는지 한 단락으로 비교합니다.
- AWS 책의 같은 todo 시스템 ECS Fargate 캡스톤을 따라간 뒤, 두 구현의 운영 결을 본인의 시나리오에 비춰 한 표로 비교합니다. 어느 시점에 어느 플랫폼이 적합한가의 결정 트리를 자기 도메인 (트래픽 패턴 · 팀 규모 · 클라우드 종속 허용도)에 맞춰 한 페이지로 정리합니다.
한 줄 요약: 6부 캡스톤은 modern-react의 Next.js와 modern-python의 FastAPI를 같은 EKS 클러스터에 13개의 PR로 함께 배포한다. Terraform + Karpenter + IRSA + ALB Controller + ExternalDNS + cert-manager의 클러스터 셋업, frontend / backend / data의 네임스페이스 분리 + NetworkPolicy + ResourceQuota, RDS + External Secrets + PgBouncer의 DB 결합, Helm 차트 3개 (infra + api + web), ArgoCD ApplicationSet의 matrix generator가 3 앱 × 2 환경의 6 Application을 한 매니페스트로 자동 생성, OpenTelemetry가 두 앱을 가로지르는 한 트레이스를 만들고, HPA + Karpenter가 두 단계의 자동 반응을 만들고, k6 + OpenCost가 한 달 약 $248의 비용 가설을 검증하며, 마지막 PR이 매일 / 매주 / 매월 / 분기 / 반기 / 연 의 운영 캘린더를 git에 두는 흐름이다. 1~30장의 30 가지 도구가 한 시스템 안에서 한 위치씩 자리를 잡는 모양이 K8s 트랙의 목표이다. 작은 팀 + 단일 도메인이라면 AWS의 ECS Fargate가 더 효율적일 수 있고, 멀티 도메인 + GitOps + 멀티 클라우드 옵션이 필요하면 EKS가 적합하다.
책의 끝 — 다음 단계 #
본 캡스톤으로 본 책의 30장이 한 시스템 안에서 어떻게 맞물리는지의 시야가 완성됐습니다. 그러나 본 책은 K8s의 목표가 아닙니다 — 시작점입니다. 다음 트랙으로 갈 수 있는 주제들을 짚어 둡니다.
- Service Mesh — Istio · Linkerd. mTLS · 세밀한 트래픽 라우팅 · observability mesh.
- MLOps on K8s — Kubeflow · KServe · Argo Workflows. ML 모델 학습 · 배포 · 서빙의 전용 스택.
- 멀티 클러스터 — 단일 클러스터의 한계를 넘는 패턴. 클러스터 페더레이션 · 멀티 region · ArgoCD ApplicationSet의 멀티 클러스터 모드.
- eBPF 깊이 — Cilium 너머. 보안 / 옵저버빌리티 / 네트워킹의 다음 세대.
- eks-anywhere / on-prem K8s — 매니지드를 벗어난 클러스터 운영의 결.
이 주제들은 별도 책의 영역이고, 본 책의 30장이 그 출발점에 서 있는 시야를 만들어 줍니다.
마지막으로 부록 A — docker-compose에서 K8s로가 입문 독자를 위한 마이그레이션 가이드로 책을 닫습니다. 본 책을 다 따라온 독자에게는 부록이지만, Docker / docker-compose까지 와 보고 본 책을 처음 펼친 독자에게는 출발점이 됩니다.