booklista tech blog

booklista のエンジニアリングに関する情報を公開しています。

Laravel Queue を使ってデータ連携を準リアルタイム化した話

自己紹介

はじめまして。 株式会社ブックリスタ プロダクト開発部でエンジニアをしている八幡と申します。

現在は、コミックアプリ「コミック ROLLY(運営:株式会社ソニー・ミュージックエンタテインメント)」の開発・運用を担当しています。

はじめに

システム間のデータ連携方式は、大きく分けるとオンラインとバッチの 2 種類があります。

方式 連携の起点 即時性
オンライン 連携元 即時
バッチ 連携先 遅延あり

コミック ROLLY では、以下の観点からオンライン連携方式を採用することにしました。

  • 今回の案件では連携元側の開発が必要であったので、連携方式は自由に決めることが可能
  • 緊急でデータ状態を変更したいような業務フローが存在する
  • 連携頻度はそこまで高くない

今回はオンラインデータ連携方式を用いて、準リアルタイム化を実現したことについてお話いたします。

最終的な構成

  • 連携元システムの対応がシンプルになるように Http エンドポイントでの連携通知受領
  • データそのものは直接連携せずに連携先システム側へ照会する
    • 更新通知には照会のためのキーだけが存在する
  • 再実行は Http のエンドポイントに対して更新通知を直接再送する

メッセージキュー(Amazon SQS)を利用した非同期オンライン連携

オンラインデータ連携方式で最初に考慮すべき点が、同期で行うか非同期で行うかとなります。

同期連携を採用した場合、リアルタイム性は非常に高いものの、連携元システム側は連携先システム側の処理完了を待たなければなりません。
また、連携が失敗した場合に再送やトランザクションのロールバックといったことも考慮しなければなりません。

今回求められていた要件では、加工処理が複雑・業務上不整合を起こすデータは意図的にエラーとする、といった特徴があったので、同期方式ではなくメッセージキューを利用した非同期方式を採用しました。

メッセージキューとしては、Amazon Web Services のフルマネージドなメッセージキューサービス、Amazon SQSを採用しています。

Laravel Queue の活用

PHP の Web フレームワークであるLaravel には、Queue 連携機能があります。

今回のプロジェクトでは、一部サブシステムに Laravel を利用していることもあり、技術スタック的・ソースコード管理的にこの Laravel Queue を利用するのが最善と判断しました。

初期設定

Laravel ではconfig/queue.phpにてキューの定義をしますが、プリセットの状態で SQS 接続に対する定義が存在します。

        'sqs' => [
            'driver' => 'sqs',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
            'queue' => env('SQS_QUEUE', 'default'),
            'suffix' => env('SQS_SUFFIX'),
            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
            'after_commit' => false,
        ],

環境変数として、最小で以下の3つを利用している AWS 環境に併せた設定とすることで、SQS 接続の初期設定は完了となります。

環境変数 設定値 備考
SQS_PREFIX https://sqs.ap-northeast-1.amazonaws.com/9999999 東京リージョン&アカウント ID:9999999 の場合の設定
SQS_QUEUE キュー名称 SQS 上のキュー名をそのまま設定
AWS_DEFAULT_REGION ap-northeast-1 東京リージョンの例

key/secret は AWS 接続のためのアクセスキー情報ですが、今回はキューを扱うプロセスは ECS 上で稼働するため、アクセスキー接続ではなく IAM 認証で対応しています。

ジョブの作成

ジョブは、キューから取得したメッセージを処理するクラスです。

キューメッセージのコンシューム(ポーリング)は、Laravel 自体が適切に処理してくれるので、処理の実態となるジョブの実装に集中できます。

以下のコマンドでapp/jobs配下にジョブクラスが作成されます。

php artisan make:job TestJob

TestJob

class TestJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     */
    public function __construct()
    {
        // コンストラクタ処理を記述
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        // 実ジョブ処理を記述
    }
}

キューメッセージの投入

前述のジョブを動かすためには、キューにメッセージを投入する必要があります。

今回の構成では、Http エンドポイントとして連携通知を受け取る仕組みとしていますので、シンプルに API として作成します。

route/api.php

 Route::post('enqueue', [EnqueueController::class, 'create']);

EnqueueController

class EnqueueController extends Controller
{
    /**
     * Store a new job.
     */
    public function create(EnqueueFormRequest $request): mixed
    {
        TestJob::dispatch(json_encode($request->toArray()));
        return response()->json('success');
    }
}

JSON 形式のメッセージを受け取るようにしていますので、コード例は割愛しますが FormRequest による Validation も実施しています。

ここで実行する Job クラスを指定していますが、この情報はメッセージ内に書き込まれます。

キューワーカーの起動

この状態で Laravel を起動した場合、API で連携通知の受信 → キューへのメッセージ投入は行われますが、ジョブは実行されません。
ジョブを動かすためには、Worker を別のプロセスとして起動する必要があります。

php artisan queue:work

Worker プロセスはキューをポーリングし、メッセージを受信したらジョブを起動します。

前述のキューメッセージの投入の項にある通り、実行する Job クラス情報がメッセージに含まれていますので、適切なジョブの実行が可能です。

また、同じキューに対して別の Job クラスを指定して投入したメッセージであれば、その別の Job クラスで処理することになります。

これにより、1つのキューで複数の Job クラスを稼働させることも可能です。

複数キュー指定

複数のキューを 1 つの Worker で処理する方法もあります。

php artisan queue:work --queue=queue,low-queue

--queueオプションを利用することで、queue.phpに定義したキュー名とは別のものを指定できます。

キュー名の指定は、カンマ区切りで複数指定が可能で、指定した順で優先度が設定されます。
複数キューを指定した場合、優先度が高いキューから処理し、すべてのメッセージを処理し終えたら次の優先度のキューを処理します。
よって前者のキューにメッセージが滞留する状況になると、後者のキューのメッセージは待たされ続けることになります。

複数キューを利用する場合は、この点を踏まえた設計が必要です。

処理済みメッセージの削除

Worker によって受信したメッセージは、Job 処理がタイムアウトとならない限り成否にかかわらずキューから削除されます。

タイムアウトによるメッセージの解放、処理完了によるメッセージの削除は、Worker が適切に実施してくれます。

なお、SQS を利用している場合、タイムアウト時間は SQS 側の可視性タイムアウト時間に準拠します。

可視性タイムアウトに関しては、以下を参照ください。

https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html

失敗時の自動リトライ

Laravel Queue では、失敗時に自動で再実行する仕組みが用意されています。
再実行回数の範囲内であれば失敗として扱われないことになるので、ネットワークの問題などの一時的な障害に対応できます。

Job クラスで、2つのプロパティを設定することでこの機能は利用できます。 以下の例では、失敗時には 10 秒待機してリトライするようになります。

class TestJob implements ShouldQueue
{
    /**
     * jobのリトライ回数
     */
    public int $tries = 3;

    /**
     * jobのリトライまでの待機時間(秒数)
     */
    public int $backoff = 10;

    ...
}

失敗時の通知処理

Laravel Queue では、失敗時の後処理を記述するためのフックポイントが用意されています。
今回の案件では、このフックポイントで Slack への通知連携を行うことによって、失敗を即時検知できるようにしました。

class TestJob implements ShouldQueue
{
    ...

    public function failed(Throwable $exception): void
    {
        // Slackへの通知処理
    }
}

失敗処理の保管

Laravel Queue では、失敗した情報をテーブルに書き込みます。
書き込まれた失敗情報をもとに、再実行をするための機能も用意されています。

今回は、Http による連携通知で再実行することとしたため利用しませんでしたが、興味がある方はぜひ調べて活用ください。

最後に

今回は Laravel Queue を利用した非同期データ連携による準リアルタイム化の実現についてお話させていただきました。

データ加工要件の複雑性から今回は採用を見送っていますが、よりシンプルな構成として SQS+Lambda 構成などもよく使われる手法ですので、興味がある方はぜひ調べてください。

データ連携をバッチで実現しているのをオンライン化したい、Laravel を活用したい、といった方にとって本記事が少しでも参考になることを祈っております。

頻出利用パターンで攻略するIAMロール(前編)

こんにちは。プロダクト開発部でクラウドインフラエンジニアとして業務を行っている高澤です。

AWS構築・運用にてIAMロールを使用する機会は多くありますが、細かい設定・状況による違いがあり、混乱している方も多いのではないでしょうか。

実は「日常でよく使うIAMロール利用パターン」は限られており、すべての設定・詳細を熟知しなくても問題ありません。

