Infrastructure

NIPA인증 2. Kubernetes 데이터 백업 구현

0BigLife 2024. 5. 11. 16:20
728x90

 NIPA인증 1. 시작글에 이어 '확장성'에 대한 기술 구현을 적어보려고 한다. 인증 취득을 위한 SaaS 솔루션은 보장되어야할 주요 요건들이 있는데, 이번 글에서 다룰 것은 '신뢰성'에 대한 것이다. 신뢰성이 뭔지 간략히 설명하자면, 선뢰성을 가지는 솔루션은 리소스 관리에 있어서 장애 발생 또는 특정 동작을 통해 삭제되었을시 서비스가 다시 회복되는 데에 얼마나 걸리는지를 의미하는 '서비스 회복 시간'이 보장되어야한다. 운영을 위해 유지되어야할 데이터가 백업준수율을 지키고 있는지도 보장되어야하고, 데이터 보관(백업), 반환, 그리고 폐지에 대한 정책 및 기술도 구현되어있어야 한다. 그럼 시작해보자-!

데이터 백업

 데이터는 왜 백업되어야하는가? 를 먼저 얘기해보려고 한다. '단순히 서버가 다운되면 데이터베이스는 사라질테니까 해야하는거 아니야?' 라고 생각이 들겠지만 이를 인프라 측면에서 살펴보자. 인프라에 구성되는 대부분의 리소스는 클러스터 내부에 존재하며, 배포된 모든 애플리케이션의 최소 단위는 파드(Pod)의 형태로 존재한다. 각 Pod는 일반적으로 Deployment를 통하여 생성된다는 가정 하에 이름의 뒤에 임의의 랜덤 문자열이 붙는다.(이 네이밍 규칙도 Kind별 서로 다른 방식이 있으나 여기서 다루지는 않는다.) 이러한 특성을 가지는 파드는 일회성 리소스로서 존재하기 때문에 문제가 발생하여 다운된다면 다운되기 직전 상태로 다시 복구시킬 수 없다. 그렇기 때문에 기업에서의 애플리케이션별 데이터 백업 전략이 수립되어야하는 것이다.

 

 그렇다면 백업을 위한 기술을 어떻게 구현할 수 있을까? 기업마다 다르겠지만 가장 일반적인 방식으로는 스냅샷이 떠오른다. 물론 스냅샷은 개발이 간편하고 비용이 저렴하지만, 필자가 개발중인 프로젝트는 스냅샷을 사용할 수 없기에 다른 방식이 필요하다. 현재의 SaaS 솔루션은 기업 인프라 내부에 설치하는 방식으로 세팅이 되는데, 설치를 하게 되면 AWS, Microsoft Azure, CGP와 같은 퍼블릭 클라우드 프로바이더, 혹은 온프레미스 클라우드와 같은 환경에 최대한 의존하지 않고서 솔루션의 데이터 백업 전략이 구현되어야 한다. 즉, 다양한 클라우드 환경에서 공용으로 쓰일 수 있는 전략이 필요하다. 그렇기 때문에 설치할 때마다 프로바이더별 스냅샷 설정을 해주는 것보다 설치하는 과정에서 필요한 리소스들을 코드로 넣어 자동화해주는 방식이 필요했다. 

https://blog.stackademic.com/scheduling-mysql-and-mongodb-database-backups-using-cron-46a83fad97e9


 위와 같은 이유로 Cronjob을 통한 주기적인 스케줄링을 통한 데이터 백업 전략을 채택했다. 솔루션을 사용하기 위한 설치 과정에서 클러스터 내부에 솔루션에 대한 데이터가 주기적으로 백업되기 위한 일종의 준비물들이 모두 세팅되는 것이다. 구현을 위한 필요 리소스는 다음과 같은 코드로 설치 과정에 들어간다. 설치 방식은 이 게시글에서 중요하지 않기에 생략하고 필요한 리소스만 설명하자면, StorageClass, PersistentVolume, PersidstentVolumeClaim, Statefulset, Service가 필요하고 백업된 경로를 배포된 SaaS Deployment yaml 내부에 넣어줘야한다. 

// 업로드된 yaml을 퍼블릭 url로 받아서 클러스터 내부에 설치
kubectl apply -f https://<external-url>.com/yaml/saas-storageclass.yaml
kubectl apply -f https://<external-url>.com/yaml/saas-pv.yaml
kubectl apply -f https://<external-url>.com/yaml/saas-pvc.yaml
kubectl apply -f https://<external-url>com/yaml/saas-headless-service.yaml
kubectl apply -f https://<external-url>.com/yaml/saas-statefulset.yaml

