sayu.day

Kubernetes 운영 심화: Istio 서비스 메시, 오토스케일링, 보안

Kubernetes 클러스터를 만드는 일과 프로덕션에서 운영하는 일은 별개의 문제입니다. 서비스가 늘어나면 서비스 간 통신을 제어할 수단이 필요하고, 트래픽 변동에 맞춰 Pod와 노드 용량을 조절해야 하며, 커진 클러스터에는 명확한 보안 경계가 있어야 합니다. 이 글은 프로덕션 운영의 3대 축인 트래픽 제어(Istio 서비스 메시), 용량 관리(HPA·VPA·Karpenter·KEDA), 보안(RBAC·NetworkPolicy·PSS)을 AWS EKS 환경을 공통 맥락으로 정리합니다.

Istio 서비스 메시: 트래픽 제어

왜 서비스 메시인가

마이크로서비스 아키텍처에서는 서비스 간 통신이 시스템 신뢰성을 좌우합니다. 그런데 통신 제어를 각 서비스가 직접 구현하면 같은 문제가 반복됩니다.

  • 재시도·타임아웃 로직이 서비스마다 중복됩니다.
  • 서킷 브레이커를 서비스별로 각각 구현해야 합니다.
  • mTLS 인증서를 직접 관리해야 합니다.
  • 트래픽 분할과 카나리 배포가 어렵습니다.
  • 분산 추적 연동이 복잡해집니다.

서비스 메시는 이러한 횡단 관심사(Cross-cutting Concerns)를 애플리케이션에서 분리해 인프라 레이어에서 처리합니다. 각 Pod 옆에 Envoy 프록시(사이드카)가 붙어 mTLS, 재시도, 서킷 브레이커를 애플리케이션 대신 수행하는 구조입니다.

아키텍처: Control Plane과 Data Plane

flowchart TB
    subgraph ControlPlane [Control Plane]
        Istiod[istiod<br/>- Pilot: 설정 배포<br/>- Citadel: 인증서 관리<br/>- Galley: 설정 검증]
    end
    
    subgraph DataPlane [Data Plane]
        subgraph PodA [Service A Pod]
            AppA[Application]
            EnvoyA[Envoy Sidecar]
        end
        
        subgraph PodB [Service B Pod]
            AppB[Application]
            EnvoyB[Envoy Sidecar]
        end
        
        subgraph PodC [Service C Pod]
            AppC[Application]
            EnvoyC[Envoy Sidecar]
        end
    end
    
    subgraph IngressEgress [Ingress/Egress]
        IG[Ingress Gateway<br/>Envoy]
        EG[Egress Gateway<br/>Envoy]
    end
    
    Istiod -->|xDS 프로토콜| EnvoyA & EnvoyB & EnvoyC & IG & EG
    
    External[외부 트래픽] --> IG --> EnvoyA
    EnvoyA <--> EnvoyB <--> EnvoyC
    EnvoyC --> EG --> ExtService[외부 서비스]

istiod(Control Plane)가 설정 배포(Pilot), 인증서 관리(Citadel), 설정 검증(Galley)을 담당하고, 각 Pod의 Envoy 사이드카(Data Plane)에 xDS 프로토콜로 설정을 내려보냅니다.

사이드카는 네임스페이스 라벨 하나로 자동 주입됩니다.

# 네임스페이스에 라벨 추가
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  labels:
    istio-injection: enabled  # 자동 sidecar 주입

이후 생성되는 모든 Pod에 Envoy 컨테이너가 자동으로 추가됩니다.

# kubectl get pod my-app-xxx -o yaml 로 확인
# 컨테이너 2개: app, istio-proxy
containers:
- name: my-app
  image: my-app:v1
- name: istio-proxy
  image: docker.io/istio/proxyv2:1.20.0

Gateway: 외부 트래픽 진입점

Gateway는 외부 트래픽이 메시로 들어오는 지점의 포트·프로토콜·TLS를 정의합니다.

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: my-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway  # Istio Ingress Gateway Pod 선택
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "api.example.com"
    - "dashboard.example.com"

HTTPS는 tls 블록으로 설정하며, 와일드카드 호스트와 결합하면 인증서 하나로 모든 서브도메인을 처리할 수 있습니다.

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: secure-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: my-tls-secret  # Kubernetes TLS Secret
    hosts:
    - "*.example.com"  # 와일드카드

와일드카드 서브도메인 라우팅: AWS 실전 패턴

여러 서비스를 api.example.com, dashboard.example.com, admin.example.com 형태로 쉽게 배포할 수 있는 것은 Istio + AWS Route 53 + ACM 통합 덕분입니다. 전체 구조는 다음과 같습니다.

