본 실습에서는 AWS EKS 환경을 배포하고, 스토리지 및 노드 그룹을 설정합니다. 실습 환경은 AWS CloudFormation 및 eksctl을 활용하여 자동으로 구축되며, 이후 다양한 스토리지 및 네트워크 구성을 실습합니다.
EKS 실습 환경은 다음과 같은 구조를 가집니다:

# CloudFormation YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-3week.yaml
# CloudFormation 스택 배포
aws cloudformation deploy --template-file myeks-3week.yaml \
--stack-name myeks \
--parameter-overrides KeyName=eks-key SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 \
--region ap-northeast-2
# 배포된 운영서버 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks \
--query 'Stacks[*].Outputs[*].OutputValue' --output text
# SSH로 운영서버 접속
ssh -i <ssh 키파일> ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)



export CLUSTER_NAME=myeks
# VPC 및 서브넷 정보 확인
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
export PubSubnet1=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)

cat << EOF > myeks.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: myeks
region: ap-northeast-2
version: "1.31"
iam:
withOIDC: true # enables the IAM OIDC provider as well as IRSA for the Amazon CNI plugin
serviceAccounts: # service accounts to create in the cluster. See IAM Service Accounts
- metadata:
name: aws-load-balancer-controller
namespace: kube-system
wellKnownPolicies:
awsLoadBalancerController: true
vpc:
cidr: 192.168.0.0/16
clusterEndpoints:
privateAccess: true # if you only want to allow private access to the cluster
publicAccess: true # if you want to allow public access to the cluster
id: $VPCID
subnets:
public:
ap-northeast-2a:
az: ap-northeast-2a
cidr: 192.168.1.0/24
id: $PubSubnet1
ap-northeast-2b:
az: ap-northeast-2b
cidr: 192.168.2.0/24
id: $PubSubnet2
ap-northeast-2c:
az: ap-northeast-2c
cidr: 192.168.3.0/24
id: $PubSubnet3
addons:
- name: vpc-cni # no version is specified so it deploys the default version
version: latest # auto discovers the latest available
attachPolicyARNs: # attach IAM policies to the add-on's service account
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
configurationValues: |-
enableNetworkPolicy: "true"
- name: kube-proxy
version: latest
- name: coredns
version: latest
- name: metrics-server
version: latest
managedNodeGroups:
- amiFamily: AmazonLinux2023
desiredCapacity: 3
iam:
withAddonPolicies:
certManager: true # Enable cert-manager
externalDNS: true # Enable ExternalDNS
instanceType: t3.medium
preBootstrapCommands:
# install additional packages
- "dnf install nvme-cli links tree tcpdump sysstat ipvsadm ipset bind-utils htop -y"
labels:
alpha.eksctl.io/cluster-name: myeks
alpha.eksctl.io/nodegroup-name: ng1
maxPodsPerNode: 100 # t3.medium 기본 설정인 17개보다 많은 Pod 배포 가능
maxSize: 3
minSize: 3
name: ng1
ssh:
allow: true # 이 설정을 활성화하면 클러스터 생성 시 자동으로 원격 접속이 가능한 보안 그룹이 생성됨
publicKeyName: $SSHKEYNAME
tags:
alpha.eksctl.io/nodegroup-name: ng1
alpha.eksctl.io/nodegroup-type: managed
volumeIOPS: 3000
volumeSize: 120
volumeThroughput: 125
volumeType: gp3
EOF

eksctl create cluster -f myeks.yaml --verbose 4


kubectl cluster-info
# 네임스페이스 default 변경 적용
kubens default
kubectl ctx
kubectl config rename-context "<각자 자신의 IAM User>@myeks.ap-northeast-2.eksctl.io" "eksworkshop"
kubectl config rename-context "admin@myeks.ap-northeast-2.eksctl.io" "eksworkshop"
kubectl get nodes --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get nodes -v=6
kubectl get pods -A
kubectl get pdb -n kube-system
# 관리형 노드 그룹 확인
eksctl get nodegroup --cluster $CLUSTER_NAME
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng1 | jq
# eks addon 확인
eksctl get addon --cluster $CLUSTER_NAME
# aws-load-balancer-controller를 위한 iam service account 생성 확인 : AWS IAM role bound to a Kubernetes service account
eksctl get iamserviceaccount --cluster $CLUSTER_NAME


