【第6回】API破産を防ぐ多段防御!PHPとFail2banで作るレート制限とエラーハンドリング

アプリ公開前夜の悪夢。「API破産」のリスクを知る

こんにちは!「LINUX工房」管理人の「リナックス先生」です。
前回の第5回までで、テキスト生成から画像生成、そしてデータベース連携まで、AI Webアプリケーションの「機能」はほぼ完成しました。
いよいよインターネットに公開して、世界中のユーザーに使ってもらう準備が整った……と言いたいところですが、ここでサーバーサイドエンジニアとしての最大の試練が待ち受けています。

コウ君

先生!ついに僕のオリジナルAIアプリが完成しました!
今からX(旧Twitter)にURLを貼って、みんなに無料で使ってもらおうと思います!バズるといいなぁ〜。

リナックス先生

ちょっと待ちなさい!!そのまま公開したら、明日には請求書を見て泣くことになるわよ!!
もし悪意のあるプログラム(Bot)があなたのアプリを見つけて、1秒間に100回のペースでAIに質問を投げ続けたらどうなると思う?
あなたのAPIキーを使って、あなたのクレジットカードから数十万円が自動的に引き落とされるのよ。「API破産」を防ぐための防御壁を作らない限り、公開は絶対に禁止よ!

本記事では、AlmaLinux 9上に構築したLAMP環境をベースに、APIへの過剰なアクセスを制御する「レート制限(Rate Limiting)」の実装と、AI特有の遅延や障害に対応する堅牢な「エラーハンドリング」の手法を徹底解説します。


1. 多段防御アーキテクチャの設計思想

WebサービスにおけるAPIの保護は、1つの仕組みだけに依存するのは危険です。プロの現場では「アプリケーション層」「インフラ(OS/ネットワーク)層」の2段構えで防御します。

  1. アプリケーション層(PHP + MySQL)の防御:
    「1ユーザー(または1IP)につき、1分間に5回までしかAIを利用できない」といった、きめ細やかなビジネスロジックを実装します。制限を超えたユーザーには「しばらく待ってから再度お試しください」という親切なメッセージを返します。
  2. インフラ層(AlmaLinux 9 + Fail2ban)の防御:
    PHPの制限を無視して、1秒間に何十回も無差別にアクセスしてくる攻撃者(DDoS攻撃や悪質Bot)は、PHPを起動してMySQLに問い合わせる処理自体がサーバー(Apache)の負荷を高めます。そこで、OSレベルのセキュリティツール「Fail2ban」を使い、ファイアウォール(firewalld)でIPアドレスごと遮断します。

この強固な要塞を、一つずつ構築していきましょう。


2. アプリケーション層:PHPとMySQLによるレート制限

まずはMariaDBに、アクセス元IPごとの利用回数を記録するテーブルを作成します。

レート制限用テーブルの作成

USE ai_chat_db;

