コンテンツにスキップ

本番運用:フォールバック、テレメトリ、アーカイブ、保護

このページでは、パッケージが基本的なレンダー処理に加えて扱う 4 つの本番運用上の関心事、つまりローカルフォールバック、エッジテレメトリ、R2 アーカイブ、インバウンド API 保護レイヤーを取り上げます。各セクションは、検証済みのクラス動作に対応しています。

Worker に到達できず、fallbackToLocaltrue の場合、ブリッジはローカルレンダラーに委譲します。このローカルレンダラーは LocalRendererFactoryInterface を通じて提供されます。ブリッジはファクトリを遅延呼び出しするため、ファクトリの create() はフォールバック経路でのみ実行されます。

<?php
declare(strict_types=1);
use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;
use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface
{
public function __construct(
private readonly \NextPDF\Artisan\ChromeHtmlRenderer $chrome,
) {}
public function create(): LocalRendererInterface
{
return new readonly class($this->chrome) implements LocalRendererInterface {
public function __construct(
private \NextPDF\Artisan\ChromeHtmlRenderer $chrome,
) {}
/** @param array<string, mixed> $options */
public function render(string $html, array $options = []): string
{
// Delegate to the local Chrome renderer; return raw PDF bytes.
return $this->chrome->renderToString($html, $options);
}
};
}
}

ファクトリをレンダラーに配線します。

use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer(
config: $config,
httpClient: $httpClient,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
logger: $logger,
localRendererFactory: new ArtisanLocalRendererFactory($chrome),
responseFactory: $httpFactory,
);

フォールバックが実行されると、結果の renderLocation はリテラル文字列 local となり、heightPt0.0 になります。ローカル経路では、エッジロケーションも測定済みの高さも報告されません。ブリッジは、要求された幅を widthPt オプションキーを通じてローカルレンダラーに渡します。

以下は CloudflareHtmlRenderer から直接読み取った動作です。

状況結果
設定が不完全、fallbackToLocal: falseCloudflareNotAvailableException
設定が不完全、fallbackToLocal: true、ファクトリ配線済みローカルレンダー
Worker がトランスポートエラーをスロー、フォールバック有効、ファクトリ配線済みローカルレンダー、warning でログ記録後 info
Worker がスロー、フォールバック有効、Artisan インストール済み、ファクトリなし欠落しているファクトリを示す CloudflareNotAvailableException
Worker がスロー、フォールバック有効、Artisan がインストール欠落しているパッケージを示す CloudflareNotAvailableException
Worker が HTTP エラー/不正な本文を返すCloudflareRenderException決してフォールバックしない

最後の行が重要な分岐です。エラーで応答する Worker はレンダー失敗であり、到達可能性の失敗ではありません。これは再スローされるため、コード側ではレンダー失敗と到達不能なエッジを区別できます。

バイナリ経路で成功したすべてのレンダーには、レスポンスヘッダーから導出されたテレメトリが付随します。

$result = $renderer->render($html);
$logger->info('edge render', [
'edge' => $result->renderLocation, // e.g. 'TPE', 'NRT'
'render_time_ms' => $result->renderTimeMs,
'content_px' => $result->contentHeightPx,
'pdf_bytes' => $result->size(),
]);

レンダラーは CF-Ray レスポンスヘッダーから renderLocation を読み取り、最後のハイフン以降のセグメントを取得します。CF-Ray: 8abc123def456-TPE の場合、ロケーションは TPE です。ヘッダーが存在しない場合、ロケーションは空文字列になります。JSON レスポンス経路では、代わりに JSON の renderLocation フィールドから値を取得します。これらは Worker からの可観測性シグナルとして扱い、プラットフォームの保証としては扱わないでください。

R2ArchiveManager は、S3 互換 API を通じて PDF のバイト列を Cloudflare R2 にアップロードし、リクエストを AWS Signature V4 で署名します。

use NextPDF\Cloudflare\R2ArchiveConfig;
use NextPDF\Cloudflare\R2ArchiveManager;
$r2 = new R2ArchiveManager(
config: new R2ArchiveConfig(
bucketName: 'pdf-archive',
accountId: getenv('CF_ACCOUNT_ID') ?: '',
accessKeyId: getenv('R2_ACCESS_KEY_ID') ?: '',
secretAccessKey: getenv('R2_SECRET_ACCESS_KEY') ?: '',
pathPrefix: 'invoices/',
),
httpClient: $httpClient,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
);
$upload = $r2->upload($result->pdfData, 'invoice-2026-0042.pdf', [
'tenant' => 'acme',
]);
if (!$upload->success) {
$logger->error('r2 upload failed', ['error' => $upload->error]);
}

