배경
On-Premise 환경에서 Terraform을 활용해 NFS 기반 Kubernetes 인프라를 AWS EKS로 이관하는 업무를 진행하고 있었습니다.
이관 과정에서 On-Premise 환경에서 사용하는 NFS 기반 공유 스토리지를 EFS로 전환하였습니다.
이때 기존의 배포 구조를 활용했고 파드가 하나의 static pv-pvc를 사용하는 것은 문제없이 EFS로 마운트가 가능했었습니다.
하지만 파드가 다수의 static PV–PVC를 사용할 땐 EFS의 제약조건으로 인해 EFS 환경에서 문제가 발생하였습니다.
이관 및 구축 과정에서 비용과 일정 리스크를 고려하여, 온프레미스에서 사용하던 구조를 그대로 활용했었는데 사실 AWS에선 dynamic provisoning 으로 pvc를 통해 pv를 생성해 마운트하는 방식으로 간단하게 붙는 방식에선 발생하지 않는 이슈이기도 했었습니다.
이후 문제 발생 원인을 분석하고 해결 방안을 정리한 뒤, 테스트 단계에서는 시간적 제약을 고려해 동일 구조를 활용할 수 있게 문제를 해결하였습니다.
본 문서는 NFS에서 EFS로 이관하는 과정에서 발생한 문제의 원인과, 왜 해당 방식을 사용할 수밖에 없었는지,
마지막으로 어떤 방식으로 문제를 해결했는지를 정리하여 공유하기 위해 작성하였습니다.
문제와 연관있었던 구축환경은 다음과 같습니다.
- os: ubuntu( on-premise ) , linux( aws ) 계열의 os를 사용했습니다.
- 공유 스토리지: nfs ( on-premise), efs( aws )
- k8s : 구축형 kubernetes ( on-premise) vs managed kubernetes (aws) ( control plane 관리 여부이며 큰 차이는 없습니다.)
- pv , pvc : nfs, efs 모두 static pv, static pvc로 binding, mount 방식을 사용하고 있습니다.
- iac: ansible ( on-premise ) , terraform ( aws )
- gitops: argocd
문제 현상
terraform을 기반으로 cloud의 모든 리소스를 생성하였으며,
파드가 하나의 static PV–PVC를 사용해 EFS를 마운트하는 구성까지는 정상적으로 컨테이너가 생성되고 마운트도 가능했습니다.
이후 하나의 파드가 다수의 static PV–PVC를 사용하는 구성을 적용하는 과정에서 각 PV–PVC를 생성하고 파드에 마운트 하는 과정에서 생각과 달리 파드가 생성되지 않는 상태로 계속 Pending상태 즉 교착상태에 빠진 것처럼 확인된 것을 확인했습니다.
지금부터 이 이슈를 하나하나 상세하게 확인 및 분석해보겠습니다.
문제 현상 시 사용한 static pv - pvc 파일
제가 사용했던 다수의 static pv - pvc의 형태는 하단과 같고 두 개 이상의 다수의 pv - pvc를 사용한 과정에서 volumeHandle이
동일하다면 파드가 생성되지 않는 이슈가 계속 재현되었습니다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
labels:
info.kubernetes.io/type: efs
info.kubernetes.io/phase: example
spec:
capacity:
storage: 1Ti
claimRef:
name: example-pvc
namespace: example
mountOptions:
- tls # 전송 암호화
csi:
driver: efs.csi.aws.com
volumeHandle: fs-**** # fs- 형식을 띄는 uuid
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
namespace: example
spec:
storageClassName: ""
resources:
requests:
storage: 1Ti
selector:
matchExpressions:
- {key: info.kubernetes.io/phase, operator: In, values: [example]}
- {key: info.kubernetes.io/type, operator: In, values: [efs]}
PV 상태 - 모두 정상
$ kubectl get pv | grep -E "(engine|kafka|data|file)-"
data-pv 10Gi RWX Retain Bound addon/data-pvc 3h
kafka-pv 10Gi RWX Retain Bound addon/kafka-pvc 3h
file-pv 10Gi RWX Retain Bound addon/rootdir-pvc 3h
PVC 상태 - 모두 정상 Bound
$ kubectl get pvc -n addon
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-pvc Bound data-pv 10Gi RWX 3h
kafka-pvc Bound kafka-pv 10Gi RWX 3h
rootdir-pvc Bound file-pv 10Gi RWX 3h
Argocd의 UI를 통해 가독성을 돕는 컨테이너의 상태

Argocd의 UI를 통한 상세 Event로그는 하단과 같습니다.
(kubectl describe po/<pod-id> -n <namespace>의 명령어 중 event부분)

