✅ PV (Persistent Volume)란?
데이터 저장소를 영구적으로 관리하는 리소스
pod가 죽거나 다시 시작돼도 데이터를 보존할 수 있게 해주는 디스크 공간을 제공함
- 영구적인 볼륨
- 물리적인 자원을 뜻함
- 도커에서 -v 같은 옵션을 통해 컨테이너의 데이터를 영구적으로 호스트에 백업을 했던 적이 있음.
- 왜냐하면 컨테이너가 삭제되면 내부의 데이터는 사라지기 때문임.
- 컨테이너의 데이터는 영구적이지 않음
✅ PV와 PVC의 관계
쿠버네티스에서는 PV랑 PVC를 구분해서 사용함
PV: 실제 디스크 공간을 나타내는 리소스, 클러스터의 관리자가 영구적 저장소
PVC: 사용자가 요청하는 스토리지, pod가 필요한 저장소 크기와 사용 방법을 요청할 수 있음
- PV로 사용할 공간을 NFS로 미리 정의해놓자
- master노드에 NFS-server를 설치하고, worker노드들을 NFS-client로 구성
root@master:~# apt-get install -y nfs-kernel-server
- 마스터에 nfs-server 설치
root@master:~# mkdir /shared
root@master:~# chmod 777 -R /shared
root@master:~# vi /etc/exports
/shared *(rw,sync,no_subtree_check,no_root_squash)

- sync: 데이터가 정상적으로 저장된 후 응답
- no_subtress_check: 하위디렉터리 검사 x => 속도 증가
- no_root_squash: 외부 root 계정도 이 서버의 root계정처럼 동작함
root@master:~# systemctl restart nfs-server
root@master:~# systemctl enable nfs-server
root@master:~# showmount -e

- 워커노드인 worker 1이랑 worker2에 클라이언트 설치
root@worker-1:~# apt-get install -y nfs-common
root@worker-1:~# apt-get install -y nfs-common
root@worker-1:~# mkdir /shared
root@worker-1:~# mount -t nfs 211.183.3.100:/shared /shared
- 마운트포인트와 마운트대상을 동일하게 /shared로 하자.
root@worker-2:~# apt-get install -y nfs-common
root@worker-2:~# mkdir /shared
root@worker-2:~# mount -t nfs 211.183.3.100:/shared /shared
root@worker-2:~# touch /shared/test

- 마운트가 잘 된 걸 확인.
root@worker-1:~# vi /etc/fstab
211.183.3.20:/shared /shared nfs defaults 0 0
root@worker-2:~# vi /etc/fstab
211.183.3.20:/shared /shared nfs defaults 0 0
pv 생성
root@master:~# cd ~/mani/
root@master:~/mani# mkdir pv
root@master:~/mani# cd pv/
vi pv1.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv1
spec:
capacity:
storage: 1Gi #용량
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /shared/pv1
server: 211.183.3.20
- 용량: 나중에 pvc를 통해 pv를 요청할 때, 요청량 > pv의 용량이면 pvc와 pv가 연동이 안됨
-> 만약에 100Gi를 요청(pvc)했는데 pv가 1Gi라면 성립이 안됨
vi pvc1.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
- 요청하는 용량(1Gi)은 pv보다 작거나 같아야 됨
- accessModes가 호환이 되어야 됨
kubectl apply -f pv1.yml
# pv 생성
kubectl apply -f pvc1.yml
# pvc 생성

- pv와 pvc의 용량, access mode 같은 것들이 호환이 됐기 때문에 서로 bound가 됨
- 이번엔 이 pv를 사용할 pod를 한번 생성해 보자
- status가 Available에서 Bound로 바뀜 -> pvc요청을 받을 수 있는 상태에서 pvc파일을 설정하고 나서 요청을 받은 상태가 됨
- volume이 nfs-pv1이 됨
vi pod1.yml
이번에는 pv를 사용할 pod를 한 번 생성해 보자
apiVersion: v1
kind: Pod
metadata:
name: nfs-pod1
spec:
containers:
- name: nfs-con
image: 61.254.18.30:5000/ipnginx
volumeMounts:
- name: nfs-vol
mountPath: /vol
volumes:
- name: nfs-vol
persistentVolumeClaim:
claimName: nfs-pvc

kubectl apply -f pod1.yml
kubectl exec -it nfs-pod1 -- bash

