AEWS Study #3 – EKS Storage & Node 관리

AEWS 3회차는 EKS Storage와 Node 관리에 대한 내용을 다룬다.

0. 환경 구성

이번에도 스터디에서 제공 된 One Click 배포를 사용하여 환경을 구성한다.
이번에 새롭게 조금 수정 된 yaml 파일을 다운로드 받고 배포 스크립트를 실행하면 잠시의 시간이 지난 뒤 Cloudformation에서 모든 배포가 완료된 것을 확인할 수 있다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick2.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick2.yaml --stack-name myeks --parameter-overrides KeyName=aewspair SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i aewspair.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

배포 완료를 확인하고 AWS LB Controller, ExternalDNS와 kube-ops-view을 설치하여 환경 구성을 마무리한다.
kube ops view를 호출해 화면이 제대로 나오는 것까지 확인을 한다.

# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

# ExternalDNS
MyDomain=bs-yang.com
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

1. EKS Storage?

k8s에서 pod의 데이터는 pod가 정지되면 모두 삭제되게 되어있다. (stateless application)

하지만 pod에 데이터베이스를 올릴 때도 있고 데이터가 존속되어야 하는 경우도 필요하다. (stateful application) 이럴 때 사용하는 방식이 PV/PVC이다.

Pod가 생성될 때 자동으로 볼륨을 Pod에 마운틑하여 PV/PVC을 사용할 수 있게끔 하는 방식이 동적 프로비저닝(Dynamic provisioning)이라고 한다.

stateless 환경으로 배포하는 것을 테스트해보려고 한다.
data 명령으로 현재 시간을 10초 간격으로 out file하는 pod을 배포해서 시간이 찍히는 것을 확인하고 해당 pod을 삭제 후 재배포한 뒤 이전 기록이 남아있는지 확인해볼 예정이다.


위 사진을 보면 첫 사진의 시간대는 06:54:29부터 시작인데 삭제 후 재배포 된 pod에서는 06:55:21부터 시작하는 것을 확인할 수 있다. 이제 이 부분을 host path을 사용하는 PV/PVC을 통해 statefull 환경으로 배포를 해볼 예정이다.

# 배포
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml

# 확인
kubectl get-all -n local-path-storage
NAME                                                   NAMESPACE           AGE
configmap/kube-root-ca.crt                             local-path-storage  7s
configmap/local-path-config                            local-path-storage  7s
pod/local-path-provisioner-759f6bd7c9-rqh7l            local-path-storage  7s
serviceaccount/default                                 local-path-storage  7s
serviceaccount/local-path-provisioner-service-account  local-path-storage  7s
deployment.apps/local-path-provisioner                 local-path-storage  7s
replicaset.apps/local-path-provisioner-759f6bd7c9      local-path-storage  7s

kubectl get pod -n local-path-storage -owide
NAME                                      READY   STATUS    RESTARTS   AGE   IP             NODE                                               NOMINATED NODE   READINESS GATES
local-path-provisioner-759f6bd7c9-rqh7l   1/1     Running   0          12s   192.168.1.29   ip-192-168-1-231.ap-northeast-2.compute.internal   <none>           <none>

kubectl describe cm -n local-path-storage local-path-config
kubectl get sc
kubectl get sc local-path
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  84s

PV/PVC를 사용하는 pod을 배포하고 해당 pod에서 파일을 조회하고 해당 pod가 배포되어 있는 Worker node에서도 local path을 사용해서 파일을 조회해본다.
둘 다 동일한 결과값을 갖고있는 것을 확인할 수 있다.

# PVC 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath1.yaml
cat localpath1.yaml | yh
kubectl apply -f localpath1.yaml

# PVC 확인
kubectl get pvc
kubectl describe pvc

# 파드 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath2.yaml
cat localpath2.yaml | yh
kubectl apply -f localpath2.yaml

# 파드 확인
kubectl get pod,pv,pvc
kubectl describe pv    # Node Affinity 확인
kubectl exec -it app -- tail -f /data/out.txt
Thu May 11 07:00:04 UTC 2023
Thu May 11 07:00:09 UTC 2023
... 

# 워커노드 중 현재 파드가 배포되어 있다만, 아래 경로에 out.txt 파일 존재 확인
ssh ec2-user@$N2 tree /opt/local-path-provisioner
/opt/local-path-provisioner
└── pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim
    └── out.txt

# 해당 워커노드 자체에서 out.txt 파일 확인 : 아래 굵은 부분은 각자 실습 환경에 따라 다름
ssh ec2-user@$N2 tail -f /opt/local-path-provisioner/pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim/out.txt
Thu May 11 07:01:34 UTC 2023
Thu May 11 07:01:39 UTC 2023
... 

이제 해당 pod을 삭제했을 때도 파일이 남아있는지 그리고 재배포를 했을 때도 동일하게 파일을 조회할 수 있는지 확인해보도록 하겠다.
Local Path에도 그대로 남아있고 Pod을 새로 배포했을 때도 동일하게 파일이 조회되는 것을 확인할 수 있다.