それら「日常でよく使うIAMロール利用パターン」を紹介するにあたり、IAMロールは単体の機能ではなく、関連サービスや前提を抑えたのちに理解するのがスムーズであるため、前編・後編に記事をわけました。

まず前編である本記事では、主に「IAMロールとは何か」についての概要を説明し、後編では「IAMロールの利用パターン」について具体的な例を紹介する予定です。

この記事がみなさまのIAMロール理解の一助となれば幸いです。

目次

  • この記事の目的
  • IAMロールとは何か
    • AWS Identity and Access Managementについて
    • ポリシーとは何か
    • ポリシーを設定できる対象について
    • IAMロールの位置付け
    • IAMロールとIAMユーザー(またはAWSリソース)の違いは何か
    • IAMロールを設定できる対象について
    • ロールを利用する、とはどういうことか
    • IAMロールの利点とは
  • 前半のまとめと次回予定
  • 参考URL

この記事の目的

IAMロールについて、主要な利用パターンを把握し、利用シーン・利用方法が容易に理解できる、ということが目的です。

前編はその前提となる「IAMロールとは何か」の解説となります。

詳細な網羅・例外の説明は対象外とします。

IAMロールとは何か

まず、そもそもIAMロールとは何か、について説明します。

IAMロールは「AWS Identity and Access Management」(略称IAM)の機能の一部です。

IAMがどのようなサービスなのか、から話を進めていきます。

AWS Identity and Access Managementについて

AWS Identity and Access Managementについて、AWS Identity and Access Management のドキュメントから抜粋します。

AWS のサービスへのアクセスをセキュアに制御するためのウェブサービスです。IAM を使用すると、ユーザー、セキュリティ認証情報 (アクセスキーなど)、およびユーザーとアプリケーションがアクセスできる AWS リソースを制御するアクセス許可を集中管理できます。

となっています。

ざっくりまとめます。

「AWSサービスの利用権限を管理するサービス」

です。

そして、利用権限の定義は「ポリシー」というもので行います。

ポリシーとは何か

ポリシーを日本語に訳すと「規定」であり、IAMでは「アクセス許可の設定」のことを指します。

権限の設定内容、と捉えると良いのではないでしょうか。

具体的にはポリシーは 「JSONで書かれたドキュメント設定」 として作成・管理されます。

IAM でのポリシーとアクセス許可

では、このポリシー(つまりアクセス許可の設定)は何に対して設定できるのでしょうか。

ポリシーを設定できる対象について

ポリシーを設定することを「ポリシーをアタッチする」と言います。

ポリシーは「権限の設定内容」です。

そのため、ポリシーを使う対象にアタッチし、対象がどのようなアクセス許可(許可・拒否)を持っているのか、を規定できます。

つまり、権限の設定をしたい対象のみにアタッチできるということですね。

(権限が関係ないものにアタッチはできない。では権限が関係するものは何なのか、ということです)

さて、そのポリシーをアタッチできる対象です。

「アイデンティティ(ユーザー、ユーザーのグループ、ロール)やリソース(AWSリソース)」 となっています。

アイデンティティベースおよびリソースベースのポリシー

「アイデンティティ」の対象である「ユーザー、ユーザーのグループ」についてはイメージしやすいですが、 「ロール・リソース」がちょっとわかりにくいですね。

ロールについては後ほど説明しますが、AWSリソースの具体例はどういったものがあるか記載します。

AWSリソース で、よくアタッチするリソースの例としては以下です。

  • S3
  • APIGateway
  • CloudWatch Logs

AWSコンソールなどで、S3のポリシーを作成する、ということをよく実施するのではないでしょうか。

注意点としては AWSリソース はAWSに存在するすべてのリソースではなく、特定のリソースです。

こちらに一覧があります(リソースベースのポリシー で はい となっているものです)

IAM と連携する AWS のサービス

IAMロールの位置付け

上記で説明したポリシーについて、の説明「アイデンティティ(ユーザー、ユーザーのグループ、ロール)に付与(アタッチ)できます」というところで「ロール」がでてきました。

このロールはもちろんIAMロールのことですが、ロールとはなにか、というと、日本語に訳した通り「役割」となります。

役割自体には操作する主体が存在しないので、その役割を担う主体がユーザーか、ユーザーのグループか、と考えるのが分かりやすいです。

何らかの操作・作業がしたい場合に、その権限を付与したロール(役割)を定義します。

そして、そのロール(役割)を「操作をする主体」に付与すると、操作をする主体が「その役割を担うことができる」ということです。

IAMロールとIAMユーザー(または AWS リソース)の違いは何か

IAMロールとIAMユーザー(またはAWSリソース)の違いを簡単に書きます。

  • IAMロールは「役割」の定義であるため、IAMロール自体は操作をしない
  • IAMユーザー(またはAWSリソース)は操作をする主体

です。

つまり、IAMロールはユーザー または AWSリソース に付与された場合効果を発揮する、ということになります。

IAMロールを設定できる対象について

では、このロールは何に付与(アタッチ)できるのでしょうか。

「ロールをそのまま利用できる形」でアタッチできるのは「リソース(AWSリソース)」だけです。(そう振る舞う、ということで、内部の仕組みは省きます)

ここで「ユーザーやグループなどにもできるのではないか」という疑問が湧きます。

ユーザーやグループなど には、 「ロールをそのまま利用できる形」ではなく、 「ロールを利用できる(ロールを引き受けることができる)」という指定ができます。

ここはわかりにくいところなのでしっかり説明します。

上で説明した通り、ロールの「基本的な使い方」は、「リソース(AWSリソース)にアタッチし、リソースがそのロールの権限でAWSサービスを利用できるようにする」というものです。

アイデンティティ(ユーザー、ユーザーのグループ、ロール)には、「ロールを利用できる(ロールを引き受けることができる)」という権限をつけられます。

この場合は、リソース(AWSリソース)とは違い、ロールの権限は、「自動的に使える状態」にはなりません。

では、どう利用するかというと、利用するタイミングでロールを引き受けたい、というアクションを実行し承認されると一時的にそのロールの権限でAWSサービスを利用できるようになります。

「ロールを引き受けたい、というアクションを実行」の詳細です。

Security Token Service の AssumeRole という操作にて、このロールを引き受けたい、というアクションを行うということになります。

AssumeRoleについては次のセクションで説明します。

ロールを利用する、とはどういうことか

ここまで、ロールを利用する、といってきましたが、具体的にどう利用をするのかを説明します。

リソースがロールを利用する場合

リソースにロールをアタッチ(付与)すると、リソースからロールに付与されているポリシーで許可された操作が可能になります。

リソースがロールで利用を許可された別のロールを利用する場合

ロールに別のロールの利用許可がついている場合(sts:AssumeRole で、別のロールの利用許可がついている場合)は、そのままでは別のロールについている利用許可は使えません。

なぜなら、別のロールになれる、という権限がついているだけであるためです。

別のロールに許可されている操作をする場合には、 sts:AssumeRoleを行い、そのロールを引き受ける操作を実施する必要があります。 別のロールを引き受けることにより、一時的にその別のロールの権限で操作ができます。

注意としてその場合に主体ができる権限は 操作の主体がなにか、によって挙動が変わります。

たとえばユーザーがスイッチロールで別アカウントへ遷移する場合は、 スイッチロール先のアカウントでは、元のアカウントで許可されていた操作はすべてできず、 引き受けたロールの操作のみが可能となります。

プログラムなどでそのような動作になるとしたらこまりますね。

その場合、引き受けたロールで一時的なクレデンシャルを発行し、そのクレデンシャルを使用して操作をしたいクライアントを作成して何らかの操作をすることになります。

元の操作をできるクライアントも生きているが、ロールを引き受けて別のロールの権限での操作が可能なクライアントもいる、 というイメージです。

IAMロールの利点とは

ここまでIAMロールを説明してきましたが、 そもそも、IAMロールにはどのような利点があるのでしょうか。

利点の大きい順に書いていきます。

1.リソース(AWS リソース)につけられる

一番の利点であり、IAMロールの主要な利用方法です。 AWSリソースには直接権限を付与できませんが、ロールを付与することで、AWSリソースが主体として振る舞う際に、ロールの権限を利用できます。

具体例:EC2,FargateやRDSなどのリソースにアタッチでき、リソースによる操作にロールの権限が利用されます。

2.一時的に使える許可を取得できる(AssumeRoleの場合)

ロールではなく、ポリシーを付与した場合には、常時許可された状態になります。

ですが、ロールを利用することにより、常時ではなく、使う時のみ一時的に権限を取得して利用ができます。

