15 分で読了 #devops #runbooks
このページの目次

wildwindstudio.com 証明書の自動更新 + Tencent Cloud CLB 再バインド

ホスト:bj-middleware-1(root、TencentOS、Tencent Cloud 北京)


1. 全体フロー

全体フロー: 定期トリガー → 証明書ごとの更新 → 成功時のみ CLB 再バインド /etc/crontab · 毎月1日・15日 02:00 にトリガー renew-and-update-clb.sh · オーケストレーションスクリプト(中核) 各証明書に対して certbot renew --no-random-sleep-on-renew を実行 --dns-tencentcloud-propagation-seconds 30 --deploy-hook …; 各処理完了後 sleep 60(負荷分散、レートリミット回避) certbot(Let's Encrypt · DNS-01 検証) certbot-dns-tencentcloud プラグインを使用して DNSPod に _acme-challenge TXT レコードを書き込み · 認証情報は tencentcloud.ini 証明書が「実際に更新された場合のみ」呼び出し → clb-deploy-hook.sh(deploy-hook、更新成功時のみ呼び出される) ① tccli ssl UploadCertificate — 新証明書を Tencent Cloud SSL にアップロードし、CertId を取得 ② tccli clb ModifyListener — CertId を対応する CLB listener に再バインド · 認証情報は tccli.env(600) Tencent Cloud CLB listener が新証明書に更新される 重要な設計: 証明書ごとの直列実行 + sleep 60 で Tencent Cloud DNS API のレートリミットを根本から回避; deploy-hook は実際に更新された場合のみ再バインドを実行 —— 失敗しても誤って更新されないが、更新自体の監視が必要。

ログは /var/log/certbot-renewal.log に一元化されます。certbot 自身のデバッグログは /var/log/letsencrypt/ にあります。

重要な設計ポイント

  • 証明書ごとの直列実行 + sleep 60:根本的な目的は Tencent Cloud DNS API のレートリミットを回避することです(第4節の根本原因参照)。certbot renew で一括実行する旧方式に戻さないでください。
  • deploy-hook モデル⁠:証明書が「実際に更新された場合のみ」アップロード/再バインドがトリガーされ、同じ証明書の無意味な再アップロードを避けます。これは旧スクリプト(無条件で再送信)よりも正しいアプローチですが、同時に——⁠更新に失敗した場合、CLB は更新されないことを意味するため、更新自体の健全性監視が必要です(第6節参照)。

2. 証明書 ↔ CLB マッピング

リージョンは固定 ap-beijing、現在はすべて単一ドメイン証明書です:

証明書(certbot cert-name)ドメインCLBListener
jsvc.wildwindstudio.com-0001jsvc.wildwindstudio.comlb-fkcz6lotlbl-ehxkll9d
passport.wildwindstudio.compassport.wildwindstudio.comlb-b6j4kn47lbl-0jzb58rx
cn-reserve-api.wildwindstudio.comcn-reserve-api.wildwindstudio.comlb-mm70ra69lbl-naeanxnv

マッピングは clb-deploy-hook.shcase ブランチで管理されています。証明書の新規追加/変更時は、ここと renew-and-update-clb.sh 上部の CERTS=(...) リストを修正してください。

jsvc 証明書の cert-name に -0001 サフィックスが付いているのは歴史的経緯です(同名の3ドメイン SAN 系譜が jsvc.wildwindstudio.com という名前を占有していたため)。機能には影響ありません。 resflow.wildwindstudio.com は退役済み⁠(DNS なし、オンラインサービスなし)であり、その旧 CLB lb-mm70ra69 は現在 cn-reserve-api で再利用されています。


3. ファイル一覧

パス説明権限
/etc/crontab定期エントリ(平文キーを含まない)644
/usr/local/bin/renew-and-update-clb.sh更新オーケストレーションスクリプト755
/usr/local/bin/clb-deploy-hook.shcertbot deploy-hook: アップロード + CLB 再バインド755
/usr/local/bin/clb-bind-one.sh手動単一証明書再バインドツール(緊急用)755
/etc/letsencrypt/tccli.envtccli 認証情報(アップロード/再バインド用)600
/etc/letsencrypt/tencentcloud.inicertbot DNS プラグイン認証情報(DNS-01 TXT 書き込み用)600
/var/log/certbot-renewal.log更新チェーンログ-

注意:DNS プラグインと tccli は異なる2セットの Tencent Cloud API Keyを使用しています。前者は DNSPod 権限のみ必要、後者は SSL + CLB 権限が必要です。


4. トラブルシューティング記録(2026-06-24)

現象

  • CLB lb-fkcz6lot(jsvc.wildwindstudio.com)の証明書が期限切れ、「更新スクリプトが反応せず」、オンラインでは期限切れの証明書が発行されていました。
  • 同一バッチの passport、jsvc はすべて 2026-06-24 に期限切れとなりました。

調査手順と発見

  1. エントリポイントの特定⁠:/etc/crontab に毎月1日/15日 02:00 に renew-and-update-clb.sh を実行する設定があり、ログは /var/log/certbot-renewal.log に出力されます。
  2. ログの確認⁠:06-15 の certbot renew で jsvc/resflow に対して以下が報告されました。
    failed to determine base domain, please report to dev.
    Tried: ['wildwindstudio.com', 'jsvc.wildwindstudio.com']
    
    つまり、certbot の更新は常に失敗していました⁠。
  3. プラグインソースコードの読取 certbot_dns_tencentcloud/certbot_tencentcloud_plugins.pydetermine_base_domain()
    try:
        resp = client.describe_record_list(dt)
    except APIException as _:   # ← 任意の API 例外を「このドメインを所有していない」として扱う
        continue
    ...
    raise PluginError("failed to determine base domain ...")
    
    任意の API 例外(レートリミット / QPS / 一時的エラー)がすべてキャッチされ⁠、最終的にこの誤解を招く "base domain" エラーがスローされます。
  4. 対照確認⁠:cn-reserve-api は同じアカウント、同じ tencentcloud.ini、同じルートドメインですが、06-16 に個別に発行した場合は成功しました。違いは「個別実行 vs 一括連続実行」のみです。
  5. staging / 本番発行の再現⁠:単一 certbot renew --cert-name jsvc... を実行すると base-domain のステップをスキップして更新に成功 → 一括連続実行がレートリミットをトリガーし、プラグインによって base-domain エラーとして隠蔽されていたことが確定⁠。
  6. 旧スクリプトが問題を隠蔽⁠:06-16 以前の旧スクリプトロジックは「certbot の成否に関わらず、無条件で既存証明書を再アップロードして CLB に再バインド」でした。そのため、04〜06月は毎回「✓ 再バインド成功」と表示されていましたが、バインドされていたのは03-26に発行され06-24に期限切れとなる古い証明書であり、「更新が正常に動作している」という偽りの状態を演出していました。06-16 に deploy-hook モデルに変更した後、更新失敗時に再バインドが行われなくなったため、問題が表面化しました。

根本原因

予約(cn-reserve)サービスで2つのドメインが新規追加された後、1回の更新バッチで処理するドメイン数が増加し、短時間で Tencent Cloud DNS API への密集呼び出しが発生 → レートリミットをトリガー → certbot DNS 検証で結果が取得できず → 更新失敗 → 証明書が期限切れになっても自動更新されなかった。


5. 今回の対応

緊急更新

  • jsvc / passport を個別(直列、隔離)で手動更新し、レートリミットを回避;clb-bind-one.sh を使用して対応する CLB にアップロードおよび再バインド。
  • 結果:lb-fkcz6lot、lb-b6j4kn47 のオンライン証明書がすべて更新され、有効期限は 2026-09-22 まで。

根本解決(スクリプト改造、オンライン + 自己テスト EXIT=0)

  • renew-and-update-clb.sh証明書ごとの直列実行 + 各処理後に sleep 60 + --no-random-sleep-on-renew に変更し、根本からレートリミットを回避。
  • clb-deploy-hook.sh で証明書名マッピングを修正し、「実際の更新後のみ再バインド」を維持。

クリーンアップ

  • 期限切れで冗長な jsvc 3ドメイン SAN 系譜(退役済み resflow を含む)を削除。
  • 平文キーを含む旧 .bak スクリプトを削除。
  • 平文 tccli キーを 644 の /etc/crontab から 600 の /etc/letsencrypt/tccli.env へ移行し、3つのスクリプトが統一してここから読み込むように変更;crontab のバックアップを 600 に権限降格。

6. 運用 Runbook

手動更新 + 単一証明書の再バインド(緊急時)

# 1) 更新(必ず単一、隔離して実行し、一度に複数実行しないこと)
export PATH=/usr/local/bin:$PATH
certbot renew --cert-name <cert-name> \
  --no-random-sleep-on-renew \
  --dns-tencentcloud-propagation-seconds 30

# 2) アップロード + 再バインド(認証情報は /etc/letsencrypt/tccli.env から自動読み込み)
/usr/local/bin/clb-bind-one.sh \
  /etc/letsencrypt/live/<cert-name> <alias> <lb-id> <listener-id>

CLB オンライン証明書の検証

echo | openssl s_client -connect <ドメイン>:443 -servername <ドメイン> 2>/dev/null \
  | openssl x509 -noout -subject -dates

証明書一覧 / 期限の確認

certbot certificates

ドメイン/証明書の追加

  1. certbot certonly --dns-tencentcloud --dns-tencentcloud-credentials /etc/letsencrypt/tencentcloud.ini -d <ドメイン>
  2. clb-deploy-hook.shcase にマッピングを追加;renew-and-update-clb.shCERTS=(...) に cert-name を追加。
  3. 初回のみ手動で clb-bind-one.sh でバインド。

未完了 / 改善項目

  • tccli API Key のローテーション⁠(AKIDqgLrCLI2...):644 の crontab で平文保存されていた期間が数ヶ月に及び、漏洩として扱う必要があります。
  • 証明書の残り日数アラート追加⁠(既存の Prometheus + DingTalk に接続)、残り < 14 日でアラート;「スクリプトがエラーを返さない」ことに依存しないようにする。
  • certbot-dns-tencentcloud プラグインにリトライ/指数バックオフを追加することを検討(注意:site-packages の変更はアップグレードで上書きされる可能性があります;現在はスクリプト層での直列実行 + 負荷分散で十分対応可能)。