Table of Contents

cgroup, image layer, request/limit, Docker·containerd·Podman, HPA를 한 번씩 만져보는 체험형 랩


1. 실습 목표

이 실습의 목표는 단순히 개념을 읽는 것이 아니라, 아래 내용을 직접 손으로 확인해보는 것이다.

  • cgroup이 실제로 어디에 적용되는지
  • cgroup v1 / v2 구조가 어떻게 다른지
  • 컨테이너 이미지 layer가 어떻게 쌓이는지
  • Docker / containerd / Podman의 차이를 어떻게 체감할 수 있는지
  • Kubernetes의 request / limit이 스케줄링과 런타임에서 어떻게 다르게 작동하는지
  • CPU limit은 왜 throttling이 되고, Memory limit은 왜 OOMKilled가 되는지
  • HPA가 왜 바로 반응하지 않는지
  • “이걸 알면 실무에서 무엇을 할 수 있는지”를 스스로 연결할 수 있는지

2. 실습 컨셉

이번 랩은 정석 구축보다 “개념 체험”에 초점을 둔다.
즉, 운영용 클러스터를 완벽하게 만드는 것이 목적이 아니라:

  • 빠르게
  • 반복 가능하게
  • 블로그에 정리할 수 있게
  • 각 개념을 한 번씩 명확히 체험할 수 있게

구성하는 것이 목적이다.


3. 권장 환경

3-1. 가장 추천하는 방식

VirtualBox(또는 VMware Workstation) + Rocky Linux 9.x VM 4대

VM 역할 권장 사양 IP 예시
lab-tools Docker / Podman / 빌드 / 부하발생 4 vCPU / 8GB / 60GB 192.168.56.10
k8s-cp Kubernetes Control Plane 2 vCPU / 4GB / 40GB 192.168.56.20
k8s-w1 Kubernetes Worker 1 2 vCPU / 4GB / 40GB 192.168.56.21
k8s-w2 Kubernetes Worker 2(선택) 2 vCPU / 4GB / 40GB 192.168.56.22

메모리가 부족하면 k8s-w2는 생략 가능하다.
다만 HPA, Pending, 스케줄링 체감은 3노드보다 4노드가 훨씬 좋다.


3-2. 왜 이렇게 나누는가?

  • lab-tools
    • Docker / Podman 비교
    • image layer 실습
    • 부하 발생 도구 실행
  • k8s 노드
    • containerd 확인
    • request/limit
    • OOM / throttling
    • HPA
    • Pod Pending

즉,

  • 개발/빌드/로컬 컨테이너 체험은 lab-tools
  • 오케스트레이션과 리소스 제어는 Kubernetes 노드

로 분리하면 개념이 훨씬 잘 잡힌다.


4. 실습 아키텍처

[ lab-tools ]
  ├─ Docker
  ├─ Podman
  ├─ Dockerfile / image layer 실습
  ├─ 부하 발생(hey / curl / busybox 등)
  └─ kubectl 접속

[ k8s-cp ]
  ├─ k3s server (containerd 내장)
  └─ kubectl / control plane

[ k8s-w1 ]
  └─ k3s agent

[ k8s-w2 ] (선택)
  └─ k3s agent

5. 이번 랩에서 왜 k3s를 권장하는가?

정석적으로 kubeadm이나 Kubespray를 써도 된다.
하지만 이번 글의 목적은 클러스터 구축 자체가 아니라 개념 체험이다.

그래서 이번 실습은:

  • 구축이 빠르고
  • containerd가 기본 포함되고
  • HPA 실습까지 빠르게 가기 쉬운

k3s를 권장한다.

이미 Kubespray 실습 환경이 있다면 그대로 사용해도 된다.
다만 처음부터 새로 만들 거라면 이번 체험 랩은 k3s가 더 효율적이다.


6. 사전 준비

6-1. Rocky Linux 공통 초기 설정

모든 VM에서 공통으로 수행

sudo dnf update -y
sudo dnf install epel-release -y
sudo dnf install -y vim git curl wget jq tar net-tools bind-utils tree htop
sudo systemctl disable --now firewalld
sudo setenforce 0
sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab

