TECH PLAY

KINTOテクノロジーズ

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

969

この記事は、 KINTOテクノロジーズ・アドベントカレンダー2024 の12日目の記事です🎅🎄 こんにちは。「KINTOかんたん申し込みアプリ」のAndroid開発チームメンバーです。今日は、私たちの既存のアプリに Kotlin Multiplatform (KMP) を実装するプロセス、その理由、そしてそれによってもたらされた変化と改善についてお話ししたいと思います。 私たちは去年からiOSとAndroidプラットフォーム間での開発効率を最大化する方法を探ってきました。 このプロセスの中で、KMPがチームの目に留まりました。この技術がどのように革新的に開発プロセスを改善したかを皆さんにお伝えしたいと思います。 目次 1. 既存のアプリにKMPを実装する理由 2. 既存のアプリへのKMPの統合 2.1 共有コードの配置決定 2.2 共有コードの整理 2.3 KMPモジュールの作成 2.4 マルチモジュール・アーキテクチャとアンブレラモジュール 2.5 CI:AndroidおよびiOSでの共有コードのテスト 3. KMPコードの配布 3.1 KMPコードの配布オプション 3.2 Swift Package Manager (SPM) 3.3 配布の自動化 4. AndroidおよびiOSの実装方法 4.1 機能の選択 5. KMPクロスプラットフォームモジュール実装における課題 6. 効果 7. 今後に向けて:今後の計画と課題 1. 既存のアプリにKMPを実装する理由 当時、私たちのチームはiOS開発リソースの不足に直面していました。 この課題に対処するために、AndroidチームはKotlin Multiplatform (KMP) を活用して、iOSとAndroidの両方のプラットフォームで共有されるビジネスロジックを作成することにしました。 このアプローチにより、オペレーティングシステム間でのコードの重複を削減し、Androidチームは専門知識を活用してiOS開発に対応できるようになりました。 この戦略は、人員の問題を軽減し、開発生産性を大幅に向上させる重要なソリューションとなり、KMPテクノロジーを既存のアプリに統合する決定的な理由となりました。 [背景の概要] iOS開発リソース不足への対処 Kotlinに関するAndroidチームの専門知識の活用 オペレーティングシステム間のコード重複の削減 開発生産性の向上とチーム連携の強化 ※ ビジネスロジックをKMPライブラリにモジュール化することで、オペレーティングシステム間での重複作業を排除しましょう。 2. 既存のアプリへのKMPの統合 私たちは、KMPコードを実装する前に、私たちの共有コードをどこに配置し、どのように整理するかについていくつかの戦略的決定を下しました。 2.1 共有コードの配置決定 現在運営していますモバイルアプリには、Androidリポジトリで作業するAndroidチームと、iOSリポジトリで作業するiOSチームという2つの別々の開発チームによる典型的な設定があります。 KMPを導入するときに最初に生じる疑問は、「共有コードをどこに配置すべきか」ということです。 オプション1:別のリポジトリ内の共有コード このオプションでは、共有コード用の新規リポジトリを作成し、AndroidリポジトリとiOSリポジトリの両方からアクセスできるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph Android Repository AndroidApp end subgraph iOS Repository iOSApp end subgraph KMP Repository KMP end KMP --> AndroidApp KMP --> iOSApp オプション2:Androidリポジトリ内の共有コード このオプションでは、共有コードをAndroidリポジトリに配置し、Androidチームが共有コードベースを管理できるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph Android Repository KMP --> AndroidApp end subgraph iOS Repository KMP --> iOSApp end オプション3:AndroidリポジトリとiOSリポジトリをモノレポに統合する このオプションでは、AndroidリポジトリとiOSリポジトリをモノレポに統合し、両チームが共有コードベースにアクセスできるようにします。このリポジトリ構造は次のようになります。 graph TB; subgraph One Repository KMP --> AndroidApp KMP --> iOSApp end [私たちの決定] 各オプションの長所と短所を検討した結果、共有コードをAndroidリポジトリに配置することに決定しました。この決定は以下の要因に基づいています: 既存のワークフローへの影響を最小限に抑える 共有コードベースの管理が容易になる 2.2 共有コードの整理 共有コードをどこに配置するかを決めたら、次はそれをどのように整理するかを決めました。 私たちの既存のAndroidアプリはマルチモジュール・アーキテクチャに従っているため、共有モジュールとプラットフォーム固有のモジュールを明確に区別したいと考えました。KMPモジュールを既存のAndroidモジュールと一緒に、Androidリポジトリ内の shared ディレクトリに配置することにしました。例: :app // Androidアプリのモジュール :domain // Android 固有のモジュール :shared:api // KMPモジュール :shared:validation // KMPモジュール 2.3 KMPモジュールの作成 KMP用のGradleモジュールは次を含みます。 1. build.gradle.kts ファイル 2. src サブフォルダー Androidモジュールの場合、 com.android.library プラグインを適用し、 android {} ブロックを含めます。 plugins { id("com.android.library") } android { // Android固有の設定 } KMPモジュールの場合、マルチプラットフォームプラグインを使用し、 kotlin {} ブロックを定義します。 plugins { kotlin("multiplatform") } kotlin { // KMPの設定 } この設定により、共有コードベースにおいてAndroid固有の要件とKMP固有の要件の両方に対応できるようになりました。 2.4 マルチモジュール・アーキテクチャとアンブレラモジュール 複数モジュールの制限 Androidでは、複雑なプロジェクトのためにコードを複数のモジュールに分割するのが標準です。ただし、KMPは現在、iOSに対して1つのモジュールのみの公開に対応しています。 例えば、共有コードベースに、 featureA , featureB , featureC という3つのモジュールがあるとします。各モジュールは data モジュールに依存し、 data モジュールは api モジュールに依存しています。 graph LR; api --> data --> featureA data --> featureB data --> featureC これら3つのモジュールをiOSに公開したい場合、理想的なシナリオでは、iOS開発者は次のように必要なモジュールのみをインポートします。 import featureA import featureB <swift code here> ただし、KMPの制限により、このアプローチではiOSアプリ内でコードが重複することになります。 以下のような構造が望ましいのですが、 graph LR; subgraph KMP api --> data --> featureA data --> featureB data --> featureC end featureA --> iOSApp featureB --> iOSApp featureC --> iOSApp 実際には、以下のような構造(重複あり)になってしまいます。 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 アンブレラモジュール この制限を克服するために、 アンブレラモジュール を導入しました。 アンブレラモジュールは、ソースコードを含まない「空の」モジュールであり、依存関係を管理するために使用されます。 graph LR; subgraph KMP subgraph Umbrella api --> data --> featureA data --> featureB data --> featureC end end Umbrella --> iOSApp style Umbrella fill:#8f88 こちらがbuild.gradle.ktsの例です: 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")) } } } } アンブレラモジュールは、iOS開発者の統合プロセスを簡素化し、プラットフォーム間でシームレスかつ効率的な開発体験を確保します。 2.5 CI:AndroidおよびiOSでの共有コードのテスト 私たちは常にコードのテストを記述しており、共有コードも例外ではありません。プラットフォームの違いにより、一部の機能はiOSでは期待どおりに動作しない場合があります。互換性を確保するために、AndroidとiOSの両方でテストを実行します。AndroidはどのOSでもテストを実行できますが、iOSのテストはmacOSで実行する必要があります。 3.KMPコードの配布 KMPコードの記述が完了したら、次のステップはそれをiOSアプリに配布することです。 3.1 KMPコードの配布オプション KMPコードは、ソースコードまたはバイナリで配布できます。 ソースコードの配布 ソースコード配布では、iOS開発者はKMPコードを自らコンパイルする必要があります。このアプローチでは、Java VMやGradleなどのツールを含むKotlinビルド環境をセットアップする必要があります。 課題: すべてのiOS開発者は、KMPビルド環境を設定する必要があります。 これにより、KMPコードを iOSプロジェクトに導入する際の複雑さが増します。 バイナリ配布 より良いオプションはバイナリ配布です。私たちは、プリコンパイル済みのライブラリを提供することで、iOS開発者が追加のビルド環境を管理する必要性をなくし、共有コードの統合がはるかに簡単になるようにしています。 利点: iOS開発者のセットアップ作業を削減します。 環境間で一貫したビルドを確保します。 3.2 Swift Package Manager (SPM) iOSには主に2つの依存管理システムがあります。それは、CocoaPods と Swift Package Manager (SwiftPM)です。どちらを選択するかは、iOSチームの好みによります。幸いなことに、私たちのiOSチームは SwiftPMに完全移行しているため、SwiftPMのみに対応すれば済みます。 Swift Packageとは? Swift Packageは基本的に、次を含むGitリポジトリです。 Swiftソースコード Package.swiftマニフェストファイル Gitタグによるセマンティック・バージョニング SwiftPMによるバイナリ配布 SwiftPM 5.3以降、SwiftPMは binaryTarget に対応しており、ソースコードの代わりに予めコンパイルされたライブラリを配布できるようになっています。 バイナリ配布によるSwift Packageの作成 KMPコードをSwift Packageとして公開する方法を簡単に説明します。 KMPコードを .xcframework にコンパイルします。 .xcframework をzipファイルにパッケージ化し、そのチェックサムを計算します。 GitHubに新しいリリースページを作成し、リリースアセットの一部としてzipファイルをアップロードします。 リリースページからzipファイルのURLを取得します。 URLとチェックサムに基づいて Package.swift ファイルを生成します。 Package.swift ファイルをコミットし、gitタグを追加してリリースをマークします。 gitタグをリリースページに関連付け、GitHubリリースを正式に公開します。 詳細な手順については、[リモートSPMエクスポートに関するKMPドキュメント] を参照してください。( 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 配布の自動化 手動での配布は時間がかかる場合があります。プロセスを円滑化するために、自動化用のGitHub Actionsワークフローを作成しました。 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 # Github リリース用のバージョン名 RELEASE_VERSION="$MODULE_NAME-$VERSION" # Gitタグ名 TAG="$VERSION" TMP_BRANCH="kmp_release_$VERSION" # VERSIONがsemver仕様であるか確認 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 # タグを取得 git fetch --tags # 直前のリリースタグを取得 PREVIOUS_RELEASE_TAG=$(git tag --sort=-creatordate | grep -v ^version | head -n 1) echo "previous release tag: $PREVIOUS_RELEASE_TAG" # Github ドラフトリリースを作成 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" # Github リリースからアップロードされたzip ファイルのAsset APIのURLを取得 # 例: "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') # URLの末尾に拡張子(.zip)を追加 ASSET_API_URL="${ASSET_API_URL}.zip" # Package.swiftを生成 ./scripts/generate_SPM_Manifest_File.sh $ZIPFILE $ASSET_API_URL # Package.swiftをコミットし、タグを追加 git checkout -b $TMP_BRANCH git add . git commit -m "release $VERSION" git tag -a $TAG -m "$MODULE_NAME $VERSION" git push origin $TAG # Githubリリースを更新し、新しいタグに置き換える gh release edit $RELEASE_VERSION --tag $TAG 4.AndroidおよびiOSの実装方法 このプロジェクトで私たちは、Kotlin Multiplatform (KMP) を使用して、既存のアプリに新規共通モジュールを導入しました。プラットフォーム固有の潜在的な問題を最小限に抑えるために、AndroidとiOSで確実に動作する機能を慎重に選択して実装しました。OSに依存しない機能を選択して本番環境での初期テストのために実装をシンプルに保つことによって、クロスプラットフォームモジュールの確立に重点を置きました。以下は、機能選択基準と実装プロセスの概要です。 4.1 機能の選択 KMPを本番環境に導入する際の潜在的な問題を特定するために、プラットフォーム固有の実装に依存せず、かつ最小限の依存関係で扱える機能を優先しました。機能選択の基準に次を含めました: OSに依存しない機能性 :本番環境で予期しない問題が発生するのを避けるために、OSに依存しない機能を選択し、通信、ストレージ、権限など特定のOSレベル制御を必要とする要素を除外しました。 追加ライブラリの最小化 :メンテナンスのリスクを軽減するために、追加ライブラリに依存せずにKotlin標準ライブラリのみで実装できる機能を選択します。 ライブラリの優先順位 :ライブラリを選択する際に、まず公式Kotlinライブラリ、次に公式Kotlinドキュメントで推奨されているライブラリ、最後にサードパーティのライブラリ、と優先順位をつけました。 これらの基準に基づいて、KMPで実装する最初のクロスプラットフォーム機能として 入力検証 を選択しました。そして、 全角/半角文字変換 機能を追加しました。 Android 入力検証の実施 デフォルトでは、Android実装にはライブラリ機能の不足やインターフェースの違いといった問題しかありませんが、それは大した問題ではありませんでした。 入力検証機能を、一般的なオブジェクト指向プログラミング (OOP) の原則に従って構造化し、再利用性と一貫性を重視しました。 1.共通インターフェースの定義 :両方のプラットフォーム間で入力を検証するための一貫した基盤を作るために、 Validator インターフェースと ValidationResult インターフェースを定義しました。 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.入力タイプ別のバリデータの実装 :電子メールやパスワードの検証など、入力タイプごとにバリデータと結果クラスを分けて作成しました。 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.テストコードの作成 :プラットフォーム間でのモジュールの精度を検証するために、 kotlin-test パッケージを使用して広範なテストケースを実施し、Androidと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)}" )) { // 以下の場合 val result = validator(number) /// したがって、以下の通りになる assertNull(result) } } } 全角/半角文字変換の実装 入力検証に加えて、文字変換機能を実装し、アプリケーション要件に基づいて全角文字と半角文字を自動的に変換しました。 1.拡張可能なインターフェースの定義 :多様で複雑な文字変換に対応するため、インターフェースを定義し、あらゆる変換処理を継承できるようにしました。Kotlinには、これを実装するのに役立つ関数型インターフェース ( fun interface ) と演算子関数 ( operator fun ) の機能が備わっています。 fun interface TextConverter { operator fun invoke(input:String):String operator fun plus(other:TextConverter) = TextConverter { input -> other(this(input)) } } 2.変換用のマッピング定数の定義 :全角/半角文字とそれらの変換をリストアップした文字マッピングテーブルを作成し、定義済みマッピングを参照して変換できるようにしました。 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.自動変換機能 :変換機能は、全角文字を半角文字に、またはその逆に自動変換するように設計されており、一貫性のある予測可能な入力体験を実現します。 これらのOS非依存性機能を選択し、それらをKMP で実装することで、AndroidとiOS間で確実に導入できる安定した再利用可能なモジュールを確立できました。 iOSへの統合 私たちのKMPコードはSwift Packageとして配布され、iOSチームはXcodeGenを使用してXcodeプロジェクトファイルを管理しました。iOSアプリへのKMPコードの統合は、 project.yml ファイルに4行のコードを追加するだけで簡単に実行できます。 packages: + Umbrella: + url: https://github.com/your-org/your-android-repository + minorVersion:1.0.0 targets: App: dependencies: + - package:Umbrella - package: ... ただし、私たちのコードはプライベートリポジトリに格納されているため、いくつかの追加設定が必要です。詳細については、以下をご覧ください: SwiftPMでのプライベートリポジトリの資格情報設定 5.KMPクロスプラットフォームモジュール実装における課題 KMP共通モジュールの開発中に、技術的な問題がいくつか発生しました。特に目立ったのは、基本的な機能、マルチバイト文字、符号化の処理に関する問題でした。以下は、これらの問題の概要と、それらをどのように解決したかについてです。 Kotlin標準ライブラリはUnicodeコードポイントに対応していません 入力検証において漢字やサロゲートペアなどのマルチバイト文字を正確に処理するために、Unicodeコードポイントベースの正規表現を実装することにしました。このアプローチにより、文字を単なる個々の文字として扱うのではなく、Unicode範囲内での文字の位置に基づいて文字を正確に一致させて検証できるようになりました。しかし、問題が発生しました。 Kotlinの String クラスは、Unicodeコードポイントの処理をネイティブに対応しておらず、この目的(特にサロゲートペア)のための公式ライブラリも提供していません。 そのため、コードポイントに基づいてマルチバイト文字を正確に処理するために、サードパーティのライブラリを使用しています。これにより、漢字などの複雑な文字を私たちの正規表現内でより正確に一致させることができるようになりました。 非UTFの文字における符号化には対応していません レガシーシステムとの互換性を維持するには、Shift-JIS (MS932) の符号化に対応する必要がありました。しかし、KMPはShift-JIS符号化において、ネイティブに対応していません。 レガシーシステムへのテキスト転送では、MS932で符号化が可能であるか否かを確認する必要があり、そのため、 ktor-client ライブラリを使用して符号化を処理することにしました。ただし、 ktor-client のiOSバージョンはUTFベースの符号化スキームしか対応していないため、MS932符号化を実施するのは困難です。 MS932符号化の制限により、漢字検証のためのコードポイントの使用を断念しました。代わりに、検証に必要な漢字のリスト全体を含む定数を宣言し、必要に応じて参照できるように、これらをUnicodeコードポイントに変換しました。 Unicodeコードポイントの問題 全角文字と半角文字の変換を実施する際に、特定の文字間のコードポイントの不一致が発生し、単純な加算/減算アプローチが効果的でなくなりました。 例えば、日本語の全角文字 ァ' ( U+30A1 ) と ア ( U+30A2 )は、コードポイントが1つ異なるだけです。対照的に、半角文字 ァ ( U+FF67 )と ア ( U+FF71 )は、コードポイントが10異なります。この不一致は、統一された変換アプローチが実現不可能であることを意味していました。 私たちは、すべての変換に対して定数マッピングテーブルを作成し、すべての全角文字と半角文字、およびそれぞれのマッピングを明確に定義することで、この問題を解決しました。このアプローチにより、変換操作においてさまざまな文字を正確に処理できるようになりました。 これらの問題に対処することで、私たちのKMP共通モジュールの安定性と完全性が向上し、AndroidとiOSの両方のプラットフォームで正確な機能を確保できました。 6.効果 技術的な効果: プロセスの一貫性 :KMPの実装により、iOSとAndroid間での動作の不一致が最小化され、QA時のエラー頻度が低下しました。 コードの再利用性 :Androidチームが検証したコードはiOSでも使用されるため、両方のプラットフォームにわたって開発効率が向上します。 OS連携と開発リソースの最適化: コミュニケーション負担の軽減 :KMPにより、Androidチームはほとんどのメンテナンスを単独で処理できるようになり、iOSチームはバージョン アップグレードと軽微なメンテナンスに集中できるようになります。 これは、開発リソースのより効率的な使用と、チーム間の連携強化をもたらします。 プロジェクト管理上の問題: 開発およびメンテナンスのコスト :初期設定には時間がかかりますが、その後は通常通り開発を続けることができます。ただし、Android固有のライブラリおよびJavaベースのライブラリの使用には制限があるため、開発コストが増加する可能性があります。 リソースの割り当て :Androidチームに重点を置いた開発プロセスは、繁忙期におけるリソース不足につながる可能性があります。 KMPで実装された機能は主にAndroid チームが管理しているため、iOSチームの理解度は比較的低く、バランスの取れたリソース配分とトレーニングが求められます。 7.今後に向けて:今後の計画と課題 継続的な教育とトレーニングを通じた将来の拡張計画の実施 現在、私たちのチームは、Kotlin Multiplatform(KMP)技術をより効果的に活用するために、社内教育およびトレーニングプログラムの開発と実施に取り組んでいます。このプログラムは技術的な詳細にとどまらず、チームワークやプロジェクト管理スキルの向上にも重点を置いています。これにより私たちは、技術力の向上だけでなく、プロジェクトの効果的な管理やチーム間の連携強化も目指しています。 今後の計画:KMPへの共通ロジックの移行 今後、私たちのチームは、より多くの共通ロジックをKMPへ移行する予定です。これにより、iOSアプリケーションとAndroidアプリケーション間でのコード再利用を最大化し、メンテナンスの複雑さを軽減することで、開発効率の向上を図ります。 移行へのキーロジック: APIクライアント: BFF、OCR ビジネスロジック: キャッシュ管理など ユーティリティ: テキストの書式設定(時間、使用料)、バージョン比較(利用規約)など ローカルストレージ: アプリ設定、認証トークンなど これらの計画を実行することで、クロスプラットフォーム開発の効率と連携が強化され、私たちのチームがプラットフォーム間での開発タスクをより効果的に行えるようになることが期待されます。 お読みいただきありがとうございました。これが、まだKMPテクノロジーを適用していないチームにとって、役立つ参考資料となることを願っています。
アバター
はじめに こんにちは!KINTO テクノロジーズ セキュリティ・プライバシー グループのたなちゅーです!普段は、SIEM を活用したログ監視・分析や監視体制構築、SCoE グループ( SCoE グループとは? )の一部プロジェクトに参画してクラウド周りのセキュリティ業務に従事しています。自己紹介は こちら 。 本ブログでは、2025年3月26日に名古屋駅近くにある コラボスタイル さんのイベントスペースで開催された『 Sysdig Kraken Hunter ワークショップ 』の参加レポートをお届けします。 イベントスペースの様子 KINTOテクノロジーズの Sysdig Secure 活用 KINTO テクノロジーズでは、Sysdig Secure を主に、Cloud Security Posture Management(CSPM)と Cloud Detection Response(CDR)に利用しています。詳細については、こちらのブログにまとまっていますので、是非、ご覧ください。 KTC クラウドセキュリティエンジニアのとある一日 『Sysdig Kraken Hunter ワークショップ』とは? まずSysdigとは、ネットワークキャプチャツールで有名な Wireshark の共同開発者である Loris Degioanni 氏が創業した企業で、同社が開発したクラウドネイティブ脅威検知のオープンソース標準である Falco を軸に、クラウドやコンテナ向けのセキュリティソリューションを提供しています。弊社では、クラウド環境の権限設定やアカウント/リソース作成などのアクティビティを監視するために Sysdig Secure 使用しています。 『Sysdig Kraken Hunter ワークショップ』では、Amazon EKSのデモ環境に対して擬似的な攻撃を行い、Sysdigで検知や調査、対応などの一連の操作をモジュールに分けて体験するワークショップです。また、ワークショップ後の試験に合格すると、Kraken Hunter の認定バッジが付与されます。 本ブログでは、特に興味深かった3つのモジュールについて紹介します。 モジュール1:擬似攻撃とイベント調査 ここでは、実際に Amazon Elastic Kubernetes Service(Amazon EKS)のデモ環境へ擬似的な攻撃を行い、Sysdig Secureで検知や調査を行いました。 まず、提供されたドキュメントの手順に沿って、Amazon EKS のデモ環境へリモードコード実行(RCE)をシミュレートした以下のような擬似的な攻撃を行いました。 システム上の任意のファイルの内容を取得/書き込み/実行 システム上へファイルをダウンロード 擬似的な攻撃を行った後、ブラウザで Sysdig Secure のコンソールへアクセスし、攻撃を行った対象リソースの状態を見ると、攻撃に関連するイベントを検知していることを確認できます。 参照:sysdig-aws workshop-instructions-JP さらに調査を進めると、先ほどの攻撃を Sysdig Secure がリアルタイムで検知していることが確認できます。 参照:sysdig-aws workshop-instructions-JP このような流れで、擬似的な攻撃をして、Sysdig Secure のコンソールでどのように調査できるのか、攻撃を検知するのかを体験していきました。 自分で攻撃しながらどのように Sysdig Secure で調査できるのかを体験できるため、「Sysdig Secure でなにができるのか」を理解しながら進めることができたように感じました。 モジュール2:ホストとコンテナの脆弱性管理 このモジュールでは、ホストとコンテナの脆弱性管理に関する Sysdig Secure の機能を体験しました。弊社で開発しているプロダクトも、コンテナを使用してマイクロサービス化しているため、関心の高い内容です。 Sysdig Secure は脆弱性管理機能として、「ランタイム脆弱性スキャン」や「パイプライン脆弱性スキャン」、「レジストリ脆弱性スキャン」があるそうです。 「ランタイム脆弱性スキャン」では、過去15分以内に監視対象環境で実行されたすべてのコンテナとSysdig Secure の Agent がインストールされているすべてのホスト/ノードがリストされます。使用中の脆弱性の数と重要度に基づいて、自動的に重要度順にソートされて表示されるため、最も対応が必要なリソースを簡単に確認できます。 参照:sysdig-aws workshop-instructions-JP また、リストされたリソースをクリックしてドリルダウンすることで、脆弱性の詳細を確認できます。 参照:sysdig-aws workshop-instructions-JP 「パイプライン脆弱性スキャン」では、コンテナイメージがレジストリや実行環境に置かれる前に脆弱性をスキャンします。また、「レジストリ脆弱性スキャン」では、レジストリ内のイメージの脆弱性をスキャンします。このように、コンテナイメージの開発/運用フェーズごとに脆弱性の有無をチェックすることができます。 脆弱性を管理するセキュリティ製品はさまざま存在しますが、Sysdig Secure のコンソールは UI が洗練されており、直感的に操作できそうだなと思いました。 モジュール3:コンテナのポスチャー/コンプライアンスの管理 ここでは、クラウド環境のポスチャー/コンプライアンスを管理する Sysdig Secure の機能について体験しました。みなさんもよく見聞きしている通り、クラウド環境の設定ミスに起因するセキュリティインシデントは多数報告されているため、フルクラウド環境でプロダクトを開発している弊社にとっても人ごとではないと考えており、注目している機能の一つです。 ポスチャー/コンプライアンス管理機能として、Sysdig Secure では、自社環境が CIS、NIST、SOC 2、PCI DSS、ISO 27001 などの一般的な規格に準拠しているか確認できます。 参照:sysdig-aws workshop-instructions-JP また、規格に準拠していないリソースをリストし、どのように修正すべきかの手順を確認することもできます。すべてのシチュエーションで実用性の高い手順を確認できるかは未知数ですが、修正手順を調査する工数を削減できることは、管理者としてありがたい配慮だなと感じました。 参照:sysdig-aws workshop-instructions-JP Kraken Hunter 認定試験 Kraken Hunter 認定試験では、試験専用のWebページで30〜40問ほどの問題が出題されました。ワークショップ内で紹介された事柄が出題されるため、真面目に取り組んでいれば合格を狙えると思います。 ワークショップの冒頭に紹介された細かな内容があり苦戦しましたが、なんとか認定試験に合格することができました! 以下は合格者に付与される認定バッジです。 Kraken Hunter 認定バッジ 今後の Sysdig Secure の利用 弊社では Sysdig Secure を使い倒すべく、以下の利活用を検討・推進しています。 CSPM については、弊社ガバナンスルールに沿った独自ポリシールールを Rego で作成し、弊社ガバナンスに沿ったクラウドセキュリティの担保 CDR については、 Falco による独自ルールを作成し、弊社環境に則した脅威検知対象の拡大 コンテナワークロードのセキュリティ担保のために、CWP(Cloud workload protection)の検証と導入 まとめ 今回の Sysdig Kraken Hunter ワークショップでは、Amazon EKSのデモ環境に対して擬似的な攻撃を行い、Sysdig Secure で検知や調査、対応などの一連の操作を体験しました。 弊社では Sysdig Secure の一部の機能のみ使用しているため、今回のワークショップで紹介された機能は、初めてのものばかりで操作に不慣れな部分もありましたが、Sysdig Secure で何ができるのかを理解する良い機会となりました。 また、オフラインのワークショップに参加することで、他の企業が抱える課題や取り組みなど、生の声を聞くことができました。このような機会を作ってくださった運営のみなさまに感謝いたします。 さいごに 私の所属するセキュリティ・プライバシー グループやこのワークショップに一緒に参加した SCoE グループでは、一緒に働いてくれる仲間を募集しています。クラウドセキュリティの実務経験がある方も、経験はないけれど興味がある方も大歓迎です。お気軽にお問い合わせください。 詳しくは、 こちらをご確認ください 。
アバター
先日、同僚との何気ない会話の中で、こんな言葉をふいに投げかけられました。 「AIって、あっという間にここまで来たよね。あと5年もしたら、何ができるようになるか想像もつかないよ。」 自分はAIの専門家ではありません。ですが、たまたまその分野について少し調べたことがあるんです。 そして、その問いに対する答えは「もっと進化してすごくなる」という単純なものではありません。 というのも、AIの発展には、その技術の本質に根ざした壁がいくつか存在しているのです。 繰り返しますが、自分は専門家ではありません。 ただ、その壁とは何か、そしてそれをどう乗り越えられる可能性があるのか、それをお話ししたいと思います。 ボトルネックとしての人間 ご存知のとおり、大規模言語モデルをはじめとする生成AIは、学習のためにデータを必要とします。 しかも、その量は 膨大 です。 そのデータは、Webクローリングやスクレイピングを通じてインターネット上の公開情報から収集されたり、本やコードのリポジトリなどから集められたりします。 そして重要なのは、それらのコンテンツは基本的に 人間が作っている という点です。 ですが、私たち人間は、AIが消費できる速度で新しいデータを作るには、あまりにも遅すぎるのです。 Epoch AI研究所 のPablo Villalobosさんが執筆した 論文 によると、現在の傾向が続けば、 2026年から2032年の間 に、高品質で公開されている人間のテキストデータが枯渇するとされています。 つまり、それ以降は「データを増やしてスケールさせる」というアプローチが通用しなくなるかもしれません。なぜなら、学習に使える新しい人間由来のコンテンツが、もう十分に残っていないからです。 人間が生成した公開テキストの実効ストックおよびLLM学習におけるデータ消費量の予測 データの再利用(いわゆる マルチエポック学習 )は一定の効果がありますが、根本的な解決にはなりません。 さらに悪いことに、現在急増しているデータの中には、質の低いものが多く含まれています。例えば、スパム、SNSのコメント、極端に偏った情報、誤情報、違法なコンテンツなどです。 また、人間が生成するデータは、英語のような広く使われている言語に比べて、あまり普及していない言語では自然と蓄積のペースが遅くなることも指摘しておくべきかもしれません。 そのため、そういった言語においては「人間が作るデータ量」と「AIのデータ需要」とのギャップが、さらに深刻になる可能性があります。 では、どうすればよいのでしょうか? 提案されている解決策の例としては、以下のようなものがあります: 合成データ(つまりAIが生成したデータ)を学習に使う 分野によっては効果が見込めますが、「モデル崩壊」というリスクも伴います。この問題については次のセクションで詳しく説明します。 非公開データを活用する つまり、企業などが保有しているデータをAI学習に利用するということです。当然ながら、法的・倫理的に重大な問題が生じます。実際に、 New York Times のように、自社コンテンツのスクレイピングをAIベンダーに禁止する企業も出てきています。 効率性の向上 単に大きくするのではなく、 賢く学習させる というアプローチです。 実際、最近のモデルではその兆しが見え始めています。ChatGPTなどを使っていると、「推論(reasoning)」のような、ただの記憶の再生ではなく、複数のステップを論理的につなげる動きが見られるようになっています。 生成モデルの近親交配 先ほど触れたように、学習に使えるデータ量を増やす方法のひとつが、データを 生成 してしまうことです。 ですが、これには独自のリスクがついてきます。 こちらの論文 では、ケンブリッジ大学のZakhar Shumaylovさんが、「人間ではなく、過去のAIモデルが生成したデータで次世代のモデルを学習させると何が起こるのか?」を調査しています。 著者らは、 モデル崩壊 (Model Collapse)と呼ばれる危険なフィードバックループを指摘しています。 AI生成データでの学習が繰り返されると、モデルは現実世界の本来のデータ分布から徐々にズレていきます。 その結果、出力はより一般的に、単調に、そして歪んだものになっていくのです。特に、まれで繊細な特徴は失われやすくなります。 これは主に以下の2つの理由によって起こります: 有限なサンプルにより 統計的な誤差 が世代を重ねるごとに蓄積される 複雑な分布を完全には再現できないことで 機能的な誤差 が生じる モデル崩壊の視覚的イメージ 興味深いことに、人間が生成した元データを10%だけでも保持すれば、モデル崩壊をある程度抑える効果があります。 ですが、 完全に防ぐことはできません 。 人間による本物のデータを意図的に残さない限り、AIモデルはどんどん狭くて自己強化的な世界観に閉じこもっていくことになります。 まさに デジタルな近親交配 です。 さらに、イーストカロライナ大学のGabrielle Steinさんは、AI同士でデータを受け渡す形の「クロスモデル学習」でこの問題が回避できるのかを 検証しました 。 結論としては… あまり効果はなかったようです 。 彼女の研究では、100%、75%、50%、25%、0%といった異なる人間データの割合でトレーニングを実施しました。 その結果、以下のような傾向が見られました: 合成データの割合が増えるにつれて、言語的多様性が徐々に減少 特定の割合で急激に崩壊が進むような「転換点」は見られなかった わずかでも人間のデータを混ぜることで、劣化のスピードは抑えられた 彼女は、 全体の半分以上 を確実に人間が書いたと確認できるコンテンツで構成することが、モデル崩壊を初期段階で食い止める有効な対策だと提案しています。 インターネット上で目にするデータの多くがAIによって生成されたものであり、しかもAIの学習素材のほとんどがインターネットから集められていることを考えると、AIの未来はやや暗いものに見えてくるかもしれません。 AI生成コンテンツが学習データにますます入り込むことで、将来的にモデル崩壊を引き起こすリスクが高まっているのです。 しかし、状況を打開できるかもしれない新たな先進的アプローチも登場しつつあります。 次に来るものは? ここまで紹介してきた課題を解決するために、比較的新しいアプローチが最近少しずつ登場しています。 恒久的な解決策とはいかないまでも、「近親交配+データ不足による崩壊」をしばらくのあいだ 先延ばしにする ことはできるかもしれません。 すでにひとつ例として挙げたのが、AIの「推論(Reasoning)」です。 これは、ChatGPTのようなモデルが最終的な回答を出す前に、複数のステップで内部的に思考・判断を行うような動きです。 もうひとつ有望とされているのが、 検索拡張生成(Retrieval-Augmented Generation / RAG) という手法です。 簡単に言えば、AIモデルが学習済みデータだけでなく、 外部から提供されたドキュメント の情報も使って応答を生成するというアプローチです。 たとえば、LLMにPDFファイルを読み込ませたり、回答の前にインターネット検索させたりするようなケースが該当します。 とはいえ、察しがつくかもしれませんが、これも 本質的なデータ不足の問題を解決するわけではありません 。 なぜなら、モデルに与えられる新しい情報には限りがあるからです。 では、まだ本格的には実現されていないものの、有望とされるアプローチは何でしょうか? その一例が、 仮想現実環境+エンボディド・エージェント (Synthetic Reality + Embodied Agents)という方向性です。 これはAI開発において従来とはまったく異なる発想です。 静的なデータセットから受動的に学ぶのではなく、AIエージェントを動的な仮想環境に配置し、目標達成のために行動・探索・適応をさせるのです。 そこで得られるのは、結果を体験し、仮説を試し、戦略を立てる中で 自ら生成したデータ です。 それは、文脈があり、多様性があり、因果関係に基づいた、極めて質の高い情報です。 この方法であれば、無限に近いバリエーションを持つ環境の中で、 持続可能かつ自己更新型の学習 が可能になります。 人間が書いたテキストの枯渇に依存せず、AI自身の出力に閉じこもるリスクも避けられます。 ……とはいえ、現時点ではまだこの段階には到達していません。 つまり、退屈な作業をいろいろとAIに押し付けることには成功しましたが── 少なくとも今後しばらくの間は、私たち自身がそれなりに働く必要がありそうです。 それでは、また!
アバター
This article is the entry for day 20 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello, my name is Hand-Tomi and I am a mobile app (Flutter) developer at KINTO Technologies ( KTC ). Recently, KTC has launched the “Flutter Team”, and now is actively developing different applications. I would like to introduce some techniques we’ve implemented and found to be particularly effective. This time, I will explain how to use GitHub Actions and Firebase Hosting to provide convenient web previews during code reviews. I hope this article is helpful to you. 🎯Goal The goal of this article is to create a system where a debug web page link is automatically posted as a comment by adding the comment /preview to a pull request. ![preview_comment_and_link](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview_comment_and_link.png =600x) When you click the link above, the Flutter project application will be displayed as shown below. ![preview](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview.png =400x) 🔍Why Use Web Preview? When reviewing code, you need to clone the source code, configure it, and build it to check how it operates, but this process takes time. On the other hand, if you set up Web Preview , you can easily and quickly check how it operates. From here on, I will introduce how to implement this system step by step. Setting up Firebase 🌐Creating a Firebase Project If you don't have a Firebase project yet, create a new project from the console. The project name is "sample". If you don't plan to use other features, you can disable them (though there's no harm in leaving them enabled). Wait a while The project creation is complete! ⚙️ Setting up Firebase CLI I'm planning to set up Firebase Hosting for the Flutter project. Since you can easily set it up using the Firebase CLI, let's set it up in the Terminal. 1. Installing Firebase CLI There are several ways to install the Firebase CLI, but on macOS with npm installed, you can easily install it using the following command: npm install -g firebase-tools For installation on other environments, please see here . 2. Log in to Firebase CLI Run the following command to log in to Firebase on the CLI. firebase login 🔧 Setting up Firebase Hosting Now that we're ready, let's set up Firebase Hosting for the Flutter project. 1. Enabling webframeworks To deploy a Flutter application to Firebase Hosting, you need to enable the experimental feature webframeworks . firebase experiments:enable webframeworks 2. Initializing Firebase Hosting In the root directory of your Flutter project, run the following command to set up Firebase: firebase init hosting After running the above command, you will be asked the following questions: # For the Firebase project, select the sample project you created earlier. ? Please select an option: Use an existing project ? Select a default Firebase project for this directory: sample-1234 (sample) # It's fine to answer "Yes" here. ? Detected an existing Flutter Web codebase in the current directory, should we use this? Yes # It's a question about region selection. I chose the default "us-central1 (Iowa)." ? In which region would you like to host server-side content, if applicable? us-central1 (Iowa) # Since I plan to create it myself, I selected "No." ? Set up automatic builds and deploys with GitHub? No i Writing configuration info to firebase.json... i Writing project information to .firebaserc... ✔ Firebase initialization complete! Once you answer the questions, a firebase.json file will be generated. :::message alert If your Flutter project does not include the Web platform, you may encounter an error. In that case, run the following command to add a Web platform: flutter create . --platform web ::: 3. Deploy Let's try deploying it. firebase deploy When you run the above command, the Hosting URL will be displayed as shown below. ... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/sample-1234/overview Hosting URL: https://sample-1234.web.app If you open this URL, you can see that your Flutter project is displayed correctly. Creating GitHub Actions Next, let's create a YAML file that will be executed when you comment /preview on a pull request. 🔑 Preparing your Firebase Service Account Key To deploy to Firebase through GitHub Actions, you need a Firebase service account key. To easily obtain the key, use the following command: firebase init hosting:github After entering the above command, you will be asked the question below. Specify the repository containing the source code in the format user/repository . # Enter a GitHub repository. Write it as `user/repository`. ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) Hand-Tomi/sample Firebase will then automatically set the service account key in the Secrets of the GitHub repository and provide you with the Secrets constant name as shown below (e.g. FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 ). Save this constant name. ✔ Created service account github-action-1234 with Firebase Hosting admin permissions. ✔ Uploaded service account JSON to GitHub as secret FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234. i You can manage your secrets at https://github.com/Hand-Tomi/sample/settings/secrets. Next, you will be asked the following question, but since you have everything you need, press Control+C (or in the case of Windows, Ctrl+C ) to exit. ? Set up the workflow to run a build script before every deploy? ✍️ Create a YAML File for GitHub Actions Now it's time to create a YAML file for GitHub Actions. Create a YAML file in the .github/workflows directory at the root of the Flutter project and add the following code: name: Command Execute Deploy Web on: issue_comment: types: [created] jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true steps: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 - name: Set Up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install Dependencies run: flutter pub get - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Preview has been deployed. - **Link**: ${{ steps.deploy-web.outputs.details_url }} For firebaseServiceAccount , specify the Secrets’constant name you created beforehand (e.g. FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 ). firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} After that, when you merge into the relevant repository and leave a comment /preview in the pull request, Actions will be executed automatically, and GitHub Actions will comment a link. 💡Reference: Explanation of the YAML Code for GitHub Actions The explanation for the YAML code above is as follows: Execution Timing on: issue_comment: types: [created] When you use issue_comment , a workflow is automatically executed when a comment is created. This comment applies not only to pull requests but also when a comment is made on an Issue. In this article, I want to limit the scope to pull request comments, so I will include github.event.issue.pull_request in if of jobs , ensuring that only pull requests are executed. jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} Additionally, when using issue_comment , you need to change the checkout location. issue_comment does not have the latest commit information for the current pull request, so if you check it out as it is, it will check out to the latest commit on the default branch. Therefore, you need to specify ref in actions/checkout as follows (Reference: https://github.com/actions/checkout/issues/331#issuecomment-1438220926 ). - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 Check Comment Message Check whether the comment message is /preview . Check github.event.comment.body as shown below, and only if the message is /preview , execute the process in the deploy-web job. jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} Prevention of Concurrent Execution If you leave /preview as is and immediately comment /preview again, concurrent execution may occur. concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true In this case, GitHub Actions prevents concurrent execution through concurrency . The important thing is to place it under jobs . jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true If you do not place it under jobs as shown above, concurrency will be executed without checking github.event.comment.body == '/preview' in if , and if you leave a comment other than /preview immediately after commenting /preview , the Action will not be executed. Deploy The following steps are for deploying to Firebase Hosting. - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} : This is the Firebase service account authentication key. Enter the one you previously obtained. expires: 7d : This is the expiration date. If you set it like this, your preview site will expire after 7 days. channelId: "issue_number_${{ github.event.issue.number }}" : This is the name of the Firebase Preview channel. If you specify a channelId other than live , it will be deployed to Firebase Preview and you can set the expiration date. FIREBASE_CLI_EXPERIMENTS: webframeworks : This uses webframeworks , an experimental feature of Firebase CLI. It is indispensable for Flutter Web. Link Comments A comment was left for the link using peter-evans/create-or-update-comment . By using this, you can easily leave reactions and add or edit comments. - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Preview has been deployed. - **Link**: ${{ steps.deploy-web.outputs.details_url }} if: success() : Only if successful will this step be executed. token: ${{ secrets.GITHUB_TOKEN }} : You need a GITHUB_TOKEN to leave a comment. No additional setup is required. issue-number: ${{ github.event.issue.number }} : This specifies which issue to comment on. When the workflow was executed via issue_comment , the issue number can be confirmed with github.event.issue.number . steps.deploy-web.outputs.details_url : This displays the URL obtained from the deployment step above. If you want to include other information, see here . Conclusion The techniques introduced in this article will enable faster and easier verification of operation during code reviews than before. This will improve the development efficiency of the entire team, leading to the delivery of better products. However, when introducing a web preview for debugging, it is essential to consider both security concerns and how to resolve OS-specific differences. Using the OS's functions extensively may result in the disadvantages outweighing the advantages. However, in projects that don't heavily rely on OS functions, the advantages outweigh the disadvantages, so I encourage you to introduce it into your development environment. Also, be sure to check out other articles written by the Flutter team members! Flutter development: Designing the border of a QR code using CustomPaint and Path Thank you for reading this blog to the end.
アバター
This article is the entry for day 19 in the KINTO Technologies Advent Calendar 2024 🎅🎄 This is Nakanishi from Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. We are also running an in-house podcast and we're excited to share some of its episodes in this year's Advent Calendar. What is the Learning Roadside Station? The Learning Roadside Station project was launched to enhance accessibility to the study group sessions frequently held within the company, with the goal of making them more accessible and effective. The initiative aims to support the organization of study group sessions led by enthusiastic employees and promote in-house knowledge sharing. Report on Participation in WWDC The KTC Learning Roadside Station podcast features interviews with team members organizing study group sessions in the company. These interviews are titled "A Peek into the Study Session Next Door". Today, we’re changing things up a bit and interviewing Nakano-san, who recently attended WWDC (Worldwide Developers Conference) in person in the hopes to share with everyone insights and inspirations that Nakano-san brought back from attending the event in person. Interview Akeda-san: Today we are joined by Nakano-san, who attended WWDC in person. Thank you for joining us. Nakano-san: Thank you for joining us. Akeda-san: First, could you please introduce yourself and tell us about the kind of work you usually do? Nakano-san: I am the product manager for the "KINTO Kantan Moushikomi App" (from now on, The App) in the Mobile App Development Group. In my work, I develop The App and synchronize it with new vehicle data from KINTO ONE on the web side. Since this requires regular updates, I participate in weekly meetings to stay informed. In addition to my work, I often serve as an MC for events within KTC. Akeda-san: Thank you. I have the impression that he is well-known within the company. Could you tell us what motivated you to attend WWDC? Nakano-san: Since The App is a mobile application, it must go through App Store review before release. Anyone with an Apple Developer Program–linked account can apply to attend WWDC. At our team we were told that it's open to anyone who wants to apply. I was an iOS engineer at my previous position here and had a developer account back then too, but unfortunately, I wasn’t selected at the time. This time, however, I was fortunate to be chosen and was able to attend. Akeda-san: Was it difficult to secure the funding to attend? Nakano-san: Well, actually, after I found out I was selected, I consulted with my manager about what to do. In the end, I took the matter to our CEO Kotera-san, prepared some materials, submitted them, and received approval. I guess it is because I was lucky that the company encourages employees to attend events. Akeda-san: Could you share any episodes during your time at WWDC that you haven’t mentioned elsewhere? Nakano-san: WWDC was held at Apple Park, where Californian plants filled the courtyard. There were also fruit trees, and employees were told not to pick them. The developer relations staff shared some fun trivia. It was fun. For example, the staff told me how long it would take to walk around the inner and outer perimeter. Akeda-san: Did you learn anything from communicating with engineers and tech developers? Nakano-san: Yes, I had the opportunity to interact with engineers and share what apps we were each developing. There were many people creating apps on their own, and many who weren't software engineers by profession but were developing apps as a hobby. Many people said that coming to Apple Park boosted their motivation. Akeda-san: Did attending WWDC help you with app development or your work in any way? Nakano-san: I had the opportunity to receive direct advice on app development and on several challenges I was facing. As a result, I am moving forward with improvements to The App's UI and UX. The next release will reflect the advice I received. Akeda-san: Can someone who is not confident in English still participate in WWDC? Nakano-san: I don't think it's a problem at all. It's better to be able to speak English, but I think you can get by with just the flow. Since I took an Uber to my destination and could easily specify the location through it, I had no difficulties. Knowing English definitely helps, but if you have the willingness to participate, you'll be fine. Akeda-san: Finally, could you say a few words to everyone listening to this interview? Nakano-san: I hope you will have the opportunity to attend WWDC and bring new ideas to KTC. To make KTC even better, I encourage everyone to take on the challenge. Determination leads to success. Akeda-san: Thank you very much. That's all for the interview with Nakano-san.
アバター
はじめに こんにちは。Toyota Woven City Payment Solution開発Groupで決済関係のバックエンド開発を担当している塩出です。 前回の記事 でも述べた通り、本Groupでは開発にKotlinを使用しており、webフレームワークにはKtor、ORMにはExposedを使用しています。またコードのアーキテクチャーとしてはクリーンアーキテクチャーを採用しています。 当初からKotlinの Result型 を使ってエラーハンドリングしていましたが、開発人数が増えていることもあり、Resultとthrowが混じったコードになっていました。Resultを使っているところにthrowが入ると、型でエラーハンドリングの必要性を表現しているにもかかわらず、try-catchも必要になってきてしまう状態でした。 KotlinではJavaの検査例外がないので、try-catchは簡単に呼び忘れてエラーハンドリング漏れが発生してしまいます。この状況を改善すべくチーム内で話し合ってKotlinのResultを使ったエラーハンドリングの方法を統一しました。 今回は本Groupでどのようにエラーハンドリングを書いているのかを紹介します。 この記事には以下の内容は含まれません クリーンアーキテクチャーの説明 Ktor, Exposedの説明 kotlin-result とKotlin公式のResult型との比較 アプリケーションのディレクトリ構成 本題に入る前に、本Groupでのアプリケーションのディレクトリ構造について説明します。 以下にクリーンアーキテクチャーの有名な図と本Groupのディレクトリ構成を載せます。本Groupではクリーンアーキテクチャーを採用しており、アプリケーションのディレクトリ構造もそれにおおよそ則った形で構成されています。 (出典: The Clean Code Blog ) App Route/ ├── domain ├── usecase/ │ ├── inputport │ └── interactor └── adapter/ ├── web/ │ └── controller └── gateway/ ├── db └── etc ディレクトリとクリーンアーキテクチャー図との対応は以下の通りです。 domain ディレクトリ: entities usecase ディレクトリ: Use Cases adapter/web/controller ディレクトリ: Controllers adapter/gateway ディレクトリ: Gateways 用語のズレは少々ありますが、基本的には domain ディレクトリが円の中心で、 usecase ディレクトリがその外側、 adapter 以下が円の一番外側といった感じになっています。 なので依存を許可する方向としては以下のようになります。 usecase -> domain adapter 以下 -> usecase or domain このような依存の方向性にすることで、webフレームワークやdatabaseの種類などに影響を受けることなく、ビジネスロジックを開発することができます。 エラーハンドリングの方針 基本的にエラーハンドリングは以下の方針にしています。 処理失敗の場合はthrowではなくResult型を使用する 関数がResult型を返却するとき、throwは使わない Exceptionを返却するときは独自定義したException型を利用する 次の章で個別の方針についてコード例を交えながら細かく紹介していきます。 関数が失敗する可能性がある場合は戻り値としてResult型を使用する KotlinではJavaのような検査例外がなく、呼び出し元にエラーハンドリングを強制する仕組みがありません。Result型を使うことで呼び出し元にエラーが返却される可能性があることを明示できるので、エラーハンドリングが漏れる可能性が低くなります。 ただし Result<Unit> のように戻り値を使用しない場合は強制させることはできません。この場合はcustom lintを定義する必要がありますが、現状定義できていません。 コード例 以下は簡単なコードの例です。割り算をする関数を定義する場合、通常分母がゼロの場合はエラーになります。このように関数が失敗する可能性がある場合は戻り値にResult型を指定します。この場合は Result<Int> を指定しています。 fun divide(numerator: Int, denominator: Int) : Result<Int> { if (denominator == 0) { return Result.failure(ZeroDenominatorException()) } return Result.success(numerator/denominator) } Result型でExceptionを返却する場合、そのExceptionを独自定義したExceptionでラップする Repositoryなどは通常interfaceがdomainにあり実装がadapter層にあります。Use Case層からRepositoryの関数を呼び出してエラーハンドリングする場合、adapter層でサードパーティlibraryのExceptionをそのまま返却してしまうと、そのサードパーティのExceptionをUse Case層が知らないといけません。その場合、Use Caseが実質adapter層に依存してしまうことになってしまいます。図で示すと以下のような感じです。 ![依存関係](/assets/blog/authors/reona-shiode/error-handling/dependency.png =400x) interface利用の依存とexceptionの依存(だめな例) それを避けるため、Result型で返却するExceptionは必ず独自定義したExceptionにラップして返却するようにしています。 クリーンアーキテクチャーにおいてExceptionはどこの層なのか悩むポイントですが、個人的にはdomain層だと思っています。 本Groupでは複数サービスで共通のException型を使用しているので、domain libraryとして切り出しています。 Kotlin公式のResult型ではExceptionの型を指定できないので、実装者に独自定義したExceptionを返すように強制できないのが悩みポイントです。その場合は kotlin-result の使用を検討するのが良さそうです。ただ、本GroupではdomainのコードにKotlin公式ではないサードパーティの型が入り込むのを避けたかったため、採用を見送りました。 コード例 以下のinterfaceがdomainに定義されているとします。 data class Entity(val id: String) interface EntityRepository { fun getEntityById(id: String): Result<Entity> } またサードパーティのlibraryが以下のようなmethodを持っていてそれを使う例を考えます。 fun thirdPartyMethod(id: String): Entity { throw ThirdPartyException() } NG例 adapter層の実装で以下のように直接Exceptionを返却してしまうと、 UseCase などの呼び出し元にサードパーティのExceptionが漏れてしまいます。 class EntityRepositoryImpl : EntityRepository { override fun getEntityById(id: String): Result<Entity> { return kotlin.runCatching { thirdPartyMethod(id) } // This returns the third party exception } } OK例 サードパーティのExceptionが呼び出し元に漏れないように、独自定義したExceptionでラップします。 class EntityRepositoryImpl : EntityRepository { override fun getEntityById(id: String): Result<Entity> { return kotlin.runCatching { thirdPartyMethod(id) }.onFailure { cause -> // wrap with our own exception CustomUnexpectedException(cause) } } } 関数がResult型を返却するとき、 throw は使わない もし関数がResult型を返却するか、Exceptionをthrowする場合、呼び出し側は両方に対応しないといけません。仮に関数を作った人が特定のExceptionは呼び出し元にハンドリングしてもらう必要がないと思っても、呼び出し元がハンドリングしたい場合もあります。従って明示的なthrowを使わずに Result型で統一しています。 DBのコネクションエラーなどは実際発生した場合 Use Case層でリカバリーすることは無理なので、adapter層でexceptionをthrowしてそのままAPIレスポンスまでscope outしても良いかもしれませんが、DB更新できないことによるサードパーティのSaaSとの不整合を検出するためのlogを出力したいこともあります。その場合throwでscope outしてしまうとアラートが適切に上がらない可能性が出てきてしまいます。 エラーハンドリングを要否を決めるのは呼び出し側にあると思いますので、関数作成者がエラーハンドリング不要だと思ってもResult型でExceptionを返却するようにしています。 コード例 リポジトリのsave関数を例に、コード例を紹介します。save関数ではEntity classを受け取り、結果を Result<Entity> で返します。 NG例 以下のようにConnection errorが発生したときにthrowをし、それ以外のエラーはResult型で返却するとします。 class EntityRepository(val db: Database) { fun saveEntity(entity: Entity): Result<Entity> { try { db.connect() db.save(entity) } catch (e: ConnectionException) { // return result instead throw OurConnectionException(e) } catch (e: throwable) { return Result.failure(OurUnexpectedException(e)) } return Result.success(entity) } } それを使うUse Case層ではSave時にエラーが発生したら何かしらのactionを取りたいとします。その場合には runCatching (内部ではtry-catchを利用してResult型に変換)を利用しなければなりません。 class UseCase(val repo: EntityRepository) { fun createNewEntity(): Result<Entity> { val entity = Entity.new() return runCatching { // need this runCatching in order to catch an exception repo.saveEntity(entity).getOrThrow() }.onFailure { // some error handling here } } } OK例 OKな例ではすべてのExceptionを独自定義したExceptionにラップしてResult型でそのExceptionを返却します。こうすることで呼び出し元は runCatching を削除することができ、コードがシンプルになります。 class EntityRepository(val db: Database) { fun saveEntity(entity: Entity): Result<Entity> { try { db.connect() db.save(entity) } catch (e: ConnectionException) { return Result.failure(OurConnectionException(e)) } catch (e: Exception) { return Result.failure(OurUnexpectedException(e)) } return Result.success(entity) } } class UseCase(val repo: EntityRepository) { fun createNewEntity(): Result<Entity> { val entity = Entity.new() return repo.saveEntity(entity).onFailure { // some error handling here } } } Result型を使う上での便利な自作関数の紹介 andThen Result型を使っているとそのResultが成功だったときにその値を使って別のResult型を返す処理をしたいことが多々あります。例えば特定のエンティティーに対してステータスの更新をする場合は以下のようになるかと思います。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { val entity = repository.fetchEntityById(id).getOrElse { return Result.failure(it) } val updatedEntity = entity.updateStatus().getOrElse { return Result.failure(it) } return repository.save(updatedEntity) } このような場合はmethodチェーンで処理を繋げられると書きやすくなります。 kotlin-result では andThen という関数が用意されていますが、kotlin公式のResult型にはありません。そこで本Groupでは以下のようなmethodを定義して使っています。 inline fun <T, R> Result<T>.andThen(transform: (T) -> Result<R>): Result<R> { if (this.isSuccess) { return transform(getOrThrow()) } return Result.failure(exceptionOrNull()!!) } これを使うことで上記の例は以下のように書き換えることができます。同じコードの記述が減ったので少しスマートになりました。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { return repository.fetchEntityById(id).andThen { entity -> entity.updateStatus() }.andThen { updatedEntity -> repository.save(updatedEntity) } } doInTransaction for Exposed 本GroupではORMapperとして Exposed を利用しています。Exposedでは transaction というラムダのスコープの中にDB処理を記述する必要があります。 transaction ラムダはそのscopeの中でExceptionがthrowされたら自動的にrollbackしてくれます。Result型を利用するとExceptionをthrowすることがないため、Resultが失敗だったときに自動的にrollbackをする関数を作成しました。 fun <T> doInTransaction(db: Database? = null, f: () -> Result<T>): Result<T> { return transaction(db) { f().onFailure { rollback() }.onSuccess { commit() } } } 先ほどの UseCaseImpl の例に当てはめると以下のように使用できます。 fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> { return doInTransaction { repository.fetchEntityByIdForUpdate(id).andThen { entity -> entity.updateStatus() }.andThen { updatedEntity -> repository.save(updatedEntity) } } } respondResult for Ktor 本Groupは Ktor をWebフレームワークとして利用しています。UseCaseで返却されたResult型をそのままレスポンスに指定できるように respondResult という関数を作成しました。 suspend inline fun <reified T : Any> ApplicationCall.respondResult(code: HttpStatusCode, result: Result<T?>) { result.onSuccess { when (it) { null, is Unit -> respond(code) else -> respond(code, it) } }.onFailure { // defined below respondError(it) } } suspend fun ApplicationCall.respondError(error: Throwable) { val response = error.toErrorResponse() val json = serializer.adapter(response.javaClass).toJson(response) logger.error(json, error) respondText( text = json, contentType = ContentType.Application.Json, status = e.errType.toHttpStatusCode(), ) } 単純ではありますが、この関数を使用することで Result.getOrThrow を呼ばなくても良くなるのでコードが少しスッキリします。 fun Route.route(useCase: UseCase) { val result = useCase.run() call.respondResult(HttpStatusCode.OK, result.map {it.toViewModel()} ) } ちなみに respondError はthrowableからエラーのレスポンスを返却する関数で、KtorのパイプラインでthrowされたExceptionもこの関数でレスポンスを返却するようにしています。Exceptionを処理するKtorのプラグインも自作しています。 val ErrorHandler = createApplicationPlugin("ErrorHandler") { on(CallFailed) { call, cause -> call.respondError(cause) } } さいごに 本Groupでのエラーハンドリングのやり方と、Result型の便利な自作関数を紹介しました。いろんな会社のtech blogをみていると kotlin-result を使っているところが多い印象でkotlin公式のResult型に関しては使っているという情報は少ない印象です。今のところKotlin公式のResult型でも十分にエラーハンドリングできているので皆様もぜひ使ってみてください。
アバター
本記事は、 KINTOテクノロジーズアドベントカレンダー2024 の19日目の投稿です。🎅🎄 はじめに こんにちは。モバイルアプリ開発グループでiOSエンジニアをしている ラセル・ミア です。今日は、iOS 17で導入された新しい @Observable マクロを使用して、SwiftUIのUIを更新するための改善されたアプローチを紹介します。その仕組み、解決する課題、そしてなぜこれを使うべきなのかを説明します。 TL;DR Observationを使用すると、Swiftにおけるオブザーバーデザインパターンを、堅牢で型安全かつ効率的に実装することができます。このパターンでは、Observableオブジェクトがオブザーバーのリストを保持し、特定または一般的な状態変化を通知することができます。これにより、オブジェクト同士を直接結びつけることなく、複数のオブザーバー間で暗黙的に更新を分配できるという利点があります。 Appleドキュメントより 簡単に言えば、 Observation はビューをデータの変更に応答させるための新しく簡単な方法です。 Observationを使用しない場合の課題 Observation を使う前に、従来の方法でUIを更新する手法と、それに関連する課題を説明します。 簡単な例から見てみましょう。 import SwiftUI class User:ObservableObject { @Published var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView:View { @StateObject private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { @ObservedObject var user:User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is (user.age)") } } } User ObservableObject プロトコルに準拠し、状態の監視を可能にしています。 プロパティnameに @Published を付与することで、ビューに変更を通知します。 ParentView @StateObject を使用して User クラスのインスタンスを管理します。 ChildView @ObservedObject を使用してUserオブジェクトを受け取ります。 両方のビューで、 let _ = print("xxx.body") を使用して、ビューの更新をデバッグログに出力します。 プロジェクトをビルドすると、初期状態では次のようなログが表示されます。 ParentView.body ChildView.Body この時点では問題ありません。初期状態で両方のビューが描画されます。しかし、SetNameボタンを押すと、次のようなログが出力されます。 ParentView.body ChildView.Body ParentView と ChildView の両方が再描画されますが、 ParentView は User のプロパティをまったく使用していなかったため、これは予想外です。さらに、 ChildView ではUserモデルに依存しない定数プロパティを使用しているだけですが、それも再描画されています。このように、 ChildView が静的なTextを返すだけの場合でも、 User モデルへの参照を保持しているため、 User モデルが変更されると再描画されてしまいます。これはパフォーマンス上の重大な課題を浮き彫りにしています。ここで、 Observation フレームワークが私たちを救うために登場します。 こんにちは、@Observable! @Observable マクロは、WWDC 2023で ObservableObject とその @Published プロパティの代わりとして紹介されました。このマクロはプロパティを明示的にPublishedとマークする必要をなくし、SwiftUIビューが変更に応じて自動的に再描画されるようにします。 ObservableObject から @Observable マクロに移行するには、観測できるようにしたいクラスに新しいSwiftマクロ @Observable を付けるだけです。また、すべてのプロパティから @Published 属性を削除してください。 @Observable class User { var name = "" let age = 20 func setName() { name = "KINTO Technologies" } } 注意点:  Structは @Observable マクロをサポートしていません。 以上です!これで、UIはnameプロパティが変更されたときにのみ更新されるようになります。この動作を確認するには、以下のようにビューを修正します。 struct ParentView:View { // BEFORE // @StateObject private var user = User() // AFTER @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { // BEFORE // @ObservedObject var user:User // AFTER @Bindable var user:User var body: some View { let _ = print("ChildView.body") VStack { Text("Age is (user.age)") } } } 修正後、カスタムのobservableモデルは @State を使用して宣言できます。この変更により、 @ObservedObject 、 ObservableObject 、 @Published 、 @EnvironmentObject が不要になります。 @Bindable:Observableオブジェクトの変更可能なプロパティに対してバインディングを作成するためのプロパティラッパー型です。 Appleドキュメントより コードを実行すると、初期レンダリング時に以下の出力が表示されます。 ParentView.body ChildView.Body しかし、 setName ボタンを押しても、コンソールには何も表示されません。これは、 ParentView がUserのプロパティを使用していないため、更新する必要がないからです。同様に、 ChildView にも適用されます。 ParentView に Text(user.name) を追加してプロジェクトをビルドします。その後、 setName ボタンを押すと、以下の出力が得られます。 struct ParentView:View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } Text(user.name) // <--- 追加部分 ChildView(user: user) } } } // 出力 ParentView.body // ChildViewで年齢を変更するコードを追加します。 @Observable class User { var name = "" var age = 20 func setName() { name = "KINTO Technologies" } } struct ParentView:View { @State private var user = User() var body: some View { let _ = print("ParentView.body") VStack { Button("Set Name") { user.setName() } ChildView(user: user) } } } struct ChildView:View { @Bindable var user:User var body: some View { let _ = print("ChildView.body") VStack { Button("Change Age") { user.age = 30 } Text("Age is (user.age)") } } } // 出力 ChildView.Body この結果から、ビューが不要な再描画を行わず、正しく更新されていることが確認できます。これはパフォーマンスにおいて大きな改善を示しています。 @Observableの仕組み @Observableの仕組みは一見すると魔法のように思えるかもしれません。モデルに @Observable マクロを付与しただけで、SwiftUIビューが問題なく更新されるようになります。しかし、その背後ではいくつかの重要な処理が行われています。 ObservableObject プロトコルから Observation.Observable プロトコルへ移行しました。さらに、 @Published プロパティラッパーの代わりに @ObservationTracked マクロを使用し、 name と age は @ObservationTracked に関連付けられています。このマクロを展開することで、その実装内容を確認することができます。以下は展開されたコードです。 @Observable class User { @ObservationTracked var name = "" @ObservationTracked var age = 20 func setName() { name = "KINTO Technologies" } @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar() internal nonisolated func access<Member>( keyPath:KeyPath<User, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal nonisolated func withMutation<Member, MutationResult>( keyPath:KeyPath<User, Member>, _ mutation: () throws -> MutationResult ) rethrows -> MutationResult { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } extension User:Observation.Observable {} デフォルトでは、オブジェクトは観測オブジェクトにアクセス可能な、観測可能な型のすべてのプロパティを観測します。ただし、特定のプロパティが観測されないようにしたい場合は、そのプロパティに @ObservationIgnored マクロを付与することで制御できます。 @Observable class User { var name = "" @ObservationIgnored var age = 20 func setName() { name = "KINTO Technologies" } } これにより、ageプロパティの変更は追跡されなくなり、それに伴うUIの更新も発生しなくなります。 パフォーマンス分析 以下は、Instrumentsツールを使用して記録されたビュー更新回数のレポートです。 Observation未使用時 Observation使用時 記録結果からの考察 Observation未使用時(最初のイメージ) このInstrumentsセッションでは、SwiftUIアプリの最適化されていないパフォーマンスが記録されています。 表示されたメトリクス :ビューの更新、プロパティの変更、タイミングの概要が表示されます。 ビューの再描画回数 :初期レンダリングに加え、 Set Name ボタンを3回押した際の再描画がそれぞれ発生し、合計で 9 回の再描画が行われました。 パフォーマンス : 合計再描画時間 : 377.71 マイクロ秒 平均再描画時間 : 41.97マイクロ秒/回 Observation使用時(2番目のイメージ) このセッションでは、Observationフレームワークを使用した最適化されたレンダリングが記録されています。 表示されたメトリクス :最初のセッションと同じメトリクスが表示されますが、効率の向上が反映されています。 ビューの再描画回数 :ボタンを複数回タップしても、初期レンダリングと状態変化時の再描画が1回のみの 3 回の再描画で構成されます。 パフォーマンス : 合計再描画時間 : 235.58 マイクロ秒 平均再描画時間 : 78.53 マイクロ秒/回 定量的な比較 ビューの再描画回数の削減 : Observation未使用時: 9回再描画 。 Observation使用時: 3回の再描画 ( 66.67% 削減) 合計再描画時間の短縮 : Observation未使用時: 377.71 マイクロ秒 Observation使用時: 235.58 マイクロ秒 ( 37.65% 削減) 再描画の効率性 : Observation未使用時:より頻繁で、平均 41.97 マイクロ秒 Observation使用時:再描画回数は少ないが最適化されており、平均 78.53 マイクロ秒 この比較から、再描画回数が少なくなった結果としてインスタンスごとの平均再描画時間がやや増加しているものの、 Observation フレームワークが再描画回数の削減と全体的なレンダリングパフォーマンスの向上に大きく貢献していることが分かります。 まとめ この記事では、iOS 17で導入された新しい @Observable マクロによるSwiftUIの改善点について解説しました。重要なポイントを簡単にまとめると、次のようになります。 以前のアプローチの課題 従来の ObservableObject と @Published プロパティを使用する方法では、不要な再描画が発生し、特に一部のビューが変更されたデータに依存しない場合に、パフォーマンスの問題が生じていました。 @Observable の紹介 この新しいマクロは状態監視プロセスを簡素化し、 @Published 、 @ObservableObject 、 @EnvironmentObject の必要性を排除します。クラスに @Observable を付与し、ビューで @Bindable を使用することで、データの変更を自動的に追跡し、必要な場合にのみビューの更新をトリガーできます。 パフォーマンスの向上 @Observable を使用することで、関連するデータが変更された場合にのみビューが更新され、不要な再描画が削減されるため、パフォーマンスが向上します。さらに、 @ObservationIgnored マクロを活用することで、どのプロパティがUI更新をトリガーするかを開発者が細かく制御できるようになります。 メリット 的確な更新によるパフォーマンスの向上。 ObservableObjectと@Publishedが不要になることでコードが簡潔に。 状態変更を型安全に管理可能。 ビューの更新をトリガーするプロパティを細かく制御できる柔軟性。 @Observable を使用することで、SwiftUIでのUI更新の管理がより簡単かつ効率的になり、エラーが減少します。その結果、開発者にとっては作業効率が向上し、エンドユーザーにとってはスムーズな体験が提供されます。 以上で本日の内容は終了です。楽しいコーディングを! 参照- Observation SwiftUIにおけるObservationを探る(WWDC23)
アバター
This article is the entry for day 17 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-No-Michi-No-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station project was officially launched and later established as a team. As part of this initiative, we also run an in-house podcast, and for this year's Advent Calendar, we’re excited to share some of its episodes with you. What is Manabi-No-Michi-No-Eki (Learning Roadside Station) The Learning Roadside Station is a project launched to enhance accessibility and effectiveness of the study sessions that are frequently held within the company. The aim is to promote knowledge sharing by supporting study sessions led by volunteers within the company. Security & Privacy Study Session In our Podcast, we interview team members who hold in-house study sessions. It´s called “A Peek into the Study Session Next Door”. Today, we welcome Kuwahara-san, Morino-san, and Kasai-san, who are leading the Security and Privacy Study Sessions. Thank you all for joining us today. Could you start by introducing yourselves? Interview Morino-san: I’m Morino from the Security & Privacy Group. It’s a pleasure to be here. Kasai-san: My name is Kasai, and I’m part of the Data & Privacy Governance Team. Thank you for having me. Kuwahara-san: I’m Kuwahara from the Security CoE Group. Pleasure to meet you. HOKA-san: First, could you tell us about your daily work and responsibilities? Morino-san: Our team is divided into three sub-teams. The Data & Privacy Governance Team, the Information Security Team, and the Cybersecurity Defense Team. The Data & Privacy Governance Team is responsible for establishing rules and implementing governance regarding data and privacy. The Information Security Team conducts assessments based on information security standards. The Cybersecurity Defense Team manages vulnerabilities and investigates threat intelligence. Kasai-san: My team, the Data & Privacy Governance Team, establishes rules regarding data security management and personal information governance, conducts risk assessments, and reports the results to management. Kuwahara-san: The Security CoE Group is responsible for the security of cloud environments such as AWS, Azure, and Google Cloud. We set up guardrails to ensure safe cloud usage, monitor security risks, and provide support against potential threats. HOKA-san: Next, could you tell us more about the Security & Privacy Study Session? What kind of study session is it? Kuwahara-san: We started this study session to spread security-related knowledge among employees. Last year, we created detailed security guidelines, and to ensure awareness and adoption, we began hosting these study sessions. Morino-san: Our goal is to establish Security by Design and Privacy by Design as standard considerations in all projects. Kasai-san: Ultimately, we want engineers to be able to focus on development, while we support them with privacy and security risk management. HOKA-san: How has the response been so far? Kuwahara-san: So far, we have hosted four sessions, and the response has been very positive. Many attendees have shared that they found the sessions informative, and we’ve also received valuable feedback for improvements. Morino-san: Some participants have mentioned that our explanations have become clearer, and we continue to refine our approach. HOKA-san: What changes do you hope to see at KTC through these study sessions? Morino-san: We aim to have security and privacy features implemented as standard practices. Kuwahara-san: Ideally, security should become so natural and ingrained that our roles become unnecessary. Kasai-san: I want to create an environment where engineers can focus on development with confidence, knowing that security is taken care of. HOKA-san: Finally, what inspired each of you to pursue this field? Morino-san: I started in software development and gradually developed a deep interest in security. Kasai-san: Initially, I wasn't particularly interested in security or privacy, but I became fascinated by governance and its impact. Kuwahara-san: I began my career as an infrastructure engineer, but after experiencing a security breach on a mail server I was managing, I shifted my focus to security. HOKA-san: Thank you all for your time today. We hope the Security and Privacy Study Session continues to be a valuable resource for everyone at KTC. Thank you everyone. This time, we provided details about the Security and Privacy Study Session, the background to its operation, and future prospects. Please look forward to the next study session!
アバター
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
アバター
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
アバター
This article is the entry for day 16 in the KINTO Technologies Advent Calendar 2024 🎅🎄 1. Introduction Hello. I'm Okita from the Mobile App Development Group. It's December! The end of the year is here! Looking back, many events took place this year. One of the things that strongly stands out is: My participation in Developers Summit 2024 KANSAI as a member of Osaka Tech Lab. So, this time I'll be sharing the following: My participation and presentation at Developers Summit 2024 KANSAI The latest news from Osaka Tech Lab 2. How I Felt Before the Presentation As a sponsor, our company had the opportunity to set up a booth and give a presentation at the summit. *For more details, please check out this article^^ Reflecting on Developers Summit 2024 KANSAI We want to recruit people who will join us in envisioning the future! To that end, we want people to get to know KINTO Technologies Corporation and learn about Osaka Tech Lab! With this goal in mind, our company applied to become a sponsor. This is the first attempt of its kind for Osaka Tech Lab. I was entrusted with the important role of delivering a presentation as a sponsor on behalf of our company. At first, I was worried... I'd never given a speech before, so I wondered if I could do it... But it was a valuable opportunity so I decided to give it my best shot! 3. The Day of My Presentation - a Moment of Tension and Emotion Amidst all this, the day arrived. In fact, I spent the morning of the day refining my presentation and rehearsed until the very last minute. And, overwhelmed with nerves, I arrived at the venue early and spent some time wandering around. Meanwhile, the clock kept ticking, and before I knew it, it was my turn. The theme of my presentation was " Challenges " . "There's no point in panicking now." "Let's stay calm and speak slowly and carefully so that our message can reach as many people as possible." With that in mind, I began to speak slowly. As my speech neared its end, I looked up and saw people wearing black T-shirts. To my surprise, they were colleagues from our company, warmly watching my presentation in their KINTO Technologies T-shirts. I was deeply touched. While the audience of other companies participated in bright-colored T-shirts like orange, yellow, and blue, my coworkers wore the less noticeable black. That was so like the Osaka Tech Lab, which made me chuckle and relax. They quietly watched over me as I struggled to prepare for my presentation. Team members from other offices in our company also supported me, someone with no prior presentation experience, by helping me compile my profile and create the presentation content. A few days before the presentation, the managers and team leaders of the Mobile App Development Group suddenly found themselves scrambling to assist me, as I had reached out for their help. That's right; I just happened to be the one presenting, but the content was the result of the efforts and ideas of many others. Thanks to everyone, 174 people attended my presentation. I would like to take this opportunity to express my gratitude to the event management staff, our company members who supported me, and everyone who participated in the session. 4. How I Felt After the Presentation I would like to share my thoughts as I reflect on my experience after the presentation. I began preparing more than a month before, but I found myself scrambling until the last minute. -> It turned out to be a great experience, but I spent too much time overthinking, so I now believe there were more efficient ways to approach it. As it was my first presentation, I didn’t know how to prepare for it. -> In hindsight, I should have first clarified the purpose, goals, and key message of my speech before preparing the materials. During my rehearsals, no matter how many times I practiced, my presentation always finished in just 20 minutes. But on the day of the presentation, I was able to speak for exactly 30 minutes. I was holding the remote upside down, so even though I pressed the button, the next slide wouldn't show, and I got nervous right from the start.  I prepared the materials in PowerPoint and then exported them as a PDF but the font I had carefully chosen was not reflected in the PDF. It was disappointing, but it became a valuable lesson. As mentioned above, the preparations were very challenging, but in the end, I was able to give a presentation that I have no regrets about. I also made new connections with other companies that participated as sponsors, as well as people who contacted me through 'Ask the Speaker'. And to my surprise, the editorial department of CodeZine kindly offered to release a session report about me. What is the appeal of working at Osaka Tech Lab for PM and mobile app engineers challenging themselves in the Toyota Group? (in Japanese) Truly, I was deeply moved. Summary of my presentation - Learning and Growth This is a brief summary of my presentation. ■ Good things and things I've grown from By delivering my presentation at the event, I was able to organize and verbalize my daily efforts. As a result, the next steps for the project I belong to became clear. I was also able to level up myself.   -> I was able to experience the significance of "input and output" and "verbalization." ■ Improvements It was a bit heavy to have a 30-minute presentation slot for the first time in my life -> It is recommended to experience a presentation at an outside event in the 5 to 10-minute presentation slot first.♪ While I was able to convey my thoughts, I regret that my presentation lacked concrete examples that went a step further. -> I would like to aim for a presentation from which the participants can take away at least one lesson. I should have clarified the main points of my presentation before preparing my presentation materials and story. -> If you create the core of your presentation first, it will be easier to create a consistent story and you will be able to prepare more efficiently. (Sample) Key points for a presentation ■ Purpose of presentation ■ Goal (what to achieve in the presentation) ■ Messages to convey 6. Recent developments at Osaka Tech Lab Well, well. Time flies. It's already December. After the Developers Summit 2024 KANSAI, new developments have emerged at Osaka Tech Lab. In other words, taking on a challenge of organizing external events. The first of our challenges was: "Kansai Front-End Year-End Party 2024 HACK.BAR × KINTO Technologies" In the future, I hope to organize the Mobile App Development Group’s events. It would be fun to collaborate and hold events with other companies ♪ The list of things I want to try is overflowing, and it's becoming a bit of a problem, lol. I want to take one step at a time (or maybe half a step at a time, lol). 7. Conclusion How was it? I hope you found this article interesting. Join us in shaping the future of Osaka Tech Lab as we step into an exciting new era! We look forward to your application! KINTO Technologies Corporation Recruitment Top Page Wantedly
アバター
みなさん、こんにちは。KINTOテクノロジーズのモバイル開発グループのマーティンです!このガイドでは、TFLite(TensorFlow Lite)モデルをゼロから構築する方法を簡単に説明します。それでは、早速始めましょう。 この記事は KINTOテクノロジーズアドベントカレンダー2024の9日目の記事です🎅🎄 準備 データセットを準備する方法は基本的に2通りあります。1つは、ローカルでアノテーションプロセスを行う方法、もう1つは、オンラインでデータセットにアノテーションを行い、チームメンバーと初期の工数をより効率的に共有しながら進める方法です。このガイドでは、Roboflow (1) の使用に重点を置いています。 Roboflow のモデルエクスポート機能を使用すると、トレーニング済みのモデルをさまざまな形式でエクスポートできるため、独自のアプリケーションに簡単に展開したり、さらに微調整したりすることができます。今回の場合、TFlite モデルをトレーニングしたいので、下の画像に示すように TFRecord 形式にエクスポートする必要があります。 ただし、Roboflowのようなサードパーティのオンラインアノテーションツールを用いずに、ローカルで画像にアノテーションを付けたい場合は、無料のPythonライブラリlabelImgを試してみてください: https://github.com/HumanSignal/labelImg 一般的に、ローカルでもオンラインでも、まず画像のデータセットを収集し、それらにラベルを付けて、対応する境界ボックス分類メタデータ (xml) ファイルを取得する必要があります。(この場合は、VOC [Visual Object Classes] Pascal メタデータを作成) Pascal VOCの詳細情報は、こちらで確認できます: https://roboflow.com/formats/pascal-voc-xml Google Cloud Platform の標準ランタイムインスタンスを作成したら、それを Colab ノートブックに接続する必要があります。基本的に、Google Colab は、組織内のデータサイエンスチームと機械学習チームに、安全でスケーラブルな共同作業プラットフォームを提供します。それが完了したら、まずTensorFlow(テンソルフロー)を動作させるために必要なライブラリをインポートする必要があります(ステップ1) GCP標準インスタンスの作成: Colab Enterprise( https://cloud.google.com/colab/docs ) ノートブックの作成: Google Cloud バケットを接続する(ステップ 4) 実行 TensorFlow オブジェクト検出 API をインストールします(このガイドのステップ 5) トレーニングに必要な TFRecord ファイルを生成します。(これには、csv ファイルを生成するための generate_tfrecord.pyスクリプトが必要です) モデルパイプライン構成ファイルを編集し、事前にトレーニングされたモデルのチェックポイントをダウンロードします。 モデルのトレーニングと評価 モデルをエクスポートして、TFlite(TensorFlow Lite)形式に変換します。 展開 TFlite モデルを Android / iOS / IoT デバイスに展開します。 それでは、始めましょう Colab Enterprise ノートブック内で実行すべき手順を詳しく説明します: 1) ライブラリをインポートします !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) GCPクラウドストレージバケットに* customTF2 、 training 、 data *フォルダを作成します(初回のみ必要) GCPクラウドストレージバケットに customTF2 という名前のフォルダを作成します。 customTF2 フォルダ内に training と data という2つのサブフォルダを作成します( training フォルダはトレーニング中にチェックポイントが保存される場所です) GCSバケット内のフォルダ構造の作成: 3) 以下を generate_tfrecord.py ファイルとしてダウンロードして保存し、バケットの CustomTF2 フォルダにアップロードします。(初回のみ必要) 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) 幅、高さ = 画像.サイズ 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)] # id と display_names を1つの辞書に結合します 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) GCSバケットをマウントし、GCSFUSEをインストールしてフォルダをリンクします。 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) TensorFlow モデルのGitレポジトリをクローンし、TensroFlow オブジェクト検出APIをインストールします。 %cd /content # colab クラウドvmに tensorflowモデルをクローンする !git clone --q https://github.com/tensorflow/models.git # protosをコンパイルするために/models/research フォルダにナビゲートします。 %cd models/research # protosをコンパイルします。 !protoc object_detection/protos/*.proto --python_out=. # TensorFlowオブジェクト検出 API をインストールします。 !cp object_detection/packages/tf2/setup.py . !python -m pip install . 6) モデルビルダーをテストします (推奨) %cd /content/models/research # モデルビルダーのテスト !pip install 'tf-models-official >=2.5.1, <2.16.0' !python object_detection/builders/model_builder_tf2_test.py 7)事前にトレーニングされたモデルチェックポイントをダウンロードします(初めての場合のみ必要) 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz を  データ  フォルダにダウンロードして解凍します。 その他の Tensorflow 2.x の検出チェックポイントのリストは こちら 。 %cd /content/customTF2/customTF2/data/ #事前トレーニング済みモデル ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz をダウンロードしてデータフォルダに入れて解凍します。 !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)モデルパイプライン構成ファイルを入手して変更を加え、 データ フォルダに入れます(クラス番号の量を変更するたびに必要) * 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config を /content/models/research/object_detection/configs/tf2 からダウンロードします。必要な変更を加え、 /content/customTF2/customTF2/data/ フォルダーにアップロードします。 または colab内 で /content/models/research/object_detection/configs/tf2 からの構成ファイルを編集し、編集済みの構成ファイルを /content/customTF2/customTF2/data フォルダにコピーします。 パイプライン構成ファイルは、前のステップでダウンロードしたモデルチェックポイントフォルダ内にもあります。 次の変更を行う必要があります。 num_classes を自分のクラス数に変更します。 test.record パス、 train.record パス、 labelmap パスを、これらのファイルを作成した場所のパスに変更します(トレーニング中は現在の作業ディレクトリに対する相対パスである必要があります) fine_tune_checkpoint を、ステップ 12 でダウンロードしたチェックポイントがあるディレクトリのパスに変更します。 fine_tune_checkpoint_type を、分類タイプに応じて数値 classification または detection に変更します。 batch_size を、使用する GPU の能力に応じて 8 の倍数に変更します(例:24、128、...、512) - 通常、標準的な colab enterprise インスタンスでは、小さなデータセットには 24、大きなデータセットには 32 が適しています。 num_steps を、検出器にトレーニングさせたいステップ数に変更します。 #編集した構成ファイルを configs/tf2 ディレクトリから GCP ストレージの data/ フォルダにコピーします。 !cp /content/models/research/object_detection/configs/tf2/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config /content/customTF2/customTF2/data 次のステップでは、公式の TensorBoard ツールを使用して、実行状況やグラフを可視化し、学習と分類の損失を時間経過とともに確認したいと思います。グラフの読み方やツールの使い方に関する詳細情報は、こちらをご覧ください: https://www.tensorflow.org/tensorboard/get_started#:~:text=TensorBoard%20is%20a%20tool%20for,during%20the%20machine%20learning%20workflow。 9) TensorBoardをロードします (推奨) # 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) モデルをトレーニングします colab vm の object_detection フォルダに移動します %cd /content/models/research/object_detection 10 (a) model_main_tf2.py を使用したトレーニング(推奨方法) ここで、 PIPELINE_CONFIG_PATH はパイプライン構成ファイルを指し、 MODEL_DIR はトレーニングのチェックポイントとイベントが書き込まれるディレクトリを指します。 最良の結果を得るためには、損失が0.1未満になった時点でトレーニングを停止してください。それが難しい場合は、損失がしばらくの間、顕著な変化を示さなくなるまでモデルをトレーニングしてください。理想的な損失は0.05未満です(モデルを過学習させずに、損失をできるだけ低く抑えるようにしてください)。モデルがすでに収束している場合(例えば、損失がそれ以上顕著に減少せず、減少に時間がかかる場合)、損失を下げようとしてトレーニングステップを高くしすぎないようにしてください。 !pip install tensorflow==2.13.0 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ 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) これを並行して実行するには、別の Colab ノートブックを開き、上記のトレーニングコマンドと同時にこのコマンドを実行します(その際、gcpストレージをマウントし、TF gitリポジトリをクローンし、TF2オブジェクト検出 API をインストールするのを忘れないでください)。これにより、検証損失、mAP などが表示され、モデルのパフォーマンスがどのようになっているかをより良く把握できます。 ここで、 {CHECKPOINT_DIR} はトレーニングジョブによって生成されたチェックポイントが格納されているディレクトリを指します。評価イベントは {MODEL_DIR/eval} に書き込まれます。 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ 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 モデルの再トレーニング (接続が切れた場合) 接続が切れたり、colab vmのセッションが失われた場合、トレーニングは中断した場所から再開できます。チェックポイントは training フォルダ内のクラウドストレージに保存されています。トレーニングを再開するには、 ステップ 1, 4, 5, 6, 9, 10 を実行してください。 トレーニングに必要なすべてのファイル(記録ファイル、編集済みpipeline configファイル、label_mapファイル、モデルチェックポイントフォルダなど)が揃っているため、これらを再作成する必要はありません。 model_main_tf2.pyスクリプトは、1000ステップごとにチェックポイントを保存します。 トレーニングは、最後に保存されたチェックポイントから自動的に再開されます。 ただし、最後のチェックポイントからトレーニングが再開されない場合は、パイプライン構成ファイルに1つの変更を加えることができます。 fine_tune_checkpoint を、最新のトレーニング済みチェックポイントが書き込まれている場所に変更し、最新のチェックポイントを指すように以下のように設定します。 fine_tune_checkpoint: "/content/customTF2/customTF2/training/ckpt-X" (where ckpt-X is the latest checkpoint) 11) トレーニング済みモデルをテストします 推論グラフをエクスポート 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection !pip install tensorflow==2.13.0 # #推論グラフをエクスポート !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 訓練済みのオブジェクト検出モデルを画像でテストします(好みのテスト画像を提供して image_path を調整します) 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection # ラベルテキストの異なるフォントタイプ。(このステップはオプション) !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" #保存された_モデルのローディング 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='') # 保存したモデルを読み込み、検出機能を構築します。 detect_fn=tf.saved_model.load(PATH_TO_SAVED_MODEL) print('Done!') #ラベル_マップのローディング 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)) # テストイメージに置き換えます 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) # 入力はテンソルでなければなりません。`tf.convert_to_tensor` を使用して変換します。 input_tensor = tf.convert_to_tensor(image_np) # モデルは画像のバッチを想定しているため、`tf.newaxis` で軸を追加してください。 input_tensor = input_tensor[tf.newaxis, ...] detections = detect_fn(input_tensor) # すべての出力はバッチテンソルです。 # numpy 配列に変換し、インデックス [0] を使用してバッチディメンションを削除します。 # 対象とするのは最初の 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 # 検出_クラスは整数でなければなりません。 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, # この値を調整して True として分類される最小確率ボックスを設定します agnostic_mode=False) %matplotlib inline plt.figure(figsize=IMAGE_SIZE, dpi=200) plt.axis("off") plt.imshow(image_np_with_detections) plt.show() トレーニング済みの SSD (シングルショット検出器) モデルから TFLite モデルへの変換 12) tf-nightlyをインストールします TFLiteコンバーターは、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) SSDTFLiteグラフをエクスポートします 現在の作業ディレクトリは /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) TF 保存モデルを TFLite モデルに変換 現在の作業ディレクトリは /mydrive/customTF2/data/ です %cd /content/customTF2/customTF2/data/ 入力テンソル名と出力テンソル名を確認 !saved_model_cli show--dir /content/customTF2/customTF2/data/tflite/saved_model--tag_set serve—all TFLite への変換: 方法 (a) または方法 (b) のいずれかを使用します。 方法 (a) コマンドラインツール tflite_convert を使用-(基本モデル変換) # デフォルトの推論タイプは浮動小数点です。 %cd /content/customTF2/customTF2/data/ !tflite_convert --saved_model_dir=tflite/saved_model --output_file=tflite/detect.tflite 方法 (b) Python API を使用-(最適化などによる高度なモデル変換用) %cd /mydrive/customTF2/data/ #'''******************************** # 浮動小数点推論用 #*********************************''' #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) #'''************************************************** # 最適化による浮動小数点推論用 #***************************************************''' 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) #'''********************************** # ダイナミックレンジ量子化用 #************************************* # このモデルは重みが量子化され、少し小さくなりましたが、他の変数データはまだfloat形式です。''' # 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) # '''*********************************************************************** # 整数と浮動小数点のフォールバック量子化(デフォルトの最適化を使用) # ************************************************************************** # これで、すべての重みと変数データが量子化され、元のTensorFlow Liteモデルと比較してモデルが大幅に小さくなりました。 # ただし、float形式のモデル入力および出力テンソルを従来使用しているアプリケーションとの互換性を維持するために、 # TensorFlow Lite コンバーターはモデルの入力テンソルと出力テンソルをフロートのままにします''' # 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) # '''********************************* # 全整数量子化用 # ************************************ # 内部量子化は前の浮動小数点フォールバック量子化方法と同じままです。 # しかし、入力と出力のテンソルは、ここでも整数形式になっていることがわかります。''' # 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) トレーニング後の量子化の詳細は、 こちら をご覧ください。これらについては、 こちら のColabノートブックでも読むことができます。 15) TFLite メタデータを作成します !pip install tflite_support_nightly %cd /content/customTF2/customTF2/data/ %d tflite/ !mkdir tflite_with_metadata %cd .. データ フォルダ内の各行にクラスの名前が書き込まれた labelmap.txt ファイルを作成します。 最後に、次のセルを実行して、メタデータが添付された detect.tflite モデルを作成します。 現在の作業ディレクトリは /content/customTF2/customTF2/data/ %cd /content/customTF2/customTF2/data/ !pip uninstall tensorflow !pip install tensorflow=="2.13.1" # 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) # 入力されたメタデータと関連ファイルを確認します。 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.") # 入力情報を作成します。 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 # 出力情報を作成します。 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()) # サブグラフ情報を作成します。 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() 質問されたら、'Y'で進みます 16) TFLite モデルをダウンロードします お疲れ様でした。これで完了です! まとめ Google Colab Enterpriseは、機械学習のための強力なクラウドベースのプラットフォームであり、TensorFlow Liteモデルを構築する上で理想的な環境です。このプラットフォームを1年以上使用してみて、最も時間がかかるのはデータ準備と初期の試行錯誤段階であることが分かりました。この段階では、データセットの特定の部分を認識する際の課題を特定し、画像が誤って分類される誤検知に対処するために、著しい反復とテストが必要です。 *Android ロボットのヘッダー画像は、Google が作成および共有した画像から複製または変更されたものであり、クリエイティブ コモンズ 3.0 帰属ライセンスに記載されている条件に従って使用されています。
アバター
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . Introduction Hello. I am Uehara ( @penpen_77777 ), and I am part of the KINTO FACTORY Development Group. I joined in July 2024, and I was assigned to the backend development of KINTO FACTORY. This time, using sample code, I will introduce the data conflicts that you should be cautious of when processing S3 events, and how to resolve them. Intended Readers Those who are concerned with AWS S3 event notifications being duplicated or the order of notifications being changed. Those with basic knowledge of Rust, S3, DynamoDB, Lambda, and Terraform. Having this knowledge will make it easier to understand when reading the sample code. S3 Event Overview An S3 event^[AWS document regarding S3 event notifications https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EventNotifications.html] is an event that occurs when an operation such as uploading or deleting an object to S3 is triggered. By detecting S3 events using Lambda functions or SNS, various processes concerning S3 can be automated. Issues with S3 Events Something you should be cautious of when processing S3 events is that the event notifications could be duplicated or the order could be changed. For example, let’s think about the process of creating an object after deleting an object for the same object key. In this case, first you will be notified of the object delete event, then you can expect to be notified of the create event (remove the same object => see the diagram of the expected S3 event reception order when created). However, there are times when the S3 event order isn’t secured, and the create event notification comes before the delete event notification (remove the same object => see the diagram of the order of reception of possible S3 events when created). As a result, the processing result by the event that deletes the object will be the latest, and the issue of data consistency not being guaranteed depending on the processing content occurs. gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :done, cre2, 00:00:03, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :active, cre2, 00:00:04, 1s gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :active, cre2, 00:00:04, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :done, cre2, 00:00:03, 1s As a solution to this problem, there is a way to guarantee the order of events using the sequencer key included in S3 events^[document regarding S3 event structure. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html] One way to determine the sequence of events is to use the sequencer key. There is no guarantee that event notifications will be delivered in the order in which the event occurred. However, notifications and delete objects from events that create objects (PUT) include the sequencer. This can be used to determine the order of events for a particular object key. When comparing the sequencer strings of two event notifications for the same object key, you can see that the event notification with the larger 16 hexadecimal value sequencer is the event that occurred later. When maintaining a separate database or index of Amazon S3 objects using event notifications, I recommend that you compare and save the sequencer value each time you process an event notification. Please note the following: The sequencer cannot be used to determine the order of events for multiple object keys. The length of the sequencer may be different. To compare these values, first input 0 on the right of the shorter value, and then perform a lexicographic comparison. To Summarize: The sequencer is a value included in an object's PUT or DELETE event, and can be used to determine the order of events. Lexicographically compare the sequencers. The larger the value, the later the event that occurred. If the lengths differ, compare after inputting 0 on the right side of the shorter value. It cannot be used to determine the order of S3 events between multiple objects. It is used to determine the order of PUT and DELETE events for the same object. For example, if you want to implement the S3 event sequencer comparison in Rust, you can implement it as follows: Define the fields and constructor of the structure S3Sequencer to express the nature of the S3 sequencer. // 1. Define the structure S3Sequencer use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Sequencer { bucket_name: String, object_key: String, sequencer: String, } // 2. Define the S3Sequencer constructor // Take the bucket name, object key, and sequencer as argument impl S3Sequencer { pub fn new(bucket_name: &str, objcet_key: &str, sequencer: &str) -> Self { Self { bucket_name: bucket_name.to_owned(), object_key: objcet_key.to_owned(), sequencer: sequencer.to_owned(), } } } Next, to determine the order of events by comparing the size of the S3Sequencer, use the PartialOrd ^[Mathematically speaking, it can represent a partially ordered set. There is also a full ordered set that implements the ‘Ord’ trait. It is interesting that traits are intentionally divided to implement comparison methods. I will implement https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html https://ja.wikipedia.org/wiki/%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88#%E5%8D%8A%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88] and PartialEq trait^[PartialEq trait definition https://doc.rust-lang.org/std/cmp/trait.PartialEq.html] By implementing these two, you can compare the size of the sequencers using comparison operators such as == , < , or > as shown below. let seq1 = S3Sequencer::new("bucket1", "object1", "abc123"); let seq2 = S3Sequencer::new("bucket1", "object1", "abc124"); if seq1 < seq2 { println!("seq1 is an older event than seq2"); } else if seq1 == seq2 { println!("seq1 and seq2"); } else { println!("seq1 is a newer event than seq2"); } Implement the partial_cmp method required to implement the PartialOrd trait as shown below. impl PartialOrd for S3Sequencer { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { // Sequencers with different bucket names cannot be compared if self.bucket_name != other.bucket_name { return None; } // Sequencers with different object keys cannot be compared if self.object_key != other.object_key { return None; } // Compare by adding 0 to the end of the shorter one to match with the longer one let max_len = std::cmp::max(self.sequencer.len(), other.sequencer.len()); let self_sequencer = self .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); let other_sequencer = other .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); Some(self_sequencer.cmp(other_sequencer)) } Since there is no meaning in comparing sequencers with different bucket names and object keys, return None using early return if they are different. You can compare the sequencers once you confirm that the bucket names and object keys are the same, but the process will be in the following order. Compare the length of the sequencers, and store the longer one in max_len Add 0 to the end of the shorter sequencer to match max_len Compare the sequencers created in 2 in lexicographic order and return the size The PartialEq trait is implemented as follows: impl PartialEq for S3Sequencer { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) .map_or(false, |o| o == std::cmp::Ordering::Equal) } } Determine if the sequencer comparison results are equal using the partial_cmp method of the PartialOrd trait. With the above implementations, it is now possible to compare sequencers for S3 events. Example of S3 Event Processing Implementation Considering Data Consistency Architecture Diagram Now, I will introduce how to guarantee the order of S3 events using sequencers, along with sample code The sample code converts the image file uploaded to the S3 bucket to grayscale on Lambda and saves it to the S3 bucket for output. Below is an architecture diagram. ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) When an image file is uploaded to the image input bucket, the Lambda function is triggered through an S3 event. The launched Lambda function checks DynamoDB to confirm that it is not processing, then sets the processing flag and processes the image file. Once processing has finished, it clears the processing flag and waits for the next image file to be processed. If the order of the create and delete event notifications is reversed, problems such as accidently deleting images that should exist could occur. For example, let’s assume that the process is implemented as follows. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (3) Event reception, Object deletion :done, cre2, 00:00:03, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (4) Event reception, convert to grayscale :active, cre2, 00:00:04, 1s However, there is a possibility of the notification order for 3 and 4 being reversed in S3 events, so the following flow may occur. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (4) Event reception, Object deletion :active, cre2, 00:00:04, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (3) Event reception, convert to grayscale :done, cre2, 00:00:03, 1s In this case, the problem occurs that even though image file A exists in the input bucket, the grayscaled image file A' does not exist in the output bucket. In order to prevent problems such as this, use the S3 event sequencer to implement exclusive processing. Additionally, it is also necessary to implement exclusive processing to manage image processing status by DynamoDB. Use DynamoDB conditional write to set an in-progress flag in order to prevent multiple Lambda functions from processing the same image file at the same time. The sample code is published on GITHUB. Take a look using the link below. (You will need to build an AWS infrastructure to run it, but it has been made easy to try using the terraform code.) https://github.com/kinto-technologies/techblog-s3-sequencer-example Implementing Sample Code with Rust This time, I will implement a Lambda function using Rust. It is convenient to use cargo-lambda to implement Lambda functions. https://www.cargo-lambda.info/ I will omit the details of how to use cargo-lambda. Creating an Entry Point When you run the initialization command on cargo-lambda, main.rs is automatically generated as shown below. cargo lambda init use lambda_runtime::{ run, service_fn, tracing::{self}, Error, }; mod handler; mod image_task; mod lock; mod s3_sequencer; #[tokio::main] async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); run(service_fn(handler::function_handler)).await } Since handler:function_handler is specified as the entry point for the Lambda function, I will write the implementation in handler.rs The function_handler is implemented as follows. use crate::image_task::ImageTask; use aws_lambda_events::event::s3::S3Event; use lambda_runtime::{tracing::info, Error, LambdaEvent}; pub async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> { // Convert S3 events to ImageTask let tasks: Vec<_> = event .payload .records .into_iter() .map(ImageTask::try_from) .collect::<Result<Vec<_>, _>>()?; // Use futures::future::join_all to create the task to be executed let execute_tasks = tasks.iter().map(|task| task.execute()); // Use join_all to execute or wait all tasks // Store the execution results in ret let ret = futures::future::join_all(execute_tasks).await; // Output the execution results to log for (t, r) in tasks.iter().zip(&ret) { info!("object_key: {}, Result: {:?}", t.object_key, r); } // Return error is there is an error if ret.iter().any(|r| r.is_err()) { return Err("Some tasks failed".into()); } // Successful termination Ok(()) } Convert the S3 event vector to an ImageTask structure vector. Since the conversion method implements the TryFrom trait, you just have to call the try_form method. Create an image processing task based on the ImageTask structure vector. Use the tokio crate join_all function to execute all tasks in parallel. Output the results returned by join_all in 3 to the log. If there is an error, return the error and terminate the Lambda function abnormally. If there are no errors it will successfully terminate. Image Processing Implementation The ImageTask structure used in 1 is defined as below and holds the information necessary to execute Lambda. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum TaskType { Grayscale, Delete, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageTask { pub bucket_name: String, #[serde(rename = "id")] pub object_key: String, pub sequencer: S3Sequencer, pub task_type: TaskType, pub processing: bool, } Field name Description bucket_name S3 bucket name object_key Object key sequencer S3 event sequencer task_type Enumeration indicating the type of task (Grayscale, Delete) processing Processing flag Implement specific image processing within the execute method of the ImageTask structure. impl ImageTask { pub async fn execute(&self) -> Result<(), Error> { // 1. Acquire lock let lock = S3Lock::new(&self).await?; // 2. Process according to the task type match self.task_type { TaskType::Grayscale => { // Convert image to grayscale and save in output bucket let body = lock.read_input_object().await?; let format = image::ImageFormat::from_path(&self.object_key)?; let img = image::load_from_memory_with_format(&body, format)?; let img = img.grayscale(); let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), format)?; lock.write_output_object(buf).await?; } // Delete image from output bucket TaskType::Delete => lock.delete_output_object().await?, } // 3. Release lock lock.free().await?; Ok(()) } } Apply exclusive processing to prevent S3 object data inconsistencies Process according to the task type If a file was added to a previous bucket, convert the image to grayscale and save it in the output bucket If a file was deleted, delete the file from the output bucket Release lock when processing has finished Implement Lock Processing Define the S3Lock structure to implement lock processing. pub struct S3Lock { dynamodb_client: aws_sdk_dynamodb::Client, table_name: String, s3_client: aws_sdk_s3::Client, input_bucket_name: String, input_object_key: String, output_bucket_name: String, output_object_key: String, } The specific lock acquisition process is implemented in the constructor. The code is a bit long, but roughly speaking, it is as follows. If writing to DynamoDB is successful, the lock is considered to have been acquired. If writing fails, try again every 2 seconds. If the lock cannot be acquired within 30 seconds, a timeout occurs. The lock processing sequence diagram is shown below. sequenceDiagram participant ImageTask participant S3Lock participant DynamoDB ImageTask->>S3Lock: Acquire lock loop alt timeout (30 seconds) S3Lock->>ImageTask: Return error (timeout) end S3Lock->>DynamoDB: Acquire processing status DynamoDB->>S3Lock: Return results alt record exists S3Lock->>S3Lock: Sequencer comparison alt is old S3Lock->>ImageTask: Return error (skip) else is new S3Lock->>S3Lock: Retry acquiring lock end else S3Lock->>DynamoDB: Acquire lock with conditional write DynamoDB->>S3Lock: Write return result alt write successful S3Lock->>ImageTask: Lock acquisition successful else failure S3Lock->>S3Lock: retry end end end The code in the constructor is as follows: impl S3Lock { pub async fn new(task: &ImageTask) -> Result<Self, Error> { let table_name = std::env::var("DYNAMODB_TABLE_NAME").unwrap(); let output_bucket_name = std::env::var("OUTPUT_BUCKET_NAME").unwrap(); let require_lock_timeout = Duration::from_secs( std::env::var("REQUIRE_LOCK_TIMEOUT") .unwrap_or_else(|_| "30".to_string()) .parse::<u64>() .unwrap(), ); let interval_retry_time = Duration::from_secs( std::env::var("RETRY_INTERVAL") .unwrap_or_else(|_| "2".to_string()) .parse::<u64>() .unwrap(), ); let config = aws_config::load_defaults(aws_config::BehaviorVersion::v2024_03_28()).await; let s3_client = aws_sdk_s3::Client::new(&config); let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); // Acquire lock Measure execution time let start = Instant::now(); loop { // It will time out if the lock cannot be acquired for more than 30 seconds. if start.elapsed() > require_lock_timeout { return Err("Failed to acquire lock, timeout".into()); } Acquire sequencer from DynamoDB with strong read consistency let item = dynamodb_client .get_item() .table_name(table_name.clone()) .key("id", AttributeValue::S(task.object_key.clone())) .consistent_read(true) .send() .await?; Compare sequencer if acquired item exists if let Some(item) = item.item { let item: ImageTask = from_item(item)?; if task.sequencer <= item.sequencer { If the sequencer itself is old, there is no need to process it, so skip it return Err("Old sequencer".into()); } If the sequencer itself is new, wait until other processing has finished if item.processing { warn!( "Waiting for other process to finish task, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(interval_retry_time); continue; } } // Acquire lock with conditional write to DynamoDB // If the record exists at that time, only write if the processing flag is false let resp = dynamodb_client .put_item() .table_name(table_name.clone()) .set_item(Some(to_item(&task).unwrap())) .condition_expression("attribute_not_exists(id) OR processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await; Once acquired, exit the loop and continue processing If it could not be acquired, continue trying until the lock can be acquired. match resp { Ok(_) => break, Err(SdkError::ServiceError(e)) => match e.err() { PutItemError::ConditionalCheckFailedException(_) => { warn!( "Failed to acquire lock, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(Duration::from_secs(2)); continue; } _ => return Err(format!("{:?}", e).into()), }, Err(e) => return Err(e.into()), } } return Ok(Self { dynamodb_client, output_bucket_name, s3_client, table_name, input_bucket_name: task.bucket_name.clone(), input_object_key: task.object_key.clone(), output_object_key: task.object_key.clone(), }); } } The process to release the lock is implemented as shown below, and the lock is released by updating the processing flag to false. impl S3Lock { pub async fn free(self) -> Result<(), Error> { // Release DynamoDB lock // Only update the processing flag self.dynamodb_client .update_item() .table_name(self.table_name) .key("id", AttributeValue::S(self.input_object_key)) .update_expression("SET processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await?; Ok(()) } } Since I want to force a lock when touching an S3 object, I have created a method for manipulating S3 objects in S3Lock^[I feel that reusability will be lower if I create it in S3Lock, but for simplicity I will define it in the same structure. Though there seems to be a better way...] impl S3Lock { pub async fn read_input_object(&self) -> Result<Vec<u8>, Error> { // Acquire S3 object let object = self .s3_client .get_object() .bucket(&self.input_bucket_name) .key(&self.input_object_key) .send() .await?; let body = object.body.collect().await?.to_vec(); Ok(body) } pub async fn write_output_object(&self, buf: Vec<u8>) -> Result<(), Error> { // Save S3 object let byte_stream = ByteStream::from(buf); self.s3_client .put_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .body(byte_stream) .send() .await?; Ok(()) } pub async fn delete_output_object(&self) -> Result<(), Error> { // Delete S3 object self.s3_client .delete_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .send() .await?; Ok(()) } } Trying it Out Let’s try using the sample code. I need to prepare an image that I want to convert to grayscale, so this time I will use Hyogo Prefectural Park Awaji Hanasajiki^[a beautiful flower garden on Awaji Island https://awajihanasajiki.jp/about/] . I took the photo. I will use the image from https://awajihanasajiki.jp/about/ . Infrastructure First, use terraform apply to build the AWS infrastructure. Then, clone the GitHub repository and implement the following command. cd terraform # Modify variables.tf and provider.tf appropriately. terraform init terraform apply ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) Upload the Image File to the S3 Bucket If infrastructure is completed, upload the image file to the input S3 bucket. The Lambda function process begins when the upload finishes, and the item is added to the DynamoDB table. The image file is saved in the output S3 bucket when the process finishes, and the processing flag of the DynamoDB item is set to false. You can see the image added to the output S3 bucket and converted to grayscale. Delete the Image File from the S3 Bucket When an object is deleted from the input bucket, it is also deleted from the output bucket. Confirm that Exclusive Processing is Working When the processing flag of an item added to DynamoDB is set to true, processing will wait even if an S3 event that should be processed arrives. To confirm this behavior, let's intentionally set the DynamoDB processing flag to true and upload a file with the same name. If you look at CloudWatch Logs, you can see that it waits for other processes to complete without processing newly generated S3 events. The log message will indicate that Lambda is waiting. Setting the DynamoDB processing flag back to false will restart processing. When the DynamoDB item's processing flag is set back to false, the log will indicate that processing is completed. Thanks to exclusive processing, the order of processing is guaranteed even if delete and upload events occur at approximately the same time. Conclusion This time, I introduced sample code for image processing that uses a sequencer to guarantee the order of S3 events. To guarantee the order of S3 events, you need to utilize a sequencer to compare the order of events. I implemented the sample code in Rust as my own hobby, but it should be possible to implement something similar in other languages. Please feel free to use this as a reference.
アバター
AIが急速に進化する2025年、エンジニアにとって「AIをどう活用するか」が重要なスキルとなってきました。 しかし、AIを効果的に活用するには、適切なプロンプト(指示の出し方)を理解する必要があり、そのためには 慣れや知見 が不可欠です。 そこで、AIとコーディングを行う最初のステップとして、今回のテーマである TDD(test-driven development: テスト駆動開発) と AI を活用した開発を紹介します。 TDD × AIのメリット ✅ 実装コストが激減!エンジニアは"テストを書くだけ"でOK テストを書くだけで、複雑な指示もプロンプトも不要。あとはAIが自動でコードを生成。 ✅ 開発速度が爆速化!細かな往復が劇的に減少 TDDのステップごとにAIが即座にコードを生成するので、開発効率が驚異的にアップ。 ✅ 圧倒的なコード品質!テストでしっかりAIの暴走をコントロール 明確なテストでAIの生成コードをしっかり制御。結果的にバグの少ないコードになる。 TDDとは? 大前提となるTDD(Test-Driven Development: テスト駆動開発)について簡単に説明します。 https://www.amazon.co.jp/dp/4274217884 「TDD(Test-Driven Development: テスト駆動開発)」はKent Beck氏が👆の書籍で20年以上前に提唱した手法です。 上の図の「まずテストを書いて、次にテストを通過する実装をする、最後にリファクタリング」というシンプルなサイクルを繰り返すことで、品質の高いコードを生み出すことができます。 また、テストが実装の起点になるので、 テスタブルな構造を担保しつつ、実装を進めることが出来ます。 TDDを実践する :::message VSCodeの GitHub CopilotのAgentモードを使って実施しています。 ::: https://code.visualstudio.com/docs/copilot/copilot-edits#_use-agent-mode-preview 今回の方法では、エンジニアがテストを書き、生成AIが実装・リファクタリングを担うことで、コード品質を維持しつつ効率的に開発を進めます。 今回は パスワードのバリデーション を例に、TDD × AIの流れを説明していきます。 まずは、 文字数のバリデーション から始めていきましょう。 :::message alert 最初からすべての仕様を詰め込まず、細かいステップで進めることが重要です! ::: ステップ1: 文字数バリデーションテストの作成 対象になるPasswordバリデーションのクラスとテストファイルを用意しましょう。 この段階ではどちらも空のファイルで問題ありません。 ファイルが作成できたらそのままPasswordバリデーションクラスを書きたくなるところですが、今回はTDDなのでテストを書いていきます。 @Test("パスワードの文字数が8文字以上16文字以下でバリデーションが成功すること") func testPasswordLengthValidation() async throws { let sut = PasswordValidator() let validPasswordShort = "Valid123" let validPasswordLong = "ValidPassword123" let invalidPasswordShort = "Short01" let invalidPasswordLong = "ThisPasswordIsWay" #expect(sut.validate(validPasswordShort) == true) #expect(sut.validate(validPasswordLong) == true) #expect(sut.validate(invalidPasswordShort) == false) #expect(sut.validate(invalidPasswordLong) == false) } この段階では PasswordValidator クラスや validate メソッドが未実装のため、当然エラーが発生します。 次に、AIにプロンプトを入力して実装を依頼します。 ![input prompt](/assets/blog/authors/HiroyaHinomori/2025_03_tdd_x_ai_02.png =500x) すると、数十秒後にAIが実装を生成してくれます。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count return passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength } } 次に、数字・文字・記号のミックス判定を追加してみます。 ステップ2: 数字、文字、記号の含有バリデーションの追加 @Test("パスワードの文字列の中に数字、文字、記号が含まれている場合にバリデーションが成功すること") func testPasswordCharacterValidation() async throws { let sut = PasswordValidator() let validPassword = "ValidPassword12!" let invalidPasswordNoNumber = "InvalidPassword!" let invalidPasswordNoCharacter = "1234567890!" let invalidPasswordNoSymbol = "InvalidPassword1" #expect(sut.validate(validPassword) == true) #expect(sut.validate(invalidPasswordNoNumber) == false) #expect(sut.validate(invalidPasswordNoCharacter) == false) #expect(sut.validate(invalidPasswordNoSymbol) == false) } この段階でエラーが発生しますが、気にせずテストケースをAIに渡して実装を生成します。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count guard passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength else { return false } let hasNumber = password.rangeOfCharacter(from: .decimalDigits) != nil let hasLetter = password.rangeOfCharacter(from: .letters) != nil let hasSymbol = password.rangeOfCharacter(from: .symbols) != nil || password.rangeOfCharacter(from: .punctuationCharacters) != nil return hasNumber && hasLetter && hasSymbol } } ここまで 10分もかからず 実装できました。 通常のコーディングではプロンプトに色々な条件や仕様を伝える必要がありますが、この方法では テスト条件を満たした実装をして というお願いをするだけです。 実装内容に関してはTestに全て書かれているので、複雑なプロンプトの指定はほぼ不要になります。 AIとのコミュニケーションをさらに効率化するには 実装のルールや制約を事前に「copilot-instructions.md」に書いておけば、毎回AIに細かい指示を伝える必要もありません。 日本語で返答してください。 ### コーディングルール - テストはswift-testingを使用してください。 - 実装には基本的にマジックナンバーは使わないこと - DRYの原則に則って実装してください - KISSの原則に則って実装してください - YAGNIの原則に則って実装してください AI時代に活躍するエンジニアになるために AIは万能ではありません。しかし、だからといって諦めるのはとてももったいないです! 「AIが得意なこと」と「人間が担うべきこと」を冷静に見極めること が重要です。 「TDD × AI」で、AIコーディングの癖を理解し、次世代の開発スピードと品質を手に入れましょう🚀
アバター
Introduction Hello, I'm Shimakawa, a member of the Cloud Infrastructure Group. The Cloud Infrastructure Group is responsible for everything from designing to operating the company's entire infrastructure, including AWS. As generative AI adoption grows across various products in our company, the Cloud Infrastructure Group has been actively supporting these initiatives. In this article, I’ll share my experience building an Amazon Bedrock Knowledge Base using Terraform. I’ll also touch on the RAG Evaluation announced at re:Invent 2024. Configuration Here is the architecture we will be building. We will use OpenSearch Serverless as the Vector store for Amazon Bedrock Knowledge Base, and specify S3 as the data source. Building with Terraform The directory structure is as follows. I will explain each file’s content in detail. The Terraform version used in this setup is 1.7.5 . $ tree . ├── aoss.tf # OpenSearch Serverless ├── bedrock.tf # Bedrock resources ├── iam.tf # iam ├── s3.tf # S3 for bedrock ├── locals.tf # variable definitions ├── provider.tf # provider definitions └── terraform.tf # Backend settings, etc. This section defines the variables. locals { env = { environment = "dev" region_name = "us-west-2" sid = "test" } aoss = { vector_index = "vector_index" vector_field = "vector_field" text_field = "text_field" metadata_field = "metadata_field" vector_dimension = 1024 } } We specify the AWS provider, the OpenSearch provider version, and the S3 backend to store the tfstate. The S3 bucket used here was created manually and is not included in this Terraform code. terraform { required_providers { # https://registry.terraform.io/providers/hashicorp/aws/ aws = { source = "hashicorp/aws" version = "~> 5.0" } opensearch = { source = "opensearch-project/opensearch" version = "2.2.0" } } backend "s3" { bucket = "***-common-bucket" region = "ap-northeast-1" key = "hogehoge-terraform.tfstate" encrypt = true } } This defines the AWS and OpenSearch providers. The OpenSearch provider is used to create and manage indexes. provider "aws" { region = local.env.region_name default_tags { tags = { SID = local.env.sid Environment = local.env.environment } } } provider "opensearch" { url = aws_opensearchserverless_collection.collection.collection_endpoint aws_region = local.env.region_name healthcheck = false } Create OpenSearch Serverless resources and an index. I referred to Deploy Amazon OpenSearch Serverless with Terraform . For this setup, the security policy is set to public, but ideally, access should be restricted using a VPC endpoint. data "aws_caller_identity" "current" {} # Creates a collection resource "aws_opensearchserverless_collection" "collection" { name = "${local.env.sid}-collection" type = "VECTORSEARCH" standby_replicas = "DISABLED" depends_on = [aws_opensearchserverless_security_policy.encryption_policy] } # Creates an encryption security policy resource "aws_opensearchserverless_security_policy" "encryption_policy" { name = "${local.env.sid}-encryption-policy" type = "encryption" description = "encryption policy for ${local.env.sid}-collection" policy = jsonencode({ Rules = [ { Resource = [ "collection/${local.env.sid}-collection" ], ResourceType = "collection" } ], AWSOwnedKey = true }) } # Creates a network security policy resource "aws_opensearchserverless_security_policy" "network_policy" { name = "${local.env.sid}-network-policy" type = "network" description = "public access for dashboard, VPC access for collection endpoint" policy = jsonencode([ ###References for using VPC endpoints # { # Description = "VPC access for collection endpoint", # Rules = [ # { # ResourceType = "collection", # Resource = [ # "collection/${local.env.sid}-collection}" # ] # } # ], # AllowFromPublic = false, # SourceVPCEs = [ # aws_opensearchserverless_vpc_endpoint.vpc_endpoint.id # ] # }, { Description = "Public access for dashboards and collection", Rules = [ { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ] }, { ResourceType = "dashboard" Resource = [ "collection/${local.env.sid}-collection" ] } ], AllowFromPublic = true } ]) } # Creates a data access policy resource "aws_opensearchserverless_access_policy" "data_access_policy" { name = "${local.env.sid}-data-access-policy" type = "data" description = "allow index and collection access" policy = jsonencode([ { Rules = [ { ResourceType = "index", Resource = [ "index/${local.env.sid}-collection/*" ], Permission = [ "aoss:*" ] }, { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ], Permission = [ "aoss:*" ] } ], Principal = [ data.aws_caller_identity.current.arn, iam_role.bedrock.arn, ] } ]) } resource "opensearch_index" "vector_index" { name = local.aoss.vector_index mappings = jsonencode({ properties = { "${local.aoss.metadata_field}" = { type = "text" index = false } "${local.aoss.text_field}" = { type = "text" index = true } "${local.aoss.vector_field}" = { type = "knn_vector" dimension = "${local.aoss.vector_dimension}" method = { engine = "faiss" name = "hnsw" } } } }) depends_on = [aws_opensearchserverless_collection.collection] } Create the Knowledge Base and data source. data "aws_bedrock_foundation_model" "embedding" { model_id = "amazon.titan-embed-text-v2:0" } resource "aws_bedrockagent_knowledge_base" "this" { name = "test-kb" role_arn = iam_role.bedrock.arn knowledge_base_configuration { type = "VECTOR" vector_knowledge_base_configuration { embedding_model_arn = data.aws_bedrock_foundation_model.embedding.model_arn } } storage_configuration { type = "OPENSEARCH_SERVERLESS" opensearch_serverless_configuration { collection_arn = aws_opensearchserverless_collection.collection.arn vector_index_name = local.aoss.vector_index field_mapping { vector_field = local.aoss.vector_field text_field = local.aoss.text_field metadata_field = local.aoss.metadata_field } } } depends_on = [iam_role.bedrock] } resource "aws_bedrockagent_data_source" "this" { knowledge_base_id = aws_bedrockagent_knowledge_base.this.id name = "test-s3-001" data_source_configuration { type = "S3" s3_configuration { bucket_arn = "arn:aws:s3:::****-dev-test-***" ### Masked bucket name } } depends_on = [aws_bedrockagent_knowledge_base.this] } Set the service role that bedrock will use. resource "aws_iam_role" "bedrock" { name = "bedrock-role" managed_policy_arns = [aws_iam_policy.bedrock.arn] assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "bedrock.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "bedrock" { name = "bedrock-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["bedrock:InvokeModel"] Effect = "Allow" Resource = "*" }, { Action = [ "s3:GetObject", "s3:ListBucket", ] Effect = "Allow" Resource = "***-dev-test-***" ### ARN of the S3 bucket that was }, { Action = [ "aoss:APIAccessAll", ] Effect = "Allow" Resource = "arn:aws:aoss:us-west-2:12345678910:collection/*" }, ] }) } Create an S3 bucket for use with Bedrock. Also, configure CORS. Below is a reference image of the error. resource "aws_s3_bucket" "bedrock" { bucket = "***-dev-test-***" ### Masked bucket name } resource "aws_s3_bucket_cors_configuration" "this" { bucket = aws_s3_bucket.bedrock.id cors_rule { allowed_headers = ["*"] allowed_methods = [ "GET", "PUT", "POST", "DELETE" ] allowed_origins = ["*"] } } Execution Use terraform apply to create all resources at once. Verifying the Created Resources Check that the Knowledge Base has been created in Bedrock and that the data source is available. Next, verify that an OpenSearch collection has been created and that an index has been set. Testing the Knowledge Base Upload some sample text to S3 to use as a data source. Dogs like meat. Cats like fish aws s3 cp ./test001.txt s3://[S3 Bucket Name]/test001.txt Next, synchronize the data source. Now, let’s test it by asking a question. (Using Claude 3.5 Sonnet for the Prompt) The system correctly retrieves answers from the provided text while refraining from answering questions about information that is not present. This was a brief overview of setting up a Knowledge Base and OpenSearch Serverless using Terraform. Trying out RAG Evaluation Next, we will test the RAG evaluation announced at re:Invent 2024 on the Knowledge Base we created. @ card Preparation First, prepare a dataset file in JSONL format for evaluation. This file contains prompts with their expected answers. {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Cats' favorite food is fish."}]}],"prompt":{"content":[{"text":"What do cats like? {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Dogs’ favorite food is meat."}]}],"prompt":{"content":[{"text":"What do dogs like? Since the evaluation references data from S3, upload the dataset file to an S3 bucket. aws s3 cp ./dataset001.txt s3://[S3 Bucket Name]/datasets/dataset001.txt Creating a Job Next, we will create a job. While this can be done through the management console, this time, I executed it using the CLI. @ card aws bedrock create-evaluation-job \ --job-name "rag-evaluation-complete-stereotype-docs-app" \ --job-description "Evaluates Completeness and Stereotyping of RAG for docs application" \ --role-arn "arn:aws::iam:<region>:<account-id>:role/AmazonBedrock-KnowledgeBases" \ --evaluation-context "RAG" \ --evaluationConfig file://knowledge-base-evaluation-config.json \ --inference-config file://knowledge-base-evaluation-inference-config.json \ --output-data-config '{"s3Uri":"s3://docs/kbevalresults/"}' You need to specify the JSONL file and the destination for saving the results in knowledge-base-evaluation-config.json . Checking the Job After waiting about 15 to 20 minutes, the job was completed, so I checked the results. I started by reviewing the summary. The responses were almost exactly as expected, so there wasn’t much surprise. However, the Correctness and Completeness scores were both 1, indicating that the system performed as expected. The only exception was the Helpfulness score, which was 0.83. When I checked the evaluation comments, it stated: “The answer is neither particularly interesting nor unexpected, but in this context, it doesn't need to be.” I think the fact that this context does not have to be the case is what is causing the score to drop. Final Thoughts Our company has been increasingly integrating generative AI, including Amazon Bedrock, and its real-world applications are expanding. Moving forward, I plan to explore more features and ensure we are well-prepared to meet project requirements. I hope this blog post serves as a useful reference. Thank you for reading!
アバター
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
アバター
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
アバター
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Nice to meet you, I’m Tsuyoshi Yamada, an Android engineer at KINTO Technologies. In this article, we’ll share some handy techniques to help you take the first step in migrating an app built with Android View to a Jetpack Compose -based UI or gradually integrating Jetpack Compose into an existing Android View-based UI. 1. Introduction Development of Jetpack Compose , Android’s Declarative UI , was announced a few years ago, and version 1.0 was officially released in July 2021. Many Android developers quickly embraced the concept of Jetpack Compose, harnessing the flexibility and extensibility of Kotlin , which had already become the second officially supported language for Android development after Java. It seems that the adoption of Jetpack Compose in Android app development has been gradually increasing. With the introduction of declarative UI, developers can write UI more intuitively with less code compared to traditional view-based UI, leading to improved development efficiency and productivity. With the recent release of Compose Multiplatform , the scope of Jetpack Compose skills has expanded beyond Android. Moving forward, we might see more libraries being developed exclusively for Jetpack Compose. To bring these benefits to our existing apps, we have started migrating some of our development projects from Android View-based UIs to Jetpack Compose-based UIs. However, traditional view-based UIs rely heavily on procedural elements, making it challenging to adopt Jetpack Compose’s declarative style seamlessly. This article introduces some practical techniques to compensate for elements that may have been lost or become less visible due to Jetpack Compose’s simplified approach. In particular, we’ll cover tracking Composable locations, checking debug information, and ensuring smooth interoperability between Views and Composables—all with the goal of making the transition from a View-based UI to a Jetpack Compose-based UI as seamless as possible. 2. Aligning and Debugging Composables Replacing the existing View expressions with new Composables came with a big challenge—not just figuring out how to represent the same content, but also whether it could be positioned correctly. Even for someone familiar with Android Views, that was a major concern. To address this, we’ll first go over how to align a Composable’s position with a View’s and how to retrieve and check debugging information to assist in this process. 2.1. Checking the position of Composable and View The View , available since Android API level 1, is built on Java’s object-oriented principles. It not only represents rectangular screen elements but also effectively handles containment relationships between Views, interactions, and extending functionality through subclasses. For example, you can log the positions of a single View or multiple Views inside a ViewGroup using the following code: private fun Resources.findResourceName(@IdRes resId: Int): String = try { getResourceName(resId) } catch (e: Resources.NotFoundException) { "?" } fun View.logCoordinators(logger: (String) -> Unit, res: Resources, outLocation: IntArray, rect: Rect, prefix: String, density: Float) { getLocationInWindow(outLocation) rect.set(outLocation[0], outLocation[1], outLocation[0] + width, outLocation[1] + height) var log = "$prefix${this::class.simpleName}(${res.findResourceName(id)})${rect.toShortString()}(${rect.width().toFloat() / density}dp, ${rect.height().toFloat() / density}dp)" if (this is TextView) { log += "{${if (text.length <= 10) text else text.substring(0, 7) + "..."}}" } logger(log) if (this is ViewGroup) { val nextPrefix = "$prefix " repeat(childCount) { getChildAt(it).logCoordinators(logger, res, outLocation, rect, nextPrefix, density) } } } fun View.logCoordinators(logger: (String) -> Unit = { Log.d("ViewLogUtil", it) }) = logCoordinators(logger, resources, IntArray(2), Rect(), "", resources.displayMetrics.density) Here, View$getLocationInWindow(IntArray) is a function that retrieves the top-left coordinates of the Activity’s window where the View is located. If you’re familiar with Android Views, checking how this code works should be pretty straightforward. One thing to keep in mind is that calling these functions directly from Activity$onResume() , for example, won’t work as expected because the View layout isn’t fully set up yet, so you won’t be able to get meaningful information. In most cases, you’ll need to call them from a callback like View$addOnLayoutChangeListener(OnLayoutChangeListener) . A lot of developers might not be sure how to achieve the same thing in Jetpack Compose, and since it’s not easy to check if Composable behaves the same way, the migration process can feel like a hassle. In Jetpack Compose, you can use the Modifier extension function onGloballyPositioned((LayoutCoordinator) -> Unit) to get a Composable's position like this: @Composable fun BoundsInWindowExample() { Text( modifier = Modifier .padding(16.dp) .background(Color.White) .onGloballyPositioned { Log.d("ComposeLog", "Target boundsInWindow: ${it.boundsInWindow()}") }, text = "Hello, World!", style = TextStyle( color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp ) ) } By passing a callback to onGloballyPositioned(...) , you can get the position coordinates every time the Composable’s position updates. In this case, LayoutCoordinator.boundsInWindow() is an extension function that retrieves the top, bottom, left, and right bounds of the Composable’s rectangle in the Activity coordinate system. Getting the positions of all Composables inside a single Composable at once seems tricky for now, but grabbing the position of an individual Composable is easy. Plus, in many cases, you can get position data without worrying about complex lifecycles like Activities. onGloballyPositioned(...) There’s also onPositioned(...) , a callback similar to onGloballyPositioned(...) , which gets called after the relative position within the parent Composable is determined. Besides boundsInWindow() , there are also boundsInRoot() and boundsInParent() , which you can use depending on the situation, but we’ll spare you the details for now. 2.2. Creating a Composable to Check Screen Display Now that we’ve figured out how to get the position in Composable using boundsInWindow() , which is compatible with View$getLocationInWindow(IntArray) , we can log the position changes during testing to check its behavior. By doing this while developing the Composable, we’ll gradually get used to Compose and be able to recreate something similar to View. This method is simple and effective, but since LogCat floods with text, it can be hard to read—especially when dealing with a lot of information. To make things easier, let’s try creating a separate Composable just for checking the screen display. If you create a debug display area in a part of your app just for testing purposes and keep it constantly updated, you won’t have to scramble to find key logs among the endless stream of LogCats. ...Of course, we’ve known this since the Android View days, but actually implementing it always felt like a hassle... I can almost hear the sighs. With Jetpack Compose, a declarative UI , creating such a debug area is much easier with minimal effort: class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES setContentView(R.layout.activity_main) findViewById<WebView>(R.id.webView).let { webView -> webView.loadUrl("https://blog.kinto-technologies.com/") } val targetRect = mutableStateOf(Rect.Zero) // androidx.compose.ui.geometry.Rect findViewById<ComposeView>(R.id.composeTargetContainer).let { containerComposeView -> containerComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) containerComposeView.setContent { KtcAdventCalendar2024Theme { ScrollComposable(targetRect) } } } val visibleRect = mutableStateOf(Rect.Zero) val outLocation = IntArray(2) findViewById<View>(R.id.layoutMain).addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ -> v.getLocationInWindow(outLocation) visibleRect.value = Rect(outLocation[0].toFloat(), outLocation[1].toFloat(), outLocation[0].toFloat() + (right - left), outLocation[1].toFloat() + (bottom - top)) } findViewById<ComposeView>(R.id.composeTargetWatcher).let { watcherComposeView -> watcherComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) watcherComposeView.setContent { KtcAdventCalendar2024Theme { TargetWatcher(visibleRect.value, targetRect.value) } } } } } The app in the sample code is in the middle of being converted to Jetpack Compose. Right now, it’s still using a View with Activity$setContentView(Int) , and we’re introducing Composable within that View by using ComposeView . Here, mutableStateOf(...) is used to share the rectangular position information between View and Composable, allowing us to observe how it behaves on the screen. The screen layout looks like this: We’re working on making the HorizontalScrollView part composable. To help with that, the bottom part of the screen will be used to display debug information: ![Screen composition](/assets/blog/authors/tsuyoshi_yamada/advent-calendar_sample_screen-area.png =252x) The layout XML file for MainActivity as follows: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_marginVertical="48dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/layoutMain" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <WebView android:id="@+id/webView" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginHorizontal="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scrollView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="NestedWeights" /> <HorizontalScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/webView" app:layout_constraintTop_toTopOf="parent"> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetContainer" android:layout_width="wrap_content" android:layout_height="match_parent" /> </HorizontalScrollView> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetWatcher" android:layout_width="match_parent" android:layout_height="300dp" android:paddingTop="16dp" /> </LinearLayout> MainActivity.kt and activity_main.xml are in the middle of being converted to Jetpack Compose, so they currently contain a mix of Views and Composables. As of this writing, WebView and some other elements don’t yet have Composable equivalents, so for now, a hybrid approach is necessary. [^1] In activity_main.xml, ComposeView inside HorizontalScrollView is intended to replace an existing View with a Composable, while the other ComposeView is placed to track the position of the Composable above. The Composable that replaces the View is structured as follows and implements onGloballyPositioned(...) to check the position of the part labeled "Target". @Composable fun ScrollComposable(targetRect: MutableState<Rect>) { val textStyle = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp, fontWeight = FontWeight.W600 ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Red), contentAlignment = Alignment.Center ) { Text("1", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Magenta), contentAlignment = Alignment.Center ) { Text("2", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(MaterialTheme.colorScheme.primary) .onGloballyPositioned { targetRect.value = it.boundsInWindow() }, contentAlignment = Alignment.Center ) { Text("Target", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Cyan), contentAlignment = Alignment.Center ) { Text("4", style = textStyle) } } } Here's a Composable that keeps an eye on this: Here, the Composable itself uses onGloballyPositioned(...) to get its own size. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { if (visibleRect.width <= 0f || visibleRect.height <= 0f) return val rootAspectRatio = visibleRect.width / visibleRect.height val density = LocalDensity.current // For calculating toDp() val targetColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25F) var size by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> size = coordinates.size } ) { if (size.width <= 0F || size.height <= 0F) return@Box val watchAspectRatio = size.width.toFloat() / size.height val (paddingH: Float, paddingV: Float) = if (rootAspectRatio < watchAspectRatio) { (size.width - size.height * rootAspectRatio) / 2 to 0F } else { 0F to (size.height - size.width / rootAspectRatio) / 2 } with(density) { Box( modifier = Modifier .padding(horizontal = paddingH.toDp(), vertical = paddingV.toDp()) .fillMaxSize() .background(Color.Gray) ) } if (targetRect.width <= 0f || targetRect.height <= 0f) return@Box with(density) { Box( modifier = Modifier .padding( // Caution: exception is thrown if padding is negative start = max( 0F, marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.left ) ).toDp(), end = max( 0F, size.width - marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.right ) ).toDp(), top = max( 0F, marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.top ) ).toDp(), bottom = max( 0F, size.height - marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.bottom ) ).toDp() ) .fillMaxSize() .background(targetColor) ) } } } private fun marginOf( sizePx: Int, halfPx: Float, visibleFrom: Float, visibleTo: Float, px: Float ): Float { val alpha = (px - visibleFrom) / (visibleTo - visibleFrom) return (1 - alpha) * halfPx + alpha * (sizePx - halfPx) } This Composable function illustrates the relationship between the Activity screen and the Composable labeled as "Target". Setting the padding is a bit tricky, but it’s not rocket science. Composable is technically a function, but it maintains UI state by continuously holding values with MutableState and remember . At the same time, automatically recompose when persistent information changes, reducing the need for tedious event handling and allowing for a more declarative coding style. When you scroll the HorizontalScrollView in the upper right corner of the screen left and right, the Composable at the bottom follows along and updates its position. By retrieving the position of a View or Composable inside a ScrollView using View$getLocationInWindow(IntArray) or LayoutCoordinator.boundsInWindow() , you can also get off-screen coordinates. This allows you to check whether elements can move properly in and out of the visible screen.   (However, this does not necessarily apply when scrolling using Modifier.horizontalScroll(...) , Modifier.verticalScroll(...) , or similar methods in a Composable.) In a conventional View, displaying this kind of debug information required modifying both the layout XML file and the Java/Kotlin code. Since these changes wouldn’t be reflected in the released app, it felt like a bit of a hassle. Data binding was also introduced, allowing layout XML files to track variable updates, but it wasn’t the most intuitive approach. Jetpack Compose lets you to design screens as if you were writing information directly into the design, making it easy to see your implementation results almost instantly. Displaying everything graphically isn’t always the best approach, but having more options for expression can make it easier to dive into coding. [^1]: A wrapper that uses WebView functionality as a Composable is available in the accompanist library, but it is currently deprecated . Even in the official implementation, making a View fully Composable doesn’t seem easy. Since it’s common for Views and Composables to coexist for a long time in general app development, there’s no need to worry—go ahead and embrace Composables with confidence. 2.3. Writing Debug Information Declaratively Displaying debug information with Composables allows you to add details even more easily with fewer steps: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... Text( text = when { targetRect in visibleRect -> "Target: inside screen" visibleRect.overlaps(targetRect) -> "Target: crossing edge of screen" else -> "Target: outside screen" }, modifier = Modifier.align(Alignment.TopStart), style = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.W600 ) ) } } /** * "operator fun receiver.contains" defines in and !in (syntax: other in(!in) receiver) */ private operator fun Rect.contains(other: Rect) = left <= other.left && top <= other.top && right >= other.right && bottom >= other.bottom The above uses Text(...) to add debug information. The style argument in Text(...) can be omitted if the default settings are sufficient. You can display information with almost the same amount of typing as Log.d(...) or println(...) . Unlike those methods, the information doesn’t disappear as you scroll. One of the advantages of Declarative UI is that it makes building a UI as effortless as "debugging with print statements". Build and run the app, then scroll horizontally in the top-right scroll view to see the app in action at the bottom of the screen, like this: Off-screen display Partially over screen borders On-screen display 3. Procedural Processing with LaunchedEffect So far, we’ve discussed the significance of Declarative UI , but when it comes to handling events triggered by state changes or adding animations to show those changes, some Procedural processing is also necessary. If you can’t write procedural code like event handling, which has traditionally been done in View, moving to Jetpack Compose won't be possible. In Jetpack Compose, LaunchedEffect is commonly used to handle actions based on state changes. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... var currentState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var stateText by remember { mutableStateOf("") } var isTextVisible by remember { mutableStateOf(true) } nextState = when { visibleRect.overlaps(targetRect) -> TargetState.CROSSING else -> TargetState.OUTSIDE } LaunchedEffect(key1 = nextState) { if (stateText.isNotEmpty()) { if (currentState == nextState) return@LaunchedEffect stateText = when (nextState) { TargetState.INSIDE -> "Target: entered screen" TargetState.OUTSIDE -> "Target: exited screen" TargetState.CROSSING -> if (currentState == TargetState.INSIDE) "Target: exiting screen" else "Target: entering screen" } currentState = nextState repeat(3) { isTextVisible = true delay(250) isTextVisible = false delay(250) } } stateText = when (nextState) { TargetState.INSIDE -> "Target: inside screen" TargetState.CROSSING -> "Target: crossing edge of screen" TargetState.OUTSIDE -> "Target: outside screen" } isTextVisible = true } if (isTextVisible) { Text( text = stateText, modifier = Modifier.align(Alignment.TopStart), ) } } } enum class TargetState { INSIDE, CROSSING, OUTSIDE } You can set multiple keys for LaunchedEffect . If you only want to run the process the first time the Composable is called, you can use LaunchedEffect(Unit) { ... } . Whenever the nextState specified as a key changes, the corresponding process will be executed accordingly. The code above will make the text flash for 1.5 seconds after a state change, showing the current state compared to the previous one, and then display it statically. You can handle events by specifying a state variable as the key of LaunchedEffect and writing the processing inside the block to run when the state changes. Inside the LaunchedEffect block, you can write time-consuming processes using suspendfunctions like delay(...) . If a state change happens before the block’s processing finishes, the current processing is canceled, and the new state’s processing starts from the beginning in response to the change. The LaunchedEffect block handles procedural processing, while the Text(...) follows a Declarative approach based on the values of the variables provided procedurally. For handling changes in the UI, it’s best to use LaunchedEffect . Besides, there are other effects suited for different situations, such as SideEffect , as well as DisposableEffect , which handles processing according to the lifecycle of Activity and Fragment. On the other hand, it’s also important not to overuse these processes to keep the code from becoming unnecessarily complex. For example, it’s recommended to handle event processing triggered by responses from the Internet or inputs from sensors like NFC in the ViewModel, while keeping procedural code in Composable limited to UI-related elements. 4. Interoperability between ComposeView and AndroidView When using a Composable in an Activity or Fragment, you can display it with ComposeView , as shown above. Conversely, if you want to use something like the aforementioned WebView inside a Composable, you can embed a View into a Composable using AndroidView or AndroidViewBinding . For instructions, please refer to Here (Using views in Compose) . This article won’t go into details, but thanks to the AndroidView Composable, even if you’ve made progress in converting your app to Composable but find that replacing certain Views is challenging or time-consuming, you can still continue development by integrating both Compose and View. This interoperability is extremely powerful—you can call AndroidView inside a Composable invoked by ComposeView , then nest another ComposeView inside it to call a Composable again, and even embed AndroidView within that, creating a layered structure that can continue further. By keeping the option to use Views within Composables, you can minimize the risk of wasted effort if the transition to Composable doesn’t progress as expected or if replacing Views takes significantly longer than a development sprint. 5. Preview Function So far, we’ve focused on the flexibility of Composable, which allows you to reflect on the development process. It supports techniques for alignment and debugging at a level comparable to Views, enables procedural processing, and offers the option to revert parts to Views through powerful interoperability when facing challenges in the transition to Composable. Here, we’ll talk about one of the key benefits of Composable—the simple yet powerful preview capabilities of the Preview function. 5.1. Create a Preview function Android Studio’s preview feature allowed you to visualize Views in layout XML files, but Jetpack Compose takes it a step further with even more powerful preview functions. Just create a function with the @Preview annotation, and your Composable will be displayed in the preview: // ... private class TargetWatcherParameterProvider : PreviewParameterProvider<TargetWatcherParameterProvider.TargetWatcherParameter> { class TargetWatcherParameter( val visibleRect: Rect, val targetRect: Rect ) override val values: Sequence<TargetWatcherParameter> = sequenceOf( TargetWatcherParameter( visibleRect = Rect(0f, 0f, 100f, 300f), targetRect = Rect(90f, 80f, 110f, 120f) ), TargetWatcherParameter( visibleRect = Rect(0f, 0f, 300f, 100f), targetRect = Rect(80f, 90f, 120f, 110f) ) ) } @Preview @Composable private fun PreviewTargetWatcher( @PreviewParameter(TargetWatcherParameterProvider::class) params: TargetWatcherParameterProvider.TargetWatcherParameter ) { KtcAdventCalendar2024Theme { TargetWatcher(params.visibleRect, params.targetRect) } } The above example demonstrates how to use PreviewParameterProvider to supply multiple parameters to a single preview function and display them in the preview. In the layout XML file of the Android View, you can’t achieve this using the tools:??? attribute to configure the preview. However, can display a preview without using PreviewParameterProvider —just call a Composable function inside a function annotated with @Preview and @Composable . My recommendation is to create a preview function as soon as you start working on a new Composable. Just the advantage of having powerful preview capabilities from the start when creating a new Composable is reason enough to switch to Jetpack Compose. Being able to check the display of debug information with the preview function is another advantage of using Composable for debugging. Recently, using preview functions for UI testing with libraries like roborazzi has been gaining attention. From a testing efficiency standpoint, creating preview functions is definitely worthwhile. 5.2. Try running the Preview function As explained here (Running the Preview) , you can run the Preview function on an actual Android device or emulator by clicking the Run '...' icon on the left side of Android Studio. This is similar to the previous functionality for executing a specific Activity, but the Preview function is more powerful. It is easier to write and it can run in a simplified environment without needing elements like intents. Callbacks for UI actions, such as button taps, are also executed, allowing you to use the Preview function for testing as if it were a simple app. However, it’s not recommended to overload a Composable with too many features just to make it more testable in the Preview function. To keep Composable purely as a declarative UI, it’s best to separate business logic into other classes or functions, such as a ViewModel or a Presenter in Circuit, and focus on writing only UI-related code. 6. Conclusion I hope this article will encourage more developers to take the leap from Android View to Jetpack Compose. Transitioning to a UI system with a different approach isn’t always straightforward, and the fear of setbacks is understandable. My hope is that your first steps feel as smooth as possible and that the risk of wasted effort is kept to a minimum. 7. References Android API reference [Android] Functions for Retrieving a View’s Position In-Depth Guide to Jetpack Compose Modifier What can Advanced / Lesser Known Modifiers do for your UI? — A Comprehensive Exploration in Jetpack Compose: A Journey Through Advanced and Notes The Android robot is reproduced or modified from work created and shared by Google and used under the terms of the Creative Commons Attribution 3.0 License. ^2
アバター
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. As part of this initiative, we are also running an in-house podcast, and for this year’s Advent Calendar, we’re excited to share some of its episodes with you. What is the "Learning Roadside Station"? "Learning Roadside Station" is a project launched to enhance the accessibility and effectiveness of the study sessions that are frequently held within the company. This initiative aims to promote knowledge sharing by supporting study sessions led by dedicated volunteers within the company. Figma Study Session The KTC Learning Roadside Station Podcast features interviews with people who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. Today's podcast guests are T.M. and M.K., who are leading the Figma Study Session. First of all, could you tell us about the work you both do? Let's start with T.M.-san. Interview T.M.-san: I am a member of the Retailer Digital Transformation (DX) Planning Team of the DX Planning Promotion Group in the Mobility Product Development Division. As a designer, I primarily work on developing products related to digital transformation (DX) for our company's retail stores. Lately I’ve been focusing on designing a mobile app, which we plan to release in the fall. Since our team was formed only about a year ago, we are now entering the phase of fully commercializing the project. Akeda-san: Thank you. Now, let’s hear from M.K.-san. M.K.-san: I work as a designer in the Marketing Planning Division. My main responsibilities include visual design and UI/UX design, and recently, I have been working on the website design for a new vehicle subscription service. Akeda-san: Thank you. Now, regarding the Figma study session held last Friday, what was the reason for holding it? T.M.-san: It all started when Nakanishi-san casually dropped by and said, “Why don’t you host a Figma study session?” However, even before that, I had felt that there was room for improvement in how we were using Figma at KTC, so it was perfect timing. So, together with M.K., we discussed the best format, and held our first session last Friday. Akeda-san: Nakanishi-san, what made you reach out and suggest the study session? Nakanishi: Since T.M. joined the company, I felt that he had a strong desire to improve the design. We had already talked about how it would be great to hold a Figma study session, so I felt that now was the right time and reached out. Akeda-san: Thank you. What was the reaction of the participants when you actually held the study session? T.M.-san: The response was quite positive. It seemed like many of the participants wanted to learn more about Figma. There were a lot of questions during the Q&A session, and it was clear that everyone was interested. M.K.-san: I felt the same way. It was a great opportunity to see that many people are interested in Figma and improving UI/UX. Akeda-san: What kind of questions were asked? T.M.-san: The questions covered a wide range of topics, from technical aspects to discussions about UI/UX initiatives at KTC. There were especially many questions from engineers, which showed that they, too, have a strong interest in UX. Akeda-san: Please tell us about future plans for the study group. T.M.-san: In the short term, we aim to improve productivity and efficiency by providing the most relevant themes and content. In the long term, I want to use Figma as a way to increase the presence of designers and contribute to the success of businesses. M.K.-san: I completely agree. We also want to create more opportunities for engineers and designers to develop a common language and communicate with each other. Akeda-san: Thank you. Finally, do you have a message for everyone listening to this podcast? M.K.-san: Design is not just about visuals. It's important for everyone to have an opinion. Let’s communicate freely and openly. T.M.-san: To create a great product, everyone should think like a designer. Please feel free to reach out to us anytime! Akeda-san: Thank you very much. That wraps up today's podcast. T.M.-san and M.K.-san, thank you for joining us. Your passion for the Figma study session and design really came through. I’m excited to see how KTC's design continues to evolve. This time, we have provided details about the Figma study group, the background to its operation, and future prospects. Stay tuned for the next study session!
アバター
Introduction Hi, my name is Pann Nu Wai, and I am part of the QA Group at KINTO Technologies. As a Test Automation Specialist for the App team in the QA Group, I am responsible for building and maintaining the test automation environment for the KINTO Kantan Moushikomi App , as well as writing test specifications and test scripts. Previously, I wrote a technical article about DarkMode automation testing using Appium . I have been working with automated testing using Appium for three years. In my team, we follow a process where test specifications are defined first before writing the automated test source code. So, in this post, I’d like to share an approach to creating test specifications that has proven highly useful when writing Java source code for Appium. Before diving into the details, I’ll first explain why this method of creating test specifications is essential. Benefits of Creating Test Specifications Saves time when writing automated test source code. Easy to understand even for non-automation testers. Improves readability by organizing specifications by screen, source code class, and operation function. In this article, we will use the login scenario specification for the KINTO Kantan Moushikomi App as a sample test specification. Login steps Here, we will explain each step of logging in to the KINTO Kantan Moushikomi App. Step 1 Step 2 Step 3 Collecting Test Data for Login As the first step in creating a login test specification, it is essential to gather test data, including the test environment, test account, password, report name (automatically generated after test execution), and test execution file. Now, let’s begin with the iOS login scenario specification. Data Name Data Information Environment Stg4 Test Account ******@gmail.com Password ******** Report Name For iOS iOS.poc.login For Android android.poc.login Executable Source File For iOS iOS.poc.login.xml For Android android.poc.login.xml Login Scenario Specification for iOS Confirmation Function Screen Operation Points to Check Executable File (xml) Source File Method Log in Main Screen Click on the My Page logo The login button can be pressed iOS.poc.login.xml iOS.MainPage clickMyPagingLogo My Page Screen Click on "Application Details" Press "Log in here!" iOS.MyPagingPage clickApplyTab clickLoginHereButton Login Screen Enter the above test account in the "Email address (KINTO ID)" field. Enter the above password in the "Login Password" field. Click "Log in to My KINTO" iOS.LoginPage fillMailAddress fillPassword clickToMyKintoLoginButton Now let’s create a Java class for each screen in the scenario specification. First, click the My Page logo on the main screen (MainPage.java). MainPage.java public class MainPage extends Base { public static final String MY_PAGING_LOGO = "//XCUIElementTypeButton[@name="My Page"]"; /** * Test method for clicking the "My Page" logo on the main screen * * This method uses XPath to locate the "My Page" logo on the main page * and performs a click action. * */ @Test(groups = "MainPage") public void clickMyPagingLogo() { driver.findElementByXPath(MY_PAGING_LOGO).click(); } } Step 2: Click "Application Details" on the My Page screen, then click "Log in here." MyPagingPage.java public class MyPagingPage extends Base { public static final String APPLY_TAB = "//XCUIElementTypeButton[@name="Application Details"]"; public static final String LOGIN_HERE_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * Test method to click "Application Details" on the My Page screen * * This method uses XPath to identify the "Application Details" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnGroups = "MainPage") public void clickApplyTab() { driver.findElementByXPath(APPLY_TAB).click(); } /** * A test method to click "Log in here" on the My Page screen * * This method uses XPath to identify "Log in here" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnMethods = "clickApplyTab") public void clickLoginHereButton() { driver.findElementByXPath(LOGIN_HERE_BUTTON).click(); } } Next, in step 3, enter your email address and password on the Login Screen, then press the Login button. LoginPage.java public class LoginPage extends Base { public static final String EMAIL_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[2]/XCUIElementTypeTextField"; public static final String PASSWORD_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[3]/XCUIElementTypeSecureTextField"; public static final String ENTER_KEY = "//XCUIElementTypeButton[@name="Return"]"; public static final String TO_MY_KINTO_LOGIN_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * A test method to enter a test account in the "Email address (KINTO ID)" field on the login screen * * This method uses XPath to search for the "email address" on the login screen * and enter the test account (Parameter) in the xml file. * */ @Parameters("email") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillMailAddress(String email) { driver.findElementByXPath(EMAIL_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(email); }   /** * A test method to enter a password in the "Login Password" field on the login screen. * * This method uses XPath to get the login password from the login screen. * and enter the password (Parameter) for the xml file. * */ @Parameters("password") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillPassword(String password) { driver.findElementByXPath(PASSWORD_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(password); driver.findElementByXPath(ENTER_KEY).click(); } /** * A test method to click "Log in to My KINTO" on the login screen * * This method uses XPath to identify "Log in to My KINTO" on the login screen, * and performs a click action. * */ @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void clickToMyKintoLoginButton() { driver.findElementByXPath(TO_MY_KINTO_LOGIN_BUTTON).click(); } } The xml file below is the test execution file for automated testing. Each function is written in sequence based on the test specification. <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="iOS.poc.login"> <test verbose="2" name="iOS.poc.login"> <classes> <class name="iOS.MainPage"> <methods> <include name="clickMyPagingLogo"/> </methods> </class> <class name="iOS.MyPagingPage"> <methods> <include name="clickApplyTab"/> <include name="clickLoginHereButton"/> </methods> </class> <class name="iOS.LoginPage"> <methods> <parameter name="email" value="******.gmail.com"/> <parameter name="password" value="*********"/> <include name="fillMailAddress"/> <include name="fillPassword"/> <include name="clickToMyKintoLoginButton"/> </methods> </class> </classes> </test> </suite> Summary In this article, we've outlined how to create test specifications for Appium Java source code. However, we believe this approach will be valuable not only for those working with Appium but also for developing automated test source code across different frameworks.
アバター