[doik2] 3주차 - Cloud Native PostgreSQL 오퍼레이터
Published: 2023-10-29 09:01:41

# Overview
CloudNet@ 에서 운영하고 있는 DOIK (Database Operator In Kubernetes) 3주차 스터디에서 PostgreSQL을 쿠버네티스에서 운영하는 실습을 진행하고 학습한 내용을 정리하였습니다.

## CloudNativePG 소개 및 설치

https://cloudnative-pg.io/
CloudNativePG는 PostgreSQL의 기본 스트리밍 레플리케이션 기능을 사용하는 Primary/Standby 아키텍쳐를 갖춘 고가용성 PostgreSQL 데이터베이스 클러스터의 전체 수명 주기를 관장하는 Kubernetes 오퍼레이터이다.


## CloudNativePG 설치 및 실습

**1. OLM 설치 (Operator Lifecycle Manager) 설치**
```
curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.25.0/install.sh | bash -s v0.25.0
```

다음 커맨드를 통해 어떤 리소스가 생성되어 있는지 확인한다.

`kubectl get ns`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/16bzs0ltqorxr5npj8xt.png)
OLM 설치 이후 olm, operators 라는 두개의 네임스페이스가 생성되었다.


`kubectl get all -n olm`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j6ncsn6waav2utnp3n6s.png)

`kubectl get all -n operators`
현재 OLM 만 설치된 상태이므로 operators 네임스페이스에는 리소스가 없는 상태이다.


**2. PostgreSQL 오퍼레이터 설치**

아래의 커맨드를 통해 PostgreSQL 오퍼레이터인 CloudNativePG를 설치한다.
```
curl -s -O https://operatorhub.io/install/cloudnative-pg.yaml
kubectl create -f cloudnative-pg.yaml
```

`kubectl get all -n operators`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/739imcb7ene02r8p0i16.png)
CloudNativePG 배포이후 operators 네임스페이스를 확인해보면 사진처럼 cnpg 컨트롤러 매니저가 설치된것을 확인할 수 있다.


**3. 실습! PostgreSQL 클러스터 배포**
```
cat <<EOT> mycluster1.yaml
# Example of PostgreSQL cluster
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: mycluster
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:15.3
  instances: 3  
  storage:
    size: 3Gi
  postgresql:
    parameters:
      max_worker_processes: "40"
      timezone: "Asia/Seoul"
    pg_hba:
      - host all postgres all trust
  primaryUpdateStrategy: unsupervised 
  enableSuperuserAccess: true
  bootstrap:
    initdb:
      database: app
      encoding: UTF8
      localeCType: C
      localeCollate: C
      owner: app

  monitoring:
    enablePodMonitor: true
EOT
```

_c.f. spec 에 대한 간략 설명_
- `pg_hba` : 파드에서 사용하는 pg_hba.conf 를 생성하는데 사용되는 PostgreSQL 호스트 기반 인증 규칙의 목록이다.
- `primaryUpdateStrategy` : `unsupervised` 설정되면 롤링 업데이트 프로세스가 쿠버네티스에 의해 관리되며 완전히 자동화된다. 레플리카가 업그레이드되면, 선택한 primaryUpdateMethod 작업이 프라이머리에서 시작된다.



