---
title: '프론트엔드 개발자를 위한 쿠버네티스 개념 지도: 파드에서 오토스케일러까지'
tags:
  - kubernetes
  - frontend
  - nodejs
  - infrastructure
  - devops
published: true
date: 2026-08-05 20:00:00
description: 'SSR을 운영하는 프론트엔드 개발자가 마주치는 쿠버네티스 용어와 구조를 실무 흐름 순서로 정리했다. 클러스터의 전체 구조부터 배포, 파드의 상태와 자원, 트래픽 경로, 오토스케일링까지. 시리즈의 첫 편이자 이후 편들의 참조 지도다.'
thumbnail: /thumbnails/2026/08/k8s-for-frontend-1.png
series: '프론트엔드 개발자가 알아야 할 쿠버네티스'
seriesOrder: 1
---

## Table of Contents

## 프론트엔드 개발자와 쿠버네티스

Next.js든 Remix든, SSR이나 BFF(Backend For Frontend, 프론트엔드 팀이 직접 관리하는 API 중간 서버)를 운영하는 순간 프론트엔드 개발자도 쿠버네티스 사용자가 된다. 배포 파이프라인이 파드를 갈아 끼우고, 모니터링 알림에 OOMKilled 같은 단어가 찍히고, 인프라 조직과의 대화에 requests와 readiness가 오간다. 그런데 이 용어들을 체계적으로 배울 기회는 의외로 드물다. 대부분은 필요할 때마다 하나씩 검색해 파편으로 알게 되고, 파편들이 서로 어떻게 연결되는지는 흐릿한 채로 남는다.

이 글은 그 파편들을 하나의 지도로 잇는 용어·개념 정리다. 사전처럼 나열하는 대신 실무에서 만나는 흐름 순서로 묶었다. 클러스터의 전체 구조를 먼저 그리고, 배포를 이루는 것들, 파드의 상태와 자원, 트래픽 경로, 오토스케일링 순으로 내려간다. 뒤쪽에는 경계가 흐려지기 쉬운 개념 쌍들과, 시리즈 전체에서 돌아와 참조할 수 있는 용어 인덱스를 표로 모아 두었다.

이 글은 "프론트엔드 개발자가 알아야 할 쿠버네티스" 시리즈의 첫 편이기도 하다. 여기서는 용어와 구조를 정리하는 데 집중하고, 실제 측정은 다음 편들이 맡는다. 컨테이너와 파드의 실체를 확인하는 [2편](/2026/08/k8s-for-frontend-2), 트래픽 경로를 따라가는 편, 파드의 삶과 죽음을 다루는 편, 오토스케일링 편으로 이어지며, 이미 공개된 [Node.js 파드 사이징 글](/2026/08/nodejs-k8s-pod-sizing)이 시리즈의 심화 종착점이다.

> 이 글의 쿠버네티스 관련 서술은 Kubernetes v1.36 기준이다. 개념 정리가 목적이라 버전을 타는 세부 동작은 최소화했다.

## 클러스터의 전체 구조

쿠버네티스 클러스터는 크게 두 부분으로 나뉜다. 결정을 내리는 **컨트롤 플레인**(control plane)과, 앱이 실제로 올라가는 **노드**(Node)들이다. 관리 회사에 비유하면 본사와 각 동에 해당한다.

노드는 그냥 서버다. AWS라면 EC2 인스턴스 하나가 노드 하나다. 앱은 **파드**(Pod)라는 단위로 포장되어 이 노드들 위에 흩어져 올라간다. 파드가 정확히 무엇인지는 다음 절에서 다루고, 지금은 "실행 중인 앱 한 부"로 잡아 둔다.

컨트롤 플레인 쪽에서 알아둘 구성 요소는 셋이다.

- **API 서버**: 클러스터의 유일한 관문이다. 뒤에 나올 `kubectl`도, 배포 파이프라인도, 쿠버네티스 내부 구성 요소들끼리도 전부 API 서버를 통해서만 대화한다. 클러스터에 무언가를 시키는 방법은 이 API에 요청을 보내는 것 하나뿐이다.
- **etcd**: 클러스터의 모든 상태(어떤 앱이 몇 개, 어디에, 어떤 설정으로 떠 있어야 하는지)가 기록되는 장부 데이터베이스다. 직접 만질 일은 거의 없지만, 클러스터의 진실은 여기 적힌 것뿐이라는 감각은 여러 동작을 이해하는 데 유용하다.
- **스케줄러(scheduler)**: 새로 만들어졌지만 아직 갈 곳이 정해지지 않은 파드를 보고, 어느 노드에 둘지 정한다. 각 노드에 남은 예약량을 따져서 자리를 고른다.

노드 쪽에는 둘이 있다.

- **kubelet**: 각 노드에 상주하는 에이전트다. "이 노드에 배정된 파드 목록"을 API 서버에서 받아, 컨테이너 런타임(containerd 등)을 시켜 실제 프로세스로 만든다. 파드의 건강 검사(뒤에 나올 probe)도 kubelet의 일이다.
- **kube-proxy**: 파드로 트래픽이 찾아올 수 있게 각 노드의 네트워크 규칙을 관리한다. 트래픽 절에서 다시 나온다.

