🧐

「SQLiteは本番で使えない」は本当か? 未経験がRails 8 × 格安VPSで負荷試験を乗り越え、PHP学習アプリをリリースしました!

に公開

初めまして、sekitoと申します。

この度、PHP8技術者認定初級試験の受験者に向けた学習サポートアプリ、『PHP8技術者認定初級試験スタディ』をリリースしました。

この記事でお伝えしたいのはアプリの宣伝だけではありません。

このアプリは、月額800円のVPSとSQLiteという、一般的には「本番向きではない」とされる構成で動いています。しかし、負荷試験では 同時接続150人(秒間数百リクエスト)をエラー率0% で捌き切りました。

「SQLiteは本番では使えない」 「個人開発でもDBサーバーは分けるべき」

そんな常識に、Rails 8 と Kamal は立ち向かえるのか。未経験エンジニアが泥臭く検証した、 「技術とコストの戦いの記録」 も共有したいと思っています。

ランディングページのスクリーンショット

URL

https://php-study.jp

概要

PHP8技術者認定初級試験スタディは、その名の通り「PHP8初級試験の合格」に特化した学習アプリです。

公式教材を参考に独自作成した解説付きの200問を超える問題プールからのランダム出題機能にくわえて本番試験と同様の配分で出題される模擬試験モードを搭載。さらに、模擬試験の結果を記録する機能も備えており、合格までの道のりを伴走する環境を提供します。
また、レスポンシブ対応済みなので、日々の隙間時間にご活用いただけます。

スクリーンショット

問題画面 解説画面
問題画面のスクリーンショット 解説画面のスクリーンショット
採点結果画面 再開確認画面
採点結果のスクリーンショット 再開確認画面のスクリーンショット

なぜ作ったのか

答えを覚えてしまうことへの弊害と、理想の学習環境への渇望

きっかけは、私自身が未経験からエンジニアを目指して学習を進める中で感じた既存の学習サービスに対する、あと一歩の物足りなさでした。

私の住む北海道では、Web開発においてPHPを採用している企業が多く、転職の幅を広げるためにはRubyだけでなく、PHPのキャッチアップも必要だと考えていました。 そこでまずは基礎固めとして初級資格の学習を始めましたが、既存のサービスでは出題セットが固定されているケースが多くありました。これでは繰り返し解くうちに「理解した」のではなく「答えの場所を覚えてしまった」という状態に陥りやすく、本質的な学習効果が得られないことに強い危機感を覚えました。

また、これまで数々の学習サイトにお世話になってきましたが、答えの丸暗記を防ぐためのランダム出題や、 思考を邪魔しない、ノイズのない直感的なUI、こうした要素を兼ね備えた、心から使いやすいと思えるサービスに出会うことは稀でした。

ないなら自分で作ればいい。自分が一番欲しいと思えるツールを作ろう

そう決意したのがすべての始まりです。 しかし開発を進める中で、その動機は自分自身のためから、次第に未来のエンジニア仲間のためへと変化していきました。

エンジニアを目指す同志たちが、学習の本質とは無関係な使いにくさで時間を浪費するのはあまりに惜しい。
だからこそ、現時点で私が提供できる全てを注ぎ込み、最短距離で知識の獲得を目指せる環境を作ることにしました。

PHPの学習アプリでありながら、バックエンドにあえて私の得意とする Rails を採用したのも、ユーザー体験を最大化するための必然的な選択でした。

開発における3つのテーマ

開発にあたり、単に機能を作るだけでなく、以下の3つの軸を最優先事項としました。

1. ユーザー体験の追求

既存の学習サイトで私が最もストレスを感じていたのは、「試験中にブラウザを閉じると終了扱いになる」「前の問題に戻った時に自分の解答が分からない」といった、システムの制約による理不尽さでした。

もちろん、これらは一例に過ぎません。
私はこうした「ユーザーの事情よりもシステムの都合が優先されている仕様」が、学習の至る所に存在していることに常に違和感を抱いていました。

だからこそ本アプリでは、そうしたあらゆる「使いにくさ」を徹底的に排除することにこだわりました。
中断・再開機能や、柔軟な回答確認機能の実装はその現れです。学習者がシステムに合わせるのではなく、システムが学習者の行動に寄り添う。この当たり前の快適さを、細部に至るまで追求しました。