flowchart TB
    subgraph Internet [인터넷]
        User[사용자]
    end
    
    subgraph AWS [AWS]
        R53[Route 53<br/>*.example.com → ALB]
        ACM[ACM 인증서<br/>*.example.com]
        ALB[Application Load Balancer]
    end
    
    subgraph EKS [EKS 클러스터]
        IG[Istio Ingress Gateway<br/>istio-ingressgateway]
        
        GW[Gateway<br/>hosts: '*.example.com']
        
        VS1[VirtualService<br/>api.example.com]
        VS2[VirtualService<br/>dashboard.example.com]
        VS3[VirtualService<br/>admin.example.com]
        
        SVC1[api-service]
        SVC2[dashboard-service]
        SVC3[admin-service]
    end
    
    User --> R53 --> ALB --> IG
    IG --> GW
    GW --> VS1 & VS2 & VS3
    VS1 --> SVC1
    VS2 --> SVC2
    VS3 --> SVC3

Step 1: Route 53 와일드카드 레코드

# Route 53에 와일드카드 A 레코드 생성
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "*.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "my-alb-123456789.ap-northeast-2.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Step 2: ACM 와일드카드 인증서

# 와일드카드 인증서 요청
aws acm request-certificate \
  --domain-name "*.example.com" \
  --subject-alternative-names "example.com" \
  --validation-method DNS

Step 3: ALB와 Istio Ingress Gateway 연결

TLS는 ALB에서 ACM 인증서로 종료하고, 평문 HTTP를 Istio Gateway로 전달합니다.

# Istio Ingress Gateway Service (LoadBalancer)
apiVersion: v1
kind: Service
metadata:
  name: istio-ingressgateway
  namespace: istio-system
  annotations:
    # ALB 사용
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
    
    # HTTPS 종료는 ALB에서 (ACM 인증서 사용)
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:ap-northeast-2:123456789012:certificate/xxx"
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
spec:
  type: LoadBalancer
  ports:
  - port: 443
    targetPort: 8080  # Istio Gateway는 8080에서 수신
    name: https

Step 4: 와일드카드 Gateway

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: wildcard-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 8080      # ALB에서 TLS 종료 후 8080으로 전달
      name: http
      protocol: HTTP
    hosts:
    - "*.example.com"   # 와일드카드로 모든 서브도메인 수신

Step 5: 서비스별 VirtualService

# API 서비스
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-vs
  namespace: my-app
spec:
  hosts:
  - "api.example.com"
  gateways:
  - istio-system/wildcard-gateway
  http:
  - route:
    - destination:
        host: api-service
        port:
          number: 80
---
# Dashboard 서비스
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: dashboard-vs
  namespace: my-app
spec:
  hosts:
  - "dashboard.example.com"
  gateways:
  - istio-system/wildcard-gateway
  http:
  - route:
    - destination:
        host: dashboard-service
        port:
          number: 80
---
# Admin 서비스
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: admin-vs
  namespace: my-app
spec:
  hosts:
  - "admin.example.com"
  gateways:
  - istio-system/wildcard-gateway
  http:
  - route:
    - destination:
        host: admin-service
        port:
          number: 80

이 구조에서 새 서비스 추가는 VirtualService 하나만 추가하면 됩니다. DNS나 인증서 작업이 필요 없습니다.

VirtualService: L7 라우팅 규칙

경로 기반 라우팅

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-routing
spec:
  hosts:
  - "api.example.com"
  gateways:
  - istio-system/wildcard-gateway
  http:
  # /v1/* → v1 서비스
  - match:
    - uri:
        prefix: /v1/
    route:
    - destination:
        host: api-v1
        
  # /v2/* → v2 서비스
  - match:
    - uri:
        prefix: /v2/
    route:
    - destination:
        host: api-v2
        
  # 기본 → latest
  - route:
    - destination:
        host: api-latest

가중치 기반 트래픽 분할 (카나리 배포)

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: canary-rollout
spec:
  hosts:
  - api-service
  http:
  - route:
    - destination:
        host: api-service
        subset: stable
      weight: 90
    - destination:
        host: api-service
        subset: canary
      weight: 10

헤더 기반 라우팅 (A/B 테스팅)

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: ab-testing
spec:
  hosts:
  - api-service
  http:
  # Beta 사용자 → 새 버전
  - match:
    - headers:
        x-user-group:
          exact: beta
    route:
    - destination:
        host: api-service
        subset: v2
        
  # 나머지 → 기존 버전
  - route:
    - destination:
        host: api-service
        subset: v1

타임아웃과 재시도

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: timeout-retry
spec:
  hosts:
  - api-service
  http:
  - route:
    - destination:
        host: api-service
    timeout: 10s  # 10초 타임아웃
    retries:
      attempts: 3          # 최대 3회 재시도
      perTryTimeout: 2s    # 각 시도당 2초 타임아웃
      retryOn: "5xx,reset,connect-failure"

DestinationRule: 트래픽 정책

VirtualService가 "어디로 보낼지"를 정한다면, DestinationRule은 "도착지에서 어떻게 다룰지"를 정합니다.

Subset 정의

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api-service-dr
spec:
  host: api-service
  subsets:
  - name: stable
    labels:
      version: v1
  - name: canary
    labels:
      version: v2