6-2. /etc/hosts 등록

모든 VM에 등록

cat <<EOF | sudo tee -a /etc/hosts
192.168.56.10 lab-tools
192.168.56.20 k8s-cp
192.168.56.21 k8s-w1
192.168.56.22 k8s-w2
EOF

7. 1단계 – lab-tools VM 준비


7-1. Podman 설치

Rocky 9 계열은 Podman이 비교적 자연스럽다.

sudo dnf install -y podman buildah skopeo
podman --version

확인:

podman info | grep -i rootless -A 3

7-2. Docker 설치

sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker version

7-3. 실습용 도구 설치

sudo dnf install -y stress-ng

부하 발생 도구 예시:

sudo dnf install -y golang
go install github.com/rakyll/hey@latest
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc
hey -h

hey 설치가 번거로우면 curl, wget, ab, busybox만 써도 된다.


8. 2단계 – Kubernetes(k3s) 구축


8-1. k8s-cp 에서 k3s server 설치

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --write-kubeconfig-mode 644 --node-ip 192.168.56.20" sh -

상태 확인:

sudo systemctl status k3s
kubectl get nodes

토큰 확인:

sudo cat /var/lib/rancher/k3s/server/node-token

8-2. k8s-w1, k8s-w2 에서 agent 설치

아래의 K3S_TOKEN 값은 위에서 확인한 토큰으로 교체

curl -sfL https://get.k3s.io | \
K3S_URL=https://192.168.56.20:6443 \
K3S_TOKEN="여기에_토큰값" \
INSTALL_K3S_EXEC="agent --node-ip 192.168.56.21" sh -

# 만약 아래와 같은 에러가 발생한다면:
# Job for k3s-agent.service failed because the control process exited with error code.

# 1. 서비스 상태 확인
sudo systemctl status k3s-agent.service

# 2. 상세 로그 확인
sudo journalctl -xeu k3s-agent.service

# 3. 자주 발생하는 원인 및 해결 방법

# (1) K3S_URL 접속 불가 (네트워크 문제)
# - control plane 노드(k8s-cp)와 통신 가능한지 확인
ping 192.168.56.20
curl -k https://192.168.56.20:6443

# (2) 토큰 불일치
# - k8s-cp에서 토큰 재확인
sudo cat /var/lib/rancher/k3s/server/node-token
# 예시 출력: K10d52b055e72d4e3eee85317a00483358f2e854328f398ed2976918972fe67c521::server:b5e12e7bbb1ab8965ac312fdf0e8a706
# 실제 환경에서는 위 명령어로 출력된 값을 그대로 복사해서 K3S_TOKEN에 넣으면 된다.

# (3) 이전 설치 잔여물 충돌
sudo /usr/local/bin/k3s-agent-uninstall.sh
sudo rm -rf /var/lib/rancher/k3s /etc/rancher/k3s

# (4) 방화벽 / SELinux 문제
sudo systemctl disable --now firewalld
sudo setenforce 0

# (5) node-ip 설정 오류
# - 실제 인터페이스 IP와 일치하는지 확인
ip a

# 문제 해결 후 다시 실행
curl -sfL https://get.k3s.io | \
K3S_URL=https://192.168.56.20:6443 \
K3S_TOKEN="여기에_토큰값" \
INSTALL_K3S_EXEC="agent --node-ip 192.168.56.21" sh -

w2는 IP만 192.168.56.22로 바꿔서 동일하게 수행


8-3. 클러스터 확인

k8s-cp 또는 lab-tools에서 kubeconfig를 가져와 확인 (예: scp root@192.168.56.20:/etc/rancher/k3s/k3s.yaml ~/.kube/config 로 복사한 뒤, 파일 내 server 주소를 127.0.0.1에서 192.168.56.20으로 수정하여 사용)

k8s-cp에서 kubeconfig 복사

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
kubectl get nodes -o wide

lab-tools에서도 kubectl 사용하고 싶다면

