Lambda를 이용한 AWS 자원관리 예시

2020-05-23

.

Data_Engineering_TIL(20200523)

** 베스핀글로벌 빅데이터팀에 정민님이 개발한 AWS 자원관리 Lambda function을 공부하고 작성한 학습노트

[목적]

Lambda function을 이용해서 주기적으로 현재 계정에서 구동되고 있는 자원(EC2,RDS)에 대해 TAG 정보에 따라 expiry-date가 만료되었다던지, TAG 양식이 안맞다던지 등을 하루에 한번씩 체크해서 특정메일로 공지(전송)하는 아키텍처 구현

** 참고사항

1) 룰이벤트는 schedule이고 매주 월요일 ~ 금요일 오전 10:00에 실행되는 구조

2) Lambda function의 외부 라이브러리는 S3 bucket에서 불러오도록 설정

[Lambda function]

  • 람다함수 구조는 아래 그림과 같은 형태

** 해당 람다함수 생성하면서 연결해줘야 하는 IAM

1) SES : E-mail address 확인&전송

2) EC2, RDS : tag정보 확인

3) CloudWatch : log 정보 기록

image

참고로 위에 그림에서는 누락된 비동기식 호출 부분 설정 = 최대 이벤트 수명정보 : 6시간0분0초, 재시도 횟수 : 2

또한 해당 람다함수에서 사용할 외부 라이브러리(판다스 패키지)를 S3에서 임포트할 수 있도록 ‘계층’ 메뉴에서 아래 그림과 같이 설정해준다.

image

CloudWatch Event는 아래와 같이 설정하여 생성해준다.

규칙 생성 -> 이벤트 소스 : 일정 -> Cron 표현식 : 0 10 ? * MON-FRI * (매주 월요일 ~ 금요일 오전 10:00에 실행)-> 대상 : 호출될 lambda function

그리고 참고로 위에 룰이벤트에서 이메일을 보내는 것은 AWS SES(SImple Email Service)라는 서비스를 이용했다. 참고로 서울리전에서 지원하지 않아서 오레곤 리전에서 SES를 이용해 이메일을 보낼것이다. SES에서 어떤 이메일에 보낼건지 아래와 같이 등록해준다.

image

  • Lambda_function.py
import boto3
import datetime
import pandas as pd
import ec2
import rds
import tagging
from botocore.exceptions import ClientError

# expiry-date 기준 날짜
date = datetime.datetime.now() + datetime.timedelta(hours=9) 
# expiry-date 수정이 필요한 날짜
modified_date = date + datetime.timedelta(days=3)

