[쿠버네티스 패턴] 11장 Stateless Service

bocopile·2025년 10월 18일

쿠버네티스 패턴

목록 보기
10/28

1. 왜 Stateless 인가?

마이크로서비스는 단일 책임, 데이터 소유, 명확한 배포 경계를 지향하며 대체로 Twelve-Factor App 원칙을 따릅니다.
이러한 애플리케이션은 에페메럴(ephemeral) — 즉, 부작용 없이 생성·확장·종료가 가능한 형태일 때 운영이 용이하며, 그 핵심이 바로 Stateless입니다.

  • 정의: 인스턴스가 요청 간 내부 상태를 보존하지 않습니다.
    필요한 상태는 DB, 메시지 큐, 외부 파일 스토리지 등 외부 저장소에 둡니다.
    Sticky Session 없이 무작위 분산이 가능하다면 Stateless라고 할 수 있습니다.

2. K8s 빌딩 블록으로 조립하기

Kubernetes에는 ‘애플리케이션’이라는 1급 개념이 존재하지 않습니다. 대신 Pod / ReplicaSet / Deployment / Service / ConfigMap / Secret / PVC 같은 리소스들을 조합하여 애플리케이션을 구성합니다.

Stateless 워크로드의 핵심 컨트롤러는 ReplicaSet이지만, 일반적으로 사용자는 Deployment를 통해 관리합니다.
(업데이트, 롤백, 스케일 전략 등을 포함합니다)

구조 요약
Pod(헬스체크) → ReplicaSet(복제본 수 유지) → Deployment(업데이트/롤백 자동화) → Service(레디 Pod로 트래픽 분산) → 필요 시 PVC(파일 스토리지).

이 관계는 Kubernetes가 강제하지 않으므로 의도적인 조합이 필요합니다.

3. 구성 요소 상세

1) Networking — Service (고정 진입점) & 준비된 Pod만 사용

  • Pod는 재생성 시마다 IP가 변경됩니다.
    Service는 고정 IP(ClusterIP)를 제공하고, 헬시(Healthy) & 레디(Readiness) 상태의 Pod로 트래픽을 분산합니다.
  • 핵심 포인트: Deployment/ReplicaSet은 Service를 인지하지 않으며, Service도 독립적으로 존재합니다. 즉, 약결합 구조입니다.

2) Instances — ReplicaSet / Deployment

  • ReplicaSet: 원하는 복제본 수와 셀렉터/템플릿을 기반으로 Pod를 생성·삭제하여 목표 개수를 유지합니다.
  • Deployment: 롤링 또는 리크리에이트 업데이트 전략을 포함하는 사용자 친화적 추상화입니다.
    내부적으로 ReplicaSet을 관리하며 Stateless 애플리케이션의 배포에 가장 권장됩니다.

3) Storage — 필요 시 “상태-비의존적” 파일 스토리지 사용

  • 순수 Stateless 애플리케이션은 Pod 내 로컬 디스크(emptyDir)나 외부 시스템(DB, Object Storage 등)에만 의존하며, Pod 재생성 시 데이터를 유지하지 않습니다.
  • 그러나 일부 애플리케이션은 임시 캐시나 공용 리소스 파일처럼 제한적인 파일 스토리지가 필요할 수 있습니다. 이 경우 emptyDir 또는 외부 로그 수집 시스템(Promtail, FluentBit, Loki 등)을 사용하는 것이 Stateless 특성에 부합합니다.
  • PVC(PersistentVolumeClaim)는 Pod 재시작 후에도 데이터를 유지할 수 있지만, Pod 간 공유나 노드 이동 시 제약이 있으며, 사용 시 해당 Pod는 부분적으로 Stateful 특성을 가지게 됩니다.
    따라서 PVC는*StatefulSet 기반 서비스(MySQL, Redis, Elasticsearch 등)에 적합하며,
    Stateless Deployment에서는 공유 볼륨이나 임시 캐시 목적 외에는 비권장입니다.
  • 핵심 포인트: Stateless는 “데이터 손실 시에도 복구 가능해야 하는 설계”이고,PVC는 “데이터 지속성 보장”을 목표로 하기 때문에, 두 개념은 목적이 다릅니다.

4. 매니페스트

1) ReplicaSet 예시

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: random-generator
  labels:
    app: random-generator
spec:
  replicas: 3
  selector:
    matchLabels:
      app: random-generator
  template:
    metadata:
      labels:
        app: random-generator
    spec:
      containers:
      - name: random-generator
        image: k8spatterns/random-generator:1.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /live
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

random-generator를 3개 복제하여 동일 라벨로 관리합니다.
템플릿을 통해 항상 동일한 Pod 구성이 유지됩니다.

2) Service 예시

apiVersion: v1
kind: Service
metadata:
  name: random-generator
  labels:
    app: random-generator
spec:
  selector:
    app: random-generator
  ports:
  - name: http
    port: 80
    targetPort: 8080
  type: ClusterIP

random-generator ClusterIP Service는 80 → 8080으로 전달하며,
셀렉터로 매칭된 Pod에 트래픽을 분산시킵니다.
ClusterIP는 변하지 않는 고정 진입점 역할을 합니다.