k8s-cp:/etc/rancher/k3s/k3s.yaml 파일을 복사한 뒤
server 주소를 127.0.0.1192.168.56.20 으로 수정


8-4. metrics 확인

k3s에서는 보통 metrics-server가 포함되어 있어 kubectl top이 가능하다.

확인:

kubectl top nodes
kubectl top pods -A

만약 동작하지 않으면 별도 metrics-server를 설치해야 한다.
이번 랩은 kubectl top이 되는 상태를 전제로 진행한다.


9. 실습 디렉토리 구조

이 디렉토리는 실습 파일을 직접 만들고 Docker/Podman 빌드 및 부하 테스트를 수행하는 lab-tools 서버에 생성하는 것을 권장한다.

container-lab/
├── 01-cgroup
├── 02-image-layer
├── 03-runtime
├── 04-k8s-request-limit
├── 05-hpa
├── manifests
└── screenshots

# 디렉토리 생성 명령어
mkdir -p container-lab/{01-cgroup,02-image-layer,03-runtime,04-k8s-request-limit,05-hpa,manifests,screenshots}

10. 실습 1 – cgroup 구조 확인 + 장애 시나리오 체험


10-1. 목표

  • 현재 호스트가 cgroup v1인지 v2인지 확인
  • 컨테이너에 limit을 걸면 어디에 반영되는지 확인
  • namespace와 달리 cgroup은 “얼마나 쓸 수 있는가”를 제어한다는 것을 체감
  • cgroup 제한으로 인해 실제 장애가 어떻게 발생하는지 체험
  • 장애 발생 시 어떤 지표와 로그를 통해 원인을 추적할 수 있는지 이해

10-2. 호스트에서 cgroup 버전 확인

lab-tools 또는 k8s 노드에서 실행

stat -fc %T /sys/fs/cgroup
mount | grep cgroup

예상:

  • cgroup2fs 가 나오면 v2

추가 확인:

cat /sys/fs/cgroup/cgroup.controllers

10-3. systemd 계층 확인

systemd-cgls

또는

systemd-cgtop

이걸 보면 프로세스가 cgroup tree 안에 어떻게 들어가는지 감이 잡힌다.


10-4. Docker 컨테이너에 CPU / Memory 제한 걸기

docker run -d --name cg-demo --cpus 0.5 --memory 256m alpine sleep 300
docker inspect -f '{{.State.Pid}}' cg-demo

PID를 확인한 뒤:

cat /proc/<PID>/cgroup

이 경로를 보면 해당 컨테이너 프로세스가 어떤 cgroup에 속했는지 볼 수 있다.


10-5. Podman으로도 확인

podman run -d --name podman-cg-demo --cpus 0.5 --memory 256m alpine sleep 300
podman inspect --format '{{.State.Pid}}' podman-cg-demo
cat /proc/<PID>/cgroup

여기서 Docker와 Podman의 cgroup path가 조금 다르게 보일 수 있다.


10-6. cgroup 기반 장애 시나리오 체험

시나리오 1 – CPU 제한으로 인한 성능 저하 (느려지는 장애)

docker run --rm --name cpu-issue --cpus 0.2 polinux/stress stress --cpu 2 --timeout 60s

다른 터미널에서:

docker stats

또는:

top

관찰 포인트:

  • CPU를 계속 사용하려 하지만 20% 수준에서 제한됨
  • 프로세스는 죽지 않지만 응답이 매우 느려짐
  • 실제 서비스라면 “장애처럼 느껴지는 성능 저하” 상황

원인 분석:

docker inspect cpu-issue | grep -i cpu

또는 cgroup 확인:

docker inspect -f '{{.State.Pid}}' <컨테이너이름>
cat /proc/<PID>/cgroup  # 이걸 보면 해당 컨테이너 프로세스가 어떤 cgroup 경로에 속해 있는지 확인할 수 있음

여기서 나온 결과 예시:

0::/system.slice/docker-430b6fc6b889ac5664faf8f6b5678d9d9b3257c4f4d9e3374865a14e674e3586.scope

