Kubernetes의 기본 설정에서 Pod는 임시 저장소를 사용합니다. 이는 Pod가 종료되거나 재시작될 때, 그 안의 데이터가 사라진다는 의미입니다. 예를 들어, 애플리케이션이 실행 중인 동안에 파일을 저장하거나 데이터베이스에 값을 기록했다 하더라도, Pod가 재시작되면 이러한 데이터는 사라지게 됩니다.
이 문제를 해결하려면 Persistent Storage를 사용해야 합니다. Persistent Storage를 사용하면 Pod가 재시작되더라도 데이터를 지속적으로 저장할 수 있습니다. Kubernetes에서는 이를 Persistent Volume (PV)과 Persistent Volume Claim (PVC)을 통해 처리할 수 있습니다. 이 두 가지를 사용하면, 데이터는 Pod의 생애 주기와 관계없이 지속적으로 유지됩니다.
볼륨 타입
Persistent Volume (PV)
PV는 클러스터 내 실제 스토리지 자원입니다. EKS에서는 EBS(Elastic Block Storage), EFS(Elastic File System)와 같은 AWS 스토리지 서비스를 PV로 사용할 수 있습니다.
PV는 실제 스토리지와 연결되어 Kubernetes 클러스터 내에서 데이터를 영구적으로 저장할 수 있게 해줍니다.
Persistent Volume Claim (PVC)
PVC는 사용자가 필요한 저장소를 요청하는 방법입니다.
PVC를 통해 사용자는 필요한 저장 용량, 액세스 모드 등을 정의하고, Kubernetes는 이 조건에 맞는 PV를 찾아 연결해줍니다.
Pod는 PVC를 통해 스토리지에 접근합니다. Pod는 직접 PV를 다루지 않고, PVC가 필요한 PV와 연결을 대신 처리합니다.
PVC에 연결된 PV에 저장된 데이터를 Pod는 사용하게 되며, Pod가 재시작돼도 데이터는 유지됩니다.
간단한 예시
PV 생성: 클러스터에서 100GB 크기의 EBS 볼륨을 생성하여 PV로 설정합니다.
PVC 생성: 사용자가 "20GB 크기의 스토리지가 필요해"라고 PVC를 생성합니다.
Kubernetes는 100GB PV 중 20GB를 사용할 수 있는 공간을 찾아 PVC와 연결합니다.
Pod는 PVC를 통해 이 PV에 접근하여 데이터를 저장하고, Pod 재시작 시에도 데이터는 유지됩니다.
StorageClass
StorageClass는 스토리지의 특성을 정의하는 Kubernetes 리소스입니다. 예를 들어, 고속 SSD나 저장 용량이 큰 HDD를 할지 아니면 암호화와 백업이 필요한 스토리지를 정의할지를 StorageClass를 통해 이를 설정할 수 있습니다.
사용자가 PVC를 생성할 때, StorageClass를 지정하면 Kubernetes가 동적으로 PV를 생성하고 연결해 줍니다. 이를 통해 사용자는 스토리지 프로비저너가 자동으로 스토리지를 생성하는 과정을 간소화할 수 있는데 이를 동적 프로비저닝(Dynamic Provisioning)이라 합니다.
파드 기본 저장소 동작 확인
# redis 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
terminationGracePeriodSeconds: 0
containers:
- name: redis
image: redis
EOF
# redis 파드 내에 파일 작성
kubectl exec -it redis -- pwd
kubectl exec -it redis -- sh -c "echo hello > /data/hello.txt"
kubectl exec -it redis -- cat /data/hello.txt
# redis 파드 내에 ps 설치
kubectl exec -it redis -- sh -c "apt update && apt install procps -y"
kubectl exec -it redis -- ps aux
파드 내에 redis라는 프로세스를 강제 종료하게 되면 어떻게 될까요?
메인 프로세스가 종료되면 파드 전체가 종료되게 되는데 restartPolicy값에 따라 파드가 재시작되거나 종료 상태로 유지됩니다. 일반적으로 파드의 restartPolicy값은 Always이므로 파드가 재시작되었습니다.
그렇다면 재시작된 redis 파드 내에 파일은 어떻게 되었는지 확인해보겠습니다.
# redis 파드 내에 파일 확인
kubectl exec -it redis -- cat /data/hello.txt
kubectl exec -it redis -- ls -l /data
기본 저장소는 휘발성이기 때문에 파드가 재시작되거나 종료될 경우 데이터는 모두 사라지게 됩니다.
empty Dir
emptyDir는 Pod가 시작될 때 빈 디렉터리를 생성하고, Pod가 실행되는 동안만 데이터를 저장합니다.
Pod가 종료되거나 재시작되면 데이터는 사라지기 때문에 주로 임시 데이터를 저장하거나, 컨테이너 간에 짧은 기간 동안 공유할 데이터가 있을 때 사용합니다.
파드 emptyDir 동작 확인
# redis 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
terminationGracePeriodSeconds: 0
containers:
- name: redis
image: redis
volumeMounts:
- name: redis-storage
mountPath: /data/redis
volumes:
- name: redis-storage
emptyDir: {}
EOF
# redis 파드 내에 파일 작성
kubectl exec -it redis -- pwd
kubectl exec -it redis -- sh -c "echo hello > /data/redis/hello.txt"
kubectl exec -it redis -- cat /data/redis/hello.txt
#ps 설치
kubectl exec -it redis -- sh -c "apt update && apt install procps -y"
kubectl exec -it redis -- ps aux
위 기본 저장소와 마찬가지로 redis 파드 내에 redis 프로세스를 강제 종료를 해보면 동일하게 재시작되게 됩니다.
파드 내에 파일은 어떻게 되었을까요?
기본 저장소와 다르게 emptyDir을 사용하였을 경우 파드가 재시작되어도 데이터는 유지됩니다.
하지만 파드를 삭제하게 되면 emptyDir도 함께 사라지기 때문에 동일한 redis 삭제하였다가 재생성한다 하여도 새롭게 생성된 파드에서는 파일들이 존재하지 않는 것을 확인할 수 있습니다.
# 파드 삭제 후 파일 확인
kubectl delete pod redis
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
terminationGracePeriodSeconds: 0
containers:
- name: redis
image: redis
volumeMounts:
- name: redis-storage
mountPath: /data/redis
volumes:
- name: redis-storage
emptyDir: {}
EOF
hostPath
Pod가 종료되어도 데이터는 유지되지만 해당 노드에만 데이터가 저장되므로 노드 장애 시 데이터 손실이 발생할 수 있습니다.
로컬 디스크에 저장되므로 빠르지만, 데이터를 공유하거나 이중화하기 어렵습니다.
외부 네트워크의 볼륨과 내부 네트워크의 볼륨들이 어떻게 연결되고 저장되는지는 아래 그림을 통해 확인할 수 있습니다.
Dynamic Provisioning과 Persistent Volume (PV)의 상태(Status) 및 ReclaimPolicy가 어떻게 동작하는지는 아래 그림을 참고하시면 됩니다.
Local Path Provisioner
kubernetes에서 노드의 로컬 디스크를 자동으로 할당하고 관리도구로 hostPath와 달리 PVC를 통해 동적으로 볼륨을 생성하고 삭제할 수 있도록 지원합니다.
다만, 노드 종속적이기 때문에 Pod가 다른 노드로 이동하면 기존 데이터를 사용할 수 없기 때문에 단일 노드에서 실행되는 애플리케이션이나 특정 노드에서 유지되어야 하는 워크로드에 적합합니다.
# Local Path Provisioner 배포
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml
...
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
apiVersion: v1
kind: ConfigMap
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["/opt/local-path-provisioner"]
}
]
}
setup: |-
#!/bin/sh
set -eu
mkdir -m 0777 -p "$VOL_DIR"
teardown: |-
#!/bin/sh
set -eu
rm -rf "$VOL_DIR"
...
# 확인
kubectl get-all -n local-path-storage
kubectl get pod -n local-path-storage -owide
kubectl describe cm -n local-path-storage local-path-config
kubectl get sc
kubectl get sc local-path
참고: Local Path Provisioner는 자동으로 설정을 다시 로드(재적용)하는 기능을 지원하는데 ConfigMap을 수정하면 Provisioner가 이를 감지하고 설정을 자동으로 업데이트합니다. 즉, Pod를 재시작하거나 Provisioner를 다시 배포할 필요 없이 설정을 즉시 적용할 수 있습니다. 볼륨을 저장할 디렉토리를 변경하거나, 볼륨 크기 제한 등을 수정할 때 유용합니다.
PV/PVC 를 사용하는 파드 생성
# PVC 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: localpath-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
EOF
# 파드 생성
cat <<EOF | kubectl apply -f -
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: localpath-claim
EOF
#PV,PVC,Pod 확인
kubectl get pod,pv,pvc
#Node Affinity 확인
kubectl describe pv
# 워커노드 중 현재 파드가 배포되어 있다만, 아래 경로에 out.txt 파일 존재 확인
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done
ssh ec2-user@$N2 tail -f /opt/local-path-provisioner/pvc-9df92729-ccc5-452a-a9d7-792ee0c3c9e6_default_localpath-claim/out.txt
파드 삭제 후 파드 재생성해서 데이터 유지 되는지 확인
# 파드 삭제 후 PV/PVC 확인
kubectl delete pod app
kubectl get pod,pv,pvc
# 파드 다시 실행
cat <<EOF | kubectl apply -f -
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: localpath-claim
EOF
# 확인
kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt
AWS CSI는 Kubernetes에서 AWS 스토리지(EBS, EFS, FSx 등)를 쉽게 연결하고 관리할 수 있게 해줍니다. 특히 Dynamic Provisioning과 StorageClass를 통해 PVC 요청 시 스토리지를 자동으로 할당하고 Pod 연결하며, Pod 이동 시에도 데이터를 안전하게 유지할 수 있어 EKS 환경에서 필수적인 스토리지 솔루션입니다.
이 때 실제로 스토리지를 생성하고 연결하는 역할은 AWS CSI Driver가 담당하게 되는데, AWS 스토리지 서비스별로 각각 다른 CSI Driver가 있습니다.
아래는 일반적인 Kubernetes CSI Driver 구조입니다. AWS EBS CIS Driver뿐만 아니라 모든 Kubernetes CSI Driver가 공통으로 사용하는 구조입니다.
StatefulSet/Deployment(Controller Pod)는 AWS API를 사용해 EBS Volume을 생성합니다. 사용자가 PVC (PersistentVolumeClaim)를 생성하면 StorageClass 설정에 따라 EBS Volume을 자동으로 만들고 Kubernetes에 PersistentVolume (PV)으로 등록합니다.
DaemonSet(Node Pod)은 AWS API를 사용해 생성된 EBS Volume을 Kubernetes Node (EC2 인스턴스)에 Attach합니다. Pod가 PVC를 사용하면 EBS Volume을 EC2에 연결하고 Pod에 마운트하여 Pod가 데이터를 읽고 쓸 수 있게 해줍니다.
AWS CSI Driver는 크게 2가지 구성요소가 있습니다.
AWS EBS CSI Driver
※ PersistentVolume 및 PersistentVolumeClaim의 accessModes 설정
AWS EBS를 사용할 때는 반드시 ReadWriteOnce(RWO)로 설정해야 하는 이유가 있습니다. RWO는 하나의 노드에서만 읽기/쓰기 가능한 모드로 한개의 워커 노드에서만 EBS 볼륨을 마운트할 수 있습니다. 여러 개의 Pod가 같은 노드에서 실행되면 공유할 수 있지만, 다른 노드에서는 동시에 접근할 수 없습니다.
※ AWS EBS 스토리지와 동일 AZ 내 EC2 연결 필요성 및 파드 스케줄링 방안
EBS는 네트워크 블록 스토리지로, 네트워크를 통해 EC2 인스턴스에 연결되기 때문에 다른 AZ일 경우 EC2에 연결할 수 없습니다. Pod가 올바른 AZ의 노드에서 실행되도록 nodeSelector, WaitForFirstConsumer, nodeAffinity 등을 활용해야 합니다.
#aws-ebs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
aws eks describe-addon-versions --addon-name aws-ebs-csi-driver --kubernetes-version 1.31 --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" --output text
EKS에서 EBS CSI Driver가 사용할 IAM Role을 생성하는 작업을 수행하여 EBS CSI Driver가 AWS API를 호출하여 EBS 볼륨을 생성, 삭제, 연결할 수 있습니다.
# 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
참고: IRSA (IAM Role Service Account)는 Kubernetes Service Account와 AWS IAM Role을 연결하여, Pod가 AWS 리소스에 접근할 때 해당 Role의 권한을 사용하도록 하는 기능입니다.
# Amazon EBS CSI driver addon 배포(설치)
export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
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
kubectl get sa -n kube-system ebs-csi-controller-sa -o yaml | head -5
#설치 확인
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
# csinodes 확인
kubectl api-resources | grep -i csi
kubectl get csinodes
kubectl describe csinodes
#Amazon EBS CSI Driver의 상태 및 설정 정보를 확인
kubectl get csidrivers
kubectl describe csidrivers ebs.csi.aws.com
# (참고) 노드에 최대 EBS 부착 수량 변경
aws eks update-addon --cluster-name ${CLUSTER_NAME} --addon-name aws-ebs-csi-driver \
--addon-version v1.39.0-eksbuild.1 --configuration-values '{
"node": {
"volumeAttachLimit": 31,
"enableMetrics": true
}
}'
#변경 확인
kubectl get ds -n kube-system ebs-csi-node -o yaml
...
containers:
- args:
- node
- --endpoint=$(CSI_ENDPOINT)
- --csi-mount-point-prefix=/var/lib/kubelet/plugins/kubernetes.io/csi/ebs.csi.aws.com/
- --volume-attach-limit=31
- --logging-format=text
- --v=2
또는
kubectl describe csinodes
...
Spec:
Drivers:
ebs.csi.aws.com:
Node ID: i-0660cdc75451595ab
Allocatables:
Count: 31
gp3 스토리지 클래스 생성
# gp3 스토리지 클래스 생성
cat <<EOF | kubectl apply -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
#iops: "5000"
#throughput: "250"
allowAutoIOPSPerGBIncrease: 'true'
encrypted: 'true'
fsType: xfs # 기본값이 ext4
EOF
#확인
kubectl get sc
kubectl describe sc gp3 | grep Parameters
참고
※ volumeBindingMode는 Persistent Volume (PV)이 Persistent Volume Claim (PVC)과 언제 바인딩(연결)되는지를 결정하는 설정입니다. 또한 동적 프로비저닝(스토리지 자동 생성)이 언제 실행될지도 제어합니다.
◎ Immediate(기본값) 모드에서는 PersistentVolumeClaim(PVC)이 생성되면 즉시 PersistentVolume(PV)과 바인딩되고, 동적 프로비저닝도 즉시 실행됩니다. 하지만 스토리지 백엔드가 특정 노드에서만 접근 가능한 경우, Pod의 스케줄링 요구 사항을 고려하지 않고 PV가 미리 바인딩될 수 있습니다. 이로 인해 Pod가 특정 노드에서 실행될 수 없는 상황이 발생할 가능성이 있으며, 결과적으로 스케줄되지 않은 Pod가 생길 수도 있습니다.
◎ WaitForFirstConsumer 모드를 사용하면 PVC를 생성하더라도 즉시 PV와 바인딩되지 않고, 해당 PVC를 사용하는 Pod가 생성될 때까지 바인딩과 프로비저닝이 지연됩니다. 이 모드는 Pod의 스케줄링 조건을 고려하여 PV를 선택하거나 프로비저닝하기 때문에, 특정 노드에 종속적인 스토리지(Amazon EBS 등)가 올바르게 연결될 수 있도록 합니다. 또한, Pod의 배치를 결정하는 요소인 리소스 요구 사항, 노드 셀렉터, 어피니티(Affinity) 및 안티-어피니티(Anti-Affinity), 테인트(Taint) 및 톨러레이션(Toleration) 등을 반영하여 적절한 노드에서 실행될 수 있도록 도와줍니다.
PVC/PV 테스트
# 워커노드의 EBS 볼륨 확인 : tag(키/값) 필터링 - 링크
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --output table
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].Attachments" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].[VolumeId, VolumeType, Attachments[].[InstanceId, State][]][]" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq
# 워커노드에서 파드에 추가한 EBS 볼륨 확인
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --output table
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq
# PVC 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi
storageClassName: gp3
EOF
# 파드 생성
cat <<EOF | kubectl apply -f -
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
EOF
# PVC, 파드 확인
kubectl get pvc,pv,pod
kubectl get VolumeAttachment
kubectl df-pv
# 추가된 EBS 볼륨 상세 정보 확인 : AWS 관리콘솔 EC2(EBS)에서 확인
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
#PV 상세 확인
kubectl get pv -o yaml
apiVersion: v1
items:
- apiVersion: v1
kind: PersistentVolume
metadata:
annotations:
pv.kubernetes.io/provisioned-by: ebs.csi.aws.com
volume.kubernetes.io/provisioner-deletion-secret-name: ""
volume.kubernetes.io/provisioner-deletion-secret-namespace: ""
creationTimestamp: "2025-02-20T05:43:40Z"
finalizers:
- external-provisioner.volume.kubernetes.io/finalizer
- kubernetes.io/pv-protection
- external-attacher/ebs-csi-aws-com
name: pvc-a412b3be-a478-4942-bc25-0e82260647f8
resourceVersion: "352582"
uid: c5a9838b-01bd-4ca6-93eb-0e1701f94030
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 4Gi
claimRef:
apiVersion: v1
kind: PersistentVolumeClaim
name: ebs-claim
namespace: default
resourceVersion: "352552"
uid: a412b3be-a478-4942-bc25-0e82260647f8
csi:
driver: ebs.csi.aws.com
fsType: xfs
volumeAttributes:
storage.kubernetes.io/csiProvisionerIdentity: 1740028544849-6398-ebs.csi.aws.com
volumeHandle: vol-009addd418d3c6248
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2c
persistentVolumeReclaimPolicy: Delete
storageClassName: gp3
volumeMode: Filesystem
status:
lastPhaseTransitionTime: "2025-02-20T05:43:40Z"
phase: Bound
kind: List
metadata:
resourceVersion: ""
여기서 nodeAffinity 설정은 이 PV가 특정 노드(EC2 인스턴스)에서만 사용 가능하도록 제한하는 조건을 정의한 것입니다.
즉, 위 PV는 nodeAffinity 설정을 통해 ap-northeast-2c 가용 영역(AZ)에 있는 노드에서만 사용할 수 있도록 제한되며, topology.kubernetes.io/zone 키를 사용해 특정 AZ를 지정하고, operator: In 조건에 따라 해당 AZ의 노드에서만 바인딩될 수 있도록 설정됩니다.
# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt
#파드 내에서 볼륨 정보 확인
kubectl exec -it app -- sh -c 'df -hT --type=overlay'
kubectl exec -it app -- sh -c 'df -hT --type=xfs'
# 현재 pv 의 이름을 기준하여 .spec.resources.requests.storage 확인(4Gi)
kubectl get pvc ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
kubectl get pvc ebs-claim -o jsonpath={.status.capacity.storage} ; echo
#4G > 10G 로 변경
kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
# 확인 : 볼륨 용량 수정 반영이 되어야 되니, 수치 반영이 조금 느릴수 있습니다.
kubectl exec -it app -- sh -c 'df -hT --type=xfs'
kubectl df-pv
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
kubernetes에서 AWS EBS 볼륨의 스냅샷을 자동으로 생성, 관리, 복원할 수 있도록 AWS API와 연동하는 컨트롤러입니다. 이를 통해 사용자는 특정 시점의 EBS 데이터를 저장하고, 필요할 때 스냅샷을 기반으로 새로운 PersistentVolumeClaim(PVC)을 생성하여 복구할 수 있습니다. kubernetes의 VolumeSnapshot 리소스를 이용해 스냅샷을 관리하며, 이를 통해 백업 및 데이터 보호를 효율적으로 수행할 수 있습니다.
AWS 스냅샷을 kubernetes에서 관리하려면 VolumeSnapshot 관련 CRDs(Custom Resource Definition)를 먼저 설치해야 합니다.
# Install Snapshot CRDs
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl get crd | grep snapshot
kubectl api-resources | grep snapshot
다음 Common Snapshot Controller를 설치합니다.
# Install Common Snapshot Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl get deploy -n kube-system snapshot-controller
kubectl get pod -n kube-system
다음 Snapshotclass를 설치해줍니다.
# Install Snapshotclass
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml
kubectl get vsclass # 혹은 volumesnapshotclasses
kubectl describe vsclass
테스트
# PVC 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi
storageClassName: gp3
EOF
#확인
kubectl get pvc,pv
# 파드 생성
cat <<EOF | kubectl apply -f -
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
EOF
# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt
# VolumeSnapshot 생성 : Create a VolumeSnapshot referencing the PersistentVolumeClaim name
# AWS 관리 콘솔 EBS 스냅샷 확인
cat <<EOF | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: ebs-volume-snapshot
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: ebs-claim
EOF
# 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
# 강제로 장애 재현하기 위해 app & pvc 제거
kubectl delete pod app && kubectl delete pvc ebs-claim
스냅샷으로 복원
# 스냅샷에서 PVC 로 복원
kubectl get pvc,pv
cat <<EOF | kubectl apply -f -
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
EOF
# 확인
kubectl get pvc,pv
# 파드 생성
cat <<EOF | kubectl apply -f -
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-snapshot-restored-claim
EOF
AWS EFS Controller는 Kubernetes 환경에서 EFS를 자동으로 관리하는 컨트롤러로 AWS가 제공하는 Container Storage Interface(CSI) 드라이버를 활용하여, Kubernetes 클러스터에서 EFS 볼륨을 생성하고, 파드에 자동으로 연결해 줍니다.
EFS는 ReadWriteMany(RWX) 액세스 모드를 지원하기 때문에, 여러 개의 Kubernetes 파드가 동시에 동일한 EFS 볼륨을 읽고 쓸 수 있으며, 여러 애플리케이션이 공유 데이터나 로그를 저장하여 사용할 수 있습니다.
AWS EBS CSI와 마찬가지로 EKS에 add-on형태로 AWS에서 제공합니다.
AWS EFS CSI
AWS EFS CSI Driver 전제 버전 정보 확인
aws eks describe-addon-versions --addon-name aws-efs-csi-driver --kubernetes-version 1.31 --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" --output text
IRSA 설정
#생성
eksctl create iamserviceaccount --name efs-csi-controller-sa --namespace kube-system --cluster ${CLUSTER_NAME} --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy --approve --role-only --role-name AmazonEKS_EFS_CSI_DriverRole
#확인
eksctl get iamserviceaccount --cluster ${CLUSTER_NAME}
AWS EFS CSI Driver 설치(Add-On 설치)
export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
eksctl create addon --name aws-efs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/AmazonEKS_EFS_CSI_DriverRole --force
kubectl get sa -n kube-system efs-csi-controller-sa -o yaml | head -5
#확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get pod -n kube-system -l "app.kubernetes.io/name=aws-efs-csi-driver,app.kubernetes.io/instance=aws-efs-csi-driver"
kubectl get pod -n kube-system -l app=efs-csi-controller -o jsonpath='{.items[0].spec.containers[*].name}' ; echo
kubectl get csidrivers efs.csi.aws.com -o yaml
EFS 파일시스템을 파드가 사용하게 설정하기
EC2 서버에서 실습을 위한 코드를 준비합니다.
# 실습 코드 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
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
kubectl apply -f pv.yaml
kubectl get pv; kubectl describe pv
PVC 생성 및 확인
kubectl apply -f claim.yaml
kubectl get pvc
Pod 생성 및 연동
# 파드 내에 /data 데이터는 EFS를 사용
# 추후에 파드1,2가 각기 다른 노드에 배포되게 추가해두자!
cat pod1.yaml pod2.yaml
kubectl apply -f pod1.yaml,pod2.yaml
# 파드 정보 확인
kubectl get pods
kubectl exec -ti app1 -- sh -c "df -hT -t nfs4"
kubectl exec -ti app2 -- sh -c "df -hT -t nfs4"
공유 저장소 저장 동작 확인
tree /mnt/myefs # EC2 에서 확인
tail -f /mnt/myefs/out1.txt # EC2 에서 확인
tail -f /mnt/myefs/out2.txt # EC2 에서 확인
kubectl exec -ti app1 -- tail -f /data/out1.txt
kubectl exec -ti app2 -- tail -f /data/out2.txt
실습이 완료되면 생성한 리소스는 삭제해 주시면됩니다.
kubectl delete pod app1 app2
kubectl delete pvc efs-claim && kubectl delete pv efs-pv && kubectl delete sc efs-sc
EFS 파일시스템을 다수의 파드가 사용하게 설정하기
마찬가지로 실습을 위한 코드를 준비합니다.
# EFS 스토리지클래스 생성 및 확인
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml
cat storageclass.yaml
sed -i "s/fs-92107410/$EfsFsId/g" storageclass.yaml
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc
# PVC/파드 생성 및 확인
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/pod.yaml
cat pod.yaml
kubectl apply -f pod.yaml
kubectl get pvc,pv,pod
PVC/PV 생성 로그 확인
#stern 플러그인 설치
kubectl krew install stern
#실사간 로그 확인
kubectl stern -n kube-system -l app=efs-csi-controller -c csi-provisioner
#또는
kubectl logs -n kube-system -l app=efs-csi-controller -c csi-provisioner -f
#Pod 정보 확인
kubectl exec -it efs-app -- sh -c "df -hT -t nfs4"
EFS Access Point
보통 기본 EFS 파일 시스템은 전체 파일 시스템을 마운트하지만, EFS Access Point를 사용하면 EFS의 특정 부분을 격리하고, UID/GID를 강제하여 보안성을 높일 수 있습니다.
EFS의 액세스 포인트에서 확인이 가능합니다.
AWS EKS에서 Instance Store가 포함된 EC2 인스턴스를 사용하면, 해당 로컬 디스크를 Persistent Volume(PV)으로 설정하여 사용할 수 있습니다.
Instance Store는 기본적으로 EC2 인스턴스에 직접 연결된 초고속 NVMe SSD 또는 HDD이며, 일반적인 EBS(Elastic Block Store)와 달리 인스턴스가 종료되면 데이터 손실이 발생하게 됩니다.
참고: Instance Store는 EC2 스토리지(EBS) 정보에 출력되지 않습니다.
Instance Store를 지원하는 EC2 인스턴스 타입 목록
인스턴스 타입 | 스토리지 유형 | 설명 |
c5d | NVMe SSD | c5 인스턴스의 디스크 버전, 고속 로컬 스토리지 제공 |
c6gd | NVMe SSD | graviton2 기반, 비용 최적화된 컴퓨팅 워크로드 |
EKS에서 Instance Store를 Persistent Volume(PV)으로 사용하고, 새로운 노드 그룹(NodeGroup)을 추가하는 실습을 진행해 보겠습니다.
# 실습에서 사용할 Instance Store 볼륨이 있는 c5 모든 타입의 스토리지 크기 확인
aws ec2 describe-instance-types --filters "Name=instance-type,Values=c5*" "Name=instance-storage-supported,Values=true" --query "InstanceTypes[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" --output table
신규 노드 그룹 생성
cat << EOF > myng2.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: myeks
region: ap-northeast-2
version: "1.31"
managedNodeGroups:
- amiFamily: AmazonLinux2
desiredCapacity: 1
instanceType: c5d.large
labels:
alpha.eksctl.io/cluster-name: myeks
alpha.eksctl.io/nodegroup-name: ng2
disk: instancestore
maxPodsPerNode: 110
maxSize: 1
minSize: 1
name: ng2
ssh:
allow: true
publicKeyName: $SSHKEYNAME
subnets:
- $PubSubnet1
- $PubSubnet2
- $PubSubnet3
tags:
alpha.eksctl.io/nodegroup-name: ng2
alpha.eksctl.io/nodegroup-type: managed
volumeIOPS: 3000
volumeSize: 30
volumeThroughput: 125
volumeType: gp3
preBootstrapCommands:
- |
# Install Tools
yum install nvme-cli links tree jq tcpdump sysstat -y
# Filesystem & Mount
mkfs -t xfs /dev/nvme1n1
mkdir /data
mount /dev/nvme1n1 /data
# Get disk UUID
uuid=\$(blkid -o value -s UUID mount /dev/nvme1n1 /data)
# Mount the disk during a reboot
echo /dev/nvme1n1 /data xfs defaults,noatime 0 2 >> /etc/fstab
EOF
# 신규 노드 그룹 생성
eksctl create nodegroup -f myng2.yaml
# 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get node -l disk=instancestore
# ng2 노드 그룹 *ng2-remoteAccess* 포함된 보안그룹 ID
aws ec2 describe-security-groups --filters "Name=group-name,Values=*ng2-remoteAccess*" | jq
export NG2SGID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=*ng2-remoteAccess*" --query 'SecurityGroups[*].GroupId' --output text)
# 보안그룹에 정책 추가
aws ec2 authorize-security-group-ingress --group-id $NG2SGID --protocol '-1' --cidr $(curl -s ipinfo.io/ip)/32
aws ec2 authorize-security-group-ingress --group-id $NG2SGID --protocol '-1' --cidr 172.20.1.100/32
# 확인
ssh ec2-user@$N4 sudo nvme list
ssh ec2-user@$N4 sudo lsblk -e 7 -d
ssh ec2-user@$N4 sudo df -hT -t xfs
ssh ec2-user@$N4 sudo tree /data
ssh ec2-user@$N4 sudo cat /etc/fstab
# (옵션) max-pod 확인
kubectl describe node -l disk=instancestore | grep Allocatable: -A7
# (옵션) kubelet 데몬 파라미터 확인 : --max-pods=29 --max-pods=110
ssh ec2-user@$N4 cat /etc/eks/bootstrap.sh
ssh ec2-user@$N4 sudo ps -ef | grep kubelet
#Local Path Provisioner 설치 및 기본 경로 변경
curl -sL https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml | sed 's/opt/data/g' | kubectl apply -f -
#local-path-storage의 ConfigMap 상세 정보 확인
kubectl describe cm -n local-path-storage local-path-config
# Read 측정을 위한 fio 파일 생성
cat << EOF > fio-read.fio
> [global]
> ioengine=libaio
> direct=1
> bs=4k
> runtime=120
> time_based=1
> iodepth=16
> numjobs=4
> group_reporting
> size=1g
> rw=randread
> [read]
> EOF
#Read성능 측정
kubestr fio -f fio-read.fio -s local-path --size 10G --nodeselector disk=instancestore
테스트 완료되었다면 생성하였던 자원은 삭제해주시면 됩니다.
# local-path 스토리지 클래스 삭제
kubectl delete -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml
# ng2 노드그룹 삭제
eksctl delete nodegroup -c $CLUSTER_NAME -n ng2
5주차 Study - Autoscaling (0) | 2025.03.09 |
---|---|
4주차 Study - EKS Observability (0) | 2025.03.01 |
2주차 Study - EKS Networking (2) (0) | 2025.02.15 |
2주차 Study - EKS Networking (1) (1) | 2025.02.15 |
2주차 Study - EKS Networking (실습 환경 구축) (0) | 2025.02.15 |