마지막으로 **kubectl**은 개발자가 API 서버에 요청을 보낼 때 쓰는 공식 CLI다. `kubectl get pods`는 장부에서 파드 목록을 읽는 GET 요청이고, `kubectl apply`는 원하는 상태를 제출하는 요청이다. 특별한 도구가 아니라 HTTP 클라이언트라는 점을 기억해 두면 쿠버네티스의 많은 동작이 단순해 보인다.

구조는 이게 전부다. 컨트롤 플레인이 장부(etcd)를 근거로 결정을 내리면, 각 노드의 kubelet이 그 결정을 실행한다. 이제 이 구조 위에서 용어들을 흐름 순서로 본다.

## 배포를 이루는 것들: 이미지, Deployment, 파드

머지 버튼을 누르고 몇 분 뒤 서비스에 반영되기까지, 그 사이에 등장하는 개념들부터다.

CI가 하는 일은 코드를 **이미지**(image)로 만드는 것이다. 이미지는 앱과 그 실행에 필요한 파일 전체(Node.js 런타임, node_modules, 빌드 산출물)를 통째로 찍은 스냅샷이고, 완성되면 **레지스트리**(registry, 이미지 저장소. ECR이나 Docker Hub 같은 것)에 올라간다. 노드는 파드를 띄울 때 이 레지스트리에서 이미지를 내려받는다(pull). 이미지가 왜 생각보다 훨씬 크고 그 크기가 배포 속도에 어떻게 영향을 주는지는 [2편](/2026/08/k8s-for-frontend-2)에서 직접 잰다.

배포에서 실제로 작성하고 수정하는 것은 이미지가 아니라 **매니페스트**(manifest)라는 YAML 문서이고, 그 중심에 **Deployment**가 있다. Deployment는 "이 이미지를 파드 몇 개로 유지하라"는 **선언**이다. 여기서 쿠버네티스의 중심 사상이 나온다. "파드를 띄워라"라고 명령하는 것이 아니라, "파드 3개가 떠 있는 상태를 원한다"라고 장부에 적는 것이다. 그러면 컨트롤러라는 자동화 장치들이 현재 상태를 장부에 적힌 상태 쪽으로 계속 밀고 간다. 파드 하나가 죽으면 별도 명령 없이 새로 만들어지는 이유가 이것이다. 선언과 현실의 차이를 메우는 것이 컨트롤러의 일이기 때문이다.

Deployment와 파드 사이에는 **ReplicaSet**이라는 중간 관리자가 하나 더 있다. 평소에는 존재를 몰라도 되지만, `kubectl get pods`에서 파드 이름이 `my-app-6c8fb44888-wvx2x`처럼 생긴 이유가 이것이다. `my-app`(Deployment) 뒤의 해시가 ReplicaSet, 마지막 다섯 글자가 파드 고유 접미사다. 계층으로 정리하면 이렇다.

| 계층       | 역할                                           | 우리가 만지는가            |
| ---------- | ---------------------------------------------- | -------------------------- |
| Deployment | "이 앱을 N개 유지, 새 버전은 이렇게 교체" 선언 | 직접 작성한다              |
| ReplicaSet | 특정 버전의 파드 개수를 유지하는 중간 관리자   | 자동 생성, 만질 일 없음    |
| Pod        | 실행의 최소 단위. 소모품                       | 직접 만들지 않는 것이 원칙 |

파드가 소모품이라는 성질이 중요하다. 파드는 죽고, 대체되고, 다른 노드로 옮겨 다시 만들어지며, 이름도 그때마다 바뀐다. 그래서 특정 파드에 의존하는 설계(파드 로컬 파일에 상태 저장, 특정 파드 IP 하드코딩)는 처음부터 어긋난다. 관리 대상은 파드가 아니라 Deployment라는 선언이다.

새 버전으로의 교체는 이 구조 위에서 **롤링 업데이트**(rolling update)로 이뤄진다. Deployment가 새 이미지로 새 ReplicaSet을 만들고, 새 파드를 하나 띄우고, 준비가 확인되면 옛 파드를 하나 줄이는 식으로 점진적으로 교체한다. "준비가 확인되면"의 판정 기준이 다음 절의 readiness고, 이 교체 도중 요청이 유실되지 않기 위한 조건들은 파드의 삶과 죽음 편에서 실측으로 다룬다.

배포를 되돌리는 일도 이 구조 덕에 단순하다. 이전 ReplicaSet이 남아 있으므로 `kubectl rollout undo`는 새로 빌드할 것 없이 이전 버전의 파드를 다시 늘리는 것으로 **롤백**을 수행한다. 배포 진행 상황을 지켜보는 `kubectl rollout status`와 함께, rollout이라는 단어가 붙은 명령들은 전부 이 Deployment의 버전 교체를 다루는 도구다.

이 절에는 조용한 개념이 둘 더 있다. **레이블**(label)과 **셀렉터**(selector)다. 쿠버네티스에서 "이 Deployment의 파드들", "이 서비스가 트래픽을 보낼 파드들" 같은 소속 관계는 전부, 파드에 붙은 레이블(`app: my-app` 같은 키-값)을 셀렉터로 골라내는 방식으로 성립한다. 오브젝트들이 서로를 이름으로 직접 참조하지 않고 레이블로 느슨하게 묶이는 구조라서, 트래픽이 엉뚱한 파드로 가는 문제를 만나면 대개 이 레이블-셀렉터 매칭부터 확인하게 된다.

