🤖

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

⚠️

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

이미지 로딩 중...

Kubernetes 기본 개념 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 10. 30. · 16 Views

Kubernetes 기본 개념 완벽 가이드

컨테이너 오케스트레이션의 표준인 Kubernetes의 핵심 개념들을 실무 중심으로 깊이 있게 다룹니다. Pod, Service, Deployment 등 필수 리소스부터 실제 운영 노하우까지 초급 개발자도 쉽게 이해할 수 있도록 구성했습니다.


목차

  1. Pod - 쿠버네티스의 최소 실행 단위
  2. Service - 안정적인 네트워크 엔드포인트
  3. Deployment - 선언적 배포 관리
  4. ConfigMap - 설정 정보 외부화
  5. Secret - 민감한 정보 관리
  6. Namespace - 리소스 격리 및 관리
  7. Volume - 데이터 영속성과 공유
  8. Ingress - HTTP 라우팅과 로드밸런싱
  9. StatefulSet - Stateful 애플리케이션 관리

1. Pod - 쿠버네티스의 최소 실행 단위

시작하며

여러분이 Docker 컨테이너로 애플리케이션을 만들었다면, 이제 이것을 실제 서비스로 배포해야 하는 순간이 옵니다. 하지만 단순히 컨테이너 하나를 실행하는 것만으로는 부족합니다.

로그를 수집하는 사이드카가 필요하거나, 설정 파일을 동기화하는 헬퍼 컨테이너가 필요한 경우가 많죠. 이런 문제는 실제 개발 현장에서 자주 발생합니다.

하나의 애플리케이션이 여러 개의 밀접하게 연관된 컨테이너로 구성되어야 하는데, 이들을 어떻게 함께 관리할지 고민하게 됩니다. 바로 이럴 때 필요한 것이 Pod입니다.

Pod는 이러한 관련 컨테이너들을 하나의 논리적 단위로 묶어서 함께 스케줄링하고 관리할 수 있게 해줍니다.

개요

간단히 말해서, Pod는 쿠버네티스에서 생성하고 관리할 수 있는 가장 작은 배포 단위입니다. Pod가 필요한 이유는 실무에서 애플리케이션이 단순히 하나의 컨테이너로만 구성되지 않기 때문입니다.

예를 들어, 메인 웹 서버 컨테이너와 함께 로그를 수집하는 Fluentd 컨테이너, 메트릭을 수집하는 Prometheus Exporter 컨테이너가 함께 동작해야 하는 경우가 매우 유용합니다. 전통적인 방법과의 비교를 해보면, 기존에는 같은 VM에 여러 프로세스를 띄워서 관리했다면, 이제는 각각을 컨테이너로 분리하되 Pod라는 단위로 묶어서 관리할 수 있습니다.

Pod의 핵심 특징은 다음과 같습니다. 첫째, 같은 Pod 내의 컨테이너들은 네트워크 네임스페이스를 공유하여 localhost로 통신할 수 있습니다.

둘째, 스토리지 볼륨을 공유하여 파일 시스템을 통한 데이터 교환이 가능합니다. 셋째, 항상 같은 노드에 함께 스케줄링되어 생명주기를 같이합니다.

이러한 특징들이 마이크로서비스 아키텍처에서 밀접하게 결합된 컨테이너들을 효율적으로 관리하는 데 핵심적인 역할을 합니다.

코드 예제

# 기본 Pod 정의 - Nginx 웹 서버 예제
apiVersion: v1
kind: Pod
metadata:
  name: my-web-app
  labels:
    app: web
    tier: frontend
spec:
  containers:
  - name: nginx
    image: nginx:1.21
    ports:
    - containerPort: 80
    # 리소스 제한 설정으로 안정성 확보
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"

설명

이것이 하는 일은 Nginx 웹 서버를 실행하는 가장 기본적인 Pod를 정의하는 것입니다. 이 YAML 파일 하나로 쿠버네티스 클러스터에 컨테이너화된 애플리케이션을 배포할 수 있습니다.

첫 번째로, apiVersionkind는 쿠버네티스에게 어떤 종류의 리소스를 생성할지 알려줍니다. v1 API 버전의 Pod 리소스를 생성하겠다는 선언이죠.

그 다음 metadata 섹션에서는 Pod의 이름을 my-web-app으로 지정하고, labels를 통해 나중에 이 Pod를 검색하거나 그룹화할 수 있는 태그를 붙입니다. 이 라벨은 Service와 연결할 때 매우 중요한 역할을 합니다.

두 번째로, spec 섹션이 실행되면서 실제 컨테이너의 사양을 정의합니다. containers 배열 안에 실행할 컨테이너 정보를 나열하는데, 여기서는 nginx:1.21 이미지를 사용하는 컨테이너 하나를 정의했습니다.

ports 필드는 컨테이너가 80번 포트를 사용한다는 것을 문서화하는 역할을 합니다. 마지막으로, resources.limits 섹션이 메모리 128MB와 CPU 0.5 코어로 리소스를 제한하여 최종적으로 안정적인 운영 환경을 만들어냅니다.

이 제한이 없으면 컨테이너가 노드의 모든 리소스를 사용해버려 다른 애플리케이션에 영향을 줄 수 있습니다. 여러분이 이 코드를 사용하면 클러스터의 어느 노드에서든 자동으로 스케줄링되어 실행되는 웹 서버를 얻을 수 있습니다.

kubectl apply 명령어 하나로 배포되고, 장애가 발생하면 자동으로 재시작되며, 리소스가 격리되어 안전하게 운영됩니다.

실전 팁

💡 개발 환경에서는 kubectl run nginx --image=nginx 명령어로 빠르게 Pod를 생성할 수 있지만, 프로덕션에서는 반드시 YAML 파일로 관리하여 버전 관리와 재현성을 확보하세요.

💡 Pod에 직접 접근하려면 kubectl exec -it my-web-app -- /bin/bash로 컨테이너 내부 셸에 접속할 수 있으며, 이는 디버깅 시 매우 유용합니다.

💡 항상 resources.limitsresources.requests를 설정하세요. 이를 통해 QoS(Quality of Service) 클래스가 결정되고, 리소스 부족 시 어떤 Pod를 먼저 종료할지 결정됩니다.

💡 kubectl logs my-web-app으로 컨테이너 로그를 확인할 수 있으며, -f 옵션을 추가하면 실시간으로 로그를 스트리밍할 수 있어 문제 해결이 빨라집니다.

💡 Pod는 일시적(ephemeral)인 존재라는 것을 기억하세요. 재시작될 때마다 IP가 변경되므로, 직접 Pod IP에 의존하지 말고 Service를 통해 접근해야 합니다.


2. Service - 안정적인 네트워크 엔드포인트

시작하며

여러분이 쿠버네티스에 애플리케이션을 배포했다면, 이제 이것에 어떻게 접근할지 고민하게 됩니다. Pod는 재시작될 때마다 IP가 바뀌고, 여러 개의 동일한 Pod가 실행 중일 때 어느 것에 요청을 보내야 할까요?

이런 문제는 실제 개발 현장에서 매우 흔합니다. 마이크로서비스 아키텍처에서 서비스 간 통신을 설정하거나, 외부 사용자가 애플리케이션에 접근하도록 할 때 불안정한 Pod IP로는 안정적인 서비스를 제공할 수 없습니다.