이걸로 알 수 있는 핵심은 다음이야:

  1. cgroup v2 환경이라는 것
    • 0:: 형태는 cgroup v2 unified hierarchy를 의미함
    • v1이면 cpu, memory 등 컨트롤러별로 여러 줄이 나옴
  2. 이 프로세스가 속한 cgroup 경로
    • /system.slice/docker-<컨테이너ID>.scope
    • 즉, systemd가 관리하는 docker 컨테이너 단위 cgroup 안에 들어가 있음
  3. 컨테이너 ID와 매핑 가능
    • docker-430b6f...scope 부분이 실제 컨테이너 ID와 연결됨
    • 어떤 컨테이너인지 추적 가능
  4. 리소스 제한이 실제로 적용되는 위치
    • 이 경로 아래에서 CPU, memory 제한이 적용됨
    • 예:
      cd /sys/fs/cgroup/system.slice/docker-430b6f...scope
      cat cpu.stat
      cat memory.current
      

즉, 이 명령은 단순히 “어디에 속해있다”를 넘어서
👉 컨테이너 리소스 제한이 실제로 적용되는 커널 위치를 찾는 출발점이라고 보면 된다.


시나리오 2 – Memory 제한으로 인한 프로세스 종료 (OOM 장애)

docker run --rm --name mem-issue -m 128m polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1

관찰 포인트:

  • 일정 시간 후 컨테이너 종료
  • 단순 성능 저하가 아니라 “프로세스가 죽는 장애”

로그 확인:

docker logs mem-issue

커널 로그 확인:

dmesg | grep -i oom

또는:

journalctl -k | grep -i oom

원인 분석 포인트:

  • 메모리 limit 초과
  • 커널 OOM Killer 동작
  • cgroup memory 제한에 의해 강제 종료

시나리오 3 – cgroup 내부 상태 직접 확인

컨테이너 PID 확인:

docker rm -f cg-demo 2>/dev/null || true
docker run -d --name cg-demo alpine sleep 300
docker inspect -f '{{.State.Pid}}' cg-demo

cgroup 경로 확인:

cat /proc/<PID>/cgroup | grep docker

해당 경로에서:

cat /sys/fs/cgroup/<경로>/cpu.stat  # throttled_usec 값이 0에서 증가하면 CPU throttling이 발생한 것이며, 값이 커질수록 제한으로 인해 지연된 시간이 누적된 것
cat /sys/fs/cgroup/<경로>/memory.current  # 현재 컨테이너가 실제로 사용 중인 메모리 양 (bytes 단위)
cat /sys/fs/cgroup/<경로>/memory.max      # 해당 cgroup에 설정된 메모리 최대 제한값 (limit), "max"로 나오면 제한 없음

확인 포인트:

  • CPU throttling 발생 여부 (nr_throttled)
  • 현재 메모리 사용량
  • 설정된 메모리 limit

10-7. 실습 포인트

확인할 것:

  • docker / podman 각각 cgroup path는 어디인지
  • 루트 권한 기반과 rootless 경로 차이는 없는지
  • systemd와 cgroup 계층이 어떤 관계인지
  • CPU 제한 시 “죽지 않고 느려지는” 이유
  • Memory 제한 시 “바로 종료되는” 이유
  • 장애 발생 시 어떤 로그(dmesg, journalctl)를 보면 되는지

11. 실습 2 – CPU throttling과 Memory OOM 체험


11-1. 목표

  • CPU limit 초과 시 왜 “느려지는지”
  • Memory limit 초과 시 왜 “죽는지”
  • 두 limit이 같은 limit이 아니라는 점을 실감

11-2. Docker에서 CPU throttling 체험 (lab-tools VM에서 수행)

이 실습은 Docker가 설치된 lab-tools VM에서 수행한다.

docker run --rm --cpus 0.5 polinux/stress stress --cpu 2

다른 터미널에서:

docker stats

관찰 포인트:

  • CPU를 계속 쓰려고 하지만 제한값 이상 올라가지 않음
  • 프로세스가 바로 죽지는 않음

