쿠버네티스에서 아마존 EFS 사용하기: efs-provisioner
들어가며
아마존 EFSAmazon Elastic File System은 AWSAmazon Web Service에서 제공하는 매니지드 NFS 서버입니다. NFS는 여러 서버에서 동시에 같은 파일을 공유할 때 편리하게 사용할 수 있지만, 관리가 어렵다는 단점이 있습니다. 아마존 EFS를 사용하면 좀 더 쉽게 NFS로 서버들 간에 파일을 공유할 수 있습니다. 쿠버네티스Kubernetes와 같은 분산 환경에서도 같은 용도의 서버들 간에 공유 스토리지로 EFS를 사용할 수 있습니다. EFS를 사용할 수 있는 대표적인 방법으로는 external-storage의 하위 프로젝트인 efs-provisioner를 사용하는 방법이 있습니다.
단, external-storage 프로젝트 자체가 EOLEnd of Life 상태로 더 이상 개발되고 있지 않습니다. 현재도 잘 동작하기는 하지만, kubernetes-sigs 아래에서 개발되고 있는 aws-efs-csi-driver를 사용하는 것을 권장합니다. CSIContainer Storage Interface와 aws-efs-csi-driver에 대해서도 다른 글에서 소개하도록 하겠습니다.
EFS 생성
아마존 EFS 서비스 페이지에서 EFS 파일 시스템을 생성할 수 있습니다. EFS는 VPC만 지정하면 손쉽게 생성할 수 있습니다. 쿠버네티스가 배포된 VPC를 선택하고 생성Create합니다.

