本開発の経緯
本開発の背景はAWSを使って何かを作ってみたかったからになります。CLFを取得しSAAを勉強中ですが、現職では機会がなく、だったら自分でやってみようと思ったのと同期とも「やってみようぜ!」という話になり本開発を実施しました。システムとしては非常にシンプルでAPI GatewayとLambdaを使用してGoogleMapsAPI呼び出し、おすすめの場所などをユーザーへ返却するサービスを開発しました。
システム構成図
下記が構成図になります。非常にシンプルな構成となっています。
・想定動作
Users → Internet → API Gateway → Lambda → GoogleMapsAPI → Lambda→API → Gateway → Users
・AWS CLIでのコマンド実行を想定。
・リクエストはJSON形式を想定し、返却結果もJSON形式で受信。
・API GatewayにはAPIキーを設定し、セキュリティを担保。
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を作成 ]を押下する。
3.[ APIを作成 ]をクリックする。
下記に設定し、[ APIを作成 ]をクリックする。
・API:Google-Maps-APIGW(任意)
・説明-オプション:API Gatewayの説明などを記載
・APIエンドポイントタイプ:リージョン
4.[ メソッドを作成 ]をクリックする。
クリック後、メソッド作成画面に遷移するので下記設定を行い、[ メソッド作成 ]をクリックする。
・メソッドタイプ:GET
・統合タイプ:Lambda関数
・Lambdaプロキシ統合:ON
・Lambda関数:リージョン、統合したいLambdaのARN
5.[ APIをデプロイ ]をクリックする。
クリック後6の画面に遷移する。
6.ステージを選択し、ステージ名を記載後に[ デプロイ ]をクリックする。
下記設定をして
・ステージ:新しいステージを選択。(ステージ名は任意)
・デプロイメントの説明:デプロイメントの説明を記載する。(任意)
APIキー設定手順
1.APIキーページに遷移し、APIキーの作成をクリックする。
2.以下を入力・選択し、[ 保存 ]をクリックする。
下記を入力し、保存するとAPIキーが生成される。
・名前:Google-Maps-APIkey(任意)
・説明-オプション:APIキーの説明を記載。
・APIキー:自動生成
3.使用量プランのページに遷移し、以下を入力・選択し、[ 使用量プランを作成 ]をクリックする。
4.以下を入力・選択し、[ 使用量プランを作成 ]をクリックする。
・名前:Google-Maps-APIplan(任意)
・説明-オプション:使用量プランの説明を記載。
・レート:任意の数値
・バースト:任意の数値
・リクエスト:任意の数値と期間
5.APIキー画面に戻り、関連付けられた使用量プランを選択し[ 使用量プランに追加 ]をクリックする。
6.作成した使用量プランにキーを追加し、結びつけ[ 保存 ]をクリックする。
7.再度、使用量プラン詳細画面に遷移し、関連付けられたステージを選択し、[ ステージを追加 ]をクリックする。
8.関連付けたいAPI、ステージを設定し、[ 使用量プランに追加 ]をクリックする。
9.リソース画面に遷移し、メソッドリクエストタブの[ 編集 ]をクリックし、再デプロイ。
10.メソッドリクエストを編集画面に遷移し、[ APIキーが必須です ]にチェックし、[ 保存 ]をクリックする。
Lambda設定手順
1.マネジメントコンソール画面より、Lambdaを選択する。
2.Lambdaの関数ページに遷移し、右上の[ 関数を作成 ]をクリックする。
3.以下を入力・選択し、[ 関数の作成 ]をクリックする。
・一から作成
・関数名:Google-Maps-API-Lambda(任意)
・ランタイム:python3.13
・アーキテクチャ:x86_64
・実行ロール:基本的な Lambda アクセス権限で新しいロールを作成
4.[ コード ] をクリックし、赤枠内に作成したじコードをコピー&ペーストする。
5.コピー&ペーストされたことを確認し、左のペインにある[ Deploy ]をクリックする。
6.レイヤーにPython_pakcageを追加し、[ 追加 ]をクリックする。
API GatewayとLambdaの統合手順
2.ソースを選択からAPI Gatewayを選択し[ 追加 ]をクリックする。
実際に動かしてみた
コマンド
★コマンド例
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だとできたので、再度調査してまた具体的な原因、対処方法を記事にします。
最後に
今回は残念ながらエラーでしたが、原因が恐らくAPI Gateway側にあるということが分かったので、再度調査し記事にします。
ただ、Lambdaの設定、API Gatewayも設定方法をアウトプットする良い機会になりました。
やっぱり、アウトプットって大事ですね...
初めての記事だったので、読みづらい所もあるとは思いますが、最後まで見ていただきありがとうございます。
また近いうちに対処記事を上げますので、そちらもぜひ見ていただければと思います。