アクセスが多い場合は、API GatewayよりもALB(Application Load Balancer)経由の方がコストは抑えられます。API Gatewayがリクエスト数に対して、ALBは時間+トラフィックの為。
また、キャパ(捌ける数)もALBの方が多いので、API Gatewayスタートでスケールし始めてからALBに切り替えるのが良さそう。
API Gateway+LambdaでDynamoDB(MediaConvertの情報)を返す で作成したLambdaをターゲットにALB経由でアクセスできるようにしてみました。Lambdaは少し改修が必要です。

Saving Money By Replacing API Gateway With Application Load Balancer’s Lambda Integration · Serverless Training by Jeremy Thomerson
Amazon API Gateway のクォータと重要な注意点 – Amazon API Gateway
Application Load Balancer のクォータ – Elastic Load Balancing

セキュリティグループ作成

https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#CreateSecurityGroup

セキュリティグループを作成
	基本的な詳細
		セキュリティグループ名: AllowHttpGroup
		説明: Allow HTTP/HTTPS security group
HTTP/HTTPS
	インバウンドルール
		[ルールを追加]
			タイプ: HTTP
			ソース: AnywhereIPv4 = 0.0.0.0/0
		[ルールを追加]
			タイプ: HTTP
			ソース: AnywhereIPv4
		[ルールを追加]
			タイプ: HTTPS
			ソース: AnywhereIPv6 = ::/0
		[ルールを追加]
			タイプ: HTTPS
			ソース: AnywhereIPv6
	アウトバウンドルール
		[ルールを追加]
			タイプ: すべてのトラフィック
			ソース: AnywhereIPv6
	[セキュリティグループを作成]

ターゲットグループ作成

https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#CreateTargetGroup:

※mediaConvertInputFileToJobIdApiとmediaConvertJobIdToStatusApiの2つ作成

Specify group details
	Basic configuration
		Choose a target type
			●Lambda function
		Target group name
			mediaConvertInputFileToJobIdApi / mediaConvertJobIdToStatusApi
		[Next]
	Lambda function
		Choose a Lambda function from the list or
			mediaConvertInputFileToJobIdApi / mediaConvertJobIdToStatusApi
	[Create target group]

ロードバランサー作成

https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#SelectCreateELBWizard:

Select load balancer type
	Application Load Balancer
		[Create]

Create Application Load Balancer
	Basic configuration
		Load balancer name: api-lb
	Network mapping
		Mappings
			■ap-northeast-1a
			■ap-northeast-1c
			■ap-northeast-1d
	Security groups
		default [×(削除)]
		AllowHttpGroup	※上記で作成したセキュリティグループ
	Listeners and routing
		Protocol: HTTPS
		Default action: mediaConvertInputFileToJobIdApi
	Listeners and routing
		Default SSL/TLS certificate
			*.nightonly.com	※作成済みの証明書を選択
	[Create load balancer]

Create Application Load Balancer
	[View load balancer]

[アクション][属性の編集]
ロードバランサーの属性の編集
	削除保護: ■有効化
	アクセスログ: ■有効化
		S3の場所: logs-api-lb.nightonly.com	※新しいバケット名
		■この場所の作成
	[保存]

DNS設定

Route53または使用しているDNSサーバーに設定。

CNAME	api-lb.nightonly.com	api-lb-1445519200.ap-northeast-1.elb.amazonaws.com

動作確認

% nslookup api-lb.nightonly.com
api-lb.nightonly.com	canonical name = api-lb-1445519200.ap-northeast-1.elb.amazonaws.com.
Name:	api-lb-1445519200.ap-northeast-1.elb.amazonaws.com
Address: 3.112.139.157
Name:	api-lb-1445519200.ap-northeast-1.elb.amazonaws.com
Address: 54.95.69.24
Name:	api-lb-1445519200.ap-northeast-1.elb.amazonaws.com
Address: 52.192.148.61

ルール編集

リスナー
	[ルールの表示/編集]

api-lb | HTTPS:443
	[+(追加)]
		[ルールの挿入]
			ルールID: 1
				パス: /media/job_id
				転送先: mediaConvertInputFileToJobIdApi
		[保存]
		[ルールの挿入]
			ルールID: 2
				パス: /media/status
				転送先: mediaConvertJobIdToStatusApi
		[保存]
	[鉛筆(変更)]
		ルールID: 最後
			転送先: mediaConvertInputFileToJobIdApi -> [削除]
			固定レスポンスを返す
				レスポンスコード: 404
				Content-Type: application/json
				レスポンス本文: {"message":"Not Found"}
		[更新]
	[<(戻る)]

動作確認