로드밸런싱 알고리즘

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: lb-policy
spec:
  host: api-service
  trafficPolicy:
    loadBalancer:
      simple: LEAST_CONN  # 최소 연결 수
      # ROUND_ROBIN (기본값)
      # RANDOM
      # PASSTHROUGH (원래 IP로 전달)

서킷 브레이커

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: circuit-breaker
spec:
  host: api-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100      # 최대 연결 수
      http:
        h2UpgradePolicy: UPGRADE
        http1MaxPendingRequests: 100
        http2MaxRequests: 1000
        
    outlierDetection:
      consecutive5xxErrors: 5    # 연속 5xx 에러 5번
      interval: 30s              # 30초 간격으로 체크
      baseEjectionTime: 30s      # 30초간 트래픽 제외
      maxEjectionPercent: 50     # 최대 50%까지 제외

장애 Pod가 감지되면 트래픽에서 제외했다가 일정 시간 후 복귀시키는 흐름입니다.

sequenceDiagram
    participant Client as 클라이언트
    participant Envoy as Envoy Proxy
    participant PodA as Pod A (정상)
    participant PodB as Pod B (장애)
    
    Client->>Envoy: 요청 1
    Envoy->>PodB: 전달
    PodB-->>Envoy: 500 Error
    
    Client->>Envoy: 요청 2-5
    Envoy->>PodB: 전달
    PodB-->>Envoy: 500 Error (5회 연속)
    
    Note over Envoy,PodB: 서킷 오픈! Pod B 제외
    
    Client->>Envoy: 요청 6
    Envoy->>PodA: Pod A로만 전달
    PodA-->>Envoy: 200 OK
    
    Note over Envoy,PodB: 30초 후 재시도
    Envoy->>PodB: 헬스체크
    PodB-->>Envoy: 200 OK
    Note over Envoy,PodB: 서킷 닫힘, 트래픽 복구

mTLS와 접근 제어

PeerAuthentication: 서비스 간 암호화

# 네임스페이스 전체에 mTLS 강제
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: my-app
spec:
  mtls:
    mode: STRICT  # mTLS 필수
모드 설명
DISABLE mTLS 비활성화
PERMISSIVE mTLS와 평문 모두 허용 (마이그레이션 시 유용)
STRICT mTLS만 허용

AuthorizationPolicy: 접근 제어

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: api-access-policy
  namespace: my-app
spec:
  selector:
    matchLabels:
      app: api-service
  rules:
  # frontend에서만 접근 허용
  - from:
    - source:
        principals: ["cluster.local/ns/my-app/sa/frontend"]
    to:
    - operation:
        methods: ["GET", "POST"]
        paths: ["/api/*"]

관측성: Kiali와 Jaeger

Istio는 메트릭, 로그, 트레이스를 자동으로 수집합니다. Kiali로 서비스 메시 토폴로지를 시각화하고, Jaeger로 분산 추적을 확인할 수 있습니다.

# Kiali 설치
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.20/samples/addons/kiali.yaml
kubectl port-forward -n istio-system svc/kiali 20001:20001

# Jaeger 설치
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.20/samples/addons/jaeger.yaml
kubectl port-forward -n istio-system svc/tracing 16686:80

오토스케일링: 세 가지 축

트래픽 제어가 갖춰졌다면 다음은 용량입니다. Kubernetes 오토스케일링은 무엇을 스케일링하느냐에 따라 세 가지로 구분됩니다.

스케일링 유형 대상 도구
수평 (Horizontal) Pod 개수 HPA, KEDA
수직 (Vertical) Pod 리소스 (CPU/Memory) VPA
클러스터 Node 개수 Cluster Autoscaler, Karpenter

HPA: Pod 수평 스케일링

중요

HPA는 현재 autoscaling/v2가 stable입니다. v2beta2는 deprecated되었습니다.

기본 설정: CPU 기반

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # 평균 CPU 사용률 70%

다중 메트릭: CPU + Memory

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
  # CPU 기반
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  # Memory 기반
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 500Mi

노트

다중 메트릭 사용 시 HPA는 가장 큰 replica 수를 요구하는 메트릭을 기준으로 스케일링합니다.

메트릭 타입 정리

타입 설명 예시
Resource CPU, Memory (Metrics Server 필요) 평균 CPU 70%
Pods Pod당 커스텀 메트릭 requests-per-second
Object 다른 K8s 오브젝트의 메트릭 Ingress의 requests-per-second
External 외부 시스템 메트릭 SQS 큐 메시지 수

Resource는 Metrics Server, 나머지는 Prometheus Adapter 같은 어댑터가 필요합니다.

External 메트릭 예시: AWS SQS

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: worker-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 1
  maxReplicas: 50
  metrics:
  - type: External
    external:
      metric:
        name: sqs_messages_visible
        selector:
          matchLabels:
            queue_name: "order-processing"
      target:
        type: AverageValue
        averageValue: "30"  # Pod당 30개 메시지 처리

