TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

969

この記事は KINTOテクノロジーズアドベントカレンダー2024 の9日目の記事です🎅🎄 弊社KINTOテクノロジーズは、300人を超えるエンジニア中心の組織です。複雑化する事業環境の中で、私たちは常に組織の効率性と創造性のバランスを模索し続けています。拠点の分散やリモートワークの増加により、部門を超えたコミュニケーションは限定的なものとなり、この課題に真剣に向き合う必要がありました。 この記事では日々のコミュニケーションで利用しているSlackを軸にどのように組織を活性化するのか、そしてSlackをベースにタレントサーチを構築してどのようなことを実現しようとしているのか、その取組の第一歩をご紹介すべく技術広報グループの中西が書いています。 タレントサーチとは社員のスキル情報をデータ化して検索できる仕組みのことです なぜタレントサーチが必要だったのか 効率的に業務をこなすことは大切ですが、イノベーションを生み出すためには、偶然の出会いや「無駄」に思える会話が実は重要な役割を果たします。私たちの組織では、日々の業務に集中するあまり、「暗黙知」や「潜在的な可能性」を見落としがちでした。 例えば、「このスキルを持つ人はいないだろうか」と思っても、誰に相談すればいいか分からない。組織の規模が大きくなるにつれ、こうした情報の非対称性は深刻な課題となっていました。そこで私たちは、Slackプロフィールを戦略的に活用し、この課題に正面から取り組むことにしたのです。 Slackプロフィール活用のメリット 技術広報グループの視点 これまで目立たなかった人材の発見 組織内には、その才能や可能性に気づかれていない人材が多く存在します。技術広報グループのメンバーは日々社内の皆さんとコミュニケーションを取っておりますが、それでも全ての社員の皆さんを深く知ることは難しく、Slackプロフィールは、そうした「隠れた人材」を可視化する新しい手段となっていきます。 プロジェクト支援の迅速化 適切なスキルを持つ人材を素早く特定できることで、プロジェクトの立ち上げや課題解決のスピードが劇的に向上することを期待しています。今までは、〇〇というスキルを持っている人居ないですか?などと社内でも口伝てで探し回ったりしていますが我々のような組織のハブとなる組織を介さずにコミュニケーションが取れるネットワークを構築していくことは今後の組織の成長にとってとても重要なことです 部署間のコラボレーション促進 これまで技術広報グループでは、社内の勉強会や交流イベント、社外講師をお招きしての勉強会など、様々な企画を実施し、それまで接点のなかった部署間でのつながりを作り続けることで社内でも自然にコミュニケーションが発生し、一度繋がったところから数珠つなぎにネットワークが構築され、日々新たなコラボレーションが生まれています。今回のSlack施策もそれに拍車をかけていくことでしょう 全社員にとってのメリット キャリア成長の機会拡大 自分のスキルや興味を明確に表現することで、これまで気づかなかった新たな可能性が開かれます。自分では思わぬキーワードでつながることで、草の根で様々な機会が生まれてきます。これは単に業務に限らず、共通の悩みを持つ方々がお互いに学んで成長する機会が生まれてきます。 スキルを持つ同僚への素早いアクセス 具体的には、新入社員が「Next.jsに詳しいフロントエンドエンジニア」を探す際、Slack上ですぐに簡単に検索できるようになり、学習や課題解決における大きな助けとなっていきます。Slack上でメッセージを検索する延長線上に社内のタレントデータベースが構築されて検索できるようになります。 自然な社内交流の活性化 社内には様々な趣味の草の根活動も存在しています。それが趣味と呼べるかどうかは別にして興味領域ごとに、腰の健康に関するチャンネルから簡単に作れるレシピをシェアするチャンネル。各種スポーツやゲーム、もちろん新しい技術に関するチャンネルもありますし、これらに個人がより紐づけやすくなってきて、業務以外でのつながりがあることで、業務で発生した緊急時の対応でもスムーズに執り行えるということもあります。 プロフィール作成をサポートする仕組み 「何を書けばいいか分からない」という声に応えるため、技術広報グループが積極的にサポートしています。このアプローチは、単なる情報収集ではなく、社員一人ひとりの可能性を引き出すための丁寧な対話プロセスです。 弊社はテックブログを開始した当初より社員の皆さんの才能を見つけ出せるようにインタビューを実施させていただいたり、伴走しながら記事の執筆や登壇資料の作成、イベント企画や運営、勉強会の運営サポートなど行っています。今回のプロフィール作成に関しても、この記事を読んでいる社員の方でまだプロフィールを埋めていないという方や、何を書いたらよいかわからないという方はぜひお声がけください。一緒にあなたの魅力を見つけて社内で発信していきましょう! サポート内容: 個別ヒアリングによる経験や興味の引き出し 1対1の対話を通じて、本人も気づいていない潜在的な強みを探ります。 プロフィール作成用のテンプレート 自己表現が苦手な方でも、安心して記入できます。 自己表現が苦手な方への言語化支援 専門スタッフが寄り添いながら、自分の強みや興味を適切に表現する手伝いをします。 テンプレート: 検索結果 今後の展望 現在は手動でのタレントサーチですが、将来的にはAIを活用したスキルマッチングシステムの構築を目指しています。蓄積されたデータを効果的に活用し、より効率的で戦略的な人材活用の実現を視野に入れています。将来的に個々の社員の可能性をさらに深く理解し、最適な機会と結びつけることができるでしょう。 おわりに Slackプロフィールは、単なる自己紹介欄ではありません。それは、人と人とを結びつける組織の潜在能力を引き出すための戦略的なツールであり、一人ひとりの可能性を解放する鍵なのです。 あなたの興味、スキル、可能性を積極的に発信することで、組織全体の可能性を広げることができます。私たちは、この小さな一歩が、やがて大きな変革につながると信じています。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の8日目の記事です🎅🎄 こんにちは、学びの道の駅チームのHOKAです。 学びの道の駅が発足されてからそろそろ1年が経過するので、ちょっとした振り返りBlogを書いてみます。 一年前に考えていたこと 学びの道の駅チームはちょうど1年前の2023年11月末に、きんちゃん、中西、HOKAの3人が集まって「このたくさんの勉強会をもっと盛り上げていきたい」というところからスタートしました。 そこで、私たちは2024年の年明けすぐに集まり、インセプションデッキを作りました。 その結果がこちら↓ 社内の「勉強会」と「勉強会」が交わる「道の駅」として、勉強会を軸にした社内活性を支援します。 社内広報活動 今度、こういうテーマで勉強会やるよ! 気になるあの勉強会、どんな感じなんだろう? 勉強会の支援 勉強会を始めてみたいけど、どうやって始めると良いのか? 勉強会の運営しているけど、盛り上がらない… などの、お悩み相談 詳しくは「 はじまりのテックブログ 」に記載しております。 はじめの6か月でやったこと まず、月に一度開催されるKTCの全社MTG(通称:本部会)で学びの道の駅の活動を毎月報告しました。 おそらく、下記のスライドは社内でお馴染みになったかと思います。 活動サマリー 勉強会全般の相談​:勉強会を始めたい!相談したい!等「勉強会に関するご相談」を承る場​ 突撃!​となりの勉強会:事務局が「みなさんの勉強会」を見学する活動​ KTC Podcast​:勉強会の運営者にインタビュー!音声で空気感ごと社内にお届け​ TechBlog​:TechBlogで「KTCにこんな勉強会あるよ、参加したよ」を伝えます​ 新たな活動 ーその1ー 6か月を過ぎた頃から、私たちの活動に賛同してくれた方が現れ始めました。 「どんな勉強会があるかを検索できるようにしたい」という課題を抱えていたのですが、モバイルアプリ開発グループのエンジニアがSlackを活用した検索システムを作ってくれました。 これにより、勉強会検索方法は、Slackチャンネルでキャラクターまなびぃにメンションすると、勉強会を見つけられることになったのです。 詳しくは開発者のBlogをご覧ください https://blog.kinto-technologies.com/posts/2024-12-04_manabyi/ 新たな活動 ーその2ー 「勉強会の資料や動画を誰でもいつでも閲覧できる状態にしたい」という課題もありました。 そんな中、コーポレートITグループのエンジニアが自ら名乗り出て、Sharepointを活用した勉強会のポータルサイトを作ってくれました。 その名も「学びの道の駅 Portal」 無味乾燥だったフォルダが、まるでYouTubeのようになりました。 動画や資料がそろったことにより、 「当日参加できなくなってしまった勉強会、資料だけでも見ておこう」 「先日の勉強会を復習したいな、動画を見るか」 といったムーブメントができるようになりました。 その他、うれしかったこと 勉強会を運営中の方から相談の依頼が来るようになったこと。 Podcastの出演依頼をすると、皆さん快諾してくれること。 全社イベントのポスターに「学びの道の駅」という言葉が使われていたこと。 グループ会社の社内向け資料に、KTCを紹介する文脈で「学びの道の駅」が紹介されたこと。 まだ1年も経っていないのに、社内だけでなくグループ会社まで私たちの活動が届くようになるとは、1年前の自分たちでは想像さえしておりませんでした。 ふとした時に、社員の方から「道の駅、めっちゃ良いね!」と言ってもらえるのも大きな励みとなっています。 技術広報グループにジョイン そんな学びの道の駅チームですが、2024年9月から技術広報グループの傘下に入ることになりました。 技術広報グループとは 2022年にKINTOテクノロジーズ TechBlogを立ち上げ、社外に向けたイベントや、社内の勉強会の開催などエンジニアがアウトプットをする場づくりをしてきました。そもそも業務で成果を出すこともアウトプットですが、勉強会やTechBlogもエンジニアにとっては重要なアウトプットの場だと私たちは考えております。技術広報グループでは「業務と業務の間」とも言えるアウトプットの場づくりをしてきました。 2023年末に私たちの始めた「学びの道の駅」は、「業務と業務の間」に必要とされる学びの場=インプットの場を作っており、技術広報グループと非常に近い存在だったのです。 ※そもそも技術広報グループ発起人の中西は、「インプットからアウトプットまで下支えすることでエンジニアの成長を促す」という構想だったので、別々のものではなかったのかもしれません。 今後の学びの道の駅チームは ぜひ中西によるTechBlogをご覧いただければ幸いです。 https://blog.kinto-technologies.com/posts/2024-12-03-the-next-goal/
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の8日目の記事です🎅🎄 はじめに こんにちは。モバイルアプリ開発グループの中本です。 普段は、大阪にいながら東京のメンバーと協力して、 KINTO Unlimitedアプリ のiOS開発をしています。 本記事では、KINTO Unlimitedアプリ(iOS)のアーキテクチャ改善の過程について詳しく説明します。 このアプリのアーキテクチャは、1st → 2nd → 3rdと段階的に進化し、最終的には独自のアーキテクチャへと移行しました。 それぞれの段階における設計や課題について、以下でお話しします。 1st Generation Architecture VIPERアーキテクチャを採用 すべての画面をUIKit + xib/storyboardで実装 Combineを使用してViewを更新 ファーストリリースに向けて納期が短かったこともあり、社内で実績のあったアーキテクチャを採用 1stの設計 flowchart TD id1(ViewController) -- publish --> id2(ViewModel) -- subscribe --> id1 id2 -- request --> id3(Interactor) id1 -- apply view property --> id4(UIView) id1 -- transition --> id5(Router) ViewController ViewModelにイベントを通知 ViewModelからのイベントに基づいてアウトプットを購読 購読結果に応じてViewを更新し、Routerを呼び出して画面遷移を実施 ViewModel Combineを使用してリアクティブに状態を変化 イベントPublisherを変換し、Viewの状態をPublisher経由でアウトプット Interactor APIや内部DBにリクエストを実施 Router 他の画面への遷移処理を実施 UIView コード/xib/storyboardを使用してレイアウト 1stの課題 UIKitを使用したレイアウトは開発コストが高く、特にxib/storyboardを使用した場合は変更が容易ではない → SwiftUIへ移行したい! 2nd Generation Architecture UIKitからSwiftUIへ移行 UIKitによるレイアウトをSwiftUIに置き換え、開発効率を改善 UIHostingControllerを使用してSwiftUIのViewをViewControllerに注入 画面遷移は従来通りUIKitで実施 当時、SwiftUIの画面遷移APIは不安定だったためUIKitのまま SwiftUIへの移行に専念する 一度にたくさん変更すると、機能仕様のデグレが懸念されるため 2ndの設計 flowchart TD id1(ViewController) -- input --> id2(ViewModel) -- output --> id1 id2 -- request --> id6(Interactor) id1 -- mapping --> id3(ScreenModel) -- publish --> id1 id3 -- publish --> id4(View) -- publish --> id3 id1 -- transit --> id5(Router) ViewController HostingControllerInjectableプロトコルを実装し、SwiftUI Viewを追加 ViewModelのアウトプットを購読し、ScreenModel(ObservableObject)に反映 ViewModelのアウトプットやScreenModelのPublisherを購読し、Routerを用いて画面遷移を実施 ScreenModel Viewの状態を保持するObservableObject ViewModel / Interactor / Router 1st Generationと同様の機能 2ndの課題 状態管理がViewModelとScreenModelの両方で行われるため、ロジックが分散し、開発・保守コストが増加 1stからの課題 Combineによるリアクティブな状態変化の実装は保守性に懸念があり、コード量が多く可読性に難がある 1画面につき1つのViewModelであるため、機能の多い画面ではViewModelが巨大化 → CombineやViewModelから脱却したい! 3rd Generation Architecture Combineを用いたViewModelから状態を集中管理するViewStoreを中心としたアーキテクチャへ移行 イベントの結果をAnyPublisherを経ずに直接ObservableObjectに反映できる仕組みを実現 Combineを使用せず、async/awaitを用いてリアクティブな状態変更を実現 状態管理ロジックを機能ごとに分割可能 3rdの設計 flowchart TD subgraph ViewStore id1(ActionHandler) -- update --> id2(State) end id2 -- bind --> id5(View) -- publish action --> id1 id1 -- publish routing --> id3(ViewController) -- publish action --> id1 id3 -- transit --> id4(Router) id1 -- request --> id6(Interactor) ViewStore State Viewの状態を保持するObservableObjectで、SwiftUIのViewで使用 Action 従来のViewModelのtransformメソッドにおけるINPUTに相当する機能を提供するenum ActionHandler Actionを引数にとり、Stateを更新するハンドラー async/awaitを使用して実装 ViewController routerSubjectを購読し、Routerを用いて画面遷移を実施 Interactor / Router 2nd Generationと同様 ActionHandlerの分割 機能の多い画面では、ActionHandlerとStateを分割することで、コードの可読性や保守性を向上させることができる StateのactionPublisherを他のStateにバインドすることで、あるViewから他のViewにアクションを送ることが可能 flowchart TD subgraph ViewStore id2 -- action --> id1 id1(ActionHandler1) -- update --> id2(State1) id5 -- action --> id4 id4(ActionHandler2) -- update --> id5(State2) id8 -- action --> id7 id7(ActionHandler3) -- update --> id8(State3) end subgraph Parent View id3 id6 id9 end id2 -- bind --> id3(View1) id5 -- bind --> id6(View2) id8 -- bind --> id9(View3) おわりに 今回の取り組みは、機能開発と並行して1年以上かけて進めてきました。 現在では、ほぼすべてのソースコードが3rd Generation Architectureに置き換わっています。 その結果、コードの可読性や保守性が向上し、今後の開発が非常にやりやすくなったと感じています。 引き続き、さらなる改善を重ねていければと思います!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の12日目の記事です🎅🎄 はじめに こんにちは。KINTOテクノロジーズ モバイル開発グループ プロデューサーチームの やまゆき です。  弊社では生成AIの使い方を学ぶ機会が、たくさん提供されています。  私はエンジニアではありませんが、今回、生成AIでビジネスサイド向けの運用ツール「お知らせHTML作成ツール」を開発してみました。 この話をすると、よく「え!?生成AIを組み込んだの!?」と勘違いをされるのですが、 そうではなく「生成AIを道具として使って開発した」というのがこの記事の内容です。  非エンジニアでも、自分で開発して業務を改善できる!そのことを実感したので、事例としてご紹介したいと思います。 背景:やろうと思った理由 私の担当アプリでは、PUSH通知をタップすると「お知らせの詳細ページ」が開きます。このページはWeb Viewになっており、HTMLファイルをWebにアップロードすることでお知らせを表示できます。 そのため、次のようなステップで運用されていました。 (1)ビジネスサイドから、表示したい内容をExcel・Word形式で入稿 (2)開発サイドの誰かがコーディングしてHTMLファイルを作成 (3)ビジネスサイドでHTMLファイルをチェック (4)開発サイドの誰かが修正 [場合によって(3)(4)をくり返し] (5)Web反映  入稿は、週に1〜2回のペース。 入稿元のビジネスサイドでは、入稿時に手元で完成形を確認できないため、何度も修正依頼をせざるを得ないという課題がありました。また、開発サイドとしても、修正を含め ”ちょっとしたコーディング” が頻発することで業務が圧迫され、スムーズな運用とは言い難い状況でした。  これらの課題を解決したい!ただ、CMS導入など大掛かりなことはできない・・・。 そこで、生成AIを使って今回開発した運用ツール「お知らせHTML作成ツール」を作るに至りました。  - 解決したかった課題(まとめ) 入稿時に手元で完成形を確認できるようにする(=修正を無くす) HTMLコーディング作業を無くす(=業務圧迫の改善) - 「Excel・WordのHTMLファイル出力」や「入稿時に生成AIでHTMLファイル化」ではダメだった理由 これらの方法でもHTMLファイルを作成できますが、ここで必要とされるHTMLファイルは単純な文章だけでなく、アプリ内のボタンやYouTubeの埋め込み動画、デザインが施されたトピックエリアなど、複雑な内容を含んでいました。また、Webの知識がない担当者でも手元で完成形を確認できるように、一発で高い品質のHTMLファイルを作成する必要があったため、これらの方法では運用が難しいと判断しました。 手順1:必要最小限の機能を定義する まずやったことは、必要最小限の機能は何か?を書き出すことです。あれやこれや理想を言い出すとキリがないものですが、 非エンジニアの自分が一人で開発するのですから、ツール開発を夢で終わらせないためMVP開発を目指すことにしました。 - 私が書き出した必要最小限の機能 (1)フォーム形式で入力した内容を、HTMLファイルとして出力できる (2)必要な入力欄は「タイトル・日付・見出しetc…」 (3)入力欄はユーザーの任意で、いくつでも追加・削除できる (4)出力するHTMLファイルには、指定のデザインを反映する (5)出力するHTMLファイルには、指定の計測パラメーターを反映する 手順2:プロンプトを書く 手順1で書き出した必要最小限の機能をもとに、以下のようなプロンプトを書いて生成AIへ指示を出しました。 私はCopilotを使いましたが、ChatGPT・Geminiなど、何を使っても問題ないと思います。 - 私が書いたプロンプト ユーザーが入力した"お知らせ"の内容を、 HTMLファイルとしてダウンロードできるWEBページを作成してください。 # 必要な入力欄 タイトル 日付 見出し サブ見出し 段落 画像 ボタン Youtube埋め込みタグ # 指示 (1)フォーム形式で入力した内容を、HTMLファイルとして出力できる (2)必要な入力欄は上記を参照 (3)入力欄はユーザーの任意で、いくつでも追加・削除できる (4)出力するHTMLファイルには、指定のデザインを反映する (5)出力するHTMLファイルには、指定の計測パラメーターを反映する # 規定のデザイン(CSS) https://〜 # 計測パラメーター 〜〜〜 手順3:調整する 手順2のプロンプトを生成AIへ入力すると、初手としては十分すぎるアウトプットが返ってくるはずです。それを元に、何だかちょっと違うな?あと少しこうだったら、という部分を生成AIと会話しながら調整します。 例 先ほどのフォームを、以下の指示を元に改善してください。 # 指示 画像の入力欄はURLではなく、その場でアップロードできるようにしてください。 難しい手順は踏まず、誰でも画像を反映できる状態が望ましいです。 - 調整する時のポイント 調整時は、改善点を1つずつ指示して進めることをおすすめします。例えば、「画像は〜」「ボタンの動作は〜」「入力欄は〜」と複数の改善点を一気に伝えてしまうと、生成が失敗した際に、どこで躓いたか分かりにくくなるためです。また、同じ理由で、生成されたコードやプロンプトの履歴は、バージョンを付けて残しておくことをおすすめします。 会話をしながら、少しづつ調整していきましょう! - 粘り強く調整すれば、想像以上のものを生成できる! 私の場合、この調整手順で必要最小限の機能を実現したのはもちろんですが、最終的には以下のような機能まで生成し、組み込むことに成功しました。 入力内容をプレビューする 入力欄の順番を後から入れ替えられるようにする 過去に出力したHTMLファイルを読み込んで編集する 画像サイズが極端に大きい場合にエラーを出し、リサイズ方法を案内する - デザインを調整して仕上げる 最後に見栄えがよくなるようCSSを調整しました。CSSは調べたり、これもまた生成AIに聞いたりして調整が可能ですが、面倒な方は以下のようなプロンプトで生成AIに指示してもいいかもしれません。 例 一般的な入力フォームとして使いやすいデザインとは、どのようなデザインですか? それでは、それらのデザインを先ほどのフォームに反映してください。 - 完成例 こちらが、実際に私が生成AIを使って開発した運用ツールです! https://www.youtube.com/watch?v=F-eyKyS8HSo さいごに この運用ツール開発によって、当初あった5ステップを2ステップに削減することに成功しました。 (1)ビジネスサイドから、表示したい内容をExcel・Word形式で入稿 (2)開発サイドの誰かがコーディングしてHTMLファイルを作成 (3)ビジネスサイドでHTMLファイルをチェック (4)開発サイドの誰かが修正 (場合によって(3)(4)をくり返し) (5)Web反映 ↓ (1)ビジネスサイドから、表示したい内容をHTMLファイルで入稿 (2)Web反映 また、ビジネスサイドからは入稿が楽になったと嬉しい声をもらうことができ、ビジネスサイドも開発サイドも嬉しい、WIN-WINな業務改善となりました! 私個人としては、同じプロジェクトのバックエンドエンジニアに「昔ならこれと同じものを人の手で開発するのに、2週間ほどの時間とお金がもらえたのに…」と嘆かれたことが印象的でした。エンジニアではない私が、それだけの価値あるものを一人で開発できたことに驚いています。 生成AIには怖さすら感じることがありますが、怖がっていても何もならないので、たくさん活用したい!そう強く思い直すきっかけになりました。 何事も同じですが、コツは何度もチャレンジすることだと思います(笑)最後までご覧いただき、ありがとうございました。
アバター
This article is part of day 12 of KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello. Yamayuki here, from the Producer Team in KINTO Technologies’ Mobile Development Group.  Our company offers many opportunities to explore the use generative AI.  While I am not an engineer, I took the initiative to experiment with generative AI to develop an operational tool for business-side staff. This tool simplifies the creation of HTML announcements. When I share this with others, they often get the idea that I pulled off the amazing feat of building generative AI into it, but that is not the case. What I actually did was use generative AI to help develop it. This article is about that process. Even non-engineers can develop solutions to improve work! Having experienced this firsthand, I wanted to share it as a concrete example. Background: Why I Decided to Do It For the apps I am responsible for, tapping a push notification opens the announcement details page. This page is a web view that displays announcements by uploading an HTML file to the web. The operation looks as follows: (1) Business-side staff submit an Excel or Word manuscript of the content they want to display. (2) Someone on the development side codes the HTML file. (3) The business-side staff review the HTML file. (4) Someone on the development side makes corrections (Steps 3 and 4 were repeat in some cases.) (5) The content is reflected on the web.  Manuscripts were submitted at a pace of approximately two per week. A challenge with this process was that the business-side staff submitting the manuscripts did not have the final version ready for review, leading to repeated requests for corrections. Additionally, the frequent need for "a bit of coding" to handle submissions and corrections became a significant burden for the development team as well. Overall, the process was far from efficient or smooth.  We wanted to solve these issues! But at the same time, we were unable to do any drastic changes like introducing a CMS. That is when I decided to leverage generative AI to develop the operational tool I want to discuss in this article: a tool designed to create HTML for announcements.  - Issues we wanted to solve (Summary) Allow manuscript submitters to have the finalized version available for review during submission (eliminating the need for corrections) and remove the necessity for HTML coding (reducing the workload). - Why neither using Excel/Word’s HTML file output nor relying on generative AI to create HTML files during manuscript submission provided an effective solution While these methods can generate HTML files, the requirements in this case went beyond simple text. The HTML needed to include complex elements such as in-app buttons, embedded YouTube videos, and intricately designed sections for specific topics. Additionally, it was essential to produce a high-quality HTML file in a single step, allowing staff with no web expertise to have the finalized version readily available for review. As a result, I concluded that relying on these methods would complicate the operation. Step 1: Define the Minimum Features Required The first thing I did was write down what the minimum features it would require were. I could have written a virtually never-ending want-list, but since I am a non-engineer, I decided to aim for an MVP to ensure that developing the tool would not end up as just a dream. - The minimum required features I wrote down (1) You can input content via a form and get it output as an HTML file. (2) The required input fields are the title, date, headings, etc. (3) Users can add or delete input fields at will. (4) The outputted HTML file reflects the specified design. (5) The outputted HTML file reflects the specified measurement parameters. Step 2: Write a Prompt Based on the minimum required functions I wrote down in step 1, I wrote the following prompt to give instructions to the generative AI. I used Copilot, but I doubt it really matters which one I had used (notable alternatives being ChatGPT and Gemini). - The prompt I wrote Please take announcement content inputted by the user, and create a web page of it that can be downloaded as an HTML file. # Required input fields Title Date Headings Subheadings Paragraphs Images Buttons YouTube embedding tags # Instructions (1) Content inputted via a form can be outputted as an HTML file. (2) See above for the required input fields. (3) Users can add or delete any number of input fields at will. (4) The specified design is reflected in the outputted HTML file. (5) The specified measurement parameters are reflected in the outputted HTML file. # Prescribed design (CSS) https://... # Measurement parameters ... Step 3: Make Adjustments By inputting the prompt from step 2 into the generative AI should produce output that is more than adequate for a first attempt. Based on that, you then adjust the parts that are a slightly off or could use a little tweaking, engaging with the generative AI as you go. Example Please improve the previous form based on the instructions below. # Instructions Make it so that instead of having to enter a URL into the image input fields, you can upload the images right there and then. Preferably, anyone should be able to include images without having to go through a difficult procedure. - The key point when making adjustments When making adjustments, I suggest providing instructions to focus on one improvement at a time. For instance, if you give instructions like, "Images should..., button behavior should..., and input fields should...," you’re asking generative AI to handle multiple improvements at once. If it makes a mistake, it can be challenging to pinpoint where things went wrong. For the same reason, I also recommend maintaining a version-numbered history of the generated code and prompts. Make adjustments gradually, engaging in a step-by-step conversation as you go! things little by little, talking to it as you go! - Through persistent fine-tuning, you can achieve results that surpass your original expectations! In my case, this adjustment process not only allowed me to achieve the minimum required features but also successfully generate and incorporate the following additional ones. Preview the inputted content. Let the order of the input fields be changed later. Import and edit previously outputted HTML files. If an image is excessively large, display an error message and provide instructions on how to resize it - Adjust and polish up the design. Finally, I adjusted the CSS to make it all look nicer. You can adjust the CSS by reviewing the results and providing feedback to the generative AI as you go. However, if that feels cumbersome, you might prefer to provide instructions to the generative AI using a prompt like this: Example What kinds of designs make for easy-to-use general input forms? Okay, please reflect those designs in the previous form. - Finished example Here is the operation tool that I actually developed using generative AI! https://www.youtube.com/watch?v=F-eyKyS8HSo Conclusion Developing this operational tool successfully reduced the initial five-step process to just two. Before: (1) Business-side staff submit an Excel or Word manuscript of the content they want to display. (2) Someone on the development side does codes the HTML file. (3) The business-side staff check the HTML file. (4) Someone on the development side makes corrections (Steps 3 and 4 were often repeated.) (5) The content is reflected on the web. ↓ After: (1) Business-side staff submit an HTML manuscript of the content they want to display. (2) The content is reflected on the web. Additionally, I received positive feedback from the business side, noting that submitting manuscripts had become much easier. Both the business and development teams were pleased, making it a win-win improvement for everyone involved! What particularly stood out to me was when the back-end engineers on the same project pointed out that, in the past, it would have taken about two weeks and a significant amount of money to develop the same functionality manuall. I’m amazed that, as a non-engineer, I was able to independently develop something so valuable. Generative AI can feel intimidating, but letting fear hold me back won’t help, so I’m determined to make the most of it! This experience truly made me reconsider my approach to generative AI in a serious and thoughtful way. This applies to everything, but I believe the key is to keep trying lol. Thank you for reading all the way to the end!
アバター
This article is the entry for day 19 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello, This is Rasel Miah , an iOS Engineer from the Mobile Application Development Group. Today, I’ll introduce an improved approach to updating the UI in SwiftUI using the new @Observable macro introduced in iOS 17. I’ll explain how it works, the problems it solves, and why we should use it. TL;DR Observation provides a robust, type-safe, and performant implementation of the observer design pattern in Swift. This pattern allows an observable object to maintain a list of observers and notify them of specific or general state changes. This has the advantages of not directly coupling objects together and allowing implicit distribution of updates across potential multiple observers. From Apple Documentation In simple terms, Observation is a new and easier way to make a view respond to data changes. Challenges Without Using Observation Before diving into Observation , let me first show you the old method of updating the UI and the challenges associated with it. Let’s start with a simple example. import SwiftUI class User: ObservableObject { @Published var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView: View { @StateObject private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView: View { @ObservedObject var user: User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is \(user.age)") } } } User Conforms to the ObservableObject protocol to enable state observation. Contains a @Published property name to notify views about changes. ParentView @StateObject to manage an instance of the User class. ChildView Accepts a User object as an @ObservedObject . Both the parent and child views will use let _ = print("xxx.body") for debugging purposes to log updates to the view. If you build the project, you’ll see the following output in the debug log. ParentView.body ChildView.body No issues so far, as this is the initial state and both views are rendered. However, if you press the setName button, you’ll see the following output. ParentView.body ChildView.body Both ParentView and ChildView are re-drawn, which is not expected since ParentView didn't use any properties of User . Moreover, ChildView relies on a constant variable that doesn’t change, yet it still gets re-drawn. Even if ChildView only returns a static Text, it will still be re-drawn whenever the User model changes because it holds a reference to the User model. This highlights a significant performance issue. This is where the Observation framework steps in to rescue us. Hello @Observable ! The @Observable macro was introduced at WWDC 2023 as a replacement for ObservableObject and its @Published properties. This macro eliminates the need for explicitly marking properties as published while still enabling SwiftUI views to automatically re-render when changes occur. To migrate from ObservableObject to the @Observable macro, simply mark the class you want to make observable with the new Swift macro, @Observable . Additionally, remove the @Published attribute from all properties. @Observable class User { var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } Note:  Struct does not support the  @Observable  macro. That’s it! Now, the UI will only update when the name property changes. To verify this behavior, modify the views as follows: struct ParentView: View { // BEFORE // @StateObject private var user = User() // AFTER @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView: View { // BEFORE // @ObservedObject var user: User // AFTER @Bindable var user: User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is \(user.age)") } } } Now, we can declare our custom observable model using @State , eliminating the need for @ObservedObject , ObservableObject , @Published , or @EnvironmentObject. @Bindable : A property wrapper type that supports creating bindings to the mutable properties of observable objects. From Apple Documentation After running the code, you’ll see the following output during the initial rendering: ParentView.body ChildView.body If you press the setName button, nothing will appear in the console because ParentView doesn’t need to update as it didn't use any properties of User. The same applies to ChildView . Add Text(user.name) to the ParentView and then build the project. After pressing the setName button, you will see the output: struct ParentView: View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } Text(user.name) // <--- Added ChildView(user: user) } } } // output ParentView.body // change the age from child @Observable class User { var name = "" var age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView: View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView: View { @Bindable var user: User var body: some View { let _ = print("ChildView.body") VStack { Button("Change Age") { user.age = 30 } Text("Age is \(user.age)") } } } // output ChildView.body This indicates that the view is updating correctly without any unnecessary re-rendering. This represents a significant improvement in performance. How does @Observable work? This might seem miraculous. SwiftUI views update without any issues as we simply checked our model with the @Observable macro. But there's more happening behind the scenes. We’ve moved from using the ObservableObject protocol to the Observation.Observable protocol. Additionally, our name & age property is now associated with the @ObservationTracked Macro instead of the @Published Property Wrapper. You can expand the macro to reveal its implementation. The following is the expanded code. @Observable class User { @ObservationTracked var name = "" @ObservationTracked var age = 20 func setName() { name = "KINTO Technologies" } @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar() internal nonisolated func access<Member>( keyPath: KeyPath<User, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal nonisolated func withMutation<Member, MutationResult>( keyPath: KeyPath<User, Member>, _ mutation: () throws -> MutationResult ) rethrows -> MutationResult { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } extension User: Observation.Observable {} By default, an object can observe any property of an observable type that is accessible to the observing object. To prevent a property from being observed, simply attach the @ObservationIgnored macro to that property. @Observable class User { var name = "" @ObservationIgnored var age = 20 func setName() { name = "KINTO Technologies" } } Now, any change to the age property won’t be tracked meaning no UI update will be happened. Performance Analysis Here is the view count report recorded using the Instruments tool. Without Observation With Observation Insights from the Capture Without Observation (First Image) This Instruments session captures the unoptimized performance of a SwiftUI app: Metrics Displayed : View body updates, property changes, and timing summary. View Redraw Count : 9 redraws, including the initial rendering and one redraw for each of the three Set Name button taps. Performance : Total Redraw Duration : 377.71 µs . Average Redraw Time : 41.97 µs per redraw . With Observation (Second Image) This session highlights the optimized rendering achieved with the Observation framework: Metrics Displayed : Same metrics as in the first session, now reflecting improved efficiency. View Redraw Count : 3 redraws, consisting of the initial rendering and only one redraw for state changes, regardless of multiple button taps. Performance : Total Redraw Duration : 235.58 µs . Average Redraw Time : 78.53 µs per redraw . Quantitative Highlights View Redraw Count Reduction : Without Observation: 9 redraws . With Observation: 3 redraws (reduced by 66.67% ). Total Redraw Duration Improvement : Without Observation: 377.71 µs . With Observation: 235.58 µs (reduced by 37.65% ). Redraw Efficiency : Without Observation: More frequent, averaging 41.97 µs . With Observation: Fewer but optimized redraws, averaging 78.53 µs . This comparison illustrates the significant impact of the Observation framework on reducing the number of redraws and improving overall rendering performance, even though the average redraw time per instance is slightly higher due to fewer redraws. Summary In this post, we explored the improvements in SwiftUI with the new @Observable macro introduced in iOS 17. Here’s a quick recap of the key points: Challenges with Previous Approach The old method of using ObservableObject and @Published properties caused unnecessary re-renders, resulting in performance issues, especially when some views did not depend on the changing data. Introducing @Observable This new macro simplifies the state observation process, eliminating the need for @Published , @ObservableObject , and @EnvironmentObject . By marking a class with @Observable , and using @Bindable in views, we can automatically track changes in the data and only trigger view updates when necessary. Performance Improvements The use of @Observable ensures that views are updated only when the relevant data changes, reducing unnecessary re-renders and improving performance. With the @ObservationIgnored macro, developers have more control over which properties should or should not trigger UI updates. Benefits Better performance through more targeted updates. A simplified codebase by removing the need for ObservableObject and @Published. Type-safe management of state changes. More control over which properties trigger view updates. With @Observable , managing UI updates in SwiftUI becomes easier, more efficient, and less error-prone, offering a smoother experience for both developers and end users. That’s all for today. Happy Coding! 👨‍💻 Ref- Observation Discover Observation in SwiftUI, WWDC23
アバター
はじめに こんにちは!コーポレートITG兼技術広報G 学びの道の駅チーム所属の明田です。 普段はコーポレートエンジニアとして、IT機器に関するオン/オフボーディングやグループ内のプロセス/業務改善をしたりしています。 今回はコーポレートエンジニアの自分がひょんなことから「学びの道の駅プロジェクト」に参加し、活発的に開催されている社内勉強会の動画が自発的に集まるポータルをつくった話を紹介しようと思います。 きっかけ:社内勉強会を後で見る手段ってなくないか? 当社、下記のように何度か記事で紹介している通り社内勉強会が頻繁に行われています。 https://blog.kinto-technologies.com/posts/2024-04-23_%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F/ https://blog.kinto-technologies.com/posts/2024-05-21-%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85-iOS%E3%83%81%E3%83%BC%E3%83%A0%E3%81%AE%E5%8B%89%E5%BC%B7%E4%BC%9A%E3%81%AB%E7%AA%81%E6%92%83/ 開催後は社内Slackチャンネルにて録画ファイルが投稿されますが、コーポレートエンジニアとして次のことが社内の課題だと感じていました。 社内にはConfluenceやSharepoint、Boxといったようにドキュメントファイルを共有する場所はあるが、動画コンテンツの置き場が定まっていない。 特に、社内の有益な勉強会の動画ファイルは社内Slackチャンネルにて録画ファイルが投稿されるだけで後から探すことが難しい 全社情報共有チャンネルがメインの投稿場所になっており、他の業務関連情報等も流れてくることから簡単なスクロールだけでは探せない状態だった 勉強会開催後に入社したメンバーはそもそも勉強会があったことも、その動画やファイルがあることを知る術がない これらを解決するにはどうしたらいいだろうか…と考え、思いついたのが動画プラットフォームを構築して勉強会の動画が一堂に集まる場所を作ったらどうか、という案でした。 はじまり:学びの道の駅プロジェクトのメンバーに突撃してみた 勉強会の動画が集まる場所を作る、と思ったときにすぐに思いついたのは学びの道の駅プロジェクトの存在でした。学びの道の駅は「社内の「勉強会」と「勉強会」が交わる「道の駅」として、勉強会を軸にした社内活性を支援」する活動を行っており、自分がやりたいことと活動がバッティングしないか・もし検討しているなら協力して進められるといいな、と勝手な思いを抱きました。 鉄は熱いうちに打て、ということで同じグループかつ学びの道の駅に参加している きんちゃん にまずはその旨を相談したお返事が「いいですね!賛成です!!」とのこと。(めちゃくちゃ嬉しかった記憶があります)その後、コーポレートITGとして本件を進めてOKとなった後に早速他のプロジェクトメンバーにも相談しに行きました。 当時のSlackのやりとりが、学びの道の駅メンバーの熱さが伝わると思うため皆さんに紹介します。 まずは私の相談文章がこちら。 ここで絵文字リアクションがすごいことにお気づきでしょうか?この勢いのままのお返事がこちらです。 話がとっても早い!その後、集まった当日に「明田さんも学びの道の駅に入っちゃえばいいじゃん!」とお誘いをいただき学びの道の駅メンバーとして動画プラットフォームを進めていくことになりました。 本編:動画プラットフォームを構築・社内展開した このブログを読んでくださっている方も、全社MTGやグループ内の勉強会を集めた動画プラットフォームを社内に展開したいという方はいらっしゃると思いますのでここからは実施内容の中身と、その中からいくつかピックアップしてなぜそれを実施したのかを紹介します。 実施内容 動画プラットフォームの構築 社内で使用されていたSharepointサイトを活用し、動画掲載方法等をリニューアル 動画収集方法の決定 社内の公開OKな会議、社内勉強会の動画を収集する 会の主催者に録画ファイルと使用した公開可能なファイルをSharepintサイトのドキュメントにアップロード Sharepointサイトのトップページは「強調表示されたコンテンツ」機能を使用し、勉強会名でフィルター設定する。 一度の設定以降、同じ勉強会名の動画がアップロードされるとトップページに自動で動画が掲載される仕組み 周知、運用 8月末の全社MTGで動画プラットフォーム紹介&動画収集の依頼 Q.なぜSharepointを採用したのか A. 当社がグループウェアでMicrosoft365を採用しているため 動画プラットフォームはYoutubeやVimeo、Brightcove等ありますがこれらは新規に契約をしなければならなかったことや元々動画収集は会の主催者に実施してほしいという要件があったことから慣れ親しんでいるSharepointを採用しました。 Q.なぜ会の主催者自身に動画をアップロードしてもらう方針にしたのか A. 不要な個所の削除などの編集など、主催者自身に行ってほしかった 動画プラットフォームへアップロードすべきかどうかは自主的に判断してもらうのがベストだと考えた ここを第三者である私達学びの道の駅メンバーが作業を代行して請け負うこともできましたが、勉強会開催前のわちゃわちゃした会話やリアルタイムだからこそ伝えられること等を動画にも含むかどうかということと、動画プラットフォームにアップロードすべきかを判断できるのは、勉強会の主催者がいちばん適任だと考えました。 Q.なぜ「強調表示されたコンテンツ」機能を使うことにしたのか A. シンプルに楽だったから アップロードしてもらった動画を見てもらうためにはどうしたらよいか、を考えたときに文字よりも画像表示・何回かクリックした先で動画を見るよりもトップページから興味ある内容を見るほうがリーチ数が高くなると仮定しました。 その後、動画へのアクセス方法をいくつかのパターン分作成して学びの道の駅メンバーに見てもらったときにサムネイル表示ができ、かつ勉強会ごとに動画がまとまっている状態がSharepointサイトのトップページに並んでいる状態がいちばん閲覧者の興味を引くという答えになったことでそれが実現できる「強調されたコンテンツ」機能を活用することになりました。 使用例 参考: https://support.microsoft.com/ja-jp/office/強調表示されたコンテンツの-web-パーツを使用する-e34199b0-ff1a-47fb-8f4d-dbcaed329efd これを使う上での注意ですが、アップロード直後の動画は勉強会名(=動画ファイル名)で行うフィルターに引っかからないことがあります。そのときは少し時間を置いてから、再度試してみてください。 Q.周知は全社MTGの1回だけなのか A. 新入社員向けのオリエンテーションで毎月紹介 毎月ある全社MTGの学びの道の駅&技術広報Gの枠で紹介 冒頭に記載した通り「勉強会開催後に入社したメンバーはそもそも勉強会があったことも、その動画やファイルがあることを知る術がない」という課題を持っていたことから、ここはもちろん対処しました。この2枠あることで、存在感を出すことができています! おわりに:社内の学びをいろいろな方向からこれからも支援します! 動画プラットフォームは全社MTG以降、アクセス数が著しく減少することもなく勉強会動画も収集されており元々感じていた課題を解消する一助になりつつあるのではないかと感じつつここまで、「社内の課題感」「学びの道の駅という強力なサポーターが社内にいる話」と「本編/実際の構築話」の3本をお話しました。 弊社のいいところは、新しいチャレンジに寛容であるところでまさに今回の取り組みはそれを体現できていると感じており、本編にたどりつくまで長く語ってしまいましたが、ここまでこの記事を読んでくださった皆さんにはそれが伝わったのではないかと勝手に思っております。 現在、学びの道の駅は1プロジェクトから技術広報Gの1チームとして活動しています。これからも変わらず「社内の「勉強会」と「勉強会」が交わる「道の駅」として、勉強会を軸にした社内活性を支援」する活動を行い、それらの活動をテックブログ等で紹介していきます。引き続きよろしくお願いします!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の7日目の記事です🎅🎄 はじめに こんにちは! CloudInfrastructure G (Osaka Tech Lab) の井木です。 今回は、Osaka Tech LabのメンバーでMVP開発手法を用いて生成AIを活用した会話APIを作成したことについてい話したいと思います。 ちなみにMVP(Minimum Viable Product)開発とは 最小限の機能を備えたものをまず作成して、ユーザ ( 今回だと発案者のPdM ) のフィードバックを受けながら検証改善を進めていく手法です。 今回のざっくりとした要件に対して非常に効率的な開発手法だと思っています。 作成しようと思ったきっかけ 至極当然な理由です。 KINTOテクノロジーズは、東京、名古屋、大阪 (Osaka Tech Lab) に拠点がありますが、社員の8~9割近く東京に偏っています。 また、拠点ごとで担当を分けているわけではないため、東京のプロジェクトにそれぞれ参加して仕事を行っています。 この状況はよくある状況で、問題があるわけではないですが 大阪メンバーは他の拠点に比べて仲が非常にいいです!(自己申告) そうなると、大阪メンバーだけでやりたいよねとなるのは当たり前ですね。 そのタイミングで、生成AIを活用した会話が成り立つのかを考えているPdMとの出会いは必然です。 テーマ 今回は、生成AIを活用した会話が成り立つのかどうかの仮説をMVP開発で検証することにしました。 会話といってもいろいろあり、接客対応も上司の報告も会話になります。 緊張したかたい会話ではなく、家族、友人と話すような「自然な安心した会話ができるのか」を焦点に充てています。 とりあえずできたもの 全体構成 ※会話にロボットがいたほうがいいとの判断からユカイ工学様の市販ロボット (BOCCO emo) を利用 Azure構成図 ![Azure構成図(簡易版)](/assets/blog/authors/norio_iki/chmcha_azure_architecture.png =700x) 作成するにあたり考慮した点 時間 今回のゴールは、生成AIを活用した会話が成り立つのかをテーマとしましたが 時間と人をかけてできたとしてもそれは、もう生成AIを利用した会話が世の中にあるのでそれはできるでしょうになります。 また今回については、会話という人は簡単にやっているけど実は考えると非常に奥深いテーマとなっており、 やりたいことなどが山ほど出てくるテーマとなっています。そのため時間があればいくらでもできるものです。 MVPに時間をかけて作成してもそれはMVPの価値はないと考え、今回は決めた時間を超えない前提で進めました。 使用した時間 要件検討/MVP作成 2 Day フィードバックを受けながら検証改善 15 hour(Max) 実際に何をやったか 要件検討/MVP作成 今回のMVP開発における環境は決めていなかったのですが、生成AIを活用したシステムをどのように作ればいいかも知見がほとんどない状態でした。 このままだと「自然な安心した会話ができるのか」のテーマを検証する前に生成AIのシステムをどのように作ればいいかからの検証からスタートしなければいけないところでしたが 生成AIのシステムの知見はAzure Light-upのプログラムを提供している ZENARCHITECTS様 の協力を受け、自分たちは今回のテーマに集中できる状態を作り上げています。 ZENARCHITECTS様には、生成AIシステム構築の伴走だけではなく 今回ざっくりとしたテーマから生成AIを利用するために気を付けたほうがいい点なども実経験からのアイデアをいただき、 2日間でMVPを完成することろまで引っ張っていただいております。 フィードバックを受けながら検証改善 実際に利用してみたコメントを受け、開発するメンバーで話し合い改善内容を決定。 現在の場所から会話をする機能を追加したり、カフェばかり話すロボットを直すためプロンプトとか 気が付いたところをフィードバックしてもらうサイクルを1カ月間 (15h) を利用して実施しまいた。 現在の場所から会話する機能の検証には、場所情報を変えながらオフィスで検証するのではなく、本当に移動しながらの検証も実施。 車のサブスクを提供するKINTOならではの、車上でフィードバックを受けながらデプロイを行うなどのリアルタイムでのアップデートも 内製ならできる対応です!(こんな感じで検証改善を繰り返しました!) ![ドライブしながらのデプロイ](/assets/blog/authors/norio_iki/drive_deploy.jpg#right =400x) さいごに 今回作成した生成AIを活用した会話APIについては、現在社内で将来性について検証中となります。 将来かかると想定されるコストに対して、価値が上回ったらさらに開発を進める予定です。 だた、もし時期早々の判断になって継続開発が中止となる場合もあります。 しかし、たとえ継続がなくても今回確認した結果と、新しい分野(生成AIシステムのAzure開発)の経験という成果が残ります。 その成果は、既存システムへのフィードバックにいかせたり、新たなアイデア創出の糧になるものです。 今回は、たとえ継続がなくても失敗ではないと考えます。失敗しないMVP開発を行うにあたり、常に前進して イノベーション・サイクルを回して行ける環境を作っていけると考えます。 今後ともMVPでのチャレンジ自体は進めていきたいと思います! また、もう少し詳しく内容を知りたい方は、ZENARCHITECTS様の事例紹介に記載されておりますのでそちらをご覧くださいませ Azure Light-up
アバター
Introduction Hello. I’m Hiroya (@___TRAsh) from the Mobile App Development Group. At our company, we have several in-house product teams, and many of them use Xcode Cloud. Xcode Cloud is an official CI/CD service provided by Apple that automates iOS app builds and CD (deployment to TestFlight). In this post, I’ll cover how to integrate a private repository as a library in Xcode Cloud. Since there weren’t many references available, I faced some challenges getting it to work and wanted to share the solutions I found. Target Readers This guide is intended for readers with some experience in iOS development, especially in setting up CI/CD for iOS environments. Environment - Using Xcode 15.4 - Managing libraries with SwiftPM - Referencing a private repository in libraries - Deploying to TestFlight with GitHub Actions + Fastlane Objective To shift TestFlight deployments from GitHub Actions + Fastlane to Xcode Cloud. By doing so, we can reduce dependency on Fastlane and minimize the tools required in the app submission process. Additionally, Xcode Cloud allows direct reference to Apple Developer certificates, making it easier to manage the certificates needed for app submission. Challenges Xcode Cloud offers many benefits, but using a private repository as a library requires user authentication. Since Xcode Cloud does not natively support these authentication settings, an extra step is needed to reference a private repository as a library. To achieve this, we can use the ci_scripts/ci_post_clone.sh provided by Xcode Cloud to set up the necessary authentication, allowing access to the private repository. .netrc configuration Since Xcode 12.5, .netrc has been supported as a way to store usernames and passwords,`` which can be automatically accessed during git clone operations. By placing in ~/.netrc , the authentication information is automatically applied. Additionally, for managing the private repository as a GitHub Release, I’ve added api.github.com to the .netric configuration. touch ~/.netrc echo "machine github.com login $GITHUB_USER password $GITHUB_ACCESS_TOKEN" >> ~/.netrc echo "machine api.github.com login $GITHUB_USER password $GITHUB_ACCESS_TOKEN" >> ~/.netrc I set my username and access token as secrets in Xcode Cloud’s environment variables and configured ci_post_clone.sh to reference them. Adding the Repository URL In Xcode Cloud settings within App Store Connect, I added the repository URL of the library under Additional Repositories . Removing Settings with Defaults Delete Even after configuring access to the private repository’s library, I encountered an issue where the library dependencies couldn’t be resolved, resulting in the following error: :::message alert Could not resolve package dependencies: a resolved file is required when automatic dependency resolution is disabled and should be placed at XX/XX/Package.resolved. Running resolver because the following dependencies were added: 'XXXX' ( https://github.com/~~/~~.git ) fatalError ::: This error occurs because, on Xcode Cloud, SwiftPM does not reference Package.resolved and instead attempts to resolve package versions automatically. To fix this issue and allow the build to succeed, I deleted certain Xcode defaults. defaults delete com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile defaults delete com.apple.dt.Xcode IDEDisableAutomaticPackageResolution Although I applied these two settings, I couldn't clearly identify the difference between them... To get more information, I ran the xcodebuild help command locally, where I found some similar settings that could help clarify their roles. $ xcodebuild -help ... -disableAutomaticPackageResolution prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file -onlyUsePackageVersionsFromResolvedFile prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file This prevents SwiftPM from automatically resolving packages to versions other than those recorded in the Package.resolved file. However, both settings seem to do exactly the same thing, as no additional differences were found. I also came across a similar question in a SwiftPM issue thread, and this approach was confirmed to work. So, for now, I believe this setup is sufficient. https://github.com/swiftlang/swift-package-manager/issues/6914 For now, by deleting these two settings, SwiftPM will only refer to Package.resolved for library dependencies and resolve them. Conclusion By configuring .netrc in ci_scripts/ci_post_clone.sh , which Xcode Cloud references before starting, I was able to access the private repository. Additionally, setting the defaults delete ensures that SwiftPM resolves dependencies based solely on Package.resolved, allowing the build to succeed on Xcode Cloud. #!/bin/sh defaults delete com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile defaults delete com.apple.dt.Xcode IDEDisableAutomaticPackageResolution touch ~/.netrc echo "machine github.com login $GITHUB_USER password $GITHUB_ACCESS_TOKEN" >> ~/.netrc echo "machine api.github.com login $GITHUB_USER password $GITHUB_ACCESS_TOKEN" >> ~/.netrc Lastly Fastlane is a great tool that has been around for a long time, but by using Xcode Cloud, the process of submitting an app has been simplified. As mentioned earlier, Xcode Cloud offers numerous benefits, so I encourage you to consider implementing it in your workflow. Appendix https://developer.apple.com/documentation/xcode/writing-custom-build-scripts https://speakerdeck.com/ryunen344/swiftpm-with-kmmwoprivatenagithub-releasedeyun-yong-suru https://qiita.com/tichise/items/87ff3f7c02d33d8c7370 https://github.com/swiftlang/swift-package-manager/issues/6914
アバター
Introduction Hello! My name is Ren.M I work on developing the front end of KINTO ONE (Used Vehicle) . KINTO Technologies Corporation will be serving as the premium sponsor of JSConf JP 2024 , which will be held at the KS Building Kudansakaue in Tokyo on Saturday, November 23, 2024. ■ About JSConf JP 2024 JSConf JP 2024 is a Japanese JavaScript festival organized by the Japan Node.js Association. This will be the 5th JSConf event in Japan. Sponsor Booth Yo can visit our booth and take part in our JavaScript questionnaire! Those who answer the questions can spin the gacha and receive an exclusive novelty gift! Here are pictures of some of them! Paper clips Tote bag Sponsor Workshop In the workshop, we will give a presentation on "Building Vehicle Subscription Services In-House with Next.js and Reflections One Year Later (tentative title)!" Click the link below to learn more! (in Japanese) https://jsconf.jp/2024/talk/kinto-technologies/ We Are Hiring! At KINTO Technologies we’re looking for talented people to join our team! If you’re interested, let’s start with a casual meeting. Even if you're just a bit curious, don't hesitate to apply using the link below! https://hrmos.co/pages/kinto-technologies/jobs/1955878275904303141 Conclusion If you're interested, we'd love for you to visit our booth and join our workshop! We're excited to welcome you to the venue. We will be looking forward to seeing you there!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の6日目の記事です🎅🎄 はじめに こんにちは。KINTO FACTORY開発グループの上原( @penpen_77777 )です。 2024年の7月に入社し、KINTO FACTORYのバックエンドの開発を担当しています。 今回は、業務の中でS3イベントを処理する際に注意すべきだったデータ競合とその解決策についてサンプルコードを通じてご紹介します。 今回想定する読者 AWSのS3イベントが重複して通知されたり、通知の順序が入れ替わることに悩んでいる方 Rust、S3、DynamoDB、Lambda、Terraformについて基本的な知識がある方 サンプルコードを読む際にこの辺りの知識があると理解しやすいです S3イベントの概要 S3イベント^[AWSによるS3イベント通知に関するドキュメント https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EventNotifications.html]とは、S3へのオブジェクトをアップロード、削除などの操作をトリガーに発生するイベントのことです。 S3イベントをLambda関数やSNSなどで検知することによって、S3にまつわる様々な処理を自動化できます。 S3イベントの問題点 S3イベントを処理する上で注意するべきなのは、イベントが重複して通知されたり順序が入れ替わったりすることがあるという点です。 例えば、同一オブジェクトキーに対してオブジェクト削除後にオブジェクト作成する処理を考えてみましょう。 この場合、オブジェクトの削除イベントが先に通知され、その後に作成イベントが通知されることが期待されます(同一オブジェクトに対して削除→作成した場合の期待するS3イベントの受信順序の図を参照) しかし、S3イベントは順序が保証されないため、作成イベントが先に通知され、後に削除イベントが通知されることがあります(同一オブジェクトに対して削除→作成した場合の起こりうるS3イベントの受信順序の図を参照) 結果、オブジェクトを削除するイベントによる処理結果が最新になってしまい、処理内容によってはデータの一貫性が保証されないという問題が発生することがあります。 gantt title 同一オブジェクトに対して削除→作成した場合の期待するS3イベントの受信順序 dateFormat HH:mm:ss axisFormat %H:%M:%S section オブジェクトの削除 オブジェクト削除 :done, cre1, 00:00:01, 1s 削除イベント受信・処理 :done, cre2, 00:00:03, 1s section オブジェクトのアップロード オブジェクトアップロード :done, cre1, 00:00:02, 1s 作成イベント受信・処理 :active, cre2, 00:00:04, 1s gantt title 同一オブジェクトに対して削除→作成した場合の実際に起こりうるS3イベントの受信順序 dateFormat HH:mm:ss axisFormat %H:%M:%S section オブジェクトの削除 オブジェクト削除 :done, cre1, 00:00:01, 1s 削除イベント受信・処理 :active, cre2, 00:00:04, 1s section オブジェクトのアップロード オブジェクトアップロード :done, cre1, 00:00:02, 1s 作成イベント受信・処理 :done, cre2, 00:00:03, 1s この問題の解決策としてS3イベントに含まれるsequencerキーを使ってイベントの順序を保証する方法があります^[S3イベントの構造に関するドキュメント。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html]。 イベントのシーケンスを決定する方法の 1 つとして、sequencer キーがあります。イベントが発生した順序でイベント通知が届く保証はありません。ただし、オブジェクト (PUT) を作成するイベントからの通知 と削除オブジェクトは sequencer を含みます。これは、特定のオブジェクトキーのイベントの順序を決定するために使用できます。 同じオブジェクトキーに対する 2 つのイベント通知の sequencer の文字列を比較すると、sequencer の 16 進値が大きいほうのイベント通知が後に発生したイベントであることがわかります。イベント通知を使用して Amazon S3 オブジェクトの別のデータベースまたはインデックスを維持している場合は、イベント通知を処理するたびに sequencer の値を比較し、保存することを推奨します。 次の点に注意してください。 複数のオブジェクトキーのイベントの順序を決定するために sequencer を使用することはできません。 sequencer の長さが異なる場合があります。これらの値を比較するには、最初に短い方の値を右に 0 と挿入してから、辞書式比較を実行します。 まとめると以下の通りです。 sequencerはオブジェクトのPUTやDELETEイベントに含まれる値で、イベントの順序を決定するために使用可能 sequencerを辞書式比較し、値が大きい方が後に発生したイベント 長さが異なる場合は、短い方の値の右側に0を挿入してから比較 複数のオブジェクト同士のS3イベント順序を決定するために使用できない 同一オブジェクトに対するPUTやDELETEイベントの順序を決定するために使用する 例えば、Rust上でS3イベントのシーケンサ比較を実装する場合は以下のように実装できます。 S3のシーケンサの性質を表現するための構造体 S3Sequencer のフィールドとコンストラクタを定義します。 // 1. 構造体S3Sequencerを定義する use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Sequencer { bucket_name: String, object_key: String, sequencer: String, } // 2. S3Sequencerのコンストラクタを定義する // バケット名、オブジェクトキー、シーケンサを引数に取る impl S3Sequencer { pub fn new(bucket_name: &str, objcet_key: &str, sequencer: &str) -> Self { Self { bucket_name: bucket_name.to_owned(), object_key: objcet_key.to_owned(), sequencer: sequencer.to_owned(), } } } 次に、イベントの前後関係をS3Sequencerの大小を比較することで判別させるため、 PartialOrd トレイト^[数学的に言うと半順序集合を表現できます。 全順序集合と言うのもありそちらは Ord トレイトを実装します。比較方法を実装するためにわざわざトレイトが分けられているのは面白いなと感じます。 https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html https://ja.wikipedia.org/wiki/%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88#%E5%8D%8A%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88]と PartialEq トレイト^[PartialEqトレイトの定義 https://doc.rust-lang.org/std/cmp/trait.PartialEq.html] を実装していきます。 この2つのトレイトを実装すれば、以下のように == や < 、 > などの比較演算子を使ってシーケンサの大小を比較できます。 let seq1 = S3Sequencer::new("bucket1", "object1", "abc123"); let seq2 = S3Sequencer::new("bucket1", "object1", "abc124"); if seq1 < seq2 { println!("seq1はseq2より古いイベントです"); } else if seq1 == seq2 { println!("seq1とseq2は同じイベントです"); } else { println!("seq1はseq2より新しいイベントです"); } PartialOrdトレイトの実装に必要なpartial_cmpメソッドは以下のように実装します。 impl PartialOrd for S3Sequencer { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { // バケット名が異なるシーケンサは比較できない if self.bucket_name != other.bucket_name { return None; } // オブジェクトキーが異なるシーケンサは比較できない if self.object_key != other.object_key { return None; } // 長い方に合わせて、短い方の末尾に0を追加して比較 let max_len = std::cmp::max(self.sequencer.len(), other.sequencer.len()); let self_sequencer = self .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); let other_sequencer = other .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); Some(self_sequencer.cmp(other_sequencer)) } バケット名およびオブジェクトキーが異なるシーケンサの比較に意味はないため、それぞれが異なる場合はearly returnでNoneを返します。 バケット名とオブジェクトキーが同じになっていることを確認できたらシーケンサの比較に入るわけですが、以下の順で処理をしていきます。 シーケンサの長さを比較して、長い方の長さを max_len に格納する max_len に合わせて、短い方のシーケンサの末尾に0を追加する 2で作成したシーケンサを辞書順で比較して、大小を返す PartialEqトレイトは以下のように実装します。 impl PartialEq for S3Sequencer { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) .map_or(false, |o| o == std::cmp::Ordering::Equal) } } PartialOrdトレイトのpartial_cmpメソッドを使って、シーケンサの比較結果が等しいかどうかを判定しています。 以上の実装により、S3イベントのシーケンサを比較できるようになりました。 データ一貫性を考慮したS3イベント処理の実装例 アーキテクチャ図 ここからは、シーケンサを用いてS3イベントの順序を保証する方法をサンプルコードを交えながら紹介します。 サンプルコードではS3バケットにアップロードされた画像ファイルをLambda上でグレイスケールに変換して、出力用のS3バケットに保存する処理を行います。 以下にアーキテクチャ図を示します。 ![サンプルコードのアーキテクチャ図。S3バケットにアップロードされた画像ファイルをLambda上でグレイスケールに変換して、出力用のS3バケットに保存する。DynamoDBを使ってロック処理を実装する。](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) 入力画像用バケットに画像ファイルがアップロードされると、S3イベントを通じてLambda関数がトリガーされます。起動したLambda関数は、DynamoDBを見て処理中でないことを確認し、処理中フラグを立てて画像ファイルを処理します。処理が完了したら、処理中フラグを解除して次の画像ファイルの処理を待ちます。 作成・削除のイベントの通知の順序が逆転してしまうと、本来存在するはずの画像が誤って削除されるといった問題が発生します。 例えば、以下の流れで処理を行うと想定し実装したとします。 画像ファイルAが入力用バケットから削除される 画像ファイルAが入力用バケットに再度アップロードされる Lambdaが削除イベント(1に対応)を受信、画像ファイルAを出力用バケットから削除する Lambdaが作成イベント(2に対応)を受信、画像ファイルAを処理しグレースケールに変換して出力用バケットに保存する gantt title 同一オブジェクトに対して削除→作成した場合の期待するS3イベントの受信順序 dateFormat HH:mm:ss axisFormat %H:%M:%S section オブジェクトの削除 (1) オブジェクト削除 :done, cre1, 00:00:01, 1s (3) イベント受信・オブジェクトを削除 :done, cre2, 00:00:03, 1s section オブジェクトのアップロード (2) オブジェクトアップロード :done, cre1, 00:00:02, 1s (4) イベント受信・グレースケールに変換 :active, cre2, 00:00:04, 1s しかし、S3イベントでは3と4の通知順序が逆転する可能性があるため、以下のような流れになることがあります。 画像ファイルAが入力用バケットから削除される 画像ファイルAが入力用バケットに再度アップロードされる Lambdaが作成イベント(2に対応)を受信、画像ファイルAを処理しグレースケールに変換して出力用バケットに保存する Lambdaが削除イベント(1に対応)を受信、画像ファイルAを出力用バケットから削除する gantt title 同一オブジェクトに対して削除→作成した場合の実際に起こりうるS3イベントの受信順序 dateFormat HH:mm:ss axisFormat %H:%M:%S section オブジェクトの削除 (1) オブジェクト削除 :done, cre1, 00:00:01, 1s (4) イベント受信・オブジェクトを削除 :active, cre2, 00:00:04, 1s section オブジェクトのアップロード (2)オブジェクトアップロード :done, cre1, 00:00:02, 1s (3) イベント受信・グレースケールに変換 :done, cre2, 00:00:03, 1s この場合、入力用バケットには画像ファイルAが存在するにもかかわらず、出力用バケットにはグレースケール化した画像ファイルA'が存在しないという問題が発生します。 このような問題を防ぐために、S3イベントのシーケンサを使って排他処理を実装します。 加えてDynamoDBによって画像処理状況を管理させることも排他処理の実装には必要です。 DynamoDBの条件付き書き込みを使って、処理中フラグを立てることで、複数のLambda関数が同時に同じ画像ファイルを処理することを防ぎます。 今回のサンプルコードはGitHubに公開しています。以下のリンクからご確認ください。 (実行にはAWSインフラの構築が必要ですが、terraformコードにより容易に試せるようにしております) https://github.com/kinto-technologies/techblog-s3-sequencer-example Rustによるサンプルコードの実装 今回はRustを使ってLambda関数を実装します。Lambda関数の実装にはcargo-lambdaを使用するのが便利です。 https://www.cargo-lambda.info/ cargo-lambdaの詳しい使用方法については割愛します エントリーポイントの作成 cargo-lambdaで初期化コマンドを叩くと以下のようにmain.rsが自動的に生成されます。 cargo lambda init use lambda_runtime::{ run, service_fn, tracing::{self}, Error, }; mod handler; mod image_task; mod lock; mod s3_sequencer; #[tokio::main] async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); run(service_fn(handler::function_handler)).await } handler:function_handler をLambda関数のエントリポイントとして指定しているため、 handler.rs に実装を記述します。 function_handler は以下のように実装します。 use crate::image_task::ImageTask; use aws_lambda_events::event::s3::S3Event; use lambda_runtime::{tracing::info, Error, LambdaEvent}; pub async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> { // S3イベントをImageTaskに変換する let tasks: Vec<_> = event .payload .records .into_iter() .map(ImageTask::try_from) .collect::<Result<Vec<_>, _>>()?; // futures::future::join_allで実行するタスクを作成する let execute_tasks = tasks.iter().map(|task| task.execute()); // join_allで全てのタスクを実行・待機する // 実行結果をretに格納する let ret = futures::future::join_all(execute_tasks).await; // 実行結果をログに出力する for (t, r) in tasks.iter().zip(&ret) { info!("object_key: {}, Result: {:?}", t.object_key, r); } // エラーがある場合はエラーを返す if ret.iter().any(|r| r.is_err()) { return Err("Some tasks failed".into()); } // 正常終了 Ok(()) } S3イベントのベクタをImageTask構造体のベクタに変換します。変換方法はTryFromトレイトを実装しているため、try_fromメソッドを呼ぶだけで良いです。 ImageTask構造体のベクタを元に、画像処理タスクを作成します。 tokioクレートの join_all 関数を使って、全てのタスクを並列実行します。 3の join_all で帰ってきた結果をログに出力します。 エラーがある場合はエラーを返却し、Lambda関数を異常終了させます。 エラーがなければ正常終了します。 画像処理の実装 1で使用するImageTask構造体は以下のように定義されており、Lambdaの実行に必要な情報を保持しています。 #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum TaskType { Grayscale, Delete, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageTask { pub bucket_name: String, #[serde(rename = "id")] pub object_key: String, pub sequencer: S3Sequencer, pub task_type: TaskType, pub processing: bool, } フィールド名 説明 bucket_name S3バケット名 object_key オブジェクトキー sequencer S3イベントのsequencer task_type タスクの種類を示す列挙体(Grayscale, Delete) processing 処理中フラグ 具体的な画像処理についてはImageTask構造体のexecuteメソッド内で実装します。 impl ImageTask { pub async fn execute(&self) -> Result<(), Error> { // 1. ロックを取得する let lock = S3Lock::new(&self).await?; // 2. タスクの種類に応じて処理を行う match self.task_type { TaskType::Grayscale => { // 画像をグレースケールに変換し、出力バケットに保存する let body = lock.read_input_object().await?; let format = image::ImageFormat::from_path(&self.object_key)?; let img = image::load_from_memory_with_format(&body, format)?; let img = img.grayscale(); let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), format)?; lock.write_output_object(buf).await?; } // 画像を出力用バケットから削除する TaskType::Delete => lock.delete_output_object().await?, } // 3. ロックを解放する lock.free().await?; Ok(()) } } S3のオブジェクトのデータ不整合が起きないように排他処理をかける タスクの種類に応じて処理を行う 元のバケットにファイルが追加された場合は、画像をグレースケールに変換し、出力用バケットに保存する ファイルが削除された場合には、出力用バケットからファイルを削除する 処理が終わったらロックを解放する ロック処理を実装する ロック処理を実装するため、S3Lock構造体を定義します。 pub struct S3Lock { dynamodb_client: aws_sdk_dynamodb::Client, table_name: String, s3_client: aws_sdk_s3::Client, input_bucket_name: String, input_object_key: String, output_bucket_name: String, output_object_key: String, } 具体的なロック取得処理はコンストラクタに実装します。 少しコードが長いですが、ざっくり言うと以下の通りです。 DynamoDBに書き込みが成功したらロックを取得できたとみなす。 書き込みに失敗した場合は2秒ごとにリトライする。 30秒以上ロックが取れない場合はタイムアウトする。 以下にロック処理のシーケンス図を示します。 sequenceDiagram participant ImageTask participant S3Lock participant DynamoDB ImageTask->>S3Lock: ロック取得 loop alt タイムアウト(30秒) S3Lock->>ImageTask: エラー返却(タイムアウト) end S3Lock->>DynamoDB: 処理状況取得 DynamoDB->>S3Lock: 結果返却 alt レコードが存在する場合 S3Lock->>S3Lock: シーケンサ比較 alt 自分自身が古い場合 S3Lock->>ImageTask: エラー返却(スキップ) else 自分自身が新しい場合 S3Lock->>S3Lock: ロック取得リトライ end else S3Lock->>DynamoDB: 条件付き書き込みでロック取得 DynamoDB->>S3Lock: 書き込み結果返却 alt 書き込み成功 S3Lock->>ImageTask: ロック取得成功 else 失敗 S3Lock->>S3Lock: リトライ end end end コンストラクタ内のコードは以下の通りです。 impl S3Lock { pub async fn new(task: &ImageTask) -> Result<Self, Error> { let table_name = std::env::var("DYNAMODB_TABLE_NAME").unwrap(); let output_bucket_name = std::env::var("OUTPUT_BUCKET_NAME").unwrap(); let require_lock_timeout = Duration::from_secs( std::env::var("REQUIRE_LOCK_TIMEOUT") .unwrap_or_else(|_| "30".to_string()) .parse::<u64>() .unwrap(), ); let interval_retry_time = Duration::from_secs( std::env::var("RETRY_INTERVAL") .unwrap_or_else(|_| "2".to_string()) .parse::<u64>() .unwrap(), ); let config = aws_config::load_defaults(aws_config::BehaviorVersion::v2024_03_28()).await; let s3_client = aws_sdk_s3::Client::new(&config); let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); // ロックを取得する // 実行時間を計測する let start = Instant::now(); loop { // 30秒以上ロックが取れない場合はタイムアウトする if start.elapsed() > require_lock_timeout { return Err("Failed to acquire lock, timeout".into()); } // 強力な読み取り整合性を利用してDynamoDBからシーケンサを取得する let item = dynamodb_client .get_item() .table_name(table_name.clone()) .key("id", AttributeValue::S(task.object_key.clone())) .consistent_read(true) .send() .await?; // 取得したアイテムが存在する場合はシーケンサを比較する if let Some(item) = item.item { let item: ImageTask = from_item(item)?; if task.sequencer <= item.sequencer { // 自分自身が古いシーケンサの場合は処理する必要がないのでスキップする return Err("Old sequencer".into()); } // 自分自身が新しいシーケンサの場合は他の処理が終わるまで待機する if item.processing { warn!( "Waiting for other process to finish task, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(interval_retry_time); continue; } } // DynamoDBに条件付き書き込みでロックを取得する // その際にレコードが存在していたらprocessingフラグがfalseの場合のみ書き込む let resp = dynamodb_client .put_item() .table_name(table_name.clone()) .set_item(Some(to_item(&task).unwrap())) .condition_expression("attribute_not_exists(id) OR processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await; // 取得できたらループを抜け処理を続行する // 取得できなかった場合はロックが取れるまでリトライを繰り返す match resp { Ok(_) => break, Err(SdkError::ServiceError(e)) => match e.err() { PutItemError::ConditionalCheckFailedException(_) => { warn!( "Failed to acquire lock, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(Duration::from_secs(2)); continue; } _ => return Err(format!("{:?}", e).into()), }, Err(e) => return Err(e.into()), } } return Ok(Self { dynamodb_client, output_bucket_name, s3_client, table_name, input_bucket_name: task.bucket_name.clone(), input_object_key: task.object_key.clone(), output_object_key: task.object_key.clone(), }); } } ロックを解除する処理は以下のように実装しており、processingフラグをfalseに更新することでロックを解除します。 impl S3Lock { pub async fn free(self) -> Result<(), Error> { // DynamoDBのロックを解放する // processingフラグのみを更新する self.dynamodb_client .update_item() .table_name(self.table_name) .key("id", AttributeValue::S(self.input_object_key)) .update_expression("SET processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await?; Ok(()) } } S3オブジェクトを触るのにロックを強制させたいため、S3LockにS3のオブジェクトを操作するメソッドを生やしています^[S3Lockに生やすと再利用性が低くなりそうだなと感じますが、簡単のため同じ構造体に定義しておきます。もっと良いやり方がありそうですが...]。 impl S3Lock { pub async fn read_input_object(&self) -> Result<Vec<u8>, Error> { // S3オブジェクトを取得する let object = self .s3_client .get_object() .bucket(&self.input_bucket_name) .key(&self.input_object_key) .send() .await?; let body = object.body.collect().await?.to_vec(); Ok(body) } pub async fn write_output_object(&self, buf: Vec<u8>) -> Result<(), Error> { // S3オブジェクトを保存する let byte_stream = ByteStream::from(buf); self.s3_client .put_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .body(byte_stream) .send() .await?; Ok(()) } pub async fn delete_output_object(&self) -> Result<(), Error> { // S3オブジェクトを削除する self.s3_client .delete_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .send() .await?; Ok(()) } } 実際に動かしてみる サンプルコードを実際に動かしてみましょう。 グレースケールにしたい画像を用意しなければならないわけですが、今回は兵庫県立公園あわじ花さじき^[淡路島にある綺麗なお花畑です。私が撮影しました。 https://awajihanasajiki.jp/about/]の画像を使用します。 インフラ構築 まずはterraform applyしてAWSインフラを構築します。 GitHubレポジトリをクローンして、以下のコマンドを実行してください。 cd terraform # variables.tfやprovider.tfをよしなに修正しておく terraform init terraform apply ![サンプルコードのアーキテクチャ図。S3バケットにアップロードされた画像ファイルをLambda上でグレイスケールに変換して、出力用のS3バケットに保存する。DynamoDBを使ってロック処理を実装する。](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) S3バケットに画像ファイルをアップロード インフラ構築ができたら、入力用のS3バケットに画像ファイルをアップロードします。 アップロードが終わるとLambda関数の処理が始まり、DynamoDBのテーブルにアイテムが追加されます。 処理が終わると出力用のS3バケットに画像ファイルが保存され、DynamoDBのアイテムの processing フラグがfalseになります。 出力用のS3バケットに画像が追加され、グレースケールに変換されていることが確認できます。 S3バケットから画像ファイルを削除 入力用バケットからオブジェクトが削除されると、出力用バケットからもオブジェクトが削除されます。 排他処理が効いているか確認 DynamoDBに追加されるアイテムの processing フラグがtrueになると、処理すべきS3イベントが飛んできたとしてもその処理は待機します。 この挙動を確かめるためにDynamoDBの processing フラグをわざとtrueにして、同じ名前のファイルをアップロードしてみます。 CloudWatch Logsを見ると、新たに発生したS3イベントが処理されずに他の処理の完了を待機していることがわかります。 DynamoDBの processing フラグをfalseに戻すと、処理が再開されます。 排他処理のおかげで、削除イベントとアップロードイベントがほぼ同時に発生しても処理の順序が保証されます。 まとめ 今回はS3イベントの順序を保証するためのシーケンサを利用した、画像処理のサンプルコードを紹介しました。 S3イベントの順序を保証するためには、シーケンサを利用してイベントの順序を比較する必要があります。 自分の趣味でRustでサンプルコードを実装してみましたが、他の言語でも同様の実装が可能なはずです。 ぜひ参考にしてみてください。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の6日目の記事です🎅🎄 はじめに Merry Christmas🔔 モバイルアプリ開発Gでmy routeアプリのAndroid側の開発を担当しておりますOsaka Tech LabのRomie( @Romie_ktc )です。 現在、my routeのAndroidチームではxmlからJetpack Compose(以下Compose)へUIの実装を切り替えております。 ただ、一気に全てをリファクタリングできませんので、どうしてもxmlの上にCompose化したパーツが乗っかるということも出てきてしまいます。 今回は、BottomSheetのxmlの上にComposeを載っける形で実装したお話をします。 完成イメージ :::message alert この記事ではリファクタリングの過程における実装を紹介しております。完全にxmlかComposeかのどちらかで実装するのをおすすめします。 ::: 基本編 BottomSheetDialogFragmentを継承した以下のクラスにて実装します。 class MixComposedBottomSheetDialog : BottomSheetDialogFragment() BottomSheetの基本的な挙動を設定する BottomSheetの挙動を設定します。 以下のコードをonCreateViewに記載します。 dialog?.setOnShowListener { dialogInterface -> val bottomSheetDialog = dialogInterface as BottomSheetDialog val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) // 角丸や背景色を設定したい場合は以下で対応します context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) } val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) } bottomSheetBehavior?.let { behavior -> // maxHeightやpeekHeightは任意の高さを設定してください。 behavior.maxHeight = EXPANDED_HEIGHT // BottomSheetが最大限まで拡張されたときの高さを設定 behavior.peekHeight = COLLAPSED_HEIGHT // BottomSheetが初期状態で表示される高さを設定 behavior.isHideable = false behavior.isDraggable = true } } Compose onCreateViewにてComposeViewをreturnすることによって、BottomSheetDialogFragmentの上にComposeを載せることができます。 return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() } } これだけならわかりやすいですが、ここにBottomSheet下部に 常に 固定されたButtonを追加するとなると難易度が上がります。 発展編 Button自体はComposeで実装します。 ただしこのようにButtonを追加しても、ContentsをスクロールしないとButtonは表示されません。 return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() ButtonOnBottomSheet() } } 常にButtonをBottomSheetの下部に固定させ、Contentsをスクロールしても引っ張られることなく表示させるためには、以下のような実装が必要です。 val button = ComposeView(context ?: return@setOnShowListener).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { ButtonOnBottomSheet() } } BottomSheetの下部にButtonを固定させるためには、以下のコードを使います。 このコードを使用することで、BottomSheetDialogFragmentで実装されているレイアウトを直接取得できます。したがってより柔軟にViewの操作が可能になります。 val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container) val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator) clipChildrenはViewGroupのプロパティで、子Viewが親Viewの範囲外に描画される場合に描画をクリップするかどうかを指定します。 BottomSheetの他の要素と重なる場合に用いられます。 // デフォルト値はtrueで、falseに設定すると子Viewが親の境界を超えてもそのまま表示できます。 button.clipChildren = false button.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, ).apply { gravity = Gravity.BOTTOM } containerLayout?.addView(button) button.post { val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams layoutParams?.apply { button.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), ) this.bottomMargin = button.measuredHeight containerLayout?.requestLayout() } } まとめ これまでの実装をまとめますと、以下の通りです。 override fun onCreateView(): View { dialog?.setOnShowListener { dialogInterface -> val bottomSheetDialog = dialogInterface as BottomSheetDialog val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container) val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator) val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) } val button = ComposeView(context ?: return@setOnShowListener).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { ButtonOnBottomSheet() } } button.clipChildren = false button.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, ).apply { gravity = Gravity.BOTTOM } containerLayout?.addView(button) button.post { val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams layoutParams?.apply { button.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), ) this.bottomMargin = button.measuredHeight containerLayout?.requestLayout() val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) } bottomSheetBehavior?.let { behavior -> // maxHeightやpeekHeightは任意の高さを設定してください。 behavior.maxHeight = EXPANDED_HEIGHT // BottomSheetが最大限まで拡張されたときの高さを設定 behavior.peekHeight = COLLAPSED_HEIGHT // BottomSheetが初期状態で表示される高さを設定 behavior.isHideable = false behavior.isDraggable = true } } } } return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() } } } つまりどうしたかと言いますと xmlでBottomSheetを作成し 高さなどのレイアウトを調整し BottomSheetの下部にButtonをくっつけるために更にレイアウトを用意し合体させ Compose化したコンテンツをBottomSheetに上乗せした ということです。 ここまで読んでいただきありがとうございました!
アバター
はじめに こんにちは! 先日、弊社もプレミアムスポンサーを務めた「 JSConf JP 2024 」が行われました! 今回は実際に現地参加したメンバーによるセッションレポートをお届けします! ITOYU You Don’t Know Figma Yet - FigmaをJSでハックする https://jsconf.jp/2024/talk/hiroki-tani-corey-lee/ Figmaはブラウザで開ける、だからこそdevtoolsを使ってJSで操作出来るということを知りました。 Figma公式が用意しているAPIを、 figma グローバルオブジェクトを使って操作することで、要素のCSS情報を取得したり、レイヤーを作成したり出来るそうです。 Figmaがブラウザで開けるからこそのメリットを最大限に活かせるということで、Figmaの無限の可能性を感じました。 グローバルオブジェクトを通して出来ることについては、以下のFigma公式ドキュメントを参照してみてください。 https://www.figma.com/plugin-docs/api/global-objects/ 幸せの形はどれも似ているが、不幸なプロジェクトはそれぞれの形がある https://jsconf.jp/2024/talk/mizchi/ 普段mizchiさんがパフォーマンスチューニングのコンサルをする中での経験を元に、パフォーマンス課題の発生傾向を紹介していました。 その中で「安易なアンチパターンの採用をした結果、後々爆発する」という話を聞いて、私の過去の経験を思い出しました。 ドキュメント上では非推奨とされているけど、目の前の問題を解決するためにStackOverflowで見つけたHackを採用した結果さらに厄介な問題を引き起こすという経験、 あると思います。 その場しのぎの解決策を採用するのではなく、根本的な解決策を見つけることが大切だとあらためて感じました。 nam 生成AIでコーディング試験を解いてみよう(株式会社ハイヤールー) https://jsconf.jp/2024/talk/hireroo/ このワークショップの主催は、コーディング試験サービスを提供しているHireRooさんなので、正真正銘本物のコーディング試験です。 その本物のコーディング試験を生成AIで解いてみる、という通常は試す機会のないワークショップでした。 (セッションだと思い込んで見に行ったところ、PCが必要だった模様…申し訳ないですがHireRooの方のPCで解かせていただきました。すみません…) Chat GPTで取り組んだのですが、問題文だけをそのまま渡してもなかなか思った通りのコードは返ってこず…やはり工夫が必要でした。そうウマいこといきませんね。 ちなみに実際にコーディング試験で生成AIを使うと結構わかるそうです。 LT: JavaScriptを支えるエコシステム(漫才) https://jsconf.jp/2024/talk/ecma-boys/ このセッションでは、登壇者の方のお母様が今気になっているパッケージマネージャーやバンドラーの名前を忘れてしまったそうで、お母様から聞き出した特徴をもとにそれらが何なのかを推測する、という内容でした。 「オカンが気になっているパッケージマネージャー」など、パワーワードが頻出しており大変興味深かったです。 個人的にはバンドラに関する話題で「設定ファイルがわかりやすいならwebpackではない」と断じられていた部分が心に残りました。 きーゆの LT: JavaScriptのモジュール解決の相互運用性 https://jsconf.jp/2024/talk/berlysia/ このセッションではJavaScriptのモジュール解決についてお話されていました。 つい最近もSwiperを含むコンポーネントのUT実装でCJS/ESM問題に直面し、モジュール解決の知識不足に危機感を抱いたばかりでした。 また、JestからVitestへのマイグレーションもチーム内で少し話題になったこともあり、この辺りの話はしっかり理解しておく必要性を感じました。 ちなみに、私にはとても難しく、聞くのがやっとでした🥲 早くこの手の話にうんうんできる人になりたいです。 LT: クルマのサブスクサービスをNext.jsで内製化した経験とその1年後 https://jsconf.jp/2024/talk/kinto-technologies/ 自社のセッションになります。 内製化PJは私が入社するよりはるか昔の話なので、歴史を学ぶ意味でも聴講しました。 技術スタックが担当しているプロダクトとほぼ同じなので、課題や今後の展望も自分事として受け取りました。 Ren.M LT: romajip: 日本の住所CSVデータを活用した英語住所変換ライブラリを作った話 https://jsconf.jp/2024/talk/kang-sangun/ こちらのセッションではromajipというライブラリの制作についてお話しされていました。 個人的に郵便局だけでなく、デジタル庁も日本の住所マスタを提供しているのが驚きました。 また、同名の地名などの対策などが大変そうだなと感じました(東京と大阪にある日本橋など) 私自身もライブラリを自作する機会があれば躓いたポイントやこだわりをまとめておきたいです! LINEヤフーにおけるPrerender技術の導入とその効果 https://jsconf.jp/2024/talk/masanari-hamada-tomoki-kiraku/ こちらのセッションではLINEヤフー様によるPrerender技術の導入検証ついてお話しされていました。 Prerenderとはリンクをホバーしたタイミングで遷移先のページを事前に読み込む技術のことのようです! その結果、ページ表示スピードを格段に上げ、ユーザ体験を大幅に向上させます。 LINEヤフー様では様々な検証した結果、導入には至らなかったとのことでした。(リンクの密集問題など) しかし、使い方によっては格段に読み込みスピードが早くなりそうでしたので私もこれから勉強してみたいと思いました! ノベルティ スポンサー企業のブースを回り、様々なノベルティをいただきました! 個人的にメルカリ様の「SOLD OUTキーホルダー」が嬉しかったです! 公式Tシャツとトートバッグ スポンサーノベルティ We Are Hiring! KINTOテクノロジーズでは一緒に働く仲間を探しています! まずは気軽にカジュアル面談からの対応も可能です。少しでも興味のある方は以下のリンクからご応募ください! https://hrmos.co/pages/kinto-technologies/jobs/1955878275904303141 おわりに いかがだったでしょうか! 来年のJSConf JPもぜひ現地参加できればと思います! ここまでご覧いただきありがとうございました!
アバター
A.K Self-introduction I'm A from the my route Development Group. I am from Latvia. In my previous job at a startup, I was working broadly as a full-stack developer. How is your team structured? Six members including myself. What was your first impression of KINTO Technologies when you joined? Were there any surprises? Even though it's part of a large group company, I found my team surprisingly easy to work with because of the friendly atmosphere. I really appreciated that there are study groups available for basically any technology I'm interested in. What is the atmosphere like in the workplace? Surprisingly, there are many foreign nationals, and they all have a high level of technical skill and are easy to talk to. How did you feel about writing a blog post? I'm not good at it. Question from M.O.: A, as a smart home user, what's the question you ask Alexa most often? Well, I definitely ask, "What's the weather today?" before heading out. I also use "What's this song?" or "Play [song name]" almost daily, since it's connected to Spotify. As a fun fact, Alexa can be used as a TTS speaker, so I enjoy playing custom messages. The most useful one for me, though simple, is a reminder that to plays the time plus a message every 5 minutes between 7:00 and 8:00 a.m. "It's already 7:35! Are you getting up or what?! "Something like that. S.D Self-introduction I am Deguchi from the Production Group. In my previous job, I worked on car navigation and map data related fields. I was also involved in natural language processing and machine learning. How is your team structured? It is a group of 5 people including myself. Each member is individually involved in different projects. What was your first impression of KINTO Technologies when you joined? Were there any surprises? Since the company's atmosphere was explained during the interview, there weren't any major surprises. What is the atmosphere at the site? While most communication happens on Slack, there's also a lot of face-to-face interaction, creating an environment that's easy to communicate in. How did you feel about writing a blog post? I like having the opportunity to share information outside the company. It allows me to review past cases and Tech Blogs and gather a variety of insights from internal team members. Question from A.K Out of all the gadgets you've collected, which do you think is the most useful? It's hard to pick just one, so let me share a few of my favorites! Raspberry Pi: An amazing product that allows you to easily challenge IoT and actually create things! It's impressive that Ubuntu (with GUI) runs properly for this price. Insta360 Flow: It's great to get this level of gimbal performance at this cost! Subject tracking is also convenient! Mitene GPS: I recommend this for small children to have. It's a useful gadget that it can be taken to places where cell phones aren't allowed. The battery life is also good. K.N Self-introduction I am Nishi from the Data Engineering Team at Analysis Group. How is your team structured? The Analytics Group consists of three teams: the Data Science Team, the Data Engineering Team, and the Data Produce Team. What was your first impression of KINTO Technologies when you joined? Were there any surprises? There are many in-house study groups! What is the atmosphere at the site? In daily morning meeting, we share our progress, issues, and consultations. Since we have three locations, Tokyo, Nagoya, and Osaka, and a mixed work system of home and office, we communicate through Slack Huddles, sharing screens as needed. How did you feel about writing a blog post? After going through past articles for reference, I felt it was a great opportunity to get to know other employees. Question from S.D: Looking back to when you joined the company, is there anything you wish you had more information about, or a system you wish had been in place? I took many business model orientation courses after joining the company, but I think having a review session about three months later would be helpful for retaining the information better. W ![W avatar](/assets/blog/authors/numami/maymember/4.png =200x) Self-introduction I'm Watanabe and I belong to the Organizational Human Resources Team in Human Resources Group. I worked as a sales manager at a human resources firm and handled human resources at a startup. How is your team structured? The Human Resources Group includes the Organizational HR Team, the Recruiting Team, and the Labor Relations and General Affairs Team. The group had a total of 13 members. What was your first impression of KINTO Technologies when you joined? Were there any surprises? There were no particular surprises. I expected the internal control system to be well-organized since it's a Toyota Group company, and it was well-structured as I had anticipated. However, I feel we have enough freedom. I'd already heard about various internal challenges both positive and negative from the interview, so there were no surprises in that point either. What is the atmosphere at the site? My impression is that everyone is positively engaged in their work. During my first month with the company, I had conversations with the managers. They were all supportive and welcoming, which helped me start comfortably. How did you feel about writing a blog post? I thought it was great how much effort goes into both internal and external communication. Given that it's a major affiliate, I expected stricter control over external communications. However, I feel there's a high level of freedom on this point, much like a venture company. Question from K.N: What kind of challenges would you like to take on at KINTO Technologies? I want to take on the challenge of creating an environment where everyone can move forward as one. K Self-introduction I'm part of the IT/IS Division. In my previous job, I worked in MS infrastructure, .NET development, and information system operations at a SIer company. How is your team structured? The IT/IS Division consists of four teams: Asset Platform, Corporate Engineering, Tech-Service, and Enterprise Technology. As a member of the Corporate Engineering team, I am mainly involved in addressing business issues and requests through system implementation, renovation, and improvement. What was your first impression of KINTO Technologies when you joined? Were there any surprises? There were no surprises. Everyone in the IT/IS Division is thinking about, "How do my work tasks provide value?" My first impression was how impressive it was that things were so well-managed. What is the atmosphere at the site? I usually work in the Muromachi office. The atmosphere makes it easy for us to consult with one another and think of each other's work as if it were our own. We regularly have 1-on-1 meetings with leaders, managers, and general managers to discuss honest opinions, impressions, requests, and concerns. As a result, it feels easy to communicate openly with others outside of these 1-on-1 meetings. How did you feel about writing a blog post? I thought it was simply a good measure, as we are actively communicating with the outside world beyond this blog. Question from W: Any interesting places (travel destinations, etc.) you have visited recently? I recently moved to a new house and visited a public bathhouse with a college friend who came to visit. The Showa-style appearance and atmosphere of the bathhouse were elegant, providing us with an extraordinary experience. I wasn't really into public bathhouses before, but I actually found it to be an enjoyable place to refresh and unwind. JK ![JK avatar](/assets/blog/authors/numami/maymember/6.png =200x) Self-introduction I am Kim from the Toyota Woven City Payment Solution Development Group. How is your team structured? It is a group of 6 members including myself. We are actually working on the Woven side, and there are other Woven members in the team besides KINTO Technologies. Our work covers a wide range, from frontend and backend development to infrastructure. What was your first impression of KINTO Technologies when you joined? Were there any surprises? It was nice to hear various details during the orientation. What is the atmosphere at the site? Basically, we set up a sprint plan once a week and work according to the target. We also make time for tech talks and document reading. Since we often work remotely, we use Slack, Meet, and other tools to stay connected with team members. How did you feel about writing a blog post? I wanted to give even a little useful information to those who read my article. Question from K: What do you value the most in your work? I think it's the same everywhere, but communication with people is the most important. Especially in development side, if there's miscommunication about functional requirements, something entirely different might be created (lol). The other is continuation. By persisting in my work, not only does my own work improve, but also the team can achieve a higher level of completion. M ![M avatar](/assets/blog/authors/numami/maymember/7.png =200x) Self-introduction I am M from Data Integration Platform Team. How is your team structured? There are two people, including myself, working on product maintenance. The product I am responsible for is connected to many systems, so I frequently need to communicate with various people. What was your first impression of KINTO Technologies when you joined? Were there any surprises? I was surprised by the proactive introduction of study groups, as well as new tools and services. What is the atmosphere at the site? It's like a calm atmosphere where conversations occur when needed. How did you feel about writing a blog post? I didn't think anything in particular. Question from JK: What were the onboarding and catch-up processes like for your work after joining the company?Please share any positives you noticed during the process! First, I had a 1-on-1 session to go over the team structure, the purpose and role of the product I'd be working on. After that, I set up the machines and development environment using the provided documentation. So far, pretty standard onboarding up to that point. Afterward, we went through a hands-on process of signing a contract for a new KINTO car to better understand the business. The hands-on materials were carefully prepared, making it easy to understand the car rental process! D ![D avatar](/assets/blog/authors/numami/maymember/8.png =200x) Self-introduction I'm D from the my route Development Group. How is your team structured? It is a group of 6 people including myself. What was your first impression of KINTO Technologies when you joined? Were there any surprises? I felt that the company offers lots of opportunities for sharing information. The atmosphere was also more relaxed than I had imagined. What is the atmosphere at the site? We often work individually in a quiet and focused manner. All the team members are kind. How did you feel about writing a blog post? I was very nervous at the thought of someone outside the company reading my article. Question from M: Have you found a favorite lunch spot? If so, let us know! Yes, an Indian restaurant on the first floor of the building. M.O Self-introduction I am Onuma from the Mobile Development Group. Previously, I worked as an Android engineer for the voice platform Voicy. I was also involved in backend development (Go), as well as frontend (Angular/TypeScript) and iOS app development. How is your team structured? The my route Android development team consists of four people, including myself. What was your first impression of KINTO Technologies when you joined? Were there any surprises? I had heard that there were many engineers from outside Japan, but there were even more than I expected. There are abundant in-house study groups, providing plenty of opportunities for learning and output. What is the atmosphere like in the workplace? Each person's area of responsibility is clearly defined within the company, which allows me to focus on my work and coding. I also believe this is an environment where we can immediately share any knowledge gained in the course of our work with one another. How did you feel about writing a blog post? I enjoy sharing knowledge about technology. Question from D: If any, what has been your biggest problem since joining the company? Adding home working schedules to Outlook to comply with remote work rules.
アバター
はじめに KTCデータ分析部 分析G マネージャの西口です。 未来に向けた技術開発や研究が、現実社会での課題解決にどのように貢献できるかを考えることは、イノベーションを推進する上で重要なテーマです。しかし、研究者と企業の間には「未来に必要な研究」と「現場で今すぐ使いたい技術」というズレが存在します。このため、両者が連携しづらいという課題が生じています。この記事では、この「マッチングの壁」を乗り越えるための取り組みについて、これまでの活動と今後の展望を紹介します。 (Generated by Microsoft Copilot) これまでの活動 トヨタ自動車には 未来創生センター (以下、FRC)という「未来につながる研究」を行っている部門があります。FRCとはご縁があって、KINTOテクノロジーズ(以下、KTC)のデータ分析部で抱える研究的な課題の助っ人として参加していただきました。それがこの取り組みの始まりでした。当初は、具体的なデータサイエンス課題があり、特に問題もなく新規性のある研究案件として進めることができました。教科書に出てくるデータサイエンスの課題では、予測値そのものが評価の対象となります。しかし、ビジネスの観点から見ると、予測値の信頼性やばらつきについても重要です。つまり、どれくらいの確度で予測ができるのかを知りたいということです。その後もいくつかの課題が浮上し、さらなる取り組みを続けていくことになりました。当初はそれほど苦労することもなく、それらの課題をビジネスに寄与する研究案件とでき、ご支援をいただいてきました。 マッチングの苦労 しかし、問題が出てきました。それは取り組む案件の制約事項がお互いで異なるということが明らかになってきたのです。具体的には、共に目指すのはユーザーの体験価値の向上ですが、その成果がKTC側は「製品化」であるのに対して、FRC側は「未来につながる研究」(成果物としては特許化や論文化)だということです。それらの違いが、例えばスケジュール感だったり、完成度のレベル感だったりと、協業しづらい制約となってきました。このような制約事項の中でマッチングすると、「あったら嬉しいが無くても困らない技術」を研究の対象とせざるを得ませんでした。結果、構築された技術は現場にとっては優先度が低く活用されないため、「研究部隊」と「ビジネス現場」の連携の難しさを痛感しました。 (Generated by Microsoft Copilot) アイデアソンの実施 1. アイデアソンへの期待 その解決策の1つとして、「アイデアソン」を実施しました。アイデアソンとは、短期間で課題に対する新しいアイデアを出し合い発展させるためのワークショップ形式のイベントです。このイベントを通して、FRCとKTCの双方が参加し、自由な発想でお互いの技術や研究を生かし合える方法を考える機会になるのではと考えました。また中長期的にも両者が潜在的な協力の可能性を発見し、KTCとしては技術の応用方法について新しい視点を持つこと、FRCでも次の研究の“種”に繋がっていくことを期待しました。 取り組みの目的 アイデアソンでの期待 研究サイド 未来に必要な研究 種の発見 ビジネスサイド 今すぐ使いたい技術 新たな視点 2.実施の流れ 具体的な動きとしては、FRCに紹介できそうな技術リストを作ってもらい、それをKTCでアンケートを取り、その結果をもとに2つの技術紹介をお願いしました。当日は、その2つの技術の紹介と簡単な質疑応答の後、アイデアソンを行いました。 3.実施状況 2024年9月に実施 16:00~17:30 勉強会:2つの技術紹介 技術A:レコメンデーションに関する技術 技術B:顧客心理測定に関する技術 17:40~19:00 アイデアソン(各テーブル25分) FRCから7名(オンライン1名)、KTCから11名(オンライン3名)が集まりました。アイデアソンではKTCのメンバーを3つに分け、技術Aのテーブルと技術Bのテーブル、そしてフリーディスカッションのテーブルを順に回ってもらう形を取りました。それぞれのテーブルはFRC2~3名、KTC3~4名という構成で行われました。一部のメンバーを除き、今回がほぼ初対面だったので、最初は自己紹介と業務内容の紹介がなされたあと、それをもとにそれぞれのテーブルのテーマでディスカッションが始まりました。25分はあっという間で、ちょうど盛り上がってきたところでタイムアップというような場面も多く見受けられました。そんななか、実際にマッチングできそうな案件が出てきて、FRC研究者とKTCエンジニアですぐに詳細の擦り合わせを始めています。具体的なことは言えませんが、KTCが追求する「顧客理解」と研究部門の技術がうまく噛み合うと、話が進展しそうな手応えを感じました。 4.実施後の参加メンバーからの感想と今回の反省点 実施後にKTC参加メンバーにアンケートを行いました。満足度は、5点中 4.11で、「内容に興味があれば」の条件ありの場合も含めると全員が次回も「参加したい」という意向でしたので、有意義な時間になったものと考えられます。 ただ、以下のような改善点も出てきました。 勉強会の説明の時間が長い オンラインでの説明は音声が聞き取りにくかった 勉強会の技術説明はKTCの実際のデータを適用した例を含めて欲しい。具体例をもっと増やして欲しい アイデアソンの時間をもっと増やして欲しい アイデアソンの各セッションの冒頭での自己紹介や業務説明が冗長で議論の時間を多くとれなかった これらの点を踏まえ、次回開催時はより円滑な進め方で実施したいと思います。 今後の展望 トヨタグループにはFRCだけでなく、他にも多くの研究部隊が存在します。機会があれば別の研究部隊とも意見交換を行い、マッチングのための場を作っていきたいと考えています。具体的には、ビジネス現場が求める技術や解決策をより明確に提示し、研究者が自身の研究をビジネス現場で活用できる場を設けることです。その逆も同様に、研究者の関心(テーマや課題)を企業に伝えることで、双方の理解が深まっていくと考えます。 このようにすることで、技術が実際の現場で活かされる事例が増えていき、双方にとってWin-Winの関係を築くことができると考えています。そのためには、アイデアソンやマッチングイベントの実績を重ね、よりスムーズな技術と研究の連携を実現する必要があります。これにより、未来に向けた研究の現実世界での実装サイクルが高速化し、より良い社会づくりに貢献できると信じています。 まとめ 未来につながる研究と今使いたい技術を結びつけるには、両者のニーズや期待を理解し合う場が重要です。上記のような勉強会やアイデアソン、ワークショップを通じて、研究者とビジネス側の相互理解が深まり、より実用的な連携が生まれると考えています。 その連携により、 KTCが掲げる「内製開発組織と顧客視点」 にさまざまな新しい技術が加わることで、顧客にいち早く新たな「感動」を届けられると思っています。 今後もKTCは、FRCと新たな技術の開発とその利活用の道を探っていきたいと思います。それは、私たちがモビリティプラットフォーマーのトップランナーとして一人ひとりの「移動」に「感動」をもたらすためにも大切なことだと考えているからです。 (Generated by Microsoft Copilot) <当サイトの内容、テキスト、画像等の無断転載・無断使用を固く禁じます。>
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の5日目の記事です🎅🎄 KINTOテクノロジーズで my route(iOS) を開発しているRyomm a.k.a 幻のbot職人 です。 まなびぃ から小ネタです。 ここではSlack CLIの話として書いていますが、内部的にはSlack CLIもSlack APIを使用していますので、Slack APIを直接使用しているケースでも当てはまるかもしれません。 背景 Slack CLIで以下のような処理を行おうとしたとき、なぜかBlock Kitのメッセージが送れないことがありました。 フォームの入力でrich_text型として値を受け取る 値をrich_text型としてDataStoreに保存 DataStoreから保存したいくつかのrich_text型のデータを取り出して結合し、整形する postMessage を使って作成したblockを送信する しかし、 parameter_validation_failed というエラーで落ちてしまいました。 ![エラー](/assets/blog/authors/ryomm/2024-12-04-2/02.png =600x) 原因 送信部分でパラメータ不正のエラーが出ており、送信しようとしていたblockを調べてみると、以下のように block_id が重複していることがわかりました。 [ { "type": "rich_text", "block_id": "xdrwH", // <- this "elements": [ /* ... */ ] }, { "type": "rich_text", "block_id": "xdrwH", // <- this "elements": [ /* ... */ ] } ] 送ろうとしているメッセージの block_id が衝突しているため、Slackにメッセージを送れないようです。 block_id block_id とは、ブロックの一意の識別子です。 公式ドキュメントには以下のように説明されています。 A unique identifier for a block. If not specified, a block_id will be generated. You can use this block_id when you receive an interaction payload to identify the source of the action. Maximum length for this field is 255 characters. block_id should be unique for each message and each iteration of a message. If a message is updated, use a new block_id. https://api.slack.com/reference/block-kit/blocks block_id を指定せずにブロックを作成した場合は、自動的に block_id が生成されます。 主にインタラクティブなやり取りをしたい場合に使用します。例えばボタンが押された際にどのブロックのボタンを押されたのか?などの特定に役立ちます。 1回のメッセージ、もしくはメッセージの反復(=一連の双方向なやり取り)の中で一意である必要があります。 また、メッセージが更新された際には新しい block_id を使用します。 今回のようにフォームでの入力の場合、受け取ったrich_textに含まれる block_id は自動生成されたものになります。 さらに、冒頭の処理では入力で受け取るrich_textは別々のメッセージとして受け取っているため、 block_id も重複している可能性があります。 実際に今回の問題は block_id が衝突していることで起こりました。 ここで一句 なぜなのか 自動生成 信じてた 衝突するよ block_id 自動生成されたblock_idはUUID的な簡単には衝突しないものだと信じていたのに、衝突したなぁ...という唖然とした気持ちが表れていますね。 これは私の推測ですが、Slack側が自動生成する block_id はブロックの内容をもとに作られていると思われます。 試しに全く同じ入力を行うと、全く同じ block_id が取得できます。 ![エラー](/assets/blog/authors/ryomm/2024-12-04-2/03.png =600x) rich_textにhogeと入力する → 毎回 RlmLN というblock_idが生成される { "type": "rich_text", "block_id": "RlmLN", "elements": [ { "type": "rich_text_section", "elements": [ { "text": "hoge", "type": "text" } ] } ] } このため、全く同じ入力がある可能性がある場合、同じくらい block_id が衝突する可能性もあると考えると良いでしょう。 解決策 さて、いくつかのブロックを結合して1つのメッセージとしてSlackに送りたいとき、メッセージ内のそれぞれのblock_idは一意にする必要があります。 インタラクティブな動作を行わない場合、 block_id を保持する必要性はあまりないので削除してしまうのが一番シンプルな解決策です。 block_id が指定されていない場合はSlack側が自動で生成してくれるため、 block_id を削除したまま送信します。 これは整形を行なっているメソッドの一部です。delete演算子でオブジェクトからblock_idプロパティを削除しています。 // client.apps.datastore.query で取得した結果のitemsが引数eventに入る // 参考: https://api.slack.com/methods/apps.datastore.query#examples function eventMessage(event) { // ... event.description.forEach((description) => { if (description.block_id) { delete description.block_id // 🐈❗️ } message.push(description) }) // ... } これで block_id の衝突を気にせずメッセージを送ることができるようになりました! ただし、インタラクティブなやり取りをしたいときには、オブジェクトから block_id を削除するのは良い方法ではないです。 その場合は、送信時にアプリ側で block_id を作成して割り当てると良いと思います。 おわりに block_id が衝突してメッセージが送れないはなしでした!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の5日目の記事です🎅🎄 KINTOテクノロジーズ株式会社のモバイル開発グループでAndroidエンジニアをしています、大沼です。 普段はモビリティサービス「my route」アプリの開発に従事しています。 本記事では、Android Automotive OSをビルドする手順とAndroid AutomotiveアプリおよびAutoアプリなど車載向けアプリの開発方法をご紹介します。 Android Automotive OSをRaspberry Piに入れて起動する Android Automotive OSとは Android Automotive は AOSPの枠組みに含まれるAndroid ベースの車載用プラットフォームであり、プリインストールされた IVI システムの Android アプリに加えて、セカンドパーティとサードパーティの Android アプリも動作します。 詳しくは公式のドキュメントをご覧ください。→  https://developer.android.com/training/cars?hl=ja#automotive-os AOSPとは AOSPはAndroid Open Source Projectの略で、Android OSを構成するすべての要素がオープンソースで公開されています。 Android オープンソース プロジェクト Googleの開発した最新のOSは一定の非公開期間を経たのち、オープンソースとして公開されます。この公開されたOSをベースに、デバイス開発元が用途に合わせた機能追加や修正を加え、自社のスマートフォンやタブレットなど各種端末にOSを搭載します。 Android Automotive OSをビルドするために準備するもの PC *後述のビルドするためのハードウェア要件を満たしている必要があります ディスプレイ *タッチモニターがベター RaspberryPi 4B MicroSD 16GBあればいいはず MicroHDMI-HDMIケーブル ビルドするためのハードウェア要件 OS : Ubuntu 22.04 Intel Gold 6226R (16コア、32スレッド) 16 GB 以上の RAM HD: 1TB * 注意: Windows または MacOS 上でのビルドはサポートされていません。 AWS EC2で環境つくってビルドしようとしたけど無料枠で上記スペックは用意できないので諦めました。 ビルド環境の構築 ビルドに必要なツールをインストールします。 sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig Repoとlocal_manifestの追加 Android OSは多くのソースコードの群で構成されています。 RepoはAndroid ソースコードのチェックアウトに利用します。 コンポーネントの結合は疎結合で、それぞれが独立したGitレポジトリで管理・開発がされています。 これら多くのGitレポジトリをManifestファイルと呼ばれる管理ファイルをもとに管理するのが Repo というツールになります。 ## Repo ランチャーをインストール repo init -u [https://android.googlesource.​com/platform/manifest](https://android.googlesource.com/platform/manifest) -b android-13.0.0\_r35 --depth=1 ## local_manifestの追加 git clone [https://github.com/grapeup/​aaos\_local\_manifest.git](https://github.com/grapeup/aaos_local_manifest.git) .repo/local\_manifests .repo/local_manifests/manifest_brcm_rpi4.xml の46行目 <!-- FFmpeg --> 以下にdav1dを追記 Added missing dav1d library in the local manifest by jijith700 · Pull Request #5 · grapeup/aaos_local_manifest · GitHub ## local_manifestに不足しているdav1dライブラリを追加 <!-- FFmpeg --> <project path="external/dav1d" name="raspberry-vanilla/android_external_dav1d" remote="github" revision="android-13.0" /> コンパイル . build/envsetup.sh lunch aosp_rpi4-userdebug make bootimage systemimage vendorimage -j$(nproc) イメージの書き込みとデプロイ MicroSDをクリーンします。 sudo umount /dev/sdb* sudo wipefs -a /dev/sdb* sudo wipefs -a /dev/sdb 次にMicroSDに4つのパーティションテーブルを作成しイメージ書き込みします。 MicroSDに書き込むイメージは boot.img , system.img , vendor.img の3つです。 sudo dd if=boot.img of=/dev/sdb1 bs=1M というコマンドで書き込みできると思ってトライしましたが、手順が多く難しかったので GParted というパーティション編集ソフトを使いました。 Android Automotive OSの起動 MicroSDをRaspberry Piに刺して起動します。Raspberry Piにはタッチモニターを接続しておくとマウスがなくても操作が可能なので便利です。 私は手持ちのタッチモニターがなく、泣く泣くPC用モニターに繋げています。 Android Auto や Android Automotive OS で動く車載向けアプリを開発する 次にAndroidで車載アプリを実装、デバッグする上で基礎となるところをご紹介します。 Android Autoはスマートフォンと連携して車載ディスプレイにアプリを表示するのに対し、 Android Automotive OSは車載システム自体にAndroidが組み込まれており、アプリを直接インストールできます。 今回は経路案内アプリを試しに実装しました。 以下開発環境はMacです。 サポートされるアプリのカテゴリと対応するAndroidのAPI カテゴリ 説明 対応する Android API メディア 音楽、ポッドキャスト、オーディオブック向けのアプリ MediaBrowserService を使用して、コンテンツのブラウジングや再生制御を行います。 MediaSession を使用して、再生状態やメタデータをシステムに通知します。 ナビゲーション 音声案内や視覚ガイドによるターンバイターンの道案内 CarAppLibrary の NavigationManager を使用して、ナビゲーションの開始、終了、 目的地設定、 ターンバイターンの案内などを制御します。 ポイント・オブ・インタレスト (POI) 駐車場、EV充電スポット、ガソリンスタンドなどの場所を見つけるアプリ PlaceClient を使用して、場所の検索、詳細情報の取得、 プレイスオートコンプリートなどの機能を実装します。 CarAppLibrary の PlaceListMapTemplate を使用して、POI を地図上に表示します。 メッセージング( Android Auto のみ) 音声入力によるハンズフリーメッセージの返信 CarAppLibrary の MessagingManager を使用して、メッセージの送受信、音声入力、 テンプレートメッセージの送信などを制御します。 ゲーム 駐車時のエンターテイメント用アプリ CarAppLibrary の ScreenManager を使用して、駐車時にゲーム画面を表示します。 InputManager を使用して、ゲームのコントロール入力を受け取ります。 ブラウザ & ビデオ ブラウザの統合やビデオ再生機能(AAOS特有、 駐車中に使用されることが多い) CarAppLibrary の WebTemplate を使用して、Web コンテンツを表示します。 VideoTemplate を使用して、ビデオコンテンツを再生します。 これらのテンプレートは、 駐車時にのみ使用することが推奨されます。 補足 公式ドキュメント の対応表の要点を抜粋しました。 毎年新しいカテゴリが追加されているため、まだアプリを広くリリースできない場合でも、将来的にはリリースできるようになる可能性があります。 CarAppLibrary は、Android Auto および Android Automotive OS アプリ開発のための Jetpack ライブラリです。 PlaceClient は、Google Places API を使用するクライアントです。 Desktop Head Unit (DHU) DHUとは? Android Autoの環境をデスクトップでエミュレートするツールです。 実際の車載端末を使わずに、車内体験をシミュレーションできます。 なぜDHUを使うのか? アプリが車載環境でどのように動作し、表示されるかをテストできます。 UI/UXが運転者の注意を逸らさないようにガイドラインに準拠しているかをデバッグし確認できます。 DHUを起動する DHUを起動する手順を行うために以下が必要です。 Macbook Android デバイス SDK ManagerでAndroid Auto Desktop Head Unit Emulatorをインストールする Library/Android/sdk/extras/google/auto に desktop-head-unit があることを確認する desktop-head-unit に権限を与えます chmod +x ./desktop-head-unit Android デバイスの同じポート番号にソケット接続を転送します。 adb forward tcp:5277 tcp:5277 Android デバイスでAutoの設定を開きます [アプリ] > [Android Auto] > [詳細設定] > [アプリ内のその他の設定] をタップします。 バージョンと権限情報を10回ほどタップして開発モードにします。 起動します ./desktop-head-unit --usb Hostについて Android Auto や Android Automotive対応車で作成したアプリを動かすとき、アプリは車と直接やりとりする訳ではありません。このときの接続先はAndroid デバイスに入っている Android Auto アプリです。 DHUのインストール手順で、USB接続した実機と接続する必要があるのはこのホストの役割をする、Android Autoアプリと連携する必要があるからです。 Android Auto アプリはホストと呼ばれ、全ての Auto 対応アプリはこのホストとやりとりします。 もし Android Automotive 対応の車で動かす場合は車載器に OS が入っているので、Android Automotive がホストになります。 ライブラリ CarAppLibrary は、Android Auto および Android Automotive OS アプリ開発のための Jetpack ライブラリです。 CarAppLibrary を使用して構築されたアプリは、Auto または Automotive 上で直接実行されるのではなく、ホストアプリを介して動作します。 プロジェクトレベルbuild.gradleにCarAppLibraryのバージョンを宣言します。 buildscript { ext { car_app_library_version = '1.4.0' } } dependencies { ... implementation "androidx.car.app:app:$car_app_library_version" ... } サービス、セッションの追加 CarAppServiceを継承したクラスを追加します。 ホストによってバインドされるCarAppServiceを拡張する必要があります。 インテントフィルターで、自動車アプリのカテゴリとして androidx.car.app.category.POI を宣言する必要があります。 <service android:name="com.example.places.carappservice.PlacesCarAppService" android:exported="true"> <intent-filter> <action android:name="androidx.car.app.CarAppService" /> <category android:name="androidx.car.app.category.POI" /> </intent-filter> </service> CarAppService抽象クラスは、 onBind や onUnbind などのオーバーライドはできません。ホストアプリとの適切な相互の運用はライブラリがよしなにやってくれてます。 createHostValidator と onCreateSession を実装するだけです。 createHostValidator で返すHostValidatorは、CarAppServiceがバインドされるときに参照され、ホストが信頼されていることを確認し、ホストが定義したパラメータと一致しない場合にバインドが失敗するようにします。 ALLOW_ALL_HOSTS_VALIDATOR は検証でのみ使えるHostValidatorです。 class PlacesCarAppService : CarAppService() { override fun createHostValidator(): HostValidator { return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR } override fun onCreateSession(): Session { return PlacesSession() } } PlacesSessionクラスを追加します。 class PlacesSession : Session() { override fun onCreateScreen(intent: Intent): Screen { return MainScreen(carContext) } } Template 決まったテンプレートの中から選んでガイドラインに沿って実装する必要があります。 自動車向けアプリはドライバーに最適なUIでなければいけないので、UI UXが限定的になってきます。 参照元:公式のテンプレートのドキュメント また、マップを表示するテンプレートにアクセスするために使用するパーミッションを追加します。 <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" /> 場所情報をリストアップする 起動したら場所のリストアップをします。 UIはComposableで実装可能です。 CarAppLibraryの Screen を継承した MainScreen を追加します。 場所の一覧とマップを表示するため、 onGetTemplate で PlaceListMapTemplate を返します。TemplateはBuilderデザインパターンで実装されています。 一覧表示するアイテムを setItemList にて渡しTemplateをビルドし返します。 一覧表示するアイテムの構築には ItemListBuilder を使います。 class MainScreen( carContext: CarContext, ) : Screen(carContext) { override fun onGetTemplate(): Template { val placesRepository = PlacesRepository() val itemListBuilder = ItemList.Builder() .setNoItemsMessage("No data") placesRepository.getPlaces() .forEach { itemListBuilder.addItem( Row.Builder() .setTitle(it.name) // リスト内の各項目は、タイトルまたはテキスト行にDistanceSpanを追加する必要があります。 .addText( SpannableString(" ").apply { setSpan( DistanceSpan.create( Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS), ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE, ) }, ) .setOnClickListener { screenManager.push(DetailScreen(carContext = carContext, placeId = it.id)) } .setMetadata( Metadata.Builder() .setPlace( Place.Builder(CarLocation.create(it.latitude, it.longitude)) .setMarker(PlaceMarker.Builder().build()) .build(), ) .build(), ).build(), ) } return PlaceListMapTemplate.Builder() .setTitle("Places") .setItemList(itemListBuilder.build()) .build() } } 場所の詳細情報を表示する PaneTemplateを使って詳細画面を実装します。 class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) { private var isFavorite = false override fun onGetTemplate(): Template { val place = PlacesRepository().getPlace(placeId) ?: return MessageTemplate.Builder("Place not found") .setHeaderAction(Action.BACK) .build() val navigateAction = Action.Builder() .setTitle("Navigate") .setIcon( CarIcon.Builder( IconCompat.createWithResource( carContext, R.drawable.baseline_navigation_24 ) ).build() ) .setOnClickListener { carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) } .build() val actionStrip = ActionStrip.Builder() .addAction( Action.Builder() .setIcon( CarIcon.Builder( IconCompat.createWithResource( carContext, R.drawable.baseline_favorite_24 ) ).setTint( if (isFavorite) CarColor.RED else CarColor.createCustom( Color.LTGRAY, Color.DKGRAY ) ).build() ) .setOnClickListener { isFavorite = !isFavorite // 画面の状態の更新を拾えるように、`onGetTemplate`を再度呼び出すようにinvalidate()をコールする invalidate() }.build() ) .build() return PaneTemplate.Builder( Pane.Builder() .addAction(navigateAction) .addRow( Row.Builder() .setTitle("Coordinates") .addText("${place.latitude}, ${place.longitude}") .build() ).addRow( Row.Builder() .setTitle("Description") .addText(place.description) .build() ).build() ) .setTitle(place.name) .setHeaderAction(Action.BACK) .setActionStrip(actionStrip) .build() } } アプリの起動 他のアプリを起動しようとするとエラー Caused by: androidx.car.app.HostException: Remote startCarApp call failed ナビゲーションを開始しようとして(startCarAppをコールする場所)エラーが発生する可能性があります。 その場合、ナビゲーションアプリがインストールされていないことが原因です。 エミュレータ上のPlayストアで探してもらえたらナビゲーションアプリはすぐ見つかります。 アプリで得られる車両プロパティ 検証はまだしていませんが、以下が取得できるとのことです。エミュレータで設定値を変えることができるのかもしれません。 参照元 速度情報 (Vehicle Speed) 車両の現在の速度を取得できます。通常は km/h で提供され、速度制限や運転支援機能に基づいたアクションに使用されます。 燃料レベル (Fuel Level) ガソリン車であれば、タンク内の燃料残量を取得できます。これはアプリで「燃料が少ない」警告や最寄りのガソリンスタンドの提案などに使用されることがあります。 バッテリー残量 (Battery Level) 電気自動車(EV)やハイブリッド車の場合、車両バッテリーの状態をモニタリングできます。充電状況やバッテリー残量を表示するために利用されます。 ドアステータス (Door Status) 各ドア(フロント、リア、トランク、フード)の開閉状況を取得できます。ドアが開いている場合に通知したり、閉じ忘れを防ぐアラートを設定できます。 ライトの状態 (Light Status) 車両のライト(ヘッドライト、ハイビーム、フォグライトなど)のオンオフの状態を取得できます。これにより、夜間モードの切り替えやドライバーへのフィードバックが可能です。 エンジンの状態 (Engine Status) エンジンのオンオフやアイドリング状態を取得できます。アプリケーションは、エンジンがオフのときに特定の操作を制限できます。 パーキングブレーキの状態 (Parking Brake Status) パーキングブレーキがかかっているか、解除されているかの状態を取得できます。これにより、駐車中のインタラクションやアプリ機能を制御できます。 ギアの位置 (Gear Position) シフトレバーの位置(パーキング、リバース、ニュートラル、ドライブなど)を取得できます。これにより、バックカメラの自動起動やギアに基づいたインターフェースの切り替えを行うことが可能です。 タイヤの状態 (Tire Pressure) タイヤの空気圧などの情報を取得できます。これにより、低圧の警告やメンテナンスのアラートを通知できます。 外部温度 (External Temperature) 車外の温度を取得でき、天候や走行条件に基づいたインターフェースや運転者への通知に利用できます。 座席センサー (Seat Occupancy Status) 各座席の乗員の有無やシートベルトの装着状況を取得します。安全のため、シートベルト未装着の警告を表示する場合に使用されます。 ウィンドウステータス (Window Status) 各窓の開閉状況をモニタリングできます。例えば、運転終了時にウィンドウが開いたままの場合、通知を出すことができます。 HVAC(エアコン)の状態 (HVAC Status) 車両の空調システム(暖房、冷房、風量、風向)の設定や状態を取得できます。これにより、快適な車内環境をアプリで制御できます。 位置情報 (GPS Location) 車両の現在の GPS 位置情報を取得できます。これにより、ナビゲーションアプリや場所ベースのサービスが利用可能です。 ワイパーの状態 (Wiper Status) ワイパーの作動状態を取得できます。天候や視界状況に基づいたUIの調整に役立ちます。 最後に 最後まで読んでいただきありがとうございます。 Android Automotive OS 素人がクローンとビルドして起動できるくらいに、Androidのオープンソースは品質維持され、簡単でした。ですが、要求されるPCスペックは高いです。弊社エンジニアに世界には最初からAndroid Automotive OSがインストールされた基盤が売ってるよと教えてくれ、早く言ってよ〜と思いましたが、OSを起動できた時は感動しました。 AutoおよびAutomotiveアプリ開発について 自動車向けアプリの実装はどういったものか、概観を掴む目的のため大雑把な記事になってしまいましたが、手順が少なく実装できることがわかりました。 Hostアプリの概念やエミュレータ起動の手順がやや面倒なところがあります。 自動車向けアプリの開発はUIのカスタマイズ性が無いぶん、より「何ができるアプリなのか」を洗練することに醍醐味があるかもしれません。 今後、自動運転が普通に乗れる未来がくれば、運転者もゲームができたりカテゴリが増える未来が来るかもしれませんね。 おまけ 運転について幼い頃を振り返り、 弊社の先輩の記事 にインスパイアされ音楽生成AIで音楽を作ってみたのが以下ですが、エモくていい感じでした。 ここまで聴いてくれるのは同僚くらいかと思います。感想お待ちしております。 https://soundcloud.com/numami-775711983/5qclozsqk1mz
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の4日目の記事です🎅🎄 メリークリスマス✌️🎅 KINTOテクノロジーズ(以下KTC)で my route(iOS) を開発しているRyommです…が、今回は幻のbot職人 Ryommとして、学びの道の駅プロジェクトと共同で開発した超エキサイティングなSlack bot「まなびぃ」を紹介します。 まなびぃとは 社内の勉強会やイベントを収集し、集めたデータを活用するための超エキサイティングなSlack botです。 イベント関係のすべてを内包しています。 ![まなびぃ](/assets/blog/authors/ryomm/2024-12-04/01.png =200x) まなびぃの主な役割としては、以下の2つがあります。 イベントの検索 新規イベントの登録・周知 まなびぃに新規立ち上げイベントを入力すると、然るべきチャンネルにイベントを周知します。 また、ユーザは上記のチャンネルをウォッチしたり、まなびぃに問い合わせることで社内イベントにアクセスできます。 直接の関係者じゃなくても情報を得られる まなびぃの活用に関しては別日の【学びの道の駅シリーズ】にて言及がある(はず)なので、本記事ではまなびぃ周辺の技術に関して紹介します。 まなびぃの技術 まなびぃはSlack CLIを使用して作成しています。 https://api.slack.com/automation/quickstart Slack CLIは、Slack側でDataStoreを立ててくれたりなど、インフラを構築する手間が省けるところが気軽です。 また、開発用の環境もSlack CLI側で作れるところも良いですね。 まなびぃのざっくりとした構成は以下のようになっています。 トリガー まなびぃには3つのトリガーが生えています。 機能 トリガーの種類 イベントの追加 linkトリガー イベントの削除 linkトリガー イベントの検索 eventトリガー ![リンクトリガー](/assets/blog/authors/ryomm/2024-12-04/03.png =500x) ![リンクトリガーフォーム](/assets/blog/authors/ryomm/2024-12-04/04.png =500x) ![eventトリガー](/assets/blog/authors/ryomm/2024-12-04/02.png =500x) Slackのワークフローのトリガーには4種類あります。 トリガー名 説明 linkトリガー 作成するとURLが発行され、Slack上でそのリンクが押されたら実行(Slack以外では無効) scheduledトリガー 時間で実行 eventトリガー メンションやリアクションきっかけで実行 webhookトリガー 特定のURLがPOSTリクエストを受信したときに実行 https://api.slack.com/automation/triggers まなびぃでは基本的にユーザーは検索機能を使用し、イベント運営者のみが追加・削除機能を使用する想定をしています。 そのため、うっかり間違えてイベントを追加・削除されてしまわないようにトリガーの種類を分けています。 またイベントの登録が完了すると、botを呼び出したチャンネルと登録を周知するチャンネル( #notice-new-event )の2つに通知されます。 ![通知](/assets/blog/authors/ryomm/2024-12-04/05.png =500x) こうすることで、作成したチャンネルに関わらず新規に立ち上がったイベントを知ることができます。 もちろん、興味があるキーワードでまなびぃに問い合わせることでも情報を得ることができます。 Block Kit SlackはBlock Kitというフレームワークを使ってリッチなビジュアルのメッセージを作成できます。 以下のBlock Kit Builderというツールを使って体験できるので、使ったことがない方は試してみてください。 https://api.slack.com/tools/block-kit-builder イベント情報の取得結果もBlock Kitを使用してみやすくしています。 https://api.slack.com/block-kit ![Block Kitを使ったメッセージ](/assets/blog/authors/ryomm/2024-12-04/08.png =500x) 参照ページのリンク先がSlackチャンネルへのリンクや、Confluenceページ、キックオフ時の動画など、ものによってはとても長くなりノイズとなってしまうため、「詳細」ボタンのリンクとして設定しています。 文字列だけでなくリンクを含ませたいなどの要望もあり、説明テキストのフィールドはrich_text型を採用しています。 🕺Slack bot開発Tipsのコーナー🕺 送信時にBlock Kitを使うかどうかははじめに決めておく まなびぃはstring型で少し運用してからrich_text型に変更することにしたのですが、string型とrich_text型は互換性がないためDataStoreのマイグレーションが一筋縄ではいきません。 さらに、登録済データもrich_textになったなら改行やリンクを含めたい!など諸々の事情を踏まえた判断の上、1回DataStoreのデータを吹き飛ばす荒技を行いました。超エキサイティング! 決められるなら、はじめにBlock Kitを使うかどうかを判断しておくと苦労せずに済みます。 https://api.slack.com/automation/datastores block_idの扱いに気をつける 詳細はこちらの記事へ: 【Slack CLI】block_idが衝突してSlackにメッセージを送れない Workflowはビルド時しか実行されない テストを考えたとき、UUIDを渡すようなメソッドを作成する際はidをメソッド外から渡すようにしたいです。 const addEventFunctionStep = AddEventWorkflow.addStep( AddEventFunction, { id: crypto.randomUUID(), // メソッドの呼び出し側でIDを生成したい title: formData.outputs.fields.title } ) しかし、Workflowの定義部分はビルド時のみ実行され、その後の呼び出し時にはfunctionの中のみが実行されます。 そのため、UUIDの生成など、処理のたびに実行されてほしいコードはメソッドの中に含めるようにします。 ドキュメントを見つけるのが大変すぎる 基本的にSlack APIのドキュメントがそのまま適用できることが多いです。 特に、私はtriggerのinputや、フォーム、DataStoreでどんな型が使えるのか彷徨いました。 そんなあなたにはこのドキュメントで万事解決です! https://api.slack.com/automation/types まなびぃを支える技術 まなびぃは一応インナーソースという扱いであり、開発環境もかなり整えてあるのでご紹介します。 CI/CD 【テスト】 Slack CLIはDenoで動いているため、 deno test でテストを実行できます。 name: 🧪 Slack App Test on: pull_request: types: [opened, synchronize, reopened, ready_for_review] jobs: build: runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Install Deno runtime uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Install Slack CLI if: steps.cache-slack.outputs.cache-hit != 'true' run: | curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash - name: Test the app run: | cd app/ deno test --no-check 【デプロイ】 手元のコンソールで slack auth token コマンドを実行すると xoxp- からはじまるサービストークンが取得できます。これをGitHubのシークレットに登録しておきます。 ワークフローの定義は以下のとおりです。 name: 🏃‍➡️ Slack App Deploy on: push: branches: [ main ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Install Deno runtime uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Install Slack CLI if: steps.cache-slack.outputs.cache-hit != 'true' run: | curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash - name: Deploy the app env: SLACK_SERVICE_TOKEN: ${{ secrets.SLACK_SERVICE_TOKEN }} run: | cd app/ slack deploy -s --token $SLACK_SERVICE_TOKEN これで通常のアプリのコードの更新はデプロイできるようになりました。ただし、トリガーの更新とデータストアの構成変更は自動化できないため、これらのデプロイについては手元で実行する必要がある点に注意です。 Issue Template バグや機能リクエスト、質問などを受け付けられるようにテンプレートを用意しています。 正直ほぼ使われてはいないですが、OSSっぽくて気に入っています。 Project GitHub Projectを使っています。 社内のプロジェクトでは基本Jiraを使用していますが、まなびぃではConfluenceを使っていないこともあり、GitHubに寄せた方が便利なので寄せてます。(あとGitHub Projectの良さを体験して欲しかった) というわけで、全体的にGitHub上で完結するようにしてみました。 私の所属はモバイル開発GなのでSwiftやKotlinがメインで、TypeScriptを書くとなるとハードルが高く感じられる方が多いです。また、部署外にこんなプロジェクトがあるということをアピールするのが難しいためインナーソースプロジェクトとしてはまだまだ課題が多いのが現状です。 まなびぃが成長して大きくなったら、いつか誰かコントリビュートしてくれるかな…と願いながら開発環境を整えています。 おわりに まなびぃを紹介しました。 まだまだ生まれたてのまなびぃですが、KTCのカルチャーと共に成長していければと思います...!
アバター
こんにちは。プラットフォームGのPlatformEngneeringチームでPlatformEngineeringの考え方をベースにツール周りの開発・運用・展開の役割(とチームリーダーのようなことをしている、最近はスクラッチ開発側も担当するようになったんでスクラムとかプログラミング言語周りにひーひー言っている) 島村 です。 この記事は KINTOテクノロジーズアドベントカレンダー2024 の4日目の記事です🎅🎄 背景 SBOM(Software Bill of Materials)というものがあります。 2024年に 経済産業省がセキュリティ対策に役立てましょう というのも言ってますし、2021年にアメリカでは標準で作ろうというお話にもなっております。とはいえ、そのために新しいSaaSとかOSSとか色々と導入するのはハードルが高い!と考えてしまうかもしれません。 KTCでは、OSSを活用してCICDのパイプラインに組み込んで、標準的にSBOMを作成して管理しています。 DevSecOpsの文脈で、SBOM作成に合わせて 脆弱性スキャン EOLの検査 も行っています。 「ミニマムスタート」と、その改善の事例としてご紹介します。 利点と欠点 何を使ってたっけ?が見えることが一番! ちょっと前だと、log4jの脆弱性対応が記憶にあるとおもいます。弊社でも使ってないよね?という調査依頼が出されました。当時は設定ファイル見たりとか暫定の回避設定を共通で入れるように周知しましたが、今なら検索すれば一発でわかります。 利点 無料 でEOL/SBOM管理が始められる!👌 有償だと Yamory(Cloudサービス) とか BlackDuck があります SBOM生成だとMicrosoftが提供している SBOM-TOOL もあります 有償ソフトウェアとかSaaSは申請とか諸々の手間が… 使っているツール群はGitHubActionsのActionとして準備されているので、使いやすい。 ローカルでも動かせるので、色々とユースケースとして考えることができそう 欠点 endoflifeなど有志に支えられているものが多いです xeol/syftともに開発が進んで頻繁にバージョンアップされていて、ファイルの不整合が起きることがたまに ツール一覧 名称 機能 概要 Syft SBOM生成 Anchore が提供しているファイルやコンテナイメージからSBOMを生成するソフトウェア。SBOMの標準形式であるCycloneDXもSPDXも対応していますが、標準だとSyft独自形式になるのでご注意を。 KTCではCycloneDXのXMLで出力しています。JSONだとバージョン変わったりすると構成がかなり変わって取込時の考慮が増えるため XEOL EOLスキャナ XEOL が提供しているEOLが含まれているかどうかを判断するスキャナ。内部構成はSyftをベースに、 endoffile.date の情報とマッチングをかけて判断している。主だった言語とOSは対応していることと、Issue上げたら結構早く修正してくれる感じがよいです。SaaSも提供していますが、今回はOSSを利用します。 Trivy 脆弱性スキャナ aqua が提供している脆弱性スキャナ。Anchoreも Grype という脆弱性スキャナを出しているので、Syftと組み合わせるならそっちが良いかと思いましたが、脆弱性検知をした際のCICD上の挙動がTrivyの方が好ましい(表示とか)と判断してこちらを。ファイル、リポジトリ、コンテナイメージ、SBOMからなどスキャンできる対象は多いので使える範囲は多いと思います。SBOMも生成できますが、XEOLの中身がSyftなので、相性を考慮して、今回は脆弱性スキャンだけ使います GitHubActions CICDツール GitHubに包含されているCICDツール。KINTOテクノロジーズではGitHubActionsを使用してアプリケーションのビルド・リリースなどを実行しています。SBOM管理では、コンテナ作成タイミングのワークフローにSBOM生成を組み込んでいます。 CMDB(内製) CMDB Configuration Management Database。構成管理のデータベースのこと。リッチな機能までは不要でしたので、KINTOテクノロジーズではCMDBを内製しています。最近はリポジトリ情報管理、EOL情報やSBOMのパッケージも取り込んで検索できるように機能追加されました。 ワークフロー概要図 パイプライン抜粋(GitHubActions) :::message コンテナビルドやPushについては、タイミングは個々で判断だと思いますので除外しています。 Trivyの脆弱性スキャンの対象をImageにしているので、この抜粋の前にビルドしている想定です。 ::: 基本的にはアプリケーションビルドではなくShipのタイミングで実施しましょう。 SBOMファイルはバージョン管理をしてもよかったんですが、最新のみで問題はないかなと考えて、上書きするようにしています。ビルドに組み込むと、実際にワークロードにあるコンテナのSBOMじゃないものになるので、注意が必要です。 パイプラインでXEOLを2回呼び出しているのは、GitHubActionへのログ表示とファイル生成のためです。モードが別のようで、一括でやってくれなかったんで、分離することになりました。 ## Trivyでの脆弱性診断 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: '${{ ImageName }}:${{ IMAGETAG }}' format: 'table' exit-code: '0' ## 脆弱性があってもImageBuild/Pushをする場合はここを「0」にする ignore-unfixed: false vuln-type: 'os' ## Javaなども含みたい場合は「library」を追加する severity: 'CRITICAL,HIGH' ## SYFTでのSBOM作成 - name: Run Syft make sbom files(format cyclone-dx) uses: anchore/sbom-action@v0 with: image: '${{ ImageName }}:${{ IMAGETAG }}' format: cyclonedx artifact-name: "${{ github.event.repository.name }}-sbom.cyclonedx.xml" output-file: "${{ github.event.repository.name }}-sbom.cyclonedx.xml" upload-artifact-retention: 5 ## Artifactの有効期限 ## XEOLでのSBOMからのEOLライブラリ検知(WF中での表示)とファイル作成 - name: Run XEOL mw/sw EOL scanner from sbom file uses: noqcks/xeol-action@v1.1.1 with: sbom: "${{ github.event.repository.name }}-sbom.cyclonedx.xml" output-format: table fail-build: false - name: Run XEOL mw/sw EOL scanner from sbom file and Output file uses: noqcks/xeol-action@v1.1.1 id: xeol with: sbom: "${{ github.event.repository.name }}-sbom.cyclonedx.xml" output-format: json fail-build: false ## AWSのクレデンシャルの設定(SBOM) - name: AWSクレデンシャル uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ S3_ACCOUNT_ROLE }} aws-region: ${{ AWS_REGION }} ## SBOM/EOLを管理するTeamのS3Bucketに保存する ## イメージ名やリポジトリ名でS3の中で階層分けをするので、Cutで抽出。 - name: SBOM and EOL file sync to s3 bucket run: | ECRREPOS=`echo ${{ ImageName }} | cut -d "/" -f 2,3` echo $ECRREPOS aws s3 cp ${{ github.event.repository.name }}-sbom.cyclonedx.xml s3://${{ s3-bucket-name }}/${{ github.event.repository.name }}/$ECRREPOS/sbom-cyclonedx.xml aws s3 cp ${{ steps.xeol.outputs.report }} s3://${{ s3-bucket-name }}/${{ github.event.repository.name }}/$ECRREPOS/eol-result.json 改善されたことと今後 自分の部署で、AWS-COREのライブラリを使っているかどうかを確認した結果 自分の部署で、EOLがあるかどうかを確認した結果 KTCではCMDBへSBOM/EOLの一覧を取り込んだため、 自身のプロダクトに何のライブラリが使われているか? EOLになっているものはないか? が上記の通り、検索して確認できるようになりました。内製CMDBでは直近でEOLになるライブラリ・パッケージも出してくれるので、事前対応も容易です。 SBOM管理の第一歩は、SBOMファイルなどをJSONで出力して、jqで成形してエクセル管理でもいいと思います。 今後は、定期的にEOLを含んだプロダクトへ、チケットで対応依頼を行う運用を開始したいと考えています。 セキュリティチームとか色々と巻き込んでいくつもりです。 所感 EOL管理周りのツールは調べましたが多くはなく、SBOM管理とかソフトウェア管理、資産管理の延長として提供しているものが多い印象です。 OSSやライブラリを使った開発も多いので、EOLを定期的に見れる改善は脆弱性対策としてもかなり有用かと感じています。ライブラリも1年~2年でEOLとなって、バージョンの追従をする必要が多いと思いますし。 見えるというのは、"気付かない"・"見なかったことに"という対策には良いと思いますので、O11yと同じでまずは「みえる」を目標にしましょう。 実際には、修正依頼も含めた運用まで建付けないと、実効性はないのかもしれません。 が、「ミニマルから始める」と題して、まずは作ろう!始めよう!ということで、今回の記事を書きました。 さいごに PlatformEngneeringチームは、社内向けの横断ツールを統制して必要なものを開発しています。 Platformグループの他チームが作ったものを受け入れたり、必要なものを新規作成や既存のものをマイグレーションしたりしています。MSPチームの定例作業の自動化とか、CDKにも手を出そうとしてるので、ManagedService以外にもプログラミングも行い始めました。 こういった活動に少しでも興味を持ったり話を聞いてみたいと思った方は、お気軽にご連絡いただければと思います。 @ card
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の4日目の記事です🎅🎄 はじめに こんにちは。モバイルアプリ開発グループでiOSチームのチームリーダーをやっている中口と申します。 普段の業務では、 KINTOかんたん申し込みアプリ (以下「申し込みアプリ」とします。) Prism Japan( スマホアプリ版 / 最近リリースされたばかりのWeb版 ) のiOS開発を担当しています。 本記事では、担当している申し込みアプリをSwiftUI化することになりましたので、その過程や方針について書いていきます。 こちらのブログは iOSエンジニアの方 SwiftUIのアーキテクチャに興味のある方 チームにおいてSwiftUIを導入することになった方 などに読んでもらえたら嬉しいです。 また、本記事は先日開催されました KINTOテクノロジーズ×RIZAPテクノロジーズ Mobile Tips での発表を元に執筆させていただきます。 本記事では、ソースコードなどを用いた具体的なSwiftUI化の実例などは記載しておりません。 一方で、チームとしてどのようにSwiftUI化をすることになったのか、という過程の部分を中心に書いております。 チームでのSwiftUI化に悩みがある方などの一助になれば幸いです。 ファーストリリースの時のアーキテクチャ選定 申し込みアプリは2023年9月にリリースされました。 その際に選定された主なアーキテクチャは UIKit VIPER Combine でした。 2023年の3月ごろより開発が始まった本アプリですが、2023年に新規でiOSアプリを開発する場合「UIKit」にするか「SwiftUI」にするか結構迷いどころですよね?? この時期であればSwiftUIによる開発の方が世間的にもメジャーになっている感覚がありました。 ですが申し込みアプリではUIKitを選定しました。 その理由としては、本アプリが 短納期 であったことと、メンバーに SwiftUIに明るいメンバーがいなかった ことが挙げられます。 新技術を使ってリスクを犯すより、慣れた技術を使って安定的にリリースすることを重視したためです。 ではファーストリリース後、どのような流れでSwiftUI化に至ったのか、次に書いていきます。 # SwiftUIへ移行したい:第1波 2023年のファーストリリース後、バグ改修や小規模アップデート、リファクタリングなどを粛々と進めていましたが、その中で何か新しいことに取り組んでみたいよね、という話が出ました。 いくつかの候補が上がりましたが、その中で人気が高かったのがSwiftUIでした。 2024年の3月ごろにswiftUI化1回目の波が来る チーム内にてSwiftUIを進めるかどうか議論したところ下記のような意見がありました。 ●やる理由 SwiftUIへの興味 ●やらない理由 チーム内にSwiftUIに精通したメンバーが不在 SwiftUIへの明確は必要性を感じない(その当時の時点で) チームメンバーの入れ替えなどもあり新メンバーも多く、SwiftUIに着手している人的、時間的リソースが無い 私自身がチームリーダーとしてSwiftUI化をやり切れる自身がなかった この時はやらない理由が多かった こういった理由からSwiftUI化はまだ先と判断いたしました。 SwiftUIへ移行したい:第2波 それから、約半年が経ちました。 1on1などをやっていると、SwiftUIをやってみたいという意見はまだまだ多く、改めてSwiftUI化について議論することになりました。 2024年の8月ごろにSwiftUI化2回目の波が来る 第1波とは状況も変化しておりまして、再度議論したところ下記のような意見となりました。 ●やる理由 SwiftUIへの興味が徐々に情熱に変わってきた SwiftUI有識者が加入した(社内的な組織変更により) 当時の新メンバーが中心メンバーになっていき、チーム全体として人的、時間的リソースが確保できると感じた ●やらない理由 本当にSwiftUI化に着手して大丈夫かという不安 この時はやる理由が多かった これらの状況からSwiftUI化を進めることと判断いたしました。 目的を間違えてはいけない このようにチーム一丸となってSwiftUI化を進めようとなりましたが、目的を間違えてはいけないと考えています。 (×) 技術的好奇心でSwiftUI化したい、が目的となってしまってはダメ (○) 将来のメンテナンス性をあげる  ・ デファクトスタンダードに追随する  ・ Combineによる実装をしていたが、それが複雑化してきていまっているので脱却したい (×) SwiftUI化することでアプリの質が落ちてしまってはダメ (○) これまでと同水準以上の質を担保する (×) 本来のリリースタスクを後回しにしてSwiftUI化を進めるなど、作業の優先順位を間違えてはダメ (○) 追加機能はこれまでと同じペースで対応する 上記のことはしっかりと意識しつつ、チーム内でどのようにSwiftUI化を進めるか議論を進めました。 # SwiftUI化のアーキテクチャ選定 チーム内でどのようなアーキテクチャを採用したいか議論しましたが、主な意見としては下記が挙げられました。 ライブラリは使いたくない ViewModelは使いたくない 1.ライブラリは使いたくない こちらは主に The Composable Architecture(TCA) を指すのですが、ライブラリを使用するとその「アップデートを常に気にしなくてはいけない」、「サポートが終了したらどうしよう」、などの懸念点からなるべくTCAを使いたくないという意見が多かったです。 また、社内の別プロジェクトてTCAを使っているプロジェクトがあるのですが、そこでも使用感としても、「学習コストが高い」、「ライブラリのアップデートが早すぎてついていくのが大変」、「親Reducerの依存関係が強くなりすぎる」など課題を感じていたこともあり、TCAは見送りました。 2.ViewModelは使いたくない SwiftUIのアーキテクチャにViewModelを採用するかどうかは、よく議論の対象になるかと思いますが、SwiftUIはすでにbindingの機能を有しておりMVVMを使うことでむしろSwiftUIの良さが活かしきれないのではないか、という意見が多かったです。 そのため、ViewModelを使わない方針で意見がまとまりました。 # MVアーキテクチャを採用 その結果、我々のチームは**MVアーキテクチャ**を採用することになりました。 図で示すと下記のような感じになります。 MVアーキテクチャ Viewは原則Modelのみとやりとりを行います。 またAPIで取得したデータなどはモデルを介してViewに渡すようにしています。 現時点で我々はMVアーキテクチャに対して、 シンプルになり、将来的なメンテナンス性の向上につながる(Combine脱却) ライブラリに依存しない SwiftUIの機能を最大限に発揮できる このようなメリットを感じており、上記のアーキテクチャ選定の議論で出たような内容を反映できたと考えています。 SwiftUI化の導入部分における方針 また、SwiftUI化するにあたって導入部分の方針として下記どちらにするかをチームにて議論しました。 まず個別のViewをSwiftUI化する まず画面遷移に関する部分をSwiftUI化する その結果、 「まず画面遷移に関する部分をSwiftUI化する」 という方針にてSwiftUI化を進めることになりました。 その理由としては 経験的に画面遷移にまつわる部分が後々つまずくことが多かった 遷移を管理するViewがUIKitのままだと、個別のViewをSwiftUI化したのにそれを(臨時的に)UIKitにwrapするケースが多く発生する などの理由からです。 # SwiftUI化これから ここまでSwiftUI化の過程や方針を書かせていただきましたが、申し込みアプリのSwiftUI化はまだまだ始まったばかりです。 本記事を掲載する2024年12月時点ではまだ、プロダクションコードにSwiftUIのコードは一切入っていません。 現在は、「毎朝行っている朝会の中で余った時間(20分くらい)を利用する」、「毎週1時間SwiftUIに特化したMtgを行う」など、SwiftUI化のためのコミュニケーションの時間を増やしております。 その中で一部のメンバーにより、SwiftUI化を進めるためのサンプルコードを用意してもらいそちらをベースにチームメンバー全体にレクチャーしたり、チーム内で共通認識を持つためのコーディング規約を整備し始めたところです。 今後もSwiftUI化の実装が本格化したらペアプロやモブプロなども導入していき、チーム全体としてSwiftUIのレベルを上げていきたいと考えております。
アバター