ベンダー提供の自動化に自分の層を足す

ベンダー提供の自動化に自分の層を足す

前回、ホスティング事業者が用意したDocker構成で、後から追加したドメインの証明書だけが静かに期限切れしていた話を書いた。原因は、毎日走る更新スクリプトが単一ドメインを名指ししていたこと。

→ 前編: certbotの後付けドメインだけが静かに期限切れする話

証明書は復旧した。本題はここからで、次の90日後に同じことを繰り返さないための設計をどう組むか。


論点:提供物に載るか、自分の機構を作るか

最初はこの二択で考えていた。しかし問いの立て方が間違っていた。

正しい問いは「このスクリプトは誰のものか」だった。自作なら直せばいい。ベンダー提供なら話が変わる。

判別材料はこうだった。

観点 判定
公式構成にはない override ファイルを併用 事業者独自
/root 直下に運用スクリプト イメージ由来
ファイル日付が環境構築時期と一致 イメージ由来

ベンダー提供物と確定した時点で、選択肢は絞られた。


結論:書き換えず、併走させる

なぜ書き換えないか

提供物を直接編集する案は却下した。理由は3つある。

上書きされる可能性がある。 イメージ更新や管理パネルからの操作で提供物が再配置されたとき、自分の変更は消える。しかも消えたことに気づけない。前回と同じ「静かに壊れる」パターンを、自分の手で仕込むことになる。

責任境界が曖昧になる。 提供物を改変すると、トラブル時に「提供物の問題か、自分の改変の問題か」が切り分けられなくなる。サポートに問い合わせる際も不利になる。

剥がせなくなる。 元に戻したくなったとき、何をどう変えたか覚えていないと戻せない。

採用した構成

/etc/cron.d/xxx        → /root/ssl_update.sh        ← 提供物。触らない
/etc/cron.d/xxx-extra  → /root/ssl_update_extra.sh  ← 自前。全ドメインを回す

層を分ける。 自分の層は2ファイル消せば完全に元に戻る。提供物は最初から何も変わっていない。

cat > /root/ssl_update_extra.sh <<'EOF'
#!/bin/bash
set -uo pipefail
LOG=/var/log/ssl_update_extra.log
exec >> "$LOG" 2>&1
echo "=== $(date '+%F %T') start ==="
docker exec <certbot-container> certbot renew --quiet
docker exec <nginx-container> nginx -s reload
echo "=== $(date '+%F %T') done ==="
EOF
chmod +x /root/ssl_update_extra.sh

echo '30 2 * * * root bash /root/ssl_update_extra.sh' > /etc/cron.d/xxx-extra

設計上のポイント

ドメイン非依存にした。 certonly -d <domain> ではなく certbot renew を使う。renewal設定ファイル単位で全証明書を見るので、ドメインを増やしてもスクリプトを触らなくていい。前回の失敗パターンが構造的に起きなくなる。

実行時刻をずらした。 提供物の2:00に対して2:30。同時実行によるロック競合を避ける。certbotは残り30日未満のものしか更新しないので、二重に走っても無駄な発行は起きない。

ログを残した。 これが最大の変更点。前回いちばん困ったのは「certbotのログに何の記録もない」ことだった。実行の痕跡さえあれば、もっと早く気づけた。exec >> "$LOG" 2>&1 で標準出力・標準エラーごとファイルに落とす。

set -e は使わなかった。 set -uo pipefail のみ。-e を入れると certbot が非ゼロ終了したとき nginx reload に到達しない。片方が失敗しても残りは実行したい。

動作確認

bash /root/ssl_update_extra.sh
cat /var/log/ssl_update_extra.log
# === 2026-08-07 10:44:37 start ===
# 2026/08/07 01:44:39 [notice] 346#346: signal process started
# === 2026-08-07 10:44:39 done ===

signal process started は nginx reload が効いた証拠。certbotが無言なのは --quiet かつ全ドメインが更新時期前だから。正常動作。

却下した案:環境変数にドメインを列挙する

カンマ区切りで複数ドメインを書く方法も理論上ある。しかし採らなかった。


監視設計 — 「エラーの不在」を検知する

前回の教訓は「失敗ログすら出ないので、エラー監視では拾えない」だった。では何を見るか。

内部状態ではなく、結果を見る

THRESHOLD=20
for d in main.example.com new.example.com other.example.com; do
  end=$(echo | openssl s_client -connect "$d:443" -servername "$d" 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2)
  days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
  [ "$days" -lt "$THRESHOLD" ] && echo "WARN: $d expires in $days days"
done

外側から見ているのが肝。 certbot certificates でファイルの状態を確認するのではなく、実際に443で配信されている証明書を取得する。