そのため、高い権限を利用するアプリケーションなどの場合、利用シーンを限定することによって安全性を高めることができます。

3.アカウント間で使える(AssumeRoleの場合)

アカウント間での権限許可をロール以外で行う場合、以下の2つが可能です。

  • ユーザーを作成してそれぞれログインする
  • 特定のユーザーの操作を双方のアカウントで許可する

これらは個別に設定する必要があるため、管理が煩雑になります。

ロールであれば、許可したい側のアカウントで権限を指定し、管理ができます。

許可された側のアカウントでも、ロールの使用を許可するグループ、ポリシーによって、使えるユーザー・リソースを柔軟に管理できます。

4.役割であるため、ロールの利用を許可しておけば、都度切り替えて利用できる

あるアカウントのアプリケーションが別のアカウントA・Bのロールを利用する場合、ロールであれば、特定の操作をする際にロールを引き受けて利用できます。

途中で利用するリソースが変わった場合も、ロールであるならば付け替えやロールの利用を複数許可できます。

もしも操作対象としてアカウントCが増えた場合も、使えるロールを増やすだけで操作が可能となります。

前半のまとめと次回予定

以上、まずはIAMロールの説明、および利用できる対象とその特徴について説明してきました。

今回この記事を作成するにあたり、知っているようでよく考えるとなんだろう、ということも多かったため、知ったあとはその内容を他の人に説明できるとより内容理解の助けになると思われます。

説明した内容の理解確認として、以下を簡単に説明できると良いです。

  • IAMサービスとはどういうものか
  • IAMポリシーとは何か
  • IAMロールとは何か
  • IAMロールとIAMユーザー(またはAWSリソース)の違いは何か
  • IAMロールを設定できる対象は何か
  • AssumeRoleについて日本語で説明すると

後編では以下について、よくある例を交えて具体的にイメージできるような記事を書く予定です。

  • IAMロールの利用パターン一覧
  • IAMロールの利用パターンの具体的な例、および設定・利用方法

乞うご期待ください。

参考URL

以下、参考にした記事です。

テクニカルトレーナーと学ぶ AWS IAM ロール

IAM でのポリシーとアクセス許可

アイデンティティベースおよびリソースベースのポリシー

IAM ロール

IAM ID (ユーザー、ユーザーグループ、ロール)

スイッチロールなしで別アカウントの S3 バケットにマネジメントコンソールからアクセスしてみた

AWS初心者にIAM Policy/User/Roleについてざっくり説明する

Principal 要素で IAM ロールを指定するのと IAM ロールを引き受けたセッションを指定するのは何が違うのか? 72 個のパターンで考えてみた

AssumeRole(スイッチロール)を理解して、AWSへのデプロイを少しでも安全に実施しよう #devio2021

すべての AWS プリンシパルを信頼ポリシーで許可している IAM ロールが環境にないか確認する

イラストで理解するIAMロール

AssumeRoleとはなんぞや!

IAM Roleを理解する

IAMポリシーの概要とデザインパターン

多言語展開のためにFigmaプラグインを開発した話

アイキャッチ

はじめに

株式会社ブックリスタのプロダクト開発部/新規事業開発チームでエンジニアをしている城と申します。 iOS向け推し活アプリOshibanaとショートマンガまとめ読みアプリヨムコマをより良くすべく日々奮闘しています。

Oshibanaについては以前の記事で紹介をしていますので、ぜひともご一読ください。

App Storeで3位になったアプリをFlutterでつくっている話 https://techblog.booklista.co.jp/entry/2022/07/11/155531

Oshibanaは日本語表示のみ対応しておりましたが、多言語展開をすべくまずは中国語での表示を実装中です。 多言語展開するにあたりUIラベリングの翻訳データをどのように管理するかチームで話し合い、最終的にFigma1のプラグインを開発するに至りました。 この記事ではチームで検討したことを中心に、Figmaプラグイン開発でのつまずきポイントもお伝えします。

プラグインの紹介

今回Oshibanaが公開したFigmaプラグインText to variablesです。

Figma内のテキストを一括でバリアブルズに取り込む事が可能です。 バリアブルズ取り込み時にテキストとバリアブルズの紐付けも自動で行います。 取り込み不要なレイヤーはロックをかければ取り込まない設定が可能で、さらに取り込み対象テキストをチェックボックスで選択可能です。

Figmaベースで多言語対応を検討されている方はぜひ使ってみて頂けると嬉しいです。

Oshibana開発の流れ

Oshibanaでは次にリリースする機能の概要が決まると、デザイナーが中心となってFigmaでデザインデータを作成しています。 この時SlackやミーティングでPdM / デザイナー / エンジニアが集まってFigmaを囲んで機能詳細を詰めていきます。 デザインや仕様の議論はSlackやミーティングで直接聴くこともありますが、後で見返しやすいようFigmaのコメント機能を使うことが多いです。 こうして完成したFigmaデザインデータを見ながら担当エンジニアがアプリを実装し、検証を経てリリースします。

OshibanaはFigmaに仕様が詰め込まれた状態となっていて、Figmaの各レイヤーには日本語テキストが置かれた状態です。 日本国内でアプリ展開している場合、よくある状況なのではないでしょうか。

さてここで”中国語表示に対応しよう”となったら多言語リソースの管理はどのように行えばよいでしょう。

プラグイン開発までの経緯

新規事業開発チームはアジャイルで開発しているので、素早く価値を提供し検証していく事をとても大切にしています。 仕様を詰めてから開発するよりも「素早く提供できる方法」を大切な仕様選定方法としています。 結論としてOshibanaでは多言語翻訳データの管理のためにFigmaプラグインを開発するのですが、そこに辿りつくまでには別案もありました。 新規事業開発チームがなぜプラグイン開発に至ったか、その経緯を公開します。

エンジニアの要望

エンジニアの要望は2点ありました。

1つ目

多言語リソースの管理方法にかかわらず、最終的にJSONでエクスポートが可能なこと 欲しいJSONイメージはこのようなものです。

日本語.json {
  "推し": "推し",
  "生年月日": "生年月日",
  "プレミアム": "プレミアム"
}

中国語.json {
  "推し": "我推",
  "生年月日": "出生日期",
  "プレミアム": "会员专享区"
}

2つ目

できる限り実装時に見るウィンドウを増やしたくない 普段エンジニアはFigmaとVSCodeの2つのウィンドウを見て実装しています。 さらに別ウィンドウで言語リソースファイルを見る必要が出てきたら実装時の負担が増えます。 必要ならある程度は仕方がない事ですが、少ない負担で気持ちよく仕事をする環境作りは大事な事なので要望としてあがりました。

見送った案その①

まず「多言語表示がFigmaで完結できたらいいね」という意見が真っ先に出ました。 日本語で入力されたテキストから長い矢印を引いて他の言語を書くというもので、イメージはこんな感じです。   案①

Pros

  • 訳語がUI上のどの位置に紐づいているのか視覚的に分かりやすい
  • UIに関する情報はFigmaで完結できる

Cons

  • デザイナーが中国語表示時のレイアウト崩れに気がつきにくい
  • JSONデータに整形する際、Figmaからテキストをコピペする必要がある
  • デザイナーがUI作成時に矢印を引く必要があるのでUI作成負荷が上がる
  • Figma内が矢印だらけで情報が煩雑になる

Summary

この方法はメリットよりデメリットが勝ります。 特にFigma内の情報が煩雑になる事がクリティカルであるため別案を模索することになりました。

見送った案その②

次に出たのはスプレッドシートを使った案でした。 アプリのソースコード内にあるUIラベリングをすべてスプレッドシートA列へ抜き出しB列に中国語を書く、別言語を足す時はC列に書くというものです。

案②

Pros

  • シンプルで直感的に分かりやすい

Cons

  • デザイナーが中国語表示時のレイアウト崩れに気がつきにくい
  • スプレッドシートに追加文言を転記するのが大変
  • 初回の文言抜き出し時及びUI追加時に文言更新もれが起きそう
  • エンジニアが実装時に開くウィンドウが増える

Summary

すぐにでも作業を始められるシンプルさは大きなメリットです。 デメリットは案①と比較してクリティカルと思われる欠点はありませんが、デメリット数は気になるところです。

採用した案

前述のように素早く価値を提供することも重要なので、仕様が分かりやすいスプレッドシート案に決まり一度舵を切りました。 ですが”他にいい方法はないものか…デザインデータと仕様の管理はFigmaで完結できたらいいのに”という思いが残っていました。 Figmaのプラグインを探しているうちに、Figmaには後述のバリアブルズというオープンベータ機能(202403時点)が存在することに気がつきました。