EC2 IAM role 확인

# 인스턴스 정보 확인 1: 전체 노드 목록 조회
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# EC2 인스턴스 정보 확인: AZ, ID, 공인IP 출력
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=myeks-ng1-Node" \
--query "Reservations[].Instances[].{InstanceID:InstanceId, PublicIP:PublicIpAddress, AZ:Placement.AvailabilityZone}" \
--output table
# AZ별 공인 IP 확인
# AZ1 배치된 EC2 공인 IP
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" \
--query 'Reservations[*].Instances[*].PublicIpAddress' \
--output text
# AZ2 배치된 EC2 공인 IP
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" \
--query 'Reservations[*].Instances[*].PublicIpAddress' \
--output text
# AZ3 배치된 EC2 공인 IP
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" \
--query 'Reservations[*].Instances[*].PublicIpAddress' \
--output text
# 공인 IP를 변수로 저장
export N1=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N2=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N3=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
echo $N1, $N2, $N3


SSH 접속을 위해 집 공인 IP 및 운영 서버 IP를 보안 그룹(remoteAccess) 인바운드 규칙에 추가합니다.
# 'remoteAccess' 포함된 관리형 노드 그룹의 보안 그룹 ID 확인
aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" | jq
export MNSGID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" --query 'SecurityGroups[*].GroupId' --output text)
# 해당 보안그룹 inbound에 집 공인 IP 허용
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr $(curl -s ipinfo.io/ip)/32
# 해당 보안그룹 inbound에 운영 서버 내부 IP 허용
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr 172.20.1.100/32
# ping 테스트
ping -c 2 $N1
ping -c 2 $N2
ping -c 2 $N3
# SSH 접속 확인
ssh -i <SSH 키> -o StrictHostKeyChecking=no ec2-user@$N1 hostname
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$i hostname; echo; done
ssh ec2-user@$N1
exit
ssh ec2-user@$N2
exit
ssh ec2-user@$N2
exit


트러블슈팅
현상
(eksworkshop:default) [root@operator-host ~]# for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$i hostname; echo; done
>> node 3.34.49.155 <<
Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
>> node 3.38.196.249 <<
Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
>> node 52.79.226.243 <<
Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

해결
# SSH Agent 실행
eval $(ssh-agent)
# 키 등록
ssh-add ~/.ssh/eks-key.pem
# 등록된 키 확인
ssh-add -l
# 영구 적용 시
vi ~/.bashrc
# SSH Agent 자동 실행 및 키 추가
if [ -z "$SSH_AUTH_SOCK" ]; then
eval $(ssh-agent -s)
ssh-add ~/.ssh/eks-key.pem
fi

# 노드의 기본 정보 출력
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i hostnamectl; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
# 스토리지 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i lsblk; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i df -hT /; echo; done
# 스토리지 클래스 및 CSI 노드 정보 확인
kubectl get sc
kubectl describe sc gp2
kubectl get crd
kubectl get csinodes



⚠️ 참고
gp3는 gp2보다 보장된 최소 IOPS가 높고 비용이 절감됩니다.
# 노드별 max-pods 값 확인
kubectl describe node | grep Capacity: -A13
kubectl get nodes -o custom-columns="NAME:.metadata.name,MAXPODS:.status.capacity.pods"
# 노드에서 확인
ssh ec2-user@$N1 sudo cat /etc/kubernetes/kubelet/config.json | jq
# 각 노드에서 maxPods 기본 값 확인 (17)
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /etc/kubernetes/kubelet/config.json | grep maxPods; echo; done
# 사용자 지정 maxPods 값 확인 (100) (config.json.d/00-nodeadm.conf에서 오버레이 적용)
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /etc/kubernetes/kubelet/config.json.d/00-nodeadm.conf | grep maxPods; echo; done

⚠️ 참고
AmazonLinux2에서는 초기 노드 세팅 관련 여러 절차가 정의된 /etc/eks/bootstrap.sh 파일을 사용했습니다.
for i in N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@i cat /etc/eks/bootstrap.sh; echo; done
-> AmazonLinux2023에서는 사용X
운영 서버에서 EKS 클러스터를 제어하기 위해 kubeconfig 설정을 진행합니다.
# IAM 자격 증명 설정
aws configure
# get-caller-identity로 현재 IAM 사용자 확인
aws sts get-caller-identity --query Arn
# kubeconfig 생성 및 적용
aws eks update-kubeconfig --name myeks --user-alias admin
# 클러스터 정보 확인
kubectl cluster-info
kubectl ns default
kubectl get node -v6

