목차
31 장

풀스택 앱 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의 변경량은 의도적으로 작게 두어 리뷰 가능한 크기로 유지합니다.

목표의 아키텍처 #

todo 시스템의 한 그림
[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) 한 장입니다.

docs/adr/0001-eks-architecture.md
# 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를 처음부터 도입 합니다.

terraform/main.tf
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와의 결정 트리"의 결합입니다.

kustomize/karpenter/default-nodepool.yaml
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.budgets30장 업그레이드 전략의 blast radius 결입니다 — 평일 업무 시간에 한 번에 10% 이하의 노드만 교체합니다.

보조 컴포넌트 한 묶음 #

ALB Controller + ExternalDNS + cert-manager
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=true

22장 앱 배포 골격 §“cert-manager와 external-dns"에서 짚었던 셋업 그대로입니다.

PR #3 — 네임스페이스 / RBAC / NetworkPolicy 골격 #

워크로드를 띄우기 전에 격리 골격을 잡아 둡니다.

kustomize/namespaces/todo.yaml
---
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 — 격리의 본격 #

netpol — frontend만 backend api 호출 가능
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: 8000

14장 RBAC / NetworkPolicy / ResourceQuota의 NetworkPolicy 모델이 본격적인 격리로 이어집니다. frontend가 직접 RDS에 못 가고, 반드시 backend를 거치는 강제 흐름입니다.

ResourceQuota — 팀별 한도 #

quota — backend 네임스페이스의 한도
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를 옵션으로 두는 결입니다.

terraform/modules/todo-rds/main.tf
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 도메인의 부하가 작아 충분합니다.

ExternalSecret — todo-api의 DB 자격
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: password

23장의 매니페스트 그대로이고, 29장 시크릿 운영 §“비밀번호 0"의 RDS IAM auth는 본 캡스톤에서는 옵션으로 둡니다 — todo의 트래픽이 작아 PgBouncer + 비밀번호 모델로 충분합니다.

PR #5 — FastAPI 백엔드 배포 #

모던 파이썬 4부 캡스톤의 FastAPI todo 백엔드가 입력입니다. (modern-python은 구 파이썬 강좌와의 차별 의미를 살려 “모던” 접두어를 유지합니다.) 컨테이너화는 본 책의 범위 밖이지만, Dockerfile의 핵심을 짚어 둡니다.

Dockerfile — multi-stage
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 #

charts/todo-api/templates/deployment.yaml — 일부
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: 60

22장 앱 배포 골격의 표준 매니페스트에 30장의 graceful shutdown 결 (preStop + terminationGracePeriodSeconds)까지 결합한 모양입니다.

ServiceAccount + IRSA #

todo-api의 ServiceAccount
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 안에서는 다음과 같이 동작합니다.

Next.js (App Router)가 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로 부릅니다.

charts/todo-web/templates/deployment.yaml — 일부
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: 512Mi

Next.js Pod의 메모리 가설은 11장 자원 요청과 한도의 측정 결로 잡습니다 — SSR + RSC의 한 요청당 메모리 점유가 일정 정도 누적되므로, requests를 256 Mi로 두는 게 보수적인 출발점입니다. 28장 비용 최적화의 VPA recommendation으로 한 달 뒤 적정값에 수렴시킵니다.

PR #7 — Ingress + ALB #

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: 80

alb.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/ 디렉터리
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 한 매니페스트로 정리됩니다.

argocd/applicationset.yaml
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: true

matrix 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를 추가해 두 앱의 트레이스를 묶는 결입니다.

otel-collector — DaemonSet 패턴
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 #

todo-api의 4 golden signals
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이 결합되어 두 단계의 자동 반응을 만듭니다.

todo-api의 HPA
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 — 부하 테스트와 비용 추정 #

k6/script.js — 부하 시나리오
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 부하 실행
k6 run k6/script.js

측정할 결입니다.

  • HPA scale-up의 응답 시간 — 트래픽 50 → 200 사이에서 Pod가 몇 초 만에 늘어나는가
  • Karpenter의 노드 추가 시간 — Pod가 Pending에 머무른 시간
  • P95 latency — 부하 정점 (500 VUs)에서 latency가 어떻게 변하는가
  • 5xx 비율 — 부하 중 에러율이 25장의 임계치 (5%)를 넘는지

비용 검증 #

OpenCost — 부하 테스트 후 비용 측정
helm install opencost opencost/opencost \
  -n opencost --create-namespace

OpenCost의 출력에서 한 달 가설 비용을 검증합니다.

항목예상 (월)
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장 업그레이드 전략의 업그레이드 체크리스트를 본 시스템에 적용합니다.

docs/runbooks/todo-operations.md
# 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장 Deploymenttodo-api / todo-web의 RollingUpdate 전략
5장 Servicetodo-api ↔ todo-web의 cluster DNS 연결
6장 ConfigMap · Secret환경변수 주입의 표준
7장 Namespace와 라벨frontend / backend / data 분리
9장 PV / PVC / StorageClassEBS CSI Driver (직접 PV는 안 씀 — RDS)
10장 IngressALB 한 대 + 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장 IRSAtodo-api의 AWS 자격 증명
17장 Admission ControllerKyverno 정책 (선택)
18장 CRD와 OperatorESO, Karpenter, ALB Controller, OTel
19장 옵저버빌리티OpenTelemetry + Tempo의 트레이스
20장 GitOpsArgoCD 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, ArgoCDService Auto Scaling, CodePipeline
옵저버빌리티Prometheus + GrafanaCloudWatch 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가 실패합니다.

연습문제 #

  1. 본 캡스톤의 13 PR을 실제로 본인 GitHub 조직에 적용해 보고, 마지막 부하 테스트의 결과를 OpenCost의 비용 출력과 함께 한 페이지로 정리합니다. 예상 비용 가설 ($248 정도)과 실측치의 격차가 어디서 발생했는지 (특히 NAT 데이터 전송 · ALB LCU · spot 비율)를 28장 비용 최적화의 §“청구서 review 체크리스트"와 매핑합니다.
  2. 본 캡스톤의 ApplicationSet 매니페스트를 분기해 dev와 prod의 sync 정책이 다르게 동작하도록 수정합니다 (dev는 automated + selfHeal, prod는 수동 sync). 일부러 dev의 매니페스트에 깨진 값 (예: 존재하지 않는 이미지 태그)을 적용해 selfHeal이 어떻게 보호하는지, prod의 수동 sync가 어떻게 사람의 게이트로 작동하는지 한 단락으로 비교합니다.
  3. 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까지 와 보고 본 책을 처음 펼친 독자에게는 출발점이 됩니다.

X