心理的ハードルを下げるお試し問題と直通フィードバック

どれほど機能が優れていても、利用開始のハードルが高ければ使ってもらえません。
そこで、まだ価値の分からないサービスにアカウントを連携する心理的抵抗を減らすため、会員登録なしで5問だけ即座に解けるお試しモードを実装しました。
まずはアプリの価値を体験してもらい、納得した上で登録してもらう動線を作ることで、離脱率の低下を狙っています。

また、学習中の違和感やバグを即座に報告できるよう、フッターと各問題解説画面に開発者直通のフィードバックボタンを設置しました。 入力された内容は Discord Webhook を通じて即時通知され、私がリアルタイムで内容を確認・修正できるサイクルを回しています。 これは単なるバグ報告機能ではなく、ユーザーと共にアプリを育てていくという姿勢の実践です。

技術Note: 複数コントローラーに跨る制限ロジックの共通化

お試し制限は「問題画面」と「解説画面」の両方にかける必要があります。
そこで、共通のフック処理を Concern として切り出し、さらに回数判定などの複雑なロジックは PORO へと委譲する設計にしました。

app/controllers/concerns/guest_trial_limitable.rb
# 複数のコントローラーで include して使用
module GuestTrialLimitable
  extend ActiveSupport::Concern

  private
    def check_guest_trial_limit
      # Sessionの操作や回数判定のロジックは GuestTrialSession に隠蔽する
      trial_session = GuestTrialSession.new(session, current_user)

      unless trial_session.allow?(params[:id] || params[:question_id])
        redirect_to root_path, alert: "お試し版は#{GuestTrialSession::LIMIT}問までです。会員登録して続きを学習しましょう!"
      end
    end
end

Concernは依存関係が複雑になりがちですが、ここではあくまでbefore_actionの定義という役割に徹し、実際のビジネスロジックを独立したクラスに切り出すことで、保守性とテスト容易性を担保しています。

2. 堅牢、かつ低コストなインフラ

実は、私は前職で水道インフラに関わる土木作業員をしていました。
「インフラは止まってはいけない」「見えない部分こそ頑丈に作る」
現場仕事で培ったこの責任感は、Webエンジニアを目指す上でも私の根底にあります。

個人開発において、サービス終了の最大の要因はランニングコストです。しかし、コストを下げるために信頼性を犠牲にし、データが消えたり頻繁に落ちたりするようでは、水道屋としてのプライドが許しません。
そこで今回は、「個人開発=不安定・短命」というイメージを技術選定で覆すことに挑みました。

Rails 8 の新機能を活用した SQLite + Litestream 構成は、AWSのような従量課金による「クラウド破産」のリスクを回避しつつ、サーバーデータ消失時でも直前の状態にデータを復元できる「Point-in-Time Recovery」を実現しています。

結果として、私のこだわりである「高耐久かつ堅牢なインフラ」を、VPS代のみの月額わずか800円程度(Cloudflare R2は無料枠内)で実現しました。

安かろう悪かろうではなく、コストと信頼性を両立させること。これこそが持続可能な運営の鍵だと考え、このインフラを構築しました。

同時接続とロック競合への懸念について

SQLiteを採用する上で懸念されがちなのが書き込み時のロック競合ですが、本アプリケーションにおいては以下の理由から問題にならないと判断しました。

  1. Rails 8 のデフォルト設定 (WALモード)
    Rails 8 ではデフォルトで WAL (Write-Ahead Logging) モードが有効化されています。これにより、以前のSQLiteとは異なり「読み込み」と「書き込み」が互いをブロックせず並行処理されるため、ユーザー体験を損ないません。

  2. IMMEDIATE トランザクションによるデッドロック回避
    従来のRailsでは、書き込みの瞬間にロックを取得しようとしてデッドロックが発生しがちでした。
    しかし Rails 8.0 からはデフォルトで IMMEDIATE モードでトランザクションが開始されるよう変更されました。これにより、書き込み意図のある処理は入り口で整列され、途中で詰まることが構造的に防がれています。

  3. 「思考時間」によるリクエストの分散
    模擬試験モードでは回答ごとに書き込みが発生しますが、学習アプリの特性上、ユーザーには問題を読んで考える思考時間が必ず発生します。常に書き込みが発生するようなチャットアプリなどとは異なり、リクエストは自然と分散されます。結果として、DBのワークロードはSQLiteが得意とする読み込み主体の状態に落ち着くと判断しました。

