안녕하세요


SRE 5팀 김태훈입니다.


SMB 3파트 고객사 중 한 곳에서 EFS의 Credit이 60G 이하가 될 때마다 SNS가 Lambda를 Trigger하여 자동으로 Throughput 모드를 Bursting에서 Provisioned로 변환해주는 Lambda 스크립트가 구동되고 있습니다.


하지만 원인 모를 이유로 인해 가끔 스크립트가 동작하지 않았고(권한 문제로 추측) 수동으로 모니터링하여 변경해줄 때도 있었습니다.


일련의 일들을 매번 수동으로 하기에 번거로움을 느껴 Lambda 코드를 개선하여 작성하였습니다.


(Runtime을 2.7에서 3.8로 업그레이드 하면서 로직 부분을 일부분 수정하였습니다.)




1. 개요


1) EFS는 EC2와 동일하게 Credit이란 개념이 있으며, 해당 Credit 모드를 다 쓰게 되면 성능이 저하될 수 있습니다.


2) 그 중, Credit 소모량을 결정하는 2가지 모드가 있는데, Bursting Mode와 Provisioned Mode가 있습니다.


3) Credit을 모두 소모하게 되면 Provisioned Mode로 변경되어야 하며, Credit이 꽉 차게 되면 Bursting Mode가 되어야 합니다.




2. 고려해야 할 사항


1) EFS에서 ThroughPut 모드는 변경할 경우 24시간 동안 변경할 수 없습니다.

    => 이로 인해 24시간 이후에 Lambdaa를 Trigger 해야 함


2) 모든 변경 사항은 관리자에게 보고되어야 합니다.

    => EFS의 BurstCreditBalance가 60 G 이하로 떨어질 경우

    => EFS의 ThroughPut Mode가 변경될 경우


3) 각 Service 마다 적절한 권한 설정

    => Event Bridge가 Lambda를 Trigger할 수 있는 권한을 부여하지 않을 경우, 24시간 10분 후에 권한 문제로 인해 Mode를 원복시킬 수 없습니다.


4) Lambda Timeout 설정

    => 3초에서 15초로 변경하였으며, 필자는 Throughput Mode가 변경되었는지 확인하기 위해 Python 내의 sleep 함수를 사용하였습니다.


5) Lambda Tag 설정

    => Lambda Tags에 EFS의 ID, SNS의 ARN, MIBPS (프로비저닝 모드 시 초당 적용할 Mib)를 기입해주었습니다.




3. 구성, 전체적인 과정 요약


구성도 및 전체적인 과정 요약입니다.


1) Cloud Watch에서 EFS의 BurstCreditBalance 메트릭 값을 경보로 걸어 두고, 특정 값 이하(60G)가 되면 경보 발생

    => 경보가 울릴 경우 AWS SNS 서비스로  각 관리자들에게 E-Mail 발송


2) AWS SNS가 발송될 시, Lambda를 Trigger한다.

    => Lambda를 통해 EFS의 Throughput Mode를 Bursting -> Provisioned 모드로 변경

    => Lambda 함수 실행 시각 기준으로 24시간 10분 후에 Lambda를 Trigger하는 Event Bridge 생성


3) 24시간 10분 후에 Lambda가 Evnet Bridge에 의해 Trigger되어 실행

    => Provisioned -> Bursting 모드로 변경

    => 기존의 Event Bridge는 삭제




4. 과정


1. Lambda의 IAM Role, Policy 생성


>> IAM Policy 


: Lambda 리소스가 Script를 구동하면서 필요한 권한들을 추가해주었습니다.


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "sns:publish",
                "lambda:AddPermission",
                "lambda:RemovePermission",
                "lambda:TagResource",
                "lambda:ListTags",
                "elasticfilesystem:UpdateFileSystem",
                "elasticfilesystem:DescribeFileSystems",
                "events:DeleteRule",
                "events:PutTargets",
                "events:DescribeRule",
                "events:EnableRule",
                "events:RemoveTargets",
                "events:ListTargetsByRule",
                "events:DisableRule",
                "events:PutRule",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:CreateLogGroup"
            ],
            "Resource": "*"
        }
    ]
}




^<* IAM Role




3. SNS 생성


^<* 구독 및 구독자 생성




4. Lambda 생성


1) Python Runtime : 3.8

2) 제한 시간 : 15초

3) Tag 값 변경 (상황에 따라 변겨이 필요한 변수들은 Tag로 빼두었습니다.)


4) Code


간단히 코드 설명을 드리자면, Event Source가 SNS인지, Event Bridge인지에 따라 다른 Script를 구동할 수 있도록


if, elif 문을 작성하였습니다.


Event Source가 SNS일 경우 CloudWatch로부터 경보가 발생하여 SNS가 Lambda를 Trigger한 것이기 때문에 Bursting 모드에서 Provisioned 모드로