# 파드 삭제 후 PV/PVC 확인
kubectl delete pod app
kubectl get pod,pv,pvc
ssh ec2-user@$N2 tree /opt/local-path-provisioner
/opt/local-path-provisioner
└── pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim
    └── out.txt

# 파드 다시 실행
kubectl apply -f localpath2.yaml
 
# 확인
kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt

2. AWS EBS Controller

CSI(Container Storage Interface)는의 컨테이너와 스토리지 시스템 간의 통합을 표준화하기 위한 인터페이스로 CSI를 사용하면 컨테이너 Kubernetes과 스토리지 공급자 간의 인터페이스를 표준화하여, 서로 다른 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 호환성을 보장할 수 있다.

이전에는 Kubernetes가 FlexVolume과 같은 사용자 정의 볼륨 플러그인을 사용하여 컨테이너와 스토리지 시스템 간의 인터페이스를 구현했지만 FlexVolume은 이식성과 유연성 측면에서 한계가 있었기 때문에, CSI가 도입되면서 대체되었다.

CSI는 다음과 같은 이점을 제공합니다.

  • 유연성: 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 인터페이스를 표준화하여, 서로 다른 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 호환성 보장
  • 이식성: CSI 스펙을 준수하는 스토리지 공급자는 어떤 컨테이너 오케스트레이션 시스템에서도 사용 가능
  • 모듈성: CSI는 별도의 드라이버를 개발하지 않아도 되기 때문에, 스토리지 공급자는 CSI 스펙을 준수하는 드라이버만 개발 가능

EBS CSI driver 동작은 볼륨을 생성하고 Pod에 해당 볼륨을 연결하는 동작이다.
persistentvolume, persistentvolumeclaim의 accessModes는 ReadWriteOnce로 설정해야 하는데 그 이유는 데이터 일관성과 무결성을 유지하기 위함이다. 여러 Node에서 해당 스토리지를 마운트하고 동시에 Write 작업을 수행하게 될 경우 데이터의 무결성이 보장되지 않을 수 있기 때문에 ReadWriteOnce AccessMode을 사용하여 데이터 일관성과 무결성을 유지해야 한다.
그리고 EBS 스토리지 기본 설정은 동일 AZ에 있는 EC2인스턴스와 그 Pod에 연결되는데 그 이유는 데이터 전송 속도가 빨라지고 더 높은 I/O 처리량을 얻을 수 있기 때문이다. 비용적인 측면에서도 동일 AZ에서의 데이터 전송 네트워크 대역폭 비용이 발생하지 않기 때문에 유리한 부분이 있다. 그리고 위의 AccessMode와 마찬가지로 데이터 일관성을 더 쉽게 보장 받을 수 있기 때문에 동일 AZ에 연결하게끔 되어 있다고 볼 수 있다.

EBS Controller을 설치하는 건 하기 내용을 통해 진행할 수 있다.

# 아래는 aws-ebs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
aws eks describe-addon-versions \
    --addon-name aws-ebs-csi-driver \
    --kubernetes-version 1.24 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text
v1.18.0-eksbuild.1
Tru
v1.17.0-eksbuild.1
False
...

# ISRA 설정 : AWS관리형 정책 AmazonEBSCSIDriverPolicy 사용
eksctl create iamserviceaccount \
  --name ebs-csi-controller-sa \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
  --approve \
  --role-only \
  --role-name AmazonEKS_EBS_CSI_DriverRole

# ISRA 확인
kubectl get sa -n kube-system ebs-csi-controller-sa -o yaml | head -5
eksctl get iamserviceaccount --cluster myeks
NAMESPACE	    NAME				            ROLE ARN
kube-system 	ebs-csi-controller-sa		arn:aws:iam::911283464785:role/AmazonEKS_EBS_CSI_DriverRole
...

# Amazon EBS CSI driver addon 추가
eksctl create addon --name aws-ebs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/AmazonEKS_EBS_CSI_DriverRole --force

# 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get deploy,ds -l=app.kubernetes.io/name=aws-ebs-csi-driver -n kube-system
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get pod -n kube-system -l app.kubernetes.io/component=csi-driver

# ebs-csi-controller 파드에 6개 컨테이너 확인
kubectl get pod -n kube-system -l app=ebs-csi-controller -o jsonpath='{.items[0].spec.containers[*].name}' ; echo
ebs-plugin csi-provisioner csi-attacher csi-snapshotter csi-resizer liveness-probe

# csinodes 확인
kubectl get csinodes

# gp3 스토리지 클래스 생성
kubectl get sc
cat <<EOT > gp3-sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp3
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  #fsType: ext4 # 기본값이 ext4 이며 xfs 등 변경 가능 >> 단 스냅샷 경우 ext4를 기본으로하여 동작하여 xfs 사용 시 문제가 될 수 있음 - 테스트해보자
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc
kubectl describe sc gp3 | grep Parameters

