Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Kind로 Kubernetes control plane의 로그 레벨 설정하기

Kubernetes에서 동작을 확인해 보고 싶은 게 있어서 Kubernetes의 control plane의 로그를 좀 더 보고 싶었다. 실제 운영하는 클러스터의 로그 레벨을 수정할 수는 없고 수정한다고 하더라도 너무 많은 게 돌고 있어서 확인이 어려우니 로컬에서 Kubernetes 클러스터를 실행할 수 있는 kind를 써보기로 했다. kind는 이전 그룹스터디에서 다른 분이 사용하는 건 본 적이 있는데 실제로 써본 것은 처음이다.

설치 문서에 따라 설치하면 되는데 릴리스 바이너리를 다운받아서 설치했다.

클러스터는 kind create clusterkind delete cluster로 바로 만들고 지우고 할 수 있을 정도로 사용법이 쉬웠지만 나는 control plane의 옵션을 설정하고 싶어서 문서를 많이 찾아봐야 했다. 기본으로 쓰려면 그냥 명령어만 사용해도 되지만 구성 파일을 만들어서 좀 더 구체적인 설정을 할 수 있다.

클러스터 구성 파일

다음과 같이 YAML 파일을 구성해서 컨트롤 플레인 1대 워커 2대로 클러스터를 구성할 수 있는데 자세한 내용은 문서예시 설정 파일을 참고하면 된다.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315
- role: worker
  image: kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315
- role: worker
  image: kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315

노드에서 image는 꼭 지정하지 않아도 되는데 나는 특정 Kubernetes 버전을 쓰고 싶어서 지정했다. 이 이미지와 SHA는 릴리스 페이지를 보면 Kubernetes 버전별로 이미지가 지정되어 있으니 이를 가져다가 사용하면 된다.

위 파일이 kind-config.yaml라고 하면 다음과 같이 클러스터를 생성할 수 있다.

$ kind create cluster --config kind-config.yaml
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.24.7)
 ✓ Preparing nodes 
 ✓ Writing configuration 
 ✓ Starting control-plane
 ✓ Installing CNI 
 ✓ Installing StorageClass 
 ✓ Joining worker nodes 
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community


kube-controller-manager의 로그 레벨 설정

Kubernetes에서는 공유상태를 감시하면서 루프를 돌면서 컨트롤러가 처리하는데 kube-controller-manager가 이를 처리하는 데몬이다. 컨트롤러의 동작을 확인하고 싶었기에 옵션을 봤더니 -v, --v int로 로그 레벨의 verbosity를 설정할 수 있다는 것을 알게 되었다.

Kubernetes는 로깅에 klog를 사용하는데 로깅 문서를 보면 klog.ErrorSklog.InfoS 두 가지로 크게 갈리고 로그 레벨은 0~5까지 있는데 기본값을 2레벨이다. 문서에 나온 로그 레벨을 정리해 보면 다음과 같다.

  • klog.V(0).InfoS = klog.InfoS - 클러스터 운영자가 항상 볼 수 있도록 하는데 유용하다.

    • 프로그래머 오류
    • 패니겡 대한 추가 정보
    • CLI 인자 처리
  • klog.V(1).InfoS - 장황하길 원하지 않는다면 합리적인 기본 로그 레벨

    • 구성에 관한 정보(X를 리스닝하고 Y를 워칭함)
    • 수정할 수 있는 조건과 관련하여 자주 반복되는 오류(언헬시로 탐지된 Pod)
  • klog.V(2).InfoS - 서비스에 대한 유용한 정상 상태 정보, 시스템에 중요한 변경 사항과 관련 있을 수 있는 중요한 로그 메시지가 포함된다. 대부분의 시스템에서 권장하는 기볼 로그 레벨이다.

    • HTTP 요청과 종료 코드를 로깅
    • 시스템 상태 변경(pod 삭제)
    • 컨트롤러 상태 변경 이벤트(팟 시작)
    • 스케쥴러 로그 메시지
  • klog.V(3).InfoS - 변경에 관한 확장된 정보

    • 시스템 상태 변경에 대한 추가 정보
  • klog.V(4).InfoS - 디버그 레

    • 나중에 다시 돌아와서 확인하고 싶을 수 있는 특히 까다로운 부분을 로깅
  • klog.V(5).InfoS - 트레이스 레벨

    • 오류와 경고로 이어지는 단계를 이해하기 위한 컨텍스트
    • 보고된 이슈를 트러블슈팅하기 위한 추가 정보