これらを踏まえ、現段階では過剰なスペックとなるクライアントサーバー型のRDBMS(PostgreSQLなど)ではなく、アプリケーションの特性にフィットしたSQLiteを選定しました。

とはいえ、これらはあくまで机上の計算に過ぎません。実際にユーザーが殺到した際にシステムが破綻しないという確証を得るため、以下の負荷試験を実施しました。

負荷試験による性能検証:SQLiteは書き込みに耐えられるのか?

「読み込みは速くても、書き込みが直列になるSQLiteでは、アクセスが集中するとロックエラーが起きるのではないか?」
この懸念を払拭するため、実際の試験利用を想定した「読み込み(出題)」と「書き込み(回答)」が交互に発生するシナリオに加え、CDNやキャッシュを完全に無効化した過酷な条件下での負荷試験を実施しました。

検証条件(限界突破テスト)

  • 同時接続数 150ユーザー
  • 検証環境 本番サーバー(VPS / Rails 8 / SQLite / 2vCPU / 1GB RAM)
  • 通信経路 CloudflareなどのCDNをバイパスし、VPSへ直接リクエストを送信
  • シナリオ 単なるトップページの閲覧だけではSQLiteの限界は測れません。
    そこで、150人が3分間、休みなく問題を解き、解答履歴をDBに書き込み続けるという、SQLiteにとって最も不利な状況をあえてシミュレートしました。
  • 負荷の再現 現状のアプリケーションロジックの実測値は数ms程度ですが、将来的な機能追加による処理時間の増加を見越し、意図的に sleep(0.05) をトランザクション内に挿入しました。 これにより、本来の処理よりも遥かにロック拘束時間が長い状況をシミュレートし、それでも捌き切れるかという安全性を検証しています。
🔍 実行した負荷試験スクリプト (k6)

CDN(Cloudflare)を回避するためにVPSのIPアドレスへ直接リクエストを送信し、Hostヘッダ偽装やキャッシュバスターの実装を行うことで、キャッシュ層が一切介在しない生のDB性能を計測しました。

import http from 'k6/http';
import { check, sleep } from 'k6';

// 150人が同時に試験を受けている高負荷状態をシミュレート
export const options = {
  insecureSkipTLSVerify: true, // IP直打ちのため証明書エラーを無視
  // DNSを無視してVPSの生IPへ強制接続(Cloudflare回避)
  hosts: { 'php-study.jp': 'xxx.xxx.xxx.xxx' },
  stages: [
    { duration: '30s', target: 150 }, // 30秒かけて一気に150人まで増員
    { duration: '3m',  target: 150 }, // 3分間、回答し続ける(耐久試験)
    { duration: '30s', target: 0 },   // 試験終了
  ],
};

export default function () {
  const queryParam = `?_t=${Date.now()}_${Math.random()}`; // キャッシュバスター

  // 1. 問題を表示 (Read / キャッシュなし)
  const resRead = http.get(`https://php-study.jp/load_test/read${queryParam}`);
  check(resRead, { 'Read OK': (r) => r.status == 200 });

  // 2. 思考時間 (平均1.5秒)
  sleep(Math.random() * 1 + 1);

  // 3. 回答を送信 (Write / トランザクション内で0.05秒の待機ハンデあり)
  const resWrite = http.post(`https://php-study.jp/load_test/write${queryParam}`);
  check(resWrite, { 'Write OK': (r) => r.status == 200 });

  sleep(0.5); 
}
測定結果

K6による負荷テスト結果

項目 結果 評価
リクエスト成功率 100% (23,686回) Read/Write共にエラーゼロ
95%タイル値 (p95) 501.73 ms 150人同時利用でも、95%のレスポンスは0.5秒程度で完了
最大レスポンスタイム 900.42 ms 意図的な遅延(50ms)を入れているにも関わらず、詰まりはほぼ発生せず
CPU使用率 Max 18% 平時は0-1%なのである程度の負荷は認められるが、余力あり
メモリ使用率 約 480MB / 1GB 平常時とほぼ変化なし(約20MB増加)

