Kubernetesにおけるデータのバックアップ

はじめに

今まで構築してきたKubernetes(以下k8s)環境では,下記のようにCephFs/Rookを用いてk8s Cluster内のストレージシステムを構築してきました.

tenzen.hatenablog.com

k8s Cluster内のストレージシステムはこれで良いのですが,バックアップ機能は必須です.今回は,k8s Cluster外でストレージシステムを用意して自動的にバックアップされるものを構築してみたいと思います.

本記事では以下図1のような構成でバックアップを構築していきます.外部ストレージでは汎用NASを使用します.コンテナ内にNFSマウントしたNASとCephFSをrsyncによって定期的にk8sのcronjobリソースで自動的にバックアップします.また図1にあるように,フルバックアップと差分バックアップを使用してバックアップデータの削減も目指します.

component_fig
図1:今回の構成

CronJobとは

k8sapiリソースの一つであるCronJobで自動バックアップを実行していきます. k8sapiリソースにはJobと呼ばれるWorkLoadsResourceが存在し, Replicasetと同じようにPodを管理します.JobのReplicasetとの大きな違いは,一度起動したPodで処理を実行して正常終了した場合,そのまま再度起動することはないところです.

CronJobはDeploymentと同じ立ち位置でJobを管理し,一定周期で起動してくれます.今回はこのCronJobを用いてrsyncを定期実行していきます.

実装

まずはじめにPersistentVolumeを作成し,CronJobリソースを用いてフルバックアップを実装したのちに,差分バックアップを実装していきます.

フルバックアップの実装

本節でははじめにPersistent Volumeを作成した後に,バックアップ処理用のDockerイメージを作成してフルバックアップ処理を行うCronJobをデプロイしていきます.

Persistent Volumeの準備

まずはじめに,k8s cluster内のストレージではCephFS/Rookを用いているので,それを用いてバックアップ対象のストレージを作成していきます.

以下のような「ceph-pvc.yaml」を作成してください.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tenzen-pvc
  namespace: tenzen
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 200Gi
  storageClassName: csi-cephfs

次にバックアップで使用するPersistent Volume(以下PV)を作成していきますが,こちらは今回k8s cluster外にNFSサーバを用意しておきます.NFS以外でも可能ですので,何かしらk8s cluster外にストレージサーバを建てておいてください. 用意できたら,pvcとpvを含む下記のような「nfs-pvc.yaml」を作成してください.k8s clusterへのnfsマウントの詳しいことは過去記事「 Kubernetes(k8s) v1.16とNvidia-Docker2を用いたマルチノードDeepLearning環境の構築 Part2 - てんぜんの生存日誌」 を参照してください.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs
  namespace: tenzen
  labels:
    type: nfs
    environment: hdd
    speed: slow
    user: tenzen
spec:
  capacity:
    storage: 1000Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: xxx.xxx.xxx.xxx
    path: /data/workspace/tenzen/k8s-nfs
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
  namespace: tenzen
spec:
  selector:
    matchLabels:
      environment: hdd
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi

以上でPVの作成はできましたが,まだマニフェストに追加していくのでデプロイはしないでください.

バックアップ処理用Dockerイメージの作成

バックアップで使用するDockerイメージをUbuntu18.04をベースに作成していきます.以下のようなDockerfileを作成してください.また,コンテナをroot権限で起動させることはよくないためコンテナ内で使用するユーザを作成しておきます.

FROM ubuntu:18.04

ENV USER_NAME tenzen                          #コンテナ内で使用するユーザ名
ENV USER_NAME_UID 1000                     #コンテナ内で使用するユーザID
RUN useradd -m -u ${USER_NAME_UID} ${USER_NAME}          #ユーザを作成

RUN apt-get update && \
    apt-get install -y rsync

上記Dockerfileを元にDockerイメージを作成し,使用しているDocker Registryにpushして使用できるようにしておいてください.

フルバックアップ処理用CronJobの実装

