TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

目次 はじめに 勉強会の概要 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) LT枠2: Go言語で行うメール解析 (every きょー。) LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) まとめ はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 今回の記事では、先日OPTiMさんと共同で開催した勉強会についてご紹介したいと思います。 勉強会の概要 7月2日(火)に、弊社everyとOPTiMさんとの2社合同でのGoの勉強会 OPTiM × every Golang Developer Night を開催しました。 この勉強会は、OPTiMさんのご厚意により、OPTiMさんのオフィスで開催させていただきました。 受付では各社のノベルティも配られました。また、暑いなか参加上限に近い数の方にご参加いただき、勉強会も懇親会も盛況でした。 受付のノベルティ 会場の様子 勉強会中の風景 懇親会の様子 勉強会では、OPTiMさんとeveryからそれぞれメンバーが登壇し、Golangを利用したプロジェクトでの成功事例や課題克服にまつわるLTが行われました。 以下が当日のタイムスケジュールとなっています。 時間 内容 19:30 - 19:35 オープニング 19:35 - 19:40 OPTiM 会社紹介 19:40 - 19:45 every 会社紹介 19:45 - 20:00 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) 20:00 - 20:15 LT枠2: Go言語で行うメール解析 (every きょー。) 20:15 - 20:30 LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) 20:30 - 20:45 LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) 20:45 - 21:00 質疑応答 21:00 - 21:10 クロージング 21:10 - 懇親会 LT枠1: Go x LLMで 新たなコード生成の可能性を探る (OPTiM 今枝) speakerdeck.com このセッションでは、Go言語のコード生成について紹介が行われました。 コード生成は、DX向上や品質・セキュリティ向上、パフォーマンス最適化、保守性・拡張性の向上、相互運用性・プラットフォーム互換性の向上などの目的で行われます。 oapi-codegen 1 のような api interface や templategen 2 のようなテンプレート生成など、 Go言語自体がコード/テンプレートの自動生成をサポートしているため、手軽にコード生成を行うことができます。 そこに、LLMによるコード生成も加わることで、より効率的なコード生成が可能になるという提案に関するお話でした。 コード生成ツールとして、 plandex 3 というGo言語で書かれたOSSも紹介されました。 コードを自動で生成できることで、開発効率が向上し、品質やセキュリティの向上にもつながるため、 LLMを織り交ぜたコード生成を上手く取り込むことで、より効率的な開発が目指せそうだと感じました。 LT枠2: Go言語で行うメール解析 (every きょー。) speakerdeck.com このセッションでは、Go言語を使ったメール解析について紹介が行われました。 メールのプロトコルの話から始まり、ヘッダーのようなメールの構成などの説明があり、実際にGo言語でメール解析を行う方法についても解説がありました。 メールのメッセージは、RFC2822 4 で規定されたメールプロトコルで構成されており、 メールヘッダーの情報を利用することで、トラブルシューティングやスパム検出、セキュリティ分析など様々な用途に活用できるようです。 Goではnet/mailパッケージ 5 が標準で提供されており、 メッセージからヘッダーやボディの情報を取得することで、様々なメール解析を行うことができるとのことでした。 発表中には実際のユースケースの紹介もあり、メール解析の活用方法についてのイメージをより具体的に持つことができました。 ちなみに、Goによるメール解析の実際のユースケースとして、きょー。が弊社everyのテックブログも書いているので、興味のある方はぜひご覧ください。 tech.every.tv LT枠3: プロダクトでどれくらいMELTしてますか? (OPTiM 坂井) (発表資料は公開準備中です。公開され次第追記します。) このセッションでは、MELT 6 という、モニタリング/オブザーバビリティに関する概念についての紹介が行われました。 MELTとは、システムレベルの懸念を理解するための「モニタリング」とアプリケーションレベルの懸念を理解するオブザーバビリティに関する考え方であり、 SaaSなどの発展で複雑化して管理者による制御が難しくなってきているシステムに対して、 Metrics/Events/Logs/Tracesを基本的な観測可能性のシグナルとして捉えることで、観測可能なシステムの開発ライフサイクルの実現を目指すための概念とのことでした。 OpenTelemetry 7 などを用いて、Goでどのように計装し可視化するのか、デモも交えて説明していただけました。 モニタリング/オブザーバビリティの考え方は、システムの問題の特定や解決を行うための重要な概念であり、 Go言語を用いた計装方法や可視化方法を学ぶことで、システムの理解を深めることができると感じました。 また、実際にデモを行っていただいたことで、どのように可視化されるのかが明確になり、ツールの良さをより実感することができました。 LT枠4: slices/maps pkgを活用してオレオレ実装を撲滅したい (every ayaka.yoshida) speakerdeck.com このセッションでは、Go言語のslices/mapsパッケージの活用方法について紹介が行われました。 Go1.21でslicesパッケージ 8 およびmapsパッケージ 9 が追加され、sliceやmapの操作をより簡潔に行うことができるようになりました。 これにより、以前まではforループなどを駆使して自前で実装していた処理を、公式提供のslices/mapsパッケージのメソッドに置き換えることができるようになったということでした。 実際にプロジェクトで使われているコードを新しいパッケージのメソッドに置き換えた例が提示され、コードがより簡潔で可読性の高いものになることが参加者も実感できたのではないかと思います。 Goでは、最低限の機能のみを提供することによるシンプルな言語設計を方針とされていますが、その影響で自前の実装が長大になるケースもあったかと思います。 しかし、slices/mapsのように、公式によるパッケージ提供のおかげで徐々にプログラムの記述が簡潔になってきているのではないかと感じました。 1.22以降でも、ループ変数のスコープの変更やイテレータなど、様々な改善が行われているため、今後もGo言語の進展に注目していきたいと思いました。 まとめ 今回の記事では、7月2日(火)にOPTiMさんと共同で開催した勉強会について紹介しました。 LT枠では、Go言語を使ったコード生成やメール解析、MELTを意識したGoによるシステムのmonitoring/observation、slices/mapsパッケージの活用について発表がありました。 それぞれのセッションを通じて、Go言語の新たな活用方法や機能、システムの可視化方法などを学ぶことができ、非常に有意義な時間を過ごすことができました。 最後に、このような機会を提供していただいたOPTiMさん、登壇者の皆様、参加者の皆様、本当にありがとうございました。お疲れ様でした。 最後まで読んでいただき、ありがとうございました。 oapi-codegen ↩ templategen ↩ plandex ↩ RFC2822 ↩ net/mailパッケージ ↩ New Relic | メトリクス、ログ、トレース、イベントとは?違いを解説 ↩ OpenTelemetry ↩ slicesパッケージ ↩ mapsパッケージ ↩
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 28日目の記事です。 はじめに DelishKitchenとヘルシカでバックエンドエンジニアをしているyoshikenです。 今回は新規アプリ開発の際に、Android/iOSアプリの課金処理(subscription)について awa/go-iapを使用して大変便利だったので、その紹介をしたいと思います。 awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS. Android/iOSの課金処理のフローについてはサイバーエージェントさんのテックブログで詳しく説明されているので、そちらを参照してください。(この記事を書いてくださった方がそのまま awa/go-iapの原型を作成されたかと 自動購読課金について【Android編】 | サイバーエージェント 公式エンジニアブログ 自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ awa/go-iapとは awa/go-iapは、In-App Purchase (IAP) のサポートを提供するライブラリで、実際にawaでも使用されているので一定の信頼性があり、更新頻度も高く、以前自分がissueを立てた際は次の日には修正されていました。 offerDiscountType is missing in JWSTransactionDecodedPayload · Issue #266 · awa/go-iap また、今回はAndroid/iOSについての説明をしますが、他にもAmazon AppStoreやHuawei HMSといった決済方法に対応しています。 Android編 サーバー側でやることは 署名の検証 receipt問い合わせ acknowledgeの送信 となります 署名の検証 サイバーエージェントさんのテックブログ にも書かれていますが、送られてきたreceiptデータの検証に署名を利用します。 awa/go-iapを使用しない場合の実装はサイバーエージェントさんにお任せするとして… awa/go-iapのVerifySignature関数を使用することで、簡単に署名の検証ができます。 https://pkg.go.dev/github.com/awa/go-iap@v1.32.0/playstore#VerifySignature 以下に実装例を出します import ( "encoding/base64" "github.com/awa/go-iap/playstore" "yourapp/types" ) // Verify receiptが改竄されていないかを検証 // receiptData: レシートデータ、Androidから送られてくるのはbase64化されている // signature: レシートデータの署名 func Verify(receiptData, signature string ) ( bool , error ) { // base64化されているので戻す b, err := base64.StdEncoding.DecodeString(receiptData) if err != nil { return false , err } // レシートデータ自体が正しいか署名を検証 return playstore.VerifySignature(types.GooglePlayPublicKey, b, signature) } Androidから送られてくるreceiptデータがbase64化されているのでデコード処理が必要という意味で関数化しましたが、実際の処理は一行で終わるので無理に関数化しなくても良いかもしれません。 また、検証にはGooglePlayのPublicKeyが必要ですが、これはGooglePlayConsoleから取得できます。 subscription問い合わせ receipt自体の検証が完了したら、購入情報を取得するために再度GooglePlayに問い合わせを行います。 こちらの問い合わせもVerifySubscriptionV2関数を使用することで簡単に実装できます。 https://pkg.go.dev/github.com/awa/go-iap@v1.32.0/playstore#Client.VerifySubscriptionV2 import ( "context" "github.com/awa/go-iap/playstore" "google.golang.org/api/androidpublisher/v3" "github.com/avast/retry-go/v4" "yourapp/types" ) type PurchaseData struct { AutoRenewing bool `json:"autoRenewing"` OrderID string `json:"orderId"` PackageName string `json:"packageName"` ProductID string `json:"productId"` PurchaseTime int64 `json:"purchaseTime"` PurchaseState int64 `json:"purchaseState"` DeveloperPayload string `json:"developerPayload"` PurchaseToken string `json:"purchaseToken"` } func Verifyreceipt(ctx context.Context, receiptData, signature string ) (*androidpublisher.SubscriptionPurchaseV2, error ) {    // 先程のVerify関数で返り値にdecode済みのreceiptデータを追加しても良いかもしれません b, err := base64.StdEncoding.DecodeString(receiptData) if err != nil { return nil , err } var purchaseData PurchaseData err = json.Unmarshal(b, &purchaseData) if err != nil { return nil , err } request, err := playstore.New([] byte (types.GooglePlayServiceAccount)) if err != nil { return nil , err } var res *androidpublisher.SubscriptionPurchaseV2   // ちょくちょく5xxを返すことがあるのでリトライ処理を入れています err = retry.Do( func () error { res = &androidpublisher.SubscriptionPurchaseV2{} res, err = request.VerifySubscriptionV2(ctx, purchaseData.PackageName, purchaseData.PurchaseToken) if err != nil { return err } return nil }, retry.RetryIf( func (err error ) bool { // 任意のエラーハンドリング if strings.Contains(err.Error(), "Service Unavailable" ) { return true } }), ) if err != nil { return nil , err } return res, nil } acknowledgeの送信 署名の検証やらdb保存やら諸々が完了してレスポンスを返却するまえにGooglePlayに対してacknowledgeを送信して購入を承認する必要があります。 逆をいうと、acknowledgeを送信しないと購入が完了しないので、エラー時はacknowledgeさえ送信しないことに注意を払えばなんとかなります。 acknowledgeですが、こちらも関数が用意されており、 AcknowledgeSubscription を使用します。 import ( "context" "github.com/awa/go-iap/playstore" "google.golang.org/api/androidpublisher/v3" "yourapp/types" ) type PurchaseData struct { AutoRenewing bool `json:"autoRenewing"` OrderID string `json:"orderId"` PackageName string `json:"packageName"` ProductID string `json:"productId"` PurchaseTime int64 `json:"purchaseTime"` PurchaseState int64 `json:"purchaseState"` DeveloperPayload string `json:"developerPayload"` PurchaseToken string `json:"purchaseToken"` } func (u *UseCaseImpl) AckSub(ctx context.Context, purchaseReceipt PurchaseData) error { cl, err := playstore.New([] byte (types.GooglePlayServiceAccount)) if err != nil { return err } return cl.AcknowledgeSubscription(ctx, purchaseReceipt.PackageName, purchaseReceipt.ProductID, purchaseReceipt.PurchaseToken, &androidpublisher.SubscriptionPurchasesAcknowledgeRequest{}) } acknowledgeには冪等性があるので何度再送しても大丈夫です。 iOS編 iOSもAndroidと似たようなものですが、serverから見るとAndroidより簡単です。 実際にApplestoreに問い合わせが必要な部分はreceiptの検証のみです。 以下が実装例です import ( "context" "github.com/awa/go-iap/appstore" "github.com/avast/retry-go/v4" ) func VerifyReceipt(ctx context.Context, receiptData, password string ) (*appstore.IAPResponse, error ) { request := appstore.IAPRequest{ ReceiptData: receiptData, Password: password, } var res *appstore.IAPResponse err := retry.Do( func () error { res = &appstore.IAPResponse{} err := appstore.IAPClient.Verify(ctx, request, res) if err != nil { return err } err = appstore.HandleError(res.Status) if err != nil { return err } return nil }, retry.RetryIf( func (err error ) bool { // 任意のエラーハンドリング if strings.Contains(err.Error(), "Service Unavailable" ) { return true } }), ) if err != nil { return res, err } return res, nil } GooglePlayと同じく時折5xxを返すのでリトライ処理を入れています。 iOSはクライアント購入時にack相当はしているのでサーバー側でのacknowledgeは不要です。 おわりに 以上のように、awa/go-iapを使用することで、Android/iOSの課金の購入処理を簡単に実装することができます 今回例に上げたのはsubscriptionのみですが、単発購入の場合も似たような実装になるかと思います。 ただ、subscriptionの場合は購入後の処理のほうが複雑で、定期的な購入情報を問い合わせるか、通知( App Store Server Notifications / RTDN )を受けるかしたあとにそれぞれのプロダクトに合わせた処理をしていきます。(今回は購入処理だけです 自分が実装するさいにawa/go-iapの参考になるドキュメントが少なかったため、今回の記事を書きました。誰かのお役に立てれば幸いです。 参考 awa/go-iap: go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS. 自動購読課金について【Android編】 | サイバーエージェント 公式エンジニアブログ 自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 27日目の記事です。 目次 はじめに 背景と問題点 既存設計の問題 新しい設計方針 リファクタリングの準備 テストを書く リファクタリング作業 実装する テストする まとめ 終わりに はじめに DELISH KITCHENのiOSアプリ開発を担当している池田です。DELISH KITCHENでは皆様の料理体験がより良いものになるよう、日々新しい機能を追加しています。今回は「リアーキテクチャを支えるテスト駆動開発:効果的なリファクタリングの方法」について、実際の経験をもとにお伝えします。テスト駆動開発の重要性を改めて確認しながら、効果的なリファクタリングの方法を紹介します。 背景と問題点 DELISH KITCHENのiOSアプリは2016年のリリース以来、様々な機能を追加してきました。しかし、しっかりとした設計方針がないまま開発を続けてきたため、今後も継続的に機能を追加することが困難になっていました。そのため、一度まとまった時間を取り、リファクタリングを行うことにしました。 既存設計の問題 DELISH KITCHENのiOSアプリでは、SPMを用いたマルチモジュールで設計されています。現在は下図のようなモジュール構成になっています。 例えば、Networkingモジュールは通信に関する実装を含んでいますが、以下のような問題がありました: レスポンスそのものをアプリ全体で使い回しているため、通信を意識する必要のないUIモジュールがNetworkingモジュールに依存している。 レスポンスがサーバの返却するJSONをそのままの形でパースしたものであり、アプリで使いやすい形ではない。 新しい設計方針 上記の問題を解決するために、クリーンアーキテクチャを元にした設計に移行することにしました。クリーンアーキテクチャとは、ドメインモデルを中心とした設計であり、各層が独立して動作することを目指します。下図のような構成を目標にリファクタリングを進めていきます。 リファクタリングの準備 テストを書く リファクタリングを行う前に、まず既存の動作をテストすることが重要です。レスポンスの変換に対してテストを行うことで、変更後の動作を担保します。以下に、簡易的なデコードテストの実装例を示します: // テストコード例 class DecodableTests : XCTestCase { func testGetRecipeResponseDecoding () { if let _ : GetRecipeResponse = Self .decodeJSON(from : "GetRecipeResponse" ) { XCTAssert( true ) } } } extension DecodableTests { /// 共通のJSONデコードテストメソッド static func decodeJSON < T : Decodable > (from fileName : String ) -> T? { guard let fileURL = Bundle.module.url(forResource : fileName , withExtension : "json" ) else { XCTFail( "Failed to find file \( fileName ) .json" ) return nil } do { let data = try Data(contentsOf : fileURL ) let decodedObject = try JSONDecoder.decoder.decode(T. self , from : data ) return decodedObject } catch { XCTFail( "Decoding failed: \( error ) " ) return nil } } } このテストではOpenAPIのJSONレスポンスをJSONファイルとしてプロジェクトのローカルに配置し、そのJSONのデコードが失敗しないことをチェックしています。 リファクタリング作業 実装する 設計方針に従って少しずつリファクタリングを行います。今回は、NetworkingモジュールからModelを切り出し、サーバから取得したレスポンスをドメインモデルに変換するように変更します。以下に、リファクタリングのステップとコード例を示します: Modelモジュールを作り、アプリで利用しやすいドメインモデルを定義し直す。 Networkingモジュールではサーバから取得したレスポンスをドメインモデルへと変換する。 既存実装ではレスポンスとModelの型を一致させることで、ModelをCodableに準拠させ、JSONからの変換のコードの実装が不要でした。今回はModelをレスポンスに依存させるのではなく、レスポンスをModelに依存させるように関係を変更すること、レスポンスとModelの構造が異なることからJSONから変換する処理を実装する必要が出てきました。 // リファクタリング前のコード例 // Networkingモジュール struct Recipe : Codable { let id : Int let title : String let category : String // 以下略 } // リファクタリング後のコード例 // Modelモジュール struct Recipe { let id : Int let title : String let category : Category // 以下略 enum Category { case unknown case typeA case typeB } } // Networkingモジュール(将来的にはInfraモジュールへ変更予定) extension Recipe : Decodable { enum CodingKeys : String , CodingKey { case id case title case category // 以下略 } public init (from decoder : Decoder ) throws { let container = try decoder.container(keyedBy : CodingKeys.self ) let id = try container.decode(Int. self , forKey : .id) let title = try container.decode(String. self , forKey : .title) let category = try container.decode(Category. self , forKey : .category) // 以下略 self . init ( id : id , title : title , category : category // 以下略 ) } } extension Recipe.Category : Decodable { public init (from decoder : Decoder ) throws { let container = try decoder.singleValueContainer() let value = try container.decode(String. self ) switch value { case "typeA" : self = .typeA case "typeB" : self = .typeB default : self = .unknown } } } テストする ビルドできるコードが実装できたら、テストを実行します。全てのテストが成功すればここで終了ですが、変更内容が大きいため、いくつかのテストは失敗することが予想されます。失敗したテストの該当する実装を修正しながら、全てのテストが成功するまで続けます。 まとめ リファクタリングを実施するにあたり、テスト駆動開発の考え方を取り入れました。既存コードに対してテストを書いてからリファクタリングを行うことで、変更後のコードに問題がないことを確認でき、比較的大きな規模のリファクタリングでも安心して進めることができました。今後も継続的にリファクタリングを行い、リアーキテクチャを進め、開発しやすいコードを目指していきます。 終わりに テスト駆動開発は、効果的なリファクタリングを実現するための強力な手法です。テスト駆動開発の重要性を改めて認識し、日々の開発に取り入れることで、より健全で拡張性の高いアプリケーションを構築することができます。この記事が、皆さんの開発においても役立つことを願っています。
アバター
はじめに はじめまして!開発本部のデータ&AIチームに4月に新卒入社した蜜澤です。 最近Amazon QuickSightを使用してダッシュボード作成に励んでいるので、QuickSightにおけるインタラクティブなグラフの作り方を紹介しようと思います!中でも、割合系の指標に対してフィルターを適応するのに苦戦したので、そのあたりの作り方を説明します。 この記事は every Tech Blog Advent Calendar 2024(夏) 26日目の記事になります。 作成するグラフのイメージ 性別、年代、レシピ名を指定すると、CTRの折れ線グラフが表示される。 使用する模擬データ 実際のデータを使用してしまうとアレなので、今回は以下の模擬データを使用します。 それぞれのカラムの定義は以下の通りです。 date:日付(2024-04-01~2024-04-07) recipe:レシピ名(ハンバーグ、からあげ、生姜焼き) gender:性別(男性、女性) age:年代(10代、20代、30代、40代、50代、60代以上) click:レシピをクリックした回数(1~10の整数の乱数) impression:レシピが表示された回数(500~1000の整数の乱数) また、今回は「CTR=click / impression * 100」と定義します。 CTRカラムを作ることは可能ですが、フィルターをかける都合上、CTRはquicksight上で定義します。 使用するデータの注意点として、date 、gender、age、recipe全ての組み合わせごとにclick、impressionを集計したデータである必要があります。 このようなデータを使用することで「10代と20代の男性にハンバーグが表示された場合のCTR」としてフィルターをかけることができるようになります。 データセットを作成する 今回使用する模擬データをQuickSight上にアップロードします。 今回使用するデータはCSVファイルなのでファイルのアップロードを選択します。 ファイルをアップロードすると確認画面が出てくるので、想定通りのデータなら「次へ」を押します。 データの編集/プレビューを押して、データの型などを確認します。 問題がなければ、「保存して視覚化」を押します。 「保存して視覚化」を押すと、分析ページに飛びます。 グラフ作成 左上の「視覚化」を押すと、作成可能なビジュアルのアイコンが表示されるので、作成したいものを選択します。今回は「折れ線グラフ」を選択します X軸とY軸をどのカラムにするかを選択します。 使用しているデータセットに含まれるカラムが表示されているので、その中から必要なものを選んで、「X軸」と「値」にドラッグ&ドロップします。 X軸に「date」、値に「impression」をドラッグ&ドロップしてみると、、 日付ごとのimpressionの折れ線グラフが表示されます。 今回作りたいグラフはCTRのグラフなので、これからCTRを定義します。 カラム名の上にある「計算フィールド」を押します。 このような画面が表示されるので、「名前」と「定義」を入力します。 名前が「CTR」、定義は「sum({click}) / sum({impression}) * 100」としました。{カラム名}でカラムを計算フィールド内で指定できます。 ここで、「{click} / {impression} * 100」と記入すると、性別や年代を複数フィルターするときに正しく計算できなくなってしますので注意が必要です! 入力したら、「保存」を押します。 CTRが追加されたので、値に「CTR」をドラッグ&ドロップすると、日付ごとのCTRのグラフになりました! しかし、このままでは性別、年代、レシピ名を指定できないので、指定するために「パラメータ」と「フィルター」を作成します。 パラメータの作成 この後作成するフィルターで使用する値を転送できる名前付きの変数の役割を果たす「パラメータ」を作成します。 左上の「パラメータ」を押して、「追加」を押します。 このような画面が出てくるので、それぞれの項目を入力していきます。 まずは、性別のパラメータを作ります。名前は「gender」、データ型は「文字列」、値は「複数の値」、デフォルト値は「男性、女性」にします。名前とデフォルト値はお好みで変更していただいてOKです! 入力が終わったら「保存」を押します。 作成したパラメータが表示されます。 同じように年代とレシピ名のパラメータも作成します。 今回作成するダッシュボードは「各レシピ別の傾向を分析する」ユースケースを想定しています。 そのため、レシピ名は「複数の値」ではなく、「単一の値」を選択します。 フィルター作成 次に、作成したグラフに表示するデータを選別する役割を果たす「フィルター」を作成します。 先ほど作成したパラメータを使います。 左上の「フィルター」を押し、「追加」を押します。 まずは「gender」のフィルターを追加してみます。 「gender」を押すとフィルターが作成されます。 作成されたgenderフィルターの3点部分を押して、「編集」を押します。 編集画面が出てくるので、各項目設定します。 「フィルタータイプ」は「カスタムフィルター」を選択します。 「フィルター条件」は「次と等しい」を選択して、「パラメータを使用」のチェックボックスにチェックを入れます。 チェックを入れると、すべてのビジュアルとシートにフィルターを適用するか聞かれるので、用途に合わせて選択します。 今回はビジュアルを1つしか作らないので「いいえ」を選択します。 パラメータが選択できるようになりました。 性別のフィルターなので、先ほど作成した「genderパラメータ」を選択します。 最後に「適用」を押します。 同様のやり方で年代のフィルターである「age」とレシピ名のフィルターである「recipe」も作成します。 これでフィルターの編集は終了です。 コントロールの追加 次に、「コントロール」を作成します。 フィルターに使用しているパラメータの値はダッシュボードの編集者しか変更ができませんが、コントロールを追加することで閲覧者がパラメータを変更できるようになります。 左上の「パラメータ」を押して、コントロールを追加したいパラメータの3点部分を押します。 「コントロールを追加」を押します。 このような画面が表示されるので、各項目を入力します。 genderのコントロールなので 名前 は「性別」、 スタイル は「ドロップダウン - 複数選択」、 値 は「特定の値」、 特定の値を定義 は「男性」「女性」とします。 入力したら「保存」を押します。 グラフの上部にコントロールが表示されるようになりました。 ドロップダウン形式で「男性」、「女性」、「すべて選択」を選べるようになりました。 試しに、「男性」以外のチェックボックを外してみると、、 グラフの形が変わりました。 男性と女性を集計対象としたCTRのグラフから男性のみを対象としたCTRのグラフに変わリました。 同様にageのコントロールも作ります 「gender」と「age」とは少し違う作り方で「recipe」のコントロールも作ります。 「recipe」のコントロールは「レシピ名をテキストとして指定できるようにする」ことを目指します。 そのため、「テキストフィールド」のスタイルを選択します。 今回の例ではレシピ名が3種類しかないのでドロップダウン形式でも良いのですが、実際の運用を見越すとなると、多くの種類のレシピが対象になります。 そうなると、ドロップダウン形式では見通しが悪くなるため、直接レシピ名を指定する手段を採用しました。 「性別」と「年代」はドロップダウン形式、「レシピ名」のみテキストフィールド形式になっていることがわかります。 レシピ名に「ハンバーグ」と入力してみます。 グラフの形が変わりました。先ほどまでは全てのレシピ(ハンバーグ・からあげ・生姜焼き)を集計対象としたCTRのグラフでしたが、ハンバーグのみを対象としたグラフとして絞り込むことができました。 細かい調整 ここまでできたらほぼ完成です! 最後に細かいところをいじります。 デフォルトだと日付が見にくいので、表示形式を変更します。 「date」を押します。 このような画面が表示されるので、「形式」を押して、好きな表示形式を選択します。今回は「YYYY/MM/DD」にします。 dateの表示形式が変更されてスッキリしました! グラフのタイトルも変更します。 左上の「プロパティ」を押します。 画面右側にプロパティの編集画面が表示されます。 「ディスプレイ編集」内の「タイトル編集」の筆のようなアイコンを押します。 このような画面が表示されるので、好きなタイトルを入力します。 ここで、パラメータを押すとタイトルがパラメータの内容で動的に変更できるようになるので、「recipe」を押してみます。 パラメータが入力されました! パラメータの後ろに「のCTR」と入力し、「{入力したレシピ名}のCTR」というタイトルが表示されるようにしました。 レシピ名が「ハンバーグ」の時に、グラフのタイトルが「ハンバーグのCTR」になっています。 説明は省略しますが、X軸やY軸の名前も変更できます。 軸の名前をよしなに変更して、完成です!!! 使ってみる 男性20代と女性20代の生姜焼きのCTRを比較して、日付によって男性女性どちらのCTRが高くなるのかなどを見ることができます。 (今回は乱数を使ったデータなので示唆は得られませんが、、、) この使い方はあくまでも一例であり、色々な使い方ができると思います。 最後に 今回はAmazon QuickSightを使用して、インタラクティブな可視化を行いました。 今回紹介したビジュアル以外にも多くのビジュアルがあり、様々な可視化ができるので、これからも色々な可視化に挑戦していきたいと思います。 また、今回はデータの整形に関しては触れませんでしたが、QuickSightを使用する際にデータの形式はかなり悩むところなので、今後はデータ整形に関することも紹介できたらなと思います! この記事が何かの参考になれば幸いです。 最後まで読んでいただきありがとうござました!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 25 日目の記事です。 はじめに こんにちは、24 新卒として 4 月から入社し、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている新谷です。 現在取り組んでいる業務で、画像を受け取って外部に送信する処理を行う API を作成する機会がありました。そこで学んだ、Go 言語における multipart/form-data を使った画像の取り扱い方について紹介します。 背景 開発した機能としては、クライアントがアップロードした画像を API サーバーで受け取り、外部の API に POST するというものです。開発した部分は以下の図で表すと、API サーバーの部分になります。 この処理を実現するためには、画像データの受信かつ送信を行う必要があります。今回は、画像を取り扱う際によく使われる multipart/form-data 形式を使用して画像を受け取り、外部 API に送信するように実装したので、その方法について紹介します。 multipart/form-data とは multipart/form-data は、Web ページのフォームからファイルをアップロードする際に使用されるデータ形式です。 multipart/form-data 形式のリクエストは、以下のような特徴があります。 リクエストボディが複数の部品(part)から構成される 各部品はヘッダーとボディから構成される 各部品は boundary で区切られる ヘッダーには Content-Disposition や Content-Type などが含まれる ボディにはファイルのデータが含まれる 以下は、 multipart/form-data 形式のリクエストの例です。 POST /post HTTP/1.1 Host: httpbin.org User-Agent: Go-http-client/1.1 Content-Length: 376 Content-Type: multipart/form-data; boundary=600fcf99bf89273352a59587b26bc07642bd bfc97ce30f8445ecc0c3873a Accept-Encoding: gzip --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="file"; filename="sample.txt" Content-Type: application/octet-stream this is dummy. --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="key1" value1 --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a-- ここでは、boundaryで区切られた 2 つの部品が含まれています。1 つ目の部品はファイルのデータを含み、2 つ目の部品はフォームデータを含んでいます。 実装 API Server での画像の受け取り方 本 API Server は、Go の echo フレームワークを使用しています。echo の c.FormFile を使用して画像を受け取る実装を以下に示します。 file, err := c.FormFile( "image_file" ) if err != nil { return types.ErrInvalidParameters } src, err := file.Open() if err != nil { return types.ErrInvalidParameters } defer src.Close() それぞれの処理について解説します。 c.FormFile("image_file") は、リクエストから"image_file"という名前のフィールドを取得します。先ほどの multipart/form-data 形式のリクエストの例では、 name="file" としていた部分に対応します。 image_fileフィールドから取得したfileにはmultipart.FileHeader型のファイル情報が格納され、ファイル名やファイルサイズなどの情報を取得できます。 次に、 file.Open() でファイルを開き、 src にファイルの中身を格納します。 file.Open() は、ファイルを開いてmultipart.File型のファイルオブジェクトを返します。multipart.Fileは以下のインターフェースを実装しています。 type File interface { io.Reader io.ReaderAt io.Seeker io.Closer } io.Reader 、 io.ReaderAt 、 io.Seeker 、 io.Closer のインターフェースを実装しているため、 io.ReadAll などを使用してファイルの中身を取得することもできます。 ファイルの中身を取得した後は、 defer src.Close() でファイルをクローズします。 外部 API への画像の POST 外部 API に画像を POST する際は、 multipart/form-data 形式でリクエストを送信する必要があります。Go 言語では、標準ライブラリの mime/multipart パッケージを使用してmultipart.Writerを作成し、リクエストボディを構築します。 body := & bytes.Buffer {} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile( "image_file" , file.Filename) if err != nil { return types.ErrInternalServer } _, err = io.Copy(part, src) if err != nil { return types.ErrInternalServer } err = writer.Close() if err != nil { return types.ErrInternalServer } req, err := http.NewRequest( "POST" , "https://example.com" , body) if err != nil { return types.ErrInternalServer } req.Header.Set( "Content-Type" , writer.FormDataContentType()) client := &http.Client{} resp, err := client.Do(req) if err != nil { return types.ErrInternalServer } defer resp.Body.Close() body にはリクエストボディを格納するバッファを作成します。 次に、 multipart.NewWriter(body) でマルチパート形式のリクエストボディを構築するmultipart.Writerオブジェクトを作成します。 writer.CreateFormFile("image_file", file.Filename) は、フォームフィールド名を"image_file"とし、ファイル名を file.Filename とするファイルパートを作成します。 そのファイルパートに、 io.Copy(part, src) でファイルの中身をコピーします。 writer.Close() をすることで、最後のboundaryを書き込みます。 その後は、 http.NewRequest でリクエストを作成し、 req.Header.Set("Content-Type", writer.FormDataContentType()) でリクエストヘッダーにContent-Typeを設定します。 writer.FormDataContentType() は、multipart.Writerのboundaryを含むContent-Typeを返します。 気をつけたこと 外部 API に画像を送信する際は、エラーが発生する可能性があるためリトライ処理を実装しました。 for tries < MaxLimit { // リクエスト構築 req, err = r.buildRequest(file, fileName, hurl) if err != nil { return nil , err } // リクエスト処理 body, err = r.doRequest(ctx, req) if err != nil { file.Seek( 0 , 0 ) tries++ continue } } リトライ処理で気を付けるべき点は、一度リクエストを送信すると画像ファイルが読み込まれた状態になるため、リトライ時にファイルのポインタを先頭に戻す必要がある点です。 ファイルポインタを戻さないと、リトライ時にファイルの中身が空になってしまいます。 multipart.File は io.Seeker インターフェースを実装しているため、ファイルポインタを先頭に戻すには、 file.Seek(0, 0) を使用することができます。 まとめ 今回は、Go 言語で multipart/form-data 形式を使って画像を受け取り、外部 API に送信する方法を紹介しました。echo フレームワークと mime/multipart パッケージを利用することで、画像の受け取りや送信が簡単に実装できます。しかし、まず multipart/form-data 形式のリクエストの仕組みを理解することが重要だと感じました。また、リトライ処理を実装する際には、リクエストボディを元の状態に戻すことを忘れないようにしましょう。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 24 日目の記事です。 はじめに こんにちは。DELISH KITCHEN 開発部 RHRA グループ所属の池です。 RHRA グループでは主に小売向けプロダクトの開発を行なっています。 本記事では、RDS の EBS BurstBalance が枯渇してパフォーマンスが著しく低下した事例について、その調査過程で得られた知見を共有したいと思います。 背景 事象が発生した環境では、以下の構成の RDS インスタンスを使用していました。 MySQL 5.7.44 インスタンスタイプ: db.t3.medium ストレージタイプ: 汎用 SSD (gp2) ストレージサイズ: 30GiB この環境において、BurstBalance が急激に消費され始め、最終的に枯渇してししまい、データベースのパフォーマンスが著しく低下しました。 次の図は BurstBalance が枯渇した際のメトリクスです。 BurstBalance の枯渇 BurstBalance とは まず BurstBalance について理解を深めておく必要があります。 BurstBalance の仕様 BurstBalance は、Amazon EBS の汎用 SSD (gp2) ボリュームに適用される性能指標です。これは、ベースラインパフォーマンスを超えて一時的に高い IOPS を発揮できるクレジットの残量を表します。 gp2 ボリュームの場合、ボリュームサイズに応じて以下の特性を持ちます。 ベースラインパフォーマンス: 3 IOPS/GB、ただし最小 100 IOPS バーストパフォーマンス: 最大 3,000 IOPS 最大クレジット量: 5.4 million I/O クレジット IOPS がベースラインを超える場合にバーストが利用され、最大 3,000 IOPS まで発揮される BurstBalance が完全に枯渇した場合、IOPS はベースラインの値に制限されます。例えば、30GiB のボリュームの場合、ベースラインは 100 IOPS(3 IOPS/GB × 30GB = 90 IOPS ですが、最小値の 100 IOPS が適用されます)となり、BurstBalance 枯渇時はこの値に制限されます。 これらの仕様により、gp2 ボリュームは短期的な高 I/O 要求に対応できますが、継続的に高い I/O 要求がある場合、バーストバランスが徐々に減少し、最終的にベースラインパフォーマンスに制限されることになります。 消費量の計算式 BurstBalance の消費量は以下の式で概算できます 消費量 = (使用IOPS - ベースラインIOPS) × 秒数 例えば、30GiB のボリュームの場合 ベースラインIOPS = 最小量の100IOPS 1分間300 IOPSを維持した場合の消費量 = (300 - 100) × 60 = 12,000 IOクレジット より詳細な gp2 ボリュームの仕様と BurstBalance については、 AWS の公式ドキュメント を参照してください。 調査方法 問題が発生した際、以下の調査を行いました CloudWatch メトリクスの確認 BurstBalance, ReadIOPS, WriteIOPS 等のメトリクスを確認 RDS のエラーログの確認 スロークエリログの確認 Performance Insights の利用 問題のあるクエリの特定と実行計画の分析 調査結果 調査の結果、以下のことが判明しました。 BurstBalance は主に WriteIOPS の増加によって発生 I/O を大きく消費するクエリが存在 次の図は BurstBalance と WriteIOPS の相関を表すグラフになります。 青色が BurstBalance、オレンジ色が WriteIOPS を示しています。WriteIOPS が増加すると BurstBalane が減少することがわかります。 BurstBalance と WriteIOPS の相関 ここで仮に 1 時間平均の WriteIOPS を 600IOPS/second として、そのペースで消費し続けたと仮定した場合に、枯渇するまでの時間を概算で計算すると以下のように計算できます。 1時間あたりの消費量: (600 - 100) × 3600 = 1,800,000 IOPS/hour BurstBalanceを消費し切る時間: 5,400,000 / 1,800,000 = 3時間 また、問題のクエリは複雑な SELECT 文で、実行時に I/O が急上昇していました。 このクエリに対して実行計画を確認したところ、extra に Using temporary, Using filesort という記述があり、内部的なソートの実行と一時テーブルへの書き込みが発生していることがわかりました。 これらの結果から、複雑な SELECT 文の実行により、大量の一時テーブルへの書き込みが発生し、それが BurstBalance の急激な消費につながったと考えられます。 ただ、あるタイミングで突然このクエリが一時テーブルへの書き込みを大量に行うようになった要因については調査中になります。 可能性の一つとして、日々のデータの蓄積によりこのクエリで利用する一時テーブルの容量がオンメモリ内で処理できるメモリ制限を超えてしまい、ディスクに書き込まれるようになった可能性を推察しています。 この部分については引き続き調査中なので、結果がわかったら報告できればと思います。 対策 本事象について、以下の対策が考えられます。 問題のクエリのチューニング インデックスの見直し クエリの書き換え(サブクエリの削除、JOIN の最適化など) ストレージのアップグレード gp2 から gp3 への移行を検討(より予測可能なパフォーマンス特性) ストレージサイズの増加(ベースライン IOPS の向上) 一時テーブルのサイズ制限の調整 tmp_table_size と max_heap_table_size パラメータの調整 インスタンスタイプの見直し より高性能なインスタンスタイプへの移行 定期的なクエリパフォーマンスの監視と改善 システム監視の見直し CloudWatchAlert 等による BurstBalance の監視 Pingdom 等による外形監視 まとめ RDS における BurstBalance の枯渇は、予期せぬパフォーマンス低下を引き起こす可能性がある問題です。今回の事例から、以下の学びを得ることができました 複雑なクエリが I/O に与える影響の大きさ データベース設計とクエリ最適化の継続的な改善の必要性 定期的なパフォーマンスモニタリングの重要性 今回の知見が少しでも皆様の参考になれば幸いです。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 23日目の記事です。 こんにちは 開発本部データ&AIチームでデータエンジニアを担当している塚田です。 今回は先日行われたAWS Summit Japan 2024に現地参加する時間が取れましたので、 昨日の「Kotlin Fest 2024 に ひよこスポンサー として参加してきました!」に引き続きイベントレポートですが、個人的に印象に残ったことや聴講したセッションについて触れたいと思います。 tech.every.tv セッション動画やセッション資料も期間限定で公開されているようなので、気になる方はそちらもご確認いただければと思います。 現地で参加したセッション(1日目のみ) どのセッションも満席で熱量の高さを感じました。 また、セッション内で「生成AI」という単語を聞くことがないと思うほど頻繁に耳にしました。 基調講演 AWSと創る次の時代 1時間30分の大ボリュームだったので詳細は省きますが、 やはりAIについて触れられることが多く「責任あるAI」が何回も出てきており その言葉からもあるようにAIが当たり前になり生活に溶け込んでいくようになっても 常に意識するべきことであることを再認識しました。 Amazon Q Businessの日本語対応 が発表され、使うことができるのを楽しみにしています! Dive deep on Amazon S3 S3をより深く理解するため3つの軸で詳しく説明がありました。 フロントエンドの理解 インデックスの理解 ストレージ層の理解 その他にも Amazon S3 Express One Zone や 誤削除に対して にも言及がありましが、ここでは3つの軸についてまとめたいと思います。 フロントエンドの理解 ピークのトラフィックは1PB/秒を超える規模となっている 利用者側でもできる緩和策がある マルチアップロード 独自実装せずにCommon Runtime(CRT)を利用しベストプラクティスに則る 新しいマウントポイントがいくつか出てきている S3マウントは新しいマウオンとオプションが出ているけれど、いまだにバッドプラクティス それなのに新しいマウントポイントが出てくる理由としては機械学習ニーズが大きい インデックスの理解 350兆のオブジェクトを管理しており、1億リクエスト/秒を処理している 利用状況とキャパシティを常に監視し適切にスケールを行っている プレフィックスごとにPUTとGETのリクエスト数の上限が設定されている それを超えると503を返却する キー配列によって負荷が変わる キー名のカーディなりティは左に寄せる キー名に日付を入れる際はできるだけ右側にする ストレージ層の理解 何百万ものハードドライブを利用して、エキサバイトのデータを保管している データは常に冗長化された環境で保管され保存時、保存後定期的にチェックを行なっている 感想 AWS re:Invent 2023でも同じような内容があったと記憶していますが、日本語で聞くことができ理解がより深まったと感じました。 冗長的にオブジェクト管理をしていたのは理解していましたが、どういう体系で管理されて利用できているかの理解を深めることができました。 S3 Express One Zone の利用場面がいまいちイメージでできていませんでしたが、機械学習などI/O性能が上がることでメリットを受けられるなど理解が進みました。 データ×生成 AI - 事例から学ぶビジネスインパクト創出の方程式 クラウドネイティブが進んでいる企業は生成AI活用も進んでいる傾向がある 活用が進んでいる企業では共通するポイントがある 顧客起点文化 小規模チーム 頻繁な実験 小規模チームでも大規模かつ機微なデータでも迅速に生成AIプロダクトが構築できる Amazon QやKnowledge bases for Amazon Bedrockを活用する 責任あるAIを作るためにも責任共有モデルに基づいた実現が必要になってくる 感想 活用が進んでいる企業に共通するポイントはなるほどと感じる部分が大きく責任あるAIを作っていくためにも上記に記載したもの以外もしっかり考えてアクションしていきたいと思いました。 EXPO セッション以外にも様々なブースがありましたので、少しだけ触れたいと思います。 Partner Solution Expo たくさんのパートナーブースがあり、盛り上がりがすごかったと感じました。 また、利用しているサービスのブースもありましたのでオフラインでの質問や相談ができたのがよかったです。 ご対応いただいた皆様ありがとうございます。 認定者ラウンジ 認定者ラウンジを利用させていただきましたが、利用するのにも多数の参加者が並んでおり資格取得者の多さに驚きを覚えました。 保持している資格によってステッカーがいただけたのですが、おまけでクッキーもいただきました。 あとで知りましたが、AWS Certified Data Engineer - Associateを持っているとクッキーももらえるようになっていたようです。 まとめ たくさんのインプット、知識の更新をすることができ、インプットしたことを業務やプロダクトに還元できるようにしていきたいと強く思える場になりました。 生成AIなど進歩が早い中で頻繁に実験を繰り返し開発する文化をより強くしていきたいと考えています。 1日目最初のセッションが始まる30分前には多数のサインがありました!盛り上がりがすごい!
アバター
はじめに こんにちは、株式会社 エブリー DevEnableグループです。 先日のGo Conference 2024に引き続き、本日、約5年ぶりのオフライン開催となったKotlin Fest 2024にひよこスポンサーとして参加してきました! Kotlin Fest運営の皆様および参加された皆様、お疲れ様でした! 早速参加レポートをさせていただきます。 www.kotlinfest.dev 5年ぶりのオフライン開催 Kotlin Fest 2024は今回5年ぶりのオフライン開催となりました。会場はベルサール渋谷ファーストの2Fを貸し切って、2つのセッションルームと1つのスポンサーブース兼フリースペースという形の大きな会場での開催でした。 スポンサーブースの紹介 エブリーは8年前からDELISH KITCHENアプリでKotlinを採用しています。Kotlin採用に対する振り返りを当時採用したCTO自らが綴っているのでご覧ください。 tech.every.tv いつもKotlinコミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献していきたく、スポンサーとして協賛させていただき、ブースも出展しました! ブース エブリーでは、弊社が提供するDELISH KITCHENのサービスをイメージしてブースの雰囲気を作っています。今回も多くの方からDELISH KITCHENを使っていますとの声をかけていただき、実際に使っていただいている方の声を聞ける貴重な機会で僕たち開発者もパワーをもらえました。 ノベルティ 今回は以下のようなノベルティを用意させていただきました。 クリアファイル 会社とサービスのステッカー DELISH KITCHENグッズ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。 用意してた全てのグッズがなくなるほど好評で多くの方に参加していただけました! アンケート 今回、アンケートでは『KotlinのLinter、なにを使ってる?』と題して回答をしてもらいました。また、シールの色でKotlinをAndroid開発で使っているか、サーバーサイドで使っているかがひと目でわかるような工夫もあり、参加者が楽しんでいただけるような内容を考えました。回答いただいた多くの皆様、ありがとうございました! 最終結果はこちら...! Androidエンジニア 👑1位: ktlint 2位: Android Lint 3位: detekt サーバーサイドエンジニア 👑1位: detekt 2位: ktlint 3位: その他(Konsist, CheckStyle) 全体ではktlintが1位となり、枠をはみ出すほど多くの方の回答が集まっていました。一方でサーバーサイドエンジニアの中では、ktlintとdetektを使っている方の割合がほぼ同じ僅差という面白い結果となっています。 ブースに来てくださる方はAndroidエンジニアの方が割合としては多いですが、サーバーサイドでKotlinを活用している方も多くいらっしゃり、両プラットフォーム上でのKotlinの盛り上がりを肌で感じました。 各社スポンサーブースの様子 スポンサーブースは、2つのセッションルーム間で開催され、オープニング前やセッション間での休憩中に多くの人で賑わっていました。こういった光景を見られるのもオフラインならではです。 各社のブースもそれぞれの会社の特色がノベルティや出し物から出ており、スタンプラリーをしながら楽しませてもらいました。 特にエムスリーさんのブースではKotlinのモチーフである『トリ』に関連して、『エンジニアトリ診断』を受けることができ、Kotlinや開発に関するいくつかの質問に答えることでトリタイプが診断されます。僕の結果は...『はやぶさ』タイプでした! セッションの紹介 今回発表されたセッションの中から気になったものをいくつかまとめさせていただきました。 パフォーマンスと可読性を両立:KotlinのCollection関数をマスター 発表者: Masayuki Sudaさん fortee.jp こちらのセッションでは、Kotlin の Collection 関数を有効活用する方法を紹介されていました。 Collection 関数の中には変換を行う Map や Zip、フィルタリングを行う filter や partition、グループ化を行う groupBy、部分取得を行う slice など多種多様な関数が用意されていますが、一つ一つ具体的なソースコードと処理結果を説明してくださいました。 その中で、+ 演算子や - 演算子を扱い要素の追加や削除を行う場合は新しいリストが生成されるためメモリが消費しやすい、windowed を使うと計算量が多くなり、パフォーマンスに影響が出るなど、細かい課題や問題点まで説明してくださっていた点が印象的でした。 また最後に Collection、Sequence、for の3 つでパフォーマンスの最適化の観点で、 Collection … 非常に優秀。即時評価を行い、すべての要素を評価する仕組みで、リストが大きい場合はパフォーマンスに難有り。 Sequence … 遅延評価を行う仕組みで、大規模なコレクションに対して複数の処理を行う場合に向いている。 for … 最速。 といったまとめをしてくださっていました。プログラムを作成するうえではパフォーマンスは重要なため、常に説明してくださった観点は意識しようと改めて感じました。 なお、この記事内では一部の関数のみ抜粋して紹介しましたが、Collection 関数自体はまだまだ多く提供されており、使ったことがないものも多々あるため、これを期に勉強し直したいと強く感じました。 withContextってスレッド切り替え以外にも使えるって知ってた? 発表者: T45Kさん fortee.jp こちらのセッションでは、withContext をスレッドの切り替え以外でも使うことができる、という内容を紹介されていました。 Coroutines では API の処理は withContext(Dispatchers.IO)、計算量が多い処理は withContext(Dispatchers.Default)、UI 関連の処理は withContext(Dispatchers.Main) と、スレッドの切り替えのために使用することがあり、私自身このためだけに使用するものと認識していましたが、実際は CoroutineContext を切り替えるものであり、スレッドを切り替える以外の用途で利用されるものと紹介され、しっかりと理解していないまま利用していると反省するきっかけになりました。 なお、コンパイル時に Context の検査が行われる、Coroutines でなくとも使用ができる Context Parameter というものが Kotlin 2.1 で導入予定となっているそうで、これから注目して追っていきたいと思います。 KotlinのLinterまなびなおし2024 発表者 nyafunta9858さん fortee.jp こちらのセッションでは、KotlinのLinter導入へのモチベーションや各Linterについての特徴について解説されていました。 アーキテクチャやコードルールをLinterを用いてプロジェクト内で統一するモチベーションの一つとして、メンバーの入れ替わりが挙げられていました。ルールを統一することで、メンバーの入れ替わりに影響されずコードの品質を担保できます。 everyはエンジニアが様々なプロダクトを経験することができる環境ですので、Linterの運用は強力な武器になると最認識しました。 またブースではどのLinterを使用しているかアンケートを取りました。結果としてはktlintが多く、次点でdetectでした。この発表やアンケート結果を弊社プロダクトに活かしていければと思います。 Jetpack Compose: 効果的なComposable関数のAPI設計 発表者: haru067さん fortee.jp こちらのセッションでは、主にComposable関数の引数の注意点とテクニックについて紹介されていました。 引数はそのコンポーネントの再利用性や拡張性を考えて書く必要があり、その中で有効なテクニックとして以下が紹介されていました。 stateはコンポーネント内で閉じてもよいなら閉じる。他コンポーネントと状態を共有したい場合は親に委ねる。 引数はパフォーマンスの観点から基本的にフラットに記述する。データクラスを用いると、他画面との兼ね合いで無駄な再描画を引き起こす可能性がある。引数が多くなった場合はクラスへ切り分けることを考慮する。 デフォルト引数は安易に設定しないように気をつけ、値をつど考えることが有益な場合は設定しないようにする。 など開発の際に誰もがどうしようか考える内容に触れられており、大変面白かったです。 特に引数をフラットに書く話は、パフォーマンスに直結するので気をつけようと思います。 最後に 最後になりますが、Kotlin Fest 2024の運営の皆さん、カンファレンスの運営をしていただき本当にありがとうございました! また、参加者の皆さん、カンファレンスへの参加お疲れ様でした。 弊社も当日多くのエンジニアが参加し、セッションを聞きながらKotlinコミュニティから刺激を受けるいい機会となりました。今後もイベントやこういったスポンサー活動を通じてKotlinコミュニティに貢献していきたいと思います! 今回参加できなかった皆様もぜひ来年は参加してみてください。 エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 21 日目の記事です。 はじめに こんにちは、エブリーでCTOをしている今井です。 Kotlin Fest 2024 の開催がいよいよ明日に迫ってきました。 エブリーでは8年前にDELISHKITCHENのアプリを作り始めた時からKotlinを使っており、 今回KotlinFestを通じて、Kotlinコミュニティに貢献できることを嬉しく思っております。 このブログでは、Kotlinを採用した当時の状況や、採用して良かったことに関して振り返ってみたいと思います。 Kotlinの採用 FirstCommitのスクリーンショット 改めてコミットを見たところ、Kotlinを初めて導入したのはなんと2016年のFirstCommitからでした。 当時のバージョンは1.0.3で、その時はまだGoogleの正式なサポートもまだ発表される前 1 で、 Androidアプリ開発においてまだまだJavaが主流。かつこんなにKotlinが急激に浸透するとは思ってもいませんでした。 また、開発を進めるにあたってもKotlinの導入事例はとても少なく、ちょっとしたことでも調べるのに苦労したことを覚えています。 当時のコードを振り返ると、たくさん拡張関数作って、ActivityやFragmentをシュッと書きたいという 自分なりにKotlinらしく書くことを模索した形跡がありました。 FragmentUtil.kt fun SupportFragment.inflate(layoutResId: Int , inflater: LayoutInflater?, container: ViewGroup?): View? = inflater?.inflate(layoutResId, container, false ) fun Fragment.inflate(layoutResId: Int , inflater: LayoutInflater?, container: ViewGroup?): View? = inflater?.inflate(layoutResId, container, false ) MainFragment.kt class MainFragment : AbstractFragment() { companion object { @JvmStatic fun newInstance() = MainFragment() } override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflate(R.layout.fragment_main, inflater, container) } これ自体は今思うとまだまだ未熟な書き方も多く恥ずかしい部分も多いですが、 Kotlinを採用したことでの恩恵も多く受けたと思います。 Kotlinの恩恵 Null-Safety 一番の恩恵は確実にNull-Safetyにありました。 当時Androidは自分一人での開発だったためレビューもしてもらえない環境かつ、 その時点での自分のAndroid開発経験も半年ほどと未熟だった中で、 リリース当初からCrashFreeRateが99.9%という高いスコアを出せていたのは、 Kotlinを採用していなければ実現できなかったと思います。 シンプルで短く書ける また、シンタックスがシンプルで短く書けることも初期の開発速度に寄与していたと思います。 上記で紹介した拡張関数やLambdaなどは使えるところは意地でも使うくらい、ハマってました。 Null-Safetyではどうしてもその中身のチェックに記述が増えがちなところあると思いますが、 SmartCastなどにより、書きやすさを維持したままより安全なコードが書けるも良かったです。 書いていて楽しい Kotlin自体の機能ではないですが、上記の恩恵などから、書いていて楽しかったというのも、 今思うと恩恵として大きかったと思います。まだまだ未知なものを自分の手で開拓していく感じも面白かったです。 まとめ エブリーではAndroidアプリの開発当初からKotlinを採用し、Kotlinに支えられてきました。 同様に弊社のあらゆるエンジニアは多くの技術やオープンソースに支えられています。 これらを盛り上げる技術コミュニティに支援をすることは、会社にとってもの技術投資でもあると考えています。 今回のKotlinFestをはじめ、今後も技術コミュニティにも積極的に支援していきたいと考えております! 余談: フルKotlinのアプリでストア総合一位はDELISHKITCHENが初? Kotlinが採用されたGoogleI/O 2017には現地で参加していたのですが、 ちょうどそのとき日本のストアで総合1位になってました。 今日本で1位のアプリはフルKotlinだというのをGooglerに話したところ、 フルKotlinとか聞いたことない / 多分ランキング初めてなんじゃないか? と言っていただいたので、個人的には勝手に弊社のアプリが初めてだと思ってますw GoogleI/O 2017の時のストアのスクリーンショット GoogleI/O 2017で正式サポートが発表 された ↩
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 20 日目の記事です。 はじめに こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している岡田です。 今回は KSP 化について執筆させていただきます。 概要 Kotlin でより効率的な開発を行う上で、アノテーション処理は欠かせない要素です。アノテーションを利用することで、定型的なコードを自動生成したり、コンパイル時にコードの検証を行ったりすることができます。 Kotlin のアノテーション処理といえば、従来は kapt が主流です。しかし、近年ではより高速でKotlinの言語機能を活かせる KSP が主流になりつつあります。 この記事では、kapt と KSP について調べ、 Android プロジェクトの KSP 化を検討するにあたって必要な情報を記述します。 kapt について kapt (Kotlin Annotation Processing Tool) を使用すると、 Java アノテーションプロセッサを使用して Kotlin コードを処理できます。 Room などの Android 開発でよく使用される、多くのライブラリと連携しています。 しかし、 Kotlin ファイルから Java アノテーションプロセッサが読み取れるようにするためには Java スタブを生成する必要があります。 実際に生成された Java スタブ は build/generated/source/kapt/ で確認することができます。 例えば以下は実際に DELISH KITCHEN で Hilt によって生成される、 ArticleListActivity のインスタンスに依存性を注入するためのコードです。 ArticleListActivity_GeneratedInjector.java @OriginatingElement( topLevelClass = ArticleListActivity.class ) @GeneratedEntryPoint @InstallIn(ActivityComponent.class) public interface ArticleListActivity_GeneratedInjector { void injectArticleListActivity(ArticleListActivity articleListActivity); } .*_GeneratedInjector.java は @AndroidEntryPoint や @Inject など、依存性を注入する必要がある全箇所に対して生成されます。 この例のような Java スタブが、 Hilt に関してのみでもプロジェクト内で相当な数生成されています。 Java スタブの生成は高コストのオペレーションのため、処理速度が遅く、ビルド速度に大きく影響してしまうのです。 また 2024/6/20 時点で kapt はメンテナンスモード に入っています。 「既に新しい機能が実装されることはない」と明記されており、 KSP の使用が推奨されています。 KSP がサポートされていないライブラリを用いたいなどの理由がない限りは、 kapt ではなく KSP を使用する方が良いでしょう。 KSP について KSP (Kotlin Symbol Processing) は、 Kotlin コードを直接処理するためのアノテーション処理ツールです。 kapt とは異なり、 Kotlin コンパイラのプラグインとして動作するため、 Java コードへの変換が不要です。 つまり kapt のように Java スタブを生成する必要がなくなり、処理速度が大幅に上昇し、ビルド速度も速くなります。 Googleが公開しているGithub によると、「 kapt と比較すると、 KSP を使用するアノテーションプロセッサは最大 2 倍高速に実行できる」そうです。 また Kotlin の公式ドキュメント では、コード生成に 7.5 倍ほどのパフォーマンス差があることも確認されています。 For performance evaluation, we implemented a simplified version of Glide in KSP to make it generate code for the Tachiyomi project. While the total Kotlin compilation time of the project is 21.55 seconds on our test device, it took 8.67 seconds for kapt to generate the code, and it took 1.15 seconds for our KSP implementation to generate the code. 仕組みとして、 Kotlin プログラムを Kotlin の文法に沿ってシンボルレベルでモデル化しているそうです。 KSP は Kotlin プログラムの構造を、クラスや関数などの構成要素単位で処理することができます。 Kotlin プログラムの構造をより抽象化された形で捉えて処理することで、アノテーション処理などのタスクを効率的に行います。 ただし、 if 文や for ループなどの制御構文にはアクセスできず、細かい制御フローの解析には向いていません。 kapt から KSP へ移行する方法は、 公式ドキュメント を参照ください。 また KSP の最新のリリースは こちら から確認できます。 今後のロードマップ には、マルチプラットフォーム対応やパフォーマンス改善も挙げられています。 実際の速度比較 では実際に KSP 化すると、 Kotlin のタスクはどの程度速度が上がるのでしょうか。 今回は簡単に、 Room でデータを Insert するだけの簡単なアプリを作成して比較しました。 測定方法は Android Studio の Build > Clean Project 実行後、 Build > Rebuild Project を実行し、 Build Analyzer にて測定しました。 データはそれぞれ 10 回ずつ測定し、画像は平均に一番近いデータを添付しています。 kapt KSP 1.9s 1.4s 結果として、およそ 0.5s ほど KSP の方が早かったです。 kapt で生成された Java スタブとして、以下のファイルが確認できました。 AppDatabase_Impl.java UserDao_Impl.java これらは Room の Database と Dao 関連のスタブです。たった 2 ファイルの生成ですが、如実な差を確認することができました。 Room や Hilt を導入しているプロジェクトでは、大幅な速度上昇が見込めそうです。 KSP の注意点 kapt と KSP の併用する際は注意が必要です。 Room などの主要なライブラリは KSP に対応していますが、 KSP に対応していないライブラリも存在します。 こちら で現在サポートされている主要ライブラリを確認することが可能です。 例えば 2024/6/20 時点で、 Auto Factory は KSP をサポートしていません。 またプロジェクトのライブラリ更新が滞っており、そちらを先に対応しないと KSP 化できないという場合も想定できます。 プロジェクトでこれらのような KSP に未対応のライブラリを導入している場合は、一部のみを KSP 化し、 kapt と併用する形になるでしょう。 しかし kapt と KSP の併用は、ビルド時間が増加することがあります。 これは同じアノテーションプロセッサを kapt と KSP の両方で実行する場合に処理が重複してしまうためです。 高速なアノテーション処理を期待して KSP を導入したのに、逆に処理が遅くなるという事態に陥ります。 まとめ この記事では、 Kotlin のアノテーション処理ツールである kapt と KSP について記述しました。 kapt は Java のアノテーションプロセッサを Kotlin で利用できるツールですが、 Javaスタブ の生成が必要なためビルド時間が長くなるという欠点があります。 また、現在メンテナンスモードに入っているため、今後 KSP への移行が推奨されています。 KSP は Kotlin コードを直接処理できるため、 kapt よりも高速にアノテーション処理を行うことができます。 公式ドキュメントによると、 kapt と比較して最大2倍、コード生成では 7.5 倍ほどのパフォーマンス差があるそうです。 KSP 化を進める際は、 KSP に対応していないライブラリが存在することに注意が必要です。 そのようなライブラリを使用している場合は、 kapt と KSP を併用する必要がありますが、ビルド時間が増加する可能性があるため注意が必要です。 おわりに Kotlin Fest 2024 まで、あと 2 日! https://www.kotlinfest.dev/ 株式会社エブリー は、ひよこスポンサー として Kotlin Fest 2024 に参加します。 ぜひ、ブースでお会いしましょう!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 19 日目の記事です。 はじめに こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している kikuchi です。 Kotlin Fest 2024 の開催が近づいてきましたので、今回は折角の機会ですので Kotlin に関わる話として DELISH KITCHEN で一部の処理を LiveData から Kotlin Coroutines Flow に移行した話をまとめてみたいと思います。 移行を考えた背景 現状 DELISH KITCHEN ではアーキテクチャに MVVM (Model View ViewModel) を採用しており、ViewModel で更新された LiveData を View が observe するという一般的な実装となっていますが、今回 Flow を調査する過程で オペレータ (map など) が使用できる Null 安全性が保証される 既に通信周りを Coroutines で実装していたため、Flow に移行しやすい ワンショット通知を無理やり LiveData で実現していた箇所を適切な方法に修正ができる (SharedFlow の利用) 新しい技術の習得 というメリットを感じ、今回新規で実装する箇所から徐々に Flow を採用することを決断しました。 特に新しい技術の習得というのはエンジニアにとって成長に繋がる良い機会ですので、積極的に取り入れたいと考えました。 LiveData と Flow の違いについて 早速ですが、本項目では LiveData と Flow の違いを細かくまとめていきます。 LiveData について LiveData とは Android Jetpack の一部であり、Android のライフサイクルに対応した監視ができる仕組みです。 監視のタイミングですが、オブザーバーのライフサイクルの状態が STARTED か RESUMED の場合のみ LiveData の変更通知を受け取ることができます。 オブザーバーを Activity や Fragment のライフサイクルと紐づけている場合、onStarted や onResume の場合 (画面がアクティブな状態) でのみ通知を受け取ることができ、 onPause や onStop の場合 (画面が非アクティブな状態) では通知を受け取ることができません。 また observe の処理さえ定義しておけば自動的に変更通知を受け取れるようになります。 つまりは実装上で明示的にオブザーバーを開始・終了する必要は無く、また画面がアクティブであるかの判定も不要ということになります。 上記をふまえて、オブザーバーを Activity のライフサイクルに紐づけてデータを監視する簡単な実装例を載せたいと思います。 // ViewModel 側の実装 class TestViewModel { private val _testData = MutableLiveData<Int>() val testData: LiveData<Int> = _testData fun updateTestData() { _testData.postValue(1) ...① } } // View 側の実装 class TestActivity : AppCompatActivity { private val viewModel: TestViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { viewModel.testData.observe(this) { data -> ...② data ?: return@observe ...③ // 監視しているデータの更新通知を受け取ったら実行する処理 updateView(data) } } } ViewModel 内部では更新可能な MutableLiveData を更新し、連動して外部に公開している LiveData も更新されるように実装することで、 ① のように ViewModel 内で値を更新すると、② の observe の処理が自動的に発火する挙動となります。 実装例を見て分かる通り、明示的にライフサイクルに応じて監視を開始・終了する処理はしていません。 ここで 1 つ注意点があり、LiveData は Java で実装されているため Null 安全性は保証されておらず、Null が設定されてしまう可能性があるため、 念の為 ③ のような Null チェックが必要となります。 Flow について Flow とは Kotlin Coroutines 上で非同期にデータを取り扱うための仕組みです。 Flow には常に最新の値を保持する StateFlow と、replay パラメータで設定した数分の過去の値を保持できる SharedFlow が存在しますが、 今回は StateFlow を利用してオブザーバーを Activity のライフサイクルに紐づけてデータを監視する実装例を載せたいと思います。 // ViewModel 側の実装 class TestViewModel { // MutableStateFlow では初期値が必要 private val _testData = MutableStateFlow<Int>(0) val testData: StateFlow<Int> = _testData fun updateTestData() { _testData.value = 1 ...① } } // View 側の実装 class TestActivity : AppCompatActivity { private val viewModel: TestViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { ...② repeatOnLifecycle(Lifecycle.State.STARTED) { ...③ viewModel.testData.collect { data -> ...④ // 監視しているデータの更新通知を受け取ったら実行する処理 updateView(data) } } } } } LiveData と同様、ViewModel 内部では更新可能な MutableStateFlow を更新し、連動して外部に公開している StateFlow も更新されるように実装することで、 ① のように ViewModel 内で値を更新すると、オブザーバー側で通知を受け取ることができます。 ただし LiveData と異なる点は、② のように必ず lifecycleScope.launch のスコープ内であることを明示する必要があり、③ のようにどのライフサイクルで通知を受け取るか 明示する必要があります。実装例だと画面がアクティブな場合に受け取れるよう repeatOnLifecycle(Lifecycle.State.STARTED) を指定しています。 (上記 ② と ③ の指定をすることで、LiveData のケースと同じ契機で更新通知を受け取ることができます) データを受け取る処理は ④ のように collect で受け取ります。 Flow は LiveData と違い Kotlin で実装されているため Null 安全性が保証されており、Null チェックは不要となります。 また、具体的な例は割愛しますが Flow はオペレータを使用できるため、直接 Flow 型のデータを操作する場合は filter や map などを使用することができます。 こちらは LiveData に無い Flow の強みと言えます。 LiveData と Flow の比較 今回は簡単な実装例のみ載せましたが、ほぼ同じような挙動にすることが可能だと分かりました。 ただ細かい部分で違いがありましたので、違いを一覧でまとめてみたいと思います。 LiveData Flow 実装の複雑さ 低 やや複雑 Null 安全性の保証 されていない (が実装でカバーできる) されている オペレーター使用可否 使用不可 使用可能 監視タイミングの管理コスト 低 (Android のライフサイクルと連動) 中 (実装で明示する必要あり) KMP での使用可否 不可 可能 移行にあたって苦労した点 LiveData と同じ用途で実装しようとするとどうしてもコードの記述量が増えてしまいましたが、今回は紐づけるライフサイクルを変更する必要が無いため、 以下のように拡張関数を定義することでコードの煩雑さを解消しました。 // 拡張関数 fun <T> Flow<T>.observe(viewLifecycleOwner: LifecycleOwner, action: (T) -> Unit) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { action(it) } } } } // オブザーバー側 class TestActivity : AppCompatActivity { override fun onCreate(savedInstanceState: Bundle?) { viewModel.testData.observe(this) { data -> updateView(data) } } } また、移行当初は Android Jetpack Lifecycle ライブラリで使用されており、かつ現在は Deprecated となっている lifecycleScope.launchWhenStarted で定義していたことで、 オブザーバー側が STOPPED となってもコルーチンがキャンセルされず停止された状態のままとなっており、リソースが浪費されている問題が発生していました。 こちらは前述した viewLifecycleOwner.repeatOnLifecycle を使用することで、意図通りオブザーバー側が STARTED 状態になる度に実行し、STOPPED になる度にキャンセルされる 挙動となったため回避できたのですが、Deprecated は常に意識して日々改修していく重要さを改めて痛感しました。 まとめ LiveData は Android のライフサイクルが考慮されているなど Android アプリに対して最適化された作りとなっており、対して Flow はオペレータの使用が可能である、 Null 安全性が保証されているなど、より Kotlin の恩恵を受けることができるため、どちらを採用する場合でもメリットがあります。 そのため開発するアプリの規模や実現したい機能、学習コストなどからどちらを採用するか検討すると良いかと考えています。 近年サーバーサイド Kotlin の導入事例も増えてきていますので、アプリエンジニアでもサーバー開発をすることを見越して Kotlin の標準 API である Flow を採用する、 という考え方もあるかもしれません。 エブリーではこれからも職種にとらわれずいろいろな事に挑戦できる環境を作って行きたいと考えています。 今回紹介した内容が少しでも皆さまのお役に立てれば幸いです。 おわりに Kotlin Fest 2024 まで、あと 3 日! https://www.kotlinfest.dev/ 株式会社エブリー は、ひよこスポンサー として Kotlin Fest 2024 に参加します。 ぜひ、ブースでお会いしましょう!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) の18日目の記事です DELISH KITCHEN 開発部で小売様向き合いで主にアプリ開発をしている野口です。 Flutterエンジニアをしておりますが、直近インフラやサーバーサイドもやらせていただいております。 今回は事業譲渡されたネットスーパーアプリでインフラで使用しているFJcloud-V(旧ニフクラ)で抱えていた問題を一部AWSに置き換えることで解決した話について紹介します。 課題 課題①  SSL証明書更新対応に工数が掛かる SSL証明書はFJcloud-Vで管理しており、SSL証明書の更新には以下のフローが必要になります。(やりとりが大変だとわかっていただければいいので内容は理解しなくてOKです。) 小売様の承認 小売様それぞれがドメインを管理しており、whois 情報内のメールアドレス所持者(=小売様内の特定の部署または人)による承認が必要 メールの内容に沿って承認手続きを行なってもらう エンジニアが、FJcloud-Vの管理画面から更新情報を入力し、証明書更新申請を行う 小売様に証明書更新の承認をしていただく メールの内容に沿って承認手続きを行なってもらう エンジニアが証明書更新が成功したことを確認し、ロードバランサーに紐づくSSLアクセラレーター更新する 小売様とやりとりしながら更新しなければいけないですし、有効期限ギリギリの場合は小売様を急かすことになり、最悪有効期限が過ぎてしまいサイトを一時閉鎖しないといけないリスクがあります。 また、現在弊社ではネットスーパーアプリを十数社の小売様に提供しているため、この対応を小売様分で行わないといけないのでとても工数が掛かってしまいます。 課題② FJcloud-Vのロードバランサーが小売様ごとに個別に建てられていて費用がかさんでいる FJcloud-Vのロードバランサーはいくつか種類があるのですが、ネットスーパーで使用しているものはロードバランサー(L4)になります。 https://pfs.nifcloud.com/service/lb.htm ロードバランサー(L4)は複数ドメインへの対応ができなく、小売様ごとに個別に建てられているため、十数小売様分のロードバランサー費用が掛かっています。 解決方法 課題① 前提としてドメインは小売様のものですが、DNSはエブリー管理なのでDNSレコードの作成をエブリーが行うことは可能です。 なので、DNSレコードを作成し、 ALB のDNSの検証を行うことで小売様の承認手続きが不要になります。 また、既存のFJcloud-VのロードバランサーはSSLの終端になっており、この構造を保つために、ロードバランサーをAWSの ALB に移行し、証明書を ACM に移行することで解決できます。 課題② ALBは小売様ごとではなく、ネットスーパー全体のロードバランサーを作ることで解決できます。 つまり、ネットスーパー全体のALBとACMで証明書を小売様ごとに作成し、ALBに各小売様用の証明書を紐づけることで解決しました。 構成図はこのようになります。 Route53では複数の小売様のドメインを管理し、ACMから取得した各小売様用の証明書をALBに設定しています。 FJcloud-VのネットワークとVPCは Site-to-Site VPN で疎通できるようにしてあります。 やったこと 以下にはTerraformの実装を記載しています。 FJcloud-Vのサーバーとロードバランサーが疎通できるようにターゲットグループを指定する FJcloud-Vのipに向くターゲットグループを作成します。 # ターゲットグループを作成 resource "aws_lb_target_group" "nifcloud_server" { name = "nifcloud-server" port = 80 protocol = "HTTP" target_type = "ip" } # FJcloud-Vに向くように設定 resource "aws_lb_target_group_attachment" "nifcloud_server" { target_group_arn = aws_lb_target_group.nifcloud_server.arn target_id = & { 指定したいIP } } ALBを作成してターゲットグループを紐づけを行う # ALB作成 resource "aws_lb" "server" { name = "server" internal = false load_balancer_type = "application" } # ターゲットグループで設定したFJcloud-VのIPと通信するようにする resource "aws_lb_listener" "server_https" { load_balancer_arn = aws_lb.server.arn port = 443 protocol = "HTTPS" default_action { type = "forward" target_group_arn = aws_lb_target_group.nifcloud_server.arn } } Route 53のホストゾーンと小売様用のalias レコード (ALB に向ける) を作成する # 小売様Aのホストゾーンの作成 resource "aws_route53_zone" "martA" { name = "martA.example.com" } # 小売様Aのaliasレコードを作成 resource "aws_route53_record" "martA_a" { zone_id = aws_route53_zone.martA.zone_id name = "martA.example.com" type = "A" } DNS (FJcloud-V管理) を Route53 に向ける DNSはFJcloud-Vで管理されているので、FJcloud-VからRoute53に向くようにします。 以下の記事のようにRoute53で作成したmartAのホストゾーンのNSレコードをドメインレジストラのネームサーバに設定をすることでFJcloud-VからRoute53に切り替えることができます。 https://dev.classmethod.jp/articles/route53-domain-onamae/ Route53に検証用のCNAMEレコードを登録し、ACMで証明書を発行する # 小売様Aの証明書を発行 resource "aws_acm_certificate" "martA_cert" { domain_name = local.martA_domain_name validation_method = "DNS" } # Route53に小売様Aの検証用のCNAMEレコードを登録 resource "aws_route53_record" "martA_cert" { for_each = { for dvo in aws_acm_certificate.martA_cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } allow_overwrite = true name = each.value.name records = [ each.value.record ] ttl = local.cname_default_ttl type = each.value.type zone_id = aws_route53_zone.martA.zone_id } # 小売様Aの証明書の検証 resource "aws_acm_certificate_validation" "martA_cert" { certificate_arn = aws_acm_certificate.martA_cert.arn validation_record_fqdns = [ for record in aws_route53_record.martA_cert : record.fqdn ] } 証明書の検証が通ったらALBに証明書の紐付けを行う resource "aws_lb_listener_certificate" "server_martA" { listener_arn = aws_lb_listener.server_https.arn certificate_arn = aws_acm_certificate.martA_cert.arn } 作成したALBに対して上記のようにすることで1つのALBに複数の証明書を紐づけることができます。 まとめ 今回の対応で全ての小売様に適用すれば、十数小売様のロードバランサーを1つにすることができます。 FJcloud-Vのロードバランサーが、1ヶ月あたり1万円で1年で12万、小売様が15と仮定すると 12 ✖️ 15で 180万円掛かっていたところ https://pfs.nifcloud.com/price_extax/network.htm#load ALBはざっくり概算で1ヶ月あたり5000円で1年で6万円になるので 180 - 6 = 174でだいたい150万円くらいはサーバー費用を浮かせることができます。 また、初めてインフラの構築を行いましたが、クライアントのお仕事と違って、連携するサービスが多く気にしないといけないことが多岐に渡るなと感じました。 ご覧いただきありがとうございました。
アバター
こちらは every Tech Blog Advent Calendar 2024(夏) 17日目の記事になります。 こんにちは。 開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 変わらずML周辺の開発をもりもりしています。 今回は、DatabricksのFeature Storeについて検証した内容を共有します。 Databricks Model Servingについても検証記事をまとめていますので、ぜひご覧ください。 tech.every.tv 背景 現状、DELISH KITCHENを中心に、ML開発を進めており、今後のML開発のagility向上を見据えて、Feature Storeを導入することを検討しています。 DatabricksではFeature Storeを使う理由を、下記のようにまとめています。 参考 特徴量ストアとは |Databricks on AWS 発見性 :Databricks ワークスペースからアクセスできるFeature Store UIでは、既存の特徴量を参照および検索できます。 リネージ :Databricksで特徴量テーブルを作成すると、特徴量テーブルの作成に使用されたデータソースが保存され、アクセスできるようになります。特徴量テーブルの各機能について、その機能を使用するモデル、ノートブック、ジョブ、エンドポイントにアクセスすることもできます。 モデルのスコアリングやサービングとの統合 :Feature Storeの特徴量を使用してモデルをトレーニングする場合、モデルは特徴量メタデータと一緒にパッケージ化されます。モデルをバッチスコアリングまたはオンライン推論に使用すると、Feature Storeから自動的に特徴量が取得されます。呼び出し側はこれらの特徴量について知る必要はありませんし、特徴量を検索または結合して新しいデータをスコアリングするロジックを組み込む必要もありません。これにより、モデルのデプロイメントや更新が容易になります。 ポイントインタイムのルックアップ :Feature Storeは、特定の時点での正確性を必要とする時系列およびイベントベースのユースケースをサポートします。 上記含めて、DatabricksのFeature Storeに関するドキュメントを読んだ際、以下のような疑問を持ちました。 「Feature Store使わずに、Deltaテーブルから学習/推論すれば良いのでは?」 このように思った理由は、一般的なFeature Storeを使うメリットを、Unity CatalogとDeltaテーブルの機能でカバーできているのではないかと思ったからです(Unity Catalogに関しては こちら をご確認ください)。 これらを踏まえて、Feature Storeを 使う場合 と 使わない場合 の比較を実施しました。 検証した内容の共有の前に、前提条件となる弊社のDatabricksを活用したML開発状況をサマリます。 ML開発はスモールスタート中で、モデル開発は1人もしくは2人 バッチ推論がメインで、推論結果をサーバーのRedis(ElastiCache for Redis)にデプロイする運用 DatabricksのUnity Catalogは最近使えるようになったばかり Feature Storeを使う場合と使わない場合の比較 Basic example for Feature Engineering in Unity Catalog という、Databricksが提供しているサンプルノートブック(ワインの品質を予測するモデル作成例)を使って、Feature Storeを 使う場合 と 使わない場合 の比較を行いました。 事前準備 実行環境はDatabricks 13.3LTS Runtimeを使用し、ノートブックの最初に以下を実行しています。 %pip install databricks-feature-engineering dbutils.library.restartPython() サンプルノートブックのうち、 使う場合 と 使わない場合 のどちらでも使用するコードを以下に定義します。 細かい説明はしませんが、 wine_qualityのサンプルデータセット を準備し、Unity CatalogとFeatureEnginneringClientを使えるようにしています。 import pandas as pd from pyspark.sql.functions import monotonically_increasing_id, expr, rand from databricks.feature_engineering import FeatureEngineeringClient, FeatureLookup import mlflow import mlflow.sklearn from mlflow.tracking.client import MlflowClient from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score raw_data = spark.read.load( "/databricks-datasets/wine-quality/winequality-red.csv" , format = "csv" , sep= ";" , inferSchema= "true" , header= "true" ) def addIdColumn (dataframe, id_column_name): """Add id column to dataframe""" columns = dataframe.columns new_df = dataframe.withColumn(id_column_name, monotonically_increasing_id()) return new_df[[id_column_name] + columns] def renameColumns (df): """Rename columns to be compatible with Feature Engineering in UC""" renamed_df = df for column in df.columns: renamed_df = renamed_df.withColumnRenamed(column, column.replace( " " , "_" )) return renamed_df # Run functions renamed_df = renameColumns(raw_data) df = addIdColumn(renamed_df, "wine_id" ) # Drop target column ("quality") as it is not included in the feature table features_df = df.drop( "quality" ) display(features_df) 下記コードにはreal_time_measurementという特徴量の集計があります。 これが具体的にどんな特徴量なのか想像し難いですが、今回議論したい内容ではないため、Feature Storeに保存されていない特徴量という認識で進めます。 ## inference_data_df includes wine_id (primary key), quality (prediction target), and a real time feature inference_data_df = df.select( "wine_id" , "quality" , ( 10 * rand()).alias( "real_time_measurement" )) display(inference_data_df) spark.sql( "USE CATALOG uc_sandbox" ) spark.sql( "USE SCHEMA naoki_furuhama" ) table_name = "wine_db" fe = FeatureEngineeringClient() 特徴量テーブルの作成 Feature Storeを 使う場合 fe.create_table( name=table_name, primary_keys=[ "wine_id" ], df=features_df, schema=features_df.schema, ) Feature Storeを 使わない場合 features_df.write \ .mode( "overwrite" ) \ .saveAsTable(table_name) Feature Storeに特徴量テーブルとして保存する場合はFeatureEnginneringClientのcreate_tableメソッドを使います。 テーブルを作成して、スキーマを見るという観点だけはどちらも同じように見ることができます。 違いとして、Feature Storeを使う場合はprimary_keyの指定が必須で、primaly_keyを意味する PK がカラムの横に表示されているのがわかるかと思います。 なお、使わない場合で保存したとしても、Unity Catalog内のDeltaテーブルであれば、ALTER TABLEすることで、特徴量テーブルにすることもできます。 参考 Unity Catalogでの特徴量エンジニアリング | Databricks on AWS 特徴量テーブルの更新 特徴量テーブルに新しくカラムを追加する際のコードを対比してみます。 ## Modify the dataframe containing the features so2_cols = [ "free_sulfur_dioxide" , "total_sulfur_dioxide" ] new_features_df = (features_df.withColumn( "average_so2" , expr( "+" .join(so2_cols)) / 2 )) Feature Storeを 使う場合 fe.write_table( name=table_name, df=new_features_df, mode= "merge" ) Feature Storeを 使わない場合 new_features_df.write \ .mode( "overwrite" ) \ .option( "mergeSchema" , "true" ) \ .saveAsTable(table_name) 特徴量テーブルの更新も作成と大きくは変わりません。 Feature Storeを使う場合は、write_tableメソッドを使って特徴量テーブルを更新します。 使わない場合は、mergeSchemaオプションを指定することでカラムが増えても問題なくDeltaテーブルを更新できます。 学習データの作成 Feature Storeを 使う場合 def load_data (table_name, lookup_key): # In the FeatureLookup, if you do not provide the `feature_names` parameter, all features except primary keys are returned model_feature_lookups = [ FeatureLookup( table_name=table_name, lookup_key=lookup_key ) ] # fe.create_training_set looks up features in model_feature_lookups that match the primary key from inference_data_df training_set = fe.create_training_set( df=inference_data_df, feature_lookups=model_feature_lookups, label= "quality" , exclude_columns= "wine_id" ) training_pd = training_set.load_df().toPandas() # Create train and test datasets X = training_pd.drop( "quality" , axis= 1 ) y = training_pd[ "quality" ] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2 , random_state= 42 ) return X_train, X_test, y_train, y_test, training_set # Create the train and test datasets X_train, X_test, y_train, y_test, training_set = load_data(table_name, "wine_id" ) Feature Storeを 使わない場合 def load_data () training_pd = inference_data_df.join( features_df, on=[ "wine_id" ], how= "left" ).toPandas() # Create train and test datasets X = training_pd.drop( "quality" , axis= 1 ) y = training_pd[ "quality" ] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size= 0.2 , random_state= 42 ) return X_train, X_test, y_train, y_test # Create the train and test datasets X_train, X_test, y_train, y_test = load_data() Feature Storeを使う場合は、FeatureLoookup機能を使ってFeature Storeに保存している特徴量テーブルを引っ張ってきます。 サンプルコードではFeatureLookupは1つしか指定していませんが、複数指定することで様々な特徴量テーブルを結合することが可能です。 そして、create_training_setメソッドを使って、起点となる(primary_keyやlabel等を含んだ)データと特徴量テーブルを結合して学習データを作成することができます。 使わない場合は、Deltaテーブルから特徴量テーブルを読み込んで、推論データと結合して学習データを作成します。 特徴量テーブル1つであれば、こちらの方がシンプルで可読性も高いですが、特徴量テーブルが増えるとleft joinを繰り返し記述することになります。 モデルの学習 検証用のモデルを準備します。 # Configure MLflow client to access models in Unity Catalog mlflow.set_registry_uri( "databricks-uc" ) model_name = "uc_sandbox.naoki_furuhama.wine_model" client = MlflowClient() try : client.delete_registered_model(model_name) # Delete the model if already created except : None Feature Storeを 使う場合 # Disable MLflow autologging and instead log the model using Feature Engineering in UC mlflow.sklearn.autolog(log_models= False ) def train_model (X_train, X_test, y_train, y_test, training_set, fe): ## fit and log model with mlflow.start_run() as run: rf = RandomForestRegressor(max_depth= 3 , n_estimators= 20 , random_state= 42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) mlflow.log_metric( "test_mse" , mean_squared_error(y_test, y_pred)) mlflow.log_metric( "test_r2_score" , r2_score(y_test, y_pred)) fe.log_model( model=rf, artifact_path= "wine_quality_prediction" , flavor=mlflow.sklearn, training_set=training_set, registered_model_name=model_name, ) train_model(X_train, X_test, y_train, y_test, training_set, fe) Feature Storeを 使わない場合 mlflow.sklearn.autolog(log_models= True ) def train_model (X_train, X_test, y_train, y_test): ## fit and log model with mlflow.start_run() as run: rf = RandomForestRegressor(max_depth= 3 , n_estimators= 20 , random_state= 42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) mlflow.log_metric( "test_mse" , mean_squared_error(y_test, y_pred)) mlflow.log_metric( "test_r2_score" , r2_score(y_test, y_pred)) mlflow.register_model( model_uri= "runs:/abcdefghijklmnopqrstuvwxyz123456/model" , name=model_name ) train_model(X_train, X_test, y_train, y_test) モデルの学習はFeature Storeを使う場合と使わない場合で、FeatureEnginneringClientを使うかmlflowを使うかの違いがあります。 また、Feature Storeを使う場合は、特徴量テーブルとモデルを紐づけるリネージが作成できることが確認できます。 これにより、一目でモデルがどんな特徴量から学習されたかを見ることができ、過去の実験の振り返りをもとにした改善が容易になります。 バッチ推論 登録済みのモデルのうち、最新のモデルを取得する関数を定義します。 # Helper function def get_latest_model_version (model_name): latest_version = 1 mlflow_client = MlflowClient() for mv in mlflow_client.search_model_versions(f "name=" {model_name} "" ): version_int = int (mv.version) if version_int > latest_version: latest_version = version_int return latest_version Feature Storeを 使う場合 ## For simplicity, this example uses inference_data_df as input data for prediction batch_input_df = inference_data_df.drop( "quality" ) # Drop the label column latest_model_version = get_latest_model_version(model_name) predictions_df = fe.score_batch( model_uri=f "models:/{model_name}/{latest_model_version}" , df=batch_input_df ) display(predictions_df[ "wine_id" , "prediction" ]) Feature Storeを 使わない場合 batch_input_df = inference_data_df.join( features_df, on=[ "wine_id" ], how= "left" ).drop( "quality" ) # Drop the label column latest_model_version = get_latest_model_version(model_name) from pyspark.sql.functions import struct, col # Load model as a Spark UDF. Override result_type if the model does not return double values. loaded_model = mlflow.pyfunc.spark_udf( spark, model_uri=f 'models:/wine_model_uc/{latest_model_version}' , result_type= 'double' ) # Predict on a Spark DataFrame. batch_input_df.withColumn( "prediction" , loaded_model(struct(* map (col, batch_input_df.columns)))) predictions_df = batch_input_df display(predictions_df[ "wine_id" , "prediction" ]) バッチ推論もFeature Storeを使う場合と使わない場合で、FeatureEnginneringClientを使うかmlflowを使うかの違いがあります。Feature Storeを使う場合はfe.score_batchメソッドを使い、使わない場合はmlflow.pyfunc.spark_udfメソッドを使って推論します。 また、大きな違いとして、Feature Storeを使う場合は、推論時に起点となる(primary_keyやlabel等を含んだ)データを渡せば、 特徴量をデータとして渡さずに推論することができます 。 これは、FeatureEnginneringClientがメタデータをもとに学習時に使用した特徴量を自動的に取得してくれるからだと思われます。 メリット・デメリット Feature Storeを使う場合と使わない場合で、以下の実装上の比較をしました。 特徴量テーブルの作成 特徴量テーブルの更新 学習データの作成 モデルの学習 バッチ推論 では、実際にどのようなメリット・デメリットがあるかまとめます。 メリット リネージ機能 Feature Storeを使う場合は、特徴量テーブルとモデルを紐づけることができるため、モデルがどんな特徴量から学習されたかを一目で確認できる。 特徴量の自動取得 Feature Storeを使う場合は、推論時に起点となるデータを渡せばFeature Storeがよしなに特徴量を参照してくれるため、明示的に特徴量をデータとして渡さずとも推論できる。 デメリット 可読性 FeatureLoopup機能を使った特徴量の結合は、暗黙的にleft joinしてるなど、FeatureEnginneringClientについて理解がないと、どんな処理をしているわからず、可読性が低い。 学習コスト 特徴量の自動取得な便利な側面はあるが、上記の可読性のデメリットも含めてFeatureEnginneringClientの書き方に慣れるまでの学習コストがかかる。 振り返り Databricks側が、Feature Store機能の使用を推奨している理由は冒頭で述べた通りです。 それぞれの観点で、弊社のMLのスモールスタートフェーズにおいてFeature Storeを使う理由があるか考えると、以下のようになりました。 発見性 ◯ 特徴量に関しては、Unity Catalog内のDeltaテーブルでもUnity Catalog Explorerでも参照できるため、Feature Storeを使わなくても検索やスキーマ等を確認できる。 対して、Unity Catalog外のDeltaテーブルであれば、上記ができないためFeature Storeを使えると嬉しい。 リネージ ◯ Feature Storeを使うことで特徴量テーブルとモデル間でのリネージ機能があるため、Feature Storeを使えると嬉しい。 モデルのスコアリングやサービングとの統合 △ モデルのスコアリングに関しては、バッチ推論時に特徴量を自動取得してくれる機能があるため、Feature Storeを使えると嬉しい。 MLのスモールスタートフェーズにおいて、サービングに関してはスコープ外であり、Feature Storeが使えなくてもよい。 ポイントインタイムのルックアップ ✗ MLのスモールスタートフェーズにおいて、特定の時点での正確性を必要とする時系列およびイベントベースのユースケースはスコープ外であり、Feature Storeが使えなくてもよい。 所感 個人的なDatabricksのFeature Storeに対する疑問が、今回の検証で解消されました。 特に 発見性 と リネージ に関して、これからML開発を進めていき、たくさんの実験をしていく中で大いに助けになってくれる機能だと感じました。 FeatureEnginneringClientの学習コストや可読性に関して懸念を述べましたが、今回の検証にあたりドキュメントを読み込んでいく中で整理できたかなと思います。 これから新しくMLのモデル開発するメンバーを見据えた整備も順次進めていければなと思います。 Feature Storeを導入することで、MLのモデル開発のagility向上に繋がることは間違いないと感じました。 今実装しているいくつかの特徴量から順にFeature Storeに移行していくことで、Feature Storeの恩恵を受けられるML環境の構築を目指していきたいと思います。
アバター
この記事は、 every Tech Blog Advent Calendar 2024(夏) の16日目の記事です。 はじめまして、データストラテジストのoyabuです。 N1分析、色んなメリットがあるので頼る場面が多いのですがN1分析時に注意していることを書いてみます。 N1分析のpros/cons ここでのN1分析は、1人のユーザーをアクションログ単位で分析することを指します。インタビューなどは含みません。 そういったN1分析のpros/consは以下であると考えています。 pros ユーザーの解像度が上がる 楽しい cons 適当にみちゃうと時間の無駄 楽しいので時間が溶ける なので、基本的にはある程度N1分析する目的とユーザーのあたりを付けることが大事だと思っています。 方針を決める N1分析のおすすめはまずユーザーをよしなにマッピングして対象のユーザーをどこに移動させたいか。を考えたうえで、そのギャップをみて施策を考案する方針です。 たった1人のユーザーであるN1の動きを意思決定の根拠とするのはちょっとつらいです。が、施策を決めるうえでのヒントを集めていくのに使うのはそれなりに筋がよいとも思ってます。 なので、だいたいの方針は以下な感じになると思います。 適当にユーザーを色んな切り口でマッピングする 良さげなユーザー群があれば、その人達を特定の指標でスコアリングしてグラデーションをみる 一貫性のあるスコアがみえれば、まずもっともスコアの高いユーザーを見てみる だいたいやたらすごい動きをしているので、次点にいるユーザーを抽出してその人のN1も見てみる 比較して、次点にいるユーザーにどういった行動をして欲しいかを考える 適当なデータで見ていきます。 ユーザーをマッピングしてみる 弊社が持っているレシピデータやレシートデータなど、実データを使ってしまうとアレなので 簡単のためにそれっぽいデータをスプレッドシート上に用意して、ざっくりした分析の流れをご説明していきます。 用意したN1用のログデータはこんな感じです。 仮にレシピの閲覧数とレシート登録数をユーザーごとに集計してマッピングする方針を考えてみます。 まずはユーザー単位にそれぞれを集計して、 クロス集計してみます。 レシピの閲覧数とレシート登録数はそれなりに相関してそうな雰囲気がします。 たとえばもっとユーザーさんにレシート登録して欲しい。。!という目的があった場合、ここでいうAさんとBさんのN1を見比べてみると良さそうです。 N1分析してみる Aさんをみてみます。こんな感じです。 なんとなくレシピみた後にレシート登録する動きが多そうです。 次にBさんをみてみます。こんな感じです。 Aさんのような動きはなさそうです。 アプリ上でレシピを見るタッチポイントはそれなりに多い人なので、仮に行動フローがレシピサイトを見る->買い物にいく。とAさんの行動フローと似ていた場合、ひょっとしてアプリ上でレシピみたあとにレシートの登録のお願いなんかしてみるともっと登録してくれるのでは??と施策のヒントが出てきます。 まとめ ほんとはビニングとか必要になってくると思うのでもっと複雑な工程にはなるのですが、頑張って簡単にしました。 よいN1ライフをー
アバター
新規プロダクトのリポジトリ構成にモノレポを採用してみた お久しぶりです ,DELISH KITCHEN開発部でSoftware Engineer(SE)をしている 鈴木 です. every Tech Blog Advent Calendar 2024(夏) の15日目を担当する事になりましたので,鈴木が開発に携わっている新規プロダクトで採用しているリポジトリ構成についてお話させていただきます. はじめに 私事ですが,夏の兆しを感じ始めたタイミングでトモニテ開発部からDELISH KITCHEN開発部に異動しました. 異動後も新規プロダクトの開発に携わっており,有り難いことに大部分を任せていただいているので, トモニテ相談室 の立ち上げ時に感じていた課題をユーザーへの影響を気にせずに払拭できる絶好の機会を楽しんでいます. 課題はいくつもあり,どれも払拭に向けて奔走中なのですが,大きなところとしてポリレポ構成をやめ,モノレポ構成を採用している点があります. 今回はポリレポ構成時に感じていた課題感を共有し,モノレポ構成だとどのように解決されるのかをお伝えしていこうと思います. ポリレポやモノレポとは? ポリレポとは,Git等でプロダクトに必要になるソースコードを管理する際,webやserverなどの各モジュールを別リポジトリとして管理する方式のことです. 一方でモノレポはポリレポと対を成す言葉であり,プロダクトに必要になる各モジュールを単一のリポジトリで管理する方式のことです. 従って,ポリレポではプロダクトに必要なモジュールの数だけリポジトリが作成されるのに対し,モノレポでは常に1つのリポジトリが作成されることになります. ポリレポとモノレポのイメージ ポリレポ運用時に感じていた課題 トモニテ相談室はポリレポ方式でモジュールを管理しています. 立ち上げ時の私の経験値や社内での知見の豊富さから当時は最適解であったのですが,開発を続けるにつれ以下のような点を課題に感じるようになりました. OpenAPI ファイルがserverモジュールに配置されており,server以外のモジュールがスキーマ駆動開発を実践できない(しづらい) 共通の定数を各モジュールがそれぞれ定義する必要があり不毛 順に詳細をお伝えしていこうと思います. server以外のモジュールがスキーマ駆動開発を実践できない トモニテ相談室ではOpenAPIファイルをserverモジュールに配置しています. OpenAPIはAPIの仕様を表現するためのものであるため,ポリレポ運用をしているプロダクトでserverモジュールに配置したくなるのは自然な流れかと思います. しかし,このような配置にすることでOpenAPIファイルを参照可能なモジュールがserverに限定されてしまうため,他のモジュールが単純にはスキーマ駆動開発を実践できなくなってしまいます. 共通の定数を各モジュールがそれぞれ定義する必要があり不毛 ポリレポ構成のプロダクトではユーザーのステータス等の共通で利用する定数は各モジュール毎に定義する必要があります. 各モジュールで言語を変えて同じ定数を定義していくのはいささか不毛であり,実装者もコードレビュワーも値に誤りがないかに神経をとがらせる必要があります. また,その値を変更しなければならなくなった際,定義されている全てのモジュールを調べ上げ,再び値に誤りが無いように変更しコードレビューをしていく必要があります. モノレポ構成による課題解決 現在私が開発している新規プロダクトは以下のようなモノレポ構成をとっています. . ├── web ├── server ├── constant └── openapi 定数やOpenAPIなどの共通で使われる部分をそれぞれconstantおよびopenapiという独立したモジュールに集約させており,以下のイメージのようにwebやserverなどが必要に応じてこれらに依存するようにしています. このような構成を取ることにより,ポリレポ時に感じていた課題を解決出来ています. 依存関係のイメージ どのように解決しているのかを一緒に見ていこうと思いますが,定数管理に対して感じていた課題もOpenAPIに対して感じていた課題も,本質的には各モジュールでシェアして利用すべきものをそのように出来ていなかったという点で共通しており,解決方法は酷似しています. 従って,本記事ではファイルの管理方法に特徴があるconstantモジュールをピックアップし,どのように解決したのかを見ていこうと思います. constantモジュールによる課題解決 constantモジュールは言語に依存しない形で定数を集約し,webやserver等のモジュールがタイプセーフに定数にアクセスすることを可能にする必要があります. 以下でどのように言語に依存しない形で定数を集約し,どのようにwebやserverからタイプセーフに定数にアクセス可能にしているかを紹介していきます. 言語に依存しない定数の集約方法 constantモジュールでは以下のように言語に依存しないで定数を集約するようにしています. constant/ └── model/ ├── status .json └── status .schema.json model/ 配下にはモデルに関連する定数を配置しており, (filename).json には実際の定数を, (filename).schema.json には定数のJSON Schemaを記述しています. status.json と status.schema.json の例はそれぞれ以下のようになります. { " deactive ": 0 , " active ": 1 } { " $schema ": " http://json-schema.org/draft-07/schema# ", " title ": " ステータス ", " description ": " ステータスを表現する定数を管理するオブジェクト ", " type ": " object ", " additionalProperties ": false , " required ": [ " deactive ", " active " ] , " properties ": { " deactive ": { " type ": " integer ", " example ": 0 } , " active ": { " type ": " integer ", " example ": 1 } } } webやserverから定数にアクセスする方法 quicktype というCLIツールを介してアクセス可能にしています. quicktypeはJSONやJSON Schema等から様々な言語のコードを自動生成するツールです. 以下のようにquicktypeを実行する(※)と, $ quicktype status .schema.json -s schema -o status .ts --prefer-types --readonly --no-runtime-typecheck 以下のようなコードが自動生成されます. なお短縮のために一部コメントを削除しているのでご留意下さい. export type Status = { readonly active: number; readonly deactive: number; } // Converts JSON strings to/from your types export class Convert { public static toStatus(json: string): Status { return JSON.parse(json); } public static statusToJson(value: Status): string { return JSON.stringify(value); } } このように quicktype を用いて (filename).schema.json をベースにコードを自動生成し,生成されたコードを利用し (filename).json を参照する事でwebやserverからアクセス可能にしています. openapiモジュールにおいても同様であり,OpenAPIファイルからコードを自動生成し,serverやweb等のモジュールでそれらを参照しています. ※ 短縮のために --no-runtime-typecheck フラグを利用していますが,このフラグを外すと (filename).json に記述されている値に誤りが有る際にバリデーションエラーが発生するようになり,ローカル開発時にミスを発見しやすくなるため,外すことをおすすめします. モノレポを採用してみて感じたメリット・デメリット 本記事執筆時点ではモノレポ歴は1ヶ月程度なのでまだまだ掴みきれていないと思いますが,上述の課題を解決できた以外にも以下のようなモノレポのメリットを感じています. issueが1つのリポジトリに集約されて分かりやすい プロダクトに必要なコードが1つのリポジトリに集約されていて楽.いつも同じリポジトリにアクセスすれば良い ローカル開発時に各モジュールの起動が楽.Docker Composeを用いて必要なモジュールをすぐに起動できる 一方で以下のようなデメリットも有ると思います. CI/CDが複雑になる 共通化に対して常に意識しなければならない 共通利用するものをどのように集約しどのようにアクセス可能にするかに一定のコストがかかる まとめ 本記事では私が新規プロダクトのリポジトリ構成にモノレポを採用した背景とその詳細,及びモノレポに対する所感を紹介させていただきました. ポリレポはその特徴故にプロダクトに関わる各モジュールで共通して利用すべきものをシェアすることが難しくなっています. その一方でモノレポは工夫こそ必要なものの,共通して利用すべきものを集約し,各モジュールでシェアすることが可能になっており,モノレポを採用することにより私は課題を解決することが出来ました. この記事が私と同じようにポリレポならではの課題を感じており,モノレポに対して興味を持っていらっしゃる開発者の方々のお役に立てたら大変嬉しいです. ここまでお読みいただきありがとうございました!
アバター
はじめに この記事は、 every Tech Blog Advent Calendar 2024(夏) の14日目の記事です。 株式会社エブリーでソフトウェアエンジニアをしている桝村です。 子育てメディア「MAMADAYS」は、2023年に「トモニテ」に名称変更しつつ、ロゴやアプリアイコンのデザインを刷新しました。 tomonite.com トモニテのサービス名称変更については、以下の記事でも詳しく紹介しています。 tech.every.tv また、サービス名称変更における対応の一つとして、Web メディアのドメインを mamadays.tv から tomonite.com へ変更しました。 本記事では、Web メディアのドメイン変更に伴う作業内容やそれによって得られた知見について紹介します。 この記事のゴールは、ドメイン変更を検討、または実施される方にとって、ドメイン変更の際に必要な作業や Google 検索結果への影響をできる限り抑えるためのポイント、およびドメイン変更で得た知見を共有することです。 前提 Web メディアのドメイン変更に際して、主な要件は以下の通りでした。 ・ 新ドメインでサービスを公開できていること ・ 旧ドメインへのアクセスは、新ドメインへ 301 リダイレクトされること ・ ドメインで紐付ける各種サービスを新ドメインへ再登録すること (ex. Search Console, Ad Manager) 要件を踏まえつつ、今後の Web メディアのサービス拡大を見据えて、今回は以下を意識・考慮しながらドメイン変更を進めることにしました。 ・ 大きな不具合や遅延なくドメイン変更をやり遂げること ・ Google 検索結果への影響をできる限り抑えること ちなみに、Google 検索結果への影響をできる限り抑えることにあたって、サイト移転に関する Google の公式ドキュメントがあったので、これに従って必要な作業を進めていきました。 developers.google.com また、AWS リソースの管理には Terraform を使用しているため、今回のドメイン変更においても Terraform でリソースを追加・変更する作業を進めていきます。 ドメイン変更の準備 今回のドメイン変更の関係箇所をイメージしやすいように構成図にまとめました。 アーキテクチャ図 新ドメインの公開の準備 Amazon Route 53 により、新ドメインの公開の準備を行います。 新ドメインの DNS を Route 53 に変更 Route 53 にて新ドメインのホストゾーンを作成し、お名前.com などのドメイン登録サービスにて Route 53 のネームサーバーを利用するように設定を変更 新ドメインの公開の準備 Route 53 にて 新ドメイン用の A レコードを追加する Pull Request を作成・レビュー A レコードの追加自体は、ドメイン変更の実施当日に行いますが、作業の正確性・一貫性の担保のため、事前に Terraform で PR 作成・レビューまで済ませておきます。 新ドメインへの通信を HTTPS で保護 ACM (AWS Certificate Manager) により、新ドメインへの通信を HTTPS で保護します。 証明書のリクエスト ACM から SSL/TLS 証明書をリクエスト 証明書の検証 リクエストしたドメインに対して所有権を証明するため、DNS レコードを追加 証明書のデプロイ ACMから取得した証明書を ELB (Elastic Load Balancer) にデプロイ また、Datadog などの監視ツールを使って証明書の有効期限が切れる前に通知を受け取る設定をしておきます。 旧ドメインから新ドメインへのリダイレクトの準備 AWS ALB (Application Load Balancer)により、旧ドメインから新ドメインへのリダイレクトの準備を行います。 AWS ALB のリスナールールにて、旧ドメイン mamadays.tv から新ドメイン tomonite.com へのステータスコード 301 でリダイレクトするルールを追加する Pull Request を作成・レビューします。 新ドメインへリダイレクトさせるルール 今回は、Aレコードと同様に 事前に Terraform で PR 作成・レビューまで済ませておきます。 resource "aws_alb_listener_rule" "mama_web_to_tomonite_web_rule" { listener_arn = aws_alb_listener.mamadays_ecs_lb_https.arn priority = 4 action { type = "redirect" redirect { host = "tomonite.com" port = "443" protocol = "HTTPS" status_code = "HTTP_301" } } condition { host_header { values = [ "mamadays.tv" ] } } } Search Console にて、新ドメインのプロパティを作成 Web メディアのドメイン変更を実施した際は、SEO への影響が大きな懸念事項の一つです。 今回は Google Search Console を使って、新ドメインと旧ドメインでクローラーによるページのインデックスやユーザーのトラフィックがどのように変化するかを監視します。 それを実現すべく、予め新ドメインのプロパティを作成しておきます。 support.google.com ただし、手順の一つであるサイトの所有権を確認は、DNS レコードの追加による方法で行う方針のため、新ドメインの公開後に対応します。 support.google.com 当日のドメイン変更の手順書を作成 作業の正確性・一貫性の担保や社内への共有のため、当日のドメイン変更の手順書を作成します。 # 1. 新ドメインの公開・リダイレクト # 2. Search Console にて、元のサイトのアドレス変更の通知を送信 # 3. 新ドメインをベースとしたサイトマップを構築・配置・送信 ※ 他にも細かい手順はありますが、ここではわかりやすさを重視して簡潔にまとめています。 ドメイン変更の実施 1. 新ドメインの公開・リダイレクト 「ドメイン変更の準備」にて事前に準備しておいた Aレコードの追加や ALB のリスナールールの追加を実施します。 今回は Terraform の PR をもとに terraform apply を実行するのみでした。 これでついに新ドメインが公開されました! 旧ドメインへのアクセスも新ドメインへリダイレクトされるようになります。 2. Search Console にて、元のサイトのアドレス変更の通知を送信 Google 検索の検索結果について旧ドメインから新ドメインへの移行を促すため、Search Console にて、元のサイトのアドレス変更の通知を送信しました。 support.google.com 新ドメイン側の Search Console にて以下の画面が表示されていると、アドレス変更が正常に処理されたことを示していると思われます。 アドレス変更の通知が送信完了 3. 新ドメインをベースとしたサイトマップを構築・配置・送信 Google 検索エンジンに旧ドメインでなく新ドメインのページを認識してもらう必要があります。 そのために、新ドメインをベースとしたサイトマップを構築・配置した上で、Search Console にてサイトマップを送信しました。 support.google.com 全体を通して振り返り トラフィック数は2ヶ月弱で変更前の水準に回復 ドメイン変更での大きな関心ごとの一つは、SEO への影響です。 新ドメインのページのインデックス登録は、ドメイン移行後の 1 ~ 2週間で完了しました。 また、Google 経由のトラフィック数は、ドメイン移行後の最初の2週間にはドメイン移行前の3分の2 ほどに減少しましたが、その後は徐々に回復し、ドメイン移行後の2ヶ月弱後にはドメイン移行前のトラフィックとほぼ同等の水準に戻りました! 目立った不具合なくドメイン変更を実施できたことや、Google 検索のドキュメントに従って丁寧に作業を進められたことも、この結果に寄与したのではないかと考えています。 ドメイン変更後のGoogle検索のトラフィックの推移 ※ ドメイン変更前の過去2ヶ月の平均 本番での作業を最小限にできた 当日の本番環境での作業が多かったり複雑であるほど、ヒューマンエラー等により作業の不具合や遅延が発生する可能性は高くなると思います。 今回はドメインの登録・リダイレクトを一つの CLI コマンドのみで実行できるようにする等、できるだけ本番環境での作業を最小限にでき、その結果目立った不具合などなく作業を完了できました。 事前に動作確認できる仕組みがあればよかった 今回開発工数の都合上開発環境での動作確認をメインに行いました。 しかし、理想としては本番環境との潜在的な差異を考慮してユーザーへの展開をする前に本番環境での動作確認も行うべきだったと考えています。 具体的には、特定の IP やユーザーエージェントからのアクセスのみ新ドメインへアクセスできるようにして動作確認を行うなどの方法が考えられます。 監視ツールの設定変更が漏れていた ドメイン変更を実施したタイミングにて、監視ツール Datadog により WEB の死活監視に失敗したアラートが飛んでしまいました。 ページ自体は正常に見れており、原因としては 旧ドメインはリダイレクトさせたため、Datadog の死活監視の設定もステータスコード 200 に加えて 301 も許容するように変更する必要がありました。 本番環境での作業中にアラートが飛ぶことは、作業の遅延やミスに繋がる可能性があるので、監視ツールの設定も網羅的に見直すべきでした。 おわりに 今回は Web メディアのドメイン変更に伴う作業内容やそれによって得られた知見について紹介しました。 これから ドメイン変更を検討されている方、またはドメイン変更を実施される方にとって、参考になれば幸いです。
アバター
はじめに この記事は every Tech Blog Advent Calendar 2024(夏) 13日目の記事です。 こんにちは。 株式会社エブリーの開発本部データ&AIチーム(DAI)でデータエンジニアをしている吉田です。 今回は将来的なMLモデルのサービス組み込みに向けた調査の一環として、Databricks Model ServingとAWS API Gatewayを利用してML APIを作成するPoC行ったので、その取り組みについて紹介します。 Databricks Model Serving Databricks Model Servingは、Databricksが提供するAIモデルをデプロイ・管理するためのサービスです。 Databricks上で構築・学習したモデルや、Databricks Marketplace上で公開されているモデルをデプロイし、REST APIを通じて推論を行うことができます。 https://docs.databricks.com/ja/machine-learning/model-serving/index.html データ基盤としてDatabricksを利用している現環境では、Databricks Model Servingを利用することで、モデル構築からデプロイまでの一貫した開発フローを提供できると考えました。 DatabricksでのML開発フロー AWS API Gateway AWS API Gatewayは、APIを作成・公開・管理するためのサービスです。 https://aws.amazon.com/jp/api-gateway/ 構成 今回のPoCでは、以下を参考に、Databricks Model ServingとAWS API Gatewayを組み合わせてML APIを作成しました。 https://aws.amazon.com/jp/blogs/news/creating-a-machine-learning-powered-rest-api-with-amazon-api-gateway-mapping-templates-and-amazon-sagemaker/ 以下のような構成になります。 - Databricks Model Servingでモデルをデプロイ - Model Servingで提供されるServing EndpointのRest APIをAWS API Gatewayでラップ - API Gatewayで提供されるエンドポイントにリクエストを送信し、推論結果を取得 Databricks Model Servingが提供するServing EndpointをAWS API Gatewayでラップすることで、AWS API Gatewayが持つAPIの管理や認証、モニタリングなどの便利な機能が利用できます。 構成 実装 モデルの構築 今回はサンプルとして、入力を二倍にして返す単純なモデルを構築します。 モデルの構築 import mlflow class MyModel (mlflow.pyfunc.PythonModel): def predict (self, context, model_input, params= None ): # in [1,2] -> out [2,4] return model_input * 2 モデルの登録 import mlflow from mlflow.models import infer_signature import pandas as pd model = MyModel() input_data = pd.DataFrame([{ "q" : 2 }, { "q" : 10 }]) output = model.predict( None , input_data) # 4, 20 signature = infer_signature(input_data, output) with mlflow.start_run(): model_info = mlflow.pyfunc.log_model( "test_model" , python_model=MyModel(), input_example=input_data, signature=signature, registered_model_name= "test_model" , ) model_info.model_uri databricks上で mlflow.pyfunc.log_model を実行することで、Databricks Model Registryにモデルが登録されます。 databricks models モデルのデプロイ Databricks Model ServingのUIからモデルをデプロイします。 エンドポイント名、利用するモデル、コンピュートリソースなどを設定しデプロイします。 デプロイが完了すると、Serving Endpointが提供されます。 databricks model serving Serving Endpointのテスト Serving Endpointに対してリクエストを送信し、推論結果を取得します。 REST APIを利用するため、 curl コマンドなどでリクエストを送信することができます。 また、Databricks Model ServingのUIや、Databricks SQLからもリクエストを送信することができます。 query curl \ -X POST \ -H "Content-Type: application/json" \ -d '{"dataframe_split": {"columns": ["q"], "data":[100,20]}}' \ https://{databricks-host}/serving-endpoints/test-model/invocations | jq . result { " predictions ": [ { " q ": 200 } , { " q ": 40 } ] } API Gatewayの設定 Databricks Model ServingのServing EndpointをAWS API Gatewayでラップします。 AWS API Gatewayのマッピングテンプレート機能を使い、リクエスト/レスポンスの変換を行います。 API Gatewayのマッピングテンプレート機能 統合リクエストのマッピングテンプレート # set ( $ queries = $ input . params (" query ") ) { " dataframe_split " : { " columns " : [ " q " ] , " data " : [ # foreach ( $ query in $ queries . split (","))$ query # if ($ foreach . hasNext ),# end # end ] } } 統合レスポンスのマッピングテンプレート # set ($ predictions = $ input . path ("$. predictions ")) { " results " : [ # foreach ( $ item in $ predictions )$ item . q # if ($ foreach . hasNext ),# end # end ] } APIの呼び出し API Gatewayのエンドポイントに対してリクエストを送信し、推論結果を取得します。 query curl "<invoke-url>/test?query=1,2" result { " results ": [ 2 , 4 ] } まとめ 今回はDatabricks Model ServingとAWS API Gatewayを組み合わせてML APIを作成するPoCを行いました。 Databricks Model Servingを利用することで、モデルの構築からデプロイまでの一貫した開発フローを提供できます。 また、AWS API Gatewayを利用することで、APIの管理や認証、モニタリングなどの便利な機能が利用できます。 Databricks Vector Searchなどと組み合わせることで、RAGアプリケーションの提供や、検索エンジンの構築など、様々なサービスに活用できると考えられます。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 12 日目の記事です。 今週はWWDCでiOS 18やXcode 16の情報が公開されていますが、この記事では昨年9月にリリースされたXcode 15で実装されたアセットカタログの画像/色のシンボル自動生成機能についての説明と、トモニテアプリへの適用(途上です)について書きます。 Xcode 15の画像/色のシンボル自動生成機能 Xcode 15から、アセットカタログ内の画像/色に対してSwiftのシンボルを生成できるようになりました。今までの名前文字列で指定する方法と比較すると、コード補完が効き、コンパイラの型チェックがされるため安全です。 例えば post_icon という画像がアセットカタログに含まれる時、これまでは以下のようにアセットの名前を文字列で記述していました。 // Swift UI Image( "post_icon" ) // UIKit UIImage(named : "post_icon" ) Xcode 15では、build settingsの Generate Asset Symbols (ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) がYESの時、アセットカタログに含まれる画像/色に対応するシンボルが自動的に生成されます。(デフォルトでYESですが、NOに設定することで無効化できます) 生成されたシンボルを使って以下のように書くことができます。 // SwiftUI Image(.postIcon) // UIKit UIImage(resouce : .postIcon) さらに、 Generate Swift Asset Symbol Extensions」(ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS) をYESにするとUIImageのExtensionが生成されて以下のように書けるようになります。(デフォルトは無効です) // UIKit UIImage.postIcon (ここまで画像の場合しか説明していませんが、色についてもほぼ同様です) SwiftGenなどを導入しているプロジェクトも多いと思いますが、Xcode 15からは画像/色に関してはXcode単体で同様のことが可能です。 トモニテアプリへの適用 トモニテアプリはSPMによるマルチモジュール構成になっており、機能別に分割され独立したパッケージになっています。 一方アセットカタログはメインバンドルに1つだけ存在し、アプリで使う全てのアセットを含んでおり、各パッケージから参照されています。 この環境で、生成されたシンボルを利用しようとした時に問題がありました。 生成されたシンボルはアセットカタログが含まれるモジュール内でしか利用できないため、各機能モジュールから画像を参照できません。 // モジュールから Image(.postIcon) // Type 'ImageResource' has no member 'postIcon' この問題には以下の方針で対応することにしました。 特定のモジュールだけで利用するアセットは、モジュール毎に作成したアセットカタログに移動する。 複数のモジュールで利用するアセットは、 Common モジュールに作成したアセットカタログに移動する。Common にはアセットにアクセスするためのpublicなextensionを用意する 元々の構成ではパッケージ外のアセットに暗黙的に依存しているという問題意識もあったので、この際依存関係の整理を兼ねて修正したいと考えています。 各モジュールのアセットカタログ 各パッケージのSource以下にアセットカタログファイルを作成し、画像を格納します。 SPMパッケージのSources以下のxcassetsファイルは自動的にシンボル生成の対象となるようです。そのため同パッケージ内では以下のように画像にアクセスできるようになります。 // 名前で指定 Image( "post_icon" , bundle : .module) //シンボルで指定 Image(.postIcon) // bundle指定は不要 ただしSPMパッケージではExtensionsは生成されないようなので、 UIImage.postIcon といった記述はできません。 Commonのアセットカタログ Commonパッケージ(UIの共通部分、デザインシステムを扱う)にアセットカタログを作成し、共通アセットを格納します。 また、以下のようなExtensionを定義して外部から画像にアクセスできるようにします。 extension Image { public static var close : Image { Image(.close) } } extension UIImage { public static var close : UIImage { UIImage(resource : .close) } } 共通画像は数が少なく、変更頻度も低いのでこのような運用でも許容できると考えていますが、画像が多くなったら課題になりそうです。 最後に Xcode 15の画像/色シンボル自動生成機能をSPMによるマルチモジュール環境へ適用する方法について書きました。どなたかの参考になれば幸いです。
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 11 日目の記事です。 エブリーで小売業界向き合いの開発を行っている @kosukeohmura といいます。 エブリーでは retail HUB という小売業界向けのサービスを展開しており、その開発を行う中でイベントログを収集する API を作る機会がありました。この記事ではその中でも表題の点にフォーカスして詳細をお伝えできればと思います。 イベントログを収集する API の概観 クライアントからのイベントログを API Gateway で作成した API で受け、Amazon Data Firehose ストリーム経由で S3 に保存します。 イベントログデータの流れ API では一度のリクエストで複数のイベントを受け取り、その後 Amazon Data Firehose の PutRecordBatch API を使用し、受け取ったイベントをまとめて Firehose ストリーム へと送信します。Amazon Data Firehose では受け取ったデータをバッファしながら、動的にパーティショニングを行いつつ、S3 に保存します。 ここで API Gateway から Amazon Data Firehose ストリームへデータを送信する際に、データ構造を少し変換する必要があります。次節で詳しく説明します。 API Gateway から Amazon Data Firehose ストリームへデータを送信する Amazon Data Firehose 配信ストリームへ送信する際 PutRecordBatch API を使用しますが、その API へのリクエストでは次のシンタックスを要求されます。ここで blob はレコード 1 つを Base64 エンコードしたものです。 { " DeliveryStreamName ": " string ", " Records ": [ { " Data ": blob } ] } 具体的に、API で受け取る JSON が下記のような値であったとすると、 [ { " field1 ": " value11 ", " field2 ": " value12 " } , { " field1 ": " value21 ", " field2 ": " value22 " } ] このような PutRecordBatch API へのリクエストに適合する JSON に変換する必要があります。 { " DeliveryStreamName ": " string ", " Records ": [ { " Data ": " eyJmaWVsZDEiOiAidmFsdWUxMSIsICJmaWVsZDIiOiAidmFsdWUxMiJ9Cg== " } , { " Data ": " eyJmaWVsZDEiOiAidmFsdWUyMSIsICJmaWVsZDIiOiAidmFsdWUyMiJ9Cg== " } ] } Lambda を挟んで変換処理しようかと考えましたが、その場合、管理・運用する対象が Lambda 関数のソースコード 使用メモリ量等の設定 実行ログを流す CloudWatch Logs ストリーム 関数実行時の IAM ロール その他、実行失敗時の通知機構など と増えます。加えて、Lambda ランタイムのサポート切れや実行時の料金も考慮を要します。 なので Lambda の利用をできれば避けたいと思っていたところ、API Gateway の マッピングテンプレート を利用することでリクエストボディの変換ができることを知り、今回はその方法を取りました。 マッピングテンプレートを使用してリクエストを書き換える マッピングテンプレートは Velocity Template Language (VTL) で記述します。実際に API Gateway の統合リクエストで使用しているテンプレートほぼそのままを下記に記します。 ## 1. メタデータ付加を行うパート #set($records = []) #foreach($inputRecord in $input.path('$')) #set($record = '') #foreach($key in $inputRecord.keySet()) ## 1-a. 値の型に応じて、クォートでくくったりします #set($value = $inputRecord.get($key)) #if($value == $null) #set($value = 'null') #elseif($value.getClass().getName().equals('java.lang.String')) #set($value = '"' + $value + '"') #end #set($record = $record + '"' + $key + '"' + ':' + $value + ',') #end ## 1-b. 必要なメタデータをログに付加します #set($record = $record + '"server_time":' + $context.requestTimeEpoch / 1000 + ",") #set($record = $record + '"source_ip":' + '"' + $context.identity.sourceIp + '"' + ",") #set($record = $record + '"user_agent":' + '"' + $context.identity.userAgent + '"' + ",") #set($record = $record + '"request_id":' + '"' + $context.requestId + '"') #set($record = '{' + $record + '}') ## 1-c. エラー回避のため空代入しています (もっとスマートな方法をご存じの方、教えて下さい!) #set($dummy = $records.add($record)) #end ## 2. PutRecordBatch API へのシンタックスへと変換し出力するパート { "DeliveryStreamName": <your_firehose_stream_name>, "Records": [ #foreach($record in $records) {"Data": "$util.base64Encode($record)"}#if($foreach.hasNext),#end #end ] } なおこのマッピングテンプレートでは、API のリクエストボディの JSON の形式として次を想定しています: 最上位は配列 その配下にフラットなオブジェクトが並ぶ また、テンプレート内の $ から始まる変数リストはこのリファレンスに載っています。 docs.aws.amazon.com 以下、テンプレートの内容を 2 つに分けて簡単に解説します。 1. メタデータ付加を行うパート サーバー側で取得するのが望ましいリクエスト日時や IP アドレス、User-Agent など、イベントログと合わせて保存したいメタデータを付加しています。そのため一旦 JSON をパースしますが、そこから元の JSON 文字列に戻すために value をクォートでくくりなおしたり、value が null の場合に欠落してしまう ( "key":, となる) のを避けるなど、ひと手間かかっています。 2. PutRecordBatch API へのシンタックスへと変換し出力するパート PutRecordBatch API の仕様通り、各イベントログを Base64 エンコードしつつリクエストボディを組み立てます。 Lambda を挟んでのデータ変換との良し悪し 今回 API Gateway 内で簡単なデータ変換を行いつつ、Amazon Data Firehose ストリームへとデータを送信することができました。 マッピングテンプレートでは VTL を使う必要があり、これに不慣れな方も多いと思います。ただ VTL はテンプレート言語であり、動的な処理を行うには限界があるため、VTL 自体の習得はさほど難しくはないと感じています。 今回マッピングテンプレートを完成させる中で問題だと感じたのは、API Gateway のテスト実行時に VTL の実行結果のエラーの詳細を見る方法がない(と思われる)点です。エラー原因の見当がつかない場合、マネジメントコンソール上の VTL を少しずつ修正しながら、トライアンドエラーを繰り返すような泥臭い作業が必要でした。 このことから、実装したい変換がある程度複雑であれば、管理・運用する対象を増やすことを受け入れ、Lambda 関数を呼び出し加工したデータを Firehose 配信ストリームに送信する形を検討したほうが良いかもしれません。一方 VTL で書いても十分にシンプルであれば、マッピングテンプレートを使用する方針を取りたいと思います。 さいごに ここまで読んでいただきありがとうございました。 every Tech Blog Advent Calendar 2024(夏) はまだまだ続きます!
アバター
この記事は every Tech Blog Advent Calendar 2024(夏) 10 日目の記事です。 はじめに こんにちは。DELISH KITCHEN 開発部の村上です。 エブリーでは4月に第4回挑戦weekを実施しました。挑戦week5日間の中で私たちのチームはナレッジ活用のために社内ChatAppに社内ドキュメントを参照できる仕組みづくりに取り組みを行いました。今回はその中でRAG基盤のPoCを行ったので、その取り組みについて紹介します。 挑戦weekについてはこれらの記事で初回の取り組みの様子やCTOの挑戦weekに対する考えが知れるのでぜひ読んでみてください。 https://everything.every.tv/20230428 tech.every.tv PoCの背景 まずは、なぜ社内ナレッジ活用のためのRAG基盤のPoCを行うに至ったか、その背景について説明します。 エブリーでは社内のナレッジを蓄積する場所として、ConfluenceやGoogle Docs、Google Slideを活用しています。こうしたナレッジが溜まっていくこと自体は良いことですが、以下のような問題が社内でも起こることがありました。 ドキュメント自体の量が多くなってきており、ある程度整理していても欲しい情報に簡単に辿りつけない 探す手間を省くために担当者への問い合わせが増え、コミュニケーションコストがかかってしまう 元々社内ではStreamlitを用いて作られたOpenAI APIベースの社内ChatAppがあり、業務効率化に活用されています。しかし、現状のChatAppは業務利用で安全に生成AIを活用するための基盤であり、社内ドキュメントに溜まったナレッジを外部データソースとして読み込んで、それらのデータに基づいて質問に回答することはできませんでした。 そこで今回はこうした課題を解決すべく、社内で溜まったナレッジに基づいて回答できる機能をChatAppに組み込むためのRAG基盤のPoCを実施することになりました。 RAGとは RAGとはRetrieval Augmented Generativeのことで、Retrievalの『検索』工程とGenerativeの『生成』工程を組み合わせることによってLLMが内部では持たない外部の知識に基づいた回答の生成を行うことができます。Retrivalでは与えられた質問に対して外部データソースから関連する情報を取り出し、GenerativeではRetrivalで抽出したデータをpromptにコンテキストとして渡すことでその内容に沿った回答を生成します。 RAGの詳しい動作の解説に関しては以前エブリーのテックブログで紹介されたこちらの記事をみていただけると理解が進むと思います。 tech.every.tv RAG基盤の検討 RAG基盤を構築していく中で4つの選択肢を検討し、比較を行いました。 Amazon Q Business Amazon Q BusinessとはAWSが企業用に提供するフルマネージドな生成AIサービスで、RAG基盤の提供からセキュアなChatインターフェースの提供までを全てサポートしてくれます。 docs.aws.amazon.com 行ったこと data source connector機能でのConfluence Connectorを使った外部データソースの準備 Amazon Q Businessのチャット画面での社内ドキュメントに関するQ&Aを実施 メリット RAG基盤の構築や独自のChatインターフェースの提供を行わなくても良いため、工数が削減できる 外部サービスとのデータコネクタが豊富で接続のための実装をせずに導入することが容易 デメリット 現状では日本語対応がされておらず、使用には翻訳工程を挟む必要がある フルマネージドなため、他の選択肢と比較してもカスタマイズの余地が少ない 結果 Amazon Q Businessでは特に 日本語のサポートがされておらず 、日本語でのデータ保存とそれに対する質問に正常に動作することが困難な点が採用するサービスとしては厳しかったです。 実際にこの状態で利用するためには以下の各パートで翻訳工程を独自に挟む必要があります。 data enrichment機能によるデータコネクタから取得したデータの前処理 質問、回答の入出力 こうなるとAmazon Q Businessのメリットがなくなってしまいます。とはいえ、メリットとして挙げられるデータコネクタ機能は魅力的で40以上ものサービスをサポートしているので、今後の日本語対応にも期待したいです。 Amazon Bedrock + Knowledge base Amazon BedrockとはAWSが提供する基盤モデルを活用した生成AIアプリケーションを構築するサービスです。RAGを使わずとも基盤モデルを使うことはできますが、RAGの機能としてKnowledge baseがあり、今回は一緒に検証を行いました。 docs.aws.amazon.com 行ったこと 基盤モデルはClaude 3 Sonnetを採用 Knowledge baseとしてデフォルト構成のS3 + OpenSearch Serverlessを採用 API経由での社内ドキュメントに関するQ&Aを実施 メリット 自分で実装せずにRAGに関する様々な手法を簡単に組み込める 埋め込みモデルを使ったベクトル化 ハイブリット検索の適用 参照ファイル名の返却 AWS完結でリソース連携ができる 普段使ってるS3やOpenSearch、他サービスとの連携もスムーズ デメリット RAGでの最新の手法を使いたい場合は精度向上でのカスタマイズの余地が限定的 リージョンはオレゴン、バージニア北部限定 結果 Amazon Bedrockでは自由な基盤モデルの選択とKnowledge baseを活用することでRAGで試したい手法がすでに内部ロジックとして入っていたり、オプションとして用意されているのはかなりエンジニアの工数削減になりそうです。 例えば、直近ではAmazon Bedrockで ハイブリット検索のサポート が発表されましたが、こういったRAG手法のアップデートにAWS側が追従してくれてすぐに使用可能な状態になるのは開発者としては嬉しいです。特にかなり早いスピードで手法が研究されているこの分野では日々世の中に出てくる新しい手法を自分たちで取り入れてついていくことはエンジニアのリソースが限られている場合には難しいので、Amazon Bedrockを使用する大きな理由になると感じました。 OpenAI Assistant API v2 Assistant APIはOpenAIが提供するアシスタント開発のためのRetrievalやCode Interpreterを利用できるAPIです。4月にはv2のリリースが行われ、RAGにおいてもfile_searchやvector_store機能の追加など大幅に強化されました。 https://platform.openai.com/docs/assistants/whats-new platform.openai.com 行ったこと vector_storeへのデータ格納 file_search機能を使ったRAGの処理を実装 API経由での社内ドキュメントに関するQ&Aを実施 メリット OpenAIを利用したい場合のRAGの選択肢として一番工数が削減できる 埋め込みモデルを使ったベクトル化 リランキング クエリRewrite デメリット 後述する回答精度の検証においても精度が悪く、誤回答や参照ファイルの引用ができないケースが目立つ 1アシスタントにつき、1万件がファイル上限となる RAG周りの処理でのチューニングはかなり限定的 結果 Assistant APIを使うことでAmazon Bedrockと同じようにfile_searchを使うことでRAG周りの処理での最適化が行われるのは強みに感じました。ただ、Amazon Bedrockと比較するとファイルサイズでの制限事項があったり、オプションとして選択できる余地も限定的なため、その辺りは要件に合わせて判断する必要があります。 特に今回の検証で気になったのは精度面の部分です。他のRAG基盤と同じ質問をした場合にAmazon Qを除くと最も誤回答が多く、精度改善のためにはデータ投入の仕方などいくつか試しながら試行錯誤していくことになりそうです。 OpenAI Chat API + VectorDB(pinecone) 最後はOpenAIが提供するChatAPIとVectorDBを使った自前でのRAG基盤の構築になります。今回はVectorDBとしてpineconeを選択して検証を進めました。 https://platform.openai.com/docs/api-reference/chat platform.openai.com www.pinecone.io 行ったこと ドキュメントデータをembeddings APIでベクトル化してpineconeに保存 LangChain を使ったRAGの処理を実装 社内ドキュメントに関するQ&Aを実施 メリット 自前実装のため、ブラックボックス化されているところが少なくチューニングの自由度が高い すでにChatAPIを使用している基盤がある場合は追加実装で導入可能 デメリット 専属でメンテナンスできるチームがいない場合には、実装・運用コストが高い 結果 自前でVectorDBと組み合わせて実装を行う場合、その工夫の余地は大きいのが強みです。一方でその実装の自由度の高さはチーム状況によって大きなメリットになるか、デメリットになるか大きく分かれていきそうに感じました。Amazon Bedrockでも記載の通り、RAG周りでの手法の研究が急速に進展する中で、高精度を求めてチューニングできるチーム体制があれば非常に有効ですが、そうではない場合にはAmazon Bedrockのサービスでのサポートの方が自分たちが対応するより早いという結果になり、採用するメリットが薄れると思います。 各基盤の比較 ここまでそれぞれの選択肢のメリット、デメリットを記載してきましたが、最後に回答精度の検証も含めた比較を行いました。 質問と模範回答からなるテストデータを準備 社内のドキュメントデータとしてConfluenceの特定のワークスペースをHTMLで保存する形で用意しました。精度評価に関してはより厳密にやるのであればベクトル変換した結果の意味の近さで判定する手法もありますが、今回に関してはそれぞれのConfluenceページに対する質問とそれに対して回答する場合に正しくそのページIDを引用できたかを精度の指標としています。下記のようなデータセットを50問ほど用意しました。 [ { " question ": " Google アカウントにログインできない場合、どうすればいいですか? ", " answerID ": " 1990623256 " } , { " question ": " Zoom Roomを利用する具体的な手順を教えてください。 ", " answerID ": " 2027815464 " } , { " question ": " ビルの入館証を発行するにはどうすればいいですか ", " answerID ": " 2182709419 " } , ... ] 特に今回の社内独自で溜まった知識をもとに答えさせるような場合に間違った情報をさも正しい回答かのように振る舞ってしまうハルシネーションは極力避けたいですし、回答文言が間違えていたとしても参照するページを正しく引用できていれば利用者は自分でその回答の正しさを評価できるので重要な比較基準になります。 2024年4月時点での回答精度も含めたそれぞれのRAG基盤の比較を表にしてまとめました。 RAG基盤 カスタマイズ性 実装の容易さ 正答率 Amazon Q Business × △/× - Amazon Bedrock + Knowledge base △ ○ 90% Assistant API v2 × ○ 50% OpenAI API + pinecone ○ △ 85% ※Amazon Q Businessは日本語未対応のため、精度評価は実施していません Amazon BedrockとOpenAI API + pineconeでは正答率に大きい差は今回では生まれず、どちらもほとんどチューニングをせずともある程度高い回答精度が出る結果となりました。一方で前述したようにAssistant API v2にはまだ回答精度面でいい結果が出ず、うまく参照ファイルを引用できないものも多かったです。 こうした比較検討の結果、現状の選択肢としては以下2点になりそうです。 RAG基盤のメンテナンスにリソースを十分に取れる場合にはOpenAI API + VectorDBを使った自前開発 AWSリソースとの連携を重視したり、RAG基盤開発でのリソースが十分に取れない場合にはAmazon Bedrock Amazon Bedrockと社内ChatAppの繋ぎこみ これまでの検討からエブリーでRAG基盤を構築する場合、Amazon Bedrockの導入の可能性が高い一方で社内での知見も少なかったため、Amazon Bedrockを選択した場合の社内ChatAppへの繋ぎこみを検証しました。 質問への回答テキストの生成 回答テキストの生成部分は以下のようにKnowledge baseとモデルを指定することで参照ファイルと回答を同時に取得することができます。参照ファイルが1件も見つからない場合には回答文言があってもそれを引用せずに固定文言を返すことで回答できない場合の案内も誘導できます。 def generate_text_from_knowledge_base (self, query): response = self.bedrock_client.retrieve_and_generate( input ={ 'text' : query}, retrieveAndGenerateConfiguration={ 'type' : 'KNOWLEDGE_BASE' , 'knowledgeBaseConfiguration' : { 'knowledgeBaseId' : self.kb_id, 'modelArn' : self.model_arn, } } ) text_response = response[ 'output' ][ 'text' ] urls = set () # セットを使ってURLの重複を防ぐ # citationsからretrievedReferencesを処理する if 'citations' in response: for citation in response[ 'citations' ]: if 'retrievedReferences' in citation: for ref in citation[ 'retrievedReferences' ]: if 'location' in ref and 's3Location' in ref[ 'location' ]: uri = ref[ 'location' ][ 's3Location' ][ 'uri' ] file_name = urlparse(uri).path.split( '/' )[- 1 ] # .html拡張子を削除 file_name = file_name.replace( '.html' , '' ) url = "https://xxxxxx.atlassian.net/wiki/spaces/hr/pages/" + file_name urls.add(url) # 重複しないようにセットに追加 if len (urls) > 0 : text_response += " \n\n 参考URL: \n\n " + " \n " .join(urls) else : text_response = """ 申し訳ありません。入力した質問に関する回答が見つかりませんでした。質問内容を変えるか、#all_citで担当者に問い合わせてください。 """ return text_response 社内ドキュメントのKnowledge baseへのデータ投入 今回はConfluenceのAPIを使って、特定のワークスペースの全ページのHTMLをS3に保存する形で検証を行いました。Knowledge baseではS3に投入しただけでは反映されないので同期処理を実行する必要があります。データ取得の詳細は省きますが、以下のような処理を定期実行することによって、常に最新の情報を更新することができます。 def resource_data_sync (bedrock_data_source_id, bedrock_knowledge_base_id): try : boto3.client( 'bedrock-agent' ).start_ingestion_job( dataSourceId=bedrock_data_source_id, knowledgeBaseId=bedrock_knowledge_base_id ) except ClientError: print ( "Couldn't resource data sync" ) raise def main (): try : # データ更新に必要な値をセット confluence_user = os.environ.get( "CONFLUENCE_USER" ) confluence_api_token = os.environ.get( "CONFLUENCE_API_TOKEN" ) confluence_domain = os.environ.get( "CONFLUENCE_DOMAIN" ) confluence_export_root_page_id = os.environ.get( "CONFLUENCE_EXPORT_ROOT_PAGE_ID" ) s3_bucket = os.environ.get( "S3_BUCKET" ) s3_bucket_prefix = os.environ.get( "S3_BUCKET_PREFIX" ) bedrock_data_source_id = os.environ.get( "BEDROCK_DATA_SOURCE_ID" ) bedrock_knowledge_base_id = os.environ.get( "BEDROCK_KNOWLEDGE_BASE_ID" ) # 既存で投入済みのs3データを全て削除 clear_files(s3_bucket, s3_bucket_prefix) # コンフルのページをs3にアップロード download_page(s3_bucket, s3_bucket_prefix, confluence_export_root_page_id, confluence_domain, confluence_user, confluence_api_token) # bedrockの同期を呼び出す resource_data_sync(bedrock_data_source_id, bedrock_knowledge_base_id) except Exception as e: print (f "An error occurred: {e}" ) デモページ 以上のような基盤を整えつつ、社内ChatAppにデモページの作成を行いました。RAG基盤でのチューニングやデータ前処理が中心で導入自体はそこまで手間ではなく簡単に行うことができます。 社内ChatAppのデモページ 導入上の注意点 Knowledge baseはデフォルトでOpenSearch Serverlessを使用しますが、最低スペックであってもデータ量に関わらず、月200ドル以上がコストとしてかかってきます。エブリーで今後、本格導入を検討する際には、このコストに見合うように社内の課題を整理し、費用対効果を十分に考慮した導入を進める必要があります。また、Knowledge baseでは他のベクトルDBも選択肢として利用可能なので、コスト面も含めて比較検討を進めていきたいです。 RAG基盤開発での課題 Amazon Bedrockに限らず、今回RAG基盤のPoCを行った中で以下の点は開発する中で課題と感じました。 社内ドキュメントに埋め込まれたスライドや画像などテキスト以外の部分の回答精度 テストデータでの検証を行っていく中で誤回答をするケースで一番問題だったのが、ドキュメント内にテキスト以外の埋め込みスライド、画像があるページに基づいた回答でした。今回のConfluence APIでのデータ取得ではデータをHTMLとして保存しましたが、そのままだとテキスト以外の情報のリンクまで辿って解釈できるわけではありません。実際により精度を高めるとなると前処理として参照スライドや画像情報も読み取って保存する必要が出てきそうです。 回答結果の評価 適切な質問と回答のテストデータを用意するのにまず労力がかかります。今回は工数を削減するために、社内ドキュメント情報から生成AIを活用してそれぞれのページごとに想定質問を作成しましたが、それでも適切な質問ではないものもあり、人間が評価しながら修正する必要があります。 また生成された回答の評価もより細かく行うのであればベクトル的な意味の近さで評価するといった工夫も考えないといけません。 精度改善のフィードバックループを回しにくい 精度改善するためには1パラメータでも変更したらテストを回していくようなフィードバックループを回していきたいところですが、それを実行すること自体でも基盤モデルの利用料がかかるため、積み重なると大きなコストになります。 ただ、この問題は直近のGPT-4oの登場でも起こっているように、今後より高精度で安価に利用できるモデルが次々出てくることを考えると無視できる問題になるかもしれません。 終わりに 今回の挑戦weekではチームを組んで、社内ナレッジ活用のためのRAG基盤のPoCを行いました。RAG基盤の構築はすでにいくつもの選択肢があり、専門的な知識が十分になくとも導入自体は簡単に行うことができそうです。 一方でこういったRAG基盤自体も手段の一つでしかなく、とりあえず導入してみるだと結果的に使われずにコストだけかかるものになっていたというケースも少なくなさそうです。今後は前述した通り、こうした技術検証を参考にしつつ、実際に本格導入して社内で使われるツールにするためにしっかりと課題や解決方法を整理して作業の効率化を考えていきたいです。
アバター