SLIDE 1

GCE 単一 VM の現行本番構成

GCE VM は 1 台のまま維持しつつ、`portfolio-hp` と `reading-time-tracker` の公開中コード、Git 作業コピー、DB、env、backup を分離した。単一 VM 構成のままでも、更新失敗、権限事故、DB 巻き込みを避けるための最小限の運用境界を作っている。

User
Cloudflare
GCE Ubuntu VM
Nginx
Static / Flask + SPA
STATIC

portfolio-hp

  • repo: /home/itamishotaro/portfolio-hp
  • release root: /var/www/releases/portfolio-hp
  • public: /var/www/html/index.html, /var/www/html/page2.html
  • 最新 release: 20260411000949
FLASK + SPA

reading-time-tracker

  • repo: /opt/reading-time-tracker/repo
  • backend current: /opt/reading-time-tracker/current
  • backend releases: /opt/reading-time-tracker/releases
  • frontend public: /var/www/html/books
  • 最新 release: 20260411003055
SLIDE 2

通信と公開経路のフローチャート

入口から静的ページ、SPA、API、DB までの流れを 1 枚にまとめた。Cloudflare と Nginx で入口を統一しつつ、`portfolio-hp` と `reading-time-tracker` の役割を分けている。

User Browser / Mobile Cloudflare DNS / TLS / Edge WAF / Cache GCE UBUNTU VM Nginx Single entrypoint portfolio-hp / , /page2.html /var/www/html/*.html SPA Frontend /books/ /var/www/html/books Flask API /books/api/ Gunicorn via unix socket SQLite / Backup books.db / backup timer STATIC SPA API
INGRESS

すべての公開トラフィックは Cloudflare → GCE VM → Nginx を通る。

FRONTEND

//page2.html は静的 HTML、/books/ は SPA を返す。

BACKEND

/books/api/ は Gunicorn に流し、最終的に SQLite と backup 運用へつながる。

SLIDE 3

責務分離と実パス

本番で変化するものを Git 管理配下から外し、repo と runtime を分離する。これが今回の改善の中心で、手戻りや誤操作が起きても被害を局所化できるようにしている。

REPO

Git 作業コピー

/home/itamishotaro/portfolio-hp
/opt/reading-time-tracker/repo

CURRENT

公開中コード

/opt/reading-time-tracker/current
portfolio は `index.html` と `page2.html` の symlink で公開。

DATA

永続データ

/var/lib/reading-time-tracker/books.db
/var/backups/reading-time-tracker

CONFIG

本番設定

/etc/reading-time-tracker/reading-time-tracker.env
repo 配下に `.env` を置かない。

reading-time-tracker:
repo      -> /opt/reading-time-tracker/repo
current   -> /opt/reading-time-tracker/releases/20260411003055
frontend  -> /var/www/html/books -> /var/www/releases/reading-time-tracker/20260411003055
db        -> /var/lib/reading-time-tracker/books.db
env       -> /etc/reading-time-tracker/reading-time-tracker.env
backup    -> /var/backups/reading-time-tracker/books-YYYYMMDDHHMMSS.db
SLIDE 4

公開経路と Nginx の役割

Nginx は静的サイト、SPA、API の入口を 1 つにまとめる。`/books/` は frontend release の symlink を返し、`/books/api/` は unix socket 経由で Gunicorn に流す。

ROUTING
  • / -> `portfolio-hp`
  • /page2.html -> `portfolio-hp`
  • /books/ -> `reading-time-tracker` frontend
  • /books/api/ -> `reading-time-tracker` backend
RATE LIMIT
  • /books/api/admin/session は厳しめに制限
  • /books/api/ 全体は緩めに制限
  • 設定ファイル: /etc/nginx/conf.d/reading-time-tracker-rate-limit.conf
WARNING
  • proxy_headers_hash の warning は出ている
  • 今回の反映では致命傷ではない
  • 後日 `nginx.conf` 調整で解消余地あり
location = /books/api/admin/session {
    limit_req zone=books_admin_session burst=5 nodelay;
}

location /books/api/ {
    limit_req zone=books_api burst=20 nodelay;
    proxy_pass http://unix:/run/reading-time-tracker/reading-time-tracker.sock;
}
SLIDE 5

systemd と backend runtime

backend は repo 直参照ではなく `current` を読む。virtualenv も release ごとに作らず、`shared/.venv` を使い回す。これで release 切替と runtime を分離できる。

SERVICE
  • reading-time-tracker.service
  • WorkingDirectory=/opt/reading-time-tracker/current/backend
  • EnvironmentFile=/etc/reading-time-tracker/reading-time-tracker.env
RUNTIME
  • ExecStart=/opt/reading-time-tracker/shared/.venv/bin/gunicorn
  • workers: 2
  • timeout / graceful-timeout / max-requests 設定済み
SOCKET
  • /run/reading-time-tracker/reading-time-tracker.sock
  • Nginx はここへ proxy
  • journald にアクセスログ / エラーログを出す
sudo systemctl status reading-time-tracker --no-pager -l
sudo journalctl -u reading-time-tracker -n 200 --no-pager
curl -I https://itamishotaro.com/books/api/health
SLIDE 6

Deploy, Rollback, Restore

デプロイは release 作成後に symlink を切り替える。rollback は symlink の戻しだけで完結させ、DB restore は service stop を必ず挟む。

DEPLOY
portfolio-hp:
git pull
./deploy/release.sh

reading-time-tracker:
git pull
./deploy/release.sh
ROLLBACK
frontend:
ln -sfnT previous /var/www/html/books

backend:
ln -sfnT previous /opt/reading-time-tracker/current
systemctl restart reading-time-tracker

portfolio:
ln -sfnT previous/index.html /var/www/html/index.html
RESTORE
systemctl stop reading-time-tracker
restore backup db
owner / perm 修正
systemctl start reading-time-tracker
curl /books/api/health
重要: 既存ディレクトリを symlink に置き換えるときは ln -sfnT を使う。単純な ln -sfn だと既存ディレクトリを置き換えられない。
SLIDE 7

Backup と Healthcheck

SQLite backup と healthcheck は timer に寄せた。障害が軽微なら healthcheck が 1 回だけ backend restart を試み、それでも戻らなければ journald に失敗を残す。

BACKUP
  • service: reading-time-tracker-backup.service
  • timer: reading-time-tracker-backup.timer
  • 方式: sqlite3 .backup
  • 保持: 14 日
  • 前提: VM に sqlite3 パッケージが必要
  • 直近成功: 2026-04-11 00:45:29 JST
HEALTHCHECK
  • service: reading-time-tracker-healthcheck.service
  • timer: reading-time-tracker-healthcheck.timer
  • 確認対象: /, /page2.html, /books/, /books/api/health
  • /var/www/html/books symlink も検証
  • 直近成功: 2026-04-11 00:40:10 JST
sudo systemctl list-timers --all | grep reading-time-tracker
sudo systemctl start reading-time-tracker-backup.service
sudo journalctl -u reading-time-tracker-backup.service -n 50 --no-pager
sudo journalctl -u reading-time-tracker-healthcheck.service -n 50 --no-pager
SLIDE 8

正常系の確認ポイント

この構成で「今まともに動いているか」を最短で見るなら、URL 4 本、service 1 本、symlink 4 本、backup 1 本を見れば十分です。

HTTP
curl -I https://itamishotaro.com/
curl -I https://itamishotaro.com/page2.html
curl -I https://itamishotaro.com/books/
curl -I https://itamishotaro.com/books/api/health
FILES / SERVICE
systemctl status reading-time-tracker --no-pager -l
readlink -f /opt/reading-time-tracker/current
readlink -f /var/www/html/books
readlink -f /var/www/html/index.html
readlink -f /var/www/html/page2.html
ls -1 /var/backups/reading-time-tracker

期待

/ が 200

期待

/books/ が 200

期待

/books/api/health が 200

期待

symlink が release を指す

SLIDE 9

今回の移行で詰まった点

次回の再発防止のために、実際に詰まった点を運用資料として残す。概念図だけではここが抜けやすい。

ACTUAL PITFALLS
  • shared/.venv 未作成だと deploy 開始時点で失敗
  • current 未作成で service を切り替えると status=200/CHDIR
  • DB 権限不足だと Alembic が unable to open database file
  • /var/www/html/books が既存ディレクトリだと初回 symlink 化が必要
  • backup service は sqlite3 が無いと失敗
OPERATIONS RULE
  • deploy は repo でだけ実行する
  • DB と env は repo 配下に置かない
  • 初回移行や rollback は ln -sfnT を使う
  • rm -rf /var/www/html/books は使わない
  • 障害判断は「最新の healthcheck 成功 / 失敗」で見る
現行の実測値:
backend current -> /opt/reading-time-tracker/releases/20260411003055
frontend books  -> /var/www/releases/reading-time-tracker/20260411003055
portfolio index -> /var/www/releases/portfolio-hp/20260411000949/index.html
portfolio page2 -> /var/www/releases/portfolio-hp/20260411000949/page2.html
backup file     -> /var/backups/reading-time-tracker/books-20260411004529.db