FinOps

[Lambda] 업무시간 외 Fargate Stop/Start

김붕어87 2025. 4. 16. 11:31
반응형

개요
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 설정 

 

코드 소스

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 설정 

 

 

코드 소스

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