KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

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

991

Introduction I am Okapi, from the Quality Assurance Group. Since I often participate in projects as the primary person in charge of QA, in this article, I'd like to share how QA team participates in projects, communicates with the development team, and progresses in its testing at KINTO Technologies. Purpose of This Article When proceeding a project with a team that has never worked with QA before, there is often some trial and error involved as they try to understand what QA can contribute and how the process will unfold. To ensure smoother progress in such cases, I aim to raise awareness of QA. What is QA? QA is an acronym for "Quality Assurance." "Quality Assurance" is a broad term that involves ensuring quality in various aspects. We conduct testing from the user's perspective, by focusing on scenario testing based on actual user usage assumptions and verifying screen UI to prevent any inconvenience and ensure usability. The primary roles of the QA and development teams in testing are outlined in the table below. | Item | QA | Development | Remarks | | ---- | ---- | ---- | ---- | | Verify specifications against system requirements | ㅤ◎ㅤㅤ | ㅤ〇ㅤㅤ| QA ensures that functions and performance meet system requirements, based on user's perspective. | |Verify according to scenarios of user use | ㅤ◎ㅤㅤ | ㅤ△ㅤㅤ| Mainly verified by QA | | Other than the above | ㅤ△ㅤ ㅤ| ㅤ◎ㅤㅤ| When requested, the development team's external integration testing and QA will verify within feasible resource limits | Overview of QA testing Phase Summary Test Plan Phase Share overall project schedule and specification documents (system requirements and screen specifications) to allow QA to create a test plan detailing how the subsequent phases will proceed. Test Analysis Phase Based on specification documents (system requirements and screen specifications), create test perspectives that clarify the scope of testing (to be tested/not to be tested). Test Design Phase Create test cases (prerequisites, procedures, and expectations) based on the test perspectives. Test Implementation Phase Conduct tests based on the created test cases, report defects, and verify modifications. Points Requiring Communication with the Development Team Test Plan Phase Ensure alignment with the development team based on the test plan, which outlines the QA testing period, verification environment, person in charge of conducting QA testing, development team contact, and target devices/browsers. Test Analysis Phase Use JIRA or Confluence to ask questions about the information necessary to create test perspectives. For areas with differing understandings or multiple development team contacts, hold meetings to confirm and organize the specifications. Once the specifications are organized, create test perspectives that clearly define the test scope (to be tested/not to be tested) and confirm alignment with the development team. For the test scope, from a black box testing perspective, the target is on areas users will actually interact with (as deemed necessary by QA for the project), while excluding areas users do not access, such as system administration screens. However, since scenario testing involves verifying the sequence of flow, areas that intersect with the user-side testing, even if typically excluded, are included as test targets. *For regression testing, where quality is assured, the test scope is adjusted based on the shared schedule and resources. Test Design Phase The procedure for conducting tests will be reviewed, but test cases are created based on the agreed-upon test perspectives. Therefore, alignment with the development team is basically not conducted at this phase. Test Implementation Phase Issues (expected results differing from specifications), questions (missing or unclear specifications), and improvement requests (aligned with specifications but unclear to users) are reported in JIRA. Once addressed by the development team (fixes), the QA team conducts re-verification. Testing is considered complete when all test cases and the JIRA tickets are addressed, or when the remaining JIRA tickets are excluded from the current QA test scope. Once implementation is complete, we participate in the overall retrospective (e.g., KPT analysis) to discuss areas for improvement and apply them to future projects. Future Challenges Depending on the project or product, "how to organize the documents" may differ, so after aligning on specifications in JIRA or Confluence, QA organizes and documents the overall user-related system specifications and workflows before moving forward. However, depending on the system's scale, this can be time-consuming. By organizing documents and processes within the QA team and ensuring smooth communication with the development team, we aim to efficiently summarize specifications for accurate understanding. Conclusion The QA team is an independent organization, which may give the development team the impression that they are requesting tasks to QA or that QA is simply conducting tests. However, as part of KINTO Technologies, we see ourselves as partners working together to create high-quality systems. Therefore, we aim to continue fostering a collaborative relationship.
KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 my routeアプリのライブラリ管理ツールがついに!CocoaPodsからSwift Package Manager(以下SPM)に移行しました! はじめに my routeではCocoaPodsを使用していました。 しかし2024年夏ごろ、CocoaPodsから以下のお知らせが発布されました。 https://blog.cocoapods.org/CocoaPods-Support-Plans/ CocoaPodsがメンテナンスモードに移行するというお知らせです。 使えなくなるわけではありませんし、セキュリティの問題などには対応してくれます。ただし、GitHub issueにおけるサポートや新機能の追加などは行われなくなります。 そうすると今後、新しいライブラリを入れたい時にCocoaPodsがサポートされなくなっていたり、CocoaPods自体に何か問題が起こっても直すことができない、といった場面が起こり得ます。 以前からmy routeではSPMへの移行の話は持ち上がっていましたが、このCocoaPodsのお知らせが追い風となり、満を持してSPMへの移行に着手しました。 Swift Package Managerとは SPMとは、Appleが提供する純正ライブラリ管理ツールです。 https://github.com/swiftlang/swift-package-manager iOSでライブラリ管理ツールといえば、他にはCocoaPodsやMintなどのサードパーティツールがあります。これらと最も大きく違うのは、Xcodeに統合されているという点です。 これにより、必要なタイミングで Package.resolved を更新するだけで、プロジェクトファイルを開いた時やビルド時に最新の指定バージョンのライブラリを取り込んでくれます。 バージョン更新をするたびにチームへ pod install を実行してくれ〜と声をかける必要がなくなるのです。 移行したよ ひたすら移していくのみですが、いくつか躓いたポイントや工夫点があるので紹介します。 指定したバージョンのライブラリが降ってこない 使用するバージョンを変更する際、Xcodeで「Reset Package Caches」や「Update to Latest Package Versions」を実行しても、Package.swiftで指定しているバージョンが降ってこない問題がありました。DerivedDataを消しても直りません。 これは、 ~/Library/Caches/org.swift.swiftpm や ~/Library/org.swift.swiftpm などに深淵のキャッシュが残っているためです。これらを消すと正しいバージョンのライブラリが降ってくるようになります。 テストの時だけビルドが通らない my routeは複数のスキーマを持っており、それぞれ依存するライブラリが少し異なります。 そのため以下のようにプロジェクト自体のPackage.swiftとは分離した構成になっています。 プロジェクトファイルと同じワークスペース配下にDependenciesパッケージを作成し、このパッケージ内で各スキーマと1対1になるproduct(フレームワーク)となるようにライブラリを管理しています。 これらのproductを右側のスクリーンショットのように、それぞれのターゲットの Frameworks, Libraries, and Embedded Content にて紐付けています。 しかしこの構成にしていると、 xcodebuild build は通りますが、 xcodebuild build-for-testing が通らないという問題が起こりました。XcodeのGUI上ではRunとTestに対応するコマンドです。 本来、パッケージのものはパッケージ内でテストします。 しかし上記の構成では、プロジェクト本体のメインのターゲットでテストを実行します。つまり、パッケージの外でテストしているということです。 ということはこれは...違法建築...なのですが、ゆくゆくは同じパッケージ内にテストを詰めるように直していくということで、一旦構成は維持したままテストできるようにします。 RunとTestではLinkerあたりの挙動が異なるらしいことが原因でした。 Runのビルド生成物を①、Testのビルド生成物を②とします。 ①にはDependenciesのSPMが含まれていますが、②には含まれていません。 そのため、MainTarget内のテストがDependenciesのライブラリに依存しているとビルドに失敗してしまいます。 簡単な解決策としては、MainTarget内のSPMに持たせるとRunでもTestでもビルドに含まれるため、そちらにテストで必要なライブラリを移します。 根本的に解決するには、先述のように1つのパッケージ内でテストが簡潔するようにすると良いでしょう。 SwiftLintやLicensePlistをSPMに乗せる SwiftLintやLicensePlistはプロジェクトのBuild Phaseに含めてビルド時に実行されるようにしたいため、workspaceから独立した場所に別のパッケージを作成します。 Project/ ├── Test.xcworkspace ├── Test.xcodeproj ├── Test/ │ └── ... ├── ... └── tools // <- this! ├── Package.swift └── Package.resolved 新しく作成したtoolsパッケージにSwiftLintやLicensePlistなどBuild Phaseに含めたいライブラリを入れます。 そして以下のようなシェルを用意し、初回だけ実行してローカルにライブラリを落としておきます。 SPM_PACKAGE_PATH=tools SPM_BUILD_PATH=tools/.build/release echo "swiftlint" && $SPM_BUILD_PATH/swiftlint --version || swift run -c release --package-path $SPM_PACKAGE_PATH swiftlint --version echo "license-plist" && $SPM_BUILD_PATH/license-plist --version || swift run -c release --package-path $SPM_PACKAGE_PATH license-plist --version すると tools/.build/release/swiftlint で実行可能になるので、これをBuild Phaseに入れます。 LicensePlistも同様です。 Bitriseなどから実行する際も、シェルを実行してからプロジェクトをビルドすると実行されることが確認できます。 おわりに 移行してから数ヶ月経ち、まだまだいくつかの問題もありますが、ライブラリの入れ替えは楽になったと思います。 またライブラリ起因の問題かどうか判断したい時に、アプリ版のPraygroundsでサクッと確認できることが個人的には楽に感じます。 SPMがプロジェクトで使えるようになったことで、SPMでしか提供されていないApple製ライブラリも使えるようにもなりました。 今後はこれらのライブラリも使ってより良い実装にしていきたいです。
はじめに こんにちは、クラウドインフラグループの松尾です。 私たちクラウドインフラグループでは、社内全体のインフラ領域を設計から運用まで幅広く担当しています。 今回は、re:Inventで発表された AWS Transfer Family ウェブアプリ についてご紹介します。 https://aws.amazon.com/jp/about-aws/whats-new/2024/12/aws-transfer-family-web-apps/ 本記事では、ウェブアプリの概要、作成手順、設定方法、そして実際に使用してみた感想をお伝えします。参考になれば幸いです。 概要 AWS Transfer Family ウェブアプリ とは、簡単に言うと、ブラウザ上でS3バケット内のファイルを操作できるアプリケーションを、コードを書くことなく作成できるサービスです。 従来、GUIでS3にアクセスするにはAWSコンソールやWinSCPなどの専用ソフトウェアが必要で、設定も必要でした。 そのため、非技術者や一時的なユーザーには操作が難しいという課題がありました。 AWS Transfer Family ウェブアプリは、直感的なインターフェースで、技術的な知識がなくても簡単に操作できる点が特徴です。 さらに、IAM Identity CenterやS3 Access Grantsと統合されているため、細かいアクセス制御も可能です。 料金について AWS Transfer Family ウェブアプリの料金は、ユニット数(1ユニットで5分間に最大250セッション)に基づく1時間単位の従量課金制です。 リージョン 料金 東京 ユニット数*0.5 USD/時間 例えば、1ユニットを24時間フル稼働させると 12USD/日 、1か月では 360USD となり、 長時間の利用ではかなりのコストがかかります。 手軽なサービスの割には高額と言えますね。 利用ケースによっては過剰なコストとなる可能性があるため、用途に応じたサービスの利用が大事ですね。 Webアプリの作成方法 それでは早速アプリを作成してみましょう。 (執筆時点ではTerraform未対応のため、AWS Management Consoleを使用します。) AWS Management Consoleにログインし、AWS Transfer Familyのナビゲーションペインから「ウェブアプリ」を選択し、ウェブアプリを作成をクリック。 今回はNameタグのみ設定して「次へ」をクリック。 ※ IAM Identity Centerの設定が事前に必要 です。未設定の場合、先に準備してください。 ページタイトル、ロゴ、ファビコンを設定し、「作成」をクリック。 今回、ロゴ・ファビコンは設定しないでいきます。 これでウェブアプリの作成は完了です! 簡単ですね! ユーザー/グループとS3 Access Grantsの設定 次に、ウェブアプリにグループの割り当てとS3 Access Grantsを設定していきます。 グループの割り当て ウェブアプリ詳細画面で「ユーザーとグループの割り当て」をクリック。 今回はグループを用意したので、既存のグループを選択して割り当てます。 S3 Access Grantsの設定 次にAccess Grantsの設定です。ロケーションと権限の2つを設定します。 S3のナビゲーションペインからAccess Gratsに移動すると、「権限」 「ロケーション」タブがあるのでここから設定していきます。 ロケーションから設定します。 ロケーションではアクセス対象のS3バケット及び、S3バケットに対する権限を割り当てたIAMロールを指定します。 今回は検証のためAWS管理ポリシー"AmazonS3FullAccess"を付与してます。 権限では先ほど設定したロケーションを指定し、サブプレフィックス、許可、被付与者タイプ、IAM プリンシパルタイプ、IAM プリンシパルユーザーを設定し、権限を作成します。 IAM Identity Centerで認証を行うので、画像の通り、設定しています。 今回は設定していませんが、オプションとして、アプリケーション単位で制限をすることも出来るようです。 CORSの設定 バケットの中身を参照できるように対象のS3バケットで Cross-Origin Resource Sharing (CORS) からCORS設定を追加します。 今回は以下の内容を設定しました。 [ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "PUT", "POST", "DELETE", "HEAD" ], "AllowedOrigins": [ "https://${ウェブアプリID}.transfer-webapp.ap-northeast-1.on.aws" ], "ExposeHeaders": [ "Access-Control-Allow-Origin" ] } ] 試してみる ここまで出来たら準備完了です。 ウェブアプリのURLにアクセスし、認証を行ったらS3の管理画面が出てきました! ロケーションで設定したS3バケットが表示されていることが分かります。 それじゃあ実際に今回は対象バケットにs3ファイルの削除をしてみましょう。 Folderのリンクからオブジェクトを操作しようとしましたが、あれ?権限が不足してるようですね。 どうやらAccess Grantsのロケーションで設定したIAMロールでsts:SetContextの設定が必要だったようです。 User Guide にも記載がありました。 設定を追加して再度試したところ、無事オブジェクトの確認が出来ました。 ちなみにCORSの設定ができていないとここでNetwork Errorが出てきます それじゃあ、ここから本当にオブジェクトを選択して、削除をしてみます。 操作も簡単です。 削除したいオブジェクト(今回はテスト1.txt)を選択して三点リーダーからDeleteを選択するだけです。 Webアプリ上から削除されてますね。 コンソール上からも同様に削除できてることを確認しました。 感想 IAM Identity CenterやAccess Grantsの設定が必要でしたが、ノーコードでS3の管理アプリケーションを簡単に構築できる点は非常に便利だと感じました。 設定箇所が多いため、戸惑った部分も少しありましたが、 IaCに対応することで、さらに使いやすくなると思います。 また、本サービスは同じくre:Inventで発表された Storage Browser for Amazon S3 を基にしているため、 プログラムスキルがある方はそちらのサービスを利用することで独自のカスタマイズも可能です。 https://aws.amazon.com/jp/about-aws/whats-new/2024/12/storage-browser-amazon-s3/ このブログが少しでも参考になれば幸いです。
はじめに こんにちは。KINTOテクノロジーズ(以下KTC) プラットフォームグループ Platform Engineeringチームで内製ツールの開発・運用をおこなっている山田です。 Platform Engineeringチームが開発するCMDBについて詳しく知りたい方は以下の記事をご覧ください! https://blog.kinto-technologies.com/posts/2023-12-14-CMDB/ 今回はCMDBの機能の一つであるチャットボットに、 生成AIとText-to-SQL を活用したCMDBのデータ検索機能とcsv出力機能を実装したお話をしたいと思います。 CMDBのチャットボットではCMDBの利用方法の質問やCMDBで管理されたデータに関する質問が可能です。 CMDBのデータに関する質問は、元々はChromaDBを使ったRAGの仕組みで質問に対する回答を生成していましたが、以下の理由からText-to-SQLでの実装に移行しました。 RAGと比較したText-to-SQLのメリット データの正確性と即時性 CMDBのデータベースから直接最新データをリアルタイムで取得が可能 データの更新に追加の処理が不要 システムの簡素化 ベクトルDBやEmbedding処理のためのインフラが不要(ChromaDBとEmbeddingしたデータの追加用バッチが不要になった) これらの理由から、CMDBのような構造化されたデータを扱うシステムにおいては、Text-to-SQLの方が適していると判断しました。 Text-to-SQL とは Text-to-SQLは自然言語のクエリからSQLクエリに変換する技術のことです。そのためSQLの知識を持たないユーザーでもデータベースから必要な情報を簡単に抽出することが可能になります。 これにより、CMDBのデータベースで管理しているプロダクト、ドメイン、チーム、ユーザー、ECR, VMDR等の脆弱性情報などのデータを自然言語のクエリから取得できるようになります。 KTC内で活用できそうな例だと、 適切な管理がされていないドメイン(CMDBでプロダクトと紐づいていないドメイン)一覧を取得 全社員の Atlassian ID の取得 MSP(Managed Service Provider)チームがPCの脆弱性対応などの依頼を、該当者をメンションした形でチケット作成するため グループごとに担当するプロダクトの関連リソースに検出された脆弱性数の集計 AWSリソースの起動停止スケジュールが設定されていないプロダクトの抽出 以前までだとこのようなデータの抽出依頼がPlatform Engineeringチームに来た際、担当者がCMDBのデータベースから直接SQLクエリを実行し、データを抽出、加工して依頼者にデータを渡していました。 依頼者がCMDBのチャットボットでText-to-SQLを使ってデータを抽出することができるようになると、以下の図のようにわざわざ担当者に依頼をしなくても簡単にデータの抽出ができるようになります。 Text-to-SQLは便利な機能ですが、安全でないSQL生成のリスクに気を付けなければなりません。 以下の図の例は極端なケースですが、自然言語からSQLが生成されるため意図せずにデータの更新や削除、テーブル構造の変更をするSQL文が生成される恐れがあります。 そのため以下の方法で安全でないSQLの生成を回避する必要があります。 Read OnlyのDBエンドポイントに接続する DBのユーザーを参照権限のみに設定する アプリケーションの実装で SELECT 以外のコマンドは実行しないようバリデーションチェックをおこなう システム構成 こちらがCMDBのアーキテクチャになります。今回のお話に関係のないリソースは除外してあります。 最初に説明した通り、元々ベクトルDBとしてChromaDB、CMDBの使い方に関する情報をConfluenceから取得(LlamaIndexで実装)、CMDBのデータをデータベースから取得(Spring AIで実装)してChromaDBに投入していました。 今回はCMDBのデータに関する質問の回答を Spring AI + ChromaDB でのRAG機能から、Text-to-SQLを使った機能に移行しました。 Text-to-SQLの実装 ここからは実際のコードをお見せしながら実装内容を説明したいと思います。 CMDBデータの検索機能 スキーマ情報を取得 まずはLLMにSQLを生成させるときに必要なスキーマ情報を取得します。 スキーマ情報は少ない方が精度は上がるため、必要なテーブルのみ指定する方法にしました。 また、LLMがSQL文を生成するときの判断材料としてテーブルのカラムのコメントが重要になるため、事前に全て追加しておく必要があります。 def fetch_db_schema(): cmdb_tables=['table1', 'table2', ...] cmdb_tables_str = ', '.join([f"'{table}'" for table in cmdb_tables]) query = f""" SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.TABLE_COMMENT, c.COLUMN_NAME, c.DATA_TYPE, c.COLUMN_KEY, c.COLUMN_COMMENT FROM information_schema.COLUMNS c INNER JOIN information_schema.TABLES t ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME WHERE t.TABLE_SCHEMA = 'cmdb' AND t.TABLE_NAME IN ({cmdb_tables_str}) ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, c.COLUMN_NAME """ connection = get_db_connection() try: cursor = connection.cursor() cursor.execute(query) return cursor.fetchall() finally: cursor.close() connection.close() 取得結果例 TABLE_SCHEMA TABLE_NAME TABLE_COMMENT COLUMN_NAME DATA_TYPE COLUMN_KEY COLUMN_COMMENT cmdb product プロダクトテーブル product_id bigint PRI プロダクトID cmdb product プロダクトテーブル product_name varchar プロダクト名 cmdb product プロダクトテーブル group_id varchar プロダクトの担当部署(グループ)ID cmdb product プロダクトテーブル delete_flag bit 論理削除フラグ 1=削除, 0=未削除 取得したスキーマ情報をLLMに渡すプロンプト用のテキストにフォーマットする def format_schema(schema_data): schema_str = '' for row in schema_data: schema_str += f"Schema: {row[0]}, Table Name: {row[1]}, Table Comment: {row[2]}, Column Name: {row[3]}, Data Type: {row[4]}, Primary Key: {'yes' if row[5] == 'PRI' else 'no'}, Column Comment: {row[6]}\n" return schema_str カラムごとに以下のようなテキストに変換してLLMにスキーマ情報を渡します。 Schema: cmdb, Table Name: product, Table Comment: プロダクトテーブル, Column Name: product_id, Data Type: bigint, Primary Key: PRI, Column Comment: プロダクトID Schema: cmdb, Table Name: product, Table Comment: プロダクトテーブル, Column Name: product_name, Data Type: varchar, Primary Key: no, Column Comment: プロダクト名 Schema: cmdb, Table Name: product, Table Comment: プロダクトテーブル, Column Name: group_id, Data Type: varchar, Primary Key: no, Column Comment: プロダクトの担当部署(グループ)ID Schema: cmdb, Table Name: product, Table Comment: プロダクトテーブル, Column Name: delete_flag, Data Type: bit, Primary Key: no, Column Comment: 論理削除フラグ 1=削除, 0=未削除 CMDBのチャットボットからの質問とスキーマ情報からLLMでSQLクエリを生成する ここが自然言語からSQLクエリを生成するText-to-SQLの部分です。 質問内容とスキーマ情報をもとにプロンプトで様々な条件を指定してLLMにSQLを生成させています。 例えば、 MySQL:8.0 で有効なクエリを生成する ID以外の条件式はあいまい検索にする 基本的に論理削除されたデータは検索対象外とする SQL文以外は生成しない コンテキスト情報の追加 「KTCの~」、「CMDBの~」という質問は「全ての~」に変換する リージョンに関する質問はAWSのリージョンに変換する 東京リージョン -> ap-northeast-1 に変換 などがあり、特に「SQL文以外は生成しない」の指示が重要で、これがうまく伝わらないと 「頂いた情報をもとに次のSQLを生成しました。SELECT ~」 このような不要なテキストも含めた回答になってしまうことがよくありました。 したがって不要なテキスト、説明、マークダウン形式などが生成されない、「SELECT ~」のみのSQL文が生成されるようなプロンプトが必要です。 def generate_sql(schema_str, query): prompt = f""" Generate a SQL query based on the given MySQL database schema, system contexts, and question. Follow these rules strictly: 1. Use MySQL 8.0 syntax. 2. Use `schema_name.table_name` format for all table references. 3. For WHERE clauses: - Primarily use name fields for conditions, not ID fields - Use LIKE '%value%' for non-ID fields (fuzzy search) - Use exact matching for ID fields - Include "delete_flag = 0" for normal searches - Use "delete_flag = 1" only when the question specifically asks for "deleted" items CRITICAL INSTRUCTIONS: - Output MUST contain ONLY valid SQL query. - DO NOT include any explanations, comments, or additional text. - DO NOT use markdown formatting. - DO NOT generate invalid SQL query. Process: 1. Carefully review and understand the schema. 2. Generate the SQL query using ONLY existing tables and columns. 3. Double-check query against schema for validity. System Contexts: - Company: KINTO Technologies Corporation (KTC) - System: Configuration Management Database (CMDB) - Regions: AWS Regions (e.g., Tokyo region = ap-northeast-1) Interpretation Rules: - "KTC" or "CMDB" in query: Refer to all information in the database Examples: "Employees in KTC" -> "All users" "KTC's products" -> "All products" "Domains on CMDB" -> "All domains" - Region mentions: Interpret as AWS Regions Example: "ECR repositories in Tokyo region" -> "ECR repositories in ap-northeast-1" Database Schema: {schema_str} Question: {query} """ return llm.complete(prompt).text.strip() LLM, Text-to-SQLで生成したSQLに対して、 SELECT 文のみ許可するようバリデーションチェックをおこなう 安全でないSQL生成リスクの対策としてRead OnlyのDBエンドポイントに接続していますが、クエリ以外のSQLが生成されていないか確認をします。 LLMで生成されたSQLクエリを実行する LLMで生成されたSQLクエリとSQLの実行結果、質問内容からLLMで回答を生成する 最後に実行したSQLクエリ、SQLの実行結果、質問内容をLLMに渡して回答を生成します。 ここではText-to-SQLのときの指示が多いプロンプトとは違い、多くの指示はしていないですが、DBスキーマの構成や物理名は回答に含めないような指示は追加しています。 def generate_answer(executed_sql, sql_result, query): prompt = f""" Generate an answer based on the provided executed SQL, its result, and the question. Ensure the answer does not include information about the database schema or the column names. Respond in the same language as the question. Executed SQL: {executed_sql} SQL Result: {sql_result} Question: {query} """ return llm.stream_complete(prompt) 実行結果 質問:プラットフォームグループのプロダクトを教えて この質問とデータベーススキーマからLLMが以下のようなSQLを生成します。 SELECT product_name FROM product WHERE group_name LIKE '%プラットフォーム%' AND delete_flag = 0; そして、これらの情報とSQLの実行結果をLLMに渡して回答を生成しています。 これはECRスキャン結果の脆弱性情報の取得結果です。 CMDBデータのcsvファイル出力機能 スキーマ情報を取得 取得したスキーマ情報をLLMに渡すプロンプト用のテキストにフォーマットする ここまではCMDBデータの検索機能と同じです。 CMDBのチャットボットからの出力依頼とスキーマ情報から、LLMでSQLクエリを含むJSONオブジェクトを生成する ここでCMDBのデータをcsvとして出力したい内容の自然言語から、LLMを使って出力時のカラム名とそれを検索するためのSQL文のJSONオブジェクトを生成させます。 条件の内容は基本的にCMDBデータの検索機能のプロンプトと同じですが、テンプレート通りのJSONオブジェクトを生成させるための指示を強調しています。 以下がそのプロンプトです。 prompt = f""" Generate a SQL query and column names based on the given MySQL database schema, system contexts and question. Follow these rules strictly: 1. Use MySQL 8.0 syntax. 2. Use `schema_name.table_name` format for all table references. 3. For WHERE clauses: - Primarily use name fields for conditions, not ID fields - Use LIKE '%value%' for non-ID fields (fuzzy search) - Use exact matching for ID fields - Include "delete_flag = 0" for normal searches - Use "delete_flag = 1" only when the question specifically asks for "deleted" items Process: 1. Carefully review and understand the schema. 2. Generate the SQL query using ONLY existing tables and columns. 3. Extract the column names from the query. 4. Double-check query against schema for validity. System Contexts: - Company: KINTO Technologies Corporation (KTC) - System: Configuration Management Database (CMDB) - Regions: AWS Regions (e.g., Tokyo region = ap-northeast-1) Interpretation Rules: - "KTC" or "CMDB" in query: Refer to all information in the database Examples: "Employees in KTC" -> "All users" "KTC's products" -> "All products" "Domains on CMDB" -> "All domains" - Region mentions: Interpret as AWS Regions Example: "ECR repositories in Tokyo region" -> "ECR repositories in ap-northeast-1" Output Format: Respond ONLY with a JSON object containing the SQL query and column names: {{ "sql_query": "SELECT t.column1, t.column2, t.column3 FROM schema_name.table_name t WHERE condition;", "column_names": ["column1", "column2", "column3"] }} CRITICAL INSTRUCTIONS: - Output MUST contain ONLY the JSON object specified above. - DO NOT include any explanations, comments, or additional text. - DO NOT use markdown formatting. Ensure: - "sql_query" contains only valid SQL syntax. - "column_names" array exactly matches the columns in the SQL query. Database Schema: {schema_str} Question: {query} """ LLM, Text-to-SQLで生成したSQLに対して、 SELECT 文のみ許可するようバリデーションチェックをおこなう LLMで生成されたSQLクエリを実行する ここもCMDBデータの検索機能と同じです。 実行結果を使ってcsvファイルを出力する LLMに生成してもらったSQLの結果とカラム名を使ってcsvファイルの出力をします。 column_names = response_json["column_names"] # LLMで生成したJSONオブジェクトからカラム名を取得 sql_result = execute_sql(response_json["sql_query"]) # LLMで生成したSQLの実行結果 csv_file_name = "output.csv" with open(csv_file_name, mode="w", newline="", encoding="utf-8-sig") as file: writer = csv.writer(file) writer.writerow(column_names) writer.writerows(sql_result) return FileResponse( csv_file_name, media_type="text/csv", headers={"Content-Disposition": 'attachment; filename="output.csv"'} ) 実行結果 出力したい内容とカラムを指定してチャットに投げると、以下の流れでcsvファイルを出力できるようになりました。 まず、チャットでのメッセージとデータベーススキーマからLLMが以下のようなJSONオブジェクトを作成します。 { "sql_query": "SELECT service_name, group_name, repo_name, region, critical, high, total FROM ecr_scan_report WHERE delete_flag = 0;", "column_names": ["プロダクト名", "部署名", "リポジトリ名", "リージョン名", "critical", "high", "total"] } これらの情報をもとにSQLを実行して、csvファイルを出力する流れです。 プロダクト名 部署名 リポジトリ名 リージョン名 critical high total CMDB プラットフォーム ××××× ap-northeast-1 1 2 3 CMDB プラットフォーム ××××× ap-northeast-1 1 1 2 CMDB プラットフォーム ××××× ap-northeast-1 1 1 2 次のステップ ここまで生成AIとText-to-SQLを活用して、CMDBデータの検索機能とcsvデータ出力機能を実装しましたが、以下のようにまだまだ改善の余地があります。 CMDBデータの検索機能では2回LLMを呼び出しているため、速度が遅い 複雑、あいまいな質問に弱い 自然言語は本質的にあいまいなので、質問の内容に対して複数の解釈ができてしまう スキーマの正確な理解 スキーマ情報は複雑で、テーブル間のカラムの関係を理解させるのが大変 コンテキスト情報の追加 現状は最初のプロンプトで最低限のコンテキスト情報を追加しています。今後、より多くのコンテキスト情報を追加するとなると、最初のLLM呼び出しの前に質問内容を大量のコンテキスト情報から適切な質問に変換する処理方法や、fine-tuningでKTC特有のコンテキスト情報を含むデータセットで追加学習させる方法を検討しています Query Routingの実装 フロントエンドから呼び出すAPIはCMDBのデータ検索とcsv出力で2つ分かれているため、APIは1つに統一して、質問の内容からどちらのAPIを呼び出すべきか判断できるように改善したい さいごに 今回は生成AIとText-to-SQLを活用したCMDBのデータ検索機能とcsv出力機能についてお話ししました。 生成AI関連の技術は日々新しいものが出続けているためキャッチアップが大変ですが、今後はこれまで以上にアプリケーション開発にAIが絡んでくるため、興味のあるものや社内のプロダクトに適用できそうなものは積極的に活用していきたいと思います。
はじめに こんにちは! KINTOテクノロジーズでiOSアプリ開発に携わっているFelixです。今回は、8月22日(木)から24日(土)に開催されたiOSDCでの体験を共有したいと思います。前回のブログでお話した trySwift に続き、今回が二回目のiOSカンファレンス参加となります。今回はなんとスポンサーとして参加し、自社ブースを出展しました! KINTOブース 私たちのブースはKINTOブルーを基調としたデザインで、着用する法被もブランドカラーに合わせたものでした! ブースでは、参加者が実際に使われているプロジェクトコードを読み解き、対応する設問番号をコードにスタンプしていくコーディングチャレンジを開催しました。 またノベルティとして、KINTOのマスコットステッカーや、iOSデバイスを模した台紙を来場者に配布し、ステッカーで自由に台紙を飾りつけられるようにしました。また、コーディングチャレンジの参加者には、リワードとしてエコバッグまたはマルチチェーンのいずれかをお渡ししました。 このイベントは、参加者と交流し、KINTOテクノロジーズについての意見を伺い、プロジェクトに対する貴重なフィードバックを集める絶好の機会となりました。会話を通じて、当社の製品がどのように認識されているかについて新たな気づきを得ることができ、今後の改善に役立つ有益な指針を得られました。 他社ブース 他の企業のブースも非常に興味深く、教育的なものが多くありました。特に目立っていたブースをいくつか紹介します。 Sansan のブースでは、自社のテクノロジースタックが展示されており、来場者がさまざまなツールやフレームワークに触れてその反応を確かめられる内容だったため、興味深いものでした。 DeNA DeNAのブースでは、コードを読んだり地図を見たりしてクロスワードパズルを解くという楽しいアクティビティが用意されており、非常に印象的でした。 Bitkey Bitkeyのブースでは、ビーコンアプリを実装するためにテストデバイスとMacBookが必要でしたが、テストアプリを開発してビーコンを持っている人を見つけるという体験はとても楽しいものでした。 Glassfiber Glassfiberのブースでは、クイズを開催し、多くの来場者の興味を引き付けていて、楽しくてためになる内容でした。 プレゼンテーション いくつかのセッションに参加しましたが、特に印象的だった2つを紹介します。 StoreKit 2によるモダンなアプリ内課金 まず、StoreKit 2の最新の使用法についてのセッションです。これまでStoreKitを使った経験がなかったため、とても参考になる内容でした。このセッションでは、StoreKit 2の導入、実装、テストについて説明され、StoreKit 1との詳細な比較も行われました。プレゼンテーションでは以下の重要なポイントが取り上げられました:・async/awaitを使用した非同期処理の簡素化 ・レシート検証の効率化 ・サンドボックス環境、TestFlight、StoreKitTestを用いたテスト手法 アプリ内課金を統合しようとしている人にとって非常に有益な情報でした。特に興味深かった点として、StoreKitが「顧客が支払いをしたのに購入したアイテムを受け取れない」というケースを直接サポートしていないことが挙げられます。この点には驚かされました。 StoreKit 2によるモダンなアプリ内課金 このセッションでは、StoreKit 2の実装とテストについて説明され、アプリ内課金の改善点とその簡素化に焦点を当てていました GPSでどのようにして現在地が分かるのか もう一つ興味深かったセッションは、モバイル端末が信号を受信し、位置を計算する仕組みについてのものでした。セッションでは、Core LocationがGPS、Wi-Fi、携帯電話の信号をどのように組み合わせて正確な位置を特定するかについても説明されました。基本的なGPSの原理である衛星との三角測量が紹介されただけではなく、非常に遠い、弱い信号を受信する際の複雑なエンジニアリングについても詳しく触れられていました。また、スマートフォンがネットワークデータを活用して迅速かつ正確に位置情報を取得する方法についても解説されました。私はこの分野についてほとんど知らなかったため、とても啓発的で多くの学びが得られるセッションでした。 GPSでどのようにして現在地が分かるのか この動画では、GPSとネットワークデータを活用して、スマートフォンがどのようにして迅速かつ正確に位置を特定するのかについて詳しく説明されていました。 結論 全体として、iOSDC 2024は素晴らしい経験でした。洞察に満ちたセッションから多くを学ぶ機会であると同時に、より広範なiOS開発者コミュニティと交流する貴重な場でもありました。KINTOのブースを主催することで、多くの才能ある方々と直接交流し、彼らのフィードバックを聞きながら、自分たちの仕事を有意義な形で紹介できたことは、とても意義深いものでした。また、私が参加したプレゼンテーション、特にStoreKit 2とGPS技術に関するセッションは、KINTOで現在進行中のプロジェクトに直結する、実用的で有益な情報を得ることができました。StoreKit 2のasync/awaitの改善により、アプリ内課金の実装が大幅に合理化され、プロセスがより効率的でユーザーフレンドリーなものになると確信しています。同様に、GPSやネットワーク三角測量の高度な利用方法は、アプリ内の位置情報サービスをさらに強化し、ユーザーにとってより正確で迅速な結果を提供できるようになるでしょう。これらの学びを開発プロセスに統合し、開発者として成長を続けながら、プロジェクトの改善にも活かしていきたいと考えています。最後までお読みいただきありがとうございました!
はじめに こんにちは、9月入社のkhです! 本記事では2024年8、9月入社のみなさまに、入社直後の感想をお伺いし、まとめてみました。 KINTOテクノロジーズに興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! Naito 自己紹介 Engineering Officeに所属しています。プロダクト横断のプロセス改善を担当しています。 所属チームの体制は? Engineering Officeは1月にできたばかりで、2人です。 KTCへ入社したときの第一印象?ギャップはあった? 入社前の面談等で色々お話うかがっていたので特にギャップはなかったです。 拠点が東京・名古屋・大阪にあるのですが行き来が活発で、配属初日に名古屋所属の人たち複数にご挨拶して、名古屋って近いんだな〜とおもいました。 社内でいろんな方とお話するようになって感じたのは、「カイゼン」が日常で行われていること みなさんトヨタ生産方式(TPS)を知っていて、ちょっと感動しました。 現場の雰囲気はどんな感じ? いろんな開発チームに話をきいたり、相談しにいくのですがみなさん本当に親切ですし、巻き込まれてくれて日々感謝です! ブログを書くことになってどう思った? 入社前からテックブログは見ていました。書く側にまわれてうれしいです。 Mariさんからの質問:この会社を選んだ理由はなんでしょうか? 私自身、日常生活で車に親しんでいるので関心があるドメインであることと、今後ドンドン変化していきそうな会社だなとおもっていてその変化を体感し、楽しみたいとおもいました。 R.Y. 自己紹介 香港出身、2023年来日、学校を卒業して、今年8月KTCに入社し、現在セキュリティプライバシーGに所属しております。 所属チームの体制は? セキュリティプライバシーGに業務によってチームを分けられます。私はディフェンスチームに所属し、主に脆弱性診断とセキュリティ相談チケットを対応いたします。 KTCへ入社したときの第一印象?ギャップはあった? オフィスが広いです。フリースペースがたくさんあってみんな自由に使えます。勉強会と交流会が充実していて、業務範囲外の知識も身に付けられます。 現場の雰囲気はどんな感じ? みんな親しみがあって、話しかけやすいと思います。Slackで業務以外に、趣味や地域のチャンネルもたくさんあって、同じ趣味を持っている方と交流しやすい環境です。 ブログを書くことになってどう思った? 入社三ヶ月以来経験したことを見返して、入社予定のみんなに共有できて嬉しいです。 Naitoさんからの質問:ご出身地(香港)に旅行で行ってみたいです。オススメの観光地やお食事があったら教えて下さい! ディズニーのファンであれば、ぜひ香港ディズニーランドに行ってみてください。コンパクトなので1日で回りやすいパークです。 食事としては、B級グルメがたくさんあります。例えば、弾力のあるプリプリとした食感のカレーフィッシュボール、魚肉から作ったシュウマイ、紅茶の味が濃い香港スタイルミルクティーなどがおすすめです。香港では食べ歩きが普通ですので遠慮なくこういう文化を体験してください。 (ちなみに日本でも香港流行っている食事が食べられますので、香港に興味がある方ぜひご覧ください 譚仔三哥(タムジャイサムゴー)公式サイト ) tHyt 自己紹介 モビリティプロダクト開発部に所属するtHytです。一部エンジニアリードをやりつつ、主にフロントエンドエンジニアとして日々仕事をしております。 所属チームの体制は? チームとしては9名在籍しており、フロント・バック・インフラと各方面のエキスパートが集まっています。 KTCへ入社したときの第一印象?ギャップはあった? まだ若い会社ということもあり、制度として整っていない部分も多いのかと思いきやしっかり整備されていたのでびっくりした記憶があります。 現場の雰囲気はどんな感じ? Osaka Tech Labにて勤務していますが、メンバーがそこまで多くないためチーム問わず会話が発生していて飽きません。チームのメンバーは自分を除いて室町オフィスで勤務していますが、日々のミーティングや出張などを通して楽しく仕事できています。 ブログを書くことになってどう思った? 入社前から入社エントリーの存在は知っており、ついに自分が書く日が来たか…!という気持ちです。自己紹介のみならずテックブログでアウトプットをしていきたいなと思います。 R.Y.さんからの質問:入社後、慣れてなかったことがありますか?どうやって乗り越えましたか? 圧倒的に出張ですね…。自分のチームで大阪に勤務しているのは自分だけとなるため、今はコミュニケーション促進のために月1程度の頻度で東京へ出張しています。(出張を許可していただけるのは非常にありがたい) KTCに入るまで出張は1回しかしたことがなかったですし、新幹線に普段乗らないので最初は不安だらけでした。 今では慣れてきたのもあり、出張については特に不自由なくできるようになりました。 リモートでもコミュニケーションは取れるのですが、対面で行うコミュニケーションはまた一味違うので継続していきたいなと思っています。 kh ![alt text](/assets/blog/authors/kh/kh.png =250x) 自己紹介 分析Gに所属しています。分析するためのデータの収集、基盤を構築しています。 所属チームの体制は? リーダーと自分含めて10名になります(協力会社さん、派遣さん含む) KTCへ入社したときの第一印象?ギャップはあった? 新しめな会社なので、ベンチャー気質と思っていましたが、程よいゆるさを感じました。 現場の雰囲気はどんな感じ? 様々なバックボーンを持った集まりで、それぞれの異なる良さを活かし、和気藹々としています。 ブログを書くことになってどう思った? 元々TechBlogで積極的に発信していることを認識していたので、その第一歩を踏み出したのかなと思いました。 tHytさんからの質問:データ分析のコツみたいなのがあれば教えてください! 自分はデータ分析をすると言うより、データ分析をするための基盤を作ることがメインなので、ほしい回答とは異なるかもしれません。いかにすごいデータ分析を行なっても、使用するデータの品質が悪いと全く役に立つものにはなりません。分析基盤からデータの品質を確保することがデータ分析のコツの第一歩と思います。 woshahua 自己紹介 Woven payment solution開発Gに所属してる高です!中国出身ですが、関西弁が根付いてるエンジニアです。趣味はバスケ、サウナとポーカー、最近はプライベートでポーカーの勝率分析のアプリを作ってます! 所属チームの体制は? エンジニアメインの体制です、Woven側のエンジニアとも一緒に仕事させていただいてます。所属組織には関わらず目指す目標に対してチーム一丸で走れているのがとても良いです。 KTCへ入社したときの第一印象?ギャップはあった? 車領域ではまだ新しい会社ですが、幅広く色々なサービスや開発に挑戦してる印象でした。 ギャップはほぼなかったですが、入社後はより会社が社員の技術発信を応援してる点やほぼ毎週何らか社内勉強会が実施されてることに驚きました。 現場の雰囲気はどんな感じ? 技術経験に縛られず、広い技術範囲で色々挑戦できる環境だと思います。 困ったことがある際はすぐチームメンバーが助けてくれるので、とても恩恵を受けてます。 ブログを書くことになってどう思った? 会社としても、チームとしても積極的に技術発信を応援してると思うので、積極的に色々発信していきたいです! khさんからの質問:ペットを飼われていると伺いましたが、ペットとの生活で日常や仕事に影響を与えるものはありますでしょうか? 家でモルモットを飼っているんですが、仕事終わりにとても癒されています。在宅勤務もできるので、ペットと一緒に過ごす時間が増えて嬉しいです。仕事する上で心理的な余裕は良いパフォーマンスにも貢献できると思うので、自分に対してペットは仕事の緊張を緩めて心理的な余裕を与えてくれていると思ってます! Mari 自己紹介 開発支援部人事G採用Tに所属しています。エンジニアを中心に採用活動をしています。 所属チームの体制は? 私含め、6名です(1月からは全員室町オフィスにいます!) KTCへ入社したときの第一印象?ギャップはあった? オフィスが素敵!エンジニアの方々がみんなコミュニケーションとりやすく明るく楽しいと思いました! 現場の雰囲気はどんな感じ? みんな優しくて裏表がなく楽しい雰囲気です!真面目で、周りの人に対して、いつも気を配っている人しかいません。 ブログを書くことになってどう思った? 何を書こうか悩みましたが率直な自分の今の気持ちを書きました!みんなのブログも参考にしました! woshahuaさんからの質問:仕事中や休日に行うリフレッシュ方法はありますか? 仕事中の時はリフレッシュするために散歩に行ったりします。休日はなるべく外出したりジムに行ったりして体を動かすようにしています。 C4 ![alt text](/assets/blog/authors/kh/20250123/c4.jpg =250x) 自己紹介 KTC所属ですがKINTO管理部へ出向しており、主にオフィス総務を担当しています。 MBTI診断がISFJ-Tだったので管理部ってなかなか向いているのではないかと勝手に思っています。 所属チームの体制は? 所属チームで名古屋に勤務しているのは私を含めて8名です。 KTCへ入社したときの第一印象?ギャップはあった? 凄くお洒落なオフィスだなという印象でした。 オフィス勤務は初めてで、もっと堅苦しいイメージをしていたんですがチームの雰囲気は柔らかくフレンドリーですし勤務外でも食事に行ったりとコミュニケーションが盛んなのだなと感じます。 現場の雰囲気はどんな感じ? 自チームだけに限らず、まわりの皆さんがとにかく優しいと感じます。忙しそうな中で質問をしたりしても、嫌な顔をされた事がありません。 毎週1on1やチームMTGがあるので、チームメンバーがどんな動きをしているのか自分が関わっていなくても知る事が出来ますし自分の業務の困り事なども相談出来るタイミングを作っていただけている事がすごくありがたいです。 ブログを書くことになってどう思った? 正直な所、自分が受け持っている業務が事務・庶務系なのでエンジニアでない私の話をテックブログへ掲載して果たしてどなたかの参考になれるのだろうか…と考えたりしました(笑) Mariさんからの質問:私はMBTI診断はESFPでした!診断結果が当たりすぎていて怖いと思っていますw特に「細かいことが苦手」という部分が当たっていると思っています。。ISFJ-Tという診断は結構当たっていますか?特に当たっている部分はどんなところですか? 「変化に抵抗がある」 「完璧主義」 「自分に批判的」 の部分がグサッときました(笑) 人に喜んでもらえるなら自分のことは後!なマインドもかなりあるので結構当たっているなと私も感じました。 自己分析に適していると思うのでMBTI診断、皆さんにオススメです!! Mari 自己紹介 モビリティプロダクト開発部 DX企画推進G DX Planningチーム に所属のUI / UXデザイナーです。販売店様の業務DX化をサポートするプロダクトのデザインを行なっています。 所属チームの体制は? チームリーダー1名に、ディレクター3名、デザイナー4名の体制です。 KTCへ入社したときの第一印象?ギャップはあった? 母体が大きな会社なので仕組みやルールはしっかりしている一方で、新しい会社でもあるので雰囲気は自由です。ギャップは特になかったです。 現場の雰囲気はどんな感じ? タスクは多めで忙しいですが、その分やりがいがあり、活気にあふれた充実した雰囲気です。人間関係の雰囲気はとても良く、和気あいあいとしています。 ブログを書くことになってどう思った? 何を書こうか悩みましたが、素直な感想を書きました!他の方のブログも読むのが楽しみです。 C4さんからの質問:ご自宅にお猫がおられるとの事!我が家も猫×5と同居していますので今まで購入されたお猫様グッズの中で1番反応が良かったオススメを教えて下さい。(おやつでもおもちゃでもなんでもOKです!) 5匹 !! よいですね〜 🥰 うちの子は女王様気質で何を買っても「そんな子供だましのもの、興味ないんだけど」って態度であしらわれるんですが、やはりちゅーるは麻薬みたいで、特に高級食材が使われてるちょっと高いやつだと満足そうです😂 あと、本にゃんのご意向はわかりませんが、mirutoという自動猫トイレを購入して、掃除も楽だしかなり役に立っております !! ちなみに購入したキャットタワーは1度も乗る事なく1ヶ月以上経過し、泣く泣くそのまま粗大ゴミになりました….😇 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTO テクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTO テクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
目次 はじめに この記事の対象者 GitHubリポジトリ スケルトンバッチの解説 JobとStepの基礎 コード解説JOBクラス 実際にバッチを動かしてみる DBとCSVのバッチ解説 概要 ローカルDBのセットアップ マルチデータベース設定 Repositoryクラスとjooqの解説 DBからCSVバッチ解説 CSVからDBバッチ解説 継続的インテグレーション 終わりに はじめに こんにちは。KINTOテクノロジーズの共通サービス開発グループ[^1][^2][^3][^4][^5]のエンジニア、宮下です。 今回、Spring Boot 3を使ったバッチ処理の開発を担当しました。久しぶりのバッチ作成で、基礎を思い出しながら作業を進める中で、Spring Batchのクラス構成やアノテーションに苦労しました。 特に困ったのは、Spring Boot 3に対応した日本語情報が少なかったことです。公式ドキュメントを参考にしながら試行錯誤しましたが、自分の要件に合う設定を見つけられず、マルチデータベース構成やエラー対応でつまずくこともありました。その結果、動かしては修正するという悪循環に陥り、1からバッチを作り上げる大変さを痛感しました。 この記事では、その経験を活かして同じ悩みを抱える方がスムーズにSpring Batchを導入できるように、以下の内容をまとめました。 Spring Boot3 バッチのスケルトン GitHubからクローンして実行するだけで、すぐに動くバッチが手に入ります。あとは、業務ロジックを追加するだけで、バッチ開発が完了します。余計な設定や準備に悩む必要はありません。 バッチ処理によくあるユースケースの実装例と解説 スケルトンとは別に、以下のマルチデータベース構成を活用したバッチ処理のサンプルコードも紹介します。 1. DBのレコードをCSV出力するバッチ 2. CSVの内容をDBに登録するバッチ この記事を通じて、Spring Batchの導入や開発が少しでも簡単になれば幸いです。 この記事の対象者 以下のような方々を対象にしています: 初めてSpring Batch 5 のフレームワークでバッチを開発をする方。 Spring Batchに触れるのが久しぶりで、ジョブやステップなどの基本的な実装方法を思い出す必要がある方。 Spring Boot2 Batchのサポート終了で、3へのバージョンアップに伴う新しい構文や設定変更で困っている方。 本当はJavaのmainメソッドで動くシンプルなバッチが好きだが、Springバッチで作る必要が出てきた方。(私もそうです) とにかく手早くバッチの作成タスクを終わらせたい方。 これらに当てはまる方の参考になればと思います。 githubリポジトリ https://github.com/kinto-technologies/SpringBoot3BatchStarter/ こちらが、私のリポジトリです。クローンとお使いのIDEへの取り込みをお願いします。 GUIでクローンしたい方は、 GitHub Desktop の公式アプリを使うと、簡単にリポジトリをクローンできます。 リポジトリのディレクトリ構成 リポジトリは以下のように構成されています。 . ├── gradlew # Gradleラッパー ├── settings.gradle ├── compose.yaml # Docker Compose設定ファイル ├── init-scripts # データベース初期化用SQLスクリプト │ ├── 1-create-table.sql │ └── 2-insert-data.sql ├── dbAndCsvBatch # “DB to CSV” と “CSV to DB” のバッチプロジェクト │ ├── build.gradle │ └── src │ ├── main │ └── test └── skeletonBatch # スケルトンバッチプロジェクト ├── build.gradle └── src ├── main └── test スケルトンバッチの解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/tree/main/skeletonBatch 業務ロジックを追加するだけでバッチが即完成する、必要なコードのみで構成したスケルトンコードです。 JobとStepの基礎 スケルトンに業務ロジックを追加して動かすだけでも良いですが、フレームワークの基礎を理解しておくと、より安心して使えますよね。ここでは、Springバッチの核となる Job と Step について、手短に解説します。 Jobについて Jobとは、バッチ処理の全体を管理する単位で、1つまたは複数のStepを含みます。これにより、バッチ処理を効率的に実行・管理できます。Spring Batchでは、複数のJobを定義することができ、各Jobを個別に実行したり、連携させて実行することも可能です。 graph TD A[Job] --> B[Step 1] A --> C[Step 2] A --> D[Step 3] style A fill:#f9f,stroke:#333,stroke-width:4px Jobでできること 機能 説明 ユースケース 連携実行 あるジョブの成功後に次のジョブを実行 日次バッチの順次実行 失敗時の実行 ジョブ失敗時にリカバリー処理を実行 エラーログの出力、通知送信 並行実行 複数ジョブの同時実行 独立した処理の効率化 並列実行 同一ジョブの複数インスタンス実行 大量データの分散処理 Step(ステップ)の基礎 Stepとは? ジョブ内の各処理単位を表します。1つのジョブには複数のステップを登録でき、ステップの動作もジョブと同様に柔軟に設計可能です。 実装方式の選択 - ここがポイント! Stepの実装には Chunk と Tasklet の2つの方式があります。 1.チャンク(Chunk)処理 大量データを効率的に処理するための方式です。 graph LR A[Reader] -->|100件ずつ| B[Processor] B -->|1件ずつ| C[Writer] C -->|100件まとめて| D[DB/File] 特徴 3つのフェーズで処理を分割 Reader: データを一定量ずつ読み込み 例: FlatFileItemReaderで、CSVファイルを100行単位で読取 Processor: データを1件ずつ加工 例: CustomProcessorで、日付フォーマットの変換 Writer: まとめて出力 例: JdbcBatchItemWriterで、DBへの一括INSERT トランザクションの単位は、チャンクサイズ(例:100件)ごとにコミットされます。つまり、途中でエラーになっても、正常終了したチャンクサイズ分はコミットされます。 2.タスクレット(Tasklet) 処理 シンプルな処理を1つ実行する方式です。 特徴 シンプルな実装 分かりやすい処理フロー デバッグが容易 実装方式の使い分け 観点 Chunk Tasklet データ量 大量に適する 少量に適する 処理の複雑さ 複雑な処理に適する シンプルな処理に適する 管理コスト 高め 低め デバッグ やや複雑 容易 トランザクション チャンク単位 処理単位 まとめ Jobはバッチ処理全体の管理単位 Stepは具体的な処理の実行単位 Chunkは大量データの効率的な処理に適する Taskletはシンプルな処理に最適 処理内容に応じてChunkとTaskletを適切に使い分けることが重要 :::message ポイント 複雑な処理が不要な単純なバッチでは、迷わずTaskletを選択しましょう。 今回提供するスケルトンバッチでも、もちろんTaskletを採用しています。 ::: :::message alert 注意 公式ドキュメントを初め、多くのWeb記事ではチャンク処理がスプリングバッチの基本として詳しく説明されているため、初心者の方は「チャンクで作らないといけない」と誤解しがちです。そのため、チャンクで実装を進めた後に「やっぱりタスクレットで作ればよかった」となり、手戻りが発生するケースが多いです。 ::: YAML設定とSpringバッチの管理テーブル まず、Spring Batchをバッチ専用モードで動かす基本的な設定について説明します。 YAML設定 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/resources/application.yml この設定により、Spring Bootアプリケーションはサーバーレスモードで動作します。 通常のSpring BootアプリケーションではTomcatなどのサーバーを起動してプロセスを維持しますが、バッチ処理ではそれが不要です。この設定により、バッチ処理が完了するとプロセスが終了します。 Spring Batch 管理テーブル Springバッチでは、実行結果を記録するための管理テーブルをDB上に作成する必要があります。でも、DBやテーブルの管理ってちょっと面倒ですよね。そもそもDBが無い環境で動かすこともあるはずです。その場合は、H2のオンメモリDBを使うのがおすすめです。プロセスが終わるとメモリが開放されてDBとテーブルが無くなります application.ymlにDB設定を書かなければ、この設定が自動で適用されます。 ただし、永続化のメリットとしては、実行結果を保持し、途中で中断した場合でも再開可能な状態を維持できる点が挙げられます。 コード解説JOBクラス https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/job/SampleJob.java#L16-L41 Spring BatchでJobを登録・実行するための中心的なクラスです。 クラス定義のポイント @Configuration Spring Batchの設定クラスとして認識されます このアノテーションにより、このクラスがSpringの設定を行うクラスだと認識されます Jobを定義するクラスには必須のアノテーションです @Bean Spring Frameworkで管理したいオブジェクトを生成するメソッドに付けます この場合、 createSampleJob メソッドが生成するJobをSpringが管理できるようになります バッチ処理実行時に、このメソッドで作られたJobが使用されます :::message alert 注意 メソッド名がデフォルトでBean名として使用されます。このクラスをコピーして新しいJobを作る場合、メソッド名が同じだとエラーになります。メソッド名が重複する場合は、@Bean("jobName") のように一意な名前を指定しましょう。意外なハマりポイントなので注意して下さい。 ::: 依存クラスの役割 JobRepository : ジョブの実行状態を管理します PlatformTransactionManager : データベースの整合性を保つために使用します SampleLogic : 実際の業務処理を行います 処理の流れ ジョブ登録開始のログを出力 Stepの作成(処理単位の定義) Jobの作成とStepの登録 ジョブ登録完了のログを出力 トランザクション管理 正常終了: すべての処理が成功すると、データベースの変更が確定されます 異常終了: エラーが発生すると、データベースの変更が取り消されます これにより、バッチ処理の信頼性が保証されます。 ポイント @ConfigurationアノテーションでSpring Batchの設定クラスとして認識されます JobBuilderFactoryとStepBuilderFactoryを使用してジョブとステップを構築します Taskletパターンを採用することで、シンプルな実装を実現しています Logicクラスの解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/logic/SampleLogic.java#L13-L35 このクラスは、バッチの実際の処理内容を定義するものです。 クラス定義のポイント @Component このクラスをSpringで管理できるようにします これにより、他のクラスで@Autowiredして使用できます Taskletインターフェース Spring Batchの処理単位を表すインターフェース executeメソッドに実際の処理を実装します 処理の流れ バッチ処理開始のログを出力 SampleServiceのprocessメソッドを呼び出し 正常終了時は完了ログを出力 エラー発生時は例外をログ出力して再スロー バッチ処理終了のログを出力 エラー処理 正常終了: RepeatStatus.FINISHED を返してバッチを完了 異常終了: 例外をキャッチしてログを出力し、上位に例外を通知 :::message 実装のポイント このクラスでは処理をSampleServiceに委譲していますが、シンプルな処理の場合はexecuteメソッド内に直接実装しても問題ありません。 ::: Serviceクラスの解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/service/SampleService.java#L8-L23 このクラスは、実際の業務ロジックを記述するためのクラスです。 クラス定義のポイント @Service Spring Frameworkのサービスクラスとして認識されます ビジネスロジックを実装するクラスであることを示します processメソッド バッチで実行したい業務処理を実装するメソッド 処理開始と完了のログ出力を含みます :::message 実装のポイント このクラスの特徴は、Spring Batchのクラスやインターフェースから完全に独立しています。そのため 純粋な業務ロジックの実装に集中できる JUnitテストが書きやすい 他のプロジェクトでも再利用が容易 ::: カスタマイズ方法 以下のような変更が可能です: クラス名の変更 メソッド名の変更 引数や戻り値の追加 業務ロジックの実装(例:データ検証、変換処理、外部システム連携) これで、スケルトンバッチの主要なクラスの解説は終わりです。このように疎結合な設計にすることで、メンテナンスしやすく、テストも容易なバッチを実現できます。 実際にバッチを動かしてみる IDEから起動する場合 BatchApp クラスを起動してください。通常の Spring Boot アプリケーションと同じ要領で動作します。 ターミナルからGradle経由で起動する場合 以下のコマンドを実行してください。 ./gradlew :skeletonBatch:bootRun ターミナルから実行可能JARファイルを生成して実行する場合 Gradle のデフォルトタスクを実行すると、JAR ファイルが生成されるように設定済みです。以下の手順で実行可能です。 cd skeletonBatch ../gradlew java -jar build/libs/batch-skeleton*.jar ログから実際の動きを確認 Spring Batchの実行フローを、出力されるログから順番に確認していきましょう。 1. JOB登録の確認 ----------- Registering job: sample ----------- ----------- Job registered successfully: sample-job ----------- 解説 Spring Boot起動時に、sample-jobというバッチジョブが正常に登録されました。 2. バッチ処理の実行 Started BatchApp in 0.456 seconds (process running for 0.616) Running default command line with: [] Job: [SimpleJob: [name=sample-job]] launched with the following parameters: [{}] Executing step: [processing-step] ----------- START ----------- Batch Processing ----------- --- Starting batch process --- --- Batch process completed --- Processing completed successfully ----------- END ----------- Batch Processing ----------- Step: [processing-step] executed in 3ms Job: [SimpleJob: [name=sample-job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 9ms 解説 ログから確認できる重要なポイント: sample-jobが実行され、processing-stepというステップが開始 開始(START)と終了(END)のログにより、処理の境界が明確に解ります。 ステップが正常に完了し、ジョブ全体がCOMPLETEDステータスで終了 アプリケーションが正常に終了 スケルトンバッチの解説まとめ 業務ロジックを追加するだけでバッチが完成 生成されたJARファイルはcronなどのスケジューラと連携可能 シンプルな構成で保守性が高い :::message Spring Batchの開発では、アノテーションの付け忘れや設定ミスが起きやすく、エラーメッセージの解読に時間を取られがちです。このスケルトンを使えば、そうした問題を避けて業務ロジックの実装に集中できます。 ::: このスケルトンコードが、皆様のバッチ開発の効率化に貢献できれば幸いです。 DBとCSVのバッチ解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/tree/main/dbAndCsvBatch こちらのディレクトリです。 概要 このプロジェクトには2つのバッチ処理が含まれています: DBからCSVを出力 データベースからレコードを抽出してCSV形式で出力 起動時引数でデータの抽出条件を変更可能 デフォルト設定でもすぐに動作可能 CSVからDBへ登録 CSV形式のデータを読み取ってデータベースに一括登録 実行するにはCSVが必要なため、まずは上記の出力バッチを実行してください ローカルDBのセットアップ このバッチでは、MySQLのローカルDBを使用します。以下の手順でセットアップしてください。 MySQLコンテナの起動 docker compose up -d :::message Dockerが未インストールの場合は、 Dockerの公式サイト からインストールしてください。 ::: 動作確認 MySQLコンテナに接続してサンプルデータを確認します: docker exec -it mysql-container mysql -u sampleuser -psamplepassword sampledb サンプルクエリの実行: mysql> SELECT * FROM member WHERE delete_flag = 0 AND type IN (1, 2, 3) ORDER BY type ASC; mysql> exit Bye Entityクラスの自動生成 GitHubリポジトリをクローンした直後、Entityクラスが見つからないエラーが発生する場合があります。これはjOOQによるEntityクラスの自動生成が必要なためです。 以下のどちらのコマンドでもEntityクラスが生成されます。 自動生成の実行 # デフォルトタスクの実行 cd dbAndCsvBatch/ ../gradlew # または、generatejOOQタスクを直接実行 ../gradlew generateJooq build.gradleは追加の設定を行わなくても利用できるように工夫しています。デフォルトタスクを実行すると、以下の処理が動くように設定済みです。 ビルド結果のクリーンアップ Javaコードの整形(Google Java Format) Entityクラスの自動生成(jOOQプラグイン) コンパイル 静的解析とテスト(JUnit、カバレッジ、SpotBugs) 実行可能JARの生成 生成されたEntityクラス 以下のコマンドで生成されたEntityクラスを確認できます。 tree dbAndCsvBatch/build/generated-src #treeコマンドをインストールしていない場合はこちら ls -R dbAndCsvBatch/build/generated-src dbAndCsvBatch/build/generated-src/jooq └── main └── com └── example └── batch └── jooq ├── DefaultCatalog.java ├── Keys.java ├── Sampledb.java ├── Tables.java └── tables ├── Member.java └── records └── MemberRecord.java :::message alert jOOQプラグインの自動生成コードは、ビルド時にデータベーススキーマから再生成されるため、Gitで管理すると、スキーマの変更時に差分やコンフリクトが発生しやすくなります。そのため、生成されたコードはGitで管理しないことが推奨されます。 ::: クラスが見つからないエラー発生時は お使いのIDEによっては、Entityクラスが見つからないエラーが発生するかもしれません。 その場合は以下の方法でコンパイルエラーを解消して下さい。 IDEのビルドパスにgenerated-src/jooqを追加 generated-src/jooq配下のクラスをdbAndCsvBatch/src/main/javaにコピー これらの対応でコンパイルエラーが解消され、プロジェクトが正常にビルドされるようになります。 業務環境でのエンティティ自動生成方法 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/build.gradle#L101-L124 :::message 皆さんの開発環境のDEV, STGのDB接続設定に変更してEntityクラスを生成 または、Docker Composeの初期化SQLに本番のCREATE TABLE文を追加し、コンテナを再作成する。 ::: jOOQ gradle plugin website バッチの解説 長くなってきているので、スケルトンで解説した内容と、Springバッチフレームワークに直接関係ないCSV操作のコード解説は省略し、新しいバッチの設定についてのみ解説したいと思います。 マルチデータベース設定 このバッチでは、2つのデータベースを使用するマルチデータベース構成を採用しています。 1. H2 (オンメモリデータベース) Spring Batchの管理用テーブルとして利用。オンメモリDBなのでプロセス終了時にリセットされます。 MySQL (業務ロジック用データベース) 業務ロジックで使用。Dockerコンテナ上で動作します。 設定ファイル (application-local.yml) Spring Bootの設定ファイルでは、spring.datasource 配下に2つのデータベースを設定します。 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/resources/application-local.yml 設定ファイル(yml)には2つのDBが設定されています。また、ファイル名の末尾に local と server のプロファイルを付けています。実行時引数で使用する設定ファイルを切り替えられるようになっています。server 用は、Dockerではなく、皆さんが業務で使用する MySQLサーバーの接続文字列に書き換える想定です。 データソース設定クラスの解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/config/DataSourceConfig.java#L11-L37 2つのデータベースをSpring Batchで使用するための設定クラスです。 アノテーション解説 @Configuration Springの設定クラスであることを示す このクラスで定義したBeanはSpringが管理する @ConfigurationProperties application.ymlの設定値を取得する 例: spring.datasource.mysqlmain の設定を読み込み @BatchDataSource Spring Batchの管理テーブル用のデータソースを指定 H2データベースに対して使用 @Primary 優先して使用するデータソースを指定 MySQLデータソースをデフォルトとして使用 Jobクラス解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/job/DbToCsvJob.java#L31-L39 新機能の追加 実行番号の自動採番 .incrementer(new RunIdIncrementer()) ジョブの実行番号(run.id)を自動的にインクリメント 実行履歴の管理に使用 :::message このサンプルコードでは、管理テーブルをオンメモリDB(H2)に設定しているので、 意味はありませんが、永続的なDBを利用する場合の標準的な記述方法として紹介しています。 ::: リスナーの追加 .listener(listener) ジョブの開始前と終了後に処理を追加 ログ出力やメール通知などに利用可能 Logicクラス解説 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/logic/DbToCsvLogic.java#L18-L49 パラメータ設定 @Value("${batch.types:2,3,4}") private String typesConfig; application.ymlに設定がない場合は、2,3,4がデフォルト値として使用されます 実行時に--batch.types=1,2のように引数を渡すと、その値が優先されます エラーハンドリング contribution.setExitStatus(ExitStatus.FAILED); エラー発生時に明示的に失敗ステータスを設定 正常終了時は自動的にCOMPLETEDとなる :::message 同じくH2なので意味はありませんが、標準的な記述方法として紹介しています。 ::: RepositoryクラスとjOOQの解説 アノテーション解説 @Repository Springのコンテナによって自動的に管理されます。 SQLException などのデータベース固有の例外を Spring の DataAccessException に変換します。 このクラスがデータアクセスに関わるものであることを明示的に示します。これにより、コードの可読性と保守性が向上します。 jOOQとは jOOQ は、SQLをJavaコードとして記述できるライブラリです。 SQLの構文をそのままJavaコードで表現 テーブル定義からEntityクラスを自動生成 タイプセーフなSQL操作を実現 Select処理 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/repository/MemberRepository.java#L18-L25 主な特徴 SQLライクな記述 SQL文をJavaコードで直接表現 SQLの知識がそのまま活用可能 自動生成クラス解説 MEMBER : テーブル定義を表現(カラム名など) MemberRecord : レコードのマッピング用 バルク(一括)Insert処理 https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/repository/MemberRepository.java#L54-L69 バルクInsert 1レコードずつinsertするのではなく、複数のデータを一括で登録します。 バッチ処理では、DB負荷軽減のため複数レコードをまとめて処理するのが一般的です。 :::message このバッチでは使いませんが、insert, update, deleteメソッドのコード例も載せています。 エンティティを職場環境に合わせて作成した場合は、テーブル名とカラム名を置き換えるだけで、すぐにリポジトリクラスとして活用することができます。 ::: Repositoryクラスのまとめ jOOQのGradleプラグインを使用することで、データベーススキーマからエンティティを自動生成して使用します。 自動生成されたクラスを使用することで、型安全性を確保しつつ、SQL構文を直感的にJavaで記述できます。 テーブル定義の変更があった場合でも、クラスを再生成するだけで対応可能です。 :::message Spring Batchでは、複数のデータベース操作方法が利用可能です。jOOQの他に代表的な選択肢として以下があります。 Hibernate (JPA) 強力なデータベース抽象化 Spring Bootとの統合が簡単 大規模データセットでオーバーヘッドが発生する可能性がある MyBatis 直接的なSQL管理に最適 動的クエリのサポートが強力 保守性が低下する可能性がある Spring JdbcTemplate 軽量なデータベース操作 シンプルなクエリに適している 記述が冗長になることがある ::: :::message alert 注意点 プロジェクトの要件やチームのスキルセットに応じて適切な方法を選択してください。必要であれば、ORMを使わずに直接SQLを実行する方法も検討可能です。 ::: DBからCSVバッチ解説 DBからCSVバッチを起動します。起動方法はスケルトンバッチと同じ要領です。 ターミナルからGradle経由で起動する場合 ./gradlew :dbAndCsvBatch:bootRun ターミナルから実行可能JARファイルを生成して実行する場合 cd dbAndCsvBatch/ ../gradlew java -jar build/libs/batch-dbAndCsv*.jar ここでは、スケルトンバッチの起動方法と同じく、起動引数を付けずにAppクラスを実行しました。 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jobLauncherApplicationRunner' defined in class path resource [org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.class]: Job name must be specified in case of multiple jobs at com.example.batch.DbAndCsvBatchApp.main(DbAndCsvBatchApp.java:16) Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs エラーが発生しました。 なぜエラーが発生したのか? エラーの原因は、複数のジョブが定義されているため、フレームワークがどのジョブを実行するか判断できなかったことです。そのため、実行するジョブを明示的に指定する必要があります。このプロジェクトでは2つのジョブが定義されています。一方、スケルトンバッチにはジョブが1つしか定義されていないため、このエラーは発生しません。 起動引数を指定して実行する エラーを解消するには、実行時にジョブ名と環境名を起動引数として指定します。 IDEで実行する場合は、実行時引数設定に次の内容を指定してください。 --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local --spring.batch.job.name=DB_TO_CSV: 実行するジョブ名を指定します。 --spring.profiles.active=local: ローカル環境用の設定プロファイルを有効にします。 実行コマンドの例 # Gradleを使用する場合 ./gradlew :dbAndCsvBatch:bootRun --args="--spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local" # 実行可能JARを直接使用する場合 cd dbAndCsvBatch/ java -jar build/libs/batch-dbAndCsv*.jar --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local 無事起動したと思うので、ログの内容を確認します。 ログ出力例 ##### KEY:"sun.java.command", VALUE:"com.example.batch.DbAndCsvBatchApp --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local" ##### Spring Batch ##### - Job: DB_TO_CSV, Profile: local 起動引数をログ出力するコードを App クラスに仕込んでいます。 ログから、ジョブ名とプロファイル名が正しく渡されていることが確認できました。 ログ出力例 ----------- JOB [Job Name:DB_TO_CSV] START! ----------- Executing step: [DB_TO_CSV-step] Fetching members with types = [1, 2, 3] リスナー (BatchNotificationListener) の beforeJob メソッドのログが出力されています。 また、typesに設定ファイルの値が設定されたことが確認できました。 ログ出力例 -> with bind values : select `sampledb`.`member`.`id`, `sampledb`.`member`.`type`, `sampledb`.`member`.`name`, `sampledb`.`member`.`email`, `sampledb`.`member`.`phone`, `sampledb`.`member`.`address`, `sampledb`.`member`.`delete_flag`, `sampledb`.`member`.`created_at`, `sampledb`.`member`.`updated_at` from `sampledb`.`member` where (`sampledb`.`member`.`delete_flag` = 0 and `sampledb`.`member`.`type` in (1, 2, 3)) order by `sampledb`.`member`.`type` Version : Database version is supported by dialect MYSQL: 9.1.0 Fetched result : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ : | id|type|name |email |phone |address |delete_flag|created_at |updated_at | : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ : | 1| 1|John Doe |john.doe@example.com |1234567890|123 Main St, City, Country | 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : | 2| 1|Jane Smith|jane.smith@example.com|0987654321|456 Oak St, Town, Country | 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : | 26| 1|John Doe |john.doe@example.com |1234567890|123 Main St, City, Country | 0|2024-12-09T05:36:37|2024-12-09T05:36:37| : | 27| 1|Jane Smith|jane.smith@example.com|0987654321|456 Oak St, Town, Country | 0|2024-12-09T05:36:37|2024-12-09T05:36:37| : | 3| 2|ABC Corp |contact@abccorp.com |5678901234|789 Pine St, Village, Country| 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ Fetched row(s) : 5 (or more) Batch process completed successfully. Step: [DB_TO_CSV-step] executed in 193ms Job: [SimpleJob: [name=DB_TO_CSV]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 212ms ----------- JOB [Job Name:DB_TO_CSV] FINISHED! status:[COMPLETED] ----------- https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/resources/logback.xml#L21-L22 logback.xmlの設定で、jOOQのクエリとその結果をログ出力するように設定しています。実際のクエリーと結果が見られるとデバックしやすいですよね。 また、リスナーの終了ログも出力されており、バッチが正常に終了したことが確認できました。 生成されたCSVファイルの確認 "id","type","name","email","phone","address","deleteFlag","createdAt","updatedAt" "1","1","John Doe","john.doe@example.com","1234567890","123 Main St, City, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "2","1","Jane Smith","jane.smith@example.com","0987654321","456 Oak St, Town, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "3","2","ABC Corp","contact@abccorp.com","5678901234","789 Pine St, Village, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "5","3","Alice Premium","alice.premium@example.com","4561237890","987 Maple St, City, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "6","3","Charlie Davis","charlie.davis@example.com","1112223333",,"0","2024-12-11T06:05:26","2024-12-11T06:05:26" CSVファイルが出力された事が確認できました。CSVへの出力と、読み込みにはopencsvのライブラリを使用しています。 opencsv website 実行時引数での types の上書き確認 最後に、このバッチには実行時引数にwhereのin句に設定するtypesを起動引数で渡せる機能がありました。試してみましょう。以下の起動引数を指定することで、where の in 条件に指定する types をカスタマイズできます。 --spring.batch.job.name=DB_TO_CSV --batch.types=4,5 --spring.profiles.active=local ログ出力例 -> with bind values : select `sampledb`.`member`.`id`, `sampledb`.`member`.`type`, `sampledb`.`member`.`name`, `sampledb`.`member`.`email`, `sampledb`.`member`.`phone`, `sampledb`.`member`.`address`, `sampledb`.`member`.`delete_flag`, `sampledb`.`member`.`created_at`, `sampledb`.`member`.`updated_at` from `sampledb`.`member` where (`sampledb`.`member`.`delete_flag` = 0 and `sampledb`.`member`.`type` in (4, 5)) order by `sampledb`.`member`.`type` 設定ファイルの値が、実行時引数で上書きされていることが解ります。 CSVからDBバッチ解説 バッチを起動します。実行時引数でジョブ名を指定して実行します。 --spring.batch.job.name=CSV_TO_DB --spring.profiles.active=local ログを確認します。 insert into `sampledb`.`member` (`name`, `email`, `phone`, `address`, `type`) values ('Premium Corp', 'premium@corporate.com', '8889997777', '555 High St, City, Country', 4), ('Elite Ltd', 'elite@elitecorp.com', '4445556666', '777 Sky Ave, Town, Country', 4), ('Guest User1', 'guest1@example.com', '', 'Guest Address 1, City, Country', 5), ('Guest User2', 'guest2@example.com', '9998887777', '', 5) Affected row(s) : 4 バルク (一括) でinsertされているのが確認できました。 継続的インテグレーション このプロジェクトでは、GitHub Actionsを使用してCI(継続的インテグレーション)を実現しています。コードの変更がプッシュまたはプルリクエストされるたびに、全ての処理が自動で行われ、品質管理が効率的に進められます。 主な流れ 1. MySQLのセットアップ: Docker Composeを使ってMySQLを起動し、必要なテーブルを確認。 2. JDK 21のセットアップ: Java 21をインストールしてビルド環境を整備。 3. jOOQでのクラス生成: データベーススキーマから自動でエンティティクラスを生成。 4. ビルドとテスト: Gradleでビルドを行い、JUnitやJacocoやSpotBugsを用いて品質チェックを実施。 コードの変更時、コンパイルエラーやJUnitテストエラーを即座に検出し、迅速に対応できます。 さらに、ビルドとテストが成功すると、GitHubリポジトリのREADMEに以下のようなバッジが表示され、プロジェクトの状態を一目で確認できます: コードカバレッジの可視化 このプロジェクトでは Codecov を使用してテストカバレッジを測定・可視化しています。プルリクエスト時にカバレッジレポートが自動生成され、以下のバッジでカバレッジ率を確認できます: これにより: テストの網羅性を視覚的に把握 カバレッジの変更を迅速に検知 品質管理の透明性を向上 カバレッジレポートの詳細はCodecovのダッシュボードで確認できます。 graph LR D[👩‍💻 Developer] -->|Push/PR| G[🐙 GitHub] G -->|Trigger| GHA[⚡ GitHub Actions] GHA -->|Build and Test Results| R[📄 README.md] GHA -->|Coverage Report| C[☁️ Codecov] C -->|Coverage Badge| R style G fill:#f4f4f4,stroke:#171515 style C fill:#f01f7a style R fill:#e6e6e6 終わりに いかがでしたでしょうか? このプロジェクトでは、スケルトンコードを基盤に、Spring Batchを活用したバッチ開発を効率よく進められるよう工夫しています。「DB to CSV」「CSV to DB」といったよくあるユースケースを取り上げ、DB設定やテーブル定義、CSVレイアウトを変更するだけで簡単にカスタマイズできる柔軟性を備えています。 このスケルトンを活用して、皆さんの業務に合わせたロジックを追加し、バッチ開発がスムーズになることを願っています。 この記事が参考になった、コピペでバッチが動いた!という方は、GitHubリポジトリに⭐を付けていただけると嬉しいです。 最後までお読みいただき、ありがとうございました🙇‍♂️ [^1]: 共通サービス開発グループメンバーによる投稿 1 [ グローバル展開も視野に入れた決済プラットフォームにドメイン駆動設計(DDD)を取り入れた ] [^2]: 共通サービス開発グループメンバーによる投稿 2 [ 入社 1 年未満メンバーだけのチームによる新システム開発をリモートモブプログラミングで成功させた話 ] [^3]: 共通サービス開発グループメンバーによる投稿 3 [ JIRA と GitHub Actions を活用した複数環境へのデプロイトレーサビリティ向上の取り組み ] [^4]: 共通サービス開発グループメンバーによる投稿 4 [ VSCode Dev Container を使った開発環境構築 ] [^5]: 共通サービス開発グループメンバーによる投稿 5 [ MinIOを用いたS3ローカル開発環境の構築ガイド(AWS SDK for Java 2.x) ]
Contents Introduction Target Audience Repository Setup Skeleton Batch Guide DB and CSV Batch Guide Continuous Integration Conclusion Introduction Hello, I'm Miyashita, an engineer from KINTO Technologies' Common Service Development Group[^1][^2][^3][^4][^5]. While developing batch processes with Spring Boot 3, I encountered several challenges with Spring Batch's class structure and annotations. The transition to Spring Boot 3 presented additional complexities, particularly with multi-database configurations and error handling. A particular challenge was the lack of Japanese documentation for Spring Boot 3. While referring to the official documentation, I had trouble finding configurations that matched my requirements and encountered issues with multi-database setup and error handling. This led to a cycle of trial and error, making me acutely aware of the challenges in building a batch process from scratch. In this article, I've compiled information to help others facing similar challenges implement Spring Batch more smoothly: Spring Boot 3 Batch Skeleton Immediate deployment through GitHub clone and run Ready for business logic implementation Pre-configured for production use Common Batch Processing Use Cases In addition to the skeleton, I'll introduce sample code for batch processing utilizing a multi-database configuration: Batch process for exporting DB records to CSV Batch process for importing CSV data to DB I hope this article helps make Spring Batch implementation and development easier. Target Audience This guide is designed for developers who are: ✓ New to Spring Batch 5 framework implementation ✓ Returning to Spring Batch after a hiatus ✓ Migrating from Spring Boot 2 Batch due to end of support ✓ Experienced with Java main method batches but need Spring Batch ✓ Seeking rapid batch development solutions Repository Setup https://github.com/kinto-technologies/SpringBoot3BatchStarter/ This is my repository. Please clone it and import it into your IDE. For GUI-based operations, use GitHub Desktop, Repository Structure The repository is structured as follows: . ├── gradlew # Gradle wrapper for build automation ├── settings.gradle ├── compose.yaml # Docker Compose configuration ├── init-scripts # Database initialization │ ├── 1-create-table.sql │ └── 2-insert-data.sql ├── dbAndCsvBatch # DB/CSV processing implementation │ ├── build.gradle │ └── src │ ├── main │ └── test └── skeletonBatch # Basic batch implementation ├── build.gradle └── src ├── main └── test Skeleton Batch Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/tree/main/skeletonBatch This is a skeleton code that becomes a complete batch process just by adding business logic. It consists of only the necessary code components. Core Concepts: Jobs and Steps Spring Batch is built on two fundamental concepts: Jobs and Steps. Jobs A Job represents the complete batch process: Contains one or multiple Steps Manages batch execution flow Supports sequential and parallel processing graph TD A[Job] --> B[Step 1] A --> C[Step 2] A --> D[Step 3] style A fill:#f9f,stroke:#333,stroke-width:4px Job Capabilities Capability Implementation Example Sequential Chain multiple jobs Daily data processing pipeline Error Handling Define recovery actions Automated error notifications Concurrent Run multiple jobs simultaneously Parallel data processing Parallel Multiple instances of same job Large dataset processing Step A Step represents a processing unit within a job. One job can register multiple steps, and step behavior can be designed flexibly like jobs. Implementation Method Steps can be implemented using either Chunk or Tasklet processing. 1. Chunk Processing This is a method for efficiently processing large volumes of data. graph LR A[Reader] -->|100 items at once| B[Processor] B -->|One by one| C[Writer] C -->|100 items at once| D[DB/File] Implementation Characteristics Processing is divided into three phases: Reader: Reads data in fixed quantities Example: Reading 100 lines at a time from CSV file using FlatFileItemReader Processor: Processes data one item at a time Example: Converting date formats using CustomProcessor Writer: Outputs data in bulk Example: Bulk INSERT to DB using JdbcBatchItemWriter Transactions are committed by chunk size (e.g., 100 items). This means that even if an error occurs midway, the successfully completed chunks are committed. 2. Tasklet Processing This is a method for executing simple, single-unit processes. Characteristics Simple implementation Easy to understand processing flow Straightforward debugging Choosing Between Implementation Methods Aspect Chunk Tasklet Data Volume Suitable for large volumes Suitable for small volumes Processing Complexity Suitable for complex processing Suitable for simple processing Management Cost Higher Lower Debugging Somewhat complex Easy Transaction By chunk By process unit Summary Job is the management unit for the entire batch process Step is the execution unit for specific processes Chunk is suitable for efficient processing of large data volumes Tasklet is optimal for simple processing Choose between Chunk and Tasklet based on processing requirements :::message Key Point For simple batches that don't require complex processing, choose Tasklet without hesitation. This skeleton batch also adopts Tasklet processing. ::: :::message alert Note In official documentation and many web articles, chunk processing is explained as the basic approach of Spring Batch, which often leads beginners to mistakenly think they must use chunks. This frequently results in implementing with chunks first, then realizing later that Tasklet would have been more appropriate, causing rework. ::: YAML Configuration and Spring Batch Management Tables Let's first explain the basic configuration needed to run Spring Batch in batch-dedicated mode. YAML Configuration https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/resources/application.yml This configuration allows the Spring Boot application to run in serverless mode. While typical Spring Boot applications maintain their process by starting servers like Tomcat, this is unnecessary for batch processing. With this configuration, the process terminates when batch processing completes. Spring Batch Management Tables Spring Batch requires management tables in a database to record execution results. However, managing databases and tables can be cumbersome. Moreover, you might need to run batches in environments without databases. In such cases, using H2 in-memory database is recommended. When the process ends: Memory is released Database and tables are cleared Next execution starts with a fresh state H2 is automatically configured if no database settings are specified in application.yml :::message Benefits of Persistent Storage While using H2 is convenient for development, using a persistent database allows you to: Maintain execution history Resume from interruptions Track batch execution status ::: Job Class Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/job/SampleJob.java#L16-L41 This is the core class for registering and executing Jobs in Spring Batch. Class Definition Key Points @Configuration Recognized as a Spring Batch configuration class Indicates that this class provides Spring configuration Required annotation for Job definition classes @Bean Applied to methods that generate objects managed by Spring Framework In this case, allows Spring to manage the Job created by createSampleJob The Job instance is used during batch execution :::message alert Important Note By default, the method name is used as the Bean name. If you copy this class to create a new Job, using the same method name will cause an error. To avoid this, specify a unique name like @Bean("jobName") . This is a common pitfall to watch out for. ::: Dependency Class Roles JobRepository : Manages job execution state PlatformTransactionManager : Maintains database consistency SampleLogic : Handles actual business processing Processing Flow Output job registration start log Create Step (define processing unit) Create Job and register Step Output job registration completion log Transaction Management Normal completion: Database changes are confirmed when all processing succeeds Abnormal termination: Database changes are rolled back when errors occur This ensures the reliability of batch processing operations. Logic Class Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/logic/SampleLogic.java#L13-L35 This class defines the actual processing content of the batch. Class Definition Key Points @Component Makes this class manageable by Spring Allows usage with @Autowired in other classes Tasklet Interface Interface representing Spring Batch processing unit Implement actual processing in the execute method Processing Flow Output batch processing start log Call SampleService's process method Output completion log on normal termination Log exception and re-throw on error occurrence Output batch processing end log Error Handling Normal completion: Return RepeatStatus.FINISHED to complete batch Abnormal termination: Catch exception, log it, and notify upper layer :::message Implementation Point While this class delegates processing to SampleService, you can implement simple processing directly in the execute method. ::: Service Class Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/skeletonBatch/src/main/java/com/example/batch/service/SampleService.java#L8-L23 This class is for implementing actual business logic. Class Definition Key Points @Service Recognized as a Spring Framework service class Indicates this class implements business logic process Method Method for implementing batch business processing Includes start and completion log output :::message Implementation Point This class is completely independent from Spring Batch classes and interfaces. This means: Focus purely on business logic implementation Easy to write JUnit tests Easily reusable in other projects ::: Customization Options You can modify: Class name Method name Arguments and return values Business logic implementation (e.g., data validation, transformation, external system integration) This concludes our explanation of the main skeleton batch classes. This loosely coupled design makes the batch easy to maintain and test. Running the Batch Running from IDE Start the BatchApp class. It works like a regular Spring Boot application with no special startup arguments required. Running via Gradle from Terminal Execute the following command: ./gradlew :skeletonBatch:bootRun Running by Generating and Executing JAR File Gradle's default task is configured to generate a JAR file. Execute as follows: cd skeletonBatch ../gradlew java -jar build/libs/batch-skeleton*.jar Checking Execution Logs Let's examine the Spring Batch execution flow through the logs. 1. Job Registration Check ----------- Registering job: sample ----------- ----------- Job registered successfully: sample-job ----------- During Spring Boot startup, the batch job 'sample-job' was successfully registered. 2. Batch Processing Execution Started BatchApp in 0.456 seconds (process running for 0.616) Running default command line with: [] Job: [SimpleJob: [name=sample-job]] launched with the following parameters: [{}] Executing step: [processing-step] ----------- START ----------- Batch Processing ----------- --- Starting batch process --- --- Batch process completed --- Processing completed successfully ----------- END ----------- Batch Processing ----------- Step: [processing-step] executed in 3ms Job: [SimpleJob: [name=sample-job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 9ms Important Points from Logs: 'sample-job' executed and 'processing-step' started START and END markers clearly show processing boundaries Step completed successfully and job finished with COMPLETED status Application terminated normally Skeleton Batch Summary Advantages Complete batch process by just adding business logic Integration with schedulers like cron using generated JAR file High maintainability through simple structure :::message Development Support Spring Batch development often faces issues like: Missing annotations Configuration mistakes Complex error messages This skeleton helps you avoid these problems and focus on implementing business logic. ::: This skeleton code aims to contribute to more efficient batch development. DB and CSV Batch Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/tree/main/dbAndCsvBatch Overview This project includes two batch processes: DB to CSV Export Exports database records to CSV format Customizable extraction conditions via startup arguments Works with default settings CSV to DB Import Bulk imports CSV data into database Requires CSV file, so run export batch first Local Database Setup This batch uses a MySQL local database. Follow these steps for setup: Start MySQL Container docker compose up -d :::message If Docker is not installed, download and install it from the Docker official website . ::: Verify Setup Connect to MySQL container and check sample data: docker exec -it mysql-container mysql -u sampleuser -psamplepassword sampledb Run sample query: mysql> SELECT * FROM member WHERE delete_flag = 0 AND type IN (1, 2, 3) ORDER BY type ASC; mysql> exit Bye Entity Class Auto-generation After cloning the GitHub repository, you might encounter a "Entity class not found" error. This occurs because jOOQ needs to auto-generate Entity classes. Running Auto-generation Either command will generate Entity classes: # Execute default task cd dbAndCsvBatch/ ../gradlew # Or directly execute jOOQ generation ../gradlew generateJooq The build.gradle is configured for immediate use without additional settings. Default task execution includes: Cleanup of build results Java code formatting (Google Java Format) Entity class auto-generation (jOOQ plugin) Compilation Static analysis and testing (JUnit, coverage, SpotBugs) Executable JAR generation Generated Entity Classes Check generated Entity classes with: tree dbAndCsvBatch/build/generated-src # If tree command is not installed: ls -R dbAndCsvBatch/build/generated-src Example output: dbAndCsvBatch/build/generated-src/jooq └── main └── com └── example └── batch └── jooq ├── DefaultCatalog.java ├── Keys.java ├── Sampledb.java ├── Tables.java └── tables ├── Member.java └── records └── MemberRecord.java :::message alert Since jOOQ plugin auto-generates code from database schema during build, managing it with Git can cause conflicts when schema changes. Therefore, generated code should not be included in Git management. ::: Handling "Class Not Found" Errors Your IDE might report Entity classes not found. Resolve this by either: Add generated-src/jooq to IDE's build path Copy classes from generated-src/jooq to dbAndCsvBatch/src/main/java Production Environment Setup https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/build.gradle#L101-L124 :::message Change connection settings to your DEV/STG database Or add your production CREATE TABLE statements to Docker Compose initialization SQL ::: jooq gradle plugin website Multi-database Configuration This batch uses a dual database configuration: H2 (In-memory Database) Used for Spring Batch management tables Resets when process ends Ideal for development and testing MySQL (Business Logic Database) Used for actual business data processing Runs in Docker container Configuration File (application-local.yml) Spring Boot's configuration file defines two databases under spring.datasource. https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/resources/application-local.yml The configuration file includes: Two database settings Profile suffixes (local and server) Runtime profile selection capability The server profile is intended for your production MySQL server settings. Data Source Configuration Class https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/config/DataSourceConfig.java#L11-L37 Key Annotations @Configuration Marks class as Spring configuration Spring manages beans defined here @ConfigurationProperties Maps YAML settings to class properties Example: maps 'spring.datasource.mysqlmain' @BatchDataSource Specifies data source for Spring Batch tables Applied to H2 database @Primary Designates default data source Applied to MySQL data source Job Class Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/job/DbToCsvJob.java#L31-L39 New Features Added Automatic Run ID Increment .incrementer(new RunIdIncrementer()) Automatically increments job execution number (run.id) Used for execution history management :::message While this sample uses H2 (in-memory DB) making the run ID transient, this is a standard implementation pattern when using persistent databases. ::: Listener Integration .listener(listener) Add processing before and after job execution Useful for logging, email notifications, etc. Logic Class Guide https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/logic/DbToCsvLogic.java#L18-L49 This example demonstrates basic logging implementation, but you can add custom processing like error handling and notifications. Parameter Configuration @Value("${batch.types:2,3,4}") private String typesConfig; Uses 2,3,4 as default values even without application.yml Can be overridden by passing --batch.types=1,2 at runtime Error Handling contribution.setExitStatus(ExitStatus.FAILED); Explicitly sets failure status on error Automatically sets COMPLETED status on normal completion Repository Class and jOOQ Guide About jOOQ jooq is a library that allows writing SQL as Java code: Express SQL syntax directly in Java code Auto-generate Entity classes from table definitions Type-safe SQL operations Select Processing https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/repository/MemberRepository.java#L18-L25 Key Features SQL-like Syntax Direct expression of SQL in Java code Leverage existing SQL knowledge Auto-generated Class Usage MEMBER: Represents table definition (column names, etc.) MemberRecord: For record mapping Bulk Insert Processing https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/java/com/example/batch/repository/MemberRepository.java#L54-L69 Bulk Insert Features Registers multiple records at once instead of one-by-one Common in batch processing to reduce database load :::message While not used in this batch, example code for insert, update, and delete operations is included. You can quickly adapt the repository class by replacing table and column names to match your business environment. ::: Repository Class Summary Use jOOQ's Gradle plugin to auto-generate entities from database schema Auto-generated classes enable type-safe SQL writing in Java Easy to handle schema changes - just regenerate classes :::message Spring Batch supports multiple database operation methods: Hibernate (JPA) Powerful database abstraction Easy Spring Boot integration May have overhead with large datasets MyBatis Ideal for direct SQL management Strong dynamic query support May reduce maintainability Spring JdbcTemplate Lightweight database operations Good for simple queries Can be verbose ::: :::message alert Note Choose the appropriate method based on: Project requirements Team skill set Direct SQL execution if needed ::: DB to CSV Batch Execution Guide Run the DB to CSV batch following similar steps as the skeleton batch. Running via Gradle ./gradlew :dbAndCsvBatch:bootRun Running JAR File cd dbAndCsvBatch/ ../gradlew java -jar build/libs/batch-dbAndCsv*.jar Running without arguments produces this error: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jobLauncherApplicationRunner' defined in class path resource [org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.class]: Job name must be specified in case of multiple jobs at com.example.batch.DbAndCsvBatchApp.main(DbAndCsvBatchApp.java:16) Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs Why This Error Occurs Unlike the skeleton batch with a single job, this project contains multiple jobs: CSV to DB: Importing CSV data into database DB to CSV: Exporting database data to CSV The framework cannot determine which job to execute without explicit specification. Running with Arguments Specify the job name and environment in startup arguments: --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local --spring.batch.job.name=DB_TO_CSV: Specifies which job to run --spring.profiles.active=local: Activates local environment settings Example Commands # Using Gradle ./gradlew :dbAndCsvBatch:bootRun --args="--spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local" # Using JAR directly cd dbAndCsvBatch/ java -jar build/libs/batch-dbAndCsv*.jar --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local Examining the Logs Let's check the execution logs: 1. Initial Startup Log ##### KEY:"sun.java.command", VALUE:"com.example.batch.DbAndCsvBatchApp --spring.batch.job.name=DB_TO_CSV --spring.profiles.active=local" ##### Spring Batch ##### - Job: DB_TO_CSV, Profile: local This confirms our startup arguments were correctly received. 2. Job Start Log ----------- JOB [Job Name:DB_TO_CSV] START! ----------- Executing step: [DB_TO_CSV-step] Fetching members with types = [1, 2, 3] Shows BatchNotificationListener's beforeJob method execution and configured types. 3. Database Operation Log -> with bind values : select `sampledb`.`member`.`id`, `sampledb`.`member`.`type`, `sampledb`.`member`.`name`, `sampledb`.`member`.`email`, `sampledb`.`member`.`phone`, `sampledb`.`member`.`address`, `sampledb`.`member`.`delete_flag`, `sampledb`.`member`.`created_at`, `sampledb`.`member`.`updated_at` from `sampledb`.`member` where (`sampledb`.`member`.`delete_flag` = 0 and `sampledb`.`member`.`type` in (1, 2, 3)) order by `sampledb`.`member`.`type` Version : Database version is supported by dialect MYSQL: 9.1.0 Fetched result : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ : | id|type|name |email |phone |address |delete_flag|created_at |updated_at | : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ : | 1| 1|John Doe |john.doe@example.com |1234567890|123 Main St, City, Country | 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : | 2| 1|Jane Smith|jane.smith@example.com|0987654321|456 Oak St, Town, Country | 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : | 26| 1|John Doe |john.doe@example.com |1234567890|123 Main St, City, Country | 0|2024-12-09T05:36:37|2024-12-09T05:36:37| : | 27| 1|Jane Smith|jane.smith@example.com|0987654321|456 Oak St, Town, Country | 0|2024-12-09T05:36:37|2024-12-09T05:36:37| : | 3| 2|ABC Corp |contact@abccorp.com |5678901234|789 Pine St, Village, Country| 0|2024-12-07T09:46:07|2024-12-07T09:46:07| : +----+----+----------+----------------------+----------+-----------------------------+-----------+-------------------+-------------------+ Fetched row(s) : 5 (or more) Batch process completed successfully. Step: [DB_TO_CSV-step] executed in 193ms Job: [SimpleJob: [name=DB_TO_CSV]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 212ms ----------- JOB [Job Name:DB_TO_CSV] FINISHED! status:[COMPLETED] ----------- https://github.com/kinto-technologies/SpringBoot3BatchStarter/blob/main/dbAndCsvBatch/src/main/resources/logback.xml#L21-L22 jOOQ's query and result logging is enabled in logback.xml for easier debugging. Generated CSV Check "id","type","name","email","phone","address","deleteFlag","createdAt","updatedAt" "1","1","John Doe","john.doe@example.com","1234567890","123 Main St, City, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "2","1","Jane Smith","jane.smith@example.com","0987654321","456 Oak St, Town, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "3","2","ABC Corp","contact@abccorp.com","5678901234","789 Pine St, Village, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "5","3","Alice Premium","alice.premium@example.com","4561237890","987 Maple St, City, Country","0","2024-12-11T06:05:26","2024-12-11T06:05:26" "6","3","Charlie Davis","charlie.davis@example.com","1112223333",,"0","2024-12-11T06:05:26","2024-12-11T06:05:26" The CSV file is generated using the OpenCSV library. opencsv website Testing Runtime Argument Override Let's try customizing the types parameter at runtime: --spring.batch.job.name=DB_TO_CSV --batch.types=4,5 --spring.profiles.active=local Log Output -> with bind values : select `sampledb`.`member`.`id`, `sampledb`.`member`.`type`, `sampledb`.`member`.`name`, `sampledb`.`member`.`email`, `sampledb`.`member`.`phone`, `sampledb`.`member`.`address`, `sampledb`.`member`.`delete_flag`, `sampledb`.`member`.`created_at`, `sampledb`.`member`.`updated_at` from `sampledb`.`member` where (`sampledb`.`member`.`delete_flag` = 0 and `sampledb`.`member`.`type` in (4, 5)) order by `sampledb`.`member`.`type` This confirms that the configuration file values were successfully overridden by command line arguments. CSV to DB Batch Execution Run the batch with the specified job name: --spring.batch.job.name=CSV_TO_DB --spring.profiles.active=local Log Output insert into `sampledb`.`member` (`name`, `email`, `phone`, `address`, `type`) values ('Premium Corp', 'premium@corporate.com', '8889997777', '555 High St, City, Country', 4), ('Elite Ltd', 'elite@elitecorp.com', '4445556666', '777 Sky Ave, Town, Country', 4), ('Guest User1', 'guest1@example.com', '', 'Guest Address 1, City, Country', 5), ('Guest User2', 'guest2@example.com', '9998887777', '', 5) Affected row(s) : 4 This confirms successful bulk insertion of records. Continuous Integration This project implements CI (Continuous Integration) using GitHub Actions. Quality management is efficiently handled through automated processing triggered by code pushes or pull requests. Main Workflow MySQL Setup Launch MySQL using Docker Compose Verify required tables JDK 21 Setup Install Java 21 Configure build environment jOOQ Class Generation Auto-generate entity classes from database schema Build and Test Execute Gradle build Run quality checks: JUnit tests Jacoco coverage SpotBugs analysis This workflow enables: Immediate detection of compilation errors Quick identification of JUnit test failures Rapid response to issues Consistent code quality maintenance Dynamic Badge Updates When the build and tests succeed, a dynamic badge is displayed on the GitHub README, indicating the status of the project. Code Coverage Visualization This project uses Codecov to measure and visualize test coverage. Coverage reports are automatically generated during pull requests, and the coverage rate can be checked via this badge: graph LR D[👩‍💻 Developer] -->|Push/PR| G[🐙 GitHub] G -->|Trigger| GHA[⚡ GitHub Actions] GHA -->|Build and Test Results| R[📄 README.md] GHA -->|Coverage Report| C[☁️ Codecov] C -->|Coverage Badge| R style G fill:#f4f4f4,stroke:#171515 style C fill:#f01f7a style R fill:#e6e6e6 This enables: Visual tracking of test coverage Quick detection of coverage changes Enhanced transparency in quality management Detailed coverage reports are available on the Codecov dashboard. Conclusion We hope you found this guide helpful! This project provides: Skeleton code foundation for efficient Spring Batch development Common use cases like "DB to CSV" and "CSV to DB" Flexible customization through database settings and CSV layouts We hope this skeleton helps streamline your batch development by allowing you to focus on business logic implementation. If you found this article helpful and got your Spring Batch up and running quickly, we would appreciate a ⭐ on our GitHub repository! Thanks for reading! [^1]: Post by Common Service Development Group Member 1 [ Implementing Domain-Driven Design (DDD) in Payment Platform with Global Expansion in Mind ] [^2]: Post by Common Service Development Group Member 2 [ Success Story: New System Development through Remote Mob Programming by Team Members with Less Than One Year Experience ] [^3]: Post by Common Service Development Group Member 3 [ Improving Deploy Traceability Across Multiple Environments Using JIRA and GitHub Actions ] [^4]: Post by Common Service Development Group Member 4 [ Development Environment Setup Using VSCode Dev Container ] [^5]: Post by Common Service Development Group Member 5 [ Guide to Setting Up S3 Local Development Environment Using MinIO (AWS SDK for Java 2.x) ]
はじめに VSCodeとCopilotの組み合わせが最高に楽しいです。楽しすぎて本業を忘れてしまいそうです。 本日はそんな楽しすぎる開発体験をみなさんにもぜひ知って欲しいという思いで記事を書きます。 この記事は KINTOテクノロジーズアドベントカレンダー2024 の25日目の記事です🎅🎄 今回の内容 去年辺りから熱くなったAI界隈ですが、今年もAIの話題が尽きない一年でしたね! KTCでは積極的にAIを取り入れて開発生産性低減の道を探っています。 今回はVSCode x Copilotの組み合わせでできることを紹介します。 でも、AIってなんか微妙なコードしか出してこないし、自分で書いた方が早いから使わないよね、という方もいるかもしれません。 僕も初めはそうでしたが、先日開催されたGitHub Universeで発表されたCopilot Chat/Editsを見て、AIとの共同作業が楽しくなるかもしれないと感じました。 と言うことで、今回は実際にCopilot Chat/Editsを使って、AIと共同でプログラミングをする中で、普段使いで有効だと思うTipsを紹介します。 https://reg.githubuniverse.com/flow/github/universe24/attendee-portal/page/livestream?tab.day=20241029 なんでFlutter? モバイル開発をする上で、iOSもAndroidも各OSの標準IDEを使って開発をするのが主流です。 しかし、今回はGitHub Copilotを使ってプログラミングするという観点で、VSCodeを使って開発することを前提とします。 そう考えると、モバイル開発で公式にVSCodeをサポートしているのはFlutterがメジャーです。 先日 FlutterKaigi にも参加し得るモノが多かったので、今回はFlutterを使ってAIとの共同作業をすることにします。ちなみに、FlutterKaigiのアプリ、本当に些細なIssueですが、僕もコントリビュートしました😀 実際に何ができるのか? GitHub Universeで発表された機能はPreview機能が多く、使用できる機能は限られていますが、すでにリリースされているCopilot Chat/Editsを使いこなすだけでも、AIとの共同作業が楽しくなり、開発者体験の向上が実感できました。 エンジニアの皆さんがAIを使ってコーディングするというと、ノーコードでアプリを作るとか、コードを自動生成するとか、そういったイメージがあるかもしれません。確かに、コードを自動生成する事は可能ですが、それはAIのほんの一部の機能でしかないです。 また、複雑な機能を持つコードを一度の指示で動作保証できるほどのコードを生成することは(指示出しが)難しいです。 この記事で紹介するのは、テストコードの作成、コードの整形・分割、リファクタリングなど、普段皆さんがコーディングでやっていることをAIとの共同作業を通じて行い、コードの品質を向上させることができるCopilot Chat/EditsのTipsになります。 それでは、ここからTipsの紹介です。 コードの要約 Copilot Chatの機能を使うと、コードの要約を簡単に作成できます。これを使えば、オンボーディング時のコード理解が早まります。 コメント記載 BEFORE AFTER コードにコメントを追加する際、Copilot Editsを使うと、Copilotが直接ファイルにコメントを追加してくれます。これにより、コードの理解が深まります。 また、すでにあるコメントを修正する際も、Copilotが適切なコメントを修正してくれます。 例えば上のクラスのaddメソッドに対して、バリデーションを追加した場合以下の様に指示するとコメントを更新してくれます。 BEFORE AFTER テストコードの作成 copilot editsのテキスト入力欄の上に現在のファイル以外に参照できるファイルを増やす事ができるので、テストコードを書いてもらうときは、 テスト用のファイルを作成し、そのファイルを開いたまま、参照ファイルに対象ファイルを追加すれば、テストコードを書いてくれます。 コードの整形・分割 Flutter等の宣言型UIを採用している言語でUIを作成しているとどうしてもネストが深くなってしまう問題が頻発します。 :::details 整形前のコード class SomethingPage extends StatelessWidget { const SomethingPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Something Page'), ), body: Center( child: Container( padding: const EdgeInsets.all(16.0), color: Colors.blueAccent, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), color: Colors.redAccent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(4.0), color: Colors.greenAccent, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(2.0), color: Colors.yellowAccent, child: const Text('Deeply Nested Widget 1'), ), Container( padding: const EdgeInsets.all(2.0), color: Colors.purpleAccent, child: const Text('Deeply Nested Widget 2'), ), ], ), ), ], ), ), ], ), ), ), ); } } ::: こういった問題もCopilot Editsを使うと、コードを見やすい単位で分割、整形してくれます。 :::details 整形後のコード class SomethingPage extends StatelessWidget { const SomethingPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Something Page'), ), body: Center( child: Container( padding: const EdgeInsets.all(16.0), color: Colors.blueAccent, child: const NestedContainer(), ), ), ); } } class NestedContainer extends StatelessWidget { const NestedContainer({super.key}); @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ InnerContainer(), ], ); } } class InnerContainer extends StatelessWidget { const InnerContainer({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8.0), color: Colors.redAccent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ DeeplyNestedContainer(), ], ), ); } } class DeeplyNestedContainer extends StatelessWidget { const DeeplyNestedContainer({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(4.0), color: Colors.greenAccent, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ DeeplyNestedWidget1(), DeeplyNestedWidget2(), ], ), ); } } class DeeplyNestedWidget1 extends StatelessWidget { const DeeplyNestedWidget1({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(2.0), color: Colors.yellowAccent, child: const Text('Deeply Nested Widget 1'), ); } } class DeeplyNestedWidget2 extends StatelessWidget { const DeeplyNestedWidget2({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(2.0), color: Colors.purpleAccent, child: const Text('Deeply Nested Widget 2'), ); } } ::: リファクタリング 歴史あるコードでよく見かける、依存度が複雑に絡み合ってしまったメソッドも、Copilot Editsを使うことで、SOLID原則に基づいたリファクタリングが可能です。 :::details 依存度が複雑なメソッドを持つクラス class SomethingLogic { Future<void> performComplexOperation() async { // Fetch data from remote server final response = await http.get(Uri.parse('https://example.com/data')); if (response.statusCode != 200) { throw Exception('Failed to load data'); } final data = jsonDecode(response.body); // Save data to local database final database = openDatabase( join(await getDatabasesPath(), 'app_database.db'), onCreate: (db, version) { return db.execute( 'CREATE TABLE data(id INTEGER PRIMARY KEY, value TEXT)', ); }, version: 1, ); final db = await database; await db.insert( 'data', {'id': data['id'], 'value': data['value']}, conflictAlgorithm: ConflictAlgorithm.replace, ); // Save data to shared preferences final prefs = await SharedPreferences.getInstance(); await prefs.setString('data', jsonEncode(data)); // Perform some complex logic if (data['value'] == 'special') { // ...existing code... print('Special value found!'); // ...existing code... } else { // ...existing code... print('Regular value found.'); // ...existing code... } } } :::: :::details SOLID原則に基づいたリファクタリングをしてもらったコード class DataFetcher { final http.Client client; DataFetcher(this.client); Future<Map<String, dynamic>> fetchData(String url) async { final response = await client.get(Uri.parse(url)); if (response.statusCode != 200) { throw Exception('Failed to load data'); } return jsonDecode(response.body); } } class DatabaseHelper { Future<Database> getDatabase() async { return openDatabase( join(await getDatabasesPath(), 'app_database.db'), onCreate: (db, version) { return db.execute( 'CREATE TABLE data(id INTEGER PRIMARY KEY, value TEXT)', ); }, version: 1, ); } Future<void> saveData(Database db, Map<String, dynamic> data) async { await db.insert( 'data', {'id': data['id'], 'value': data['value']}, conflictAlgorithm: ConflictAlgorithm.replace, ); } } class SharedPreferencesHelper { Future<void> saveData(Map<String, dynamic> data) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('data', jsonEncode(data)); } } class SomethingLogic { final DataFetcher dataFetcher; final DatabaseHelper databaseHelper; final SharedPreferencesHelper sharedPreferencesHelper; SomethingLogic(this.dataFetcher, this.databaseHelper, this.sharedPreferencesHelper); Future<void> performComplexOperation() async { final data = await dataFetcher.fetchData('https://example.com/data'); final db = await databaseHelper.getDatabase(); await databaseHelper.saveData(db, data); await sharedPreferencesHelper.saveData(data); if (data['value'] == 'special') { // ...existing code... print('Special value found!'); // ...existing code... } else { // ...existing code... print('Regular value found.'); // ...existing code... } } } :::: ここまでのリファクタリングを自分でやると、時間がかかりますが、Copilot Editsを使うことで、短時間でリファクタリングが可能です。 だいぶ見やすいコードになったので、ここから自身の経験をもとに、さらにリファクタリングを行う事も可能です。 その他 これ以外にも、基本的なSuggestionsの機能が強力で、普段であれば実装に困った際はブラウザとIDEを行き来していましたが、ほとんどの困りごとをCopilotとの相談で解決できるので、開発への集中が途切れなくなりました。これは地味に大きいです。 指示だしのコツ AIとの共同作業は、新しい発見がたくさんありますが、AIに対する指示だしが重要な要素だなと感じました。 僕の周りにもAIが使いづらく感じているエンジニアは多くいますが、AIに対して具体的な指示を出すことに慣れると、AIとの共同作業が楽しくなると思います。 僕が感じた指示だしのコツは シンプルな内容 具体的な内容 どのファイル 何をする どのようにする 一度に複数の指示を出さない 特に、指示を出す側がしっかりとしたプログラミング原則や設計思想を理解していることが重要だと感じます。 まとめ 今回はVSCode x Copilotの組み合わせによって、AIとできることを紹介しました。 Copilot Chat/Editsを使うことで、コードの要約、コメント記載、テストコードの作成、コードの整形・分割、リファクタリングが簡単にできる事が少しでも伝えられたら幸いです。 エンジニア界隈ではAIに対して賛否両論あり、 僕の周りのエンジニアもまだまだAIに対して抵抗がある人が多いです。 世間の流れは確実にAIに向かっていて、今後もAIは進化し、どんどんできることが増えていくことが予想されます。 僕らエンジニアはどうAIを使っていくかを考える時代になったのかなと感じでいます。 この記事が少しでもAIとの共同作業に対して興味を持っていただけたら幸いです。
KINTOテクノロジーズの景山です! 年末恒例ですが、2024年の振り返りと2025年の展望について書こうと思います。 2024年の振り返り 振り返ると、1年前とくらべて、やるべきことが増えました。 それにともなって、社員も増え、組織も拡大しました。 一方で、組織が拡大しても、内製開発組織としてのメリットを失わないように手をうってきたつもりです。 今年は本格的に販売店のデジタルトランスフォーメーションのサポート(販売店DXプロジェクト)が立ち上がりました。 すでに見積もり関連で販売店の工数を削減するツールや、お客様管理アプリの提供が始まっています。 また、AIを活用したプロダクト開発も順調に進んでいます。 AIやクラウド利用が進むにあたり、セキュリティもガバナンスする範囲が広がっています。 クラウドセキュリティの専門部隊の設置や、AIプロジェクトのセキュリティについても取り組んできました。 社内のAI活用への取り組みも強化してきました。 社内コミュニケーションツールのSlackとAIを組み合わせた仕組みは社内で一般化してきています。 非エンジニア社員からは生成AIを活用して、アプリケーション開発をしてしまうような例も出てきました。 さらにグループ会社や販売店への生成AI研修もはじめました。 重要施策の進捗 重要施策として、 昨年のアドベントカレンダー では下記3つを高めていく、と書きました。 技術力 開発生産性 リリーススピード まず、技術力については、トヨタ自動車およびグループ会社へのテック支援が増えました。 ITベンダー各社からも評価されるユースケースを作ることもできました。 とくに、テックブログは、多くの読者に支持され、なんと年間12万ユニークユーザーを達成しました。 多くの人が執筆に参加してくれるようになり、その結果、社外からの反響も大きくなっています。 つぎに、開発生産性については、Findy Team+で定量的に自分たちの生産性を把握し、個々の工程の生産性向上に資する取り組みが広がっています。また生成AIを活かした仕組みの導入、例えば、GitHub Copilotの活用など社員の開発生産性向上の意識も高まり、ビジネス企画からリリースまでの全体スループットをあげる取り組みなどいくつもの新しいイニシアティブがスタートしています。 また、リリーススピードについては、おもに要件定義までの工程の圧縮をはかってきました。要件定義工程を開発側がリードすることで実現可能かつ開発工数が少ない方法でプロダクト開発を行う方向にビジネス側をリードする、ということができはじめています。 まだまだ試行錯誤の段階でもありますが、このスピードに関する意識を社内でもっともっと浸透させたいと思います。 2025年の展望 では来年はどうするかというと、下記2つの「ファースト」に注力したいと考えています。 AIファースト リリースファースト(最短リリース) AIファースト AIファースト は文字通りなのですが、次のような動きを強めていきます。 すべてのプロダクトにAIをインテグレートしていく AIプロダクトを多く開発する 販売店、トヨタグループのAI活用を推進するドライバーになる これまでの社員みなさんの取り組みのおかげで、今年はこれらを推し進める下地が十分にできました。 来年は多くのアウトプットを出していけるはずと期待しています。 これからはAIを使わなくてもできるものでも、AIをあえて使って開発してみよう、というマインドが重要だと思います。そうした中で我々がAIファーストを実現するためのアイディアやコラボレーションが生まれてくることを期待しています。 リリースファースト リリースファースト(最短リリース) は、われわれが開発するプロダクトをいかに最短でリリースするか、知恵と技術を使ってここにこだわっていきたいと思っています。 ビジネスオーナーやプロダクトごとに最短リリースするためにやるべきことは異なります。 今年は一部のプロジェクトやチームでの要件定義工程及び、実装工程の生産性向上がメインでした。 来年は特定の工程の生産性向上にとどまることなく、各プロダクトチームが自分たちのプロダクトをステークホルダーも巻き込みどこをどうしたらリリース期間を短くし、最短リリースを実現できるのかを考えてほしいと思っています。 MVP(最小価値製品)の考え方を全面的に取り入れる必要もあると思っています。 当社の全プロダクトが同じやりかたで実現できるとは思っていません。 自分たちでプロダクトの価値について深く考え、最短リリースを実現することが内製開発部隊の強みであり、これが最大の事業貢献だと思っています。 来年は徹底的にこれを突き詰めていきます。 ユーザーファーストと組織インテンシティ またAIファーストとリリースファーストを支える基礎として、来年は改めて「ユーザーファースト」と「組織インテンシティ」にも注力していきます。 ユーザーファースト ではサービスを使うユーザーだけでなく、ビジネスパートナーやその向こう側にいる顧客としっかり向き合うことを徹底していきたいと思っています。 ユーザーや顧客にとっての価値が何なのかを能動的に見つけられることは、われわれ内製開発の価値を高めることに繋がります。 そのためにはユーザーリサーチなどの方法論も武器として身に付ける必要もあります。 インテンシティ は サッカー用語 です。 優れた能力をもつメンバーが多く集まっていることはKINTOテクノロジーズの強みですが、内製開発部隊としてより高い成果を実現するためにはチームや組織のレベルをもう一段、二段と高めていく必要があります。 KTCをインテンシティの高い組織にするため、ひとりひとりがどのような心がけや行動をすべきか考え抜いてもらいたいと思います。 全力投球 次の段階に進むため2025年のテーマには入れなかった技術力と発信力の向上は社内に定着し、あたりまえになってきていると感じています。これからも各自が意識して、継続的に取り組んでいってほしいKTCの永遠のテーマです。 これらも含めて、来年も手を抜かないで全力投球していきます。 KINTOテクノロジーズ 取締役副社長 景山 均
Happy Christmas🎄 Hello! I am Watanabe from the KINTO ONE Development Group, where I work as a front-end engineer specializing in product development with Next.js and TypeScript. I also contributed to the development and launch of our Tech Blog as part of the Tech Blog Management team. On the final day of our Advent Calendar, I’ll be sharing insights into the design choices and front-end development process behind the Tech Blog. Assembling the Tech Blog Team The Tech Blog management team was originally formed by four volunteers, led by Nakanishi-san, with the aim of fostering a culture of output within the company. We discussed the operation system and blog specifications with the awareness of visualizing the remaining tasks, and I recall how quickly decisions were made. Among the many roles on the Tech Blog, I was responsible for the overall design and front-end implementation. During weekly meetings, ideas from our discussions were incorporated into prototypes and shared with the team. Prototypes motivates the team, so as the implementation manager, I approached development while focusing on visible progress. Building the Tech Blog with Next.js The front-end framework (library) we use is Next.js. I chose Next.js because I'm most familiar with it. From the concept stage, I envisioned storing blog posts as Markdown files and generating a static site using a Static Site Generator, which seemed like a great fit. The static content is generated at build time from the project, and the deliverables are then hosted. Next.js also publishes examples on GitHub that combine various libraries. One of these was blog-starter , which provided a template for the blog, so it was easy to get started, which is another reason I chose Next.js. Based on the above, I categorized the steps needed to create our Tech Blog into the following three development stages: Design Selection Screen Implementation (article list page, article detail pages) Preparing Markdown Tools Design Selection I took on the responsibility for the design, but since I had no prior experience with blog design, I began by researching case studies from other companies. Taking the features and findings obtained from other companies' case studies into account, our team organized the UI elements and information necessary for the Tech Blog. Documentation of features and findings During our design research, we focused on four components—header, cards, list, and footer—with the goal of creating a simple and easy-to-understand design. Since I had decided to take responsibility for the implementation as well, I created the design with easy coding and scalability in mind. Component Classification When the design was nearly finished, I consulted with the site design department and received their feedback. Screen Implementation Since the screen template was provided by blog-starter, we adopted Tailwind CSS as our CSS framework to focus on coding styles. Tailwind CSS uses well-defined, standard class names, making it easy to recognize the styles they represent. Its utility-first approach makes style adjustments intuitive, ultimately lowering development costs. Several of our in-house front-end engineers have contributed to enhancing the tech blog and fixing bugs. I believe this framework is well-suited for team development because they can implement it smoothly with minimal learning costs. The ability to write responsive support within the same className is also a great advantage. <div className="md:px-6 lg:px-10 md:py-10 md:hover:shadow-xl"> <div className="mb-6"> <CoverImage slug={slug} title={title} src={coverImage} /> </div> <div className="flex items-center mb-4 md:mb-6"> <div className="background-color h-7 flex items-center rounded-lg p-2 md:p-4 mr-3"> <span className="text-white text-xs md:text-base">{category}</span> </div> <div className="text-gray-500 text-sm md:text-base transition"> <DateFormatter dateString={date} /> </div> </div> <h3 className="text-xl sm:text-2xl md:text-2xl lg:text-3xl mb-2 md:mb-6 leading-snug text-color"> <Link href={`/posts/${slug}`}> <a className="hover:underline">{title}</a> </Link> </h3> </div> Preparing Markdown Tools Our Tech Blog uses zenn-markdown-html and zenn-markdown-css as Markdown parsers. We chose these because the default Markdown notation provided by blog-starter lacks variation and is difficult to customize. The role of each parser is as follows. zenn-markdown-html Package for converting Markdown to HTML (markdownToHtml), including Zenn's own notation zenn-content-css CSS for applying to HTML converted from Markdown with zenn-markdown-html Specify className=znc to the component or block to which you want to apply CSS A wide range of embedded content is also available, making it easy to create engaging blogs. (Content can be easily displayed by simply stating the URL of the Tweet.) https://x.com/KintoTech_Dev/status/1597900747538046978 Functions I Want to Try in the Future The Tech Blog started small with the goal of being simple and easy to understand. I'll also summarize ideas for functions that I want to implement in the future. Since its release, we've received many comments from both inside and outside the company, asking, "Why not add this function?" I'm thrilled to see that we're getting more attention every day. We are actually working on updates, such as adding RSS function . Implementation of article search and refinement functions by adding tags and categories Implementation of localization function to support multiple languages Move content management to Micro CMS or cloud servers, and stop local management Summary Despite the limited resources, we were able to explore new front-end technologies and had a lot of fun developing. Furthermore, the growing number of people submitting pull requests and reporting issues suggests that a blogging culture is gradually taking root within the company. While there is still room for improvement in the Tech Blog, I'll continue to do my best and stay updated with the latest technologies. Conclusion I hope this year's advent calendar helps you learn more about our company's initiatives and how our employees work! Furthermore, KINTO Technologies is looking for people to work with us! You can find more information here Thank you for reading my article all the way to the end! Reference List of Zenn's Markdown Notation
こんにちは( º∀º )/ 技術広報G イベントチームのゆかちです。 2024年、カンファレンスのスポンサー活動等も開始し外部発信活動が活発になり、 以前に比べて社内外どちらもイベントがたくさん増えました…! 社外向けイベントを開催していて、エントランスまでの行き方を伝えるのが難しいな~とモヤモヤしておりました。 「写真で説明したい…はっテックブログがある…!」と思い立った先の記事デス。 オフィスについて 弊社、拠点が4つあります! そういえば2年前にオフィス紹介の記事を書いていた…! @ card そんな中でイベント会場になることが多いのが、 室町オフィスの TOKYO JCT です。 コレド室町2のオフィス棟16階にあります! ![TOKYO JCT](/assets/blog/authors/uka/access/jct.jpg =600x) ** 住所:〒103-0022 東京都中央区日本橋室町2丁目3−1 室町古河三井ビルディング(COREDO室町2) 東京メトロ銀座線『三越前』駅直結 ※改札でて徒歩 2 分 東京メトロ半蔵門線『三越前』駅直結 ※改札でて徒歩 5 分 JR 総武快速『新日本橋』駅直結 ※改札でて徒歩 5 分 JR 中央線・京浜東北線・山手線『神田』駅徒歩 10 分 ではでは各駅からのアクセス方法を写真にて紹介します! アクセス案内へのショートカット 東京メトロ銀座線『三越前』駅 東京メトロ半蔵門線『三越前』駅 JR総武快速『新日本橋』駅 JR中央線・京浜東北線・山手線『神田』駅 東京メトロ銀座線『三越前』駅 まずは一番近い銀座線から! ![銀座線からオフィスへの道案内1](/assets/blog/authors/uka/access/gin1.png =600x) 三越方面改札をでて左へ ![銀座線からオフィスへの道案内2](/assets/blog/authors/uka/access/gin2.png =600x) コレド室町1があるので通り抜ける ![銀座線からオフィスへの道案内3](/assets/blog/authors/uka/access/muro.png =600x) 通り抜けた先にコレド室町2の入口があります ![銀座線からオフィスへの道案内4](/assets/blog/authors/uka/access/es.png =600x) はいってエスカレーターを上って… ![室町オフィスエントランス1](/assets/blog/authors/uka/access/en1.png =600x) すぐにみえるオフィスエリアになります! :::message イベント受付時間内はオフィスエリアのエントランスに受付が立っています! コレド室町2からオフィスエリアへは入館カードがないと入れないため、受付時間内は来場者っぽいかたを見つけたら扉を開けております。 ::: イベントではなくアポなど、18:00 までに来客者のかたは、 一旦外にでていただいてすぐ左手前のオフィスエリアの表入口からエントランスへお入りください。 ![室町オフィスエントランス2](/assets/blog/authors/uka/access/en2.jpg =600x) こちらから江戸桜通り沿いへ一旦でる ![室町オフィスエントランス3](/assets/blog/authors/uka/access/en3.png =600x) こちらが正面入口 エントランス入って正面のビル受付にて入館手続き後、担当者より指定の階におあがりください。 :::message イベント受付時間以降にいらっしゃるかたも、同じく表入り口からエントランスへ入っていただき、 イベント情報に記載のある担当者の電話番号、もしくは公式 X アカウントへご連絡ください。 担当者が1階へお迎えにあがります。 @ card ::: 東京メトロ半蔵門線『三越前』駅 半蔵門線三越前駅からも直結ですがすこし歩きます・・・ ![半蔵門線からオフィスへの道案内1](/assets/blog/authors/uka/access/han1.png =600x) 日本橋方面改札をでて右にまっすぐ ![半蔵門線からオフィスへの道案内2](/assets/blog/authors/uka/access/han2.png =600x) 小さい階段があるので下って斜め右に ![半蔵門線からオフィスへの道案内3](/assets/blog/authors/uka/access/han3.png =600x) まっすぐ進んで突き当りひだり ![半蔵門線からオフィスへの道案内4](/assets/blog/authors/uka/access/han4.png =600x) ここがコレド室町2です! ![半蔵門線からオフィスへの道案内5](/assets/blog/authors/uka/access/es.png =600x) はいってエスカレーターを上って… ![半蔵門線からオフィスへの道案内6](/assets/blog/authors/uka/access/en1.png =600x) すぐにみえるオフィスエリアになります! :::message イベント受付時間内はオフィスエリアのエントランスに受付が立っています! コレド室町2からオフィスエリアへは入館カードがないと入れないため、受付時間内は来場者っぽいかたを見つけたら扉を開けております。 ::: イベントではなくアポなど、18:00 までに来客者のかたは、 一旦外にでていただいてすぐ左手前のオフィスエリアの表入口からエントランスへお入りください。 ![室町オフィスエントランス2](/assets/blog/authors/uka/access/en2.jpg =600x) こちらから江戸桜通り沿いへ一旦でる ![室町オフィスエントランス3](/assets/blog/authors/uka/access/en3.png =600x) こちらが正面入口 エントランス入って正面のビル受付にて入館手続き後、担当者より指定の階におあがりください。 :::message イベント受付時間以降にいらっしゃるかたも、同じく表入り口からエントランスへ入っていただき、 イベント情報に記載のある担当者の電話番号、もしくは公式 X アカウントへご連絡ください。 担当者が1階へお迎えにあがります。 @ card ::: JR総武快速『新日本橋』駅 新日本橋駅からも直結ですがすこし歩きます・・・ ![新日本橋駅からオフィスへの道案内1](/assets/blog/authors/uka/access/sin1.png =600x) 改札をでて左にまっすぐ三越駅方面へ(新日本橋駅は改札がひとつ!) ![新日本橋駅からオフィスへの道案内2](/assets/blog/authors/uka/access/sin2.png =600x) 三越駅方面へ左に曲がる ![新日本橋駅からオフィスへの道案内3](/assets/blog/authors/uka/access/sin3.png =600x) 銀座線を右手にさらにまっすぐ ![新日本橋駅からオフィスへの道案内4](/assets/blog/authors/uka/access/sin4.png =600x) コレド室町1が左手にでてくるので入って通り抜ける ![新日本橋駅からオフィスへの道案内5](/assets/blog/authors/uka/access/muro.png =600x) 通り抜けた先にコレド室町2の入口があります ![新日本橋駅からオフィスへの道案内6](/assets/blog/authors/uka/access/es.png =600x) はいってエスカレーターを上って… ![室町オフィスエントランス1](/assets/blog/authors/uka/access/en1.png =600x) すぐにみえるオフィスエリアになります! :::message イベント受付時間内はオフィスエリアのエントランスに受付が立っています! コレド室町2からオフィスエリアへは入館カードがないと入れないため、受付時間内は来場者っぽいかたを見つけたら扉を開けております。 ::: イベントではなくアポなど、18:00 までに来客者のかたは、 一旦外にでていただいてすぐ左手前のオフィスエリアの表入口からエントランスへお入りください。 ![室町オフィスエントランス2](/assets/blog/authors/uka/access/en2.jpg =600x) こちらから江戸桜通り沿いへ一旦でる ![室町オフィスエントランス3](/assets/blog/authors/uka/access/en3.png =600x) こちらが正面入口 エントランス入って正面のビル受付にて入館手続き後、担当者より指定の階におあがりください。 :::message イベント受付時間以降にいらっしゃるかたも、同じく表入り口からエントランスへ入っていただき、 イベント情報に記載のある担当者の電話番号、もしくは公式 X アカウントへご連絡ください。 担当者が1階へお迎えにあがります。 @ card ::: JR中央線・京浜東北線・山手線『神田』駅 最後に神田駅からの行き方を紹介しますね! ![神田駅からオフィスへの道案内1](/assets/blog/authors/uka/access/kan1.png =600x) 南口(日本橋方面口)をでてスターバックス沿いを進む ![神田駅からオフィスへの道案内2](/assets/blog/authors/uka/access/kan2.png =600x) 横断歩道を渡って左へまっすぐ ![神田駅からオフィスへの道案内3](/assets/blog/authors/uka/access/kan3.png =600x) 横断歩道を渡って右へまっすぐ ![神田駅からオフィスへの道案内4](/assets/blog/authors/uka/access/kan4.png =600x) まだまだまっすぐ ![神田駅からオフィスへの道案内5](/assets/blog/authors/uka/access/kan5.png =600x) コレド室町3の手前を左へまっすぐ ![神田駅からオフィスへの道案内6](/assets/blog/authors/uka/access/kan6.png =600x) コレド室町2が見えてきますが、そのまままっすぐ通過してください ![神田駅からオフィスへの道案内7](/assets/blog/authors/uka/access/kan7.png =600x) 左手にオフィスエントランスが見えてくるのでこちらになります! ![神田駅からオフィスへの道案内8](/assets/blog/authors/uka/access/en1.png =600x) オフィスエリアのエントランス内に看板をもった受付が立っています(イベント受付時間内) イベントではなくアポなど、18:00 までに来客者のかたは エントランス入って正面のビル受付にて入館手続き後、担当者より指定の階におあがりください。 :::message イベント受付時間以降にいらっしゃるかたも、同じく表入り口からエントランスへ入っていただき、 イベント情報に記載のある担当者の電話番号、もしくは公式 X アカウントへご連絡ください。 担当者が1階へお迎えにあがります。 @ card ::: さいごに 以上、KINTOテクノロジーズ室町オフィスに足を運んでくださる人に 少しでも役に立てれば!な記事でした! ご来場いただきありがとうございます! またのお越しをお待ちしております(^_^)/
Introduction Hello. I am Okita from the Mobile App Development Group. I'm involved in developing a smartphone app for a specific service project. Although I'm working with the Tokyo team members, I'm primarily based at the Osaka Tech Lab. So, let me introduce you to the Osaka Tech Lab! History of Osaka Tech Lab Born in April 2022. The Osaka Tech Lab was established with the aim of strengthening our capabilities as an IT engineering company and broadening our reach, including recruiting talented engineers. Believe it or not...I started it by myself! I had the whole floor to myself at the start. Our company consists of four locations: Nagoya Office Muromachi Office Jinbocho Office Osaka Tech Lab "Why is only the Osaka location called Osaka Tech Lab?" I’m often asked this question. The reason is that 'Osaka Tech Lab' was perceived as sounding more stylish than 'Osaka Branch' or 'Osaka Office,' and it was thought to appeal to a wider audience. In July 2022. Our first Osaka recruitment brought in four people. And from there, we've steadily grown. As of May 2023, we've expanded to 16 members! Where is the Osaka Tech Lab? The closest station is Shinsaibashi Station! Osaka Tech Lab Just a 1-minute walk from Exit 3 of Shinsaibashi Station on the Osaka Metro Midosuji Line and Nagahori Tsurumi-ryokuchi Line. *Non-smoking facility (Indoor smoking area available). What's the Atmosphere Like at the Osaka Tech Lab? Let me give you a glimpse with some photos. Entrance Bookshelf On the top shelf, you'll find a collection of miniature cars featuring the KINTO logo. There are also cactuses. Free Space The area offers both sofa and table seating. We use this space to enjoy meals, take a quick break, or use it as a corner to focus on our tasks. Snack Area This space brings together team members working on different tasks , designed to encourage casual conversations during a quick break. It’s been a big hit! Clock Our discussions led to the purchase of this clock, which we all selected together. It went something like this. “It’s nice to check the time at a glance, isn’t it?” "Oh, I was thinking the same!" "Let’s ask if we can get one installed." "I like something stylish." "Let’s make sure it’s earthquake-proof!'" This is the kind of casual conversation we have, whether in person or on Slack. We Also Have Seasonal Decorations During the Christmas season, we even set up a Christmas tree. What Kind of Team Members Work at Osaka Tech Lab? There are a total of 16 team members, representing various groups from the following departments: Project Promotion Group Owned Media & Incubation Development Group Platform Group Common Service Development Group Analysis Group Corporate IT Group Human Resources Group Mobile App Development Group There are veterans who provide steady guidance, sometimes leading, sometimes mentoring others, as well as juniors who actively contribute a variety of fresh ideas. The balance of age groups feels just right. What Makes Osaka Tech Lab So Special Arguably the people and the unknown future. The people It's what makes it a comfortable place to be. You can say it has a warm, at-home atmosphere. Even visitors on business trips often compliment us like, Even visitors on business trips often compliment us, saying, "This is my first time here, but Osaka Tech Lab has such a great atmosphere!" I've thought about why it feels so comfortable here, and I believe it's because we have team members that respects each other, even with different roles. Everyone has their own opinions, but they’re also open to listening to others and engaging in discussions to improve things. It might seem like a given, but isn’t it actually quite rare? I think this is the strength of Osaka Tech Lab! The unknown future It’s not every day that you get the chance to help set up an office! I decided to join the company because I wanted to have such an experience. Whenever I raise my hand and say, "I want to try this," the response is always "Go ahead, it’s all yours!" Of course, it is not a free-for-all, but as long as I set clear goals and outline the steps, I'm encouraged to take on new challenges. Here's one example: We are organizing information-sharing meetings at The Osaka Tech Lab with the following objectives: To enhance communication among Osaka Tech Lab members To understand what our colleagues are working on and strengthen cross-functional connections To share team and individual experiences and inspire new initiatives Here's what we're doing: A group introduction An LT (Lightning Talk) A discussion to make Osaka Tech Lab better In January 2023, as the members had grown closer, we hosted the third session, inviting managers from various groups who are usually based at other locations. Talk, talk and talk more: Osaka Tech Lab's first step in shaping the future We listened to the managers’ expectations for Osaka Tech Lab, and each team member shared their thoughts. Looking back, I think this was a significant first step. At first, these sessions started with just 10 members. But as people invited others, it expanded to include other locations. Now, the number of participants from other locations exceeds the members of Osaka Tech Lab itself. While we occasionally meet members from other locations during business trips, these information-sharing sessions have also created connections with members we normally wouldn't interact with. Osaka Tech Lab's Ambition Each of us is currently working on Tokyo-based projects, spending our days in a fast-paced, stimulating environment. One day... Someday... We hope to launch, develop, and operate a service entirely from Osaka Tech Lab. It seems many of us secretly share this dream. I'm excited for the unseen future that lies ahead! Conclusion So, what do you think? Why not join us in building the Osaka Tech Lab together toward an exciting, unknown future? We look forward to your application! KINTO Technologies Recruitment TOP page Wantedly
こんにちは! KINTOテクノロジーズ(以下、KTC)の生成AI活用PJTで生成AIエバンジェリストをしている和田( @cognac_n )です。 さて、KTCでは、様々なシーンでの生成AI活用が進んでいます。例えば @ card @ card @ card :::details その他、生成AI関連のKTCテックブログ @ card @ card @ card @ card @ card ::: エンジニアも非エンジニアも、自分のロールや業務に合わせた生成AI活用をしていますね。生成AI活用PJTはこのように「だれもが当たり前に生成AIを活用している企業」を目指して活動をしてきました。 今回はその取り組みについてご紹介します。 1. はじめに、生成AI活用PJTの紹介 社内の生成AI活用を促進するため、2024年1月に立ち上げられた組織です。 生成AI活用PJTは現在、主に3つの機能を持っています。 PJTが持つ、3つの重点機能 生成AI活用PJTの機能 これらは独立した機能ではなく 良いアイデアを生み出す 実現の目処をつける 実装してデリバリーする 事例展開 という、生成AIがあらゆるシーンで活用されるためのサイクルを加速させることを目的としています。 生成AI活用PJTの機能とサイクル 今回はその中でも、 教育・研修 を中心に取り組みの紹介をしていきます。 2. 教育・研修体系の基礎となる考え方 「だれもが当たり前に生成AIを活用している企業」を具体化するために、3つの考え方を採用しています。 生成AIは特定のスペシャリストだけのものではありません 各自の役割に応じた「最適な活用レベル」があります 基礎から専門まで、段階的な学習を重視します 研修体系の基礎となる考え方 KTCでは複数の講師が様々なトピックについて研修をしています。また、受講者にはエンジニアも非エンジニアも、多様な業務に関わる人が含まれます。そのような状況下でもブレのない高品質な研修を提供するために、基礎的な考え方を共通認識として持つに至りました。 最初からこの考え方が固まっていたわけではなく、社内からのフィードバックをもとに改善を繰り返す中で自然と生まれた考え方です。 3. 実施している研修体系 3つの考え方をベースとして、段階的かつ体系的な研修プログラムを展開しています。 研修名 想定受講者 内容 初級 全社員 生成AIやプロンプトエンジニアリングの基礎知識。全ての基本となる最初の一歩 事例紹介 全社員 社内外の活用事例紹介。良い事例を取り込み、自らアレンジする力を身につける 事務生産性向上 各部から選抜(アンバサダー制) 生成AIをツールとして使いこなし、業務価値を生み出す。生成AIの活用を前提とした業務プロセスの改革を目指す。社内の生成AI活用推進者・伝道師となる ジェネラリスト システム開発関係者 生成AIを用いたシステム開発の勘所を学ぶ。技術の目利きや、価値の創出/検証力を身につける エンジニアリング 実装を行うエンジニア 生成AIを用いたシステム開発の実装力を身につける。価値を実現するための具体的知識と経験を得る それぞれの道のりにおいて、目指すべき生成AI活用のレベルを独自に定義しています。 4. 生まれ始めている価値 エンジニアの変化 既存システムへの生成AI機能の追加提案 生成AIを活用した新規サービスの企画提案 生成AIを活用した業務効率化ツールの自主的な開発 GitHub Copilotなどの生成AIツールの高度な活用 非エンジニアの変化 日常業務における積極的な生成AI活用 生成AIの支援によるテクニカルコミュニケーションの向上 簡易的なツール開発への挑戦 冒頭にブログを紹介した通り、研修を受けた社員が、それぞれの役割や業務の中で生成AIの価値を発揮し始めています。日常業務、コミュニケーション、システム開発・・・ありとあらゆるシーンで生成AIによる効率化、価値の向上が行われています。 我々に届く相談も「何ができるかわからない」ようなものから「やってみた!さらに良くするにはどうしたらいい?」「きっとこんなことができると思うので協力してほしい」と変化してきています。生成AIリテラシーが身についているからこそ恐れずに「まず試す」ことができ、「こんなことができそう(そしてそれは価値がある)」というセンスが身につきます。 5. 今後の展望 生成AI技術は日進月歩で進化しています。「当たり前の活用」が実現し始めているKTC/KINTOですが、この「当たり前」の水準にゴールはありません。 「だれもが当たり前に生成AIを活用している企業を目指して」 これからも取り組みを続けていきます! We Are Hiring! KINTOテクノロジーズでは、事業における生成AIの活用を推進する仲間を探しています。まずは気軽にカジュアル面談からの対応も可能です。少しでも興味のある方は以下のリンク、または XのDM などからご連絡ください。お待ちしております!! @ card ここまでお読みいただき、ありがとうございました!
この記事は 技術広報カレンダー2024 の23日目の記事です🎅🎄 はじめに こんにちは!リナ( @chimrindayo )です。 KINTOテクノロジーズで、エンジニアとして モビリティマーケット の開発運用と技術広報を兼務しています。 さて、KINTOテクノロジーズは2022年7月にテックブログを開設し、執筆者及び読者のみなさんのおかげで今日までテックブログが運営できています。いつもありがとうございます! 中でもアドベントカレンダーは、テックブログの一大イベントです🎄 そんな一大イベントで、今年はついに シリーズ4まで計100記事 を投稿できました👏(すごい) こうして100記事リリースできるようになるまで、技術広報のメンバーはさまざまな工夫に日々取り組んできました。今回はアドベントカレンダーを100記事リリースできるまでの軌跡と工夫の一部をご紹介したいと思います。 KINTOテクノロジーズ アドベントカレンダーの軌跡 KINTOテクノロジーズのアドベントカレンダーは、有志のメンバーによってテックブログ開設前の2021年に開始しました。そして翌年、2022年4月にテックブログ運用プロジェクト(現:技術広報グループ)という有志のチームが発足し、2022年7月にテックブログを開設しています。 テックブログ開設以降は毎年欠かさずにアドベントカレンダーを投稿し、25記事ずつ投稿数を増やしています⤴️ 年 URL 投稿数 2021 KINTO Technologies - トヨタ車のサブスク「KINTO」開発中! Advent Calendar 2021 24 2022 4月:テックブログ運用チーム発足 7月:テックブログ開設🎉 - 2022 KINTOテクノロジーズ Advent Calendar 2022 KINTOテクノロジーズ グループ紹介 Advent Calendar 2022 50 2023 KINTOテクノロジーズ Advent Calendar 2023 75 2024 KINTOテクノロジーズ Advent Calendar 2024 100 では私たちが記事を増やすためにどんな工夫に取り組んできたのか、各年ごとにふりかえっていきます。 テックブログ開設初期〜継続期で各フェーズに合わせて工夫を凝らしているため、これからテックブログを開設する or したい方、現在テックブログを運用しているが執筆者が増えなくて悩んでいる方のご参考になれば嬉しいです🙌 2022年 アドベントカレンダーの工夫 まずはじめに、テックブログ開設初期の工夫点です。 ふりかえってみると開設初期は、テックブログを執筆してもらうために 各個人にアプローチ をしていたのが大きな特徴だと思います。では具体的な内容をご紹介します。 執筆者への感謝のスタンスをルールにする まずはじめにやったことが「執筆者への感謝」をレビュアーのスタンスとしてルールにすることです。 このルールは 中西さん を中心に有志のメンバーで決めていきました。 そして、レビュアーになり得るマネージャーやリーダーに全社会議や1on1などの場で依頼したり、テックブログのPRテンプレートに書くことで執筆者に感謝するマインドを伝え続けています。 テックブログのPRテンプレートから抜粋 今でも執筆に感謝するということは、技術広報チーム全員が大切にしています。 全てのマネージャーに記事を書いてもらう 次にやったことは、各部署のマネージャーに記事を書いてもらうことです。 テックブログ開設初期は、「テックブログって何?どうしてやるの?」と思っている社内のメンバーも少なくありませんでした。よって、まずは各グループをリードしているマネージャー1人1人にテックブログの必要性を理解してもらうこと、そして全社にテックブログを広めるためには上位レイヤーの人から執筆する必要があると考え、マネージャーのみなさんと30分ほどのミーティングをしたうえで実現したのが「KINTOテクノロジーズ グループ紹介」です。 https://qiita.com/advent-calendar/2022/kinto-technologies-introduction 「社員・オフィスの様子をみんなに知ってもらう」をテーマに、各グループのマネージャーおよびリーダがアドベントカレンダーを執筆しました。 クリスマスには副社長の景山さんが「2022年振り返り&2023年展望」と言う記事で締めくくっています🎅 2022年以降クリスマスは景山さんの記事が毎年恒例になり、採用候補者の方や社員がほぼ全員目を通しています。 KINTOテクノロジーズが どのような組織で この1年どんなことをやってきたか 今後1年どんなことをやっていくのか がこの記事に凝縮されていて、組織の道しるべとなるような記事です。 景山さんが年に1度力を入れて執筆しています。 振り返り&展望の記事はこちら👇 https://blog.kinto-technologies.com/posts/2022-12-25-LookBack2022/ 記事の構成を一緒に考える 主に記事の執筆意欲はあるが技術記事を執筆した経験がなく、何を書いたらいいか相談したい人向けに30分程度で記事の構成を一緒に考えるミーティングを実施しています。 技術広報のメンバーが執筆者にインタビューする形式で 今までどんな業務をしていたのか 各プロジェクトの失敗/成功談と改善策 技術スタック などをヒアリングして、技術広報のメンバーがインタビュー内容を聞きながらその場で記事の構成を組み立てていきます。一通りインタビューが終わったら、作成した記事の構成をその場で執筆者の方に見ていただきます。すると、ほとんどの人に「思ったより簡単書けるかも!」と言っていただけます。 2022年のテックブログを開設した当時は、この取り組みを執筆者全員に行っていました。 (今考えたら力技すぎる...!ですが全員とコミュニケーションを取るこの力技こそが、私たちKINTOテクノロジーズの技術広報の強みであると考えています。よくこの話を他社の技術広報の方に共有すると「素晴らしい取り組み」と評価をいただけることが多いです。) 全員に行っていた理由はいくつかあります。 主な理由は、 テックブログの必要性をみんなに理解してもらいたい テックブログを書くことのハードルを下げたい 誰が何の業務を担当しているのか知りたい 気軽になんでも相談してもらえる関係を築く などです。 しかし、最も大事だと考えているのが、みなさんの 業務に価値があると再認識 してもらうことです。 実際に当時みなさんと対面でミーティングをした時に「書いてもいいけど、自分の業務は普通のことだからブログにするほどじゃないよ」と言う人が多くいました。 いやいや普通でいいんです! 絶対に同じように悩んでいる人が世界に1人か2人いて、その人に届けばいいんです。 誰か1人に届けば、誰か1人が面白いと思えば、十分にあなたの業務をテックブログ発信する価値があります。 だから一緒に執筆してみましょう!! というのを執筆者1人1人に伝え続けました。 この考えは現在でも技術広報チームが大事にしており、先日リリースされたブログでもご紹介しています👇 https://blog.kinto-technologies.com/posts/2024-12-11-gekishin/ このように記事の構成を一緒に考えるだけでなく、執筆者がこれから書こうとしている記事の内容に自信を持ってもらうことが重要であると考えています。 2023年 アドベントカレンダーの工夫 テックブログの開設初期は、1人1人と会話することで「まずはテックブログを書いてもらう」ための工夫を中心に取り組んできました。 次に2023年はいかにテックブログを 継続 して書いてもらうかに着目し、さらに執筆者を増やすための取り組みを実践しています。 インプットを支援する 2023年まず技術広報のメンバーで実施したことは、 Udemy Business の導入支援です。 なぜ導入を支援したかというと、そもそもテックブログへのアウトプットは知識やスキルのインプットがなければできないと考えているためです。したがって、Udemyのアカウント登録条件を「テックブログを書くこと」にしています。 実際にUdemyを受講して記事を書いているメンバーがいます👇 https://blog.kinto-technologies.com/posts/2024-08-30-udemy-kotlin-coroutines-and-flow/ またその後2024年にUdemyの導入支援以外に技術広報で知識のインプットからアウトプットまで包括的にサポートできるように「学びの道の駅」というチームが発足しています。 学びの道の駅チーム発足の経緯はこちら👇 https://blog.kinto-technologies.com/posts/2024-04-23_%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F/ 入社エントリを書いてもらう そして2023年10月から入社エントリを開始しました。 これから入社する人に会社の雰囲気をわかってもらえたら嬉しい テックブログへのアウトプットへの抵抗がなくなってほしい 入社同期の横の繋がりを作れたら嬉しい という思いから、入社エントリの記入をお願いしています。 入社時に誰もがテックブログを書くことで、テックブログの書き方や書くこと自体のハードルを下げること、そしてテックブログを書くことが当たり前になることを願っています。 https://blog.kinto-technologies.com/posts/2024-01-01-newcomers-introduction/ また、この入社エントリは入社メンバー1人1人に執筆してもらうとして技術広報メンバーで企画を進めていたのですが、10月入社の Ryommさん が「共同執筆したい!」と提案してくれたのをきっかけに1記事を共同で執筆する流れができました。 入社エントリの例のように技術広報グループのメンバーだけで企画を決めて進行するだけでなく、社員全員でアウトプットするカルチャーを創りあげています。 チーム単位で執筆を依頼する 次に、アドベントカレンダーをプロジェクト・部署単位のチームで執筆してもらう企画を実践しました。 5グループ・5名ずつ、計25日分執筆しています。 新車サブスクサイトのリニューアル アジャイル開発 KINTO FACTORY QA Diversity & Inclusion テックブログを日頃業務をともにしているチームのメンバーみんなで書くことによって、執筆者がテックブログのレビュー依頼や執筆の相談がチーム内でよりやりやすくだろうと考えたためです。 そしてチームのメンバーと一緒に、また共通のトピックで記事を執筆したことは、テックブログは読み手である社内のメンバーにとっては身近なコンテンツになるため、テックブログを読むきっかけになったり、SNSでブログを共有するきっかけになると考えました。 実際の企画はこちらのブログで紹介しています👇 https://blog.kinto-technologies.com/posts/2023-11-20-advent-calendar/#2.-%E8%A8%98%E4%BA%8B%E3%83%AA%E3%83%AC%E3%83%BC 2024年 アドベントカレンダーの工夫 最後に今年実践した工夫を紹介します。2024年はさらなる テックブログの拡張と品質向上 をテーマに取り組んできました。 テックブログの勉強会を開催する 2024年8月に技術広報のメンバーである p2skさん による社内向けのテックブログの勉強会を実施しました。 「テックブログの振り返りと発信力向上のためのベストプラクティス」というテーマで、KINTOテクノロジーズのテックブログ全記事を読んだ上で、Goodポイントや記事をよりよくするためのアイディアを共有しています。 社内の勉強会参加者からは「モチベーションが上がった!」「記事が書いてみたくなった」「アドベントカレンダー頑張ろう!」などの声があがり大変好評でした。 勉強会の内容は、非常に濃く本記事では割愛します。 企画運営を公募する 例年は技術広報のメンバーでアドベントカレンダーを企画・リードしていたのですが、今年は有志のメンバーを募集しました。狙いは、技術広報グループ以外の新しいメンバーに運営に入ってもらうことで、技術広報が今までアプローチできていなかったメンバーにリーチすることです。 ![](/assets/blog/authors/rina.k/100article/member.png =600x) 最大5名ほど集まったら嬉しいなと思っていたのですが、手を挙げてくれたのは1名でした。 その手を挙げてくれた1名が naka_shimaさん です。 naka_shimaさんが挙手してくれたことがきっかけで後述の部署単位でのシリーズの追加に繋がったと思います👏 部署単位でシリーズを埋める 2024年はグループ(部署)単位でシリーズを埋めました。 シリーズというのは、1~25日のアドベントカレンダー全日程 計25記事投稿することを指しています。 対象となったグループは、運営として挙手してくれたnaka_shimaさんが所属するモバイルグループと技術広報グループです。 モバイルグループ 技術広報グループ まずモバイルグループが選出された理由は、今年最も積極的に情報発信を行なっていたグループだからです。iOSDC2024やDroid Kaigiなどのスポンサーイベントの企画運営はもちろん、日頃のテックブログの発信も自発的に行なっていたのが特に目立っていたグループです。そこで「もしかしたら、1~25日全部モバイルグループで埋められるんじゃない・・・?」という無茶振りのもと実現したのが、モバイルグループで1シリーズです🎉 次に技術広報グループで1シリーズ実施することになった理由は、情報発信をリードするチーム自身が積極的にテックブログでのアウトプットを行う姿を見せることが重要だと考えたからです。 こうして ランダムテーマ(有志のメンバーが自由に執筆できる) モバイルグループ 技術広報グループ 翻訳(ローカライゼーションチームによる英日翻訳記事) の計100本をリリースすることができたのです。 今後の展望 テックブログを開設した2022年から2024年現在に至るまでの3年間の工夫をアドベントカレンダーを通してご紹介しました。みなさんのご協力とこれまでの工夫が2024年のアドベントカレンダー100本リリースに繋がっていると思います! 2025年は 執筆のしやすさ 執筆のモチベーション維持 を中心に工夫に取り組んでいきたいと思います。 2024年もありがとうございました☺️
こんにちは(=゚ω゚)ノ KINTOテクノロジーズで採用担当をしている たけの ひかる @t_hikarutaaaan です! KTC歴はもう少しで3年ですが、人事としてはまだ1年目のぴよぴよです。 今回はそんなぴよぴよ人事の私が 初めてイベントの PM をして実践したこと、学んだことを共有したいと思います! イベント概要 イベント名: プロダクトヒストリーカンファレンス 主催:株式会社 YOUTRUST 実施日:2024 年 11 月 30 日(土)10:00-20:00 場所:TODA HALL & CONFERENCE TOKYO @ card はじまり 開催日から3ヶ月切ったタイミングでKTCがゴールドスポンサーとして協賛することが決定! 「よし、決裁とれたぜ!!」って安心し… それと同時に「あれ? なにからやるんだ…?」ってなっていた私… そんな私に 神の手 が °˖✧◝(⁰▿⁰)◜✧˖° 技術広報Gのマネージャーである きんちゃん が他イベントでのSlackチャンネルを共有してくださり、 「こうゆう動きをしたらいいんだよ!」とアドバイスをくださいました…( ;∀;) 実践で吸収するタイプの私にとって、一番わかりやすい情報を提供してくれました。感謝です。 次にしたこと 「よし、ほなSlackチャンネル見ていこか」 … (ㆆωㆆ)ジー 「うーん、なんかわからんけど、まずは関係者集めてMTGや!!!」 MTG セット して 社内の Confluence を漁る それっぽい 議事録 を発見 コピー! ! ![Confluence のキャプチャ](/assets/blog/authors/takeno/prhs/prhs1.png =600x) 過去資料をコピー! なんだかんだで 社内の Confluence と過去のイベント関連の Slack チャンネル を漁ることで大体のことが解決できました! ! 改めて振り返ってみても、 ナレッジを溜めやすい環境が整っているんだな~ と感心と感謝の気持ちが沢山です…タスカッタ… いざ本番当日 前日にブースの準備は終えていたので 09:30 に会場入り 。 ![ブース準備の様子](/assets/blog/authors/takeno/prhs/prhs2.png =600x) 簡単なセッティングを終えたブース 少し落ち着いたところで開場時間が近づき…と思ったら… 「協賛企業みんなで円陣を組みましょう!!」 と Σ(・ω・ノ)ノ! しっかり両サイドの方と肩を組ませて頂きました。 さて、イベントスタート! 徐々に人も増えてきて、 お昼時にはブースに人だかり! ![ブースの様子](/assets/blog/authors/takeno/prhs/prhs3.png =600x) たくさんの方が興味を持ってくださいました! 本当にずーーーーーっと喋ってました! ! ブースでアンケートを実施していたのですが、予想より KTC のことを知っている方が多くて とても嬉しかったです。 一番多かった反応: 「色んなイベントでお見かけしました!!」 コツコツと沢山のイベントに参加してきた社員みんなの力を再認識しました! ! ![おなじみKTCのくもびぃ](/assets/blog/authors/takeno/prhs/prhs4.jpg =600x) KTCのマスコット「くもびぃ」も活躍中 景山さん登壇 今回は 取締役副社長の景山均 と 株式会社Luup CTO 岡田直道氏 によるクロストークを実施! 「テクノロジーの活用でモビリティの新常識をつくる 2 社が描く未来」をテーマに 創業・設立時の想い 各フェーズにおける開発組織の変遷 制約の多いプロダクト開発に取り組む難しさと面白さ などなど、両社の挑戦についてざっくばらんにお話いただきました。 ![登壇の様子](/assets/blog/authors/takeno/prhs/prhs5.jpg =600x) クロストーク 学んだこと 運営MTGはアジェンダ決めて、必要なタイミングで実施が良い 週次固定の定例スタイルも試してみると良いかも? くもびぃ人気 やっぱり可愛いは最強だった くもびぃは KINTO 公式マスコットキャラクターです @ card 社内にナレッジが溜まっている環境が強すぎる Slack のオープンチャンネル文化のおかげで探しやすい 経験値高い社員が集まりすぎ 後夜祭を予定していてよかった! イベント当日だけで繋がりを終わらせず、次に繋げやすい。 後夜祭には25~30名の方に参加頂きました! ![後夜祭の様子](/assets/blog/authors/takeno/prhs/prhs6.jpg =600x) 後夜祭には他社の方も参加してくださいました X Mile さん @xmile_product 、 アスエネ さん @asuene_inc_ 、 note さん @note_corp 、ありがとうございました!! 次のイベントに活かしたいこと ブースに来た人に “どんな体験をしてほしいのか” をもっと考えたい ブースのセッティングやノベルティのアレンジに活かしたい 実際にお話した方限定で選考ステップをスキップできるような“express card”を用意したい キャリアSNSであるYOUTRUSTさんのイベントでもあることから転職意欲がある人も多かった 優秀な方を確実に選考に乗ってもらえるようなフローを作っても良かったかも KPI を立てたい 振り返って ビビりな私は 「何が起きるかわかんない…!」 とドキドキしながらスタートしましたが、 結果、 「なんとかなったーーー!」 と大きな問題なく終了できたのは、 様々な方面から先回りして「これ決めたほうがいいんじゃない?」「これどうなってる?」と誘導してくれたり 準備だけでなく当日も臨機応変に対応してくれた運営メンバーのおかげでした!! 本当にありがとうございました!!! ![集合写真](/assets/blog/authors/takeno/prhs/prhs7.jpg =600x) みんなでくもびぃ着用!
This article is the entry for day [24] in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hi, I'm Rasel , currently working in KINTO Technologies Corporation as an Android Engineer. Today, I’ll briefly introduce the approach to testing in Kotlin Multiplatform (KMP). Cross-platform testing in KMP allows you to ensure the reliability of shared code across Android and iOS. While simple tests are a great start, many applications require more complex setups to verify business logic, handle async functions, and test interdependencies between components. This post explores advanced testing scenarios, including handling async code, testing dependencies, using parameterized tests, and structuring complex setups. Let’s dig into the details! Setting Up for Advanced Cross-Platform Testing Configure Common Test Dependencies Here’s an enhanced setup with testing libraries for assertions, coroutines, and mocking: // In shared module’s build.gradle.kts plugins { id("dev.mokkery") version "2.5.1" // Mocking library for multiplatform } sourceSets { val androidInstrumentedTest by getting { dependencies { implementation(libs.core.ktx.test) implementation(libs.androidx.test.junit) implementation(libs.androidx.espresso.core) implementation(libs.kotlin.test) } } commonTest.dependencies { implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlin.test.annotations.common) } iosTest.dependencies { implementation(libs.kotlin.test) } } With this setup, you’ll be equipped to handle async tests, and dependency mocking necessary for testing. Basic Testing in KMP KMP makes it easy to write shared tests in commonTest while allowing platform-specific tests in androidTest and iosTest . Here's an overview of how you can get started: class GrepTest { companion object { val sampleData = listOf( "123 abc", "abc 123", "123 ABC", "ABC 123", ) } @Test fun shouldFindMatches() { val results = mutableListOf<String>() // Let's get the matching results with our global function grep which is defined in commonMain module grep(sampleData, "[a-z]+") { results.add(it) } // Check results with expectations assertEquals(2, results.size) for (result in results) { assertContains(result, "abc") } } } With this simple test in commonTest , you can verify the shared logic in the commonMain module across platforms. Advanced Test Scenarios 1. Testing Asynchronous Functions with Coroutines Many shared KMP projects use coroutines for asynchronous work. Testing coroutines effectively requires using kotlinx-coroutines-test , which provides tools for controlling and advancing coroutine execution in tests. Here’s a sample scenario: Imagine you have a UserRepository that fetches user data asynchronously. We’ll write a test that controls the coroutine flow to simulate data retrieval. @OptIn(ExperimentalCoroutinesApi::class) class UserRepositoryTest { private val testDispatcher = StandardTestDispatcher() private val userRepository = UserRepository(testDispatcher) @BeforeTest fun setup() { Dispatchers.setMain(testDispatcher) // Set test dispatcher as main } @AfterTest fun tearDown() { Dispatchers.resetMain() // Reset main dispatcher } @Test fun `fetch user successfully`() = runTest { val user = userRepository.fetchUser("123") assertEquals("John Doe", user.name) } } In this example: Dispatchers.setMain(testDispatcher) replaces the default dispatcher, allowing us to control coroutine execution. runTest {} automatically handles coroutine setup and cleanup, making it easier to manage suspending functions. We can control the flow within runTest to simulate delays and other asynchronous behaviors. 2. Using Mocks and Verifying Interactions Mocking dependencies in a multiplatform setup can streamline testing complex interactions between classes. Here, we’ll mock an API client to simulate a network call and verify interactions with the repository. class NewsRepositoryTest { private val testDispatcher = StandardTestDispatcher() private val apiService = mock<ApiService> { everySuspend { getNews(defaultSectionName) } returns TopStoryResponse(mockNewsArticles) } private val repository = NewsRepository(apiService, testDispatcher) @BeforeTest fun setup() { Dispatchers.setMain(testDispatcher) // Set test dispatcher as main } @AfterTest fun tearDown() { Dispatchers.resetMain() // Reset main dispatcher } @Test fun `fetch news list for home section successfully`() = runTest { val articleList: List<Article> = repository.fetchNews() assertEquals(mockNewsArticles.size, articleList.size) assertEquals(mockNewsArticles.first(), articleList.first()) assertEquals(mockNewsArticles.last(), articleList.last()) verifySuspend { apiService.getNews(defaultSectionName) } } } const val defaultSectionName = "home" In this example: mock<ApiService>() allows the mock to return default values without explicit behavior definitions. verifySuspend checks that getNews() was called properly, ensuring the correct flow in the repository. everySuspend sets the mock data that will be returned in every call of getNews() . getNews() method of NewsRepository fetches data from ApiService as TopStoryResponse and returns the result as List<Article> 3. Parameterized Tests for Different Input Scenarios KMP currently doesn't natively support parameterized tests in the same way libraries like JUnit5 do. However, there are workarounds to achieve parameterized-like testing behavior using Kotlin's features, allowing you to test functions with various inputs in a concise, reusable way. Here’s an example of testing different date formats for a DateUtils function. class DateUtilsTest { private val testCases = listOf( Triple("2024-11-18T00:00:00", "dd MMM yyyy", "18 Nov 2024"), Triple("2024-11-18T15:34:00", "hh:mm", "03:34"), Triple("2024-11-18T15:34:00", "dd MMM yyyy hh:mma", "18 Nov 2024 03:34PM"), ) @Test fun `check date format`() { testCases.forEach { (dateString, outputFormat, expectedOutput) -> val actualOutput = formatDatetime(dateString = dateString, inputFormat = "yyyy-MM-dd'T'HH:mm:ss", outputFormat = outputFormat) assertEquals(expectedOutput, actualOutput, "Failed for date $dateString and output format $outputFormat") } } } [!NOTE] Here, formatDatetime() is a global function defined inside DateUtils to format datetime string. With this example: We define expected results for each input, making it easy to expand test coverage without duplicating code. Works in both androidTest and iosTest without relying on platform-specific test libraries. Platform-Specific Tests with Complex Setups Some test cases require using platform-specific APIs or behaviors, especially when working with Android’s SharedPreferences or iOS’s NSUserDefaults . Here’s a breakdown for both platforms. Android-Specific Test: Testing SharedPreferences In Android, you may need to test data storage with SharedPreferences . Here’s a setup using AndroidX’s ApplicationProvider to access a test context. @RunWith(AndroidJUnit4::class) class SharedPreferencesTest { private lateinit var sharedPreferences: SharedPreferences private val expectedAppStartCount = 10 @BeforeTest fun setup() { val context = ApplicationProvider.getApplicationContext<Context>() sharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) } @AfterTest fun tearDown() { sharedPreferences.edit().clear().apply() } @Test fun checkAppStartCountStoresCorrectly() { sharedPreferences.edit().putInt(KEY_APP_START_COUNT, expectedAppStartCount).apply() val actualAppStartCount = sharedPreferences.getInt(KEY_APP_START_COUNT, -1) assertEquals(expectedAppStartCount, actualAppStartCount) } companion object { private const val SHARED_PREF_NAME = "test_prefs" private const val KEY_APP_START_COUNT = "app_start_count" } } This platform dependent test needs to be placed inside androidInstrumentedTest and it will require an Emulator or real Android device to run. iOS-Specific Test: Testing NSUserDefaults In iOS, similar logic can be tested with NSUserDefaults . You’d typically write this test in iosTest , as shown below. class NSUserDefaultsTest { private lateinit var userDefaults: NSUserDefaults private val expectedAppStartCount = 10 @BeforeTest fun setup() { userDefaults = NSUserDefaults.standardUserDefaults() } @AfterTest fun teardown() { userDefaults.removeObjectForKey(KEY_APP_START_COUNT) // Clean up after test } @Test fun `test app start count stores correctly in NSUserDefaults`() { userDefaults.setObject(expectedAppStartCount, forKey = KEY_APP_START_COUNT) val actualAppStartCount = userDefaults.integerForKey(KEY_APP_START_COUNT) assertEquals(expectedAppStartCount, actualAppStartCount.toInt()) } companion object { private const val KEY_APP_START_COUNT = "app_start_count" } } Complex Test Setups: Testing Interdependencies Advanced tests sometimes need to simulate multiple dependencies interacting with each other. Here’s an example involving a repository that depends on both an API client and a local cache. class ComplexRepositoryTest { private val apiService = mock<ApiService>() private val localCache = mock<LocalCache>() private val repository = ComplexRepository(apiService, localCache) @Test fun `fetch data from local cache`() = runTest { everySuspend { localCache.getArticles() } returns mockNewsArticles val data = repository.getNewsArticles() assertEquals(mockNewsArticles.size, data.size) verifySuspend { localCache.getArticles() } } @Test fun `fetch data from remote source`() = runTest { everySuspend { localCache.getArticles() } returns emptyList() everySuspend { apiService.getNews(defaultSectionName) } returns TopStoryResponse(mockNewsArticles) everySuspend { localCache.insertAllArticles(mockNewsArticles) } returns Unit val data = repository.getNewsArticles() assertEquals(mockNewsArticles.size, data.size) verifySuspend { localCache.getArticles() apiService.getNews(defaultSectionName) localCache.insertAllArticles(mockNewsArticles) } } } In this example: The repository checks the cache first, and only calls the API if the cache is empty. By verifying interactions, we ensure the repository follows the intended flow and doesn’t call the API unnecessarily. With all those above, you have done writing tests for common, Android, and iOS implementations. You can now run those test individually or can also run all tests at once using gradle task named allTests , by which, every test in your project will be run with the corresponding test runner. ![Gradle task allTests](/assets/blog/authors/ahsan_rasel/2024-12-24-testing-kmp/gradle-all-test-task.png =800x) With every run of tests, HTML reports will be generated in the shared/build/reports/ directory: ![Test report](/assets/blog/authors/ahsan_rasel/2024-12-24-testing-kmp/test-report.png =550x) In this way, we can proceed with our KMP journey while ensuring app quality with properly tuned tests. Best Practices for Complex KMP Tests Isolate Test Setup : Create helper functions or classes to set up mocks, reducing repetitive code. Control Asynchronous Behavior : Use kotlinx-coroutines-test to control coroutine timing and simulate delays or network responses. Mix Cross-Platform and Platform-Specific Tests : Use platform-specific tests for native behaviors and cross-platform tests for shared logic. Monitor Test Coverage : Ensure comprehensive coverage, especially for complex flows with dependencies. Conclusion Cross-platform testing in Kotlin Multiplatform can handle simple and complex test scenarios, providing robust tools to ensure code reliability across Android and iOS. By setting up reusable tests, controlling async behavior, and isolating dependencies, you can write advanced tests that verify critical business logic and enhance overall code quality. With these techniques, your KMP project will be well-prepared for consistent, reliable performance across platforms, delivering a seamless experience for users everywhere. Happy KMPing!
1. はじめに こんにちは、共通サービス開発グループの鳥居( @yu_torii )です。主にバックエンド/フロントエンド領域を担当しています。 KINTO 会員プラットフォーム開発チームでフロントエンドエンジニアを務めながら、社内での生成 AI 活用にも携わっています。 本記事では、Slack 上で LLM を活用する社内チャットボット「しぇるぱ」の RAG や Slack リアクションを活用した翻訳機能を紹介します。 しぇるぱは、社内での生成 AI 活用を目指して開発された Slack チャットボットです。 フロントエンド開発を省略して素早く社内に展開し、社員が普段使う Slack 上で生成 AI を自然に利用できる環境を目標としています。 「しぇるぱ」という名前はエベレスト登山のガイドとして知られるシェルパ族に由来し、彼らが登山者を支える頼れる存在であるように、しぇるぱもまた社内の業務効率の向上や情報共有の円滑化のための頼れるパートナーでありたいという願いが込められています。 ちなみに、バナーの「しぇるぱ」さんはクリエイティブグループの方が KINTO テクノジーズ全体会議(KTC 超本部会)に際してサプライズで描いてくれたものです。ありがとうございます! この取り組みは、生成 AI 活用プロジェクトを推進する和田さん( @cognac_n )との協力により進められました。RAG パイプラインの導入、ローカル開発環境の整備、Slack 絵文字リアクションを活用した翻訳・要約機能など、多角的な改善を行い、社内での生成 AI 利活用を推進しています。 :::details 和田さんの記事はこちら @ card @ card @ card ::: なお、RAG や生成 AI 技術そのものの詳細解説は省き、実装や機能追加の過程を中心にまとめています。 また、しぇるぱの LLM は Azure OpenAI を利用しています。 この記事で得られること Slack Botによる生成AIを活用したチャット機能の利用方法 Slack と LLM を組み合わせ、絵文字リアクションや自然なメッセージ投稿で翻訳・要約を呼び出すチャットボット実装例を紹介 HTMLサニタイズを含むConfluenceデータ取得の工夫 Confluence ドキュメントを Go で取得し、HTML サニタイズにより要約・Embedding に適したテキストを整える方法 FAISS とS3を使った簡易的なRAGパイプライン導入 FAISS インデックスと S3 を活用した簡易 RAG パイプライン導入の手順と注意点を解説 応答速度はあまり早くないものの、低コストで簡易的な RAG を組み込む際の実装ステップや注意点をまとめます。 :::message 本記事は、執筆時点での実装状況をもとにしています。改善点が多分に含まれている点をご了承ください。 社内向けのツールであるため、プロダクションレベルのパフォーマンスを必要とした実装ではありません。 AWS SAM でのデプロイや Step Functions のパイプライン構築、GitHub Actions による CI/CD など、開発環境の整備については、本記事では詳細を省略しています。 ::: :::details 目次(展開) 1. はじめに この記事で得られること 2. 全体アーキテクチャの概要 3. Slack Botによる生成AIを活用したチャット機能 「チャット機能」と「リアクション機能」 チャット機能:利用シナリオ リアクション機能:利用シナリオ この構成のメリット 3.5 実際の利用例 3章まとめ 4. 実装方針と内部設計 全体的な処理フロー 条件分岐による機能判定 サニタイズの役割 RAG利用をConfluence参照に限定 拡張性・保守性への考慮 5. コード例と設定ファイルの紹介 5.1 [Go] Slackイベント受信と解析 5.2 [Go] HTMLテキストのサニタイズ 5.3 [Python] LLM問い合わせの具体例 5.4 [Python] RAG検索呼び出し例 5.5 [Python] EmbeddingとFAISSインデックス化 ::: 2. 全体アーキテクチャの概要 ここでは、しぇるぱが生成 AI による回答を返すまでの流れを、「ユーザとの生成 AI チャット機能」と「Confluence ドキュメントをインデックス化する処理」という 2 つの視点で整理します。 ユーザとの生成AIチャット機能の処理 Slackでしぇるぱを呼び出す しぇるぱの呼び出し方は、チャットでの呼び出しと、リアクション絵文字での呼び出しの 2 通りがあります。 チャットでの呼び出しは、チャンネルでメンションを付けて投稿するか、ダイレクトメッセージで直接投稿することで、生成 AI による回答をリクエストできます。 リアクションでの呼び出しは、翻訳・要約用の絵文字リアクションを付けることで、そのメッセージの翻訳や要約をリクエストできます。 Go Slack Bot Lambda Slack イベントを受け取る Bot は Go で実装し、Lambda 上で動作します。 ユーザからの質問やリアクションを受け取り、リクエストの種類に応じて処理を振り分けます。 LLM へのリクエストや、Embedding 済みデータの参照など Azure OpenAI を利用する場合は、ここでリクエストを生成し、Python Lambda へ送信します。 Python LambdaでのLLMへのリクエスト・RAG参照 Python Lambda は機能ごとに分割し、LLM 問い合わせや RAG 参照などを担当します。 Go Lambda からのリクエストを受け取り、LLM への問い合わせや RAG による回答生成を行います。 Slackへの回答返送 生成された回答は、Go Slack Bot Lambda を経由して Slack へ返送します。 絵文字リアクションで呼び出す翻訳・要約機能も、このフローに組み込まれています。 :::details アーキテクチャ図(ユーザからの呼び出しに対応する処理) flowchart LR subgraph "ユーザからの呼び出しに対応する処理" A["User(Slack)"] -->|"質問・絵文字"| B["Go Slack Bot(Lambda)"] B -->|"リクエスト"| C["Python Lambda RAG/LLM"] C -->|"回答"| B B -->|"回答"| A end ::: Confluence ドキュメントをインデックス化する処理 RAG パイプラインを利用するには、Confluence ドキュメントを要約・Embedding しやすい状態に整える必要があります。これらの前処理は StepFunctions でワークフロー化し、定期的かつ自動的に実行しています。 ドキュメント取得・HTMLサニタイズ(Go実装) Go で実装した Lambda が Confluence API からドキュメントを取得し、HTML タグを整理してテキストを扱いやすい状態にします。 サニタイズ済みテキストは JSON として出力します。 要約処理(Go + Python Lambda呼び出し) 要約処理の目的は、テキストを Embedding や RAG で扱いやすい範囲に絞ることです。 Go 実装の Lambda が Azure OpenAI Chat API のリクエスト処理を行う Python Lambda を呼び出し、テキストを短く整え、再び JSON 化します。 FAISSインデックス化とS3保存(Indexer Lambda) 要約後のテキストを Embedding し、FAISS インデックスを生成する Indexer Lambda がインデックスやメタ情報を S3 へ格納します。 これにより、問い合わせ発生時にはインデックス済みデータを即座に参照でき、RAG パイプラインがスムーズに稼働します。 :::details アーキテクチャ図(Confluence ドキュメントのインデックス化フロー) flowchart LR subgraph "インデックス化準備処理 StepFunctions" D["Go Lambda(ドキュメント取得/HTMLサニタイズ)"] D --> E["Go Lambda(要約/Python呼出)"] E --> F["Indexer Lambda(Embedding/FAISS/S3)"] end ::: こうした事前処理と問い合わせ時の処理が組み合わさることで、しぇるぱは Slack 上の簡易な操作で社内特有のナレッジを反映した生成 AI 回答を返せるようになっています。 以降の章では、これらの要素についてさらに詳細を見ていきます。 3. Slack Botによる生成AIを活用したチャット機能 前章でアーキテクチャの全体像を示しましたが、本章では、ユーザが Slack 上で自然な操作を行うだけで得られる機能について、その使い方や有用性に焦点を当てます。 ここでは「何ができるか」「どんなシナリオで役立つか」という利用イメージを示し、実装詳細は次章以降で体系的に紹介します。 「チャット機能」と「リアクション機能」 しぇるぱは、Slack 上での自然な操作を原動力に、多様な生成 AI 機能を提供します。 チャット機能 : 質問を投稿したり、ファイルや画像、外部リンクを貼ったり、特定の形式でメッセージを送ることで、LLM への問い合わせや(特定の場合)RAG 検索が行われ、必要な回答が得られます。 Slack という日常的なツールに機能を統合することで、ユーザは新たな環境やコマンドを学ぶ必要なく、生成 AI を自然に日常業務へ取り込むことができます。 リアクション機能 : 特定の絵文字リアクションをメッセージに付与するだけで翻訳やしぇるぱ発言の削除などをトリガーでき、コマンドレスでの追加操作が可能になります。 チャット機能:利用シナリオ 基本的な質問・回答 普通に質問を投稿するだけで、LLM を通じて回答を取得できます。 利用シーン例: 「このプロジェクトの手順は?」と尋ねれば即座に回答が得られ、スレッド内の履歴や発言者情報を考慮して文脈に沿った応答が可能です。 ファイル・画像・外部リンク参照 ファイルを貼り付けて「要約して」と依頼すれば、ファイル内容を要約。 画像を添付すれば文字起こしや画像内容を踏まえた回答が可能。 外部リンクを貼れば、そのページ内容を整理して LLM 回答に生かせます。 利用シーン例: 会議メモが入ったテキストファイルを「要約して」で短いサマリを得る、画像内文字情報を抜き出す、外部記事リンクを要約するといった、手間を省く活用ができます。 Confluenceページ参照(RAG活用) :confluence: index:Index名 で、社内でのルールや申請方法等のドキュメントが含まれた Confluence ページを RAG 検索。 利用シーン例: 社内申請手順や内部ルールが Confluence にまとまっている場合、必要な情報を即座に引き出し、一般的な回答以上に社内事情に即したアドバイスが得られます。 利用シーン例: プロジェクト独自の手順書や設定値を検索して、その内容を回答に反映。検索しづらい情報を簡単に引き出せます。 リアクション機能:利用シナリオ 特定の絵文字リアクションを付けることで、さらに直感的な機能呼び出しが可能です。 翻訳 : 翻訳用絵文字を付ければ、そのメッセージを指定言語へ翻訳可能です。言語の壁を低くし、コミュニケーションを円滑化します。 削除 不要になったしぇるぱ発言を絵文字 1 つで簡易に削除し、チャンネルを整理できます。 この構成のメリット 自然な操作で活用可能 : ユーザは慣れた Slack 操作(メッセージ投稿、絵文字付与など)だけで生成 AI 機能を使え、学習コストを低減できます。 日常ツールへの統合で生成AI定着 : Slack という普段使うコミュニケーション基盤に機能を溶け込ませることで、生成 AI を身近に利用することが可能です。またリアクション機能は文字入力をすることなく、翻訳や削除などの操作ができるため、手軽に利用できます。 拡張に対応しやすい土台 : 今後、新たなモデルや追加機能を組み込みたくなった場合も、既存のフロー(質問投稿、絵文字操作)に条件を増やすだけで拡張可能です。 3.5 実際の利用例 ここで、しぇるぱが Slack 上でどのように使われているか、実際のシナリオをいくつか紹介します。 例1:画像の認識 メッセージに画像を添付すると、しぇるぱは画像内のテキストを認識し、その内容を回答します。 ![画像の認識](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-context.png =500x) 画像の認識 例2:Confluenceドキュメントに基づく回答 :confluence: という絵文字を使うことで、Confluence ドキュメントを参照した回答を得ることができます。 ![Confluence ドキュメントに基づく回答](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-confluence-rag.png =500x) Confluenceドキュメントに基づく回答 ご覧の通り、しぇるぱはワークフローから呼び出すことも可能で、プロンプトストアのような利用も可能です。 例3:翻訳リアクションでの英訳依頼 ユーザが英訳したいメッセージに翻訳リアクションを付与 英語話者に共有したい日本語メッセージに :sherpa_translate_to_english: といったリアクション絵文字を付けると、そのメッセージのテキストが自動で英訳されます。 ![英語翻訳リアクション](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-translation.png =500x) 英語翻訳リアクション 翻訳内容を過信しないように、自動翻訳であることを複数言語で通知することで、誤解を防ぎます。 また、機能を普及させるために、利用方法も記載しています。 英訳の他に複数言語の翻訳機能を提供しています。 3章まとめ 本章では、「どんな操作で何ができるか」というユーザ視点の使い方に注目しました。 ここで得た利用イメージを基に、次の章以降で実装内容を体系的に示し、Go 製 Bot や Python Lambda の連携方法、Slack イベントの処理や RAG 検索のしくみ、ファイル抽出やサニタイズ処理などの詳細を紹介します。 4. 実装方針と内部設計 この章では、しぇるぱが Slack のメッセージやリアクションを通じて多様な生成 AI 機能を提供するための実装方針と内部設計について説明します。 ここで紹介するのは、あくまで全体的な考え方や役割分担、拡張性への配慮に関する部分です。具体的なコードや設定ファイルは次章以降に記載します。 全体的な処理フロー Slackイベント受信(Go Lambda) Slack で行われるメッセージ投稿、ファイル添付、画像挿入、外部リンク記載、絵文字リアクションなどのイベントは、Slack API 経由で Go 実装の AWS Lambda へ通知されます。 Go 側はこれらのイベントを解析し、ユーザが求める処理(通常のチャット、翻訳、Confluence 参照など)を判定します。 Go側でのテキスト処理・サニタイズ 外部リンクやファイルからテキストを取得し、コンテキストとしてプロンプトに追加します。 外部リンクを参照する場合は table 、 ol 、 ul など意味のあるタグは残し、不要なタグだけを除去してトークンを節約します。 Python側でのLLM問い合わせ・RAG検索 必要な場合、Go 側は LLM 問い合わせ用の Python Lambda(LLM 用)か、RAG 検索用の Python Lambda(Confluence 参照用)を呼び出します。 例えば、 :confluence: が含まれていれば RAG 検索用 Lambda を呼び出し、index が指定されていなければデフォルトインデックスで検索します。 そうでなければ通常は LLM 用 Lambda へテキストを渡し、LLM への問い合わせを行います。 回答返却とSlack表示 Python 側の Lambda が生成した回答を Go 側に戻し、Go 側が Slack へ投稿します。 これにより、ユーザはコマンドを暗記する必要なく、絵文字やキーワード、ファイル添付など日常的な操作だけで高度な機能を利用できます。 条件分岐による機能判定 特定の絵文字( :confluence: など)やキーワード、ファイルや画像の有無、外部リンク存在などにより処理を振り分けます。 新機能を追加する場合は、Go 側で新たな条件を追加して、必要なら対応する Python 側の Lambda(LLM 用、RAG 用など)を呼び出すロジックを増やすことで実現できます。 サニタイズの役割 Go 側でサニタイズし、不要な HTML タグを除去することで、モデルへ渡すテキストを効率的に扱い、トークン消費を抑えます。 table、ol、ul など意味あるタグは残して情報構造を保持し、モデルにとって有用な文脈を損ねないようにしています。 RAG利用をConfluence参照に限定 RAG 検索は :confluence: 指定時のみ利用します。 これにより通常の要約や翻訳、Q&A は LLM 直接問い合わせで済み、RAG 関連ロジックは Confluence 参照時だけ発動します。 Confluence ドキュメントの Embedding 生成や FAISS インデックス更新は StepFunctions で定期的に実行し、問い合わせ時には常に最新インデックスを利用できます。 拡張性・保守性への考慮 絵文字やキーワード、ファイル・画像の有無による条件分岐は、機能を増やす際の変更箇所を少なく保ち、保守性を高めます。 Go 側でテキスト整形やサニタイズ、Python 側で LLM 問い合わせ・RAG 検索を行う役割分担も、コードの見通しを良くし、将来的なモデル切り替えや処理追加を容易にします。 次章では、これらの方針を踏まえた具体的なコードスニペットや設定ファイル例を紹介します。 5. コード例と設定ファイルの紹介 この章では、第 4 章で説明した実装方針や設計上の考え方に基づいて、実装例を要点を絞って紹介します。 本章は次のセクションで構成します。 5.1 Slack イベント受信と解析(Go 側) Slack の Events API を用いてメッセージや絵文字リアクションなどのイベントを受信・解析する方法を示します。 5.2 Go 側でサニタイズ例 外部リンク参照した際の HTML サニタイズ処理の例を示します。 5.3 Python 側 LLM 問い合わせの具体例 LLM(LLM)への問い合わせを行う Lambda のコード例を提示します。 5.4 Python 側 RAG 検索呼び出し例 Confluence 参照など RAG を使う場合の検索呼び出し例を紹介します。 5.5 Python 側 Embedding と FAISS インデックス化 Confluence ドキュメントを定期的に Embedding し、FAISS インデックスを更新する Lambda のコード例を示します。 5.1 [Go] Slackイベント受信と解析 このセクションでは、Slack の Events API を利用して、AWS Lambda 上で Go コードでイベントを受信・解析する基本的な流れを説明します。 Slack 側での設定(OAuth & Permissions、イベント購読)や、 chat.postMessage メソッド利用時に必要なスコープ( chat:write など)の確認方法にも触れ、読者が実装を始めるまでに必要な準備を明確にします。 Slack側での設定手順 App作成とApp IDの確認 : https://api.slack.com/apps で新規 App を作成します。 作成後、Basic Information ページ( https://api.slack.com/apps/APP_ID/general 、 APP_ID は App 固有の ID)で App ID( A で始まる文字列)を確認します。 この App ID は Slack App の識別子であり、後述の OAuth & Permissions や Event Subscriptions ページの URL アクセスに用いることができます。 OAuth & Permissionsでスコープ付与 : 「OAuth & Permissions」ページ( https://api.slack.com/apps/APP_ID/oauth )にアクセスし、Bot Token Scopes に必要なスコープを追加します。 例えば、チャンネルへメッセージ投稿に chat.postMessage メソッドが必要である場合、 https://api.slack.com/methods/chat.postMessage で「Required scopes」を確認すると chat:write が必要であるとわかります。 スコープ付与後、「reinstall your app」をクリックし、ワークスペースに再インストールすると、変更が反映されます。 ![Required scopes の確認](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image.png =500x) Required scopesの確認 ![Scope の設定](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-1.png =500x) Scopeの設定 Events API有効化とイベント購読 : 「Event Subscriptions」ページ( https://api.slack.com/apps/APP_ID/event-subscriptions )で Events API を有効化し、「Request URL」に後述の AWS Lambda エンドポイントを設定します。 message.channels や reaction_added など、購読するイベントを追加します。これにより、対象イベント発生時に Slack が指定 URL へ通知を送るようになります。 ![Event Subscriptions の説明](/assets/blog/authors/torii/2024-12-23_sherpa_slack/images/image-3.png =500x) AWS Lambda側でのイベント受信・解析 Slack 側での設定が完了すると、購読対象のイベントが発生するたびに Slack は API Gateway 経由で Lambda へ POST リクエストを送ります。 ステップ1:Slackイベントの解析 slack-go/slackevents パッケージを用いて、受信した JSON を EventsAPIEvent へ変換します。 これにより、URL 検証や CallbackEvent などイベントタイプを判別しやすくなります。 func parseSlackEvent(body string) (*slackevents.EventsAPIEvent, error) { event, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { return nil, fmt.Errorf("Slackイベントの解析に失敗しました: %w", err) } return &event, nil } @ card ステップ2:URL検証要求対応 初回に Slack は type=url_verification のイベントを送ってきます。この中の challenge をそのまま返すことで Slack は URL 有効性を確認し、その後イベントを通知してくれるようになります。 func handleURLVerification(body string) (events.APIGatewayProxyResponse, error) { var r struct { Challenge string `json:"challenge"` } if err := json.Unmarshal([]byte(body), &r); err != nil { return createErrorResponse(400, err) } return events.APIGatewayProxyResponse{ StatusCode: 200, Body: r.Challenge, }, nil } @ card ステップ3:署名検証・再試行リクエスト無視 Slack はリクエスト署名を付与し、正当性を検証できます(実装は省略)。 また、障害時などに再試行リクエストが送られる場合があり、 X-Slack-Retry-Num ヘッダで再試行を判定して同じイベントを二重処理しないようにできます。 func verifySlackRequest(body string, headers http.Header) error { // 署名検証処理(省略) return nil } func isSlackRetry(headers http.Header) bool { return headers.Get("X-Slack-Retry-Num") != "" } func createIgnoredRetryResponse() (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"message": "Slackの再試行を無視します"}) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, nil } ステップ4:CallbackEvent処理 CallbackEvent は実際のメッセージ投稿やリアクション追加などが含まれます。ここで、 :confluence: が含まれるか、ファイルがあるか、翻訳用絵文字が付いているかなどを判定し、5.2 以降で示すテキスト処理や Python Lambda 呼び出しへ進みます。 // handleCallbackEvent は、コールバックイベントを処理します。(5.1での説明対象) func handleCallbackEvent(ctx context.Context, isOrchestrator bool, event *slackevents.EventsAPIEvent) (events.APIGatewayProxyResponse, error) { innerEvent := event.InnerEvent switch innerEvent.Data.(type) { case *slackevents.AppMentionEvent: // AppMentionEventの場合の処理(5.2で詳細説明) case *slackevents.MessageEvent: // MessageEventの場合の処理(5.2で詳細説明) case *slackevents.ReactionAddedEvent: // ReactionAddedEventの場合の処理(5.2で詳細説明) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: http.StatusOK}, nil } ハンドラ全体のコード例 これらのステップを組み合わせて AWS Lambda ハンドラを定義します。 :::details ハンドラ全体のコード例 func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { event, err := parseSlackEvent(request.Body) if err != nil { return createErrorResponse(400, err) } if event.Type == slackevents.URLVerification { return handleURLVerification(request.Body) } headers := convertToHTTPHeader(request.Headers) err = verifySlackRequest(request.Body, headers) if err != nil { return createErrorResponse(http.StatusUnauthorized, fmt.Errorf("リクエストの検証に失敗しました: %w", err)) } if isSlackRetry(headers) { return createIgnoredRetryResponse() } if event.Type == slackevents.CallbackEvent { return handleCallbackEvent(ctx, event) } return events.APIGatewayProxyResponse{Body: "OK", StatusCode: 200}, nil } func convertToHTTPHeader(headers map[string]string) http.Header { httpHeaders := http.Header{} for key, value := range headers { httpHeaders.Set(key, value) } return httpHeaders } func createErrorResponse(statusCode int, err error) (events.APIGatewayProxyResponse, error) { responseBody, _ := json.Marshal(map[string]string{"error": err.Error()}) return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map[string]string{"Content-Type": "application/json"}, Body: string(responseBody), }, err } ::: 5.1のまとめ このセクションで、Slack App の App ID や OAuth & Permissions でのスコープ付与方法、Event Subscriptions でのイベント購読設定を説明し、Slack イベントの受信・解析プロセス(URL Verification 対応、署名検証、再試行リクエスト無視、CallbackEvent 処理)を示しました。 次の 5.2 以降では、CallbackEvent 処理の具体例や、Go 側でのテキスト処理、Python Lambda への問い合わせ方法などを紹介します。 5.2 [Go] HTMLテキストのサニタイズ 外部リンク参照内容のサニタイズ 外部リンクから取得した HTML テキストには、 script や style など回答に不要なタグが含まれる場合があります。そのまま LLM に渡すと、トークン数が増えてモデルコストが上昇し、回答精度が下がる可能性があります。次のコードでは、 bluemonday パッケージを用いて基本的なサニタイズを行ったうえで、 table や ol 、 ul など重要なタグを残しつつ不要なタグを除去し、読みやすい形に整形します。 addNewlinesForTags 関数を活用し、特定のタグ後に改行を挿入してテキストを整えることで、モデルへの問い合わせ時に必要な情報のみを最適なフォーマットで渡すことが可能になります。 @ card func sanitizeContent(htmlContent string) string { // 基本的なサニタイズ ugcPolicy := bluemonday.UGCPolicy() sanitized := ugcPolicy.Sanitize(htmlContent) // カスタムポリシーで特定タグを許可 customPolicy := bluemonday.NewPolicy() customPolicy.AllowLists() customPolicy.AllowTables() customPolicy.AllowAttrs("href").OnElements("a") // タグごとに改行を追加して可読性向上 formattedContent := addNewlinesForTags(sanitized, "p") // カスタムポリシー適用後の最終サニタイズ finalContent := customPolicy.Sanitize(formattedContent) return finalContent } func addNewlinesForTags(htmlStr string, tags ...string) string { for _, tag := range tags { closeTag := fmt.Sprintf("</%s>", tag) htmlStr = strings.ReplaceAll(htmlStr, closeTag, closeTag+"\n") } return htmlStr } この処理により、モデルへの入力は不要なタグが取り除かれたテキストのみとなり、回答精度とコスト効率が向上します。必要な構造(テーブルや箇条書き)は保持しつつ、特定タグの終了後に改行を挿入することで、モデルが情報を理解しやすい形でコンテキストを提供できます。 5.3 [Python] LLM問い合わせの具体例 以下は、Python で LLM(たとえば Azure OpenAI)へ問い合わせる処理の例です。 OpenAIClientFactory を用いてモデルやエンドポイントを動的に切り替えられるため、複数の Lambda ハンドラ間で共通のクライアント生成処理を再利用できます。 クライアント生成処理 OpenAIClientFactory は api_type や model に応じて Azure OpenAI または OpenAI 用のクライアントを動的に生成します。 環境変数や秘密情報により API キー、エンドポイントを取得するため、モデル変更や設定変更時もコード修正が最小限で済みます。 import openai from shared.secrets import get_secret class OpenAIClientFactory: @staticmethod def create_client(region="eastus2", model="gpt-4o") -> openai.OpenAI: secret = get_secret() api_type = secret.get("openai_api_type", "azure") if api_type == "azure": return openai.AzureOpenAI( api_key=secret.get(f"azure_openai_api_key_{region}"), azure_endpoint=secret.get(f"azure_openai_endpoint_{region}"), api_version=secret.get( f"azure_openai_api_version_{region}", "2024-07-01-preview" ), ) elif api_type == "openai": return openai.OpenAI(api_key=secret.get("openai_api_key")) raise ValueError(f"Invalid api_type: {api_type}") LLM問い合わせ処理 chatCompletionHandler 関数では、HTTP リクエストとして受け取った JSON から messages や model 、 temperature などを取得し、 OpenAIClientFactory で生成したクライアントを用いて LLM に問い合わせます。 レスポンスは JSON 形式で返し、エラー発生時は共通のエラーレスポンス生成関数によって整形されたレスポンスを返します。 import json from typing import Any, Dict, List import openai from openai.types.chat import ChatCompletionMessageParam from shared.openai_client import OpenAIClientFactory def chatCompletionHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: request_body = json.loads(event["body"]) messages: List[ChatCompletionMessageParam] = request_body.get("messages", []) model = request_body.get("model", "gpt-4o") client = OpenAIClientFactory.create_client(model=model) temperature = request_body.get("temperature", 0.7) max_tokens = request_body.get("max_tokens", 4000) response_format = request_body.get("response_format", None) completion = client.chat.completions.create( model=model, stream=False, messages=messages, max_tokens=max_tokens, frequency_penalty=0, presence_penalty=0, temperature=temperature, response_format=response_format, ) return { "statusCode": 200, "body": json.dumps(completion.to_dict()), "headers": { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST", "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", }, } この仕組みにより、異なる Lambda ハンドラでも同様の手順で LLM 問い合わせが可能となり、モデルやエンドポイント変更にも柔軟に対応できる構成となっています。 5.4 [Python] RAG検索呼び出し例 このセクションでは、Python で RAG(Retrieval Augmented Generation)検索を行うための手順を示します。 Confluence ドキュメントなど社内のナレッジをベクトル化し、FAISS インデックスを用いて類似文書検索を行うことで、LLM 回答に特化した情報を組み込むことが可能です。 ここで注目すべき点は faiss ライブラリの取り扱いです。 faiss は非常にサイズが大きく、Lambda Layer の容量制限を超える可能性があるため、通常は EFS を利用するか、Lambda をコンテナ化する必要があります。 今回はその手間を省くため、 setup_faiss 関数により S3 から faiss 関連パッケージをダウンロード・展開し、 sys.path に動的に追加することで faiss を利用可能にしています。 FAISS とは FAISS(Facebook AI Similarity Search)は、Meta(Facebook)製の近似最近傍探索ライブラリであり、類似の画像やテキストを検索するためのインデックスを作成するツールです。 @ card setup_faiss 関数によるFAISSセットアップ FAISS パッケージを Lambda 環境で利用するために、 setup_faiss 関数では次の手順を行います。 ローカル/CI環境でfaissパッケージをビルド・アーカイブ 開発者が GitHub Actions などの CI 環境で faiss-cpu パッケージをインストールし、必要なバイナリを tar.gz 形式でまとめます。 S3へアップロード CI でビルド・アーカイブした faiss_package.tar.gz を S3 にアップロードします。 本番やステージング環境に合わせて、適切なバケットやパスへ格納することで、Lambda 実行時に S3 から動的に取得可能です。 Lambda実行時に setup_faiss で動的ロード Lambda 実行環境上では、 setup_faiss 関数が起動時に S3 から faiss_package.tar.gz をダウンロード・展開し、 sys.path に追加します。 これにより、Lambda コード内で import faiss が可能となり、Embedding 処理で作成したベクトルを高速に検索できます。 GitHub Actions でfaissパッケージをS3へアップロードする例 以下は、 faiss-cpu をインストールし、Lambda で利用できるようにパッケージ化した上で、S3 にアップロードする GitHub Actions の例です。 ここでは、GitHub Actions の Secret や Environment Variables 機能を利用することで、AWS 認証情報や S3 バケット名などをハードコーディングせずに管理しています。 @ card name: Build and Upload FAISS on: workflow_dispatch: inputs: environment: description: デプロイ環境 type: environment default: dev jobs: build-and-upload-faiss: environment: ${{ inputs.environment }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" # 必要なパッケージのインストール(faiss-cpu) - name: Install faiss-cpu run: | set -e echo "Installing faiss-cpu..." pip install faiss-cpu --no-deps # faiss のバイナリをアーカイブ - name: Archive faiss binaries run: | mkdir -p faiss_package pip install --target=faiss_package faiss-cpu tar -czvf faiss_package.tar.gz faiss_package # AWS認証情報の設定(環境に合わせてSecretsやRoleを指定) - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.CICD_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.CICD_AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # S3 にアップロード - name: Upload faiss binaries to S3 run: | echo "Uploading faiss_package.tar.gz to S3..." aws s3 cp faiss_package.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/lambda/faiss_package.tar.gz echo "Upload complete." 上記の例では、 faiss_package.tar.gz が S3 に lambda/faiss_package.tar.gz というキーでアップロードされます。 Lambda側での動的ロード処理 ( setup_faiss 関数) setup_faiss 関数は、Lambda 実行時に S3 から faiss_package.tar.gz をダウンロードし、 /tmp ディレクトリに展開、 sys.path にパッケージパスを追加します。こうして Lambda 内で import faiss が可能になり、FAISS インデックス検索を実行できるようになります。 # setup_faiss例:S3上のfaissパッケージをダウンロードし、sys.pathに追加 import os import sys import tarfile from shared.logger import getLogger from shared.s3_client import S3Client logger = getLogger(__name__) def setup_faiss(s3_client: S3Client, s3_bucket: str) -> None: try: import faiss logger.info("faiss が既にインポートされています。") except ImportError: logger.info("faiss が見つかりません。S3からダウンロードします。") faiss_package_key = "lambda/faiss_package.tar.gz" faiss_package_path = "/tmp/faiss_package.tar.gz" faiss_extract_path = "/tmp/faiss_package" # S3からパッケージをダウンロードして展開 s3_client.download_file(bucket_name=s3_bucket, key=faiss_package_key, file_path=faiss_package_path) with tarfile.open(faiss_package_path, "r:gz") as tar: for member in tar.getmembers(): member.name = os.path.relpath(member.name, start=member.name.split("/")[0]) tar.extract(member, faiss_extract_path) sys.path.insert(0, faiss_extract_path) import faiss logger.info("faiss のインポートに成功しました。") EmbeddingsとFAISSインデックスを用いたRAG検索 search_data 関数では、S3 から取得した FAISS インデックスをローカルでロードし、クエリに合致する文書を検索します。 get_embeddings 関数によって生成される Embeddings クライアント(Azure OpenAI または OpenAI)を用いて文書をベクトル化しており、 faiss を活用した高速検索が可能です。 from typing import Any, Dict, List, Optional from langchain_community.vectorstores import FAISS from langchain_core.documents.base import Document from langchain_core.vectorstores.base import VectorStoreRetriever from shared.secrets import get_secret from shared.logger import getLogger from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings logger = getLogger(__name__) def get_embeddings(secrets: Dict[str, str]): api_type: str = secrets.get("openai_api_type", "azure") if api_type == "azure": return AzureOpenAIEmbeddings( openai_api_key=secrets.get("azure_openai_api_key_eastus2"), azure_endpoint=secrets.get("azure_openai_endpoint_eastus2"), model="text-embedding-3-large", api_version=secrets.get("azure_openai_api_version_eastus2", "2023-07-01-preview"), ) elif api_type == "openai": return OpenAIEmbeddings( openai_api_key=secrets.get("openai_api_key"), model="text-embedding-3-large", ) else: logger.error("無効なAPIタイプが指定されています。") raise ValueError("Invalid api_type") def search_data( query: str, index_folder_path: str, search_type: str = "similarity", score_threshold: Optional[float] = None, k: Optional[int] = None, fetch_k: Optional[int] = None, lambda_mult: Optional[float] = None, ) -> List[Dict]: secrets: Dict[str, str] = get_secret() embeddings = get_embeddings(secrets) db: FAISS = FAISS.load_local( folder_path=index_folder_path, embeddings=embeddings, allow_dangerous_deserialization=True, ) search_kwargs = {"k": k} if search_type == "similarity_score_threshold" and score_threshold is not None: search_kwargs["score_threshold"] = score_threshold elif search_type == "mmr": search_kwargs["fetch_k"] = fetch_k or k * 4 if lambda_mult is not None: search_kwargs["lambda_mult"] = lambda_mult retriever: VectorStoreRetriever = db.as_retriever( search_type=search_type, search_kwargs=search_kwargs, ) results: List[Document] = retriever.invoke(input=query) return [{"content": doc.page_content, "metadata": doc.metadata} for doc in results] 非同期ダウンロードとLambdaハンドラ async_handler 内で setup_faiss を実行し、 download_files で S3 から FAISS インデックスファイルを取得します。その後 search_data で RAG 検索を実行し、結果を JSON で返します。 import asyncio import json import os from shared.s3_client import S3Client from shared.logger import getLogger from shared.token_verifier import with_token_verification logger = getLogger(__name__) RESULT_NUM = 5 @with_token_verification async def async_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: env = os.getenv("ENV") s3_client = S3Client() s3_bucket = "bucket-name" setup_faiss(s3_client, s3_bucket) request_body_str = event.get("body", "{}") request_body = json.loads(request_body_str) query = request_body.get("query") index_path = request_body.get("index_path") local_index_dir = "/tmp/index_faiss" await download_files(s3_client, s3_bucket, index_path, local_index_dir) results = search_data( query, local_index_dir, search_type=request_body.get("search_type", "similarity"), score_threshold=request_body.get("score_threshold"), k=request_body.get("k", RESULT_NUM), fetch_k=request_body.get("fetch_k"), lambda_mult=request_body.get("lambda_mult"), ) return create_response(200, results) def retrieverHandler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: return asyncio.run(async_handler(event, context)) def create_response(status_code: int, body: Any) -> Dict[str, Any]: return { "statusCode": status_code, "body": json.dumps(body, ensure_ascii=False), "headers": { "Content-Type": "application/json", }, } async def download_files(s3_client: S3Client, bucket: str, key: str, file_path: str) -> None: loop = asyncio.get_running_loop() await loop.run_in_executor(None, download_files_from_s3, s3_client, bucket, key, file_path) def download_files_from_s3(s3_client: S3Client, s3_bucket: str, prefix: str, local_dir: str) -> None: keys = s3_client.list_objects(bucket_name=s3_bucket, prefix=prefix) if not keys: logger.info(f"'{prefix}'内にファイルが存在しません。") return for key in keys: relative_path = os.path.relpath(key, prefix) local_file_path = os.path.join(local_dir, relative_path) os.makedirs(os.path.dirname(local_file_path), exist_ok=True) s3_client.download_file(bucket_name=s3_bucket, key=key, file_path=local_file_path) 5.4のまとめ setup_faiss による faiss 動的ロードで Lambda レイヤー容量問題を回避 非同期 I/O と S3 利用により、コンテナ化や EFS 接続なしで FAISS インデックスをロード search_data で Embedding 済みインデックスを検索し、RAG が迅速な類似文書提供を実現 これにより、RAG 検索を用いた高速なナレッジ検索が可能となり、LLM 回答に特化した情報提供が実現できます。 5.5 [Python] EmbeddingとFAISSインデックス化 このセクションでは、Confluence ドキュメントなどの社内ドキュメントを Embedding し、FAISS インデックスを作成・更新する定期バッチ処理の例を示します。 RAG パイプラインで参照するインデックスは、生成 AI が社内特有の知識を回答に反映するための重要な鍵です。そのため、定期的なドキュメント更新時には Embedding と FAISS インデックスを再構築し、最新情報を常に参照できるようにします。 処理の概要 S3 から JSON フォーマットのドキュメントを取得 取得したドキュメントを Embedding(OpenAI や Azure OpenAI の Embeddings API を利用) Embedding 済みのテキスト群を FAISS インデックス化 作成した FAISS インデックスを S3 へアップロード これらの手順を Lambda バッチ処理や Step Functions ワークフローで定期的に実行することで、問い合わせ時には常に最新インデックスを用いた RAG 検索が行えます。 ステップ1:JSONドキュメントの読み込み S3 上の JSON ファイル(Confluence ページ等を要約済みのもの)をダウンロード・パースし、 Document オブジェクトのリストに変換します。 import json from typing import Any, Dict, List from langchain_core.documents.base import Document from shared.logger import getLogger logger = getLogger(__name__) def load_json(file_path: str) -> List[Document]: """ JSONファイルを読み込み、Documentオブジェクトのリストを返します。 JSONは [{"title": "...", "content": "...", "id": "...", "url": "..."}] のような形式を想定。 """ with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, list): raise ValueError("JSONトップレベルがリストではありません。") documents = [] for record in data: if not isinstance(record, dict): logger.warning(f"スキップされたレコード(辞書でない): {record}") continue title = record.get("title", "") content = record.get("content", "") metadata = { "id": record.get("id"), "title": title, "url": record.get("url"), } # タイトルとコンテンツをまとめたテキストとしてDocument化 doc = Document(page_content=f"Title: {title}\nContent: {content}", metadata=metadata) documents.append(doc) logger.info(f"{len(documents)} 件のドキュメントをロードしました。") return documents ステップ2:EmbeddingとFAISSインデックス化 vectorize_and_save 関数では、 get_embeddings で取得した Embeddings クライアントでドキュメントを Embedding し、 FAISS インデックスを作成します。その後、ローカルにインデックスを保存します。 import os from langchain_community.vectorstores import FAISS from langchain_core.text_splitter import RecursiveCharacterTextSplitter from shared.logger import getLogger logger = getLogger(__name__) def vectorize_and_save(documents: List[Document], output_dir: str, embeddings) -> None: """ ドキュメントをEmbeddingし、FAISSインデックスを作成してローカルに保存します。 """ # テキスト分割器を使用してドキュメントを小さなチャンクに分割 text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=128) split_docs = text_splitter.split_documents(documents) logger.info(f"{len(split_docs)} 件の分割済みドキュメント") # Embeddingsでベクトル化し、FAISSインデックス構築 db: FAISS = FAISS.from_documents(split_docs, embeddings) logger.info("ベクトルDBの構築が完了しました。") os.makedirs(output_dir, exist_ok=True) db.save_local(output_dir) logger.info(f"ベクトルDBを {output_dir} に保存しました。") ステップ3:インデックスをS3へアップロード ローカルで作成した FAISS インデックスを S3 にアップロードすることで、RAG 検索用 Lambda から容易に取得可能になります。 from shared.s3_client import S3Client from shared.logger import getLogger logger = getLogger(__name__) def upload_faiss_to_s3(s3_client: S3Client, s3_bucket: str, local_index_dir: str, index_s3_path: str) -> None: """ FAISSインデックスをS3にアップロードします。 """ index_files = ["index.faiss", "index.pkl"] for file_name in index_files: local_file_path = os.path.join(local_index_dir, file_name) s3_index_key = os.path.join(index_s3_path, file_name) s3_client.upload_file(local_file_path, s3_bucket, s3_index_key) logger.info(f"FAISSインデックスファイルを s3://{s3_bucket}/{s3_index_key} にアップロードしました。") ステップ4:全体フローをLambdaで実行 index_to_s3 関数は全体フローをまとめています。S3 から JSON をダウンロード、Embedding と FAISS インデックス作成、そしてインデックスを S3 へアップロードします。この処理を Step Functions などのワークフローで定期的に実行し、常に最新のインデックスを用意します。 import os from shared.faiss import setup_faiss from shared.logger import getLogger from shared.s3_client import S3Client from shared.secrets import get_secret logger = getLogger(__name__) def index_to_s3(json_s3_key: str, index_s3_path: str) -> Dict[str, Any]: """ S3からJSONファイルをダウンロードし、EmbeddingとFAISSインデックス作成を行い、インデックスをS3に保存します。 """ env = os.getenv("ENV") if env is None: error_msg = "ENV 環境変数が設定されていません。" logger.error(error_msg) return {"status": "error", "message": error_msg} try: s3_client = S3Client() s3_bucket = "bucket-name" local_json_path = "/tmp/json_file.json" local_index_dir = "/tmp/index" # 必要なら faiss のセットアップ(S3からダウンロード) setup_faiss(s3_client, s3_bucket) # JSONファイルをS3からダウンロード s3_client.download_file(s3_bucket, json_s3_key, local_json_path) documents = load_json(local_json_path) # Embeddingsクライアント取得 secrets = get_secret() embeddings = get_embeddings(secrets) # ベクトル化とFAISSインデックス作成 vectorize_and_save(documents, local_index_dir, embeddings) # インデックスファイルをS3へアップロード upload_faiss_to_s3(s3_client, s3_bucket, local_index_dir, index_s3_path) return { "status": "success", "message": "FAISSインデックスを作成し、S3にアップロードしました。", "output": { "bucket": s3_bucket, "index_key": index_s3_path, }, } except Exception as e: logger.error(f"インデックス作成処理中にエラー発生: {e}") return {"status": "error", "message": str(e)} 5.5のまとめ load_json で JSON ファイルを読み込み、 vectorize_and_save で Embedding と FAISS インデックス作成 upload_faiss_to_s3 でローカルインデックスを S3 へアップロード index_to_s3 で全体フローをまとめ、定期バッチ処理で最新インデックスを作成・更新 これにより、社内ドキュメントを Embedding し、RAG 検索用の FAISS インデックスを作成・更新するバッチ処理を実現できます。 6. まとめ 本記事では、Slack 上で LLM を活用する社内チャットボットしぇるぱの開発背景や技術的実装ポイント、RAG パイプラインの導入手順、Confluence ドキュメントのサニタイズや Embedding/FAISS インデックスによる検索基盤の整備、さらには翻訳・要約などの機能拡張について紹介しました。 こうした仕組みにより、Slack 内で自然な操作で生成 AI を利用でき、社員は新たなツールやコマンドを学ぶことなく高度な情報活用が可能になります。 7. 今後の展望 私たちはしぇるぱをさらに進化させるため次の改善・拡張に積極的に取り組みます。 Azure 環境での構築強化 Azure Functions や Azure CosmosDB などの Azure サービスとの連携を本格化し RAG パイプラインのパフォーマンスや拡張性を抜本的に向上させます。 Azure Cosmos DBベクトル検索の導入 Azure Cosmos DB for NoSQL 上でベクトル検索機能を実用化しより高度な検索を提供します。 @ card AI Document Intelligenceの活用 AI Document Intelligence を積極的に取り込み RAG のナレッジ範囲を拡大させより多彩な情報活用を実現します。 @ card モデルの多様化・高度化 GPT-4o のみならず GPT-o1 や Google Gemini など最新で多様なモデルへの対応を推進し常に最先端のモデルを統合します。 Web UIの実装 Slack 依存の表現上の制約を解消するため Web UI を構築し多彩なインタラクションや新機能を柔軟に展開します。 プロンプト管理の拡充 既存のプロンプトをテンプレート化し用途別に容易な再利用を実現します。またプロンプト共有機能を充実させ社内全体での生成 AI 活用を一層促進します。 マルチエージェント化の実現 要約や翻訳、 RAG 検索など特定機能に特化した専門エージェントを配置し、エージェントビルダー機能で自由に組み合わせることでより柔軟で高度な情報活用を可能にします。 RAG精度の評価・改善 テストセットを構築し回答の自動評価を実施して精度を定量的に把握し継続的な品質向上につなげます。 ユーザーフィードバックを起点とした改善 利用実態やフィードバックを反映し対話フローの最適化やプロンプトチューニング外部サービス連携強化など実運用を通じて常にしぇるぱの利便性と有用性を高めていきます。 私たちはこれらの取り組みによってしぇるぱを持続的に進化させ、より多様なニーズに応えられる力強い社内支援ツールへ成長させます。
この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄 1. はじめに こんにちは、KINTO FACTORY でバックエンドエンジニアをしている西田です。 今回はAWSコスト削減をテーマにお話をしたいと思います。 2. コスト削減に取り組んだきっかけ KINTO テクノロジーズでは AWS のサービス利用料を Amazon QuickSight を使って可視化しており、プロダクト単位で利用料を確認することができます。 KINTO FACTORY をローンチしてしばらく経った頃、何気なくプロジェクトのコストを確認してみたところ、なんと社内全プロダクトの中で2番目にコストがかかっていることが分かりました。まだサービスを開始して間もない段階でここまでのコストが発生しているのは想定外でした。この「えっ!?」という発見をきっかけに、アプリケーションチームを主体に私たちのコスト削減への取り組みが始まります。 3. 具体的にやったこと それでは、私たちが実際に取り組んだコスト削減施策について詳しくご紹介していきます。 ECS Fargate のインスタンス数と起動タイプの見直し コストの内訳を眺めてみると、ECS Fargateの利用料が断トツ1位でした。KINTO FACTORYのアプリケーションがECS Fargate上で動いているため当然といえば当然なのですが、このコストを何とか最適化できないか検討することにしました。 まず最初に気づいたのが、「あれ?開発環境のインスタンス数、本番と同じになってない?」という点。開発環境ってそんなにガッツリしたスペックはいらないはず。そこで、開発環境のインスタンス数を必要最低限まで減らすことにしました。 さらに調べてみると、Fargate の起動タイプには通常とは別の Fargate Spot というものがあることがわかりました。Fargate Spot は AWS 上にある未使用のリソースを利用する仕組みで、通常の Fargate に比べて最大70%の割引が適用されます。使わない手はないですね。 Fargate Spot を使用すると、割り込み許容のある Amazon ECS タスクを、Fargate 料金と比較して割引料金で実行できます。Fargate Spot は、予備のコンピュートキャパシティーでタスクを実行します。AWS がキャパシティを戻す必要がある場合、タスクは中断され、2 分間の警告が表示されます。 -- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/fargate-capacity-providers.html ただ、ドキュメントにもある通り Fargate Spot は予備のコンピュートキャパシティーでタスクを実行している仕組みとなるため、AWS 側のリソース調整でタスクが途中で中断される可能性があります。本番環境への適用は難しいので中断されても問題ない開発環境を対象に設定を変更しました。 開発環境の起動・停止の自動化 本番環境だけでなく開発環境も24/365稼働していたため、深夜帯や休日といった利用しない期間は開発環境を停止させるようにしました。 構成としては Step Functions を使って、Application、DB の起動停止を自動化しました。 前段に EventBridge を使い、cron で指定した日時に Step Functions に対して起動・停止のトリガーを送信します。 ポイントとしては DB の起動には時間がかかり起動直後に ECS を起動するとコネクションエラーになってしまうため、DB のステータスを確認してから ECS を起動するようにしています。 参考のため、以下にStep Functionsのワークフローを記述するサンプルコード(YAMLファイル)を示します。 "DB起動": { "Type": "Task", "Parameters": { "DbClusterIdentifier": "${db_cluster_identifier}" }, "Resource": "arn:aws:states:::aws-sdk:rds:startDBCluster", "Next": "Wait" }, "Wait": { "Type": "Wait", "Seconds": 300, "Next": "起動後のDBの状態取得" }, "起動後のDBの状態取得": { "Type": "Task", "Parameters": { "DbClusterIdentifier": "${db_cluster_identifier}" }, "Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters", "Next": "起動完了しているかチェック" }, "起動完了しているかチェック": { "Type": "Choice", "Choices": [ { "Variable": "$.DbClusters[0].Status", "StringEquals": "available", "Next": "各サービスの起動" } ], "Default": "Wait" }, サーバーレスアーキテクチャへの移行 次に、ある機能のバッチ処理を見直していた時に処理自体は1分もかからないのにECSを使った常駐サービスで稼働させていることがわかりました。 こちらもコスト的に効率が悪いと考え、Lambdaを用いたサーバーレスアーキテクチャへ移行しました。 Lambda はリクエスト数と実行時間に応じて課金されるため、短時間で終わる処理や常時起動不要な処理を実行するには最適なサービスです。 4. コスト削減の結果 結果として、コスト削減に取り組んですぐに効果が現れ、ピーク時と比較すると 65% も削減となりました。まさかの半分以下です。 これだけ削減できる余地があったのは正直驚きでした。。。 5. さいごに 今回は私たちのコスト削減への取り組みについてお話しました。 実施したことはどれも簡単で特別なものではなかったですが、大きな成果を出すことができました。 また、取り組んでみて分かったことがあります。コスト削減って、単純に「お金を節約できた!」というだけじゃないんですよね。 無駄なリソースを見直すことで、システム全体の見える化ができた アーキテクチャを見直すきっかけになった 「本当にこのリソースいる?」を考える習慣が身についた その先に得られるメリットも大きいので、改めて取り組みをして良かったなと感じます。 もし似たような課題をお持ちの方の参考になれば嬉しいです。
はじめに Flutterのマルチパッケージプロジェクトでは、アセット管理、特にローカルJSONファイルの読み込みが課題となることがあります。 通常のシングルパッケージプロジェクトとは異なるアプローチが必要となり、開発者を悩ませることがあります。 この記事では、Flutterのマルチパッケージプロジェクトにおいて、ローカルのJSONファイルを効果的に読み込む方法について詳しく解説します。 この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄 今回用意したテストプロジェクト 今回、調査のため、マルチパッケージで管理する簡潔なプロジェクトを用意しました。 このプロジェクトは、以下のような構成になっています。 🎯 Dart SDK: 3.5.4 🪽 Flutter SDK: 3.24.5 🧰 melos: 6.2.0 ├── .github ├── .vscode ├── app/ │ ├── android/ │ ├── ios/ │ ├── lib/ │ │ └── main.dart │ └── pubspec.yaml ├── packages/ │ ├── features/ │ │ ├── assets/ │ │ │ └── sample.json │ │ ├── lib/ │ │ │ └── package1.dart │ │ └── pubspec.yaml │ ├── .../ ├── analysis_options.yaml ├── melos.yaml ├── pubspec.yaml └── README.md Assetにあるファイルを読み込む 一般的なシングルパッケージでのAssetの読み込みは、以下のようにすればできるという説明はよく見かけます。 flutter: assets: - assets/ # アセットフォルダを指定 import 'package:flutter/services.dart' show rootBundle; Future<String> loadAsset() async { return await rootBundle.loadString('assets/sample.json'); } 公式も同じ様な説明をしています。 https://docs.flutter.dev/ui/assets/assets-and-images 実際にAssetからJSONファイルを読み込んでTextにString表示するだけのWidgetを作ってみました。 import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class LocalAssetPage extends StatefulWidget { const LocalAssetPage({super.key}); @override LocalAssetPageState createState() => LocalAssetPageState(); } class LocalAssetPageState extends State<LocalAssetPage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = json.encode(data); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local Asset Page'), ), body: Center( child: _jsonContent.isEmpty ? const CircularProgressIndicator() : Text(_jsonContent), ), ); } } しかし、マルチパッケージでのAssetの読み込みは、この方法ではうまくいきません。 🤨 flutter_genを使ってAssetを読み込む 大抵の場合、マルチパッケージでのAssetの読み込みは、flutter_genを使って解決できます。 flutter_genは、AssetやLocalization等のパスからコード生成をして、タイプセーフにアセットの読み込みが実現できるツールで、マルチパッケージのAssetの読み込みもサポートしています。 https://github.com/FlutterGen/flutter_gen flutter_genでマルチパッケージのAssetを読み込むには、以下の設定が必要になります。 flutter_gen: assets: outputs: package_parameter_enabled: true この設定を入れて、flutter_genを実行すると、マルチパッケージのAssetを読み込むためのコードが生成されます。 そこで生成されたコードを使ってタイプセーフにAssetを読み込むことができます。 上記の例をflutter_genを使って書き換えると、以下のようになります。 import 'package:{YOUR_PACKAGE_NAME}/gen/assets.gen.dart'; Future<String> loadAsset() async { return await rootBundle.loadString(Assets.sample); } 実際に以下の様なコードを書くことで、マルチパッケージのAssetを読み込むことができます。 import 'dart:convert'; + import 'package:feature_flutter_gen_sample/gen/assets.gen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class FlutterGenSamplePage extends StatefulWidget { const FlutterGenSamplePage({super.key}); @override FlutterGenSamplePageState createState() => FlutterGenSamplePageState(); } class FlutterGenSamplePageState extends State<FlutterGenSamplePage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { + final response = await rootBundle.loadString(Assets.sample); - final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = data.toString(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('FlutterGen Sample'), ), body: Center( child: _jsonContent.isNotEmpty ? Text(_jsonContent) : const CircularProgressIndicator(), ), ); } } ファイルのパスが構造化されるので、とてもキレイですね! チーム開発のAsset管理はこれで安心です。 ですが、なるべくツールに頼らない方法を取りたい場合も多々ありますよね? 次はその方法についてもお話します。 flutter_genを使わないでAssetを読み込む flutter_genを使わずにマルチパッケージのAssetを読み込む方法ももちろんあります。 その場合は、以下のルールでパスを指定することでAssetを読み込むことができます。 :::message packages/ {パッケージ名} / {フォルダパス} /ファイル名 ::: パッケージ名 は、そのassetが格納されているPackageのpubspec.yamlのnameに指定した名前になります。 フォルダパス は、そのPackageのpubspec.yamlのassetsに指定したパスになります。 name: local_asset ... flutter: assets: - assets/ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; class LocalAssetPage extends StatefulWidget { const LocalAssetPage({super.key}); @override LocalAssetPageState createState() => LocalAssetPageState(); } class LocalAssetPageState extends State<LocalAssetPage> { String _jsonContent = ''; @override void initState() { super.initState(); _loadJson(); } Future<void> _loadJson() async { + final response = await rootBundle.loadString('packages/local_asset/assets/sample.json'); - final response = await rootBundle.loadString('assets/sample.json'); final data = await json.decode(response); setState(() { _jsonContent = json.encode(data); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local Asset Page'), ), body: Center( child: _jsonContent.isEmpty ? const CircularProgressIndicator() : Text(_jsonContent), ), ); } } このようにして、flutter_genを使わずにマルチパッケージのAssetを読み込むことができます。 小規模プロジェクトや、個人開発ではこの方法でも十分に対応できそうです。 このパスの作成ルールが理解できておらず、ファイルの相対パスを指定したり、パスの設定を色々工夫してみたりしましたが、 そもそもエラーでビルドできなかったりととても苦戦しました... pubspec.yamlで指定するパスについて ローカルでアセットを管理する時に、pubspec.yamlで指定するパスについても注意が必要です。 assetsのパスは、pubspec.yamlからの相対パスで指定しますが、 /assets と /assets/{サブフォルダ} の扱いにも注意が必要です。 以下の様にJSONファイルをサブフォルダに移動した場合、 ├── packages/ │ ├── features/ │ │ ├── assets/ │ │ │ └── jsons/ │ │ │ └── sample.json <<< HERE │ │ ├── lib/ │ │ │ └── package1.dart │ │ └── pubspec.yaml │ ├── .../ 僕はてっきり /assets と指定すれば、 /assets/{サブフォルダ} 以下のファイルも読み込めると思っていましたが、 loadStringのパスを packages/local_asset/assets/jsons/sample.json に変更しても読み込めなくなります。 サブフォルダに移動した場合は、以下の様にサブフォルダを明示的に指定する必要があります。 flutter: assets: - /assets/jsons/ これで、 packages/local_asset/assets/jsons/sample.json をロードできる様になりました。 サブフォルダで細かくアセットを管理するのであれば、サブフォルダの指定もpubspec.yamlに追加するのを忘れない様にしないといけません。 ちなみに、ここまで  assets  フォルダで話をしていましたが、このフォルダ名も変更できます。 pubspec.yamlと実際のパス構成が合っていれば、 assets フォルダ以外でも問題なく読み込めます。 まとめ 今回はFlutterのマルチパッケージの中でローカルのJSONを読み込む方法についてお話しました。 普段はiOSの開発がメインなので、簡単にできるだろうと思っていたのですが、意外と苦労しました。 マルチパッケージ下での開発手法はまだあまり情報がない様なので、今後もこういった情報があれば共有していきたいと思います。