CloudFormation을 통해서 해당 인프라 전체를 구축했고, 다음과 같은 환경에서 진행된다.
구축된 전체적인 환경은 다음과 같다.
여기서 왼쪽을 보면 AWS EFS
를 생성하고 작업용 베스천 호스트 EC2
에 마운트하는 것을 확인할 수 있다.
마운트 타겟은 3개의 가용영역으로 설정되어있고, 베스천 호스트에서 이를 사용하기 위해서 연결
을 통해 EFS를 마운트했다.
연결
을 누를 시 나오는 화면이제 작업용 EC2 (베스천)에서 실제로 EFS가 마운트 되어 있는 지 확인해보자.
// NFS4로 마운트 된 디스크 확인
df -hT --type nfs4
또는
mount | grep nfs4
// EFS에 파일 생성, 확인, 삭제
echo "efs file test" > /mnt/myefs/memo.txt
cat /mnt/myefs/memo.txt
rm -f /mnt/myefs/memo.txt
마운트된 EFS를 확인할 수 있고, 마운트 경로에 파일을 정상적으로 생성하고, 삭제할 수 있는 것 또한 확인했다.
kubectl get sc
AWS EKS에는 기본적으로 제공되는 Default Storage Class가 있다. provisioner는 AWS EBS
로 설정되어있고, 스토리지는 gp2
type으로 좋은 성능은 아니다.
💡이후 내용은 AWS LB Controller, ExternalDNS, kube-ops-view 설치한 상태로 진행하였다.
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
MyDomain=<자신의 도메인>
MyDnsHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnsHostedZoneId
curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/externaldns.yaml
MyDomain=$MyDomain MyDnsHostedZoneId=$MyDnsHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -
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"
이제 Amazon EKS 환경에서 기본 스토리지 실습을 진행해 보자.
실습 단계는 1. 임시 파일 시스템, 2. emptyDir 볼륨, 3. local-path-provisioner로 구성된다.
컨테이너 내부에 구성되는 임시 파일 시스템을 구성하고 검증해 보자.
// 파드 배포 - 10초 간격으로 date 명령을 입력하고 /home/pod-out.txt 파일에 저장
curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/temporary-fs.yaml
cat temporary-fs.yaml | yh
kubectl apply -f temporary-fs.yaml
날짜와 시간을 출력하는 date 명령어를 컨테이너에 입력해 10초 간격으로 날짜와 시간을 기록하도록 구성되어 있다.
// exec를 통해 컨테이너에 명령어 전달
kubectl exec busybox -- tail -f /home/pod-out.txt
// 신규 터미널 생성해서 접속하는 경우
kubectl exec -it busybox -- /bin/sh
10초 간격으로 날짜와 시간이 컨테이너 내 /home/pod-out.txt
에 기록된 것을 확인할 수 있다. 즉, 컨테이너 내에 기록되는 것이므로 임시 파일 시스템이다
파드 내부에 구성되는 emptyDir 볼륨을 구성하고 확인해 보자.
// 컨테이너 2대 생성 - emptyDir 볼륨 마운트
curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/emptydir.yaml
cat emptydir.yaml | yh
kubectl apply -f emptydir.yaml
이번에는 동일한 명령어를 입력하나, 하나의 파드에 컨테이너를 2개 만들고 emptyDir를 통해서 2개의 컨테이너가 한 볼륨을 공유하도록 했다.
// 결과 확인
kubectl exec busybox -- tail -f /mount/1-out.txt
kubectl exec busybox -- tail -f /mount/2-out.txt
결과를 확인해보면 다음과 같이 2개의 컨테이너에서 같은 emptyDir 볼륨에 txt
파일을 기록했으므로 2개의 파일이 생성된 것을 확인할 수 있다.
파드를 삭제❌하고 다시 생성하게되면 파드에 종속적인 볼륨이므로 기존 내용이 전부 지워지고 새로 생성된 것을 확인할 수 있다.
// 파드 삭제 후 재생성
kubectl delete pod busybox
kubectl apply -f emptydir.yaml
// 저장된 파일 다시 확인
kubectl exec busybox -- tail -f /mount/1-out.txt
kubectl exec busybox -- tail -f /mount/2-out.txt
이번에는 실제로 PV
와 PVC
가 구성되는 영구 볼륨 환경에서 local-path-provisioner를 통해서 동적 프로비저닝을 통해 스토리지를 구성해보자.
이때 AWS CSI를 사용하는 것이 아니므로 외부 스토리지(AWS EBS, EFS)가 아닌 노드 내 경로를 PV로 사용된다.
// local-path-provisioner 배포
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml
// local-path-provisioner 확인
kubectl get-all -n local-path-storage
// storageClass 확인
kubectl get sc
kubectl get sc local-path -o yaml | yh
다음과 같이 local-path-provisioner를 배포하게 되면 이러한 필수적인 구성요소들이 함께 배포되게 된다.
local-path-provisioner 파드를 포함하여 StorageClass까지 자동으로 생성된다.
// PVC 생성
curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/localpath-pvc.yaml
cat localpath-pvc.yaml | yh
kubectl apply -f localpath-pvc.yaml
// PVC 정보 확인
kubectl describe pvc
PVC를 생성해보자. PVC를 생성할 때 storageClassName
에 이전에 생성했던 local-path 스토리지 클래스를 넣어 PV가 동적으로 프로비저닝 되도록 한다.
PVC의 상세 정보를 확인해보면 waiting for first consumer로 새로운 PV 생성 요청이 들어오길 기다리고 있는 것을 확인할 수 있다.
이제 새로운 PV 생성 요청을 보내기 위해서 파드에 PVC를 정의하는 것을 통해 해당 PVC를 사용해보자.
curl -s -O https://raw.githubusercontent.com/cloudneta/cnaeblab/master/_data/localpath-pod.yaml
cat localpath-pod.yaml | yh
kubectl apply -f localpath-pod.yaml
파드를 생성하면 파드 내 PVC에 정의된 Storage Class를 통해서 PV가 생성되고, PVC에 바인딩된다.
생성된 PV의 정보를 확인해보자.
// PV 정보 확인
kubectl describe pv
Local Path Provisioner는 노드의 hostPath를 PV로 생성한다.
따라서 파드가 배포될 때, 볼륨이 사용되기 위해서는 해당 파드가 배포된 노드에 맞춰서 배포가 되어야한다.
이를 위해 nodeAffinity가 PV에 정의되어, PV가 특정 노드에만 바인딩 되도록 사용되었다.
// PV가 마운팅할 노드 IP를 변수 지정
NODE_PV=$(kubectl describe pv | grep 'Term 0' | cut -b 50-62 | sed 's/[.|a-z]//g' | sed 's/-/./g')
// PV 이름 변수 지정
LP_PVC_NAME=$(kubectl get pvc localpath-claim -o jsonpath='{.spec.volumeName}')
이제 생성된 파일을 확인해 보자.
// 파드에서 out.txt 파일 내용 확인
kubectl exec -it app -- tail -f /data/out.txt
// 노드에서 PV 경로 확인
ssh ec2-user@$NODE_PV tree /opt/local-path-provisioner
// 노드에서 out.txt 파일 내용 확인
ssh ec2-user@$NODE_PV tail -f /opt/local-path-provisioner/${LP_PVC_NAME}_default_localpath-claim/out.txt
노드와 파드에서 동일한 파일을 참조하고 있는 것을 확인할 수 있다. 노드의 HostPath로 생성된 PV이기 때문!💎📁
그렇다면 파드를 삭제해도 여전히 노드에는 해당 데이터가 유지되어야 할 것이다. 이를 확인해 보도록 하자.
// 파드 삭제
kubectl delete pod app
// 노드에서 PV 경로 확인
ssh ec2-user@$NODE_PV tree /opt/local-path-provisioner
// 노드에서 out.txt 파일 내용 확인
ssh ec2-user@$NODE_PV tail -f /opt/local-path-provisioner/${LP_PVC_NAME}_default_localpath-claim/out.txt
여전히 잘 확인된다.
// 파드 재생성
kubectl apply -f localpath-pod.yaml
// 파드에서 out.txt 파일 내용 확인 (Head)
kubectl exec -it app -- head /data/out.txt
// 파드에서 out.txt 파일 내용 확인 (Tail)
kubectl exec -it app -- tail -f /data/out.txt
파드를 재 생성하고 확인해보면?
이전 결과를 그대로 참조하는 것을 확인할 수 있다.
새로운 파드를 생성할 때 권한과 상관없이 노드
만 같다면 동일한 PVC 및 PV를 사용할 수 있다.
예를 들어 ReadWriteOnce (RWO)권한은 한번에 하나의 노드
만이 PV에 접근 가능한 것이므로 파드의 기준에서 보면 해당 노드에만 있다면 100개든 1000개든 이론상 같은 PV와 PVC를 사용할 수 있다.
apiVersion: v1
kind: Pod
metadata:
name: test
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: persistent-storage
mountPath: /data
dnsPolicy: ClusterFirst
restartPolicy: Always
affinity: # 노드 어피니티 설정을 추가
nodeAffinity: # 노드 어피니티 섹션
requiredDuringSchedulingIgnoredDuringExecution: # 스케줄링 시 필요, 실행 중에는 무시
nodeSelectorTerms: # 노드 선택 조건
- matchExpressions: # 표현식을 매칭
- key: kubernetes.io/hostname
operator: In # 'In' 연산자를 사용하여 주어진 값 중 하나와 일치하는 노드 선택
values:
- ip-192-168-3-33.ap-northeast-2.compute.internal # 대상 노드
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: localpath-claim
이처럼 노드 어피니티 조건을 걸어서 해당 노드에만 파드를 위치시키면 해당 파드또한 PV, PVC를 동일하게 사용할 수 있다.
// 파드 삭제
kubectl delete pod app
// PVC 삭제 (reclaimPolicy가 delete이므로 PV도 같이 삭제)
kubectl delete pvc localpath-claim
💡권한 정리
ReadWriteOnce (RWO)
: 볼륨을 한 번에 하나의 노드에서 읽기 및 쓰기가 가능합니다. 이 모드에서는 여러 파드가 동일한 노드에서 실행되는 경우에 한해 해당 PV를 공유할 수 있습니다.ReadOnlyMany (ROX)
: 볼륨을 여러 노드에서 읽기 전용으로 접근할 수 있습니다. 이 모드에서는 여러 파드가 다른 노드에서 실행되어도 PV를 공유할 수 있지만, 모든 접근은 읽기 전용입니다.ReadWriteMany (RWX)
: 볼륨을 여러 노드에서 읽고 쓸 수 있습니다. 이 모드는 볼륨을 여러 파드 간에 가장 유연하게 공유할 수 있는 방법을 제공합니다.
Reference📎 | CloudNet@와 함께하는 Amazon EKS 기본 강의