🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

쿠버네티스 리소스 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 22. · 3 Views

쿠버네티스 리소스 완벽 가이드

쿠버네티스의 핵심 리소스들을 실무 중심으로 배워봅니다. Deployment부터 Namespace까지, 초급 개발자도 쉽게 이해할 수 있도록 친절하게 설명합니다. 실제 프로젝트에서 바로 활용할 수 있는 실전 예제와 함께 합니다.


목차

  1. Deployment와 ReplicaSet
  2. Service 타입
  3. ConfigMap과 Secret
  4. PersistentVolume
  5. Namespace 활용
  6. Label과 Selector

1. Deployment와 ReplicaSet

신입 개발자 이쿠버 씨는 첫 배포를 앞두고 긴장하고 있습니다. "서버가 갑자기 죽으면 어떡하죠?" 걱정하는 이쿠버 씨에게 팀장 최운영 씨가 말합니다.

"우리는 Deployment를 사용하니까 걱정 마세요."

Deployment는 쿠버네티스에서 애플리케이션을 배포하고 관리하는 핵심 리소스입니다. 마치 공장의 생산 라인 관리자처럼, 지정한 개수만큼의 파드를 자동으로 유지하고 업데이트를 안전하게 처리합니다.

Deployment는 내부적으로 ReplicaSet을 생성하여 파드의 복제본을 관리합니다.

다음 코드를 살펴봅시다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3  # 3개의 파드를 유지합니다
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80

이쿠버 씨는 입사 1개월 차 백엔드 개발자입니다. 오늘은 처음으로 쿠버네티스 클러스터에 자신이 만든 애플리케이션을 배포하는 날입니다.

설레는 마음으로 배포 파일을 작성하는데, 선배 개발자 최운영 씨가 다가옵니다. "이쿠버 씨, Deployment 파일 작성 중이신가요?

처음이시죠?" 최운영 씨가 친절하게 물어봅니다. "네, 그런데 이게 정확히 뭘 하는 건지 잘 모르겠어요." Deployment란 무엇일까요? 쉽게 비유하자면, Deployment는 마치 레스토랑의 주방장과 같습니다.

주방장은 요리사가 몇 명 필요한지 결정하고, 누군가 퇴근하면 새로운 요리사를 채용하며, 메뉴가 바뀌면 요리사들에게 새로운 레시피를 전달합니다. Deployment도 이와 똑같이 파드가 몇 개 필요한지 관리하고, 문제가 생기면 자동으로 복구하며, 새 버전을 배포할 때 안전하게 교체합니다.

Deployment가 없던 시절은 어땠을까요? 초창기 컨테이너 환경에서는 개발자가 직접 파드를 하나하나 생성하고 관리해야 했습니다. 서버 하나가 죽으면 수동으로 다시 띄워야 했고, 업데이트할 때도 하나씩 내렸다가 새 버전을 올려야 했습니다.

실수로 파드를 너무 많이 띄우거나, 반대로 하나도 안 띄운 채로 퇴근하는 일도 생겼습니다. 더 큰 문제는 무중단 배포였습니다.

서비스를 멈추지 않고 새 버전을 배포하려면 복잡한 스크립트를 작성해야 했고, 한 번의 실수로 서비스 전체가 다운되는 일도 비일비재했습니다. Deployment의 등장 바로 이런 문제를 해결하기 위해 Deployment가 등장했습니다.

Deployment를 사용하면 원하는 상태만 선언하면 됩니다. "나는 nginx 파드 3개를 항상 유지하고 싶어"라고 말하면, 쿠버네티스가 알아서 그 상태를 만들고 유지합니다.

파드 하나가 죽으면 즉시 새로운 파드를 생성합니다. 개발자는 더 이상 일일이 관리할 필요가 없습니다.

또한 롤링 업데이트가 가능해집니다. 새 버전을 배포할 때 한 번에 전부 교체하는 게 아니라, 하나씩 천천히 교체합니다.

문제가 생기면 자동으로 롤백도 가능합니다. ReplicaSet의 역할 Deployment는 실제로 혼자 일하지 않습니다.

내부적으로 ReplicaSet이라는 부하 직원을 둡니다. 최운영 씨가 설명합니다.

"Deployment는 전략을 세우는 관리자이고, ReplicaSet은 실제로 파드를 만들고 관리하는 실무자예요. 우리가 Deployment를 만들면 자동으로 ReplicaSet이 생성되고, ReplicaSet이 파드를 관리합니다." 업데이트할 때를 생각해보면 더 명확합니다.

버전 1.0을 배포할 때 ReplicaSet-A가 생성되어 파드 3개를 관리합니다. 이제 버전 2.0으로 업데이트하면 새로운 ReplicaSet-B가 생성되고, ReplicaSet-A의 파드는 하나씩 줄이고 ReplicaSet-B의 파드는 하나씩 늘립니다.

이렇게 안전하게 교체됩니다. 코드 분석 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 replicas: 3을 보면 파드를 3개 유지하겠다는 의미입니다. 이것이 원하는 상태입니다.

selector.matchLabels는 어떤 파드를 관리할지 선택하는 라벨입니다. template 부분은 파드를 어떻게 만들지 정의합니다.

nginx 컨테이너를 사용하고 80 포트를 열겠다는 뜻입니다. 실무 활용 사례 실제 회사에서는 어떻게 활용할까요?

예를 들어 온라인 쇼핑몰을 운영한다고 가정해봅시다. 평소에는 파드 3개로 충분하지만, 블랙프라이데이 같은 대목에는 트래픽이 급증합니다.

이때 replicas: 10으로 변경하면 쿠버네티스가 자동으로 파드 7개를 추가로 생성합니다. 이벤트가 끝나면 다시 3으로 줄이면 됩니다.

또한 새 기능을 배포할 때도 안심할 수 있습니다. Deployment가 롤링 업데이트를 처리하므로, 서비스 중단 없이 새 버전으로 전환됩니다.

