1
2
Production Kubernetes Online Study (=PKOS)는 쿠버네티스 실무 실습 스터디입니다.
CloudNet@ Gasida(가시다)님이 진행하시며, 책 "24단계 실습으로 정복하는 쿠버네티스"을 기반으로 진행하고 있습니다.

2주차 스터디에서는 쿠버네티스의 네트워크와 스토리지를 중점적으로 공부하였다. 분량이 많아 네트워크와 스토리지를 나눠서 블로그를 작성할 예정이다. 이번 블로그 글에서는 로컬 스토리지에 대해 공유하겠다.

일반적으로 로컬 스토리지는 IOPS 성능이 특화되어 있지만 노드에 종속되어 있어 고가용성이나 스토리지 기능에 제한이 있다. 이러한 제한을 없애기 위한 과정으로 로컬 스토리지의 Hostpath, local 볼륨을 마운트하여 테스트를 진행할 것이고, 마지막으로는 로컬 볼륨에서 고가용성과 스토리지 기능(백업)을 가진 Mysql 데이터베이스를 구성하겠다. 추가로 스토리지 성능 측정과 모니터링 과정, QnA를 준비하였다.

본론으로 들어가서, 쿠버네티스에서 스토리지를 사용하는 이유는 무엇일까? 스토리지가 데이터를 저장하는 용도인 것처럼 데이터 저장을 위해서이다. 예를 들어, 데이터베이스같은 애플리케이션을 파드로 운영한다고 가정해보자, 파드 라이프사이클과 별개로 데이터가 보존되어야 한다. 이를 위해 쿠버네티스에서는 PV(Persistent Volume)과 PVC(Persistent Volume Claim) 리소스를 제공한다. 또한, 데이터 관리 방법(데이터 저장 위치, 데이터 공유, 확장성)에 따라 여러 스토리지 볼륨과 기능을 제공한다. 이처럼 데이터를 보존해야 하는 애플리케이션을 상태있는(Stateful) 애플리케이션이라 칭하며 스토리지를 통해 클러스터 내의 컨테이너에 안정적이고 지속적인 데이터를 제공할 수 있다.




로컬 스토리지

로컬 스토리지는 말 그대로 파드의 스토리지로 서버 내부 볼륨의 스토리지를 사용하는 것이다. AWS EC2는 내부 볼륨인 인스토어 스토어를 사용한다. 로컬 스토리지 구현은 쿠버네티스에서 HostPath, Local 볼륨 마운트로 나뉘어 사용이 가능하다. HostPath 볼륨과 Local 볼륨의 차이는 쿠버네티스 볼륨 리소스(PV) 사용 유무에 따라 구분한다.


일반적으로 내부 볼륨의 스토리지를 사용하는 만큼, 다른 원격 스토리지와 비교했을때 IOPS 성능이 뛰어나다. cncf 공식사이트에서 IOPS 기준 약 2~3배의 차이가 난다고 하니 성능 필요의 애플리케이션에서는 도입을 고려할 만하다. 그리고 로컬스토리지는 EC2 에 종속되어 있어 고가용성 구성과 백업같은 기능 사용에 추가 구성이 필요하다. 필자는 이를 해결하기 위해 볼륨프로비저닝 플러그인인 Local-path-provisioner 와 백업 솔루션인 Velero를 사용하였다.


먼저 Local-path-provisioner 플러그인 설치 과정을 살펴볼 것이고, 로컬스토리지 이해를 위해 3가지의 케이스로 나뉘어 테스트를 진행하겠다.

  • Local-path-provisioner PV를 통한 고가용성 테스트
  • Hospath PV를 통한 고가용성 테스트
  • 파드간 데이터 동기화 구성

local-path-provisioner

볼륨 프로비저닝 플러그인 중 하나로, 로컬 노드의 파일 시스템 경로를 사용하여 PVC(Persistent Volume Claim)를 만들어주는 역할을 한다. PVC를 통해 볼륨을 요청하면 PV가 자동으로 생성되어 연결된다고 보면 된다.

설치시, 로컬 노드에 대한 볼륨 설정이 필요하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.23/deploy/local-path-storage.yaml
vim local-path-storage.yaml
----------------------------
apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-path-provisioner
  namespace: local-path-storage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: local-path-provisioner
  template:
    metadata:
      labels:
        app: local-path-provisioner
    spec:  # 추가 부분  
      nodeSelector:
        kubernetes.io/hostname: "마스터 노드 이름 입력 "
      tolerations:
        - effect: NoSchedule
          key: node-role.kubernetes.io/control-plane
          operator: Exists
...
kind: ConfigMap
apiVersion: v1
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-
    {
            "nodePathMap":[
            {
                    "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
                    "paths":["/data/local-path"] # 추가 부분
            }
            ]
    }
----------------------------
  • Deployment / spec.spec 에서 nodeselector 와 tolerations 로 볼륨 배치 노드 설정(마스터 노드로 파드 배치)
  • Configmap / data config.json에서 로컬 볼륨 PATH 설정

설치 확인

1
2
3
4
kubectl get sc local-path
----------------------------
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  29m