운영 서버가 VPC Peering을 통해 EFS를 원격 마운트하여 스토리지를 사용하는지 테스트합니다. 이를 통해 운영 서버에서 EFS에 접근하고 데이터를 저장 및 조회할 수 있는지 확인합니다.
먼저, AWS EFS의 파일 시스템 및 마운트 타겟 정보를 조회합니다.
# 현재 EFS 정보 확인
aws efs describe-file-systems | jq
# 파일 시스템 ID만 출력
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text
# EFS 마운트 타겟 확인
aws efs describe-mount-targets --file-system-id $(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text) | jq
# EFS 마운트 타겟 확인 (IP 주소만 출력)
aws efs describe-mount-targets --file-system-id $(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text) --query "MountTargets[*].IpAddress" --output text


EFS에는 3개의 네트워크 인터페이스(ENI)가 설정되어 있으며, 이 중 하나를 선택하여 마운트할 수 있습니다.
EFS는 일반적으로 Private DNS를 통해 네트워크 내부에서 접근이 가능합니다.
VPC 내에서는 fs-xxxxxx.efs.ap-northeast-2.amazonaws.com 같은 도메인을 사용하여 연결됩니다.
그러나 운영 서버가 같은 VPC에 있지 않은 경우, DNS 조회가 실패할 수 있습니다.
# EFS 도메인으로 DNS 질의
dig +short $(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text).efs.ap-northeast-2.amazonaws.com

✅ DNS 조회 실패 시 원인
📌 해결 방법
EFS는 NFS(Network File System) 프로토콜을 사용하여 EC2에서 공유 스토리지로 마운트할 수 있습니다.
# EFS 마운트
EFSIP1=<EFS 마운트 타겟 중 하나>
EFSIP1=192.168.1.71
# 현재 마운트된 파일 시스템 확인
df -hT
# 마운트할 디렉터리 생성
mkdir /mnt/myefs
# EFS 마운트 실행
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport $EFSIP1:/ /mnt/myefs
# 마운트 확인
findmnt -t nfs4
df -hT --type nfs4
✅ 결과 확인

EFS가 정상적으로 마운트되었는지 테스트하기 위해 파일을 생성하고 저장된 데이터를 확인합니다.
# NFS 요청 통계 확인
nfsstat
# EFS에 데이터 저장
echo "EKS Workshop" > /mnt/myefs/memo.txt
# 다시 NFS 요청 통계 확인 (NFS를 통해 요청 발생)
nfsstat
# 저장된 파일 확인
ls -l /mnt/myefs
cat /mnt/myefs/memo.txt

✅ 결과 확인
파일이 정상적으로 저장되었으며, nfsstat을 통해 NFS 트래픽이 발생한 것을 확인할 수 있습니다.
운영 서버를 재부팅한 이후에도 EFS가 자동으로 마운트되도록 설정해야 합니다.
이를 위해 /etc/fstab 파일을 수정하여 EFS를 영구적으로 마운트합니다.
# 기존 fstab 백업
sudo cp /etc/fstab /etc/fstab.backup
# EFS 영구 마운트 설정 추가
echo "$EFSIP1:/ /mnt/myefs nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport 0 0" | sudo tee -a /etc/fstab
# /etc/fstab 파일 내용 확인
cat /etc/fstab

VPC Peering 환경에서는 efs.ap-northeast-2.amazonaws.com 같은 DNS 주소 대신 IP 주소(192.168.1.20:/)를 사용해야 합니다.
# 마운트 적용 테스트
sudo mount -a
df -hT --type nfs4
✅ 설정이 정상적으로 완료되었는지 확인하는 방법

EKS에서 ALB Ingress, 자동 DNS 등록, Kubernetes 리소스 모니터링 도구를 설치하고 설정합니다.
# 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 service.main.type=ClusterIP --set env.TZ="Asia/Seoul" --namespace kube-system
# AWS LoadBalancerController 설치
helm repo add eks https://aws.github.io/eks-charts
helm repo update
kubectl get sa -n kube-system aws-load-balancer-controller
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=<자신의 도메인>
MyDomain=gasida.link
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)
curl -s https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml | MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst | kubectl apply -f -

