KSTECHBLOG

自分の分身AIと音声会話できるアプリ⑤

Cloudflare Tunnelを使い、Vercelで公開するWebアプリから自宅PCの文字起こしAPIへ安全に接続する環境を構築します。

はじめに

前回の記事では、GPT-SoVITSを使って自分の声を再現し、入力した文章を自分の声で読み上げられるところまで確認しました。

今回は、Vercelで公開するWebアプリから自宅PCで動作するWhisper、LM Studio、GPT-SoVITSのAPIへ接続するため、Cloudflare Tunnelを構築します。各APIをインターネットへ直接公開せず、Cloudflare Accessの認証を通して接続できる構成を目指します。

Cloudflare Tunnelへ独自のホスト名を割り当てるには、Cloudflareで管理するドメインが必要です。まずはCloudflare Registrarでドメインを登録します。すでにCloudflareで管理しているドメインがある場合、この手順は読み飛ばしてください。

Cloudflareでドメインを登録する

Cloudflareダッシュボードへログインし、左側のメニューから「ドメイン」→「登録」を開きます。続いて、画面右上の「ドメインを購入」を選択します。

Cloudflareダッシュボードのドメイン登録画面。アカウント名・アカウントID・登録済みドメインはモザイク加工済み

希望するドメインを検索する

検索欄へ取得したいドメイン名を入力し、「検索」を選択します。完全なドメイン名だけでなく、キーワードを入力して候補を探すこともできます。

検索結果に希望するドメインが表示されたら、ドメイン名と初年度・更新時の料金を確認して「購入」を選択します。検索結果へ表示されていても、購入時の最終確認で取得できないと判定される場合があります。その場合は別の候補を検索します。

Cloudflare Registrarでは、Cloudflareのネームサーバーを使用します。登録後に別のDNS事業者のネームサーバーへ変更することはできないため、ほかのDNSサービスを利用する予定がある場合は事前に構成を確認してください。また、日本語ドメインなどの国際化ドメイン名には対応していません。

購入を完了する

画面に表示されるドメイン名、登録期間、料金、連絡先、自動更新の設定を確認します。利用規約とプライバシーポリシーへ同意し、「購入を完了」を選択します。

登録が完了するとドメインの管理画面へ移動し、Cloudflareから確認メールが届きます。「ドメイン」→「登録」の一覧に取得したドメインが表示され、ステータスが「アクティブ」になっていることを確認します。これで、Cloudflare Tunnelへ割り当てるドメインを用意できました。

詳しい要件や画面が変わった場合の最新手順は、Cloudflare Registrar公式ドキュメントを参照してください。

Named Tunnelを作成する

続いて、Cloudflare Zero TrustでNamed Tunnelを作成します。Named Tunnelには名前と固定のIDが割り当てられるため、トンネルを停止・再起動しても同じ設定を継続して使用できます。

Cloudflareダッシュボードの左側にある「ネットワーク」→「コネクタ」を開き、画面右上の「トンネルを作成」を選択します。中央に「トンネルを追加」が表示されている場合は、こちらから進んでも同じです。

Cloudflare Zero Trustのコネクタ画面。アカウント名・メールアドレス・アカウントIDはモザイク加工済み

トンネルの種類と名前を設定する

Cloudflare Tunnelのトンネルタイプ選択画面。アカウント名とメールアドレスはモザイク加工済み

コネクタにはcloudflaredを選択します。次の画面でトンネルを識別する名前を入力し、「トンネルを保存」を選択します。今回は音声AI用であることが分かるように、voice-aiなどの名前を設定します。

トンネル名はCloudflareダッシュボードで管理するための名前です。外部からアクセスするURLには使用されません。

cloudflaredを接続する

トンネルを保存すると、cloudflaredを実行する環境の選択画面と起動コマンドが表示されます。今回は文字起こしAPIと同じDockerネットワークでcloudflaredを起動するため、オペレーティングシステムに「Docker」を選択します。

表示されるコマンドには、このトンネルをCloudflareへ接続するためのTunnel Tokenが含まれています。トークンをGitへコミットしたり、記事やスクリーンショットへ掲載したりしないでください。具体的な保存方法と起動手順は、後述する「Tunnel TokenをPCへ設定する」で説明します。

公開ホスト名を設定する

「パブリックホスト名」へ、外部公開するホスト名と接続先サービスを設定します。

Cloudflare Tunnelのパブリックホスト名設定画面。実ドメインとアカウント情報はモザイク加工済み

今回の設定内容は次のとおりです。

