[AWS EKS] EKS Storage-1

주영·2025년 2월 22일
0

EKS 실습 환경 배포

1. 개요

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

2. 최종 구성도

EKS 실습 환경은 다음과 같은 구조를 가집니다:

  • 2개의 VPC: EKS 배포용(myeks-vpc)과 운영용(operator-vpc)으로 구분
  • myeks-vpc 에 각기 AZ를 사용하는 퍼블릭/프라이빗 서브넷 배치
  • 로드밸런서 배포를 위한 퍼블릭/프라이빗 서브넷에 태그 설정
  • EFS 추가: myeks-vpc의 Public 서브넷에 배포
  • VPC Peering 설정: 내부 통신을 위한 VPC 연결 구성
  • Node Group 배포: EKS 클러스터 내 관리형 노드 그룹 생성

3. 실습 환경 배포

3.1 CloudFormation을 활용한 기본 환경 배포

# 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)

3.2 eksctl을 활용한 EKS 클러스터 배포

3.2.1 환경 변수 설정

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)

3.2.2 EKS 클러스터 YAML 파일 작성

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

3.2.3 클러스터 생성

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

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 확인

5. 관리형 노드 그룹 접속 및 정보 확인

5.1 EC2 인스턴스 정보 확인 및 SSH 접속

# 인스턴스 정보 확인 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

5.2 보안 그룹 인바운드 규칙 추가

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

5.3 노드 정보 확인 및 max-pods 확인

5.3.1 노드 기본정보 조회

# 노드의 기본 정보 출력
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가 높고 비용이 절감됩니다.

5.3.2 max-pods 정보 확인

# 노드별 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

6. 운영 서버 EC2에서 EKS kubeconfig 설정 및 EFS 마운트 테스트

6.1 EKS kubeconfig 설정

운영 서버에서 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

6.2 EFS 마운트 테스트

운영 서버가 VPC Peering을 통해 EFS를 원격 마운트하여 스토리지를 사용하는지 테스트합니다. 이를 통해 운영 서버에서 EFS에 접근하고 데이터를 저장 및 조회할 수 있는지 확인합니다.

1️⃣ 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)가 설정되어 있으며, 이 중 하나를 선택하여 마운트할 수 있습니다.

2️⃣ DNS 질의 테스트 (EFS 도메인 확인)

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 조회 실패 시 원인

  • 같은 VPC 내라면 Private Hosted Zone을 통해 정상적으로 응답이 되지만,
  • 다른 VPC에서 VPC Peering을 통해 접근하는 경우, Private Hosted Zone을 사용할 수 없어 DNS 조회가 실패할 수 있음

📌 해결 방법

  • DNS 수동 설정: EFS 마운트 타겟의 IP 주소를 직접 사용
  • AWS PrivateLink 활용: EFS에 PrivateLink를 설정하여 Cross-VPC 접근 허용
  • Route 53 Private Hosted Zone 공유: 동일한 Private DNS 설정을 활용

3️⃣ EFS 마운트 테스트 (NFS 기반 마운트)

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

✅ 결과 확인

  • /mnt/myefs에 EFS가 정상적으로 마운트되었는지 확인
  • df -hT --type nfs4 명령어 실행 시, 마운트된 EFS가 nfs4 타입으로 표시되어야 합니다.

4️⃣ EFS에 데이터 저장 및 확인

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 트래픽이 발생한 것을 확인할 수 있습니다.

5️⃣ 운영 서버에서 EFS를 영구 마운트하기

운영 서버를 재부팅한 이후에도 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

✅ 설정이 정상적으로 완료되었는지 확인하는 방법

  • /mnt/myefs가 유지되는지 확인
  • df -hT --type nfs4 실행 시 nfs4 타입으로 마운트된 것이 유지되는지 확인

7. AWS LoadBalancerController, ExternalDNS, kube-ops-view 설치

EKS에서 ALB Ingress, 자동 DNS 등록, Kubernetes 리소스 모니터링 도구를 설치하고 설정합니다.

7.1 AWS LoadBalancerController, ExternalDNS, kube-ops-view 설치

  • kube-ops-view: 쿠버네티스 클러스터의 리소스를 모니터링할 수 있는 도구
  • AWS LoadBalancerController: ALB Ingress를 관리
  • ExternalDNS: Route 53을 이용한 자동 DNS 등록을 위한 도구
# 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 -

7.2 HTTPS 통신을 위한 ALB Ingress 설정

AWS Certificate Manager(ACM)을 사용하여 HTTPS 트래픽을 허용하도록 ALB Ingress를 설정합니다.

7.2.1 ACM 인증서 확인

먼저, 해당 리전에 발급된 SSL/TLS 인증서(ACM)가 있는지 확인하고 ARN을 가져옵니다.

# 사용 리전의 인증서 ARN 확인
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
echo $CERT_ARN

인증서가 없는 경우 AWS Certificate Manager에서 새로 발급해야 합니다.

7.2.2 kube-ops-view Ingress 설정

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로 접근 가능하도록 설정합니다.

