[서론] 컨테이너가 죽지 않아!

 운영 중인 Kubernetes 노드에서 컨테이너 런타임(Containerd)을 업데이트하거나 재시작해야 할 때가 있습니다. 상식적으로 생각해 보면 부모 프로세스인 데몬이 종료되면, 그 자식 프로세스인 컨테이너들도 함께 종료되거나 고아(Orphan)가 되어 문제가 생겨야 할 것 같습니다. 하지만 실제로 그런일은 일어나지 않습니다. 서비스 중인 컨테이너(Pod)들은 아무런 영향 없이 계속 동작합니다. 어떻게 이것이 가능할까요? 도대체 리눅스 커널에서는 어떤 일이 벌어지고 있는 걸까요? 이 '죽지 않는 컨테이너'의 비밀을 풀기 위해 프로세스 트리의 족보를 파해져 보았습니다.

장비를 정지합니다. 정지하겠습니다. 어? 안 되잖아?

 이번 포스팅에는 잘 작동하고 있는 Kubernetes 클러스터의 Control plane 노드(Locky Linux 10, root 사용자 환경)에서 진행됩니다.

멀쩡히 작동하는 Kubernetes 클러스터

 위와 같이 잘 작동하고 있는 Kubernetes 클러스터가 있습니다. API 서버도 잘 작동중이고, Containerd 프로세스도 문제없이 작동 중입니다. 이제 Containerd 프로세스를 정지시켜 보겠습니다. 그리고 Kube-apiserver가 잘 응답하는지 확인해 보겠습니다.

# Conatinerd 프로세스 중지
systemctl stop containerd

# kube-apiserver Port 확인 및 kubectl 응답 확인
ss -nltp | grep 6443

kubectl get pods -n kube-system | grep api

Containerd 서비스는 중지 되었지만, kube-apiserver는 정상 작동 중

 분명 Containerd 서비스는 중지 되었지만, 여전히 6443 포트는 LISTEN 상태이고, kubectl을 통해서도 문제없이 Kubernetes 클러스터와 통신이 가능합니다. 정말 컨테이너가 죽지 않습니다. 그런데 Containerd 프로세스의 상태를 자세히 살펴보면 Cgroup 필드에 특이한 점을 발견할 수 있습니다.

  • 앞서 존재했던 1088523 containerd 프로세스(/usr/local/bin/containerd)가 보이지 않음
  • 그러나 여전히 containerd-shim-runc-v2라는 프로세스들은 여러 개가 보임

 일반적으로 프로세스가 중지되고 나면 Cgroup 필드 자체가 표시되지 않는데 Containerd는 Cgroup 필드가 살아 있습니다. 도대체 왜 이런 현상이 일어나고 있을까요?

중지된 kubelet 프로세스는 Cgroup 필드도 사라짐

[관찰] 족보가 이상한데요...? feat. pstree

 Containerd 프로세스가 중지 되었는데도 컨테이너(Pod)가 문제없이 작동한다는 것은, 아무래도 containerd와 컨테이너(Pod)가 서로 부모-자식 관계가 아닌것일지도 모르겠다는 추측을 하게 합니다. (리눅스 프로세스의 부모-자식 관계에 대한 상세한 내용은 이전 포스팅을 참고해 주세요.) 다음 명령어를 통해서 프로세스 트리를 확인해 봅시다.

 

 

Linux 프로세스 관리 - 좀비 프로세스에 관하여 [1편]

리눅스에서 지금 당장 아래 명령어를 입력해보자!$ ps aux$ pstree 아마도 리눅스를 조금 다루어 보았다면 'ps'는 상당히 익숙한 명령어일 것입니다. 잘 아시겠지만, 리눅스에서 명령어 'ps'는 현재 프

tech-recipe.tistory.com

# Conatinerd process 재시작
systemctl start containerd

# 프로세스 트리 확인
pstree

containerd와 형제 관계로 보이는 containerd-shim 프로세스들

 pstree를 통해 살펴보면 위에서 확인할 수 있듯, containerd와 containerd-shim은 부모-자식 관계가 아닌 형제 관계인것 처럼 보입니다. 예상했던 예상밖의(?) 결과입니다. 우선 여기서 확실하게 확인되는 것은 컨테이너(Pod)와 containerd 프로세스는 확실히 부모-자식 관계가 아니라는 점이고, 이 때문에 containerd 프로세스가 중지되거나 재시작되어도 컨테이너(Pod)는 아무 문제 없이 잘 작동할 수 있었던 것입니다. 그런데 containerd-shim은 도대체 무엇일까요? 왜 이 프로세스들이 컨테이너(Pod)들의 부모 프로세스의 위치를 가지고 있을까요?

[실험] 왜 컨테이너들이 containerd-shim의 자식이 되어 있을까?

커널에서는 무슨 일이 일어났던 것일까?

 strace 명령어를 사용하여 커널 레벨에서의 시스템 콜(System Call)을 추적, 컨테이너(Pod)가 생성될 때 실제로 어떤 일들이 벌어지고 있는지 확인해 봅시다. 현재 우리가 살펴보고 있는 컨트롤 플레인 노드에서 Pod를 고정적으로 생성하기 위해 Static Pod를 사용할 예정입니다. 다음과 같은 명령어를 통해 Static pod를 생성하고, 시스템 콜을 추적해 봅시다.

# containerd 프로세스의 시스템 콜을 추적
sudo strace -f -e trace=clone,execve,exit_group,prctl -p $(systemctl show containerd -p MainPID --value) 2>&1 | grep -v "SIGURG"
# 새로운 터미널 창을 열고 control plane에 접근
# nginx.yaml 파일 생성
kubectl run nginx --image=nginx --dry-run=client -o yaml > nginx.yaml

# static pod 생성
mv nginx.yaml /etc/kubernetes/manifests/

# 프로세스 트리 확인
pstree

strace와 pstree 명령어를 통한 프로세스 상태 추적

 

kubernetes-study/process-tree-and-cgruop at main · garlicKim21/kubernetes-study

Kubernetes Study의 기록입니다. Contribute to garlicKim21/kubernetes-study development by creating an account on GitHub.

github.com

containerd-shim(PID 1224703)과 pause 컨테이너(PID 1224727)

 strace와 pstree 명령어를 통해 확인할 수 있는 로그 및 프로세스 트리는 위 Github 레포 링크에서 자세하게 살펴볼 수 있습니다. 우선 pstree.txt 파일을 살펴보면 containerd의 PID는 1108358이고 nginx static pod의 부모인 containerd-shim의 PID는 1224703이며 pause 컨테이너의 PID는 1224727, nginx 컨테이너의 PID는 1224754 임을 확인할 수 있습니다. 이 프로세스들이 언제 어떻게 탄생했는지는 strace.logs 파일을 통해 확인할 수 있습니다.

# nginx Pod의 부모 프로세스 PID 1224703 생성
165 [pid 1224700] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224703 attached
166  <unfinished ...>