3. AWS Volume SnapShots Controller


Volume Snapshots Controller는 Kubernetes 클러스터 내에서 스냅샷을 생성하고 복원하기 위한 컨트롤이다. 이 컨트롤러는 Kubernetes Volume Snapshot API를 사용하여 스냅샷을 관리한다.
Volume Snapshot API를 사용하면 스토리지 클래스에서 스냅샷을 지원하는 경우 스냅샷을 생성할 수 있으며, 이를 사용하여 데이터를 백업하거나 특정 시점의 데이터로 복원할 수 있다.
Kubernetes Volume Snapshots Controller는 스냅샷을 생성하고 복원하기 위한 작업을 수행하고 스냅샷 생성을 위해서는 PVC(Persistent Volume Claim)을 사용하여 스냅샷 대상이 되는 볼륨을 식별하고 이를 기반으로 스냅샷을 생성한다. 스냅샷 생성 후에는 해당 스냅샷을 복원하여 이전 데이터를 다시 가져올 수 있다.
Kubernetes Volume Snapshots Controller는 또한 스냅샷 수명 주기 관리를 지원한다. 이를 통해 스냅샷을 자동으로 삭제하거나 보존할 수 있다. 스냅샷을 자동으로 삭제하면 비용을 절감하고 클러스터 용량을 확보할 수 있다. 반면에 스냅샷을 보존하면 장애 복구 및 데이터 분석 등에 유용하니 환경에 따라 유동적으로 관리할 수 있다.
정리하면 Kubernetes Volume Snapshots Controller를 사용하면 스토리지 클래스에서 스냅샷을 지원하는 경우 PVC를 사용하여 스냅샷을 생성하고 복원할 수 있고, 스냅샷 수명 주기 관리를 통해 비용을 절감하고 데이터를 보존할 수 있다.

VolumeSnapshotController을 설치하고 테스트 PVC/Pod을 통해 테스트를 진행해 볼 생각이다.
스냅샷을 생성하고 Pod와 PVC을 제거한 뒤 만들어둔 스냅샷을 통해 복원하는 과정으로 진행 된다.

# Install Snapshot CRDs
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f snapshot.storage.k8s.io_volumesnapshots.yaml,snapshot.storage.k8s.io_volumesnapshotclasses.yaml,snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl get crd | grep snapshot
volumesnapshotclasses.snapshot.storage.k8s.io    2023-05-12T03:46:15Z
volumesnapshotcontents.snapshot.storage.k8s.io   2023-05-12T03:46:15Z
volumesnapshots.snapshot.storage.k8s.io          2023-05-12T03:46:15Z
kubectl api-resources  | grep snapshot
volumesnapshotclasses             vsclass,vsclasses   snapshot.storage.k8s.io/v1             false        VolumeSnapshotClass
volumesnapshotcontents            vsc,vscs            snapshot.storage.k8s.io/v1             false        VolumeSnapshotContent
volumesnapshots                   vs                  snapshot.storage.k8s.io/v1             true         VolumeSnapshot

# Install Common Snapshot Controller
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl apply -f rbac-snapshot-controller.yaml,setup-snapshot-controller.yaml
serviceaccount/snapshot-controller created
clusterrole.rbac.authorization.k8s.io/snapshot-controller-runner created
clusterrolebinding.rbac.authorization.k8s.io/snapshot-controller-role created
role.rbac.authorization.k8s.io/snapshot-controller-leaderelection created
rolebinding.rbac.authorization.k8s.io/snapshot-controller-leaderelection created
deployment.apps/snapshot-controller created

kubectl get deploy -n kube-system snapshot-controller
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
snapshot-controller   2/2     2            0           19s

kubectl get pod -n kube-system -l app=snapshot-controller
NAME                                   READY   STATUS    RESTARTS   AGE
snapshot-controller-76494bf6c9-j8t2x   1/1     Running   0          42s
snapshot-controller-76494bf6c9-z7275   1/1     Running   0          42s

# Install Snapshotclass
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml
kubectl apply -f snapshotclass.yaml
kubectl get vsclass # 혹은 volumesnapshotclasses
# PVC yaml
cat <<EOT > awsebs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  storageClassName: gp3
EOT

# Pod yaml
cat <<EOT > awsebs-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
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: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim
EOT

# PVC 생성
kubectl apply -f awsebs-pvc.yaml

# 파드 생성
kubectl apply -f awsebs-pod.yaml

# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt

# VolumeSnapshot 생성 : Create a VolumeSnapshot referencing the PersistentVolumeClaim name >> EBS 스냅샷 확인
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-volume-snapshot.yaml
cat ebs-volume-snapshot.yaml | yh
kubectl apply -f ebs-volume-snapshot.yaml

