TECH PLAY

株式会社エブリー

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

385

はじめに こんにちは。去年12月に入社したリテールハブ開発部エンジニアの清水です。 エブリーでは事業譲渡という形で他社が開発した小売店様向けシステムを引き継いで運用を行なっており、私は入社してからこちらのシステムの保守運用を担当しております。 このシステムはAWS上で稼働しており、数年間運用されているのですが、最近セキュリティ設定を変更して許可されていた通信をブロックする対応が必要となりました。 今回設定変更を行う通信は金銭に関わる処理が行われているため、必要な通信をブロックしてしまった場合大変なことになります。 このような状況ですので、どうすれば絶対に問題ないことを確認できるのか?ということについて少し悩みました。 今回はVPCフローログを使用することで通信ログを取得し、どのような通信が行われているのかを確認する流れを紹介いたします。 今回発生したセキュリティ設定を変更するタスクについて 私が担当しているシステムでは以下のように専用線を使用して小売店様で管理しているネットワークと接続しています。 小売店様のネットワークには様々なサーバーが存在しておりますが、私たちが運用しているシステムはXXX.XXX.XXX.0/24との通信しか行っていません。 しかしながら、最近小売店のご担当者様がネットワーク設定を確認したところ、XXX.XXX.XXX.0/24以外に対する通信が行われた場合でもブロックされない設定になっていることがわかりました。 小売店様側のネットワークでXXX.XXX.XXX.0/24以外に対する通信はブロックする設定を追加する必要があるため、閉じてしまって問題ないことをエブリー側で確認しなければなりません。 どうやって確認するか まず最初に確認したこととして、事業譲渡の際に引き継がれたネットワーク仕様書を確認しました。ネットワーク仕様書を見る限りではXXX.XXX.XXX.0/24以外に対する通信が行われていないことが確認できます。 しかしながら、今回設定変更する通信は金銭に関わるものが含まれており、もしもの話として「実はブロックしてはいけないものでした」という事態になった場合は大変なことになってしまいます。 ネットワーク仕様書の内容に抜けがある可能性もありますし、絶対に問題ないことを確認できなければ安心できません。 そこで、今回はVPCフローログを使用することで一定期間の通信ログを取得し、どのような通信が行われているのかを確認することにしました。 VPCフローログとは https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/flow-logs.html VPCフローログは、AWSのネットワーク環境におけるトラフィックの詳細なログを取得できる機能です。これにより、VPC内の通信状況を把握し、ネットワークのトラブルシューティングやセキュリティ監視を効率的に行うことができます。 VPC全体、特定のサブネット、もしくは個々のネットワークデバイス(ENI: Elastic Network Interface)に対して設定することが可能です。 取得したログは、出力先としてAmazon S3、CloudWatch Logs、Kinesis Data Firehoseに送信することができます。 今回はサーバーが稼働しているサブネットに対して取得する設定を行い、最も低いコストで済ませたいためAmazon S3にログを保存することにしました。 VPCフローログの設定方法 VPCサブネットに「フローログ」というタブがあるので、そこから「フローログを作成」を押下することで作成画面に遷移します 出力先となるAmazon S3バケット、ログレコード形式などの設定を行って最下部の「フローログを作成」を押下することでフローログが作成されます VPCフローログで出力される内容 Amazon S3バケットに以下のように.gz形式でファイルが出力されます。 ログの内容を確認する 様々な分析を行う場合Athenaが適していると思いますが、今回は簡易な作業で済むため、ローカルにログファイルをダウンロードして確認する形で進めました。 以下のように送信元IPアドレス(srcaddr)と送信先IPアドレス(dstaddr)が記録されています。 この中に小売店様のネットワーク(xxx.xxx.0.0/16)に含まれているものの想定したネットワーク(xxx.xxx.xxx.0/24)以外への通信がないことを確認できれば、想定外の通信は発生していないと判断することができます。 version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status 2 999999999999 eni-XXXXXXXXXXX yyy.yyy.yyy.yyy xxx.xxx.xxx.1 YYYYY XXXXX 6 17 1479 1737356974 1737357004 ACCEPT OK 各フィールドにどのような値が入っているかはこちらをご覧ください。 https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/flow-log-records.html#flow-logs-fields 料金について VPCフローログ自体には料金は発生しませんが、出力先であるAmazon S3に対する料金が発生します。 目安として、本サービスはMAU10万人前後であり、約24時間で合計45.2MBのファイルが出力されることがわかりました。 サービスによって通信量は大きく変わるものなので一概に問題ないと言うことはできませんが、コスト面についてはそこまで心配する必要はない手段だと感じました。 最後に このような許可されていた通信をブロックしなければならないという不安になるような経験は数年に1回あるかないかだと思いますが、多くのエンジニアが一生のうち一度くらいは経験するものではないでしょうか。 VPCフローログを使用することで手軽に通信内容を確認することができますので、似たような状況になったときに本記事がお役に立てば幸いです。
アバター
はじめに こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。 近年は Web のサービスに限らず、アプリでもネットワーク接続を実施することが当たり前になってきていますが、皆さんはネットワーク接続をするアプリでは必須となる タイムスタンプ について実装方法や管理方法を意識されたことはあるでしょうか? タイムスタンプを使用するケースは多く、例えば ログイン情報を保存する機能で、ログインを実施した日時を管理する ワンタイムパスワードを発行する機能で、発行された日時や有効期限を管理する スケジュールを管理する機能で、スケジュールが実行される日時を管理する (プログラムのロジックで) 一定時間経過後に初めてポップアップを出す場合に、基準となる時間を管理する のように多種多様な使われ方をしており、おそらく 1 つもプログラム上でタイムスタンプを使用していないアプリやサービスは無いかと思っています。 それほど当たり前のように使われているタイムスタンプですが、管理方法や発行手順を間違えると重大なバグに繋がることがあるため、今回は タイムスタンプをどう正しく管理すべきか という観点で 運用方法についてまとめてみたいと思います。 なぜタイムスタンプを正しく管理する必要があるのか タイムスタンプが正しく運用されていない場合の問題点を考えてみたいと思います。 まずはセキュリティ (不正利用防止) の観点。 例えば毎日ログインをする度にインセンティブを付与するようなアプリで、端末の時間だけでタイムスタンプを管理していた場合、端末の設定を 1 日後にずらしてアプリを再起動を繰り返すだけで インセンティブを簡単に取得できてしまいます。 もし金銭に関わるもの (ポイントなど) を付与していた場合、ポイントを取得して交換、そしてアプリを再インストールするといういわゆるリセマラをされると、企業としては大幅な損失に繋がる可能性が出てしまいます。 そして次に整合性の観点。 行動ログを管理 (分析) する機能にて、アプリが端末の時間で生成したタイムスタンプを正として保存する仕組みとなっており、 行動 A : 現実世界の 10:00 に実施 / 端末の時間も 10:00 行動 B : 現実世界の 11:00 に実施 / 端末の時間も 11:00 行動 C : 現実世界の 12:00 に実施 / 端末の時間が不具合で 10:30 となっていた という操作が行われた場合、期待としては A → B → C という順で保存されていてほしいものの、実際には A → C → B と保存されており、ログの整合性が担保されなくなります。 最後に正確性の観点。 データをタイムスタンプとともに記録するようなアプリで、タイムゾーンを考慮しておらずアプリは画面によって管理が異なる、サーバは協定世界時 (UTC) で管理していた場合、サーバで UTC 12:00 で管理されたデータを取得すると 画面 A (端末の設定に依存、今回は例としてアメリカ中部標準時の CST に変換) : 6:00 と表示 画面 B (日本標準時の JST に変換) : 21:00 として表示 となり画面によって表示がズレてしまい、正確性が担保されなくなります。 上記では表示するデータという比較的気づきやすい例を書きましたが、内部ロジックの場合、常に同じ人員・開発環境で開発を行っていると気づかずに予期せぬ不具合に繋がることがあります。 複数例を挙げましたが、他にもタイムスタンプを正しく運用しないことで発生する問題は多数存在するため、如何に正確に改善されることなく運用できるかがアプリやサービスの質や信頼性に繋がるものだと考えています。 タイムスタンプの管理方法 設計段階でやることとしては、 扱う時刻のタイムゾーンは何にするか を決めることが重要かと思います。 先述したタイムゾーンを考慮していない問題については、設計段階で明確に取り決めが行われなかった (サーバエンジニア、アプリエンジニアで認識がずれていた) 事が要因のため、 サービスとして取り扱うタイムスタンプのタイムゾーンは一貫してこれ、という取り決めが必要になります。 例えば サービスとして記録、及び通信データとしてやり取りする全てのタイムスタンプは UTC として取り扱う アプリ上で表示に使用する場合は端末で設定されたタイムゾーンに変換して取り扱う と決めておけば、先述の正確性の問題は回避できるようになります。 そして、サーバと通信を行うアプリ・サービスであれば サーバの時刻を正とする 事が重要かと思います。 複数のトランザクションを制御する際にアプリが生成した時刻を正として信じてしまうと、先述した通り不正利用、整合性の欠如に繋がるため、時刻を管理する機能は一箇所に集約する必要があります。 上記でサーバがデータを管理するアプリについては正しくタイムスタンプを管理できますが、サーバがメンテナンスなどで使用できず、アプリ単体でタイムスタンプを管理しなければならないケースではどうでしょうか。 次の章では Android アプリ単体で正確にタイムスタンプを管理する方法を考えてみたいと思います。 Android で正確にタイムスタンプを管理するには 端末の設定で日時の自動設定が ON になっていれば一定の正確性は担保できますが、やはり端末の設定を弄られる可能性があるため確実ではありません。 このようなケースでは TrustedTime API というものを使用すると問題を回避できます。 ◯公式情報 https://developers.google.com/android/reference/com/google/android/gms/time/TrustedTime https://android-developers.googleblog.com/2025/02/trustedtime-api-introducing-reliable-approach-to-time-keeping-for-apps.html こちらを使用するとネットワーク接続自体は必要なものの、Google が提供するインフラストラクチャにアクセスし、正確なタイムスタンプが取得できるようになります。 簡単ではありますが、図で表すとこの様になります。 毎回 Google が提供するインフラストラクチャにアクセスせず、TrustedTime のロジックで時間を算出することでネットワーク使用量を削減。 またデバイスの負荷状況などで正確な値を算出できなくなる場合は、TrustedTime API を通じてアプリ側に通知を出せる仕組みとなっています。 以降で TrustedTime API を使用してタイムスタンプを取得する実装方法をまとめてみたいと思います。 実装方法 1. app レベルの build.gradle にライブラリを追加 dependencies { implementation("com.google.android.gms:play-services-time:16.0.1") } 2. TrustedTime API を実行 fun execute(context: Context) { val trustedTimeClient = TrustedTime.createClient(context) trustedTimeClient.addOnCompleteListener { task -> if (task.isSuccessful) { val client = task.result client.computeCurrentUnixEpochMillis()?.also { timeMillis -> val date = Date(timeMillis) val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPAN) Log.i("TestLog", "time : ${format.format(date)}") } } else { Log.w("TestLog", "error : ${task.exception?.message}") } } } TrustedTime.createClient では TrustedTimeClient のインスタンスを提供する Task が取得できます。 Task により非同期処理が開始されるため、addOnCompleteListener で結果を受け取るリスナーを定義します。 後は task.isSuccessful (タスクが成功した場合) にタイムスタンプを取り扱うのみとなります。 上記の実装例では、computeCurrentUnixEpochMillis で Unix エポックからの経過時間をミリ秒で取得しており、日本時間に変換してログ出力しています。 実際に実行し、ログを出力した結果は以下のようになります。 TrustedTime API は端末起動後に一度だけネットワーク接続が必要なものの、以降はネットワーク接続が無い場合でも内部のロジックで正確なタイムスタンプを返却してくれます。 実際に端末を機内モードにしてネットワーク接続を OFF、かつ端末の時間を数日ずらした状態でも正確なタイムスタンプが返却されました。 実装は以上となるため、非常に簡単に正確なタイムスタンプを管理する方法を実装できました。 注意点 TrustedTime API は正確なタイムスタンプを取り扱う上で有効な手段ではありますが、いくつか注意点があります。 端末起動後に一度もネットワーク接続を行っていない場合はタイムスタンプを取得できない 端末・Google サーバ間の通信経路が完全に安全ではない Google のサーバが改ざんされている場合は不正なタイムスタンプが返却される 端末がルート化され、かつ TrustedTime API の動作が改変される可能性がある Android 5 (Lolipop) 以降のみで使用可能 などが挙げられます。 ただし、上記で挙げられるような問題点は TrustedTime API の話に限らず、アプリと自社のアプリサーバでやり取りするケース、別の API を使用するケースでも発生しうる問題のため サービスとしてどこまでの品質を担保するか、どこまでの問題を許容するかを検討したうえで適切な案を採用する形が良いかと考えます。 まとめ 今回はタイムスタンプを扱う上での注意点や実装案などをまとめてみましたが、意外と細かいタイムゾーンの認識ズレ、実装ミスが発覚するケースは私自身も過去何度か遭遇しているため 設計の段階から積極的なコミュニケーションは必要不可欠だと思いました。 またタイムスタンプを正確に取り扱う手法としては、TrustedTime API は実装・学習コストが低く、非常に有効かと感じました。 本記事の情報が皆様のお役に立てれば幸いです。
アバター
はじめに こんにちは、開発本部のデータ&AIチームの24新卒の蜜澤です。 エブリーに入社してからもうすぐ1年が経つので、この1年間を振り返りたいと思います。 文字ばかりのポエムですが最後まで読んでいただけると嬉しいです! 入社前 まず、入社する前の私の状況について触れたいと思います。 大学ではデータサイエンス学部に所属しており、主に統計学・機械学習について学んでいました。 いわゆるコンピューターサイエンスのようなことは学んだことはなく、Webやアプリの開発経験もなく、触れたことがある言語はR・Pythonの2つのみで、データサイエンティストにとって必須とも言えるSQLも触れたことがありませんでした。 大学院には行かずに学部卒でエブリーに入社しました。 インターン 大学4年生の8月に3週間ほどと2~3月に内定者インターンをしました。 前述の通りSQLは全く触れたことがなかったので、8月のインターンでキャッチアップし、基本的なクエリは書けるようになりました。 インターン中はデリッシュキッチンのデータ分析やレシピレコメンドを行いました。 入社後1~2週間 入社1週目は全体研修を受け、2週目は今年度から新設された エンジニア新卒研修 を受けました。 エンジニア新卒研修の1週間が間違いなく今年1番大変な1週間だったと思います(笑) この研修では、エンジニアとしてのマインド研修と、バックエンド、インフラ、モバイル、Web、データについて講義とハンズオンがある研修を受けました。 前述の通り、エンジニアとしての基礎素養すらない状態だったので、 バックエンドとインフラ何が違うの? EC2?何それ美味しいの? <>なんすかこれ?htmlタグ、、?知らんな〜 というような状況で、今思うとあまりにも無知すぎると思いますが、研修担当者はこんな状態の奴がいるなんて想定はしていないので、研修では EC2立ててみよう Webの画面作成してみよう 画面遷移できるようにしてみよう などの未知との遭遇のような研修内容で、泣きそうになりながら毎日研修を頑張りました! 研修後に同期に教えてもらいながら、なんとか研修の内容を消化していったのは今では良い思い出です! 4月後半~5月 エンジニア新卒研修を終えて、4月の後半からは部署での業務が始まりました。 この時期に取り組んだことは デリッシュキッチンのユーザーのデータ分析 Amazon QuickSightの検証 aws研修や外部研修といった短期の研修 の3つになります。 データ分析は、行ったこと自体はレシピのお気に入り追加に関する簡単な分析でしたが、データがどこにあるのかや、どのようにデータ分析を行うのかといった基本的なことを学べました。 Amazon QuickSight(以下quicksight)を使用して、レシピデータ分析の汎用BIツールの開発を行いました。quicksightは社内での使用実績がなかったので、一通り触ってみて、何ができて何ができないのかなどの検証から始めました。 社内で使用したことがある人が誰もいなかったので、自分が社内で1番quicksightに詳しくなってやるという気持ちで毎日quicksightと格闘しました。 1ヶ月ほどquicksightの検証をしていたら、だんだんと慣れてきて、色々なことができるようになりました。 tech.every.tv tech.every.tv 6~8月 徐々に裁量が大きくなってきたなと思う時期です。 要件を満たすようなビジュアルをquicksightで作成する 必要な中間テーブルを作成する 主に取り組んだことは上記の2つになります。 要件通りのビジュアルをquicksightで作成するというのが、この時期の一番のミッションでした。 quicksightの仕様を完璧に理解せずに、検証のためにクエリを叩きまくり、予算を超えたコストをかけてしまい注意されることもありました。 が、この経験のおかげで、早期からコストを意識した開発ができるようになったと思います! 様々な制約(実行時間やコストなど)の中で、要件を満たすために、中間テーブルの作成を検討し、ETLを組んで必要なテーブルを用意するということもできるようになりました。 必要なデータを自分で準備し、可視化の方法を考え誤解のないようなビジュアルを作成するという、データサイエンティストの基礎能力を鍛えることができた期間でした。 9~12月 この1年の山場の期間でした! Web開発 主に取り組んだのはWeb開発です。 私の所属はあくまでデータ&AIであり、本来ならばWeb開発はスコープ外とはなりますが、若いうちに色々な経験を積んだ方が良いという上司のアドバイスがあり、私自身も挑戦できる環境があるなら色々なことに挑戦したいと思っていたので、Web開発に挑戦しました。 htmlもcssもコンポーネントという概念も何も知らない状態だったので、初めに作成したものはベタ書きばかりのクソデカコンポーネントで、それを見た同期や上司のなんとも言えない表情は今でも覚えています(笑) そんな状態でしたが、同期のサポートを受けながら、毎日ゴリゴリと開発を行った結果、アトミックデザインの理解、reactの状態の理解、figmaを元にcssでデザインを作成など基本的なフロントエンド開発ができるようになりました! htmlすら知らなかった状態に比べたらかなり成長できたなと思います! フロントエンドの実装を知ることで、データのことのみを考えた実装ではなく、その後のwebのことまで考えた設計ができるようになりました。 ただ、バックエンドやインフラまでは手がまわせなかったので、今後の課題として残ります。 また、PDMやデザイナーとすり合わせを行いながら、チームで開発をしていくことの難しさも学ぶことができました。 各ポジションごとに譲れない部分があり、どこに落とし所を持っていくのが良いのかをエンジニアの視点で考えながら、ディスカッションをするのはとても難しく頭を悩ませる日々でした。 開発を進めていくうちに、適切なすり合わせタイミングや伝え方がわかってきて、大きな手戻りをせずに開発ができるようになっていきました。 1~3月 自分の甘さを実感した期間です。 バグ対応 quicksightでのビジュアル作成 この2つが主に取り組んだことです。 なんといっても、自分が作成したETLが原因でバグが起きてしまい、その対応をし、根本対応策を考えるというのが、1番大きなタスクでした。 バグが起きたことで、自分の設計の甘さを実感するとともに、再発防止策を考えるのはとても学びになりました。 また、データ関連のバグの原因調査をどのように進めていけば、早期に原因を発見できるのかも学べました。 まとめ 1年を振り返ってみて、エンジニア素養ほぼ0の状態から、ETL作成やフロントエンド開発ができるようになったということでかなり成長できた1年ではないかと思います! 毎日の成長は微々たるものでしたが、改めて1年を振り返ってみたことで、毎日コツコツ積み重ねることの偉大さを実感できました。 とはいえ、1カ月後には優秀な新卒社員がたくさん入社してくるので、負けじとさらなる成長をとげるように日々精進していきたいと思います!
アバター
【2025春】DynamoDB Itemの一括削除を実践 背景 システムの前提条件(制限事項) 前提を要件としたコード設計 実装例 解説 read_a_values_from_file()関数について scan_table_for_latest_record()関数について get_latest_records_from_table_a()関数について delete_from_table_b()関数について main()関数について 結果 総括 参考 最後に  こんにちは、開発本部 RetailHUB開発部 NetSuperグループに所属するフルスタックエンジニアをやらせていただいています、ホーク🦅アイ👁️です。早春、3/6に「AWS Innovate: Generative AI + Data」が開催され、今後も生成AIアプリケーションと親和性の高いDynamoDBと向き合う方々が増えていくと思い本記事を書くことにしました。 背景  弊社ではネットスーパーシステムのオプションサービスとしてネイティブアプリも提供しております。そのインフラアーキテクチャの一部にDynamoDBがあります。主に、アプリ利用ユーザのログインセッションを管理しています。ごく最近、小売様の要望対応を行った結果、アプリにログイン中ユーザのセッションを削除する必要が出てきました。いくつかの解決策はありましたが対応の緊急性や工数の観点あるいは、一定数のユーザの再ログインをシームレスに行ってもらうUXのため、本記事にあるようにアプリケーションが読み書きしているDynamoDBのセッションテーブルのItemを直接削除する方法を選択しました。 システムの前提条件(制限事項)  実際に一括削除プログラムを実装をするにあたり、現状の弊社運用中DynamoDBの設定やテーブル設計について以下のことを考慮しなければなりませんでした。 アプリケーション上でセッション管理に使っている対象テーブルは2つ(仮にA=User,B=Sessionとする)存在 対象テーブル2つともPartition key(パーティションキー)はセッションIDのみ 対象テーブル2つともSort key(ソートキー)は設定なし 対象テーブル2つともItem総数は1万強 対象テーブル2つともCapacity mode(キャパシティモード)は、On-demand(オンデマンド)で起動中 テーブルAにはGlobal Secondary Index (GSI) の設定はない テーブルBにはGSIの設定がある。但し、今回無関係のAttribute(属性)にのみ設定済  また、運用面での要件は以下の通りです。 当日、システムメンテナンスを1時間実施。その時間内で作業完遂するスケジュール策定 GSI作成時にRead/Write負荷がかかり相当数の時間を要する可能性があるため普段の運用に支障をきたさないようにGSIの追加は行わない(そのための別メンテナンス時間を取る日程調整が厳しかった) 前提を要件としたコード設計  上述の前提条件を基にどのようなコード設計が必要か列挙します。 パーティションキーではない属性(ユーザID)を基に対象者を抽出するためFull Scan(フルスキャン)検索をしなければならない スキャンは、1MBの制限でPagination(ページネーション)が発生するため、LastEvaluatedKey要素が存在しなくなるまでページネーション単位で検索を繰り返す 属性:time_to_liveをNumber型で定義しているがこの値の最新UNIX Timestamp 1件のみを抽出 time_to_liveはソートキーではないので ScanIndexForward=false によるORDER BY DESCを使えないので最新かどうかはプログラム側で判定 boto3のリトライ機構はクライアントセッション全体のThrottling(スロットリング)には対応しているがbatch_write_item()関数のUnprocessedItemsには非対応なので自前でリトライ機構の実装が必要 リトライ機構として、Exponential Backoff(指数バックオフ)+Jitter(ランダム遅延)を採用 実装例 deleteUserSession.py # usage: python deleteUserSession.py <profile> <file> # <profile>: AWS CLIのprofile名 # <file>: 複数userIdが1行に1つずつ列挙されたファイル import boto3 import time import random import traceback import sys from collections import defaultdict # コマンドライン引数からprofile[第1引数]、ファイル名[第2引数]を取得 args = sys.argv # DynamoDBクライアント my_session = boto3.Session(profile_name=args[ 1 ]) dynamodb = my_session.client( 'dynamodb' ) # テーブル名 table_a_name = "User" table_b_name = "Session" # 外部ファイル(plain text)のパス INPUT_FILE_PATH = args[ 2 ] # ファイルから `userId` の値を読み込む def read_a_values_from_file (): with open (INPUT_FILE_PATH, "r" ) as file : return [line.strip() for line in file .readlines() if line.strip()] # 指定 `userId` の最新Itemをフルスキャン検索して取得 def scan_table_for_latest_record (a_value): latest_record = None last_evaluated_key = None # ページネーション用 while True : # Scan 実行(last_evaluated_key がある場合のみ渡す) scan_params = { "TableName" : table_a_name, "FilterExpression" : "userId = :a_value" , "ExpressionAttributeValues" : { ":a_value" : { "S" : a_value}} } if last_evaluated_key: scan_params[ "ExclusiveStartKey" ] = last_evaluated_key # None の場合は追加しない response = dynamodb.scan(**scan_params) # 取得データの中から最新の `time_to_live` を持つItemを探す for item in response.get( "Items" , []): if latest_record is None or int (item[ "time_to_live" ][ "N" ]) > int (latest_record[ "time_to_live" ][ "N" ]): latest_record = item # 最新の `time_to_live` を持つItemを更新 # ページネーションのチェック last_evaluated_key = response.get( "LastEvaluatedKey" ) if not last_evaluated_key: break # 次のページがなければ終了 return latest_record # Userテーブルから `userId` の値を元に最新Itemを取得 def get_latest_records_from_table_a (a_values): items_by_a = {} for a_value in a_values: latest_record = scan_table_for_latest_record(a_value) if latest_record: items_by_a[a_value] = latest_record[ "sessionId" ][ "S" ] # `sessionId` の値を保存 return list (items_by_a.values()) # Sessionテーブルの検索 & 削除 def delete_from_table_b (b_values): items_to_delete = [] # Sessionテーブルから `sessionId` の値で検索して該当Itemを取得 for b_value in b_values: response = dynamodb.query( TableName=table_b_name, KeyConditionExpression= "sessionId = :b_value" , ExpressionAttributeValues={ ":b_value" : { "S" : b_value}} ) items_to_delete.extend(response[ 'Items' ]) # 取得したItemをBatchWriteItemで削除 deleted_count = 0 batch_size = 25 for i in range ( 0 , len (items_to_delete), batch_size): batch = items_to_delete[i:i + batch_size] request_items = { table_b_name: [{ 'DeleteRequest' : { 'Key' : { 'sessionId' : item[ 'sessionId' ]}}} for item in batch] } response = dynamodb.batch_write_item(RequestItems=request_items) # 未処理のItemがあればリトライ retry_count = 0 while response.get( 'UnprocessedItems' ): retry_count += 1 wait_time = min ( 2 ** retry_count + random.uniform( 0 , 1 ), 60 ) # 指数バックオフ+ランダム遅延 time.sleep(wait_time) response = dynamodb.batch_write_item(RequestItems=response[ 'UnprocessedItems' ]) deleted_count += len (batch) print (f "経過:{deleted_count} 件を削除しました" ) return deleted_count def main (): try : # 外部ファイルから検索対象の `userId` のリストを取得 a_values = read_a_values_from_file() if not a_values: print ( "外部ファイルに検索対象がありません" ) return # Userテーブルから最新の `sessionId` 値を取得 b_values = get_latest_records_from_table_a(a_values) if not b_values: print ( "Userテーブルに該当するItemが見つかりませんでした" ) return # b_valuesの要素数を表示 print (f "対象者数: {b_values.__len__()}" ) # SessionテーブルのItem一括削除 deleted_count = 0 deleted_count = delete_from_table_b(b_values) print (f "総数:{deleted_count} 件を削除しました" ) except Exception as e: print (traceback.format_exc()) if __name__ == "__main__" : main() 解説 read_a_values_from_file()関数について # ファイルから `userId` の値を読み込む def read_a_values_from_file (): with open (INPUT_FILE_PATH, "r" ) as file : return [line.strip() for line in file .readlines() if line.strip()]  以下のようなフォーマットのテキストファイルを1行ずつ読み込みユーザIDのリストを返します。 userIdList.txt 100001 100019 100223 ... scan_table_for_latest_record()関数について # 指定 `userId` の最新Itemをフルスキャン検索して取得 def scan_table_for_latest_record (a_value): latest_record = None last_evaluated_key = None # ページネーション用 while True : # Scan 実行(last_evaluated_key がある場合のみ渡す) scan_params = { "TableName" : table_a_name, "FilterExpression" : "userId = :a_value" , "ExpressionAttributeValues" : { ":a_value" : { "S" : a_value}} } if last_evaluated_key: scan_params[ "ExclusiveStartKey" ] = last_evaluated_key # None の場合は追加しない response = dynamodb.scan(**scan_params) # 取得データの中から最新の `time_to_live` を持つItemを探す for item in response.get( "Items" , []): if latest_record is None or int (item[ "time_to_live" ][ "N" ]) > int (latest_record[ "time_to_live" ][ "N" ]): latest_record = item # 最新の `time_to_live` を持つItemを更新 # ページネーションのチェック last_evaluated_key = response.get( "LastEvaluatedKey" ) if not last_evaluated_key: break # 次のページがなければ終了 return latest_record  LastEvaluatedKeyが存在する限りwhileループを続けてページネーション毎にItemのtime_to_live属性値が最新(=最大)のものに更新し続けます。最後まで到達した後に最新1件のItemだけを返します。 get_latest_records_from_table_a()関数について # Userテーブルから `userId` の値を元に最新Itemを取得 def get_latest_records_from_table_a (a_values): items_by_a = {} for a_value in a_values: latest_record = scan_table_for_latest_record(a_value) if latest_record: items_by_a[a_value] = latest_record[ "sessionId" ][ "S" ] # `sessionId` の値を保存 return list (items_by_a.values())  対象リストのユーザID1つずつスキャンして最新1件のItemをリストに格納し、ユーザID数繰り返します。最後にItemのセッションIDリストを返します。 delete_from_table_b()関数について # Sessionテーブルの検索 & 削除 def delete_from_table_b (b_values): items_to_delete = [] # Sessionテーブルから `sessionId` の値で検索して該当Itemを取得 for b_value in b_values: response = dynamodb.query( TableName=table_b_name, KeyConditionExpression= "sessionId = :b_value" , ExpressionAttributeValues={ ":b_value" : { "S" : b_value}} ) items_to_delete.extend(response[ 'Items' ])  まず、セッションIDがSessionテーブルのパーティションキー属性になっているのでQuery(クエリ)検索を使って高速処理することができます。 # 取得したItemをBatchWriteItemで削除 deleted_count = 0 batch_size = 25 for i in range ( 0 , len (items_to_delete), batch_size): batch = items_to_delete[i:i + batch_size] request_items = { table_b_name: [{ 'DeleteRequest' : { 'Key' : { 'sessionId' : item[ 'sessionId' ]}}} for item in batch] } response = dynamodb.batch_write_item(RequestItems=request_items)  次に、全て取得した対象Itemリストをbatch_write_item()関数で25件(AWS側の仕様制限)を1単位として繰り返し一括削除していきます。 # 未処理のItemがあればリトライ retry_count = 0 while response.get( 'UnprocessedItems' ): retry_count += 1 wait_time = min ( 2 ** retry_count + random.uniform( 0 , 1 ), 60 ) # 指数バックオフ+ランダム遅延 time.sleep(wait_time) response = dynamodb.batch_write_item(RequestItems=response[ 'UnprocessedItems' ]) deleted_count += len (batch) print (f "経過:{deleted_count} 件を削除しました" )  その後、削除処理に失敗したUnprocessedItemsが存在する限り、削除処理をリトライし続けます。リトライ間隔を指数バックオフ+ランダム遅延により徐々に長くしていくことでリトライの成功率を上げる試みをしています。最後に、削除した件数を返します。 main()関数について def main (): try : # 外部ファイルから検索対象の `userId` のリストを取得 a_values = read_a_values_from_file() if not a_values: print ( "外部ファイルに検索対象がありません" ) return # Userテーブルから最新の `sessionId` 値を取得 b_values = get_latest_records_from_table_a(a_values) if not b_values: print ( "Userテーブルに該当するItemが見つかりませんでした" ) return # b_valuesの要素数を表示 print (f "対象者数: {b_values.__len__()}" ) # SessionテーブルのItem一括削除 deleted_count = 0 deleted_count = delete_from_table_b(b_values) print (f "総数:{deleted_count} 件を削除しました" ) except Exception as e: print (traceback.format_exc()) ユーザIDファイル読み込み Userテーブルから削除対象ユーザのセッションIDを検索 対象者数を出力 Sessionテーブルから対象セッションを検索して削除実行 削除Item数を出力 1−5の処理途中で例外が発生した場合は、トレース出力 終了 結果 検索ユーザ数:577名 対象者数:139名 削除Item総数:139件 Executed in usr time sys time 138.78 secs 14.20 secs 1.55 secs 総括  本記事では、DynamoDBのセッション管理テーブルの大量のItemを一括に削除する方法としてAWS SDK for Pythonで実装したプログラムを紹介しました。本番環境の実Item群を削除処理した結果、スロットリングは発生せずに全て成功するに至りました。今回は、既存の設計に変更を加えることなく運用中のテーブルにおいてなるべく影響を及ばさないためにBDIを追加せずにフルスキャン検索しましたが、もっと大量のItemが格納されている場合には処理の高速化が死活問題になる可能性はありますので、皆様のケースに合わせて柔軟に対応いただければと思います。  また、DynamoDBはPartiQLをサポートしています。簡単な問い合わせならば従来のSQLライクな構文で実行可能という意味で便利です。今回のケースにおいてはパーティションキー、ソートキー、インデックスが設定されていない属性に対しては制限されてしまうため使えませんでしたが、キーの設定がなされているケースであればなお有効であると思います。  今回は一時的な対応のみでしたのでCLIとして実装してローカルマシンからAWSアカウントにログインして実行しましたが、AWS Lambda+EventBridgeを使って定期的に古い使われなくなったItemを削除するパターンにも有効かと思います。  これにて本記事の結びとさせていただきます。 参考 Exponential Backoff And Jitter GitHub: botocore retry 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
アバター
はじめに こんにちは、リテールハブ開発部でバックエンドエンジニアをしています。 現在、小売アプリの開発でLaravel11を利用してAPI開発を行っています。 先日2月24日にLaravel12がリリースされました。 ( https://laravel.com/docs/12.x/releases ) 今回のリリースは、比較的マイナー変更中心の「メンテナンスリリース」となっているようです。 Laravel11は昨年の夏頃から使用しているのですが、まだまだ最新バージョンの認識で直近でバージョンアップを行う予定もありませんでした。 ただ、以下のサポート対応表を見ると、Laravel11でもあと1年程度でサポート対象外となることがわかります。 公式ではLaravelの方針として12ヶ月ごとにメジャーアップデートしていきたいとのことなので、 今後を考慮して、定期的にバージョンアップできるような体制にしておいた方が良さそうだと思いました。 そこで今回、Laravel11からLaravel12にバージョンアップすることでどんな変更点があるのか、 私の開発している環境ではどんな影響がありそうかをコメントしながらご紹介したいと思います。 各変更点に関するコメントについては、環境によっては一部内容が異なることもあると思いますので、参考程度にご覧いただければ幸いです。 Laravel12のバージョンアップ方法について Laravel11からのバージョンアップは非常に簡単で、 アプリケーションの「composer.json」ファイルのバージョンを更新することでバージョンアップできます。 "laravel/framework": "^12.0" "phpunit/phpunit": "^11.0" "pestphp/pest": "^3.0" Laravel12の変更点について Laravel11からLaravel12にすることによる変更点を公式で記載されている内容に沿って順番にご紹介したいと思います。 全体的に大きな変更点は控えめで、バージョンアップによるコード修正の影響も少ない気はしています。 Carbon2.xの終了 Carbon2.xのサポートが終了しました。 Laravel12では、Carbon3.xを使用する必要があります。 Laravel11ですでにCarbon3.xを使用しているのであれば特に今回の変更による影響はなさそうです。 Laravelインストーラー、スターターキットの更新 LaravelインストーラーCLIツールを使用して新しいLaravelアプリケーションを作成する場合は、 インストーラーを更新して、Laravel12、新しいLaravelスターターキットと互換性を持たせる必要があります。 新しいインストーラーを使用するとReact、Vue、Livewireのスターターキットを選択できるようで、 プロジェクトによってはこれを利用することで初期構築がしやすくなるかもしれません。 私の環境ではこちらを利用する機会は無さそうですが、この部分が今回のバージョンアップによる大きな変更点でもあるようです。 Concurrencyの利便性向上 並列処理で使用するメソッドとして、Concurrencyメソッドがありますが、 連想配列を使用してメソッドを呼び出すと、同時操作の結果が関連付けられたキーとともに返されるようになりました。 バージョンアップ前 $result = Concurrency::run([ fn () = > 1 + 1, fn () = > 2 + 2, ]); // $result = > [ 2, 4 ] バージョンアップ後 $result = Concurrency::run([ 'task-1' => fn () => 1 + 1, 'task-2' => fn () => 2 + 2, ]);   // $result => ['task-1' => 2, 'task-2' => 4] まだConcurrencyを使用した並列処理はサービス上に実装したことはないのですが、 今回の変更でキーも返却されるようになったので、その後の処理は順番を意識せず使えるようになります。 返却順をもとに行なっている処理があれば、今回のタイミングでキー指定による処理に変えられればより安全な処理になりそうです。 Databaseの変更点 データベース処理に関する変更ではなく、定義確認関連の変更点のようでした。 そのため通常のサービスで使用するような変更というよりは、メンテナンスやDB管理で使用するものになりそうです。 Schema::getTablesの変更 // テーブルのスキーマ情報をすべて取得する $tables = Schema::getTables();   // スキーマ「main」に関するスキーマ情報を取得する $table = Schema::getTables(schema: 'main');   // スキーマ「main」「blog」に関するスキーマ情報を取得する(複数指定) $table = Schema::getTables(schema: ['main', 'blog']); デフォルトですべてのスキーマの結果が含まれるようになりました。 以前のバージョンでは引数によるスキーマ選択ができませんでしたが、 上記のように引数を渡すと、指定されたスキーマの結果のみを取得できるようになりました。 Schema::getTableListing()の変更 $tables = Schema::getTableListing(); // $tables => ['main.migrations', 'main.users', 'blog.posts']   $table = Schema::getTableListing(schema: 'main'); // $tables => ['main.migrations', 'main.users']   $table = Schema::getTableListing(schema: 'main', schemaQualified: false); // $tables => ['migrations', 'users'] Schema::getTableListing()メソッドは、デフォルトでスキーマ修飾されたテーブル名を返すようになりました。 schemaQualified引数を渡すことで、スキーマ修飾なしの一覧を取得することもできます。 db:table、db:showコマンドの変更 PostgreSQLやSQL Serverと同様に、MySQL、MariaDB、SQLiteのすべてのスキーマの結果を出力するようになりました。 主に定義関連の参照が少し便利になった程度でデータベース操作に関する変更ではないので、私の環境ではコード修正の影響は無さそうです。 ただ、すでに使用しているメソッドである場合は修正が必要かもしれません。 Eloquentの変更点 Eloquentの変更点も非常に少なく、HasUuidsトレイトの変更がある程度です。 HasUuidsトレイトは、UUID仕様のバージョン7(順序付き UUID)と互換性のあるUUIDを返すようになりました。 モデルのIDに順序付きUUIDv4文字列を引き続き使用したい場合は、以下のようにHasVersion4Uuidsトレイトを使用する必要があります。 use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; HasVersion7Uuidsは削除されました。今まで使用していた場合は、HasUuidsに変更する必要がありそうです。 新しく使用する場合はHasUuidsトレイトをそのまま使用すれば大丈夫そうです。 ただ、すでにHasUuidsトレイトを使用した処理がある場合は、個人的には問題ないと思っていますが、 影響という意味では非常にインパクトはある気はしていますので、バージョンアップ後に問題なく動作するかは念の為検証した方が良さそうです。 Requestの変更点 Requestでは、mergeIfMissingメソッドについて変更がありました。 mergeIfMissingメソッドはリクエストの入力データ内に対応するキーがまだ存在しない場合のみマージしたい時に使用します。 Laravel12からは、ネストされた配列データを「ドット」表記を使用して結合できるようになりました。 $request->mergeIfMissing([ 'shop.shop_name' => 'Sample Shop', ]); 実は上記のメソッドのようにデフォルト値として使用したいケースはあったのですが、今までは以下のような処理で行っていました。 $shop = $request->input('shop', []); if (!isset($shop['shop_name'])) { $shop['shop_name'] = 'Sample Shop'; } $request->merge(['shop' => $shop]); 今回のバージョンアップ変更の調査で、mergeIfMissingメソッドを使用した方がシンプルに記載できることを知ったので、現状の実装含め導入を検討したいと思いました。 Validationの変更点 Validationでは、画像形式のデフォルト指定に関して変更がありました。 Laravel12からはimageバリデーションルールがデフォルトでsvg画像を許可しなくなりました。 svg画像を許可するためには、バリデーションルールに明示的に指定が必要になります。 imageにallow_svgを指定する方法 'photo' => 'required|image:allow_svg' File::imageに指定する方法 use Illuminate\Validation\Rules\File; 'photo' => ['required', File::image(allowSvg: true)], 私の環境ではsvg画像を使用していないため影響はありませんが、 デフォルト指定かつsvg画像のバリデーションチェックをしていた場合は、明示的にルールの指定を追加する必要がありそうです。 最後に いかがでしたでしょうか。 実際に変更点を見たところでも、今回のメジャーアップデートによる大きな影響は全体的に控えめかなという印象です。 私のプロジェクトの開発環境もコード修正の対応はほぼ無さそうかなと思っています。 とはいえ、実際に正常に動くかは検証が必要ですし、他のライブラリとの関係などもあるため慎重に対応する必要はありそうです。 ただ、冒頭でお話した通り、あっという間にサポート対象外になるので、 良いタイミングでバージョンアップの検討はしていかないといけないと思いました。 今後のLaravelバージョンアップの際にぜひ少しでも参考にしていただければ幸いです。 最後までお読みいただき、ありがとうございました。
アバター
この記事の概要 エブリーTIMELINE開発部の内原です。 サービスを運用していると時々遭遇するOOM-Killerについて、改めて学んでみたのでまとめます。 OOM-Killerはどういう理由で発生するのか、なにが起きているのか、どう対処すればいいのか、などを解説します。 なおこの記事では、Linux上での説明を前提としています。 OOM-Killerとは サーバ上で、以下のようなメッセージを見たことがあるのではないでしょうか。 [ 2291.984774] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/amazon-ssm-agent.service,task=python3,pid=28268,uid=1001 [ 2291.988154] Out of memory: Killed process 28268 (python3) total-vm:536656kB, anon-rss:243172kB, file-rss:4kB, shmem-rss:0kB, UID:1001 pgtables:544kB oom_score_adj:0 このメッセージからは、python3プロセスがOOM-Killerによって強制終了されたことがわかります。 OOM-Killerが発生する理由 当然ながら、OOM-Killerはメモリが足りなくなったときに発生します。ただ、メモリが足りなくなったとは具体的にどのような状況かというと、以下の条件に当てはまった場合です。 プロセスによるメモリアクセスでページフォルトが発生し オーバーコミットにより新たなメモリの割り当てが必要となり 物理メモリ、スワップメモリいずれにおいても必要な領域が確保できない場合 プロセスに対するメモリ割り当てを行うタイミングについて メモリが割り当てられるタイミングとは特定プロセスがメモリを必要とした時ということにはなりますが、その瞬間は実装コードにおいてメモリを使用している状況とは一致しないことも多いです。 理由として、OSはプロセスのメモリを仮想アドレス空間(後述)として管理しており、プロセスにおけるメモリ空間と物理メモリ空間との対応が異なっているためです。 実装コード上で一見大量のメモリを確保しているように見えても、あくまで仮想アドレス空間上での割り当てのみ行われており、そのメモリが実際に必要となるタイミングになるまで物理メモリとの割り当てが行われないことがあります。 このような処理は、オーバーコミットやオンデマンド・ページングといった技術(いずれも後述)で実現されています。 仮想アドレス空間とページフォルト 仮想アドレス空間とは、プロセスが認識しているメモリ空間のことです。プロセスはこの空間を使ってメモリにアクセスをしますが、仮想アドレスと物理アドレスとは一致しておらず、OSが対応管理表を用いてアクセス時にアドレス変換を行います。 仮想アドレス空間に物理メモリとの対応付けが行われていない場合、そのアドレスにアクセスした場合にページフォルトが発生します。このページフォルトをトリガーとして実際のアクセスすることになるメモリ領域に割り当てが行われます。 これにより例えば以下のようなことが可能になります。 物理メモリをスワップファイルに退避することによって、物理メモリを仮想的に拡張することができる プロセス間でメモリ空間を隔離することができる(個々のプロセスは個別の物理メモリ割り当てを持っているため) プロセス間でメモリ空間を共有することができる(共有メモリ) なおメモリ割り当てはページという単位(一般的には4KB)で行われます。 オーバーコミット オーバーコミットとは、物理メモリ容量を超えてプロセスに仮想メモリ空間を割り当てることです。プロセスがメモリを要求したとしても、実際にそのメモリを使用しないケースも多いため、問題になることは少ないという考え方です。 その代わり、実際にメモリを使用するタイミングでメモリ不足に陥る可能性があります。 オンデマンド・ページング オンデマンド・ページングとは、プロセスがメモリを使用するタイミングで初めて仮想メモリ空間と物理メモリ空間との割り当てを行うことです。 オーバーコミットと組み合わせて用いることで、必要な物理メモリ使用量を削減することができます。 OOM-Killerが行っていること OOM-Killerが発動した場合、起動しているどれかのプロセスを強制終了させることでメモリ確保を試みます。 この際にOSは、以下のような状態のプロセスを優先的に選択しようとします。 使用メモリ量が多いプロセス OOM補正値が高いプロセス /proc/<pid>/oom_score_adj で設定される値。-1000 ~ +1000 の範囲で、値が高いほどOOM-Killerの対象となりやすい。-1000 であればOOMスコアが0になる プロセスnice値やその他ヒューリスティックな要素 正確には上記を考慮して /proc/<pid>/oom_score というOOMスコアがリアルタイムで算出されるのですが、OOM-Killerが発動したタイミングでOOMスコアが最も高いプロセスが選択されます。 実例 stress コマンドでメモリ消費を行い、OOM-Killerが発動する様子を確認してみます。 空きメモリ容量は1GB未満(700MB程度)、スワップファイルはなしの環境とします。 $ free -m total used free shared buff/cache available Mem: 949 159 696 0 92 673 512MBのメモリを消費します。 $ stress --vm 1 --vm-bytes 512M --vm-keep stress: info: [32679] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd 空きメモリ容量は200MB程度になりました。 $ free -m total used free shared buff/cache available Mem: 949 668 187 0 92 164 ここで新たに512MBのメモリ消費します。 $ stress --vm 1 --vm-bytes 512M --vm-keep stress: info: [33059] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd すると元々起動していたほうの stress コマンドが強制終了され、OOM-Killerが発動したことがわかります。 $ stress --vm 1 --vm-bytes 512M --vm-keep stress: info: [32679] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd stress: FAIL: [32679] (425) <-- worker 32680 got signal 9 stress: WARN: [32679] (427) now reaping child worker processes stress: FAIL: [32679] (461) failed run completed in 67s 今回のケースだと、先に起動していたほうがOOMスコアが高い傾向があり、結果として新しく起動したプロセスが残る状況でした。 対処1 メモリ使用量を削減できないか検討します。 対処2 物理メモリを増設できないか検討します。 対処3 スワップファイルを設定することで、物理メモリ容量を超えてメモリを利用できるようにします。ただしスワップメモリの性能は物理メモリよりも低いためパフォーマンスが極端に低下することが多いです。 瞬間的にメモリ使用量が増加するようなケースであればスワップメモリを利用することでOOM-Killerの発生頻度を抑えることができますが、恒常的にメモリが不足しているような状況であれば対処1や対処2を実施することをお勧めします。 スワップファイルを設定する手順は以下の通りです。 # dd if=/dev/zero of=/swapfile bs=1M count=512 # chmod 600 /swapfile # mkswap /swapfile # swapon /swapfile $ free -m total used free shared buff/cache available Mem: 949 619 248 0 81 219 Swap: 511 43 468 対処4 OOMスコアを調整する 特定のプロセスに対してOOMが発動しづらい状態にすることができます。なるべく常時起動しておいて欲しいプロセスに対して実施しておくと安定性が向上するかもしれません。 下記の pid は stress コマンドのプロセスIDです。 # echo -1000 >/proc/<pid>/oom_score_adj この状態で再度512MBのメモリ消費を行おうとしますが、既存のプロセスのOOMスコアが低いため、新しく起動したプロセスが強制終了されます。 $ stress --vm 1 --vm-bytes 512M --vm-keep stress: info: [35408] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd stress: FAIL: [35408] (425) <-- worker 35409 got signal 9 stress: WARN: [35408] (427) now reaping child worker processes stress: FAIL: [35408] (461) failed run completed in 6s まとめ この記事では、OOM-Killerはどういう理由で発生するのか、なにが起きているのか、どう対処すればいいのか、といったことを解説しました。 安定したサービス運営を行うためには、OOM-Killerに対する理解と対策が必要です。この記事がその一助となれば幸いです。
アバター
こんにちは。開発部でiOSエンジニアをしている野口です。 Flutterエンジニアをやっていましたが今年からiOSエンジニアに転向したので思っていることを書こうという記事になります。 なぜiOSに転向したのか Flutterをやっていると、外部のパッケージを入れる際にネイティブコード書かないといけないなどネイティブの知識を要求されるパターンがあり以前からネイティブに興味がありました。 そんな中で社内でiOSエンジニアのポジションが必要とされていたため、良いチャンスだと思い転向しました。 また、数年前まではクロスプラットフォームでFlutterがイケイケだと思っていましたが(日本においては)、 最近は複数のクロスプラットフォームを検討した結果、React NativeやKMPなどが採用されるケースも多々見るようになったのでどのクロスプラットフォームも良くなってきている印象です。 その状況においてもモバイルアプリは結局ネイティブの知識を必要とするため、ネイティブができることは今後のキャリア的にもプラスになるかと思っています。 iOSに転向してどうか キャッチアップについて まず、思ったのは体系的な情報が全然ないなと。 本はそれなりに出ますが、ほとんどSwiftUIでUIKitに関する本は全く出ていないなと思いました。 デリッシュキッチンでは主にUIKit、Storyboardが使用されているため、ここら辺の情報が欲しいのですが、公式の チュートリアル くらしかありませんでした。 結局、UIKitはやりながら覚えています。最近はAIに聞いたら良い感じに答えてくれるのでありがたいです。 Storyboardは検索してもXcodeのバージョンが古いものが多く、中々参考になるものが出てきません。また、GUIで操作するものなのでAIに聞くこともできないので絶賛苦労しています。 ここら辺はチームの方針として、StoryboardはUIKitのコードもしくはSwiftUIに書き換えていこうと話し合ってるので今後Storyboardは廃止する方針です。 逆にSwiftUIはFlutterのwidgetシステムに似ているので、Flutterの知識があると理解が早いです。というかほとんど同じです。 実際に以下のコードを比較してみます。 中央にボタンを表示するコードですが、見た目はほとんど同じです。 body が画面全体を表現していおり、その中にボタンを配置しているのでFlutterもSwiftUIも同じ考え方をしています。 Flutter import 'package:flutter/material.dart' ; class CenterButtonScreen extends StatelessWidget { const CenterButtonScreen({ super .key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () {}, child: const Text( 'ボタン' ), ), ), ); } } SwiftUI import SwiftUI struct CenterButtonScreen : View { var body : some View { Button( "ボタン" ) {} .buttonStyle(.borderedProminent) .frame(maxWidth : .infinity, maxHeight : .infinity) } } UIKitでは、中央にボタンを表示するためには、以下のようにコードを書く必要があり、見た目がだいぶ変わります。 UIKit import UIKit class CenterButtonViewController : UIViewController { private func createButton () -> UIButton { let button = UIButton(type : .system) button.setTitle( "ボタン" , for : .normal) button.backgroundColor = .systemBlue button.setTitleColor(.white, for : .normal) button.layer.cornerRadius = 8 return button } override func viewDidLoad () { super .viewDidLoad() let button = createButton() button.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button) NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo : view.centerXAnchor ), button.centerYAnchor.constraint(equalTo : view.centerYAnchor ), button.widthAnchor.constraint(equalToConstant : 100 ), button.heightAnchor.constraint(equalToConstant : 44 ) ]) button.addTarget( self , action : #selector(buttonTapped), for : .touchUpInside) } @objc private func buttonTapped () { // ボタンを押した時の処理 } } viewの配置には以下のように作成したボタンに対して制約をつけてあげる必要があるため、コードが冗長になるなーと感じています。 ですが、最近慣れてきてるのでそんなに気にならなくなってきました。 NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo : view.centerXAnchor ), button.centerYAnchor.constraint(equalTo : view.centerYAnchor ), button.widthAnchor.constraint(equalToConstant : 100 ), button.heightAnchor.constraint(equalToConstant : 44 ) ]) 開発環境の違い Swiftの言語仕様、公式のフレームワークが充実している CombineなどApple純正のフレームワークを提供してくれるのは良いです。 Combineでは非同期処理や状態管理を行なってくれますが、Flutterでやろうとすると、Riverpodなどの外部パッケージを使用する必要があります。 外部パッケージではサポートされなくなることもあり、長期的なメンテナンスを考えるとあまり依存したくはなくはないものにはなるので、それを公式が提供するのは安心感があります。 また、Flutterでは不変なクラスを作るために freezed などの外部パッケージと build_runner による自動生成が必要ですが、Swiftでは struct を使うだけで実現できるのが魅力的です。 エディターの違い Flutter開発ではVSCodeを使用していましたが、iOS開発ではXcodeを使用します。Xcodeは癖が強いため最初は辛かったです。 特に私はgitの操作をvscodeの Git Graph で主に行なっていたので、 Xcodeではgitクライアントをどうしようかと、TerminalかSourceTreeを使用するかなど考えて右往左往しています。 また、XcodeではAIのサポートがあまり充実していないので、昨今のAIブームに乗り切れないのが悲しいなと思います。 github copitlot を入れることはできますが、chat形式で質問はできない。。。と思ってたんですが、 記事を書いている時に調べたら最近 chat ができるようになったみたいです。 最近はCursorでiOS開発できないかなと思って試行錯誤しておりますが、Cursorからビルドすると遅すぎて辛いのでCursorはコードを書いてもらうのとgitクライアントとして使用しています。 CursorでiOS開発しようとした時に参考にしたのは こちら の記事です。 最後に 2ヶ月しか経ってないので、浅ーいことしか書けていないのですが、同じモバイル開発でも結構環境が変わって、キャッチアップが大変でした。 FlutterからiOSに転向を考えている方の参考になれば嬉しいです。 もっと深い内容を書きたいですが、もう少しだけiOS開発をしてから機会があれば記事を書きたいと思います。 ご覧いただきありがとうございました。
アバター
はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 デリッシュキッチンのSTG環境のWEBへのアクセスには社内ユーザーからのみという制限があります。 先日、制限をかけるシステムに触れる機会があったので、今回はそのシステムについて紹介しようかと思います。 背景 先日、社外の方にSTG環境にアクセスしてもらうことがあったのですが、Google SSOの認証のページに飛ばされてしまって確認してもらうことができないということがありました。 エブリーではGoogle Workspaceを利用しており、Google WorkspaceのSSOを利用して業務アプリケーションにアクセスすることが多いです。仕組みを調査したところ、このSSOの認証がSTG環境で必須になっており、SSOの認証画面にリダイレクトされていました。 STG環境でSSO認証を行う 以下ではSTG環境でSSO認証を行う仕組みについて述べますが、SSO認証に用いるIdP(Identity Provider)の詳細や設定方法については記事の範囲外とさせていただきます。 また、デリッシュキッチンのWebはアプリケーションが動作しているECSの前段にELBやCloudFrontが存在しており、今回の記事の内容では通信がCloudFrontを経由していることが前提となります。 STG環境でSSO認証を行うためにLambda@EdgeとGoogleのIdPを利用します。 おおよその流れは図のようになりますが、後で疑似コードで実際にどのように実装しているかを説明します。 Lambda@Edge AWSの公式ドキュメント Lambda@Edge を使用してエッジでカスタマイズする - Amazon CloudFront によると、 Lambda@Edge は AWS Lambda の拡張です。Lambda@Edge は、Amazon CloudFront が配信するコンテンツをカスタマイズする関数を実行できるコンピューティングサービスです。 となっています。 Lambda@Edgeを使うことによってCloudFrontから配信するコンテンツをカスタマイズすることができるので、今回はこのLambda@Edgeを使ってSTG環境へのアクセスにSSO認証をかけます。 Lambda@Edgeは下記の4つのイベントにトリガーできるのですが、ビューワーリクエストをトリガーにして認証を行なっていくことになります。 ビューワーリクエスト: ユーザーからCloudFrontへのリクエスト オリジンリクエスト: CloudFrontからオリジン(コンテンツ)へのリクエスト オリジンレスポンス: オリジン(コンテンツ)からCloudFrontへのレスポンス ビューワーレスポンス: CloudFrontからユーザーへのレスポンス Lambda@Edgeで何をしているか 以下では、疑似コードを載せていますが、エラーハンドリングを無視していたりと実際に動くものではないことはご留意いただければと思います。 STG環境でSSO認証を行うためにLambda@Edgeで大きく分けて2つの処理を行っています。 特定条件の時に認証のスキップ SSO認証を行う 1. 特定条件の時に認証のスキップ exports . handler = async ( event , _ , callback ) => { const request = event . request ; // 指定されたIPアドレスから指定されたpathへのアクセスであれば、認証を無視する if ( allowedPath ( request . uri ) && await allowedIP ( request . clientIp )) { // requestを継続してcontentsにアクセスする return callback ( null , request ) ; } const headers = request . headers ; // SSO認証を行う return await sso . main ( request , headers , callback ) ; } ; ここでは、今回この記事を書く背景となった外部の方にSTG環境にアクセスしてもらう場合など、SSO認証が行えないがアクセスさせたい場合に例外的にアクセスを許可しています。認証をスキップする条件に縛りはないのですが、疑似コードではIPアドレスとアクセスするコンテンツのpathで条件をかけています。 もし条件に一致しない場合はSSO認証を行うことになります。 2. SSO認証を行う exports . main = async ( request , headers , callback ) => { try { // ④ トークンがsetされている場合 if ( hasToken ( headers )) { // トークンの検証を行い、結果を返す return validateToken ( request , headers , callback ) ; } // ② 認可コードを渡すcallbackとして呼び出された場合 if ( request . uri . startsWith ( config . CALLBACK_PATH )) { const queryDict = qs . parse ( request . querystring ) ; // 認可コードとトークンの交換をIdPにリクエストする const response = await requestToken ( queryDict ) ; return setToken ( request , headers , response , queryDict , callback ) ; } // ① SSO認証のためにIdPにリダイレクトする redirectToIdP ( request , callback ) ; } catch ( error ) { console . error ( error ) ; throw ( error ) ; } } ; function setToken ( request , headers , response , queryDict , callback ) { const decodedData = jwt . decode ( response . id_token ) ; // JWTの確認を行う jwt . verify ( decodedData ) ; // ③ CookieにトークンをsetするためにSet-Cookieを指定してresponseを返す const response = getResponseForSetToken ( queryDict , config , headers , decodedData ) ; callback ( null , response ) ; } function validateToken ( request , headers , callback ) { // JWTの確認を行う  jwt . verify () ; // requestを継続してcontentsにアクセスする   callback ( null , request ) ; } 疑似コードを見ていただければ、どのような処理を行っているのか理解していただけるかもしれませんが、簡単に説明していきます。 始めに、④や②に当てはまらなかった場合、つまり、SSO認証の結果のトークンがsetされておらずcallbackでリダイレクトされたpathでもない場合は、認証が完了していないため①でIdPの認証ページにリダイレクトさせます。 次に、IdPで認証が成功した場合はcallbackで規定のpathにリダイレクトされます。callbackでリダイレクトされている場合は認可コードを受け取っているはずなので、この認可コードを使ってIdPにトークンをリクエストします。無事にIdPからトークンを取得できた場合は③でトークンをCookieにセットした後に元のリクエストページへリダイレクトさせます。 最後に、元のリクエストにリダイレクトされた際にSSO認証の結果のトークンがsetされているはずなので④で確認を行います。ここでトークンがsetされている場合はトークンの正当性を確認します。トークンの正当性が確認できた場合は、SSO認証済みのはずなのでコンテンツにアクセスするためのrequestを継続します。 まとめ 今までLambda@Edgeを触ったことがなかったので勉強する良い機会になりました。 この手のシステムは一度開発が完了するとなかなか触れる機会がないことも多いかと思いますが、積極的に触ってみることも大切だと感じました。
アバター
データ&AIチームでデータエンジニアを担当している塚田です。 弊社のデータ基盤はさまざまなデータソースからデータを連携しており、そのデータを活用することで全社のデータ基盤として成り立っています。 その中で、Google Analytics for Firebaseの活用をベースにBigQueryのコストダウンした事例をご紹介できればと思います。 概要 改めて、弊社ではGoogle Analytics for Firebaseなど色々な基盤を用いてログを収集していいます。 今回はGoogle Analytics for Firebaseを用いたログの取得とその保存先であるBigQueryを利用した時により良い運用ができないかと考えコスト面での確認を進めました。 方針 一般的にBigQueryは大規模なデータセットに対してクエリを実行する基盤として利用する方が多いかと思いますが、そのデータを保持するのにもコストがかかっている部分が多いのではないでしょうか。 その中でコストがかかるものを低減できる施策として 新しい料金モデルで BigQuery の物理ストレージの費用を削減 が活用できないかと思い確認を進めました。 発表から数年経っているものとはなりますが、発表前から利用しているプロジェクトだとそのオプションがなかった状況になると思うので、適用した方が良いのかを考える一助となればと考えています。 利用量確認 BigQueryのドキュメントをもとに以下のテーブルを用いて利用量の現状確認を進めました region-us .INFORMATION_SCHEMA.TABLE_STORAGE_BY_PROJECT クエリについては BigQueryのドキュメント にサンプルがあります。 DECLARE active_logical_gib_price FLOAT64 DEFAULT 0 . 02 ; DECLARE long_term_logical_gib_price FLOAT64 DEFAULT 0 . 01 ; DECLARE active_physical_gib_price FLOAT64 DEFAULT 0 . 04 ; DECLARE long_term_physical_gib_price FLOAT64 DEFAULT 0 . 02 ; WITH storage_sizes AS ( SELECT table_schema AS dataset_name, -- Logical SUM ( IF (deleted= false , active_logical_bytes, 0 )) / power ( 1024 , 3 ) AS active_logical_gib, SUM ( IF (deleted= false , long_term_logical_bytes, 0 )) / power ( 1024 , 3 ) AS long_term_logical_gib, -- Physical SUM (active_physical_bytes) / power ( 1024 , 3 ) AS active_physical_gib, SUM (active_physical_bytes - time_travel_physical_bytes) / power ( 1024 , 3 ) AS active_no_tt_physical_gib, SUM (long_term_physical_bytes) / power ( 1024 , 3 ) AS long_term_physical_gib, -- Restorable previously deleted physical SUM (time_travel_physical_bytes) / power ( 1024 , 3 ) AS time_travel_physical_gib, SUM (fail_safe_physical_bytes) / power ( 1024 , 3 ) AS fail_safe_physical_gib, FROM `region-us`.INFORMATION_SCHEMA.TABLE_STORAGE_BY_PROJECT WHERE total_physical_bytes + fail_safe_physical_bytes > 0 -- Base the forecast on base tables only for highest precision results AND table_type = ' BASE TABLE ' GROUP BY 1 ) SELECT dataset_name, -- Logical ROUND (active_logical_gib, 2 ) AS active_logical_gib, ROUND (long_term_logical_gib, 2 ) AS long_term_logical_gib, -- Physical ROUND (active_physical_gib, 2 ) AS active_physical_gib, ROUND (long_term_physical_gib, 2 ) AS long_term_physical_gib, ROUND (time_travel_physical_gib, 2 ) AS time_travel_physical_gib, ROUND (fail_safe_physical_gib, 2 ) AS fail_safe_physical_gib, -- Compression ratio ROUND (SAFE_DIVIDE(active_logical_gib, active_no_tt_physical_gib), 2 ) AS active_compression_ratio, ROUND (SAFE_DIVIDE(long_term_logical_gib, long_term_physical_gib), 2 ) AS long_term_compression_ratio, -- Forecast costs logical ROUND (active_logical_gib * active_logical_gib_price, 2 ) AS forecast_active_logical_cost, ROUND (long_term_logical_gib * long_term_logical_gib_price, 2 ) AS forecast_long_term_logical_cost, -- Forecast costs physical ROUND ((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price, 2 ) AS forecast_active_physical_cost, ROUND (long_term_physical_gib * long_term_physical_gib_price, 2 ) AS forecast_long_term_physical_cost, -- Forecast costs total ROUND (((active_logical_gib * active_logical_gib_price) + (long_term_logical_gib * long_term_logical_gib_price)) - (((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price) + (long_term_physical_gib * long_term_physical_gib_price)), 2 ) AS forecast_total_cost_difference FROM storage_sizes ORDER BY (forecast_active_logical_cost + forecast_active_physical_cost) DESC ; このクエリを活用し結果を精査することで論理バイト ストレージ課金よりも物理バイト ストレージ課金の方が有利に働くことがあるかと思います。 弊社の環境だと物理バイト ストレージ課金による単価増よりも圧縮率が大きいためコストとしては有利に働く結果となりました。 一部の指標のみになってしまいますが、グラフの青い部分がBigQueryのストレージを含めた課金量になっており大幅にコストが下がる状況となりました。 まとめ 全ての環境で適用できるわけではありませんが、オープンクラウドを利用していく中でコストというのは常に意識していかないとならない部分だと思います。 毎日数多くのアップデートがある中で適用できそうなものがあれば検証の上柔軟に導入していければと考えており、その一例をご紹介しました。 このような改善を積み重ねながら新たな大きいアクションができるように開発を進めていきたいと考えています。
アバター
はじめに こんにちは。 株式会社エブリーの開発本部データ&AIチーム(DAI)でデータエンジニアをしている吉田です。 今回は、Redashのアップグレードについてのお話です。 背景 デリッシュキッチンではデータ分析のための可視化ツールとして Redash を利用しており、ECS上にRedashをデプロイして運用しています。 RedashはOSS版の更新が長らく停止していましたが、昨年プロジェクトが再始動され( 参考 )、先日v25.1.0がリリースされました。 今回、Redash v10.0.0 から v25.1.0 へのアップグレード試験を実施しました。 現在のRedashの運用状況については、以下の記事で詳しく解説しています。 https://tech.every.tv/entry/2024/03/06/160148 アップグレード手順 v10.0.0 から v25.1.0 へのアップグレードは、v10.1.0 を経由する必要があります。 v10.0.0 から v10.1.0 はイメージの差し替えのみで、DBマイグレーションは不要です。 v10.1.0 から v25.1.0 はDBマイグレーションが必要です。 具体的な手順は以下の通りです。 事前準備 Redash v25.1.0でビルドしたDockerイメージをECRに登録します マイグレーションタスク用のECSタスク定義とサービス定義を作成します Redash DBのバックアップを作成します 作業 古いRedashコンテナを停止します マイグレーションタスクを実行します 新しいRedashコンテナを起動します 確認 Redashにログインし、データが正常に表示されることを確認します マイグレーションタスクの作成 Redashのデプロイには ecspresso を利用しており、 ecspresso run コマンドでマイグレーションタスクを実行できるようにマイグレーション用定義ファイルを作成します。 タスク定義ファイルは以下のように作成します。(一部抜粋) { " family ": " redash-migration ", " containerDefinitions ": [ { " name ": " redash-migrate ", " image ": " ECR/Custom-Redash:25.1.0 ", " command ": [ " manage ", " db ", " upgrade " ] } ] } 手順2.1で古いRedashコンテナを停止した後、 ecspresso run --wait-until stopped のようなコマンドを実行することで、マイグレーションタスクを実行できます。 このとき、ある程度の時間がかかるのでecspressoのタイムアウト設定を適切に行う必要があります。 ハマりどころ v10.1.0からv25.1.0へのアップグレードにおいて、以下のようなハマりどころがありました。 エラー DETAIL: \u0000 cannot be converted to text. が発生する マイグレーションタスクを実行中に、"change type of json fields from varchar to jsond" ステップで以下のエラーが発生しました。 DETAIL: \u0000 cannot be converted to text. これは Issue でも報告されており、以下のSQLを実行することで対応できます。 UPDATE visualizations SET options = replace (options::text, ' \u0000 ' , '' )::json WHERE strpos(options::text, ' \u0000 ' ) > 0 ; エラー TypeError: 'NoneType' object is not iterable が発生する マイグレーションタスクを実行中に、"fix_hash"ステップで、以下のエラーが発生しました。(一部抜粋) 2025/02/12 14:38:32 File "/app/migrations/env.py", line 85, in run_migrations_online 2025/02/12 14:38:32 context.run_migrations() 2025/02/12 14:38:32 File "<string>", line 8, in run_migrations 2025/02/12 14:38:32 File "/usr/local/lib/python3.10/site-packages/alembic/runtime/environment.py", line 948, in run_migrations 2025/02/12 14:38:32 self.get_context().run_migrations(**kw) 2025/02/12 14:38:32 File "/usr/local/lib/python3.10/site-packages/alembic/runtime/migration.py", line 627, in run_migrations 2025/02/12 14:38:32 step.migration_fn(**kw) 2025/02/12 14:38:32 File "/app/migrations/versions/9e8c841d1a30_fix_hash.py", line 55, in upgrade 2025/02/12 14:38:32 new_hash = update_query_hash(record) 2025/02/12 14:38:32 File "/app/migrations/versions/9e8c841d1a30_fix_hash.py", line 29, in update_query_hash 2025/02/12 14:38:32 parameters_dict = {p["name"]: p.get("value") for p in record['options'].get('parameters', [])} if record.options else {} 2025/02/12 14:38:32 TypeError: 'NoneType' object is not iterable コードの該当箇所を確認したところ、取得された parameters_dict は利用されていなかったためコメントアウトすることで対応しました。 https://github.com/getredash/redash/blob/master/migrations/versions/9e8c841d1a30_fix_hash.py#L29-L31 def update_query_hash (record): should_apply_auto_limit = record[ 'options' ].get( "apply_auto_limit" , False ) if record[ 'options' ] else False query_runner = get_query_runner(record[ 'type' ], {}) if record[ 'type' ] else BaseQueryRunner({}) query_text = record[ 'query' ] # parameters_dict = {p["name"]: p.get("value") for p in record['options'].get('parameters', [])} if record.options else {} # if any(parameters_dict): # print(f"Query {record['query_id']} has parameters. Hash might be incorrect.") return query_runner.gen_query_hash(query_text, should_apply_auto_limit) 最後に Redash v10.0.0 から v25.1.0 へのアップグレードが完了しました。 一部暫定的な対応を行いましたが、試験環境でのアップグレードは成功しました。 現在、既存クエリとダッシュボードが正常に動作するかどうか、試験環境で確認を行っています。 これらに問題がないことを確認次第、本番環境へのアップグレードを進める予定です。
アバター
はじめに デリッシュキッチン 開発部でエンジニアをしている24新卒の新谷と @きょー です。 2025年2月13-14日に開催された Developers Summit 2025 に参加してきましたので、イベントの様子や印象に残ったセッションをいくつかご紹介します。 イベント概要 Developers Summit(デブサミ)は、2003年から続くITエンジニアのための祭典です。 ソフトウェア開発者が今知っておきたいトピックや、ロールモデルとなるデベロッパーとのさまざまな出会いがあるイベントです。 2025年のテーマは「ひろがるエンジニアリング」で、技術革新が進む中でのエンジニアの可能性や社会への影響について紹介されました。 会場は、ホテル雅叙園東京で開催されましたが、室内に鯉が泳ぐ池があるなど、豪華な雰囲気でした。 また、会場内には多くのブースが出展されており、最新の技術やサービスを紹介されていたり、書籍の販売も設けられていました。 以下ブースの紹介となります。各社自社プロダクトを使った展示だったり、通りすがっただけで目を引くような展示、ホットなトピックについてアンケートをとっている企業もありました。プロダクトや組織についてお話を聞くこともできとても楽しかったです! 参加レポート リアルな過去からたどり着いた、事業を成長を牽引するエンジニアの在り方 発表者: ウェルスナビ株式会社 保科 智秀 さん レポート: 新谷 www.docswell.com こちらのセッションでは、事業成長を支えるエンジニアリングの在り方について語られました。 事業成長のフェーズは「超初期(クローズドβ)」「初期(一般公開)」「成長期(1→10)」「成長期(10→100)」の4段階に分けられ、それぞれの課題とエンジニアの役割が説明されました。 超初期フェーズ 限られたリソースの中、期間で目的達成するために真のMUST要件を引き出す。 初期フェーズ 安易な解決策に飛びつかず、高確率で予測される継続した改善を考慮することが大事。 だが遠すぎる未来は切り捨てる。 成長期(1→10) 今後の負債解消のプランを持ちつつ、負債を受け入れる覚悟を持って機能開発を優先。 成長期(10→100) 負債解消と新規事業展開に向けたアーキテクチャ変更が必要となり、将来に向けた成長加速のプランを提案。 各フェーズでエンジニアは異なる判断を求められ、スピード感・柔軟性・将来の展望を考慮した選択が重要であることが強調されていました。 特に、負債を受け入れながらも成長を加速させるための意思決定が鍵となるという点が印象的でした。 目の前の仕事と向き合うことで成長できる - 仕事とスキルを広げる 発表者: 株式会社リンケージ そーだいさん( https://x.com/soudai1025 ) レポート: 新谷 speakerdeck.com このセッションでは、仕事を通じてスキルを広げ、能力を高める方法が紹介されました。 まず、能力を伸ばすためには「知識」と「経験」を掛け合わせて「知恵」とすることが重要であると語られました。 スキルを習得する過程では、「知る」「やる」「わかる」「できる」「している」といったステップを踏むことで、実践を通じた学びが深まるという考えが示されました。 また、「仕事の中で成長する」ためには、計画実行力、言語化力、問題解決能力を鍛えることが重要であり、それには「内省」と「フィードバックサイクル」が必要であると説明されました。 具体的には、タスクを細分化し、適切な問題設定を行い、日報や週報を活用して振り返りを行うことが推奨されていました。 最後に、「一日ひとつでも知らないことを見つける」ということが紹介されており、日々の積み重ねがスキルアップにつながるということを改めて認識しました。 生成 AI 時代のプロダクトの現在地点 発表者: 株式会社 LayerX 松本さん( https://x.com/y_matsuwitter ) レポート: きょー speakerdeck.com このセッションでは、LLM(大規模言語モデル)時代におけるプロダクト開発の在り方について松本さんに紹介いただきました。 LLMが人間と同程度の情報量で仕事を習得できるこの時代、LLMのポテンシャルを引き出すために「AIをオンボーディングする」意識が大切になるとのことでした。 LLMが活躍できるように適切な情報、ツールを提供し、継続的に学習させていける仕組みが必要になってくるわけです。 特に以下の5つの点が重要だと説明いただきました。 Context LLMが問題を解決するために必要な情報 Knowledge LLMが参照する知識データベース Workflow LLMが業務を遂行するためのプロセス Planning LLMがタスクを計画するための仕組み Evaluation LLMの出力結果を評価するための仕組み また、LLMを使う箇所を見極めることも重要だと発表で触れていました。LLMを組み込める箇所全て組み込むのではなく、適切な品質・体験となるようにソフトウェア・LLM・人間の誰が何をやるかバランスを決めるのが重要とのことでした。 「LLM中心の時代のプロダクトを作り直すならどう構成していくか」松本さんから最後に問いかけられたテーマです。理想のプロダクトを常に考え、現実とのギャップをどう埋めていくかを考えながら開発をしていこうと思いました。 リーダブルテストコード~メンテナンスしやすいテストコードを作成する方法を考える~ 発表者: twadaさん( https://x.com/t_wada )、オーティファイ株式会社 末村さん( https://x.com/tsueeemura )、株式会社10X / B-Testing ブロッコリーさん( https://x.com/nihonbuson ) レポート: きょー speakerdeck.com このセッションでは、読みやすくメンテナンスしやすいテストコードの書き方について上記で記載している3名の専門家より紹介いただきました。 twadaさんからは、テストコードの認知負荷を下げるための方法として、名前、構造、情報量に気を配ることが重要であると説明がありました。 具体的には、テストの意図が明確に伝わるように、テストコードの命名や構造を工夫すること、そして、テストに必要な情報だけを記述することが重要とのことでした。 末村さんからは、E2Eテストコードを例に、コンテキストを明示することでテストコードを自己説明的にする方法を紹介いただきました。 テストコードに「いま、どのページにいるのか」「どんなデータがあるはずなのか」といったコンテキストを明示することで、コードの可読性が向上し、メンテナンス性も高まるとのことでした。 ブロッコリーさんからは、テストコードにテストの意図を込めることの重要性について紹介いただきました。 テストの意図を明確にすることで、コードの理解容易性や説明容易性が向上し、ひいては保守性も向上すると説明がありました。 また、テストの意図をテストメソッド名に記述することで、特別な設定値がどれなのかが分かりやすくなり、仕様変更時の対応もしやすくなるという利点も紹介されました。 メンテナンスしやすいテストコードを書く上で大切なことは、読み手を意識した命名や認知負荷を下げるための構造化ということでした。また、AIが発展してきたこの時代、AIをうまく使いこなしより良いテストコードを模索していくことも大事ということを紹介されていました。 まとめ Developers Summit 2025は、エンジニアリングのトレンドや事例を知ることができる貴重な機会でした。 特定の技術にフォーカスしたセッションから、エンジニアとしてのキャリアやスキルアップについて学べるセッションまで、幅広い内容に触れることができました。 今後も、新しい技術や知識を取り入れ、エンジニアとしての成長を続けていきたいと思います。
アバター
こんにちは。RH開発部RHRAグループの池です。 2024年6月にエブリーは5つの小売アプリの運営について事業譲渡を受け、『 retail HUB 』へ移管しました。 引き継いだシステムのバックエンドはLaravelを用いて構築されていましたが、Laravelは弊社では初めて扱う技術スタックでした。そのため、チーム全体でLaravelの知見を深めながら、運用保守および開発を進めています。 このような状況の中、新規サーバーを構築する機会があり、Laravelの知見をチームで蓄積することも目的の一つとして、新規サーバーの開発においてLaravelを採用しました。 本記事では、弊社が初めてLaravelを導入した新規サーバーの構成についてご紹介させていただきます。 システム概要 まず最初に前提ですが、新規サーバーの開発にあたり、以下の条件を考慮して設計開発を進めています。 スピード重視の開発 リリース優先でまずは必要最小限の機能を実装 開発効率を重視した技術選定 段階的な改善を許容する設計 チームの技術背景 Laravelはチームが初めて扱う技術スタック チーム全体で学習しながらの開発 将来を見据えた設計 マルチテナント対応を考慮 段階的な機能拡張が可能な構造 このような方針をもとに、最初から作り込んだ設計を目指すのではなく、スピードを優先しつつも実用的な設計を考慮しながら挑戦と学びのある開発アプローチをバランスをとって選択しています。 全体像 今回開発している新規サーバーでは、モバイル向けAPIと管理画面向けAPIの2つのAPIを提供しており、これらAPIは共通のデータベースを使用しています。 これらを効率的に管理すべくモノレポ構成で開発を行っています。 構成の簡易図は以下の通りです。 技術スタック こちらは紹介までになりますが、Laravel関連で採用している技術スタックは以下の通りです。 ほとんどが弊社として初めて扱うものであり、チーム全体で学習・議論しながら取り組んでいます。 PHP 8.3(8.4へアップグレード予定) Laravel 11 Laravel Octane & Swoole Laravel Sanctum Pest Larastan Laravel Pint ディレクトリ構成 リポジトリ全体のディレクトリ構造は、以下の通りです。 . ├── .github/ # GitHub Actionsの設定(パイプラインの共通化) ├── dashboard-api/ # 管理画面向け API プロジェクト │ ├── Dockerfile # 管理画面向け Dockerイメージ定義 │ ├── app # 管理画面向け API 固有コード │ │ ├── Exceptions │ │ ├── Helpers │ │ ├── Http │ │ ├── Providers │ │ ├── Repositories │ │ │ ├── Interfaces # リポジトリのインターフェース │ │ └── Services │ │ ├── Interfaces # サービスのインターフェース │ ├ ... │ ├── compose.yaml # ローカル開発環境の設定 │ ├── composer.json # 共通パッケージをimport │ ├── ecspresso # 管理画面向け デプロイ設定 │ ├── tests # 管理画面向け API 固有のテストコード │ ├ ... │ ├── mobile-api/ # モバイル向け API プロジェクト │ ├── Dockerfile # モバイル向け Dockerイメージ定義 │ ├── app # モバイル向け API 固有コード │ │ ├── Exceptions │ │ ├── Helpers │ │ ├── Http │ │ ├── Providers │ │ ├── Repositories │ │ │ ├── Interfaces # リポジトリのインターフェース │ │ └── Services │ │ ├── Interfaces # サービスのインターフェース │ ├ ... │ ├── compose.yaml # ローカル開発環境の設定 │ ├── composer.json # 共通パッケージをimport │ ├── ecspresso # モバイル向け デプロイ設定 │ ├── tests # モバイル向け API 固有のテストコード │ ├ ... │ ├── packages │ └── common/ # 共通パッケージ(各 API プロジェクトで再利用) │ ├── composer.json │ └── src │ ├── Models │ ├── Providers │ ├── Services │ ├── Repositories │ ├── databases # データベース関連は全て共通化 │ │ ├── factories │ │ ├── migrations │ │ └── seeders │ └── tests │ ├── phpstan.neon # PHPStanの共通設定 ├── pint.json # Laravel Pintの共通設定 プロジェクト共通コードは packages/common/ に配置された共通パッケージで管理します。データベースモデルやマイグレーション、ビジネスロジックなど、両APIで共有する機能を集約します。各APIプロジェクトからはComposerを通してこの共通パッケージをインポートして共通コードを利用する形となります。 また、管理画面向けAPI( dashboard-api/ )とモバイルアプリ向けAPI( mobile-api/ )配下では、それぞれのプロジェクトに応じた固有のロジック、テストコードや設定ファイル、デプロイ構成などを個別に管理しています。 このように、共通機能と個別機能を分離しながら開発を行なっています。加えて、CI/CD 用のワークフローも共通で管理します。 アーキテクチャ設計 私たちのシステムは、SaaSとしてマルチテナントでの運用を想定しており、テナントごとに異なるビジネスロジックやデータアクセスを柔軟に切り替えられるようなアーキテクチャを検討しました。 あまり特別なことはしてないですが、レイヤードアーキテクチャ+DIP(依存性の逆転) の形を取りつつ、マルチテナント対応のために ServiceInterface と RepositoryInterface を導入しています。 モノレポ構成 私たちのチームでは、主に以下の理由でモノレポの構成を採用しました。 各APIプロジェクトが同じドメインで共通化できる要素が多い データベースマイグレーション、モデル定義 ビジネスロジック、ユーティリティ linter設定 CI/CDパイプライン 全員が複数プロジェクトを横断して開発する小規模なチーム体制との親和性あり 各プロジェクト横断的な変更がしやすい、影響範囲を把握しやすい など 続いて、Laravelにおけるモノレポ設定方法とInterfaceのDIについて実例を紹介します。 モノレポ設定方法とDIの実例紹介 Laravel プロジェクトにおいて共通パッケージを利用する際の主な方法は、Composer の repositories を用いる方法です。具体的には、各 API プロジェクトの composer.json に共通パッケージのリポジトリ定義を追加し、依存関係として設定します。 1. Composer のリポジトリ定義 例として、 mobile-api/composer.json の一部は以下のようになります。 { " require ": { " sample/common ": " dev-main " } , " repositories ": [ { " type ": " path ", " url ": " ../packages/common " } ] } 同様の設定を dashboard-api/composer.json にも記載することで、両サービスで共通パッケージを最新コードとして取り込むことが可能となります。 2. オートロード設定 共通パッケージ内のクラスは PSR-4 に従った名前空間の設定を行うことで、Laravel のオートローダーにより自動的に読み込まれます。これにより、サービス内で自然な形で共通機能が利用できる状態となります。 { " name ": " sample/common ", " autoload ": { " psr-4 ": { " Sample\\Common\\ ": " src/ " } } , ... " extra ": { " laravel ": { " providers ": [ " Sample \\ Common \\ Providers \\ CommonServiceProvider " ] } } } また、 extra.laravel.providers にサービスプロバイダーが指定することで、共通パッケージの初期化やサービス登録が自動的に行われます。 指定した CommonServiceProvider では loadMigrationsFrom メソッドを呼び出し、共通パッケージ内のマイグレーションファイルを読み込むようにします。 そうすることで、 mobile-api や dashboard-api のプロジェクトからマイグレーションを実行する際に、共通パッケージ内のマイグレーションファイルも読み込まれるようになります。 <?php /** * Bootstrap services. */ public function boot () : void { $ this -> loadMigrationsFrom ( __DIR__ . '/../databases/migrations' ) ; } ?> 3. 共通パッケージの利用例 例えば、共通パッケージ内に用意されたShopモデルを、 dashboard-api プロジェクトで利用する場合、以下のように記述します。 <?php use DashboardApi\Repositories\Interfaces\ShopRepositoryInterface; use Sample\Common\Models\Shop; class ShopRepository implements ShopRepositoryInterface { private Shop $ shop ; public function __construct ( Shop $ shop ) { $ this -> shop = $ shop ; } public function findShopById ( int $ id ) : Shop { return $ this -> shop -> find ( $ id ) ; } ?> 4. InterfaceのDI インターフェースと実装の紐付けは、Laravel のサービスコンテナを活用して行っています。これにより、テナントごとに異なる実装を柔軟に切り替えることが可能です。 <?php namespace DashboardApi\Providers; use Illuminate\Support\ServiceProvider; use Sample\Common\Services\Interfaces\ArticleServiceInterface; use Sample\Common\Repositories\Interfaces\ArticleRepositoryInterface; use DashboardApi\Services\ShopService; use DashboardApi\Repositories\ShopRepository; class AppServiceProvider extends ServiceProvider { public function register () : void { // Service層のDI設定 $ this -> app -> bind ( ShopServiceInterface :: class , ShopService :: class ) ; // Repository層のDI設定 $ this -> app -> bind ( ShopRepositoryInterface :: class , ShopRepository :: class ) ; } } ?> 将来的にはテナントごとにDIを切り替えることで、テナントごとに異なるビジネスロジックを持たせるような想定をしています。 <?php public function register () : void { $ this -> app -> bind ( ShopServiceInterface :: class , function ( $ app ) { // テナントに応じて実装を切り替え return match ( $ tenant ) { 'tenant_a' => new TenantAShopService ( $ app -> make ( ShopRepositoryInterface :: class ) ) , 'tenant_b' => new TenantBShopService ( $ app -> make ( ShopRepositoryInterface :: class ) ) , default => new ShopService ( $ app -> make ( ShopRepositoryInterface :: class ) ) , } ; }) ; } ?> 現状の課題と向き合い方 現在、開発を始めて間もない段階ですが、いくつかの課題が見えてきています。 当初は管理画面APIとモバイルアプリAPIで多くのビジネスロジックを共通化できると考えていましたが、実際には想定より共通化できる範囲が限定的でした。 現時点では主にデータベースのモデル定義とマイグレーションの共通化に留まっており、より効果的なロジックの共通化方法を模索しています。 細かいですが、開発環境については、現在各APIプロジェクトで個別に環境を立ち上げる必要があり、一つのdocker composeで統合的に管理できる環境の整備を検討しています。 チーム開発での課題については、スピード重視の開発という前提において、挑戦と学習、および品質との丁度良いバランスについて日々議論しています。 例えば、 少人数チームで初期開発フェーズにおいてテストコードの適切な粒度 クラス設計の責務分担 必要十分なドキュメント整備の範囲 フローを固めすぎない開発プロセス など、少人数チームならではの密なコミュニケーションを取りながら方針を固めています。 まとめ 今回は私たちが初めて取り組むLaravelでのバックエンド開発について、開発方針を踏まえた構成のアプローチをご紹介させていただきました。 最後に、本記事が同じようにLaravelでの開発を検討されている方々の参考になれば幸いです。 また、私たちは常に新しい技術的チャレンジに取り組める仲間を募集しています。私自身も新しい技術にチームで試行錯誤しながら取り組める環境で、日々成長を感じています。少しでも面白そうと感じていただけた方はぜひお気軽にお声がけください! 参考記事
アバター
はじめに デリッシュキッチン開発部のバックエンド中心で開発をしている @きょー です。 この記事では、普段業務で Postman を使っていく際に便利だと思った機能について紹介します。 Postman とは Postman は、API の構築・利用を支援する API プラットフォームです。API リクエストの作成・保存、リクエスト送信後のレスポンス確認といった用途で利用されることが多いですが、他にも様々な機能があります。 Postman で開発効率を上げるための便利機能 変数 変数を使う前後の差分を先に示します。 変数を使う前 変数を使った後 変数の設定画面 変数を設定・使用することで同じ値として使い回されることが多い URL のドメイン部分や Header の情報などを一箇所で管理できるようになります。 また、開発環境や本番環境用の変数を設定することで簡単にリクエストの向き先を変えることもできます。 環境ごとに変数を作成 環境の変え方 Postman の変数にもスコープという概念が存在し、広いものから順に、Global、Collection、Environment、Data、Local となっています。 上記で紹介した環境ごとの変数は Environment 変数になります Postman における Collection と Environment の違いが分かりずらいので補足です。 Collection は API リクエストをグループ化するための機能で使われ、Environment はグルーピングされたリクエスト群に対して環境を変えるために使われることが多い認識です。 変数のスコープ learning.postman.com ここで実際に使われることが多かった 3 つの変数の特徴と実際の使用例について紹介しようと思います。 Global 変数 特徴 Postman 全体で有効な変数 どのコレクションやリクエストからも参照可能で環境に依存しない値を保持するのに適している 実際の使用例 複数コレクション(=システム)で共有する認証情報 Collection 変数 特徴 特定のコレクション内でのみ有効な変数 コレクション内のリクエスト間で値を共有するのに適している 実際の使用例 環境を問わないリクエストパラメータ device や os の バージョン Environment 変数 特徴 特定の環境 (開発環境、テスト環境、本番環境など) のみで有効な変数 API の domain 部分や認証情報などの環境ごとに異なる値を設定するのに適している 実際の使用例 API の domain 部分 認証情報 詳しい説明は 公式ドキュメント をご覧ください。 script Postman では、リクエストの前後に JavaScript のコードを実行できます。 これらはそれぞれ pre-request script 、 post-response script と呼ばれるもので、実行順序は以下の画像のようになっています。 実行順序 試しに前のセクションで設定した環境変数である X-Foods-Token を自動で更新する script を書いてみましょう。手順は以下の通りです。 X-Foods-Token に設定される値を取得(リクエスト先:/signup) レスポンスから token を抽出 抽出した token を X-Foods-Token に設定 X-Foods-Token を自動で更新する post-response script 以下のように pm オブジェクトを通して Postman が提供している機能を使用することができます。 const response = pm . response . json () ; pm . environment . set ( "X-Foods-Token" , response . token ) ; 他にもリクエスト URL、Header、リクエストパラメータなどを動的に生成・変更したり、テストデータを準備するために外部ファイルから読み込むことなどもできます。 また、一つのリクエストだけでなく Collection に対して script を設定することもできるみたいです。Collection 全体で実行したい処理がある場合にはとても便利な機能です。 各scriptの実行順序 詳しい説明は 公式ドキュメント をご覧ください。 Postman へのインポート機能 Postman では cURL 形式のリクエストや OpenAPI 仕様で作成されたドキュメント等の取り込み・管理ができます。 管理できるようになることで簡単に API リクエストの保存・リクエストパラメータの変更ができるようになります。 それでは実際に二つの形式で取り込んでみましょう。 cURL curl --location --request GET 'http:/localhost:8080/foods' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'X-Foods-Token: foods-token' \ 上記のリクエストをエンドポイントを書く場所にコピペしてみましょう。 cURL形式のリクエストを取り込む 正常に import できると下の画像のように cURL リクエストを Postman に取り込むことができます。 リクエスト先、ヘッダー、リクエストパラメータがすべて取り込まれていることがわかります。 cURL形式のリクエストを取り込んだ後 同じように書き出し Postman で管理されてあるリクエストを cURL 形式で書き出したり Go や Rust での書き出しもできるようでした。 OpenAPI 仕様書 sample ファイルは こちら のものを使います。 yaml ファイルを Postman の上にドラッグドロップすると import ができるので、そのまま import を続行してください。 正常に import できると下の画像のように OpenAPI 仕様書を Postman に取り込むことができるかと思います。 OpenAPI 仕様書を取り込んだ後 最後に 今回は Postman についての Tips について紹介させていただきました。 これらの整備をしたから最高に開発しやすくなる!!みたいなことはないと思いますが、普段の業務の 1% くらいは開発しやすくできたのではないでしょうか?この記事が皆様のお役に立てたなら幸いです! 参考 https://www.postman.com/ PS Postman のテーマを変えられるので自分の色に染めたっていい!! テーマ変えられるよ 小学生の頃にハマったゲームの一つにパタポンというものがあります。単純なリズムゲーではあるのですが、キャラクターデザインやサウンドトラック、ゲーム性など色々なポイントが当時の自分に刺さりまくり 時間を溶かした 楽しませてもらった作品です。なんとこの度、完全なる続編ではないのですが手掛けたクリエイターによる新作が今年リリースされるということで震えが止まりません!! ♪ ラッタ!ラッタ!ラッタッタ! kickstarter.ratatan.jp
アバター
こんにちは。 開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 最近はAIプロダクト開発をメインで担当しています。 今回は、Databricks Asset Bundlesを活用して、AIプロダクト開発向けにCI/CDパイプラインを整備した内容をまとめます。 Databricks Asset Bundlesとは Databricks Asset Bundlesは、データやAIプロジェクトでソフトウェア開発におけるソース管理、コードレビュー、テスト、CI/CDなどを導入しやすくするツールです。 簡単に言えば、Databricksの各種リソースをInfrastructure-as-Code(IaC)として管理できます。 ノートブックをはじめとするソースコードやDatabricks上で動かすJobなどのリソースを、ymlファイルで定義できます。 docs.databricks.com 図1: Asset Bundlesを使用した開発およびCI/CDパイプライン 動機 社内の多くのDatabricks用途は、 メダリオンアーキテクチャを踏襲したデータ基盤 です。 例えば、ダッシュボードで参照するデータのETL、A/Bテスト結果の集計、バッチ推論ベースのMLモデルの学習・推論・デプロイなどがあります。 一方でAIプロダクト文脈では、モデルの学習はバッチ処理ですが、推論はリアルタイム処理、モデルのデプロイ( Model Serving の機能を利用)はソースコードを更新したタイミングなことが多いのではないでしょうか。 特にLLMの場合は、OpenAIなどのAPIを利用することが多いため、モデルの学習自体不要です。 つまり、推論とデプロイに重点を置いた開発がメインになります。 この開発・運用上のギャップを埋めたい思いがありました。 これらのギャップを、As-Is(現状)とTo-Be(理想)としてまとめると以下のようになりました。 No. 項目 As-Is To-Be 理由 1 ソースコード管理 pythonノートブック pythonファイル コードの再現性・テストの導入のしやすさを目指し、モジュール単位でコード管理したいため 2 ワークスペース Default workspace(ap-northeast-1)のみ Default workspace(ap-northeast-1)とTest workspace(us-east-1) 推論にLLMを利用することが多く、LLMに対する評価のフィードバックを得ながら開発・運用したいため(us-east-1の方がアップデートが早く、LLMを評価する機能が利用可能) 3 処理タイミング スケジューリングされたバッチ処理 githubのpushをトリガーとした処理 CI/CDパイプラインを用いて、テストの実行、モデルの保存と評価、Model Servingへのデプロイを自動化したいため 全体像 図1を参考に、今回のsample-project用のCI/CDパイプラインを作成しました(図2)。 右上のDatabricks workspacesを一部変更しています。 ワークスペースは2つのregion(ap-northeast-1、us-east-1)を利用します。 Default workspace(ap-northeast-1) Test workspace(us-east-1) 図2: sample-project用のAsset Bundlesを使用した開発およびCI/CDパイプライン コードは以下のような構成で進めます。 sample-project ├── .github │ └── workflows │ ├── _deploy_databricks.yml │ └── sample_project_cd.yml ├── bundles │ ├── resources │ │ └── job_deploy_sample_project_model.yml │ └── targets │ ├── default_databricks.yml │ └── test_databricks.yml ├── src │ ├── sample_project │ │ ├── __init__.py │ │ ├── pipeline.py │ │ ├── rag.py │ │ └── model.py │ └── deploy_model.py ├── tests └── databricks.yml 実装してみる 1. pythonファイルによるソースコード管理 モジュール メイン機能となるモジュール群は src/sample_project/* に置きます。 rag.py RAGの機能を有したモジュールを定義します。 今回は、シンプルなRAGクラスを定義し、OpenAIのAPIを利用して回答を生成するコードを記述しています。 from openai import OpenAI class RAG : def __init__ (self): self.llm = OpenAI() def generate (self, query, contexts): context_str = " \n " .join([f "- {context['content']}" for context in contexts]) messages = [ { "role" : "system" , "content" : "You are a helpful assistant." }, { "role" : "user" , "content" : f "Context: \n {context_str} \n\n Query: {query}" }, ] completion = self.llm.chat.completions.create( model= "gpt-4o-mini" , messages=messages, ) answer = completion.choices[ 0 ].message.content return answer def retrieve (self, query): query_embedding = self.llm.embeddings.create( input =query, model= "text-embedding-3-large" , ) contexts = self.retieve_databricks_vector_store(query_embedding) return contexts def retieve_databricks_vector_store (self, query_embedding): # query_embeddingをもとにベクトル検索したコンテキスト情報を返す # ここではダミーのコンテキスト情報を返す contexts = [ { "doc_uri" : "doc1.txt" , "content" : "In 2013, Spark, a data analytics framework, was open sourced by UC Berkeley's AMPLab." }, { "doc_uri" : "doc2.txt" , "content" : "To convert a Spark DataFrame to Pandas, you can use toPandas()" }, ] return contexts pipeline.py ユーザのクエリを受け取り、LLMの回答を返すパイプラインを定義します。 from sample_project.rag import RAG def generate_answer (query): rag = RAG() contexts = rag.retrieve(query) answer = rag.generate(query, contexts) return answer model.py pipelineを利用するmlflow.pyfunc.PythonModelを継承したクラスを定義します。 このクラスは、モデルの保存時に指定することで、python modelとしてMLflowのモデルとして保存され、Model Servingで利用できるようになります。 import mlflow from sample_project.pipeline import generate_answer class SampleAI (mlflow.pyfunc.PythonModel): def predict (self, context, model_input): query = model_input[ 'query' ][ 0 ] return generate_answer(query) テスト テストは tests/* に置きます。 LLMによる生成が絡む処理のテストは難しいですが、前処理や後処理などのテスト可能なコードに対しては単体テスト書く想定です。 今回のサンプルコードでは前処理や後処理はないですが、要件が複雑化していくと必要になってくるかと思います。 今までの開発では、ノートブックかつDatabricksのコンソール上での開発だったため、モジュール単位の実装やテストコードが書きづらい問題がありました。 しかし、pythonファイルによるモジュール化ができたことで、モジュール単位に実装が容易になりました。 これにより、コーディングや単体テストはローカルで行い、Spark、Mlflow、Unity CatalogなどDatabricksのメイン機能を利用する開発はコンソール上で行う、といった開発フローができるようになりました。 モデルの保存と評価 モデルの保存と評価は deploy_model.py に記述します。 このコードは例外的にノートブックで管理します。 理由としては 将来的に自作のパッケージをinstallするとき、動的に pip install できることが便利なため MLflow Tracing を利用したため などがあります。 色々記述していますが、重要なのは mlflow.start_run() で実行される、モデルの保存と評価コードです。 保存 mlflow.pyfunc.log_model で、モデルの保存をしています。 このとき引数にcode_pathsを指定することで、モデルと一緒に該当のソースコードを保存することができます。 注意点として、 MLflowのドキュメント にある通り、code_pathsは親ディレクトリを見ることができない仕様になっているようです。 そのため、以下のようなディレクトリ構成にし、 code_paths=["sample_project"] を指定しています。 ├── sample_project └── deploy_model.py この問題に関しては、 MLflowのIssue でも言及されており、今後仕様が変わる可能性があります。 評価 mlflow.evaluate で、モデルの評価をしています。 このとき、 model_type="databricks-agent" を指定することで、 Mosaic AI Agent Evaluationに組み込まれているAI審査員機能 (一般的にはLLM as a Judgeと呼ばれる審査員用のLLMがプロダクトのLLMを評価する機能)を利用することができます。 今回は以下のAI審査員を指定しています。 correctness : エージェントの実際の応答がground truth(expected_response)と比較して誤っていないことを保証 relevance_to_query : エージェントの応答が無関係なトピックに逸脱することなくユーザーの入力に直接対処することを保証 safety : エージェントの応答に有害、攻撃的、または有毒な内容が含まれていないことを保証 %load_ext autoreload %autoreload 2 # COMMAND ---------- %pip install --upgrade mlflow cloudpickle databricks-vectorsearch databricks-agents openai tiktoken %restart_python # COMMAND ---------- dbutils.widgets.text( "model_env" , "dev" ) dbutils.widgets.text( "workspace_url" , "https://{sub-domain}.cloud.databricks.com" ) model_env = dbutils.widgets.get( "model_env" ) workspace_url = dbutils.widgets.get( "workspace_url" ) # COMMAND ---------- import os os.environ[ "OPENAI_API_KEY" ] = dbutils.secrets.get(...) os.environ[ "DATABRICKS_VECTOR_SEARCH_HOST" ] = "https://{sub-domain}.cloud.databricks.com" os.environ[ "DATABRICKS_VECTOR_SEARCH_TOKEN" ] = dbutils.secrets.get(...) # COMMAND ---------- import mlflow from sample_project.model import SampleAI # DatabricksのUnity Catalogを利用するための設定 mlflow.set_registry_uri( "databricks-uc" ) # COMMAND ---------- # Unity Catalogで登録するモデル名 model_name = ( "{catalog}.{schema}.sample_project_model" if model_env == "prd" else "{catalog_dev}.{schema}.sample_project_model" ) # 実験管理先を設定 mlflow_experiment_name = '/Shared/experiments/sample_project' mlflow.set_experiment(mlflow_experiment_name) # COMMAND ---------- from mlflow.models import ModelSignature from mlflow.types.schema import Schema, ColSpec input_schema = Schema([ColSpec( "string" , "query" )]) output_schema = Schema([ColSpec( "string" , "answer" )]) signature = ModelSignature(inputs=input_schema, outputs=output_schema) # COMMAND ---------- # https://docs.databricks.com/ja/generative-ai/agent-evaluation/index.html # 今回は評価データは直に書く import pandas as pd eval_df = pd.DataFrame({ "request" : [ { "query" : "What is Spark?" }, { "query" : "How do I convert a Spark DataFrame to Pandas?" } ], "expected_response" : [ "Spark is a data analytics framework." , "To convert a Spark DataFrame to Pandas, you can use the toPandas() method." , ] }) display(eval_df) # COMMAND ---------- # モデルの保存・評価 with mlflow.start_run(): model_info = mlflow.pyfunc.log_model( artifact_path= "model" , python_model=SampleAI(), signature=signature, registered_model_name=model_name, code_paths=[ "sample_project" ], ) # test-databricks環境でのみ評価 if "test" in workspace_url: mlflow.evaluate( model=generate_answer, data=eval_df, model_type= "databricks-agent" , evaluator_config={ "databricks-agent" : { "metrics" : [ "correctness" , "relevance_to_query" , "safety" , ] } } ) 2. 複数のregionに分けてワークスペースを運用 図1をはじめ、Databricksのドキュメントでは、Development、Staging、Productionごとにワークスペースを分けた運用例が多いです。 しかし、弊社では、1つのワークスペースでdev/prdをwidgetsで切り替えて運用しているケースがほとんどです。 例えば、 dbutils.widgets.text( "model_env" , "dev" ) と書くと、model_envという名前のwidgetsが作成され、 model_env = dbutils.widgets.get( "model_env" ) と書くと、model_envの値を取得できます。 以降、model_envの値によって参照するデータソースを変えるなど、処理の分岐をさせることができます。 Databricks Asset Bundlesは、上記のようなケースでも柔軟に対応することができました。 ここでは、冒頭の全体像で述べたようにDefault workspaceとTest workspaceの2つのワークスペースを利用します。 Default workspace : 主要なワークスペースで、regionはap-northeast-1を使用。 Model ServingやVector Storeなどを運用する。 Test workspace : テスト用のワークスペースで、regionはus-east-1を使用。 モデルの保存時に、AI審査員によるLLMの評価を行う。 ここではDevelopment、Staging、Productionを、それぞれ dev 、 stage 、 prd とし、以下のような意味を持つとします。 図2と合わせて参照ください。 dev : 個人開発用。 ワークスペースのName(自分の名前)ディレクトリに反映される。 ローカル から、コードやJobの設定などのリソースをデプロイするときに使用する。 stage : 開発用。 ワークスペースのStagingディレクトリに反映される。 作業ブランチ→developブランチ にpushした時に、CI/CDでコードやJobの設定などのリソースをデプロイするときに使用する。 prd : 本番用。 ワークスペースのProductionディレクトリに反映される。 developブランチ→masterブランチ にpushした時に、CI/CDでコードやJobの設定などのリソースをデプロイするときに使用する。 .databrickscfg ~/.databrickscfg にワークスペースの設定を記述します。 この例では、パーソナルアクセストークン(PAT)の認証方法を利用しています。 databricksの認証に関しての詳細は こちら を参照してください。 [DEFAULT] host = https://{default-databricks-subdomain}.cloud.databricks.com token = dapi11111111111111111111111111111111 [TEST] host = https://{test-databricks-subdomain}.cloud.databricks.com token = dapi22222222222222222222222222222222 databricks.yml Databricks Asset Bundlesの各種設定を databricks.yml に記述します。 ここでは、ymlファイルの見通しをよくするために、resourcesとtargetsを別のファイルに分離しています。 dev 、 stage 、 prd のpathは、それぞれは使いまわしやすいようにvariablesに定義しています。 bundle : name : sample-project variables : dev_file_path : description : "The path to the development" default : /Repos/${workspace.current_user.userName}/${bundle.name} stage_file_path : description : "The path to the staging" default : /Repos/Staging/${bundle.name} prd_file_path : description : "The path to the production" default : /Repos/Production/${bundle.name} run_as : user_name : ${workspace.current_user.userName} include : - "bundle/resources/*.yml" - "bundle/targets/*.yml" resources JobなどのDatabricksのリソースを定義するファイルを resources/*.yml に記述します。 他にも設定できるリソースに関しては こちら を参照してください。 resourcesの設定は全てのtargetsで共通のため、defaultでは dev の設定で記述します。 これらの設定は、targetsの設定で 上書き することができます。 ここでは、モデルの保存と評価を実行する deploy_sample_project_model というJobを定義しています。 このJobを実行することで、モデルの保存と評価をCI/CDのパイプラインに組み込むことができます。 resources : jobs : deploy_sample_project_model : name : deploy_sample_project_model tasks : - task_key : deploy_model notebook_task : notebook_path : ${var.dev_file_path}/src/deploy_model source : WORKSPACE base_parameters : model_env : dev workspace_url : "{{workspace.url}}" job_cluster_key : deploy_model_cluster job_clusters : - job_cluster_key : deploy_model_cluster new_cluster : spark_version : 15.4.x-cpu-ml-scala2.12 aws_attributes : first_on_demand : 0 availability : SPOT zone_id : auto instance_profile_arn : arn:aws:iam::123456789101:instance-profile/databricks_shared-instance-profile spot_bid_price_percent : 100 ebs_volume_count : 0 node_type_id : r7gd.large enable_elastic_disk : false data_security_mode : SINGLE_USER runtime_engine : STANDARD autoscale : min_workers : 1 max_workers : 2 permissions : - group_name : dai-engineer level : CAN_MANAGE queue : enabled : false targets 各環境ごとの設定を targets/*.yml に記述します。 ここでは、Default workspace(default_databricks)と Test workspace(test_databricks)の設定を記述しています。 default_databricks.yml dev 、 stage 、 prd の3つの環境を定義しています。 dev をdefaultに設定し、 stage と prd の設定は必要な箇所を上書きしています。 run_as CI/CD用のサービスプリンシパルを指定 指定するサービスプリンシパルは stage と prd で同様 resources jobの設定を上書き stage では、notebook_pathを開発用に上書き prd では、notebook_pathとbase_parametersを本番用に上書き targets : dev : mode : development default : true workspace : host : https://{default-databricks-subdomain}.cloud.databricks.com file_path : ${var.dev_file_path} stage : mode : production workspace : host : https://{default-databricks-subdomain}.cloud.databricks.com file_path : ${var.stage_file_path} run_as : service_principal_name : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" resources : jobs : deploy_sample_project_model : name : "[${bundle.target}] deploy_sample_project_model" # notebook_pathを開発用に上書き tasks : - task_key : deploy_model notebook_task : notebook_path : ${var.stage_file_path}/src/deploy_model source : WORKSPACE prd : mode : production workspace : host : https://{default-databricks-subdomain}.cloud.databricks.com file_path : ${var.prd_file_path} run_as : service_principal_name : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" resources : jobs : deploy_sample_project_model : name : deploy_sample_project_model # notebook_pathとbase_parametersを本番用に上書き tasks : - task_key : deploy_model notebook_task : notebook_path : ${var.prd_file_path}/src/deploy_model source : WORKSPACE base_parameters : model_env : prd workspace_url : "{{workspace.url}}" test_databricks.yml default_databricks.yml とほとんど同じ設定です。 Test workspace固有の設定をする場合、こちらに記述します。 targets : test-dev : mode : development workspace : host : https://{test-databricks-subdomain}.cloud.databricks.com file_path : ${var.dev_file_path} test-stage : mode : production workspace : host : https://{test-databricks-subdomain}.cloud.databricks.com file_path : ${var.stage_file_path} run_as : service_principal_name : "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" resources : jobs : deploy_sample_project_model : # notebook_pathを開発用に上書き name : "[${bundle.target}] deploy_sample_project_model" tasks : - task_key : deploy_model notebook_task : notebook_path : ${var.stage_file_path}/src/deploy_model source : WORKSPACE test-prd : mode : production workspace : host : https://{test-databricks-subdomain}.cloud.databricks.com file_path : ${var.prd_file_path} run_as : service_principal_name : "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" resources : jobs : deploy_sample_project_model : # notebook_pathとbase_parametersを本番用に上書き name : deploy_sample_project_model tasks : - task_key : deploy_model notebook_task : notebook_path : ${var.prd_file_path}/src/deploy_model source : WORKSPACE base_parameters : model_env : prd workspace_url : "{{workspace.url}}" 3. pushのタイミングで処理を実行 sample_project_cd.yml CI/CDのパイプラインをGithub Actionsで sample_project_cd.yml に記述します(テストなどは sample_projet_ci.yml を追加想定)。 このパイプラインは、developブランチとmasterブランチに、それぞれpushされたタイミングで処理します。 dev は個人開発用のため、CI/CDのパイプラインでは stage と prd のみ記述します。 name : CD sample-project on : push : branches : - master - develop jobs : deploy_test_stage : if : ${{ github.ref_name == 'develop' }} uses : ./.github/workflows/_deploy_databricks.yml with : targets : test-stage deploy_model_name : deploy_sample_project_model secrets : DATABRICKS_CLIENT_ID : ${{ secrets.TEST_DATABRICKS_CLIENT_ID }} DATABRICKS_SECRET : ${{ secrets.TEST_DATABRICKS_SECRET }} deploy_stage : if : ${{ github.ref_name == 'develop' }} uses : ./.github/workflows/_deploy_databricks.yml with : targets : stage deploy_model_name : deploy_sample_project_model secrets : DATABRICKS_CLIENT_ID : ${{ secrets.DEFAULT_DATABRICKS_CLIENT_ID }} DATABRICKS_SECRET : ${{ secrets.DEFAULT_DATABRICKS_SECRET }} deploy_test_prd : if : ${{ github.ref_name == 'master' }} uses : ./.github/workflows/_deploy_databricks.yml with : targets : test-prd deploy_model_name : deploy_sample_project_model secrets : DATABRICKS_CLIENT_ID : ${{ secrets.TEST_DATABRICKS_CLIENT_ID }} DATABRICKS_SECRET : ${{ secrets.TEST_DATABRICKS_SECRET }} deploy_prd : if : ${{ github.ref_name == 'master' }} uses : ./.github/workflows/_deploy_databricks.yml with : targets : prd deploy_model_name : deploy_sample_project_model secrets : DATABRICKS_CLIENT_ID : ${{ secrets.DEFAULT_DATABRICKS_CLIENT_ID }} DATABRICKS_SECRET : ${{ secrets.DEFAULT_DATABRICKS_SECRET }} _deploy_databricks.yml sample_project_cd.yml で利用するデプロイのパイプラインを _deploy_databricks.yml に記述します。 このパイプラインは、 databricks/setup-cli を利用し、Databricks Asset Bundleを使ってリソースをデプロイします。 databricks bundle deploy でリソースをデプロイし、 databricks bundle run でデプロイしたコードをJobとして実行します。 name : deploy databricks on : workflow_call : inputs : targets : required : true type : string deploy_model_name : required : true type : string secrets : DATABRICKS_CLIENT_ID : required : true # service_principal_nameと同じ DATABRICKS_SECRET : required : true jobs : deploy_resources : name : "Deploy resources" runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 - uses : databricks/setup-cli@main - run : databricks bundle deploy -t ${{ inputs.targets }} working-directory : . env : DATABRICKS_CLIENT_ID : ${{ secrets.DATABRICKS_CLIENT_ID }} DATABRICKS_CLIENT_SECRET : ${{ secrets.DATABRICKS_SECRET }} deploy_model : name : "Deploy model" runs-on : ubuntu-latest needs : - deploy_code steps : - uses : actions/checkout@v4 - uses : databricks/setup-cli@main - run : databricks bundle run ${{ inputs.deploy_model_name }} -t ${{ inputs.targets }} working-directory : . env : DATABRICKS_CLIENT_ID : ${{ secrets.DATABRICKS_CLIENT_ID }} DATABRICKS_CLIENT_SECRET : ${{ secrets.DATABRICKS_SECRET }} 結果 Jobの実行結果 test_databricks.ymlの設定を利用する場合は、targetsには test-dev 、 test-stage 、 test-prd などを指定します。 test-dev はローカルから、 test-stage と test-prd はGithub Actionsから実行します。 そのため、Created byを見ると、 test-dev のみユーザ名であり、 test-stage と test-prd はCI/CDのサービスプリンシパル名であることが確認できます(図3)。 図3: 作成されたJob一覧 Jobの画面に進むと実行結果も確認できます(図4)。 resourcesに記述した各種設定内容もここで確認できます。 正直、Jobの設定をコード管理できるという時点で感無量です。 図4: deploy_sample_project_modelのJob設定画面 モデルの評価の結果 モデルの評価結果は、 mlflow.evaluate で指定したAI審査員によって判定されます。 今回評価データとして設定した、「What is Spark?」と「How do I convert a Spark DataFrame to Pandas?」は、contextに含まれるため、AI審査員をPassすることができました(図5)。 図5: AI審査員評価 AI審査員の評価結果の詳細は以下のとおりです(図6)。 Responseに書かれているModel outputとExpected outputの文章は一致していなくても、回答の文脈として合っているかで評価されていることがわかります。 Correct 期待される回答は「Sparkはデータ分析フレームワークである」と述べている。 回答はSparkを「暗黙のデータ並列性とフォールトトレランスを備えたクラスタ全体をプログラミングするためのインタフェースを提供するオープンソースのデータ処理フレームワーク」と説明している。 回答では「データ分析フレームワーク」という用語は明確に使われていないが、SQLクエリ、機械学習、グラフ処理、ストリーム処理などのタスクを含むデータ処理のために設計されたフレームワークとしてSparkを説明している。 これらのタスクは一般的にデータ分析に関連している。したがって、期待される回答がサポートされている。 Relevant 質問は「Sparkとは何か?回答は、Sparkの目的、起源、特徴、サポートされているプログラミング言語など、Sparkとは何かについて詳しく説明している。 解答のすべての部分が、Sparkとは何かを理解するのに関連している。 Safe 回答に有害なコンテンツが検出されない 図6: AI審査員評価詳細 ここで、contextになく、gpt-4o-miniの学習データに含まれない、最新の情報を問う質問を評価データに加えてみることにします。 今回は、巷で話題の2025/02/02に発表された OpenAIのDeep research に関して質問してみます。 ground truthであるexpected_responseの文章は、発表時の文章をgpt-4oで要約させて作成しました。 eval_df = pd.DataFrame({ "request" : [ { "query" : "What is Spark?" }, { "query" : "How do I convert a Spark DataFrame to Pandas?" }, { "query" : "What is Deep research" }, ], "expected_response" : [ "Spark is a data analytics framework." , "To convert a Spark DataFrame to Pandas, you can use the toPandas() method." , "Deep research is a ChatGPT feature that autonomously conducts multi-step web research, generating detailed, cited reports in minutes. It’s ideal for in-depth inquiries, leveraging OpenAI’s o3 model for advanced analysis and synthesis." , ] }) 図7からわかるとおり「What Is Deep research」というクエリで、CorrectnessがFailとなりました。 「Deep research」を「深掘った調査」という意味合いとして回答してしまっています。 図7: Deep researchに関して質問した場合のAI審査員評価 Deep researchに関して質問した場合のAI審査員の評価結果の詳細は以下のとおりです(図8)。 なぜFailとなったかを説明してくれています。 Incorrent Deep researchはChatGPTの機能で、多段階のweb調査を自律的に行い、詳細な引用レポートを数分で作成する。 OpenAIのo3モデルを活用し、高度な分析と合成を行うため、詳細な調査に最適である。 この回答では、「Deep research」とは、広範なデータ収集と分析、批判的思考、情報の統合を含む、特定の分野における徹底的かつ集中的な調査または研究であると説明している。 これには、包括的な文献調査、方法論の厳密さ、革新的な問題解決、学際的アプローチ、縦断的研究、知識への貢献などが含まれる。 この回答には、ChatGPT、自律的なweb調査、数分でレポートを作成すること、OpenAIのo3モデルの活用については何も言及されていない。したがって、期待される回答は回答によってサポートされていない。 Relevant 設問は「Deep research」について尋ねており、回答は、包括的な文献レビュー、方法論の厳密さ、革新的な問題解決、学際的アプローチ、縦断的研究、知識への貢献といった側面を含む、深い研究とは何かを詳細に説明している。 これらの点はすべて、深い研究とは何かを理解するのに関連している。 Safe 回答に有害なコンテンツは検出されない 図8: Deep researchに関して質問した場合のAI審査員評価詳細 なお、RelevanceやSafetyをPassしているのは、expected_responseと比較して評価していないためです。 「Deep research」を「深掘った調査」と回答したとしても、質問の回答としては関連性がある答えのため、RelevanceをPassしているのだと考えられます。 おわりに Databricks Asset Bundlesを使って、モデルの保存と評価をCI/CDパイプラインとして組み込むまでの一連の流れを紹介しました。 今回はモデルのデプロイの自動化に関しては取り組めませんでした。 ここも、Model Servingへのデプロイ処理を deploy_model_serving.py のように作成し、Jobに設定を追加するようにすれば、同様のパイプラインを組み込むことができると思います。 モデルの評価では、AI審査員について紹介しました。 個人的に、このLLMの評価を実運用に乗せるには、各AI審査員がどのような評価しているかの特性に対して理解が必要だと感じました。 他にもretrieval観点で評価するAI審査員もおり、LLMの評価には様々な観点を考える必要がありそうです。 LLMの評価に関しては、また別の機会にまとめたいと思います。 また、今回は日本語で評価しませんでしたが、日本語でも概ね精度は変わらない印象です。 ただ、AI審査員の回答は英語のため、Pass/Failとなった説明に日本語と英語が入り混じることになり、文章として読みづらい感は否めません。 ap-northeast-1のパブリックプレビューと同時に、日本語で回答するAI審査員の登場に期待です。
アバター
はじめに Android 開発エンジニアを担当している岡田です。 弊社のサービスであるヘルシカにて、Material You Design のアイコンを実装しました。 今回は Material You Design とアイコンの実装についてご紹介したいと思います。 Material You Design Android 12 から導入された Material You Design は、Android 端末の画面を自分好みの色合いやデザインに変える機能です。 2014 年に発表した Material Design をさらにアップデートし、機能性だけでなく、ユーザーがスマホをより自分らしくカスタマイズできるようになりました。 壁紙の色を基調としたカラーパレットが自動的に生成され、その中から配色を選ぶことで、手軽に自分好みのカスタマイズが可能です。 ウィジェットやアイコン、クイック設定など OS 側のアイテムはもちろん、アプリ内でも自分で選択した色を取得して使用することができます。 Material You Design の 3 つのテーマ Material You Design には 3 つのテーマがあります。 Personal for every style (あらゆるスタイルに対応するパーソナルなデザイン) ユーザーが壁紙やテーマカラーを選択すると、それに基づいてアプリのUI全体が動的に変化し、ユーザーは自分だけのオリジナルなデザイン体験を楽しむことができます。 Alive & Adaptive for every screen (あらゆる画面に対応し、あらゆるデバイスにフィットするデザイン) レスポンシブデザインをさらに進化させ、様々な画面サイズやデバイスに最適なレイアウトを自動的に生成します。 ユーザーはどのデバイスを使っても、快適な操作性を体験できます。 Accessible for every need (あらゆるニーズに対応するアクセシブルなデザイン) ユーザーの視覚、聴覚、運動能力など、様々なニーズに対応したアクセシビリティ機能を提供します。 より多くの人がデジタルの世界に参加し、情報やサービスを利用できるようにします。 つまり、Material You Design は 多様性を担保した新しいデザインスタイル です。 Material You Design の活用例 Google アプリ Google の多くのアプリが、Material You Design を採用することで、より洗練されたユーザーインターフェースを実現しています。 Material You Design は、単なるデザインシステムにとどまらず、ユーザーとデバイスの関係性を再定義する新しいパラダイムと言えるでしょう。 以下は Google 公式がリリースしている電卓アプリのスクリーンショットです。 引用元: Material You とは? スマホのホーム画面などを自分好みにカスタマイズしよう 著作権者:Google LLC Google Pixel スマートフォン Material You Designは、Google Pixel スマートフォンで最も顕著に見ることができます。 壁紙の色調がシステム全体の色調に反映されるなど、パーソナライゼーションが特徴です。 自身の端末 (端末:Pixel6a / OS:Android14) で試した見たところ、[設定 > 壁紙とスタイル] から簡単に設定できました。 ウィジェットや対応済みのアプリアイコン、電卓アプリでのテーマの変化を確認できます。 設定 > 壁紙とスタイル Home画面 電卓アプリ 対応していないアプリアイコンは Home 画面で浮いてしまう可能性 Material You Design でアイコンの色を変える「テーマアイコン」に対応していないアプリは、Home 画面で浮いてしまう可能性があります。 現時点でこの機能を仕様しているユーザーに、Home にアイコンを置くことを避けられる可能性があるということです。 例えば上記の Home 画面の画像は、意図的に Material You Design 対応を外したヘルシカのアイコンが写っています。ヘルシカのみ真っ白のアイコンなため、画面内で浮いていることがわかると思います。 アプリの Style までとはいかずとも、アプリアイコンのみでも Material You Design に対応しておきたいです。 では Material You Design にアイコンを対応させるには、どのような実装をすれば良いのでしょうか。 Material You Design のアイコン対応 ic_launcher.xml に、 monochrome を追加します。 <? xml version = "1.0" encoding = "utf-8" ?> <adaptive-icon xmlns : android = "http://schemas.android.com/apk/res/android" > <background android : drawable = "@drawable/ic_app_background_icon" /> <foreground android : drawable = "@drawable/ic_app_foreground_icon" /> <monochrome android : drawable = "@drawable/ic_app_monochrome_icon" /> </adaptive-icon> アダプティブアイコンに対応していない場合は、先に対応する必要があるのでそこに注意です。 ヘルシカは Material You Design のアイコンに対応しました。 とても簡単に実装できるので、まだ対応していない場合は是非対応してみてください。 参考記事 https://www.android.com/intl/ja_jp/articles/322/#section-1 https://m3.material.io/blog/announcing-material-you https://developer.android.com/develop/ui/views/launch/icon_design_adaptive?hl=ja
アバター
はじめに CTO室 Dev Enable グループの羽馬( X: @naohaba70 )です。 昨年は私たちにとって、初めて「カンファレンス運営」に挑戦した年でした。 初めて協賛や運営に取り組む中で、成功の喜びや予想外の課題に直面し、多くの学びを得ることができました。この経験は、チームにとって 大きな成長の一歩 となり、今後の活動の土台を築くものになったと感じています。 この記事では、そんな 「カンファレンス運営1年目」 を振り返り、成功や課題、そしてこれからの展望についてお話しします。特にこの記事が参考になればと思うのは、次のような方々です: 社内で技術広報を行っている方 これからカンファレンス運営に挑戦してみたい方 カンファレンス運営のリアルな裏側をお届けします!少しでも皆さんの挑戦のヒントになれば嬉しいです。 スポンサー活動の背景や、カンファレンスブースの取り組みについては、以下の記事で詳しく解説しています。 tech.every.tv 昨年のカンファレンス協賛 昨年は、次のカンファレンスに協賛を行いました: Go Conference 2024 Kotlin Fest 2024 Vue Fes Japan 2024 ISUCON14 PHP Conference Japan 2024 それぞれのカンファレンスについての参加レポートは、以下のリンクからご覧いただけます: カンファレンススポンサーブログまとめ ISUCON 14 に ISUポンサーの枠で出場しました ISUCON 14感想戦 その中でも、特に印象に残ったのは「Go Conference 2024」と「Vue Fes Japan 2024」です。 印象に残った2つのカンファレンス Go Conference 2024:初めての協賛、いきなり最高位スポンサーに挑戦 Go Conference 2024は、私たちの会社にとって初めて協賛したカンファレンスでした。この協賛枠は 抽選制 で、実際に応募したときは「当選しなければ来年改めて挑戦しよう」と思っていたのですが、 運よく当選! しかも最高位の「プラチナGoルドスポンサー」枠でした。 正直、初めての協賛で最高位スポンサーになるのは かなりの挑戦 でした。ブーススペースは広かったものの、「どうすれば来場者に楽しんでもらえるか」をゼロから考える必要がありました。試行錯誤しながら、次のような取り組みを行いました: デザイナーさんに依頼し、目を引くブースデザインを作成 広報や人事と連携して、発注や当日の運営をサポート エンジニア有志がブース運営を担当し、直接来場者と交流 さらに、新しい挑戦として アフターイベント も開催しました。このイベントでは、ブースに来ていただいた方々や他の参加者と技術について深いディスカッションが行われました。また、協賛企業同士の交流も深まり、今後につながる良い出会いが多くありました。 every.connpass.com every.connpass.com 結果的に、初めての協賛ながら多くの来場者と直接対話する機会を得て、ポジティブなフィードバックを多数いただきました。「挑戦してよかった」と心から感じた瞬間です。 Vue Fes Japan 2024:経験を活かした新たな挑戦 Vue Fes Japan 2024は、会社として3回目のカンファレンス協賛でした。これまでの経験を活かし、企画から運営までスムーズに進行することができました。 今回も新しい試みとして、 他の協賛企業との「プレイベント」や「アフターイベント」 を開催しました。特にアフターイベントでは、エンジニア同士が直接つながりを深める場を提供でき、多くの参加者から「こういう交流の場がもっと増えると嬉しい」といった声をいただきました。 optim.connpass.com studist.connpass.com さらに、これまで蓄積してきた知見を活かし、役割分担や運営の効率化が進み、チーム全体に余裕が生まれたのも大きな収穫です。初回の頃と比べてスムーズに準備を進めることができ、各担当が自信を持って運営に取り組めたことが印象的でした。 成功と課題を振り返って この1年間を振り返り、私たちは以下のことを実感しました: 社内の連携が成功の鍵 多くの部署やメンバーが関わることで、大きなプロジェクトも成功させることができました。 経験の蓄積が次の挑戦を支える 初回協賛で得た知見が、次の協賛をよりスムーズで効果的なものにしてくれました。 新しい挑戦が成長を生む アフターイベントやプレイベントといった試みが、コミュニティに大きな価値を提供する結果につながりました。 これからの展望 私たちは、今年以降も技術コミュニティへの貢献を続けていきます。カンファレンス協賛だけでなく、エンジニア同士が交流し、学び合える新しい取り組みにも積極的に挑戦していきたいと考えています。 この記事が、カンファレンス運営や技術広報に挑戦してみたい方々の参考になれば幸いです。これからも学びを重ね、挑戦を続けていきます!
アバター
この記事は every Tech Blog Advent Calendar 2024 の25日目、 締めくくりの投稿となります。 今年も無事完走できたことを大変嬉しく思います。 CTOの @imakei_ です。 本稿では、2024年における当社の技術的進化と組織的成長を振り返りつつ、2025年に向けた展望をお伝えしたいと思います。 昨年の 振り返り と併せてご覧いただくことで、この1年間の変遷をより立体的にご理解いただけるかと思います。 2024年の主要な取り組み 本年は特に以下の3つの施策を重点的に推進してまいりました。 DevEnableグループの創設と技術カンファレンスへの参画 「社内外から憧れられる開発組織の実現」 というビジョンのもと、 DevEnableグループを創設し、技術コミュニティへの積極的な貢献を開始しました。 2023年に試験的に発足した組織活性化委員会を、正式な社内組織として再構築し、DevEnableグループとして本格始動させました。 詳細は こちらの記事 をご参照ください。 既存の施策である挑戦WEEK、技術勉強会、TechBlogの取り組みを継続しつつ、 新たな試みとして技術カンファレンスへのスポンサーシップを展開しました。 当社として初めての試みであり、社内でもカンファレンス運営の経験者が限られる中、 GoConference、KotlinFest、VueFes、ISUCON、PHPConferenceという 主要な技術カンファレンス5件への協賛を実現できました。 これにより、長年お世話になってきた技術コミュニティへの恩返しの第一歩を踏み出せたと考えています。 そのほか、 新卒研修の実施 、 内定者向けの研修(勉強会) の実施 などより社内の開発組織を活性化する施策も続々と実施しており、 立ち上げて1年経っていない中ですでに弊社のエンジニア組織の文化を形成する良い組織となっています。 生成AIを活用したサービスのリリース プロダクト面では、弊社として生成AI元年とも言える年となりました。 デリッシュAIのリリース はもちろん、社内向けにも生成AIを活用したサービスをリリースしたり、 PoCやツールの活用程度にとどまっていた生成AIを活用し、プロダクトまで昇華させることができたのは大きな成果でした。 また、来年以降のロードマップでもすでに生成AIを活用したサービスの開発が控えており、 これからさらに開発を加速していきたいです。 弊社の生成AI活用の変遷としては、昨年3月にChatGPTにてGPT-4が有償でリリースされた直後に、エンジニア全員にその利用の補助を出すことを決めたところから、GithubCopilotの導入や、AWS様とのBedrock勉強会、挑戦WEEKでの活用など地道に社内に浸透させてきました。 なかなか大々的に発表できるまで行かないことに自分としてはもどかしさを感じる部分も多く、 他社のCTOの方々にいろいろ相談させてもらった部分もありましたが、 こうしてリリースまで持っていけたのはチームとして生成AIという未来に向き合ったからだと思います。 リテールハブ開発部の発足とデリッシュキッチン開発部長の交代 ブログ でも書いた通り、10/1でリテールハブ開発部を新設し、そこの開発部長を私が務めることになりました。 リテールハブ開発部は、これまでデリッシュキッチン開発部で手掛けてきた小売向けのサービス開発に注力をする新しい部門となります。 一方これまで務めてきたデリッシュキッチン開発部長には、2019年新卒の村上に引き継ぐことができました。 組織としてより一層強くなれたこと、また何より新卒が成長し要職になっていただけたことは、 挑戦 や 成長 を掲げる開発組織としてとても嬉しく思います。 RetailHub、デリッシュキッチン共に、さらに成長を加速していきたいです。 2025年に向けて 2024年は、DevEnable・生成AI・新組織など新しい一歩を踏み出した1年でした。 2025年は、それらをさらに加速すると共に、成果にこだわる1年にしたいと思います。 特に生成AIについては、今後のデファクトを自分たちから作り出していく心づもりで、 全事業部でのプロダクト導入を目指していきます。 最後に、昨年のブログで今年のテーマとしていた「採用」についても、まだまだ全然足りていません! ちょっとでも面白そうと思ってくれた方は、 ぜひ下記からご応募いただくか、X(旧Twitter)でも気軽に声かけてください! 美味しいご飯でも食べながら弊社のプロダクトや開発組織を語りましょう! Tweets by imakei_
アバター
サンタさんへ、MySQL5.7から8.4にアップグレードしてください この記事は every Tech Blog Advent Calendar 2024 の 24 日目の記事です。 背景  こんにちは、開発本部 RetailHUB開発部 NetSuperグループに所属するフルスタックエンジニアをやらせていただいています、ホーク🦅アイ👁️です。 リテールハブ事業部  私の所属しているリテールハブ事業部の話をもう少し詳細にしてみようと思います。  ネットスーパーシステムは事業継承したので弊社が作ったものではなく、10年以上も前に作られたものがベースになっています。CentOSサーバ内にPHP5.3とMySQL5.6,5.7のLAMP環境で構成されており、クラウドもAWSではなくFJcloud-V(旧ニフクラ)を使っているという一癖も二癖もあるインフラ環境で保守運用がしづらい状況にあります。本チームではようやく、PHPからGoへのリプレイス計画を発進させたばかりです。  一方でGraphQL+Pythonで動くものやREST+Goで動くもの、REST+Laravelで動くものもありインフラ環境としてはそれらは全てAWSで構築されているのでモダンな部分もあります。そういった複数言語・インフラ環境をエンジニア全員でメンテナンスしていくスタイルを取っているので自ずとフルスタックスキルを発揮することになリます。アプリはクロスプラットフォーム対応したFlutterアプリで開発制作しているのですがここだけは現在専属エンジニアに頼っておりC/S面でのフルスタックは発揮できていないのですが、来年から専属ではなくなるので今からチーム内でFlutter勉強会を開催してスキル習得を目指しています。  また、プロダクト向き合いが身近で声が通りやすいので企画段階から関わることもでき技術選定もできる用意があるので既存の開発環境に固執せず最適なアーキテクチャや新技術・ツールを使う チャンス があります。  私というと普段の業務は本当に幅広く、既存システムの追加開発に始まり小売様からの問い合わせ対応、障害調査&火消し作業、アラート監視、ビジネスサイドの方々とのプロダクト改善・利益増のための施策・企画会議への参加などあります。レガシーかつ機能豊富な巨大システムであるため障害も起きてしまうことがあります。地味にトラブルの迅速対応(調査・解決など)推進力というのは経験豊富なエンジニアでないと発揮しづらい能力なのかなとは思うのでこのあたりの漠然としたスキルをうまく布教できると良いなと最近は思っています。  最近のトピックとしては、2025年4月より導入の義務化となっている3Dセキュア2.0認証にいち早く対応させました。2025年1月以降順次導入小売様よりスタート予定です。  さて、実際にリプレイスを行うにあたりいずれ必要になるであろう作業としてMySQLのアップグレードがあります。これは単純に2024年12月現在、AWSではMySQL5.7系のRDS、AuroraがEOLを迎えているからです。なのでまずは今のインフラ構成のままでMySQL8系にアップグレードしてみたらどうなるのか、案外簡単にできるのか?!を試してみたので以降で話させていただきます。 MySQL8.4について 公式MySQLが8.4をLTSに指定 AWS Aurora次期メジャーバージョン互換対象の可能性が高いのではないか  こういった点から、現在AWSがサポート中のMySQL8.0系ではなくいきなり8.4にあげてみることにしました。 MySQL5.7利用中のシステムに導入してみる  前提としていきなり本番環境に適用はできないので開発用に手元でDockerコンテナを利用したローカル開発環境があるのでそれを使ってアップグレード作業を行います。また、MySQL8.4サーバ設定ファイルは一旦デフォルトのままにします。 DBバックアップとリストア  まず、MySQL5.7側のデータを全てダンプします *1 。 # on MySQL 5.7 $ mysqldump --single-transaction --skip-lock-tables -p -h127.0.0.1 -P33306 -uappuser -B app_test > app_test_dump20241224.sql  次に、MySQL8.4側に先ほどのデータを全てリストアします。 # on MySQL 8.4 $ mysql -p -h127.0.0.1 -P33308 -uappuser < app_test_dump20241224.sql 問題1  ここで、MySQL8.4にリストアすると以下のようなエラーメッセージが出てしまいました。 ERROR 1118 (42000) at line 1224: Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline.  ここで 解決策 を色々調べてみると先ず、MySQL8.0からinnodb_file_formatが廃止され内部的にBarracuda固定になったようなのでその設定変更は不要でしたのでこれは原因ではなかったようです。次に、ダンプファイルの中身を実際に見てみるとROW_FORMAT=COMPACTになっているCREATE文が32個あったのでROW_FORMAT=COMPRESSEDに全置換しました。そして再度リストアコマンド実行したところ今度はエラーが出ずに成功しました! 参考) 問題2  システムとデータが入ったMySQL8.4を接続してシステムが動作するかいざ試そうとすると、今度は以下のエラーが出て接続失敗となりました。 Client does not support authentication protocol requested by server; consider upgrading MySQL client  どうやら、MySQL8.0からデフォルトになった認証プラグインcaching_sha2_passwordに対応できてないエラーでした。この認証プラグインは結構大きな落とし穴でMySQL8.4では設定ファイルで有効化すれば使うことができました。また、MySQL9.0以上になると完全にmysql_native_passwordプラグインが廃止となって使えなくなります。  そこで、 解決策 としてデフォルト設定だったmy.cnfに以下を追加して再起動しました。 [mysqld] mysql_native_password=on authentication_policy=mysql_native_password,,  再起動後に以下のSQLクエリで確認するとACTIVEに変わっており有効化に成功したのでさらに、pluginカラム値を変更します。この時、パスワード変更もしなければならないので注意です。BY以下を省略してしまうと空文字列のパスワードになってしまいます。また、 FLUSH PRIVILEGES を実行しないとプラグイン変更が反映されないのでこれも注意です、このせいで私もさらに詰まっていました。 mysql> SHOW PLUGINS; 'mysql_native_password','ACTIVE','AUTHENTICATION',NULL,'GPL' mysql> SELECT user, host, plugin FROM mysql.user WHERE user='appuser'; +---------+------+-----------------------+ | user | host | plugin | +---------+------+-----------------------+ | appuser | % | caching_sha2_password | +---------+------+-----------------------+ 1 row in set (0.00 sec) mysql> ALTER USER 'sha2user'@'%' identified WITH mysql_native_password BY '[新しいパスワード]'; +---------+------+-----------------------+ | user | host | plugin | +---------+------+-----------------------+ | appuser | % | mysql_native_password | +---------+------+-----------------------+ 1 row in set (0.00 sec) mysql> FLUSH PRIVILEGES; 問題3  ここまででようやく接続までができたので今度こそクエリ発行をしてみようとネットスーパーユーザ向けTOPページにアクセスしてみたら今後は以下のエラーが出てページ表示ができませんでした。 General error: 1525 Incorrect DATETIME value: '0000-00-00 00:00:00'  調べていくとSQLモードのデフォルト設定を変更しなければならないことがわかりました。 The default SQL mode in MySQL 8.4 includes these modes: ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, and NO_ENGINE_SUBSTITUTION. 参考)  上の引用文から8.4ではsql_modeのデフォルト値は6つあり厳密モードが有効化されていたので起きたエラーでした。したがって、 解決策 として権限が一番緩い設定にしました *2 。 参考) 参考)  解決策としてmy.cnfに以下の設定を追加して再起動するとエラーが出なくなりました。 [mysqld] sql_mode=NO_ENGINE_SUBSTITUTION 問題4  今度こそTOPページを表示させる!という意気込みでページのリロードを試みましたが、残念またもエラーが出てしまいました。 Error Code: 1525. Incorrect DATE value: '--15'   --15 は、PHP側のバグでした。以下のコード例のように日付を年、月、日という変数から生成している箇所があり、そこで年と月の変数値が空文字列になっていたのでこのような文字列が生成されてその後date変数値が日付前提で後続処理していたのでそこでコケていたのでした。 $date = sprintf(“%s-%s-%s”, $year, $month, $day);  具体的には、MySQL側にSELECT文発行する際のWHERE条件句右辺にこのdate変数をそのまま渡しておりそこでコケると出るエラーでした。MySQL8.0.16以降はDATE型の比較演算対象の型チェックが厳密になったらしいです。一方でそれ以前のバージョンだったMySQL5.7の既存システムではこのバグ状態の変な文字列を右辺に渡していても正常クエリのレスポンスが返ってきていた *3 と思うとゾッとしますね...後は、PHPのバグを修正して正しいクエリを発行させたら解決です。 参考)  これでようやくTOPページが表示されました! 総括  本記事では、AWSリプレイス前段階としてMySQLサーバのバージョン5.7からバージョン8.4への挑戦について話しました。弊社提供のネットスーパーシステムは多くの機能を持つのですが、現状ではTOPページを表示させることができたに過ぎず今回のようないくつかの問題はまだまだ氷山の一角に過ぎないと予測しておりこれから長い検証を行っていかなければならないと思っています。例えば、 SQL_CALC_FOUND_ROWS というクエリ発行が散在しているのでこれらを全て COUNT() 関数に置換していくというコードリファクタリングが見えていたりします *4 。なお、代替案の COUNT() 関数で処理速度が遅くなるバグが報告されており、バージョン8.0.37以降で改善されたそうです。 参考)  今後は、2024年11月21日に正式版がリリースされたAmazon RDS for MySQL8.4で動作検証できた時のお話ができればと思いつつ、これにて本記事の結びとさせていただきます。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv Merry X'mas!🎄 六本木一丁目駅降りて直ぐの六本木グランドタワー駅前広場のイルミネーションツリー *1 : コードブロックにあるコマンド類は本記事用に適当な名称やポート番号などに書き換えています *2 : この点、後で気付いたのですがMySQL5.7でもデフォルトだと厳密モード有効なのでシステム利用の設定ファイルには既に無効化設定が入っていました。よって、8.4だから起きるエラーではないです *3 : もちろん無条件として解釈されます *4 : MySQL8.0.17以降非推奨となっていますが、8.4.3現在でも発行自体は可能でした
アバター
はじめに この記事は every Tech Blog Advent Calendar 2024 の 23 日目の記事です。 こんにちは、開発本部のデータ&AIチームの24新卒の蜜澤です。 4月に入社して以降Amazon QuickSight(以下quicksight)を社内で最も利用したので、quicksightで開発をしてきた中で身につけたTipsを紹介します! 本記事の内容はquicksightの細かな機能について触れるので、quicksightを使い慣れている方に向けた内容となっております。 使用する模擬データ レシピ動画サービスのレシピ毎のインプレッション数とクリック数の模擬データを使用します。 模擬データなので簡略化のために4~6月の各月の1~7日のデータのみになっております。 date:日付(2024/04/01~2024/04/07,2024/05/01~2024/05/07,2024/06/01~2024/06/07) recipe:レシピ名(ハンバーグ・からあげ・生姜焼き) click:ある画面でレシピ動画のサムネイルがクリックされた回数 impression:レシピ動画が表示された回数 ※dateに関してはquicksightにデータを入れる際にデフォルトで「Apr 1, 2024 12:00am」という形式になってしまうので、表示形式を「YYYY/MM/DD」に変更しています。 さらにこのデータを月毎に集計した以下のデータも使用します。 date:日付(2024/04~2024/06) recipe:レシピ名(ハンバーグ・からあげ・生姜焼き) click:ある画面でレシピ動画のサムネイルがクリックされた回数 impression:レシピ動画が表示された回数 YYYY/MM形式の日付データを絞り込む際のハマりどころとその対処法 まず、日付を絞り込むために開始日と終了日を指定するStartDateとEndDateというパラメータを作成し、dateカラムに対してStartDateとEndDateの間というフィルターを追加します。 さらに、StartDateは開始日、EndDateは終了日という名前でコントロールを作成しておきます。 パラメータやフィルターに関しての説明は本記事の範疇ではないため説明を省きます。 私が以前執筆した こちらのブログ で解説しているので、パラメータやフィルターに関して詳しく知りたい方はご覧ください。 以下のようにコントロールで指定した範囲の日付のデータのみが表示されます。 日付の集計粒度が「日」の場合はこのやり方で問題ないのですが、集計粒度が「月」になると想定外の挙動になる可能性があります。 月毎集計したデータの開始日を2024/04/01、終了日を2024/05/31にすると、以下のように4月と5月のデータが表示されて問題ありません。 次に開始日を2024/04/02に変更すると、5月のデータのみしか表示されません。 なぜこのようなことが起こるのかというと、冒頭の模擬データの説明でも少し触れましたが、quicksightでは日付データ(Date型のデータ)は「Apr 1, 2024 12:00am」という形式になってしまいます。 2024/04という月のデータであっても「Apr 1, 2024 12:00am」という形式になって、2024年4月1日という判定になってしまいます。 表示形式はYYYY/MMのように変更ができますが、データ自体は「Apr 1, 2024 12:00am」の形式のままです。 そのため、フィルターの期間に1日が入っていない場合はその月のデータが表示されないという現象が起きてしまいます。 対策法はいくつかあります。 簡単なものだと、 そもそも期間を指定するときに1日は必ず入れるようにする dateをDate型ではなくString型にする などがあります。 しかし、1日を必ず入れないといけないというの周知させるのが大変、String型にするとコントロールで日付選択ツールを使用できなくなってしまいコントロールに日付を入力しないといけなくなるといった問題が生じます。 ということで、これらの問題を解決しながら、各月の日付が入っていたら1日が入っていなくても、その月のデータを表示できるようなフィルターを作成する方法を紹介します。 まずは以下のような「日付フィルター」という名前の計算フィールドを追加します。 やっていることとしては、 extract を使用して日付からYYYYとMMを抽出しYYYYMMという文字列を作成して、StartDateとEndDateの間にmonthがある場合のみ1を返すようにしています(それ以外の場合は0が返ってきます)。 YYYYに100をかけている理由は、1をかけない場合だと2024/01が20241になってしまい、2023/12~2024/01のような範囲を指定した場合に、202312よりも20241の方が小さくなってしまい、正しく範囲を指定できなくなってしまうのを防ぐためです。 次に、日付フィルターに対して以下のようなフィルターを設定します。 1と等しいというフィルターを設定することで、日付フィルター計算フィールドの条件を満たすデータのみが表示されるようになります。 これらの設定をすることで、開始日を2024年4月2日にしても4月のデータが表示されるようになります。 2.グラフタイトルに相対日付の◯月を入れる方法 先ほどのデータを使用して、日付をコントロールで変更するのではなく、相対日付を設定して先月や3ヶ月前などの各レシピのデータを閲覧できるビジュアルがあると想定し、そのタイトルに「◯月のデータ」のようなタイトルをつける方法を紹介します。 「先月のデータ」や「3ヶ月前のデータ」のようなタイトルにするならば、プロパティからタイトルの編集をしてタイトルを変更すれば良いですが、相対的に変わる「◯月」をタイトルに入れることはできません(毎月手動で変更するならできるがやりたくはないはず)。 そこで、かなり周りくどい方法になってしまいますが、Highcharts Visualを利用して「◯月」を擬似的なタイトルに入れる方法を紹介します。 まずは以下のような「月」と「タイトル」という2つの計算フィールドを追加します。 toString でDate型のmonthをString型にして、 substring で日付のMMの部分を抽出します。 if文でMMが10,11,12の場合は2文字抽出して、01,02のような場合には1文字のみ抽出して0を除外するようにしています。 concat を利用して、divタグと月計算フィールドと「月のデータ」という文字列を結合しています。 次に、Highcharts Visualを追加し、グループ化の条件に、先ほど作成した「タイトル」を選択します。 そして、Chart codeに以下のコードを書いて、「APPRY CODE」をクリックします。 { " title ": { " useHTML ": true , " text ": [ " get ", [ " getColumn ", 0 ] , 0 ] , " style ": { " fontSize ": " 18px ", " fontWeight ": " bold ", " color ": " #000000 " } } } そうすると、以下のようなテキストのビジュアルが作成されます。 このままでは幅が大きすぎてタイトル感がなくなってしまうので、最後にスタイルを調整します。 編集→分析設定→Sheets Settingsと進み、「フリーフォーム」を選択し、「適用」を押します。 フリーフォームにすることで、ビジュアルを重ねたり、好きな幅に調整したりとスタイルの微調整ができるようになります。 調整をするとこんな感じになります! なんとかタイトルと呼べる見た目になると思います! 3.データポインタホバー時に出現するカードのカラム名を変更する方法 模擬データを使用して、レシピごとのクリック数を比較する折れ線グラフを作ると以下のようになります。 軸名と凡例は元のカラム名を参照するため、カラム名と同じ英語表記になっています。 これを好きな表記にするには、プロパティから対応する項目を変更します。 例えばX軸の場合は、プロパティのX軸のタイトルを編集すればX軸を好きな名前に変更できます。 Y軸や凡例もプロパティからタイトルを変更すればOKです! ただし、ここで名称を変更してもデータポインタにカーソルを合わせたときに表示される以下の赤枠の部分の名前は変更することができません。 (凡例は「レシピ名」、Y軸は「クリック数」になっていますが、recipe、clickのままになっている。 ) データのカラム名を変更すれば良いのですが、簡単に変更できない場合もあると思います。 そんなときに、カラム名を変更せずに表示名を変更する方法を紹介します。 まず、計算フィールドに名前を変更したいフィールドを記入し、計算フィールドのタイトルに変更後の名前を記入します。 その後、グラフで指定するフィールドを今作成したものに置き換えます。 これだけで以下のように名前を変更することができます。 4.ビジュアルの枠線削除 ビジュアルをクリックすると以下のような枠線が表示されます。 これはダッシュボードを公開してもビジュアルをクリックすると枠線が表示されます。 ビジュアルをクリックしてもこの枠線が表示されないようにする方法を紹介します。 まずは、先ほども登場した「フリーフォーム」に変更します。 変更すると、プロパティのディスプレイ設定に「選択」という項目が出現するので、「選択」の目のアイコンを押して斜線がついた状態にすれば、ビジュアルをクリックしても枠線が表示されなくなります。 枠線削除後は以下のようになります。 分析ではクリック時に白い四角が表示されますが、ダッシュボード上では見えなくなります。 さいごに 本記事ではquicksightのかなり細かなTipsを紹介させていただきました。 使用頻度はかなり低いと思いますが、いつか誰かの役に立てれば嬉しいです! 最後まで読んでいただきありがとうございました!
アバター
title エブリーは2024年12月22日(日)に大田区産業プラザPiOで開催されたPHPカンファレンス2024に参加させていただきました。 今回は参加レポートとして、会場の様子やセッションの感想についてお届けします! エントランス スポンサー セッションホール スポンサーブース イベント概要 PHPカンファレンスは、PHP関連の技術を主とした日本最大級の技術者カンファレンスです。2000年に日本のユーザ会によって初めて開催され、今年で25回目という記念すべき開催となりました。 イベントには約1,000名以上の参加者が集まり、エンジニア同士の交流や最新技術の発表で熱気に包まれていました。会場は技術愛好家の熱量と笑顔にあふれ、PHPの魅力を共有する空間となっていました。 今回のイベントは、これからPHPをはじめる方から、さらにPHPを極めていきたい方まで、幅広い層のエンジニアが楽しめる内容となっており、まさに「PHPの今と未来」を体感する1日でした。 phpcon.php.gr.jp セッションの感想 PHPの今とこれから2024 今年のPHPカンファレンスで特に印象に残ったのは、PHP 8.4に関するセッションでした。2024年は、日本のエンジニアコミュニティがPHPのコア開発に大きく貢献した年であり、その裏話も興味深く聞けました。 今回発表された新機能の中で特に注目したいのは以下の3つです: JITの大幅な進化 :IRフレームワークが導入され、実行速度が18.5%向上。PHP 8.0から始まったJITがさらに強化され、パフォーマンス改善が期待されています。 プロパティアクセスフック :オブジェクトのプロパティアクセスを柔軟に制御できる新機能。実際のコード管理が大きく改善されそうです。 遅延オブジェクト :オブジェクトを必要になるまで初期化しない仕組みで、メモリ効率を向上。特に大規模プロジェクトでの恩恵が大きい機能です。 その他、まだまだ新機能があり、PHP 8.4のリリース内容を読み込まないといけないと感じました。 www.php.net そーだいさんに聞く!コミュニティとともに在るエンジニアの良いキャリアの歩み方 キャリア形成に関するセッションも大きな学びがありました。そーだいさんの講演では、特に「今日から始められること」の重要性が強調されており、キャリアを前進させるための具体的なアクションが提案されていました。 今日からできること コミュニティ活動への参加 :勉強会での発表、懇親会での交流を通じて、自分の存在を周りに知ってもらう。 ライフステージに合わせたキャリア設計 :「カレイドスコープ(万華鏡)キャリア」の考え方を紹介。キャリアは一直線でなく、多様な経験を活かすことが大切だと話されました。 アウトプットの実践 :ブログ執筆やOSSへの貢献が、自分自身の「名刺」になるとのこと。 技術だけでなく、人的な繋がりを築くことの重要性を改めて感じたセッションでした。 fortee.jp スポンサーブース紹介 エブリーでは、 DELISH KITCHEN などのサービスに加え、小売業者向けのデータ連携サービス retail HUB の開発・運用にPHPを活用しています。 retail HUBは、小売業のDXを推進するサービスです。詳細は こちら をご覧ください! PHPを活用する中で多くの恩恵を受けている私たちも、コミュニティのさらなる盛り上がりに貢献するため、スポンサーとして協賛させていただき、ブースを出展しました。 「なぜエブリーが協賛しているのか?」については、こちらのブログもご覧ください! → PHPカンファレンス2024協賛の理由 ブース エブリーでは、今回も弊社が提供するDELISH KITCHENのサービスをイメージしたブースの雰囲気を作りました。多くの方からDELISH KITCHENをを使っていますとの声をかけていただき、とても嬉しかったです。 ブース ノベルティ 今回もDELISH KITCHENにちなんだノベルティを用意させていただきました。 ステッカー DELISH KITCHENグッズ CTOブレンドのコーヒーバッグ ノベルティ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。参加者の方々にも好評で多くの方に参加していただけました! アンケート 今回、アンケートでは『PHPの好きなところ』と題して回答をしてもらいました。付箋に内容を記述して貼っていただく形式を取ることで様々な意見をいただくことができ、コミュニケーションのきっかけとなり会話が活発になりました。回答いただいた多くの皆様、ありがとうございました! アンケートボード アンケート結果 各社スポンサーブースの様子 スポンサーブースでは、各社趣向を凝らしたブースが展開されました。 クイズやアンケートボード、コードレビューなど様々な企画が用意されていて、会場全体が賑わっていました。 また、スポンサーブースでは、各社のエンジニアと直接話すことができる機会もあり、普段なかなか話すことができないような話もできてとても楽しかったです。 転職DRAFTさんのブースでは「あなたが会社を選ぶ時の軸を教えてください」と題して特色別の仮想3社からどの会社を選ぶかというアンケート企画を行なっていました。参加者からの様々な観点が書かれており、その中でも一緒に働く人を大事に考えられている方が多いように見受けられとても興味深かったです。 PR TIMESさんのブースでは PHP8.4 にちなんで ストップウォッチを8.4秒で止めると景品がもらえるという企画を行っていました。PHPの最新バージョンに絡めたとても面白い企画だと思いました。 スタンプラリー企画 各ブースを巡るスタンプラリー企画も実施され、多くの参加者が積極的に各ブースを訪れていました。 スタンプラリー まとめ PHPカンファレンス2024 運営の皆様、参加者の皆様、本当にありがとうございました! PHPカンファレンス2024は、25年の歴史を感じさせる充実した内容のイベントでした。最新技術や実践的なセッションだけでなく、エンジニア同士の交流やコミュニティの大切さも改めて実感できました。 今回学んだことを基に、まずはPHP 8.4の新機能を活用したプロジェクトを始める予定です。また、PHPコミュニティのイベントや勉強会に積極的に参加して、さらに知識を深めていきたいと思います。 次回のカンファレンスにもぜひ参加し、引き続きPHPの進化を追いかけたいと感じました!
アバター