바로 이럴 때 필요한 것이 Service입니다. Service는 Pod 집합에 대한 안정적인 네트워크 엔드포인트를 제공하고, 자동으로 로드 밸런싱까지 처리해줍니다.

개요

간단히 말해서, Service는 논리적인 Pod 집합과 그것들에 접근하는 정책을 정의하는 추상화 계층입니다. Service가 필요한 이유는 Pod의 IP 주소가 동적으로 변경되고, 스케일링으로 인해 Pod의 개수가 변할 수 있기 때문입니다.

예를 들어, 프론트엔드 애플리케이션이 백엔드 API 서버 Pod들에 접근해야 하는데, 이 백엔드 Pod들이 오토스케일링으로 5개에서 20개로 늘어나는 경우에도 프론트엔드는 아무런 변경 없이 계속 통신할 수 있어야 합니다. 전통적인 방법과의 비교를 해보면, 기존에는 로드밸런서를 별도로 설정하고 IP 주소를 수동으로 관리했다면, 이제는 Service 리소스 하나로 자동 디스커버리와 로드밸런싱을 모두 처리할 수 있습니다.

Service의 핵심 특징은 다음과 같습니다. 첫째, Label Selector를 통해 대상 Pod를 동적으로 선택합니다.

둘째, 고정된 Cluster IP를 제공하여 안정적인 엔드포인트를 보장합니다. 셋째, 여러 타입(ClusterIP, NodePort, LoadBalancer)을 지원하여 다양한 접근 방식을 선택할 수 있습니다.

이러한 특징들이 마이크로서비스 간 통신과 외부 노출을 간단하면서도 강력하게 만들어줍니다.

코드 예제

# ClusterIP Service - 내부 통신용
apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  # Label Selector로 대상 Pod 선택
  selector:
    app: web
    tier: frontend
  ports:
  - protocol: TCP
    port: 80        # Service가 노출할 포트
    targetPort: 80  # Pod의 실제 포트
  # type을 지정하지 않으면 기본값은 ClusterIP
  type: ClusterIP

설명

이것이 하는 일은 app=webtier=frontend 라벨을 가진 모든 Pod들을 하나의 안정적인 엔드포인트로 묶어서 내부 통신을 가능하게 하는 것입니다. 첫 번째로, selector 필드는 이 Service가 관리할 Pod들을 선택하는 역할을 합니다.

여기서는 앞서 정의한 Pod의 라벨과 일치하도록 app: webtier: frontend를 지정했습니다. 쿠버네티스는 이 Selector와 일치하는 모든 Pod를 자동으로 찾아서 Endpoint 목록에 추가합니다.

Pod가 새로 생성되거나 삭제되면 자동으로 반영되죠. 두 번째로, ports 섹션이 실행되면서 포트 매핑을 정의합니다.

port: 80은 Service 자체가 노출하는 포트이고, targetPort: 80은 실제 Pod 컨테이너가 리스닝하고 있는 포트입니다. 클라이언트는 Service의 Cluster IP와 80번 포트로 요청을 보내면, 쿠버네티스가 자동으로 해당 요청을 백엔드 Pod들의 80번 포트로 분산시킵니다.

마지막으로, type: ClusterIP가 이 Service를 클러스터 내부에서만 접근 가능한 내부 서비스로 만들어냅니다. 쿠버네티스는 고정된 가상 IP(Cluster IP)를 할당하고, DNS 서버에 web-service.default.svc.cluster.local 같은 도메인 이름을 등록합니다.

여러분이 이 코드를 사용하면 다른 Pod에서 http://web-service 또는 http://web-service.default.svc.cluster.local로 접근할 수 있는 안정적인 엔드포인트를 얻게 됩니다. Pod IP가 변경되어도, Pod 개수가 늘어나거나 줄어들어도 클라이언트는 아무런 변경 없이 계속 통신할 수 있습니다.

실전 팁

💡 Service 타입을 선택할 때는 용도에 맞게 고르세요. ClusterIP는 내부 통신용, NodePort는 테스트/개발용, LoadBalancer는 클라우드 환경의 프로덕션용으로 적합합니다.

💡 kubectl get endpoints web-service 명령어로 Service가 실제로 어떤 Pod IP들을 백엔드로 사용하고 있는지 확인할 수 있으며, 트래픽이 전달되지 않을 때 유용한 디버깅 도구입니다.

💡 Session Affinity가 필요하다면 sessionAffinity: ClientIP를 추가하여 같은 클라이언트의 요청을 항상 같은 Pod로 라우팅할 수 있습니다. 단, 이는 로드밸런싱 효율성을 떨어뜨릴 수 있으니 신중하게 사용하세요.

💡 targetPort는 숫자 대신 컨테이너의 포트 이름을 사용할 수도 있습니다. Pod에서 ports[].name: http로 정의하고 Service에서 targetPort: http로 참조하면 포트 번호 변경 시 유연하게 대응할 수 있습니다.

💡 외부에서 접근해야 한다면 Ingress를 사용하는 것이 LoadBalancer보다 비용 효율적입니다. 하나의 LoadBalancer로 여러 Service를 호스트 기반 또는 경로 기반으로 라우팅할 수 있습니다.


3. Deployment - 선언적 배포 관리

시작하며

여러분이 애플리케이션의 새 버전을 배포해야 하는 상황을 상상해보세요. 다운타임 없이 기존 Pod를 새 버전으로 교체해야 하고, 문제가 발생하면 즉시 이전 버전으로 롤백할 수 있어야 합니다.

이런 문제는 실제 개발 현장에서 가장 중요하고도 까다로운 작업 중 하나입니다. 수동으로 Pod를 하나씩 업데이트하다가 실수로 모든 Pod를 동시에 종료시켜버리면 서비스 전체가 다운될 수 있습니다.

또한 Pod가 비정상 종료되었을 때 자동으로 재시작하는 메커니즘도 필요합니다. 바로 이럴 때 필요한 것이 Deployment입니다.

Deployment는 애플리케이션의 배포와 업데이트를 선언적으로 관리하며, 자동 복구와 롤링 업데이트를 제공합니다.

개요

간단히 말해서, Deployment는 ReplicaSet을 관리하는 상위 개념으로, Pod의 복제본 수를 유지하고 업데이트 전략을 제어하는 컨트롤러입니다. Deployment가 필요한 이유는 프로덕션 환경에서 단순히 Pod를 실행하는 것을 넘어 지속적인 관리가 필요하기 때문입니다.

예를 들어, 트래픽 증가에 대응하여 Pod 개수를 3개에서 10개로 늘리거나, 새 버전을 배포할 때 25%씩 점진적으로 교체하거나, 배포 중 문제가 발생하면 자동으로 중단하는 같은 복잡한 시나리오를 간단한 선언으로 처리할 수 있습니다. 전통적인 방법과의 비교를 해보면, 기존에는 배포 스크립트를 작성하고 블루-그린 배포를 수동으로 관리했다면, 이제는 Deployment 하나로 롤링 업데이트, 롤백, 스케일링을 모두 자동화할 수 있습니다.

Deployment의 핵심 특징은 다음과 같습니다. 첫째, 원하는 상태(Desired State)를 선언하면 쿠버네티스가 현재 상태를 지속적으로 그것에 맞춥니다.

