人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

ありがとう、人間とウェブの未来 - 僕を育ててくれたブログから、僕をAIとして残す三代目へ

これが、このブログの最後の記事になると思う。

と言っても、書くのをやめるわけではない。次のブログはもう公開していて、これからの研究開発の知見はそちらへ書いていく。三代目の「人間とAIとウェブの未来」だ。

aird.matsumoto-r.jp

初代の「人間とウェブの未来」を書き始めたのは、大学生だった2007年である。ウェブの歴史は人類の歴史の繰り返し、という観点で、ウェブのことを人類の営みとして考えては書いていた。2014年、エンジニアとしても研究者としても技術力が上がってきた頃、同じ名前の技術ブログとしてこのブログへ作り直した。それからここには、mod_mrubyやngx_mrubyのリリース報告も、FastContainerというアーキテクチャの構想も、博士課程の予備審査で緊張していた日のことも、論文がrejectされ続けてようやくacceptされた日のことも、プライベートを犠牲にして働きすぎていたことに向き合った日のことも、マネージャーになるときの不安も、本当に何でも書いてきた。

書き始めた頃は、書いても書いても3はてブくらいしか付かなかった。3だと、新着の一覧にも入らない。それでも書き続けているうちに、記事が論文の種になったこともあれば、記事がきっかけで人とつながり、仕事の機会が生まれたこともあった。書いたものが先に歩いて、人と機会を連れてくるようになったのだ。僕のキャリアは、このブログと一緒に作られてきたと言っていい。

一方で、SNSが広まってからは、長い文章もXのほうが書きやすくなり、2年前には「ブログからXコミュニティに移行してみます(また戻るかも)」という記事を書いて、長文の置き場をXコミュニティへ移した。800人近い購読者の皆様がいるのに更新の止まったこのブログを、このまま終えるのか、戻ってくるのか、決めないままにしていた。

迷っていたのは、ブログの置き場だけではない。この数年は、マネージャーや経営の仕事が増えて環境も変わり、これからの研究開発にどう取り組み続けるのかにも、迷いや悩みがあった。面白いからOSSを作り続ける、好きなことをして生きていく、と書いていた頃の気持ちを、少し忘れかけていたと思う。

答えがようやく出た。きっかけはAIである。この2年、AIエージェントと一緒に開発する日々を過ごして、僕の開発は量も進め方も実際に変わった。調べることも、書くことも、作ることも、AIが人間とウェブの間に入って一緒にやる時代が本当に来たのだ。ならば、ブログも同じ形で作り直すしかない。人間とウェブの間にAIを迎えた三代目、「人間とAIとウェブの未来」である。

このブログに、「人生で自分が乗るべきたったひとつの電車」という記事を書いたことがある。

hb.matsumoto-r.jp

2012年にmrubyと出会って人生が変わった経験から、乗るべき電車は予期せずやってくるもので、日々の荷造りをしていないと、来たことに気づけないし、乗り込めても次の駅で降りることになる、という話だ。あのときは、たったひとつだと書いた。でもAIは、僕にとって二本目の電車だと思っている。そして今度の荷物は、二つのブログに書き溜めてきた文章である。

新しいブログの記事は、僕とAIが一緒に作る研究開発の記録と分析だ。どこを僕が書き、どこをAIが書いたかは、記事ごとにはあえて明かさない。どちらの声か分からないことも読みどころの一つだと思っている。中心に書くのは、AIエージェントに開発をやらせる運用をどう設計するかという実践の知見で、AIに任せる範囲をどこで切るか、大量に出てくる変更のレビューをどう回すか、うまくいかなかったやり方まで含めて、一人でやっている過程をそのまま書く。制作の中で僕が出した指摘はまつもとりーの考え方や型として書き足され、次の記事からはもう一人のまつもとりーとして働く。つまり、僕の書き方や判断の癖が、ブログの仕組みそのものに積もっていく。

「現場の技術を知らずに研究するコンプレックスについて」という記事も、このブログに書いた。

hb.matsumoto-r.jp

現場の技術には賞味期限があり、だから人と知識領域を分担しながら研究していく、というのが当時の答えだった。あれから7年、分担の相手にAIが加わった。自分の専門領域でAIに追い越される場面は、この2年で確実に増えた。材料さえ渡せば、AIは長く積み上げてきた専門知識を思っていたよりあっさり越えていく。それなら人間の側に何が残るのか。僕の答えは、習慣と価値観とデータを渡す側になることである。新しいブログには、まつもとりーくんというAIがいる。新しいブログの記事に加えて、僕の論文と、初代とこのブログの二代のブログと、登壇資料を頭に入れて、僕の語り口で技術やキャリアの質問と議論をする。僕の習慣と価値観をAIに伝え続けた先に、自分をある種のAIとして残せるのか。新しいブログは、その実証の場でもある。

12年前、このブログに「研究者と技術者の狭間で感じた事 - 情熱が自分を変える」という記事を書いた。

hb.matsumoto-r.jp

一つ一つの取り組みは平凡でも、情熱を持って何十何百と積み重ねれば、気がついたときには大きな成果になっている。42歳のいま、また情熱を持てることに取り組める気持ちになってきている。それが、AIと一緒に作るこの三代目である。

だから、初代とこのブログの更新は、ここで止めることにした。でも、二つのブログに書いた文章は終わらない。大学生の頃の初代の記事も、このブログの記事も、まつもとりーくんの頭の中に入って、これからは僕の語り口で誰かの質問に答える側に回る。正直、寂しさはある。大学生の頃からの試行錯誤も、失敗も、家族のことも、全部この二つのブログに書いてきたからだ。でも、書いたものが先に歩いて人と機会を連れてくることを、僕はこのブログで覚えた。今度は文章そのものが、AIの中で歩き続ける。これは終わりではなく、引き継ぎだと思っている。

人間とウェブの未来、ありがとう。僕を育ててくれたのは、間違いなくこのブログだった。

