[AWS/PYTHON] Lambda URL을 이용하여 EC2 stop/run 기능을 가진 Serverless 정적 웹사이트 Upgrade 버전

NewNewDaddy·2023년 11월 6일
0

AWS

목록 보기
10/13
post-thumbnail

0. INTRO

최초 버전 : Lambda를 이용하여 EC2 stop/run 기능을 가진 Serverless 정적 웹사이트 만들기

  • 앞선 글 'Lambda를 이용하여 EC2 stop/run 기능을 가진 Serverless 정적 웹사이트 만들기' 에서는 Lambda URL 기능을 활용하여 어느 기기에서든 내 계정에 있는 EC2를 start / stop 시킬 수 있는 간단한 HTML UI를 만들어보았다.
  • 이 기능을 만든 이유는 다양한 기기에서 쉽게 접근하여 계정 내에 작동중인 EC2들이 실수로 계속 켜져있어 과금되는 것을 방지하기 위해서였다. 이 경우에 EC2 서버의 작동 여부는 쉽게 알아챌 수 있지만 stop이 되어도 계속 과금이 발생하고 있는 곳이 하나가 있었다. 바로 EBS Volume이다.
  • EBS의 경우 타입에 따라 비용이 다르지만 우리가 흔히 사용하는 범용 SSD(gp2) 볼륨의 경우 0.114 USD/GB로 만약 EC2 생성시 함께 붙는 32gb 볼륨을 선택했다면 EBS 가격으로만 매월 약 5000원 정도가 나가게 되는 것이다. 서버가 많아질수록 당연히 볼륨의 용량은 늘어나게 되고 그렇게 된다면 서버가 Stop 상태에 있어 우리가 AWS 서비스를 직접적으로 사용하지 않더라도 매월 무시할 수 없을 정도의 비용이 야금야금 나가게 되는 것이다.
  • 이러한 EBS 볼륨 비용 절약을 위해 사용할 수 있는 방법이 볼륨에 대한 스냅샷을 만들어 스냅샷을 보관하고 볼륨은 지우는 것이다. 스냅샷의 경우 유지 비용이 EBS의 절반 이하인 0.05 USD/GB 이므로 상당한 비용 절감을 할 수 있다.

  • 기존에는 단순히 EC2를 시작하고 멈추는 기능만 있었다면 이번에 추가된 기능은 아래와 같다.

    1. EC2 Stop 버튼 -> 스냅샷 생성 -> EBS 볼륨 분리 -> 볼륨 삭제 -> EC2 멈춤

    2. EC2 Start 버튼 -> 스냅샷에서 볼륨 생성 -> 볼륨 연결 -> 스냅샷 삭제 -> EC2 시작
  • 이렇게 서버가 멈추면서 볼륨에 대한 스냅샷만 남고 볼륨은 지워지며, 서버 재시작시 스냅샷을 다시 볼륨으로 만들고 서버에 연결 후 서버를 시작하여 기존에 비해 더 많은 비용 절감이 가능하게 되었다.

    1. 본문

    1. STOP 명령 -> 볼륨 분리 후 스냅샷 생성 함수

    • 아래 함수는 특정 EC2에 stop 명령이 내려졌을 때, Lambda에서 Eventbridge로 서버의 상태를 모니터링 하고있다가 Stopped 상태가 되면 Trigger를 받아 실행되는 함수이다.

    • 크게 네 부분으로 나눠져있다.

      1. 해당 EC2에 대한 기존 스냅샷이 있다면 삭제
      2. Tag 4개(스냅샷 이름, 가용 영역, Instance ID, Device Name)를 붙여 볼륨으로부터 스냅샷 생성
      3. EC2로부터 볼륨 분리
      4. 볼륨 상태가 'available'이면 해당 볼륨 삭제
      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))

    2. START 명령 -> 스냅샷으로부터 볼륨 생성 후 서버에 연결 함수

    • 아래 함수는 특정 EC2에 start 명령이 내려졌을 때, 해당 서버의 스냅샷으로부터 볼륨을 생성하여 연결해주는 함수이다.

    • 크게 네 부분으로 나눠져있다.

      1. 저장된 스냅샷에 대한 정보 검색
      2. 스냅샷의 Tag 4개(스냅샷 이름, 가용 영역, Instance ID, Device Name)의 정보를 활용하여 새로운 EBS 볼륨 생성
      3. 생성한 볼륨이 'available' 상태가 되면 서버에 볼륨 연결
      4. 볼륨 연결이 성공하여 볼륨이 'in-use' 상태가 되면 함수 종료 후 EC2를 재시작하는 함수 실행
      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)

2. 실행

  1. EC2 START

    • Start EC2 버튼을 눌러 EC2가 실행되는 것을 확인한다.

    • 볼륨 생성 후 연결이 제대로 되지 않으면 실행 자체가 안되기 때문에 running 상태로 잘 바뀌었다면 로직이 잘 작동했다고 볼 수 있다.

  1. 볼륨 연결 확인

    • 실행한 시간이 11월 6일 9시 14분 즈음이므로 위의 Start 명령을 통해 새로운 볼륨이 스냅샷을 통해 생성되고 서버에 연결된 것을 볼 수 있다.
  2. EC2 STOP

    • Stop EC2 버튼을 눌러 EC2가 정지되는 것을 확인한다.
    • 상태가 'stopped'로 바뀌면 eventbridge trigger를 통해 볼륨 분리 후 스냅샷 생성하는 Lambda를 실행시킨다.
  3. 스냅샷 생성 확인

    • 서버가 stopped 상태로 바뀐 후 곧바로 Lambda 함수가 실행되어 스냅샷이 생성된 것을 볼 수 있다.

  4. 볼륨 분리 후 삭제 확인

    • 스냅샷이 생성되었으므로 정지된 EC2에 연결되었던 볼륨이 분리되고 삭제까지 되는 것을 확인할 수 있다.

3. Lambda 코드 공유

  1. Lambda URL code + EBS attach

    Lambda-EC2-Controller-Upgrade.zip

  2. EBS detach, delete & create snapshot

    Lambda-EC2-snapshot.zip -> EC2 stopped 상태에 대한 eventbridge trigger 설정 필요

4. OUTRO

  • EC2 서버를 시작하고 멈추는 기능에 EBS 볼륨 삭제/생성 기능까지 추가되어 개인적인 클라우드 개발환경에서 더 많은 요금 절약을 기대할 수 있게 되었다.
profile
데이터 엔지니어의 작업공간 / #PYTHON #CLOUD #SPARK #AWS #GCP #NCLOUD

0개의 댓글