_c.f. 설치 순서 확인_
kubectl apply -f mycluster1.yaml && kubectl get pod -w
```
cluster.postgresql.cnpg.io/mycluster created
NAME                       READY   STATUS    RESTARTS   AGE
mycluster-1-initdb-9pfpg   0/1     Pending   0          1s
mycluster-1-initdb-9pfpg   0/1     Init:0/1   0          5s
mycluster-1-initdb-9pfpg   0/1     PodInitializing   0          9s
mycluster-1-initdb-9pfpg   1/1     Running           0          55s
mycluster-1-initdb-9pfpg   0/1     Completed         0          62s
mycluster-1                0/1     Pending           0          0s
mycluster-1                0/1     Init:0/1          0          5s
mycluster-1                0/1     PodInitializing   0          6s
mycluster-1                1/1     Running           0          15s
mycluster-2-join-kpblh     0/1     Pending           0          4s
mycluster-2-join-kpblh     0/1     Init:0/1          0          16s
mycluster-2-join-kpblh     0/1     PodInitializing   0          17s
mycluster-2-join-kpblh     1/1     Running           0          29s
mycluster-2-join-kpblh     0/1     Completed         0          38s
mycluster-2                0/1     Pending           0          0s
mycluster-2                0/1     Init:0/1          0          11s
mycluster-2                0/1     PodInitializing   0          12s
mycluster-2                1/1     Running           0          21s
mycluster-3-join-hxh4v     0/1     Pending           0          4s
mycluster-3-join-hxh4v     0/1     Init:0/1          0          4s
mycluster-3-join-hxh4v     0/1     PodInitializing   0          16s
mycluster-3-join-hxh4v     1/1     Running           0          44s
mycluster-3-join-hxh4v     0/1     Completed         0          56s
mycluster-3                0/1     Pending           0          0s
mycluster-3                0/1     Init:0/1          0          3s
mycluster-3                0/1     PodInitializing   0          4s
mycluster-3                1/1     Running           0          14s
mycluster-1-initdb-9pfpg   0/1     Terminating       0          3m28s
mycluster-2-join-kpblh     0/1     Terminating       0          2m9s
mycluster-3-join-hxh4v     0/1     Terminating       0          70s
```
1. initdb Job 리소스 생성
2. Primary 서버 생성
3. Secondary 서버가 Primary 서버에 Join 하는 Job 리소스 생성



_c.f. 아래 커맨드를 통해 생성된 리소스들을 확인해보자_
```
kubectl get pod,pvc,pv,svc,ep
kubectl df-pv
kubectl describe pod -l cnpg.io/cluster=mycluster # TCP 9187 메트릭 제공
kubectl get pod -l cnpg.io/cluster=mycluster -owide
curl -s <파드IP>:9187/metrics
curl -s 192.168.1.84:9187/metrics
```

_c.f. 그라파나 대시보드 설치하기_
```
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/docs/src/samples/monitoring/grafana-configmap.yaml
```

설치된 대시보드를 확인.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fipyqrfelrb249yo40rj.png)


_c.f. krew 커맨드를 통해 CloudNativePG 오퍼레이터 관리툴 설치하기_

CRD를 통해 쿠버네티스 내에 설치되어있는 클러스터를 아래 커맨드를 통해 확인할 수 있다.
`kubectl get cluster`
```
NAME        AGE     INSTANCES   READY   STATUS                     PRIMARY
mycluster   7m30s   3           3       Cluster in healthy state   mycluster-1
```

cnpg 관리 툴 설치하기
```
kubectl krew install cnpg
```

아래 커맨드를 통해 설치된 pg 클러스터의 상태를 쉽게 조회할 수 있다.
`kubectl cnpg status mycluster`
```
Cluster Summary
Name:                mycluster
Namespace:           default
System ID:           7295339077458657299
PostgreSQL Image:    ghcr.io/cloudnative-pg/postgresql:15.3
Primary instance:    mycluster-1
Primary start time:  2023-10-29 11:21:52 +0000 UTC (uptime 7m35s)
Status:              Cluster in healthy state
Instances:           3
Ready instances:     3
Current Write LSN:   0/7000060 (Timeline: 1 - WAL File: 000000010000000000000007)

Certificates Status
Certificate Name       Expiration Date                Days Left Until Expiration
----------------       ---------------                --------------------------
mycluster-ca           2024-01-27 11:16:11 +0000 UTC  89.99
mycluster-replication  2024-01-27 11:16:11 +0000 UTC  89.99
mycluster-server       2024-01-27 11:16:11 +0000 UTC  89.99

Continuous Backup status
Not configured

Streaming Replication status
Replication Slots Enabled
Name         Sent LSN   Write LSN  Flush LSN  Replay LSN  Write Lag  Flush Lag  Replay Lag  State      Sync State  Sync Priority  Replication Slot
----         --------   ---------  ---------  ----------  ---------  ---------  ----------  -----      ----------  -------------  ----------------
mycluster-2  0/7000060  0/7000060  0/7000060  0/7000060   00:00:00   00:00:00   00:00:00    streaming  async       0              active
mycluster-3  0/7000060  0/7000060  0/7000060  0/7000060   00:00:00   00:00:00   00:00:00    streaming  async       0              active

Unmanaged Replication Slot Status
No unmanaged replication slots found

Instances status
Name         Database Size  Current LSN  Replication role  Status  QoS         Manager Version  Node
----         -------------  -----------  ----------------  ------  ---         ---------------  ----
mycluster-1  29 MB          0/7000060    Primary           OK      BestEffort  1.21.0           ip-192-168-3-223.ap-northeast-2.compute.internal
mycluster-2  29 MB          0/7000060    Standby (async)   OK      BestEffort  1.21.0           ip-192-168-1-9.ap-northeast-2.compute.internal
mycluster-3  29 MB          0/7000060    Standby (async)   OK      BestEffort  1.21.0           ip-192-168-2-128.ap-northeast-2.compute.internal
```