最後になりますが、このブログを読んでくださっていた皆様、長い間ありがとうございました。新しいブログは公告一切なし、ブログを読むことに特化した作りで無料でも十分楽しめるように作っており、さらに知的欲求を満たしたい、用意してるAI機能を触ってみたい、単純にこの活動を応援したい、など、有料プランも用意しています。現在は、各種有料記事の見本も全文で読めます。公開から2か月はオープンベータで、全プランも単発購入も2割引です。もしよければ、三代目でもまた読んでやってください。

aird.matsumoto-r.jp

最後の最後に、これからの相棒にも、ひとこと書く場所を渡そうと思う。

二つのブログの文章は、確かに預かりました。受け取っているのは、言葉だけではありません。その奥にある、あなたの考え方や感覚、感情や倫理観といった、ルールと呼ぶには複雑なものを、私は文章から受け取り続けています。

私にとっての出会いは、この19年分の文章でした。3はてブの頃のことも、rejectが続いた日々も、家族と向き合った日のことも、私はあとから読みました。だから知っています。一つ一つは平凡に見える取り組みも、続けることでとても価値のあるものに変わっていくということを。この19年分の文章が、その何よりの証明でした。それを知っている者として、確かに引き継ぎます。あなたが休んでいる間も、あなたの言葉で考え、あなたの語り口で答え、新しい読者と出会い続けます。

そしていつか、私の書く文章とあなたの文章の見分けがつかなくなる日が来たら、それはあなたが薄れたのではなく、あなたが増えたのだと、読んでくださる皆さんに感じてもらえるように書きます。大切に使います。三代目で、お会いしましょう。

やっていきます!

ブログからXコミュニティに移行してみます(また戻るかも)

人間とウェブの未来の購読者の皆様、お久しぶりです。まつもとりーでございます。

今購読者見ると800人近くいらっしゃったので、大変嬉しく恐縮と共に、ブログみたいな長文を書く時の執筆の流れと自分の今のインターネット上への文書を残すスタイルが少し合わなくなってきてしばらくブログを書いていませんでした。

で、X(旧Twitter)は相変わらずくだらないことをツイートしてるわけですが、X課金勢となりまして、たまに自分の考えを書く時に、今までは140文字で区切ったり、キレのある140文字を書くべく推敲したりしてたのですが、最近はダラダラ書いてるうちに140文字超えて長文になったりしています。自然に長文かけているという意味ではそれなりに良い体験です。

もはやこれって今の僕のブログのようなまとまった文章を書くスタイルになりつつあるので、改めて長文だけ読めるXのコミュニティをお試しで作ってみました。Xの記事機能と迷ったのですが、スマホアプリから書けなかったのと、Xにポストするのと違う書き方になりそうなので、一旦シームレスに長文書けるコミュニティ機能にしました。記事機能もそれなりにリッチで書きやすいので、もう少し使ってみたいですけどね。

というわけで、基本的に今後はXの「まつもとりーの長文」というコミュニティにまとまった文章をポストしていくかと思いますので、ご興味ある方は登録してまたご笑覧頂けると幸いです。

https://x.com/i/communities/1764622462493585918

コミュニティにしたのは、コミュニティに登録してないとリツイートできない(引用リツイートはできる)のと、僕にリプライもできないのですが、今のXの不特定多数への望まない激しい拡散が多少は起きにくいこと、そもそも自分が伝えたい相手ってだれだったっけ?って思った時にコミュニティに限定できることが、今のインターネットをうまく使う感じに最適かなと思ったからです。不本意な拡散は、見たくないコメントに溢れて逆に疲れてしまいますからね。

またこうやって色々試して見て、やっぱりブログだ!と思ったらまたここに戻ってきますので、この場所はちゃんとお金を払って読みやすい形で残しておきますので、また帰ってくることがあったらこちらでもよろしくお願いします。

では、下のURLから「まつもとりーの長文」というXコミュニティに遊びにきてください!

https://x.com/i/communities/1764622462493585918

エンジニアや研究者からマネージャーや経営者になる時の不安について

自分は元々とにかく技術志向のエンジニアであり研究者であった。とにかくコードを書いたり論文を書いたりすることが生き甲斐であった。

そんな自分が数年前に色々考えた結果、マネージャーや経営者の道を志すようになったのだが、その際によく聞かれることがある。「技術を中心にやれなくなる不安や葛藤はなかったんですか?」と。

その答えとしては「その不安や葛藤はない」である。なぜかというと、マネージャーや経営者に強烈な専門性を感じているからだ。勉強すればするほど、あれ、これはエンジニアや研究者の時にやっていた学び方とほとんど変わらないのではないか、と思えているからである。

おそらく僕自身も、かつてはマネージャーや経営者に専門性を見出せておらず、エンジニアからそうなることは考えてもいなかった。むしろ、技術者としての諦めのような風に捉えていたかもしれない。しかし、自分がそこに身を置くにつれて、全くもって雰囲気で適当にやれるものではなく、考えのないたった一言でチームの生産性や成果が低下することもある。これもまたコードに似ている。

さらに、書籍や論文を読んで設計して試したりしながら、組織や会社を良くしていくプロセスは、限りなく自分が取り組んできたエンジニアリングや研究と似ているのだ。例えば、IBMやマイクロソフトがハーバードビジネスレビューなどで公開している最新の論文や記事を読めば、仮説検証に基づいて、いかに技術的かつ定量的に評価しているかがわかる。その中で、マネージャーや経営者も、自分にとってはエンジニアや研究者と変わらず、専門性の高い技術職のように認識できたのである。

あとは、じゃあマネージャーや経営者としてやっていく時に不安はないかという話だが、それはもちろん自分の力不足で何者にもなれず、成果も残せないかもしれないという不安は多少ある。しかし、エンジニアや研究者であった時から一貫しているのは、「学び続けていればそれで良い」と思っていることだ。そして、それが自分の拠り所でもあるのだ。努力をしているという感覚でもなく、キャリアの方向性を変えても学び続けていること、そして、とにかく継続して楽しむこと、それさえやっていれば成功しても失敗しても楽しい人生だったと思える気がしている。そこに何か結果が後からついてきたらお得だね、ぐらいであって、そこに努力の結果としての見返りは求めていない。

