배경 및 문제상황
발생 일자: 2026-05-19
배경
EKS 클러스터에 관측성(Observability) 스택을 얹는 작업을 하고 있었다.
로그 수집은 Loki, 트레이스 수집은 Tempo를 쓰기로 하고, 두 컴포넌트를 ArgoCD 애플리케이션으로 정의해 monitoring 네임스페이스에 배포했다.
솔직히 이때는 수집한 로그,트레이스가 어디에 어떻게 저장되는지까지 깊게 따져보지않았다.
이 컴포넌트들이 떠있으려면 별도의 영구 볼륨을 먼저 붙여야 한다는 것을 잊고 있었다.
변명하자면 이제 막 초기 셋팅 진행 중이라 정신 없어서 신경 쓸 겨를이 없었다.
문제상황
ArgoCD가 Sync를 끝냈는데도 Loki/Tempo Pod가 Running으로 넘어가지 못하고
계속 Pending 상태에 머물렀다. 시간이 지나도 Running이 되지 못했다.
kubectl get pods -n monitoring | grep "loki|tempo"
infra-loki-0 0/2 Pending 0 5m55s
infra-tempo-0 0/1 Pending 0 5m51sPod가 어떤 노드에도 스케줄되지 못한 채 대기만 하는 상황이었다.
애플리케이션 컨테이너 자체의 문제(이미지, 설정 등)라기보다는 뜨는 데 필요한 선행 조건이 충족되지 않아 스케줄링 단계에서 막혀 있다는
신호로 보였고, 여기서부터 원인을 좁혀 나갔다.
원인 분석
PVC가 Pending인 이유
그래서 Pod의 Pending을 PVC 쪽으로 따라 내려갔더니 PVC도 전부 Pending이었다.
Pending인 Pod를 보니 노드에 배치되지 못한 이유는 PVC였다.
Pod는 자신이 요구하는 PVC가 실제 볼륨에 바인딩되어야 노드에 스케줄된다.
요청한 PVC가 볼륨에 바인딩되지 않아 스케줄 자체가 멈춰 있었다.
PVC들도 확인해보니 전부 Pending이었다.


PVC의 이벤트로 이유를 확인했다.
kubectl describe pvc storage-infra-tempo-0 -n monitoring
Events:
Normal ExternalProvisioning ... persistentvolume-controller
Waiting for a volume to be created either by the external provisioner
'ebs.csi.aws.com' or manually by the system administrator.동적 프로비저닝은 PVC가 StorageClass를 보고 누구한테 볼륨을 만들어 달라고 할지를 정한 다음 그 프로비저너(드라이버)가 실제 EBS를 만들어 PV로 붙여주는 흐름이다.
그런데 이벤트를 보면 PVC가 ebs.csi.aws.com 앞으로 요청은 걸어놓고 한없이 기다리고만 있었다.
요청은 나갔는데 받아주는 쪽이 없다는 얘기다.
그러면 막힐 수 있는 지점이 둘로 좁혀진다. 하나는 PVC가 애초에 엉뚱하거나 빈 StorageClass를 물어서 요청 자체가 잘못 나간 경우, 다른 하나는 요청은 제대로 나갔는데 정작 그 프로비저너가 클러스터에 없어서 아무도 받지 못하는 경우다.
일단 양쪽 다 짚어봤다.
1차 원인 — StorageClass 미지정
처음 이 문제를 봤을 때 STORAGECLASS 컬럼이 <unset>이였다.
kubectl get pvc -n monitoring
NAME STATUS STORAGECLASS AGE
storage-infra-loki-0 Pending <unset> 2m41s
storage-infra-tempo-0 Pending <unset> 2m37sPVC가 쓸 StorageClass를 못 정했다는 뜻이다. Loki/Tempo values에 StorageClass를 적어두지 않았고, 그렇다고 지정이 없을 때 대신 쓰일 default StorageClass가 클러스터에 있는 것도 아니었다.
둘 다 없으니 PVC 입장에선 어디에 볼륨을 달라고 해야 할지 알 길이 없었다.
values에 적었는데도 안 먹힌것도 있었다.. Loki 6.x 차트는 키가storageClassName이 아니라 storageClass인데, 이름을 잘못 쓰면 에러도 없이 그냥 무시됐다.
# 반영 안 됨
singleBinary:
persistence:
storageClassName: gp2 # X
# 올바른 키
singleBinary:
persistence:
storageClass: gp2
그래서 default StorageClass(gp2-csi)를 따로 만들어두고 values 키도 바로잡았다.
(기록을 적는 지금은 이 수정이 이미 들어가 있어서 PVC가 <unset> 대신 gp2-csi로 잡힌다.)
그런데 SC를 제대로 물렸는데도 PVC는 여전히 Pending이었다. StorageClass를 올바르게 지정하는 것과 그 SC가 가리키는 프로비저너가 실제로 볼륨을 만들어 주는 건 별개였던거다.
그래서 두 번째로 그 프로비저너가 진짜 있긴 한지 확인해보았다.
2차 원인 — EBS CSI 드라이버 미설치
앞 단계에서 SC는 멀쩡한데도 막혔으니, 이번엔 프로비저너 쪽을 봤다. 단서는 이벤트에 찍힌 ebs.csi.aws.com라는 이름 그 자체였다. CSI 드라이버는 자신을 reverse-DNS 형식 이름으로 등록하는데(예: efs.csi.aws.com, pd.csi.storage.gke.io), ebs.csi.aws.com은 AWS EBS CSI 드라이버가 등록하는 바로 그 이름이다.
이름을 그대로 읽으면 ebs + csi + aws.com 이고. 그러니까 프로비저너와 드라이버가 따로 있는 게 아니라 이 이름으로 들어온 볼륨 요청을 처리하는 실체가 곧 EBS CSI 드라이버다. (get-storageclass 캡처에서도 gp2-csi의 provisioner가 ebs.csi.aws.com으로 찍혀 있다.)
이 드라이버는 kube-system에 컨트롤러와 노드 파드로 떠 있어야 한다.
그래서 거기에 파드가 있는지부터 봤다.


