소개
첫 번째 과정에서는 클러스터를 변경하지 않고 Kubernetes 클러스터를 탐색했습니다. kubectl이 API 서버로 요청을 보내며, Kubernetes 컨트롤러가 원하는 상태와 실제 상태를 지속적으로 비교한다는 점을 배웠습니다.
이제 처음으로 애플리케이션을 요청해 보겠습니다. Kubernetes 에 수행해야 할 모든 저수준 작업을 하나씩 지시하는 대신, 매니페스트라고 부르는 YAML 파일에 원하는 결과를 기술합니다. Kubernetes 는 이러한 객체 정의를 저장하고, 실제 상태가 정의된 내용과 일치하도록 작업합니다.
먼저 단일 Pod 를 정의하여 기본 매니페스트 구조를 쉽게 살펴봅니다. 그런 다음 두 개의 Pod 를 관리하는 Deployment 를 정의합니다. 단독으로 실행되는 Pod 와 Deployment 가 관리하는 Pod 를 비교하면, 애플리케이션에 더 높은 수준의 컨트롤러를 사용하는 이유를 이해할 수 있습니다.
이 실습에서는 의도적으로 워크로드 생성에 집중합니다. Pod, 레이블, Deployment 에 익숙해진 후 다음 단계에서 Service 를 통해 애플리케이션을 외부에 노출하는 방법을 배웁니다.
선언적 Kubernetes 객체 이해하기
환경 시작: 이 실습에서는 완전한 Kubernetes 클러스터가 시작됩니다. 컨트롤 플레인, 노드 및 네트워크 구성 요소를 설정하는 데 일반적으로 2~3분이 걸립니다. 환경 로딩이 완료될 때까지 잠시 기다린 후 작업을 시작하세요.
이 단계에서는 이전 실습에서 배운 원하는 상태의 개념을 Kubernetes 매니페스트와 연결하고, 첫 번째 애플리케이션 정의를 작성할 작업 공간을 준비합니다.
명령에서 원하는 상태로
Kubernetes 는 크게 두 가지 관리 방식을 지원합니다.
- 명령형 명령을 사용하면“
first-nginx라는 이름의 Pod 를 생성하라”와 같이 특정 작업을 직접 요청합니다. - 선언적 매니페스트를 사용하면 원하는 객체 구성을 파일에 저장한 뒤, 클러스터가 해당 상태와 일치하도록 요청합니다.
선언적 파일은 변경 전에 내용을 확인하고, 반복해서 적용하며, 차이를 검토하고, 버전 관리 시스템에 저장할 수 있다는 점에서 유용합니다. 이 과정에서는 선언적 방식을 중점적으로 사용합니다.
API 에서 반환되는 모든 Kubernetes 객체에는 다음과 같은 중요한 최상위 필드가 있습니다.
apiVersion은 Kubernetes API 그룹과 버전을 선택합니다.kind는Pod또는Deployment와 같은 객체 유형을 지정합니다.metadata는 이름과 레이블을 포함한 객체의 식별 정보를 제공합니다.spec은 해당 객체의 원하는 상태를 정의합니다.status는 관찰된 실제 상태를 보고합니다. 일반적으로 생성 후 Kubernetes 가 채우며, 매니페스트에 직접 작성하지 않습니다.
이 구조는 다음과 같은 순환 과정을 만듭니다.
manifest spec -> API server stores desired state -> controllers act -> object status reports actual state
매니페스트 디렉터리 준비
이 실습을 위해 준비된 디렉터리로 이동합니다.
cd /home/labex/project/k8s-manifests
현재 위치를 확인합니다. pwd는 작업 디렉터리 출력을 의미합니다.
pwd
/home/labex/project/k8s-manifests
앞으로 사용할 네 가지 매니페스트 필드를 기록하는 짧은 메모 파일을 만듭니다. printf 명령은 따옴표로 묶인 각 문자열을 별도의 줄에 출력하고, >는 그 출력을 파일로 리디렉션합니다. 파일이 이미 있으면 기존 내용을 덮어씁니다.
printf '%s\n' apiVersion kind metadata spec > manifest-fields.txt
파일을 출력하여 내용을 확인합니다.
cat manifest-fields.txt
apiVersion
kind
metadata
spec
이 메모 파일은 작은 학습 확인 지점입니다. 다음에 만들 두 매니페스트 모두에 이 네 가지 필드가 나타납니다.
Pod 매니페스트 작성 및 검증
이 단계에서는 하나의 Pod 를 정의하는 YAML 매니페스트를 작성하고, 클러스터에 전송하기 전에 구조를 검증합니다.
Pod 알아보기
Pod는 Kubernetes 에서 배포할 수 있는 가장 작은 객체입니다. 하나 이상의 밀접하게 연관된 컨테이너에 네트워크 식별자와 스토리지 환경을 함께 제공합니다. 입문 예제에서는 일반적으로 Pod 하나당 컨테이너 하나를 사용합니다.
이 실습의 Pod 는 작은 웹 서버인 NGINX 를 실행합니다. 사용할 이미지는 nginx:1.27-alpine으로 고정되어 있습니다. 버전을 고정하면 변경되는 latest 태그를 사용하는 것보다 실행 결과를 재현하기 쉽습니다. 학습자의 작업이 인터넷에서 이미지를 내려받는 과정에 의존하지 않도록 해당 이미지는 클러스터에 미리 캐시되어 있습니다.
YAML 파일 생성
매니페스트 디렉터리에 있는지 확인합니다.
cd /home/labex/project/k8s-manifests
here-document를 사용하여 파일을 생성합니다. 셸은 <<'EOF'와 닫는 EOF 사이의 모든 줄을 first-pod.yaml에 기록합니다. 첫 번째 EOF를 따옴표로 묶으면 YAML 내부의 특수 문자를 셸이 확장하지 않습니다.
cat <<'EOF' > first-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: first-nginx
namespace: default
labels:
app: first-nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
EOF
YAML 은 들여쓰기로 계층 구조를 표현합니다. 공백을 일관되게 사용해야 하며, 탭을 사용하면 YAML 이 유효하지 않을 수 있습니다. - name: nginx처럼 하이픈은 목록 항목의 시작을 나타냅니다.
객체를 위에서 아래로 살펴보면 다음과 같습니다.
apiVersion: v1은 Pod 에서 사용하는 코어 API 를 선택합니다.kind: Pod는 리소스 유형이 Pod 임을 선언합니다.metadata.name은 Pod 에 안정적인 이름인first-nginx를 부여합니다.metadata.namespace: default는 시스템 네임스페이스가 아닌 과정에서 사용하는 일반 애플리케이션 네임스페이스에 Pod 를 배치합니다.metadata.labels는 나중에 이 Pod 를 선택할 수 있도록app=first-nginx레이블을 추가합니다.spec.containers는 Pod 가 실행해야 하는 컨테이너 목록입니다.imagePullPolicy: IfNotPresent는 캐시된 이미지가 있으면 해당 이미지를 사용합니다.- 이름이
http인 포트는containerPort: 80과protocol: TCP를 사용합니다. 이는 컨테이너 내부에서 NGINX 가 수신 대기하는 위치를 문서화할 뿐이며, 클러스터 외부에 Pod 를 노출하지는 않습니다.
생성 전에 검증하기
클라이언트 측 드라이 런을 사용하여 Pod 를 생성하지 않고 파일을 파싱합니다. -f 옵션은 파일을 의미하며, --dry-run=client는 요청을 로컬에서만 처리하도록 합니다.
kubectl apply --dry-run=client -f first-pod.yaml
pod/first-nginx created (dry run)
여기서 dry run이라는 문구가 중요합니다. 구문은 유효하지만 클러스터에는 아무런 변경도 발생하지 않았습니다.
kubectl이 정규화된 객체를 YAML 형식으로 출력하도록 요청합니다.
출력 옵션 -o는 출력 형식을 의미합니다. yaml을 지정하면 kubectl이 한 줄짜리 결과만 출력하는 대신 파싱된 객체를 YAML 로 표시합니다.
kubectl apply --dry-run=client -f first-pod.yaml -o yaml
작성한 필드와 클라이언트가 추가한 기본값을 함께 확인할 수 있습니다. 실제로 적용하기 전에 들여쓰기, 필드 이름, 자료형 오류를 발견하는 데 유용합니다.
첫 번째 Pod 생성 및 확인
이 단계에서는 검증된 매니페스트를 적용하고, Kubernetes 가 Pod 를 원하는 상태로 전환하는 과정을 확인하며, 생성된 객체를 살펴봅니다.
매니페스트 적용
필요하다면 매니페스트 디렉터리로 이동합니다.
cd /home/labex/project/k8s-manifests
드라이 런 옵션 없이 파일을 적용합니다.
kubectl apply -f first-pod.yaml
pod/first-nginx created
kubectl apply는 객체를 API 서버로 전송합니다. API 서버는 원하는 Pod 사양을 저장하고, 스케줄러와 kubelet 은 협력하여 노드에서 Pod 를 실행합니다.
준비 상태 대기
Pod 생성은 비동기적으로 진행됩니다. 따라서 kubectl apply가 컨테이너가 준비되기 전에 반환될 수 있습니다. kubectl wait를 사용하여 Pod 의 Ready 조건을 기다립니다. 조건이 참이 되면 명령이 성공적으로 종료되고, 60 초가 지나면 실패합니다.
kubectl wait --for=condition=Ready pod/first-nginx --timeout=60s
pod/first-nginx condition met
이제 Pod 를 조회합니다. 각 실습을 독립적으로 사용할 수 있도록 이 실습에서는 -o wide를 반복해서 사용합니다. -o는 출력 형식을 선택하고, wide는 Pod IP 와 노드 이름 같은 필드를 추가합니다.
kubectl get pod first-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE
first-nginx 1/1 Running ... ... ... labex-v135
READY=1/1은 컨테이너 하나가 준비되었다는 뜻이며, STATUS=Running은 Pod 단계가 실행 중임을 나타냅니다. wide 형식에서는 Pod IP 와 할당된 노드도 확인할 수 있습니다. Pod IP 와 생성 후 경과 시간은 실행 시 생성되는 값이므로 달라질 수 있습니다.
레이블 및 소유 관계 확인
Pod 의 레이블을 출력합니다.
kubectl get pod first-nginx --show-labels
app=first-nginx를 찾습니다. 레이블은 객체와 함께 저장되며, 이후 Deployment 와 Service 가 Pod 를 선택할 때 중요한 역할을 합니다.
Kubernetes 에 이 Pod 를 관리하는 다른 객체가 있는지 확인합니다. -o jsonpath='...'는 객체 전체를 출력하는 대신 선택한 필드만 추출합니다. 이 표현식은 metadata.ownerReferences를 따라가며, {"\n"}은 마지막에 줄 바꿈을 추가하여 셸 프롬프트가 다음 줄에 표시되도록 합니다.
kubectl get pod first-nginx -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}{"\n"}'
Owner:
직접 생성한 단독 Pod이므로 소유자가 비어 있습니다. 이 Pod 가 삭제되어도 상위 컨트롤러가 이를 대신 생성해야 한다는 사실을 알지 못합니다. 다음 단계에서 관리되는 Pod 와 비교해 보겠습니다.
단계 확인
로컬의 원하는 상태 파일을 실행 중인 Kubernetes 객체로 변환했습니다. API 가 Pod 를 승인했고, 스케줄러가 노드를 할당했으며, kubelet 이 컨테이너를 준비 상태로 만들었습니다. Pod 는 존재하지만 수명 주기를 관리하는 컨트롤러는 없습니다.
Deployment 정의
이 단계에서는 NGINX Pod 를 두 개 유지하도록 Kubernetes 에 요청하는 Deployment 를 정의합니다.
Deployment 를 사용하는 이유
단독 Pod 는 학습에 유용하지만, 애플리케이션에는 일반적으로 컨트롤러가 필요합니다. Deployment는 원하는 복제본 수와 Pod 템플릿을 제공합니다. Deployment 는 ReplicaSet을 생성하고, ReplicaSet 은 요청된 수의 Pod 를 유지합니다.
소유 관계는 다음과 같습니다.
Deployment -> ReplicaSet -> Pods -> containers
관리 대상 Pod 가 사라지면 ReplicaSet 은 실제 복제본 수가 원하는 수보다 적다는 사실을 감지하고 대체 Pod 를 생성합니다. 이후 실습에서는 Deployment 를 사용하여 확장과 롤링 업데이트를 수행합니다.
Deployment 매니페스트 생성
매니페스트 디렉터리로 돌아갑니다.
cd /home/labex/project/k8s-manifests
here-document 를 사용하여 course-web-deployment.yaml을 생성합니다.
cat <<'EOF' > course-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: course-web
labels:
app: course-web
spec:
replicas: 2
selector:
matchLabels:
app: course-web
template:
metadata:
labels:
app: course-web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
EOF
Deployment 는 Deployment 의 안정적인 API 인 apps/v1을 사용합니다. spec에는 다음 세 가지 중요한 필드가 있습니다.
replicas: 2는 원하는 Pod 수가 2 개임을 의미합니다.selector.matchLabels는 Deployment 가 관리할 Pod 를 식별합니다.template은 각 Pod 를 생성할 때 사용하는 설계도입니다.
selector 와 template.metadata.labels는 모두 app: course-web을 사용합니다. 두 값은 반드시 일치해야 합니다. 일치하지 않으면 Deployment 가 자체 템플릿으로 생성한 Pod 를 식별할 수 없습니다.
Deployment 검증
클러스터를 변경하지 않고 매니페스트를 파싱합니다.
kubectl apply --dry-run=client -f course-web-deployment.yaml
deployment.apps/course-web created (dry run)
kubectl diff를 사용하여 매니페스트와 현재 클러스터 상태를 비교합니다. 0 이 아닌 diff 종료 코드는 객체가 변경될 예정이라는 뜻일 뿐입니다. || true는 예상된 차이를 오류로 처리하지 않도록 셸 명령을 계속 진행하게 합니다.
kubectl diff -f course-web-deployment.yaml || true
아직 course-web이 존재하지 않으므로 출력에는 추가될 전체 객체가 표시되고, 각 줄은 +로 시작합니다. apply와 달리 diff는 클러스터를 변경하지 않습니다.
관리되는 애플리케이션 배포 및 확인
이 단계에서는 Deployment 를 적용하고, 두 복제본이 준비될 때까지 기다린 다음, 동일한 레이블로 선택되는 관련 리소스를 확인합니다.
Deployment 적용 및 대기
먼저 매니페스트가 있는 디렉터리로 이동합니다. 그런 다음 저장해 둔 원하는 상태를 적용합니다. -f는 kubectl이 해당 파일을 읽도록 지정합니다.
cd /home/labex/project/k8s-manifests
kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web created
Deployment 롤아웃이 완료될 때까지 기다립니다. 롤아웃은 Deployment 의 Pod 를 원하는 템플릿과 복제본 수에 맞추는 과정입니다.
kubectl rollout status deployment/course-web --timeout=60s
deployment "course-web" successfully rolled out
Deployment 를 조회합니다.
kubectl get deployment course-web
NAME READY UP-TO-DATE AVAILABLE AGE
course-web 2/2 2 2 ...
READY=2/2는 원하는 복제본 두 개가 모두 준비되었다는 뜻입니다. UP-TO-DATE=2는 두 복제본 모두 현재 Pod 템플릿을 사용한다는 뜻이며, AVAILABLE=2는 두 복제본 모두 사용 가능한 상태라는 뜻입니다.
관련 리소스 확인
레이블 선택기 -l app=course-web을 사용하여 관련 리소스를 조회합니다.
kubectl get deployment,replicaset,pods -l app=course-web
출력에는 Deployment 하나, ReplicaSet 하나, Pod 두 개가 포함됩니다. ReplicaSet 과 Pod 에 자동으로 생성되는 접미사는 실행할 때마다 달라질 수 있습니다.
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/course-web 2/2 2 2 ...
NAME DESIRED CURRENT READY AGE
replicaset.apps/course-web-... 2 2 2 ...
NAME READY STATUS RESTARTS AGE
pod/course-web-...-... 1/1 Running ... ...
pod/course-web-...-... 1/1 Running ... ...
이 결과를 통해 Deployment 가 원하는 복제본 두 개를 준비된 Pod 두 개로 생성했음을 확인할 수 있습니다. 다음 단계에서는 이러한 리소스 간의 소유 관계를 추적합니다.
단계 확인
Deployment 매니페스트를 적용하고 원하는 상태가 될 때까지 기다렸습니다. Kubernetes 는 ReplicaSet 하나와 Pod 두 개를 생성했으며, 공유된 app=course-web 레이블을 사용하여 이들을 하나의 애플리케이션 그룹으로 조회할 수 있었습니다.
컨트롤러 소유 관계 추적
이 단계에서는 관리되는 Pod 에서 ReplicaSet 으로, 다시 Deployment 로 이어지는 Kubernetes 소유자 참조를 따라갑니다. 또한 매니페스트를 다시 적용하여 선언적 멱등성을 확인합니다.
Pod 소유자 확인
생성된 Pod 이름 하나를 셸 변수에 저장합니다. NAME=$(command) 구문은 명령 치환입니다. 셸이 명령을 실행한 뒤 그 출력을 NAME에 저장합니다. 여기서는 -l app=course-web로 일치하는 Pod 를 선택하고 JSONPath 로 첫 번째 Pod 의 자동 생성 이름을 추출합니다.
POD_NAME=$(kubectl get pods -l app=course-web -o jsonpath='{.items[0].metadata.name}')
선택된 Pod 를 확인할 수 있도록 이름을 출력합니다.
echo "$POD_NAME"
이제 직접 소유자를 확인합니다. "$POD_NAME"을 따옴표로 묶으면 저장된 이름이 하나의 안전한 명령 인수로 전달됩니다.
kubectl get pod "$POD_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: ReplicaSet/course-web-...
단독으로 생성한 first-nginx Pod 와 달리, Deployment 가 관리하는 Pod 의 소유자는 ReplicaSet 입니다. 그리고 ReplicaSet 자체는 Deployment 가 소유합니다.
ReplicaSet 의 소유자를 출력합니다. 첫 번째 명령은 같은 명령 치환 방식을 다시 사용하지만, 이번에는 ReplicaSet 이름을 RS_NAME에 저장합니다.
RS_NAME=$(kubectl get replicaset -l app=course-web -o jsonpath='{.items[0].metadata.name}')
kubectl get replicaset "$RS_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: Deployment/course-web
원하는 상태 다시 적용
같은 매니페스트를 다시 적용합니다.
kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web unchanged
unchanged는 선언적 관리의 중요한 특성을 보여 줍니다. 동일한 원하는 상태를 반복해서 적용해도 안전합니다. Kubernetes 는 원하는 구성과 실제 구성이 다를 때만 조치를 취합니다.
단계 확인
이제 단독 Pod 와 Deployment 가 관리하는 애플리케이션을 모두 갖게 되었습니다. 두 리소스 모두 컨테이너를 실행하지만, Deployment 는 두 개의 복제본을 유지하고 향후 확장과 롤링 업데이트를 수행할 수 있는 컨트롤러 계층을 추가합니다.
요약
읽기 전용 클러스터 탐색에서 선언적 애플리케이션 관리로 나아갔습니다. apiVersion, kind, metadata, spec의 역할을 익히고, 클라이언트 측 드라이 런으로 매니페스트를 검증했으며, 단독 Pod 를 생성하고 확인했습니다. 또한 Deployment 를 사용하여 관리되는 복제본 두 개를 배포했습니다.
무엇보다 Deployment 에서 ReplicaSet 을 거쳐 Pod 로 이어지는 컨트롤러 소유 관계를 직접 확인했습니다. 이러한 원하는 상태 기반의 구조는 이후 과정에서 애플리케이션을 진단하고, Service 를 통해 애플리케이션을 노출하며, 복제본을 확장하고, 롤링 업데이트를 수행하는 데 필요한 기반이 됩니다.


