EC2들이 실수로 계속 켜져있어 과금되는 것을 방지
하기 위해서였다. 이 경우에 EC2 서버의 작동 여부는 쉽게 알아챌 수 있지만 stop이 되어도 계속 과금이 발생하고 있는 곳이 하나가 있었다. 바로 EBS Volume이다.0.114 USD/GB
로 만약 EC2 생성시 함께 붙는 32gb 볼륨을 선택했다면 EBS 가격으로만 매월 약 5000원 정도가 나가게 되는 것이다. 서버가 많아질수록 당연히 볼륨의 용량은 늘어나게 되고 그렇게 된다면 서버가 Stop 상태에 있어 우리가 AWS 서비스를 직접적으로 사용하지 않더라도 매월 무시할 수 없을 정도의 비용이 야금야금 나가게 되는 것이다.
볼륨에 대한 스냅샷을 만들어 스냅샷을 보관하고 볼륨은 지우는 것
이다. 스냅샷의 경우 유지 비용이 EBS의 절반 이하인 0.05 USD/GB
이므로 상당한 비용 절감을 할 수 있다.기존에는 단순히 EC2를 시작하고 멈추는 기능만 있었다면 이번에 추가된 기능은 아래와 같다.
- EC2 Stop 버튼 -> 스냅샷 생성 -> EBS 볼륨 분리 -> 볼륨 삭제 -> EC2 멈춤
- EC2 Start 버튼 -> 스냅샷에서 볼륨 생성 -> 볼륨 연결 -> 스냅샷 삭제 -> EC2 시작
이렇게 서버가 멈추면서 볼륨에 대한 스냅샷만 남고 볼륨은 지워지며, 서버 재시작시 스냅샷을 다시 볼륨으로 만들고 서버에 연결 후 서버를 시작하여 기존에 비해 더 많은 비용 절감이 가능하게 되었다.
아래 함수는 특정 EC2에 stop 명령이 내려졌을 때, Lambda에서 Eventbridge로 서버의 상태를 모니터링 하고있다가 Stopped 상태가 되면 Trigger를 받아 실행되는 함수이다.
크게 네 부분으로 나눠져있다.
import boto3, time, json
# boto3 client 및 resource 생성
ec2 = boto3.resource('ec2')
ec2_client = boto3.client('ec2')
# 인스턴스 정보 받아오는 함수로 서버의 이름, EBS 연결 path, 가용영역이 return으로 나온다.
def describe_ec2(instance_id):
ec2_client = boto3.client('ec2')
response = ec2_client.describe_instances(InstanceIds = [instance_id])
instance_name = response['Reservations'][0]['Instances'][0]['Tags'][0]['Value']
volume_path = response['Reservations'][0]['Instances'][0]['BlockDeviceMappings'][0]['DeviceName']
availability_zone = response['Reservations'][0]['Instances'][0]['Placement']['AvailabilityZone']
return instance_name, volume_path, availability_zone
def lambda_handler(event, context):
instance_id = event['detail']['instance-id']
instance_name, volume_path, availability_zone = describe_ec2(instance_id)
# EC2이름 + snapshot 으로 스냅샷의 이름을 명명
snapshot_name = instance_name + '-snapshot'
# Get the instance
instance = ec2.Instance(instance_id)
# EC2에 연결된 특정 볼륨 검색
for vol in instance.volumes.all():
volume = vol
if vol.attachments[0]['Device'] == volume_path:
volume_id = vol.id
break
# 스냅샷 목록을 받아와 기존에 해당 서버에 대한 스냅샷이 남아있다면 삭제
response_snapshot = ec2_client.describe_snapshots(
Filters=[
{
'Name': 'tag:Name',
'Values': [
f'{instance_name}-snapshot',
]
},
]
)
if response_snapshot['Snapshots']:
snapshot_id = response_snapshot['Snapshots'][0]['SnapshotId']
ec2_client.delete_snapshot(SnapshotId=snapshot_id)
# 위에서 받아온 정보들을 Tag에 넣어 볼륨에 대한 스냅샷 생성
ec2.create_snapshot(
VolumeId=volume_id,
Description=f'{instance_name}-snapshot',
TagSpecifications=[
{
'ResourceType' : 'snapshot',
'Tags' : [
{
'Key' : 'Name',
'Value' : snapshot_name
},
{
'Key' : 'AvailabilityZone',
'Value' : availability_zone
},
{
'Key' : 'instanceId',
'Value' : instance_id
},
{
'Key' : 'DeviceName',
'Value' : volume_path
}
]
}
]
)
# EC2로부터 볼륨 분리
response_detach = volume.detach_from_instance(Device=volume_path, InstanceId=instance_id)
# 볼륨 상태가 available 될 때까지 기다렸다가 볼륨 삭제
while True:
response_volumes = ec2_client.describe_volumes(VolumeIds = [volume_id])
volume_state = response_volumes['Volumes'][0]['State']
if volume_state == 'available':
response_delete = ec2_client.delete_volume(VolumeId = volume_id)
return True
time.sleep(2)
print("response_detach : ", json.dumps(response_detach, default=str))
print("response_volumes : ", json.dumps(response_volumes, default=str))
print("response_delete : ", json.dumps(response_delete, default=str))
아래 함수는 특정 EC2에 start 명령이 내려졌을 때, 해당 서버의 스냅샷으로부터 볼륨을 생성하여 연결해주는 함수이다.
크게 네 부분으로 나눠져있다.
import boto3, json
ec2 = boto3.resource('ec2')
ec2_client = boto3.client('ec2')
def describe_elements(instance_name, type):
filter = {
'Name': 'tag:Name',
'Values': [f'{instance_name}-{type}']
}
if type == 'snapshot':
response = ec2_client.describe_snapshots(Filters=[filter])
if type == 'ebs':
response = ec2_client.describe_volumes(Filters=[filter])
return response
def attach_volume_to_ec2(instance_name):
tag_dict = {}
# 저장되어 있는 스냅샷에 대한 정보를 가져옴
response_snapshot = describe_elements(instance_name, 'snapshot')
# 스냅샷의 Tag로부터 아래의 정보들을 가져옴
tags = response_snapshot['Snapshots'][0]['Tags']
for t in tags:
tag_dict[t['Key']] = t['Value']
instance_id = tag_dict['instanceId']
volume_path = tag_dict['DeviceName']
availability_zone = tag_dict['AvailabilityZone']
snapshot_id = response_snapshot['Snapshots'][0]['SnapshotId']
# 스냅샷으로 새로운 EBS 볼륨 생성
volume = ec2.create_volume(
SnapshotId=snapshot_id,
AvailabilityZone=availability_zone,
TagSpecifications=[
{
'ResourceType' : 'volume',
'Tags' : [
{
'Key' : 'Name',
'Value' : f'{instance_name}-ebs',
}
]
}
]
)
# 생성한 볼륨 상태가 available일 때까지 기다림
while True:
response_volume = describe_elements(instance_name, 'ebs')
current_volume = [i for i in response_volume['Volumes'] if i['State'] != 'deleting'][0]
volume_state = current_volume['State']
if volume_state == 'available':
break
volume_id = current_volume['VolumeId']
instance = ec2.Instance(instance_id)
# 새로 생성한 볼륨을 원래의 instance에 연결
response_attach = instance.attach_volume(
VolumeId=volume_id,
Device=volume_path
)
while True:
response_volume = ec2_client.describe_volumes(VolumeIds=[volume_id])
volume_state = response_volume['Volumes'][0]['State']
if volume_state == 'in-use':
print("response_attach : ", json.dumps(response_attach, default=str))
return True
else: time.sleep(2)
EC2 START
Start EC2 버튼을 눌러 EC2가 실행되는 것을 확인한다.
볼륨 생성 후 연결이 제대로 되지 않으면 실행 자체가 안되기 때문에 running 상태로 잘 바뀌었다면 로직이 잘 작동했다고 볼 수 있다.
볼륨 연결 확인
EC2 STOP
스냅샷 생성 확인
서버가 stopped 상태로 바뀐 후 곧바로 Lambda 함수가 실행되어 스냅샷이 생성된 것을 볼 수 있다.
볼륨 분리 후 삭제 확인
Lambda URL code + EBS attach
EBS detach, delete & create snapshot
Lambda-EC2-snapshot.zip -> EC2 stopped 상태에 대한 eventbridge trigger 설정 필요