문제가 생기면 kubectl rollout undo 명령어 하나로 이전 버전으로 돌아갑니다. 주의사항 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수는 Deployment와 ReplicaSet을 동시에 직접 만드는 것입니다. Deployment만 만들면 ReplicaSet은 자동으로 생성되므로, 직접 만들 필요가 없습니다.

두 개를 동시에 만들면 관리가 꼬일 수 있습니다. 또한 replicas를 너무 크게 설정하면 클러스터 리소스가 부족해질 수 있습니다.

처음에는 적은 수로 시작해서 모니터링하며 점차 늘리는 것이 좋습니다. 정리 다시 이쿠버 씨의 이야기로 돌아가 봅시다.

최운영 씨의 설명을 들은 이쿠버 씨는 자신감이 생겼습니다. "아, Deployment가 알아서 관리해 주는구나!" Deployment를 제대로 이해하면 안정적인 서비스 운영이 가능합니다.

파드가 죽어도 자동으로 복구되고, 배포도 안전하게 진행됩니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - replicas는 홀수로 설정하는 것이 좋습니다 (3, 5, 7 등). 네트워크 분할 시 다수결 원칙에 유리합니다.

  • 배포 전에 항상 kubectl apply --dry-run으로 미리 테스트하세요.
  • kubectl rollout status deployment/web-app으로 배포 진행 상황을 실시간 모니터링할 수 있습니다.

2. Service 타입

파드를 성공적으로 배포한 이쿠버 씨는 웹 브라우저로 접속을 시도합니다. 하지만 어떻게 접속해야 할지 막막합니다.

"IP 주소가 계속 바뀌는데요?" 당황하는 이쿠버 씨에게 최운영 씨가 말합니다. "그래서 Service가 필요한 거예요."

Service는 쿠버네티스에서 파드들에게 고정된 접근 방법을 제공하는 리소스입니다. 마치 회사의 대표번호처럼, 직원이 바뀌어도 대표번호는 그대로입니다.

Service는 ClusterIP, NodePort, LoadBalancer 세 가지 주요 타입이 있으며, 각각 다른 용도로 사용됩니다.

다음 코드를 살펴봅시다.

# ClusterIP: 클러스터 내부에서만 접근
apiVersion: v1
kind: Service
metadata:
  name: internal-api
spec:
  type: ClusterIP
  selector:
    app: api
  ports:
  - port: 8080
    targetPort: 8080
---
# NodePort: 외부에서 노드 IP로 접근
apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
spec:
  type: NodePort
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080  # 30000-32767 범위
---
# LoadBalancer: 클라우드 로드밸런서 생성
apiVersion: v1
kind: Service
metadata:
  name: web-lb
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80

이쿠버 씨는 드디어 Deployment로 파드 3개를 띄우는 데 성공했습니다. 이제 웹 브라우저로 접속해서 서비스가 잘 동작하는지 확인하려고 합니다.

하지만 문제가 생겼습니다. "파드 IP가 10.244.1.5인데, 이걸로 접속하면 되나요?" 이쿠버 씨가 묻습니다.

최운영 씨가 고개를 젓습니다. "파드는 언제든지 재시작될 수 있어요.

그러면 IP가 바뀌죠. 직접 파드 IP를 사용하면 안 됩니다." Service란 무엇일까요? 쉽게 비유하자면, Service는 마치 회사의 대표 전화번호와 같습니다.

회사에는 여러 직원이 있고, 직원은 퇴사하거나 새로 입사합니다. 하지만 고객은 항상 같은 대표번호로 전화를 걸고, 교환원이 적절한 직원에게 연결해 줍니다.

Service도 똑같습니다. 파드는 계속 바뀌지만, Service는 고정된 IP와 DNS 이름을 제공하고, 트래픽을 살아있는 파드로 자동으로 분배합니다.

Service가 없다면? Service가 없던 시절을 상상해봅시다. 개발자는 파드의 IP 주소를 직접 추적해야 했습니다.

파드가 재시작될 때마다 새 IP를 찾아서 설정 파일을 수정하고 다른 서비스를 재배포해야 했습니다. 파드가 3개라면 어떤 파드로 요청을 보낼지도 직접 결정해야 했습니다.

로드 밸런싱 로직을 애플리케이션에 직접 구현해야 했죠. 실수로 죽은 파드에 요청을 보내는 일도 잦았습니다.

사용자는 간헐적으로 오류를 경험했고, 개발자는 밤새 원인을 찾아 헤맸습니다. Service의 세 가지 타입 Service는 용도에 따라 세 가지 타입으로 나뉩니다.

최운영 씨가 화이트보드에 그림을 그리며 설명합니다. "첫 번째는 ClusterIP입니다.

이건 클러스터 내부에서만 사용하는 서비스예요. 데이터베이스나 내부 API처럼 외부에 노출할 필요 없는 서비스에 사용합니다.

클러스터 안의 다른 파드들만 접근할 수 있어요." 실제로 마이크로서비스 아키텍처에서는 대부분의 서비스가 ClusterIP입니다. 예를 들어 결제 서비스, 주문 서비스, 재고 서비스는 모두 내부에서만 통신하므로 ClusterIP면 충분합니다.

"두 번째는 NodePort입니다. 이건 클러스터 외부에서 노드의 IP와 특정 포트로 접근할 수 있게 해줘요.

주로 개발이나 테스트 환경에서 사용합니다." NodePort는 30000에서 32767 사이의 포트를 사용합니다. 예를 들어 노드 IP가 192.168.1.10이고 NodePort가 30080이라면, http://192.168.1.10:30080으로 접속할 수 있습니다.

간단하지만 프로덕션에서는 권장되지 않습니다. 포트 번호를 기억해야 하고, 노드가 바뀌면 IP도 바뀌기 때문입니다.

"세 번째는 LoadBalancer입니다. 이게 진짜 프로덕션용이에요.

클라우드 환경에서 자동으로 로드밸런서를 만들어줍니다." AWS의 ELB, GCP의 Cloud Load Balancer, Azure의 Load Balancer가 자동으로 생성됩니다. 사용자는 로드밸런서의 공인 IP나 도메인으로 접속하고, 트래픽은 자동으로 여러 노드의 파드들에게 분산됩니다.

