フルタイムでやる仕事を作る #wantedlydev

先日 Wantedly さんのエンジニアリングマネージャー座談会に出演させていただいた。

wantedly.connpass.com

テーマは、「エンジニアリングマネージャーの課題を相談したい人が多い」「その相談パブリックにしよう」なので、自分が最近課題に思っている「変化の速度感」についてざっくばらんに会話できたらなーというのが期待だった。

イベント中には、大きく 4 つの話をしたのかな。それぞれ会話の中では話しきれなかったことも補足しつつ書いていく。

  • 技術スタックが違うチーム
  • プロダクトと専門組織のバランス
  • 専門組織を立ち上げるポイント
  • 採用と oss-guild

技術スタックが違うチーム

リンク先を見て貰うと顕著に分かると思うけど、はてなでは、そこそこバラバラな技術スタックを使っている。

hatenacorp.jp

インフラは AWSGoogle Cloud (オンプレはやっと撲滅した)。コンテナ運用基盤も ECS、k8s それぞれある。バックエンドでは Scala、Go、PerlPython、Node。Web フロントエンドは TypeScript/React/GraphQL に統一しつつあるが、Apollo と Relay を敢えて使い分けていたりする。(基本的には Apollo Client だけど、遊べる余地のある小さなプロダクトでは Relay を使おうみたいな)

1 プロダクトの中でも複数言語が使われていて、はてなで Web アプリケーションエンジニアをやるならマルチリンガルであることは求めていくことになる。

こうなっている理由は、事業のために最適な技術を選んだ結果なんだけど、これを成立させるためにどうしているかだと、1 プロダクトの中では専門性が足りなくても、会社全体で見ると統制が取れている、にしているつもり。そのために技術選定時に DesignDoc を書いているし、サブ会 があるし、はてな教科書 がある。

developer.hatenastaff.com

全社で 1 プロダクトでしか使っていない技術はあまり存在しなくて、横のつながりで同期を取ったり、専門性を高めたり、入門のハードルを下げたりしている。

プロダクトと専門組織のバランス

「横のつながりで同期を取る」と言ったが、本当にそんなことが可能なのか、という話題。10 年以上、技術イベントで登壇できる程度の位置は維持できているので「可能である」と思っています。

ここの肝は情報の透明性だろう。同じインプットを入れると同じ結論が出てくるので。どのチームがいつ何をやるか、どんな課題があるかを知っている状態である。毎週やっている 技術勉強会 や、スタッフが発信している社内ブログ、個人/社の技術ブログ。また、サブ会の会合を通して、技術選択や、技術ロードマップの優先度、スケジュール感の共有が行われている。

情報の発信が上手い人、情報の pull が上手い人、情報のハブになるのが上手い人がそれぞれいるので回っているんじゃないかと思う。リモートシフトで少しサイロ化が進んでいるので、すべてのベースが情報共有にあることを強く認識して、是正していきたい。

プロダクト側が、会社/世間の標準から外れすぎないよう意識して行動できているチームだからというのもあるかもしれない。

全体で緩やかな意思決定をしていると、緩やかなので、意図した変化速度にならないことが多い、というのがこの体制での悩み。その気になったら3ヶ月〜半年でグイッとやれないこともないが、やれる範囲でやると1〜2年かかってしまう、という速度感です。借金を返しきることがないが、返済に困り果てもしないぐらいで生きて行けているんじゃないか。

専門組織を立ち上げるポイント

サブ会のような、プロダクト側 90%、残りの 10% で参加する、という形だと、速度感が足りないし、専門性も高めきれない。サブ会の主軸のメンバーに対しては調整して 50% ぐらい持って貰うようにしてきたが、それにも限界がある。本当に力を入れたいならハコ (組織) を作ることになる。

が、かろうじて回ってしまっている中で、どう組織を立ち上げるか、というのが僕の悩みだったが、@kawasy がサクッと答えてくれたのが表題の言葉。

片手間では解ききれない量・質の問題がそこにあることを示し、その上でフルタイムで取り組む範囲を定義して、これで何が解決するのかを言語化することで、組織としてやる価値を示すのだろう。「フルタイムでやる仕事を作る」のが僕の仕事だなーと感じた。

