第4回: サブドメイン / Cron / SSL を Xserver API で運用する — idempotent と日次監視

TL;DR

  • サブドメイン作成は POST /subdomain 一発、SSL は Xserver が Let’s Encrypt で自動発行してくれる (DNS が浸透していれば成功する)。
  • Cron 登録は API で書けるが 「使える記号が制限される」(~, >> が NG)。コマンドは絶対パス + リダイレクトは > 1 回まで。
  • Cron は同コマンドの登録があったらスキップする idempotent 化を入れる。
  • SSL は Xserver 任せでもたまに更新失敗するので、「残日数が閾値を切ったら exit 1」のチェックを cron で日次に走らせる。

1. サブドメイン作成 (xs-subdomain-create.sh)

POST /subdomain{"subdomain":"dev-api.example.com"} を投げるだけ:

res=$(curl -sS -X POST -H "${AUTH}" -H 'Content-Type: application/json' \
  -d "{\"subdomain\":\"${FQDN}\"}" "${BASE}/subdomain")

成功すると:

  • doc root に ~/{親}/public_html/{subdomain}/ が掘られて Xserver の placeholder が置かれる
  • SSL は Let’s Encrypt が自動発行される (DNS 浸透次第、通常 1〜数分で active)

POST する前の既存チェックも 1 ブロック:

existing=$(xs_get "subdomain")
if echo "$existing" | jq -e --arg fqdn "$FQDN" \
     '.subdomains[]? | select(.subdomain == $fqdn)' > /dev/null; then
  echo "  $FQDN は既に存在します。"; exit 0
fi

これで「dev-api.example.com を作るスクリプト」を 何回叩いても同じ結果になる。

2. Cron 登録 (xs-cron-set.sh)

目的は Laravel の scheduler を毎分動かすこと:

* * * * * cd /home/.../laravel && /usr/bin/php8.3 artisan schedule:run > /dev/null

これを POST /cron で登録する。

Cron で使える記号制限

Xserver cron のコマンドフィールドは 使える記号が API レベルで制限されている。

  • 使える記号: -_;/.?:><'"()$+%\=&*|
  • 使えない記号: ~ (HOME 略記)、>> (二重リダイレクト)

最初これに気づかずに ~/scripts/foo.sh >> ~/logs/foo.log を投げて 400 エラーで悩んだ。対処は単純:

# ~ は使わず絶対パスを組み立てる
HOME_DIR="/home/${XSERVER_SSH_USER}"
COMMAND="cd ${HOME_DIR}/${API_DOMAIN}/laravel && /usr/bin/php8.3 artisan schedule:run > /dev/null"
  • ~ の代わりに /home/<user>/ 絶対パス
  • >> の代わりに > /dev/null (出力捨て) を 1 回だけ
  • ログを取りたいなら Laravel 側のロガーに任せる (cron 側でリダイレクトしない)

PHP のフルパスを明示する理由

/usr/bin/php8.3 をフルパスで書いている。これは「~/bin/php がシステムデフォルトの PHP 7.4 を指す」ため。Laravel 13 は PHP 8.2+ 必須で、7.4 だと起動しない。

# ❌ システム PHP (7.4) を拾うので壊れる
* * * * * cd .../laravel && php artisan schedule:run

# ✅ 8.3 を明示
* * * * * cd .../laravel && /usr/bin/php8.3 artisan schedule:run > /dev/null

cron だけでなく後段の rsync デプロイ (第 5 回) でも php artisan を ssh 越しに叩くので、スクリプト全体で /usr/bin/php8.3 縛りにしておくと事故が消える。

Cron の idempotent 化

「同じコマンドの cron がすでに登録されていたらスキップ」:

existing=$(curl -fsS -H "${AUTH}" "${BASE}/cron")
if echo "${existing}" | jq -e --arg cmd "${COMMAND}" \
     '.crons[]? | select(.command == $cmd)' > /dev/null; then
  echo "  同じコマンドの cron が既に登録されています。スキップ。"
  exit 0
fi