**4. 실습! 장애 테스트 - Primary Pod 삭제**

장애 테스트 준비 사항

현재 Primary 가 어떤 Pod 인지 찾는다.
`kubectl cnpg status mycluster`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/et83me0w3m9tre2ctwo3.png)

```
# POD ip 환경 변수 셋팅
POD1=$(kubectl get pod mycluster-1 -o jsonpath={.status.podIP})
POD2=$(kubectl get pod mycluster-2 -o jsonpath={.status.podIP})
POD3=$(kubectl get pod mycluster-3 -o jsonpath={.status.podIP})
```

아래 커맨드를 통해 대량의 데이터를 삽입 한다. 
`for ((i=301; i<=10000; i++)); do kubectl exec -it myclient2 -- psql -U postgres -h mycluster-rw -p 5432 -d test -c "INSERT INTO t1 VALUES ($i, 'Luis$i');";echo; done`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orjlwu7cd6rk9ly5knb2.png)

대량의 데이터 삽입 중 Primary Pod 를 삭제한다.
`kubectl delete pvc/mycluster-1 pod/mycluster-1`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfez2lop8f2mx6rthiwt.png)


데이터 삽입 커맨드 중 중간에 순단 현상이 있으나 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yls3qp8zuah6dl15yb2m.png)

클러스터의 Primary Pod 는 mycluster2 로 즉시 변경되는 것을 확인할 수 있다.
`kubectl cnpg status mycluster`
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inr3ab9vat2fe8pyparv.png)


**5. 실습! 장애 테스트 - Primary Pod 가 위치한 Node 삭제**

클러스터의 Primary Pod 가 배포되어 있는 Node 를 찾는다.

아래 명령을 순차적으로 입력한다.
```
kubectl cnpg status mycluster
kubectl get nodes
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mefenwe32nkfs2cknxze.png)

터미널 한개에 데이터 삽입 커맨드를 입력한다.
```
for ((i=10001; i<=20000; i++)); do kubectl exec -it myclient2 -- psql -U postgres -h mycluster-rw -p 5432 -d test -c "INSERT INTO t1 VALUES ($i, 'Luis$i');";echo; done
```

또 다른 터미널에는 데이터 조회 커맨드를 입력한다.
```
while true; do kubectl exec -it myclient2 -- psql -U postgres -h mycluster-ro -p 5432 -d test -c "SELECT COUNT(*) FROM t1"; date;sleep 1; done
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlda7o0z34vgetxvoisb.png)


아래 명령을 통해 Primary Pod 가 배포되어 있는 Node 를 Drain 시킨다.
```
kubectl drain $NODE --delete-emptydir-data --force --ignore-daemonsets && kubectl get node -w
```

Primary Pod 가 Pending 상태로 변경되었다.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7la2gfdvhbo1k86s5ni.png)

`kubectl cnpg status mycluster` 커맨드로 클러스터 상태를 조회해보면
mycluster-3 이라는 pod 가 Primary로 변경된 것을 확인할 수 있다.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wq1nm7kjgle14ykyzjrp.png)


**4. 실습! cnpg 플러그인 커맨드**
krew 플러그인인 cnpg 는 단순히 상태를 조회하는 것 이상으로 다양한 커맨드를 지원한다.

**`status`**
클러스터의 현재 상태를 조회할 수 있음.

**`promote`**
클러스터의 pod를 primary 로 승격할 수 있음.

현재 primary pod 는 mycluster-3
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1wx05al9vwvzm6mik56.png)