・https://api-lb.nightonly.com/
{"message":"Not Found"}

・https://api-lb.nightonly.com/media/job_id
・https://api-lb.nightonly.com/media/status
502 Bad Gateway

・https://api-lb.nightonly.com/media/job_id?input_file=Big%20Buck%20Bunny.mp4
・https://api-lb.nightonly.com/media/status?job_id=c123456789012-smb6o7
502 Bad Gateway

エラーになってしまいましたが、CloudWatch LogsにLambdaのログが出ていたので、疎通はOK。

リスナー追加

http:// の場合に https:// にリダイレクトするようにしておきます。

リスナー
	[リスナーの追加]

Listener details
	Default actions
		Redirect
			Port: 443
	[Add]
	[View listeners]

動作確認

・http://api-lb.nightonly.com/
→ https://api-lb.nightonly.com/
{"message":"Not Found"}

・http://api-lb-1445519200.ap-northeast-1.elb.amazonaws.com
→ https://api-lb-1445519200.ap-northeast-1.elb.amazonaws.com
証明書エラー → 普通には使えないのでOK

・http://api-lb.nightonly.com/media/job_id
→ https://api-lb.nightonly.com/media/job_id
502 Bad Gateway

SSLリダイレクトしているのでOK

Lambdaコード(API GatewayとALBの違い)

ALB経由かの判断は、requestContext → elbが存在するかで出来る。

1. メソッドの場所が違う

API Gateway: requestContext → http → method
ALB: httpMethod

mediaConvertInputFileToJobIdApi, mediaConvertJobIdToStatusApi

+    if 'elb' in event['requestContext']:
+        method = event['httpMethod']
+    else:
        method = event['requestContext']['http']['method']

2. パラメータがデコードされない

/media/job_id?input_file=Big%20Buck%20Bunny.mp4 の例
API Gateway: Big Buck Bunny.mp4
ALB: Big+Buck+Bunny.mp4

mediaConvertInputFileToJobIdApi

    input_file = event['queryStringParameters']['input_file']
+    if 'elb' in event['requestContext']:
+        input_file = urllib.parse.unquote_plus(input_file)

mediaConvertJobIdToStatusApi ※job_idは半角英数・ハイフンのみっぽいけど念の為

    job_id = event['queryStringParameters']['job_id']
+    if 'elb' in event['requestContext']:
+        job_id = urllib.parse.unquote_plus(job_id)

3. headersに数値型を渡すと「502 Bad Gateway」になる

ログにエラー内容が出ないので気付きにくかったですが、数値で渡すとエラーになってしまいます。

mediaConvertInputFileToJobIdApi, mediaConvertJobIdToStatusApi

headers = { # Tips: API GatewayのCORS設定が効かない為
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': '*', # Tips: ChromeでCORSエラーになる為
-    'Access-Control-Max-Age': 7200,
+    'Access-Control-Max-Age': '7200', # Tips: LB経由で数値型を渡すと「502 Bad Gateway」になる
    'Content-Type': 'application/json; charset=UTF-8'
}

Lambdaコード(最終版)

最後にそれぞれのコードを記載しておきます。
似た所が多いので共通化したい所ですが。。。難しいかな?

mediaConvertInputFileToJobIdApi

最新のコードこちら → https://dev.azure.com/nightonly/_git/lambda-origin?path=/mediaConvertInputFileToJobIdApi/lambda_function.py

import boto3
import os
import re
import urllib.parse
import json

dynamodb = boto3.resource('dynamodb')
table_to_job_id = dynamodb.Table('MediaConvertInputFileToJobId')

ALLOWED_ORIGINS = os.environ['ALLOWED_ORIGINS']

headers = { # Tips: API GatewayのCORS設定が効かない為
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': '*', # Tips: ChromeでCORSエラーになる為
    'Access-Control-Max-Age': '7200', # Tips: LB経由で数値型を渡すと「502 Bad Gateway」になる
    'Content-Type': 'application/json; charset=UTF-8'
}