採用と oss-guild

こういう緩やかな繋がりで「各位いい感じに」で回せる人をどう採用するかという話。これは OSS と地続きであることを是とする文化であれば採用も育成も実現できるんじゃないかと思っている。

前職のときに書いた内容だけど、大枠で言うとこういう意思です。

これをやっている人を採用するのだと書類選考から見ているし、

育成も含めてやっていくぞというのは社内の文化醸成として頑張っているところです。

tarao.hatenablog.com

まとめ

楽しい会だった。呼んでいただいてありがとうございます!

【PR】はてなWantedly さんも、積極的に採用しているのでお声掛けください!

d.hatena.ne.jp (キャリア採用 中途の転職・求人 - 採用情報 - 株式会社はてな や、Twitter 等で直接お声掛けください><)

www.wantedly.com

第 4 回のゲストは id:Songmu さんです。楽しみですね。 wantedly.connpass.com

チームに浸透させるのが近年では難しくなっている

昨日「動いたけどチームメンバーを説得するのが面倒で、秘蔵のブランチになってしまう」って言ったけど、この気持ちはどこから出てくるのか。

分かりやすい Cons があると、反発が予想できて、その反発を解決するところまで労力を割くほどの気持ちが無いので困る。「直ちに問題になるわけじゃないが、どちらかというとやった方がいい、でもリスクもある」という選択肢を選べずにズルズルと現状維持に向かう圧力は、ある。チームの同質性が高いうちはほとんど困らないんだが、人数が増えたり、別の職種が増えたりするごとに「面倒」さはどうしても増していく。

我々の信念として以下を持ってはいるが、現状維持に向かう圧力がある中で変化を加えるのはそこそこ労力が要り、閾値を超えると変化が発生しなくなってしまう。

業務・開発フローは「変えることは無条件に正しい」くらいに思って良いと思っています。 素早く変えてもし仮にダメだったら素早く戻せばよい。

Mackerel開発チームカイゼンの旅(振り返り・モブプロ・スキルマップなど) - Hatena Developer Blog

現代の Web アプリケーションを作るのに、「PdM、エンジニア、デザイナーの 3 人で作る」というのは難しくなっていて、そもそも 6 人とか 10 人とか 30 人とかのチームでやる場合が増えています。そうすると、変化を始めるのは勢いだけでも可能かもしれないけど、やりきるのは以前より難しくなっていませんか。

この問題を倒すのに、進め方を知っていると楽になるよね、というのが今日の主題です。

進め方 is 問題解決や意志決定、組織変革のプロセス。

例えば

  • トヨタ生産方式の 5S
    • 躾 が含まれている
  • 問題解決の 8 ステップ
    • 目標設定や計画、定着させることが含まれている
  • ADR (Architecture Decision Records) や Design Doc
    • 意志決定をテンプレ化し、記録を取っていく
  • インセプションデッキ
    • 前提を丁寧に揃える役に立つ
  • DACI フレームワーク
    • 意志決定に関わる人の役割を明示することで推進力を出す
  • コッターの 8 段階プロセス
    • 変革に当たっての 8 段階プロセス
  • ADKAR モデル
    • 同じく変革のための 5 段階プロセス

辺りを僕は意識しながらやっているかしら。

勢いだけでは導入することができない場合があり、そういうときはちゃんと変革をマネジメントしなければいけない、という意識で物事を進めていくと、導入できない・最後までやりきれない問題を倒しやすくなる。前提を丁寧に共有しようねとか、共有用のいいテンプレートがあるよとか、やりきるところまで含めて計画しようねとか。

横から細かい Cons を刺され続けて疲弊する場合に対しても、DACI フレームワークを使うと、「Informed ですよね?」と封じ込めることができて (というか、すべてに対応する必要は無い、Approver が判断できれば十分である、という意識に全員を持って行くことができて)、進めやすくなることもあるかもしれません。

