AWS初心者のLambda開発

## 本開発の経緯
本開発の背景はAWSを使って何かを作ってみたかったからになります。CLFを取得しSAAを勉強中ですが、現職では機会がなく、だったら自分でやってみようと思ったのと同期とも「やってみようぜ！」という話になり本開発を実施しました。システムとしては非常にシンプルでAPI GatewayとLambdaを使用してGoogleMapsAPI呼び出し、おすすめの場所などをユーザーへ返却するサービスを開発しました。

## システム構成図
下記が構成図になります。非常にシンプルな構成となっています。
・想定動作
```
Users → Internet → API Gateway → Lambda → GoogleMapsAPI → Lambda→API → Gateway → Users
```
・AWS CLIでのコマンド実行を想定。
・リクエストはJSON形式を想定し、返却結果もJSON形式で受信。
・API GatewayにはAPIキーを設定し、セキュリティを担保。
![名称未設定ファイル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/624b3b24-1c21-479a-9db6-1acb29c81bf9.png)

## Lambdaへの実装コード
同期と分担して作成しました。新人研修以来のPythonで難しかったです…
私はクライアントからのパラメータを受信するapi_param_recvを担当しました。
★作成したコード★
```
import json
import requests

def api_param_recv(event: dict, context: object) -> dict:
    event_param_recv = {key: str(value) for key, value in event.items()}

    for value in event_param_recv.values():
        if not isinstance(value, str):
            return send_response(1,{})
        
    # Step 2: Validate parameters
    if not param_validation(event_param_recv):
        return send_response("Invalid parameters", None)

    # Step 3: Call external API
    api_result, error_message, api_response = call_find_place_api(event_param_recv)

    if(error_message is not None):
        send_data = error_message
    else:
        send_data = api_response 

    # Step 4: Send response
    return send_response(api_result, send_data)


def param_validation(event_str_params: dict) -> bool:

    # Initialize return values
    result = 1  # Default to failure
    error_message = []  # List to store error reasons
    validated_params = {}  # Placeholder for validated parameters

    # Validation rules
    required_fields = {
        "queryText": str,
    }

    optional_fields = {
        "placeType": str,
        "latitude": float,
        "longitude": float,
        "radius": float,
        "minRate": float,
        "strictType": bool,
        "priceLevel": list,
        "searchFields": list,
    }

    coordinate_fields = ["latitude", "longitude", "radius"]

    # Check required fields
    for field, field_type in required_fields.items():
        if field not in event_str_params or not isinstance(event_str_params[field], field_type):
            error_message.append(f"{field}: Missing or invalid type")

    # Check optional fields
    for field, field_type in optional_fields.items():
        if field in event_str_params:
            if field == "priceLevel" or field == "searchFields":
                if not isinstance(event_str_params[field], list):
                    error_message.append(f"{field}: Must be a list")
                else:
                    if field == "priceLevel" and not all(isinstance(i, int) and i in [0, 1, 2, 3, 4] for i in event_str_params[field]):
                        error_message.append(f"{field}: Must contain values 0, 1, 2, 3, or 4")
            elif field == "minRate":
                if not (0 <= event_str_params[field] <= 5):
                    error_message.append(f"{field}: Must be between 0 and 5")
            elif not isinstance(event_str_params[field], field_type):
                error_message.append(f"{field}: Invalid type")

    # Check coordinate dependencies
    coordinates_present = [field for field in coordinate_fields if field in event_str_params]
    if 0 < len(coordinates_present) < 3:
        error_message.append("latitude, longitude, and radius must be specified together")

    # If no validation errors, set validated_params
    if not error_message:  # If no errors occurred
        result = 0  # Validation successful
        validated_params = event_str_params  # Assume all parameters are valid for now
    else:
        validated_params = None  # Set validated_params to None on failure

    return result, (error_message if error_message else None), validated_params

def call_find_place_api(api_params: dict[str]):

    try:
        # Find Place APIを呼び出す
        response = requests.get("URL", params=api_params)
        
        # レスポンスのステータスコードを確認
        if response.status_code == 200:
            data = response.json()
            result = 0
            error_message = None
            find_place_info = data  # 必要に応じて data の中身を加工してください
        else:
            result = 1
            error_message = f"HTTPエラー: {response.status_code}"
            find_place_info = None
    except Exception as e:
        result = 1
        error_message = str(e)
        find_place_info = None

    return result, error_message, find_place_info

def send_response(result: int, find_place_info: dict):

    # Determine the status code
    if result == 0:
        status = 200
    else:
        status = 400

    # Convert result and find_place_info to a string
    result_message = json.dumps({"result": result, "info": find_place_info})

    # Create the response as a JSON object
    response = {"status": status, "result": result_message}

    # Simulate sending the response (replace this with actual response logic in a real application)
    print("Response sent to client:", response)

    return response
```