이제 로깅 옵션을 알았으니 이를 클러스터에 적용해야 한다. 먼저 kube-system 네임스페이스에서 kube-controller-manager Pod을 확인하면 kube-controller-manager를 실행하면서 다음 옵션이 설정된 것을 확인할 수 있다. verbosity 옵션이 없는 걸로 보아 기본 레벨인 2가 적용되었다고 생각했다.

$ kubectl -n kube-system get pod/kube-controller-manager-kind-control-plane -o yaml
apiVersion: v1
kind: Pod
metadata:
  name: kube-controller-manager-kind-control-plane
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
    - --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --bind-address=127.0.0.1
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --cluster-cidr=10.244.0.0/16
    - --cluster-name=kind
    - --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
    - --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
    - --controllers=*,bootstrapsigner,tokencleaner
    - --enable-hostpath-provisioner=true
    - --kubeconfig=/etc/kubernetes/controller-manager.conf
    - --leader-elect=true
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --root-ca-file=/etc/kubernetes/pki/ca.crt
    - --service-account-private-key-file=/etc/kubernetes/pki/sa.key
    - --service-cluster-ip-range=10.96.0.0/16
    - --use-service-account-credentials=true

kind는 kubeadm을 사용하므로 kubeadm의 구성을 수정하는 방법을 제공하고 있다. 앞에서 작성한 kind-config.yaml 파일에 다음과 같이 kubeadmConfigPatches를 추가했다.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
  kind: ClusterConfiguration
  controllerManager:
      extraArgs:
          v: "5"
nodes:
# 생략

설정은 간단하지만, 처음에는 어떻게 하는지 몰라서 찾는 데 좀 오래 걸렸다. 여기서 v 옵션은 숫자를 넘기지만 여기서는 문자열로 넘겨야 한다. 숫자로 넘기면 json: cannot unmarshal number into Go struct field ControlPlaneComponent.controllerManager.extraArgs of type string 오류가 발생한다.

이제 다시 클러스터를 띄운 후 kube-controller-manager의 설정을 살펴보면 다음과 같이 마지막에 v 옵션이 추가된 것을 볼 수 있다.

$ kubectl -n kube-system get pod/kube-controller-manager-kind-control-plane -o yaml
apiVersion: v1
kind: Pod
metadata:
  name: kube-controller-manager-kind-control-plane
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
    - --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --bind-address=127.0.0.1
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --cluster-cidr=10.244.0.0/16
    - --cluster-name=kind
    - --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
    - --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
    - --controllers=*,bootstrapsigner,tokencleaner
    - --enable-hostpath-provisioner=true
    - --kubeconfig=/etc/kubernetes/controller-manager.conf
    - --leader-elect=true
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --root-ca-file=/etc/kubernetes/pki/ca.crt
    - --service-account-private-key-file=/etc/kubernetes/pki/sa.key
    - --service-cluster-ip-range=10.96.0.0/16
    - --use-service-account-credentials=true
    - --v=5

kube-controller-manager Pod의 로그를 보면 다음과 같이 로그가 남는데 로그에 로그 레벨은 표시가 안 되기 때문에 헷갈리지만 없을때와 비교하면 훨씬 많은 로그가 남겨진 것을 볼 수 있다. 이제 컨트롤러의 로그를 좀 더 상세하게 살펴볼 수 있다.

I0313 10:49:55.524101       1 discovery.go:214] Invalidating discovery information
I0313 10:49:55.731929       1 graph_builder.go:632] GraphBuilder process object: coordination.k8s.io/v1/Lease, namespace kube-system, name kube-controller-manager, uid 13886ed9-eb76-4139-be45-381965fd5ba8, event type update, virtual=false
I0313 10:49:55.732106       1 leaderelection.go:278] successfully renewed lease kube-system/kube-controller-manager
I0313 10:49:55.750466       1 graph_builder.go:632] GraphBuilder process object: coordination.k8s.io/v1/Lease, namespace kube-system, name kube-scheduler, uid e27f7933-a9cc-452a-942e-cc1abb752939, event type update, virtual=false
I0313 10:49:57.756106       1 graph_builder.go:632] GraphBuilder process object: coordination.k8s.io/v1/Lease, namespace kube-system, name kube-controller-manager, uid 13886ed9-eb76-4139-be45-381965fd5ba8, event type update, virtual=false
I0313 10:49:57.756193       1 leaderelection.go:278] successfully renewed lease kube-system/kube-controller-manager
I0313 10:49:57.756660       1 graph_builder.go:632] GraphBuilder process object: coordination.k8s.io/v1/Lease, namespace kube-system, name kube-scheduler, uid e27f7933-a9cc-452a-942e-cc1abb752939, event type update, virtual=false
I0313 10:49:58.529421       1 gc_controller.go:214] GC'ing orphaned
I0313 10:49:58.529465       1 gc_controller.go:277] GC'ing unscheduled pods which are terminating.
I0313 10:49:58.750836       1 pathrecorder.go:241] controller-manager: "/healthz" satisfied by exact match
I0313 10:49:58.750871       1 pathrecorder.go:241] healthz: "/healthz" satisfied by exact match
I0313 10:49:58.750979       1 httplog.go:131] "HTTP" verb="GET" URI="/healthz" latency="195.167µs" userAgent="kube-probe/1.24" audit-ID="" srcIP="127.0.0.1:40960" resp=200