項目設定値説明
サブドメインwhisper音声文字起こしAPIへ割り当てる名前
ドメイン登録したドメイン前の手順でCloudflareへ登録したドメイン
パス空欄ホスト名以下のすべてのパスを転送する
サービスのタイプHTTPDockerネットワーク内のHTTPサービスへ接続する
URLtranscription:8000文字起こしコンテナのサービス名とポート番号

サブドメインとドメインを組み合わせた完全なホスト名が、外部からアクセスするURLになります。たとえばドメインがexample.comの場合は、https://whisper.example.comです。

transcription:8000はDocker Composeのサービス名とポート番号です。cloudflaredと文字起こしAPIを同じDockerネットワークで起動することで、コンテナ間をこの名前で接続できます。

入力内容を確認して「セットアップを完了」を選択します。Cloudflare DNSには、このNamed Tunnelへ向けたDNSレコードが自動的に作成されます。

LM StudioとGPT-SoVITSの公開ホスト名を追加する

Whisper用の設定が完了したら、同じNamed TunnelにLM Studio用とGPT-SoVITS用の公開ホスト名も追加します。トンネルをサービスごとに作成する必要はなく、1つのトンネルに3つのルートを設定します。

Cloudflare Zero Trustの「ネットワーク」→「コネクタ」から作成したトンネルを開き、「パブリックホスト名を追加」を選択します。Whisperと同じ手順で、次のサブドメインと接続先を設定します。

用途サブドメインサービスURLの例
文字起こしwhisperhttp://transcription:8000
返答生成lmstudiohttp://host.docker.internal:1234
音声合成sovitshttp://host.docker.internal:9880

cloudflaredをDockerで起動し、LM StudioとGPT-SoVITSをWindows上で起動する場合は、コンテナからホストPCへ接続するためにhost.docker.internalを使用します。ポート番号は各APIの起動設定に合わせて変更してください。また、LM Studioはローカルネットワークからの接続を許可した状態でAPIサーバーを起動します。

ドメインがexample.comの場合、追加したURLはそれぞれhttps://lmstudio.example.comhttps://sovits.example.comになります。

これでNamed Tunnelと公開ホスト名の作成は完了です。ただし、この時点ではURLを知っている第三者からも接続できるため、続けてCloudflare Accessを設定してAPIを保護します。

画面構成が変わった場合は、Cloudflare Tunnelの公式セットアップガイドも参照してください。

Tunnel TokenをPCへ設定する

cloudflaredをDockerコンテナとして起動し、Named Tunnelへ接続するためのTunnel TokenをPCへ設定します。トークンをcompose.yamlや起動コマンドへ直接書かず、環境変数としてDockerへ渡します。

Cloudflare Zero Trustの「ネットワーク」→「コネクタ」を開き、作成したトンネルを選択します。「コネクター」タブからコネクターの追加画面を開き、オペレーティングシステムに「Docker」を選択します。

Cloudflare TunnelのDocker用コネクターコマンド画面。アカウント情報・トンネル名・Tunnel Tokenはモザイク加工済み

表示されたDockerコマンドのうち、--tokenの後ろにある文字列がTunnel Tokenです。コピーしたトークンを、PowerShellからWindowsユーザーの環境変数TUNNEL_TOKENへ保存します。

[Environment]::SetEnvironmentVariable(
  "TUNNEL_TOKEN",
  "<TUNNEL_TOKEN>",
  [EnvironmentVariableTarget]::User
)

<TUNNEL_TOKEN>はCloudflareダッシュボードからコピーした実際の値へ置き換えます。設定後に新しいPowerShellを開き、環境変数が読み込まれていることを確認します。トークンそのものを画面へ表示しないよう、値の有無だけを判定します。

if ([string]::IsNullOrWhiteSpace($env:TUNNEL_TOKEN)) {
  throw "TUNNEL_TOKENが設定されていません。"
}

Write-Host "TUNNEL_TOKENを読み込みました。"

環境変数を使ってcloudflaredを起動する

Dockerでは、-e TUNNEL_TOKENを指定するとPCに設定した環境変数をコンテナへ引き渡せます。