Scaling Behavior: 급격한 스케일링 방지

스케일 업은 빠르게, 스케일 다운은 천천히 하는 것이 일반적인 패턴입니다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2
  maxReplicas: 100
  
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 5분간 안정화
      policies:
      - type: Percent
        value: 10        # 최대 10%씩 감소
        periodSeconds: 60
      selectPolicy: Max
      
    scaleUp:
      stabilizationWindowSeconds: 0  # 즉시 스케일 업
      policies:
      - type: Percent
        value: 100       # 최대 100%씩 증가
        periodSeconds: 15
      - type: Pods
        value: 4         # 또는 최대 4개씩 증가
        periodSeconds: 15
      selectPolicy: Max
  
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

VPA: Pod 수직 스케일링

경고

VPA는 Kubernetes 기본 제공이 아닙니다. 별도 설치가 필요합니다.

# VPA 설치 (공식 리포지토리)
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh

VPA 4가지 모드

모드 동작 사용 사례
Auto Pod 재생성으로 리소스 변경 일반적인 워크로드
Recreate Auto와 동일 Auto의 별칭
Initial 생성 시에만 적용 StatefulSet, 중단 민감 워크로드
Off 추천만 제공, 변경 없음 모니터링 용도

설정 예시

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  
  updatePolicy:
    updateMode: "Auto"  # Auto, Recreate, Initial, Off
  
  resourcePolicy:
    containerPolicies:
    - containerName: api
      # 리소스 범위 제한
      minAllowed:
        cpu: 100m
        memory: 128Mi
      maxAllowed:
        cpu: 2
        memory: 4Gi
      # 특정 리소스만 조정
      controlledResources: ["cpu", "memory"]

추천값 확인

kubectl describe vpa api-vpa

# Status:
#   Recommendation:
#     Container Recommendations:
#       Container Name: api
#       Lower Bound:
#         Cpu:     25m
#         Memory:  128Mi
#       Target:
#         Cpu:     100m    # 추천 CPU
#         Memory:  256Mi   # 추천 Memory
#       Upper Bound:
#         Cpu:     1
#         Memory:  1Gi

In-place Pod Resizing (K8s 1.33+)

Kubernetes 1.33부터 In-place Pod Resizing이 beta로 기본 활성화됩니다. Pod를 재생성하지 않고 리소스를 변경할 수 있습니다. 단, 2024년 기준 VPA는 아직 In-place Resizing을 지원하지 않으며, 통합이 진행 중입니다.

# 수동 In-place Resizing 예시
apiVersion: v1
kind: Pod
metadata:
  name: api-pod
spec:
  containers:
  - name: api
    image: api:latest
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
    resizePolicy:        # K8s 1.33+ 필요
    - resourceName: cpu
      restartPolicy: NotRequired  # 재시작 없이 변경
    - resourceName: memory
      restartPolicy: RestartContainer  # 메모리는 재시작 필요

HPA와 VPA를 함께 쓸 때

주의

HPA와 VPA를 같은 메트릭(CPU/Memory)으로 동시에 사용하면 충돌이 발생합니다. HPA가 Pod 수를 늘리면 VPA가 리소스를 줄이고, 다시 HPA가 Pod를 늘리는 악순환이 발생할 수 있습니다.

권장 패턴은 다음과 같습니다.

  • HPA: Custom/External 메트릭 (requests-per-second, 큐 길이)
  • VPA: CPU/Memory (Off 모드로 추천만 활용)

노드 스케일링: Cluster Autoscaler vs Karpenter

노트

Kubernetes SIG Autoscaling은 Cluster Autoscaler와 Karpenter 두 가지를 공식 지원합니다.

특성 Cluster Autoscaler Karpenter
프로비저닝 Node Group 사전 정의 필요 자동 프로비저닝 (constraints만)
인스턴스 선택 Node Group당 고정 워크로드에 맞게 자동 선택
스케일 속도 느림 (Node Group 확인) 빠름 (직접 EC2 API 호출)
Node Lifecycle 스케일링만 Disruption, 자동 업그레이드
클라우드 지원 다양 (AWS, GCP, Azure 등) AWS, Azure
Spot 통합 별도 Node Group 필요 NodePool에서 바로 설정

Cluster Autoscaler는 미리 정의한 Node Group 단위로 노드를 늘리고 줄입니다. 반면 Karpenter는 Pending Pod의 요구사항을 보고 적합한 인스턴스 타입을 직접 골라 프로비저닝합니다.

Karpenter 설정 예시 (AWS)

# NodePool 정의
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"]  # Spot 우선
      - key: karpenter.k8s.aws/instance-category
        operator: In
        values: ["c", "m", "r"]
      - key: karpenter.k8s.aws/instance-size
        operator: In
        values: ["medium", "large", "xlarge", "2xlarge"]
      
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  
  # 자동 정리 (Disruption)
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
  
  limits:
    cpu: 1000
    memory: 1000Gi
---
# EC2NodeClass 정의
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  role: KarpenterNodeRole-my-cluster