def lambda_handler(event, context):
    # 발신자
    SENDER=event['SENDER']
    # 수신자 endpoint
    RECIPIENT=event['RECIPIENT']
    # region
    AWS_REGION=event['AWS_REGION']
    # 제목.
    SUBJECT=event['SUBJECT']
    # Encoding type
    CHARSET=event['CHARSET']

    # 체크된 tag 정보 받아오기
    ec2_df=ec2.check_state()
    rds_df=rds.check_state()
    tag_df=tagging.tag_state()

    # EC2와 RDS 정보 결합
    df=pd.concat([ec2_df, rds_df])

    # SES resource 만들기
    client = boto3.client('ses',region_name=AWS_REGION)

    # 조건에 맞게 각각의 dataframe filtering
    del_df = df[df['expiry-date'] < date.strftime('%Y-%m-%d')]
    mod_df = df[df['expiry-date'] > modified_date.strftime('%Y-%m-%d')]
    tag_df = tag_df[(tag_df['expiry-date'] == 'X') | (tag_df['name'] == 'X') | (tag_df['owner'] == 'X')]

    # 삭제할 instance가 있으면
    if len(del_df) > 0:
        index_1=list(range(1,len(del_df.index)+1))
        del_df.index=index_1
        print(del_df)
        del_msg=del_df.to_html()

    else:
        del_msg = "오늘은 삭제할 Instance가 없습니다."
    
    # 수정할 instance가 있으면
    if len(mod_df) > 0:
        index_2=list(range(1,len(mod_df.index)+1))
        mod_df.index=index_2
        print(mod_df)
        mod_msg=mod_df.to_html()

    else:
        mod_msg = "오늘은 수정할 Instance가 없습니다."
    
    # tag 수정할 instance가 있으면
    if len(tag_df) > 0:
        index_2=list(range(1,len(tag_df.index)+1))
        tag_df.index=index_2
        print(tag_df)
        tag_msg=tag_df.to_html()

    else:
        tag_msg = "tag 수정할 Instance가 없습니다."

    BODY_HTML  = """\
    <html>
    <head></head>
    <body>
    <h2>안녕하세요.<br> AWS 계정 담당자입니다.</h2><br>
    <p>자동으로 발송되는 메일로 계정내 공용계정관리 정책을 위반한 리소스를 공지합니다.
    <br>
    아래 내용을 확인하여 본인이 생성한 자원에 대해 규정에 맞게 조치해주시기 바랍니다.</p>
    <h3>1) 삭제 권고 - expiry-date 만료</h3>
    <hr>
        {0}
    <br>
    <h3>2) 수정 권고 - expiry-date 3일 초과</h3>
    <hr>
        {1}
    <br>
    <h3>3) tagging 처리 필요 - Name, expiry-date, owner</h3>
    <hr>
        {2}
    <br>

    <h2>감사합니다.<h/2>
    </body>
    </html>
    """.format(del_msg, mod_msg, tag_msg) 

    # E-mail 전송
    try:
        # E-mail 내용
        response = client.send_email(
            Destination={
                'ToAddresses': [
                    RECIPIENT,
                ],
            },
            Message={
                'Body': {
                    'Html': {
                        'Charset': CHARSET,
                        'Data': BODY_HTML,
                    }
                },
                'Subject': {
                    'Charset': CHARSET,
                    'Data': SUBJECT,
                },
            },
            Source=SENDER
        )
    # E-mail 전송시 오류가 발생할 경우
    except ClientError as e:
        print(e.response['Error']['Message'])
    else:

        print("Email sent! Message ID: " + response['MessageId'])
  • ec2.py
import boto3
import pandas as pd


def check_state():

    # 인스턴스의 태그 정보를 저장할 딕셔너리
    instance_lists = {
                'resource' : [],
                'region' : [],
                'instance-name': [],
                'instance-id': [],
                'expiry-date': []
                }

    ec2 = boto3.client(
        'ec2'
        )
    # 허용 리전을 대상으로 수행
    regions_list = ec2.describe_regions()


    for region in regions_list['Regions']:
        print('region : ' + region['RegionName'])
        
        try:
            ec2_list = boto3.resource('ec2', region_name = region['RegionName'])
            instances = ec2_list.instances.all()
            for instance in instances:
                if instance.tags is None:
                    pass
                else:
                    for tag in instance.tags:
                        # 태그가 expiry-date 일때
                        if tag['Key'].strip().lower()=="expiry-date":
                            instance_lists['expiry-date'].append(tag['Value'])
                            instance_lists['resource'].append('EC2')
                            instance_lists['region'].append(region['RegionName'])
                            instance_lists['instance-id'].append(instance.id)

                        # 태그가 name 일때
                        elif tag['Key'].strip().lower()=="name":
                            instance_lists['instance-name'].append(tag['Value'])
                            
                    # 루프를 돌았을 때 name tag와 expiry-date 태그의 갯수가 일치하지 않을 경우
                    if len(instance_lists['instance-name']) > len(instance_lists['expiry-date']):
                        # 마지막에 추가된 name tagdhk id를 제거
                        instance_lists['instance-name'].pop()
                        
                    elif len(instance_lists['instance-name']) < len(instance_lists['expiry-date']):
                        # name tag를 공란으로 추가
                        instance_lists['instance-name'].append('')
                        
        except :
            continue

    df=pd.DataFrame(instance_lists)
    return df
  • rds.py
import boto3
import pandas as pd


