1. 쿠버네티스, 원하는 상태와 컨테이너 이미지
컨테이너를 실행한 뒤 남는 운영 문제에서 출발해, 선언형 관리와 배포 가능한 이미지의 조건을 연결한다.
쿠버네티스는 실행 이후의 운영을 맡는다
쿠버네티스의 핵심은 컨테이너를 한 번 띄우는 것이 아니라, 선언한 상태를 계속 유지하는 것이다. 예제 API가 지금 실행된다는 사실과, 내일 장애가 나도 필요한 개수로 서비스한다는 사실은 다르다.
예를 들어 demo-api를 세 개 실행했다고 해보자. 하나가 사라지면 누군가는 이를 발견하고 새로 띄워야 한다. 새 Pod의 IP가 달라지면 트래픽 연결 대상도 바꿔야 한다. 이미지 v2를 배포할 때는 새 버전이 준비되기 전에 기존 버전을 전부 내리지 않아야 한다. 쿠버네티스는 이런 일을 여러 컴포넌트가 나눠 처리하도록 만든 시스템이다.
이 시리즈는 원하는 상태를 선언하는 원리부터 앱 운영에 필요한 조건까지 여섯 파트로 다룬다. 예제 앱은 demo-api로 통일하고, 4편 실험실에서 명령과 상태 변화를 직접 연결한다.
| 파트 | 먼저 답할 질문 | 핵심 개념 |
|---|---|---|
| 1. 원하는 상태와 이미지 | 무엇을 쿠버네티스에 맡기는가? | 선언형 관리, 컨테이너 이미지 |
| 2. 클러스터 내부 동작 | 누가 Pod를 만들고 실행하는가? | Control Plane, Worker Node, 컨트롤러 |
| 3. kubectl과 Pod 진단 | 어떤 상태와 증거를 읽어야 하는가? | kubectl, Pod 상태, Events |
| 4. 복구와 배포 실험실 | 삭제, 확장, 배포, 롤백은 어떻게 이어지는가? | Reconciliation, 확장, 롤아웃, 롤백 |
| 5. Service와 요청 경로 | 사용자의 요청은 어떤 Pod에 도달하는가? | Service, EndpointSlice, DNS, Ingress |
| 6. 설정, 저장소와 운영 | 재시작과 배포 뒤에도 무엇이 남아야 하는가? | ConfigMap, Secret, 저장소, 프로브 |
1. 원하는 상태를 적으면 차이를 메운다
replicas: 3은 지금 세 번 실행하라는 절차가 아니라, 관리할 복제본이 세 개여야 한다는 목표다. 이 목표가 남아 있기 때문에 Pod를 하나 지워도 다시 채워진다.
원하는 상태: 3개
현재 상태: 3개 → 유지
Pod 하나 삭제
현재 상태: 2개 → 차이 1개 감지 → 새 Pod 생성
새 Pod가 준비됨 → Ready 3개
이 반복이 조정, Reconciliation이다. 여기서 새로 생기는 것은 삭제한 Pod와 이름과 식별자가 다른 객체다. 같은 컨테이너 안의 프로세스가 재시작되는 경우와 구분해야 한다.
spec에는 원하는 개수와 Pod template 등을 적는다. status에는 실제 복제본 수, Ready 수 같은 관찰 결과가 들어간다. 목표가 3개인데 Ready가 2개라면, 아직 준비 중이거나 어떤 단계에서 막혀 있다는 뜻이다. 목표를 기록했다고 실제 상태가 즉시 따라오는 것은 아니다.
2. 매니페스트 파일과 클러스터 객체는 다르다
매니페스트는 원하는 상태를 표현한 문서이고, 리소스 객체는 API를 통해 클러스터가 관리하는 대상이다. 로컬 YAML을 편집하는 것만으로 실행 중인 객체가 바뀌지는 않는다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
spec:
replicas: 3
selector:
matchLabels:
app: demo-api
template:
metadata:
labels:
app: demo-api
spec:
containers:
- name: app
image: demo-api:v1
이 YAML은 필드 구조를 읽는 예시다. 실제 배포에는 접근 가능한 이미지 저장소가 필요하다. demo-api:v1이라는 모의 이미지는 실험실에서만 준비되어 있다.
실제 환경에서 kubectl apply -f deployment.yaml을 실행하면 파일의 선언이 API 요청으로 전달된다. 반대로 로컬 파일을 지워도 이미 생성된 리소스는 남아 있다. 그래서 운영에서는 Git에 남긴 선언과 실제 클러스터 상태가 일치하는지 함께 관리한다.
kubectl scale처럼 개수를 즉시 바꾸는 명령도 결국 API 객체의 원하는 상태를 바꾼다. 명령형 인터페이스를 썼다고 컨트롤러의 조정 루프가 사라지는 것은 아니다.
3. 이미지와 컨테이너는 설계도와 실행 결과의 관계다
이미지는 실행 재료이고, 컨테이너는 그 재료를 사용해 실제로 실행한 격리된 프로세스 환경이다. 같은 이미지로 여러 컨테이너를 만들 수 있지만, 각 컨테이너가 실행 중에 기록한 임시 파일까지 공유하는 것은 아니다.
리눅스 컨테이너는 namespace로 보이는 자원을 나누고, cgroup으로 자원 사용을 관리한다. 파일 시스템은 이미지 레이어와 쓰기 가능한 영역을 조합한다. VM처럼 컨테이너마다 별도 게스트 커널을 부팅하는 구조와는 다르다. macOS의 Docker 환경에서는 리눅스 VM 안에서 리눅스 컨테이너가 실행된다는 점도 함께 생각하면 좋다.
이미지 한 벌
├── 컨테이너 A + A의 임시 쓰기 영역
└── 컨테이너 B + B의 임시 쓰기 영역
남겨야 하는 데이터 → 별도 저장소로 분리
컨테이너 이미지 작성과 노드에서의 실행도 역할이 다르다. Docker로 만든 호환 이미지를 containerd가 실행할 수 있다. 쿠버네티스가 앱의 소스 코드를 읽고 이미지를 빌드해 주는 것은 아니다.
4. 빌드 환경을 분리하고 이미지의 정체를 남긴다
배포 이미지는 필요한 실행 파일만 담고, 어떤 빌드 결과인지 다시 특정할 수 있어야 한다. 그래야 장애 시 이전에 검증한 버전으로 돌아갈 수 있다.
멀티 스테이지 빌드는 JDK, Gradle, 소스 코드가 필요한 빌드 단계와 JRE, JAR가 필요한 실행 단계를 나눈다. 의존성 관련 파일을 먼저 복사하고 자주 바뀌는 소스를 뒤에 두면, 빌드 캐시를 재사용할 기회가 생긴다. 실제 크기와 시간 감소는 프로젝트와 캐시 조건에 따라 달라진다.
| 결정 | 이유 | 확인할 것 |
|---|---|---|
| 빌드 단계와 실행 단계 분리 | 최종 이미지의 불필요한 파일을 줄인다 | 실행 단계에 필요한 파일이 빠지지 않았는가 |
| exec 형식 ENTRYPOINT 사용 | 주 프로세스가 종료 신호를 받기 쉽게 한다 | SIGTERM을 받고 앱이 정리하는가 |
| 적절한 실행 사용자 지정 | 필요한 권한으로 앱을 실행한다 | 파일 접근과 포트 바인딩이 정상인가 |
| 버전 또는 커밋을 태그에 기록 | 코드와 배포 결과를 추적한다 | 동일 태그를 다른 내용으로 덮어쓰지 않는가 |
| digest로 이미지 특정 | 태그가 이동해도 같은 대상을 찾는다 | 해당 digest의 이미지가 저장소에 유지되는가 |
태그는 바뀔 수 있는 이름이다. digest는 이미지 매니페스트 또는 인덱스의 내용을 특정한다. 베이스 이미지를 digest로 고정하면 베이스의 변동은 막을 수 있지만, 외부 의존성, 빌드 도구, 시간 정보까지 자동으로 동일해지는 것은 아니다.
5. 로컬 성공과 클러스터 성공 사이의 조건을 맞춘다
앱 코드가 같아도 CPU 구조, 메모리 한도, 설정과 이미지 접근 권한이 다르면 실행 결과가 달라질 수 있다. 배포 전에 이 차이를 줄여야 한다.
예를 들어 Apple Silicon에서 만든 arm64 이미지를 amd64 노드에 올린다면 대상 플랫폼을 지원하는 이미지가 필요하다. JVM 메모리도 힙만 보아서는 안 된다. 스레드 스택, 메타스페이스, 직접 버퍼 등 힙 밖의 사용량이 더해지므로, 특정 힙 비율을 모든 앱에 그대로 적용하기보다 한도를 건 상태에서 확인해야 한다.
내 노트북의 레지스트리 로그인과 노드의 이미지 다운로드 권한도 별개다. 개발자가 이미지를 받을 수 있어도 노드는 못 받을 수 있다. 이때 관찰할 증상이 ImagePullBackOff이며, 3편에서 Events를 읽는 방법으로 이어진다.
다음 파트
다음에는 이 선언을 받은 컴포넌트들이 어떤 객체를 만드는지 따라간다. 2편, 클러스터 내부 동작으로 이어진다.
COMMENTS
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions에 공개 저장되며, 작성 내용과 GitHub 프로필 정보가 다른 방문자에게 보일 수 있습니다.