# VolumeSnapshot 확인
kubectl get volumesnapshot
kubectl get volumesnapshot ebs-volume-snapshot -o jsonpath={.status.boundVolumeSnapshotContentName} ; echo
kubectl describe volumesnapshot.snapshot.storage.k8s.io ebs-volume-snapshot
kubectl get volumesnapshotcontents
NAME                                               READYTOUSE   RESTORESIZE   DELETIONPOLICY   DRIVER            VOLUMESNAPSHOTCLASS   VOLUMESNAPSHOT        VOLUMESNAPSHOTNAMESPACE   AGE
snapcontent-3bed7592-e760-42fc-8f2c-eb59d0b6714f   false        4294967296    Delete           ebs.csi.aws.com   csi-aws-vsc           ebs-volume-snapshot   default                   16s

# VolumeSnapshot ID 확인 
kubectl get volumesnapshotcontents -o jsonpath='{.items[*].status.snapshotHandle}' ; echo

# AWS EBS 스냅샷 확인
aws ec2 describe-snapshots --owner-ids self | jq
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[]' --output table

# app & pvc 제거 : 강제로 장애 재현
kubectl delete pod app && kubectl delete pvc ebs-claim


Snapshot 생성을 확인하고 App/PVC 삭제를 진행했다.
이후 스냅샷을 통해 복원한 후 기존 내용이 그대로 유지되는지 확인하도록 한다.
Pod의 특정 경로 /data/out.txt을 조회했을 때 위의 삭제 된 Pod에서 기존에 조회한 내용과 동일한 내용이 저장되어 있는 것을 확인할 수 있다.

# 스냅샷에서 PVC 로 복원
kubectl get pvc,pv
cat <<EOT > ebs-snapshot-restored-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-snapshot-restored-claim
spec:
  storageClassName: gp3
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  dataSource:
    name: ebs-volume-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
EOT
cat ebs-snapshot-restored-claim.yaml | yh
kubectl apply -f ebs-snapshot-restored-claim.yaml

# 확인
kubectl get pvc,pv

# 파드 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-snapshot-restored-pod.yaml
cat ebs-snapshot-restored-pod.yaml | yh
kubectl apply -f ebs-snapshot-restored-pod.yaml

# 파일 내용 저장 확인 : 파드 삭제 전까지의 저장 기록이 남아 있다. 이후 파드 재생성 후 기록도 잘 저장되고 있다
kubectl exec app -- cat /data/out.txt
Fri May 12 04:55:23 UTC 2023
Fri May 12 04:55:28 UTC 2023
Fri May 12 04:55:33 UTC 2023
Fri May 12 04:55:38 UTC 2023
Fri May 12 04:55:43 UTC 2023
Fri May 12 04:55:48 UTC 2023
...

# 삭제
kubectl delete pod app && kubectl delete pvc ebs-snapshot-restored-claim && kubectl delete volumesnapshots ebs-volume-snapshot

4. AWS EFS Controller

EFS Controller는 k8s에서 EFS을 마운트해서 사용하기 위해 사용하는 Controller이다.
EKS EFS Controller를 사용하면 EFS 파일 시스템을 생성하고, 해당 파일 시스템에서 EFS 볼륨을 동적으로 프로비저닝하고, 해당 볼륨을 파드에 마운트할 수 있다.
EKS EFS Controller를 사용하려면, 먼저 EFS 파일 시스템을 생성하고, 해당 파일 시스템에서 EFS 볼륨을 동적으로 프로비저닝할 수 있는 권한을 갖는 IAM 역할이 있어야 한다. 그런 다음, EKS 클러스터에 EFS CSI 드라이버를 설치하고, EKS EFS Controller를 설치하면 된다.

EKS EFS Controller를 사용하면 다음과 같은 이점이 있다.

  • EFS 파일 시스템에서 동적으로 프로비저닝된 EFS 볼륨 관리 편리
  • EFS 파일 시스템에 대한 별도의 설정 불필요
  • EKS 클러스터에서 파일 시스템을 생성하고 EFS 볼륨을 동적으로 프로비저닝하기 위한 AWS 리소스를 관리 불필요
  • EFS 파일 시스템과 EFS 볼륨에 대한 권한 관리 간편

EFS Controller을 설치하고 EFS Filesystem을 만든 뒤 해당 파일 시스템을 다수의 Pod가 사용할 수 있도록 설정하는 테스트를 진행할 예정이다.

우선 아래 내용을 참고해서 EFS Controller을 설치한다.

# EFS 정보 확인 
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text

# IAM 정책 생성
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/docs/iam-policy-example.json
aws iam create-policy --policy-name AmazonEKS_EFS_CSI_Driver_Policy --policy-document file://iam-policy-example.json

# ISRA 설정 : 고객관리형 정책 AmazonEKS_EFS_CSI_Driver_Policy 사용
eksctl create iamserviceaccount \
  --name efs-csi-controller-sa \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AmazonEKS_EFS_CSI_Driver_Policy \
  --approve

# ISRA 확인
kubectl get sa -n kube-system efs-csi-controller-sa -o yaml | head -5
eksctl get iamserviceaccount --cluster myeks