root@nfs-pod1:/# echo test > /vol/test.txt
- pod 내에서 파일 생성
- Ctrl + D로 컨테이너 밖으로 빠져나온 다음

- 호스트에 존재하는 test.txt 파일을 확인.
- 컨테이너의 데이터가 호스트에 잘 백업된 걸 확인 가능함.

- 노드에서 컨테이너 내부 파일 확인.
root@master:~/mani/pv# kubectl delete pod nfs-pod1
# 파드 삭제

- pv와 pvc는 pod가 삭제되어도 잘 살아있음
-> pod는 결국 여러 차례 삭제 및 재성성되어도 동일한 데이터가 유지될 것
kubectl apply -f pod1.yml
- pod 생성
kubectl exec nfs-pod1 -- ls /vol
- 파드가 삭제 및 재성생 될 때, 동일한 pvc에만 volumeMount 된다면, 데이터는 동일하게 유지됨

PV를 프로비저닝 하는 두 가지 방식
- 수동(static)
-> pvc요청이 올 때 생성을 해주거나, 올 것을 예상해서 pv를 미리 생성해두는 방식
-> 위에서 실습했던 방식
- 자동(dynamic)
-> pvc요청이 올때 자동으로 pv를 생성
-> 다이나믹 프로비저너를 설치
- Reclaim 방식(PVC가 삭제 됐을 때 정책)
- Recycle 방식: 재활용 PVC가 삭제된 다음에 다른 PVC요청이 오면 사용 가능.
현재는 정책상 사용 불가함
AccessMode
pv-pvc의 모드가 같아야 bound 됨. 일치하지 않으면. pending 상태에 머무름.
ReadWriteOnce(RWO) : 단일 노드에서 읽기 및 쓰기 가능.
ReadOnlyMany(ROX) : 다수 노드에서 읽기만 가능.
ReadWriteMany(RWX) : 여러 노드에서 읽기 및 쓰기 가능.
ReadWriteOncePod(RWOP) : 단일 Pod에서만 읽기 및 쓰기 가능.
실습 1)
=> 모드를 맞춰줘야 됨
https://kubernetes.io/ko/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/
wordpress.yml 파일
mysql.yml 파일
위 링크에 존재하는 wordpress와 mysql 매니페스트를 위와 같이 수정하고,
이 매니페스트에서 필요한 리소스를 생성하며 wordpress가 잘 동작하고 접속 가능하도록 만들어보자
풀이

- wordpress pod에서 mysql을 어떻게 찾아갈 수 있냐? 쿠버네티스에서는 서비스의 이름이 곧 주소.
- mysql svc의 이름은 wordpress-mysql 이므로 wordpress-mysql 이것 자체가 주소가 된다.
1. pv 생성
vi pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv2
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /shared/pv2
server: 211.183.3.20
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv3
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /shared/pv3
server: 211.183.3.20
kubectl apply -f pv.yml
2. mysql 매니페스트
vi mysql.yml
- 추가 수정한 내용이 있으니 참고하시오!
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
ports:
- port: 3306
selector:
app: wordpress
tier: mysql
clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
labels:
app: wordpress
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: mysql
spec:
containers:
- image: 61.254.18.30:5000/mysql
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
3. wordpress.yml 에서 수정할 부분
vi wordpress.yml
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:
- ReadWriteMany
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- image: 61.254.18.30:5000/wp
name: wordpress
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
value: password
ports:
- containerPort: 80
name: wordpress
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim
- mysql을 어떻게 찾아갈 수 있냐? mysql의 서비스이름으로 찾아갈 수 있음
- 서비스 이름은 내가 정할 수 있음 = 항상 일정한 주소로 찾아갈 수 있음
StorageClass
- 스토리지에는 다양한 종류가 있을 수 있음
- 대표적인 예가 HDD, SDD를 비교하면 상대적으로 HDD는 느리고 SSD는 빠를 것임. 스토리지의 성격에 맞게 분류를 해놓는 게 스토리지클래스이며, 관리자입장에서는 용도에 맞게 스토리지를 분류해 놓고, 사용자 입장에서는 용도에 맞게 스토리지클래스를 지정해서 pvc로 요청하면 될 것 임
- 사용자 "빠른 스토리지로 부탁해요"
- 관리자 "그렇다면 상대적으로 빠른 SSD를 제공해 드릴게요~"
aws eks에 보면, gp2, gp3의 경우 SSD이고 efs-sc-EFS 스토리지 이런 다양한 스토리지들이 존재함
mkdir /shared/pv-sc
- 경로를 만들어주고
vi pv-pvc.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: manual-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: manual-sc
persistentVolumeReclaimPolicy: Retain
nfs:
path: /shared/pv-sc
server: 211.183.3.100
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: manual-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: manual-sc
- pvc를 생성할 때 sc를 명시해서 생성하면, pv도 동일한 sc를 갖는 pv가 맵핑이 됨