AWS Certificate Manager(ACM)을 사용하여 HTTPS 트래픽을 허용하도록 ALB Ingress를 설정합니다.
먼저, 해당 리전에 발급된 SSL/TLS 인증서(ACM)가 있는지 확인하고 ARN을 가져옵니다.
# 사용 리전의 인증서 ARN 확인
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
echo $CERT_ARN

인증서가 없는 경우 AWS Certificate Manager에서 새로 발급해야 합니다.
Ingress 그룹을 사용하여 여러 Ingress 리소스가 하나의 ALB를 공유하도록 설정합니다.
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/target-type: ip
labels:
app.kubernetes.io/name: kubeopsview
name: kubeopsview
namespace: kube-system
spec:
ingressClassName: alb
rules:
- host: kubeopsview.$MyDomain
http:
paths:
- backend:
service:
name: kube-ops-view
port:
number: 8080
path: /
pathType: Prefix
EOF
ALB Ingress를 설정하여 kubeopsview.$MyDomain 도메인에서 HTTPS로 접근 가능하도록 설정합니다.

설치된 리소스가 정상적으로 동작하는지 확인합니다.
# 설치된 파드 확인
kubectl get pods -n kube-system
# 서비스, 엔드포인트, Ingress 확인
kubectl get ingress,svc,ep -n kube-system
# Kube Ops View URL 출력
echo -e "Kube Ops View URL = https://kubeopsview.$MyDomain/#scale=1.5"
# macOS에서는 자동으로 브라우저 열기
open "https://kubeopsview.$MyDomain/#scale=1.5"

kubeopsview.$MyDomain 도메인으로 HTTPS를 통해 Kubernetes 리소스 모니터링 화면을 확인할 수 있습니다.
EKS 환경에서의 스토리지 활용 방식과 퍼시스턴트 볼륨(PV, PVC)을 통한 데이터 영속성 관리 방법을 정리합니다.
쿠버네티스에서 파드 내부의 데이터는 파드가 삭제되면 모두 사라지며, 기본적으로 Stateless(상태가 없는) 애플리케이션으로 동작합니다.
즉, 기본적으로 임시 파일 시스템(Temporary Filesystem)을 사용합니다.
📌 예제: Pod 내부의 임시 파일 저장 방식
kubectl exec -it redis -- sh -c "echo 'hello' > /data/hello.txt"
kubectl exec -it redis -- cat /data/hello.txt
볼륨 : emptyDir, hostPath, PV/PVC

다양한 볼륨 사용 : K8S 자체 제공(hostPath, local), 온프렘 솔루션(ceph 등), NFS, 클라우드 스토리지(AWS EBS 등)

동적 프로비저닝 & 볼륨 상태 , ReclaimPolicy

Kubernetes는 다양한 스토리지 솔루션을 지원하기 위해 Container Storage Interface(CSI)라는 표준 인터페이스를 제공합니다. CSI를 통해 다양한 스토리지 벤더가 Kubernetes에서 사용할 수 있는 스토리지 플러그인(Driver)을 개발할 수 있으며, Kubernetes의 자체 코드 변경 없이 새로운 스토리지 솔루션을 통합할 수 있습니다.
Kubernetes는 네트워크(CNI)와 마찬가지로 스토리지 관련 표준 인터페이스(CSI)를 만들어, 여러 벤더들이 자신들의 스토리지 솔루션을 Kubernetes에 쉽게 연동할 수 있도록 지원합니다.
📌 CSI 도입 배경
📌 CSI의 주요 특징
kubectl get csinodes 명령어를 통해 각 노드의 CSI 드라이버 정보를 확인 가능AWS EBS CSI 드라이버는 EBS를 Kubernetes의 퍼시스턴트 볼륨(PV)으로 사용할 수 있도록 지원하는 CSI 드라이버입니다.
이 드라이버를 사용하면 EBS 볼륨을 자동으로 프로비저닝하고, Kubernetes 파드에서 사용할 수 있도록 관리할 수 있습니다.
📌 EBS CSI 드라이버의 주요 기능
📌 EBS CSI 드라이버 구조
AWS EBS CSI 드라이버는 크게 두 가지 주요 컴포넌트로 구성됩니다.
Controller Pod (StatefulSet/Deployment)
Node Pod (DaemonSet)
📌 EBS CSI 드라이버의 동작 과정
📌 EBS CSI 드라이버 아키텍처


