おそらくはそれさえも平凡な日々

ISUCON14参戦記 (カラアゲネイティブ 14,987点)

今年もISUCONに参戦した。今年も無事に開催され、素晴らしい出題と運営で良かった。ありがとうございました。

いつものカラアゲネイティブなメンツで出ようと思っていたが、 @toricls さんが出場できなかったため、私が技術顧問を務めているMOSH社CTOの村井さんを無理やり誘ってチームを組んだ。と言うことで、今年のカラアゲネイティブは、 @motemen, @RyosukeIketeru, @songmu の3名。言語はGo。リポジトリはこちら。

https://github.com/motemen/isucon14

序盤の作戦会議

前半戦

この頃合いに、motemenと村井さんがpt-query-digestを見ながらインデックスの追加だったり、テーブル構造の変更などをやってくれていた。

だいたい、前段のやるべきことは終え、後半戦は想定通り本命のマッチングと通知の改善かな、と言うところに至った。ただ、この時点で15時近くなっていた。てこずって時間をかけすぎたのでこのあたりもう少しスムーズにやりたかった。何はともあれ、通知周りの改善は他の二人に任せ、僕はマッチングの改善に取り組むことにした。

後半戦

と言うことで、終了間際に回したベンチが通って、14,987点でフィニッシュ。順位は多分103位。

反省

技術系アドベントカレンダーの歴史に思うこと

この記事は pyspa Advent Calendar 2024 の14日目の記事です。この記事で言いたいことを先にまとめると以下になります。

技術系以外のトピックでもアドベントカレンダーが作られることがありますが、この記事では便宜上それらも含めて技術系アドベントカレンダーと呼称します。

技術系アドベントカレンダー

日本の主に技術系のインターネット界隈では毎年12月になると、技術系アドベントカレンダーというムーブメントが発生します。ある技術トピックに対して、12月1日から25日まで複数人が持ち回りでブログを書くというのが基本的なスタイルです。

ニッチなトピックに対して一人でがんばって全部書くスタイル、技術以外のトピックのカレンダー、企業単位で社員持ち回りで書くなど、様々な派生を見せています。私が所属している株式会社ヘンリーも去年、今年と実施しています。

これは、お互い背中を押し合って、普段ブログをなかなか書く機会がない人がブログを書く機会になったり、結果として多くの有用なコンテンツがインターネットに放流されたりするので、良いイベントで、長く続いて欲しいと願っています。

技術系アドベントカレンダーの歴史はこれまでも多くの場所で語られていますが、今後この文化を長く続けるためにも歴史を知っておくことは有用だと思うので、改めて歴史をひも解きます。

原義の物理アドベントカレンダー

そもそもアドベント(待降節・降臨節)とは、11月末からクリスマスイブにかけて、キリストの降誕を待ち望む期間のことと日本語のWikipediaに書かれています。

そして、アドベントカレンダーとは、その期間に使う、ビンゴ的UIの日めくりカレンダー的アイテムです。それぞれの日付の扉を開けると、絵や聖書の一節、お菓子などが現れる仕組みです。カレンダーの開始日はその年のアドベントの始まりの日もしくはシンプルに12月1日、終了日は12月24日か25日のようです。これも、英語版のWikipediaに書かれていました。

つまり、降臨を待ち望む期間の日めくりのカウントダウンカレンダー的なアイテムであり、特に子供向けで毎日小さなお菓子が出てくるものが良く知られています。日本でも近年はカルディなどおしゃれな輸入食品を扱うようなお店で見られるようになりました。

つまり、そもそもは宗教的な催しでありアイテムなのです。

技術系アドベントカレンダーの萌芽

このアドベントカレンダーをモチーフに2000年に作られたのが、英語のPerl Advent Calendarです。これが恐らく技術系アドベントカレンダーの発祥です。2000年から始まったこの本家アドベントカレンダーがまだ今年まで存続しており、アーカイブも残っていることに感動を覚えます。

Perl Advent Calendar Archives

2000年から2004年にかけては、創始者のMark Fowler氏個人によるPerlモジュール紹介リレー形式になっていました。そして、彼が当初書いたAboutページがまだ残っており、その冒頭が奮っています。

This goes along way to proving what I always say: I come up with the best ideas when I'm hung over.
-- https://perladvent.org/2000/about.html

訳すと「これは私が常々言っていることを証明するものだが、二日酔いの時に最高のアイデアが浮かぶ。」と言った具合でしょうか。このページを読み進めると、London.pmの会合がその二日酔いの原因で、その勢いで翌日の昼休みにこのAdvent Calendarを作ったと書かれています。

それが、2024年まで継続していることは胸熱ですが、海外では他言語や技術コミュニティにその文化が輸出されることはあまりされていないようです。これは私の観測範囲の問題かも知れないのでご存知の方がいれば教えて下さい。

ちなみに私も2015年に "Perl and Redis" という記事を寄稿しました。お誘いのメールをいただいたときは大変嬉しかったので引き受けたことを覚えています。翌年も誘ってもらったのですが、それ以降残念ながら寄稿できていません。

日本への輸入

この技術アドベントカレンダーが日本のPerlコミュニティにより輸入されたのが2008年です。これも、ちゃんと当時のコンテンツが残っていて素晴らしいですね。

JPerl Advent Calendar 2008

記念すべき初日の記事は定数の展開という記事で、サンプルコード含めて6行しかなく、何なら誰が書いたかすらも書かれていない、非常にシンプルなTipsの紹介記事です。