7.3 배포 결과 확인

설치된 리소스가 정상적으로 동작하는지 확인합니다.

# 설치된 파드 확인
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 리소스 모니터링 화면을 확인할 수 있습니다.

8. EKS 스토리지 개념 및 PV 관리

EKS 환경에서의 스토리지 활용 방식과 퍼시스턴트 볼륨(PV, PVC)을 통한 데이터 영속성 관리 방법을 정리합니다.

8.1 파드의 데이터 저장 방식

쿠버네티스에서 파드 내부의 데이터는 파드가 삭제되면 모두 사라지며, 기본적으로 Stateless(상태가 없는) 애플리케이션으로 동작합니다.
즉, 기본적으로 임시 파일 시스템(Temporary Filesystem)을 사용합니다.

  • 임시 저장소(Temporary Filesystem, emptyDir)
    • 기본적으로 컨테이너 내부에서 데이터를 저장할 경우, 파드가 삭제되면 모든 데이터도 함께 삭제됨
    • 쿠버네티스는 ephemeral volume(일시적인 저장소)을 지원하며, 파드가 삭제되거나 재시작될 경우 데이터는 유지되지 않음
    • 대표적인 예: emptyDir, hostPath

📌 예제: Pod 내부의 임시 파일 저장 방식

kubectl exec -it redis -- sh -c "echo 'hello' > /data/hello.txt"
kubectl exec -it redis -- cat /data/hello.txt

8.2 스토리지 소개

  • 볼륨 : emptyDir, hostPath, PV/PVC

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

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

8.3 CSI(Container Storage Interface)와 EBS CSI Driver

Kubernetes는 다양한 스토리지 솔루션을 지원하기 위해 Container Storage Interface(CSI)라는 표준 인터페이스를 제공합니다. CSI를 통해 다양한 스토리지 벤더가 Kubernetes에서 사용할 수 있는 스토리지 플러그인(Driver)을 개발할 수 있으며, Kubernetes의 자체 코드 변경 없이 새로운 스토리지 솔루션을 통합할 수 있습니다.

8.3.1 CSI(Container Storage Interface)란?

Kubernetes는 네트워크(CNI)와 마찬가지로 스토리지 관련 표준 인터페이스(CSI)를 만들어, 여러 벤더들이 자신들의 스토리지 솔루션을 Kubernetes에 쉽게 연동할 수 있도록 지원합니다.

📌 CSI 도입 배경

  • 기존에는 Kubernetes 내장 스토리지 플러그인(In-tree)이 사용되었으나, 새로운 기능 추가를 위해 Kubernetes 자체를 업그레이드해야 하는 문제가 있었습니다.
  • CSI 도입 이후, 스토리지 벤더들은 독립적으로 CSI 드라이버를 배포할 수 있게 되었고, Kubernetes 클러스터를 업데이트하지 않고도 새로운 기능을 사용할 수 있게 되었습니다.

📌 CSI의 주요 특징

  • Kubernetes의 내부 코드 수정 없이 스토리지 드라이버를 독립적으로 배포 및 관리 가능
  • AWS, Azure, Google Cloud 등의 다양한 클라우드 및 온프레미스 스토리지 지원
  • 동적 프로비저닝(Dynamic Provisioning)을 지원하여, 필요할 때 자동으로 볼륨을 생성하여 연결 가능
  • kubectl get csinodes 명령어를 통해 각 노드의 CSI 드라이버 정보를 확인 가능

8.3.2 AWS EBS CSI Driver

AWS EBS CSI 드라이버는 EBS를 Kubernetes의 퍼시스턴트 볼륨(PV)으로 사용할 수 있도록 지원하는 CSI 드라이버입니다.
이 드라이버를 사용하면 EBS 볼륨을 자동으로 프로비저닝하고, Kubernetes 파드에서 사용할 수 있도록 관리할 수 있습니다.

📌 EBS CSI 드라이버의 주요 기능

  • AWS API를 활용한 EBS 볼륨 자동 프로비저닝
  • 볼륨 확장(Expansion) 및 동적 프로비저닝(Dynamic Provisioning)
  • Persistent Volume(PV)과 PVC를 통해 상태 저장 애플리케이션 지원
  • EBS 볼륨을 특정 노드에 자동으로 Attach/Detach

📌 EBS CSI 드라이버 구조
AWS EBS CSI 드라이버는 크게 두 가지 주요 컴포넌트로 구성됩니다.

  1. Controller Pod (StatefulSet/Deployment)

    • Kubernetes 클러스터에서 EBS 볼륨을 생성, 삭제 및 관리하는 역할
    • AWS API와 통신하여 EBS 볼륨을 동적으로 프로비저닝
  2. Node Pod (DaemonSet)

    • 각 Kubernetes 노드에서 EBS 볼륨을 해당 노드에 Attach/Detach하는 역할
    • Kubernetes에서 해당 노드의 EBS 볼륨 상태를 관리하고, 컨테이너가 EBS를 마운트할 수 있도록 지원