실무에서는 이 YAML들을 손으로 직접 관리하기보다 도구를 거치는 경우가 많다. **Helm**은 매니페스트를 템플릿과 값 파일로 나눠 패키지("차트")로 만드는 도구이고, **Kustomize**는 공통 매니페스트에 환경별 차이(dev/prod)를 덧대는 방식이다. 어느 쪽이든 최종 산출물은 결국 API 서버에 제출되는 같은 YAML이라는 점을 알아두면, 도구가 낯설어도 길을 잃지 않는다.

## 매니페스트의 기본 문법

Deployment가 무엇인지 알았으니, 그 YAML이 실제로 어떻게 생겼는지도 여기서 읽어 둔다. 쿠버네티스의 모든 오브젝트는 종류와 무관하게 같은 골격을 공유하기 때문에, 이 골격 하나만 익히면 처음 보는 매니페스트도 구조가 눈에 들어온다.

- **apiVersion**: 이 오브젝트가 속한 API 그룹과 버전. Deployment는 `apps/v1`, Pod나 Service처럼 초기부터 있던 것들은 그냥 `v1`이다.
- **kind**: 오브젝트의 종류. `Deployment`, `Service`, `ConfigMap` 등.
- **metadata**: 이름, Namespace, 레이블 같은 신원 정보.
- **spec**: 원하는 상태의 본문. 오브젝트 종류마다 내용이 다르고, 우리가 작성하는 내용의 대부분이 여기 들어간다.

이 넷이 우리가 쓰는 전부다(조회하면 보이는 `status` 필드는 시스템이 채우는 현재 상태라 직접 쓰지 않는다). 최소한의 Deployment와 Service를 주석과 함께 붙여 보면 이렇다.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3 # 이 파드를 3개 유지하라
  selector:
    matchLabels:
      app: my-app # 아래 template의 레이블과 일치해야 한다
  template: # 여기부터가 파드의 설계도
    metadata:
      labels:
        app: my-app # 파드마다 붙는 레이블
    spec:
      containers:
        - name: app
          image: my-registry/my-app:1.2.3
          ports:
            - containerPort: 3000
          resources: # 자원 계약. 다음 절에서 설명한다
            requests: {cpu: '500m', memory: '256Mi'}
            limits: {cpu: '1', memory: '512Mi'}
          readinessProbe: # 트래픽을 받아도 되는지 검사. 역시 다음 절에서
            httpGet: {path: /api/health, port: 3000}
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app # 이 레이블이 붙은 파드들에게 트래픽을 보낸다
  ports:
    - port: 80
      targetPort: 3000