docker run --rm `
  --name cloudflared `
  --network <DOCKER_NETWORK_NAME> `
  -e TUNNEL_TOKEN `
  cloudflare/cloudflared:latest `
  tunnel --no-autoupdate run

<DOCKER_NETWORK_NAME>は、文字起こしAPIのtranscriptionコンテナが参加しているDockerネットワーク名へ置き換えます。cloudflaredtranscriptionを同じネットワークへ参加させることで、公開ホスト名に設定したhttp://transcription:8000へサービス名で接続できます。

ログにCloudflareへの接続完了を示すメッセージが表示され、ダッシュボード上でコネクターが接続済みになれば設定完了です。常時稼働させる場合は、Docker Composeのcloudflaredサービスへ同じ環境変数とネットワークを設定し、restart: unless-stoppedなどの再起動ポリシーを指定します。

Tunnel Tokenを知っている人は、そのトンネルへ新しいコネクターを接続できます。トークンをGit、スクリーンショット、ログ、チャットへ貼り付けないでください。同じWindowsユーザーで実行されるプロセスからはユーザー環境変数を読み取れるため、PCのアカウント自体も適切に保護します。

トークンが漏えいした可能性がある場合は、画像にある「トークンを更新」から新しいトークンを発行します。更新すると旧トークンでは新しい接続を開始できなくなりますが、すでに確立している接続は直ちには切断されません。新しいトークンをPCへ設定し直したあと、cloudflaredを再起動します。

利用できる起動パラメーターと環境変数は、Cloudflare公式のTunnel run parametersを参照してください。

Accessコントロールで認証を適用する

作成したwhisperlmstudiosovitsの公開ホスト名へCloudflare Accessを適用し、サービス・トークンを持つVercelアプリだけが各APIへ接続できるようにします。サービス・トークンは、ブラウザでログインできないバックエンド同士の通信を認証するための資格情報です。

サービス・トークンを作成する

Cloudflare Zero Trustの左側にある「Accessコントロール」→「サービス資格情報」を開き、「サービス・トークンを作成」を選択します。サービス・トークン名には、用途が分かるvercel-voice-aiなどの名前を入力します。

有効期間を選択してトークンを作成すると、クライアントIDとクライアントシークレットが表示されます。

Cloudflare Accessのサービス・トークン詳細画面。アカウント情報・クライアントID・クライアントシークレットはモザイク加工済み

クライアントシークレットを確認できるのは作成直後だけです。画面を閉じる前に、次の2つの値をパスワードマネージャーなどの安全な場所へ保存します。

HTTPヘッダー保存する値Vercelの環境変数名
CF-Access-Client-IdクライアントIDCF_ACCESS_CLIENT_ID
CF-Access-Client-SecretクライアントシークレットCF_ACCESS_CLIENT_SECRET

画像では検証用に有効期間を「無期限」としています。本番運用では有効期限を設定し、期限が近づいたら新しいトークンへローテーションする運用が安全です。これらの値はソースコードや.envファイルへ直接記載してGitへコミットしないでください。

セルフホストアプリケーションを作成する

Cloudflare Zero Trustの左側にある「Accessコントロール」→「アプリケーション」を開きます。既存のアプリケーションが一覧表示されたら、画面右上の「新規アプリケーションを作成」を選択します。

Cloudflare Accessのアプリケーション一覧画面。ダッシュボードURL・アカウント情報・実ドメインはモザイク加工済み

アプリケーションの種類から「セルフホスト」を選択し、アプリケーション名には用途が分かるvoice-ai-apiなどの名前を入力します。「アプリケーションの詳細」にある「宛先」では、「パブリックホスト名」にNamed Tunnelで作成した各API用のホスト名を設定します。

今回の設定内容は次のとおりです。

サブドメイン用途パス説明
whisper文字起こし空欄Whisper APIのすべてのパスを保護する
lmstudio返答生成空欄LM Studio APIのすべてのパスを保護する
sovits音声合成空欄GPT-SoVITS APIのすべてのパスを保護する

3つの宛先には、いずれもCloudflareで管理している同じドメインを指定します。画面の「パブリックホスト名を追加」から宛先を追加し、すべてのAPIに同じAccessポリシーを適用します。

ブラウザーレンダリングはRDP、SSH、VNCなどをブラウザーから利用する場合の機能です。今回はHTTPで文字起こしAPIへ接続するため、無効のままにします。

「Accessポリシー」では、Vercelからの通信を許可するサービス認証ポリシーを設定します。すでにAllow Vercelポリシーを作成している場合は一覧から追加できます。まだ作成していない場合は「新しいポリシーを作成」を選択し、次の手順で設定します。

Cloudflare Accessの新規セルフホストアプリケーション設定画面。ダッシュボードURL・アカウント情報・実ドメイン・ポリシーIDはモザイク加工済み

サービス認証ポリシーを設定する

同じ画面の「Accessポリシー」で、Vercelからのリクエストを許可するポリシーを作成します。「新しいポリシーを作成」を選択し、ポリシー名とアクション、対象にするサービス・トークンを設定します。

Cloudflare Accessのセルフホストアプリケーションとサービストークンポリシーの設定画面。アカウント情報と実ドメインはモザイク加工済み

ポリシーには次の内容を設定します。

項目設定値
ポリシー名Allow Vercel
アクションサービス認証
IncludeのセレクターService Token
Includeの値vercel-voice-ai

アクションには必ず「サービス認証」を選択します。「許可」を選択するとIDプロバイダーによるユーザーログインが要求され、Vercelのバックエンドから接続できません。入力内容を確認してセルフホストアプリケーションを保存します。

Vercelから認証ヘッダーを送信する

Accessで保護したURLへリクエストするときは、保存したクライアントIDとクライアントシークレットを次のHTTPヘッダーへ設定します。

const response = await fetch("https://whisper.example.com/transcribe", {
  method: "POST",
  headers: {
    "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID ?? "",
    "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET ?? "",
  },
  body: formData,
});

whisper.example.comは実際に設定したホスト名へ置き換えます。VercelプロジェクトにもCF_ACCESS_CLIENT_IDCF_ACCESS_CLIENT_SECRETを環境変数として登録します。

これらは秘密情報のため、ブラウザー側のコードやNEXT_PUBLIC_で始まる環境変数へ設定してはいけません。必ずVercelのサーバー側だけで参照し、環境変数を追加または更新したあとは再デプロイして反映します。

正しいヘッダーを持たないリクエストはCloudflare Accessで拒否され、文字起こしAPIまで到達しません。これにより、文字起こしAPIをインターネットへ直接公開せず、Vercelアプリからの通信だけを許可できます。

接続を確認する

最後に、認証情報がないリクエストは拒否され、正しいサービス・トークンを付けたリクエストだけが文字起こしAPIへ到達することを確認します。テストには短い音声ファイルsample.wavを使用します。

認証ヘッダーなしで拒否されることを確認する

PowerShellで次のコマンドを実行します。whisper.example.comは実際のホスト名へ置き換えてください。

curl.exe -i -X POST "https://whisper.example.com/transcribe" `
  -F "file=@sample.wav"