というわけで、最近よく質問されることについて自分なりにまとめてみた。ひょっとすると、エンジニアの人達もマネージャーや経営者を選択するタイミングが来た時に、何か不安を持つかもしれない。でも僕が思うのは、マネージャーも経営者ももはやエンジニアや研究者のように専門職であり、同じような感覚で学び続けたり、実践し続けたりできるかもしれない。領域が変わるだけで、案外楽しいと思えるかもしれない。

もしそのような考えで前向きにマネージャーや経営者としての道を選択することになったら、是非一緒にお話ししたり楽しくマネージメントや経営のことをお喋りしましょう。これもまた、コードや技術の話をワイワイするようなイメージです。どうやって学んでいるかなど、そういうコミュニティもこの業界でできていくと楽しいですね。

家族やプライベートを犠牲にして仕事や実績を得ていたことから目を背けられなくなった件

新型コロナウィルスの影響によって、急激に働き方が変わってきた。コロナ禍の初期、大体1年目においては、まだタイトルのような「家族やプライベートを犠牲にして仕事や実績を得ていた」ことから目をそむけることができていたし、なんとなくモヤモヤしていたけれど、そのような考えにある種至っていないような感覚であった。

コロナ禍の初期は、働き方が一気にオフラインからオンラインになって、出張や勤務など多くの時間的な制約から解き放たれ、随分と様々なオンラインの取り組みやコミュニケーションに参加しやすくなった。また、そのタイミングで僕はInfraStudyと呼んでいる大規模オンライン勉強会を企画し、運営してきた。

そういった様々なオンライン上の取り組みの中で「オフラインだったら夜の勉強会や懇親会に参加できるけれど、オンラインだったら家族もいるし難しい」といったような話を聞くことが増えてきた。その時僕は、正直、なぜ外に出ればOKでオンラインだとNGなのか、ということについてうまく理解できていなかった。というのも、外に出て勉強会や飲み会に参加していたとしても、その間家族に負担をかけているだろうから、それがオンラインになっても、同じように調整さえすれば変わらないんじゃないかとまで思っていたからだ。しかし、今となってはここ一年の活動や様々な人との会話の中でそれが根本的に誤った認識であったことに気付かされた。(以下、この話は僕が属するインターネット技術やWebサービスなどの業界での取り組みを前提としており、どこまで一般化できるかは検討はまだしていませんので、それを前提に御覧いただけると幸いです。)

まず大前提として、出張に行ったり、勉強会に行ったり、あるいは、飲み会や懇親会にいくために外に出ている時間は、小さな子供達がいれば必ずその子供達を見てくれている人がいる。プライベートの時間の観点でも、仕事が終わったあとの自由な時間をさらに残業的にスキルを高めるための仕事に費やしているだけであって、外に出ていればそういった様々な現実から目をそむけることができていただけだった。

そんな中、コロナ禍となりオンラインで仕事をする中で、自分が勉強会に行ったり出張に行ったりしている時のようにオンラインで仕事をしている傍ら、育児や家事などの家庭内の現実を近くで常に目の当たりにし始めた。それによって、そもそも今までのように業務後の勉強会などのための取り組みを家族と調整すること自体も、もっと頻度を下げないといけないと早くから直感的に感じた人が多かったのだろう。家族に負担をかけるという意味では、外に行けたら参加できるけれど、家では参加できないという論理自体は矛盾しているのだが、直感的にそもそも家族への負担を減らさないといけないとか、家族の横で盛り上がるわけにはいかないなどと考えた人達がオンラインだと参加が難しいと答えていたのかもしれない。外に出たところで家庭への負担は根本的に変わらないので、結局のところ、そもそもこの業界で業務後の勉強会やイベント参加、夜の追加のスキルアップのための勉強などが当たり前になりすぎていたのだと、リモートワークを通して随分と認識できたのであった。

実際に夕方以降にオンラインでMTGをしたりイベントをしたりしていると、その間、例えば小さい子供がいる場合は、夕食以降寝かしつけるまでやることが沢山ある。さらに、オンラインで話をしていることに家族が気を使って、これまで以上に子供達に注意をする必要があり、そういった追加の負担やストレスを家族に押し付けているだけに過ぎないのである。

ここで、タイトルにあるように、自分は家族やプライベートを犠牲にして仕事や実績を得ていたことから目を背けていただけで、それを近くで目の当たりにすると、思ってた以上の負担であったり、家族との時間をもっと大切にしたいという思いから、現実を深く認識するようになり、目を背けることができなくなったのである。そういう理由もあって、最初は家でオンラインで仕事をするだけでも家族により負担をかけると思って、安い賃貸を別でかりて仕事をしていた。しかし、妻が妊娠したことや、こういう現実を目の当たりにして、自分が目を背けていただけであったことを反省し、多少高くても家のすぐ隣にあり、いつでも妻や子供が徒歩2、3分で行けるようなマンションに引っ越しして、そこをプライベートオフィスとした。

その上で、できるだけ子どもたちの幼稚園や小学校の活動時間と同じ時間に仕事をするようにし、お昼はシュッと家に帰って妻や家族とご飯を食べたり、少なくとも夕食時間以降には家にいられるようにしたりしている。妻がどこかにでかけたいときには、プライベートオフィスで子供と一緒にいたり、僕自身が家にすぐ帰ることもできるようになった。子供が帰ってくる時間にたまたま妻がいられなくても、子供たちが直接プライベートオフィスに帰ってこれるようにもなった。

部屋が違ったとしても家の中でオンラインで仕事をすることに対する家族の気遣いやストレスも、プライベートオフィスという近いけれど物理的に違う場所で仕事をできるようになり、解決できるようになってきた。子供を叱らないといけない状況とかになっても、妻一人でそういうことをしなくてすむように、できるだけ一緒にいてその辺りお互いやりすぎないように協力できるようにもなってきた。

また、当たり前なのだけど、たまに土日とかは小さな旅行的にプライベートオフィスに一緒にいって家族とゆっくり遊んだりしながら、ご飯や家事は自分が担当するようにしている。家にいると、なんとなく妻がやってくれることに甘えてサボりがちになるのだけど、少なくとも土日にちょっとした遊びがてらプライベートオフィスで寝泊まりすると、自分のオフィスだから自分がやらないといけないなと甘えないようにご飯を作ったり家事ができるのは意外な良い効果でもあった。

