EKS 장애 진단 및 대응 가이드
📅 작성일: 2026-02-10 | 수정일: 2026-02-13 | ⏱️ 읽는 시간: 약 23분
📌 기준 환경: EKS 1.30+, kubectl 1.30+, AWS CLI v2
1. 개요
EKS 운영 중 발생하는 문제는 컨트롤 플레인, 노드, 네트워크, 워크로드, 스토리지, 옵저버빌리티 등 다양한 레이어에 걸쳐 나타납니다. 본 문서는 SRE, DevOps 엔지니어, 플랫폼 팀이 이러한 문제를 체계적으로 진단하고 신속하게 해결하기 위한 종합 디버깅 가이드입니다.
모든 명령어와 예제는 즉시 실행 가능하도록 작성되었으며, Decision Tree와 플로우차트를 통해 빠른 판단을 돕습니다.
EKS 디버깅 레이어
디버깅 접근 방법론
EKS 문제 진단에는 두 가지 접근 방식이 있습니다.
| 접근 방식 | 설명 | 적합한 상황 |
|---|---|---|
| Top-down (증상 → 원인) | 사용자가 보고한 증상에서 시작하여 원인을 추적 | 서비스 장애, 성능 저하 등 즉각적인 문제 대응 |
| Bottom-up (인프라 → 앱) | 인프라 레이어부터 순 차적으로 점검 | 예방적 점검, 클러스터 마이그레이션 후 검증 |
프로덕션 인시던트에서는 Top-down 접근을 권장합니다. 먼저 증상을 파악하고 (Section 2 인시던트 트리아지), 해당 레이어의 디버깅 섹션으로 이동하세요.
2. 인시던트 트리아지 (빠른 장애 판단)
First 5 Minutes 체크리스트
인시던트 발생 시 가장 중요한 것은 스코프 판별과 초동 대응입니다.
30초: 초기 진단
# 클러스터 상태 확인
aws eks describe-cluster --name <cluster-name> --query 'cluster.status' --output text
# 노드 상태 확인
kubectl get nodes
# 비정상 Pod 확인
kubectl get pods --all-namespaces | grep -v Running | grep -v Completed
2분: 스코프 판별
# 최근 이벤트 확인 (전체 네임스페이스)
kubectl get events --all-namespaces --sort-by='.lastTimestamp' | tail -20
# 특정 네임스페이스 Pod 상태 집계
kubectl get pods -n <namespace> --no-headers | awk '{print $3}' | sort | uniq -c | sort -rn
# 노드별 비정상 Pod 분포 확인
kubectl get pods --all-namespaces -o wide --field-selector=status.phase!=Running | \
awk 'NR>1 {print $8}' | sort | uniq -c | sort -rn
5분: 초동 대응
# 문제 Pod의 상세 정보
kubectl describe pod <pod-name> -n <namespace>
# 이전 컨테이너 로그 (CrashLoopBackOff인 경우)
kubectl logs <pod-name> -n <namespace> --previous
# 리소스 사용량 확인
kubectl top nodes
kubectl top pods -n <namespace> --sort-by=cpu
스코프 판별 Decision Tree
AZ 장애 감지
aws health describe-events API는 AWS Business 또는 Enterprise Support 플랜에서만 사용 가능합니다. Support 플랜이 없는 경우 AWS Health Dashboard 콘솔에서 직접 확인하거나, EventBridge 규칙으로 Health 이벤트를 캡처하세요.
# AWS Health API로 EKS/EC2 관련 이벤트 확인 (Business/Enterprise Support 플랜 필요)
aws health describe-events \
--filter '{"services":["EKS","EC2"],"eventStatusCodes":["open"]}' \
--region us-east-1
# 대안: Support 플랜 없이 AZ 장애 감지 — EventBridge 규칙 생성
aws events put-rule \
--name "aws-health-eks-events" \
--event-pattern '{
"source": ["aws.health"],
"detail-type": ["AWS Health Event"],
"detail": {
"service": ["EKS", "EC2"],
"eventTypeCategory": ["issue"]
}
}'
# AZ별 비정상 Pod 집계 (노드에 스케줄링된 Pod만 대상)
kubectl get pods --all-namespaces -o json | jq -r '
.items[] |
select(.status.phase != "Running" and .status.phase != "Succeeded") |
select(.spec.nodeName != null) |
.spec.nodeName
' | sort -u | while read node; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}' 2>/dev/null)
[ -n "$zone" ] && echo "$zone"
done | sort | uniq -c | sort -rn
# ARC Zonal Shift 상태 확인
aws arc-zonal-shift list-zonal-shifts \
--resource-identifier arn:aws:eks:region:account:cluster/name
ARC Zonal Shift를 사용한 AZ 장애 대응
# EKS에서 Zonal Shift 활성화
aws eks update-cluster-config \
--name <cluster-name> \
--zonal-shift-config enabled=true
# 수동 Zonal Shift 시작 (장애 AZ 로부터 트래픽 이동)
aws arc-zonal-shift start-zonal-shift \
--resource-identifier arn:aws:eks:region:account:cluster/name \
--away-from us-east-1a \
--expires-in 3h \
--comment "AZ impairment detected"
Zonal Shift의 최대 지속 시간은 3일이며 연장 가능합니다. Shift를 시작하면 해당 AZ의 노드에서 실행 중인 Pod으로의 새로운 트래픽이 차단되므 로, 다른 AZ에 충분한 용량이 있는지 먼저 확인하세요.
ARC Zonal Shift는 Load Balancer / Service 레벨의 트래픽 라우팅만 변경합니다.
Karpenter NodePool, ASG(Managed Node Group)의 AZ 설정은 자동으로 업데이트되지 않습니다. 따라서 완전한 AZ 대피를 위해서는 추가 작업이 필요합니다:
- Zonal Shift 시작 → 새 트래픽 차단 (자동)
- 해당 AZ 노 드 drain → 기존 Pod 이동
- Karpenter NodePool 또는 ASG 서브넷에서 해당 AZ 제거 → 새 노드 프로비저닝 방지
# 1. 장애 AZ의 노드 식별 및 drain
for node in $(kubectl get nodes -l topology.kubernetes.io/zone=us-east-1a -o name); do
kubectl cordon $node
kubectl drain $node --ignore-daemonsets --delete-emptydir-data --grace-period=60
done
# 2. Karpenter NodePool에서 해당 AZ 일시 제외 (requirements 수정)
kubectl patch nodepool default --type=merge -p '{
"spec": {"template": {"spec": {"requirements": [
{"key": "topology.kubernetes.io/zone", "operator": "In", "values": ["us-east-1b", "us-east-1c"]}
]}}}
}'
# 3. Managed Node Group은 ASG 서브넷 변경이 필요 (콘솔 또는 IaC에서 수행)
Zonal Shift 해제 후에는 위 변경사항을 원복해야 합니다.
CloudWatch 이상 탐지
# Pod 재시작 횟수에 대한 Anomaly Detection 알람 설정
aws cloudwatch put-anomaly-detector \
--single-metric-anomaly-detector '{
"Namespace": "ContainerInsights",
"MetricName": "pod_number_of_container_restarts",
"Dimensions": [
{"Name": "ClusterName", "Value": "<cluster-name>"},
{"Name": "Namespace", "Value": "production"}
],
"Stat": "Average"
}'
인시던트 대응 에스컬레이션 매트릭스
아키텍처 수준의 장애 회복 전략(TopologySpreadConstraints, PodDisruptionBudget, 멀티AZ 배포 등)은 EKS 고가용성 아키텍처 가이드를 참조하세요.
3. EKS 컨트롤 플레인 디버깅
컨트롤 플레인 로그 타입
EKS 컨트롤 플레인은 5가지 로그 타입을 CloudWatch Logs에 전송할 수 있습니다.
apikube-apiserverkube-apiserver-audit-*auditkube-apiserver-auditkube-apiserver-audit-*authenticatoraws-iam-authenticatorauthenticator-*controllerManagerkube-controller-managerkube-controller-manager-*schedulerkube-schedulerscheduler-*로그 활성화
# 모든 컨트롤 플레인 로그 활성화
aws eks update-cluster-config \
--region <region> \
--name <cluster-name> \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'
모든 로그 타입을 활성화하면 CloudWatch Logs 비용이 증가합니다. 프로덕션에서는 audit과 authenticator를 필수로 활성화하고, 디버깅이 필요할 때 나머지를 추가 활성화하는 전략을 권장합니다.
CloudWatch Logs Insights 쿼리
API 서버 에러 (400+) 분석
fields @timestamp, @message
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code >= 400
| stats count() by responseStatus.code
| sort count desc
인증 실패 추적
fields @timestamp, @message
| filter @logStream like /authenticator/
| filter @message like /error/ or @message like /denied/
| sort @timestamp desc
aws-auth ConfigMap 변경 감지
fields @timestamp, @message
| filter @logStream like /kube-apiserver-audit/
| filter objectRef.resource = "configmaps" and objectRef.name = "aws-auth"
| filter verb in ["update", "patch", "delete"]
| sort @timestamp desc
API Throttling 탐지
fields @timestamp, @message
| filter @logStream like /kube-apiserver/
| filter @message like /throttle/ or @message like /rate limit/
| stats count() by bin(5m)
비인가 접근 시도 (보안 이벤트)
fields @timestamp, @message
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code = 403
| stats count() by user.username
| sort count desc
인증/인가 디버깅
IAM 인증 확인
# 현재 IAM 자격증명 확인
aws sts get-caller-identity
# 클러스터 인증 모드 확인
aws eks describe-cluster --name <cluster-name> \
--query 'cluster.accessConfig.authenticationMode' --output text
aws-auth ConfigMap (CONFIG_MAP 모드)
# aws-auth ConfigMap 확인
kubectl describe configmap aws-auth -n kube-system
EKS Access Entries (API / API_AND_CONFIG_MAP 모드)
# Access Entry 생성
aws eks create-access-entry \
--cluster-name <cluster-name> \
--principal-arn arn:aws:iam::ACCOUNT:role/ROLE-NAME \
--type STANDARD
# Access Entry 목록 확인
aws eks list-access-entries --cluster-name <cluster-name>
IRSA (IAM Roles for Service Accounts) 디버깅 체크리스트
# 1. ServiceAccount에 annotation 확인
kubectl get sa <sa-name> -n <namespace> -o yaml
# 2. Pod 내 AWS 환경변수 확인
kubectl exec -it <pod-name> -- env | grep AWS
# 3. OIDC Provider 확인
aws eks describe-cluster --name <cluster-name> \
--query 'cluster.identity.oidc.issuer' --output text
# 4. IAM Role의 Trust Policy에서 OIDC Provider ARN 및 조건 확인
aws iam get-role --role-name <role-name> \
--query 'Role.AssumeRolePolicyDocument'
- ServiceAccount annotation의 role ARN 오타
- IAM Role Trust Policy에 서 namespace/sa 이름 불일치
- OIDC Provider가 클러스터와 연결되지 않음
- Pod가 ServiceAccount를 사용하도록
spec.serviceAccountName미지정
서비스 어카운트 토큰 만료 (HTTP 401 Unauthorized)
Kubernetes 1.21+에서 서비스 어카운트 토큰은 기본 1시간 유효하며, kubelet에 의해 자동 갱신됩니다. 그러나 레거시 SDK를 사용하는 경우 토큰 갱신 로직이 없어 장기 실행 워크로드에서 401 Unauthorized 에러가 발생할 수 있습니다.
증상:
- Pod이 일정 시간(보통 1시간) 후 갑자기
HTTP 401 Unauthorized에러를 반환 - 재시작 후 일시적으로 정상 동작하다가 다시 401 발생
원인:
- 프로젝티드 서비스 어카운트 토큰(Projected Service Account Token)은 기본 1시간 만료
- kubelet이 토큰을 자동 갱신하지만, 애플리케이션이 파일에서 토큰을 한 번만 읽고 캐싱하면 만료된 토큰을 계속 사용
필요한 최소 SDK 버전:
| 언어 | SDK | 최소 버전 |
|---|---|---|
| Go | client-go | v0.15.7+ |
| Python | kubernetes | 12.0.0+ |
| Java | fabric8 | 5.0.0+ |
SDK가 토큰 자동 갱신을 지원하는지 확인하세요. 지원하지 않는 경우 애플리케이션에서 주기적으로 /var/run/secrets/kubernetes.io/serviceaccount/token 파일을 다시 읽도록 구현해야 합니다.
EKS Pod Identity 디버깅
EKS Pod Identity는 IRSA의 대안으로, 보다 간단한 설정으로 Pod에 AWS IAM 권한을 부여합니다.
# Pod Identity Association 확인
aws eks list-pod-identity-associations --cluster-name $CLUSTER
aws eks describe-pod-identity-association --cluster-name $CLUSTER \
--association-id $ASSOC_ID
# Pod Identity Agent 상태 확인
kubectl get pods -n kube-system -l app.kubernetes.io/name=eks-pod-identity-agent
kubectl logs -n kube-system -l app.kubernetes.io/name=eks-pod-identity-agent --tail=50
Pod Identity 디버깅 체크리스트:
- eks-pod-identity-agent Add-on이 설치되어 있는지
- Pod의 ServiceAccount에 올바른 association이 연결되어 있는지
- IAM Role trust policy에
pods.eks.amazonaws.com서비스 프린시펄이 있는지
Pod Identity는 IRSA보다 설정이 간단하며, cross-account 접근이 더 용이합니다. 신규 워크로드에서는 Pod Identity 사용을 권장합니다.
EKS Add-on 트러블슈팅
# Add-on 목록 확인
aws eks list-addons --cluster-name <cluster-name>
# Add-on 상태 상세 확인
aws eks describe-addon --cluster-name <cluster-name> --addon-name <addon-name>
# Add-on 업데이트 (충돌 해결: PRESERVE로 기존 설정 유지)
aws eks update-addon --cluster-name <cluster-name> --addon-name <addon-name> \
--addon-version <version> --resolve-conflicts PRESERVE
| Add-on | 일반적인 에러 패턴 | 진단 방법 | 해결 방법 |
|---|---|---|---|
| CoreDNS | Pod CrashLoopBackOff, DNS 타임아웃 | kubectl logs -n kube-system -l k8s-app=kube-dns | ConfigMap 점검, kubectl rollout restart deployment coredns -n kube-system |
| kube-proxy | Service 통신 불가, iptables 에러 | kubectl logs -n kube-system -l k8s-app=kube-proxy | DaemonSet 이미지 버전 확인, kubectl rollout restart daemonset kube-proxy -n kube-system |
| VPC CNI | Pod IP 할당 실패, ENI 에러 | kubectl logs -n kube-system -l k8s-app=aws-node | IPAMD 로그 확인, ENI/IP 한도 점검 (Section 6 참조) |
| EBS CSI | PVC Pending, 볼륨 attach 실패 | kubectl logs -n kube-system -l app.kubernetes.io/name=aws-ebs-csi-driver | IRSA 권한, AZ 매칭 확인 (Section 7 참조) |
클러스터 헬스 이슈 코드
EKS 클러스터 자체의 인프라 수준 문제를 진단할 때는 클러스터 헬스 상태를 확인합니다.
# 클러스터 헬스 이슈 확인
aws eks describe-cluster --name $CLUSTER \
--query 'cluster.health' --output json
SUBNET_NOT_FOUNDSECURITY_GROUP_NOT_FOUNDIP_NOT_AVAILABLEVPC_NOT_FOUNDASSUME_ROLE_ACCESS_DENIEDKMS_KEY_DISABLEDKMS_KEY_NOT_FOUNDVPC_NOT_FOUND와 KMS_KEY_NOT_FOUND는 복구가 불가능합니다. 클러스터를 새로 생성해야 합니다.
4. 노드 레벨 디버깅
노드 조인 실패 디버깅
노드가 클러스터에 조인하지 못하는 경우 다양한 원인이 있습니다. 다음은 가장 흔한 8가지 원인과 진단 방법입니다.
노드 조인 실패의 일반적인 원인:
- aws-auth ConfigMap에 노드 IAM Role이 등록되지 않음 (또는 Access Entry 미생성) — 노드가 API 서버에 인증할 수 없음
- 부트스트랩 스크립트의 ClusterName이 실제 클러스터명과 불일치 — kubelet이 잘못된 클러스터에 연결 시도
- 노드 보안그룹이 컨트롤 플레인과의 통신을 허용하지 않음 — TCP 443 (API 서버), TCP 10250 (kubelet) 포트가 필요
- 퍼블릭 서브넷에서 auto-assign public IP가 비활성화됨 — 퍼블릭 엔드포인트만 활성화된 클러스터에서 인터넷 접근 불가
- VPC DNS 설정 문제 —
enableDnsHostnames,enableDnsSupport가 비활성화됨 - STS 리전 엔드포인트가 비활성화됨 — IAM 인증 시 STS 호출 실패
- 인스턴스 프로파일 ARN을 노드 IAM Role ARN 대신 aws-auth에 등록 — aws-auth에는 Role ARN만 등록해야 함
eks:kubernetes.io/cluster-name태그 누락 (자체관리형 노드) — EKS가 노드를 클러스터 소속으로 인식하지 못함
진단 명령어:
# 노드 부트스트랩 로그 확인 (SSM 접속 후)
sudo journalctl -u kubelet --no-pager | tail -50
sudo cat /var/log/cloud-init-output.log | tail -50
# 보안그룹 규칙 확인
aws ec2 describe-security-groups --group-ids $CLUSTER_SG \
--query 'SecurityGroups[].IpPermissions' --output table
# VPC DNS 설정 확인
aws ec2 describe-vpc-attribute --vpc-id $VPC_ID --attribute enableDnsHostnames
aws ec2 describe-vpc-attribute --vpc-id $VPC_ID --attribute enableDnsSupport
aws-auth ConfigMap에는 인스턴스 프로파일 ARN (arn:aws:iam::ACCOUNT:instance-profile/...)이 아닌, IAM Role ARN (arn:aws:iam::ACCOUNT:role/...)을 등록해야 합니다. 이 실수는 매우 빈번하며 노드 조인 실패의 주요 원인입니다.
Node NotReady Decision Tree
kubelet / containerd 디버깅
# SSM을 통한 노드 접속
aws ssm start-session --target <instance-id>
# kubelet 상태 확인
systemctl status kubelet
journalctl -u kubelet -n 100 -f
# containerd 상태 확인
systemctl status containerd
# 컨테이너 런타임 상태 확인
crictl pods
crictl ps -a
# 특정 컨테이너 로그 확인
crictl logs <container-id>
SSM 접속을 위해서는 노드의 IAM Role에 AmazonSSMManagedInstanceCore 정책이 연결되어 있어야 합니다. EKS 관리형 노드 그룹에서는 기본 포함되지만, 커스텀 AMI를 사용하는 경우 SSM Agent 설치를 확인하세요.
리소스 압박 진단 및 해결
# 노드 상태 확인
kubectl describe node <node-name>
| Condition | 임계값 | 진단 명령어 | 해결 방법 |
|---|---|---|---|
| DiskPressure | 사용 가능 디스크 < 10% | df -h (SSM 접속 후) | crictl rmi --prune 으로 미사용 이미지 정리, crictl rm 으로 중지된 컨테이너 삭제 |
| MemoryPressure | 사용 가능 메모리 < 100Mi | free -m (SSM 접속 후) | 저우선순위 Pod 축출, 메모리 requests/limits 조정, 노드 교체 |
| PIDPressure | 사용 가능 PID < 5% | ps aux | wc -l (SSM 접속 후) | kernel.pid_max 증가, PID leak 원인 컨테이너 식별 및 재시작 |
Karpenter 노드 프로비저닝 디버깅
# Karpenter 컨트롤러 로그 확인
kubectl logs -f deployment/karpenter -n kube-system
# NodePool 상태 확인
kubectl get nodepool
kubectl describe nodepool <nodepool-name>
# EC2NodeClass 확인
kubectl get ec2nodeclass
kubectl describe ec2nodeclass <nodeclass-name>
# 프로비저닝 실패 시 확인 사항:
# 1. NodePool의 limits가 초과되지 않았는지
# 2. EC2NodeClass의 서브넷/보안그룹 셀렉터가 올바른지
# 3. 인스턴스 타입에 대한 Service Quotas가 충분한지
# 4. Pod의 nodeSelector/affinity가 NodePool requirements와 매칭되는지
Karpenter v1.0+에서는 Provisioner → NodePool, AWSNodeTemplate → EC2NodeClass로 변경되었습니다. 기존 v0.x 설정을 사용 중이라면 마이그레이션이 필요합니다. API 그룹도 karpenter.sh/v1로 업데이트하세요.
Managed Node Group 에러 코드
Managed Node Group의 헬스 상태를 확인하여 프로비저닝 및 운영 문제를 진단합니다.
# 노드 그룹 헬스 상태 확인
aws eks describe-nodegroup --cluster-name $CLUSTER --nodegroup-name $NODEGROUP \
--query 'nodegroup.health' --output json
AccessDeniedAmiIdNotFoundAutoScalingGroupNotFoundClusterUnreachableEc2SecurityGroupNotFoundEc2LaunchTemplateNotFoundEc2LaunchTemplateVersionMismatchIamInstanceProfileNotFoundIamNodeRoleNotFoundAsgInstanceLaunchFailuresNodeCreationFailureInstanceLimitExceededInsufficientFreeAddressesInternalFailureAccessDenied 에러 복구 — eks:node-manager ClusterRole 확인:
AccessDenied 에러는 주로 eks:node-manager ClusterRole 또는 ClusterRoleBinding이 삭제되거나 변경된 경우 발생합니다.
# eks:node-manager ClusterRole 확인
kubectl get clusterrole eks:node-manager
kubectl get clusterrolebinding eks:node-manager
eks:node-manager ClusterRole/ClusterRoleBinding이 누락된 경우, EKS는 이를 자동으로 복원하지 않습니다. 다음 방법으로 직접 복구해야 합니다:
방법 1: 수동 재생성 (권장)
# eks-node-manager-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: eks:node-manager
rules:
- apiGroups: ['']
resources: [pods]
verbs: [get, list, watch, delete]
- apiGroups: ['']
resources: [nodes]
verbs: [get, list, watch, patch]
- apiGroups: ['']
resources: [pods/eviction]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: eks:node-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: eks:node-manager
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: eks:node-manager
kubectl auth reconcile -f eks-node-manager-role.yaml
방법 2: 노드 그룹 재생성
# 새 노드 그룹 생성 시 RBAC 리소스가 함께 생성됨
eksctl create nodegroup --cluster=<cluster-name> --name=<new-nodegroup-name>
방법 3: 노드 그룹 업그레이드
# 업그레이드 과정에서 RBAC 재설정이 트리거될 수 있음
eksctl upgrade nodegroup --cluster=<cluster-name> --name=<nodegroup-name>
참고: Kubernetes 기본 시스템 ClusterRole(
system:*)은 API 서버가 자동 reconcile하지만, EKS 전용 ClusterRole(eks:*)은 자동 복원 대상이 아닙니다. RBAC 리소스를 삭제하기 전에 반드시 백업하세요.
Node Readiness Controller를 활용한 노드 부트스트랩 디버깅
Node Readiness Controller는 Kubernetes 공식 블로그에서 발표된 새로운 프로젝트로, 노드 부트스트랩 과정에서 발생하는 조기 스케줄링 문제를 선언적으로 해결합니다.
문제 상황
기존 Kubernetes에서는 노드가 Ready 상태가 되면 즉시 워크로드가 스케줄링됩니다. 하지만 실제로는 아직 준비가 완료되지 않은 경우가 많습니다:
| 미완료 구성 요소 | 증상 | 영향 |
|---|---|---|
| GPU 드라이버/펌웨어 로딩 중 | nvidia-smi 실패, Pod CrashLoopBackOff | GPU 워크로드 실패 |
| CNI 플러그인 초기화 중 | Pod IP 미할당, NetworkNotReady | 네트워크 통신 불가 |
| CSI 드라이버 미등록 | PVC Pending, volume mount 실패 | 스토리지 접근 불가 |
| 보안 에이전트 미설치 | 컴플라이언스 위반 | 보안 정책 미충족 |
Node Readiness Controller 동작 원리
Node Readiness Controller는 커스텀 taint를 선언적으로 관리하여, 모든 인프라 요구사항이 충족될 때까지 워크로드 스케줄링을 지연시킵니다:
디버깅 체크리스트
노드가 Ready인데 Pod가 스케줄링되지 않는 경우:
# 1. 노드의 커스텀 readiness taint 확인
kubectl get node <node-name> -o jsonpath='{.spec.taints}' | jq .
# 2. node.readiness 관련 taint 필터링
kubectl get nodes -o json | jq '
.items[] |
select(.spec.taints // [] | any(.key | startswith("node.readiness"))) |
{name: .metadata.name, taints: [.spec.taints[] | select(.key | startswith("node.readiness"))]}
'
# 3. Pod의 tolerations와 노드 taint 불일치 확인
kubectl describe pod <pending-pod> | grep -A 20 "Events:"
관련 기능: Pod Scheduling Readiness (K8s 1.30 GA)
schedulingGates를 사용하면 Pod 측에서도 스케줄링 준비 상태를 제어할 수 있습니다:
apiVersion: v1
kind: Pod
metadata:
name: gated-pod
spec:
schedulingGates:
- name: "example.com/gpu-validation" # 이 gate가 제거될 때까지 스케줄링 대기
containers:
- name: app
image: app:latest
# schedulingGates가 있는 Pod 확인
kubectl get pods -o json | jq '
.items[] |
select(.spec.schedulingGates != null and (.spec.schedulingGates | length > 0)) |
{name: .metadata.name, namespace: .metadata.namespace, gates: .spec.schedulingGates}
'
관련 기능: Pod Readiness Gates (AWS LB Controller)
AWS Load Balancer Controller는 elbv2.k8s.aws/pod-readiness-gate-inject 어노테이션을 통해 Pod가 ALB/NLB 타겟 등록이 완료될 때까지 Ready 상태 전환을 지연시킵니다:
# Readiness Gate 상태 확인
kubectl get pod <pod-name> -o jsonpath='{.status.conditions}' | jq '
[.[] | select(.type | contains("target-health"))]
'
# Namespace에 readiness gate injection 활성화 확인
kubectl get namespace <ns> -o jsonpath='{.metadata.labels.elbv2\.k8s\.aws/pod-readiness-gate-inject}'
| 기능 | 적용 대상 | 제어 방식 | 상태 |
|---|---|---|---|
| Node Readiness Controller | 노드 | Taint 기반 | New (2026.02) |
| Pod Scheduling Readiness | Pod | schedulingGates | GA (K8s 1.30) |
| Pod Readiness Gates | Pod | Readiness Conditions | GA (AWS LB Controller) |
eks-node-viewer 사용법
eks-node-viewer는 노드의 리소스 사용률을 터미널에서 실시간으로 시각화하는 도구입니다.
# 기본 사용 (CPU 기준)
eks-node-viewer
# CPU와 메모리 함께 확인
eks-node-viewer --resources cpu,memory
# 특정 NodePool만 확인
eks-node-viewer --node-selector karpenter.sh/nodepool=<nodepool-name>
5. 워크로드 디버깅
Pod 상태별 디버깅 플로우차트
기본 디버깅 명령어
# Pod 상태 확인
kubectl get pods -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
# 현재/이전 컨테이너 로그 확인
kubectl logs <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous
# 네임스페이스 이벤트 확인
kubectl get events -n <namespace> --sort-by='.lastTimestamp'
# 리소스 사용량 확인
kubectl top pods -n <namespace>
kubectl debug 활용법
Ephemeral Container (실행 중인 Pod에 디버그 컨테이너 추가)
# 기본 ephemeral container
kubectl debug <pod-name> -it --image=busybox --target=<container-name>
# 네트워크 디버깅 도구가 포함된 이미지
kubectl debug <pod-name> -it --image=nicolaka/netshoot --target=<container-name>
Pod Copy (Pod을 복제하여 디버깅)
# Pod을 복제하고 다른 이미지로 시작
kubectl debug <pod-name> --copy-to=debug-pod --image=ubuntu
# Pod 복제 시 커맨드 변경
kubectl debug <pod-name> --copy-to=debug-pod --container=<container-name> -- sh
Node Debugging (노드에 직접 접근)
# 노드 디버깅 (호스트 파일시스템은 /host에 마운트됨)
kubectl debug node/<node-name> -it --image=ubuntu
kubectl debug node/ 는 SSM Agent가 설치되지 않은 노드에서도 사용 가능합니다. 다만, 호스트 네트워크 네임스페이스에 접근하려면 --profile=sysadmin 옵션을 추가하세요.
Deployment 롤아웃 디버깅
# 롤아웃 상태 확인
kubectl rollout status deployment/<name>
# 롤아웃 히스토리
kubectl rollout history deployment/<name>
# 이전 버전으로 롤백
kubectl rollout undo deployment/<name>
# 특정 리비전으로 롤백
kubectl rollout undo deployment/<name> --to-revision=2
# Deployment 재시작 (Rolling restart)
kubectl rollout restart deployment/<name>
HPA / VPA 디버깅
# HPA 상태 확인
kubectl get hpa
kubectl describe hpa <hpa-name>
# metrics-server 동작 확인
kubectl get deployment metrics-server -n kube-system
kubectl top pods # 이 명령어가 실패하면 metrics-server 문제
# HPA 이벤트에서 스케일링 실패 원인 확인
kubectl describe hpa <hpa-name> | grep -A 5 "Events"
HPA 스케일링 불가 원인 분석:
| 증상 | 원인 | 해결 |
|---|---|---|
unable to get metrics | metrics-server 미설치 또는 장애 | metrics-server Pod 상태 확인 및 재시작 |
current metrics unknown | 대상 Pod에서 메트릭 수집 실패 | Pod의 resource requests 설정 확인 |
target not found | scaleTargetRef 불일치 | Deployment/StatefulSet 이름 및 apiVersion 확인 |
| 스케일업 후 즉시 스케일다운 | stabilizationWindow 미설정 | behavior.scaleDown.stabilizationWindowSeconds 설정 |
Probe 디버깅 및 Best Practices
# 권장 Probe 설정 예제
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8080
# Startup Probe: 앱 시작 완료 확인 (시작이 느린 앱에 필수)
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30 # 최대 300초(30 x 10s) 대기
periodSeconds: 10
# Liveness Probe: 앱이 살아있는지 확인 (데드락 감지)
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
# Readiness Probe: 트래픽 수신 가능 여부 확인
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
- Liveness Probe에 외부 의존성을 포함하지 마세요 (DB 연결 확인 등). 외부 서비스 장애 시 전체 Pod이 재시작되는 cascading failure를 유발합니다.
- startupProbe 없이 높은 initialDelaySeconds를 설정하지 마세요. startupProbe가 성공할 때까지 liveness/readiness probe는 비활성화되므로, 시작이 느린 앱에서는 startupProbe를 사용하세요.
- Readiness Probe 실패는 Pod을 재시작하지 않고 Service Endpoint에서만 제거합니다.
6. 네트워킹 디버깅
네트워킹 디버깅 워크플로우
VPC CNI 디버깅
# VPC CNI Pod 상태 확인
kubectl get pods -n kube-system -l k8s-app=aws-node
# VPC CNI 로그 확인
kubectl logs -n kube-system -l k8s-app=aws-node --tail=50
# 현재 VPC CNI 버전 확인
kubectl describe daemonset aws-node -n kube-system | grep Image
IP 고갈 문제 해결:
# 서브넷별 사용 가능 IP 확인
aws ec2 describe-subnets --subnet-ids <subnet-id> \
--query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,Available:AvailableIpAddressCount}'
# Prefix Delegation 활성화 (IP 용량 16배 확대)
kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true
ENI 제한 및 IP 한도:
각 EC2 인스턴스 타입에 따라 연결 가능한 ENI 수와 ENI당 IP 수가 제한됩니다. Prefix Delegation을 활성화하면 ENI당 IP 할당이 크게 증가합니다.
DNS 트러블슈팅
# CoreDNS Pod 상태 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns
# CoreDNS 로그 확인
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
# DNS 해석 테스트
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup kubernetes.default
# CoreDNS 설정 확인
kubectl get configmap coredns -n kube-system -o yaml
# CoreDNS 재시작
kubectl rollout restart deployment coredns -n kube-system
Kubernetes의 기본 resolv.conf 설정에서 ndots:5는 도메인에 dot이 5개 미만이면 클러스터 내부 DNS 서픽스를 먼저 시도합니다. 외부 도메인 접근 시 불필요한 DNS 쿼리가 4회 추가 발생하여 지연이 증가합니다.
해결: Pod spec에서 dnsConfig.options로 ndots:2를 설정하거나, 외부 도메인 접근 시 FQDN 뒤에 .을 추가하세요 (예: api.example.com.).
참고: VPC DNS 스로틀링 한도는 ENI당 1,024 packets/sec입니다.
Service 디버깅
# Service 상태 확인
kubectl get svc <service-name>
# Endpoints 확인 (백엔드 Pod이 연결되어 있는지)
kubectl get endpoints <service-name>
# Service 상세 정보 (selector 확인)
kubectl describe svc <service-name>
# Selector 확인
kubectl get svc <service-name> -o jsonpath='{.spec.selector}'
# Selector와 일치하는 Pod 확인
kubectl get pods -l <key>=<value>
일반적인 Service 문제:
| 증상 | 확인 사항 | 해결 |
|---|---|---|
| Endpoints가 비어있음 | Service selector와 Pod label 불일치 | label 수정 |
| ClusterIP 접근 불가 | kube-proxy 정상 동작 여부 | kubectl logs -n kube-system -l k8s-app=kube-proxy |
| NodePort 접근 불가 | Security Group에서 30000-32767 허용 여부 | SG Inbound 규칙 추가 |
| LoadBalancer Pending | AWS Load Balancer Controller 설치 여부 | controller 설치 및 IAM 권한 확인 |
NetworkPolicy 디버깅
NetworkPolicy에서 가장 흔한 실수는 AND vs OR 셀렉터의 혼동입니다.
# AND 로직 (같은 from 항목 안에 두 셀렉터를 결합)
# "alice 네임스페이스의 client 역할 Pod" 만 허용
- from:
- namespaceSelector:
matchLabels:
user: alice
podSelector:
matchLabels:
role: client
# OR 로직 (별도의 from 항목으로 분리)
# "alice 네임스페이스의 모든 Pod" 또는 "모든 네임스페이스의 client 역할 Pod" 허용
- from:
- namespaceSelector:
matchLabels:
user: alice
- podSelector:
matchLabels:
role: client
위 두 YAML은 indent 한 레벨 차이로 완전히 다른 보안 정책이 됩니다. AND 로직에서는 namespaceSelector와 podSelector가 같은 - from 항목 안에 있고, OR 로직에서는 별도의 - from 항목으로 분리됩니다.
netshoot 활용법
netshoot은 네트워크 디버깅에 필요한 모든 도구가 포함된 컨테이너 이미지입니다.
# 기존 Pod에 ephemeral container로 추가
kubectl debug <pod-name> -it --image=nicolaka/netshoot
# 독립 디버깅 Pod 실행
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot
# 내부에서 사용할 수 있는 도구 예시:
# - curl, wget: HTTP 테스트
# - dig, nslookup: DNS 테스트
# - tcpdump: 패킷 캡처
# - iperf3: 대역폭 테스트
# - ss, netstat: 소켓 상태 확인
# - traceroute, mtr: 경로 추적
실전 디버깅 시나리오: Pod 간 통신 확인
# netshoot Pod에서 다른 Service로 연결 테스트
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot -- bash
# DNS 해석 확인
dig <service-name>.<namespace>.svc.cluster.local
# TCP 연결 테스트
curl -v http://<service-name>.<namespace>.svc.cluster.local:<port>/health
# 패킷 캡처 (특정 Pod IP로의 트래픽)
tcpdump -i any host <pod-ip> -n
7. 스토리지 디버깅
스토리지 디버깅 Decision Tree
EBS CSI Driver 디버깅
# EBS CSI Driver Pod 상태 확인
kubectl get pods -n kube-system -l app.kubernetes.io/name=aws-ebs-csi-driver
# Controller 로그 확인
kubectl logs -n kube-system -l app=ebs-csi-controller -c ebs-plugin --tail=100
# Node 로그 확인
kubectl logs -n kube-system -l app=ebs-csi-node -c ebs-plugin --tail=100
# IRSA ServiceAccount 확인
kubectl describe sa ebs-csi-controller-sa -n kube-system
EBS CSI Driver 에러 패턴:
| 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
could not create volume | IAM 권한 부족 | IRSA Role에 ec2:CreateVolume, ec2:AttachVolume 등 추가 |
volume is already attached to another node | 이전 노드에서 미분리 | 이전 Pod/노드 정리, EBS 볼륨 detach 대기 (~6분) |
could not attach volume: already at max | 인스턴스 EBS 볼륨 수 제한 초과 | 더 큰 인스턴스 타입 사용 (Nitro 인스턴스: 타입별 상이, 최대 128개. aws ec2 describe-instance-types --instance-types <type> --query 'InstanceTypes[].EbsInfo.MaximumVolumeCount'로 확인) |
failed to provision volume with StorageClass | StorageClass 미존재 또는 설정 오류 | StorageClass 이름/파라미터 확인 |
권장 StorageClass 설정:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: topology-aware-ebs
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer를 사용하면 PVC가 Pod 스케줄링 시점에 바인딩됩니다. 이를 통해 Pod이 스케줄링되는 AZ에 볼륨이 생성되어 AZ 불일치 문제를 방지할 수 있습니다.
EFS CSI Driver 디버깅
# EFS CSI Driver Pod 상태 확인
kubectl get pods -n kube-system -l app.kubernetes.io/name=aws-efs-csi-driver
# Controller 로그 확인
kubectl logs -n kube-system -l app=efs-csi-controller -c efs-plugin --tail=100
# EFS 파일시스템 상태 확인
aws efs describe-file-systems --file-system-id <fs-id>
# Mount Target 확인 (각 AZ에 존재해야 함)
aws efs describe-mount-targets --file-system-id <fs-id>
EFS 체크리스트:
- Mount Target이 Pod이 실행되는 모든 AZ의 서브넷에 존재하는지 확인
- Mount Target의 Security Group이 TCP 2049 (NFS) 포트를 허용하는지 확인
- 노드의 Security Group에서 EFS Mount Target으로의 아웃바운드 TCP 2049 허용 확인
PV/PVC 상태 확인 및 stuck 해결
# PVC 상태 확인
kubectl get pvc -n <namespace>
# PV 상태 확인
kubectl get pv
# PVC가 Terminating에서 멈춘 경우 (finalizer 제거)
kubectl patch pvc <pvc-name> -n <namespace> -p '{"metadata":{"finalizers":null}}'
# PV가 Released 상태에서 Available로 변경 (재사용 시)
kubectl patch pv <pv-name> -p '{"spec":{"claimRef":null}}'
Finalizer를 수동으로 제거하면 연결된 스토리지 리소스(EBS 볼륨 등)가 정리되지 않을 수 있습니다. 먼저 볼륨이 사용 중이지 않은지 확인하고, AWS 콘솔에서 고아(orphan) 볼륨이 생기지 않는지 확인하세요.
8. 옵저버빌리티 및 모니터링
옵저버빌리티 스택 아키텍처
Container Insights 설정
# Container Insights Add-on 설치
aws eks create-addon \
--cluster-name <cluster-name> \
--addon-name amazon-cloudwatch-observability
# 설치 확인
kubectl get pods -n amazon-cloudwatch
메트릭 디버깅: PromQL 쿼리
CPU Throttling 감지
sum(rate(container_cpu_cfs_throttled_periods_total{namespace="production"}[5m]))
/ sum(rate(container_cpu_cfs_periods_total{namespace="production"}[5m])) > 0.25
25% 이상의 throttling은 성능 저하를 유발합니다. CPU limits를 제거하거나 증가시키는 것을 고려하세요. 많은 조직이 CPU limits를 설정하지 않고 requests만 설정하는 전략을 채택하고 있습니다.
OOMKilled 감지
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} > 0
Pod 재시작률
sum(rate(kube_pod_container_status_restarts_total[15m])) by (namespace, pod) > 0
Node CPU 사용률 (80% 초과 경고)
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
Node 메모리 사용률 (85% 초과 경고)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
로그 디버깅: CloudWatch Logs Insights
에러 로그 분석
fields @timestamp, @message, kubernetes.container_name, kubernetes.pod_name
| filter @message like /ERROR|FATAL|Exception/
| sort @timestamp desc
| limit 50
레이턴시 분석
fields @timestamp, @message
| filter @message like /latency|duration|elapsed/
| parse @message /latency[=:]\s*(?<latency_ms>\d+)/
| stats avg(latency_ms), max(latency_ms), p99(latency_ms) by bin(5m)
특정 Pod의 에러 패턴 분석
fields @timestamp, @message
| filter kubernetes.pod_name like /api-server/
| filter @message like /error|Error|ERROR/
| stats count() by bin(1m)
| sort bin asc
OOMKilled 이벤트 추적
fields @timestamp, @message
| filter @message like /OOMKilled|oom-kill|Out of memory/
| sort @timestamp desc
| limit 20
컨테이너 재시작 이벤트
fields @timestamp, @message, kubernetes.pod_name
| filter @message like /Back-off restarting failed container|CrashLoopBackOff/
| stats count() by kubernetes.pod_name
| sort count desc