📌 EBS CSI 드라이버의 동작 과정

  1. 사용자가 Persistent Volume Claim(PVC)을 요청하면, StorageClass를 기반으로 EBS 볼륨이 동적으로 생성됨
  2. Controller Pod이 AWS API를 호출하여 새로운 EBS 볼륨을 생성
  3. Node Pod (DaemonSet)이 해당 EBS 볼륨을 EC2 인스턴스(노드)에 Attach
  4. Kubernetes 파드에서 EBS 볼륨을 마운트하여 사용 가능
  5. 파드가 삭제되더라도 EBS 볼륨은 그대로 유지되어 데이터가 보존됨

📌 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

8.4 Node-Specific Volume Limits (노드별 볼륨 제한)

Kubernetes에서 노드에 연결할 수 있는 스토리지 볼륨 개수는 EC2 인스턴스 유형에 따라 다릅니다.
각 인스턴스 유형에 따라 최대 연결 가능한 EBS 볼륨 개수가 제한되며, Kubernetes에서 이를 적용하여 볼륨 부착 한도를 관리합니다.

8.4.1 노드별 볼륨 제한 개요

AWS EC2 인스턴스의 유형에 따라 노드가 지원할 수 있는 최대 볼륨 개수는 다음과 같이 다릅니다.

  • M5, C5, R5, T3, Z1D 인스턴스 유형:
    • 최대 25개의 EBS 볼륨 부착 가능
  • 기타 Amazon EC2 인스턴스 유형:
    • 최대 39개의 EBS 볼륨 부착 가능

🔗 참고 자료:

8.4.2 Kubernetes에서 노드별 볼륨 제한 확인

EBS CSI 드라이버가 배포된 경우, 각 노드가 지원하는 최대 볼륨 개수를 kubectl describe csinodes 명령어로 확인할 수 있습니다.

📌 T3.medium 인스턴스의 볼륨 한도 확인

kubectl describe csinodes

8.5 파드 기본 저장소 및 emptyDir 볼륨 동작 확인

Kubernetes에서 파드는 기본적으로 Stateless(무상태) 방식으로 동작하며, 파드 내부의 데이터는 삭제 시 모두 유실됩니다.
따라서, 데이터를 유지하려면 볼륨을 마운트해야 하며, 이를 위해 emptyDir 볼륨과 같은 Kubernetes의 기본적인 볼륨을 사용할 수 있습니다.

8.5.1 기본 저장소 동작 확인

기본적으로 파드 내부의 저장소는 파드가 재시작되거나 삭제될 경우 데이터가 사라집니다. 이를 실습을 통해 확인합니다.

🔗 참고 자료: 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

8.5.2 emptyDir 볼륨 동작 확인

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 볼륨은 파드가 삭제되면 함께 삭제됨

8.6 'local-path-provisioner'를 활용한 hostpath PV/PVC

Kubernetes에서 특정 노드의 로컬 디렉토리를 볼륨으로 활용하기 위해 local-path-provisioner를 사용할 수 있습니다.
이는 스토리지가 물리적으로 특정 노드에 종속되는 경우에 유용하며, 특정 노드에서만 접근할 수 있는 로컬 스토리지를 관리하는 방식입니다.

🔗 관련 자료


8.6.1 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 정책을 사용하여, 파드가 생성될 때까지 실제 볼륨이 생성되지 않습니다.

8.6.2 PV/PVC를 사용하는 파드 생성

스토리지를 요청하는 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이 자동으로 바인딩되며, 특정 노드에 디렉토리가 생성됨.

8.6.3 저장된 데이터 확인

📌 파드 내부에서 생성된 파일 확인:

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

8.6.4 파드 삭제 후 데이터 유지 여부 확인

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를 활용하여 데이터 영속성이 보장됨.

8.6.5 PVC 및 리소스 정리

다음 실습을 위해 생성한 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도 자동 삭제되며, 호스트 디렉토리에서도 데이터가 삭제됨.

8.7 Kubestr를 활용한 스토리지 성능 테스트

Kubestr는 Kubernetes에서 다양한 스토리지 성능을 벤치마킹하는 도구입니다.

8.7.1 Kubestr 설치

# 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

8.7.2 스토리지 클래스 확인

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

✅ 스토리지 클래스별로 속도를 측정하고, 최적의 옵션을 선택할 수 있습니다.

8.7.3 랜덤 읽기 성능 테스트

  • 3분 정도 소요
  • libaio 엔진과 다이렉트 I/O를 사용하여 고성능 스토리지의 랜덤 읽기 성능을 측정.
  • OS 캐시를 사용하지 않고 직접 디스크 I/O 수행 (direct 플래그)
# 랜덤 읽기 성능 테스트 설정 파일 생성
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 수준

8.7.4 랜덤 쓰기 성능 테스트

  • 5분 정도 소요
  • numjobs=16, iodepth=16 : 총 16×16 = 256개의 I/O 요청이 동시에 발생
# 랜덤 쓰기 성능 테스트 설정 파일 생성
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 기반의 스토리지를 고려하는 것이 유리합니다.

0개의 댓글