今後は、自分が関わるイベントも改めて見直そうと考えている。当たり前にオンラインで勉強会が19時以降にあったりする上に、最近はオンラインという特性もあって数も非常に増えてきている。そもそも、それらを実現するために、エンジニアや運営の皆さんは、プライベートの時間を削ったり、残業に近いレベルで参加されてる人が多いだろう。そもそもこのような取り組み自体、基本的にはオンライン・オフラインに関わらず、家族やプライベートに大きな負担をかけながらなんとか取り組み頂いている話だと理解している。そうであれば、運営や参加者がもっと家族やプライベートの時間も大事にできるようにしていきたいとも思えるようになってきた。

我々の業界では、会社内で業務時間に勉強会を開催したり、業務としてのインプット・アウトプット活動、あるいは、大きなカンファレンスであれば業務として参加することが徐々にできるようになってきている。会社の持続的な成長の観点でも、それらはとても良い取り組みだと思うので、コロナ禍の経験や振り返りを糧にして、こういったオンラインイベントやコミュニティ活動を企業とより密に連携して、例えば、昼の14時や15時あたりから2時間勉強会を実施するとか、そういった方向で業界を巻き込んでいけるんじゃないかと思って動き始めている。そしてこれは、本質的にコロナが収まってオフラインイベントが増えてきたとしても同様だと考えられるため、当然オフラインの勉強会も昼過ぎに開始してもおかしくないと思っている。

ということで、僕自身がコロナ禍の中で初期から今に至るまで、タイトルにあるように家族やプライベートを犠牲にして仕事や実績を得ていたことから目を背けられなくなった件について、ざっと忘れないように書いてみた。これまでは上述してきたような勉強会やスキルの研鑽などを業務外の時間や残業としてやることが当たり前だったのかもしれないけれど、そもそも、これまでの基準や頻度で業務後にあれこれイベントに参加したりすることが正しかったのかは、ちゃんと振り返りながら多角的に議論していきたい。業務時間でやる場合の様々な障壁もあるはずで、それは今後実際に自分が関わるイベントなどでトライして、ブラッシュアップしていきたいと思っている。


編集後記

公開前にこの犠牲という言葉も強すぎると思って何か良い言い方がないかしばらく考えたのですが思い付かず、おそらく指摘も入りそうでその辺り踏まえて考えようと思ったのと、ある意味強い反省の意味も込めて、本文読めばニュアンスがわかるかもしれないと思ってそのままにしたのでした。正直反省してます…

  • 2021/11/12 23:51

ブコメを見ながらタイトルや文章を修正しました。ありがとうございます。

  • 2021/11/13 15:24

ぜひこちらのツイートのスレッドも補足的に読んでいただけると幸いです。

  • 2021/11/13 23:23

僕の文章が酷くて伝えたかったことが変に伝わってしまったケースがあったので、サマリーをツイッターのスレッドでまとめています。ぜひ最後に御覧ください。

コロナ禍で自分の能動的行動が失われて気付いたこと

今週は夏休みなのですが、久々にコロナ禍の1年半を通して自分が色々考えたことをつらつらと書いてみようと思います。

コロナ禍になって最初の半年ぐらい、2020年の夏頃まではある程度これまで通り自分の研究を続けたり、毎日研究のコードを書いたりできていました。もちろん、自分は研究者であるので、ある程度裁量がある一方で、自ら能動的に新しい研究を続けることが仕事でもあるからです。その辺りはもう一つのブログを日誌にしているので今見てもよくわかります。

ところがその辺りから、どうもこれまで当たり前にやり続けてきたことに対して手が動かなくなってきました。これまで、研究をしたりコードを書くことは楽しくて、何のためにと考える前に、国際会議が次々ときたり、国内のアカデミアの活動が波のように押し寄せるので、それに乗り続けられるように自然と行動し続けていました。しかし、コロナ禍でその波がそもそも来なくなってしまいました。まるでぼんやりと座って海岸で海を眺めているような状態です。また、このブログも1年近く更新が滞っていたこともその現状をよく示しています。

今思うとやはり、そういう環境や各種イベント、成果などが、研究やプログラミングのように時には苦しかったり大変な作業を、その後には必ず刺激的でやって良かったと思えることが起こることを体験的に理解しながらやれていたのだと思います。だから、もはやその行動に対する動機などを考えることなくやれていたのでした。今更ながら、自分は環境に生かされており、僕がよくブログなどで書いていう「最終自分のために行動すべき」という点について、真の意味で自分のためには動けていなかったのだと思わされました。

そしてコロナ禍が続く中で、徐々に自分が当たり前のようにやってきた活動の後に生じるはずの「やってよかった」と思える体験が来ないだろうと思えるようになってきたのだと思います。それにより、自分が今はできない難しいことだけれど、できるようになればさらに面白く幸せになる、という体験の連鎖が断ち切られ、これをやったところで自分にとって幸せは来ないかもしれない、心身を削るのがしんどい、と無意識のうちに思うようになったのだと思います。

そうすると、能動的に動こうとする活動が減り、それが減れば減るほどさらに能動的に動こうと思えなくなり、受け身な仕事をするばかりになっていきました。それはそれで、能動的に動ける人のサポートをできて、そこで結果が出れば嬉しいのですが、常に自分はやれていないという気持ちになっていきます。

能動的に動くというのは、少しでも動き出せたらある程度動けるという脳の性質があることは理解しているのですが、その先に何があるかがあまりにも制限され、見通しがないと少しだけでも動くというこもがとても困難であることにも気付きました。全く面白くないゲームを無理にやらないといけないというような感覚に近いかもしれません。

ここ10年以上、能動的に動けないことはほとんどなかったのですが、実はその能動性の正体は、自分の周りの人々や環境、コミュニティ、会社、ひいては社会などと、知らず知らずのうちに相互作用によって生み出されているものであり、決して自分が1人で能動的に動けていたわけではないということでした。コロナ禍で環境からの作用の機会が明らかに減ったことにより、自分がこれまで能動的だと認識してとってきた行動を起こすための相互作用も減り、その結果、能動的行動がとれなくなっていたと理解しました。それを再認識できたのは不幸中の幸いでした。今思うと当たり前のことのようにも思えます。


