記事
消えたブログをWebアーカイブから復元する【後編】AstroとAWSで、更新できるブログに作り直した話
この記事の目次 6項目
前編では、Google Analyticsのアクセス履歴を手がかりに、Webアーカイブから記事と画像を回収した。調査した記事候補のURLは184件。そのうち161件からHTMLを取り戻せた。
手元には昔の記事がある。ドメインも残っている。さて、どんな形で公開しようか。
今回は、記事をMarkdownで管理し、Astroで生成したページをAmazon S3とCloudFrontで配信することにした。決め手は、次に何かあったときの復旧手順と、ほとんど更新しない月にもかかる費用だった。
次にブログを失ったら、何を使って戻すか
前回の障害では、バックアップを取ったつもりでいた。ところが、いざ戻そうとしたら、復旧に必要なコンテンツが揃っていなかった。
WordPressでは、記事本文はデータベースに、アップロードした画像はサーバー上のファイルに入っている。元の環境を再現するには、テーマやプラグイン、設定ファイル、それらが動くPHPのバージョンも考える必要がある。管理対象が増えると、バックアップを取る箇所も、復元時に組み合わせて確認する箇所も増える。今回は、その難しさを自分のブログで経験した。
費用の面でも考えた。例えば、AWS上でEC2にWordPressを載せ、データベースにRDSを使うなら、記事を書かない月も、読者にページを届けるために両方を動かしておく。数年間ほとんど更新していなかった個人ブログに、その維持費をかける積極的な理由はなかった。
そこで、記事を更新したときにHTMLを生成しておき、読者が来たらそのファイルを配信する構成を選んだ。S3の保存やCloudFrontの配信、Route 53のホストゾーンなどには費用がかかるが、アプリケーションサーバーとDBの常時稼働費を省ける。自分の更新頻度なら、このコスト構造のほうが合っている。
復旧の手順も考えやすい。記事本文、記事情報、画像、サイトのソースコードを保全しておけば、そこから公開用ファイルを再生成できる。バックアップを取るときも、「この一式からブログをビルドできるか」という具体的な確認ができる。
回収したHTMLを、今後も編集できる記事にする
アーカイブの回収処理と、ブログの公開処理には、別々のリポジトリを用意した。過去のページを探しにいく作業は復旧のために一度必要だったが、次の記事を書くたびにWayback Machineへアクセスする理由はないからだ。
回収側では、元URLとHTML、画像を集めた。公開側では、そこから記事本文をMarkdownに移し、記事情報をJSONの台帳にまとめた。昔の記事に残っていたテーマのナビゲーションも、新しいブログへそのまま持ち込む必要はない。残したいのは自分が書いた本文と、それを読むための図やコードだ。 ページの見た目や目次は、新しいサイト側で組み立てることにした。
その役割を担うのが、静的サイト生成ツールのAstroだ。Markdownと記事台帳から、個別の記事、記事一覧、カテゴリ・タグ、目次、RSS、サイトマップをビルド時に生成する。
検索も同じ考え方にした。ビルド時に検索用のJSONを作り、読者が入力したキーワードをブラウザ側で照合する。この規模のブログなら、検索のためにもう一つAPIやDBを運用せずに済む。
読者が記事を開くとき、AWSでは何が動くのか
現在、このブログは次の構成で配信している。
読者が yolo-kiyoshi.com を開くと、ブラウザはDNSで配信先を調べ、CloudFrontへアクセスする。CloudFrontに記事のキャッシュがあればそこで応答し、なければS3からファイルを取得する。独自ドメインのHTTPSに使う証明書は、ACMの米国東部(バージニア北部)リージョンで管理している。
S3には、ビルド済みのHTML、CSS、JavaScript、画像を置いた。バケットは非公開にし、CloudFrontがOAC(Origin Access Control)を使ってファイルを読み取れるようにした。読者に公開する入口はCloudFrontに揃えている。配信時の異常を把握するため、CloudFrontの5xxエラーはCloudWatchのアラームで監視する。
ドメインの登録・更新は、以前と同じお名前.comのままだ。DNS管理をRoute 53へ移し、お名前.com側で、このドメインが使うネームサーバーをRoute 53のものに変更した。これで、長く使ってきたドメインを維持したまま、配信先をAWSへ切り替えられた。
昔のブックマークから、同じ記事を開けるようにする
今回、記事の本文と同じくらい残したかったのがURLだ。そもそも、閉鎖したブログのブックマークが目に入るたびに寂しくなったことが、復旧を始めたきっかけだった。せっかく記事を戻すなら、そのブックマークからまた読めるようにしたい。
例えば、以前書いたレコメンドの記事には、次のURLがある。
読者が開くパス /2020/08/26/post-2234/
S3上のファイル 2020/08/26/post-2234/index.html
ブラウザから見ると自然な記事URLだが、S3に保存しているのは末尾に index.html が付いたファイルだ。今回使っている非公開S3の構成では、CloudFrontのデフォルトルートオブジェクトに index.html を指定しても、記事ごとの階層には自動で補われない。
そこで、CloudFront Functionsにルート表を持たせ、読者が開いたパスをS3上のファイルに対応付けた。1記事分だけに単純化すると、次のような処理になる。
// 説明用の例。実際の構成ではルート表を使う。
function handler(event) {
var request = event.request;
if (request.uri === "/2020/08/26/post-2234/") {
request.uri = "/2020/08/26/post-2234/index.html";
}
return request;
}
ここで変更するのは、CloudFrontがファイルを取りにいくパスだ。読者のアドレスバーには、以前と同じ記事URLが残る。一方、www 付きのドメインを開いた場合は、正規のドメインへリダイレクトする。
この選択には、運用上の手間もある。今の構成では、新しい記事URLを追加するときに、CloudFront Functionのルート表も更新する必要がある。 既存記事の誤字を直すだけなら本文を再配置すればよい。新規記事の場合は、先にTerraformでルート表の変更を反映し、その後に記事を配置する。今後、投稿のたびに手間が気になるようなら、まずここを見直したい。
記事を書いてから公開するまで
AWSの構成はTerraformで管理し、その管理状態を記録するstateは専用のS3に保存している。同時更新の衝突を防ぐため、ロックも使う。一方、日々更新する記事や画像は、サイトのビルド成果物として配置する。これで、本文を少し直すたびにTerraformでインフラを更新せずに済む。
普段の公開手順は次の通りだ。
- Markdownを編集し、必要な画像と記事台帳を更新してGitHubへ登録する。
- 新しい記事URLを追加した場合は、Terraformでルート表を更新する。
- GitHub Actionsを実行し、型チェック、Astroのビルド、リンクや画像の検証を行う。
- 検証した成果物をS3へ差分配置し、CloudFrontのキャッシュを無効化して更新を反映する。
GitHub ActionsからAWSへ接続するときは、OIDCを使って配信専用のIAMロールの短期認証情報を取得する。これで、長期アクセスキーをGitHubに保存せずにデプロイできる。
公開は今のところGitHub Actionsの手動実行を基本にした。設定を変えれば main の更新時に自動公開できるし、手元から実行したいときは make deploy も使える。
Codexに頼むときは、プレビューの起動方法まで指定する
Webアーカイブからの回収と、AWS上で再び公開するところまで、今回の作業は1日で済んだ。コードの実装はCodexにかなり任せられたが、UIについては、自分で画面を開いて確認しながら指示を出した。
特に、目次やサイドバーが本文を読むときに邪魔にならないか、検索結果から目当ての記事を開きやすいか、といった部分は、自分が読者のつもりで操作したほうが判断しやすい。ビルドが通った時点では、そこまでは分からない。
そのため、Codexにはサイトの実装と一緒に、手元ですぐプレビューできるコマンドも用意してほしいと伝える。例えば、開発サーバーの起動やビルド後の確認をMakefileにまとめておく。そうすれば、修正が返ってきたら同じ操作ですぐ画面を開き、「この幅だと目次が読みづらい」と、実際の表示に沿って次の指示を出せる。
今回のように短い時間で作り直すなら、コードを書く速さだけでなく、自分が確認して返事をするまでの時間も効いてくる。次に似た作業を頼むときも、最初の指示にプレビュー方法を含めておくつもりだ。
一度ブログを閉じたとき、ブラウザに残ったブックマークは少し寂しいものだった。今は、そのURLで昔の記事を読める。気になる箇所を見つけたらMarkdownを直して、手元で表示を確かめて、もう一度公開できる。
過去の記事を取り戻したところで終わらず、こうしてその続きを書けるようにした。それが、今回ブログを作り直して一番よかったことだ。