はじめに
普段はL1アラート監視・ジョブスケジューラー運用の業務をしています。
この記事は、EC販売の在庫管理を題材に、AWS未経験の状態から、よくあるサーバーレス構成(HTML→API Gateway→Lambda→DynamoDB)を、無料利用枠の範囲内で実際に組んでみた記録です。
構築自体より、その過程で何度もハマったエラーとの格闘の方が学びが多かったので、そこを中心にまとめます。
(正直、この規模ならGoogleスプレッドシートで管理すれば十分説あります笑。とはいえAWSの練習にはちょうど良かったので、気にせず進めます。)
きっかけ:在庫が「頭の中」にしかなかった
商品の在庫は自宅の部屋に保管しており、SNS(Facebookなど)のチャットで注文が来るたびに、部屋まで在庫を確認しに行く必要がありました。この手間を、AWSの練習を兼ねて解消できないかと考え、スマホからワンタップで在庫を確認・更新できるダッシュボードを作ることにしました。
システム構成
最終的な構成は以下の通りです。
この構成(静的サイト+API Gateway+Lambda+DynamoDB)は、AWSのサーバーレスアプリケーションとして最もよく紹介される定番パターンで、複数のチュートリアル記事でも「最も一般的な組み合わせ」として取り上げられています。問い合わせフォームや小規模なECサイトなど、幅広い用途に応用が効きます。
[スタッフのスマホ]
│ ブラウザでアクセス
▼
[S3(静的ウェブサイトホスティング)]
HTML / CSS / JavaScript のダッシュボード
│ 在庫確認: GET /inventory
│ 在庫更新: POST /inventory/update
▼
[API Gateway]
リクエストの受付窓口、CORS設定
│
├──▶ [Lambda: GetInventoryList] ──▶ [DynamoDB] (読み取り専用)
│
└──▶ [Lambda: UpdateInventory] ──▶ [DynamoDB] (読み書き)
│
│ 在庫が閾値(2個)以下になったら
▼
[SNS: InventoryAlerts]
│
▼
[担当者のEメール]
一時的な処理(初回のみ)
[Googleスプレッドシート] → CSVエクスポート → [S3] → [DynamoDBへ一括インポート]
使用したAWSサービス
| サービス | 役割 |
|---|---|
| S3 | ダッシュボード(HTML)の公開、CSVの一時置き場 |
| DynamoDB | 在庫データの保存 |
| Lambda | 在庫取得・更新のロジック(Python) |
| API Gateway | HTTP APIの窓口、Lambdaとの統合、CORS制御 |
| SNS | 在庫が少なくなった際のEメール通知 |
| IAM | 各Lambdaへの最小権限(読み取り専用/読み書き)の付与 |
| CloudWatch | Lambdaの実行ログ確認・デバッグ |
①データ準備フェーズ
- Googleスプレッドシートで管理していた販売ログをCSVエクスポート
- S3バケットにCSVをアップロード
- DynamoDBの「S3からのインポート」機能で一括投入
つまずき:ソートキーの設定漏れ
最初、DynamoDBのパーティションキーを商品名(SKU)だけにしていました。すると、同じ商品の色違い・サイズ違い(例:Black / Red、ST / FL)が同じキーとして扱われ、後から取り込んだ方が前のデータを上書きしてしまいました。60件あったはずのデータが32件にまで減っていました。
解決策:パーティションキーをSKU、ソートキーをVariationとする複合キーでテーブルを作り直し、色違い・サイズ違いを正しく別レコードとして扱えるようにしました。
②バックエンド構築フェーズ
GetInventoryList(在庫一覧の取得)
import json
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('TableTennisInventoryEN')
def lambda_handler(event, context):
response = table.scan()
items = response.get('Items', [])
return {
'statusCode': 200,
'headers': {'Access-Control-Allow-Origin': '*'},
'body': json.dumps(items, default=str)
}
UpdateInventory(在庫の更新)
在庫を更新した後、閾値以下ならSNSに通知を送るロジックを組み込みました(詳細は後述)。
API Gatewayのルート設計
| メソッド | パス | 統合先Lambda |
|---|---|---|
| GET | /inventory | GetInventoryList |
| POST | /inventory/update | UpdateInventory |
$defaultステージの自動デプロイを有効化し、設定変更が即座に反映される構成にしました。
③トラブル対応フェーズ
ここが今回一番時間を要した部分です。2つのエラーが、まるでバトンリレーのように連続して発生しました。
トラブル1:DynamoDBの構文エラー(500エラー)
An error occurred (ValidationException) when calling the UpdateItem operation:
Invalid UpdateExpression: Syntax error; token: "現", near: "SET 現在庫数"
原因:DynamoDBの更新式(UpdateExpression)に、日本語の属性名(現在庫数)をそのまま書いたため、構文解析エンジンが解釈できなかった。
一時対応:ExpressionAttributeNamesでプレースホルダー(#stock)に置き換えて回避。
table.update_item(
Key={'SKU': sku, 'バリエーション': variation},
UpdateExpression='SET #stock = :val',
ExpressionAttributeNames={'#stock': '現在庫数'},
ExpressionAttributeValues={':val': new_stock}
)
トラブル2:文字コードエラー
トラブル1の対応後、さらに別のエラーが発生しました。
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 265-269:
Body ('現在庫数') is not valid Latin-1. Use body.encode('utf-8') if you want to send it encoded in UTF-8.
原因:新しいPythonランタイムの内部HTTPクライアントが、レスポンスの日本語文字を、本来UTF-8で扱うべきところをLatin-1でエンコードしようとして失敗していた。
④根本解決:属性名を英語に統一
2つのエラーはそれぞれ対症療法で個別に回避できましたが、根っこの原因は同じでした。日本語の属性名を、日本語を想定していない仕組み(DynamoDBの構文解析、HTTPの文字エンコーディング)に通そうとしていたことです。
そこで、小手先の対処を重ねるのをやめ、テーブルを作り直して属性名を SKU / Variation / Stock の英語に統一しました。CSV・Lambda・HTMLもすべて英語属性名に書き換えたところ、両方のエラーが同時に解消しました。
# 修正後:プレースホルダー不要でシンプルに
table.update_item(
Key={'SKU': sku, 'Variation': variation},
UpdateExpression='SET Stock = :val',
ExpressionAttributeValues={':val': new_stock}
)
⑤追加機能:在庫少アラート(SNS通知)
基本機能が動くようになった後、「在庫が少なくなったら自動でメールが届く」という機能を追加しました。
構成
- SNSトピック(標準タイプ、
InventoryAlerts)を作成 - Eメールサブスクリプションを登録
-
UpdateInventoryのロジックの中で、更新後の在庫数が閾値(2個)以下になったらSNSへpublish
LOW_STOCK_THRESHOLD = 2
if new_stock <= LOW_STOCK_THRESHOLD:
try:
sns.publish(
TopicArn=TOPIC_ARN,
Subject='Low stock alert',
Message=f'{sku} / {variation} stock is now {new_stock}'
)
except Exception as sns_error:
# 通知が失敗しても在庫更新自体は成功として扱う
print(f'SNS publish failed: {sns_error}')
設計上意識した点
- SNSの発行処理は独立した
try/exceptで囲み、通知の失敗が在庫更新自体の失敗として扱われないようにしました。通知は「あれば嬉しい付加機能」であり、本処理(在庫更新)の信頼性を落としてはいけないという考え方です - ここでも前回の教訓を踏まえ、通知の件名・本文はすべて英語で統一しました
おわりに:振り返って
正直に言うと、在庫確認・更新という機能だけを見れば、Googleスプレッドシートの共有だけでも同等のことは実現でき、今回の構築にかけた時間対効果は見合っていません。
一方で、DynamoDBのキー設計・構文制約、文字エンコーディングという、実務でも頻出する典型的なつまずきポイントを、実際に自分の手でエラーに直面しながら切り分けた経験は、単なる座学では得られない学びになりました。特に、2つのエラーが一見バラバラに見えて、実は「日本語文字の扱い」という同じ根っこの問題だったと気づけたことが、今回一番の収穫だったと思います。
AWS未経験からの学習中の方の参考に、少しでもなれば幸いです。
今後の展望
現状は自宅在庫のみを管理する構成ですが、今後はShopeeやTikTok Shopなどの公開APIとも連携し、各ECサイト上の在庫もこの仕組みに取り込んで、一元管理できるようにしていきたいと考えています。