<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="https://codedchords.dev/feed_style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <tabi:metadata xmlns:tabi="https://github.com/welpo/tabi">
        <tabi:base_url>https:&#x2F;&#x2F;codedchords.dev</tabi:base_url>
        <tabi:separator>
            •
        </tabi:separator>
        <tabi:about_feeds>This is a web feed, also known as an Atom feed. Subscribe by copying the URL from the address bar into your newsreader. Visit About Feeds to learn more and get started. It&#x27;s free.</tabi:about_feeds>
        <tabi:visit_the_site>Visit website</tabi:visit_the_site>
        <tabi:recent_posts>Recent posts</tabi:recent_posts>
        <tabi:last_updated_on>Updated on $DATE</tabi:last_updated_on>
        <tabi:default_theme></tabi:default_theme>
        <tabi:post_listing_date>date</tabi:post_listing_date>
        <tabi:current_section>Cloudflare</tabi:current_section>
    </tabi:metadata><title>Coded Chords - Cloudflare</title>
        <subtitle>A personal blog</subtitle>
    <link href="https://codedchords.dev/tags/cloudflare/atom.xml" rel="self" type="application/atom+xml"/>
    <link href="https://codedchords.dev/tags/cloudflare/" rel="alternate" type="text/html"/>
    <generator uri="https://www.getzola.org/">Zola</generator><updated>2026-05-22T00:00:00+00:00</updated><id>https://codedchords.dev/tags/cloudflare/atom.xml</id><entry xml:lang="en">
        <title>Amazon S3からCloudflare R2への移行</title>
        <published>2026-05-22T00:00:00+00:00</published>
        <updated>2026-05-22T00:00:00+00:00</updated>
        <author>
            <name>Toshiyuki Yoshida</name>
        </author>
        <link rel="alternate" href="https://codedchords.dev/blog/2026/05/cloudflare-r2/" type="text/html"/>
        <id>https://codedchords.dev/blog/2026/05/cloudflare-r2/</id>
        
            <content type="html">&lt;!-- textlint-disable --&gt;
&lt;img
  src=&quot;https:&amp;#x2F;&amp;#x2F;codedchords.dev&amp;#x2F;blog&amp;#x2F;2026&amp;#x2F;05&amp;#x2F;cloudflare-r2&amp;#x2F;cover.webp?h=1e92776e8f350ad0d4c1&quot;
  alt=&quot;Cover&quot; width=&quot;1536&quot; height=&quot;864&quot; loading=&quot;lazy&quot;