생성된 EFS의 상세 페이지의 네트워크Network 탭에서 서브넷 별로 마운트 타깃mount target을 추가해주어야합니다. 네트워크 탭에서 관리Manage 버튼을 클릭합니다. 마운트 타깃 추가Add mount target을 클릭하고, 사용하고자 하는 AZ와 Subnet ID를 지정해줍니다. 보안 그룹Security groups에는 쿠버네티스 워커 노드와 통신 가능한 보안 그룹을 지정해주어야합니다. IP란은 공란으로 둡니다. 사용하는 AZ 별로 마운트 타깃을 작성하고 저장Save합니다.
이걸로 EFS 준비는 모두 마쳤습니다.
efs-provisioner 배포
EFS 연동을 위해서 efs-provisioner를 배포해야합니다. 공식적으로 제공되는 예제를 참고해 efs-provisioner 배포를 위한 YAML 파일들을 작성해보겠습니다.
먼저 서비스 어카운트ServiceAccount부터 정의합니다.*
* 여기서는 default 네임스페이스에 배포한다고 가정하고 있습니다. 필요하다면 모든 리소스의 네임스페이스를 적절히 변경해줍니다.
# service_account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: default
name: efs-provisioner
rbac.yaml 파일을 참고해 RBAC을 정의합니다. 이름과 네임스페이스 이외에 특별히 변경해줄 부분은 없습니다.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: efs-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-efs-provisioner
subjects:
- kind: ServiceAccount
name: efs-provisioner
namespace: default
roleRef:
kind: ClusterRole
name: efs-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-efs-provisioner
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-efs-provisioner
subjects:
- kind: ServiceAccount
name: efs-provisioner
namespace: default
roleRef:
kind: Role
name: leader-locking-efs-provisioner
apiGroup: rbac.authorization.k8s.io
다음으로 디플로이먼트Deployment를 정의합니다. 여기서는 efs-provisioner
를 이름으로 사용했습니다. 이미지는 quay.io/external_storage/efs-provisioner:v2.4.0
를 사용합니다.* 다음 환경변수들을 적절하게 지정해줍니다.
-
FILE_SYSTEM_ID
: EFS 파일 시스템 ID를 지정합니다. 예:fs-xxxxxxxx
-
AWS_REGION
: AWS 리전을 지정합니다. 서울이면 ap-northeast-2를 지정합니다. -
PROVISIONER_NAME
: 프로비저너 이름으로 사용할 임의의 값입니다.
마지막으로 spec.template.spec.volumes[].nfs.server
의 값으로 EFS 서버 주소를 지정해줍니다.
* v2.4.0이 최신 버전이며, 앞서 이야기한대로 더 이상 업데이트는 없을 것으로 보입니다. 이미지 저장소.
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: default
name: efs-provisioner
spec:
selector:
matchLabels:
app: efs-provisioner
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: efs-provisioner
spec:
serviceAccount: efs-provisioner
containers:
- name: efs-provisioner
image: quay.io/external_storage/efs-provisioner:v2.4.0
env:
- name: FILE_SYSTEM_ID
value: "<FILE_SYSTEM_ID>"
- name: AWS_REGION
value: "<AWS_REGION>"
- name: PROVISIONER_NAME
value: "<PROVISIONER_NAME>"
volumeMounts:
- name: pvcs
mountPath: /pvcs
volumes:
- name: pvcs
nfs:
server: "<EFS_SERVER_URL>"
path: /
마지막으로 스토리지 클래스StorageClass를 정의합니다. provisioner
값에는 앞서 디플로이먼트에서 지정한 <PROVISIONER_NAME>
값을 지정해줍니다.
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
namespace: default
name: efs-provisioner
provisioner: <PROVISIONER_NAME>
efs-provisioner YAML 파일 작성은 여기까지 끝났습니다. 지금까지 작성한 내용을 클러스터에 적용합니다.
$ kubectl apply -f .
kubectl
로 디플로이먼트와 스토리지 클래스가 잘 생성되었는지 확인해봅니다.
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
efs-provisioner-d8d69f896-5mb6z 1/1 Running 0 12m
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
efs-provisioner <PROVISIONER_NAME> Delete Immediate false 12m
잘 실행되고 있는 것을 확인할 수 있습니다. 이걸로 efs-provisoner 배포는 성공적으로 마쳤습니다.
EFS 볼륨 사용하기
그럼 마지막으로 다른 디플로이먼트를 정의할 때 efs-provisioner를 사용하는 방법에 대해서 알아보겠습니다.
다른 작업 디렉터리를 하나 만들어 YAML 파일을 작성합니다. 먼저 PVC를 정의해야합니다. 적절한 이름을 붙여 <PVC_NAME>
부분을 치환해줍니다. volume.beta.kubernetes.io/storage-class
에는 앞서 정의한 스토리지 클래스의 이름을 지정해줍니다. 위에서 스토리지 클래스를 정의할 때 efs-provisioner를 사용했으므로 여기서는 그 값을 그대로 지정합니다.
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: <PVC_NAME>
annotations:
volume.beta.kubernetes.io/storage-class: "efs-provisioner"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
여기서는 nginx를 사용하는 디플로이먼트 예제를 작성해보겠습니다. <PVC_NAME>
에는 위의 PVC 정의에서 사용한 값을 그대로 사용합니다. <VOLUME_NAME>
에는 적절한 값을 붙여줍니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: Always
ports:
- containerPort: 80
volumeMounts:
- name: <VOLUME_NAME>
mountPath: "/usr/share/nginx/html"
volumes:
- name: <VOLUME_NAME>
persistentVolumeClaim:
claimName: <PVC_NAME>
지금까지 정의한 YMAL 파일을 클러스터에 적용합니다.
$ kubectl apply -f .
PVC와 파드 목록을 확인할 수 있습니다.
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nginx-efs Bound pvc-70b6915c-19f3-4bf4-9bed-1f9b49869d26 10Gi RWX efs-provisioner 45s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5467b5f547-74dgv 1/1 Running 0 52s
nginx-5467b5f547-hlsgx 1/1 Running 0 52s
이제 실제로 EFS가 잘 사용되고 있는지 직접 확인해보겠습니다. 먼저 첫 번째 파드에서 /usr/share/nginx/html/
로 이동해서 index.html
파일을 작성합니다.
$ kubectl exec -it nginx-5467b5f547-74dgv -- bash
root@nginx-5467b5f547-74dgv:/# cd /usr/share/nginx/html/
root@nginx-5467b5f547-74dgv:/usr/share/nginx/html# ls
root@nginx-5467b5f547-74dgv:/usr/share/nginx/html# echo '<h1>Hello, world</h1>' > index.html
root@nginx-5467b5f547-74dgv:/usr/share/nginx/html# cat index.html
<h1>Hello, world</h1>
root@nginx-5467b5f547-74dgv:/usr/share/nginx/html# exit
이번엔 두 번째 파드에서 첫 번째 파드에서 작성한 내용이 확인 가능한지 보겠습니다.
$ kubectl exec -it nginx-5467b5f547-hlsgx -- bash
root@nginx-5467b5f547-hlsgx:/# cd /usr/share/nginx/html/
root@nginx-5467b5f547-hlsgx:/usr/share/nginx/html# ls
index.html
root@nginx-5467b5f547-hlsgx:/usr/share/nginx/html# cat index.html
<h1>Hello, world</h1>
첫 번째 파드에서 작성한 index.html
을 두 번째 파드 환경에서도 확인할 수 있습니다. 파드 안에서 mount | grep efs
를 실행해 EFS가 마운트된 것도 확인할 수 있습니다.
여기까지 efs-provisioner를 사용해서 쿠버네티스에서 아마존 EFS를 사용하는 방법을 알아보았습니다.