가장 강력하지만, 클라우드 로드밸런서는 유료이므로 비용이 발생합니다. 코드 분석 위의 세 가지 예제를 살펴봅시다.

첫 번째 ClusterIP는 type: ClusterIP로 지정했습니다. selectorapp: api 라벨을 가진 파드들을 선택합니다.

port: 8080은 Service가 받을 포트이고, targetPort: 8080은 파드의 실제 포트입니다. 두 번째 NodePort는 nodePort: 30080을 지정했습니다.

이제 모든 노드의 30080 포트로 요청이 들어오면 이 Service로 전달됩니다. 세 번째 LoadBalancer는 클라우드에서 실행하면 자동으로 외부 IP가 할당됩니다.

kubectl get service web-lb 명령어로 확인하면 EXTERNAL-IP 항목에 공인 IP가 표시됩니다. 실무 활용 사례 실제 쇼핑몰 서비스를 구축한다고 가정해봅시다.

프론트엔드 웹 서버는 LoadBalancer 타입으로 만듭니다. 사용자가 인터넷에서 접속해야 하므로 공인 IP가 필요합니다.

백엔드 API 서버는 ClusterIP로 만듭니다. 프론트엔드에서만 호출하므로 내부 통신으로 충분합니다.

데이터베이스도 ClusterIP입니다. API 서버만 접근하면 되기 때문입니다.

개발 환경에서는 비용을 아끼기 위해 LoadBalancer 대신 NodePort를 사용하기도 합니다. 로컬 minikube나 개발 클러스터에서는 NodePort가 더 간단하고 빠릅니다.

주의사항 Service를 사용할 때 주의할 점이 있습니다. 초보자들이 자주 하는 실수는 selector 라벨을 잘못 지정하는 것입니다.

Service의 selector와 파드의 label이 정확히 일치해야 연결됩니다. 오타 하나만 나도 연결이 안 됩니다.

kubectl get endpoints로 파드가 제대로 연결되었는지 확인하세요. 또한 LoadBalancer는 클라우드 환경에서만 동작합니다.

로컬 환경이나 온프레미스에서는 EXTERNAL-IP가 계속 pending 상태로 남습니다. 이런 환경에서는 MetalLB 같은 별도 솔루션이 필요합니다.

정리 이쿠버 씨는 이제 자신있게 Service를 만들 수 있게 되었습니다. "내부 서비스는 ClusterIP, 외부 서비스는 LoadBalancer를 쓰면 되겠네요!" Service를 제대로 이해하면 안정적인 네트워킹을 구축할 수 있습니다.

파드가 재시작되어도 걱정없고, 트래픽도 자동으로 분산됩니다. 여러분도 서비스 타입을 상황에 맞게 선택해 보세요.

실전 팁

💡 - kubectl get endpoints <service-name>으로 Service가 어떤 파드를 바라보는지 확인하세요.

  • 내부 통신은 항상 ClusterIP를 사용하세요. 불필요하게 외부에 노출하면 보안 위험이 생깁니다.
  • LoadBalancer는 비용이 발생하므로, 꼭 필요한 곳에만 사용하세요.

3. ConfigMap과 Secret

이쿠버 씨는 데이터베이스 비밀번호를 코드에 하드코딩했다가 팀장에게 혼났습니다. "코드에 비밀번호를 넣으면 안 돼요!" 당황한 이쿠버 씨에게 최운영 씨가 ConfigMap과 Secret을 소개합니다.

ConfigMap은 설정 데이터를 저장하는 리소스이고, Secret은 민감한 정보를 암호화하여 저장하는 리소스입니다. 마치 금고와 공개 게시판의 차이와 같습니다.

애플리케이션 설정은 ConfigMap에, 비밀번호나 API 키는 Secret에 보관합니다.

다음 코드를 살펴봅시다.

# ConfigMap: 일반 설정 데이터
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  database.host: "mysql.default.svc.cluster.local"
  database.port: "3306"
  log.level: "info"
---
# Secret: 민감 정보 (base64 인코딩)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  username: admin
  password: super-secret-password
---
# Pod에서 사용하기
apiVersion: v1
kind: Pod
metadata:
  name: app-pod
spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    - name: DB_HOST
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: database.host
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: password

이쿠버 씨는 열심히 애플리케이션을 개발했습니다. 데이터베이스 연결을 위해 코드 안에 이렇게 작성했습니다.

const password = "mypassword123". 코드 리뷰 시간, 팀장이 화면을 보더니 놀라며 말합니다.

"이쿠버 씨, 비밀번호를 코드에 직접 넣으면 안 됩니다! 이 코드가 GitHub에 올라가면 어떡하려고요?" 이쿠버 씨는 식은땀을 흘립니다.

"그럼 어떻게 해야 하죠?" 최운영 씨가 옆에서 설명합니다. "ConfigMap과 Secret을 사용하세요.

설정과 코드를 분리하는 거예요." ConfigMap과 Secret이란? 쉽게 비유하자면, ConfigMap은 회사의 공개 게시판이고 Secret은 금고입니다. 공개 게시판에는 회사 주소, 전화번호, 업무 시간 같은 일반 정보를 게시합니다.

누구나 볼 수 있지만 문제없는 정보들입니다. 반면 금고에는 직원 급여 정보, 회사 계좌 비밀번호 같은 민감한 정보를 보관합니다.

열쇠를 가진 사람만 접근할 수 있습니다. ConfigMap은 데이터베이스 주소, 포트 번호, 로그 레벨처럼 공개되어도 괜찮은 설정을 저장합니다.

Secret은 비밀번호, API 키, 인증서처럼 외부에 노출되면 안 되는 정보를 암호화하여 저장합니다. 설정을 코드에 넣으면 생기는 문제 예전에는 개발자들이 설정값을 코드에 직접 작성했습니다.

개발 환경, 테스트 환경, 프로덕션 환경마다 데이터베이스 주소가 다릅니다. 환경이 바뀔 때마다 코드를 수정하고 다시 빌드해야 했습니다.