아래 커맨드를 입력하여 mycluster-2 pod 를 primary 로 승격 시도한다.
```
kubectl cnpg promote mycluster mycluster-2
```

그 결과 mycluster-2 가 primary pod 로 승격되었음을 확인할 수 있다.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak9vvwy369tcncauuexh.png)


**`report`**
status 명령을 통해 현재 클러스터 상태를 단편적이고, 즉시 얻을 수도 있지만, 
배포 상태, 이벤트, webhook, logs 등의 클러스터에 대한 여러 방면의 정보 또는 특정 시간대에 대한 상세한 클러스터 상태 정보를 획득하고자 할때 사용된다.

주로 아래와 같은 정보를 얻을 수 있다.
- **cluster resources**: 클러스터 정보
- **cluster pods**: 클러스터에서 사용되는 Pod 정보
- **cluster jobs**: 클러스터에서 사용된 jobs 정보
- **events**: 클러스터가 배포된 네임스페이스의 이벤트 정보
- **pod logs**: 클러스터 pod의 log
- **job logs**: 클러스터 jobs pod의 log

아래와 같이 입력하면 'mycluster' 클러스터에 대한 자세한 정보를 모아서 zip 파일로 말아준다.
```
kubectl cnpg report cluster mycluster
```

**`logs`**
PostgreSQL 클러스터의 로그 혹은 클러스터 pod의 로그를 확인할 수 있는 커맨드.

**`pgbench`**
데이터베이스 클러스터의 성능을 밴치마킹 할 수 있는 커맨드

**`fio`**
데이터베이스 클러스터가 사용하는 스토리지 클래스를 밴치마킹할 수 있는 커맨드

**`psql`**
postgresql 클라이언트를 실행하는 커맨드, 실제 Pod 에서 실행하는 것처럼 cnpg 플러그인을 통해 postgresql 클라이언트를 실행하는 커맨드를 지원한다.

`kubectl cnpg psql mycluster`

```
\conninfo
You are connected to database "postgres" as user "postgres" via socket in "/controller/run" at port "5432".
```

**`backup`**
PostgreSQL 클러스터의 일관된 스냅샷을 생성한다.

postgresql 클러스터에 대한 스냅샷은 아래와 같은 순서로 이루어진다.
1. 작업 대상 replica Pod를 선택하기.
2. replica를 펜싱하기.
3. 스냅샷 생성하기
4. replica 펜싱 해제하기

해당 명령은 하나 이상의 replica가 있는 클러스터에만 사용할 수 있으며, 해당 replica는 스냅샷의 일관성을 보장하기 위해 펜실 절차에 의해 종료된다. (이를 Cold Backup 이라함)
이 기능은 개발 중인 쿠버네티스의 VolumeSnapshot API에 대한 지원을 따라간다.

# Wrap-Up
현재 현업에서 개발하고 있는 서비스의 백엔드 데이터베이스는 PostgreSQL로 개발되어 있다. AWS RDS Cluster 를 사용하여 개발하고 있으나, 서비스 오픈 이전에 많은 데이터베이스 비용이 발생하여 이에 대해 많은 고민을 하고 있었다.

처음 생각한 대안은 EC2 한대에 PostgreSQL을 기동시키는 것을 고민했다. 가장 비용적으로 절감할 수 있는 대안이었으나, 개발자로만 이루어진 팀에서 온프렘 DB의 운영 대응을 하기엔 구축시간 또는 운영대응에서 좋은 대안이 아니라고 판단하여 비싼 비용을 감내하고 AWS RDS Cluster 로 구축하여 개발을 진행하고 있다.

이 와중에 CloudNativePG를 학습하게 되면서 현재 우리 서비스의 데이터베이스로 고려해볼만한 솔루션이라고 생각한다. CloudNativePG 오퍼레이터를 설치하고, yaml에 원하는 스팩의 클러스터만 정의하면 PostgreSQL 클러스터가 기동되니, 이보다 편할 수 가 없다!

추가로 krew를 통해 지원되는 cnpg 플러그인에 대해 자세하게 살펴보면서, 장애 발생상황에서 리포트 추출이나, 갖가지 운영성 업무들을 커맨드를 통해 지원해주니, 개발팀만으로 이루어진 우리팀에서 사용하기 간편할 것으로 생각된다.