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

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

Slack キーワード通知に設定しているワード 100 連発

エゴサが趣味なので、社内の Slack のキーワード通知をめちゃくちゃ便利に使っている。

slack.com

何か見ておくと良いことがありそうなものを片っ端から通知するようにしているんだけど、100 個が上限なの知ってた?

f:id:onk:20210331222523p:plain
100 個以上設定しようとするとこうなる

僕はこんなキーワードを入れています。適当に分類しながら見てみよう。

自分

onk, o/nk, on/k, おんく, 大仲, onaka, 眼科

自分の名前やハンドルネームを入れておくと、どこかで自分が呼ばれたときに気づけるようになる。

定時外だと親切でキーワード通知避けのために / を入れて発言する人もいるので、それも通知させるためにこういうキーワードになっています。戸籍ネームはほっっとんど呼ばれることは無いんだけど、念のため。

「眼科」は中心性漿液性網脈絡膜症になってるっぽいけど放置してたら T シャツが作られたので、戒めのためにずっと通知しています。

suzuri.jp

チーム

自分が所属しているチーム名だったり、プロダクトのコードネームだったり、サブ会 名だったり。

他のチームから言及されたときにすぐに気づける用。

サービス

小説, ノベル, カクヨム, kakuyomu, 魔法, maho, らんど, ランド

カクヨム魔法のiらんどを運営しているチームに居るので、サービスに関連しそうな話題が出たときに気づけるように。

障害検知

CRITICAL, 障害, 不具合, 負荷, 脆弱性, 重い, 重く, Incident, 申し送り

障害っぽかったら通知が来るようにしてある。

「申し送り」は障害対応後とかに使われることが多いワードですね。「重い」は月初に勤怠管理システムに対しての発言をよく見る。

インフラ系

infra, インフラ, SRE, SLA, SLO, SLI, 監視, PWG

インフラっぽい話題が出たら通知が来るようにしてある。

「PWG」は Performance Working Group の略で、パフォーマンス等が悪くなっていないかを定期的に (主に月次開催) チェックする会です。サービスのレスポンス速度やエラー率、発報されたアラートや障害などを3ヶ月ぐらいの目線で確認し、キャパシティプランニングを行ったり、監視設定や根本対策に思いを馳せたりします。

はてなにおける日々の仕事の中にあらわれるMackerelの活用 - Mackerel ブログ #mackerelio でも紹介されていますね。当時とは組織体制はかなり変わっていますが、目的はあまり変わっていません。

mackerel.io

コミュニケーション

(グループウェアのドメイン), 議事録

bot の発言ではなく、人間が URL を貼ったなら、何かしら人に伝えたい意図が含まれた面白い記事なんだろうってところから。実際、キャッチアップしておくと便利な記事がよく流れてきます。

「議事録」は割と興味本意かな。社内のミーティングでは「議事録」ってワードはあまり出ないので、他社さんとの会話が見えることが多い。

マネージャ仕草

cost, コスト, 予実, 予算, 実績, 費用, 数字, 稟議, 申請, release, リリース, キックオフ

はい。

他部署のものでも数字に敏感になっておくと色んな場面で役に立つし、キックオフやリリース時点で話題をキャッチできていると、これも会話の種になる。「デプロイ」はさすがに流速がヤバかったのでキーワードから外した。

マネージャ仕草 人事編

全社, ミッション, ビジョン, バリュー, MVV, 目標, 評価, グレード, 昇格, 昇給, 1on1, 面接, 面談, 異動, 採用, 新卒, インターン, CTO, チーフ, シニア, リーダー, メンター, メンティー, マネージャ, 方針, オンボーディング

1on1 した感想とか、目標設定や評価に対しての感想とかを拾っておきたい。

ふりかえり

ふり返り, 振り返り, 振りかえり, ふりかえり, KPT, YWT

ふりかえりだけでも見ておくとチームの現状が分かるので、覗きに行ってます。

技術

MySQL, RDS, Aurora, redis, Elasticache, Elasticsearch, ruby, rb, rails, レイルズ, レールズ, アジャイル, scrum, スクラム