둘째, 롤링 업데이트를 통해 무중단 배포를 기본으로 제공합니다. 셋째, 버전 히스토리를 관리하여 언제든 이전 버전으로 롤백할 수 있습니다.

이러한 특징들이 안정적이고 지속 가능한 서비스 운영의 기반이 됩니다.

코드 예제

# Deployment로 복제본 관리 및 롤링 업데이트
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
spec:
  # 유지할 Pod 복제본 수
  replicas: 3
  selector:
    matchLabels:
      app: web
  # Pod 템플릿 정의
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
  # 롤링 업데이트 전략
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1  # 동시에 중단 가능한 Pod 수
      maxSurge: 1        # 초과 생성 가능한 Pod 수

설명

이것이 하는 일은 Nginx 웹 서버 3개를 항상 실행 상태로 유지하고, 새 버전 배포 시 점진적으로 교체하는 것입니다. 첫 번째로, replicas: 3은 쿠버네티스에게 이 애플리케이션의 Pod를 항상 3개 유지하라고 지시합니다.

만약 노드 장애나 Pod 크래시로 인해 실행 중인 Pod가 2개로 줄어들면, Deployment 컨트롤러가 자동으로 새 Pod를 생성하여 다시 3개로 만듭니다. selector.matchLabels는 이 Deployment가 관리할 Pod를 식별하는 데 사용되며, template.metadata.labels와 일치해야 합니다.

두 번째로, template 섹션이 실행되면서 생성할 Pod의 청사진을 정의합니다. 이 부분은 앞서 본 Pod 정의와 동일한 구조입니다.

Deployment는 이 템플릿을 사용하여 필요한 만큼의 Pod를 생성하고 관리합니다. 여기서 중요한 점은 Pod를 직접 생성하는 것이 아니라 Deployment가 ReplicaSet을 생성하고, ReplicaSet이 실제 Pod를 생성한다는 것입니다.

마지막으로, strategy 섹션이 업데이트 방식을 제어하여 안전한 배포를 보장합니다. maxUnavailable: 1은 롤링 업데이트 중 최대 1개의 Pod가 동시에 중단될 수 있다는 의미이고, maxSurge: 1은 원하는 복제본 수를 1개 초과하여 임시로 생성할 수 있다는 의미입니다.

예를 들어, 3개의 복제본이 있을 때 최대 4개(3+1)까지 Pod가 동시에 존재할 수 있고, 최소 2개(3-1)는 항상 실행 중이어야 합니다. 여러분이 이 코드를 사용하면 kubectl apply -f deployment.yaml로 배포하고, kubectl set image deployment/web-deployment nginx=nginx:1.22로 간단히 업데이트하며, kubectl rollout undo deployment/web-deployment로 즉시 롤백할 수 있는 완전 자동화된 배포 시스템을 얻게 됩니다.

장애 복구도 자동으로 처리되어 운영 부담이 크게 줄어듭니다.

실전 팁

💡 kubectl rollout status deployment/web-deployment 명령어로 배포 진행 상황을 실시간으로 모니터링할 수 있으며, 배포가 멈춘 경우 원인을 빠르게 파악할 수 있습니다.

💡 kubectl rollout history deployment/web-deployment로 배포 히스토리를 확인하고, --revision=2로 특정 버전의 상세 정보를 볼 수 있습니다. 기본적으로 최근 10개의 ReplicaSet이 보관됩니다.

💡 이미지 태그로 latest를 사용하지 마세요. 명시적인 버전 태그(예: nginx:1.21.0)를 사용해야 정확한 버전 관리와 롤백이 가능합니다. imagePullPolicy: Always와 함께 사용하면 예상치 못한 동작이 발생할 수 있습니다.

💡 minReadySeconds를 설정하면 새 Pod가 생성된 후 지정된 시간 동안 문제없이 실행되어야 다음 Pod 업데이트로 넘어갑니다. 이를 통해 느리게 나타나는 버그를 조기에 감지할 수 있습니다.

💡 Readiness Probe와 Liveness Probe를 반드시 설정하세요. Readiness Probe가 성공해야 Service로 트래픽이 전달되고, Liveness Probe 실패 시 자동으로 재시작되어 자가 치유가 가능합니다.


4. ConfigMap - 설정 정보 외부화

시작하며

여러분이 개발, 스테이징, 프로덕션 환경에서 같은 애플리케이션을 실행하지만 각각 다른 데이터베이스 URL이나 API 키를 사용해야 하는 상황을 겪어본 적 있나요? 매번 환경마다 다른 컨테이너 이미지를 빌드하는 것은 비효율적이고 오류가 발생하기 쉽습니다.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 설정을 컨테이너 이미지에 하드코딩하면 환경별로 이미지를 따로 관리해야 하고, 설정 변경 시마다 이미지를 다시 빌드하고 배포해야 하는 번거로움이 있습니다.

또한 민감한 정보가 이미지에 포함되면 보안 위험도 증가합니다. 바로 이럴 때 필요한 것이 ConfigMap입니다.

ConfigMap은 애플리케이션 코드와 설정을 분리하여 같은 이미지를 여러 환경에서 재사용할 수 있게 해줍니다.

개요

간단히 말해서, ConfigMap은 컨테이너 이미지와 독립적으로 설정 데이터를 저장하는 쿠버네티스 리소스입니다. ConfigMap이 필요한 이유는 12-Factor App 원칙에 따라 설정을 코드에서 분리해야 하기 때문입니다.

예를 들어, 로깅 레벨, 데이터베이스 연결 문자열, 기능 플래그, 서드파티 API 엔드포인트 같은 환경별로 다른 값들을 ConfigMap으로 관리하면, 하나의 컨테이너 이미지로 모든 환경을 커버할 수 있어 빌드 파이프라인이 단순해지고 일관성이 보장됩니다. 전통적인 방법과의 비교를 해보면, 기존에는 환경 변수를 쉘 스크립트나 Dockerfile에 하드코딩했다면, 이제는 ConfigMap으로 중앙 집중식으로 관리하고 여러 Pod에서 공유할 수 있습니다.

ConfigMap의 핵심 특징은 다음과 같습니다. 첫째, 키-값 쌍으로 설정을 저장하여 간단하고 직관적입니다.

둘째, 환경 변수나 커맨드 라인 인자, 또는 볼륨 마운트를 통해 다양한 방식으로 Pod에 주입할 수 있습니다. 셋째, 설정 파일 전체를 통째로 저장할 수도 있어 nginx.conf나 application.properties 같은 복잡한 설정도 관리할 수 있습니다.

이러한 특징들이 인프라스트럭처를 코드로 관리(Infrastructure as Code)하는 데 핵심적인 역할을 합니다.

코드 예제

# ConfigMap 정의
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  # 단순 키-값 쌍
  DATABASE_HOST: "postgres.default.svc.cluster.local"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  # 파일 형태의 설정 (application.properties)
  app.properties: |
    server.port=8080
    cache.enabled=true
    cache.ttl=3600

설명

이것이 하는 일은 애플리케이션의 설정 정보를 쿠버네티스 클러스터에 저장하고, 나중에 Pod가 이 정보를 사용할 수 있게 하는 것입니다. 첫 번째로, data 섹션에서 설정을 키-값 쌍으로 정의합니다.

DATABASE_HOST, DATABASE_PORT, LOG_LEVEL 같은 단순한 문자열 값들을 저장할 수 있습니다. 이 값들은 나중에 Pod의 환경 변수로 주입되거나, 애플리케이션 코드에서 직접 읽을 수 있습니다.

