[AWS 비용 절감 #2] EC2 인스턴스 예약 실행으로 비용 최적화
목표: 개발 서버 EC2 인스턴스를 평일 오전 10:00부터 오후 10:00까지만 활성화하여 불필요한 비용 발생을 방지한다.
구현 방식:
- EventBridge (이벤트 브리지): 특정 시간에 Lambda 함수를 트리거하는 스케줄러 역할을 한다.
- Lambda (람다): EC2 인스턴스를 시작하고 중지하는 코드를 실행한다.
- IAM (Identity and Access Management): Lambda와 EC2가 서로 통신하고 필요한 작업을 수행할 수 있도록 권한을 부여한다.
- SSM (Systems Manager): Lambda가 EC2 인스턴스 내부에서 명령어를 실행할 수 있도록 돕는다.
블루/그린 배포 환경 고려:
개발 서버는 블루/그린 배포 환경으로 구성되어 있다. Lambda 코드는 EC2 인스턴스 내의 파일을 확인하여 현재 활성화된 서버(블루 또는 그린)를 파악하고, 해당 서버만 실행하도록 구현한다.
전체 흐름 요약
- IAM 역할 생성: Lambda와 EC2에 필요한 권한을 부여하는 역할을 각각 생성한다.
- Lambda 함수 생성: EC2 인스턴스를 시작하고 중지하는 Python 코드로 Lambda 함수를 생성한다.
- EC2에 IAM 역할 부여: Lambda가 SSM을 통해 EC2 내부에서 명령어를 실행할 수 있도록 EC2에 생성한 IAM 역할을 연결한다.
- EventBridge 규칙 생성: 정해진 시간에 Lambda 함수를 트리거하도록 EventBridge 규칙을 생성하고 Lambda 함수와 연결한다.
1. IAM 역할 생성
1.1. Lambda를 위한 IAM 역할 생성
경로: IAM > 역할 > 역할 만들기
Lambda가 EC2를 제어하고, EC2 인스턴스가 켜졌을 때 스크립트를 실행하여 Docker 컨테이너를 실행할 수 있도록 미리 IAM 역할을 생성한다.

필요 권한:
AmazonEC2FullAccess: EC2 인스턴스를 제어하기 위한 권한AmazonSSMFullAccess: SSM을 통해 EC2 내부에서 명령어를 실행하기 위한 권한
위 권한을 추가하고 원하는 역할 이름을 설정하여 IAM 역할 생성을 완료한다.
1.2. EC2를 위한 IAM 역할 생성
경로: IAM > 역할 > 역할 만들기
EC2 인스턴스에서 SSM Agent가 정상적으로 동작하기 위해서는 AmazonSSMManagedInstanceCore 권한이 필요하다. 위와 동일한 방법으로 해당 권한을 가진 IAM 역할을 생성한다.
2. Lambda 함수 생성
경로: Lambda > 함수 > 함수 생성

- 함수 이름: 원하는 함수 이름을 입력한다.
- 런타임:
Python 3.9(또는 원하는 언어)를 선택한다. - 아키텍처:
x86_64를 선택한다. - 권한: "기존 역할 사용"을 선택하고, 1.1. Lambda를 위한 IAM 역할 생성에서 생성한 IAM 역할을 연결한다.

Lambda 코드 작성
import boto3
import datetime
import time
region = 'ap-northeast-2 (REGION)'
instance_id = 'ec2-인스턴스-id'
ssm_doc_path = '/home/ubuntu/nginx/conf.d/service-container.inc' (ec2 종료되기전 실행했던 blue, green 이 저장되있는파일)
ec2 = boto3.client('ec2', region_name=region)
ssm = boto3.client('ssm', region_name=region)
def lambda_handler(event, context):
now = datetime.datetime.utcnow()
hour = now.hour
minute = now.minute
# 서버를 킨다.
if hour == 1 and 0 <= minute <= 5: # 10:00 KST = 01:00 UTC
# EC2 시작
ec2.start_instances(InstanceIds=[instance_id])
print("EC2 started.")
# SSM Agent Online 될 때까지 기다리기
while True:
response = ssm.describe_instance_information(
Filters=[
{
'Key': 'InstanceIds',
'Values': [instance_id]
}
]
)
instance_info = response['InstanceInformationList']
if instance_info and instance_info[0]['PingStatus'] == 'Online':
print("SSM Agent is Online!")
break
else:
print("SSM Agent not ready... waiting 5s")
time.sleep(5)
# 2️⃣ dev-blue / dev-green 제외 나머지 서비스 up
pre_command = f"""
cd /home/ubuntu
docker compose config --services | grep -vE "dev-blue|dev-green" | xargs -r docker compose up -d
"""
response_pre = ssm.send_command(
InstanceIds=[instance_id],
DocumentName="AWS-RunShellScript",
Parameters={'commands': [pre_command]},
)
print("SSM Run Command for other services sent:", response_pre)
# 3️⃣ dev-blue / dev-green 중 하나만 up
command = f"""
cd /home/ubuntu
if grep -q "dev-blue" {ssm_doc_path}; then
docker compose up -d dev-blue
elif grep -q "dev-green" {ssm_doc_path}; then
docker compose up -d dev-green
else
echo "No active container info."
fi
"""
response = ssm.send_command(
InstanceIds=[instance_id],
DocumentName="AWS-RunShellScript",
Parameters={'commands': [command]},
)
print("SSM Run Command for dev-blue/green sent:", response)
# ✅ 2️⃣ 서버 종료
elif hour == 13 and 0 <= minute <= 1: # 22:00 KST = 13:00 UTC
ec2.stop_instances(InstanceIds=[instance_id])
print("EC2 stopped.")
else:
print("No action at this time.")
print(f"Current time: {hour}:{minute}")
작성한 코드를 Lambda 함수에 적용하고 Deploy 버튼을 클릭한다.
Lambda 제한 시간 설정:
경로: 구성 > 일반 구성 > 편집 > 제한 시간
Lambda 함수의 기본 제한 시간은 3초이다. EC2 인스턴스 시작 및 스크립트 실행에 시간이 더 걸릴 수 있으므로 제한 시간을 충분히 늘려준다. (예: 1분)
3. EC2에 IAM 역할 부여
경로: EC2 > 인스턴스 > (해당 인스턴스 선택) > 작업 > 보안 > IAM 역할 수정
1.2. EC2를 위한 IAM 역할 생성에서 생성한 AmazonSSMManagedInstanceCore 권한을 가진 IAM 역할을 EC2 인스턴스에 연결한다.
4. EventBridge 규칙 생성
경로: Amazon EventBridge > 규칙 > 규칙 생성
EC2 인스턴스를 시작하고 중지하는 두 가지 규칙을 각각 생성한다.

4.1. EC2 시작 규칙
규칙 유형: 일정을 선택한다.
Cron 표현식:
0 1 ? * MON-FRI *(매주 월요일부터 금요일까지 01:00 UTC에 실행)- 참고: KST 기준 오전 10:00는 UTC 기준 01:00이다.

대상 선택:
- 대상:
Lambda 함수 - 함수: 위에서 생성한 Lambda 함수를 선택한다.
- 대상:
4.2. EC2 중지 규칙
규칙 유형: 일정을 선택한다.
Cron 표현식:
0 13 ? * MON-FRI *(매주 월요일부터 금요일까지 13:00 UTC에 실행)- 참고: KST 기준 오후 10:00는 UTC 기준 13:00이다.
대상 선택: 시작 규칙과 동일하게 설정한다.
세 줄 요약
- IAM 역할을 생성하여 Lambda와 EC2에 필요한 권한을 부여한다.
- EC2를 켜고 끄는 Lambda 함수를 작성하고, EventBridge를 사용하여 특정 시간에 함수를 트리거하도록 설정한다.
- EC2에 IAM 역할을 연결하여 Lambda가 SSM을 통해 원격으로 명령을 실행할 수 있도록 한다.
📌 업그레이드 버전 (2026년 1월)
기존 버전에서는 사용자가 지정한 시간에 EventBridge에 의해 서버가 실제로 운영 중인지 확인하기 어려웠습니다.
해당 어려움을 자동 스케줄러에 Slack 알림 기능을 추가하여 서버 상태를 실시간으로 확인할 수 있도록 개선했습니다.
주요 개선사항
1. Slack Webhook 알림 기능 추가
- 서버 시작 시: "🚀 개발 서버를 시작합니다... (10:00 KST)"
- 서버 종료 시: "🛑 개발 서버가 종료되었습니다. (22:00 KST)"
- 이미 실행 중일 때: "ℹ️ 개발 서버가 이미 실행 중입니다."
- 모든 서비스 시작 완료 시: "✅ 개발 서버와 모든 서비스가 정상적으로 시작되었습니다!"
2. 개선된 Lambda 코드
import boto3
import datetime
import time
import json
import os
import urllib.request
region = 'ap-northeast-2' # 예: ap-northeast-2
instance_id = 'instance-id'
ec2 = boto3.client('ec2', region_name=region)
ssm = boto3.client('ssm', region_name=region)
# Slack Webhook URL (환경 변수에서 가져오기)
SLACK_WEBHOOK_URL = os.environ.get('SLACK_WEBHOOK_URL', '')
def send_slack_message(text):
"""Slack Webhook으로 메시지 전송"""
if not SLACK_WEBHOOK_URL:
print("SLACK_WEBHOOK_URL not set, skipping notification")
return
payload = json.dumps({"text": text}).encode('utf-8')
req = urllib.request.Request(
SLACK_WEBHOOK_URL,
data=payload,
headers={'Content-Type': 'application/json'},
method='POST'
)
try:
urllib.request.urlopen(req)
print(f"Slack message sent: {text}")
except Exception as e:
print(f"Failed to send Slack message: {e}")
def lambda_handler(event, context):
now = datetime.datetime.utcnow()
hour = now.hour
minute = now.minute
# ------------------------------------------------------------
# EC2 상태 확인
# ------------------------------------------------------------
def is_instance_running():
res = ec2.describe_instances(InstanceIds=[instance_id])
state = res['Reservations'][0]['Instances'][0]['State']['Name']
return state == "running"
# ------------------------------------------------------------
# STARTUP (KST 10:00 → UTC 01:00)
# ------------------------------------------------------------
if hour == 1 and 0 <= minute <= 20:
# 이미 켜져 있으면 전부 skip
if is_instance_running():
print("EC2 already running. Skip ALL startup steps.")
send_slack_message("ℹ️ 개발 서버가 이미 실행 중입니다. (스케줄: 10:00 KST)")
return {"status": "skipped_running"}
# 1) EC2 켜기
ec2.start_instances(InstanceIds=[instance_id])
print("EC2 started.")
send_slack_message("🚀 개발 서버를 시작합니다... (10:00 KST)")
# 2) SSM Agent Online 대기
while True:
info = ssm.describe_instance_information(
Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
)['InstanceInformationList']
if info and info[0]['PingStatus'] == 'Online':
print("SSM Agent Online.")
break
print("Waiting for SSM Agent… (5s)")
time.sleep(5)
# TODO: 배포에 필요한 작업
send_slack_message("✅ 개발 서버와 모든 서비스가 정상적으로 시작되었습니다!")
return {"status": "started"}
# ------------------------------------------------------------
# SHUTDOWN (KST 22:00 → UTC 13:00)
# ------------------------------------------------------------
elif hour == 13 and 0 <= minute <= 20:
ec2.stop_instances(InstanceIds=[instance_id])
print("EC2 stopped.")
send_slack_message("🛑 개발 서버가 종료되었습니다. (22:00 KST)")
return {"status": "stopped"}
# ------------------------------------------------------------
# NOTHING
# ------------------------------------------------------------
else:
print(f"No action at this time. ({hour}:{minute})")
return {"status": "noop", "time": f"{hour}:{minute}"}
3. 환경 변수 설정
Lambda 함수에 다음 환경 변수를 추가해야 합니다:
SLACK_WEBHOOK_URL: Slack Incoming Webhook URL
경로: Lambda > 함수 > 구성 > 환경 변수 > 편집
4. 기존 코드와의 차이점
| 구분 | 기존 버전 | 업그레이드 버전 |
|---|---|---|
| 알림 기능 | ❌ 없음 | ✅ Slack 알림 지원 |
| 서버 중복 실행 감지 | 단순 skip | Slack 알림으로 명확히 표시 |
| 서비스 시작 완료 알림 | ❌ 없음 | ✅ 완료 메시지 전송 |
| 코드 구조 | 인라인 코드 | 함수 분리로 가독성 향상 |
'AWS' 카테고리의 다른 글
| CloudWatch Logs 를 통해 로그 수집하기 (0) | 2025.12.26 |
|---|---|
| CloudFormation을 활용한 이미지 변환 및 리사이징 (0) | 2025.11.18 |
| [AWS 비용 절감 #3]slack과 aws의 lambda 연동 (3) | 2025.07.09 |
| [AWS 비용 절감 #1] S3 + CloudFront + HTTPS 정적 웹사이트 (0) | 2025.05.19 |