始まりの経緯はtokuhiromさんの技術的アドベントカレンダーの有用性についてという記事に残っています。初年度は前日にアップした人が翌日の人を指名しながら、バトンを繋いでいく形式で、必然的に5分でさくっと書けるようなtipsが集まっていたようです。確かに毎日ちょっとしたお菓子が食べられるという原義のアドベントカレンダーともコンセプトがマッチしています。

バトン形式で繋いでいく方式も緊張感はありますが、その分全日埋まることは期待されていなかったように感じます。逆に案外初年度がちゃんと埋まってしまったというところでしょう。上記の記事内の寿司奢る云々も多分ネタだったのではないでしょうか。

実際、その後のエントリー形式になった2012のHacker Trackの3日目で、gfxさんが体調不良により記事を落としています。

@__gfx__は病欠です

今では見られませんが、代理でgfxさんのアイコンがぐるぐる回るアニメーションが投稿され、コミュニティ内で楽しんでいたのを覚えています。当時はそういうゆるさがありました。

上で"Hacker Track"と書きましたが、2年目の2009年ではHacker TrackとCasual Track、その他2トラック合わせて合計4トラック構成になりました。

私もこの2009年のCasual Trackの15日目に「PerlでEmEditorマクロを書こう」という記事を初寄稿しています。Perlコミュニティに初めて参加できた喜びを感じたのを覚えています。

ちなみに、当時の記事投稿方法はCodeReposのSubversionリポジトリのコミット権をYappoさんから貰い、はてな記法で書いた記事をコミットするとサイトが更新されるという方式でした。

この記事のエントリのタイトルの通り、私は当時はWindows上のEmEditorでPerlを書いていました。しかし、TortoiseSVNで上手くCodeReposにコミットできず、焦って当日ヨドバシカメラにMacbookを買いに走り、セットアップして、なんとか記事のコミットに漕ぎ着けたことを覚えています。これが私にとっての初Macでした。これはもちろんWindowsやTortoiseSVNの問題ではなく、当時の私が何も分かっていなかったという笑い話です。

2010年は8トラック、2011年は9トラックとなり、この頃がPerlのアドベントカレンダーの最盛期と言えるでしょう。その後、独自サイトはやめて、2013年からはQiitaで記事を募る形をとっています。

独自進化と定着期

これが日本では他の技術コミュニティに速やかに横展開され、すでに2011年時点でかなりの数が実施されていることが以下の記事に記録されています。

また、2012年にエンジニア向けナレッジシェアサービスであるQiitaにアドベントカレンダー機能が追加され、同年に技術記事以外にも気軽に使えるアドベントカレンダープラットォームであるAdventarがリリースされたことにより、アドベントカレンダーの開催がとても簡単になりました。それが追い風となり、その後数えきれない程の技術系アドベントカレンダーが作られることになり、完全に独自の文化として定着して今に至ります。

今の技術系アドベントカレンダーは毎日ちょっとしたお菓子が出てくるというよりも、毎日ホールケーキが出てくるような様相を呈していますが、それも面白い変化です。ただ、力を抜いた昔のようなアドベントカレンダーもあっても良いと思っています。

上記2サービスは、日本の技術系アドベントカレンダーの発展に大きく寄与したと言えます。しかも、Adventarは @hokaccha さんの個人サービスです。彼はGitHub Sponsorを開けているのでご利用の方は是非スポンサーを検討してみてください。私は先程小額ですがスポンサーしました。

https://github.com/sponsors/hokaccha

文化の盗用への懸念

ここまで書いてきた通り、この技術系アドベントカレンダーは、元々宗教色のある物がアレンジされ、日本で独自発展しているものです。私はこれが文化の盗用(cultural appropriation)のような形で批判されないか少し心配しています。

私としては元々の文化が理解されて敬意が払われており、アドベント期間を有意義に過ごすためのアイデアというコンセプトを外さなければ問題無いと考えています。元々の本家の英語のPerlアドベントカレンダーがアドベント期間に開催されているように。

ただ、アドベント期間を外れているのにアドベントカレンダーを名乗るのは良くないと私は考えています。それは元の文化への理解に欠ける行為に感じるからです。

そういった一部の逸脱が行きすぎた結果、それが文化の盗用だという妥当な批判をされ「アドベントカレンダーという名前は適切じゃないからみんな使うのを止めよう」となってしまうかも知れません。それは、この文化が好きな私としては悲しいですし、そうなって欲しくありません。

この技術系アドベントカレンダー文化を長く楽しく続けるためにも、歴史や元のコンセプトの理解が大切だと思い、このエントリーをしたためた次第です。

ソフトウェアエンジニアという人生の選択肢

最近、Webエンジニア界隈で、共通項を感じる印象的な出来事があった。具体的には以下の2件。

共通項はそれぞれ長めのブランクがありながら、ソフトウェアエンジニアリングの世界に戻ってきて一線級以上の活躍をしているということだ。二人とも僕と同世代かそれ以上の年齢でもある。これは勇気と希望をもらえることだ。

もちろん彼らの能力の高さゆえに第一線に戻ってこられたのかもしれない。ただ、どちらにせよ、別のことに興味があれば、職業エンジニアを離れて、フォーカスする期間があっても良いと言うことだ。能力不足ならなおさら中途半端になるよりフォーカスしたほうが良いとも言える。

それに多分戻ってこられる。ゆーすけべーの様に世界的エンジニアになるのは難しいにせよ、別に満足に働けるくらいには戻せるのではなかろうか。