Case 1. Local-path-provisioner PV를 통한 고가용성 테스트

HostPath 볼륨이 IOPS 성능이 좋은 것을 앞서 확인하였다. 성능적으로 사용하기 좋은 볼륨이라 할 수 있으나 고가용성 구성이 필요하다.

Hostpath 볼륨의 문제점으로 노드간 마이그레이션에 자유롭지 못하기 때문이다. 이해를 위해 직접 예제 파드를 배포해보고 고가용성을 테스트 해보겠다.

Local-path-provisioner PV를 통한 고가용성 테스트

앞서 배포한 Local-path-provisioner 를 통해 PV를 생성하여 파드를 배포하고 고가용성 구성을 테스트해보겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 파드 예제 
apiVersion: v1
kind: PersistentVolumeClaim
metadata: 
  name: localpath-claim
spec: 
  accessModes: 
    - ReadWriteOnce
  resources: 
    requests: 
      storage: 2Gi
  storageClassName: "local-path"
---
apiVersion: apps/v1
kind: Deployment
metadata: 
  name: date-pod
  labels: 
    app: date
spec: 
  replicas: 1
  selector: 
    matchLabels: 
      app: date
  template: 
    metadata: 
      labels: 
        app: date
    spec: 
      terminationGracePeriodSeconds: 3
      containers: 
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"] 
        volumeMounts: 
        - name: pod-persistent-volume
          mountPath: /data
      volumes: 
      - name: pod-persistent-volume
        persistentVolumeClaim: 
          claimName: localpath-claim

파드 배포 후 노드 드레인을 진행해보겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

# 배포 파드의 노드 확인
PODNODE=$(kubectl get pod -l app=date -o jsonpath={.items[0].spec.nodeName})
echo $PODNODE

# 노드 드레인과 파드 모니터링
kubectl drain $PODNODE --force --ignore-daemonsets --delete-emptydir-data && kubectl get pod -w
---------------------------------
node/i-0c41dc0f6eeb01730 cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/aws-node-w8bxm, kube-system/ebs-csi-node-thndq, kube-system/node-local-dns-v2hhc
evicting pod default/date-pod-d95d6b8f-q9skb
evicting pod kube-system/metrics-server-5f65d889cd-9btc7
pod/metrics-server-5f65d889cd-9btc7 evicted
pod/date-pod-d95d6b8f-q9skb evicted
node/i-0c41dc0f6eeb01730 drained

NAME                      READY   STATUS    RESTARTS   AGE
date-pod-d95d6b8f-x5vrb   0/1     Pending   0          2m

노드 드레인시, 상태가 Pending 인 것을 확인할 수 있다. 이는 다른 노드에 PV볼륨이 없기 때문이다.

마찬가지로 파드를 5개로 추가해도 볼륨이 있는 노드에만 파드가 올라온 것을 확인할 수 있다.

1
2
# 예제 파드 개수 5개로 증가
kubectl scale deployment date-pod --replicas=5

case1-pod5.png




Case2. HostPath를 통한 고가용성 테스트

볼륨 Hostpath로 노드 PATH를 직접 지정하여 고가용성을 테스트해보겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1
kind: Deployment
metadata:
  name: date-pod
  labels:
    app: date
spec:
  replicas: 1
  selector:
    matchLabels:
      app: date
  template:
    metadata:
      labels:
        app: date
    spec:
      terminationGracePeriodSeconds: 3
      containers:
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
        volumeMounts:
        - name: log
          mountPath: /data
      volumes: 
      - name: log
        hostPath:
          path: "/data"
          type: DirectoryOrCreate

파드 드레인과 개수 조절시 노드에 상관없이 배포가 진행된다.

case2-5pod.png

하지만, 볼륨별로 데이터가 쌓이는 것이 다르다. 각 노드에 들어가서 로그를 확인해보면 찍히는 것이 다른 것을 확인할 수 있다.

case2-data-fail.png

고가용성이라 할 수 있지만 적재되어 있는 데이터가 달라 stateful 애플리케이션(ex. MySQL)을 운영하기엔 한계가 있다.

참고 책에서는 이러한 제약사항을 해결하기 위해서는 애플리케이션 단에서 다른 노드의 파드와 데이터를 동기화해서 해결할 수 있다고 한다.

동기화 방법을 찾아보니 NFS 볼륨(ex. AWS EFS)을 구성하여 HostPath 를 연결하거나, 볼륨간 rsync를 사용하라 나오지만, 성능(로컬SSD가 아님)이 떨어져 해결 방법은 아닌 것 같다.




Case3. 파드간 데이터 동기화 구성

그렇다면 성능 좋고, 고가용성도 보장되고, 데이터 동기화를 보장할 수 있는 Stateful 애플리케이션을 구성할 수 있을까?

쿠버네티스 공식문서 예제에서 이를 확인할 수 있는데 해당 예제를 구성해보고 테스트해보겠다. 해당 예제에서는 데이터베이스 리소스로 StatefulSet를 사용한다.

StatefulSet 리소스는 이름처럼 Stateful한 애플리케이션을 위해 만든 리소스이다. StatefulSet 리소스의 특징은 다음과 같다.