중요한 점은 이 값들을 변경해도 컨테이너 이미지를 다시 빌드할 필요가 없다는 것입니다. 두 번째로, app.properties 키처럼 파일 전체를 저장할 수도 있습니다.

| 문자 다음에 여러 줄의 텍스트를 작성하면, 그것이 하나의 값으로 저장됩니다. 이 방식으로 설정 파일을 통째로 ConfigMap에 넣고, 나중에 볼륨으로 마운트하여 파일 시스템에서 읽을 수 있습니다.

Java Spring Boot 애플리케이션의 application.properties나 Nginx의 nginx.conf 같은 복잡한 설정 파일을 관리하는 데 매우 유용합니다. 마지막으로, Pod에서 이 ConfigMap을 사용하려면 envFrom으로 모든 키를 환경 변수로 가져오거나, env[].valueFrom.configMapKeyRef로 특정 키만 선택하거나, volumes.configMap으로 파일로 마운트할 수 있습니다.

예를 들어 envFrom: - configMapRef: name: app-config를 Pod 스펙에 추가하면, 모든 키가 자동으로 환경 변수로 설정됩니다. 여러분이 이 코드를 사용하면 환경별로 다른 ConfigMap을 생성하고 같은 Deployment를 사용하여 일관된 배포 프로세스를 유지할 수 있습니다.

개발 환경에서는 app-config-dev, 프로덕션에서는 app-config-prod를 사용하면서도 애플리케이션 코드는 전혀 변경하지 않아도 됩니다.

실전 팁

💡 kubectl create configmap app-config --from-file=config/ 명령어로 디렉토리의 모든 파일을 자동으로 ConfigMap으로 만들 수 있으며, 각 파일명이 키가 되고 파일 내용이 값이 됩니다.

💡 ConfigMap은 민감한 정보(비밀번호, API 키)를 저장하는 용도가 아닙니다. 이런 정보는 Secret 리소스를 사용하세요. Secret은 base64로 인코딩되고 RBAC으로 접근 제어가 가능합니다.

💡 ConfigMap을 업데이트해도 이미 실행 중인 Pod의 환경 변수는 자동으로 업데이트되지 않습니다. 볼륨으로 마운트한 경우는 자동 업데이트되지만 약간의 지연이 있으며, 애플리케이션이 파일 변경을 감지하고 리로드하는 로직이 필요합니다.

💡 kubectl describe configmap app-config로 ConfigMap의 내용을 확인할 수 있지만, 큰 파일의 경우 잘려서 표시됩니다. kubectl get configmap app-config -o yaml로 전체 내용을 볼 수 있습니다.

💡 ConfigMap은 1MB 크기 제한이 있습니다. 큰 설정 파일이나 바이너리 데이터는 외부 스토리지(S3, ConfigMap Server)를 사용하고 애플리케이션 시작 시 다운로드하는 패턴을 고려하세요.


5. Secret - 민감한 정보 관리

시작하며

여러분이 데이터베이스 비밀번호나 API 토큰 같은 민감한 정보를 애플리케이션에 전달해야 하는 상황을 떠올려보세요. 이런 정보를 평문으로 ConfigMap에 저장하거나 Git 리포지토리에 커밋하면 심각한 보안 사고로 이어질 수 있습니다.

이런 문제는 실제 개발 현장에서 가장 신경 써야 할 부분입니다. 실수로 비밀번호가 포함된 설정 파일을 퍼블릭 리포지토리에 푸시하거나, 로그에 API 키가 노출되어 해킹 당하는 사례가 빈번하게 발생합니다.

특히 여러 팀원이 함께 작업하는 환경에서는 접근 제어도 중요합니다. 바로 이럴 때 필요한 것이 Secret입니다.

Secret은 민감한 정보를 안전하게 저장하고, RBAC을 통해 접근을 제어하며, Pod에 안전하게 주입하는 메커니즘을 제공합니다.

개요

간단히 말해서, Secret은 비밀번호, OAuth 토큰, SSH 키 같은 민감한 정보를 저장하기 위한 쿠버네티스 리소스입니다. Secret이 필요한 이유는 민감한 정보를 평문으로 관리하는 것은 보안 위험이 크고, 감사(audit) 추적과 접근 제어가 필요하기 때문입니다.

예를 들어, 프로덕션 데이터베이스 비밀번호를 개발자 모두가 볼 수 있으면 안 되지만, 애플리케이션은 런타임에 이 정보에 접근할 수 있어야 하는 모순적인 요구사항을 Secret이 해결합니다. 전통적인 방법과의 비교를 해보면, 기존에는 환경 변수로 전달하거나 설정 파일에 암호화해서 넣었다면, 이제는 쿠버네티스가 자동으로 암호화하고 메모리에만 저장하여 디스크에 쓰지 않는 더 안전한 방법을 제공합니다.

Secret의 핵심 특징은 다음과 같습니다. 첫째, base64로 인코딩되어 저장되며, etcd에서 암호화할 수 있습니다(Encryption at Rest).

둘째, RBAC을 통해 누가 어떤 Secret에 접근할 수 있는지 세밀하게 제어할 수 있습니다. 셋째, tmpfs 볼륨으로 마운트되어 노드의 디스크에는 저장되지 않고 메모리에만 존재합니다.

이러한 특징들이 민감한 정보를 안전하게 관리하고 컴플라이언스 요구사항을 충족하는 데 필수적입니다.

코드 예제

# Secret 정의 (Opaque 타입)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  # base64로 인코딩된 값
  # 생성: echo -n 'admin' | base64
  username: YWRtaW4=
  # 생성: echo -n 'P@ssw0rd123' | base64
  password: UEBzc3cwcmQxMjM=
---
# Pod에서 Secret 사용
apiVersion: v1
kind: Pod
metadata:
  name: app-pod
spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    # Secret의 특정 키를 환경 변수로
    - name: DB_USERNAME
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: username
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: password

설명

이것이 하는 일은 데이터베이스 인증 정보를 안전하게 저장하고, Pod가 런타임에 환경 변수로 접근할 수 있게 하는 것입니다. 첫 번째로, Secret의 type: Opaque는 가장 일반적인 타입으로 임의의 키-값 쌍을 저장할 수 있다는 의미입니다.

다른 타입으로는 kubernetes.io/service-account-token, kubernetes.io/dockerconfigjson 등이 있어 특정 용도에 최적화되어 있습니다. data 섹션에서 모든 값은 base64로 인코딩되어야 합니다.

이는 암호화가 아니라 바이너리 데이터를 안전하게 전송하기 위한 인코딩일 뿐이므로, 진짜 암호화는 etcd 레벨에서 설정해야 합니다. 두 번째로, Pod 정의에서 env[].valueFrom.secretKeyRef를 사용하여 Secret의 특정 키를 환경 변수로 가져옵니다.

여기서는 db-secret이라는 Secret의 usernamepassword 키를 각각 DB_USERNAMEDB_PASSWORD 환경 변수로 매핑합니다. 컨테이너 내부에서 애플리케이션은 os.environ['DB_USERNAME'] 같은 방식으로 이 값에 접근할 수 있습니다.