技術は日進月歩で、キャッチアップを怠ると途端に置いていかれる不安があるかも知れない。でもそこにしんどさを感じ始めているのなら無理しないほうが良い。それに、そんな厳しい業界だったら新しい人が誰も参入できなくなって、消え去ってしまう。実際には優秀な若者が新たにどんどん業界に入ってきてくれている。

もちろん、若い人の方が我々よりも優秀であるという事実はあるが、経験や結晶性知能で勝っている部分もある。AIなどのテクノロジーに補助してもらえる部分も増えている。眼鏡がそうであるように。何より、いくらAIが発展しようと、ソフトウェアエンジニアは足りない状況が続くので、少ないパイを競って蹴落としあう必要はなく、寧ろ皆で高めあっていく必要がある。

人事になりました

私事ですが、10月から人事に異動しました。正式には「株式会社ヘンリー 経営管理部門 人事本部 VP of Engineering」というタイトルです。

私はなんだかんだでこれまで兼務ベースで約10年エンジニア採用やエンジニアリング組織開発にも携わっていて、知見やノウハウもあるのでフォーカスする期間があっても良かろうというところ。

今後はエンジニアとしての発信だけではなく、採用や組織的な発信も増やしていければ良いかと思っている。エンジニアとしての経験を活かし、人事関係のノウハウを抽象化してパブリックに公開するというのをもっとやりたい。

なので、今はプロダクションコードを書いていないが、またいずれコードを書く仕事に戻る気持ちは全然ある。今、どういう役割の帽子をかぶって、どこにフォーカスするかが大事。必要に応じて柔軟に帽子をかぶり変えて行きたい。

冒頭の話もあって、いつだって戻ってこられる安心感が強まったから別のところにフォーカスしようと思えたのもある。元々、プレイングマネージャー否定派ではあった。プレイングマネージャーやってた時期も長いけど。

家族や子育てにフォーカスしたって良い(当たり前)

別に子育てに限った話ではないが、結婚や子育てを機に、以前ほど趣味のエンジニアリングに時間や情熱を割けなくなっていることを不安に感じている人が結構見られる。私も感じることはある。

でも別に働くことや趣味のエンジニアリングを緩めて、家族や子育てに集中する時期があっても良いと思う。それは間違いなく人生を豊かにする。私自身も最近それを強く感じるようになった。自己正当化バイアスかもしれないが、そんなバイアスなら歓迎である。

これも当たり前だけど、家族や子育てに比重を置くのもあくまで個々人の選択であって、別のところに目を向けてみたって良い。

怖いのは情熱が枯渇すること

結局怖いのは情熱が枯渇すること。例えばエンジニアリングへの情熱が以前より失われているとかそういったこと。枯渇させないためにも、休んだり、気分転換で別のことをやったり、別のことにフォーカスしてみたりすることが大事。

何かにフォーカスすることで情熱が生まれることもある。そして何かにフォーカスしないとなかなか情熱は生まれづらい。その結果として、エンジニアリングへの情熱が失われたとしても、他のことに情熱を持てればそれでも良いとも言える。中途半端に色々なことをこなすことに終始して、消耗してしまうのが巧くない。

自分の人生のリソースには限りがあり、やりたいことを全部やれないもどかしさを抱えながら生きる人も多い。私もそう。寧ろそういう人が幸せと言える。そういう中でフォーカスポイントを変化させながら情熱を持ち続けることが幸せに生きるコツなのだと思う。選択と集中である。

「選択と集中」における選択の重要性

「選択と集中」はよく言われる言葉で、集中・フォーカスすることの大事さはよく説かれるが、それと同じように選択できることも大事である。

歴史上の選択と集中の失敗例として、発展途上国が一部の一次産品に生産を集中した結果、余計貧困が進んだ、と言う話がある。いわゆるモノカルチャー経済である。選択し直せる選択肢を失って袋小路に入り込んでしまったという点が示唆的である。

人生でも事業でも、その時その時のフォーカスポイントを定めることはとても重要だが、それと同時に定期的に選択し直せるように選択肢を確保しておくことも非常に重要なのだ。

ソフトウェアエンジニアは人生において有力で魅力的な選択肢だと思う。私自身もそういう選択肢を常に持っていることは幸せだし、人生を充実させてくれるものだと確信している。万人に向いてるかどうかは分かりませんが、オススメです。

緩やかに変化し続けるソフトウェア

当ブログのRSSを全件配信するようにした。Perl製OSSの拙作ブログエンジンであるところのRiji側に手を入れた。ファイルサイズが大きくなるし、RSS分割を実装するのもめんどいので単純に直近30件配信にとどめていたが、今日日普通に1ファイルで全件配信して良いだろうと思い変更した。時代の流れで富豪的アプローチが許容される(?)よくある話。

ちなみに、全件配信しようと思ったきっかけは、ポッドキャスト「趣味でOSSをやっている者だ」を始めるにあたって、RebuildのRSSを観察したところ、全件配信しているのに気付いたので、じゃあいいか、となったというのがありました。

その昔の以下のnaoyaさんの19年前の記事で、RSS内に単独エントリの全文配信の是非について書かれているが、今や全件全文配信である。

RSSの全文配信をはじめました

Riji v1.1.1をリリースした

https://github.com/Songmu/p5-Riji/releases/tag/v1.1.1