.command の完全一致で判定。メモやスケジュールが違っても、同じコマンドなら 1 つでいい、という割り切り。メモだけ変えたい時は別途 PATCH /cron/{id} を使う (本稿では扱わない)。

3. SSL 監視 (xs-ssl-check.sh)

Xserver は Let’s Encrypt を 90 日ごとに自動更新する。ただし稀に失敗する。サーバ移転直後 / DNS が一時的に死んだ / 何かのエラーでスタックした、など。

放置すると証明書が切れる。なので「残り 30 日を切ったら exit 1」のスクリプトを cron で日次に動かす。

BSD date と GNU date のパース流儀

/ssl.expires_at2026-08-30T00:00:00+09:00 形式の ISO8601。これを epoch に変換するのが面倒。

# macOS (BSD date)
exp_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "${expires}" +%s 2>/dev/null || \
            date -j -f "%Y-%m-%d" "${expires%T*}" +%s 2>/dev/null || \
            echo 0)

BSD date は -j -f で書式指定、GNU date は -d 一発。スクリプトを書くマシンが macOS なら BSD 流で書きつつ、|| で形式違いの fallback を挟むのが楽。

実行先 (Xserver / Linux) で動かす時は GNU date 流に書き換える必要がある。「ローカルで開発した日付処理が cron 先で動かない」は、エンドツーエンドだと案外気づきにくい類のバグなので、CI で実行先と同じ Linux 環境のテストを通すのが理想。

サブシェルの変数問題

「全 SSL を走査して、閾値割れがあったら警告フラグを立てる」を素直に書くとハマる:

warn=0
echo "${filtered}" | jq -r '.[] | "..."' | while IFS=$'\t' read -r cn ...; do
  if [[ ${exp_epoch} -lt ${threshold_epoch} ]]; then
    warn=1   # ← これは親シェルに伝わらない!
  fi
done
echo "warn=${warn}"   # → 常に 0

理由: パイプの右側 (while) は別プロセス。サブシェル内の代入は親に反映されない。

対処は 2 回走査する:

# 1 回目: 表示用に整形 (副作用なし)
echo "${filtered}" | jq -r '.[] | "..."' | while IFS=$'\t' read -r cn ...; do
  printf "  %s %-30s ...\n" "${mark}" "${cn}" ...
done

# 2 回目: jq だけで件数を返して exit code 判定
need_warn=$(echo "${filtered}" | jq -r --argjson thr "${threshold_epoch}" '
  [.[] | select(.status != "active" or
                (.expires_at | sub("T.*"; "") | strptime("%Y-%m-%d") | mktime) < $thr)
  ] | length
')
[[ "$need_warn" != "0" ]] && exit 1

「表示」と「判定」を分けると jq の集約計算が綺麗に書けて、サブシェル問題も消える。一石二鳥。

日次監視を Xserver cron に乗せる

完成したら xs-cron-set.sh で登録する:

bash deploy/scripts/xs-cron-set.sh --apply \
  --command '/home/<user>/scripts/xs-ssl-check.sh' \
  --memo 'SSL daily check'

失敗 (exit 1) 時のメール通知は Xserver cron 標準機能 (notification_email フィールド) に任せる。Slack に流したいなら xs-ssl-check.sh の末尾に curl -X POST https://hooks.slack.com/... を 1 行足すだけ。

まとめと次回予告

  • 3 スクリプト共通: GET で状態確認 → DRY-RUN で内容表示 → --apply で実行 → idempotent
  • Xserver cron の記号制約は API ドキュメントには小さく書いてあるだけ。事前に知らないと「なぜか動かない cron」を量産する
  • SSL は「Xserver 任せでだいたい大丈夫」を過信しない

次回は連載最終回。rsync + SSH で Laravel API + フロント + LP を 1 コマンドでデプロイする。--delete を使うか使わないかの判断、Composer のシバン罠、mktemp のパーミッション問題など、API ではなく ファイル転送側の細かい運用知見を扱う。

コメント

タイトルとURLをコピーしました