&#x2F;&gt;&lt;!-- textlint-enable --&gt;
&lt;p&gt;以前「&lt;a href=&quot;https:&#x2F;&#x2F;codedchords.dev&#x2F;blog&#x2F;2026&#x2F;01&#x2F;blog-to-zola-aws-cleanup&#x2F;&quot;&gt;ブログをZolaに移行、AWSの後始末&lt;&#x2F;a&gt;」で、Next.js + Amplify構成のブログをZolaへ移し、Amplifyを含む大半のAWSリソースを削除しました。ただ、そのとき1つだけ削除せずに残したものがあります。有償のWEBフォントを配信するためのS3バケットとCloudFrontです。&lt;&#x2F;p&gt;
&lt;p&gt;本記事では、この最後のAWSリソースをCloudflare R2へ移した記録をまとめます。題材はフォント1つですが、個人ブログのスタティックなリソースをS3とR2のどちらに置くか、という選択の話でもあります。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;移行前の構成&quot;&gt;移行前の構成&lt;&#x2F;h2&gt;
&lt;p&gt;配信していたのは有償のWEBフォントです。このフォントはライセンス上、誰でもwoff2ファイルをダウンロードできる状態にしておくわけにはいきません。一方でWEBフォントである以上、ブラウザからは取得できる必要があります。この相反する要件を、移行前はAWS側で次のように満たしていました&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-oac-1&quot;&gt;&lt;a href=&quot;#fn-oac&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;woff2ファイルをS3バケットに保管し、その前段のCloudFrontから配信&lt;&#x2F;li&gt;
&lt;li&gt;CloudFront FunctionでリクエストのRefererヘッダを検査し、ブログのドメイン以外からのアクセスを遮断&lt;&#x2F;li&gt;
&lt;li&gt;S3バケットポリシーにも同じReferer条件を設定し、CloudFrontを経由しないS3への直接アクセスも遮断&lt;&#x2F;li&gt;
&lt;li&gt;フォントがCloudFrontのドメイン(ブログとは別オリジン)から読み込まれるため、S3側のCORS設定でブログのドメインからの要求のみを許可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;ブログのCSSでは、すべての &lt;code&gt;@font-face&lt;&#x2F;code&gt; がこのCloudFrontのURLを参照していました。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;Cloudflare_R2_への移行&quot;&gt;Cloudflare R2 への移行&lt;&#x2F;h2&gt;
&lt;p&gt;ブログのワークロードは、すでにCloudflareに移っていました。ホスティングはCloudflare Workersの静的アセット配信で動いており、フォントだけをAWSに残しておく理由は薄くなっていました。配信元をS3 + CloudFrontからCloudflare R2に移すと、次のメリットがあります。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;AWSとCloudflareに分かれていたインフラの一本化&lt;&#x2F;li&gt;
&lt;li&gt;Referer制限のための仕組み(CloudFront Function・バケットポリシーの条件・CORS)の全廃&lt;&#x2F;li&gt;
&lt;li&gt;フォントの同一オリジン配信化(別オリジンへの &lt;code&gt;preconnect&lt;&#x2F;code&gt; やCSP調整が不要)&lt;&#x2F;li&gt;
&lt;li&gt;R2のストレージ無料枠(10GB)に収まる配信コスト&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;特に大きいのは2番目です。R2バケットは、公開設定を有効にしない限り外部から到達できるURLを持たず、フォントへのアクセス手段はWorkerからのR2 Bindingだけになります。S3 + CloudFrontでは、公開バケットへのアクセスをReferer条件で絞り込む必要がありました。R2では公開URLがそもそも存在しないため、アクセス元を判定する仕組み自体が要らなくなります。守るための要素が減ることは、設定ミスの余地が減ることでもあります。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;移行後の保護モデル&quot;&gt;移行後の保護モデル&lt;&#x2F;h2&gt;
&lt;p&gt;新しい構成では、フォントの保護を4つのレイヤーで担保しています。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;レイヤー&lt;&#x2F;th&gt;&lt;th&gt;設定の実体&lt;&#x2F;th&gt;&lt;th&gt;防ぐ脅威&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;バケット非公開&lt;&#x2F;td&gt;&lt;td&gt;r2.dev 無効・カスタムドメイン未割当・CORS なし&lt;&#x2F;td&gt;&lt;td&gt;バケット URL の直接スクレイプ、検索エンジンの索引&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;R2 Binding 経由のみ&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;[[r2_buckets]]&lt;&#x2F;code&gt; 宣言により同一 Cloudflare アカウント内に限定&lt;&#x2F;td&gt;&lt;td&gt;他アカウントや外部ネットワークからの参照&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;Worker が公開面を制限&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;fonts&#x2F;&amp;lt;file&amp;gt;&lt;&#x2F;code&gt; パスのみを公開し、不正なキーは弾く&lt;&#x2F;td&gt;&lt;td&gt;パストラバーサル、任意キーの READ、バケット列挙&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;長寿命クレデンシャル不在&lt;&#x2F;td&gt;&lt;td&gt;R2 API トークンを発行しない&lt;&#x2F;td&gt;&lt;td&gt;コミットや CI ログ経由のトークン漏洩&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;設計の本質は、ライセンス違反が最も起きやすい経路、つまり公開バケットのURLがクローラに拾われて検索結果に載ることを消す点にあります。閲覧者がDevToolsからフォントを保存する行為や、第三者サイトからのホットリンクは別レイヤーの問題で、WEBフォント配信の仕組み上、完全には防げません。必要ならWorkerにReferer許可リストを足すこともできますが、今回は入れていません。私が利用しているフォントのライセンスでは、ライセンシーのドメインから配信することが求められていて、リクエスト元の検査までは要件になっていないためです。ただし、これはフォントごとに異なります。CORSやドメイン制限を技術的保護措置としてEULAで義務づけているフォントもあるので、利用するフォントのライセンスは必ず確認してください。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;Worker_と_R2_の構成&quot;&gt;Worker と R2 の構成&lt;&#x2F;h2&gt;
&lt;p&gt;実装は驚くほど小さく収まりました。まずR2バケットを作成します(ここでは &lt;code&gt;web-fonts&lt;&#x2F;code&gt; という名前にします)。公開設定は無効のままにし、S3と同じキー名でwoff2をアップロードします。あとはWorkerからbindingでつなぐだけです。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;wrangler.toml&lt;&#x2F;code&gt; にR2バケットのbindingを宣言します。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;toml&quot; data-name=&quot;wrangler.toml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;name&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;blog&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;main&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;src&#x2F;index.js&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;compatibility_date&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;2026-02-17&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;assets&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;directory&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;.&#x2F;public&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;binding&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;ASSETS&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;[[&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;r2_buckets&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;binding&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;FONTS&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;bucket_name&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;web-fonts&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Worker本体は、&lt;code&gt;&#x2F;fonts&#x2F;&lt;&#x2F;code&gt; で始まるリクエストをR2に取り次ぎ、それ以外は静的アセットに渡すだけです。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;javascript&quot; data-name=&quot;src&#x2F;index.js&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-js&quot;&gt; FONTS_PREFIX&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;&#x2F;fonts&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;export default&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-modifier&quot;&gt;  async&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; fetch&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt;req&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;    const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-js&quot;&gt; url&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-new&quot;&gt; new&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; URL&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(req&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-js z-source&quot;&gt;url)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (url&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;pathname&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;startsWith&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-variable z-other z-constant z-js&quot;&gt;(FONTS_PREFIX))&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;      const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-js&quot;&gt; key&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; decodeURIComponent&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(url&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;pathname&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;slice&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;FONTS_PREFIX&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;length))&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;      &#x2F;&#x2F; Only flat keys in R2 (no subdir, no traversal). Anything else falls&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;      &#x2F;&#x2F; through to static assets — the theme also publishes under &#x2F;fonts&#x2F;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;      if&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt;key&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; &amp;amp;&amp;amp; !&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;key&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;..&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; &amp;amp;&amp;amp; !&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;key&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;))&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;        const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-js&quot;&gt; obj&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; await&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;FONTS&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;get&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt;key&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        if&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt;obj&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;          const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-js&quot;&gt; headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-new&quot;&gt; new&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; Headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;()&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;          obj&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;writeHttpMetadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt;headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;          headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;set&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;Content-Type&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;font&#x2F;woff2&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;          headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;set&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;Cache-Control&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;public, max-age=31536000, immutable&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;          headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;set&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;ETag&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; obj&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-js z-source&quot;&gt;httpEtag)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;          return&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-new&quot;&gt; new&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; Response&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(obj&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-js&quot;&gt;body&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;, {&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt; headers&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; }&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;ASSETS&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;fetch&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite z-js&quot;&gt;req&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後にCSSの参照先を書き換えます。&lt;code&gt;@font-face&lt;&#x2F;code&gt; の &lt;code&gt;src&lt;&#x2F;code&gt; は、これまでCloudFrontの絶対URLを指していましたが、同一オリジンの相対パス &lt;code&gt;&#x2F;fonts&#x2F;&lt;&#x2F;code&gt; に変わります。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;css&quot; data-name=&quot;custom.css&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-keyword z-keyword&quot;&gt;@font-face&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-type z-property-name z-css&quot;&gt;  font-family&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;WebFont&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-type z-property-name z-css&quot;&gt;  src&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function&quot;&gt; url&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;&#x2F;fonts&#x2F;WebFont-Regular.woff2&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-function z-misc&quot;&gt;    format&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;woff2&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-type z-property-name z-css&quot;&gt;  font-display&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; swap&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-type z-property-name z-css&quot;&gt;  font-weight&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 400&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support z-type z-property-name z-css&quot;&gt;  font-style&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; normal&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CSS内のすべての &lt;code&gt;@font-face&lt;&#x2F;code&gt; で、CloudFrontの絶対URLを &lt;code&gt;&#x2F;fonts&#x2F;&lt;&#x2F;code&gt; で始まる相対パスに置き換えます。デプロイはmainへのpushでGitHub Actionsが &lt;code&gt;wrangler deploy&lt;&#x2F;code&gt; を実行する流れに乗せます。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;つまずいた点&quot;&gt;つまずいた点&lt;&#x2F;h2&gt;
&lt;p&gt;デプロイ用のAPIトークンの権限不足でつまずきました。GitHub Actionsが使う &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;&#x2F;code&gt; は旧構成(静的アセット配信のみ)のときに発行したもので、Workers Scriptsの編集権限しかありませんでした。R2 bindingを含むWorkerを &lt;code&gt;wrangler deploy&lt;&#x2F;code&gt; すると、宣言したバケットの存在確認でR2のAPIを呼び出すため、初回のpushは &lt;code&gt;code: 10000 Authentication error&lt;&#x2F;code&gt; で失敗しました。&lt;&#x2F;p&gt;
&lt;p&gt;幸い、Cloudflare Workersのデプロイは原子的です。失敗時は旧Workerが稼働し続けるため、復旧作業中もサイトは旧CloudFront経由でフォントを配信し続け、見た目は保たれていました。&lt;&#x2F;p&gt;
&lt;p&gt;対応として、APIトークンにAccountスコープの &lt;code&gt;Workers R2 Storage: Edit&lt;&#x2F;code&gt; を追加しました。最終的に付与した権限セットは次のとおりです。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;スコープ&lt;&#x2F;th&gt;&lt;th&gt;パーミッション&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Account&lt;&#x2F;td&gt;&lt;td&gt;Workers Scripts: Edit&lt;&#x2F;td&gt;&lt;td&gt;Worker スクリプトのデプロイ&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Account&lt;&#x2F;td&gt;&lt;td&gt;Workers R2 Storage: Edit&lt;&#x2F;td&gt;&lt;td&gt;R2 binding 宣言時のバケット存在確認&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Account&lt;&#x2F;td&gt;&lt;td&gt;Account Settings: Read&lt;&#x2F;td&gt;&lt;td&gt;アカウント情報の照会&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Zone&lt;&#x2F;td&gt;&lt;td&gt;Workers Routes: Edit&lt;&#x2F;td&gt;&lt;td&gt;カスタムルートの設定&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;User&lt;&#x2F;td&gt;&lt;td&gt;Memberships: Read&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;memberships&lt;&#x2F;code&gt; API の呼び出し&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;User&lt;&#x2F;td&gt;&lt;td&gt;User Details: Read&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wrangler whoami&lt;&#x2F;code&gt; 相当の照会&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Cloudflare公式の「Edit Cloudflare Workers」テンプレートにはR2の権限が含まれていません。新しいbinding(R2 &#x2F; D1 &#x2F; KV &#x2F; Queuesなど)をWorkerに足すときは、デプロイ前にCIトークンの権限を見直すのが安全です。トークンを差し替えたあとは、失敗したジョブだけを再実行しました。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;DEPLOY_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;gh&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; run list --branch main&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --workflow=&amp;quot;&amp;lt;デプロイ用ワークフロー名&amp;gt;&amp;quot; --limit&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --json databaseId --jq &amp;#39;.[0].databaseId&amp;#39;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;gh&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; run rerun &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DEPLOY_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot; --failed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;AWS_リソースの撤去&quot;&gt;AWS リソースの撤去&lt;&#x2F;h2&gt;
&lt;p&gt;R2配信が本番で安定したことを確認してから、AWS側を停止しました。撤去対象はCloudFrontディストリビューション、CloudFront Function、S3バケットの3つです。ディストリビューションIDは &lt;code&gt;aws cloudfront list-distributions&lt;&#x2F;code&gt;、バケット名は &lt;code&gt;aws s3 ls&lt;&#x2F;code&gt; で確認できます。以降のコマンドでは、それぞれの識別子を変数に入れてあるものとします。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;&amp;lt;対象ディストリビューションのID&amp;gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;FUNCTION_NAME&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;&amp;lt;Referer を検査していた CloudFront Function 名&amp;gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;BUCKET&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;&amp;lt;フォントを置いていた S3 バケット名&amp;gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CloudFrontディストリビューションは、いきなり削除できません。先に &lt;code&gt;Enabled = false&lt;&#x2F;code&gt; で無効化し、設定が全エッジに伝播するのを待ってから削除します。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront get-distribution-config --id &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;  &amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; &#x2F;tmp&#x2F;dist-config.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;jq&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; -r &amp;#39;.ETag&amp;#39; &#x2F;tmp&#x2F;dist-config.json&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; &#x2F;tmp&#x2F;dist-etag.txt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;jq&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;#39;.DistributionConfig | .Enabled = false&amp;#39; &#x2F;tmp&#x2F;dist-config.json&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;  &amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; &#x2F;tmp&#x2F;dist-config-disabled.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront update-distribution --id &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --if-match &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; &#x2F;tmp&#x2F;dist-etag.txt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --distribution-config &amp;quot;file:&#x2F;&#x2F;&#x2F;tmp&#x2F;dist-config-disabled.json&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;update-distribution&lt;&#x2F;code&gt; は &lt;code&gt;Status: InProgress&lt;&#x2F;code&gt; を返します。&lt;code&gt;Status: Deployed&lt;&#x2F;code&gt; に戻るまで伝播を待ちます(実測で約3分)。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;until&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; [&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront get-distribution --id &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --query &amp;#39;Distribution.Status&amp;#39; --output text&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;Deployed&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; ];&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; do&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;  sleep&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 30&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;done&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Deployed&lt;&#x2F;code&gt; に戻ったら削除します。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;ETAG&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront get-distribution --id &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --query &amp;#39;ETag&amp;#39; --output text&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront delete-distribution --id &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$DIST_ID&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot; --if-match &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$ETAG&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CloudFront Functionの削除では、&lt;code&gt;describe-function&lt;&#x2F;code&gt; で取得した現在のETagを &lt;code&gt;delete-function&lt;&#x2F;code&gt; に渡します。注意点として、FunctionはDEVELOPMENTとLIVEの2つのステージを持ち、両者を別々に更新しているとETagが食い違います。私の環境ではDEVELOPMENT側が新しく、&lt;code&gt;describe-function --stage DEVELOPMENT&lt;&#x2F;code&gt; で取ったETagでなければ &lt;code&gt;PreconditionFailed&lt;&#x2F;code&gt; になりました。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;ETAG&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront describe-function --name &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$FUNCTION_NAME&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt;  --stage DEVELOPMENT --query &amp;#39;ETag&amp;#39; --output text&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; cloudfront delete-function --name &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$FUNCTION_NAME&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot; --if-match &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$ETAG&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後にS3バケットを空にして削除します。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; s3 rm &amp;quot;s3:&#x2F;&#x2F;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$BUCKET&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot; --recursive&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;aws&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-punctuation z-definition z-string&quot;&gt; s3api delete-bucket --bucket &amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-normal z-shell&quot;&gt;$BUCKET&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ACM証明書は、CloudFrontのデフォルトドメイン(&lt;code&gt;*.cloudfront.net&lt;&#x2F;code&gt;)のまま運用していて発行していなかったため、削除対象はありませんでした。これでフォント配信に関わるAWSリソースはすべて無くなり、配信はR2経由に一本化されました。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;移行して変わったこと&quot;&gt;移行して変わったこと&lt;&#x2F;h2&gt;
&lt;p&gt;移行の前後で、AWS側にあったCloudFrontディストリビューション、CloudFront Function、S3バケット、バケットポリシー、CORS設定がすべて消えました。残ったのはR2バケット1つと、ごく短いWorkerスクリプト1つです。&lt;&#x2F;p&gt;
&lt;p&gt;今回移したのはフォント一式というごく小さなリソースで、Cloudflareの無料枠の範囲で十分にまかなえています。コスト面より、管理対象のサービス数が減ったことのほうが効きます。フォント1種類のためにCloudFrontとS3とFunctionの3サービスを横断して設定を追っていた状態から、Cloudflareのダッシュボード内で完結する状態になりました。&lt;&#x2F;p&gt;
&lt;p&gt;今回はフォントが題材でしたが、結論はもっと一般的だと考えています。個人ブログのスタティックなリソースの置き場所としては、Amazon S3 + CloudFrontよりもCloudflare R2のほうが向いています。R2なら公開URLを持たせずにWorkerから読め、S3のように公開バケットを設定で囲い込む手間がいりません。ホスティングがCloudflareにあれば置き場も同じプラットフォームにまとまり、費用も無料枠と転送料無料で個人規模なら気になりません。&lt;&#x2F;p&gt;
&lt;p&gt;S3が劣るという話ではありません。ただ、個人ブログの規模で、すでにCloudflareを使っているなら、スタティックなリソースはR2に置くのが素直な選択だと思います。&lt;&#x2F;p&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-oac&quot;&gt;
&lt;p&gt;より確実な構成にするなら、S3バケットを非公開にしてOAC(Origin Access Control)経由でのみ読ませる方法もありました。ただしWEBフォントはページの一部としてブラウザに読み込まれる以上、閲覧者によるダウンロードまで完全に防ぐことはどのみちできません。完全な防御は不可能だという前提で、ある程度は妥協した構成でした。 &lt;a href=&quot;#fr-oac-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</content>
        <summary type="html">ブログに最後まで残っていたAWSリソースは、有償WEBフォントの
配信でした。これをAmazon S3とCloudFrontからCloudflare R2へ
移した記録であり、個人ブログのスタティックなリソースを
どこに置くべきかを考える材料でもあります。
</summary>
        </entry><entry xml:lang="en">
        <title>無料で始める Cloudflare セキュリティ対策</title>
        <published>2026-03-23T00:00:00+00:00</published>
        <updated>2026-03-23T00:00:00+00:00</updated>
        <author>
            <name>Toshiyuki Yoshida</name>
        </author>
        <link rel="alternate" href="https://codedchords.dev/blog/2026/03/cloudflare-free-security/" type="text/html"/>
        <id>https://codedchords.dev/blog/2026/03/cloudflare-free-security/</id>
        
            <content type="html">&lt;!-- textlint-disable --&gt;
&lt;img
  src=&quot;https:&amp;#x2F;&amp;#x2F;codedchords.dev&amp;#x2F;blog&amp;#x2F;2026&amp;#x2F;03&amp;#x2F;cloudflare-free-security&amp;#x2F;cover.webp?h=9bfc915254c8cd5bf47d&quot;
  alt=&quot;Cover&quot; width=&quot;1792&quot; height=&quot;1024&quot; loading=&quot;lazy&quot;
&#x2F;&gt;&lt;!-- textlint-enable --&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;&lt;h2 id=&quot;公開翌日に何が起きたか&quot;&gt;公開翌日に何が起きたか&lt;&#x2F;h2&gt;
&lt;p&gt;先日の記事で紹介した生成AIを使った&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ask.codedchords.dev&quot;&gt;Ask&lt;&#x2F;a&gt; ページを公開した翌日、CloudflareのObservabilityを確認したところ、攻撃と思われるリクエストが並んでいました。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET &#x2F;.env&lt;&#x2F;code&gt; — 環境変数ファイルの探索&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;checkout&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;plans&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;signup&lt;&#x2F;code&gt; — SaaS系パスの総当たり&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;wp-login&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;wp-admin&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;xmlrpc.php&lt;&#x2F;code&gt; — WordPress管理画面の探索&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;api&#x2F;v1&#x2F;stripe&#x2F;config.js&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;api&#x2F;v2&#x2F;payment&#x2F;keys.js&lt;&#x2F;code&gt; — 決済APIの設定ファイル探索（Stripeはオンライン決済プラットフォーム）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;config.php&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;api&#x2F;system&#x2F;info&lt;&#x2F;code&gt; — サーバー情報の収集&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;主にWordPressなどのCMSやStripeなどの決済サービスを狙ったスキャンです。静的サイト＋Workerという構成なのでそんなサービスは乗せていないのですが。公開されているエンドポイントに対してよく狙われる既知のパスを片っ端から叩いているのでしょう。&lt;&#x2F;p&gt;
&lt;p&gt;実害はありませんが、放置するのも気持ちが悪いですし、Workers AIのNeuronsを無駄に消費されるのは避けたいところです。Cloudflareの無料プランのセキュリティ機能で対策しました。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;対策1:_Bot_Fight_Mode&quot;&gt;対策1: Bot Fight Mode&lt;&#x2F;h2&gt;
&lt;p&gt;最も手軽な対策です。Cloudflareダッシュボードの &lt;strong&gt;Security &amp;gt; Bots&lt;&#x2F;strong&gt; から、ワンクリックで有効化できます。&lt;&#x2F;p&gt;
&lt;p&gt;Bot Fight ModeはCloudflareが持つボットの行動パターンデータベースを使い、自動化されたアクセスを検出して計算コストの高いチャレンジを返します。検索エンジンのクローラーなど検証済みの正当なボットは通過させる仕組みです。&lt;&#x2F;p&gt;
&lt;p&gt;設定はON&#x2F;OFFだけで、細かなチューニングは有料プラン（Super Bot Fight Mode）の機能になります。それでも、汎用的なスキャンボットの大半はこれで弾けます。&lt;&#x2F;p&gt;
&lt;p&gt;注意点として、Bot Fight Modeは外部サービスからのWebhookやRSSリーダーのフィード取得など、正当な自動アクセスもブロックする場合があります。無料プランではWAFルールによるバイパス（Skip）ができないため、これらのサービスに影響が出た場合はBot Fight Mode自体を無効にする必要があります。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;対策2:_WAF_カスタムルール&quot;&gt;対策2: WAF カスタムルール&lt;&#x2F;h2&gt;
&lt;p&gt;Bot Fight Modeをすり抜けるアクセスに対しては、WAF（Web Application Firewall）のカスタムルールで対処します。無料プランでは5つまでルールを作成できます。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ルール1:_許可パスの制限（ホワイトリスト）&quot;&gt;ルール1: 許可パスの制限（ホワイトリスト）&lt;&#x2F;h3&gt;
&lt;p&gt;最も効果が高いルールを最優先に配置します。ask.codedchords.devは &lt;code&gt;&#x2F;&lt;&#x2F;code&gt;（フロントエンド）と &lt;code&gt;&#x2F;api&#x2F;&lt;&#x2F;code&gt;（Workers API）しか使わないので、それ以外を全てブロックします。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;plain&quot; data-name=&quot;Expression&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(http.host eq &amp;quot;ask.codedchords.dev&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;and not (http.request.uri.path eq &amp;quot;&#x2F;&amp;quot; or http.request.uri.path contains &amp;quot;&#x2F;api&#x2F;&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Actionは &lt;strong&gt;Block&lt;&#x2F;strong&gt;、Place atは &lt;strong&gt;First&lt;&#x2F;strong&gt; に設定します。&lt;&#x2F;p&gt;
&lt;p&gt;「正当なパスだけを許可する」ホワイトリスト方式なので、攻撃者がどんなパスを試してもすり抜けられません。ただし、対象サイトの構成を把握している必要があります。新しいパスを追加した際にルールの更新を忘れると自分自身のコンテンツがブロックされるため、サイト構成を変更したらルールも見直してください。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ルール2:_スキャンパスの遮断（ブラックリスト）&quot;&gt;ルール2: スキャンパスの遮断（ブラックリスト）&lt;&#x2F;h3&gt;
&lt;p&gt;ルール1はaskサブドメイン限定なので、ドメイン全体に適用する汎用ルールも設定します。WordPressやphpMyAdminなど、よく狙われるパスへのアクセスを一括でブロックします。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Security &amp;gt; Security rules &amp;gt; Create rule&lt;&#x2F;strong&gt; から、以下のExpressionを設定します。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;plain&quot; data-name=&quot;Expression&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(http.request.uri.path contains &amp;quot;.env&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;wp-login&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;wp-admin&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;xmlrpc.php&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;phpmyadmin&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;wp-content&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;or (http.request.uri.path contains &amp;quot;wp-includes&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Actionは &lt;strong&gt;Block&lt;&#x2F;strong&gt; です。&lt;&#x2F;p&gt;
&lt;p&gt;このルールはサイトの構成に依存しないため、Cloudflareで管理しているドメイン全体に適用できます。WordPressを使っていないサイトであれば、デメリットはありません。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;対策3:_Rate_Limiting&quot;&gt;対策3: Rate Limiting&lt;&#x2F;h2&gt;
&lt;p&gt;Bot Fight ModeとWAFルールを通過したリクエストに対する最後の砦です。同一IPからの過剰なリクエストを制限します。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Security &amp;gt; Security rules &amp;gt; Rate limiting rules&lt;&#x2F;strong&gt; から設定します。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;項目&lt;&#x2F;th&gt;&lt;th&gt;設定値&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;対象&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;http.host eq &quot;ask.codedchords.dev&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;判定基準&lt;&#x2F;td&gt;&lt;td&gt;IP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;レート&lt;&#x2F;td&gt;&lt;td&gt;10リクエスト &#x2F; 10秒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Action&lt;&#x2F;td&gt;&lt;td&gt;Block（10秒間）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;無料プランではRate Limitingルールは1つまで、ブロック期間は最大10秒という制約があります。大規模な攻撃には有料プランのRate Limiting（より長いブロック期間、より多くのルール）が必要ですが、スキャンボット対策としては十分です。上記の設定値は一例です。ページ内のリソース数や想定されるアクセスパターンに応じて調整してください。&lt;&#x2F;p&gt;
&lt;p&gt;なお、今回はWorkers側にも &lt;code&gt;&#x2F;api&#x2F;ask&lt;&#x2F;code&gt; エンドポイントに対して同一IP 60秒間10リクエストの制限を入れています。CloudflareのRate Limitingはインフラ層での粗いフィルタリング、Workers側はアプリケーション層での細かな制御という役割分担です。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3層防御の全体像&quot;&gt;3層防御の全体像&lt;&#x2F;h2&gt;
&lt;p&gt;設定した対策を整理します。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;レイヤー&lt;&#x2F;th&gt;&lt;th&gt;対策&lt;&#x2F;th&gt;&lt;th&gt;役割&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;第1層&lt;&#x2F;td&gt;&lt;td&gt;Bot Fight Mode&lt;&#x2F;td&gt;&lt;td&gt;既知のボットパターンを自動検出&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;第2層&lt;&#x2F;td&gt;&lt;td&gt;WAF カスタムルール&lt;&#x2F;td&gt;&lt;td&gt;不正なパス・不要なパスを遮断&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;第3層&lt;&#x2F;td&gt;&lt;td&gt;Rate Limiting&lt;&#x2F;td&gt;&lt;td&gt;過剰なリクエストを制限&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;いずれもCloudflareの無料プランで利用でき、設定はダッシュボードからの操作だけで完結します。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;&#x2F;h2&gt;
&lt;p&gt;これらの設定を適用した直後から、Observability上でスキャン系のリクエストがブロックされていることを確認できました。Workers AIのNeuronsが無駄に消費されることもなくなっています。&lt;&#x2F;p&gt;
&lt;p&gt;個人サイトであっても、公開すればスキャンボットは来ます。「小規模だから狙われない」ということはありません。ボットは規模を問わず、見つけたエンドポイントを片っ端から叩きます。&lt;&#x2F;p&gt;
&lt;p&gt;Cloudflareを使っているなら、無料プランの範囲でもBot Fight Mode、WAFカスタムルール、Rate Limitingの3つを設定するだけで、大半の自動化されたスキャンは防げます。設定にかかる時間は10分程度です。特にWorkers AIやAPIエンドポイントを公開している場合は、不要なリクエストによるリソース消費を防ぐためにも早めの対策をおすすめします。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;References&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;!-- textlint-disable --&gt;
&lt;div class=&quot;references&quot;&gt;
    &lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;bots&#x2F;get-started&#x2F;free&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt;. &quot;Bot Fight Mode&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;waf&#x2F;custom-rules&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt;. &quot;Custom rules&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;waf&#x2F;rate-limiting-rules&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt;. &quot;Rate limiting rules&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;div&gt;
&lt;!-- textlint-enable --&gt;
</content>
        <summary type="html">個人ブログにAI Chat機能を追加した翌日、スキャンボットの集中アクセスを確認しました。Cloudflareの無料プランだけで実施できるBot Fight Mode、WAFカスタムルール、Rate Limitingの3つの対策を紹介します。</summary>
        </entry><entry xml:lang="en">
        <title>Cloudflare Workers AI + Vectorize で RAG</title>
        <published>2026-03-20T00:00:01+09:00</published>
        <updated>2026-05-07T00:00:00+00:00</updated>
        <author>
            <name>Toshiyuki Yoshida</name>
        </author>
        <link rel="alternate" href="https://codedchords.dev/blog/2026/03/ask-my-digital-double/" type="text/html"/>
        <id>https://codedchords.dev/blog/2026/03/ask-my-digital-double/</id>
        
            <content type="html">&lt;!-- textlint-disable --&gt;
&lt;img
  src=&quot;https:&amp;#x2F;&amp;#x2F;codedchords.dev&amp;#x2F;blog&amp;#x2F;2026&amp;#x2F;03&amp;#x2F;ask-my-digital-double&amp;#x2F;cover.webp?h=1c0e8f975b65a3d29155&quot;
  alt=&quot;Cover&quot; width=&quot;1792&quot; height=&quot;1024&quot; loading=&quot;lazy&quot;
&#x2F;&gt;&lt;!-- textlint-enable --&gt;
&lt;details&gt;
&lt;summary&gt;Table of Contents&lt;&#x2F;summary&gt;
&lt;!-- toc --&gt;
&lt;&#x2F;details&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;&lt;h2 id=&quot;何を作ったのか&quot;&gt;何を作ったのか&lt;&#x2F;h2&gt;
&lt;p&gt;このブログのナビゲーションに「ask」というメニューを追加しました。
私のデジタル分身(bot)に質問できるページが表示されます。質問を入力すると、ブログ記事の内容を踏まえて私っぽい回答を返します。&lt;&#x2F;p&gt;
&lt;p&gt;ブログに書いてある話題はもちろん、書いていない話題についても「私ならこう答えるだろう」という推測で回答を生成します。
ただし、たかだか個人ブログの文書量ですから大した学習量ではないため、平気で嘘をつきます。それも含めて楽しんでもらえればと思います。&lt;&#x2F;p&gt;
&lt;p&gt;たとえば、こんな質問を投げてみます。&lt;&#x2F;p&gt;
&lt;!-- textlint-disable --&gt;
&lt;div class=&quot;admonition tip&quot;&gt;
    &lt;div class=&quot;admonition-icon admonition-icon-tip&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;admonition-content&quot;&gt;
        &lt;strong class=&quot;admonition-title&quot;&gt;Q&amp;A の例&lt;&#x2F;strong&gt;
        &lt;p&gt;&lt;strong&gt;Q:&lt;&#x2F;strong&gt; CIOも設置せず、IT Governanceの取り組みを行なわない理事長が「生成AIを導入したい」と言っています。どうすべきですか？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;&#x2F;strong&gt; その理事長に、生成AIの導入が「新しいシステムの導入」と違うことを説明する必要があります。生成AIは、組織の判断の中枢に組み込まれ、自社固有の文脈で動く必要があります。そのためには、IT部門だけでなく、業務部門の理解と協力が必要です。
また、生成AIの導入には、データの整備、セキュリティの確保、コンプライアンスの確保などの準備が必要です。CIOを設置せず、IT Governanceの取り組みを行なわない組織では、これらの準備を十分に整えることが難しいでしょう。（以下、省略）&lt;&#x2F;p&gt;

    &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;!-- textlint-enable --&gt;
&lt;p&gt;私が答えそうな回答を返してきました。ブログ記事の内容をベースにしつつ、ペルソナの設定に基づいた回答を生成しています。&lt;&#x2F;p&gt;
&lt;p&gt;ソースコードはGitHubで公開しています。&lt;&#x2F;p&gt;
&lt;!-- textlint-disable --&gt;
&lt;iframe class=&quot;hatenablogcard&quot; src=&quot;https:&#x2F;&#x2F;hatenablog-parts.com&#x2F;embed?url=https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;yostos&amp;#x2F;yostos-persona&quot; width=&quot;100%&quot; height=&quot;155&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; loading=&quot;lazy&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;!-- textlint-enable --&gt;
&lt;h2 id=&quot;動機&quot;&gt;動機&lt;&#x2F;h2&gt;
&lt;p&gt;仕事を終えて職場を変えることになったとき、同僚が「質問できなくなるので、あなたのbotがあればいいのに」と言ってくれました。ありがたい話です。真に受けました。&lt;&#x2F;p&gt;
&lt;p&gt;もう1つの動機は、せっかくブログをCloudflare Pagesに移行したのでWorkers AIも試してみようという技術的な興味です。Workers AI、Vectorize、Workersを組み合わせたRAGシステムを自分で組んでみたかったですし、構築過程がそのままブログのネタになるのも都合が良いと思いました。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;アーキテクチャ&quot;&gt;アーキテクチャ&lt;&#x2F;h2&gt;
&lt;p&gt;処理フローはシンプルなRAGパターンです。&lt;&#x2F;p&gt;
&lt;p&gt;まず事前処理として、ブログ記事をVectorizeに投入しておきます。&lt;&#x2F;p&gt;
&lt;!-- textlint-disable --&gt;


&lt;noscript&gt;
    &lt;strong&gt;⚠️ JavaScript is required to render the diagram.&lt;&#x2F;strong&gt;
&lt;&#x2F;noscript&gt;
&lt;pre class=&quot;mermaid invertible-image&quot;&gt;
    graph LR
Articles[ブログ記事&lt;br&gt;Markdown]--&gt; Parse[front matter解析&lt;br&gt;+ セクション分割]--&gt; EMB1[BGE-M3&lt;br&gt;embedding]--&gt; V[(Vectorize)]
&lt;&#x2F;pre&gt;
&lt;!-- textlint-enable --&gt;
&lt;p&gt;質問時は、入力をベクトル化して類似チャンクを検索し、ペルソナ設定と合わせてLLMに渡します。&lt;&#x2F;p&gt;
&lt;!-- textlint-disable --&gt;


&lt;noscript&gt;
    &lt;strong&gt;⚠️ JavaScript is required to render the diagram.&lt;&#x2F;strong&gt;
&lt;&#x2F;noscript&gt;
&lt;pre class=&quot;mermaid invertible-image&quot;&gt;
    graph LR
Q[質問]--&gt; EMB2[BGE-M3&lt;br&gt;embedding]--&gt; V[(Vectorize)]
V --&gt;|類似チャンク| LLM[Llama 3.3 70B]
Persona[ペルソナ設定]--&gt; LLM
LLM --&gt; A[回答 +&lt;br&gt;参考記事リンク]
&lt;&#x2F;pre&gt;
&lt;!-- textlint-enable --&gt;
&lt;ol&gt;
&lt;li&gt;ユーザーが質問を入力&lt;&#x2F;li&gt;
&lt;li&gt;WorkersがBGE-M3で質問をベクトル化&lt;&#x2F;li&gt;
&lt;li&gt;Vectorizeで類似チャンクを検索（上位5件、スコア閾値0.3）&lt;&#x2F;li&gt;
&lt;li&gt;システムプロンプト（ペルソナ設定）＋ 取得したチャンク ＋ 質問をLlama 3.3 70Bに渡す&lt;&#x2F;li&gt;
&lt;li&gt;回答テキスト＋参考記事リンクをフロントエンドに返す&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;使用しているCloudflareサービスはWorkers、Workers AI（EmbeddingとLLM）、Vectorizeの3つだけです。フロントエンドはWorkersのAssets機能で配信しています。フレームワークは使わず、素のHTML&#x2F;CSS&#x2F;JSで構築しました。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;モデル選定&quot;&gt;モデル選定&lt;&#x2F;h2&gt;
&lt;p&gt;EmbeddingモデルはBGE-M3（&lt;code&gt;@cf&#x2F;baai&#x2F;bge-m3&lt;&#x2F;code&gt;）を採用しました。BAAI開発の多言語対応モデルで、出力は1024次元です。日本語と英語が混在するブログ記事に対応できることが決め手でした。当初はPLaMo-Embedding-1Bを検討しましたが、出力が2048次元でVectorizeの上限（1536次元）を超えるため断念しています。&lt;&#x2F;p&gt;
&lt;p&gt;LLMはLlama 3.3 70B Instruct FP8 Fast（&lt;code&gt;@cf&#x2F;meta&#x2F;llama-3.3-70b-instruct-fp8-fast&lt;&#x2F;code&gt;）です。128Kトークンのコンテキストウィンドウを持ち、日本語でのペルソナ付き回答生成は概ね良好です。中国語文字の混入が発生すると既知の問題として上がっています。今回は、システムプロンプトで抑制しています。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;事前処理:_ナレッジベースの構築&quot;&gt;事前処理: ナレッジベースの構築&lt;&#x2F;h2&gt;
&lt;p&gt;ブログのMarkdownファイル群をナレッジとして使用しており、これらを解析して事前にVectorizeする処理スクリプト(ingest)が行う処理は以下の通りです。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;記事のファイルパス、front matterからslug、title、date、tagsを抽出し、本文を見出し（&lt;code&gt;##&lt;&#x2F;code&gt;）単位でセクション分割&lt;&#x2F;li&gt;
&lt;li&gt;各チャンクにメタデータ（ファイルパス、slug、title、section、URL）を付与&lt;&#x2F;li&gt;
&lt;li&gt;BGE-M3でベクトル化してVectorizeにアップサート&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;差分検出にはマニフェストファイル（slug → MD5ハッシュのマッピング）を使い、変更のあった記事のみ再処理します。初回ingestでは286記事を処理しました。Vectorizeのメタデータ上限（10,240 bytes）対策として、チャンク本文を8,000 bytesに切り詰めています。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;質問(Request)対応:_Workers_AI_のコード&quot;&gt;質問(Request)対応: Workers AI のコード&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;Vectorize処理&quot;&gt;Vectorize処理&lt;&#x2F;h3&gt;
&lt;p&gt;Workers AIの特徴は &lt;code&gt;env.AI.run()&lt;&#x2F;code&gt; というバインディング呼び出しです。APIキーの管理もSDKのインストールも不要で、&lt;code&gt;wrangler.toml&lt;&#x2F;code&gt; の &lt;code&gt;[ai]&lt;&#x2F;code&gt; セクションに &lt;code&gt;binding = &quot;AI&quot;&lt;&#x2F;code&gt; と書くだけで使えます。&lt;&#x2F;p&gt;
&lt;p&gt;RAGの検索部分（rag.ts）を丸ごと載せます。Embedding生成からVectorize検索まで、これだけで完結しています。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;typescript&quot; data-name=&quot;src&#x2F;worker&#x2F;rag.ts&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;import type&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite&quot;&gt; Env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; }&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; from&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;.&#x2F;types&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-storage z-type&quot;&gt;export interface&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-type&quot;&gt; RetrievedChunk&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;  text&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;  meta&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;    slug&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;    title&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;    section&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;    url&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt;  score&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; number&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; SCORE_THRESHOLD&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 0.3&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; TOP_K&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-storage z-modifier z-storage z-type&quot;&gt;export async function&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; retrieveChunks&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt;  question&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt;  env&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-type&quot;&gt; Env&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-type&quot;&gt; Promise&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-typeparameters&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-type z-meta z-type&quot;&gt;RetrievedChunk[]&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-typeparameters&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  &#x2F;&#x2F; Embed the question&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; embeddingResult&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; await&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;AI&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;run&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;@cf&#x2F;baai&#x2F;bge-m3&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;, {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    text&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-variable z-other z-readwrite&quot;&gt; [question]&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  }&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; queryVector&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; embeddingResult&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts z-source&quot;&gt;data[&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt;0&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;]&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  &#x2F;&#x2F; Search Vectorize&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; results&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; await&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;VECTORIZE&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;query&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-variable z-other z-readwrite&quot;&gt;(queryVector&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;, {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    topK&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; TOP_K&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    returnValues&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-boolean&quot;&gt; false&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    returnMetadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;all&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  }&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  &#x2F;&#x2F; Filter by score threshold and map to chunks&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; results&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;matches&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;    .&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;filter&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt;m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-function z-arrow z-ts&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;score&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; &amp;gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts z-source&quot;&gt; SCORE_THRESHOLD)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;    .&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;map&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter&quot;&gt;m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-function z-arrow z-ts&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;      text&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;?.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;text&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-support z-type z-primitive&quot;&gt; as string&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;      meta&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;        slug&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;?.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;slug&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-support z-type z-primitive&quot;&gt; as string&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;        title&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;?.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;title&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-support z-type z-primitive&quot;&gt; as string&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;        section&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;?.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;section&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-support z-type z-primitive&quot;&gt; as string&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;        url&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; (m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;?.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;url&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-support z-type z-primitive&quot;&gt; as string&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;      score&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; m&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;score&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;    }&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;))&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;LLM呼び出し&quot;&gt;LLM呼び出し&lt;&#x2F;h3&gt;
&lt;p&gt;LLMの呼び出しも同じパターンです。&lt;code&gt;env.AI.run()&lt;&#x2F;code&gt; にモデル名とメッセージを渡すだけで回答が返ってきます。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;typescript&quot; data-name=&quot;src&#x2F;worker&#x2F;index.ts（抜粋）&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; llmResult&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; await&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; env&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt;AI&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;run&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;  &amp;quot;@cf&#x2F;meta&#x2F;llama-3.3-70b-instruct-fp8-fast&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    messages&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;      {&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; role&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;system&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; content&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite&quot;&gt; systemPrompt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;      {&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; role&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; content&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-readwrite&quot;&gt; question&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    ]&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    max_tokens&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value&quot;&gt;:&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 512&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;const&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-constant z-ts&quot;&gt; answer&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-variable z-other z-readwrite&quot;&gt; (llmResult&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; as&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-object z-property&quot;&gt; response&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;?:&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-type z-primitive&quot;&gt; string&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; }&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-property z-ts&quot;&gt;response&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;  ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;回答を生成できませんでした。&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;外部APIを叩いている感覚がなく、Workersの中でAIを呼んでいるだけという体験は新鮮です。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ペルソナ設定&quot;&gt;ペルソナ設定&lt;&#x2F;h3&gt;
&lt;p&gt;コードの &lt;code&gt;systemPrompt&lt;&#x2F;code&gt; として渡しているのがペルソナ設定です。回答は私のキャラクタ設定に基づいて生成されます。元IBM&#x2F;AWSのエンジニア、ギター愛好家、CLIツール開発が趣味といったプロフィールに加えて、口調や回答スタイルも定義しています。敬語ベースで、好きな話題には熱量が上がり、知らないことでもぺらぺら喋るという設定です。こうしてみると、オタクそのものですね。&lt;&#x2F;p&gt;
&lt;p&gt;回答スタイルとして、簡潔に答えるだけでなく、ときどき脱線したり問い返したりするような振る舞いも仕込んでいます。200〜300文字程度を目安にしていますが、実際の挙動はモデル次第です。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;コスト&quot;&gt;コスト&lt;&#x2F;h2&gt;
&lt;p&gt;個人ブログのQ&amp;amp;Aという用途なので、非機能要件は「1日数十件の質問を無料枠内で処理できること」に設定しています。&lt;&#x2F;p&gt;
&lt;p&gt;Workers AIの課金単位はNeuronsです。1リクエストあたりの消費量を見積もります。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;処理&lt;&#x2F;th&gt;&lt;th&gt;モデル&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;トークン数（概算）&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Neurons&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Embedding（質問文）&lt;&#x2F;td&gt;&lt;td&gt;BGE-M3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;50 入力&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.05&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;LLM 入力（プロンプト＋チャンク＋質問）&lt;&#x2F;td&gt;&lt;td&gt;Llama 3.3 70B&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;2,000 入力&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;53&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;LLM 出力（回答生成）&lt;&#x2F;td&gt;&lt;td&gt;Llama 3.3 70B&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;512 出力&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;105&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;合計&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;約160&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Workers AIの無料枠は10,000 Neurons&#x2F;日なので、&lt;strong&gt;約60リクエスト&#x2F;日&lt;&#x2F;strong&gt;が上限になります。&lt;&#x2F;p&gt;
&lt;p&gt;Vectorizeも無料枠に十分収まります。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;リソース&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;使用量&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;無料枠&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;消費率&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;格納次元数（286記事 × 1,024次元）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;約29万&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;500万&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;6%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;クエリ次元数&#x2F;月&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;数千&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;3,000万&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.1%未満&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;超過した場合はWorkers Paidプラン（月$5）で$0.011&#x2F;1,000 Neuronsの従量課金になりますが、現在の利用量では発生しない見込みです。仮に想定の倍（120リクエスト&#x2F;日）が30日続いたとしても、超過分は月約288,000 Neuronsで約$3.2、プラン基本料と合わせて月$8程度です。&lt;&#x2F;p&gt;
&lt;p&gt;悪用防止として、KVベースでIPあたり60秒間10リクエストのレート制限をかけています。単一IPからの過剰なリクエストは抑制できますが、分散アクセスに対しては無力なので、無料枠の維持を保証するものではありません。個人ブログの自然なトラフィックであれば問題ない想定です。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;今後の課題&quot;&gt;今後の課題&lt;&#x2F;h2&gt;
&lt;p&gt;現時点で把握している課題は、BGE-M3の日本語検索精度、チャンク分割の粒度、LLMのmax_tokens最適値（回答品質とコストのバランス）、同一質問に対するキャッシュによる重複推論の回避です。&lt;&#x2F;p&gt;
&lt;p&gt;いずれの改善も、実際にどんな質問が来るかわからない段階では手の打ちようがありません。精度を上げるには実際の質問パターンに基づいた評価用クエリが必要で、つまりフィードバックループを回す必要があります。ただし、面白半分で作ったお遊びですし、質問内容を保持するのはプライバシーの観点で望ましくないため、質問内容のロギングは行っていません。一方で、ブログ記事が増えればナレッジベースが厚くなり、検索精度は自然と改善していく可能性があります。&lt;&#x2F;p&gt;
&lt;p&gt;完璧なものを作ってから公開するよりも、動くものを出して改善していくほうが性に合っています。質問を投げてみて、変な回答が返ってきたら、それはそれで面白いと思ってください。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;References&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;!-- textlint-disable --&gt;
&lt;div class=&quot;references&quot;&gt;
    &lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;workers-ai&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt;. &quot;Workers AI&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;vectorize&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt;. &quot;Vectorize&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;BAAI&#x2F;bge-m3&quot;&gt;BAAI&lt;&#x2F;a&gt;. &quot;BGE-M3&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;div&gt;
&lt;!-- textlint-enable --&gt;
</content>
        <summary type="html">ブログ記事をナレッジベースにして、著者のペルソナで質問に答えるAI Q&amp;Aページを Cloudflare Workers AI と Vectorize で構築しました。BGE-M3 による日本語 embedding、Llama 3.3 70B によるペルソナ付き回答生成、差分 ingest パイプラインまで、Cloudflare スタックだけで完結しています。</summary>
        </entry>
</feed>
