API Gatewayについて触ったことがなかったため、とある構成を模倣してテストを行った備忘

やりたいこと

"API①を叩く何か"がAPI①を叩いてメンバーIDの情報を取ろうとする ↓ API①がLambda①を呼び出す ↓ Lambda①がコードの中でAPI②を叩く ↓ API②がメンバーIDの情報を返す

前提条件

"API①を叩く何か"は直接API②を叩きたくない なので中継としてAPI①、Lambda①を作成してAPI②を叩き、情報を取ってくる API②はアクセス元を絞っているため、Lambda①からのアクセスIPは固定する必要がある →Lambda①をVPC内に配置し、固定IPを振ったNATから外に出るようにする Lambda②では、認証キーの認証が行われる リクエスト、レスポンスの変換は特になし IAM認証は省きます

手順

API①側のためのVPC①を作成 VPC①にパブリックサブネット①を作成(0.0.0.0/0 → IGW ルート) VPC①にプライベートサブネット①を作成(0.0.0.0/0 → NAT GW ルート) VPC①にIGWをアタッチ パブリックサブネット①にNAT GWを作成 image.png image.png

API②側のためのVPC②を作成 VPC②にプライベートサブネット②を作成 image.png

Lambda① を VPC①のプライベートサブネット① に作成(SG/NACL設定含む) image.png

以下参考Lambda

バリデーションチェック、適当な認証処理、メンバーリストがある(DBを作るのが面倒だったため、テスト用に)

import json
import base64

def lambda_handler(event, context):
    """
    POSTリクエストのJSONボディからMemberIDを受け取り、
    バリデーションとIDごとの情報取得を行うLambda関数
    """

    # 応答データとステータスコードを初期化
    response_data = {
        'success': False,
        'errorMessage': '内部サーバーエラー',
        'responseWrapper': {}
    }
    status_code = 500

    try:
        # --- 認証処理 ---
        headers = event.get('headers', {})
        authorization_header = headers.get('Authorization') 

        # Authorizationヘッダーが存在しない、または値が'12345'でない場合
        if not authorization_header or authorization_header != '12345':
            response_data['errorMessage'] = '認証エラー: Authorization ヘッダーが不正です。'
            status_code = 401
            return {
                'statusCode': status_code,
                'body': json.dumps(response_data, ensure_ascii=False)
           }

        # リクエストボディをデコード
        body_string = event['body']
        if event.get('isBase64Encoded', False):
            try:
                body_string = base64.b64decode(body_string).decode('utf-8')
            except Exception:
                response_data['errorMessage'] = 'リクエストボディが不正なBase64形式です。'
                status_code = 400
                return {
                    'statusCode': status_code,
                    'body': json.dumps(response_data, ensure_ascii=False)
                }

        body = json.loads(body_string)
        member_id = body.get('inputs', {}).get('MemberID')

        # ① MemberIDのバリデーションチェック
        if not member_id:
            response_data['errorMessage'] = 'MemberIDがリクエストに含まれていません。'
            status_code = 400
            return {
                'statusCode': status_code,
                'body': json.dumps(response_data, ensure_ascii=False)
            }

        if not (isinstance(member_id, str) and len(member_id) == 4 and member_id.isdigit()):
            response_data['errorMessage'] = 'MemberIDは4桁の数字文字列で指定してください。'
            status_code = 400
            return {
                'statusCode': status_code,
                'body': json.dumps(response_data, ensure_ascii=False)
            }

        valid_ids = ['0000', '0001', '0002', '0003']
        if member_id not in valid_ids:
            response_data['errorMessage'] = '無効なMemberIDです。利用可能な値は0000〜0003です。'
            status_code = 400
            return {
                'statusCode': status_code,
                'body': json.dumps(response_data, ensure_ascii=False)
            }

        # ② IDごとの情報定義
        members_info = {
            "0000": {"points": 133, "point_expiry_date": "20251212"},
            "0001": {"points": 1333, "point_expiry_date": "20251112"},
            "0002": {"points": 1, "point_expiry_date": "20250912"},
            "0003": {"points": 9999, "point_expiry_date": "20251012"}
        }

        # ③ IDに対応する情報を取得
        member_info = members_info.get(member_id)

        # ④ 成功時のレスポンスを組み立て
        response_data = {
            'success': True,
            'errorMessage': None,
            'responseWrapper': {
                'memberId': member_id,
                'points': member_info['points'],
                'point_expiry_date': member_info['point_expiry_date']
            }
        }
        status_code = 200

    except json.JSONDecodeError:
        response_data['errorMessage'] = 'リクエストボディが不正なJSON形式です。'
        status_code = 400
    except Exception as e:
        # その他の予期せぬエラー
        response_data['errorMessage'] = f'予期しないエラーが発生しました: {str(e)}'
        status_code = 500

    # 最終的なレスポンスを返す
    return {
        'statusCode': status_code,
        'body': json.dumps(response_data, ensure_ascii=False)
    }

API①をPOSTで作成し、Lambda①と統合してデプロイ 必要に応じてヘッダーの必須条件を設定する(認証キーが絶対必要とか。今回はなし) image.png

Lambda② を VPC②のプライベートサブネット② に作成

API②をPOSTで作成し、Lambda②と統合、リソースポリシーで NAT GWのEIPのみ を許可してデプロイ image.png

以下参考Lambda

API②のエンドポイントと認証キーはSecretManager管理なのでそこから取ってくる

import os
import json
import base64
import urllib.request
import urllib.error
import boto3

# 環境変数
URL_SECRET_NAME = os.environ["URL_SECRET_NAME"]
TOKEN_SECRET_NAME = os.environ["TOKEN_SECRET_NAME"] 


def get_secret(secret_name: str) -> str:
    #Secrets Manager から文字列を取得
    sm = boto3.client("secretsmanager")
    res = sm.get_secret_value(SecretId=secret_name)
    return res["SecretString"]


def lambda_handler(event, context):
    try:
        #Secrets取得
        url = get_secret(URL_SECRET_NAME)
        token = get_secret(TOKEN_SECRET_NAME)

        #リクエストボディ
        body = event.get("body")
        if event.get("isBase64Encoded"):
            body = base64.b64decode(body).decode("utf-8")

        # body が dict の場合は JSON に変換
        if isinstance(body, dict):
            body_str = json.dumps(body)
        else:
            body_str = body

        #ヘッダー 
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        }

        #API呼び出し
        req = urllib.request.Request(
            url=url,
            data=body_str.encode("utf-8") if body_str else None,
            headers=headers,
            method="POST"
        )

        with urllib.request.urlopen(req, timeout=8) as resp:
            return {
                "statusCode": resp.getcode(),
                "headers": {"Content-Type": "application/json"},
                "body": resp.read().decode("utf-8")
            }

    except urllib.error.HTTPError as e:
        # APIが 4xx / 5xx を返したとき
        return {
            "statusCode": e.code,
            "body": e.read().decode("utf-8", errors="replace")
        }
    except Exception as e:
        # Secrets取得失敗、JSON処理失敗、ネットワークエラーなど
        return {
            "statusCode": 500,
            "body": json.dumps({"error": str(e)}, ensure_ascii=False)
        }


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "【API②】"
        }
    ]
}

実行

PostmanからAPIを叩いてみる ヘッダーに認証コードを入れる image.png image.png

返ってきた、OK