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 のログで中身を確認する。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