※ ハードウェアリソースに関してはVPS側でhtopを利用し、監視しました。

考察:なぜここまで速いのか?

特筆すべきは、最大レスポンスタイムが 1秒 を切っている点です。
Rails 8 のコネクションプールとSQLiteのWALモードが最適に機能し、行列をほとんど作らずにリクエストを消化し続けられることが確認できました。

以前のSQLiteであれば書き込みロックで詰まっていたであろう場面でも、Rails 8 の構成では驚くほどスムーズに並列処理が行われています。

🔍 技術Note: 負荷試験モデルの選定について(Closed Model vs Open Model)

本試験では k6 の標準的な executor を使用した Closed Model(ユーザーがレスポンスを待ってから次の行動に移るモデル)を採用しています。

厳密なサーバーの限界性能を測定するには、レスポンス遅延に関わらず一定のリクエストを送り続ける Open Model が推奨される場合があります。
しかし、本アプリのような学習サービスにおいては、「画面が表示されないのに次の問題を解こうとするユーザー」は存在しません。

今回は「150人が学習を継続できるか」という ユーザー体験の検証 を目的としているため、現実のユーザー行動に近い Closed Model での検証が妥当であると判断しました。
なお、結果として最大レイテンシが1秒未満に収まっているため、Closed Model 特有の「遅延による負荷の意図せぬ低下」の影響は軽微であると考えられます。

結論

150人が休みなく高速で問題を解き続けるという、実際の運用ではほぼあり得ないであろう過酷なシナリオを課しましたが、システムは一度も破綻することなく完走しました。

150人同時接続という過酷な条件下でもエラー率0%を維持できたことから、Rails 8 × SQLite × 格安VPS という構成は、一般的なWebアプリケーションにおいて極めて実用的な選択肢であることが実証されました。

月額800円というミニマムなリソースであっても、技術選定と設定次第で、ここまでのパフォーマンスと信頼性を引き出せるという事実は、大きな収穫でした。

3. コミュニティへの還元としての「他者が読めるコード」

このアプリのソースコードは、単なるポートフォリオ以上の意味を持っています。それは、私が所属するフィヨルドブートキャンプのコミュニティへの還元です。

私自身、学習中には先輩たちが書いたコードを読み、そこから多くの設計思想を学びました。だからこそ、今度は私が「後に続く仲間たちが参考にしたくなるコード」を書く番だと考えました。

「未来のエンジニア仲間(他者)に向けて、意図が伝わるように書く」

この意識は、そのまま実務におけるチーム開発の要諦でもあります。
自分だけが読めればいい実装を避け、Railsのレールに徹底して乗ること。そして ViewComponent のようにインターフェースを明確にする設計を採用したのも、コードという共通言語を通じて、コミュニティの仲間や未来のチームメンバーと円滑にコミュニケーションを取るためです。

私のコードが、学習中の誰かの「生きた教科書」となり、ひいてはそれが将来のチームへの貢献にも繋がると信じて丁寧に実装しました。

技術スタックと選定意図

Rails 8 を採用し、PaaSを使わずにKamalを用いてVPSへデプロイしています。
流行りだから使うのではなく、解決したい課題に対して最適な道具を選ぶことを心がけ、以下の構成を採用しました。

