TECH PLAY

KINTOテクノロジーズ

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

969

この記事は KINTOテクノロジーズアドベントカレンダー2024 の13日目の記事です🎅🎄 Impact Effort Matrix(インパクト・エフォートマトリックス)を使って社内交流を実践してみた こんにちは、KINTO テクノロジーズの技術広報グループに所属しているMayaと木下です。 はじめに 私たちは過去に神保町で社内交流会を開催し、オフィス内の交流不足を解消し、チーム間のつながりを強化することを目指しました。 実施したときの記事はこちら↓ https://blog.kinto-technologies.com/posts/2023-09-19-JimbochoISM/ このイベントでは、企画段階にImpact/Effort Matrix(インパクト・エフォートマトリックス)を活用して、催しの内容を決定しました。 これにより、イベントを盛り上げるためのアイデアを整理し、タスクの優先順位を明確にすることができました。 その結果、複数回のイベントをスムーズに運営することができました。 この記事では、その時の経験を基に、Impact/Effort Matrixを用いたイベントの企画・実践、振り返りまでのアプローチを紹介します。 本記事が、読者の皆さんのプロジェクトやイベント計画に役立てば幸いです。 ぜひ、最後までお楽しみください。 Impact/Effort Matrixについて Impact/Effort Matrix(インパクト・エフォートマトリックス)は、プロジェクトやタスクの優先順位を効率的に決定するためのシンプルかつ効果的なツールです。 このマトリックスは、タスクやアイデアを 「成果(Impact)」 と 「労力(Effort)」 の2軸で分類することで、どの項目にリソースを集中させるべきかを視覚的に判断できます。 基本構造 Impact/Effort Matrix は4つの象限で構成されます: Quick Wins(すぐに実行すべきタスク) 高い成果をもたらし、必要な労力が少ないタスク 優先的に取り組むべきです Major Projects(戦略的に取り組むべきタスク) 高い成果が期待できるが、労力も多く必要なタスク リソース計画が重要になります Fill-ins(余裕があるときに実施するタスク) 労力が少なく成果も小さいタスク 優先順位は低めです Parking Lot(避けるべきタスク) 成果が低く、労力が多いタスク 基本的に取り組むべきではありません さらに詳しくは Miroのテンプレート説明ページ をご参照ください。 なぜ導入したのか 第1回目の神保町共有会開催後、それなりに良い感想をいただくことができました。 しかし、参加者全員が一体感を持てているわけではなかったため、もっと「ワイワイ感」を出すにはどうすればいいのか?というブレインストーミングをしたかったのです。 運営メンバーで議論し合いながら、目指す目的を明確にし、やるべきタスクとやらないタスクを選定しました。 そして、優先順位をメンバーで共有するための適切な手法を探している中、メンバーが過去に利用経験のあるImpact/Effort Matrixを用いることにしました。 活用して感じたメリット 優先順位付けが明確になる: 4象限に相対的に全ての課題を配置するので、議論を進め、付箋を並べるにつれて、優先するべきタスクが視覚的に浮き上がってくる 容易に共有でき、全員の理解が一致しやすい: 上記の延長線ではありますが、何を優先すべきかを議論するきっかけにもなるため、認識をチームで合わせる時間になります 全員のコンセンサスを得ながら進めるため、コミットメントも高く、全員が自分ごとと捉えて進めやすい チームのリソースを最適化できる: 次のアクションへも全員が納得する落とし込みにもつながるため、出戻りの発生を防げます 苦労したところ・工夫したところ よくある話ではありますが、一番チームで苦労したところは意思決定の部分でした。 2回実施したImpact/Effort Matrixの初回では、意見や共通認識の課題が多く出たものの、その情報をどう整理し、次のイベントの改善に繋げるかのコンセンサスに至るまでたくさん議論を重ねる必要がありました。 整理した内容の粒度がバラバラで、やりたいことのスコープも広かったため、思うように成果を出せませんでした。 2回目では、上記の経験を踏まえて課題の粒度を揃え、目的を明確にし、スコープを絞ることで、より具体的な結果を得ることができました。 メンバーの理解が深まり、プロセスをスムーズに進めることができました。 イベントの方向性をより具体化することができて、二回目も無事に成功し、神保町共有会の三回目に向けたアクションプランを策定し、Jiraボードでタスクを見える化しました。 この時、新しいメンバーが数名加わり、初めてIEMを体験する人たちでした。 未経験者を多く含む状況ながらも、これまでの経験からスムーズに進行することができ、三回目の神保町共有会を盛り上げることができました。 Impact/Effort matrixやってみての感想 Impact/Effort Matrixの手法を理解しても、実際に自分たちの状況に当てはめて実践しようとすると、最初は本当にこれで良いのかと迷うことがあり、チーム全体に不安が広がることもありました。 しかし、違和感を覚える点や改善すべき点について、皆が意見を出し合い、それを尊重し合うことで、私たちは様々なアイデアを整理し、どのタスクを優先すべきかを明確にすることができました。 このImpact/Effort Matrixの手法が上手く機能するまでの過程では、意見の対立もありましたが、それぞれのアイデアを客観的に評価することで、全員が納得できる形で進めることができました。 イベントに向けて準備が進むにつれ、チームとしての一体感が高まり、最終的には社内交流会の参加者から高い評価を得ることができました。 終わりに 今回の記事では、神保町での社内交流会を例に、Impact/Effort Matrixを活用したイベントの企画・実践、振り返りのアプローチをご紹介しました。 初めは手探りで不安もあり、うまくできているのか自信が持てない部分もありましたが、何度か実践を重ねる中で次第に慣れ、運営メンバーが途中で増えたにもかかわらず、スムーズに運営できるようになりました。 この手法が、皆さんのプロジェクトやイベントの成功に役立つことを願っています。 皆さんのプロジェクトやイベントの参考になれば幸いです。 KINTO テクノロジーズでは、一緒に働く仲間を広く募集しています。 ご興味を持たれた方は、ぜひお気軽にご連絡ください。お待ちしております! https://hrmos.co/pages/kinto-technologies/jobs
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の13日目の記事です🎅🎄 はじめまして。 KINTOテクノロジーズでUnlimited(Android)を開発しているfabtと申します。 最近何かと話題のスクリーンショットテストを導入してみたので、導入の流れを追いながら、つまづいた点とその解決方法について紹介します。 スクリーンショットテストの導入を考えている方の参考になれば幸いです。 スクリーンショットテストとは 開発中のソースコードを基に画面のスクリーンショットを撮影し、過去のソースコードを基に同様の手順で撮影されたスクリーンショットと比較し、変更点を確認・検証するテスト手法です。(ざっくりですが・・・) 人間の目では認識しづらい1dpの差も検出してくれるため、UIに意図しない変更が含まれていないかを容易に判断できます。 DroidKaigi 2024などのカンファレンスでも多くのセッションがあり、最近話題になっていると感じます。 スクリーンショットテストライブラリの選定 カンファレンスなどでの発表も多く、弊社の他のAndroid向けアプリでも導入実績のある roborazzi を採用することにしました。 なにはともあれ実行してみる 公式 のセットアップ手順やインターネット上の情報をもとに導入し、ローカル環境で実行してみます。 ひとまず特殊なことはしていないので、さっくり進めていきます。 まずはスクリーンショットテスト実行に必要なライブラリを導入します。 バージョンカタログ(libs.versions.toml) [versions] robolectric = "4.13" roborazzi = "1.29.0" [libraries] androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } [plugins] roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } rootのbuild.gradle.ktsファイル plugins { alias(libs.plugins.roborazzi) version libs.versions.roborazzi apply false } moduleのbuild.gradle.ktsファイル plugins { alias(libs.plugins.roborazzi) } android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" } } } } dependencies { // robolectric testImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.robolectric) // roborazzi testImplementation(libs.roborazzi) testImplementation(libs.roborazzi.compose) testImplementation(libs.roborazzi.junit.rule) } これで必要なライブラリを導入できたはずです。 下記のようなテスト用クラスを作成してとりあえずローカルで実行してみましょう! import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { @get:Rule val composeTestRule = createComposeRule() @Test fun sample() { composeTestRule.apply { setContent { MaterialTheme { Surface { Text(text = "screen shot test sample") } } } onRoot().captureRoboImage( filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/sample.png" ) } } } 下記のスクリーンショット保存コマンドをAndroid Studioのターミナルから実行すれば Text(text = ・・・) のスクリーンショットがpngとして出力されるはずです。 ./gradlew recordRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_empty_roborazzi.png =750x) なんと、何も出力されませんでした。 予想外の結果です。 テストがスキップされる コマンド実行は成功しているのに、なぜかスクリーンショットの結果が出力されませんでした。 調査しつつローカルでユニットテストを実行してみると、以下のような表示がされていることに気がつきました。 ![](/assets/blog/authors/f.tsuji/2024-12-13/Test_events_were_not_received.png =750x) なぜかテストイベントを受信できておらずスキップされているようです。 たしかに、それであればコマンド成功かつ結果出力なしも納得できます。 さらに調査を進めていった結果、JUnitバージョンの競合が原因のようでした。 弊アプリでの通常のユニットテストはkotestを使用しているのですが、そちらのテストランナーはJUnit5、 roborazzi(というよりもrobolectric)のテストランナーがJUnit4であることが問題でした。 なんと roborazziのissueにも似たような問題がありました。 解決策は、上記issueにもある通り junit-vintage-engine という ライブラリ を使用することでした。 上記ライブラリはざっくりいうとJUnit4とJUnit5を共存させることができ、移行にも使われることがあるとのことです。 それでは junit-vintage-engine を導入し、再度コマンドを実行してみることにします。 まずは依存関係を追加(前段で導入した部分は割愛) バージョンカタログ(libs.versions.toml) [versions] junit-vintage-engine = "5.11.2" [libraries] junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit-vintage-engine" } moduleのbuild.gradle.ktsファイル dependencies { testImplementation(libs.junit.vintage.engine) } 再度スクリーンショット保存コマンドを実行します。今度こそ Text(text = ・・・) のスクリーンショットがpngとして出力されるはずです。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure.png =750x) 実行結果 :::message alert junit-vintage-engine を導入し、JUnitバージョンを共存させたことでテストは実行できましたが、結果は失敗でした。 ::: なにかしらの初期化に失敗 なんと実行に失敗してしまったようです。 ライブラリを追加する前は実行すらできていなかったので、ポジティブに一歩前進と捉えて愚直に解決を目指しましょう。トライアンドエラーです。 問題解決の最初の一手ということでログを見てみると、以下が出力されていました。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_log.png =750x) なにかしらの初期化に失敗しているようです。 出力されたログを確認していくと、どうも弊アプリのApplicationクラスを初期化しようとしているみたいです。 Robolectricの設定 を詳しく調査すると、以下の記載を見つけました。 Robolectric will attempt to create an instance of your Application class as specified in the AndroidManifest. Robolectricは、 AndroidManifest.xml で指定されたApplicationクラスのインスタンスを作成しようとするようです。 ログの内容と一致しました! 初期化に失敗しないよう、公式を参考に @Config アノテーションを使用しシンプルなApplicationクラスを使用するよう伝えてみることにします。 import android.app.Application import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(application = Application::class) // 追加 @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { 期待を込めて、スクリーンショット保存コマンド(割愛)をAndroid Studioのターミナルから実行します。 さすがに今度こそText(text = ・・・)のスクリーンショットがpngとして出力されるはずです。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi.png =750x) ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample.png =250x) スクリーンショットが出力されました! :::message Robolectric の設定を調査し、 @Config アノテーションを用いて使用するApplicationクラスを明示することでスクリーンショットテストが成功しました。 ::: 比較(スクリーンショットテスト)も試してみるべく、テスト用クラスを少しだけ変更しコマンドを実行してみます。 // Text(text = "screen shot test sample") Text(text = "compare sample") // textを変更 ./gradlew compareRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_compare_roborazzi.png =750x) 比較結果が出力されました! ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_actual.png =250x) 比較画像 ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_compare.png =750x) 比較結果 成功したのでここまでのまとめ 簡易的なテストクラスを作成し、無事にスクリーンショットテストを実行できました。 導入しているユニットテストやプロパティの指定など、プロジェクトによって細かい部分の調整が大変な印象ですが、 変更を目で確認できるのは明瞭かつ高速なため、可能であれば導入を検討するとアプリの品質を担保しやすくなるかと思います。 余談 弊アプリではこのタイミングでCIの実装も行っています。 GitHub Actions Workflowを使用し、スクリーンショットの保存・比較・コメントをPRにするところまで実現しています。 特別テクニカルなことをしているわけではないため割愛しますが、 公式 にあるように companion branch approach を用いて結果を保持する手法を選択しています。 (ymlファイルの統合などは行いましたが、導入する際の一般的なレベルの最適化程度です。) :::message ./gradlew recordRoborazziDebug を実行した際、同一モジュールに通常のUnitTestがあるとそちらも実行されてしまいます。 弊アプリでは独自のプロパティを定義し、UnitTestを実行するためのGradleタスクとRoborazziを用いたスクリーンショットテスト用のタスクとに分離し管理しています。 参考issue https://github.com/android/nowinandroid/issues/911 https://github.com/takahirom/roborazzi/issues/36 ::: 閑話休題。 Preview関数もテスト対象にする Composable関数を実装する際にPreview用関数も同時に作成することが多いと思います。 この記事の前半部分のように手動でテストも実装するとなると、 Composable実装 Preview実装 テスト実装 となかなか大変です・・・。 せっかくならばPreview関数をそのままスクリーンショットテストの対象にしてしまおう!というのが本項の内容です。 こちらも特に話題の中心になっている技術ですね。 なにはともあれ実行してみる 手順は概ね以下です。 Preview関数を収集する 収集した関数をスクリーンショットテストする シンプルですね。 Preview関数の収集にはさまざまな方法があると思いますが、今回は ComposablePreviewScanner を使用していこうと思います。 導入手順がまとめられていること(調査がしやすいこと)、将来的に公式でも連携・サポートされそうなこと( 執筆時点ではExperimental )が主な理由です。 では、必要なライブラリを導入していきましょう。 バージョンカタログ(libs.versions.toml) [versions] composable-preview-scanner = "0.3.2" [libraries] composable-preview-scanner = { module = "io.github.sergio-sastre.ComposablePreviewScanner:android", version.ref = "composable-preview-scanner" } moduleのbuild.gradle.ktsファイル dependencies { // screenshot testing(Composable Preview) testImplementation(libs.composable.preview.scanner) } 続いて、composable-preview-scannerのREADMEや先人たちの情報を参考にテスト用クラスを作成してみましょう。 import android.app.Application import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import com.kinto.unlimited.ui.compose.preview.annotation.DialogPreview import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @RunWith(ParameterizedRobolectricTestRunner::class) class ComposePreviewTest( private val preview: ComposablePreview<AndroidPreviewInfo> ) { @get:Rule val composeTestRule = createComposeRule() @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val fileName = AndroidPreviewScreenshotIdBuilder(preview).ignoreClassName().build() val filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/$fileName.png" // Preview関数名.png composeTestRule.apply { setContent { preview() } onRoot().captureRoboImage(filePath = filePath) } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("・・・"), // Preview関数を探すパッケージを設定する exclude = listOf() ) .includePrivatePreviews() // PrivateなPreview関数も含める .getPreviews() } @JvmStatic @ParameterizedRobolectricTestRunner.Parameters fun values(): List<ComposablePreview<AndroidPreviewInfo>> = cachedPreviews } } では、スクリーンショット保存コマンドを実行します。 ・・・したのですが。 やたらと時間がかかり1時間経っても終わる気配がありません。Preview関数の数も200弱なのでそこまでかかるかな?といった感じです。さすがにすんなりとはいかないですね。 :::message alert Preview関数をスクリーンショットテストの対象に含めたところ、テスト実行にとても長い時間がかかるようになってしまいました。 ::: テスト実行に時間がかかる 色々調べていくうちに似たような症例を見つけました。 https://github.com/takahirom/roborazzi/issues/388 1つのテストを完了するのに約5分かかり CircularProgressIndicator を削除したところ高速で実行されたとのこと。 Issueのやりとりを深掘りしていくと、どうやら無限アニメーションを含むComposableをテストしようとすると時間がかかっている模様。 解決策としては mainClock.autoAdvance = false を設定し、Compose UIとの自動同期を停止した上で手動で時間を進める方法が紹介されていました。 手動で時間を操作することで任意のタイミングでスクリーンショットを撮影できるため無限アニメーションによる影響を受けない、ということのようです。 弊アプリでもまさに CircularProgressIndicator を使用しているため、これは試すしかない・・・!と早速実装してみます。 mainClock. ・・・ の箇所をテストクラスに追加しました。 時間を止め、 1_000ミリ秒(1秒)進め、 スクリーンショットを撮影し、 時間停止を解除する。 という流れですね。 composeTestRule.apply { mainClock.autoAdvance = false // 1 setContent { preview() } mainClock.advanceTimeBy(1_000) // 2 onRoot().captureRoboImage(filePath = filePath) // 3 mainClock.autoAdvance = true // 4 } (別件ですが、画像を非同期で読み込みための coil というライブラリがありますが、そちらも 正しくテストできない可能性があるとのこと。 ただ解決方法は同様そうなのでまとめて対応できそうです。) さあ、実行です!!! ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_time.png =500x) 高速で実行できるようになったものの、失敗してしまいました。 :::message alert 無限アニメーションがボトルネックとなっていましたが、テスト時に手動で時間を操作することで解消。高速でテストを実行できるようになりましたが、結果は失敗でした。 ::: ダイアログを含んでいると失敗する 実行速度は上がったものの、失敗しては意味がありません。こういう時に頼れるのはやはりログですね。 ComposePreviewTest > [17] > snapshot[17] FAILED java.lang.AssertionError: fail to captureRoboImage Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot) Nodes found: 1) Node #534 at (l=0.0, t=0.0, r=0.0, b=0.0)px 2) Node #535 at (l=0.0, t=0.0, r=320.0, b=253.0)px Has 1 child at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow(SemanticsNodeInteraction.kt:178) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow$default(SemanticsNodeInteraction.kt:150) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:84) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:278) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:268) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage$default(Roborazzi.kt:263) at com.kinto.unlimited.ui.compose.ComposePreviewTest.snapshot(ComposePreviewTest.kt:49) 上記のようなログが複数出力されていました。2つのnodeがあるとのこと。 ログの情報で検索すると 気になるIssue を見つけました。 compose-multiplatform についてなので原因は違うかもしれませんが、たしかに失敗しているのは Dialog()Composable を使用している箇所のようでした。 諦めるしかないのか・・・と悩んでいたところ、Experimentalとしてダイアログを含むイメージをキャプチャする 関数が追加されている とのことです! Experimentalのため全てのテストに使用するのは憚られますが、テスト対象がダイアログかどうかを判定できれば成功するかもしれません・・・。 ということで DialogPreview() というカスタムアノテーションを作成し、ダイアログを含むPreviewに付与、テストクラスで情報を取得し判定することにしました。 annotation class DialogPreview() @OptIn(ExperimentalRoborazziApi::class) // 追加 @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val isDialog = preview.getAnnotation<DialogPreview>() != null // 追加 composeTestRule.apply { mainClock.autoAdvance = false setContent { preview() } mainClock.advanceTimeBy(1_000) if (isDialog) { // 追加 captureScreenRoboImage(filePath = filePath) } else { onRoot().captureRoboImage(filePath = filePath) } mainClock.autoAdvance = true } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("・・・"), exclude = listOf() ) .includePrivatePreviews() .includeAnnotationInfoForAllOf(DialogPreview::class.java) // 追加 .getPreviews() } } previewに DialogPreviewアノテーション が付与されているか(nullではないか)を判定し、ダイアログの場合はcaptureScreenRoboImage()を使用するようにしました。 実行してみます。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi_preview.png =750x) Preview関数の性格上、画像やファイル名をマスクしています。 Preview関数を読み込み、それらのスクリーンショットを保存できました! :::message ダイアログを含むComposableは、Nodeを複数持ってしまうため正しくRootを判断できずにテスト実行で失敗していました。 Experimentalではありますが、ダイアログをキャプチャできる関数を使用することで正常にテストを実行できました。 ::: スクリーンショットの比較は前段で確認済なので、そのままで問題なさそうです。 長々とお付き合いいただきありがとうございました。 最後のまとめ この記事では、スクリーンショットテストの導入の流れを追いながら、つまづいた点とその解決方法について紹介しました。 当初はライブラリを追加して実行するだけだと思っていましたが、なかなかうまくいかず、試行錯誤を通じてなんとかスクリーンショットテストを実行できるようになりました。 つまづいた点の解決方法はライブラリの追加やアノテーションの付与など、対応としては単純でしたが、そこに辿り着くまでの情報が少なく、意外と大変でした。 この記事が少しでも皆さんのお役に立てば幸いです。
アバター
この記事は KINTO テクノロジーズアドベントカレンダー 2024 の 13 日目の記事です 🎅🎄 はじめに こんにちは。KINTO ONE開発部の新車サブスク開発グループでフロントエンド開発を担当しているITOYUです。 エンジニアの皆さん、GitHub を使っていますか?我々 KINTO テクノロジーズでも GitHub を利用しています。 チーム開発をする上で Pull Request 機能を使ってコードレビューとマージを行っています。 マージの実行時にオプション指定をすることが出来るのですが、オプション指定をすることが出来るのをご存知でしょうか? 各オプションの違いの説明と、私が過去に躓いた罠について説明します。 何を説明するのか GitHub 上での Pull Request のマージオプション Create a merge commit Squash and merge Rebase and merge 罠 Rebase and merge と git rebase は一緒じゃない 自分が前に実行したオプションが次回のデフォルトになる 前提 develop ブランチと feature ブランチが存在している develop ブランチから feature ブランチを切って作業をして、develop ブランチに対して Pull Request を作成する develop ブランチと feature ブランチのコミット履歴は以下のようになっている develop ブランチの commit 履歴 feature ブランチの commit 履歴 GitHub 上での Pull Request のマージオプション Create a merge commit Create a merge commit は、feature ブランチのコミットを hash 値を保持したまま develop ブランチにマージした後、新たにマージコミットを作成します。 実際にマージを行うと以下のようなコミット履歴になります。 マージ後の develop ブランチの commit 履歴 hash 値が保持されたまま develop ブランチにマージされていることが確認できます。そして新たにマージコミットが作成されていることが確認できます。 特徴 マージ元のコミット履歴の hash 値が保持される マージコミットが作成されることでマージによる形跡が残る 使用例 複数のコミットをそのまま保持したい場合 マージの履歴を明確に残したい場合 Squash and merge Squash and merge は、feature ブランチのコミットを 1 つのコミットにまとめて develop ブランチにマージします。 実際にマージを行うと以下のようなコミット履歴になります。 マージ後の develop ブランチの commit 履歴 feature ブランチでは複数の commit がありましたが、develop ブランチには 1 つの commit しか存在していません。 特徴 マージ元のコミット履歴が1つにまとめられる 使用例 コミット履歴をシンプルに保ちたい場合 小さな変更をまとめて1つのコミットにしたい場合 Rebase and merge Rebase and merge は、feature ブランチのコミットを develop ブランチの最新のコミットの直後にコピーし、develop ブランチにマージします。 その際コミットはまとめられず、feature ブランチのコミット履歴がそのまま develop ブランチにマージされます。 マージ後の develop ブランチの commit 履歴 この際マージコミットが作成されていないことが確認できます。 特徴 マージ元のコミット履歴がそのまま develop ブランチにマージされる マージコミットが作成されないので、コミット履歴が綺麗になる マージ元の hash 値とは異なり、新たなコミットとして作成される 使用例 コミット履歴をそのまま保持しつつ、マージコミットを作成したくない場合 リベースを行ってコミット履歴を整理したい場合 罠 上記では各オプションの説明をしました。ここからは私が過去に躓いた罠について説明します。 Rebase and merge と git rebase は一緒じゃない 中規模プロジェクト用の開発ブランチを用意して共同で開発をしていました。 そこでブランチ元のdevelopブランチが更新されたのでプロジェクト用のブランチを git rebase を利用してコミット履歴を整理しようと考えました。 ただそうなると force push が必要になるので、共同で作業しているブランチには force push を避けたいと考えました。 そこでGitHub PullRequest機能のオプションの Rebase and merge を使えば代用可能なのではないかと考えました。 そうすることでコミット履歴も綺麗に保たれ、ローカルでの作業もなくなり安全だと考えました。 さて、develop ブランチに feature ブランチから出した Pull Request を Rebase and merge オプションを使ってマージを行った後、差分が無いかチェックして見ました。 めちゃくちゃ差分がありました。 一見 develop ブランチと feature ブランチのコミット履歴は同じに見えますが、hash 値が異なっていました。 Rebase and merge の特徴の1つに「マージ元の hash 値とは異なり、新たなコミットとして作成される」というものがあるためです。 Rebase and merge を実行した時と git rebase を実行した時には挙動が異なるので、同一の挙動を期待してはいけないと学びました。 自分が前に実行したオプションが次回のデフォルトになる これは罠というか、不注意によるミスです。 前提として、私のチームではいつも Squash and merge を利用して作業ブランチのコミット履歴を綺麗に保っています。 先ほど Rebase and merge による実験をして失敗に終わった後、通常の作業に戻りました。 そして私の出した Pull Request が approve されたのでいつも通りマージを行いました。 そこで気付く違和感。何かがおかしい。 なぜか Rebase and merge が実行されている... Pull Request のマージオプションは、自分が前に実行したオプションが次回のデフォルトになるようです。ちょっと考えれば当たり前のことですね。 今まで意識していなかったので気付かなかったのですが、オプションをいじる際は次の Pull Request にも影響があるので注意が必要だと学びました。 まとめ GitHubのPull Requestマージ時のオプション指定の特徴を踏まえた上で、目的に合わせて適切なオプションを選択することが重要です。 私は以下のような使い分けをしています。 Create a merge commit : マージ元のコミットハッシュを保持し、マージの履歴を明確に残したい場合に使用します。これにより、どのブランチがどのタイミングでマージされたかが一目でわかります。 Squash and merge : 作業コミットを1つにまとめて、コミット履歴をシンプルに保ちたい場合に使用します。これにより、細かいコミットが1つにまとめられ、履歴が見やすくなります。 Rebase and merge : マージコミットを作成せずに、コミット履歴を直線的に保ちたい場合に使用します。これにより、変更の流れが追いやすくなり、履歴が綺麗になります。 また、Pull Requestをマージする際には、今設定されているオプションが何なのかを必ず確認してからマージを行うことで予期せぬ事故を防ぐようにしましょう。
アバター
こんにちは。 DBRE チーム所属の @p2sk と @hoshino です。 DBRE(Database Reliability Engineering)チームでは、横断組織としてデータベース(DB)に関する課題解決やプラットフォーム開発に取り組んでいます。 本記事では、AWS の生成 AI サービス Amazon Bedrock を活用し、Serverless アーキテクチャで構築した DB テーブル設計の自動レビュー機能を紹介します。この機能は GitHub Actions と連携し、プルリクエスト(PR)がオープンされると AI が自動でレビューし、修正案をコメントとして提案します。また、生成 AI アプリケーションの評価をどのように設計・実装したかも解説します。LLMOps のライフサイクルにおける 3 つのフェーズ(モデル選定、開発、運用)ごとに採用した評価手法を説明し、特に運用フェーズでは「LLM-as-a-Judge」を活用した生成 AI による自動評価について紹介します。 本記事の目的 本記事では生成 AI アプリケーションの評価を、抽象的な概念から具体的な実装例までわかりやすく情報提供することを目指します。本記事を読んでいただくことで、私たち DBRE チームのように機械学習の専門知識がないエンジニアでも、生成 AI の開発ライフサイクルの理解度が上がることを目指しています。また、生成 AI をサービスで活用する中で直面した課題とその解決策についても具体例を交えて紹介します。加えて、先日開催された AWS AI Day でのセッション「 コンテンツ審査を題材とした生成AI機能実装のベストプラクティス 」で紹介されている「LLM の実導⼊における考慮点と打ち⼿」に対する実装例の 1 つとしてもお読みいただけるかと思います。 少しでも皆さまの参考になれば幸いです。 目次 この記事の構成は以下のとおりです。長文のため「どんな仕組みか」にだけ興味がある方は「完成した仕組み」まで、生成 AI アプリケーション開発に関心がある方はそれ以降も読んでいただければと思います。 背景 設計 完成した仕組み(デモ動画あり) 実装時の工夫点 生成 AI アプリケーションの評価 得られた学びと今後の展望 まとめ 背景 DB のテーブル設計の重要性 一般的に DB のテーブルは、一度作成すると修正が難しいという特性があります。サービス成長に伴いデータ量や参照頻度は増加する傾向にあるため「設計時点で⚪︎⚪︎にしておけばよかった・・・」と後から後悔するような技術的負債を抱え続けることは、できる限り避けたいものです。したがって、統一された基準に基づく「良い設計」でテーブルが作り続けられる仕組みを整えることが重要です。「 AWSでデータベースを始めるのに 必要な 10 のこと 」においても、クラウドで DB 管理が自動化されてもテーブル設計は依然として価値の高いタスクとされています。 また、生成 AI の普及によりデータ基盤の重要性はさらに高まっています。統一基準で設計されたテーブルは分析がしやすく、分かりやすい命名や適切なコメントは生成 AI に良質なコンテキストを提供できる、というメリットもあります。 こうした背景から、DB テーブル設計の質が組織に与える影響は以前よりも大きくなっています。質を担保する手段として、例えば社内ガイドラインの作成や、それに基づくレビューの実施が考えられます。 レビューに関する弊社の現状 弊社では、テーブル設計レビューは各プロダクトの担当者が行っています。DBRE チームから「設計ガイドライン」を提供していますが、現状のところ拘束力はありません。DBRE が全プロダクトのテーブル設計を横断的にレビューする案も検討しましたが、プロダクトが数十個に及ぶため、DBRE がゲートキーパー的に振る舞うと開発のボトルネックになる懸念があり、断念しました。 以上の背景から、ガードレール的に振る舞う自動レビューの仕組みを DBRE で開発してプロダクトへ提供する、という手段を採用しました。 設計 抽象的なアーキテクチャ図と機能要件 以下は、自動テーブル設計レビュー機能の抽象的なアーキテクチャ図です。 自動レビューを継続的に実行するには、開発フローへの統合が重要です。そのため、PR をトリガとして AWS 上でアプリケーションを自動実行し、PR 内にテーブル定義(DDL)の修正案をコメントでフィードバックする仕組みを採用しました。アプリケーションの要件は以下の通りです。 弊社独自のレビュー観点を設定できること 人間のレビューを補完する目的で、100%でなくとも可能な限り高精度であること レビュー機能の実現方針 テーブル設計レビューの自動化には「① 構文解析によるレビュー」と「② 生成 AI によるレビュー」の 2 種類の方針が考えられます。それぞれの特徴を以下にまとめます。 構文解析で対応可能なレビュー観点は①、それ以外は②を適用するのが理想です。たとえば「オブジェクト名は Lower Snake Case で定義する」という命名規則の確認は①で対応できます。一方で「格納されているデータを推測できるオブジェクト名をつける」などの主観的な観点は②が適しています。 このように、レビュー観点に応じて両者を使い分けるのが理想ですが、今回は以下の理由から「②生成 AI によるレビュー」のみで実装する方針としました。 ①は実現可能性が見えているが、②はやってみないと分からず、先に挑戦する価値があると判断 ①で対応可能な項目も②で実装することで、両者の精度や実装コストの比較に関する知見を得たい レビュー対象のガイドライン 提供までの時間を短縮するため、レビュー項目を以下の 6 つに絞りました。 インデックスが DBRE 指定の命名規則に準拠 オブジェクト名は Lower snake case で定義 オブジェクト名は英数字とアンダースコアのみで構成 オブジェクト名にローマ字を使わない 格納されているデータを推測できるオブジェクト名をつける 真偽値を格納するカラムは「flag」を使わずに命名 上の 3 つは構文解析で対応可能ですが、下の 3 つは修正案の提示も考慮すると生成 AI の方が適切だと考えられます。 なぜ専用の仕組みを作るのか 「生成 AI によるレビューの仕組み」は既に複数存在しますが、今回の要件を満たせないと判断し、専用の仕組みを作ることにしました。例えば、 PR-Agent や CodeRabbit は有名な生成 AI レビューサービスで、弊社でも PR-Agent を導入し、 コードやテックブログのレビューに活用 しています。また、 GitHub Copilot の自動レビュー機能 は現在 Public Preview として提供中で、今後 GA される可能性があります。この機能ではコードを Push する前に Visual Studio Code 上でレビューを受けることも可能で、「生成 AI によるレビューの仕組み」は今後さらに開発フローに自然に統合されていくと想定されます。さらに、GitHub の管理画面で独自のコーディング規約を定義し、それをもとに Copilot にレビューさせることも可能です。 それでも自前で仕組みを構築する理由は以下の通りです。 多数のガイドラインを生成 AI で高精度にチェックするのは難しく、外部サービスでの対応は現状困難と判断 フィードバックの方法を柔軟に調整したい 例:意味が曖昧な「data1」のようなカラムは、修正案の提示が難しいためコメントのみに留めたい 将来的に構文解析とのハイブリッド構成で精度向上を目指す 次に、完成した仕組みを紹介します。 完成した仕組み デモ動画 PR 作成後に GitHub Actions が実行され、生成 AI がレビュー結果を PR のコメントとしてフィードバックします。実際の処理時間は約 1 分40 秒ですが、動画では待ち時間を省略しています。なお、生成 AI のコストは Claude 3.5 Sonnet を用いた場合、 DDL を 1 つレビューするために約 0.1 USD 必要という試算になりました。 https://www.youtube.com/watch?v=bGcXu9FjmJI アーキテクチャ 最終的なアーキテクチャは下図の通りです。なお、使用するプロンプトをチューニングするための評価アプリケーションは別途構築しており、詳細は後述します。 処理の流れ PR のオープンをトリガに GitHub Actions ワークフローを実行し、AWS Step Functions を起動します。この際、PR の URL とワークフロー内で生成した GITHUB_TOKEN を DynamoDB に保存します。DDL を直接 Step Functions に渡さないのは、入力文字数制限を回避するためです。PR の URL を元に Lambda 側で DDL を抽出します。Step Functions は Map ステート を利用し、各 DDL を並列でレビューします。1 回のレビューでチェックするガイドラインは 1 項目だけです。複数のガイドライン観点でレビューするために、最初のプロンプトで得た「修正後の DDL」を次のプロンプトに渡す処理を繰り返し、最終的な DDL を生成します(理由は後述)。レビュー完了後、PR にコメントとしてフィードバックします。レビュー結果は S3 に保存され、LLM-as-a-Judge を用いて生成 AI がその結果を評価します(詳細は後述)。 結果例を以下に図示します。 生成 AI からのフィードバックとして「適用したガイドライン」と「修正案」がコメントされます(画像左)。詳細は折りたたまれており、展開すると DDL への具体的な修正内容やコメントが確認できます(画像右)。 導入に必要な手順 以下の 2 ステップでテーブル設計レビュー機能の導入が完了します。数分で設定できるため、簡単に導入して即座に生成 AI によるレビューを開始できます。 AWS リソースへのアクセスに必要なキーを GitHub Actions Secrets に登録 対象の GitHub リポジトリにレビュー機能用の GitHub Actions ワークフローを追加 DBRE が提供するテンプレートファイルにプロダクト名を追記するだけ 次に、実装で工夫した点を何点か紹介します。 実装時の工夫点 コンテナイメージと Step Functions の活用 当初はシンプルに Lambda のみで実装予定でしたが、以下の課題がありました。 使用するライブラリのサイズが大きく、Lambda のデプロイパッケージサイズ制限(250 MB)を超える 複数のガイドライン観点を Chain して評価する場合、Lambda の最大実行時間(15分)に達してしまう懸念 DDL を直列に処理すると、DDL の数が増えるほど実行時間が長くなる 1 を解決するために Lambda にコンテナイメージを採用しました。また、2 と 3 を解決するため Step Functions を導入し、1 回の Lambda 実行で 1 つの DDL を 1 つのガイドライン観点で評価する設計に変更しました。さらに、Map ステートを使い DDL ごとに並列処理を行うことで、全体の処理時間が DDL の数に影響されないようにしました。下図は Map ステートの実装を示しており、ループ部分でプロンプトの Chain を実現しています。 Bedrock API のスロットリング対策 レビュー時に DDL の数 x ガイドラインの数 だけ Bedrock の InvokeModel リクエストが発生し、クォータ制限によるエラーが発生することがありました。 AWS のドキュメント によると、この制限は緩和できません。そのため、DDL 単位でリクエストを複数リージョンに分散し、エラー時はさらに別のリージョンでリトライする仕組みを導入しました。これにより、RateLimit に達することがほぼなくなり安定したレビューが可能になりました。 ただし、現在は クロスリージョン推論 を利用することで複数リージョン間でトラフィックが動的にルーティングされ、スロットリング対策は AWS 側に任せることが可能なため、今後はこちらに移行予定です。 Lambda から GitHub API を実行するための権限付与方法の整理 Lambda で「対象 PR の変更ファイルの取得」と「対象 PR へのコメント投稿」を実現するため、以下の 3 種類の権限付与方法を比較検討しました。 トークン種別 有効期限 メリット デメリット Personal Access Token 設定次第、無期限も可能 権限の適用範囲が広い 個人への依存 GITHUB_TOKEN ワークフロー実行中のみ 取得が簡単 対象の処理次第で権限不足の懸念 GitHub App(installation access token) 1 時間 GITHUB_TOKEN で未対応の権限も付与可能 プロダクトへ導入する際の手順の複雑化 今回は以下の理由から GITHUB_TOKEN を採用しました。 トークンは短期間(ワークフロー中のみ)有効で、セキュリティリスクが低い トークンの発行・管理が自動化され、運用負荷が低い 今回の処理に必要な権限を付与可能 トークンは有効期限(TTL)付きで DynamoDB に保存し、必要なときに Lambda から取得して使用します。これにより、トークン受け渡し処理のログへの記録有無を調査する必要がなく、安全にトークンを利用できます。 以降では、生成 AI アプリケーションの評価事例について紹介します。 生成 AI アプリケーションの評価 生成 AI アプリケーションの評価について、 Microsoft 社のドキュメント に記載の下図を参考にしました。 出典: Microsoft 社 - 生成 AI アプリケーションの評価 この図によると、GenAIOps(今回は LLM が対象のため LLMOps)ライフサイクルの中で実施すべき評価は 3 種類です。 モデル選定フェーズ 基盤モデルを評価し、使用するモデルを決定 アプリケーション開発フェーズ アプリケーションの出力(≒ 生成 AI の応答)を、品質・安全などの観点で評価しチューニングを実施 デプロイ後の運用フェーズ Production 環境へデプロイ後も、品質・安全性などについて継続的な評価を実施 以降では、各フェーズにおける評価をどのように実施したか、事例を紹介します。 モデル選定フェーズにおける評価 今回は Amazon Bedrock の基盤モデルから選定を行い、 Chatbot Arena のスコアと 社内の生成 AI 有識者 のアドバイスをもとに評価し、Anthropic 社の Claude を採用しました。着手時点で最も高性能だった Claude 3.0 Opus を用いて DDL のレビューを実施し、一定の精度を確認しました。モデルごとにベースの性能やレスポンス速度、金銭的コストが異なりますが、今回はレビュー発生頻度は高くなく、かつ「できる限り高速に」といった要件はないため、性能を最も重視したモデル選定を実施しています。あとは Claude のベストプラクティスに基づきプロンプトチューニングを行うことで、さらに高い精度が期待できると判断し、次のフェーズへ進みました。 なお、途中でより高性能・高速な Claude 3.5 Sonnet がリリースされ、推論の精度はさらに向上しました。 アプリケーション開発フェーズにおける評価 生成 AI の評価手法については、 こちらの記事 に分かりやすくまとめられています。記事内で、 プロンプト、基盤モデル、RAGの有無で、さまざまな評価のパターンが考えられます。 と述べられているように、「何を評価するか」によって評価のパターンが異なります。今回は「単一のプロンプト」の評価に焦点を当て、「DB のテーブル設計を社内独自のガイドラインに従いレビューさせる」という特定のユースケースで、評価の設計と実装の具体例を紹介します。 プロンプトチューニングと評価の流れ プロンプトチューニングと評価は Claude のドキュメント に記載の下図に沿って実施しました。 出典: Anthropic 社 - Create strong empirical evaluations ポイントは、「プロンプトの実行結果が期待にどれだけ近いか」などの評価観点を「何らかの方法で算出したスコア」として定義し、最良のスコアを得たプロンプトを採用する点です。評価の仕組みがない場合、チューニング前後の精度向上を判断する際に主観に頼りがちになり、結果として曖昧さや作業時間の増加を招く恐れがあります。 以降では、まず生成 AI の評価手法について、その後にプロンプトチューニング事例を紹介します。 「生成 AI の評価」とは deep checks という生成 AI 評価用プロダクトのページには、評価に関して以下の記載があります。 Evaluation = Quality + Compliance これは生成 AI アプリケーションの評価を最も端的に表現していると感じました。さらに細分化すると、 こちらの記事 ではサービス提供者の評価観点として「真実性、安全性、公平性、堅牢性」の 4 つの観点に分類されています。評価の観点とスコア算出方法は、アプリケーションの特性に応じて選ぶ必要があります。たとえば、 Amazon Bedrock では、テキスト要約に「BERT スコア」、質問回答には「F1 スコア」など、タスクごとに異なる指標が使われています。 評価スコアの算出方法 anthropic-cookbook では、スコアの算出方法を以下の 3 つに大別しています。 anthropic-cookbook に記載のスコア算出方法まとめ スコアの計算ロジックは、クラウドサービスや OSS を利用するか、自作するかを選択します。いずれにせよ、評価基準は自分たちで設定する必要があります。例えば LLM の出力が JSON 形式の場合、「文字列の完全一致」より「各要素単位の一致」が適切な場合もあります。 Model-based grading について、 anthropic-cookbook に記載のコードをより簡潔に表現すると以下のようになります。 def model_based_grading(answer_by_llm, rubric): prompt = f"""<answer>タグの回答を、<rubric>タグの観点で評価して。「正しい」または「正しくない」と回答して。 <answer>{answer_by_llm}</answer> <rubric>{rubric}</rubric> """ return llm_invoke(prompt) # 作成したプロンプトをLLMに渡して推論させる rubric = "正しい回答は、2 種類以上のトレーニングプランを含んでいる必要があります。" answer_by_llm_1 = "おすすめのトレーニングは、「腕立て伏せ」「スクワット」「腹筋」です。" # 実際はLLMの出力 grade = model_based_grading(answer_by_llm_1, rubric) print(grade) # 「正しい」と出力されるはず answer_by_llm_2 = "おすすめのトレーニングは、「腕立て伏せ」です。" # 実際はLLMの出力 grade = model_based_grading(answer_by_llm_2, rubric) print(grade) # 「正しくない」と出力されるはず 評価についてのまとめ ここまでの内容をまとめると、評価手法は以下の図で表現できます。 生成 AI の評価は、抽象的には Quality と Compliance に分解されます。これらをさらに細分化し、ユースケースごとに自分たちで具体的な評価観点を設定します。各観点は数値化が必要で、その手段は「Code」「Human」「Model」のいずれかをベースに実現します。 以降では、「DB のテーブル設計レビュー」という観点での具体的な評価方法について説明します。 DB のテーブル設計レビューにおける評価設計 Quality の評価は以下の理由で Code-based アプローチを選択しました。 Human による評価とチューニングのサイクルは工数増大につながり、得られるメリットに見合わない Model-based も検討したが、正解 DDL との完全一致をベストスコアにしたく、Code-based の方が適切と判断 正解データとの「DDL における類似度」を新たに実装するのは難しいため、テキスト間の距離を計測する手法の 1 つであるレーベンシュタイン距離(Levenshtein Distance)をスコアの算出方法に採用しました。この手法では、完全一致時の距離は 0 となり、値が大きいほど類似度が低くなります。ただし、「DDL における類似度」を完全に表す指標ではないため、基本的には全データセットで 0 スコアを目指し、非 0 スコアのデータセットに対してプロンプトチューニングを行う方針としました。アルゴリズムは LangChain の String Evaluators(String Distance) でも提供されており、こちらを使用しています。 一方、Compliance の観点については社内向けアプリケーションであることや、プロンプトに埋め込むユーザー入力を DDL に限定する実装になっていることから、今回は不要と判断しました。 評価の実装 実装した評価の流れは以下のとおりです。 各レビュー観点ごとに、入力用 DDL と正解 DDL を組み合わせた 10 パターンのデータセットを作成しました。効率的にプロンプトチューニングと評価を繰り返すため、Python と Streamlit で専用アプリケーションを開発しました。データセットは jsonl 形式で保存し、ファイルを指定すると自動で評価を実行して結果が表示されます。以下のように各 json は「評価対象のガイドライン」「LLM を Invoke する際のパラメータ」「入力 DDL」「正解 DDL」が含まれています。 { "guidline_ids": [1,2], "top_p": 0, "temperature": 0, "max_tokens": 10000, "input_ddl": "CREATE TABLE sample_table (...);", "ground_truth": "CREATE TABLE sample_table (...);" } 個別の結果表示では、出力 DDL と正解 DDL の diff を表示し、視覚的に差異(=チューニングポイント)が分かるよう工夫しました。 評価が全て終わると、スコアの集計結果も確認できます。 プロンプトチューニング Claude のドキュメント に基づき、以下のポイントを意識してプロンプトの作成・チューニングを行い、最終的に 60 個のデータセットほぼ全てにおいて、最良(0スコア)の結果を達成しました。 ロールを設定 XML タグを活用 Claude に思考させる(思考過程の出力を指示することで、期待外れの回答時にデバッグを容易に) Few-Shot Prompting(出力例を提示) 参照データを冒頭に、指示を末尾に配置 明確かつ具体的な指示を与える プロンプトを Chain させる 有名な手法も多く詳細は省略しますが、最後の 2 点について補足します。 「明確で具体的な指示を与える」 当初は社内のテーブル設計ガイドラインの文章をそのままプロンプトに埋め込んでいました。しかし、ガイドラインには「あるべき姿」のみ記載され、「具体的な修正手順」が含まれていませんでした。そこで、Step-by-step 形式の「具体的な修正指示」に書き換えました。例えば「真偽値を格納するカラム名に xxx_flag を使用しない」というガイドラインについては、以下のように修正しました。 以下の手順に従い、真偽値が格納されているカラム名を抽出し、必要に応じて適切なカラム名に変更してください。 1. 真偽値が格納されているカラム名を抽出します。判断基準は、boolean型が使われているか「flag」文字が入っているかです。 2. 真偽値カラムの名前を1つずつチェックし、カラムの意味をまずは理解します。 3. 真偽値カラムの名前を1つずつチェックし、より適切な名前があると判断した場合は、カラム名を修正します。 4. 適切なカラム名の条件は<appropriate_column_name></appropriate_column_name>タグの中を参照してください。 <appropriate_column_name> 「flag」という文字を使わず... ... </appropriate_column_name> 「プロンプトを Chain させる」 チェックするガイドラインが増えるほど、1 回で全てを確認しようとするとプロンプトが複雑化し、チェック漏れや精度低下の懸念が高まります。そのため、1 回のプロンプト実行で AI にチェックさせる項目を 1 つに限定しました。最初のプロンプトで得た「修正後の DDL」を次のプロンプトの入力として渡し(Chain)、繰り返し処理することで最終的な DDL を得る仕組みにして、アーキテクチャにも反映しました。プロンプトの Chain で、以下のメリットも得られました。 プロンプトが短く、タスクが 1 つに絞られるため精度が向上 ガイドライン追加時は新規プロンプトを作成するだけでよく、既存プロンプトへの精度影響がない 一方で、LLM の Invoke 回数が増えるため、時間と金銭的コストは増加します。 デプロイ後の運用フェーズにおける評価 プロンプト作成段階では、手動で作成した正解データを用いて Quality を評価しました。しかし、Production 環境では正解データが存在しないため、別の評価手法が必要です。そこで「LLM の応答を LLM に評価させる」手法である LLM-as-a-Judge を採用しました。 Confident AI のドキュメント によると、この手法には 3 種類の方法があるとされています。 Single Output Scoring (正解データなし) 「LLM の出力」と「評価基準(Criteria)」を与え、基準に基づき LLM にスコアを付けさせる Single Output Scoring (正解データあり) 上記に加え「正解データ」も提供。より高精度な評価が期待できる Pairwise Comparison 2 つの出力を比較し、どちらが優れているかを判定。「優れた」の Criteria は自分で定義する 今回は Single Output Scoring (正解データなし) を採用しました。この手法も LangChain でサポート されており、提供されている関数を使用しました。なお、現在は LangSmith による実装が推奨されています。 評価基準(Criteria)は以下の 2 つを定義し、それぞれ 10 点満点で採点させています。 Appropriateness LLM の出力がガイドラインに沿って適切に修正されているか Formatting Consistency 不要な改行や空白などが付与されておらず、フォーマットの一貫性が保たれているか コードとプロンプトのイメージは以下の通りです。 input_prompt =""" <input_sql_ddl>CREATE TABLE ...</input_sql_ddl> <table_check_rule>曖昧なオブジェクト名は...</table_check_rule> 指示書:table_check_ruleを元に、input_sql_ddlを適切なDDLに修正してください。 """ output_ddl = "CREATE TABLE ..." # 実際はLLMが生成したDDLをセット appropriateness_criteria = { "appropriateness": """ Score 1: ... ... Score 7: 入力の指示に概ね従った応答が生成されており、不適切な修正が2箇所以下である。 Score 10: 入力の指示に完全に従った応答が生成されている。 """ } evaluator = langchain.evaluation.load_evaluator( "score_string", llm=model, criteria=appropriateness_criteria ) result = evaluator.evaluate_strings( prediction=output_ddl, input=input_prompt ) print(result) この実装で以下のような出力が得られます。(省略箇所あり) この回答は、与えられた指示に完全に従っています。以下に評価の理由を説明します: 1. カラム名の抽出と評価: 回答者は全てのカラム名を抽出し、それぞれのカラム名がデータの中身を推測できるかどうかを適切に判断しています。 2. 曖昧なカラム名の特定: 提供されたDDL内のカラム名は全て、その目的や格納されるデータの種類を明確に示しています。例えば、... ... この回答は、与えられた指示を完全に理解し、適切に実行しています。 Rating: [[10]] この仕組みは、アーキテクチャ図における下図赤枠部分に相当します。 LLM のレビュー結果が S3 に保存されると、SQS を介して非同期で LLM-as-a-Judge 用の Lambda が起動します。この Lambda が評価を実行し、結果はログとして S3 に保存され、スコアは CloudWatch のカスタムメトリクスとして送信されます。また、CloudWatch Alarm により閾値を下回る場合は Slack に通知されます。 このスコアは完全に信頼できるものではありませんが、対象が社内向けシステムであるため、ユーザーからのフィードバックを得やすい環境です。そのため、定量的なスコアで継続的にモニタリングしつつ、定期的にユーザーフィードバックを収集する体制をとりました。 得られた学びと今後の展望 最後に、生成 AI アプリケーション開発に挑戦して得た学びと、今後の方向性についてまとめます。 評価が非常に重要かつ難しい プロンプトの結果を同じ観点で評価することで、主観を排除しつつ高速にチューニングと評価を繰り返すことができました。この経験から、評価設計の重要性を強く感じました。ただし、GenAIOps における3つの評価(モデル選定時、開発時、運用時)はユースケースごとに判断が必要で「自分たちの評価設計の妥当性判断」は難しいと感じました。また、評価の観点が不足していると、コンプライアンス面で問題を抱えたアプリケーションを提供するリスクもあります。今後、より体系的でマネージドな評価方法や仕組みが提供されれば、GenAIOps が実現しやすくなっていくと考えます。 生成 AI のユースケースの想像の幅が広がった 実際に自分たちで生成 AI アプリケーションに関して調査・実装したことで解像度が上がり、ユースケースの想像の幅が広がりました。例えば、 エージェント と ロック競合の情報を収集する仕組み などを組み合わせて、よりマネージドかつ高速にインシデント調査ができる仕組みなどを想像できるようになりました。 プログラマブルなタスクの代替としての 生成 AI 活用 生成 AI をアプリケーション開発に活用する方法は、大きく次の 2 つに分けられます。 タスクそのものを 生成 AI に実施させる プログラム開発の生産性を 生成 AI で向上させる 今回は、 生成 AI に推論させるべきタスクだけでなく、本来はプログラムで処理すべきタスクも生成 AI を使って実装しました。当初は「プロンプトを工夫すれば迅速に高精度の結果が得られるかもしれない」と期待していましたが、実際にはプロンプトチューニングに多くの時間が必要であると痛感しました。一方、Claude の複数モデルで同じタスクを解かせた結果、モデルの精度が高いほど明確に結果が改善されることを確認しました。さらに、精度が高いモデルでは予測不能な挙動が減り、プロンプトチューニングに要する時間も短縮できることが分かりました。 これらの経験を踏まえると、今後モデル精度がさらに向上した場合、要件次第では「タスクを実施するプログラムを書く」代わりに「タスクそのものを 生成 AI に実施させる」という手段を選択するケースが増えるかもしれません。 今後の展望 今後は以下のようなことに取り組んでいきたいと考えています。 対応ガイドラインの拡充 導入プロダクトの拡大 プログラムによる構文解析とのハイブリッド構成にする レビュー結果をユーザーへフィードバックする際の「分かりやすさ」の改善 現状の簡易的な LLMOps を、モニタリングだけでなくログを活用したプロンプト・モデル改善にまで拡張 参考: @hiro_gamo 氏の Post まとめ 本記事では、Amazon Bedrock を活用し、Serverless アーキテクチャで実装した DB テーブル設計の自動レビュー機能を紹介しました。また、生成 AI アプリケーションの評価方法についても解説しました。先日開催された AWS AI Day でのセッション「 コンテンツ審査を題材とした生成AI機能実装のベストプラクティス 」で紹介されている「 LLM の実導⼊における考慮点と打ち⼿ 」の各項目にマッピングする形で、今回の取り組みを以下にまとめます。 項目 内容 他の手段との棲み分け ● DBRE チームで LLM による自動化に挑戦 ● 将来的にはルールベースとの併用を目指す 精度 ● 「テーブル設計レビュー」というユースケースに沿った評価観点を設計 ● プロンプトチューニングと評価を高速に繰り返すために、専用のアプリケーションを開発 ● 選定したモデル(Claude)のベストプラクティスに従ってプロンプトチューニングを実施 ● 1 回のプロンプト実行で AI にチェックさせる項目を 1 つに限定 & プロンプトの Chain で精度向上 コスト ● DDL の文字数にもよるが、1 DDL あたり約 0.1 USD ● レビュー発生頻度は低いため、コストより精度を重視したモデル選定(Claude 3.5 sonnet) ● プロンプトの Chain も同様に、コストは増えるが精度向上のメリットが大きいと判断 可用性・スループット ● クォータを意識してリージョン間のリクエスト分散やリトライ処理を実装 ● よりマネージドな クロスリージョン推論 へと移行予定 レスポンス速度 ● 「できる限り高速に」といった要件はないため、速度よりも精度を優先したモデル選定 ● 各 DDL を並列でレビューすることで速度向上 ● 数 10 個 の DDL に対して 2-5 分程度で レスポンスを返却 LLMOps ● LLM-as-a-Judge を用いて継続的に精度をモニタリング セキュリティ ● GitHub との連携には、GHA ワークフロー実行中だけ有効な GITHUB_TOKEN を採用 ● 社内アプリ & 入力が DDL に限定されるため、LLM の応答への Compliance 観点評価は未実施 本プロダクトは現在複数のプロダクトへの導入が進行中で、今後もユーザーフィードバックを基に改善を進める予定です。生成 AI アプリケーションは Amazon Bedrock Prompt フロー など開発用のサービスも進化を続けており、今後もより便利になっていくと思います。今後も積極的に生成 AI 領域にも挑戦していきたいです。 KINTO テクノロジーズ DBRE チームでは一緒に働いてくれる仲間を絶賛募集中です!カジュアルな面談も歓迎ですので、 少しでも興味を持っていただけた方はお気軽に X の DM 等でご連絡ください。併せて、 弊社の採用 X アカウント もよろしければフォローお願いします!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の12日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 合同勉強会 第一回目の「学びの道の駅ポッドキャスト」では、社内で行われた合同勉強会について、運営メンバーである朝日さん、きーゆのさん、リナさんにインタビューを行いました。このポッドキャストでは、勉強会の背景や目的、運営の工夫、そして今後の展望について詳しくお聞きしました。 インタビュー HOKAさん(インタビュアー): まずは「道の駅プロジェクト」がどのように始まったのか教えてください。 HOKAさん: このプロジェクトは社内で学びに対して前向きな文化をさらに広めるために始まりました。景山さんをはじめとする数名が、社内の勉強会を支援する仕組みを作ろうと集まったのがきっかけです。 HOKAさん: 合同勉強会の背景と目的について詳しく教えてください。 朝日さん: 新入社員向けのオリエンテーションで気づいたのですが、他のシステムと関連する情報を知る機会が少ないと感じました。それで、最新の情報をキャッチアップできる場が必要だと思い、合同勉強会を始めることになりました。 HOKAさん: 勉強会の運営で工夫されている点は何ですか? リナさん: 他のシステムやプロダクトの最新情報を共有できるようにして、より広範な知識を提供することを目指しています。また、参加者を増やすために事前の準備をしっかり行い、運営メンバー自身が積極的に関与しています。 HOKAさん: 初回の勉強会の成果と反響はいかがでしたか? きーゆのさん: 事前の見込みでは34人程度の参加でしたが、実際にはZoom参加者を含めて約80名が集まりました。普段関わらない人との交流ができたり、他のプロダクトの最新情報を得ることができたというポジティブな反応が多くありました。 HOKAさん: 勉強会の継続と今後の取り組みについて教えてください。 朝日さん: 今後は、新たなテーマ別ディスカッションや職種別の交流の場を作り、より多くの社員が気軽に参加できる環境を整えていきたいと考えています。 リナさん: 次回の登壇者も既に決定しており、継続的な開催を目指しています。社内外の情報発信も強化していきます。 HOKAさん: 「道の駅プロジェクト」を社外に発信する狙いは何ですか? リナさん: 社内外の興味を引くことで、より多くの参加者を募ることです。社外への発信を通じて、社内の学びの文化を広めたいと思っています。 HOKAさん: 最後に、今回のインタビューを振り返って感じることはありますか? 朝日さん: 参加者としての視点と運営側としての視点の違いを実感しました。運営側に立つことで、イベントの重要性や意義がわかり、盛り上げたいという気持ちが強くなりました。 きーゆのさん: 今までズームで見るだけだったのですが、運営側に立つことでコメントやリアクションが力になると感じました。今後も積極的に参加していきたいです。 まとめ 「道の駅プロジェクト」が目指すのは、単なる勉強会の場の提供にとどまらず、社員同士の交流と知識共有を通じて、社内の文化を一層豊かにすることです。継続的な勉強会の開催と広報活動を通じて、より多くの社員が参加し、学び合う場を作り上げていくことで、社内全体のスキルアップと連帯感の醸成に貢献していくことでしょう。 今回は合同勉強会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
This article is the 12th day of the KINTO Technologies Advent Calendar 2024 . 🎅🎄 Hello, we are the Android development team at the 'KINTO Kantan Moushikomi App' (KINTO Easy Application App). Today, we want to share the process of implementing Kotlin Multiplatform (KMP) into our existing app, the reasons behind it, and the changes and improvements it has brought. Over the past year, we have been exploring ways to maximize development efficiency between iOS and Android platforms. During this process, KMP caught our attention, and our team would like to share how this technology has innovatively improved our development process. Contents 1. Reasons for Implementing KMP in an Existing App 2. Integrating KMP into Our Existing App 2.1 Deciding on Shared Code Placement 2.2 Organizing the Shared Code 2.3 Creating a KMP Module 2.4 Multi-module Architecture and Umbrella Module 2.5 CI: Testing Shared Code on Android and iOS 3. Distributing Your KMP Code 3.1 Options for Distributing KMP Code 3.2 Swift Package Manager (SPM) 3.3 Automating Distribution 4. Android and iOS Implementation Methods 4.1 Feature Selection 5. Issues in KMP Cross-Platform Module Implementation 6. Effects 7. Moving Forward: Future Plans and Challenges 1. Reasons for Implementing KMP in an Existing App At the time, our team faced a shortage of iOS development resources. To address this challenge, the Android team decided to leverage Kotlin Multiplatform (KMP) to create shared business logic for both iOS and Android platforms. This approach reduced code duplication across operating systems and allowed the Android team to utilize their expertise to support iOS development. This strategy was seen as a crucial solution to alleviate staffing issues and significantly enhance development productivity, which became the decisive reason for integrating KMP technology into our existing app. [Summary of the Background] Addressing the shortage of iOS development resources Leveraging the Android team’s expertise in Kotlin Reducing code duplication across operating systems Improving development productivity and strengthening team collaboration ※ Let's eliminate duplicated efforts across operating systems by modularizing business logic into a KMP library. 2. Integrating KMP into Our Existing App Before implementing any KMP code, we made several strategic decisions about where to place and how to organize our shared code. 2.1 Deciding on Shared Code Placement Our mobile app has a typical setup with two separate development teams: an Android team working on the Android repository and an iOS team working on the iOS repository. When introducing KMP, the first question that arises is: where should the shared code reside? Option 1: Shared Code in a Separate Repository This option involves creating a new repository for the shared code, which can be accessed by both the Android and iOS repositories. The repository structure would look like this: graph TB; subgraph Android Repository AndroidApp end subgraph iOS Repository iOSApp end subgraph KMP Repository KMP end KMP --> AndroidApp KMP --> iOSApp Option 2: Shared Code in the Android Repository In this option, the shared code is placed in the Android repository, allowing the Android team to manage the shared codebase. The repository structure would look like this: graph TB; subgraph Android Repository KMP --> AndroidApp end subgraph iOS Repository KMP --> iOSApp end Option 3: Merge Android and iOS Repositories into a Monorepo This option involves merging the Android and iOS repositories into a monorepo, allowing both teams to access the shared codebase. The repository structure would look like this: graph TB; subgraph One Repository KMP --> AndroidApp KMP --> iOSApp end [Our Decision] After considering the pros and cons of each option, we decided to place the shared code in the Android repository. This decision was based on the following factors: Minimizing the impact on existing workflows Easier to manage the shared codebase 2.2 Organizing the Shared Code Once we decided where to place the shared code, the next decision was how to organize it. Since our existing Android app follows a multi-module architecture, we wanted to maintain a clear separation between the shared module and the platform-specific modules. We decided to place the KMP module in the shared directory within the Android repository, alongside the existing Android modules. for example: :app // Android app module :domain // Android-specific module :shared:api // KMP module :shared:validation // KMP module 2.3 Creating a KMP Module A Gradle module for KMP includes: 1. A build.gradle.kts file. 2. A src subfolder. For Android modules, we apply the com.android.library plugin and include an android {} block: plugins { id("com.android.library") } android { // Android-specific configurations } For KMP modules, we use the multiplatform plugin and define a kotlin {} block: plugins { kotlin("multiplatform") } kotlin { // KMP configurations } This setup allowed us to support both Android- and KMP-specific requirements in our shared codebase. 2.4 Multi-module Architecture and Umbrella Module Limitations of Multiple Modules In Android, splitting code into multiple modules is standard for complex projects. However, KMP currently supports exposing only one module to iOS. For example, suppose we have three modules in our shared codebase: featureA , featureB , and featureC . Each module depends on the data module, which in turn depends on the api module. graph LR; api --> data --> featureA data --> featureB data --> featureC We want to expose these three modules to iOS. In an ideal scenario, iOS developers would import only the modules they need, like so: import featureA import featureB <swift code here> However, due to the limitations of KMP, this approach results in duplicated code in the iOS app. What we want: graph LR; subgraph KMP api --> data --> featureA data --> featureB data --> featureC end featureA --> iOSApp featureB --> iOSApp featureC --> iOSApp What we get (with duplication): graph LR; subgraph KMP api --> data --> featureA end subgraph KMP1 api1(api copy) --> data1(data copy) --> featureB end subgraph KMP2 api2(api copy2) --> data2(data copy2) --> featureC end featureA --> iOSApp featureB --> iOSApp featureC --> iOSApp style api1 fill:#f88 style api2 fill:#f88 style data1 fill:#f88 style data2 fill:#f88 Umbrella Module To work around this limitation, we introduced an umbrella module . An umbrella module is a "empty" module that does not contain source code but used to manage dependencies. graph LR; subgraph KMP subgraph Umbrella api --> data --> featureA data --> featureB data --> featureC end end Umbrella --> iOSApp style Umbrella fill:#8f88 Here is a build.gradle.kts example: kotlin { val xcf = XCFramework() listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "Umbrella" binaryOption("bundleId", "com.example.shared") export(project(":shared:featureA")) export(project(":shared:featureB")) export(project(":shared:featureC")) xcf.add(this) } } sourceSets { val commonMain by getting { dependencies { api(project(":shared:featureA")) api(project(":shared:featureB")) api(project(":shared:featureC")) } } } } The umbrella module simplifies the integration process for iOS developers, ensuring a seamless and efficient development experience across platforms. 2.5 CI: Testing Shared Code on Android and iOS We always write tests for our code, and the shared code is no exception. Due to platform differences, some features may not work as expected on iOS. To ensure compatibility, run tests on both Android and iOS. Unlike Android, which can run tests on any OS, iOS tests must be run on macOS. 3. Distributing your KMP code Once you finish writing your KMP code, the next step is to distribute it to iOS app. 3.1 Options for Distributing KMP Code You can distribute your KMP code by source code or binary. Source Code Distribution With source code distribution, iOS developers must compile the KMP code themselves. This approach requires setting up a Kotlin build environment, including tools like Java VM and Gradle. Challenges: Every iOS developer needs to configure the KMP build environment. This increases the complexity of onboarding KMP code into the iOS project. Binary Distribution A better option is binary distribution. By providing precompiled libraries, we eliminate the need for iOS developers to manage an additional build environment, making it much easier to integrate shared code. Advantages: Reduces setup effort for iOS developers. Ensures consistent builds across environments. 3.2 Swift Package Manager (SPM) For iOS, there are two main dependency management systems: CocoaPods and Swift Package Manager (SwiftPM). The choice depends on your iOS team’s preferences. Fortunately, our iOS team has fully transitioned to SwiftPM, so we only need to support SwiftPM. What is a Swift Package? A Swift Package is essentially a Git repository that includes: Swift source code. A Package.swift manifest file. Semantic versioning via Git tags. Binary Distribution with SwiftPM Since SwiftPM 5.3, it has supported binaryTarget , allows you to distribute precompiled libraries instead of source code. Creating a Swift Package with Binary Distribution Here’s a brief explanation of how we publish KMP code as a Swift Package: Compile the KMP code into an .xcframework . Package the .xcframework into a zip file and calculate its checksum. Create a new release page on GitHub and upload the zip file as part of the release assets. Obtain the zip file’s URL from the release page. Generate the Package.swift file based on the URL and checksum. Commit the Package.swift file and add a git tag to mark the release. Associate the git tag with the release page and officially publish the GitHub release. For detailed instructions, refer to the [KMP documentation on Remote SPM export].( https://kotlinlang.org/docs/native-spm.html ) // swift-tools-version: 5.10 import PackageDescription let packageName = "Umbrella" let package = Package( name: packageName, platforms: [ .iOS(.v13) ], products: [ .library( name: packageName, targets: [packageName]), ], targets: [ .binaryTarget( name: packageName, url: "https://url/to/some/remote/xcframework.zip", checksum: "The checksum of the ZIP archive that contains the XCFramework." ] ) 3.3 Automating Distribution Manual distribution can be time-consuming. To streamline the process, we created a GitHub Actions workflow for automation. name: Publish KMP for iOS on: workflow_dispatch: inputs: release_version: description: 'Semantic Version' required: true default: '1.0.0' env: DEVELOPER_DIR: /Applications/Xcode_15.3.app jobs: build: runs-on: macos-14 steps: - name: Checkout uses: actions/checkout@master - name: set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'zulu' - name: "Build and Publish" env: RELEASE_VERSION: ${{ github.event.inputs.release_version }} GH_TOKEN: ${{ github.token }} run: ./scripts/publish_iOS_Framework.sh $RELEASE_VERSION #!/bin/sh set -e MODULE_NAME="<your module name>" VERSION=$1 # version name for github release RELEASE_VERSION="$MODULE_NAME-$VERSION" # tag name for git tag TAG="$VERSION" TMP_BRANCH="kmp_release_$VERSION" # check if VERSION is in semver format if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "VERSION should be in semver format like 1.0.0" exit 1 fi ZIPFILE=./shared/$MODULE_NAME/build/XCFrameworks/release/$MODULE_NAME.xcframework.zip echo "Building $MODULE_NAME $VERSION" ./gradlew assembleKintoOneCoreReleaseXCFramework echo "creating zip file" pushd ./shared/$MODULE_NAME/build/XCFrameworks/release/ zip -r $MODULE_NAME.xcframework.zip $MODULE_NAME.xcframework popd # fetch tags git fetch --tags # get previous release tag PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -v ^version | head -n 1) echo "previous release tag: $PREVIOUS_RELEASE_TAG" # create github draft release echo "creating github release $RELEASE_VERSION" gh release create $RELEASE_VERSION -d --generate-notes --notes-start-tag $PREVIOUS_RELEASE_TAG gh release upload $RELEASE_VERSION $ZIPFILE echo "retrieving asset api url" # get asset api url of uploaded zip file from github release # eg: "https://api.github.com/repos/{username}/{repo}/releases/assets/132406451" ASSET_API_URL=$(gh release view $RELEASE_VERSION --json assets | jq -r '.assets[0].apiUrl') # add suffix .zip to url ASSET_API_URL="${ASSET_API_URL}.zip" # Generate Package.swift ./scripts/generate_SPM_Manifest_File.sh $ZIPFILE $ASSET_API_URL # commit Package.swift and add tag git checkout -b $TMP_BRANCH git add . git commit -m "release $VERSION" git tag -a $TAG -m "$MODULE_NAME $VERSION" git push origin $TAG # update github release to point to the new tag gh release edit $RELEASE_VERSION --tag $TAG 4. Android and iOS Implementation Methods In this project, we introduced a new common module to our existing app using Kotlin Multiplatform (KMP). To minimize potential platform-specific issues, we carefully selected and implemented features that could work reliably across Android and iOS. The focus was on establishing a cross-platform module by selecting OS-independent functionality and keeping implementations simple for initial testing in production environments. Below is an outline of the feature selection criteria and the implementation process. 4.1 Feature Selection To identify potential challenges of deploying KMP in production, we prioritized features that would not depend on platform-specific implementations and could be handled with minimal dependencies. The criteria for feature selection included: OS-Independent Functionality : We select the feature that would be OS-independent to avoid unexpected issues in production, leaving out elements that required specific OS-level controls, such as communication, storage, and permissions. Minimizing Additional Libraries : To reduce the risk of maintenance, we select the feature that could be implemented only with the Kotlin standard library without relying on additional libraries. Library Prioritization : When selecting libraries, we prioritized official Kotlin libraries first, then libraries recommended in official Kotlin documentation, and finally, third-party libraries as a last selection. Based on these criteria, input validation were chosen as the initial cross-platform functionality to implement with KMP. And full-width/half-width character transformation feature added. Android Input Validation Implementation By default, Android implementation has only lack of library functionality or interface difference problems, but it was no-big deal. The input validation feature was structured according to general object-oriented programming (OOP) principles, with an emphasis on reusability and consistency. 1. Defining Common Interfaces : We defined Validator and ValidationResult interfaces to create a consistent foundation for validating input across both platforms. abstract class ValidationResult( /** * Informations about input and fail reason. */ val arguments: Map<String, Any?>, requiredKeys: Set<String> ) fun interface Validator<T, R : ValidationResult> { /** * @return validation result or `null` if the target is valid. */ operator fun invoke(target: T): R? } 2. Validator Implementation by Input Type : Separate validators and result classes were created for different input types, such as email and password validation. class IntRangeValidator( /** * min bound(inclusive). */ val min: Int, /** * Max bound(inclusive). */ val max: Int ) : Validator<String, IntRangeValidationResult> { companion object { const val PATTERN = "0|(-?[1-9][0-9]*)" val REGEX = PATTERN.toRegex() const val ARG_NUMBER = "number" const val ARG_RANGE = "range" const val ARG_PATTERN = "pattern" } val range = min..max override fun invoke(target: String): IntRangeValidationResult? { when { target.isEmpty() -> return RequiredIntRangeValidationResult() !target.matches(REGEX) -> return IllegalPatternIntRangeValidationResult(target, PATTERN) } return try { target.toInt(10).let { number -> if (number !in range) { OutOfRangeIntRangeValidationResult(target, range) } else { null } } } catch (e: NumberFormatException) { OutOfRangeIntRangeValidationResult(target, range) } } } 3. Test Code Creation : To validate the module’s accuracy across platforms, we implemented extensive test cases using the kotlin-test package, ensuring stable functionality on both Android and iOS. import kotlin.random.Random import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull class IntRangeValidatorTest { private var min = 0 private var max = 0 private lateinit var validator: IntRangeValidator @BeforeTest fun setUp() { min = Random.nextInt() max = Random.nextInt(min + 1, Int.MAX_VALUE) validator = IntRangeValidator(min, max) } @AfterTest fun tearDown() { min = 0 max = 0 } @Test fun `invoke - decimal number string`() { val validator = IntRangeValidator(Int.MIN_VALUE, Int.MAX_VALUE) for (number in listOf( "0", "1", "111", "${Int.MAX_VALUE}", "-1", "${Int.MIN_VALUE}", "${Random.nextInt(Int.MAX_VALUE)}", "-${Random.nextInt(Int.MAX_VALUE - 1)}" )) { // WHEN val result = validator(number) // THEN assertNull(result) } } } Full-width/Half-width Character Transformation Implementation In addition to input validation, we implemented a character transformation feature to automatically convert between full-width and half-width characters based on application requirements. 1. Defining Extendable Interface : To enable multiple and complex conversions, we defined an interface that could be inherited to handle various character transformations. Kotlin has functional interface( fun interface ) and operator function( operator fun ) features helped to implement this. fun interface TextConverter { operator fun invoke(input: String): String operator fun plus(other: TextConverter) = TextConverter { input -> other(this(input)) } } 2. Defining Mapping Constants for Conversion : We created a character mapping table that listed the full-width/half-width characters and their conversions, allowing transformations by referencing predefined mappings. open class SimpleTextConverter( val map: Map<String, String> ) : TextConverter { override operator fun invoke(input: String): String { var result = input for ((key, value) in map) { result = result.replace(key, value) } return result } } class RemoveLineSeparator( map: Map<String, String> = mapOf( "\n" to "", "\r" to "" ) ) : SimpleTextConverter(map) object HalfwidthDigitToFullwidthDigitConverter : SimpleTextConverter( mapOf( "0" to "0", "1" to "1", "2" to "2", "3" to "3", "4" to "4", "5" to "5", "6" to "6", "7" to "7", "8" to "8", "9" to "9" ) ) val NUMBER_CONVERTER = FullwidthDigitToHalfwidthDigitConverter + RemoveLineSeparator() 3. Automatic Conversion Functionality : The transformation function was designed to automatically convert full-width characters to half-width or vice versa, creating a consistent and predictable input experience. By selecting these OS-independent features and implementing them with KMP, we were able to establish a stable, reusable module that could be deployed reliably across Android and iOS. Integration into iOS Our KMP code was distributed as a Swift Package, our iOS team using XcodeGen to manage Xcode project files. Integrating KMP code into iOS app can be easily done by add 4 lines code to project.yml file. packages: + Umbrella: + url: https://github.com/your-org/your-android-repository + minorVersion: 1.0.0 targets: App: dependencies: + - package: Umbrella - package: ... However, since our code resides in private repositories, some additional setup is required. For full details see: Credential Setup for Private Repositories in SwiftPM 5. Issues in KMP Cross-Platform Module Implementation During the development of a KMP common module, several technical challenges arose, particularly with handling basic functionalities, multibyte characters, encoding. Below is an overview of these issues and how they were resolved. No Unicode Codepoints Support in Kotlin Standard Library In order to accurately process multibyte characters such as Kanji and surrogate pairs within input validation, we decided to implement Unicode Codepoint-based regular expressions. This approach allowed us to precisely match and validate characters based on their positions within the Unicode spectrum rather than merely treating them as individual characters. However, we encountered issue. Kotlin’s String class does not natively support handling Unicode Codepoints, nor does it provide an official library for this purpose, especially surrogate pairs. So to ensure precise handling of multibyte characters based on codepoints, we use a third-party library, which allowed us to match complex characters like Kanji more accurately within our regular expressions. No Encoding Support for Non-UTF To maintain compatibility with legacy systems, it was necessary to support encoding in Shift-JIS (MS932). But KMP does not support Shift-JIS encoding natively. Text transfer to the legacy system required to check encodable or not in MS932, for which we opted to use the ktor-client library to handle encoding. However, the iOS version of ktor-client only supports UTF-based encoding schemes, making it challenging to implement MS932 encoding. Due to MS932 encoding limitations, we abandoned the use of code points for Kanji verification. Instead, we declared a constant that included the entire list of Kanji characters required for validation, converting these to Unicode codepoints for reference when needed. Unicode Codepoint Issue When implementing full-width and half-width character transformations, we encountered discrepancies in codepoint differences between certain characters, making a simple addition/subtraction approach ineffective. For example, the Japanese full-width characters ァ' ( U+30A1 ) and ア ( U+30A2 ) differ by only 1 in codepoints. In contrast, the half-width characters ァ ( U+FF67 ) and ア ( U+FF71 ) differ by 10 in codepoints. This inconsistency meant that a unified approach to transformation was not feasible. We resolved this by creating a constant mapping table for all transformations, explicitly defining all full-width and half-width characters and their respective mappings. This approach allowed us to handle a variety of characters accurately in transformation operations. By addressing these challenges, we enhanced the stability and completeness of our KMP common module, ensuring accurate functionality across both Android and iOS platforms. 6. Effects Technical Effects: Process Consistency : The implementation of KMP has minimized operational discrepancies between iOS and Android, reducing the frequency of errors during QA. Code Reusability : Code validated by the Android team is also used on iOS, enhancing development efficiency across both platforms. OS Collaboration and Optimization of Development Resources: Reduced Communication Burden : KMP allows the Android team to handle most maintenance independently, enabling the iOS team to focus on version upgrades and minor maintenance. This leads to more efficient use of development resources and strengthened collaboration between the teams. Project Management Challenges: Development and Maintenance Costs : Initial setup requires time, but afterward, development can continue as usual. However, development costs may increase due to restrictions on using Android-specific and Java-based libraries. Resource Allocation : Development processes focused on the Android team can lead to resource shortages during busy periods. As the Android team primarily manages features implemented with KMP, the iOS team's understanding is relatively low, necessitating balanced resource distribution and training. 7. Moving Forward: Future Plans and Challenges Implementing Future Expansion Plans Through Ongoing Education and Training Currently, our team is developing and executing an internal education and training program to make more effective use of Kotlin Multiplatform (KMP) technology. This program goes beyond technical details, focusing on enhancing teamwork and project management skills. By doing so, we aim to not only improve technical abilities but also manage projects more effectively and strengthen collaboration between teams. Future Plans: Transitioning Common Logic to KMP Going forward, our team plans to transition more common logic to KMP, which will help maximize code reuse between iOS and Android applications and reduce the complexity of maintenance, thereby enhancing development efficiency. Key Logic to Transition: API Client: BFF, OCR Business Logic: Cache management, etc. Utilities: Formatting of text (time, usage fees), version comparison (terms of use), etc. Local Storage: App settings, authentication tokens, etc. By implementing these plans, we expect to strengthen the efficiency and collaboration of cross-platform development, enabling our team to perform development tasks across platforms more effectively. Thank you for reading, and we hope this provides a useful reference for teams that have not yet applied KMP technology.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の12日目の記事です🎅🎄 はじめに みなさんこんにちは!KTC(KINTOテクノロジーズ)にてプラットフォームグループMSPチームに所属しているマツノです(入社時エントリーは こちら )。 前職ではSES事業会社に在籍し、インフラエンジニアとしてオンプレやAWSに構築されているシステムの保守・運用を担当していました。もう少しシステムやそこに関わる人達と深く関わりたいなと考えていたところ、縁あってKTCにジョインし本日に至ります。 この記事を書こうと思ったきっかけ 突然ですが、みなさんはMSPと聞いてどんなものをイメージしますか?なんの略称かご存じですか? 恥ずかしながら、自分はリクルーターの方に求人票を見せてもらうまでMSPという単語を知りませんでした…。 そこで、この記事では自分がKTCにMSPチームとして入社してから学んだことや苦労したことを紹介しながら、KTCでのMSPチームの取り組みを紹介したいと思います! MSPとは? まずはMSPについて一般的な説明をさせてください。MSPとは「Managed Service Provider」の略称でGartner社の公開している 用語集 では以下のように紹介されています。(原文をDeepLにて翻訳) マネージド・サービス・プロバイダー(MSP)は、ネットワーク、アプリケーション、インフラ、セキュリティなどのサービスを、顧客の構内、MSPのデータセンター(ホスティング)、またはサードパーティのデータセンターで、継続的かつ定期的なサポートと能動的な管理を通じて提供する。 MSPは、自社固有のサービスを他のプロバイダーのサービスと組み合わせて提供することもある(例えば、セキュリティMSPがサードパーティのクラウドIaaSの上でシステム管理を提供するなど)。ピュアプレイMSPは、1つのベンダーやテクノロジーに特化し、通常は自社のコア・サービスを提供する。多くのMSPは、他のタイプのプロバイダーのサービスを含んでいる。 MSPという用語は、従来はインフラやデバイスを中心としたタイプのサービスに適用されていたが、現在では継続的、定期的な管理、メンテナンス、サポートを含むようになった。 Gartner Glossary: Managed Service Provider (MSP)より引用 「継続的かつ定期的なサポートと能動的な管理を通じて提供する。」 ここら辺がMSPの根幹となる考え方になってきます。 MSPという用語を調べてみると事業所によって若干の違いはあるようですが、多くは稼働中のシステムの保守・運用・監視を専門に担当するサービスのことを指すようです。 KTCでのMSPチームの取り組み MSPチームの成り立ち ここからはKTC社内でのMSPチームの取り組みなどを紹介させていただきます! まず、KTCでのMSPチームのミッションですが、 「アプリケーション運用サポートにより間接的な開発スピードと品質向上に貢献する」 となっています。 なぜそのようなミッションになったのかを理解するために、MSPチーム発足当時のことを調べてみました。 MSPチームの構想が立ち上がった当初、KTCでは以下のような課題がありました。 開発中のシステムにおいて開発者と運用者が同一のため、運用作業に追われて開発スピードが上がらない。 障害対応において、システム稼働時間と同等の時間でサポート体制が取れていない。 こういった課題を解決するために、KTCにおけるMSPチームは以下の2チームで構成されています。 MSPサービスデスク(アウトソーシング) MSP内製チーム MSPサービスデスクはアウトソーシングしており、いわゆる24h365d対応の部隊になります。 一方でMSP内勤チームは2023年5月に発足した比較的新しいチームで、平日の日中帯にKTC社内の各開発チームより引き継いだ業務を対応しています。 MSPチームの具体的な業務内容 KTCでは日々様々な内製プロダクトが開発されており、MSPチームでは主に以下のような業務対応をしています。 アカウント管理 アカウント登録・削除 パスワードリセット アカウント棚卸 離職者対応 セキュリティレポート集計・周知対応 データ連携バッチ失敗時のリラン対応 システム監視アラート1次対応 各種問い合わせ対応 上記のリストからイメージ出来る方もいらっしゃるかもしれませんが、 現在のMSPチームではシステム管理・運用において定型的に対応できるもの、 複数のチームで対応する必要があるが対応方法が統一されていなかったもの、 もしくは対応できていなかったものを中心に対応しています。 実際にどんなことをやっているの? いまいちイメージが湧かないと思うので、MSPチームで取り組んでいるとある業務の詳細を紹介していきます。 現在、MSPチームでは毎月社内向けに公開される人事情報を元に、離職者対応というものを実施しています。 業務内容としては退職や育児休暇等で離職される社員の情報をとりまとめ、対象システム(内製・SaaS含む)のアカウント有無の確認から削除までを一括対応するというものです。2024年11月時点では2つのグループが開発・管理している、合計7つのシステムに関して離職者対応を実施しています。 この業務をMSPチームが一括で対応することのメリットは、例えば以下のようなことが挙げられると思います。 業務の標準化 グループ間やシステム間でのアカウント管理の差異をなくせる。 運用コスト削減と開発への注力 システム開発・管理を担当しているチームの運用コストを減らせる。 業務の属人化防止 MSP内製チームにて手順書を作成し、チームメンバー全員が対応できる状態を維持する。 運用コスト削減については、ここで例に挙げている離職者対応について具体的な計算をしてみます。 7つのシステムについて担当者が月次で離職者アカウントの削除対応しているとしましょう。そしてそれぞれの作業が大体2時間程度掛かると仮定します。 そうすると全体で必要な月次・年次運用コストは 2(月次の工数) × 7(対象システム数) = 14(時間/月) 14(時間/月) × 12 = 168(時間/年) となります。あくまで概算ですが、年間で約150時間程度の工数を開発チームの代わりにMSPチームが担当しているということがわかりますね。 自分がKTCにジョインしたタイミングで、すでに離職者対応はMSPチームにて対応していたのですが、月半ばであっても離職者の方が離職された翌稼働日にアカウント削除しており、かなりきっちり対応しているなという印象を受けたのを覚えています。 こういったメリットがある一方で、当然デメリットもあります。 以下のようなものが考えられるでしょう。 コミュニケーションコストの肥大化 業務によっては引継ぎ元チームとのやりとりが増え、業務を手放した恩恵を感じにくい。 引継ぎリスク MSPチームの作業ミスによって、リカバリー対応等を求められる。 これらメリット・デメリットを踏まえ、 業務を引き継ぐタイミングで如何にデメリットを最小限に抑え、メリットを増やしていくのかが腕の見せ所になります。 ここまではKTCでのMSPチームの成り立ちや、具体的な業務の紹介をさせて頂きました。 かっこいいことも書きましたが、自分はまだまだ未熟者なので日々勉強中です…。 MSPチームのこれから 求められたこと ここからは自分自身がKTCに入社してから求められたことや取り組んだことをお話ししつつ、これからのMSPチームについて紹介させていただいてこの記事を締めようと思います! まず、KTCに入社する際に自分に求められたことは大きく以下の2点でした。 MSP内製チーム現場リーダーとして成長し、内製チームをリードすること。 今まで経験したシステム保守・運用の実務経験を活かし、MSP内製チームの業務拡大に貢献すること。 これらは自分にとってとてもチャレンジングな内容でした。なぜなら、今までの働き方は日次・週次・月次といった各スパンで一定の決められた業務があり、それらをミスなく同じクオリティでアウトプットすることが求められるようなものだったからです。組織としてもシステムとしても安定期にある環境がほとんどでした。 一方でKTCは組織としてもビジネスとしても拡大を狙う組織であり、当然それに伴い新規システムの開発も進んでいます。開発チームの開発スピードと品質向上をミッションに掲げるMSPチームとしては、対応業務の拡大を進めたいということになってきます。 現場リーダーとして意識したこと 前述したように自分に求められたことは理解していたつもりなのですが、1人のエンジニアとして目の前の業務をこなすのと現場リーダーとして立ち回るのとでは全く違いました。 今までは自分自身のアウトプットにのみ気を配ればよかったのですが、現場リーダーとして成長するためには、チームメンバー全員のアウトプットにまで気を配る必要があります。 もちろん自分一人が責任を負う必要はないのですが、どこまで把握する必要があって、どこまでを委ねて良いのかのさじ加減が難しいと感じています。 周りのサポートもありだいぶ慣れてきましたが、日々勉強だなぁと感じています。 MSP内製チームの業務拡大に向けて 最後に自分のシステム保守・運用の実務経験を活かし、MSP内製チームの業務拡大に貢献するという部分ですが、こちらは絶賛悪戦苦闘中です。 過去に組織として縮小傾向にある開発現場にいたことがあるのですが、そこでは業務の属人化が進んでしまっていました。担当者の高齢化や過負荷な状況が続いていたこともあり、属人化を解消するのがかなり難しい状況になっていると感じたのを覚えています。自分なりに出来ることはやったつもりですが、工数も限られているため、出来ることには限界がありました。 そういった経験を踏まえても、今のMSPチームが取り組んでいることはKTCにとって有用性があると感じています。 チームとしてやろうとしていることやその必要性はとても良く理解できる、しかしノウハウが自分の中にないという状況です。 MSPチームの取り組みを拡大するためには、自分自身が以下のようなことが出来るようになる・実践する必要があると考えています。 適切な業務設計・フローとするために、システム観点だけでなく業務観点で考える。 新たな業務を引き継ぐ際には、MSPチームとしてのアウトプットが揃う手順とする。 KTC社内でのMSPチームの取り組みを知ってもらう。 もともと手順書などのドキュメント類を作ることは嫌いではなく、定型作業のようなルーチンワークにも抵抗がないため、それなりに適正はあると自負しているのですが、業務を作る部分が本当に難しいと感じます…。今まで自分自身がシステム開発寄りの働き方をしていたため、思考の癖としてシステムの仕様や内部で利用しているAWSサービスが気になってしまいます。ですが良い業務を作るためにはそれではいけません。「業務観点ってなんだ?」と自問自答しながらドキュメントと向き合う毎日です。 アウトプットを揃えるという観点についても、業務フローを整えチケット起票したうえで、そこに必要な情報を集めてから対応する手順が基本だと理解しています。ですが業務フローを整えるのに四苦八苦しています。ちゃんと考えるべき部分と、あまり考えなくて良い部分の判断が難しいです。 上記のことが自分のスタイルを確立したうえで実践できれば、業務引継ぎのリスクを最小限に抑えメリットを最大化することができると思うのですが、これがなかなか難しい…。一朝一夕で身に付かないものだと感じているので、今は上長やマネージャーに助けてもらいながら日々の業務に取り組んでいます。ただ、自分の働きによって良い業務を作ることが出来れば、それがMSPチーム拡大に繋がるので頑張ろうと思います。 さいごに この記事では一般的なMSPの話から始まり、2024年4月にKTCに入社した筆者の目線から見たMSPチームの取り組みや、有用性、これからやりたいことを紹介させていただきました。 普段の業務で技術的に高度なことに取り組んでいるようなことがないため、他のテックブログと違い技術系の話や、技術的に困ったことを紹介するものではなく、自身の主観を交えながらKTCでのMSPチームを紹介させて頂きました。少しでもMSPチームでの取り組みに興味を持ってもらえたら嬉しいです。
アバター
Introduction Hello everyone! This is Mori from the Global Development Division and the Tech Blog Operations Team. KINTO Technologies currently has bases in Tokyo, Nagoya, and Osaka, as well as two offices in Tokyo: at Muromachi (Nihonbashi) and Jinbocho. The Global Development Division, which I belong to, sits at the Jinbocho Office. In this article, I'll cover the information sharing meetings held twice at the Jinbocho Office, also known as Jinbocho ISM (Information Sharing Meeting) . *On a side note, some members thought it was read as "Jinbochoism," which I thought was a nice way to capture our unique Jinbocho style. Background It has been a year since the Jinbocho Office opened in June 2022, and the number of groups and new employees has increased to a whopping 100. However, we still often hear comments like, "I have no idea what other teams or groups are working on," or "I don't really know who's working here." With that in mind, we thought, "Why not host an information-sharing meeting at the Jinbocho office, like the ones held at the Osaka Tech Lab ?" Driven by this idea, the Tech Blog Operations Team led by team members from the Jinbocho Office, dove right in and planned the event with full enthusiasm! The 1st Jinbocho ISM Held on June 23, 2023! For the first meeting, we decided to start small! So, we kept it to 30 minutes and designed a simple agenda as shown below. Opening (5 min) Ask me anything* (20 min) Closing: Survey (5 min) What is an Ask Me Anything? An Ask Me Anything, commonly abbreviated as AMA, is a format where a host or guest invites people to ask them any questions, often starting with 'I'm ○○, feel free to ask me anything.' It is a format that allows people to ask anything to particular hosts or guests, mainly on social media. Popular on social media, AMA sessions allow participants to ask anything—from professional background and current work to personal topics. You can find plenty of examples of AMA sessions online, so feel free to explore 😄 Reference: What is AMA? Understanding the Basics of Ask Me Anything Since an AMA was included in the plan, we started by selecting people to answer the questions. For the first meeting, we invited the Group Manager of the Global Development Division. However, with only one available day, we had to prepare, communicate, and hold the meeting within about two weeks. Despite the short preparation time, around 30 people participated, and we received positive feedback from them. Here are some of their comments: I really appreciated having a space to hear from people I don’t usually get to talk to, learning about their backgrounds, hobbies, and other topics beyond work. It was very interesting to hear about the early days of KINTO, especially since there are few remaining documents about that time. I'd like to hear more stories from different team members. [1st post-event survey] would you like to participate again next time? On the other hand, since AMAs mainly involve asking questions and listening to one person, they can lack a sense of full participation for everyone. We held a retrospective to discuss how we could create a more lively and engaging atmosphere as the next challenge for the operations team. As for how we approached the retrospective and planned the next meeting, we employed various methods, but I'll save those details for another time. The 1st meeting photo The 2nd Jinbocho ISM The 2nd Jinbocho ISM, planned as such, was held on August 25, 2023, with an extended duration of one hour. The agenda was as follows: Opening (5 min) Talk with Neighbors (25 min) 🆕❗ Ask Me Anything (25 min) Closing (5 min) The AMA was so well-received that we decided to keep the format and invite Kamei-san from the Woven Payment Solution Development Group as the guest speaker. To encourage more lively participation from everyone, the operations team introduced a section called Talk with Neighbors . We gave the title in English to make it sound cool, but it's simply a segment where participants are divided into teams of 4-5 people for a casual conversation. Since free talk with people meeting for the first time can benefit from some ice-breaker topics, we adopted a dice (a.k.a. Korosuke) to provide prompts. Dice? Free talk? Yes, I was referencing a popular Japanese program, likely familiar to those who grew up during the Heisei era lol During the planning process, I was concerned about whether the teams would be able to start conversing easily after being divided in teams. However, I was impressed by how naturally the teams began to talk, making my worries unnecessary. When people ran out of topics to discuss, they rolled the dice and talked about a new theme✨ *Divided into teams, full of lively chatter. Do you see that? * The AMA was held in the second half hour. More people asked questions than in the previous meeting, partly because the Woven Payment Solution Development Group usually works away from KINTO. Some people said, "I wanted to hear more!"but as time is limited during the information-sharing meeting, we hope this gave more people the opportunity to interact with other teams outside of this session✨ [2nd post-event survey] would you like to participate again next time? Around 30 members participated in this next session, roughly the same number as the previous one and we received positive feedback such as: "It was good to be able to talk to people across divisions," "It was a great opportunity to get to know people who work in the same office," "I was able to interact with colleagues I don't usually interact with, and I look forward to the next one!"The number of people saying, "I'd like to participate again next time" exceeded that of the previous meeting.✨ Conclusion The Jinbocho ISM has been held twice so far, and overall, it has become clear that everyone values opportunities for cross-team interaction. While at first glance, these gatherings might seem unrelated to work, it can spark ideas like, "I know this person knows a lot about this, maybe I can ask for advice," which can ultimately benefit your work. Although we've only had two sessions so far and there is still room for improvement, we plan to continue holding them regularly to deepen interactions among members and energize the entire Jinbocho Office.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の11日目の記事です🎅🎄 メリークリスマス✌️🎅 KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 本記事ではカスタムスタイルの紹介をします。 はじめに 私がこの書き方を知ったのはApp Dev Tutorialsがきっかけです。 https://developer.apple.com/tutorials/app-dev-training/creating-a-card-view#Customize-the-label-style なんてスタイリッシュなんだ…! カスタムスタイルを使うことで、SwiftUIのコードが格段に読みやすく、洗練されたコードになる…!私もスタイリッシュなコードを書きたい! そんな衝動に駆られて使い始めましたが、実際かなり便利で読みやすいのでおすすめです。 特に、用途別に乱立した構造体名を覚えていなくても ~~Style() にドットで候補を探せるところが気に入っています。 カスタムスタイルのつくりかた 例えばLabelのカスタムスタイルを作成する場合、 LabelStyle を継承した構造体を作成し、プロトコルに準拠したメソッド(ここでは makeBody(configuration:) )内にスタイルを定義します。 configurationに含まれる値はものによって異なるので都度調べる必要がありますが、LabelStyleConfigurationに関してはTextとImageのViewが入っています。 /// 文字+アイコン のラベルスタイル struct TrailingIconLabelStyle: LabelStyle { func makeBody(configuration: Configuration) -> some View { HStack { configuration.title configuration.icon } } } さらに LabelStyle を拡張して、作成したカスタムスタイルを静的プロパティとして追加すると、呼び出し時に .labelStyle(.trailingIcon) のように呼び出すことができて可読性が向上します。ン〜スタイリッシュ! extension LabelStyle where Self == TrailingIconLabelStyle { static var trailingIcon: Self { Self() } } もし「spaceを指定したい」など、引数を持たせたい場合はカスタムスタイルにメンバプロパティを追加することで実現できます。 /// 文字+アイコン のラベルスタイル struct TrailingIconLabelStyle: LabelStyle { // デフォルト値を設定しておくとドット始まりの呼び出し方法もキープできる var spacing: CGFloat = 4 func makeBody(configuration: Configuration) -> some View { HStack(spacing: spacing) { configuration.title configuration.icon } } } // 呼び出し Label().labelStyle(.trailingIcon) // spaceにはデフォルト値が使われる Label().labelStyle(TrailingIconLabelStyle(spacing: 2)) // spaceを2に指定 使いどころ アプリ全体で広く使う共通デザインや、上記の TrailingIconLabelStyle のように普遍的なカスタムスタイルに使うと良いでしょう。 たとえば、my routeではProgressViewで使っています。 ProgressView自体のスタイル設定もですが、ProgressViewを表示中に背景をグレーっぽくするのもスタイルに含めることができます。 struct CommonProgressViewStyle: ProgressViewStyle { func makeBody(configuration: Configuration) -> some View { ZStack { ProgressView(configuration) .tint(Color(.gray)) .controlSize(.large) Color(.loadingBackground) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } extension ProgressViewStyle where Self == CommonProgressViewStyle { static var common: Self { Self() } } ちなみに、ProgressViewに background() を指定するとProgressViewに必要なサイズのみしか描画されないので、ZStackでColorをProgressViewの下に敷き、背景色が与えられたサイズ全体に広がるようにしています。 このようにスタイルを作成することで、以下のように簡潔でスタイリッシュに書けるようになりました。 struct SomeView: View { @State var loadingStatus: LoadingStatus var body: some View { SomeContentView .overlay { if loadingStatus == .loading { ProgressView() .progressViewStyle(.common) } } .disabled(loadingStatus == .loading) } } おわりに カスタムスタイルの紹介でした! 以下のページにあるものはカスタムスタイルを作成できます。 https://developer.apple.com/documentation/swiftui/view-styles よりスタイリッシュなコードを目指して一歩前進 🦌 🎄
アバター
This article is part of day 11 of KINTO Technologies Advent Calendar 2024 Merry Christmas! ✌ I'm Ryomm, and I work on developing the My Route iOS app at KINTO Technologies. In this article, I will introduce custom styles. Introduction App Dev Tutorials were the reason I learned to create custom styles. https://developer.apple.com/tutorials/app-dev-training/creating-a-card-view#Customize-the-label-style How stylish...! Using custom styles significantly enhances the readability and sophistication of SwiftUI code...! "I want to write stylish code, too!" Initially, that was what inspired me to start using it, but now I recommend it because it’s genuinely convenient and makes the code much easier to read. What I particularly like is that you can search for options using dots in ~~Style() even if you don’t remember the specific structure names, as they are organized based on their purpose. How to create a custom style For example, if you want to create a custom style for a Label, create a structure that inherits LabelStyle and define the style in a protocol-compliant method (in this case makeBody(configuration:) ). The values within the configuration object vary depending on what you're creating, so it's important to check each time. For LabelStyleConfiguration, it includes Text and Image views. /// Character + Icon LabelStyle struct TrailingIconLabelStyle: LabelStyle { func makeBody(configuration: Configuration) -> some View { HStack { configuration.title configuration.icon } } } You can also extend LabelStyle to add your custom style as a static property, which can be called as .labelStyle(.trailingIcon) when invoked, and improve readability. So~ stylish! extension LabelStyle where Self == TrailingIconLabelStyle { static var trailingIcon: Self { Self() } } If you want to have a parameter, such as "specify a space," you can do this by adding a member property to your custom style. /// Character + Icon LabelStyle struct TrailingIconLabelStyle: LabelStyle { // you can set the default value to preserve the way the dot starts are called var spacing: CGFloat = 4 func makeBody(configuration: Configuration) -> some View { HStack(spacing: spacing) { configuration.title configuration.icon } } } // call The default value is used in Label().labelStyle(.trailingIcon) // space Label().labelStyle(TrailingIconLabelStyle(spacing: 2)) // Set space to 2 Uses You can use it for common designs that you use widely throughout apps, or for universal custom styles like TrailingIconLabelStyle above. For example, my route uses it in ProgressView. While ProgressView itself is styled, you can also include a grayish background when ProgressView is displayed. struct CommonProgressViewStyle: ProgressViewStyle { func makeBody(configuration: Configuration) -> some View { ZStack { ProgressView(configuration) .tint(Color(.gray)) .controlSize(.large) Color(.loadingBackground) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } extension ProgressViewStyle where Self == CommonProgressViewStyle { static var common: Self { Self() } } By the way, when you use background() with a ProgressView, it only applies to the area required by the ProgressView. To ensure the background color covers a larger area, you can use a ZStack to place the color beneath the ProgressView, allowing the background to expand to the desired size. By defining a style in this way, you can achieve concise and elegant code, as shown in the example below. struct SomeView: View { @State var loadingStatus: LoadingStatus var body: some View { SomeContentView .overlay { if loadingStatus == .loading { ProgressView() .progressViewStyle(.common) } } .disabled(loadingStatus == .loading) } } Conclusion That wraps up this introduction to custom styles! You can create custom styles on the following page. https://developer.apple.com/documentation/swiftui/view-styles Take a step toward writing more stylish and elegant code!
アバター
こんにちは、学びの道の駅チームのHOKAです。 学びの道の駅チームは、部活動のような感じで業務時間でありながら、本業+オンで活動していました。が、この秋(9月?)から技術広報グループにジョインしました。 その詳細についてはこちらのブログを御覧ください↓↓ https://blog.kinto-technologies.com/posts/2024-12-03-the-next-goal/ 技術広報グループにジョインしたこともあり、12月のアドベントカレンダーを学びの道の駅チームからも書こうということになりました。 以前、共同でTech Blogを書いたので、今回も同様に気軽なノリでMTGをセッティングしたら、KINTOテクノロジーズ Tech Blogの発起人である中西さんが「15本、書こう」と息まいております。 「あれ、そんな話だったかな~」と思って、まずは15本のテーマを伺いました。 それがこちら ポッドキャスト 10本 まなびぃ 1本 学びの道の駅ポータル1本 もうすぐ一年 技術広報グループでこんなことやるよ このテーマを聞いて改めて、「書くことある?面白い?」と思ってしまった私。 「いやいや、どんどん書いていこう。例えば、春にBlog書いたじゃん。社内の反響とか、雰囲気が変わったとか、あるじゃん。」と自信のある中西。 もともと広報をやっていたので、なんらか文章にすることは出来ると思って、「はぁ、じゃあ、まぁやってみます。」というテンションで終話しようしたところ、きんちゃんが 「HOKAさん、納得してないんじゃないですか?無理していませんか?」と声をかけてくれました。 仕事なので、納得していないことも無理することもあるのは当たり前だろうと思い、正直に「YES」と答えました。そして、「書くほどのネタがないのに、なぜ書くのか?」ということも尋ねました。私なら、現状の活動内容を特に伝えるべきとも思わないし、読んでも面白いとも思わないのです。(言いたい放題) ここから対話形式でお届けします。 中西「KTCに入る前の自分に語りかけるように書いてほしい。こういう社内の雰囲気だったら入社したいと思うかもしれないじゃん。」 HOKA「うーん。全然、読みたいと思わないな...。」 中西「そもそも、テックブログは、1年に一人か二人にしか刺さらない記事で良いんです。」 HOKA「!?」 中西「正直、TechBlogの中にはどこの会社でも起きている当たり前のことが書かれた記事もあります。でも、TechBlogがなかったら、外部の人からはKTCで今何が起きているかは見えないんです。だから、大発見でなくても良い。会社で起きたことをただ書けば良い。それを読んだ人には、そんなことがあったんだと伝わるから。つまり、やったことを文章に残すだけで良いんです」 HOKA「!?!?!?!?!?!?!?」 中西「自分がやったことをただ書くだけ。それならハードルが低いし、誰でも書ける。そして、その内容がたとえ会社で起きたことの一部分だったとしても、各自がそれをやっていけば、集合したときにKTCってこんな会社だってことが見えるようになる。」 HOKA「めっちゃ理解しました。(頭にパッチワークの図を浮かべながら)」 きん「それが中西さんの戦略ですよね。他社のTechBlogとの差別化ポイントなんです。ちなみに、HOKAさんの悩みはTechBlogを書いていないエンジニアの悩みと同じです。私もHOKAさんの悩みを聞いてスッキリしました。」 ちなみに、私はというと、過去に企業広報を10年やっており、「いかに私的感情を省き、端的に業績やブランドイメージを伝えていくか」ということをゴールに置いて文章を書いて来ました。読み手は時間のない記者さんや編集者だったからです。 エンジニアが中心の会社で、エンジニアのコミュニケーションに触れ、学ぶことができたと感じる一日でした。入社してから一番と言っても良いくらい衝撃だったので、早速Blogにしたためました。 まとめ TechBlogは起きたことをただ書けば良い。 学びの道の駅は、正直な気持ちを話せる、素晴らしいチームです。 そして、メンバーの「分からない」に寄り添ってくれる素晴らしい仲間です。 参加している人も学んでいます。
アバター
はじめに こんにちは!Webサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がける共通サービス開発グループの中谷( @tksnakatani )です。 本記事では、多くの方が一度は経験したことがある「ヒヤッとするインシデント」の中から、決済プラットフォームで実際に本番環境で発生したAurora MySQLのデッドロックの事例をご紹介します。 デッドロックが発生した状況 2024年の某日、ログ監視システムからインシデント通知が届きました。通知内容を確認したところ、クレジットカード決済を実行している処理で以下のエラーログが記録されていました。 Deadlock found when trying to get lock; try restarting transaction さらに、Slackにはプロダクト担当者から「クレジットカード決済が失敗した」という問い合わせが寄せられていました。この時点で、非常に深刻な状況だと直感し、冷や汗をかいたことを今でも鮮明に覚えています。 原因調査 ロジック確認 デッドロック自体は約30分後に自然と解消されました。 エラーが発生した時間帯には、人気商品の発売があり、その商品への購入申し込みが集中していたことが判明しました。 通常、デッドロックとは、複数のトランザクションが互いに必要なリソースを保持し合い、いずれの処理も進行できなくなる状態を指します。このような状況を想定し、負荷試験を実施していたにもかかわらず、デッドロックが発生した点は謎でした。 当初は、リソースが競合するような処理が見当たらず、理論的にはデッドロックが発生する原因を特定するのが困難な状況でした。 再現確認 次にデッドロックが発生した前後のAPIへのリクエストをローカル環境で再現することを試みました。 本番環境で問題が発生したリクエストパラメータを使用し、curlコマンドで以下の2つのリクエストをほぼ同時に送信しました。その結果、本番環境と同様に1つのリクエストは成功しましたが、もう1つのリクエストではシステムエラーが返却されました。 :::details curlコマンドの例 curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000002" } ::: またエラーログには次のメッセージが出力されていました。 Deadlock found when trying to get lock; try restarting transaction が出力されていました。 再現できたことで調査の糸口が見え、ひとまず安堵しました。 SHOW ENGINE INNODB STATUS さらに、MySQLのSHOW ENGINE INNODB STATUSコマンドを使い、InnoDBストレージエンジンの状態を確認しました。 SHOW ENGINE INNODB STATUSコマンドでは、InnoDBストレージエンジンの状態に関する広範囲な情報を提供します。 この情報を基に、ロックやトランザクションの詳細を調べ、デッドロック発生の具体的な原因を特定するための手がかりを得ることができます。 mysql> set GLOBAL innodb_status_output=ON; mysql> set GLOBAL innodb_status_output_locks=ON; ・・・再びcurlコマンドを使って、2つのリクエストを送信。 mysql> SHOW ENGINE INNODB STATUS; その時の結果は以下の通りです。 ※一部抜粋、マスキング処理をしています。 ===================================== 2024-xx-xx 10:05:27 0x7fe300290700 INNODB MONITOR OUTPUT ===================================== Per second averages calculated from the last 2 seconds ----------------- BACKGROUND THREAD ----------------- srv_master_thread loops: 463 srv_active, 0 srv_shutdown, 7176 srv_idle srv_master_thread log flush and writes: 0 ---------- SEMAPHORES ---------- OS WAIT ARRAY INFO: reservation count 318 OS WAIT ARRAY INFO: signal count 440 RW-shared spins 290, rounds 306, OS waits 16 RW-excl spins 1768, rounds 5746, OS waits 48 RW-sx spins 0, rounds 0, OS waits 0 Spin rounds per wait: 1.06 RW-shared, 3.25 RW-excl, 0.00 RW-sx ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2024-04-18 10:04:02 0x7fe3059a4700 *** (1) TRANSACTION: TRANSACTION 12085, ACTIVE 6 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 70, OS thread handle 140612935517952, query id 28138 192.168.65.1 user update insert into payments (.... *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) TRANSACTION: TRANSACTION 12084, ACTIVE 7 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 69, OS thread handle 140612935812864, query id 28139 192.168.65.1 user update insert into payments (.... *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** WE ROLL BACK TRANSACTION (2) ---------------------------- END OF INNODB MONITOR OUTPUT ============================ ここから読み取れたこととしては TRANSACTION 12085 とTRANSACTION 12084 が存在する。 TRANSACTION 12085 とTRANSACTION 12084 が同じ「ギャップロック」を取得した。 TRANSACTION 12085 がインサートする前に「挿入インテンションギャップロック」を取得しようとしたが、TRANSACTION 12084のギャップロックと競合し待ちになった。 TRANSACTION 12084 がインサートする前に「挿入インテンションギャップロック」を取得しようとしたが、TRANSACTION 12085のギャップロックと競合し待ちになった。 MySQLがデッドロックを検知してTRANSACTION 12084をロールバックした。 ギャップロック・挿入インテンションギャップロックとは? ギャップロック ギャップロックは、インデックスレコード間のギャップのロック、または最初のインデックスレコードの前または最後のインデックスレコードの後のギャップのロックです。 たとえば、 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; では、範囲内の既存のすべての値間のギャップがロックされているため、カラムにそのような値がすでに存在するかどうかにかかわらず、 他のトランザクションが 15 の値をカラム t.c1 に挿入できなくなります。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-gap-locks 挿入インテンションギャップロック 挿入意図ロックは、行の挿入前に INSERT 操作によって設定されるギャップロックのタイプです。 このロックは、同じインデックスギャップに挿入する複数のトランザクションは、そのギャップ内の同じ場所に挿入しなければ相互に待機する必要がないように、意図的に挿入することを示しています。 値が 4 と 7 のインデックスレコードが存在すると仮定します。 5 と 6 の値をそれぞれ挿入しようとする個別のトランザクションでは、挿入された行の排他ロックを取得する前に、 挿入意図ロックを使用して 4 と 7 のギャップがロックされますが、行が競合していないため相互にブロックされません。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-insert-intention-locks ギャップロックが今回の問題につながったことが判明したため、次にクレジットカード決済処理の中でギャップロックを取得している箇所を特定する作業を進めました。 決済処理の全体の流れは、以下の3つのステップに分かれています。 同じリクエストIDで決済が行われていないか確認する 決済代行会社に決済を依頼する 決済代行会社からの結果をデータベースに書き込み、レスポンスを返す SQLが発行される箇所を中心にブレークポイントを設定し、ローカル環境でデバッグを行ったところ、以下のクエリの実行直後にギャップロックが取得されていることが確認されました。 SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; その時のperformance_schema.data_locksのデータが以下です。 原因 全ての情報が揃い、原因の特定が完了しました。 決済プラットフォームでは、リクエストが重複していないかを確認するために request_id をリクエスト元から受け取り、この値を後続の参照にも利用するため、ユニークなインデックスを付与していました。 一方、プロダクト側では request_id を以下のルールで生成していました: 商品ID-YYYYMMDD-連番 デッドロックが発生した際には、人気商品の発売により同一商品の購入リクエストが短時間に集中して送信されていました。その結果、 request_id の連番部分が急速にカウントアップされたリクエストが大量に送信されました。 :::details curlコマンドの例 curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } ::: 前述の通りクレジットカード決済処理の主な流れは以下となっています。 同じリクエストIDで決済が行われていないか確認する 決済代行会社に決済を依頼する 決済代行会社からの結果をデータベースに書き込み、レスポンスを返す 問題は、1つ目の処理で発生しました: SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; このクエリは、通常は同じ request_id のリクエストが来ない前提で実行されます。しかし、当該 request_id のデータのINSERT前であるため、クエリが空振りしギャップロックを取得しました。 その後、3つ目の処理で結果を書き込むINSERT処理が発生し、挿入インテンションギャップロックを取得しようとしました。しかし、すでに取得されていたギャップロックと競合し、待ちが発生。その結果、MySQLがデッドロックを検知し、1つのトランザクションがロールバックされました。 解決策 重複決済の確認用として使用していた SELECT FROM ... FOR UPDATE が原因であることが判明したため、このクエリを廃止しました。代わりに、リクエストを受け付けた時点でデータを仮登録し、トランザクションをコミットする設計に変更しました。 コミットのタイミングが増えることでパフォーマンス劣化が懸念されましたが、負荷テストにより必要な性能を担保できることが確認されたため、この仕様でリリースしました。 反省 ギャップロックについての理解が十分ではありませんでした。 SELECT FROM ... FOR UPDATE で結果が0件の場合にギャップロックが取得されることを十分に理解していませんでした。普段からマニュアルをよく読み、設計に取り入れているつもりでしたが、すべてを知った気になっていたと反省しています。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html また、テストで気付けるポイントがあったことも残念に感じています。 負荷テストではインデックスの断片化や再構築によるパフォーマンス劣化を懸念し、 request_id にランダムな値(UUID)を使用していました。このためデッドロックが発生せず、テストは正常に完了していました。 まとめ MySQLやInnoDBストレージエンジンを扱う際には、トランザクションやロックの動作を深く理解することが非常に重要です。日頃からドキュメントや仕様をしっかり読み込み、必要に応じて有識者のサポートを得ることの重要性を改めて認識しました。 また、本番環境で実際に利用されているパラメータを調査し、それに基づいたリクエスト値を使用してテストを行うことが、問題の早期発見や品質向上につながると学びました。
アバター
This article is the entry for day 4 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello. My name is Nakaguchi, and I am the team leader of the iOS team in the Mobile App Development Group. In my day-to-day job, I work on: the KINTO Kantan Moushikomi App (“the app”) and Prism Japan ( Smartphone app version / Recently released web version ) As an iOS developer. After deciding to migrate one of the apps I work on to SwiftUI, I’m using this article to share the process we followed and the principles that informed the decision. I hope this article appeals to: iOS engineers, those interested in SwiftUI architecture, and teams considering adopting SwiftUI. I’d love for readers in these groups to find value in it. This article is also based on a presentation I gave at the KINTO Technologies × RIZAP Technologies Mobile Tips event held recently. It does not contain specific, actual examples of converting to SwiftUI that use source code, etc. Instead, I have primarily focused on detailing the process the team followed to arrive at the decision to transition to SwiftUI. I hope it will help people who are having trouble with SwiftUI conversion in their own teams. Choosing the Architecture for the First Release The application app was released in September 2023. The main architectures chosen were: UIKit VIPER Combine Development of the app started around March 2023, but creating a new iOS app in 2023 presented a challenging decision: UIKit or SwiftUI, wouldn’t you agree? At the time, SwiftUI was rapidly gaining popularity within the broader development community. However, we chose UIKit for the Moushikomi app. The reasons for this were the app’s tight delivery deadline , and the lack of team members proficient in SwiftUI . We prioritized achieving a stable release using a technology we were used to rather than risk using a new. Now, I’ll discuss the factors that prompted us to transition to SwiftUI after the app's first release. Wanting to Shift to SwiftUI: The First Wave After the initial release in 2023, we quietly focused on bug fixes, minor updates, and refactoring. However, during this period, some team members began expressing a desire to explore something new. Several options were proposed, but SwiftUI emerged as the most popular choice. The first wave of the SwiftUI conversion came around March 2024. When we discussed within the team whether to adopt SwiftUI, opinions such as the following were shared. ● Reasons in favor of doing it: Interest in SwiftUI ● Reasons against doing it: No one on the team had prior experience with SwiftUI. At the time, we didn’t feel a pressing need to adopt SwiftUI. The team was also experiencing significant changes, with many new members joining due to replacements and other factors. This left us lacking both the personnel and time resources to start learning and adopting SwiftUI. As the team leader, I wasn’t confident in my ability to successfully lead a SwiftUI conversion. At that time, there were plenty of reasons not to proceed with it. For these reasons, we decided that a SwiftUI conversion would need to be postponed. The Desire to Shift to SwiftUI: The Second Wave Around six months went by after that. During one-on-ones, many team members expressed a strong interest in exploring SwiftUI, prompting us to revisit the idea of migrating to it. The second wave of the SwiftUI conversion came around August 2024. The situation had evolved since the first wave, and when we revisited the idea, the following opinions were shared: ● Reasons in favor of doing it Interest in SwiftUI had gradually evolved into a passion for it. Some SwiftUI experts had joined the team as a result of in-house organizational changes. The new team members were quickly becoming core contributors, and we felt that the team as a whole now had sufficient time and human resources to take on the challenge. ● Reasons against doing it There were still concerns about whether it was truly the right time to start a SwiftUI conversion. This time, there were plenty of reasons supporting the decision to move forward with it Considering these circumstances, we decided to proceed with the SwiftUI conversion. Never Get Your Goals Wrong Thus, the team unanimously agreed to move forward with the SwiftUI conversion. However, I firmly believe it’s crucial to never lose sight of your goals. [NO] The goal should not be to pursue a SwiftUI conversion purely out of technical curiosity. [YES] Focus on improving future maintainability, aligning with de facto industry standards, and addressing the complexity of using Combine in development, which we aim to move away from. [NO] The SwiftUI conversion must not compromise the app’s quality. [YES] Ensure the app's quality is maintained at least at its previous level, if not improved. [NO] Avoid misplacing work priorities, such as sidelining original release tasks in favor of the SwiftUI conversion. [YES] Continue delivering additional features at the same pace as before. With the above points firmly in mind, we engaged in team discussions on how to approach the SwiftUI conversion. Choosing the Architecture for the SwiftUI Conversion We discussed the type of architecture we wanted to adopt within the team, and the key opinions were as follows: Not wanting to use libraries Not wanting to use a view model Not wanting to use libraries This primarily referred to The Composable Architecture(TCA) .Many team members expressed a preference to avoid using TCA if possible, citing concerns such as the need to constantly monitor for updates and the potential challenges if support for the library were to be discontinued. Additionally, other projects within the company using TCA had reported usability issues, including a steep learning curve, the challenge of keeping up with the library's rapid update cycle, and an overreliance on parent reducers. Taking these factors into account, we decided to forgo using TCA. Not wanting to use a view model The decision to adopt a view model as the architecture for SwiftUI is a topic of much debate. In our case, several team members noted that SwiftUI's built-in binding capabilities make using MVVM less optimal, as it does not fully leverage SwiftUI's inherent strengths. Consequently, we agreed on a policy of not using a view model. Adopting an MV Architecture — As a Result, Our Team Opted for an MV Architecture . The following figure will give you a picture of what it is like. MV architecture Ideally, views should interact directly with the model. Similarly, data retrieved from APIs is passed to the views through the model. Currently, we are discovering that an MV architecture, makes things simpler and will lead to better maintainability in the future (moving away from Combine); does not depend on libraries; and lets us get the most out of SwiftUI’s features. We are experiencing advantages like these, which suggests that the chosen architecture effectively addresses the concerns raised during our discussions about which approach to adopt. Guidelines for Deciding Which Parts to Convert to SwiftUI Regarding our policy for determining which parts to convert to SwiftUI, the team deliberated on which of the following approaches to adopt: The first approach involves converting individual views to SwiftUI. First, convert the parts related to screen transitions to SwiftUI. As a result, we decided to proceed with the conversion to SwiftUI based on a policy of first converting the parts related to screen transitions . The reasons for this included the following: In our experience, we frequently encountered challenges with screen transitions later in the process If the view responsible for managing transitions remains in UIKit, it often necessitates (temporarily) wrapping individual views in UIKit, even after they have been converted to SwiftUI. The Path Forward for SwiftUI Conversion So far, I have outlined the process and policies for converting the application to SwiftUI. However, the actual conversion process is still in its early stages. As of December 2024, at the time of posting this article, the production code does not yet include any SwiftUI code. Currently, we are dedicating more time to discussions about the SwiftUI conversion through various initiatives. These include utilizing approximately 20 minutes left over in our daily morning meetings and holding a dedicated one-hour meeting each week to focus specifically on this topic. As we progress, we have begun establishing coding rules to foster a shared understanding within the team regarding the SwiftUI conversion. For instance, some team members have been creating sample code and conducting lectures for the entire team based on those examples. In the future, as the SwiftUI conversion gains momentum, we plan to introduce pair and mob programming to enhance the team's overall expertise in SwiftUI.
アバター
Introduction Hello, we are Chang and Hosaka, in charge of the my route by KINTO iOS app development in the Mobile App Development Group. In our mobile app development group, we usually use GitHub Actions as a CI/CD tool. This time, we introduced Bitrise for the first time to the my route by KINTO iOS app, so we would like to talk about it. What is Bitrise ? Bitrise is a cloud-based CI/CD (continuous integration / continuous delivery) service for the automated building, testing, and deployment of mobile apps. Bitrise is designed to streamline mobile app development, and it supports major mobile app development frameworks such as iOS, Android, React Native, and Flutter. Some of the key features of Bitrise include: Build Automation Builds are automatically triggered when code in the repository is updated. Builds can be easily configured using the visual interface. Test Automation Tests are automatically run after the builds are complete. Bitrise supports integration with a variety of testing tools, allowing automation at different test levels, including unit testing and UI testing. Automated Deployment If the test passes, Bitrise will automatically take steps to deploy the app. Bitrise supports deployment to app stores such as the App Store and Google Play. Variety of Integration Bitrise supports integration with a variety of tools and services, including GitHub, Bitbucket, Slack, Jira, Firebase, and more. Cloud-based Services Bitrise is a cloud-based service that does not require infrastructure configuration or management. Developers can easily take advantage of Bitrise’s features. Bitrise is a powerful tool for streamlining and improving the quality of mobile app development, and is a very valuable CI/CD service to developers and development teams. The Reason for Introducing Bitrise There are two reasons why we implemented Bitrise in the my route by KINTO iOS app. We had an opportunity to hear from Bitrise Ltd. before building a CI/CD environment, and at that time all the team members had replaced their PCs from Intel to M1, so we were fascinated by Bitrise, which can be built in the same M1 environment. The results of the experiment below, comparing Bitrise and Github Actions on an Intel Medium machine (the lowest performance), show that the cost can be reduced by about 30% and the processing time can be shortened by about 50%. Bitrise and GitHub Actions performance comparison experiment (tested in a different app): Processing Time Comparison Experimental Attempt / Machine Name Bitrise (Intel Medium) Github Actions 1 07:48 16:24 2 11:42 16:18 3 06:53 16:09 Average 08:48 16:17 Cost Comparison Github Actions cost per minute is $0.08 Bitrise cost per minute is $0.106666 Bitrise calculation: Given that 1 credit (cr.) = elapsed minutes (min.) × machine spec (2)... (i), and $400/7,500 credit = apprrx. 0.05333($/cr)... (ii), For (i) and (ii), apprx. 0.05333($/cr.) × 2(cr./min.) = apprx. 0.106666 ($/min.) Experimental Count / Machine Name Bitrise (Intel Medium) Github Actions 1 $0.85 $1.36 2 $1.28 $1.36 3 $0.75 $1.36 Average $0.96 $1.36 Using Bitrise in my route by KINTO To implement Bitrise in my route KINTO, we signed up for Bitrise's Teams pricing plan and adopted an M1 Medium machine consuming 2 credits per minute. The Teams plan has a credit limit set according to the price, and exceeding that limit incurs additional costs, so we aimed to optimize costs by also using GitHub Actions. With GitHub Actions, Linux is 1/10 the cost of macOS . Therefore, we use GitHub Actions for steps that can run on Linux (no app build required) and Bitrise for steps that require macOS (app build required). Bitrise Workflow my route by KINTO, mainly automates the following: unit testing, deployment to App Store and TestFlight, and build result notifications to Slack. Currently, builds are triggered by pushing to the develop and release branches, and scheduled builds are done on weekday mornings. We have observed that a single build takes about 6-11 minutes (12-22 credits). GitHub Actions Workflow GitHub Actions automates the static analysis flow. SwiftLint: A static analysis tool for Swift that automatically points out any code violations in the PR. SonarQube: A static analysis tool that analyzes code duplication and other issues that SwiftLint cannot cover. Summary and Future Prospects Looking ahead, Bitrise is expected to continue to expand and improve its features to meet the needs of mobile app development. For example, we can expect more advanced testing and deployment options, more flexible workflow settings, and further expansion of cloud-based resources. It is also expected to provide a more seamless development experience, including collaboration with the developer community and improved integration with other tools. KINTO Technologies would like to keep a close eye on the trends and lead to further utilization of this technology. Here is the review. https://findy-tools.io/products/bitrise/18/39
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の10日目の記事です🎅🎄 「Mobility Night」は、モビリティ領域のソフトウェア技術者、ビジネスパーソン、研究者、プロダクトマネージャーなどが気軽に集まり、業界特有の知見や課題を共有する勉強会シリーズです。#0(初回)のクローズド開催を経て、いよいよ第1回(#1)をオープンな形で開催することができました。 今回のテーマは、モビリティサービスの基盤技術である「GPS・位置情報」。カーナビや地図アプリ、オンデマンド交通、そして将来の自動運転やスマートシティ基盤まで、「いまどこにいるか」を正確に把握し、活用することはサービス価値の根幹を支えます。 当日は以下の5つのセッションが行われ、GPS・位置情報技術を軸に、それぞれが独自の切り口から課題と可能性を示しました。 この記事は技術広報グループでMobility Nightの企画運営も行っている中西が執筆しています。 1. Exploring New Google Places API 登壇者: KINTOテクノロジーズ株式会社 numaさん Google Places APIは地図プラットフォームの中核機能の一つであり、周辺検索や施設情報取得を効率的に行うための重要なインターフェースです。このセッションでは、テキスト入力中から即時に候補を提示するAutocomplete機能の強化や、Fieldsパラメータによる必要情報の絞り込みなど、最新の改善点が紹介されました。 ポイント: パフォーマンスとコスト最適化: Fields指定で不要なデータ取得を削減し、APIコストを抑えるとともにレスポンス高速化が可能。 ユーザーエクスペリエンス向上: 欲しい情報に素早くアクセスできる体験は、移動中のユーザーにとって大きな利点。Autocomplete強化で検索負荷を軽減し、UXを磨き上げる。 将来への展望: 現在は位置情報取得が中心だが、将来的にはIoTセンサーや行動履歴分析と組み合わせたパーソナライズ戦略も期待できる。 https://speakerdeck.com/kintotechdev/exploring-new-google-places-api 2. AIドラレコサービスの走行データで作る位置情報データプロダクト 登壇者: GO株式会社 松浦慎平さん ドライブレコーダーは事故記録用のデバイスという印象が強いですが、このセッションでは「走行データ=街をセンサー化するプラットフォーム」として再解釈されました。映像+GPSデータをAI解析することで、道路標識や信号、舗装工事などの情報を動的に地図へ反映できる可能性が示唆されました。 ポイント: ダイナミックな地図更新: 静的だった地図を“生きた情報基盤”へ進化させ、道路インフラ変化をほぼリアルタイムで反映。 複数車両データの統合: 異なる車両から得られるデータを突き合わせることで、一時的な標識や工事箇所などを高精度に検出。 プライバシー対策: 個人情報が映り込む映像を適切に匿名化しつつ道路情報を保持する技術・運用が必須。 将来的応用: 自動運転用HDマップ整備、スマートシティ計画、新サービス創出など、多面的なビジネス展開が期待。 https://speakerdeck.com/pemugi/aidorarekosabisunozou-xing-deta-dezuo-ruwei-zhi-qing-bao-detapurodakuto-wei-zhi-qing-bao-jing-du-xiang-shang-nogong-fu 3. GPSモジュールを触って学ぶ、衛星測位技術の概要 登壇者: チャリチャリ株式会社 VP of Engineering 蛭田慎也さん GPSは当たり前に利用されていますが、都市環境では電波反射や視界不良、衛星数の偏りなど多くの実務的課題が存在します。このセッションでは基礎的な衛星測位技術を理解し、精度向上の可能性と対策を探りました。 ポイント: 環境依存課題: ビル街でのマルチパス、トンネル下での衛星ロストなど、ロケーションごとの特殊要件が精度を左右。 マルチGNSS活用: GPS単独でなくGLONASS、Galileo、BeiDou、みちびき(QZSS)など複数システムを組み合わせて精度底上げ。 ハイブリッド手法: 加速度・ジャイロセンサ、Wi-Fi/Bluetoothビーコン、マップマッチングなど補完技術で精度改善。 基礎知識が指針に: こうした理解が将来のプロダクトデザインや品質保証、データ分析を行う際の指針となる。 4. 後処理で位置情報を補正する技術を試してみた(仮) 登壇者: 株式会社Luup IoTチーム 高原健輔さん リアルタイムでの高精度測位が困難な場合、後から精度を引き上げる「PPK(Post-Processing Kinematic)」という選択肢があります。高価なRTK装置や特別な通信インフラを用いず、取得済みデータと基準局データを組み合わせて後処理する手法です。 ポイント: PPKのメリット: リアルタイムにこだわらず、後日精度向上が可能。初期投資を抑えながらセンチメートル級精度を最終的に実現。 コスト効率と拡張性: 将来的に需要が増すシナリオで後から精度改善を行える柔軟性。配送ロボット、ドローン、シェアモビリティなどで有効。 応用範囲: 地図整備、走行ログ高度化、インフラ検査など、事後分析が中心の領域で大きな価値を発揮。 https://speakerdeck.com/kensuketakahara/hou-chu-li-dewei-zhi-qing-bao-wobu-zheng-suruji-shu-woshi-sitemita 5. オンデマンドバスサービス導入前のシミュレーションロジックの構築(仮) 登壇者: トヨタコネクティッド株式会社 先行企画部 新技術開発室 Halufy(ハルフィ)さん オンデマンド交通は柔軟性が魅力ですが、収益性や持続可能性を確保するのは容易ではありません。このセッションでは事前シミュレーションによる精緻な需要予測や運行計画設計が紹介されました。 ポイント: 持続可能なモデル構築: 補助金頼みにならずに最適なステーション配置、運行台数、時間帯設定をデータで検証。 戦略的データ活用: 位置情報を中心にODデータや予約希望を統合し、需要予測や価格戦略、ルート最適化を試行。 長期的ビジョン: 他のモビリティ手段やインフラと連携し、都市全体の交通効率化や利便性向上を目指す土台となる。 今後扱いたいトピックとMobility Nightの展望 今回のMobility Night #1は、GPS・位置情報に特化することで、モビリティ業界の「現在地把握」技術に深く切り込みました。参加者からは「位置情報という身近なテーマがこんなに奥深いとは」「基礎から先端活用まで通して聞けるのは貴重」という声が多数寄せられています。 しかし、モビリティ業界にはGPS・位置情報以外にも多くのテーマが存在します。今後は、 IoTデバイス活用: センサーからのリアルタイムデータ収集・制御 データ分析: 需要予測や高度なオペレーション最適化 プロダクトデザイン: UX向上やユーザー満足度最大化 品質保証: 信頼性確保や安全基準遵守 といった領域も掘り下げ、業界全体のイノベーションを促す場にしていきたいと考えています。 Mobility Nightは、運営メンバーが企画するだけでなく、参加者からの登壇希望やテーマ提案も歓迎しています。Discordを通じて意見交換や共催募集が可能な環境を整え、誰もが関わりやすいコミュニティを目指します。 https://discord.com/invite/nn7QW5pn8B まとめ 「Mobility Night #1」では、GPS・位置情報技術を軸に、モビリティサービスの中核をなす技術的課題と、その克服による新たな価値創造の可能性が明確になりました。静的な地図を動的な情報基盤へアップデートする試み、環境に左右されるGPS精度を高度な手法で補正する取り組み、オンデマンド交通をデータ駆動型で計画する戦略など、多様なアプローチが交錯しました。 これらの知見は、今後のMobility Nightで扱うIoT、データ分析、プロダクトデザイン、品質保証などのテーマとも結びつき、業界全体の進歩を加速させるはずです。引き続きMobility Nightにご注目いただき、ともに学び、交流し、新たな価値を創造していきましょう!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の10日目の記事です🎅🎄 Background When developing the KINTO かんたん申し込みアプリ App, we implemented some shared code using KMP (Kotlin Multiplatform) and published it as a Swift Package. This approach allowed us to efficiently share code across platforms and simplify the development process by avoiding code duplication. Our iOS Team currently uses XcodeGen to manage dependencies, and importing KMP code can be as simple as making a 4-line modification to the project.yml file. Here is an example of such a modification: packages: + Shared: + url: https://github.com/[your organization]/private-android-repository + minorVersion: 1.0.0 targets: App: dependencies: + - package: Shared - package: ... However, since our code resides in private repositories, some additional setup is required. This blog will outline those steps and explain how we streamlined the process. About Package.swift Here’s a brief explanation of how we publish KMP code as a Swift Package: Compile the KMP code into an .xcframework . Package the .xcframework into a zip file and calculate its checksum. Create a new release page on GitHub and upload the zip file as part of the release assets. Obtain the zip file’s URL from the release page. Generate the Package.swift file based on the URL and checksum. Commit the Package.swift file and add a git tag to mark the release. Associate the git tag with the release page and officially publish the GitHub release. The resulting Package.swift file will look something like this: // swift-tools-version: 5.10 import PackageDescription let packageName = "Shared" let package = Package( name: packageName, ... targets: [ .binaryTarget( name: packageName, url: "https://api.github.com/repos/[your organization]/private-android-repository/releases/assets/<asset_id>.zip", checksum: "<checksum>" ) ] ) Permission Setup for Development Environment Since the URL resides in a private repository, you will encounter the following error if no permission configuration is done: To resolve this, we explore two options: .netrc files and Keychain. Option 1: Using a .netrc File You can store your GitHub credentials in a .netrc file, which is a simple way to authenticate API requests: #Example: echo "machine api.github.com login username password ghp_AbCdEf1234567890" >> ~/.netrc echo "machine api.github.com login <Your Github Username> password <Your Personal Access Token>" >> ~/.netrc This method is quick and effective but may pose security risks since the token is stored in plaintext. Option 2: Using Keychain If you prefer not to store the token in plaintext, you can use Keychain to securely store your credentials: Open Keychain Access.app . Select ①, the login keychain. Select ②, to create a new Password Item. In the dialog box, enter the following information: Keychain Item Name: https://api.github.com Account Name: Your GitHub username Password: Your Personal Access Token This approach is more secure and integrates seamlessly with macOS authentication mechanisms. For SSH Users The above instructions assume you cloned the iOS repository using the https protocol. If you did, you already have the necessary permissions for github.com configured. However, if you cloned the repository using the ssh protocol, you might lack permissions for github.com , leading to permission-related errors during the resolveDependencies phase. To fix this, you can add an entry for the domain github.com in the .netrc file: #Example: echo "machine github.com login username password ghp_AbCdEf1234567890" >> ~/.netrc echo "machine github.com login <Your Github Username> password <Your Personal Access Token>" >> ~/.netrc Alternatively, use Keychain Access to add an item with the name https://github.com . Either method ensures your system has the required permissions. GitHub Actions After resolving the local development environment issues, you also need to address permission issues in the CI environment to ensure smooth automation during builds. Retrieving Tokens in GitHub Actions Using a Personal Token One straightforward approach is to create a Personal Access Token (PAT) with access to private repositories and pass it to the CI environment via Actions secrets. While effective, this method has several drawbacks: Token Expiration Tokens with an expiration date require periodic updates, and forgetting to update them may cause CI failures. Tokens without an expiration date pose long-term security risks. Broad Permissions A personal account usually has access to multiple private repositories, making it difficult to restrict PAT permissions to a single repository. Personal Dependency If the account owner loses access to private repositories due to role changes, CI workflows will fail. Using a GitHub App Using a GitHub App is a more robust solution, offering several advantages: Fine-grained permissions for repositories No dependency on individual accounts Temporary tokens that enhance security Setting Up a GitHub App We ultimately used a GitHub App to configure access permissions. Here is the process: Create a GitHub App in your organization. Install the App in both iOS and Android projects to manage repository access. Configure the App’s AppID and Private Key in the iOS project’s Actions secrets. Add code in the workflows to retrieve a temporary Access Token. Here’s an example: steps: - name: create app token uses: actions/create-github-app-token@v1 id: app-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: "YourOrgName" - name: set access token for private repository shell: bash env: ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} run: | git config --global url."https://x-access-token:$ACCESS_TOKEN@github.com/".insteadOf "https://github.com/" touch ~/.netrc echo "machine github.com login x-access-token password $ACCESS_TOKEN" >> ~/.netrc echo "machine api.github.com login x-access-token password $ACCESS_TOKEN" >> ~/.netrc By using a GitHub App, we ensure our CI workflows are secure, efficient, and free from dependency on individual user accounts. This approach minimizes risk and streamlines development across teams.
アバター
GitHub Actionsだけで実現するKubernetesアプリケーションのContinuous Delivery こんにちは。Toyota Woven City Payment Solution開発グループの楢崎と申します。 我々は、 Woven by Toyota で Toyota Woven City で利用される決済基盤アプリケーションの開発に携わっており、バックエンドからWebフロントエンド、モバイルアプリケーションまで決済に関わる機能を横断的に開発しています。 決済バックエンドはKubernetes上で動作し、いわゆるクラウドネイティブなツール群を使って開発しています。 今回はKubernetesアプリケーションを構築・安定運用していく上でキーとなる、GitOps(Gitでインフラ構成ファイルを管理し変更を適用指定する運用方法)を踏襲しつつ、CD(Continuous Delivery)プロセスに関して、一般に用いられている、いわゆる「クラウドネイティブなツール」ではなく、GitHub Actionsだけで構築することを目指します。 ここでいうCDの機能はあくまで Kubernetesの構成管理ファイルの変更の適用 コンテナイメージのアップデート です。他にもBlue / GreenやCanaryデプロイなど応用的なCDプロセスはありますが、「小さくスタート」することを想定しています。既にDevOpsチームが組成されていて、その恩恵にあやかれる人ではなく、最小の開発人数で、かつ新たなツールなしに普段利用しているGitHub Actionsのみを利用してKubernetes上で動作するアプリケーションを生産性高く継続的デリバリーさせたい人が対象になります。 レポジトリもアプリケーションのコードとKubernetesの構成管理ファイルを同じレポジトリで管理していることを想定しています。(権限の設定次第ではレポジトリをまたいで実行可能だとは思いますがここでは触れません) (Gitlabを普段お使いの方であれば Auto DevOps という非常に優秀なツールがあるので、決してGitHub及びGitHub Actions最高!というつもりはありませんので悪しからず) クラウドネイティブなKubernetes向けCI/CDツール Kubernetes向けアプリケーションのCI/CDと聞いて読者の皆さんはどのようなツールを思いつきますか? Argo CD Flux CD PipeCD Tekton などが挙げられます。 いずれのツールも非常に高機能で、Kubernetesの機能をフルに活かすために非常に有用です。 またGitOpsを実践する上で、Kubernetesの構成ファイルやアプリケーションイメージを柔軟に安全に更新できます。 一方でツール特有の知識や運用も必要で、DevOpsに専門の人員がいるような大きな組織ではないと継続的に運用するのは難しいのではないでしょうか? CDツールの運用そのものにKubernetesが必要で、Kubernetesの構成ファイルを管理するツールのためにKubernetesの構成ファイルが必要ということで、少人数の組織では導入や運用の敷居も非常に高いと思っています。 この記事では以下の図のようなパイプラインを、GitHub Actionsだけで構成することを考えます。 Kubernetesは特定のクラウドではなく汎用的なクラスタを想定しています。何かしらのコンテナレジストリがあることを想定しています。構成管理ファイルはKustomizeを例にしますが、Helm, Terraformなど何でも応用可能だと思います。 flowchart TD A[コードの変更] -->|ビルドパイプラインを実行| B[コンテナイメージのビルド、プッシュ] B -->|コンテナイメージ更新用のパイプラインが起動| C[新しいコンテナイメージでkustomizationを書き換えたプルリクエストができる] C -->|プルリクエストのレビュー| D[新しいコンテナイメージをKubernetesにデプロイ] linkStyle default stroke-width:2px,color:blue,stroke-dasharray:0 デモ Kubernetesの構成ファイルを管理するフォルダとアプリケーションのフォルダが入っているレポジトリを考えます。 フォルダ構成は以下になります。ここでは具体的なコードやDockerfileの中身、各アプリケーションのソース等は省略します。 ├── .github │   ├── actions │   │   └── image-tag-update │   │   └── action.yaml │   └── workflows │   ├── build-go.yaml │   ├── build-java.yaml │   ├── build-node.yaml │   └── kubectl.yaml ├── go-app │   ├── src/ │   └── Dockerfile ├── java-app │   ├── src/ │   └── Dockerfile ├── k8s │   ├── service-go.yaml │   ├── service-java.yaml │   ├── service-node.yaml │   └── kustomization.yaml └── node-app    ├── src/ └── Dockerfile それぞれのアプリケーションは以下のような形でplaceholderとし、 apiVersion: apps/v1 kind: Deployment metadata: name: app spec: ... template: ... spec: containers: - name: some-server image: go-placeholder # placeholderとしてkustomizationと同じ文字列を入れておく その複数のplaceholderをkustomization.yaml上で一元管理します。 apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: techblog resources: - service-go.yaml - service-java.yaml - service-node.yaml images: - name: go-placeholder newName: go-app newTag: v1.1.1 - name: java-placeholder newName: java-app newTag: v2.7.9alpha - name: node-placeholder newName: node-app newTag: latest まずKubernetesの構成ファイルを適用するために、以下のようなyamlのGitHub Actionsを構成します。 name: kubectl on: pull_request: branches: - "**" paths: - "k8s/**" #Kubernetesのmanifest fileが入っている場所 push: branches: - main paths: - "k8s/**" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: azure/setup-kubectl@v4 - env: KUBECONFIG_CONTENTS: ${{ secrets.KUBECONFIG_CONTENTS }} #事前にkubeconfigをGitHubのシークレットに格納しておく run: | echo "${KUBECONFIG_CONTENTS}" > $HOME/.kube/config chmod 600 $HOME/.kube/config - run: kubectl apply --dry-run=server -k ./k8s >> $GITHUB_STEP_SUMMARY - if: github.ref == 'refs/heads/main # mainブランチだったら実際に適用 run: kubectl apply -k k8s/ これは管理者権限を持ったkubeconfigが手元にある場合の一般的なKubernetesの構成ファイルを適用するパイプラインです。 各クラウドなどのクラスタの設定方法に応じてconfigの取得方法は変更してください。 次に各アプリケーションをpushする際に自動でプルリクエストを作るか、コンテナのイメージタグを書き換えるcompositeを作成します。 name: image-tag-update description: 'コンテナイメージの更新時にkustomizationのイメージタグを書き換えるタスク' inputs: target_app: description: '対象のアプリケーション' required: true tag_value: description: '新しいコンテナイメージタグ' required: true token: description: 'PRや内容の更新の権限を持ったトークン' required: true runs: using: 'composite' steps: - uses: actions/checkout@v4 id: check-branch-exists continue-on-error: true with: ref: "image-tag-update" # タグ更新用のデフォルトブランチ名 - uses: actions/checkout@v4 # checkoutはブランチが存在しないとデフォルトブランチにフォールバック、みたいなことはできないのでこういう書き方 if: steps.check-branch-exists.outcome == 'failure' with: ref: main - uses: mikefarah/yq@master # yqで対象のplaceholderのタグの値を置換 with: cmd: yq eval '(.images[] | select(.name == "'"${{ inputs.target_app }}-placeholder"'")).newTag = "'"${{ inputs.tag_value }}"'"' -i k8s/kustomization.yaml - uses: peter-evans/create-pull-request@v6 if: steps.check-branch-exists.outcome == 'failure' # プルリクエストがないと新しいプルリクエストを作成 with: title: 'コンテナイメージの更新' body: | `${{ inputs.target_app }}` を更新します branch: "image-tag-update" - uses: stefanzweifel/git-auto-commit-action@v5 if: steps.check-branch-exists.outcome == 'success' # チェックアウトが成功したら、既存のブランチにコミットを追加 with: commit_message: "Image update for ${{ inputs.target_app }}" 各アプリケーションのイメージを作成する過程で、上記のcompositeを呼びます。複数のアプリケーションを管理している場合は、それぞれのアプリケーションの後に付け加えるとよいでしょう。 ... - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 with: file: ./Dockerfile push: true tags: ${{ env.tag }} # なにかしらのタグ - uses: ./.github/actions/image_update if: github.ref == 'refs/heads/main' with: target_app: go tag_value: ${{ env.tag }} token: ${{ secrets.GITHUB_TOKEN }} # コンテンツ、プルリクエスト編集権限を持ったgithub token これで、アプリケーションを実行するタイミングでコンテナイメージが自動でアップデートされ、プルリクエストベースで新しいコンテナイメージがデプロイできるようになります! (タグの導出方法は各自のワークフローにおまかせします。下記の例はマイナーバージョンをインクリメントした例です) - name: go-placeholder newName: go-app - newTag: v1.1.1 + newTag: v1.1.2 運用上の注意点 デプロイのタイミング イメージ更新用のプルリクエストが、マージされた瞬間にデプロイされます。インフラの構成ファイルの修正と合わせてリリースしたい場合は、このブランチに修正を追加するか、マージするタイミングを合わせて適用するといいでしょう。 新規コンテナアプリケーションの追加 例えば上記の例でPythonのアプリケーションを足したいという時に、イメージ更新用のプルリクエストがそのまま残っていると、Pythonのイメージタグをどれだけ更新してもプルリクエスト自体に最新版の変更が反映されてない限り、空振りし続けるので注意が必要です。 切り戻し Commitをrevertすれば戻せるので非常にシンプルです。 Reconcileのタイミング GitOps Toolの多くがドリフトを抑制するためのリコンサイルをほぼリアルタイムで実施できるのに対して、このやり方だとCDパイプラインが動作したタイミングでないと実施できません。 Kubernetesのクラスタに更新権限をどれくらいのチームメイトが保有して権限を行使しているかにも応じてツールの使い分けは大事だと思います。 Container Registryを直接見ているわけではない コンテナレジストリから直接コンテナイメージの最新版を取得するものもありますが、この方法では実際に見ているわけではありません。確実にコンテナが存在するか、確認するステップをコンテナレジストリごとに実装したほうが良さそうです。 GitHub Actionsの権限設定に関して contents と pull-requests の更新権限が必要になってきます。Actionsのパーミッション、GitHub Appなどに権限をアサインして使ってください。詳しくは こちら 。 後に実行されたコンテナイメージで上書きされる CDツールには、コンテナイメージのタグの値をみて、Semantic Versioningなどの規則に従ってどちらが新しいバージョンか判別する仕組みがあります。 上記で示したworkflowはタグの値に関係なく、後に実行されたパイプラインでイメージタグを上書きします。 この挙動が問題であれば値を検証して、上書きすべきか判定する必要があります。 まとめ このやり方を用いることで、GitOpsがGitHub上で完結して、非常にシンプルにKubernetesアプリケーションの継続的デリバリーが実践できるのではないのかなと思います。 CDツールのエラーもGitHub Actions上に集約できるので、普段のCIプロセスと同じ方法で実行結果やエラーの内容が確認できるのは非常に嬉しいですね。 色々なツールが多く存在し、目移りすることも多いKubernetesのツール選定ですが、身の丈にあったツールを利用してKubernetesアプリケーション開発の生産性を高めていきたいですね。
アバター
Introduction My name is PannNuWai and I work in the Global Development Group at KINTO Technologies. In Global Development Group test automation team, I build and maintain test automation environments for the product development teams, and write test scripts with the product test team. In my previous company, I was involved in testing, but after joining KINTO Technologies, I had my first experience with automation testing using Appium, which provided me with valuable learning opportunities. I didn’t have any experience in Appium, so I had to start studying from scratch. However, I am now capable of handling everything from initial configuration to designing server architecture. In this automation testing role, I primarily focused on testing smartphone apps and will outline the issues I resolved during the process. In this article, I would like to talk about how to switch to dark mode using Appium version 1.22.3 for automation testing. What is automation testing? Software testing is the process of identifying issues in software to ensure that defective products are not released. In this article, 'automation testing' refers to the use of tools that support and automate the software testing process. Benefits of automation testing [^1] Early fault detection Improve quality while keeping costs down Tests can be performed even with a lack of human resources Tests can be performed more quickly Human error can be eliminated Tests can be performed outside of business hours [^1]: https://products.sint.co.jp/obpm/blog/test_automation What is Appium? It is an open source tool for testing native, web views, and hybrid apps on iOS, Android, and desktop platforms. [^2] [^2]: https://appium.io Appium supports Java, PHP, and Python programming languages, so it is an automation testing tool that testers can easily use while choosing their preferred language. There are three components: Appium Client Appium Server End Device in the architecture of Appium. The mobile device and app details are set up in the Appium Client. The Appium Server uses Node.js language to connect the simulator (iOS) or emulator (Android) while launching the json file. Finally, the end device is managed through the Appium server that has been launched. What is Appium Inspector? Appium Inspector is a standard procedure for uniquely identifying the UI elements of a mobile app. It works on both actual devices or simulators (iOS) or emulators (Android). Note - the Appium Inspector tool is specifically designed to retrieve only native mobile application attributes, so it does not support finding locators in a web browser (Chrome). The Appium desktop application is a combination of the Appium server itself and the Element inspector, designed to detect all visible elements of mobile applications while developing test scripts. [^3] [^3]: https://www.kobiton.com/book/chapter-5-the-appium-inspector-2 What is Dark Mode? Dark mode is a display setting for the user interface of smartphones, laptops, etc. Instead of displaying dark text (dark mode) on a bright screen, light text (light mode) is displayed on a black screen. In addition to the existing dark mode feature on both Android and iOS phones, we often use the dark mode feature in our apps. When testing mobile app automation, testing the dark mode feature was also a key checking point. So, I would like to talk about the dark mode of mobile apps using Appium. Problem There is a problem when using Appium to test dark mode. For example, when testing the login screen to see if the characters of username and password are displayed, the Appium inspector retrieves the location of the element for username and password . Normally, you only need to check that the element is displayed as follows. AssertTrue(driver.findElementByXPath("USER_NAME").isDisplayed()); AssertTrue(driver.findElementByXPath("PASSWORD").isDisplayed()); However, in dark mode, it is not enough to just retrieve the location of the element and check its display. Checking that the screen has changed to black is an important part of dark mode. You need to check the hexadecimal values for the black and white colors. Test Method Now, let’s actually write the source code of the dark mode test case using Appium. changeToDarkTheme Step 1 Retrieve the location (ElementId) of the element from the Appium inspector. Step 2 Use assertElementColorMode(MobileElement elementId, ColorMode colorMode) to confirm if light mode is the Default setting. Step 3 Press the dark mode button. Step 4 Use assertElementColorMode(MobileElement elementId, ColorMode colorMode) to confirm if the Display setting changes to dark mode. public class DisplayChangePage extends Base { public static final String THEME_CELL_ID = "id/theme_parent"; @Test(groups = "DisplayChangePage", dependsOnGroups = "Setting") public void changeToDarkTheme() { driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); MobileElement themeCell = getDriver().findElementById(THEME_CELL_ID); assertElementColorMode(themeCell, ColorMode.LIGHT); themeCell.click(); driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); tapElement( findElementByTextContains(ViewType.CHECKED_TEXT, resourceText("darkTheme")) ); themeCell = getDriver().findElementById(THEME_CELL_ID); assertElementColorMode(themeCell, ColorMode.DARK); } } assertElementColorMode Set the ElementId where the Theme cell is located and the ColorMode you want to change as parameters. Step 1 Retrieve evidence of the Element where the Theme cell is located. Use getElementBufferedImage(MobileElement element) to save the evidence as an image file. Step 2 Check that the saved image file does not become null . Step 3 Get the color from the (x = 10, y = 10) image file point and check the color of the dark mode you want to change. public interface AppiumHelpersInterface extends FindElementsInterface { AppiumDriver<MobileElement> getDriver(); Device getDevice(); /** * Get buffered image of mobile element * * @param element Mobile element * @return Buffered image */ default BufferedImage getElementBufferedImage(MobileElement element) { File image = element.getScreenshotAs(OutputType.FILE); try { return ImageIO.read(image); } catch (IOException e) { return null; } } /** * Assert element's color mode * * @param element Mobile Element * @param mode Color mode */ default void assertElementColorMode(MobileElement element, ColorMode mode) { BufferedImage image = getElementBufferedImage(element); Assert.assertNotNull(image); Assert.assertTrue(Utils.getColorString(image, 10, 10).matches(mode.cellRegex())); } } getColorString Change the color of x-point and y-point of the acquired image to hexadecimal and return the array. /** * Get color string from image at point x and y * * @param image BufferedImage * @param x int * @param y int * @return Hexadecimal Color String */ public static String getColorString(BufferedImage image, int x, int y) { int rgba = image.getRGB(x, y); int[] rgb = new int[]{ (rgba >> 16) & 0xff, (rgba >> 8) & 0xff, (rgba) & 0xff }; return String.format("%02x%02x%02x", rgb[0], rgb[1], rgb[2]); } cellRegex Determine the values for dark and light modes. public enum ColorMode { LIGHT, DARK; public String cellRegex() { // 22222 - lighter black if (this == DARK) return "2[(0-9|a-f)]2[(0-9|a-f)]2[(0-9|a-f)]"; // ffffff return "f[(0-9|a-f)]f[(0-9|a-f)]f[(0-9|a-f)]"; } } public interface ColorModeInterface { String darkModeScript(); Map<String, Object> darkModeSettings(); Map<String, Object> lightModeSettings(); default void configureDarkMode(ColorMode mode) { getDriver().executeScript(darkModeScript(), mode == ColorMode.DARK ? darkModeSettings() : lightModeSettings()); } } Caution This case involves using Appium, so only the native app's dark mode feature can be utilized. Summary In this article, I have explained how to switch to dark mode. It can be used on both iOS (version 13 and up) and Android (version 5.0 and up) for dark mode automation testing. This time, I tested the Native App's dark mode feature, but I would also like to explore testing the Web App's dark mode feature in the future Since December, the number of members on the test automation team in Global Development has increased. In the future, I hope to collaborate with team members on automation testing using not only Appium but also other tools like Katalon. Reference DarkMode Appium Architecture
アバター
Hello everyone, this is Martin from the Mobile Development Group here at KINTO Technologies! With this guide I hope to give you a quick overview on how to build your TFLite (TensorFlow Lite) models from scratch so let's dive straight into it. This article is the entry for December 9th in the KINTO Technologies Advent Calendar 2024 🎅🎄 Preparation There are basically two ways to prepare your dataset. One is to do the annotation process locally and the other is to annotate your dataset online whilst collaborating and sharing the initial workload better with your team members. This guide tries to emphasize the use of Roboflow ( https://roboflow.com/ ). Roboflow's model export functionality allows you to export trained models in various formats, making it easy to deploy them in your own applications or further fine-tune them. In our case we want to train TFlite models so we would want to export to the TFRecord format as shown in the image below. However, in case you are not using any third party online annotation tools such as Roboflow to annotate your images online and you want to annotate locally, you could try out the free Python library labelImg: https://github.com/HumanSignal/labelImg In general, either locally or online, first we need to collect the dataset of images and label them to get the corresponding bounding box classification meta data (xml) files. (in our case creating the VOC [Visual Object Classes] Pascal meta data) more information about Pascal VOC can be found here: https://roboflow.com/formats/pascal-voc-xml After creating a Google Cloud Platform standard runtime instance you will need to connect it to your Colab notebook. In essence, Google Colab provides a secure, scalable, and collaborative platform for data science and machine learning teams within organizations. Once that is done we first need to import the necessary libraries to get Tensorflow going (Step 1) Creation of GCP standard instance: Creation of Colab Enterprise ( https://cloud.google.com/colab/docs ) notebook: Connect your Google Cloud bucket (Step 4) Execution Install the TensorFlow Object Detection API (Step 5 in this guide) Generate the TFRecord files required for training. (need generate_tfrecord.py script to produce csv files for this) Edit the model pipeline config file and download the pre-trained model checkpoint Train and evaluate the model Export and convert the model into TFlite(TensorFlow Lite) format Deployment Deploy the TFlite model on Android / iOS / IoT devices So now get's started Here are the steps that you should undergo within your Colab Enterprise notebook in detail: 1) Import Libraries !pip install tensorflow==2.13.0 import os import glob import xml.etree.ElementTree as ET import pandas as pd import tensorflow as tf print(tf.__version__) * 2) Create customTF2 , training and data folders in your GCP cloud storage bucket (Necessary only the first time) * Create a folder named customTF2 in your GCP cloud storage bucket Create two sub-folders called training and data inside the customTF2 folder (The training folder is where the checkpoints will be saved during training) Creation of folder structure in your GCS bucket: 3) Download, save and upload the following as generate_tfrecord.py file to the customTF2 folder to your bucket. (Necessary only for the first time) from __future__ import division from __future__ import print_function from __future__ import absolute_import import os import io import pandas as pd import tensorflow as tf import argparse from PIL import Image from tqdm import tqdm from object_detection.utils import dataset_util from collections import namedtuple, OrderedDict def __split(df, group): data = namedtuple('data', ['filename', 'object']) gb = df.groupby(group) return [data(filename, gb.get_group(x)) for filename, x in zip(gb.groups.keys(), gb.groups)] def create_tf_example(group, path, class_dict): with tf.io.gfile.GFile(os.path.join(path, '{}'.format(group.filename)), 'rb') as fid: encoded_jpg = fid.read() encoded_jpg_io = io.BytesIO(encoded_jpg) image = Image.open(encoded_jpg_io) width, height = image.size filename = group.filename.encode('utf8') image_format = b'jpg' xmins = [] xmaxs = [] ymins = [] ymaxs = [] classes_text = [] classes = [] for index, row in group.object.iterrows(): if set(['xmin_rel', 'xmax_rel', 'ymin_rel', 'ymax_rel']).issubset(set(row.index)): xmin = row['xmin_rel'] xmax = row['xmax_rel'] ymin = row['ymin_rel'] ymax = row['ymax_rel'] elif set(['xmin', 'xmax', 'ymin', 'ymax']).issubset(set(row.index)): xmin = row['xmin'] / width xmax = row['xmax'] / width ymin = row['ymin'] / height ymax = row['ymax'] / height xmins.append(xmin) xmaxs.append(xmax) ymins.append(ymin) ymaxs.append(ymax) classes_text.append(str(row['class']).encode('utf8')) classes.append(class_dict[str(row['class'])]) tf_example = tf.train.Example(features=tf.train.Features( feature={ 'image/height': dataset_util.int64_feature(height), 'image/width': dataset_util.int64_feature(width), 'image/filename': dataset_util.bytes_feature(filename), 'image/source_id': dataset_util.bytes_feature(filename), 'image/encoded': dataset_util.bytes_feature(encoded_jpg), 'image/format': dataset_util.bytes_feature(image_format), 'image/object/bbox/xmin': dataset_util.float_list_feature(xmins), 'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs), 'image/object/bbox/ymin': dataset_util.float_list_feature(ymins), 'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs), 'image/object/class/text': dataset_util.bytes_list_feature(classes_text), 'image/object/class/label': dataset_util.int64_list_feature(classes), })) return tf_example def class_dict_from_pbtxt(pbtxt_path): # open file, strip \n, trim lines and keep only # lines beginning with id or display_name with open(pbtxt_path, 'r', encoding='utf-8-sig') as f: data = f.readlines() name_key = None if any('display_name:' in s for s in data): name_key = 'display_name:' elif any('name:' in s for s in data): name_key = 'name:' if name_key is None: raise ValueError( "label map does not have class names, provided by values with the 'display_name' or 'name' keys in the contents of the file" ) data = [l.rstrip('\n').strip() for l in data if 'id:' in l or name_key in l] ids = [int(l.replace('id:', '')) for l in data if l.startswith('id')] names = [ l.replace(name_key, '').replace('"', '').replace("'", '').strip() for l in data if l.startswith(name_key)] # join ids and display_names into a single dictionary class_dict = {} for i in range(len(ids)): class_dict[names[i]] = ids[i] return class_dict if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create a TFRecord file for use with the TensorFlow Object Detection API.', formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('csv_input', metavar='csv_input', type=str, help='Path to the CSV input') parser.add_argument('pbtxt_input', metavar='pbtxt_input', type=str, help='Path to a pbtxt file containing class ids and display names') parser.add_argument('image_dir', metavar='image_dir', type=str, help='Path to the directory containing all images') parser.add_argument('output_path', metavar='output_path', type=str, help='Path to output TFRecord') args = parser.parse_args() class_dict = class_dict_from_pbtxt(args.pbtxt_input) writer = tf.compat.v1.python_io.TFRecordWriter(args.output_path) path = os.path.join(args.image_dir) examples = pd.read_csv(args.csv_input) grouped = __split(examples, 'filename') for group in tqdm(grouped, desc='groups'): tf_example = create_tf_example(group, path, class_dict) writer.write(tf_example.SerializeToString()) writer.close() output_path = os.path.join(os.getcwd(), args.output_path) print('Successfully created the TFRecords: {}'.format(output_path)) 4) Mount your GCS bucket, install GCSFUSE and link your folder from google.colab import auth auth.authenticate_user() !echo "deb https://packages.cloud.google.com/apt gcsfuse-bionic main" > /etc/apt/sources.list.d/gcsfuse.list !curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - !apt -qq update !apt -qq install gcsfuse !gsutil ls -r gs://your-cloud-storage-bucket-name !mkdir customTF2 !gcsfuse --implicit-dirs your-cloud-storage-bucket-name customTF2 5) Clone the tensorflow models git repository & Install TensorFlow Object Detection API %cd /content # clone the tensorflow models on the colab cloud vm !git clone --q https://github.com/tensorflow/models.git #navigate to /models/research folder to compile protos %cd models/research # Compile protos. !protoc object_detection/protos/*.proto --python_out=. # Install TensorFlow Object Detection API. !cp object_detection/packages/tf2/setup.py . !python -m pip install . 6) Test the model builder (Suggested) %cd /content/models/research # testing the model builder !pip install 'tf-models-official >=2.5.1, <2.16.0' !python object_detection/builders/model_builder_tf2_test.py 7) Download pre-trained model checkpoint (Necessary only for the first time) Current working directory is /content/customTF2/customTF2/data/ Download ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz into the data folder & unzip it. A list of detection checkpoints for other tensorflow 2.x can be found here . %cd /content/customTF2/customTF2/data/ #Download the pre-trained model ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz into the data folder & unzip it. !wget http://download.tensorflow.org/models/object_detection/tf2/20200711/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz !tar -xzvf ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz 8) Get the model pipeline config file, make changes to it and put it inside the data folder (Necessary every time and when you change the amount of class numbers) Current working directory is /content/customTF2/customTF2/data/ Download ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config from /content/models/research/object_detection/configs/tf2 . Make the required changes to it and upload it to the /content/customTF2/customTF2/data/ folder. OR Edit the config file from /content/models/research/object_detection/configs/tf2 in colab and copy the edited config file to the /content/customTF2/customTF2/data folder. You can also find the pipeline config file inside the model checkpoint folder we just downloaded in the previous step. You need to make the following changes: change num_classes to the number of your classes change test.record path, train.record path & labelmap path to the paths where you have created these files (paths should be relative to your current working directory while training) change fine_tune_checkpoint to the path of the directory where the downloaded checkpoint from step 12 is change fine_tune_checkpoint_type with value classification or detection depending on your classification type change batch_size to any multiple of 8 depending upon the capability of your GPU (eg:- 24,128,...,512) - usually 24 for smaller datasets and 32 for larger datasets works well with a standard colab enterprise instance change num_steps to number of steps you want the detector to train. #copy the edited config file from the configs/tf2 directory to the data/ folder in your GCP storage !cp /content/models/research/object_detection/configs/tf2/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config /content/customTF2/customTF2/data In the next step we want to make use of the official TensorBoard tool to visualize our runs and graphs to inspect learning and classification loss over time. More information on how to read graphs and how to use the tool can be found here: https://www.tensorflow.org/tensorboard/get_started#:~:text=TensorBoard%20is%20a%20tool%20for,during%20the%20machine%20learning%20workflow . 9) Load TensorBoard (Recommended) # cload tensorboard %cd /content/customTF2/customTF2/training # !pip install tensorboard # tensorboard --inspect --logdir /content/customTF2/customTF2/training # !gcloud init # !gcloud auth application-default login %reload_ext tensorboard %tensorboard --logdir '/content/customTF2/customTF2/training' 10) Train the model Navigate to the object_detection folder in colab vm %cd /content/models/research/object_detection 10 (a) Training using model_main_tf2.py (Suggested method) Here {PIPELINE_CONFIG_PATH} points to the pipeline config and {MODEL_DIR} points to the directory in which training checkpoints and events will be written. For best results, you should stop the training when the loss is less than 0.1 if possible, else train the model until the loss does not show any significant change for a while. The ideal loss should be below 0.05 (Try to get the loss as low as possible without overfitting the model. Don’t go too high on training steps to try and lower the loss if the model has already converged viz. if it does not reduce loss significantly any further and takes a while to go down. ) !pip install tensorflow==2.13.0 # Run the command below from the content/models/research/object_detection directory """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training --alsologtostderr 10 (b) Evaluation using model_main_tf2.py (Optional, just if you want more customization) You can run this in parallel by opening another colab notebook and running this command simultaneously along with the training command above (don't forget to mount your gcp storage, clone the TF git repo and install the TF2 object detection API there as well). This will give you validation loss, mAP, etc so you have a better idea of how your model is performing. Here {CHECKPOINT_DIR} points to the directory with checkpoints produced by the training job. Evaluation events are written to {MODEL_DIR/eval} . # Run the command below from the content/models/research/object_detection directory """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory CHECKPOINT_DIR=${MODEL_DIR} NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --checkpoint_dir=${CHECKPOINT_DIR} \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training/ --checkpoint_dir=/content/customTF2/customTF2/training/ --alsologtostderr Retraining your model (In case you get disconnected) If you get disconnected or lose your session on colab vm, you can start your training where you left off as the checkpoint is saved on your cloud storage inside the training folder. To restart the training simply run steps 1, 4, 5, 6, 9 and 10. Note that since we have all the files required for training like the record files, our edited pipeline config file, the label_map file and the model checkpoint folder, we do not need to create these again. The model_main_tf2.py script saves the checkpoint every 1000 steps. The training automatically restarts from the last saved checkpoint itself. However, if you see that it doesn't restart training from the last checkpoint you can make 1 change in the pipeline config file. Change fine_tune_checkpoint to where your latest trained checkpoints have been written and have it point to the latest checkpoint as shown below: fine_tune_checkpoint: "/content/customTF2/customTF2/training/ckpt-X" (where ckpt-X is the latest checkpoint) 11) Test your trained model Export inference graph Current working directory is /content/models/research/object_detection %cd /content/models/research/object_detection !pip install tensorflow==2.13.0 ##Export inference graph !python exporter_main_v2.py --trained_checkpoint_dir=/content/customTF2/customTF2/training --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --output_directory /content/customTF2/customTF2/data/inference_graph Test your trained Object Detection model on images (Provide test image of your liking and adjust image_path) Current working directory is /content/models/research/object_detection %cd /content/models/research/object_detection # Different font-type for labels text.(This step is optional) !wget https://www.freefontspro.com/d/14454/arial.zip !unzip arial.zip -d . %cd utils/ !sed -i "s/font = ImageFont.truetype('arial.ttf', 24)/font = ImageFont.truetype('arial.ttf', 50)/" visualization_utils.py %cd .. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" #Loading the saved_model import tensorflow as tf import time import numpy as np import warnings warnings.filterwarnings('ignore') from PIL import Image from google.colab.patches import cv2_imshow from object_detection.utils import label_map_util from object_detection.utils import visualization_utils as viz_utils IMAGE_SIZE = (12, 8) # Output display size as you want import matplotlib.pyplot as plt PATH_TO_SAVED_MODEL="/content/customTF2/customTF2/data/inference_graph/saved_model" print('Loading model...', end='') # Load saved model and build the detection function detect_fn=tf.saved_model.load(PATH_TO_SAVED_MODEL) print('Done!') #Loading the label_map category_index=label_map_util.create_category_index_from_labelmap("/content/customTF2/customTF2/data/label_map.pbtxt",use_display_name=True) def load_image_into_numpy_array(path): return np.array(Image.open(path)) # Replace with your test image image_path = "/content/customTF2/customTF2/data/images/your_test.jpg" #print('Running inference for {}... '.format(image_path), end='') image_np = load_image_into_numpy_array(image_path) # The input needs to be a tensor, convert it using `tf.convert_to_tensor`. input_tensor = tf.convert_to_tensor(image_np) # The model expects a batch of images, so add an axis with `tf.newaxis`. input_tensor = input_tensor[tf.newaxis, ...] detections = detect_fn(input_tensor) # All outputs are batches tensors. # Convert to numpy arrays, and take index [0] to remove the batch dimension. # We're only interested in the first num_detections. num_detections = int(detections.pop('num_detections')) detections = {key: value[0, :num_detections].numpy() for key, value in detections.items()} detections['num_detections'] = num_detections # detection_classes should be ints. detections['detection_classes'] = detections['detection_classes'].astype(np.int64) image_np_with_detections = image_np.copy() viz_utils.visualize_boxes_and_labels_on_image_array( image_np_with_detections, detections['detection_boxes'], detections['detection_classes'], detections['detection_scores'], category_index, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=.8, # Adjust this value to set the minimum probability boxes to be classified as True agnostic_mode=False) %matplotlib inline plt.figure(figsize=IMAGE_SIZE, dpi=200) plt.axis("off") plt.imshow(image_np_with_detections) plt.show() Converting trained SSD (Single Shot Detector) model to TFLite model 12) Install tf-nightly TFLite converter works better with tf-nightly. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" !pip install numpy==1.26.4 !pip install tf-nightly 13) Export SSD TFLite graph Current working directory is /content/models/research/object_detection # !pip3 uninstall keras # !pip3 install keras==2.14.0 !pip3 install --upgrade tensorflow keras !pip3 install tensorflow=="2.12.0" # !pip3 install --upgrade tensorflow keras # !pip3 install tensorflow=="2.13.1" # !pip3 install numpy --upgrade # !pip3 uninstall numpy # !pip3 install numpy=="1.22.0" # !pip3 install tensorflow --upgrade #!python --version %cd /content/models/research/object_detection !python export_tflite_graph_tf2.py --pipeline_config_path /content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --trained_checkpoint_dir /content/customTF2/customTF2/training --output_directory /content/customTF2/customTF2/data/tflite 14) Convert TF saved model to TFLite model Current working directory is /mydrive/customTF2/data/ %cd /content/customTF2/customTF2/data/ Check input and output tensor names !saved_model_cli show --dir /content/customTF2/customTF2/data/tflite/saved_model --tag_set serve --all Converting to TFlite: Use either Method (a) or Method (b). METHOD (a) Using command-line tool tflite_convert - (Basic model conversion) # The default inference type is Floating-point. %cd /content/customTF2/customTF2/data/ !tflite_convert --saved_model_dir=tflite/saved_model --output_file=tflite/detect.tflite METHOD (b) Using Python API - (For advanced model conversion with optimizations etc) %cd /mydrive/customTF2/data/ #'''******************************** # FOR FLOATING-POINT INFERENCE #*********************************''' #import tensorflow as tf saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' #converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) #tflite_model = converter.convert() #open("/content/customTF2/customTF2/data/tflite/detect.tflite", "wb").write(tflite_model) #'''************************************************** # FOR FLOATING-POINT INFERENCE WITH OPTIMIZATIONS #***************************************************''' import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir,signature_keys=['serving_default']) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.experimental_new_converter = True converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS] tflite_model = converter.convert() with tf.io.gfile.GFile('/mydrive/customTF2/data/tflite/detect.tflite', 'wb') as f: f.write(tflite_model) #'''********************************** # FOR DYNAMIC RANGE QUANTIZATION #************************************* # The model is now a bit smaller with quantized weights, but other variable data is still in float format.''' # import tensorflow as tf # converter = tf.lite.TFLiteConverter.from_saved_model('/content/customTF2/customTF2/data/tflite/saved_model',signature_keys=['serving_default']) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # tflite_quant_model = converter.convert() # with tf.io.gfile.GFile('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''*********************************************************************** # FOR INTEGER WITH FLOAT FALLBACK QUANTIZATION WITH DEFAULT OPTMIZATIONS # ************************************************************************** # Now all weights and variable data are quantized, and the model is significantly smaller compared to the original TensorFlow Lite model. # However, to maintain compatibility with applications that traditionally use float model input and output tensors, # the TensorFlow Lite Converter leaves the model input and output tensors in float''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # tflite_quant_model = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''********************************* # FOR FULL INTEGER QUANTIZATION # ************************************ # The internal quantization remains the same as previous float fallback quantization method, # but you can see the input and output tensors here are also now integer format''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] # converter.inference_input_type = tf.uint8 # converter.inference_output_type = tf.uint8 # tflite_quant_model_full_int = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model_full_int) Read more about post-training quantization here . You can also read about these in this colab notebook. 15) Create TFLite metadata !pip install tflite_support_nightly %cd /content/customTF2/customTF2/data/ %cd tflite/ !mkdir tflite_with_metadata %cd .. Create a labelmap.txt file with the names of the classes written in each line inside the data folder. Finally run the following cell to create the detect.tflite model with metadata attached to it. Current working directory is /content/customTF2/customTF2/data/ %cd /content/customTF2/customTF2/data/ !pip uninstall tensorflow !pip install tensorflow=="2.13.1" # Attach Metadata to TFLite from tflite_support.metadata_writers import object_detector from tflite_support.metadata_writers import writer_utils import flatbuffers import platform from tensorflow_lite_support.metadata import metadata_schema_py_generated from tensorflow_lite_support.metadata import schema_py_generated from tensorflow_lite_support.metadata.python import metadata from tensorflow_lite_support.metadata.python import metadata_writers import flatbuffers import os from tensorflow_lite_support.metadata import metadata_schema_py_generated as _metadata_fb from tensorflow_lite_support.metadata.python import metadata as _metadata from tensorflow_lite_support.metadata.python.metadata_writers import metadata_info from tensorflow_lite_support.metadata.python.metadata_writers import metadata_writer from tensorflow_lite_support.metadata.python.metadata_writers import writer_utils ObjectDetectorWriter = object_detector.MetadataWriter _MODEL_PATH = "/content/customTF2/customTF2/data/tflite/detect.tflite" _LABEL_FILE = "/content/customTF2/customTF2/data/labelmap.txt" _SAVE_TO_PATH = "/content/customTF2/customTF2/data/tflite/tflite_with_metadata/detect.tflite" writer = ObjectDetectorWriter.create_for_inference( writer_utils.load_file(_MODEL_PATH), [127.5], [127.5], [_LABEL_FILE]) writer_utils.save_file(writer.populate(), _SAVE_TO_PATH) # Verify the populated metadata and associated files. displayer = metadata.MetadataDisplayer.with_model_file(_SAVE_TO_PATH) print("Metadata populated:") print(displayer.get_metadata_json()) print("Associated file(s) populated:") print(displayer.get_packed_associated_file_list()) model_meta = _metadata_fb.ModelMetadataT() model_meta.name = "SSD_Detector" model_meta.description = ( "Identify which of a known set of objects might be present and provide " "information about their positions within the given image or a video " "stream.") # Creates input info. input_meta = _metadata_fb.TensorMetadataT() input_meta.name = "image" input_meta.content = _metadata_fb.ContentT() input_meta.content.contentProperties = _metadata_fb.ImagePropertiesT() input_meta.content.contentProperties.colorSpace = ( _metadata_fb.ColorSpaceType.RGB) input_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.ImageProperties) input_normalization = _metadata_fb.ProcessUnitT() input_normalization.optionsType = ( _metadata_fb.ProcessUnitOptions.NormalizationOptions) input_normalization.options = _metadata_fb.NormalizationOptionsT() input_normalization.options.mean = [127.5] input_normalization.options.std = [127.5] input_meta.processUnits = [input_normalization] input_stats = _metadata_fb.StatsT() input_stats.max = [255] input_stats.min = [0] input_meta.stats = input_stats # Creates outputs info. output_location_meta = _metadata_fb.TensorMetadataT() output_location_meta.name = "location" output_location_meta.description = "The locations of the detected boxes." output_location_meta.content = _metadata_fb.ContentT() output_location_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.BoundingBoxProperties) output_location_meta.content.contentProperties = ( _metadata_fb.BoundingBoxPropertiesT()) output_location_meta.content.contentProperties.index = [1, 0, 3, 2] output_location_meta.content.contentProperties.type = ( _metadata_fb.BoundingBoxType.BOUNDARIES) output_location_meta.content.contentProperties.coordinateType = ( _metadata_fb.CoordinateType.RATIO) output_location_meta.content.range = _metadata_fb.ValueRangeT() output_location_meta.content.range.min = 2 output_location_meta.content.range.max = 2 output_class_meta = _metadata_fb.TensorMetadataT() output_class_meta.name = "category" output_class_meta.description = "The categories of the detected boxes." output_class_meta.content = _metadata_fb.ContentT() output_class_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_class_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_class_meta.content.range = _metadata_fb.ValueRangeT() output_class_meta.content.range.min = 2 output_class_meta.content.range.max = 2 label_file = _metadata_fb.AssociatedFileT() label_file.name = os.path.basename("labelmap.txt") label_file.description = "Label of objects that this model can recognize." label_file.type = _metadata_fb.AssociatedFileType.TENSOR_VALUE_LABELS output_class_meta.associatedFiles = [label_file] output_score_meta = _metadata_fb.TensorMetadataT() output_score_meta.name = "score" output_score_meta.description = "The scores of the detected boxes." output_score_meta.content = _metadata_fb.ContentT() output_score_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_score_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_score_meta.content.range = _metadata_fb.ValueRangeT() output_score_meta.content.range.min = 2 output_score_meta.content.range.max = 2 output_number_meta = _metadata_fb.TensorMetadataT() output_number_meta.name = "number of detections" output_number_meta.description = "The number of the detected boxes." output_number_meta.content = _metadata_fb.ContentT() output_number_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_number_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) # Creates subgraph info. group = _metadata_fb.TensorGroupT() group.name = "detection result" group.tensorNames = [ output_location_meta.name, output_class_meta.name, output_score_meta.name ] subgraph = _metadata_fb.SubGraphMetadataT() subgraph.inputTensorMetadata = [input_meta] subgraph.outputTensorMetadata = [ output_location_meta, output_class_meta, output_score_meta, output_number_meta ] subgraph.outputTensorGroups = [group] model_meta.subgraphMetadata = [subgraph] b = flatbuffers.Builder(0) b.Finish( model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER) metadata_buf = b.Output() When asked, proceed with 'Y' 16) Download the TFLite model Congrats, you are done! Final thoughts Google Colab Enterprise is a powerful cloud-based platform for machine learning, making it an ideal environment for building TensorFlow Lite models. After over a year of using this platform, I've found that the most time-consuming part of the process is data preparation and the initial trial-and-error phase. This stage requires significant iteration and testing to identify challenges in recognizing specific parts of the dataset and to address false positives, where images are incorrectly classified. *The Android robot header image was reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の9日目の記事です🎅🎄 弊社KINTOテクノロジーズは、300人を超えるエンジニア中心の組織です。複雑化する事業環境の中で、私たちは常に組織の効率性と創造性のバランスを模索し続けています。拠点の分散やリモートワークの増加により、部門を超えたコミュニケーションは限定的なものとなり、この課題に真剣に向き合う必要がありました。 この記事では日々のコミュニケーションで利用しているSlackを軸にどのように組織を活性化するのか、そしてSlackをベースにタレントサーチを構築してどのようなことを実現しようとしているのか、その取組の第一歩をご紹介すべく技術広報グループの中西が書いています。 タレントサーチとは社員のスキル情報をデータ化して検索できる仕組みのことです なぜタレントサーチが必要だったのか 効率的に業務をこなすことは大切ですが、イノベーションを生み出すためには、偶然の出会いや「無駄」に思える会話が実は重要な役割を果たします。私たちの組織では、日々の業務に集中するあまり、「暗黙知」や「潜在的な可能性」を見落としがちでした。 例えば、「このスキルを持つ人はいないだろうか」と思っても、誰に相談すればいいか分からない。組織の規模が大きくなるにつれ、こうした情報の非対称性は深刻な課題となっていました。そこで私たちは、Slackプロフィールを戦略的に活用し、この課題に正面から取り組むことにしたのです。 Slackプロフィール活用のメリット 技術広報グループの視点 これまで目立たなかった人材の発見 組織内には、その才能や可能性に気づかれていない人材が多く存在します。技術広報グループのメンバーは日々社内の皆さんとコミュニケーションを取っておりますが、それでも全ての社員の皆さんを深く知ることは難しく、Slackプロフィールは、そうした「隠れた人材」を可視化する新しい手段となっていきます。 プロジェクト支援の迅速化 適切なスキルを持つ人材を素早く特定できることで、プロジェクトの立ち上げや課題解決のスピードが劇的に向上することを期待しています。今までは、〇〇というスキルを持っている人居ないですか?などと社内でも口伝てで探し回ったりしていますが我々のような組織のハブとなる組織を介さずにコミュニケーションが取れるネットワークを構築していくことは今後の組織の成長にとってとても重要なことです 部署間のコラボレーション促進 これまで技術広報グループでは、社内の勉強会や交流イベント、社外講師をお招きしての勉強会など、様々な企画を実施し、それまで接点のなかった部署間でのつながりを作り続けることで社内でも自然にコミュニケーションが発生し、一度繋がったところから数珠つなぎにネットワークが構築され、日々新たなコラボレーションが生まれています。今回のSlack施策もそれに拍車をかけていくことでしょう 全社員にとってのメリット キャリア成長の機会拡大 自分のスキルや興味を明確に表現することで、これまで気づかなかった新たな可能性が開かれます。自分では思わぬキーワードでつながることで、草の根で様々な機会が生まれてきます。これは単に業務に限らず、共通の悩みを持つ方々がお互いに学んで成長する機会が生まれてきます。 スキルを持つ同僚への素早いアクセス 具体的には、新入社員が「Next.jsに詳しいフロントエンドエンジニア」を探す際、Slack上ですぐに簡単に検索できるようになり、学習や課題解決における大きな助けとなっていきます。Slack上でメッセージを検索する延長線上に社内のタレントデータベースが構築されて検索できるようになります。 自然な社内交流の活性化 社内には様々な趣味の草の根活動も存在しています。それが趣味と呼べるかどうかは別にして興味領域ごとに、腰の健康に関するチャンネルから簡単に作れるレシピをシェアするチャンネル。各種スポーツやゲーム、もちろん新しい技術に関するチャンネルもありますし、これらに個人がより紐づけやすくなってきて、業務以外でのつながりがあることで、業務で発生した緊急時の対応でもスムーズに執り行えるということもあります。 プロフィール作成をサポートする仕組み 「何を書けばいいか分からない」という声に応えるため、技術広報グループが積極的にサポートしています。このアプローチは、単なる情報収集ではなく、社員一人ひとりの可能性を引き出すための丁寧な対話プロセスです。 弊社はテックブログを開始した当初より社員の皆さんの才能を見つけ出せるようにインタビューを実施させていただいたり、伴走しながら記事の執筆や登壇資料の作成、イベント企画や運営、勉強会の運営サポートなど行っています。今回のプロフィール作成に関しても、この記事を読んでいる社員の方でまだプロフィールを埋めていないという方や、何を書いたらよいかわからないという方はぜひお声がけください。一緒にあなたの魅力を見つけて社内で発信していきましょう! サポート内容: 個別ヒアリングによる経験や興味の引き出し 1対1の対話を通じて、本人も気づいていない潜在的な強みを探ります。 プロフィール作成用のテンプレート 自己表現が苦手な方でも、安心して記入できます。 自己表現が苦手な方への言語化支援 専門スタッフが寄り添いながら、自分の強みや興味を適切に表現する手伝いをします。 テンプレート: 検索結果 今後の展望 現在は手動でのタレントサーチですが、将来的にはAIを活用したスキルマッチングシステムの構築を目指しています。蓄積されたデータを効果的に活用し、より効率的で戦略的な人材活用の実現を視野に入れています。将来的に個々の社員の可能性をさらに深く理解し、最適な機会と結びつけることができるでしょう。 おわりに Slackプロフィールは、単なる自己紹介欄ではありません。それは、人と人とを結びつける組織の潜在能力を引き出すための戦略的なツールであり、一人ひとりの可能性を解放する鍵なのです。 あなたの興味、スキル、可能性を積極的に発信することで、組織全体の可能性を広げることができます。私たちは、この小さな一歩が、やがて大きな変革につながると信じています。
アバター