그런데 ebs-csi-controller, ebs-csi-node 한 줄도 잡히지 않았다.
고장이라면 CrashLoop으로라도 목록엔 떴을 텐데 아예 안 보인다는 건 워크로드가 배포된 적조차 없다는 뜻이다.
드라이버 자체가 설치되지 않은 것이었다.
여기서 멈칫했다. 내가 EBS CSI 드라이버를 따로 설치하지 않았구나를 깨달았다.
CSI(Container Storage Interface)는 쿠버네티스가 외부 스토리지와 통신하는 표준 규격이고, EBS CSI 드라이버는 그 AWS 구현체다. PVC가 볼륨을 요청하면 이 드라이버가 실제로 AWS EBS API를 호출해 볼륨을 만들고 노드에 붙여준다. 이게 없으면 요청을 받아 EBS를 만들어 줄 주체 자체가 없다.
문제는 이게 EKS에 기본으로 깔려 오지 않는다는 점이다. 찾아보니 쿠버네티스 1.23이전에는 EBS 연동 코드가 쿠버네티스 안에 in-tree로 박혀 있어서 아무것도 안해도 되었다고 했다.(위 storageclass 목록에서 gp2가 아직 in-tree 프로비저너 kubernetes.io/aws-ebs로 찍혀 있는 게 그 흔적이다.) 그런데 1.23부터 이 in-tree 코드는 deprecated되고 ebs.csi.aws.com 드라이버로 위임됐다(CSI Migration).
(https://aws.amazon.com/ko/blogs/containers/amazon-eks-now-supports-kubernetes-1-23/)
이제는 EBS CSI 드라이버를 애드온으로 직접 설치해 줘야 한다.
안 하면 방금처럼 PVC가 영원히 Pending에 걸린다. 결국 이 드라이버가 빠져 있던 게 근본 원인이었다.
3차 원인 — EBS CSI 컨트롤러 IRSA 미설정
드라이버 애드온만 깔면 끝일 줄 알았는데 아니었다.
애드온을 추가하고 terraform apply를 돌렸는데 이번엔 apply가 끝나질 않았다.aws_eks_addon.ebs_csi_driver가 Still creating...만 반복하며 18분을 넘겼다.

결국 20분 타임아웃에 걸려 에러로 끝났다. 애드온이 ACTIVE가 되기를 기다리는데
계속 CREATING에 머물러 있다는 메시지였다.

에러 메시지에서 last state: 'CREATING'. 애드온 생성 요청 자체는 받아들여졌고(생성에 실패한 게 아니다), 다만 그게 ACTIVE로 넘어가질 못하고 있다는 뜻이다.
terraform은 aws_eks_addon을 만들 때 EKS에 애드온 생성을 요청하고, 그 뒤로는 애드온이 ACTIVE가 될 때까지 폴링하며 기다리기만 한다. 애드온이 CREATING에 묶여 있는 건 파드는 떴는데 health check를 못 넘길 때 나오는 신호다. 그래서 다른 터미널에서 클러스터 안 파드를 직접 확인해봤다.

노드 파드(ebs-csi-node)는 세 개 다 3/3 Running으로 멀쩡한데 컨트롤러(ebs-csi-controller)만 CrashLoopBackOff에 빠져 수십 번씩 재시작을 반복하고있었다.
같은 드라이버를 이루는 두 워크로드인데 한쪽만 죽는 게 이상했다. 그래서 컨트롤러가 왜 죽는지부터 봐야 했다.
로그를 한 줄씩 따라가 봤다.
kubectl logs -n kube-system -l app=ebs-csi-controller -c ebs-plugin
"GRPC error" err="rpc error: code = FailedPrecondition desc = Failed health check
(verify network connection and IAM credentials): dry-run EC2 API call failed:
operation error EC2: DescribeAvailabilityZones, get identity: get credentials:
failed to refresh cached credentials, no EC2 IMDS role found,
operation error ec2imds: GetMetadata, canceled, context deadline exceeded"컨트롤러는 뜰 때 자신이 EC2 API를 쓸 수 있는지 DescribeAvailabilityZones를 한 번 호출해 보는 health check를 한다. 그런데 그 호출에 쓸 자격증명을 구하지 못하고 있었다(no EC2 IMDS role found).
마지막 줄에서 자격증명을 IMDS에서 가져오려다 GetMetadata가 context deadline exceeded로 시간 초과됐다. 컨트롤러 Pod가 IMDS에 닿지를 못한 것이다.
EBS CSI 컨트롤러는 실제로 EBS 볼륨을 만들고 붙이려고 AWS API를 호출하는 쪽이라 자격증명이 꼭 필요하다. 따로 설정해 준 게 없으니 노드의 IMDS에 기대는데, 컨트롤러 Pod는 거기에 닿지를 못했다.
같은 노드 위에 떠 있는 ebs-csi-node(DaemonSet)는 멀쩡한데 컨트롤러만 못 닿은 건, 둘이 IMDS까지 가는 네트워크 경로(hop)가 다르기 때문이다.
IMDS랑 hop 자세히
IMDS(Instance Metadata Service) 는 EC2 인스턴스마다 내부 주소(169.254.169.254)에 떠 있는 서비스다. 그 인스턴스의 정보와 인스턴스에 붙은 IAM 역할의 임시 자격증명을 돌려준다.
AWS SDK는 명시적으로 받은 자격증명이 없으면 마지막에 이 IMDS에 물어보는데, EBS CSI 컨트롤러도 그 경로로 자격증명을 얻으려다 실패한 것이다.
문제는 IMDS로 가는 요청이 몇 hop까지 갈 수 있는지를 제한하는 설정(HttpPutResponseHopLimit)이 있고, 그 기본값이 보통 1이라는 점이다. 같은 노드 위에 떠 있어도 두 파드가 IMDS까지 가는 경로가 다르다.
ebs-csi-node(DaemonSet) —hostNetwork: true로 떠서 노드의 네트워크 네임스페이스를 그대로 공유한다. IMDS 요청이 노드가 직접 보내는 것과 같아 1 hop, 제한(1) 안에 들어오니 자격증명을 잘 받아온다.ebs-csi-controller(Deployment) — 자기만의 파드 네트워크 네임스페이스를 쓴다(hostNetwork 아님). IMDS 요청이파드 → veth → 노드로 네임스페이스를 한 칸 더 건너야 하고, 그 경계를 넘을 때 hop이 하나 늘어 2 hop이 된다. 제한이 1이면 노드 경계에서패킷의 TTL이 0이 돼 버려지고, IMDS는 응답하지 않는다 ->context deadline exceeded.
즉 같은 드라이버인데도 노드 파드는 닿고 컨트롤러만 못 닿은 건 이 hop 차이 때문이다.
사실 고치는 길은 둘이다. (1) IMDS hop 제한을 2로 올려 컨트롤러도 닿게 하거나, (2) 아예
IMDS를 안 타도록 IRSA로 컨트롤러 전용 자격증명을 주거나. 1번은 결국 노드 IAM 역할을
그대로 쓰게 돼 권한 분리가 안 되니 최소 권한 면에서 2번이 깔끔하고 AWS도 이쪽을 권장한다.
처음엔 노드 IAM 역할에 AmazonEBSCSIDriverPolicy만 붙이면 되지 않나 싶었다. 그런데 그것만으로는 안 됐다. 권한이 노드 역할에 있어도 컨트롤러가 IMDS를 통해 그 권한을 가져오지 못하면 아무 소용이 없기 때문이다.
그럼 컨트롤러한테 IMDS 말고 다른 경로로 자격증명을 쥐여줘야 한다는 건데 EKS에서 이런 컨트롤러에 권한을 주는 표준 방법이 IRSA였다.
- IRSA(IAM Roles for Service Accounts) 는 IMDS를 거치지 않고, 쿠버네티스 ServiceAccount에 IAM 역할을 직접 묶어 Pod에 AWS 자격증명을 주는 방식이다. 그 ServiceAccount로 도는 Pod는 발급받은 토큰을 들고 AWS STS에 가서 해당 역할의 임시자격증명을 받아온다. 노드 네트워크(IMDS)를 타지 않으니 hop 문제도 자연히 사라진다.
그래서 컨트롤러의 ServiceAccount(ebs-csi-controller-sa)에 IAM Role을 연결하고, 애드온이 그 Role을 쓰도록 지정해 주도록 했다.
문제해결
Terraform에 IRSA 추가
컨트롤러 ServiceAccount(ebs-csi-controller-sa)가 쓸 IAM Role을 만들고, 애드온이 그 Role을 쓰도록 service_account_role_arn을 지정했다. Role의 신뢰 정책에는 그 ServiceAccount만 이 Role을 가져갈 수 있게 OIDC sub 조건을 걸었다.
# EBS CSI 컨트롤러용 IRSA Role
resource "aws_iam_role" "ebs_csi" {
name = "${var.project}-${var.env}-ebs-csi-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.eks.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
# 이 SA만 이 Role을 가져갈 수 있도록 제한
"${oidc}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
"${oidc}:aud" = "sts.amazonaws.com"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
role = aws_iam_role.ebs_csi.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}
resource "aws_eks_addon" "ebs_csi_driver" {
cluster_name = module.eks.cluster_name
addon_name = "aws-ebs-csi-driver"
service_account_role_arn = aws_iam_role.ebs_csi.arn # 애드온이 이 Role을 컨트롤러 SA에 연결 → IMDS 안 타고 자격증명 획득
depends_on = [aws_iam_role_policy_attachment.ebs_csi]
}
(oidc는 replace(aws_iam_openid_connect_provider.eks.url, "https://", "")를 줄인 표기다.)
적용
terraform apply — 이번엔 애드온이 CREATING에 묶이지 않고 곧 ACTIVE로 넘어가면서 apply가 정상 종료됐다.
컨트롤러가 IRSA로 자격증명을 받자마자 health check를 통과했기 때문이다.


컨트롤러부터 확인해보면 CrashLoop이 멈추고 살아 있다.
그러자 막혀 있던 게 줄줄이 풀렸다. 컨트롤러가 EBS 볼륨을 만들어 PV로 붙여주니 PVC가 Bound로 바뀌고, 볼륨을 받은 Loki/Tempo Pod도 Running으로 올라왔다.
처음 Pending에서 출발해 StorageClass -> 드라이버 -> IRSA를 차례로 짚고 나서 PVC가 볼륨을 잡고 Pod가 떴다.
재발방지대책
- 새 EKS 클러스터엔 EBS CSI 드라이버가 기본으로 없다. 스토리지를 쓰는 워크로드를 올리기 전에 애드온이 깔려 있는지부터 확인하는 걸 셋업 체크리스트에 넣었다.
- 애드온은 IRSA까지 묶여야
ACTIVE가 된다.terraform apply가aws_eks_addon에서 안 끝나고 한참 멈춰 있으면, 컨트롤러가 자격증명을 못 받아 health check에서 떨어지는 경우일 수 있다. 그땐 apply를 노려보지 말고 다른 터미널에서 파드와 로그부터 본다. - 컨트롤러는 노드 IAM 권한만으로는 부족하다. IMDS 경로가 hop에 막히기 때문에 권한을 노드 역할에 아무리 잘 붙여도 소용없다. 이 컨트롤러는 IRSA를 붙여야 한다.
- PVC가 Pending이면
kubectl describe pvc의 Events부터 본다.ExternalProvisioning메시지에 어떤 프로비저너를 기다리는지가 찍혀서 원인을 가장 빠르게 좁혀준다.
'TroubleShooting' 카테고리의 다른 글
| ArgoCD Sync 해도 안 풀리는 OutOfSync 트러블슈팅 (0) | 2026.06.01 |
|---|---|
| [AWS] 프리티어 노드 용량 부족 이슈 (0) | 2026.05.18 |
| [AWS] ESO(External Secrets Operator) 설정 (0) | 2026.05.18 |
| [AWS] EKS kubectl 접근 트러블슈팅 기록 (0) | 2026.05.18 |
| [AWS] Terraform으로 EKS Cluster SG 포트 범위 최소화하기 (0) | 2026.05.18 |