バリアブルズとは

ここでは簡単に説明するので、詳細はFigma公式のガイドを参照してください。

バリアブルズとはその名の通りFigma上で定義できる変数を指し、1つの名前に対し複数ので構成された変数を定義できます。 バリアブルズはコレクション > グループ > バリアブル > 名前 > 値 という階層構造になっていて、目的に応じてコレクションやグループ分けが可能です。

バリアブルズがどんなものか、下記の画像を参照ください。

バリアブルズとは

左がバリアブルズの中身、右がウィジェット/サイズ選択/という画面のデザインデータです。 バリアブルズの中身はブログサンプルというコレクションの中に、ウィジェット/サイズ選択/と画面名でグループ化されていて、値には画面に必要な全てのテキストを入れています。 は2つ作成し、それぞれの値のタイトルを日本語と中国語に設定しました。

またテキストに対し単一のバリアブルを紐付けることで、セクションやフレーム内のテキストを一括で日本語/中国語の切り替えることができます。 テキストにバリアブルを適用するには、紐付けしたいテキストを選択した状態で画像赤丸のバリアブルズ適用を押すとバリアブルがリストされるので、紐付けたいバリアブルを選択するだけです。 テキストとバリアブルの紐付け

全てのテキストにバリアブルを適用したら、中国語UIに一括で切り替えてみましょう。 ウィジェット/サイズ選択/画面全体のレイヤーを選択し、右のデザインパネルで六角形のバリアブルズボタンを押し、コレクションと値を選択します。 値は自動になっていましたが中国語を選択します。

言語切り替え

画面全体が中国語に切り替わりました。

💡 Tip
レイヤーに対するバリアブルズ設定はレイヤーの親子要素それぞれに設定できます。 親子レイヤーそれぞれにバリアブルズが設定されている場合は子レイヤーの設定が優先されます。 親レイヤーの選択でテキストが切り替わらない場合は、子レイヤー個別にが適用されていないか確認してみてください。

Pros

  • Figma上で日本語/中国語の表示切り替えが可能
  • エンジニア要望を全て満たせる
  • UIに関する情報はFigmaで完結できる

Cons

  • Figma上の全テキストをバリアブルズに設定する方法が未定
  • バリアブルズ機能がベータ

Summary

バリアブルズを使うメリットが多いのでこちらの案を採用することになりました。

バリアブルズを使う

バリアブルズを使えば多言語リソース管理をFigmaで完結して、Oshibanaの多言語展開にマッチすることはわかりました。

問題は今あるFigmaのレイヤーに配置されたテキストをどうやってバリアブルズに取り込むのか。 私たちはこれを実現する機能を備えたFigmaプラグインをこんな条件で探し始めました。

  1. テキストを一括でバリアブルズに取り込める
  2. Figma内には説明文章等も配置されているので、特定のセクションやフレーム内のテキストだけを拾いたい
  3. バリアブルの名前と同じものを値にも入れたい
  4. バリアブルズへ取込む時にグループを指定したい

条件に近いものは見つけましたが、取り込み対象がFigmaドキュメント全体だったり、グループ指定できなかったりと完全にマッチするプラグインを見つけることができませんでした。 もっと時間をかければ希望のプラグインが見つかった可能性はありますが、そこに時間をかけず必要な機能は自分達で作れば良いし、プラグインを作ってみたら楽しそうという思いもあり作ることに決まりました。

バリアブルズをJSONにエクスポートする機能はFigma Variables to JSONというプラグインで満たせたため、開発スコープから外しました。

ここまでが私たち新規事業開発チームが検討した流れです。

Figmaプラグイン開発

プラグインはHTMLとTypeScriptで開発できます。

Figmaプラグイン開発の公式ドキュメントは英語のみですが、わかりやすく書かれているのでアイデアを形にできるか悩まれた際は見てみることをおすすめします。

サンプルのプラグインを作って動かしてみます。

必要なもの

  1. Figmaデスクトップ版
  2. VSCodeなどのエディタ

サンプルプラグインを作成

Figmaデスクトップ版でプラグイン > 開発 > プラグインの新規作成...を選択します。 サンプルプラグインを作成①

プラグイン名は後からでも変更可能ですがFigmaコミュニティに表示される名称となります。 Figmaデザイン+FigJamを選択したまま次へを押します。 サンプルプラグインを作成②

今回は画面を伴うプラグインを作るのでカスタムUIを選択して名前を付けて保存を押します。 保存先を聞かれるので、ソースコードを置く場所を選択してください。 サンプルプラグインを作成③

完了を選択して作成されたフォルダをVSCodeで開きます。 サンプルプラグインを作成④

作成されたフォルダ配下で下記コマンドを実行します。

  1. npm install
  2. npm install -D @figma/plugin-typings
  3. npm run watch

Figmaデスクトップでプラグイン > 開発 > 作成したプラグイン名を押すと下記の画面が起動します。 サンプルプラグインを作成⑤

Createを押して画像のようなオレンジの四角が出てきたら成功です。 サンプルプラグインを作成⑥

解説

ui.html

プラグインのUI部分で、普通のhtmlと同様にstyleとscript要素も置けます。 ui.html > a.htmlのような画面遷移はできないようなので、表示内容を切り替えたい場合はstyle.displayを使ってdomの表示/非表示を切り替える事で実現可能です。

後述するcode.ts内の処理を呼び出すには下記にように書きます。

parent.postMessage({ pluginMessage: { type: 'create-shapes', count } }, '*')
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')

code.tsから値を受け取る時は下記の書き方です。

onmessage = (event) => {
    const msg = event.data.pluginMessage;
    if (msg.type === 'receive-shapes') {
    // receive-shapes受信時の処理
  } else if (msg.type === 'on-error') {
    // on-error受信時の処理
  }
}

code.ts

FigmaAPIを使用することでFigmaに配置済みのレイヤーを取得したり、バリアブルズを操作する処理を書くファイルです。

先述のui.htmlを開く処理です。

figma.showUI(__html__);

ui.htmlでparent.postMessage()を実行した時に呼ばれる処理です。 ui.htmlでも記載しましたがtypeで処理を分けて記述すると良いでしょう。

figma.ui.onmessage =  (msg: {type: string, count: number}) => {
  if (msg.type === 'create-shapes') {
    // ui.htmlからcreate-shapesが呼び出された
  } else if (msg.type === 'cancel') {
    // ui.htmlからcancelが呼び出された
  }
};

ui.htmlの処理を呼び出したい時は下記の書き方です。

figma.ui.postMessage({
  payload: {
    id: shape.id,
  },
  type: 'receive-shapes',
});

code.js

code.tsをコンパイルした後のファイルで、このファイルを直接修正することはしません。

manifest.json

プラグインのメタ情報が書かれたファイルです。 Figmaコミュニティで表示されるプラグイン名の指定や、実行ファイル名が記述されていますが詳細は公式のPlugin Manifestを参照してください。

あとがき

ブックリスタの新規事業開発チームは「やってみたい」を尊重してくれる環境です。 チームみんな「やってみたい」に対して「やってみようか、こんな感じの機能がいつまでにあると嬉しい〜」と背中を押してくれるので感謝しています。 チームの雰囲気①

新規事業開発チームは、素早く最小機能でリリースをしてからユーザーフィードバックに基づいて改善を繰り返すMVP開発を採用しています。 Figmaプラグインについても同様で、まずは1日でも早くチームのデザイナーに使ってもらう事を目的として最小機能でリリースしました。 その後現在も、このような雰囲気でデザイナーからフィードバックをもらい改善するサイクルが不定期で走っています。 チームの雰囲気②


  1. Figmaとはブラウザもしくはデスクトップアプリで使えるリアルタイムコラボレーションに特化したデザインツールです。

Streamlitでレコメンドの定性評価アプリを作った話

アイキャッチ

はじめに

MLエンジニアのmixidota2です。ブックリスタでは主に推薦システムの開発などをしています。
今回は推薦モデルの定性評価にstreamlitを使った話をします。
どのような課題をstreamlitで解決したか、またなぜstreamlitが推薦モデルの評価というユースケースに合っていたのかを紹介します。
想定読者としては、推薦システム(や機械学習システム)に関わるMLエンジニア・データサイエンティストやソフトウェアエンジニアの方々です。

推薦モデルの評価とは