以下のような内容を先ほど作成したマニフェスト「ceph-pvc.yaml」に追加します.今回フルバックアップは一週間に一回とるように設定していますが,rm-containerでフルバックアップが10個を超えた場合削除する処理を行っています.

apiVersion: v1
kind: ConfigMap
metadata:
  name: backup-config
  namespace: tenzen
data:
  original-dir: "/tenzen-pvc/"
  dest-base-dir: "/nfs/backup/full/"
  dest-diff-dir: "/nfs/backup/diff/"
  keep-dirs-num: "10"
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: tenzen-pvc-fullbackup
  namespace: tenzen
spec:
  schedule: "52 3 * * */6"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 0
      template:
        spec:
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
          containers:
            - name: rm-container
              image: hogehoge
              env:
                - name: DEST_BASE_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-base-dir
                - name: KEEP_DIRS_NUM
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: keep-dirs-num              
              command:
              - sh
              - "-c"
              - |
                  DIRS_NUM=$(ls ${DEST_BASE_DIR} | wc -w)
                  RM_DIRS=$(ls -d ${DEST_BASE_DIR}* | xargs readlink -f | head -n $((${DIRS_NUM}-${KEEP_DIRS_NUM})))
                  if [ ${DIRS_NUM} -gt ${KEEP_DIRS_NUM} ];then rm -vr ${RM_DIRS};fi
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
            - name: fullbackup-container
              image: hogehoge
              env:
                - name: ORIGINAL_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: original-dir
                - name: DEST_BASE_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-base-dir
              command:
              - sh
              - "-c"
              - |
                  DEST_FULL_FILE=$(date "+%Y%m%d-%H%M%S")
                  rsync -rlOtcv --delete ${ORIGINAL_DIR} ${DEST_BASE_DIR}${DEST_FULL_FILE}            
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
          volumes:
            - name: tenzen-pvc
              persistentVolumeClaim:
                  claimName: tenzen-pvc
                  readOnly: false
            - name: nfs-pvc
              persistentVolumeClaim:
                  claimName: nfs-pvc
                  readOnly: false
          restartPolicy: Never

上記のbackup-containerでわかりにくいところを少し触れていきます. 基本的にrm-containerもほとんど同じ構成です.

data:
  original-dir: "/tenzen-pvc/"
  dest-base-dir: "/nfs/backup/full/"
  dest-diff-dir: "/nfs/backup/diff/"
  keep-dirs-num: "10"
  • CronJoBのspec以下にはscheduleに毎週3時52分にJobを作成するように,concurrencyPolicyには一つ前のJobが実行中の場合は起動しないように,startingDeadlineSecondsには開始時刻のズレを5分許容するように,successfulJobsHistoryLimit:には正常終了したJobの履歴を5つまで保持するように,failedJobsHistoryLimitには異常終了したJobの履歴を5つまで保持するように,suspendには一時停止しないように指定しています.

    • schedule : Jobの作成間隔を指定します.フォーマットはUNIX標準のcrontab形式で自由に記述することができます.以下のGCPドキュメントに例が豊富に載っているためオススメです.cloud.google.com

    • concurrencyPolicy : 前回のJobがまだ終了してない場合にJobを作成するかを設定できます.

      • Allow : 制御しない.
      • Forbid : 前回のJobが終了していない場合新たにJobを作成しない.
      • Replace : 前回のJobをキャンセルして新たにJobを作成する.
    • startingDeadlineSeconds : Job作成の時刻が遅れてしまった際に許容できる時間を設定できます.設定した許容時間内の場合遅れてもJobが作成されます.

    • successfulJobsHistoryLimit : 正常終了したJobを後から参照できるように保持しておく数を指定できます.

    • failedJobsHistoryLimit : 異常終了したJobを後から参照できるように保持しておく数を指定できます.

    • suspend : CronJobをデプロイした後一時的にJobを実行して欲しくない場合 trueにすると一時的にJobの作成を停止させることができます.