Karpenter Disruption (자동 최적화)

Karpenter는 활용도가 낮은 노드를 감지해 더 적합한 크기의 노드로 통합하며 비용을 최적화합니다.

sequenceDiagram
    participant KP as Karpenter
    participant Node as Under-utilized Node
    participant Pod as Pods
    participant NewNode as Optimal Node
    
    Note over KP: consolidateAfter: 1m 경과
    KP->>Node: Node 활용도 분석
    Note over KP: 사용률 낮음 감지
    
    KP->>NewNode: 최적 크기 Node 프로비저닝
    KP->>Pod: Pod 이동 (drain)
    Pod->>NewNode: Pod 스케줄링
    KP->>Node: Node 종료
    Note over KP: 비용 최적화 완료

KEDA: 이벤트 기반 스케일링

KEDA는 HPA를 확장하여 이벤트 기반 스케일링을 제공합니다. AWS SQS, Kafka, Prometheus, Cron, HTTP 등 60개 이상의 트리거를 지원하며, ScaledObject를 정의하면 내부적으로 HPA를 자동 생성합니다.

# Helm으로 설치
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace

ScaledObject: AWS SQS 예시

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-processor-scaler
spec:
  scaleTargetRef:
    name: order-processor
  
  minReplicaCount: 0   # 0까지 스케일 다운 가능!
  maxReplicaCount: 100
  
  pollingInterval: 30  # 30초마다 확인
  cooldownPeriod: 300  # 5분 대기 후 스케일 다운
  
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.ap-northeast-2.amazonaws.com/123456789012/orders
      queueLength: "10"  # 메시지 10개당 1개 Pod
      awsRegion: ap-northeast-2
    authenticationRef:
      name: keda-aws-credentials
---
# 인증 설정
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: keda-aws-credentials
spec:
  podIdentity:
    provider: aws-eks  # IRSA 사용

ScaledObject: Kafka 예시

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-scaler
spec:
  scaleTargetRef:
    name: kafka-consumer
  minReplicaCount: 1
  maxReplicaCount: 50
  
  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka.default.svc:9092
      consumerGroup: my-consumer-group
      topic: events
      lagThreshold: "100"  # lag 100 초과 시 스케일 업

ScaledObject: Cron (시간 기반)

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: business-hours-scaler
spec:
  scaleTargetRef:
    name: api-server
  minReplicaCount: 2
  maxReplicaCount: 20
  
  triggers:
  # 업무 시간 (9시-18시 평일)
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 0 9 * * 1-5   # 월-금 9시
      end: 0 18 * * 1-5    # 월-금 18시
      desiredReplicas: "10"
  
  # 비업무 시간
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 0 18 * * 1-5
      end: 0 9 * * 2-6
      desiredReplicas: "2"

KEDA의 핵심 장점

기능 HPA KEDA
0으로 스케일 다운 불가 (min 1) 가능
외부 메트릭 연동 복잡 (Adapter 필요) 간단 (내장)
이벤트 기반 미지원 지원
Cron 스케줄링 미지원 지원

보안: 네 가지 레이어

Kubernetes 보안은 클러스터(API Server 인증/인가, etcd 암호화), 워크로드(Pod Security Standards, ServiceAccount), 네트워크(NetworkPolicy, Istio mTLS), 런타임(Falco, Seccomp)의 네 레이어로 나눌 수 있습니다. 여기서는 운영에서 가장 자주 다루는 RBAC, NetworkPolicy, PSS, ServiceAccount를 살펴봅니다.

RBAC: 최소 권한 설계

RBAC은 네 가지 리소스로 구성됩니다.

리소스 범위 용도
Role 네임스페이스 특정 네임스페이스 내 권한 정의
ClusterRole 클러스터 클러스터 전체 권한 정의
RoleBinding 네임스페이스 Role/ClusterRole을 주체에 바인딩
ClusterRoleBinding 클러스터 ClusterRole을 클러스터 전체에 바인딩

Role 정의

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: prod
  name: pod-reader
rules:
- apiGroups: [""]           # "" = core API group
  resources: ["pods"]
  verbs: ["get", "watch", "list"]
  
- apiGroups: [""]
  resources: ["pods/log"]   # 하위 리소스
  verbs: ["get"]

Verbs 정리

Verb 설명 HTTP 메서드
get 단일 리소스 조회 GET
list 리소스 목록 조회 GET
watch 변경 감시 GET (watch=true)
create 리소스 생성 POST
update 리소스 전체 업데이트 PUT
patch 리소스 부분 업데이트 PATCH
delete 단일 리소스 삭제 DELETE
deletecollection 여러 리소스 삭제 DELETE

ClusterRole 정의

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: secret-reader
rules:
# 모든 네임스페이스의 secrets 읽기
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "watch", "list"]
  
# 특정 이름의 secrets만 (resourceNames)
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["db-credentials", "api-key"]
  verbs: ["get"]
  
# 클러스터 범위 리소스
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "list"]
  