def check_state():
    # 인스턴스의 태그 정보를 저장할 딕셔너리
    instance_lists = {
                'resource' : [],
                'region' : [],
                'instance-name': [],
                'instance-id': [],
                'expiry-date': []
                }

    ec2 = boto3.client(
        'ec2'
        )
    # 허용 리전을 대상으로 수행
    regions_list = ec2.describe_regions()


    for region in regions_list['Regions']:
        print('region : ' + region['RegionName'])

        
        # aurora tag 값 기반으로 만료일자가 지난 cluster stop
        try:
            client = boto3.client('rds', region_name = region['RegionName'])
            aurora_clusters = client.describe_db_clusters()['DBClusters']
            rds_instances = client.describe_db_instances()['DBInstances']
            for aurora_cluster in aurora_clusters:
                aurora_tags = client.list_tags_for_resource(ResourceName=aurora_cluster['DBClusterArn'])
                for tag in aurora_tags['TagList']:
                    # 태그가 expiry-date 일때
                    if tag['Key'].strip().lower()=="expiry-date":
                        instance_lists['expiry-date'].append(tag['Value'])
                        instance_lists['resource'].append('RDS')
                        instance_lists['region'].append(region['RegionName'])
                        instance_lists['instance-id'].append(aurora_cluster['DBClusterIdentifier'])

                    # 태그가 name 일때
                    elif tag['Key'].strip().lower()=="name":
                        instance_lists['instance-name'].append(tag['Value'])
                        
                # 루프를 돌았을 때 name tag와 expiry-date 태그의 갯수가 일치하지 않을 경우
                if len(instance_lists['instance-name']) > len(instance_lists['expiry-date']):
                    # 마지막에 추가된 name tagdhk id를 제거
                    instance_lists['instance-name'].pop()

                elif len(instance_lists['instance-name']) < len(instance_lists['expiry-date']):
                    # name tag를 공란으로 추가
                    instance_lists['instance-name'].append('')

        except Exception as e:
            print(e)
            continue

        # rds tag 값 기반으로 만료일자가 지난 instance stop
        try:
            for rds_instance in rds_instances:
                rds_tags = client.list_tags_for_resource(ResourceName=rds_instance['DBInstanceArn'])
                for tag in rds_tags['TagList']:
                    # 태그가 expiry-date 일때
                    if tag['Key'].strip().lower()=="expiry-date":
                        instance_lists['expiry-date'].append(tag['Value'])
                        instance_lists['resource'].append('RDS')
                        instance_lists['region'].append(region['RegionName'])
                        instance_lists['instance-id'].append(rds_instance['DBInstanceIdentifier'])
                    
                    # 태그가 name 일때
                    elif tag['Key'].strip().lower()=="name":
                        instance_lists['instance-name'].append(tag['Value'])

                # 루프를 돌았을 때 name tag와 expiry-date 태그의 갯수가 일치하지 않을 경우
                if len(instance_lists['instance-name']) > len(instance_lists['expiry-date']):
                    # 마지막에 추가된 name tagdhk id를 제거
                    instance_lists['instance-name'].pop()

                elif len(instance_lists['instance-name']) < len(instance_lists['expiry-date']):
                    # name tag를 공란으로 추가
                    instance_lists['instance-name'].append('')

        except Exception as e:
            print(e)
            continue

    df=pd.DataFrame(instance_lists)
    return df
  • tagging.py
import boto3
import pandas as pd