StatefulSet 리소스의 특징

  1. Pod 이름

    StatefulSet에 의해 생성된 파드들은 {Pod 이름}-{순번} 식으로 이름이 정해진다. 이는 클러스터 내부 환경에서 데이터베이스에 접근할 때 사용하기 위해서이다.

  2. 파드 순차적 배포

    Pod 생성시 모든 Pod 가 동시에 생성되지 않고 순서대로 하나씩 생성된다. 이는 데이터베이스에서 마스터 파드 → 슬레이브 파드로 기동해야 하는 조건등에서 유용하게 사용 될 수 있다.

  3. 파드별 볼륨 마운트

    일반적으로 PVC & PV에 중복적으로 Pod를 사용할 수 없다. 연결된 Pod가 존재하면 그 다음 파드들은 PVC를 얻지 못해 볼륨을 사용하지 못한다. 반면, Statefulset에서 PVC를 템플릿 형태로 정의하여 Pod마다 PVC, PV를 생성하여 파드별로 볼륨을 마운트할 수 있게 된다.


다시 돌아가서 쿠버네티스 공식문서의 예제는 클러스터에 MySQL 스테이트풀셋이 배포되고 각 레플리카에 순서대로 배포되는 예제이다. 중요하게 볼 점은 스테이트 풀셋의 매니페스트이다.

해당 StatefulSet 매니페스트는 3개의 replica를 가진 MySQL을 생성한다. init 컨테이너는 두개 배포되며, init-container는 MySQL init 설정을 수행하고, xtrabackup init-container는 MySQL 클러스터 복제를 위해 데이터를 클론하여 동기화를 진행한다. init 컨테이너 이후 MySQL 컨테이너는 데이터베이스 작업을 수행하며, xtrabackup 컨테이너는 클론 작업을 진행한다.

공식 문서의 예제를 그대로 배포하면 스토리지 클래스가 default로 EBS 볼륨(외부)에 연결된다. 이대로 진행하면 local-path-provisioner에서 배포한 로컬 볼륨에서 마운트되지 않는다.


로컬 볼륨에 마운트하기 위해서는 추가 작업이 필요하다.

local-path-provisioner 배포 파일 수정

local-path-provisioner 파드를 워크 노드에만 배포하도록 설정한다. 해당 설정을 통해 워크 노드에만 로컬 볼륨을 생성하고 파드에 연결할 수 있도록 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
vim local-path-storage.yaml
----------------------------
apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-path-provisioner
  namespace: local-path-storage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: local-path-provisioner
  template:
    metadata:
      labels:
        app: local-path-provisioner
    spec:
spec: # 아래 부분 추가 
    tolerations:
      - effect: NoSchedule
        key: node-role.kubernetes.io/node
        operator: Exists
  1. 스테이트 풀셋의 매니페스트 내 스토리지클래스 지정
1
2
3
4
5
6
7
8
9
volumeClaimTemplates:
- metadata:
    name: data
  spec:
    accessModes: ["ReadWriteOnce"]
    storageClassName: local-path # 로컬 볼륨 추가 
    resources:
      requests:
        storage: 10Gi
  1. 스테이트 풀셋의 매니페스트 replica count 를 워크노드(2) 개수 만큼 수정
1
2
3
serviceName: mysql
  replicas: 2 # 수정
  template:

배포 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
kubectl apply -f ./
----------------------------
configmap/mysql created
service/mysql created
service/mysql-read created
statefulset.apps/mysql created


kubectl get pods
----------------------------
NAME      READY   STATUS    RESTARTS   AGE
mysql-0   2/2     Running   0          31m
mysql-1   2/2     Running   0          31m

동기화 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 파드 0에서 Mysql Data 생성
kubectl exec -it pod/mysql-0 -- /bin/bash
---------------------------------
Defaulted container "mysql" out of: mysql, xtrabackup, init-mysql (init), clone-mysql (init)
---------------------------------

bash-4.2# mysql -u root -p
---------------------------------
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 140
Server version: 5.7.41-log MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
---------------------------------

mysql> create database testdb;
---------------------------------
Query OK, 1 row affected (0.01 sec)
---------------------------------

mysql> use testdb;
---------------------------------
Database changed
---------------------------------

mysql> create table test(name varchar(10), testdata varchar(50));
---------------------------------
Query OK, 0 rows affected (0.02 sec)
---------------------------------

mysql> insert into test values('han', 'mysql example test');
---------------------------------
Query OK, 1 row affected (0.01 sec)
---------------------------------

mysql> select * from test;
---------------------------------
+------+--------------------+
| name | testdata           |
+------+--------------------+
| han  | mysql example test |
+------+--------------------+
1 row in set (0.00 sec)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 파드 1에서 확인
kubectl exec -it pod/mysql-1 -- /bin/bash
---------------------------------
Defaulted container "mysql" out of: mysql, xtrabackup, init-mysql (init), clone-mysql (init)
---------------------------------

bash-4.2# mysql -u root -p
---------------------------------
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 306
Server version: 5.7.41 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
---------------------------------

mysql> use testdb
---------------------------------
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
---------------------------------