ということで、実に2年10ヶ月ぶりのリリースとなった。最新のPerl 5.40.0で依存モジュールも最新化しても、ちゃんとテストもビルドも通るのが素晴らしい。Perlの後方互換を大事にする文化の賜物だと感じる。もちろんPerl自体の変化が緩やかになっていて、ライブラリの更新が活発にされることが減っているのも一因にあるとは思うけど。

Carmelとcpm導入して依存ロックした

https://github.com/Songmu/p5-Riji/pull/39

依存をバージョンロックしなくてもそんなに困っていなかったのだけど、ghcrに上げるコンテナビルドの再現性のために依存モジュールのバージョンをロックすることにした。令和だし。具体的には、Carmelを導入して、cpanfile.snapshotを作って、Dockerビルド時にはcpmでモジュールインストールするようにした。cpmがcpanfile.snapshotをちゃんと見てくれるので良かった。ちなみに、CPANに上げるtarballにはcpanfile.snapshotは含めないようにしている。

この辺のツールチェインがちゃんと動くのは嬉しい。このサイト構築に使っているコンテナもビルドし直せたので、まだまだ戦える。

緩やかに変化し続けること

ソフトウェアを数年放置してても、ちょっとメンテナンスすれば、ちゃんと最新に更新できるのは素晴らしい。Perlのエコシステム含めた変化が遅くなって、追随しやすくなっているという側面はあるが、Perl自体は毎年更新されて新バージョンがリリースされているので進化は止まっていない。

最近ソフトウェアの変化の速度について思うところがある。変化し続けることは必須だが、速すぎる変化はソフトウェアの寿命を縮めてしまうのではないか、当事者が燃え尽きやすくなってしまったり、追随できない人を振るい落としすぎてしまうのではないか、そんなことである。着実に変化・進化し続けられるラインを模索する必要がある。

Perlの使用をもはや積極的に勧めるものではないが、このRijiのように、普段は塩漬けにしておいて、数年に一回くらいお手入れをするくらいで使い続けられる、そんなソフトウェアもあって良いと思っている。

R2を同期するr2syncというツールをRustで書いてcrate公開した

https://crates.io/crates/r2sync

コマンドラインツールであり以下でインストールできる。

$ brew install Songmu/tap/r2sync
# or
$ cargo install r2sync

これはローカルディレクトリの中身をCloudflare R2に簡易的に同期するごく単純なツールで以下のように使う。

$ r2sync ./dir r2://your-bucket/path

リモートに同一ファイルが存在する場合にputをスキップするようになっていて、それが欲しくて作った。ちなみに、--public-domain というオプションを付けると、同一ファイルチェックを公開URL経由で行うようになってAPIアクセスを減らせる。

$ r2sync --public-domain files.example.com ./dir r2://your-bucket/path

ファイルの同一性チェックは、Content-LengthとETagを見ている。S3やR2はETagがコンテンツのMD5ハッシュ値なので、それで同一性チェックをしている。この挙動が未来永劫担保されるかわからないが、単にContent-Lengthだけ見るのも嫌だったし、実際にContent-Lengthだけ主に見ている aws s3 sync がたまにハマるという話も聞くのでそうした。

GitHub Actions

カスタムアクションも公開していて、以下のように使える。oss4.funでも導入した。

- uses: Songmu/r2sync@v0
  with:
    r2_account_id: ${{ secrets.R2_ACCOUNT_ID }}
    r2_access_key_id: ${{ secrets.R2_ACCESS_KEY_ID }}
    r2_secret_access_key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
    src: ./audio
    dest: r2://<your-bucket>/audio
    public_domain: files.example.com

作った動機

ポッドキャストの音声ファイルのアップロードをGitHub Actionsでやっているが、ディレクトリ内の全部の音声ファイルを毎回素朴にputしていたので流石に富豪すぎるので解決しようと考えたのが契機。

案外、既存の良いツールが見つけられなかったのと、aws s3 sync を使っても良かったのだけど、前述のETagの話もあったし、せっかくR2はエグレス料金が無料なのだからファイルの同一性チェックを公開URL経由でおこなうアイデアを盛り込んで作った。

Rust

習作がてらちょっとしたものをRustで作ってみたいと思っていたので、ちょうどよい題材だった。strとStringの使い分けとか、unwrapを使いすぎだったりとかまだまだお作法が分からない部分が多いが、とりあえずcrate公開までいけたのは良かった。Resultとかパターンマッチ含めた言語自体の書き味はかなり良い。

思っていたよりクロスビルド周りが難しくて、各プラットフォームにバイナリを提供するのに手こずった。とりあえず、GitHub Actions上で作るのは一旦断念して手元でバイナリをビルドしてGitHub Releasesにghrでアップロードするという一昔前のスタイルでお茶を濁した。

Rustを書き始めるにあたって「Rustの練習帳」が参考になった。Rustの考え方やコマンドラインツール作成について実践を通して学べる点で有益だった。Goでもこういう本があると良さそう(すでにあるかも)、とか思った。

「趣味でOSSをやっている者だ」というポッドキャストを始めた

最近御存知の通り(?)ポッドキャストづいていて、ポッドキャストについて色々調べてサイト構築ツールなどを作っていたが、ツールを作ったらやはり使いたくなってポッドキャストを始めてみることにした。

以前アナウンスした拙作のポッドキャスト生成OSSのPodbardの実例を示す場にもしたかったので、運営リポジトリも公開している。是非参考にしてみてください。一応、同期しているprivateリポジトリもあって、そのあたりの仕組みは別途解説するかも。