예상 결과:

  • docker stats 출력에서 해당 컨테이너의 CPU 사용률이 약 **50% 내외(0.5 CPU 기준)*\*로 제한되는 것을 확인할 수 있다.
  • 예를 들어 아래와 비슷한 형태로 보일 수 있다:
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT
abcd1234       hopeful_x   49.80%    5MiB / 256MiB
  • stress 프로세스는 내부적으로 CPU를 2개 쓰려고 시도하지만, --cpus 0.5 제한 때문에 실제로는 절반 수준으로만 실행된다.
  • 컨테이너는 종료되지 않고 계속 실행되며, CPU 사용이 제한된 상태로 유지된다.

11-3. Docker에서 Memory OOM 체험 (lab-tools VM에서 수행)

docker run --rm -m 128m polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1

실행 결과 예시:

stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 1s

관찰 포인트:

  • CPU처럼 그냥 “조금 느려짐”이 아니라
  • 메모리 한도를 넘기면 프로세스가 종료됨
  • signal 9는 SIGKILL이며, 이는 커널의 OOM Killer에 의해 강제 종료된 것
  • 실행 직후 바로 종료되는 이유는 컨테이너 메모리 제한(128MB)보다 훨씬 큰 메모리(256MB)를 즉시 할당하려 했기 때문

정리:

이 경우는 실행과 동시에 OOM이 발생한 것이 맞다.
컨테이너가 시작되자마자 메모리 할당 요청이 limit을 초과했고, 커널이 즉시 해당 프로세스를 종료 시킨 상황이다.


11-4. Podman으로도 동일 실습

podman run --rm --cpus 0.5 docker.io/polinux/stress stress --cpu 2
podman run --rm -m 128m docker.io/polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1

12. 실습 3 – cgroup v1 / v2 비교(선택)


12-1. 왜 선택 실습인가?

Rocky 9는 기본적으로 cgroup v2가 자연스럽다.
그래서 v1은 비교용 VM을 하나 더 복제해서 보는 것이 가장 안전하다.


12-2. 권장 방법

lab-tools를 복제해서 lab-tools-v1 같은 VM을 하나 더 만든 뒤
부팅 파라미터를 조정해 cgroup v1 형태를 비교한다.

주의: 배포판/커널 조합에 따라 동작 방식이 조금 다를 수 있다.
이 실습은 “정확히 운영 전환”이 아니라 “구조 비교”가 목적이다.

예시 개념:

sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=0"
sudo reboot

재부팅 후:

stat -fc %T /sys/fs/cgroup
mount | grep cgroup

12-3. 비교 포인트

확인 항목 v1에서 볼 것 v2에서 볼 것
파일시스템 controller별 분리 unified hierarchy
경로 cpu, memory, blkio 분리 /sys/fs/cgroup 아래 통합
체감 유연하지만 복잡 단순하고 일관적

13. 실습 4 – 컨테이너 이미지 layer 구조 체험 (lab-tools VM에서 진행)


13-1. 목표

  • 이미지가 단일 파일이 아니라 layer의 집합이라는 점 체험
  • Dockerfile 작성 방식이 이미지 크기와 캐시에 어떤 영향을 주는지 체험

13-2. 실습 파일 준비

~/container-lab/02-image-layer 에서 진행

cd ~/container-lab/02-image-layer

index.html 생성

방법 1 – echo 사용

echo "<h1>Hello Layer Lab</h1>" > index.html

방법 2 – 편집기로 생성

vi index.html

내용 입력:

<h1>Hello Layer Lab</h1>

Dockerfile.bad 생성

방법 1 – echo 사용

cat <<EOF > Dockerfile.bad
FROM rockylinux:9

RUN dnf install -y nginx
RUN dnf clean all

COPY index.html /usr/share/nginx/html/index.html

CMD ["nginx", "-g", "daemon off;"]
EOF

방법 2 – 편집기로 생성

vi Dockerfile.bad

내용 입력:

FROM rockylinux:9

RUN dnf install -y nginx
RUN dnf clean all

COPY index.html /usr/share/nginx/html/index.html

CMD ["nginx", "-g", "daemon off;"]

Dockerfile.good 생성

방법 1 – echo 사용