실수로 프로덕션 비밀번호를 개발 환경에 사용하거나, 반대로 개발 비밀번호를 프로덕션에 사용하는 일도 생겼습니다. 더 큰 문제는 보안입니다.

코드에 비밀번호를 넣으면 Git 히스토리에 영원히 남습니다. 나중에 삭제해도 이전 커밋에는 남아있어서, 누구나 찾아볼 수 있습니다.

실제로 GitHub에 AWS 키가 올라가서 수천만 원의 요금 폭탄을 맞은 사례도 많습니다. ConfigMap과 Secret의 장점 ConfigMap과 Secret을 사용하면 이런 문제가 해결됩니다.

첫째, 코드와 설정이 분리됩니다. 같은 컨테이너 이미지를 여러 환경에서 사용할 수 있습니다.

환경마다 다른 ConfigMap과 Secret을 만들어서 주입하면 됩니다. 코드는 한 번만 빌드하고 어디서든 실행할 수 있습니다.

둘째, 보안이 강화됩니다. Secret은 base64로 인코딩되어 저장되고, etcd에 암호화되어 보관됩니다.

RBAC으로 접근 권한도 제어할 수 있습니다. 개발자는 프로덕션 Secret을 볼 수 없게 설정할 수 있습니다.

셋째, 동적 업데이트가 가능합니다. ConfigMap을 수정하면 파드를 재시작하지 않고도 설정이 변경됩니다.

물론 애플리케이션이 파일 변경을 감지하도록 구현해야 합니다. 코드 분석 위의 예제를 자세히 살펴봅시다.

첫 번째 ConfigMap은 data 필드에 키-값 쌍으로 설정을 저장합니다. database.hostdatabase.port는 데이터베이스 연결 정보입니다.

누구나 볼 수 있으므로 민감하지 않은 정보만 넣습니다. 두 번째 Secret은 stringData 필드를 사용했습니다.

이 방식은 평문으로 작성하면 쿠버네티스가 자동으로 base64 인코딩합니다. data 필드를 사용하면 직접 base64로 인코딩된 값을 넣어야 합니다.

세 번째 Pod 정의에서는 환경변수로 주입합니다. configMapKeyRef로 ConfigMap의 값을, secretKeyRef로 Secret의 값을 가져옵니다.

애플리케이션은 process.env.DB_HOSTos.environ['DB_PASSWORD']로 사용하면 됩니다. 실무 활용 사례 실제 회사에서는 어떻게 사용할까요?

환경별로 다른 ConfigMap을 만듭니다. app-config-dev, app-config-staging, app-config-prod처럼 이름을 구분합니다.

개발 환경에서는 app-config-dev를, 프로덕션에서는 app-config-prod를 사용합니다. 코드는 그대로인데 환경만 바뀌는 것입니다.

Secret은 더 민감하게 관리합니다. 프로덕션 Secret은 보안팀만 만들 수 있게 RBAC으로 제한합니다.

개발자는 자기 네임스페이스의 Secret만 관리할 수 있습니다. 주기적으로 비밀번호를 변경하고, 변경 이력도 감사 로그로 남깁니다.

고급 기술로는 Sealed SecretsExternal Secrets Operator를 사용합니다. Git에 암호화된 Secret을 저장하거나, AWS Secrets Manager 같은 외부 시스템과 연동할 수 있습니다.

주의사항 ConfigMap과 Secret을 사용할 때 주의점이 있습니다. 초보자들이 자주 하는 실수는 Secret에 평문을 넣은 줄 알고 안심하는 것입니다.

base64는 암호화가 아니라 인코딩입니다. 누구나 디코딩할 수 있습니다.

진짜 보안을 위해서는 etcd 암호화를 활성화하고, RBAC으로 접근을 제한해야 합니다. 또한 Secret을 환경변수로 주입하면 로그에 노출될 위험이 있습니다.

가능하면 볼륨으로 마운트해서 파일로 읽는 것이 더 안전합니다. ConfigMap이 너무 크면 etcd 용량 문제가 생길 수 있습니다.

큰 파일은 PersistentVolume을 사용하는 것이 좋습니다. 정리 이쿠버 씨는 이제 코드에서 모든 설정을 제거하고 ConfigMap과 Secret으로 옮겼습니다.

"이제 GitHub에 올려도 안전하겠어요!" 설정과 코드를 분리하면 보안도 강화되고 운영도 편리해집니다. 환경이 바뀌어도 코드는 그대로, ConfigMap만 바꾸면 됩니다.

여러분도 민감한 정보는 반드시 Secret으로 관리하세요.

실전 팁

💡 - kubectl create configmapkubectl create secret 명령어로 쉽게 생성할 수 있습니다.

  • Secret을 볼륨으로 마운트하면 /etc/secrets/password 같은 파일로 사용할 수 있어 더 안전합니다.
  • ConfigMap 변경을 자동으로 감지하려면 애플리케이션이 파일 시스템 이벤트를 리스닝해야 합니다.

4. PersistentVolume

이쿠버 씨의 데이터베이스 파드가 재시작되었는데, 모든 데이터가 사라졌습니다. "어제까지 있던 데이터가 다 날아갔어요!" 패닉에 빠진 이쿠버 씨에게 최운영 씨가 말합니다.

"PersistentVolume을 안 쓰셨군요."

**PersistentVolume(PV)**은 클러스터의 저장 공간이고, **PersistentVolumeClaim(PVC)**은 그 공간을 요청하는 리소스입니다. 마치 아파트 임대와 같습니다.

건물주가 아파트를 제공하면, 세입자가 신청서를 내서 사용합니다. 파드가 재시작되어도 데이터는 영구적으로 보존됩니다.

다음 코드를 살펴봅시다.

# PersistentVolume: 실제 스토리지
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce  # 한 노드에서만 읽기/쓰기
  hostPath:
    path: /data/mysql  # 로컬 디렉토리 (테스트용)