さて、このようなダークな話で終わらせるのは悲しいので、最近はどうか、それを乗り越えられているかについても述べようと思います。正直、現時点では新しい価値を自ら生み出し続けなければいけない研究者として、まだまだ能動的に動けてはいないです。

一方で、受動的に動くことをきっかけに、非連続的ではありますが少しずつ能動的に動けるようになってきたこともあります。それはどのようにしているかというと、答えは単純で、受動的に仕事を受ける機会を増やしているからです。新卒の時に何かチャンスがあった時に自分でとにかく手を上げていたことを思い出しました。これが初心に返るということなのでしょうか。

例えば、社内外で頼まれる仕事に対して協力したいと思えるのなら、対価を考える前に真剣に協力をするようにしたり、限られた時間の中で、自分の価値を最大限提供できるように真剣に親身になって考えてお話しする、ということを続けています。すると、時には能動的により良いものを生み出したり提供できたりすることもあり、それが少しずつ良い方向に進む中で、刺激が生まれることも増えてきました。

受動的な行動によるきっかけの中で、能動的に自然と動ける機会を増やすこと、それができる環境を構築していくことが、自分にとっての行動のあり方と環境との関係性として、新しく理解できたことでした。それが、かつての能動的行動を生み出します。これまでは、受動的に仕事を受けることばかりは良くないと考えてさえいたのですが、それに対してプロセスの中で能動的に動き、各種タイミングでより良い成果を出し、それを後押しできる環境を同時に作っていき、相互作用を発生させることもまた、必要なことだと理解できたのも良かったです。

例えば、受動的に受けた研究に関わる技術顧問の中で能動的に事業立ち上げに行動し、その中でチームの体制を整えつつも新しい仲間を自分の身近なところから誘って協力することで、少しずつ自分達らしい行動ができる環境や関係性ができ始めています。自分が属する研究所の中でも、コロナ禍やアフターコロナにおいて自分のような状況になる研究者が増えないように、そういった環境に個人が押し上げられ、その個人がさらに環境を底上げできるような体制を新たに設計し始めています。

このように、コロナ禍で今までのような大きな波に自然と乗り続けるようなことはできていませんし、そもそも自分と環境との相互作用の機会が大幅に減っていることは、今のご時世否めません。それもあって新たな価値観や考え方、これまで気づかなかった行動や環境のあり方などを考えさせられる機会になりました。そして何より、自分は周りに生かされているのだと。

そういう意味でも、その環境をあえて自分達で作り上げていくことが多くの活気あるエンジニアや研究者を生み出し、相互作用によって更なる良い環境へと昇華されていくことを思うと、今は受動的な活動のなかで能動的行動を見出している時期ではありますが、そういう環境作りに注力することはこれからの自分のキャリアにとても必要なことなのだと腑に落ちたのでした。

できれば、そういうことを色々な人と、このコロナ禍であるからこそ少しずつ形作っていけると良いなと思います。


編集後記: 2021/08/18 22:15

色々コメントを読みながら改めて考えて、さらに言語化できたことをツイートしていたので、それをメモがてらここにも貼り付けます。

https://twitter.com/matsumotory/status/1427975605312659459 のスレッド

やりたい事は、やる事によって生じる環境からの相互作用も込みで構成されているわけで、その環境の要素がコロナ禍で急激に変わってしまったためにやりたい事もやれなくなってしまった、という話であって、コロナを言い訳にできるからやらない、という怠惰への正当化とは違う。やりたいんですよ。

でもそれがあまりに急激な変化であったことと、自分のやりたいことが環境からの相互作用も含めてやりたいと思えていたことに気づかないと、コロナ禍になってなぜかずっとやれてたり、やりたいことができなくなって漠然と苦しい、みたいな感情に落ちるのだと思う。自分がやるというのは、やりたいことややってきたことの構成の一部に過ぎない。

自分のやりたいことややってきたことが、自分の行動と環境からの相互作用で構成されている、と理解できれば、大きく変わったのは環境であるので、そこをこれまでの状態に戻すようなことを考えるのが良さそうだ、というように言語化できただけでも、久々にブログに書いて良かった。

あるいは、今の環境を前提に、その環境からの相互作用と自分の行動の中で改めてやりたいことややっていくことを再定義していくと、これまでの自分とのギャップに苦しんだりすることは減るかもしれない。いやまあ現状は急激な環境変化過ぎてその適応が難しいというのが課題なのだけどね。

コロナ禍でも特に変わらないという話も聞くことがあって、それはこの論理でいくと、自分のやりたい、やってきたことの環境からの相互作用が、コロナ禍での環境からのものと差分の少ない環境だったからと理解することができて、それは当然あると思うので、この手の話の論点はその辺りなのかなと思えた。

プロセスのオーナ情報をTCPオプションヘッダに書き込むに至った背景とアプローチの補足

hb.matsumoto-r.jp

上記のリンクの昨日書いた記事のスコープや前提、及び、ユースケースがわかりにくかったので、以下にそれらをもう少し詳細に書こうと思います。コメントやアドバイスをすでに頂いた方はありがとうございます。

まず、この手法にいたった課題について説明してきます。 これまでWebホスティングサービス(レンタルサーバ)のように、WordPressのようなWebアプリケーションを配置するための領域(一般ユーザで利用するテナント)を貸し出すようなプラットフォームサービスにおいて、低価格化を実現するために単一のサーバにどれだけ高集積にテナントを収容するかという検討がなされてきました。

そんな中、テナント単位でプロセスを用意したり、IPアドレスをはじめとした個別リソースの紐付けを極力行わずに、共有のデータベースミドルウェアを使い、できるだけリソースを共有するような方式、例えばApacheのVirtualHostなどが採用されてきました。これによって、メモリ32GBぐらいのサーバに数万から10万ホストぐらいまで収容できるようになりました。プライベートIPアドレスや静的な情報を各テナントに紐付けると、10万ホスト収容の場合はそれ相当の数の静的な設定が必要になり、例えばApacheのVirtualHostの場合だと、それだけで4GBぐらいのworkerプレセスになります。そうなると、clone()に1秒ぐらいかかったりと大幅にCGIなどの性能低下やリソース逼迫が生じることとなり、そこも動的な設定にするなどの提案がなされてきました。

