TL;DR
- 破壊的 API は 「DRY-RUN を既定 /
--applyで初めて実行 / 直前に y/N プロンプト」の 3 段ガードを入れる。 - DB 作成は
POST /db→POST /db/user→POST /db/user/<user>/grantの 3 連 POST。途中失敗時のロールバックは書かず早期exitで止める。 - ランダム PW を
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16で生成する常套句は、set -o pipefail下ではtrが SIGPIPE で 141 を返して落ちる。一時的にset +o pipefail。 - 生成した接続情報は
deploy/.env.production.secretsにchmod 600で書き出し、Laravel の.env.productionに貼り付ける運用に倒す。
なぜ DRY-RUN を既定にするのか
「--apply を打たないと何も起きない」設計にすると、3 つの利点がある:
- 誤実行が物理的に起きない —
bash xs-db-create.shが常に副作用ゼロ。AI エージェントに任せても怖くない。 - レビュー素材になる — DRY-RUN の出力をそのままチャットや PR に貼って「これで OK?」と聞ける。
--applyの重みが揃う — 全スクリプトで--applyを統一旗にしておくと、それ自体が「ここで本気を出すぞ」というマーカーになる。
DRY-RUN は「リクエストを送らず、送る内容だけ表示する」モード。echo で十分:
if [[ ${APPLY} -eq 0 ]]; then
echo "[DRY-RUN] 以下のリクエストが送られます (実行はしません)。"
echo
echo "POST ${BASE}/db"
echo " body: {\"name_suffix\": \"${SUFFIX}\", \"character_set\": \"utf8mb4\", \"memo\": \"${MEMO}\"}"
echo
echo "実行する場合は --apply を付けて再度実行してください。"
exit 0
fi
引数パーサ
bash の素朴な while + case で十分:
APPLY=0
SUFFIX="myapp"
MEMO="myproject (SaaS Phase 4)"
PASSWORD=""
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) APPLY=1; shift ;;
--suffix) SUFFIX="$2"; shift 2 ;;
--memo) MEMO="$2"; shift 2 ;;
--password) PASSWORD="$2"; shift 2 ;;
-h|--help)
sed -n '/^# /{s/^# \?//;p;}' "$0" | head -30
exit 0
;;
*) echo "未知のオプション: $1" >&2; exit 2 ;;
esac
done
-h|--help の中身がトリッキー。スクリプト先頭の # コメント から行頭の # と続く空白だけ sed で削って head -30 で出している。「ドキュメントをコード冒頭に書く」と「--help で出す」を二重管理しなくていい、という小ワザ。
DB 名を機械的に組み立てる
Xserver は契約ごとに DB 名 / ユーザ名に サーバ ID プレフィクス が強制される。myserver.xsrv.jp 契約なら DB 名は myserver_xxxx の形でしか作れない。
# myserver.xsrv.jp → myserver
SERVER_PREFIX="${XSERVER_TARGET_SERVER%%.*}"
DB_NAME="${SERVER_PREFIX}_${SUFFIX}"
DB_USER="${DB_NAME}" # 同一名で揃える方が運用が楽
${VAR%%.*} は Bash パラメータ展開で「. 以降を最長マッチで削る」。cut や sed を持ち出さなくていい。
DB 名と DB ユーザ名は揃える。1 DB に 1 ユーザの個人開発スケールでは、それで十分。
ランダムパスワード生成と SIGPIPE の罠
Xserver の MySQL ユーザは パスワード 16 文字上限 という地味な制約がある。32 字 UUID は弾かれる。素朴に /dev/urandom から 16 文字:
PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16)
これを set -euo pipefail 下で動かすと 落ちる。理由:
head -c 16は 16 バイト読んだら閉じる- 直前の
trがまだ書き込もうとしているところに SIGPIPE が飛ぶ trの終了コードは 141 (= 128 + 13、SIGPIPE)pipefail下では「パイプ内のどれかが非ゼロなら全体も非ゼロ」なのでスクリプトが死ぬ
解決は その行だけ pipefail を切る:
if [[ -z "${PASSWORD}" ]]; then
set +o pipefail
PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16)
set -o pipefail
fi
tr | head は Bash イディオムとして頻出するので、覚えておくと刺さりにくい。
--apply の確認プロンプト
--apply を渡しても、もう一段「本当に?」を挟む:
read -r -p "上記の内容で実際に本番サーバへ送信します。続けますか? [y/N] " ans
case "${ans:-N}" in
[yY]|[yY][eE][sS]) ;;
*) echo "中止しました。"; exit 0 ;;
esac
read -rでバックスラッシュ展開を抑制${ans:-N}で空入力時のデフォルトを N に。[y/N]表記と整合caseでy,Y,yes,YES全部許す (“yes” を打ち込みたい人もいる)
3 連 POST の本体
DB 作成 → ユーザ作成 → 権限付与 を順に投げる:
# 1) DB
db_res=$(curl -sS -X POST -H "${AUTH}" -H 'Content-Type: application/json' \
-d "{\"name_suffix\": \"${SUFFIX}\", \"character_set\": \"utf8mb4\", \"memo\": \"${MEMO}\"}" \
"${BASE}/db")
[[ "${db_res}" == *'"error"'* ]] && { echo "ERROR: DB 作成に失敗"; exit 1; }
# 2) ユーザ
user_res=$(curl -sS -X POST -H "${AUTH}" -H 'Content-Type: application/json' \
-d "{\"name_suffix\": \"${SUFFIX}\", \"password\": \"${PASSWORD}\", \"memo\": \"${MEMO}\"}" \
"${BASE}/db/user")
# パスワードは絶対にログに出さない
echo " response: $(echo "${user_res}" | sed 's/"password":[^,}]*/"password":"***"/g')"
[[ "${user_res}" == *'"error"'* ]] && { echo "ERROR: ユーザ作成に失敗"; exit 1; }
# 3) grant
grant_res=$(curl -sS -X POST -H "${AUTH}" -H 'Content-Type: application/json' \
-d "{\"db_name\": \"${DB_NAME}\"}" \
"${BASE}/db/user/${DB_USER}/grant")
[[ "${grant_res}" == *'"error"'* ]] && { echo "ERROR: 権限付与に失敗"; exit 1; }
ポイント:
- エラー判定は素朴に
*'"error"'*— Xserver API のエラー応答は{"error": {...}}を返すので、JSON にこの文字列が入っていれば失敗と判断できる。jq -eで完全な判定もできるが、個人開発スケールでは substring match で十分。 POST /db/userのレスポンスからパスワードを sed で消す — Xserver API の応答にはリクエストエコーとしてpasswordが含まれることがある。s/"password":[^,}]*/"password":"***"/gで必ず潰す。- ロールバックを書かない — 途中失敗時は手動で残骸を消す (Xserver API に DELETE もある)。個人開発スケールなら「2 回も失敗するなら何かが根本的におかしい」のでロールバックより根本対応を促す方が健全。
secrets ファイル書き出し
成功したら、生成 PW を含む接続情報を Laravel の .env にコピペできる形で書き出す:
SECRETS_FILE="${REPO_ROOT}/deploy/.env.production.secrets"
{
echo "# 自動生成: $(date '+%Y-%m-%d %H:%M:%S') by xs-db-create.sh"
echo "# このファイルは gitignored。api/.env.production の DB_* に丸ごと貼って使う。"
echo ""
echo "DB_CONNECTION=mysql"
echo "DB_HOST=mysql${SERVER_PREFIX}.xserver.jp"
echo "DB_PORT=3306"
echo "DB_DATABASE=${DB_NAME}"
echo "DB_USERNAME=${DB_USER}"
echo "DB_PASSWORD=\"${PASSWORD}\""
} > "${SECRETS_FILE}"
chmod 600 "${SECRETS_FILE}"
chmod 600 は必須。.gitignore でリポジトリにも絶対に入れない。
{ echo; echo; } > FILE は 複数行を 1 ファイルにアトミックに書く Bash イディオム。echo ... >> FILE を連発するより、サブシェルでまとめて 1 回書き込む方が安全で速い。
「次の手順」を最後に出す設計
スクリプトの最終出力で 人間がやる次の作業を誘導する:
echo " 次の手順:"
echo " 1) bash deploy/scripts/xs-info.sh db で作成を確認"
echo " 2) ${SECRETS_FILE} の中身を api/.env.production の DB_* に貼る"
echo " 3) ssh myserver 'mysql -h mysql${SERVER_PREFIX}.xserver.jp -u ${DB_USER} -p ${DB_NAME}' で接続テスト"
これは AI エージェントとの分担にも効く。エージェントが --apply を叩いた後、ターミナル出力にこの「次の手順」が残っていれば、続きの作業を引き継ぎやすい。
まとめと次回予告
- DRY-RUN を既定にする 3 段ガード設計
- 3 連 POST を素朴な curl の連鎖で書く
- ランダム PW の SIGPIPE 罠は
pipefailの局所オフで回避 - secrets はローカルファイル経由で
.envに渡す
次回はこの設計を 「非同期な API」に適用する。POST /mail は事前にドメイン認証 TXT が立っていないと弾かれる仕様なので、GET /server-info から token を取って TXT を投入し、DNS 反映を待ってリトライする、というワンクッションが必要になる。


コメント