---
# PersistentVolumeClaim: 스토리지 요청
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
# Pod에서 사용하기
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - name: mysql
    image: mysql:8.0
    volumeMounts:
    - name: mysql-storage
      mountPath: /var/lib/mysql  # 컨테이너 내부 경로
  volumes:
  - name: mysql-storage
    persistentVolumeClaim:
      claimName: mysql-pvc

이쿠버 씨는 MySQL 데이터베이스를 파드로 실행하고 있었습니다. 며칠 동안 열심히 테스트 데이터를 입력했고, 모든 게 잘 동작했습니다.

그런데 어느 날 아침, 파드가 재시작되었습니다. "어?

데이터가 다 사라졌어요!" 이쿠버 씨가 외칩니다. 데이터베이스에 접속해보니 텅 비어있습니다.

며칠간의 작업이 물거품이 되었습니다. 최운영 씨가 달려옵니다.

"PersistentVolume을 설정하지 않으셨군요. 컨테이너 안의 데이터는 파드가 죽으면 같이 사라집니다." PersistentVolume이란? 쉽게 비유하자면, PersistentVolume은 외장 하드디스크와 같습니다.

컴퓨터 내장 디스크에 저장한 파일은 컴퓨터가 고장나면 사라집니다. 하지만 외장 하드디스크에 저장하면 컴퓨터를 바꿔도 파일이 남아있습니다.

PersistentVolume도 마찬가지입니다. 파드 내부에 저장한 데이터는 파드가 죽으면 사라지지만, PersistentVolume에 저장하면 파드가 재생성되어도 데이터가 유지됩니다.

PersistentVolume이 없던 시절 초기 컨테이너 환경에서는 데이터 영속성이 큰 문제였습니다. 데이터베이스를 컨테이너로 실행하면 파드가 재시작될 때마다 데이터가 날아갔습니다.

그래서 개발자들은 데이터베이스만큼은 컨테이너 밖에서 따로 운영했습니다. 쿠버네티스의 장점을 활용하지 못하는 것이죠.

일부 개발자들은 호스트 경로를 직접 마운트했습니다. 하지만 파드가 다른 노드로 이동하면 데이터를 찾을 수 없었습니다.

수동으로 데이터를 옮기거나, 복잡한 스크립트를 작성해야 했습니다. PersistentVolume과 PVC의 분리 쿠버네티스는 스토리지를 두 단계로 나눴습니다.

**PersistentVolume(PV)**는 관리자가 생성합니다. "우리 클러스터에는 10GB짜리 디스크가 있어"라고 선언하는 것입니다.

AWS EBS, GCP Persistent Disk, NFS 등 다양한 실제 스토리지를 추상화합니다. **PersistentVolumeClaim(PVC)**는 개발자가 생성합니다.

"나는 10GB 디스크가 필요해"라고 요청하는 것입니다. 쿠버네티스가 조건에 맞는 PV를 찾아서 자동으로 바인딩합니다.

이렇게 분리하면 개발자는 실제 스토리지 세부사항을 몰라도 됩니다. 그냥 "10GB 필요해요"라고 말하면, 관리자가 미리 준비한 스토리지 중에서 자동으로 할당됩니다.

AccessMode의 중요성 스토리지에는 세 가지 접근 모드가 있습니다. **ReadWriteOnce(RWO)**는 한 노드에서만 읽고 쓸 수 있습니다.

대부분의 클라우드 블록 스토리지가 이 모드입니다. 데이터베이스처럼 단일 파드만 접근하는 경우에 적합합니다.

**ReadOnlyMany(ROX)**는 여러 노드에서 읽기만 가능합니다. 정적 파일을 여러 파드에서 공유할 때 유용합니다.

**ReadWriteMany(RWX)**는 여러 노드에서 동시에 읽고 쓸 수 있습니다. NFS 같은 공유 스토리지에서 지원합니다.

여러 파드가 동시에 로그 파일을 쓰는 경우에 필요합니다. 코드 분석 위의 예제를 단계별로 살펴봅시다.

첫 번째, PersistentVolume을 만듭니다. capacity.storage: 10Gi로 크기를 지정하고, accessModes: ReadWriteOnce로 한 노드만 사용하도록 합니다.

hostPath는 테스트용이고, 실제로는 AWS EBS나 NFS를 사용합니다. 두 번째, PersistentVolumeClaim을 만듭니다.

"10Gi 크기의 ReadWriteOnce 스토리지를 주세요"라고 요청합니다. 쿠버네티스가 조건에 맞는 PV를 찾아서 바인딩합니다.

세 번째, Pod에서 사용합니다. volumes에서 PVC를 참조하고, volumeMounts에서 컨테이너 내부 경로에 마운트합니다.

MySQL은 /var/lib/mysql에 데이터를 저장하므로, 이 경로를 PVC에 연결하면 데이터가 영구 보존됩니다. 실무 활용 사례 실제 프로덕션에서는 어떻게 사용할까요?

데이터베이스는 반드시 PersistentVolume을 사용합니다. MySQL, PostgreSQL, MongoDB 모두 PVC를 통해 데이터를 보관합니다.

클라우드 환경에서는 StorageClass를 사용하여 동적 프로비저닝합니다. PVC만 만들면 자동으로 클라우드 디스크가 생성되고 연결됩니다.

로그 수집 시스템도 PV를 사용합니다. Elasticsearch나 Prometheus는 대용량 데이터를 저장하므로 큰 PV가 필요합니다.

성능을 위해 SSD 기반 스토리지를 선택합니다. 파일 업로드 서비스는 ReadWriteMany 모드가 필요합니다.

여러 웹 서버 파드가 동시에 업로드된 파일에 접근해야 하므로, NFS나 클라우드 파일 스토리지를 사용합니다. 주의사항 PersistentVolume을 사용할 때 주의할 점이 있습니다.

초보자들이 자주 하는 실수는 hostPath를 프로덕션에서 사용하는 것입니다. hostPath는 특정 노드의 로컬 디렉토리를 마운트하므로, 파드가 다른 노드로 이동하면 데이터를 찾을 수 없습니다.