cat <<EOF > Dockerfile.good
FROM rockylinux:9

RUN dnf install -y nginx && \
    dnf clean all && \
    rm -rf /var/cache/dnf

COPY index.html /usr/share/nginx/html/index.html

CMD ["nginx", "-g", "daemon off;"]
EOF

방법 2 – 편집기로 생성

vi Dockerfile.good

내용 입력:

FROM rockylinux:9

RUN dnf install -y nginx && \
    dnf clean all && \
    rm -rf /var/cache/dnf

COPY index.html /usr/share/nginx/html/index.html

CMD ["nginx", "-g", "daemon off;"]

13-3. 이미지 빌드

docker build -t layer-demo:bad -f Dockerfile.bad .
docker build -t layer-demo:good -f Dockerfile.good .

13-4. history 확인

docker history layer-demo:bad
docker history layer-demo:good
docker images | grep layer-demo

Podman으로도 가능:

podman build -t layer-demo:good -f Dockerfile.good .
podman history layer-demo:good

13-5. 캐시 체험

index.html만 수정하고 다시 빌드

docker build -t layer-demo:good -f Dockerfile.good .

확인 포인트:

  • COPY index.html 이후 layer만 다시 빌드되는지
  • 앞단 layer는 캐시 재사용되는지

14. 실습 5 – Docker / Podman / containerd 차이 체험


14-1. 목표

  • 셋이 “비슷하게 보여도” 계층과 목적이 다르다는 것 체험
  • 개발용, 운영용, K8s 노드용의 구분 감 잡기

14-2. Docker 확인

systemctl status docker
ps -ef | grep dockerd
docker ps
docker images

포인트:

  • dockerd daemon이 존재함

14-3. Podman 확인

podman ps
podman images
podman info --format '{{.Host.Security.Rootless}}'

포인트:

  • daemonless(별도의 백그라운드 데몬 프로세스 없이, 명령을 실행할 때마다 필요한 컨테이너 작업을 직접 수행하는 구조) 구조
  • rootless(일반 사용자 권한으로 컨테이너를 실행하여 root 권한 없이도 격리된 환경을 구성할 수 있는 방식) 친화적

추가로 systemd unit 생성도 가능:

podman run -d --name podman-nginx -p 8080:80 docker.io/library/nginx
podman generate systemd --name podman-nginx --new

14-4. Kubernetes 노드에서 containerd 확인

k8s-cp 또는 k8s-w1 에서

crictl ps
crictl images
ctr -n k8s.io containers ls
ctr -n k8s.io images ls

포인트:

  • Kubernetes는 보통 containerd/CRI-O 같은 런타임과 직접 연결된다.
  • Docker CLI가 없더라도 Kubernetes 컨테이너는 containerd에서 실행될 수 있다.

15. 실습 6 – Kubernetes request / limit / Pending 체험 (k8s-cp 또는 kubectl 접근 가능한 VM에서 진행)


15-1. 목표

  • request는 스케줄링 기준
  • limit은 런타임 상한
  • request가 너무 크면 Pending
  • CPU limit은 throttling
  • Memory limit은 OOM

을 Kubernetes에서 직접 체험


15-2. 네임스페이스 생성

kubectl create namespace lab

15-3. CPU limit 체험용 Deployment

mkdir -p ~/container-lab/manifests

cd ~/container-lab

vi cpu-demo.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cpu-demo
  namespace: lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cpu-demo
  template:
    metadata:
      labels:
        app: cpu-demo
    spec:
      containers:
      - name: stress
        image: docker.io/polinux/stress
        command: ["stress"]
        args: ["--cpu", "2"]
        resources:
          requests:
            cpu: "100m"
            memory: "64Mi"
          limits:
            cpu: "200m"
            memory: "128Mi"

적용:

kubectl apply -f manifests/cpu-demo.yaml
kubectl get pods -n lab -o wide -w
kubectl top pod -n lab

관찰 포인트:

  • CPU를 더 쓰고 싶어도 limit 근처에서 제어됨
  • Pending이 아니라 정상 스케줄링됨