spec:
  schedule: "52 3 * * */6"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  suspend: false
  • spec.jobTemplate.specにはcompletionに1回きりの正常終了を求めるために,parallelismに1並列で実行させるように,backoffLimitに失敗を許さないように設定します.
    • completion : 一回のJob実行で正常終了する回数を指定できます.
    • parallelism : 並列実行させる数を指定できます.
    • backoffLimit : 許容できる異常終了の回数を指定できます.
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 0
      template:
  • spec.jobTemplate.spec.template.spec.securityContext にはrunAsNonRootにroot権限のコンテナを作成しないように,runAsUserにDockerfileで作成した一般ユーザのUIDを指定します.

    • runAsNonRoot : trueで非rootで,falseでroot権限でコンテナが作成されます.Dockerイメージにおいてroot権限を要求していてtrueに設定していた場合,エラーがでてコンテナの作成がとまります.

    • runAsUser : コンテナで作成するUIDを指定します.

template:
  spec:
    securityContext:
      runAsNonRoot: true
      runAsUser: 1000
  • spec.jobTemplate.spec.template.spec.containersにはimageにDockerfileをビルドした際に指定した名前を指定します.envにはconfigmapを読み込んでコンテナ内に環境変数を設定します.

    • image : バックアップコンテナのDockerイメージ
    • configMapKeyRef.name : configmapの名前
    • configMapKeyRef.key : 登録する環境変数の名前
containers:
  - name: fullbackup-container
    image: hogehoge
    env:
      - name: ORIGINAL_DIR
        valueFrom:
            configMapKeyRef:
              name: backup-config
              key: original-dir
      - name: DEST_BASE_DIR
        valueFrom:
            configMapKeyRef:
              name: backup-config
              key: dest-base-dir
  • spec.jobTemplate.spec.template.spec.containers.commandでrsyncを実行します.

    • DEST_FULL_FILE : バックアップ先フォルダ名,今回は日時を取得している.
    • ${ORIGINAL_DIR} : バックアップ元ディレクト
    • ${DEST_BASE_DIR}${DEST_FULL_FILE} : バックアップ先ディレクトリのフルパス
command:
- sh
- "-c"
- |
    DEST_FULL_FILE=$(date "+%Y%m%d-%H%M%S")
    rsync -rlOtcv --delete ${ORIGINAL_DIR} ${DEST_BASE_DIR}${DEST_FULL_FILE}

つなぎ合わせて完成したマニフェスト「ceph-pvc.yaml」は以下のようになります.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tenzen-pvc
  namespace: tenzen
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 200Gi
  storageClassName: csi-cephfs
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: backup-config
  namespace: tenzen
data:
  original-dir: "/tenzen-pvc/"
  dest-base-dir: "/nfs/backup/full/"
  dest-diff-dir: "/nfs/backup/diff/"
  keep-dirs-num: "10"
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: tenzen-pvc-fullbackup
  namespace: tenzen
spec:
  schedule: "52 3 * * */6"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 0
      template:
        spec:
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
          containers:
            - name: rm-container
              image: hogehoge
              env:
                - name: DEST_BASE_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-base-dir
                - name: KEEP_DIRS_NUM
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: keep-dirs-num              
              command:
              - sh
              - "-c"
              - |
                  DIRS_NUM=$(ls ${DEST_BASE_DIR} | wc -w)
                  RM_DIRS=$(ls -d ${DEST_BASE_DIR}* | xargs readlink -f | head -n $((${DIRS_NUM}-${KEEP_DIRS_NUM})))
                  if [ ${DIRS_NUM} -gt ${KEEP_DIRS_NUM} ];then rm -vr ${RM_DIRS};fi
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
            - name: fullbackup-container
              image: hogehoge
              env:
                - name: ORIGINAL_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: original-dir
                - name: DEST_BASE_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-base-dir
              command:
              - sh
              - "-c"
              - |
                  DEST_FULL_FILE=$(date "+%Y%m%d-%H%M%S")
                  rsync -rlOtcv --delete ${ORIGINAL_DIR} ${DEST_BASE_DIR}${DEST_FULL_FILE}            
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
          volumes:
            - name: tenzen-pvc
              persistentVolumeClaim:
                  claimName: tenzen-pvc
                  readOnly: false
            - name: nfs-pvc
              persistentVolumeClaim:
                  claimName: nfs-pvc
                  readOnly: false
          restartPolicy: Never