機械学習モデルを本番投入する前にはオフライン評価をするのが一般的です。
推薦モデルの場合も同様で、オフライン評価により本番投入前にモデルの性能を事前に見積もることでユーザー体験やビジネス指標を損なわないモデルを本番投入できます。
評価についてはビジネス指標に対する定量的な評価は当然必要ですが、一方で定性的な評価も重要です。
定性評価の例としては、ユーザーに推薦されるアイテムを人が確認することで期待通りの出力になっているかを評価するなどがあります。
特に推薦モデルの出力は推薦の文脈によって多様であるため、定量的な評価だけでは見落としてしまいそうな課題も発見できます。

これまでの課題

理想的には定性評価については色んな人に触ってもらえるアプリのような環境があるとより良いのですが、そういう環境を作るのはフロントやバックエンドの知識も必要で、MLエンジニアだけでは難しいです。
そこでこれまでは推薦モデルの定性評価については、モデルを作ったMLエンジニアが推薦結果をバッチ推論でmarkdownにまとめたものを見てもらうという形で行っていました。

ただしこの方法で運用していく中での課題としては次のようなものがありました。

  • 探索的に見るには見辛い
  • 全てのユーザーやアイテムに対しての推薦結果を見ることができない
  • モデルの改善をするたびに手動で更新する必要があり、モデルの改善に伴う定性評価のコストが高い
  • 定性評価を見たいモデルの数に比例して定性評価を見られるようにするためのコストが増加する

どうやって解決したか

streamlitの採用

既存の課題から、次のようなことが実現できる方法を探していました。

  • インターフェースを使って任意のユーザーやアイテムに対しての推薦結果を見ることができる
  • できればモデルの改善をするたびに手動で更新する必要がない、あるいはその手間が現状より減らせる

この課題の解決手段として結局はwebアプリをホストしてその背後で推薦モデルを動かせる環境が一番手っ取り早いという結論に至りました。
またその上でなるべく専門知識を必要とせずそれを実現する方法を探しました。
そこでstreamlitというwebアプリの知識を必要としなくてもアプリが作成できるライブラリーに辿り着き、その簡便さと豊富なインターフェースが我々のユースケースに合っていると判断し採用しました。

streamlit採用のメリット

streamlitを使うことで次のようなメリットがありました。

  • MLエンジニアだけで簡単にアプリを作ることができた
  • 色んな人に触ってもらいやすいインターフェースができたことで、いろんな視点からフィードバックを受けることができるようになった
  • モデルの改善をするたびに手動で更新する手間が大幅に減らせた
  • アプリの背後でリアルタイム推論を走らせることにより、全てのユーザーやアイテムに対しての推論結果を手軽に見ることができるようになった

実際に作ったもの

最終的にはこのようなアプリを作りました。

ではstreamlitが具体的にどんなもので、我々のユースケースにどうfitしていたのかを説明していきます。

streamlitとは

概要

Streamlit turns data scripts into shareable web apps in minutes.
All in pure Python. No front‑end experience required.
ref. https://streamlit.io/

streamlit公式サイトの説明を引用すると、streamlitはデータのスクリプトをシェア可能なWebアプリに変換できるPythonのライブラリーです。
バックエンドやフロントエンドの知識を必要とすることなく、MLエンジニアやデータサイエンティストのみで簡単にWebアプリを作ることができます。
streamlitはデータのアプリ作成に特化しており、pandasのデータフレームやplotlyのグラフなどのデータの操作や可視化を扱うためのインターフェースが豊富に用意されています。

どんなふうに使うことができるか

基本的には表示したいオブジェクトをコードに並べていくだけで、それが自動的に解釈されてWebアプリとして表示されます。
極端に言えば、jupyter notebook内でコードを可視化する感覚でアプリの画面が書けるという感じです。
例えば次のようなコードを書くだけで、データフレームやグラフを表示するだけのアプリが作成できます。

# ref: https://docs.streamlit.io/library/api-reference/write-magic/magic
# Draw a title and some text to the app:
'''
# This is the document title

This is some _markdown_.
'''

import pandas as pd
df = pd.DataFrame({'col1': [1,2,3]})
df  # 👈 Draw the dataframe

x = 10
'x', x  # 👈 Draw the string 'x' and then the value of x

# Also works with most supported chart types
import matplotlib.pyplot as plt
import numpy as np

arr = np.random.normal(1, 1, size=100)
fig, ax = plt.subplots()
ax.hist(arr, bins=20)

fig  # 👈 Draw a Matplotlib chart

もちろん単純にオブジェクトを並べるだけがstreamlitで出来ることの全てではなく、様々なインターフェースや画面構成をstreamlitのモジュールを使用して簡単に表現できます。

streamlitのどんな点が良かったか

今回推薦モデルの定性評価にstreamlitを採用してみて、このユースケースに特にfitしているなと感じた点をいくつか紹介します。

インタラクティブに推薦結果がみれるアプリが簡単に作れる

定性評価のフィードバックを様々な人に受けてもらう観点からすると、使用者の認知負荷が低いインターフェースであることでより多くの人に触ってもらえるようになることが期待できます。
また推薦モデルの推薦起点となるユーザー(あるいはアイテム)の候補数は大量にあることが一般的です。
このような状況において現実的に任意の使用者が任意の対象における推薦結果を定性評価したい場合、コスト的な観点からもリアルタイム推論をできるような仕組みが望ましいです。
streamlitでは、このような仕組みを追加の専門的な知識を必要とせず簡単に作ることができました。
今回のユースケースのように、なるべく多くの人たちに定性評価をしてもらいたい場合には、streamlitでのアプリ作成は手段として有効だと感じました。

jupyter notebookで可視化する感覚でアプリの画面が書ける

streamlit特有の文法をほとんど覚える必要がなく、普段MLエンジニアやデータサイエンティストが使用しているツールをそのまま使用してアプリの開発ができます。
例えばpandasのデータフレームを表示したい場合は簡単には次のように書くことができます。

import pandas as pd 
import streamlit as st

df = pd.DataFrame({'col1': [1,2,3]})
st.write(df) # st.writeに渡すだけで表示してくれる

またグラフを表示したい場合は、次のようにグラフのfigを渡すだけで表示してくれるので普段の可視化のコードをほとんどそのまま使用できます。

import plotly.express as px
import streamlit as st

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length")
st.plotly_chart(fig) # st.plotly_chartにfigを渡すだけで表示してくれる

markdown形式で書いていた文章もそのまま書けます。

import streamlit as st

st.markdown("*Streamlit* is **really** ***cool***.")
st.markdown('''
    :red[Streamlit] :orange[can] :green[write] :blue[text] :violet[in]
    :gray[pretty] :rainbow[colors].''')

このように普段と同じようにコードを書くことができるので、開発コストを低く抑えられる点が推薦モデルを開発している人にとっては嬉しい点でした。

標準で用意されてるインターフェースが豊富なので、やれる操作の幅が広い

アプリ上でのインターフェースについても、streamlitでは標準で用意されているものを使うだけで多くの表現が簡単に実現できます。
例えば任意の推薦枠で任意のユーザーに対しての推薦結果を表示したい、というようなケースにおいては次のようにインターフェースや画面表示を簡単に作ることができます。

# ドロップダウンを使った推薦枠の選択 
frame = st.selectbox('推薦枠を選択してね', ['枠1', '枠2', '枠3'])


# テキストボックスで任意のアイテムIDを入力させる
item_id = st.text_input('item_idを入力してね')


# 入力したアイテムIDについてのitem2itemの推薦結果を表示する
pred_items: list[str] = predict(item_id, frame) # 適当な関数を想像してください
for item_id in pred_items:
    img_url = get_image_url(item_id) # 適当な関数を想像してください
    st.write(item_id)
    st.image(img_url)

詳しくは公式ドキュメントに任せますが、非常に多くのインターフェースが用意されています。
システムの入出力に決まった形式のあるわけではない推薦モデルのようなシステムを再現して定性評価をするようなケースにおいては、このようなインターフェースが豊富であることは有用な点かなと感じています。

streamlit用のサーバーを簡単にホストできる

streamlitはローカルでも実行できますが、アプリを公開するためにはサーバーを建ててそこでアプリをホストしたいです。
ここについても、streamlit側では簡単にサーバーを建てることのできる機能が用意されています。
ホスト環境でstreamlit run app.pyのようにコマンドを打つだけでサーバーを建てることができます。

コードの変更がリアルタイムに反映されるのでプロトタイピングが楽

これは推薦システムの定性評価にメリットがあるというよりはフロントの知識が少ないエンジニアにとってのメリットです。
streamlitでホストしたアプリはコードを変更すると自動的にリロードされる仕組みになっています。
これの恩恵としては、コードを変更するたびに手動でサーバーをホストしなおす必要がないためプロトタイピングをしやすいという点があります。
アプリ開発の時間を少なく抑えて価値提供(今回の場合定性評価ができるようになること)を素早く行うという観点で地味ながら便利な点です。

