본문 바로가기

AWS

AWS Elemental MediaConvert 사용 후기 (feat. Lambda)

어느 날 회사 내 모든 고객사 홈페이지에 비디오 스트리밍이 막혔다는 연락을 받고 무슨 일인지 확인하러 갔다.

기존에 사용하고 있던 업체에서 트래픽을 너무 과도하게 사용하여 서빙을 중지하고 돈을 더 내지 않으면 계속 미디어 스트리밍을 중지하겠단 연락을 받은 것이다. (협박일지도?)

 

굉장히 당혹 스러웠다.

얘기를 들어본 바 트래픽 사이즈가 그렇게 큰 것도 아니었고 한데 비용이 너무 과하게 청구된 것이었다.

 

해당 이슈를 해결하기 위해 다음 사항이 필요했다.

1. 어떤 영상이던 특정 해상도를 지원하도록 수정할 것. (1080p, 480p, 720p 등등)

2. 트래픽의 제한이 없어야 한다.

3. 서버에 대한 영향을 받지 않아야 한다. (ecs, ec2 등을 이용해 서빙시 서버의 관리 코스트 증가 이슈 발생)

 

어떻게 할지 고민하던 차에 예전에 세미나에서 들었던 MediaConvert 기능이 생각이 났다.

밑져야 본전이고 어차피 AWS도 사용하니 써봐야겠다고 생각했다.

 

기존 업체에선 4k ~ sd 그래픽까지 다양한 옵션으로 포맷을 지원하고 있었다.

하지만 고객사의 패턴을 분석한 결과 4k는 필요없고 1080p, 480p, 720p 정도면 될것 같다고 영업측에서 전달을 해주었고 그 포맷 정도로만 중복으로 처리해서 해결을 하는 것으로 방향을 잡았다.

 

 

1. S3에 파일 추가 (origin_new)

2. S3 트리거에 의해 Lambda Action 발동

3. Media Convert 동작

4. 파일 변환 후 S3에 적재 (converted_new)

5. Cloudfront로 Media Cache

 

 

람다 코드는 아래와 같다.

import os
import boto3
import json
import http.client
import urllib.parse