kubectl apply -f https://<external-url>.com/yaml/saas-deployment.yaml


  각 리소스에 대한 역할을 간단히 설명하고, 정확히 기술적으로 어떤 속성값을 가져 yaml이 배포되는지를 기술한다. 그리고 cronjob  프로세스를 설명하는 것이 이해하는 데에 도움이 될 것이다. 

- StorageClass: Storage의 동적 프로비저닝 방법을 정의하며 이는 PersistentVolumeClaim에서 요청할 때 쓰인다. 즉, 저장소와 관련하여 특정 애플리케이션이나 서비스 요구 사항을 충족시키기 위하여 필요한 자원을 하당하고  구성한다. 
- PersistentVolume(PV): 클러스터에서 사용 가능한 Storage의 논리적인 볼륨을 의미한다.
- PersistentVolumeClaim(PVC): 애플리케이션이 Storage에 대하여 요청할 때 사용된다. 사용 가능한 PV 중에서 애플리케이션에 할당될 볼륨을 선택하고 요청한다.
- StatefulSet: 무작위로 생성되는 파드에 '순서'를 부여해주는 워크로드 리소스이며, PVC와 함께 사용되어 각 파드에 고유한 PV를 할당 및 보존시킨다. StatefulSet으로 생성된 파드들은 고유한 식별자를 가지기 때문에 파드가 다운되어도 재생성되어 내부에서 DNS를 통하여 연결시켜주는 네이밍 규칙에 대해 규정하는 중요한 역할을 한다.(이는 하단에서 더 자세히 다루겠다)
- Headless Service: 클러스터 내 각 파드에 대한 개별적인 DNS 레코드를 생성하는 서비스 유형이다. 일반적인 서비스처럼 apply하되, ClusterIP 속성값이 none을 주면 생성이 가능하며, StatefulSet과 함께 사용될 때 파드별 고유 DNS 레코드가 생성되기 때문에 네트워크 통신에 유용하다.


 필자가 스냅샷이 아닌 Cronjob을 통하여 다양한 클라우드 환경에서 데이터 백업 전략을 구축한 이유는 위 리소스 중에서 StorageClass와 밀접한 관계를 가진다. StorageClass의 속성값 중에서 parameters와 provisioner에 주목하자. 마운트 시 사용자 권한을 제어하는 mountOption 속성이나 PV 삭제 시 스토리지 해제할 때의 동작을 지정하는 reclaimPolicy 등의 속성도 중요하지만 필자가 위에서 클라우드 프로바이더 종류에 제약을 받지 않도록 설정했다는 것은 parametersprovisioner를 통하여 제어가 가능하다. parameters 속성은 특정 프로바이더에 해당하는 추가 매개변수를 지정하고 스토리지 유형을 지정하며, provisioner는 CSI 드라이버를 지정한다. 

// azure - storageclass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: azure-sample
mountOptions:
- dir_mode=0777
- file_mode=0777
parameters:
  skuName: Standard_LRS
provisioner: file.csi.azure.com
reclaimPolicy: Retain
volumeBindingMode: Immediate

// aws - storageclass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-sample
mountOptions:
- dir_mode=0777
- file_mode=0777
parameters:
  type: gp2
provisioner: kubernetes.io/aws-ebs
reclaimPolicy: Retain
volumeBindingMode: Immediate

// gcp - storageclass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gcp-sample
mountOptions:
- dir_mode=0777
- file_mode=0777
parameters:
  type: pd-standard
provisioner: kubernetes.io/gce-pd
reclaimPolicy: Retain
volumeBindingMode: Immediate


 그렇다면 이제 클라우드 프로바이더별로 StorageClass가 마련이 되어있고, 실제 운영되는 Deployment와 배포되는 데이터베이스는 어떻게 연결되며, 백업을 위한 PV, PVC, StorageClass까지 어떻게 이어지는지를 살펴볼 차례다.