# PID 1224703 프로세스의 containerd-shim 실행
167 [pid 1224703] execve("/usr/local/bin/containerd-shim-runc-v2", ["/usr/local/bin/containerd-shim-r"..., "-namespace", "k8s.i    o", "-id", "c8b394369634a6c6755d4e9094907a92"..., "-address", "/run/containerd/containerd.sock"], 0xc000170900 /* 14 vars *    / <unfinished ...>
168 [pid 1224700] <... clone resumed>, parent_tid=[12]) = 1224703

# containerd-shim의 부모 프로세스였던 PID 1224700 프로세스 그룹 종료
169 [pid 1224703] <... execve resumed>)     = 0
170 [pid 1224700] exit_group(0)             = ?
171 [pid 1224701] +++ exited with 0 +++
172 [pid 1224696] +++ exited with 0 +++
173 [pid 1224699] +++ exited with 0 +++
174 [pid 1224695] +++ exited with 0 +++
175 [pid 1224700] +++ exited with 0 +++
176 [pid 1224698] +++ exited with 0 +++
177 [pid 1224697] +++ exited with 0 +++
178 [pid 1224694] +++ exited with 0 +++

 먼저 containerd-shim(PID 1142827)이 생겨나는 과정을 뜯어봅시다. 위 내용은 containerd-shim이 독립하여 systemd(PID 1)에 입양되어 containerd와 형제 프로세스가 되는 부분의 로그입니다. 중간 과정이 약간 생략되었지만, 큰 틀은 아래 그림과 같습니다.

containerd-shim이 systemd에 입양되는 과정

 contianerd로부터 생성된 프로세스 PID 1224700PID 1224703을 생성한 후 종료되면서, containerd-shim(PID 1224703)고아(Orphan) 상태가 되고 systemd(PID 1)이 이를 입양, containerd와 형제 프로세스가 되는 것입니다.

그럼 컨테이너(Pod)는 언제 생겼을까?

 다시 pstree를 상세하게 뜯어보겠습니다. containerd-shim(PID 1224703)의 자식 프로세스로 nginx(PID 1224754)pause(PID 1224727)이 보입니다. 그런데 그 어디를 살펴보아도 Pod는 보이지 않습니다. 그 이유는 Pod는 Kubernetes가 만든 논리적인 그룹일 뿐, 리눅스 커널에는 존재하지 않는 객체이기 때문입니다. 여기서 Kubernetes의 Infra 컨테이너(또는 sandbox 컨테이너)라는 개념이 등장합니다. Infra 컨테이너가 바로 pause(PID 1224727)이고 이것이 Pod의 구현체입니다. Kubernetes는 Pod라는 논리적 그룹을 구현하기 위해 리눅스 네임스페이스 기능을 사용하는데, 이 네임스페이스는 최소한 하나의 프로세스가 그 안에 살아 있어야만 유지됩니다. 이를 위해 아무 일도 하지 않지만 네임스페이스를 유지할 수 있는 프로세스인 pause를 생성하여 sandbox 환경(Pod)을 구현하게 됩니다. 그럼 이 pause 컨테이너가 어떤 과정을 통해 생성되고 있는지 strace 로그를 통해 추적해 보겠습니다.

# pause(PID 1224727) 프로세스 추적

# pause(PID 1224727)의 부모는 PID 1224726 확인
# PID 1224726의 부모는 PID 1224723 확인
# pause(PID 1224727) 프로세스 생성 후 부모 프로세스(PID 1224726), 조부모 프로세스(PID 1224723) 모두 종료
235 [pid 1224723] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224726 attached
236 ) = 1224726
237 [pid 1224726] prctl(PR_SET_NAME, "runc:[1:CHILD]") = 0
238 [pid 1224726] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224727 attached
239 ) = 1224727
240 [pid 1224727] prctl(PR_SET_NAME, "runc:[2:INIT]") = 0
241 [pid 1224726] exit_group(0)             = ?
242 [pid 1224726] +++ exited with 0 +++
243 [pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224726, si_uid=0, si_status=0, si_utime=0, si_stim    e=0} ---
244 [pid 1224723] exit_group(0)             = ?
245 [pid 1224723] +++ exited with 0 +++

# PID 1224723 부모는 PID 1224714
226 [pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224723 attached

# PID 1224714의 부모는 PID 1224711
# PID 1224711은 정확히는 프로세스가 아니라 스레드
207 [pid 1224711] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224714 attached

# 스레드 PID 1224711의 부모는 스레드 PID 1224709
197 [pid 1224709] clone(child_stack=0xc00022c000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
strace: Process 1224711 attached

# 스레드 PID 1224709의 부모는 스레드 PID 1224706
# 191 라인의 로그 뒷 부분을 분리해서 보아야 함
190 [pid 1224706] clone(child_stack=0xc00021a000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
<unfinished ...>
191 strace: Process 1224709 attached
193 [pid 1224706] <... clone resumed>, tls=0xc00005b198) = 1224709

# 스레드 PID 1224706의 부모는 PID 1224703(containerd-shim 프로세스)
184 [pid 1224703] clone(child_stack=0xc000070000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
strace: Process 1224706 attached
185 , tls=0xc000058e98) = 1224706

# 주요 포인트! - containerd-shim(PID 1224703)으로 부터 생성되는 스레드 PID 1224710의 생성
182 [pid 1224703] clone(child_stack=0xc000074000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLSstrace: Process 1224705 attached
183 , tls=0xc000058798) = 1224705

# (PR_SET_CHILD_SUBREAPER, 1) 속성을 추가하는 스레드 PID 1224710
191 [pid 1224705] clone(child_stack=0xc000216000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
192  <unfinished ...>
195 [pid 1224705] <... clone resumed>, tls=0xc000100798) = 1224710
196 [pid 1224710] prctl(PR_SET_CHILD_SUBREAPER, 1) = 0

 

[다이어그램] Pause 컨테이너 프로세스 생성 단계

 

 Pause 컨테이너 하나를 생성하는데도 이렇게 많고 복잡한 과정을 거칩니다.(심지어 이것도 많은 단계들이 생략된 것입니다.)이 복잡한 과정의 주요 내용을 요약해 보면 다음과 같습니다.

단계 내용 비고
1 단계 - containerd-shim 프로세스에서 시작하여 여러 프로세스와 스레드를 거치면서 Pause 컨테이너 프로세스를 생성
- Pause 컨테이너가 생성되고 나면 containerd-shim과 Pause 프로세스 사이의 중간 단계 프로세스와 스레드는 모두 종료하여 Pause 컨테이너 프로세스를 고아(Orphan) 상태로 만들어 입양 보낼 준비를 진행
 
2 단계 - containerd-shim의 또 다른 스레드에서 PR_SET_CHILD_SUBREAPER를 선언
- 이로인해 containerd-shim도 Subreaper 속성을 부여 받음
1단계와 거의 동시에 일어남
3 단계 - 고아가 된 Pause 컨테이너가 가장 가까운 reaper를 찾음
- Subreaper 속성이 부여된 containerd-shim 프로세스가 Pause 컨테이너 프로세스를 입양
 

하청의 하청... 저수준 컨테이너 런타임 runc의 역할

 Kubernetes에서 Pod 생성 요청이 들어오면 노드의 kubelet 데몬이 container runtime(containerd)에게 컨테이너 생성을 명령합니다. 그러면 그 명령을 받은 containerd는 containerd-shim을 통해 컨테이너 프로세스를 구동시킵니다. 그런데, 사실 containerd-shim 역시도 직접 컨테이너를 만드는 것이 아니라 runc라는 저수준 컨테이너 런타임을 통해 실제 컨테이너를 생성합니다. 정말 하청에 하청을 몇 번이나 내리는지...

 

 다시 한번 strace.logs 파일을 살펴봅시다. Pause 컨테이너와 Nginx 컨테이너가 생성되는 시점을 유심히 살펴보면서 runc가 어떤 역할을 하는지 알아봅시다. 이번에는 PID 1224714를 중심으로 살펴보겠습니다. 그 이유는 PID 1224714가 runc를 실행하기 때문입니다.

# containerd-shim의 스레드인 PID 1224711이 PID 1224714 프로세스를 생성
[pid 1224711] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224714 attached
[pid 1224714] prctl(PR_SET_PDEATHSIG, SIGKILL) = 0

# PID 1224814 프로세스가 runc 명령어를 실행하면서 runc로 변형됨 
# runc create 명령을 통해 컨테이너 격리공간(Namespace, Cgroups)만 만들고, 프로세스는 실행하지 않음
[pid 1224714] execve("/usr/local/bin/runc", ["/usr/local/bin/runc", "--root", "/run/containerd/runc/k8s.io", "--log", "/run/containerd/io.containerd.ru"..., "--log-format", "json", "--systemd-cgroup", "create", "--bundle", "/run/containerd/io.containerd.ru"..., "--pid-file", "/run/containerd/io.containerd.ru"..., "c8b394369634a6c6755d4e9094907a92"...], 0xc000283030 /* 13 vars */ <unfinished ...>
[pid 1224711] <... clone resumed>, parent_tid=[15]) = 1224714
[pid 1224714] <... execve resumed>)     = 0
[pid 1224714] prctl(PR_SET_CHILD_SUBREAPER, 1) = 0
[pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORKstrace: Process 1224722 attached
[pid 1224714] <... clone resumed>, parent_tid=[17]) = 1224722

# PID 1224714는 PID 1224723을 생셩
# PID 1224723이 runc init을 사용하여 컨테이너 초기화 작업을 진행
# 이후 PID 1224726 > Pause 컨테이너(PID 1224727)로 이어지는 과정
[pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224723 attached
[pid 1224723] execve("/proc/self/fd/6", ["/usr/local/bin/runc", "init"], 0xc0001ff7c0 /* 7 vars */ <unfinished ...>
[pid 1224714] <... clone resumed>, parent_tid=[18]) = 1224723
[pid 1224723] <... execve resumed>)     = 0
[pid 1224723] prctl(PR_SET_DUMPABLE, SUID_DUMP_DISABLE) = 0
[pid 1224723] prctl(PR_SET_NAME, "runc:[0:PARENT]"...) = 0
[pid 1224723] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224726 attached
[pid 1224723] exit_group(0)             = ?
[pid 1224723] +++ exited with 0 +++
[pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224726, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224723, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 1224714] exit_group(0)             = ?
[pid 1224714] +++ exited with 0 +++
[pid 1224711] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224714, si_uid=0, si_status=0, si_utime=0, si_stime=1 /* 0.01 s */} ---

 위 내용을 해석해 보면 다음과 같습니다.

  1. containerd-shim의 스레드인 PID 1224711이 프로세스 PID 1224714를 생성
  2. 프로세스 PID 1224714는 'runc create'로 Namespace와 Cgroups만 만들고 일시 중지
  3. PID 1224714가 PID 1224723을 생성하여 'runc init'으로 컨테이너 프로세스 초기화 진행
  4. 이후 PID 1224726 생성 → Pause 컨테이너(PID 1224727) 생성으로 이어짐

 여기서 한 가지 짚고 넘어가야 할 것이 있습니다. 바로 PID 1224723PID 122476입니다. 다른 프로세스와 다르게 이들만 특별하게 CLONE_PARENT 설정을 사용하고 있는데, 이는 runc의 특별한 역할 때문입니다. runc의 역할은 오로지 컨테이너 생성과 실행에만 있으며, 이것이 끝나면 바로 종료됩니다. 따라서 CLONE_PARENT 옵션을 통해 미리 고아가 될 컨테이너 프로세스의 부모를 더 상위 프로세스로 지정해 주어 프로세스 상속 관계의 안정성을 확보합니다.

 참고로, runc는 OCI(Open Container Initiative) 표준을 준수하는 저수준 런타임으로 containerd가 이를 사용함으로 해서 역시 OCI 표준을 만족할 수 있게 되는 것입니다.

 

 추가로 Nginx 컨테이너(PID 1224754) 역시도 그 프로세스를 추적해 보면 우리가 지금까지 Pause 컨테이너를 추적했던 것과 거의 흡사한 내용을 확인할 수 있을 것입니다.

[분석] 왜 containerd는 containerd-shim을 입양 보냈을까?

 왜 이런 복잡한 과정을 거쳐서 컨테이너를 만들까요? 이것이 바로 Daemonless Architecture라는 설계 철학의 구현이기 때문입니다. 만약 containerd-shim이 containerd의 자식 프로세스라면 container runtime 업그레이드와 같은 상황에서 모든 서비스가 중지되는 문제가 발생할 것입니다. 하지만 containerd-shim을 의도적으로 고아(Orphan)로 만들고 systemd로 입양 보내면서 container runtime과 컨테이너 프로세스 간의 결합도를 분리하고 고가용성을 확보할 수 있게 된 것입니다.

 

 또한, runc의 경우에는 의도적으로 컨테이너 프로세스의 부모 프로세스로 존재하지 않게 하기 위해 CLONE_PARENT 옵션을 주어 실행, 미리 더 상위 프로세스로 입양되게 하여 프로세스 상속 관계의 안정성을 확보하는 것은 매우 영리한 설계라 할 수 있겠습니다.

[결론 및 예고] 리소스를 통제하는 보이지 않는 손: Cgroup

 지금까지 프로세스 트리가 어떻게 '생명 주기'의 안정성을 위해 분리되었는지 확인해 보았습니다. Daemonless Architecture와 영리한 결합도 분리를 통해 시스템 안정성을 확보하기 위해 이런 복잡한 과정을 거친다는 것이... 리눅스 시스템은 파고들수록 정교하면서도 또 이해하기에 어렵다고 느껴집니다. 그런데 여기서 한 가지 의문이 듭니다.

분명 containerd와 containerd-shim은 서로 남남이 되었는데 어째서 systemctl status containerd 명령어에서 Cgroup에 함께 보일까?

 

 다음 포스팅에서는 pstree 뒤에 숨겨진 리소스를 통제하는 보이지 않는 손 Cgroup과 Systemd Slice의 이중적인 구조에 대해서 파해쳐 보도록 하겠습니다.

 

[Oracle Cloud] OCI Free Tier 소개

홈랩의 확장을 위해 여러 CSP를 찾아 보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능과 사용량을 보장하기 때

tech-recipe.tistory.com

 이전 포스팅에서, 오라클 클라우드의 Free Tier에서 제공하는 기능에 대해서 알아보았습니다. 이번 포스팅은 오라클 클라우드를 제어하는데 필요한 CLI 도구를 설치하고, 사용하는 방법에 대해서 알아보겠습니다.


1. OCI CLI 설치하기

 

Quickstart

This section documents how to quickly install and configure the OCI Command Line Interface (CLI).

docs.oracle.com

 이번 포스팅에는 MacOS를 기반으로한 OCI CLI 설치 방법을 설명드립니다. 다른 운영체제에서 OCI CLI를 설치하는 방법은 위 링크를 통해 확인할 수 있습니다.

Brew Install

 MacOS에서는 Homebrew를 사용합니다. 따라서 Homebrew가 먼저 설치되어 있어야 합니다.

CLI Install

brew update && brew install oci-cli

CLI Upgrade

brew update && brew upgrade oci-cli

CLI Uninstall

brew uninstall oci-cli

 

 설치가 완료되면 아래 명령어를 통해 설치를 확인할 수 있습니다.