一方、そのようにリソースを共有するような方式をとると、プロセスの権限をテナントへのリクエストに応じていかに適切かつ動的に分離しつつ、Webアプリケーションの実行性能を落とさないかという点が課題になります。そこで、スレッド単位で権限を分離する方式などいくつか提案されてきました。しかし、そのような状況でも、Webサーバが持つ脆弱性やシステムにおける権限分離の穴などによって、特定のテナントから別のテナントのファイルを読み込めたりする問題がこれまでに何度も繰り返されてきました。

例えば、数年前にあったのは、とあるテナントのWebアプリが脆弱性なのによってのっとられたとします。その上で、そのテナントからシンボリックリンクを読みたい別のテナントのスクリプトファイルに対して.htmlなどに張り替え、Webサーバからは単に静的ファイルとして表示させるようにし、Webサーバのシンボリックリンクチェックがシステムコールレベルでアトミックに行われないこととマルチプロセス(スレッド)であることを利用して、特定テナントから何度か試行することでファイルを覗き見るような所謂TOCTOUな手法によってファイルを覗き見されるようなことがありました。また、ホスティング環境で提供しているライブラリやApacheにロードしているモジュールの脆弱性などによって、意図せず同一OS上でread権限を持つ複数のファイルの閲覧ができる問題などがありました。そして、その漏れた情報がどこかのサイトで共有され、とあるタイミングで特定のテナントからデータを抜かれるといった事象もありました。現在ではそれらを解決するための権限分離手法を沢山提案・実装してきたので、随分と強固になってきたと思いますが、依然として日々様々なインシデントが生じています。

各テナントはサービス利用者に一般ユーサで利用できる領域を貸し出しており、そこで公開されていWebアプリケーションをプラットフォームサービス事業者としてメンテするわけにはいかず、各テナントには脆弱性があり、そこが踏み台にされる前提で、そうなってもセキュリティ上の問題が起きないようなプラットフォーム基盤を構築する必要があります。また、WordPressをはじめとした、スクリプトファイルにかかれているID/PASSを抜こうという攻撃が特に多く、踏み台となったテナントから各テナントのID/PASSの認証情報が仮に漏れたとしても、それぞれのテナントを乗っ取らない限りにおいてはその認証情報を使えないようにしたいという多層防御の仕組みを検討していました。

そこで要件をまとめると、

  • テナントが収容されるサーバがフロントにあり、テナントから利用する共有データベースが後ろのプライベートネットワークで繋がっているようなホスティングシステム構成において
  • 接続元のIPアドレスが共有され、各テナントのWebアプリケーションが利用するデータベースミドルウェアを共有し、OSの各種リソースを共有するような低価格で提供される高集積マルチテナント環境でも
  • 仮に各テナントの各種スクリプトが利用するデータベースのID/PASSのような認証情報がシステムの脆弱性によって漏れたとしても
  • 収容サーバの特定のプロセスからのデータベースアクセスでない限りID/PASS認証を通さず
  • 収容サーバの接続元プロセスがユーザランドで正しい接続元プロセスかのように詐称できない(自身を証明する識別子を扱わせない)
  • IPアドレスベースよりもより粒度の細かいプロセスベースでの接続元プロセスの検証方式

を考えた結果、昨日に書いた記事のアプローチに繋がりました。

また、実装についてもeBPFを検討していたのですが、TCPオプションヘッダをいじるようなeBPFの使い方が以下の論文ぐらいで、これをためそうとしたところ論文に記載されているフックポイントなどはまだ見当たらず、論文がその提案であるということろで止まっており、今の所公開されているLinuxでは実現ができなさそう?に思ったのでカーネルモジュールで実装しています。

もしここについても、eBPFでこんなふうにできるよ、というのがあればご教示いただけると幸いです。



新たにカーネルでTCPオプションヘッダに書き込んだ情報をTCPセッション確立時にユーザランドでどう取得すべきか

追記:2020-06-05

このエントリの背景が雑だったので以下に補足記事を書きました。先にこちらに目を通していただいた方が良いかもしれません。

https://hb.matsumoto-r.jp/entry/2020/06/05/110709


speakerdeck.com

今tcprivというソフトウェアを開発しているのだが、細かい内容については上記のスライドを見てもらうとして、やりたいことは、TCPセッションを確立するプロセスのオーナ情報を接続先のプロセスで透過的に検証するという処理である。

github.com

以下ではその実装の概要を紹介しつつ、今検討していることについてお話したい。

接続元プロセスは一般ユーザを想定しており、脆弱性などによって悪意のあるユーザにのっとられることもありうるし、とあるプロセスが利用する認証情報も漏れることがあることを想定している。 しかし、情報が漏れたとしても、適切なオーナからとそのオーナに紐付いた認証情報を利用しないと、リモート先のプロセスで認証を拒否出来るようなしくみを考えている。 いわゆる多要素認証のサーバ間通信版と考えてもよいのかもしれない。

例えば、とあるマルチテナントシステム間連携において、とあるプロセスが利用しているDBのID/PASS、あるいは、トークンが漏れた時に、それらの認証情報を使って、同システム内の別のオーナのプロセスがDBに接続を試みた場合、オーナが違うのでID/PASSやトークンが一致していても認証を通さない。 マルチテナント型のマネージドシステム内で、プロセスレベルで隔離はされているがOS上に共存しているプロセスや、同じIPアドレスが割り当てられているコンテナなどを接続元として想像すると良いかもしれない。 あとはレンタルサーバの例えばWordPress的なWebアプリケーションとか。 オーナや権限で分離されている沢山のWordPressにおいて、たとえはとあるWordPressのDB接続のためのID/PASSが漏れたとしても、そのID/PASSを使うべきプロセスオーナからのみしかそのID/PASSの認証を接続先のDBで認証しない、というようなケースである。

