1. 들어가기 앞서
개발 / 품질 환경의 경우 Instance Scheduler 를 사용하여 EKS 의 노드를 종료시킬 수 있다. 자세한 내용은 아래의 포스팅을 참고 부탁하며 그 중 핵심은 ASG(Auto Scaling Group) 의 desired, min, max 를 각각 0 으로 조정하여 내리는 방법이다.
https://hyukops.tistory.com/39
AWS: Instance Scheduling
들어가기 앞서현재 운영중인 프로젝트의 비용최적화 방안으로 개발 및 품질과 같은 운영을 제외한 환경에 있는, EC2를 포함한 RDS와 같은 인스턴스 등을 업무시간 이외에 자원을 중지시키고 업무
hyukops.tistory.com
문제는 EKS Automode 의 경우 ASG 를 사용하지 않는다는 것이다. EKS automode 는 Karpenter 를 기반으로 Nodepool 을 생성하여 노드를 관리하기 때문에 외부에서 이를 조정할 수 없다. 해당 해결 방법에 대해 아래에 공유하고자 한다. 우선, 앞서 말한대로 노드 사이즈를 수정하는 것이 아니라, 아래 예시의 Nodepool 의 limits.cpu 를 "4" 에서 "0" 으로 수정하여 적용해야 한다.

그렇게 되면 노드는 추가 노드를 생성하지 않게 된다. 이때 노드를 cordon -> drain 시킨 뒤 delete 하면 추가 노드는 생성되지 않게 된다. 해당 솔루션을 구축하기 위해 간략히 방법을 설명하면 아래와 같다.
- IAM Role 생성 (Lambda 부착)
- EKS 액세스 허용
- Lambda Layer 생성
- Lambda 생성 및 세팅
- Lambda 코드 작성 및 테스트
2. IAM Role 생성 (Lambda 부착)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"eks:DescribeCluster",
"eks:ListClusters",
"sts:GetCallerIdentity",
"ec2:DescribeInstances",
"ec2:DescribeTags"
],
"Resource": "*"
}
]
}
- 위 내용으로 Policy 생성 후 Lambda 용 Role 을 생성하여 부착 (신뢰 관계는 lambda 가 되도록 사용자는 lambda 지정)
3. EKS 액세스 허용
- EKS 콘솔 -> 클러스터 선택 -> 중간의 액세스 탭 이동

- 우측 상단 생성 버튼 클릭 -> 'IAM 보안 주체'에 위 2번에서 생성한 Role 선택 후 다음
- 정책 연결 'AmazonEKSClusterAdminPolicy' 선택 -> 액세스 범위 '클러스터' 선택 후 최종 생성
4. Lambda Layer 생성
Lambda에서는 kubernetes 모듈과 eks token 모듈이 없다. 즉, Python 패키지가 Lambda 환경에 설치되어 있지 않은 상태이므로 Layer 를 생성하여 Lambda 에 연결해야 한다.
mkdir python
pip install kubernetes eks-token -t python/
zip -r lambda-layer.zip python
- 개인 PC 등의 로컬 환경에서 위의 명령어를 통해 zip 파일을 생성한다.

- 이후 람다 콘솔로 이동하여 좌측 계층 탭으로 이동한다.
- 우측 상단의 계층 생성 선택

- zip 파일 업로드를 선택한 뒤 위에서 생성한 lambda-layer.zip 을 업로드한다.
- 호환 아키텍처와 호환 런타임은 람다 설정값과 일치시켜야 한다. 현재 람다를 아직 생성하지 않았으므로 우선 위 같이 선택한 뒤 생성
5. Lambda 생성 및 세팅

- 이름은 편하신대로 하시고..
- 아키텍처와 런타임은 layer 를 생성할 때 지정한 값과 일치시킨다.
- '기존 실행 역할 변경'에서 '기존 역할 사용'을 선택한 뒤 2번에서 생성한 Role 을 선택한다.

- 생성한 람다 - 함수 콘솔로 들어와서 아래의 '구성' - '일반 구성' 탭에서 제한시간은 10초 이상(넉넉히 30초 추천)으로 설정하고 메모리도 512MB로 변경한다.

- '환경 변수' 탭에서 위와 같이 CLUSTER_NAME(본인의 EKS 클러스터 이름) 과 REGION(클러스터가 생성된 리전) 지정한다.

- 람다 함수 메인 페이지에서 아래로 쭉 내리면 '계층' 위젯을 확인할 수 있다.