로컬 테스트에만 사용하세요. 또한 PVC를 삭제하면 기본적으로 PV도 삭제됩니다.

실수로 PVC를 지우면 데이터가 날아갈 수 있습니다. 중요한 데이터는 백업을 꼭 하세요.

AccessMode를 잘못 선택하면 파드가 시작되지 않습니다. AWS EBS는 RWX를 지원하지 않으므로, 여러 노드에서 접근하려면 EFS 같은 다른 스토리지를 사용해야 합니다.

정리 이쿠버 씨는 이제 모든 Stateful 애플리케이션에 PersistentVolume을 설정했습니다. "이제 파드가 재시작되어도 데이터가 안전해요!" PersistentVolume을 제대로 사용하면 쿠버네티스에서도 안전하게 데이터를 관리할 수 있습니다.

데이터베이스, 로그, 파일 업로드 모두 걱정없습니다. 여러분도 중요한 데이터는 반드시 PV에 저장하세요.

실전 팁

💡 - kubectl get pv,pvc로 PV와 PVC의 바인딩 상태를 확인하세요.

  • StorageClass를 사용하면 PVC만 만들어도 자동으로 PV가 생성됩니다 (동적 프로비저닝).
  • 중요한 데이터는 스냅샷을 주기적으로 생성하여 백업하세요.

5. Namespace 활용

회사에 프로젝트가 여러 개 생기면서 클러스터가 복잡해졌습니다. 개발팀 리소스와 운영팀 리소스가 뒤섞여 이쿠버 씨는 혼란스럽습니다.

"어느 게 우리 팀 거죠?" 최운영 씨가 답합니다. "Namespace로 분리해야죠."

Namespace는 쿠버네티스 클러스터를 가상으로 분할하는 방법입니다. 마치 아파트 동 구분과 같습니다.

101동, 102동으로 나누면 같은 호수여도 구분되듯이, Namespace로 팀이나 환경을 분리합니다. 리소스 충돌을 방지하고 권한을 격리할 수 있습니다.

다음 코드를 살펴봅시다.

# Namespace 생성
apiVersion: v1
kind: Namespace
metadata:
  name: development
---
# Namespace에 리소스 배포
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: development  # 개발 네임스페이스에 배포
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
---
# ResourceQuota: 네임스페이스별 리소스 제한
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: development
spec:
  hard:
    requests.cpu: "10"      # 최대 10 CPU
    requests.memory: "20Gi"  # 최대 20GB 메모리
    pods: "50"               # 최대 50개 파드

회사가 성장하면서 프로젝트가 늘어났습니다. 쇼핑몰 프로젝트, 관리자 시스템, 데이터 분석 시스템이 모두 같은 클러스터에서 돌아갑니다.

이쿠버 씨는 kubectl get pods 명령어를 실행할 때마다 수백 개의 파드가 나열되어 어지럽습니다. "우리 팀 파드가 어디 있죠?" 이쿠버 씨가 묻습니다.

최운영 씨가 한숨을 쉽니다. "Namespace를 안 나눠서 그래요.

지금은 전부 default 네임스페이스에 있어서 뒤죽박죽이에요." Namespace란 무엇일까요? 쉽게 비유하자면, Namespace는 아파트 단지의 동 번호와 같습니다. 큰 아파트 단지에는 101동, 102동, 103동이 있습니다.

각 동에는 같은 호수가 있을 수 있습니다. 101동 301호와 102동 301호는 다른 집입니다.

Namespace도 마찬가지입니다. development 네임스페이스의 web-app과 production 네임스페이스의 web-app은 완전히 다른 리소스입니다.

이렇게 분리하면 팀별로, 환경별로, 프로젝트별로 리소스를 깔끔하게 관리할 수 있습니다. Namespace가 없으면 생기는 문제 모든 리소스를 default 네임스페이스에 넣으면 어떤 문제가 생길까요?

첫째, 이름 충돌입니다. A팀과 B팀이 모두 web-service라는 이름을 사용하려고 하면 충돌합니다.

누군가는 이름을 바꿔야 합니다. a-team-web-service, b-team-web-service 같은 긴 이름을 사용하면 복잡해집니다.

둘째, 권한 관리가 어렵습니다. A팀 개발자가 실수로 B팀의 리소스를 삭제할 수 있습니다.

모두가 모든 걸 볼 수 있으므로 보안도 취약합니다. 셋째, 리소스 제한이 불가능합니다.

A팀이 클러스터 리소스를 독차지하면 B팀은 파드를 띄울 수 없습니다. 공평하게 나누는 메커니즘이 없습니다.

Namespace의 실전 활용 실제 회사에서는 어떻게 Namespace를 나눌까요? 가장 흔한 방법은 환경별 분리입니다.

development, staging, production 세 개의 네임스페이스를 만듭니다. 개발 중인 기능은 development에, 테스트는 staging에, 실제 서비스는 production에 배포합니다.

환경별로 설정이 다르므로 분리하면 실수를 줄일 수 있습니다. 또 다른 방법은 팀별 분리입니다.

team-backend, team-frontend, team-data 같은 네임스페이스를 만듭니다. 각 팀은 자기 네임스페이스만 관리합니다.

다른 팀 리소스를 실수로 건드릴 일이 없습니다. 큰 회사에서는 프로젝트별 분리도 합니다.

project-shopping-mall, project-admin, project-analytics처럼 프로젝트마다 네임스페이스를 만듭니다. 프로젝트가 끝나면 네임스페이스 전체를 삭제하면 깔끔합니다.

ResourceQuota로 자원 통제 Namespace만으로는 부족합니다. ResourceQuota를 함께 사용해야 합니다.

최운영 씨가 설명합니다. "개발팀에게 무제한 리소스를 주면, 실수로 파드를 1000개 띄울 수도 있어요.

ResourceQuota로 상한선을 정해야 합니다." 위의 예제에서 requests.cpu: "10"은 개발 네임스페이스가 최대 10개의 CPU 코어를 사용할 수 있다는 뜻입니다. pods: "50"은 최대 50개의 파드만 생성할 수 있습니다.