streamlitのちょっとコツがいる部分

これまで説明してきたように便利な点が多いstreamlitですが、ちょっとコツがいる部分もあります。
特に機械学習モデルをハンドルするという観点で気にしたい点をいくつか紹介します。

重めの処理をする場合はキャッシュを効かせる必要がある

streamlitはインターフェースへのインタラクションあるいはコードの変更のたびにサーバー側でコード全体の再実行が走る仕組みになっています。
そのため、例えば巨大なデータのロードや重めの前処理など実行時間が長くなるようなコードを書いている場合は再実行のたびに再度実行時間の長い処理が走ってしまいます。
このような場合にはstreamlitの機能でキャッシュを効かせることでこれを防ぐことができるのですが、この使い方に少しコツが必要です。

簡単には次のようにオブジェクトを取得する関数にデコレーターをつけることでキャッシュを効かせることができます。

@st.cache_data
def get_data():
    # 重めの処理
    return data

基本的にはデコレーターをつけた関数は、引数が同じであればキャッシュを効かせてくれるので、このように書くだけでキャッシュを効かせることができます。
なのですが、このデコレーターを使う場合にはいくつか注意しなければいけない点があります。

まずはキャッシュに使うデコレーターの種類についてです。
キャッシュのために使用するデコレーターには次の2種類が存在します。

  • st.cache_dataを使う方法

    • pythonでシリアライズ可能なオブジェクトをキャッシュできる
    • キャッシュはセッションベースで行われる
  • st.cache_resourceを使う方法

    • シリアライズ可能でないオブジェクトをキャッシュできる
    • キャッシュはglobalで行われる

どのようなオブジェクトをどのようにキャッシュしたいのかによってこれらを使い分ける必要があります。

次にデコレーターをつける関数の引数についてです。
キャッシュを使うか関数を実行するかは同じ引数を使っているかどうかで決まるのですが、ここの引数はハッシュ可能なものでなければなりません。
なので関数の引数についてはハッシュ可能なものを与えるように実装する必要があります。

まだできてないけどやりたいこと

実験管理との統合

推薦モデルの開発においては、モデルの実験設定や評価結果などを実験管理しています。
現状はアプリ側でこの実験管理の結果を取得して、ベストモデルの推薦結果を見られるようにしています。
ただしこの方法だと過去のモデルと最新のモデルの結果の定性的な比較ができません。
モデルの実験設定によってどのようにモデルの推薦結果が変わるのかが分かると、実験結果をより深く解釈できることが期待できます。
そこで定性評価のアプリと実験管理結果の連携部分を改善して、任意の実験設定で生まれたモデルに対する推薦結果を見られるような仕組みにしたいです。

テンプレート化

現状だと新しい推薦モデルを開発した際に、アプリ側の実装も新しく作っています。
アプリ側に関しては複数箇所について汎用的に表現できそうなことが実装を進める中でわかってきました。
よって汎用表現できる部分はテンプレート化することで、初期実装の開発コストを下げてより早く定性評価を回せるようにしたいです。

実際のプロダクト上と同じ表示のシミュレート

推薦結果を定性評価するにあたって、実際のプロダクト上と同じ表示をシミュレートしたいとも考えています。
理由としては、実際にプロダクト上で見られる推薦結果とただ単に推薦結果を並べただけのものでは、同じアイテムが並んでいたとしても観測され方に異なるバイアスが発生し得るからです。
例としては次のようなケースでバイアスが発生し得ると考えています。

  • プロダクト上では推薦結果をn列m行で表示している場合、ただ配列としてアイテムを並べただけとは発生するポジションバイアスが異なる
  • プロダクト上では推薦結果のおすすめ順ごとに表示するアイテムのサイズが異なる

このようなケースを考慮することで、単に推薦結果を並べただけでは気付けないバイアスを事前に発見できるメリットがあると考えられるため、可能な限り実現したい要素です。

まとめ

今回ご紹介したように、streamlitはひとりのMLエンジニア・データサイエンティストが出せる価値の幅を広くするツールとして有用だと感じています。
推薦モデルの定性評価の手法の選択肢の1つとして、streamlitを検討してみるのも良いのではないでしょうか。

ブックリスタのレコメンドチームでは様々な角度からより良い推薦をより高速に提供するための取り組みをしています。
またブックリスタでは推薦システムの開発を加速させるため、現在MLOpsエンジニアを募集中です。
興味のある方は是非ご応募ください。

機能リリースに干渉しない定期的なパッケージ更新のやり方

アイキャッチ

目次

はじめに

はじめまして、株式会社ブックリスタのプロダクト開発部に所属している伊藤です。
弊社開発のコミックアプリ、「コミックROLLY(運営:株式会社ソニー・ミュージックエンタテインメント)」のバックエンドエンジニアとして保守・開発を担当しています。

コミックROLLYは2023年8月1日にローンチしたアプリです。弊社のwebtoon制作スタジオである「booklistaSTUDIO」の作品をはじめ、さまざまなコミック作品を閲覧できます。ご興味ある方は是非下記のリンクからダウンロードいただけると嬉しいです。

https://rolly.jp/

この記事の目的

「コミックROLLY」のプロジェクトでは以下2点の課題を抱えていました。 - オープンソースパッケージを定期的にアップデート確認したい - パッケージアップデートのデプロイを機能リリースに干渉せず実行したい

この記事では課題にどう対応していったかをお話しさせていただきます。

コミックROLLYのシステムの構成について

まずコミックROLLYのシステム構成について説明します。

コミックROLLYのシステムは以下の3つで構成され、日々保守・開発しています。

  • モバイルアプリ
  • アプリに情報を渡すAPI
  • アプリに出すコミック情報を管理するCMS

上記3つのシステムは使用言語、機能などオープンソースのものを使用しており、使用バージョンはyarnなどのパッケージマネージャーを通して管理しています。

パッケージ更新の悩み

上記の通り、オープンソースのソフトウェアを使用しているので、使用する機能は継続的にアップデートされて行きます。これらのアップデート反映を長期間放置すると、セキュリティの問題があったり重大なバグの修正を見落としたりするので、こちらもなるべく最新のアップデート状態を保つ必要があります。

ただし、コミックROLLYのプロジェクトがパッケージを随時アップデートしていくにあたっては以下2つの課題があります。

1つ目は「パッケージ更新の検知と内容確認が出来ない」ことです。
ローンチ前はパッケージマネージャーから直接アップデートで解決していました。ですがこの方法だとアップデート内容がどの程度影響するかがわかりません。ローンチ直前やローンチ以降アップデートの検知と変更・更新内容は確認してからアップデートするかを判断したいので、方法を再度検討する必要がありました。

2つ目は「機能リリースとパッケージアップデートリリースは同時に行えない」ことです。
コミックROLLYはソースをGitでバージョン管理しているので、パッケージのアップデートも同様にGitで管理したいです。ですが、普段の機能開発とパッケージアップデートを同じバージョンで管理すると、バグが発生したときに開発した機能自体の問題なのか、更新されたパッケージの問題なのかの切り分けが困難になります。

Renovate導入とブランチの分岐運用で解決

私たちは、上記の問題をRenovateの使用と、パッケージ更新用のブランチを用意することで解決しました。

Renovateとは
https://github.com/renovatebot/renovate

パッケージアップデートを自動で検出してくれるシステムです。GitHubなどのプラットフォームから使用できて、ボットがアップデートを検知、自動で更新用のプルリクエストを作成してくれます。 依存関係のあるパッケージも合わせてアップデート検知と更新をしてくれるので、悩みで提示した「①検知が必要なバージョンアップの数が多すぎる」はRenovateの使用で解決できます。


このRenovateで更新用プルリクエストのマージ先を指定できるので、パッケージ更新用のブランチを機能開発用のブランチと別で用意して管理することにしました。ブランチを分けることで、パッケージ更新のみのリリースのスケジュールを開発のリリースと別対応することが可能になり、悩みの「②機能リリースとパッケージアップデートリリースは同時に行えない」が解決します。

Renovateの設定と運用

以下に実際のパッケージ更新とリリースの解決方法の詳細を記載していきます。

①パッケージ更新検知の自動化

Renovateを使用してパッケージの更新の検知、パッケージ更新した状態のPR作成を自動で行うよう設定します。

私たちはGitHubでコード管理しているので、Renovateで用意されているGithub Actionsでの自動検知・更新のアプリを導入していきます。 登録対象のアプリは以下です。

