TECH PLAY

株式会社LIFULL

株式会社LIFULL の技術ブログ

656

エンジニアの市川と申します。 LIFULL HOME'S の売買領域の開発を担当しています。 さて、さっそくではありますが、読者の皆さんは普段ABテストを実施しているでしょうか。 私たちの開発しているLIFULL HOME'Sでも、日々多くのABテストが実施されています。 ABテストの実施によって市場学習回数を増やし、より良いプロダクトを作り上げることが目的です。 その中で私たちエンジニアが貢献できる点といえば、市場学習のスピードを上げることです。 LIFULL HOME'Sでは長年ABテストを実施してきていますが、いくつかの問題がありました。 今回はその問題と、どのように解決に向けて動いたのかという点について紹介します。 ※LIFULL HOME'SでのABテストにつきましては、以下のリンクをご参照ください。 A/Bテストは事前準備で決まる!?LIFULLのA/Bテスト事前設計の取り組み|LIFULL Product Growth ABテストにまつわる問題 ABテストを実施する1サイクルの期間が長い 各マイクロサービスを開発する度にABのテストの仕組みが必要になる 解決のための取り組み 共通のABテスト基盤の開発 設定情報の独立化 まとめ ABテストにまつわる問題 ABテストを実施する1サイクルの期間が長い まず、1点目の問題としては、ABテストを1回実施するまでの期間が長いことでした。 LIFULL HOME'Sのプロダクトには、独自のABテストの仕組みが実装されています。 そのABテストの仕組みを使うと下記のような手順と、期間がかかります。 gantt axisFormat %m/%d title 従来のABテスト section ABテスト実装 ABテストの設定情報を設定ファイルに追記 :active, des1, 01/01,1d 各パターンの挙動を実装 :active, des2, after des1, 3d テスト :active, des3, after des2, 1d リリース :active, des4, after des3, 1d 計測期間 :active, des5, after des4, 7d section Bパターン100%適用対応 設定ファイルに記載されている比率を修正 :crit, des6, after des5,1d テスト :crit, des7, after des6, 1d リリース :crit, des8, after des7, 1d Aパターン50%, Bパターン50%で設定し、計測期間が1週間という短めの例です。 ABテストを実装し、Bパターンに有意差ありと判断した場合は、なるべく早くBパターンを100%にしたいです。 しかし、設定ファイルの比率を書き換えるだけでもプロダクトコードのリリースが発生するため、最短でも翌日のリリースとなってしまいます。 1日とはいえ、LIFULL HOME'Sのトラフィックを考えるとなるべく即時反映させたいです。 各マイクロサービスを開発する度にABのテストの仕組みが必要になる 2点目の問題です。 弊社ではLIFULL HOME'Sというプロダクトをメインに開発・運用していますが、このプロダクトはいくつかのマイクロサービスに分かれています。 そして、各サービスにABテストの仕組みが実装されています。 例: LIFULL HOME'S 賃貸基盤刷新におけるABテスト実施システムの構築 - LIFULL Creators Blog graph TD A("ABテスト") B("ABテスト") C("※ABテスト") D("ABテスト") L --- S L --- R L --- M subgraph L["LIFULL HOME'S"] A end subgraph R["賃貸"] B end subgraph S["売買"] C end subgraph M["マイページ"] D end 今後もプロダクトの成長に伴い、新たにマイクロサービスが生まれる可能性はあります。 その度にABテストの仕組みを実装していてはコストが高いです。 現に売買領域のアーキテクチャリプレイスのプロジェクトでは、新しくマイクロサービス化されたアプリケーションが存在しますが、初期段階ではABテストの仕組みを実装していませんでした。 www.lifull.blog ※突貫的に外部サービスを利用し、画面の表示要素を差し替えてABテストを行っていた時期もありましたが、開発体験の良いものではありませんでした。 解決のための取り組み 共通のABテスト基盤の開発 前述した問題を解消するために、各マイクロサービス共通で利用できるpackageを開発しました。 弊社で基盤刷新を目的としたマイクロサービスのアプリケーションは、Node.jsを利用するよう統一化が図られています。 従って、npm packageとしてABテストの仕組みを提供すれば各アプリケーションで利用可能になります。 graph TD 売買-->ABテスト 賃貸-.->ABテスト マイページ-.->ABテスト 現在は売買のアプリケーションでのみ利用している状況ですが、ゆくゆくは統一していきたいと考えています。 設定情報の独立化 また、ABテストの設定情報に関しては、従来のアプリケーション内のファイルに追加する方法ではなく、独立したリポジトリで管理するようにしました。 graph TD A(ABテスト package) -->|read| S[(S3)] R[GitHub ab setting repository] -->|upload| S R -->|PR merge| R ABテストの設定情報を管理するためのGitHub repositoryを用意し、PRをマージするとその設定情報がS3にアップロードされるような仕組みです。 S3にアップロードされると、その設定情報がアプリケーション側に即時適用されます。 このように独立化させることで、本プロダクトへの影響を気にすることなくレビューや承認が行えるほか、プロダクト側のリリースフローに乗せる必要がなくなります。 結果的に、Bパターン採用の意思決定をしてから早ければ数分で100%適用を実現できます。 gantt axisFormat %m/%d title 現在のABテスト section ABテスト実装 ABテストの設定情報を設定ファイルに追記 :active, des1, 01/01,1d 各パターンの挙動を実装 :active, des2, after des1, 3d テスト :active, des3, after des2, 1d リリース :active, des4, after des3, 1d 計測期間 :active, des5, after des4, 7d section Bパターン100%適用対応 設定ファイルに記載されている比率を修正 :crit, des6, after des5,1d まとめ 今回は共通のABテストの仕組みについて紹介しました。 この共通のABテスト基盤を通して、開発プロセスの効率化を図り、よりすばやく市場のニーズに応えるプロダクトを提供できるようになりました。 まだ導入フェーズですので、作ったものを最大限生かせるように働きかけていこうと思います。 ともに良いプロダクト作りをしてくれる仲間を募集しています。 hrmos.co hrmos.co
賃貸領域でフロントエンドエンジニアをしている齋藤です。 今回はここ数年取り組んでいたフロントエンド領域における自動テスト導入とテスト工数削減について、書いていきたいと思います。 なぜ導入したのか、導入して見えた課題、そしてその課題を解決するためにどうしていったのか、という流れで書いていきます。 目次 目次 なぜ導入したのか たびたび障害が発生 開発効率が悪い 自動テストを導入する 自動テストの導入 導入して見えてきたもの ほぼコードを書かない解決策 まとめ お知らせ なぜ導入したのか まず弊社では日々の機能開発は基本的にABテストを実施して効果を検証しながら進めていきます。 そのため、対象となるページが見られたか、対象となる要素が表示されたか・使われたかなどを計測する必要があります。 静的な単一ページであればそれほど問題はありませんが、動的であったり複数のページでそれを行うとなると正しく計測できているかをテストするケースが膨れ上がっていきます。 LIFULL HOME'Sにおいては、物件の検索結果ページがまさにその典型例で、さまざまな切り口で検索でき、またその切り口によって表示されるものが異なります。 意図した切り口で表示されていること、意図しない切り口では表示されないこと、ちゃんと使えるか、計測が正しく行えているかなどをABテストのパターン数×切り口数×テスト項目数でテストする必要があります。 ちなみに切り口は以下のようなものがあります。 マーケット(賃貸・マンション・一戸建て等) エリアの指定方法(都道府県、市区町村、路線・駅等) テーマ(条件を組み合わせたもの) タグ(物件に付けられたタグ) などなど、さまざまな切り口およびその組み合わせで表示できます。 そういった中、弊社で機能開発時にどんなテストをしているかというと、基本的にはスプレッドシート等で作ったテストケースを元に、 デバイス・ブラウザの組み合わせで手動テストを実施しています。 このテスト方法自体はシンプルで、テストを書くのはもちろん、実施するのも誰でもできるという面があります。 一方でテストするページが増えるなどテストケースが増えるとそれだけテスト工数が増えるという一面もあります。 日々機能開発・改修が行われているので、このテスト工数が開発効率に大きな影響を及ぼしてきます。 たびたび障害が発生 上記の通り、切り口の組み合わせで出るものが変わるような複雑なページにさらにABテストが組み合わさるというのは、障害が発生しやすいということでもあります。 当然というとおかしいですが、意図しないページに改修したものが影響していたということが何度も起こりました。 事前調査やコードレビュー等で影響範囲等を見ていても網羅することは難しいのです。 開発効率が悪い 複雑で障害発生しやすいとなると、調査も時間が掛かりますし、テストも手を抜けません。 結果として、物件の検索結果ページに機能追加等改修をかける際には、非常に多くのURLでテストを実施する必要があります。 精神衛生的にもストレスが掛かって良くないですね。 自動テストを導入する ではどうしていくかというと、設計やコード面での複雑性を解消していくというのが一つの方法です。 当然そういう活動も大小行われていますがそれらは一朝一夕では終わりません。 そこで、別の切り口として自動テストを導入し、開発効率を上げていこうということになりました。 自動テストの導入 導入に際しては、まずは開発効率が上がるのかが重要ですので、お手軽にできることから始めることにしました。 実はこれ以前からPuppeteerを使って要素の存在確認やスクリーンショットを撮るなどしていたということがありました。 ですので、使い慣れているということもあってPuppeteerを使った自動テストを行うことにしました。 ※ PuppeteerとはChromeを操作するためのAPIを提供するNode.jsのライブラリです。 導入して見えてきたもの いくつかの機能開発PJで自動テストを書いたのち、それらを共有して導入を進めていきました。 そこで見えてきたのは、テスト実施は効率化されるが、テストを書くということの障壁でした。 PuppeteerはNode.jsのライブラリであるため、普段JavaScriptを書かないエンジニアにとっては障壁があります。 また、テスト項目が増えればそれだけコードも増えるため、テスト作成だけでなくそのコードレビューにも時間が掛かるということもわかりました。 使い捨てに近いテストですので、テストを書くこと自体に対しての障壁があるということは、非常に大きな問題でした。 ほぼコードを書かない解決策 導入して見えてきたものは課題だけではありません。 いくつもの施策でテストを書いてきたことで、何をテストしたいかはおおむね見えてきました。 そこで、テストしたい内容ごとにコードを共通化しました。 ただしそれだけだと共通化したテストコード以外の部分のコードはまだ書かなくてはいけない。 なのでさらに踏み込んで、設定ファイルを読み込んで共通化したテストコードを実行できる様にしました。 const testCase = [ path , status: 200 , specs: [ ... { type : 'hasStyle' , title : 'ダイアログが表示されていないこと' , selector : '.dialog[aria-hidden="true"]' , styleValue : [ 'display' , 'none' ] , } , { type : 'click' , title : `ボタンをクリックする` , selector : 'button[data-target="xxx"]' , } , { type : 'waitForResponses' , title : `レスポンスが返ってくること` , waitForResponses : [ `https:// ${ domain } /result/` , ] , } , { type : 'hasElement' , title : `ダイアログが表示されていること` , selector : '.dialog[aria-hidden="false"]' , expected : true , } , ... ] , ] ; 設定の一例は上記の通りで、テストタイプとセレクタや値などを記載していく様な感じです。 見ての通り、テストしたいことも明確になりましたし、何よりもテストを書くことが楽になりました。 そのほかにもテスト設定をテンプレート化し、headlessモード設定、Cookieの設定、assets読み込みオンオフなどを別に切り出した設定ファイルで管理しています。 サーバレスポンスのHTMLのみでテスト可能な場合には、assets読み込みオフで非常に高速にテストが実行できるなど、テストの実行速度も向上しました。 まとめ 自動テストに取り組み始めてから2年、テストコードの共通化を図ってから約1年が経ちました。 この1年もテストできることを増やしたり、安定して実行できる様にするなど改善を続けてきました。 その結果として、誰でも気軽に自動テストを書ける環境が整い、自動テストを活用した開発がチーム全体に広がったと感じています。 副次効果として、テストだけではなく事前調査での活用にもつながり、非常に効率的な開発が進められるようにもなってきました。 当然、デバイスやブラウザの違いが気になる様な実装に関しては手動テストも行います。 それでもそれらは最低限に抑え、できるものは自動テストで行うというスタンスを今は取っています。 自動テストの活用により以前より安心して開発ができる様になりました。 今後も改善を続けつつ、より安心した開発に向けても取り組んでいきたいと思います。 お知らせ LIFULLではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは、エンジニアの中島です。 この記事は2024年2月〜3月のLIFULL社でのアクセシビリティ改善およびやっていき活動の報告です。 この活動報告は月次で出すかもしれないし出さないかもしれないくらいの温度感で運用されています。 さっそく2月と3月の合算記事となりました。 目次 目次 サービス改善 トップのフリーワード検索の入力欄に設定された誤ったroleの除去 物件情報の編集画面のフォームのアクセシビリティ対応 駅選択ページ内の絞り込み機能のフォーカス改善 物件一覧(ブランド名による検索)ページ内の絞り込み機能のフォーカス改善 物件画像一覧ページ内の「画像をもっと見る」ボタンのアクセシビリティ対応 売却査定フォームのバリデーションを改善 育成・啓発の取り組み アクセシビリティ1on1 WCAG解説書 輪読会 外部発表および発信 お知らせ サービス改善 本期間中の改善取り組みのターゲットはLIFULL HOME'S 不動産アーカイブのPCページです。 諸事情で発表できないものもありますが公開可能な取り組みを紹介させていただきます。 トップのフリーワード検索の入力欄に設定された誤ったroleの除去 アーカイブサイトのトップページにはフリーワードで物件を検索できる入力欄があります。 現在の仕様を確認したところ、comboboxではないただの入力フィールドなのですがcombobox風のroleが誤って設定されていました。 不要なroleを除去し、普通のtextboxとして扱う修正をしました。 物件情報の編集画面のフォームのアクセシビリティ対応 アーカイブサイトでは物件の所有者や詳しい方などが物件情報を編集リクエストするための投稿フォームが存在しています。 そちらのフォームを確認したところ、いくつかのアクセシビリティ要件上の問題が発見されました。 いくつかのフォームコントロールに適切な名前が設定されていない いくつかのフォームコントロールのフォーカスインジケータが見えない(あるいは見づらい) 入力エラーと項目の関連付け 全体エラーの読み上げ 入力ステップ表示に適切な現在地を示すマークアップがされていない これらの問題を解消する修正を実装しました。 ※仕様の調整がつかず全体エラーは現在のところ読み上げのみの対応となります。 駅選択ページ内の絞り込み機能のフォーカス改善 路線・駅から物件を絞り込む検索フロー中の駅を選ぶステップで、駅が所属する都道府県ごとに表示を切り替えるUIがあります。 そちらのUIのトリガとなるボタンにフォーカスが当たらない不具合がありました。 こちらもフォーカスがあたるよう、またそのフォーカスインジケータが視認できるよう修正を加えました。 物件一覧(ブランド名による検索)ページ内の絞り込み機能のフォーカス改善 ブランド名から物件を絞り込んだ際の検索結果ページに、表示された物件を都道府県で絞り込むためのUIがあります。 そのUIのトリガとなるボタンにフォーカスが当たらなかったり、開閉状態を正しく支援技術に伝えられたない等の問題の対応を行いました。 物件画像一覧ページ内の「画像をもっと見る」ボタンのアクセシビリティ対応 物件の画像をギャラリー形式で一覧表示できるページ内に存在する「画像をもっと見る」という表示ボタンがあるのですが、そちらがフォーカス不能・押下時にボタンが消滅するにもかかわらずフォーカスが表示されたコンテンツに移動しないといった問題がありました。 フォーカスを可能にし、フォーカス管理を行う修正をしました。 売却査定フォームのバリデーションを改善 物件の詳細画面内に、その物件を売却査定するフォーム(所有者向け)が存在します。 入力に不備がある際に、入力エラーについて記載がでるよう実装されていますが、支援技術でそれを検知することが難しい実装になっていました。 また、入力エラー時にボタンがdisabledになり、フォーカスができなくなるといった問題も合わせて確認できました。 これらを修正し、フォーカスは可能にしたまま、ボタンが有効でないことを伝え、押下時にエラー内容をフィードバックするように修正しました。 また個別のコントロールにも入力エラーの内容を関連付けるようにしました。 育成・啓発の取り組み アクセシビリティ1on1 本期間中は先月と同じくフロントエンドエンジニア6人、デザイナー1人に対して行いました。 内容はWCAGの解説、APG(aria authoring practices)の解説、coga-usableの解説、実際のアプリケーション開発時でのアクセシビリティ配慮に関する相談などが主なものとなります。 WCAG解説書 輪読会 アクセシビリティやっていき勢向けにWCAGの輪読会を隔週で行っています。 本期間中は別イベントとバッティングが何度かあったため、2024/02/05, 同03/18の2回のみ開催でした。 外部発表および発信 以前の月報記事でも触れましたが、昨年12月8日に行われた「アクセシビリティチームの立ち上げと成長する組織づくり」という勉強会で弊社嶌田が登壇しました。 その際の発表が記事になりました。 お知らせ LIFULLではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
エンジニアの渡邉です。普段はLIFULL HOME'Sの売買領域のエンジニアチームにて技術リーダーとして開発を担当しています。好きなNginxのモジュールはngx_small_lightです。 ここ数年、LIFULLの開発部門では「開発生産性」と「品質担保」の重要性が再注目されています。 LIFULL HOME'Sの主要なリポジトリは、10年以上にわたり運用され続けており、数多くの開発者が日々の改善に尽力しています。 しかし、長年にわたる蓄積によって、アプリケーションの要件を満たすための実装が複雑化し、現在では実装時に調査、開発、レビュー、テストのすべての工程でそれぞれ必要以上に時間がかかる結果となっており、開発の生産性を低下させています。 この問題に対処するため、LIFULL HOME'Sでは既存のアプリケーションから必要に応じてシステムを切り出し、部門ごとでの運用管理を行っています。 売買部門もこのアプローチでの分割を進め、不動産の「売買」に関わるアプリケーションを新基盤に移行しました。 そこで、今回はアプリケーションをリプレイスする際に考慮したことその成果についてお話させていただきます。 リニューアル前後のLIFULL HOME'S ユーザー体験の改善 物件詳細ページ 物件一覧ページ パフォーマンスの改善 LIFULL HOME'Sの抱えていた課題とその対応 開発プロセスやコードを簡素化したい 設計思想をわかりやすく伝達したい データ構造を把握しやすくしたい 成果 リリース速度の改善 設計意図の伝達の容易化 開発プロセスの工数削減 今後の課題 複数アプリケーションに適用したい処理の取り扱い クリーンアーキテクチャの依存関係保護の仕組み アプリケーションをどこまで分割して基盤刷新するか 終わりに リニューアル前後のLIFULL HOME'S ユーザー体験の改善 基盤刷新のタイミングでUI/UXの改善に踏み切り、レスポンシブデザイン化による体験の統一、アクセシビリティに配慮し、スクリーンリーダーやキーボードでも利用可能にするなど、WAI-ARIAを使用したUIマークアップやAccessible Perceptual Contrast Algorithm(APCA)の値が60を超えた高いコントラスト比を適用した結果、アクセシビリティスコアが改善されました。 物件詳細ページ 物件詳細 旧UI 物件詳細 新UI 物件一覧ページ 物件一覧 旧UI 物件一覧 新UI ※ 画像は開発中のものです。 パフォーマンスの改善 レスポンススピードの改善により、スムーズにページが閲覧できるようになりました。またページの読み込みパフォーマンス、インタラクティブ性、視覚的安定性が向上し、Core Web Vitalsのスコアも改善しています。 リプレイスしたアプリケーションには以下の技術を採用しています。 項目 技術 アーキテクチャ Clean Architecture フレームワーク Express, LoopBack x TypeScript HTML Preact x TypeScript CSS Tailwind CSS JavaScript Stimulus, Catalyst インフラ基盤      Kubernetes LIFULL HOME'Sの抱えていた課題とその対応 開発プロセスやコードを簡素化したい 現在、LIFULL HOME'Sは50人以上のエンジニアによって運営され、年間1000件以上のリリースが行われています。 開発が始まって10年以上が経過し、プロジェクトが成熟するにつれ、ソースコードの複雑性が高まってきました。この複雑性は、リリースの速度を落とし、ソースコードの品質を年々劣化させる原因の一つです。 開発プロセスには、調査、設計、実装、レビュー、テスト、リリースという一連の段階が含まれます。しかし、10年以上の開発を経る中で増大したコードの複雑さは、過去の仕様が入り組んでいることや、古い技術を使用しているため、各工程の進行を妨げ、プロジェクトの期間を不必要に延ばしています。 これに対処するために、効率的な開発プロセスとコードの簡素化に向けた取り組みが求められています。 元々、LIFULL HOME'Sは賃貸と売買の領域が共存するアプリケーションでした。これらの領域は表面的に似ているように感じられるかもしれませんが、実際には多くの点で異なっていました。それにもかかわらず、無理な共通化を進めた結果、賃貸と売買の領域での差異を吸収するようなコードが増加し、システムの複雑性が時間とともに増していきました。これにより、少しの変更が多くの部分に影響を及ぼし、売買領域の機能改修が賃貸領域に予期せぬ影響を与えることが頻繁に発生しました。この結果、リリースごとに他部署との調整や、調査、実装、テストの工数が増大しました。 そこで、新しい基盤への移行に当たり、元々同一のプロダクトとして扱っていたものを分離し、賃貸と売買を別々のプロダクトとして扱うことで、無理な共通化を排除しました。 設計思想をわかりやすく伝達したい LIFULL HOME'SはもともとMVCモデルで開発されていたアプリケーションでしたが、開始時は少人数であったため、設計方針が適切に伝達されていました。 しかし、時間が経過するにつれて開発者の数が増加し、初期の設計思想の伝達が不十分となりました。 この結果、レビュアーと開発者間の合意が優先されるようになり、当初の設計方針と異なる実装が増加しました。 これにより、多様な思想が混在したアプリケーションが形成されました。 この問題に対処するために、実装者とレビュアーの合意のみに頼らず、アプリケーションデザインを重視して設計方針を徹底的に管理し、見通しの良いアプリケーションを目指す方針に切り替えました。 その実現手段として、レイヤードアーキテクチャの一種であるクリーンアーキテクチャを採用し、責務を適切に設定し、依存関係を明確化しました。 また、既存のLIFULL HOME'Sでは似たようなドメインロジックの再実装が多くの箇所で頻繁に行われており、仕様変更時に多数の実装箇所を変更する必要がありました。 この問題に対応するため、不動産という扱いやすいドメイン特性を活かし、ドメイン層にドメインロジックを集約し、再利用可能なクリーンアーキテクチャを採用しました。 データ構造を把握しやすくしたい LIFULL HOME'SはもともとPHP、Rubyを主言語として開発されていたアプリケーションでした。 年数を重ねソースコードの量が増加していく中で、非常に多くのデータフローをたどり、さまざまなデータが変数に入ることが懸念される状況でした。 また不動産にまつわる情報も多岐に渡るため、正規化しづらく、余計にデータの内容に対する信頼は低い状態でした。 したがって、実装時にも型やデータ形式等の可能性をひとつずつ検証し、レビュー時も実際のデータを見てみないとわからないなど、調査、設計、実装、テストそれぞれの工数が大きくなり、品質的にも良くない状況にありました。 そこで、静的型付け言語であるTypeScriptを採用しました。 数ある静的型付け言語の中でもTypeScriptを採用したのは、フロントエンドとバックエンドの両方を同一の言語で実装できる利点と、全社の技術方針をTypeScriptに寄せようという動きがあったためです。 TypeScriptを採用した理由などの詳細な内容については、 以前寄稿されたこちらの記事に詳細が記載されていますので、気になる方はぜひこちらもご覧ください。 www.lifull.blog 成果 リリース速度の改善 アプリケーションを領域ごとに分割することで、アプリケーション内での責任範囲が明確になり、他のマーケットへの影響を避け、リグレッションチェックが不要となりました。 これにより、テスト時に考慮すべきポイントが減少しました。 さらに、自治権を自部署内で完結させたことで、調整コストの削減やリリースタイミングの管理が容易になり、最短でリリース承認から本番リリースまでを一日で完了できるようになりました。そのため、ユーザーへのプロダクト提供までの時間を短縮できました。 設計意図の伝達の容易化 厳格な制約を課すクリーンアーキテクチャを取り入れることで、独自の思想を持つアプリケーションの意図を理解するのが容易になりました。 クリーンアーキテクチャの採用により、後からプロジェクトに参加した開発者も迷わず開発を進めることができます。 開発プロセスの工数削減 クリーンアーキテクチャや静的型付け言語の採用により、強い設計思想やデータ構造の明確化が促され、設計に関する相談が減少しました。 また、データ構造の明確化やドメインロジックのドメイン層への集約化により、既存仕様の理解やバグ修正の工数が短縮されました。 静的型付け言語を採用したメリットも大きく型定義がしっかりしていることで実装者もレビュアーもデータを正しく認識したうえでソースコードが追えるようになり、工数の軽減に成功しました。 さらに、基盤の刷新により、パソコンとスマートフォンでの見た目を統一するレスポンシブデザインを採用して開発工数を削減しました。 今後の課題 複数アプリケーションに適用したい処理の取り扱い アプリケーションを分割することで、新たな課題が生まれました。 特定のドメインロジックを共通で利用したいといった要望や、ABテストを行うための方法などのどのアプリケーションにも必要になりそうなものをそれぞれのアプリケーションごとに実装したくないといった課題です。 このような状況に対応するため、効率的なリソースの活用を目指し、再利用可能なコンポーネントを慎重に選択し、パッケージ化による再開発の回避を進めています。 パッケージ化は一定の調整を必要としますが、このプロセスにより、多くのアプリケーションが一貫性を持つ方法で利益を受けることが可能になります。 完全にパッケージ化されたソリューションを提供するための調整はありますが、これは品質の向上と開発プロセスの高速化に向けての積極的な取り組みの一環です。 私たちは、必要なアップデートを定期的に行い、より少ない工数で高品質な成果を提供することに努めています。 クリーンアーキテクチャの依存関係保護の仕組み クリーンアーキテクチャに触れたことがある人なら、最初はどのレイヤーがどのレイヤーに依存して良いのかなど、覚えるべきことが多く苦労したことがあるでしょう。 クリーンアーキテクチャは強い制約があるため設計方針が崩れにくい一方で、最初に全体感を把握することは難しいです。 当初はCodeOwnerによるレビューを必須化し、依存関係の保全に努めていましたが、人間の目だけではどうしても抜け漏れが出てしまい、この方法が厳しいという結論に至りました。 そこで、dependency-cruiserによる制御を導入し、CodeOwnerのレビューに加えてシステマティックな保全を図りました。この方法では、dependency-cruiserの設定を徹底的に検証することで、違反が発生する可能性を格段に減らし、開発者がクリーンアーキテクチャを正確に理解していなくても、レイヤー間の依存関係による破壊を避けることができ、目標であったソースコードの品質保証を実現しました。 クリーンアーキテクチャを人の目だけで管理するのは困難です。したがって、このシステム化された保全方法を今後も積極的に導入していく予定です。 アプリケーションをどこまで分割して基盤刷新するか 新規に売買領域のアプリケーションを切り出すことになった際、2つの分割方法を検討しました。 それは以下の二つです。 BFF層とフロントエンド用アプリケーションのパターン 売買領域を一まとめにし、モノリスアプリケーションとして運用するパターン これらのアプリケーションを運用した経験から、それぞれに特有のメリットとデメリットが明らかになりました。 BFFとフロントエンドを分離したアプリケーションでは、フロントエンドのみの変更を行いたい場合、最小限の調査と実装コストでリリースが可能です。 また、バックエンドエンジニアとフロントエンドエンジニアの担当領域が完全に分かれているため、開発のしやすさとコンフリクトのリスク低減のメリットがあります。 I/Oの仕様が合意されていれば、各エンジニアが得意な方法で実装することが可能です。 さらに、BFF層を分けることで他のアプリケーションからのリクエストに柔軟に対応できる利点もあります。 ただし、リリース時には二度手間がかかるデメリットがあり、二つのアプリケーションに関わる修正を行う場合、リリースタイミングによるデータ不整合のリスクが常にありました。 そのため、データが変更されても問題なく機能するように実装する必要がありました。 また、BFFとフロントエンド間のデータ交換はすべてプリミティブな値で行われるため、フロントエンド側でも型定義が必要な二度手間が生じました。 モノリス形態のアプリケーションのメリットは、リリースの頻度が一度で済むため、リリースのタイミングを気にする必要がないです。 また、ドメイン層のValueObjectをフロントエンドでも利用できる設計にしているため、フロントエンド側で新たに型定義を作成することなく使用でき、ドメインロジックの利用が可能になりました。 これにより、フロントエンドでのロジック実装が削減できます。 しかし、このアプローチではフロントエンドエンジニアとバックエンドエンジニアの作業領域が衝突する頻度が増え、レイヤードアーキテクチャ内での役割分担の明確化が必要となりました。 以上のように、どちらのアプリケーションにも運用面、実装面での明確なメリットとデメリットがありました。 そのため、今後も最適な形を追求しながらアップデートを続けていく予定です。 終わりに アプリケーションの再構築には、我々に大きな変化をもたらす可能性が秘められています。課題感を徹底的に分析し、アプリケーションの役割を明確にすることで、そのシンプル性が増すことが期待されます。時には、既存のアプリケーションを思い切って捨て去り、新たな軌道に乗ることで、画期的な進化を遂げるチャンスをつかむこともあります。 当然、新しいアプリケーションへの移行プロセスは、時間がかかり非常に困難なものです。知られざる仕様や対応の漏れなど、予想外の障壁に直面することも少なくありません。 しかし、長期的な視野に立てば、ソースコードの知識を未来世代に継承するとともに、開発生産性を飛躍的に高める絶好の機会であるとも言えます。これは、時に勇気を持ってアプリケーションの全面的なリプレイスを決断する理由となります。 本稿では、技術的な詳細には深く踏み込んでいませんが、その点については今後別の機会で詳しく掘り下げていく予定です。 最後に、LIFULL ではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは! LIFULLエンジニアの吉永です。 普段はLIFULL HOME'SのtoC向けCRMチームにてエンジニアリングマネジャーをやっています。 マネジャーとなり、未経験分野へチャレンジしてくれるメンバーと接する機会が増えました。 自身が経験や知見のある分野であれば相談にのったりサポートはしやすいですが、未経験の分野となるとどのようにしてメンバーと接していくかは悩むことが多いと思います。 メンバーに「これお願い」と丸投げできると楽ですが、現実問題なかなか難しい場面も多いと思います。 マネジャーは自身がレビュアーになるか、レビュー対応できない場合は、他部署の有識者を募ってレビュアーやアドバイスをくれる人を確保する必要もあるでしょう。 本日はこんな悩みを抱えているマネジャーの方向けに、私なりにどんな風にメンバーと接したか、その結果どうだったかについて共有したいと思います。 アジェンダ プレイングマネジャーとして意識していることと不安や悩みについて 施策はどのようにして進行したか 施策の結果はどうだったか まとめ 最後に プレイングマネジャーとして意識していることと不安や悩みについて 意識していること 私はプレイヤー比重が少し多めのプレイングマネジャーをさせていただいておりまして、なるべく現場で自身の手も動かしていたいとは思っています。 一方、マネジャーに求められているのはチームとしての成果を最大化することですので、自身の手を動かすことが最適解ではない時はメンバーへ任せる、お願いすることも重要だと認識しています。 よって、プレイヤーとマネジャーとしてのバランスは常に意識しています。 どんな不安があったか プレイヤーとマネジャーとしてのバランスを考慮しつつ立ち回っていくと、おのずとチーム内でこぼれ球となった施策を拾って対応するということも多くなります。 もしこのこぼれ球が自分で対応できないものだったらどうしよう?という漠然とした不安はありました。 どんな悩みがあったか 実際にメンバーにも未経験分野へチャレンジしてもらう機会も増えてきたことから、マネジャーは未経験分野の技術をどこまでキャッチアップしたらよいか?という悩みがありました。 どこまでキャッチアップすべきか? メンバーで対応しきれない不測の事態が起きた際に自身で巻き取って対応ができる状態にまでキャッチアップできていることは理想だと思います。 ただ、自身の業務とマネジメント業務の傍らで詳細な部分までキャッチアップするのはなかなか大変です。 よってどこまでキャッチアップすべきか?についての線引きが非常に重要だと思います。 線引きはケースバイケースだと思うので、以降で紹介する機械学習を用いて物件をレコメンドするモデルを構築する施策をメンバーへお願いした際の話を通して、その施策でどこまでキャッチアップしたかを紹介します。 施策はどのようにして進行したか お互い手探り状態で施策はスタート メンバーも機械学習は未経験だったので、そもそもどこから手をつけようか?という状態で施策はスタートしました。 まずは二人で定期的にMTGを行い、お互いにインプットした情報を共有しあい、メモ書きにどんどんと追記していきました。 調査は分担しながら進行 機械学習にチャレンジしてみようというきっかけはBigQuery MLでした。 ※SQLでモデル構築が完結するので、学習用のデータをセレクトして各パラメータの調整を行うことでレコメンドモデルを簡単に構築できそうだという理由で選定。 メンバーにはBigQuery MLの詳細な利用方法の調査を、私は物件をレコメンドするモデルを構築する為にどんなデータを用意すればよいか?どんな手法で機械学習させたら良いか?を調査するように分担しました。 幸いBigQuery MLのチュートリアルではコンテンツをレコメンドするモデル構築の流れを公開してくれていました。 cloud.google.com 行列分解という手法を用いれば何かしらのレコメンドはできそうだというところまではスムーズに行きつき、ある程度具体的な実現までの道筋も見えました。 なお、その際にインプットした内容はQiitaの下記記事へアウトプット済みです。 qiita.com 方向性が決まった後は動作検証サイクルを回しながら進行 メンバーの方ではLIFULLが保有している各種データをBigQuery MLに学習させる手段も分かってきたところで、お互いの調査結果や知識をマージし、以降のモデル構築の為のSQL作成はメンバーに担当してもらいました。 私は作ってもらったSQLのレビューや学習させるデータの組み合わせのアイデアをメンバーに伝え、そのアイデアを実現する中間テーブル作成やSQLをくみ上げてもらい、レビューおよび動作検証をするというサイクルを回しながら開発は進行していきました。 最終的にキャッチアップはどの程度まで行ったか 私はBigQuery MLで構築した物件レコメンドモデルについてはある程度細部までキャッチアップできている状態でした。 ですが、BigQuery MLで他にどんなことができる?まではキャッチアップしきれておらず、あくまで今回の要件を満たす為に必要だった上流工程で得た知識止まりではあったと思います。 施策の結果はどうだったか 無事にリリース 無事にモデルは構築し終わり、ルールベースで構築された物件レコメンドとのABテストを行うことができました。 ABテストの結果は残念ながら優劣が付くほどの差はつかずでしたが、大幅に負けることがなかっただけでもある程度の収穫はあったと思いました。 リリース後の振り返り リリース後、メンバー含めた振り返り会を実施しました。 次回以降での改善点から今回のテーマと関連していたものを一部抜粋して紹介します。 今回は機械学習、BigQuery MLともに初挑戦だったのでひたすら自分たちでキャッチアップ、少々強引に実装してしまった感はあった。もっと社内の有識者に相談する。 「何がわからないのかもわからない、何を質問すればよいのかもわからない」というフェーズを脱した段階で有識者へ早めに相談する。 まとめ 未経験分野へチャレンジしてくれるメンバーにマネジャーとしてどう接していくべきか?について、私なりにまとめました。 ※まとめ部分に関しては、正直まだ私の中でも試行錯誤を今後も繰り返していく部分は多いと思っています。 ※よって、適宜アップデートされる可能性は高いですが、あくまで現時点での私なりのまとめだと思っていただけますと幸いです。 メンバーと頻度高くコミュニケーションを取り、いつでも気軽に相談できる環境にする メンバーと頻度高くコミュニケーションを取っていくことに合意が取れている前提ですが、メンバーが孤独感を感じないように、1日15分でもよいので施策についてコミュニケーションを取る時間を取った方が良いと思います。 特に試行錯誤している時期は、オフィスへ出社しているなら近い席に座って話しやすいようにする、リモートであれば定期的にSlackのハドルでコミュニケーションを取るなどが有効だったかなと思います。 メンバーや有識者と会話できる程度には未経験分野のキャッチアップを行っておく 今回の具体例だと「行列分解」や「BigQuery ML」の概要を他者へ説明できる程度にはキャッチアップしておいたことは、後工程でレビューや実装の一部を手伝ったりもできたので良かったと思っています。 でしゃばりすぎないように気を付ける あくまで主役はメンバーであり、自身はサポートすることが役目だということは意識しておかないと、メンバーの成長の機会を奪ってしまうこともあるので注意が必要です。 チャレンジしてくれるメンバーへの敬意を持って接することが大切だと思います。 社内有識者へ相談できる経路はあらかじめ確保しておく LIFULLには機械学習のエキスパートや、BigQuery MLを用いてモデルを構築しているスペシャリストの方がすでに在籍しています。 この人たちへ相談できるようにあらかじめ社内調整を進めておくことはマネジャーとしての責務だと思います。 最後に 最後まで読んでいただきありがとうございました。 最後に、LIFULLでは共に成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは。エンジニアの菊地です。 今回は LIFULL HOME'S アプリでおこなっている Kotlin Multiplatform(以下、KMP)の 導入についてご紹介させていただきます。 LIFULL HOME'S アプリでは、2022年10月頃に KMPの導入の検討を開始しました。他の技術を含めて KMP 発表当時から簡単な調査や検討は行われていましたが、ちょうどこの頃に KMP が β版となったことにより本格的にプロダクトへの導入を検討することが可能となりました。 KMP とは 簡単に説明すると、Kotlin で書かれたコードを Android / iOS 両方で実行できるようにする技術になります。 KMP にする対象は? LIFULL HOME'S アプリでは、下記の図にある Business / Domain、Data / Core の領域で KMP の導入をおこなっています。 導入するにあたって KMP の導入にあたり、既存の LIFULL HOME'S アプリ(Android、iOS)とは別リポジトリで開発を行い、ライブラリとしてそれぞれ取り込んでもらう形を取りました。 ちなみに、LIFULL HOME'S アプリ(Android、iOS)で使用する社内ライブラリなどは別リポジトリで OS 別に存在しており、それらもゆくゆくは KMP に集約されるという構想になっています。 Compose Multiplatform の存在 LIFULL HOME'S アプリ(Android、iOS)では、UI 部分について Jetpack Compose(Android)や Swift UI(iOS)の導入をしていますが、全ての UI について移行できているわけではありません。 そのためコスト面を最適化するために Compose Multiplatform などもこのタイミングで検討することはできましたが、KMP と UI については分けて進めることができるため、現段階では Jetpack Compose(Android)や Swift UI(iOS)といったネイティブの部品を使うという選択をおこなっており、UI 部分は順次 Jetpack Compose(Android)、Swift UI(iOS)への移行を進めています。 どんな課題があったか ドメイン知識の壁 LIFULL HOME'S は Android および iOS 向けのアプリのみを提供しているわけではなく、PC 向けやスマートフォン向けの Webサイトとしても提供されています。 取り扱っている物件の情報は LIFULL が提供しているものではなく、物件の情報を提供いただいて掲載しているものとなるため、物件を探しているサービスの利用ユーザーに対して提供する部分の開発だけではなく物件を掲載してくれているクライアント向けのシステムを含めて知識として知っておく必要があります。 LIFULL HOME'S アプリの開発・運用をしているだけではこれらを把握することは正直難しい部分もあるのですが、部署を横断して連携するようなシステムの開発となった際にはどうしてもこのドメイン知識が必要となってきます。LIFULL HOME'S アプリの開発をしたいのに、アプリ外のドメイン知識が少ないことで開発コストが膨れ上がってしまったり、意図せずバグを生んでしまう可能性もあったりしてアプリ開発の難易度が上がってきてしまっていました。 長年の積み重ね(技術的負債) LIFULL HOME'S アプリは 2009年12月の iPhoneアプリの最初のリリースからこれまで長年にわたり運用しているため、蓄積された機能やノウハウと同じだけ負債も溜まっていました。 技術的な負債については、日々の運用業務の中で改善活動をおこなっているおかげもあり、負債が増え続けてどうしようもなくなるというような状態とはならずに運用ができています。 ただし今後も LIFULL HOME'S アプリの運用が同じようにできるとは限らず、どこかのタイミングで開発に割くコストよりも改善に割くコストの方が膨れ上がる可能性があり続けるため、さまざまなリスクが考えられました。 参考までに検討の中で挙がったリスクは下記のようなものです。 新しい機能を提供する際に、様々な負債と付き合い続けてきたがために先に負債を返済しないと開発に取り掛かれなくなり、ユーザーへの価値提供が遅れてしまう チームメンバーの入れ替わりにより、ドメイン知識を持つ人が減るとどうしてこうなっているのか?がわからないままメンテナンスせざるを得なくなる 開発規模が大きくなればなるほどメンテナンスする量も増えてしまうため、機能開発ではなくメンテナンスをし続ける人員が出てきてしまう どのサービスでも運用していく上で避けて通れないことばかりでありますが、サービスを健全に運営し成長させ続けていくためには向き合わなければならないことになるため、何か方法がないか?とずっと KMP 以外の選択肢も含めて検討を続けてきました。 リソースの問題 LIFULL HOME'S アプリでは、アプリチームとして Android および iOS アプリの開発をおこなっていますが、基本的にはそれぞれがメインとなる OS を持って業務をおこなっているため、Android チームと iOS チームの2つのチームでアプリチームが成り立っています。 普段の開発ではアプリのプラットフォームが別なのでそれぞれのチームで個別で進められていきます。アプリとしては同じサービスとなるため共通となる API等の開発ももちろん行いますが、そこに割くリソースの割合はそこまで大きくはありません。 この時、チーム間でこれまでの経験の差などにより、どちらかの OS の開発が遅れてしまうといった際にもう一方のチームのリソースは空きがあるのに知識がないため、手伝うことができずどちらか一方のチームだけ残業が増えたりといったことが起きていました。 テストなど分担できるようなものは問題がないのですが、Android と iOS を横断して開発している人は少ない(一部いる)ため、チームとしてリソースの最適化が難しいという課題を長年抱えておりました。 この状態になってしまうと、どちらもしっかりと仕事をしているのに同じアプリチーム内で極端に業務量に差があるように見えてしまうため、健全な開発はしにくくなってしまいます。 なぜ KMP を選んだのか まず選択肢としては KMP 以外も含めて検討を繰り返してきました。Web ベースにしてしまうことも検討されたことはありますし、近年では有力な候補として Flutter で書き直すという検討もありました。 ではなぜそれらの候補の中から KMP を選んだか?についてですが、いくつかの理由があります。 自社開発のアプリチームが存在している 仮に Flutter に移行した場合を検討した際に下記のようなことが懸念事項としてありました。 ネイティブアプリエンジニアから Flutter へは実質的なスキルチェンジが必要となるため、それぞれのキャリアにも影響がある Flutter にしたとしてもコアとなる部分はネイティブコードを理解する必要が出てくる LIFULL では社内で開発チームを持ってアプリを作っているので、これまでの運用に培った運用のノウハウなどのかけがえのない経験やネイティブアプリのコードが書けるエンジニアもチームには揃っています。 その資産(ノウハウや人)をチームとして活かしていくことを考えると、Flutter ではなく KMP を選び、ネイティブアプリを書きたいエンジニアが書ける環境を残した方が良いと判断しました。 コードの共通化 開発効率を向上させるためには、サービスとしては共通なのに OS 毎で実装してしまっているビジネスロジックやライブラリを共通化する必要がありました。これは開発だけでなくメンテナンスコストも含めて純粋にコストの削減につながります。 これは KMP に限らず解決する手段はありますが、Android エンジニアが Kotlin の知識をそのまま活用して開発することができるというメリットが大きいと考えました。 ドメイン知識のハードルを下げることができる LIFULL HOME'S のアプリを開発・運用していく上で悩まされることの一つとして膨大な量のドメイン知識の問題があります。 この問題について、ドメイン知識が必要となるビジネスロジック部分などの開発に経験豊富なメンバーを当てて開発を行いライブラリとして提供することで、経験が浅い若手がドメイン知識のハードルを気にすることなく UI 部分の開発などが行えるようになると考えました。 UI 部分の自由さ 今回のような KMP の導入の仕方であれば、UI については Jetpack Compose(Android)、Swift UI(iOS)といったネイティブで提供されるものをエンジニアが自由に選択できるというメリットがあり、ネイティブエンジニアの楽しみを奪うことがありません。 新技術への挑戦のしやすさ Flutter などでも新しい技術は早い段階で導入が行われますが、安定性などを考慮するとネイティブに軍配が上がります。 LIFULL HOME'S アプリでは、かざして検索といった AR を駆使した機能などを比較的早い段階で導入してリリースすることがあるため、そういった観点でもビジネスロジックのみを共通化できる KMP の方が向いていると考えました。 lifull.com エンジニアのキャリア LIFULL HOME'S のアプリチームでは、全社で利用する共通的な API を呼び出すために自分達のチームで管理しているマイクロサービスとなる API を運用しています。このマイクロサービスの開発を行うためには共通的な API を理解する必要があり、ドメイン知識が必要となってきます。 これまではアプリエンジニアのキャリアとして、アプリを作りながらある程度慣れてくるとドメイン知識が必要となるマイクロサービスの開発を行うということがあり、調査だけでなく理解するのも一苦労となるため、結果としてアプリを作りたいのにサーバーサイドの開発に時間がかかるようになってしまうということがありました。 KMP の導入にあたりマイクロサービスに集約されていたビジネスロジックを KMP に移行していくため、マイクロサービスの開発の負荷を減らすこともでき、何よりビジネスロジックを作るのはアプリ側となるため、純粋にアプリ側の開発をする機会が多くなりました。 そして何よりも、これまでは難しかった片方の OS のアプリチームの手が空いているのにもう片方の OS のアプリチームのタスクを手伝うことができないといった課題について、KMP 部分でビジネスロジックの開発を請け負うことや、設計周りが共通化されることで相談やレビューもしやすくなるというメリットも生まれてきており、Android エンジニアや iOS エンジニアという枠組みではなく純粋にネイティブアプリエンジニアとしてのキャリアを模索することができるようになりました。 KMP 導入における課題 iOS エンジニアの Kotlin に対する恐怖心 Android と iOS のどちらの経験もあると Swift がかければ Kotlin もそんなに怖がることないと思うのですが(あくまで個人的な感想です)、やはり未知の環境での開発に急に移行するとなると不安の声はありました。 こちらについては、KMP 導入を進める際に私が先行して調査などもしていたため、Android エンジニアと iOS エンジニアそれぞれから KMP 開発に人員を当ててもらい、開発しやすいところからお試しで体験してもらうといった形で慣れていってもらいました。 実際に、Kotlin の経験がなかった iOS エンジニアも特に違和感なく KMP の開発ができるようになっています。 プラットフォームごとに考慮が必要なことが意外とある これは KMP で開発を進めていくと、プラットフォームごとにそれぞれのコードを書く必要が出てきた場合や、KMP として提供するライブラリをネイティブ側で取り込んで使おうとした際に躓くケースがいくつかありました。 ですが、LIFULL よりも先に KMP の導入をおこなっている方々が多くいるため、先人の知恵をお借りしてあまり苦労せずに切り抜けることができました。 移行する対象が膨大 当たり前ですが長年の積み重ねでアプリ内に存在するビジネスロジックは膨大なものになっています。これを開発サイクルをできるだけ止めずに移植していくのを考えるというのが非常に大変でした。 LIFULL HOME'S アプリでは、Android 側のロジックを優先的に移植していき、iOS では機能開発する際に取り込めるものがあれば取り込んで徐々に KMP を導入するという形をとっています。 そのため、ビジネスロジックについては Android の方が先に KMP への移行が完了する見込みですが、KMP を導入した機能の開発は iOS 側で行なわれることが多く相互的に補完することができています。 まとめ 今回は、LIFULL HOME'S アプリにおける Kotlin Multiplatform の導入についてご紹介させていただきました。 LIFULL では長年運用されている LIFULL HOME'S アプリにおいて様々な課題に対する一つの答えとして、KMP を導入するという選択を行いました。 完全な移行はこれからでまだまだ時間がかかりますが、KMP 導入という判断を行ったことでチーム内でこれまで Android と iOS でビジネスロジックは同じであるにも関わらず、それぞれのプラットフォーム向けに開発やテストを行う必要があること、膨れ上がった負債についてのメンテナンスコストは仕方のないことなど、半ば諦めながら開発をおこなってきたところが解決できる兆しが見え、やりたかったことができるようになるのではないか?という感覚をチームのみんなが持つようになりました。同じような課題で悩まれている方々や KMP の導入を検討している方々に対して、選択肢の一つとして KMP を検討しても問題ないと自信を言える状況になっています。 またこのタイミングでこれまでやれていなかったこと、やりたかったことも併せて検討してやっていこうというチーム内の雰囲気も出てきているため、さらに様々な改善が行われていくことになると思います。 KMP 導入により様々な課題の解決が見えてきていますが、UI であったりテストであったり、運用面の改善含めてまだまだやることはたくさんあるため、継続的に検討を行い続けることでチームとしてサービス開発・運用を楽しみながら成果が出せる環境作りを進めていこうと思います。 最後に、LIFULL では LIFULL HOME'S アプリの KMP 導入を一緒にしてくれる仲間を募集しております。ご興味ある方はぜひご応募ください。 hrmos.co ※ 今回、ご紹介した LIFULL HOME'S アプリはこちらになります 賃貸物件検索 ホームズ 不動産・部屋探しHOME'S LIFULL Co., Ltd ナビゲーション 無料 apps.apple.com play.google.com
こんにちは!LIFULLクリエイターの日運営委員のいしやまです。 社内のモノづくりイベント『創民祭』が開催されましたので、その様子を共有させていただきます。 記念すべき第10回の創民祭ですが、今回は3年ぶり、オフラインとしては4年ぶりのイベント開催となりました! 創民祭とは? 創民祭(そうみんさい)とは、業務や「クリエイターの日」、プライベートで創った物など、LIFULL社員が作ったプロダクトをお酒を飲み、ピザ・寿司を食べながらお披露目するイベントです。近年はWebに限らず、VRやイラスト等、多種多様なプロダクトが展示されています。今回はブース出展とLT(ライトニングトーク)の2本だてで開催いたしました! 前回の様子はこちら https://www.lifull.blog/entry/2019/06/13/125358 クリエイターの日とは LIFULLでは、マーケティング能力や技術開発能力を高めてイノベーションを創造するため、通常業務の枠を離れて、新たな技術や手法に取り組む機会を設けています。 希望者は、3ヵ月ごとに最大7営業日を使って、好きなものを開発できます。 展示内容 前回同様、Webに限らずいろんなプロダクトが展示されました。以下に展示内容を紹介します。 ここでは選抜された受賞4作品を紹介していきます。 ハンドジェスチャーとラズパイで光を制御する新しいアプローチ VRゴーグルを装着した目の前に表示されている9つの球体をR, B, Yの形になぞって指パッチンをすると、ライトの色が変わります。 VRでこんなに魔法みたいなおもしろく、便利なことができるということに感動しました! 3Dでつくるセルルックアニメーション blenderという3Dソフトを用いて、セルルック(=手書きのような質感にレンダリングすること)のアニメーションを個人で制作しています。 3Dを用いることで、動きやカメラワークが多いアニメーションでも、手書きに比べて少ない労力で作品を作ったり、安定した作画にできます。 2Dイラストと3DCGのいいとこ取りができます。 かわいいイラストが自在に動いてアニメーションの可能性が広がりますね! Icon CDN プロトタイプ 現在、フロントエンドエンジニアがアイコンを利用する場合には、デザインデータからアイコンを手動でSVGファイルとして書き出し、最適化し、アプリケーションに配置するという作業が繰り返されています。この手作業はアプリケーションごとに行われるため、工数が多くかかります。 そこでIconをCDNで一元管理・配信できるCDNの構築を有志メンバーで進めています。 このプロジェクトによって、全社の生産性の向上間違いなしですね! スキルアップを目指してFlutterでチーム開発 ネット麻雀と違い、対面で麻雀では点数計算がハードルとなります。 実践においてはパターン化されたものを覚えれば、7−8割はカバーできると言われています。 そこで、専用カリキュラムを講義・クイズの形式にしたアプリを、Flutterで開発しています。 こちらは同期エンジニアのみで結成された勉強会チームの取り組みだそうです。 こういう取り組みはたのしそうですね、僕もやってみたいです。 ライトニングトーク ライトニングトーク(LT)は3~5分程度の短い時間で発表するプレゼンテーションで、今回の創民祭でも実施されました! LIFULL Tech Malaysia こちらはLFTMの代表取締役社長の方に、即興でプレゼンしていただきました。 カレーの作り方を例に挙げて、マレーシアの方々との関わり方をユーモアたっぷりに紹介していただきました。 コミュニケーションというものについて考えさせられました。 マレーシアの方々がとても身近に感じられました! LIFULL Tech Vietnam 弊社ではLFTVを通して、ベトナムのエンジニアも開発に携わっています。 当日の急な参加でしたが、快く発表していただきました。 ベトナムのことが急に身近な存在に感じられました! 最後に 創民祭、その一部をちょっとだけお伝えしました! そんなLIFULLでは、一緒に働くメンバーを募集中!新卒も中途も絶賛採用中です。ご応募お待ちしてますので、ぜひみてください! hrmos.co hrmos.co
プロダクトエンジニアリング部の興津です。 私は現在、LIFULLの海外拠点の一つである、LIFULL Tech Malaysia Sdn. Bhd.(以下LFTM)のメンバーとともにLIFULL HOME'Sの賃貸領域でサイト改善業務をしています。 今回は、言語や文化の違う私たちがどのようにコミュニケーションをとりながら働いているのかを紹介します。 LIFULLの海外拠点の紹介 2024年現在、LIFULLにはベトナム(LIFULL Tech Vietnam Co.,Ltd.以下LFTV)とマレーシアにグループ会社があります。 さらなる事業拡大を目指すために、より優秀な開発リソースを確保したいという考えから行き着いたのが、これらのグローバルな開発拠点の設立でした。 それぞれの現地で採用されたエンジニア達が本社メンバーと協働しながら、開発業務を担っています。 本社メンバーが現地に駐在したり、短期で各拠点に滞在して仕事をすることも可能です。 しかし、普段は現地で採用された各国のエンジニアが中心となって業務を行っています。 なお、LIFULL社内では、LFTVとLFTMを総称して「LFTx」と呼んでいるため、本稿でもこの2つを合わせて指す時はこちらの表現を使用させていただきます。 場所に捉われない1つのチームへ 2023年にLIFULLでは、「LFTxに対してオフショアという言葉を使わない」という宣言をしました。 それは「オフショア」という言葉に、以下のようなイメージが想起されやすいことを懸念したからです。 本社からの発注を納品すればよしとされる状態 業務が固定化してエンジニアが一定以上のレベルから成長できない環境 本社と距離感のある主従関係と不活性なコミュニケーション 先述のように、LFTxが設立された理由は、優秀な開発リソースの確保です。場所に捉われず優秀なエンジニアがいたら一緒に働きたいという気持ちで LFTxは作られました。 LFTxがより能力を発揮できる環境作りや、今後のさらなる発展と増員を目指すにあたって、上記のようなイメージは妨げになると考えました。 目指す方向性に即したLFTxとの関係性を、私たちは以下のようにとらえています。 継続的な開発を続けていく一体感のあるチーム さまざまな業務に挑戦する機会の提供と成長の促進 会社の違いによる距離を感じないフラットなコミュニケーション すなわち、LFTxであることや海外拠点であることは特に意識をしない、1つのチームを作っていく存在としていきたいと考えています。 そのための第一歩として、「オフショア」という言葉を使わないことで、意識の醸成を作ることから始めました。 この宣言をする前のLFTxは、LIFULLから発注した開発業務を受託し、設計からテストまでを一貫して作業してできた成果物を納品する形での協働が中心でした。 つまり、冒頭で書いた「オフショア」という言葉で想起しやすい関係性であったと言えます。 しかし、この宣言と同時に、意識や言葉の扱いだけではなく、体制的にも積極的に理想の実現に向けて取り組んでいます。 本稿では、現在、実施しているさまざまな取り組みを紹介できればと思います。 チーム発足時の状況 上述の通り、2023年の10月から、私が所属するチームにLFTMメンバーがアサインされました。 当時の私は、場所に捉われない1つのチームになりたいという会社の思いに強く共感していました。その一方で、言語や文化が違うマレーシアの人たちと働くことは日本国内で暮らす人と働くことよりは難易度が高く、自分では力不足なのでは、と不安も大きかったです。 その当時のチームメンバーとLFTMがそれぞれどのような状況であったのかを簡単に説明します。 LIFULLのチームメンバー LFTMと協働することが決まった時、私はチームメンバーの一人一人に英語の経験やLFTMとコミュニケーションを取りながら仕事をすることの意向について簡単にヒアリングをしました。 その結果、全員が「これを機にLFTMとのコミュニケーションを取りながら英語力も上げていきたいが、英会話経験はほとんどなく、自信がない。マレーシアの文化もよく知らない」という回答でした。 スキル面は若干心許ないものの、一番大切な意欲は十分にある、という状態です。 これはチームのエンジニアリーダーである筆者も含まれています。 LFTMのメンバー LFTMは2023年3月に設立されたばかりで、10月の時点ではあまりLIFULLとも連携をしていませんでした。 特に、我々のチームである賃貸領域との連携経験はまったくなく、LIFULL HOME'Sを開発する環境やドメイン知識もありませんでした。 また、LFTMの求人要項では、特に日本語のスキルは求めていません。当然、メンバーは日本語がわからない状態です。(なお、LFTMでは2023年12月から日本語のレッスンが受けられる制度を発足しました) LIFULL-tech.my つまり、エンジニアとしてのスキルはあるもののLIFULLの知識は少なく、日本語でのコミュニケーションも難しい状態でした。 LFTMと協働するための取り組み そんな私たち本社メンバーとLFTMメンバーが、どのようにコミュニケーションを取り、協働しているのか具体的な方法を紹介します。 コンセプトは、「言語と文化は尊重しつつ、チーム内の役割は会社の垣根をなくした開発チームを組成する」 同じチームのメンバーとして受け入れるからには、「LIFULLだから」「LFTMだから」という考えを極力捨てることにしました。 ドキュメント類は日英併記で記載するようにして、MTGの多くをLFTMと一緒に行い、重要事項は日本語と英語を交えて会話するようにしています。 PJのアサインも、まとまった単位の仕事をLFTMに任せ、LIFULLは納品されるのを待つといった、受託らしいスタイルは廃止しました。 作業のアサインは会社の垣根をなくし、LIFULLに新入社員が参画した時と同じように行っています。たとえば、コーディングはLFTMメンバーでレビューは本社メンバーが行ったり、LFTMメンバーが作成したテスト仕様書をもとに本社メンバーがテストを行うというスタイルも採用しています。 また、従来ではブリッジSEと呼ばれる日本語が堪能な現地社員を仲介してコミュニケーションを取っていましたが、それを撤廃し、担当者と直接やりとりをするスタイルをとっています。 MTGへの参加を積極的に促していく中で、LFTMメンバーからも仕様やサイト改善施策の提案をしてくれる機会も少なくありません。私たちだけでなく、LFTMも自分たちが受託したことをやるだけではない、ともにLIFULL HOME'Sをよくしていくメンバーであるという意識を持っていることが伺えます。 ただし、現時点でも不十分な点はまだ多いです。たとえば、施策のブレストなど、すべてのMTGをLFTMと行っているわけではありません。今後も改善を繰り返しながら、理想に近付いていきたいと考えています。 言語の壁を越えるためのツール利用 最初の高い壁である言語の差を解消すべく、私たちは以下に挙げるような多くのツールを使用しています。 Meet 私たちは状況に応じてさまざまなWeb MTGのツールを使用していますが、LFTMも交えたMTGではMeetを使用するようにしています。 字幕を各々で設定ができることと、画面共有中も特に設定が不要で相手の顔が同時に表示される状態になっているためです。 私たちは字幕に頼りながら、時にはボディランゲージやリアクション機能も交えて会話をしています。 Googleスプレッドシート ドキュメントを作成する際、私たちはGoogleスプレッドシートを積極的に使用しています。 なぜなら、googletranslate関数を使うことで、容易に翻訳ができるためです。 support.google.com 私たちはサイト改善施策の仕様書や、テスト仕様書などさまざまな場面でこの関数を使って翻訳することで、翻訳コストを削減しています。 Chrome拡張機能「DeepL翻訳」 www.deepl.com GitHubのレビューなど、スプレッドシートを介入することが難しい場所ではChrome拡張機能のDeepL翻訳を使用しています。 書いたものをワンクリックで簡単に訳してくれるので重宝しています。自分の書いた日本語を英語に変換する時やLFTMの書いた英語を日本語に変換する時だけでなく、自動翻訳した英語を日本語に再度翻訳することで、意図しない意味に変換されていないかのチェックにも使用しています。 Slackアプリケーション「Kiara」 www.getkiara.com 私たちは普段の非同期コミュニケーションはSlackで行っています。 このSlackのチームチャンネルに、それぞれの投稿に返信する形で自動翻訳を投稿してくれる「Kiara」を導入しています。 日英どちらで書いても自動で判定し、日本語なら英語に、英語なら日本語に変換してくれるため、Slackのやりとりは言語をまったく意識することなくコミュニケーションが取れている状態です。 Slackワークフロービルダーとkeelai keelaiとは、社内で開発・運用されているAIチャットbotです。詳しくはこちらの記事をご参照ください。 www.LIFULL.blog このkeelaiとSlackワークフロービルダーを掛け合わせることで、自動翻訳を行うこともあります。 たとえば、Slackで任意の投稿に特定のリアクションをつけた時、keelaiにその投稿を翻訳するように指示をする、というワークフローを作成します。 Kiaraではチャンネルすべての投稿を自動的に翻訳するのに対し、この方法では任意の投稿に対して翻訳できるため、チャンネルの特性に応じて使い分けをしています。 さらに、keelaiには「この文章を翻訳しやすい言葉に添削してください」という指示を与えることも可能です。複雑なことを伝える時は、一度翻訳しやすい日本語にしてから英語に翻訳をすることもあります。 自動翻訳を活用するために気を付けていること このツールの使い方からわかる通り、私たちは無理に言語を合わせるのではなく、それぞれの言語を自動翻訳することを主としてコミュニケーションを取っています。しかし、何も考慮せず自動翻訳に頼りきってしまうと、思わぬところで認識の相違が生まれてしまいます。 そのため、私たちは以下のような工夫をすることで、より精度の高いコミュニケーションを目指しています。 一つの文で伝えることは一つにする ×「レビューをしたので確認をお願いします。」⚪︎「レビューをしました。確認をお願いします。」 言葉はシンプルにする ×「教えていただけないでしょうか?」⚪︎「教えてください」 ×「チャレンジパターンが優勢です」⚪︎「チャレンジパターンが勝っています」 主語や目的語を明確にする 日本語は主語や目的語を省略しても伝わってしまう言語です。これらを意識的につけることで自動翻訳のミスを防ぐことができます。 ×「今日は欠席します」⚪︎「私は今日のMTGを欠席します」 動詞は漢字で表現する ひらがなの動詞は変換でミスしやすいので、翻訳の選択肢を狭めるためにも漢字にした方がうまくいきやすいです。 ×「ひらがなの動詞は変換でミスしやすいので」⚪︎「ひらがなの動詞は変換でミスが発生することが多いので」 感情は絵文字で伝える 感情を伝える文章は、特に自動翻訳が失敗しやすいです。 また、翻訳された言葉自体が意図した通りだったとしても、褒めているつもりで言ったコメントが、否定的な印象を与えてしまうこともありました。 そのため、私たちは感情は絵文字を多用して伝えています。 日本語に自動翻訳されたもので、複雑な表現があるものはネイティブチェックを入れる 私たちはLFTMメンバーが作成したテスト仕様書を日本語に自動翻訳して、それを使って本社メンバーがテストを実施することがあります。 テスト手順などは複雑なものも多く、自動翻訳では仕様を把握していないテスト実施者にはうまく伝わらない部分も発生してしまいます。 そこで、仕様を把握している本社メンバーが自動翻訳をネイティブチェックをすることで、テスト手順にミスが発生しないようにしています。 文化の違いを越えるための取り組み 言語は機械的に解決する方法がある一方で、文化的な違いはツールなどで解決することは不可能です。 そこで私たちは、以下のような手段で互いの文化を紹介しています。 Slackで日常を紹介 LFTVとLFTMでは、それぞれが自社の日常を紹介する専用チャンネルを作成しています。 内容は社内イベントや、社員の紹介など多岐にわたっています。 それぞれのチャンネルは本社メンバーも投稿が可能です。 日本で行われたベトナムフェスに行ったレポートなども投稿されていたり、時にはお互いの飼い猫の写真を投稿するだけのスレッドができた日もありました。 このSlackチャンネルは所属するチームも関係なく交流できるツールの一つとなっています。 互いのことを紹介する時間を作る 私たちのチームでは互いの文化を知るための一環として、週に一度それぞれが自由に自分たちの趣味や体験などを紹介する時間を持ち回りで作っています。 内容は最近行った旅行の話や、自分の住んでいる街のことなどその日によってさまざまです。担当者の話をコメントやリアクションも使いながら、楽しんで聞いています。 「この写真に写っているものは何?」「マレーシアと違って日本ではこんな感じです」など、担当者以外が話を膨らませることも多いです。 これはLFTMのメンバーが紹介した、旧正月の過ごし方について書かれたスライドの一部です。 最初は「自分の英語が通じるだろうか」ということばかりが気がかりだったのが、回を重ねるごとに「この話題は向こうの文化だとどう映るだろうか?」ということも考えられるようになりました。 具体的には、私はLIFULLのダイバーシティ&インクルージョンを推進する委員会のLGBTQ+チームに所属し、性的指向やジェンダー・アイデンティティに捉われない環境づくりを推進しています。この取り組みを紹介したいと考えた一方で、「ムスリムが多いマレーシアではLGBTQ+に対してどのような考え方を持っているのか?」と立ち止まることができました。 最終的にはLFTMの代表取締役社長である松尾さんにも相談の上、「あくまでLIFULLではこのような取り組みを推進している」という表現で伝えることにしました。まったく違う話題にすることもできましたが、文化の違いを見せないようにするのではなく、見せた上で受け入れ合うことを目指したいと考えたためです。幸いにも、LFTMメンバーは特に拒否反応を示すことなく話を聞いてくれました。 すべての人・事柄が同じようにはならないだろうということは念頭におきつつ、これからも少しずつ互いの文化を受け入れ合えたらと考えています。 LIFULLのサービスに触れる時間を作る お互いのことを紹介している中で発覚したことですが、日本とマレーシアでは住み替えの方法が大きく異なります。 私たちが当たり前に利用しているLIFULLのサービスが、LFTxのメンバーにとっては当たり前ではないのです。 そこで、LIFULLのサービスを一通り触って見てもらう時間を作ることにしました。 また、LIFULLがこのサービスでどのように利益を得ているのかを簡単に説明する時間も設けました。 その結果、LFTMメンバーは自分たちから「ユーザーだけではなく、クライアント(LIFULL HOME'Sに情報を入稿する不動産会社)が触れるサービスも見たい」と提案してくれました。私たちの説明も真剣に聞いてくれました。 自分たちが作っているサービスを理解しながら開発することで、高いモチベーションと品質を維持できていると感じています。 最後に LIFULL・LFTV・LFTMではそれぞれ一緒に働いてくれるメンバーを募集しています。 語学力に自信がないけれど、海外の人と仕事がしたいと考えている人には良い体験を提供できる組織だと思います。当てはまる方はぜひカジュアル面談などのページを見ていただけたら幸いです。 日本人の方でも現地で暮らすことができるのであれば、LFTVやLFTMで働くことも可能です。 hrmos.co hrmos.co LIFULL-tech.vn LIFULL-tech.my
こんにちは、エンジニアの中島です。 この記事は2024年1月のLIFULL社でのアクセシビリティ改善およびやっていき活動の報告です。 この活動報告は月次で出すかもしれないし出さないかもしれないくらいの温度感で運用されています。 目次 目次 サービス改善 トップの探し方設定ポップアップ エリア選択フローのチェックボックスのフォーカス可視化 トップレベルランドマークを設定 レコメンド物件のカルーセル機能 路線から探す・地域から探す検索フロー中にある送信ボタンのバリデーション設定 育成・啓発の取り組み アクセシビリティ1on1 WCAG解説書 輪読会 お知らせ サービス改善 本期間中の改善取り組みのターゲットはLIFULL HOME'S 不動産アーカイブのPCページです。 諸事情で発表できないものもありますが公開可能な取り組みを紹介させていただきます。 トップの探し方設定ポップアップ アーカイブサイトのトップページにはどのエリアの物件情報を探すかを選ぶための都道府県選択のUIがあります。 押すと、探し方を選択するポップアップが表示されるようになっています。 当初、この都道府県ボタンはhref属性のないリンクとして実装されており、ロールの間違いやフォーカス不能、表示されたポップアップ内にフォーカスが移動しないといった複数の問題がありました。 改修後、都道府県リンクはボタンとなり、ポップアップへのフォーカス移動も実装されました。 エリア選択フローのチェックボックスのフォーカス可視化 アーカイブのエリア選択のフローは路線、駅と絞り込んでいくフローと市区町村、町域と絞り込んでいくフローの2つがあります。 いずれもエリアごとにチェックボックスが用意されており、物件情報を調べたいエリアのチェックボックスにチェックを入れて絞り込んでいくものですが、これらのチェックボックスにはカスタムデザインが当たっており、それを実現する際にチェックボックスのフォーカスインジケータが見えなくなっていました。 改修後、カスタムデザインはそのままでフォーカスインジケータが視認できるようになりました。 トップレベルランドマークを設定 前回アーカイブのSPサイト側を対応しましたが、PC側ももともとトップレベルランドマークの設定がなされておらず、いくつかの要素がランドマーク外に漏れていました。 そのため、ランドマーク間の移動を利用するユーザーのコンテンツ見落としにつながってしまう懸念がありました。 また設定がなされてないことで、メイン領域への支援技術を用いたジャンプなどが行えない問題もありました。 本対応ではトップレベルランドマークを設定し、見落としの防止や領域間ジャンプの利便性向上のための改修を行いました。 レコメンド物件のカルーセル機能 アーカイブサイト内ではフロー中の各所にレコメンドで物件をカルーセル表示する機能がありますが、こちらも名前不足やロール設定の間違い、アクセシビリティツリー上で非表示にされるべきコンテンツが漏れているなどの問題がありました。 こちらも見た目はそのままに実装を変更し、適切な指定・挙動をしたものに差し替えました。 路線から探す・地域から探す検索フロー中にある送信ボタンのバリデーション設定 各エリア絞り込みのフローの中にある送信ボタン(次のステップに進むためのボタン)が、エリア未選択時に disabled になっており、タブシーケンス上からボタンがなくなり、文脈を見つけづらいという問題がありました。 ボタンをタブシーケンスに含め、aria-disabledによる無効化状態の説明をしたうえで、押した際にネイティブアラートでフィードバックする実装に改修しました。 育成・啓発の取り組み アクセシビリティ1on1 本期間中はフロントエンドエンジニア6人、デザイナー1人に対して行いました。 内容はWCAGの解説、APG(aria authoring practices)の解説、coga-usableの解説、実際のアプリケーション開発時でのアクセシビリティ配慮に関する相談などが主なものとなります。 WCAG解説書 輪読会 アクセシビリティやっていき勢向けにWCAGの輪読会を隔週に行うことになりました。 本期間中はお正月休みも重なったことから2024/01/22の一回のみの開催でした。 お知らせ LIFULLではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは。フロントエンドエンジニアの根本です。 LIFULL HOME'Sのプロダクト開発と、スポーツ関連の新規事業開発に携わっています。 過去のブログでは新規事業開発におけるUXエンジニアとしての取り組みを紹介しました。 ご興味のある方はぜひご覧ください。 UXエンジニアとは?新規事業での取り組み - LIFULL Creators Blog 新規事業開発におけるUXリサーチの実践 - LIFULL Creators Blog 今回はLIFULL HOME'Sの既存事業におけるUXエンジニアとしての取り組みを紹介したいと思います。 新規事業とは異なる点の一つとして挙げられるステークホルダーの違いに焦点を当て紹介したいと思います。 はじめに 私の携わっている新規事業では、事業オーナー1名・エンジニア2名という体制のため必然と全部やらないといけない = やりたいことは全部できる環境です。 対照的に既存事業は、企画・デザイナー・エンジニア(アプリケーションエンジニア・フロントエンドエンジニア)と役割が明確に分かれたメンバーでプロダクトチームが組成されます(こちらの方がプロダクト開発において一般的かと思います)。 ちなみに現在、LIFULLにはUXエンジニアという職種はありませんが、実装だけではなく体験設計にもコミットしていくという決意表明の意味で勝手に「UXエンジニア」と名乗らせていただいています。そんなプロダクトチームにおけるUXエンジニアとしての立ち振る舞いの事例を紹介します。 実際の取り組み 1. UXリサーチに参加しリサーチャーと一緒にユーザー体験を検討 UXリサーチャーが推進するUXリサーチに参加しユーザー体験上の課題の洗い出しや仮説検証を一緒に行います。その中でこうした方が良いのではないかという解決方法をエンジニアリングの立場から意見します。 フロントエンド側でのインタラクティブな提案もあれば、データ仕様を踏まえた施策の可否まで含まれます。こういった上流の段階から企画・リサーチャーと壁打ちをすることで要件定義段階での早い意思決定につながると考えています。 2. テクニカルプロトタイプを用いた施策検討 要件定義の段階で早急にテクニカルプロトタイプをコーディングし企画・デザイナーと擦り合わせを行います。 仕様検討段階から動くプロダクトを用いて議論することで企画・デザイナー・エンジニア間での共通見解を持ち、ユーザー体験の底上げと開発スピードの向上につなげられればと考えています。 直近では下記のプロトタイプをコーディングし、チームメンバーとプロダクトを触りながら何度もブラッシュアップした後に本格的な開発フェーズへ移行しています。 チャットbotフォームの表示速度や発話UIの確認/調整 新規画像ビューアの操作性の確認/調整 3. ノーコード・ローコードツールを用いたプロダクト開発 開発工数がかかりすぎるもの、早く世の中に出して感触を得たいもの、短命なプロダクトなどプロダクトの要件のバランスを考慮しノーコード・ローコードツールで開発を行います。 その際、下記のようなエンジニア主体の進め方を採用しリリースまで最速で進められるように動いています。 実際に開発したプロトタイプに対してデザイナーに最終的なデザイン作業を進めてもらう ツールの仕様上できること・できないことを明示し企画と最終的な仕様すり合わせを行う 職種横断チームにおけるUXエンジニアの課題 UXエンジニアは、企画・デザイナー・エンジニア(特にフロントエンドエンジニア)と役割がグラデーションで交差する職種になるため、開発プロセスのどの段階を担うかチームメンバーと適切なコミュニケーションをとっていくことが重要だと感じています。 最後に LIFULLのエンジニアは、LIFULLで理想とするエンジニアの在り方として「エンジニアとして経営をリードする」と掲げています。 プロダクト提供のスピードをあげ、ユーザー体験を高めていくことで、結果として売り上げ貢献していくため、職種の垣根を越えプロダクトチームとしてどのようなプロセスを踏んでいくことが理想的かを考え続け、その中でUXエンジニアという役割がより良いプロセスを作り出せる一役になれるように取り組んでいければと考えています。 LIFULLではともに成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
プロダクトエンジニアリング部の興津です。私は前職が客先常駐(SES)専門の会社でエンジニアをしていて、2022年10月にLIFULLへ中途入社しました。 客先常駐でお客様のシステムを開発する前職から、自社のサービスを開発するLIFULLへ転職することは、不安な点が多かったことを覚えています。 今回は、転職して1年が経過した今振り返ってみて、これまでの経験が活かせた点と、活かせなかった点の乗り越え方を紹介します。 同じように客先常駐エンジニアから自社サービスエンジニアへのキャリアチェンジを検討している方の参考になれば幸いです。 (ただし、あくまでも私の・LIFULLに転職した結果であることはご留意ください) 私がLIFULLに転職を決めた理由 もともとは前職と同じ客先常駐ができるような会社に転職をしたいと考えておりました。 そんな中登録した転職サービスでLIFULLからスカウトメッセージをもらいました。 希望業種とは違ったけれど、自分の職務経歴書をしっかり読み込んでくれたことがわかる丁寧なメッセージだったため、カジュアル面談を受けることにしました。それがLIFULLへ入社するきっかけとなりました。 そして、社会課題を解決するという会社の理念に共感し、そのまま縁あって入社を決めました。 その結果、想定外のキャリアチェンジとなってしまい、入社にあたっては多くの不安がありました。 客先常駐エンジニアから自社サービスエンジニアへとキャリアを変える時に感じた不安 求められているスキルが自分にあるか 技術的なスキルも、これまでは常駐先の技術をいち早く把握することが求められました。 しかし自社のサービスとなると、早さよりも深さが求められ、時には新しい技術を提案できなければいけないのではと考えていました。 この向き合い方のギャップがどんな影響をもたらすかが未知数でとても不安でした。 同じ会社の人と長期的なコミュニケーションを取ること 前職は客先常駐の中でも、一人で常駐することをメインとする会社であったため、自社の人とはコミュニケーションの機会がほとんどありませんでした。 また、当然のことながら常駐先が変われば一緒に仕事をする人が変わります。 そのため、技術と同じように、コミュニケーションスキルも、初対面の人しかいない中でより早く馴染むよりも、同じ人たちと長期的に関係性を構築していく方向に変化させる必要があるのではと考えていました。 具体的には、私にコミュニケーション面での欠点があったとしても、これまで一緒に仕事をしてきた人は「このPJが終わったらもう関わらない相手だから」と目を瞑ってくれていたかもしれません。しかし、これからもずっと仕事をする相手だとストレスを与え続けることになってしまうことが不安でした。 自分らしい働き方ができるのかという懸念 客先常駐であれば、働き方のほとんどが「常駐先に準じる」というものです。 一見すると客先の言う通りにしなければならず、自由がないように見えます。しかし、逆に言えば自分の働き方と合った客先に常駐すれば理想通りの働き方が簡単にかないます。 途中でライフスタイルが変わって理想の働き方が変わった場合は、客先をそれに合わせて変えればよかったのです(もちろん、それをかなえてくれるような会社に所属していることが前提となります)。 同じ会社でずっと働くということはそんなに簡単に働き方を変えるわけにはいかないのではないか、と言う懸念がありました。 特に私は小さい子どもがいるため、この不安が最も大きかったです。 入社してから感じたFIT&GAP FITしていると感じた部分 開発業務 当然のことながら、コーディングやテスト・レビューなどは今までの経験がほとんどそのまま活かされました。 特に、ドキュメント化の整備は客先常駐エンジニアとしての経験が重宝されました。客先常駐はメンバーの入れ替わりが激しく、一度離れたメンバーとは連絡を取ることが難しい環境に身を置くことが多いため、「自分がこの現場からいなくなった時のこと」を考えさせられる機会が多かったです。その対策として自分の作業のドキュメント化を普段から当たり前のようにしてきました。LIFULLのような自社サービス会社では、客先常駐ほど入れ替わりが多くなく、異動で離任していたとしても同じ社内にいるのであれば容易に連絡をとることができます。その結果、ドキュメントの整備が進んでいない箇所も多く、有識者に都度質問をするコストが肥大化しています。そんな中で自分がこれまで自然とやってきたドキュメント化作業はチームに貢献できる一手となりました。 ちなみにLIFULLではこの「有識者に都度質問をするコスト」が解消できる方法の一つとして、社内向けのAIチャットBotが存在します。詳細はこちらの記事をご参照ください。 www.LIFULL.blog www.LIFULL.blog また、技術的なスキルのギャップはおおむね当初の予想通りではあるものの、LIFULLでは社内の有識者に気軽に相談できるしくみが充実しています。そのため、メンバーの一人として通常業務をする分には大きな問題とはなっていません。 このしくみの一例については、こちらの記事が詳しいです。 www.LIFULL.blog マネジメント手法 自社サービスといえど、チームメンバーとともにリリース日に合わせて開発業務をすることは変わりがないため、マネジメント手法についてもこれまでの経験や勉強してきたことが役に立っています。 ただし、リリース(納品)した後の状況が受託のSIとは異なるため、スケジュールのバッファの取り方や、進捗に遅れが見られた時の対応方法については差異があり、自分の考え方を微調整する必要がありました。 しかし、PMBOKで言うリスクマネジメントや品質マネジメントなどは大きく変わらないと感じています。 自分らしい働き方 私が転職する際に感じていた最も大きな不安でした。結論から言えば、自分の働き方と合った客先に常駐した時ほど理想的な働き方ではないにしろ、LIFULLの制度を利用することで大きな問題なく就労できています。 まず、LIFULLは週2日のリモートワークが可能ですが、申請が承認された場合は、リモートワークの日数を増やすことが可能です。これを利用して私は現在週4日リモートワークをしています。 次に、LIFULLはフレックス制度を導入しているため、1日の始業時間と終業時間を自分の状況に合わせてコントロールできます。担当業務をスケジュール通りに完遂できる状態であれば、今日は6時間だけ働いて、明日は10時間働くということもできます。 私の場合は、子どもの保育園の送り迎えや育児の時間を確保しようとすると、仕事ができる時間は9時半〜18時・または21時以降という非常に限定的な稼働時間となってしまいます。 しかし、LIFULLでは休憩時間の取り方も個人の裁量に任されているため、18時から休憩を取得して保育園に子どもを迎えに行き、子どもが就寝したら仕事を再開することも可能です。 (ただし、22時以降は上長の許可が必要なほか、深夜時間帯はシステムの都合上できる業務が限定されます) 何より、LIFULLはダイバーシティ&インクルージョンに積極的に取り組んでいることもあり、多様な働き方を受け入れる土壌が会社全体に広がっています。 今後ライフスタイルが変わって働き方を変えたくなったとしても、LIFULLとLIFULLのメンバーならきっと受け入れてくれると確信しています。 LIFULLのダイバーシティ&インクルージョンについては、下記を参照してください。 LIFULL.com GAPとして感じた部分と、乗り越え方 度重なる保守運用業務 LIFULLでは自社のサービスを改善するだけではなく、すでにリリースされたサービスの障害対応や、利用しているライブラリのバージョンアップや脆弱性対応など、保守運用の作業も存在します。 私は前職では常駐先も受託のSI企業が多かったため、納品した後の対応は、基本的に納品先の担当者に一任していました。そのため、自分の開発作業の手をとめて保守運用作業をすることの切り替えコストや、その結果進捗が遅れてしまうことに最初は慣れず、戸惑いました。 現在は、あらかじめ突発的な保守運用業務の発生を見越したPJ進行をするように心がけることで対応しています。具体的には、開発業務の見積もりを出すときにこの作業のリソースも加味したスケジュールにしたり、保守運用の状況もエンジニア以外のチームメンバーに伝えて進捗が遅れることの理解を得るようにしています。LIFULLのエンジニア以外のメンバーは、開発作業以外のエンジニア業務を好意的に応援してくれる人ばかりであるため、衝突が発生することはありません。 また、LIFULLのプロダクトエンジニアリング部では小さな保守運用業務も積極的に取り組むことが数字として評価されやすい環境も形成されています。たとえば、PRマージ数を評価基準として重視することです。 この環境があることで、急な保守運用業務の発生も落ち着いて前向きに取り組むことができています。 自分の仕事が必ずしも会社にとってよい結果につながる訳ではない環境 サイト改善のための開発をしても、リリースした結果、売上などのビジネス的成果の数字が落ちてしまうこともあります。LIFULL HOME'Sの場合、リリース後の一定期間はABテストという、ユーザーを2分して改修前と改修後のサイトを見せて、どちらの層の反応がよいかを計測することを行います。最初から改修後の方がよいことはまれで、何度かのマイナーチェンジを経た後で全ユーザーに向けてリリースします。完全に改修前に戻してしまうことも少なくありません。 客先常駐専門の会社は、在籍エンジニアが客先に常駐する時間を提供することで対価を得ています。つまり、真面目に常駐先で仕事をしていれば、そのまま会社の売上になりました。しかし、LIFULLのような自社サービスの会社では、頑張って改修した結果が会社の売上につながるとは限らないため、うまくいかなかった時の気持ちの保ち方に難しさを感じました。 この気持ちに折り合いをつけるには、自分一人の気の持ちようだけでは難しいです。私は「失敗した施策も次の成功する施策のための学習材料である」と考える土壌や、売上はどうあれリリースを通して市場学習をしたということを賞賛するチームを作っていくことが大切だと考えています。 幸い、LIFULLではその空気があらかじめ作られていて、KPIに市場学習回数を置くなど、しくみの面からも空気を醸成するような取り組みが積極的に行われています。そのため、入社して一年経過した今は、施策が直接ビジネス成果につながらなくても前向きに受け入れられるようになりました。 長期の付き合いを見越したメンバーとのコミュニケーション チームメンバーとのコミュニケーションの取り方のギャップは、おおむね入社前から予想していた通りでした。 しかし、そのギャップに対する不安のほとんどが杞憂に終わりました。なぜなら、LIFULLでは入社者のオンボーディングやチーム間のビルディングをとても大切にしている会社だからです。 中途入社であってもメンターがつき、入社者が希望する限りは毎日メンターと上長それぞれの1on1の時間が設けられます。また、半期に1回はチームビルディングといって、所属するチームとの関係性を醸成するための時間が業務時間中に作られます。やることはチームに委ねられていて、ゲームをしたりBBQをしたり、さまざまなことを行います。これらのオンボーディングやチームビルディングはPJの進行には関係なく行われます。「PJが忙しいからチームビルディングは後回しにしよう」という価値観ではないのです。 このように、あらかじめチームの関係性を作っているため、お互いに改善点がある時も、思いやりを持って伝えることができて、受け入れる側も過度に傷つく状態にはならないように感じています。いわゆる建設的な話し合いができているという状態です。 さらに当然のことながら、LIFULLでは改善点の指摘の方法を教えてくれる機会や、業務上の困りごとを第三者に相談する機関も存在するため、改善点の指摘が相手の否定にはならないことも記載しておきます。 最後に 私のように自社サービスのシステム開発経験がない人でも、LIFULLのビジョンに共感できるのであれば、きっと活躍できるはずです。 LIFULLではともに働いてくれるエンジニアを募集しています。興味がある方はぜひ下記のページを見ていただけるとうれしいです。 hrmos.co hrmos.co
こんにちは! LIFULLエンジニアの吉永、三宅、森です。 2023/11/15~16 東京ビックサイトにて開催されたGoogle Cloud Next Tokyo ’23に参加してきました。3人がそれぞれ現地で聴講したセッションの感想をメインに参加レポートを共有します。 cloudonair.withgoogle.com アジェンダ セッションについて 所感 まとめ セッションについて 聴講したセッションの中から印象的だったものを抜粋してご紹介します。 day1 現場発!工場の生産材料を AI 予測で最適需要予測 & コスト削減 三菱重工業株式会社の工場で生産に利用している材料の需要予測を行いコスト削減につながった事例の紹介。 現場発から実施される改善系PJはLIFULL行動理念の部分で通じる部分があり非常に共感できた。 ローカルで構築済みのDBをBigQueryにインポート、Vertex AIを活用した機械学習による需要予測モデルを構築。 いくつかの失敗はあったが、精度は大幅に向上。月40時間の工数も10分程度に圧縮、廃棄コストも大幅にカットできた。 まさにこれこそがデジタルトランスフォーメーションと言えるような改善を現場発で行い、Google Cloudがしっかり伴走してくれたという良い事例紹介だった。 day1 Jagu'e'r 小売分科会と考える 生成 AI 活用最前線! イオンリテール株式会社、株式会社すかいらーくホールディングス、株式会社セブン‐イレブン・ジャパン、フューチャーアーキテクト株式会社の共同発表で、小売における生成AIの代表的な利用シーンを企業毎に事例を紹介。 【1つだけ抜粋】株式会社セブン‐イレブン・ジャパンは従業員向けの社内情報検索事例について紹介していた(まだ検証までの段階とのこと) セブンイレブンは教育マニュアルのファイル数が42個、3000ページあるとのこと。 検索機能はあったが、探したい情報に辿りつけないという課題があった。 Vertex AI Search と Google Cloud Storageのみで実現可能とのことで、PoCで使う環境構築の動画が流され、説明しながらでも1分程度で環境構築が出来ていた。 企画から検証にかかった時間は1日、コーディング不要、金額も無料の範囲で可能だったとのこと。 必要な情報に辿り着く工数を減らせるのは非常に良い取り組みだと思った。また基調講演でも紹介があったがプロジェクトの内容確認以外に、分析に使う情報などもすぐに出てくるとさらに工数削減が見込めて良さそう。 day2 生成 AI による次世代のユーザー行動解析 株式会社プレイドの事例紹介セッション。ユーザーの行動ログの分析を生成AIに行わせる為に取り組んだ事例の紹介。 LLMでユーザーの行動ログを非構造化データから、構造化データに変更。データ分析やレコメンドに活かしている。 標準のLLMだと一般的な知識に基づく分析になる。生成AIで良い体験を届けるにはエンタープライズデータを学習させ、標準の知識にドメイン知識を加えたLLMを使っていく必要がある。 N1分析のような、従来は人間が時間をかけて行っていたものを、短時間でより精度高く分析ができるようになる可能性を感じた良い事例紹介だった。 day2 求人ボックスにおける Vertex AI Vector Search を利用したレコメンド 価格.comや食べログ、求人ボックスなどを運営する株式会社カカクコムで、求人ボックスでレコメンド機能を実装した時の事例紹介。 Vertex AI Vector Searchを利用し、サイト内での行動データを元に利用者ごとにおすすめの求人を表示する機能を実現している。 エンジニア向けにVertex AI Vector Searchを用いて開発する際の手順を実際のデータを例にTipsを交えて説明されており、レコメンドを作る時のイメージを掴むことができた。 LIFULLとしてユーザーの行動データを元にサイト利用者ごとにおすすめの物件などを表示するレコメンド機能を実現するためのヒントを得られた。 所感 吉永 生成AIがらみのセッションが目立ち、この1年間で生成AI分野が目覚ましい発展を遂げてきたんだということを改めて実感。 Duet AIのようなアシスタント機能を活用することで、スライドやドキュメントの作成効率を大幅に向上できそうな予感を感じた。 Google Meetでもそう遠くない未来に双方向のリアルタイム翻訳字幕表示や、途中参加者向けのそれまでの議事録の要約を作成してくれる機能の提供を予定している。業務効率を向上することが期待できる。 Vertex AIには色々なソリューションがある。少しづつでもいいから各ソリューションに触れておくことが大切。 LIFULLでも株式会社プレイドのようにログの分析に生成AIを活用することで効率化&精度向上に取り組んでいきたい。エンタープライズデータを基盤モデルに学習させるアダプターチューニングにもチャレンジしていきたい。 三宅 Duet AIによって仕事の進め方が変わる事が想像できた ミーティングの後追い機能や同時翻訳、議事録の自動化、資料作成サポート機能など 生成AIに任せられるところを常に探し続ける事が大事だと改めて感じた 契約次第ではあるが現状でも簡単にAIを使った機能開発が可能 森 AIの利用が個人でも会社でも浸透してきており、活用する方法やその手段を考える機会が増えている。 セッションを通してVertex AIやDuet AIなど様々なステークホルダーでAIを活用する手段が整ってきたことを実感し、これまで以上にAIを活用したサービスや利用を推進していくべきと感じた。 ブレイクアウト セッションでは企業ごとに特色のある活用事例を知ることができ、LIFULLのサービスとしてどのようにAIを活用し発展させていくことができるのか、想像を膨らませる良い機会になった。 まとめ Google Cloud Next Tokyo '23に参加して印象に残ったキーワードは「Vertex AI」、「Duet AI」、「生成AI」でした。 どのセッションの中でも多かれ少なかれ3つのキーワードには触れられていたことからも、今後Google Cloudが更に注力していく分野だと思います。 私達のチームでは今後、生成AIをより活用し作業効率を上げていけるようにアップデート情報は定期的にチェックしていこうと思います。 生成AIにまつわる最新情報に触れることができ、来年も参加したいなと思える良いイベントでした!※来年は横浜で2024/8/1~2に開催予定のようです。 最後に、LIFULLでは共に成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
グループデータ本部データサイエンスグループの嶋村です。 今回、データサイエンスグループが主催でデータサイエンス系の自社イベント『 LIFULL AI Hub 100ミニッツ #1 「LLM(大規模言語モデル)の研究開発」 』を開催しました。どのようなイベントになったのか、またイベントの今後についても、ご紹介したいと思います。 データサイエンスグループはLIFULLにおける研究開発組織で、以前は AI戦略室 に属していましたが、2023年10月に改組がありグループデータ本部に属する形となりました。まずは簡単に組織の紹介をさせて下さい。 グループデータ本部データサイエンスグループの紹介 グループデータ本部は、LIFULLグループで生まれる新たなデータを安全かつ効果的に活用できるようにし、事業の変化と持続的な成長を促進することを目指している組織です。グループデータ本部の中に、データガバナンスやデータ基盤を管轄する部署が存在しますが、データサイエンスグループは研究開発組織として、「活用価値のあるデータを創出」し、「データを活用した新たな機能やサービス」の研究開発に取り組んでいます。 データサイエンスグループはビジョンとして『データ科学と研究開発の成果によって ワクワクと喜びを生み出す』を掲げ、事業とうまく連携できる研究開発組織を目指しています。研究開発を通じて新たなAI技術シーズを創出することはもちろんのこと、たとえ枯れた技術であっても自社にとって新たなAI技術シーズであれば積極的に活用し、創出と活用のバランスを重視しています。そして、社会課題や事業課題の解決をし、社会や事業に対して研究開発で貢献をしていきたいと日々革進を続けています。 2023年10月から始まった新たな期では、特に新たなAI技術シーズや知見を蓄積し、社内外へ発信を増やしていこうと組織運営しています。その一環として、社外とのコミュニケーションを取る方法として、後述の『LIFULL AI Hub 100ミニッツ』の開催をすることにしました。 LIFULL AI Hub 100ミニッツの紹介 LIFULL AI Hub 100ミニッツはデータサイエンスグループがトークと交流会の100分でAIを語るイベントです。研究開発を通じて得た知見や成果事例を社外に共有し、参加者(聴講者)のみなさまとインタラクティブに議論ができればと考えています。 自社イベントとしてはエンジニア向けで Ltech というイベントがあり、自社のプロダクト開発等の取り組みを紹介しています。今回、よりデータサイエンス系に特化したこと、また、自社発表だけでなく社外の専門家を招き勉強会形式で開催してみようと思い、別の形での実施となりました。 LIFULL AI Hub 100ミニッツの第1回目のテーマは『 LLM(大規模言語モデル)の研究開発 』です。ChatGPTの登場をきっかけに昨年から急激に普及し始めた大規模言語モデルですが、弊社でも社内の生産性向上に向けて活用に取り組んでいます。今回、その大規模言語モデルの最新の研究動向はどのようになっているのか、また、どのように研究開発として活用していくのか、というテーマで開催しました。当日は、X(旧Twitter)でも 実況 しておりましたので、当日の様子が少しでも伝わればと思います。 LLM(大規模言語モデル)の研究開発 イベントは2部構成で、第1部に講演を設け、第2部にパネルディスカッション形式のクロストークを設けました。第1部の登壇者は、データサイエンスグループの データサイエンスパートナー であり、ピープルアナリティクスの専門家でもある株式会社シンギュレイトの鹿内学さんです。鹿内さんはデータサイエンスグループで推進する大規模言語モデルを活用したAIエージェント研究にも携わっております。 今回、その鹿内さんに大規模言語モデルを活用したAIエージェント研究の最新動向について、「 LLM(大規模言語モデル)の研究開発 」というタイトルで講演をしていただきました。大規模言語モデルをどのようにマネジメントするのかという視点で、どのようなプロンプトエンジニアリングがあり、どのような研究課題があるのかが語られており、大変興味深い内容でした。 LLMのマネジメント 第2部ではデータサイエンスグループの主席研究員であり人工知能学会の理事でもある清田陽司さんを交えて、鹿内さんと清田さんの対談をしました。その中で、「意識の実態は、どこにあるのか」や、「バーチャル世界の中で生まれる社会的知性とはどのようなものか」など、哲学的な話を交え、どのように大規模言語モデルが台頭する時代と向き合っていくのかについて語られました。 第2部クロストーク 興味を少しでも持っていただけた読者の皆様には是非次回のイベントにお越しいただければ嬉しいです。懇親会の参加者からは「最新の動向を知ることができて良かった」や「難しい内容だったが普段考えることのない新たな視点で得られるものが多かった」という嬉しい声をいただきました。イベントのねらいであった参加者のみなさまとの交流や議論も、懇親会を通じてできたため、初回のイベントとしては順調な走り出しになったと思います。 LIFULL AI Hub 100ミニッツ 次回のお知らせ 今後も定期的に「LIFULL AI Hub 100ミニッツ」を開催していきたいと思います。次回および次々回は「Machine Learning 15minutes!」さまに協賛する形式で、弊社LIFULLの本社オフィスでの現地開催とオンライン開催のハイブリッド形式で実施する予定です。  第85回 Machine Learning 15minutes! Broadcast (協賛: LIFULL AI Hub)   日時: 2024/01/27(土)14:00 〜 17:00   会場: 株式会社LIFULL 東京都千代田区麹町1丁目4−4  第86回 Machine Learning 15minutes! Broadcast (協賛: LIFULL AI Hub)   日時: 2024/02/24(土)14:00 〜 17:00   会場: 株式会社LIFULL 東京都千代田区麹町1丁目4−4 是非みなさまにお目にかかれればと思いますので、ご都合良い方は是非お越しいただけると嬉しいです! おわりに 今回はデータサイエンス系の自社イベント「LIFULL AI Hub 100ミニッツ」の取り組みについて紹介しました。今後も継続的に開催していきますので、気軽にご参加いただけると嬉しいです。 最後になりますが、LIFULLでは共に成長できるような仲間を募っております。現在、データサイエンスグループではシニアデータサイエンティストを2枠募集しています。ひとつは 創出的な研究開発に取り組み高難易度な技術課題の解決にコミットするポジション で、もうひとつは 研究開発成果や新規技術を活用して事業課題の解決にコミットするポジション です。 カジュアル面談もありますのでご興味ある方は是非ご応募ください! hrmos.co
こんにちは、エンジニアの中島です。 この記事は2023年10月から翌年1月までのLIFULL社でのアクセシビリティ改善およびやっていき活動の報告です。 この活動報告は月次で出すかもしれないし出さないかもしれないくらいの温度感で運用されていく予定です。 目次 目次 サービス改善 トップページのエリア選択UIのフォーカス管理 地域・路線から探す検索フロー中にあるエリア選択リンクにアクセシブルな名前を設定 路線から探す・地域から探す検索フロー中にある送信ボタンのバリデーション設定 フッタ近くにある物件の種類から選び直すことができる機能の「もっと見る」ボタンのフォーカス関連の不具合を修正 物件一覧ページの見出し順番の不整合を修正 物件一覧の「もっと見る」ボタンを押した後にフォーカスが失われる不具合の修正 おすすめ物件一覧の「もっと見る」ボタンを押した後にフォーカスが失われる不具合の修正 ページトップに戻るボタンがフォーカスを伴わない不具合の修正 物件画像の編集画面のバリデーション情報の読み上げ対応 物件(部屋)の詳細ページの青塗りボタンのフォーカスコントラスト確保 物件(戸)の詳細ページのフォーカスを受け取らないコントロールを修正 物件詳細ページの周辺地図モーダル及び、その中の各コントロールの改善 トップレベルランドマークを設定 育成・啓発の取り組み アクセシビリティ1on1 アクセシビリティUT動画を見る会の実施 2023/11/08 全盲ユーザーのUT動画を見る会 2023/12/11 弱視ユーザーのUT動画を見る会 社内表彰 外部発表 Orangeの会 発表スライド 【実践者に学ぶ】アクセシビリティチームの立ち上げと成長する組織づくり お知らせ サービス改善 本期間中の改善取り組みのターゲットはLIFULL HOME'S 不動産アーカイブのスマートフォンページです。 諸事情で発表できないものもありますが公開可能な取り組みを紹介させていただきます。 トップページのエリア選択UIのフォーカス管理 アーカイブサイトのトップページにあるエリアを絞り込むためのUIは大域エリアを選択すると、小域エリアのリストが表示され、それを選択すると探し方を選べるようになる多段の状態をもつUIです。 しかしながら、各エリアや、UI内の戻るボタンを押した際にフォーカスが失われてしまう問題がありました。 この問題を解消するべくフォーカス管理を行うように改修いたしました。 また各ボタンが適切な role で表現されていなかった問題も合わせて修正しました。 地域・路線から探す検索フロー中にあるエリア選択リンクにアクセシブルな名前を設定 「地域から探す」、および「路線から探す」の検索フロー中にある市区町村・町域、あるいは路線・駅を選択してページ遷移をするためのリンクにアクセシブルネームが存在せず、名前なしリンクとなっていました。 スクリーンリーダーによってはURL文字列の読み上げがなされるなどといった問題がありましたが、これに名前を設定し対象エリアが読み上げられるように改修しました。 路線から探す・地域から探す検索フロー中にある送信ボタンのバリデーション設定 各エリア絞り込みのフローの中にある送信ボタン(次のステップに進むためのボタン)が、エリア未選択時に視覚的に消えているという仕様になっておりました。 そもそも消すべきではないというのはさておき、視覚的には消えているものの、アクセシビリティツリーには表示され、エリア未選択時のフォームバリデーションもなされていない状態でした。 そのため、キーボード操作 + 支援技術による読み上げを利用しているユーザーからすれば遷移ボタンがあるので押したら未選択がゆえに次のページでエラーになるという挙動が見られました。 ボタンの可視化とバリデーション設定が本筋ですが、応急処置として未選択時にボタンが非活性状態であることをaria-disabledで伝えつつ、押した際にネイティブアラートでフィードバックする実装に改修しました。 フッタ近くにある物件の種類から選び直すことができる機能の「もっと見る」ボタンのフォーカス関連の不具合を修正 各ページのフッタ近くにある、サイト回遊用のUI内にある物件種別の「もっと見る」ボタンを押した後にフォーカスが失われてしまうという問題があったため、展開されたコンテンツにフォーカスを移動する改修を行いました。 物件一覧ページの見出し順番の不整合を修正 物件一覧ページでh1見出しの次のレベルとして各物件の見出しがありましたが、h3で実装されていることがわかりました。 物件の一覧を示すための包括的な見出しをh2として設定しました。 デザイン変更を伴うため、一旦はVisuallyHidden(アクセシビリティツリーには存在しているが視覚的には見えない)見出しとして設定しました。 物件一覧の「もっと見る」ボタンを押した後にフォーカスが失われる不具合の修正 物件一覧では物件が列挙されますが、末尾に「もっと見る」ボタンが設置されており、それをクリックすると、次の10件が読み込まれるようになっています。 しかしながらボタンを押した後、フォーカスが表示されたコンテンツに移動せずに失われてしまう不具合がありましたので、表示されたコンテンツの最初のフォーカス可能要素に移動するように改修しました。 おすすめ物件一覧の「もっと見る」ボタンを押した後にフォーカスが失われる不具合の修正 各ページに配置されているレコメンド機能であるおすすめ物件の「もっと見る」ボタンのフォーカスも同様に押した後にフォーカスが失われてしまう不具合がありましたので、表示されたコンテンツの最初のフォーカス可能要素に移動するように改修しました。 ページトップに戻るボタンがフォーカスを伴わない不具合の修正 各ページの末尾には、ページの上部にスクロールアップするためのボタンが設置されています。 しかしながら、スクロールはするもののフォーカスがボタンに残りっぱなしになっており次のフォーカス移動の際にスクロール位置が巻き戻ってしまう問題がありました。 フォーカスを上部の要素に設定し、そういった現象が起こらないように改修しました。 物件画像の編集画面のバリデーション情報の読み上げ対応 アーカイブサイトには物件情報に誤りがあった際、外部から一定編集できる機能があります。 そこでのフォームのバリデーションメッセージが支援技術に正しく伝わらないという問題がありました。 送信ボタン押下時に全体エラーを読み上げ、各エラーを出しているフォームコントロールにエラーメッセージを関連付ける対応を行いました。 物件(部屋)の詳細ページの青塗りボタンのフォーカスコントラスト確保 サービス内の重要度の高いボタンには青塗りのものがよく使われています。 OSによってはフォーカスインジケータが青色でフォーカスコントラストが確保できず、フォーカスを見失うことにつながるため、 outline-offset の設定を見直し、フォーカスコントラストを確保するよう修正を行いました。 物件(戸)の詳細ページのフォーカスを受け取らないコントロールを修正 同ページ内で本来フォーカスを受け取るはずのコントロールが受け取らない実装になっているものが散見されたため、フォーカス可能に修正しました。 物件詳細ページの周辺地図モーダル及び、その中の各コントロールの改善 物件詳細ページでは物件の周辺の地図情報を表示するモーダルが用意されています。 モーダルの起動ボタンを押してモーダルを立ち上げた後もフォーカスがモーダルに移動しなかったり、中にあるスイッチやトグルボタンなどのコントロールがキーボード操作できない・正しいrole設定がなされてないなどの問題がありましたので適切に動作するよう改修いたしました。 トップレベルランドマークを設定 元々トップレベルランドマークの設定がなされておらず、いくつかの要素がランドマーク外に漏れており、ランドマーク間の移動を利用するユーザーのコンテンツ見落としにつながってしまう懸念がありました。 また設定がなされてないことで、メイン領域への支援技術を用いたジャンプなどが行えない問題がありました。 本対応ではトップレベルランドマークを設定し、見落としの防止や領域間ジャンプの利便性向上のための改修を行いました。 育成・啓発の取り組み アクセシビリティ1on1 本期間中はフロントエンドエンジニア6人、デザイナー1人に対して行いました。 内容はWCAGの解説、APG(aria authoring practices)の解説、coga-usableの解説、実際のアプリケーション開発時でのアクセシビリティ配慮に関する相談などが主なものとなります。 アクセシビリティUT動画を見る会の実施 弊社では不定期で、さまざまな特性をお持ちの方がどのように自社のサービスを使われるのかを知るためのUT動画を見る会を実施しています。 それを見ることでどのようなことが求められるのか、どのようにサービスを考えていけばよいのかを考えるきっかけづくりに役立ててもらっています。 2023/11/08 全盲ユーザーのUT動画を見る会 この日も様々な職種の方々が計18名お集まりいただけました。 動画を見たメンバーから スクリーンリーダーユーザーの具体的なイメージがわいた いろいろなデバイス(点字ディスプレイなど)の存在が知れた 適切なタイミングで適切な読み上げを行う重要性 アクセシブルネームの重要性 予測可能であることの重要性 DOM順序の考え方 見出しやランドマークの重要性 カスタムなフォームコントロール実装の落とし穴 フォームエラーの伝え方の勘所 などいろいろな発見の声や感想が聞こえてきました。 2023/12/11 弱視ユーザーのUT動画を見る会 この日も様々な職種の方々が計21名お集まりいただけました。 動画を見たメンバーから コントラストの重要性 拡大操作時の誤操作を誘発してしまうデザイン 識別しづらいフォントの存在 カードUIの2列目以降の発見性の低さ 拡大時のローディング等の状態変更の気付き辛さ 予測可能であることの重要性 などいろいろな発見の声や感想が聞こえてきました。 社内表彰 LIFULL社ではクォータに一度、エンジニアがそろうエンジニア総会というものがあり、そこにアクセシビリティへの取り組みを表彰する枠が用意されています。 1Q(2023/10)の表彰はLIFULL HOME'S iOS開発チームでした。 スクリーンリーダーを利用して検出できたiOSアプリのアクセシビリティ上の問題をまとめ、それぞれへの対応をプロジェクト化していく企画整理を行ってくださいました。 今後のLIFULL HOME'S iOSアプリのアクセシビリティ改善にご期待ください。 外部発表 Orangeの会 2023年08月08日にOrangeの会という勉強会で弊社嶌田が登壇しました。 発表スライド 【実践者に学ぶ】アクセシビリティチームの立ち上げと成長する組織づくり 2023年12月08日の「アクセシビリティチームの立ち上げと成長する組織づくり」という勉強会で弊社嶌田が登壇しました。 https://peatix.com/event/3767934 お知らせ LIFULLではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
テクノロジー本部の布川です。 私の所属するチームではこれまで、ユーザ体験向上のためにLIFULL HOME'Sの高速化に取り組んできました。 本記事では、上記プロジェクトの一環として実施した、一度チューニングを行ったページの性能を将来に渡って維持するための仕組みづくりについて紹介したいと思います。 背景と目的 性能改善後の再劣化の検知 本プロジェクトにおいては、LIFULL HOME'S内の各ページのパフォーマンスを一覧で閲覧できるような仕組みを用意して、優先的に改善対応を行うべき箇所の特定や、施策の評価などに活用していました。 www.lifull.blog Webページのパフォーマンスは、一度改善の対応を行っても、その後さまざまなリリースが積み重なることによって再び劣化してしまうものです。再劣化を検知したタイミングですぐに原因を特定して対応できれば、改善されたパフォーマンスを長く維持することにつながります。 しかし、従来はその変化に気付くためには日常的にダッシュボードを確認する必要があり、パフォーマンス維持に向けた取り組みが効率的な形で行われているとは言えない状況でした。 そこで、高速化チームやリリースを行ったチームが早い段階で問題に気付き、必要に応じて対応できるようにするため、パフォーマンスの劣化を検知して通知する仕組みを用意することにしました。 課題 この仕組みを作るにあたっては、劣化の検知に限られた時間範囲のデータしか使えないことが課題となっていました。 現状のメトリクス収集基盤 現行のサイト監視の仕組みにおいて取得した計測データは、データ収集ツールであるPrometheusに集積されます。 これには、LIFULLのKEELチームが運用するPrometheus, Thanos, AlertManager, Grafanaといった基盤をそのまま使っています。 www.lifull.blog PrometheusとGrafanaで追求する、より良いアプリケーションの可観測性 | ドクセル ページ性能に関するメトリクス取得の仕組み ここでは、メトリクスの収集先であるPrometheusに向けてこのようなpromqlを叩くことで、収集したメトリクスを任意の方法で可視化したり、比較することができます。 avg by (category)(wpt_score{uri="https://www.homes.co.jp/"}) アラート生成にまつわる問題 ここで、Grafanaなどでメトリクスにアクセスする際はPrometheusの永続化層であるThanosのデータにもアクセスできますが、アラートはその前段のPrometheus上のストレージのみを元に生成します。 ページパフォーマンスが劣化したという事象について信頼性を担保するには長期間のメトリクスをもとにアラートを生成することが望ましいですが、永続化前段では3時間分のデータしか保持しないため、この点について対処する必要がありました。 Thanos Rulerを通して長期間のメトリクスをもとにアラートを生成することはできますが、信頼性に関していくつかのトレードオフがあり信頼性を担保したい今回のユースケースには適さないと判断しています。 解決策 新たなメトリクスの生成 それぞれのページの各バイタル(パフォーマンス計測ツールである Lighthouse の評価指標をもとに定義しています)について「一定の値を連続で上回った回数」という新たなメトリクスを導入し、既存のメトリクスとは別途で取得するようにしました。 上図中のWebpagetest Exporterにて、テスト結果を取得する度にこのメトリクスを更新し、ほかのメトリクスと一緒にPrometheusへ登録するようなイメージです。 Prometheusでのデータ保持を24時間程度に増やしてもらう lambdaやcronjobを用意して、定期的にThanosにクエリを行いアラートを生成する などといった手段も考えられましたが、 追加のリソースコストをかけたくない 共通の基盤に対して、一部でしか使われないような特殊な機能を組み込むことは避けたい といった理由から、これらの手段は選択しませんでした。 閾値の設定 バイタルの劣化をカウントする基準となる値は、以下のようにして設定しました。 これらの値は、バイタルの揺れの幅や頻度、絶対値の大きさといった要素を考慮しつつ、暫定的に設定したものになります。 LCP, FCP, SI, TTFB: ある特定の1週間の90パーセンタイル × 1.2 [ms] TBT: ある特定の1週間の90パーセンタイル + 300 [ms] CLS: ある特定の1週間の90パーセンタイル + 0.05 これらの値を連続して所定の回数上回った際にそのバイタルがテスト対象のページにおいて劣化したと判断することで、揺れによる誤検知を防ぎ、アラートの精度を上げることを狙っています。 以上の設定を元に、今まで取得していたバイタルの実測値やそのスコアと同様に、それらが一定の値を連続で上回った回数もメトリクスとして記録されるようにしました。 たとえばLCPの閾値が2680msのページだと、新たに追加したメトリクスは次のような形で時系列に記録されます。 バイタルの実測値(既に存在していたメトリクス) 一定の値を連続で上回った回数(新たに追加したメトリクス) 新たなメトリクスを元に劣化アラートを生成 これをもとに、それぞれのページの各バイタルについて一定の値を連続で上回った回数が閾値に達すると、以下のようなpromqlをベースに指定したslackチャンネルへ通知が行われるようになりました。 avg by (uri, target_group)(wpt_overrun_count{id="CLS"}) >= 7 以上の対応によって、アラート生成の際により長い時間範囲のメトリクスを参照できるようになり、信頼性の高いパフォーマンスの劣化アラートを出すことができるようになりました。 まとめ ここまで、LIFULL HOME'Sのページ性能を維持するための劣化アラートの仕組みづくりについてお話ししてきました。 対応につながるような有意なアラートを実装することを目指して、ページパフォーマンスが継続的に劣化していることを信頼性高く示すためにアラート生成の際に参照できるメトリクスの時間範囲を拡張しました。 実装の際には新しいストレージやツールを用意することなく、今既にある仕組みを拡張する形で問題を解決できたことも良かったと思います。本質的な問題を見極めてから、それを解決する手段として最善なものを選ぶということを継続して行きたいと感じました。 LIFULLでは、今回のようなプラットフォームの効率化にも積極的に取り組んでいます。これらの取り組みに興味を持った方がいらっしゃいましたら、ぜひ以下のリンクからお問い合わせください。 hrmos.co hrmos.co
こんにちは、グループデータ本部データサイエンスグループの清田です。 LIFULLでは、不動産や住まい探しに関する研究の活性化や、AI・情報学分野での人材育成への貢献を目的として、学術研究者向けに LIFULL HOME’Sデータセット を2015年から提供しています。 日本における情報学の中核研究機関である 国立情報学研究所(NII) が運営する 情報学研究データリポジトリ(IDR) の枠組みを活用した取り組みです。 現在では、国内外の150を超える大学・公的研究機関の研究室にデータを提供し、数多くの研究成果も生まれています。 本記事では、2023年12月11日に開催されたIDRユーザフォーラムの様子をお伝えします。 IDRユーザフォーラムとは 情報学研究データリポジトリ(IDR)は、民間企業などが保有しているさまざまなデータ資源のうち、学術研究にも有用なものを受け入れ、適切な契約のもとに研究者に配布することを目的に運営されています。 現在では、LIFULLを含む18社もの企業が、延べ1600以上の研究室にデータセットを提供しています。 外部発表された研究成果の総数は約1500件に達しており、情報学の研究の活性化に大きく貢献していることが見て取れます。 引用元: 大山 敬三, 大須賀 智子, 国立情報学研究所における研究用データセットの共同利用, 情報管理, 2016, 59 巻, 2 号, p. 105-112, 公開日 2016/05/01, Online ISSN 1347-1597, Print ISSN 0021-7298, https://doi.org/10.1241/johokanri.59.105 IDRにおける特筆すべき取り組みの一つとして、データセットを提供する企業と利用者が一堂に会し、直接意見交換できる場としての「 IDRユーザフォーラム 」があります。 2016年に初めて開催され、今回で8回目を迎えます。 2016年のIDRユーザフォーラムの様子については、集会報告記事を書いていますので、もしよろしければ以下の記事もご覧ください。 doi.org 会場の様子 2020年初頭からのコロナ禍により、直近の3回(2020、2021、2022)はオンラインで行われたIDRユーザフォーラムですが、今回は4年ぶりに現地会場での開催が実現しました。 会場となった国立情報学研究所(NII)の建物 今回は、最後に現地開催された2019年を大幅に上回る150名もの現地参加者を得て、大変盛況でした。 各社のデータセットを利用した研究発表26件、研究アイディア発表21件がポスター発表として行われ、多数の学生さんや指導教員の先生方、企業の担当者が活発な議論を通じた交流を持ちました。 発表プログラムについては以下のページをご覧ください。 情報学研究データリポジトリ ユーザフォーラム 「住まい」に関連した研究発表の紹介 LIFULL HOME’Sのデータを利用した住まいに関する研究発表について、いくつか紹介したいと思います。 関西学院大学の福地さんらによるポスター研究発表「地域特性推定のための地物カテゴリを利用した自己教師あり学習」は、 LIFULL HOME’Sまちむすび のデータを活用し、地図画像をCNNモデルにより学習して地域特性を推定しようとするチャレンジングな研究です。 ポスター資料 が公開されていますので、ぜひご覧ください。 この発表は実行委員の方々からも高い評価を受け、 日本データベース学会特別賞 が授与されました。おめでとうございます! 山口大学の石本さんらによる研究アイディア発表「部屋数を与えられた住居間取りのCGANに基づく自動生成」( ポスター資料 )は、LIFULL HOME’Sの高精細度間取り図画像データを用い、指定した部屋数の間取り画像を自動生成するという、大変興味深い研究でした。 生成AIは、主にチャットボットや絵画生成で実用の域に達しつつありますが、今後は、間取りのデザインや設計図の自動生成など、建築やリノベーションの分野でも急速に技術が発展することが期待されます。 兵庫県立大の中山さんらによる研究アイディア発表「不動産情報探索のためのVRインタフェースにおける情報との物理的距離によるLoD制御」( ポスター資料 )は、「賃料」「駅との近さ」「間取り」「築年数」などのさまざまな属性を、VR空間内で可視化することで比較を容易にしようというアイディアです。デモシステムの実演も行われ、多くの参加者が熱心に質問していました。 LIFULL HOME’Sデータセットのこれから LIFULL HOME’Sデータセットは公開から8年が経過し、「 LIFULL HOME’S 3D間取り 」など、サービス価値の向上につながる大きな成果を生み出しています。 2022年度末時点で、 171件もの研究成果 が公開されています。 一方で、「より新しいデータを利用したい」というお声も頂戴しているところです。 ステークホルダーの方々の理解を得つつ、データセットの更新や拡充も検討していますので、ご期待ください。 LIFULLでは、共に成長しながら働く仲間を募っております。 現在、以下の職種を募集しております。LIFULL HOME’Sデータセットなど、豊富な研究開発資源を活かしながら、多様な社会課題の解決に向けた研究開発やプロダクト創出に取り組んでみませんか? シニアデータサイエンティスト(AI活用促進)/AI技術創出からプロダクト創出のPdM or テックリード シニアデータサイエンティスト(AI研究開発)/AI研究開発からビジネス適用/プロダクト創出 多くの方々のご応募をお待ちしております!
エンジニアの松尾です。LIFULL HOME'S の売買領域でエンジニアのマネジメントを担当しています。 チーム開発やプロダクトの運用をしていくにあたって開発ドキュメントは重要です。LIFULLにおいても日々作成やメンテナンスをしていますが、運用にあたって問題もあります。今回はこれらを少しだけでも改善すべく、「断捨離」に取り組んだ話を紹介します。 ドキュメント管理の現状 「断捨離」の意味 開発ドキュメントの「断捨離」 年末の大掃除 個人ワーク グループワーク 試してみた結果 まとめ ドキュメント管理の現状 LIFULLの開発ドキュメントの大半は下記のいずれかで管理されています。 GitHub Google Docs Confluence GitHubではリポジトリに紐づく補足情報や知識を記載しており、Google Docsは主にミーティングの議事録に利用されていることが多いです。これらの中では大きな問題は発生していない認識です。 Confluence も職種を問わず全社員が利用できるツールとして、開発ドキュメントだけではなく多くの用途で活用されています。誰でも利用できる便利さがある一方で、いくつかの問題があります。 ページの重複があり、公式寄りの情報がわかりづらい 更新されていない情報も含めてページの総量が多い 個人スペースに有益な情報があるときもある このような問題の一部を「断捨離」のイメージで解決できないかと考えました。 「断捨離」の意味 「ものを捨てる」文脈で利用されることが多いことばですが、実際には「ものを捨てる」は「断捨離」の一部分を切り取ったものです。「断捨離」の起源はヨガの哲学にある「断行」、「捨行」、「離行」であるそうです。 断行: これから入ってくる不要なものを断つこと 捨行: 既存にある不要なものを捨てること 離行: ものへの執着から離れること 開発ドキュメントの「断捨離」 そこで、ドキュメント管理の問題に「断捨離」の動きを当てはめてみました。 断: 重複ドキュメントをまとめて増えないようにする 捨: 不要なドキュメント/記載を削除する 離: 個人スペースにあるドキュメントをあるべき場所に置く これらを「断捨離」の実践例として、問題解決に取り組んでみることにします。 Googleのソフトウェアエンジニアリング においても、「カノニカルな情報源の確立」が重要であると述べられています。組織全体で使える中央集権的なリファレンスを目指していけるように、まずは個人レベルでの行動を促してみます。 年末の大掃除 気付けばもう12月です。 株式会社サーバーワークスの事例 も拝見し、「年末の大掃除」を称して開発ドキュメントの断捨離に取り組んでみることにしました。 LIFULLには部署の社員で集合して行う総会の文化があり、部署ごとにさまざまなコンテンツでの実施をしています。今回は私が所属しているプロダクトエンジニアリング部2Uの総会の場で、個人ワークとグループワークを行いました。 個人ワーク 前述の開発ドキュメントの「断捨離」を実践例として、まずは自分の手元にのメモ/ドキュメント類を整理してもらいます。時間に余裕があれば所属するグループや組織に関連するページについても見てもらうことにしました。 グループワーク 各自が行ったことを共有しあってもらいます。コミュニケーションと割り切って気楽に話してもらうのがメインですが、「実はそのページ消しちゃダメかも…?」というような万一の抑止が入ることも狙っています。 試してみた結果 15名程度で実際に取り組んでみた結果、実践例やツールを問わずさまざまな動きが見えました。 ゴミ箱、ダウンロードフォルダなどパーソナルなスペースからのファイル削除 自身のまとめで有用そうなファイルを技術横断のスペースに移動 必要なドキュメントの情報の最新化 Slackチャンネルからのリンクの精査 現在時点では未使用のツールのドキュメントをアーカイブ化 「断」でしくみを整えることは、対象や影響範囲が大きい場合は難しいこともあり今後の課題と感じました。とはいえ怪しいドキュメントに「非推奨」とラベルを付けるだけでも効果はありますし、捨てられず量が増えても効率化する手段はあると思います。 ゴミ箱やダウンロードフォルダからの削除はセキュリティリスクを鑑みてもあらためて重要であり、定期的に整理の時間を取ることが望ましいと思います。年末の大掃除は理にかなっていそうですが、年1回と言わず適切な頻度で見直すのが良さそうです。 まとめ 年末の大掃除にかこつけて、開発ドキュメントの整理に取り組んでみました。たった1時間の取組みなので大きな成果とは言えませんが、これを第一歩として組織全体で「断捨離」の文化を作っていければと思っています。 最後に、LIFULL ではともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
本記事は LIFULL Advent Calendar 2023 の17日目の記事です。 qiita.com 事業基盤のチームのマネジメントを担当している磯野です。 自他共に認めるGitHubおじさんとして社内では活動しています。 私たちのチームは開発生産性をより高めるため、開発エコシステムの改善に取り組んでおり、特にGitHubを中心とした生産性向上に注力しています。 今回はその取り組みの一環として、GitHub Actions においてマシンユーザーやAppsを減らしつつ、セキュリティと利便性を向上させるための施策について紹介します。 GitHub Actionsでの課題 GitHub Actionsを用いた処理の実行に際し、いくつかの課題に直面しました。 課題としては以下のような点があります: 標準で使用可能なGITHUB_TOKENではworkflowのトリガーが不可能で、また .github/workflows ディレクトリの更新もできない マシンユーザーやGitHub Appsの管理にコストがかかる マシンユーザーやGitHub Appsを利用するには、リポジトリごとにシークレットを設定し、すべての利用箇所に対して必要な権限を付与する必要があり、セキュリティ上のリスクが存在する これらの課題への解決策として、GitHub Actionsでの認証情報の設定を単純化する処理を開発しました。 これにより、ワークフローに必要な権限のみを付与することでセキュリティの向上が期待できます。 課題解決後の利用シナリオ GitHub Actionsでの認証情報の設定を単純化することで、次のようなことが可能になりました: GitHub ActionsからのpushまたはPR作成時において、Actionsをトリガーすることが可能 .github/workflows/ ディレクトリ内のファイル更新が可能 標準のGITHUB_TOKENではできない処理も簡易に実行可能 また、 リポジトリ側でシークレット情報を管理する必要がない ため、シークレットを各リポジトリへ配布する必要がなく、運用上のメリットもあります。 弊社ではマイクロサービス化が進むにつれてRepositoryが増加し、それぞれに対しマシンユーザーを個別に追加することで運用コストが肥大化しており、大きな運用コスト削減につながっています。 課題を解決するための仕組み GitHub Actionsでの認証情報を単純化するため、以下のような仕組みを開発しました。実線はActions内の処理の流れを、点線は各処理からのAPI呼び出しを表しています。 graph TB subgraph GitHub Actions direction TB START((開始)) ACTION1("aws-actions/configure-aws-credentials<br>次のLambda呼び出し用") ACTION2("configure-github-credentials<br>(今回作成したもの)") ACTION3("実処理") END((終了)) end subgraph GitHub API direction TB API1("POST /app/installations/{installation_id}/access_tokens") API2("その他のAPI") end subgraph AWS direction TB STS["AWS STS<br>後続のLambdaを呼び出すトークンを取得"] LAMBDA["AWS Lambda<br>(IDトークンの検証と<br>GitHubトークンの取得)"] end START --> ACTION1 ACTION1 --> ACTION2 ACTION2 --> ACTION3 ACTION3 --> END ACTION1 -."①後続のLambdaを呼び出す権限のある<br>AWSクレデンシャルの取得".-> STS ACTION2 -."②後続のActionで利用するための<br>GitHubのTokenの取得".-> LAMBDA LAMBDA -."③指定権限のトークン取得".-> API1 ACTION3 -..-> API2 Composite Actionとしての実装は以下のようになっています。 name: Create specified scoped token workflow descroption: | Create specified scoped token workflow inputs: role: required: true type: string outputs: token: description: "GitHub App token" value: ${{ steps.create-app-token.outputs.token }} runs: using: "composite" steps: - name: set env shell: bash run: | echo "LAMBDA_NAME="<<<lambda function name>>" >> $GITHUB_ENV echo "IAM_ROME_ARN="<<<iam role arn>>>" >> $GITHUB_ENV - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: "${{ env.IAM_ROME_ARN }}" aws-region: "<<<region>>>" - id: create-app-token name: create app token shell: bash run: | export AWS_DEFAULT_REGION="<<<region>>>" ID_TOKEN="$(curl --silent -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=configure-github-credentials" | jq -r '.value')" aws lambda invoke --cli-binary-format raw-in-base64-out --function-name "${{ env.LAMBDA_NAME }}" --payload "{ \"id_token\": \"${ID_TOKEN}\", \"role\": \"${{ inputs.role }}\" }" outputfile.txt || : TOKEN=$(cat outputfile.txt | jq -re ".token // empty" || echo "") if [ -z "${TOKEN}" ]; then echo "Token is missing in the JSON file" cat outputfile.txt | jq -re ".errorMessage // empty" exit 1 fi echo "::add-mask::$TOKEN" echo "GITHUB_TOKEN=$TOKEN" >> $GITHUB_ENV echo "token=$TOKEN" >> $GITHUB_OUTPUT 各処理の詳細 ① 後続のLambdaを呼び出すためのAWSクレデンシャルの取得 aws-actions/configure-aws-credentials を用いて、後続のLambdaを呼び出すためのIAMロールのクレデンシャルを取得します。 事前にGitHubをIDプロバイダとして登録し、GitHub経由でAssumeRoleを実行できるよう設定しておく必要があります。 ②GitHubのTokenの取得 IDトークンを用いた呼び出し元の検証を行い、Lambda上で適切な権限を付与します。 IDトークンの正式な検証を行うことで、トークン内に含まれるリポジトリ名やブランチ名、実行者などの情報が利用でき、指定の権限を付与すべき対象かどうかを判断します。 IDトークンから得られる情報については こちら を参照してください。 graph TB subgraph configure-github-credentials direction LR CGC1("IDトークンの取得") CGC2("Lambdaの呼び出し") CGC3("トークンの環境変数への設定") CGC4("トークンをシークレットとして設定") end subgraph Lambda direction LR LAMBDA1("IDトークンの検証") LAMBDA2("指定の権限が利用可能か検証") LAMBDA3("GitHubトークンの取得") LAMBDA4("GitHubトークンの返却") end CGC1 --> CGC2 CGC2 --"IDトークン, 必要な権限"--> LAMBDA1 LAMBDA4 --"GitHubトークン、有効期限"--> CGC3 CGC3 --> CGC4 LAMBDA1 --> LAMBDA2 LAMBDA2 --> LAMBDA3 LAMBDA3 --> LAMBDA4 ③ 指定権限のトークン取得 ②で述べたLambdaから呼び出されるAPIです。 GitHub AppsのInstallation access tokenを生成します。このAPIを呼び出す際はApps側にその権限が必要です。 今後の展望と課題 この仕組みは現在一部のプライベートリポジトリで利用を開始していますが、将来的にはさらに拡張し、最終的にはオープンソースとして公開したいと考えています。 まとめ GitHubのOIDCとAWS Lambdaを活用し、GitHub AppsのInstallation access tokenを安全に取得できるようになりました。これにより、GitHub Actionsでの認証情報のセットアップを単純化し、よりセキュアな実行が可能です。 LIFULLでは開発生産性向上のための開発エコシステムの改善に積極的に行っています。今回の仕組みはその一環であり、AIを活用して全社的な生産性向上を目指すkeelaiや、Kubernetesを用いたアプリケーション実行基盤であるKEELなど、多くの取り組みを進めています。 LIFULLでは一緒に働いてくれる仲間を募集しています。これらの取り組みに興味を持った方がいらっしゃいましたら以下からぜひお問い合わせください。 hrmos.co
KEELチーム の相原です。 今回はeBPFを利用してKubernetesクラスタの可観測性の隙間を埋めている話です。 前回のエントリではLLMにうつつを抜かしていたので本業(?)の話をしようと思います。 www.lifull.blog LIFULLの可観測性の現在地 eBPFとは 可観測性の隙間 NAT Loopback eBPFを実行するには BPF CO-RE libbpf-rsを利用したNAT Loopbackの検知 1. (ユーザ空間) コマンドライン引数として受け取ったDNSをTTLごとに名前解決してIPアドレスを取得する 2. (ユーザ空間) IPアドレスに変化がある度にカーネル空間で動くBPFプログラムにそのIPアドレスのリストを渡す 3. (カーネル空間) Kprobesで tcp_v4_connect/tcp_v6_connect にフックを仕込む 4. (カーネル空間) 受け取ったIPアドレスに対する tcp_v4_connect/tcp_v6_connect があればユーザ空間に対してその実行元のプロセスIDとコマンド名を返す 5. (ユーザ空間) カーネル空間から受け取ったプロセスIDからKubernetes上のコンテナIDを取得する 6. (ユーザ空間) 得られたコンテナIDとコマンド名とともに接続先をPrometheusのMetricsとして公開する 最後に LIFULLの可観測性の現在地 はじめに、LIFULLの可観測性の現在地について軽く書きます。 可観測性にはPrimary Signalsと呼ばれるLogs, Metrics, Tracesの3つの指標があり、我々が開発するKubernetesベースの内製PaaSであるKEELにはそれぞれに対応するプラットフォームが構築されています。 github.com それぞれGrafana Loki, Thanos, Grafana Tempoを採用しており、Grafanaで横断的に閲覧可能です。 加えてContinuous ProfilingのためにPyroscopeも構築されており、Logs, Metrics, Traces, Profilesと4つの指標をプラットフォームとしてサポートしています。 (ここまで来ると全てGrafana製品で統一したいですが、Thanosはかれこれ5年以上運用していて十分に実績があるのでアーキテクチャにそれほど違いがないこともありGrafana Mimirへの移行は検討中です) Logsはアプリケーションの標準出力・標準エラーに加えてService Meshのレイヤで取得した共通フォーマットのアクセスログを集めていて、TracesとProfilesはそれぞれ我々が管理する共通のアプリケーションフレームワークに事前に組み込まれているOpenTelemetryとPyroscope SDKによって自動で収集しています。 Metricsも同様にOpenTelemetryで取得していますが、その他にもアクセスログから集計したURIごとのレイテンシ・サクセスレートをfluentdで出力していたり、拙作の kube-trivy-exporter を使ってアプリケーションの脆弱性情報を収集していたり Core Web Vitals を計測したりとPrometheus Exporterを適宜作りながらあらゆる情報を集めています。 クラスタレベルだと prometheus/node_exporter や kubernetes/kube-state-metrics , kubernetes/node-problem-detector の他に、Podごとの利用料を按分するPrometheus Exporterなどがあり、内製PaaSの利用者は様々な事象を観測できるようになっています。 しかし、これだけやっていてもまだ観測できないものがあります。LinuxカーネルレイヤのMetricsです。 そしてそれはeBPFを利用することで取得可能です。 eBPFとは 既にeBPFの説明はありふれていますが軽く説明しておきます。 eBPFとはカーネル空間で安全にプログラムを実行するためのサンドボックス技術です。 eBPF is a revolutionary technology that can run sandboxed programs in the Linux kernel without changing kernel source code or loading a kernel module. ebpf.io サンドボックス内で実行されるC言語のプログラムをBPFプログラムと呼ぶことが多いです。 BPFプログラムはイベント駆動でネットワーク上のイベントやカーネル上のイベントなどを起点として実行されます。 カーネル上のイベントは主に事前にLinuxカーネル上に定義されたフックポイントである Tracepoints の他に、カーネル空間の任意の関数の実行にフックを仕込むことのできる Kprobes が利用できます。 Kprobesは任意の関数に仕込んでなんでもできる一方でカーネルのバージョンアップによって関数名が変わった際などに追従することが難しく、Tracepointsは事前に定義されているためカーネルのバージョンアップに左右されないものの定義されていない場所を起点に発火させることができないといった違いがあります。 eBPFは Maps というデータ構造を持っていてこれでユーザ空間と状態を共有できるため、KprobesやTracepointsをもとに発火したBPFプログラムでMetricsを収集し、Mapsを通してユーザ空間に出力することでLinuxカーネルレイヤのMetricsを観測できるようになります。 可観測性の隙間 では実際に観測したいLinuxカーネルレイヤのMetricsとはなんでしょうか。 例えばどんなMetricsが取れるかを知りたい場合は iovisor/bcc#tools がお勧めです。 bccとは詳しくは後述しますが、BPF Compiler Collectionの略でeBPFを簡単に実行するための仕組みです。 同時に様々なeBPFを利用したツールも提供されていて、 bcc: General Perfomance Checklist を見たことある方はいらっしゃるのではないでしょうか。 この中のうちあなたが管理するシステムの潜在的な問題にまつわるものが観測したいMetricsとなるわけですが、ここではLIFULLでの分かりやすい例を一つ紹介したいと思います。 私達が観測したいLinuxカーネルレイヤのMetricsの一つは、 Kubernetesクラスタ内からのある接続先に対するプロセスごとの接続回数 でした。 なぜそんなMetricsを取得したいかを説明するためにはまずNAT Loopback(hairpinning)ついて説明する必要があります。 NAT Loopback NAT Loopbackとはhairpinningとしても知られる機能で、NAT環境下においてLAN内のクライアントが自身に対してWANからアクセスする際にその通信をループバックさせるというものです。 これは利用しているルータやロードバランサによっては対応していないことがあり、実際にAWSのNetwork Load Balancerは Preserve client IP addresses を有効にしているとNAT Loopbackは機能せず接続がタイムアウトしてしまうということが知られています。 docs.aws.amazon.com Kubernetesクラスタにおいてはクラスタ前段にIngress Controllerに紐づいた Type: LoadBalancer なNetwork Load Balancerを立ててクラスタ外のリクエストを受けるというものはよくあるパターンです。 この時、クラスタ内のPodからそのNetwork Load Balancerに接続してしまうとタイムアウトしてしまう可能性があるということになります。 KubernetesクラスタとしてもLAN内であればKubernetesのサービスディスカバリを使って接続した方がレイテンシが低いため、プラットフォーマーとしては Kubernetesクラスタ内からIngress Controllerに紐づいたNetwork Load Balancerに対して接続しているクライアント を検知する必要があります。 全てのPodにService Meshが入っていれば検知可能でしょうし、パケットキャプチャでもクライアントの存在自体は検知可能です。 しかし歴史的理由から私達のIstioは一部導入できていないアプリケーションがあったり、Kubernetesクラスタには複数のアプリケーションが載っているためクライアントの存在を検知できただけでは不十分でクライアントの特定まで行う必要があります。 そこでeBPFでTCPの接続処理にあたる tcp_v4_connect/tcp_v6_connect をフックして検知をしようということになりました。 (eBPFを利用すればパケットの向き先を勝手に変えてしまうこともできますが今回は検知のお話をします) eBPFを実行するには さて、それではeBPFを動かすにはどうしたらいいでしょうか。 eBPFはサンドボックス化されたVM上でBPFプログラムを実行することで安全性を担保しているため、そのVMが解釈できるバイトコードにBPFプログラムをコンパイルして実行する必要があります。 そこでよく使われていたものが先ほど紹介したBPF Compiler Collection、bccです。 bccはeBPFを簡単に実行するための仕組みで、bccをライブラリとして利用したソフトウェアを実行すると、ClangをフロントエンドとしたLLVMでBPFプログラムをコンパイルし成果物のバイトコードをVMにロードしてeBPFが実行されます。 これにより利用者はBPFプログラムだけを書けば簡単にeBPFを動かすことができるといったわけです。 しかし、ご存じの通りClangは重いバイナリですし実行時にコンパイルするというアプローチは実行時のオーバーヘッドを伴います。 監視対象のサーバの台数分だけClangをインストールしてコンパイルしてとなると支払うコストが大きくなるためプロダクション環境に手放しに導入できるものではありません。 "よく使われていた"とbccを過去形で紹介しましたが、現在はその問題を解決するためにBPF CO-REという仕組みがあります。 BPF CO-RE BPF CO-REの説明もわざわざここでしなくても感がありますが一応簡単にしておきます。 BPF CO-REはBPF Compile Once - Run Everywhereの略で、その名の通りコンパイルを一度だけすれば成果物のバイナリをどこででも動かすことができるというものです。 詳細な仕組みについては省きますが、 libbpf/libbpf というBPF CO-REをサポートしたライブラリを使うことで利用できます。 libbpfはC言語向けのライブラリですが、 libbpf/libbpf-rs というRustバインディングも公式に提供されているためRustでも開発可能です。 我々KEELチームはproxy-wasmでEnvoyの拡張を書く際にもRustを利用しているため、ここからはlibbpf-rsを使って Kubernetesクラスタ内からIngress Controllerに紐づいたNetwork Load Balancerに対して接続しているクライアント を検知する方法を説明してきます。 今回はユーザ空間でも多少処理が必要となるためRustで書いた方が無難でしょう。 2021年当時はいくつかlibbpf-rsに不足している機能がありましたが今はlibbpfと遜色なく利用できるようになりました。 libbpf-rsを利用したNAT Loopbackの検知 まずは大まかな設計を決めましょう。 改めて、今回実現したいことは Kubernetesクラスタ内からIngress Controllerに紐づいたNetwork Load Balancerに対して接続しているクライアント の検知です。 この仕組みは他にも"退役予定のデータストアにクエリしているクライアントの洗い出し" などにも使えるため、今のNetwork Load BalancerはIPアドレスが変わらなくなりましたがDNSベースで汎用的に作ってみます。 大まかな処理の流れは以下といったところでしょうか。 (ユーザ空間) コマンドライン引数として受け取ったDNSをTTLごとに名前解決してIPアドレスを取得する (ユーザ空間) IPアドレスに変化がある度にカーネル空間で動くBPFプログラムにそのIPアドレスのリストを渡す (カーネル空間) Kprobesで tcp_v4_connect/tcp_v6_connect にフックを仕込む (カーネル空間) 受け取ったIPアドレスに対する tcp_v4_connect/tcp_v6_connect があればユーザ空間に対してその実行元のプロセスIDとコマンド名を返す (ユーザ空間) カーネル空間から受け取ったプロセスIDからKubernetes上のコンテナIDを取得する (ユーザ空間) 得られたコンテナIDとコマンド名とともに接続先をPrometheusのMetricsとして公開する 今回はクライアントの特定まで行う必要があるため、プロセスIDからKubernetes上のコンテナIDを取得して公開することで、コンテナIDからPodを特定できるようにしています。 最終的にこのソフトウェアをKubernetes上にDaemonSetとしてデプロイするイメージです。 順番に見ていきます。 1. (ユーザ空間) コマンドライン引数として受け取ったDNSをTTLごとに名前解決してIPアドレスを取得する ここは本筋ではないのでさらっと流します。 こちらが今回開発するソフトウェアのエントリポイントとなる main.rs です。 ご覧の通り、当然普通のRustアプリケーションとして開発できます。 #[tokio::main] async fn main () -> Result < (), Box < dyn std :: error :: Error + Send + Sync + 'static >> { let args: Args = Args :: parse (); < snip > let mut handles = vec! []; let ns = args.nameserver. parse :: < std :: net :: SocketAddr > () ? ; let conn = trust_dns_client :: udp :: UdpClientStream :: < tokio :: net :: UdpSocket > :: with_timeout ( ns, std :: time :: Duration :: from_secs ( 5 ), ); let (client, bg) = trust_dns_client :: client :: AsyncClient :: connect (conn).await ? ; handles. push ( tokio :: spawn (bg)); let (tx, mut rx): ( tokio :: sync :: mpsc :: UnboundedSender < IPMap > , tokio :: sync :: mpsc :: UnboundedReceiver < IPMap > , ) = tokio :: sync :: mpsc :: unbounded_channel (); let hm = std :: sync :: Arc :: new ( futures :: lock :: Mutex :: new ( IPMap :: new ())); for host in args.hosts { for record_type in [ trust_dns_client :: rr :: RecordType :: A, trust_dns_client :: rr :: RecordType :: AAAA, ] { let mut cloned_client = client. clone (); let cloned_hm = std :: sync :: Arc :: clone ( & hm); let cloned_tx = tx. clone (); let host = host. clone (); handles. push ( tokio :: spawn (async move { let name = trust_dns_client :: rr :: Name :: from_str ( & host). unwrap (); let mut cache = IPCache :: new (); loop { let response: trust_dns_client :: op :: DnsResponse = cloned_client . query ( name. clone (), trust_dns_client :: rr :: DNSClass :: IN, record_type, ) .await . unwrap (); let answers: & [ trust_dns_client :: rr :: Record] = response. answers (); let mut max_ttl = 0 ; match record_type { trust_dns_client :: proto :: rr :: RecordType :: A => { let mut new = vec! []; for record in answers { if record. ttl () > max_ttl { max_ttl = record. ttl (); } if let Some ( trust_dns_client :: proto :: rr :: RData :: A ( ref ip)) = record. data () { new. push ( u32 :: swap_bytes (( * ip). into ())) } } new. sort (); let default = vec! []; let old = cache.ipv4. get ( & host). unwrap_or ( & default); if old != & new { let mut hm = cloned_hm. lock ().await; if let trust_dns_client :: proto :: rr :: RecordType :: A = record_type { for ip in old { hm.ipv4. remove (ip); } for ip in new. iter () { hm.ipv4. insert ( * ip, host. clone ()); } } cloned_tx. send (hm. clone ()). unwrap (); cache.ipv4. insert (host. clone (), new); } } < snip > _ => { continue ; } } if max_ttl > 60 { tokio :: time :: sleep ( std :: time :: Duration :: from_secs (max_ttl as u64 )).await; } else { tokio :: time :: sleep ( std :: time :: Duration :: from_secs ( 60 )).await; } } })); } } < snip > Ok (()) } 処理内容は単純で、行儀よくTTLごとに名前解決をしながらIPアドレスに変更があればそれをチャネルで送信しています。 実際には AAAA レコードの実装もしてIPv6に対応する必要がある点にご注意ください。 エラーハンドリングも省略しているので必要に応じて修正する必要があります。 2. (ユーザ空間) IPアドレスに変化がある度にカーネル空間で動くBPFプログラムにそのIPアドレスのリストを渡す 次は本題となるカーネル空間との接合部分です。 libbpf-rs 周辺のエコシステムには libbpf-cargo というBPFプログラムからRustのスケルトンをビルド時に生成してくれるツールがあります。 以下のような build.rs を書いておくと、 fn main () -> Result < (), Box < dyn std :: error :: Error + Send + Sync + 'static >> { libbpf_cargo :: SkeletonBuilder :: new () . source ( "src/bpf/connect.bpf.c" ) . build_and_generate ( std :: path :: Path :: new ( "src/bpf/skel.rs" )) ? ; Ok (()) } src/bpf/skel.rs が生成されて src ディレクトリ内でこのように利用できるというものです。 mod skel { include! ( "bpf/skel.rs" ); } そうすると skel モジュール以下に *Builder が生えてくるのでこれを使ってカーネル空間で動くBPFプログラムにそのIPアドレスのリストを渡していきましょう。 use skel :: * ; unsafe impl plain :: Plain for connect_bss_types :: event {} pub fn watch ( map: crate :: IPMap, stop: std :: sync :: Arc < std :: sync :: atomic :: AtomicBool > , ) -> Result < (), Box < dyn std :: error :: Error + Send + Sync + 'static >> { let builder = ConnectSkelBuilder :: default (); let mut open = builder. open () ? ; let v4_keys = map.ipv4. keys (); let mut v4_keys_array: [ u32 ; 16 ] = [ 0 ; 16 ]; let v4_keys_len = v4_keys. len (); for (i, key) in v4_keys. enumerate () { v4_keys_array[i] = * key; } open. rodata ().tool_config.daddr_v4 = v4_keys_array; open. rodata ().tool_config.daddr_v4_len = v4_keys_len as u32 ; let mut load = open. load () ? ; load. attach () ? ; < snip > } この watch 関数はチャネルから送られてきた crate::IPMap を受け取ってBPFプログラムとやり取りをするというものです。 BPFプログラムに値を渡すためには、先に説明した Maps の他に .rodata セクションを利用できます。 open.rodata() で .rodata セクションに書き込まれた値はC言語のBPFプログラムで const として参照できるというものです。(感覚的には逆に思いますがそういうものみたいです) 本来IPアドレスのリストは動的に変化するためReadOnlyな .rodata セクションではなくMapsが望ましいですが今回は単純化して .rodata セクションを利用しています。 ( Arc<AtomicBool> な stop という変数でIPMapに変更があった際に古いIPMapを持った watch を止めるみたいなことをイメージしています) そして load.attach() でBPFプログラムをカーネルにロードしたらようやくBPFプログラムです。 3. (カーネル空間) Kprobesで tcp_v4_connect/tcp_v6_connect にフックを仕込む 今回フックしたい tcp_v4_connect/tcp_v6_connect には事前定義されたTracepointsがないためKprobesを使います。 メインの処理はこのようになります。 SEC ( "kprobe/tcp_v4_connect" ) int BPF_KPROBE (tcp_v4_connect, struct sock *sk, struct sockaddr *uaddr, int addr_len) { u64 __pid_tgid = bpf_get_current_pid_tgid (); gid_t tgid = __pid_tgid >> 32 ; pid_t pid = __pid_tgid; bpf_map_update_elem (&sockets, &pid, &sk, 0 ); return 0 ; } SEC ( "kretprobe/tcp_v4_connect" ) int BPF_KRETPROBE (tcp_v4_connect_ret, int ret) { u64 __pid_tgid = bpf_get_current_pid_tgid (); gid_t tgid = __pid_tgid >> 32 ; pid_t pid = __pid_tgid; struct sock **skpp = bpf_map_lookup_elem (&sockets, &pid); if (!skpp) { return 0 ; } if (ret) { goto end; } <snip> } Kprobesには kprobe と kretprobe という2つのエントリがありそれぞれ関数の開始と終了に紐づいています。 この実装では tcp_v4_connect 関数の開始と終了をフックしているというわけです。 関数が呼び出された時点では実際に接続が行われたかどうかは判断できないため、終了時にMetricsを送信したいところですが kretprobe では終了ステータスしか取ることができません。 そのため、 kprobe の bpf_map_update_elem で引数をMapsで保持しつつ kretprobe の bpf_map_lookup_elem でそれを取り出して処理をします。 実際には tcp_v6_connect の実装もしてIPv6に対応する必要がある点にご注意ください。 kprobe で保存する引数は実際にLinuxカーネルの関数のシグネチャと一致している必要があり、それを調べるためにはLinuxクロスリファレンスがお勧めです。 いくつか候補がありますが私は https://elixir.bootlin.com/ を使っていて、関数名で検索するとこのように定義元にジャンプできます。(この時カーネルのバージョンによる差異に注意する必要があります) https://elixir.bootlin.com/linux/v6.6.1/source/net/ipv4/tcp_ipv4.c#L201 4. (カーネル空間) 受け取ったIPアドレスに対する tcp_v4_connect/tcp_v6_connect があればユーザ空間に対してその実行元のプロセスIDとコマンド名を返す 以下は kretprobe の完全版です。 bpf/bpf_helpers.h や bpf/bpf_core_read.h にヘルパ関数が色々入っているのでそれらを使いながら必要な情報を取り出しています。 詳細な説明は省きますが関数名からなんとなく雰囲気はつかめるはずです。 #include "../../vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_core_read.h> #include <bpf/bpf_tracing.h> SEC ( "kretprobe/tcp_v4_connect" ) int BPF_KRETPROBE (tcp_v4_connect_ret, int ret) { u64 __pid_tgid = bpf_get_current_pid_tgid (); gid_t tgid = __pid_tgid >> 32 ; pid_t pid = __pid_tgid; struct sock **skpp = bpf_map_lookup_elem (&sockets, &pid); if (!skpp) { return 0 ; } if (ret) { goto end; } struct sock *sk = *skpp; u32 daddr_v4 = BPF_CORE_READ (sk, __sk_common.skc_daddr); if (! filter_daddr_v4 (daddr_v4)) { goto end; } uid_t uid = bpf_get_current_uid_gid (); struct event event = { .tgid = tgid, .pid = pid, .uid = uid, .protocol = ipv4, }; BPF_CORE_READ_INTO (&event.daddr_v4, sk, __sk_common.skc_daddr); bpf_get_current_comm (event.comm, sizeof (event.comm)); bpf_perf_event_output (ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof (event)); end : bpf_map_delete_elem (&sockets, &pid); return 0 ; } sock のメンバも同様にLinuxクロスリファレンスで検索して確認することができ、 __sk_common.skc_daddr で向き先のIPアドレスを取得できます。 https://elixir.bootlin.com/linux/v6.6.1/source/include/net/sock.h#L357 filter_daddr_v4 関数は先ほど .rodata セクション経由で渡した tool_config を使いながら対象のIPアドレスへの接続をフィルタリングする関数です。 const volatile struct { u32 daddr_v4[ADDR_LEN]; u32 daddr_v4_len; u8 daddr_v6[ADDR_LEN][ 16 ]; u32 daddr_v6_len; } tool_config; static __always_inline bool filter_daddr_v4 (u32 daddr) { if (tool_config.daddr_v4_len == 0 ) { return true ; } for ( int i = 0 ; i < tool_config.daddr_v4_len; i++) { if (daddr == tool_config.daddr_v4[i]) { return true ; } } return false ; } bpf_perf_event_output は BPF_MAP_TYPE_PERF_EVENT_ARRAY というリングバッファのMapsを使ってユーザ空間に値を送信するための関数で、Mapsは以下のように events として定義されています。 SEC ( ".maps" ) struct { __uint (type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint (key_size, sizeof (u32)); __uint (value_size, sizeof (u32)); } events; これを使うことでユーザ空間に対して実行元のプロセスIDやコマンド名を event という構造体に詰めて返すことができます。 eBPFで利用できるリングバッファには BPF_MAP_TYPE_RINGBUF もありますがLinuxカーネルのバージョンが5.8以上でないと利用できず、例えばUbuntu 20.04とかでは利用できないためご注意ください。(今回紹介している事例は2021年のものであるため BPF_MAP_TYPE_PERF_EVENT_ARRAY を利用していました) sockets というMapsは kprobe と kretprobe の間で引数を持ち回すためだけのものなので用が済んだら中身を削除しています。 5. (ユーザ空間) カーネル空間から受け取ったプロセスIDからKubernetes上のコンテナIDを取得する プロセスIDからコンテナIDを取得する方法は少なくとも2021年時点ではあまり情報がなかった記憶があるので説明しておきます。 コンテナランタイムはcri-o想定です。 コードを見ていただくと早いでしょう。 use std :: io :: BufRead; pub struct Metadata { container_id: String , } pub fn from_pid (pid: i32 ) -> Option < Metadata > { let var = std :: env :: var ( "PROCFS_PATH" ); let path = if let Ok ( ref path) = var { std :: path :: Path :: new (path) } else { std :: path :: Path :: new ( "/proc" ) }; let cgroup = path. join (pid. to_string ()). join ( "cgroup" ); if let Ok (file) = std :: fs :: File :: open (cgroup) { let mut reader = std :: io :: BufReader :: new (file); let mut buf = String :: new (); let _ = reader. read_line ( &mut buf); return buf . trim_end () . split ( ':' ) . last () . and_then (extract_container_id) . map ( | container_id | Metadata { container_id }); } None } enum CgroupDriver { Cgroupfs, Systemd, } fn detect_cgroup_driver < T: AsRef < str >> (cgroup_path: T) -> CgroupDriver { if cgroup_path. as_ref (). starts_with ( "/kubepods.slice" ) { // https://github.com/kubernetes/kubernetes/blob/v1.26.1/pkg/kubelet/cm/cgroup_manager_linux.go#L82 CgroupDriver :: Systemd } else { // https://github.com/kubernetes/kubernetes/blob/v1.26.1/pkg/kubelet/cm/cgroup_manager_linux.go#L111 CgroupDriver :: Cgroupfs } } fn extract_container_id < T: AsRef < str >> (cgroup_path: T) -> Option < String > { // https://github.com/kubernetes/kubernetes/blob/v1.26.1/pkg/kubelet/cm/node_container_manager_linux.go#L40 if ! cgroup_path. as_ref (). starts_with ( "/kubepods" ) { return None ; } match detect_cgroup_driver ( & cgroup_path) { // https://github.com/cri-o/cri-o/blob/v1.26.1/internal/config/cgmgr/cgroupfs.go#L65 CgroupDriver :: Cgroupfs => cgroup_path . as_ref () . split ( '/' ) . last () . map ( | s | s. to_string ()), // https://github.com/cri-o/cri-o/blob/v1.26.1/internal/config/cgmgr/systemd.go#L80 CgroupDriver :: Systemd => cgroup_path . as_ref () . split ( '/' ) . last () . and_then ( | unit | unit. trim_end_matches ( ".scope" ). split ( '-' ). last ()) . map ( | s | s. to_string ()), } } 基本的にはprocfsからcgroupの情報にアクセスして、cgroupドライバに応じて判断するという流れになっています。 cgroupのパスの中にコンテナIDが含まれているのでそれを取り出すだけです。 このソフトウェアはDaemonSetとしてKubernetesクラスタにデプロイすることを想定しており、その際にPodにはホストのprocfsをマウントする必要があるため環境変数 PROCFS_PATH からprocfsのマウントポイントを受け取れるようにしています。 6. (ユーザ空間) 得られたコンテナIDとコマンド名とともに接続先をPrometheusのMetricsとして公開する BPF_MAP_TYPE_PERF_EVENT_ARRAY から送信されてきた値は、ユーザ空間では libbpf_rs::PerfBufferBuilder のコールバックとして取得できます。 use skel :: * ; unsafe impl plain :: Plain for connect_bss_types :: event {} pub fn watch ( map: crate :: IPMap, stop: std :: sync :: Arc < std :: sync :: atomic :: AtomicBool > , ) -> Result < (), Box < dyn std :: error :: Error + Send + Sync + 'static >> { < snip > let meter = opentelemetry :: global :: meter ( "connectracer" ); let counter = meter. u64_counter ( "connect_total" ). init (); let buffer = libbpf_rs :: PerfBufferBuilder :: new (load. maps_mut (). events ()) . sample_cb ( move | _cpu: i32 , data: & [ u8 ] | { let mut event = connect_bss_types :: event :: default (); plain :: copy_from_bytes ( &mut event, data). expect ( "Data buffer was too short" ); if let Some (host) = match event.protocol { connect_bss_types :: protocol :: ipv4 => map.ipv4. get ( & event.daddr_v4), connect_bss_types :: protocol :: ipv6 => { map.ipv6. get ( &u128 :: from_be_bytes (event.daddr_v6)) } } { let command = if let Ok (s) = std :: str :: from_utf8 ( & event.comm) { s. trim_end_matches ( char :: from ( 0 )) } else { "" }; let mut attributes = vec! [ opentelemetry :: KeyValue :: new ( "host" , host. clone ()), opentelemetry :: KeyValue :: new ( "command" , command. to_string ()), ]; if let Some (metadata) = crate :: metadata :: kubernetes :: from_pid (event.pid) { let mut m = metadata. into (); attributes. append ( &mut m); } counter. add ( & opentelemetry :: Context :: current (), 1 , & attributes); } }) . build () ? ; < snip > } (先ほどのKubernetesの Metadata は以下のようなFromトレイトを実装しているため、そのまま metadata.into() できます) impl From < Metadata > for Vec < opentelemetry :: KeyValue > { fn from (metadata: Metadata) -> Self { vec! [ opentelemetry :: KeyValue :: new ( "container_id" , metadata.container_id, )] } } PrometheusのMetricsとして公開するためにはOpenTelemetryを利用するとして、あとは取得したコンテナIDとともにインクリメントするだけです。 コンテナIDさえ取得できてしまえば kubernetes/kube-state-metrics が出力する kube_pod_container_info と組み合わせて以下のようなクエリでPodと紐づけることができるため、ここではそれ以上のことはしません。 tcp_v4_connect_total * on(container_id) group_left(namespace, pod) label_replace(kube_pod_container_info{container_id!=""}, "container_id", "$2", "container_id", "(.+)://(.+)") 最後に 少し長くなってしまいましたが、あとはOpenTelemetryのregistryのMetricsを公開するサーバを書けば、晴れてeBPFによる Kubernetesクラスタ内からIngress Controllerに紐づいたNetwork Load Balancerに対して接続しているクライアント の検知が完成です。 このように、eBPFを利用することでKubernetesクラスタの可観測性の隙間を埋めることができました。 コンテナIDの取得など、実際にKubernetesクラスタで利用するイメージもついたのではないでしょうか。 (一部のbccベースのトレーシングツールと異なり)ユーザ空間のリソース消費は非常に軽微で、このソフトウェアの場合はメモリ使用量が6MB未満程度でCPUも処理内容をご覧の通りほとんど使わないためご安心ください。 BPF CO-REで可搬性のあるバイナリにすることでbccの時にあったClangへの依存や実行時コンパイルを取り払うことができ、プロダクション環境でも比較的気軽にeBPFを導入できます。 eBPFは kretprobe で返り値を上書きできたりと副作用があったり、パフォーマンスのオーバーヘッドも0ではないため導入には慎重になるべきですが、実際に数年のプロダクション環境での運用の中で今のところ問題は発生していません。 (LIFULLでは kretprobe で返り値が上書きできることを利用して簡単なCircuit Breakerの仕組みを準備していたりもします) あわせて、Network Load BalancerのNAT Loopback問題についてもくれぐれもご注意ください。 性質上クラスタが巨大になるほど発生率が低くなるため、しっかり監視していないと謎のTail Latencyに悩まされることになります。 ブログを書くのをサボってしまいeBPFの旬はとっくに過ぎてしまった感がありますが、時に(当時の)最新技術を使いながらPlatform Engineeringすることに興味がある方がいれば是非こちらからお問い合わせください! hrmos.co
プロダクトエンジニアリング部の二宮です。 私は 有料集客のデータを扱う部署の仕事 をしながら、サイドプロジェクトとしてKEELチームとともに keelaiという社内のAIチャットボット の開発にも関わっています。keelaiについての詳細は相原がこちらの記事で解説しています。 www.lifull.blog keelaiはSlack上で動くAIチャットボットを含んだ "汎用AI(仮)" 技術スタックで、LIFULLグループのSlackユーザーおよそ1000人程度の中で月間200人以上に利用して頂いてます。これはけっこうな成功例と言っていいんじゃないでしょうか? 結果的にですが、keelaiの社内広報やサポートを担当することが多くありました。また私はエンジニア向けの Q&Aフォーラムを開設 していること、 ベトナム拠点との交流会 の企画にも関わっていることから、社内の技術広報やコミュニケーションについて考えることが多くありました。そこで培ったノウハウや考えも含めて共有します。 大きく考える いきなり精神論だし、社内広報に限らない話ですが、新しい基盤を作るのに大事なことだと思ってます。 keelaiが大きなユーザー数を獲得できた一番大きな要素は、 相原の記事 にもある通り「子会社や業務委託の人々にも使ってもらおう!」と大きく考えて狙っていき、そのために必要なこと(例えば予算や権限管理等)を整備していったことだと思ってます。 私達の汎用AI(仮) keelaiは多言語対応や契約形態やグループ会社ごとのFunction Callingのアクセス制御を経て、現在は国内外のグループ会社全体で利用されています。 社内知識からの回答やWebブラウジングはもちろんのこと、画像・音声に関する操作や社内システムとのインテグレーション、WebAssemblyでサンドボックス化された安全なCode Interpreter相当の機能も準備中です。 特に、keelaiの開発チームではけっこう冗談みたいな会話をしていて、「200人に使ってもらったし、次は2000人だな(※社員数超えてる)」っていう話から「じゃあ子会社も入れなきゃ(※実際には入れても足りない)」っていう実際にできる話に繋がっていきました。 私たちはついつい現在の延長上で考えてしまうのですが、他の人に面白いそうだと思ってもらうためには、今までやってきていない話の中から「意外といけそうじゃない?」っていう面白いアイデアを実現していることが大事じゃないかと思います。これは ベトナム拠点との交流会 でも共通していたと思います。 面白いアイデアを探索するために、みんなで心にイケイケ社長を宿しましょう😎 次の行動を喚起する 広報は主にSlackで行っています。ただ、ハンガーフライトの告知でも感じているのですが、かなり「あのイベント面白そうだけどいつやるの?え?先週終わった?」みたいな話をされてしまうことも多いです。 それなりの高頻度(週に1~2回程度)で投稿する 何らかの行動を喚起する 具体的には「カレンダーの予定追加」「プロダクトを触ってもらう」など keelaiでは「showcaseの記事を読んでもらう」ことを置いて、週に数回程度で次のような投稿を雑談チャットに投稿しています。 この「行動を喚起する」という話は、『 システム運用アンチパターン 』の「コミュニケーションを適切に定義する」という章の内容が参考になっています。以前、読書会をしたログが こちら にあり、他の項目も役立つはずです。 こういうとき私たちは「こんなにたくさんの機能を実装したぞ!すげーだろ!」みたいなことを言いがちですが、むしろ読者に次にどんな行動を取ってほしいのか考えて、ちょっと軽めの文章で誘ってみるのがコツなんじゃないかと思ってます。 keelaiはSlack Botとして実装されており、単に「次は君たちも使ってみてくれ!」とも言いやすいし、一般的なChatGPTの利用方法も集めやすいため、その点では楽です☺️ 継続的に接点を持つ 定期的な広報をすることにはもう一つ意味があって、広報を見た人からの問い合わせが来るきっかけになることです。なんとなく質問や提案をするタイミングを逃したまま忘れてしまっている人も多いと思っていて、その相手の周知にもなります。 実際に「keelaiのAPIがあれば、CIで呼び出して社内情報も加味した自動コードレビューに使いたい」という話が来て案内したり、「ドキュメントを見ても導入方法が分からない」と言われドキュメントの不備をアップデートしたりしています。 keelaiはサポート用の公式のSlackチャンネルも用意していますが、実際にはこういうカジュアルな問い合わせのほうが多いです。また、 交流会の運営 としては、逆にマネージャー職の社内キャリア相談の広報に対して「一緒にマネージャーの座談会をやりましょう」と私から提案して実現したこともあります。 継続的に接点を持つことと、思いつきのアイデアを投稿しやすい雰囲気を作ることが、後から考えるとけっこう面白いコラボレーションに繋がっていたと感じてます。 まとめ 特にエンジニアには、いい仕事をしていて他の人の役に立つモノを作っているはずなのに、本来のプロダクトやアイデアの持つポテンシャルを発揮できていない人も多いんじゃないかと感じることがあります。この記事がそういう人がうまくコラボレーションを広げられるきっかけになると嬉しいです。 また、少し話が逸れるので書きませんでしたが、keelaiの開発に関わっていて、こうした基盤を作ることによって、同じ会社の仲間にベストプラクティスやいいアイデアを広げることに貢献できると感じています。こちらについては「 LLM活用促進に向けたPlatform Engineeringからのアプローチ 」を読んでください。 最後に、LIFULLにはこうした新しいアイデアをどんどん議論していく文化の素地があるし、まだまだ発展できると思ってます。こうした文化を作っていきたいエンジニアは、ぜひ求人やカジュアル面談のページも見て頂けると嬉しいです。 hrmos.co hrmos.co