# EFS Controller 설치
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set image.repository=602401143452.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/eks/aws-efs-csi-driver \
    --set controller.serviceAccount.create=false \
    --set controller.serviceAccount.name=efs-csi-controller-sa

# 확인
helm list -n kube-system
kubectl get pod -n kube-system -l "app.kubernetes.io/name=aws-efs-csi-driver,app.kubernetes.io/instance=aws-efs-csi-driver"

EFS 파일시스템을 다수의 Pod가 사용하도록 설정을 해서 배포한다.
pod1은 out1.txt에 pod2는 out2.txt에 각각 시간을 입력하는 작업을 진행한다.
Bastion에서 Pod에 연결 된 EFS을 마운트해서 로컬에서도 파일이 동일하게 열리는지 확인하였다.
아래 캡쳐화면들을 보면 Local과 각 Pod에서 문제없이 파일을 불러올 수 있는 것을 확인할 수 있다.

# 모니터링
watch 'kubectl get sc efs-sc; echo; kubectl get pv,pvc,pod'

# 실습 코드 clone
git clone https://github.com/kubernetes-sigs/aws-efs-csi-driver.git /root/efs-csi
cd /root/efs-csi/examples/kubernetes/multiple_pods/specs && tree

# EFS 스토리지클래스 생성 및 확인
cat storageclass.yaml | yh
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc

# PV 생성 및 확인 : volumeHandle을 자신의 EFS 파일시스템ID로 변경
EfsFsId=$(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text)
sed -i "s/fs-4af69aab/$EfsFsId/g" pv.yaml

# EFS 확인 : AWS 관리콘솔 EFS 확인해보자
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport $EfsFsId.efs.ap-northeast-2.amazonaws.com:/ /mnt/myefs
df -hT --type nfs4
mount | grep nfs4

cat pv.yaml | yh

kubectl apply -f pv.yaml
kubectl get pv; kubectl describe pv

# PVC 생성 및 확인
cat claim.yaml | yh
kubectl apply -f claim.yaml
kubectl get pvc

# 파드 생성 및 연동 : 파드 내에 /data 데이터는 EFS를 사용
cat pod1.yaml pod2.yaml | yh
kubectl apply -f pod1.yaml,pod2.yaml
kubectl df-pv

# 파드 정보 확인 : PV에 5Gi 와 파드 내에서 확인한 NFS4 볼륨 크리 8.0E의 차이는 무엇? 파드에 6Gi 이상 저장 가능한가?
kubectl get pods
kubectl exec -ti app1 -- sh -c "df -hT -t nfs4"
kubectl exec -ti app2 -- sh -c "df -hT -t nfs4"
Filesystem           Type            Size      Used Available Use% Mounted on
127.0.0.1:/          nfs4            8.0E         0      8.0E   0% /data

# 공유 저장소 저장 동작 확인
tree /mnt/myefs              # 작업용EC2에서 확인
tail -f /mnt/myefs/out1.txt  # 작업용EC2에서 확인
kubectl exec -ti app1 -- tail -f /data/out1.txt
kubectl exec -ti app2 -- tail -f /data/out2.txt

5. Deploying WordPress and MySQL with Persistent Volumes

Persistent Volume을 사용하는 WorkPress와 MySQL을 배포하는 실습을 진행해보려고 한다. PV을 사용하지 않을 경우 Pod가 Node의 장애로 인해 죽게 될 경우 데이터가 유실되기 때문에 PV을 사용하는 WordPress와 MySQL을 배포해보려고 한다.
실습은 https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/ 해당 링크를 참고하였다.

deployment yaml 파일을 다운로드 받는 작업으로 시작한다.

#workdpress, mysql deployment yaml Download
curl -s -O https://kubernetes.io/examples/application/wordpress/mysql-deployment.yaml
curl -s -O https://kubernetes.io/examples/application/wordpress/wordpress-deployment.yaml

cat mysql-deployment.yaml | yh
apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
...

cat wordpress-deployment.yaml | yh
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
...

kustomization.yaml 파일을 생성한다. 여기서 kustomization은 Kubernetes 클러스터에서 배포하려는 리소스들을 커스터마이징하는 방법을 제공하는 도구로 kustomization.yaml 파일을 통해 배포하고자 하는 리소스들의 목록과 각 리소스별로 커스터마이징을 수행하는 설정을 지정할 수 있다.
Kustomization은 다음과 같은 기능을 갖고 있다.

  • 기존 manifest 파일을 수정하지 않고, 수정된 내용을 적용할 수 있다.
  • 배포 시에 여러 개의 환경(예: dev, stage, prod)에 대한 다른 설정을 지정할 수 있다.
  • 배포할 리소스를 기반으로 자동으로 이름을 생성하여, 리소스 이름 충돌을 방지한다.

kustomization.yaml에 secret manager을 추가해서 mysql secret을 생성할 계획이다.

cat <<EOF >./kustomization.yaml
secretGenerator:
- name: mysql-pass
  literals:
  - password=YOUR_PASSWORD
