TECH PLAY

タイミー

タイミー の技術ブログ

274

タイミーのyama_sitter、須貝、小林です。 国内最大級の アジャイル 、 スクラム 関連のイベント「Regional Scrum Gathering Tokyo 2024(RSGT2024)」 が1/10〜1/12の3日間にわたって開催されました。 2024.scrumgatheringtokyo.org タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。 productpr.timee.co.jp こちらの制度を利用して今年は スクラム マスター3名、エンジニア3名が参加してきました。 前編 に続き、後編の今回はエンジニア3名から参加レポートをお送りします。 SM/EMからエンジニアに視点を変えて参加 エンジニアの yama_sitter です。 参加は今年で2回目になります。前回は前職からSM/EMの立場として参加しましたが、今回はタイミーの現場エンジニアという立場で参加する形となりました。 「エンジニアからSMになり、EMを経てエンジニアに戻った人」という視点で、特に印象に残ったセッションが3つありましたので紹介します。 Badプラクティスを選んで失敗しながら進めた新規プロダクト開発 自分はSM時代に「形」に捕らわれ過ぎて色々な失敗を重ねてきました。一方で今所属するチームではいわゆる スクラム の形式では仕事をしておらず、それでもちゃんと成果が出せている状態です(※1)。これらの経験から、この1年ほどは「 アジャイル は形じゃない」というのを自分なりに理解し、また考え続けていました。(この間、世に出回る「べき論」や「プ ラク ティス」みたいなものをとにかく疑ってかかっていました) そんな中見たのがこの「Badプ ラク ティスを選んで~」のセッションです。ここでは世間的にはNGとされるプ ラク ティスを敢えて用い、(もちろん苦しみながらも)ちゃんと成果を出した事例が語られていました。Badと言われるものだと分かった上で、そのリスクを理解し取り入れ、結果に結び付ける。このあり方は教科書的でこそないものの、ちゃんとした一つの解だなぁ、と。 つまるところ「価値に向き合い、自分たちが考えうる/持ちうる最大の力で成果を生み出す」ことが重要なのであって、形ではないなという話です。今の自分が感じていることを後押しし更に前に進めてくれたこのセッションは非常に有意義なものでした。 こちらはスライドもあるので是非ご一読を。 speakerdeck.com ※1 あくまで会社全体としては スクラム に力を入れており、自チームは特殊なケースです Lack of curiosity: The silent killer of agile 「好奇心の無さはアジリティを殺す」という話です。内容は勿論ですが、特にこのタイトルが強く印象に残っています。 入社して半年、以前考えていた抽象的な問いからは離れて今はひたすらに具象と向き合っています。入社直後に感じていた様々な疑問も少しずつ薄れてきました。アウトプットこそ出せているとは思います。が、少しずつ「疑問に思う力」や「ワクワクする力」がマヒしていっているのを感じています。 これだと、本当に向き合うべき課題に気付き向き合うことができないんですよね。セッションでも述べられていましたが、「人間はルーチンが好き」なので尚更です。 自分や目の前のコードとひたすら向き合うだけでは高い視点からは物事を考えられません。疑問に思わなければ脳が刺激を受けずに鈍化していきます。ワクワクしていなければ途中で力尽きます。結果として気付ける課題、向き合う課題がすごくミニマムになっていく。 現場にフォーカスするだけだと、巡り巡って良いエンジニアにはなれないなぁ、ちゃんと好奇心を持ち続ける努力をしないとなぁ、と感じさせるセッションでした。 Solving The Value Equation 「レガシーコード改善ガイド」の著者であるMichael Feathersさんのキーノートです。 めちゃくちゃためになる話ばかりでしたが、個人的に刺さったのは「プロセス、 アーキテクチャ 、チームの能力、組織構造といった各レイヤーの目標は、 “価値に至るまでのプロキシ” でしかない」という話です。 “プロキシ” というのが特に良くて、これを意識すると「今の取り組みとその結果は、必ず何らかの価値に転換されるために存在する」という考え方ができます。当然のことではあるんですが、目の前のことにフォーカスしたり「べき論」に囚われたりすると忘れがちだなぁ…と思ったりします。 余談ですが、前職のSM最初期の頃は「それっぽいメトリクス」を見つけては右往左往していました。あれはあれで必要なプロセスだったものの、できればこの “プロキシ” の考え方を持っておきたかったです。 視点を変えての参加となりましたが、選ぶセッションも見るスタンスも感じることも得られるものもかなり違うし、得るものの幅が増えていることに気付いて面白かったです。一度全力でSMをやってからエンジニアに戻ると面白いですね。 とまぁここまで色々書きましたが、正直セッションよりも「素晴らしいセッションを見たあとに同じ熱量を持ったタイミーメンバーと集まり、酒を飲みながら議論する」時間が一番良かったです。こういった時間のために行っているフシすらある。 総じて、RSGTは今年も良いイベントでした。企画/運営の皆さま、ありがとうございます。 メンバーと自分たちの組織について語り合うきっかけに エンジニアの 須貝 (CSM保持)です。 RSGTは今回が初参加でして熾烈なオンサイトチケットの争奪戦を勝ち抜いて現地に行ってまいりました。自分は現在はマネージャーでも スクラム マスターでも無いのですが、以前からチーム・組織で成果を出すにはどうすれば良いかといったテーマに興味があったので参加しました。 基本はセッションを聞くことを中心に、合間の休憩時間などで知り合いの方とお話をして過ごしました。中でも印象に残ったセッションのひとつはJoe Justiceさんの「 ジョーが語る、Teslaでの衝撃的な開発スピード 」です。電気自動車メーカーTeslaの開発サイクルのあまりの速さに本当に自動車の開発なのか!?と驚きの連続でした。と同時にハードでこのスピードでできているのだからソフトウェア開発をしている自分たちももっとやれるのでは、勝手にできないと思っているだけなのでは、という反省もありました。 オフトピックなところだと会場内でプロのカメラマンに写真を撮ってもらえるフォトブースがあったのが良かったです。Global Scrum Gathering Amsterdamで行われていたものが日本にも輸入されたそうです。 プロのカメラマンに撮影されるりっきーさんとShinoPさん 自分たちは普段フルリモートなので、弊社のメンバーと自分たちの組織などについて対面でじっくり話せたのも非常に良い体験でした。チャンスがあればまた参加したいと思います。 RSGTは気づきと学びの場 こんにちは!タイミーでQA スペシャ リストを担当している小林依光です。 RSGTはオンラインで参加しましたが、今回も「やっぱりそうだったんだ」と自分の知識や経験を後押ししてくれる発表内容に、感謝の気持ちでいっぱいになりました。 事例/実績、考え方の共有は気づきと学びの場だと、改めて思いました。 少しですが、私が気になったセッションについて、紹介したいと思います。 「 ベロシティ Deep Dive 」ではベロシティを生産性に活用することをやめましょう!ということを多様な角度から説明していました。どの角度からの説明も納得感のあるものばかりでしたが、一番印象的だったのが、“ アジャイル や スクラム が目指すのは価値の実現であり、この文脈での生産性は「付加価値生産」である”という説明でした。 アジャイル を採用し探索的/実験的にプロダクトを開発し価値を提供するという基本を忘れてはいけないと学べました。 「 プロダクトをあきらめるとき 」のセッションは一般的な スクラム の話ではありませんでしたが、実際のプロダクト開発で遭遇する実例でした。特に“プロダクトを終了させるときの費用”をプロダクト バックログ の順位の下の方に入れておくという考えには共感でき、あきらめる判断やサービス撤退に必要な洗い出しは、プロダクトを止めるときでなく開発を始める際に決めておく必要があると何度か痛感していたので、改めていままでの経験は役に立つことがあると思いました。 紹介したセッション以外からも多くを学べたRSGT、来年は現地で参加できるようにしたいと思いました。 まとめ いかがでしたでしょうか。 参加して満足して終了では意味がない!ということで後日参加者全員で振り返り(Fun Done Learn)も行いました。しっかりと ネクス トアクションも生まれたので、ここからチーム・組織に今回得た学びを還元していければと思います。 来年もまたGatheringしましょう!
アバター
タイミーのmaho、ShinoP、りっきーです。 国内最大級の アジャイル 、 スクラム 関連のイベント「Regional Scrum Gathering Tokyo 2024(RSGT2024)」 が1/10〜1/12の3日間にわたって開催されました。 2024.scrumgatheringtokyo.org タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。 productpr.timee.co.jp こちらの制度を利用して今年は スクラム マスター3名、エンジニア3名が参加してきました。 その参加レポートとして印象に残ったセッション、ワークショップや学びなどを参加者それぞれの視点で共有できればと思います。今回は前後編の前編として スクラム マスター3名のレポートをお送りします! 初参加で OST のホストに挑戦 スクラム マスターをしているmahoです。RSGTは今回が初参加で緊張していましたが、一緒に現地参加するメンバーがいたので心強かったです。 私はまだまだ スクラム マスター初心者ということもあり、セッションを聴いて学びを深めることを一番の目的にしていました。どのセッションも興味深い内容でしたが、全体を通して感じた共通点は以下の三つです。 チームの基盤を支える重要な柱のひとつは関係性であること 変化は時間がかかることを理解して忍耐を持つこと システム全体を捉えるよう努めること 私もこれが体現できるように、学びを実践しながら精進していきたいと思います。 また、三日目の OST には話したいテーマを発表し、ホストとしての参加に挑戦してみました。不安でいっぱいの中、いざ始まってみると集まってくださった方々と予想以上に議論が盛り上がり、とても楽しい時間を過ごすことができました。セッションを聴くだけでなく、Gatheringとしてもよい体験ができたので、もし次回も参加することになれば、より積極的に話す機会を作ってみたいと思いました。 どう社内に持ち帰るかを話す時間は何事にも変え難い タイミーで スクラム マスターをやっているりっきーと申します。昨年に引き続き2回目の現地参加になります。 私は毎年年末にテーマを決めていまして、1年かけて達成したかどうかを振り返っています。 その中で、RSGTは年初一発目ということで年末に決めたテーマについて知見を得たり、ディスカッションをできるので非常に助かっています。 私の本年度のテーマは「 スクラム マスターとして成長できる・やってみたくなる環境を作る」です! テーマに最も関連していたので以下を紹介いたします。 「 スクラムマスターを職能にする挑戦 - 健全なチームを増やし組織をチームワークであふれさせる道のり 」 スクラム マスターをどのように増やしていったか、どのような仕組みを作っていったかは大変参考になり、勇気がもらえました! 1on1や コーチン グ、メンタリング活動は弊社としても取り組み始めていたのですが、給与レンジの検討や成長支援の予算などは検討できてない部分なので弊社ならではを組み合わせつつ、一つの事例として社内に紹介したいと思います。 また、メンターとしては初心者の方もRSGTで楽しめるように社外の人との会話に入ってもらったり、 OST に参加するように促したりを 裏目 標として持っていましたが、楽しめたようで何よりでした! 皆さんも書いてますが、同じものを見て聞いてそれについてどのように社内に持ち帰るかを話す時間は何事にも変え難いので、来年も参加したいなと思いました。(オフライン参加は難しいかもしれませんが) RSGTのオススメの歩き方 こんにちは!タイミーで スクラム マスターをしている ShinoP です! 今回のRSGTは現地参加としては2回目なのですが、前回はセッションを聴くことに夢中になっていて、本当の意味でのGatheringができませんでした。 その中でも OST のホストをやった事で、参加者同士の触れ合いを体験しGatheringの本当の意味を理解しました。 ですので、セッションに関しての感想というよりは、RSGTのオススメの歩き方を紹介したいと思います! 今回のRSGTでの立ち回りはセッションを聴くだけでなく… 「廊下で初めて会った人と話す」 「お昼に社外の人と話す」 「研修やコミュニ ティー で面識のある人と話す」 「登壇者と話す」 「飲み会に参加する」 「お昼の30秒宣伝コーナーで話す」 などを積極的に行った結果、同じような悩みを抱えている方と話すことによってヒントを得られたり、登壇者の方と話してアド バイス をもらったり、面識のある方々とワイワイできたり…このような歩き方も学びがあるので是非試してみてください! また、RSGTや スクラム フェスは人に話しかけやすい構造になっていると思いますので、隣に座っている人とかに話しかけたりすると繋がりが広がっていくと思っていますので、是非思い切って話しかける事がオススメです! 今回のレポートは以上となります。 続きの後編は「エンジニア編」として近日公開予定です。
アバター
こちらは Timee Advent Calendar 2023 シリーズ1の25日目の記事になります。 昨日は @tomoyuki_HAYAKAWA による Swift Concurrency AsyncStreamを使ってみる #Swift - Qiita でした。 タイミーでバックエンドエンジニアをしている id:euglena1215 です。 メリークリスマス🎄 みなさんの手元にはプレゼントは届いているでしょうか。 Ruby の世界では Ruby コミッターサンタさんがクリスマスプレゼントとして新しい Ruby バージョンをリリースしてくれます。 今年は Ruby 3.3 ですね。個人的には 3.3 の YJIT がどれだけ速くなるのか楽しみです。 また、新しいバージョンのリリースにはアップグレードがつきものです。アップグレードせずには新しいバージョンの恩恵を受けることはできません。 ということで、今回はタイミーが社内で運用している社内版 Ruby Rails アップグレー ドガ イドを社外に公開します。 社内版の情報を黒塗りしては社内版を公開する意味がありません。そのため、最大限 原文ママ でお送りします。前提として足りない部分は「記事用補足」と追記しています。 社内版 Rails アップグレー ドガ イド このドキュメントは Rails アップグレードのために普段からやること、実際のアップグレードの際にやることをまとめたものです。基本的には一般的な Rails アップグレー ドガ イドと同じですが、QAチェックの必要性など社内特有の事情も考慮されています。 下記のドキュメントは Rails のメジャーバージョン・マイナーバージョンを上げる際に意識すべきことです。パッチバージョンは普段のライブラリアップデート同様に上げてもらって構いません。 🟥 MUST: 安全なアップグレードのために必ず実施しなければいけません。 🟨 SHOULD: 必ず実施しなくてもいいですが、やっておくことを推奨します。 🟩 MAY: 行わなくてもアップグレードは実施できます。関心がある方だけで構いません。 以下の作業は基本的に並列かつ複数人で実施可能です。 社内版 Rails アップグレードガイド 普段やること Rails Edge のキャッチアップ(🟩 MAY) Rails Edge 起因で失敗しているテストの修正(🟨 SHOULD) Rails へのコントリビュート(🟩 MAY) アップグレード前にやること アップグレード先のCIを用意する(🟨 SHOULD) Railsバージョン起因でスキップしているテストをなくす(🟥 MUST) deprecation warning をなくす(🟨 SHOULD) rails app:update の実行(🟥 MUST) Rails アップグレードガイドのバージョンに対応した変更を読んで問題ないことを確認(🟥 MUST) リリース前の手動での動作確認(🟥 MUST) アップグレード作業中に行うこと アップグレードの事前周知(🟥 MUST) rollback 用のコミットハッシュを取得する(🟥 MUST) Revert PR を作成する(🟩 MAY) 各種監視を行う(🟥 MUST) アップグレード後にやること new_framework_defaults_x_y.rbの有効化(🟨 SHOULD) config.load_defaultsの更新(🟨 SHOULD) 参考 普段やること Rails Edge のキャッチアップ(🟩 MAY) #guild_rails_edge には毎日 rails / rails の main branch にどんな変更がマージされたのかが流れてきます。ここで次のアップデートでどんな変更が入るのかをチェックしましょう。日頃からチェックしておくとアップデート時に情報の波に飲まれにくくなります。 (記事用補足:ここで流しているのは y-yagi さんのブログです) y-yagi.hatenablog.com Rails Edge 起因で失敗しているテストの修正(🟨 SHOULD) 通常の Rails バージョンでは動作するが Rails Edge で動かないテストは pending: pending_if_rails_edge をつけて pending してあります。 grep して pending しているテストを見つけて問題を特定し修正しましょう。 (記事用補足:タイミーでは CI で Rails Edge を使ったテストを実行しています。詳しくは以下) tech.timee.co.jp Rails へのコントリビュート(🟩 MAY) 「 Rails Edge 起因で失敗しているテストの修正」の大半はアプリケーションコードに起因する問題であることが多いですが、たまに Rails 本体のバグに遭遇することもあります。その際は Rails にバグレポートや修正 PR を送ることを推奨しています。 リリース済みのバージョンの仕様を変えるのは難しいですが、betaやrc版ではissueやPRを送ることで仕様を変えられる可能性があります。意図しない破壊的変更を見かけた場合は積極的にやりとりしましょう。 参考: Get started with OSS contributions - Speaker Deck アップグレード前にやること アップグレード先のCIを用意する(🟨 SHOULD) アップグレードを検討している頃には既にアップグレード先のバージョンがリリースされているため、 Rails Edge はその次のバージョンになっていることが多いです。そのため、 Rails Edge CI ではアップグレード先のバージョンでのテストは実行できません。 アップグレード先の Rails バージョンでの CI を用意しましょう。そうすることで、アップグレード先バージョンでのテストが普段から実行できるようになるので問題を早期発見できます。 参考 (ここに Rails 7.1 の CI を用意した Pull Request URL が貼ってありました) Rails バージョン起因でスキップしているテストをなくす(🟥 MUST) Rails バージョン起因でスキップしているテストはアップグレード後に動かなくなるテストです。このテストを放置したままアップグレードを実施すると該当機能が動かなくなるので必ずスキップしているテストがないことを確認しましょう。 「 Rails Edge 起因で失敗しているテストの修正」を実施していればスキップしているテストはほとんどなくなっているはずなので、このステップの対応が楽になります。 参考 (ここに Rails バージョン起因でスキップしているテストを修正している Pull Request URL が貼ってありました) deprecation warning をなくす(🟨 SHOULD) ほとんどの問題は「 Rails バージョン起因でスキップしているテストをなくす」によって解消されますが、テスト環境では捉えきれない問題も存在します。それらの問題を洗い出すために、タイミーでは本番環境で発生した deprecation warning をログとして出力するようにしています。出力されたログは Datadog Logs で確認できます。 また、ログが出力されすぎて Datadog のコストを圧迫することを防ぐためにログ出力は確率的としています。deprecation warning が減ってきたら合わせて確率を100%に近づけ漏れなくキャッチできるようにすることを推奨します。 rails app:update の実行(🟥 MUST) Rails では app:update というコマンドが提供されています。 Gemfile に記載されている Rails のバージョンを更新後、このコマンドを実行することで、新しいバージョンでのファイル作成や既存ファイルの変更を対話形式で行うことができます。 $ bin/rails app:update exist config conflict config/application.rb Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] force config/application.rb create config/initializers/new_framework_defaults_7_0.rb ... 予期しなかった変更が発生した場合は、必ず差分を十分チェックしてください。 https://railsguides.jp/upgrading_ruby_on_rails.html#アップデートタスク デフォルトに合わせていた方が今後のアップグレードが楽になるため、既存の挙動をなるべく変化させないように更新分を取り込みましょう。 変更差分を知りたい場合は https://railsdiff.org/ から確認できます。 Rails アップグレー ドガ イドのバージョンに対応した変更を読んで問題ないことを確認(🟥 MUST) 公式の Rails アップグレー ドガ イドにはメジャーバージョン・マイナーバージョンのアップグレードに対して破壊的変更など何らかの対応が必要な変更がまとまっています。これらを確認して問題がないことを確認してください。懸念がある場合は #prd_ch_ruby_on_rails で相談するなど適切な対応を行なってください。 例:7.0→7.1 Rails アップグレードガイド - Railsガイド 参考 (ここに確認したログをまとめた Notion リンクがありました) リリース前の手動での動作確認(🟥 MUST) リリース前の手動での動作確認は必須ですが、社内向け管理画面の最低限の疎通確認のみで問題ありません。 背景: (ここに意思決定の証跡として MTG 議事録リンクが貼ってありました) また、社内向け管理画面においても十分なテスト カバレッジ があれば動作確認を省略することができます。社内向け管理画面のテスト カバレッジ を上げるプロジェクトについては(Notion URL が貼ってありました)を確認してください。 (記事用補足:プロジェクトの Notion URL よりもTimee Advent Calendar 2023シリーズ1の5日目記事の方がより詳細にプロジェクトの説明をしています。興味があればご覧ください。) tech.timee.co.jp アップグレード作業中に行うこと アップグレードの事前周知(🟥 MUST) Rails アップグレードのリリース直後に rollback が困難な変更が行われると Rails アップグレード起因で問題が発生した際に rollback が困難になり、問題の復旧が遅れます。そのため、アップグレードを事前に周知しアップグレード後30分間はデプロイ(=マージ)を行なわないよう呼びかけてください。 rollback 用のコミットハッシュを取得する(🟥 MUST) 問題があった際の復旧を早めるために rollback で戻す先のコミットハッシュを用意しておきましょう。 https://github.com/x/x/commits/master から確認できます。 Revert PR を作成する(🟩 MAY) 問題があった際の復旧を早めるために merge 後すぐに Revert PR を作成しておくことを推奨します。先に Revert PR を作っておけば feature branch での CI 待ちをショートカットすることができます。 問題が発生せずに無事リリースできた場合はきちんと PR を close しておきましょう。 各種監視を行う(🟥 MUST) リリース直後は問題を早期発見するために各種の監視を行なってください。15分様子を見て問題が見つからなければ急いでrollbackする必要はありません。 Sentry 普段見かけないエラーが発生していないかどうか Datadog ダッシュ ボード 大きなメトリクスの変化がないかどうか APM レイテンシやエラーレートに変化がないか (記事用補足:Sentry や Datadog など各種 URL が貼ってありましたがさすがに削除させていただきました) アップグレード後にやること new_framework_defaults_x_y.rbの有効化(🟨 SHOULD) Rails をアップグレードしただけでは多くの新しい機能は有効化されていません。 rails app:update で作成された new_framework_defaults_x_x.rb の コメントアウト を1つずつ確認してデフォルトに合わせられそうなものは有効化しデフォルトに合わせ、意図を持ってデフォルトとは異なる設定を行う場合は application.rb に設定を追記しましょう。 config.load_defaultsの更新(🟨 SHOULD) app:update タスクでは、アプリケーションを新しいデフォルト設定に1つずつアップグレードできるように、 config/initializers/new_framework_defaults_X.Y.rb ファイルが作成されます(ファイル名には Rails のバージョンが含まれます)。このファイル内のコメントを解除して、新しいデフォルト設定を有効にする必要があります。この作業は、数回のデプロイに分けて段階的に実行できます。アプリケーションを新しいデフォルト設定で動かせる準備が整ったら、このファイルを削除して config.load_defaults の値を反転できます。 https://railsguides.jp/upgrading_ruby_on_rails.html#フレームワークのデフォルトを設定する 参考 railsguides.jp qiita.com いかがでしたか? タイミーは上記の Rails アップグレー ドガ イドを用いて Rails アップグレードを行なっています。とは言ったものの、このアップグレー ドガ イドは Rails 7.1 アップグレードでやったことを体系的にまとめたもので本当に運用され始めるのは Rails 7.2 アップグレードのタイミングになります。 Rails アップグレードは属人的なタスクになりがちです。特定の人物がアップグレードの番人になるのではなく、誰でもアップグレードにチャレンジできるようにしたかったのがアップグレー ドガ イドを作成した背景になります。 ネットには質の高い Rails アップグレー ドガ イドがたくさん存在します。ですが、どのステップをどんな期待値で実施するか・動作確認はどこまでやるかは各社の状況に依ると思います。社内の暗黙値を減らすためにも社内版 Rails アップグレー ドガ イドを作ってみてはいかがでしょうか。
アバター
こんにちは!株式会社タイミーでプロダクトマネージャーをしているAndrewです。 私はオフショ アメンバー が関与するSquadに所属しています。このSquadはエンジニア組織の中でもユニークな環境であり、ここで直面した問題とそれを乗り越えるための取り組みをこの記事で紹介したいと考えています。 オフショ アメンバー とのコラボレーションの課題 約半年前、オフショ アメンバー のいるSquadと関わり始め、このSquadの各メンバーを知るきっかけとなりました。 最初は、日本メンバーとオフショ アメンバー のコミュニケーションがスムーズでないと感じました。ミーティングでは通訳担当の方以外、オフショ アメンバー 全員がほとんど発言しませんでした。 また、ミーティングで話されている内容が彼らが理解できる言語で記録されていないケースが多く、情報の透明性に欠けていました。開発した機能の目的が何かを理解しない場面を見かけたこともありました。 しかし、これは単なる課題ではなく、Squad全体のポテンシャルを引き出すためのチャンスでもあると考えました。 エンジニアは機能を作るだけでなく、プロダクトに関心を持ち、価値を届けることに注力すれば、より多くのアイディアやソリューションが生まれ、良いプロダクトが作れるのではないかと。そのためには、オフショ アメンバー が積極的にディスカッションに参加できるようにする必要がありました。 最初は言語の壁があると感じたため、心配もありましたが、様々な施策を試してみました。その一環として、ドキュメントの整理とオンライン言語交流会を実施してみました。 情報透明性を向上するためのドキュメント 「ドキュメントのメンテナンスが面倒くさい!」、「コードを読む方が早い!」と思う方は少なくないでしょう。 コードにコメントがないと、そのコードが何をしているか理解することが難しくなります。それと同様に、ドキュメントがないと、システムや機能の全体像を把握するのに時間がかかり、理想的な状態と言えません。 コードで説明できない背景や設計の意図、特定の決定の背後にある論理をコメントで説明することで、コードを読む人が理解しやすくなります。しかし、コードのコメントでわかりやすく説明できる内容はテキストベースの情報に限られているということもあり、コードは特定のメンバーしかアクセスできないため、ここでドキュメントが強力な情報共有手段になります。 ドキュメントに対する工夫 ドキュメントを書くときにわかりやすくするためにいろんなテクニックがあります。 テキストに色をつけたり、フォントサイズを変えたり、ページの余白を活かすことで視覚的なメリハリをつけることが重要です。 これにより、複雑な情報も読みやすくなり、重要な情報を強調することができ、逆に強調したくない情報を目立たなくすることもできます。デザインが好きな私にとっては、ドキュメントは良いデザインを表現することのできる優れたツールです。 わかりやすいドキュメントを書くことは簡単ではありません。いろんなドキュメントの種類の中で、エンジニア向けのものもあれば、 ステークホルダー (非エンジニア)向けのものもあるので、伝わるように読み手のバックグラウンドや知識に合わせて書くことが必要です。 難しい単語ではなく、あえてシンプルな単語を選び、物事を説明することで言語の壁をなくせるので、自分の言葉の知識を見せびらかす必要なんてありません。 このような意識をもって書けば、ドキュメントを書くことは単調な作業ではなく、実は楽しい作業なんです。 バランスがすべて 口頭でコミュニケーションが取りづらい状況では、ドキュメントは尚更重要です。 ドキュメントがあることで、メンバーはいつでも情報にアクセスでき、フィードバックや提案もしやすくなります。ただし、あまりにも詳細な内容、たとえばコード実装のレベルの情報まで書いてしまうと、細かなコードの変更が生じるたびにドキュメントを更新しないといけなくなってしまいます。 同様に、テキストだけの長い文章を書いてしまうと、ドキュメントの恩恵が受けられなくなり、わかりやすさが損なわれ、逆に効果が薄れてしまいます。適度な詳細と抽象度のバランスを取ることが大切であり、ドキュメントが管理の手助けとなるよう心がけが必要です。 活用されるドキュメント 実際にこの取り組みを導入して、Squadの振り返りミーティングで「 ドキュメンテーション をよく参照するようになって、仕事において役立った」「ドキュメントが整理されていてわかりやすい」というメンバーからの声がありました。 今後、ドキュメントを書くのは私ではなく、オフショ アメンバー 全員も同じようなケイパビリティを持たせたいので、以上に書いたポイントを彼らに教えました。 今ではオフショ アメンバー 全員がドキュメントを書くようになり、 ステークホルダー から仕様確認の問い合わせが来ても、すぐ回答できるようになりました。これにより、プロジェクト全体の透明性が向上し、知識の共有がスムーズになりました。 長期的な視点で見れば、ドキュメントはSquad全体の知識の蓄積として機能し、メンバーが増えても情報の共有の負担は軽減されます。 オンライン言語交流会 ドキュメント整理が進む中、私たちはSquadメンバーがより積極的に参加し、アイディアを共有できる環境を整えるためにもう一つの取り組みを始めました。それが、オンライン言語交流会です。 知っている人とコミュニケーションを取ることは楽ですが、知らない人となると相手の意図がどのようなものかもわからないし、ミスコミュニケーションの原因にもなります。できるだけオフショ アメンバー とのコミュニケーションを増やすことで、彼らもより気軽に話しかけてくれるだろうと期待しています。しかし、日本語もできず、英語にも自信がないメンバーとどのようにすれば交流できるのか悩んでいました。そこで、オンラインで言語交流会を試してみました。 オンラインで実施しやすいアイスブレイク いきなり何かを英語で話せと言われても難しいですよね。 そこでアイスブレイクゲームを導入し、わざわざ自分でトピックを考える必要がなく、よりカジュアルに話せる環境にしました。最初は簡単なものから始め、英語で自分のカバンに入っているものを紹介したり、謎解きをしたりしました。 カバンに入っているものを紹介する中で、お弁当の中身だったり、意外なエアコン用のリモコンの話も出てきて、会話が弾みました。謎解きをするときもトリッキーな謎が多く、あるメンバーがたくさん正解を回答できて、他のメンバーから カンニング しているだろうと冗談で言われたりしていた覚えがあります (笑)。 これにより、お互いの趣味や考え方を知ることができ、メンバー同士のコミュニケーションが深まりました。 伝わることの喜び オフショ アメンバー と会話を重ね、最初は英語に対して悲観的で、喋ろうとしないメンバーも自然と喋るようになりました。この記事を書く際に彼らにその理由を聞いてみました。 「英語の発音は下手ですが、理解してくれる人がいるので勇気が出る」「私の英語で喋っても笑われていないし、Andrewからの促進で一歩踏み出そうと思うようになって、ディスカッションに関与する重要性を理解できるようになった」という素敵なコメントをいただきました。 これらの言葉を通じて、協力し合いながら成長するプロセスに感謝しています。 このオンライン言語交流会の ファシリテーション は最初は私が担当していましたが、現在はオフショ アメンバー が積極的に ファシリテーション することもあります。今では毎回アイスブレイクゲームだけでなく、単純に英語で雑談することもできる環境になりました。 また、この会の名前の通り、英語に限らず、最近私が彼らから言語を学ぶ機会も得ました。 Squadの成長と今後 まだ完璧な状態には達していませんが、Squad全体がどんどん成長している実感があります。オフショ アメンバー が積極的にコミュニケーションに参加し、自ら意見を言うようになったことは、大きな進歩と言えます。 異なる文化やバックグラウンドの人と仕事することは、チームが結束するまでには時間がかかりますが、結束ができた際には異なる視点から物事を判断でき、それによるメリットも大きいです。 私の過去の経験では、さまざまなバックグラウンドを持つ人々が集まれば、クリエイティブなアイディアが生まれやすくなります。そのため、このような多様性を大切にしていきたいです。 最後に、今後もSquad全体で協力し、より良いプロダクトを生み出すために努力していきたいと思います。
アバター
こちらは Timee Advent Calendar 2023 シリーズ1の19日目の記事です。 はじめに こんにちは、タイミーで Android エンジニアをしているsyam( @arus4869 )です。 この記事では、タイミーで実際に利用しているBitriseとFirebase App Distributionを用いた Android アプリの配布方法を実例を交えながら紹介していこうと思います。 この記事を通じて、同じようなツールを利用している他の開発者の皆さんにとって、参考になる情報を提供できればと思います。 BitriseとFirebase App Distributionについて Bitriseは、モバイルアプリのCI/CDプロセスを自動化するプラットフォームです。ビルドからデプロイまでを簡単に管理でき、開発サイクルを効率化します。 Firebase App Distributionは、アプリのベータ版やテスト版をテスターに迅速に配布するためのツールです。新しいビルドをテスターに届け、フィードバック収集が可能です。 これらのツールを組み合わせることで、 アプリ開発 の流れをスムーズにしてくれます。 事前準備 BitriseとFirebase App Distributionを使用する前に、いくつかの事前準備が必要です。 まず、BitriseとFirebase、そして Google Cloud Platform( GCP )のアカウントを準備します。 それぞれのツールを利用するには適切なアクセス権と設定が必要です。事前に準備しましょう。 詳細なアカウント作成方法については、それぞれの公式を参照してください。 参照URL Bitrise Firebase Google Cloud Platform(GCP) 次は具体的な手順へ進めていきます。 手順1: Firebase App Distributionのグループとテスターの設定 Firebase App Distributionを使用してアプリを効率的に配布するためには、適切なグループとテスターの設定が必要です。 以下のステップに従って設定を行いましょう。 Firebaseプロジェクトにアクセス Firebaseコンソールにログインし、対象のプロジェクトを選択します。 テスターグループの作成 「App Distribution」セクションに移動し、「テスターとグループ」タブを選択します。 「新しいグループを作成」をクリックし、グループ名(例:product_team)を入力します。 テスターの追加 新しいグループを選択し、「テスターを追加」をクリックします。 テスターのメールアドレスを入力し、グループに追加します。 テスターは開発用と本番用のアプリに分けてグループ化すると管理がしやすくなります。 この手順により、特定のテスターグループに対して、アプリのテストビルドを簡単に配布できるようになります。 グループとテスターの設定が完了したら、次にFirebaseの認証用サービスアカウントの作成をしていきます。 手順2: Firebaseの認証用サービスアカウントの作成 Firebase App Distributionでアプリを配布するためには、Firebaseの認証用のサービスアカウントを設定する必要があります。この手順では、その設定方法を説明します。 Google Cloud Platform( GCP )でサービスアカウントを作成 GCP の ダッシュ ボードにアクセスし、「サービスアカウント」ページを開きます。 「サービスアカウントを作成」を選択し、アカウントの詳細を入力します。 必要なロールを割り当て サービスアカウントに以下のロールを割り当てます: Firebase 品質管理者 Firebase App Distribution Admin SDK サービス エージェント これらのロールにより、アカウントはFirebase App Distributionでのアプリ配布に必要な権限を持つことになります。 秘密鍵 の生成と保存 サービスアカウントに対して新しい 秘密鍵 を生成します。 生成された 秘密鍵 を安全な場所に保存し、後のBitriseの設定で使用します。 次は、このサービスアカウントを使用して、Bitriseでのプロジェクト設定を行います。 手順3: Bitriseの設定 次は、BitriseからFirebase App Distributionを通じてアプリを配布するためには、Bitrise上での適切な設定が必要です。以下のステップに従って、Bitriseでの設定を行いましょう。 Bitriseプロジェクトのセットアップ : Bitriseにログインし、新しいプロジェクトを作成または既存のプロジェクトを選択します。 プロジェクトのビルド設定を確認し、必要に応じて調整します。 Firebase App Distributionステップの追加 : プロジェクトのワークフローを編集し、「[BETA] Firebase App Distribution」ステップを追加します。 このステップにより、ビルドが成功すると自動的にFirebase App Distributionを通じてアプリが配布されます。 認証情報の設定 : GCP で生成したサービスアカウントの 秘密鍵 をBitriseの「Code Signing & Files」タブにある「GENERIC FILE STORAGE 」 セクションにアップロードします。 「[BETA] Firebase App Distribution」ステップの設定で、Service Credentials Fileの項目に「GENERIC FILE STORAGE」セクションにアップロードしたサービスアカウントの 秘密鍵 のパスを設定します。 これらのステップにより、BitriseはFirebase App Distributionと連携し、アプリのビルドと配布を自動化できるようになります。設定が完了すれば、BitriseからFirebase App Distributionを通じてアプリをテスターに配布する準備が整います。 参考程度に「[BETA] Firebase App Distribution」に設定している項目を記載します。 項目 説明 例 Service Credentials File Bitriseのアップロードした 秘密鍵 のパス $BITRISEIO_development_service_account_key_URL App Path APK及びAABファイルが格納されているパス $BITRISE_APK_PATH Firebase App ID Firebaseコンソールのプロジェクトの設定から見れるアプリ ID 1:1234567890: android :0a1b2c3d4e5f67890 Release Notes テスターが見るリリースノート メインブランチにマージされたビルドです。 Test Groups 手順1で設定したテスターグループ product_team まとめ この記事では、BitriseとFirebase App Distributionを活用して Android アプリを配布するための手順を紹介しました。以下は、ポイントのまとめです。 事前準備が鍵 :BitriseとFirebase、 GCP のアカウント設定から始め、必要なツールと情報を確認することが重要です。 Firebase App Distributionの設定 :適切なテスターグループとテスターの設定を行い、アプリのテスト版を簡単に配布できる環境を作ります。 Bitriseの自動化設定 :ビルドプロセスをBitriseで自動化し、Firebase App Distributionを通じてアプリを配布する設定を行います。 このプロセスを通じて、テストアプリの配布が可能となります。あとはテストアプリをどのように使ってもらえるか、開発者以外のメンバーにもフィードバックをもらえるようにドキュメント化することをお勧めします。本記事が、みなさまの開発に役立つことを願っています。
アバター
こんにちは、データ統括部BIグループ所属のtakahideです。 本記事では、BIグループで取り組んでいる全社的なデータ活用に関してご紹介します。 この記事を通して、少しでも「社内のデータ活用を進めたい」と思っている方のお役に立てたら幸いです。 ※ Timee Advent Calendar2023 の12月18日分の記事です。 課題感 データ活用に役立つスキル 講習会の開催 少人数の相談会 データ活用スキルの指標化 おわりに We’re Hiring! 課題感 まずは、データ活用を進めるに至った経緯を簡単に説明させてください。 タイミーは、データを用いた意思決定を大切にしているのですが、 近年の組織拡大にともない、データ活用を全社的に推進するニーズが高まっていました。 一方で、データを利用するメンバーのスキルとニーズとの間に乖離が存在していることが分かってきました。 そこで「データ活用を全社的に推進するPJT」をBIグループ中心に立ち上げることになりました。 データ活用に役立つスキル データ活用といっても人によって解釈が異なるため、 データアナリストが日々の業務で用いているスキルの言語化から始めました。 機械学習やプログラミングといった一部の職種で使われるスキルは除外して、 どの職種の方でも役に立つ汎用的なスキルを選択しています。 「課題の発見→課題の解像度の向上→検証に必要なデータの解釈→結果のまとめ」といった流れを意識しています。 講習会の開催 スキルの整理ができたので、次にスキルの意義や使い方を伝える講習会を行うことにしました。 データの価値を実感してもらうことを優先して「データ抽出」の講習会を実施して、 顧客の行動を数値で見る体験をしてもらいました。 具体的には、簡易にデータを扱えるツールとしてLookerの講習会を開催しました。 講習会をキッカケにLookerの利用者が増え、データを見る習慣が作られたと考えています。 最初はお試しで始めた講習会も、現在は毎月実施していて、新入社員の方がデータに触れる機会を作っています。 (詳細は こちらのブログ をご覧ください) 次に行ったのが「論理的思考」「データ構造の理解」「データの解釈と可視化」の講習会です。 これらはデータ抽出の前後に用いることが多く、「データ抽出」の後に実施することで理解が深まると考えました。 下記のように、First Step→Second Stepと段階を踏んで進めてきました。 少人数の相談会 ここで、講習会を通じて見えてきた課題とその対応をお伝えできたらと思います。 課題として分かったことは大きく二つで、 「講習会の難易度」と「質疑応答のしづらさ」になります。 「講習会の難易度」ですが、 講習会では必要最低限の内容をコンテンツに盛り込んでいるものの、 参加者の習熟度の違いを吸収しきれていませんでした。 また、「質疑応答のしづらさ」ですが、 講習会の参加人数が50~100人になるため、質問しづらい雰囲気がありました。 特に、どのレベルの質問をして良いかが分かりづらいという声が挙がりました。 これらを踏まえて、少人数の相談会を進めようとしています。 テーマを絞った数人〜十数人の会にすることで、活発な意見が出ることを期待しています。 全社向けの講習会を定期開催しつつ、補完的に相談会を実施することで、 講習会の内容の充実にも繋がると考えています。 データ活用スキルの指標化 最後に、データ活用の測定に関してお話しできたらと思います。 全社のデータ活用を推進する上で、データ活用スキルの習熟度の把握が重要になります。 タイミー全体でデータ活用がどの程度進んでいるかが分かることで、 データ活用のPDCAが回しやすくなり、スキル習得のモチベーションにも繋がると考えます。 おわりに この記事では、BIグループが実施している、データ活用の取り組みを紹介しました。 改めて「社内のデータ活用を進めたい」と思っている方のお役に少しでも貢献できていたら嬉しいです。 今回は全体の紹介で、個別の詳細はお伝えできていなかったので、 そちらも機会がありましたらお話しできたらと思います。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
アバター
好きな水風呂の温度は16℃でお馴染み edy69x です。 Timee Advent Calendar 2023 の16日目を担当します。 本記事では今年完遂したUIリニューアル(SPA化)を通してタイミーで実施した工夫や学びを普段バックエンドの開発を担当する私の視点からお伝えします。 先日のイベントでの登壇内容を補完した内容となっています。気になる方は下記資料もご覧ください。 speakerdeck.com イベントの方はプロジェクト終盤での断捨離やリファクタリングなどがテーマになっていたので本記事ではプロジェクト進行過程全般での知見をシェアしていきます。 プロジェクト概要 まずプロジェクトの概要です。大雑把に言うとフロントエンドの技術基盤を移行しながらUIリニューアルを実施しました。 それだけだと「何のことだ?」となるので前提からご説明します。 タイミーでは単発のアルバイト求人の掲載を行う事業者用のWeb管理画面を提供しています。 リニューアル前はRailsエンジニアがテンプレートエンジンのSlimを活用し、フロントエンド開発も担っていました。Railsで作られたサーバは動的に生成したHTMLをレンダリングする責務も持っていました。いわゆる Server Side Rendering(SSR) です。タイミー創業当初から変わらない技術スタックで構成されていて価値も生み出し続けていました。 しかし、サービス拡大にともない、古くから存在する Model や Controller への継ぎ足しの実装などが生まれ、Railsアプリケーションのバックエンドとフロントエンドの密結合なロジックが増えたり Fat Controller に頭を悩ませられるシーンが発生し始めました。 一方のフロントエンドでは保守性や情報設計(Information Architecture)を始めとして品質向上を狙ったり、jQueryに代わるモダンWebフロント技術の導入、またフロントエンド専任のエンジニアの採用拡大も狙っていました。 上記の理由などからUIリニューアル及びフロントエンドの技術基盤を刷新するプロジェクトがスタートしました。 フロントエンドの技術は下記の構成で、Railsのリポジトリとは別で切り出しました。 言語: TypeScript ライブラリ: React フレームワーク: Next.js APIドキュメント: OpenAPI (Swagger) Webページ構成やレンダリング手法もこのタイミングから Server Side Rendering(SSR) から Single Page Application(SPA) に変更しました。 いざ始まったUIリニューアル UIリニューアルを進める上で不確実性を大いに孕んでいたので管理画面全体を一気にビッグバンリリース的に旧バージョンから新バージョンに切り替える方針は見送りました。 方針としてはハレーションを抑えるためにアクセス数の少ない画面から徐々にリプレイスしていき、顧客が最も利用する事業のコア機能を終盤にリプレイスする計画を立てました。 同時にDatadogやGoogleAnalyticsなどの監視・分析ツールを用いて、アクセス数の解析、利用デバイス比率の特定をしたり、売上規模の上位を占める事業者を選定し、移行に際して障壁となるポイントが無いかを探っていきました。 従来デザイン使い続けたい問題 進行する中で発見されたのが「従来のデザインを使い続けたい」というユーザーニーズです。 背景には、既存UI/UXに対してのメンタルモデルが形成されているので変化を忌避してしまったり、画面構成やボタン単位で指定オペレーションを厳密に組んでいるケースなどがありました。 強行突破する方法もあると思いますが、打ち手として新デザインのリリース後も従来の旧UIに切り替えられる仕組みを導入しました。デバイス変更やセッション切れ等で、従来デザインを選択したユーザーが新デザインに切り戻されることを回避したり、データ分析観点で活用するために従来デザインを選択した状態はDBに永続化しました。 プロジェクト終了時には削除する前提だったので明確さやアクセスのし易さを優先しました。フロントエンドは使用状況をAPI経由で取得する方針を採用しています。 [従来のデザインに切り替える] ボタンを配置 この仕組みのお陰でリリース後に利用者に多少影響があっても完全にロールバックすることなく、利用者ごとの判断に委ねることが出来ました。新デザインを許容できる利用者には新デザインを提供し、どうしても従来のバージョンを利用したい方には一定期間の従来バージョン利用を許容する運用をしました。 事前告知による期待値調整も勿論大事だと思うのですが、石橋を叩きすぎずにリリース後の ”動くソフトウェア” を通しての実際のユーザーのフィードバックから次の意思決定に繋げられるアプローチを経験できたのは大きな財産になりました。 画面移行の約80~90%程度が終わる頃には新デザインがマジョリティになったので、上記機能はほぼ役目を終え勇退しました。 影響範囲を限定する話での余談ですが、カナリアリリースの仕組みがあると影響を抑えられた障害などもあったので今思い返すと導入しておけると良かったです(プロジェクト中には追加出来なかったので今後導入していきたい)。 ユースケースロジックを切り出すFormObjectを活用 プロジェクトも佳境に差し掛かった中で、とあるエンドポイントに対応するAPIを実装する工程が最も難を極めました。タイミーのコアドメインに関わる機能を司っている Controller のリプレイスです。元々が Fat Controller だったケースでして既存の仕様理解や根本的なリファクタリングが求められたのを覚えています。 リクエストを受けてからHTMLを返却する一連の処理の中に、データベースへの書き込みや外部APIへの通信、ビジネスロジックに関係する処理など様々な責務を Controller が抱えていました。テスタビリティも低く、RSpecで書いていた controller spec では仕様は網羅されていませんでした。 その際に Controller の責務を一部切り出す方法として FormObject を採用しました。ユースケースに対応したロジックを FormObject クラスに閉じ込め、Controllerはクラスのパブリックメソッドの呼び出しとリクエストの受付、及び適切なレスポンスの返却に特化する様な作りにしました。 FormObject には単に移行するだけではなく「テストピラミッド」も意識し、テストケースの割合が結合テスト < ユニットテスト になるように必要に応じて責務の見直しも行いました。 テストピラミッド 結果的に Fat Controller は消え去り、元々はAPIリクエストの受付からレスポンスまでの過程を通して結合テストでカバーしていたテストケースをユニットテストで再現できるようになりました。Controller は RSpec の request spec にて受け付けるリクエストとレスポンスを明示し、副作用のうち代表的なケースをテストすることで不要に FormObject 側のユニットテストと重複することも回避しました。 Controller の上部に君臨し続けていたTODOコメントを削除できたときは非常に感慨深かったことを今でも覚えています。 古来からタイミーを見守ってきた長老の様なTODOコメント プロジェクトの中断 これは少しオフトピック的な話ですがプロジェクト進行のメタ的な話として触れておきます。 一般的にプロジェクトのみならず仕事を進める中で優先度が高い別Issueが割り込まれることはありますよね。今回のUIリニューアルプロジェクトでも起きました。 プロジェクトの性質的には「技術改善」をテーマとし基本的に従来機能を踏襲しながらUIリニューアルを進行する前提でした。そのため、割り込み的な機能開発に対してスイッチングコストが発生したり、リニューアルとの優先順位付けが必要になったりと苦労した覚えがあります。移行期間全体が約3年あったうち合計1年ほどはチーム構成やミッションが根本から変化し、別テーマの開発を担当する時期もありました。 結果として生存期間が短い前提で作られた機構が想定より長く存在し続けることになりました。例えば先に述べたデザイン切り替えの仕組みや、Rails, Next.js それぞれのリポジトリが互いに依存する様な暫定的なロジックなどです。 当時の自分はイレギュラーに対してある種「当たり前」くらいで考えていたのですが、水面下ではコードベース上に認知負荷を高める要因が生まれていました。新規参画メンバーから質問が来ることもありましたが、当時は影響を理解したり、言語化することは出来ていませんでした。 今思い返すと内部品質低下に繋がることから、プロジェクト完遂までのリードタイムを短縮したり、テーマが変化する前に最低限の内部品質改善をするように働きかける余地はあったなと感じます。 プロジェクトが順調に進んだり完了することだけが学びではなく、むしろ想定外の中に自分の枠を超えた学びのきっかけがあると気付かされた瞬間でした。 プロジェクト完遂 過程では様々な事があり、ここでは語り切れないのですが3年の歳月を経てプロジェクトは完遂しました。RailsリポジトリからはHTMLレンダリングにまつわるコードやCSSライブラリなど5万行を超えるコードが削除されました。特に1つのPull Requestで1万行を超えるコード削除をした時は非常に爽快でした。 Webpackerやyarnなどにも別れを告げ、外部顧客向けの機能についてはRailsリポジトリはWebAPIの開発に特化する様な存在になっています。GraphQL化を進める様なチャレンジも生まれました。 WebFrontも1名でプロジェクトがスタートしましたが今では技術コミュニティが生まれたり、技術顧問の方が付いていたりと2020年当時と比べてだいぶ拡大しています。いい話ですね。 おわりに プロジェクトが終われど、フロントエンドにとってはスタートラインに立ったとも言えますし、バックエンド側も保守性が多少増したとは言え、会社の成長を支える面でいうとまだまだ改善余地があります。 会社に興味がある方がいたら、タイミーワーカーで運営が成り立っている THE 赤提灯 という居酒屋が新橋オフィス近くにあるので是非飲みに行きましょう。 一応採用ページも貼っておきます。 product-recruit.timee.co.jp 忘れていましたが、声を大にして改めて言っておきたい。 「タイミーはSPA化やり遂げたぞ!!!!」 完
アバター
こちらは Timee Advent Calendar 2023 シリーズ1の15日目の記事になります。 こんにちは、タイミーで Android エンジニアとして働いている @orerus こと村田です。 私は現在タイミーのAndroidChapter(弊社は特定領域のメンバーの集まりのことをChapterと呼称しています)の一員で、喜ばしいことに来年早々にメンバーが大きく増加する予定です・・・! 以前はAndroidChapterのメンバーが全員同じチームで開発を行っていましたが、タイミーのエンジニア組織拡大に伴い全員が異なる開発チーム(弊社ではSquadと呼称しています)に所属する形へと変化しました。その為、各Squadには Android エンジニアが少人数しか所属しておらず、 Android アプリに関するPRレビューについてはSquadを跨いで行うことになります。必然、故も知らないPRレビューが飛んでくる為、PRレビューの負荷が上がり 工数 的にも 心理的 にも辛いものとなってしまいました。 そこで今回は、そういった組織のスケールに伴い発生する課題解決の為の施策の一環として、弊社AndroidChatperで導入した独自のLADR(Lightweight Architecture Decision Records)について紹介したいと思います。 また、導入して得た学びや見えてきた改善点についても共有できればと思います。 施策の背景、課題 独自LADRについて LADRのサンプル LADR導入の経緯 導入後の成果と学び、および見えてきた改善点 AndroidChapterメンバーからのFB 自身の振り返り 学びと改善点のまとめ 当初抱えていた課題は解決できたのか 施策の背景、課題 先述の通り、タイミーでは現在 Android メンバーが全員異なるSquadに所属し、それぞれが異なるプロダクトゴールに向けた機能開発を行っています。 AndroidChapterで毎日10分程度のデイリー MTG は行っていますが、そこでは現在取り組んでいるもの・これから触るコード領域、困りごと等の共有に留まります。(各Squadのデイリー スクラム もある為、あえて軽い内容にしてあります) そのような状態で開発サイクルを回す中で、いくつかの大きな課題が見えてきました。 PRレビュー レビューに入る前に、PRが行っている開発内容を把握する為の多大な コンテキストスイッチ コストがかかる コンテキストを十分に理解できていない為、仕様や設計の妥当性の判断が困難で表面的なレビューになりがち PRという完成形で初めてレビュー依頼が飛んでくる為、仕様や設計で致命的な問題があった場合に後戻りが難しい(修正コストが高い) PRレビューコストが高い為、PRレビューを行う際にまとまった時間を確保する必要がありレビュー完了までのリードタイムが長くなる 改修/影響調査 昔のコードや自身が触っていないコードについて、開発当初の意思決定根拠や設計意図が分からず、影響調査に時間がかかったり変更妥当性の判断が難しい そこで、これらの課題を解決するために実験的に導入を始めたのが独自LADRでした。 独自LADRについて ここでは我がAndroidChapterで定義している独自LADRについて紹介します。なお、ここでいうLADRは弊社AndroidChapter用に独自改変したLADRですが、AndroidChapter内では単に「LADR」と呼称している為、この記事でも以後は単にLADRと記述します。 そんなAndroidChapterで導入している独自LADRですが、株式会社 ラク スの鈴木さんが公開されている LADR-template をベースにさせていただいています。 また、 鈴木さんの記事 にある通り、LADRの本来の用途は アーキテクチャ 選定において「導入しなかった」記録を残す為であると認識していますが、AndroidChapterではLADRの目的や対象をそこから拡大解釈して利用しています。 目的や対象を具体的にどのように拡大解釈しLADRを運用しているかについては、実際に私が作成した「LADRを導入する為のLADR」を以下に添付しますので、こちらをご覧いただければ雰囲気が掴めるかと思います。(「Context - 文脈」「Specification - 仕様、やること、やらないこと」の項目のみご覧いただければ十分です) ご覧の通り、LADRのドキュメント自体は軽量なものになっている事が分かるかと思います。ここがLightweight形式を採用した最大の理由でして、ハードルを下げることでまずはこのドキュメントが書かれることを一番の狙いとしています。 LADRのサンプル 先ほどお見せしたLADRは機能開発を伴うものではなかった為にいくつか項目を省略してますが、機能開発時のLADRでは必要に応じて詳細な仕様を記述したり、実装時の設計についても触れています。 以下、実際に作成されたLADRのサンプルです。(雰囲気が伝わるようにシンプルなものと重めなものを抜粋しています) サンプルA. 機能追加のためのLADR サンプルB. 不具合修正のためのLADR このようなLADRを作成した後、AndroidChapterにLADRのレビュー依頼をPRとして投げ、そこでLADRに書かれたコンテキストや仕様、設計について 開発着手前に レビューを受けるようにしています。 LADR導入の経緯 先述のLADRについての記事を拝見したときに「これだ!」と思い、すぐさま「LADR導入の為のLADR」、および「LADRのテンプレート」を作成し、 Android メンバーにPRという形で投げつけました。LADRについて解説するより、まず実物を見せ、そこから話をするのが早いと感じた為です。 PR上でLADRの狙いや、他ドキュメントとの棲み分けについてなどの多少のラリーはありましたが、好意的に受け入れられすんなりと実験導入が始まりました。 運用を初めて2ヶ月程度ですが、現在以下の18個のLADRが作成されています。 導入後の成果と学び、および見えてきた改善点 AndroidChapterメンバーからのFB まずは2ヶ月運用してきて、AndroidChapterのメンバーはどう感じているのかを探るためにアンケートを実施してみました。 アンケート結果について、AndroidChapterメンバーの中でLADR作成経験済み、作成機会がまだ無い状態の2人の回答を紹介します。 「ありのままを記事に書きたいので率直に書いてね」とお願いをしておいたので、きっちりと本音が書かれているかと思います・・・! ※なお、アンケートの設問はChatGPT先生にご協力願いました その結果がこちらです。 まずは分かりやすく数値で評価してもらいました。 一番右の私の回答分はバイアスがかかりまくっているのでスルーして、他メンバーの評価は以下のような感じです。 満足度はどちらも7と「やや良い」点数 プロセスの効率化や課題解決度合いの感じ方はLADR作成機会があったメンバーは全体的に高め、機会が無かったメンバーは低め Bの方が「LADR導入によって解決された課題の効果度」の項目の3点について、「まだ自身で作成したことがないために自分事感が薄い」とのFBをもらっています 次に、フリーテキストで回答してもらったものを 原文ママ で掲載します。 ※2人分の回答をマージしています まずは 導入による成果 についての質問についてです。 こちらにより以下のことが言えそうです A、Bの両者ともに「コンテキスト共有」については効果を明確に感じている LADRを作成する機会があったメンバーは仕様/思考の整理に効果を感じている LADRを作成する機会がなかったメンバーもPRレビューの効率化には効果を感じている ただし、自身でLADR作成を行っていないと課題解決の実感が薄い 次に 改善点 についての質問です。 こちらにより以下のことが言えそうです LADRの制度自体の課題点はまだ発生していない 一方、ドキュメントの参照のしやすさ、ドキュメント作成のオンボーディングといったLADRの活用方法についての改善点が挙がっている この点は当初から想像していた懸念点であった為、今後の明確な改善点だといえる 組織がスケールしてもコンテキスト共有に関する課題解決の効果が見込めそう 自身の振り返り また、私自身が感じているLADRのメリットデメリットも軽く挙げておきます。 メリット 何にせよ意思決定のログが残るようになった メンテを保証できない仕様書にするより、意思決定のログと割り切って扱っている PR作成時のコンテキスト共有が楽になったように感じた 「実装→ドキュメントを書く」が「ドキュメントを書く→実装」と順序が変わったのが大きいのかもしれない 設計段階の指摘や懸念点の伝達が事前にできるようになった 実装者以外のメンバーのコンテキストの理解度合いが深くなったように感じた デメリット LADRの作成の 工数 がかかる 以前もPR本文に一定のコンテキストを書いていたので、劇的に 工数 が増加した訳では無い LADRのPR、機能開発のPRと、レビューが必要となるPRの数が増える 学びと改善点のまとめ 以上から、学びと改善点について乱暴にまとめると以下のようなことが言えるかと思います。 課題解決に一定の効果は確実に出ており、デメリットを上回るメリットがあるといえる LADRドキュメントの参照性に課題がある LADRドキュメントを作成するまでのハードルがまだ感じられる オンボーディング時に1度作成してもらう等のケアが検討余地としてありそう 当初抱えていた課題は解決できたのか 最後に、当初挙げていた課題がどの程度解決できたかの所感をまとめます。 課題1 : レビューに入る前に、PRが行っている開発内容を把握するための コンテキストスイッチ コストがかかる 解決状況 : ✅ 完全に解決 課題2 : コンテキストを十分に理解できず、仕様や設計の妥当性判断が困難 解決状況 : ✅ 完全に解決 課題3 : PRという完成形で初めてレビュー依頼が飛んでくる為、仕様や設計で致命的な問題があった場合の修正コストが高い 解決状況 : 🤔 様子見 現状では問題なし。設計難易度の高い開発での対応を今後注視 課題4 : PRレビューコストが高く、レビュー完了までのリードタイムが長い 解決状況 : 🟢 一定の解決 PRレビューコスト減少、ラリーの回数も減少 課題5 : 昔のコードや自身が触っていないコードの開発意図が分かりづらい 解決状況 : 🟢 様子見、ただし解決の兆しあり まだ運用期間が短く見返す機会が少ないが、LADRのルールを見直す際に当初作成したLADRドキュメントが役立った 結論として、全体的には解決に向かっていると感じています。ただし、組織のスケールアップに対する対応として導入した施策であるため、今後も組織がスケールした際の変化に注視しつつ、より良い形へと進化をさせ続けていく所存です。
アバター
※ Timeeのカレンダー | Advent Calendar 2023 - Qiita の12月13日分の記事です。 はじめに こんにちは okodoooooonです dbtユーザーの皆さん。dbtモデルのbuild、どうやって分割して実行してますか? 何かしらの方針に従って分割をすることなく、毎回全件ビルドをするような運用方針だと使い勝手が悪かったりするんじゃないかなあと思います。 現在進行中のdbtのもろもろの環境をいい感じにするプロジェクトの中で、Jobの分割実行について考える機会があったので、現状考えている設計と思考を公開します!(弊社は一般的なレイヤー設計に従っている方だと思うのでJob構成の参考にしやすいと思います) この辺をテーマに語られていることってあんまりないなあと思ったので、ファーストペンギンとして衆目に晒すことで、いい感じのフィードバックをもらえたらなーと思ってます。 弊社のデータ基盤全体のデザインについて把握してからの方が読みやすいと思うので、そちらをご覧になりたい方は こちらの記事 からご覧ください。 これまでの弊社のデータ更新周りについて これまでの弊社のdbt Job設計の概念図を作るとしたらこんな感じになります。 走っているクエリがフォルダ単位/モデルファイル単位などバラバラな粒度で実行されている リネージュを意識したJobの走らせ方になっておらず更新頻度の噛み合わせが悪い 明確なルールがないので意図しないタイミングで同じモデルがビルドされたりする 明確なルールがないのでビルドから漏れているモデルがひっそり存在している dbtコマンドで示すとこんな具合のカオス感 dbt build --select models/path_to_model_dir1/model1-1.sql dbt build --select resource_name dbt build --select models/path_to_model_dir2/model2-1.sql dbt build --select models/path_to_model_dir3 .... 改善のための試行錯誤 v1 レイヤーごとにtagを付与して以下のようなビルドコマンドが走らせられるようにしました。 dbt build --select tag:staging_layer dbt build --select tag:dwh_layer dbt build --select tag:dm_layer 更新頻度tagの概念を設計して、以下のようなビルドコマンドを走らせられる構成を検討しました。 dbt build --select tag:hourly_00 # 毎時00分にビルドするものに付与するタグ dbt build --select tag:daily_09 # 毎日9時にビルドするものに付与するタグ これらの設計をしてみて、レイヤー単位でJobを走らせることを可能にできたのと、更新頻度をdbt上で管理可能な形にはできました。 しかし、ユーザーのデータ更新ニーズに基づいたJob設計になっていないことから、ユーザーニーズベースでのJob管理方針の検討を進めました。 データ活用ユーザーのデータ更新ニーズの整理 弊社のレイヤー設計とデータ活用ユーザーのアクセス範囲は以下のような形になります。 アナリストはステージング層以降の層に対するアクセス権を保持していますが、現状アナリスト業務はモデリング済みテーブルではなく、3NFテーブルに対するクエリを作成する作業が支配的です(モデリング済みテーブルの拡充とその布教をもっと頑張りたい)。 データ組織外の社内ユーザーはデータ組織が作成したアウトプットを通して分析基盤のデータを活用できるような状態になっています。 ※弊社内ではデータ組織以外のユーザーにBigQueryのクエリ環境を開放しておらず、LookerやLookerStudioなどのアウトプットを経由して社内データにアクセスする状態にしています。 それぞれの層ごとの現在顕在化しているデータ更新ニーズを整理すると以下のようになります。 アナリストと野良アウトプットによる「ステージング層のなるはや更新ニーズ」 アナリストのアドホッククエリ 分析要件は予測ができず、要件次第では最新のデータが必要となるため、なるべく早く更新されることが期待されます。 マートから作成されずステージングテーブルから作られるアウトプット 更新頻度ニーズのすり合わせなく作られて提供されるものも多く、要件次第では最新のデータが必要となるため、なるべく早く更新されることが期待されます。 データ活用ユーザーによる「アウトプット更新ニーズ」 データ活用ユーザーに提供するデータマート経由のアウトプットは更新頻度を擦り合わせられているものだけになっている(今はまだ数が少ないだけだが)ため、hourly,daily,weekly,monthlyなどの頻度ごとの更新設計が可能となっています。 例えば弊社のよく使われるLooker環境はhourlyの頻度で更新が要求されています。 単純なジョブ設計をしてみる 単純な ジョブ実行案① ステージングなるはや更新プラン ソースシステムごとのデータ輸送完了後にそのソースシステムを参照しているモデルを全てビルドするプラン コマンド例 salesforceのデータ輸送完了後のコマンド: dbt build --select tag:salesforce_source+ dbt build --select models/staging/salesforce+ アプリケーションDBのデータ輸送完了後のコマンド: dbt build --select tag:app_source+ dbt build --select models/staging/app_db+ 実装方法 airflowなど何かしらのオーケストレーションツールで転送完了をトリガーに下流一括ビルドのコマンドを実行する必要がある。 この場合の更新ジョブを概念図に表すと以下のようなものになると思います(上流から下流へのビルド時のデータリネージュを黄色の三角形で表現しています)。 メリデメをまとめると以下のようになるかと思います メリット 下流が全てビルドされるのでビルド漏れが発生しない。 常にモデルの鮮度が最新になる = アナリストのstaging最新化ニーズは満たせている デメリット 更新がそこまで不要なモデルに対してまでビルドが実行される。 BigQueryのクエリ料金が高額になる 単純な ジョブ実行案② ユーザーニーズに合わせるプラン ユーザーのデータ更新ニーズに合わせて更新頻度をアウトプットごとに設定。更新ニーズの頻度ごとに最新化されるようなプラン コマンド例 1時間おきにビルドされて欲しいアウトプットを更新するコマンド: dbt build --select +tag:hourly 1日おきにビルドされて欲しいアウトプットを更新するコマンド: dbt build --select +tag:daily この場合の更新ジョブを概念図に表すと以下のようなものになります(下流から上流へのビルド時のデータリネージュを赤色の三角形で表現しています)。 メリデメをまとめると以下のようになります。 メリット:アウトプットの更新頻度が最適化される。 デメリット stagingのモデル作成の重複が発生する。 出力ニーズが存在しないstagingテーブルなどがビルド対象から漏れてしまう ではジョブ分割をどのように設計するか ステージングなるはや更新に合わせてリネージュの上流から実行すると下流で無駄なビルドが発生してしまう。 ユーザーニーズベースでリネージュの下流から実行すると上流で無駄なビルドが発生してしまう。 この二つの問題の折衷案を考える必要があると考えました。 現状のジョブ実行案 更新ジョブの概念図は以下のようなものになります(上流から下流へのビルド時のデータリネージュを黄色の三角形、下流から上流へのビルド時のデータリネージュを赤色の三角形で表現しています)。 ソースデータ ~ ステージング層のビルド ソースデータが連携されたらステージング層までを下流に向かってビルド コマンド: dbt build --select tags:source_a+ --exclude tags:dwh_layer tags:dm_layer DWH層~アウトプットの間のビルド ステークホルダーと定めた更新頻度に合わせて上流に向かってビルド コマンド: dbt build —select +tags:daily_00_00 —exclude tags:staging_layer tags:datalake_layer こうすることで、ステージング層までは常に可能な限り最新化されつつ、アウトプットは要件ごとに更新頻度が最適化された状態となります。 ただ、DWH層やデータマート層に多少のビルドの重複は発生してしまいますが、そこは許容しています。 本当はこうしたい案 アナリストがモデリング済みのDWH層を使うことが常態化するような世界観になってくると、上図のように「レイク~DWHまでは最新鮮度」「それ以降はユーザーニーズベースで更新」って流れがいいのかなと思っています。 また source_status:fresher+ などのstate管理をうまく使って、更新があったものだけをビルドするような方式を模索していきたいです。 おわりに dbtの環境リプレイスとともにこのJob設計も実戦投入しようと考えているので、想定していなかったデメリットが発覚したり、改善点が見つかったら改善していこうと思います! 弊社データ基盤でもストリーミングデータが取り扱えるようになったので、そのデータの使用が本格化すると ストリーミング✖️バッチ のJob構成などを考える必要があり、まだまだ俺たちの戦いはこれからだ。と思います ていうかみなさんどうやって分割して実行してるの!!!教えてほしすぎる We’re Hiring タイミーのデータ統括部はやることがまだまだいっぱいで仲間を募集しています!興味のある募集があればこちらから是非是非ご応募ください。 product-recruit.timee.co.jp
アバター
こんにちは、タイミーのデータ統括部、DRE(Data Reliability Engineer)グループ & DS(Data Science)グループ所属の筑紫です。 DSグループではML基盤の構築・運用保守を担当しています。 本記事では、ML基盤を再構築した話を紹介したいと思います。 ※ Timee Advent Calendar2023 のシリーズ 2の12月13日分の記事です。 経緯と課題 DSグループでは、様々なプロジェクトのML基盤を構築しています。 当初は、1つのCloud Composerの上に、全てのpipelineを載せて運用していました。 以下に当初の構成のイメージ図を示します。 ただ、このML基盤についてはいくつか課題がありました。 本番環境しかない 本番稼働しているpipelineが複数ある中、環境が本番環境しかありませんでした。 Cloud Composerも本番の インスタンス 1つしかなく、例えばCloud Composerのimageのupdateが必要な際もupdateして既存本番に影響がないか検証できる環境がない、などの問題が発生しており、保守に耐えられない状況でした。 サービスレベルの異なるpipelineが同じワークフロー上に乗っている 当時ML基盤の予測結果を社外に提供する取り組みも開始している中で、社内利用のML pipelineと社外利用のML pipelineが同じワークフローツール(Cloud Composer)上に乗っている状況でした。 ML基盤でもその結果を社内で使用するものと社外で使用するものでは、サービスレベルが変わってきます。 異なるサービスレベルを持つ複数のpipelineが一つのワークフローに乗っている状況では、あるpipelineの挙動が他のpipelineにも影響を及ぼし、サービスレベルに影響を与える可能性があるため、運用上好ましくありませんでした。 インフラ反映までのプロセスが長い GoogleCloud環境はTerraformで管理していますが、その リポジトリ はDREグループ管理になっており、DSグループがリソースを作成する際に、以下の手順を踏む必要がありました。 DSグループで修正のPullRequest(以降PR)を作成 DREグループにレビューを依頼 DREグループのレビュー通過後にマージ & デプロイされるので、反映結果を確認 このような手順を踏むので、 検証のために、試しにリソースを作成したり変更したりする際に、都度PRを作成し、レビュー依頼しないといけない 他チームのレビューを挟むので、GoogleCloudのリソースを作成するまで、リードタイムがかかりがち という問題があり、DX(Developer eXperience)に影響を与えていました。 ソリューション まず本番環境しかない問題については、開発環境を作り、Cloud Composerを本番環境、開発環境それぞれに配置することにしました。 それに合わせてGoogleCloudのprojectも分けることにしました。 1つのproject上に本番環境と開発環境を構成するよりも、分けた方が事故が起こりにくく、Terraform上の管理もしやすかったためです。 また、サービスレベルの異なるpipelineが同じワークフローに載っている問題について、サービスレベルが大きく異なる ユースケース ごとにCloud Composerを分けることにしました。 コストとの トレードオフ になりますが、運用・保守のしやすさを考えると、社内で使う ユースケース と社外などConsumerなどに提供する ユースケース でCloud Composerを分けた方が良いという判断からこの構成にしました。 結果、下図のような構成になりました。 その上で、上記環境のTerraformのコードをDRE管理の リポジトリ と分けて、DSグループ管理の別 リポジトリ で管理する方針としました。 また、ブランチ戦略を整理して、featureブランチにpushする度に開発環境にデプロイされることにしました。 これにより、デプロイ都度レビューを挟まないといけないフローが、featureブランチにpushするだけでデプロイされるようになり、検証までのリードタイムを改善できたと思ってます。 結果 最終的に、数ヶ月かけてこの新環境の構築とpipelineの移行を行いました。 結果、上記の3つの課題については、解消することができたと思ってます。 特に、開発環境ができたことと、DSグループ管理のTerraform リポジトリ ができたこと、featureブランチのpushでデプロイできるようになったことから、インフラの検証のしやすさは格段に上がったと思っており、DSグループ内のDX向上に寄与できたのではと思っております。 We’re Hiring タイミーのデータ統括部では、データサイエンティスト、DRE、Data Analystなど、様々な職種のメンバーを募集してます! https://hrmos.co/pages/timee/jobs カジュアル面談 からでも対応できますので、少しでも気になった方は申し込み頂けると嬉しいです!
アバター
こちらは Timee Advent Calendar 2023 の13日目の記事です。 タイミーでバックエンドエンジニアをしている @Juju_62q です。 記事内でワーカーさんや事業者さんに関して敬称を省略させていただきます。 タイミーは雇用者である事業者に求人を投稿してもらい、労働者であるワーカーが求人を選ぶという形でマッチングを実現しています。ワーカーが求人を選ぶためにはなんらかの形でワーカーが自分にあった求人を見つけられる必要があります。検索はワーカーが求人を見つけるために最もよく使われる経路です。今回はそんな検索機能において今後の開発をスムーズにするための リファクタリング を実施した話を紹介します。 背景 リファクタリングの切り口 実施したリファクタリング before after 結果と所感 終わりに 背景 タイミーの検索は以下の機能です。 ワーカーの状況や希望に合わせて求人を表示しています。求人はサーバーサイドで取得、絞り込みを行っています。この絞り込みはFilterというクラスが一手に担っていました。Filterクラスでは具体的に以下の処理を行なっています。 該当日の全ての求人を取得する ワーカーや事業者の設定した絞り込み条件やタイミーのルールに合わせて絞り込みを行う 前述した通り検索はマッチングが最も生まれている 動線 です。そのために機能追加の圧力も非常に大きく、さまざまな機能が追加されていきました。結果としてFilterクラスは見通しが悪くなっていきました。最大では16の絞り込み処理が1つのクラスで行われていました。 また、このFilterクラスですが利用箇所が検索だけであればいいものの、タイミーのルールに合わせた絞り込みを行なっていたために検索以外にもお気に入り求人やお仕事リク エス トなどさまざまな機能に利用されています。 ただでさえ問題を起こした時に大きくなってしまう検索機能です。コードの見通しが悪く、影響範囲も不透明となると変更するのもどんどん億劫になってきます。大切な機能であるからこそチームとして改修をした方が今後も含めて開発が早くなるだろうと考えて リファクタリング に踏み切りました。 リファクタリング の切り口 見通しが悪いのは絞り込み処理を行なっているFilterクラスなので、Filterクラスに絞って リファクタリング を検討します。Filterクラスがどんな処理を担っているのかをチーム内で話しました。 取得部分を一旦置いておくと、Filterクラスの絞り込みには大きく以下の2種類があるとわかりました。 ワーカーが設定した検索条件による絞り込み(10処理) 良いマッチングを生むためにタイミーが実施している可視性の制御(7処理) また、Filterクラスを使っている箇所を眺めてみても検索では1, 2の両方を利用しているものの、それ以外の箇所では2の「良いマッチングを生むためにタイミーが実施している可視性の制御」しか利用していませんでした。 これを踏まえて3つの要素を満たすような リファクタリング をすることにしました。 「ワーカーが設定した検索条件による絞り込み」と「良いマッチングを生むためにタイミーが実施している可視性の制御」の2つに処理が分かれること 今後の開発で処理の要素を既存処理に影響を与えずに3つ以上にできること 利用するクラスが適用するロジックを選択できること なぜこれらの要素を選択したかを解説します。まず ”「ワーカーが設定した検索条件による絞り込み」と「良いマッチングを生むためにタイミーが実施している可視性の制御」の2つに処理が分かれること”です。まず、ロジックを精査する前にFilterクラスの見通しが悪いということは単一責任の原則に反していると考えていました。実際に話し合ったところ2つの責務が見えてきたので責務が単一になるように分割することを考えます。 次にその他2つに関してはオープンクローズドの原則を考えたものです。タイミーのフィルタリングロジックはまだまだ発展途上で、今後どんどん拡張される可能性があります。その時に簡単に処理を追加・削除できる状況を作ることを考えると重要であると考えました。 実施した リファクタリング 結論から行くと、以下のようなbefore-afterとなっています。 Offeringは求人を表現しているモデルで、Userはワーカーです。 before クラス図 Filterクラスの利用例 offerings = :: Offering :: Filter .new(current_user, search_params) .result after クラス図 Filterクラスの利用例 offerings = :: Offering :: Filter .new( conditions : [ :: Offering :: Filter :: SearchCondition .new(search_params), :: Offering :: Filter :: Viewable .new(current_user) ]) .result よくみるとわかるのですが、ストラテジパターンを用いて リファクタリング をすることとなりました。SearchConditionクラスが「ワーカーが設定した検索条件による絞り込み」を担い、Viewableクラスが「良いマッチングを生むためにタイミーが実施している可視性の制御」を担っています。 この変更によって元々考えていた要素は以下のように達成されました。 「ワーカーが設定した検索条件による絞り込み」と「良いマッチングを生むためにタイミーが実施している可視性の制御」の2つに処理が分かれること → SearchConditionとViewableの2つに分かれた 今後の開発で処理の要素を既存処理に影響を与えずに3つ以上にできること → Conditionのインターフェースを持ったクラスは追加可能、Conditionを追加しても他のクラスに影響はない 利用するクラスが適用するロジックを選択できること → 引数でConditionを選択することで制御が可能 クラスや省略したコードだけ見るとかなり複雑に見えますが、まとまりごとで制御できるので全体としてはすっきりしたと思います。 結果と所感 今回の リファクタリング によって検索にかなり変更を加えやすくなりました。実際に リファクタリング 後に検索を3度ほど触っていますがかなり機能の追加はしやすくなりました。早くロジックが大きくして、いろんなConditionが見つかると良いなと思っています。 ここまでの話で特に話していませんでしたが、この リファクタリング は スクラム 開発の中でプロダクトゴールを最速で達成する一環として実施しています。品質が開発速度に好影響を与えた例としてとても感慨深かったことを覚えています。 ところで、 Ruby でストラテジパターンを実現する時って皆さんどうしていますか?今回は以下のような抽象クラスのようなものを作成しました。ただ、今回の用途で継承を利用するのはToo muchであると思います。何かいい方法があれば教えていただけると嬉しいです。 # frozen_string_literal: true class Offering class Filter class Condition # 一定の検索条件で絞り込むために利用される。 # Offering::Filterクラスで利用するinterfaceとなる。 # # @param [Offering::ActiveRecord_Relation] offerings # @return [Offering::ActiveRecord_Relation] def apply (offerings) raise NotImplementedError , " You must implement #{ self .class } # #{ __method__ }" end end end end 終わりに 今回はタイミーの検索を司るFilterクラスを リファクタリング した話を紹介しました。重要な機能には仕様がどんどん追加されます。開発がしにくいと感じた際には責務や基本的な原則に立ち返って リファクタリング してみるのもよいかなと思います。今後もユーザーへの価値提供をしながら技術品質をどんどん高めていきたいと思います。
アバター
この記事は Timee Advent Calendar 2023 シリーズ 3 の12日目の記事です。 qiita.com はじめに DREグループでデータエンジニアをやっている 西山 です。 今回は、データ転送まわりの運用自動化について書きます。 タイミーのアプリログが分析できる状態になるまでのリードタイムが長く、効果検証や意思決定に遅れが出ていた問題に対して、dbtに関連する運用を自動化することで改善しました。 タイミーでのアプリログの転送について タイミーではS3に貯まったサーバーログを定期的にデータ基盤(GCPのBigQuery)へ転送しており、ログがLake層へ追加されるとdbt(data build tool)によって型変換等の加工処理がなされてStaging層へと転送されます。 アプリの機能追加によって新しいログテーブルが追加された場合、dbtの処理追加が必要になるため、GitHub Actionsで定期的にLake層とStaging層でログテーブルの差異をチェックし、Lake層に新しく追加されたテーブルがあれば処理追加を促す通知をslackへ送信していました。 通知を確認したら、以下の対応を行います。 新しく追加されたテーブルのスキーマを確認 dbtでmodelを作成 他のログテーブルのmodelをコピー カラム名やキャスト処理を修正 {{ config( alias={{ テーブル名を書く }} ) }} {% set column_lists = [ {{ カラム名を書く }}] %} {{ production_logs_template({{ テーブル名を書く }}, column_lists) }} , result_record AS ( SELECT {{ カラム名を書く }}, dre_transfer_at, ROW_NUMBER() OVER (PARTITION BY {{ カラム名を書く }}) AS row_number FROM records ), stg_record AS ( SELECT {{ 各カラムの変換処理を書く }}, FROM result_record WHERE row_number = 1 ) SELECT *, {{ set_record_exported_at(column_lists) }} AS dre__record_exported_at, FROM stg_record チーム内レビュー dbtのジョブを実行してStging層にもテーブルを追加 基本的には通知を確認次第、上記の対応を行う方針でしたが、他に優先度の高い障害対応や他チームからの依頼があると後回しになってしまうことも多く、1週間以内にStaging層へ新規ログテーブルを転送する取り決めとなっていました。 しかし、これだとログが分析できるようになるまでのリードタイムが長く、プロダクト開発チームの効果検証や意思決定に遅れが出てしまいます。 そこで、今回はこのリードタイムの短縮を目指すことにします。 実装案 アプリのログに関しては、dbtで実装する加工処理の内容がほぼ決まりきっているため、modelの追加が自動化できそうです。 (例) 末尾にidが付くカラムはINT型へキャスト 末尾にatが付くカラムはJSTのDATETIME型へキャスト これまで新しいログテーブルを検知していたGitHub Actionsのツール内で、dbtのmodel追加のPRを生成することにします。 実装してみた 大まかにやったこととしては以下の通りです。 GitHub Apps によるAPIリクエストの準備 pythonで書かれている監視ツールの修正 dbtのmodel(sqlファイル)とtest(ymlファイル)の生成処理追加 GitHubへのリクエスト処理追加 slackへの通知内容修正 GitHub AppsによるAPIリクエストの準備 今回はログテーブルの監視ツールとdbtプロジェクトが格納されているリポジトリが異なったため、GitHub Appsを作成してAPI操作ができるようにします。 以下の手順に沿ってAppを作成 https://docs.github.com/ja/apps/creating-github-apps/registering-a-github-app/registering-a-github-app Appの秘密鍵を取得 https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps 認証に必要な INSTALLATION_ID を生成 https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#generating-an-installation-access-token 3.で生成した INSTALLATION_ID を使ってinstallation access tokenを発行 今回は監視ツールに以下のクラスを追加しました。 import json import os from datetime import datetime, timedelta import jwt import requests class GitHubAppTokenManager : def __init__ (self) -> None : self.GITHUB_APP_ID = os.environ.get( "APP_ID" ) self.GITHUB_APP_PRIVATE_KEY = os.environ.get( "APP_PRIVATE_KEY" ) self.GITHUB_APP_INSTALLATION_ID = os.environ.get( "APP_INSTALLATION_ID" ) def _generate_jwt (self) -> str : PEM = ( self.GITHUB_APP_PRIVATE_KEY.replace( " \\ n" , " \n " ) if self.GITHUB_APP_PRIVATE_KEY is not None else None ) utcnow = datetime.utcnow() alg = "RS256" payload = { "typ" : "JWT" , "alg" : alg, "iat" : utcnow, "exp" : utcnow + timedelta(seconds= 60 ), "iss" : self.GITHUB_APP_ID, } return jwt.encode(payload, PEM, algorithm=alg) def _generate_jwt_headers (self) -> dict : jwt_token = self._generate_jwt() return { "Authorization" : f "Bearer {jwt_token}" , "Accept" : "application/vnd.github.machine-man-preview+json" , } def _fetch_access_token (self) -> str : url = f "https://api.github.com/app/installations/{self.GITHUB_APP_INSTALLATION_ID}/access_tokens" response = requests.post(url, headers=self._generate_jwt_headers()) response.raise_for_status() return json.loads(response.text).get( "token" ) def generate_token_header (self) -> dict : token = self._fetch_access_token() return { "Authorization" : f "token {token}" , "Accept" : "application/vnd.github.inertia-preview+json" , } ここまでできればAPIのリクエスト準備完了です。 dbtのmodelとtestの生成処理追加 GitHub APIをリクエストする前にコミット対象となるファイル生成の処理を追加します。 まずはmodelのsqlファイルとtestのymlファイルのテンプレートを用意します。 ↓modelのテンプレートです。 dbtで使っているJinjaに反応して意図したところで値が置き換わらなくなるので、一部エスケープしています。 やっぱり多少見づらくなるので他に良い方法はないだろうか・・・ {{ ' {{ ' }} config( alias= ' {{ table_name }} ' , ) {{ ' }} ' }} {{ ' {% ' }} set column_lists = {{ column_list }} {{ ' %} ' }} {{ ' {{ ' }} production_logs_template( ' {{ table_name }} ' , column_lists) {{ ' }} ' }} , result_record AS ( SELECT {{ select_list }}, dre_transfer_at, ROW_NUMBER() OVER (PARTITION BY {{ partition_column_list }}) AS row_number FROM records ), stg_record AS ( SELECT {{ jst_converted_select_list }}, dre_transfer_at, {{ ' {{ ' }} jst_now() {{ ' }} ' }} AS dre_delivered_at, FROM result_record WHERE row_number = 1 ) SELECT *, {{ ' {{ ' }} set_record_exported_at(column_lists) {{ ' }} ' }} AS dre__record_exported_at, FROM stg_record ↓testのテンプレートです。ログの一意性を担保するテストを追加します。 version : 2 models : - name : TABLE_NAME tests : - dbt_utils.unique_combination_of_columns : combination_of_columns : COLUMN_LISTS 次に以下のクラスを追加します。 BQから追加対象テーブルのカラム名を取得して generate_dbt_file_info 関数に渡すことで、先ほど作成したテンプレートを元にファイルが生成されます。 import json import os import re import tempfile from typing import Any, Dict, List, Tuple from jinja2 import Environment, FileSystemLoader from ruamel.yaml import YAML class DBTModelFileGenerator : def _convert_column_with_at (self, column_name: str ) -> str : return f "TIMESTAMP({column_name})" def _convert_column_with_id (self, column_name: str ) -> str : return f "CAST({column_name} AS INT)" def _convert_utc_to_jst (self, column_name: str ) -> str : return f "{{{{ timestamp_utc2datetime_jst('{column_name}') }}}}" def _generate_select_list (self, column_list: List[ str ]) -> str : converted_columns = [] for col in column_list: if col.endswith( "_at" ): converted_columns.append( f "{self._convert_column_with_at(col)} as {col}" ) elif re.search( r"(_id$|^id$)" , col): converted_columns.append( f "{self._convert_column_with_id(col)} as {col}" ) else : converted_columns.append(col) return ", \n " .join(converted_columns) def _generate_jst_converted_select_list (self, column_list: List[ str ]) -> str : converted_list = [] for col in column_list: if col.endswith( "_at" ): converted_list.append(f "{self._convert_utc_to_jst(col)} as {col}" ) else : converted_list.append(col) return ", \n " .join(converted_list) def _generate_staging_sql (self, table_name: str , column_list: List[ str ]) -> str : select_list = self._generate_select_list(column_list) partition_column_list = ", " .join(column_list) jst_converted_select_list = self._generate_jst_converted_select_list( column_list ) # テンプレートのロード file_loader = FileSystemLoader( "テンプレートファイルのパス" ) env = Environment(loader=file_loader) template = env.get_template( "template_stg_production_logs.sql" ) # テンプレートのレンダリング sql = template.render( table_name=table_name, column_list=column_list, select_list=select_list, partition_column_list=partition_column_list, jst_converted_select_list=jst_converted_select_list, ) # tempdirにファイルを保存 output_path = os.path.join( tempfile.gettempdir(), f "{table_name}_stg_production_logs.sql" ) with open (output_path, "w" ) as tmp: tmp.write(sql) return output_path def _generate_staging_test (self, table_name: str , column_list: List[ str ]) -> str : yaml = YAML() yaml.indent(sequence= 4 , offset= 2 ) with open ( "テンプレートファイルのパス" , "r" , ) as template_file: template_content = yaml.load(template_file) template_content[ "models" ][ 0 ][ "name" ] = f "{table_name}_stg_production_logs" test_definition = template_content[ "models" ][ 0 ][ "tests" ][ 0 ] test_definition[ "dbt_utils.unique_combination_of_columns" ][ "combination_of_columns" ] = column_list output_path = os.path.join( tempfile.gettempdir(), f "{table_name}_stg_production_logs.yml" ) with open (output_path, "w" ) as tmp: yaml.dump(template_content, tmp) return output_path def generate_dbt_file_info ( self, new_table_and_column_names: List[Dict[ str , Any]] ) -> List[Tuple[ str , str , str ]]: files = [] for record in new_table_and_column_names: table_name = record[ "table_name" ] record_dict = json.loads(record[ "record" ]) column_list = list (record_dict.keys()) sql_file_path = self._generate_staging_sql(table_name, column_list) test_file_path = self._generate_staging_test(table_name, column_list) files.append( ( f "追加先のdbt modelのパス/{table_name}_stg_production_logs.sql" , sql_file_path, f "Add staging SQL for {table_name}" , ) ) files.append( ( f "追加先のdbt testのパス/{table_name}_stg_production_logs.yml" , test_file_path, f "Add staging test for {table_name}" , ) ) return files GitHubへのリクエスト処理追加 以下のクラスを追加し、 create_pr 関数に前段で生成したファイルの情報を渡すことでPRが生成されます。 認証にはAPIのリクエスト準備で発行したinstallation access tokenを使います。 import base64 import json import re from typing import Any, Dict, List, Tuple import jwt import requests from deleted_logs.adapter.output.github_app_token_manager import GitHubAppTokenManager class GitHubPRCreator : def __init__ (self, session_start_time: str ) -> None : self.OWNER = "XXX" self.REPO = "XXX" self.BASE_BRANCH_NAME = "main" numeric_only_session_start_time = re.sub( r"\D" , "" , session_start_time) self.NEW_BRANCH_NAME = f "feature/add_new_table_of_production_logs_to_staging_{numeric_only_session_start_time}" self.github_app_token_manager = GitHubAppTokenManager() def _is_branch_present (self) -> bool : headers = self.github_app_token_manager.generate_token_header() branch_url = f "https://api.github.com/repos/{self.OWNER}/{self.REPO}/git/ref/heads/{self.NEW_BRANCH_NAME}" response = requests.get(branch_url, headers=headers) if response.status_code == 200 : return True elif response.status_code == 404 : return False else : response.raise_for_status() return False def _create_branch (self) -> None : if self._is_branch_present(): return headers = self.github_app_token_manager.generate_token_header() # ベースブランチのSHAを取得 base_ref_url = f "https://api.github.com/repos/{self.OWNER}/{self.REPO}/git/ref/heads/{self.BASE_BRANCH_NAME}" response = requests.get(base_ref_url, headers=headers) response.raise_for_status() sha = response.json()[ "object" ][ "sha" ] # ブランチ作成 branch_ref_url = ( f "https://api.github.com/repos/{self.OWNER}/{self.REPO}/git/refs" ) data = { "ref" : f "refs/heads/{self.NEW_BRANCH_NAME}" , "sha" : sha} response = requests.post(branch_ref_url, headers=headers, json=data) response.raise_for_status() def _upload_file_to_repository ( self, git_file_path: str , local_file_path: str , message: str ) -> None : headers = self.github_app_token_manager.generate_token_header() with open (local_file_path, "r" ) as file : content = file .read() encoded_content = base64.b64encode(content.encode()).decode() url = f "https://api.github.com/repos/{self.OWNER}/{self.REPO}/contents/{git_file_path}" # 同様のファイルが既に存在するか確認 response = requests.get(url, headers=headers) sha = None if response.status_code == 200 : sha = response.json()[ "sha" ] data = { "message" : message, "content" : encoded_content, "branch" : self.NEW_BRANCH_NAME, } if sha: data[ "sha" ] = sha response = requests.put(url, headers=headers, json=data) response.raise_for_status() def _push_files (self, files: List[Tuple[ str , str , str ]]) -> None : self._create_branch() for git_file_path, local_file_path, message in files: self._upload_file_to_repository(git_file_path, local_file_path, message) def _generate_pr_title (self, table_names: List[ str ]) -> str : return f "production_logs新規テーブル({','.join(table_names)})追加" def _generate_pr_body (self, table_and_column_names: List[Dict[ str , Any]]) -> str : header = "このPRはdeleted_logsによって自動生成されたものです。 \n\n 以下のテーブルのステージング処理を追加しています: \n " lines = [] for record in table_and_column_names: table_name = record[ "table_name" ] record_dict = json.loads(record[ "record" ]) columns = ", " .join(record_dict.keys()) lines.append(f "**{table_name}** \n Columns: {columns} \n " ) return header + " \n " .join(lines) def create_pr ( self, table_names: List[ str ], column_names: List[Dict[ str , Any]], files: List[Tuple[ str , str , str ]], ) -> str : self._push_files(files) headers = self.github_app_token_manager.generate_token_header() url = f "https://api.github.com/repos/{self.OWNER}/{self.REPO}/pulls" title = self._generate_pr_title(table_names) body = self._generate_pr_body(column_names) data = { "title" : title, "body" : body, "head" : self.NEW_BRANCH_NAME, "base" : self.BASE_BRANCH_NAME, } response = requests.post(url, headers=headers, json=data) response.raise_for_status() return response.json()[ "html_url" ] こんな感じのPRが生成されました。 ※テストで作成したものなのでクローズしてます。 slackへの通知内容の修正 前段で生成したPRのURLを通知メッセージに追加します。 ↓テストで飛ばした通知はこんな感じです。 ここで通知されたPRのレビューとマージを行うことで、Staging層のdbt modelが作られるようになりました。 まとめ 今回dbtのmodel生成を自動化したことで、2つの効果が得られました。 1つ目は、トイルの削減です。 これまで以下のフローで対応していましたが、 既存のdbt modelをコピペしてPR作成 → レビュー依頼 → マージ 自動化したことで、 PRを確認してマージ だけででよくなりました。 元々大した工数はかかっていなかったものの、繰り返し発生する価値のない定型作業を減らすことができました。 そして2つ目は、Staging層へ新規ログテーブルが反映されるまでのラグ短縮です。 自動化したことで、他の対応でひっ迫している際に後回しにされることがなくなり、 これまではログ出力し始めてから分析可能になるまで最長7日かかっていたところ、導入後は大体1日以内で分析可能な状態になりました。 個人的には、タイミーにジョインするまでDevOps的なことがやれていなかったこともあり、運用は地味でつまらないイメージがあったのですが(すみません)、こうやって改善がまわせると運用も面白いなと最近思うようになりました。 We’re Hiring DREグループではまだまだやっていきたいことがたくさんあるのですが、まだまだ手が足りておらず、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
アバター
この記事は "Timee Advent Calendar 2023" の11日目の記事です。 qiita.com こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の菊地です。 今回は、定例会議の議事録をNotion DBで構造化して、いい感じにした話を紹介したいと思います! 前提 タイミーでは社内ドキュメントツールとしてNotionを採用しており、私が担当しているプロジェクトで週1回開催される定例では、議事録をNotion DBとして管理しています。当初は以下のような定例議事録用テンプレートを作成して運用していました。 定例の内容としては、プロジェクト進行上同期的に議論すべき アジェンダ を定例出席者が持ち寄って議論し、決定事項とToDoを記載していくような内容となっています。 旧定例議事録テンプレート 上記の定例議事録で感じていた課題 上記の定例議事録用テンプレートから定例の議事録を作成して運用していた際に、以下のような課題を感じていました。 「 アジェンダ 」パート アジェンダ が多いと一覧性が悪く、優先度の高い アジェンダ を先に話すなどの判断がしづらい アジェンダ に関する議論が複数回にわたる際に、議論内容が複数ページに渡り、情報が追いにくい アジェンダ が多く、その回の定例で話せず持ち越しになった場合に、次回の議事録に転記する必要がある 「決定事項」パート 何の アジェンダ について、どんな意思決定が行われたのかが追いにくい ある決定事項がどこに書かれていたかを調べたい際に、決定事項が書かれた会の議事録のページを探し出す必要がある 「ToDo」パート 前回の定例で決まったToDoを確認するために、前回の議事録を参照or次回の議事録に転記する必要がある 進行状況が分からない 期限が不明瞭 やったこと 上記で感じていた課題を解消するために、下記のように「構成要素」と「構成要素間の関係」をNotion DBで整理(構造化)し、合わせてNotion DBテンプレートページの作成を行いました。 「 アジェンダ 」パート アジェンダ を管理するNotion DB(Agendas)を作成し、議事録ページからはLinkded viewで参照する 「決定事項」パート 決定事項を管理するNotion DB(Decisions)を作成し、議事録ページからはLinkded viewで参照する Agendas DBとrelationsを持たせる。 「ToDo」パート プロジェクトのタスク管理で使用しているNotion DB(ここでは「Tasks」とします)をそのまま流用し、議事録ページからはLinkded viewで参照する 議事録から参照する際は、定例で作成されたToDoタスクだと判別できるようにtagをつけてフィルターを設定して絞り込む Agendas DBとリレーションを持たせる Notion DBのリレーション Notion DBのリレーションは以下のような構成になっています。 Meetings: 議事録を管理するNotion DBで、Agendas、Decisions、TasksをLinked Viewで参照する Agendas: 会議の アジェンダ を管理するNotion DB Decisions: 会議の決定事項を管理するNotion DB Tasks: プロジェクトのタスクを管理するNotion DB erDiagram Meetings Agendas ||--o{ Tasks: "1つのAgendaは0以上のTaskを持つ" Agendas ||--o{ Decisions: "1つのAgendaは0以上のDecisionsを持つ" Notion DBテンプレートページ 定例議事録と、定例議事録内の各 アジェンダ のNotion DBテンプレートページを以下のように作成しました。 「Meetings」DBの定例議事録テンプレート 「ToDo」パートの「Tasks」は、「ステータス」が「完了」以外を表示するようにフィルタを設定 「 アジェンダ 」パートの「Agendas」は、「解決済み」が「チェックなし」のみを表示するようにフィルタを設定 「決定事項」パートの「Decisions」は、決定事項が定例会議実施日のみに作成されるという想定の元、「作成日時」のフィルタ条件として、定例実施日に設定する運用にする 新定例議事録テンプレート 「Agendas」DBの アジェンダ テンプレート 「ToDo」パートの「Tasks」は、フィルタ条件「Agendas」をテンプレートページに設定しておくことで、テンプレートから作成されたページが自動で設定されるように 「決定事項」パートの「Decisions」は、フィルタ条件「Agendas」をテンプレートページに設定しておくことで、テンプレートから作成されたページが自動で設定されるように アジェンダ テンプレート 結果 定例議事録ページで感じていた課題は、議事録の「構成要素」と「構成要素間の関係」をNotion DBで整理(構造化)することで、以下のように解消することができました。 「 アジェンダ 」パート アジェンダ の一覧性が悪く、優先度の高い アジェンダ を先に話すなどの判断がしづらい → アジェンダ がDBのページとして表現されることで、一覧として表示することができ、一覧性が向上 → DBは一覧の並び替えができるので、優先度の高い アジェンダ を上に持ってくることが可能になった アジェンダ に関する議論が複数回にわたる際に、議論内容が複数ページに渡り、情報が追いにくい → アジェンダ に関する議論は、 アジェンダ ページに集約されるので、議論内容が追いやすくなった アジェンダ が多く、その回の定例で話せず持ち越しになった場合に、次回の議事録に転記する必要がある → 解決していない アジェンダ は自動的に次回に持ち越されるようになり、転記が不要になった 「決定事項」パート 何の アジェンダ について、どんな意思決定が行われたのかが追いにくい → アジェンダ に紐づけて決定事項をDBとして管理することで、意思決定が追いやすくなった ある決定事項がどこに書かれていたかを調べたい際に、決定事項が書かれた会の議事録のページを探し出す必要がある → 決定事項はDBにまとまっていて、 アジェンダ も紐づいているので、情報の検索性が向上し、素早く決定事項にたどり着けるようになった 「ToDo」パート 前回の定例で決まったToDoを確認するために、前回の議事録を参照or次回の議事録に転記する必要がある → ToDoはLinked viewとして次回定例に引き継がれるので、前回の議事録を参照する必要性も、次回の議事録に転記する必要性もなくなった 進行状況が分からない → DBとして表現することで進行状況をpropertyとして表現することが可能になり、進行状況を追えるようになった 期限が不明瞭 → DBとして表現することで、期限をpropertyとして表現することが可能になり、期限が明確になった まとめ 今回は、定例会議の議事録をNotion DBで構造化して、いい感じにした話を紹介しました。 タイミーではNotionをフルに活用しており、私が所属しているデータ統括部DSグループでも、 スクラム 運用や開発チケット管理など、さまざまな場面でNotionを活用して業務を効率化しています。 今後も定期的に、Notionの活用について発信していきたいと思います! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! mermaid.initialize({startOnLoad: true});
アバター
この記事は Timee Advent Calendar 2023 シリーズ 2 の10日目の記事です。 qiita.com こんにちは! @lucky_pool です。 タイミーでプロダクトマネージャーをしています。 はじめに 何らかのシステム障害が起こったとき、サービスを利用するあらゆる人に影響が出て、普段通りにサービスを利用できなくなってしまいます。そんな状況になった際、 “なんとかする” しかありません。 私はプロダクトマネージャーという役割で働いていますが、サービスのコード修正をすることや、データ変更のオペレーションをすることはありません。また、過去や新規に開発された機能や仕様をすべて熟知しているわけでもありません。ですが、障害対応においてインシデント コマンダー を担うことが何度かありました。 そこで、私がタイミーでインシデント コマンダー をやった経験から、一般的に役立ちそうな内容ををここでは紹介したいと思います。 インシデント コマンダー は誰でもなれる ここでは、障害対応をなんとかする人を「インシデント コマンダー 」と呼びます。 PagerDuty Incident Response *1 *2 を参照すれば、インシデント コマンダー の説明は以下のとおりです。※ この記事はとても良い内容でして、なんと邦訳版としても公開されております。 *3 *4 ありがたい! Keep the incident moving towards resolution. (意訳) インシデントを解決に向かわせ続ける人 そして次のようにも説明があります。 You don't need to be a senior team member to become an IC, anyone can do it providing you have the requisite knowledge (yes, even an intern!) (意訳) インシデント コマンダー になるには、シニ アメンバー になる必要はなく、 必要な知識があれば誰でもなれる (もちろん、 インターン でも) そうです、やり方さえ分かれば誰でもなれるとのことです。私も、タイミーでのインシデント コマンダー の経験上、これは一定「確かにそう」と思っています。 PagerDuty のドキュメントには Requisite knowledge (必要な知識, 予備知識) と説明がありますが、実際は所属するチームや会社によって、対応できる権限や関係者、関係するツールが異なります。方法論を知ることは必要ですが、それらの知識をベースにした “経験” が必須だと考えています。故に実行することができます。 実際に私がどんな経験をしたかをかいつまんで説明します。 事前に経験したこと 私が入社したのは2022年10月です。約1年ちょと前です。入社後から(今もですが)、システム障害等、何らか問題が起きたとき、その事象を “なんとかしたい” という気持ちだけは持っていました。 そんな気持ちからか、いつのまにか以下のことを経験していました。 ①障害対応に何度もオブザーブする タイミーでは、何らかのシステム障害が観測されたとき、Slackの WF にて報告されることになっています。そして @障害報_通知グループ なるグループに通知が飛ぶようになっています。 障害報WFで投稿された内容 この通知グループは、障害報告を知りたい人が多く入っており、例えば、プロダクト開発の関係者だけではなく、カスタマーサポート、広報、管掌役員なども入っています また、メンション対象にいなくとも、投稿先のチャンネルは、全社への情報発信をする場所であり 数100人以上が見ているチャンネルだったりします 私はこの通知を受け取る一人となりました。WFによって自動的に Google Meet のURLを提示してくれるため、作業担当者が会話しはじめます。この Google Meet に入ることで状況を知るようにしました。 その過程で大まかな障害対応の手順がわかるようになっていきました。 例えば以下の通り。 影響範囲の調査 誰に影響があるのか、何に影響があるのかを調べます システム的な影響(xxx のエラーが xxx 件ある等)だけでなく、問合せ状況(xxxに関する問合せが、通常よりxxx件多い)、また手元での確認状況(xxxxの操作をするとxxxxになる)を把握するのに努めます 暫定対応方針の検討と実行 顧客説明方針 広範囲な障害であれば、なんらか顧客周知をすることを検討しなければなりませんが、そうではなく、社内一部業務フローのみの影響であれば、関係者に対し周知する方針にします システム対応方針 できる限り早く、影響範囲を小さくできる方針を検討します revert するほうが手っ取り早ければそうしますが、そうはいかない場合は、なんらかコード差分を作りデプロイが必要になるかもしれません 定期的な情報共有 1および2の対応ステータスをリアルタイムに更新していきます、そうすることで、解消見込み時間などが分かり、安心する人たちもでてきます オンコール対応の収束 暫定対応を講じ、影響範囲を少なくすることに成功した場合、チームは解散します もし土日や深夜であれば、何らかの残対応があっても、翌営業日にすることがあるでしょう ポストモーテムの実施及び恒久対応の計画 恒久的な再発防止策だけでなく、プロセスの改善の検討と実施を計画します ②障害対応時のインシデント コマンダー の補佐をする 障害対応状況をオブザーブしていると、インシデント コマンダー を補佐できるようなことがいくつかあります。例えば以下の対応ができます。 対応状況の記録を取る タイミーの場合はドキュメントは Notion にまとめており、障害対応時においてもNotion ページに情報を集めていきます インシデント コマンダー は話をすること、情報を整理することに集中するため、文字列に整理することは難しいことがあり、故にこの補佐をすることはとても頼りになります また以下にも関連しますが、関係者にライブで状況を伝える役割にも一助になります 関係者にライブで状況を伝える WF で生成されたスレッドに、今の対応状況を箇条書きで投下しました 例えば 影響範囲がわからず、その調査を開始した、解消は未定 影響範囲は未確定だが規模として xxx が見込まれることがわかった 対応方針を検討しているが、大きく xxxx と xxxx の方針がでている xxx の方針を取ったため、 xxxx に解消が見込まれる これらのただの書き殴りだとしても、関係者にとっては有用な情報になります 障害対応時のタイムラインとして後で役に立ちます 関係者にメンションし呼び出してくる 対応に必要な人が Meet に来ていない場合は、容赦なくメンションします インシデント コマンダー や作業者が「xxxさん、xxxxチームに来て欲しい」という発言があれば、すぐに「じゃ、私が呼びますね」と対応します。 インシデント コマンダー や作業者が解決に向かうことに集中してもらうことに役立てます また、作業者が複数チームに分かれる場合もあるため、両チームのハブとなるような動きをしてインシデント コマンダー の補佐ができます 例: A: 障害原因の調査および対処方針を検討するチーム B: 顧客周知方針を検討するチーム ③ポストモーテムにオブザーブする タイミーにおいて、障害対応をしたあとは関係者でポストモーテムを実施しています。もし、障害対応をオブザーブしていなかったり何らかの補佐をしていなくても、どのような障害があり、対応がされていたのかが理解できるため、ポストモーテムの参加からでも良い経験になると思います。 ポストモーテムにおいては、再発防止だけでなく、障害対応のプロセスの改善についても会話がされます。例えば、私が参加したポストモーテムでは以下のような会話がありました。 xxx さん(xxxチーム) にて早期に報告があったのはいい動きだった! 以前に対応した xxx によって、今回の対応が早期に解決できてよかった! 今回 xxx の対応ができたことによって、顧客問い合せが 0件 で影響範囲を少なくできた! もちろん、このような良い会話だけではなく、 xxx をすることによって、もっと効果的にできるというような具体的なアクションにつながるものもあります。 システム障害、それは突然やってくる 誰もが対応できる時間帯にやってくるとは限りません。なんとかする間には、サービスのあらゆる利用者に対して真摯に説明する必要がありますし、可能な限りはやく解消するしかありません。故に、事前にこういった経験をしておくと焦らずにすむと思います。また、事前にある程度経験した上で、書籍 *5 も読むと体系だった知識としても理解しやすくなるかもしれません。 本稿が転ばぬ先の杖として、参考になれば幸いです。 *1 : https://response.pagerduty.com/ *2 : https://response.pagerduty.com/training/incident_commander/ *3 : https://i-beam.org/2020/09/22/pagerduty-incident-response/ *4 : https://ueokande.github.io/incident-response-docs-ja/ *5 : https://www.amazon.co.jp/dp/4297112655/
アバター
はじめに ※ Timee Product - Qiita Advent Calendar 2023 - Qiita の12月8日分の記事です。 okodooooooonです BigQueryの料金爆発。怖いですよね。 dbtでの開発が進んでたくさんのモデルを作るようになると、デイリーのビルドだけでも凄まじいお金が消えていったりします(僕はもう現職で数え切れないくらいやらかしてます)。 コストの対策として「パーティショニング」「クラスタリング」などが挙げられますが、今回は「増分更新」の観点で話せたらと思います。 「dbtのmaterialized=’incremental’って増分更新できておしゃれでかっこよくてコストもなんとなく軽くなりそう!」くらいの認識でさまざまな失敗を経てきた僕が、BigQueryにおけるincrementalの挙動を説明した上で、タイミーデータ基盤における増分更新の使い方についてまとめたいと思います。 ※この記事はdbt + BigQueryの構成に限定した内容となります。BigQuery以外のデータウェアハウス環境では今回紹介する2つ以外のincremental_strategyが用いられるので、そちらご注意ください! BigQueryの増分更新の挙動についておさらい dbtはincremental処理において、BigQuery向けに merge と insert_overwrite という2つのincremental_strategy(増分更新のやり方のオプション)を提供しています。 名前だけ見てもちんぷんかんぷんなのでそれぞれ解説していこうと思います。 MERGE戦略 MERGEはBigQueryにおけるデフォルトのincremental方針となります。 dbtのconfigブロックの定義は以下のようになります。 {{ config( materialized='incremental' , unique_key='xx_key' ) }} incremental_strategy = mergeの時に実際にBigQueryに発行されるMERGE文はこんな感じになります。 merge into `xxxx_project`.`xxx_dataset`.`atesaki_table` as DBT_INTERNAL_DEST using ( {{model_sql}} ) as DBT_INTERNAL_SOURCE on ( DBT_INTERNAL_SOURCE.ユニークキー 1 = DBT_INTERNAL_DEST.ユニークキー 1 AND DBT_INTERNAL_SOURCE.ユニークキー 2 = DBT_INTERNAL_DEST.ユニークキー 2 ... ... ) -- ユニーク指定キーがマッチングした場合 when matched then update set ソース側の行でマッチングした宛先テーブルの行を上書き -- 宛先テーブルにユニーク指定キーが存在しなかった場合 when not matched then insert ソース側の行を追加 この際のON句の中身が問題で on ( DBT_INTERNAL_SOURCE.ユニーク指定キー 1 = DBT_INTERNAL_DEST.ユニーク指定キー 1 AND DBT_INTERNAL_SOURCE.ユニーク指定キー 2 = DBT_INTERNAL_DEST.ユニーク指定キー 2 ... ) といった式になっているので、宛先テーブルの全件を走査して、マッチする列がないかを確認しています。宛先テーブルが巨大になればなるほど、クエリコストが問題となってきます。 概念図で示すとこんな感じ INSERT+OVERWRITE戦略 dbtのconfigブロックは以下のようになります。 {{ config( materialized='incremental', incremental_strategy='insert_overwrite', partitions=var('last_31_days'), # dbt_project.ymlで定義した日付のリストが格納された変数 partition_by= { 'field' : 'xx_datetime' , 'data_type' : 'datetime' , 'granularity' : 'day' } ) }} 発行されるMERGE文は以下のような形。参考: https://docs.getdbt.com/reference/resource-configs/bigquery-configs#the-insert_overwrite-strategy create temporary table {{ model_name }}__dbt_tmp as ( {{ model_sql }} ); declare dbt_partitions_for_replacement array< date >; -- ソーステーブルから上書きすべき日付の配列をdbt_partitions_for_replacementにセット set (dbt_partitions_for_replacement) = ( select as struct array_agg( distinct date (max_tstamp)) from {{ model_name }}__dbt_tmp ); merge into {{ destination_table }} AS DEST using {{ model_name }}__dbt_tmp AS SRC on FALSE -- on Falseにすることでキー同士のマッチングは発生しない -- 宛先テーブルの該当パーティションを削除する。参考:https://cloud.google.com/bigquery/docs/using-dml-with-partitioned-tables?hl=ja#using_a_filter_in_the_search_condition_of_a_when_clause when not matched by source and {{ partition_column }} in unnest(dbt_partitions_for_replacement) then delete -- ↑で削除した後にソースをinsertする when not matched then insert ... 指定したパーティションを削除した後に、ソーステーブルをinsertするような処理をしています。 ソースにも宛先にもfull scanが走らないので、クエリコストとしては軽くなります 概念図で示すとこんな感じ 増分更新ロジックの使い分けについて 上記のロジックを理解した上で増分更新についてまとめると以下のようになります。 incremental_strategy=mergeのユースケース ソーステーブルが巨大で宛先テーブルが小さい場合の増分更新 ソーステーブルにフルスキャンを走らせることなくコストを抑えた状態で、ユニークを保って増分更新ができます incremental_strategy=insert_overwriteのユースケース 宛先テーブルが巨大な場合の増分更新 ソーステーブル, 宛先テーブルともにフルスキャンを走らせることなくコストを抑えた状態で増分更新できます insert_overwriteの注意点 💡 パーティションを静的に指定しないといけない点 insert_overwriteではdbt_project.ymlにて以下のように静的なリストとしてパーティション指定のリスト変数を作成した後に last_2_days : [ current_date('Asia/Tokyo') , date_sub(current_date('Asia/Tokyo'), interval 1 day) ] 以下のように {{ config( materialized='incremental', incremental_strategy='insert_overwrite', partitions=var('last_2_days'), partitionsに渡す必要があります 上の直近2日間のpartitions指定だと、もし何かしらの要因でJobが3日間転んだりしていたら、3日目に復旧したとしても 障害1日目:レコード無。障害2日目:レコード有。障害3日目:レコード有 といった形の歯抜けテーブルになってしまいます。障害を踏まえたバッファ分を余裕を持って指定しておくことが大切です。 💡 ユニークが担保されない点 insert_overwriteはパーティションをごっそり入れ替えるようなロジックであるため、ユニークがこのロジックだけでは担保されません。これによって生じる想定される不具合を見てみましょう。 例えば「ユーザーごとの月毎と日毎の両方のステータスを保持するようなテーブル」を想定してみます。 月単位のステータスが12/9で petapeta ⇒ potepote に変更されたとします。 この時に直近1週間をpartitionsに指定するinsert_overwriteで増分更新をすると仮定すると12/9の更新で以下のようになります。 このように直近1週間のパーティションをそのまま置き換えただけなので、12/1, 12/2の月単位のステータスがpetapetaのままになってしまい、同じ月に2つの月単位ステータスを持つ形になってしまいます。このようなケースを防ぐには、partitionsに「直近1ヶ月分の日付のリスト」を渡してあげる必要があります。 過去の値が変わりうるテーブル(月末の締め処理で金額が変動しうる会計処理とか?)に対してinsert_overwriteを適用するには、「どの期間をpartitionsに渡せば安全なのか」を考えることがとても重要になります! タイミーの増分更新の対象について 挙動を正確に理解できたところでタイミーデータ基盤における増分更新のユースケースを見ていきましょう! mergeを使用して増分更新しているケース firebaseなどのログテーブルから一部種別のログだけを抽出するようなテーブル 巨大なソーステーブルから集約した値を蓄積するようなテーブル insert_overwriteを使用して増分更新しているケース 宛先がソースと比較して膨大になるsnapshot factテーブル その他ログテーブルなどから作られる巨大なfactテーブル こんな形で当社では現状は使い分けています。 実装難度がinsert_overwriteの方が高いので、mergeで済むケースはmergeにしちゃってます。 おわりに BigQueryにおけるincrementalの挙動と、それぞれのメリデメ、なぜコストが下がるのかあたりを解説しました。 incrementalに関する公式Docを見てもなんとなく言ってることわかりそうでわからない。 MERGE文などを通して公式Docなどで紹介されているが、普段SELECT文を書くことが主となってしまって、DMLにあたるMERGE文は読みづらいしわかりづらい。などといった方々の参考になれたら嬉しいです。 We’re Hiring タイミーのデータ統括部はやることがまだまだいっぱいで仲間を募集しています!興味のある募集があればこちらから是非是非ご応募ください。 product-recruit.timee.co.jp
アバター
こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小関 ( @ozeshun )です。 本記事では、タイミーで取り組んでいるレコメンドに使用する アルゴリズム を検証する際に活用した、RecBoleでの実験方法について紹介したいと思います。 ※ Timee Advent Calendar2023 の12月8日分の記事です。 RecBoleとは RecBoleを活用したアルゴリズムの実験手順 0. ディレクトリ構成 1. 学習データの準備とRecBoleで使用するconfig fileの用意 2. 学習データをAtomic file *3 へ変換 3. モデルの学習 4. 学習したモデルの検証 おわりに We’re Hiring! RecBoleとは RecBole とは、レコメンド アルゴリズム を統一されたインターフェースで提供する事を目的としたプロジェクトであり、後述のように アルゴリズム 間の比較を簡単に実現出来ます。2023/12/8現在、91種類の アルゴリズム が実装されており、 Python のライブラリ *1 として公開されています。 実装されている アルゴリズム は、 Model Introduction から確認できます。 今回は、実装されている アルゴリズム の中でもexplicitなフィードバックを予測すること *2 を目的とした、 Context-aware Recommendation の アルゴリズム をRecBoleを使用して検証する一連の流れを紹介したいと思います。 RecBoleを活用した アルゴリズム の実験手順 0. ディレクト リ構成 今回は、以下のような ディレクト リ構成の元、ノートブック上で実験を進める手順を説明します。 ├── notebook.ipynb ├── artifact │ ├── saved # 学習したモデルの保存先 │ │ ├── AFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── DeepFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── FM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── NFM-%m-%d-%Y_%H-%M-%S.pth │ └── train_data # 学習用のデータの保存先. pickle fileは、Atomic fileに変換する際のソース. │ ├── interact.pkl │ ├── items.pkl │ ├── users.pkl │ ├── train_dataset # RecBoleが学習で使用する、Atomic fileの保存先 │ ├── train_dataset.inter │ ├── train_dataset.item │ └── train_dataset.user ├── base_dataset.py # データセットのI/Oをコントロールするクラスのベース (詳細はStep.2に記述) ├── config ├── model.hyper # 探索したいハイパーパラメータと探索範囲を記述したファイル └── train.yaml # RecBoleで使用する、学習方法などを記述したconfig file 1. 学習データの準備とRecBoleで使用するconfig fileの用意 学習用データの準備 今回は、Context-awareなモデルを学習することを目的としているので、下記のように、user_id、item_id、explicitなフィードバックを表すカラム、ユーザー・アイテムの特徴量を含む pandas.DataFrame 形式のオリジナルデータを用意します。   user_id item_id target user_feature item_feature 1 1 0 0 -1.0 2000 2 1 2 1 0.5 3000 3 2 1 1 -0.8 4000 4 2 2 0 0.0 5000 RecBole用のconfig fileの用意 このファイルには、データの保存先などの環境の設定、使用するデータに関する情報、学習方法や評価方法の設定を記述します。 # config/train.yaml # 使用するモデル名とデータセット名を指定----------------------------------- model : FM dataset : train_dataset # Environment Settings----------------------------------------------- # https://recbole.io/docs/user_guide/config/environment_settings.html gpu_id : 0 use_gpu : False seed : 2023 state : INFO reproducibility : True data_path : 'artifact/train_data/' checkpoint_dir : 'artifact/saved/' show_progress : True save_dataset : True save_dataloaders : False # Data Settings------------------------------------------------------ # https://recbole.io/docs/user_guide/config/data_settings.html # Atomic File Format field_separator : " \t " seq_separator : "@" # Common Features USER_ID_FIELD : user_id ITEM_ID_FIELD : item_id LABEL_FIELD : target # Selectively Loading load_col : # interaction inter : [ user_id, item_id, target ] # ユーザー特徴量 user : [ user_id, user_feature, ] # アイテム特徴量 item : [ item_id, item_feature, ] # Preprocessing # 標準化する特徴量を指定 normalize_field : [ item_feature, ] # Training Setting--------------------------------------------------- # https://recbole.io/docs/user_guide/config/training_settings.html epochs : 100 train_batch_size : 1024 learner : 'adam' train_neg_sample_args : ~ eval_step : 1 stopping_step : 3 loss_decimal_place : 4 weight_decay : 0 # Evaluation Settings------------------------------------------------ # https://recbole.io/docs/user_guide/config/evaluation_settings.html eval_args : group_by : user split : { 'RS' : [ 0.8 , 0.1 , 0.1 ]} mode : labeled repeatable : True metrics : [ 'LogLoss' , 'AUC' ] topk : 20 valid_metric : LogLoss eval_batch_size : 1024 metric_decimal_place : 4 eval_neg_sample_args : ~ 次のステップでAtomic fileに変換する際のソースとなるように、用意したDataFrameをpkl形式で保存 下記のコードをノートブック上で実行すると、ユーザー×アイテムのインタ ラク ション、ユーザーの特徴量、アイテムの特徴量を抽出したデー タセット が ARTIFACT_PATH 配下に保存されます。 import os import yaml import pandas as pd # データの読み込み (データソースはなんでも良い) train_df = pd.read_csv( '/path/to/train.csv' ) # yaml形式で書かれたRecBoleのcofig fileを読み込む TRAIN_YAML_PATH = 'config/train.yaml' with open (TRAIN_YAML_PATH, 'r' ) as yaml_file: train_config = yaml.safe_load(yaml_file) # cofig fileから各データセットに使用するカラム名を抽出 INTERACTION_COLUMNS = train_config[ 'load_col' ][ 'inter' ] USER_COLUMNS = train_config[ 'load_col' ][ 'user' ] ITEM_COLUMNS = train_config[ 'load_col' ][ 'item' ] TOKEN_COLUMNS = [ 'item_id' , 'user_id' , ] # Atomic fileに変換する際のソースデータとしてpkl形式で保存 ARTIFACT_PATH = 'artifact/train_data/' train_df[INTERACTION_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'interact.pkl' )) train_df[ITEM_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'items.pkl' )) train_df[USER_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'users.pkl' )) 2. 学習データをAtomic file *3 へ変換 変換時に使用する、デー タセット のI/Oをコン トロール するクラスをノートブック上に記述 このクラスには、インプット・アウトプット先や、使用するカラムの情報とそのデータ型を記述しておきます。 # https://github.com/RUCAIBox/RecSysDatasets/blob/master/conversion_tools/src/base_dataset.py をそのままimportして継承 from base_dataset import BaseDataset class TrainDataset (BaseDataset): def __init__ (self, input_path, output_path): super (TrainDataset, self).__init__(input_path, output_path) self.dataset_name = 'train_dataset' # input_path self.inter_file = os.path.join(self.input_path, 'interact.pkl' ) self.item_file = os.path.join(self.input_path, 'items.pkl' ) self.user_file = os.path.join(self.input_path, 'users.pkl' ) self.sep = ',' # output_path output_files = self.get_output_files() self.output_inter_file = output_files[ 0 ] self.output_item_file = output_files[ 1 ] self.output_user_file = output_files[ 2 ] # selected feature fields inter_fields = { i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (INTERACTION_COLUMNS) } item_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (OFFER_COLUMNS)} user_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (USER_COLUMNS)} self.inter_fields = inter_fields self.item_fields = item_fields self.user_fields = user_fields def load_inter_data (self): return pd.read_pickle(self.inter_file) def load_item_data (self): return pd.read_pickle(self.item_file) def load_user_data (self): return pd.read_pickle(self.user_file) 下記コードをノートブック上で実行して、 ARTIFACT_PATH 配下に格納したデー タセット をAtomic file形式に変換 ARTIFACT_PATH = 'artifact/train_data/' # 前のステップで保存したデータセットの保存先 input_path = ARTIFACT_PATH # Atomic fileの書き出し先 output_path = os.path.join(ARTIFACT_PATH, 'train_dataset' ) i_o_args = [input_path, output_path] # 前のステップで作成したI/Oクラスにinput,outputの情報を渡す datasets = TrainDataset(*i_o_args) # DatasetをAtomic fileに変換 datasets.convert_inter() datasets.convert_item() datasets.convert_user() 3. モデルの学習 モデルを学習する関数をノートブック上に定義 パラメータチューニングの各種設定については、コメントで記述したページに詳しく書いてあります。 from recbole.quick_start import objective_function, run_recbole from recbole.trainer import HyperTuning # (再掲) 事前に準備したRecBoleのcofig fileへのパス TRAIN_YAML_PATH = 'config/train.yaml' # 探索したいハイパーパラメータと探索範囲を記述したファイルへのパス HYPER_PARAMS_PATH = 'config/model.hyper' def train_model (model_name: str , config_file_list: str = TRAIN_YAML_PATH, params_file: str = HYPER_PARAMS_PATH) -> None : # ハイパーパラメータチューニングの条件を設定 # 参考: https://recbole.io/docs/user_guide/usage/parameter_tuning.html hp = HyperTuning( objective_function=objective_function, algo= 'bayes' , early_stop= 3 , max_evals= 15 , params_file=params_file, fixed_config_file_list=config_file_list, ) # チューニングを実行 hp.run() # print best parameters print ( 'best params: ' , hp.best_params) # print best result print ( 'best result: ' ) print (hp.params2result[hp.params2str(hp.best_params)]) # bestなパラメータを取得 parameter_dict = { 'train_neg_sample_args' : None , } | hp.best_params # bestなパラメータでモデルを学習 run_recbole( model=model_name, dataset= 'train_dataset' , config_file_list=config_file_list, config_dict=parameter_dict, ) 探索したいハイパーパラメータとその探索範囲を model.hyper というファイルに記述して用意 # config/model.hyper (使用アルゴリズムがFMの場合) learning_rate choice [0.1, 0.05, 0.01] embedding_size choice [10, 16, 32] 定義した学習用の関数に試したい アルゴリズム 名を指定して、学習を実行すると train.yaml に記述した checkpoint_dir 配下に学習済のモデルが保存されます。 # FM train_model('FM') # NFM train_model('NFM') # AFM train_model('AFM') # DeepFM train_model('DeepFM') 4. 学習したモデルの検証 学習したモデルでテストデータに対する予測値を計算し、その評価指標を算出する関数をノートブック上に定義 import torch from recbole.data.interaction import Interaction from recbole.quick_start import load_data_and_model def eval_model ( model_file_name: str , model_saved_dir: str = train_config[ 'checkpoint_dir' ], target: str = train_config[ 'LABEL_FIELD' ], user_columns: list = USER_COLUMNS, item_columns: list = ITEM_COLUMNS, token_columns: list = TOKEN_COLUMNS, ): # 学習したモデルとテストデータを読み込み _, model, _, _, _, test_data = load_data_and_model(model_file=os.path.join(model_saved_dir, model_file_name)) columns = user_columns + item_columns + [target] # テストデータをモデルが予測出来る形式に変換 interactions = {} test_df = pd.DataFrame([]) for c in columns: test_features = torch.tensor([]) for data in test_data: test_features = torch.cat([test_features, data[ 0 ][c]]) if c in token_columns: test_features = test_features.to(torch.int) interactions[c] = test_features if c in [ 'user_id' ] + [target]: test_df[c] = test_features test_interaction_input = Interaction(interactions) # テストデータに対する予測結果を作成 model.eval() with torch.no_grad(): test_result = model.predict(test_interaction_input.to(model.device)) test_df[ 'pred' ] = test_result # テストデータに対するランキングメトリクス、AUC, Loglossを算出する関数を実行 (今回は実装は割愛) # 現状のRecBoleの仕様だとmode: labeledで学習した場合、ランキングメトリクスを指定できないので、自前で計算する必要があります return test_df 定義した検証用の関数に学習済のモデルのファイル名を入れて実行することで、テストデータに対する予測結果とメトリクスが計算されます。 # FM test_df = eval_model( 'FM-%m-%d-%Y_%H-%M-%S.pth' ) # NFM test_df = eval_model( 'NFM-%m-%d-%Y_%H-%M-%S.pth' ) # AFM test_df = eval_model( 'AFM-%m-%d-%Y_%H-%M-%S.pth' ) # DeepFM test_df = eval_model( 'DeepFM-%m-%d-%Y_%H-%M-%S.pth' ) 作成された test_df にはランキングメトリクスが計算出来るようにuser_id、真値、予測値が書き込まれています。   user_id target pred 1 1 1 0.9 2 1 1 0.7 3 1 0 0.3 4 1 0 0.1 test_df を元にテストデータに対するメトリクスを計算することで、 アルゴリズム 間の比較検証が出来ます。   ROC -AUC Logloss Recall@20 MAP@20 MRR@20 FM 0.865 0.105 0.491 0.489 0.577 NFM 0.843 0.110 0.502 0.470 0.540 AFM 0.865 0.103 0.507 0.487 0.568 DeepFM 0.862 0.101 0.523 0.522 0.598 おわりに 今回は、多様なレコメンド アルゴリズム を検証できるRecBoleを活用した実験の手順について紹介しました。 この記事が、レコメンド アルゴリズム 構築に関わる方々の助けに少しでもなれたら嬉しいです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! *1 : PyPI : https://pypi.org/project/recbole/ *2 : 例: CTR予測など *3 : RecBoleが学習に用いる データ形式
アバター
この記事は Timee Advent Calendar 2023 の7日目の記事です。 qiita.com こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小栗です。 本記事では、メンバーの相互理解を深めるためにDSグループで取り組んでいる施策を紹介します。 そもそもの課題感 以下の要素により、DSメンバー間の相互理解が今後難しくなりそう…という課題感が当時あり、諸々の施策をスタートさせました。 フルリモート前提の働き方をしている チームメンバーの数がすごい勢いで増えてきた(1年で2.5倍に) メンバーがそれぞれ担当する部署横断PJがいくつも並行に走っており、逆にチーム内での接点が少なくなってきた メンバーの相互理解を深めるためにやっていること データ統括部やDSグループで取り組んでいる取り組みは他にもたくさんありますが、今回は以下の2つに絞って紹介します。 スキルマップ共有会 ストレングスファインダー共有会 スキルマップ共有会 スキルマップとは、業務に関係あるハードスキルとそれに対する各メンバーの習熟度を可視化するツールです。 スキルマップの作成・共有をする目的は主に2つあります。 各メンバーが持つハードスキルを把握し、相互理解に繋げる 業務に関係あるスキルと、そのスキルに自信がある人を把握できるようにする 具体的には、下図のようなスキルマップを スプレッドシート で簡単に作成しました。 スキルマップの一例 社内や社外のスキルマップを参考にしつつも、タイミーのDSグループに強く関係するスキルや技術を独自にピックアップしています。 具体的には、「データサイエンス」「データエンジニアリング」「ビジネス」「アカデミック ドメイン 」「業界 ドメイン 」の軸でそれぞれ10~20個ほど要素(≒ スキル)を選定し、軸ごとにスキルマップを作成しています。 また、スキルの習熟度とは別に、「今後伸ばしたい」スキルも可視化するようにしています。 工夫した点(というか難しい点)として、各スキルの習熟度に対して厳密な基準を設けない形にしています。 厳密な基準を設けるのは難しいこと、そして、相互理解が目的なのでメンバーそれぞれの各スキルに対する「自信度」がザックリわかればいい、といった理由から、現在の形で運用しています。 定期的に各メンバーはスキルマップへの記入を行います。 その後に共有会を開催し、1人ずつ自分のスキルを発表して、それに対する感想や質問をする、という形で運用しています。 共有会で各メンバーの 保有 スキルや今後伸ばしたいスキルが分かるため、それを通して相互理解が深まり仕事がしやすくなる、という想いで運用しています。 ストレングスファインダー共有会 ストレングスファインダー は、米国のギャラップ社が開発した「強みの診断」ツールです。 WEB上で診断を受けると、34の資質の中から自分の強みや資質を知ることができます。 スキルマップはハードスキルに着目していましたが、ストレングスファインダーではソフトスキルに焦点が当たっており、DSグループでは棲み分けをする形でどちらも運用しています。 新メンバーがオンボーディングの過程で診断を受けるフローになっており、メンバーが増えるごとに都度共有会を開いています。 共有会では、1人ずつ自分の強みを発表して、それに対する感想や質問をする、という形で賑やかに開催しています。 また、診断結果を盲目的に信じて決めつけたコミュニケーションをするのではなく「この診断結果は的確/そうでもない」といった会話も挟むなど、あくまで診断結果を踏み台にして生まれる会話によって相互理解を深めています。 ストレングスファインダーの診断結果を通して、普段の業務では把握できない各人の「強み」や「考え」がわかるのが魅力だと思っています。 ※データ統括部BIチームのyuzukaさんが執筆した ストレングスファインダーに関するアドベントカレンダー記事 もあるため、そちらもぜひご一読ください! おわりに この記事では、DSグループが実施しているメンバー相互理解のための取り組みを紹介しました。 データ統括部およびDSグループは今後もメンバーを増員する予定なので、施策をさらにアップデートしていきたいと考えています! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう!
アバター
この記事はTimee Advent Calendar 2023シリーズ 2の5日目の記事です。 はじめに DREグループの石井です。 今回はDREグループの管理するデータ基盤に関するインフラのterraformのテスト環境の話をしようと思います。 導入前の課題感 我々のチームではデータ基盤として複数の GCP Projectを管理していますが、その全てをterraformで管理しています。 この時点でGithubActionsによる自動テスト(validate, plan) 及び 自動デプロイは導入されていたため、レビューさえ通れば誰でもインフラの変更を反映できる状態になっています。 しかし、この時点でよく起こっていた問題として以下のようなものがありました。 validationが実装されていないリソースの 命名規則 などでデプロイ時に落ちる 実際にapplyして試してみたいけど、デプロイ先が本番しかない そこでPRごとに GCP Projectを作成しその中でapplyを実際に試せる仕組みを実装して、しばらく運用してみたので実装からその所感までをまとめてみようと思います。 やったこと PR時にテスト環境が作成するために、ざっくりいうと以下のようなことをやっていますので、それぞれ詳細に記載しようと思います。 リポジトリ 構成の変更 CIで実際にapplyされるときのリソース名の調整 GithubActionsの実装 リポジトリ 構成の変更 元々、1つの Github Repositoryで全ての GCP Projectを取り扱っていたのですが、元々は以下のような構造でした。 (必要なところのみ抜粋) envs/ GCP_ProjectA/ 各種terraform .. GCP_ProjectB/ modules/ module_A/ module_B/ global.tfvars ... ユーザの管理等 envs配下にproject単位で切られており、modulesは共通の部品だけ切り出しておくという構成です。 これを以下の様に変更しています。 envs/ GCP_ProjectA/ environment/ prod/ main.tf backend.tf ...            test/ main.tf backend.tf modules/ module_X/ 各種terraform .. GCP_ProjectB/ modules/ module_A/ module_B/ global.tfvars ... ユーザの管理等 プロジェクトを跨いでグローバルに使用していたモジュールはそのままとして、モジュール化されていなかったプロジェクト固有のterraformファイルをモジュールとしてまとめています。 そして、main.tf内でモジュールを呼び出すという一般的なモジュール構成に似た形になっています。 リソース名の修正 リソース名もvalidation対象ではあると思うので本来はそのまま適用していきたいのですが、GCSのようなグローバルに一意にしないといけないリソースはこのまま実行するとテスト環境を壊すのと本番環境を作るときのラグ等でリソース名が利用できなかったりして困るケースがあります。 そのため gcs_suffix というvariableを用意しておいて、module側で例えば以下のようにしています。 resource " google_storage_bucket " " gcs " { name = " test${var.gcs_suffix} " .. } default値を ""(空文字) としておくことで、本番環境には影響を与えないようにしています。 Github Actionsの実装 それでは上で整理したモジュールを使ってテスト環境を生成する部分の話に移ります。 基本的にはPJを作るテンプレートを別途用意しておき、それをコピーしてきてapplyしてプロジェクトを作成、その後、environment/test内をapplyする、という流れになっています。 project.tf (Projectの作成) resource " google_project " " test-environment " { billing_account = var.BILLING_ACCOUNT_ID name = local.project_id project_id = local.project_id org_id = var.ORGANIZATION_ID } resource " google_billing_budget " " budget " { depends_on = [ google_project.test-environment ] provider = google-beta billing_account = var.BILLING_ACCOUNT_ID # 消し忘れ対策にバジェットを指定 amount { specified_amount { currency_code = " JPY " units = 10000 } } budget_filter { projects = [ " projects/${google_project.test-environment.number} " ] } threshold_rules { threshold_percent = 0.5 ... } services.tf (サービスの有効化) resource " google_project_service " " service " { depends_on = [ google_project.test-environment ] for_each = local.services project = local.project_id service = each.value disable_dependent_services = true } (variable等は割愛します) これをapplyした後に実際のtest配下をapplyするのですが、backend設定だけ差し替えないといけないため、以下のようなテンプレートを用意して書き換えています。 backedn.tf.template terraform { backend \ " gcs\ " { bucket = \ " terraform-backend-bucket-name\ " prefix = \ " ci_projects/${PROJECT_ID}/projects.tfstate\ " } } これらを用いて、以下のようなActionsになりました。なお、PJがかなり多いため実際によく変更されるプロジェクトのみを対象としたかったのでそうなるようにしています。 name: terraform-test-apply on : pull_request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GOOGLE_BACKEND_CREDENTIALS: ${{ secrets.GOOGLE_BACKEND_CREDENTIALS }} GOOGLE_CLOUD_KEYFILE_JSON: ${{ secrets.TIMEE_CORE__GOOGLE_CREDENTIALS }} jobs: set -matrix: runs- on : ubuntu-latest outputs: target_project: ${{ steps.get-diff.outputs.value }} steps: - uses: actions/checkout@v3 - name: Fetch changes run: git fetch origin ${{ github.base_ref }} - name: id: get-diff run: | diff=$( echo " $(git diff --name-only origin/master..HEAD | \ cut -d'/' -f2 | \ grep -e 'Project1' -e 'Project2' -e 'Project3\' | \ jq -R . | jq -s . | jq -c '.|unique') " ) echo $diff echo " value=${diff} " >> $GITHUB_OUTPUT apply: needs: set -matrix if : ${{ needs. set -matrix.outputs.target_project != ' [] ' }} strategy: fail-fast: false matrix: PROJECT_NAME: ${{fromJson(needs. set -matrix.outputs.target_project)}} runs- on : ubuntu-latest env: PROJECT_ID: " tf-test-${{ matrix.PROJECT_NAME }}-${{ github.event.number }} " PR_NUMBER: ${{ github.event. number }} steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 - name: create project tf and apply run: | pushd tests/project eval " echo \ " $(cat ../templates/backend.tf.template )\ "" > backend.tf terraform init - lock = true - lock -timeout=60s terraform apply -var= " PROJECT_ID=${PROJECT_ID} " -auto-approve -var- file =../../global.tfvars - lock = true - lock -timeout=60s project_number=$(terraform output google_project_number | head -n 2 | tail -1 ) echo " project_number=$project_number " >> $GITHUB_ENV popd - name: apply `iam/` run: | pushd envs/${{ matrix.PROJECT_NAME }}/environment/test/ terraform init -backend-config= " prefix=${{ matrix.PROJECT_NAME }}/${PROJECT_ID}/test.tfstate " terraform apply -var= " project=${PROJECT_ID} " -auto-approve -var- file =../../../../../global.tfvars - lock = true - lock -timeout=60s -parallelism= 20 - name: Terraform apply Link uses: actions/github-script@v6 with : result-encoding: string script: | github.rest.issues.createComment({ issue_number: context.issue. number , owner: context.repo.owner, repo: context.repo.repo, body: " ### Test Apply of `${{ matrix.PROJECT_NAME }}` :rocket: " + " \n " + " Link: https://console.cloud.google.com/welcome?project= " + process.env.PROJECT_ID }); やってみた感想と課題 正直1PRごとに1GCP Projectを立てるのはやりすぎかなと思っていましたが、実際立っていると確認はしやすく、かつ他の影響も受けないため特に新しい機能を開発するようなタイミングでは大変良かったように思います。 ただ、現実問題としてapplyにかかる時間がだいぶ長いという問題はあり、環境によっては4,50分かかっていたこともありました。 検証する必要性の薄いリソースを対象外とするなど色々改善しましたが、それでも軽微な変更をするのにもこれを回さないといけないというのはやりすぎでは?という側面も正直あるかなと思っています。 このあたりは程度問題な気もするので、今後も見極めていければとは思っています。  個人的には初めて行う設定などでterraformの書き方にやや自信がないものをある程度自信を持ってレビューを出せるようになったことに最も価値を感じているところではあります。 We’re Hiring DREグループではまだまだやっていきたいことがたくさんあるのですが、まだまだ手が足りておら ず、ともに働くメンバーを募集しています!! データエンジニアのポジション データに係る他のポジションやプロダクト開発などのポジションも絶賛募集中なので こちら からご覧ください
アバター
こんにちは、CTO室グループでQA スペシャ リストを担っている依光です。 今年を振り返ってという視点から、施策として動き始めた「障害対応をFactから改善する取り組み」について紹介させてください。 今までの取り組みと課題 タイミーのプロダクト部ではFour Keysを活用して改善サイクルに取り組んでおり、 プロダクトの品質を改善するという側面から「変更失敗率」と「サービス復元時間」を 計測しています。 この「サービス復元時間」を短縮するに当たり、障害を時系列にまとめて事後検証として 振り返るポストモーテムだけでは、改善するポイントを客観的に判断することが難しい という課題がありました。 取り入れた施策 障害発生の時間を短縮するために発生時間の内訳を計測して、客観的に判断するようにしました。 まず計測する際に時間を分解する切り口は、O'Reilly社から出版されている「Seeking SRE(SREの探求)」に記載されているプロセスを参考に収集するようにしました。 図の 参照元 : https://www.oreilly.com/library/view/seeking-sre/9781491978856/ch04.html TTD:検出時間 TTE:エンゲージ時間( エス カレーションプロセスの時間) TTF:修正時間 TTM:軽減時間(障害発生から対応が完了するまでの時間) 続いて客観的に判断するために、各障害のレベルごとにTTD、TTE、TTF、TTMの目標指標を定義しました。 表:障害レベルと各プロセスの目標指標の例 施策を取り入れた効果 ポストモーテムの実施タイミングで、発生した障害のレベルに応じて TTD、TTE、TTF、TTMが目標指標内に収まっているかを確認する ステップが追加されました。 そして未達の指標に対して改善案を深ぼるアプローチができるようになり 障害発生時間の短縮に結びつく改善に繋がりやすくなりました。 指標例:TTD(検出時間)が未達だった 改善例:メトリクスの追加とアラートについて検討する 今後の取り組みに向けて 今回紹介した施策は、時系列にまとめて事後検証を行うポストモーテムだけでは 見つけられなかった改善ポイントが、障害発生時間の内訳時間というFactを基に、 客観的に把握して品質改善ができるようになった1例だと考えています。 今回の事例だけでなく、今後もタイミーのプロダクト本部ではFactを基にした 品質の維持、向上の取り組みを大切にしていきたいと考えています。 私達の取り組みにご興味がありましたら、 情報交換など気軽にご連絡下さい。 最後まで読んでいただき、ありがとうございました。
アバター
こちらは Timee Advent Calendar 2023 シリーズ1の5日目の記事になります。 昨日は @redshoga による Vercel REST APIを用いたステージング環境反映botについて で明日は @yama_sitter による フロントエンドアプリケーションの認知負荷とテスタビリティに立ち向かう です。 タイミーでバックエンドエンジニアをしている id:euglena1215 です。 タイミーはユーザー向け・企業向け・社内向けの機能を1つの Rails アプリケーション上で動かしています。 10/5に Rails 7.1 がリリースされ、タイミーも11/1に 7.1.1 に上げることができました。現在は Rails 7.1.2 が本番で元気に動いています。 Rails 7.1.1 へのアップグレードは比較的スムーズに行うことができたものの、アップグレードのプロセスにはまだ改善の余地があると感じました。今回はどこに改善の余地があると思ったのか、具体的な改善の取り組みについて紹介したいと思います。 背景・課題 指標その1:Code Line Coverage Code Line Coverage とは 指標その2:Endpoint Coverage 改善その1:index actionのテストめっちゃ書く 改善その2:使われていない batch_action の削除 まとめ 背景・課題 タイミーでは、 Rails アップグレードの動作確認は手動での動作確認をせずとも自動テストで動作を担保して問題ないという合意が得られています。これはテストの網羅性が高く(Code Line Coverage が 91%)十分に動作が担保できているだろうという前提があるためです。 しかしこのルールにもいくつかの例外があります。例えば、暗号化 アルゴリズム の変更やキャッシュフォーマットの変更、 Cookie の属性変更など普段書くアプリケーションロジックの自動テストでは検出が難しいと考えられるものです。これらの例外は毎アップグレードで確認すべき箇所が異なり自動テストで検出するのは現実的ではないと考えているため、自動テスト以外の方法で動作を担保しています。 一方、他の例外として「社内向け管理画面は手動でのチェックを行う」というものがあります。これは社内向け管理画面はテストコードの網羅性が低く、自動テストを信頼できないというのが理由です。理由としてはもっともだと思いますが、ユーザー向けの機能は手動でのチェックをしなくていいのに、社内向けの機能は手動でのチェックが必要なのはチグハグさを感じました。 今回はこの 社内向け管理画面はテストの網羅性が低く自動テストを信用できないため、 Rails アップグレードの手間が増えている ことが課題だと捉えました。 指標その1:Code Line Coverage テスト カバレッジ が低いのならテストを書けばいいじゃないということで、社内向け管理画面 ( /admin/* )の Code Line Coverage を指標としテストを書き始めました。 Code Line Coverage とは 「Code Line Coverage」は、 ソフトウェアテスト の際に使用される指標です。その目的は、自動テストによって実行された ソースコード の行数の割合を測定し、どの程度のコードがテストされているかを把握することにあります。計算方法は、テストで実行されたコード行数を全コード行数で割り、それを100倍してパーセンテージで表します。この指標はテストの カバレッジ を 定量 的に評価するために使われます。 タイミーの Rails アプリケーションでは codecov を使って以下のようにテストで実行されたコード、実行されていないコードを可視化しています。 この時点での app/admin/* ディレクト リの Code Line Coverage は 75% でした。 前提として、タイミーでは社内向け管理画面を実装するために ActiveAdmin gem を利用しています。 ActiveAdmin gem を使うこと自体には社内でも賛否両論ありますが、大いなるデメリットもあれば大いなるメリットもあるということで使い続けています(この辺りの話に興味があればぜひ話を聞きに来てください。カジュアル面談は こちら )。 問題とは、ActiveAdmin を使って管理画面を実装した場合 Code Line Coverage と体感のテスト カバレッジ が一致しないことです。 代表例を挙げます。下記は User モデルに対応した管理画面を実装するコードです。 この2行を書くだけでいくつのエンドポイントが定義されるかを確認してみましょう。 # app/admin/users.rb ActiveAdmin .register User do end 下記は上記2行の実装によって定義されたエンドポイントの一覧です。2行書くだけで9つものエンドポイントが定義されています。 root@ba8730884fed:/usr/src/app# bundle exec rails routes | grep admin/users batch_action_admin_users POST /admin/users/batch_action(.:format) admin/users#batch_action admin_users GET /admin/users(.:format) admin/users#index POST /admin/users(.:format) admin/users#create new_admin_user GET /admin/users/new(.:format) admin/users#new edit_admin_user GET /admin/users/:id/edit(.:format) admin/users#edit admin_user GET /admin/users/:id(.:format) admin/users#show PATCH /admin/users/:id(.:format) admin/users#update PUT /admin/users/:id(.:format) admin/users#update DELETE /admin/users/:id(.:format) admin/users#destroy また、ActiveAdmin は動的にルーティングを生成するため、 /app/admin/* ディレクト リ以下のファイルは Rails 起動時に読み込まれ評価されます。そのため、 app/admin/users.rb に対応するテストを1つも書かなくても app/admin/users.rb の Code Line Coverage は 100% になります。 もちろんこれは極端な例で、普段は一覧の要素を変更するなど実際にアクセスしないとカバーできないコードが生まれるため Code Line Coverage が 100% になるケースは少ないです。だとしても対応するテストがないのに 50%以上のファイルがいくつかあったりと全体的に高く出過ぎているように感じました。 ActiveAdmin は高機能な DSL によって数行でいくつもの画面を生成できます。そのため、コード量の機能量が比例しません。よって、ActiveAdmin による管理画面の実装において Code Line Coverage は指標として不適切だろうと判断しました。 指標その2:Endpoint Coverage タイミーでは一番外側のテストとして system spec は書かず request spec を書いています。エンドポイントに対応したテストが1つ以上あれば一定動作は保証されているだろうと考え、 /admin/* のエンドポイントに対して request spec が何割カバーできているかを指標として改善をしていくことにしました。 具体例 GET /admin/users テスト🙆‍♂️ GET /admin/users/:id テスト🙆‍♂️ POST /admin/users テスト🙅‍♂️ ↓ ↓ ↓ ↓ ↓ ↓ テストされたルート数: 2 テストされていないルート数: 1 全ルート数: 3 カバレッジ: 66.67% ここでは Endpoint Coverage と呼称することにします。(正式名称あれば教えてください。訂正します。) なるべくシンプルな方法で Endpoint Coverage を集計することにしました。ステップは以下です。 rails routes 相当の情報を取得 spec/requests/* ファイルを読み込みルーティングに対応する describe 句を抜き出す(describe 句があればテストケースは1つ以上あるだろうと判断) 1.と2.で得られたデータを組み合わせて、テストされているエンドポイント・テストされていないエンドポイントを分類し、割合を算出する 集計用の Rake タスクは以下になります。興味があれば見てみてください。 エンドポイントごとのテストカバレッジを計測するための rake タスク · GitHub 上記の Rake タスクをタイミーの Rails アプリケーションで実行すると以下の結果が得られました。 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 165 テストされていないルート数: 367 全ルート数: 504 カバレッジ: 32.74% root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect テストされたルート数: 457 テストされていないルート数: 445 全ルート数: 827 カバレッジ: 55.26% 確かに admin は カバレッジ が 32.74% と高くないことが分かります。体感としてもそのくらいです。 次に全体の カバレッジ は 55.26% でした。こちらの結果には admin も含まれているため、admin を抜くと 78.9% です。 上記の結果より、admin とそれ以外とでは2倍以上の開きがあることが分かりました。これならテストの網羅性が低いという判断も納得できます。ということで、Endpoint Coverage を指標として用い社内向け管理画面のテストの網羅性を高めていくことにしました。 また、「社内管理画面の手動での動作確認では機能の60%もカバーできていないだろう」ということで Endpoint Coverage は 60% を目標として進めていくことにしました。 改善その1:index actionのテストめっちゃ書く Endpoint Coverage の集計 Rake タスクの副産物として、テストが書かれていないエンドポイント一覧を入手しました。これを元にテストの拡充を進めていきたいと思います。 まずは、index action に対応する request spec を書いていくことにしました。テストケースとしては 200 を返すことを検証します。 この判断をした理由は以下の通りです。 index action は一覧を取得するためのエンドポイントであり、多くの利用者は一覧画面を起点にして操作を行うため比較的重要度が高い index action の 200 を返すテストはある程度 機械的 に追加できるため、テストを追加するのが楽 spec ファイルが元々あるのとないのとではテストを追加する際の 心理的 ハードルが異なるため、とりあえずファイルだけでも作っておくことで他のエンドポイントのテスト追加がされやすくなるのではという期待 というわけでテストを書き始めていると嬉しい誤算に気付きました。それは GitHub Copilot がかなり補完してくれることです。 # frozen_string_literal: true require ' rails_helper ' RSpec .describe ' Admin::User ' do describe ' GET /admin/users ' do # ここまで書くと... end end # frozen_string_literal: true require ' rails_helper ' RSpec .describe ' Admin::User ' do describe ' GET /admin/users ' do # 以下を全て補完してくれる! subject { get admin_users_path } context ' 正常系 ' do before do create_list( :user , 2 ) end it ' 200を返す ' do subject expect(response).to have_http_status( :ok ) end end end end テストが書かれていないエンドポイント一覧は手元にあるため、ほとんどエンドポイントをコピペするだけで index action に対応する request spec を増やしていくことができました。 その結果、以下のように 32.74% → 41.47% まで カバレッジ を伸ばすことができました 🎉 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 209 テストされていないルート数: 326 全ルート数: 504 カバレッジ: 41.47% 改善その2:使われていない batch_action の削除 テストが書かれていないエンドポイント一覧を眺めていると、多くのリソースに対して batch_action エンドポイントが生えていることが分かりました。これは一覧画面で各リソースにチェックを入れて一括で削除するといった処理を行うために ActiveAdmin が用意しているものになります。 Active Admin | The administration framework for Ruby on Rails batch_action をデフォルトで有効にするかどうかは設定で変更可能なのですが、タイミーでは有効になっているようでした。 「一括操作ってそんなに行うことある?」と思い過去1年間のログを確認したところ、batch_action が使われているリソースは1つしかありませんでした。そのため、全体では無効化し使われているリソースにのみ batch_action を有効化しました。 これまで定義されていた batch_action は気付かず定義されていたのか、開発者による善意のものだったのかは判断できませんが Code Line Coverage では気付くことが難しく Endpoint Coverage を見ていたからこそ気付けたものかなと思っています。 その結果、以下のように 41.47% → 48.98% まで カバレッジ を伸ばすことができました 🎉 root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin テストされたルート数: 217 テストされていないルート数: 257 全ルート数: 443 カバレッジ: 48.98% 目標としていた Endpoint Coverage 60% にはまだ届いていませんが、元々が 32.74% だったことを考えるとかなり近づいたのではと思っています。 まとめ Rails アップグレードの手間を減らすために社内向け管理画面のテストを拡充させようと考え、Endpoint Coverage という指標を定義しました。Endpoint Coverage を改善するためにいくつかの改善を行い、約30%から約50%と目標の60%に近づけることができました。Endpoint Coverage を定義し、テストされていないエンドポイント一覧がわかったことで様々な改善ア イデア が思いついたように感じます。 これからも Endpoint Coverage を高めていき Rails 7.2 アップグレードでは手動での動作確認が必要なくなるよう頑張っていこうと思います。
アバター