最後にデプロイしていきます.

$ kubectl apply -f nfs-pvc.yaml
$ kubectl apply -f ceph-pvc.yaml

差分バックアップ処理用CronJobの実装

本節では,最初に今回使用する差分バックアップについて少し触れた後実際に実装を行っていきます.

今回使用する差分バックアップ

今回使用する差分バックアップでは,rsyncで使用できるハードリンクを使用していきます.ハードリンクとは1回目のフルバックアップから増えた分のみをコピーし,変更がなかった箇所はフルバックアップへのリンクを作ります.つまり,ユーザからは世代間りしつつ全ての世代で全部のフォルダを見ることができるようになります.またフルバックアップと同様に,差分バックアップにおいてもrm-containerで10個を超えた分を削除する処理を行っています.

マニフェストの準備とデプロイ

以下のようなマニフェスト「diff_backup.yaml」を作成してください.各パラメータは基本的に「ceph-pvc.yaml」のCronJobで解説したものなので参考にして設定をカスタマイズしてください.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: tenzen-pvc-diffbackup
  namespace: tenzen
spec:
  schedule: "35 * * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 5
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 0
      template:
        spec:
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
          containers:
            - name: rm-container
              image: hogehoge
              env:
                - name: DEST_DIFF_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-diff-dir
                - name: KEEP_DIRS_NUM
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: keep-dirs-num              
              command:
              - sh
              - "-c"
              - |
                  DIRS_NUM=$(ls ${DEST_DIFF_DIR} | wc -w)
                  RM_DIRS=$(ls -d ${DEST_DIFF_DIR}* | xargs readlink -f | head -n $((${DIRS_NUM}-${KEEP_DIRS_NUM})))
                  if [ ${DIRS_NUM} -gt ${KEEP_DIRS_NUM} ];then rm -vr ${RM_DIRS};fi
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
            - name: backup-container
              image: hogehoge
              env:
                - name: LINK_BASE_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-base-dir
                - name: ORIGINAL_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: original-dir
                - name: DEST_DIFF_DIR
                  valueFrom:
                      configMapKeyRef:
                        name: backup-config
                        key: dest-diff-dir
              command:
              - sh
              - "-c"
              - |
                  DEST_FULL_FILE=$(date "+%Y%m%d-%H%M%S")
                  LINK_FULL_FILE=$(ls ${LINK_BASE_DIR} | tail -n 1)
                  rsync -rlOtcv --delete --link-dest=${LINK_BASE_DIR}${LINK_FULL_FILE} ${ORIGINAL_DIR} ${DEST_DIFF_DIR}${DEST_FULL_FILE}
              volumeMounts:
                - mountPath: "/tenzen-pvc"
                  name: tenzen-pvc
                - mountPath: "/nfs"
                  name: nfs-pvc
              resources:
                  limits:
                    nvidia.com/gpu: 0
                    cpu: 2000m
                    memory: 8Gi
          volumes:
            - name: tenzen-pvc
              persistentVolumeClaim:
                  claimName: tenzen-pvc
                  readOnly: false
            - name: nfs-pvc
              persistentVolumeClaim:
                  claimName: nfs-pvc
                  readOnly: false
          restartPolicy: Never

最後にデプロイしていきます.

$ kubectl apply -f diff_backup.yaml

補足

今回作成したCronJobは全てUTCで作成されてしまうので,それを考慮してJobの起動タイミングなどを設定してください.本来はタイムゾーンを日本にして作成するべきなので,今後時間を見てタイムゾーンを日本に変更していきたいと思います.

最後に

今回はk8sにおけるデータのバックアップ処理について見ていきました.CronJobはまだまだ応用が効くと思うので,これからも活用していきたいと思います.