更新頻度はあまり考えてないけど、月に数本、できれば毎週、30分程度のエピソードを出したいと思っている。第2回目までは録り終わっていて今週公開予定。ゲストも次の次まで決まっているので一応しばらく続くと思いたい。

名前の由来

お気付きの通り、ワンパンマンの有名なセリフ「趣味でヒーローをやっている者だ」のオマージュ。力が抜けつつも強そうで良い。

このセリフの英訳が I'm just a guy who’s a hero for fun. らしいので、ポッドキャストの英題も "Just a guy who develops OSS for fun." として、そこからサイトのドメインやハッシュタグを決めた次第。#oss4fun です。よろしく。思いがけず"ossan"に空目した、という意見もあって、なるほど、となった。

ドメインは取るつもり無かったのだけど、空いていたし、短くて気に入ったので取得した。funドメイン、割とお安めで良かった。

購読お待ちしています

少し話してしまったが、ポッドキャストについてはポッドキャスト内で話せればと思うので、ここではこれくらいにしておきます。もう少しノウハウ溜まってきたら、それはそれでエントリを書くかも知れない。

まずは購読してもらえると嬉しいですが、上記のハッシュタグかお便りフォーム(https://oss4.fun/voice)で感想や要望などいただけると更に大変嬉しいです。何も考えてないけど、上記リポジトリのDiscussionsとかに何か投稿してもらえるとだいぶ面白いと思うのでチャレンジャー求む。

おまけ

独自YAMLファイルをJSON SchemaでLSP補完する

Podbardpodbard.yamlに設定を記述するが、これをエディタで補完したりヒントを出せたりするようにした。

yaml-language-serverとJSON Schema

普段vimで開発してて、GitHub ActionsのYAMLを触ってるときなどに、エディタが適切にヒントを出してくれるのを便利に感じつつ「多分LSPがうまいことやってくれてるんだろうな」くらいに考えて深く追いかけていなかった。これは、JSON Schemaで実現されていることを、今回podbard.yamlの仕様をJSON Schemaで記述している過程で発見した。

GitHub ActionsのJSON Schemaは https://json.schemastore.org/github-action.jsonhttps://json.schemastore.org/github-workflow.json で公開されており、YAMLファイルを編集するときに、yaml-language-serverが、それをいい感じに読み込んで支援してくれる。LSPなので当然VSCodeやその他エディタでも利用できる。

JSON Schema Store

このいい感じにスキーマを読み込むための、世界共通のスキーマ類を管理しているのが、JSON Schema Storeというサイト。ここで、https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/api/json/catalog.jsonという大きなJSONファイルが公開されており、どういったファイルがどのスキーマに対応するか、という情報が記述されている。以下はGitHub ActionsのWorkflowの例。

{
  "name": "GitHub Workflow",
  "description": "YAML GitHub Workflow",
  "fileMatch": [
    "**/.github/workflows/*.yml",
    "**/.github/workflows/*.yaml",
    "**/.gitea/workflows/*.yml",
    "**/.gitea/workflows/*.yaml",
    "**/.forgejo/workflows/*.yml",
    "**/.forgejo/workflows/*.yaml"
  ],
  "url": "https://json.schemastore.org/github-workflow.json"
}

yaml-language-serveryaml.schemaStore.enable という設定があり、これがtrueだとこのサイトから自動でスキーマを引っ張ってきてくれる。デフォルトでtrue

独自のJSON Schema

もちろん独自のJSON Schema設定も記述できる。README.mdに詳細に書かれているが、 主に、yaml.schemas 設定と、YAMLファイル内にモードラインコメントを記述する方法がある。モードラインコメントは、プロジェクトのリポジトリ内で、チームメンバーに共通でスキーマを読み込んでもらいたい場合に重宝しそうです。これは至極単純で、YAMLファイル内に以下のようなコメントを記述するだけです。

# yaml-language-server: $schema=<urlToTheSchema>

$schema に指定する文字列は公開URL、絶対・相対パス形式、いずれも可能です。ちなみに、JSON Schemaは本来JSONで記述しますが、YAMLで記述したものを直接読み込ませることも可能でした。

今回のPodbardの場合、以下のコメントを書けばLSPの支援が効くようになった。GitHubのrawコンテンツを指定するだけでお手軽。

# yaml-language-server: $schema=https://raw.githubusercontent.com/Songmu/podbard/main/schema.yaml

JSON Schema Storeへの登録

さて、今回のPodbardの場合、できればモードラインコメントせずとも補完が効くようにしたい。これは https://github.com/SchemaStore/schemastorecatalog.json を編集してpull requestを送れば良いようだ。

しかし、ここにマイナーOSSの設定ファイルのスキーマを登録するのは少し気後れする。そこで、直近のpull requestをいくつか観察してみると、割と個人レベルでのOSSであっても、快く迅速に取り込んでくれている様子だったので、以下のpull requestを送ったら半日程度でシュッとマージしてくれてありがたかった。

https://github.com/SchemaStore/schemastore/pull/4091

{
  "name": "podbard.yaml",
  "description": "Configuration file for Podbard - a podcast site generator",
  "fileMatch": ["podbard.yaml"],
  "url": "https://raw.githubusercontent.com/Songmu/pokkdbard/main/schema.yaml"
}

変更内容は上記で、スキーマのURLにyamlを直接指定する暴挙にでているが、すでに登録されている設定でもYAMLを直接しているものもあったので、いけるかな思って恐る恐るpull requestを出してみたが取り込んでもらえた。まあ、podbard.yamlはYAMLでしか設定ファイルを提供するつもりは無いので、スキーマもYAMLで良いでしょというところ。

vim-lsp-settingsへの対応

これはおまけだが、大変お世話になっているvim-lsp-settingsでは、上記のJSON Schema Storeのcatalog.json をリポジトリ内に同期して保持し、それを読み込むようになっている。yaml-language-serverは前述の通り、実はJSON Schema Storeを直接見に行くようになっていたので対応不要だったが、このスキーマ情報は当然 json-languageserverやその他LSPでも使われるものなので、更新しておく価値はある。ということで以下のpull requestで取り込んでもらった。

https://github.com/mattn/vim-lsp-settings/pull/774

この記事で書いたように、このcatalog.jsonは結構カジュアルに更新されるものなので、自動更新の仕組みが入っても良いとも思ったが、そこまではやらなかった。

まとめ

YAML設定ファイルに対してJSON Schemaを書いておくことで簡単にエディタの支援が効くようになるのは、今更の話かもしれないが、かなりお役立ち情報だった。

また、JSON Schema Storeへの登録は割と気軽にできることが分かったのも収穫だった。当然節度は必要だが、皆さんも機会があれば自作OSS等定義した有用なスキーマを登録してみてはいかがだろうか。

Podbardというポッドキャストサイト構築ツールを作った

https://github.com/Songmu/podbard

結果としてできたものはyattecastHugoの間の子のようなモノになった。音声ファイルとそれに対応するエピソードファイルをfrontmatter付きのMarkdownで記述する。最終的に静的サイトとしてポッドキャストサイトを生成する。

podbard-starterというテンプレートリポジトリがあるので、ここからリポジトリを作ればすぐにポッドキャストサイトを作成できる。このテンプレートはGitHub Pagesにデプロイするモノだが、Cloudflare Pagesにデプロイする、podbard-cloudflare-starterや、それを応用してプライベートプッドキャストを構築する、podbard-private-podcast-starterというのも用意している。

まだ不十分だがドキュメントも以下に用意してある。このドキュメントサイトもPodbardを使ってドッグフーディングしているのがおもしろポイント。ドキュメントをOpenAIのText to speechに読み上げさせたものを音声としている。

https://junkyard.song.mu/podbard/

作った動機

ポッドキャストはブログの拡張技術なので、そもそも作りたいと思っていた。発想はyattecastとほとんど同じだが、社内ポッドキャスト構築にあたって、GitHub Page以外にも簡単にデプロイできることや、音声ファイルをS3やR2などのオブジェクトストレージに分離するアプローチを取りやすくしている。

今だったら、Cloudflare Pagesにコンテンツを配置して、Cloudflare R2に音声ファイルを置くのが個人で始めるにはお手軽のおすすめ構成。

ブログとポッドキャスト

古いWebエンジニアであれば、ポッドキャストがブログの拡張から始まったことは知っている人は多いと思う。ブログの各エントリに音声ファイルを添付する、技術的にはブログのRSSの各itemにenclosure要素というものを追加し、そこに音声ファイルのURLを指定しておくだけでポッドキャストになる。例えばこんな具合。

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
  <channel>
    <title>Podcast Title</title>
    <link>https://example.com</link>
    <language>en-us</language>
    <description>This is a sample podcast description.</description>
    <item>
      <title>Episode 1 Title</title>
      <description>This is a description of episode 1.</description>
      <link>https://example.com/episode1</link>
      <guid>https://example.com/episode1</guid>
      <pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate>
      <enclosure url="https://example.com/episode1.mp3" length="12345678" type="audio/mpeg"/>
    </item>
  </channel>
</rss>

ポッドキャストは、オープンで枯れた規格であり、それは今も変わらない。なので、ブログ同様に専用サービスを利用せずとも、個人でサイト運営を始められる気軽な選択肢があって欲しい。ポッドキャストはそういうIndieな活動でもあるからだ。

専用サービスを使わずともポッドキャストができることはもっと知られて欲しいし、少なくともRSSを配信していないサービスがポッドキャストとか言われてしまうのはちょっと勘弁、という気持ちがある。

そういう思いや、単なる技術的興味、そして必要にかられたのにかこつけて、カッとなって作ったのがこのPodbard。

Podbardの由来など

当初は音声ファイル一覧からRSSを生成するだけのシンプルなものを考えていたが、やっぱり音声のメタ情報を書く場所が必要だと言うことに気づき、それをfrontmatter付きのMarkdownにするのが良かろうという考えに至った。結果としてyattecastと同じ発想に至っていた。

また、私は当ブログのエンジンであるRijiの開発や、はてなブログ管理ツールであるblogsyncのメンテナをしていていたりもするので、「またブログツールを作ってるな…」みたいな気持ちにもなった。

Podbardは、ポッドキャストのPodと吟遊詩人のbardを掛け合わせた言葉。色々候補名は考えたが、類似の名前も使われてなさそうだったんでこれにした。少し中世の雰囲気を出したかったので良かったと思う。今調べていて気づいたけど、GoogleのGeminiの旧称がBardでしたね。

今回名前候補を考える壁打ちだったり、starterのデフォルトCSSものベース作成などでChat GPTを活用した。あと、アイキャッチイラストもImageFX に出力してもらうなど、生成AIを活用した。音声読み上げにText to speechを使った話も冒頭に書いたが、こういう個人サービス開発でAIツールがかなり便利に活用できることを感じた開発だった。

Cloudflare PagesにそれなりにちゃんとBasic認証をかける

前回の、社内プライベートポッドキャスト実現方法で、ポッドキャストサイトを静的配信しつつBasic認証をかけるというアイデアを書いた。しかし、Basic認証などなかなか使わなくなり、ネイティブでサポートしている静的ホスティングサービスも少ない。今回はCloudflare PagesのFunctions機能でリクエストをラップするミドルウェアを書けば実現できることが分かり、その方式を採用することにした。多少実装必要になるのと、認証周りを自前で書くのはあまりやりたくはないが、廉価に比較的省力で実現できるので受け入れる。

ネット上にいくつかサンプルは見つかるが、今回実装するにあたっては以下の点を留意した。

これらを以下のように解決することとした。

ちなみに、定数時間比較については、サボって普通の文字列比較をしていたら、同僚が指摘をしてくれた。持つべきものは優秀な同僚である。やはりこういう処理は自分では書きたくないですね。定数時間比較は、Node.jsにもcrypto.timingSafeEqual が標準搭載なので便利な世の中になった。ところで、文字列の定数時間比較については10年前にも元同僚が、GitHubのWebhookの署名文字列の検証関連で指摘してくれたことも思い出したので昔から周囲に恵まれている。

ということで、作ったものが以下。これをプロジェクトリポジトリにfunctions/_middleware.ts として配置し、Cloudflare Pagesにデプロイすれば、Basic認証をかけてくれる。便利。これは、podbardのプライベートポッドキャストのテンプレートリポジトリでも公開している ものと同じなので、何か問題があれば指摘してもらえると嬉しいです。

// Created by Masayuki Matsuki (Songmu) on 2024-09-11
// Copyright © 2024 Masayuki Matsuki. Licensed under MIT.
//
// ref. https://developers.cloudflare.com/pages/functions/middleware/
// ref. https://developers.cloudflare.com/pages/functions/api-reference/#eventcontext
type EventContext = {
  request: Request;
  next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
  env: {
    ASSETS: object;
    CF_PAGES: string;
    CF_PAGES_BRANCH: string;
    CF_PAGES_COMMIT_SHA: string;
    CF_PAGES_URL: string;
    [key: string]: any;
  };
};

type Middleware = (context: EventContext) => Promise<Response>;

type Middlewares = Middleware | Middleware[];

const errorHandler: Middleware = async ({ next }: EventContext):
  try {
    return await next();
  } catch (err: unknown) {
    console.log(`Error: ${err.message}\n${err.stack}`);
    return new Response("Internal Server Error. Please contact the admin", { status: 500 });
  }
};

