このハンズオンでは、サーバレスでHTTP APIを公開する方法を段階的に学びます。
サーバレスAPIを選ぶ際、実行時間制限は重要な判断材料です。
サービス | プラン | 実行時間の上限 |
Google Apps Script | 無料版 | 6分 |
Google Apps Script | Google Workspace(有料版) | 30分 |
AWS Lambda | — | 15分 |
このハンズオンでは、以下のように段階的に構成を発展させます。
Step 1: GAS → Webアプリとして公開(手軽だが制約あり)
↓
Step 2: Lambda Function URL(AWSでの同等構成、制約も同様)
↓
Step 3: セキュリティ暫定対策(GAS: スプレッドシート照合 / Lambda: DynamoDB照合)
↓
Step 4: API Gateway + WAF + Lambda(本格的・セキュアな構成)
本ハンズオンでは、特記ない限りプロジェクトのルートディレクトリ(12-AWS-handson... フォルダ)でコマンドを実行することを前提としています。
また、Windows のコマンドプロンプトを使用する場合、ターミナルを閉じると set コマンドで設定した環境変数(API_ID や API_KEY 等)が消えてしまいます。ターミナルを再起動した場合は、必要に応じて変数を再設定してください。
本ハンズオンを実行するためにはAWSアカウントが必要です。 すでにAWSから直接払い出されたアカウント、ハンズオンイベント等で払い出されているAWSアカウントをご利用の場合は、このステップをスキップしてかまいません。
新しく個人で作成する場合、2026年5月6日現在、アカウント作成時にはクレジットカードの登録が必要となりますが、登録から半年間は200 USD 分の無料クレジット枠が付与されるキャンペーンや、常時無料枠が用意されています。
詳細な作成手順については、以下の公式ページをご覧ください。
2025年7月16日にAWSの無料利用枠の仕組みが変更となり、元の「12ヶ月の無料利用枠」が新規ユーザー向けに新しい「無料プラン」へと変更になりました。以下の手順で無料プランのアカウントを作成できます。
作成に必要なもの
手順
no-reply@signup.aws から届いた認証メールの検証コードを確認します。Tokyo のようにローマ字入力が必要です。無料枠の確認方法 AWSマネジメントコンソールにログイン後、右上の <アカウント名> ▼ をクリックすると、無料プランの残りクレジットと残りの日数を確認できます。
AWSには、クレジットを消費せずに利用できる**「常に無料」のサービス**と、クレジットを利用して評価・検証が可能なサービスがあります。
以下のサービスは、無料プラン対象のインスタンスを選択したり、付与されたクレジットを消費することで無料で評価・利用が可能です。
AWS Kiro-IDE などのツールを利用・連携するために、「AWS Builder ID」が必要となります。 AWS Builder ID は AWS や Amazon.co.jp アカウントとは異なる、個人のための無料アカウントです(クレジットカードの登録なども不要です)。
本ハンズオンは、ご自身のローカルPC(Windows または macOS/Linux)または AWS Kiro-IDE などのクラウドIDEで実行することを前提としています。 以下のツールがインストールされているか確認し、インストールされていない場合はセットアップを行ってください。
本ハンズオンでは、デスクトップIDEである AWS Kiro-IDE を使用します。 https://kiro.dev/ にアクセスし、サイトの手順に従ってダウンロード・インストールを行ってください。
以降のファイル編集などの操作は、このエディタ環境内で行うものとします。
GASのコマンドラインツール(clasp)を使用するために必要です。
Windows の場合Node.js 公式サイト から Windows Installer (.msi) をダウンロードしてインストールしてください(LTS版を推奨)。
macOS の場合 Homebrew を使用してインストールします。
brew install node
Linux (Ubuntu/Debian) の場合
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs
AWS Lambda のコードを作成・パッケージングするために使用します。
Windows の場合Python 公式サイト からインストーラをダウンロードしてインストールしてください。 ※ インストール時に「Add Python to PATH」にチェックを入れるのを忘れないでください。
macOS の場合
brew install python
Linux (Ubuntu/Debian) の場合
sudo apt-get update && sudo apt-get install -y python3 python3-pip
AWS リソースをコマンドラインから操作するために必要です。
Windows の場合AWS CLI MSI インストーラ をダウンロードして実行してください。
macOS の場合
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg" sudo installer -pkg AWSCLIV2.pkg -target /
Linux の場合
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install
Google Apps Script のプロジェクトをローカルで管理するためのツールです。
コマンドプロンプト(Windows)またはターミナル(macOS/Linux)を開き、以下のコマンドを実行します。
npm install -g @google/clasp
インストールが完了したら、Google アカウントにログインします。
clasp login
ターミナル(またはコマンドプロンプト)で以下のコマンドを実行し、バージョンが表示されれば準備完了です。
Windows (コマンドプロンプト) の場合
aws --version python -V node -v npm -v clasp --version
macOS/Linux (ターミナル) の場合
aws --version; python3 -V; node -v; npm -v; clasp --version
実行結果の例
aws-cli/2.15.0 Python/3.11.6 ...
Python 3.12.1
v20.10.0
10.2.3
2.4.2
AWS CLI から AWS リソースを操作するための専用ユーザーを作成し、アクセスキーを発行します。最初のユーザー作成は、AWS マネジメントコンソール(ブラウザ)から行います。
handson-userAdministratorAccess と入力し、チェックボックスをオンにします。handson-user の名前をクリックして詳細画面を開きます。ターミナル(またはコマンドプロンプト)で以下のコマンドを実行し、先ほど発行したアクセスキーを設定します。
aws configure
項目 | 値 |
AWS Access Key ID | 発行したアクセスキー |
AWS Secret Access Key | 発行したシークレットアクセスキー |
Default region name |
|
Default output format |
|
aws sts get-caller-identity
実行結果の例
{
"UserId": "AIDASAMPLEUSERID",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/handson-user"
}
正しく設定されていれば、アカウントIDやユーザーARNが表示されます。
ターミナル(またはコマンドプロンプト)で clasp を使ってプロジェクトを作成します。
Windows (コマンドプロンプト) の場合
mkdir gas\04 cd gas\04 clasp create --title "handson-text-api" --type standalone
macOS/Linux (ターミナル) の場合
mkdir -p gas/04 && cd gas/04 clasp create --title "handson-text-api" --type standalone
実行結果の例
Created new standalone script: handson-text-api
...
Kiro を使用して作成されたディレクトリ内の Code.js を開き、以下の内容で上書き保存してください。
/**
* GETリクエストのハンドラー
* @param {Object} e - イベントオブジェクト
* @return {TextOutput} JSON形式のレスポンス
*/
function doGet(e) {
var output = {
service: "テキスト要約API(GAS版)",
version: "1.0",
usage: "POSTリクエストで {text: '要約したいテキスト', max_sentences: 3} を送信してください"
};
return ContentService
.createTextOutput(JSON.stringify(output))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* POSTリクエストのハンドラー - テキスト要約処理
* @param {Object} e - イベントオブジェクト
* @return {TextOutput} JSON形式のレスポンス
*/
function doPost(e) {
try {
var data = JSON.parse(e.postData.contents);
var text = data.text || "";
var maxSentences = data.max_sentences || 3;
if (!text) {
return ContentService
.createTextOutput(JSON.stringify({
error: "テキストが指定されていません"
}))
.setMimeType(ContentService.MimeType.JSON);
}
// 簡易要約処理: 文を分割して先頭N文を抽出
var sentences;
if (text.indexOf("。") !== -1) {
sentences = text.split("。").filter(function(s) { return s.trim(); });
sentences = sentences.map(function(s) { return s.trim() + "。"; });
} else {
sentences = text.split(".").filter(function(s) { return s.trim(); });
sentences = sentences.map(function(s) { return s.trim() + "."; });
}
var summarySentences = sentences.slice(0, maxSentences);
var summary = summarySentences.join("");
var result = {
original_length: text.length,
summary: summary,
sentence_count: sentences.length,
summary_sentence_count: summarySentences.length
};
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({
error: "リクエストの処理中にエラーが発生しました: " + error.message
}))
.setMimeType(ContentService.MimeType.JSON);
}
}
Windows (コマンドプロンプト) の場合
:: コードを Apps Script にアップロード clasp push :: バージョンを作成 clasp version "初回デプロイ" :: Webアプリとしてデプロイ clasp deploy --description "テキスト要約API"
macOS/Linux (ターミナル) の場合
clasp push clasp version "初回デプロイ" clasp deploy --description "テキスト要約API"
実行結果の例
Created version 1.
- AKfycbwEXAMPLE_DEPLOY_ID @1.
デプロイIDが表示されます。WebアプリのURLは以下の形式です。
https://script.google.com/macros/s/{デプロイID}/exec
デプロイID(AKfy...のような文字列)をコピーしておきます。
Windows (コマンドプロンプト) の場合
:: デプロイIDを変数に設定(表示されたIDに置き換え)
set DEPLOY_ID=ここにデプロイIDを貼り付け
:: GETリクエスト
curl -L "https://script.google.com/macros/s/%DEPLOY_ID%/exec"
:: POSTリクエスト(WindowsコマンドプロンプトではJSONのエスケープが必要です)
curl -L -X POST -H "Content-Type: application/json" -d "{\"text\": \"AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。\", \"max_sentences\": 2}" "https://script.google.com/macros/s/%DEPLOY_ID%/exec"
macOS/Linux (ターミナル) の場合
# デプロイIDを変数に設定(表示されたIDに置き換え)
DEPLOY_ID="ここにデプロイIDを貼り付け"
# GETリクエスト
curl -L "https://script.google.com/macros/s/${DEPLOY_ID}/exec"
# POSTリクエスト
curl -L -X POST \
-H "Content-Type: application/json" \
-d '{"text": "AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。", "max_sentences": 2}' \
"https://script.google.com/macros/s/${DEPLOY_ID}/exec"
実行結果の例
{
"original_length": 68,
"summary": "AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。",
"sentence_count": 3,
"summary_sentence_count": 2
}
Lambda関数用の実行ロールを作成します。
Windows (コマンドプロンプト) の場合
mkdir lambda\05 cd lambda\05
macOS/Linux (ターミナル) の場合
mkdir -p lambda/05 && cd lambda/05
Kiro を使用して同ディレクトリ内に trust-policy.json を作成し、以下の内容を保存します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
Windows (コマンドプロンプト) の場合
:: IAMロールの作成 aws iam create-role ^ --role-name handson-furl-lambda-role ^ --assume-role-policy-document file://trust-policy.json :: CloudWatchLogsFullAccess ポリシーをアタッチ aws iam attach-role-policy ^ --role-name handson-furl-lambda-role ^ --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
macOS/Linux (ターミナル) の場合
# IAMロールの作成 aws iam create-role \ --role-name handson-furl-lambda-role \ --assume-role-policy-document file://trust-policy.json # CloudWatchLogsFullAccess ポリシーをアタッチ aws iam attach-role-policy \ --role-name handson-furl-lambda-role \ --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
Kiro を使用して同ディレクトリ内(lambda/05)に lambda_function.py を作成し、以下の内容を保存します。
import json
def lambda_handler(event, context):
"""Lambda Function URL用のテキスト要約API"""
# リクエストメソッドの取得
http_method = event.get('requestContext', {}).get('http', {}).get('method', 'GET')
# GETリクエスト: API情報を返す
if http_method == 'GET':
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json; charset=utf-8'
},
'body': json.dumps({
'service': 'テキスト要約API(Lambda版)',
'version': '1.0',
'usage': 'POSTリクエストで {"text": "要約したいテキスト", "max_sentences": 3} を送信してください'
}, ensure_ascii=False)
}
# POSTリクエスト: テキスト要約処理
if http_method == 'POST':
try:
body = event.get('body') or '{}'
if event.get('isBase64Encoded', False):
import base64
body = base64.b64decode(body).decode('utf-8')
data = json.loads(body)
text = data.get('text', '')
max_sentences = data.get('max_sentences', 3)
if not text:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({
'error': 'テキストが指定されていません'
}, ensure_ascii=False)
}
# 簡易要約処理
if '。' in text:
sentences = [s.strip() + '。' for s in text.split('。') if s.strip()]
else:
sentences = [s.strip() + '.' for s in text.split('.') if s.strip()]
summary_sentences = sentences[:max_sentences]
summary = ''.join(summary_sentences) if '。' in text else ' '.join(summary_sentences)
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({
'original_length': len(text),
'summary': summary,
'sentence_count': len(sentences),
'summary_sentence_count': len(summary_sentences)
}, ensure_ascii=False)
}
except json.JSONDecodeError:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({'error': 'JSONの形式が不正です'}, ensure_ascii=False)
}
return {
'statusCode': 405,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({'error': f'{http_method}はサポートされていません'}, ensure_ascii=False)
}
コードを保存したら、ZIPファイルに圧縮し、Lambda関数を作成します。
Windows (コマンドプロンプト) の場合
:: zipファイルの作成 (Windows 10/11 以降で利用可能な tar コマンドを使用)
tar -a -c -f function.zip lambda_function.py
:: アカウントIDの取得と関数の作成
for /f "delims=" %i in ('aws sts get-caller-identity --query Account --output text') do set ACCOUNT_ID=%i
aws lambda create-function ^
--function-name handson-text-summarizer ^
--runtime python3.12 ^
--handler lambda_function.lambda_handler ^
--role arn:aws:iam::%ACCOUNT_ID%:role/handson-furl-lambda-role ^
--zip-file fileb://function.zip
macOS/Linux (ターミナル) の場合
# zipファイルの作成
zip function.zip lambda_function.py
# アカウントIDの取得と関数の作成
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws lambda create-function \
--function-name handson-text-summarizer \
--runtime python3.12 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::${ACCOUNT_ID}:role/handson-furl-lambda-role \
--zip-file fileb://function.zip
実行結果の例
{
"FunctionName": "handson-text-summarizer",
"FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:handson-text-summarizer",
"Runtime": "python3.12",
...
}
Windows (コマンドプロンプト) の場合
:: Function URLの作成
aws lambda create-function-url-config ^
--function-name handson-text-summarizer ^
--auth-type NONE ^
--cors "{\"AllowOrigins\":[\"*\"],\"AllowMethods\":[\"GET\",\"POST\"],\"AllowHeaders\":[\"Content-Type\"]}"
:: 公開アクセス許可の付与
aws lambda add-permission ^
--function-name handson-text-summarizer ^
--statement-id FunctionURLAllowPublicAccess ^
--action lambda:InvokeFunctionUrl ^
--principal "*" ^
--function-url-auth-type NONE
macOS/Linux (ターミナル) の場合
# Function URLの作成
aws lambda create-function-url-config \
--function-name handson-text-summarizer \
--auth-type NONE \
--cors '{"AllowOrigins":["*"],"AllowMethods":["GET","POST"],"AllowHeaders":["Content-Type"]}'
# 公開アクセス許可の付与
aws lambda add-permission \
--function-name handson-text-summarizer \
--statement-id FunctionURLAllowPublicAccess \
--action lambda:InvokeFunctionUrl \
--principal "*" \
--function-url-auth-type NONE
Function URLを取得します。
Windows (コマンドプロンプト) の場合
for /f "delims=" %i in ('aws lambda get-function-url-config --function-name handson-text-summarizer --query FunctionUrl --output text') do set FUNCTION_URL=%i
echo %FUNCTION_URL%
macOS/Linux (ターミナル) の場合
FUNCTION_URL=$(aws lambda get-function-url-config \ --function-name handson-text-summarizer \ --query FunctionUrl --output text) echo $FUNCTION_URL
Windows (コマンドプロンプト) の場合
:: GETリクエスト
curl %FUNCTION_URL%
:: POSTリクエスト
curl -X POST %FUNCTION_URL% -H "Content-Type: application/json" -d "{\"text\": \"AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。\", \"max_sentences\": 2}"
macOS/Linux (ターミナル) の場合
# GETリクエスト
curl $FUNCTION_URL
# POSTリクエスト
curl -X POST $FUNCTION_URL \
-H "Content-Type: application/json" \
-d '{"text": "AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。", "max_sentences": 2}'
ここまでの実装を踏まえて、GASとLambda Function URLを比較します。
比較項目 | GAS(無料版) | GAS(有料版) | Lambda Function URL |
実行時間上限 | 6分 | 30分 | 15分 |
デプロイ | ワンクリック | ワンクリック | Deploy+URL設定 |
言語 | JavaScript | JavaScript | Python, Node.js 等 |
コスト | 無料 | Workspace料金 | 従量課金 |
認証 | Googleアカウント or なし | 同左 | NONE or IAM |
カスタムドメイン | 不可 | 不可 | 不可 |
WAF連携 | 不可 | 不可 | 不可 |
レートリミット | GAS側の制限のみ | 同左 | なし(課金増大リスク) |
どちらの方式も、URLを知っていれば誰でもアクセスできるというセキュリティ上の課題があります。
API GatewayやWAFを使わなくても、アプリケーションレベルでの認証チェックを導入することで、ある程度のアクセス制御が可能です。
APIKeys に変更しますkey、B1セルに description と入力しますhandson-demo-key-2026)を入力します/d/ と /edit の間の文字列)新しいディレクトリを作成し、プロジェクトを初期化します。
Windows (コマンドプロンプト) の場合
mkdir gas\06 cd gas\06 clasp create --title "handson-text-api-v2" --type standalone
macOS/Linux (ターミナル) の場合
mkdir -p gas/06 && cd gas/06 clasp create --title "handson-text-api-v2" --type standalone
Kiro を使用してディレクトリ内に生成された Code.js を開き、以下の内容で上書き保存してください。
// スプレッドシートID(自分のスプレッドシートIDに置き換え)
var SPREADSHEET_ID = "{あなたのスプレッドシートID}";
/**
* APIキーを検証する
* @param {string} apiKey - 検証するAPIキー
* @return {boolean} 有効なキーならtrue
*/
function validateApiKey(apiKey) {
if (!apiKey) return false;
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName("APIKeys");
var lastRow = sheet.getLastRow();
if (lastRow < 2) return false;
var keys = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (var i = 0; i < keys.length; i++) {
if (keys[i][0] === apiKey) return true;
}
return false;
}
/**
* GETリクエストのハンドラー
*/
function doGet(e) {
return ContentService
.createTextOutput(JSON.stringify({
service: "テキスト要約API(GAS版・認証あり)",
version: "2.0",
usage: "x-api-keyパラメータにAPIキーを指定してください"
}))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* POSTリクエストのハンドラー - APIキー認証付き
*/
function doPost(e) {
try {
var data = JSON.parse(e.postData.contents);
// APIキーの検証
var apiKey = data.api_key || e.parameter.api_key || "";
if (!validateApiKey(apiKey)) {
return ContentService
.createTextOutput(JSON.stringify({
error: "無効なAPIキーです。正しいAPIキーを指定してください。"
}))
.setMimeType(ContentService.MimeType.JSON);
}
var text = data.text || "";
var maxSentences = data.max_sentences || 3;
if (!text) {
return ContentService
.createTextOutput(JSON.stringify({ error: "テキストが指定されていません" }))
.setMimeType(ContentService.MimeType.JSON);
}
// 簡易要約処理
var sentences;
if (text.indexOf("。") !== -1) {
sentences = text.split("。").filter(function(s) { return s.trim(); });
sentences = sentences.map(function(s) { return s.trim() + "。"; });
} else {
sentences = text.split(".").filter(function(s) { return s.trim(); });
sentences = sentences.map(function(s) { return s.trim() + "."; });
}
var summarySentences = sentences.slice(0, maxSentences);
return ContentService
.createTextOutput(JSON.stringify({
original_length: text.length,
summary: summarySentences.join(""),
sentence_count: sentences.length,
summary_sentence_count: summarySentences.length
}))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ error: "エラー: " + error.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
Windows (コマンドプロンプト) の場合
clasp push clasp version "APIキー認証追加" clasp deploy --description "APIキー認証付きテキスト要約API"
macOS/Linux (ターミナル) の場合
clasp push clasp version "APIキー認証追加" clasp deploy --description "APIキー認証付きテキスト要約API"
Windows (コマンドプロンプト) の場合
set DEPLOY_ID=ここにデプロイIDを貼り付け
:: APIキーなし → エラー
curl -L -X POST -H "Content-Type: application/json" -d "{\"text\": \"テストです。要約します。\"}" "https://script.google.com/macros/s/%DEPLOY_ID%/exec"
:: APIキーあり → 成功
curl -L -X POST -H "Content-Type: application/json" -d "{\"api_key\": \"handson-demo-key-2026\", \"text\": \"テストです。要約します。\", \"max_sentences\": 1}" "https://script.google.com/macros/s/%DEPLOY_ID%/exec"
macOS/Linux (ターミナル) の場合
# 新しいデプロイIDを変数に設定(表示されたIDに置き換え)
DEPLOY_ID="ここにデプロイIDを貼り付け"
# APIキーなし → エラー
curl -L -X POST \
-H "Content-Type: application/json" \
-d '{"text": "テストです。要約します。"}' \
"https://script.google.com/macros/s/${DEPLOY_ID}/exec"
# APIキーあり → 成功
curl -L -X POST \
-H "Content-Type: application/json" \
-d '{"api_key": "handson-demo-key-2026", "text": "テストです。要約します。", "max_sentences": 1}' \
"https://script.google.com/macros/s/${DEPLOY_ID}/exec"
Windows (コマンドプロンプト) の場合
aws dynamodb create-table ^ --table-name handson-api-keys ^ --attribute-definitions AttributeName=api_key,AttributeType=S ^ --key-schema AttributeName=api_key,KeyType=HASH ^ --billing-mode PAY_PER_REQUEST
macOS/Linux (ターミナル) の場合
aws dynamodb create-table \ --table-name handson-api-keys \ --attribute-definitions AttributeName=api_key,AttributeType=S \ --key-schema AttributeName=api_key,KeyType=HASH \ --billing-mode PAY_PER_REQUEST
実行結果の例
{
"TableDescription": {
"TableName": "handson-api-keys",
"TableStatus": "CREATING",
...
}
}
Windows (コマンドプロンプト) の場合
aws dynamodb put-item ^
--table-name handson-api-keys ^
--item "{\"api_key\": {\"S\": \"handson-demo-key-2026\"}}"
macOS/Linux (ターミナル) の場合
aws dynamodb put-item \
--table-name handson-api-keys \
--item '{"api_key": {"S": "handson-demo-key-2026"}}'
Lambda関数がDynamoDBにアクセスできるよう、ポリシーを追加します。
Windows (コマンドプロンプト) の場合
aws iam attach-role-policy ^ --role-name handson-furl-lambda-role ^ --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
macOS/Linux (ターミナル) の場合
aws iam attach-role-policy \ --role-name handson-furl-lambda-role \ --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
Lambda関数のコードを以下に更新します。
Windows (コマンドプロンプト) の場合
mkdir lambda\06 cd lambda\06
macOS/Linux (ターミナル) の場合
mkdir -p lambda/06 && cd lambda/06
Kiro を使用してディレクトリ内に lambda_function.py を作成し、以下の内容を保存します。
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('handson-api-keys')
def validate_api_key(api_key):
"""DynamoDBでAPIキーを検証する"""
if not api_key:
return False
try:
response = table.get_item(Key={'api_key': api_key})
return 'Item' in response
except Exception:
return False
def lambda_handler(event, context):
"""Lambda Function URL用のテキスト要約API(APIキー認証付き)"""
http_method = event.get('requestContext', {}).get('http', {}).get('method', 'GET')
# GETリクエスト: API情報を返す
if http_method == 'GET':
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({
'service': 'テキスト要約API(Lambda版・認証あり)',
'version': '2.0',
'usage': 'POSTリクエストで {"api_key": "...", "text": "...", "max_sentences": 3} を送信'
}, ensure_ascii=False)
}
# POSTリクエスト: APIキー認証 + テキスト要約
if http_method == 'POST':
try:
body = event.get('body', '{}')
if event.get('isBase64Encoded', False):
import base64
body = base64.b64decode(body).decode('utf-8')
data = json.loads(body)
# APIキーの検証
api_key = data.get('api_key', '')
if not validate_api_key(api_key):
return {
'statusCode': 403,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({
'error': '無効なAPIキーです'
}, ensure_ascii=False)
}
text = data.get('text', '')
max_sentences = data.get('max_sentences', 3)
if not text:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({'error': 'テキストが指定されていません'}, ensure_ascii=False)
}
# 簡易要約処理
if '。' in text:
sentences = [s.strip() + '。' for s in text.split('。') if s.strip()]
else:
sentences = [s.strip() + '.' for s in text.split('.') if s.strip()]
summary_sentences = sentences[:max_sentences]
summary = ''.join(summary_sentences) if '。' in text else ' '.join(summary_sentences)
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({
'original_length': len(text),
'summary': summary,
'sentence_count': len(sentences),
'summary_sentence_count': len(summary_sentences)
}, ensure_ascii=False)
}
except json.JSONDecodeError:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({'error': 'JSONの形式が不正です'}, ensure_ascii=False)
}
return {
'statusCode': 405,
'headers': {'Content-Type': 'application/json; charset=utf-8'},
'body': json.dumps({'error': f'{http_method}はサポートされていません'}, ensure_ascii=False)
}
ファイルを保存したら、圧縮して更新します。
Windows (コマンドプロンプト) の場合
tar -a -c -f function.zip lambda_function.py aws lambda update-function-code ^ --function-name handson-text-summarizer ^ --zip-file fileb://function.zip
macOS/Linux (ターミナル) の場合
zip function.zip lambda_function.py aws lambda update-function-code \ --function-name handson-text-summarizer \ --zip-file fileb://function.zip
Windows (コマンドプロンプト) の場合
:: APIキーなし → 403
curl -X POST %FUNCTION_URL% -H "Content-Type: application/json" -d "{\"text\": \"テストです。\"}"
:: APIキーあり → 200
curl -X POST %FUNCTION_URL% -H "Content-Type: application/json" -d "{\"api_key\": \"handson-demo-key-2026\", \"text\": \"テストです。要約します。\", \"max_sentences\": 1}"
macOS/Linux (ターミナル) の場合
# APIキーなし → 403
curl -X POST $FUNCTION_URL \
-H "Content-Type: application/json" \
-d '{"text": "テストです。"}'
# APIキーあり → 200
curl -X POST $FUNCTION_URL \
-H "Content-Type: application/json" \
-d '{"api_key": "handson-demo-key-2026", "text": "テストです。要約します。", "max_sentences": 1}'
スプレッドシートやDynamoDBでのキー照合は暫定対策であり、以下の課題が残ります。
課題 | 説明 |
レートリミットなし | 大量リクエストを防げない |
DDoS対策なし | エンドポイントへの攻撃を防げない |
キー管理の煩雑さ | キーのローテーションや失効管理が手動 |
ログ・監査 | APIの利用状況を体系的に把握しづらい |
ここからは、Lambda Function URLの代わりにAPI Gatewayを使った本格的な構成を構築します。
API Gateway用にLambda関数を更新します。既存の handson-text-summarizer のコードを以下に更新します。
(DynamoDBのAPIキー認証は、API Gateway側のAPIキー認証に置き換えるため削除します)
Windows (コマンドプロンプト) の場合
mkdir lambda\07 cd lambda\07
macOS/Linux (ターミナル) の場合
mkdir -p lambda/07 && cd lambda/07
Kiro を使用してディレクトリ内に lambda_function.py を作成し、以下の内容を保存します。
import json
def lambda_handler(event, context):
"""API Gateway用のテキスト要約API"""
http_method = event.get('httpMethod', 'GET')
if http_method == 'GET':
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'service': 'テキスト要約API',
'version': '3.0',
'description': 'API Gateway + WAF経由のセキュアなAPI'
}, ensure_ascii=False)
}
if http_method == 'POST':
try:
body = event.get('body') or '{}'
data = json.loads(body) if isinstance(body, str) else body
text = data.get('text', '')
max_sentences = data.get('max_sentences', 3)
if not text:
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': 'テキストが指定されていません'}, ensure_ascii=False)
}
if '。' in text:
sentences = [s.strip() + '。' for s in text.split('。') if s.strip()]
else:
sentences = [s.strip() + '.' for s in text.split('.') if s.strip()]
summary_sentences = sentences[:max_sentences]
summary = ''.join(summary_sentences) if '。' in text else ' '.join(summary_sentences)
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'original_length': len(text),
'summary': summary,
'sentence_count': len(sentences),
'summary_sentence_count': len(summary_sentences)
}, ensure_ascii=False)
}
except json.JSONDecodeError:
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': 'JSONの形式が不正です'}, ensure_ascii=False)
}
return {
'statusCode': 405,
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': f'{http_method}はサポートされていません'}, ensure_ascii=False)
}
Windows (コマンドプロンプト) の場合
tar -a -c -f function.zip lambda_function.py aws lambda update-function-code ^ --function-name handson-text-summarizer ^ --zip-file fileb://function.zip
macOS/Linux (ターミナル) の場合
zip function.zip lambda_function.py aws lambda update-function-code \ --function-name handson-text-summarizer \ --zip-file fileb://function.zip
API Gateway経由に切り替えるため、既存のFunction URLを削除します。
Windows (コマンドプロンプト) の場合
aws lambda delete-function-url-config --function-name handson-text-summarizer aws lambda remove-permission --function-name handson-text-summarizer ^ --statement-id FunctionURLAllowPublicAccess
macOS/Linux (ターミナル) の場合
aws lambda delete-function-url-config --function-name handson-text-summarizer aws lambda remove-permission --function-name handson-text-summarizer \ --statement-id FunctionURLAllowPublicAccess
以降の手順では、コマンドの出力を変数に格納して利用します。
Windows (コマンドプロンプト) の場合
:: REST APIの作成
for /f "delims=" %i in ('aws apigateway create-rest-api --name handson-text-summarizer-api --description "テキスト要約REST API" --endpoint-configuration types=REGIONAL --query id --output text') do set API_ID=%i
echo API ID: %API_ID%
:: ルートリソースIDの取得
for /f "delims=" %i in ('aws apigateway get-resources --rest-api-id %API_ID% --query "items[?path=='/'].id" --output text') do set ROOT_ID=%i
:: /summarize リソースの作成
for /f "delims=" %i in ('aws apigateway create-resource --rest-api-id %API_ID% --parent-id %ROOT_ID% --path-part summarize --query id --output text') do set RESOURCE_ID=%i
echo Resource ID: %RESOURCE_ID%
macOS/Linux (ターミナル) の場合
# REST APIの作成 API_ID=$(aws apigateway create-rest-api \ --name handson-text-summarizer-api \ --description "テキスト要約REST API" \ --endpoint-configuration types=REGIONAL \ --query id --output text) echo "API ID: $API_ID" # ルートリソースIDの取得 ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID \ --query "items[?path=='/'].id" --output text) # /summarize リソースの作成 RESOURCE_ID=$(aws apigateway create-resource \ --rest-api-id $API_ID \ --parent-id $ROOT_ID \ --path-part summarize \ --query id --output text) echo "Resource ID: $RESOURCE_ID"
実行結果の例
API ID: abcdef1234
Resource ID: ghijk5678
Windows (コマンドプロンプト) の場合
for /f "delims=" %i in ('aws sts get-caller-identity --query Account --output text') do set ACCOUNT_ID=%i
set REGION=ap-northeast-1
aws apigateway put-method --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method GET --authorization-type NONE
aws apigateway put-integration --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method GET --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:%REGION%:lambda:path/2015-03-31/functions/arn:aws:lambda:%REGION%:%ACCOUNT_ID%:function:handson-text-summarizer/invocations"
aws apigateway put-method --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method POST --authorization-type NONE
aws apigateway put-integration --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method POST --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:%REGION%:lambda:path/2015-03-31/functions/arn:aws:lambda:%REGION%:%ACCOUNT_ID%:function:handson-text-summarizer/invocations"
aws apigateway put-method --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method OPTIONS --authorization-type NONE
aws apigateway put-integration --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method OPTIONS --type MOCK --request-templates "{\"application/json\": \"{\\\"statusCode\\\": 200}\"}"
aws apigateway put-method-response --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method OPTIONS --status-code 200 --response-parameters "{\"method.response.header.Access-Control-Allow-Headers\":false,\"method.response.header.Access-Control-Allow-Methods\":false,\"method.response.header.Access-Control-Allow-Origin\":false}"
aws apigateway put-integration-response --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method OPTIONS --status-code 200 --response-parameters "{\"method.response.header.Access-Control-Allow-Headers\":\"'Content-Type,X-Amz-Date,Authorization,X-Api-Key'\",\"method.response.header.Access-Control-Allow-Methods\":\"'GET,POST,OPTIONS'\",\"method.response.header.Access-Control-Allow-Origin\":\"'*'\"}"
macOS/Linux (ターミナル) の場合
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-1
# GETメソッドの作成
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method GET \
--authorization-type NONE
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:handson-text-summarizer/invocations"
# POSTメソッドの作成
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method POST \
--authorization-type NONE
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method POST \
--type AWS_PROXY \
--integration-http-method POST \
--uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:handson-text-summarizer/invocations"
# OPTIONSメソッドの作成(CORS用)
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method OPTIONS \
--authorization-type NONE
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method OPTIONS \
--type MOCK \
--request-templates '{"application/json": "{\"statusCode\": 200}"}'
aws apigateway put-method-response \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method OPTIONS \
--status-code 200 \
--response-parameters '{"method.response.header.Access-Control-Allow-Headers":false,"method.response.header.Access-Control-Allow-Methods":false,"method.response.header.Access-Control-Allow-Origin":false}'
aws apigateway put-integration-response \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method OPTIONS \
--status-code 200 \
--response-parameters '{"method.response.header.Access-Control-Allow-Headers":"'"'"'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"'"'","method.response.header.Access-Control-Allow-Methods":"'"'"'GET,POST,OPTIONS'"'"'","method.response.header.Access-Control-Allow-Origin":"'"'"'*'"'"'"}'
API GatewayがLambda関数を呼び出せるよう権限を付与します。
Windows (コマンドプロンプト) の場合
aws lambda add-permission --function-name handson-text-summarizer --statement-id apigateway-invoke --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:%REGION%:%ACCOUNT_ID%:%API_ID%/*"
macOS/Linux (ターミナル) の場合
aws lambda add-permission \
--function-name handson-text-summarizer \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:${REGION}:${ACCOUNT_ID}:${API_ID}/*"
Windows (コマンドプロンプト) の場合
aws apigateway create-deployment --rest-api-id %API_ID% --stage-name dev set API_URL=https://%API_ID%.execute-api.%REGION%.amazonaws.com/dev/summarize echo Endpoint: %API_URL%
macOS/Linux (ターミナル) の場合
aws apigateway create-deployment \
--rest-api-id $API_ID \
--stage-name dev
API_URL="https://${API_ID}.execute-api.${REGION}.amazonaws.com/dev/summarize"
echo "Endpoint: $API_URL"
Windows (コマンドプロンプト) の場合
curl %API_URL%
curl -X POST %API_URL% -H "Content-Type: application/json" -d "{\"text\": \"AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。\", \"max_sentences\": 2}"
macOS/Linux (ターミナル) の場合
curl $API_URL
curl -X POST $API_URL \
-H "Content-Type: application/json" \
-d '{"text": "AWSは包括的なクラウドプラットフォームです。200以上のサービスを提供しています。多くの企業がAWSを利用しています。", "max_sentences": 2}'
Windows (コマンドプロンプト) の場合
for /f "delims=" %i in ('aws apigateway create-usage-plan --name handson-basic-plan --throttle burstLimit^=5^,rateLimit^=10 --quota limit^=1000^,period^=DAY --api-stages apiId^=%API_ID%^,stage^=dev --query id --output text') do set PLAN_ID=%i
echo Usage Plan ID: %PLAN_ID%
macOS/Linux (ターミナル) の場合
# 使用量プランの作成
PLAN_ID=$(aws apigateway create-usage-plan \
--name handson-basic-plan \
--throttle burstLimit=5,rateLimit=10 \
--quota limit=1000,period=DAY \
--api-stages apiId=${API_ID},stage=dev \
--query id --output text)
echo "Usage Plan ID: $PLAN_ID"
Windows (コマンドプロンプト) の場合
for /f "delims=" %i in ('aws apigateway create-api-key --name handson-test-key --enabled --query id --output text') do set KEY_ID=%i
aws apigateway create-usage-plan-key --usage-plan-id %PLAN_ID% --key-id %KEY_ID% --key-type API_KEY
for /f "delims=" %i in ('aws apigateway get-api-key --api-key %KEY_ID% --include-value --query value --output text') do set API_KEY=%i
echo API Key: %API_KEY%
macOS/Linux (ターミナル) の場合
# APIキーの作成 KEY_ID=$(aws apigateway create-api-key \ --name handson-test-key \ --enabled \ --query id --output text) # APIキーを使用量プランに紐付け aws apigateway create-usage-plan-key \ --usage-plan-id $PLAN_ID \ --key-id $KEY_ID \ --key-type API_KEY # APIキーの値を取得 API_KEY=$(aws apigateway get-api-key --api-key $KEY_ID --include-value \ --query value --output text) echo "API Key: $API_KEY"
実行結果の例
Usage Plan ID: lmno9012
API Key: ABCDEFGHIJKLMNOPQRSTUVWXYZ123456
Windows (コマンドプロンプト) の場合
aws apigateway update-method --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method GET --patch-operations op=replace,path=/apiKeyRequired,value=true aws apigateway update-method --rest-api-id %API_ID% --resource-id %RESOURCE_ID% --http-method POST --patch-operations op=replace,path=/apiKeyRequired,value=true aws apigateway create-deployment --rest-api-id %API_ID% --stage-name dev
macOS/Linux (ターミナル) の場合
# GETメソッドにAPIキー必須を設定 aws apigateway update-method \ --rest-api-id $API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --patch-operations op=replace,path=/apiKeyRequired,value=true # POSTメソッドにAPIキー必須を設定 aws apigateway update-method \ --rest-api-id $API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --patch-operations op=replace,path=/apiKeyRequired,value=true # 再デプロイ aws apigateway create-deployment \ --rest-api-id $API_ID \ --stage-name dev
Windows (コマンドプロンプト) の場合
:: APIキーなし → 403 Forbidden
curl %API_URL%
:: APIキーあり → 200 OK
curl -H "x-api-key: %API_KEY%" %API_URL%
:: POSTリクエスト + APIキー
curl -X POST -H "x-api-key: %API_KEY%" -H "Content-Type: application/json" -d "{\"text\": \"テストです。要約します。\", \"max_sentences\": 1}" %API_URL%
macOS/Linux (ターミナル) の場合
# APIキーなし → 403 Forbidden
curl $API_URL
# APIキーあり → 200 OK
curl -H "x-api-key: $API_KEY" $API_URL
# POSTリクエスト + APIキー
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "テストです。要約します。", "max_sentences": 1}' \
$API_URL
AWS WAF(Web Application Firewall)は、Webアプリケーションを一般的な攻撃から保護するサービスです。
API Gatewayと連携することで、APIに対する不正アクセスを自動的にブロックできます。
まず、WAFのルールを定義したJSONファイルを作成します。
Kiro を使用してコマンドを実行しているディレクトリに waf-rules.json を作成し、以下の内容を保存してください。
[
{
"Name": "AWSManagedRulesCommonRuleSet",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSManagedRulesCommonRuleSet"
}
},
{
"Name": "AWSManagedRulesKnownBadInputsRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesKnownBadInputsRuleSet"
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSManagedRulesKnownBadInputsRuleSet"
}
},
{
"Name": "handson-rate-limit",
"Priority": 3,
"Statement": {
"RateBasedStatement": {
"Limit": 100,
"AggregateKeyType": "IP"
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "handson-rate-limit"
}
}
]
次に、Web ACLを作成します。
Windows (コマンドプロンプト) の場合
set API_GW_ARN=arn:aws:apigateway:%REGION%::/restapis/%API_ID%/stages/dev
:: Web ACLの作成
aws wafv2 create-web-acl ^
--name handson-api-waf ^
--scope REGIONAL ^
--region %REGION% ^
--default-action Allow={} ^
--description "ハンズオンAPI用WAF" ^
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=handson-api-waf ^
--rules file://waf-rules.json
:: ARNの取得
for /f "delims=" %i in ('aws wafv2 list-web-acls --scope REGIONAL --region %REGION% --query "WebACLs[?Name=='handson-api-waf'].ARN" --output text') do set WAF_ACL_ARN=%i
echo WAF ACL ARN: %WAF_ACL_ARN%
macOS/Linux (ターミナル) の場合
# API GatewayのARNを取得
API_GW_ARN="arn:aws:apigateway:${REGION}::/restapis/${API_ID}/stages/dev"
# Web ACLの作成
WAF_ACL_ARN=$(aws wafv2 create-web-acl \
--name handson-api-waf \
--scope REGIONAL \
--region $REGION \
--default-action Allow={} \
--description "ハンズオンAPI用WAF" \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=handson-api-waf \
--rules file://waf-rules.json \
--query Summary.ARN --output text)
echo "WAF ACL ARN: $WAF_ACL_ARN"
実行結果の例
WAF ACL ARN: arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/handson-api-waf/...
ルールグループ | 説明 |
AWSManagedRulesCommonRuleSet | 一般的なWebアプリケーション攻撃を防御 |
AWSManagedRulesKnownBadInputsRuleSet | 既知の悪意のある入力パターンを検出 |
handson-rate-limit | 5分あたり100リクエストを超えるとブロック |
Windows (コマンドプロンプト) の場合
aws wafv2 associate-web-acl --web-acl-arn %WAF_ACL_ARN% --resource-arn %API_GW_ARN% --region %REGION%
macOS/Linux (ターミナル) の場合
aws wafv2 associate-web-acl \ --web-acl-arn $WAF_ACL_ARN \ --resource-arn $API_GW_ARN \ --region $REGION
APIキーを使って正常にアクセスできることを確認します。
Windows (コマンドプロンプト) の場合
curl -H "x-api-key: %API_KEY%" %API_URL%
macOS/Linux (ターミナル) の場合
curl -H "x-api-key: $API_KEY" $API_URL
正常なレスポンスが返れば、WAFが正常なリクエストを通過させていることを確認できます。
WAFが悪意のあるリクエストをブロックすることを確認します。
Windows (コマンドプロンプト) の場合
curl -X POST -H "x-api-key: %API_KEY%" -H "Content-Type: application/json" -d "{\"text\": \"SELECT * FROM users; DROP TABLE users;--\"}" %API_URL%
macOS/Linux (ターミナル) の場合
# SQLインジェクション風のリクエスト → WAFがブロック
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "SELECT * FROM users; DROP TABLE users;--"}' \
$API_URL
WAFがブロックした場合、403 Forbidden が返ります。
短時間に大量のリクエストを送信して、レートリミットが機能することを確認します。
Windows (コマンドプロンプト) の場合
for /l %i in (1,1,120) do @curl -s -o nul -w "Request %i: %%{http_code}\n" -H "x-api-key: %API_KEY%" %API_URL%
macOS/Linux (ターミナル) の場合
# 連続リクエストでレートリミットをテスト
for i in $(seq 1 120); do
echo "Request $i: $(curl -s -o /dev/null -w '%{http_code}' \
-H "x-api-key: $API_KEY" \
$API_URL)"
done
100リクエストを超えた辺りから 403 レスポンスが返り始めれば、レートリミットが機能しています。
構成 | 認証 | レートリミット | 攻撃防御 | 運用負荷 |
GAS(単体) | なし | GAS制限のみ | なし | 低 |
GAS + スプレッドシート照合 | 簡易キー | なし | なし | 中 |
Lambda Function URL(単体) | なし | なし | なし | 低 |
Lambda + DynamoDB照合 | 簡易キー | なし | なし | 中 |
API Gateway + APIキー | APIキー | ✅ | なし | 中 |
API Gateway + WAF + Lambda | APIキー | ✅ | ✅ | 中〜高 |
ハンズオンが完了したら、以下のコマンドでリソースを削除して料金の発生を防ぎましょう。
Windowsのコマンドプロンプトでは変数の動的取得が複雑になるため、ここではマネジメントコンソールからの削除を推奨しますが、コマンドで削除する場合は以下のようになります。
Windows (コマンドプロンプト) の場合
:: 関連付けの解除
aws wafv2 disassociate-web-acl --resource-arn %API_GW_ARN% --region %REGION%
:: Web ACLの削除
for /f "delims=" %i in ('aws wafv2 list-web-acls --scope REGIONAL --region %REGION% --query "WebACLs[?Name=='handson-api-waf'].Id" --output text') do set WAF_ACL_ID=%i
for /f "delims=" %i in ('aws wafv2 get-web-acl --name handson-api-waf --scope REGIONAL --id %WAF_ACL_ID% --region %REGION% --query "LockToken" --output text') do set WAF_ACL_LOCK=%i
aws wafv2 delete-web-acl --name handson-api-waf --scope REGIONAL --id %WAF_ACL_ID% --lock-token %WAF_ACL_LOCK% --region %REGION%
macOS/Linux (ターミナル) の場合
# 関連付けの解除 aws wafv2 disassociate-web-acl --resource-arn $API_GW_ARN --region $REGION # Web ACLの削除 WAF_ACL_ID=$(aws wafv2 list-web-acls --scope REGIONAL --region $REGION \ --query "WebACLs[?Name=='handson-api-waf'].Id" --output text) WAF_ACL_LOCK=$(aws wafv2 get-web-acl --name handson-api-waf --scope REGIONAL \ --id $WAF_ACL_ID --region $REGION --query "LockToken" --output text) aws wafv2 delete-web-acl --name handson-api-waf --scope REGIONAL \ --id $WAF_ACL_ID --lock-token $WAF_ACL_LOCK --region $REGION
Windows (コマンドプロンプト) の場合
:: 変数が残っていない場合は再取得が必要です
for /f "delims=" %i in ('aws apigateway get-rest-apis --query "items[?name=='handson-text-summarizer-api'].id" --output text') do set API_ID=%i
for /f "delims=" %i in ('aws apigateway get-api-keys --query "items[?name=='handson-test-key'].id" --output text') do set KEY_ID=%i
for /f "delims=" %i in ('aws apigateway get-usage-plans --query "items[?name=='handson-basic-plan'].id" --output text') do set PLAN_ID=%i
:: 削除実行(REST API -> 使用量プラン -> APIキー の順)
aws apigateway delete-rest-api --rest-api-id %API_ID%
aws apigateway delete-usage-plan --usage-plan-id %PLAN_ID%
aws apigateway delete-api-key --api-key %KEY_ID%
macOS/Linux (ターミナル) の場合
API_ID=$(aws apigateway get-rest-apis \ --query "items[?name=='handson-text-summarizer-api'].id" --output text) KEY_ID=$(aws apigateway get-api-keys \ --query "items[?name=='handson-test-key'].id" --output text) PLAN_ID=$(aws apigateway get-usage-plans \ --query "items[?name=='handson-basic-plan'].id" --output text) # 削除実行(REST API -> 使用量プラン -> APIキー の順) aws apigateway delete-rest-api --rest-api-id $API_ID aws apigateway delete-usage-plan --usage-plan-id $PLAN_ID aws apigateway delete-api-key --api-key $KEY_ID
Windows環境の場合は、AWSマネジメントコンソールから手動で「WAF Web ACL」「API Gateway」を削除することをおすすめします(変数が維持されていれば、作成時と同様のコマンドで削除可能です)。
aws dynamodb delete-table --table-name handson-api-keys
aws lambda delete-function --function-name handson-text-summarizer
Windows (コマンドプロンプト) の場合
aws iam detach-role-policy --role-name handson-furl-lambda-role ^ --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess aws iam detach-role-policy --role-name handson-furl-lambda-role ^ --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess aws iam delete-role --role-name handson-furl-lambda-role
macOS/Linux (ターミナル) の場合
aws iam detach-role-policy --role-name handson-furl-lambda-role \ --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess aws iam detach-role-policy --role-name handson-furl-lambda-role \ --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess aws iam delete-role --role-name handson-furl-lambda-role
Windows (コマンドプロンプト) の場合
:: アクセキーの削除
for /f "delims=" %i in ('aws iam list-access-keys --user-name handson-user --query "AccessKeyMetadata[0].AccessKeyId" --output text') do set ACCESS_KEY_ID=%i
aws iam delete-access-key --user-name handson-user --access-key-id %ACCESS_KEY_ID%
:: ポリシーのデタッチとユーザー削除
aws iam detach-user-policy --user-name handson-user ^
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam delete-user --user-name handson-user
macOS/Linux (ターミナル) の場合
# アクセスキーの削除 ACCESS_KEY_ID=$(aws iam list-access-keys --user-name handson-user \ --query "AccessKeyMetadata[0].AccessKeyId" --output text) aws iam delete-access-key --user-name handson-user --access-key-id $ACCESS_KEY_ID # ポリシーのデタッチとユーザー削除 aws iam detach-user-policy --user-name handson-user \ --policy-arn arn:aws:iam::aws:policy/AdministratorAccess aws iam delete-user --user-name handson-user
Windows (コマンドプロンプト) の場合
cd gas\04 && clasp undeploy --all cd gas\06 && clasp undeploy --all
macOS/Linux (ターミナル) の場合
cd gas/04 && clasp undeploy --all cd gas/06 && clasp undeploy --all
Windows (コマンドプロンプト) の場合
rmdir /S /Q gas lambda
macOS/Linux (ターミナル) の場合
rm -rf gas lambda
このハンズオンでは、サーバレスAPIの構築と保護について段階的に学びました。
ステップ | 内容 |
GAS Webアプリ | 最も手軽なAPI公開方法を体験 |
Lambda Function URL | AWS上での同等構成を体験 |
GAS vs Lambda 比較 | 実行時間制限・コスト・制約の違い |
暫定セキュリティ対策 | スプレッドシート/DynamoDBでのキー照合 |
API Gateway | 本格的なAPI管理基盤 |
APIキー認証 | アクセス制御とレートリミット |
AWS WAF | Web攻撃からの保護 |
# | テーマ | 使用サービス |
09 | テキストの翻訳・音声化 | Lambda, Step Functions, S3, Translate, Polly |
10 | 静的Webサイトホスティング | CDK, CloudFront, S3, CloudWatch |
11 | 動的Webアプリ | Amplify |
12 | サーバレスAPIの構築と保護 | GAS, Lambda, API Gateway, WAF, DynamoDB |
[AWS Lambda Function URL ドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html)
[Amazon API Gateway ドキュメント](https://docs.aws.amazon.com/ja_jp/apigateway/)
[AWS WAF ドキュメント](https://docs.aws.amazon.com/ja_jp/waf/)
[Google Apps Script ドキュメント](https://developers.google.com/apps-script)