Cloudflare Quick Tunnelsを試してみた!ngrokの代替になるか?

研究のデモシステムでStreamlitを使ったデモをする機会が多かったです。これまではngrokを使っていたのですが、制限が多く実験中に困ることがありました。そんな中、Cloudflare Quick Tunnelsで代替できそうだったので実際に試してみました。

結論から言うと、アカウント不要で即使えるのは想像以上に便利ですね。ただし、万能ではないので、その辺りも書いておこうと思います。

なぜngrokから移行を検討したのか

Streamlitのシステムデモをするとき、ngrok(無料版)にはいくつか困る点があった。

  1. 転送量・リクエスト数などの上限に当たりやすい
  2. 同時接続数が制限されている

WSL環境で設定する

Windowsの導入は比較的ドキュメントが多いようだったので、今回は以下の環境で試してみました。

  • Windows 11
  • WSL2 (Ubuntu)
  • Python 3.12

1. cloudflaredのインストール

WSLのターミナルで以下を実行します。

$ wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
$ sudo dpkg -i cloudflared-linux-amd64.deb
$ cloudflared --version

特に問題なくインストールでき、バージョン情報が表示されればOKです。

2. 事前設定

事前にプロジェクトの設定を行なっておきます。

$ mkdir cloudflare-Tunnels
$ cd cloudflare-Tunnels/

$ uv venv
Using CPython 3.12.3 interpreter at: /usr/bin/python3
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

$ source .venv/bin/activate

$ uv pip install streamlit pandas numpy

2. Streamlitアプリを起動

テスト用に簡単なアプリを作成します。

test_app.py

# test_app.py
import streamlit as st
import pandas as pd
import numpy as np

st.title('データ分析デモ')

data = pd.DataFrame({
    '日付': pd.date_range('2024-01-01', periods=100),
    '数値': np.random.randint(10, 100, 100)
})

st.line_chart(data.set_index('日付'))

プログラムの実行

$ streamlit run test_app.py

ブラウザでhttp://localhost:8501で起動することを確認。

3. Quick Tunnelで公開

別のターミナルで以下を実行:

$ cloudflared tunnel --url http://localhost:8501

すると、こんな感じでURLが表示されます。

+--------------------------------------------------------------------------------------------+
|  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |
|  https://shanghai-pierce-possibility-famous.trycloudflare.com                              |
+--------------------------------------------------------------------------------------------+

URLをブラウザで開くと、Streamlitアプリが表示された。本当にこれだけ

以下は別回線からiPadでアクセスを行なった場合のスクリーンショット

スマホからもアクセスできるし、HTTPSで接続されているのも良い🤩

良かった点😊

1. アカウント登録が不要

これが最大のメリット。cloudflaredをインストールするだけで使える。

2. 転送量・リクエスト数などの上限にひっかかりにくい

ngrokではアクセス数の上限に引っかかることが多かったのですが、変更することで引っかかりにくくなったようです。被験者の多い実験では結構これは大きいかな。

3. WSLでそのまま使える

WSL内のlocalhostを指定するだけで動く。IPアドレスを調べたり、ポートフォワーディングを設定したりする必要がない。 Windows + WSL環境でも、Linuxと同じ感覚で使えるのは助かる。

4. URLがHTTPSでアクセスできる

生成されるURLはHTTPSでアクセスできる。セキュリティ的にも安心だし、ブラウザの警告も出ない。

5. プログラムを起動後に、トンネルが起動できる

検証するアプリを起動してから、別のターミナルでトンネルを起動できる。順番を気にせずに使えるのは便利。

困った点・注意点🥲

もちろん万能ではない。使ってみて気づいた注意点も書いておく。

1. URLが毎回変わる

これはngrok(無料版)と同じ。再起動するたびにURLが変わる。

デモ中は起動したままにしておく必要がある。「ちょっとアプリを修正して再起動」すると、URLを共有し直さないといけない。

対策: デモ前にしっかりテストして、本番は再起動しないようにする。

2. URLが覚えにくい

生成されるURLは https://形容詞-名詞-動詞-副詞.trycloudflare.com みたいな感じで、かなり長い。

口頭で伝えるのは難しいので、Slackやメールで送るか、QRコード化するのが良さそう。

ということで、補助用のラッパースクリプトをpythonで作ってみました。

#!/usr/bin/env python3
"""
cloudflared_tunnel.py - Cloudflare Quick Tunnelを起動してURLを取得・保存するツール

使い方:
    python cloudflared_tunnel.py --url http://localhost:8501

必要なライブラリのインストール:
    pip install qrcode pillow
"""

import argparse
import subprocess
import re
import sys
import time
import os
import signal
from pathlib import Path