https://github.com/apps/renovate

この設定時点では、すでに社内でRenovateを導入しているチームがあり、GitHubの組織アカウント単位で導入済みでした。なので組織アカウントの管理者に使用を要求して許可をいただくことですぐに使用可能になりました。

GitHubにRenovateのアプリを導入できたら、以下画像のように自動でRenovateの設定ファイル作成のプルリクエストが作成されるので、ここから各種設定をしていきます。

Renovateの設定用プルリクエスト

https://github.com/marketplace/renovate から画像を引用しています。

設定値はプルリクエストの実施スケジュールやレビュワー、アサイニーの指定ができますが、コミックROLLYでは以下のように設定しています。

{
  "extends": ["config:base", "schedule:nonOfficeHours"],
  "timezone": "Asia/Tokyo",
  "baseBranches": [{マージ元のブランチ名を設定}],
  "labels": ["renovate"],
  "automerge": false,
  "assignees": [{担当者のGitHubアカウント名を設定}],
  "assigneesSampleSize": 1,
  "prHourlyLimit": 5,
  "prConcurrentLimit": 100,
  "ignorePaths": [{renovateの自動更新対象から除外したいファイルのパスを設定}]
}

この設定のプルリクエストをマージすると、以降はこちらが設定したスケジュールをもとに自動でパッケージ更新したプルリクエストを作成してくれます。 私たちのチームではassigneeに指定された人が主担当になるルールがあるのでassigneesを指定していますが、reviewersの指定も可能です。

②パッケージ更新用のブランチを用意する

Renovateの設定が終わったので、パッケージ更新用のブランチを設定していきます。
コミックROLLYではGitflowモデルを基準に以下のブランチで運用しています。

運用するブランチ名 役割
feature 機能追加用ブランチ
機能追加するためにdevelopブランチから切られる
develop 開発用ブランチ
機能の追加・改修が継続的にマージされる
staging リリース前確認用ブランチ
Gitflowでいうreleaseブランチだが、随時確認できるようにプロダクト環境と同じ状態にして確認用環境のために常駐させる
main プロダクトリリースブランチ
Gitflowでいうmasterブランチ

パッケージ更新は機能開発と違うバージョンで管理したいので、stagingブランチから更新用ブランチ(名称:update)を作成します。
このブランチをRenovateでbaseBranchesに設定することで、パッケージ更新のマージ先がupdateブランチになり、機能開発とパッケージ更新の管理が別々に出来ます。

③パッケージ更新と機能改善のリリーススケジュールを分ける

ブランチを分けたことで関心が分離できたので、それぞれのブランチで更新・リリースが実行できます。
ただし、リリースを分けたことで、以下の問題が発生してしまいます。 - 機能開発で追加したパッケージが更新用ブランチにないので更新対象にならない - パッケージ更新が一方で行われているので、機能開発側のパッケージが古いままになる

私たちのチームでは以下の画像のようにブランチ運用をして解決しています。

パッケージ更新のリリース

updateブランチにまとめられたパッケージ更新をリリース管理用ブランチ(stagingブランチ)にマージし動作検証を実施・完了後、mainブランチにマージしてリリースします。 リリース後はstagingブランチの内容をdevelopとupdateブランチにそれぞれマージすることで、updateブランチがdevelopと同期されます。

機能開発のリリース

developブランチにまとめられた開発機能をstagingブランチにマージ、検証後、mainにマージしてリリースします。
リリース後はstagingブランチの内容をdevelopとupdateブランチにそれぞれマージすることで、updateブランチがdevelopと同期されます。

上記のようにパッケージ更新・機能追加を別々にリリース、その内容をdevelop、updateブランチにそれぞれマージすることで先ほどの課題を解決できました。

まとめ

このようにパッケージ更新の自動検知と機能開発とパッケージ更新を分けることで、パッケージを最新の状態に保つことが出来ました。 同じような悩みを抱えている方の参考に少しでもなれば幸いです。

DynamoDB を使用した API ページネーションの実現

アイキャッチ

こんにちは。私は株式会社ブックリスタのプロダクト開発部の姚と申します。
現在、コミックROLLY(運営:株式会社ソニー・ミュージックエンタテインメント)のバックエンド開発を担当しています。
この記事では、DynamoDB を使用した API ページネーションの作成方法を紹介します。

はじめに

なぜ DynamoDB を使用するのか

DynamoDB は大量の非構造化データの処理に適しており、スケーラビリティと柔軟性に優れています。
データの性質や成長の見込みによっては、DynamoDB が最適な選択肢になることがあります。

ページネーション方式

一般的なデータベースのページネーションは、主にオフセット型とカーソル型の 2 種類が存在します。

オフセット型

オフセット型は、指定した offset から limit 件のデータを取得する方法です。RDBMS では一般的によく使われていますが、DynamoDB ではサポートされていません。

GET /items?limit=<limit>&offset=<offset>

リクエストした limit と offset の値から、次のページを取得する場合の offset の値が決まるため、レスポンスにはページングに関する情報を含める必要がありません。

{
  "items": [
    ...
  ]
}

カーソル型(本記事で紹介する方法)

カーソル型は、直前のページの最後のデータと limit 件数を指定して、次のページを取得する方法です。
本記事ではカーソル型ページネーションの実現方式を紹介します。

GET /items?limit=<limit>&lastKey=<lastKey>

レスポンスには、取得したデータと次のページのための lastKey を返します。

{
  "items": [
    ...
  ],
  "lastKey": "xxx"
}


カーソル型のページネーションはオフセット型と比べて、以下のメリットとデメリットがあります。

メリット

  • 重複・不足データが発生しない: 連続してデータを取得する際、データの増減が発生しても、データの重複や漏れが発生しません
  • パフォーマンス向上: カーソル方式を採用しているため、後のページに移動してもレスポンス速度が低下しません

デメリット

  • 任意のページに直接移動できません
    • クライアント側が移動したページのカーソル情報を保持すれば、擬似的なページ移動が可能です
  • クエリのソートで使用されるカラムに、インデックスを貼る必要があります
  • 実装が少し複雑になります

前提知識

DynamoDB のインデックス

DynamoDB テーブル内のデータのクエリを行う場合、インデックス(プライマリキー もしくは セカンダリインデックス)を指定することで、効率的にデータを取得できます。

プライマリキー

テーブルを作成する際に、テーブルのプライマリキーを指定する必要があります。
プライマリキーは以下 2 種類あります。

  • パーティションキー
  • パーティションキーとソートキーの組み合わせ

セカンダリインデックス

プライマリキー以外のインデックスを定義する場合、セカンダリインデックスを作成する必要があります。
セカンダリインデックスはパーティションキーとソートキーの組み合わせの形ですが、以下 2 種類あります。

  • グローバルセカンダリインデックス
    • パーティションキーとソートキーがテーブルと異なるインデックス
  • ローカルセカンダリインデックス
    • パーティションキーはテーブルと同じですが、ソートキーが異なるインデックス

本記事では、パーティションキーとソートキーの組み合わせを使用した場合のページネーションを紹介します。

インデックスとソートキーについて、もっと詳しく知りたい人は以下のドキュメントを参照してください。

DynamoDB テーブルクエリのページネーション

1MB の制限

DynamoDB の クエリー操作では、最大 1MB のデータしか取得できません。

クエリー結果が 1MB 以上の場合、1MB 以内の結果セットだけを返して、クエリー結果に LastEvaluatedKey という要素が返されます。
後続のデータを取得するには、LastEvaluatedKey を次のクエリーの ExclusiveStartKey として使用する必要があります。
これを繰り返して、一度に 1 ページずつ結果を取り出せます。
クエリー結果に LastEvaluatedKey がない場合、これ以上取得する項目がないことを示します。

limit の指定

ほとんどの場合、1 ページを表示するために DB から取得するデータは 1MB 以下であるため、暗黙なページネーションをそのまま利用できません。
そのため、意図的にページネーションしたい場合には、テーブルクエリーのオプレーションに Limit のパラメータを使用して、1 ページのデータ件数を制限できます。

DynamoDB のテーブルクエリー結果のページ分割について、もっと詳細をしたい場合は以下のドキュメントを参照してください。

実現

API インターフェースの設計

DynamoDB のテーブルクエリーを使用する場合、以下の情報が必要です。

limit: number;
lastEvaluatedKey: {
  partitionKey: string;
  sortKey: string;
}

これを API で受け取る場合、LastEvaluatedKey がオブジェクト構造であるため、GET リクエストの URL に直接指定できない問題があります。

