2026年5月28日
2026年5月28日
AWS Lambda + API Gatewayの設定方法|REST APIとAPIキーでWordPressの公開通知を受けるWebhookを作る
WordPress で記事を公開したときに、キャッシュの削除や外部サービスへの連携を Lambda で動かしたい場合は、API Gateway の REST API で受け口の URL を作り、Lambda と「Lambda プロキシ統合」でつなぎます。この記事では、WordPress から送る公開通知(Webhook)を例に、REST API の作成、API キーと使用量プランによる流量制限、署名による送信元の確認、WordPress 側の送信コード、エラーコード別のつまずきまでを説明します。手順を終えると、正しい署名が付いたリクエストだけを Lambda が受け付ける構成ができます。
API Gateway の種類ごとの機能差、上限値、コンソールの表記は更新されることがあります。ここでは AWS の公開ドキュメント(2026年10月現在)に沿って説明しますが、要件に関わる機能は最新のドキュメントでも確認してください。コマンドとコードは例なので、名前やリージョンは自分の環境に合わせて書き換え、まずステージング環境で試します。
この構成でREST APIを選ぶ理由
API Gateway には HTTP API と REST API があります。単純に Lambda を呼ぶだけなら HTTP API のほうが構成は簡単ですが、今回は次の理由で REST API を使います。
- API キーと使用量プラン:呼び出し元ごとに、1秒あたりのリクエスト数や1日の上限を設定できます。万一 URL が漏れても、Lambda が大量に呼ばれ続ける事態を抑えられます。この機能は REST API 側にあります。
- AWS WAF を直接付けられる:IP アドレスでの制限などを API の手前で行いたいときに使えます。
ただし、API キーは利用量を管理するためのもので、認証の代わりにはなりません。キーはリクエストのヘッダーにそのまま載るため、漏れれば誰でも使えます。そこでこの記事では、API キーに加えて、WordPress と Lambda だけが知っている秘密の値で本文に署名し、Lambda 側で検証します。
手順1:Webhookを受けるLambda関数を作る
Lambda プロキシ統合では、HTTP リクエストの内容がそのまま event に入って関数へ渡されます。本文は event["body"] に文字列で入り、ヘッダーは event["headers"] に入ります。REST API ではヘッダー名の大文字・小文字が送られたままで届くことがあるため、小文字にそろえてから読みます。
# lambda_function.py(関数名: wp-publish-hook / ランタイム: Python)
import base64
import hashlib
import hmac
import json
import os
import time
SECRET = os.environ["WEBHOOK_SECRET"].encode("utf-8")
MAX_SKEW = 300 # 署名の有効期間(秒)。これより古いリクエストは拒否する
def resp(status, payload):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(payload),
}
def lambda_handler(event, context):
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
body = event.get("body") or ""
if event.get("isBase64Encoded"):
body = base64.b64decode(body).decode("utf-8")
ts = headers.get("x-timestamp", "")
sig = headers.get("x-signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > MAX_SKEW:
return resp(401, {"error": "invalid timestamp"})
expected = hmac.new(SECRET, f"{ts}.{body}".encode("utf-8"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return resp(401, {"error": "invalid signature"})
try:
data = json.loads(body)
except json.JSONDecodeError:
return resp(400, {"error": "invalid json"})
if not isinstance(data, dict) or data.get("event") != "post_published":
return resp(400, {"error": "unsupported event"})
# ここで CloudFront のキャッシュ削除や SQS への投入などを行う
print(json.dumps({"event": data["event"], "post_id": data.get("post_id")}))
return resp(202, {"status": "accepted"})
ポイントは3つあります。署名は本文を加工する前の文字列に対して計算すること、比較には hmac.compare_digest を使うこと、時刻を署名に含めて古いリクエストの使い回しを防ぐことです。秘密の値 WEBHOOK_SECRET は環境変数で渡していますが、より厳しく管理したいなら AWS Secrets Manager に置いて関数から読み出します。どちらの場合も、コードやリポジトリには書きません。
戻り値は statusCode・headers・body を持つ辞書で、body は文字列にします。ここを辞書のまま返すなど形が崩れていると、API Gateway は 502 を返します。
手順2:REST APIを作り、Lambdaとつなぐ
コンソールでも作れますが、手順を再現しやすいように CLI で示します。/publish というリソースに POST メソッドを作り、API キーを必須にして Lambda プロキシ統合でつなぎます。
REGION=ap-northeast-1
ACCOUNT_ID=123456789012
FUNC_ARN="arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:wp-publish-hook"
API_ID=$(aws apigateway create-rest-api --name wp-publish-hook-api \
--endpoint-configuration types=REGIONAL --query id --output text)
ROOT_ID=$(aws apigateway get-resources --rest-api-id "$API_ID" \
--query 'items[?path==`/`].id' --output text)
RES_ID=$(aws apigateway create-resource --rest-api-id "$API_ID" \
--parent-id "$ROOT_ID" --path-part publish --query id --output text)
# POST /publish(API キー必須)
aws apigateway put-method --rest-api-id "$API_ID" --resource-id "$RES_ID" \
--http-method POST --authorization-type NONE --api-key-required
# Lambda プロキシ統合(Lambda の呼び出し自体は常に POST)
aws apigateway put-integration --rest-api-id "$API_ID" --resource-id "$RES_ID" \
--http-method POST --type AWS_PROXY --integration-http-method POST \
--uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/${FUNC_ARN}/invocations"
# API Gateway に、この API の POST /publish からの呼び出しだけを許可する
aws lambda add-permission --function-name wp-publish-hook \
--statement-id apigw-post-publish --action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:${REGION}:${ACCOUNT_ID}:${API_ID}/*/POST/publish"
# デプロイしてステージ prod を作る
aws apigateway create-deployment --rest-api-id "$API_ID" --stage-name prod
REST API は、設定を変えただけでは公開中の URL に反映されません。変更のたびにデプロイ が必要です。ここを忘れて「設定したのに動かない」となる例が多いので、変更後は create-deployment を実行する習慣をつけてください。
呼び出し URL は https://{API_ID}.execute-api.ap-northeast-1.amazonaws.com/prod/publish の形になります。URL にステージ名(prod)が入る点も、つまずきやすいところです。
手順3:APIキーと使用量プランを設定する
API キーは、使用量プランを通してステージに結び付けて初めて有効になります。次の例は、1秒あたり5リクエスト(瞬間的には10まで)、1日1万リクエストを上限にするものです。
KEY_ID=$(aws apigateway create-api-key --name wp-site --enabled --query id --output text)
PLAN_ID=$(aws apigateway create-usage-plan --name wp-site-plan \
--api-stages "apiId=${API_ID},stage=prod" \
--throttle burstLimit=10,rateLimit=5 \
--quota limit=10000,period=DAY \
--query id --output text)
aws apigateway create-usage-plan-key --usage-plan-id "$PLAN_ID" \
--key-id "$KEY_ID" --key-type API_KEY
# WordPress に設定するキーの値を表示する(画面共有中などは注意)
aws apigateway get-api-key --api-key "$KEY_ID" --include-value --query value --output text
上限値は、記事の公開頻度に対して十分に余裕のある値にします。WordPress からの通知は多くても1分に数回程度のはずなので、上限に達するようなら何かがおかしいと判断できます。
手順4:WordPressから公開通知を送る
WordPress 側は mu-plugin(wp-content/mu-plugins/ に置く PHP ファイル)で作ると、テーマの変更やプラグインの無効化で通知が止まる心配がありません。接続先と秘密の値は wp-config.php の定数に置きます。
// wp-config.php(値はダミー。実際の値に置き換え、リポジトリには含めない)
define( 'LSC_HOOK_ENDPOINT', 'https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/publish' );
define( 'LSC_HOOK_API_KEY', 'your-api-key' );
define( 'LSC_HOOK_SECRET', 'your-webhook-secret' );
<?php
/**
* Plugin Name: Publish Notify to Lambda
* Description: 投稿の公開時に API Gateway 経由で Lambda へ通知する。
*/
defined( 'ABSPATH' ) || exit;
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
// 新たに公開された投稿だけを対象にする(更新のたびには送らない)
if ( 'publish' !== $new_status || 'publish' === $old_status ) {
return;
}
if ( 'post' !== $post->post_type || wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
return;
}
// 保存処理を遅くしないよう、送信は WP-Cron に回す
wp_schedule_single_event( time(), 'lsc_send_publish_hook', array( $post->ID ) );
}, 10, 3 );
add_action( 'lsc_send_publish_hook', function ( $post_id ) {
if ( ! defined( 'LSC_HOOK_ENDPOINT' ) || ! defined( 'LSC_HOOK_API_KEY' ) || ! defined( 'LSC_HOOK_SECRET' ) ) {
return;
}
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
return;
}
$body = wp_json_encode( array(
'event' => 'post_published',
'post_id' => $post->ID,
'url' => get_permalink( $post ),
'title' => get_post_field( 'post_title', $post, 'raw' ),
) );
$timestamp = (string) time();
$signature = hash_hmac( 'sha256', $timestamp . '.' . $body, LSC_HOOK_SECRET );
$response = wp_remote_post( LSC_HOOK_ENDPOINT, array(
'timeout' => 10,
'headers' => array(
'Content-Type' => 'application/json',
'x-api-key' => LSC_HOOK_API_KEY,
'X-Timestamp' => $timestamp,
'X-Signature' => $signature,
),
'body' => $body,
) );
if ( is_wp_error( $response ) ) {
error_log( 'Publish hook failed: ' . $response->get_error_message() );
return;
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( 202 !== $code ) {
error_log( sprintf( 'Publish hook returned HTTP %d for post %d', $code, $post_id ) );
}
} );
ログには HTTP ステータスと投稿 ID だけを残し、API キーや秘密の値、署名は出力しないようにしています。WP-Cron はアクセスがあったときに動く仕組みなので、アクセスの少ないサイトでは送信が遅れることがあります。即時性が必要なら、サーバーの cron から WP-Cron を定期実行する設定にしてください。
動作確認とエラーコード別のつまずき
まず curl で、署名なしのリクエストが拒否されることを確かめます。
curl -i -X POST "https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/publish" \
-H "Content-Type: application/json" -H "x-api-key: your-api-key" \
-d '{"event":"post_published","post_id":1}'
正しく設定できていれば、署名が無いので 401 が返ります。続いてステージングの WordPress で記事を公開し、Lambda の CloudWatch Logs(ロググループ /aws/lambda/wp-publish-hook)に投稿 ID が出力されるかを確認します。
うまくいかないときは、返ってきたステータスコードで原因を絞ります。
- **403 と
Missing Authentication Token**:その URL にメソッドが存在しないという意味です。パスやステージ名の誤り、POST 以外での呼び出し、デプロイ忘れを確認します。 - **403 と
Forbidden**:API キーが付いていないか、キーが使用量プランでこのステージに結び付いていません。 - 429:使用量プランのスロットリングか、1日の上限に達しています。通知が繰り返し送られていないか、WordPress 側を確認します。
- 401(Lambda が返したもの):署名か時刻の不一致です。サーバーの時計がずれていないか、WordPress と Lambda の秘密の値が同じか、本文を途中で加工していないかを確認します。
- 502:Lambda が例外で止まったか、戻り値の形が正しくありません。Lambda のログでエラー内容を確認します。
- 504:処理が API Gateway の統合タイムアウトを超えています。既定では約29秒です。重い処理は SQS に入れるなどして非同期にし、Lambda はすぐ 202 を返す形にします。
API Gateway 自体の実行ログを見たい場合は、アカウント単位で CloudWatch Logs 用の IAM ロールを設定したうえで、ステージのログ出力を有効にします。
まとめ
- API キーと使用量プランで流量を制限したいなら REST API を使う。API キーは認証の代わりにはならないので、署名で送信元を確認する。
- Lambda プロキシ統合では、戻り値の
bodyを文字列にする。形が崩れると 502 になる。 - REST API は変更のたびにデプロイが必要で、URL にはステージ名が入る。
- WordPress 側は mu-plugin で、新規公開のときだけ WP-Cron 経由で送る。秘密の値は
wp-config.phpの定数に置き、ログに出さない。 - エラーはステータスコードと本文のメッセージで切り分け、Lambda のログで中身を確認する。