mysql> select * from test;
---------------------------------
+------+--------------------+
| name | testdata           |
+------+--------------------+
| han  | mysql example test |
+------+--------------------+
1 row in set (0.00 sec)

됐다! 로컬 볼륨 기반으로 Mysql 예제를 배포하였고 고가용성 구성을 위해 동기화까지 진행을 완료했다!




로컬 볼륨 백업과 복원

쿠버네티스에서는 HostPath 볼륨의 대한 백업과 복원 기능은 지원하지 않는다. 노드 별로 백업 스크립트를 작성하거나 써드파티 솔루션을 사용해야 한다. 이번 절에서는 local-path-provisioner 을 사용해서 볼륨을 프로비저닝한만큼, 해당 볼륨에 맞게 백업을 할 수 있는 써드파트 솔루션을 찾아보았다.

local-path-provisioner 깃허브 이슈를 찾아보니 Velero 솔루션을 이용해서 백업을 할 수 있다고 하여 테스트를 진행해보고자 한다.

Velero? 는 쿠버네티스 클러스터의 리소스와 퍼시스턴트 볼륨을 백업하고 복원하는 데 사용되는 오픈 소스 툴이다.

Velero 을 사용하기전 local-path-provisioner 볼륨 타입을 Local로 수정해야 한다. Hostpath 볼륨을 지원하지 않으나 Local 볼륨은 Restic 과 연계하여 백업을 지원하기 때문이다. (공식문서)

verelo-docs.png

이는 Local 볼륨이 쿠버네티스의 자원으로 관리되며, 스토리지 클래스와 퍼시스턴트 볼륨 클레임(PVC)을 사용할 수 있기 때문으로 보인다.


local-path-provisioner 사전 작업

Local 볼륨을 수정하기 위해서는 local-path-provisioner 파드의 수정 작업이 필요하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
volumeClaimTemplates:
  - metadata:
      name: data
      annotations:
        volumeType: local # 추가 
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: local-path
      resources:
        requests:
          storage: 10Gi

로컬 볼륨 확인

1
2
3
4
5
6
kubectl get pv <pv-name> -o yaml
---------------------------------
spec:
  local: # local or HostPath 볼륨
    path: /mnt/local-storage/ssd/vol1
  ...

Velero 설치

필자는 Velero 백업 버킷을 AWS S3 설정하여 설치를 진행하였다.

설치는 S3 버켓 생성 및 설정 / Veleo CLI 로 나뉜다.

  • S3 버켓 지정 및 IAM 설정

    Velero 에서 S3 버킷을 접근하기 위한 IAM USER ID와 KEY 생성

    1
    
    aws s3 mb s3://<bucket-name> --region ap-northeast-2
    

    IAM Policy 생성

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    
    # 버킷 변수 설정 
    export BUCKET=<bucket-name>
    
    # IAM Policy 생성 
    cat > velero-policy.json <<EOF
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:DescribeVolumes",
                    "ec2:DescribeSnapshots",
                    "ec2:CreateTags",
                    "ec2:CreateVolume",
                    "ec2:CreateSnapshot",
                    "ec2:DeleteSnapshot"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject",
                    "s3:DeleteObject",
                    "s3:PutObject",
                    "s3:AbortMultipartUpload",
                    "s3:ListMultipartUploadParts"
                ],
                "Resource": [
                    "arn:aws:s3:::${BUCKET}/*"  
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::${BUCKET}" 
                ]
            }
        ]
    }
    EOF
    
    # IAM Policy Attach
    aws iam put-user-policy \
      --user-name velero \
      --policy-name velero \
      --policy-document file://velero-policy.json
    
    # IAM user 정보 가져오기 
    aws iam create-access-key --user-name velero
    ---------------------------------
    {
        "AccessKey": {
            "UserName": "velero",
            "AccessKeyId": "{ID}", # 밑의 credentials-velero ID에 저장
            "Status": "Active",
            "SecretAccessKey": "{KEY}", # 밑의 credentials-velero KEY에 저장 
            "CreateDate": "2023-03-16T04:31:23+00:00"
        }
    }
    
    # credentials-velero 생성 및 IAM 정보 저장 
    cat << EOF > credentials-velero
    [default]
    aws_access_key_id=<AWS_ACCESS_KEY_ID>
    aws_secret_access_key=<AWS_SECRET_ACCESS_KEY>
    EOF
    
  • Velero CLI 설치 후 서버 설치

    restic은 버전 velero 1.10(최신버전) 이상에서 더 이상 지원되지 않는다. 버전을 1.9.6으로 맞춰서 다운받아야 한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    # arch 확인 
    uname -m
    ---------------------------------
    x86_64
    
    # velero CLI 설치 
    wget https://github.com/vmware-tanzu/velero/releases/download/v1.9.6/velero-v1.9.6-linux-amd64.tar.gz
    tar xzvf velero-v1.9.6-linux-amd64.tar.gz
    cp velero-v1.9.6-linux-amd64/velero ~/bin
    
    # CLI 확인 
    velero
    ---------------------------------
    Velero is a tool for managing disaster recovery, specifically for Kubernetes
    cluster resources. It provides a simple, configurable, and operationally robust
    way to back up your application state and associated data.
    
    If you're familiar with kubectl, Velero supports a similar model, allowing you to
    execute commands such as 'velero get backup' and 'velero create schedule'. The same
    operations can also be performed as 'velero backup get' and 'velero schedule create'.
     ...
    
  • Velero 설치

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    export BUCKET=<bucket-name>
    export REGION=ap-northeast-2
    
    velero install \
        --provider aws \
        --bucket $BUCKET \
        --secret-file ./credentials-velero \
        --backup-location-config region=$REGION \
        --use-volume-snapshots=false \
        --plugins velero/velero-plugin-for-aws:v1.3.0 \
        --use-restic
    ---------------------------------
     ...
    Deployment/velero: created
    DaemonSet/restic: attempting to create resource
    DaemonSet/restic: attempting to create resource client
    DaemonSet/restic: created
    Velero is installed! ⛵ Use 'kubectl logs deployment/velero -n velero' to view the status.
    
    # Velero 확인
    kubectl get all -n velero 
    NAME                          READY   STATUS    RESTARTS   AGE
    pod/restic-f5ngz              1/1     Running   0          38s
    pod/restic-x9sk9              1/1     Running   0          37s
    pod/velero-5f6657d4c8-jttxv   1/1     Running   0          38s
    
    NAME                    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
    daemonset.apps/restic   2         2         2       2            2           <none>          38s
    
    NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/velero   1/1     1            1           38s
    
    NAME                                DESIRED   CURRENT   READY   AGE
    replicaset.apps/velero-5f6657d4c8   1         1         1       38s
    