이렇게 제한하면 한 팀이 클러스터 전체를 독차지하는 일을 막을 수 있습니다. 코드 분석 위의 예제를 살펴봅시다.

첫 번째, Namespace를 생성합니다. 이름은 development입니다.

이제 이 네임스페이스 안에 리소스를 배포할 수 있습니다. 두 번째, Deployment를 배포할 때 metadata.namespace: development를 지정합니다.

이 Deployment는 development 네임스페이스에 속합니다. 다른 네임스페이스에서는 보이지 않습니다.

세 번째, ResourceQuota를 설정합니다. development 네임스페이스는 CPU 10개, 메모리 20GB, 파드 50개까지만 사용할 수 있습니다.

이 한도를 넘으면 새로운 파드가 생성되지 않습니다. 크로스 네임스페이스 통신 다른 네임스페이스의 서비스와 통신하려면 어떻게 할까요?

Service는 DNS 이름을 가집니다. 같은 네임스페이스 안에서는 짧은 이름으로 접근합니다.

http://api-service처럼요. 하지만 다른 네임스페이스에 있다면 전체 도메인 이름을 사용해야 합니다.

http://api-service.production.svc.cluster.local처럼 말이죠. 형식은 <서비스이름>.<네임스페이스>.svc.cluster.local입니다.

이렇게 하면 네임스페이스를 넘어서도 통신할 수 있습니다. 주의사항 Namespace를 사용할 때 주의할 점이 있습니다.

초보자들이 자주 하는 실수는 모든 리소스가 네임스페이스에 속한다고 생각하는 것입니다. NodePersistentVolume 같은 일부 리소스는 클러스터 수준이므로 네임스페이스에 속하지 않습니다.

kubectl api-resources --namespaced=false로 확인할 수 있습니다. 또한 네임스페이스를 너무 많이 만들면 관리가 복잡해집니다.

적절한 균형이 필요합니다. 보통 5~10개 정도가 적당합니다.

네임스페이스를 삭제하면 그 안의 모든 리소스가 삭제됩니다. 실수로 production 네임스페이스를 지우면 서비스가 전부 다운됩니다.

조심하세요. 정리 이쿠버 씨의 회사는 이제 환경별, 팀별로 Namespace를 나눴습니다.

kubectl get pods -n development 명령어로 개발 환경만 깔끔하게 볼 수 있습니다. Namespace를 제대로 활용하면 클러스터 관리가 훨씬 쉬워집니다.

권한도 분리되고, 리소스도 공평하게 나눌 수 있습니다. 여러분도 프로젝트 규모가 커지면 Namespace로 정리해 보세요.

실전 팁

💡 - kubectl config set-context --current --namespace=development로 기본 네임스페이스를 변경할 수 있습니다.

  • kubectl get all -n <namespace>로 네임스페이스의 모든 리소스를 한눈에 볼 수 있습니다.
  • 네임스페이스마다 NetworkPolicy를 설정하여 네트워크도 격리할 수 있습니다.

6. Label과 Selector

이쿠버 씨는 파드가 수십 개로 늘어나자 관리가 어려워졌습니다. "프론트엔드 파드만 재시작하려면 어떻게 하죠?" 최운영 씨가 Label을 가리키며 말합니다.

"바로 이걸 위해 Label이 있어요."

Label은 쿠버네티스 리소스에 붙이는 태그입니다. Selector는 그 Label로 리소스를 선택하는 방법입니다.

마치 도서관의 분류 시스템과 같습니다. 책에 과학, 문학, 역사 라벨을 붙이면, 나중에 필요한 카테고리만 찾을 수 있습니다.

다음 코드를 살펴봅시다.

# Label이 있는 Pod
apiVersion: v1
kind: Pod
metadata:
  name: frontend-pod-1
  labels:
    app: web           # 애플리케이션 이름
    tier: frontend     # 계층
    environment: prod  # 환경
spec:
  containers:
  - name: nginx
    image: nginx:1.21
---
# Selector로 선택하는 Service
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    tier: frontend    # tier=frontend인 파드들을 선택
  ports:
  - port: 80
    targetPort: 80
---
# matchLabels와 matchExpressions 사용
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deploy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
    matchExpressions:
    - key: environment
      operator: In
      values:
      - prod
      - staging  # prod 또는 staging인 파드만 선택

프로젝트가 커지면서 파드가 폭발적으로 늘어났습니다. 프론트엔드 파드, 백엔드 파드, 데이터베이스 파드, 캐시 파드가 섞여 있습니다.

이쿠버 씨는 프론트엔드만 업데이트하려고 하는데, 어떤 파드가 프론트엔드인지 찾기가 어렵습니다. "파드 이름에 frontend라는 단어가 있긴 한데, 하나하나 찾기 너무 힘들어요." 이쿠버 씨가 말합니다.

최운영 씨가 화이트보드에 Label 개념을 그립니다. Label이란 무엇일까요? 쉽게 비유하자면, Label은 마트에서 상품에 붙이는 스티커와 같습니다.

과일 코너에 가면 사과에 국내산, 수입산, 유기농 같은 스티커가 붙어있습니다. 고객은 원하는 스티커를 보고 상품을 고릅니다.

Label도 똑같습니다. 파드에 tier: frontend, environment: prod 같은 라벨을 붙이면, 나중에 필요한 조건의 파드만 골라낼 수 있습니다.

Label은 키-값 쌍입니다. app: web, version: v2.0, team: backend처럼 의미 있는 정보를 담습니다.

하나의 리소스에 여러 개의 Label을 붙일 수 있습니다. Label이 없으면 생기는 문제 Label 없이 리소스를 관리하면 어떤 일이 생길까요?

개발자는 파드 이름으로만 구분해야 합니다. frontend-web-prod-1, frontend-web-prod-2 같은 긴 이름을 사용합니다.

이름 규칙이 복잡해지고, 실수도 잦아집니다. 특정 그룹의 파드를 한 번에 선택하는 것도 불가능합니다.