oci -v

# 2025년 10월 18일 기준
3.68.0

2. Configuration file 설정하기

명령어를 사용한 Setup

 oci setup config 명령어를 사용하면 대화 형식을 통해 Configuration file을 설정할 수 있습니다.

oci setup config

# 아래 내용이 출력됨
    This command provides a walkthrough of creating a valid CLI config file.

    The following links explain where to find the information required by this
    script:

    User API Signing Key, OCID and Tenancy OCID:

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#Other

    Region:

        https://docs.cloud.oracle.com/Content/General/Concepts/regions.htm

    General config documentation:

        https://docs.cloud.oracle.com/Content/API/Concepts/sdkconfig.htm


Enter a location for your config [/Users/<사용자 이름>/.oci/config]: # Enter를 입력하면 기본 경로에 config 파일이 생성됨
Enter a user OCID: # <사용자 OCID> 입력
Enter a tenancy OCID: # <Tenacy OCID> 입력
Enter a region by index or name(e.g.
1: af-johannesburg-1, 2: ap-batam-1, 3: ap-chiyoda-1, 4: ap-chuncheon-1, 5: ap-chuncheon-2,
6: ap-dcc-canberra-1, 7: ap-dcc-gazipur-1, 8: ap-delhi-1, 9: ap-hyderabad-1, 10: ap-ibaraki-1,
11: ap-melbourne-1, 12: ap-mumbai-1, 13: ap-osaka-1, 14: ap-seoul-1, 15: ap-seoul-2,
16: ap-singapore-1, 17: ap-singapore-2, 18: ap-suwon-1, 19: ap-sydney-1, 20: ap-tokyo-1,
21: ca-montreal-1, 22: ca-toronto-1, 23: eu-amsterdam-1, 24: eu-budapest-1, 25: eu-crissier-1,
26: eu-dcc-dublin-1, 27: eu-dcc-dublin-2, 28: eu-dcc-milan-1, 29: eu-dcc-milan-2, 30: eu-dcc-rating-1,
31: eu-dcc-rating-2, 32: eu-dcc-zurich-1, 33: eu-frankfurt-1, 34: eu-frankfurt-2, 35: eu-jovanovac-1,
36: eu-madrid-1, 37: eu-madrid-2, 38: eu-marseille-1, 39: eu-milan-1, 40: eu-paris-1,
41: eu-stockholm-1, 42: eu-zurich-1, 43: il-jerusalem-1, 44: me-abudhabi-1, 45: me-abudhabi-2,
46: me-abudhabi-3, 47: me-abudhabi-4, 48: me-alain-1, 49: me-dcc-doha-1, 50: me-dcc-muscat-1,
51: me-dubai-1, 52: me-ibri-1, 53: me-jeddah-1, 54: me-riyadh-1, 55: mx-monterrey-1,
56: mx-queretaro-1, 57: sa-bogota-1, 58: sa-santiago-1, 59: sa-saopaulo-1, 60: sa-valparaiso-1,
61: sa-vinhedo-1, 62: uk-cardiff-1, 63: uk-gov-cardiff-1, 64: uk-gov-london-1, 65: uk-london-1,
66: us-ashburn-1, 67: us-ashburn-2, 68: us-chicago-1, 69: us-gov-ashburn-1, 70: us-gov-chicago-1,
71: us-gov-phoenix-1, 72: us-langley-1, 73: us-luke-1, 74: us-newark-1, 75: us-phoenix-1,
76: us-saltlake-2, 77: us-sanjose-1, 78: us-somerset-1, 79: us-thames-1): # <사용자 Home region 번호> 입력
Do you want to generate a new API Signing RSA key pair? (If you decline you will be asked to supply the path to an existing key.) [Y/n]: y # 'y' 입력하여 새로운 api 키 생성
Enter a directory for your keys to be created [/Users/<사용자 이름>/.oci]: # Enter를 입력하면 key가 저장될 위치로 기본 위치가 지정됨
Enter a name for your key [oci_api_key]: # OCI Key 이름 지정, Enter를 입력하면 'oci_api_key'라는 기본 이름으로 지정
Public key written to: /Users/<사용자 이름>/.oci/oci_api_key_public.pem
Enter a passphrase for your private key ("N/A" for no passphrase): # Private Key의 passphrase를 입력, 'N/A'를 입력하면 passphrase 없이 진행
Repeat for confirmation: # 위에 입력했던 passphrase를 다시 입력, 'N/A'를 입력했어도 동일하게 입력
Private key written to: /Users/<사용자 이름>/.oci/oci_api_key.pem
Fingerprint: 74:5b:fa:ec:21:22:65:d6:31:c1:7a:08:12:41:0b:67
Config written to /Users/<사용자 이름>/.oci/config


    If you haven't already uploaded your API Signing public key through the
    console, follow the instructions on the page linked below in the section
    'How to upload the public key':

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#How2

 위 대화 형식의 configuration setup을 진행에 userOCID, teancy OCID, region index 정보를 입력해야 합니다. 각 정보를 확인하는 방법은 아래와 같습니다.

userOCID

OCI Profile 메뉴

 userID는 화면 우측 상단의 사람모양 아이콘을 클릭하고, 드롭다운 메뉴의 User settings를 클릭합니다.

 화면에 userOCID값이 출력됩니다. 가급적이면 이 값은 유출되지 않도록 하는게 좋습니다.

tenacyOCID / Home region / Home region key

 TenacyOCID 정보 역시 화면 우측 상단의 사람 모양 아이콘을 클릭하고 나타나는 드롭다운 메뉴에서 Teancy를 클릭하여 확인할 수 있습니다. 이 화면에서는 또한 Home region과 그에 대한 Key 값도 확인할 수 있습니다. Home region key 값을 통해 Region Identifier를 확인할 수 있기 때문에, 대화형 configration file setup에서 region index가 불확실한 경우 꼭 참고하여야 합니다.

 

Regions and Availability Domains

Open the Help menu , go to Support and click Request service limit increase. Enter the following: Primary Contact Details: Enter the name and email address of the person making the request. Enter one email address only. A confirmation will be sent to this

docs.oracle.com

 위 링크는 Region Key와 Region Identifier를 함께 확인할 수 있는 문서로 참고하여 region index를 확인하면 됩니다.

3. API Public key 업로드

 Configration file이 생성되면 기본적으로 홈(~/) 경로에 .oci/ 디렉토리 아래에 oci_api_key_public.pem이라는 파일이 생성됩니다. 해당 파일의 내용을 읽고 이를 OCI 콘솔에 업로드 합니다.

cat ~/.oci/oci_api_key_public.pem

# 아래 공개키 전체 내용을 복사
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA78+/qFddYMqYyA3Lvd9A
<중략>
AZmsXXl3seTPo189zcy3IwEC2PYeW/WLhc1QdINL6P7wWETLzNI0aDpNbNTL+HHH
3wIDAQAB
-----END PUBLIC KEY-----

사용자 Profile > Add API Key
oci_api_key_public.pem 파일의 내용을 입력

4. OCI CLI 설정 확인

 아래와 같은 명령어를 통해 OCI CLI가 제대로 작동하는지 확인할 수 있다.

# 사용자 리스트업
oci iam user list