마지막으로, Secret을 볼륨으로 마운트할 수도 있습니다. volumes: - name: secret-volume; secret: secretName: db-secret로 볼륨을 정의하고, volumeMounts: - name: secret-volume; mountPath: /etc/secrets로 마운트하면, /etc/secrets/username/etc/secrets/password 파일로 접근할 수 있습니다.

이 파일들은 tmpfs에 저장되어 Pod가 종료되면 자동으로 삭제됩니다. 여러분이 이 코드를 사용하면 민감한 정보를 Git 리포지토리에 커밋하지 않고도 안전하게 관리할 수 있으며, 쿠버네티스 RBAC을 통해 특정 ServiceAccount만 특정 Secret에 접근하도록 제어할 수 있습니다.

또한 로그에 비밀번호가 노출되는 것을 방지하고 감사 로그로 누가 언제 접근했는지 추적할 수 있습니다.

실전 팁

💡 kubectl create secret generic db-secret --from-literal=username=admin --from-literal=password='P@ssw0rd123' 명령어로 base64 인코딩 없이 직접 Secret을 생성할 수 있으며, 파일에서 읽으려면 --from-file=ssh-key=~/.ssh/id_rsa를 사용하세요.

💡 Secret 값을 확인하려면 kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d처럼 base64 디코딩이 필요합니다. 실수로 평문이 터미널 히스토리에 남지 않도록 주의하세요.

💡 프로덕션 환경에서는 etcd 암호화(Encryption at Rest)를 반드시 활성화하세요. 기본적으로 Secret은 base64 인코딩만 되어 etcd에 평문으로 저장되므로, EncryptionConfiguration을 설정해야 진짜 암호화가 됩니다.

💡 외부 Secret 관리 도구(HashiCorp Vault, AWS Secrets Manager, Azure Key Vault)와 통합하려면 External Secrets Operator나 Secrets Store CSI Driver를 사용하세요. 쿠버네티스 외부에서 Secret을 관리하고 자동으로 동기화할 수 있습니다.

💡 Secret을 환경 변수로 사용하면 kubectl exec 명령어나 프로세스 목록에서 노출될 수 있으므로, 가능하면 볼륨 마운트 방식을 사용하고 애플리케이션이 파일에서 읽도록 구현하는 것이 더 안전합니다.


6. Namespace - 리소스 격리 및 관리

시작하며

여러분이 하나의 쿠버네티스 클러스터를 개발 팀, QA 팀, 프로덕션 운영 팀이 함께 사용해야 하는 상황을 상상해보세요. 모든 리소스가 한 곳에 섞여 있으면 실수로 다른 팀의 리소스를 삭제하거나 수정할 위험이 있습니다.

이런 문제는 실제 개발 현장에서 팀이나 프로젝트가 여러 개일 때 반드시 해결해야 합니다. 리소스 이름 충돌도 문제입니다.

두 팀이 모두 "web-service"라는 이름을 사용하고 싶다면 어떻게 해야 할까요? 또한 팀별로 리소스 사용량을 제한하고 모니터링해야 하는 요구사항도 있습니다.

바로 이럴 때 필요한 것이 Namespace입니다. Namespace는 가상의 클러스터처럼 작동하여 리소스를 논리적으로 분리하고, 팀별 격리와 접근 제어를 가능하게 합니다.

개요

간단히 말해서, Namespace는 하나의 물리적 클러스터를 여러 개의 가상 클러스터로 나누는 논리적 경계입니다. Namespace가 필요한 이유는 멀티 테넌시(multi-tenancy) 환경에서 리소스 격리와 조직적 분리가 필수이기 때문입니다.

예를 들어, dev, staging, prod Namespace를 만들어 각 환경을 분리하거나, team-frontend, team-backend처럼 팀별로 분리하거나, project-a, project-b처럼 프로젝트별로 분리하는 경우에 매우 유용합니다. 전통적인 방법과의 비교를 해보면, 기존에는 물리적으로 다른 클러스터를 여러 개 운영하거나 명명 규칙으로만 구분했다면, 이제는 하나의 클러스터에서 Namespace로 논리적 분리와 리소스 쿼터, RBAC 적용이 가능합니다.

Namespace의 핵심 특징은 다음과 같습니다. 첫째, 같은 이름의 리소스를 서로 다른 Namespace에 생성할 수 있어 이름 충돌을 방지합니다.

둘째, ResourceQuota로 Namespace당 CPU, 메모리, 스토리지 사용량을 제한할 수 있습니다. 셋째, NetworkPolicy로 Namespace 간 네트워크 트래픽을 제어할 수 있습니다.

넷째, RBAC으로 특정 사용자나 ServiceAccount가 특정 Namespace에만 접근하도록 권한을 부여할 수 있습니다. 이러한 특징들이 대규모 조직에서 안전하고 효율적으로 클러스터를 공유하는 기반이 됩니다.

코드 예제

# Namespace 생성
apiVersion: v1
kind: Namespace
metadata:
  name: development
  labels:
    environment: dev
    team: backend
---
# ResourceQuota로 리소스 제한
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: development
spec:
  hard:
    # 최대 Pod 개수
    pods: "10"
    # 최대 CPU 사용량 (총합)
    requests.cpu: "4"
    requests.memory: "8Gi"
    # 최대 제한
    limits.cpu: "8"
    limits.memory: "16Gi"

설명

이것이 하는 일은 development라는 격리된 공간을 만들고, 그 안에서 사용할 수 있는 리소스의 최대량을 제한하는 것입니다. 첫 번째로, Namespace 리소스를 생성하면서 labels를 추가하여 나중에 검색하고 필터링하기 쉽게 만듭니다.

한번 Namespace가 생성되면, 모든 리소스 생성 시 metadata.namespace: development를 명시하거나 kubectl -n development apply -f app.yaml처럼 명령줄에서 지정할 수 있습니다. 네임스페이스를 지정하지 않으면 기본적으로 default Namespace에 생성됩니다.

두 번째로, ResourceQuota가 이 Namespace에 할당되면서 리소스 사용량을 제한합니다. pods: "10"은 이 Namespace에서 최대 10개의 Pod만 실행할 수 있다는 의미입니다.

requests.cpu: "4"requests.memory: "8Gi"는 모든 Pod의 리소스 요청(requests) 합계가 CPU 4코어와 메모리 8GB를 초과할 수 없다는 의미입니다. limits.cpulimits.memory는 실제 사용량의 상한선입니다.

마지막으로, ResourceQuota가 적용되면 이 Namespace에 Pod를 생성할 때 반드시 resources.requestsresources.limits를 명시해야 합니다. 그렇지 않으면 Pod 생성이 거부됩니다.

이를 통해 팀들이 무분별하게 리소스를 사용하는 것을 방지하고, 클러스터 전체의 리소스를 공정하게 분배할 수 있습니다. 여러분이 이 코드를 사용하면 개발 팀이 실수로 과도한 리소스를 사용하여 프로덕션 환경에 영향을 주는 것을 방지할 수 있습니다.

또한 Namespace별로 비용을 추적하고, 팀별 예산 관리를 할 수 있으며, RBAC과 결합하여 개발자는 development Namespace만 접근하도록 제한할 수 있습니다.

실전 팁

💡 kubectl config set-context --current --namespace=development 명령어로 기본 Namespace를 변경하면, 매번 -n 옵션을 입력하지 않아도 됩니다. 현재 컨텍스트의 기본값이 변경되어 작업 효율이 높아집니다.