定着までをゴールに見据えていると、外部登壇を経由して社内により効率的に広めるとか、意識を向けるための定例の場の設定とか、横展開のためにあらかじめ楔を打っておくとか、そういう動きをやっていくこともあります。

開発合宿のようなイベントを企画して、勢い MAX で乗り越えるという手段を執ることもありますね。

この話はどこに向かっているのか

  • 変化が大変になっているという事実があることを認識する
  • 進め方が世の中にあることを認識する
    • 試しに使ってみて、馴染むものを普段使いする

というのをやっていけると、変化を起こしたり、浸透させたりするのが難しい問題に対応しやすくなる。

んだけど、その上で「丁寧に進めなければいけないことは分かっているがめんどくさい」は乗り越えられないので、こういうエンジニアのエンパワーメントを一緒にやってくれる人や、丁寧に進めなくても良いチームを一緒に作っていく人を強く募集しています。

まずはカジュアル面談からいかがでしょうか。

meety.net

branch を寝かせるときは TODO.txt を置いている

タイトルがすべて。

思いついたら手を動かしてしまう性質なので、秘蔵のブランチを大量に持っている。

秘蔵のブランチというのは、動いたけどチームメンバーを説得するのが面倒とか、テスト書くのが面倒とか、だいたい動いているけどやりきるのが面倒とかで main にマージしていないヤツ。 例えばフレームワークのメジャーバージョン上げるブランチとか、依存ライブラリをより一般的/現代的なものに交換するブランチや、より良い設計を思いついたのでガッと書き換えてしまうブランチが多いかな。

だいたい 1 年所属していると 80 ブランチぐらい溜まるので、週 1 つ以上は何か作りかけてる計算になる。

もちろん自分でいいアイディアだと思っているから実装しているので、何か起きたときに「こんなこともあろうかと」と出せるのが強み。 脆弱性が見つかったときとか、データ量が増えたこと等で設計の悪さが発火したときとかに、スッと懐から出して、爆速で修正リリースできることがある。 何かしら問題が起きた後だと、多少の体裁の整ってなさは些細な問題になるので、エイってやりやすい。

秘蔵のブランチというとカッコイイ(?)んだけど、要は仕掛かり品です。在庫。完成させていないのでリリースできない、JIT 生産方式で嫌われるヤツ。稀に繰り出せると最高に気持ちがいいので、ついやってしまうんだよね。。

で、一晩グワーッて書いた後に、完成していないと数日〜数ヶ月寝かせることがあって、そういうときは続きは何をやろうとしていたのか完全に忘れているので、最後に TODO.txt を commit しておくようにしている。

例えば

  • 本家が対応していないので issue #xxx が解消するまで待ち
  • xxx の場合にエラーになるので対策する必要があるんだが、そういう仕組みがまだ存在していない
  • model の a-h 始まりまで終わったので、残りの 200 ファイルやる
  • 先に Route53 に移管する必要があった……。
  • xxx の場合の動作確認まだしていない
  • 全員のローカル環境を更新させるのが面倒

とかとか、完成させていない理由を書いてある。*1

TODO を書いておくことによって、次にブランチ一覧を眺めたときに「これ何だっけ」というのが減って、1 年後でも続きの作業ができるというのがオススメポイントです。異動/退職するときも、あと何をしたらいいのかが書いてあるので、続きを人にやってもらいやすい。

ブランチに限らず、あまりヨシヨシできていないリポジトリは TODO.txt を書いて commit しておくと、次に作業をするときに当時の記憶が蘇りやすい。僕はよく TODO.txt を読んで脳のワーキングメモリに乗せた後に git reset HEAD~1 して作業開始しています。

*1:1 行で済むことは滅多に無いです!それぐらいの障害物なら完成まで持って行ける

体制を考えるときに意識していること

1on1 で伝えたので外にも書いておく。

プロダクトやチーム、メンバーのフェーズ