Category Selection Decision Record (Why)
Framework Rails 8.1.1 私自身が最も生産性を発揮でき、かつRedis等の複雑なミドルウェア構成を必要とせず、SQLite一本で完結するシンプルなインフラを構築できるため。
Database SQLite 3.51.1
(+ Litestream)
読み込み主体の学習アプリであり、WALモードで十分な性能が出せると判断。LitestreamによるS3(R2)へのリアルタイムレプリケーションで、低コストにPITRを実現するため。
Frontend Hotwire
(Turbo / Stimulus)
フロントエンドとバックエンドの分離によるAPI開発コストを排除するため。HTMLをそのまま配信するSPA風の挙動により、開発速度とUXを両立させました。
UI Architecture ViewComponent 複雑な試験画面や管理画面のUIロジックをカプセル化し、変更に強く単体テストが容易な堅牢なコンポーネント設計にするため。
CSS Tailwind CSS クラス名の命名コストをゼロにし、デザインの修正サイクルを高速化するため。ViewComponentと組み合わせることで、CSSの肥大化も防いでいます。
Infrastructure Kamal + VPS
(Docker / Cloudflare)
クラウド破産(従量課金リスク)を回避し、月額固定費でスケール可能なインフラを確保するため。PaaSへのロックインを防ぎ、ポータビリティを重視しました。
Auth GitHub OAuth ターゲット層(エンジニア層)のアカウント所持率が高く、登録障壁を下げられるため。また、パスワード漏洩リスクをゼロにする「持たないセキュリティ」の実践でもあります。

コラム:なぜ「PHP学習アプリ」を「Rails」で作ったのか?

「PHPを学ぶアプリなら、Laravelで作るのが筋ではないか?」
そう思われる方も多いかもしれません。しかし、これには明確な戦略的意図があります。

DHHが提唱する「The One Person Framework 」という思想への共感です。

個人開発において最も枯渇しやすいリソースは「開発者の時間とモチベーション」です。
今回、私が提供したかった価値は「PHPの知識を網羅的に獲得できるクイズ」というコンテンツでした。この価値を最速でデリバリーするために、インフラからフロントエンドまでを一人で完結できるエコシステムとして、現時点で最も完成されている Rails 8 を選択しました。

  • Kamal によるインフラの自律(脱PaaS)
  • Solid Queue/Cache によるミドルウェアの排除(脱Redis)
  • Hotwire によるSPA風体験の低コスト実装

この「インフラ構成のシンプルさ」こそが、私が学習コンテンツの作成に集中するための最大の武器となりました。
結果として、「Railsという巨人の肩に乗ることで、PHPという言語の学習環境を再発明する」という、ユニークなアプローチが実現できたと考えています。

🛠️ 技術Note: Kamalの設定ファイルは驚くほどシンプル

Kamalなら たった1つのYAMLファイル でサーバー構成を定義できます。

以下は実際に使用している config/deploy.yml の主要部分です(秘匿情報は環境変数化しています)。

service: php8-study
image: sekito1107/php8-study-app

servers:
  web:
    - <%= ENV['SERVER_IP'] %>

proxy:
  ssl: true
  host: php-study.jp
  app_port: 3000
  healthcheck:
    interval: 5
    timeout: 90

registry:
  server: ghcr.io
  username: sekito1107
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
    - LITESTREAM_ACCESS_KEY_ID
    - LITESTREAM_SECRET_ACCESS_KEY
    - LITESTREAM_ENDPOINT

ssh:
  user: ubuntu
  keys: ["~/.ssh/github_ssh"]

builder:
  arch: amd64

volumes:
  - "php8_study_storage:/rails/storage"

こだわった技術ポイント

認証における「持たないセキュリティ」の実践

ユーザー認証には OmniAuth を用いた GitHub 連携のみを採用しました。これには2つの意図があります。

  1. ターゲットへの最適化: 利用者をエンジニア(および志望者)に絞っているため、GitHubアカウントはほぼ全員が所持している前提としたこと。
  2. セキュリティリスクの最小化: パスワードなどのセンシティブな認証情報をDBで一切保持せず、取得情報もパブリックなプロフィール情報に限定しています。これにより、万が一の漏洩時におけるユーザーへの被害を最小限に抑える「持たないセキュリティ」を意識しました。

ViewComponent による「堅牢なUI設計」

Railsの標準的な render partial: は便利ですが、インスタンス変数が暗黙的に参照できてしまったり、必要な変数が何かがコード上で一目で分かりにくいという課題を感じていました。 そこで、UI部品には全面的に ViewComponent を採用しました。

特に気に入っているのは、コンポーネントがカプセル化指向である点です。 initialize メソッドで必要な引数を明記することで、「何が必要か」という契約がコード上で定義されます。曖昧なデータ渡しを排除し、渡された値だけで厳密にViewを構築するスタイルは、変更に強く、単体テストも容易です。