📌 EBS CSI 드라이버 설치 및 확인
# EBS CSI 드라이버 설치
kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/ecr/?ref=master"
# 설치된 CSI 드라이버 확인
kubectl get csinodes

Kubernetes에서 노드에 연결할 수 있는 스토리지 볼륨 개수는 EC2 인스턴스 유형에 따라 다릅니다.
각 인스턴스 유형에 따라 최대 연결 가능한 EBS 볼륨 개수가 제한되며, Kubernetes에서 이를 적용하여 볼륨 부착 한도를 관리합니다.
AWS EC2 인스턴스의 유형에 따라 노드가 지원할 수 있는 최대 볼륨 개수는 다음과 같이 다릅니다.
🔗 참고 자료:
EBS CSI 드라이버가 배포된 경우, 각 노드가 지원하는 최대 볼륨 개수를 kubectl describe csinodes 명령어로 확인할 수 있습니다.
📌 T3.medium 인스턴스의 볼륨 한도 확인
kubectl describe csinodes

Kubernetes에서 파드는 기본적으로 Stateless(무상태) 방식으로 동작하며, 파드 내부의 데이터는 삭제 시 모두 유실됩니다.
따라서, 데이터를 유지하려면 볼륨을 마운트해야 하며, 이를 위해 emptyDir 볼륨과 같은 Kubernetes의 기본적인 볼륨을 사용할 수 있습니다.
기본적으로 파드 내부의 저장소는 파드가 재시작되거나 삭제될 경우 데이터가 사라집니다. 이를 실습을 통해 확인합니다.
🔗 참고 자료: Pod Storage 설정 공식 문서
# 파드 상태 모니터링
kubectl get pod -w
📌 Redis 파드 생성 (볼륨 미사용)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
terminationGracePeriodSeconds: 0
containers:
- name: redis
image: redis
EOF
📌 파드 내부에서 파일 생성 후 데이터 확인
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 프로세스를 강제 종료하면 파드는 유지되지만 컨테이너가 재시작됨
kubectl exec -it redis -- sh -c "apt update && apt install procps -y"
kubectl exec -it redis -- ps aux
kubectl exec -it redis -- kill 1
kubectl get pod
📌 재시작된 컨테이너에서 파일 확인
kubectl exec -it redis -- cat /data/hello.txt
kubectl exec -it redis -- ls -l /data
# => 파일이 없음 (데이터가 유실됨)
❗ 컨테이너가 재시작되면서 데이터가 삭제됨
❗ 파드를 삭제하고 다시 실행하면 기존 데이터는 유지되지 않음

📌 파드 삭제
kubectl delete pod redis
emptyDir은 파드가 생성될 때 자동으로 생성되는 임시 저장소이며, 파드가 삭제되면 볼륨도 사라집니다.
하지만 컨테이너가 재시작되는 경우(restartPolicy) emptyDir의 데이터는 유지됩니다.
📌 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

📌 emptyDir 볼륨이 redis-storage라는 이름으로 생성되었으며, /data/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
kubectl exec -it redis -- kill 1
kubectl get pod

✔ 파드는 종료되지 않고, 컨테이너만 재시작됨
✔ 파일이 유지됨 (emptyDir은 파드의 라이프사이클 동안 유지됨)
kubectl exec -it redis -- cat /data/redis/hello.txt
kubectl exec -it redis -- ls -l /data/redis
# => 파일이 존재함 ✅

📌 파드 삭제 후 데이터 확인
❗ emptyDir 볼륨은 파드가 삭제되면 데이터가 사라짐
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

📌 파일이 유지되는지 확인
kubectl exec -it redis -- cat /data/redis/hello.txt
kubectl exec -it redis -- ls -l /data/redis
# => 파일이 없음 ❌

❗ 새로운 파드가 생성되면서 emptyDir도 새롭게 초기화됨
❗ emptyDir 볼륨은 파드가 삭제되면 함께 삭제됨
Kubernetes에서 특정 노드의 로컬 디렉토리를 볼륨으로 활용하기 위해 local-path-provisioner를 사용할 수 있습니다.
이는 스토리지가 물리적으로 특정 노드에 종속되는 경우에 유용하며, 특정 노드에서만 접근할 수 있는 로컬 스토리지를 관리하는 방식입니다.
🔗 관련 자료
기본적으로 각 노드의 특정한 호스트 디렉토리를 동적 디렉터리명으로 마운트해주는 컨트롤러입니다.
# local-path-provisioner 배포
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml

