쿠버네티스에서 아마존 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는 VPC만 지정해 손쉽게 생성할 수 있습니다

생성된 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를 사용합니다.* 다음 환경변수들을 적절하게 지정해줍니다.

마지막으로 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를 사용하는 방법을 알아보았습니다.

AWS 람다(AWS Lambda) 커스텀 런타임 만들기(feat. 루비 2.6.0)

🗒 기사, 2019-01-04 - AWS 람다에서 공식 지원하지 않는 언어나 버전을 사용하고 싶은 경우 커스텀 런타임 기능을 활용할 수 있습니다. 이 글에서는 아직 AWS 람다에서 공식 지원하고 있지 않은 루비 2.6 최신 버전을 커스텀 런타임 기능을 사용해 실행하는 방법을 소개합니다.
'44bits.io의 2018년, 그리고 2019년 새해인사' 대표 이미지

44bits.io의 2018년, 그리고 2019년 새해인사

🗒 기사, 2019-01-15 - 세상에는 많은 정보가 있지만 한국어로 작성된 온라인에서 곧바로 접근 가능한 잘 정리된 글은 생각만큼 많지 않습니다. 이러한 빈 틈을 조금이나마 메워보려는 목표와 함께 44bits는 지난 6월 첫 글을 올리며 시작되었습니다. 2018년 한 해를 되돌아보고, 2019년 새해 인사를 드립니다.

44BITS 뉴스레터 2020년 24-25주

🗞 새소식, 2020-06-22 - 2020년 24-25주 44BITS 뉴스레터입니다. 44BITS의 업데이트와 IT / 개발 관련 새소식을 전합니다.