New user sign up using AWS Builder ID
New user sign up using AWS Builder ID is currently unavailable on re:Post. To sign up, please use the AWS Management Console instead.
Karpenter 로그를 통해 EKS Worker Node의 Lifecycle Event를 추출하고 한눈에 파악하는 방안
해당 기사에서는 EKS의 Node Scheduler 중 하나인 Karpenter 사용시 Karpenter 로그를 이용하여 Node의 Lifecycle 이벤트를 CSV 파일로 추출하고 활용하는 방안에 대해 알아봅니다.
본 기사는 EKS Auto Mode에서는 호환되지 않습니다. EKS Auto Mode에서는 Karpenter 로그 추출이 지원되지 않습니다.
Karpenter에 의해 관리되는 Node의 Lifecycle
Karpenter는 쿠버네티스 클러스터에서 노드의 동적 프로비저닝을 관리하는 오픈소스 노드 수명주기 관리 프로젝트로, NodeClaim이라는 고유한 객체를 통해 노드를 생성하고 관리합니다. Karpenter는 NodePool과 NodeClass를 지속적으로 모니터링하면서, 클러스터의 요구사항에 따라 노드의 생명주기를 관리합니다. 새로운 노드 생성은 주로 두 가지 상황에서 발생합니다. 첫 번째는 새로운 파드의 요구사항이 기존 노드의 리소스나 조건과 맞지 않을 때입니다. 이러한 경우 Karpenter는 NodeClaim을 생성하여 적절한 사양의 새로운 노드를 프로비저닝합니다. 두 번째는 기존 노드의 중단이 필요한 상황에서 발생합니다. Karpenter는 다양한 노드 중단 메커니즘을 보유하고 있으며, 노드 중단이 예정된 경우 해당 노드를 대체하기 위한 새로운 NodeClaim을 미리 생성하여 워크로드의 연속성을 보장합니다. 이러한 방식으로 Karpenter는 클러스터의 리소스 요구사항과 노드의 상태를 지속적으로 평가하면서, 효율적이고 안정적인 노드 관리를 수행합니다.
Karpenter는 노드의 생성과 삭제 과정에서 체계적이고 일관된 단계별 절차를 따릅니다.
Node 생성 단계 (참고문서)
- Create NodeClaim: Karpenter는 배포(Provisioning) 혹은 중단(Disrupting) 요구에 따라 새로운 NodeClaim을 생성합니다.
- Launch NodeClaim: AWS에 새로운 EC2 Instance를 생성하기 위해 CreateFleet API를 호출합니다.
- Register NodeClaim: EC2 Instance가 생성되고 Cluster에 등록된 Node를 NodeClaim과 연결합니다.
- Initialize NodeClaim: Node가 Ready 상태가 될 때까지 기다립니다.
Node 삭제 단계 (참고문서)
- Disrupt NodeClaim: Karpenter는 Consolidation, Drift 혹은 Interruption의 이유로 중단 가능한 Node를 발견하고 해당 NodeClaim을 중단합니다.
- Taint Node: Node에 "karpenter.sh/disrupted:NoSchedule" Taint를 추가하여 해당 노드로 Pod이 스케쥴링 되지 못하도록 합니다.
- Delete Node: Node를 종료하도록 AWS에 TerminateInstances API를 호출하고 Cluster에서 Node를 삭제합니다.
- Delete NodeClaim: NodeClaim을 삭제합니다.
Karpenter는 모든 단계별 작업이 완료된 후 해당 작업의 세부 내용을 시스템 로그에 기록합니다. 아래는 해당 로그 중 일부의 예시입니다.
## Create NodeClaim
{"level":"INFO","time":"2024-12-31T09:50:28.720Z","logger":"controller","message":"created nodeclaim","commit":"0a85efb","controller":"provisioner","namespace":"","name":"","reconcileID":"63c2695c-4c54-4a9b-9b64-1804d9ddbb82","NodePool":{"name":"default"},"NodeClaim":{"name":"default-abcde"},"requests":{"cpu":"1516m","memory":"1187Mi","pods":"17"},"instance-types":"c4.large, c4.xlarge, c5.large, c5.xlarge, c5a.2xlarge and 55 other(s)"}
## Disrupting NodeClaim
{"level":"INFO","time":"2024-12-31T010:50:03.419Z","logger":"controller","message":"disrupting nodeclaim(s) via replace, terminating 1 nodes (4 pods) ip-10-0-0-10.ap-northeast-2.compute.internal/c5a.large/on-demand and replacing with on-demand node from types c5a.large, c7i-flex.large, c5.large, c6i.large, c7i.large and 55 other(s)","commit":"0a85efb","controller":"disruption","namespace":"","name":"","reconcileID":"8a0853b1-888b-486b-a64f-33dcd07a7c01","command-id":"198a3f19-6857-4bef-8606-fc6f8e9d3d0e","reason":"drifted"}
쿠버네티스 클러스터에서 각 노드는 고유한 NodeClaim과 1:1로 매핑되며, 이 NodeClaim은 특정 NodePool에 소속됩니다. 노드를 식별하는 주요 속성으로는 다음과 같은 것들이 있습니다.
- NodePool: NodeClaim이 속한 NodePool의 이름
- NodeClaim: Node와 연결된 NodeClaim의 ID
- NodeName: 쿠버네티스 시스템에서 인식하고있는 Node 이름 (주로 EC2 Instance의 Private DNS Name 사용)
- InstanceId: Node에 대응하는 EC2 Instance의 ID
Karpenter는 이러한 속성들을 로그에 기록하지만, 대부분의 경우 각 로그 항목에는 이러한 속성들의 일부만 포함됩니다. 예를 들어, 어떤 로그에는 NodeClaim만 기록되어 있고, 다른 로그에는 NodeName만 기록되어 있을 수 있습니다. 이러한 로그 구조로 인해 특정 노드의 전체 생명주기를 파악하기 위해서는 각 로그에 기록된 노드 속성들을 상호 연관지어 분석해야 합니다. 특히 대규모 클러스터에서 여러 NodePool을 운영하고 많은 수의 노드를 관리하는 경우, 이러한 로그 분석 작업은 상당한 시간과 노력을 필요로 합니다.
Karpenter 로그 분석 스크립트
따라서 본 기사에서는 Karpenter 로그 분석을 자동화하기 위한 Python 스크립트를 제공합니다. 이 스크립트는 Karpenter 로그를 파싱하고, 각 로그 항목에서 노드 식별 속성을 추출하여 이를 체계적으로 정리된 CSV 파일로 변환합니다. 다음은 이 작업을 수행하기 위한 단계별 절차를 설명합니다.
1. Karpenter 로그 추출
다음의 명령어를 통해 Karpenter 로그를 추출합니다. 파일 이름을 karpenter.log
로 저장합니다.
## Karpenter는 일반적으로 kube-system Namespace에 설치하는것이 일반적이지만, 다른 Namespace에 설치한 경우 아래 명령어에서 Namespace를 적절히 교체합니다. $ kubectl logs deploy/karpenter --namespcae kube-system > karpenter.log
2. Karpenter 로그 분석 스크립트 저장
다음의 스크립트를 복사하여 karpenter-log-analyzer.py
로 저장합니다.
import json import re import csv from dataclasses import dataclass, asdict from typing import List, Optional from operator import itemgetter @dataclass class Event: time: str level: str message: str full_log: str @dataclass class Node: nodePool: str = "" nodeClaim: str = "" instanceId: str = "" nodeName: str = "" isAlive: bool = True events: List[Event] = None def __post_init__(self): self.events = self.events or [] class KarpenterLogParser: def __init__(self, log_file_path: str): self.log_file_path = log_file_path self.nodes: List[Node] = [] self.not_node_events: List[Event] = [] def find_node(self, node_params: dict) -> Optional[Node]: for param in [node_params.get("nodeClaim"), node_params.get("nodeName")]: if not param: continue for node in self.nodes: if node.isAlive and (node.nodeClaim == param or node.nodeName == param): return node return None def process_node_event(self, node_params: dict, event_params: dict) -> None: node = self.find_node(node_params) if not node: node = Node(**node_params) self.nodes.append(node) else: for key, value in node_params.items(): if not getattr(node, key): setattr(node, key, value) event = Event(**event_params) node.events.append(event) if event.message == "deleted nodeclaim": node.isAlive = False def parse_log_entry(self, log: dict) -> None: full_log = json.dumps(log) time = log.get("time") level = log.get("level") message = log.get("message") node_pool = log.get("NodeClaim", {}).get("name", "").rsplit("-", 1)[0] node_claim = log.get("NodeClaim", {}).get("name", "") instance_id = log.get("provider-id", "").split("/")[-1] node_name = log.get("Node", {}).get("name", "") node_params = {"nodePool": node_pool, "nodeClaim": node_claim, "instanceId": instance_id, "nodeName": node_name} event_params = {"time": time, "level": level, "message": message, "full_log": full_log} if not event_params["message"].startswith("disrupting nodeclaim") and all(not v for v in node_params.values()): self.not_node_events.append(Event(**event_params)) return if event_params["message"].startswith("disrupting nodeclaim"): disruption_type = re.search(r"via (\w+)", message).group(1) node_names = [node.strip().split("/")[0] for node in re.findall(r"([\w.-]+/[\w.-]+/[\w.-]+)", message)] for node_name in node_names: node_params = {**node_params, "nodeName": node_name} event_params = {**event_params, "message": f"disrupting nodeclaim via {disruption_type}"} self.process_node_event(node_params, event_params) return self.process_node_event(node_params, event_params) def write_csv(self, output_file: str = "karpenter-events.csv") -> None: rows = [] for node in self.nodes: node_dict = asdict(node) base_row = {k: v for k, v in node_dict.items() if k != "events"} for event in node.events: rows.append({**asdict(event), **base_row}) for log in self.not_node_events: row = {**asdict(log), "nodePool": "", "nodeClaim": "", "nodeName": "", "instanceId": "", "isAlive": ""} rows.append(row) sorted_rows = sorted(rows, key=itemgetter("time")) fieldnames = ["time", "level", "message", "nodePool", "nodeClaim", "nodeName", "instanceId", "isAlive", "full_log"] with open(output_file, "w", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() writer.writerows(sorted_rows) def process(self) -> None: try: with open(self.log_file_path, "r", encoding="utf-8") as log_file: for line in log_file: stripped_line = line.strip() if stripped_line: log_entry = json.loads(stripped_line) self.parse_log_entry(log_entry) self.write_csv() except Exception as e: print(f"Error processing log file: {e}") if __name__ == "__main__": parser = KarpenterLogParser("karpenter.log") parser.process()
3. CSV 파일 추출
Karpenter 로그 분석 스크립트와 Karpenter 로그 파일을 하나의 디렉토리에 위치시킨 뒤, 실행시킵니다.
## 디렉토리에 다음과 같이 저장합니다. . ├── karpenter-log-analyzer.py └── karpenter.log ## 스크립트를 실행시킵니다. $ python3 karpenter-log-analyzer.py
스크립트를 실행시키면 karpenter-events.csv
라는 CSV 파일이 생성됩니다.
4. 생성된 CSV 파일 확인
생성된 CSV 파일에서 NodeClaim 컬럼을 기준으로 필터링을 수행하면, 특정 NodeClaim과 관련된 모든 이벤트를 시간 순서대로 확인할 수 있습니다. 이는 노드의 생성부터 삭제까지의 전체 생명주기를 한눈에 파악할 수 있게 해줍니다.
이러한 자동화된 로그 분석 도구를 통해 복잡했던 로그 분석 과정이 크게 단순화되며, 관리자는 Karpenter가 설정된 정책에 따라 정상적으로 노드를 관리하고 있는지 효율적으로 검증할 수 있습니다.
고려 사항
Karpenter 노드 수명주기와 Karpenter 로그 분석 스크립트 사용에 관해 다음과 같은 사항을 고려하십시오.
1. Karpenter disruption reconciliation interval
Karpenter에서 Disruption reconciliation interval은 5분으로 기본 설정되어 있습니다(v1.0 기준, 2025.01.01). 이러한 Reconciliation 프로세스는 시스템 전체가 아닌 각각의 NodeClaim에 대해 독립적으로 운영되며, 각 NodeClaim은 자체적으로 5분 간격으로 Disruption 조건을 평가합니다.
2. Disruption methods
Karpenter에서 노드의 Disruption 발생 시, 시스템은 해당 노드를 "Delete" 또는 "Replace" 방식으로 처리할지 결정합니다. 이 결정 과정은 다음과 같은 체계적인 단계를 따릅니다.
- Delete: 시스템은 먼저 Disruption 대상 노드의 파드들을 기존의 다른 노드로 이전할 수 있는지 평가합니다. 가능한 경우 즉시 파드 이전을 시도합니다. 만약 기존 노드로의 이전이 어려운 경우, 현재 생성 중인 새로운 노드가 있다면 해당 노드로의 파드 이전을 시도합니다.
- Replace: 위의 "Delete" 처리과정이 실패한 경우, 시스템은 "Replace" 모드로 전환하여 새로운 노드를 생성하고 해당 노드로 파드 이전을 시도합니다.
시스템은 모든 파드가 성공적으로 새로운 위치로 이전될 때까지 이 과정을 반복합니다. 이러한 프로세스에서 "Delete" 처리가 항상 "Replace" 처리보다 우선순위를 가지며, 이는 불필요한 노드 생성을 최소화하기 위한 설계입니다. 또한, "Delete" 처리를 통한 노드 삭제는 즉각적인 새 노드 생성으로 이어지지 않습니다. 그러나 클러스터의 추가적인 요구사항 평가에 의해 새로운 노드 생성이 트리거될 수 있습니다.
3. Node 수명주기와 관계없는 로그의 처리
Karpenter 로그 중 노드의 생명주기와 직접적인 관련이 없는 항목들은 대부분 노드 식별 속성을 포함하지 않습니다. 이러한 로그 항목들은 CSV 파일로 변환될 때 노드 식별 속성을 제외한 단순화된 형태로 저장됩니다.
4. Disruption 로그의 처리
Karpenter의 로그 중 "disrupting nodeclaim" 로그는 특별한 형태의 노드 생명주기 로그입니다. 대부분의 노드 생명주기 관련 로그가 단일 노드만을 대상으로 하는 반면, Disruption 로그는 한 시점에 여러 노드를 동시에 대상으로 할 수 있는 특징이 있습니다. 예를 들면 다음과 같습니다.
{"level": "INFO", "time": "2024-12-31T03:01:03.419Z", "logger": "controller", "message": "disrupting nodeclaim(s) via replace, terminating 4 nodes (11 pods) ip-10-0-0-10.ap-northeast-2.compute.internal/c6i.xlarge/on-demand, ip-10-0-0-11.ap-northeast-2.compute.internal/c6i.xlarge/on-demand, ip-10-0-0-13.ap-northeast-2.compute.internal/c6i.xlarge/on-demand, ip-10-0-0-20.ap-northeast-2.compute.internal/c6i.xlarge/on-demand and replacing with on-demand node from types m5.xlarge, m6i.xlarge, m7i.xlarge", "commit": "652e6aa", "controller": "disruption", "namespace": "", "name": "", "reconcileID": "acda29f6-2707-4973-8b9f-f5f9e672cbb6", "command-id": "bcc21108-deba-49be-81cc-a30dc37b1c76", "reason": "underutilized"}
이렇게 여러 노드를 대상으로한 Disruption 로그는 CSV 파일로 변환되는 과정에서 특별한 처리를 거칩니다. 하나의 로그 항목에 포함된 여러 노드의 정보는 각각 개별 행으로 분리되어 저장되며, 각 노드에 대한 Disruption 처리 방식("Delete" 또는 "Replace")도 함께 기록됩니다.
5. isAlive 컬럼
Karpenter 로그의 CSV 변환 과정에서 각 노드의 현재 존재여부는 isAlive 컬럼을 통해 표시됩니다. "deleted nodeclaim" 로그가 발생한 노드의 경우 isAlive 값이 False로 설정되어 해당 노드가 더 이상 클러스터에 존재하지 않음을 나타냅니다. 반면, 아직 삭제되지 않은 노드들은 isAlive 값이 True로 유지되어 현재 활성 상태임을 표시합니다.
결론
본 기사에서 소개한 로그 분석 도구는 Karpenter의 로그를 체계적으로 분석하여 CSV 형태로 변환함으로써, 노드의 생명주기 추적과정을 크게 단순화합니다. 이러한 도구를 활용하여 운영자는 Karpenter가 설정된 정책에 따라 올바르게 작동하고 있는지 효율적으로 검증할 수 있으며, 필요한 경우 신속한 대응이 가능해집니다.
참고 자료
[1] Karpenter - Understanding NodeClaim
https://karpenter.sh/v1.0/concepts/nodeclaims/
[2] Karpenter - Understanding Disruption
관련 콘텐츠
- 질문됨 2달 전lg...
- 질문됨 18일 전lg...
- AWS 공식업데이트됨 2년 전
- AWS 공식업데이트됨 9달 전
- AWS 공식업데이트됨 일 년 전
- AWS 공식업데이트됨 일 년 전