const passEnvPrefix = "PASSWORD_";

const basicAuth: Middleware = async ({ request, next, env }: EventContext): Promise<Response> => {
  if (!request.headers.has("Authorization")) {
    return new Response("You need to login.", {
      status: 401,
      headers: {
        "WWW-Authenticate": 'Basic realm="Input username and password"',
      },
    });
  }
  const authorizationHeader = request.headers.get("Authorization");
  if (!authorizationHeader) {
    return new Response("Authorization header is missing.", {
      status: 400,
    });
  }

  const [scheme, encoded] = authorizationHeader.split(" ");
  if (!encoded || scheme !== "Basic") {
    return new Response("Malformed authorization header.", {
      status: 400,
    });
  }

  const buffer = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
  const decoded = new TextDecoder().decode(buffer).normalize();
  const index = decoded.indexOf(":");

  if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
    return new Response("Invalid authorization value.", {
      status: 400,
    });
  }

  const username = decoded.substring(0, index);
  const password = decoded.substring(index + 1);
  if (password === "") {
    return new Response("Password is empty.", {
      status: 401,
    });
  }

  const key = passEnvPrefix + username.toUpperCase();
  const storedPassword = env[key];
  const userExists = typeof storedPassword === "string";
  // Even if the user does not exist, a constant time comparison with a dummy string is performed
  // to prevent timing attacks.
  const matchPassword = await compareStrings(password, userExists ? storedPassword : "dummy");
  if (!userExists || !matchPassword) {
    return new Response("Invalid username or password.", {
      status: 401,
    });
  }

  return await next();
};

