Jitrak Blog

Backup MongoDB in GKE by Cronjob Workload to GCS

ทำ Backup MongoDB ใน Google Kubernetes Engine ด้วย CronJob Workload to Google Cloud Storage

28 Oct 2020 18:00

Written by: Yosapol Jitrak

Tags:

MongoDB

Automation

Kubernetes

k8s

GKE

GCP

GCS

Cronjob

ล่าสุดในงาน GDG Cloud Bangkok DevFest 2020
ผมได้เป็น Speaker ในหัวข้อ Backup MongoDB in GKE by Cronjob Workload to GCS
ซึ่งเวลาค่อนข้างจำกัด ไม่มีเวลาอธิบายเท่าไหร่ จึงคิดว่าจะมาอธิบายแบบละเอียดในบทความนี้

Yosapol Jitrak ขอบคุณรูปจาก คลาวด์ เอซ - Cloud Ace Thailand

Why

เริ่มต้นด้วยทำไมเราจะต้องทำสิ่งนี้ ทุกคนที่ทำงานสายนี้น่าจะรู้ดีนะครับว่าการสำรองข้อมูล ถือเป็นเรื่องที่สำคัญ ล่าสุดเพิ่งจะมีข่าวดังไป คือ โรงพยาบาลแห่งนึงในประเทศโดน Ransomware เรียกค่าไถไป โดยถึงแม้ว่าจะมี Backup ไว้ แต่ก็เป็นข้อมูลที่ทำสำรองไว้เมื่อหลายปีมาแล้ว เห็นข่าวแบบนี้แล้วเราก็น่าจะเห็นความสำคัญของการสำรองข้อมูลมากขึ้นไม่มากก็น้อย ซึ่งแน่นอนการสำรองข้อมูลด้วยวิธีการแบบ Manual ก็เป็นงานที่ซ้ำซาก และค่อนข้างที่จะน่าเบื่อ คนเลยไม่ค่อยนิยมทำกัน ถ้าจะทำโดยส่วนมากแล้วเขาก็จะใช้เครื่องมือพวก Cron ในการทำ Schedule backup ยกตัวอย่างเป็นทุก ๆ วัน ตอนตี 2 เป็นต้น แต่คราวนี้ถ้ามันอยู่ใน Kubernetes (K8s) เราจะทำอย่างไรดี แน่นอนเราคงไม่อยากไป Set CronJob ใน Container ที่ Run DB ของเราอยู่แน่นอน เพราะจะขัดหลักการ Single Responsibility ที่มีหลาย Service run อยู่ใน Container เดียวกัน (https://runnable.com/blog/9-common-dockerfile-mistakes)

โจทย์ของผมมีดังนี้ครับ

ขยายเรื่องความต้องการที่ว่าข้อมูลสามารถย้ายข้ามไปยัง Cluster อื่นได้ ในกรณีที่เราแยก Environment ระหว่าง Develop, Staging, Production ด้วยการแยก Cluster เราสามารถเอาข้อมูลของ Environment จาก Cluster นึงไปยังอีก Cluster นึงได้ อาจจะเป็นการเอาข้อมูลจาก Develop ไปทดสอบที่ Staging หรือนำข้อมูลจาก Production มาวิเคราะห์ปัญหาที่เกิดขึ้น ใน Environment อื่น เป็นต้น

ขยายเรื่องความต้องการสามารถกู้ข้อมูลคืนแค่บาง Collection ได้ แน่นอนว่าบางครั้งเราไม่ต้องการกู้คืนข้อมูลทั้งหมดทุก Collection อาจจะต้องการแค่บาง Collection เท่านั้น

Ideas

ไม่นานมานี้ผมได้รู้จักกับ Verelo ซึ่งเป็นเครื่องมือเอาไว้สำหรับ Backup และ Restore ได้ทั้ง Kubernetes (K8s) resources และ Persistent Volumes (PV) ซึ่งจากที่ดูแล้วน่าจะตอบโจทย์เราได้เกือบหมดครับ ยกเว้นเสียแต่เรื่องสามารถกู้ข้อมูลคืนแค่บาง Collection ได้ ซึ่งถ้าเราใช้ Velero ก็ทำไม่ได้ครับ

Velero Reference: https://velero.io/

คราวนี้เราลองมาดูกันนะครับ จริง ๆ แล้วใน K8s ก็มี Workload ที่เป็น CronJob ที่ถึงแม้ว่าจะเป็น Beta version แต่ก็มีมานานแล้ว ซึ่งดูน่าจะตอบโจทย์ของเราพอดี
CronJob Workload Reference: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/

นอกจากนี้ คำถามต่อมาคือเราจะเอาข้อมูล Backup ของเราไปเก็บไว้ที่ไหน ซึ่งถ้าใครเคยใช้ Cloud มาบ้างน่าจะพอรู้จักพวก Simple Storage Service (S3) ของ AWS หรือ Google Cloud Storage (GCS) ของ Google Cloud เป็นต้น ซึ่งดูแล้วน่าจะเหมาะสมกว่าการที่เราจะต้องสร้าง PV และ Disk มาเก็บไว้เอง ทั้งเรื่องของ Service-level agreement (SLA) และการ Maintain ต่าง ๆ แน่นอนงานนี้เราอยู่ในงาน GDG Cloud Bangkok DevFest 2020 แถม Cluster อยู่ใน GKE ผมจึงจะกล่าวถึง GCS เป็นหลัก ใน GCS ยังมีสิ่งที่เรียกว่า Object Lifecycle Management ที่ทำให้เราสามารถตั้งกฎได้ว่าเมื่อ Object ที่ถูกเก็บไว้ถึงเวลาที่กำหนด เราสามารถสั่งให้ลบ หรือย้าย Class ของ Object นั้นไปยัง Class ที่มีราคาถูกกว่าเดิมได้

Object Lifecycle Management Reference: https://cloud.google.com/storage/docs/lifecycle

Backup MongoDB in GKE by Cronjob Workload to GCS Overview ภาพไอเดียคร่าว ๆ

คราวนี้มาต่อของที่จำเป็นจะต้องใช้นะครับ

ขยายส่วน Service account key เราจำเป็นที่จะต้องมีก็เพื่อให้สิทธิ์ในการเข้าไปเขียนไฟล์ใน GCS Bucket ที่เราสร้างขึ้นมา โดยใน Google Cloud เราจะต้องทำการสร้าง Service account ขึ้นมาก่อน นำ Service account นั้นไปให้สิทธิ์ต่าง ๆ โดยในที่นี้เราจะสร้าง Service account key เป็นสิ่งที่เอาไว้สำหรับใช้ในการ Authentication ว่าเป็น Service account นั้น

คำถามถัดมาคือเราจะทำอย่างไรให้มีทั้ง mongodump และ gsutil ใน Container

เริ่มแรกสุดก็คิดแบบง่ายก่อนเลย ก็คือเราจะต้องทำการ Build image ใหม่ขึ้นมา เพื่อให้มี command ทั้ง 2 ตัว ซึ่งถ้าลอง Googling ดูก็จะมีคนคิดเหมือนกัน แต่เป็นใน Version MySQL ตามรูปด้านล่างนี้

MySQL Backup Docker Image Reference: https://medium.com/searce/cronjob-to-backup-mysql-on-gke-23bb706d9bbf

แต่ก็มาคิดต่ออีกว่า แล้วถ้าเกิดต้องการเปลี่ยน Version ของ MongoDB หรือ Google Cloud SDK เราก็จะต้องมาทำ Build image ใหม่อีกอย่างนั้นเหรอ ตอนนั้น MongoDB ที่ดูแลอยู่มีหลาย Cluster รวมถึงแต่ละ Cluster ก็มีหลาย Version อีก เราจะต้อง Build image แยกของใครของมันก็ดูจะถึกเกินไป แล้วเวลาที่จะต้อง Update Google Cloud SDK version ทีนี้จะต้อง Build ใหม่กี่ image แค่คิดก็ปวดหัวแล้ว

Think Meme

ซึ่งถัดมาก็คิดต่อได้ว่า Pod ใน K8s สามารถมีได้หลาย Container รวมถึงมี Init containers ด้วย

Sidecar Container

Reference: https://banzaicloud.com/blog/k8s-sidecars/

Backup MongoDB in GKE by Cronjob Workload to GCS Detail ภาพในหัวจึงออกมาได้เป็นแบบนี้

Pod มี 2 Container ที่มี image ตามนี้

ข้อดีของการใช้ Sidecar container แบบนี้ คือเราไม่ต้องมาสร้าง และดูแล Container image เอง google/cloud-sdk อยากมี Update ก็ใช้ตัวล่าสุดไปเลย โอกาสที่จะ Breaking change ก็มี แต่น้อยมาก และส่วนตัวคิดว่าดีกว่าปล่อยให้ Deprecate ในอนาคตการไปขึ้น Backup schedule ตัวใหม่ก็แค่เปลี่ยน image mongo ให้ตรงก็พอแล้ว

Demo

สำรวจสิ่งที่เตรียมไว้ก่อน

Create Service Account and Key

อย่างแรกเลยจะทำการเตรียม Service account

ไปที่ IAM และเลือกไปที่ Service Accounts

จากนั้นกดปุ่ม CREATE SERVICE ACCOUNT

ทำการตั้งชื่อ Service account name จากนั้น กดปุ่ม CREATE

ถึงตรงนี้จริง ๆ เราสามารถให้สิทธิ์จัดการ Google Cloud Storage กับ Service account นี้กับทั้ง Google Cloud Project นี้ได้เลย แต่ผมจะไม่ให้สิทธิ์ที่ตรงนี้ เพราะเราต้องการให้สิทธิ์กับแค่ Bucket เดียวเท่านั้น เราควรให้สิทธิ์เท่าที่จำเป็น เพราะฉะนั้นกด DONE ไปได้เลย

เมื่อสร้าง Service account เสร็จแล้ว ให้ทำการ Copy Email ของ Service account ที่เพิ่งสร้างเก็บไว้ก่อน

ถัดไปเราจะมาทำการสร้าง Key จาก Service account นี้ กดไปที่จุด 3 จุด ตรงคอลัมน์ Actions แล้วกด Create key ต่อเลย

เมื่อกด Create key มาแล้ว จะมีให้เลือกระหว่าง JSON และ P12 ปกติจะใช้ JSON ก็เลือก JSON และกด CREATE ไป หลังจากนั้นเราก็เลือกเซฟไฟล์ไว้สักที่ในเครื่องเราก่อน

Create GCS Bucket

ต่อมาจะทำการสร้าง Google Cloud Storage Bucket ใหม่

ไปที่ Storage ใน Google Cloud Console

กดปุ่ม Create Bucket

ตั้งชื่อ แล้วกด CONTINUE

เลือก Location type จากนั้นกดปุ่ม CREATE

Binding Service account to Bucket

เมื่อเราสร้าง GCS Bucket เป็นที่เรียบร้อยแล้ว เราจะทำการผูกสิทธิ์ในการจัดการกับ Bucket นี้ให้กับ Service account ที่เราสร้างไว้ก่อนหน้านี้

ไปที่ PERMISSIONS

จากนั้นกดไปที่ปุ่ม +ADD

Create ConfigMaps

ตอนนี้เรากำลังจะทำการสร้าง Configmaps ไว้สำหรับเก็บตัวแปร และ File scripts ของเรา แบ่งออกเป็น 3 ConfigMap ดังนี้

Create mongodb-backup-schedule-env.yaml

apiVersion: v1
data:
  BACKUPS_FOLDER: /backups
  GCS_BUCKET: gdg-cloud-devfest-mongodb-backup
  MONGO_HOST: mongodb
kind: ConfigMap
metadata:
  name: mongodb-backup-schedule-env
  namespace: mongodb

เป็น ConfigMap ที่เอาไว้เก็บค่าตัวแปรที่ตัวที่ Scripts จะเอาไปใช้อีกที โดยที่ไม่ต้องไป Hardcode อยู่ใน Script

Create mongodump-all-db-sh.yaml

apiVersion: v1
data:
  mongodump-all-db.sh: |-
    #!/bin/bash

    mongodump -h $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -o $BACKUPS_FOLDER --forceTableScan --gzip
kind: ConfigMap
metadata:
  name: mongodump-all-db-sh
  namespace: mongodb

เป็น ConfigMap ที่จะเอาไปทำการสร้างเป็น File Bash script ในการ Dump database ที่ต้องการ Backup ออกมาไปเป็นไว้ที่ path ตามตัวแปร BACKUPS_FOLDER

Create gsutil-to-gcs-sh.yaml

apiVersion: v1
data:
  gsutil-to-gcs.sh: |-
    #!/bin/bash

    FOLDER=$(date +%Y%m%d)
    gcloud auth activate-service-account --key-file=/service-key/service-key.json
    gsutil -m rsync -r $BACKUPS_FOLDER gs://$GCS_BUCKET/$FOLDER
kind: ConfigMap
metadata:
  name: gsutil-to-gcs-sh
  namespace: mongodb

เป็น ConfigMap ที่จะเอาไปทำการสร้างเป็น File Bash script ในการนำ Backup file ทั้ง BACKUPS_FOLDER ไปเก็บไว้ที่ GCS Bucket ซึ่งก่อนที่จะนำไฟล์ไปเก็บไว้ที่ GCS ได้ จะต้องทำการ Authentication กับ Service account key ที่ Download มา และจะตั้งชื่อตาม ปีเดือนวันที่ทำการ Backup ไว้

หลังจากนั้นให้ทำการ Create configmap ทั้งหมดเข้าไปใน K8s Cluster โดย YAML ทั้งหมด ผมจะทำอยู่ที่ Namespace เดียวกับที่ Database run อยู่

kubectl create -f configs/

Create Secrets

ก่อนหน้านี้เราได้ทำการสร้าง ConfigMap ไป ซึ่งจะเห็นว่ายังมีบางตัวแปรที่ขาดอยู่ อย่าง MONGO_USERNAME, MONGO_PASSWORD, service-key.json ที่ไม่รู้ไปเอามาจากไหน เนื่องจากของพวกนี้เป็น Credential ที่ไม่ควรเก็บเป็น Plain text ในระบบ ใน K8s จะมีสิ่งที่เรียกว่า Secret เอาไว้เก็บของพวกนี้โดยเฉพาะ ถึงแม้จะเป็นแค่การเข้า Base64 ก็เถอะ แต่ถ้าอยากได้ Security มากกว่านี้ สามารถใช้ Third party ได้นะครับ

ให้ทำการเข้า Base64 ทั้ง MONGO_USERNAME, MONGO_PASSWORD, service-key.json

โดยใช้คำสั่ง Base64 และแทนที่ DATA ด้วยข้อมูลที่ต้องการเข้า Base64 นะครับ

echo -n "DATA" | base64

สำหรับ File Service account key ให้ใช้

cat service-key.json | base64

สำหรับ Linux อาจจะต้องใช้ท่านี้ในการเข้า Base64 กับ File Service account key เพื่อให้ผลลัพท์อยู่ในบรรทัดเดียว

cat service-key.json | base64 -w 0

mongodb-user-password.yaml

apiVersion: v1
data:
  MONGO_PASSWORD:
  MONGO_USERNAME:
kind: Secret
metadata:
  name: mongodb-user-password
  namespace: mongodb
type: Opaque

เป็น Secret ที่เก็บค่า username และ password สำหรับในการต่อ Database โดยให้เติมค่าที่ทำการเข้า Base64 เองเลยนะครับ

gcs-service-account-key.yaml

apiVersion: v1
data:
  service-key.json:
kind: Secret
metadata:
  name: gcs-service-account-key
  namespace: mongodb
type: Opaque

เป็น Secret ที่เก็บค่า Service account key ที่ Download มาก่อนหน้านี้ และทำการเข้า Base64 เรียบแล้วแล้ว อย่าลืมเติมค่าที่ทำการเข้า Base64 แล้วเองนะครับ

ทำการสร้าง Secret ทั้งหมด เข้าไปใน K8s Cluster โดย YAML ทั้งหมด ผมจะทำอยู่ที่ Namespace เดียวกับที่ Database run อยู่

kubectl create -f secrets/

Create Backup CronJob

คราวนี้ก็มาถึงพระเอกของงานเราแล้ว ก็คือตัว CronJob นั้นเอง

mongodb-backup-schedule.yaml

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: mongodb-backup-schedule
  namespace: mongodb
spec:
  concurrencyPolicy: Replace
  failedJobsHistoryLimit: 10
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - command:
                - /gsutil-to-gcs-sh/gsutil-to-gcs.sh
              envFrom:
                - configMapRef:
                    name: mongodb-backup-schedule-env
              image: google/cloud-sdk
              imagePullPolicy: Always
              name: gsutil-to-gcs
              resources:
                limits:
                  cpu: 500m
                  ephemeral-storage: 10Gi
                  memory: 256Mi
                requests:
                  cpu: 200m
                  ephemeral-storage: 10Gi
                  memory: 256Mi
              volumeMounts:
                - mountPath: /backups
                  name: backups
                - mountPath: /service-key
                  name: service-key
                - mountPath: /gsutil-to-gcs-sh
                  name: gsutil-to-gcs-sh
          initContainers:
            - command:
                - /mongodump-all-db-sh/mongodump-all-db.sh
              envFrom:
                - secretRef:
                    name: mongodb-user-password
                - configMapRef:
                    name: mongodb-backup-schedule-env
              image: mongo:4.2
              imagePullPolicy: Always
              name: mongodump-all-db
              resources:
                limits:
                  cpu: 500m
                  ephemeral-storage: 10Gi
                  memory: 256Mi
                requests:
                  cpu: 200m
                  ephemeral-storage: 10Gi
                  memory: 256Mi
              volumeMounts:
                - mountPath: /mongodump-all-db-sh
                  name: mongodump-all-db-sh
                - mountPath: /backups
                  name: backups
          restartPolicy: Never
          volumes:
            - emptyDir:
                sizeLimit: 10Gi
              name: backups
            - configMap:
                defaultMode: 320
                items:
                  - key: mongodump-all-db.sh
                    path: mongodump-all-db.sh
                name: mongodump-all-db-sh
              name: mongodump-all-db-sh
            - configMap:
                defaultMode: 320
                items:
                  - key: gsutil-to-gcs.sh
                    path: gsutil-to-gcs.sh
                name: gsutil-to-gcs-sh
              name: gsutil-to-gcs-sh
            - name: service-key
              secret:
                defaultMode: 320
                items:
                  - key: service-key.json
                    path: service-key.json
                secretName: gcs-service-account-key
  schedule: '*/5 * * * *'
  successfulJobsHistoryLimit: 10

อธิบายเพิ่มเติม

ทำการสร้าง CronJob เข้าไปใน K8s Cluster โดย YAML ผมจะทำอยู่ที่ Namespace เดียวกับที่ Database run อยู่

kubectl create -f cronjobs/

หลักจากนั้นรอสักพัก เพราะเราตั้งไว้ว่าทุกเวลาที่หาร 5 นาทีลงตัวถึงจะเริ่มทำงาน


ภาพเมื่อ CronJob Backup ทำงานเสร็จ


ภาพ Database ที่ Backup ได้


ภาพไฟล์ Collection ที่ทำการ Backup ได้

เนื่องจากจุดประสงค์หลักของบทความนี้คือการทำ Schedule Backup MongoDB ใน GKE Cluster ไม่ได้กล่าวถึงการ Restore เพราะวิธีการต่อ MongoDB ที่ Run อยู่ใน K8s มันมีได้หลายวิธี ให้คุณผู้อ่านไปเลือกที่เหมาะสมเองจะดีกว่า แต่สามารถไปดูคลิปเต็มตัวอย่างการ Restore ในงานได้ที่ Backup MongoDB in GKE by Cronjob Workload to GCS - Yosapol Jitrak

เป็นบทความที่ค่อนข้างยาวเลย หวังว่าผู้ที่หลงเข้ามาอ่านจะได้ประโยชน์ไปบ้างนะครับ บทความนี้ตอนแรกคิดว่าจะเขียนตั้งแต่ปีที่แล้ว แต่ก็เลื่อนมาเรื่อย จนมีโอกาสได้มาเป็น Speaker ก็เลยถือโอกาสเขียนมันซะเลยแล้วกัน ไว้เจอกันที่บทความหน้าครับ