CREATE TABLE api_rate_limits (
    ip_address VARCHAR(45) PRIMARY KEY, -- IPv4/IPv6両対応
    request_count INT DEFAULT 1,
    last_request_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

PHPでのレート制限ロジック

APIを呼び出す直前のPHPファイル(例:api_chat.php)の先頭に、以下のチェックロジックを挿入します。今回は「1分間に5回まで」という制限を設けます。

<?php
require_once 'db_connect.php';

// クライアントのIPアドレスを取得
$client_ip = $_SERVER['REMOTE_ADDR'];
$limit_count = 5;       // 制限回数
$time_window = 60;      // 制限時間(秒)

// 1. IPアドレスのアクセス記録を取得
$stmt = $pdo->prepare("SELECT request_count, UNIX_TIMESTAMP(last_request_time) as last_time FROM api_rate_limits WHERE ip_address = ?");
$stmt->execute([$client_ip]);
$record = $stmt->fetch();

$current_time = time();

if ($record) {
    // 記録がある場合
    $time_passed = $current_time - $record['last_time'];

    if ($time_passed < $time_window) {
        // 制限時間内のアクセス
        if ($record['request_count'] >= $limit_count) {
            // レート制限超過!
            
            // 【重要】後述するFail2banで検知させるため、Apacheのエラーログに特定の書式で書き出す
            error_log("[AI_API_RATE_LIMIT] IP: {$client_ip} exceeded the limit.");
            
            header("HTTP/1.1 429 Too Many Requests");
            echo json_encode(["error" => "アクセスが集中しています。1分後に再度お試しください。"]);
            exit;
        } else {
            // 制限回数内ならカウントアップ
            $stmt = $pdo->prepare("UPDATE api_rate_limits SET request_count = request_count + 1 WHERE ip_address = ?");
            $stmt->execute([$client_ip]);
        }
    } else {
        // 制限時間を過ぎていればカウントをリセット
        $stmt = $pdo->prepare("UPDATE api_rate_limits SET request_count = 1, last_request_time = CURRENT_TIMESTAMP WHERE ip_address = ?");
        $stmt->execute([$client_ip]);
    }
} else {
    // 初回アクセスの場合はレコードを作成
    $stmt = $pdo->prepare("INSERT INTO api_rate_limits (ip_address, request_count) VALUES (?, 1)");
    $stmt->execute([$client_ip]);
}

// ----------------------------------------------------
// ここから下に、通常のAI API呼び出し(cURL)処理を書く
// ----------------------------------------------------
?>

これで、ブラウザのリロードを連打するようなアクセスからはAPIキーの課金を守ることができるようになりました。


3. インフラ層:Fail2banによる悪意あるIPの自動ブロック

先ほどのPHPコードで重要なのは、制限を超えた際に error_log() 関数を使って Apacheのエラーログ(/var/log/httpd/error_log)に特定のメッセージを出力している 点です。
AlmaLinux 9の強力な防御ツールである「Fail2ban」にこのログを監視させ、悪質なIPをサーバーレベル(firewalld)で完全遮断させます。

コウ君

PHPでブロックしてるのに、さらにFail2banを使うんですか?

リナックス先生

そうよ。PHPが起動してMySQLにアクセスするだけでも、メモリとCPUを消費するわ。攻撃者が1秒間に1000回リクエストしてきたら、AIの課金は守れても、今度はApacheがパンクしてサーバーが落ちてしまうの。
Fail2banでIPをブロックすれば、Apacheに届く前にパケットを破棄できるから、サーバー自体を守ることができるのよ。

Fail2banのカスタムフィルタ作成

まず、Fail2banに「何を検知するか」を教えるフィルタファイルを作成します。

sudo nano /etc/fail2ban/filter.d/ai-api.conf

以下の内容を記述します。PHPの error_log() で出力した形式にマッチする正規表現です。

[Definition]
# Apacheのエラーログから [AI_API_RATE_LIMIT] IP: xxx.xxx.xxx.xxx という文字列を検知
failregex = \[AI_API_RATE_LIMIT\] IP: <HOST> exceeded the limit\.
ignoreregex =

jailの設定と有効化

次に、このフィルタを使って「何度検知したら、何分間ブロックするか」を jail.local に追記します。

sudo nano /etc/fail2ban/jail.local
# 以下のブロックをファイルの末尾に追記
[ai-api]
enabled = true
port    = http,https
filter  = ai-api
logpath = /var/log/httpd/error_log
# 10分間に3回検知(つまり3回レート制限に引っかかったら)
findtime = 600
maxretry = 3
# 1時間(3600秒)IPアドレスをブロック
bantime = 3600

設定を保存し、Fail2banを再起動します。

sudo systemctl restart fail2ban
# 状態確認
sudo fail2ban-client status ai-api

これで、しつこくアクセスしてくるBotは、数回エラーを出した時点でサーバーから完全に遮断(アクセス不可)されます。AlmaLinux 9ならではの、非常に強力で実践的なセキュリティ設定です。


4. 予測不能なAI APIのエラーハンドリング(タイムアウト対策)

防御の次は、AI側の不具合に対する備えです。
生成AIのAPIは、複雑な推論を行うため、レスポンスが返ってくるまでに10秒〜30秒かかることも珍しくありません。また、AI提供元のサーバーがダウンして「500 Internal Server Error」を返してくることもあります。

これを適切に処理しないと、ユーザーの画面が真っ白なまま固まったり、不要なエラーメッセージが表示されたりします。

cURLの堅牢なエラーハンドリング実装

第3回などで作成したcURLの処理を、以下のように強化します。

<?php
// APIへのリクエスト設定(一部抜粋)
$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $api_key
]);