async function sha256(str: string): Promise<ArrayBuffer> {
  const encoder = new TextEncoder();
  const data = encoder.encode(str);
  return crypto.subtle.digest("SHA-256", data);
}

async function compareStrings(a: string, b: string): Promise<boolean> {
  const hashA = await sha256(a);
  const hashB = await sha256(b);

  // The buffer lengths must be the same, and they were aligned to the same length by hashing,
  // but we are checking to be sure because timeSafeEqual will throw an exception if the lengths
  // are different.
  // ref. https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/
  if (hashA.byteLength !== hashB.byteLength) {
    return false;
  }
  // ref. https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/
  return crypto.subtle.timingSafeEqual(hashA, hashB);
}

export const onRequest: Middlewares = [errorHandler, basicAuth];

社内プライベートポッドキャスト実現方法

所属している、ヘンリー社には、社内ラジオコンテンツがあり、Notion上に音声ファイルを置く形で実現されている。これを、ポッドキャスト化してポッドキャストクライアントで聞きたいというのが動機。ちゃんとしたオープンな規格としてのポッドキャストにしたい。

もちろん、公開はせずプライベートなものにしたい。ただ、ポッドキャストはオープンコンテンツ前提の規格になっているため完全な実現は難しい。認証のかかっていないRSSフィード及び、そのRSSフィードに埋め込まれたMP3等の音声ファイルにも認証がかかっていないことが前提となるからだ。