# 비-리소스 URL
- nonResourceURLs: ["/healthz", "/healthz/*"]
  verbs: ["get"]

RoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: prod
subjects:
# 사용자
- kind: User
  name: jane
  apiGroup: rbac.authorization.k8s.io
  
# 그룹
- kind: Group
  name: developers
  apiGroup: rbac.authorization.k8s.io
  
# ServiceAccount
- kind: ServiceAccount
  name: app-sa
  namespace: prod
  
roleRef:
  kind: Role  # 또는 ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

기본 ClusterRole 활용

Kubernetes가 기본 제공하는 ClusterRole을 활용하면 대부분의 권한 설계를 처음부터 만들 필요가 없습니다.

ClusterRole 설명
cluster-admin 모든 권한 (슈퍼유저)
admin 네임스페이스 내 모든 권한 (RBAC 포함)
edit 대부분의 리소스 CRUD (RBAC 제외)
view 읽기 전용 (Secrets 제외)

최소 권한 설계 패턴

개발자 그룹에게 프로덕션은 읽기 전용(view), 개발·스테이징은 편집 권한(edit)을 부여하는 패턴이 대표적입니다.

# 1. 프로덕션: 읽기 전용
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developers-view
  namespace: prod
subjects:
- kind: Group
  name: developers
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io
---
# 2. 개발/스테이징: 편집 권한
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developers-edit
  namespace: dev
subjects:
- kind: Group
  name: developers
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

권한 확인 명령어

# 현재 사용자 권한 확인
kubectl auth can-i create pods --namespace=prod
# yes 또는 no

# 특정 사용자/SA 권한 확인
kubectl auth can-i create pods --namespace=prod --as=system:serviceaccount:prod:app-sa

# 모든 권한 나열
kubectl auth can-i --list --namespace=prod

NetworkPolicy: Zero Trust 네트워크

중요

NetworkPolicy는 CNI 플러그인 지원이 필요합니다. Calico, Cilium, Weave Net 등이 지원하며, AWS VPC CNI 단독으로는 지원하지 않습니다.

정책이 없으면 클러스터 내 모든 Pod가 서로 자유롭게 통신합니다. NetworkPolicy를 적용하면 명시적으로 허용한 경로만 남습니다.

기본 구조

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
  namespace: prod
spec:
  # 이 정책이 적용될 Pod
  podSelector:
    matchLabels:
      app: backend
  
  # 정책 유형 (Ingress, Egress, 또는 둘 다)
  policyTypes:
  - Ingress
  - Egress
  
  # 인바운드 트래픽 규칙
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  
  # 아웃바운드 트래픽 규칙
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432

from/to 셀렉터 유형

셀렉터 설명
podSelector 같은 네임스페이스의 Pod
namespaceSelector 다른 네임스페이스의 모든 Pod
podSelector + namespaceSelector 특정 네임스페이스의 특정 Pod
ipBlock CIDR 기반 IP 범위
ingress:
# OR 관계 (배열의 각 항목)
- from:
  # 1. 같은 네임스페이스의 frontend Pod
  - podSelector:
      matchLabels:
        app: frontend
  
  # 2. monitoring 네임스페이스의 모든 Pod
  - namespaceSelector:
      matchLabels:
        name: monitoring
  
  # 3. 특정 IP 대역
  - ipBlock:
      cidr: 10.0.0.0/8
      except:
      - 10.1.0.0/16

# AND 관계 (같은 항목 내)
- from:
  - namespaceSelector:
      matchLabels:
        env: prod
    podSelector:
      matchLabels:
        role: client

주의

podSelectornamespaceSelector가 같은 - 아래에 있으면 AND 조건입니다. 별도의 -로 분리하면 OR 조건입니다.

Default Deny 정책 (Zero Trust 기반)

# 1. 모든 인바운드 트래픽 차단 (기본)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: prod
spec:
  podSelector: {}  # 모든 Pod에 적용
  policyTypes:
  - Ingress
  # ingress 규칙 없음 = 모든 인바운드 차단
---
# 2. 모든 아웃바운드 트래픽 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
  - Egress
  # DNS는 허용 (필수)
  egress:
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53

실전 예시: 3-tier 앱

인터넷 → Frontend → Backend → Database로만 흐르고, 그 외 경로는 모두 차단하는 구성입니다.

flowchart LR
    Internet["인터넷"] --> FE["Frontend\n(app: frontend)"]
    FE --> BE["Backend\n(app: backend)"]
    BE --> DB["Database\n(app: database)"]
    
    FE -.-x DB
    Internet -.-x BE
    Internet -.-x DB
# Frontend: 외부에서만 접근 가능
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-policy
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  # 외부(Ingress Controller)에서 접근
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - port: 80
  egress:
  # Backend로만 나감
  - to:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - port: 8080
  # DNS 허용
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
---
# Backend: Frontend에서만 접근, Database로만 나감
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - port: 5432
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
---
# Database: Backend에서만 접근, 외부 나감 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-policy
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - port: 5432
  egress: []  # 아웃바운드 없음