resources:
  - mysql-deployment.yaml
  - wordpress-deployment.yaml
EOF

kustomization.yaml 파일에 Resources 정보를 담았기 때문에 kustomization.yaml 파일과 deployment yaml을 같은 폴더에 두고 배포를 진행할 예정이다.
deployment yaml 파일들을 보면 pvc를 20Gi로 생성하는 것으로 되어있는데 테스트 목적이기 때문에 4Gi로 변경하고 Internet Facing NLB을 생성하기 위해 wordpress deployment을 일부분 수정하였다.

# Internet Facing NLB 생성을 위해 wordpress deployment yaml 파일 수정
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
    service.beta.kubernetes.io/aws-load-balancer-internal: "false"
spec:
  ports:
    - port: 80
      targetPort: 80

# 배포 진행
kubectl apply -k ./
secret/mysql-pass-2ffhd9htbk unchanged
service/wordpress unchanged
service/wordpress-mysql unchanged
persistentvolumeclaim/mysql-pv-claim created
persistentvolumeclaim/wp-pv-claim created
deployment.apps/wordpress created
deployment.apps/wordpress-mysql created

# secret 생성 확인
kubectl get secrets
NAME                    TYPE     DATA   AGE
mysql-pass-2ffhd9htbk   Opaque   1      8m9s

# PVC가 Dynamic Provisioning 되는지 확인
kubectl get pvc, pv
NAME                                   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/mysql-pv-claim   Bound    pvc-14773585-e62e-47df-884a-4415a1961d7a   4Gi        RWO            gp2            97s
persistentvolumeclaim/wp-pv-claim      Bound    pvc-1652c71f-ffd7-423a-8632-c59a2f3ec44c   4Gi        RWO            gp2            97s
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
persistentvolume/pvc-14773585-e62e-47df-884a-4415a1961d7a   4Gi        RWO            Delete           Bound    default/mysql-pv-claim   gp2                     93s
persistentvolume/pvc-1652c71f-ffd7-423a-8632-c59a2f3ec44c   4Gi        RWO            Delete           Bound    default/wp-pv-claim      gp2                     93s

# Pod 정보 확인
kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
wordpress-664bfdc845-nmh8p         1/1     Running   0          4m
wordpress-mysql-85648459b5-qzcwj   1/1     Running   0          4m

# Service 확인
kubectl get services wordpress
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP                                                                         PORT(S)        AGE
wordpress   LoadBalancer   10.100.62.174   k8s-default-wordpres-d30dc22441-01684caff9c44457.elb.ap-northeast-2.amazonaws.com   80:30479/TCP   12m

# Service NLB을 Domain에 연결
kubectl annotate service wordpress -n default "external-dns.alpha.kubernetes.io/hostname=wptest.$MyDomain"
echo -e "Wordpress Test URL = http://wptest.$MyDomain"

설치 후 NLB가 연결 된 Domain을 호출하여 WordPress 설치페이지에 접속하였다.
이후 kustomization.yaml 에서 설정한 암호를 이용해서 wordpress 설치를 진행해봤다.
설치는 무리 없이 잘 설치되었고 이를 통해 mysql password 설정 또한 제대로 됐음을 알 수 있다. (mysql password 설정에 문제가 있다면 wordpress 설치가 정상적으로 되지 않기 때문에)

해당 실습이 종료됐으니 삭제를 진행한다.

# 삭제 진행
kubectl delete -k ./
secret "mysql-pass-2ffhd9htbk" deleted
service "wordpress" deleted
service "wordpress-mysql" deleted
persistentvolumeclaim "mysql-pv-claim" deleted
persistentvolumeclaim "wp-pv-claim" deleted
deployment.apps "wordpress" deleted
deployment.apps "wordpress-mysql" deleted

6. 정리

Storage의 경우 kOps 실습 때와 크게 다르지 않았지만 wordpress 배포를 통해 조금 더 상세한 내용을 테스트해볼 수 있어서 좋았다.
컨테이너로 db 등은 운영하지 않던 예전과 다르게 최근엔 db을 운영하기도 한다고 하니 앞으로 이 부분을 잘 공부해두면 좋을 것 같다.

PKOS Study #2-2 – Kubernetes Network&Storage

PKOS Study #2-1에서 Kubernetes Network에 대한 기본적인 내용을 정리했다.

이번 2-2에서는 지난 문서에서 정리하지 못한 Loadbalancer에 대한 내용과 Storage 파트를 정리할 계획이다.

1. Pods 생성 수량 제한

Worker Node의 EC2 Instance Type 별로 Pods 생성 수량은 제한 된다. Pods 생성 수량 기준은 인스턴스에 연결 가능한 ENI 숫자와 할당 가능한 IP 수에 따라 결정되게 된다.

* aws-node, kube-proxy Pods는 Host의 IP을 같이 사용하기 때문에 배포 가능 최대 수량에서 제외 된다. (IP 조건에서 제외되기 때문에)

