[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-rIhGFBCz3nkO":3,"$f13qsdxcw2gxp8":357,"adjacent-rIhGFBCz3nkO":362},{"id":4,"title":5,"content":6,"summary":338,"charCount":339,"eyecatch":13,"tags":340,"is_no_index":353,"publishedAt":354,"createdAt":355,"updatedAt":356},"rIhGFBCz3nkO","microCMSやめた",{"type":7,"content":8},"doc",[9,18,23,27,31,35,39,43,47,52,56,60,64,68,72,76,81,85,100,116,120,124,128,132,148,152,157,161,174,183,187,196,203,207,212,216,220,224,251,255,260,264,276,280,284,291,298,302,306,311,315,326,330,334],{"type":10,"attrs":11,"content":14},"heading",{"level":12,"id":13},1,null,[15],{"type":16,"text":17},"text","はじめに",{"type":19,"content":20},"paragraph",[21],{"type":16,"text":22},"どうも、私です。",{"type":19,"content":24},[25],{"type":16,"text":26},"このサイトを作り始めてから、ずっとmicroCMSを利用してきましたが、とうとうやめることにしました。",{"type":19,"content":28},[29],{"type":16,"text":30},"お仕事の方でも、私が技術選定に関わっているプロダクトでは利用しているくらいにはmicroCMSのことが好きですし、このサイトでも長らくお世話になっていたので、それなりに愛着があります。",{"type":19,"content":32},[33],{"type":16,"text":34},"APIが使いやすいですし、ドキュメントもそれなりに充実していて、Headless CMSを選ぶ機会があれば今でも普通に候補に入れるかと思います。ですので、何か致命的な不満があってmicroCMSを嫌いになったみたいな話ではありません。",{"type":19,"content":36},[37],{"type":16,"text":38},"単純に、このサイトでのユースケースと少しずつ合わなくなってしまいました。",{"type":19,"content":40},[41],{"type":16,"text":42},"大きな理由から順に、配信トラヒック上限, リッチテキストフィールドが返すHTMLの扱い, モバイルデバイスからのエディタ操作性です。",{"type":19,"content":44},[45],{"type":16,"text":46},"そんなこんなで、最終的にはCMSごと自分で作ることにしました。",{"type":10,"attrs":48,"content":49},{"level":12,"id":13},[50],{"type":16,"text":51},"Hobbyプランの20Gi\u002Fmoが厳しい",{"type":19,"content":53},[54],{"type":16,"text":55},"今回microCMSをやめる決断に至った一番の理由がこれでした。",{"type":19,"content":57},[58],{"type":16,"text":59},"microCMSのHobbyプランでは、月あたり20Giまでのデータを配信できます。このサイトを作り始めた当初ではあまり気にしていませんでしたが、有難いことに以前よりもアクセス数が増えてしまい、月によっては上限を超過してしまいそうになることがちまちまありました。",{"type":19,"content":61},[62],{"type":16,"text":63},"それなら上位プランに乗り換えようかという話になるのですが、Hobbyの次にあたるTeamプランは月額4,900円します。",{"type":19,"content":65},[66],{"type":16,"text":67},"仕事で利用するサービスであれば、CMSを自前で運用保守するコストや管理監査機能周りを考えると寧ろ安いと思えるのですが、如何せん個人の雑記帳なので躊躇してしまいます。年額6万円弱ですからね。",{"type":19,"content":69},[70],{"type":16,"text":71},"勿論、microCMSでしか代替できない何かがあって、このサイトにとってならなくてはならないコアな存在になっていたのであれば、そのまま課金していたのかもしれません。が、ちょうど実装面でもmicroCMSに合わせ続けることに若干のﾂﾗﾐを感じることがありました。",{"type":19,"content":73},[74],{"type":16,"text":75},"であれば、月額を払っていまの構成を維持し続けるよりも、この機会にコンテンツ管理そのものを作り直したいなとなり今に至ります。",{"type":10,"attrs":77,"content":78},{"level":12,"id":13},[79],{"type":16,"text":80},"HTMLをどうにか加工したかった",{"type":19,"content":82},[83],{"type":16,"text":84},"microCMSではAPIに型のような概念があり、事前に定義した種別によって入力できる内容が異なります。このサイトでは、本文にリッチテキストフィールドを利用していました。",{"type":19,"content":86},[87,89,94,96,98],{"type":16,"text":88},"このリッチテキストフィールドはAPIの返り値としてセマンティックなHTMLを返してきます。最小限の表示を行うのであれば、そのHTMLを",{"type":16,"marks":90,"text":93},[91],{"type":92},"code","v-html",{"type":16,"text":95},"で横流しして、いい感じのCSSを当ててあげるとそれっぽいものができます。",{"type":97},"hardBreak",{"type":16,"text":99},"しかし、長いこと触っていると色々なことがやりたくなってきます。",{"type":19,"content":101},[102,104,108,110,111,113,114],{"type":16,"text":103},"このサイトで言うと、",{"type":16,"marks":105,"text":107},[106],{"type":92},"\u003Ca href=\"https:\u002F\u002Fexample.com\">https:\u002F\u002Fexample.com\u003C\u002Fa>",{"type":16,"text":109},"のようなテキストと遷移先が一致するリンクに対して、そのまま平文を表示せず、リッチなリンクカードに差し替える処理があります。",{"type":97},{"type":16,"text":112},"コードブロックにはsyntax highlightがほしいし、言語名やファイルパスによって表示を出し分けたかったりもします。",{"type":97},{"type":16,"text":115},"GItHub, YouTube. Instagram, Twitter, MisskeyあたりのURLであれば、それぞれ専用の埋め込みが表示された方が嬉しいですし、Mermaidが書かれていればダイアグラムとして描画したいものです。",{"type":19,"content":117},[118],{"type":16,"text":119},"他にも色々仕込んでいくうちに、気付けば結構な量になってしまいました。",{"type":19,"content":121},[122],{"type":16,"text":123},"これまでは、APIから取得されたHTMLをCheerioに食わせてDOMを参照し、条件に合う要素を置き換えたり属性を追加したりで凌いでいました。できるにはできるんです。中身はHTMLなので。",{"type":19,"content":125},[126],{"type":16,"text":127},"ただ、記事内で使いたい表現を増やす度に、「microCMSがこういうHTMLを返してくるだろうから、このDOM構造を副作用なく検出して、特定のコンポーネントと置き換えて使おう…」みたいな処理が無限に増えていきます。",{"type":19,"content":129},[130],{"type":16,"text":131},"これはもうHTMLそのものを表示しているというよりかは、HTMLを中間言語として受け取り記事構造を復元しているように思えてきました。",{"type":19,"content":133},[134,136,140,142,146],{"type":16,"text":135},"先程例に挙げたコードブロックであれば、HTMLの",{"type":16,"marks":137,"text":139},[138],{"type":92},"pre > code",{"type":16,"text":141},"を見つけて置き換えるよりも、最初から",{"type":16,"marks":143,"text":145},[144],{"type":92},"codeBlock",{"type":16,"text":147},"というノードが状態として存在していた方が扱いやすいですよね。リンクカードもそうですし、画像も、今後増やすであろう何かしらの変な独自表現もすべてそうです。",{"type":19,"content":149},[150],{"type":16,"text":151},"microCMSが返してくるHTMLをどうにか加工し続けるより、自分で記事の構造を持って、自分でレンダリングした方が素直なんじゃないの、と。",{"type":10,"attrs":153,"content":154},{"level":12,"id":13},[155],{"type":16,"text":156},"スマホから改行をしたい",{"type":19,"content":158},[159],{"type":16,"text":160},"もうひとつ。",{"type":19,"content":162},[163,165,166,168,172],{"type":16,"text":164},"私はベッドからスマホで文章を書くことが多いです。最初から最後までスマホだけで書くこともありますし、あとからPCで書き直したりすることもあります。",{"type":97},{"type":16,"text":167},"で、microCMSのリッチエディタでは、スマホから",{"type":16,"marks":169,"text":171},[170],{"type":92},"Shift + Enter",{"type":16,"text":173},"と等価の操作ができません。",{"type":19,"content":175},[176,178,181],{"type":16,"text":177},"どういう事かと言うと、段落を変えずに改行したい場合(CR\u002FLFではなくLFを単体で入力したい場合)、",{"type":16,"marks":179,"text":171},[180],{"type":92},{"type":16,"text":182},"を入力する必要があります。確かWordなんかもこういう操作ですよね。",{"type":19,"content":184},[185],{"type":16,"text":186},"PCから使う分にはShiftを押しながらEnterを押せばいいので何も困りません。問題はスマホです。",{"type":19,"content":188},[189,191,194],{"type":16,"text":190},"当然ながらスマホのソフトウェアキーボードには",{"type":16,"marks":192,"text":171},[193],{"type":92},{"type":16,"text":195},"等価の入力キーはなく、同じ感覚で改行を入れるのが困難でした。加えて、このサイトでは、羅列されたパラグラフの可読性を高める目的で、記事レンダラのコンポーネント内子要素に対し、余白を意図的に持たせるスタイルを適用していたので困りものです。",{"type":19,"content":197},[198,200,201],{"type":16,"text":199},"これを読まれている方も日本語を母語としている方が多いでしょうし、おそらく肌感として理解していただけると思うのですが、現代的な日本語の文章表現として、句点と読点の間くらいの区切りとして改行がかなり用いられているように感じます。",{"type":97},{"type":16,"text":202},"浅学さを晒すようで大変恥ずかしいのですが、これが何かしらの正式な文章技法として定義されているのか、単にそのような表現が自然に広まってしまったのかは知りません。知りませんが、とにかくよく目にしますし、私自身もよく使います。",{"type":19,"content":204},[205],{"type":16,"text":206},"そんなこんなで、よく使う表現がよく使うデバイスから入力しにくい状態でした。",{"type":10,"attrs":208,"content":209},{"level":12,"id":13},[210],{"type":16,"text":211},"そうだ、CMSをつくろう",{"type":19,"content":213},[214],{"type":16,"text":215},"ということで、CMSを作ることにしました。楽しそうですし。",{"type":19,"content":217},[218],{"type":16,"text":219},"汎用的なものを作るつもりは最初からなく、このサイトでだけ使える最小限のものを実装しています。そもそも私一人しか使わないものにコラボレーション機能だとか監査機能だとかは不要ですしね。",{"type":221,"attrs":222},"linkCard",{"url":223},"https:\u002F\u002Fgithub.com\u002Fchan-mai\u002Fmq1-web\u002Fcommit\u002F7f7808a57db62445f5bfd47f36db706b84e1c188",{"type":19,"content":225},[226,228,229,231,234,236,239,241,244,246,249],{"type":16,"text":227},"記事本文についてはTiptapのJSONをそのまま構造化されたドキュメントとして持つようにしました。",{"type":97},{"type":16,"text":230},"文章は",{"type":16,"marks":232,"text":19},[233],{"type":92},{"type":16,"text":235},"、見出しは",{"type":16,"marks":237,"text":10},[238],{"type":92},{"type":16,"text":240},"、コードブロックは",{"type":16,"marks":242,"text":145},[243],{"type":92},{"type":16,"text":245},"、リンクカードなら",{"type":16,"marks":247,"text":221},[248],{"type":92},{"type":16,"text":250},"、と最初から何者なのか分かった状態で保存しておくことで、HTMLをゴネゴネ処理する必要がなくなり非常に楽になりました。当然と言えば当然なのですが。",{"type":19,"content":252},[253],{"type":16,"text":254},"記事やタグなんかのメタデータはD1に、メディア系はR2に置くようにしています。元々このサイトではいいね機能周りでD1を使っていましたし、Workers上で動いているので自然なのかなと。",{"type":10,"attrs":256,"content":257},{"level":12,"id":13},[258],{"type":16,"text":259},"エディタの話",{"type":19,"content":261},[262],{"type":16,"text":263},"ちょっと本筋とは離れるのですが、エディタを作るにあたって、一つ明確に理想としていたものがあります。",{"type":19,"content":265},[266,274],{"type":16,"marks":267,"text":273},[268],{"type":269,"attrs":270},"link",{"href":271,"target":272,"rel":13,"class":13,"title":13},"https:\u002F\u002Fsizu.me\u002F","_blank","しずかなインターネット",{"type":16,"text":275},"です。",{"type":19,"content":277},[278],{"type":16,"text":279},"以前利用していたのですが、あのサービスのエディタの書き心地がすごく好きでした。余計なものがほとんど視界に入らず、とりあえずタイトルを書いて文章を書き始められ、文章を書くことそのものがUIの中心にある感じがすごく好きです。よくあるCMSの管理画面でコンテンツを編集している体験とはまた違ったものがあります。",{"type":19,"content":281},[282],{"type":16,"text":283},"ああいうのがいいなと。",{"type":19,"content":285},[286,288,289],{"type":16,"text":287},"しずかなインターネットはReactを基盤にしていて、こちらはVue\u002FNuxtです。使っているライブラリまで含めて完全に同じものを作るとなるとまぁまぁ大変ですし、別にコピーを作りたいわけでもないので、その辺りは普通に割り切っています。",{"type":97},{"type":16,"text":290},"ただ、考えとして文章を書くことを邪魔しないという部分はかなり参考にしています。",{"type":292,"attrs":293},"image",{"src":294,"alt":13,"caption":295,"width":296,"height":297},"\u002Fimages\u002Fr2\u002FHVt3_R3fiKxhIM7x.png","記事の管理画面のスクリーンショット",1920,1080,{"type":292,"attrs":299},{"src":300,"alt":13,"caption":301,"width":296,"height":297},"\u002Fimages\u002Fr2\u002Feq4mue3h8X_-DYl1.png","記事の編集画面のスクリーンショット",{"type":19,"content":303},[304],{"type":16,"text":305},"結局のところ、私しか使わないので私が満足できればそれでいいのかなと。",{"type":10,"attrs":307,"content":308},{"level":12,"id":13},[309],{"type":16,"text":310},"さいごに",{"type":19,"content":312},[313],{"type":16,"text":314},"そんなこんなで、長いこと使ってきたmicroCMSへの依存を完全にやめてしまいました。",{"type":19,"content":316},[317,319,324],{"type":16,"text":318},"実際、作ってみると「この操作ちょっと嫌だな」と思ったらその場で直せますし、「こういう表現が欲しい」と思えば",{"type":16,"marks":320,"text":323},[321],{"type":322},"dpgk","こんな感じに",{"type":16,"text":325},"好きなノードを生やせます。",{"type":19,"content":327},[328],{"type":16,"text":329},"自分のブログのためだけにCMSを作るなんて、一般論で考えればどう考えても割に合わないと思います。でも、個人サイトなんてそれくらいでいいんじゃないでしょうか。",{"type":19,"content":331},[332],{"type":16,"text":333},"以前にも似たようなことを書いた気がしますが、自分のサイトなので。",{"type":19,"content":335},[336],{"type":16,"text":337},"長いことお世話になりました、microCMS。","どうも、私です。このサイトを作り始めてから、ずっとmicroCMSを利用してきましたが、とうとうやめることにしました。お仕事の方でも、私が技術選定に関わっているプロダクトでは利用しているくらいにはmicroCMSのことが好きですし、このサイトでも長らくお世話になっていたので、それなりに愛着があります",3762,[341,345,349],{"id":342,"name":343,"slug":344},"4oy2jc8mdg","日記","diary",{"id":346,"name":347,"slug":348},"8q8qyy8sz3","プログラミング","programming",{"id":350,"name":351,"slug":352},"qfey3yw0z1","テクノロジー","technology",false,"2026-08-23T16:09:35.280Z","2026-08-23T11:18:22.125Z","2026-08-23T16:11:23.877Z",{"url":223,"domain":358,"title":359,"description":360,"type":361},"github.com","リンク","このサイトをチェック","GENERAL",{"older":363,"newer":868},{"id":364,"title":365,"content":366,"summary":860,"charCount":861,"eyecatch":13,"tags":862,"is_no_index":353,"publishedAt":865,"createdAt":866,"updatedAt":867},"c_p__7mzq","Cloudflareスタックで非同期ジョブをいい感じに管理したい",{"type":7,"content":367},[368,373,376,383,398,400,403,407,411,415,421,432,445,449,453,457,461,467,471,478,482,488,492,498,502,506,519,523,529,533,537,541,547,551,556,560,565,569,591,595,601,605,610,614,619,629,634,638,640,644,650,654,658,662,667,689,693,697,701,707,711,715,719,723,727,733,737,741,745,749,755,759,763,767,771,775,779,785,789,799,805,812,816,820,824,830,834,838,842,846,850,854,856],{"type":10,"attrs":369,"content":371},{"level":12,"id":370},"h8d027c8ed3",[372],{"type":16,"text":17},{"type":19,"content":374},[375],{"type":16,"text":22},{"type":19,"content":377},[378,380,381],{"type":16,"text":379},"ここ数年で、Cloudflare Workersを取り巻く状況はかなり変わっているようで、Workersそのものだけでなく、QueuesやDurable Objects、D1、Analytics Engine等、Cloudflareスタックの中で使えるサービスが結構増えました。加えて、比較的安価に使えることもあり、Workersを本格的なバックエンドとして採用するケースも(私の周りでは)かなり増えたように思います。",{"type":97},{"type":16,"text":382},"利用者が増えれば当然知見も増えるもので、Durable Objectsをどう使うと嬉しいのか、D1に何を任せるべきなのか、Containersで何が解決可能なのか…といった話を見かけることが増えました。",{"type":19,"content":384},[385,387,388,390,397],{"type":16,"text":386},"そんな中で、Workersスイートで構築されたアプリケーションを運用していると、バックグラウンドジョブ周りでいくつか困ることが出てきました。",{"type":97},{"type":16,"text":389},"そのあたりをどうにかしようとして作り始めたのが、今回紹介する",{"type":16,"marks":391,"text":396},[392],{"type":269,"attrs":393},{"href":394,"target":272,"rel":395,"class":13,"title":13},"https:\u002F\u002Fgithub.com\u002Fchan-mai\u002FTsumugi","noopener noreferrer","Tsumugi",{"type":16,"text":275},{"type":221,"attrs":399},{"url":394},{"type":221,"attrs":401},{"url":402},"https:\u002F\u002Ftsumugi.mq1.dev",{"type":19,"content":404},[405],{"type":16,"text":406},"Cloudflare Workers \u002F Durable Objects \u002F Queues \u002F D1を中心に、バックグラウンドジョブの投入、遅延実行、再試行、優先度、同時実行数制御、定期実行、ジョブ同士の依存関係などをまとめてブラックボックスとしていい感じに扱うためのジョブ管理システムです。最初から汎用的なジョブシステムを作ろうと思って始めたわけではないのですが、気がついたらそこそこ大きくなっていました。",{"type":19,"content":408},[409],{"type":16,"text":410},"そしてせっかく公開したのに思っていた以上に反応をもらえず、たいへん寂しいのでこの記事を書いています。",{"type":19,"content":412},[413],{"type":16,"text":414},"これは宣伝です。笑ってください。",{"type":10,"attrs":416,"content":418},{"level":12,"id":417},"hd92040d0df",[419],{"type":16,"text":420},"きっかけ",{"type":19,"content":422},[423,425,431],{"type":16,"text":424},"Tsumugiを作る直接のきっかけは、以前作ったMisskey向け匿名質問箱サービスの",{"type":16,"marks":426,"text":430},[427],{"type":269,"attrs":428},{"href":429,"target":272,"rel":395,"class":13,"title":13},"https:\u002F\u002Fmewk.app\u002F","Mewk",{"type":16,"text":275},{"type":19,"content":433},[434,436,443],{"type":16,"text":435},"Mewkでは、バックグラウンドジョブの管理に",{"type":16,"marks":437,"text":442},[438],{"type":269,"attrs":439},{"href":440,"target":272,"rel":441,"class":13,"title":13},"https:\u002F\u002Fkiribi.pages.dev\u002F","noopener noreferrer nofollow","Kiribi",{"type":16,"text":444},"というCloudflare Queues向けのジョブ管理システムを採用していました。Queuesのインターフェースを隠して単純な同期処理らしく扱えるようにしてくれるライブラリで、状態管理や再試行まで含めて面倒を見てくれるため、かなり便利に使っていました。",{"type":19,"content":446},[447],{"type":16,"text":448},"一方で、実際のサービスに載せて運用していると、少しずつKiribiの外側に欲しいものが増えていきました。特定の種類のジョブだけ同時実行数を厳密に制限したかったり、待機中のジョブに優先順位を付けたかったり、同じ対象に対するジョブを重複して積みたくなかったり、複数の処理に依存関係を持たせたかったり、みたいなやつです。",{"type":19,"content":450},[451],{"type":16,"text":452},"どれも個別には実装できますし、Kiribi自体も使いづらいライブラリだったわけではありません。が、必要な機能をアプリケーション側で補っていくと、Kiribiを使いながら別のジョブスケジューラを作っているような微妙な状態になってきました。",{"type":19,"content":454},[455],{"type":16,"text":456},"KiribiへContributeする方向も当然考えたものの、2年以上ほどメンテナンスが続いていませんでしたし、必要としていた変更も単純な機能追加というより、スケジューリングや状態管理の責務をどこに置くかという設計そのものに関わるものであったことも踏まえ、既存の設計へ無理に差し込むよりも現在のCloudflareスタックを前提に一度作り直した方がよいのではないか、と考えました。",{"type":19,"content":458},[459],{"type":16,"text":460},"有難いことに、Kiribiが登場した当時からCloudflare側の状況もかなり変わっているようで、Cloudflareスタックの中だけでより複雑な状態管理を成立させるための選択肢も増えており、その結果として作り始めたのがTsumugiです。",{"type":10,"attrs":462,"content":464},{"level":12,"id":463},"hf2b8efe0f7",[465],{"type":16,"text":466},"Queuesだけでは少し足りない",{"type":19,"content":468},[469],{"type":16,"text":470},"Cloudflare Queuesは、consumerへの配送, 失敗時の再試行まで面倒を見てくれる便利なものです。単純な非同期処理であれば、(希に揮発することもありますが)Queueを直接使うだけで十分なことも多いです。",{"type":19,"content":472},[473,475,476],{"type":16,"text":474},"しかし、実際に使っていると、Queuesはあくまで配送のためのものであって、次にどのジョブを実行するべきかを判断するスケジューラとして使えない部分がつらくなってきます。",{"type":97},{"type":16,"text":477},"既にn件のジョブが実行されている間はn+1件目をまだ動かしたくないだとか、待機中のジョブの中でもpriorityの高いものを先に実行したいだとか、あるジョブは別のジョブが成功するまで開始させたくないみたいな場合ですね。",{"type":19,"content":479},[480],{"type":16,"text":481},"こういう細かな制御をしたい場合、今どういう状態なのかを見ながら次に何を実行するかを判別する実装を書いてあげる必要があります。",{"type":10,"attrs":483,"content":485},{"level":12,"id":484},"ha960b4c985",[486],{"type":16,"text":487},"Durable Objectで判別, Queuesで実行",{"type":19,"content":489},[490],{"type":16,"text":491},"Tsumugiでは、bindingごとにDurable Objectを置き、それをスケジューラ兼コーディネータとして利用しています。",{"type":145,"attrs":493,"content":495},{"language":494,"filename":13},"mermaid",[496],{"type":16,"text":497},"flowchart LR\n    A[Application] -->|enqueue| B[Durable Object]\n    B -->|dispatch| C[Cloudflare Queues]\n    C --> D[Performer]\n\n    B --> E[Outbox]\n    E -->|projection| F[D1]\n    F --> G[Dashboard \u002F REST API]\n",{"type":19,"content":499},[500],{"type":16,"text":501},"Durable Objectは、待機中のジョブ、現在実行中のジョブ、実行可能時刻、priority、concurrency、rate limitなどを見ながら、次に何をdispatchするかを決定しています。",{"type":19,"content":503},[504],{"type":16,"text":505},"この部分をDurable Objectに委ねた主な理由として、整合性が欲しかったためです。たとえば「このジョブは最大3件まで同時実行する」と決めたのであれば、4件目は本当に実行されてほしくありません。D1上のカウンタを何となく増減させながら判定するよりも、ひとつのDurable Objectが状態を持ち、そこで判断を直列化した方がはるかに分かりやすいはずです。",{"type":19,"content":507},[508,510,511,513,517],{"type":16,"text":509},"ただし、Durable Object自身にジョブ本体を実行させているわけではありません。",{"type":97},{"type":16,"text":512},"たとえば10分かかる処理をDurable Objectから直接",{"type":16,"marks":514,"text":516},[515],{"type":92},"await",{"type":16,"text":518},"した場合、その10分間ずっとDurable Object側のリクエストを生かしておくことになり、それはあまり嬉しくありません。",{"type":19,"content":520},[521],{"type":16,"text":522},"なのでTsumugiでは、Durable Objectは判断するだけで、実際の実行はQueuesに任せる、という形にしています。",{"type":10,"attrs":524,"content":526},{"level":12,"id":525},"hf9050c2e7f",[527],{"type":16,"text":528},"理解せず単純に扱える",{"type":19,"content":530},[531],{"type":16,"text":532},"内部の話をすると、Durable Object, SQLite, Queues, D1, outbox, projectionなど色々なものが出てきます。",{"type":19,"content":534},[535],{"type":16,"text":536},"とはいえ、利用者側までその事情を意識する必要がないようにしたかったので、表側はなるべく普通のジョブライブラリっぽくしています。",{"type":19,"content":538},[539],{"type":16,"text":540},"たとえばメールを送るジョブならこんな感じで書けます。",{"type":145,"attrs":542,"content":544},{"language":543,"filename":13},"ts",[545],{"type":16,"text":546},"import { Performer } from 'tsumugi\u002Fperformer';\n\nexport class SendMail extends Performer\u003C{ to: string }, void, {}, Env> {\n  async perform(payload: { to: string }): Promise\u003Cvoid> {\n    await this.env.MAILER.send(payload.to);\n  }\n}\n",{"type":19,"content":548},[549],{"type":16,"text":550},"これをWorkerのトップレベルからexport",{"type":145,"attrs":552,"content":553},{"language":543,"filename":13},[554],{"type":16,"text":555},"export * from '.\u002Fperformers\u002Findex.js';\n",{"type":19,"content":557},[558],{"type":16,"text":559},"あとは",{"type":145,"attrs":561,"content":562},{"language":543,"filename":13},[563],{"type":16,"text":564},"await tsumugi.enqueue(env, {\n  binding: 'SendMail',\n  payload: {\n    to: 'a@example.com',\n  },\n});\n",{"type":19,"content":566},[567],{"type":16,"text":568},"とすればenqueueできます。",{"type":19,"content":570},[571,573,577,579,583,585,589],{"type":16,"text":572},"ちなみに",{"type":16,"marks":574,"text":576},[575],{"type":92},"binding",{"type":16,"text":578},"に",{"type":16,"marks":580,"text":582},[581],{"type":92},"SendMail",{"type":16,"text":584},"を指定すると、",{"type":16,"marks":586,"text":588},[587],{"type":92},"payload",{"type":16,"text":590},"もPerformer側から型推論されます。",{"type":19,"content":592},[593],{"type":16,"text":594},"同じ情報を複数箇所に書くのは冗長ですごく嫌いなので、TsumugiではWorkerからexportされているPerformerをruntime上のregistryとして使いつつ、型も同じ場所から取るようにしています。",{"type":10,"attrs":596,"content":598},{"level":12,"id":597},"hf5d5b8f0fc",[599],{"type":16,"text":600},"ジョブらしいことは一通りできる(はず)",{"type":19,"content":602},[603],{"type":16,"text":604},"基本のenqueueに加えて、遅延実行も指定できます",{"type":145,"attrs":606,"content":607},{"language":543,"filename":13},[608],{"type":16,"text":609},"await tsumugi.enqueue(env, {\n  binding: 'SendMail',\n  payload,\n  delayMs: 60_000,\n});\n",{"type":19,"content":611},[612],{"type":16,"text":613},"priorityを付けることもできます",{"type":145,"attrs":615,"content":616},{"language":543,"filename":13},[617],{"type":16,"text":618},"await tsumugi.enqueue(env, {\n  binding: 'SendMail',\n  payload,\n  priority: 10,\n});\n",{"type":19,"content":620},[621,623,627],{"type":16,"text":622},"同じ対象に対するジョブを重複して作りたくない場合は、",{"type":16,"marks":624,"text":626},[625],{"type":92},"uniqueKey",{"type":16,"text":628},"も明示可能です",{"type":145,"attrs":630,"content":631},{"language":543,"filename":13},[632],{"type":16,"text":633},"await tsumugi.enqueue(env, {\n  binding: 'SendMail',\n  payload,\n  uniqueKey: 'a@example.com',\n});\n",{"type":19,"content":635},[636],{"type":16,"text":637},"このほかにも、bindingごとの同時実行数制御, rate limit, retry, 定期実行, remote performer, retry\u002Fcancellation, 実行履歴の追跡などがあります。また、複数のジョブに依存関係を持たせるFlowという仕組みもありますが、これは少し毛色が違うので後述します。が、全部ここで説明し始めるとそのままREADMEになってしまうので、細かい使い方はドキュメントを見てください。",{"type":221,"attrs":639},{"url":402},{"type":19,"content":641},[642],{"type":16,"text":643},"ちゃんと書きました。",{"type":10,"attrs":645,"content":647},{"level":12,"id":646},"hc234659312",[648],{"type":16,"text":649},"ジョブ同士の依存関係も扱える",{"type":19,"content":651},[652],{"type":16,"text":653},"Tsumugiには、複数のジョブを依存関係ごとまとめて扱うFlowという仕組みがあります。",{"type":19,"content":655},[656],{"type":16,"text":657},"これは、ジョブを頂点、ジョブ同士の依存関係を辺とした有向非巡回グラフ(DAG)として処理全体を表現するものです。",{"type":19,"content":659},[660],{"type":16,"text":661},"たとえば画像を受け取って、まず解析を行い、その結果を使ってサムネイルの生成とメタデータの保存を並列に実行し、両方が終わったあとに公開処理を行う場合は、概念的には次のようなグラフになります。",{"type":145,"attrs":663,"content":664},{"language":494,"filename":13},[665],{"type":16,"text":666},"graph TD\n    A[画像を取得する] --> B[画像を解析する]\n    B --> C[サムネイルを生成する]\n    B --> D[メタデータを保存する]\n    C --> E[公開処理をする]\n    D --> E\n",{"type":19,"content":668},[669,671,675,677,681,683,687],{"type":16,"text":670},"この場合、",{"type":16,"marks":672,"text":674},[673],{"type":92},"公開処理をする",{"type":16,"text":676},"は、",{"type":16,"marks":678,"text":680},[679],{"type":92},"サムネイルを生成する",{"type":16,"text":682},"と",{"type":16,"marks":684,"text":686},[685],{"type":92},"メタデータを保存する",{"type":16,"text":688},"の両方が完了するまで実行されません。",{"type":19,"content":690},[691],{"type":16,"text":692},"こうした処理自体は、Aの処理が成功したらBをenqueueし、Bが終わったらCとDをenqueueし、それぞれの完了状態をどこかに記録して、両方が終わったらEをenqueueすることでQueues単体でも一応実現可能ではあります。しかし、どのジョブが完了したのか、次に実行可能になるジョブはどれなのか、途中のジョブが失敗した場合に後続をどう扱うのか、といったことまで自分で管理しなければなりません。",{"type":19,"content":694},[695],{"type":16,"text":696},"Tsumugiでは、この依存関係そのものをジョブシステム側の状態として管理します。あるジョブが完了すると、そのジョブを依存先としている後続ジョブについて、すべての依存関係が解決したかをTsumugi側で判定し、実行可能になったものだけを順次スケジューリングします。",{"type":19,"content":698},[699],{"type":16,"text":700},"Temporalのような汎用的で大規模なworkflow engineを目指しているわけではありませんが、バックグラウンド処理を書いていると「AとBが終わったらCを動かしたい」くらいの依存関係はわりと普通に出てきます。そうした関係を個々のPerformerの中へ書き散らすのではなく、ジョブ同士の関係そのものをTsumugiに管理させられる、というのがFlowです。",{"type":10,"attrs":702,"content":704},{"level":12,"id":703},"h2c993c5785",[705],{"type":16,"text":706},"Cloudflareスタック",{"type":19,"content":708},[709],{"type":16,"text":710},"TsumugiはCloudflare専用です。",{"type":19,"content":712},[713],{"type":16,"text":714},"少なくとも今のところ、AWSでも動くとかGCPでも動くとか、そういう方向はあまり考えていません。Durable ObjectsもQueuesもD1も普通に使います。",{"type":19,"content":716},[717],{"type":16,"text":718},"これは意図的なもので、どこでも動くライブラリを作ろうとすると、どうしても各環境の一番弱い共通部分へ寄せた抽象化になってしまいます。折角CloudflareにはDurable Objectsのような特殊で便利なプリミティブがあるのに、それを使わず一般化するのはもったいないと思っています。",{"type":19,"content":720},[721],{"type":16,"text":722},"逆に言うとTsumugiは、Cloudflareでしか動かなくていいので、Cloudflareではちゃんと気持ちよく動くものを目指しています。",{"type":19,"content":724},[725],{"type":16,"text":726},"こういうことばかり言うと気持ち悪いのでデメリットっぽいものも紹介しておくと、Workers Paidが必要ですし、D1やQueuesの準備も必要です。Kiribiと比べて依存するサービス数も増えるのでコスト的には劣っています。単純に「重要度の低いWebhookを受け取って後で1回だけ処理したい」程度であれば、Tsumugiを入れる必要はありませんし、なんならQueuesを直接使った方がたぶんしあわせになれます。",{"type":10,"attrs":728,"content":730},{"level":12,"id":729},"h6bddf1fb83",[731],{"type":16,"text":732},"Kiribiとの違い",{"type":19,"content":734},[735],{"type":16,"text":736},"TsumugiはKiribiからかなり影響を受けていますが、結果として設計はだいぶ違うものになりました。",{"type":19,"content":738},[739],{"type":16,"text":740},"Kiribiは、(私の解釈に誤りがなければ)Cloudflare Queuesにジョブ管理の仕組みを足すという方向のライブラリです。それに対してTsumugiでは、Queuesをジョブシステムそのものではなく、実行時の配送手段として扱っています。",{"type":19,"content":742},[743],{"type":16,"text":744},"Kiribiの方が構成は単純ですし、Queues自身が持っているスケーラビリティの恩恵を素直に受けることができます。一方でTsumugiは、スケジューリングの判断を自分で握っている分、priorityや正確なconcurrency制御、Flow、rate limitなどをひとつの状態モデルの上で扱いやすくなっています。",{"type":19,"content":746},[747],{"type":16,"text":748},"ですので、TsumugiがKiribiの完全な上位互換だとは思っていません。そもそも、単純なジョブ管理ならKiribiの方が軽量ですし、Queuesをなるべくそのまま使いたいならKiribiの設計の方が自然に見えます。Tsumugiは、その先でもう少し制御したくなったときの選択肢として使っていただけると嬉しいです。",{"type":10,"attrs":750,"content":752},{"level":12,"id":751},"ha4ae1149d6",[753],{"type":16,"text":754},"分散システムは嫌",{"type":19,"content":756},[757],{"type":16,"text":758},"ここまで色々と書いていますが、Tsumugiの内部は割とちゃんと分散システムです。もうなんか嫌ですね。",{"type":19,"content":760},[761],{"type":16,"text":762},"Tsumugiの内部では、Durable Object, Queues, Performer, D1と複数のコンポーネントを跨いでジョブを処理しています。そのため、正常系だけでなく、それぞれの境界で処理が失敗した場合に状態をどう収束させるかはかなり意識して設計しました。",{"type":19,"content":764},[765],{"type":16,"text":766},"たとえば、Durable Object側ではジョブをdispatchする状態まで進んだもののQueueへの送信に失敗した場合や、Performerの処理自体は成功したものの、その完了をDurable Objectへ通知する段階で失敗した場合などがあります。D1へのprojectionについても同様で、途中で失敗したからといってジョブそのものの状態まで不整合になってしまうのは避けるべきです。",{"type":19,"content":768},[769],{"type":16,"text":770},"Tsumugiでは、こうした問題を扱いやすくするために、ジョブのauthoritativeな状態と実際の配送、実行、read modelへの反映をそれぞれ分けています。",{"type":19,"content":772},[773],{"type":16,"text":774},"例として、retryについてはQueues側へ判断を任せるのではなく、Durable Object側をauthorityとして扱っています。また、D1はあくまでread modelなので、projectionが一時的に遅れたり失敗したりしても、それによってジョブの実行状態そのものが変わることはないようにしています。配送保証についてもジョブ単位で扱えるようにしていて、at-most-onceを必要とするジョブについては、実行前のclaimを含めて通常のジョブとは異なる扱いをしています。",{"type":19,"content":776},[777],{"type":16,"text":778},"このあたりは機能だけを見るとあまり目立たない部分ですが、ジョブ管理システムとして実際に使ううえではかなり重要だと考えていて、正常系だけでなく、Queueへの配送やWorkerの実行、状態更新のどこかが途中で失敗した場合にも、最終的に説明可能な状態へ戻せることを意識しました。",{"type":10,"attrs":780,"content":782},{"level":12,"id":781},"hb7ef8b25a6",[783],{"type":16,"text":784},"導入周りもまとめて",{"type":19,"content":786},[787],{"type":16,"text":788},"Tsumugiはnpm packageとして公開しているほか、初期セットアップ用のCLI, D1のmigration, 管理画面, REST API, ドキュメント, 実装例等もまとめて提供しています。",{"type":19,"content":790},[791,793,797],{"type":16,"text":792},"導入自体は、packageを追加したあとに",{"type":16,"marks":794,"text":796},[795],{"type":92},"tsumugi init",{"type":16,"text":798},"を実行するところから始められます。",{"type":145,"attrs":800,"content":802},{"language":801,"filename":13},"bash",[803],{"type":16,"text":804},"pnpm add tsumugi\nnpx tsumugi init\n",{"type":19,"content":806},[807,810],{"type":16,"marks":808,"text":796},[809],{"type":92},{"type":16,"text":811},"では、Tsumugiが利用するD1 DatabaseやQueueの作成、wrangler設定やソースコードの雛形生成、migrationの適用などを行います。",{"type":19,"content":813},[814],{"type":16,"text":815},"Tsumugiは内部でDurable Objects、Queues、D1と複数のCloudflareサービスを利用するため、これらをすべて手作業で設定する形にすると導入時に確認する項目がかなり増えてしまいます。そのため、最初に必要になる構成についてはできるだけCLI側で生成するようにしました。",{"type":19,"content":817},[818],{"type":16,"text":819},"加えて、ジョブの状態を確認するためのダッシュボードもpackageに含まれており、別途管理画面を用意しなくとも、ジョブの一覧や実行履歴, エラーの確認, retry\u002Fcancelなどの操作が可能です。REST APIも提供されるため、必要であれば独自の管理ツールなどから操作することもできるかと思います。",{"type":19,"content":821},[822],{"type":16,"text":823},"内部構成はそれなりに複雑ですが、Tsumugiを利用するにあたっては、その中身を意識せず、ブラックボックスとして容易に扱えることを意識しています。",{"type":10,"attrs":825,"content":827},{"level":12,"id":826},"h9be0c3393d",[828],{"type":16,"text":829},"おわりに",{"type":19,"content":831},[832],{"type":16,"text":833},"そんなわけで、Cloudflareスタック向けのジョブ管理システム、Tsumugiを作りました。",{"type":19,"content":835},[836],{"type":16,"text":837},"もともとはKiribiを使っていて、実運用の中で「もう少しここを制御したい」と思ったことが始まりでした。Kiribi自体は便利でしたが、必要な機能が増えるにつれて既存の設計の外側へはみ出す部分が増え、さらに当時すでに2年以上メンテナンスが続いていなかったこともあり、今のCloudflareスタックを前提に一度作り直してみることにしました。",{"type":19,"content":839},[840],{"type":16,"text":841},"Kiribiが登場した頃から時間が経ち、Cloudflareのサービスも増え、Workersだけでシステムを組むこと自体もずいぶん現実的になりました。であれば、今あるプリミティブを前提に、もう一度ジョブ管理システムを考えてみよう。というのがTsumugiです。",{"type":19,"content":843},[844],{"type":16,"text":845},"Cloudflare Queuesを使っていて、「配送はQueuesでいいけれど、ジョブの実行順や状態はもう少し自分で管理したい」だとか、「同時実行数やpriority、依存関係まで含めて扱いたい」みたいなものにあたりに心当たりがあれば、試してもらえると嬉しいです。",{"type":19,"content":847},[848],{"type":16,"text":849},"まだ利用者もほとんどいないので、設計上変なところ、不具合、使いづらいAPI、こういうものが欲しい、などあれば雑にでもIssueを投げてもらえると助かります。",{"type":19,"content":851},[852],{"type":16,"text":853},"ここまで読んで、まあちょっと面白いことをやっているじゃん くらいに思っていただけたのであれば、Starをひとつ置いていってもらえると私が喜びます。",{"type":221,"attrs":855},{"url":394},{"type":19,"content":857},[858],{"type":16,"text":859},"それでは〜","どうも、私です。ここ数年で、Cloudflare Workersを取り巻く状況はかなり変わっているようで、Workersそのものだけでなく、QueuesやDurable Objects、D1、Analytics Engine等、Cloudflareスタックの中で使えるサービスが結構増えました。加えて",8111,[863,864],{"id":346,"name":347,"slug":348},{"id":350,"name":351,"slug":352},"2026-08-13T10:54:50.763Z","2026-08-13T09:19:06.202Z","2026-08-23T08:54:37.213Z",{"id":869,"title":870,"content":871,"summary":1215,"charCount":1216,"eyecatch":13,"tags":1217,"is_no_index":353,"publishedAt":1224,"createdAt":1225,"updatedAt":1226},"PNq5wx1vUeCH","Workers Tech Talks #8を見て思ったこと",{"type":7,"content":872},[873,877,880,887,891,895,899,904,908,912,916,920,927,933,940,944,949,956,960,964,968,972,976,983,987,991,995,999,1003,1008,1012,1016,1020,1027,1031,1035,1039,1043,1047,1052,1056,1060,1064,1071,1083,1087,1091,1098,1105,1109,1113,1117,1121,1125,1129,1134,1141,1145,1149,1153,1157,1161,1165,1169,1173,1177,1181,1185,1192,1196,1200,1204,1211],{"type":10,"attrs":874,"content":875},{"level":12,"id":13},[876],{"type":16,"text":17},{"type":19,"content":878},[879],{"type":16,"text":22},{"type":19,"content":881},[882,884,885],{"type":16,"text":883},"先日、Cloudflare Workers Tech Talks in Tokyo #8を見ました。",{"type":97},{"type":16,"text":886},"で、見終わった感想なんですが、あまり面白くなかったです。",{"type":19,"content":888},[889],{"type":16,"text":890},"全部が全部というわけではなく、Containersの話やメルカリの話なんかは興味深く聞いていました。ただ、全体的に技術的な踏み込みが浅いというか、聞き終わって「結局何の話だったんだろう」となる発表がちらほらあって、すごく悲しい気持ちになったというのが正直な感想です。",{"type":19,"content":892},[893],{"type":16,"text":894},"別にプレゼンが下手だったわけではありませんし、むしろ全体的に皆さん発表が上手かったと思います。",{"type":19,"content":896},[897],{"type":16,"text":898},"ただ、私はテックトークにそういうものを求めていなかったようです。",{"type":10,"attrs":900,"content":901},{"level":12,"id":13},[902],{"type":16,"text":903},"何をもってそう思ったの",{"type":19,"content":905},[906],{"type":16,"text":907},"例えば、大規模SaaSの話は、Cloudflareにはこういうサービスがあって、従来こうしていたものがこうできて、Cloudflareでも大規模なSaaSを作れそうだ、というような話だったかと思います。",{"type":19,"content":909},[910],{"type":16,"text":911},"で、何をもって作れそうだと判断したんですかね。",{"type":19,"content":913},[914],{"type":16,"text":915},"Cloudflareに何のサービスがあって、それぞれ何がどのようにできるのかは、概ね公式ドキュメントを読めば分かります。実際に大規模SaaSのアーキテクチャを検討したのであれば、既存の構成と比較してどこが問題になったのか、Cloudflare固有の制約をどう評価したのか、どこまでをCloudflareに載せられて、どこからは載せられないのか。そういう話が私は聞きたかったです。",{"type":19,"content":917},[918],{"type":16,"text":919},"他の発表を聞いていても、似たようなことを何度か思いました。",{"type":19,"content":921},[922,924,925],{"type":16,"text":923},"｢この技術を使いました。｣",{"type":97},{"type":16,"text":926},"なぜ？",{"type":19,"content":928},[929,931,932],{"type":16,"text":930},"｢この構成にしました。｣",{"type":97},{"type":16,"text":926},{"type":19,"content":934},[935,937,938],{"type":16,"text":936},"｢これで実現できました。｣",{"type":97},{"type":16,"text":939},"そもそも何が課題で、それをどう解決するためにこの構成になったの？",{"type":19,"content":941},[942],{"type":16,"text":943},"技術スタックそのものを知りたいのではなくて、その技術スタックに至るまでの判断が知りたかったです。結果だけ並べられても、｢へえ、そうなんですね、すごい｣で終わってしまいます。",{"type":10,"attrs":945,"content":946},{"level":12,"id":13},[947],{"type":16,"text":948},"Vibe Codingの話",{"type":19,"content":950},[951,953,954],{"type":16,"text":952},"今回かなり話題になっていたのが、宮本佳林さんの「アイドル視点から見たものづくりの本質」という発表でした。",{"type":97},{"type":16,"text":955},"自分の活動で使うWebサイトや配信システムなんかを、LLMを使いながら自分で作っている、という話です。",{"type":19,"content":957},[958],{"type":16,"text":959},"まず前提として、作ったこと自体はとてもすごいことだと思います。",{"type":19,"content":961},[962],{"type":16,"text":963},"必要なものがあって、それを自分で作って、実際の活動に投入している。世の中、口だけ達者で何も作らない人間が嫌なほど沢山いるので、手を動かして成果物を出していることはとてもすごい事だと思います。",{"type":19,"content":965},[966],{"type":16,"text":967},"プレゼンはすごく上手かったですし、話の組み立ても上手で、エンタメとして見れば、普段見る技術者の発表より完成度が高かったように思います。｢やっぱりアイドルってすごいんだな〜｣などと思いながら見ていました。",{"type":19,"content":969},[970],{"type":16,"text":971},"ただ、内容についてはあまり好きではありませんでした。",{"type":19,"content":973},[974],{"type":16,"text":975},"例えば、10時間配信のためのシステムを作ったという話であれば、10時間配信を成立させるにあたって何が課題だったのか、それを解決するためになぜそのアーキテクチャを選んだのか、私はその辺が気になりました。",{"type":19,"content":977},[978,980,981],{"type":16,"text":979},"KV, D1, R2を採用したのであれば、それぞれ何を保存して、何を根拠にそこへ保存することにしたのか。KVの整合性モデルは要件上問題にならなかったのか。LLMが生成した実装をどう検証して、本番投入して問題ないと判断したのか。",{"type":97},{"type":16,"text":982},"せっかくVibe Codingを題材にするのであれば、そういう話題を深堀すると面白い話ができると思います。",{"type":19,"content":984},[985],{"type":16,"text":986},"コードの生成をLLMに任せるとして、人間はどこまでシステムを理解する必要があるのか。生成された設計が妥当であることをどう判断するのか。自分の理解を超えた実装が出てきたときにどうするのか。",{"type":19,"content":988},[989],{"type":16,"text":990},"この辺の話題には私自身、かなり興味があります。",{"type":19,"content":992},[993],{"type":16,"text":994},"しかし、実際の発表ではそこまで踏み込まれず、私には概ね「LLMを使いながら色々作ってみたら楽しく作れました」という話に聞こえました。",{"type":19,"content":996},[997],{"type":16,"text":998},"それはそれで良いことだと思います。",{"type":19,"content":1000},[1001],{"type":16,"text":1002},"ただ、テックトークとして聞いて面白いかというと、すごく申し訳ないのですが、私はあまり面白く思えませんでした。",{"type":10,"attrs":1004,"content":1005},{"level":12,"id":13},[1006],{"type":16,"text":1007},"DynamoDB",{"type":19,"content":1009},[1010],{"type":16,"text":1011},"中でも特に気になったのが、DynamoDBについてのくだりでした。",{"type":19,"content":1013},[1014],{"type":16,"text":1015},"チケットシステムについて宮本さんが「これはDynamoDBではないか」と質問したところ、相手の会社の方が驚いていた、という話がありました。その理由として、チケットシステムは処理が速いからDynamoDBではないかと考えた、という趣旨の説明がされていたと思います。",{"type":19,"content":1017},[1018],{"type":16,"text":1019},"ここは聞いていて、かなりもにょもにょしました。",{"type":19,"content":1021},[1022,1024,1025],{"type":16,"text":1023},"速いからDynamoDB。",{"type":97},{"type":16,"text":1026},"いや、なんで？",{"type":19,"content":1028},[1029],{"type":16,"text":1030},"DynamoDBが低レイテンシで大規模なワークロードを捌けるのはそうなんですが、当然ながら「処理が速いシステムだからDynamoDB」というだけでデータストアを推測できるわけではありません。",{"type":19,"content":1032},[1033],{"type":16,"text":1034},"どんなデータを扱っているのか、アクセスパターンはどうなっているのか、どの程度の一貫性が必要なのか、スケール特性はどうなのか。その辺を考えた結果としてDynamoDBではないかと推測したのであれば、その思考過程こそ聞きたいところです。",{"type":19,"content":1036},[1037],{"type":16,"text":1038},"少なくとも発表中の説明だけを聞く限りでは、DynamoDBについて中途半端に見聞きした特徴をそのまま当てはめているようにしか見えず、私はあまり好きではありませんでした。",{"type":19,"content":1040},[1041],{"type":16,"text":1042},"もちろん、実際にはもっと色々考えた上での発言だった可能性はあります。発表時間の都合で省いただけかもしれません。",{"type":19,"content":1044},[1045],{"type":16,"text":1046},"なら、なおさらその話してほしかったです。",{"type":10,"attrs":1048,"content":1049},{"level":12,"id":13},[1050],{"type":16,"text":1051},"「アイドルなのにすごい」",{"type":19,"content":1053},[1054],{"type":16,"text":1055},"世の中には私が面白いと思わないものを面白いと思う人も沢山居るでしょうし、その逆も当然あります。プレゼンの上手さや、ものづくりの楽しさを伝えることに価値を感じる人もいるでしょう。",{"type":19,"content":1057},[1058],{"type":16,"text":1059},"好みの問題なので、別にそれはどうでもいいのかもしれません。",{"type":19,"content":1061},[1062],{"type":16,"text":1063},"ただ、発表後の反応を見ていて、別のところで引っ掛かりました。",{"type":19,"content":1065},[1066,1068,1069],{"type":16,"text":1067},"「アイドルなのにこんなことができるのがすごい」というような評価です。",{"type":97},{"type":16,"text":1070},"これ、褒めているようで結構失礼ではないですかね？",{"type":19,"content":1072},[1073,1075,1081],{"type":16,"text":1074},"以前、",{"type":16,"marks":1076,"text":1080},[1077],{"type":269,"attrs":1078},{"href":1079,"target":13,"rel":13,"class":13,"title":13},"https:\u002F\u002Fnote.com\u002Fcellacoates\u002Fn\u002Fn6711046dea15","〇〇なのに、凄い！n回目",{"type":16,"text":1082},"というnoteの記事を読みました。そこでは「アイドルなのにサッカーに詳しい」といった表現について、その言葉が成立する背景にある、属性への期待について書かれていました。",{"type":19,"content":1084},[1085],{"type":16,"text":1086},"「アイドルなのに技術ができてすごい」も同様であると私は思います。",{"type":19,"content":1088},[1089],{"type":16,"text":1090},"この評価が成立するためには、まず「アイドルには通常、この程度の技術的能力はないだろう」という期待が必要になります。アイドルという属性からあらかじめ期待値を下げておいて、その低く設定した基準を超えたので「すごい」と評価する。",{"type":19,"content":1092},[1093,1095,1096],{"type":16,"text":1094},"要するに「あなたの属性であれば、この程度の基準で評価するのが妥当でしょう」という話です。",{"type":97},{"type":16,"text":1097},"私はそこに、ある種の差別的、あるいは蔑視的なものを感じます。すごく傲慢ですよね。",{"type":19,"content":1099},[1100,1102,1103],{"type":16,"text":1101},"念のため書いておくと、「アイドルなのにすごい」と言っている人に悪意があるとは思っていません。",{"type":97},{"type":16,"text":1104},"ただ、悪意がないことと、その見方に差別的なものが含まれていないことは別でしょう。",{"type":19,"content":1106},[1107],{"type":16,"text":1108},"例えば「女性なのに論理的ですね」だとか、「高齢者なのにITに詳しいですね」みたいな言葉も、発言者は褒め言葉として使えます。",{"type":19,"content":1110},[1111],{"type":16,"text":1112},"しかし、その言葉が褒め言葉として成立するためには、「女性は普通それほど論理的でない」「一般的な高齢者はITに詳しくない」という前提となる期待が必要です。",{"type":19,"content":1114},[1115],{"type":16,"text":1116},"少なくとも私は言われたくないです。普通に失礼だと感じるので。",{"type":19,"content":1118},[1119],{"type":16,"text":1120},"アイドルについても同様で、アイドルという職業から技術的な能力を勝手に推測して、通常より低い評価基準を適用することが、果たして相手に敬意を持った接し方なのかは疑問です。",{"type":19,"content":1122},[1123],{"type":16,"text":1124},"以前、属性によるカテゴライズから完全に逃れることは無理だろう、というようなことを書きました。なので、「人を属性で見るな」のような綺麗なことを言うつもりは毛頭ありません。私自身、人間を様々な属性で括って見ていますし、アイドルという職業から何らかの傾向を推測すること自体を差別だと言い始めたら、人間の認知そのものが成立しなくなってしまいます。",{"type":19,"content":1126},[1127],{"type":16,"text":1128},"ただ、同時に、その属性をそのまま個人の評価基準にまで持ち込むことには、もう少し慎重であるべきだと私は考えます。",{"type":10,"attrs":1130,"content":1131},{"level":12,"id":13},[1132],{"type":16,"text":1133},"同じ土俵",{"type":19,"content":1135},[1136,1138,1139],{"type":16,"text":1137},"私は、宮本さんがアイドルだから技術的に未熟だと思いたくはありません。",{"type":97},{"type":16,"text":1140},"同時に、アイドルだから多少理解が浅くてもすごい、とも思いたくありません。",{"type":19,"content":1142},[1143],{"type":16,"text":1144},"Cloudflare Workersのテックトークに登壇して、Cloudflare Workersを使ったものづくりについて話しているのであれば、私はその発表を普通に一本のテックトークとして聞きたいと思います。",{"type":19,"content":1146},[1147],{"type":16,"text":1148},"その結果として、技術的に面白ければ面白いと言うし、理解が浅いと思えば浅いと言いたい。",{"type":19,"content":1150},[1151],{"type":16,"text":1152},"寧ろ、「本業はアイドルなんだから技術的に浅くても仕方ないよね」と評価基準を変える方が、私には余程失礼に思えます。",{"type":19,"content":1154},[1155],{"type":16,"text":1156},"もちろん、本業とは全く違う分野のものを短期間で学び、実際に動くところまで持っていったという事実は、それ単体で評価できます。",{"type":19,"content":1158},[1159],{"type":16,"text":1160},"作ったことはすごい。",{"type":19,"content":1162},[1163],{"type":16,"text":1164},"行動力もすごい。",{"type":19,"content":1166},[1167],{"type":16,"text":1168},"プレゼンも上手い。",{"type":19,"content":1170},[1171],{"type":16,"text":1172},"ただ、技術的な内容はすごく浅いように感じました。",{"type":19,"content":1174},[1175],{"type":16,"text":1176},"全てを「アイドルなのにすごい」という便利な一言に押し込める必要はないでしょうし、押し込めるべきではないでしょう。",{"type":10,"attrs":1178,"content":1179},{"level":12,"id":13},[1180],{"type":16,"text":310},{"type":19,"content":1182},[1183],{"type":16,"text":1184},"今回のWorkers Tech Talksを見ていて一番残念だったのは、宮本さんの発表というより、全体として技術的に深掘りする発表が思ったより少なかったことです。",{"type":19,"content":1186},[1187,1189,1190],{"type":16,"text":1188},"何を使ったかより、なぜそれを使ったのか。作れたという結果より、なぜそれで作れると判断したのか。",{"type":97},{"type":16,"text":1191},"そういう公式ドキュメントを読めば分かるようなサービスの紹介よりも、実際に使った人間がどこで苦しんだのかが知りたかったです。",{"type":19,"content":1193},[1194],{"type":16,"text":1195},"Vibe Codingなら、LLMで作れましたという話より、LLMに実装を委ねた状態でどうシステムの正しさを担保したのか。そういう話の方が、私は面白いと思います。",{"type":19,"content":1197},[1198],{"type":16,"text":1199},"世間の反応を見る限り、今回のイベントを楽しんだ人はかなり多かったようなので、単純に私がテックトークに求めるものとズレているだけなのかもしれません。",{"type":19,"content":1201},[1202],{"type":16,"text":1203},"好みの話なので仕方ないのかもしれませんが。",{"type":19,"content":1205},[1206,1208,1209],{"type":16,"text":1207},"ただ、「アイドルなのにすごい」という評価については、それとは別にやっぱり好きではありません。",{"type":97},{"type":16,"text":1210},"属性によって期待値を下げた上で褒めるくらいなら、普通に同じ土俵で、その人が実際にやったことを見ればいいんじゃないでしょうか。",{"type":19,"content":1212},[1213],{"type":16,"text":1214},"少なくとも私は、その方が相手に対して幾分か誠実な態度であると思います。","どうも、私です。先日、Cloudflare Workers Tech Talks in Tokyo #8を見ました。で、見終わった感想なんですが、あまり面白くなかったです。全部が全部というわけではなく、Containersの話やメルカリの話なんかは興味深く聞いていました。ただ、全体的に技術的な踏み込",4348,[1218,1219,1220],{"id":350,"name":351,"slug":352},{"id":342,"name":343,"slug":344},{"id":1221,"name":1222,"slug":1223},"wia4slp55q","独り言","thoughts","2026-08-28T20:06:27.604Z","2026-08-28T20:05:08.273Z","2026-08-29T06:56:29.446Z"]