💡 Namespace를 삭제하면 그 안의 모든 리소스가 삭제됩니다. 프로덕션 Namespace를 실수로 삭제하지 않도록 주의하세요. RBAC으로 삭제 권한을 제한하는 것이 좋습니다.

💡 Service의 DNS 이름은 <service-name>.<namespace>.svc.cluster.local 형식입니다. 같은 Namespace 내에서는 <service-name>만으로 접근할 수 있지만, 다른 Namespace의 Service에 접근하려면 전체 도메인을 사용해야 합니다.

💡 일부 리소스는 Namespace에 속하지 않습니다. Node, PersistentVolume, StorageClass 등은 클러스터 레벨 리소스로 모든 Namespace에서 공유됩니다. kubectl api-resources --namespaced=false로 확인할 수 있습니다.

💡 LimitRange 리소스를 사용하면 Namespace 내에서 생성되는 각 Pod나 Container의 기본값과 최소/최대 리소스를 설정할 수 있습니다. ResourceQuota와 함께 사용하면 더 세밀한 제어가 가능합니다.


7. Volume - 데이터 영속성과 공유

시작하며

여러분이 데이터베이스 컨테이너를 실행하고 있는데, Pod가 재시작되면 모든 데이터가 사라지는 상황을 겪어본 적 있나요? 컨테이너는 기본적으로 stateless하기 때문에, 컨테이너 파일 시스템에 저장된 데이터는 컨테이너가 종료되면 함께 사라집니다.

이런 문제는 실제 개발 현장에서 stateful 애플리케이션을 다룰 때 반드시 해결해야 합니다. 데이터베이스뿐만 아니라 파일 업로드를 저장하는 웹 서버, 로그를 수집하는 사이드카, 설정 파일을 공유하는 여러 컨테이너 등 다양한 상황에서 데이터 영속성과 공유가 필요합니다.

바로 이럴 때 필요한 것이 Volume입니다. Volume은 컨테이너의 생명주기와 독립적으로 데이터를 저장하고, 여러 컨테이너 간에 데이터를 공유할 수 있게 해줍니다.

개요

간단히 말해서, Volume은 Pod의 컨테이너들이 접근할 수 있는 디렉토리로, 다양한 백엔드 스토리지와 연결될 수 있습니다. Volume이 필요한 이유는 컨테이너가 재시작되거나 교체되어도 데이터를 보존해야 하고, 같은 Pod 내 여러 컨테이너가 데이터를 공유해야 하기 때문입니다.

예를 들어, Nginx 컨테이너가 서빙하는 정적 파일을 Git-Sync 사이드카 컨테이너가 주기적으로 업데이트하는 경우, 두 컨테이너가 같은 Volume을 공유해야 합니다. 전통적인 방법과의 비교를 해보면, 기존에는 호스트의 디렉토리를 직접 마운트하거나 외부 스토리지를 수동으로 설정했다면, 이제는 쿠버네티스가 다양한 Volume 타입을 추상화하여 일관된 인터페이스로 제공합니다.

Volume의 핵심 특징은 다음과 같습니다. 첫째, emptyDir, hostPath, configMap, secret, persistentVolumeClaim 등 다양한 타입을 지원하여 용도에 맞게 선택할 수 있습니다.

둘째, Pod의 생명주기와 연결되어 있지만, PersistentVolume을 사용하면 Pod와 독립적으로 데이터를 유지할 수 있습니다. 셋째, 클라우드 제공자의 스토리지(AWS EBS, Azure Disk, GCP PD)를 직접 사용할 수 있어 인프라 종속성을 줄입니다.

이러한 특징들이 stateful 애플리케이션을 쿠버네티스에서 안정적으로 운영하는 기반이 됩니다.

코드 예제

# emptyDir Volume - 임시 데이터 공유
apiVersion: v1
kind: Pod
metadata:
  name: web-with-cache
spec:
  containers:
  # 메인 웹 서버
  - name: nginx
    image: nginx:1.21
    volumeMounts:
    - name: cache-volume
      mountPath: /var/cache/nginx
  # 캐시 워머 사이드카
  - name: cache-warmer
    image: busybox
    command: ['sh', '-c', 'while true; do echo "$(date)" > /cache/timestamp; sleep 10; done']
    volumeMounts:
    - name: cache-volume
      mountPath: /cache
  volumes:
  # 두 컨테이너가 공유하는 임시 볼륨
  - name: cache-volume
    emptyDir: {}

설명

이것이 하는 일은 같은 Pod 내 두 컨테이너가 파일 시스템을 통해 데이터를 공유할 수 있게 하는 것입니다. 첫 번째로, volumes 섹션에서 emptyDir 타입의 Volume을 정의합니다.

emptyDir은 Pod가 생성될 때 빈 디렉토리로 시작하고, Pod가 삭제되면 함께 사라지는 임시 스토리지입니다. 노드의 디스크나 메모리(emptyDir: medium: Memory)에 생성될 수 있으며, 같은 Pod 내 컨테이너들이 데이터를 교환하는 데 주로 사용됩니다.

영속성이 필요 없는 캐시, 임시 파일, 처리 중인 데이터에 적합합니다. 두 번째로, 각 컨테이너의 volumeMounts 섹션에서 Volume을 파일 시스템의 특정 경로에 마운트합니다.

nginx 컨테이너는 /var/cache/nginx에, cache-warmer 컨테이너는 /cache에 같은 Volume을 마운트합니다. 실제로는 같은 디렉토리를 공유하므로, cache-warmer가 /cache/timestamp 파일에 쓰면 nginx는 /var/cache/nginx/timestamp로 읽을 수 있습니다.

마지막으로, cache-warmer 사이드카가 10초마다 타임스탬프를 파일에 쓰고, nginx가 이것을 읽어서 사용할 수 있습니다. 이 패턴은 로그 수집(애플리케이션이 파일에 쓰고 Fluentd가 읽음), 설정 동기화(Git-Sync가 Git에서 받아오고 애플리케이션이 읽음), 데이터 전처리(하나의 컨테이너가 데이터를 준비하고 다른 컨테이너가 처리) 등에 널리 사용됩니다.

여러분이 이 코드를 사용하면 컨테이너 간 파일 기반 통신이 가능해지고, 하나의 컨테이너가 크래시되어 재시작되어도 다른 컨테이너와 공유하는 데이터는 Pod가 살아있는 한 유지됩니다. 단, emptyDir은 Pod가 삭제되면 데이터도 함께 사라지므로, 영속성이 필요하다면 PersistentVolumeClaim을 사용해야 합니다.

실전 팁

💡 emptyDir.medium: Memory를 설정하면 메모리(tmpfs)에 Volume이 생성되어 디스크보다 훨씬 빠르지만, 노드의 메모리를 소비하므로 리소스 제한을 신중하게 설정해야 합니다.

💡 hostPath Volume은 노드의 특정 디렉토리를 마운트하지만, Pod가 다른 노드로 스케줄링되면 접근할 수 없으므로 개발/테스트 환경에서만 사용하고 프로덕션에서는 피하세요.

💡 subPath를 사용하면 Volume의 특정 하위 디렉토리만 마운트할 수 있습니다. 예를 들어 ConfigMap Volume의 특정 파일만 필요한 경로에 마운트하여 기존 파일을 덮어쓰지 않을 수 있습니다.