## 設定手順
### API Gateway設定手順
1.[ APIを作成 ]をクリックする。
　右上の[ APIを作成 ]を押下する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/a089716b-f632-424f-a6d5-ba8f0c540013.png)

2.APIタイプを選択する。
　今回はREST APIを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/fa83f642-971f-415f-996d-e039247d0b9d.png)

3.[ APIを作成 ]をクリックする。
　下記に設定し、[ APIを作成 ]をクリックする。
　・API：Google-Maps-APIGW（任意）
　・説明-オプション：API Gatewayの説明などを記載
　・APIエンドポイントタイプ：リージョン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/6ede7e83-0f69-4fb8-bef7-6ff5c21b9767.png)

4.[ メソッドを作成 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/9f6575d3-ff4f-437b-9115-c321b6ca4067.png)
　クリック後、メソッド作成画面に遷移するので下記設定を行い、[ メソッド作成 ]をクリックする。
　・メソッドタイプ：GET
　・統合タイプ：Lambda関数
　・Lambdaプロキシ統合：ON
　・Lambda関数：リージョン、統合したいLambdaのARN
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/5a459639-fced-4c4c-9cae-e63859421480.png)

5.[ APIをデプロイ ]をクリックする。
　クリック後6の画面に遷移する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d158f5e5-7b4c-4e19-8048-68e9ee2a036f.png)

6.ステージを選択し、ステージ名を記載後に[ デプロイ ]をクリックする。
　下記設定をして
　・ステージ：新しいステージを選択。（ステージ名は任意）
　・デプロイメントの説明：デプロイメントの説明を記載する。（任意）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/9b359c47-4b2c-45e0-94df-12db185420ad.png)

### APIキー設定手順
1.APIキーページに遷移し、APIキーの作成をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/85df99d7-4be1-4740-9914-093b93fde5c3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/4439222a-60ce-49de-8d05-c4cfebc58a5e.png)

2.以下を入力・選択し、[ 保存 ]をクリックする。
　下記を入力し、保存するとAPIキーが生成される。
　・名前：Google-Maps-APIkey（任意）
　・説明-オプション：APIキーの説明を記載。
　・APIキー：自動生成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/6e5d987b-063c-49d7-82b2-834277e3ef98.png)

3.使用量プランのページに遷移し、以下を入力・選択し、[ 使用量プランを作成 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/317bbf14-8a77-45d8-8861-8b8c8541c347.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/ad8a8444-0248-4a85-b5ba-d97f1b32e7fb.png)

4.以下を入力・選択し、[ 使用量プランを作成 ]をクリックする。
　・名前：Google-Maps-APIplan（任意）
　・説明-オプション：使用量プランの説明を記載。
　・レート：任意の数値
　・バースト：任意の数値
　・リクエスト：任意の数値と期間
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/31394ef8-af41-4acf-98f8-96a095c95b41.png)

5.APIキー画面に戻り、関連付けられた使用量プランを選択し[ 使用量プランに追加 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/85df99d7-4be1-4740-9914-093b93fde5c3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/10048895-9bc1-4365-9127-66b0c63068c0.png)

6.作成した使用量プランにキーを追加し、結びつけ[ 保存 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/70fac82d-e07f-4146-baa2-68701d9226f8.png)

7.再度、使用量プラン詳細画面に遷移し、関連付けられたステージを選択し、[ ステージを追加 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/37a2074d-4a7e-490b-9c5d-f410d68f1221.png)

