· 63분 읽기
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
주의
podSelector와 namespaceSelector가 같은 - 아래에 있으면 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에서 시작해 필요한 것만 열어 가는 방향이 결과적으로 관리 비용이 가장 적습니다.