이보다 직관적인 시각자료는 없다.! (출처: https://www.puzzle.ch/de/blog/articles/2021/08/10/manual-kubernetes-persistentvolumes-migration)


 가장 먼저 PV, PVC를 생성하고 바인딩시켜준다. PVC는 실제 Storage를 요청하고, 이 요청은 StorageClass를 통하여 PV로 바인딩된다. 배포된 PV, PVC를 살펴보자.

// sample-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv
spec:
  capacity:
    storage: 2Gi   # pvc와의 용량 관계 검토할 것!
  storageClassName: sample-storageclass   # storageclass 바인딩
  accessModes:
  - ReadWriteMany
  azureFile:
    secretName: sample-secret
    shareName: sampleshare   # 실제 FileShare 이름이어야 한다
    readOnly: false
  mountOptions:
  - dir_mode=0777  
  - file_mode=0777
  - uid=0
  - gid=0

// sample-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc
  namespace: default
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: sample-storageclass
  resources:
    requests:
      storage: 2Gi


 위처럼 각 PV, PVC는 StorageClass와 바인딩시켜주고, PV의 경우 민감한 데이터는 secret으로 개별 배포 후 연결지어줄 수도 있다. 

 StatefulSet은 주로 상태를 유지해야 하는 애플리케이션을 배포할 때 쓰이는 리소스이다. 캐싱 시스템, 메시지 브로커, 데이터베이스 서버 등에 쓰이는데, 그 이유는 크게 두 가지로 볼 수 있다. 데이터베이스의 경우에는 데이터의 지속성과 무결성이 중요한데 StatefulSet은 파드를 순차적으로 관리하여 각각 고유한 식별자를 부여하기 때문에 데이터베이스의 상태를 보장하는 데에 유용하다. 이는 파드 네이밍 정의를 기존 Deployment가 랜덤한 문자열로 지정한다면, StatefulSet은 <pod-name>-0, <pod-name>-1 과 같이 순서를 부여한다. 두 번째 이유는 StatefulSet은 각 파드에 고유한 PVC를 할당하므로, 데이터베이스 인스턴스마다 고유한 스토리지가 부여되어 보관 및 관리를 도와준다. 

출처: https://www.linkedin.com/pulse/running-stateful-set-pods-using-headless-service-yosr-mahfoudh/


 Deployment와 함께 배포되는 Service 형태와는 다르게 StatefulSet은 Headless Service와 함께 배포된다. 파드의 순서 보장을 한다고 하였는데, Headless Service는 각 파드에 개별적인 DNS 레코드를 생성하기 때문에 네트워크 통신을 쉽게 해준다. 클라이언트가 데이터베이스 연결할 때 각 파드를 개별적으로 식별할 수 있어야하기 때문에 꽤 중요한 역할을 하며, 이는 데이터베이스 고가용성 및 확장성과도 연관된다. 
 StatefulSet과 Service에 해주어야할 설정은 비교적 간단하다. 둘을 연결시키기 위하여 selector를 통일해주고, StatefulSet의 경우에는 spec.volumes[0].namespec.containers[0].name.volumeMounts[0].name을 동일하게 해주어야 한다! 그리고 컨테이너 내부의 volumeMountmountPath 지정은 실제 파드 내부 디렉토리 경로가 된다. 꼭 기억하자! Headless Service는 정말 할게 없다. ClusterIP를 none으로만 지정해주면 이 서비스는 Headless Service가 된다. 

잠깐! 막상 구현할 때에는 "ClusterIP가 None이면 되는구나, 간단하네?" 라고 생각하고 넘어갔지만 막상 왜 ClusterIP가 미지정되는 것이 파드 순서 보장하는 것이랑 어떤 상관관계를 가지는지 궁금해졌다. ClusterIP를 "None"으로 설정하면 이 Service에 ClusterIP가 할당되지 않는다. 그 대신 파드의 이름을 기반으로 한 DNS 레코드가 생성되기 때문에 이 Service는 생성된 DNS를 통하여 파드 간 통신을 도와주는 Service가 되는 것이다. 이로써 파드가 스케일링되거나 재시작되어도 DNS은 유지되고, 데이터베이스와 같은 상태 유지가 필요한 리소스가 관리되는 데에 필요한 순서 보장에도 크게 관여하는 것이다.

 

// sample-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample
spec:
  serviceName: sample-svc
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      restartPolicy: Always
      containers:
        - name: sample
          image: mongo:4.2
          ports:
            - containerPort: 27017
          volumeMounts:             
            - name: sample-volume   # spec.volumes[0].name과 같아야한다!
              mountPath: /data/sample   # 이 경로는 파드 내부 디렉토리 경로다!
      volumes:
        - name: sample-volume
          persistentVolumeClaim:
            claimName: sample-pvc   # pvc 연결은 필수

// sample-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-svc
spec:
  clusterIP: None   # None으로만 해주면 Headless Service로 전환된다
  selector:
    app: sample
  ports:
    - protocol: TCP
      port: 27017
      targetPort: 27017


 yaml을 보고 위에 설명한 내용을 보면 매우 간단하는 것을 알 수 있다. 이제 애플리케이션에 직접적으로 관여하는 모든 리소스 배포가 마무리되었다. 음.. 글이 좀 길어지는데 어쩔 수 없다.. 이제 Cronjob 만 살펴보면 데이터 백업에 대한 프로세스는 다 끝나간다.

 Cronjob은 주기적으로 작업을 실행하는 데에 사용되는 리소스다. 데이터 백업을 위한 필요한 설명만 할 것이기 때문에 추가적인 속성값은 개별 서치를 하길 바란다. Cronjob 구현을 위해서는 cron 표현식과 백업 스크립트 지정이 필요하다. 아래 yaml을 살펴보자.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sample-backup
spec:
  schedule: "0 0 * * *"   # 각각 "분 시 일 월 요일"을 의미한다. 이 경우 매일 자정에 실행된다.
  successfulJobsHistoryLimit: 3   # 성공적으로 완료된 Job의 최대 수량 지정
  failedJobsHistoryLimit: 3   # 실패한 Job이 유지되는 수량 지정
  jobTemplate:
    spec:
      backoffLimit: 1   # 실패시 재시도 횟수
      template:
        spec:
          containers:
          - name: sample
            image: mongo:4.2
            args:
            - /bin/sh
            - -c
            - |
              #!/bin/sh
              TIMESTAMP=$(date +%Y%m%d) 
              BACKUP_DIR="/backup/$TIMESTAMP"
              mongodump --host sample-0.sample-svc.default.svc.cluster.local:27017 --db sample --out $BACKUP_DIR
              tar -zcvf "/backup/${TIMESTAMP}.tar.gz" -C "/backup" "$TIMESTAMP"
              rm -rf "$BACKUP_DIR"
            volumeMounts:
            - name: sample-volume   # StatefulSet과 동일하게 하단 volumes name과 통일
              mountPath: /sample   # 경로 지정
          volumes:
          - name: sample-volume
            persistentVolumeClaim:
              claimName: sample-pvc
          restartPolicy: OnFailure   # 컨테이너 재시작 정책(이 경우 실패시 재시작한다)


 cron 표현식으로는 매일 자정에 데이터를 백업하겠다고 설정하였고, 백업 스크립트는 현재 MongoDB를 쓰고 있기 때문에 mongodump를 통하여 원하는 MongoDB 컬랙션 데이터를 백업 파일로 추출해서 압축한 뒤, 날짜별로 클라우드 플랫폼 내부에 보관하였다. 이 때 DNS가 등장한다. sample-0.sample-svc.default.svc.cluster.local:27017은 백업하고자 하는 IP, 즉 리소스의 경로인데 StatefulSet과 Headless Service 덕분에 무적의 DNS를 가지게 된 것이다. 규칙은 <파드명>.<서비스명>.<네임스페이스>.svc.clsuter.local:<포트> 의 형식이다. 데이터베이스 파드가 수 천번 다운되거나 재시작되어도 파드는 sample-0 이라는 이름으로 재생성되기 때문에 백업 스크립트와 리소스 모두 엔지니어가 변경 여부에 대해 신경쓸 일이 없어진다.



 데이터 백업에 대한 내용이 생각보다 길어져서 여기서 마치고 복원과 폐기에 대한 내용은 3편으로 분리하였다. 부족한 부분이 분명 있겠지만 누군가에겐 시간 절약을 해줄 수 있는 지식과 궁금증을 해소해주는 두 마리 토끼를 잡게 해주는 게시글이기를 바란다. 인프라는 바다처럼 고려할 것들이 많고 어려우면서도 그 안에 소소한 즐거움이 있다. 어려워보이던 것을 몇 주 잡고 있으면 형체가 보이면서 이해가 되는가 하면 프론트나 백에서 코드 구현을 통한 로직 구현만 하다가 인프라 관리에 관여하는 코드를 짜면 뭔가 개발이라는 분야를 조금 더 입체적으로 접근하고 공부하는 기분이다. 프론트엔드 / 백엔드 / 데브옵스 엔지니어 포지션의 분리를 떠나서 개발하는 것에 대한 새로운 즐거움을 주는 경험이 되었고, 아는 만큼 보이는 것이 나를 더 단단하게 해주었으면 좋겠는 바람이다. 

3편에서 봐요!

 

728x90

'Infrastructure' 카테고리의 다른 글

NIPA인증 1. Outline  (2) 2023.10.14