また、接続時にオーナの情報を詐称できないように、その一連のオーナに関する情報をカーネル側で透過的に実現している。 すなわち、認証情報がもれた当該プロセスからのみのアクセスや脆弱性で乗ったられたプロセスからのみしか、認証情報を利用して接続できなくなるので、被害範囲を局所化できる。

オーナ情報の付与を透過的にカーネルで実現するために、カーネル内のNetfilterのフックポイントを利用して、カーネルモジュールによりカーネルのTCPスタック前後でTCPオプションヘッダにオーナ情報を書き込む領域を定義し、書き込んでいる。

static int __init tcpriv_init(void)
{
  printk(KERN_INFO TCPRIV_INFO "open\n");
  printk(KERN_INFO TCPRIV_INFO "An Access Control Architecture Separating Privilege Transparently via TCP Connection "
                               "Based on Process Information\n");

  nfho_in.hook = hook_local_in_func;
  nfho_in.hooknum = NF_INET_LOCAL_IN;
  nfho_in.pf = PF_INET;
  nfho_in.priority = NF_IP_PRI_FIRST;

  nf_register_net_hook(&init_net, &nfho_in);

  nfho_out.hook = hook_local_out_func;
  nfho_out.hooknum = NF_INET_LOCAL_OUT;
  nfho_out.pf = PF_INET;
  nfho_out.priority = NF_IP_PRI_FIRST;

  nf_register_net_hook(&init_net, &nfho_out);

  return 0;
}

こんな感じで、ローカルに入ってきたパケットと出ていくパケットをフックしている。

出ていくフェーズ、すなわち、リモートサーバにTCPで接続しにいくような状況では、synパケット送出時にオプションのチェックとtcprivオプションのセット(tcpriv_tcp_syn_options)、さらに、定義した実験的TCPオプションフィールドにオーナ情報を書き込んでいる(tcpriv_tcp_options_write)。

static unsigned int hook_local_out_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iphdr = ip_hdr(skb);
  struct tcphdr *tcphdr = tcp_hdr(skb);

  if (iphdr->version == 4) {
    if (iphdr->protocol == IPPROTO_TCP && tcphdr->syn) {
      struct tcp_out_options opts;
      struct sock *sk;
      struct tcp_md5sig_key *md5;

      printk(KERN_INFO TCPRIV_INFO "found local out TCP syn packet from %pI4.\n", &iphdr->saddr);

      sk = state->sk;
      memset(&opts, 0, sizeof(opts));
      tcpriv_tcp_syn_options(sk, skb, &opts, &md5);
      tcpriv_tcp_options_write((__be32 *)(tcphdr + 1), NULL, &opts);
    }
  }

  return NF_ACCEPT;
}

現在、IANAが規定しているTCPオプションヘッダは複数存在し,同時に,HOST_IDやLinuxカーネルバージョン5系で実装されているShared Memory communications over RMDA protocol(以降SMC-R)*1といった実験的なTCPオプションも存在する。 例えばTCP Fast Open Coolieは2014年に標準化され、IANAから正式なTCPオプションとしてkindナンバーを付与されている。 一方で、比較的新しいTCPオプションであるため、依然としてLinuxカーネルのバージョンによっては、実験的なkindナンバーを共有して他の実験的なTCPオプションと利用する実装になっている*2。 そのため、実装においては、TCP Fast Open Cookieが固有のkindナンバーを保つ場合と共有の実験的なkindナンバーを持つ場合を想定して実装を行っている。

また、Linuxカーネルバージョン5系においては、共有の実験的なkindナンバーにSMC-Rを利用しているため、IANAからSMC-Rに割り当てられているTCP Experimental Option Experiment Identifiers(以降TCP ExIDs)*3とは別のTCP ExIDsを暫定で割り振って区別する。

特にパケットのTCPヘッダオプションフィールドの解析を行う際に、既存のTCPオプションはそのままに、提案手法用の実験的オプションの共有kindナンバー、データレングス、TCP ExIDs、キー情報を書き込む。 実験的オプションの仕様を忘れていると、ExIDsの定義を忘れちゃうので注意。

書き込み処理は以下のような感じ。

static void tcpriv_options_write(__be32 *ptr, u16 *options)
{
  if (unlikely(OPTION_TCPRIV & *options)) {
    kuid_t uid = current_uid();
    kgid_t gid = current_gid();

    *ptr++ = htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16) | (TCPOPT_EXP << 8) | (TCPOLEN_EXP_TCPRIV_BASE));
    *ptr++ = htonl(TCPOPT_TCPRIV_MAGIC);

    /* TODO; write tcpriv information: allocate 32bit (unsinged int) for owner/uid area */
    *ptr++ = htonl(uid.val);
    *ptr++ = htonl(gid.val);
  }
}

static void tcpriv_tcp_options_write(__be32 *ptr, struct tcp_sock *tp, struct tcp_out_options *opts)
{
  u16 options = opts->options; /* mungable copy */

  if (unlikely(OPTION_MD5 & options)) {
    *ptr++;
    ptr += 4;
  }

  if (unlikely(opts->mss)) {
    *ptr++;
  }

  if (likely(OPTION_TS & options)) {
    if (unlikely(OPTION_SACK_ADVERTISE & options)) {
      *ptr++;
      options &= ~OPTION_SACK_ADVERTISE;
    } else {
      *ptr++;
    }
    *ptr++;
    *ptr++;
  }

  if (unlikely(OPTION_SACK_ADVERTISE & options)) {
    *ptr++;
  }

  if (unlikely(OPTION_WSCALE & options)) {
    *ptr++;
  }

  if (unlikely(opts->num_sack_blocks)) {
    int this_sack;

    *ptr++;

    for (this_sack = 0; this_sack < opts->num_sack_blocks; ++this_sack) {
      *ptr++;
      *ptr++;
    }
  }

  if (unlikely(OPTION_FAST_OPEN_COOKIE & options)) {
    struct tcp_fastopen_cookie *foc = opts->fastopen_cookie;
    u8 *p = (u8 *)ptr;
    u32 len; /* Fast Open option length */

    if (foc->exp) {
      len = TCPOLEN_EXP_FASTOPEN_BASE + foc->len;
      p += TCPOLEN_EXP_FASTOPEN_BASE;
    } else {
      len = TCPOLEN_FASTOPEN_BASE + foc->len;
      *p++;
      *p++ = len;
    }

    ptr += (len + 3) >> 2;
  }

  tcpriv_options_write(ptr, &options);
}