app/components/common/menu_card/component.rb
# 例: 汎用的なカードコンポーネント
class Common::MenuCard::Component < ViewComponent::Base
  # 必要な情報は引数として明示的に要求する
  def initialize(title:, description:, url:, theme: :default, icon:)
    @title = title
    @description = description
    # ...
  end
end

# 呼び出す側(View)で、必要なデータが一目瞭然になるのがメリットです
<%= render Common::MenuCard::Component.new(
  title: "模擬試験",
  description: "本番形式で問題を解きます",
  # ...
) %>

これにより、複雑になりがちな試験画面や管理画面のViewも、見通しの良いコードを保つことができました。

SQLite + Litestream による低コストかつ堅牢なインフラ

先述した「持続可能なインフラ」を実現するための具体的な構成がこちらです。

ここでは Rails 8 で標準サポートが強化された SQLite と、ストリーミングバックアップツール Litestream を組み合わせました。 Litestreamは、SQLiteの変更ログ(WAL)をリアルタイムでオブジェクトストレージ(今回はCloudflare R2)に転送します。 これにより、アプリケーションサーバーのディスクが吹き飛んでも、直前のデータを復旧できる構成を実現しました。 個人開発だからと信頼性を犠牲にするのではなく、技術選定によってコストと信頼性を両立させました。

インフラ構成図

データの復元検証と運用監視

バックアップは、リストアできて初めてバックアップと呼べる
この定説に従い、本番環境にて非破壊的な復元テストを実施しました。

具体的には、稼働中のサービスには影響を与えないよう、Litestreamを用いてR2上のバックアップデータを別名ファイルとして復元し、データの整合性を確認しました。

  1. 別名での復元実行:
    稼働中の production.sqlite3 とは別に、検証用として復元を実行。
    litestream restore -o /rails/storage/verification.sqlite3 /rails/storage/production.sqlite3
  2. データの整合性確認:
    復元されたDBを開き、直前に作成されたレコードが存在すること、および破損がないことを確認。
  3. 検証完了:
    確認後、検証用ファイルを削除。

このプロセスにより、万が一の障害時にもR2から確実にデータを引き出せることを、サービスを停止させることなく実証しています。

データの整合性を守るイミュータビリティへのアプローチ

模擬試験の結果を記録するアプリとして、絶対に避けたかったのがマスタデータの変更によって、過去の受験履歴の整合性が壊れることです。

よく「ECサイトで注文後に商品価格が変わっても、購入履歴の価格は変わってはいけない」と言われますが、学習アプリの試験履歴もこれと同じです。この「スナップショットとしてのデータ設計」を問題データ全体に適用しました。

  1. 使用中の問題が編集された場合、既存のレコードは更新せず論理削除して保存しておく(過去の履歴用)
  2. 修正内容は 「新しいレコード」 として新規作成する(これからの試験用)
app/models/question.rb
# Questionモデル: 変更の影響範囲をモデル自身が判断して処理を分岐
def safe_update(params)
  assign_attributes(params)
  return false unless valid?

  if in_use?
    # 使用中なら履歴を壊さないよう、別クラスに委譲して新バージョンを作成(既存レコードは不変)
    Question::Versioner.new(self, params).create_version!
  else
    # 未使用ならそのまま更新
    save! && self
  end
end

この「論理削除された過去バージョンの問題データ」に対しては、管理画面上からの編集・復元機能をあえて実装していません。
これは開発工数の削減だけでなく、「過去の試験結果の正当性を担保する」という目的において、人為的なミスによる過去データの改変をシステム的に防ぐためです。

開発で苦労した点

1. 個人開発の限界を超える「AI×人間」のコンテンツ制作

本アプリの核となる200問以上の問題文と、その解説データの用意は、個人開発において最も高いハードルの一つでした。
学習効果を高めるためには良質な問題に加え、「なぜ正解なのか」を説く解説文が不可欠ですが、これらを一人ですべてゼロから作成していてはリリースまで数ヶ月かかってしまいます。

そこで、生成AI(LLM)を「原案作成のパートナー」として採用しました。
しかし、AIの出力をそのまま使うことはありません。生成された問題をベースに、公式テキストと照らし合わせながら私が全件校閲・修正を行うことで、「AIによる圧倒的なスピード」と「人間の責任による品質担保」の両立を実現しました。