Pods 최대 생성 수량 계산 : {Instance에 연결 가능한 최대 ENI 수량 x (ENI에 할당 가능한 IP 수량 – 1)} + 2

위 식을 계산하기 위해서는 내가 사용하는 Node Instance Type의 ENI 및 IP 할당 제한을 확인할 필요가 있는데 그럴 때는 아래와 같이 확인 할 수 있다.

Values=t3.* 부분을 확인하고 싶은 Instance Type으로 변경하면 된다.

aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table

내가 사용하는 Instance Type은 t3.medium이고 해당 Instance의 경우 연결 가능한 ENI는 3개이고 ENI당 할당 가능한 IP는 6개이다. 위 계산식에 대입하면 {3 x (6-1)} + 2가 되고 aws-node, kube-proxy 포함하여 총 17개의 Pods를 생성할 수 있게 된다.

Pods 생성 수량(Replicas)을 늘리면서 Worker Node의 ENI 정보와 IP 주소에 어떤 변화가 있는지 확인해보겠다.

우선 어떠한 Pods도 배포하지 않았을 때 Worker Node 1, 2의 ENI 상태 화면이다. (위가 1, 밑이 2)

Replicas 수량이 2개인 Deployment을 생성했을 때 ENI와 IP 모습이다.

Pods가 2개 생성되어 각각 IP을 할당 받은 것을 확인 할 수 있다. 각 Pods는 서로 다른 Node에 배포 되었다는 것을 IP을 통해 확인 할 수 있다.

이번엔 Replicas을 8로 증가하여 보았다.

Pods가 8개로 증가하며 ENI 정보와 IP가 각각 증가한 것을 알 수 있다. 현재 Worker Node는 t3.medium으로 앞서 계산한 공식에 따르면 Pods을 15개까지 생성할 수 있다. Worker Node가 2개이니 30개까지 생성이 가능한지 확인해봤다.

30개로 증가하는 명령어 이후 Running 중인 Pods 수량을 확인하면 21개만 생성 된 것을 알 수 있다. 왜 30개가 아닌 21개인지 궁금하여 현재 Node에서 실행 중인 Pods들을 모두 조회하고 해당 Pods들이 사용 중인 IP들을 확인해보았다.

kubectl get pod --all-namespaces -o wide

현재 Deployment가 생성 된 namespace가 아닌 kube-system namespace에서 사용 중인 Pods들까지 확인할 수 있었다.

Node 1은 이미 5개의 IP을 할당 받아 사용 중이었고 Node 2는 4개의 IP을 할당 받아 사용 중이었다. 총 할당 가능한 15개의 IP 중 이미 4, 5개의 IP을 기존 시스템에서 사용 중인 상태라 30개의 Pods 중 9개가 부족한 21개의 Pods만 정상적으로 배포가 된 것을 확인할 수 있었다.

그럼 EC2 Instance Type에 종속되는 Pods 배포 수량을 극복할 수 있는 방안은 없을까? 아니다 Prefix Delegation 방식을 사용하면 Instance Type에 한정되지 않고 Pods을 배포할 수 있다.

자세한 내용은 아래 링크를 참고하면 된다.Amazon EC2 노드에 사용 가능한 IP 주소의 양 늘리기 – Amazon EKS관리형 노드 그룹은 maxPods의 값에 최대 수를 적용합니다. vCPU가 30개 미만인 인스턴스의 경우 최대 수는 110이고 다른 모든 인스턴스의 경우 최대 수는 250입니다. 이 최대 수는 접두사 위임의 활성docs.aws.amazon.com

나는 t3.medium Type을 Worker Node에 사용했고 해당 Node의 Allocatable을 확인하는 명령어를 통해 가용 가능 Pods을 확인해보았다. 앞서 계산한 바와 같이 17개임을 확인 할 수 있다.

50개의 Pods을 배포하는 명령어를 실행했을 때 21개만 배포가 되어있는 것을 확인할 수 있다. 이는 앞서 배포했을 때 확인할 수 있었던 수치와 동일하다.

설정값을 바꾸기 위해 kops edit cluster을 통해 내용을 수정하고 Rolling Update을 실행했다.

kops edit cluster
<em>#maxPods 수정</em>
  kubelet:
    anonymousAuth: false
    maxPods: 50
<em>#ENABLE_PREFIX_DELEGATION 수정</em>
  networking:
    amazonvpc:
      env:
      - name: ENABLE_PREFIX_DELEGATION
        value: "true"

<em>#Rolling Update 진행, 해당 과정에서 Node들의 IP 변경 발생하고 약 15~20분 정도 소요 된다.</em>
kops update cluster --yes && echo && sleep 5 && kops rolling-update cluster --yes

Rolling Update 이후 배포 가능 Pods 확인을 진행하였다.

그 후 다시 Replicas을 50으로 수정하여 배포를 진행하였다.

50개가 모두 실행 중인 것을 확인할 수 있었다. ENI 정보와 IP 정보를 조회했을 때도 해당 수량이 증가한 것을 알 수 있었다.