- 'Add a layer' 선택 후 '사용자 지정 계층'에서 4번에서 생성한 Layer 추가 (주인장은 이미 생성한 이력이 있어서 버전이 3입니다. 아마 버저닝이 없거나 1로 되어 있습니다!)
6. Lambda 코드 작성 및 테스트
자! 다 왔습니다. 코드 샘플은 아래와 같습니다. 주석처리를 하였으니 참고 부탁드리며 필요한 부분은 수정해서 사용하시면 됩니다!
import os
import boto3
from kubernetes import client, config
import base64
from eks_token import get_token # EKS 토큰 발급 라이브러리
# 클러스터 정보 관련 상수
CERT_INFO = ''
ENDPOINT = ''
CA_CERT_FILEPATH = '/tmp/certification_file_name.pem'
# Lambda 환경변수로부터 EKS 클러스터 이름과 리전 가져오기
CLUSTER_NAME = os.environ.get('CLUSTER_NAME')
REGION = os.environ.get('REGION')
def build_k8s_api():
"""
EKS 클러스터에 접속할 수 있는 Kubernetes API client 생성
1. eks_token 라이브러리로 인증 토큰 발급
2. boto3로 클러스터 endpoint 및 CA cert 조회
3. kubeconfig 구성 후 ApiClient 반환
"""
token = get_token(cluster_name=CLUSTER_NAME)['status']['token']
eks_api = boto3.client('eks', region_name=REGION)
cluster_info = eks_api.describe_cluster(name=CLUSTER_NAME)
endpoint = cluster_info['cluster']['endpoint']
cert_info = cluster_info['cluster']['certificateAuthority']['data']
# CA 인증서를 /tmp에 파일로 저장
with open(CA_CERT_FILEPATH, "w") as f:
f.write(base64.b64decode(cert_info).decode())
config = client.Configuration()
config.host = endpoint
config.verify_ssl = True
config.ssl_ca_cert = CA_CERT_FILEPATH
config.api_key['authorization'] = token
config.api_key_prefix['authorization'] = 'Bearer'
return client.ApiClient(config)
def patch_nodepool(api):
"""
NodePool의 CPU limit을 0으로 패치하여 노드 축소를 유도
"""
body = {
"spec": {
"limits": {
"cpu": "0"
}
}
}
# NodePool은 cluster-scoped CRD라서 cluster 호출 사용
custom_api = client.CustomObjectsApi(api)
response = custom_api.patch_cluster_custom_object(
group="karpenter.sh",
version="v1",
plural="nodepools",
name="system",
body=body
)
print("NodePool patched:", response)
def drain_nodes(api):
"""PDB를 무시하고 해당 라벨 노드의 모든 파드 강제 삭제"""
core = client.CoreV1Api(api)
nodes = core.list_node(label_selector="workload-type=system").items
for node in nodes:
node_name = node.metadata.name
print(f"Draining node: {node_name}")
pods = core.list_pod_for_all_namespaces(field_selector=f"spec.nodeName={node_name}").items
for pod in pods:
# PDB 무시(force)로 강제 삭제
try:
core.delete_namespaced_pod(
name=pod.metadata.name,
namespace=pod.metadata.namespace,
grace_period_seconds=0,
propagation_policy="Foreground"
)
print(f"Deleted pod: {pod.metadata.name}")
except client.exceptions.ApiException as e:
print(f"Failed to delete {pod.metadata.name}: {e.reason}")
print(f"Node drained: {node_name}")
def delete_nodes(api):
"""
특정 라벨(workload-type=system)을 가진 노드를 실제로 삭제
"""
core = client.CoreV1Api(api)
nodes = core.list_node(label_selector="workload-type=system").items
for node in nodes:
node_name = node.metadata.name
core.delete_node(node_name)
print(f"Deleted node: {node_name}")
def lambda_handler(event, context):
"""
Lambda 엔트리 포인트
1. Kubernetes API client 생성
2. NodePool CPU limit 패치 → 노드 축소
3. 파드 drain
4. 노드 삭제
"""
api = build_k8s_api() # kubeconfig 구성
print("Starting scale-down")
patch_nodepool(api)
drain_nodes(api)
delete_nodes(api)
print("Scale-down completed")
return {"status": "success"}'Kubernetes & EKS > k8s 운영 특이사항' 카테고리의 다른 글
| [kubernetes] failed to create pod sandbox 에러 발생 (2) | 2025.08.24 |
|---|---|
| [kubernetes] HPA loop 현상 해결 및 Best Practice (3) | 2025.07.30 |
| [kubernetes] 데몬셋 파드 Pending 현상 (PriorityClass) (0) | 2025.06.26 |
| [kubernetes] EKS의 Burstable 인스턴스 타입 변경 문제 (0) | 2025.05.08 |
| [kubernetes] EKS 업그레이드 장애 (2) - net.ipv4.ip_forward (0) | 2025.03.08 |