この辺のワードが出たらいっちょかみしたい技術ワード。

こんなに登録していて仕事できるの

かろうじてなんとかなってるんじゃないかな……。

集中したいときはガン無視しているので、呼ばれても返事してないし、何ならときどきミーティングすっぽかしてる orz

Gemfile の Dual Boot 方法

Gemfile 内で切り替える

こういう ENV で切り替えるやつとか、eval_gemfile を使うやつとか。

https://github.com/discourse/discourse/blob/v2.6.2/Gemfile#L9-L16

def rails_master?
  ENV["RAILS_MASTER"] == '1'
end

if rails_master?
  gem 'arel', git: 'https://github.com/rails/arel.git'
  gem 'rails', git: 'https://github.com/rails/rails.git'
else
  # NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
  # this allows us to include the bits of rails we use without pieces we do not.
  #
  # To issue a rails update bump the version number here
  gem 'actionmailer', '6.0.3.3'
  gem 'actionpack', '6.0.3.3'
  gem 'actionview', '6.0.3.3'
  gem 'activemodel', '6.0.3.3'
  gem 'activerecord', '6.0.3.3'
  gem 'activesupport', '6.0.3.3'
  gem 'railties', '6.0.3.3'
  gem 'sprockets-rails'
end

Gemfile.lock に差分が出るので、アプリケーション (Gemfile.lock をリポジトリに含める) の場合はイマイチだと思う。

ライブラリの場合は Gemfile.lock は含めないことの方が多いので、大いにやると良さそう。マトリックステストも ENV を置くだけなので簡単。

eval_gemfile と言えば dry-rb や thredded 等で、開発用の gem を別ファイルに追い出している文化も面白いよね。(余談です)

これも余談だけど、一時期、Gemfile.local を読み込ませるために

eval_gemfile(__FILE__ + ".local") if File.exist?(__FILE__ + ".local")

と書くのが流行っていた時期もあって、これも Gemfile.lock に差分が出るので僕は苦手だったなって記憶が蘇ってきた。

各環境の Gemfile を用意する

各環境用の Gemfile を用意して、 BUNDLE_GEMFILE 環境変数を設定する。これも gem でよく見る。

# gemfiles/rails_5_2.gemfile
source "https://rubygems.org"

gem "rails", "~> 5.2.0"
gemspec path: '../'
# .github/workflows/main.yml
strategy:
  matrix:
    gemfile:
      - rails_5_2
      - rails_6_0
      - rails_6_1
env:
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile

gem は基本的に gemspec に依存が書かれていて、一部の指定が厳しい部分だけ *.gemfile で上書きすれば良いので、この形が自然に取れる。

アプリケーションの場合は eval_gemfile を利用して、共通部分は Gemfile.common とか Gemfile.global とかに抽出して同じことをやる。

3 ファイル (current 用, next 用, common) 必要なことと、common に何を追い出すかを考えるのがめんどくさいことがイマイチ。

Gemfile.next の symlink を置いて切り替える

RailsConf 2018 で発表されていた方法。

speakerdeck.com

僕は Getting Ready for Rails 6.0: How to Dual Boot - FastRuby.io | Rails Upgrade Service に書いてあるので知った。

fastruby.io

Gemfile に next? を定義して、symlink 経由で呼ばれたかどうかで切り替える。

def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem 'rails', git: 'https://github.com/rails/rails.git', branch: 'main'
else
  gem 'rails', '~> 6.1.0'
end
  • next? 用の lock ファイルは Gemfile.next.lock になる
    • 差分が別ファイルになるのでコミットできる
  • Gemfile だけで一元管理できる

という、上であげた 2 つの方法のデメリットをそれぞれ潰すことができている。

Shopify/bootboot

id:takkan_m が教えてくれた。

https://github.com/Shopify/bootboot

symlink を使わず、Bundler の plugin として実装したもの。

if ENV['DEPENDENCIES_NEXT']
  ...
end

の内容が Gemfile_next.lock に書き出される。