Velero 백업

Velero는 Restic을 사용하여 PV 볼륨에 대해 백업하는 방법에 두 가지 접근 방식을 지원한다. (공식문서)

  • 옵트인 접근 방식(default): Restic을 사용하여 백업할 볼륨이 포함된 모든 포드에 annotation을 달아야 한다.
  • 옵트아웃 접근 방식: 모든 포드 볼륨이 Restic을 사용하여 백업되고 백업되지 않아야 하는 볼륨을 옵트아웃할 수 있는 기능이 있다.

이번 절에서는 옵트인 접근 방식을 택할 것이고 Case3 의 Mysql 볼륨을 백업할 예정이다. 백업할 볼륨에 대해 backup.velero.io/backup-volumesannotation 달고 백업을 진행하겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 주석 추가 pod에 볼륨 정보를 추가
kubectl annotate pod/mysql-0 backup.velero.io/backup-volumes=data

# 백업 
velero backup create  mysql --include-namespaces default --wait
---------------------------------
Backup request "mysql" submitted successfully.
Waiting for backup to complete. You may safely press ctrl-c to stop waiting - your backup will continue in the background.
..................
Backup completed with status: Completed. You may check for more information using the commands `velero backup describe mysql` and `velero backup logs mysql`.

# 백업 목록 확인
velero get backup
---------------------------------
NAME    STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
mysql   Completed   0        0          2023-03-16 14:22:39 +0900 KST   29d       default            <none>

S3 버킷 조회할 수 있다.

S3 조회시, backups(쿠버네티스 리소스 저장 경로) 와 restic(PV 볼륨 데이터 저장 경로)에 데이터를 확인할 수 있다.

velero-backup.png

velero-restic.png


Velero 복원

mysql 배포 파일과 PV를 지우고 복원을 진행하겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#mysql 지우기
kubectl delete -f ./
kubectl delete pvc/<PVC 볼륨>

#velero 복원 
velero restore create --from-backup mysql --wait
---------------------------------
Restore request "mysql-20230316155542" submitted successfully.
Waiting for restore to complete. You may safely press ctrl-c to stop waiting - your restore will continue in the background.
...........
Restore completed with status: Completed. You may check for more information using the commands `velero restore describe mysql-20230316155542` and `velero restore logs mysql-20230316155542`.

# 쿠버네티스 리소스 복원 확인
kubectl get all
---------------------------------
NAME          READY   STATUS                  RESTARTS      AGE
pod/mysql-0   2/2     Running                 0             39s
pod/mysql-1   0/2     Init:CrashLoopBackOff   2 (18s ago)   39s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/kubernetes   ClusterIP   100.64.0.1      <none>        443/TCP    4h39m
service/mysql        ClusterIP   None            <none>        3306/TCP   39s
service/mysql-read   ClusterIP   100.69.52.194   <none>        3306/TCP   39s

kubectl get pv 
---------------------------------
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
pvc-601b919a-cf20-4478-9f28-10d541c66844   10Gi       RWO            Delete           Bound    default/data-mysql-0   local-path              71s
pvc-b8a766a6-411f-47df-a548-d6b0ee091ea1   10Gi       RWO            Delete           Bound    default/data-mysql-1   local-path              70s

# Mysql data 확인
kubectl exec -it pod/mysql-0 -- /bin/bash
---------------------------------
Defaulted container "mysql" out of: mysql, xtrabackup, restic-wait (init), init-mysql (init), clone-mysql (init)