```

읽을 때 눈여겨볼 지점이 몇 개 있다. 첫째, `app: my-app`이라는 레이블이 세 군데(Deployment의 selector, 파드 template, Service의 selector) 나오는데, 앞 절에서 말한 레이블-셀렉터 매칭이 바로 이것이다. 이 셋이 어긋나면 파드는 뜨는데 트래픽이 안 가는 상태가 된다. 둘째, `template` 아래가 통째로 파드의 설계도라서, spec이 두 번 나온다(Deployment의 spec 안에 파드의 spec). 처음 볼 때 가장 헷갈리는 중첩인데, "바깥은 Deployment의 원하는 상태, 안쪽은 파드 하나의 모양"으로 읽으면 된다. 셋째, `---`는 한 파일에 여러 문서를 잇는 YAML 문법이다. 관련 오브젝트를 한 파일에 두고 `kubectl apply -f` 한 번으로 제출하는 관례가 여기서 나온다.

단위 표기도 자주 걸리는 지점이라 짚어 둔다. CPU의 `500m`은 밀리코어, 즉 0.5코어다(`cpu: '1'`이 1코어). 메모리의 `Mi`는 2의 거듭제곱 기반(1Mi = 1,048,576바이트)이고 `M`(1M = 1,000,000바이트)과 다른 단위인데, 관례적으로 쿠버네티스에서는 `Mi`/`Gi`를 쓴다. 그리고 YAML은 따옴표 없는 값을 스스로 해석하려 든다. Helm 값 파일에 이미지 태그를 `tag: 1.20`으로 적으면 문자열이 아니라 숫자 1.2가 되어 있는 식이다. 그래서 `cpu: '500m'`처럼 문자열이어야 하는 수량 값에는 따옴표가 안전한데, 반대로 모든 값에 따옴표를 치는 것도 답이 아니다. `replicas: '3'`처럼 정수 필드에 문자열을 주면 API 서버가 타입 오류로 거부한다. 필드의 타입이 무엇인지 확인하는 방법이 바로 다음에 나온다.

필드 이름이 기억나지 않을 때는 문서를 뒤지는 것보다 `kubectl explain`이 빠르다. `kubectl explain deployment.spec.template.spec.containers`처럼 경로를 점으로 이어가며 각 필드의 설명과 타입을 터미널에서 바로 볼 수 있다.

## 파드의 상태와 자원: probe, requests, limits

파드가 떠 있는 동안의 건강과 자원에 관한 용어들이다. 모니터링 알림과 장애 대화에서 가장 자주 나오는 묶음이기도 하다.

먼저 파드의 건강을 판정하는 **probe** 3종이 있다. 전부 kubelet이 주기적으로 실행하는 검사다.

| probe     | 질문                         | 실패하면                               |
| --------- | ---------------------------- | -------------------------------------- |
| startup   | 아직 시작 중인가?            | (시작 유예를 넘기면) 컨테이너 재시작   |
| readiness | 지금 트래픽을 받아도 되는가? | 트래픽 대상에서 제외 (죽이지는 않는다) |
| liveness  | 살아는 있는가?               | 컨테이너 재시작                        |

"배포는 나갔는데 readiness가 안 떠서 트래픽이 이전 버전으로 가고 있다"는 상황은 이 표로 읽을 수 있다. 새 파드가 readiness 검사를 통과하지 못해 트래픽 대상 목록에 오르지 못했고, 롤링 업데이트가 그 지점에서 멈춰 있으니 요청은 계속 옛 파드들로 간다는 뜻이다. readiness와 liveness의 구분은 실무에서 자주 틀리는 지점인데, liveness 실패는 재시작이라는 파괴적 조치로 이어지므로 외부 의존성(DB, 다운스트림 API)의 상태를 liveness에 엮으면 의존성 장애가 전체 파드의 연쇄 재시작으로 번진다. 이 사고의 재현은 파드의 삶과 죽음 편에서 다룬다.

다음으로 자원 계약이다. 파드 명세에는 CPU와 메모리에 대해 **requests**와 **limits**라는 두 값을 적는다.

- **requests**: 예약량이다. 스케줄러가 이 파드를 어느 노드에 둘 수 있는지 계산할 때 쓰는 값이고, 실제 사용량과는 무관하다.
- **limits**: 상한이다. 실제 사용이 이 값을 넘으면 제재가 들어오는데, CPU는 느려지고(스로틀), 메모리는 죽는다(OOMKill). 이 비대칭이 중요하다.

메모리 limit을 넘겨 죽은 파드에는 **OOMKilled**(Out Of Memory)가 기록된다. `kubectl describe pod`에서 종료 코드 **137**과 함께 보이는데, 137은 SIGKILL로 종료됐다는 관례적 코드다(128 + 신호 번호 9). 이 죽음은 앱의 에러 로그에 아무것도 남기지 않는다는 특징이 있다. 커널이 프로세스 밖에서 즉시 종료시키기 때문에, JS 스택 트레이스를 뒤져도 나오지 않는다. requests와 limits를 실측 기반으로 정하는 방법은 [사이징 글](/2026/08/nodejs-k8s-pod-sizing)의 전체 주제이므로, 여기서는 예약과 상한이라는 구분만 가져가면 된다.

`kubectl get pods`의 STATUS 열에서 자주 만나는 이상 상태도 이 자리에서 묶어 둔다.

- **Pending**: 파드가 만들어졌지만 아직 어느 노드에도 배치되지 못한 상태. requests를 수용할 수 있는 노드가 없는 경우가 대표적 원인이다.
- **ImagePullBackOff**: 이미지를 레지스트리에서 내려받지 못해 재시도 간격을 벌리고 있는 상태. 이미지 태그 오타, 레지스트리 인증 실패, 존재하지 않는 태그가 흔한 원인이다.
- **CrashLoopBackOff**: 파드가 뜨자마자 죽기를 반복해, kubelet이 재시작 간격을 지수적으로 벌린 상태다(10초에서 시작해 최대 5분까지). 죽음의 원인이 해소되지 않은 채 재시작만 반복 중이라는 뜻이다.
- **Evicted**: 노드의 자원 압박(주로 메모리, 디스크) 때문에 kubelet이 파드를 쫓아낸 상태. 파드 자신의 문제가 아니라 노드 사정일 수 있다는 점에서 위의 상태들과 결이 다르다.

마지막으로 파드가 종료되는 절차다. 쿠버네티스가 파드를 내릴 때는 먼저 **SIGTERM** 신호를 보내 정리할 시간을 주고, **terminationGracePeriodSeconds**(기본 30초)가 지나도 끝나지 않으면 SIGKILL로 강제 종료한다. 앱이 SIGTERM을 받아 처리 중이던 요청을 마저 끝내고 내려가는 것을 **그레이스풀 셧다운**(graceful shutdown)이라고 하는데, Node.js 앱에서는 이 신호가 의외로 잘 전달되지 않는 함정들(PID 1 문제)이 있다. 이것도 삶과 죽음 편의 몫이다.

## 트래픽의 경로: Service, Ingress, DNS

요청이 파드까지 도달하는 경로에 등장하는 개념들이다.

출발점은 파드에 IP가 있다는 사실이다. 모든 파드는 클러스터 내부에서 유효한 자기 IP를 받는다. 그런데 앞에서 본 것처럼 파드는 소모품이라 죽고 다시 태어나며, 그때마다 IP가 바뀐다. 바뀌는 IP를 클라이언트가 쫓아다닐 수는 없으니, 파드 무리 앞에 고정된 접속 지점이 필요하다. 그것이 **Service**다.

Service는 레이블 셀렉터로 파드 무리를 묶고, 그 앞에 **ClusterIP**라는 고정된 가상 IP를 세운다. 클라이언트는 ClusterIP(또는 그 DNS 이름)로 요청을 보내고, 요청은 뒤의 건강한 파드 중 하나로 분배된다. 여기서 "건강한"의 판정 기준이 앞 절의 readiness다. readiness를 통과한 파드만 Service의 대상 목록(**Endpoints**)에 오른다. readiness, Service, Endpoints는 한 세트로 움직인다.

ClusterIP는 이름 그대로 클러스터 안에서만 통하는 주소다. 어떤 기계나 프로세스에도 붙어 있지 않은, 네트워크 규칙으로만 존재하는 가상 주소라는 점이 흥미로운데(그래서 ping도 받지 않는다), 그 실체를 열어보는 것이 트래픽 편의 핵심 실측이다. Service에는 쓰임에 따라 타입이 몇 가지 있다는 것 정도만 표시해 둔다. 클러스터 내부 전용인 ClusterIP가 기본값이고, 노드의 포트를 여는 NodePort, 클라우드 로드밸런서를 붙이는 LoadBalancer가 있다.

외부의 HTTP 트래픽이 들어오는 문은 따로 있다. **Ingress**(또는 최근의 Gateway API)다. Ingress는 "이 도메인의 이 경로로 온 요청은 이 Service로"라는 L7 라우팅 규칙이고, 그 규칙을 실제로 집행하는 프록시(nginx, ALB 등)를 Ingress 컨트롤러라고 부른다. 브라우저에서 출발한 요청의 전형적인 경로는 CDN → 로드밸런서 → Ingress → Service → 파드로 요약된다(엄밀히는 많은 Ingress 컨트롤러가 Service를 거치지 않고 Endpoints의 파드 IP로 직접 보내는데, 그 우회는 트래픽 편에서 확인한다).

프론트엔드 서비스라면 이 경로를 타는 트래픽과 타지 않는 트래픽을 구분해 둘 필요가 있다. JS 번들, 이미지 같은 **정적 자산은 대개 CDN에서 끝난다.** 클러스터의 파드까지 오는 것은 HTML을 만들어야 하는 SSR 요청과 API 호출이다. 그래서 "트래픽이 늘었다"는 말도 두 갈래로 나뉜다. CDN 히트가 늘어난 것은 파드와 무관하고, 파드의 부하로 이어지는 것은 캐시를 통과해 들어오는 동적 요청뿐이다. 이 구분은 뒤의 오토스케일링에서 어떤 숫자를 봐야 하는지와 직결된다.

클러스터 안에서의 호출은 경로가 다르다. BFF가 내부 API 서버를 부를 때는 Ingress로 나갔다 돌아올 필요 없이, **클러스터 내부 DNS**(CoreDNS)가 만들어 주는 이름으로 Service에 직접 붙는다. `http://api-service`나 `http://api-service.namespace.svc.cluster.local` 같은 주소가 그것이다. 같은 클러스터 안에서는 이쪽이 더 짧고 빠른 길이라, "인그레스 타지 말고 내부 DNS로 붙으라"는 권고가 나오는 배경이 이것이다.