📌 배포된 StorageClass(local-path) 및 설정 확인:
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 describe sc local-path



✔ local-path StorageClass는 WaitForFirstConsumer 정책을 사용하여, 파드가 생성될 때까지 실제 볼륨이 생성되지 않습니다.
스토리지를 요청하는 PVC(Persistent Volume Claim)를 먼저 생성합니다.
watch -d kubectl get pv,pvc,pod
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: localpath-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
EOF

📌 PVC 상태 확인:
kubectl get pvc
kubectl describe pvc

✔ Storage Class 설정이 VOLUMEBINDINGMODE: WaitForFirstConsumer 으로 Pending 상태가 유지되며, 파드가 생성될 때까지 PVC가 바인딩되지 않음.
📌 PVC를 사용하는 파드 생성
PVC를 연결하여 데이터를 저장하는 app 파드를 실행합니다.
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 확인:
kubectl get pod,pv,pvc
kubectl describe pv

✔ localpath-claim이 자동으로 바인딩되며, 특정 노드에 디렉토리가 생성됨.
📌 파드 내부에서 생성된 파일 확인:
kubectl exec -it app -- tail -f /data/out.txt

📌 특정 워커 노드의 호스트 경로에서 확인:
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done

✔ 파일이 특정 노드에만 저장됨.
📌 해당 노드에서 직접 데이터 확인:
ssh ec2-user@$N1 tail -f /opt/local-path-provisioner/pvc-xxxxxx_default_localpath-claim/out.txt

PVC를 유지한 상태에서 파드를 삭제 후 재생성하여 데이터가 유지되는지 확인합니다.
📌 파드 삭제
kubectl delete pod app
kubectl get pod,pv,pvc
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done

✔ PVC와 PV는 남아있음, 데이터도 그대로 유지됨.
📌 파드 재배포
kubectl apply -f - <<EOF
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

✔ 파일이 유지됨 → local-path-provisioner를 활용하여 데이터 영속성이 보장됨.
다음 실습을 위해 생성한 PVC 및 관련 리소스를 삭제합니다.
📌 파드 및 PVC 삭제
kubectl delete pod app
kubectl get pv,pvc
kubectl delete pvc localpath-claim

📌 호스트 디렉토리 데이터 삭제 확인
kubectl get pv
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done

✔ PVC 삭제 시 관련 PV도 자동 삭제되며, 호스트 디렉토리에서도 데이터가 삭제됨.
Kubestr는 Kubernetes에서 다양한 스토리지 성능을 벤치마킹하는 도구입니다.

# Kubestr 다운로드 및 설치
wget https://github.com/kastenhq/kubestr/releases/download/v0.4.48/kubestr_0.4.48_Linux_amd64.tar.gz
tar xvfz kubestr_0.4.48_Linux_amd64.tar.gz && mv kubestr /usr/local/bin/ && chmod +x /usr/local/bin/kubestr
# Kubestr 실행 가능 여부 확인
kubestr -h
kubestr

# 사용 가능한 스토리지 클래스 조회
kubestr

✅ 스토리지 클래스별로 속도를 측정하고, 최적의 옵션을 선택할 수 있습니다.
# 랜덤 읽기 성능 테스트 설정 파일 생성
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
# 랜덤 읽기 성능 테스트 실행. size 미 지정시 기본 100G로 노드 Disk full 발생하니 유의
kubestr fio -f fio-read.fio -s local-path --size 10G

📌 NVMe SSD 환경에서는 평균 IOPS가 약 20,300 수준
# 랜덤 쓰기 성능 테스트 설정 파일 생성
cat << EOF > fio-write.fio
[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
[write]
EOF
# 랜덤 쓰기 성능 테스트 실행. size 미 지정시 기본 100G로 노드 Disk full 발생하니 유의
kubestr fio -f fio-write.fio -s local-path --size 20G

📌 EBS gp3 대비 NVMe SSD의 성능이 4배 이상 높으며, 고성능 워크로드에서는 NVMe 기반의 스토리지를 고려하는 것이 유리합니다.