Pod Security Standards

노트

Pod Security Admission은 Kubernetes 1.25에서 GA가 되었으며, 더 이상 PodSecurityPolicy(PSP)를 사용하지 않습니다.

3가지 보안 레벨과 3가지 적용 모드

레벨 설명 차단 항목
privileged 제한 없음 없음
baseline 알려진 권한 상승 차단 hostNetwork, hostPID, privileged 컨테이너
restricted 최대 보안 root 사용자, 쓰기 가능 루트 파일시스템 등
모드 설명
enforce 정책 위반 시 Pod 생성 차단
audit 정책 위반 시 감사 로그 기록
warn 정책 위반 시 경고 메시지 표시

네임스페이스 라벨로 적용

# baseline 강제, restricted 경고/감사
kubectl label namespace prod \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest
# 또는 YAML로 정의
apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest

클러스터 전체 기본값 설정

# /etc/kubernetes/pss-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    apiVersion: pod-security.admission.config.k8s.io/v1
    kind: PodSecurityConfiguration
    defaults:
      enforce: "baseline"
      enforce-version: "latest"
      audit: "restricted"
      audit-version: "latest"
      warn: "restricted"
      warn-version: "latest"
    exemptions:
      usernames: []
      runtimeClasses: []
      namespaces:
      - kube-system
      - kube-public

Restricted 레벨을 통과하는 Pod 예시

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  
  containers:
  - name: app
    image: my-app:latest
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 1000
      capabilities:
        drop:
        - ALL
    
    # 쓰기 필요한 디렉토리는 emptyDir 사용
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /app/cache
  
  volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}

ServiceAccount와 IRSA

토큰 자동 마운트 비활성화

모든 Pod에는 기본적으로 ServiceAccount 토큰이 마운트됩니다. API Server 접근이 필요 없다면 비활성화하는 것이 안전합니다.

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  automountServiceAccountToken: false
  containers:
  - name: app
    image: my-app:latest

전용 ServiceAccount와 최소 권한

