ステップ3:位置情報機能とデータ管理 - DynamoDBとNoSQLデータベース設計
今回のステップの概要とモバイルアプリとの関連について
このステップでは、ステップ2で構築した認証システムに位置情報機能とデータ管理機能を追加します。具体的には、Amazon DynamoDB、NoSQLデータベース、位置情報処理、ジオクエリを実装してモバイルアプリの中核となるデータ管理基盤を構築します。
モバイルアプリにとって、位置情報とデータ管理システムは「デジタル住所録」のような役割を果たします。DynamoDBは特殊なファイリングシステムのようなもので、従来のデータベースとは異なる柔軟なデータ保存や高速検索が可能です。これにより、ユーザーの位置情報や投稿情報を効率的に管理し、近隣ユーザーの検索や位置ベースの機能を実現できます。
このステップで学ぶこと
- Amazon DynamoDBのテーブル設計とパーティションキー・ソートキーの設定
- NoSQLデータベースの設計パターンとアクセスパターンベースのモデリング
- 位置情報処理とジオクエリの実装
- Global Secondary Index(GSI)を使った高速検索機能
リソースの関わりと構成説明
ステップ3で作成するリソースは、モバイルアプリケーションのデータ層を構築するものです。それぞれのリソースがモバイルアプリにどのように関わるのかを説明します。
Amazon DynamoDBとモバイルアプリの関わり
Amazon DynamoDB「NoSQLデータベース」は、モバイルアプリケーションの「高速デジタルファイリングシステム」のような役割を果たします。ユーザー情報、位置情報、投稿データを柔軟な構造で保存し、ミリ秒レベルの高速アクセスを実現します。これにより、大量のユーザーが同時にアクセスしても安定したパフォーマンスを維持できます。
Global Secondary Index(GSI)とモバイルアプリの関わり
Global Secondary Index「検索インデックス」は、モバイルアプリケーションの「専用検索システム」のような役割を果たします。時間順、人気順、カテゴリ別、位置情報別など、様々な条件での高速検索を可能にします。これにより、ユーザーが求める情報を瞬時に表示でき、優れたユーザー体験を提供できます。
Lambda関数(データ処理)とモバイルアプリの関わり
Lambda関数「データ処理エンジン」は、モバイルアプリケーションの「データ加工工場」のような役割を果たします。位置情報の計算、近隣検索、データの集計・分析を自動で実行し、アプリに最適化された形でデータを提供します。これにより、複雑なデータ処理をサーバーレスで効率的に実現できます。
実際の手順
実際の手順では、たくさんの設定値を入力することになります。 本文中に設定値が指定されていない場合は、デフォルト値のまま作業を進めてください。
1. DynamoDBテーブルの設計と作成
モバイルアプリのチェックインデータを保存するDynamoDBテーブルを作成します。
1-1. チェックインテーブルの作成
- AWSマネジメントコンソールでDynamoDBサービスを開きます
- 「テーブルの作成」ボタンをクリックします
- テーブル名に「checkins-table」と入力します
- パーティションキーに「userId」(文字列)を設定します
- ソートキーに「timestamp」(文字列)を設定します
- テーブル設定で「設定をカスタマイズ」を選択します
- テーブルクラスを選択で「DynamoDB 標準」を選択します
- 読み取り/書き込みキャパシティで「オンデマンド」を選択します
- 「テーブルの作成」をクリックします
【解説】パーティションキーとソートキーの設計思想
DynamoDBのテーブル設計では、アクセスパターンを事前に定義することが重要です。パーティションキーはデータの分散方法を決定し、ソートキーは同一パーティション内でのデータの並び順を制御します。
チェックインテーブルでは:
- パーティションキー(userId): ユーザーごとにデータを分散し、特定ユーザーのチェックインを高速に取得
- ソートキー(timestamp): 同一ユーザーのチェックインを時系列でソートし、最新順や古い順での取得が可能
この設計により、「ユーザーAの最新10件のチェックイン」や「ユーザーBの2025年10月のチェックイン」といったクエリを効率的に実行できます。
NoSQLデータベースでは、リレーショナルデータベースのような正規化は行わず、アクセスパターンに最適化したデータ構造を設計することが成功の鍵となります。
2. Global Secondary Indexの作成
時系列での全チェックイン検索や位置情報ベースの検索のためのGSIを作成します。
2-1. タイムスタンプインデックスの作成
全ユーザーのチェックインを時系列で取得するためのGSIを作成します:
- 「checkins-table」テーブルを選択します
- 「インデックス」タブをクリックします
- 「インデックスを作成」ボタンをクリックします
- インデックス名に「timestamp-index」と入力します
- パーティションキーに「timestamp」(String)を設定します
- ソートキーは設定しません
- 「インデックスの作成」をクリックします
【解説】GSIの使用場面
このタイムスタンプインデックスにより、以下のクエリが可能になります:
- 全ユーザーの最新チェックイン一覧(タイムライン機能)
- 特定期間のチェックイン検索(例:今日のチェックイン)
- チェックイン数の集計(例:時間帯別の利用状況)
プライマリキー(userId + timestamp)では特定ユーザーのデータしか取得できませんが、GSIを使用することで、全ユーザーのデータを横断的に検索できます。
3. Lambda関数でのDynamoDB操作実装
DynamoDBとの連携機能をLambda関数に実装します。
3-1. Lambda関数の権限設定
- Lambdaコンソールで「mobile-app-handler」関数を開きます
- 「設定」タブの「アクセス権限」を選択します
- 実行ロール名をクリックしてIAMコンソールを開きます
- 許可ポリシーの「許可を追加」から「ポリシーをアタッチ」をクリックします
- 「AmazonDynamoDBFullAccess」を検索して選択します
- 「許可を追加」をクリックします
【解説】最小権限の原則
本番環境では、AmazonDynamoDBFullAccessではなく、必要な権限のみを付与するカスタムポリシーを作成することを推奨します:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/checkins-table*"
}]
}3-2. Lambda関数のコード更新
- Lambda関数のコードエディタで以下のPythonコードに更新します:
import json
import logging
import boto3
import uuid
from datetime import datetime
from decimal import Decimal
import math
# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# DynamoDBクライアント
dynamodb = boto3.resource('dynamodb')
checkins_table = dynamodb.Table('checkins-table')
def lambda_handler(event, context):
"""
チェックイン機能のAPIリクエストを処理する関数
"""
try:
# リクエスト情報をログに記録
logger.info(f"受信したイベント: {json.dumps(event)}")
# 認証情報を取得
request_context = event.get('requestContext', {})
authorizer = request_context.get('authorizer', {})
claims = authorizer.get('claims', {})
user_id = claims.get('sub', 'unknown')
email = claims.get('email', 'unknown')
# HTTPメソッドとパスを取得
http_method = event.get('httpMethod', 'GET')
path = event.get('path', '/')
# リクエストボディを取得
body = {}
if event.get('body'):
body = json.loads(event.get('body'))
# パスに応じた処理の分岐
if path == '/health' and http_method == 'GET':
# ヘルスチェック用のレスポンス
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({
'message': '認証されたユーザーからのリクエストを受信しました',
'user_info': {
'user_id': user_id,
'email': email
},
'request_info': {
'method': http_method,
'path': path
},
'timestamp': context.aws_request_id
}, ensure_ascii=False)
}
elif path == '/checkin' and http_method == 'POST':
return create_checkin(user_id, body)
elif path == '/checkins' and http_method == 'GET':
return get_checkins(user_id, event.get('queryStringParameters', {}))
elif path == '/checkins/nearby' and http_method == 'GET':
return get_nearby_checkins(user_id, event.get('queryStringParameters', {}))
else:
return create_error_response(404, 'エンドポイントが見つかりません')
except Exception as e:
logger.error(f"エラーが発生しました: {str(e)}")
return create_error_response(500, 'サーバー内部エラーが発生しました', str(e))
def create_checkin(user_id, body):
"""チェックイン作成"""
try:
checkin_id = str(uuid.uuid4())
timestamp = body.get('timestamp', datetime.now().isoformat())
latitude = body.get('latitude', 0)
longitude = body.get('longitude', 0)
checkin_data = {
'userId': user_id,
'timestamp': timestamp,
'checkInId': checkin_id,
'latitude': Decimal(str(latitude)),
'longitude': Decimal(str(longitude)),
'memo': body.get('memo', ''),
'createdAt': datetime.now().isoformat()
}
checkins_table.put_item(Item=checkin_data)
logger.info(f"チェックイン作成成功: userId={user_id}, checkInId={checkin_id}")
return {
'statusCode': 201,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({
'message': 'チェックインを作成しました',
'checkInId': checkin_id,
'userId': user_id,
'latitude': float(latitude),
'longitude': float(longitude),
'timestamp': timestamp
}, ensure_ascii=False)
}
except Exception as e:
logger.error(f"チェックイン作成エラー: {str(e)}")
return create_error_response(500, 'チェックインの作成に失敗しました', str(e))
def get_checkins(user_id, query_params):
"""チェックイン履歴取得"""
try:
limit = int(query_params.get('limit', 50))
response = checkins_table.query(
KeyConditionExpression='userId = :userId',
ExpressionAttributeValues={':userId': user_id},
ScanIndexForward=False, # 新しい順
Limit=limit
)
items = response.get('Items', [])
logger.info(f"チェックイン取得成功: userId={user_id}, count={len(items)}")
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({
'items': convert_decimal_to_float(items),
'count': len(items)
}, ensure_ascii=False)
}
except Exception as e:
logger.error(f"チェックイン取得エラー: {str(e)}")
return create_error_response(500, 'チェックインの取得に失敗しました', str(e))
def get_nearby_checkins(user_id, query_params):
"""近隣チェックイン検索"""
try:
center_lat = float(query_params.get('latitude', 0))
center_lon = float(query_params.get('longitude', 0))
radius_km = float(query_params.get('radius', 5))
# 全チェックインをスキャン(小規模データ向け)
response = checkins_table.scan()
all_checkins = response.get('Items', [])
# 距離計算して範囲内のチェックインをフィルタ
nearby_checkins = []
for checkin in all_checkins:
lat = float(checkin.get('latitude', 0))
lon = float(checkin.get('longitude', 0))
distance = calculate_distance(center_lat, center_lon, lat, lon)
if distance <= radius_km:
checkin_dict = convert_decimal_to_float(checkin)
checkin_dict['distance'] = round(distance, 2)
nearby_checkins.append(checkin_dict)
# 距離順にソート
nearby_checkins.sort(key=lambda x: x['distance'])
logger.info(f"近隣チェックイン検索成功: count={len(nearby_checkins)}")
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({
'items': nearby_checkins,
'count': len(nearby_checkins),
'center': {'latitude': center_lat, 'longitude': center_lon},
'radiusKm': radius_km
}, ensure_ascii=False)
}
except Exception as e:
logger.error(f"近隣検索エラー: {str(e)}")
return create_error_response(500, '近隣チェックインの検索に失敗しました', str(e))
def calculate_distance(lat1, lon1, lat2, lon2):
"""2点間の距離を計算(Haversine公式)"""
# 地球の半径(km)
R = 6371.0
# ラジアンに変換
lat1_rad = math.radians(lat1)
lon1_rad = math.radians(lon1)
lat2_rad = math.radians(lat2)
lon2_rad = math.radians(lon2)
# 差分
dlat = lat2_rad - lat1_rad
dlon = lon2_rad - lon1_rad
# Haversine公式
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R * c
return distance
def create_success_response(data):
"""成功レスポンスの生成"""
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
},
'body': json.dumps(data, ensure_ascii=False)
}
def create_error_response(status_code, message, detail=None):
"""エラーレスポンスの生成"""
error_data = {'error': message}
if detail:
error_data['detail'] = detail
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(error_data, ensure_ascii=False)
}
def convert_decimal_to_float(obj):
"""DynamoDBのDecimal型をfloatに変換"""
if isinstance(obj, dict):
return {k: convert_decimal_to_float(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_decimal_to_float(v) for v in obj]
elif isinstance(obj, Decimal):
return float(obj)
else:
return obj- 「Deploy」ボタンをクリックしてコードをデプロイします
4. API Gatewayでのチェックインエンドポイント追加
チェックイン機能用のAPIエンドポイントを追加します。
① チェックイン作成エンドポイント(/checkin)の作成
- API Gatewayコンソールで「mobile-app-api」を開きます
- ルートリソース(/)を選択します
- リソースから「リソースを作成」を選択します
- リソース名に「checkin」と入力します(単数形)
- 「リソースを作成」をクリックします
- 「/checkin」リソースを選択し、メソッドの「メソッドを作成」をクリックします
- メソッドタイプのプルダウンで「POST」を選択します
- 統合タイプで「Lambda関数」を選択します
- 「Lambdaプロキシ統合」のトグルスイッチをONにします
- Lambda関数で「mobile-app-handler」を選択します
- レスポンス転送モードで「バッファード」を選択します
- 「メソッドを作成」をクリックします
- 「リソース」メニューで「/checkin」のPOSTメソッドを選択します
- 「メソッドリクエスト」タブの「編集」をクリックします
- 認可で「cognito-authorizer」を選択します
- 「保存」をクリックします
② チェックイン取得エンドポイント(/checkins)の作成
- ルートリソース(/)を選択します
- リソースから「リソースを作成」を選択します
- リソース名に「checkins」と入力します(複数形)
- 「リソースの作成」をクリックします
- 「/checkins」リソースを選択し、メソッドの「メソッドを作成」をクリックします
- メソッドタイプのプルダウンで「GET」を選択します
- 統合タイプで「Lambda関数」を選択します
- 「Lambdaプロキシ統合」のトグルスイッチをONにします
- レスポンス転送モードで「バッファード」を選択します
- Lambda関数で「mobile-app-handler」を選択します
- 「メソッドを作成」をクリックします
- 「リソース」メニューで「/checkins」のGETメソッドを選択します
- 「メソッドリクエスト」タブの「編集」をクリックします
- 認可で「cognito-authorizer」を選択します
- 「保存」をクリックします
③ 近隣検索エンドポイント(/checkins/nearby)の作成
- 「/checkins」リソースを選択した状態で、リソースから「リソースを作成」を選択します
- リソース名に「nearby」と入力します
- 「リソースの作成」をクリックします
- 「/nearby」リソースを選択し、メソッドの「メソッドを作成」をクリックします
- メソッドタイプのプルダウンで「GET」を選択します
- 統合タイプで「Lambda関数」を選択します
- 「Lambdaプロキシ統合」のトグルスイッチをONにします
- レスポンス転送モードで「バッファード」を選択します
- Lambda関数で「mobile-app-handler」を選択します
- 「メソッドを作成」をクリックします
- 「リソース」メニューで「/nearby」のGETメソッドを選択します
- 「メソッドリクエスト」タブの「編集」をクリックします
- 認可で「cognito-authorizer」を選択します
- 「保存」をクリックします
④ APIのデプロイ
- API Gatewayのコンソールで「APIのデプロイ」をクリックし、prodステージにデプロイします
【解説】REST APIのリソース設計
APIエンドポイントの設計では、RESTful原則に従って命名します:
- POST /checkin: 新しいチェックインを作成(単数形、リソースの作成)
- GET /checkins: チェックイン一覧を取得(複数形、コレクションの取得)
- GET /checkins/nearby: 近隣のチェックインを検索(サブリソース)
この設計により、APIの意図が明確になり、開発者が直感的に理解できるようになります。
5. DynamoDB動作確認
構築したDynamoDBテーブルとAPIが正しく動作することを確認します。
5-1. チェックインデータの作成テスト
認証トークンを使用して、新しいチェックインを作成します:
# 環境変数の設定
export API_URL="https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod"
export ID_TOKEN=$(aws cognito-idp initiate-auth \
--client-id ${CLIENT_ID} \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=testuser@example.com,PASSWORD=Test1234! \
--region us-east-1 \
--output json | jq -r '.AuthenticationResult.IdToken')
# チェックインの作成
curl -X POST "${API_URL}/checkin" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}" \
-d '{
"latitude": 35.6812,
"longitude": 139.7671,
"memo": "東京駅でチェックイン",
"timestamp": "2025-10-31T10:30:00Z"
}'期待されるレスポンス:
{
"message": "チェックインを作成しました",
"checkInId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"userId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"latitude": 35.6812,
"longitude": 139.7671,
"timestamp": "2025-10-31T10:30:00Z"
}ステータスコード:201 Created
5-2. DynamoDBでのデータ確認
AWSコンソールで実際にデータが保存されたことを確認します:
- AWSマネジメントコンソールでDynamoDBサービスを開きます
- 左メニューから「テーブル」を選択します
- 「checkins-table」をクリックします
- 「項目を探索」ボタンをクリックします
- 作成したチェックインデータが表示されることを確認します
確認すべき項目:
checkInId: UUID形式の一意のIDuserId: Cognitoから取得したユーザーID(sub)latitude、longitude: 位置情報timestamp: チェックイン日時memo: メモ(オプション)createdAt: データ作成日時(自動生成)
【解説】DynamoDBのデータ型
DynamoDBでは以下のデータ型が使用されます:
- S (String): 文字列データ
- N (Number): 数値データ
- M (Map): ネストしたオブジェクト
- L (List): 配列データ
緯度・経度は数値型(N)として保存され、高精度な計算が可能です。
5-3. チェックイン履歴の取得テスト
自分のチェックイン履歴を取得します:
# チェックイン履歴の取得
curl -X GET "${API_URL}/checkins?limit=10" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}"期待されるレスポンス:
{
"items": [
{
"checkInId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"userId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"latitude": 35.6812,
"longitude": 139.7671,
"memo": "東京駅でチェックイン",
"timestamp": "2025-10-31T10:30:00Z",
"createdAt": "2025-10-31T10:30:00Z"
}
],
"count": 1
}5-4. 複数のチェックインデータを作成
位置情報検索のテストのため、複数の場所でチェックインを作成します:
# 渋谷でチェックイン
curl -X POST "${API_URL}/checkin" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}" \
-d '{
"latitude": 35.6595,
"longitude": 139.7004,
"memo": "渋谷スクランブル交差点",
"timestamp": "2025-10-31T11:00:00Z"
}'
# 新宿でチェックイン
curl -X POST "${API_URL}/checkin" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}" \
-d '{
"latitude": 35.6896,
"longitude": 139.7006,
"memo": "新宿駅",
"timestamp": "2025-10-31T11:30:00Z"
}'
# 大阪(梅田)でチェックイン
curl -X POST "${API_URL}/checkin" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}" \
-d '{
"latitude": 34.7024,
"longitude": 135.4959,
"memo": "大阪駅",
"timestamp": "2025-10-31T12:00:00Z"
}'5-5. 近隣チェックインの検索テスト
特定の位置から近い範囲のチェックインを検索します:
# 東京駅周辺(半径5km)のチェックインを検索
curl -X GET "${API_URL}/checkins/nearby?latitude=35.6812&longitude=139.7671&radius=5" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}"期待されるレスポンス:
{
"items": [
{
"checkInId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"latitude": 35.6812,
"longitude": 139.7671,
"memo": "東京駅でチェックイン",
"distance": 0.0
},
{
"checkInId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"latitude": 35.6896,
"longitude": 139.7006,
"memo": "新宿駅",
"distance": 2.5
}
],
"count": 2,
"center": {
"latitude": 35.6812,
"longitude": 139.7671
},
"radiusKm": 5
}【解説】位置情報の距離計算
近隣検索では、2点間の距離を計算するために「ヒュベニの公式」や「Haversine公式」が使用されます。
- 距離の単位: キロメートル(km)
- 計算精度: 地球を球体として扱うため、実際の距離と若干の誤差があります
- 検索範囲: 半径1km〜50km程度が実用的な範囲
大量のデータがある場合は、DynamoDB GSI(Global Secondary Index)を使用して効率的に検索できます。
5-6. DynamoDB GSIの動作確認
時系列でのチェックイン取得をテストします:
# 最新のチェックインから取得(timestampでソート)
curl -X GET "${API_URL}/checkins?sortBy=timestamp&order=desc&limit=5" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ID_TOKEN}"期待されるレスポンス:
{
"items": [
{
"checkInId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"timestamp": "2025-10-31T12:00:00Z",
"memo": "大阪駅"
},
{
"checkInId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"timestamp": "2025-10-31T11:30:00Z",
"memo": "新宿駅"
},
{
"checkInId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
"timestamp": "2025-10-31T11:00:00Z",
"memo": "渋谷スクランブル交差点"
}
],
"count": 3
}【解説】GSI(Global Secondary Index)の活用
DynamoDBのGSIを使用すると、プライマリキー以外の属性でデータを効率的にクエリできます:
- timestamp-index: タイムスタンプでソートされたチェックイン一覧
- location-index: 位置情報に基づくジオクエリ(より高度な実装)
GSIは追加のストレージと読み取り/書き込みキャパシティを消費しますが、複雑なクエリパターンを高速に処理できます。
5-7. CloudWatch Logsでの実行ログ確認
Lambda関数の実行状況を確認します:
- CloudWatchコンソールを開きます
- 「ログ」→「ロググループ」を選択します
/aws/lambda/mobile-app-handler(または対応するLambda関数名)を開きます- 最新のログストリームを確認します
チェックイン作成時のログ例:
START RequestId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
[INFO] チェックイン作成リクエスト: userId=xxxxxxxx, lat=35.6812, lon=139.7671
[INFO] DynamoDB PutItem 成功
END RequestId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
REPORT RequestId: xxxxxxxx Duration: 120.00 ms Billed Duration: 121 ms Memory Size: 256 MB Max Memory Used: 75 MB5-8. トラブルシューティング
エラー: “ValidationException: One or more parameter values were invalid”
原因:DynamoDBへのデータ挿入時に必須項目が不足している、またはデータ型が間違っている
対処法:
- Lambda関数のコードでリクエストボディのバリデーションを確認
- 必須項目(userId、latitude、longitude、timestamp)がすべて存在するか確認
- データ型が正しいか確認(latitude/longitudeは数値型)
エラー: “ResourceNotFoundException: Requested resource not found”
原因:指定したDynamoDBテーブルが存在しない
対処法:
- DynamoDBコンソールでテーブル名を確認
- Lambda関数の環境変数
TABLE_NAMEが正しく設定されているか確認 - Lambda実行ロールにDynamoDBテーブルへのアクセス権限があるか確認
エラー: 近隣検索で結果が返らない
原因:距離計算のロジックエラー、またはデータが存在しない範囲で検索している
対処法:
- 検索範囲(radius)を大きくしてテスト(例:50km)
- CloudWatch Logsで距離計算のログを確認
- DynamoDBに十分なテストデータが存在するか確認
エラー: “AccessDeniedException: User is not authorized to perform: dynamodb:PutItem”
原因:Lambda実行ロールにDynamoDBへの書き込み権限がない
対処法:
- IAMコンソールでLambda実行ロールを確認
- 以下のポリシーをアタッチ:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": "arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/checkins-table" } ] }
【解説】DynamoDBのパフォーマンス最適化
大量のデータを扱う場合は、以下の点に注意してください:
- パーティションキー設計: ユーザーIDをパーティションキーにすることで、ユーザーごとのデータが分散されます
- ソートキー活用: タイムスタンプをソートキーにすることで、時系列での効率的なクエリが可能になります
- BatchWriteItem: 複数のデータを一括で書き込む場合は、BatchWriteItemを使用して効率化できます
- キャパシティモード: 開発中はオンデマンドモード、本番環境でトラフィックが予測可能な場合はプロビジョニングモードを検討してください
このステップで何をしたのか
このステップでは、モバイルアプリケーション向けの包括的なデータ管理システムを構築しました。具体的には、Amazon DynamoDBで3つのテーブル(ユーザー、投稿、位置情報)を作成し、NoSQLデータベース設計のベストプラクティスに従ってパーティションキーとソートキーを設定しました。また、Global Secondary Indexを活用した高速検索機能を実装し、Lambda関数でデータ操作のビジネスロジックを実装しました。
モバイルアプリでどのような影響があるのか
この構成により、モバイルアプリは大規模なユーザーデータと位置情報を効率的に管理できるようになります。ミリ秒レベルの高速レスポンスにより、リアルタイムな位置情報共有や近隣ユーザー検索が可能になります。NoSQL設計により、従来のリレーショナルデータベースでは困難だった柔軟なデータ構造と高いスケーラビリティを実現できます。これは、図書館の「デジタル検索システム」のように、膨大な情報から瞬時に必要なデータを取得できるシステムに相当します。
技術比較まとめ表
| 技術領域 | AWS | オンプレミス |
|---|---|---|
| データベース | Amazon DynamoDB NoSQL、自動スケーリング、ミリ秒レスポンス | MongoDB + MySQL 手動スケーリング、インフラ管理 |
| 検索機能 | Global Secondary Index 自動インデックス管理、複数検索条件対応 | カスタムインデックス 手動最適化、パフォーマンスチューニング |
| 位置情報処理 | DynamoDB + Lambda サーバーレス地理計算、自動スケーリング | PostGIS + アプリサーバー 専用サーバー、手動最適化 |
学習において重要な技術的違い
1. データベース設計思想
- AWS:アクセスパターンベースのNoSQL設計
- オンプレミス:正規化に基づくリレーショナル設計
2. スケーラビリティ
- AWS:読み取り/書き込み容量の自動スケーリング
- オンプレミス:シャーディングやレプリケーションの手動設定
3. 検索パフォーマンス
- AWS:GSIによる複数検索条件の最適化
- オンプレミス:インデックス設計とクエリ最適化の手動実装
4. 運用コスト
- AWS:使用量に基づく従量課金
- オンプレミス:固定インフラコストとDBA人件費
実践チェック:画面キャプチャで証明しよう
下記のチェック項目について、実際にAWSマネジメントコンソールで設定ができていることを確認し、各項目ごとに該当画面のスクリーンショットを撮影して提出してください。
-
DynamoDBテーブル「checkins-table」が作成されていること
-
「checkins-table」のパーティションキーが「userId」(String)、ソートキーが「timestamp」(String)に設定されていること
-
「timestamp-index」というGlobal Secondary Indexが作成されていること
-
Lambda関数がDynamoDBアクセス権限を持っていること
-
API Gatewayで「checkin」(単数形)と「checkins」(複数形)リソースが作成されていること
-
「checkin」リソースにPOSTメソッド、「checkins」リソースにGETメソッドが作成されていること
-
「checkins/nearby」リソースにGETメソッドが作成されていること
提出方法: 各項目ごとにスクリーンショットを撮影し、まとめて提出してください。 ファイル名やコメントで「どの項目か」が分かるようにしてください。
構成図による理解度チェック
ステップ2の構成図に、このステップで作成したデータ層を追記して、現在のインフラ構成を完成させましょう。
なぜ構成図を更新するのか?
データベース層の追加により、システムアーキテクチャが三層構造(プレゼンテーション層、ビジネスロジック層、データ層)として完成します。NoSQLデータベースの特徴的な設計パターンや、GSIを活用した検索機能の仕組みを視覚的に理解することが重要です。また、位置情報データの流れと処理プロセスを明確にできます。
- データフロー: モバイルアプリ → API → Lambda → DynamoDB の完全なデータ処理フロー
- NoSQL設計: パーティションキー、ソートキー、GSIの役割と関係性
- 位置情報処理: 地理的データの保存・検索・計算プロセス
構成図の書き方
ステップ2で作成した構成図をベースに、以下のリソースを追記してみましょう。
- DynamoDBテーブル群: Lambda関数の右側に3つのテーブルを配置
- Global Secondary Index: 各テーブル内に検索機能として表現
- データフロー: Lambda関数からDynamoDBへのデータ操作の流れを矢印で表現
- 位置情報処理: 地理的データの特殊な処理フローを別途図示
💡 ヒント: DynamoDBのパーティションキーとソートキーの概念を図に含めると、NoSQL設計の理解が深まります
理解度チェック:なぜ?を考えてみよう
AWSの各リソースや設計には、必ず”理由”や”目的”があります。 下記の「なぜ?」という問いに自分なりの言葉で答えてみましょう。 仕組みや設計意図を自分で説明できることが、真の理解につながります。 ぜひ、単なる暗記ではなく「なぜそうなっているのか?」を意識して考えてみてください。
Q. なぜモバイルアプリでNoSQLデータベース(DynamoDB)を選択するのか?リレーショナルデータベース(RDS)と比較してどのようなメリットがあるか?
Q. なぜDynamoDBでGlobal Secondary Index(GSI)を作成するのか?プライマリキーのみでのクエリと比較してどのような違いがあるか?
Q. なぜ位置情報データでlocation_hashをパーティションキーとして使用するのか?user_idをパーティションキーにした場合と比較してどのような問題が発生するか?
今回のステップで利用したAWSサービス名一覧
- Amazon DynamoDB:高パフォーマンスなNoSQLデータベースサービス
- AWS Lambda:DynamoDBとの連携処理を行うサーバーレスコンピューティング
- Amazon API Gateway:新しいデータ操作エンドポイントを提供するAPIサービス
- AWS IAM:LambdaからDynamoDBへのアクセス権限を管理するサービス
- Amazon CloudWatch:データベース操作のログ監視・パフォーマンス分析を行うサービス