try:
    import qrcode
except ImportError:
    print("エラー: qrcodeライブラリがインストールされていません。")
    print("以下のコマンドでインストールしてください:")
    print("  pip install qrcode pillow")
    sys.exit(1)


# グローバル変数
cloudflared_process = None


def signal_handler(sig, frame):
    """シグナルハンドラ(Ctrl+C)"""
    print("\n")
    print("🛑 トンネルを停止しています...")
    if cloudflared_process:
        cloudflared_process.terminate()
        cloudflared_process.wait(timeout=5)
    print("✅ トンネルを停止しました")
    sys.exit(0)


def parse_arguments():
    """コマンドライン引数を解析"""
    parser = argparse.ArgumentParser(
        description='Cloudflare Quick Tunnelを起動してURLを取得・保存します',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用例:
  %(prog)s --url http://localhost:8501
  %(prog)s --url http://localhost:5000 --output-dir ./output

出力ファイル:
  URL.txt  - 生成されたトンネルのURL
  QR.png   - URLのQRコード画像

停止方法:
  Enter キーを押す、または Ctrl+C
        """
    )
    
    parser.add_argument(
        '--url',
        required=True,
        help='公開するローカルアプリのURL (例: http://localhost:8501)'
    )
    
    parser.add_argument(
        '--output-dir',
        default='.',
        help='出力ディレクトリ (デフォルト: カレントディレクトリ)'
    )
    
    parser.add_argument(
        '--url-file',
        default='URL.txt',
        help='URLを保存するファイル名 (デフォルト: URL.txt)'
    )
    
    parser.add_argument(
        '--qr-file',
        default='QR.png',
        help='QRコード画像のファイル名 (デフォルト: QR.png)'
    )
    
    return parser.parse_args()


def extract_tunnel_url(output_line):
    """cloudflaredの出力からトンネルURLを抽出"""
    # https://xxxxx.trycloudflare.com の形式のURLを探す
    url_pattern = r'https://[a-z0-9\-]+\.trycloudflare\.com'
    match = re.search(url_pattern, output_line)
    if match:
        return match.group(0)
    return None


def start_cloudflared_tunnel(local_url):
    """cloudflared tunnelを起動してURLを取得"""
    global cloudflared_process
    
    print(f"cloudflared tunnel を起動中...")
    print(f"ローカルURL: {local_url}")
    print("トンネルURLの取得を待機中...\n")
    
    try:
        # cloudflaredを起動
        cloudflared_process = subprocess.Popen(
            ['cloudflared', 'tunnel', '--url', local_url],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
            bufsize=1
        )
        
        # 出力を読み取ってURLを探す
        tunnel_url = None
        timeout = 30  # 30秒タイムアウト
        start_time = time.time()
        
        for line in cloudflared_process.stdout:
            print(line.rstrip())  # 出力を表示
            
            # URLを抽出
            url = extract_tunnel_url(line)
            if url:
                tunnel_url = url
                print(f"\n✅ トンネルURL取得成功: {tunnel_url}\n")
                break
            
            # タイムアウトチェック
            if time.time() - start_time > timeout:
                print(f"\n⚠️ タイムアウト: {timeout}秒以内にURLを取得できませんでした")
                cloudflared_process.terminate()
                return None
        
        if not tunnel_url:
            print("\n⚠️ トンネルURLを取得できませんでした")
            cloudflared_process.terminate()
            return None
        
        print("📝 cloudflaredプロセスが起動しました")
        print(f"   プロセスID: {cloudflared_process.pid}\n")
        
        return tunnel_url
        
    except FileNotFoundError:
        print("エラー: cloudflaredコマンドが見つかりません")
        print("cloudflaredがインストールされていることを確認してください")
        print("インストール方法: https://github.com/cloudflare/cloudflared")
        return None
    except Exception as e:
        print(f"エラー: cloudflaredの起動に失敗しました - {e}")
        return None


def save_url_to_file(url, file_path):
    """URLをテキストファイルに保存"""
    try:
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(url + '\n')
        print(f"✅ URLをファイルに保存しました: {file_path}")
        return True
    except Exception as e:
        print(f"⚠️ URLファイルの保存に失敗しました: {e}")
        return False


def generate_qr_code(url, file_path):
    """URLからQRコードを生成して画像ファイルに保存"""
    try:
        # QRコードを生成
        qr = qrcode.QRCode(
            version=1,  # 自動調整
            error_correction=qrcode.constants.ERROR_CORRECT_L,
            box_size=10,
            border=4,
        )
        qr.add_data(url)
        qr.make(fit=True)
        
        # 画像を生成
        img = qr.make_image(fill_color="black", back_color="white")
        
        # ファイルに保存
        img.save(file_path)
        print(f"✅ QRコードを生成しました: {file_path}")
        return True
    except Exception as e:
        print(f"⚠️ QRコードの生成に失敗しました: {e}")
        return False


def main():
    """メイン処理"""
    # シグナルハンドラを登録
    signal.signal(signal.SIGINT, signal_handler)
    
    args = parse_arguments()
    
    # 出力ディレクトリの作成
    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # 出力ファイルのパス
    url_file_path = output_dir / args.url_file
    qr_file_path = output_dir / args.qr_file
    
    print("=" * 60)
    print("Cloudflare Quick Tunnel URLキャプチャツール")
    print("=" * 60)
    print()
    
    # cloudflared tunnelを起動してURLを取得
    tunnel_url = start_cloudflared_tunnel(args.url)
    
    if not tunnel_url:
        print("\n❌ トンネルURLの取得に失敗しました")
        sys.exit(1)
    
    # URLをファイルに保存
    save_url_to_file(tunnel_url, url_file_path)
    
    # QRコードを生成
    generate_qr_code(tunnel_url, qr_file_path)
    
    print()
    print("=" * 60)
    print("✅ 処理完了")
    print("=" * 60)
    print(f"トンネルURL: {tunnel_url}")
    print(f"URLファイル: {url_file_path}")
    print(f"QRコード:   {qr_file_path}")
    print()
    print("💡 このURLを共有してアクセスしてもらってください")
    print("=" * 60)
    print()
    print("📌 トンネルは実行中です")
    print(f"   プロセスID: {cloudflared_process.pid}")
    print()
    print("⏸️  停止するには Enter キーを押してください")
    print("   (または Ctrl+C)")
    print()
    
    try:
        # キー入力待ち
        input()
    except KeyboardInterrupt:
        # Ctrl+Cの場合はシグナルハンドラが処理
        pass
    
    # トンネルを停止
    print("\n🛑 トンネルを停止しています...")
    if cloudflared_process:
        cloudflared_process.terminate()
        try:
            cloudflared_process.wait(timeout=5)
            print("✅ トンネルを停止しました")
        except subprocess.TimeoutExpired:
            cloudflared_process.kill()
            print("✅ トンネルを強制停止しました")
    print("=" * 60)


if __name__ == '__main__':
    main()

プログラムの実行

$ python cloudflared_tunnel.py --url http://localhost:8501

3. アクセス制限はかけられない

URLを知っていれば誰でもアクセスできる。手軽さの裏返しで、セキュリティ面では注意が必要。 短時間のデモなら問題ないが、機密情報を扱う場合は注意が必要。アプリ側で認証を実装するか、別の方法(永続トンネル + Cloudflare Access)を検討する必要がある。

今回の実験で気付いたこと 複数のトンネルを起動が可能!

最初、「1つのトンネルで複数のアプリを公開できないのか?」と思っていたけど、別のターミナルで別のトンネルを起動すればOKだった。つまり、複数のアプリを同時に公開できる。

# ターミナル1
$ streamlit run app1.py &
$ cloudflared tunnel --url http://localhost:8501

# ターミナル2
$ python -m http.server 8000 &
$ cloudflared tunnel --url http://localhost:8000

それぞれ異なるURLが生成される。当たり前のことだが、最初気づかなかったけど、複数のアプリを同時に公開できるのは便利。

おわりに

Cloudflare Quick Tunnelsを実際に使ってみた感想としては以下のようになるでしょうか。

良かったこと

  • 時間制限がなく、安心してデモできる
  • WSL環境でもそのまま使える
  • 複数のトンネルを同時に起動できる
  • HTTPSでアクセスできる

注意すること

  • URLが毎回変わる(再起動で変わる)
  • アクセス制限はかけられない
  • URLが長くて覚えにくいかも

向いている用途

  • 授業・ワークショップでのデモ
  • 一時的なプレビュー共有
  • PoCシステムの発表

向いていない用途

  • 長期運用(固定URLが必要)
  • 機密情報を扱うシステム(認証が必要)

個人的にはngrokの「面倒な部分」(アカウント登録、2時間制限)を解消してくれるサービスといえるかな。 デモで「ちょっと見せたい」には最適。ただし、本格的な運用には向かないので、用途に応じて使い分けるのが良さそう。

もし、固定URLが必要になったら、cloudflareにある永続トンネル(ドメイン必要)を検討するのも良いかも。 ただ、ドメインが必要なので、ドメインの管理コストも必要です。

参考リンク

github.com

developers.cloudflare.com