以下は、R2ArchiveManagerR2ObjectKey から検証された動作です。

  • オブジェクトキーは日付でパーティション分割されます。形式は <pathPrefix><Y>/<m>/<d>/<sanitized-filename> です。例えば invoices/2026/05/18/invoice-2026-0042.pdf のようになります。
  • ファイル名はサニタイズされます。basename() が適用されてパストラバーサルを除去し、続いてヌルバイトと制御文字(\x00\x1f\x7f)を除去します。結果が空になった場合は document.pdf になります。
  • カスタムメタデータは x-amz-meta-<lowercased-key> ヘッダーとして送信され、V4 の署名対象ヘッダーセットに含まれます。
  • maxFileSizeBytes(デフォルト 104857600)より大きいアップロードは、リクエスト送信前に拒否され、R2UploadResultsuccess: false で返します。
  • R2UploadResult::isValid() は、success、空でない key、空でない etag がそろっていることを必要とします。
$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);

generateSignedUrl() は、AWS Signature V4 でクエリ署名した GET URL を構築します。この URL には、呼び出し側が制御する X-Amz-Expires(デフォルト 3600 秒)が含まれます。正規リクエストでは、コンテンツハッシュのセンチネルとして UNSIGNED-PAYLOAD を使用します。クエリ署名された読み取り URL では、本文が署名対象リクエストの一部ではないため、この形式を使用します。これは、R2ArchiveManager から読み取った、パッケージ実装の署名動作を記述したものです。Amazon のサービスドキュメントは AWS Signature Version 4 を定義していますが、SDO 標準ではないため、ここでは規範的な条項をピン留めしていません。オブジェクトアクセスキーは #[SensitiveParameter] です。ログに残さないでください。

R2UploadResult::publicUrl($customDomain) は、ドメインが指定されていない場合は素のキーを返し、指定されている場合は https://<domain>/<key> を返します。指定されたドメインにスキームがない場合は、HTTPS スキームを強制します。これはプライベートバケットを公開状態にするものではありません。公開状態は R2 バケットの構成で決まります。

ApiProtection は、Worker の手前にある PHP ゲートウェイに到着するレンダーリクエストに適用するレイヤーです。固定された順序で 3 つのチェックを実行します。API キー、ペイロードサイズ、レート制限の順です。

use NextPDF\Cloudflare\ApiKeyValidator;
use NextPDF\Cloudflare\ApiProtection;
use NextPDF\Cloudflare\ApiProtectionConfig;
$protection = new ApiProtection(
config: new ApiProtectionConfig(
maxRequestsPerMinute: 30,
maxRequestsPerHour: 500,
maxPayloadSizeBytes: 5_000_000,
requireApiKey: true,
),
keyValidator: new ApiKeyValidator([getenv('GATEWAY_API_KEY') ?: '']),
);
$decision = $protection->checkRequest(
clientId: $clientIp,
payloadSize: strlen($requestBody),
apiKey: $request->getHeaderLine('X-Api-Key'),
);
if (!$decision->allowed) {
http_response_code(429);
foreach ($decision->toHeaders() as $name => $value) {
header("{$name}: {$value}");
}
echo $decision->denialReason;
exit;
}

検証済みの動作は次のとおりです。

  • 順序は API キー → ペイロードサイズ → レート制限です。最初に失敗したチェックで処理を短絡し、固有の denialReason を返します。
  • ApiKeyValidator::validate() は、タイミングセーフな比較のために hash_equals() を使用し、空のキーを拒否します。validateHashed() は、保管用の SHA-256 ハッシュと比較します。キーのパラメータには #[SensitiveParameter] が付与されています。
  • レート制限ストアはプロセスごとのインメモリです。1 分単位のウィンドウ(rateLimitWindowSeconds、デフォルト 60)と 1 時間単位のウィンドウ(固定 3600 秒)を追跡します。ワーカーや再起動をまたいで永続化されることはありません。プロセス間で共有される制限が必要な場合は、共有ストアを前段に配置してください。
  • ApiProtectionResult::toHeaders() は常に X-Content-Type-Options: nosniffX-Frame-Options: DENY を追加し、レート制限ヘッダー(X-RateLimit-RemainingX-RateLimit-Reset、拒否時には加えて Retry-After)をマージします。

このブリッジは PDF に署名しません。本番の署名パイプラインでは、エッジでレンダーした後、返されたバイト列をエンジンで署名します。

  1. render()CloudflareRenderResult::$pdfData
  2. $pdfDatanextpdf/core(または NextPDF Pro、PAdES B-B 署名用)に渡します。長期検証プロファイルは Enterprise の機能です。このコアブリッジは、それらの機能を主張しません。

署名鍵がエッジ境界を越えないように、署名ステップは自身のプロセス内に保持してください。

  • /integrations/cloudflare/security-and-operations/ — ピン留め、SSRF 防御、シークレットローテーション、運用ランブック。
  • /integrations/cloudflare/troubleshooting/ — 障害モードのカタログ。
  • /integrations/cloudflare/configuration/ — すべてのフィールドとデフォルト。