Cloudflare Accessから401 Unauthorizedまたは403 Forbiddenが返り、文字起こし結果が取得できなければ保護されています。レスポンスコードはAccessアプリケーションの設定によって異なります。

サービス・トークンを付けて接続する

新しいPowerShellを開き、クライアントIDとクライアントシークレットを環境変数へ設定します。値をコマンド履歴へ残したくない場合は、PowerShellの入力ダイアログから設定します。

$env:CF_ACCESS_CLIENT_ID = Read-Host "Cloudflare Access Client ID"
$secret = Read-Host "Cloudflare Access Client Secret" -AsSecureString
$env:CF_ACCESS_CLIENT_SECRET = [System.Net.NetworkCredential]::new("", $secret).Password

認証ヘッダーを付けて、同じ音声ファイルを送信します。

curl.exe -i -X POST "https://whisper.example.com/transcribe" `
  -H "CF-Access-Client-Id: $env:CF_ACCESS_CLIENT_ID" `
  -H "CF-Access-Client-Secret: $env:CF_ACCESS_CLIENT_SECRET" `
  -F "file=@sample.wav"

2xxのレスポンスと文字起こし結果が返れば、Cloudflare Access、Cloudflare Tunnel、文字起こしAPIまでの経路を利用できる状態です。最後にVercelアプリを再デプロイし、実際の画面から音声ファイルを送信して同じ結果が返ることも確認します。

502 Bad Gatewayが返る場合は、cloudflaredtranscriptionが同じDockerネットワークに参加していること、文字起こしAPIがポート8000で起動していること、サービスURLがhttp://transcription:8000になっていることを確認します。

確認後は、現在のPowerShellから一時的な認証情報を削除します。

Remove-Item Env:CF_ACCESS_CLIENT_ID
Remove-Item Env:CF_ACCESS_CLIENT_SECRET

最新の設定項目やトークンの更新方法は、Cloudflare公式のサービス・トークンドキュメントを参照してください。

次回予告

次回の記事では、分身AIの性格や話し方をpersona.mdへ記載し、Next.jsのAPI RouteからLM Studioへシステムプロンプトとして送信します。

「自分の分身AIと音声会話できるアプリ」全6回

  1. 開発のきっかけ・全体構成
  2. Whisperの環境構築
  3. LM Studioの導入
  4. GPT-SoVITSで自分の声を再現
  5. Cloudflare Tunnelの設定(この記事)
  6. persona.mdで性格・話し方を設定
トップページへ戻る