まず現状分析。

  • 自プロダクトは PPM で言う花形、金のなる木、問題児、負け犬のいずれに当たるのか
    • 勢い MAX でめっちゃ盛り上げるのか、地味に役割を達成するのか。自チーム全集中なのか他チームのフォローに回るのかみたいな方針が変わる
  • 自チームは エラスティックリーダーシップ で言うサバイバルモード、学習モード、自己組織化モードのいずれに当たるのか
    • チームを改善しなければいけないのか、プロダクトだけを見ていて良いのか。チームで改善できるのか、リーダーや外部の強い意志が必要なのか
  • 各メンバーは、期待される役割において SL理論 で言うとどのフェーズなのか
    • 指示的行動が必要だとマイクロマネジメントすることになり、マネージャ/メンター的な人/行動を増やす必要がある

役割を網羅しているか

こういう軸で考えていることが多い。

  • 体制図上の役割
    • サービス開発に必要な役割を (チーム規模によっては兼務アリで) 網羅しているか
    • PdM、PjM、TL、etc
  • 問題発見、課題化、遂行
    • 問題を見つけられる人、タスクに落とし込める人、タスクを遂行する人はそれぞれいるか
  • 専門家、ジェネラリスト、ムードメーカー、気が利く人
    • 専門家:深く掘らないと解決できない問題を、解決まで持って行ける人
    • ジェネラリスト:連携しないと解決できない問題を、人と人を繋げたり、両方の仕事をやったりして、解決まで持って行ける人
    • ムードメーカー:ハードな状況でもチームをポジティブなムードに持って行ける人。提案に対して第一声が「面白そう」な人
    • 気が利く人:なんか漏れてるものを拾ってくれたり、ムラがあるので解消しませんか or 私がやりますって言い出したりする人
  • EM の四象

コミュニケーションパス

動きが変わるってことはつまり話す相手や内容が変わっているってことなので、その変化を設計する。

  • 体制図上の役割に則った定例設計
    • 会議体だったり、ただのコミュニケーションの場だったり
  • 体制図上の役割以外のコミュニケーション設計
    • 体制図上の役割を持たない人でも何らかの役割は持っているので、その役割を最大限に発揮できるようなコミュニケーションパス(「この人同士が話しておくと全体で上手くいきそう」という目論見)を考えておく
    • 明確に期待を伝えることもあるし、例えばペアで作業に当たるようにタスクを差配して、雑談から何らかの同期が発生するのを期待することもある
  • 各メンバーに対するバックアップ設計
    • SPoF になっていると退職・異動・病気等に弱いので、バックアップとして誰が入れるかを考えたり、バックアップを作っていけるようなコミュニケーション設計をしたり
    • 既にスキルを持っている人がフォローに入る場合と、サブを育成していく場合とがある

期末は来期体制を考えるためにひたすら丸と四角と線を書いて、「この人に TL を担って貰いたいが、少しチャレンジングなのでフォロー体制や壁打ち相手を用意しておかないと」とか「インフラ強めの人が居ないので他チームからフォローできる体制を作っておかないと、ということはこのチームに目標と余裕が必要」とか「変化を言い出す人が少ないので、世間とのギャップを認識しやすいプラクティスを導入して育成しよう」とか「来期にフロントエンド刷新を狙うために、今からフロントエンドチームを作って、早いうちにタックマンモデルで言う形成期、混乱期を乗り越えておきたい」とかをパズルしています。

観測して、状況を整理して、ハコとコミュニケーションパスを設計する、を繰り返すことで前に進む力を作っているんだと思う。キーマンがいっぱい居たらパズルが楽なので、いいチームにしたい。

いつ考えるのか

期初のチームのキックオフで提示できればチームの自走力が高まるので、そこに合わせて。

あとは 1on1 や四半期面談、半期面談のタイミングで現状認識を更新しておけば、なんか風呂場とかで思いつくはずという設定で暮らしています。細かいコミュニケーションの問題は、チームのイテレーションのふりかえりで拾えるので、設計するのは半期ごとにやっています。

寿限無とピカソ

