개요
DEV환경은 업무시간에만 사용하는 테스트 환경입니다.
업무시간(09:00 ~ 18:00) 외 모든 리소스를 Stop하고 업무시간에는 모든 리소스를 Start 해서 요금을 줄입니다.
업무시간 : 09:00 ~ 18:00 (총 9시간)
업무시간 외 : 24시간 - 업무시간 (총 15시간)
1DAY : 업무시간 외 Stop을 하면 하루에 15시간 요금을 줄일 수 있습니다.
365DAY : 15 * 365 = 5475시간 ( 5475시간 / 24시간 = 228.125일)
1년 기준으로 하면 228일 요금을 줄일 수 있습니다.
1. 요금을 줄일 수 있는 리소스
- EC2
- RDS DB
- WorkerNode
- Fargate
- 등
2. 요금을 줄이는 스케쥴링 방식
Stop & Start 스케쥴링은 여러 방식이 있습니다.
이 글에서는 Lambda + EventBrige 방식으로 진행합니다.
ex)
EventBridge Rule : 09:00시 -> Start Fargate Lambda 실행
EventBridge Rule : 18:00시 -> Stop Fargate Lambda 실행
3. Fargate 스케쥴링
3-1. IAM Role 생성
[ Lambda에서 사용할 Role Policy 생성 ]
Lambda 함수에서 Fargate Stop & Start 권한 넣기
AWS Consoel -> IAM -> Policy -> policy 생성
Policy Name : dev-lambda-fargate-Policy
{
"Statement": [
{
"Action": [
"eks:DescribeCluster",
"eks:ListClusters",
"eks:DescribeNodegroup",
"eks:ListNodegroups",
"eks:AccessKubernetesApi",
"sts:GetCallerIdentity",
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ec2:AssignPrivateIpAddresses",
"ec2:UnassignPrivateIpAddresses"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
}
AWS Consoel -> IAM -> Role -> Role 생성
Role Name : dev-lambda-schedule-role
Policy Attach : dev-lambda-fargate-Policy
[ EventBridge에서 사용할 Role & Policy 생성 ]
EventBridge에서 Lambda 함수를 실행할 권한 넣기
AWS Consoel -> IAM -> Policy -> policy 생성
Policy Name : dev-Amazon-EventBridge-Scheduler-Execution-Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:ap-northeast-2:xxx:function:StopFargate_POD:*",
"arn:aws:lambda:ap-northeast-2:xxx:function:StopFargate_POD",
"arn:aws:lambda:ap-northeast-2:xxx:function:StartFargate_POD:*",
"arn:aws:lambda:ap-northeast-2:xxx:function:StartFargate_POD"
]
}
]
}
AWS Consoel -> IAM -> Role -> Role 생성
Role Name : dev-Amazon_EventBridge_Scheduler_LAMBDA_role
Policy Attach : dev-Amazon-EventBridge-Scheduler-Execution-Policy
3-2. Lambda 함수 생성
- StopFargate_POD Lambda 함수 만들기
AWS Console -> Lambda -> 함수 -> 함수 생성
함수 이름 : StopFargate_POD
런타임 : Python 3.9
기본 실행 역할 변경 -> 실행 역할 : 기존 역할 사용 클릭
기존 역할 : dev-lambda-schedule-role 클릭
함수 생성
lambda Layer 설정
- StopFargate_POD Lambda 함수에서는 aws cli를 사용하기 때문에 aws cli Layer를 추가해야합니다.
- 메뉴얼 참조 : https://dongwook35.tistory.com/107
코드 소스
import subprocess
import os
import json
def scale_down_resource_subprocess(kind, name, namespace, replicas=0, kubeconfig_path=None):
# subprocess를 사용하여 스케일 다운
command = [
"kubectl",
"--kubeconfig", # Lambda 환경에서는 kubeconfig 파일 경로를 설정해야 함
kubeconfig_path,
"-n",
namespace,
"scale",
f"--replicas={replicas}",
kind,
name
]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
#print(f"{kind} {namespace}/{name} 스케일 결과: {result.stdout}")
print(f"kubectl --kubeconfig {kubeconfig_path} -n {namespace} scale --replicas={replicas} {kind} {name} 스케일 결과: {result.stdout}")
except subprocess.CalledProcessError as e:
#print(f"{kind} {namespace}/{name} 스케일 실패: {e.stderr}")
print(f"kubectl --kubeconfig {kubeconfig_path} -n {namespace} scale --replicas={replicas} {kind} {name} 스케일 실패: {e.stderr}")
except FileNotFoundError:
print("kubectl 명령어를 찾을 수 없습니다. Lambda 환경에 kubectl을 포함해야 합니다.")
def lambda_handler(event, context):
kubeconfig_path = '/tmp/kubeconfig'
cluster_name = os.environ.get('EKS_CLUSTER_NAME')
region = os.environ.get('AWS_REGION', 'ap-northeast-2')
if not cluster_name:
error_message = "Error: EKS_CLUSTER_NAME environment variable not set."
print(error_message)
return {
'statusCode': 400,
'body': error_message
}
try:
# kubeconfig 파일 업데이트
update_command = [
'/opt/bin/aws',
'eks',
'update-kubeconfig',
'--name',
cluster_name,
'--region',
region,
'--kubeconfig',
kubeconfig_path
]
subprocess.run(update_command, check=True, capture_output=True, text=True)
print(f"kubeconfig updated successfully to: {kubeconfig_path}")
# kubectl scale 작업 #
target = [
{"kind": "deployment", "name": "kube-state-metrics", "namespace": "kube-state-metrics"},
{"kind": "deployment", "name": "coredns", "namespace": "kube-system"},
{"kind": "deployment", "name": "metrics-server", "namespace": "kube-system"},
{"kind": "statefulset", "name": "adot-collector", "namespace": "fargate-container-insights"},
]
results = []
for item in target:
result = scale_down_resource_subprocess(item["kind"], item["name"], item["namespace"], kubeconfig_path=kubeconfig_path)
results.append(f"Scaling {item['namespace']} {item['kind']}/{item['name']}: {result}")
return {
'statusCode': 200,
'body': json.dumps({"message": "Successfully updated kubeconfig and attempted to scale down target resources.", "results": results})
}
except subprocess.CalledProcessError as e:
print(f"오류 발생: {e.stderr}")
return {
'statusCode': 500,
'body': f"Error: {e.stderr}"
}
except FileNotFoundError:
print("Error: /opt/bin/aws command not found. Ensure AWS CLI is available in the Lambda environment.")
return {
'statusCode': 500,
'body': "Error: AWS CLI not found."
}
except Exception as e:
print(f"예상치 못한 오류 발생: {e}")
return {
'statusCode': 500,
'body': f"An unexpected error occurred: {e}"
}
import subprocess
import os
import json
def get_all_resources(kind, kubeconfig_path):
try:
command = [
"kubectl",
"--kubeconfig", kubeconfig_path,
"get", kind,
"-A", # All namespaces
"-o", "json"
]
result = subprocess.run(command, capture_output=True, text=True, check=True)
resources = json.loads(result.stdout)
# 각 리소스에 대해 (namespace, name) 튜플로 반환
return [
(item["metadata"]["namespace"], item["metadata"]["name"])
for item in resources.get("items", [])
]
except subprocess.CalledProcessError as e:
print(f"❌ {kind} 전체 조회 실패: {e.stderr.strip()}")
return []
def scale_down_resource(kind, name, namespace, kubeconfig_path, replicas=0):
command = [
"kubectl",
"--kubeconfig", kubeconfig_path,
"-n", namespace,
"scale",
f"--replicas={replicas}",
kind,
name
]
try:
subprocess.run(command, capture_output=True, text=True, check=True)
print(f"✅ Scaled {kind} {namespace}/{name} to {replicas}")
return f"Scaled {kind} {namespace}/{name} to {replicas}"
except subprocess.CalledProcessError as e:
print(f"❌ Failed to scale {kind} {namespace}/{name}: {e.stderr.strip()}")
return f"Failed to scale {kind} {namespace}/{name}: {e.stderr.strip()}"
def lambda_handler(event, context):
kubeconfig_path = '/tmp/kubeconfig'
cluster_name = os.environ.get('EKS_CLUSTER_NAME')
region = os.environ.get('AWS_REGION', 'ap-northeast-2')
if not cluster_name:
return {
'statusCode': 400,
'body': "Error: EKS_CLUSTER_NAME environment variable not set."
}
try:
# kubeconfig 구성
update_command = [
'/opt/bin/aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--region', region,
'--kubeconfig', kubeconfig_path
]
subprocess.run(update_command, check=True, capture_output=True, text=True)
print("✅ kubeconfig updated")
results = []
# 모든 deployment와 statefulset 조회
for kind in ["deployment", "statefulset"]:
resources = get_all_resources(kind, kubeconfig_path)
for namespace, name in resources:
res = scale_down_resource(kind, name, namespace, kubeconfig_path)
results.append(res)
return {
'statusCode': 200,
'body': json.dumps({
"message": "Scale down complete",
"results": results
})
}
except subprocess.CalledProcessError as e:
return {
'statusCode': 500,
'body': f"Error: {e.stderr}"
}
except FileNotFoundError:
return {
'statusCode': 500,
'body': "Error: AWS CLI not found in Lambda environment."
}
except Exception as e:
return {
'statusCode': 500,
'body': f"Unexpected error: {e}"
}
Deploy 코드 업로드
TEST 실행
AWS -> AWS CloudTrail -> 이벤트 기록 클릭
속성 조회 : 이벤트 이름
값 : stopFargate
Lambda가 Fargate stop되었는지 감사로그 확인 가능
- StartFargate_POD Lambda 함수 만들기
AWS Console -> Lambda -> 함수 -> 함수 생성
함수 이름 : StartFargate_POD
런타임 : Python 3.9
기본 실행 역할 변경 -> 실행 역할 : 기존 역할 사용 클릭
기존 역할 : dev-lambda-schedule-role 클릭
함수 생성
lambda Layer 설정
- StopFargate_POD Lambda 함수에서는 aws cli를 사용하기 때문에 aws cli Layer를 추가해야합니다.
- 메뉴얼 참조 : https://dongwook35.tistory.com/107
코드 소스
import subprocess
import os
import json
def scale_down_resource_subprocess(kind, name, namespace, replicas, kubeconfig_path=None):
# subprocess를 사용하여 스케일 아웃
command = [
"kubectl",
"--kubeconfig", # Lambda 환경에서는 kubeconfig 파일 경로를 설정해야 함
kubeconfig_path,
"-n",
namespace,
"scale",
f"--replicas={replicas}",
kind,
name
]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
#print(f"{kind} {namespace}/{name} 스케일 결과: {result.stdout}")
print(f"kubectl --kubeconfig {kubeconfig_path} -n {namespace} scale --replicas={replicas} {kind} {name} 스케일 결과: {result.stdout}")
except subprocess.CalledProcessError as e:
#print(f"{kind} {namespace}/{name} 스케일 실패: {e.stderr}")
print(f"kubectl --kubeconfig {kubeconfig_path} -n {namespace} scale --replicas={replicas} {kind} {name} 스케일 실패: {e.stderr}")
except FileNotFoundError:
print("kubectl 명령어를 찾을 수 없습니다. Lambda 환경에 kubectl을 포함해야 합니다.")
def lambda_handler(event, context):
kubeconfig_path = '/tmp/kubeconfig'
cluster_name = os.environ.get('EKS_CLUSTER_NAME')
region = os.environ.get('AWS_REGION', 'ap-northeast-2')
if not cluster_name:
error_message = "Error: EKS_CLUSTER_NAME environment variable not set."
print(error_message)
return {
'statusCode': 400,
'body': error_message
}
try:
# kubeconfig 파일 업데이트
update_command = [
'/opt/bin/aws',
'eks',
'update-kubeconfig',
'--name',
cluster_name,
'--region',
region,
'--kubeconfig',
kubeconfig_path
]
subprocess.run(update_command, check=True, capture_output=True, text=True)
print(f"kubeconfig updated successfully to: {kubeconfig_path}")
# kubectl scale 작업 #
target = [
{"kind": "deployment", "name": "kube-state-metrics", "namespace": "kube-state-metrics", "replicas": "1"},
{"kind": "deployment", "name": "coredns", "namespace": "kube-system", "replicas": "2"},
{"kind": "deployment", "name": "metrics-server", "namespace": "kube-system", "replicas": "2"},
{"kind": "statefulset", "name": "adot-collector", "namespace": "fargate-container-insights", "replicas": "1"},
]
results = []
for item in target:
result = scale_down_resource_subprocess(item["kind"], item["name"], item["namespace"], item["replicas"], kubeconfig_path=kubeconfig_path)
results.append(f"Scaling {item['namespace']} {item['kind']}/{item['name']}: {result}")
return {
'statusCode': 200,
'body': json.dumps({"message": "Successfully updated kubeconfig and attempted to scale down target resources.", "results": results})
}
except subprocess.CalledProcessError as e:
print(f"오류 발생: {e.stderr}")
return {
'statusCode': 500,
'body': f"Error: {e.stderr}"
}
except FileNotFoundError:
print("Error: /opt/bin/aws command not found. Ensure AWS CLI is available in the Lambda environment.")
return {
'statusCode': 500,
'body': "Error: AWS CLI not found."
}
except Exception as e:
print(f"예상치 못한 오류 발생: {e}")
return {
'statusCode': 500,
'body': f"An unexpected error occurred: {e}"
}
import subprocess
import os
import json
def get_all_resources(kind, kubeconfig_path):
try:
command = [
"kubectl",
"--kubeconfig", kubeconfig_path,
"get", kind,
"-A", # All namespaces
"-o", "json"
]
result = subprocess.run(command, capture_output=True, text=True, check=True)
resources = json.loads(result.stdout)
# 각 리소스에 대해 (namespace, name) 튜플로 반환
return [
(item["metadata"]["namespace"], item["metadata"]["name"])
for item in resources.get("items", [])
]
except subprocess.CalledProcessError as e:
print(f"❌ {kind} 전체 조회 실패: {e.stderr.strip()}")
return []
def scale_down_resource(kind, name, namespace, kubeconfig_path, replicas=1):
command = [
"kubectl",
"--kubeconfig", kubeconfig_path,
"-n", namespace,
"scale",
f"--replicas={replicas}",
kind,
name
]
try:
subprocess.run(command, capture_output=True, text=True, check=True)
print(f"✅ Scaled {kind} {namespace}/{name} to {replicas}")
return f"Scaled {kind} {namespace}/{name} to {replicas}"
except subprocess.CalledProcessError as e:
print(f"❌ Failed to scale {kind} {namespace}/{name}: {e.stderr.strip()}")
return f"Failed to scale {kind} {namespace}/{name}: {e.stderr.strip()}"
def lambda_handler(event, context):
kubeconfig_path = '/tmp/kubeconfig'
cluster_name = os.environ.get('EKS_CLUSTER_NAME')
region = os.environ.get('AWS_REGION', 'ap-northeast-2')
if not cluster_name:
return {
'statusCode': 400,
'body': "Error: EKS_CLUSTER_NAME environment variable not set."
}
try:
# kubeconfig 구성
update_command = [
'/opt/bin/aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--region', region,
'--kubeconfig', kubeconfig_path
]
subprocess.run(update_command, check=True, capture_output=True, text=True)
print("✅ kubeconfig updated")
results = []
# 모든 deployment와 statefulset 조회
for kind in ["deployment", "statefulset"]:
resources = get_all_resources(kind, kubeconfig_path)
for namespace, name in resources:
res = scale_down_resource(kind, name, namespace, kubeconfig_path)
results.append(res)
return {
'statusCode': 200,
'body': json.dumps({
"message": "Scale down complete",
"results": results
})
}
except subprocess.CalledProcessError as e:
return {
'statusCode': 500,
'body': f"Error: {e.stderr}"
}
except FileNotFoundError:
return {
'statusCode': 500,
'body': "Error: AWS CLI not found in Lambda environment."
}
except Exception as e:
return {
'statusCode': 500,
'body': f"Unexpected error: {e}"
}
Deploy 코스 업로드
TEST 실행
3-1. EventBridge 생성
AWS Console -> Amazon EventBridge -> 일정 -> 일정 생성
일정 이름 : StopFargate_POD
설명 : Scheduling server off outside of business hours
일정 그룹 : 선택
일정 패턴 : 반복 일정 선택
시간대 : (UTC+09:00) Asia/Seoul)
일정 유형 : Cron 기반 일정
Cron 표현식 : 0 18 ? * MON-FRI *
유연한 기간 : 꺼짐
대상 세부 정보 : AWS Lambda 선택
Lambda 함수 : StopFargate_POD 선택
일정 완료 후 작업 : NONE
재시도 정책 : No
DLQ : 없음
기존 역할 사용 : dev-Amazon_EventBridge_Scheduler_LAMBDA_role 선택
'FinOps' 카테고리의 다른 글
[Lambda] awscli Layer 설정 (0) | 2025.04.16 |
---|---|
[Lambda] 업무시간 외 AuroraDB Stop/Start (0) | 2025.04.14 |
[Lambda] 업무시간 외 EC2 Stop/Start (0) | 2025.04.14 |