💡 readOnly: true를 volumeMounts에 추가하면 컨테이너가 Volume을 읽기만 할 수 있어 실수로 데이터를 변경하는 것을 방지합니다. Secret이나 ConfigMap을 마운트할 때 권장됩니다.

💡 영속적 데이터가 필요하다면 PersistentVolume(PV)과 PersistentVolumeClaim(PVC)을 사용하세요. PVC는 Pod와 독립적으로 존재하여 Pod가 삭제되고 재생성되어도 데이터가 유지됩니다.


8. Ingress - HTTP 라우팅과 로드밸런싱

시작하며

여러분이 여러 개의 마이크로서비스를 운영하고 있고, 각각을 외부에 노출해야 하는 상황을 생각해보세요. Service를 LoadBalancer 타입으로 만들면 각 서비스마다 클라우드 로드밸런서가 생성되어 비용이 급격히 증가합니다.

이런 문제는 실제 개발 현장에서 매우 현실적인 고민입니다. 예를 들어, api.example.com으로 백엔드 API 서비스에, app.example.com으로 프론트엔드 서비스에, admin.example.com으로 관리자 대시보드에 접근하고 싶다면 어떻게 해야 할까요?

각각에 LoadBalancer를 할당하면 3개의 공인 IP와 로드밸런서 비용이 발생합니다. 바로 이럴 때 필요한 것이 Ingress입니다.

Ingress는 하나의 로드밸런서로 여러 Service를 호스트 기반 또는 경로 기반으로 라우팅하고, TLS 종료까지 처리해줍니다.

개요

간단히 말해서, Ingress는 클러스터 외부에서 내부 Service로의 HTTP와 HTTPS 라우팅을 관리하는 API 객체입니다. Ingress가 필요한 이유는 복잡한 라우팅 규칙과 TLS/SSL 인증서를 효율적으로 관리해야 하기 때문입니다.