Service는 어떤 파드로 트래픽을 보낼지 판단할 수 없습니다. Deployment는 어떤 파드를 관리해야 할지 모릅니다.

수동으로 일일이 연결해야 하므로 자동화가 불가능합니다. Selector의 역할 Label을 붙였으면 이제 선택할 차례입니다.

Selector가 그 역할을 합니다. 최운영 씨가 설명합니다.

"Label은 스티커를 붙이는 행위고, Selector는 그 스티커로 필터링하는 행위예요. Service, Deployment, ReplicaSet 모두 Selector를 사용합니다." 예를 들어 Service에서 selector: {tier: frontend}라고 쓰면, tier: frontend 라벨을 가진 모든 파드로 트래픽을 보냅니다.

파드가 새로 생성되거나 삭제되어도, Label만 맞으면 자동으로 포함되거나 제외됩니다. 두 가지 Selector 방식 Selector에는 두 가지 방식이 있습니다.

첫째, matchLabels는 단순한 등호 매칭입니다. matchLabels: {app: web, tier: frontend}는 app이 web이고 tier가 frontend인 파드만 선택합니다.

간단하고 직관적입니다. 둘째, matchExpressions는 더 복잡한 조건을 표현합니다.

operator: In은 값이 목록 중 하나와 일치하는지 확인합니다. operator: NotIn은 제외하고, operator: Exists는 키가 있기만 하면 됩니다.

위의 예제에서 matchExpressions는 environment가 prod 또는 staging인 파드를 선택합니다. development는 제외됩니다.

이런 유연성이 실무에서 매우 유용합니다. 코드 분석 위의 예제를 자세히 살펴봅시다.

첫 번째 Pod는 세 개의 Label을 가지고 있습니다. app: web은 애플리케이션 이름, tier: frontend는 계층 구조, environment: prod는 환경입니다.

이 세 가지 축으로 파드를 분류합니다. 두 번째 Service는 selector: {tier: frontend}로 프론트엔드 파드만 선택합니다.

백엔드나 데이터베이스 파드는 무시됩니다. 이 Service로 들어온 트래픽은 프론트엔드 파드들에게만 분배됩니다.

세 번째 Deployment는 matchLabelsmatchExpressions를 조합했습니다. app이 api이면서, environment가 prod나 staging인 파드를 관리합니다.

development 환경은 별도의 Deployment가 관리합니다. 실무 활용 사례 실제 프로젝트에서는 어떻게 Label을 활용할까요?

일반적인 Label 구조는 이렇습니다. app은 애플리케이션 이름, version은 버전, tier는 frontend/backend/database 같은 계층, environment는 dev/staging/prod 같은 환경입니다.

이 네 가지만 있어도 대부분의 상황을 커버할 수 있습니다. 카나리 배포를 할 때도 Label이 유용합니다.

version: v1.0 파드와 version: v2.0 파드를 함께 띄우고, Service가 10%는 v2.0으로, 90%는 v1.0으로 보내도록 설정합니다. Label로 버전을 구분하기 때문에 가능합니다.

모니터링 시스템도 Label을 활용합니다. Prometheus는 Label로 메트릭을 필터링합니다.

{app="web", environment="prod"}처럼 쿼리를 작성하면 프로덕션 웹 서버의 메트릭만 볼 수 있습니다. kubectl과 Label kubectl 명령어에서도 Label을 적극 활용합니다.

kubectl get pods -l app=web은 app이 web인 파드만 조회합니다. kubectl delete pods -l tier=frontend는 프론트엔드 파드만 삭제합니다.

일일이 이름을 지정할 필요가 없습니다. kubectl label pods frontend-pod-1 version=v2.0으로 기존 파드에 새로운 Label을 추가할 수 있습니다.

kubectl label pods frontend-pod-1 version-로 Label을 삭제할 수도 있습니다. 주의사항 Label을 사용할 때 주의할 점이 있습니다.

초보자들이 자주 하는 실수는 Label을 너무 많이 붙이는 것입니다. 필요한 것만 붙이세요.

Label이 10개, 20개가 되면 관리가 복잡해집니다. 보통 3~5개면 충분합니다.

또한 Label 키와 값에는 제약이 있습니다. 키는 63자 이하여야 하고, 영문 소문자, 숫자, 대시, 언더스코어만 사용할 수 있습니다.

한글이나 특수문자는 안 됩니다. Selector를 잘못 작성하면 파드를 찾지 못합니다.

app: webapp: Web은 다릅니다. 대소문자를 구분하므로 정확히 써야 합니다.

Annotation과의 차이 Label과 비슷한 것으로 Annotation이 있습니다. 최운영 씨가 설명합니다.

"Label은 선택하기 위한 것이고, Annotation은 메타데이터를 기록하기 위한 것입니다. Selector는 Label만 사용할 수 있어요." Annotation은 배포 담당자 이름, 변경 사유, 외부 시스템 ID 같은 정보를 저장합니다.

쿠버네티스가 선택 로직에 사용하지 않으므로 제약이 적습니다. 긴 문자열이나 JSON도 저장할 수 있습니다.

정리 이쿠버 씨는 이제 모든 리소스에 일관된 Label을 붙였습니다. kubectl get pods -l tier=frontend로 프론트엔드 파드만 한눈에 볼 수 있습니다.

Label과 Selector를 제대로 활용하면 수백 개의 리소스도 쉽게 관리할 수 있습니다. 그룹으로 묶어서 한 번에 조작하고, 자동으로 연결되도록 만들 수 있습니다.

여러분도 처음부터 Label을 잘 설계해서 사용하세요.

실전 팁

💡 - Label 키에 도메인을 붙이면 충돌을 방지할 수 있습니다. 예: mycompany.com/app: web

  • kubectl get pods --show-labels로 모든 파드의 Label을 한눈에 확인하세요.
  • 일관된 Label 체계를 팀 전체가 공유하면 협업이 훨씬 수월해집니다.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Kubernetes#Deployment#Service#ConfigMap#PersistentVolume

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.