여기서 이름이 겹치는 용어 하나를 정리해 둘 필요가 있다. 방금 나온 `namespace`다. 쿠버네티스의 **Namespace**는 클러스터 안을 팀이나 환경 단위로 나누는 논리적 칸막이다(`production`, `staging` 같은). 반면 2편에서 만나는 리눅스 커널의 **namespace**는 컨테이너를 만드는 격리 장치로, 이름만 같고 완전히 다른 것이다. 쿠버네티스 오브젝트 문맥에서는 칸막이, 컨테이너 내부 문맥에서는 격리 장치로 읽으면 된다.

개발 장비에서 쓰는 `kubectl port-forward`는 이 경로 전체를 우회해 로컬 머신과 파드 사이에 터널을 뚫는 디버깅 도구다. 정확히는 API 서버를 경유하는 터널이라, 내 장비가 파드 네트워크에 직접 닿을 수 없어도 동작한다. 편리하지만 실제 트래픽 경로(Ingress, Service)를 하나도 통과하지 않으므로, port-forward로는 되는데 실서비스에서는 안 되는 상황은 그 사이 어딘가에 문제가 있다는 힌트가 된다.

## 스케일링: HPA와 노드 오토스케일러

트래픽에 따라 파드 개수를 조절하는 자동화가 **HPA**(Horizontal Pod Autoscaler)다. 이름에 내용이 다 들어 있다. Horizontal(파드 하나를 키우는 것이 아니라 개수를 늘린다), Pod(노드가 아니라 파드 단위), Autoscaler(메트릭을 보고 자동으로).

HPA는 메트릭(대표적으로 CPU 사용률)을 주기적으로 보고, 목표치를 유지하는 데 필요한 파드 수를 계산해 Deployment의 replicas를 조정한다. 여기서 앞 절의 개념들이 연결된다. "CPU 사용률 70%"라고 할 때 그 분모가 **requests**다. 예약량 대비 실사용 비율로 계산하므로, requests가 현실과 동떨어져 있으면 오토스케일링 판단도 함께 어긋난다. 그리고 파드 수가 늘어난다는 것은 새 파드가 스케줄러를 거쳐 노드에 배치되고, 이미지를 받고, readiness를 통과해야 트래픽을 받는다는 뜻이므로, 스케일 아웃은 자동이지만 즉시는 아니다. 그 지연이 실제로 어떤 구간들로 이루어져 있는지는 오토스케일링 편에서 측정한다.