변경해주었으며, 해당 시각 기준 24시간 10분 후에 Lambda를 Trigger할 수 있도록 Event Bridge를 생성하였습니다.


(주의 사항으로 Event rule이 Lambda를 Trigger하는 권한이 있어야 하므로, Script 내에서 추가해주셔야 합니다.)


Event Source가 Event Bridge일 경우 Provisioned 모드에서 Bursting Mode로 다시 변경해주며, Event rule은 다시 삭제 해주었습니다.


이러한 일련의 과정을 관리자가 파악할 수 있도록 SNS로 보냈습니다.


import boto3
import datetime
from time import sleep

"""
Lambda IAM Policy :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "sns:publish",
                "lambda:AddPermission",
                "lambda:RemovePermission",
                "lambda:TagResource",
                "lambda:ListTags",
                "elasticfilesystem:UpdateFileSystem",
                "elasticfilesystem:DescribeFileSystems",
                "events:DeleteRule",
                "events:PutTargets",
                "events:DescribeRule",
                "events:EnableRule",
                "events:RemoveTargets",
                "events:ListTargetsByRule",
                "events:DisableRule",
                "events:PutRule",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:CreateLogGroup"
            ],
            "Resource": "*"
        }
    ]
}
"""


def lambda_handler(event, context):
    # Set Variables
    r_name = 'ap-northeast-2'
    lambda_fnc_arn = context.invoked_function_arn
    lambda_fnc_name = context.function_name
    rule_name = "lambda_efs_change_rule"
    print(f"function arn, name : {lambda_fnc_arn}, {lambda_fnc_name}")

    efs_cli = boto3.client(service_name='efs', region_name=r_name)
    sns_cli = boto3.client(service_name='sns', region_name=r_name)
    lambda_cli = boto3.client(service_name='lambda', region_name=r_name)
    event_cli = boto3.client(service_name='events', region_name=r_name)

    def get_lambda_tag(lambda_client, resource_arn):
        response = lambda_client.list_tags(
            Resource=resource_arn
        )
        return response.get('Tags')

    def response_check(res):
        try:
            is_succeeded = True
            status_code = res['ResponseMetadata']['HTTPStatusCode']
            if status_code != 200:
                is_succeeded = False
        except Exception:
            is_succeeded = False
        return is_succeeded

    def send_message_to_admin(subject, message, arn):
        try:
            print(subject)
            print(message)
            print(arn)
            if arn:
                response = sns_cli.publish(
                    Subject=subject,
                    TopicArn=arn,
                    Message=message 
                )
                if not response_check(response):
                    raise Exception
                print('[Successes Send Email to Admin]')
            else:
                print('[Failed Send Email to Admin]\nThis lambda does not have a topicArn tag.')
            return True
        except Exception:
            print('[Failed Send Email to Admin]')
            return False

    def get_efs_throughput_mode(id):
        response = efs_cli.describe_file_systems(
            FileSystemId=id
        ).get('FileSystems')[0]
        mode = response.get('ThroughputMode')
        return mode

    def get_lambda_event_source(event):
        # SNS
        try:
            if event.get('Records')[0].get('EventSource') == 'aws:sns':
                return 'sns'
        except Exception:
            pass
        # Event Bridge
        try:
            if event['source'] == 'aws.events':
                return 'scheduled_event'
        except Exception:
            pass

    # Get provisioned_mibps & sns_arn
    lambda_tags = get_lambda_tag(lambda_cli, lambda_fnc_arn)
    efs_mibps = int(lambda_tags.get('mibps'))
    sns_arn = lambda_tags.get('sns_arn')
    efs_id = lambda_tags.get('efs_id')
    print(efs_id)
    
    print(event)

    # Check who triggered lambda.
    event_src = get_lambda_event_source(event)
    print(f"event source : {event_src}")

    # Check EFS Throughput Mode and get name
    efs_mode = get_efs_throughput_mode(id=efs_id)
    efs_name = efs_cli.describe_file_systems(
        FileSystemId=efs_id
    ).get('FileSystems')[0].get('Name')

    # Source == SNS
    if event_src == "sns":
        # Bursting -> Provisioned
        if efs_mode == "bursting":
            # Update EFS Mode to Provisioned
            efs_cli.update_file_system(
                FileSystemId=efs_id,
                ThroughputMode="provisioned",
                ProvisionedThroughputInMibps=efs_mibps)

            sleep(3)
            after_efs_mode = get_efs_throughput_mode(
                id=efs_id
            )

            if efs_mode != after_efs_mode:
                # Succeeded mode update, Send Success Message to Admin
                success_sns_message = 'Successfully updated ' + efs_name + "\n변경 대상: " + efs_name +\
                            " (" + efs_id + ") " + "\n변경 사항 : " + efs_mode + " => " + after_efs_mode
                send_message_to_admin(
                    subject="[EFS Automation Success] " + efs_name,
                    message=success_sns_message,
                    arn=sns_arn
                    )

                # Create Event Bridge rule to change to bursting mode after 24 hours and 10 minutes.
                cron_date = datetime.datetime.today() + datetime.timedelta(days=1, minutes=10)
                expression = 'cron(' + str(cron_date.minute) + ' ' + str(cron_date.hour) + ' ' + str(
                    cron_date.day) + ' * ? *' + ')'
                
                # 1. Put Rule
                rule = event_cli.put_rule(
                    Name=rule_name,
                    ScheduleExpression=expression,
                    State="ENABLED"
                )
                print(rule)
                
                # 2. Put Targets
                putted_target = event_cli.put_targets(
                    Rule=rule_name,
                    Targets=[
                        {
                            "Id": rule_name,
                            "Arn": lambda_fnc_arn,
                        }
                    ]
                )
                print(putted_target)

                # 3. Add Permission
                addad_perm = lambda_cli.add_permission(
                    FunctionName=lambda_fnc_name,
                    StatementId="LambdaInvokeRule",
                    Action="lambda:InvokeFunction",
                    Principal="events.amazonaws.com",
                    SourceArn=rule.get('RuleArn')
                )
                print(addad_perm)
                
            else:
                # Failed File System mode update, Send Fail Message to Admin
                failure_sns_message = 'Failed update of ' + efs_name
                send_message_to_admin(
                    subject="[EFS Automation Failure] " + efs_name,
                    message=failure_sns_message,
                    arn=sns_arn
                )

        # If Provisioned
        else:
            print("EFS is already provisioned mode.")

    # Source == Event Bridge
    if event_src == "scheduled_event":
        # Provisioned -> Bursting
        if efs_mode == "provisioned":
            # Update EFS Mode to Bursting

            efs_updater = efs_cli.update_file_system(
                FileSystemId=efs_id,
                ThroughputMode="bursting")
            
            sleep(3)
            after_efs_mode = get_efs_throughput_mode(
                id=efs_id
            )
            
            if efs_mode != after_efs_mode:
                # Succeeded mode update, Send Success Message to Admin
                sns_message = 'Successfully updated ' + efs_name + "\n변경 대상: " + efs_name + " (" + efs_id + ") " + \
                    "\n변경 사항 : " + efs_mode + " => " + \
                    after_efs_mode
                send_message_to_admin(
                    subject="EFS Automation Alarm Success " + efs_name,
                    message=sns_message,
                    arn=sns_arn
                )

                # Delete Event Bridge rule because event rule successfully changed EFS Mode to bursting
                # 1. Remove Target
                removed_rule = event_cli.remove_targets(
                    Rule=rule_name,
                    Ids=[
                        rule_name
                    ]
                )
                print(removed_rule)

                # 2. Delete Rule
                deleted_rule = event_cli.delete_rule(
                    Name=rule_name
                )
                print(deleted_rule)

                # 3. Remove Permission
                removed_perm = lambda_cli.remove_permission(
                    FunctionName=lambda_fnc_arn,
                    StatementId="LambdaInvokeRule"
                )
                print(removed_perm)

            else:
                # Failed File System mode update, Send Fail Message to Admin
                sns_message = 'Failed update of ' + efs_name
                send_message_to_admin(
                    subject="[EFS Automation Failure] " + efs_name,
                    message=sns_message,
                    arn=sns_arn
                )
        else:
            print("EFS is already bursting mode.")



5) Lambda Tags


: efs의 id나 sns의 arn 등을 Tags 값으로 설정하고 하기의 함수로부터 값을 가져올 수 있도록 설정하였습니다.


def get_lambda_tag(lambda_client, resource_arn):

    response = lambda_client.list_tags(

        Resource=resource_arn

    )

    return response.get('Tags')




6. CloudWatch 경보 생성


임계값 : 60G로 설정,

Metric Name : BurstCreditBalance

Action : SNS로 Notification 발생





5. 결과


임계치 (60G)에 도달할 경우 자동으로 EFS의 BurstCreditBalance가 변경되어 Credit이 다시 축적되는 것을 확인할 수 있으며, 정확히 24시간 10분 이후에 Bursting Mode로 변경되는 것을 보실 수 있습니다.



또한, 관리자들이 해당 내용을 파악할 수 있도록 E-Mail도 오고 있습니다.








사소하지만 간단한 스크립트 작성으로 퇴근 후에도 좀 더 여유로운 시간을 보낼 수 있게 되었습니다.


이상 글 마치도록 하겠습니다. 읽어 주셔서 감사합니다.