bash-4.2# mysql -u root -p
---------------------------------
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 73
Server version: 5.7.41-log MySQL Community Server (GPL)

mysql> use testdb;
---------------------------------
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
---------------------------------
mysql>  select * from test;
+------+--------------------+
| name | testdata           |
+------+--------------------+
| han  | mysql example test |
+------+--------------------+
1 row in set (0.01 sec)

데이터가 그대로 보존되어 있다!


Velero 스케쥴 백업

크론탭처럼 백업도 Velero가 가능하다. 다음은 5분마다 백업을 진행하는 예제이다.

백업된 오브젝트별 데이터 변화를 위해 데이터베이스의 데이터를 수정 후 복원을 해보겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
velero schedule create mysql-crontab --include-namespaces default --schedule="*/5 * * * *" 

velero get backup
---------------------------------
NAME                           STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
mysql                          Completed   0        0          2023-03-16 15:51:44 +0900 KST   29d       default            <none>
mysql-crontab-20230316072517   Completed   0        0          2023-03-16 16:25:17 +0900 KST   29d       default            <none>
mysql-crontab-20230316072017   Completed   0        0          2023-03-16 16:20:17 +0900 KST   29d       default            <none>

#DB 접속 후 데이터 삭제
select * from test;
+------+---------------------+
| name | testdata            |
+------+---------------------+
| han  | mysql example test  |
| han  | mysql example test2 |
| han  | mysql example test3 |
+------+---------------------+
3 rows in set (0.00 sec)

mysql> delete from test;
Query OK, 3 rows affected (0.00 sec)

mysql> select * from test;
Empty set (0.00 sec)

#velero 복원 
velero restore create --from-backup mysql-crontab-20230316072017  --wait
---------------------------------
Restore request "mysql-crontab-20230316072017-20230316163030" submitted successfully.
Waiting for restore to complete. You may safely press ctrl-c to stop waiting - your restore will continue in the background.

Restore completed with status: Completed. You may check for more information using the commands `velero restore describe mysql-crontab-20230316072017-20230316163030` and `velero restore logs mysql-crontab-20230316072017-20230316163030`.

# Mysql DATA 확인 
mysql> select * from test;
Empty set (0.00 sec)

Mysql DATA 확인 의 결과가 예상과 다르다. 백업 진행 이후 백업 데이터인 행3개가 있어야 하나, 최신 데이터가 조회된다.

이는 Mysql 파드가 2개 있어 파드간 데이터 무결성이 보장되어 백업 데이터 파일을 옮긴다 한들 수정이 안되기 때문이다. 따라서 백업본의 결과를 얻기 위해서는 PV 볼륨과 Mysql 리소스를 지우고 다시 복원을 해야한다.


Velero 클러스터 마이그레이션

클러스터간 마이그레이션 방법으로 Velero를 활용할 수 있다. 공식문서를 참고하여 테스트를 진행해보겠다.

진행 전 Velero 에서 클러스터 마이그레이션시 제약사항이 있으니 확인이 필요하다.

  • Velero는 기본적으로 클라우드 공급자 간에 PV 스냅샷의 마이그레이션을 지원하지 않는다. 이를 위한 방안으로 restic을 이용하여 파일시스템 레벨의 마이그레이션을 진행해야 한다.
  • Velero는 백업이 수행된 위치보다 낮은 Kubernetes 버전이 있는 클러스터로의 복원을 지원하지 않는다.
  • 동일한 버전의 Kubernetes를 실행하지 않는 클러스터 간에 워크로드를 마이그레이션하는 것이 가능할 수 있지만 마이그레이션 전에 각 사용자 정의 리소스에 대한 클러스터 간 API 그룹의 호환성을 포함하여 몇 가지 요인을 고려해야 한다.
  • AWS 및 Azure용 Velero 플러그인은 리전 간 데이터 마이그레이션을 지원하지 않는다. 이를 위한 방안으로 restic을 사용해야 한다.

우리는 restic을 사용하니 제약사항에 자유롭다. 클러스터 마이그레이션의 예에 대한 일환으로 클러스터를 재 구축하여 앞서 생성한 Mysql 애플리케이션을 복원해보겠다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# velero 설치
export BUCKET=hanhorang-velero-s3
export REGION=ap-northeast-2

velero install \
    --provider aws \
    --bucket $BUCKET \
    --secret-file ./credentials-velero \
    --backup-location-config region=$REGION \
    --use-volume-snapshots=false \
    --plugins velero/velero-plugin-for-aws:v1.3.0 \
    --use-restic
---------------------------------

velero get backup
---------------------------------
NAME    STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
mysql   Completed   0        0          2023-03-17 00:31:21 +0900 KST   29d       default            <none>

velero 백업 개체가 그대로 있다. 복원 과정은 앞 과정과 동일하기에 생략하였다. 클러스터를 재구축해도 Velero 백업 객체가 남아있는 이유가 무엇일까?