kube-apiserver의 Audit 로그 설정

Kubernetes는 모든 걸 API로 다루기 때문에 kube-apiserver를 거쳐 가게 된다. kube-apiserver에도 로그가 있고 로그 레벨의 verbosity를 설정할 수 있지만 kube-apiserver에는 감사 정책을 설정할 수 있는 기능이 있다. 이 감사 로그를 기록하면 API 서버를 거치는 이벤트를 기록할 수 있다.

  • None - 이 규칙에 해당하는 이벤트는 로깅 하지 않는다.
  • Metadata - 요청 메타데이터(요청하는 사용자, 타임스탬프, 리소스, 동사(verb) 등)는 로깅 하지만 요청/응답 본문은 로깅 하지 않는다.
  • Request - 이벤트 메타데이터 및 요청 본문을 로깅 하지만 응답 본문은 로깅 하지 않는다. 리소스 외의 요청에는 적용되지 않는다.
  • RequestResponse - 이벤트 메타데이터 및 요청/응답 본문을 로깅한다. 리소스 외의 요청에는 적용되지 않는다.

최근 트러블슈팅하면서 AWS EKS 컨트롤 플레인의 로그를 보려고 CloudWatch에서 봤던 로그도 이 감사 로그라는 것을 알게 되었다.

이를 kind에서 설정해야 했는데 다행히도 Auditing 문서가 있어서 쉽게 할 수 있었다. 먼저 아래 내용으로 audit-policy.yaml 파일을 생성한다.

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse

너무 많아서 보기가 쉽진 않지만, 요청과 응답을 다 같이 보고 싶었기 때문에 RequestResponse로 설정했다. 그리고 이 파일을 마운트해서 클러스터 설정에 적용되도록 kind-config.yaml 파일을 다음과 같이 수정한다.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
  kind: ClusterConfiguration
  controllerManager:
      extraArgs:
          v: "5"
- |
  kind: ClusterConfiguration
  apiServer:
    extraArgs:
      audit-log-path: /var/log/kubernetes/kube-apiserver-audit.log
      audit-policy-file: /etc/kubernetes/policies/audit-policy.yaml
    extraVolumes:
      - name: audit-policies
        hostPath: /etc/kubernetes/policies
        mountPath: /etc/kubernetes/policies
        readOnly: true
        pathType: "DirectoryOrCreate"
      - name: "audit-logs"
        hostPath: "/var/log/kubernetes"
        mountPath: "/var/log/kubernetes"
        readOnly: false
        pathType: DirectoryOrCreate
nodes:
- role: control-plane
  image: kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315
  extraMounts:
  - hostPath: ./audit-policy.yaml
    containerPath: /etc/kubernetes/policies/audit-policy.yaml
    readOnly: true
- role: worker
# 생략

감사로그는 kube-controller-manager처럼 Pod의 로그가 아니라 위 설정대로 Audit 로그 파일로 컨테이너 안에 남겨지는 것이다. Kind가 로컬의 Docker를 그대로 이용하기 때문에 실행된 kind-control-plane 도커 이미지를 통해서 kube-apiserver의 Audit 로그를 다음과 같이 볼 수 있다.

$ docker exec kind-control-plane cat /var/log/kubernetes/kube-apiserver-audit.log

{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"65eb1733-508b-49eb-b968-d40bc64d4523","stage":"RequestReceived","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["172.21.0.4"],"userAgent":"kube-probe/1.24","requestReceivedTimestamp":"2023-03-13T11:23:51.227412Z","stageTimestamp":"2023-03-13T11:23:51.227412Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"65eb1733-508b-49eb-b968-d40bc64d4523","stage":"ResponseComplete","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["172.21.0.4"],"userAgent":"kube-probe/1.24","responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2023-03-13T11:23:51.227412Z","stageTimestamp":"2023-03-13T11:23:51.229685Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\""}}

추가로 로그가 너무 많아서 보기 어렵다면 kind에서 익스포트 기능을 제공해 주고 있으므로 로컬에 내려받아서 살펴볼 수 있다.

2023/03/13 20:35 2023/03/13 20:35