파드가 늘다 보면 노드가 모자라는 순간이 온다. 노드를 늘리고 줄이는 것은 HPA가 아니라 별도의 **노드 오토스케일러**(Cluster Autoscaler, 혹은 AWS 계열에서 주로 쓰는 Karpenter)의 일이다. 파드 스케일링과 노드 스케일링이 서로 다른 층의 다른 도구라는 구분은 비용 이야기에서 특히 중요하다. 클라우드 청구서는 파드가 아니라 노드(EC2) 단위로 나오고, 노드 수를 결정하는 것은 결국 파드들의 requests 합이기 때문이다. requests가 곧 청구서라는 이 연결은 [사이징 글](/2026/08/nodejs-k8s-pod-sizing)의 비용 절에서 자세히 다뤘다.

설정과 비밀값도 이 부근에서 정리해 둔다. 파드가 어느 노드에서 몇 개로 떠도 같은 설정을 받아야 하므로, 설정은 이미지에 굽지 않고 **ConfigMap**(일반 설정)과 **Secret**(비밀값)이라는 오브젝트로 분리해 환경변수나 파일로 주입하는 것이 관례다. 앱의 환경변수가 어디서 오는지에 대한 답이 대개 이 둘이다.

여기에 프론트엔드 특유의 함정이 하나 있다. **빌드 타임 환경변수와 런타임 환경변수의 구분**이다. Next.js의 `NEXT_PUBLIC_*` 변수는 빌드 시점에 값이 번들 JavaScript 안에 문자열로 박힌다. 즉 이미지가 만들어질 때 이미 결정되는 값이라, 파드에 ConfigMap으로 다른 값을 주입해도 클라이언트 번들에는 반영되지 않는다. "dev 이미지를 prod에 올렸더니 API 주소가 dev를 가리킨다" 같은 사고의 뿌리가 이것이다. ConfigMap/Secret으로 바꿀 수 있는 것은 서버 코드가 `process.env`로 **실행 중에** 읽는 값뿐이고, 브라우저로 가는 값은 이미지 빌드 단계의 소관이다.

## 로그와 메트릭은 어디로 가는가

파드가 여러 노드에 흩어져 계속 교체되는 구조에서는, 로그를 어디서 보는가도 다시 정의된다.

쿠버네티스의 로그 모델은 단순하다. **앱은 파일이 아니라 stdout/stderr로 로그를 쓴다.** 컨테이너 런타임이 그 출력을 노드에 받아 두고, `kubectl logs`가 그것을 읽어온다. 파드가 여럿이면 노드마다 도는 수집 에이전트(Fluent Bit 등. 이런 노드당 하나짜리 배치가 다음 절에 나올 DaemonSet이다)가 모든 파드의 stdout을 모아 중앙 저장소(Elasticsearch, Loki, CloudWatch 등)로 보내는 것이 일반적인 구성이다. 앱 입장에서 기억할 것은 하나다. 로그 파일을 만들고 회전시키는 일은 앱의 몫이 아니고, stdout에 쓰면 나머지는 바깥이 처리한다.

조사할 때 유용한 옵션이 `kubectl logs --previous`다. `kubectl logs`는 현재 실행 중인 컨테이너의 로그를 보여주므로, 파드가 재시작된 직후에 보면 새 컨테이너의 깨끗한 로그만 보인다. 죽기 직전 무슨 일이 있었는지는 `--previous`로 **직전 컨테이너**의 로그를 열어야 보인다. OOMKilled처럼 앱 로그에 흔적을 남기지 않는 죽음이라도, 죽기 직전까지 무엇을 하고 있었는지는 여기서 단서를 얻는다.

메트릭 쪽 이름도 두 개만 적어 둔다. **Prometheus**는 파드와 노드의 메트릭(CPU, 메모리, 요청 수 등)을 주기적으로 수집해 쌓는 시계열 저장소이고, **Grafana**는 그것을 그래프로 보여주는 대시보드다. 팀에서 "그라파나 보세요"라는 말을 들었다면 이 조합이 떠 있다는 뜻이다. 다만 HPA가 기본으로 참조하는 CPU·메모리 메트릭은 이 계통이 아니라 **metrics-server**라는 별도의 경량 수집기에서 온다. Prometheus 계통이 HPA에 연결되는 것은 요청 수 같은 커스텀 메트릭으로 스케일할 때다.

## 그 밖에 마주치는 것들

주 흐름에는 들어가지 않지만 클러스터를 구경하다 보면 반드시 마주치는 이름들이 있다.

먼저 Deployment 외의 **워크로드 종류**다. SSR/BFF는 거의 항상 Deployment지만, `kubectl get pods -A`로 클러스터 전체를 보면 다른 형태의 파드들이 함께 보인다.

- **DaemonSet**: 모든 노드에 하나씩 띄우는 형태. 로그 수집기(Fluent Bit 등)나 노드 모니터링 에이전트가 대표적이다.
- **StatefulSet**: 파드마다 고정된 이름과 저장소를 주는 형태. 데이터베이스처럼 파드가 소모품이어서는 곤란한 워크로드용이고, 무상태 웹 서비스와는 반대편에 있다.
- **Job / CronJob**: 끝이 있는 작업을 한 번(Job) 또는 주기적으로(CronJob) 실행하는 형태. 배치성 작업이 여기에 실린다.