やるからには、あまりコストを掛けずに静的配信をベースにしたい。お手軽なプライベートポッドキャストサービスもあまりないようだ。

基本方針

それに対する現実的な妥当解を考え、その実現のために、まずポッドキャストサイトを生成するpodbardというOSSを作った。そして、それを使って、簡単にプライベートポッドキャストを開始できる、podbard-private-podcast-starterというテンプレートリポジトリも公開した。これは、コンテンツをCloudflare Pagesに、音声ファイルをR2にdeployする仕組みになっている。外部サービスを使わず、自前でポッドキャストサイトを生成するものだ。

podbardやこのCloudflareを利用したこのテンプレートに関して別途解説するが、ここでは、実験結果も踏まえ、プライベートポッドキャストの実現方法を解説する。完全にプライベートにするのは難しいが、多重に防御することで実現しようとしている。具体的には以下。ちなみに、上記のテンプレートはこれら対策がすべて盛り込まれている。

これで、よっぽどの機密情報が話されているとかではなければ、実用上は十分プライベートと言えるのではないだろうか。代表的な動画配信サイトのプライベート動画であっても、実は生の動画URLを入手できればダウンロード可能だったりする。ちなみに、GitHubのアップロード画像は、ちゃんと認証がかかるようになりましたね。

Spotifyにもプライベートポッドキャストの機能があるようだが、「プライベートRSSフィード」という言葉があるので、概ね似たような仕組みになっているのではないかと思う。Basic認証は使ってない気はするが。

さて、防御方法についてそれぞれ解説していく。

推測しづらいURLにする

例えば、以下のような具合。

FQDN(ホスト名)は平文DNSから漏れる可能性があるが、パスをランダム文字列にすればhttpsであればテクニカルにはURLが判明する可能性は低い。

Basic認証をかける

冒頭に「認証はかけられない」と書いたが、HTTPに備わっているステートレスな認証方式であるBasic認証はかけられる。これによって、URLが漏洩しても直ちに影響はないし、ユーザー毎に異なるパスワードも設定できるので退職時等の失効も可能。

ちなみに、Basic認証は以下のように認証情報をURLに含められる。この文字列が漏洩してしまうと、クリック一発でコンテンツにアクセスできてしまうので注意。

https://username:[email protected]/{random-path}/feed.xml

この完全なフィードURLをポッドキャストアプリに登録してやれば、ポッドキャストの購読ができる。もしくは、認証情報を含まないURLであっても、認証情報を聞かれるダイアログが開いたのでそれに入力すれば購読できる。iPhoneのポッドキャストアプリや、私が使っているOvercast では、いずれの方法でも購読が確認できた。

クローラーを避ける

検索エンジンのクローラーを避け、余計なインデックス登録をブロックする。robots.txtを配置する。HTMLにはmetaタグを記述すればよいが、HTML以外のコンテンツのインデックス登録を防ぐために、HTTPヘッダも付与できると良い。

robots.txt

User-Agent: *
Disallow: /

参考:

リファラでのURL漏洩を防ぐ

Show Note上のリンクなど、外部サイト遷移時のリファラによるURL漏洩は避けたい。とは言え、現代のブラウザのリファラポリシーは基本的にはstrict-origin-when-cross-originになっており、外部遷移時の漏洩は実はそこまで気にする必要はない。URLのパスは漏洩せず、FQDNが渡るだけである。平文DNSがまだ多い現時点では、FQDNは漏れうるものだという前提に立った方が良い。

とは言え、要らぬ情報が外部に渡ることは避けたいだろうので、その場合はクローラー避け同様に、メタタグ及び可能ならHTTPヘッダにリファラポリシーを設定すると良い。

ポリシーはno-referrer でもよいが、同一オリジン内であれば、リファラがわたっても問題ないし、寧ろ遷移元情報が分かったほうが良いという話もあるので、same-originが良いのではないでしょうか。

参考:

音声ファイルに認証をかけないことについて

そもそも、サイトコンテンツは静的に書き出してBasic認証をかぶせる、音声は何らかのオブジェクトストレージにアップロードする、というのがお手軽なのでそうしたかった。

音声ファイルにもBasic認証をかけ、RSS内の音声ファイルのURLを https://username:[email protected]/{random-path}/foo.mp3 のように、認証情報を含めた記述にするというのも機能するのかも知れないが(未検証)、その場合、RSSフィードをユーザーごとに動的配信したり、全ユーザー分書き出すなどの対応が必要になるので、やらないことにした。

何にせよ、せいぜいBasic認証しかかけられないし、それでも結局、完全なURL文字列が漏れたら、一発でアクセス可能なので、あまりリスク軽減になっていないというのもある。なので、オブジェクトストレージのバケットのルートにrobots.txtを置いておくくらいの対策に留めた。

まとめ

ということで、プライベートポッドキャスト実現方法について書いてみた。ご意見あればフィードバックいただけると嬉しいです。

また、podbard及び、podbard-private-podcast-starterは一応使い始められるようになっている。テンプレートリポジトリについては、そこから新たにリポジトリを作れば、すぐにこのページで解説した方法でのプライベートポッドキャストが実現できるようになっている。

使い方などは追って解説エントリを書きますが、是非試してみてほしいし、わからないことがあれば聞いてくれればできる限り回答したいので、ぜひお試しください。