Velero 리소스는 오브젝트 스토리지의 백업 파일과 동기화되기 때문이다. 설치 과정에서 삭제한 클러스터와 새로운 클러스터의 Velero 버킷이 동일하므로 백업 객체가 그대로 있음을 확인하였다. 클러스터1과 클러스터2가 병행 운영시에도 버킷 데이터에 따라 Velero 리소스가 동기화가 이루어진다는데 기본 동기화 간격이 1분으로 이 부분을 확인하여 마이그레이션을 진행하면 될 것 같다.




로컬 볼륨 모니터링

PV 볼륨 성능 확인할 수 있는 krew df-pv 도구가 있으나, HostPath 볼륨은 인스토어스토어라서 확인되지 않는다. 하지만 Local 볼륨은 확인이 가능하다.

1
2
3
4
5
kubectl krew df-pv && kubectl df-pv 
---------------------------------
PV NAME                                   PVC NAME      NAMESPACE  NODE NAME            POD NAME  VOLUME MOUNT NAME  SIZE   USED  AVAILABLE  %USED  IUSED   IFREE     %IUSED 
 pvc-678e7407-9f76-4fd1-a9ad-8c2581b8df36  data-mysql-0  default    i-0314088c74eee3276  mysql-0   data               123Gi  4Gi   119Gi      3.68   119172  16395900  0.72   
 pvc-d2c3cbcb-13ec-46de-81e9-1178d25dd4ad  data-mysql-1  default    i-0ab66ac834dc8710d  mysql-1   data               123Gi  4Gi   119Gi      3.74   121203  16393869  0.73



성능 측정

로컬 스토리지의 성능 측정 방법으로 iostat 명령어와 krew 툴인 kubestr을 사용하여 성능을 측정하겠다.

kubestr

스토리지 IOPS 측정 툴이다. 스토리지 사용에 따른 검증용으로 사용하기 좋은 툴인 것 같다. 예제도 많으니 링크를 통해 확인하자. 이번 예제에서는 실습 참고용 책에서 제공해주신 예제 스크립트를 통해 성능을 측정할 것이다.

fio-read.fio

fio를 사용하여 4KB 블록 크기를 가지는 랜덤 읽기 및 쓰기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[global]
ioengine=libaio
direct=1
bs=4k
runtime=120
time_based=1
iodepth=16
numjobs=4
# numjobs=16
size=1g
group_reporting
rw=randrw
rwmixread=100
rwmixwrite=0
[read]
  • ioengine=libaio : Asynchronous I/O를 수행하기 위해 libaio 라이브러리를 사용합니다.
  • direct=1 : Direct I/O를 사용합니다.
  • bs=4k : I/O 요청에 사용되는 블록 크기는 4KB입니다.
  • runtime=120 : 120초 동안 작업을 실행합니다.
  • time_based=1 : 시간 기반으로 작업을 수행합니다.
  • iodepth=16 : 각 작업에 대한 I/O 요청 수를 16개로 설정합니다.
  • numjobs=4 : 4개의 작업을 수행합니다.
  • size=1g : 각 작업에 대한 데이터 크기는 1GB입니다.
  • group_reporting : 모든 작업 결과를 통합하여 보고합니다.
  • rw=randrw : 랜덤 읽기 및 쓰기 작업을 수행합니다.
  • rwmixread=100 : 작업 중 읽기 작업의 비율은 100%입니다.
  • rwmixwrite=0 : 작업 중 쓰기 작업의 비율은 0%입니다.

fio-write.fio

루트 디렉토리에서 4KB 블록 크기로 16개의 job이 16개의 i/o depth로 실행되며, 실행 시간이 120초인 1GB 파일에 대해 100% 쓰기 랜덤 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[global]
ioengine=libaio
numjobs=16
iodepth=16
direct=1
bs=4k
runtime=120
time_based=1
size=1g
group_reporting
rw=randrw
rwmixread=0
rwmixwrite=100
directory=/
[read]
  • rwmixwrite=100: 100% 쓰기 테스트를 수행합니다.
  • directory=/: 테스트할 디렉토리를 루트 디렉토리로 설정합니다.

성능 측정

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
kubestr fio -f fio-write.fio -s local-path --size 10G
---------------------------------
PVC created kubestr-fio-pvc-hp69m
Pod created kubestr-fio-pod-fcvcp
Running FIO test (fio-write.fio) on StorageClass (local-path) with a PVC of Size (10G)
Elapsed time- 4m11.664545514s
FIO test results:
  
FIO version - fio-3.30
Global options - ioengine=libaio verify= direct=1 gtod_reduce=

JobName: 
  blocksize= filesize= iodepth= rw=
write:
  IOPS=3023.577881 BW(KiB/s)=12094
  iops: min=992 max=8640 avg=3023.464355
  bw(KiB/s): min=3968 max=34564 avg=12093.908203

Disk stats (read/write):
  nvme0n1: ios=0/362587 merge=0/173 ticks=0/6330627 in_queue=6330627, util=99.954132%

fio-write 실행 결과, 쓰기 평균 iops가 3034인 것을 확인할 수 있다.


Q. 테스트가 안될 경우, PV 상태 Pending?

해당 경우는 PVC 요청에 맞는 볼륨의 PV가 없을때 발생한다. PVC 요청에 맞는 볼륨이 있는지 또는 Local-path-provisioner 설정을 확인하자. 필자의 경우 Local-path-provisioner 설정으로 Pending 이 발생했다.