5. 운영 모범사례

1) Idempotent 처리

Idempotent(멱등성) 처리란 같은 요청이 여러번 들어와도 시스템 상태나 결과가 변하지 않는 특성을 의미합니다.
stateless 환경에서는 인스턴스가 언제든지 종료/복제/재시작이 될수 있으므로, 요청이 중복으로 수행될 가능성이 높습니다.

[모범사례]

  • API나 Job이 동일한 요청 ID를 인식하도록 설계합니다.
    • X-Request-Id 헤더나 operationId를 사용하여 중복 실행 방지 로직
    • DB 처리 시 INSERT ... ON DUPLICATE KEY UPDATE 또는 UPSERT 구문을 사용합니다.
    • 메시지 큐(RabbitMQ, Kafka 등)는 Ack 기반 처리와 중복 방지 키(Deduplication key) 를 적용합니다.
    • 외부 API 호출 시 재시도 정책과 Timeout을 명확히 설정합니다.

2) 프로브 정밀화

Kubernetes는 Pod의 상태를 두 가지 프로브로 관리합니다.

  • Liveness Probe: 컨테이너 자체가 “살아 있는지” 확인하고, 실패 시 컨테이너 재시작
  • Readiness Probe: “트래픽을 받을 준비가 되었는지” 확인하고, 실패 시 Service 엔드포인트에서 제외

[Liveness와 Readiness를 분리해야 하는 이유]

  • 단일 Probe만 설정할 경우, 단순 서비스 지연(예: DB 커넥션 지연)에도 컨테이너가 불필요하게 재시작될 수 있습니다.
  • 반대로 Liveness만 설정하지 않으면, 프로세스가 멈춰도 계속 트래픽이 유입될 수 있습니다.

[모범 사례]

  • Liveness: /healthz/live — 앱이 프로세스 단에서 살아 있는지 확인합니다.
  • Readiness: /healthz/ready — DB 연결, 캐시, 외부 API 등 업무 의존 리소스 준비 상태를 확인합니다.
  • 초기 구동이 느린 애플리케이션은 initialDelaySeconds를 충분히 설정합니다.

3) 약결합 배포

Deployment(애플리케이션) 와 Service(트래픽 분산) 는 서로 의존하지 않는 것이 이상적입니다.
즉, 새로운 버전이 배포되더라도 서비스 전체 중단 없이 점진적으로 교체될 수 있어야 합니다.

[Deployment, Service를 분리해야 하는 이유]

  • Deployment는 ReplicaSet을 교체하며 롤링 업데이트를 수행합니다.
  • Service는 “준비된 Pod(Readiness OK)” 만 트래픽 대상으로 선택합니다.
  • 두 리소스가 강하게 결합되어 있으면, 배포 중 잠시라도 트래픽이 끊기거나 요청 실패가 발생할 수 있습니다.

[모범사례]

  • RollingUpdate 전략 사용 (기본값)
  • 새로운 버전의 Pod가 Ready 상태가 될 때까지 Service가 기존 Pod로 트래픽을 유지합니다.
  • ConfigMap, Secret 변경 시에도 Deployment 롤링 재배포가 자동으로 반영되도록 설정합니다.
  • Ingress Controller 레벨에서는 Zero-downtime Reload를 지원하는 구성(Nginx, Istio 등)을 선택합니다.

4) 스케일 전략

Stateless 애플리케이션은 인스턴스 간 상태 공유가 없으므로, 수평 확장(Scale-out) 이 가장 효과적인 운영 방식입니다.

[주요 방식]
1. HPA (Horizontal Pod Autoscaler)

  • CPU, 메모리, 사용자 정의 메트릭(Prometheus Adapter 등)에 따라 Pod 개수를 자동 조정합니다.
  • 예시: CPU 사용률 70% 초과 시 복제본 3 → 6으로 확장
  1. VPA (Vertical Pod Autoscaler)
  • Pod 재시작 시 컨테이너의 리소스 요청(requests/limits)을 자동 조정합니다.
  • 리소스 사용 패턴이 일정한 애플리케이션에 적합합니다.
  1. KEDA (Kubernetes Event-driven Autoscaler)
  • 메시지 큐(RabbitMQ, Kafka), HTTP 요청 수, Prometheus 쿼리 등 이벤트 기반 스케일링을 지원합니다.
  • 예시: RabbitMQ 큐 길이가 100 초과 시 Worker Pod 자동 증가

[모범 사례]

  • 관측 시스템(Prometheus, Grafana, Loki)과 연동하여 부하 지표를 시각화합니다.
  • AutoScaler가 너무 민감하게 동작하지 않도록 cooldown 설정을 둡니다.
  • Stateless 구조를 유지하기 위해 Pod 간 세션 공유는 피합니다.

5) Knative

Knative Serving은 Kubernetes 위에서 동작하는 서버리스 프레임워크입니다.
트래픽이 없을 때 Pod을 0으로 줄였다가, 요청이 들어오면 자동으로 Scale-up 시킵니다.
즉, Stateless 워크로드의 극단적 확장성(Auto 0→N→0) 을 제공합니다.

