ブログ

  • Criticalが4件並んだ日にやったこと

    Criticalが4件並んだ日にやったこと

    朝、通知を見たらCriticalが4件並んでいました。どれも同じ相手、Next.js本体です。

    この日にやったことを順に書きます。結論から言うと、上げました。ただ、上げる前にやったことのほうが本題です。

    この記事で扱うこと

    脆弱性の通知が来たときに、何をどの順で確かめたか。自分のサイトが実際に影響を受けるのかを切り分ける手順と、それでも上げた理由です。

    まず、中身を読む

    4件は、実は2種類でした。同じものが2つのファイルで検出されているので、件数が倍に見えていました。使っているものを書いた一覧と、実際に入った版を固定した一覧の2つです。

    種類 内容
    A Windowsで動かしているサーバーでの、認証不要の遠隔コード実行
    B 画像最適化の機能で、AVIF形式を扱ったときの遠隔コード実行

    どちらも「認証不要」で「遠隔からコードを実行される」。言葉としては、これ以上ないくらい重い部類です。

    Aは、条件に当てはまらなかった

    Aの説明には、条件がはっきり書いてありました。WindowsのファイルシステムでNext.jsのサーバーが動いている場合に成立する、と。

    そして、LinuxとmacOSは影響を受けないと明記されていました。

    このサイトの本番はVercelで動いています。Linuxです。開発しているのもMacです。どちらも対象外でした。

    「Critical」は自分への影響度ではない

    重大度は、その脆弱性が最悪の場合にどれだけひどいかを表しています。自分の環境で成立するかどうかは、また別です。

    色と数字だけ見て慌てると、条件を読まずに動くことになります。読めば1分でした。

    Bは、入口の広さを自分で確かめた

    Bのほうは、条件が「AVIF形式の画像を最適化したとき」です。攻撃者が用意した画像を最適化させられると成立します。

    このサイトは画像の最適化機能を使っています。ここで確かめるのは、「誰でも好きな画像を最適化させられる状態か」です。

    設定を見ると、最適化を許可している場所が2つだけ指定してありました。

    remotePatterns: [
      { hostname: "localhost", port: "8080", ... },
      { hostname: "cms.kisaku.site",
        pathname: "/wp/wp-content/uploads/**" },
    ]

    つまり、CMSの画像置き場にあるファイルしか最適化できません。任意のURLを渡しても弾かれます。

    そこに置くには、CMSにログインして画像をアップロードする必要があります。入口はかなり狭いと言えます。

    「狭い」は「無い」ではない

    ここで止めたくなりますが、狭いだけで塞がってはいません。

    CMSのログインが破られれば成立します。そしてCMSは共用サーバー上のWordPressで、攻撃対象としてはありふれた構成です。「入口が狭いから大丈夫」は、入口が破られない前提に乗っています。

    結局、上げた

    Aは対象外、Bは入口が狭い。それでも上げました。

    理由は単純で、上げない理由のほうが薄かったからです。

    上げる 上げない
    作業量 修正版への更新1回 0
    壊れる可能性 細かい版の更新なので低い —
    放置した場合 — 入口が破られた時点で成立する

    大きな作り替えを伴う更新なら、慎重になる理由があります。今回は末尾の数字が1つ上がるだけの修正版で、その中身も「問題のある機能を止める」というものでした。

    本番に入るものと、そうでないものを分けて数える

    更新したあと、残りを確かめました。ここで1つ、見方を変えています。

    そのまま数えると12件残っていました。ところが「本番に配るものだけ」で数え直すと0件です。

    npm audit --omit=dev
    found 0 vulnerabilities

    残っていた12件は、全部開発中にしか動かない道具のものでした。型を生成するツールが使っている、さらにその中のライブラリです。

    配られないものは、訪問者に届かない

    これらはビルドのときに自分のマシンで動くだけで、配信されるファイルには入りません。訪問者のブラウザにも、サーバーにも届きません。

    0件にはしたいのですが、直すには型生成ツールを大きく更新する必要があり、そちらのほうが壊れる可能性が高い。今回は見送って、理由を書き残しました。

    数字を0にすることが目的になると、危険度の低いもののために動いているものを壊します。分けて数えると、どちらを急ぐかが決まります。

    やったことをまとめると

    順番 やったこと かかった時間
    1 4件が実は2種類だと確認 1分
    2 条件を読んで、対象外を1つ外した 2分
    3 残りの入口の広さを設定で確かめた 5分
    4 更新して、型検査を通した 5分
    5 本番に入るものだけで数え直した 1分

    手を動かしたのは4だけで、残りは読んで確かめる時間でした。

    今回の学び

    まず、重大度は「最悪の場合」の話であって、自分への影響ではないということ。条件が書いてあるので、読めば当てはまるかどうかが分かります。今回は片方が最初から対象外でした。

    次に、「入口が狭い」で止めないこと。狭いのは事実でしたが、狭さはこちらの設定に依存しています。設定を変えた日に広がるかもしれません。狭いことを理由に放置すると、その理由を覚えている人がいなくなった時点で危なくなります。

    そして、数える範囲を先に決めること。全部で12件と、配信されるものの中で0件では、判断がまったく変わります。件数を減らすこと自体を目的にすると、動いているものを壊しにいくことになります。

  • 印を付けただけでは、探せるようにならなかった

    印を付けただけでは、探せるようにならなかった

    記事一覧のカードには、前から「読了済み」の印を付けていました。

    付けた当時は57本でした。いまは110本を超えています。印は役に立たなくなっていました。

    この記事で扱うこと

    読了の印だけでは足りず、未読だけを出す絞り込みを足した話です。URLに入れなかった理由、読んだ人にだけボタンを出した理由、そして0件の見せ方を書きます。

    見分けることと、探すことは違う

    印が付いていれば、目の前のカードが既読かどうかは分かります。見分けることはできます。

    けれど「まだ読んでいないものを見たい」ときにやることは、100枚のカードを上から順に見て、印の付いていないものを拾う作業になります。これは探す作業で、印はそれを助けていません。

    それは一覧の仕事だった

    並んでいるものから条件に合うものを取り出すのは、一覧の側がやることです。

    印だけ出して残りを目でやらせるのは、判定の材料は渡したが、判定はそちらでやってくれと言っているのと同じでした。

    URLには入れなかった

    この一覧には、すでにカテゴリとタグの絞り込みがあります。どちらも /blog?category=tech のようにURLに入ります。

    未読の絞り込みは、同じ形にしませんでした。

    絞り込み 誰にとっても同じか URLに入れるか
    カテゴリ・タグ 同じ。誰が開いても同じ記事が出る 入れる
    未読 人によって違う 入れない

    URLに入れるということは、その状態を他の人に渡せるということです。カテゴリなら渡せます。「技術系の記事はここ」と共有できます。

    未読は渡せません。渡された側にとっては、自分と関係のない条件で記事が消えている一覧になります。共有したつもりの人も、渡された人も、何が起きたか分かりません。

    読んだことがある人にだけ出す

    ボタンは、1本でも読んだ記録がある人にだけ出しています。

    初めて来た人にとっては、全部が未読です。押しても何も変わりません。

    効かないものが置いてあると、その場所ごと見なくなる

    押して何も起きない経験を一度すると、そのボタンは「効かないもの」として記憶されます。

    後日ちゃんと効く状態になっても、もう押されません。意味を持つまで出さないほうが、意味を持ったときに使われます。

    0件を、失敗のように見せない

    未読だけにして0件になる状態は、全部読み終えたという意味です。良い状態です。

    ところが検索の0件と同じ文面を出すと、何かを間違えたように見えます。そこで分けました。

    状況 出す文面
    検索で0件 「〜に当てはまる記事はありませんでした」+用語集への案内
    未読が0件 「記事はすべて読み終えています」+絞り込みを解除するボタン

    解除のボタンを添えているのは、行き止まりにしないためです。0件の画面で、絞り込みを外す手段がその場に無いと、戻り方を探すことになります。

    戻ってきたときに、数が合わない

    実装して最初に気になったのが、ここでした。

    一覧から記事を開いて読み、ブラウザの「戻る」で一覧に帰ってくる。このときページは作り直されません。離れたときの画面がそのまま出てきます。

    いま読んだ記事が、未読のまま残る

    読了の記録は更新されているのに、一覧は古いままです。「未読だけ」と書いてあるのに、たったいま読んだ記事が並びます。

    戻ってきたときに発生する合図(pageshow)で読み直すようにしました。結果として、戻った瞬間にいま読んだ記事が一覧から消えます。目の前で消えるのは少し驚きますが、「未読だけ」と書いてある以上、消えるほうが正しいと判断しました。

    読了の記録は、こちらには届いていない

    念のため書いておくと、どの記事を読んだかという記録はその人のブラウザの中にだけあります。こちらのサーバーには送っていませんし、見ることもできません。

    だからこの絞り込みは、サーバー側では掛けられません。ブラウザ側でやっているのはそのためです。

    今回の学び

    まず、材料を出すことと、仕事をすることは違うということ。印は判定の材料でしたが、判定そのものは読む人に残っていました。数が増えると、その差がそのまま手間になりました。

    次に、渡せる条件と渡せない条件があること。URLに入れるかどうかは、実装の都合ではなく「他の人に渡して意味があるか」で決まりました。

    そして、0件には種類があること。見つからなかった0件と、やり終えた0件は別のものです。同じ文面で済ませると、良い知らせが悪い知らせに見えます。

  • 崩れる理由が分かるCSS — ボックスモデルと詳細度(第2回)

    崩れる理由が分かるCSS — ボックスモデルと詳細度(第2回)

    「なぜか効かないCSS」と「なぜか幅が合わない」。CSSで詰まる場所は、ほぼこの2つに集約されます。

    原因は文法ではありません。要素同士の影響関係です。今回はそこだけを扱います。

    そもそもCSSとは

    HTMLで書いた要素に、見た目を指定する言語です。

    HTMLが「ここは見出し」と宣言するのに対し、CSSは「見出しは32pxの太字で青色」と決めます。同じHTMLでも、CSSを差し替えるだけで全く別のデザインになります。

    セレクタ { プロパティ: 値; }
    h1      { color: navy; }
    

    書き方はこの1パターンだけ。覚えることが多いように見えるのは、プロパティの種類が多いからで、文法そのものは10秒で理解できます。難しいのは文法ではなく、要素同士の影響関係です。今回扱うのはそこです。

    この回で解ける困りごと

    「なぜか効かないCSS」と「なぜか幅が合わない」。原因はほぼ2つ、詳細度とボックスモデルです。

    よくある書き方(Before)

    .card { width: 300px; padding: 20px; border: 1px solid #ccc; }
    

    3枚並べると横に収まらない。width: 300px と指定したのに、実際の幅は342pxだからです。

    直した書き方(After)

    *, *::before, *::after { box-sizing: border-box; }
    
    .card { width: 300px; padding: 20px; border: 1px solid #ccc; }
    

    border-box にすると、paddingとborderを含めて300pxになります。これはほぼ全ての現場で最初に書く1行です。

    なぜそうなるか

    ボックスモデル

    要素は内側から content → padding → border → margin の4層です。初期値の content-box では width が最内側のcontentだけを指すため、余白と枠線が外側に足されます。

    詳細度

    CSSが効かないときは、後から書いたものが勝つとは限りません。強さの順は次の通りです。

    インラインstyle  >  #id  >  .class / :hover / [属性]  >  要素名
    

    !important で殴ると一時的に直りますが、次に上書きしたい人がさらに !important を書くことになります。詳細度は上げず、揃えるのが原則です。

    並べる2つの道具

    /* 横一列。中央寄せや折り返しが得意 */
    .list { display: flex; gap: 16px; flex-wrap: wrap; }
    
    /* 格子。列数を決めたいときはこちら */
    .list { display: grid; gap: 16px; grid-template-columns: repeat(3, 1fr); }
    

    使い分けは「1方向に並べるならFlex、行と列の両方を揃えたいならGrid」。迷ったらどちらで書いても動きます。

    手を動かす課題

    第1回で作った一覧に、カード型のCSSを当ててください。同じ見た目をFlexboxとGridの両方で組むのが課題です。書き終えたら、どちらが少ない記述で済んだかメモしてください。

    自己チェックリスト

    • □ box-sizing: border-box を最初に書いた
    • □ !important を使っていない
    • □ gap で余白を取っている(margin-right の最後だけ消す、をしていない)
    • □ 効かないCSSがあったとき、DevToolsで打ち消し線を確認した

    次回はCSS応用です。画面幅が変わっても崩れない書き方に進みます。

    今回の学び

    まず、幅の指定は、どこまでを幅と呼ぶかの取り決めに依存しているということ。300pxと書いたのに342pxになるのは不具合ではなく、初期値がそう決まっているからでした。

    次に、強さで殴ると、次の人はもっと強く殴ること。一度その手を使うと、以降その場所を触る全員が同じ手を使うことになります。上げるのではなく揃えるのが原則です。

    そして、効かないときは推測せずに打ち消し線を見ること。ブラウザの開発者ツールは、どの指定がどれに負けたかを線で表示してくれます。読むだけで答えが出ます。

  • 生成した画像に、undefinedと書いてあった

    生成した画像に、undefinedと書いてあった

    記事のアイキャッチは、自分で書いたスクリプトで作っています。タイトルと一言、それに題材に合わせた線画を1枚に組んで、画像として書き出す仕組みです。

    連載を続けるために作った5枚を見返したとき、そのうち1枚に「undefined」と書いてありました。

    この記事で扱うこと

    生成した画像に文字化けのような欠損が入っていたのに気づかなかった話です。なぜ止まらなかったのか、なぜ見落としたのか、そして次から止まるようにした変更を書きます。

    線画の代わりに、文字が出ていた

    本来そこには、その回の題材を表す薄い線画が入ります。地図、盾、矢印、電波。背景に溶ける程度の濃さで置いてあります。

    1枚だけ、その位置に undefined という薄い文字が入っていました。

    回 入るはずだったもの 実際
    第1回 地図 地図
    第2回 目次を表す線 undefined
    第3回 盾 盾
    第4回 矢印 矢印
    第5回 電波 電波

    なぜ止まらなかったか

    生成の仕組みは、記事ごとの設定(題名・一言・図柄の名前)を受け取って、HTMLの文字列を組み立てます。その文字列をブラウザに読ませて、写真を撮るように書き出しています。

    図柄は、名前から一覧を引いて取り出します。第2回だけ、その引き当てに失敗していました。

    無いものを文字列に混ぜても、そこでは止まらない

    一覧に無い名前を引くと、返ってくるのは「無い」という値です。

    それをそのまま文字列に埋め込むと、「undefined」という7文字の文字として混ざります。組み立ては成功し、ブラウザも普通に描き、画像も無事に書き出されます。

    どこにも失敗がないので、どこにも警告が出ません。

    なぜ見落としたか

    理由は3つ重なっていました。

    理由 内容
    薄い 背景に溶ける濃さで置いている。意識して見ないと読めない
    1枚だけ 5枚並べて見れば違和感が出るが、1枚ずつ確認していた
    まだ公開前 予約投稿だったので、SNSのカードにも一覧にもまだ出ていない

    気づいたのは、続きの回を作るために元のデザインを見に行ったときでした。同じ見た目に揃えるつもりで拡大したら、書いてありました。

    作った直後には気づかず、別の目的で見返したときに見つかっています。出力を最後にちゃんと見ていなかったということです。

    欠けたら止まるようにした

    直した内容は2つです。

    ひとつは、その1枚を作り直したこと。もうひとつは、生成する前に図柄の指定があるかを確かめて、無ければその場で止めるようにしたことです。

    if (!MARKS[a.mark]) throw new Error('図柄の指定が無い: ' + a.file);
    

    1行ですが、これがあれば今回は画像が書き出される前に止まっていました。

    見て気づく仕組みには、限界がある

    「次から気をつける」は、次に同じことが起きたときの対策になりません。今回も気をつけていなかったわけではなく、薄い文字が1枚だけ混ざっている状態が、気をつけて見つかる範囲の外にありました。

    気をつける代わりに、止まるようにしておく。そのほうが確実でした。

    公開前でよかった、で終わらせない

    今回は予約投稿の段階で見つかったので、公開された画像に混ざることはありませんでした。

    ただ、これはたまたま続きを作る用事があっただけです。無ければそのまま公開され、SNSで共有されるたびにその文字が一緒に流れていました。アイキャッチは、記事の中で一番先に人の目に触れる部分です。

    見つかった理由が偶然である場合、同じ偶然は次に起きないものとして扱ったほうがよいと考えています。

    今回の学び

    まず、欠けた値は、文字として混ざるということ。文字列を組み立てる処理は、中身が欠けていても組み立てには成功します。成功したという事実は、中身が正しいことを何も保証していませんでした。

    次に、1枚ずつ見ると、揃っていないことに気づけないこと。並べて比べれば違和感として出ますが、順番に見ると「そういうもの」として通り過ぎます。

    そして、気をつけるより、止まるようにすること。1行の確認を足すだけで、次からは書き出される前に分かります。注意力に任せていた部分を、仕組みに移し替えられました。

  • 勉強法(4) 教える側に回ると、穴が見える

    勉強法(4) 教える側に回ると、穴が見える

    プログラミングを教えていると、質問が2種類に分かれます。

    片方は調べれば答えが出ます。もう片方は、自分が何を根拠に答えているのか分からなくなります。

    この記事で扱うこと

    教える側に回って見えた、自分の理解の穴。答えられなかった質問の種類と、そのあと説明の仕方をどう変えたかです。

    答えられない質問は、いつも同じ形だった

    質問 答え方
    この書き方が分からない 調べれば出る。一緒に調べればいい
    エラーが出た 読めば書いてある。読み方を渡せばいい
    どっちがいいですか 答えが1つに決まらない

    3つめで詰まります。「そっちでいいと思います」とは言える。ただ、なぜそう思うのかを説明しようとすると、経験からそう思う、としか言えませんでした。

    自分が使えていることと、人に渡せることは別

    自分で書くときは迷いません。手が勝手に選んでいます。

    ところが理由を言葉にしようとすると出てこない。選べているのに、選び方を説明できない状態でした。使えてはいるが、分かってはいなかったわけです。

    教えやすいことと、実務で差が出ることがずれている

    教材にしやすいのは、正解のあるものです。文法、関数の使い方、エラーの読み方。手順が決まっていて、できたかどうかも判定できます。

    ところが、仕事で差が出るのはそこではありませんでした。

    教えやすいこと 実際に差が出ること
    書き方を覚える どこまで作り込むかを決める
    エラーを直す 同じエラーが起きない形にする
    言われたとおりに作る 言われていない前提を確認する

    右の列は、教科書の形になりにくい。正解が状況によって変わるので、「こうしなさい」と言えないからです。

    結論ではなく、後で起きることを渡す

    説明の仕方を変えました。それまでは「こう書くといい」と結論だけを渡していました。いまは「こうすると後で何が起きるか」を先に話します。

    たとえば、同じ設定値を2か所に書いた場合。その場では動きます。問題が出るのは、片方だけ変えたときです。

    「重複は避けましょう」だと、禁止として覚えて終わります。「今は動くが、変えるときに壊れる」だと、理由として残ります。次に似た場面が来たとき、自分で判断できる形になります。

    判断の材料を渡すと、質問が変わる

    この形にしてから、「どっちがいいですか」が「こっちだと後でこうなりますよね」に変わることが増えました。

    答えを渡していたときは、次も同じ質問が来ていました。材料を渡すと、途中まで自分で進んでから聞きに来ます。

    自分の側にも効いた

    説明のために言葉にしたことが、自分の作業にも戻ってきました。

    このサイトのコードには、処理の説明ではなく「なぜその選択をしたか」を書いています。1か所にまとめた理由、別のファイルに切り出した理由、選ばなかった案。

    これは人に説明するときに必要だった形と同じです。半年後の自分は、今の自分が何に困っていたかを覚えていません。困りごとごと残しておかないと、また同じ場所で悩みます。

    残す内容 あとで役に立つか
    何をしているか コードを読めば分かる
    なぜそうしたか コードからは分からない
    選ばなかった案 直すときに、戻って考えずに済む

    教える相手が、自分の穴を教えてくれる

    答えられなかった質問は、そのまま自分が言葉にできていない部分でした。

    1人で書いているかぎり、そこは表に出てきません。動いているからです。聞かれて初めて、説明できないことに気づきます。

    本を読んで穴を埋めたこともありますが、穴の場所を教えてくれたのは、いつも質問のほうでした。

    今回の学び

    まず、選べることと、選び方を説明できることは別だということ。手が勝手に選んでいるうちは、使えてはいても分かってはいませんでした。

    次に、結論より、後で起きることを渡すこと。「こう書きなさい」は覚えて終わりますが、「こうしないと何が起きるか」は判断の材料になります。渡す形を変えただけで、返ってくる質問が変わりました。

    そして、自分の穴は、自分では見つけにくいこと。1人で書いていると、動いている場所は疑いません。答えに詰まった質問の一覧が、そのまま埋めるべき場所の一覧でした。

  • 勉強法(3) 調べる場所が決まらない時間

    勉強法(3) 調べる場所が決まらない時間

    手が止まっている時間の中身を振り返ると、書き方が分からない時間はそれほど長くありませんでした。

    長かったのは、どこを調べればいいか決まらない時間のほうです。

    この記事で扱うこと

    「読めない」「分からない」を、調べられる形まで分解する話です。CMSベンダーにいたころの詰まり方と、いまも同じ構造で出てくることを書きます。

    3つのどれかは分かるのに、どれかが分からない

    製品のベースがRailsの会社にいました。仕事は管理画面やフロントの改修です。

    コードは目の前にあって、動いてもいます。それなのに読めない。理由を探していくと、こういう詰まり方でした。

    目の前の1行が、Railsが用意した仕組みなのか、Rubyの文法なのか、製品独自の実装なのかが区別できない。

    その行が何か 調べに行く先
    フレームワークの機能 そのバージョンのドキュメント
    言語の文法 言語のリファレンス
    製品独自の実装 社内のコードと、書いた人

    3つのどれかであることは分かります。どれなのかが分からない。結果として、調べ始める場所が決まりませんでした。

    検索の言葉すら決まらない

    調べるという行為は、探すものの見当が付いていて初めて成立します。

    見当が付いていないと、当てずっぽうで言葉を変えながら探すことになります。これが時間を食う。しかも、進んでいる実感が無いまま食います。

    「読めない」では、対処が決まらない

    この状態を「経験が足りない」で片付けていたうちは、何をすればいいか分かりませんでした。

    分解して「言語かフレームワークかの区別がつかない」まで持っていくと、読むべき本は1冊に決まります。フレームワークの本ではなく、言語そのものの本です。

    読んだあと、知識の量よりも先に変わったのは手が止まる回数でした。「ここは言語の書き方だ」と分かると、調べ先が決まるからです。

    分解の粒度は、対処が1つに決まるところまで

    「分からない」では何も決まりません。「読めない」でもまだ決まりません。

    「区別がつかない」まで下ろすと、やることが決まります。そこが止め時でした。それ以上細かくしても、やることは変わりません。

    いまも、同じ構造で出てくる

    これは過去の話ではなくて、今も形を変えて出てきます。

    最近だと、記事を投稿しようとしてサーバーに弾かれました。403が返るだけで理由は出ません。ここで手が止まる理由は、Railsのときとまったく同じです。原因がどこにあるか分からないので、調べ始める場所が決まらない。

    このときやったのは、本文を半分に割って試すことでした。知識を足したのではなく、「どこ」を絞り込んだだけです。2回割ったら、引っかかっている行が分かりました。

    Railsのとき 403のとき
    止まった理由 調べ先が決まらない 調べ先が決まらない
    やったこと 3つのどれかを見分ける 範囲を半分に割る
    必要だったもの 言語の知識 絞り込む手順

    片方は知識で解決し、もう片方は手順で解決しました。共通していたのは、詰まりを「どこ」の問題に置き換えたことです。

    教えていて気づいたこと

    プログラミングを教えていると、同じ止まり方をよく見ます。

    「動きません」と言われて画面を見ると、エラーは出ています。読めば書いてある。ただ、そのエラーが自分の書いた行の話なのか、道具の設定の話なのかが分からない状態です。

    このとき答えを教えると、その場は進みます。ただ次に同じ形で止まります。渡すべきだったのは答えではなく、「いまのは3つのうちどれだと思うか」という問いのほうでした。

    今回の学び

    まず、いちばん時間を食うのは、探す時間ではなく探す場所を決められない時間だということ。答えを見つけるのは、場所さえ決まればそれほどかかりません。

    次に、分解は「対処が1つに決まる」ところまでやること。「分からない」のままでは動けませんが、細かくしすぎても意味がありません。やることが決まった時点が止め時でした。

    そして、同じ詰まり方は、分野を変えて何度も出てくること。言語とフレームワークの区別も、403の原因の特定も、困っている形は同じでした。1回分解できると、次はそれが型として使えます。

  • 再試行は入れていた。それでもデプロイが落ちた

    再試行は入れていた。それでもデプロイが落ちた

    再試行の処理は、半年前から入れていました。CMSが500を返したら、少し待ってもう一度投げる。3回まで。

    それでも、デプロイが落ちました。

    この記事で扱うこと

    再試行を入れてあったのに効かなかった話です。何が返ってきていたのか、なぜ3回では足りなかったのか、そして回数ではなく間隔を増やした理由を書きます。

    ページを先に作る構成では、ビルド中に問い合わせが集中する

    このサイトは、訪問者が来てからCMSに聞くのではなく、デプロイのときに全ページを作ってしまう構成です。表示は速くなりますが、そのぶんビルドの最中にCMSへ何十回も問い合わせが飛びます。

    記事が増えたいまは、1回のビルドで82ページぶんの問い合わせが、1分足らずのあいだに走ります。

    失敗の意味が変わっていた

    動的に表示していたころは、1回の問い合わせが失敗してもその1回の表示が失敗するだけでした。次に開けば直ります。

    事前生成に変えたあとは、同じ1回の失敗がデプロイ全体を止めます。新しい記事も、直したところも、まとめて出なくなります。

    速くするために取った選択が、別の場所の壊れやすさを生んでいたことになります。

    返ってきていたのは、WordPressのエラーではなかった

    ビルドのログを見ると、82ページ中40ページを作ったあたりで止まっていました。

    Error: GraphQL Error (Code: 500): {"response":{"error":"<!DOCTYPE HTML PUBLIC ...
    

    500という数字だけ見ると「WordPressが処理に失敗した」と読みたくなりますが、中身はHTMLでした。しかもWordPressのエラー画面ではなく、Apacheが出す素のエラーページです。

    返ってきたもの そこから分かること
    WordPressのエラー(JSON) WordPressまでは届いている。中で失敗した
    Apacheの素のエラーページ WordPressに届く前に断られている

    つまりPHPが動く手前の段階で弾かれていました。短時間に問い合わせが集中したことで、共用サーバー側がまとめて断る状態に入っていたのだと思います。

    3回では足りなかった理由

    入れてあった再試行は、こういう設定でした。

    回 待ち時間 累計
    1回目 — 0秒
    2回目 1秒 1秒
    3回目 3秒 4秒

    最初から最後まで、4秒で終わります。

    相手が「一瞬もたついた」だけなら、これで通ります。実際これまでは通っていました。今回そうならなかったのは、断られている状態が数秒では明けなかったからです。

    同じ壁に3回ぶつかっていた

    再試行は動いていました。ログにも3回ぶんの失敗が残ります。

    ただ、4秒のあいだに3回投げても、3回とも同じ状態の相手に当たります。回数を増やしても、待っている時間が短ければ結果は変わりません。

    回数ではなく、間隔を増やした

    直したのは、待ち時間の表です。

    const MAX_ATTEMPTS = 5;
    const RETRY_DELAY_MS = [1000, 3000, 8000, 15000];
    

    最後まで粘ると、1件あたり最長27秒かかります。ビルドは確実に遅くなります。

    それでもこちらを選んだのは、待って通るほうが、デプロイごと落ちるより安いからです。27秒は待てば終わりますが、落ちたビルドは自分では直りません。気づいて、原因を調べて、押し直すまで止まったままになります。

    なぜ回数を増やすだけでは足りないか

    回数だけ10回に増やしても、間隔が1秒なら全体で10秒です。混んでいる相手にとってはさらに叩きに来ただけになります。

    相手が回復するのを待つのが目的なので、増やすべきは回数ではなく間隔のほうでした。

    気づくのが遅れた理由

    この失敗に気づいたのは、デプロイした数時間後でした。新しく足したページを開いたら404だったからです。

    ビルドが失敗しても、サイトは前の版のまま動き続けます。これ自体は正しい挙動で、壊れた版が公開されるよりずっと良い。ただし見ている側には何も起きません。止まったことは、止まった画面を見ても分からないということです。

    いまは通知を入れていませんが、次に同じことが起きたときに数時間気づかない状態は残っています。ここは別に手を打つ必要があると考えています。

    今回の学び

    まず、速くする変更が、壊れやすさを持ち込むことがあるということ。事前生成そのものは良い選択でしたが、「1回の失敗が何を止めるか」が変わっていました。変えたときに、失敗の重さも一緒に変わります。

    次に、500の中身を見ないと、誰が断ったか分からないこと。同じ500でも、アプリが失敗したのか、その手前で断られたのかで打つ手が違います。今回は中身がHTMLだったことが手がかりになりました。

    そして、再試行は「回数」ではなく「どれだけ待つか」だということ。入れてあること自体は安心材料になりません。相手が何秒で回復するのかを考えないまま回数だけ決めていた、というのが本当の原因でした。

  • 勉強法(2) 測らないと、分かったことにならない

    勉強法(2) 測らないと、分かったことにならない

    「分かった」と思った直後に、間違っていたことが何度かありました。

    いちばん効いたのは、自分の理解を外から確かめる手順を持つことでした。今回はその話です。

    この記事で扱うこと

    このサイトで実際にやった「確かめ方」を3つ。どれも、確かめていなかったら間違ったまま進んでいたものです。

    1. 数字を出したら、始まりと終わりを見る

    記事の読み進み具合を出すバーを作りました。動いています。そう思っていたら、開いた瞬間の数字が目に入りました。

    ページを開いた瞬間に9.5%を指していました。まだ何もしていません。

    調べると、計算そのものは正しかった。画面の下端を基準にしていたので、開いた時点で本文の一部がすでに映っていたのです。「画面に映った本文の割合」としては9.5%が正確な値でした。

    式が答えている問いと、表示が名乗っている問いが違った

    表示が名乗っていたのは「読み進み具合」です。読み進み具合なら、読み始める前は0のはずでした。

    計算間違いではなく、意味のずれです。数字が合っているかを確かめても見つかりません。「0から始まっているか」「最後で100になるか」という、両端を見る確かめ方でしか出てきませんでした。

    2. 試した環境が、確かめたい条件を満たしているか

    同じ読了判定で、もっと危ない間違いをしました。

    ブラウザを自動で動かして、記事を最後までスクロールさせました。読了として記録されません。「記録されない不具合がある」と結論を出しかけました。

    念のため、スクロールのイベントが何回起きたかを数えました。

    発火回数: 0

    自動で動かしているタブでは、スクロールのイベントがそもそも発生していませんでした。不具合が無くても、同じ結果になります。

    結論を取り下げるほうが、直すより先だった

    この時点で分かったのは「読了が記録されない」ではなく、「この方法では確かめられない」ということだけです。

    ここで直しにかかっていたら、動いているものを壊していました。試した環境が条件を満たしているかは、結果より先に確かめる必要がありました。

    そのあとは、ブラウザで試すのをやめて、実際の寸法を使って計算で確かめました。

    3. 原因が分からないときは、半分に割る

    コードを含む記事を投稿しようとしたら、サーバーに弾かれました。403が返るだけで、理由は何も出ません。

    本文は4500字あります。どこが原因かは分かりません。

    そこで、本文を4つに割って1つずつ試しました。

    試した範囲 結果
    1/4ずつ(4回) 2番目と3番目が弾かれる
    そのうち1つを8分割 2か所に絞れた
    怪しい行を単体で 2つの書き方に特定

    最終的に、引っかかっていたのは2つの書き方だけだと分かりました。理由を推測せずに済んだのは、割って試したからです。

    推測で当てにいくと、外れたことにも気づけない

    最初は「コードの記号が多いからだろう」と考えて、記号を別の表記に置き換えました。それでも弾かれました。

    置き換えが効かなかったこと自体が情報で、相手が表記を戻してから見ていると分かりました。推測が外れたと分かったのは、確かめる手順があったからです。

    確かめ方は、覚えるものではなかった

    3つ並べてみると、覚えている知識はほとんど出てきません。

    場面 やったこと
    数字を出した 両端の値を見た
    試したら失敗した 試し方が成立しているか数えた
    原因が分からない 範囲を半分に割った

    どれも技術というより手順です。そして3つとも、自分の理解を疑うところから始まっています。

    今回の学び

    まず、「動いた」は「合っている」ではないということ。進捗バーは最初から最後まで動いていました。動きを見ているかぎり、9.5%から始まっていることには気づけません。

    次に、確かめた結果より先に、確かめ方を確かめること。0回しか起きていないイベントを待っていたら、どんな実装でも同じ結果になります。出た結果を信じる前に、その試し方で違いが出るのかを見る必要がありました。

    そして、分からないときは推測ではなく分割すること。4500字から2つの書き方まで絞るのに、知識は要りませんでした。要ったのは、半分に割って試す根気だけでした。

  • 勉強法(1) 1つ作り切ると、広く当たる

    勉強法(1) 1つ作り切ると、広く当たる

    勉強法の本を読んで、思い出したことがあります。

    いま自分が手を出せる範囲は、勉強して広げたものではありませんでした。このサイトを1つ作り切る過程で、必要になって触っただけです。

    この記事で扱うこと

    ポートフォリオサイトを1つ作るのに、結果として何を触ることになったか。その一覧と、それが「順番に勉強する」やり方とどう違ったかです。

    触ることになったもの

    やりたくて始めた領域は、ほとんどありません。進めようとしたら、そこで止まったから触ったという順番です。

    領域 止まった場面
    DNS ドメインを繋いだら、wwwの有無で別のサイトとして扱われた
    TLS 証明書が発行されるまで、何が起きているのか分からなかった
    共用サーバー 混むと500が返り、公開作業ごと止まった
    WAF コードを含む本文を投稿すると、403で弾かれた
    CMSの設計 本文をコードに書くか、CMSに置くかで運用が変わった
    キャッシュ 常に最新を取る指定のせいで、初回表示が1.7秒かかっていた
    アクセス解析 入れる前に、何を数えたいのかを決める必要があった
    検索エンジン 同じ中身が複数のURLで見える状態を、56件指摘された
    アクセシビリティ 画面では分かる区別が、読み上げでは同じに見えていた
    CSS設計 角丸の値を、どこまで段階として揃えるかで迷った

    この一覧は、事前に立てた計画ではありません。詰まった順に並んでいるだけです。

    どの順で何が起きたのかは、公開するまでに、何をどの順でやったか に並べました。この表の各行が、4つの段のどこで出てきたのかが分かります。

    順番に勉強していたら、たぶん届いていない

    もし「Web開発を学ぶ」という目的で本を選んでいたら、上の表の上半分には行き着かなかったと思います。

    DNSもTLSもWAFも、作る話ではなく、公開してから出てくる話だからです。教材の多くは動くところまでを扱っていて、そこから先は自分のドメインを持たないと出てきません。

    「必要になっていない知識」は、読んでも残らない

    実は、DNSの説明は以前にも読んだことがありました。読んだ記憶はあるのに、いざ設定画面を前にすると手が止まりました。

    困っていない状態で読んだものは、知識としては入っても、使う場所と結びついていません。結びつけるのは、詰まった経験のほうでした。

    1つ作り切ると、全部が繋がって出てくる

    もうひとつ効いたのは、これらがばらばらに出てこなかったことです。

    たとえば「表示を速くする」という話。キャッシュの置き場所を変えたら、公開作業の途中でCMSへ問い合わせに行くようになり、共用サーバーが混んでいると公開ごと止まるようになりました。速度の話をしていたつもりが、失敗したときにどうなるかを一緒に変えていたわけです。

    速度とサーバーの安定性と公開の仕組みは、別々に勉強していたら別々の知識のままでした。1つのものを動かし続けていると、つながっている場所のほうから見えてきます。

    範囲が決まっているから、終わりがある

    「全部やる」ではなく「このサイトを動かし続ける」が目的なので、どこまでやるかが自然に決まります。

    やらなかったことも多い。データベースの設計も、大量のアクセスをさばく話も、チームで開発する話も出てきていません。出てこなかったのは、必要にならなかったからです。

    作り切る、の意味

    ここで大事なのは「作る」ではなく「作り切る」のほうでした。

    手元で動かすところで止めていたら、公開に関わる領域は一切出てきません。公開しても放置していたら、運用で出てくる問題に当たりません。

    実際、上の表のうち半分以上は公開したあとに出てきたものです。作り終えてからのほうが、触る範囲は広がりました。

    どこで止めるか 出てこないもの
    手元で動いた ドメイン、証明書、サーバー、公開の仕組み
    公開した 検索エンジン、解析、速度、壊れたときの対処
    使い続けている —

    証拠が残るのも、同じ理由

    この一覧は、そのまま記事の一覧でもあります。詰まった場面を1つずつ書いてきた結果、60本を超えました。

    「勉強しました」と言うより、詰まった場所と直し方が並んでいるほうが、何ができるかは伝わります。狙って作った実績ではなく、作り切る過程の副産物です。

    今回の学び

    まず、触る範囲は、目的の広さではなく深さで決まるということ。「広く学ぶ」と決めて広がったのではなく、1つを最後まで進めたら、必要な順に出てきました。

    次に、困っていないときに読んだものは、使う場所と結びつかないこと。同じ説明でも、詰まった後に読むと入り方が違いました。順番としては、詰まるほうが先でした。

    そして、「作る」と「作り切る」では、出てくるものが違うこと。手元で動いた時点で止めていたら、触ったのは表の下半分だけだったはずです。公開して、使い続けて、初めて出てくるものがありました。

  • divだらけをやめる — セマンティックHTML(第1回)

    divだらけをやめる — セマンティックHTML(第1回)

    見た目は作れる。ただ、自分が書いたHTMLを読み返すと、タグの選び方が雑だと分かる。ここで止まっている人は多いと思います。

    HTMLは見た目を作る言語ではなく、意味を伝える言語です。そこが入れ替わっていると、何度書き直しても同じところで雑になります。

    そもそもHTMLとは

    文書の構造を説明するための言語です。プログラミング言語ではありません。

    Wordで文章を書くとき、見出しには見出しスタイル、本文には本文スタイルを当てますよね。あれと同じことを、タグという記号でやります。<h1>と書けば「ここは大見出しです」、<p>と書けば「ここは段落です」という意味になります。

    重要なのは、HTMLが意味を伝える担当だという点です。見た目はCSS、動きはJavaScriptが担当します。HTMLで見た目を作ろうとすると、この分担が崩れます。

    この回で解ける困りごと

    「見た目は作れるけど、タグの選び方に自信がない」。これを今回で終わらせます。

    よくある書き方(Before)

    <div class="title">旧車図鑑</div>
    <div class="list">
      <div class="item">
        <div class="name">スカイライン GT-R</div>
        <div class="text">1969年発売。</div>
      </div>
    </div>
    

    見た目はCSSでいくらでも作れます。問題は、このHTMLが何も意味を持っていないことです。

    直した書き方(After)

    <h1>旧車図鑑</h1>
    <ul class="list">
      <li>
        <article>
          <h2>スカイライン GT-R</h2>
          <p>1969年発売。</p>
        </article>
      </li>
    </ul>
    

    なぜそうなるか

    HTMLは「見た目を作る言語」ではなく「文書の構造を説明する言語」です。構造が正しく書かれていると、次の3者が正しく読めます。

    • 検索エンジン — 何が見出しで、何が本文かを判断できる
    • スクリーンリーダー — 見出しだけを拾って読み上げ、目の見えない人がページを移動できる
    • 未来の自分と同僚 — div.item の中身を読まなくても、記事の繰り返しだと分かる

    覚えるタグはそう多くありません。まずはこれだけ。

    タグ 使う場面
    h1〜h6 見出し。レベルを飛ばさない
    p 段落
    ul / ol / li 並列に並ぶもの
    article 単体で切り出しても意味が通るまとまり
    section 見出しを持つ意味の区切り
    nav ページ間の主要なリンク群
    header / footer 冒頭と末尾のまとまり
    button クリックして何かが起きるもの

    特に事故が多いのが最後の button です。<div onclick="..."> はキーボードで操作できず、Tabキーで選べません。クリックできるものは button か a にする、と覚えてください。

    手を動かす課題

    好きな題材で、一覧ページのHTMLを書いてください。CSSはまだ書きません。条件は3つです。

    1. h1 はページに1つだけ
    2. 一覧は ul と li で組む
    3. div は「意味はないがCSSのために囲みたい」場所だけに使う

    書けたら、Chromeの拡張機能「HeadingsMap」でアウトラインを見てください。見出しがきれいな階層になっていれば成功です。

    自己チェックリスト

    • □ 見出しレベルを飛ばしていない(h2の次にh4が来ていない)
    • □ 画像に alt を書いた(装飾目的なら alt="")
    • □ クリックできる要素は button か a になっている
    • □ CSSを全部外しても、文書として読める

    次回はCSSの基礎です。このHTMLに、崩れないレイアウトを与えます。

    今回の学び

    まず、見た目はCSSでいくらでも作れるということ。だからHTMLの良し悪しは見た目では判定できません。判定できるのは、CSSを全部外したときに文書として読めるかどうかでした。

    次に、構造は3者に向けて書いていること。検索エンジン、読み上げソフト、そして未来の自分と同僚です。3者とも、書いた本人がその場で見ている画面とは違うものを見ています。

    そして、クリックできるものはbuttonかaにすること。これだけは例外なしで覚えてよい規則です。見た目が同じでも、キーボードで操作できるかどうかが変わります。