エンジニアの役割は、単にコードを書くことだけではありません。
使える技術は何でも使い、リソースの制約を乗り越えてユーザーに価値を届けること。今回の開発を通して、その重要性を強く再認識しました。

2. 公式ドキュメント通りにいかない? Cloudflare R2 と Litestream の接続

今回、最も時間を要したのはインフラ周りのトラブルシュートでした。
特に、データベースのバックアップ先として Cloudflare R2 を Litestream に接続する工程で、公式手順通りに設定しても動作しない(InternalError が返ってくる)という問題に直面しました。

エラーログを見ても原因が特定できず、最初は認証情報やネットワーク設定といった自分側のミスを疑いました。
そこで、以下の切り分けを行い、問題の所在を絞り込んでいきました。

  1. 認証情報の検証: AWS CLIでR2へアクセスすると成功する → 認証キーは正しい
  2. ネットワークの検証: サーバーから外部への疎通はできている → FWの問題ではない
  3. 設定の再鑑: 公式ドキュメントや類似の実装記事と何度照らし合わせても、記述に誤りが見当たらない

「環境(ネットワーク・認証)は正常、かつ設定も正しい」
この状況から、自分のミスではなく「Litestream自体、あるいは R2 との互換性に問題があるのではないか?」という仮説に至り、本家の Issues を調査しにいきました。

結果、以下のIssueにて、私が利用しようとしていたバージョン(v0.5.4以降)と R2 の組み合わせで発生する不具合報告を発見しました。

https://github.com/benbjohnson/litestream/issues/912

どうやら内部で使用されている AWS SDK のアップデートに伴う「チェックサム計算の挙動変更」が、R2 との互換性問題を引き起こしていたようです。
最終的には、このIssue内で議論されていた解決策を元にバージョンを調整することで無事動作させることができました。

エラーログとにらめっこするだけでなく、困ったら本家のIssueを見に行くという癖がついたのは、自分にとって大きな収穫でした。

著者について

sekito
元・水道土木作業員
現場仕事で培った体力と根性を武器に、2024年にプログラミング学習を開始。現在はフィヨルドブートキャンプにて、未経験からのWebエンジニア就職を目指して学習中です。

現在、地域を問わずWebエンジニアとして働ける環境を探しています。
本記事やアプリについて少しでも興味を持って頂けましたら、X(旧Twitter)のDM等でお気軽にお声がけください!

保有資格

  • AWS Certified Solutions Architect – Associate
  • Ruby Association Certified Ruby Programmer Silver
  • LPIC Level 1
  • 基本情報技術者試験 (FE)
  • 給水装置工事主任技術者

https://x.com/sekito1107

最後に

このアプリは、単なる学習ツールとしてだけでなく、エンジニアとしての私の設計思想を詰め込んだポートフォリオでもあります。

Rails 8 の新機能を活用した開発体験は非常に楽しく、またデータの整合性やインフラの選定といった足回りの重要性を再認識する良い機会となりました。

PHPの学習アプリでありながら、裏で動いているのは Rails。 一見すると矛盾した構成に見えるかもしれませんが、そこには私なりの明確な意図があります。

言語やフレームワークは、あくまでユーザーに価値を届けるための手段だと考えています。

今回は最高の学習体験を最速で届けるために、現時点で私が最もパフォーマンスを発揮できる Rails を迷わず選びました。
どのような技術スタックであれ、その環境でベストを尽くし、ユーザーの課題解決にコミットする。このアプリには、そんな私のエンジニアとしての姿勢も込めています。

3つのテーマで話させて頂いたユーザー体験についてはこだわった部分も多く、ここでは語りきれません! PHP初級試験を受ける予定の方も、そうでない方も、ぜひ一度触ってみてください! 皆さんから頂いたフィードバックでこのサービスが成長していくのを、何よりの楽しみとしてここで失礼させていただきます。

※ フィードバックはGitHubのIssueやX(旧Twitter)でも受け付けています。最高の学習サービスを作る手助けを心よりお待ちしています!

https://php-study.jp

ソースコードは全て GitHub 上で公開しています

https://github.com/php8-study/php8-study-app

Discussion