# https://ja.wikipedia.org/wiki/%E5%AF%BF%E9%99%90%E7%84%A1
jugem = <<EOS.gsub(/(?<=[^])(|)/, "\\1\n").gsub(/|/, "\n").gsub(/\n+/m, "\n").lines(chomp: true)
寿限無、寿限無、
五劫の擦り切れ、
海砂利水魚の、
水行末・雲来末・風来末、
喰う寝る処に住む処、
藪ら柑子の藪柑子、
パイポ・パイポ・パイポのシューリンガン、
シューリンガンのグーリンダイ、
グーリンダイのポンポコピーのポンポコナの、
長久命の長助
EOS

# https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%96%E3%83%AD%E3%83%BB%E3%83%94%E3%82%AB%E3%82%BD
picasso = <<EOS.chomp.split("")
パブロ・ディエゴ・ホセ・フランシスコ・デ・パウラ・ホアン・ネポムセーノ・マリア・デ・ロス・レメディオス・クリスピーン・クリスピアーノ・デ・ラ・サンティシマ・トリニダード・ルイス・イ・ピカソ
EOS

puts jugem.zip(picasso).flatten.compact.join("")

寿限無・パブロ・寿限無・ディエゴ・五劫の擦り切れ・ホセ・海砂利水魚の・フランシスコ・水行末・デ・雲来末・パウラ・風来末・ホアン・喰う寝る処に・ネポムセーノ・住む処・マリア・藪ら柑子の・デ・藪柑子・ロス・パイポ・レメディオスパイポ・クリスピーン・パイポの・クリスピアーノシューリンガン・デ・シューリンガンの・ラ・グーリンダイ・サンティシマ・グーリンダイの・トリニダードポンポコピーの・ルイス・ポンポコナの・イ・長久命の・ピカソ・長助

AWS Lambda でコンテナに入れた Sinatra を動かす

何番煎じか分からないけど、最近やったので。

前提知識

つまりコンテナ化した Sinatra アプリを Lambda 上にデプロイして HTTP リクエストを受け付けることができる。

動かす準備はもう全部整っていて、お手軽そうですね。

Ruby アプリを Lambda で動かすコンテナイメージを作る

Sinatra 以前に、そもそも Ruby はどうやって Lambda Container Image 上で動くのか。公式にチュートリアルがあるのでこの通りで良い。

Deploy Ruby Lambda functions with container images - AWS Lambda

https://gallery.ecr.aws/lambda/ruby の Usage をなぞる。

  1. https://gallery.ecr.aws/lambda/ruby から base image を選んで、Dockerfile を作って
  2. docker build して
  3. docker run で Image を立ち上げると HTTP で待ち受けるので
  4. curl で発火させる
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'

謎 URL だけど「こういうもの」として覚えておけば良い。

特筆すべき点

ENV["GEM_PATH"]
#=> "/var/task/vendor/bundle/ruby/2.7.0:/opt/ruby/gems/2.7.0"

なので、ここに gem を入れておくと bundle exec しなくても gem を使える。いやまぁ bundler 使えば良いと思いますが。。

Rack アプリを Lambda で動かす

前述した公式の https://github.com/aws-samples/serverless-sinatra-sample の他に、 https://github.com/logandk/serverless-rack というものもある。

仕組み

どちらも肝は Rack::Builder.parse_file です。 config.ru を eval することで実行したい Rack App を取り出す処理。

App を取得できたので、env を組み立てて

app.call(env) して

Rack の status, headers, body の組から、Lambda の response になるように JSON を組み立て直す

Lambda の response というのはコレ。https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format

{
    "isBase64Encoded": true|false,
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
    "body": "..."
}

実装

これを自分の Rack アプリにどう組み込むかというと

app/config.ru と lambda.rb を置いて Dockerfile の CMD では lambda.handler を指定すると良い。

PROJECT_ROOT
├── app/
│  ├── config.ru
│  ├── Gemfile
│  └── Gemfile.lock
├── Dockerfile
└── lambda.rb

lambda.rb は https://github.com/aws-samples/serverless-sinatra-sample/ からコピーします。*1

他のファイルはこんな感じ。

# app/Gemfile
source "https://rubygems.org"
gem "rack"
# app/config.ru
run ->(env) { ["200", { "Content-Type" => "text/plain" }, ["OK"]] }
FROM public.ecr.aws/lambda/ruby:2.7

