‘kubectl create pod’를 실행하면 발생하는 일 — kube-apiserver 감사 로그(Audit Log)로 엿보기
안녕하세요, 당근 SRE팀 클러스터 파트에서 일하고 있는 Karin(카린)이에요. 클러스터 파트는 당근 내부의 모든 서비스가 운영되는 쿠버네티스 환경을 설계하고 관리해요. 쿠버네티스의 관측가시성과 서비스 안정성을 확보하기 위해 모니터링 인프라부터 서비스 메시, 보안까지 여러 방면에서 정책과 로드맵을 설계하고 적용해요.
쿠버네티스를 처음 배우는 사람이라면 kubectl create namespace
, kubectl create -f sample-pod.yaml
명령어를 한 번쯤 실행해보셨을 거예요.
파드를 하나 생성했을 때, 쿠버네티스 내부에서 어떤 일이 일어나는지 상상해 본 적 있나요? 파드가 어떤 노드로 갈지 어떻게 알지? 파드의 MAC, IP 주소는 누가 어떻게 부여하는 거지? 컨테이너는 어떻게 만들어지지? 같은 질문들이요. 저는 쿠버네티스를 처음 배우면서부터 운영을 하고 있는 지금까지, 여전히 이 질문에 대한 답을 더 깊게 찾아가고 있는 것 같아요😢
하지만, kubernetes apiserver audit log(감사 로그)를 살펴보면서 많은 부분을 배울 수 있었어요! kube-apiserver는 모든 컴포넌트의 중심이 되는 api 서버인 만큼 요청에 대한 자세한 기록, 즉 audit log를 기록해요.
쿠버네티스의 각각의 컴포넌트가 어떤 일을 하는지 문서로만 보는 것보다, 로그를 통해 누가 어떤 요청을 보내는지 직접 보니까 훨씬 이해하기 쉬웠어요.
공식문서에서 설명하는 audit log로 알 수 있는 것들은 아래와 같아요.
- 무슨 일이 일어났는지
- 언제 일어났는지
- 누가 시작했는지
- 누구에게 일어난 일인지
- 어디에서 관측되었는지
- 어디에서 시작되었는지
- 어디로 가는지
로그에 대한 건 아래에서 더 자세히 설명할게요 🙂
이 글에서는 파드를 생성할 때 쿠버네티스의 각 컴포넌트(특히 컨트롤 플레인)가 어떤 일을 하는지 audit log를 참고하여 살펴보려고 해요. 아래 내용은 쿠버네티스 공식 문서를 참고해서 간단히 정리한 쿠버네티스 컴포넌트의 역할이에요. 이렇게 보면 누가 무슨 일을 하는지 모르겠지 않나요?? 여기서 다 이해하지 못 하는게 당연해요! 그냥 훑고 넘어가 봅시다. 이 글을 다 읽고 이 부분을 다시 한 번 봐주세요😉
컨트롤 플레인
컴포넌트: 쿠버네티스 클러스터를 운영하는 중추
- kube-apiserver : 쿠버네티스 api를 노출하는, 컨트롤 플레인의 ‘프론트엔드’ → 모든 구성요소는 apiserver를 통해 소통해요!
- etcd: 모든 클러스터 데이터를 저장하는 저장소예요.
- kube-scheduler: 새로 생성된 파드, 즉 ‘노드가 할당되지 않은’ 파드를 감지하고 노드를 배정해 주는 역할이에요.
- kube-controller-manager: 수많은 컨트롤러 프로세스를 실행하는 단일 파이너리로 컴파일된 컨트롤러예요. (컨트롤러 목록 참고)
- cloud-controller-manager: 클라우드 공급자에 따라 컨트롤 로직을 포함하는 컨트롤러로, 노드(ec2), 라우트(aws-vpc), 서비스(load balancer)와 관련된 다양한 컨트롤러 포함해요.
노드
컴포넌트: 노드를 쿠버네티스로서 작동하게 하는 요소
- kubelet: 클러스터의 각 노드에서 실행되는 에이전트로, 파드 스펙에 맞춰 컨테이너가 건강하게 동작하도록 관리해요.
- kube-proxy: 클러스터의 각 노드에서 실행되는 네트워크 프록시로, 쿠버네티스의 ‘서비스(Service)’를 구현해요.
- 컨테이너 런타임: 컨테이너 실행을 담당하는 소프트웨어 (containerd, CRI-O 등 모든 CRI(컨테이너 런타임 인터페이스) 구현체를 지원한다.)
이 그림이 바로 오늘 설명하려는 내용의 핵심 요약이라 할 수 있어요. 이 내용을 완벽하게 아시는 분이면 다음 글에서 만나뵙겠습니다. 안녕히 가세요 ~ 😆 아직 헷갈리거나 궁금한 게 더 있는 분이라면 아래 글을 쭉 함께해주세요❣️
컨트롤 플레인 컴포넌트에 대한 설명을 위에 짧게 써봤는데, 그래서 쟤네가 각각 무슨 일을 하는건데😾?? 뭐가 다른거야? 에 대한 궁금증을 해결하기 위해! 직접 그 과정을 함께 살펴봐요. kubectl create pod
를 실행할 때 컨트롤 플레인에서 발생되는 로그를 쭉 읽어보면 파드가 생성되는 과정을 그려볼 수 있어요.
위 그림에서 각 단계에 해당하는 kube-apiserver 감사 로그를 살펴볼게요.
사전준비
EKS 제어 플레인 로깅 활성화하기
aws eks 환경에서는 eks > 클러스터 > 관찰성 탭의 ‘제어 플레인(컨트롤 플레인) 로깅’ 에서 API서버, 감사, 스케줄러 로그를 켜주셔야 cloudwatch에서 확인이 가능해요.
샘플 파드(pod) 생성하기
우선, 테스트를 위한 파드 명세를 준비해요. 저는 공식 문서에 있는 가장 간단한 샘플을 이용했어요. (출처)
저는 AWS EKS의 1.28 버전을 사용했는데, audit log 내용이나 파드 명세는 클러스터 버전에 따라 살짝 다를 수 있어요.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
kubectl create -f pod.yaml -n test 를 실행하니 1초도 안 걸려서 파드가 생성되었어요.
kubectl get pod -n test
명령어로 조회해보면 아래와 같이 방금 생성된 파드에 대한 상세 내용을 볼 수 있어요.
(여기서 잠깐! 제가 만든 건 nginx 컨테이너 하나만 있는 파드인데, 얘네는 왜 이렇게 복잡하죠 😵💫
라는 생각이 들텐데, 쿠버네티스에서는 파드에 필요한 default 요소를 미리 기본 스펙으로 준비해 두고, 내가 제공한 명세의 내용을 패치(patch) 해주는 식으로 파드 스펙을 완성해요. 고로, 아래에 나오는 저 복잡한 것들을 필요에 따라 바꿔서 적용할 수 있어요.)
# kubectl get pod -n test -o yaml
apiVersion: v1
items:
- apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2024-04-14T08:30:59Z"
name: nginx
namespace: test
resourceVersion: "2169746"
uid: 16cfa6a0-8da1-4134-b9f5-7036fc0bddfc
spec:
containers:
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-7rh8d
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: ip-10-10-23-220.ap-northeast-2.compute.internal
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: kube-api-access-7rh8d
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-04-14T08:31:00Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-04-14T08:31:00Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-04-14T08:31:00Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-04-14T08:31:00Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://1ef282dc82547b2f5ba1fd90edaf596f60cecc29fb8621c903822f787ce9a0ef
image: docker.io/library/nginx:1.14.2
imageID: docker.io/library/nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d
lastState: {}
name: nginx
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2024-04-14T08:31:00Z"
hostIP: 10.10.23.220
phase: Running
podIP: 10.10.23.116
podIPs:
- ip: 10.10.23.116
qosClass: BestEffort
startTime: "2024-04-14T08:31:00Z"
kind: List
metadata:
resourceVersion: ""
audit log 보는 꿀팁
audit log를 처음 보면 당황스럽기 때문에… 가장 간단한 형태의 로그를 보며 어떤 필드 위주로 보면 좋을지 짚어볼게요. 위의 로그는 kubelet이 파드 정보를 조회한다는 내용이에요.
requestURI
- kube-apiserver의 API path를 나타내요. (나 여기로 요청한다!)
- ‘어떤 자원에 대한 요청이냐’로 이해할 수 있어요.
- 여기서는 ‘my-namespace의 my-pod’ 라는 자원에 대한 요청이겠죠?
verb
- kube-apiserver로 어떤 액션을 했는지 나타내요. (나 이거 한다!)
- 기본적으로 standatd HTTP verbs와 같지만, 쿠버네티스만이 쓰는 별도의 verbs도 있어요. (watch, list, get, post, put, patch, delete)
- 여기서는 파드에 대해 get 요청을 하고 있네요. 파드의 상태와 스펙을 조회만 하는 목적이겠죠?
userAgent
- 요청하는 주체를 나타내요. (나 누구다!)
- 이 부분이 kube-apiserver, kube-scheduler, kubelet 중 누구인지 집중해서 봐주세요!
- 여기서는 kubelet이 요청했어요. 저 파드가 올라가 있는 노드의 kubelet이겠네요.
objectRef
, requestObject
, responseObject
+ 그 외의 object
친구들
- 요청에 대한 상세 내역을 확인할 수 있어요. (나 뭐 한다!)
- ‘object’라는 단어가 들어간 필드를 잘 보면 정보가 많답니다 😉
파드(pod) 생성 과정 살펴보기
준비가 끝났으니! 이제 kubectl create -f pod.yaml -n test
명령어가 어떤 과정을 거치며 파드를 생성했는지 kube-apiserver 감사 로그로 살펴볼게요. (watch에 대한 로그는 첨부하지 않았어요 💦) 크게 7단계로 구분할 수 있어요.
- kube-scheduer(컨트롤 플레인) & kubelet(노드 컴포넌트)은 파드 생성 요청 감시 중 (watch)
kubectl create pod -n test
실행하여 파드 생성 요청- kube-scheduler는 파드가 할당될 노드 선정
- 노드의 kubelet은 파드 생성을 시작하며 kube-apiserver에 상태 업데이트
- 컨테이너 생성의 각 단계에서 이벤트를 생성하며 상태 업데이트
- 파드 생성이 완료되며 상태 업데이트
kubectl create pod -n test
명령어 완료
1. 파드 생성 요청 감시 중 (watch)
kube-scheduler와 kubelet은 kube-apiserver를 계속 쳐다보고 있어요. kube-apiserver에 watch 요청을 해서 현재 상태와 다른 점을 발견하기를 기다려요! kube-apiserver는 etcd에 저장된 상태를 알려줘요.
2. 파드 생성 요청
kubectl create pod -n test
를 실행하면, kube-apiserver와 kubectl은 사용자가 요구한 바를 처리하기 위해 연쇄적인 요청을 주고받고, 이 부분은 사용자에게 보이지 않아요. kubectl
을 간단히 설명하면, 컨트롤 플레인과 통신하기 위한 커맨드라인 툴이에요. kube-apiserver와 REST API로 통신하며 사용자가 쿠버네티스를 변경할 수 있게 도와줘요. 이 부분은 여기서 자세히 다루지는 않을게요😂 우리는 kube-apiserver가 어떤 일을 시작하는지 살펴보러 가봅시다.
kubectl의 요청을 받은 kube-apiserver는 kube-apiserver로 새로운 요청을 보내네요.
kube-apiserver: /api/v1/namespaces/test/limitranges 에 요청한다! test 네임스페이스의 limitranges를 알려줘라!
kube-apiserver: /api/v1/namespaces/test/resourcequotas 에 요청한다! test 네임스페이스의 resourcequotas를 알려줘라!
limitranges, resourcequotas는 네임스페이스의 컴퓨팅 리소스 제한에 대한 오브젝트예요. ‘a라는 네임스페이스는 cpu 총 사용량을 3으로 제한하고 싶어’같은 제약을 지정할 수 있어요. 자세한 내용은 공식문서를 참고해주세요 :)
kube-apiserver가 limitranges, resourcequotas를 조회해서 test 네임스페이스에 파드가 들어가도 되는 상태이지 확인하는 모습이에요. 네임스페이스가 이미 리소스를 꽉 채워 사용하고 있다면 파드를 더 추가되면 안되겠죠? 그래서 클러스터의 노드 상태와는 완전 별개로 일단 네임스페이스 상태를 체크하는 것을 것을 감사 로그를 통해 확인할 수 있어요.
이 단계를 통과하지 못 하면 파드를 생성하지 않고 kubectl에 생성 실패를 응답하고 끝나요!
3. 파드가 들어갈 노드 선정
네임스페이스에 파드를 생성할 수 있는 상태이기 때문에, kubectl은 이어서 파드 생성 요청을 해요.
아래 로그는 kube-apiserver가 kubectl에 응답한 내용이에요.
kubectl : /api/v1/namespaces/test/pods 로 요청한다! 내가 준 pod.yaml 내용을 기반으로 파드를 생성해라!
kube-apiserver: 너가 준 스펙에서 이것저것 기본값을 붙여 파드 생성 요청을 받아들였다! 너의 파드는 아직 Pending 상태이다!
위에 1번에서 kubelet과 kube-scheduler는 kube-apiserver를 감시하고 있다고 언급했죠?
이 단계에서,
- kubectl → kube-apiserver에 create 요청을 보내고
- kube-apiserver는 그 내용을 etcd에 저장해요.
- kube-scheduler는 kube-apiserver가 응답하는 ‘현재 상태’가 바뀌면서 새 파드가 생성되었다는걸 알게 돼요.
- kube-scheduler는 노드가 할당되지 않은 파드를 어떤 노드에 할당해야 할 지 계산해서 알려줘요. (스케쥴링 될 노드를 결정할 때 굉장히 많은 요소를 고려하게 되는데, 이 글에선 다루지 않을게요.)
kube-scheduler가 노드를 하나 선정했네요! 이제 이 노드와 파드를 binding 해달라고 kube-apiserver에 요청했어요.
kube-scheduler:
/api/v1/namespaces/test/pods/nginx/binding
에 요청한다! ip-10-10-23-220.ap-northeast-2.compute.internal 노드에 nginx 파드를 할당해라!
‘ip-10–10–23–220.ap-northeast-2.compute.internal 노드에 test 네임스페이스의 파드가 성공적으로 바인딩 되었습니다!’ 라는 로그가 kube-scheduler에도 남았네요 👍🏻 이제 kube-scheduler의 역할은 여기서 끝❗
I0414 06:41:08.738797 9 schedule_one.go:286] "Successfully bound pod to node" pod="test/nginx" node="ip-10-10-23-220.ap-northeast-2.compute.internal" evaluatedNodes=1 feasibleNodes=1
4. kubelet이 파드 생성 & 상태 보고 + 5. 이벤트 생성
이제 ‘컨트롤 플레인’의 역할이 끝나고, ‘노드 컴포넌트’가 활약할 시간이에요❗
kube-scheduler가 파드를 할당한 노드(ip-10–10–23–220.ap-northeast-2.compute.internal)의 kubelet은 ‘오! 내가 만들어야 하는 파드가 있구나!’ 바로 알아차리고 파드 생성을 시작해요.
kube-apiserver 로그를 보면 kubelet이 어떤 일을 하고 있는지 알 수 있어요.
- kubelet은 내가 만들어야 하는 파드의 정보를 가져와요
kubelet: /api/v1/namespaces/test/pods/nginx 에 요청한다! 파드 정보 줘라!
2. 파드를 생성하며 kube-apiserver에 상태 변화를 업데이트해요.
kubelet:
/api/v1/namespaces/test/pods/nginx/status
에 요청한다! 파드 상태를 아래와 같이 업데이트 해라!
- 파드 상태
Initialized: True
Ready: False
ContainersReady: False
PodScheduled: True
- 컨테이너 상태
waiting (ContainerCreating)
3. 컨테이너를 생성하며 관련된 events
를 생성해요.
kubelet은 노드 컴포넌트인 CRI, CNI 구현체를 이용하여 컨테이너를 생성해요. 그리고 각 단계에서 생기는 events
를 기록해요.
CRI, CNI 구현체는 노드 컴포넌트로서 각각 ‘컨테이너 실행’과 ‘컨테이너에 네트워크 제공’이라는 책임을 갖고 있어요. eks에서는 containerd와 vpc-cni가 각각 담당하고 있어요.
(노드 컴포넌트에 대해 자세히 다루는 글을 따로 써보고 싶네요 😉)
kubelet이 컨테이너를 생성할 때 필요한 Image를 불러와요. Image Pull Policy(공식문서 참고)에 따라 다른데요, 이미지를 항상 불러올 수도 있고, 이미 머신에 존재한다면 불러오지 않을 수도 있어요.
이미지를 불러와야 하는 상황이면 Pulling 이벤트가 발생하고, 성공적으로 불러왔으면 Pulled 이벤트가 발생해요.
이번에는 IfNotPresent 정책을 사용했는데 머신에 nginx 이미지가 있어서 바로 Pulled
됐네요.
이때 문제가 발생하면 FailedToPullImage
, ImagePullBackOff
등의 이벤트가 발생해요.
kubelet: /api/v1/namespaces/test/events 에 요청한다! 컨테이너 이미지 Pulled 이벤트를 생성해라! nginx:1.14.2 이미지는 이미 있었다!
kubelet이 컨테이너를 성공적으로 생성하고 실행했어요! Created
, Started
이벤트를 볼 수 있어요.
Created
과 Started
의 차이는 도커에서의 상황과 유사해요. 이미지를 기반으로 컨테이너를 생성하는 것까지가 Created
이고, 그 후 cmd를 실행하거나 run을 하여 컨테이너를 실행된 상태가 Started
이에요.
여기서 문제가 생기면 Failed
, FailedCreatePodContainer
, NetworkNotReady
등의 이벤트가 발생해요.
kubelet: /api/v1/namespaces/test/events 에 요청한다! 컨테이너 Created 이벤트를 생성해라! nginx 컨테이너가 생성되었다!
kubelet: /api/v1/namespaces/test/events 에 요청한다! 컨테이너 Started 이벤트를 생성해라! nginx 컨테이너가 시작되었다!
6. 파드 생성 완료!
파드 스펙에 명시된 컨테이너가 성공적으로 생성되었으니, kubelet은 kube-apiserver에 상태를 업데이트해요.
kubelet: /api/v1/namespaces/test/pods/nginx/status 에 요청한다! 파드 상태를 아래와 같이 업데이트 해라!
- 파드 상태
Initialized: True
Ready: True (← updated!)
ContainersReady: True (←updated!)
PodScheduled: True
- 컨테이너 상태
Running
- 파드의 IP 정보! 도 알려줘야겠죠?
10.10.23.4
7. kubectl 응답
kubectl이 응답을 받으며 마무리 됩니다. 끝~
요약 정리 & 당근의 활용 사례
지금까지 kube-apiserver의 감사 로그를 통해 파드가 생성되는 과정을 간단하게 살펴봤어요. 각 구성요소의 역할과 책임이 매우 뚜렷한 게 보이지 않나요?
세 줄 요약!
kube-apiserver
는 etcd(저장소)와 통신하는유일한
컴포넌트로, 모든 구성요소는kube-apiserver
를 중심으로 소통한다.kube-scheduler
는 노드와 파드의 다양한 요소를 고려하여파드에 노드를 할당
, 즉 스케줄링 해주는 역할만 한다.kubelet
은 노드 컴포넌트와 상호작용하여 컨테이너를 운영하고, 각 단계를 kube-apiserver를 통해 업데이트한다.
kube-controller-manager는 이 글에서 다루지 않았어요. 파드만 생성할 때는 개입할 일이 없거든요. 파드 외의 다른 오브젝트를 생성할 때는 이 친구가 필요해요. 예를 들어 쿠버네티스를 쓰면 보통 deployment로 파드를 관리하는데, 그때 kube-controller-manager의 deployment-controller가 그 일을 담당하게 됩니다.
kube-apiserver audit log
와도 조금 친해지는 계기가 되었으면 좋겠어요. audit log를 잘 활용하면 보안 가시성을 높일 수 있고, 트러블슈팅에도 큰 도움이 돼요. 당근 SRE팀에서는 보안 가시성을 높이기 위해 audit log와 aws의 eventbridge 기능을 사용해서 ‘엔지니어들이 클러스터에 kubectl로 취하는 액션’을 모두 슬랙 메시지로 전송하고 있어요.
이 외에, 노드 컴포넌트가 컨테이너를 생성하고 실행하는 과정도 과정도 매우 복잡하고 재밌어요. 파드가 IP를 어떻게 할당받는지, 컨테이너가 어떻게 실행되는지는 이 글에서 다루지 못해서 아쉬운데, 다른 글로 찾아올 기회가 있다면… 언젠가… 🥹
쿠버네티스 운영 업무를 하면서 쿠버네티스가 얼마나 방대하고 멋진 프로젝트인지 계속 깨닫고 있어요. 팀원들과 대화하다보면 서로 모르는 지식을 얻기도 해요.
당근 SRE팀이 어떤 일을 하는지는 ‘SRE MEET UP’ 영상을 통해 더 자세히 알 수 있어요. 특히 클러스터 파트의 업무가 궁금하신 분들은 아래 영상을 참고하세요 :-)