15-4. Memory OOM 체험용 Pod

vi manifests/mem-oom.yaml

apiVersion: v1
kind: Pod
metadata:
  name: mem-oom
  namespace: lab
spec:
  restartPolicy: Never
  containers:
  - name: stress
    image: docker.io/alpine:3.18
    command: ["/bin/sh", "-c"]
    args:
      - apk add --no-cache stress-ng && stress-ng --vm 1 --vm-bytes 256M --vm-hang 1
    resources:
      requests:
        cpu: "100m"
        memory: "64Mi"
      limits:
        cpu: "200m"
        memory: "128Mi"

적용:

kubectl apply -f manifests/mem-oom.yaml
kubectl get pod mem-oom -n lab -w
kubectl describe pod mem-oom -n lab

관찰 포인트:

  • OOMKilled
  • Last State
  • Reason

15-5. Pod Pending 체험

vi manifests/pending-demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pending-demo
  namespace: lab
spec:
  containers:
  - name: nginx
    image: docker.io/library/nginx
    resources:
      requests:
        cpu: "8"
        memory: "16Gi"
      limits:
        cpu: "8"
        memory: "16Gi"

적용:

kubectl apply -f manifests/pending-demo.yaml
kubectl get pod pending-demo -n lab -w
kubectl describe pod pending-demo -n lab

관찰 포인트:

  • 상태는 Pending
  • 이벤트에 Insufficient cpu, Insufficient memory 같은 메시지 확인 가능

15-6. 고급 확인 – cgroup까지 내려가 보기

worker 노드에서:

crictl ps | grep cpu-demo
crictl inspect <컨테이너ID> | jq '.info.pid'
cat /proc/<PID>/cgroup

해당 cgroup 경로에서 cpu.stat 확인:

cat /sys/fs/cgroup/<해당경로>/cpu.stat

여기서 nr_throttled 같은 값으로 throttling 흔적을 볼 수 있다.


16. 실습 7 – HPA 체험


16-1. 목표

  • HPA가 어떻게 scale-out 하는지 확인
  • 왜 즉시 반응하지 않는지 확인
  • stateless 구조에서 scale-out이 잘 맞는 이유 체감

16-2. 샘플 애플리케이션 배포

vi manifests/php-apache.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
  namespace: lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: php-apache
  template:
    metadata:
      labels:
        app: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
  namespace: lab
spec:
  selector:
    app: php-apache
  ports:
  - port: 80
    targetPort: 80

적용:

kubectl apply -f manifests/php-apache.yaml
kubectl get all -n lab

16-3. HPA 생성

vi manifests/php-apache-hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
  namespace: lab
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

적용:

kubectl apply -f manifests/php-apache-hpa.yaml
kubectl get hpa -n lab

16-4. 부하 발생

방법 1 – kubectl run으로 간단 부하 생성

kubectl run -it --rm load-generator -n lab --image=busybox:1.28 -- /bin/sh

쉘 안에서:

while true; do wget -q -O- http://php-apache; done

방법 2 – lab-tools에서 hey 사용

Service를 NodePort나 port-forward로 노출한 뒤:

kubectl port-forward svc/php-apache 8080:80 -n lab
hey -z 2m -c 20 http://127.0.0.1:8080/

16-5. HPA 관찰

다른 터미널에서:

kubectl get hpa -n lab -w
kubectl get deploy,pod -n lab -w
kubectl top pod -n lab
kubectl top pod -n lab --containers

레플리카 수를 확인하려면:

kubectl get deploy php-apache -n lab
kubectl get deploy php-apache -n lab -o wide
kubectl get deploy php-apache -n lab -o jsonpath='{.status.replicas}'
kubectl get deploy php-apache -n lab -o jsonpath='{.status.availableReplicas}'

또는 HPA 기준으로 확인하려면:

kubectl get hpa php-apache -n lab

출력에서 TARGETS, MINPODS, MAXPODS, REPLICAS 값을 보면 현재 스케일 상태를 확인할 수 있다.

예를 들어 아래와 같은 결과가 나온다면:

kubectl get deploy php-apache -n lab -o wide
kubectl get hpa php-apache -n lab
  • Deployment: 5/5 (READY / AVAILABLE)
  • HPA: cpu: 66%/50%, REPLICAS: 5

이 의미는 다음과 같다:

  • 현재 평균 CPU 사용률이 목표치(50%)보다 높은 상태 (66%)
  • HPA가 이를 감지하고 scale-out을 수행
  • 최대 replica 수(maxReplicas=5)까지 이미 확장된 상태

즉, 이런 출력이 보이면 HPA가 정상적으로 동작 중이며, 이미 스케일 아웃이 완료된 상태라고 볼 수 있다.

추가로 HPA와 기존 파드를 함께 모니터링하려면:

kubectl describe hpa php-apache -n lab
kubectl get hpa php-apache -n lab -o yaml

관찰 포인트:

  • CPU 사용률 증가 (각 Pod별로 확인)
  • HPA가 target 초과를 인식하는 시점
  • replica 수 증가 과정
  • 기존 Pod와 새로 생성된 Pod의 리소스 사용량 비교
  • 새 Pod가 뜨고 Ready 되기까지 시간 필요

부하가 줄어들면 어떻게 되는가?

결론부터 말하면 부하가 줄어들면 HPA는 scale-in(레플리카 감소)도 수행한다.

하지만 중요한 포인트가 있다:

  • 즉시 줄어들지 않는다
  • 일정 시간 동안 안정적으로 낮은 상태가 유지되어야 한다

예를 들어:

  • 부하 테스트를 멈추고 CPU 사용률이 20% 이하로 떨어졌다고 해도
  • 바로 5 → 1로 줄어들지 않는다

이유는 다음과 같다:

  1. stabilization window (안정화 시간)
    • 급격한 scale-in으로 인한 서비스 불안정을 방지하기 위해 일정 시간 대기
  2. metrics 수집 주기
    • metrics-server → HPA 반영까지 시간 지연 존재
  3. scale-down 정책
    • 한 번에 줄이는 개수에도 제한이 있을 수 있음

실제로 확인하려면:

kubectl get hpa php-apache -n lab -w
kubectl get deploy php-apache -n lab -w

부하를 멈춘 뒤 관찰하면:

  • CPU 사용률이 점점 낮아짐
  • 일정 시간 후 replica 수가 서서히 감소

예시 흐름:

5 replicas → (부하 감소) → 5 유지 → 4 → 3 → 2 → 1

즉, HPA는 단순히 “늘리는 기능”이 아니라
부하에 따라 자동으로 늘리고 줄이는 양방향(auto scale) 기능이다.


실무에서 중요한 포인트

  • scale-out보다 scale-in이 더 위험할 수 있다
  • 너무 빨리 줄이면 트래픽이 다시 올라올 때 대응이 늦어질 수 있다
  • 그래서 기본적으로 Kubernetes는 scale-in을 더 보수적으로 수행한다

정리하면:

  • 부하 증가 → 비교적 빠르게 scale-out
  • 부하 감소 → 천천히 scale-in

이 차이를 이해하는 것이 HPA를 제대로 쓰는 핵심이다.


16-6. 왜 HPA가 즉시 반응하지 않는지 체감하는 포인트

확인할 것:

  • 메트릭 수집 시간
  • HPA sync 주기
  • 새 Pod 생성 시간
  • 이미지 pull 시간
  • readiness 통과 시간

즉, “부하가 오르면 바로 scale-out”이 아니라
부하 감지 → 판단 → 생성 → Ready 의 시간이 필요하다.


17. 선택 실습 – Podman rootless 체험


17-1. 목표

  • rootless가 무엇인지 체험
  • Docker와 Podman의 운영 철학 차이를 느껴보기

17-2. 일반 사용자로 실행

podman run -d --name rootless-nginx -p 8081:80 docker.io/library/nginx
podman ps
curl http://127.0.0.1:8081

17-3. 확인 포인트

  • daemon 없이 동작
  • 사용자 단위로 컨테이너 실행
  • podman generate systemd 와 연결 가능