[특징]

  • 요청량에 따라 Pod 수를 실시간으로 조정합니다.
  • Revision 단위 배포를 지원하여 Canary / Blue-Green 배포가 용이합니다.
  • Istio, Kourier, Contour 등과 통합되어 네트워크 트래픽 라우팅을 자동 관리합니다.

[활용 예시]

  • API Gateway나 이벤트 트리거 기반의 단기 Task 처리
  • 비정기적으로 호출되는 Batch Job / CronJob 대체
  • 비용 최적화가 중요한 PoC 환경

[예시 매니페스트]

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: echo-server
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/maxScale: "10"
    spec:
      containers:
        - image: hashicorp/http-echo:1.0.0
          args: ["-text=hello knative", "-listen=:8080"]

6. Stateless vs Stateful의 경계

  • StatefulSet은 “고유 아이덴티티, 순서성, 스토리지, 네트워크”를 보장하며 ‘펫(Pet)’에 가깝습니다.
  • ReplicaSet은 ‘가축(Cattle)’처럼 교체 가능한 Pod를 관리합니다.
  • Stateless를 설계할 때 “정말 인스턴스 고유성이 필요한가?”를 항상 자문하셔야 합니다.

7. 실습

0) 기본 환경 설치

  • metric-server component yaml 파일 설정 변경
    wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -O metrics-server.yaml
    vi metrics-server.yaml
  • container args에 아래 설정 추가

     - --kubelet-insecure-tls
  • 적용

    kubectl apply -f metrics-server.yaml
  • 적용 확인
     kubectl get pods -n kube-system -l k8s-app=metrics-server -o wide
     kubectl describe deployment metrics-server -n kube-system | grep -A10 "Args"

1) 기본 Stateless (Deployment + Service)

  • stateless 적용

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: stateless-echo
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: stateless-echo
      template:
        metadata:
          labels:
            app: stateless-echo
        spec:
          containers:
            - name: app
              image: hashicorp/http-echo:1.0.0
              args: ["-text=hello", "-listen=:8080"]
              ports:
                - containerPort: 8080
              resources:
                requests:
                  cpu: "100m"
                  memory: "64Mi"
                limits:
                  cpu: "200m"
                  memory: "128Mi"
              readinessProbe:
                httpGet:
                  path: /
                  port: 8080
                initialDelaySeconds: 2
                periodSeconds: 5
              livenessProbe:
                httpGet:
                  path: /
                  port: 8080
                initialDelaySeconds: 5
                periodSeconds: 10
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: stateless-echo
      labels:
        app: stateless-echo
    spec:
      selector:
        app: stateless-echo
      ports:
        - name: http
          port: 80
          targetPort: 8080
      type: ClusterIP
    
  • 배포

    kubectl apply -f stateless-echo.yaml
    kubectl get pods -l app=stateless-echo -w

  • 배포 확인
kubectl port-forward svc/stateless-echo 8080:80
curl -s localhost:8080

2) HPA 적용

CPU 사용량에 따라 Pod 수를 자동으로 조정하는 HPA(Horizontal Pod Autoscaler)를 실습하여
Stateless 애플리케이션의 수평 확장성(Scale-out) 개념을 체험합니다.

  • HPA 생성

    kubectl autoscale deployment stateless-echo --cpu-percent=50 --min=1 --max=5
  • HPA 생성 확인 :

    kubectl get hpa
    kubectl describe hpa stateless-echo

  • 부하 테스트 진행 (ARM64 기준)

      for i in $(seq 1 5); do
        kubectl run load-generator-$i \
          --image=busybox \
          --restart=Never \
          -- /bin/sh -c "while true; do wget -q -O- http://stateless-echo.default.svc.cluster.local >/dev/null; done" &
      done
     kubectl get po	

  • hpa 확인

    kubectl get hpa -w

  • 부하테스트 중단

    for i in $(seq 1 5); do kubectl delete pod load-generator-$i; done

8. 참고 링크

Kubernetes 공식 문서

Architectural Patterns

Best Practices & Guides

블로그 및 실습 참고


profile
DevOps Engineer

3개의 댓글

comment-user-thumbnail
2025년 10월 19일

안녕하세요. 작성해주신 글 잘 읽었습니다. knative라는 것을 덕분에 알게 됐습니다. 혹시 현업에서 knative를 사용한 경험이 있으시면 어떤 식으로 주로 사용하시는지 그리고 빈번히 사용되는지에 대해 여쭤보고싶습니다. 처음 들어본 내용이고 검색을 해봐도 애플리케이션에 대한 설명만 나와있어서 경험적인 내용을 찾기 어려워서요.

1개의 답글
comment-user-thumbnail
2025년 10월 19일

안녕하세요~ 오늘도 잘 읽었습니다. 아래 몇가지 질문이 있는데요~

이벤트 기반 오토스케일링을 구현해야 하는데, Knative Serving과 KEDA 중 어떤 것을 선택하는 것이 좋을까요? 실 업무에서 사용경험이 있으신지 궁금합니다.

답글 달기