개념부터 실전 운영까지 — AI·데이터 직군을 위한 실습 정리

Kubernetes Cluster Architecture (출처: kubernetes.io)
| “로컬에서는 잘 되는데 서버에서만 안 돼요…” “환경이 달라서 재현이 안 됩니다…” |
AI·빅데이터 업무를 하다 보면 한 번쯤 겪어봤거나 들어봤을 상황입니다. 이런 문제를 줄이기 위해 등장한 것이 컨테이너이고, 그 컨테이너를 여러 서버에 자동으로 배치·운영해 주는 시스템이 바로 쿠버네티스(Kubernetes)입니다.
이 문서는 쿠버네티스를 처음 접하는 분들, 특히 AI·데이터 직군을 대상으로 개념부터 실습, 운영 핵심 개념까지 한 번에 정리한 통합 가이드입니다. 개념편(Part 1)에서 머릿속 그림을 그리고, 실습편(Part 2)에서 Minikube 위에 FastAPI 앱을 직접 올려 보면서 손으로 익힌 뒤, 마무리(Part 3)에서 자주 마주치는 문제와 다음 학습 방향을 정리합니다.
Part 1 개념편
1. 왜 쿠버네티스를 쓸까?
클라우드가 보편화되면서 서비스 구조가 크게 바뀌었습니다.
- 하나의 덩치 큰 서버 → 여러 개의 작은 서비스(마이크로서비스)
- 한두 대 서버 → 수십·수백 대 서버
- 수동 배포 → 자동 배포·자동 복구·자동 확장
이때 꼭 필요해진 것이 두 가지입니다.
- 환경을 통째로 묶어서 어디서나 똑같이 돌릴 수 있는 방식 → 컨테이너
- 그 컨테이너들을 여러 서버에 자동으로 배치·관리해 주는 시스템 → 쿠버네티스
| 쿠버네티스 = 여러 대의 서버에 컨테이너를 알아서 배치·운영해 주는 오케스트레이션 시스템 |
우리는 “이 이미지들을 특정 조건으로 몇 개 기동해줘”만 선언적으로 적어 두고, 어디에 올릴지 / 죽으면 다시 살릴지 / 몇 개를 유지할지 같은 운영은 쿠버네티스가 알아서 맡는 구조입니다.
2. 컨테이너와 Docker
쿠버네티스를 이해하려면 먼저 컨테이너 개념이 필요합니다.