{
  "data": [
    {
      "capabilities": {
        "can-use-api-keys": true,
        "can-use-auth-tokens": true,
        "can-use-console-password": true,
        "can-use-customer-secret-keys": true,
        "can-use-db-credentials": true,
        "can-use-o-auth2-client-credentials": true,
        "can-use-smtp-credentials": true
      },
      "compartment-id": "<compartment OCID>",
# ... 생략

# 사용자 정보 확인
oci iam user get --user-id <사용자 OCID>
{
  "data": {
    "capabilities": {
      "can-use-api-keys": true,
      "can-use-auth-tokens": true,
      "can-use-console-password": true,
      "can-use-customer-secret-keys": true,
      "can-use-db-credentials": true,
      "can-use-o-auth2-client-credentials": true,
      "can-use-smtp-credentials": true
    },
# ... 생략
}

 위 설정을 통해 Oracle Cloud의 CLI 툴을 로컬에 설치하고, API 공개키 등록을 통해 클라우드 인프라에 명령어를 통해 제어할 수 있는 환경을 구성해 보았습니다.

 

 본격적인 홈랩 구성을 위한 준비단계가 잘 진행되고 있는것 같습니다.

 홈랩의 확장을 위해 여러 CSP를 찾아보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능과 사용량을 보장하기 때문에 1일 개발자나 테스트 용도의 인프라가 필요한 분들이라면 아주 매력적인 클라우드 서비스라 생각됩니다.


Free Tier 가입하기

 

클라우드 서비스 무료 이용

Oracle Cloud Free Tier는 기업에게 무제한으로 사용할 수 있는 상시 무료 클라우드 서비스를 제공합니다.

www.oracle.com

 오라클 클라우드 Free Tier 가입은 위 페이지를 통해 할 수 있습니다. 가입 절차는 간단한 편이고, 신용 카드 정보를 입력하면 여느 클라우드처럼 해외 결제가 이루어졌다가 취소됩니다. 저의 경우에는 홈 리전을 싱가포르로 설정하였기 때문에 1.38 싱가포르 달러가 결제되었다가 취소되었습니다.

Oracel Free Tier 가입 페이지

 최초 가입 시 홈 리전을 선택하게 되는데, 이때 선택된 홈 리전은 변경이 불가능합니다. 또한 홈 리전에서만 Free Tier 제품들을 사용할 수 있으므로 선택에 신중을 가해야 합니다. 저의 경우에는 ARM 기반의 Compute Instance 사용이 주목적이었는데, 한국 리전에는 해당 자원의 여유가 없다는 경고가 있어, 싱가포르를 홈 리전으로 선택하게 되었습니다.

Free Tier에서 제공하는 기능들

 

Access Cloud Services for Free

Oracle Cloud Free Tier provides enterprises with Always Free Cloud Services that can be used for an unlimited time.

www.oracle.com

 오라클 Free Tier는 상당히 다양하고, 넉넉한 기능과 자원을 Free Tier로 제공합니다. 위 페이지에서 다양한 Free Tier 제품군들을 검색해 볼 수 있습니다. 총 27개의 서비스를 Always Free로 제공하고 있으며, 추가로 가입한 30일 동안 300$ 상당의 크레디트를 제공하여 Free Tier가 아닌 제품도 사용해 볼 수 있습니다. 아래는 주요 리소스 카테고리 별 Free Tier 서비스에 대한 요약입니다.

1. Infrastructure

서비스 리소스 상세 내용 비고
Compute AMD 프로세서 기반 VM 인스턴스 - Shape: VM.M.Standard.E2.1.Micro
- 수량: 최대 2개
- 프로세서: 1/8 OCPU (추가 리소스 사용 가능)
- 메모리: 1GB
- 네트워킹: VNIC 1개, 공용 IP 1개, 인터넷 대역폭 최대 50Mbps (지역 내/온프레미스는 최대 480Mbps)
유휴 인스턴스 정책

7일 동안 CPU, 네트워크, 메모리(A1) 사용률이 20% 미만인 유휴 인스턴스는 Oracle에 의해 회수될 수 있음
ARM 프로세서 기반 VM 인스턴스 - Shape: VM.M.Standard.A1.Flex
- 제공량: 월 3,000 OCPU-시간 및 18,000 GB-시간 무료
(총 4 OCPU, 24GB 메모리에 해당, 자유롭게 조합 가능)
- 네트워킹: OCPU 수에 비례하여 확장
Block Volume 블록 스토리지 - 총 용량: 200 GB (부트 볼륨과 블록 볼륨 합산)
- 볼륨 백업: 5개
 
Object Storage 오브젝트 스토리지 - 총 용량: 20 GB (Standard, Infrequent Access, Archive 티어 합산)
- API 요청: 월 50,000건
 
  아카이브 스토리지 오브젝트 스토리지 20GB 용량에 포함됨  
Certificatges 인증서 - 인증 기관(CA): 5개
- 인증서: 150개
 
Valult 키 관리 - 소프트웨어 보호 키: 모두 무료
- HSM 보호 키 버전: 20개
- Vault 시크릿: 150개
 
Resource Manager 인프라 자동화 - 스택: 100개
- 동시 작업: 2개
- 프라이빗 엔드포인트: 1개
 

2. Database

서비스 리소스 상세 내용 비고
Autonomouse Database 자율형 데이터베이스 - 수량: 2개
- 프로세서: 1 OCPU (확장 불가)
- 스토리지: 20 GB (확장 불가)
- 워크로드 유형: TP, Data Warehouse, APEX, JSON 중 선택
- 최대 동시 세션: 20
 
NoSQL Database NoSQL 데이터베이스 - 테이블: 3개
- 스토리지: 테이블당 25GB
- 읽기/쓰기: 월 1억 3300만 건
 
MySQL HeatWave MySQL 데이터베이스 - DB 시스템: 1개
- 스토리지: 50GB
- 백업 스토리지: 50GB
 

3. Networking

서비스 리소스 상세 내용 비고
Load Balancing 로드 밸런서 - 수량: 1개
- Shape: Flexible (최소/최대 10 Mbps)
- 리스너/가상 호스트명/백엔드셋: 각 16개
- 백엔드 서버: 1024개
 
Network Load Balancer 네트워크 로드 밸런서 - 수량: 1개
- 리스너/백엔드셋: 각 50개
- 총 백엔드 서버: 1024개
 
Virtual Cloud Network (VCN) 가상 클라우드 네트워크 - 수량: 최대 2개  
Site-to-Site VPN 사이트 간 VPN - IPSec 연결: 최대 50개  
VCN Flow Logs VCN 흐름 로그 - 용량: 월 10GB (OCI Logging 서비스와 공유)  

4. Observability & Management

서비스 리소스 상세 내용 비고
Monitoring 모니터링 - 수집 데이터 포인트: 월 5억 건
- 조회 데이터 포인트: 월 10억 건
 
Logging 로깅 월 10GB 무료 (VCN 흐름 로그와 공유)  
Application Performance Monitoring APM - 추적 이벤트: 시간당 1,000건
- Synthetic 모니터 실행: 시간당 10회
 
Notifications 알림 - HTTPS 알림: 월 100만 건
- 이메일 알림: 월 1,000건
 
Email Delivery 이메일 전송 - 전송량: 월 3,000건  
Connector Hub 커넥트 허브 - 커넥터: 2개  
Console Dashboards 콘솔 대시보드 - 대시보드: 테넌시당 100개  
Bastion 배스천 모든 계정에 무료로 제공  

5. 추가 서비스

서비스 리소스 상세 내용 비고
Outbound Data Transfer
아웃바운드 데이터 전송 - 전송량: 월 10 TB  

 


 이 정도 수준이면, Compute Instance의 유휴 인스턴스 정책만 잘 준수한다면 소규모 서비스를 하는 데에도 부족함이 없는 수준의 강력한 기능이라 할 수 있겠습니다. 웬만한 개발 환경이나 테스트 환경은 찜 쪄먹는 수준이 아닐까 합니다.

 

 오라클 클라우드의 Free Tier를 활용해서 도전적인 홈랩 프로젝트를 시작해 보려고 합니다. 기존 On-Premise 장비와 클라우드 인프라를 엮어서 하이브리드 클라우드로 진화시켜 볼 예정입니다. 시간은 좀 걸리겠지만, 재미있는 여정이 될 것 같습니다.


 

[Oracle Cloud] OCI CLI 사용하기

[Oracle Cloud] OCI Free Tier 소개홈랩의 확장을 위해 여러 CSP를 찾아 보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능

tech-recipe.tistory.com

 

 

지금까지 우리는 Cilium의 다양한 네트워킹 및 보안 기능을 학습하며 강력한 쿠버네티스 클러스터를 구축하는 방법을 익혔습니다. 하지만 클러스터의 규모가 수백, 수천 개의 노드로 확장되고 수만 개의 파드가 동작하는 대규모 환경이 되면, 우리는 새로운 도전에 직면하게 됩니다. 바로 '성능' 입니다. 응답이 느려지는 API 서버, 원인 모를 파드 생성 실패, 간헐적인 네트워크 드롭 등은 클러스터의 안정성을 심각하게 위협할 수 있습니다.

이번 7주차 스터디에서는 대규모 클러스터에서 발생할 수 있는 다양한 성능 병목 지점을 진단하고, 이를 해결하기 위한 구체적인 튜닝 전략을 심층적으로 다루고자 합니다. Kubernetes 컨트롤 플레인의 심장인 kube-apiserveretcd의 동작 원리부터 Cilium 데이터 플레인의 핵심인 eBPF Map 관리까지, 성능 최적화의 여정을 함께 떠나보겠습니다.

이미지 출처: https://cilium.io/use-cases/host-firewall/

1. Kubernetes 성능 병목 현상 탐구: API 서버는 왜 느려지는가?

클러스터의 모든 상태 변경은 kube-apiserver를 통해 etcd에 기록됩니다. 따라서 클러스터의 규모가 커질수록 컨트롤 플레인의 부하는 기하급수적으로 증가하며, 이곳에서 가장 먼저 성능 문제가 발생합니다. 이러한 문제를 재현하고 분석하기 위해 kube-burner와 같은 부하 테스트 도구를 활용할 수 있습니다.

부하 테스트를 통해 발견된 병목 지점

kube-burner를 사용하여 수백 개의 파드를 동시에 생성하는 시나리오를 시뮬레이션하면 다음과 같은 대표적인 문제들을 마주하게 됩니다.

  1. Too many pods (노드 당 파드 개수 제한): Kubelet은 maxPods 설정값에 따라 노드 당 생성 가능한 파드의 최대 개수를 제한합니다. 이 한계를 초과하면 스케줄러는 더 이상 해당 노드에 파드를 할당하지 못하고 FailedScheduling 이벤트를 발생시킵니다.
  2. No IP addresses available (PodCIDR 고갈): 각 노드는 PodCIDR이라는 고유한 IP 대역을 할당받아 자신이 관리하는 파드들에게 IP를 순차적으로 부여합니다. 만약 maxPods를 늘리더라도 할당된 PodCIDR 대역 내의 IP를 모두 소진하면, CNI 플러그인은 FailedCreatePodSandBox 에러를 내며 파드 생성을 실패시킵니다.

이러한 현상들은 표면적인 문제일 뿐, 그 근본 원인은 대량의 리소스 생성 요청이 컨트롤 플레인에 집중될 때 발생하는 부하와 관련이 깊습니다.

근본 원인: 대규모 LIST 요청과 메모리 폭증

쿠버네티스 컨트롤러(kubelet, CNI 에이전트 등)들은 클러스터의 최신 상태를 유지하기 위해 API 서버에 주기적으로 리소스 목록을 요청(LIST API Call)합니다. 문제는 클러스터에 수만 개의 파드가 존재할 때 발생합니다.

  • 동작 방식: 클라이언트가 kubectl get pods --all-namespaces와 같은 명령을 실행하면, kube-apiserveretcd로부터 모든 파드 정보를 가져와야 합니다. 이때 etcd는 트랜잭션의 일관성을 보장하기 위해 특정 시점의 데이터를 메모리에 복제한 후 응답합니다. kube-apiserver는 이 데이터를 받아 다시 Go 구조체로 변환(deserialization)하고, 최종적으로 클라이언트가 요청한 형식(JSON, YAML 등)으로 변환(serialization)하여 전달합니다.
  • 결과: 이 과정에서 etcdkube-apiserver 양쪽 모두에서 응답 데이터 크기의 몇 배에 달하는 막대한 메모리가 일시적으로 할당됩니다. 만약 이러한 대규모 LIST 요청이 여러 클라이언트로부터 동시에 발생하면, 컨트롤 플레인 컴포넌트는 메모리 사용량이 급증하여 OOM(Out of Memory)으로 종료될 수 있습니다. 이는 클러스터 전체의 가용성을 위협하는 심각한 장애로 이어집니다.

2. Kubernetes 성능 튜닝 전략

컨트롤 플레인의 안정성을 확보하기 위해서는 API 요청을 효율적으로 관리하고 인프라 전반의 설정을 최적화하는 다각적인 접근이 필요합니다.

API 요청 관리 기법

  1. 페이지네이션 (Limit & Continue): 대규모 리소스를 조회하는 클라이언트는 반드시 limitcontinue 파라미터를 사용하여 응답을 여러 페이지로 나누어 요청해야 합니다. 이는 한 번의 요청으로 인한 메모리 부담을 줄이는 가장 기본적인 방법입니다. kubectl을 포함한 대부분의 공식 클라이언트는 이 방식을 기본적으로 사용합니다.
  2. API 서버 캐시 활용 (ResourceVersion="0"): LIST 요청 시 resourceVersion="0" 파라미터를 사용하면, kube-apiserveretcd에 직접 요청하는 대신 자신의 내부 캐시에서 데이터를 반환합니다. 이는 etcd의 부하를 극적으로 줄여주지만, 약간의 지연이 있는 데이터(eventual consistency)를 받을 수 있다는 점을 감안해야 합니다. 하지만 대부분의 컨트롤러 동기화 작업에는 이 방식으로 충분합니다.
  3. API 우선순위 및 공정성 (APF - API Priority and Fairness): Kubernetes 1.20부터 도입된 APF는 단순한 요청 속도 제한을 넘어, 요청의 중요도에 따라 우선순위를 부여하고 큐를 분리하여 관리합니다.
    • FlowSchema: 들어오는 요청을 '누가(Subject)', '무엇을(Resource)' 요청하는지에 따라 분류합니다.
    • PriorityLevelConfiguration: 각 요청 흐름에 'system-critical', 'workload-high' 등과 같은 우선순위 레벨과 동시 처리 한도를 할당합니다. 이를 통해 중요도가 낮은 컨트롤러의 과도한 요청이 노드의 keepalive와 같은 핵심 시스템 동작을 방해하는 것을 방지하고, 클러스터의 안정성을 보장합니다.

인프라 및 커널 튜닝

  • 컨트롤 플레인 고가용성(HA): 운영 환경에서는 최소 3대 이상의 컨트롤 플레인 노드를 구성하고, etcd 클러스터는 API 서버와 분리된 전용 노드에 배치하는 것이 권장됩니다. 특히, 생성과 삭제가 빈번한 Event 리소스는 별도의 etcd 클러스터에 저장하여 메인 etcd의 부하를 줄일 수 있습니다.
  • ARP 캐시 튜닝: 대규모 L2 네트워크 환경에서는 노드 간 ARP 요청이 빈번해져 커널의 ARP 캐시가 가득 차는 neighbor table overflow 문제가 발생할 수 있습니다. sysctl을 통해 net.ipv4.neigh.default.gc_thresh 관련 파라미터들을 상향 조정하여 캐시 크기를 늘려야 합니다.
  • Kubelet 동시성 제어: serializeImagePulls 플래그를 false로 설정하면 노드에서 여러 컨테이너 이미지를 병렬로 다운로드하여 파드 생성 속도를 높일 수 있습니다. 또한, Kubelet이 API 서버에 가하는 부하를 제어하기 위해 kubeAPIQPSkubeAPIBurst 값을 적절히 조정해야 합니다(1.27부터 기본값이 상향됨).

핵심 성능 지표 모니터링

Prometheus와 Grafana를 활용하여 다음과 같은 핵심 지표를 지속적으로 모니터링해야 병목 지점을 조기에 발견할 수 있습니다.

  • apiserver_request_total: API 서버의 QPS와 에러율
  • apiserver_request_duration_seconds_bucket: API 요청의 99%ile 지연 시간
  • etcd_request_duration_seconds_bucket: etcd 요청 지연 시간
  • workqueue_depth: 컨트롤러의 작업 큐 깊이 (큐가 계속 쌓이면 병목)
  • workqueue_queue_duration_seconds_bucket: 작업이 큐에서 대기한 시간

3. Cilium 성능 분석 및 최적화

Cilium은 eBPF를 통해 고성능 데이터 플레인을 제공하지만, 대규모 환경에서는 Cilium 자체의 내부 상태 관리 메커니즘이 성능에 영향을 미칠 수 있습니다.

BPF Map Pressure와 StateDB

  • BPF Map: Cilium은 서비스, 엔드포인트, 정책 등의 정보를 커널 공간의 BPF Map이라는 특수한 자료구조에 저장하여 빠르게 조회합니다. 하지만 이 Map들은 생성 시 최대 크기가 정해져 있어, 서비스나 엔드포인트의 수가 이 한계를 초과하면 더 이상 새로운 정보를 저장할 수 없습니다.
  • 장애 현상: BPF Map이 가득 차면(BPF map pressure 메트릭 증가), Cilium 에이전트는 새로운 서비스 정보를 Map에 업데이트하지 못하고 Service backend not found와 같은 사유로 패킷을 드롭(drop)하기 시작합니다.
  • Cilium의 회복성 설계: Cilium은 이러한 일시적인 실패에 대응하기 위해 StateDB라는 인메모리 데이터베이스와 Reconciler 패턴을 사용합니다. API 서버로부터 받은 모든 desired state는 먼저 StateDB에 저장됩니다. Reconciler는 주기적으로 StateDB의 상태와 실제 BPF Map의 상태를 비교하여, 실패했거나 누락된 항목을 재시도하여 최종적 일관성을 보장합니다.

Cilium 튜닝 파라미터

  • bpf.mapDynamicSizeRatio: 노드의 전체 메모리 대비 BPF Map에 할당할 메모리의 비율을 조정합니다. 서비스나 엔드포인트가 매우 많은 환경에서는 이 비율을 기본값(0.0025)보다 높여 BPF Map의 크기를 늘려야 합니다.
  • k8s.client-qps / k8s.client-burst: Cilium 에이전트가 kube-apiserver에 요청을 보내는 속도를 제어합니다. 대규모 클러스터에서는 이 값을 적절히 상향 조정하여 Cilium이 클러스터 변경 사항을 더 빠르게 동기화하도록 할 수 있습니다.

결론

대규모 쿠버네티스 클러스터의 성능 튜닝은 단일 파라미터를 조정하는 것을 넘어, 시스템 전반의 동작 원리를 깊이 이해하는 것에서 시작됩니다. 특히 컨트롤 플레인의 심장인 API 서버와 etcd의 상호작용, 그리고 각 컨트롤러의 동기화 메커니즘을 파악하는 것이 중요합니다. kube-burner와 같은 도구로 한계 상황을 시뮬레이션하고, Prometheus 메트릭을 통해 병목 지점을 정량적으로 분석하는 체계적인 접근이 필요합니다.

Cilium 환경에서는 여기에 더해, eBPF 데이터 플레인의 핵심인 BPF Map의 동작 방식과 StateDB를 통한 회복성 메커니즘을 이해하고, 관련 파라미터를 클러스터의 특성에 맞게 최적화해야 합니다. 성능 문제는 복잡하고 다층적이지만, 그 원리를 파고들면 반드시 명확한 해결책을 찾을 수 있습니다.

지난 5주차 스터디에서는 BGP와 ClusterMesh를 통해 클러스터의 경계를 넘어 외부 네트워크, 그리고 다른 클러스터와 통신하는 방법을 다루었습니다. 클러스터의 '남-북(North-South)' 트래픽과 클러스터 간 통신을 마스터한 셈이죠. 이제 우리의 시선은 다시 클러스터 내부로, 마이크로서비스 간의 복잡하고 동적인 '동-서(East-West)' 트래픽으로 향합니다. 바로 서비스 메시(Service Mesh) 의 영역입니다.

이번 포스팅에서는 서비스 메시의 기본 개념부터 시작하여, 기존의 Sidecar 모델이 가졌던 한계와 이를 eBPF로 극복하는 Cilium의 혁신적인 접근법을 심층적으로 분석합니다. 또한, Ingress의 차세대 표준인 Gateway API와 강력한 워크로드 신원 증명 프레임워크인 SPIFFE까지, Cilium이 제공하는 차세대 서비스 메시의 구성 요소들을 자세히 살펴보겠습니다.

이미지 출처: https://cilium.io/blog/2019/08/20/cilium-16/

1. 서비스 메시(Service Mesh)란 무엇인가?

마이크로서비스 아키텍처(MSA)는 애플리케이션을 작고 독립적인 서비스 단위로 분리하여 개발과 배포의 민첩성을 높였습니다. 하지만 이는 곧 서비스 간의 네트워크 통신이 폭발적으로 증가함을 의미했고, 다음과 같은 새로운 운영상의 과제들을 낳았습니다.

  • 관측 가능성(Observability): 수많은 서비스 중 어디에서 병목이나 에러가 발생하는지 추적하기 어렵습니다. 특정 사용자 요청이 여러 서비스를 거칠 때, 어느 구간에서 지연이 발생하는지, 어떤 서비스의 실패가 연쇄적인 장애를 유발하는지 파악하기가 매우 힘들어집니다.
  • 트래픽 관리(Traffic Management): A/B 테스팅, 카나리 배포, 서킷 브레이킹과 같은 정교한 트래픽 제어를 어떻게 구현할 것인가? 예를 들어, 신규 버전의 서비스에 전체 트래픽의 1%만 보내거나, 특정 HTTP 헤더를 가진 요청만 신규 버전으로 라우팅하는 등의 고급 전략을 애플리케이션 코드 수정 없이 적용하기 어렵습니다.
  • 보안(Security): 모든 서비스 간 통신을 어떻게 안전하게 암호화하고, 허가된 서비스끼리만 통신하도록 제어할 것인가? 제로 트러스트(Zero Trust) 보안 모델을 구현하기 위해 서비스 간 상호 TLS(mTLS) 인증을 적용하고 관리하는 것은 매우 복잡한 작업입니다.

이러한 공통의 관심사들을 각 애플리케이션 개발팀이 개별적으로 구현하는 것은 비효율적입니다. 서비스 메시는 바로 이 문제들을 해결하기 위해 등장한 전용 인프라 계층입니다. 애플리케이션 코드 변경 없이, 서비스 간의 모든 네트워크 통신을 가로채어 신뢰성, 보안, 관측 가능성을 제공하는 것이 핵심입니다.

전통적인 접근: 사이드카(Sidecar) 패턴

Istio, Linkerd와 같은 1세대 서비스 메시들은 이 기능을 사이드카 패턴으로 구현했습니다. 각 애플리케이션 파드에 Envoy와 같은 고성능 프록시 컨테이너를 '사이드카'로 함께 주입하는 방식입니다.

  • 동작 방식: 파드가 시작될 때, 초기화 컨테이너(init container)가 파드 내부의 iptables 규칙을 수정하여 모든 네트워크 트래픽이 이 사이드카 프록시를 거치도록 강제합니다. 이 프록시가 트래픽 라우팅, mTLS 암호화, 메트릭 수집과 같은 서비스 메시의 모든 기능을 대신 처리하며, 중앙의 컨트롤 플레인으로부터 정책을 받아 동적으로 적용합니다.
  • 한계점: 이 방식은 애플리케이션에 투명성을 제공했지만, 모든 파드마다 프록시를 띄워야 하므로 상당한 리소스 오버헤드를 유발했습니다. 또한, App -> Sidecar -> Node로 이어지는 과정에서 패킷이 커널의 TCP/IP 스택을 여러 번 거치게 되어 네트워크 지연 시간(Latency)이 증가하는 문제도 있었습니다.

2. Cilium의 새로운 접근: Sidecar-less 서비스 메시

Cilium은 eBPF를 활용하여 사이드카 모델의 근본적인 한계를 극복하는 Sidecar-less 서비스 메시라는 새로운 패러다임을 제시합니다.

  • 동작 방식: 파드마다 프록시를 두는 대신, 노드(Node)당 하나의 Envoy 프록시만 데몬셋으로 배포합니다. 그리고 eBPF를 사용하여 파드의 소켓(Socket) 레벨에서 나가는 트래픽을 가로채, 커널의 복잡한 네트워크 스택을 건너뛰고 바로 이 노드-로컬 Envoy 프록시로 전달합니다. 애플리케이션이 connect()나 send() 같은 시스템 콜을 호출하는 순간, eBPF가 이를 감지하여 패킷의 경로를 효율적으로 변경하는 것입니다.
  • 핵심 기술 (TPROXY): 이 투명한 리다이렉션은 커널의 TPROXY 기능을 통해 이루어집니다. 일반적인 NAT(REDIRECT)와 달리, TPROXY는 패킷의 원본 출발지/목적지 IP와 포트 정보를 그대로 유지한 채 트래픽을 프록시로 전달할 수 있게 해줍니다. 덕분에 Envoy 프록시는 자신이 실제 목적지인 것처럼 트래픽을 처리하면서도, 클라이언트의 원본 IP를 유실하지 않아 완전한 투명성을 보장합니다.
  • 장점: 리소스 사용량이 획기적으로 줄어들고, 커널 스택을 우회하여 네트워크 지연 시간이 단축됩니다. 또한, 모든 파드를 재시작하지 않고도 서비스 메시 기능을 전체 클러스터에 적용하거나 업그레이드할 수 있어 운영 편의성이 크게 향상됩니다.

3. Ingress를 넘어 Gateway API로

서비스 메시는 클러스터 내부 통신뿐만 아니라, 외부 트래픽이 클러스터로 들어오는 관문, 즉 인그레스(Ingress) 와도 밀접한 관련이 있습니다. Cilium은 표준 쿠버네티스 Ingress를 완벽하게 지원하며, 한 걸음 더 나아가 그 차세대 표준인 Gateway API를 핵심 기능으로 채택했습니다.

Gateway API는 기존 Ingress가 가진 표현력의 한계와 역할 불분명성을 해결하기 위해 등장했습니다.

  • 역할 지향(Role-Oriented) 아키텍처:
  • GatewayClass: 클러스터 관리자가 "우리 회사에서는 Cilium을 Gateway 구현체로 사용한다"고 정의하는 템플릿입니다.
  • Gateway: 인프라 운영자가 "80번, 443번 포트를 사용하는 my-gateway라는 LoadBalancer를 생성하겠다"고 선언하는 관문입니다.
  • HTTPRoute: 애플리케이션 개발자가 "my-gateway로 들어오는 /foo 경로는 foo-service로 보내달라"고 라우팅 규칙을 정의합니다.
    이처럼 역할이 명확히 분리되어, 각 팀은 자신의 책임 영역에 맞는 리소스만 관리하면 되므로 협업이 용이해지고 실수를 줄일 수 있습니다.
  • 표현력과 확장성: Ingress가 HTTP(S)에 국한되었던 것과 달리, Gateway API는 TCP, UDP, gRPC, TLS 라우팅을 표준 리소스로 지원합니다. 또한, 헤더 기반 라우팅, 트래픽 가중치 분산, 트래픽 미러링과 같은 고급 기능들을 어노테이션이 아닌 정식 API 필드로 정의하여 표준화된 방식으로 사용할 수 있습니다.

Cilium은 Gateway API를 네이티브로 구현하여, eBPF 기반의 고성능 데이터 플레인 위에서 이러한 정교한 L7 트래픽 관리 기능을 완벽하게 제공합니다.

4. 신뢰의 기반, 상호 인증(mTLS)과 SPIFFE

동적인 마이크로서비스 환경에서는 IP 주소가 더 이상 신뢰할 수 있는 식별자가 아닙니다. 따라서 서비스 간 통신을 보호하기 위해서는 IP 주소가 아닌, 암호학적으로 검증 가능한 워크로드 신원(Workload Identity) 이 필요합니다.

SPIFFE(Secure Production Identity Framework for Everyone) 는 이러한 강력한 신원을 모든 워크로드에 보편적으로 제공하기 위한 오픈소스 표준입니다.

  • 핵심 개념:
  • SPIFFE ID: spiffe://trust-domain/ns/default/sa/myapp과 같은 URI 형식의 고유한 워크로드 식별자입니다. 여기서 trust-domain은 신뢰의 루트(예: cluster.local)를, path는 네임스페이스와 서비스 어카운트 등을 조합하여 해당 워크로드를 고유하게 식별합니다.
  • SVID (SPIFFE Verifiable Identity Document): 이 SPIFFE ID가 담겨있는 검증 가능한 문서로, 일반적으로 X.509 인증서 형태로 제공됩니다.
  • 신원 발급 과정: 파드가 생성되면, 쿠버네티스는 해당 파드의 서비스 어카운트(Service Account) 정보를 담은 JWT 토큰을 주입합니다. 워크로드는 이 토큰을 증거로 SPIFFE 런타임 환경(예: SPIRE)에 자신의 신원을 증명하고, 해당 신원이 담긴 SVID(인증서)를 발급받습니다. 이 과정은 주기적으로 자동 갱신되어 신원의 유효성을 보장합니다.

Cilium은 이 SPIFFE 표준을 채택하여 클러스터 내 모든 서비스에 대한 상호 TLS(mTLS) 인증을 자동화합니다. 두 서비스가 통신을 시작할 때, 각자는 자신의 SVID를 상대방에게 제시하여 신원을 증명하고, 이 과정을 통과해야만 암호화된 통신 채널이 수립됩니다. 이 모든 과정이 애플리케이션에 투명하게, 인프라 레벨에서 자동으로 처리됩니다.

결론

이번 스터디를 통해 우리는 Cilium이 단순한 CNI를 넘어, eBPF라는 강력한 무기를 통해 어떻게 차세대 서비스 메시로 진화하고 있는지를 확인했습니다. 리소스 효율성과 성능을 극대화한 Sidecar-less 아키텍처, Ingress의 한계를 뛰어넘는 Gateway API 지원, 그리고 SPIFFE 기반의 강력한 mTLS 보안까지, Cilium은 현대적인 마이크로서비스 환경이 요구하는 복잡한 과제들을 하나의 통합된 솔루션으로 해결하고 있습니다.

Cilium과 함께라면, 더 이상 서비스 메시 도입을 위해 복잡한 사이드카 주입이나 별도의 솔루션을 고민할 필요 없이, 네트워킹 스택의 가장 낮은 레벨에서부터 가장 높은 애플리케이션 레벨까지 일관되고 효율적으로 관리할 수 있게 될 것입니다.

지난 3주차 스터디에서는 IPAM, 라우팅, 마스커레이딩 등 파드 간 통신을 가능하게 하는 쿠버네티스 네트워킹의 내부 동작 원리를 깊이 있게 살펴보았습니다. 이제 클러스터라는 우리만의 세상 안에서 통신하는 법을 익혔으니, 드디어 클러스터 외부의 사용자들이 우리 애플리케이션을 만날 수 있도록 문을 열어줄 시간입니다.

이번 포스팅에서는 쿠버네티스에서 외부 트래픽을 처리하는 핵심 관문인 Service의 동작 원리를 알아보고, Cilium이 제공하는 강력한 LoadBalancer 기능을 통해 온프레미스 환경에서도 클라우드처럼 서비스를 외부에 노출하는 방법을 자세히 다뤄보겠습니다.

이미지 출처: https://cilium.io/use-cases/load-balancer/


1. 쿠버네티스 Service 다시 보기: 왜 필요할까?

쿠버네티스에서 파드는 언제든지 사라지고 다시 생성될 수 있는 '임시적인' 존재입니다. 파드가 재시작되면 IP 주소도 바뀌어 버리죠. 만약 우리가 이 파드의 IP 주소를 코드에 직접 기록해두었다면, 파드가 재시작될 때마다 코드를 수정해야 하는 끔찍한 상황이 발생할 겁니다.

바로 이 문제를 해결하기 위해 서비스(Service) 가 등장했습니다. 서비스는 여러 개의 파드에 대한 고정된 단일 진입점(Single, Stable Entrypoint) 을 제공합니다. 우리는 변하기 쉬운 파드의 IP 대신, 변하지 않는 서비스의 고유한 IP(ClusterIP)나 도메인 이름을 바라보면 됩니다. 서비스는 마치 똑똑한 중간 관리자처럼, 자신에게 온 요청을 현재 실행 중인 건강한 파드들에게 알아서 분배(로드 밸런싱)해주는 역할을 합니다.

kube-proxy: 서비스의 마법을 현실로 만드는 일꾼

이러한 서비스의 마법은 각 노드에서 실행되는 kube-proxy라는 컴포넌트 덕분에 가능합니다. kube-proxy는 API 서버를 지켜보다가 서비스나 엔드포인트(서비스에 연결된 파드들)에 변경이 생기면, 각 노드의 네트워크 규칙을 업데이트하여 서비스로 가는 트래픽이 실제 파드로 전달되도록 설정합니다. 이 kube-proxy가 동작하는 방식은 역사적으로 발전해 왔습니다.

  1. Userspace 모드 (초기 방식): 모든 서비스 트래픽이 커널 공간과 사용자 공간을 넘나들며 kube-proxy 프로세스를 직접 거쳐 파드로 전달되었습니다. 구조는 간단했지만, 잦은 컨텍스트 스위칭으로 인한 성능 저하가 심해 현재는 거의 사용되지 않습니다.
  2. iptables 모드 (오랜 기간 기본값): kube-proxy가 트래픽을 직접 처리하지 않고, 커널의 netfilter 모듈(iptables)에 서비스 IP를 실제 파드 IP로 변환(DNAT)하는 규칙을 기록해둡니다. 트래픽은 커널 공간에서 바로 처리되므로 성능이 크게 향상되었습니다. 하지만 서비스와 파드가 수천, 수만 개로 늘어나면 iptables 규칙의 양이 방대해져 규칙 업데이트와 패킷 처리에 지연이 발생하는 확장성 문제가 있었습니다.
  3. IPVS 모드: iptables의 확장성 문제를 해결하기 위해 등장했습니다. IPVS(IP Virtual Server)는 해시 테이블을 사용하는 고성능 L4 로드 밸런서로, 훨씬 더 많은 수의 서비스를 효율적으로 처리할 수 있습니다. 현재 많은 환경에서 권장되는 방식입니다.
  4. eBPF (Cilium의 방식): Cilium은 kube-proxy를 완전히 대체하고 eBPF를 사용하여 서비스 로드 밸런싱을 구현합니다. 패킷이 커널의 네트워크 스택을 거치는 복잡한 과정을 우회하고, 네트워크 인터페이스(NIC)에 도착하는 시점에서 eBPF 프로그램이 직접 패킷을 처리하여 목적지 파드로 전달합니다. 이는 가장 높은 성능과 유연성을 제공하는 최신 방식입니다.

2. 서비스를 외부에 노출하는 방법들

ClusterIP 타입의 서비스는 클러스터 내부에서만 유효합니다. 외부 사용자가 서비스에 접근하게 하려면 NodePortLoadBalancer 타입을 사용해야 합니다.

  • NodePort: 모든 노드에 특정 포트(기본 30000-32767)를 열고, http://<아무노드IP>:<NodePort>로 들어온 요청을 해당 서비스로 전달합니다. 간단하지만, 노드 IP가 변경될 수 있고 3만번대 포트를 사용해야 하는 등의 제약이 있습니다.
  • LoadBalancer: 클라우드 환경에서 가장 이상적인 방식입니다. 이 타입으로 서비스를 생성하면, AWS의 ELB나 GCP의 Cloud Load Balancer와 같은 클라우드 제공사의 로드 밸런서가 자동으로 생성되고, 이 로드 밸런서의 고유한 외부 IP 주소를 통해 서비스에 접근할 수 있게 됩니다.

하지만 온프레미스(On-premise) 환경에서는 이런 외부 로드 밸런서를 자동으로 생성해주는 주체가 없습니다. 과거에는 MetalLB와 같은 별도의 솔루션을 설치해야만 LoadBalancer 타입의 서비스를 사용할 수 있었죠. 하지만 이제 Cilium이 이 기능을 자체적으로 품게 되었습니다.


3. Cilium의 LoadBalancer IPAM과 L2 Announcement

Cilium은 온프레미스 환경에서도 클라우드와 유사한 경험을 제공하기 위해 LoadBalancer IPAM(LB-IPAM)L2 Announcement 기능을 제공합니다.

LoadBalancer IPAM (LB-IPAM)

LB-IPAM은 LoadBalancer 타입의 서비스에 할당할 외부 IP 주소 풀(Pool)을 사용자가 직접 정의하고 관리할 수 있게 해주는 기능입니다.

# LoadBalancer 서비스에 할당할 IP 주소 풀을 정의하는 CRD
apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "cilium-lb-ippool"
spec:
  blocks:
    - start: "192.168.10.211"
      stop: "192.168.10.215"

위와 같이 IP 풀을 생성해두고, webpod 서비스를 LoadBalancer 타입으로 변경하면, Cilium은 정의된 풀에서 사용 가능한 IP(192.168.10.211)를 자동으로 할당하여 서비스의 EXTERNAL-IP로 설정해줍니다.

# webpod 서비스를 LoadBalancer 타입으로 변경
kubectl patch svc webpod -p '{"spec":{"type": "LoadBalancer"}}'

# 할당된 EXTERNAL-IP 확인
kubectl get svc webpod
# NAME     TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
# webpod   LoadBalancer   10.96.32.212   192.168.10.211   80:32039/TCP   150m

L2 Announcement: "192.168.10.211은 바로 나야!"

이제 서비스에 외부 IP가 할당되었지만, 아직 한 가지 문제가 남았습니다. 같은 네트워크 대역에 있는 다른 장비들(예: router)은 192.168.10.211이라는 IP가 실제로 어디에 있는지 알지 못합니다. 이 IP 주소에 대한 MAC 주소를 모르기 때문에 ARP 요청을 보내도 아무도 응답하지 않아 통신이 불가능하죠.

이때 필요한 것이 L2 Announcement 기능입니다. 이 기능이 활성화되면, LoadBalancer 서비스의 트래픽을 처리하게 될 노드(일반적으로 서비스의 엔드포인트 파드가 실행 중인 노드 중 하나)가 "192.168.10.211 주소의 주인은 바로 나(의 MAC 주소)야!" 라고 네트워크 전체에 알리기 위해 Gratuitous ARP (GARP) 패킷을 주기적으로 브로드캐스트합니다.

이 GARP 응답을 받은 스위치나 라우터는 자신의 ARP 테이블에 192.168.10.211 IP와 해당 노드의 MAC 주소를 매핑하여 기록합니다. 그 결과, 외부 클라이언트가 192.168.10.211로 보내는 트래픽은 L2 레벨에서 정확히 해당 노드로 전달될 수 있게 됩니다. 이는 MetalLB의 L2 모드와 동일한 원리로 동작합니다.

결론

쿠버네티스 서비스는 변덕스러운 파드들의 세상에 안정성이라는 질서를 부여하는 핵심 개념입니다. 그리고 kube-proxy의 발전사와 Cilium의 eBPF 기반 서비스 구현은 쿠버네티스 네트워킹이 어떻게 성능과 확장성을 향해 진화해왔는지를 보여주는 좋은 예시입니다.

특히 Cilium의 LB-IPAM과 L2 Announcement 기능은 온프레미스 환경의 제약을 뛰어넘어, 클라우드 환경과 거의 동일한 수준의 편리한 서비스 노출을 가능하게 합니다. 더 이상 외부 노출을 위해 복잡한 솔루션을 고민할 필요 없이, Cilium 하나로 클러스터 내부와 외부 네트워킹을 모두 해결할 수 있게 된 것입니다. 다음 스터디에서는 BGP를 연동하여 더욱 정교하고 확장성 있는 외부 라우팅을 구성하는 방법을 알아보겠습니다.

 

 

[Cilium Study] 2주차 - 1. Cilium Hubble을 통한 네트워크 관측

지난 1주 차 포스팅에서는 Cilium과 eBPF의 기본 개념을 맛보고, 복잡한 iptables의 세계에서 벗어날 수 있다는 희망을 보았습니다. 하지만 CNI를 설치하고 클러스터가 잘 동작한다고 해서 모든 것을

tech-recipe.tistory.com

 이전 Hubble 포스팅에서는 네트워크 흐름을 실시간으로 관찰하는 방법에 대해 알아보았습니다. 하지만 시스템의 상태를 지속적으로 추적하고 잠재적인 문제를 감지하기 위해서는 수치화된 데이터를 수집하고 분석하는 과정이 필수적입니다. 이것이 바로 모니터링(Monitoring) 의 영역입니다.

 이번 포스팅에서는 Cilium 환경에서 모니터링 시스템의 표준으로 자리 잡은 Prometheus(프로메테우스)Grafana(그라파나) 를 설치하고, 이를 활용하여 클러스터의 주요 메트릭을 수집하고 시각화하는 방법을 다루겠습니다.


1. 모니터링과 관측 가능성(Observability)

이미지 출처: https://cilium.io/use-cases/metrics-export/

 

시스템을 이해하는 접근 방식에는 모니터링과 관측 가능성, 두 가지 주요 개념이 있습니다.

  • 모니터링(Monitoring): CPU, 메모리 사용량과 같이 사전에 정의된 메트릭을 추적하여 시스템의 상태를 감시하고, 문제가 발생했을 때 경고를 발생시키는 활동입니다. 주로 "시스템이 다운되었는가?"와 같이 알려진 문제(Known-Unknowns) 를 감지하는 데 중점을 둡니다.
  • 관측 가능성(Observability): 로그, 메트릭, 트레이스 등 시스템이 외부로 출력하는 데이터를 통해 그 내부 상태를 추론하고, 예상치 못한 알려지지 않은 문제(Unknown-Unknowns) 의 원인을 파악하는 능력입니다. "왜 특정 요청의 지연 시간이 급증했는가?"와 같은 질문에 답할 수 있게 해줍니다.

 관측 가능성은 일반적으로 메트릭, 로그, 추적(Tracing) 이라는 세 가지 데이터를 통해 구현됩니다.

관측 가능성의 3대 요소

비교 항목 메트릭 (Metrics) 로그 (Logs) 추적 (Tracing)
정의 수치로 표현된 시계열 데이터 시스템 이벤트의 기록 단일 요청의 전체 처리 과정 추적
목적 시스템 성능 모니터링 및 경고 이벤트 분석 및 디버깅 서비스 간 호출 경로 및 병목 분석
대표 도구 Prometheus, Grafana ELK Stack, Loki Jaeger, Zipkin

2. Prometheus & Grafana 설치

 실습 환경에서는 사전 구성된 YAML 파일을 통해 Prometheus와 Grafana 스택을 간편하게 배포합니다.

Prometheus & Grafana 스택 배포

 monitoring-example.yaml 파일은 Prometheus, Grafana 및 관련 설정을 한 번에 배포합니다. 이 설정에는 Cilium 및 Hubble 대시보드가 Grafana에 자동으로 포함되는 내용이 정의되어 있습니다.

kubectl apply -f [https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/kubernetes/addons/prometheus/monitoring-example.yaml](https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/kubernetes/addons/prometheus/monitoring-example.yaml)

Cilium 및 Hubble 메트릭 활성화

 Cilium 에이전트, Operator, Hubble이 메트릭을 외부에 노출하도록 Helm 설정을 통해 활성화해야 합니다. 이 설정은 이전 실습에서 이미 완료되었습니다.

  • prometheus.enabled=true: Cilium 에이전트의 메트릭을 활성화합니다.
  • operator.prometheus.enabled=true: Cilium Operator의 메트릭을 활성화합니다.
  • hubble.metrics.enabled: Hubble의 메트릭 목록을 활성화합니다.

외부 접속을 위한 NodePort 설정

 배포된 Prometheus와 Grafana 서비스는 기본적으로 ClusterIP 타입이므로, 외부 웹 UI에서 접근하기 위해 NodePort 타입으로 변경합니다.

# Prometheus 서비스 타입을 NodePort로 변경
kubectl patch svc -n cilium-monitoring prometheus -p '{"spec": {"type": "NodePort", "ports": [{"port": 9090, "nodePort": 30001}]}}'

# Grafana 서비스 타입을 NodePort로 변경
kubectl patch svc -n cilium-monitoring grafana -p '{"spec": {"type": "NodePort", "ports": [{"port": 3000, "nodePort": 30002}]}}'

 설정 후 노드 IP와 지정된 포트(예: Prometheus - 30001, Grafana - 30002)를 통해 접속할 수 있습니다.


3. Prometheus & Grafana 사용법

Prometheus UI

 Prometheus UI는 데이터 수집 상태를 확인하고 PromQL을 테스트하는 데 유용합니다.

  • Status → Targets: Prometheus가 현재 메트릭을 수집하고 있는 대상(endpoint)들의 상태를 확인할 수 있습니다.
  • Status → Configuration: 현재 적용된 Prometheus의 설정 파일을 조회할 수 있습니다.
  • Graph: PromQL(Prometheus Query Language)을 사용해 메트릭을 직접 쿼리하고 그래프로 시각화할 수 있습니다.

Grafana UI

 Grafana는 수집된 데이터를 시각적으로 표현하는 데 사용됩니다.

  • Data Sources: Grafana가 데이터를 가져올 소스를 설정합니다. 배포된 스택에는 Prometheus가 이미 등록되어 있습니다.
  • Dashboards: Prometheus로 수집한 데이터를 시각화하는 공간입니다. monitoring-example.yaml을 통해 Cilium, Hubble 관련 대시보드가 미리 생성되어 있습니다.

4. PromQL 쿼리 예제 분석

 Grafana의 "Cilium Metrics" 대시보드에 있는 map ops 패널의 쿼리를 통해 PromQL의 동작을 이해할 수 있습니다.

예제 쿼리

topk(5, avg(rate(cilium_bpf_map_ops_total{k8s_app="cilium", pod=~"$pod"}[5m])) by (pod, map_name, operation))

단계별 분석

  1. cilium_bpf_map_ops_total{...}: cilium_bpf_map_ops_total이라는 Counter 타입 메트릭을 선택합니다.
  2. rate(...[5m]): 위 메트릭의 최근 5분간 초당 평균 증가율을 계산합니다.
  3. avg(...) by (pod, map_name, operation): 계산된 증가율을 pod, map_name, operation 레이블 기준으로 그룹화하여 평균을 계산합니다.
  4. topk(5, ...): 그룹화된 결과 중 값이 가장 큰 상위 5개를 선택하여 보여줍니다.

5. 커스텀 Grafana 대시보드 만들기

 Grafana에서는 PromQL을 사용하여 다양한 시각화 패널로 구성된 맞춤형 대시보드를 만들 수 있습니다.

패널 타입별 활용법

패널 타입 설명 예시 PromQL 쿼리
Gauge 단일 메트릭이 임계값 대비 어느 정도인지를 보여줍니다. 1 - (avg(rate(node_cpu_seconds_total{mode="idle"}[1m])) by (instance)) (노드별 CPU 사용률)
Bar chart 범주형 데이터를 막대그래프로 보여줍니다. count(kube_deployment_status_replicas_available) by (namespace) (네임스페이스별 디플로이먼트 개수)
Stat 중요한 단일 수치를 크게 표시합니다. kube_deployment_spec_replicas{deployment="nginx"} (Nginx 파드 개수)
Time series 시간에 따른 데이터 변화를 그래프로 보여줍니다. sum(rate(node_cpu_seconds_total[5m])) by (instance) (노드별 5분간 CPU 사용 변화율)
Table 데이터를 테이블 형태로 보여줍니다. node_os_info (노드 OS 정보)

6. Grafana Alerting

 Grafana를 사용하여 특정 조건이 충족되었을 때 알림을 보내도록 구성할 수 있습니다.

알림 설정 구성 요소

  • Contact points: 슬랙(Slack), 이메일 등 알림을 수신할 채널을 설정합니다.
  • Notification policy: 어떤 알림을 어떤 Contact point로 보낼지 정책을 정의합니다.
  • Alert rule: 특정 PromQL 쿼리 결과가 임계값을 넘는 등 알림을 발생시킬 조건을 생성합니다.

알림 설정 순서

  1. Contact points 설정: 알림을 받을 채널(이메일, 슬랙 등)을 구성합니다.
  2. Notification policy 정의: 알림 라우팅 규칙을 설정합니다.
  3. Alert rule 생성: 알림 발생 조건 및 임계값을 설정합니다.
  4. 테스트 및 모니터링: 설정된 알림이 정상적으로 동작하는지 확인합니다.

결론

 Prometheus와 Grafana를 통한 모니터링 시스템은 Cilium 환경에서 네트워크 성능과 보안을 효과적으로 관찰할 수 있게 해줍니다. PromQL을 활용한 메트릭 쿼리와 Grafana의 다양한 시각화 옵션을 통해 시스템의 상태를 실시간으로 파악하고, 알림 기능을 통해 문제 상황에 신속하게 대응할 수 있습니다.

 지난 포스팅에서는 Cilium과 eBPF의 기본 개념에 대해 다루었습니다. CNI가 정상적으로 배포되어 클러스터가 동작하더라도, 파드 간의 네트워크 흐름이나 적용된 정책을 직관적으로 파악하기는 어렵습니다. 이러한 내부 동작을 시각적으로 확인하고 분석하기 위해서는 별도의 관측 도구가 필요합니다.

 Cilium 환경에서 이 역할을 수행하는 것이 바로 Hubble(허블) 입니다. Hubble은 Cilium에 특화된 관측 가능성(Observability) 플랫폼으로, 이번 포스팅에서는 Hubble을 설치하고 활용하여 쿠버네티스 네트워크를 관찰하는 방법에 대해 알아보겠습니다.


1. Hubble 소개

이미지 출처: https://github.com/cilium/hubble

 Hubble은 Cilium과 eBPF를 기반으로 구축된 분산 네트워킹 및 보안 관측 플랫폼입니다. eBPF를 직접 활용하여 낮은 오버헤드로 서비스 통신, 네트워크 인프라에 대한 깊은 가시성을 제공하는 것이 핵심적인 특징입니다.

Hubble의 주요 기능

 Hubble을 통해 얻을 수 있는 주요 기능은 다음과 같습니다.

  • 서비스 의존성 및 통신 맵: 서비스 간의 통신 흐름과 의존성 그래프를 시각화하고, HTTP 호출과 같은 L7 트래픽을 확인할 수 있습니다.
  • 네트워크 모니터링 및 알림: DNS 조회 실패, TCP 연결 중단 등 네트워크 통신의 실패 여부와 원인을 파악합니다.
  • 애플리케이션 모니터링: 특정 서비스의 HTTP 4xx/5xx 응답 코드 비율이나 95/99번째 백분위수 지연 시간 등을 측정합니다.
  • 보안 관측: 네트워크 정책에 의해 차단된 연결을 확인하고, 클러스터 외부에서의 접근이나 특정 DNS 조회 이력을 추적합니다.

Hubble 구성 요소

 Hubble은 다음과 같은 주요 구성 요소로 이루어져 있습니다.

  • Hubble API: 각 노드의 Cilium 에이전트 내에서 관찰된 트래픽에 대한 인사이트를 제공합니다.
  • Hubble CLI: 로컬 유닉스 도메인 소켓을 통해 Hubble API를 쿼리하는 데 사용되는 CLI 도구입니다.
  • Hubble Relay: 클러스터 전체의 Hubble API 데이터를 집계하여 중앙화된 가시성을 제공하는 서비스입니다.
  • Hubble UI: Hubble Relay를 통해 수집된 데이터를 사용자 친화적인 서비스 맵과 필터링 기능을 갖춘 웹 인터페이스로 시각화합니다.

2. Hubble 설치 및 설정

 Cilium 설치 후, helm upgrade 또는 cilium CLI 명령을 사용하여 Hubble을 활성화할 수 있습니다.

Hubble 활성화

방법 1: helm upgrade 사용

 Hubble UI 활성화, NodePort 타입으로 서비스 노출, Prometheus 메트릭 활성화를 동시에 진행할 수 있습니다.

helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true \
  --set hubble.ui.service.type=NodePort \
  --set hubble.ui.service.nodePort=31234 \
  --set prometheus.enabled=true \
  --set operator.prometheus.enabled=true \
  --set hubble.metrics.enableOpenMetrics=true \
  --set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload}"

방법 2: cilium CLI 사용

 간단하게 Hubble과 UI를 활성화하는 경우 사용합니다.

cilium hubble enable --ui

Hubble UI 접속

 UI 서비스가 NodePort로 설정되면 아래 명령으로 접속 주소를 확인할 수 있습니다.

NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo -e "http://$NODEIP:31234"

웹 브라우저로 해당 주소에 접속한 후, kube-system 네임스페이스를 선택하여 서비스 맵을 확인합니다.

Hubble CLI 사용 및 API 접근

로컬 PC에 Hubble CLI 설치

 로컬 환경에 맞는 Hubble CLI 바이너리를 다운로드하여 설치합니다.

포트 포워딩을 통한 API 접속

 로컬 머신에서 클러스터의 Hubble Relay 서비스로 포트 포워딩을 설정하여 Hubble API에 접근할 수 있습니다.

cilium hubble port-forward &

포트 포워딩 후 hubble status 명령으로 API 접근을 확인합니다.

hubble status
# Healthcheck (via localhost:4245): Ok
# Current/Max Flows: 12,285/12,285 (100.00%)
# Flows/s: 41.28

3. Hubble 활용 실습 (Star Wars 데모)

데모 애플리케이션 배포

 Star Wars 예제 애플리케이션(deathstar, tiefighter, xwing)을 배포하여 트래픽을 생성합니다.

kubectl apply -f [https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/minikube/http-sw-app.yaml](https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/minikube/http-sw-app.yaml)

L3/L4 정책 적용 및 관찰

정책 미적용 상태 확인

 초기에는 네트워크 정책이 없으므로 xwingtiefighter 모두 deathstar 서비스에 접근할 수 있습니다. hubble observe 명령으로 트래픽을 모니터링할 수 있습니다.

# xwing에서 deathstar로 호출
kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing

# tiefighter에서 deathstar로 호출
kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing

Hubble UI에서도 xwingtiefighterdeathstar로 연결되는 것을 확인할 수 있습니다.

L3/L4 정책 적용

 org=empire 레이블을 가진 파드만 deathstar에 접근하도록 L3/L4 CiliumNetworkPolicy를 적용합니다. 이 정책은 tiefighter의 접근은 허용하지만, xwing의 접근은 차단합니다.

# sw_l3_l4_policy.yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "L3-L4 policy to restrict deathstar access to empire ships only"
  endpointSelector:
    matchLabels:
      org: empire
      class: deathstar
  ingress:
  - fromEndpoints:
    - matchLabels:
        org: empire
    toPorts:
    - ports:
      - port: "80"
        protocol: "TCP"
```bash
# 정책 적용
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/minikube/sw_l3_l4_policy.yaml

정책 적용 후 관찰

 다시 xwing에서 deathstar로의 접근을 시도하면 타임아웃이 발생합니다.

kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing --connect-timeout 2

 Hubble CLI로 차단된(DROPPED) 패킷을 확인할 수 있습니다.

hubble observe -f --type drop

L7 정책 적용 및 관찰

L7 정책의 필요성

 L3/L4 정책만으로는 같은 org=empire 레이블을 가진 tiefighter가 허용되지 않은 API(예: PUT /v1/exhaust-port)를 호출하는 것을 막을 수 없습니다.

L7 (HTTP) 정책 적용

 기존 정책(rule1)을 수정하여 tiefighterPOST /v1/request-landing API만 호출하도록 제한하는 L7 정책을 추가합니다.

# sw_l3_l4_l7_policy.yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "L7 policy to restrict access to specific HTTP call"
  endpointSelector:
    matchLabels:
      org: empire
      class: deathstar
  ingress:
  - fromEndpoints:
    - matchLabels:
        org: empire
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          path: "/v1/request-landing"
```bash
kubectl apply -f [https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/minikube/sw_l3_l4_l7_policy.yaml](https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/minikube/sw_l3_l4_l7_policy.yaml)

정책 적용 후 관찰

 tiefighter에서 허용되지 않은 exhaust-port로의 PUT 요청을 보내면 "Access denied" 응답을 받습니다.

kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port

 Hubble에서 해당 요청이 정책에 의해 차단(DROPPED)되었음을 L7 수준에서 확인할 수 있습니다.

hubble observe -f --pod deathstar --verdict DROPPED
# ... http-request DROPPED (HTTP/1.1 PUT [http://deathstar.default.svc.cluster.local/v1/exhaust-port](http://deathstar.default.svc.cluster.local/v1/exhaust-port))

4. Hubble Exporter (흐름 로그)

 Hubble Exporter는 Hubble의 흐름(flow) 로그를 파일에 저장하는 기능입니다. 파일 로테이션, 크기 제한, 필터링을 지원합니다.

설정 및 확인

 Helm 업그레이드 시 관련 플래그를 설정하여 활성화할 수 있습니다.

# helm upgrade ...
  --set hubble.export.static.enabled=true \
  --set hubble.export.static.filePath=/var/run/cilium/hubble/events.log

 Cilium 파드 내에서 로그 파일이 정상적으로 생성되는지 확인할 수 있습니다.

kubectl -n kube-system exec ds/cilium -- tail -f /var/run/cilium/hubble/events.log | jq

성능 튜닝 및 동적 설정

 allowList, denyList, fieldMask 같은 옵션을 사용하여 저장되는 로그의 양을 조절하여 성능에 미치는 영향을 최소화할 수 있습니다. 또한, ConfigMap을 통해 Cilium 파드를 재시작하지 않고도 흐름 로그 설정을 동적으로 변경할 수 있습니다.

결론

 Hubble은 Cilium과 eBPF를 기반으로 하여 Kubernetes 클러스터의 네트워크를 효과적으로 관찰하고 정책을 적용할 수 있는 강력한 도구입니다. L3/L4부터 L7까지의 네트워크 정책을 세밀하게 제어할 수 있으며, 실시간 트래픽 모니터링과 보안 관측을 통해 클러스터의 네트워크 상태를 명확하게 파악할 수 있습니다.

+ Recent posts