素直に設計した場合

最初に考えられる設計は、LastEvaluatedKeyの属性を分解し、それを GET リクエストのクエリパラメーターに指定する方法です。

リクエスト

  • limit: 必須
  • partitionKey: 任意
  • sortKey: 任意
GET /items?limit=<limit>&partitionKey=<partitionKey>&sortKey=<sortKey>

レスポンス

{
  "items": [
    ...
  ],
  "lastEvaluatedKey": {
    "partitionKey": "xxx",
    "sortKey": "yyy"
  }
}

しかし、この設計には以下の問題があります。

  • クエリーのパラメータが多くなるため、API のリクエストが複雑になります。
  • DynamoDB のキー構造のキー構造は内部的な情報なので、可能な限り露出させたくありません。

これらの問題を解決するために、以下のように設計を改善しました。

改善された設計

改善された設計は、 LastEvaluatedKey を Base64 エンコードした文字列を GET リクエストのクエリパラメーターに指定する方法です。

必要に応じて、エンコードした文字列をさらに暗号化できます。

リクエスト

  • limit: 必須
  • lastKey: 任意(Base64 エンコードした文字列)
GET /items?limit=<limit>&lastKey=<base64_encoded_lastEvaluatedKey>

レスポンス

{
  "items": [
    ...
  ],
  "lastKey": "base64_encoded_lastEvaluatedKey"
}

この設計には以下のメリットがあります。

  • API のインタフェースがシンプルになりました。
  • クライアント側も、DynamoDB のキー構造を意識する必要がなくなります。

ただし、Base64 エンコードは JSON オブジェクトをエンコードするためのものではないため、LastEvaluatedKey をエンコードする前に、JSON 文字列に変換する必要があります。

リクエスト仕方

以下に、提案する設計に基づいたリクエストの方法を示します。

初回のリクエスト

初回のリクエストでは、lastKeyパラメーターは指定しません。

GET /items?limit=<limit>

2 回目以降のリクエスト

2 回目以降のリクエストでは、前回のレスポンスで得られた lastKey を指定します。

GET /items?limit=<limit>&lastKey=<base64_encoded_lastEvaluatedKey>

最終ページの判定

レスポンスの lastKey がない場合、最終ページと判定し、次のページを取得する必要がないです。

処理のシーケンス図

シーケンス図

注意点

DynamoDB のテーブルクエリーの特性ですが、指定した limit 件数までデータがある場合、LastEvaluatedKey を返します。
例えば、limit が 10 件で、テーブルに 10 件のデータがある場合、LastEvaluatedKey を返します。
そのため、ちょうど最後の 1 件まで返した時、LastKey があるが、次のページを取得するとデータがないという状況が発生します。
クライアント側で、この状況を判定する必要があります。

まとめ

DynamoDB でサポートされているカーソル型のページネーションは、大規模データを扱い、頻繁にデータを書き込み又は無限スクロールでデータを表示する場合など、効率よくデータを取得できます。
しかしオフセット型と違って、ページネーションのパラメーター設定と実装が少し複雑になります。
今回は LastEvaluatedKey の Base64 化と暗号化によって、クライアントからリクエストが行いやすい形に実装ができました。
DynamoDB を採用してサービスの開発する際に、ご参考になれば幸いです。

iOS17の新機能「スタンバイ」を推し活アプリに導入してみた話

アイキャッチ

株式会社ブックリスタ プロダクト開発部の酒井です。

2023年9月18日にiOS17が正式リリースされました。
メッセージやAirDropが強化されたり、ウィジェット上でボタン操作などができる「インタラクティブウィジェット」が実装されるなど、いくつかの新機能が追加されました。
その中でも「スタンバイ」という新機能に着目し、弊社で開発している推し活アプリ「Oshibana」に実装してみることにしました。

過去にはiOS16の新機能「ロック画面ウィジェット」に関する記事も執筆していますので、そちらもご一読ください。

iOS16の新機能「ロック画面ウィジェット」を推し活アプリに導入してみた話
https://techblog.booklista.co.jp/entry/2022/09/13/110000

スタンバイについて

スタンバイとは、スマホを横向きに置くことでウィジェットを常時表示させておくことのできる機能のことです。
ウィジェットは小サイズのものが2つ配置できるようになっており、カレンダーウィジェットを表示させれば卓上カレンダーのようになり、時計ウィジェットを表示させれば置き時計のような役割を担うことができます。
ウィジェットは小サイズであれば種類を問わず表示させることができ、もちろんOshibanaで実装されているウィジェットも全て配置できます。

スタンバイ1

使い方は以下の通りです。

  • 設定アプリからスタンバイを選択し、「スタンバイ」をONにする
  • 充電中のスマホをロック状態にし、横向きに立てる(スマホスタンドがあると一番良いです)
  • しばらくするとスタンバイの画面になる
  • 表示されているウィジェット部分を長押しすると、ウィジェットを変更できる
  • ウィジェット変更画面の左上の+ボタンを押すことでウィジェットを追加できる

スタンバイ2
スタンバイ3

スタンバイの実装方法

スタンバイの実装方法ですが、実は特別なコーディングを行う必要はありません。
iOS17のSDKをサポートしているXcode15以降でビルドすれば使用可能になります。

ただし、iOS17ではウィジェットのViewの仕様にいくつか変更点があり、Xcode15でビルドすると必然的に仕様変更の影響を受けます。
今回スタンバイを使用するにあたり、iOS16以下の端末でOshibanaのウィジェットが今まで通り動くようにするため、いくつかソースを修正する必要がありました。
※Xcode14以前でビルドしていればiOS16の互換性でウィジェットが動きますが、スタンバイにアプリが表示されないため、Xcode15へのバージョンアップが必須です。

まず、ウィジェットがiPhoneのホーム画面以外の様々な場所へ配置できるようになったことで、ウィジェットの背景を各画面の背景に合わせる指定が必要になりました。
よって、ビューの背景に関する定義であるcontainerBackground(_:for:))を追記する必要があります。

今回は特に背景の指定はないため、以下のように記述します。

var body: some WidgetConfiguration {
    return IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
        return oshibana_widgetEntryView(entry: entry, widgetSize: .SMALL)
            .containerBackground(for: .widget) {
                EmptyView()
            }
    }
}

これの指定がないと、ビルドは通るのですが、以下のようなエラーがウィジェット上に表示されて動かないという事象が起こります。

スタンバイ4

更に、iOS17からウィジェットにマージンが追加され、元々のレイアウトが崩れてしまうという事象も発生します。

スタンバイ5

余分なマージンを取り除くには、以下のようにcontentMarginsDisabled())を指定し、デフォルトのコンテンツマージンを無効にする必要があります。

var body: some WidgetConfiguration {
    return IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
        return oshibana_widgetEntryView(entry: entry, widgetSize: .SMALL)
    }
    .contentMarginsDisabled()
}

これらはiOS17固有の事象のため、#available(iOS 17.0, *)でiOS16以前と動きを分ける必要があります。

修正したソースの全体像は以下の通りです。

struct oshibana_widgetSmall: Widget {
    let kind: String = "oshibana_widget_small"
    var body: some WidgetConfiguration {
        if #available(iOS 17.0, *) {
            return IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
                return oshibana_widgetEntryView(entry: entry, widgetSize: .SMALL)
                    .containerBackground(for: .widget) {
                        EmptyView()
                    }
            }
            .configurationDisplayName("小サイズ")
            .description("設置したいウィジェットを選択しましょう")
            .supportedFamilies([.systemSmall])
            .contentMarginsDisabled()
        } else {
            return IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
                return oshibana_widgetEntryView(entry: entry, widgetSize: .SMALL)
            }
            .configurationDisplayName("小サイズ")
            .description("設置したいウィジェットを選択しましょう")
            .supportedFamilies([.systemSmall])
        }
    }
}

これらに関してはWWDC2023のBring widgets to new placesというセッションでも詳しく解説されています。
https://developer.apple.com/videos/play/wwdc2023/10027/

感想

今回Oshibanaにスタンバイを実装してみましたが、思った以上に簡単に追加できました。
Oshibanaは推し活アプリであるため、常に推しに関するウィジェットが画面に表示されていることの需要も高いと感じています。
Appleとしても、iPhoneのホーム画面以外にもウィジェットを表示させる対応を実施し続けていることから、ユーザーがより長くウィジェットに触れることを重要視していることがうかがえます。
今後も様々な方法でユーザーに情報を通達する手段が確立されていくと思われるので、新しい機能が発表されたら積極的に活用していけるよう心がけたいです。