Docker(단일 호스트)와 Kubernetes(다중 호스트) 구조 비교
2-1. 컨테이너란?
- 애플리케이션 + 라이브러리 + 런타임 환경을 하나의 단위로 묶은 것
- 어디에 배포해도 동일한 환경에서 실행되도록 만들어 주는 기술
- 가장 대표적인 도구가 Docker
AI·빅데이터 관점에서 보면, 특정 버전의 Python·PyTorch·CUDA·의존 라이브러리 등을 하나의 이미지(Image)로 만들어 두면 로컬·온프레미스·클라우드·쿠버네티스 어디서든 같은 환경으로 실행할 수 있습니다.
2-2. Docker만 쓰면 안 되나?
Docker만으로도 컨테이너를 실행할 수는 있습니다. 하지만 컨테이너가 많아지면 다음과 같은 문제가 생깁니다.
- 어느 서버에서 무엇이 도는지 관리가 어려워짐
- 죽은 컨테이너를 다시 띄우는 작업을 사람이 직접 해야 함
- 배포·롤백·오토스케일링을 직접 구현해야 함
- 여러 서버(노드)를 하나의 자원 풀처럼 효율적으로 쓰기 어려움
그래서 등장한 것이 쿠버네티스입니다.
| Docker = 컨테이너를 만들고 실행하는 도구 Kubernetes = 컨테이너들을 여러 서버에 알아서 배치·관리하는 플랫폼 |
3. 핵심 개념 한눈에 보기
처음 쿠버네티스를 공부할 때는 새로운 용어가 한꺼번에 쏟아져 부담스럽게 느껴지지만, AI·빅데이터 관점에서는 우선 다음 정도만 잡고 가도 충분합니다.
| 개념 | 한 줄 요약 |
| Cluster / Node | 여러 서버를 하나의 컴퓨팅 풀로 묶은 것이 클러스터, 그 안의 개별 서버가 노드 |
| Pod | 쿠버네티스의 가장 작은 배포 단위. 1개 이상의 컨테이너를 묶어 놓은 것 |
| Deployment | Pod를 N개 유지하면서 새 버전 롤링 업데이트·자동 복구를 관리하는 객체 |
| Service | 여러 Pod 앞에 고정된 엔드포인트를 두고 로드 밸런싱을 담당 |
| Namespace | 하나의 클러스터를 팀/환경(dev, stage, prod) 단위로 논리적으로 나누는 공간 |
| ConfigMap / Secret | 설정값(ConfigMap)과 민감 정보(Secret)를 이미지 밖에서 관리 |
3-1. 클러스터(Cluster)와 노드(Node)
클러스터(Cluster)는 쿠버네티스가 관리하는 전체 서버 묶음으로, Control Plane(제어부)과 여러 워커 노드로 구성됩니다. 노드(Node)는 컨테이너가 실제로 실행되는 개별 서버이며, 물리 서버일 수도 있고 가상 머신일 수도 있습니다.
3-2. 파드(Pod)
- 쿠버네티스에서 가장 작은 배포 단위
- 1개 이상의 컨테이너를 묶어 놓은 것
- 같은 Pod 안의 컨테이너들은 동일한 네트워크 네임스페이스(IP·포트 공간)를 공유하고, 볼륨을 통해 저장 공간을 함께 사용함
실무에서는 대부분 컨테이너 1개 = Pod 1개로 구성하는 경우가 많습니다.
3-3. Deployment
Deployment는 한 마디로 이렇게 이해하면 됩니다.
| “이 이미지를 N개 유지하고, 새 버전이 나오면 중단 없이 교체해 달라” |
replicas: 3으로 설정하면 쿠버네티스는 항상 Pod 3개를 유지하려고 합니다. Pod 하나가 중단되면 자동으로 새 Pod를 실행하고, 이미지 버전을 바꾸면 롤링 업데이트(조금씩 새로 띄우면서 교체)가 진행됩니다.
3-4. Service
Pod는 상태에 따라 쉽게 생기고 사라지기 때문에 IP가 자주 바뀝니다. 특정 Pod의 IP를 직접 호출하는 방식은 안정적이지 않습니다. 그래서 여러 Pod 앞단에 고정된 엔드포인트 역할을 하는 Service 개념이 있습니다.
- 뒤에 붙은 여러 Pod로 자동 로드 밸런싱
- 클라이언트는 항상 Service만 바라보고 호출 → 실제 Pod 구성이 바뀌어도 영향 없음
3-5. Namespace, ConfigMap, Secret
- Namespace: 클러스터를 dev, stage, prod, 팀/프로젝트 단위로 논리적으로 분리. 권한·리소스 제한·이름 충돌 방지에 활용
- ConfigMap: 환경 변수, 설정값 등을 이미지와 분리해 관리. 코드·이미지를 바꾸지 않고 설정만 변경 가능
- Secret: DB 비밀번호, API 토큰 등 민감 정보 관리용. ConfigMap과 비슷하지만 민감한 데이터 전용
4. AI·빅데이터 업무에서 쿠버네티스를 쓰면 좋은 이유
웹 서비스 회사만의 도구처럼 보이지만, 실제로는 AI·데이터 쪽에서도 잘 맞는 플랫폼입니다.
4-1. 실험 환경 통일 & 재현성
- 모델 학습 코드, 피처 엔지니어링, ETL 파이프라인을 컨테이너 이미지로 만들어 두면, 팀원마다 로컬 환경이 달라도 동일한 이미지 기준으로 실험 재현 가능
- 연구용 코드 → 서비스용 코드 전환 시 환경 차이로 고생하는 시간이 크게 줄어듦
4-2. 배치 작업 & 파이프라인 구성
쿠버네티스에는 Job / CronJob 리소스가 있습니다.
- Job: 한 번 돌고 끝나는 작업 (예: 하루치 데이터 전처리)
- CronJob: 특정 주기마다 실행 (예: 매일 새벽 3시 ETL, 주간 리포트 생성)
Airflow, Argo Workflows 같은 워크플로우 도구도 뒤에서 쿠버네티스를 활용해 작업을 스케줄링하는 구조를 많이 사용합니다.
4-3. 확장성과 자원 관리
- 평소에는 모델 서버 2개, 트래픽이 몰리면 자동으로 10개까지 늘리고, 야간에는 학습 Job 위주로 돌리는 식으로 자원을 유연하게 할당 가능
- GPU 학습 Job은 GPU 노드에서, 경량 API 서버는 CPU 노드 위주로 운영하도록 분리하는 자원 최적화도 가능 (HPA + nodeSelector / taint·toleration)
5. 아키텍처 — Control Plane과 워커 노드
쿠버네티스 클러스터는 크게 두 부분으로 나뉩니다.