# 최소 권한 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: prod
automountServiceAccountToken: false  # 기본 비활성화
---
# 필요한 권한만 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-role
  namespace: prod
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["app-config"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-rolebinding
  namespace: prod
subjects:
- kind: ServiceAccount
  name: app-sa
  namespace: prod
roleRef:
  kind: Role
  name: app-role
  apiGroup: rbac.authorization.k8s.io

AWS IRSA (IAM Roles for Service Accounts)

EKS에서는 ServiceAccount에 IAM Role을 연결해, AWS 자격 증명을 Pod에 하드코딩하지 않고 임시 자격 증명으로 AWS API를 호출할 수 있습니다.

# ServiceAccount에 IAM Role 연결
apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-reader
  namespace: prod
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/S3ReaderRole
---
apiVersion: v1
kind: Pod
metadata:
  name: s3-reader-pod
  namespace: prod
spec:
  serviceAccountName: s3-reader
  containers:
  - name: app
    image: amazon/aws-cli
    command: ["aws", "s3", "ls"]
    # AWS SDK가 자동으로 IRSA 토큰 사용
sequenceDiagram
    participant Pod as Pod
    participant IRSA as AWS STS
    participant S3 as AWS S3
    
    Pod->>Pod: SA 토큰 마운트됨\n(/var/run/secrets/eks.amazonaws.com/serviceaccount/token)
    Pod->>IRSA: AssumeRoleWithWebIdentity\n(SA 토큰 전달)
    IRSA-->>Pod: 임시 AWS 자격 증명
    Pod->>S3: S3 API 호출\n(임시 자격 증명 사용)
    S3-->>Pod: 응답

트러블슈팅 가이드

세 영역에서 자주 만나는 증상과 확인 순서를 모았습니다.

VirtualService가 적용되지 않을 때

# 1. Gateway와 VirtualService 연결 확인
kubectl get vs my-vs -o yaml | grep gateways -A 5

# 2. Proxy 설정 동기화 확인
istioctl proxy-status

# 3. Envoy 설정 확인
istioctl proxy-config routes deploy/my-app

흔한 원인: Gateway 이름/네임스페이스 불일치, hosts 설정 불일치, 네임스페이스에 istio-injection 라벨 누락.

503 Service Unavailable

# 1. 대상 서비스 확인
kubectl get svc api-service
kubectl get endpoints api-service

# 2. DestinationRule subset 확인
kubectl get dr api-service-dr -o yaml

# 3. Pod 라벨 확인
kubectl get pods --show-labels

흔한 원인: DestinationRule의 subset 라벨과 Pod 라벨 불일치, 서비스 포트 설정 오류, mTLS 모드 불일치.

mTLS 연결 실패

# 1. PeerAuthentication 확인
kubectl get peerauthentication -A

# 2. 특정 워크로드의 mTLS 상태 확인
istioctl authn tls-check deploy/my-app

# 3. 인증서 상태 확인
istioctl proxy-config secret deploy/my-app -o json

HPA가 스케일링하지 않을 때

# 1. HPA 상태 확인
kubectl get hpa api-hpa

# TARGETS가 <unknown>인 경우
# NAME      REFERENCE             TARGETS         MINPODS   MAXPODS
# api-hpa   Deployment/api-server <unknown>/70%   2         10

# 2. Metrics Server 확인
kubectl get deployment metrics-server -n kube-system

# 3. Pod의 resource requests 확인 (필수!)
kubectl get pod api-xxx -o yaml | grep -A 5 resources

흔한 원인: Metrics Server 미설치, Pod에 resource requests 미설정(HPA 작동 불가), 메트릭 수집 지연(15초 주기).

VPA 추천이 적용되지 않을 때

# 1. VPA 상태 확인
kubectl describe vpa api-vpa

# 2. updateMode 확인 (Off 모드는 추천만 제공)

# 3. Pod 재생성 여부 확인
kubectl get pods -w

흔한 원인: updateMode: Off 설정, minAllowed/maxAllowed 범위 밖, PDB(PodDisruptionBudget)로 인한 재생성 차단.

Karpenter 노드가 프로비저닝되지 않을 때

# 1. Karpenter 로그 확인
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter

# 2. NodePool 상태 확인
kubectl describe nodepool default

# 3. Pending Pod 확인
kubectl get pods --field-selector=status.phase=Pending

흔한 원인: requirements를 충족하는 인스턴스 타입 없음, 서브넷/보안 그룹 태그 누락, IAM 권한 부족.

RBAC 권한 오류

# 에러: User "jane" cannot list pods in namespace "prod"

# 1. 권한 확인
kubectl auth can-i list pods --namespace=prod --as=jane

# 2. RoleBinding 확인
kubectl get rolebindings -n prod -o wide

# 3. ClusterRoleBinding 확인
kubectl get clusterrolebindings -o wide | grep jane

# 4. 자세한 권한 조회
kubectl auth can-i --list --namespace=prod --as=jane

NetworkPolicy가 작동하지 않을 때

# 1. CNI 플러그인 확인 (Calico, Cilium 등)
kubectl get pods -n kube-system | grep calico

# 2. NetworkPolicy 확인
kubectl get networkpolicy -n prod

# 3. Pod 라벨 확인
kubectl get pods -n prod --show-labels

# 4. 정책 상세 확인
kubectl describe networkpolicy backend-policy -n prod

흔한 원인: CNI 미지원(AWS VPC CNI 단독은 NetworkPolicy 미지원), podSelector/namespaceSelector 라벨 오타, egress에서 DNS(UDP 53) 미허용.

Pod Security 위반

# 경고 메시지 예시
# Warning: would violate PodSecurity "restricted:latest"

# 1. 네임스페이스 라벨 확인
kubectl get ns prod --show-labels

# 2. Pod 보안 컨텍스트 확인
kubectl get pod my-pod -o yaml | grep -A 20 securityContext

# 3. 위반 항목 확인 (dry-run)
kubectl apply -f pod.yaml --dry-run=server

흔한 원인: runAsNonRoot 미설정, allowPrivilegeEscalation: true, capabilities.drop: ALL 미설정.

정리

프로덕션 Kubernetes 운영의 3대 축을 구성요소별로 요약하면 다음과 같습니다.

구성요소 핵심 역할
트래픽 제어 Gateway 외부 트래픽 진입점, 포트/프로토콜/TLS 설정
VirtualService L7 라우팅 규칙 (경로, 헤더, 가중치)
DestinationRule 트래픽 정책 (LB, 서킷 브레이커, subset)
PeerAuthentication / AuthorizationPolicy mTLS 모드, 접근 제어
용량 관리 HPA Pod 수 조절, Resource/Custom/External 메트릭
VPA Pod 리소스 추천 및 자동 조정
Cluster Autoscaler / Karpenter 노드 수 조절, 자동 프로비저닝과 Disruption
KEDA 이벤트 기반 스케일링, 0으로 스케일 다운
보안 RBAC 최소 권한 인가
NetworkPolicy ingress/egress 제어, Zero Trust
PSS Privileged/Baseline/Restricted 워크로드 보안
ServiceAccount / IRSA 워크로드 ID와 AWS 권한 연동

운영 관점의 요점은 세 가지입니다. 첫째, 와일드카드 서브도메인 패턴(Route 53 + ACM + ALB + Istio Gateway)을 깔아 두면 서비스 추가가 VirtualService 하나로 끝납니다. 둘째, 스케일링은 HPA 하나로 끝나지 않습니다. Pod 수(HPA/KEDA), Pod 크기(VPA), 노드(Karpenter)를 함께 설계하되 HPA와 VPA가 같은 메트릭을 다투지 않게 해야 합니다. 셋째, 보안은 Default Deny에서 시작해 필요한 것만 열어 가는 방향이 결과적으로 관리 비용이 가장 적습니다.