8.関連付けたいAPI、ステージを設定し、[ 使用量プランに追加 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d7dfd3a5-1996-49ae-8ef2-006af399a662.png)

9.リソース画面に遷移し、メソッドリクエストタブの[ 編集 ]をクリックし、再デプロイ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/da69c6c4-3506-4fb0-b1d7-2263c4346810.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d2704977-288c-4baf-91c6-ce2bbea71fad.png)

10.メソッドリクエストを編集画面に遷移し、[ APIキーが必須です ]にチェックし、[ 保存 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/dbf734cc-489f-42bc-8a8c-bdfc33ec3dc6.png)

### Lambda設定手順
1.マネジメントコンソール画面より、Lambdaを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/125f0bce-3beb-4046-815f-975ddc8a986a.png)

2.Lambdaの関数ページに遷移し、右上の[ 関数を作成 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/3236acd5-ac0f-49e3-82d6-819decc6f8b3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/e2c92c46-2f07-4c99-9fe8-d7b23b25bdb1.png)

3.以下を入力・選択し、[ 関数の作成 ]をクリックする。
　・一から作成
　・関数名：Google-Maps-API-Lambda（任意）
　・ランタイム：python3.13
　・アーキテクチャ：x86_64
　・実行ロール：基本的な Lambda アクセス権限で新しいロールを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/312be3f4-9df6-4282-8839-70fd0138aac5.png)

4.[ コード ] をクリックし、赤枠内に作成したじコードをコピー＆ペーストする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d74bd31c-a82f-44d9-bc61-c65ba456a5bd.png)

5.コピー&ペーストされたことを確認し、左のペインにある[ Deploy ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/95f27d65-0762-4aa8-b3d4-07828b3ac8d3.png)

6.レイヤーにPython_pakcageを追加し、[ 追加 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/ad080c75-5934-4ea3-9567-a32dfaacf559.png)

### API GatewayとLambdaの統合手順
1.トリガーを追加をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/b4baa751-a75c-41e7-ac85-25878b45efa1.png)

2.ソースを選択からAPI Gatewayを選択し[ 追加 ]をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d75f6727-e5b0-4362-91da-51ae574acfe4.png)

## 実際に動かしてみた
### コマンド
★コマンド例
```
curl -X GET "API GatewayのURL" --header "x-api-key:xxxxxxxxxxxxxxxxxxx" "Content-Type: application/json" --data-raw "{ \"xxxxxx\": \"xxxxxx\" }" -o response.json.txt
```
### 結果

{"message": "Internal server error"}
ログを確認してみたころ、200と帰ってきているので問題なさそうだが...

```
Lambdaのログ
timestamp,message
1740330499935,"START RequestId: e968ccbc-3ba7-4628-8358-499998e8d11e Version: $LATEST
"
1740330500194,"Response sent to client: {'status': 200, 'result': '{""result"": 0, ""info"": {""candidates"": [], ""error_message"": ""You must use an API key to authenticate each request to Google Maps Platform APIs. For additional information, please refer to http://g.co/dev/maps-no-account"", ""status"": ""REQUEST_DENIED""}}'}
"
1740330500196,"END RequestId: e968ccbc-3ba7-4628-8358-499998e8d11e
"
1740330500196,"REPORT RequestId: e968ccbc-3ba7-4628-8358-499998e8d11e	Duration: 260.67 ms	Billed Duration: 261 ms	Memory Size: 128 MB	Max Memory Used: 58 MB	
"
```
Postmanで確認してみたら502エラーだったので、API Gateway側の問題らしいが、対処方法がわからず...
問題原因がAPI Gatewayだとできたので、再度調査してまた具体的な原因、対処方法を記事にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017559/d4cea1c4-6519-45a6-a6c8-c5b916bef1ff.png)

## 最後に
今回は残念ながらエラーでしたが、原因が恐らくAPI Gateway側にあるということが分かったので、再度調査し記事にします。
ただ、Lambdaの設定、API Gatewayも設定方法をアウトプットする良い機会になりました。
やっぱり、アウトプットって大事ですね...
初めての記事だったので、読みづらい所もあるとは思いますが、最後まで見ていただきありがとうございます。
また近いうちに対処記事を上げますので、そちらもぜひ見ていただければと思います。