Control Plane과 Worker Nodes로 구성된 쿠버네티스 클러스터
5-1. Control Plane (제어부)
주요 컴포넌트만 간단히 보면 다음과 같습니다.
- API Server — kubectl 명령이나 다른 서비스에서 오는 요청을 받는 입구. 클러스터의 모든 요청이 이쪽으로 모입니다.
- etcd — 쿠버네티스의 상태 정보를 저장하는 키-값 저장소. 원하는 상태(Desired State)가 여기에 기록됩니다.
- Scheduler — “새 Pod를 어느 노드에 올릴까?”를 결정. 노드의 자원 상태와 제약 조건 등을 고려해 배치합니다.
- Controller Manager — 실제 상태와 원하는 상태를 끊임없이 비교·감시. 예를 들어 Deployment에 replicas: 3이 적혀 있는데 실제 Pod가 2개만 살아 있다면, Pod를 하나 더 만들어 다시 3개로 맞춥니다.
| 사용자가 선언한 상태와 실제 상태를 맞추기 위해 끊임없이 감시하고 조정하는 두뇌 → Control Plane |
5-2. 워커 노드
워커 노드에는 보통 다음과 같은 컴포넌트가 있습니다.
- kubelet — Control Plane으로부터 “이 노드에 이런 Pod를 올려라”라는 명령을 받아, 컨테이너 런타임(Docker, containerd 등)에 실제 컨테이너 실행/정지를 요청
- kube-proxy — Service와 Pod 사이의 네트워크 트래픽을 라우팅. 로드 밸런싱, iptables/ipvs 규칙 관리 등에 관여
AI·빅데이터 입장에서 기억할 포인트는 단순합니다. 실제로 모델 서버, ETL Job, 배치 스크립트가 컨테이너 형태로 돌아가는 곳은 모두 워커 노드입니다.
6. YAML 한 번 적용하면 내부에서 벌어지는 일
이제 kubectl apply -f deployment.yaml 한 줄을 실행했을 때, 클러스터 내부에서 어떤 일이 일어나는지 흐름으로 정리해 봅니다.
- 우리가 kubectl apply -f deployment.yaml 을 실행한다.
- API Server가 요청을 받고, Deployment 정의를 etcd에 저장한다. → ‘이 애플리케이션은 replicas=3으로 돌아야 한다’는 원하는 상태 기록
- Scheduler가 ‘새로 만들어야 할 Pod들을 어느 노드에 올릴지’를 결정한다.
- 선택된 각 노드의 kubelet이 컨테이너 런타임에 이미지 Pull과 컨테이너 실행을 요청한다.
- Pod가 정상적으로 Running 상태가 되면, 앞단의 Service가 해당 Pod들의 IP를 대상으로 로드 밸런싱을 수행한다.
- 만약 Pod 하나가 죽으면, Controller가 ‘replicas=3인데 지금 2개만 있네?’라고 감지하고 새 Pod를 하나 더 띄워 다시 3개를 유지한다.
| “나는 YAML에 모델 서버/배치 작업 스펙만 선언해 두고, 나머지 인프라 운영은 쿠버네티스가 선언을 기반으로 자동으로 조정 한다.” |
Part 2 실습편
7. 실습 환경 세팅
실습은 다음 환경에서 진행했습니다.
- Host OS — Windows 11
- WSL2 — Ubuntu 24.04
- Minikube — v1.37.0 (linux/amd64)
- Docker — docker.io 패키지
- kubectl — v1.34.2 (클라이언트)
7-1. Minikube 설치
| # Minikube 바이너리 다운로드 curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 # 실행 권한 부여 chmod +x minikube-linux-amd64 # /usr/local/bin 으로 이동 (PATH에 포함된 위치) sudo mv minikube-linux-amd64 /usr/local/bin/minikube # 버전 확인 minikube version |
7-2. Docker 설치
Minikube가 컨테이너를 띄우려면 컨테이너 런타임(Docker 등)이 필요합니다. Docker가 없다면 먼저 설치합니다.
| sudo apt update sudo apt install -y docker.io # Docker 서비스 실행 & 부팅 시 자동 시작 sudo systemctl enable –now docker # 현재 유저를 docker 그룹에 추가 sudo usermod -aG docker $USER # 현재 셸에 바로 반영 newgrp docker |
정상 동작 확인:
| docker version docker run –rm hello-world |
7-3. kubectl 설치
쿠버네티스 클러스터와 대화하는 CLI 도구입니다.
| # 최신 stable 버전 다운로드 curl -LO “https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl” chmod +x kubectl sudo mv kubectl /usr/local/bin/ kubectl version –client |
7-4. Minikube 클러스터 시작
| minikube start |
설치가 끝날 때까지 기다린 뒤, 클러스터 상태를 확인합니다.
| kubectl get nodes |
아래와 같이 나오면 클러스터 준비 완료입니다.
| NAME STATUS ROLES AGE VERSION minikube Ready control-plane 2m v1.32.0 |
7-5. 로컬 클러스터 도구 비교
이번 가이드에서는 Minikube를 사용하지만, 로컬 쿠버네티스를 띄우는 다른 선택지도 있습니다.
| 도구 | 특징 |
| Minikube ⭐ | 가장 널리 알려진 로컬용 쿠버네티스. 학습·테스트용으로 추천 |
| kind | “Kubernetes IN Docker”의 줄임말. 노드를 Docker 컨테이너로 띄우는 방식이라 가볍고 빠름. CI 환경(GitHub Actions 등)에서 자주 사용 |
| k3s | Rancher에서 만든 경량 쿠버네티스 배포판. 엣지·IoT, 소규모 MLOps 환경 구성에 적합 |
8. FastAPI 앱 배포 — Deployment & Service
간단한 FastAPI 앱을 만들어 Minikube에 올리고, Service로 외부에서 접속해 본 뒤, Pod를 삭제해 Self-Healing을 확인하는 단계입니다.
8-1. 실습 디렉토리 구조
| fastapi-k8s/ ├── main.py ├── requirements.txt ├── Dockerfile ├── deployment.yaml ├── service.yaml # ClusterIP Service └── service_nodeport.yaml # NodePort Service (옵션) |
| mkdir fastapi-k8s cd fastapi-k8s |
8-2. FastAPI 앱 작성
main.py
| from fastapi import FastAPI app = FastAPI() @app.get(“/”) def read_root(): return {“message”: “Hello, Kubernetes!!”} @app.get(“/health”) def health(): return {“status”: “ok”} |
- / — 간단한 인사 메시지
- /health — 헬스체크용 (쿠버네티스 readiness/liveness probe에서 사용 예정)
requirements.txt
| fastapi==0.104.0 uvicorn==0.24.0 |
8-3. Dockerfile
| FROM python:3.11-slim # 작업 디렉토리 WORKDIR /app # 의존성 먼저 복사 & 설치 COPY requirements.txt . RUN pip install –no-cache-dir -r requirements.txt # 애플리케이션 코드 복사 COPY main.py . # 컨테이너 내부에서 열어줄 포트 EXPOSE 8000 # FastAPI 앱 실행 (main.py 안의 app 객체) CMD [“uvicorn”, “main:app”, “–host”, “0.0.0.0”, “–port”, “8000”] |
8-4. Minikube Docker 환경에서 이미지 빌드
Minikube가 자체 Docker 데몬을 사용하므로, 이미지를 Minikube가 보는 Docker에 빌드하는 것이 편합니다.
| # 현재 셸에서 docker 명령이 Minikube 내부 Docker를 바라보게 함 eval $(minikube docker-env) # Deployment에서 쓸 이미지 태그(v1)에 맞춰 빌드 docker build -t fastapi-k8s:v1 . docker images | grep fastapi-k8s |
8-5. Deployment 작성 및 배포
deployment.yaml
| apiVersion: apps/v1 kind: Deployment metadata: name: fastapi-k8s labels: app: fastapi-k8s spec: replicas: 2 selector: matchLabels: app: fastapi-k8s strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 template: metadata: labels: app: fastapi-k8s spec: containers: – name: fastapi image: fastapi-k8s:v1 imagePullPolicy: IfNotPresent ports: – containerPort: 8000 resources: requests: cpu: “100m” memory: “128Mi” limits: cpu: “500m” memory: “256Mi” readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 10 periodSeconds: 10 env: – name: APP_ENV value: “prod” |
핵심 포인트:
- replicas: 2 → Pod를 항상 2개 유지
- image: fastapi-k8s:v1 → 빌드한 이미지 태그와 반드시 일치
- readinessProbe / livenessProbe → /health 엔드포인트로 앱 상태 체크. 준비/생존 여부에 따라 트래픽 전달 여부를 조절
배포 및 상태 확인:
| kubectl apply -f deployment.yaml kubectl get pods -w |
8-6. Service 작성 — ClusterIP & NodePort
ClusterIP Service (service.yaml) — 클러스터 내부에서만 접근 가능
| apiVersion: v1 kind: Service metadata: name: fastapi-k8s labels: app: fastapi-k8s spec: type: ClusterIP selector: app: fastapi-k8s ports: – name: http port: 80 targetPort: 8000 |
NodePort Service (service_nodeport.yaml) — 클러스터 외부에서도 접근 가능
| apiVersion: v1 kind: Service metadata: name: fastapi-k8s labels: app: fastapi-k8s |
ClusterIP vs NodePort 비교
| 타입 | 특징 | 용도 |
| ClusterIP | 클러스터 내부에서만 접근. 서비스에 고정 IP 할당 | 서비스 간 통신 (백엔드 ↔ DB 등) |
| NodePort | 모든 Node의 특정 Port가 오픈. NodeIP:NodePort로 외부 접속 | 개발·테스트용, 간단한 외부 노출 |
8-7. FastAPI 앱 접속하기
방법 1: port-forward (ClusterIP)
| kubectl port-forward service/fastapi-k8s 8080:80 # 다른 터미널에서 curl http://localhost:8080/ # → {“message”: “Hello, Kubernetes!!”} curl http://localhost:8080/health # → {“status”: “ok”} |
방법 2: minikube service (NodePort)
| minikube service fastapi-k8s –url # 출력 예: http://127.0.0.1:30080 # 브라우저나 curl로 접속 # http://127.0.0.1:30080/ # http://127.0.0.1:30080/health |
8-8. Self-Healing 확인 — Pod를 일부러 삭제해 보기
쿠버네티스의 Self-Healing 동작을 직접 확인하려면, Pod를 일부러 삭제해 보면 됩니다. 터미널 두 개를 띄워두고 진행하면 관찰이 편합니다.
- 터미널 A: Pod 상태 실시간 관찰
- 터미널 B: Pod 삭제
| # 터미널 A — Pod 상태 실시간 관찰 kub ectl get pods -w # 터미널 B — Pod 이름 하나 골라서 삭제 kubectl delete pod <pod-name> |
터미널 A에서는 삭제한 Pod가 Terminating → 사라지고, 곧바로 새로운 Pod가 Pending → ContainerCreating → Running 으로 올라오는 모습을 확인할 수 있습니다. Deployment의 replicas: 2 설정을 맞추기 위해 쿠버네티스가 자동으로 새 Pod를 띄우는 동작입니다.
9. Rolling Update와 Rollback — 무중단 배포
실제 서비스 운영에서 중요한 두 개념을 v1 → v2 배포와 v2 → v1 롤백으로 직접 실습해 봅니다.
9-1. v2 코드 + 이미지 빌드
main.py에 버전 정보를 추가합니다.
| from fastapi import FastAPI app = FastAPI() @app.get(“/”) def read_root(): return { “message”: “Hello, Kubernetes v2!”, “version”: “2.0” } @app.get(“/health”) def health(): return { “status”: “ok”, “version”: “2.0” } |
| eval $(minikube docker-env) docker build -t fastapi-k8s:v2 . docker images | grep fastapi-k8s |
| 🔍 포인트 “이미지를 새로 빌드했다고 해서 바로 서비스에 반영되는 것은 아닙니다.” → Deployment 설정(image 태그)을 바꿔 줘야 롤링 업데이트가 시작됩니다. |
9-2. RollingUpdate 전략 옵션
이미 작성한 deployment.yaml에는 다음 부분이 있었습니다.
| strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 |
- maxUnavailable: 0 → 운영 중인 Pod 수가 설정보다 줄어들지 않게 (서비스 중단 없음)
- maxSurge: 1 → 업데이트 중 최대 1개까지 더 많이 띄우는 것 허용
예를 들어 replicas: 2, maxSurge: 1 이라면 한 순간에 최대 3개(2 + 1)까지 떠 있을 수 있고, 최소 2개는 계속 살아 있게 유지하면서 v1 → v2로 조금씩 바꿔치기하는 구조입니다.
9-3. 이미지 태그 변경 → 롤링 업데이트 실행
deployment.yaml의 컨테이너 이미지 부분을 수정합니다.
| containers: – name: fastapi image: fastapi-k8s:v2 # ← v2로 변경 imagePullPolicy: IfNotPresent ports: – containerPort: 8000 |
터미널에서 yaml 파일을 빠르게 수정하고 싶다면 (WSL / GNU sed 기준):
| sed -i ‘s/fastapi-k8s:v1/fastapi-k8s:v2/’ deployment.yaml grep “image:” deployment.yaml |
변경 사항 적용:
| kubectl apply -f deployment.yaml |
이 명령이 실행되는 순간 기존 v1 ReplicaSet은 점점 줄어들고, 새 v2 ReplicaSet이 생기면서 RollingUpdate 전략에 따라 Pod들이 교체되기 시작합니다.
9-4. 롤아웃 진행 상황 모니터링
| # Pod 목록 실시간 관찰 kubectl get pods -w # rollout 상태 확인 kubectl rollout status deployment fastapi-k8s # → deployment “fastapi-k8s” successfully rolled out # ReplicaSet 관찰 (선택) kubectl get rs -l app=fastapi-k8s |
v1 ReplicaSet과 v2 ReplicaSet이 잠깐 공존했다가, 나중에는 v2만 남는 것을 확인할 수 있습니다.
9-5. v2 정상 반영 확인
| minikube ip # 예: 192.168.49.2 curl http://192.168.49.2:30080 # → {“message”: “Hello, Kubernetes v2!”, “version”: “2.0”} curl http://192.168.49.2:30080/health # → {“status”: “ok”, “version”: “2.0”} |
9-6. Rollback — 이전 버전으로 되돌리기
v2 배포 후 문제가 생겼다고 가정하고 v1으로 되돌리는 실습입니다.
| # Pod 상태를 다른 터미널에서 관찰 kubectl get pods -w # 직전 리비전으로 롤백 kubectl rollout undo deployment fastapi-k8s kubectl rollout status deployment fastapi-k8s |
v2 Pod들이 내려가고, v1 Pod들이 다시 올라오면서 배포 전 상태로 돌아갑니다.
배포 히스토리 확인 & 특정 리비전 롤백
| kubectl rollout history deployment fastapi-k8s # revision 1 → v1 # revision 2 → v2 # 다시 v2로 복구 kubectl rollout undo deployment fastapi-k8s –to-revision=2 kubectl rollout status deployment fastapi-k8s |
9-7. 정리
- 이미지 빌드만으로는 배포가 바뀌지 않는다. → Deployment의 image 필드를 바꿔야 함
- Deployment = 원하는 상태(버전, replicas 등)를 선언하는 객체
- RollingUpdate 전략으로 서비스 다운타임 최소화 (maxUnavailable, maxSurge)
- Rollout / Undo / History — 새 버전 배포, 문제 시 이전 버전으로 롤백, 특정 리비전으로 재배포
10. ConfigMap & Secret — 설정 분리
실무에서 애플리케이션을 운영하다 보면 환경별로 달라지는 값(GREETING, LOG_LEVEL, API_URL 등)이나 민감한 값(DB 비밀번호, API 키, 토큰 등)이 계속 바뀝니다. 이걸 코드나 Docker 이미지 안에 하드코딩하면 설정 하나 바꿀 때마다 이미지를 다시 빌드해야 하고 보안상 위험합니다.
| ConfigMap → 환경설정·플래그·URL 등 비민감 설정값 Secret → DB 비밀번호·API 키·토큰 등 민감 정보 |
애플리케이션은 대개 환경 변수(env)로 이 값들을 받아서 사용합니다.
10-1. FastAPI v3 — 환경 변수 기반 코드
main.py
| import os from fastapi import FastAPI app = FastAPI() @app.get(“/”) def read_root(): return { “message”: os.getenv(“GREETING”, “Hello (default)”), “log_level”: os.getenv(“LOG_LEVEL”, “info”), “version”: “3.0” } @app.get(“/health”) def health(): return { “status”: “ok”, “version”: “3.0”, “has_secret”: bool(os.getenv(“API_KEY”)) } |
- GREETING — 없으면 “Hello (default)” 기본값 사용
- LOG_LEVEL — 없으면 “info” 기본값
- API_KEY — 설정되어 있으면 has_secret=true
10-2. v3 이미지 빌드
| eval $(minikube docker-env) docker build -t fastapi-k8s:v3 . docker images | grep fastapi-k8s |
10-3. ConfigMap 생성
| kubectl create configmap fastapi-config \ –from-literal=GREETING=”Hello from ConfigMap!” \ –from-literal=LOG_LEVEL=”info” kubectl get configmap kubectl describe configmap fastapi-config |
Data 섹션에 GREETING, LOG_LEVEL 값이 들어 있으면 성공입니다.
10-4. Secret 생성
| kubectl create secret generic fastapi-secret \ –from-literal=DB_PASSWORD=”1234″ \ –from-literal=API_KEY=”abc123″ kubectl get secret kubectl get secret fastapi-secret -o yaml |
| Secret의 값은 base64로 인코딩되어 보이지만, Pod 안에서는 평문 문자열 환경 변수로 들어옵니다. 운영 환경에서는 외부 KMS·Vault 연동이나 EncryptionConfiguration을 함께 검토하는 것이 좋습니다. |
10-5. Deployment에 ConfigMap & Secret 연결
컨테이너 스펙 부분을 다음과 같이 맞춰 줍니다.
| apiVersion: apps/v1 kind: Deployment metadata: name: fastapi-k8s labels: app: fastapi-k8s spec: replicas: 2 selector: matchLabels: app: fastapi-k8s template: metadata: labels: app: fastapi-k8s spec: containers: – name: fastapi image: fastapi-k8s:v3 imagePullPolicy: IfNotPresent ports: – containerPort: 8000 # 🔹 ConfigMap + Secret을 한 번에 env로 가져오기 envFrom: – configMapRef: name: fastapi-config – secretRef: name: fastapi-secret # 🔹 고정 값은 개별 env로 관리 env: – name: APP_ENV value: “prod” resources: requests: cpu: “100m” memory: “128Mi” limits: cpu: “500m” memory: “256Mi” readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 10 periodSeconds: 10 |
| kubectl apply -f deployment.yaml kubectl rollout restart deployment fastapi-k8s kubectl rollout status deployment fastapi-k8s |
10-6. Pod 안에서 환경 변수 확인
| kubectl get pods -l app=fastapi-k8s kubectl exec -it <pod-name> — env | egrep ‘GREETING|LOG_LEVEL|API_KEY|APP_ENV’ |
예상 출력:
| GREETING=Hello from ConfigMap! LOG_LEVEL=info API_KEY=abc123 APP_ENV=prod |
10-7. NodePort로 최종 동작 확인
| minikube ip # 예: 192.168.49.2 curl http://192.168.49.2:30080 # → {“message”: “Hello from ConfigMap!”, “log_level”: “info”, “version”: “3.0”} curl http://192.168.49.2:30080/health # → {“status”: “ok”, “version”: “3.0”, “has_secret”: true} |
- message → ConfigMap의 GREETING 값
- log_level → ConfigMap의 LOG_LEVEL 값
- has_secret: true → Secret에서 API_KEY가 정상 주입되었다는 의미
10-8. 설정 변경 시 Pod 재시작 필요 — 자주 헷갈리는 부분
ConfigMap이나 Secret 값을 바꾸어도 이미 떠 있는 Pod의 환경 변수는 자동으로 갱신되지 않습니다. 환경 변수는 Pod가 시작할 때 주입되기 때문입니다. 새 키를 추가해도 마찬가지입니다.
일반적인 패턴은 다음과 같습니다.
- ConfigMap / Secret 수정 (kubectl apply)
- 필요 시 Deployment 스펙도 수정
- kubectl rollout restart deployment … 로 재시작
| kubectl rollout restart deployment fastapi-k8s |
참고로 ConfigMap을 볼륨으로 마운트한 경우에는 일정 시간이 지나면 파일 내용이 자동으로 갱신되지만, 환경 변수로 주입한 경우에는 위와 같이 재시작이 필요합니다.
11. Reverse Proxy + HPA — 실전 운영 구성
FastAPI 앞단에 Reverse Proxy(Nginx, Caddy)를 두고, Metrics Server + HPA로 오토스케일링까지 구성한 뒤, hey로 부하 테스트를 해 보는 단계입니다. 로컬 Minikube 환경에서 실제 운영 환경처럼 로드 밸런싱 → 프록시 → 스케일링 → 부하 테스트로 이어지는 흐름을 직접 확인할 수 있습니다.
11-1. FastAPI 서비스 준비
이번 단계에서는 새로 작성한 fastapi.yaml의 Deployment와 Service를 기동합니다.
| apiVersion: apps/v1 kind: Deployment metadata: name: fastapi-test2-k8s labels: { app: fastapi } spec: replicas: 2 selector: matchLabels: { app: fastapi } template: metadata: labels: { app: fastapi } spec: containers: – name: app image: tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim env: – name: PORT value: “80” ports: – containerPort: 80 readinessProbe: httpGet: { path: /docs, port: 80 } initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: { path: /docs, port: 80 } initialDelaySeconds: 10 periodSeconds: 10 — apiVersion: v1 kind: Service metadata: name: fastapi-svc labels: { app: fastapi } spec: selector: { app: fastapi } ports: – name: http port: 80 targetPort: 80 type: ClusterIP |
| kubectl apply -f fastapi.yaml kubectl get svc fastapi-svc |
FastAPI Pod를 2개 이상 띄워 두면 뒤에서 로드 밸런싱 테스트가 자연스럽게 됩니다.
11-2. Nginx Reverse Proxy 구성
Nginx는 서버 앞단에서 클라이언트 요청을 받아 백엔드 서버로 전달하는 Reverse Proxy로 많이 사용됩니다. 대표적인 기능은 요청 라우팅, 로드 밸런싱, TLS 종료, 캐싱 등입니다. 이번 실습에서는 FastAPI Pod 여러 개에 요청을 분산하는 프록시 역할로 사용합니다.
nginx.conf 작성 → ConfigMap 생성
Nginx 설정 파일은 Pod 내부가 아니라 ConfigMap으로 관리하는 것이 표준 패턴입니다.
| ## nginx.conf worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main ‘$remote_addr – $remote_user [$time_local] ‘ ‘”$request” $status $body_bytes_sent ‘ ‘”$http_referer” “$http_user_agent” ‘ ‘rt=$request_time urt=$upstream_response_time ‘ ‘ua=$upstream_addr’; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; keepalive_timeout 65s; server_tokens off; client_max_body_size 16m; gzip on; gzip_comp_level 5; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript application/xml text/xml application/rss+xml image/svg+xml; # K8s Service로 프록시 upstream fastapi_upstream { server fastapi-svc:80; # 클러스터 DNS로 해석 keepalive 32; } server { listen 80 default_server; server_name _; # Nginx 자체 헬스체크 엔드포인트 location = /nginx-health { return 200 ‘ok’; add_header Content-Type text/plain; } # 정적 자원 캐시 힌트 location /static/ { proxy_pass http://fastapi_upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection “”; expires 1h; add_header Cache-Control “public, max-age=3600”; add_header X-Served-By “Nginx” always; } # 기본 프록시 location / { proxy_pass http://fastapi_upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection “”; proxy_buffering on; proxy_buffers 32 8k; proxy_busy_buffers_size 64k; proxy_read_timeout 60s; proxy_send_timeout 60s; add_header X-Served-By “Nginx” always; } } } |
| kubectl create configmap nginx-config \ –from-file=nginx.conf=nginx.conf \ -o yaml –dry-run=client | kubectl apply -f – kubectl get configmap nginx-config -o yaml |
Nginx Deployment 배포 및 Service 생성
| kubectl apply -f nginx.yaml kubectl get pods -l app=nginx-proxy -w kubectl apply -f nginx-svc.yaml kubectl get svc nginx-svc minikube service nginx-svc –url |
Reverse Proxy 테스트 — 로드 밸런싱 확인
| curl http://192.168.49.2:30081 # FastAPI Pod가 2개 이상이라면 번갈아 응답 for i in {1..5}; do curl http://192.168.49.2:30081 echo “” done |
11-3. Caddy Reverse Proxy 구성
Caddy는 자동 HTTPS(Let’s Encrypt 통합)와 간결한 Caddyfile 문법이 강점인 Reverse Proxy 서버입니다. Nginx보다 설정이 짧아 개발 환경에서 인기가 많습니다.
Caddyfile 작성 → ConfigMap 생성
| ## Caddyfile { admin off log default { level INFO format console output file /var/log/caddy/access.log { roll_size 10MiB roll_keep 5 roll_keep_for 720h } } servers { trusted_proxies static private_ranges } } :80 { @health path /caddy-health handle @health { respond “ok” 200 } handle_path /static/* { reverse_proxy http://fastapi-svc:80 header Cache-Control “public, max-age=3600” } handle { reverse_proxy http://fastapi-svc:80 { health_uri /health flush_interval -1 transport http { read_timeout 60s write_timeout 60s keepalive 30s } } } encode zstd gzip } |
| kubectl create configmap caddy-config \ –from-file=Caddyfile=Caddyfile \ -o yaml –dry-run=client | kubectl apply -f – kubectl get configmap caddy-config -o yaml |
Caddy Deployment + Service 작성
caddy.yaml:
| apiVersion: apps/v1 kind: Deployment metadata: name: caddy-proxy labels: { app: caddy-proxy } spec: replicas: 1 selector: matchLabels: { app: caddy-proxy } template: metadata: labels: { app: caddy-proxy } spec: containers: – name: caddy image: caddy:2 ports: – containerPort: 80 resources: requests: { cpu: “100m”, memory: “128Mi” } limits: { cpu: “500m”, memory: “256Mi” } livenessProbe: httpGet: { path: /caddy-health, port: 80 } initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: { path: /caddy-health, port: 80 } initialDelaySeconds: 3 periodSeconds: 5 volumeMounts: – name: caddy-conf mountPath: /etc/caddy/Caddyfile subPath: Caddyfile – name: caddy-logs mountPath: /var/log/caddy volumes: – name: caddy-conf configMap: name: caddy-config items: – key: Caddyfile path: Caddyfile – name: caddy-logs emptyDir: {} |
caddy-svc.yaml:
| apiVersion: v1 kind: Service metadata: name: caddy-svc labels: { app: caddy-proxy } spec: selector: { app: caddy-proxy } type: NodePort ports: – name: http port: 80 targetPort: 80 nodePort: 30082 |
| kubectl apply -f caddy.yaml kubectl get pods -l app=caddy-proxy -w kubectl apply -f caddy-svc.yaml kubectl get svc caddy-svc minikube service caddy-svc –url |
| ⚠ 루트(/)는 Caddyfile에서 비워뒀기 때문에 /caddy-health 또는 /static/ 으로 확인해야 정상입니다. |
| # 헬스 체크 curl http://192.168.49.2:30082/caddy-health # FastAPI 응답 curl http://192.168.49.2:30082/static/ |
11-4. 현재 아키텍처
| [Client] ├──> Nginx Service(30081) ──> Nginx Pods ──> FastAPI Pods └──> Caddy Service(30082) ──> Caddy Pods ──> FastAPI Pods |
둘 다 FastAPI 뒤에서 Reverse Proxy 역할을 하며, FastAPI Pod는 2개 이상으로 로드 밸런싱이 가능합니다.
11-5. Metrics Server + HPA
Metrics Server는 Pod·Node의 CPU·메모리 메트릭을 수집해 주는 시스템입니다. HPA가 스케일링하려면 반드시 Metrics Server가 필요합니다.
Metrics Server 활성화
| minikube addons enable metrics-server kubectl get deployment metrics-server -n kube-system sleep 60 kubectl top nodes kubectl top pods |
메트릭 수집은 약 1~2분 뒤부터 시작됩니다.
HPA용 FastAPI Deployment (deployment.yaml)
| apiVersion: apps/v1 kind: Deployment metadata: name: fastapi-deploy spec: replicas: 2 selector: matchLabels: { app: fastapi } template: metadata: labels: { app: fastapi } spec: containers: – name: app image: tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim ports: – containerPort: 80 # ↙︎ HPA가 참조할 리소스 요청/제한 resources: requests: cpu: “100m” memory: “128Mi” limits: cpu: “500m” memory: “256Mi” readinessProbe: httpGet: { path: /docs, port: 80 } initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: { path: /docs, port: 80 } initialDelaySeconds: 10 periodSeconds: 10 |
| kubectl apply -f deployment.yaml kubectl rollout status deployment fastapi-deploy kubectl get pods -l app=fastapi sleep 60 kubectl top pods -l app=fastapi |
HPA 생성
| kubectl autoscale deployment fastapi-deploy –cpu=”50%” –min=2 –max=10 kubectl get hpa # unknown 이 뜨면 kubectl delete deploy fastapi-test2-k8s 후 다시 확인 |
11-6. 부하 테스트 (hey)
부하 테스트 도구 비교:
| 도구 | 특징 |
| ab | 기본적인 HTTP 벤치마크 |
| wrk | 고성능, Lua 스크립팅 가능 |
| hey | Go 기반, 가볍고 간편 |
| k6 | 시나리오 기반, 기업용 테스트 |
이번 실습에는 hey를 사용합니다.
| wget https://hey-release.s3.us-east-2.amazonaws.com/hey_linux_amd64 chmod +x hey_linux_amd64 sudo mv hey_linux_amd64 /usr/local/bin/hey hey -h |
Reverse Proxy 정상 응답 확인
| curl -i http://192.168.49.2:30082/caddy-health curl -i http://192.168.49.2:30082/static/ |
고부하 테스트 → HPA 스케일링 확인
| # Terminal #1: Pod 증가 관찰 watch -n 1 ‘kubectl get pods -l app=fastapi’ # Terminal #2: HPA 확인 kubectl get hpa # Terminal #3: 3분간 폭주 요청 hey -z 3m -c 50 -q 500 http://192.168.49.2:30082/static/ |
부하량이 높아지면 fastapi-deploy의 Pod가 자동으로 2 → 3 → 4 …로 증가하는 오토스케일링 과정을 실시간으로 볼 수 있습니다.
hey 결과 예시
| Summary: Total: 167.4590 secs Slowest: 0.1411 secs Fastest: 0.0003 secs Average: 0.0158 secs Requests/sec: 3171.1940 Total data: 48325095 bytes Size/request: 91 bytes Latency distribution: 10% in 0.0055 secs 25% in 0.0089 secs 50% in 0.0136 secs 75% in 0.0199 secs 90% in 0.0278 secs 95% in 0.0348 secs 99% in 0.0547 secs Status code distribution: [200] 531045 responses |
Part 3
마무리
12. 자주 마주치는 문제 정리
쿠버네티스를 처음 다룰 때는 개념보다 에러 처리에서 시간을 더 많이 쓰게 됩니다. 대표적인 패턴 몇 가지를 정리합니다.
12-1. ImagePull 에러 (ImagePullBackOff / ErrImagePull)
- 이미지 이름·태그 오타 확인
- Private Registry라면 Secret 생성 → imagePullSecrets 설정 여부 확인
- 이미지를 빌드만 하고 레지스트리에 Push하지 않은 경우인지 확인
간단 체크 — 로컬에서 docker pull <이미지> 가 되는지 먼저 확인하면 빠릅니다.
12-2. Service / Ingress / 포트 문제
Pod는 잘 떠 있는데 외부에서 접속이 안 되는 상황에서 자주 발생합니다.
- Service 타입(NodePort / LoadBalancer / ClusterIP) 설정 확인
- Ingress 리소스의 host, path, backend Service 이름·포트 확인
- Ingress 리소스만 만들었다고 끝이 아님 — 이를 실제로 처리해 줄 Ingress Controller(ingress-nginx, traefik 등)가 클러스터에 설치되어 있어야 함
12-3. 리소스 요청/제한 설정
리소스 requests / limits 를 대충 두면 어떤 Pod는 CPU·메모리를 과도하게 사용하고, 어떤 Pod는 너무 적게 할당받아 성능이 안 나오는 문제가 생깁니다. 특히 AI·빅데이터 작업은 메모리·CPU·GPU 요구량이 크기 때문에, 각 Job/Pod별로 리소스 설정을 명시적으로 적어 두는 것을 권장합니다.
13. 앞으로 어떻게 공부할까?
이 가이드에서는 개념과 기본 운영까지 다뤘습니다. 다음 단계로는 자연스럽게 이런 키워드들을 마주하게 됩니다.
- Helm — 여러 YAML을 템플릿화해 관리하는 쿠버네티스용 패키지 매니저
- Argo CD / Flux — GitOps 도구. Git에 저장된 선언 상태를 기준으로 클러스터를 자동 관리
- Argo Workflows / Airflow — 데이터/ML 파이프라인 오케스트레이션
- Kubeflow, KServe, Seldon, MLflow + K8s — 쿠버네티스 기반 MLOps 플랫폼
13-1. AI·빅데이터 직군에서 어디까지 알면 좋을까?
개인적으로는 다음 3단계로 나누어 볼 수 있습니다.
| 단계 | 역할 | 내용 |
| Level 1 | 사용자 관점 | Pod / Deployment / Service 개념 이해. kubectl로 배포·로그 확인·스케일 조절. 간단한 모델 서버나 ETL Job을 YAML 수정해 배포할 수 있는 수준 |
| Level 2 | 실무 활용 | Namespace, ConfigMap, Secret, Ingress 활용. Helm Chart로 공통 템플릿을 만들어 팀과 공유. 오토스케일링·리소스 최적화 개념 이해 |
| Level 3 | 플랫폼·인프라 | 클러스터 구성, 네트워크·스토리지, 모니터링·로그 수집까지. DevOps·플랫폼 엔지니어 역할에 가까운 영역. 아키텍트·리더라면 개념 정도는 파악해 두면 좋음 |
Level 1까지가 이 문서의 목표였고, Level 2·3은 위에서 소개한 도구들을 하나씩 익혀 가면서 자연스럽게 넓혀 가시면 됩니다.