COPY lambda.rb ${LAMBDA_TASK_ROOT}

# rack を ${LAMBDA_TASK_ROOT}/vendor/bundle に入れたい
WORKDIR ${LAMBDA_TASK_ROOT}/app
COPY app/Gemfile app/Gemfile.lock .
RUN bundle config set path ${LAMBDA_TASK_ROOT}/vendor/bundle
RUN bundle install

COPY app/config.ru .

WORKDIR /var/task
CMD [ "lambda.handler" ]

で、実行するときは

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"httpMethod":"GET","requestContext":{}}'

すると、Lambda の JSON response が取得できる。

{"statusCode":"200","headers":{"Content-Type":"text/plain"},"body":"OK"}

渡す JSON のパラメータは https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format を参照。

{
    "resource": "Resource path",
    "path": "Path parameter",
    "httpMethod": "Incoming request's method name"
    "headers": {String containing incoming request headers}
    "multiValueHeaders": {List of strings containing incoming request headers}
    "queryStringParameters": {query string parameters }
    "multiValueQueryStringParameters": {List of query string parameters}
    "pathParameters":  {path parameters}
    "stageVariables": {Applicable stage variables}
    "requestContext": {Request context, including authorizer-returned key-value pairs}
    "body": "A JSON string of the request payload."
    "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encoded"
}

rackup した HTTP Server として Docker Image を実行したい

Lambda の Docker Image として実行するときは Lambda function for proxy integration の形式で JSON をやりとりしないといけない、というのは今まで書いてきた通り。

開発中は bundle exec rackup しておけばいいんだけど、正しく焼けてるか不安なときがあるので、Docker Image 上の Rack アプリをブラウザから実行したい。

つまり

  • HTTP サーバが立って
  • リクエストをいいかんじに JSON に変換して Lambda に投げて
  • Lambda からのレスポンスをいいかんじに HTTP に変換して返す

してくれる proxy (つまり API Gateway や ALB 相当のもの) があると便利だよね。というわけで雑にこんなのを用意しました。

コンテナを立ててた上でこの proxy を立てておくと、http://localhost/ でコンテナの中の Lambda の中の Rack アプリとやりとりできる。

絶対どこかにもっと良いやつあると思うので全然作り込んでない。(例えば multiValueHeadersisBase64Encoded は対応していない)

#!/usr/bin/env ruby
require "json"
require "net/http"
require "webrick"

LAMBDA_ENDPOINT = "http://localhost:9000/2015-03-31/functions/function/invocations"

s = WEBrick::HTTPServer.new
s.mount_proc("/") do |req, res|
  uri = URI(LAMBDA_ENDPOINT)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme === "https"

  headers = {}
  req.each do |k, v|
    headers[k] = v
  end
  json_data = {
    httpMethod: req.request_method,
    path: req.path,
    queryStringParameters: req.query,
    body: req.body,
    headers: headers,
    requestContext: {},
  }
  lambda_res = http.post(
    uri.path,
    JSON.generate(json_data),
    { "Content-Type" => "application/json" },
  )

  lambda_inner_res = JSON.parse(lambda_res.body)

  res.status = lambda_inner_res["statusCode"]
  lambda_inner_res["headers"].each do |k, v|
    res[k] = v
  end
  res.body = lambda_inner_res["body"]
end
Signal.trap("INT") { s.shutdown }
s.start

まとめ

  • Lambda でコンテナに入れた Sinatra アプリを動かすことができる
  • HTTP Request を Rack に変換して実行する仕組みを説明した
  • Lambda function for proxy integration の形式で JSON をやりとりすることになるので、proxy 立てると便利

*1:実際はディレクトリ構造変えたり apigatewayv2 対応したりでもはや別物になっているけど説明面倒なので省略

株式会社はてなに入社しました

株式会社はてなに入社しました

株式会社はてなに入社しました - hitode909の日記

4年目ですね。 さすがにそろそろ知らないことがあると恥ずかしい時期なので、だいたい手の中にはあると思います。 前提整える必要が無い=加速できるので、出力にご期待ください。

今年もよろしくお願いします。