ConfigMap
- 클러스터 존재하는 값이나 설정을 참조하여 사용하고 싶을 때 쓰는 리소스
- 쿠버네티스에서 애플리케이션의 설정 데이터를 관리하는 방법 중 하나임
-> 애플리케이션 설정 파일이나 환경 변수 등을 쿠버네티스 클러스터 내에서 관리하고, 이를 pod와 같은 리소스에 전달하는 역할을 함
ex) 환경변수 - username, db_host 등...
ex) nginx.conf 이나 리버스프록시 설정 파일 default.conf같은 설정 파일의 내용을 Pod(컨테이너)에 넣어줄 수 있음
root@master:~/mani/cm# pwd
/root/mani/cm
kubectl create cm info --from-literal username=root --from-literal db_host=211.183.3.233
- username이라는 키(key)에 해당하는 값(value) root
kubectl delete cm info
vi info.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: info
namespace: default
data:
db_host: 211.183.3.30
username: root
kubectl apply -f info.yml

- 여기서 ip가 뭐가 나오든 상관없음
apiVersion: v1
kind: Pod
metadata:
name: my-pod-env
spec:
containers:
- name: my-container
image: 61.254.18.30:5000/nginx
env:
- name: USERNAME
valueFrom:
configMapKeyRef:
name: info
key: username
- name: DATABASE
valueFrom:
configMapKeyRef:
name: info
key: db_host
- pod안에서 USERNAME이라는 환경변수를 조회해 보면, root라는 값이 들어가 있을 것 임
root@master:~/mani/cm# kubectl exec my-pod-env -c my-container -- env | grep USERNAME
USERNAME=root
root@master:~/mani/cm# kubectl exec my-pod-env -- env | grep DATABASE
DATABASE=211.183.3.30
이번에는 설정파일을 cm으로 만들어보겠음
vi conf.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: conf
data:
nginx.conf: |
asdf;lksjdflsakjfsa;d
sad;flksdfl;jsadfl;jsadfl;k
asdf;lkjsadf;lksjdfl;ksdja
sad;flkjsadfl;ksadjf;l
sadf;lksjdf;
- 파이프라인(|): 하위에 여러 줄의 명령어를 입력할 수 있음
- nginx.conf 파일에 아래의 내용이 포함이 될 것
vi conf-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: my-pod-conf
spec:
containers:
- name: my-container
image: 61.254.18.30:5000/nginx
volumeMounts:
- name: nginx-config-vol
mountPath: /nginx.conf
subPath: nginx.conf
volumes:
- name: nginx-config-vol
configMap:
name: conf
items:
- key: nginx.conf
path: nginx.conf
subPath: ConfigMap(conf)에서 특정한 키, 파일만 마운트 할 때. 마운트포인트의 기존내용 유지하는 옵션.
path: 컨테이너 외부에 존재하는 파일이라는 뜻. 내부에 MountPath에 넣어주겠음
kubectl apply -f conf-pod.yml
kubectl exec my-pod-conf -- cat /nginx.conf

- 우리가 구성한 설정 파일의 내용이 잘 들어 있는 걸 확인 가능
Secret
- ConfigMap과 비슷하게, 특정한 값이나 설정을 컨테이너 안으로 주입을 하는 개념. 다른 점이 있다면 보안 수준이 다름
- 민감한 정보(데이터베이스의 암호 같은..)를 외부에 호출해서 쓰는 방식. base64 방식으로 인코딩 되며, etcd에서 암호화.
- 시크릿을 제외한 다양한 리소스들은 결국 api요청만 한다면 정보를 조회할 수 있는 반면, 시크릿의 경우엔 etcd에서 암호화가 되기 때문에 조회가 불가능.
echo test123 | base64
- test123이라는 평문을 base64 방식으로 인코딩.

echo dGVzdDEyMwo= | base64 --decode
- 인코딩 된 값을 디코딩

kubectl create secret generic sec --from-literal password=test123
- 시크릿을 명령어로 생성
kubectl get secret
- 시크릿 조회