// 【必須設定】タイムアウトを明示的に設定する
// 接続自体にかかる時間の最大値(秒)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
// 応答を待つ最大時間(秒)。AIの処理を考慮して長めに設定。
curl_setopt($ch, CURLOPT_TIMEOUT, 60); 

$response = curl_exec($ch);

// cURL自体のエラー(タイムアウトやネットワーク切断など)をキャッチ
if(curl_errno($ch)){
    $error_msg = curl_error($ch);
    error_log("[AI_API_CURL_ERROR] " . $error_msg);
    header("HTTP/1.1 504 Gateway Timeout");
    echo json_encode(["error" => "AIサーバーからの応答がタイムアウトしました。しばらく経ってからお試しください。"]);
    curl_close($ch);
    exit;
}

$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// HTTPステータスコードに応じた分岐
if ($http_code === 200) {
    // 正常処理
    $result = json_decode($response, true);
    echo json_encode(["reply" => $result['choices'][0]['message']['content']]);
    
} elseif ($http_code === 401) {
    error_log("[AI_API_AUTH_ERROR] APIキーが無効か期限切れです。");
    echo json_encode(["error" => "システムエラーが発生しました。(管理者に連絡してください)"]);
    
} elseif ($http_code === 429) {
    // AI提供元側のレート制限(Quota exceedなど)
    error_log("[AI_API_PROVIDER_LIMIT] 提供元のAPI上限に達しました。");
    echo json_encode(["error" => "AIが混み合っています。少し時間をおいてください。"]);
    
} else {
    // その他の500エラーなど
    error_log("[AI_API_UNKNOWN_ERROR] HTTP Status: {$http_code}, Response: {$response}");
    echo json_encode(["error" => "AIサーバーで一時的な障害が発生しています。"]);
}
?>

ポイントは、「詳細なエラーはサーバーの error_log に書き込み、ユーザーには『一時的な障害』など当たり障りのないメッセージを返す」ことです。
APIキーのエラーなどをそのままブラウザに表示してしまうのは、セキュリティリスクを高める素人実装の典型例ですので絶対に避けましょう。


総まとめ:セキュリティは「公開前」に実装すべし

第6回の解説、お疲れ様でした。

今回は、PHPとMySQLを使ったアプリケーション層のレート制限、Fail2banを活用したインフラ層のIP遮断、そしてcURLの堅牢なエラーハンドリングについて解説しました。
これらはアプリの「見た目」には一切現れない地味な作業ですが、商用環境としてサービスを安全に継続させるための「エンジニアの命綱」です。AlmaLinux 9の強固な基盤と組み合わせることで、エンタープライズレベルの安全性が手に入ります。

次回、【第7回】バッチ処理とCronによるAIタスクの完全自動化 では、夜中にAIに長時間のデータ分析や記事の要約を自動でやらせるための「バックグラウンド処理」の仕組みを解説します。
お楽しみに!LINUX工房の「リナックス先生」でした。

▼ 安全なAIアプリを運用するなら ▼

Fail2banなどの細かな設定が自由自在!
「ルート権限付きVPS」

VPSランキングを見る

セキュリティ設計ができるエンジニアへ
「IT専門の転職エージェント」

転職エージェントを見る

コメント