2024-12-17 01:09
今年もISUCONに参戦した。今年も無事に開催され、素晴らしい出題と運営で良かった。ありがとうございました。
いつものカラアゲネイティブなメンツで出ようと思っていたが、 @toricls さんが出場できなかったため、私が技術顧問を務めているMOSH社CTOの村井さんを無理やり誘ってチームを組んだ。と言うことで、今年のカラアゲネイティブは、 @motemen, @RyosukeIketeru, @songmu の3名。言語はGo。リポジトリはこちら。
https://github.com/motemen/isucon14
序盤の作戦会議
競技開始後、いつものようにmotemenがインフラ周りを設定している間に、村井さんと僕との二人でマニュアルとレギュレーションを読み合わせる。motemenも耳で参加
ここはなんだかんだで1時間くらいかかるが大事な工程
シミュレーター含めて細部まで良くできててビビった
細かくアプリを触らないと分からないボトルネックがあるという運営からのメッセージなのか?とか思ったけど、結果的にはそういうわけではなさそうだった
今回はアプリケーション部分の改善と仕様の調整がスコアに響いてくる点が新機軸で良かった
取り合えずレギュレーション読んで以下の3点が攻略ポイントだとあたりをつける
決済周りの冪等キーヘッダ対応
これは単純にやれば良いだけという話っぽい (実際そうだった)
通知の改善
最終的にマニュアルに書いてある通りSSE化まで辿り着けるのが理想だが、そこに行く前に潰すべきボトルネックが色々ありそう
マッチング改善
最後まで下二つの改善が課題になるので、そういう意味では分かりやすい問題だったと言えるかも
前半戦
まずは肩慣らしに簡単そうな冪等キーヘッダの対応 #10
初期実装のワークアラウンドクソコードに対するFIXMEコメントがリアリティあって良かった
マッチング処理のエンドポイントが定期的に叩かれてアクセスログを汚すのもイヤだったのでgroutine化 #12
マッチングエンドポイントを叩くプロセスは止める
念のため設定ファイルのINTERVALを3600にも設定した
if hostname != "ip-192-168-0-11" {
と言うクソコードによりマッチングロジックが1台でしか動かないようにもした
これらによって後のマッチングロジック改善がやりやすくする狙い
interpolateParams=true #17
goccy/go-json 投入 #18
3台負荷分散 #19
アプリケーションをisu01, isu02で動かしDBをisu03に移動
マッチングロジックはisu01でしか動かないように
しかし今や僕はISUCONでしかNginx.confを触らなくなってしまった
この頃合いに、motemenと村井さんがpt-query-digestを見ながらインデックスの追加だったり、テーブル構造の変更などをやってくれていた。
だいたい、前段のやるべきことは終え、後半戦は想定通り本命のマッチングと通知の改善かな、と言うところに至った。ただ、この時点で15時近くなっていた。てこずって時間をかけすぎたのでこのあたりもう少しスムーズにやりたかった。何はともあれ、通知周りの改善は他の二人に任せ、僕はマッチングの改善に取り組むことにした。
後半戦
当初のマッチングロジックがあまりにもひどかったので修正
とりあえず、chairs.is_occupied
というカラムを追加し、配車中かどうか判別できるように
しかしこのステータス更新で苦しんだ
マッチング時に is_occupied = true
にするのは簡単だったが、解除するタイミングがよく分からずてこずった
ライド完了時にフラグを戻してしまうと、椅子が完了通知を受けとる前にマッチングが始まってしまいエラーになる
完了通知のところでフラグを戻そうとすると、マッチング待ちが長くなりすぎてエラーになる
ここは根本究明できず、ライド完了時に、非同期で200msec遅らせてgoroutine内でフラグを戻すという酷いコードを書いて通した
これで少しスコアは上がった
ここで時間切れ
このロジックでは単に空いてる椅子が適当に選ばれるだけになっていて元のロジックと大差ない
ちゃんと近傍を割り当てる最適化に取り組みたかったのだが…
と言うことで、終了間際に回したベンチが通って、14,987点でフィニッシュ。順位は多分103位。
反省
良問で運営やポータルも快適で、コードも色々書けたので楽しかった
誘った村井さんも楽しんでくれたようで良かった
Goに不慣れな村井さんをもう少しフォローできれば良かった
motemenが苦労しているときにも、ペアプロするなりすればもう少し早くバグに気付けたかも
このあたりはmotemenがインフラ構築担当であることもあり、アプリの理解度が少し落ちると言う点も影響があったかも知れない
インフラ構築担当がコードも書く体制なら、インフラ構築後にマニュアル熟読担当が構築担当に問題概要をレクチャーする時間があっても良いのかも
何なら来年はインフラ構築担当を切り替えても良いのではないか
カラアゲネイティブチームは、結成初年度は僕がインフラ構築担当だったが翌年からずっとmotemenが担当しているし役割が固定化している
今年の構築はめっちゃ早くてトラブルもなくて熟練を感じた
いつものメンバーのスキルセットは近いので、それぞれの役割分担が固定化している今、それぞれがやっていることを棚卸しして他の人ができるようにする価値はありそう
マニュアル読みに時間はかけるのは必要だけど、その後の初速をもっと速くしたい
ISUCON用ツールを自作するのとかもやっても良いかもと思い始めた
これまで、僕が結成時に作ったものの注ぎ足し、注ぎ足しされた秘伝のMakefileを一枚持ち込むのみで戦ってきたが限界がある
アイデアとしてはスコアの記録とか、pt-query-digestやperformance schema, alp辺りの集計をいい感じにしてくれる君とか
一緒に組んでて、僕よりmotemenの方がISUCONに勝ちたい気持ちが強い感じがするので、来年はもう少しそこに協力したいかな
何にせよ来年も開催されることを期待しています
2024-12-16 00:25
この記事は 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アドベントカレンダーがアドベント期間に開催されているように。
ただ、アドベント期間を外れているのにアドベントカレンダーを名乗るのは良くないと私は考えています。それは元の文化への理解に欠ける行為に感じるからです。
そういった一部の逸脱が行きすぎた結果、それが文化の盗用だという妥当な批判をされ「アドベントカレンダーという名前は適切じゃないからみんな使うのを止めよう」となってしまうかも知れません。それは、この文化が好きな私としては悲しいですし、そうなって欲しくありません。
この技術系アドベントカレンダー文化を長く楽しく続けるためにも、歴史や元のコンセプトの理解が大切だと思い、このエントリーをしたためた次第です。
2024-11-25 01:50
最近、Webエンジニア界隈で、共通項を感じる印象的な出来事があった。具体的には以下の2件。
ゆーすけべーがHonoを作ったこと
おぎじゅんさんが職業プログラマーに戻ってきた(きていた)こと
共通項はそれぞれ長めのブランクがありながら、ソフトウェアエンジニアリングの世界に戻ってきて一線級以上の活躍をしているということだ。二人とも僕と同世代かそれ以上の年齢でもある。これは勇気と希望をもらえることだ。
もちろん彼らの能力の高さゆえに第一線に戻ってこられたのかもしれない。ただ、どちらにせよ、別のことに興味があれば、職業エンジニアを離れて、フォーカスする期間があっても良いと言うことだ。能力不足ならなおさら中途半端になるよりフォーカスしたほうが良いとも言える。
それに多分戻ってこられる。ゆーすけべーの様に世界的エンジニアになるのは難しいにせよ、別に満足に働けるくらいには戻せるのではなかろうか。
技術は日進月歩で、キャッチアップを怠ると途端に置いていかれる不安があるかも知れない。でもそこにしんどさを感じ始めているのなら無理しないほうが良い。それに、そんな厳しい業界だったら新しい人が誰も参入できなくなって、消え去ってしまう。実際には優秀な若者が新たにどんどん業界に入ってきてくれている。
もちろん、若い人の方が我々よりも優秀であるという事実はあるが、経験や結晶性知能で勝っている部分もある。AIなどのテクノロジーに補助してもらえる部分も増えている。眼鏡がそうであるように。何より、いくらAIが発展しようと、ソフトウェアエンジニアは足りない状況が続くので、少ないパイを競って蹴落としあう必要はなく、寧ろ皆で高めあっていく必要がある。
人事になりました
私事ですが、10月から人事に異動しました。正式には「株式会社ヘンリー 経営管理部門 人事本部 VP of Engineering」というタイトルです。
私はなんだかんだでこれまで兼務ベースで約10年エンジニア採用やエンジニアリング組織開発にも携わっていて、知見やノウハウもあるのでフォーカスする期間があっても良かろうというところ。
今後はエンジニアとしての発信だけではなく、採用や組織的な発信も増やしていければ良いかと思っている。エンジニアとしての経験を活かし、人事関係のノウハウを抽象化してパブリックに公開するというのをもっとやりたい。
なので、今はプロダクションコードを書いていないが、またいずれコードを書く仕事に戻る気持ちは全然ある。今、どういう役割の帽子をかぶって、どこにフォーカスするかが大事。必要に応じて柔軟に帽子をかぶり変えて行きたい。
冒頭の話もあって、いつだって戻ってこられる安心感が強まったから別のところにフォーカスしようと思えたのもある。元々、プレイングマネージャー否定派ではあった。プレイングマネージャーやってた時期も長いけど。
家族や子育てにフォーカスしたって良い(当たり前)
別に子育てに限った話ではないが、結婚や子育てを機に、以前ほど趣味のエンジニアリングに時間や情熱を割けなくなっていることを不安に感じている人が結構見られる。私も感じることはある。
でも別に働くことや趣味のエンジニアリングを緩めて、家族や子育てに集中する時期があっても良いと思う。それは間違いなく人生を豊かにする。私自身も最近それを強く感じるようになった。自己正当化バイアスかもしれないが、そんなバイアスなら歓迎である。
これも当たり前だけど、家族や子育てに比重を置くのもあくまで個々人の選択であって、別のところに目を向けてみたって良い。
怖いのは情熱が枯渇すること
結局怖いのは情熱が枯渇すること。例えばエンジニアリングへの情熱が以前より失われているとかそういったこと。枯渇させないためにも、休んだり、気分転換で別のことをやったり、別のことにフォーカスしてみたりすることが大事。
何かにフォーカスすることで情熱が生まれることもある。そして何かにフォーカスしないとなかなか情熱は生まれづらい。その結果として、エンジニアリングへの情熱が失われたとしても、他のことに情熱を持てればそれでも良いとも言える。中途半端に色々なことをこなすことに終始して、消耗してしまうのが巧くない。
自分の人生のリソースには限りがあり、やりたいことを全部やれないもどかしさを抱えながら生きる人も多い。私もそう。寧ろそういう人が幸せと言える。そういう中でフォーカスポイントを変化させながら情熱を持ち続けることが幸せに生きるコツなのだと思う。選択と集中である。
「選択と集中」における選択の重要性
「選択と集中」はよく言われる言葉で、集中・フォーカスすることの大事さはよく説かれるが、それと同じように選択できることも大事である。
歴史上の選択と集中の失敗例として、発展途上国が一部の一次産品に生産を集中した結果、余計貧困が進んだ、と言う話がある。いわゆるモノカルチャー経済である。選択し直せる選択肢を失って袋小路に入り込んでしまったという点が示唆的である。
人生でも事業でも、その時その時のフォーカスポイントを定めることはとても重要だが、それと同時に定期的に選択し直せるように選択肢を確保しておくことも非常に重要なのだ。
ソフトウェアエンジニアは人生において有力で魅力的な選択肢だと思う。私自身もそういう選択肢を常に持っていることは幸せだし、人生を充実させてくれるものだと確信している。万人に向いてるかどうかは分かりませんが、オススメです。
2024-11-03 22:24
当ブログの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のように、普段は塩漬けにしておいて、数年に一回くらいお手入れをするくらいで使い続けられる、そんなソフトウェアもあって良いと思っている。
2024-10-15 01:37
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でもこういう本があると良さそう(すでにあるかも)、とか思った。
オライリー・ジャパン
発売日 : 2024-01-18
2024-10-02 21:06
最近御存知の通り(?)ポッドキャストづいていて、ポッドキャストについて色々調べてサイト構築ツールなどを作っていたが、ツールを作ったらやはり使いたくなってポッドキャストを始めてみることにした。
以前アナウンスした拙作のポッドキャスト生成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 とかに何か投稿してもらえるとだいぶ面白いと思うのでチャレンジャー求む。
おまけ
2024-09-23 17:00
Podbard はpodbard.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.json やhttps://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-server
に yaml.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/schemastore の catalog.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等定義した有用なスキーマを登録してみてはいかがだろうか。
2024-09-17 00:23
https://github.com/Songmu/podbard
結果としてできたものはyattecast とHugo の間の子のようなモノになった。音声ファイルとそれに対応するエピソードファイルを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ツールがかなり便利に活用できることを感じた開発だった。
2024-09-14 03:29
前回の、社内プライベートポッドキャスト実現方法 で、ポッドキャストサイトを静的配信しつつBasic認証をかけるというアイデアを書いた。しかし、Basic認証などなかなか使わなくなり、ネイティブでサポートしている静的ホスティングサービスも少ない。今回はCloudflare PagesのFunctions機能 でリクエストをラップするミドルウェアを書けば実現できることが分かり、その方式を採用することにした。多少実装必要になるのと、認証周りを自前で書くのはあまりやりたくはないが、廉価に比較的省力で実現できるので受け入れる。
ネット上にいくつかサンプルは見つかるが、今回実装するにあたっては以下の点を留意した。
コード内に認証情報を載せない
複数ユーザーのIDとパスワードを管理できるようにする
パスワードは定数時間比較してタイミング攻撃を防ぐ
これらを以下のように解決することとした。
認証情報は環境変数に秘匿化された値として保存する
PASSWORD_
プレフィクスがついた環境変数を認証情報とし、そのサフィックスをユーザー名とする
例: PASSWORD_FOO
という環境変数が fooユーザーのパスワードとなる
定数時間比較はCloudflareが用意してくれている(!) timingSafeEqual
が使えて便利
ちなみに、定数時間比較については、サボって普通の文字列比較をしていたら、同僚が指摘をしてくれた。持つべきものは優秀な同僚である。やはりこういう処理は自分では書きたくないですね。定数時間比較は、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];
2024-09-12 13:47
所属している、ヘンリー 社には、社内ラジオコンテンツがあり、Notion上に音声ファイルを置く形で実現されている。これを、ポッドキャスト化してポッドキャストクライアントで聞きたいというのが動機。ちゃんとしたオープンな規格としてのポッドキャストにしたい。
もちろん、公開はせずプライベートなものにしたい。ただ、ポッドキャストはオープンコンテンツ前提の規格になっているため完全な実現は難しい。認証のかかっていないRSSフィード及び、そのRSSフィードに埋め込まれたMP3等の音声ファイルにも認証がかかっていないことが前提となるからだ。
やるからには、あまりコストを掛けずに静的配信をベースにしたい。お手軽なプライベートポッドキャストサービスもあまりないようだ。
基本方針
それに対する現実的な妥当解を考え、その実現のために、まずポッドキャストサイトを生成するpodbard というOSSを作った。そして、それを使って、簡単にプライベートポッドキャストを開始できる、podbard-private-podcast-starter というテンプレートリポジトリも公開した。これは、コンテンツをCloudflare Pages に、音声ファイルをR2 にdeployする仕組みになっている。外部サービスを使わず、自前でポッドキャストサイトを生成するものだ。
podbardやこのCloudflareを利用したこのテンプレートに関して別途解説するが、ここでは、実験結果も踏まえ、プライベートポッドキャストの実現方法を解説する。完全にプライベートにするのは難しいが、多重に防御することで実現しようとしている。具体的には以下。ちなみに、上記のテンプレートはこれら対策がすべて盛り込まれている。
サイトコンテンツ (ポッドキャストサイト及びRSSフィード)
推測しづらいURLにする
Basic認証をかける
robots.txtやHTTPヘッダ、メタタグで不要なクローリングやインデックス登録を防ぐ
HTTPヘッダやメタタグでリファラを送らないように
音声ファイル
推測しづらいURLにする
robots.txtは置く
認証はかけずURL直打ちでアクセスできてしまうことは許容する
これで、よっぽどの機密情報が話されているとかではなければ、実用上は十分プライベートと言えるのではないだろうか。代表的な動画配信サイトのプライベート動画であっても、実は生の動画URLを入手できればダウンロード可能だったりする。ちなみに、GitHubのアップロード画像は、ちゃんと認証がかかるようになりました ね。
Spotifyにもプライベートポッドキャストの機能がある ようだが、「プライベートRSSフィード」という言葉があるので、概ね似たような仕組みになっているのではないかと思う。Basic認証は使ってない気はするが。
さて、防御方法についてそれぞれ解説していく。
推測しづらいURLにする
例えば、以下のような具合。
サイトURL: https://mypodcast.example.com/{random-path}/
RSSフィード: https://mypodcast.example.com/{random-path}/feed.xml
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: /
メタタグ: <meta name="robots" content="noindex, nofollow" />
HTTPヘッダ: X-Robots-Tag: noindex, nofollow
参考:
リファラでのURL漏洩を防ぐ
Show Note上のリンクなど、外部サイト遷移時のリファラによるURL漏洩は避けたい。とは言え、現代のブラウザのリファラポリシーは基本的にはstrict-origin-when-cross-origin
になっており、外部遷移時の漏洩は実はそこまで気にする必要はない。URLのパスは漏洩せず、FQDNが渡るだけである。平文DNSがまだ多い現時点では、FQDNは漏れうるものだという前提に立った方が良い。
とは言え、要らぬ情報が外部に渡ることは避けたいだろうので、その場合はクローラー避け同様に、メタタグ及び可能ならHTTPヘッダにリファラポリシーを設定すると良い。
メタタグ: <meta name="referrer" content="same-origin">
HTTPヘッダ: Referrer-Policy: same-origin
ポリシーは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 は一応使い始められるようになっている。テンプレートリポジトリについては、そこから新たにリポジトリを作れば、すぐにこのページで解説した方法でのプライベートポッドキャストが実現できるようになっている。
使い方などは追って解説エントリを書きますが、是非試してみてほしいし、わからないことがあれば聞いてくれればできる限り回答したいので、ぜひお試しください。