1
2
3
4
kubectl get pvc -A
---------------------------------
NAMESPACE   NAME                    STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
default     kubestr-fio-pvc-dl7vx   Pending                                      local-path     4m34

노드 볼륨 IO 성능 측정

iostat 명령어를 통해 실시간으로 스토리지의 성능과 사용량에 관한 정보를 확인할 수 있다. 클러스터 운영시 스토리지 트러블슈팅의 일환으로 사용하자.

아래 예제는 fio-write 스크립트 실행 중 iostat 를 실행하여 스토리지 정보를 확인한 예제이다.

1
2
3
4
5
6
7
# iostat 패키지 설치
sudo apt install -y sysstat

iostat -xmdz 1 -p nvme2n1
---------------------------------
Device            r/s     rMB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wMB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dMB/s   drqm/s  %drqm d_await dareq-sz  aqu-sz  %util
nvme2n1          0.00      0.00     0.00   0.00    0.00     0.00   24.00      0.10     2.00   7.69    0.75     4.33    0.00      0.00     0.00   0.00    0.00     0.00    0.02   4.00
  • w/s: 초당 쓰기 요청 수
  • wMB/s: 초당 쓴 데이터 양 (메가바이트/초)
  • wrqm/s: 초당 쓰기 요청 큐에 들어간 요청 수
  • %wrqm: 쓰기 요청 큐에 들어간 요청 비율
  • w_await: 쓰기 요청 대기 시간, 드라이버 요청 대기열에서 기다린 시간과 장치의 I/O 응답시간을 모두 포함한다. (밀리초)
  • wareq-sz: 평균 쓰기 요청 크기 (섹터)
  • aqu-sz: 요청 대기열의 평균 길이
  • %util: 디스크 사용률 (0 ~ 100%)



로컬 스토리지 QnA

Q1. AWS 인스토어 스토어에 대한 볼륨 스토리지 조절이 가능한가?

인스턴스 스토어는 EC2 인스턴스의 로컬 디스크를 사용하는 것이기 때문에 크기 조정이 불가능하다. 인스턴스 스토어를 사용하는 EC2 인스턴스를 변경하거나 새로운 인스턴스를 시작하여 크기를 조정해야 한다. 일반적으로 DB(Mysql) 볼륨 사용량으로 10G~100G을 설정한다고 하지만, 애플리케이션 규모와 기간에 따라 사용량 예측이 힘들다. 필요시 백업을 통해 인스토어 스토어 볼륨을 변경할 수 있도록 하자. EC2 인스턴스에 따른 볼륨(SSD) 는 링크에서 확인이 가능하다.


Q2. hostpath 볼륨을 여러개의 파드가 동시에 사용할 수 있을까?

노드의 파일 시스템은 여러 개의 프로세스가 동시에 접근할 수 있는 공유 리소스이기 때문에 여러 개의 파드가 하나의 hostpath를 사용할 수 있다. 하지만, 데이터 손상이나 권한 오류가 발생할 수 있다. 하나의 파드가 파일을 쓴 후에 다른 파드가 동일한 파일을 읽을 경우나, 여러 개의 파드가 동시에 동일한 파일을 쓰는 경우가 있기 때문이다. 이를 위해 적절한 동기화 및 락 메커니즘을 구현이 필요하다.

테스트로 스터디에서 공유해주신 localpath-fail.yaml 를 수정해서 로그를 확인해봤다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: apps/v1
kind: Deployment
metadata:
  name: date-pod
  labels:
    app: date
spec:
  replicas: 1
  selector:
    matchLabels:
      app: date
  template:
    metadata:
      labels:
        app: date
    spec:
      terminationGracePeriodSeconds: 3
      containers:
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 0.01; done"] # 0.01 로 수정 
        volumeMounts:
        - name: pod-persistent-volume
          mountPath: /data
      volumes: 
      - name: pod-persistent-volume
        persistentVolumeClaim:
          claimName: localpath-claim
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 파드 10개로 증가후 테스트
kubectl scale deployment date-pod --replicas=10
--------------------------------------

# 로그 확인 
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:05 | wc -l
355
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:05 | wc -l
355
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:06 | wc -l
457
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:07 | wc -l
511
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:08 | wc -l
513
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:09 | wc -l
530
ubuntu@i-0993bfc0c3f4818c8:/data/local-path/pvc-9676527c-e860-4c13-acc3-cda3bd1a5f1d_default_localpath-claim$ cat out.txt | grep 03:10 | wc -l
506

0.01초 x 10 개 파드로 1초당 약 1000개의 결과가 나와야 하지만 350~530 개의 결과가 나오는 것을 확인하였다. 앞서 kubestr 측정에서 쓰기 IOPS를 측정하여 약 3000이 나왔음에도 턱없이 부족한 것을 알 수 있다.


Q3. Velero 의 백업 용량은 몇 인가?

사용 버킷에 따라 달라진다. S3 버킷 기준으로는 해당 버킷의 용량에 따라가는데 최대 5테라까지 지원이 가능하다.