def tag_state():
    # 인스턴스의 태그 정보를 저장할 딕셔너리
    instance_lists = {
                'region' : [],
                'resource' : [],
                'instance-name': [],
                'name': [],
                'expiry-date': [],
                'owner' : []
                }


    # 허용 리전을 대상으로 수행

    ec2 = boto3.client(
        'ec2'
        )
    regions_list = ec2.describe_regions()


    for region in regions_list['Regions']:
        print('region : ' + region['RegionName'])
        
        # aurora tag 값 유무 확인하기
        try:
            client = boto3.client('rds', region_name = region['RegionName'])
            # 모든 cluster를 대상으로 수행
            aurora_clusters = client.describe_db_clusters()['DBClusters']
            # 모든 db instance를 대상으로 수행
            rds_instances = client.describe_db_instances()['DBInstances']
            ec2_list = boto3.resource('ec2', region_name = region['RegionName'])
            # 모든 ec2 instance를 대상으로 수행
            instances = ec2_list.instances.all()
            for aurora_cluster in aurora_clusters:
                aurora_tags = client.list_tags_for_resource(ResourceName=aurora_cluster['DBClusterArn'])
                instance_lists['resource'].append('RDS')
                instance_lists['region'].append(region['RegionName'])
                instance_lists['instance-name'].append(aurora_cluster['DBClusterIdentifier'])
                new_aurora_tags=list()
                for i in range(len(aurora_tags['TagList'])):
                    new_aurora_tags.append(aurora_tags['TagList'][i]['Key'].strip().lower())
                
                # expiry-date tag 확인
                if any('expiry-date' in tag for tag in new_aurora_tags):
                    instance_lists['expiry-date'].append('O')
                else:
                    instance_lists['expiry-date'].append('X')

                # name tag 확인
                if any('name' in tag for tag in new_aurora_tags):
                    instance_lists['name'].append('O')
                else:
                    instance_lists['name'].append('X')

                # owner tag 확인
                if any('owner' in tag for tag in new_aurora_tags):
                    instance_lists['owner'].append('O')
                else:
                    instance_lists['owner'].append('X')

        except Exception as e:
            print(e)
            continue

        # rds tag 값 유무 체크
        try:
            for rds_instance in rds_instances:
                rds_tags = client.list_tags_for_resource(ResourceName=rds_instance['DBInstanceArn'])
                instance_lists['resource'].append('RDS')
                instance_lists['region'].append(region['RegionName'])
                instance_lists['instance-name'].append(rds_instance['DBInstanceIdentifier'])
                new_rds_tags=list()
                for i in range(len(rds_tags['TagList'])):
                    new_rds_tags.append(rds_tags['TagList'][i]['Key'].strip().lower())

                # expiry-date tag 확인
                if any('expiry-date' in tag for tag in new_rds_tags):
                    instance_lists['expiry-date'].append('O')
                else:
                    instance_lists['expiry-date'].append('X')

                # name tag 확인
                if any('name' in tag for tag in new_rds_tags):
                    instance_lists['name'].append('O')
                else:
                    instance_lists['name'].append('X')

                # owner tag 확인
                if any('owner' in tag for tag in new_rds_tags):
                    instance_lists['owner'].append('O')
                else:
                    instance_lists['owner'].append('X')

        except Exception as e:
            print(e)
            continue
        
        # ec2 tag 값 유무 체크
        try:
            for instance in instances:
                instance_lists['resource'].append('EC2')
                instance_lists['region'].append(region['RegionName'])
                
                new_ec2_tags=list()
                for i in range(len(instance.tags)):
                    new_ec2_tags.append(instance.tags[i]['Key'].strip().lower())

                # expiry-date tag 확인
                if any('expiry-date' in tag for tag in new_ec2_tags):
                    instance_lists['expiry-date'].append('O')
                else:
                    instance_lists['expiry-date'].append('X')

                # name tag 확인
                if any('name' in tag for tag in new_ec2_tags):
                    i=new_ec2_tags.index("name")
                    instance_lists['instance-name'].append(instance.tags[i]['Value'])
                    instance_lists['name'].append('O')
                else:
                    instance_lists['name'].append('X')
                    instance_lists['instance-name'].append('X')

                # owner tag 확인
                if any('owner' in tag for tag in new_ec2_tags):
                    instance_lists['owner'].append('O')
                else:
                    instance_lists['owner'].append('X')
        except Exception as e:
            print(e)
            continue


    df=pd.DataFrame(instance_lists)

    return df

[람다함수 구현결과]

아래 그림과 같이 매일 10시에 람다함수에 구현한 양식의 메일이 날라오는 것을 확인할 수 있다.

image