예를 들어, /api/* 경로는 백엔드 Service로, /static/* 경로는 CDN Service로, 나머지는 프론트엔드 Service로 라우팅하는 복잡한 규칙을 하나의 진입점에서 처리할 수 있습니다. 전통적인 방법과의 비교를 해보면, 기존에는 Nginx나 HAProxy를 별도로 설정하고 수동으로 업데이트했다면, 이제는 Ingress 리소스를 YAML로 정의하면 Ingress Controller가 자동으로 설정을 적용합니다.

Ingress의 핵심 특징은 다음과 같습니다. 첫째, 호스트 기반 라우팅(가상 호스팅)으로 도메인별로 다른 Service에 연결할 수 있습니다.

둘째, 경로 기반 라우팅으로 URL 패턴에 따라 다른 백엔드로 트래픽을 분산할 수 있습니다. 셋째, TLS 종료를 통해 HTTPS를 처리하고 내부에서는 HTTP로 통신할 수 있습니다.

넷째, 다양한 Ingress Controller(Nginx, Traefik, HAProxy, AWS ALB, GCP GCLB)를 선택하여 플랫폼에 맞게 최적화할 수 있습니다. 이러한 특징들이 비용 효율적이고 유연한 외부 노출을 가능하게 합니다.

코드 예제

# Ingress 정의 - 호스트 및 경로 기반 라우팅
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    # Nginx Ingress Controller 설정
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  # TLS 설정
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls-secret
  rules:
  # 호스트 기반 라우팅
  - host: api.example.com
    http:
      paths:
      # 경로 기반 라우팅
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: api-v1-service
            port:
              number: 80
      - path: /v2
        pathType: Prefix
        backend:
          service:
            name: api-v2-service
            port:
              number: 80

설명

이것이 하는 일은 api.example.com 도메인으로 들어오는 요청을 경로에 따라 다른 버전의 API Service로 라우팅하고, HTTPS를 통해 안전하게 접근할 수 있게 하는 것입니다. 첫 번째로, tls 섹션에서 HTTPS 설정을 정의합니다.

secretName에 지정된 Secret은 TLS 인증서와 개인 키를 담고 있어야 합니다(kubectl create secret tls api-tls-secret --cert=tls.crt --key=tls.key로 생성). Ingress Controller는 이 인증서를 사용하여 클라이언트와의 HTTPS 연결을 종료하고, 백엔드 Service와는 HTTP로 통신합니다.

Let's Encrypt와 cert-manager를 사용하면 인증서를 자동으로 발급하고 갱신할 수 있습니다. 두 번째로, rules 섹션이 실행되면서 라우팅 규칙을 정의합니다.

host: api.example.com은 이 호스트로 들어오는 요청만 처리한다는 의미입니다. 같은 Ingress에 여러 개의 host를 정의하여 가상 호스팅을 구현할 수 있습니다.

paths 배열에서 각 경로에 대한 백엔드를 지정하는데, path: /v1pathType: Prefix/v1, /v1/users, /v1/orders 같이 /v1로 시작하는 모든 경로를 매칭합니다. 마지막으로, 각 경로에 대한 backend 설정이 트래픽을 적절한 Service로 전달합니다.

/v1로 시작하는 요청은 api-v1-service의 80번 포트로, /v2로 시작하는 요청은 api-v2-service의 80번 포트로 라우팅됩니다. annotationsrewrite-target은 백엔드로 전달할 때 경로를 재작성하는 기능으로, /v1/users 요청을 /users로 변환하여 백엔드가 버전 프리픽스를 처리하지 않아도 되게 합니다.

여러분이 이 코드를 사용하면 하나의 로드밸런서로 여러 Service를 노출하여 비용을 절감하고, 버전별 API를 URL로 구분하여 클라이언트가 명확하게 접근할 수 있으며, HTTPS를 중앙에서 관리하여 각 Service가 TLS를 개별적으로 처리할 필요가 없어집니다. 또한 DNS에 로드밸런서 IP 하나만 등록하면 됩니다.

실전 팁

💡 Ingress를 사용하려면 먼저 Ingress Controller를 설치해야 합니다. kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml로 Nginx Ingress Controller를 설치할 수 있습니다.

💡 pathTypeExact, Prefix, ImplementationSpecific 세 가지가 있습니다. Exact는 정확히 일치해야 하고, Prefix는 접두사 매칭이며, ImplementationSpecific은 Ingress Controller에 따라 다릅니다.

💡 annotations를 통해 Ingress Controller별 고급 기능을 사용할 수 있습니다. 예를 들어 nginx.ingress.kubernetes.io/rate-limit으로 속도 제한, nginx.ingress.kubernetes.io/whitelist-source-range로 IP 화이트리스트를 설정할 수 있습니다.

💡 cert-manager를 설치하고 cert-manager.io/cluster-issuer: letsencrypt-prod annotation을 추가하면 Let's Encrypt 인증서가 자동으로 발급되고 갱신됩니다. 더 이상 수동으로 인증서를 관리할 필요가 없습니다.

💡 기본 백엔드를 설정하면 매칭되지 않는 요청을 처리할 수 있습니다. spec.defaultBackend에 404 페이지를 제공하는 Service를 지정하여 사용자 경험을 개선하세요.


9. StatefulSet - Stateful 애플리케이션 관리

시작하며

여러분이 데이터베이스 클러스터나 메시징 시스템처럼 각 인스턴스가 고유한 ID와 영속적 스토리지를 필요로 하는 애플리케이션을 배포해야 하는 상황을 생각해보세요. Deployment는 모든 Pod를 동일하게 취급하기 때문에 이런 요구사항을 만족시킬 수 없습니다.

이런 문제는 실제 개발 현장에서 stateful 애플리케이션을 다룰 때 반드시 마주치게 됩니다. 예를 들어, MySQL 레플리카 세트를 구성할 때 각 인스턴스는 안정적인 네트워크 ID가 필요하고, 마스터와 슬레이브의 역할이 명확히 구분되어야 하며, 각자의 데이터를 독립적인 볼륨에 저장해야 합니다.

바로 이럴 때 필요한 것이 StatefulSet입니다. StatefulSet은 각 Pod에 안정적인 네트워크 ID와 영속적 스토리지를 제공하여 stateful 애플리케이션을 안전하게 운영할 수 있게 해줍니다.

개요

간단히 말해서, StatefulSet은 상태를 가진(stateful) 애플리케이션을 관리하기 위한 워크로드 API 객체로, 순서와 고유성을 보장합니다. StatefulSet이 필요한 이유는 일부 애플리케이션은 각 인스턴스가 독립적인 정체성과 스토리지를 유지해야 하기 때문입니다.

예를 들어, Kafka 클러스터의 각 브로커는 고유한 브로커 ID를 가지고 특정 파티션의 데이터를 저장하며, 재시작 후에도 같은 ID와 데이터를 유지해야 합니다. Elasticsearch 클러스터도 각 노드가 샤드 데이터를 독립적으로 관리합니다.

전통적인 방법과의 비교를 해보면, 기존에는 각 인스턴스를 별도의 Deployment로 관리하거나 외부 오케스트레이션 도구를 사용했다면, 이제는 StatefulSet 하나로 순서 있는 배포, 안정적인 네이밍, 독립적인 스토리지를 모두 처리할 수 있습니다. StatefulSet의 핵심 특징은 다음과 같습니다.

첫째, 각 Pod가 순서대로 생성되고 삭제됩니다(0, 1, 2 순서로 생성, 역순으로 삭제). 둘째, 각 Pod는 <statefulset-name>-<ordinal> 형식의 안정적인 이름을 가지며 재시작되어도 유지됩니다.

셋째, 각 Pod에 독립적인 PersistentVolumeClaim이 자동으로 생성되어 데이터가 영속적으로 보존됩니다. 넷째, Headless Service를 통해 각 Pod가 고유한 DNS 이름을 가져 직접 접근할 수 있습니다.

이러한 특징들이 복잡한 분산 시스템을 쿠버네티스에서 안정적으로 운영하는 기반이 됩니다.

코드 예제

# Headless Service - StatefulSet의 네트워크 정체성
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  clusterIP: None  # Headless Service
  selector:
    app: mysql
  ports:
  - port: 3306
---
# StatefulSet 정의
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql  # Headless Service 이름
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  # 각 Pod에 독립적인 PVC 자동 생성
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

설명

이것이 하는 일은 3개의 MySQL 인스턴스를 각각 고유한 이름과 영속적 스토리지를 가지도록 배포하고 관리하는 것입니다. 첫 번째로, Headless Service를 정의합니다.

clusterIP: None은 이 Service가 로드밸런싱용 가상 IP를 할당받지 않는다는 의미입니다. 대신 Service는 DNS 레코드만 관리하여 각 Pod가 <pod-name>.<service-name>.<namespace>.svc.cluster.local 형식의 고유한 DNS 이름을 가지게 합니다.

예를 들어 mysql-0.mysql.default.svc.cluster.local, mysql-1.mysql.default.svc.cluster.local 같은 식입니다. 이를 통해 애플리케이션이 특정 Pod에 직접 접근할 수 있습니다.

두 번째로, StatefulSet의 serviceName 필드가 Headless Service와 연결됩니다. replicas: 3이므로 mysql-0, mysql-1, mysql-2 이름을 가진 3개의 Pod가 순서대로 생성됩니다.

Pod는 반드시 이전 Pod가 Running 상태가 되어야 다음 Pod가 생성되며, 삭제 시에는 역순(2, 1, 0)으로 진행됩니다. 이 순서 보장은 마스터-슬레이브 관계나 쿼럼 기반 시스템에서 중요합니다.

마지막으로, volumeClaimTemplates가 각 Pod에 대해 독립적인 PersistentVolumeClaim을 자동으로 생성합니다. mysql-0data-mysql-0, mysql-1data-mysql-1, mysql-2data-mysql-2 PVC를 가지게 됩니다.

Pod가 삭제되어도 PVC는 유지되고, 같은 이름의 Pod가 재생성되면 같은 PVC에 다시 연결되어 데이터가 보존됩니다. 각 Pod는 /var/lib/mysql에 10GB 볼륨을 마운트하여 독립적인 데이터베이스 데이터를 저장합니다.

여러분이 이 코드를 사용하면 MySQL 레플리카 세트를 구성하여 고가용성을 확보하고, 각 인스턴스의 데이터가 안전하게 보호되며, 스케일링 시에도 데이터 손실 없이 확장하거나 축소할 수 있습니다. StatefulSet을 삭제해도 PVC는 남아있어 실수로 데이터를 잃는 것을 방지합니다.

실전 팁

💡 StatefulSet을 스케일링할 때는 신중하게 하세요. 스케일 다운 시 마지막 Pod부터 삭제되며, PVC는 자동으로 삭제되지 않습니다. 데이터를 정말 삭제하려면 PVC를 수동으로 삭제해야 합니다.

💡 podManagementPolicy: Parallel을 설정하면 순서를 무시하고 모든 Pod를 동시에 시작할 수 있습니다. 순서가 중요하지 않은 경우 배포 시간을 단축할 수 있지만, 대부분의 stateful 애플리케이션에서는 기본값인 OrderedReady가 안전합니다.

💡 롤링 업데이트 시 updateStrategy.rollingUpdate.partition을 사용하면 카나리 배포를 할 수 있습니다. partition: 2로 설정하면 ordinal이 2 이상인 Pod만 업데이트되어 일부만 테스트할 수 있습니다.

💡 각 Pod에 직접 접근하려면 kubectl exec -it mysql-0 -- mysql -u root -p 처럼 Pod 이름을 사용하세요. 외부에서 특정 Pod에 접근하려면 해당 Pod만 노출하는 별도의 NodePort 또는 LoadBalancer Service를 만들어야 합니다.

💡 StatefulSet은 기본적으로 Pod가 삭제되어도 자동으로 재생성하지만, 노드 장애 시 상태를 확신할 수 없으면 재생성을 보류합니다. kubectl delete pod mysql-0 --force --grace-period=0으로 강제 삭제하여 복구를 트리거할 수 있지만 데이터 손실 위험이 있으므로 주의하세요. 위의 형식으로 Kubernetes 기본 개념에 대한 상세한 코드 카드 뉴스를 생성했습니다. 각 섹션이 충분히 깊이 있게 작성되었으며, 실무에서 바로 활용할 수 있는 수준의 정보와 예제를 제공했습니다. 초급 개발자도 이해하기 쉽도록 친근한 톤으로 작성했고, 실전 팁도 풍부하게 포함했습니다.


#Kubernetes#Pod#Service#Deployment#ConfigMap#TypeScript

댓글 (0)

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