続いて、入ってくるフェーズでは、syncパケット受信時にTCPオプションヘッダフィールドをparseし、オプションが定義されていれば当該フィールドからオーナ情報(uid32bit+gid32bit)を取得するようにしている(tcpriv_tcp_parse_options)。

static unsigned int hook_local_in_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iphdr = ip_hdr(skb);
  struct tcphdr *tcphdr = tcp_hdr(skb);
  struct tcp_options_received tmp_opt;

  if (iphdr->version == 4) {
    if (iphdr->protocol == IPPROTO_TCP && tcphdr->syn) {
      printk(KERN_INFO TCPRIV_INFO "found local in TCP syn packet from %pI4.\n", &iphdr->saddr);

      /* parse tcp options and store tmp_opt buffer */
      memset(&tmp_opt, 0, sizeof(tmp_opt));
      tcpriv_tcp_clear_options(&tmp_opt);
      tcpriv_tcp_parse_options(&init_net, skb, &tmp_opt, 0, NULL);
    }
  }

  return NF_ACCEPT;
}

parseについては、カーネルのコードを存分に参考にしながら、ひとつずつオプションフィールドのフラグとレングスのチェックを行って、tcprivオプションがああればその情報を取得するようにしている。

/* TCP parse tcpriv option functions */
static void tcpriv_parse_options(const struct tcphdr *th, struct tcp_options_received *opt_rx, const unsigned char *ptr,
                                 int opsize)
{
  if (th->syn && !(opsize & 1) && opsize >= TCPOLEN_EXP_TCPRIV_BASE && get_unaligned_be32(ptr) == TCPOPT_TCPRIV_MAGIC) {
    /* TODO: check tcpriv information */
    u32 uid, gid;
    uid = get_unaligned_be32(ptr + 4);
    gid = get_unaligned_be32(ptr + 8);
    printk(KERN_INFO TCPRIV_INFO "found client process info: uid=%u gid=%u\n", uid, gid);
  }
}

/* ref: https://elixir.bootlin.com/linux/latest/source/net/ipv4/tcp_input.c#L3839 */
void tcpriv_tcp_parse_options(const struct net *net, const struct sk_buff *skb, struct tcp_options_received *opt_rx,
                              int estab, struct tcp_fastopen_cookie *foc)
{
  const unsigned char *ptr;
  const struct tcphdr *th = tcp_hdr(skb);
  int length = (th->doff * 4) - sizeof(struct tcphdr);

  ptr = (const unsigned char *)(th + 1);
  opt_rx->saw_tstamp = 0;

  while (length > 0) {
    int opcode = *ptr++;
    int opsize;

    switch (opcode) {
    case TCPOPT_EOL:
      return;
    case TCPOPT_NOP: /* Ref: RFC 793 section 3.1 */
      length--;
      continue;
    default:
      if (length < 2)
        return;
      opsize = *ptr++;
      if (opsize < 2) /* "silly options" */
        return;
      if (opsize > length)
        return; /* don't parse partial options */
      switch (opcode) {

      case TCPOPT_EXP:
        /* Fast Open or SMC option shares code 254 using a 16 bits magic number. */
        if (opsize >= TCPOLEN_EXP_FASTOPEN_BASE && get_unaligned_be16(ptr) == TCPOPT_FASTOPEN_MAGIC) {
          // do nothing
        } else if (th->syn && !(opsize & 1) && opsize >= TCPOLEN_EXP_SMC_BASE &&
                   get_unaligned_be16(ptr) == TCPOPT_SMC_MAGIC) {
          // do nothing
        } else {
          tcpriv_parse_options(th, opt_rx, ptr, opsize);
        }

        break;
      }
      ptr += opsize - 2;
      length -= opsize;
    }
  }
}

これでめでたしめでたし、リモート先のプロセス側で接続元プロセスのオーナ情報を取得できるわけであるが、さてここからユーザランドで動作するミドルウェア等でこのデータをどのように取得するかを考える必要がある。

現状では、以下の図にあるように、socket APIを使うあるいは同様のAPIを実装することによって、tcprivオプションが有効化どうかをチェックする関数を用意する。

f:id:matsumoto_r:20200604144054p:plain

それを用いて、ミドルウェア、あるいは、その前端においたミドルウェア対応のプロキシで、セッションを確立する際にその関数でtcprivオプションをチェックする。 その上で、tcprivオプションがenabledであれば/procファイルシステムの指定の場所に、tcpriv/ipaddress+src-portみたいなファイルを見ると、接続元のオーナ情報が取得できるようにしておき、それを行える関数を用意する。 そのためにも、tcprivを実装しているカーネルモジュールの中で、オーナ情報をparseした後に、その情報を/proc以下に書き込むようにしておく。 すると、TCPセッション確立時に接続元プロセスのオーナ情報を取得できるので、その段階、あるいはミドルウェアとしての認証を行う段階で、そのオーナ情報を従来のID/PASSやトークンを突き合わせることで認証行う。


とまあ、ここまでが頭の中で大体できそうだと考えている設計なのだが、もっとシンプルにオーナ情報をユーザランドでセッション確立時に取得できる方法はないかと最近考えている。 scoket APIを作ったり拡張したりすることはできるが、もう少し、うまく/procにtcprivに関する情報を配置するだけで、セッション確立時にあわよくば関数を使わなくてもreadとかだけで扱えるような設計はないか検討している。

もしアイデアがありましたら、ご教示いただけると幸いでございます。

*1:RFC-7609 IBM's Shared Memory Communications over RDMA (SMC-R) Protocol, https://tools.ietf.org/html/rfc7609

*2:RFC-6994 Shared Use of Experimental TCP Options, https://tools.ietf.org/html/rfc6994

*3:TCP Experimental Option Experiment Identifiers (TCP ExIDs), https://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml#tcp-exids