나는 실습 전에 CPU Limits을 제거하고 진행했기 때문에 원할하게 Pods들이 증가했으나 Network 제한만 푼다고 해서 Pods들이 많이 생성되지 않을 수 있다. 그럴 때는 아래와 같은 방법으로 CPU Limits을 해제하면 된다.

kubectl describe limitranges <em># LimitRanges 기본 정책 확인 : 컨테이너는 기본적으로 0.1CPU(=100m vcpu)를 최소 보장(개런티)</em>
kubectl delete limitranges limits
kubectl get limitranges

위 방법으로 CPU 제한을 해제하고 배포를 진행해보면 더 많은 수의 Pods가 배포되는 것을 확인할 수 있다.

2. LoadBalancer Controller with NLB

Kubernetes의 Service에는 ClusterIP, NodePort, Loadbalancer Type이 있다.

ClusterIP : Control Plane의 iptables을 이용하여 내부에서만 접근 가능한 방식으로 외부에서의 접근은 불가능하다.

NodePort : 위 ClusterIP는 외부에서 접근이 불가능하기 때문에 외부에서 접근 가능하게 하기 위해서 Worker Node의 Port을 맵핑하여 통신할 수 있도록 한다.

Loadbalancer : NodePort 방식과 마찬가지로 외부에서 접근할 수 있는 방식이다. 다만, Worker Node의 Port을 통해 접근하는 방식이 아닌 앞단에 위치한 LoadBalancer을 통해 접근하게 된다.

Loadbalancer Controller : Public Cloud을 사용할 경우 각 CSP에서 제공하는 Loadbalancer을 사용하기 위해 설치하는 Controller이다. 이를 통해 CSP의 LB을 제어할 수 있게 된다.

나는 one-click kOps yaml을 사용하여 실습을 진행했기 때문에 그 과정에 IAM Policy 생성 및 Role Attach까지 같이 마무리를 하였다. 혹시 그 내용이 누락되어 있다면 해당 내용을 추가로 진행해야 한다. (특히 Rolling Update 등으로 IAM Profile이 빠져있을 수 있다.)

NLB가 같이 포함 된 yaml을 사용하여 Deployment 생성을 진행했다.

그 후 정보를 확인하여 NLB 주소를 확인할 수 있었다.

NLB 주소를 Client에서 접속 시도하여 정상적으로 페이지가 호출되는 것까지 확인하였고 해당 NLB 주소는 내 도메인에 연결 및 테스트까지 완료하였다.

내 도메인 레코드와 NLB을 통해 정상적으로 분산 되는지를 테스트하였고 이상 없이 분산 되는 것을 확인했다.

NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
curl -s $NLB
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr

3. Kubernetes Storage

Kubernetes Storage : hostPath, emptyDir, PV/PVC 등이 있고 이번 실습에는 PV/PVC을 진행할 예정이다.

hostPath : Pods가 배포 된 Worker Node(Host)의 Directory Path을 Pod에 Mount하여 사용하는 방법으로 Host Directory Path에 Data을 저장하기 때문에 Pods가 삭제되더라도 Data는 유지시킬 수 있다.

emptyDir : Pod 내부에 존재하고 휘발성을 뛰고 있다. hostPath와 다르게 Pod을 삭제하면 Data도 삭제되기 때문에 이 점은 유의해야 한다.

PV/PVC : hostPath와 언뜻 비슷해보이지만 hostPath는 Pods가 배포 된 Worker Node의 공간을 사용한다고 봤을 때 PV/PVC는 공유 공간이라고 보는 게 좋다. 동일한 Worker Node가 아니더라도 공간을 공유할 수 있으며 이를 위해 AWS에서는 EBS, EFS 등을 사용한다.

EBS을 생성하고 추가 된 EBS 볼륨을 확인하는 내용으로 PV/PVC 실습을 진행한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
apiVersion: v1
kind: Pod
metadata:
  name: app
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: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim

위 내용으로 PVC 및 Pods을 생성한다. 4GB의 용량에 RW Access가 설정 된 EBS을 생성한 후 Pods에 해당 EBS을 Mount하는 내용이다. 생성 된 Pods의 /data 경로를 보면 해당 EBS가 마운트되어 있어야 한다. 아래 방법으로 해당 내용을 확인한다.

/data/out.txt 파일을 호출하고 app Pod의 Filesystem 정보를 조회한 내용이다. 제대로 Mount되었음을 확인할 수 있다.

4. 마무리

지난 실습 때 Pods 생성 제한이나 NLB 등을 제대로 이해하지 못해 추가로 진행하였는데 늦게라도 해보길 잘했다는 생각이 들었다. Storage 부분은 특이한 내용은 없었고 기존 컨테이너를 공부할 때 배웠던 내용들이 같이 들어있어서 이해하는데 도움이 됐다.

Network 부분이 계속 어려웠었는데 이번 기회가 기술 성숙도를 올릴 수 있는 기회가 되길 바란다.