![[AWS] EC2 인스턴스 자동 중지 및 시작하기 - Lambda 함수 활용](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FevcBXi%2FbtsLiGo5rNm%2FsXBJbrNPC594h4x6v2Aey0%2Fimg.webp)
AWS와 같은 퍼블릭 클라우드를 사용할 때, 가장 중요한 것은 비용 관리입니다.
많은 사용자가 활용하는 EC2 인스턴스는 중지되면 비용이 부과되지 않는데요.
이전 게시글에서 인스턴스를 자동으로 중지하는 것을 Quick Setup으로 설정하는 방법을 알아봤습니다.
도입부는 중복된 내용이니, 구현 방식을 확인하실 분은 ''개발 과정''으로 넘어가시면 되겠습니다.
기본적으로 AWS EC2는 온디맨드 요금제를 채택하고 있습니다. 온디맨드 요금제는 사용한 만큼 비용을 지불하는 방식인데요. (자료: EC2 요금제)
인스턴스가 중지되면 인스턴스에 대한 비용이 부과되지 않습니다. 물론 함께 사용하는 Elastic IP, 볼륨 스토리지 등에 대한 비용은 계속 청구됩니다.
사용하지 않을 때 인스턴스를 중지시키면 운영 비용을 최소화할 수 있습니다.
하지만, 사람이 일일이 하다 보면 까먹기 마련이지요. 인스턴스의 개수가 많아지면 24시간 돌아야 하는 인스턴스도 만들어지기도 합니다.
결국 시간이 지날수록 관리의 복잡도가 점점 증가합니다. 이때, 조건에 맞춰 자동으로 동작하는 기능이 있다면 인스턴스 편하게 관리할 수 있습니다.
이러한 기능을 구현하는 것은 크게 두 가지 방법이 있습니다.
- AWS에서 제공하는 Quick Setup 기능 활용
- Lambda, EventBridge, CloudWatch를 활용하여 직접 구현
람다는 구현에 따라 좀 다르겠지만, 일반적으로 두 방식 모두 인스턴스의 태그에 지정된 값에 따라 동작하는 방식은 동일합니다.
인스턴스의 태그에 특정 Key-Value 값이 존재한다면 관리 대상으로 인식하고 인스턴스의 상태를 변경하는 것이지요.
단순히 일정에 맞춰 운영한다면 Quick Setup으로 간단히 설정할 수 있습니다.
다만, 운영을 하다보면 여러 조건들과 함께 예외 케이스가 발생하기 마련입니다.
여러 데이터를 CloudWatch와 같은 여러 솔루션과 연계해서 효과적으로 관리할 수도 있습니다.
이렇게 기능/상황에 따라 입맛대로 운영하고 싶다면 Lambda를 중심으로 직접 구현하는 방식을 채택하는 것이 유리합니다.
저는 EC2 인스턴스를 조건에 맞춰 자동으로 시작/중지 하는 기능을 AutoRunner라고 부르기로 했습니다.
이번 포스팅에서는 Lambda/Cloud Watch/Event Bridge를 활용해서 직접 구현하는 과정을 가이드 형태로 정리해 보겠습니다.
개발 과정
🔹 [IAM] 정책/역할 생성
🔸 정책
기본적으로 AutoRunner가 동작하려면 역할이 필요합니다. 이 역할은 동작에 필요한 정책(권한)을 갖고 있어야 합니다.
각 솔루션(스케줄러, 람다 등)마다 정책과 역할을 생성해도 좋겠지만, 이번에는 한 개의 역할로 모두 사용하려고 합니다.
다음은 필요한 정책을 최소한의 정책을 정리한 표입니다.
대상 | 정책 | 비고 |
EC2 | ec2:DescribeInstances ec2:Start* ec2:Stop* |
인스턴스를 조회하고, 시작하고, 중지하기. |
EventBridge | lambda:InvokeFunction | 함수를 호출한다. |
CloudWatch | logs:CreateLogGroup logs:CreateLogStream logs:PutLogEvents |
로그 그룹을 생성하고, 로그 스트림을 생성하고, 로그를 기록한다. |
먼저, 인스턴스를 시작/중지하는 기능이기 때문에 EC2에 대한 권한은 당연히 필요합니다. Lambda를 활용할 것이기 때문에 마찬가지구요.
자동으로 동작하는 기능은 동작 과정과 결과를 기록하는 것이 중요합니다. 그래야 나중에 문제가 발생했을 때 추적하기 용이합니다.
그래서 CloudWatch에서 로그 그룹, 스트림을 생성해 로그를 기록하는 정책까지 포함시켰습니다.
실제 AWS 콘솔에서 정책을 생성하기 위해, IAM > 액세스 관리 > 정책으로 이동해서 정책을 생성합니다.
정책 생성 버튼을 누른 뒤, JSON을 누르고 다음 정책을 입력하시면 됩니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:Start*",
"ec2:Stop*",
"lambda:InvokeFunction",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
정상적으로 입력이 되었다면, 검토 단계에서 정책에 정의된 권한이 올바르게 설정된 것을 보실 수 있습니다.
정책 이름과 설명, 태그를 알맞게 설정하시고 생성하시면 됩니다.
🔸 역할
이제 이 정책을 갖는 역할을 생성해 보도록 하겠습니다.
액세스 관리 > 역할 > 역할 생성 버튼을 클릭해 역할을 생성합니다.
먼저 신뢰할 수 있는 엔터티를 설정하는데요. 이 역할을 가진 사용자가 특정 리소스에 접근해서 작업을 할 수 있도록 지정하는 것입니다.
저는 Auto Runner가 동작하면서 사용할 리소스에 대한 신뢰 정책만 설정해주려고 합니다. 따라서 사용자 지정 신뢰 정책을 선택했습니다.
이후, 하단에 다음과 같은 신뢰 정책을 지정합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "scheduler.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"Service": "cloudwatch.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
트리거가 될 EventBridge Scheduler, 로직이 동작할 Lambda, 로그를 기록할 Cloudwatch에 대한 신뢰 관계를 추가했습니다.
동작 중에 특정 리소스에 대한 접근 권한이 필요할 수도 있으니, 필요하다고 하면 상황에 맞게 유동적으로 추가해 주시면 됩니다.
다음 버튼을 누르고, 방금 생성했던 정책을 지정해 줍니다. 이 계정은 다른 사용자가 없으므로 권한 경계는 설정하지 않았습니다.
그러면 검토 단계에서 정상적으로 적용된 것을 보실 수 있습니다. 마찬가지로, 이름과 태그를 적절히 설정해 주세요.
🔹 [Lambda] Lambda 함수 구현
본격적으로 로직이 돌 Lambda 함수를 구현할 차례입니다. 콘솔에서 Lambda를 검색해서 함수 생성 버튼을 눌러주세요.
먼저, 어떤 환경에서 함수를 동작시킬지 런타임에 대한 정보를 설정합니다.
개인적으로 Java가 익숙하지만, 이번에는 Python을 활용해서 구현해 보도록 하겠습니다.
Java는 함수 동작할 때 JVM 위에서 돌려야 한다는 점, Python은 다양한 자료 구조를 유연하게 활용할 수 있기 때문입니다.
아키텍처는 함수가 동작하는 컴퓨터의 프로세서 유형을 의미합니다. 자세한 내용은 정보를 눌러서 참고하시면 좋을 것 같습니다.
이번 기능은 많은 데이터를 처리하는 것도 아니고, 성능에 예민한 기능도 아니라서 비용적인 측면만 고려해서 결정했습니다.
아키텍처에 따른 요금이 다른데, ARM64 타입이 상대적으로 살짝 저렴합니다.
다음으로 권한을 설정합니다. 권한은 방금 만들었던 권한을 사용하시면 됩니다.
함수 생성이 완료되었으면, 하단에서 코드를 작성합니다.
import boto3
from datetime import datetime
from zoneinfo import ZoneInfo
# Get EC2 Client
REGION_KR = 'ap-northeast-2'
ec2 = boto3.client('ec2', region_name = REGION_KR)
DAY_OF_WEEK_MAP = {
'월': 0,
'화': 1,
'수': 2,
'목': 3,
'금': 4,
'토': 5,
'일': 6
}
"""
@brief KST(UTC+9)를 기준으로 동작한다.
"""
class CurrentDateTimeKST:
__slots__ = ['datetime', 'hour', 'weekday']
def __init__(self):
self.datetime = datetime.now().astimezone(ZoneInfo("Asia/Seoul"))
self.hour = self.datetime.hour
self.weekday = self.datetime.weekday()
"""
@brief Lambda 함수 실행 환경(컨테이너)가 매 번 독립적인 것이 아니어서 업데이트가 필요하다.
"""
def update(self):
self.datetime = datetime.now().astimezone(ZoneInfo("Asia/Seoul"))
self.hour = self.datetime.hour
self.weekday = self.datetime.weekday()
"""
@brief 현재 KST 시간을 반환한다.
@return datetime from Asia/Seoul
"""
def now(self):
return datetime.now().astimezone(ZoneInfo("Asia/Seoul"))
date_kst = CurrentDateTimeKST()
"""
@brief EC2 Auto Runner 실행 조건 확인
@param tags
'DayOfWeek': 허용 요일 (Str)
'AutoRunnerEnabled: 기능 사용 여부 (Bool)
@return 실행 가능 여부 (Bool)
"""
def is_valid_run_cond(tags):
# 요일 변환
is_valid_day_of_week = False
tag_day = tags.get('DayOfWeek', 'Invalid').lower()
if (tag_day == 'all'): # 매일
is_valid_day_of_week = True
elif (tag_day == 'weekdays'): # 평일
is_valid_day_of_week = date_kst.weekday < 5 # 월(0)-금(4)
else:
working_days = [working_day.strip() for working_day in tag_day.split(",") if working_day.strip()] # 일,월,화 같은 포맷 처리
for working_day in working_days:
if DAY_OF_WEEK_MAP.get(working_day, -1) == date_kst.weekday:
is_valid_day_of_week = True
break
return tags.get('AutoRunnerEnabled', 'False').lower() == 'true' \
and is_valid_day_of_week
def lambda_handler(event, context):
date_kst.update()
print(f"[INFO] EC2 Auto Runner is started: {date_kst.datetime} ")
all_instance_cnt = 0
start_list = []
stop_list = []
instances = ec2.describe_instances()
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
all_instance_cnt += 1
instance_id = instance['InstanceId']
tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
if is_valid_run_cond(tags):
state = instance['State']['Name']
# 상태가 변경 중이거나 종료된 경우 동작하지 않음
if state in ['pending', 'shutting-down', 'stopping', 'terminated']:
print(f'[DEBUG] "{instance_id}" is in a busy state: "{state}"')
continue
# 인스턴스 시작
if date_kst.hour == int(tags['StartHour']) and state == 'stopped':
start_list.append(instance_id)
continue
# 인스턴스 중지
if date_kst.hour == int(tags['EndHour']) and state == 'running':
stop_list.append(instance_id)
# 인스턴스 상태 변경
if start_list:
ec2.start_instances(InstanceIds=start_list)
if stop_list:
ec2.stop_instances(InstanceIds=stop_list)
# Logging
print(f'[INFO] Number of all EC2 instances: {all_instance_cnt}')
print(f'[INFO] Changed to the START > count: {len(start_list)}, list: {start_list}')
print(f'[INFO] Changed to the END state > count: {len(stop_list)}, list: {stop_list}')
print(f"[INFO] EC2 Auto Runner is done: {date_kst.now()}")
return {
'statusCode' : 200,
'body' : 'EC2 Instance Auto Runner is done.'
}
기본적인 로직은 EC2 인스턴스를 불러와서, 태그 값을 확인한 뒤 조건에 맞게 상태를 변환하는 것입니다.
동작 중에 print 함수로 출력되는 내용은 로그 스트림에 기록됩니다.
DateTime과 관련한 코드가 있는데, 이는 CloudWatch가 로그를 기록하는 시간이 UTC 기준이라서, KST로 맞추기 위함입니다.
참고로, 람다 함수가 실행되는 환경(컨테이너)은 실행될 때마다 독립된 공간에서 실행되지 않습니다.
그래서 다른 함수에서 사용하려고 전역 변수로 실행하면 이전 실행 때 초기화된 값이 적용될 수 있습니다.
현재 시간과 같이 동적인 값들은 매 번 초기화를 하고 실행할 수 있도록 구성을 해주셔야 합니다.
전체적인 코드 동작은 주석에 간단히 설명해 놓았으니 확인해 보시면 좋을 것 같습니다.
상황에 맞게 수정해서 사용하시려면 AWS의 EC2 관련 API 문서를 확인해보시면 유용합니다. (참고: BOTO3 Docs)
🔹 [EventBridge] EventBridge Scheduler 일정 생성
생성한 함수의 트리거가 될 Scheduler를 생성해 보겠습니다. 일반적으로 규칙에서 일정에 따른 규칙을 생성하는데요.
최근 AWS가 새로운 스케줄링 기능을 제공하면서, 이벤트 버스와 무관하게 일정 예약 기능을 할 수 있게 되었습니다.
EventBridge > Scheduler > 일정 > 일정 생성 버튼을 선택해서 일정을 생성합니다.
상단에 일정 이름과 설명, 그룹은 상황에 맞게 설정하시면 되고, 일정 패턴을 다음과 같이 설정하시면 매시간마다 동작합니다.
매 시간마다 동작하면서 인스턴스의 상태를 확인해야 하므로 반복 일정으로 설정합니다.
CloudWatch와 달리 스케줄러는 설정한 시간대에 맞춰 동작하므로, KST로 설정합니다.
스케줄러가 동작하는 주기는 Cron 표현식으로 설정할 수 있습니다.
🔸 Cron 표현식?
Crontab은 주기적인 작업을 위해 작업을 자동화하는 스케줄링 도구입니다.
리눅스에서 명령어를 주기적으로 실행하거나, 여러 프레임워크에서 주기적인 작업을 처리하기 위해 사용됩니다.
이때, 주기를 표현하는 방식이 Cron 표현식인데요. AWS 콘솔에서 보시는 것처럼 '분-시간-일-월-요일-연도'를 지정할 수 있습니다.
요일은 월요일(0)-일요일(6)로 표현합니다. 표준은 아니지만, 일요일을 7로 표기하는 경우도 있습니다.
연도는 생략되는 경우도 종종 있으며, 아스타리스크(*), 슬래시(-), 물음표(?) 등 여러 특수 문자를 지원합니다.
특수 문자 | 기능 | 예시 |
* | 모든 값(매 분, 매 시 등마다 동작) | * * * * * 2024 : 2024년에 매 분 마다 실행 |
, | 여러 값 | * * * * * 2024,2025 : 2024년, 2025년 매 분마다 실행 |
- | 범위 | * * * * * 2024-2026: 2024년부터 2026년까지 매 분마다 실행 |
? | 설정 안함 | * * * * ? 2024 : 2024년 매 분 실행(요일은 신경쓰지 않음) |
/ | 간격 지정 | */5 * * * * 2024 : 2024년에 5분마다 실행 |
리눅스에서 동작하는 Crontab 데몬은 일정을 6개의 인자로 표기합니다.
하지만, Quartz Scheduler와 같은 일부 라이브러리들은 초(Second)까지 포함해 7개로 구성되는 경우가 많습니다.
이 경우에는 '초-분-시-일-월-요일-연도'와 같이 맨 처음에 초가 포함됩니다.
Cron 표현식을 변환해 주는 사이트가 많이 있으니, 참고하시면 좋습니다. (http://www.cronmaker.com/)
다시 AWS로 돌아가서, AWS Scheduler는 언제 동작하는지 예시를 하단에 적어주니 참고하셔서 설정하시면 됩니다.
다음 버튼을 누르시고 Lambda 함수를 실행할 수 있도록 대상 API를 설정합니다.
하단에서 방금 작성했던 Lambda 함수를 설정해 주시고 다음 버튼을 클릭합니다.
계속 사용할 일정이므로 일정 완료 후 작업은 NONE으로 설정합니다.
주기적으로 실행되기 때문에 재시도 정책은 없음으로 설정했습니다.
암호화는 필요하시면 설정하시면 되고, 권한은 아까 생성했던 권한을 설정해 줍니다.
일정이 제대로 생성되었는지 요약본을 확인하시고 생성해 주시면 됩니다. 특히 Cron이 제대로 생성되었는지 확인해 주세요.
생성된 일정을 확인하고, 상태가 활성화 상태인지 확인하시면 끝입니다.
동작 확인
개발이 완료되었으니, 제대로 동작하는지 확인해 볼 차례입니다.
🔸 EC2 인스턴스 생성 및 태그 설정
중지/시작할 인스턴스를 생성하고 태그를 설정해야 합니다.
각자 필요에 맞게 인스턴스를 생성하시고, 태그를 다음과 같이 설정해 주시면 됩니다.
만약 코드를 수정하셨다면 그에 맞게 설정하시면 되겠지요.
설정하신 시간이 되면 인스턴스가 시작/중지되는 것을 확인하실 수 있습니다.
🔸 로그 확인
정상적으로 동작했는지 보려면 로그를 확인해야 합니다. CloudWatch > 로그 > 로그 그룹으로 이동합니다.
정상적으로 동작했다면 자동으로 로그 그룹이 생성되어 있습니다. 만약, 로그 그룹이 없다면 제대로 동작하지 못한 것입니다.
권한이 제대로 설정되지 않아 함수 자체가 동작하지 않았을 가능성이 높습니다.
제대로 생성되었으면 로그 그룹을 클릭하시면 로그 스트림을 보실 수 있습니다.
말 그대로 스트림이므로, 일정 기간 동안 생성된 로그가 한 스트림으로 묶여있습니다. 즉, 여러 번 실행된 함수의 로그가 한 스트림에 있다는 것이지요.
스트림을 들어가서 결과를 확인해 보면, 정상적으로 상태가 변경되는 로그들이 남는 것을 볼 수 있습니다.
의도한 대로 인스턴스의 상태가 변경 중이면, 로그만 남기고 넘어가는 것을 확인할 수 있습니다.
만약, 나중에 제대로 동작하지 않는다면 해당 로그 그룹을 찾아와서 확인해 보면 됩니다.
로그 스트림의 이름을 수정할 수 있으면 빠르게 볼 수 있겠지만, 아쉽게도 이름을 변경하는 것은 지원하지 않습니다.
정리
Quick Setup에 이어서 Lambda로 직접 EC2 자동 시작/중지 기능을 구현했습니다.
직접 코드를 작성할 수 있으니 자유롭게 활용할 수 있다는 것이 장점입니다.
상황과 조건에 맞는 방식을 선택해서 클라우드 비용을 효율적으로 관리하시면 좋겠습니다.
'DevOps > 클라우드' 카테고리의 다른 글
[AWS] EC2 인스턴스 자동 중지 및 시작하기 - Quick Setup 활용 (0) | 2024.11.23 |
---|