この違いは大きい。ファイルベースの確認では検知できないケースがある。

ケース ファイル確認 外部確認
更新したがreload忘れ 検知不可 検知できる
設定が古いパスを向いている 検知不可 検知できる

実際、前回の作業でも「certbot上はVALIDなのにブラウザでは警告」という状態を経由した。reloadを挟むまで古い証明書が配信され続けていた。中間状態がどれだけ正しくても、最終成果物が壊れていれば意味がない。

閾値を20日にした理由

certbotは残り30日で更新を試みる。20日を切っているということは、更新が2回以上失敗していることを意味する。単発のネットワークエラーでは鳴らず、構造的な問題のときだけ鳴る。

正常時は無言にする

通知は、出力が空なら何もしない設計にした。毎日「正常です」が届くと、そのうち読まなくなる。鳴ったら異常という状態を保つほうが、通知としての価値が高い。


作業オペレーションの知見

証明書そのものより、端末操作で時間を取られた。同じところで詰まる人がいそうなので記録しておく。

SSHが切れてもcertbotは壊れない

certbot renew 実行中に Connection reset by peer が2回発生した。焦ったが、結論としては安全だった。

certbotは一時ディレクトリで証明書を完成させてから、live/ のシンボリックリンクを差し替える設計になっている。途中で切れても中途半端なファイルが live/ に入ることはない。実際、再接続して確認したら取得は完了していた。

とはいえ docker exec はSSHセッションに紐づくので、プロセスごと落ちる可能性はある。長い処理は nohup かターミナルマルチプレクサの中で。

nohup docker exec <certbot-container> certbot renew \
  --cert-name new.example.com --webroot -w /var/www/html --force-renewal \
  > /root/certbot.log 2>&1 &

切断後に再実行するときは、ロックの残留を確認してから。

docker exec <certbot-container> ls -la /etc/letsencrypt/ | grep lock

端末の文字化けは繋ぎ直さなくても直る

制御文字が流れて代替グラフィック文字セット(罫線文字)に切り替わったまま戻らない状態。

reset            # まずこれ
printf '\033c'   # 効かなければハードリセット
stty sane

引き金は、出力中の矢印キー入力だった。ログに ^[[A が混入していた。長い処理中は端末に触らない。

screen内でスクロールできない

screenが端末のスクロールバッファを乗っ取るため、ホイールもスクロールバーも効かない。

Ctrl-a [     コピーモードに入る(↑↓ / Ctrl-u / Ctrl-d / g / G / 検索は /)
Esc          抜ける

~/.screenrc に入れると根本的に楽になる。

defscrollback 50000
termcapinfo xterm* ti@:te@

下の行がホイールスクロールを端末側に任せる設定。これを入れるとコピーモードに入らず普通にスクロールできる。

そもそもスクロールに頼らない

長い出力を目で追う運用自体を変えたほうが早い。

# ページャに通す
docker exec <certbot-container> certbot certificates | less

# ファイルに落として後から読む
docker exec <certbot-container> certbot renew --dry-run > /root/dryrun.log 2>&1

# 必要な行だけ抜く(今回これが一番読みやすかった)
docker exec <certbot-container> certbot certificates | grep -E 'Certificate Name|Expiry'

検証は終わっていない

構成は組んだが、本当に動くかはまだ確認できていない。

certbotは残り30日を切るまで何もしない。つまり設置した機構が機能するかどうかは、最初のドメインが更新時期を迎えるまで分からない。

さらに厄介なことに、提供物側が先に更新してしまうと、自前側は「更新不要」と判断してスキップする。この場合、自前機構の実効性は次のドメインの更新時期まで確認できない。

だから検証予定日をカレンダーに入れた。「組んだから安心」ではなく「組んだので、この日に確認する」まで含めて設計だと思う。前回の失敗も、要は確認していなかったことに尽きる。


この件から得た原則

外部提供物との境界を明示的に設計する。

提供物は「動いているブラックボックス」として尊重し、改変しない。足りない部分は自分の層を独立して重ねる。層が分かれていれば、提供物が更新されても壊れないし、自分の層が不要になれば消せる。

これは外部ツールを取り込むときの原則と同じで、依存を薄く保っていつでも剥がせる形にしておく、ということ。

自動化は「動いていること」ではなく「動いた記録」で確認する。

ログのない自動化は、動いているかどうかを確認する手段がない。前回のように「一度も試行されていなかった」ことすら、証拠がなければ推測にとどまる。

監視は内部状態ではなく最終成果物を見る。

利用者が実際に受け取るものを、外側から確認する。内部がどれだけ正しく見えても、出口が壊れていれば意味がない。