def lambda_handler(event, context):
    print(event)

    # メソッドチェック
    if 'elb' in event['requestContext']:
        method = event['httpMethod']
    else:
        method = event['requestContext']['http']['method']
    if method == 'OPTIONS':
        return option_response()
    elif method != 'GET':
        return error_response(404, 'No route matches.')

    # オリジンチェク # Tips: 存在する場合のみ
    if 'origin' in event['headers']:
        origin = event['headers']['origin']
        if origin != '' and origin != None:
            allowd_origins = ALLOWED_ORIGINS.split()
            print({ 'origin': origin, 'allowd_origins': allowd_origins })

            allowd = False
            for allowd_origin in allowd_origins:
                if re.fullmatch(allowd_origin, origin):
                    allowd = True
                    break
            if not allowd:
                return error_response(422, 'ORIGIN is invalid.')

    # パラメータチェック
    if 'queryStringParameters' not in event:
        return error_response(400, 'Not found parameter.')
    elif 'input_file' not in event['queryStringParameters']:
        return error_response(400, 'Not found parameter input_file.')

    input_file = event['queryStringParameters']['input_file']
    if 'elb' in event['requestContext']:
        input_file = urllib.parse.unquote_plus(input_file)
    print({ 'input_file': input_file })

    if input_file == '' or input_file == None:
        return error_response(422, 'Not exist parameter input_file.')

    # DynamoDBから出力情報取得
    response = table_to_job_id.get_item(
         Key = {
            'InputFile': input_file
        }
    )
    print(response)

    if 'Item' not in response:
        return error_response(422, 'Not found item.')

    return success_response(response['Item']['JobId'])

def option_response():
    return { 'statusCode': 200, 'headers': headers }

def error_response(code, message):
    return { 'statusCode': code, 'headers': headers, 'body': json.dumps({ 'success': False, 'message': message }) }

def success_response(job_id):
    return { 'statusCode': 200, 'headers': headers, 'body': json.dumps({ 'success': True, 'job_id': job_id }) }

mediaConvertJobIdToStatusApi

最新のコードこちら → https://dev.azure.com/nightonly/_git/lambda-origin?path=/mediaConvertJobIdToStatusApi/lambda_function.py

import boto3
import os
import re
import urllib.parse
import json

dynamodb = boto3.resource('dynamodb')
table_to_status = dynamodb.Table('MediaConvertJobIdToStatus')

ALLOWED_ORIGINS = os.environ['ALLOWED_ORIGINS']

headers = { # Tips: API GatewayのCORS設定が効かない為
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': '*', # Tips: ChromeでCORSエラーになる為
    'Access-Control-Max-Age': '7200', # Tips: LB経由で数値型を渡すと「502 Bad Gateway」になる
    'Content-Type': 'application/json; charset=UTF-8'
}

def lambda_handler(event, context):
    print(event)

    # メソッドチェック
    if 'elb' in event['requestContext']:
        method = event['httpMethod']
    else:
        method = event['requestContext']['http']['method']
    if method == 'OPTIONS':
        return option_response()
    elif method != 'GET':
        return error_response(404, 'No route matches.')

    # オリジンチェク # Tips: 存在する場合のみ
    if 'origin' in event['headers']:
        origin = event['headers']['origin']
        if origin != '' and origin != None:
            allowd_origins = ALLOWED_ORIGINS.split()
            print({ 'origin': origin, 'allowd_origins': allowd_origins })

            allowd = False
            for allowd_origin in allowd_origins:
                if re.fullmatch(allowd_origin, origin):
                    allowd = True
                    break
            if not allowd:
                return error_response(422, 'ORIGIN is invalid.')

    # パラメータチェック
    if 'queryStringParameters' not in event:
        return error_response(400, 'Not found parameter.')
    elif 'job_id' not in event['queryStringParameters']:
        return error_response(400, 'Not found parameter job_id.')

    job_id = event['queryStringParameters']['job_id']
    if 'elb' in event['requestContext']:
        job_id = urllib.parse.unquote_plus(job_id)
    print({ 'job_id': job_id })

    if job_id == '' or job_id == None:
        return error_response(422, 'Not exist parameter job_id.')

    # DynamoDBからステータス取得
    response = table_to_status.get_item(
         Key = {
            'JobId': job_id
        }
    )
    print(response)

    if 'Item' not in response:
        return error_response(422, 'Not found item.')

    return success_response(response['Item'])

def option_response():
    return { 'statusCode': 200, 'headers': headers }

def error_response(code, message):
    return { 'statusCode': code, 'headers': headers, 'body': json.dumps({ 'success': False, 'message': message }) }

def success_response(item):
    job_status = item['JobStatus']
    result = { 'success': True, 'job_status': job_status, 'progress_rate': int(item['ProgressRate']) }
    info = {}
    if job_status == 'COMPLETE':
        info = { 'duration_ms': int(item['DurationMs']), 'width_px': int(item['WidthPx']), 'height_px': int(item['HeightPx']),
                 'output_path': item['OutputPath'], 'playlists': item['Playlists'] }
    elif job_status == 'ERROR':
        info = { 'error_code': int(item['ErrorCode']), 'error_message': item['ErrorMessage'] }

    return { 'statusCode': 200, 'headers': headers, 'body': json.dumps({**result, **info}) }

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です