**PDB**(PodDisruptionBudget)는 "동시에 내려가도 되는 파드 수의 한도"를 정하는 오브젝트다. 노드 교체나 클러스터 정리처럼 관리 작업으로 파드를 비우는 **자발적 중단에만 적용되는** 안전장치라서, OOMKill이나 크래시 같은 사고까지 막아주지는 않는다. 파드의 삶과 죽음 편에서 다시 나온다.

**kubeconfig**와 **컨텍스트**(context)는 kubectl이 어느 클러스터의 어느 계정으로 요청을 보낼지 담는 설정이다. dev와 prod 클러스터를 오갈 때 `kubectl config use-context`로 전환하는데, 지금 어느 컨텍스트에 있는지 확인하지 않고 명령을 치는 것은 위험한 습관이 된다.

마지막으로 조사할 때 쓰는 kubectl 명령 다섯 개를 묶어 둔다. 이 글의 용어들을 실제로 눈으로 확인하는 최소 도구 상자다.

| 명령                                 | 용도                                                                       |
| ------------------------------------ | -------------------------------------------------------------------------- |
| `kubectl get pods`                   | 파드 목록과 상태(Running, Pending, CrashLoopBackOff...)                    |
| `kubectl describe pod <이름>`        | 파드 하나의 상세. 이벤트, 종료 코드, probe 실패 이력                       |
| `kubectl logs -f <이름>`             | 파드의 stdout 로그 스트림. `--previous`를 붙이면 죽은 직전 컨테이너의 로그 |
| `kubectl exec -it <이름> -- sh`      | 파드 안에서 셸 실행. 컨테이너 내부 확인용                                  |
| `kubectl rollout undo deploy/<이름>` | 직전 버전으로 롤백                                                         |

## 헷갈리기 쉬운 쌍들

경계가 흐려지기 쉬운 개념 쌍을 따로 모아 정리한다. 용어를 아는 것보다 이 구분들이 실무 대화의 정확도를 더 좌우한다고 생각한다.

**컨테이너와 파드.** 컨테이너는 격리된 프로세스이고, 파드는 그 컨테이너 한 개 이상을 묶어 IP와 자원 계약을 붙인 쿠버네티스의 관리 단위다. 대부분의 파드는 컨테이너 하나라서 일상 대화에서는 섞어 써도 통하지만, 사이드카(앱 옆에 붙는 보조 컨테이너)가 등장하는 순간부터는 구분이 필요하다. 로그 수집기 컨테이너와 앱 컨테이너가 한 파드에 있으면, 그 둘은 IP를 공유하고 함께 스케줄링되고 함께 죽는다.

**requests와 limits.** 예약과 상한. requests는 스케줄러의 계산에만 쓰이는 약속이고, limits는 실제 사용을 막는 벽이다. requests를 넘게 쓰는 것은 정상일 수 있지만(예약보다 많이 쓰는 중), limits를 넘으면 스로틀이나 OOMKill이다.

**liveness와 readiness.** 재시작 스위치와 트래픽 스위치. liveness 실패는 컨테이너를 죽였다 다시 살리고, readiness 실패는 트래픽만 뺀다. 회복 가능한 일시적 문제(다운스트림 지연 등)를 liveness에 걸면 회복 대신 재시작 연쇄를 얻는다.

**파드 스케일링과 노드 스케일링.** HPA는 파드 개수를, Cluster Autoscaler와 Karpenter는 노드(서버) 개수를 조정한다. 층이 다르고 도구가 다르다. 스케일 아웃을 했는데 파드가 Pending에 머문다면, 대개 파드는 늘렸는데 태울 노드가 없어 노드 오토스케일러를 기다리는 상태다.

**Namespace와 namespace.** 쿠버네티스의 Namespace는 클러스터를 나누는 논리적 칸막이, 리눅스의 namespace는 컨테이너를 만드는 커널 격리 장치다. 철자까지 같지만 서로 관련이 없다.

**이미지와 컨테이너.** 이미지는 파일 스냅샷(저장된 것), 컨테이너는 그것의 실행(실행 중인 것)이다. 같은 이미지로 컨테이너 백 개를 띄울 수 있다. 클래스와 인스턴스의 관계로 비유되곤 한다.

**컨테이너 재시작과 파드 재생성.** liveness 실패나 크래시로 인한 재시작은 **같은 파드 안에서** 컨테이너만 다시 뜨는 것이다. 파드 이름과 IP는 그대로고 `restartCount`가 올라간다. 반면 배포나 축출로 파드가 내려가면 그 파드는 끝이고, ReplicaSet이 **새 이름의 새 파드**를 만든다. "파드가 재시작됐다"는 말이 실제로는 이 둘 중 무엇이었는지에 따라 조사 방향(앱 크래시인가, 스케줄링·배포 이벤트인가)이 달라진다.

## 용어 인덱스

시리즈 전체에서 돌아와 참조할 수 있게, 이 글의 용어를 표 하나로 모아 둔다. "자세히" 열은 그 용어를 실측으로 깊게 다루는 편이다.