def lambda_handler(event, context):
    # Extract S3 bucket and object key from the event
    s3_bucket = event['Records'][0]['s3']['bucket']['name']
    s3_key = event['Records'][0]['s3']['object']['key']
    
    # Define the input video path and output base folder
    input_video = f"s3://{s3_bucket}/{s3_key}"
    output_base_folder = f"s3://{s3_bucket}/converted/"
    
    # Define the MediaConvert endpoint and role ARN
    mediaconvert_client = boto3.client('mediaconvert', region_name='ap-northeast-2')
    mediaconvert_endpoint = mediaconvert_client.describe_endpoints()['Endpoints'][0]['Url']
    role_arn = "arn:aws:iam::account:role/MediaConvertRole"
    
    # Define the job settings for MediaConvert
    job_settings = {
        "OutputGroups": [
            {
                "Name": "File Group",
                "Outputs": [
                    {
                        "NameModifier": "/hd/_1080p",
                        "ContainerSettings": {
                            "Container": "MP4",
                            "Mp4Settings": {
                                "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
                            }
                        },
                        "VideoDescription": {
                            "ScalingBehavior": "DEFAULT",
                            "Height": 1080,
                            "CodecSettings": {
                                "Codec": "H_264",
                                "H264Settings": {
                                    "RateControlMode": "CBR",
                                    "Bitrate": 5000000
                                }
                            },
                            "Width": 1920
                        },
                        "Extension": "mp4"
                    },
                    {
                        "NameModifier": "/mobile/_480p",
                        "ContainerSettings": {
                            "Container": "MP4",
                            "Mp4Settings": {
                                "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
                            }
                        },
                        "VideoDescription": {
                            "ScalingBehavior": "DEFAULT",
                            "Height": 480,
                            "CodecSettings": {
                                "Codec": "H_264",
                                "H264Settings": {
                                    "RateControlMode": "CBR",
                                    "Bitrate": 1000000
                                }
                            },
                            "Width": 854
                        },
                        "Extension": "mp4"
                    },
                    {
                        "NameModifier": "/hd720/_720p",
                        "ContainerSettings": {
                            "Container": "MP4",
                            "Mp4Settings": {
                                "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
                            }
                        },
                        "VideoDescription": {
                            "ScalingBehavior": "DEFAULT",
                            "Height": 720,
                            "CodecSettings": {
                                "Codec": "H_264",
                                "H264Settings": {
                                    "RateControlMode": "CBR",
                                    "Bitrate": 1000000
                                }
                            },
                            "Width": 1280
                        },
                        "Extension": "mp4"
                    }
                ],
                "OutputGroupSettings": {
                    "Type": "FILE_GROUP_SETTINGS",
                    "FileGroupSettings": {
                        "Destination": output_base_folder
                    }
                }
            }
        ],
        "Inputs": [
            {
                "FileInput": input_video
            }
        ]
    }
    
    # Create and start the MediaConvert job
    mediaconvert_client = boto3.client('mediaconvert', endpoint_url=mediaconvert_endpoint)
    response = mediaconvert_client.create_job(
        Role=role_arn,
        Settings=job_settings
    )
    
    # Define the Slack bot token (replace with your actual bot token)
    slack_bot_token = 'slack_token'

    # Define the Slack channel to post the message (replace with your actual channel)
    slack_channel = '#channel_name'
    
    
    # Define the CloudFront URL and prefix
    cloudfront_url = 'https://cf_url'
    prefix = 'converted'
    
    # Extract the last part of the path (file name with extension)
    filename_with_extension = os.path.basename(s3_key)
    
    # Split the file name into the root part (without extension) and the extension part
    filename_without_extension, file_extension = os.path.splitext(filename_with_extension)
    
    # Define the Slack message payload
    slack_message = {
        'channel': slack_channel,
        'text': '동영상 호스팅 파일 생성 요청',
        'attachments': json.dumps([
            {
                'title': 'Completed video hosting file creation',
                'text': f'File creation completed.\n'
                        f'고화질: {cloudfront_url}/{prefix}/{filename_without_extension}/hd/_1080p.mp4\n'
                        f'중화질: {cloudfront_url}/{prefix}/{filename_without_extension}/hd720/_720p.mp4\n'
                        f'저화질: {cloudfront_url}/{prefix}/{filename_without_extension}/mobile/_480p.mp4',
                'color': '#36a64f'  # Green color for success
            }
        ])
    }
    
    # Convert the message payload to URL-encoded form data
    form_data = urllib.parse.urlencode(slack_message).encode('utf-8')

    # Define the HTTP headers for the Slack API request
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': f'Bearer {slack_bot_token}'
    }

    # Create an HTTP connection and send the POST request to the Slack API
    connection = http.client.HTTPSConnection('slack.com')
    connection.request('POST', '/api/chat.postMessage', body=form_data, headers=headers)

    # Get the response from Slack
    response = connection.getresponse()
    response_data = json.loads(response.read().decode('utf-8'))
    
    return {
        'statusCode': 200,
        'body': json.dumps('MediaConvert job started successfully!')
    }

코드 트리거에S3 Trigger를 붙이면 알아서 잘 돌아간다.

 

하지만 사실 이 코드는 약간 함정이 있다.

async를 파이썬에서 쓰는 방법을 몰라서 그냥 순차적으로 넘어가는 통에 이미지가 전부 S3에 들어가고 Cache가 되기도 전에 슬랙으로 노티가 이루어지기 때문에 간혹 슬랙이 오자마자 누르면 영상이 보이지 않을때가 있다. 하지만 저용량으로 대체적으로 인코딩 되기 때문에 적어도 한국의 인터넷 환경에선 대체적으로 문제 없이 작동하긴 한다. 하지만 동기식 코드를 적용하여 순차적으로 되는 것을 다 확인해야 하는 것은 변함이 없으므로 수정을 하긴 해야 할거 같다.

 

변경 후 장점

1. S3에 파일을 넣어주기만 하면 자동으로 파일 변환에 슬랙으로 알려주기 까지 한다.

2. 서버리스 아키텍쳐로 인해 서버의 부하를 고민할 요소들이 적다.

3. 필요시 스크립트만 수정하면 더 낮은, 더 높은 동영상 변환도 문제 없이 가능하다.

4. 기존에 사용하던 업체 대비 요금에 90% 이상 감면 효과가 있다.

변경 후 단점

1. 마케터 급 사용자에게도 S3 권한을 넘겨야 함.

2. 동영상용 S3 버킷을 별도로 구비하여 접근 권한을 제어해야 함.

3. S3의 기본 단점인 동 파일명의 변경시 Cloudfront에서 캐시를 제대로 반영되지 않는 문제가 있음.