株式会社LIFULLのブログ - TECH PLAY

TECH PLAY

株式会社LIFULL

株式会社LIFULL の技術ブログ

664

グループデータ本部データサイエンスグループの嶋村です。 今回、データサイエンスグループが主催でデータサイエンス系の自社イベント『 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
KEELチーム の相原です。 前回のエントリ で我々KEELチームはKubernetesベースの内製PaaSであるKEELを開発・運用する傍ら、LLMという新たなパラダイムの台頭にあわせてベクトルデータベースの提供や周辺ソフトウェアを社内向けに開発していることを紹介しました。 www.lifull.blog あれから数ヶ月が経ち、現在私達はLIFULLのグループ会社全体に向けて汎用AI(仮)を提供しています。 もともと我々KEELチームはPlatform Engineeringの一環として、Kubernetesベースの内製PaaSであるKEELのほかにコードジェネレータによる一貫したPaaS体験を中心に様々なユーティリティをコマンドラインから提供するkeelctl, KEELが提供するプラットフォームのユーザ体験を向上させるブラウザ拡張のkeelextを開発してきました。 Platform Engineeringの責任は無限にスケールさせることです。 プラットフォーム・コマンドライン・ブラウザを手中に収めてソフトウェアエンジニアの生産性向上を盤石なものとした私達が次に目を向けたものが、職種問わずあらゆる業務上の課題を解決できる汎用AIでした。 そもそも社内のLLM活用が思うように進んでいなかった中で、まずは活用の背中を見せること、そして"無限にスケール"を目指す上で避けて通れない汎用AIというテーマにはスケーラビリティや信頼性に専門性を持つ私達が適任だと判断しました。 今回目指す汎用AI 汎用AI(仮) keelai Agents Function Callingの利用 マルチエージェントによるトークン消費の抑制 マルチエージェントにおける状態共有とベクトルデータベース Bot EmbeddingRetrieval Evaluation その後 OpenAI Assistants API 最後に 今回目指す汎用AI とはいえ汎用AIは壮大なテーマです。 プロダクトの鉄則は「小さく作る」なのでまずはファーストリリースのゴールを設定しましょう。 なるべく作らない テキストベースでの対話型インターフェース 職種問わずあらゆる業務上の課題を解決できるスケーラビリティを持つ スケーラビリティとコストのバランスを保つ 私達のプラットフォーム戦略は(3人というコンパクトなチームということもあり) インナーソース に重きを置いていて、無限にスケールする仕組みを用意した後は社内からContributionを集めて加速的に成長していくことを狙っています。 実際にコードジェネレータによる一貫したPaaS体験を中心に様々なユーティリティをコマンドラインから提供するkeelctlでは、あるプラクティスを浸透させたい開発者が自らの手でkeelctlに機能を実装する文化が根付いていて、社内の全体最適に貢献するとともに標準コマンドラインツールとしての地位を確立しています。 汎用AIを目指す上でもあらゆる業務上の課題を解決する機能を私達だけで実装することは現実的ではないため、 「なるべく作らない」ことでコストとバランスが取れたスケーラビリティだけを素早く示してインナーソースによって成長していく ことを目指しました。 汎用AI(仮) keelai そうして開発されたものが汎用AI(仮)であるkeelaiです。 (やっていき感を出すために社内プロダクトでもロゴを作るようにしていますが盛り上がるのでお勧めです) keelaiの基本的なコンセプトを私達はマルチエージェントと呼んでいて、サブタスクを解決するために自律的に動くエージェントを複数組み合わせて協調させることで無限にスケールすることを目指します。 現在では一般的なLLMのユースケースに加えて、例えば以下のようなユースケースにも対応しています。 Webから最新のコンテンツを取得して、社内情報と突合しながら新しいコンテンツを生成する 社内のテーブルスキーマに応じたSQLの生成とバリデーション 社内のデザインガイドラインに準拠した画像の生成 とにかく分からないことややりたいことがあれば、それが社内のことでも社外のことでも一見無理そうなことでもとりあえず指示するといい感じにしてくれるというものです。 しかしまだエージェントの実装はあらゆる課題を解決するために十分ではないし、エージェントを人間が実装しないといけない時点で...という気もするので "汎用AI(仮)" です。 そんなkeelaiは複数のコンポーネントから実現されており以下のような構成になっています。 agents: サブタスクを解決する複数のエージェントの実装 bot: agentsを呼び出しSlack Botとして稼働するテキストベースの対話型インターフェース api: 同様の機能をHTTPで提供するAPI memory: エージェント間で共有する短期記憶で軽量なベクトルデータベースであるRediSearchをバックエンドとする brain: エージェント間で共有する長期記憶でオブジェクトストレージであるAmazon S3をバックエンドとする embedding-retrieval: ChatGPT Retrieval Pluginの信頼性の問題を解決した社内知識を回答するためのソフトウェアでベクトルデータベースであるQdrantをバックエンドとする embedding-gateway: 各Embeddings APIに対する透過的なキャッシュレイヤ summarizers: やり取りが長期化した場合や巨大なドキュメントをもとに回答する場合に要約するモジュールで、用途に応じて複数の要約のオプションが用意されている loaders: embedding-retrievalに文書をインデックスするためのバッチプログラムで、GitHubやSlack, JIRA/Confluenceなど各種データソースごとに実装が存在する manager: loadersの実行管理を行うバッチプログラムで、差分インデックスや並列数の制御を行う evaluation: apiを使いながら典型的なkeelaiのユースケースを実行し、その精度をLLMによって出力したメトリクスから評価するためのバッチプログラム 私達はこれをPlatform Engineeringらしくパッケージとしても配布しており、特定のユースケース用にカスタマイズされた汎用AI(仮)を社内で開発できるようにしています。 初回リリースまでは2週間ほどと大分「なるべく作らない」ことで手を抜けたのでここからはそういった点を紹介していきます。 Agents エージェントはOpenAIのGPT-4をベースにFunction Callingを使って実装されていて、ブラウザ操作・画像生成・音声処理・社内システムとのインテグレーションなどサブタスクごとにエージェントが分かれています。 私達に自前のLLMを開発する体力はないのでGPT-4を利用することは当然として、エージェントの実装にはFunction Callingも使ってとことん楽をしています。 Function Callingの利用 Function Calling は関数の名前と引数の型をOpenAPI形式で与えるとコンテキストに応じて実行すべき関数とその引数を推論してくれるOpenAIが提供している機能で、エージェントがどの機能を呼び出すかをどうかを自律的に判断できるようになります。 似たようなことを実現するための手法として Plan-and-Solve Prompting が提案されていますが、Function Callingを利用することで極めて少ない実装量でそれっぽい挙動を再現することができます。 恐らく ChatGPT plugins の中身もFunction Callingでしょうし GPTs のActionsも同様のはずです。 以下は社内システムとのインテグレーションを司る ObservabilityAgent のイメージです。 messages = [{ "role" : "user" , "content" : "Pod/keelai-5675dfdf7b-d7c2l で起きているエラーの原因を調べて" }] tools = [ { "type" : "function" , "function" : { "name" : "get_metrics" , "description" : "Get metrics from Prometheus" , "parameters" : { "type" : "object" , "properties" : { "pod_name" : { "type" : "string" }, "metric" : { "type" : "string" , "enum" : [ "container_cpu_cfs_throttled_seconds_total" , "container_cpu_usage_seconds_total" , ], }, "duration" : { "type" : "string" , "enum" : [ "1h" , "6h" , "24h" ]}, }, "required" : [ "pod_name" , "metric" , "duration" ], }, }, }, { "type" : "function" , "function" : { "name" : "get_logs" , "description" : "Get logs from Grafana Loki" , "parameters" : { "type" : "object" , "properties" : { "pod_name" : { "type" : "string" }, "duration" : { "type" : "string" , "enum" : [ "1h" , "6h" , "24h" ]}, }, "required" : [ "pod_name" , "duration" ], }, }, }, ] response = openai.chat.completions.create( model= "gpt-3.5-turbo-1106" , messages=messages, tools=tools, tool_choice= "auto" , ) 必要に応じて実行すべき関数名と引数が推論されるため、あらかじめ用意しておいた関数を推論された引数で呼び出し、実行結果を返却することで汎用AIっぽい挙動を低コストに実現することができます。 その結果に対して更に推論を挟むことで ReAct 相当の機能も実現することができ、軽微な実装で更に精度を向上可能です。 マルチエージェントによるトークン消費の抑制 しかしFunction Callingも万能ではありません。 OpenAIの課金はトークンの入出力によって行われますが、 tools として与えた関数の候補は入力トークンとして毎回処理されます。 そのため汎用AIを目指す上で多くの関数を実装していくと、単なる「こんにちは」のような問いに対して膨大なトークンが消費されてしまいます。 そのために私達はサブタスクごとに実装されたエージェントに親子関係を持たせて、それを多段で呼び出すことによってトークン消費を抑えるアーキテクチャを採用していてこれをマルチエージェントと呼んでいます。 起点となる親のエージェントには以下のように子のエージェントを呼ぶFunction Callingを定義することで、 tools に膨大な関数群を書くことなく毎回のトークン消費を抑えつつ様々な機能の呼び出しに対応しています。 { "type" : "function" , "function" : { "name" : "launch_image_agent" , "description" : "Launch an agent to manipulate images" , "parameters" : { "type" : "object" , "properties" : { "instruction" : { "type" : "string" }, }, "required" : [ "instruction" ], }, }, }, { "type" : "function" , "function" : { "name" : "launch_observability_agent" , "description" : "Launch an agent to fetch observability signal" , "parameters" : { "type" : "object" , "properties" : { "instruction" : { "type" : "string" }, }, "required" : [ "instruction" ], }, }, }, それぞれのエージェントをどう協調させるかどうかもLLMに判断させる ということになります。 これにより画像生成に関係ないタスクの場合は launch_image_agent 分のわずかなトークン消費で抑えることが可能です。 ここにもFunction Callingを利用することでマルチエージェントも「なるべく作らない」で実現することができました。 突き詰めていくと「ある関数が実行された後にしか呼ばれない関数」のようなものが出てくるはずで、内部でコールスタックを持ちながらその依存関係をもとに tools を構築すると更にトークン消費を抑えられるなど、細かいトークン節約のテクニックはまた別のエントリで紹介することにします。 マルチエージェントにおける状態共有とベクトルデータベース 子のエージェントはRPCを通して呼ばれることもあり、負荷の特性に応じて異なるサーバ・異なる言語で実装されることがあります。 そのため、エージェント間の状態の共有には memory と brain という2つの外部記憶を通して行っています。 セッションが終了すると破棄される短期記憶である memory にはベクトルで各エージェントが処理結果を格納し、他のエージェントからは曖昧な表現でその処理結果を取り出せるようにしています。 例えば、画像生成を行うエージェントが「生成した犬の画像」として memory に画像を保存しておき、それをファイルアップロードを行うエージェントが「先ほど生成した犬の画像」として取り出すといった具合です。 素直にエージェント間で状態を共有しようと思うと、子のエージェントの実行結果を親のエージェントの入力トークンとして与えることになりますが、これは当然トークンの消費が激しくなってしまいます。 マルチエージェントにすることでコストとバランスが取れたスケーラビリティを実現するとともに、用途に応じた外部記憶を利用することで機能性を維持することができました。 この memory の実体はRedisの全文検索モジュールであるRediSearchであり、RedisStackというパッケージを利用することで簡単に用意することができるため、ここもまた「なるべく作らない」で実現されています。 この用途では永続性は不要であるためメモリ上に全て載せてパフォーマンスに優れるRedisが適切です。 Bot LLMを使ったアプリケーションを提供する上でまず最初に選択肢として挙がるものがBotインタフェースでありSlack Botでしょう。 私達も開発初期は当然Slack Botとして実装しましたが、汎用AI(仮)として成長していく中でもWebのインタフェースを用意するつもりはなくSlack Botとして作り続けています。 Slack Botは「なるべく作らない」上で色々と都合がいいです。 OpenAIはServer Sent Eventsで結果をストリームで受け取ることができるが、それをキューに溜めながらSlack APIの chat.update を呼ぶ実装でリアルタイムな返答を再現できる(Rate Limitのために適当にThrottlingする必要はある) 汎用AIを目指すと成果物をファイルとしてアップロードさせたくなるが、Slackはそのファイルの入出力先として十分機能する ダイレクトメッセージでSlack Botに話しかければクローズドに利用することができる上、そのやり取りを他の人に共有することもできるし当然パブリックチャンネルで直接利用することもでき、ChatGPTの Shared Links 相当の機能が実装不要で実現できる ユーザのメタデータは既にSlackが持っているため、所属組織ごとのFunction Callingの制限や言語の切り替えを認証の仕組みなしに実現できる 会話の履歴は当然Slack側に保存されているためこちらで保存する必要がない このようにSlack Botとして実装することでWebで同じ機能を実現するより格段に手を抜くことができ、汎用AIとして本質的な機能開発に集中することができました。 Slack Botを作るためにはBoltというフレームワークが用意されているためこれを使うだけです。 slack.dev SlackのSlash Commandとして開発者が容易に拡張できることも好みで、ChatGPTの Custom instructions 相当の機能が社内からのContributionを受けて開発されていたり、GPTsのように作成したプロンプトを配布する仕組みもSlash Commandとして実装されています。 (前述の通り会話の履歴を保存する必要がないため、長期記憶である brain の役割はこういった Custom instructions 相当の機能を実現するためにのみ利用されています) EmbeddingRetrieval EmbeddingRetrievalは 前回のエントリ でも軽く触れた社内向けの ChatGPT Retrieval Plugin のforkです。 社内知識を回答するために必要なコンポーネントで、ベクトルデータベースを利用してSemantic Searchすることで関連するドキュメントを取得することができます。 いくつかのパッチは書いたものの、結局ChatGPT Retrieval Pluginが各種データストアに対応するために膨れ上がった依存関係がネックとなりforkという道を選んでしまいました。 やはり私達としては可観測性や信頼性は重要であり、前回のエントリで触れたものを中心にいくつかの改善を施し、利用しないデータストアの実装を削除して利用しています。 その甲斐(?)あって低いエラー発生率や完全な分散トレーシングが得られるようになっており十分な信頼性で運用できています。 LangChain で TextSplitter や Vector stores を使って実現する方法もありましたが、結局LangChainも実装の箇所によって品質にムラがあることには変わりなく依存も同様に巨大となるため、シンプルなChatGPT Retrieval Pluginをforkすることが正解だったと感じています。 開発初期ではChatGPT Retrieval Pluginを使っていたため、「なるべく作らない」ためにChatGPT Retrieval Pluginを利用するということは依然有効だと思います。 私達はベクトルデータベースとして既に用意してあったQdrantを利用していますが、Qdrantも十分にシンプルなものの「なるべく作らない」というコンセプトとしてはAzure Cognitive Searchを利用することが適切でしょう。 Evaluation 汎用AIを開発する上では継続的な精度の監視が必須です。 プロンプトチューニング一つで"あちらを立てればこちらが立たぬ"になりがちで、内部のモデルを変えた時のインパクトも観測する必要があります。 継続的に監視するにあたって毎回Slack Botを手動で呼び出すわけにもいかないためAPIが必要となりますが、エージェントとSlack Botはトークン数削減を狙ったマルチエージェントな実装により疎結合になっているため開発コストは低いはずです。 そしてAPIを実行した結果を何らかの方法で評価するわけですが、この際にはAzure Machine LearningのPrompt Flowが参考になります。 Prompt Flowにはいくつかの評価メトリクスが用意されており、汎用AIの精度評価に関してもこれをそのまま利用できるはずです。 learn.microsoft.com Relevance: 質問に対する回答が与えられたコンテキストとどの程度関連しているか Coherence: 質問と回答に一貫性があるか Fluency: 質問と回答が文章的に自然か などがLLMの評価メトリクスとして用意されています。 これをそのまま利用してしまうことで「なるべく作らない」で精度監視を実現することができました。 その後 私達の汎用AI(仮) keelaiは多言語対応や契約形態やグループ会社ごとのFunction Callingのアクセス制御を経て、現在は国内外のグループ会社全体で利用されています。 社内知識からの回答やWebブラウジングはもちろんのこと、画像・音声に関する操作や社内システムとのインテグレーション、WebAssemblyでサンドボックス化された安全なCode Interpreter相当の機能も準備中です。 こういった機能の実装は独立したエージェントを開発するだけで開発者誰しもができるようになっていて、Platform Engineeringを専門とする我々KEELチームはここに新たなプラットフォームとしての可能性を見出しています。 私達のプラットフォーム戦略はインナーソースによる成長を積極的に狙っていると先に書きましたが、その進捗はまずまずと言ったところで、 ChatGPT Retrieval Pluginの構築を一緒始めた二宮以外にも チーム外のContributorは何人か生まれつつあるのでここからの横展開を頑張ろうといったところです。 今回紹介した通り、この程度であればコアとなる Agents の機能以外を「なるべく作らない」で実現することができます。 プラットフォーマー各位はプラットフォームの次の一手として是非汎用AIをご検討ください。 LIFULLでは今後プラットフォームとの連携を一層強めていき、社内システムとインテグレーションされた汎用AI(仮)による障害対応の自動化や、(うまくGitHub Copilotの隙間を縫いながら)社内の開発ガイドラインをもとにしたコードレビューの自動化をやっていく予定です。 OpenAI Assistants API と、ここまで書いておいてですが、実は似たようなものはOpenAI Assistants APIを利用することでも実現できます。 https://platform.openai.com/docs/assistants/overview platform.openai.com タイトルにのみ書いてここまで触れてきませんでしたが、OpenAI Assistants APIとは2023年11月6日のOpenAI Dev Dayで発表された機能で、汎用AI(仮)のようなAIアシスタントを開発するためのフレームワークのようなものです。 会話の履歴の保持 ファイルサーバの提供(Fine-tuningでも利用されるので厳密にはAssistants APIの持ち物ではありませんが) ファイルサーバと統合されたマネージドベクトルデータベースを利用してSemantic Searchする retrieval の提供 Code Interpreterの提供 Function Callingとのインテグレーション が主な機能と言っていいでしょう。 今後利用できる機能はOpenAIによって実装されて増えていく予定らしくこれは強力な選択肢となるはずです。 しかし、会話の履歴の保持やファイルサーバはSlack Botとして実装していればSlackに肩代わりしてもらえますし、 retrieval 相当の機能はChatGPT Retrieval PluginとAzure Cognitive Searchで十分事足ります。 クラウド時代の常としてマネージドな部分が増えるほど価格は高くなるわけで、今回はOpenAI Assistants APIを使わず「なるべく作らない」で無限にスケールする汎用AI(仮)を開発した話を紹介しました。 (そもそも私達はDev Day前にここまで作ってしまっていたこともありこのまま突っ走ろうと思います。) 最後に 我々KEELチームはKubernetesベースの内製PaaSを開発・運用する傍ら、汎用AI(仮)の開発に踏み切りプラットフォームの影響力を強めることに成功しました。 KEELチームはこれまでもコマンドラインのソフトウェアやブラウザ拡張を開発してきており、プラットフォームの成功、ひいてはLIFULLの目指す「あらゆるLIFEを、FULLに。」実現に向けてソフトウェアエンジニアとしてPlatform Engineeringの領域からあらゆる手を尽くしていきます。 もし興味を持っていただけた方がいましたら是非こちらからお問い合わせください。 hrmos.co
プロダクトエンジニアリング部の吉田と申します。 普段はRubyやTypeScriptといった言語を使ったサーバサイドエンジニアをしています。 今回、サイトの閲覧障害をきっかけに行ったポストモーテム会が個人的にとても有意義だと感じたので紹介させてください。 障害分析レポートの紹介 弊社では障害が起きた場合、障害分析レポートを書くという決まりがあります。 この障害分析レポートというものは、一般的には SRE の用語でポストモーテムとして知られている障害対応時のことを記録する文書のことです。 弊社では品質管理を行っている部署がテンプレートやフォーマットを整えてくれており、内容としては オライリーのSRE本 の付録Dに記載してある「ポストモーテムの例」にかなり似通った内容です。 かいつまんで紹介すると下記のような内容を記載するものです。 障害の概要 影響範囲 タイムライン 水面下で起きていた問題(根本の問題など) 教訓(良かったこと、悪かったこと) これらを書くことでいったい何が起きていたのかを後から振り返ることができ、同じ過ちを繰り返さないためのアクションを考えることができる、というものです。 障害分析レポートの運用上の課題感 ただ、個人的にこの障害分析レポートの"運用"に課題があると常々感じていました。 その課題とは、障害分析レポートを書く人の主観に基づいた記載だけで終わりがちであり、"障害発生時の対応方法の改善"に寄与しないという点です。 障害分析レポートは障害対応が落ち着いたら上記のフォーマットにのっとって記載する、というところまでルールとして存在していますが、それ以外については何も定まっていません。 フォーマットについて書くと、何が起きて(Why)、誰が(Who)、いつ(When)、何を対応したか(What)は分かりやすい構成になっています。 しかし、タイムラインの項目で どう対応したか(How) の部分をどれくらい書くのかは書く人の裁量に委ねられています。 障害発生時にどのような調査をして、対応したメンバーの間ではどのように連携を取っていったのか、ということまで書く人はそう多くありません。 これでは再び障害が発生して別のメンバーが対応するとなった場合、どう振る舞えば良いのかということをその場その場で学ぶしかありません。 また、リモートワークをしている昨今では、チャットの文字上だけでコミュニケーションを済ますことが多いです。 障害対応をするときは阿吽の呼吸のようなものが求められることもあり、オフィス勤務をしている際には部署を超えて一ヵ所に集まって声を掛け合いながら作業をするということも珍しくありませんでした。 しかし、リモートワークではお互いの姿が見えないのでそれも難しいことです。 これは完全に私の主観混じりの決めつけに近いものですが、お互いの姿が見えないということでコミュニケーションロスが発生していたはずだ、だからそこには障害対応時の改善点があるのではないかと睨んでいました。 そういったことも踏まえ、障害分析レポートの物自体は良いのに活用ができていない、と感じていたのです。 上記のような課題感を抱えていたあるとき、休日に障害が発生し、関係者全員がリモートワークでの障害対応を強いられる状況になりました。 そして対応後、感じていた課題感を明らかにするべくポストモーテムを振り返る場としてポストモーテム会を開くことを提案しました。 ポストモーテム会の実施前の準備 今回は私が障害分析レポートを書き、それを土台にしてチームで話し合う会として設定をしました。 話し合う内容としては下記のとおりです。 障害分析レポートに書かれていないけど伝えたかったこと Slackで書くほどでもないが当時起きていたこと Slackには書かなかったが実は思っていたこと 特にそれ以上のことは決めておらず、時系列を見ながらああでもない、こうでもないとワイワイする感じです。 また、私が書き忘れていた、書き漏らしていたという出来事も話し合う過程で補完されていくだろうという狙いがあります。 休日明けの最初のミーティングのタイミングでメンバーにポストモーテム会を提案しました。 今回参加したメンバーには申し訳ないのですが、私の感じている課題感の共有はそこそこに、やりたいからやらせてくれ、といった感じでやらせてもらいました。 本来はもう少し丁寧に共有すべきだったと反省しています。 障害分析レポートのテンプレートに沿って書いた時系列 障害分析レポートのテンプレートに沿って書いた時系列は以下のようになりました。 時間 起きたこと 対応 02:05 外部サービスの影響で特定のページが閲覧できなくなる障害が発生 10:30 ユーザーが増えたことでアラートが鳴る 10:45 エンジニア2名で調査を開始(当時のSlackへのリンクを貼っている) 11:14 特定のデータが原因である可能性が濃厚だと判断し、データを管理しているインフラ部門へ協力を要請(当時のSlackへのリンクを貼っている) 11:40 インフラ部門が対応できない可能性を踏まえ別の方法を検討しているところ、主管部門から対応を開始する連絡をもらう(当時のSlackへのリンクを貼っている) 12:10 障害解消 インフラ部門による対応が完了(当時のSlackへのリンクを貼っている) 初動から約1.5時間の出来事、わずか6行にまとまっており簡潔でわかりやすいですね。 括弧書きをしているように証跡となるSlackのやりとりのリンクを記載しているので詳細な内容は追おうと思えば追えますが、この表を見ただけでは障害対応時にコミュニケーションロスなどの課題があったかどうかまでは見えてきません。 他にも10:45のエンジニアが何をどう調査していたのかはこの記載からは分からなかったり、11:14のところでもブログ記事向けに「特定のデータ」とボカして書いていますが、社内向けに書いたレポートでも具体的に何のデータのことかを書いていませんでした。 また、11:14〜11:40まで何をやっていたのかも分かりません。 こうして振り返ってみると自分でも粗が目立つとは思うのですが、書いた直後はこれで良いと思っていました。 これを見ながら複数人で話し合うことにより情報が補完できれば、という狙いです。 ポストモーテム会をやってみての時系列 上記を元にポストモーテム会を行った結果、細かく補完された時系列は以下のようになりました。 時間 起きたこと 対応 02:05 外部サービスの影響で特定のページが閲覧できなくなる障害が発生 02:30ごろ 実はエラーが出始めていたが、そのページのアラートの設定が漏れていた 04:31 アラートが鳴っていたが Slackの @here を利用しているため通知が飛んでいなかった 10:30 @here とか関係なしに通知をする設定にしていた1名が気付く(Aさんとする) Aさんが調査を開始する 10:35 私がアラートに気付きSlackにて調査開始宣言をする (エラーログの一部をレポートに貼り付け)エラーログが分かりづらく原因の特定に難航する 10:45 Aさんと私がSlackでコミュニケーションを開始 Aさんがかつて実装に関わったこともある関係で当時の記憶から心当たりを見つける 11:14 (Aさん)原因を特定しインフラ部門に対応を依頼するが、 休日ということもあり遠慮してメンションはつけなかった   また、アプリケーション側で対応することも検討したが対応するアプリケーションが多かったのでインフラで一括対応するほうが速いと判断した 11:18 (私)緊急と判断し、メンションをつける 11:24 インフラ部門が反応、私用のため20分ほど動けない旨の連絡をもらう その間、もっと短時間で解消する方法を模索する 11:40 インフラ部門から対応を開始する連絡をもらう そのままインフラ部門に対応してもらったほうが早そうなので別の方法の模索を打ち切り影響範囲調査に作業を転換 12:10 障害解消 インフラ部門に対応してもらったが、インフラ部門内での対応手順が定まっていなかったので手こずってこの時間になってしまった 12:35 影響範囲調査が完了、Slackにて報告 12:40 解散 2倍近くの行数になり、だいぶ肉付けされましたね! 前述の10:45前後の内容が充実したおかげでどういう振る舞いをしていたかも分かりますし、原因特定に至った流れも分かるようになりましたし、11:14〜11:40の間に行っていたことも分かるようになりました。 ポストモーテム会後の時系列を受けてのアクションを決める そして太字にした部分は、 障害対応における改善点 です。 これは最初の時系列などでは見えてこないもので、ここを改善することで障害から復旧までの短縮につながると考えられます。 理想を言えば手作業なしに復旧するのが望ましいですが、現実問題としてそうはなっていないので、オペレーションを改善するのも大事なことですね。 上記で太字にした部分に対応するアクションとして大まかに下記の3つを行いました。 アラートの対応が漏れていたので設定をするチケットを作成した アラートは @here ではなく @channel にするチケットを作成した 障害発生時はタイミング関係なく、遠慮なくメンションをするというルールにした 他にもアプリケーションの改善といったアクションもあるのですが、今回の障害対応の改善の趣旨から外れるためここでは省略することにします。 まとめ 障害はいつ起きるものか分かりません。そのときに備え、ちょっとした対応でも複数人の目で振り返ってみることで新たな発見があり、小さいかもしれませんが改善を積み重ねることができるのではないかと思います。 なんなら今この記事を書いている最中にも「このアクションを入れたほうが良かったのでは?」という発見もあったりして振り返ることの大事さを噛み締めています。 みなさんもポストモーテム会を開いてみてはいかがでしょうか? LIFULLでは共に改善を積み重ねていく仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
エンジニアの寺井です。本記事では LIFULL HOME'S でのサイト高速化への取り組みについて紹介します。 今回の内容は高速化施策の一環で、サイト速度を計測して監視できるようにした話です。 LIFULL HOME'Sのサイト内の各URLのサイト速度を一覧で見られるようにして、遅い箇所の特定や改善に活用できるようにしました。この取り組みについてお話します。 はじめに ~なぜサイト高速化が重要なのか~ Webページを見る際に数秒経っても真っ白の画面で読み込まれない、あるいは読み込まれている途中で何も操作ができない、こんな経験みなさんはありませんか?中にはページが読み込まれずイライラしてそのページから離れてしまうといった経験をした方も少なくないでしょう。 2017年にGoogleが実施したモバイルページの 調査 によると、ページ読み込み速度が遅くなるにつれて直帰率(ページを離れてしまう確率)が跳ね上がるという結果が示されています。このことから、サイト速度が遅いというだけでユーザーがサイトを利用してくれる機会が損失してしまうことがわかります。 上記の調査結果はやや昔のものですが、時間あたりに得られる体験が特に重視される現代ではサイト速度による影響はさらに向上していると推測されます。 以上より、サイトの高速化は真摯に取り組むべき内容だと判断できます。 高速化指標の話 サイト高速化の重要性は前項で触れましたが、では実際にサイト速度はどのように測ればよいのでしょうか? Webページを構成する上で必要な要素は数多あります。たとえば LIFULL HOME'S だと物件情報を取得してくるまでのバックエンドの処理部分、ネットワーク通信、物件画像を表示する処理などさまざまな要素が絡み合ってページが構成されています。実際に手元の端末でサイトにアクセスして比較して...といった方法では感覚的な比較にしかなりませんし、具体的にはどの部分の処理が遅いのかという判断も難しいでしょう。 そこで、サイト速度を測るための指標として存在する パフォーマンス指標 やそのパフォーマンス指標をスコアで評価する Lighthouse score といった速度計測用の指標を計測することにしました。 サイト監視の現状と課題点 LIFULLには内製の「KEEL」というLIFULLグループ全体で利用することを目的としたKubernetesベースのアプリケーション実行基盤が存在します。 KEELについての詳しい話は以下のエントリで紹介されているのでよければご参照ください。 https://www.lifull.blog/entry/2020/12/02/000000 KEELチームの活動により、LIFULL HOME'Sへの一連のリクエストに共通のID(TraceId)が割り振られ、関連するリクエストのログを横断で絞り込むことができます。これにより一連のリクエストのトレーシングが可能になり、あるユーザーのリクエストに対して裏ではどの処理がどれほどの処理時間を占めているかを把握することが可能になりました。この機能を活用することにより、高速化指標のTTFB(Time to First Byte, サーバ処理が終わって最初のレスポンスが返ってくるまでの時間)とほぼ同値のものを詳しく解析できました。 この活動に関しての詳しい話は以下のエントリに記載されているのでこちらも合わせてお読みください。 https://www.lifull.blog/entry/2022/12/22/090000 (上記記事より引用)バックエンド処理を詳しく解析可能 一方で、実際のLighthouse scoreはコンテンツが表示されるまでの時間やJavaScriptによって操作できない時間など、フロントエンド部分の処理の評価も重要になってきます。 バックエンド部分の詳しい解析はできてもフロントエンド部分の解析はうまくできないのが現状の課題でした。 WebPageTest を使った計測 そこで WebPageTest (以下WPTと表記)を導入することにしました。 WPTのしくみとしては「agent」と呼ばれるインスタンスが実際に計測対象のページを訪問して、かかった読み込み時間を計測できます。また、通信の帯域を調整でき、擬似的にPC環境でアクセスした状況やモバイル環境でアクセスした状況を作り出すことが可能です。つまりPCサイトとモバイルサイトそれぞれで、実際のユーザーが使うシナリオに近い状態で計測を行うことが可能です。 もちろんLighthouseを直接実行して計測するのでもよかったのですが、Lighthouseと比較してWPTの方がより詳しい項目まで取得できることが決め手となりWPT導入に至りました。 WPTはブラウザからも実行できるのですが、たとえば NodeJS用のAPI実行パッケージ を使うことでAPIを叩いて実行することも可能です。そこで、WPTが動作するサーバとagentが動くインスタンスを立ち上げ、APIを叩くアプリケーションを作成し、定期的にサイト速度の計測を実行するシステムを構築しました。また、WPTで取得した計測データは、データ収集ツールのPrometheusを用いて集約した後、データ可視化ツールのGrafanaを用いてダッシュボード表示をするようにしました。これにより、LIFULL HOME'S 内の主要URLで今どれくらいの速度スコアが出ているのかを可視化することが可能になりました。 この一連のシステムを簡易的に表現すると以下のような図になります。 システム全体図 このダッシュボードを活用することにより、現在LIFULL HOME'S内で極端に遅くなっている箇所の洗い出しや、リリース前後で速度変化が起きたことも検知することが可能になりました。 LIFULL HOME'S 主要URLごとにLighthouse scoreをグラフで一覧表示するようにしました もちろん、このダッシュボードからそれぞれのWPTテスト結果へ飛ぶことも可能にしました。WPTの個別結果では処理にかかった時間が時系列で見られる「Waterfall図」など、詳しく解析するためのデータが揃っています。 ある計測結果のWaterfall図 計測をするようになって所感や今後の展望 WPTを定期実行することにより、サイト内で弱点となっている遅い箇所の特定と改善に効率的に取り組むことができました。結果として大幅なスコア改善を達成できたこともありました。 詳しい数値はお見せできませんが、Lighthouse score が一気に30ptほど改善できた箇所もありました Webサービスを継続していく上で、機能追加に伴いサイトが遅くなっていくことはどうしても起こりうる問題だと考えます。 それは知らず知らずの内に細かい処理が積み重なって遅くなってしまった、というパターンが多いと思われますが、その原因の根本はサイト速度という指標を誰もが簡単に見られないからではないでしょうか。 今回の施策では時間をかけて定期的な計測と監視をできるしくみを作り上げ、LIFULLの全エンジニアがサイト速度を監視することが可能になりました。今までは気付くことができなかった速度の劣化等も各々が検知できるようになり、各々が改善に取り組むことも可能になりました。長期間のサービス運用において、自分たち自身で気付くことができ、対応できる環境は大事だと考えるので、今後長い目で見た時に大きな効果があると良いなと思います。 おわりに LIFULLではこのような内部システムの高速化や効率化などにも積極的に取り組んでいます。 本記事を読んでLIFULLに興味を持っていただけた方はぜひカジュアル面談を受けてみませんか?よろしければ以下の求人情報もご覧ください。 hrmos.co hrmos.co
プロダクトエンジニアリング部の千葉です。 LIFULL HOME'S不動産査定 と ホームズマンション売却 の開発に携わっています。 この記事では、売却査定サービスにおけるアクセシビリティ対応の取り組みについて紹介していきます。 マンション査定シミュレーション input要素 コンボボックス 所在地選択ダイアログ キーボードフォーカス リストボックス 最後に マンション査定シミュレーション マンション査定シミュレーションは、インターネット上でマンションの価格を調べることができる簡易査定の機能です。 売却計画を立てる際や、不動産一括査定サービス利用時の参考として使用することができます。 LIFULL HOME'Sのマンション査定シミュレーションではマンション名、所在階、専有面積、間取りを入力すると参考価格を算出することができます。 まずは、ここの入力欄要素での取り組みについて紹介します。 input要素 専有面積では10~150までの数字の入力が求められます。想定外の値が入力された際には、 pattern属性 と title属性 を用いることにより、エラーメッセージを表示して入力欄までフォーカスを強制的に戻すところまでブラウザが自動的に行ってくれるようにしています。 pattern属性 正規表現で入力値のパターンを指定するもの title属性 input要素にpattern属性が指定されている場合にパターンの説明を指定するもの パターンが一致していない際にツールチップで一致するための要件を説明してくれる < input type = "text" pattern = "([1-9][0-9]|1[0-3][0-9]|14[0-9])(\.[0-9]+)?|150" title = "10〜150までの数字を入力してください" > また、入力時に表示されるソフトウェアキーボードの種類として、所在階の入力では小数入力の必要がないため、 inputmode属性 に数字が表示される numeric を指定しています。一方で小数の入力の可能性がある専有面積では、区切り文字も含んだものが表示される decimal を指定しています。 inputmode属性 input要素の入力時に表示されるソフトウェアキーボードの種類を指定するもの numeric 数字の入力ができるキーボードが表示される decimal 実数の入力ができるキーボードが表示される 数字と区切り文字 (ピリオド . または カンマ , ) が含まれる < input type = "text" inputmode = "numeric" > < input type = "text" inputmode = "decimal" > コンボボックス つづいてコンボボックスでの取り組みの紹介です。 コンボボックスはキーボードなどで文字入力することも、入力候補のリストから選択することもできる入力ボックスのことです。 WAI-ARIAの仕様に基づき役割や状態を適切に設定することで、矢印キー、Enterキー、Escapeキーなどのキーボードだけで操作が完結できるように、また、スクリーンリーダーでもコンボボックスを理解・操作できるように実装されています。 (※ 今回紹介するコンボボックスの実装はWAI-ARIA 1.1に基づくものになっています。現在の最新仕様であるWAI-ARIA 1.2は仕様が異なっていてそちらの使用が推奨されています。) role属性 に combobox を持つ要素をinput要素とlistbox要素の親要素とすることでコンボボックスと識別しています。この要素には名前が必要なので aria-labelledby属性 を使用しています。 input要素に文字の入力をすると入力された文字に対応する入力候補のリストが表示されますが、これは aria-autocomplete属性 に list を指定することにより示しています。 表示されるリストは aria-controls属性 で指定している"mansionNameList"をidに持つul要素です。この要素が表示状態であれば親要素の aria-expanded属性 には"true"が非表示状態であれば"false"が指定されます。 キーボード操作によってリストボックス内でフォーカスされている要素が変更されると aria-activedescendant属性 の値が変更されます。 role属性 要素が示す役割を明確にするためのもの aria-labelledby属性 要素とラベルを関連付けるもの label要素に対するfor属性と同じで関連付けたい要素のid属性を値として指定する aria-autocomplete属性 入力補完のサジェストを提供するためのもの aria-controls属性 指定した要素が値に指定した要素を制御することを示すもの aria-activedescendant属性 現在アクティブな子孫要素を指定するもの aria-expanded属性 要素の開閉の状態を示すもの < span id = "mansionNameLabel" > マンション名 </ span > < div role = "combobox" aria-labelledby = "mansionNameLabel" aria-expanded = "true" > < input type = "text" aria-autocomplete = "list" aria-controls = "mansionNameList" aria-activedescendant = "" > < ul id = "mansionNameList" role = "listbox" > < li role = "option" ></ li > ... </ ul > </ div > 所在地選択ダイアログ 物件の所在地の選択をする際に用いているダイアログでの取り組みも紹介します。 キーボードフォーカス キーボードの操作で所在地が選択できるように tabIndex属性 を指定しています。都道府県の選択後は市区の選択リストにフォーカスが当たるように focus()メソッド を使用しています。 tabIndex属性 Tabキーによるフォーカスの移動順序、および要素がフォーカス可能かどうかを指定するもの focus()メソッド 指定された要素にフォーカスを設定できる場合にフォーカスを設定するもの < dialog id = "prg-addressSelectDialog" open > < div data - target = "addressSelect_city" > < div tabindex = "0" role = "listbox" > ... </ div > </ div > </ dialog > document .getElementById( "prg-addressSelectDialog" ).querySelector( `[data-target="addressSelect_city"] [role="listbox"]` ).focus(); リストボックス また、市区のリストでは選択肢グループ要素を用いていますが、スクリーンリーダーでも理解できるように役割や状態を設定しています。 listbox要素の aria-labelledby属性 にはダイアログのタイトルである"addressSelectCityTitle"を指定しています。こうすることによってリストボックスにフォーカスが当たった際にタイトルが読み上げられます。 aria-activedescendant属性 にはキーボード操作によってリストボックス内でフォーカスされている要素が指定されます。以下のコードでは千代田区がフォーカスされているため、千代田区を持つli要素のidが指定されていて、千代田区を持つli要素では選択中であることを示す aria-selected属性 が"true"となっています。 グループの識別には role属性 に group を指定して、 aria-labelledby属性 でグループラベルを含む要素を参照しています。各選択可能な要素には role属性 に option を指定しています。 aria-selected属性 要素が選択されているかどうかの状態を示すもの < dialog id = "prg-addressSelectDialog" open > < div data - target = "addressSelect_city" > < p id = "addressSelectCityTitle" > 市区を選択 </ p > < div tabindex = "0" role = "listbox" aria-labelledby = "addressSelectCityTitle" aria-activedescendant = "addressSelectCity101" > < ul role = "group" aria-labelledby = "addressSelectCityCate0" > < li role = "presentation" id = "addressSelectCityCate0" > 23区 </ li > < li value = "101" role = "option" id = "addressSelectCity101" aria-selected = "true" > 千代田区 </ li > < li value = "102" role = "option" id = "addressSelectCity102" > 中央区 </ li > ... </ div > </ div > </ dialog > 最後に 売却査定サービスにおけるアクセシビリティ対応の取り組みについて紹介しました。 弊社でのアクセシビリティの取り組みについてはほかの記事でも紹介されていますのでぜひご参照ください。 www.lifull.blog LIFULLではともに成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
検索エンジンチームの加藤宏脩です。 今回は、LIFULLの検索エンジンであるSolrのバージョンアップについて紹介します。 Solrを含むミドルウェアの最新バージョンへのアップデートには多くの工夫と努力が必要です。 この記事では、私たちがLIFULL HOME`Sを支える物件検索エンジンのバージョンアップにどのように取り組んでいるのか、またv9.2.1へのバージョンアップ対応の詳細ついて紹介します 課題: Solrバージョンアップ移行時の課題 Solrのアップデートは単純な作業ではありません。 特に、古いバージョンからの大きなジャンプでは、多くの非互換性や変更点が存在します。以下は、私たちが移行時に遭遇した主要な課題です。 非互換性の問題: 既存の機能や設定が新バージョンで動作しない可能性。 テスト環境の構築: 新バージョンの動作確認に必要なテスト環境のセットアップ。 既存のカスタム機能の移行: LIFULL独自のカスタム機能を新バージョンに適応させる。 パフォーマンスの違い: 新旧バージョンの間でのパフォーマンス差。 新機能の取り込み: どの新機能を採用し、どう利用するかの判断。 影響が不明: 自社で利用している機能が把握できず、影響があるのかが分からない。 継続的にバージョンアップするために普段行っていること LIFULLでのSolr運用は日々の変更調査から始まります。 日々の変更調査 SolrやLuceneの変更内容を追跡し仕様変更や非推奨・廃止機能の調査します。 LIFULLの利用環境への影響を調査します。 調査結果をスプレッドシートにまとめます。 バージョンアップをブロックする可能性があれば、はやめにコミットします。 バージョン毎の調査 新バージョンのリリース毎に変更内容を確認し、動作確認を行います。 LIFULLの検証手順で動作確認をします。 テスト結果や修正項目、留意点をまとめます。 バージョンの選定 変更が多くなると作業量が増えるため、こまめにバージョンアップをします メジャーバージョンアップ等変更が多い場合は、 その一つ前のバージョンに上げるなどして、一つあたりのバージョンアップの作業量を減らすようにします。 バージョンアッププロジェクトで行うことについて バージョンアップは以下のような手順で進められます。 バージョンアッププロジェクトの開始 日々の調査やバージョンの調査結果をもとに、対応内容を確認。 調査計画、デプロイ計画、テスト計画を作成。 システム変更タスクの消化: 新バージョンに対応するための変更タスクを実施。 検証 LIFULLでは、物件検索エンジンの比較、検証用にテストを用意しています。 これがあることにより、 頻繁に物件データの更新がある環境で、データをそろえた2つの環境を用意したり 任意の量のクエリを抽出して性能や結果を比較するような手間のかかるタスクが簡単に行えるようになっています。 性能テスト 実際に本番環境で投げられているクエリを用意して、 新旧バージョンのクラスタにクエリを投げて、そのレスポンス速度を比較します。 回帰テスト 実際に本番環境で投げられているクエリをサンプリングして、 新旧バージョンのクラスタにクエリを投げて、レスポンスの差分を比較します。 データの差分確認テスト 新旧バージョンのクラスタに同じデータを投入して、 全件の出力を比較します。 混沌テスト(カオスエンジニアリング的な観点から行われるテストを略して混沌テストと呼んでいます) SolrのLeaderがfailoverして切り替わるのを確認します。 Zookeeperを落としてクラスタにクエリを投げて、そのレスポンスを確認します。 SolrのインスタンスにOOMを起こさせて、意図した挙動をするのかを確認します OOMは、自社で作ったOOMを起こさせるプラグインを導入してテストしています。 ディスクの使用量テスト Zookeeper、Solrのインスタンスのディスク使用量が新旧で大きく違っていないかを確認します。 Solrの台数を増減させてリクエストを受け付け続けられるかを確認します。 ログ出力テスト サーバー内のログにエラーやワーニングがないかを確認します。 cloudwatch logsに送信できているかを確認します。 デプロイ 問題がなければ、本番環境にデプロイ。 バージョンアップの運用をしたメリット バージョンアップを始めるときのハードルが低くなる バージョンアップが始まった段階で、変更内容やプロジェクトの手順など必要なタスクが分かります。 バージョンアップのPJのテンプレート化をしているので、PJの進め方も分かります。 共通のテスト手順が決まっているので、テストの手順を考える必要がなくなります。(変更内容を見て別途テストの設計はします) 知識が増える 変更調査を毎日やっているため、 メンバーが徐々にSolrの仕様変更に詳しくなります。 気になることをSlackでつぶやくと、誰かが知っているということが何度かありました。 リリースの安心感 検証は、過去の障害事例を元にしたテストも含んでおり そのテストがパスしている状態でのリリースには安心感があります。 Solr v9.2.1へのバージョンアップについて v9.2.1を選んだ理由。 当時最新のSolrバージョンであったことと、v9.x以降ベクトルが利用できるようになるため v9.2.1へのバージョンアップを行いました。 v8.x系の最新バージョンへの変更も考えましたが、 v9.2.1の検証もできており問題ないと判断したため、v9.2.1へのバージョンアップを行うことにしました。 Solrの変更について 調査や検証をする中で、いくつか問題や気を付けることがあったため その点についての紹介をさせていただきます。 ※具体的な変更については、Solrの公式の変更点を参照してください。 https://solr.apache.org/guide/solr/latest/upgrade-notes/major-changes-in-solr-9.html experimentalなAPIの機能廃止 zookeeperがlog4jを廃止して、logbackを使うようになりました logの出力先が変わるため、awslogsなどログの収集元を変更する必要があります。 FastLRUCache廃止に伴いCaffeineCacheへ変更 SolrのbooleanClausesの設定を参照する箇所の変更 節の長さが設定値を超えるようになり、バージョンアップ前まで成功してたクエリが失敗するようになるということが起きます LegacyBM25SimilarityFactoryの廃止に伴いBM25SimilarityFactoryへの変更 フリーワード検索による類似スコアでのブースト計算が変わりソートのための重みが下がります 類似度によるスコアの開きが少なくなるので、fqなど他の条件でのスコア計算の影響を受けやすくなります バージョンアップの効果 高速化 レスポンス速度が向上。 LIFULLの環境では、p99が大幅に改善しました。 CPU使用率の低下 CPU使用率が低下して、さばけるクエリ数が増えたので インスタンス数を3~40%ほど減らせました。 Solr v9というより、Javaのバージョンアップによる影響が大きかったのもあるかもしれません。 新機能の活用 新バージョンに含まれるベクトルを使った検証ができるようになりました。 まとめ 今回は、LIFULLが行っているSolrを継続的にバージョンアップする仕組みと、Solr v9.2.1へのバージョンアップについて紹介しました。 少ない工数かつ、比較的安全にバージョンアップできるようになりました。 また、最新のSolrにバージョンアップしたことで、新機能の活用やパフォーマンスの向上やコストカットなどのメリットも得られました。 ミドルウェアのバージョンアップは疎かにしがちですが、 LIFULLでは上記のような運用を行うことで、物件検索エンジンのSolrを最新バージョンに追随しています。 最後に、 このような効率化をしたいまたは得意なエンジニアの方々、 LIFULL では一緒に働く仲間を募集しています。この記事を読んで LIFULL に興味ができた方は求人情報も御覧ください。 hrmos.co hrmos.co
こんにちは。フロントエンドエンジニアの根本です。 LIFULL HOME'Sのプロダクト開発と、スポーツ関連の新規事業開発に携わっています。 ちょうど1年前のブログ( UXエンジニアとは?新規事業での取り組み - LIFULL Creators Blog )では、UXエンジニアという職種と新規事業開発でどのような取り組みを実践しているか概略を紹介しました。 今回はUXリサーチを取り入れた実際のプロダクト開発について、既存サービス改善と新規サービス開発それぞれに分けて紹介します。 既存サービス改善 インタビューの定期実施 私たちのサービスでは会員の方に常時インタビュー募集をしており定期的にインタビューを実施しています。 インタビューでは下記の共通事項を聞きながら必要があればその時に確認したい点や検討中の施策があればそれに対してヒアリングを行います。 ・サービスを認知した場所と会員登録した理由 ・現在の利用状況と登録前に期待していた事とのギャップ ・実際に使ってみて使いづらい点や要望 ユーザーストーリーマッピング作成 既存サービス改善を行うにあたって、サービス全体のユーザーストーリーマッピングを整理し直しました。 整理結果をもとに不足している機能群を洗い出し、優先度の重みづけをし重点的に対応すべき機能をピックアップしました。 また、会員には様々なユーザー属性がありその属性毎に必要な機能差分があれば色分けし整理します。 施策への落とし込み ユーザーストーリーマッピングを整理し見えてきた優先度高の機能について、インタビュー結果をもとにユーザー行動の洗い出しと発話を整理し改善のアイディアを洗い出します。 このように定期的にインタビューを実施することで、既存機能の改善をする際のインプットとして活用できPDCAを高速に回せることを実感しています。 新サービス開発 元々指導者向けにスポーツの練習メニューを提供するサービスを開発運営していましたが、今回選手向けの新規サービスの企画を進めてきました。 コンセプトテスト1回目 新規サービスを開発するにあたり、課題仮説とそれに対するコンセプトを用意し選手の保護者11名を対象にユーザーインタビューしました。 今回のイタンビューでは課題共感とコンセプト及びソリューションへのマッチ度を五段階で評価して頂き、自分達の検討している新規サービスがユーザーに受け入れられるのか検証しました。 選手年齢・競技経験・競技意欲・練習頻度など様々な基本情報をヒアリングすることで、今回の初期顧客となるペルソナ像の肉付け作業も合わせて実施しました。 初期顧客へのコンセプトテスト2回目 コンセプトテスト1回目から見えてきた初期顧客ユーザーへ追加インタビューを実施し、利用前UX・利用中UX・利用後UXを聞くことでユーザー体験設計時のインプット材料を整理しました。 【利用前UX】 ・サービスを知ってから導入するとしたらどんな流れで導入するのか ・こういったサービスや教材はどこで認知することが多いか ・認知した時に、どんなプロセスで検討するか ・導入する時の決め手は何か(期待値は何か) 【利用中UX】 ・全体感としてどのような利用の仕方をするか・いつ利用するか(曜日や時間帯など) ・誰が利用するか(本人、保護者、両方) ・利用するデバイスは何か(スマホ、PC、タブレット) ・どこで練習するか(家の中、家の前、公園など) ・練習する時にネックになりそうなことはあるか(端末など) 【利用後UX】 ・保護者として導入してよかったと価値を感じる要素は何か ・選手が価値を感じる要素は何か ワイヤーフレーム作成 プロダクトのワイヤーフレーム作成は自分が担当していますが、今回UXリサーチに時間をかけ様々なユーザーの声を聞いたことで、一つ一つの機能がなぜ必要なのか不要なのか言語化することが容易になりました。 本実装&リリース 本実装を終え、今年8月に無事プロダクトリリースを致しました。 UXリサーチを通常よりも念入りに実施したこともありリリースまでの期間は少し時間を要してしまいましたが、リリース後1ヶ月を迎えユーザー登録数も順調に推移しており、想定ペルソナのユーザーに利用いただいています。 また次のフェーズとしては、会員に対してインタビューを継続的に実施し実際の使い勝手について追加リサーチしていく予定です。 最後に LIFULL HOME'Sのプロダクト開発においても、UXリサーチは積極的に取り入れられており、リサーチ内容によって様々な検証が実施されています。 この1年様々なプロジェクトのUXリサーチに参画させていただく中で、改めてユーザーの行動や意識を理解することで、より良い体験を生み出せることを実感しました。 また、実際に自分のサービスの向こう側にいるユーザーと対話することでユーザーのためにもっと良いプロダクトにしなくてはという使命感も感じます。 これからも日常的にUXリサーチを取り入れながらプロダクトに愛を持って開発を続けていきたいと思います。 LIFULLでは共に成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは!LIFULL HOME’S iOSアプリエンジニアの山川・佐藤です。 今回は、2023年の9月1日(金) 〜 9月3日(日)の3日間で開催された、iOSに関連した技術をコアテーマにしたテックカンファレンス「iOSDC Japan 2023」に参加してきました。この記事では、3日間で行われたセッションの中から我々の印象に残ったセッションやイベントの様子について振り返ります。 iOSDC Japan 2023について iOSDC Japanは2016年に初開催され、今年で8回目となるiOSエンジニア向けのテックカンファレンスになります。 iOSDC Japan 2023の公式サイトはこちら https://iosdc.jp/2023/ 本イベントのテーマは「コミュニケーション」であるということで、オフラインの会場ではスポンサー企業ブースでさまざまな企業の方と交流したり、セッションで登壇した人に質問したり議論ができる場が用意されています。今回からはポスターセッションも始まりました。 そして、コロナ禍中は様々な行動制限があってできなかったことが多かったそうですが、今年は「フルスペックなiOSDCが帰ってきた」ということで、ビールが飲み放題な懇親会も復活しました!🍺笑 このように、学べて楽しめる要素が盛りだくさんなイベントです! 印象に残ったセッション Appleにおけるプライバシーの全容を把握する Appleが近年力を入れているプライバシーに関して、Appleが考えるプライバシーの基本原則や開発者が対応しなければいけないことを一通りおさらいするといった内容でした。 例えば2021年には、ユーザの許可を得ずにユーザの行動を追跡することを禁止するポリシー「App Tracking Transparency(ATT)」が制定されました。これによって、アプリはユーザの行動を追跡するには予めユーザに取得する旨を伝えた上でトラッキングの許可をもらう必要が出てきました。また、WWDC2023ではXcode15が発表され、Privacy Manifestsが導入されます。この対応が2024年春までに必須になります。必要な対応の要点の一つにRequired reaspon APIというものがあり、アプリで利用するのに理由が必要なAPIがあればMainifestに設定しておかなければいけません。 このAPIにはUser DefaultsのAPIも含まれており、これを利用していないアプリはほとんどないだろうということで、弊社を含めた多くの企業で対応が求められそうです。早めのキャッチアップと対応が求められるので、随時関連情報は見ていきたいですね。(山川) speakerdeck.com 法改正を乗り越えるiOSアプリのリリース戦略 運営中のサービスにダイレクトな影響のある法改正が施行されることになった際のリリース戦略について、株式会社LUUPでの対応事例の紹介でした。 2023年7月1日に電動キックボードの走行ルールが大きく変わりました。法改正施行日が決まっている中で、対応項目の洗い出しや、ユーザーに新しい交通ルールを理解してもらう方法の検討、極力サービスを止めないためのスケジュール管理など、怒涛の半年間であったことが伝わってきました。 計画的に準備を進めた結果、法改正施行当日にリリース作業を行う必要もなく、見守るのみだったというお話が非常に印象的でした。 不動産業界でも、法律はもちろんのこと、公正取引委員会の表示規約が毎年改正されます。これに合わせて弊社のサービスでも対応が必要になってくるため、非常に参考になりました。(佐藤) speakerdeck.com SwiftUI + KMM 開発で見えたそれぞれの長所と短所 Kotlin Multiplatformを導入することでiOS / Androidのビジネスロジックを共通化し、UIレイヤーの実装のみを各OSに委ねるという開発方法が登場しました。 ※最近、タイトルにもある「KMM」という呼び方はKotlinを開発するJetBrains社より、公式に「KMP」であるとされたため、以降はKMPで統一します。 📣 Update on the name of Kotlin Multiplatform From now on, “Kotlin Multiplatform” (KMP) is the preferred term when referring to the Kotlin technology for sharing code, regardless of the combination of platforms being discussed. We are deprecating the “Kotlin Multiplatform… — Kotlin by JetBrains (@kotlin) 2023年7月31日 iOS側でKMPを導入する際にはいくつか課題があるとのことでした。その一つとして、KMPはObjective-Cのモジュールを生成するため、いくつかのSwiftの機能は利用できません。そのため、KMP側で複数のクラスを持つ階層構造のものを定義したとしても、Swiftで見ると深い階層は浅い階層のクラスのオブジェクトにまとめられてしまったりするそうです。とはいえ、他にも癖のある部分もあるものの、総合的に見ると開発体験は悪くないそうでした。 実は弊社でもKMPを導入してサービス開発し始めていたので興味深い内容だったため、貴重な事例を知れて非常に参考になりました。(山川) speakerdeck.com Human Interface Guidelinesから読み解く標準アプリの素晴らしい体験 AppleのHuman Interface Guidelines(以下、HIG)を読んだことはあるでしょうか?2023年6月に公式の日本語版も公開されましたが、全てを読むのはなかなか大変ですよね。そこで、既存のApple標準アプリからHIGの理解を深めてみようという内容でした。 例えば、設定アプリは項目が多いにも関わらず、一貫したパターンやグループ分けによって整理されています。常に前の画面に戻れるようになっていたり、UIがテキストサイズの変更に対応していたりと、様々な観点での工夫が施されています。標準アプリには、Appleの知見が凝縮されています。特に標準アプリのアップデートされた箇所は、新たな発見につながるため積極的に触っていきたいですね! エンジニアだけでなく職種の壁を超えて、HIGを読んだり、標準アプリを研究しながら、サービスにとっての最適なUIを考えていきたいと思いました。(佐藤) speakerdeck.com こういうのは標準APIでいいよね サービス内で利用している外部のライブラリ数が多かったので、不要なライブラリを削減してその結果どうなるのかを説明したセッションでした。そもそも利用されていない・利用用途が大きすぎる・標準APIで充分担保できるといった理由から、元は44あったライブラリを15にまで削減されていました。例えば非同期処理でPromiseKitやRxSwiftを使っている箇所も、近年だとCombineやSwift Concurrencyの標準APIで簡単に実装できるようになってきています。 ライブラリの棚卸しをすることでメンテナンス性が向上するだけでなく、ビルド速度やアプリのサイズも改善される副次的効果もあるので、継続的に外部ライブラリと標準の機能との比較をしてリファクタリングしていく視点を持つことは事業インパクトには繋がりづらく、地道な対応ではあるものの重要なのだと改めて感じました。(山川) speakerdeck.com SwiftUIに適した新アーキテクチャの導入に挑む SwiftUIを導入するためのリアーキテクチャの検討から導入までの軌跡と、新しいアーキテクチャ「SVVS」の紹介でした。 リアーキテクチャを実施するにあたり、技術的負債の洗い出し・課題とサービス戦略にあったアーキテクチャの検討・リアークテクチャの進め方の考察の3段階の準備が行われていました。この対応はiOSチームのメンバーで行い、共通認識を持つことを意識して進めていったそうです。 採用された「SVVS」は、Store、View、ViewStoreの3つのコンポーネントで形成されます。依存関係は、ViewからViewStateへの依存、ViewStateからStoreへの依存の一方向と非常にシンプルな構成です。データの流れもシンプルになり、データ不整合の発生を防止することも期待できます。 既知のアークテクチャにこだわらず、チーム内での課題や今後の運用のしやすさを考慮して新しいアーキテクチャを生み出す姿勢を見習っていきたいですね。(佐藤) speakerdeck.com イベントの雰囲気 トーク会場は4部屋あり、自由に行き来ができるようになっていました。 今回はオンラインでも生配信されており、なんと中にはオフラインで参戦しつつも別室のセッションを聴く強者までいました! トーク会場の様子 Day1、 Day2に行われたLTでは、会場でサイリウムが配られ、それを振りながら応援していたのでライブのような雰囲気で楽しかったです。 LTはサイリウムをみんなでフリフリ! スポンサーブースでは、各企業のiOSアプリや事業の紹介のほか、iOSエンジニアならではのソースコードレビューやUIKitとSwiftUIの利用状況に関するアンケート調査などを実施していました。それらの結果を見ながらワイワイ議論できるもの、オフラインならではの醍醐味ですね⭐️ また、スポンサー企業様から技術書やバスタオル、ルービックキューブ、知恵の輪、中濃ソース!?など、個性豊かなノベルティをたくさんいただきました。 スポンサー企業ブース さらに、会場には軽食やドリンクも充実しており、その周りでは登壇者の方々に質問したり、ポスターセッションに参加したりと、コミュニケーションが活発に行われていました。 ロゴ入りのお菓子も食べ放題! X(旧:Twitter)でも楽しそうな投稿をしている方がちらほら見受けられました🤩 とても楽しく、勉強になり、本当によいカンファレンスでした😊 ありがとうございます!来年も楽しみにしています #iosdc pic.twitter.com/eMXC2fH3UC — こばやしよしのり🍎iOSエンジニア転職・オリジナルアプリ開発スクール運営 (@yoshiii514) 2023年9月3日 高まってきたな…! #iosdc pic.twitter.com/Hxpirq2H6X — Roku🐉 (@66nylon_y) 2023年9月3日 #iosdc スポンサーstmn社のTUNAG iOSアプリはVIPER使ってるようでVIPER研究読本がおすすめとおっしゃってましたー。感謝ぁ! pic.twitter.com/4XKvhYLk0B — y.imajo (@yimajo) 2023年9月3日 まとめ たくさんのセッションを聴講して、各企業の取り組みや技術的なトレンドを肌で感じることができました。iOSアプリ開発は従来UIKitを用いた開発がメインでしたが、どの企業でもSwiftUIへの移行を開始したり、SwiftUIでゼロから開発を始める事例が多く、イベントを通してiOSの主流な採用技術も大きく変わってきていることを特に強く感じました。弊社iOSアプリチームでも今後SwiftUIがメインとなる実装がどんどん入ってくることが見込まれるため、今回得られた知見をもとに弊社でのアプリ開発に活かしていきたいです。 最後に、LIFULLでは一緒に働いていただける仲間を募集しています。単にサービス開発をするだけでなく、自分たちの知見を深め・スキルを伸ばす機会がたくさんある職場です。カジュアル面談も実施していますので、もし興味があればそちらもご覧ください。 ※記事執筆時点ではアプリのエンジニアも募集していますよ😊 hrmos.co hrmos.co
プロダクトエンジニアリング部の二宮です。 私たちは「 強い個人・最高のチームになることで、価値創造を加速させ続ける 」というビジョンを掲げ、LIFULLのサービスをプロダクト開発でリードできる強い組織を目指しています。そして「最高のチーム」を目指すための取り組みをいくつか行なっています。 その活動の一環として、ベトナムの開発拠点の LFTV(LIFULL Tech Vietnam) とのハンガーフライト(雑談会)を開催しました。この記事では、その開催の背景と目的についてご紹介いたします。 ハンガーフライトについて LFTVハンガーフライトの準備段階 当日の様子と振り返り まとめと展望 ハンガーフライトについて LIFULLのエンジニア組織では、ここ数年間コミュニケーション活性化のための草の根施策としてハンガーフライト(雑談会)を行なっています。以前このブログでも「 リモートワーク下でどうやって偶発的なコミュニケーションを生み出すか: Discordを使ったコミュニケーション(ハンガーフライト編) 」で紹介した通り、次のような問題意識から始まりました。 コロナ禍の影響でリモートワークが中心になり、自部署以外のメンバーと話す機会が激減したため、ちょっとでも話す機会を作ろうと思ったのがきっかけで、私が所属する部署でもZOOMを使ってハンガーフライトを始めてみました。毎週決まった時間に自部署以外のメンバー含む数名でおしゃべりをしていました。 コロナ禍が長期化し、リモートワークが常態化しました。そのことによって、会社でも他部署の人と交流する機会が減ったことに対して課題意識が強まってきました。そこで、自分たちの周りでしかやっていなかったハンガーフライトを、エンジニア組織全体でやってみてはどうだろうと思い立ち、実施することにしました。 この活動は今ではエンジニア組織の文化として定着しており、現在は数人のメンバーで、次のような運営をしています。 色々な人に参加してもらえるように企画をしっかり練り、みんなが参加したくなるようなゲストを招待する。 無理の無いペース(月1回)でコンスタントに開催する。 参加者からチャットや口頭で質問やコメントを拾って、できる限り双方向コミュニケーションにする。 形式は座談会形式や質問形式など様々。 気軽に参加できるように聞き専もOK。 そして、7月の会ではLIFULLのベトナムの開発拠点の LFTV(LIFULL Tech Vietnam) の方々をゲストにお迎えしました。この企画の背景として、「 LFTVおよびその他海外拠点との協力開発の推進 」がLIFULLの開発組織のテーマの一環として掲げられており、実際に以前より業務連携が増えています。また、以前は単なる「案件の委託先」のような捉え方もされがちだったのですが、徐々にプロダクトチームの一員として参加するようなスタンスにシフトしようとしています。 先に結果を述べると、多くのメンバーが参加した盛況な会となり、いくつか反省点はあったものの、また同じテーマで開催したい良い会になったと思います。 LFTVハンガーフライトの準備段階 今回は普段に比べてどんな会になるのか想像できなかったため、準備をしっかり行いました。具体的には次のような形でやってみることにしました。 特に日本との関わりの強く、日本語にも詳しいブリッジエンジニアの方々を呼ぶ。 他の方々には「来てくれたら嬉しい」と伝える。 そこでベトナムの開発文化・生活の様子を聞ける会にする。 日本とベトナムでそれぞれ相手への事前質問を用意し、交互にその話題を振る形でファシリテーションする。 LFTVの社長に就任された加藤さんがハンガーフライトの運営メンバーの中にいたこともあり、意外に両方の国での広報や連絡で苦しむことは少なかったです。少し先回りした話ですが、「現地のエンジニアに楽しんでもらえる内容になるか不安なので、まずは日本側にベトナムに親しみを持ってもらおう」という方向性だったのですが、この不安は的中せず、実際にはブリッジエンジニア以外の方々にも積極的に参加して頂けました。 準備したスライド 当日の様子と振り返り 結果として、日本とベトナムが合わせて57人も参加する過去最大の参加人数の会になりました。ベトナム側の開発者はおよそ70名で、かなりの割合の方にこの会に参加してもらえました。当初の目論見通り、ベトナムや日本の文化の話題が中心となる会になりました。 アンケート結果などを踏まえ、後日の振り返りでは次の点が良かったこととして挙げられました。 参加人数も多く、アンケートでは満足度が高かった。 ベトナム側で、事前にGoogle Meetsのエクステンションで英語の字幕が出るようなものを共有してくれていた。 日本の運営側は「Google Meetsの公式機能には無い」程度しか確認できていなかった。 日本語で会話したため、日本語の堪能なブリッジエンジニアエンジニア以外に伝わるか心配だったが、ある程度は理解できていたらしい。 参加者の自発的なサポートがありがたかった(後述)。 より改善できそうな点としては次のようなものがありました。 スムーズな進行のためには英語の対応がもう少し必要だった。スライドに英語の記載があるとよかった。 Googleアカウントの権限の違いのため、LFTVのメンバーがGoogle Calendarの登録ができなかった。 暗黙の前提が違って、LFTVメンバーが質問の意図にピンと来ていなかったものもあった。 自発的なサポートで「コメント欄で発言してもらって、それを翻訳して拾う」という形になったが、明示的にその形にするとスムーズに会話が進みそうだった。 当日参加できなかった人にも録画を共有するつもりだったが、Google Meetsの録画をうっかり忘れてしまった。 当日は予想を上回るベトナム側の開発者が集まりました。これは嬉しい反響ですが、当日用意したスライドやアンケートが日本語だけだったり、非日本語話者の考慮漏れが発生したことが反省点の一つになりました。当日、自発的に同時通訳に近いことをしていただけて、かなり助かりました。 内容にピンと来ていなかった質問としては、例えば「今まで参加した中で楽しかったプロジェクトの話を教えてください」というようなものがありました。これは「LFTVでは開発タスク単位で仕事を振られることが多く、(今では変わりつつあるものの)プロジェクト単位で関わることが少なかった」という理由もあったようです。ただ、当日はベテランのエンジニアがこうした事情を補足説明していただいて、それぞれで見えている景色の違いが分かるきっかけになったとも言えるかもしれません。 こうして書くと真面目な話が多かったように思われそうですが、日本のYouTuber(ヒカキンさん)の話になったり、ベトナムオフィス周辺の美味しい料理の話だったり、カジュアルな話題でも活発に盛り上がることが出来ました。 まとめと展望 以上が今回開催したハンガーフライトの報告です。「最高のチーム」を作るためには、それぞれが異なる文化のメンバーとの交流を深めることが重要で、そのための一つの取り組みとして参考にしていただけたら嬉しいです。 「またベトナムの開発拠点との交流会を行いたい」という声が多かったのも嬉しいです。次回は今回の反省点も活かしてよりスムーズに交流できるように改善を図っていきます。LIFULLにはマレーシアの開発拠点( LFTM )もあり、3ヶ国での交流会も企画したいと思っています。 LIFULLのエンジニア組織では「越境」という言葉がよく使われていて、現実にもより良いチームワークを築くための国や職種を越えた様々な取り組みが行われています。興味を持たれた方はぜひ採用ページもご覧いただければ幸いです。 hrmos.co hrmos.co
こんにちは! LIFULLエンジニアの吉永です。 普段はLIFULL HOME'SのtoC向けCRMチームにてエンジニアリングマネージャをやっています。 本日はチームでGitのコミットメッセージ書式を Conventional Commits に準拠するようにしてから得た知見を紹介したいと思います。 コミットメッセージに書式を導入することでどんなメリットがあるのか?導入前後でどんな変化があったのか?今後の展望についてご興味のある方に参考になれば幸いです。 アジェンダ Conventional Commitsとは? チームで導入するにあたってどんな工夫をしたか? 自身が率先して規約に準拠する 定期的に自身のコミットメッセージを振り返る時間を設けた 実際に運用してみて得た知見を仲間にも共有する時間を設けた 導入してみてどうか? リリースノートの内容を分かりやすく自動生成できるようになった コミットの粒度が個人でばらつきにくくなる レビューをしやすくなった feat系の同じ文脈のコミットはレビュー完了後はまとめた方がよいかも 今後の展望 コードリーディングがしやすくなる(と思っている) コミットtypeやscopeの内訳を集計できるようになる まとめ Conventional Commitsとは? 人間と機械どちらから見ても読みやすい形式のコミットメッセージ規約です。 公式サイトに分かりやすく概要がまとまっていますので、良かったらこちらも参照してください。 www.conventionalcommits.org 規約と言われると、少し躊躇される方もいるかもしれませんがそこまで複雑な規約ではありません。 よって、導入するにあたってのハードルはさほど高くなく、気楽に始められると思います。 基本形は下記の形になります。 <type>[optional scope]: <description> [optional body] [optional footer(s)] optionalの部分は必須ではないので、必要最低限の規約を満たす為には <type> と <description> を記載すれば良いです。 例えばAという新機能を追加したコミットがあるとします。 その時のコミットメッセージは下記のようになります。 feat: A機能を追加 また、上記機能をリリース後一定期間経過後にA機能のバグを修正した場合は下記のようになります。 fix: A機能で数値に変換できない文字列を入力した場合に例外が発生するバグを修正 こんな感じで、人間から見てもそのコミットでどんな変更がコードに加えられたのかが分かりやすい規約になっています。 公式サイト上で定義されている <type> はfixとfeatのみなのですが、私達のチームではこれらに加えてAngularの規約も取り入れて運用しています。 github.com チームで導入するにあたってどんな工夫をしたか? 自身が率先して規約に準拠する まずは導入しようというチームに提案した私自身がきちんと規約を理解し、自身が行うコミットコメントを規約に準拠したものにしました。 定期的に自身のコミットメッセージを振り返る時間を設けた GitHubの検索APIを利用して、過去1週間以内のチームメンバーそれぞれのコミットコメント一覧を週一で開催しているチームの定例時間内で規約に準拠したコメントになっていたかを確認するようにしました。 最低でも1週間に一度は振り返る機会を設けたことで、徐々に規約に従うことが当たり前になっていく効果があったと今は思います。 実際に運用してみて得た知見を仲間にも共有する時間を設けた 得た知見もチームの定例で共有する時間を設けました。 このコミット <type> はrefactorかfixかchoreか分類に少し迷った、 <scope> をどこで切るか少し迷った、など実際に利用し始めて気づく疑問も数多くあったので、様々な事例を共有することで、次回からの指針になったり徐々に効率的になっていくと思っています。 導入してみてどうか? リリースノートの内容を分かりやすく自動生成できるようになった 私達のチームではリリース時に セマンティックバージョニング 形式に準拠したタグをインクリメントして自動生成していました。 約2年前に自前で作成したGitHub Actionsで行っていましたが、メジャー、マイナー、パッチをインクリメントする際のアルゴリズムがいまいちで、全ての場面においてチームメンバー全員が納得できるバージョニングになっていたかと言われると少し怪しい出来でした。 そんな中、チームのメンバーが下記のGitHub Actionsを探してきてくれ、Conventional Commitsに準拠していれば、タグのインクリメントとリリースノートも自動で生成してくれるという、既存のものよりも優れたものでした。 github.com 導入に当たってのハードルも低かったので早速採用して、運用を開始しました。 こちらのGitHub Actionsは個人的にも非常に気に入っており、Conventional Commitsに準拠することで得られる具体的なメリットとして、チーム内外にも共有しました。 コミットの粒度が個人でばらつきにくくなった Conventional Commitsを採用し、オプションである <scope> もなるべく含めようというローカルルールを採用したところ、コミットの粒度が個人でばらつきにくくなりました。 私達のチームのプロダクトではクリーンアーキテクチャを採用しているのですが、 <scope> にクリーンアーキテクチャの各レイヤー名( feat(repository):hoge とか feat(domain):fuga みたいな感じです)を入れて運用してみたところ、おのずと一つのコミットでの変更箇所が狭まったからだと思います。 また、レビューでもらった指摘を修正した後も、今まではついつい下記のようなコミットメッセージにしてしまっていました。 レビュー指摘修正 このコミット履歴は後でコードを読み返す時にはノイズにしかならないので、本来この変更を一緒に含めるべきコミットと統合すべきという意識にConventional Commits採用後は変わりました。 具体的には下記のようなコミット履歴になった場合は、 1 feat: A機能を追加 2 test: A機能のテストコードを追加 3 fix: A機能実装部分に対するレビュー指摘修正 下記のように1のコミットに3のコミットを統合するべきですね。 1 feat: A機能を追加 2 test: A機能のテストコードを追加 このようにコミット履歴をマージ前であれば、本来あるべき履歴へマージ前にリベースするということが自然と浸透しました。 レビューをしやすくなった コミットの粒度がばらつきにくくなったことで、レビューもコミット単位で行いやすくなり、この変更は何で行ったのか?をレビューアが理解しやすくなったと思います。 結果として、レビューがしやすくなったと感じる人が多く、今後も継続していこうと思える一つの要因になっています。 feat系の同じ文脈のコミットはレビュー完了後はまとめた方がよいかも レビューしやすくなったと感じる件と少し関りがあるのですが、私達のチームでは先述したようにクリーンアーキテクチャのレイヤーに沿った <scope> でコミットを分けて運用してました。 先日、Conventional Commitsを採用してから初めて大規模な実装を行い、上記の運用でコミット粒度を分けていたのでレビューは非常にやりやすかったです。 ただし、リリースノートにのるfeat一覧としてみると、正直このリリースで何の機能をリリースしたのか?が細分化されすぎているため、初見では分かりづらかったです。 コミット履歴としては下記のような感じになっていました。 feat(domain): A機能用のエンティティと変換処理を実装 test(domain): A機能用のエンティティの変換処理のテストを実装 feat(repository): A機能用のhogehogeデータを外部から取得する処理を実装 test(repository): A機能用のhogehogeデータを外部から取得する処理のテストを実装 refactor(repository): A機能用に追加した処理と既存処理とで共通化できる個所をまとめる feat(usecase): A機能用のインタラクターを実装 test(usecase): A機能用のインタラクターのテストを実装 feat(controller): A機能用のコントローラーを実装 test(controller): A機能用のコントローラーのテストを実装 コミット上ではコントローラー、リポジトリ、ユースケース、ドメインそれぞれ分けた方がレビューはしやすかったのですが、リリース後しばらくたってからコードリーディングをするときのコミット履歴としては下記が良いのではないか?とチームのメンバーと話していて、意見が出ました。 feat(api): A機能を追加 refactor(repository): A機能用に追加した処理と既存処理とで共通化できる個所をまとめる 結論、feat系はレビュー完了後は同じ機能に関するものであれば一つにまとめ、それら以外のものはそのまま個別のコミットのままで良いかもとなりました。 時間が経過して、後からコードリーディングする際のコミットコメントしても feat(api): A機能を追加 の方が、「このソースはA機能追加時に改修されたものなのか」と理解しやすく、その際にクリーンアーキテクチャのレイヤーの情報はいらないだろうからという理由です。 ※この辺りは、私達の出した結論が必ずしも正解とは限らないと思いますので、ケースバイケースかなと思いますが、一つの知見として参考になれば幸いです。 今後の展望 コードリーディングがしやすくなる(と思っている) Conventional Commitsに準拠することで、コミット粒度や形式がある程度まとまっていることから、時間が経過した後で該当個所のコードは何の作業でどんな理由が合って改修されたのか?をコミット履歴から情報を得やすくなり、結果としてコードリーディングがしやすくなると思われます。 また、自身だけでなく第三者にとっても同様の効果はあると思うので、効果を実感できる時期が来ることを今から楽しみにしています。 コミットtypeやscopeの内訳を集計できるようになる 既に少しだけ運用を開始したのですが、ある期間にチームで行った開発の割合は新機能開発系とリファクタリング系どんなバランスだったか?そのバランスは適切か?などをConventional Commitsに準拠していくことで定量的に集計することができるようになります。 今まではプロジェクト管理ツールへ日々の工数入力で行っていた集計をGitのコミット履歴の観点からも分析できるようになり、チームとしてより正確なステータスを把握しやすくなることを期待しています。 下のグラフは私達のチームの2023/06~2023/07のConventional Commitsの <type> を集計した結果です。 現時点では、この割合が健全な状態なのか?はまだあまり良く分かっていませんが、いずれデータが蓄積されて期間比較できるようになっていくことで、より活用の幅が広がっていくと思います。 まとめ Conventional Commitsについて、採用後しばらく運用してみてチームで得た知見について紹介しました。 チームで導入するには少しハードルが高いなど、それぞれ事情はバラバラかと思いますが、そのような状況の場合に私からお勧めするのは、まずは自分ひとりだけでもいいから準拠してみることだと思います。 私自身、実際に自分で採用してみて得たことをメンバーにも共有したり、Qiitaに個人として得た気づきをアウトプットしたりして理解を深めていったことで、良さに気づけた気がします。 qiita.com まずはスモールスタートでやってみることで得られることもあると思いますので、是非お試しください! 最後に、LIFULLでは共に成長できるような仲間を募っています。 よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
アクセシビリティ推進グループの中島です。 過去同グループの発信した記事の中で、弊社がJavaScriptライブラリとしてStimulusを採用していると何度か紹介させていただきました。 www.lifull.blog 今回はその中で、どんな粒度で、どんな機能のStimulusコントローラを書いているのか少しばかり紹介しようと思います。 (全て書くととても長くなってしまうのでvol.1としてますが、vol.2以降を書くかどうかは今の所わかりません。) button_controller.js 適用例 参考 disclosure_controller.js 適用例 参考 inlay_controller.js 適用例 参考 anchor_sinon_controller.js 適用例 tabs_controller.js 適用例 参考 最後に button_controller.js まずは最頻出のコントローラです。 このコントローラは37signalsから拝借したコントローラの一つです。 button_controller.jsの実装を見る export default class extends Controller { keyboardClick( event ) { if ( [ 'Enter' , ' ' ] .includes( event .key)) { event .preventDefault(); event .stopPropagation(); this .element.click(); } } } このコントローラは要素にボタンとしての機能を簡単に提供するために使っているものです。 ボタンは通常、スペースキーやエンターキーといったキーボード操作でも操作可能であるべきです。 button要素で実装していればこのキーボード操作は標準で提供されるので、こちらがコードを書く必要はないのですが長く運用されたサイトでは往々にしてdivやaタグで実装されたボタンが目立ちます。 そういったものを要素自体を差し替えずに正しくボタンのように振る舞わせるのに役立ちます。 適用例 <!-- before --> < div > click me </ div > <!-- after --> < div # フォーカス可能に tabindex = "0" # 支援技術にボタンであることを教える role = "button" data -controller= "button" # スペースキーとエンターキーをクリックイベントとして処理する data - action = "keydown->button#keyboardClick" > click me </ div > 参考 www.w3.org disclosure_controller.js 次によく使うコントローラはディスクロージャー(開閉UI)を実現するコントローラです。 disclosure_controller.jsの実装を見る export default class extends Controller { static classes = [ 'collapsed' ] ; toggle(evt) { evt?.preventDefault(); if ( this .isExpanded) { this .hide(); return ; } this .show(); } show() { this .control.classList.remove(... this .hiddenClasses); this .element.ariaExpanded = 'true' ; } hide() { this .control.classList.add(... this .hiddenClasses); this .element.ariaExpanded = 'false' ; } get hiddenClasses() { return this .hasCollapsedClass ? this .collapsedClasses : [ '!hidden' ] ; } get isExpanded() { return this .element.ariaExpanded === 'true' ; } get control() { return document .getElementById( this .element.getAttribute( 'aria-controls' ).trim() ); } } 最近ではdetails要素で実現可能なディスクロージャーですが、例によって過去に作られたディスクロージャーはdiv等で作られ、ディスクロージャーとしての機能要件を満たしてないものが多くあります。 ディスクロージャーはトリガーである要素がフォーカス可能で、ボタンとして振る舞い、視覚だけでなく支援技術等でも開閉状態を把握できるように作られるべきです。 このコントローラを使うとそういった部分を担保したディスクロージャーを簡単に実装することができます。 適用例 <!-- before --> < div > < p > ほげほげについて </ p > < div class = "!hidden" > ほげほげはふがふがのことで 一般的にぴよぴよとも 呼ばれています。 </ div > </ div > <!-- after --> < div > < p # フォーカス可能に tabindex = "0" # 支援技術にボタンであることを教える role = "button" # 関連要素のidを指定することで支援技術でジャンプ機能等を提供する aria-controls = "content" # 開閉情報を支援技術に公開する aria-expanded = "false" data -controller= "button disclosure" data - action = "keydown->button#keyboardClick click->disclosure#toggle" > ほげほげについて </ p > < div id = "content" class = "!hidden" > ほげほげはふがふがのことで一般的にぴよぴよとも呼ばれています。 </ div > </ div > 参考 www.w3.org inlay_controller.js 主にウェブで見かけるUIに「もっと見るボタン」を押したら、ボタンは消滅しつつ、隠れた要素が表示されるというものがあります。 このパターンはARIA Authoring Practices Guide等でベストプラクティスが紹介されているわけではないのですが、我々はそういった振る舞いをinlayとよび、対応するコントローラを用意しています。 inlay_controller.jsの実装を見る import { tabbable } from 'tabbable' ; export default class extends Controller { show() { this .nextContent.classList.remove( '!u-hidden' ); this .element.classList.add( '!u-hidden' ); this .firstTabbableItem.focus( { preventScroll: true } ); } get nextContent() { let control = this .element.getAttribute( 'aria-controls' ); return document .getElementById(control); } get firstTabbableItem() { return tabbable( this .nextContent) [ 0 ] || this .nextContent; } } このパターンは押したボタンそのものが消滅することで、フォーカスが失われる(bodyに戻ってしまう)問題に対処するため、表示されたコンテンツ、あるいはそのコンテンツ内の最初のフォーカス可能な要素にフォーカスを移すことが重要と考えています。 そういったことが考慮されてない古いコードをこのコントローラに差し替えることで簡単にキーボード操作の要件を満たすことができるようになります。 適用例 <!-- before --> < ul > < li >< a href = "..." > a </ a ></ li > < li >< a href = "..." > b </ a ></ li > < li >< a href = "..." > c </ a ></ li > </ ul > < div > もっと見る </ div > < ul class = "!hidden" > < li >< a href = "..." > d </ a ></ li > < li >< a href = "..." > e </ a ></ li > </ ul > <!-- after --> < ul > < li >< a href = "..." > a </ a ></ li > < li >< a href = "..." > b </ a ></ li > < li >< a href = "..." > c </ a ></ li > </ ul > < div # フォーカス可能に tabindex = "0" # 支援技術にボタンであることを教える role = "button" # 開閉可能なUIであることを支援技術に公開する aria-expanded = "false" aria-controls = "more-content" data -controller= "button inlay" data - action = "keydown->button#keyboardClick click->inlay#show" > もっと見る </ div > < ul class = "!hidden" id = "more-content" > < li >< a href = "..." > d </ a ></ li > < li >< a href = "..." > e </ a ></ li > </ ul > 参考 accessible-usable.net anchor_sinon_controller.js 主にサイトアナリティクスの文脈で流入元を特定する目的でリンクにパラメータを付与することがよくあります。 ただ、googlebot等のクロールバジェットの消費を抑制する観点で、「パラメータ付与はJavaScriptでクリック時に行ってください」ということが要件に盛り込まれることが稀にあります。 目的は違えどGoogleAnalyticsのLinker機能などが類似の動きをしますね。 そういった時のためにURLの差し替えをさっさと行えるコントローラを用意しています。 anchor-sinon_controller.jsの実装を見る export default class extends Controller { static values = { url: String } ; trick() { this .element.setAttribute( 'href' , this .urlValue); } } 私自身SEOの専門家ではないのでどの程度効果があるのかは詳しく分かってませんが、汎用的なURL差し替えを実現できるようになっています。 適用例 <!-- before --> < a href = "/path/to/page/?from=xxxx" > some page </ a > <!-- after --> < a href = "/path/to/page/" data -controller= "anchor-sinon" data - action = "click->anchor-sinon#trick" data -anchor-sinon- url - value = "/path/to/page/?from=xxxx" > some page </ a > tabs_controller.js 複数のコンテンツをタブ付きインタフェースとして表現するケースがたまにあります。(特にPCサイトに多いような気がします。) 弊社でも家賃相場情報を間取りタイプごとにグルーピングしてタブで切り替えて閲覧できるといった機能があったりしますが、これはそういったUIパターンに適合するコントローラです。 tabs_controller.jsの実装を見る export default class extends Controller { static targets = [ 'tab' , 'tabpanel' ] ; static values = { index: { default : 0, type: Number } } ; select(evt) { let tab = evt.currentTarget; let index = this .tabTargets.indexOf(tab); this .indexValue = index; } selectAt(index) { this .indexValue = index; } next(evt) { evt.preventDefault(); this .indexValue = this .indexValue === this .lastIndex ? this .indexValue : this .indexValue + 1; this .tabTargets [this .indexValue ] ?.focus( { preventScroll: true } ); } prev(evt) { evt.preventDefault(); this .indexValue = this .indexValue === 0 ? 0 : this .indexValue - 1; this .tabTargets [this .indexValue ] ?.focus( { preventScroll: true } ); } first(evt) { evt.preventDefault(); this .indexValue = 0; this .tabTargets [this .indexValue ] ?.focus( { preventScroll: true } ); } last(evt) { evt.preventDefault(); this .indexValue = this .lastIndex; this .tabTargets [this .indexValue ] ?.focus( { preventScroll: true } ); } get lastIndex() { return this .tabTargets.length - 1; } indexValueChanged(current, prev) { let tabs = this .tabTargets; let tabpanels = this .tabpanelTargets; tabs [ prev ] ?.setAttribute( 'aria-selected' , 'false' ); tabs [ prev ] ?.setAttribute( 'tabindex' , '-1' ); tabpanels [ prev ] ?.classList.add( '!hidden' ); tabs [ current ] ?.setAttribute( 'aria-selected' , 'true' ); tabs [ current ] ?.setAttribute( 'tabindex' , '0' ); tabpanels [ current ] ?.classList.remove( '!hidden' ); } } タブ付きインタフェースはタブシーケンス中に一つだけタブストップを持つといった要件や、左右キーでタブ切り替えができる、 Home/Endキーでタブの先頭、末尾切り替えができるといった要件が存在します。 そういったタブナビゲーションをこのコントローラを利用すれば簡単に実現することができます。 適用例 <!-- before --> < h2 > 〜区の家賃相場 </ h2 > < div > < ul > < li > 1人暮らし向け </ li > < li > 2人暮らし向け </ li > < li > ファミリー向け </ li > </ ul > < div > 1人暮らし向け物件の家賃相場 ...円 </ div > < div class = "!hidden" > 2人暮らし向け物件の家賃相場 ...円 </ div > < div class = "!hidden" > ファミリー向け物件の家賃相場 ...円 </ div > </ div > <!-- after --> < h2 id = "tab-label" > 〜区の家賃相場 </ h2 > < div data -controller= "tabs" > < ul role = "tablist" aria-labelledby = "tab-label" > < li id = "tab1" # 初期選択の要素だけタブシーケンスにタブストップを設定 tabindex = "0" role = "tab" # タブが選択中であることを支援技術に公開 aria-selected = "true" data -tabs- target = "tab" data - action = " keydown.left->tabs#prev keydown.right->tabs#next keydown.home->tabs#first keydown.end->tabs#last click->tabs#select" > 1人暮らし向け </ li > < li id = "tab2" tabindex = "-1" role = "tab" aria-selected = "false" data -tabs- target = "tab" data - action = " keydown.left->tabs#prev keydown.right->tabs#next keydown.home->tabs#first keydown.end->tabs#last click->tabs#select" > 2人暮らし向け </ li > < li id = "tab3" tabindex = "-1" role = "tab" aria-selected = "false" data -tabs- target = "tab" data - action = " keydown.left->tabs#prev keydown.right->tabs#next keydown.home->tabs#first keydown.end->tabs#last click->tabs#select" > ファミリー向け </ li > </ ul > < div role = "tabpanel" aria-labelledby = "tab1" data -tabs- target = "tabpanel" > 1人暮らし向け物件の家賃相場 ...円 </ div > < div role = "tabpanel" class = "!hidden" aria-labelledby = "tab2" data -tabs- target = "tabpanel" > 2人暮らし向け物件の家賃相場 ...円 </ div > < div role = "tabpanel" class = "!hidden" aria-labelledby = "tab3" data -tabs- target = "tabpanel" > ファミリー向け物件の家賃相場 ...円 </ div > </ div > 参考 www.w3.org 最後に 今回は記事の物量の関係上、よく使う5つのコントローラに絞って紹介しました。 他にもダイアログ関連のコントローラ、コンテンツの非同期読み込みコントローラ、コンボボックスを実現するコントローラ等、さまざまなコントローラがあるのでvol2があればそこで紹介しようと思います。 最後までお読みいただきありがとうございました。LIFULL では共に働く仲間を募集しています! hrmos.co hrmos.co
こんにちは。プロダクトエンジニアリング部の武井です。 普段はLIFULL HOME'Sの賃貸領域の開発をしています。 現在、LIFULL HOME'Sの賃貸領域ではシステムの基盤刷新を行っています。 詳細については 以前の記事 をご覧ください。 今回はこの基盤刷新に伴い、新たなABテスト実施システムを構築したので、その概要を紹介したいと思います。 LIFULL HOME'S におけるABテスト LIFULL HOME'Sでは、ユーザーにとってより使いやすいポータルサイトを目指し日々UI/UXの改善を行っています。 その効果測定の手段としてABテストを取り入れており、常時いくつものABテストを実施しています。 旧基盤でのABテストのしくみはシステム立ち上げ当初に実装されて以来、現在まで10年以上に渡って使われ続けています。 今回の基盤刷新に伴い、このABテストを実施するしくみを新基盤上に構築し直す必要がありました。 ABテストのシステムは旧基盤と同様に今後10年以上使われ続ける可能性があり、非常に影響度の大きいシステムになることが予想されます。 そのため今後の開発効率を左右する重要な開発プラットフォームを作るという意識で開発に臨みました。 インフラ層での振り分け システムの構築にあたって、A/Bのユーザーをどのように振り分けるかをまず考えました。 最初に出た案は、KubernetesにおけるIstioの加重ルーティングを利用したインフラ層での振り分けです。 IstioはKubernetes上のトラフィック管理などを担うservice meshです。LIFULL HOME'Sの多くのプロダクトはKubernetesの共通基盤上で動作しており、service meshとしてIstioを利用しています。 このIstioのVirtualServiceという機能を使うことでユーザーの振り分けができます。 たとえば、以下のようにmanifestを記述すると v2 のバージョンに 25% 、 v1 のバージョンに 75% のユーザーをそれぞれ流すことができます。 apiVersion : networking.istio.io/v1alpha3 kind : VirtualService metadata : name : reviews-route spec : hosts : - reviews.prod.svc.cluster.local http : - route : - destination : host : reviews.prod.svc.cluster.local subset : v2 weight : 25 - destination : host : reviews.prod.svc.cluster.local subset : v1 weight : 75 Istio VirtualService しかしこの方法では、複数のテストを同時に並行して実施する際、運用上の問題が予想されました。 たとえばテスト1とテスト2を同時並行すると、A/Bそれぞれの掛け合わせで以下の4種類のバージョンのPodが必要になります。 テスト1A * テスト2A テスト1A * テスト2B テスト1B * テスト2A テスト1B * テスト2B このようにテストの同時実施数を増やしていくと、O(2 N )で用意するべきPodの種類が増えていき、管理が非常に困難になることが予想されます。 さらに一つのテストについて振り分けのパターンがA/B/C...と3パターン以上になることもあり、こうなるとより複雑性が増します。 このような運用上の懸念から、Istioを用いたインフラ層での振り分けは断念しました。 アプリケーション層での振り分け 他にも複数の案を検討した結果、インフラ層での振り分けは行わず同一のバージョンのPodでリクエストを受けた後、アプリケーションの中でA/Bのユーザーを振り分けコードレベルで処理を分岐させる案に落ち着きました。 詳細は割愛しますが、コードのイメージは以下のようになります。 システム側で設定ファイルとユーザー情報の突き合わせを行い、A/Bテストの情報が集約された ab というオブジェクトを生成します。このオブジェクトが持つ isB() のようなメソッドを用いてコード上でパターン分岐を行う形になります。 if ( ab. get( TEST_1_ID ) .isB ()) { // テスト1でのBパターンの処理 ... return } // テスト1でのAパターンの処理 ... この場合、アプリケーションのバージョン自体は一つで済みますが、if文で分岐を行う必要があります。複数のテストを同時並行するとより分岐条件が増え、コードの複雑性が高くなりますが、この複雑性を現時点では許容することにしました。 ABテストを実施する以上、必ずどこかでユーザーを振り分けるための複雑性を引き受ける必要があります。この複雑性をインフラ層という離れた場所ではなく、アプリケーションのコードという我々が普段開発していて変更しやすい場所に持ってきたことは暫定的に良い選択だったと感じています。 使いやすいインタフェースの追求 新システムを構築する上で、このシステムを利用する際の開発者体験や使いやすさを意識してインタフェースをデザインしました。 以下は開発したシステムにおける設定ファイルの一例です。 このように設定を定義すると、アクセスしてきたユーザーを自動的に振り分けるシステムになっています。 タイトルや資料へのリンクなども含めて宣言的に定義することで一目してテスト内容を把握できるようにしています。 export const abTestConfig: AbTestConfig = { [ AB_TEST_IDS.sampleProject ] : { title: '【賃貸事業部】サンプルテスト' , specUrl: [ 'https://jira.jp/wiki/XXXXX' , 'https://docs.google.com/spreadsheets/YYYYY' , ] , startDatetime: '2023-08-01 11:00:00' , endDatetime: '2023-08-14 13:30:00' , classification: [ { patternValue: 'a' , weight: 50 , measurementValue: 'sample_test_a' , } , { patternValue: 'b' , weight: 50 , measurementValue: 'sample_test_b' , } , ] , beforePattern: 'a' , afterPattern: 'b' , } , } ; この設定はTypeScriptのObjectとして定義し、型を当てるようにしています。 また、この設定ファイルに対するlinterを実装し、細かい設定内容もチェックしています。 これらのチェック機構によって、PullRequest作成時点で設定漏れや間違いに気付けるようになっており、誤った設定でデプロイしてしまうことを防げます。 このほかにも、利用開始方法や注意点などを記載した詳細なドキュメントを作成したり、テスト実施状況を可視化する簡単なダッシュボードを作成したりしました。 開発工数の削減 このシステムを構築するにあたって、旧基盤の単純な模倣ではなく少しでも開発工数を削減することも意識しました。 なぜなら今回構築するシステムは今後長い間使われる可能性があり、わずかな違いでもレバレッジが効き、結果的に大きな効果をもたらすためです。 旧基盤でのABテスト実施システムを分析したところ、振り分け部分と分析部分が連動していないことが改善点として挙がりました。振り分け部分とはユーザーをA/Bのように振り分ける部分、分析部分は振り分けた後のユーザーがコンバージョンに至ったかなどの分析用メトリクスを送信する部分を指します。旧基盤ではこれらが連動しておらず、テストを実施するたびにメトリクスを送信する処理を書く必要がありました。 そこで新システムでは設定ファイルに分析用の項目 measurementValue を設け、これを読み取って自動的にメトリクスを送信する設計としました。 export const abTestConfig: AbTestConfig = { [ AB_TEST_IDS.sampleProject ] : { title: '【賃貸事業部】サンプルテスト' , ... classification: [ { patternValue: 'a' , weight: 50 , measurementValue: 'sample_test_a' , // この値を自動的に送信する } , { patternValue: 'b' , weight: 50 , measurementValue: 'sample_test_b' , // Bパターンのときはこちら } , ] , ... } , } ; この改善によって、実装や確認の工数削減、実装漏れの防止などにつながります。 まとめ 普段はユーザーが直接触る機能の開発が中心となるため、今回のように社内の開発者に向けたシステムの開発は新鮮で貴重な経験ができたと感じています。最近ではこのABテストの実運用が始まっており、前よりも使いやすい!という声をいただいています。 このように、LIFULL ではユーザーや社内の開発者など多様なステークホルダーの体験を考えたものづくりを行い、ともに成長していける仲間を募集しています。よろしければこちらのページもご覧ください。 hrmos.co hrmos.co
こんにちは、AI戦略室の清田です。 2023年3月に岐阜で開催された DEIM 2023 に続き、6月に熊本で開催された人工知能学会全国大会(JSAI 2023)に参加いたしました。 www.ai-gakkai.or.jp 今年は、恒例の「不動産とAI」をテーマとした企画セッションにも関わりましたので、その内容も合わせて報告します。 生成AIブームがAI研究コミュニティにもたらした影響 今回のJSAI 2023は、過去最高となる3,566名もの参加者があり、大盛況でした。 2022年に相次いで登場した生成AI技術を用いたサービス(Stable Diffusionなどの絵画生成、ChatGPTなどの自然言語生成)の影響で、AI研究が改めて大きな注目を集めていることを感じます。 ディープラーニングの登場などを受けて10年前ほどから始まった 第3次AIブーム は、ある程度の落ち着きを見せていましたが、「冬の時代」を迎える前に、第4次ブームが始まったのかもしれません。 生成AI技術は、大きな期待を集めている一方、著作権の問題、情報の信頼性の問題など、さまざまなリスクが指摘されてもいます。 私がオブザーバーとして関わっている 人工知能学会 倫理委員会 でも、マスメディアからの取材依頼などが多数入っている状況を受けて議論を行い、「人工知能学会としての大規模生成モデルに対してのメッセージ」を公表しました。 www.ai-gakkai.or.jp 本大会では、「日本は生成AIを起爆剤にできるのか」をテーマとした緊急企画セッション、「アートにおいても敗北しつつある人間 〜人の美意識もAIにハックされるのか?〜」と題した倫理委員会主催セッションなどが実施され、多くの参加者を集めていました。 その様子は、NHKなどのメディアでも報道され、生成AIへの社会からの関心の高さを実感しました。 www3.nhk.or.jp 生成AI技術をめぐっては、大規模言語モデルなどの技術面だけでなく、倫理や法律、収益の配分、人の創造性など、さまざまな課題が入り交じっていて、正しい理解に基づいた議論がなかなか成立しづらいように思います。 正しい理解に基づいた生産的な議論を成り立たせる上で、学会というコミュニティの役割に、大きな期待が寄せられています。 こうした役割を果たす上で、生成AIの開発に関わる各企業がより主体的に関わることが求められているように感じます。 企画セッション「少子高齢化と「住まい」産業のDXを考える」 LIFULLでは、名古屋で開催されたJSAI 2017から、継続的に「不動産とAI」をテーマとしたセッションを、同じ関心を持つ大学や企業の方々と共同で開催しています。 今回は、「少子高齢化と「住まい」産業のDXを考える」と題した企画セッションを開催しました。 少⼦⾼齢化の進展に伴う「住まい」の課題に取り組まれている方々をお招きし、さまざまな事例を共有いただきました。 不動産、AIにかかわる方々だけでなく、これからの時代に求められている新たな「住まい」の価値を追求されている方や、人々の行動データをさまざまな社会課題の解決に活用する取り組みをされている方も交えた議論を行うことで、「不動産とAI」の研究領域をさらに広げることを目指しました。 sites.google.com LIDAR測量、簡易BIMによるリノベ・維持管理へのデータ活用 スターツ社の清水哲志様・城戸祐一様からは、既存建築物のリノベーションにかかる膨大な手間を、スマートフォンに搭載されたLiDAR(ライダー)機能を活用して作成された簡易BIMモデルを用いて削減する取り組みなどを発表いただきました。 (国土交通省の「令和4年度住宅生産技術イノベーション促進事業」として、LIFULLも開発に協力しています) xtech.nikkei.com リノベーション事業の最前線の話題 福岡を中心に築古ビルのリノベーションを多数手がけられているスペースRデザイン代表の吉原勝己様からは、「共感価値」によって、築数十年の築古ビルの資産価値を向上するという興味深い事例を発表いただきました。 www.reizensou.com www.space-r.net これらの事例は、本セッションに参加されていたAI研究者の方々からも驚きをもって受け止められました。 地⽅⾃治体における⾼齢社会デザインへのビッグデータ活⽤ソリューション事例 ヤフー社でビッグデータを活用したデータソリューション事業を担当されている大屋誠様からは、 DS.INSIGHT を用いた分析事例をご発表いただきました。 パネルディスカッション 皆さまからのご発表を受けて、以下のようなテーマで多岐にわたる議論を行いました。 既存ストックの価値を高めるためにAI技術が果たせる役割 2025年問題にどのように向き合うべきか 九州をフィールドとした協働の可能性 今回の議論は非常に刺激的で、お互いに接点の少なかった領域の人々どうしが対話を重ねることが、「住まい」産業の形を大きく変革するきっかけになるのではという大きな可能性を感じました。 来年に浜松で開催予定のJSAI 2024でも、引き続き「不動産とAI」をテーマとしたセッションを企画予定です。 多くの方々のご参加をお待ちしております! おわりに LIFULLでは、生成AI技術を活用したプロダクトの開発を加速するため、専門部署として ジェネレーティブAIプロダクト開発室を2023年5月に設置しました 。 www.lifull.blog AI戦略室も引き続き研究開発に取り組んでおり、今後はジェネレーティブAIプロダクト開発室とともに連携して生成AIの活用に取り組んでいく予定です。 ChatGPTを活用した「 AIホームズくん LINE版 」、ChatGPTプラグインなど、国内不動産ポータルとして初めてとなるサービスを続々とリリースしています。 lifull.com lifull.com 生成AI技術は強力な技術であるがために、ユーザーにとって多くの利便性をもたらす一方、使い方によってはユーザーの利益を損ねるリスクもあります。 AI戦略室では、『創造と革進で喜びを届ける』というチームビジョンを掲げ、AI技術シーズの創出と活用を通じて、多様な社会課題や事業課題の解決に挑戦しています。 今回のJSAI 2023への参加を通じて得られた生成AI技術をめぐるさまざまな課題を踏まえつつ、社会課題や事業課題の解決につながる活用のあり方を考えてまいりたいと思います。 AI戦略室では、共に成長しながら働く仲間を募集しています。ご興味をお持ちの方のご応募をお待ちしております! hrmos.co