그런데 Pod는 167분째 ContainerCreating:
$ kubectl get pod filebrowser-5564689d56-k6nd6 -n addon
NAME READY STATUS RESTARTS AGE
filebrowser-5564689d56-k6nd6 0/1 ContainerCreating 0 167m
문제 현황을 확인했으니 짤막하게 efs에 대한 개념에 관해 설명하겠습니다.
(추후 efs에 대해 기술한 문서가 생긴다면 문서에 링크를 걸어두겠습니다.)
EFS는 Amazon Elastic File System의 약자이며 서버리스 파일 스토리지를 제공하고 스토리지 용량과 성능을 관리하지 않고도 여러 노드에서 파일 데이터를 공유할 수 있게 해주는 시스템입니다.
페타바이트급까지 온디멘드 규모로 확장할 수 있는 확장성 있는 리전별 서비스이며 여러 AZ에 걸쳐 데이터를 중복으로 저장해 놓은 가용성과 내구성을 제공하도록 설계되어 있습니다.
EFS는 Amazon Linux, Amazon Linux2, Red Hat, Ubuntu , macOS Big Sur AMI 등의 운영체제에서 사용할 수 있으며 windows에선 사용할 수 없어 이 점은 사용 시 유의해야 합니다.
요약한다면 ec2, eks등 다수의 리눅스 시스템에서 네트워크를 통해 접근할 수 있는 고가용성 서비스이며 스토리지를 신경 쓰지 않고 운영 중 확장이 가능한 형태의 서비스라고 말할 수 있을 것 같습니다.
간략하게 efs를 알아보았는데 지금부터 이런 efs가 왜 붙지 않았던 것일지 설명하겠습니다.
k8s에서 pv - pvc를 마운트하는 환경에서 CSI driver를 사용할 때 파드가 사용하는 pv는 volumeHandle이 고유해야 한다는 제약사항이 존재합니다.
https://docs.aws.amazon.com/ko_kr/filegateway/latest/files3/use-nfs-csi.html?utm_source=chatgpt.com
NFS CSI 드라이버 작업 - AWS Storage Gateway
이전 단계의 .yaml 구성 텍스트를 대부분의 타사 Kubernetes 관리 및 컨테이너화 플랫폼에 제공하여 PersistentVolume 및 PersistentVolumeClaim 객체를 생성할 수도 있습니다.
docs.aws.amazon.com
NFS를 사용했을 당시 다수의 pv-pvc가 동일 NFS server에 static 하게 마운트하여 사용하는 것이 가능했으며 efs를 구축하기 위해 확인했을 당시 efs CSI driver, 그리고 그 driver를 사용하여 구축하는 pv 및 여러 방식으로 efs에 마운트 가능한 방법을 확인하여 구축하여 위 같은 제약사항을 확인하지 못했고 이슈가 발생했을 때도 event나 로그가 나오지 않아 난항을 겪었던 것 같습니다.
efs는 Amazon의 Managed efs 서비스이며 똑같은 efs이기 때문에 제약사항이 적용되어 volumeHandle이 unique 하지 않다면 파드가 여러 pv - pvc를 마운트 하는 것이 실패해 pending 상태를 해결하지 못한 것이었습니다.
Static pv - pvc외 다른 방법으로 efs를 마운트할 수 있었으나 제가 그렇게 진행했던 이유도 설명하겠습니다.
하단에서 기술하겠지만 efs를 k8s에서 pvc로 마운트하는 경우 static 하게 pv-pvc로 마운트하는 것은 실무에서 활용도가 적고 일반적이진 않습니다.
하지만 제가 그렇게 하려고 했던 이유는 구축했던 배포 환경이 kustomize의 overlay 상속 방식으로 되어있었고 Kustomize는 상속이라는 장점은 있지만 base에 명시했다면 하위 환경에서 구체적인 정보를 기술해 상속해야 한다는 단점이 존재했기 때문입니다.
환경이 1~2개라면 kustomize를 고치거나 helm으로 전환해 해결했겠지만 poc를 진행하는 과정에선 환경이 4개로 늘어나 있었고 eks 이관이라는 우선순위의 업무상 kustomize의 단점을 고치기보단 인프라를 전체적으로 빠르게 구축하고 on-demand 배포 환경을 구축해야 했기 때문에 static pv-pvc 방법을 고수할 수밖엔 없었습니다.
그러면 이 문제를 해결할 수 있는 근원적인 방법과 다른 해결책은 무엇일까요?
1. dynamic provisoning
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: <EFS file system ID>
directoryPerms: "700"
- 위와 같은 형태의 StorageClass를 생성하고 PVC를 생성할 당시 원하는 스토리지 스펙과 생성했던 StroageClass를 지정한다면 PVC가 생성된 이후 요구하는 스펙에 맞는 pv를 생성하고 access point를 생성하여 격리된 pv - pvc 마운트를 자동으로 생성 & 연결해 주는 방식입니다.
- volumeHandle은 pv가 생성될 때마다 독립적으로 생성되는 access point를 통해 격리되며 이를 통해 사용자는 pv를 관리하는 복잡성과 volumeHandle을 unique 하게 관리해야 하는 제약사항에서 자유로워질 수 있게 됩니다.
2. access point
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-pv-static
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: static-efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: [FILE SYSTEM ID]::[ACCESS POINT ID]
- dynamic provisiong에서 자동으로 생성해 주는 access point를 직접 생성하여 static pv - pvc에 하단의 파일과 같이 기술하는 방식입니다.
- access point가 각 pv마다 달라 최종적으로 volumeHandle이 달라져 파드가 다수의 static pv - pvc를 마운트할 수 있게 되는 방식입니다.
3. directory path
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-path-pv
spec:
mountOptions:
- tls # 전송 암호화
csi:
driver: efs.csi.aws.com
volumeHandle: fs-*****:/data
- efs id 뒤 디렉터리 경로를 명시하여 volumeHandle을 독립적으로 관리하는 방식이며 path를 명시한 pv라면 efs root 경로가 아니라 명시된 path에 파드의 스토리지 경로가 마운트 된다는 차이점이 존재합니다.
- access point 방식과 다르게 directory path는 명시하기 전 efs를 만들고 path까지 미리 만들어야만 동작하며 path는 동적으로 생성할 수 없다는 단점 또한 존재해 실무에서 사용하기에 적합한 해결책이라고 하기는 어려울 수 있겠습니다.
위와 같은 해결 방법 중 제가 사용한 방법은 3번 directory path 방식이며 이유는 다음과 같습니다.
- static pv - pvc 이기 때문에 dynamic provisiong은 단기적으로 사용할 수 없었습니다.
- 테스트 기간에 tf destroy & tf apply를 하게 되면 efs id가 계속 재생성되고 access point까지 재생성되어 static 한 pv의 모든 volumeHandle을 계속 바꾸며 테스트해야 해서 제외했습니다.
- tf apply에서 산출되는 efs id는 매번 바뀌어 어느 순간 익숙해지긴 했지만 피로감이 컸는데 access point는 수십 개가 생성될 수 있고 이것들을 모두 명시하는 수고로움을 매번 감수하며 실수하지 않으리란 보장이 없었기 때문입니다.
- efs id는 재생성 간 관리해야 하지만 그 외 요소는 멱등하게 유지할 수 있는 방법이 directory path라고 생각되어 선택하게 되었고 efs가 생성된 이후 path도 직접 만들어야 한다는 제약사항은 Job을 제일 먼저 배포해 타 PV에 필요한 모든 경로를 생성하는 것으로 자동화하여 테스트 간 해결했었습니다.
정리/회고
efs를 dynamic provisoning 방식으로 마운트한다면 겪지 않을 이슈이며 실무에선 static pv - PVC를 다수의 파드에 붙이는 경우가 일반적이지 않기 때문에 대부분의 분들은 겪지 않을 이슈일 수도 있습니다.
저는 AWS에 비용 최적화, IAC 코드화 등 더 큰 범위와 우선순위가 높은 업무를 진행하는 동안 on-premise와 동일한 형상으로 구축하여 테스트 간 리스크를 줄이려고 제약사항을 정확하게 확인하지 않아 이슈를 겪는 동안 특정하는 데 어려움을 겪었던 것 같습니다.
static pv-PVC 마운트 방식에서 access point는 테스트 간 수고로움을 동반할 것이 분명하여 directory path로 고치는 동안에도 제약사항과 k8s Native 하게 해결하는 방식으로 넘어갈 수 있었는데 이슈가 발생했을 때 debugging 동안 로그나 event가 출력되지 않아 어떻게 검색하고 찾아 해결해야 할까? 처음엔 좀 막막했던 것 같습니다.
이렇게 구축해야 했던 이유인 Kustomize는 helm으로 이관하여 해결할 것 같고 또한 static pv - PVC는 dynamic provisiong으로 전환해 수십, 수백 개가 동시에 뜨고 죽는 on demand 방식의 cloud 배포 환경을 더 나은 방법으로 전환해 나가고 있고 전부 완료된다면 계속 문서를 작성해 원인 - 개선 등의 문서로 작성할 것 같습니다.
k8s를 eks로 이관할 땐 k8s에 대한 운영 경험으로 난항은 겪지 않을 것 같았지만 cloud에 대한 개념들은 구축, 운영할 때 또 다른 어려움으로 다가오는 것 같고 이 글이 보시는 데에 도움이 되셨으면 좋겠습니다.
'TroubleShooting > DevOps' 카테고리의 다른 글
| Docker - Special DNS 문제 (0) | 2025.08.17 |
|---|---|
| Docker - Zombie Process (5) | 2025.08.15 |