kubectl delete secret sec
- 삭제
vi sec.yml
- 매니페스트로 정의
apiVersion: v1
kind: Secret
metadata:
name: sec
type: Opaque # 기본 타입
stringData:
password: test123 #password 값
type: Opaque => <key>:<value> 형태를 뜻함.
vi sec-pod.yml
- 이 시크릿을 참조하는 pod 생성
apiVersion: v1
kind: Pod
metadata:
name: pod-sec
spec:
containers:
- name: sec-con
image: 61.254.18.30:5000/nginx
envFrom:
- secretRef:
name: sec
kubectl apply -f sec-pod.yml
kubectl exec pod-sec -- env | grep password
오류/ 해결 방안 - http와 https의 충돌?
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m1s default-scheduler Successfully assigned default/pod-sec to worker-2
Normal Pulling 2m kubelet Pulling image "61.254.18.30:5000/nginx"
Normal Pulled 2m kubelet Successfully pulled image "61.254.18.30:5000/nginx" in 30ms (30ms including waiting). Image size: 73096092 bytes.
Normal Created 2m kubelet Created container: sec-con
Normal Started 2m kubelet Started container sec-con
Normal Pulling 24s (x4 over 2m) kubelet Pulling image "61.254.18.30:5000/nginx"
Warning Failed 24s (x4 over 2m) kubelet Failed to pull image "61.254.18.30:5000/nginx": failed to pull and unpack image "61.254.18.30:5000/nginx:latest": failed to resolve reference "61.254.18.30:5000/nginx:latest": failed to do request: Head "https://61.254.18.30:5000/v2/nginx/manifests/latest": http: server gave HTTP response to HTTPS client
Warning Failed 24s (x4 over 2m) kubelet Error: ErrImagePull
Normal BackOff 10s (x6 over 2m) kubelet Back-off pulling image "61.254.18.30:5000/nginx"
Warning Failed 10s (x6 over 2m) kubelet Error: ImagePullBackOff
HTTP와 HTTPS 간의 연결 문제로 인해 이미지 다운로드가 실패하는 상황임
그래서
apiVersion: v1
kind: Pod
metadata:
name: pod-sec
spec:
containers:
- name: sec-con
image: 61.254.18.30:5000/nginx #image: public.ecr.aws/nginx/nginx:latest
envFrom:
- secretRef:
name: sec
여기를 바꿔주니까
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 29s default-scheduler Successfully assigned default/pod-sec to worker-2
Normal Pulling 29s kubelet Pulling image "public.ecr.aws/nginx/nginx:latest"
Normal Pulling 28s kubelet Pulling image "public.ecr.aws/nginx/nginx:latest"
Normal Pulled 19s kubelet Successfully pulled image "public.ecr.aws/nginx/nginx:latest" in 9.98s (9.98s including waiting). Image size: 73110238 bytes.
Normal Created 18s kubelet Created container: sec-con
Normal Started 18s kubelet Started container sec-con
Normal Pulled 16s kubelet Successfully pulled image "public.ecr.aws/nginx/nginx:latest" in 12.079s (12.079s including waiting). Image size: 73110238 bytes.
Normal Created 16s kubelet Created container: sec-con
Normal Started 16s kubelet Started container sec-con
event부분에 에러가 안 생기고
root@master:~/mani/cm# kubectl exec pod-sec -- env | grep password
password=test123
결괏값이 잘 나오는 걸 확인할 수 있음
- 정확하게 어디서 충돌 나는 건지는 모르겠음
kubectl exec pod-sec -- env | grep password


'AWS Cloud School 8기 > 쿠버네티스' 카테고리의 다른 글
[쿠버네티스] 헬름(Helm) (0) | 2025.04.23 |
---|---|
[쿠버네티스] 컨테이너의 헬스체크/ livenessProbe/ readinessProbe/ StatefulSet(리소스)/ DaemonSet(리소스) (4) | 2025.04.17 |
[쿠버네티스] LoadBalancer/ Ingress/ Monolithic/ Micro-service (3) | 2025.04.15 |
[쿠버네티스] label/ ReplicaSet/ kubectl 명령 자동완성/ Deployment/ namespace/ Service/ NodePort / 삭제 모음 zip (0) | 2025.04.11 |
[쿠버네티스] Kubernetes/ 클러스터/ CRI/ 매니패스트 (2) | 2025.04.10 |