| 용어                                 | 한 줄 정의                                                      | 자세히          |
| ------------------------------------ | --------------------------------------------------------------- | --------------- |
| 이미지 / 레이어                      | 앱 실행에 필요한 파일 전체의 스냅샷 / 그것을 이루는 층          | 2편             |
| 레지스트리                           | 이미지 저장소. 노드가 여기서 이미지를 내려받는다                | 2편             |
| 컨테이너                             | 이미지를 실행한 것. 실체는 격리 장치를 두른 프로세스            | 2편             |
| cgroup / namespace(리눅스)           | 컨테이너 격리의 두 축. 쓰는 양 제한 / 보이는 세계 분리          | 2편             |
| 파드(Pod)                            | 컨테이너 묶음 + IP + 자원 계약. 실행과 관리의 최소 단위         | 2편             |
| 노드(Node)                           | 파드가 올라가는 서버. 청구서의 단위                             | 2편, 사이징 글  |
| 컨트롤 플레인                        | API 서버·etcd·스케줄러 등 결정을 내리는 쪽                      | 2편             |
| kubelet                              | 노드마다 상주하며 파드를 실제 프로세스로 만드는 에이전트        | 2편             |
| Deployment / ReplicaSet              | "N개 유지" 선언 / 그 개수를 지키는 중간 관리자                  | 2편             |
| 레이블 / 셀렉터                      | 오브젝트를 묶는 키-값 태그와 그것을 고르는 조건                 | 트래픽 편       |
| 롤링 업데이트                        | 새 파드를 늘리고 옛 파드를 줄이며 무중단으로 교체하는 배포 방식 | 삶과 죽음 편    |
| requests / limits                    | 자원의 예약량 / 상한. 스케줄링과 제재의 기준                    | 사이징 글       |
| OOMKilled (137)                      | 메모리 limit 초과로 커널이 프로세스를 죽인 것                   | 사이징 글       |
| CrashLoopBackOff                     | 반복 크래시로 재시작 간격이 지수적으로 벌어진 상태              | 삶과 죽음 편    |
| probe (startup/readiness/liveness)   | kubelet의 건강 검사 3종. 시작/트래픽 수신/생존 판정             | 삶과 죽음 편    |
| SIGTERM / grace period               | 종료 예고 신호와 유예 시간. 그레이스풀 셧다운의 재료            | 삶과 죽음 편    |
| Service / ClusterIP                  | 파드 무리 앞의 고정 접속 지점 / 그 가상 IP                      | 트래픽 편       |
| Endpoints                            | Service가 트래픽을 보낼 준비된 파드 목록                        | 트래픽 편       |
| Ingress                              | 외부 HTTP 트래픽의 L7 라우팅 규칙                               | 트래픽 편       |
| 클러스터 내부 DNS                    | Service 이름을 주소로 만들어 주는 CoreDNS                       | 트래픽 편       |
| Pending / ImagePullBackOff / Evicted | 배치 대기 / 이미지 풀 실패 / 노드 압박으로 축출된 파드 상태     | 이 글           |
| Namespace(쿠버네티스)                | 클러스터를 나누는 논리적 칸막이                                 | -               |
| ConfigMap / Secret                   | 설정과 비밀값을 이미지 밖에서 주입하는 통로                     | -               |
| HPA                                  | 메트릭 기반으로 파드 개수를 조절하는 오토스케일러               | 오토스케일링 편 |
| Cluster Autoscaler / Karpenter       | 노드 개수를 조절하는 오토스케일러                               | 사이징 글       |
| stdout 로그 / `logs --previous`      | 로그는 파일이 아니라 stdout으로 / 직전 컨테이너의 로그 조회     | 이 글           |
| Prometheus / Grafana                 | 메트릭 시계열 저장소 / 그것을 보는 대시보드                     | -               |
| DaemonSet / StatefulSet / CronJob    | 노드마다 하나 / 고정 신원 / 주기 실행 워크로드                  | -               |
| PDB                                  | 동시에 내려가도 되는 파드 수의 한도                             | 삶과 죽음 편    |
| Helm / Kustomize                     | 매니페스트를 템플릿 / 오버레이로 관리하는 도구                  | -               |
| apiVersion / kind / metadata / spec  | 모든 매니페스트가 공유하는 4개 골격 필드                        | 이 글           |
| kubectl explain                      | 매니페스트 필드의 설명과 타입을 터미널에서 조회                 | 이 글           |
| kubectl                              | API 서버에 요청을 보내는 공식 CLI                               | 전체            |

## 마치며

정리하면 골격은 이렇다. 컨트롤 플레인이 장부를 근거로 결정하고 kubelet이 집행한다는 **구조**, 파드는 소모품이고 관리 대상은 Deployment라는 선언이라는 **배포 모델**, requests는 예약이고 limits는 상한이며 probe가 트래픽과 재시작을 가른다는 **상태와 자원**, readiness를 통과한 파드만 Service의 Endpoints에 올라 트래픽을 받는다는 **경로**, 그리고 파드 수는 HPA가 노드 수는 노드 오토스케일러가 조정한다는 **스케일링**. 이 다섯 줄이 이 글의 요약이다.

다만 여기까지는 정의와 관계를 정리한 것이고, 각 개념이 실제로 그렇게 동작하는지는 아직 확인하지 않았다. 컨테이너가 정말로 프로세스 하나인지 PID로 확인하고, 이미지의 1.72GB가 어디서 왔는지 레이어를 뜯어보고, ClusterIP라는 가상 IP의 실체를 네트워크 규칙에서 찾아내는 것이 다음 편들의 일이다. [2편](/2026/08/k8s-for-frontend-2)은 컨테이너와 파드부터 직접 실행하고 측정한다.
