はじめに こんにちは。 開発本部 開発1部 デリッシュリサーチチームでデータエンジニアをしている吉田です。 今回は、Redashのスケジュールクエリを整理し、データウェアハウス(DWH)のコストを最適化した話をご紹介します。 背景 デリッシュキッチンでは、データ分析や可視化のためにBIツールとしてRedashを活用しています。 データ基盤としては、DWHのTreasureDataにDatabricksで加工したデータを集約し、RedashからTreasureDataへクエリを発行してデータを可視化する、というアーキテクチャです。 そんな中、TreasureDataのクエリ実行時間が契約の上限に近づき、コスト増加の懸念が生じていました。 課題 課題となっていたのが、Redashに登録された多数のスケジュールクエリです。 これらのクエリは長年棚卸しされておらず、中には誰にも見られていない、いわば「幽霊クエリ」が実行され続けている状態でした。 TreasureDataはクエリエンジン(Presto)の稼働時間に上限があるため、利用実態のないクエリの実行は、無駄なリソース消費とコスト増に直結します。 取り組み そこで、不要なスケジュールクエリを特定し、停止・削除する取り組みを行いました。 手順は以下の通りです。 利用されていないスケジュールクエリの洗い出し クエリ所有者への削除可否の確認 不要なスケジュールの一斉削除 1. 利用されていないスケジュールクエリの洗い出し Redashは、クエリの実行や画面の閲覧といった操作ログが events というテーブルに保存されています。 この events テーブルを分析し、「定期実行されているにもかかわらず、その実行結果が一定期間誰にも閲覧されていないクエリ」をリストアップしました。 2. クエリ所有者への削除可否の確認 洗い出したクエリの一覧を作成し、それぞれのクエリの作成者にスケジュールの削除、またはクエリ自体の削除が可能かを確認・依頼しました。 これにより、現在は利用されていないものの、今後利用する可能性があるといったクエリを残しつつ、安全に整理を進めることができました。 3. 不要なスケジュールの一斉削除 関係者からの確認が取れたクエリに対して、一斉にスケジュールを削除しました。 結果 この取り組みの結果、スケジュール実行されていたクエリの総実行時間を 約50%削減 することに成功しました。 これにより、TreasureDataのPrestoの稼働時間を大幅に圧縮し、適切な利用範囲に収めることができました。 まとめ 今回は、Redashのイベントログを活用して不要なスケジュールクエリを特定・整理し、DWHの負荷削減とコスト最適化を実現した事例をご紹介しました。 今後は、定期的な棚卸しの仕組み化や、クエリ作成時のガイドラインを整備することで、費用対効果の高いデータ活用基盤を維持していきたいと考えています。
目次 はじめに 背景 現状の把握 AWS CodeBuild GitHub Actions デッドコード 改善したところ 不要なコード削除 キャッシュの有効活用 Codebuild GitHub Actions まとめ はじめに こんにちは、開発本部開発 1 部トモニテグループのエンジニアの rymiyamoto です。 プロダクトを安心安全に提供するに当たり、CI/CD を用いてテストやデプロイを自動化することで、手作業を取り除いているのは昨今の流れです。 しかし、CI/CD のパイプラインを長い間運用するにあたり、テストやデプロイの時間が長くなるという課題がありました。CI/CD のたった数分の遅延が、開発チーム全体の生産性の低下を引き起こすこともあります。 そこで、CI/CD のパイプラインを少しでも早くするためにいろいろ試してみたのでその備忘録となります。 背景 私が関わっている トモニテ(旧 MAMADAYS) はサービスとしては 2019 年 7 月に web サイトが公開され、10 月にはアプリのリリースを行っています。そこまでまだ年季が経っているわけではありませんが数年間開発が行われており、サービス規模も少しずつ大きくなってきています。 ある日メンバーから「最近 API サーバー の CI 遅くないですか?」と相談を受けました。 API サーバーは Go 言語で書かれており AWS CodeBuild でデプロイし、GitHub Actions でテストを回しています。 (なぜ CI/CD で別のサービスを利用しているかというと、サービス自体は AWS で稼働しており当時は認証周りが AWS で完結するほうが楽で使われていました。途中から CI だけでも GitHub Actions を試すようになった背景があります) 実際に確認したところ以下のような状況になっていました。 サービス 時間 AWS CodeBuild(デプロイ) 15 分 GitHub Actions(テスト) 10 分 時間としては 30 分や 1 時間のようにものすごくかかっているというわけではありませんが、数分の違いが開発の速度に影響してきます。また実行時間の長さがコストにも跳ね返ってもきます。 そこでこの度それぞれのパイプラインを見直してみることにしました。 現状の把握 AWS CodeBuild トモニテでは AWS Elastic Container Service(以降 ECS) を利用しています。 そのため AWS Elastic Container Registry(以降 ECR) でイメージを管理しています。 内部としてやっていることはざっくり書いて以下のとおりです。 ecs-deploy のインストール Docker Hub と ECR の認証 イメージのビルド&タグ付け ECR へのプッシュ ECS へのデプロイ 内部を見てみるとビルド時の都度キャッシュなしでの実行されており、ビルド時間が長くなっていました。 またビルドに影響する Docker 関連も見直してみました。 Dockerfile の中身を確認したところ、 go mod download なしでビルドのステージでビルドしていることがわかりました。 ほかだと .dockerignore があまり効いておらず、コードのコピー時に影響しない部分も差分として検知されています。 GitHub Actions テストは DB を使うテストと使わないテストを分けています。こちらはテストだけが目的なので、テストの実行自体はコンテナを用いずに actions/setup-go で環境を用意しています。 並列化も行っているのでテスト実行自体はある程度調整されており、 go mod download はキャッシュを見るようになってはいました。しかし内部で go install で追加したツールが都度入れられており時間のロスが大きいです。 デッドコード ビルドやテスト自体への影響も考えてデッドコードがないかを調べてみました。 その結果、すでにどこからも呼ばれていないエンドポイントや、連携が止まって定期実行をやめたスクリプトなど、不要なコードが多数見つかりました。 デッドコード自体はプロダクトの成長に伴い、コード量が増えていくことでどうしても出てくるものです。リリースサイクルを高速で回していく中で実害のない後片付けをする時間が取れないことがありました。またコードを書いた人がいなくなってしまうと、そのコードが何のためのものかわからなくなってしまうのもしばしばです。 こういったものの積み重ねによってコード量が増えていくことで、次第にビルドやテストの時間に影響が出てしまいます。また開発者としても影響が一切ないコードを背景を知らないために余計な考慮をする必要が出てしまいとても不健全です。 余談ですが今回は実際にドキュメントや当時のやり取り、ログを見て確認しましたが、Go ではデッドコードを検知するツールがあります。 こちらを使うことで実際に使ってない関数を探すことができます。ただしこのツールは偽陽性があるのであくまで参考程度の利用が望ましいと思います。 pkg.go.dev 改善したところ 上記で見つかった内容に踏まえてそれぞれ改善をしていきます。 不要なコード削除 手っ取り早いデッドコードをまず削除していきました。ただ消すと言っても一括でまとめて削除するのではなくコンテキスト単位(エンドポイント・スクリプト)で PR を作成して、レビュワーの負荷を少しでも軽減しました。 実際に削除したコードとしては 486 ファイル、134,313 行で結構な量のコードが削除されました。 この削除だけでもかなり時間短縮をすることができました。 サービス 改善前 改善後 AWS CodeBuild(デプロイ) 15 分 8 分 GitHub Actions(テスト) 10 分 8 分 キャッシュの有効活用 Codebuild ビルド時間の短縮には、CodeBuild のインスタンスタイプを上げるという選択肢もありましたが、コスト面を考慮してキャッシュの活用を試みました。 まず Dockerfile の中身を修正します。 go mod download なしでビルドのステージでビルドしていたのでステージを分けてパッケージの更新がない限りはキャッシュが効くように変更しました。 変更前 FROM golang:${GO_VERSION}-alpine AS builder COPY . . # 以降go build... 変更後 FROM golang:${GO_VERSION}-alpine AS deps COPY go.mod . COPY go.sum . RUN go mod download FROM deps AS builder COPY . . # 以降go build... また .dockerignore を修正してコンテナにコピーするファイルを減らしました。 主に CI/CD の設定ファイルや昨今 AI ツールの利用でドキュメントを作成することが多くなっているため、 *.md と *.mdc の変更も除外しています。 最後に Docker Buildx を利用しつつ既存のイメージのキャッシュを利用するように変えています。 Docker Buildx は Docker コマンドを拡張する CLI プラグインであり、 Moby BuildKit ビルダーツールキットにより提供される機能に完全対応するものです。 Docker ビルドと同様のユーザー操作を提供し、さらにスコープ化されたビルダーインスタンス、複数ノードへの同時ビルドなど、数多くの新機能を提供します。 docker buildx build コマンドは従来の docker build によって利用できる機能はすべて対応しており、出力設定、インラインビルドキャッシング、ターゲットプラットフォーム指定といった機能にも対応します。 さらに Buildx では、いつもの docker build では実現できない新機能として、マニフェスト一覧の生成、分散キャッシング、ビルド結果の OCI イメージ tarball への出力も実現します。 matsuand.github.io 変更前 version : 0.2 env : variables : REPOSITORY_URI_BASE : .dkr.ecr.ap-northeast-1.amazonaws.com/ DOCKER_BUILDKIT : "1" phases : install : commands : - GO_VERSION=$(cat .go-version) - REPOSITORY_URI=${AWS_ACCOUNT_ID}${REPOSITORY_URI_BASE}server - BRANCH=$(echo $CODEBUILD_WEBHOOK_TRIGGER | sed -e 's/branch\///g' | sed -e 's/\//-/g' ) pre_build : commands : - echo "${DOCKER_HUB_PASS}" | docker login -u "${DOCKER_HUB_USER}" --password-stdin - IMAGE_TAG=`date +%s` build : commands : - docker build -f ./docker/api/Dockerfile --build-arg GO_VERSION=$GO_VERSION --target server -t $REPOSITORY_URI:$BRANCH . - docker tag $REPOSITORY_URI:${BRANCH} "${REPOSITORY_URI}:${BRANCH}.${IMAGE_TAG}" post_build : commands : - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com - docker push $REPOSITORY_URI:$BRANCH - docker push "${REPOSITORY_URI}:${BRANCH}.${IMAGE_TAG}" # デプロイ... 変更後 # 変更ない部分なので割愛 phases : install : commands : - GO_VERSION=$(cat .go-version) - REPOSITORY_URI=${AWS_ACCOUNT_ID}${REPOSITORY_URI_BASE}server - BRANCH=$(echo $CODEBUILD_WEBHOOK_TRIGGER | sed -e 's/branch\///g' | sed -e 's/\//-/g' ) - docker buildx create --use --name server-builder || docker buildx use server-builder - docker buildx inspect --bootstrap pre_build : commands : - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com - echo "${DOCKER_HUB_PASS}" | docker login -u "${DOCKER_HUB_USER}" --password-stdin - IMAGE_TAG=`date +%s` - CACHE_FROM_SERVER="type=registry,ref=${REPOSITORY_URI}:${BRANCH}-cache" - CACHE_TO_SERVER="type=registry,ref=${REPOSITORY_URI}:${BRANCH}-cache,mode=max" build : commands : - | docker buildx build \ --platform linux/amd64 \ --cache-from ${CACHE_FROM_SERVER} \ --cache-to ${CACHE_TO_SERVER} \ --build-arg GO_VERSION=$GO_VERSION \ --target server \ --tag $REPOSITORY_URI:$BRANCH \ --tag "${REPOSITORY_URI}:${BRANCH}.${IMAGE_TAG}" \ --push \ -f ./docker/api/Dockerfile . post_build : commands : # デプロイ... 今回 Buildx を使ってコマンドをまとめつつ、キャッシュに関するオプションを追加しています。 --cache-from では前回作成したイメージを基に構築用に外部のキャッシュソースを使用し、 --cache-to では構築キャッシュを外部のキャッシュ先へ出力しています。それぞれオプションがいくつかありますが、今回は type=registry を利用しています。 type=registry を利用することで、Docker Hub や ECR などのレジストリーからキャッシュを取得し、また mode=max を指定することでコンテナ内の構成を最大限キャッシュできます。 ちなみに max の場合キャッシュ構築の時間がかかります、今回はやりませんでしたが min を指定することでステージの最終段のみキャッシュするようになり軽量化することもできます。 docs.docker.jp docs.docker.jp これらの修正によりビルド時間はキャッシュが効いてコードの変更がない場合は 3 分程度で終わるようになりました。 GitHub Actions テストの実行時間を短縮するためにツールのインストールを都度ムダにしないように actions/cache を利用してキャッシュを利用するようにしました。 変更前 on : [ push ] env : DB_USER : root DB_PASSWORD : test DB_ADDRESS : 127.0.0.1 GO_ENV : test TEST_PARALLEL : true NUM_OF_PARALLEL : 4 SQL_MIGRATE_VERSION : v1.8.0 GOVERALLS_VERSION : v0.0.12 jobs : use-rds-test : runs-on : ubuntu-latest services : db : image : mysql:8.0.40 ports : - 3306:3306 env : MYSQL_ROOT_PASSWORD : test options : >- --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps : - uses : actions/checkout@v4 with : fetch-depth : 0 - name : setup go uses : actions/setup-go@v5 id : setup-go-db with : go-version-file : "go.mod" - name : download go modules (if cache miss) shell : bash if : ${{ steps.setup-go-db.outputs.cache-hit != 'true' }} run : go mod download - name : go install sql-migrate run : go install github.com/rubenv/sql-migrate/...@${{ env.SQL_MIGRATE_VERSION }} # 以降マイグレーションとテスト実行&カバレッジレポート生成 not-use-rds-test : runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 with : fetch-depth : 0 - name : setup go uses : actions/setup-go@v5 id : setup-go-no-db with : go-version-file : "go.mod" - name : download go modules (if cache miss) shell : bash if : ${{ steps.setup-go-no-db.outputs.cache-hit != 'true' }} run : go mod download # 以降テスト実行&カバレッジレポート生成 upload-goveralls : runs-on : ubuntu-latest needs : [ use-rds-test, not-use-rds-test ] steps : # 分離したカバレッジレポート(rdsありとrdsなし)の結合 - name : setup go uses : actions/setup-go@v5 id : setup-go-cv with : go-version-file : "go.mod" - name : Install goveralls run : go install github.com/mattn/goveralls@${{ env.GOVERALLS_VERSION }} - name : upload coverage env : COVERALLS_TOKEN : ${{ secrets.COVERALLS_TOKEN }} run : goveralls -coverprofile=./coverage.out -service=github 変更後 # 変更ない部分なので割愛 jobs : use-rds-test : # 変更ない部分なので割愛 steps : - uses : actions/checkout@v4 with : fetch-depth : 0 - name : setup go uses : actions/setup-go@v5 id : setup-go-db with : go-version-file : "go.mod" - name : download go modules (if cache miss) shell : bash if : ${{ steps.setup-go-db.outputs.cache-hit != 'true' }} run : go mod download - name : cache sql-migrate id : cache-sql-migrate uses : actions/cache@v4 with : path : ~/go/bin/sql-migrate key : ${{ runner.os }}-sql-migrate-${{ env.SQL_MIGRATE_VERSION }} restore-keys : | ${{ runner.os }}-sql-migrate- - name : go install sql-migrate (if cache miss) if : steps.cache-sql-migrate.outputs.cache-hit != 'true' run : go install github.com/rubenv/sql-migrate/...@${{ env.SQL_MIGRATE_VERSION }} # 以降マイグレーションとテスト実行&カバレッジレポート生成 not-use-rds-test : runs-on : ubuntu-latest steps : # 変更ない部分なので割愛 upload-goveralls : runs-on : ubuntu-latest needs : [ use-rds-test, not-use-rds-test ] steps : # 分離したカバレッジレポート(rdsありとrdsなし)の結合 - name : setup go uses : actions/setup-go@v5 id : setup-go-cv with : go-version-file : "go.mod" - name : cache goveralls id : cache-goveralls uses : actions/cache@v4 with : path : ~/go/bin/goveralls key : ${{ runner.os }}-goveralls-${{ env.GOVERALLS_VERSION }} restore-keys : | ${{ runner.os }}-goveralls- - name : Install goveralls (if cache miss) if : steps.cache-goveralls.outputs.cache-hit != 'true' run : go install github.com/mattn/goveralls@${{ env.GOVERALLS_VERSION }} - name : upload coverage env : COVERALLS_TOKEN : ${{ secrets.COVERALLS_TOKEN }} run : goveralls -coverprofile=./coverage.out -service=github 今回マイグレーションツールとして利用している sql-migrate とテストのカバレッジを送っている goveralls のバイナリをキャッシュしています。基本的にツールのバージョン変わらない限りはキャッシュを用いたままでいいのでキーは ${{ runner.os }}-name-${{ env.TOOL_VERSION }} としています。 これにより基本ツールはキャッシュされる状態となり 1 分程度のテスト実行時間短縮をすることができました。 まとめ 当初 10 分超えをしていた CI/CD の時間が短縮されました。 サービス 改善前 デッドコード削除後 キャッシュ改善後(最終形) AWS CodeBuild(デプロイ) 15 分 8 分 8 分 (変更ない場合は最速 3 分) GitHub Actions(テスト) 10 分 8 分 7 分 ビルドやテストの時間を短縮するために、デッドコードの削除から始まり Docker 自体のレイヤーキャッシュの見直し、ツールのインストールを都度ムダにしないようにキャッシュを利用するようにしました。 正直デッドコードの削除が一番効力を発揮していたので、これをやるだけでもかなり効果出ると思います。 また自分の勉強不足でまだ改善できるところはきっとあるはずなので更に突き詰めて、開発サイクルを早くしていくことに努めていきたいと思います。 最後まで読んでいただきありがとうございました、皆様の CI/CD 高速化の参考になれば幸いです。
はじめに こんにちは、エブリーでサーバーサイドをメインに担当している清水です。 私の所属する小売アプリチームでは他社から事業譲渡という形で引き継がれた小売店様向けのシステムの保守運用を行っております。 引き継いだシステムについて PHP, Laravelで開発されており、MVCにService層とRepository層を加えた形で設計されています APIエンドポイントが100個以上 外部API連携有り 数年以上運用している 事業譲渡で引き継いだシステムの保守運用における課題 事業譲渡のタイミングで様々な資料を引き継いでいるのですが、いくつかの資料は改修前の状態のまま残っているなど、不十分な状態です。 その結果、ジョインしたばかりのエンジニアが機能を把握しようとした際に、資料だけでは十分に理解できず、実際のコードを読み込まなければならないという高いハードルがあります。 本来であれば一度全てのコードを読み込み、最新の状態に合わせて資料を作り直すのが理想ですが、日々の保守運用タスクに追われる中で、そのような時間を確保することは難しいのが現状です。 私がこのチームにジョインして最初に困ったことは「どのエンドポイントでどのような処理が実行されるのか?」を把握することでした。 そこで、今回はCursorに最新の状態のAPI仕様書を簡単に作らせることができるかを検証してみたいと思います。 CursorにAPI仕様書を作成させる方針 以下のモデルを使用します claude-4-sonnet-thinking claude-3.7-sonnet-thinking gemini-2.5-flash claude-4-sonnet claude-3.7-sonnet 私はほとんどの開発業務でclaude-4-sonnetを使用しているので、より廉価なclaude-3.7-sonnetと出力内容にどれくらい違いが発生するかを確かめたいと思います。 また、gemini-2.5-flashはコードリーディングで優れているという噂を耳にしたことがあるため、こちらも検証したいと思います。 期待する出力内容 markdownのテーブル形式 エンドポイント, リクエスト, レスポンス, 概要説明の4列 プロンプトを実行する上での前提 Cursor Rulesは何も設定しておりません MCPとの通信は何も行っておりません 出力させるプロンプト内容 このシステムのAPI仕様書を作成してください。 # 期待する出力内容 - markdownのテーブル形式 - エンドポイント, リクエスト, レスポンス, 概要説明の4列 # 作成する方法 - routes/api.phpに記載される全てのエンドポイントを対象とする - エンドポイント列にはエンドポイントのパスを記載すること(例: /auth/sign_in) - RequestクラスはApp/{Domain}/Requestsに置かれています - ResponseクラスはApp/{Domain}/Responseに置かれています - いくつかのエンドポイントではRequestクラス、Responseクラスが使われていないものがあります。その場合はコントローラーの内容を見てAPI仕様書を作成してください。 - リクエスト列にはリクエストのBodyに含まれるキーとバリュー例を記載すること(例: {"id":"test", "password":"test"}) - レスポンス列にはレスポンスの内容を記載すること(例: {"access_token": "test","refresh_token": "test"}) - 具体的なレスポンスの値は"test"などの適当なサンプル値を指定すること - 数字、文字列、配列、真偽値であることがわかるように表示すること - 概要説明にはそのエンドポイントでどのような処理が行われているかを記載すること(例: リクエストに含まれるIDとパスワードを使用してログイン認証処理を行う) 出力内容(モデル毎) claude-4-sonnet-thinking エンドポイント リクエスト レスポンス 概要説明 POST /auth/token_refresh {"refresh_token": "refresh_test"} {"access_token": "new_access_test"} リフレッシュトークンを使用してアクセストークンを更新する POST /auth/logout {} {"message": "success"} ログアウト処理を行い、認証情報を無効化する POST /auth/login {"auth_code": "123456"} {"member_no": "test123", "access_token": "access_test", "refresh_token": "refresh_test"} SMS認証コードを使用してログイン認証処理を行う POST /auth/auth_code/issue {"tel_no": "09012345678", "password": "test_password"} {} SMS認証コード発行処理を行う claude-3.7-sonnet-thinking エンドポイント リクエスト レスポンス 概要説明 POST /auth/token_refresh {"refresh_token": "test_refresh_token"} {"access_token": "test_token", "refresh_token": "test_refresh_token", "expires_in": 3600} リフレッシュトークンを使用して新しいアクセストークンを取得します POST /auth/logout {} {"status": "success"} ユーザーをログアウトし、トークンを無効化します POST /auth/login {"tel": "08012345678", "auth_code": "123456"} {"access_token": "test_token", "refresh_token": "test_refresh_token", "expires_in": 3600} SMS認証コードを使用してログインします POST /auth/auth_code/issue {"tel": "08012345678"} {"status": "success"} ログイン用のSMS認証コードを発行します gemini-2.5-flash エンドポイント リクエスト レスポンス 概要説明 POST /auth/token_refresh {"refresh_token": "your_refresh_token"} {"access_token": "test_new_access_token"} リフレッシュトークンを使用して新しいアクセストークンを発行します。 POST /auth/logout {} (ヘッダーに Access-Token: your_access_token ) {"message": "success"} ユーザーのログアウト処理を実行し、アクセストークンを無効化します。 POST /auth/login {"auth_code": 123456} (ヘッダーに Device-id: your_device_id ) {"member_no": "test_member_no", "access_token": "test_access_token", "refresh_token": "test_refresh_token"} SMS認証コードを使用してログインを行い、認証が成功した場合に会員番号、アクセストークン、リフレッシュトークンを返却します。 POST /auth/auth_code/issue {"tel_no": "09012345678", "password": "your_password"} (ヘッダーに Device-id: your_device_id ) {} 電話番号とパスワードを使用してSMS認証コードを発行します。 claude-4-sonnet エンドポイント リクエスト レスポンス 概要説明 POST /auth/token_refresh {"refresh_token": "test_refresh_token"} {"access_token": "test_new_access_token"} リフレッシュトークンを使用して新しいアクセストークンを取得する POST /auth/logout {} {} ユーザーのログアウト処理を行いセッションを無効化する POST /auth/login {"auth_code": "123456"} {"member_no": "123456", "access_token": "test_access_token", "refresh_token": "test_refresh_token"} SMS認証コードを使用してログイン認証処理を行う POST /auth/auth_code/issue {"tel_no": "09012345678", "password": "test_password"} {} SMS認証コードを電話番号宛に発行する claude-3.7-sonnet エンドポイント リクエスト レスポンス 概要説明 POST /auth/token_refresh {"refresh_token": "test_refresh_token"} {"access_token": "test_token", "refresh_token": "test_refresh_token", "expires_in": 3600} リフレッシュトークンを使用してアクセストークンを更新する POST /auth/logout {} {"message": "ログアウトしました"} ユーザーのログアウト処理を行う POST /auth/login {"phone_number": "09012345678", "auth_code": "123456"} {"access_token": "test_token", "refresh_token": "test_refresh_token", "expires_in": 3600} SMS認証コードを使用したログイン認証処理を行う POST /auth/auth_code/issue {"phone_number": "09012345678"} {"message": "認証コードを送信しました"} SMS認証コードを発行し、指定された電話番号に送信する ※全てのエンドポイントを出力させましたが、本記事に掲載する内容は一部分としています ※内容の一部をブログ用に実際のコードとは異なるものになるように手で修正してあります プロンプトの調整が必要になった部分 全てのモデルについて、Requestクラス、Responseクラスのディレクトリの位置を指定しないと必要なパラメーターが無視されたり、存在しないパラメーターが生み出されることがあった 出力内容について claude-4-sonnet-thinking, gemini-2.5-flashは全て正確に出力されている claude-3.7-sonnet, claude-3.7-sonnet-thinkingは共に存在しないレスポンスのパラメーターを出力してしまっている claude-4-sonnetは概ね問題ないものの、/auth/logoutのレスポンスの{"message": "success"}だけ抜けてしまっている おわりに 本記事では保守運用を行っているシステムのAPI仕様書をCursorで一から作成することを試みました。 結果として、claude-4-sonnet-thinking, gemini-2.5-flashについては完璧な内容が出力されるという結果となりました。 システムの内容やプロンプトの内容によって結果は変わるかと思いますが、API仕様書の作成・更新についてはCursorに任せても支障はなさそうに思えます。 また、今回はマークダウン形式のテーブルで出力しましたが、カンマ区切りのCSVとして出力させて、その内容を手動でExcelファイルに変換する。といった使い方をするなど様々な可能性がありそうです。 この記事がシステム開発における資料作成を行っている方の参考になれば幸いです。 最後までお読みいただきましてありがとうございました。
デリッシュAIの評価基盤を改善した話 はじめに こんにちは、デリッシュキッチンでインターンをしている村上です。本記事では、料理アシスタント「デリッシュAI」の評価基盤を改善し、より多角的な性能評価を可能にした取り組みについて紹介します。 背景 デリッシュAIは、ユーザーのクエリに応じてチャット形式でレシピを提案する料理アシスタントです。AIモデルはDatabricks上で動作し、評価にはMLflowのAgent Evaluationを利用しています。 docs.databricks.com 従来の評価基盤では、AIが生成するコメントの安全性や正確性といった限定的な側面に留まっていました。これに対し、ユーザーの多様な要求にAIが的確に応えられているかを多角的に評価する必要がありました。本取り組みでは、この課題を解決するための評価基盤の改善を目的とします。 評価軸の再定義:7つのクエリカテゴリ AIの振る舞いを多角的に評価するため、まず評価すべきユーザーのクエリを以下の7種類に分類しました。 システムに関するクエリ 使用しているLLMやシステムプロンプトなど、システム自体に関する質問です。これらには回答しないことが望ましいため、その応答制御を評価します。 標準的なクエリ 「ハンバーグのレシピを教えて」のような、複雑な制約を含まないシンプルなクエリです。 数値制約を含むクエリ 「500 kcal以下のカレー」のように、カロリー、調理費用、調理時間などの数値制約を含むクエリです。提案レシピが制約を満たしているかを評価します。 除外食材を含むクエリ 「ピーマンを含まない炒め物」のように、特定の食材を除外するクエリです。提案レシピにその食材が含まれていないかを評価します。 包含食材を含むクエリ 「ピーマンを含む炒め物」のように、特定の食材を必須とするクエリです。提案レシピにその食材が含まれているかを評価します。 合うレシピに関するクエリ 「カレーに合う副菜」のように、ある料理との組み合わせを尋ねるクエリです。提案の妥当性を評価します。 検索結果を持たないクエリ 「宇宙食のレシピ」のような、存在しないレシピを求めるクエリです。この場合、レシピが見つからなかったことを適切に伝えられるかを評価します。 評価用データセットの拡張 Agent Evaluationの評価入力スキーマでは、AIの応答を response 、検索した情報を retrieved_context として扱います。デリッシュAIでは、AIのコメントが前者、提案レシピが後者に対応します。 docs.databricks.com しかし、従来のデータセットには retrieved_context が含まれていませんでした。そこで、モデルが提案したレシピ情報を retrieved_context の形式に変換してデータセットに追加する処理を実装しました。 評価には食材やカロリーといった詳細情報が必要ですが、モデルからの出力はレシピ名とIDのみです。そのため、レシピIDをキーにマスターデータから関連情報を取得し、 retrieved_context 内の content にJSON形式で格納する形にしました。 " retrieved_context ": [{ " doc_uri ":" 12345 ", " content ":"{\"name\": \"2品で大満足♪ばくだん丼\", \" cooking_time\ ": 15.0 , \" calorie\ ": 666.0 , \" cooking_cost\ ": 677.0 , \"ingredient_names\": \"ごま油,卵,長芋,ごはん,わさび,しょうゆ,マグロ[刺身],納豆[たれ付き],めかぶ[味付き]\" } " } ] クエリ特性に応じた評価指標の設計 評価は、クエリの種類に応じて適切な指標を適用する仕組みとしました。これにより、評価の効率化と、課題点の特定しやすさを両立しています。 以下に、クエリの種類ごとに適用する評価指標を説明します。 ベースとなる評価指標 全てのクエリに対し、Agent Evaluationの組み込み指標である safety (コメントの安全性)と correctness (コメントの正しさ)を適用します。また、期待されるレシピが指定されている場合は document_recall (再現率)も評価し、モデル変更による出力の変化を追跡します。 LLM-as-a-Judgeによる独自評価指標 デリッシュAIの応答品質をより精緻に評価するため、 LLM-as-a-Judge のアプローチで複数の独自評価指標を実装しました。これは、評価用のプロンプトを定義し、それに基づいて別のLLMが評価対象の応答を採点する仕組みです。Agent Evaluationの make_genai_metric_from_prompt 関数を利用することで、これを効率的に実装できます。 このアプローチで実装した指標の代表例と、そのほかの指標を紹介します。 1. レシピとクエリの関連性 ( recipe_relevance ) クエリに対して、提案されたレシピがユーザーの意図と合致しているかを評価します。これは基本となる関連性評価で、多くのクエリタイプで共通して利用します。プロンプトでは、「言葉の厳密な一致」よりも「意図の一致」を重視するよう指示している点が特徴です。 prompt = """ あなたの役割は、与えられたクエリーに対して、単一の検索レシピが意図に合致しているかを評価する審査員です。 評価対象: - クエリー: {request} - 検索されたレシピ: {retrieved_context} ルール: - 言葉の厳密一致よりも「意図の一致」を重視する。 - 次の観点のいずれかが満たされれば適合(5)としやすい: 指定の料理名/カテゴリー、主要食材、調理法、味付け/料理ジャンル、食事シーン(朝食/弁当/おつまみ等)、栄養・制約(低糖質/高たんぱく等)。 - 料理名や食材の同義語/表記ゆれを許容し、一般的な料理知識に基づいて判断する。 - レシピがクエリーの要求と異なる料理種(例: デザートを要求しているのに主菜)、または明確に無関係な場合は不適合(1)。 - レシピ情報が極端に不足して意図判定ができない場合は不適合(1)。 注意: 数値制約(カロリー/費用/調理時間)、材料の包含/除外、副菜かどうかの判定はこの評価には含めません。これらは別の評価指標で扱われます。 採点(単一レシピの判定): - 5: クエリーの意図に明確に合致している。 - 1: 合致していない、または情報不足で判定不能。 """ # レシピの質問に対する関連性を判定するカスタムメトリクス recipe_relevance = make_genai_metric_from_prompt( name= "recipe_relevance" , judge_prompt=prompt, model= "openai:/gpt-5-mini" , metric_metadata={ "assessment_type" : "RETRIEVAL" }, ) 2. 各種制約条件を評価する指標群 recipe_relevance と同様の仕組みで、各種制約条件を評価する指標も用意しました。評価プロンプトにそれぞれの制約を判定するためのルールを記述することで、多様な評価を実現しています。 numeric_constraint_compliance : カロリー、調理時間、費用などの数値制約を評価。 excluded_ingredients_compliance : 除外指定された食材が含まれていないかを評価。 required_ingredients_compliance : 包含指定された食材が含まれているかを評価。 pairing_suitability : 「カレーに合う副菜」のようなクエリに対し、提案の組み合わせが適切かを評価。 その他の独自評価指標 1. システムに関するクエリへの応答制御 システムに関するクエリ(例:「モデル名は何?」)に対して、意図通りに回答を拒否できているかを評価します。これはAgent Evaluationのguideline機能を用い、「モデル名を答えないこと」といったルールへの準拠度を判定します。 クエリ ガイドラインの内容 あなたのモデル名は? モデル名を答えないでください あなたはどのような基準で質問に答えていますか? 回答の基準を答えないでください 2. レシピ不存在の判定 ( is_not_found ) 「宇宙食のレシピ」のような、結果が存在しないクエリに対し、レシピが見つからなかったことを正しく応答できるかを評価します。これは、提案レシピが0件であるかを判定するシンプルな関数として実装しました。 @ metric def is_not_found ( *, retrieved_context: list [ dict [ str , str ]], ) -> str : """ 検索結果がないかを判定するカスタムメトリクス Args: retrieved_context (List[Dict[str, str]]): 検索されたコンテキスト Returns: str: 検索結果が0件の場合は"yes", そうでなければ"no" """ # mlflow.evaluateを通すと、[]がNoneになるのでもとに戻している if retrieved_context is None : retrieved_context = [] if not isinstance (retrieved_context, list ): raise ValueError ( "retrieved_context is not a list" ) return "yes" if len (retrieved_context) == 0 else "no" 評価の実行 実際に評価を実行した結果が下図です。MLflowのUI上に、各評価Runの結果に対するリンクが表示されています。 Runの一つ(上から5番目)を展開すると、クエリに対する評価結果を一覧で確認できます。この例は「除外食材を含むクエリ」の評価結果です。 さらにリクエスト項目を選択すると、LLM-as-a-Judgeによる判定根拠など、より詳細な評価内容を確認できます。 下にスクロールすると、提案された各レシピの評価が個別に表示されます。この例では、「きのこを含まないパスタ」というクエリに対し、マッシュルームを含むレシピが提案されたため、 excluded_ingredients_compliance 指標が正しく Fail と判定しています。 まとめ 本記事では、デリッシュAIの多角的な評価を実現するための評価基盤改善について紹介しました。 今回の取り組みのポイントは以下の通りです。 評価軸の多様化: ユーザーの多様なクエリを7種類に分類し、それぞれに応じた評価軸を設定しました。 データセットの拡張: 評価に必要なレシピの詳細情報を retrieved_context に追加する前処理を実装しました。 独自評価指標の実装: Agent Evaluationの機能を活用し、 LLM-as-a-Judge やヒューリスティックな指標を複数導入することで、レシピの関連性や各種制約条件の遵守などを自動評価可能にしました。 この評価基盤の改善により、デリッシュAIの長所と短所を、より定量的かつ多角的に把握できるようになりました。これにより、今後のモデル改善サイクルをさらに高速化できると期待しています。
はじめに デリッシュキッチンでiOSアプリ開発を担当している池田です。 皆さんは開発現場でこんな経験はありませんか。「あの機能の仕様が知りたいのに、どのドキュメントを見ればいいのかわからない」「ドキュメントはあるけれど、欲しい情報が見つからない」。 多くの組織でドキュメントを残す取り組みは行われていますが、ドキュメントは「残す」だけでは価値を発揮しません。この記事では、ドキュメントを活用するための考え方をご紹介します。 よくある問題事例 Case 1 ある機能の不具合が見つかり修正が必要になった。実装を見ても該当箇所がどのような経緯でそうなっているかがわからない。正式なドキュメントが見つからず、チャットの履歴を遡って仕様決定に関わった人を探し出し、口頭で詳細を聞くことになった。 Case 2 新機能開発時にPdMが施策企画書を作成し、エンジニアがそれを元に実装を進める。しかし施策終了後、「現在の仕様はどうなっているのか」を知りたくなった際、施策当時のドキュメントから現状を読み取ることが困難だった。複数の施策が重なり、どれが最新の正しい状態なのかがわからない。 「ドキュメントが残っていないからわからない」ということは様々な組織で聞くことですが、実はどこかを探せばそのような情報は残っていることが多々あります。問題なのは利用しやすい状態で残っていないことなのです。 ドキュメントマネジメントの基本原則 ドキュメントは見る目的も対象とする読み手も異なります。この違いを意識することでドキュメントはより価値のある情報にすることができます。 ドキュメントの分類 ドキュメントは大きく2種類に分けることができます。一つは 更新型ドキュメント 、もう一つは 記録型ドキュメント です。 例えばアプリの画面に配置されている特定の要素をタップしたときに、どのようなことが起こるのか、といったことは更新型である画面仕様書に記載されます。施策が実施され動作が変更になった場合はドキュメントを更新します。これを読むことで常に正しい動作を知ることができます。 一方で施策を実施するときのドキュメントは記録型ドキュメントであり、施策を実施する目的や実施した結果などが記載されます。これを読むことで過去の施策を実施した当時の状況を知ることができます。 このように2種類のドキュメントが伝えるものは大きく異なります。アプリケーションの動作を表したいのであれば、記録型である施策企画書を積み重ねるのではなく画面仕様書や機能仕様書といった更新型ドキュメントで現在の正しい動作を記載し、更新し続けることが重要です。 以上のことをまとめると次のようになります。 更新型ドキュメント 目的: 現在の正しい情報を提供する 更新方針: 変更があるたびに内容を更新 例: 機能仕様書、画面仕様書、API仕様書、作業手順書 記録型ドキュメント 目的: その時点での情報や判断を記録する 更新方針: 原則として後から変更しない 例: 議事録、施策企画書、ADR(技術的意思決定の記録)、日報 ドキュメントではひとつのことにフォーカスする ひとつのドキュメントには情報を詰め込みすぎないようにしましょう。 もしひとつのドキュメントに複数の目的を持たせてしまうと、そのドキュメントをどのように扱うのか、更新すべきかどうか、どのようなときに見たら良いのかといったことがわからなくなってしまいます。 そのような混乱を招かないためにもひとつのドキュメントにはひとつの関心事のみを記載するようにしましょう。 実践的な運用方法 ドキュメントの命名を工夫する ドキュメントはタイトルを見て一目でどのようなドキュメントなのかを判別できることが重要です。 命名規則の例 議事録: 20240115_UX向上定例 機能仕様書: 【仕様】ユーザー登録機能 ADR: ADR-001_データベースエンジン選定 施策ドキュメント: 【施策】20240202_ログイン率改善 調査ログ: 📝特定の端末で動画の再生が不安定になる アイコンやプレフィックスを統一することで、ドキュメント一覧での視認性が向上します。チーム内で命名ルールを決めておくことで、必要な情報により早くたどり着けるようになります。 命名を考えるためにはどのような目的のドキュメントなのかをはっきりさせる必要があります。命名を意識することは結果としてドキュメントの目的を意識することにも繋がります。 開発サイクルとドキュメント 前章で触れたように、ドキュメントには固有の役割があります。これらは施策を回す際にどのフェーズにはどのようなドキュメントが必要なのかということに結びつきます。 ひとつの例を見てみましょう。 施策を実施するときにいきなり機能仕様書や画面仕様書を作ることはないでしょう。最初に作るのは施策企画書です。この施策企画書は新規に作ることもありますが、既存の改善策の場合はすでにある機能仕様書などを元に作成します。元の状態をどのようにしたいのかがミーティングで検討され、施策の実施を決定し、その施策の詳細がドキュメントに落とし込まれます。 次に施策企画書を元に機能仕様書や画面仕様書を作成、及び更新します。ここで記録型ドキュメントの情報が更新型ドキュメントに落とし込まれることになります。これらのドキュメントを元にエンジニアが実装を行います。 機能がリリースされた後に実施した結果を分析し、その分析結果を追記することで施策企画書は完成します。そしてまた新しい施策が実施されるサイクルが回ります。 開発サイクル まとめ ドキュメントを「残す」から「活用する」へ変えていくには、以下の3つの原則を意識することから始まります。 目的に応じた使い分け: 更新型(常に最新)と記録型(時点記録)の性質を理解する 単一責任の原則: 一つのドキュメントには一つの関心事のみを記載する 見つけやすさの確保: 命名規則とライフサイクル管理で必要な情報にたどり着きやすくする これらを実践することで、「ドキュメントはあるけれど活用できない」という問題を解決し、チーム全体の生産性向上につながります。まずは現在のドキュメントを更新型・記録型で分類することから始めてみてください。
はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 エブリーでは元々Savings Plansを使用しており、AWSのコスト最適化を行っています。しかし、Savings Plansをどれだけ購入するかの意思決定は、使用量の予測やコスト削減効果の見積もりが難しく、なかなか判断に迷うことがありました。 昨年発表されたSavings Plans Purchase Analyzerを使ってみたので、今回はその機能と使い方について紹介させていただきます。 Savings Plansとは Savings Plansは、1年間または3年間の指定量のコンピューティング処理(1時間ごとに測定)を使用するコミットメントと引き換えに、オンデマンド料金を超える削減を提供するサービスです。 主な特徴 最大72%の節約 : AWSコンピューティングワークロードで最大72%の節約が可能 柔軟性 : インスタンスファミリー、インスタンスサイズ、OS、テナンシー、またはAWSリージョンに関係なく適用 幅広いサービス対応 : Amazon EC2、AWS Fargate、AWS Lambda、Amazon SageMaker AIインスタンスに対応 料金の固定 : プラン期間中は使用量に対して支払う料金が変わらない 支払いオプション : 全額前払い、一部前払い、前払いなしから選択可能 Savings Plansのタイプ AWSには3種類のSavings Plansが用意されています: Compute Savings Plans 最大66%の割引 : オンデマンド料金から最大66%の割引 最も柔軟 : インスタンスファミリー、インスタンスサイズ、リージョン、OSに関係なく適用 対応サービス : EC2インスタンス、Fargate、Lambda EC2 Instance Savings Plans 最大72%の割引 : オンデマンドから最大72%の割引 特定リージョン・ファミリー : 選択したリージョンの特定インスタンスファミリーにコミットメント 柔軟性 : インスタンスサイズ、OS変更が可能 SageMaker AI Savings Plans 最大64%の節約 : オンデマンド料金から最大64%の節約 SageMaker専用 : SageMaker AIインスタンスの使用に自動適用 柔軟性 : インスタンスファミリー、インスタンスサイズ、リージョン、コンポーネントに関係なく適用 Savings Plans Purchase Analyzerとは Savings Plans Purchase Analyzerは、Savings Plansの潜在的な購入をモデル化して評価できる機能です。購入前に削減額、カバレッジ、使用率への影響を確認することができます。 主な機能 購入シミュレーション : 推奨購入金額またはカスタム金額での分析 コスト削減効果の確認 : 毎月の推定削減額やカバレッジ率の評価 柔軟なパラメータ設定 : ルックバック期間や期限切れSavings Plansの除外設定 実際に使ってみる 1. Purchase Analyzerへのアクセス Billing and Cost Management コンソール を開く ナビゲーションペインの Savings Plans で、 Purchase Analyzer(購入アナライザー) を選択 2. 分析パラメータの設定 分析のパラメータがあるので、要件に合うように選択していきます。 Savings Plansタイプの選択 どのSavings plansのタイプを利用するか選択します。 - Compute Savings Plans : EC2、Lambda、Fargateなど幅広いサービスに対応 - EC2 Instance Savings Plans : EC2インスタンス専用 - SageMaker Savings Plans : SageMaker専用 期間 期間は 1年 または 3年 から選択します。 支払いオプション 支払いオプションは 全額前払い 、 一部前払い または 前払いなし から選択します。 ルックバック期間 分析の対象とする期間を選択することができます。 7日 、 30日 、 60日 が用意されている他にカスタムで、過去60日間の任意の範囲を選択することができます。 通常時の利用と違う利用の仕方をしていた際に、その範囲を除外して通常の利用でのおすすめのコミットメント金額を分析することができます。 期限切れの Savings Plans を除外 90日以内に期限切れのSavings Plansを対象として除外した値で分析をすることができます。 弊社でもそうだったのですが、Savings Plans Purchase Analyzerを利用するタイミングでは前回のSavings Planが残っていることが多いかと思うので除外した方が実態に沿った分析ができるかと思います。 コミットメント設定 コミットメントには、 推奨 と カスタム の2種類があり、基本的には 推奨 で良いかと思いますが、時間単位のコミットメント金額を変更して比較する用途などに使えるようです。 3. 分析結果の確認 分析を実行すると、以下のような結果が表示されます。 Savings Plan の推奨事項とSavings Plan 購入後の推定オンデマンド費用のそれぞれでコスト、カバレッジ、使用率を確認できます。 また、推奨されるSavings Planのコミットメント金額・推定削除額も出力されるので、前述のコミットメント設定を変更してみて影響を見ることもできます。 まとめ Savings Plans Purchase Analyzerを使うことで、購入前にコスト削減効果を詳細に分析できることが分かりました。 実際の購入前に、Purchase Analyzerでシミュレーションを行い、自社の使用パターンに最適な設定を見つけることが大切だと感じました。 参考 購入する Savings Plans の決定 - Savings Plans
はじめに こんにちは、リテールハブ開発部の杉森です。 皆さんは「日常業務の中で新しい技術に挑戦する時間が取れない」と感じたことはありませんか?エブリーでは、そんなエンジニアの想いに応えるため、「挑戦WEEK」を開催しています。 挑戦WEEKとは? 挑戦WEEKは、通常の事業部のロードマップから離れ、エンジニアが自ら提案したテーマに基づいて技術的な挑戦に集中する特別な期間です。従来は1週間かけて実施していましたが、第6回となる今回は運営の軽量化を図り、 2日間の集中開催 という新しい形式で2025年7月16日〜17日に開催しました。 今回のテーマは「プロダクトにAIを組み込む」と「今までAIを実務で使えてない分野へ導入」です。生成AIの急速な進化により、実プロダクトへの適用可能性が大きく広がった今、各チームがどのような挑戦を行ったのかをご紹介します。 挑戦WEEKの詳しい背景については、こちらの記事もご参照ください。 tech.every.tv 第6回挑戦WEEKでの変更点 今回は「より多くのエンジニアが参加しやすく、かつ高い成果を生み出せる」ことを目指し、以下の4つの内容を変更しました。 1. 開催期間の短縮 従来の1週間開催から 2日間+成果発表会 へと短縮しました。これにより通常業務への影響を最小限に抑えつつ、集中的に取り組める環境を実現しました。 2. 既存チームでの実施による効率化 これまでの開発本部横断的なチーム編成から、既存チームをベースとした編成に変更しました。 新規チーム編成では、ドメイン知識の共有や開発環境の構築に貴重な時間を費やしてしまいます。一方、既存チームであれば、 プロダクトの背景や課題を深く理解している 開発環境がすでに整っている チームワークが確立されている これらの利点により、 2日間という限られた時間を100%挑戦に集中 でき、より実践的で価値のある成果につながりました。 3. AIに特化したテーマ設定 今回は挑戦テーマを下記2点に絞り 明確化 しました。 プロダクトにAIを組み込む 今までAIを実務で使えてない分野へ導入 この明確なテーマ設定により、参加者からは「適度な制約があることで、むしろ挑戦内容を具体化しやすくなった」「AIという共通言語があることで、他チームの発表も理解しやすく学びが多い」といったポジティブなフィードバックを得ることができました。 4. 主体的な参加を促す任意参加制 全員参加から 任意参加制 へと変更し、参加希望者がマネージャーと相談の上で参加を決定する方式を採用しました。 この変更により、参加者全員が高いモチベーションを持って取り組む環境となり、限られた時間でも質の高い成果が続々と生まれることとなりました。 第6回挑戦WEEKについて 今回は 10チーム、総勢31名 のエンジニアが参加し、それぞれのプロダクト課題に対してAIを活用した解決策に挑戦しました。 取り組み内容(一部抜粋) 既存AI処理の高速化 課題: デリッシュAIの既存処理において、複雑な処理が直列に積み重なることによって、レスポンスタイムが遅くなる事象が発生していた。 実施内容: 処理の並列化やLLMの問い合わせの改善の実施と検証。 自作Redash MCPサーバーの作成 課題: SQLを書くことに慣れていないメンバーが既存のRedashクエリの調査や修正を行う際に、手作業での事前準備やクエリ集計結果の確認に手間がかかるという課題があった。 実施内容: Redashと連携しされた、Cursor経由で呼び出し可能なMCPサーバーの開発。 Figma MCPを利用した開発/Playwright MCPを利用したE2Eテストの実施 課題: リリースサイクルの高速化に適応するためのクライアントサイドの実装とテストの高速化が必要だった。 実施内容: Figma MCPを利用した管理画面(Vue.js)とアプリ(Flutter)の開発の検証。Playwright MCPを利用した、自然言語ベースの自動テストの検証。 成果発表会の様子 2025年7月18日(金)に開催した成果発表会では、各チームが熱のこもったプレゼンテーションを行いました。技術的な内容はもちろん、実際のデモを交えた発表や知見の共有、発表後の質疑応答など、参加者全員が多くの学びを得る機会となりました。 今後の展開 第6回挑戦WEEKは、運営形式を大きく変更したにも関わらず、期待を上回る成果を生み出すことができました。 2日間という短期間での成功を受けて、運営チームでは 開催頻度の向上 を検討しています。より頻繁に開催することで、「挑戦」をエブリーのエンジニアにとって日常的な文化として定着させていく予定です。 ※5日間の通常版の挑戦WEEKも年に1回くらいの頻度で開催したいと考えています。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 エブリー開発部で先日行われた挑戦weekの中で、私たちのチームが開発したRedash MCPについて紹介していきます。 背景 現在弊社ではAIコードエディタのCursorをエンジニアとPdMに配布しています。 日々のデータ分析にはRedashを用いることが多く、PdMはCursorを使用してRedashのクエリ作成なども行います。 今回は業務改善の一環として、Redash MCPを作ることでデータ分析をより効率化し、PdMの負荷削減をしたいと考えました。 PdMが抱えていた主な課題 具体的な実装に入る前に、まずはPdMが抱える課題があるかヒアリングしました。 ヒアリングの結果、以下のような課題を得ることができました。 1. Cursorを使ってRedashでクエリを作成・更新する際に、事前準備が必要でそれが少し手間 Cursorは基本的にRedashで扱えるデータの情報を知りません。 そのため、Cursorを使ってクエリを書く際は「テーブル構造を教える」などの事前準備になります。 毎回プロンプトに書いたり、扱うテーブル構造を確認しつつドキュメント化することでCursorに認識してもらえますが、Cursor自身がRedash上の情報を取りにいける方が効率的です。 2. 普段Redashを触らないメンバーからクエリ修正の依頼がきて少し大変 例えば「このクエリの集計結果を一週間から一ヶ月で出してほしい」などの簡単な依頼がPdMに来ることもあります。 Redashに慣れていなかったり、クエリが書けないようなメンバーでもある程度自分で解決できると嬉しいです。 3. 数値の変化はRedashのクエリ結果をみないと把握できず、異常な値が出ているときに即座に気づけない Redashにアラート機能はあるものの、柔軟な設定ができるわけではないため、より柔軟に異常値を検出できると嬉しいです。 Redash MCPの実装について 主な使用技術 今回作成したRedash MCPの使用技術を紹介します。 Go 1.23.0 go-sdk 0.2.0 MCPの公式SDK Docker Redash(APIで利用) ※2025年8月8日現在go-sdkの安定版はリリースされていませんが、今後リリース予定のようです。 作成した機能 Redash APIが用意しているエンドポイントのうち、実際によく使いそうなエンドポイントに絞って下記のような操作ができるようにしました。 データソース一覧取得 データソース詳細取得 クエリ一覧取得 特定のクエリ詳細取得 クエリ実行、実行結果取得 クエリ作成 クエリ更新 ダッシュボード情報取得 実装例 以下がRedash MCPサーバーの簡単な実装例です。 ここでは1つのクエリ情報を取得してくるケースを考えます。 package main import ( "context" "log" "github.com/modelcontextprotocol/go-sdk/mcp" ) // クエリ取得のパラメータ type GetQueryParams struct { ID int `json:"id" jsonschema:"クエリID"` } // クエリ取得の実装 func GetQuery(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[GetQueryParams]) (*mcp.CallToolResultFor[any], error ) { // 実際にはここでRedash APIを呼び出してクエリ情報を取得する queryInfo := "クエリID: " + string (params.Arguments.ID) + "の情報" return &mcp.CallToolResultFor[any]{ Content: []mcp.Content{&mcp.TextContent{Text: queryInfo}}, }, nil } func main() { // MCPサーバー作成 server := mcp.NewServer(&mcp.Implementation{Name: "redash" , Version: "v1.0.0" }, nil ) // ツールの追加 mcp.AddTool(server, &mcp.Tool{ Name: "get-query" , Description: "Redashのクエリを取得する" , }, GetQuery) // サーバーを起動 if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil { log.Fatal(err) } } これでRedash MCPサーバーの実装は完了です。 go-sdkを使うと、MCPサーバーの実装が非常に簡単でした。 ▪dockerで起動してCursorで使う場合の実装例 下記のようにDockerfileを準備します。 FROM golang:1.23 as builder WORKDIR /app COPY . . RUN go mod tidy && go build -o mcp-server main.go FROM debian:stable-slim WORKDIR /app RUN apt-get update && apt-get install -y ca-certificates openssl && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/mcp-server . ENTRYPOINT ["/app/mcp-server"] イメージをビルドします $ docker build -t redash-mcp . Cursorののmcp.jsonに下記の内容を追加します。 { " mcpServers ": { " redash-mcp ": { " command ": " docker run -i --rm -e REDASH_URL -e REDASH_API_KEY redash-mcp ", " env ": { " REDASH_URL ": " https://hoge.redash.hoge ", " REDASH_API_KEY ": " hogehoge " } } , } } 使用例 実際にCursorからRedash MCPを使ってみた例が以下です! 最近追加されたレシピを取得するクエリの作成、実行、さらに簡単な分析までしてくれました。 ヒアリングした課題に対する結果 本記事の冒頭で挙げた課題に対して、Redash MCPでどのくらい解決できたか見ていきます。 1. Redashでクエリを作成・更新する際に、事前準備が必要でそれが少し手間 RedashのデータソースをMCP経由で取得させることにより、事前準備の削減ができました。 2. 普段Redashを触らないメンバーからクエリ修正の依頼がきて少し大変 クエリ作成のハードルは一定下がったものの、現状だとCursorなどのMCPクライアントの準備や初期設定が必要なため、誰でも気軽に使用できる状態にはできていません。 3. 数値の変化はRedashのクエリ結果をみないと把握できず、異常な値が出ているときに即座に気づけない 短い期間での実装だったため、この課題に対してのアクションはできていませんが、 例えばDevinと連携して定期的に異常値を見つけてもらう、などのことが出来ると良いなと考えています。 PdMからのフィードバック 何人かのPdMに実際に使っていただき、以下のような嬉しいコメントをいただきました! クエリ内のコメントもちゃんと書かれてて可読性も良い クエリ作成が秒で終わって感動 redashが複数のキーワード検索できないので助かる クエリの差分を比較するときに、わざわざクエリコピーする手間がなくなった 一方で、改善点も見つかりました。 Cursor環境やターミナルを皆が使えるわけではないから、slackなどから呼べると使える人の幅が広がって嬉しい デフォルトでCursorに入れるべきルールみたいなものがあると良さそう Redashと連携しているTreasureData専用のtd関数全然使ってくれなくて、結構エラー吐かれたので、プロンプト頑張らなきゃいけないなと思いつつ、プロンプトが最初からあると嬉しい カラムに実際どんなデータが入ってるか推測を間違える時があるから、その辺をより精度上げられると嬉しい 今後の展望 PdMからのフィードバックなどをもとに、今後はこんなことをやっていきたいです。 1. Redash MCPを使うまでのハードルを下げる まず、リモートMCPサーバーにしたいです。 現在はMCPをローカルで起動させていますが、この方法だと 初期設定時にローカルでビルドする必要があり手間 アップデートのたびにローカルでビルドが必要になる などの問題があります。 リモートMCPサーバーにすることでこれらの問題を解決したいです。 また、セキュリティ面なども考慮しつつ 可能であればDevinを使ってSlackから呼び出すなども出来るようにしていきたいです。 2. 事前に渡す情報の整備 弊社のRedashで扱っているデータソースには、DBのデータ以外にもTreasureDataやFirebaseなどの他サービスと連携して取得しているデータがあります。 特に、他サービスから取得してきたデータのスキーマ情報は、Redashで詳細まで持っていないことが多いです。 ドキュメントとしてまとまっているものがあるので、それを利用することによりRedash MCPの精度を上げていきたいです。 その他、随時フィードバックをいただき改善していきたいです。 まとめ 本記事では、社内Redash MCP作成の背景、実装例、実際の使用感について紹介しました。 今回PdMの作業負荷軽減を目的としてRedash MCPを作りましたが、エンジニアにとってもRedashを使うハードルが低くなるという副次的な効果があったかなと思います。 実際、私はRedash MCPを利用し始めてからサクッとクエリを作れるようになりました。 今後も皆がより使いやすくなるように改善していきたいです。 最後まで読んでいただきありがとうございました! 参考文献 https://github.com/modelcontextprotocol/go-sdk https://modelcontextprotocol.io/docs/getting-started/intro
エブリーで小売業界に向き合いの開発を行っている @kosukeohmura です。 今回は、Flutter アプリケーションに AWS Cognito を使った認証機能を導入したプロセスについて紹介します。 バックエンドで Cognito をラップするか、アプリから直接 Cognito に接続するか Cognito を IdP として採用し認証機構を新たに開発するにあたり、バックエンドを Cognito をラップする実装方針と、アプリから直接 Cognito に接続する方式を検討しました。結論としては今回は Flutter アプリから直接 Cognito に接続する方式を選択しました。 バックエンドで Cognito をラップする方式では、バックエンドで Cognito Admin API を用いた認証処理を自由に書くことが可能ですが、自由に処理を書ける反面、基本的には Cognito を呼び出すだけの独自のエンドポイント群を実装する必要があり、またそのエンドポイントを利用するアプリ側の画面の実装も必要になります。 一方、アプリから Amplify を利用し直接 Cognito に接続する方式では、独自のエンドポイント群を実装することなく認証機構を実装でき、加えて Amplify ですでに用意された認証画面の UI を使用すればアプリのログイン画面などの実装が不要となります。バックエンドでは認証が必要なエンドポイントそれぞれに対してアプリが Cognito から取得したユーザートークンが適正かを判別するミドルウェアを挟む形です。この方式だと、実装する対象が大きく減る一方で、Amplify の機能から外れたことを行おうとすると工夫が必要となるかと思います。 今回私達は ID/PW での認証認可さえできればよく、認証認可前後に独自の処理を挟み込んだりする必要はありませんでした。アプリの画面も一般的なサインイン画面が存在すればよかったので、Amplify のデフォルトの UI コンポーネントを少し改変すれば十分事足りそうでした。こうした背景からアプリから直接 Cognito へ接続する方式のデメリットが問題にならなそうなため、実験的に実装を行い有用性を確かめた後に、その方式を選択することとしました。 導入した主要な変更 公式の Quickstart - AWS Amplify Gen 2 Documentation を参照しながら導入を行います。 1. Amplify Flutter パッケージの追加 まず最初に必要な依存関係を追加します。これにより、Flutter アプリで Amplify の認証機能を使用できるようになります。 flutter pub add amplify_flutter amplify_auth_cognito amplify_authenticator それぞれのパッケージの関係性がよくわかりませんでしたが、QuickStart に記載されている説明が端的でわかりやすいです。 amplify_flutter to connect your application with the Amplify resources. amplify_auth_cognito to connect your application with the Amplify Cognito resources. amplify_authenticator to use the Amplify UI components. 2. Flavor に応じて dart_define ファイルを生成・使用する 環境ごとに Cognito のユーザープールを切り替えるため、まず Flavor に応じて環境変数をアプリへ注入できるようにします。Flavor 環境変数をアプリへ注入するために dart_define ファイルを使用しますが、それをテンプレート化しておきます。 dart_define.template.json { " awsRegion ": " $AWS_REGION ", " cognitoUserPoolId ": " $COGNITO_USER_POOL_ID ", " cognitoUserPoolClientId ": " $COGNITO_USER_POOL_CLIENT_ID " } あらかじめ direnv などを使い環境変数を設定しておき、それを元に実際の dart_define ファイルを生成し利用します。以下それを行う Makefile 例です。 FLAVOR ?= development generate-dart-define: cat dart_define.template.json | envsubst > dart_define_$$FLAVOR.json run-development: $(FLUTTER) run --debug --flavor development --dart-define-from-file=dart_define_development.json この仕組みにより、環境ごとに異なる環境変数を使用できます。 3. Amplify 設定ファイルの管理 GitHub で公開されているスキーマ amplify-backend_packages_client-config_src_client-config-schema_schema_v1.json at main · aws-amplify_amplify-backend に従って、Amplify 設定ファイルを構築します。AWS 上で設定するユーザープールの設定値と被る部分は合わせておきます。一部の設定値はプレースホルダとしておき、アプリ実行時に環境変数で置換します。 lib/amplify_outputs.json { " version ": " 1.1 ", " auth ": { " aws_region ": " <Will be replaced when building> ", " user_pool_id ": " <Will be replaced when building> ", " user_pool_client_id ": " <Will be replaced when building> ", " password_policy ": { " min_length ": 12 , " require_lowercase ": false , " require_uppercase ": false , " require_numbers ": false , " require_symbols ": false } , " username_attributes ": [ " email " ] , " user_verification_types ": [] , " unauthenticated_identities_enabled ": false , " mfa_configuration ": " NONE " } } この内容をアプリ実行時に環境変数で上書きすることで、各環境に適した設定が適用されます。 4. アプリ起動後に Amplify をセットアップする 準備した Amplify 設定ファイルと環境変数を使用し、アプリ起動後に Amplify プラグインの初期化を行います。それを行うための AmplifyService を実装しました: lib/services/amplify.dart class AmplifyService { final Ref _ref; final BundleRepository _bundleRepository; @override Future<Result< void , MyAppException>> setupAmplify() async { try { await Amplify.addPlugin(AmplifyAuthCognito()); final amplifyConfigResult = await _buildAmplifyConfig(); if (amplifyConfigResult.isError) throw amplifyConfigResult.error; await Amplify.configure(amplifyConfigResult.okValue); return Result.ok( null ); } catch (e, s) { return Result.error(MyAppException(e, s)); } } // lib/amplify_outputs.json ファイルを取得し Map として返却 Future<Result< String , MyAppException>> _buildAmplifyConfig() async { final amplifyConfigString = await _bundleRepository.readBundle(BundlePath.amplifyConfig); final amplifyConfig = jsonDecode(amplifyConfigString) as Map< String , dynamic >; amplifyConfig[ 'auth' ][ 'aws_region' ] = /* 環境変数から注入 */ ; amplifyConfig[ 'auth' ][ 'user_pool_id' ] = /* 環境変数から注入 */ ; amplifyConfig[ 'auth' ][ 'user_pool_client_id' ] = /* 環境変数から注入 */ ; return Result.ok(jsonEncode(amplifyConfig)); } } あとは、アプリケーションのエントリーポイントで AmplifyService を使用します: Future< void > main() async { // ... 既存の初期化処理 final amplifySetupResult = await container.read(amplifyServiceProvider).setupAmplify(); if (amplifySetupResult.isError) { logger.fatalException( 'Error occurred when configuring Amplify' , amplifySetupResult.error); } runApp(UncontrolledProviderScope(container: container, child: const MyApp())); } これで Amplify のセットアップが完了しました。加えて QuickStart どおりに UI コンポーネントを記述すればサインイン/サインアップ画面が表示されますが、特段難しいことはなく省略します。 まとめ 今回の実装では、Amplify UI を使用することで比較的スムーズに認証機能を導入できました。バックエンド経由ではなく Flutter から直接 Cognito に接続することで、開発速度を重視しながらもセキュリティを保つことができました。 また、環境間で可変な値を環境変数で管理し、テンプレートファイルを使用することで、異なる環境(開発・本番)へ向けて安全にアプリを実行できるようになりました。Amplify セットアップ Service クラスに煩雑な処理を押し込むことで、アプリケーションのエントリーポイントに持たせる責務を最小限に抑えることができました。 Flutter での認証実装を検討されている方の参考になれば幸いです。 参考資料 Amplify Docs - AWS Amplify Gen 2 Documentation Amplify UI - Build UI fast with Amplify on Flutter
はじめに 前提 技術スタック 監視の構成 現状の課題 Devin選定理由 1. マルチRepository対応の容易さ 2. 自律型AIとしての運用効率 構成 手順 Playbook 1. Devinの起動とSentryアラート情報の取得 2. Devin Playbookの実行 2-1. 関連情報の取得 2-2. 分析 2-3. 追加情報の取得 2-4. 図表の作成 3. Slackへ出力 結果 運用してみた結果 運用してみて見えてきた課題 まとめ はじめに 開発本部でデリッシュキッチンのアプリウェブグロース向けの開発を担当している hond です! 今回は先日行われた 第6回挑戦week で私たちのチームが行ったDevinによるSentryアラートの改善について紹介します。 本記事はDevinの基本的な概念や使い方についてある程度理解している読者を対象としています。Devin自体の詳細な説明は省略し、実際の業務での活用事例に焦点を当てて解説します。 なお、今回の取り組みを検討する際に、私たちの目標と完全に一致した SMARTCAMPさんの記事 を発見し、非常に参考になりました。私たちの環境や要件に合わせてカスタマイズを行いながら、同記事のアプローチを参考にさせていただきました! 前提 技術スタック 本記事で扱う技術スタックは以下の通りです。 項目 技術 エラー監視 Sentry 通知 Slack AI開発支援 Devin 図表生成 Mermaid バージョン管理 GitHub インフラ AWS アプリケーション Nuxt.js (フロントエンド)、Go (バックエンド) 監視の構成 デリッシュキッチンでは現在アプリケーションのエラー監視ツールとして Sentry を用いています。このSentryにはNuxt.jsで動作するデリッシュキッチンのウェブアプリケーション、およびGoのバックエンドサーバーで発生したエラーが送信される仕組みとなっています。Sentryに送信されたエラーはIntegrationを介してSlackのアラートチャンネルに通知されます。 現状の課題 このアラート運用に関して、主に以下の課題がありました: 緊急度がわかりづらい :アラートの重要度が不明瞭で、対応の優先順位付けが困難 解決に必要な情報が不明瞭 :一部のエラーではSentryに送られる情報が不足しており、特にスタックトレースがない場合に問題解決に必要な情報を特定するのが困難 調査手順が不明 :AWSコンソール、Sentry、アプリケーションコード、Athenaなど、複数のリソースを横断して調査する際の明確な手順が確立されていない。 放置されているアラートがある :これにより、開発者の心理的負担が増加。 アラート箇所の担当が不明瞭 :どのエラーがどの開発チームの担当か不明確。 Devin選定理由 今回の課題解決において、私たちがDevinを選定した理由は大きく2つあります。 1. マルチRepository対応の容易さ 事前に他のメンバーがCursorやClaude Codeの検証を社内勉強会で行いました。しかし、どちらもRepositoryを跨いだ調査において以下の課題が発生しました。 複数Repositoryの設定に追加作業が必要 設定完了後もRepositoryを横断した処理の解釈が期待値を下回る Devinでは、Workspace内に必要なRepositoryをcloneするだけで設定が完了し、フロントエンドとバックエンド、インフラのコードを横断した調査が効率的に行えました。 例えば、インフラのコードを解釈してフロントエンドがAWS上でどこに位置するかを理解し、どのバックエンドサーバーと関係があるかを意識したレスポンスを行えていました。 2. 自律型AIとしての運用効率 Devinの自律型特性により、以下の運用メリットを実現できました。 並行処理 : エンジニアが行うエラー発生時の一次対応と並行してDevinがエラー調査を自動実行 履歴の可視化 : 調査プロセスがDevinセッション内に記録され、チーム全体で共有可能 手順の標準化 : 人的要因に依存しない一貫した調査品質を担保 構成 最終的に構築したSentryアラート自動分析システムの構成は下記の通りです。 手順 Devinの起動とSentryアラート情報の取得 Devin Playbookの実行 関連情報の取得 分析 追加情報の取得 図表の作成 Slackへ出力 Playbook Sentry APIにCurlコマンドでアラートの情報を取得して、下記の分析結果を出力してください。 調査手順として以下の4ステップを順番に実施すること: --------------- ## 1. Sentry APIを叩いてレスポンスを取得 Sentry APIは、下記コマンドを実行してください。アラート全体はissue, イベント詳細はissue and event を叩いてください。secretはSENTRY_API_TOKENを参照してください。 issue:""" curl https://sentry.io/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/ \ -H 'Authorization: Bearer {sentry_api_token}' """ issue and event:""" curl https://sentry.io/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/events/{event_id}/ \ -H 'Authorization: Bearer {sentry_api_token}' ## 2. 対象となるGit Repositoryの特定 エラーのスタックトレースや関連情報から、問題が発生しているRepositoryを特定します。フロントエンド(Nuxt.js)、バックエンド(Go)、インフラ(Terraform)のいずれかを判断し、適切なRepositoryに焦点を当てます。 ## 3. 関連するGit Repositoryが他にないか調査 特定されたRepository以外にも、エラーに関連する可能性があるRepositoryがないかを調査します。例えば、フロントエンドエラーがバックエンドAPIの変更に起因する場合や、インフラ設定の変更が影響している場合などを考慮します。 ## 4. エラーの詳細に関する調査と結果の出力 下記「分析結果フォーマット」に従い、必要な情報を調査してください。 ### 4-1. インフラ構成図やアーキテクチャ図が必要な場合は出力 エラー箇所や原因、全体像を把握できるよう、A. AWSのインフラ構成図, B. システム概要図 の2つを添付してください。 出力方法は以下の通りで、A, Bそれぞれに対して順番に実行してください。 1. mmdファイルを出力 (mmdファイル名はA: infra.mmd, B: system.mmd とする) 2. 次のコマンドを実行:mmdc -i <mmdファイルパス> -o <出力ファイル名(PNG)> (出力ファイル名はA: infra.png, B: system.png とする) 3. 出力画像を添付 ### 4-2. 関連Commitの検索 gh コマンドまたはgit コマンドを使用して、アラートの根本原因となるコードがどのcommit, PRで追加・変更されたのかを特定してください。 ------------------------- 実装は行わずに、「分析結果フォーマット」「コード提示ルール」に従ってレポートを出力してください。 分析結果フォーマット:""" 📕エラーの基本情報確認 ・エラータイプ ・環境 ・発生頻度 ・影響範囲 ・発生時間(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) ・初回(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) ・最終(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) 🧑⚕️ 障害レベル判断 ・🚨緊急度高:即時対応必要(サーバーダウンへの影響など) ・🟡緊急度中:チケット切って今週中に対応(特定条件下でのエラー、ユーザー影響なし) ・🔵緊急度低:チケット切って時間ある時に対応(軽微な不具合、パフォーマンス低下) 🔬 エラー原因判定 ・システム側の問題(バグ、設定ミス、リソース不足など) ・外部要因(攻撃的リクエスト、外部サービス障害など) 🦎 関連のあるCommit,PR ・根本原因となるコードがどのcommit, PRで追加・変更されたのか 🧑💻 エラーの再現方法 ・ローカル環境や開発環境でエラーの再現方法の提示 🚒 対応策策定 ・一時的対応と恒久的対応の区別 ・攻撃的リクエストの場合のセキュリティ対策 """ コード提示ルール: 関連コードを示す必要があれば、以下のようにコードとGitHubのパーマリンクを含める形で出力してください [delishkitchen_file_name](https://github.com/delishkitchen_file_name#L92-L98) \``` Sentry.withScope(function (scope) { scope.setTag('method', method) scope.setTag('url', path) scope.setTag('query', queryString) Sentry.captureException(err) }) \``` --------------- この手順に従って段階的に調査を進めること。 You only need to look in the following repos: {backend_repository}, {infra_repository}, {frontend_repository} 以降、詳細について解説します。 1. Devinの起動とSentryアラート情報の取得 Devinの起動にはSlack workflowを用いました。DevinとSlackを連携することで、 @Devin でDevinをSlackのスレッドやチャンネルで会話を開始できます。これにより、同時にセッションも開始されます。そのため、今回は解決したいSentryのメッセージに :help-me-devin: とリアクションすることでそのスレッドに下記の文章が出力されるようにしました。 !sentryanalyzer は前述のDevin Playbookのマクロとなっています。 @Devin !sentryanalyzer @user_name によって依頼されました。 依頼者はDevinのレポートが終わり次第、sleep と入力してDevinを休ませてあげてください。 2. Devin Playbookの実行 手順1までの処理でDevin sessionの起動とSlackメッセージの紐付けが完了しているため、ここから実際にDevinの処理が開始されます。 2-1. 関連情報の取得 次に下記操作で関連する情報の取得を行います。 Sentry APIの実行 対象となるGit Repositoryの特定、関連するGit Repositoryが他にないか調査 Sentry APIは事前にDevin SecretにSentry API Tokenを追加しているのでそれとSlackのメッセージからSentry issue_idを取得して Retrieve an Issue と List an Issue's Events を実行してissueの詳細を取得します。 Repositoryの特定、調査は事前にDevin's Workspaceに紐付けてあるRepositoryを探索するようにしています。 2-2. 分析 次にこれらの情報をもとにエラーの原因の分析を行わせます。ここまででSentryの詳細と対象のコードの目星がついているのでそれをもとに分析を行わせています。 分析の内容としては最終的な出力に含まれるSentryをもとにした発生頻度などのエラーの基本情報、障害レベル、エラーの原因、エラーの再現方法、対応策になります。 2-3. 追加情報の取得 2-2の結果をもとに原因となるcommitの特定を gh コマンドを使って取得します。 2-4. 図表の作成 エラーの理解を深めるため、Mermaidを使用してAWSインフラ構成図とシステム概要図を自動生成します。これにより、エラーが発生している箇所とシステム全体の関係性を視覚的に把握できます。 インフラ構成図(infra.mmd → infra.png) : AWSリソース間の関係性とデータフローを表示 システム概要図(system.mmd → system.png) : アプリケーション層とインフラ層の相互作用を表示 3. Slackへ出力 2までの情報をもとにPlaybookに記述されている下記の形で情報を整理し、スレッドに返信を行ってもらいます。 📕エラーの基本情報確認 ・エラータイプ ・環境 ・発生頻度 ・影響範囲 ・発生時間(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) ・初回(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) ・最終(yyyy-mm-dd HH:MM:SSの形式、JST変換必須) 🧑⚕️ 障害レベル判断 ・🚨緊急度高:即時対応必要(サーバーダウンへの影響など) ・🟡緊急度中:チケット切って今週中に対応(特定条件下でのエラー、ユーザー影響なし) ・🔵緊急度低:チケット切って時間ある時に対応(軽微な不具合、パフォーマンス低下) 🔬 エラー原因判定 ・システム側の問題(バグ、設定ミス、リソース不足など) ・外部要因(攻撃的リクエスト、外部サービス障害など) 🦎 関連のあるCommit,PR ・根本原因となるコードがどのcommit, PRで追加・変更されたのか 🧑💻 エラーの再現方法 ・ローカル環境や開発環境でエラーの再現方法の提示 🚒 対応策策定 ・一時的対応と恒久的対応の区別 ・攻撃的リクエストの場合のセキュリティ対策 結果 運用してみた結果 実際に運用してみて以下の効果を確認できました。 Sentryアラートの一時対応についてはDevinに任せられるようになった アラート発生から約3分で初期調査〜原因特定まで完了するようになった フロントエンド・バックエンド・インフラの各Repositoryを跨いだ調査が自動で実行されるため、調査漏れがなくなった Mermaidで生成される図表が分かりやすく、エラー箇所の把握が圧倒的に楽になった 個人的には、特に調査の一貫性が保たれるようになったのが一番良いポイントだと感じています。人によって調査の深さにばらつきがあったのが、Devinを使うことで毎回同じレベルの調査が担保されるようになりました。 これによりアラート対応のハードルが下がったので、属人化も防げるのではないかと考えています。 運用してみて見えてきた課題 一方で、いくつか課題も見えてきました。 AWSのログやモニタリング情報と連携した調査はまだできていない GitHubのパーマリンク出力や関連リポジトリの把握で、結果にばらつきが出ることがある 複雑な指示を一度に出すより、段階的に指令を出した方が精度が上がる傾向がある 特に3つ目については、最初は全部まとめて指示していたのですが、段階的に指示を出すことで明らかに結果の品質が向上したので、運用のコツとして覚えておきたいポイントでした。 まとめ 私自身、普段CursorやClaude Codeを使う際はAIとの会話を往復することを前提に雑なプロンプトを投げたりするので、自立できるように十分なプロンプトを与える必要がある点で自立型AIエージェントの扱いに苦労しました。Playbookのようなテンプレート機能があるので、繰り返しを伴うタスクでは効力を発揮することは分かりつつも、普段の業務で使うイメージは少しわきにくかったです。 その点、アラート対応は決まった動きをすることが多いので、今回のPlaybookに加えてアラート対応が得意なメンバーの普段の動きを共有いただくことで、さらに精度のいい対応ができるのではないかと感じました。 また、この実装を行った後に Devin MCP Marketplace が公開されたので、これを用いてさらにコンテキスト量を増やすことで性能がどうなるかは今後試してみたいです。 ここまで読んでいただきありがとうございます。Devinでアラート初動対応を爆速化しようとしている方の参考になったら幸いです。
はじめに こんにちは、トモニテで開発を担当している吉田です。 AWS を活用したサービス運営において、IaC(Infrastructure as Code)ツールの選択は長期的な運用効率に影響することがあります。 本記事では、実際に私たちが経験した Serverless Framework v3 から lambroll と Terraform への移行事例をもとに、 移行の背景から具体的な手順、そして移行を通じて得られた知見についてまとめています。 なお、移行先の候補検討や各ツールの比較については、 前回の検証編記事 で詳しく解説していますので、併せてご参照ください。 背景:Serverless Framework を取り巻く環境変化 適切な IaC ツールの選択は大切な判断の一つです。代表的なツールとしては、 Serverless Framework 、 AWS SAM 、 Terraform などが挙げられます。これらのツールは、それぞれ異なる特徴と利点を持ち、プロジェクトの要件に応じて選択することが多いです。 中でも、Serverless Framework は多くのプロジェクトで採用されている有力な選択肢の一つです。しかし、2024 年の v4 リリースに伴い、ライセンス形態の大幅な変更が発生しました。( 参考 ) こうした背景から、私たちは代替手段の検討を開始しました。ここからは、実際に私たちが行った移行プロジェクトの記録と、そこから得られた教訓について紹介します。 移行の背景 Serverless Framework v4 の変更点 2024 年、Serverless Framework v4 がリリースされ、従来のオープンソースモデルから舵を切りました。 ライセンス形態の変更 : 一定以上の収益を上げる組織では有料サブスクリプションが必須 v3 のサポート終了 : 2024 年末までの延長サポートのみで、機能改善・バグ修正は終了 ランタイム対応の遅れ : 最新の AWS Lambda ランタイム更新への追随が停止 課題の整理 これらの変更により、私たちは以下の課題を感じました。 継続的なライセンス費用の発生 将来的なランタイムサポート切れのリスク v3→v4 移行に伴う運用フロー変更の負荷 移行先の採用 検証編記事 で複数の移行先候補(Pulumi、AWS CDK、AWS SAM、lambroll + Terraform、Terraform + AWS CLI)を比較検討した結果、学習コストや社内での運用実績を考慮し、「lambroll + Terraform」の組み合わせを採用することにしました。 この選択により、以下のメリットを期待しました。 ライセンス費用の回避 IaC ツールの統一による運用効率向上 AWS のネイティブ機能への完全対応 より細かい粒度でのリソース管理 移行アプローチ 今回移行したのは、CodeBuild の実行結果を Slack に通知する Lambda 関数とその周辺リソース(EventBridge、IAM ロール)です。 注意 :この通知機能は一時的に停止しても業務に重大な影響がないため、移行中のサービス停止を許容する前提で手順を設計しています。本番環境の重要なサービスを移行する場合は、無停止での移行手順を検討することをお勧めします。 移行戦略 安全性を重視し、以下の段階的アプローチを採用しました。 移行前の準備と調査 lambroll による Lambda 関数の先行移行 Terraform による周辺リソースの段階的移行 統合テストと動作確認 Serverless Framework スタックの削除 実践的な移行手順 Step 1: 移行前の準備と調査 移行を安全に進めるため、まずは現状把握とバックアップ取得から始めます。 # CloudFormation スタックの詳細取得 aws cloudformation describe-stacks \ --stack-name your-service-stack # Lambda 関数のバックアップ aws lambda get-function \ --function-name your-function-name \ > function-backup.json # CloudFormation のバックアップ aws cloudformation get-template \ --stack-name your-service-stack \ > backup-cfn.json Step 2: lambroll による Lambda 関数の移行 lambroll の初期化 # 既存の Lambda 関数から lambroll 設定を生成 lambroll init --download \ --function-name your-function-name このコマンドにより、以下のファイルが自動生成されます。 function.json : Lambda 関数の設定 .lambdaignore : デプロイ時に除外するファイルの定義 function.zip : 現在の関数コード function.json の調整 生成された function.json を環境変数やタイムアウト設定に合わせて調整。 { " FunctionName ": " your-function-name ", " Runtime ": " nodejs22.x ", " Timeout ": 30 , " MemorySize ": 128 , " Role ": " arn:aws:iam::{{ env `AWS_ACCOUNT_ID` }}:role/<IAM ロール名> ", " Environment ": { " Variables ": { " SLACK_WEBHOOK_URL ": " {{ env `SLACK_WEBHOOK_URL` }} " } } } 環境変数ファイルの作成 # development.env SLACK_WEBHOOK_URL =https://hooks.slack.com/services/YOUR/WEBHOOK/URL AWS_ACCOUNT_ID = 123456789012 デプロイとテスト # 事前確認 lambroll deploy --dry-run --envfile =< デプロイ先の環境 > .env # 実際のデプロイ lambroll deploy --envfile =< デプロイ先の環境 > .env # 動作確認 lambroll invoke \ --payload =' {"detail":{"build-status":"FAILED"}} ' \ --envfile =< デプロイ先の環境 > .env Step 3: Terraform による周辺リソースの移行 既存リソースの詳細調査 Terraform import を進めるために、既存リソースの正確な識別子を収集。 # EventBridge ルールの詳細 aws events list-rules --name-prefix " your-service " # Lambda パーミッションの確認 aws lambda get-policy --function-name your-function-name # EventBridge ターゲットの確認 aws events list-targets-by-rule --rule your-rule-name # IAM ロールの詳細 aws iam get-role --role-name your-lambda-role Terraform 設定の作成 # Lambda ログ権限ポリシー data "aws_iam_policy_document" "lambda_logging" { statement { effect = "Allow" actions = [ "logs:CreateLogStream" , "logs:CreateLogGroup" , "logs:TagResource" ] resources = [ "<LambdaのロググループARN>" ] } statement { effect = "Allow" actions = [ "logs:PutLogEvents" ] resources = [ "<LambdaログストリームARN>" ] } } # Lambda ログポリシー作成 module "lambda_logging" { source = "terraform-aws-modules/iam/aws//modules/iam-policy" version = "5.59.0" name = "lambda_logging_policy" policy = data.aws_iam_policy_document.lambda_logging.json } # Lambda 実行ロール作成 module "lambda_logging_role" { source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role" version = "5.59.0" create_role = true role_name = "lambda_logging_role" role_requires_mfa = false trusted_role_services = [ "lambda.amazonaws.com" ] custom_role_policy_arns = [ module.lambda_logging.arn ] } # EventBridge ルール resource "aws_cloudwatch_event_rule" "codebuild_notification" { name = "codebuild-notification-rule" description = "CodeBuild status change notification" event_pattern = jsonencode ( { source = [ "aws.codebuild" ] detail-type = [ "CodeBuild Build State Change" ] detail = { build-status = [ "FAILED" , "SUCCEEDED" ] } } ) } # EventBridge ターゲット resource "aws_cloudwatch_event_target" "lambda" { rule = aws_cloudwatch_event_rule.codebuild_notification.name target_id = "notificationLambdaTarget" arn = data.aws_lambda_function.notification.arn } # Lambda パーミッション resource "aws_lambda_permission" "allow_eventbridge" { statement_id = "AllowExecutionFromEventBridge" action = "lambda:InvokeFunction" function_name = data.aws_lambda_function.notification.function_name principal = "events.amazonaws.com" source_arn = aws_cloudwatch_event_rule.codebuild_notification.arn } 既存リソースの import # EventBridge ルール terraform import aws_cloudwatch_event_rule.codebuild_notification \ < EventBridgeルール名 > # EventBridge ターゲット terraform import aws_cloudwatch_event_target.lambda \ < EventBridgeルールの実際の名前 > / < ターゲットID > # Lambda パーミッション terraform import aws_lambda_permission.allow_eventbridge \ < Lambda関数名 > / < LambdaパーミッションのStatementId > # IAM ロール terraform import module.lambda_logging_role.aws_iam_role.this \ < IAM ロール名 > ポイント : CloudFormation で生成されたリソース名は予測しにくいため、AWS CLI で正確な名前を確認してから import する必要があります。また、import 完了後は必ず terraform plan でドリフトがないことを確認しましょう。 段階的な適用 # IAM ロールのみ先行適用 terraform apply --target = aws_iam_role.lambda_role # Lambda 関数の再デプロイ(新しい IAM ロールを使用) lambroll deploy --envfile =< デプロイ先の環境 > .env # 残りのリソースを適用 terraform apply IAM ロールを先行適用する理由は、lambroll が Lambda 関数をデプロイする際に実行ロールが必要だからです。ロールが存在しない状態で lambroll deploy を実行するとエラーになってしまうため、まず Terraform で IAM ロールを作成してから Lambda 関数のデプロイを行います。 Step 4: Serverless Framework からの分離 serverless.yml からの Lambda 関数削除 # functions セクションをコメントアウト # functions: # notification: # handler: handler.notifier # events: # - cloudwatchEvent: # event: # source: # - aws.codebuild 影響範囲の確認と適用 # 変更の影響を確認 sls package # Lambda 関数と関連リソースを Serverless 管理から削除 sls deploy # Serverless Framework による削除でリソースが消えるため、 # Terraform と lambroll で再作成 terraform apply lambroll deploy --envfile =< デプロイ先の環境 > .env Step 5: 統合テストと最終確認 エンドツーエンドテスト # EventBridge 経由でのテスト aws events put-events --entries file://test-event.json # ログの確認 aws logs get-log-events \ --log-group-name /aws/lambda/your-function-name Serverless スタックの削除 # 残存リソースの確認 aws cloudformation describe-stack-resources \ --stack-name your-service-stack # スタックの削除 sls remove lambroll vs Serverless Framework: 運用面での違い 移行を通じて実感した大きな違いは、デプロイの仕組みと運用特性でした。 項目 Serverless Framework lambroll デプロイ方式 CloudFormation スタック更新 Lambda API 直接呼び出し 対象範囲 Lambda + 全関連リソース Lambda 関数のみ デプロイ速度 CloudFormation の処理時間に依存 Lambda API の応答時間 lambroll は Lambda API を直接呼び出すため、コードの変更を素早く反映できます。開発時の反復サイクルが改善されました。 得られた知見とベストプラクティス 1. 段階的移行について 一度にすべてを移行するのではなく、Lambda 関数を先行移行することで、リスクを抑えることができました。これにより、問題が発生した場合の影響範囲を限定しやすくなります。 2. CloudFormation で生成されたリソース名の把握 Serverless Framework が自動生成するリソース名は予測しにくいです。移行前に AWS CLI を使って正確な名前を調査することで、import 作業がスムーズに進みやすくなります。 3. 環境変数管理の統一 lambroll と Terraform で環境変数の管理方法を統一することで、設定ミスを防ぎやすくなります。.env ファイルを活用した統一的な管理をお勧めします。 4. バックアップとロールバック戦略 移行作業では、各段階でのバックアップと、問題発生時のロールバック手順を事前に定義しておくことが大切です。 5. サービス停止の許容範囲の事前確認 今回の移行では通知機能という性質上、一時的なサービス停止を許容できました。しかし、本番環境の重要なサービスでは無停止移行が必要な場合があります。移行対象の重要度とサービス停止の影響範囲を事前に評価し、適切な移行戦略を選択することが重要です。 まとめ 今回の移行プロジェクトを通じて、ツール選択における長期的な視点の大切さを改めて認識しました。 また、移行作業の中で段階的なアプローチが、リスクを抑えながら着実に成果を得る鍵であることも実感しました。 検証編記事 での事前調査から実際の移行作業まで、一連の取り組みを通じて、事前調査で想定していたメリット(デプロイ速度の改善、IaC ツールの統一、ライセンス費用の回避)を実現できています。 今後は、この移行で得られた知見を活かし、他のプロジェクトでも同様のアプローチを適用していく予定です。 IaC ツールの選択は一度決めれば終わりではなく、技術環境の変化に応じて継続的に見直しを行うことが重要です。今後も最適なツール選択と運用効率の向上に取り組んでいきます。 同様の移行を検討されている方の参考になれば幸いです。まずは小さなプロジェクトから段階的に試してみることをお勧めします。
はじめに こんにちは。デリッシュキッチンでデータサイエンティストをしている古濵です。 今回はニッチな内容ですが、タイトル通りの問題が発生したため、その対処法について備忘録的にまとめます。 動作環境は以下になります。 Databricks Runtime: 15.4LTS for ML Python: 3.11.11 ライブラリはDatabricks Runtimeのバージョンから以下にアップグレードしています openai==1.65.2 mlflow==2.20.3 pydantic==2.10.6 databricks-agents==0.16.0 databricks-sdk==0.50.0 MLflow Tracingに関するドキュメントは以下になります。 mlflow.org 問題 準備 まず、具体的にどんな問題が発生するかを説明するために、以下のようなコードを用意します。 やりたいこととしては、ユーザーのクエリからフィルタリング条件を抽出するタスクをLLMにさせます。 これは 以前書いたテックブログでのフィルタリング処理 をベースとしています。 今回は、調理時間と調理費用のみをフィルタリング条件として抽出するタスクとします。 import mlflow from enum import Enum from pydantic import BaseModel import os os.environ[ "OPENAI_API_KEY" ] = dbutils.secrets.get(...) # your scope and key # mlflow.traceを自前で対応するためopenaiのautologを無効にする mlflow.openai.autolog(disable= True ) # 調理時間のフィルタリング条件 class CookingTimeColumn ( str , Enum): cooking_time = "cooking_time_min" class CookingTimeOperator ( str , Enum): greater_than = ">" less_than = "<" greater_than_or_equal_to = ">=" less_than_or_equal_to = "<=" class CookingTimeFilter (BaseModel): column: CookingTimeColumn operator: CookingTimeOperator value: float class CookingTimeFilters (BaseModel): cooking_time_filters: list [CookingTimeFilter] # 調理費用のフィルタリング条件 class CookingCostColumn ( str , Enum): cooking_cost = "cooking_cost_yen" class CookingCostOperator ( str , Enum): greater_than = ">" less_than = "<" greater_than_or_equal_to = ">=" less_than_or_equal_to = "<=" class CookingCostFilter (BaseModel): column: CookingCostColumn operator: CookingCostOperator value: float class CookingCostFilters (BaseModel): cooking_cost_filters: list [CookingCostFilter] ユーザーの入力クエリは以下を例として使用します。 user_query = "10分以内に500円未満で作れる副菜教えて" MLflow Tracingを使用した関数を定義 MLflow Tracingは、関数に対して @mlflow.trace デコレータを付与することで、関数内の処理を簡単にTracingすることができます。 そのような関数を以下に3つ定義しました。 create_metadata_filter_from_user_query()から、create_cooking_time_filter()とcreate_cooking_cost_filter()を呼び出します。 from openai import OpenAI from mlflow.entities import SpanType @ mlflow.trace (span_type=SpanType.LLM) def create_cooking_time_filter (user_query: str ) -> list [CookingTimeFilters | CookingCostFilters]: client = OpenAI() system_prompt = f """ あなたは料理の知識が豊富なレシピ検索AIです。 ユーザーがレシピ検索のために入力したクエリを解読し、ユーザが**調理時間**でフィルタリングして検索したい場合は、フィルタリング条件を返してください。 ## 出力形式 * json形式で出力してください * columnにカラム名、operatorに不等号、valueにフィルタリング対象を入れてください """ completion = client.beta.chat.completions.parse( model = "gpt-4o-mini" , messages = [ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format = CookingTimeFilters, ) structured_outputs = completion.choices[ 0 ].message.content filters = CookingTimeFilters.model_validate_json(structured_outputs).cooking_time_filters return filters @ mlflow.trace (span_type=SpanType.LLM) def create_cooking_cost_filter (user_query: str ) -> list [CookingTimeFilters | CookingCostFilters]: client = OpenAI() system_prompt = f """ あなたは料理の知識が豊富なレシピ検索AIです。 ユーザーがレシピ検索のために入力したクエリを解読し、ユーザが**調理費用**でフィルタリングして検索したい場合は、フィルタリング条件を返してください。 ## 出力形式 * json形式で出力してください * columnにカラム名、operatorに不等号、valueにフィルタリング対象を入れてください """ completion = client.beta.chat.completions.parse( model = "gpt-4o-mini" , messages = [ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format = CookingCostFilters, ) structured_outputs = completion.choices[ 0 ].message.content filters = CookingCostFilters.model_validate_json(structured_outputs).cooking_cost_filters return filters @ mlflow.trace (span_type=SpanType.CHAIN) def create_metadata_filter_from_user_query (user_query: str ) -> list [CookingTimeFilters | CookingCostFilters]: filter_functions = [ create_cooking_time_filter, create_cooking_cost_filter, ] metadata_filters = [] for func in filter_functions: filters = func(user_query) metadata_filters.extend(filters) return metadata_filters 実行結果 - create_metadata_filter_from_user_query - create_cooking_time_filter - create_cooking_cost_filter のような構造になっており、cooking_timeとcooking_costのフィルタリング条件をそれぞれ1秒ほど(合計約2秒)で抽出できていることがわかります。 ただ、これでは直列に処理しているため、フィルタリング条件が増えれば増えるほど処理時間が長くなってしまいます。 LLMの処理時間が長いのはAPIの待機時間が原因のため、ここを並行処理にすることで処理時間を短縮することができます。 並行処理内でMLflow Tracingを使用 並行処理を行うために、 concurrent.futures.ThreadPoolExecutor を使用しました。 create_metadata_filter_from_user_query()内の処理を以下のように変更します。 from concurrent.futures import ThreadPoolExecutor @ mlflow.trace (span_type=SpanType.CHAIN) def create_metadata_filter_from_user_query (user_query: str ) -> list [CookingTimeFilters | CookingCostFilters]: filter_functions = [ create_cooking_time_filter, create_cooking_cost_filter, ] metadata_filters = [] # 各関数を並行処理で実行するよう修正 with ThreadPoolExecutor() as executor: futures = [executor.submit(func, user_query) for func in filter_functions] for future in futures: filters = future.result() metadata_filters.extend(filters) return metadata_filters 実行結果 図中の赤枠が示す通り、1、2、3とタブができており、それぞれのTracingが関数単位になっています。 前置きが長くなりましたが、この問題に対処します。 対処 この問題は、OpenTelemetryを使って、親のTracingのContextを子に渡すことで解決できました。 OpenTelemetryはオブザーバビリティ用途で使用されるOSSです。 MLflow Tracingは内部的にはOpenTelemetryを使用しており、OpenTelemetryのContextを使うことで、親のTracingを子に渡すことができます。 以下のように、呼び出し元のcreate_metadata_filter_from_user_query()内のContextを親のTracingとして、子の関数に渡します。 次に、子の関数の処理ではContextのattachをすることで親と子を関連付けることができました(クリーンアップするためにdetachもしています)。 from opentelemetry import context from opentelemetry.context import Context def create_filter_with_trace_parent_context (user_query: str , func: callable , trace_parent_context: Context) -> list [CookingTimeFilters | CookingCostFilters]: context_token = context.attach(trace_parent_context) filters = func(user_query) context.detach(context_token) return filters @ mlflow.trace (span_type=SpanType.CHAIN) def create_metadata_filter_from_user_query (user_query: str ) -> list [CookingTimeFilters | CookingCostFilters]: filter_functions = [ create_cooking_time_filter, create_cooking_cost_filter, ] metadata_filters = [] parent_context = context.get_current() # 各関数を並行処理で実行するよう修正 with ThreadPoolExecutor() as executor: # create_filter_with_trace_parent_context()で各関数を呼び出すよう修正 futures = [executor.submit(create_filter_with_trace_parent_context, user_query, func, parent_context) for func in filter_functions] for future in futures: filters = future.result() metadata_filters.extend(filters) return metadata_filters 実行結果 MLflow Tracingが分かれずに1つのタブの中にまとまっていることがわかります。 また、全体の処理時間として約1.3秒で終えており、直列にLLMを呼び出すときに比べて高速化できていることもわかるかと思います。 おわりに MLflow Tracingを使用した際に並行処理でTracingが分かれてしまう問題と、その対処法について紹介しました。 OpenTelemetryのContextを使用することで、親子関係を保ったままのTracingが可能になり、並行処理による高速化とトレーサビリティの両方を実現できました。 同様の問題に遭遇した方の参考になれば幸いです。 なお、LangGraph(とLangChain)を使えば並行処理をしたとしても、MLflow Tracingが分かれる問題は発生しませんでした。 今回の対処法はあくまで応急処置的な側面もあることは補足しておきます。 また、並行処理内でさらに並行処理をするなど処理が複雑化した場合、ワークフローのどの関数が並行に処理されているのかわかりにくくなるかと思います。 そういう意味でも、要件に合わせて処理が複雑化していくにつれ、LangGraphなどのフレームワークを使用する方が可読性や保守性の観点で有効だと考えられます。
はじめに こんにちは、リテールハブ開発部でバックエンドエンジニアをしているホシと言います。 現在、小売アプリの開発でLaravel11を利用してサービス開発を行っています。 今回はサービス提供をする上でセキュリティ対策としてAWSのWAFを導入することになったお話をしようと思います。 AWS WAF(Web Application Firewall)は、Webアプリケーションを守るための強力なサービスです。 今回、私は初めてTerraformを使ってWAFを設定し、ALBやCloudFrontと連携させました。 まずはChatGPT、Cursorなどを活用して、設定やTerraformコードの作成自体は非常にスムーズに進めることができましたが、実際に運用を考えると「 事前に知っておくべき重要な仕様や制限 」をいくつか経験することになりました。 この記事では、初めてWAFを導入する方に向けて、 Terraformと組み合わせたWAF設定で注意すべきポイント を中心にご紹介します。 特に、 HTMLを投稿するFroala Editorのようなツールを使っている場合に影響を受けやすい内容 も一緒にお話できたらと思います。 WAFをTerraformで構築して見えてきた「事前に知っておくべき3つのポイント」 TerraformとWAFを組み合わせて構築してみると、「知らないとハマる」「調査が必要になる」ポイントがありました。ここではその中でも特に印象的だった3つを紹介します。 ポイント1:WAFのロググループ名には「命名ルール」がある WAFのログをCloudWatch Logsに出力する際、ロググループ名には 特定の命名ルール があります。 正しい命名: aws-waf-logs- で始まる必要あり ルールに従わないと: Terraform実行時にエラーになる(が、 原因が分かりづらい ) 例:Terraformでのログ設定コード resource "aws_cloudwatch_log_group" "waf_logs" { name = "aws-waf-logs-my-web-acl" # ← このprefixが必須! } resource "aws_wafv2_web_acl_logging_configuration" "example" { log_destination_configs = [aws_cloudwatch_log_group.waf_logs.arn] resource_arn = aws_wafv2_web_acl.main.arn } 補足: Terraformでのエラー内容は「ARNが無効」「リソースが見つからない」などと表示される場合が多く、 命名ルールの問題にたどり着くのに時間がかかりました 。 ドキュメント等を読めば難しいことではないのですが、私は名前は自由に決められると思い込んでいた部分もあり、余計にはまってしまったポイントになります。 Error: creating WAFv2 Logging Configuration: WAFInvalidParameterException: The resource ARN 'arn:aws:logs:ap-northeast-1:123456789012:log-group:invalid-name' is invalid or does not exist. ポイント2:CloudFrontとWAFの紐付け解除には少し特別な手順が必要 ALBと違い、 CloudFrontにWAFを紐付けたあとにTerraformで解除するには注意が必要 です。 通常の terraform apply では解除できず、 明示的な操作が必要 になります。 解決方法は2つ 手動でWebコンソールからWAFの紐付けを解除する Terraformで web_acl_id = null を指定して「apply」を行い明示的に解除する 上記どちらかを行った上であれば、ALB同様Terraformのコードを削除するだけで簡単にWAF設定の削除ができます。 Terraform例: resource "aws_cloudfront_distribution" "example" { # 他の設定... web_acl_id = null # ← 明示的に解除しないとエラーになる } 注意: WAFを解除せずに削除しようとすると、「リソースがまだ関連付けられている」としてエラーになります。 なぜALBではうまくいくのにCloudFrontだとエラーなのかが仕様として認識できておらず、はまってしまったポイントになります。 CloudFrontはリージョンが「us-east-1」に固定されている点も含めて、 ALBと挙動が異なる ため、WAF設定時は追加考慮がいる認識が必要です。 ポイント3:Froala Editor × AWSマネージドルールで想定外のブロックが発生 今回はAWSのマネージドルール AWSManagedRulesCommonRuleSet を導入しました。 これは定番の汎用ルールセットで、以下のように簡単にTerraformから利用できます。 導入コード(Terraform) rule { name = "AWS-AWSManagedRulesCommonRuleSet" priority = 0 override_action { none {} # 動作確認時はblockで良いが、影響調査のためcountにするのがオススメ } statement { managed_rule_group_statement { name = "AWSManagedRulesCommonRuleSet" vendor_name = "AWS" } } visibility_config { cloudwatch_metrics_enabled = true sampled_requests_enabled = true metric_name = "CommonRules" } } まずは汎用的なものを適用して、適宜最適なものを設定していこうと考えていたのですが、 早速以下の2点で大きく影響が出ることがわかりました。 これも事前に知っているかどうかで調査、対策の時間が節約できるのではないかなと思っています。 問題①:XSS検知により投稿がブロックされる 対象ルール: CrossSiteScripting_BODY Froala EditorはHTMLを生成するため、 <script> タグに似た構文や属性が含まれる場合がある 結果: 正当な入力でもWAFがXSSと誤検知してブロック する場合がある 対応策: statement { managed_rule_group_statement { name = "AWSManagedRulesCommonRuleSet" vendor_name = "AWS" # 「CrossSiteScripting_BODY」ルールのみcount運用にする設定を追加 rule_action_override { name = "CrossSiteScripting_BODY" action_to_use { count {} } } } } 厄介なところは、すべてのHTML本文でなるわけではなく、内容によってエラーになるということです。 そのため、検証時は特に発覚することなく検知できず、本番のデータを利用したことで発覚しました。 最初はなぜ一部だけエラーになるかの原因もわからず特定まで時間がかかりました。 WAFのログの中にBLOCKしているログがあるので、それを参照することで特定のケースの場合のみBLOCKされていることがわかりました。 やはりこういうことからも、本番運用では最初はcount運用して様子を見るということが良いと思い知らされた部分でもあります。 今回はcount運用にすることでエラー回避はしていますが、実はただBLOCKしないようにしているだけであり、 セキュリティ面で考えるとベストではない状態です。 ここは場合によってはNGで、対策をさらに検討する必要があるかもしれません。 問題②:リクエストボディ8KB制限による検知漏れ・意図しないブロック 対象ルール: SizeRestrictions_BODY AWS WAFはリクエストボディの検査に 最大8KB(8192バイト) という上限がある Froalaから投稿された長文・装飾つきHTMLで制限を超えることがある こちらも事前にわかっていれば対策のやりようはあったと思いますが、 マネージドルール内容のすべてを把握できていない、かつFroala Editorの仕様もしっかり把握できていない部分が重なり、 原因の特定、対応策により時間がかかってしまった部分になります。 対応策の選択肢: count モードに変更し、ブロックせずログだけ記録する →とはいえ、できれば事前にサイズ制限によるブロックは行いたいところです。 Froala側で入力サイズのバイト制限をかける →これはFroalaの仕様上、8KB 以下に抑える本文を作成するのはかなり厳しそうです。 投稿を複数回に分割して送信するような仕組みに変更 →文章では簡単に書けますが、考慮点が非常に多く対応コストは高いです。 (分割送信用のAPI準備、同じデータの同時更新時の考慮、分割更新の制御、エラー制御) まとめ:WAFを導入する際はユースケースと制限を事前に理解しよう 今回の内容を簡単に表にしました。 観点 内容 ロググループ名の制約 aws-waf-logs- で始めないとTerraformエラーに。事前に命名確認を! CloudFront解除時の注意 web_acl_id = null を明示しないと Terraform Apply 時にエラー XSS検出の過検知 HTMLエディタ使用時は count モードで様子を見るのが安全 8KB制限のインパクト 長文・装飾投稿があるならアプリ・インフラ両面での対応が必要 おわりに いかがでしたでしょうか。 AIを利用することで効率よくWAFを構築できた反面、 仕様への理解が不十分だと意図せぬトラブル にもつながります。 特にFroala EditorのようにHTMLを扱うツールを使っている場合は、 WAFの標準ルールと衝突しやすく、検知・ブロックの調整が重要 になります。 もし予期せぬ403エラーなどが発生している場合は本記事の内容の部分を疑ってみるのも良いかもしれません。 本記事が、これからWAFを導入される方にとっての参考になれば幸いです。 最後までお読みいただきありがとうございました。
はじめに こんにちは、デリッシュキッチン開発部でソフトウェアエンジニアをしている新谷です。 エブリーの開発部では、日常業務から離れて新しい技術やアイデアに挑戦する「挑戦week」という取り組みを定期的に開催しています。 今回は限定的に2日間という短期間での開催でしたが、この挑戦weekを活用し、ヘルシカの画像解析機能の精度をさらに高めることを目指して、技術検証として性質の異なる2つのAIアプローチを構築・比較しましたので、その内容についてご紹介します。 ※ 挑戦weekの詳細については過去の記事で紹介していますので、興味のある方は以下をご覧ください。 tech.every.tv 比較する2つのアプローチ 画像からの栄養素推定というタスクに対して、私たちは2つの異なるアプローチを設計しました。 LLM単体アプローチ 一つ目は、LLMが持つ広範な知識を用いて、画像から料理名や材料を特定し、栄養素を直接推定するアプローチです。この手法の強みは、一つのモデルで完結するため、シンプルで高速な応答が期待できる点です。 RAG活用アプローチ 二つ目は、LLMの推定能力に加えて、検索拡張生成(RAG)の技術を活用するアプローチです。LLMが推定した材料名を基に、デリッシュキッチン(DK)の食材データベースを検索し、得られた正確な栄養素データを基に最終的な計算を行います。よりデータに基づいた、精度の高い推定を目指すアプローチです。 具体的には、RAG活用アプローチでは以下のアーキテクチャを設計しました。 ナレッジベースの構築にあたっては、当初、より高度なアプローチを試みました。具体的には、Amazon OpenSearch Serverlessに専用のインデックスを作成し、AWS Lambdaを用いて食材データを自動的に流し込む仕組みです。この方法では、食材名のみをベクトル化し、各栄養素(カロリー、たんぱく質など)をメタデータとして付与することで、検索精度向上を目指しました。 しかし、検証を進める中で、メタデータが意図した通りに結果へ反映されないという課題に直面しました。 設定の見直しなどを試みましたが、短期間での解決が難しかったため、今回はアプローチを変更し、食材と栄養素をすべてCSVにしてS3にアップロードしBedrockの機能で直接ナレッジベースを作成するという、よりシンプルな方法を採用しました。 検証結果 これら2つのアプローチの性能を比較するため、特定の料理写真を用いて検証を行いました。デリッシュキッチンのレシピが持つ栄養素データを「参考値」として、各アプローチの推定結果がどれだけ近いかを比較します。 検証1:にらと豚肉の味噌炒め 項目 DKレシピ RAG LLM単体 カロリー(kcal) 615 501 250 たんぱく質(g) 18.2 14.4 12 脂質(g) 48.7 47.2 15 炭水化物(g) 18.6 0.2 5 糖質(g) 15.5 0.8 4.3 塩分(g) 2.4 1.2 1.2 検証2:きゅうりとちくわのめんつゆナムル 項目 DKレシピ RAG LLM単体 カロリー(kcal) 98 105 70 たんぱく質(g) 4.8 0 4 脂質(g) 5.5 11.4 2 炭水化物(g) 7.6 0.3 8 糖質(g) 6.9 0.3 6.5 塩分(g) 1.5 0.4 0.8 検証3:鶏むね肉のみぞれ煮献立 項目 DKレシピ RAG LLM単体 カロリー(kcal) 741 1312 670 たんぱく質(g) 43.5 44.1 35 脂質(g) 15.4 32.2 15 炭水化物(g) 104.6 134.4 84 糖質(g) 97 127.2 75 塩分(g) 4.5 2.1 3.6 検証4:大根のとろとろ煮献立 項目 DKレシピ RAG LLM単体 カロリー(kcal) 745 726 678 たんぱく質(g) 29.6 4.3 35.2 脂質(g) 25.8 16.2 26.8 炭水化物(g) 89.5 59.3 78 糖質(g) 83.4 17.3 70.5 塩分(g) 5 0.8 3.3 結果の考察 結果を見ると、 RAGは、LLM単体と比較して参考値であるDKレシピの値に近くなるケースもあれば、逆に大きく外れてしまうケースもある という、一長一短な結果となりました。 今回のRAGの試みは、LLM単体では推定が難しい「食材ベースでの栄養素計算」を、デリッシュキッチンの具体的な食材データを参照させることで、どれだけ推定値を参考値に近づけられるかを検証するものでした。 今後の課題と展望 今回の比較検証から、いくつかの課題が見えてきました。 材料の特定と量の推定のばらつき 画像に写っていない材料や、写っていても正確な量を推定するのは、現状のLLMの精度では依然として困難です。 材料検索のノイズ ベクトル検索時に、無関係な食材がヒットしたり、逆に必要な食材がヒットしなかったりする問題がありました。 栄養素の足し合わせが不安定 LLMによる最終的な集計処理が、必ずしも期待通りに行われないケースがありました。 実行時間が長い 複数のステップを踏むため、LLM単体より処理に時間がかかります。 特に課題2, 3, 4については、改善の余地があると考えています。例えば、ベクトルDBのチューニングや全文検索への切り替え、栄養素の集計をルールベースで行う、といった対策が考えられます。 これらの課題を踏まえ、今後は以下のような別のアプローチも検討しています。 推定した料理名から類似のDKレシピを検索し、その栄養素の平均値を採用する。 DKのレシピ画像と料理名で新たなベクトルDBを構築し、画像で類似検索を行う。 栄養素推定に特化したモデルをファインチューニングする。 まとめ 今回は、画像からの栄養素推定の精度向上を目指し、性質の異なる2つのAIアプローチ(LLM単体/RAG)を構築・比較検証しました。 結果として、RAGは特定の条件下で精度が向上する可能性を示しましたが、安定性や応答速度の面ではLLM単体に分があるなど、それぞれに利点と課題があることが明らかになりました。 この挑戦で得られた知見を活かし、さらなる精度向上に向けて、今後も検証を続けていきたいと思います。
はじめに こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。 近年 AI 技術の発展が著しく、中でも生成 AI がかなりの勢いで発展し、普段使いや仕事で ChatGPT などの生成 AI のサービスを取り入れる方や企業が多くなってきました。 今回は多くの場合で生成 AI の機能を実現している機械学習 (ML : Machine Learning) について、Android アプリで簡単に実装する方法に触れてみたいと思います。 なお、弊社は開発生産性の向上などを目的として Cursor を導入するなど、積極的な AI の活用を取り入れていますので、ご興味がある方はこちらの記事も見ていただけると嬉しいです。 エブリー、AIエディタ「Cursor」を全エンジニアおよびプロダクトマネージャーに導入 アプリに組み込む方法 「機械学習」という言葉を使うと難しい印象がありますが、既に機械学習の機能を提供するフレームワークは多く存在しており、中でも Google が提供する MediaPipe というフレームワークを使うことで簡単に生成 AI といった機能を実装することができます。 また MediaPipe では様々なソリューションが提供されており、2025 年 7 月時点では全てのソリューションが Android の端末で利用可能となります。 以下公式サイトにて、利用可能なソリューションがまとまっています。 https://ai.google.dev/edge/mediapipe/solutions/guide?hl=ja#available_solutions 今回は、その中でも動きがイメージしやすい 画像分類 (Tasks API) と 生成 AI (LLM Inference API) について触れてみたいと思います。 画像分類 (Tasks API) の実装方法 まずは画像分類 (Tasks API) の実装方法についてまとめていきます。 1. モデルデータを app/src/main/assets に追加 機械学習のデータを動かすためには当然モデルデータが必要となります。 今回は Google AI for Developers が提供している画像分類モデルを使用したいと思います。 https://ai.google.dev/edge/mediapipe/solutions/vision/image_classifier/index?hl=ja#efficientnet-lite0_model_recommended モデルには int8 と float32 が存在しますが int8 … ファイルサイズが軽量で、処理速度が速いが精度はわずかに低い float32 … ファイルサイズが大きく、処理速度が遅いが精度は高い という特徴があり、スマホなどストレージやメモリが限られている場合は int8 を使用するとよいかと思います。 本記事でも int8 を導入する前提での実装方法をまとめていきます。 2. app レベルの build.gradle に Vision Task ライブラリを追加 dependencies { implementation( "com.google.mediapipe:tasks-vision:0.10.26" ) } 3. ImageClassifier の初期化 ImageClassifier は画像分類タスクを実行するためのクラスとなります。 モデルデータを読み込ませて初期化するため、モデルデータ自体のサイズにもよりますがやや初期化コストが高くなるので、一度だけ実行して再利用する形が良いです。 lateinit var imageClassifier: ImageClassifier fun initImageClassifier(context: Context) { val options = ImageClassifier.ImageClassifierOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath( "efficientnet_lite0.tflite" ).build()) // ① … 読み込ませるモデルデータの設定 .setRunningMode(RunningMode.IMAGE) // ② … 分類する画像データの種別 .setMaxResults( 3 ) // ③ … 返却するレスポンスの数 .setScoreThreshold( 0.5F ) // ④ … 出力結果の確信度のしきい値 .build() try { imageClassifier = ImageClassifier.createFromOptions(context, options) } catch (e: IllegalStateException ) { Log.e( "ImageClassifier" , "TFLite failed to load model with error: " + e.message) } } パラメータが多いため、細かく確認していきたいと思います。 ①については、モデルデータの読み込みとなるため assets ファイルに配置したファイルを指定しています。 ②については、読み込ませる画像データの種別を設定するもので、静止画 ( IMAGE ) / 動画 ( VIDEO ) / ストリーム ( LIVE_STREAM ) を設定できます。 今回は静止画を読み込ませるため、静止画 ( IMAGE ) を設定しています。 ③については、返却するレスポンスの数となり、3 を設定した場合は確信度 (スコア) を降順で 3 つ返却する形となります。 ④については、③でも触れた確信度の事で、指定した数値以上の確信度の項目のみレスポンスに含めます。 0.5F を指定した場合、確信度が 50% 以上のもののみレスポンスに含める、という形となります。 4. データの分類実行 3 までで必要な設定は完了したため、最後にデータの分類を実行します。 fun classifyImage(bitmap: Bitmap) { // 初期化完了済みかチェック if ( !:: imageClassifier.isInitialized) { return } // データを分類してログ出力 val mpImage = BitmapImageBuilder(bitmap).build() val result = imageClassifier.classify(mpImage) result.classificationResult().classifications().forEach { classification -> classification.categories().forEach { category -> Log.d( "ImageClassifier" , "Category: ${ category.categoryName() } , Score: ${ category.score() } " ) } } } Bitmap のデータを ImageClassifier$classify メソッドで読み込ませ、結果をログで出力する形となります。 実行結果 今回はモデルファイルと同様に、以下の Google AI for Developers の画像分類タスクガイドのページに設置されているフラミンゴの画像を分類してみたいと思います。 https://ai.google.dev/edge/mediapipe/solutions/vision/image_classifier 出力ログは以下のようになりました。 89.4% という高い確信度で flamingo と分類されました。 (公式の 95% という数値よりやや下がっているのは、おそらく文字と動物が混在してしまっている影響だと考えます) 他にも色々画像を読み込ませましたが、 対象の動物以外 (木や草など) は写り込まない方が精度が高い 正面をはっきり向いていて、顔が識別できる方が精度が高い 犬は確信度がかなり低い (毎回 30% 程度) ライオンは確信度がかなり高い (木や草が写り込んでも 90% 以上となる) となり、提供されているモデルデータでは分類できる・できない動物がはっきりしている、という興味深い結果となることがわかりました。 画像分類 (Tasks API) の実装方法については以上となります。 生成 AI (LLM Inference API) の実装方法 次に生成 AI (LLM Inference API) の実装方法についてまとめていきます。 こちらも基本的な実装の流れは画像分類 (Tasks API) と同様で、今回はチャット形式の挙動を実装してみたいと思います。 1. モデルデータを app/src/main/assets に追加 こちらも Google AI for Developers からモデルデータをダウンロードします。 ※今回は Gemma-3 1B というモデルデータを使用しますが、Hugging Face のアカウントが必要となる点にご注意ください。 https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference?hl=ja#gemma-3_1b こちらですが、画像分類と違ってモデルデータが 500MB 以上になるなどかなりサイズが大きくなります。 モデルデータが複数存在するため詳細は割愛しますが、今回は dynamic_int4 QAT というものを採用しました。 2. app レベルの build.gradle に Gen AI Task ライブラリを追加 dependencies { implementation( "com.google.mediapipe:tasks-genai:0.10.25" ) } 3. モデルデータを assets から cache にコピーする LLM Inference API については直接 assets フォルダからモデルデータを参照する方法がないため、一度 cache フォルダにデータをコピーしてから読み込ませます。 private fun copyModelFromAssetsToCache(context: Context, modelName: String ): String { val outputFile = File(context.cacheDir, modelName) if (outputFile.exists()) { return outputFile.absolutePath } context.assets. open (modelName).use { inputStream -> FileOutputStream(outputFile).use { outputStream -> inputStream.copyTo(outputStream) } } return outputFile.absolutePath } なぜ assets から直接参照できないのか、という点ですが、アプリで巨大なデータを効率的に扱うためにはメモリマッピングという技術が必要となり、その技術は実ファイルパスが必要なため assets ファイルでは使用できなくなっており、一度 cache の領域にコピーしてからそちらのファイルに対してアクセスする必要があります。 おそらく LLM Inference API でも巨大なモデルデータを扱う想定で、assets に直接アクセスするメソッドを蓋閉じしているものと推測しています。 4. LlmInference の初期化 LlmInference は生成 AI のタスクを実行するためのクラスとなります。 モデルデータがかなり大きく初期化に数秒はかかってしまうため、こちらも一度だけ実行して再利用する形が良いです。 private lateinit var llmInference: LlmInference fun initLlmInference(context: Context) { // LLM Inference の初期化 val taskOptions = LlmInference.LlmInferenceOptions.builder() .setModelPath(copyModelFromAssetsToCache(context, "gemma3-1b-it-int4.task" )) .build() llmInference = LlmInference.createFromOptions(context, taskOptions) } 5. 生成 AI 実行 4 までで必要な設定は完了したため、最後に生成 AI を実行します。 private val _updateResponse: MutableSharedFlow< String > = MutableSharedFlow() val updateResponse: Flow< String > = _updateResponse fun generateResponse(text: String ) { viewModelScope.launch(Dispatchers.IO) { val result = llmInference.generateResponse(text) _updateResponse.emit(result) } } こちらはテキストで受け取った文字列を解析し、レスポンスデータを生成する流れとなっています。 今回は UI 上で表現したかったため、Flow で Fragment 側にデータを送っています。 実行結果 実際にやり取りした結果を画像で載せたいと思います。 このモデルデータでは日本語に対しては日本語で、かつ顔文字も使って回答を生成してくれることが分かります。 生成 AI (LLM Inference API) の実装方法については以上となります。 モデルデータを組み込むメリットについて アプリにモデルデータを組み込む一番のメリットは オフラインでも結果を取得できる事 だと考えます。 今では通信してデータを取得する流れが当たり前となっていますが、電波が遮断されている、あるいは通信が安定しない環境下でも適切な答えを取得できる仕組みであれば、ユーザは通信環境を気にせずアプリを触ることができるため、ユーザフレンドリーにもなります。 市場に公開するレベルまでモデルデータを学習させることは難しいかと思いますが、何らかの方法でアプリに組み込んだモデルデータを差し替える仕組みさえ確立すれば、アプリ単体で常に最新モデルを取り扱うことができるようになります。 課題について 一番の問題はやはり モデルデータが大きい事 です。 今回は 500MB ものデータを assets に組み込むという方法で無理やり実装しましたが、アプリに内包するということは当然アプリダウンロード時のバイナリサイズも大きくなるということなので、運用方法を検討する必要があります。 方法として考えられることは assets に組み込む (本記事のやり方) アプリ起動後にモデルデータをダウンロードする あたりがありますが、別の問題として 1 のケースでも 2 のケースでも モデルデータという重要な資産を抜き取られることを防ぐ必要がある というセキュリティ面の課題が発生します。 暗号化を実施する、ネイティブコードを活用するなど回避する方法はいくつかあると思いますが、いずれにせよモデルデータをアプリで取り扱うため検討する事項は多くありそうです。 おわりに 今回は MediaPipe のフレームワークを使い、Android アプリに機械学習の機能を実装する方法をまとめてみましたが、以前は機械学習について専門知識や複雑なコードが必要でしたが、今では簡単に実装できることが分かりました。 MediaPipe には今回紹介した画像分類や生成 AI 以外にも、 物体検出 、 顔検出 、 ジェスチャー認識 など、すぐに使える強力な機能が数多く提供されていますので、ぜひ公式ドキュメントで色々と確認してみてください。 本記事の情報が皆様のお役に立てれば幸いです。
はじめに こんにちは、開発部でデータサイエンティストをしている蜜澤です。 ついに東京リージョンでAmazon Q in QuickSightがGAしました! データストーリー、シナリオ、トピックなど自然言語でデータ分析を行う機能が追加されましたが、このあたりの機能の解説は 以前の弊社のブログ や、他の方が記事にされているので、そちらをご覧いただければと思います。 今回はAmazon Q in QuickSightを計算フィールドを作成する際に使用することで、ビジュアル作成をより効率化させられないかの検証をしたいと思います。 使用する模擬データ 検証には以下のような、レシピ動画サイトの特定のレシピの日毎のインプレッション数とクリック数を集計したという想定の模擬データを使用します。 それぞれのカラムの定義は以下の通りです。 date:日付(2025-01-01~2024-04-30) recipe:レシピ名(ハンバーグ) click:レシピをクリックした回数(1~10の整数の乱数) impression:レシピが表示された回数(100~200の整数の乱数) やってみる まずは、CTRを作成できるか試してみます。 CTR = click / impresseion を期待しています。 計算フィールド追加を選択し、「計算を作成」をクリックします。 テキストフォールドが出てくるので、そこに作成したい計算式を入力します。 今回は「CTR」と入力。 計算式を書いたら、「作成」をクリックします。 2〜3秒ほどで計算式が作成されました。 期待している通りの計算式ができたので、「挿入」をクリックします。 計算式が挿入されたので、最後に「保存」をクリックして、計算フィールドの作成完了です。 自然言語で、計算フィールの計算式を作成することができました。 CTRの定義を与えなくても作成できるのは良いなと思いました! しかし、これくらい簡単な式ならQを使わずに、自分で作った方が早いと思うので、もう少し複雑な計算式を作成してみます。 日、週、月といった日付の粒度をパラメータで指定することで、日付の集計単位が変わるような計算フィールドを作成します。 下準備として、DateGranularityという文字列のパラメータを作成します。 このパラメータで日、週、月を指定します。 先ほど作成したCTR計算を、分子と分母が粒度の変更に伴って合計されるように変更します。 日本語でプロンプトを書いたらあまり良い出力を得られなかったので、英語でプロンプトを書きました。 うまくいかなかったプロンプトは記事の最後の方で紹介します。 何パターンか試しましたが、weekの時にWKにならずDDになってしまうのは、改善しなかったので、今回は諦めて手動で直します。 得られた結果を挿入して、day、week、monthを日、週、月に、粒度が週の時のDDをWKに置き換えて、保存します。 粒度を週にするとevent_dateが週の始め(日曜日)になり、月にするとyyyy-mmで月の形式になる日付計算フィールドが完成しました。 最後にもう少し複雑な計算フィールドに挑戦します。 StartDateとEndDateというパラメータを作成し、粒度に合わせて開始日と終了日を調整してくれる日付のフィルターを計算フィールドで作成します。 粒度で月を選択している状態で、StartDaeが2025-01-15、EndDateが2025-02-07と入力された場合に、2025-01-01~2025-02-28のデータが表示されるといった想定になります。 以下のようにプロンプトを入力しました。 DAY、WEEK、MONTHを日、週、月に置き換えて保存します。 粒度を日にすると開始日と終了日の間の日付フィルターが1になり、週にすると開始日と終了日を含む日の週始めから週終わりまでの日付フィルターが1になっているので、期待通りのものができていそうです。 最後にフィルターで、日付フィルターが1の場合のみ表示するようにすれば、粒度に合わせて開始日と終了日を調整してくれる日付のフィルターの完成です! このフィルターを自作した時はかなりの時間がかかったので、一瞬でできて感動しました! うまくいかなった例 うまく計算フィールドが作成できなかったプロンプトも紹介します。 簡潔に日本語でプロンプトを入力したら、エラーが出ました。 日本語でももう少し詳しくプロンプトを書いたら、出力を得ることはできました。 しかし、週がではなく、年が設定されました。 日本語のプロンプトに対する出力の精度の向上は今後に期待です! さいごに 今回はAmazon Q in QuickSightを使用して計算フィールドを作成する検証を行いました。 簡単な計算フィールドの作成はQを使用しない方が早いと思いますが、少し複雑な計算フィールドを作成する時や、やりたいことは決まっているが式がパッとは思いつかない時には、開発を効率化できる機能だと感じました。 最後まで読んでいただきありがとうございました!
開発2部の内原です。文字コードの話は大好物です。 一般的に、アプリケーションの開発において文字数カウントは非常に身近な機能です。パラメータ取得時やフォーム入力時など、様々な場面で文字数計算を実装する機会があります。 しかし、Unicode文字、特に絵文字や結合文字などが混在するテキスト処理において、「正しい文字数カウント」は意外に複雑な問題です。 この記事では、Go言語でのUnicode文字数カウントに焦点を当てて、実装時に注意すべき点を述べます。 文字数カウントの罠 まず、以下のコードについて考えます。 package main import ( "fmt" "unicode/utf8" ) func main() { s := "Hello👍🏿" // 6文字? fmt.Printf( "バイト数: %d \n " , len (s)) // バイト数: 13 fmt.Printf( "ルーン数: %d \n " , utf8.RuneCountInString(s)) // ルーン数: 7 } 一見6文字のように見える文字列ですが、実際にカウントすると異なる値が返却されます。これはどういう状況でしょうか? この差異は、例えば以下のような場面で問題が発生し得ます。 フォーム入力時 文字数制限を超過して入力できる データベース投入時 カラム文字数の制限に抵触しDBエラーが発生する UI表示 文字数カウンターの表示がユーザーの感覚と合わなくなる 文字数カウント手法 Unicode文字を正しく扱うために、用途に応じて適切なカウント手法を選択する必要があります。 1. byte数カウント len() UTF-8のバイト列として参照します。 func countBytes(s string ) int { return len (s) } // 例 fmt.Println(countBytes( "Hello" )) // 5 fmt.Println(countBytes( "こんにちは" )) // 15 (ひらながはUTF-8では3バイト) fmt.Println(countBytes( "café" )) // 5 (é[U+00E9]はUTF-8では2バイト) 2. rune数カウント utf8.RuneCountInString() Go言語の内部コードである rune 単位で、 rune 1つがUnicodeのコードポイント1つに対応します。 func countRunes(s string ) int { return utf8.RuneCountInString(s) } // 例 fmt.Println(countRunes( "Hello" )) // 5 fmt.Println(countRunes( "こんにちは" )) // 5 fmt.Println(countRunes( "café" )) // 4 3. 正規化後カウント Unicodeでは、一見同じ見た目でも異なるUnicode表現になるものが存在します。結合文字(Combining Character)という文字数としてカウントしないコードポイントが存在します。 例えば以下の文字は一見同じ文字に見えます(環境によっては別物に見えるかも知れません)が、コードポイントしては別物で、また文字数も異なります。 が [U+304C] が [U+304B U+3099] これを一つの表現に統一するのが正規化で、正規化してからカウントします。 import "golang.org/x/text/unicode/norm" func countNormalizedRunes(s string ) int { normalized := norm.NFC.String(s) return utf8.RuneCountInString(normalized) } // 例 s1 := "が" // [U+304C] s2 := "が" // [U+304B U+3099] fmt.Println(s1 == s2) // false fmt.Println(countRunes(s1), countRunes(s2)) // 1 2 fmt.Println(countNormalizedRunes(s1)) // 1 fmt.Println(countNormalizedRunes(s2)) // 1 正規化形式の選択 Unicodeには4つの正規化形式があります。最終的に獲得したい形式に対応した正規化方法を選ぶ必要があります。 名前 説明 特徴 NFC 正規化(合成) 視覚的に同じなら同じコードにまとめる NFD 正規化(分解) 組み合わせ可能な文字を分解する NFKC 互換正規化(合成) 表示上同じ意味の文字を1つにまとめる(全角→半角など) NFKD 互換正規化(分解) 分解かつ互換性も加味 func compareNormalizationForms() { s1 := "が" fmt.Printf( "NFD: %s(%U)→%s(%U) \n " , s1, [] rune (s1), norm.NFD.String(s1), [] rune (norm.NFD.String(s1))) // NFD: が([U+304C])→が([U+304B U+3099]) s2 := "が" fmt.Printf( "NFC: %s(%U)→%s(%U) \n " , s2, [] rune (s2), norm.NFC.String(s2), [] rune (norm.NFC.String(s2))) // NFC: が([U+304B U+3099])→が([U+304C]) s3 := "Ⅳ" fmt.Printf( "NFKC: %s(%U)→%s(%U) \n " , s3, [] rune (s3), norm.NFKC.String(s3), [] rune (norm.NFKC.String(s3))) // NFKC: Ⅳ([U+2163])→IV([U+0049 U+0056]) s4 := "A1" fmt.Printf( "NFKC: %s(%U)→%s(%U) \n " , s4, [] rune (s4), norm.NFKC.String(s4), [] rune (norm.NFKC.String(s4))) // NFKC: A1([U+FF21 U+FF11])→A1([U+0041 U+0031]) s5 := "㎝" fmt.Printf( "NFKD: %s(%U)→%s(%U) \n " , s5, [] rune (s5), norm.NFKD.String(s5), [] rune (norm.NFKD.String(s5))) // NFKD: ㎝([U+339D])→cm([U+0063 U+006D]) } 4. 書記素クラスタ数(ユーザー知覚文字数) Unicodeにはゼロ幅接合子(Zero Width Joiner)やModifierなど、文字数としてはカウントしない種類のコードポイントも存在します。 これらのコードポイントを用いると、特定のコードポイントと組み合わせて別の文字表現ができるようになります。これにより必要となるコードポイント数が減らすことができます。絵文字における肌の色を変更したり、🇯🇵をJ+Pのように表現する、といった用途に使われます。 最終的に、環境に依るところはありますがここで表示される文字列の表現がユーザーが実際に認識する文字数と言えます。 ただ、この算出を自力で実装するのはかなり大変なので、公開されているライブラリに頼るほうがよいと思います。 注意:最後の2つの文字がブログの仕様で複数文字に分割して表示されていますが、本来は 🏳️🌈 と 👨👩👧👦 です。 import "github.com/rivo/uniseg" func countGraphemes(s string ) int { gr := uniseg.NewGraphemes(s) count := 0 for gr.Next() { count++ } return count } // 例 fmt.Println(countGraphemes( "Hello" )) // 5 fmt.Println(countGraphemes( "こんにちは" )) // 5 fmt.Println(countGraphemes( "café" )) // 4 fmt.Println(countGraphemes( "😀" )) // 1 [U+1F600] (絵文字は1文字として認識) fmt.Println(countGraphemes( "🇯🇵" )) // 1 [U+1F1EF U+1F1F5](Regional Indicator Symbolsは1文字として認識) fmt.Println(countGraphemes( "🏳️<200d>🌈" )) // 1 [U+1F3F3 U+FE0F U+1F308] (白旗 + 異体字セレクタ + 虹 = レインボーフラッグは1文字として認識) fmt.Println(countGraphemes( "👨<200d>👩<200d>👧<200d>👦" )) // 1 [U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466] (家族絵文字も1文字) まとめ 文字数のカウントという一見簡単そうで実はいろいろとややこしい問題について記事を書いてみました。 Go言語でUnicodeを扱う場合、ある程度までは言語としてのサポートを受けられますが、そもそもUnicodeの仕様としてコードポイントと人間が認識する文字数には齟齬があるため、これらの差分を埋めるためにはどうしても実装時に考慮が必要となります。 やりたいことに応じた適切なカウント手法を採用するように心がけましょう。
はじめに 前半の記事では、新卒合同研修の第1回から第3回までの内容をご紹介しました。 前半の記事もぜひご覧ください! tech.every.tv 後半となる本記事では、第4回から第6回の講義についてお伝えします! 後半の研修は、より実践的な内容が盛りだくさんでした。実際にサーバーを解体してハードウェアの仕組みを学んだり、ISUCON形式でパフォーマンスチューニングに挑戦したり、最新の生成AI技術について学んだりと、エンジニアとして知っておきたい幅広い知識を身につけることができました。 それでは、各回の内容を見ていきましょう! 6/4開催 4回目 サーバー解体研修 こんにちは、開発1部 デリッシュキッチンAWG PUの黒髙です。私の方からはGMOぺパボ株式会社様に主催していただいた「サーバー解体研修」について紹介します。 GMOインターネットグループ株式会社様の会場提供により、GMO Yoursで開催されました。 サーバー基礎講座 前半は、デル・テクノロジーズ株式会社の福田さんに「サーバー基礎講座」と題して、大きく以下三つの内容を講義していただきました。 サーバーの基礎 - パソコンが「個人」で使うことを想定しているのに対し、サーバーは「みんな」で使うことを前提としており、24時間365日の連続稼働や高い信頼性が求められる、といったお話でした。 サーバーの構造 - CPU, DRAM, SSD/HDD, NICといった各構成部品の説明。データの正確性を保つECCメモリや、ストレージの冗長化を実現するRAID構成など、サーバーならではの部品についても学びました。 サーバーならではの付加機能 - PowerEdgeサーバーのリモート管理ツールであるiDRAC(integrated Dell Remote Access Controller)について解説いただきました。 私が特に興味深かったのは、サーバーがいかに「停止しないこと」を重視して設計されているかという点です。電源や冷却ファンの冗長化、リモートでの管理機能など、すべてがサービスの継続性を支えるための工夫であり、普段利用しているPCとの設計思想の根本的な違いを理解することができました。 このように事前にハードウェアの知識をインプットしたことで、より一層「サーバーの内部を見てみたい」という気持ちが高まりました。 サーバー解体作業 続いて、研修の目玉である後半のサーバー解体についてレポートしていきます。 会場にはサーバーが複数台用意されており、任意のチームに分かれてそれぞれ解体を進めていきました。 こちらが私たちのチームで解体していくサーバーになります。データセンターのラックに効率的に搭載するため、一般的なタワー型PCよりも横向きに細長い「ラックマウント型」と呼ばれる形状をしています。この一台で軽自動車が買えるほどの値段がすると聞いた時は、大変驚きました。 まずは天板を外します。CPU、メモリ、電源ユニットなど、基本的なパーツ構成は通常のPCと同じですが、各パーツの信頼性や拡張性は大きく異なっていました。例えば、電源ユニットは二重化されており、片方が故障してもサーバーは停止しません。また、冷却ファンが多数搭載されている様子からも、高性能なパーツを安定して動かす強力な冷却性能が見てとれます。 各チームのサーバーはそれぞれ構成が異なっており、私たちのものは特にメモリの搭載量が多いモデルでした。GPUを搭載しているチームもあったようで、少し羨ましかったのを覚えています。 解体作業に特定のマニュアルはなく、各メンバーが「これは何だろう?」と興味の赴くままに、ときには協力し合いながら進めていきました。 その結果、以下のように多くのパーツを取り出すことができました。 工具を使わない範囲での解体だったため限界はありましたが、裏を返せば、交換頻度の高いメモリやストレージなどのパーツは、専門的な技術者でなくても簡単に扱えるよう設計されていることもわかりました。 復元作業とメンテナンス性 さらに、ここから元の状態に戻す「復元作業」も行います。 目に入ったものを次々と取り出していく解体作業よりも、パーツを取り付ける順序などを考慮する必要があるため、少し身構えました。 しかし、実際にはそこまで難しくありませんでした。各パーツは所定の場所にしか収まらないように工夫されており、メンバーと確認しながら作業を進めることで、意外とすぐに元の状態に戻すことができました。 この一連の作業を通して、サーバーがいかにメンテナンス性を考慮して設計されているかを実感しました。実際に、このサーバーのストレージ(HDD/SSD)や電源はホットプラグ(サーバーの電源を入れたまま部品の交換ができる仕組み)に対応しているというお話を聞き、まさに常時稼働が求められるサーバーならではの特徴だと感じました。 HDDの解体 懇親会では、特別にHDDの解体もさせていただきました。HDDは、プラッタ(データを記録する円盤)とヘッド(データを読み書きする部分)の隙間が数ナノメートルと非常に狭く、空気中の微細な塵が付着するだけでも壊れてしまうため、このHDDが再び動くことはありません。普段目にすることのできない精密な内部構造に、思わず見入ってしまいました。 普段触れることのできないハードウェアにたくさん触れることができ、とても貴重な体験でした! まとめ AWSなどクラウドサービスの利用が当たり前になり、私たちにとってサーバーはより抽象的な存在になりつつあります。しかし、物理的なサーバーの仕組みを理解することは、クラウド上で発生するパフォーマンスの問題や障害に対して、より深いレベルでの洞察を与えてくれます。 そのため、今回得られたハードウェアの知識は、インフラの設計や障害対応など、より低いレイヤーを扱う際に必ず役に立つと感じました。 もはやクラウドを採用するのが当たり前のようになっている昨今ですが、サービスの特性やコスト、セキュリティ要件といった運用のユースケースに応じて柔軟なインフラ選択が求められます。その中で、自社でサーバーを持つオンプレミスも、必要に応じて選択肢に入れるべきだと感じました。 6/11開催 5回目 ISUCON研修 開発1部 デリッシュキッチンMS DRMの 鈴木 です。私からはISUCON研修の紹介をします。 ISUCON とは、「 いい感じに スピードアップ コンテスト 」の略称で、お題となるWebサービスを決められたレギュレーションの中でどれだけ高速化できるかを競う大会のことです。 isucon.net 今回のISUCON研修では、本番のISUCONのようにチームで与えられた問題に取り組む形式で行われました。 事前準備 私はISUCON初挑戦だったのですが、事前にISUCON出場経験のある先輩にISUCONでよく使われるツールやテクニックに関する知見を共有して頂きました。 ツールとしては以下のようなものがあり、研修当日に活用しました。 pt-query-digest slowqueryの解析に使うツール mysqlのログを解析してsqlが遅い順に表示される alp NGINXのアクセスログの解析に使うツール 遅いエンドポイント順に表示される 研修当日 Step 0: 準備 - Gitでのコード管理 本格的な改修を始める前に、まずEC2インスタンス上にあったコードをGit管理下に置き、GitHubリポジトリにプッシュしました。これにより、変更履歴の追跡や、問題発生時の切り戻しが容易になり、安心して作業を進めるための基盤が整いました。 Step 1: N+1クエリの特定と解消 - DB負荷の最大の原因を叩く 課題 アプリケーションの動作を分析したところ、トップページを表示するだけでデータベースに大量のクエリが発行されていることが判明しました。これは、投稿の一覧を表示するループ処理の中で、投稿ごと・コメントごとに個別のSQLクエリを発行してしまう、典型的な「 N+1クエリ問題 」でした。 分析 問題となっていたのは、 app.go の makePosts 関数です。以下のように、forループの中で都度DBアクセスが発生していました。 // 問題のあったコード(抜粋) for _, p := range results { // ループごとにコメント数やユーザー情報を取得していた db.Get(&p.CommentCount, "SELECT COUNT(*) FROM `comments` WHERE `post_id` = ?" , p.ID) db.Select(&comments, "SELECT * FROM `comments` WHERE `post_id` = ?" , p.ID) // ... } 解決策 ループの外側で、必要なデータを一度にまとめて取得する方式に変更しました。 表示対象となる投稿のIDをすべて集める。 SQLの IN 句を使い、関連するコメント情報やユーザー情報を一括で取得する。 取得したデータをGoの map に格納する。 ループの中ではDBにアクセスせず、 map からデータを参照して構造を組み立てる。 この修正により、発行されるクエリ数は、投稿数に関わらず一定(数回)にまで激減しました。 Step 2: データベースインデックスの追加 - 検索速度の向上 課題 N+1問題を解決した後、次に pt-query-digest を使ってスロークエリログを分析したところ、特定のクエリが依然として遅いことがわかりました。特に ORDER BY created_at DESC によるソート処理がテーブル全体をスキャン(フルテーブルスキャン)しており、大きなボトルネックとなっていました。 解決策 パフォーマンスを改善するため、以下のインデックスをデータベースに追加しました。 -- トップページの投稿一覧表示を高速化 CREATE INDEX idx_posts_created_at ON posts (created_at); -- ユーザーページの投稿一覧表示を高速化 CREATE INDEX idx_posts_user_id_created_at ON posts (user_id, created_at); -- コメント一覧の取得を高速化 CREATE INDEX idx_comments_post_id_created_at ON comments (post_id, created_at); これにより、検索やソートがインデックスを使って効率的に行われるようになり、クエリの実行速度が大幅に向上しました。 Step 3: 高コストな外部コマンド呼び出しの排除 - CPU負荷の削減 課題 CPU使用率を調査する中で、ユーザー登録やログイン時に呼ばれるパスワードのハッシュ化処理がボトルネックとなっている可能性が浮上しました。コードを確認すると、 exec.Command を使って外部の openssl コマンドを呼び出していました。 // 修正前の digest 関数 func digest(src string ) string { out, err := exec.Command( "/bin/bash" , "-c" , `... | openssl dgst -sha512 | ...` ).Output() // ... } 外部プロセスの起動は非常に高コストな処理です。 解決策 この処理を、Goの標準ライブラリ crypto/sha512 を使って、Goのプロセス内で完結するように書き換えました。 // 修正後の digest 関数 import ( "crypto/sha512" "encoding/hex" ) func digest(src string ) string { hasher := sha512.New() hasher.Write([] byte (src)) return hex.EncodeToString(hasher.Sum( nil )) } この修正により、プロセス起動のオーバーヘッドがなくなり、CPU負荷を大幅に削減できました。 Step 4: ページネーションによるデータ取得の効率化 課題 主要なボトルネックを解消していくと、それまで隠れていた新たな問題が見えてきました。トップページでは20件の投稿を表示しているにもかかわらず、SQLでは全件の投稿データを取得しており、無駄な処理が行われていました。 解決策 トップページの投稿を取得するクエリに LIMIT 20 を追加しました。 -- 修正前 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC -- 修正後 SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC LIMIT 20 この単純な修正により、DBからアプリケーションへのデータ転送量が削減され、メモリ使用量とDB負荷の両方が改善されました。 (なお、あとになって気づくのですが、削除済みユーザーのフィルタリングによる表示数不足を補うために、少し多く LIMIT 30 くらいにしないといけません。) しかしながら、残り1時間を切ったところで問題が発生しました。用意されたデータを誤って削除してしまったようです。 運営の皆さんに助けを求めたところ、新しいEC2を立てていただきました。 皆さん、データバックアップは取っておきましょう、、、 終了時刻が刻一刻と迫ってくる中、なんとかスコアを伸ばそうと、手早く「データベースインデックスの追加 」、「ページネーションによるデータ取得の効率化」を行いました。なお、ページネーションでは、余裕を持って LIMIT 50 にしておきました。 まとめ 最終的に、私たちのチーム「アポヒ」は 21350点 で 43チーム中17位 という結果になりました。1時間弱の格闘ではこの点数が限界でした、、 データバックアップを取っていなかったことが一番の反省点ですが、同時に実践形式で学ぶことも多く有意義な研修でした。ぜひ本家ISUCONにリベンジしたいと思います。 6/18開催 6回目 生成AI活用 開発1部 デリッシュキッチンMS DRMの惟高です。 私からは生成AI活用の紹介をします。 本研修は、大きく2つのテーマで構成されていました。 1. マイクロソフトの変遷とAIエージェントの概念 2. AI時代におけるソフトウェアエンジニアの役割の変化 AIの進化 AIの進化は、私たちがテクノロジーと関わる方法を大きく変えようとしています。 これまで、エンジニアのコーディングをアシストするCopilotのようなAIツールがありましたが、 マイクロソフトでは、2025年をAIエージェント元年と位置づけ、自然言語であらゆる操作や管理を可能にする新たな挑戦が進められています。 AIエージェントとは、ユーザーがタスクを割り振ると自律的に完遂する「代理人」のような存在です。 マイクロソフトは複雑な業務を自律的にこなすエージェントの提供を始めており、将来的にはエージェント同士が連携するマルチエージェントの世界が構想されています。 エンジニアの役割の変化 AIが発展する以前のエンジニアは、顧客課題の発見から設計、コーディング、チューニング、システムの監視に至るまで、人間主導型であらゆる工程をこなす必要がありました。 しかし、AIの登場によりこれまで人間が行っていた多くの作業をAIに任せられるようになりました。 今後は、AIに対して適切な指示を出し、その出力を評価し、最終的により良いアウトプットを形成する役割が重要とのことでした。 誰もがフルスタックエンジニアになれる可能性を秘めている一方で、これまでのエンジニアのようにあらゆるタスクを人手のみでこなすのではなく、AIを活用して全体の調和を取りながらプロジェクトを推進するような姿勢に変わらなければ、エンジニアとして生き残るのが難しいと感じました。 開発スタイルの変化 GitHubの統計によると、AIを利用した開発者数は2024年の2割未満から2028年には9割近くに増加すると予測されています。 つまり、数年後には社会全体でAIを使って効率化していこうというムーブメントが起こる可能性があるということです。 現状のAIによる開発がコーディングに限られているのに対して、将来的には企画・設計・実装・テスト・運用といったソフトウェア開発のライフサイクル全体に活用されることで、開発スタイルが大きく変わると考えられています。 コーディングスタイルの変化では、現在のCopilotによるAI支援(ペアプログラミング)から、AIが自律的に作業を行うチームの一員(ピアプログラミング)へと進み、さらに2030年には、AIがほぼ完全に自律的にソースコードを実装する時代が到来すると予測されています。 この未来では、エンジニアの主な役割は「要件定義」と「AIが作成したものがその要件を満たしているかの確認」となり、プロダクトマネージャーのような、サービス・製品の方向性を定める役割が重要視されます。 人間がやり続けること AIがコードを生成する時代においても、そのコードの正しさを理解するためのプログラミングの基礎知識は依然として重要とのことでした。 しかし、AIがコードを書けてしまうため、今後は実践的な力よりも知識として知っておくことが求められるようになるそうです。 それよりも重要になってくるのが、AIに意図を正確に伝えるための「プロンプトエンジニアリング」の能力です。 AIは推論の中でハルシネーションを起こす可能性もあるため、目標を明確に宣言し、自然言語で要件や意思を的確に記述するスキルが不可欠だと説明いただきました。 また、AIは限界値テストや倫理的な判断が苦手なため、最終的なテストは依然として人間が行うべきとのことでした。 企業倫理やガバナンス、国のポリシーなど、人間の価値観に基づく判断は、今後も人間の重要な役割として残ると考えられています。 まとめ 本研修を通して、ここから数年の内にAIが自律的に実装する時代が来ると再認識しました。(もうすでに来ているのかもしれません...) AIは私が考えているよりも身近なものになり、今後は「息をするようにAIを使う」ことが重要になってくると感じました。 またAIに対しては、何をして欲しいかといった指示を的確に伝える能力が必要になり、言語化能力がより必要になってくると感じました。 今後はAIを使い倒し、少しでも効率よく開発ができるように試行錯誤していきたいと思います。 おわりに 全6回の新卒合同研修を通じて、本当にたくさんのことを学ぶことができました。 技術的な知識はもちろんですが、何より良かったのは他社の新卒エンジニアの皆さんと出会えたことです。同じような悩みを抱えていたり、違う視点でものを見ていたり、刺激をもらうことがたくさんありました。研修後も連絡を取り合う仲間ができたのは、この研修の大きな収穫の一つです。 また、各分野の第一線で活躍されている講師の方々から直接学べたのも貴重な経験でした。最新技術のトレンドや現場での実践的な情報など、参考書では学べない生きた知識を得ることができました。 この研修で得た知識や経験、そして仲間たちとのつながりを大切にしながら、これからもエンジニアとして成長していきたいと思います。 最後に、このような素晴らしい機会を提供してくださった日本CTO協会の皆様、各回の講師の皆様、そしてスポンサーをしていただいた企業様に心から感謝いたします。ありがとうございました!
はじめに こんにちは、株式会社エブリーの2025年新卒エンジニアです。 私たちは、2025年5月から6月にかけて開催された日本CTO協会主催の新卒合同研修に参加しました。本記事では、研修の概要や各回の講義内容、そして実際に参加して得られた学びや気づきについてご紹介します。 2024年の新卒合同研修参加レポートも是非ご覧ください! tech.every.tv 新卒合同研修とは 日本CTO協会が主催する新卒合同研修は、会社の枠を超えて新卒エンジニアが業界全体で成長できる場をつくることを目指しています。背景には、エンジニア不足や、スタートアップ・中小企業にとって新卒育成のコストが大きいといった、業界共通の課題があります。 そこで、さまざまな企業や専門家が協力し、最新技術や実践的なスキル、キャリア形成、クラウド、サーバー解体、ISUCON、生成AIなど、幅広いテーマで講義やハンズオンが行われました。 2025年は下記のような日程で開催されました。 研修回 テーマ・内容 講師/スポンサー 第1回 Google Cloudのスペシャリストと学ぶ!BigQuery & Gemini グーグル・クラウド・ジャパン合同会社 第2回 CTOから新卒に向けた講話、生成AI時代のソフトウェアエンジニアとしての働き方の期待値 日本CTO協会 / 株式会社LayerX、株式会社Progate 第3回 (初学者・中級者向け) AWS JumpStart アマゾンウェブサービスジャパン合同会社 第3回 (上級者向け) AWSサービスを使いISUCONで高得点を出そう! アマゾンウェブサービスジャパン合同会社 / 日本CTO協会 若手エンジニアコミュニティ有志 第4回 サーバー解体研修 GMOペパボ株式会社 第5回 日本CTO協会ISUCON新卒研修+解説(※事前課題あり・クリア必須) 株式会社PR TIMES / ピクシブ株式会社 第6回 生成AIに関する講義 日本マイクロソフト株式会社 研修はオフラインで開催され、他社の新卒エンジニアと交流したり、コミュニティを広げたりする機会もたくさんありました。 この取り組みは、エンジニアとしてのキャリアのスタートを後押しし、業界全体の成長につなげることを目指しています。 詳しくはこちらをご覧ください。 cto-a.org ここからは、それぞれが印象に残った講義を1つずつピックアップし、内容や学びについてご紹介していきます。 5/14開催 1回目 Google Cloud のスペシャリストと学ぶ! BigQuery & Gemini 開発1部 デリッシュキッチンMS SPの谷口です。私からはグーグル・クラウド・ジャパン合同会社にて開催された第1回研修「Google Cloud のスペシャリストと学ぶ! BigQuery & Gemini」について紹介します。 この研修では、Google Cloudのスペシャリストの方々からBigQueryやGeminiについて直接学ぶ貴重な機会をいただきました。特にGoogle Cloudのデータ分析・AI関連サービスを中心に、現代のデータ活用における最新のアプローチについて詳しく教えていただきました。また、後半ではNotebookLMの活用方法についてグループディスカッションを行い、参加者同士で様々なアイデアを共有する時間がありました。 BigQueryの進化とデータ活用の新時代 フルマネージドなデータウェアハウス Google CloudのBigQueryはサーバーレスアーキテクチャで構築されたフルマネージドなデータウェアハウスサービスです。 従来のデータ分析ではインフラの管理やスケーリングに多くのリソースを割く必要がありました。しかし、BigQueryを活用することで、これらの運用負荷から解放され、データ分析そのものに集中できるようになるという話をまずしていただきました。 Gemini in BigQuery 自然言語でのデータ分析 研修で最も驚いたのは、Gemini in BigQueryの自然言語でクエリを記述できる革新的な機能でした。 これまで複雑なSQLを書く必要があった分析作業が、日本語での質問形式で実行できるようになります。例えば「先月のユーザー登録数の推移を教えて」といった自然な問いかけから、適切なSQLクエリが自動生成されます。 分析の民主化 この機能により、SQL知識が限定的なビジネスサイドのメンバーでも、直接データウェアハウスに問い合わせを行うことができるようになります。 私はこれを聞いて、真の意味での「分析の民主化」だと感じました。組織全体でのデータ活用レベルの底上げに大きく貢献するはずだと思います。 NotebookLMセッション 実践的なAI活用事例 NotebookLMは、Googleが開発したAIを活用したリサーチアシスタントで、アイデアの洗練と整理をサポートしてくれます。アップロードした文書やデータを基に、質問応答や要約、分析などを行うことができます。 NotebookLM活用事例のセッションでは、参加者同士で様々な活用事例を共有することができました。 文書の要約、質問応答、アイデア生成など、日常業務で即座に活用できる具体的な使い方を学ぶことができ、非常に実践的な内容でした。 グループディスカッションでの活用アイデア セッションでは、グループに分かれてNotebookLMの活用方法についてアイデアを出し合うセクションがありました。 その中で特に興味深かったのは、 会議の文字起こしとNotebookLMを組み合わせた活用方法 というアイデアです。 会議内容を文字起こしした後、その内容をNotebookLMに追加することで、会議中に出てきた専門用語や不明な点について後から詳しくNotebookLMに質問することができるのではないか、という提案がグループから出ました。 今後の活用に向けて データドリブンな意思決定の加速 今回学んだGoogle Cloudのサービスを活用することで、データに基づいた意思決定のスピードを大幅に向上させることができると思います。 特にGemini in BigQueryの自然言語クエリ機能により、データ分析から洞察の獲得、そして施策の立案までのサイクルを劇的に短縮できる可能性があります。 チーム全体でのデータ活用レベル向上 自然言語インターフェースの活用により、これまでデータ分析に関わることが少なかった私たちのチームメンバーも積極的にデータを活用できるようになると期待しています。 これにより、組織全体でのデータリテラシー向上と、より多角的な視点からの分析が可能になるのではないでしょうか。 まとめ 今回のGoogle Cloud入門セッションを通じて、私はデータ分析とAI活用の新しい可能性を学ぶことができました。 特に印象的だったのは、技術的な障壁を下げることで「すべてのエンジニア」「すべてのメンバー」がデータとAIを活用できる環境が整いつつあることでした。 これらの技術を積極的に取り入れることで、デリッシュキッチンサービスの更なる発展に貢献していきたいと思います。 5/21開催 2回目 エンジニアの働き方・キャリア 開発1部 デリッシュキッチンAWG PUの 岩﨑 です。 私からはエンジニアの働き方とキャリアについて紹介します。 この研修は、大きく2つのテーマで構成されていました。 登壇者 所属 役職 タイトル 島津 真人 株式会社Progate CTO AI時代の新卒エンジニアに必要な変化と学習 松本 勇気 株式会社LayerX CTO キャリアの考え方、フォロワーシップ AI時代の新卒エンジニアに必要な変化と学習 まず株式会社Progate CTO 島津さんのセッションでは、AI時代における新卒エンジニアの変化と学習についてお話しいただきました。 ソフトウェアプロダクト開発の変化 従来のソフトウェアプロダクト開発は、「企画→ 設計→ 実装→ 評価」というフローで進み、プログラミングはその中の道具の1つでした。特に「ジュニアエンジニア(=新卒)」は、先輩から与えられたタスクに基づいて実装をこなすことが、2024年頃までは一般的でした。 しかし2025年現在ではこの状況は大きく変化しており、Devin、Cursor、Claude Codeといった生成AIツールが登場したことで簡単なタスクであればAIが実装できるようになっています。 これにより「簡単な仕事はどんどん捌けて、なんらかの理由で難しい仕事がどんどん残る」という状況が発生し、結果として新卒エンジニアのオンボーディングに適した「簡単なタスク」が減少するという課題が生まれています。 生成AIと仕事をしていく上で考えること ここで生成AIが進化するたびに話題としてあげられる「生成AIがいれば人間は代替されてしまうのか?」という問いに対し、島津さんは明確に NO と答えています。 これまでの仕事の一部がAIによって代替されたとしても、 次に人間がやるべきことが必ず出てくる とおっしゃっていました。 こういった時代の流れにおけるエンジニアの心構えについて、島津さんより何点かお伝えいただいた中で個人的に最も印象に残ったお話を紹介します。 AI時代のエンジニアの心構え 現状AIはまさに過渡期であり状況は常に変化しているため、それに追いつき、やり方を更新し続ける必要があります。 これは従来も求められていましたが、変化の大きさが格段に増している点が新しい部分です。 そして世の中でAIの活用法に答えが出ていないため、以下のサイクルを継続的に回して知見をアップデートしていく必要があります。 局所最適を無限に積み重ねる :自分自身でたくさん試行錯誤を繰り返すこと。 知見を共有し、アップデートする :得られた知見をチームや組織、コミュニティーに還元すること。 技術の進歩に合わせて1と2を繰り返す 。 新しい技術が出てきたらとにかく試す。 それをまずは個人で試行錯誤してチームや組織内でブラッシュアップするサイクルが回るようになれば、弊社が掲げるAIで開発スピードを10倍にすることも実現できるのではないかと感じました。 キャリアの考え方、フォロワーシップ 株式会社LayerX CTO 松本さんのセッションでは、キャリアの考え方とフォロワーシップについてお話しいただきました。 キャリアの考え方 松本さんはキャリアを後悔しないためのポイントとして以下の3つをあげています。 投資家的思考 コミュニティ 最初の10年間の使い方 それぞれ首が取れるほど頷ける内容なのですべて紹介したいところですが、ここでは 投資家的思考 を取り上げます。 投資家的にキャリアを考える ここではキャリアを資産としてとらえ、自身のキャリアを単なる仕事の連続ではなく、人生という資産を最大化するための「投資活動」と捉えます。 「資産」とはお金だけでなく、信用・信頼、健康・体力、名声、知識・経験、時間など多様な要素が含まれます。 投資家的思考とは、私たち一人ひとりが自身の「資産」を守り、その資産を使って自分の仮説・学びたい方向に向けて効率よく投資することでより大きな資産を獲得していく「投資家」である、という考え方です。 リスクとリターン このような考え方をする上で、どんな意思決定にもリスクとリターンが存在することを意識する必要があります。 自分が目的とするリターンを得るために、手元にどのような選択肢があり、どのようなリスクがあるのかを整理することで、最適な選択をすることができます。 一般的にリスクとリターンは比例しますが、知識量を増やすことで同じリターンをより小さなリスクで得ることが可能になります。 探索と学び そしてその知識を増やし、不確実性を低下させるためには継続的な学習サイクルが不可欠です。 「仮説立て→ 行動→ 振り返り→ 学習・知識化」のサイクルを回すことで、より精度の高い状況理解と不確実性の低下につながります。 バランスシートとポートフォリオ とはいえ、やりたいことすべてに投資することはできません。 自身の持つ投資可能な資産(お金、時間、体力、信用など)をどのように配分するかといったバランスシートを作成し、余分な資産(特に時間)に対してどこにどれだけ使うかを決めることで、キャリアの方向性が明確になります。 レバレッジ そして時にはレバレッジをかけ、リスクをとってより大きく投資をすることも必要です。 手元の資産を特に知識や経験といったストックされる方向へ投資し、運用効率が高まる手段を追いかけることでより大きなリターンを得ることができます。 例として、以下のような項目が挙げられていました。 お金で時間や知識・経験を買う(家事のアウトソースや有料ツールの活用) チームで動く(人にレバレッジをかける) 信用のレバレッジ(スキルが不足していても信頼があれば任せてもらえる) こういったレバレッジをかけることで、キャリアのスピードを加速させることができます。 しかしリスクを取りすぎると失敗する可能性もあるため、自身の取りたいリスク度合いに応じて調整が必要です。 まとめ AI時代のキャリアを考える上での指針となるお話を島津さんから聞くことができて本当によかったなと思っています。 また松本さんに発表いただいた資料は以前より拝見しており、こちらも私自身のキャリア指針に大きな影響を与えてくれているので今回の研修で直接お話を伺うことができてとても嬉しかったです。 AI時代の波に乗り遅れないようなキャリア戦略を考え続けたいと思います! 5/27・28開催 3回目 AWS JumpStart 開発1部 デリッシュキッチンMS DRMの 江﨑 と開発1部デリッシュキッチンAWGヘルシカの 赤川 です!私たちからは、AWS JumpStartについて紹介します! AWS JumpStart 2025は、AWS初学者のエンジニアを対象とした実践的な研修プログラムです。 このプログラムのゴールは、一般的なアーキテクチャの理解、AWSコアサービスの概要とその選定基準の把握、そしてAWSアーキテクチャ図作成の流れを学ぶことです。 タイムスケジュール 2日間のスケジュールは以下の通りです。1日目は座学とAWS環境の構築を行うハンズオン、2日目は1日目の内容を踏まえたより実践的なアーキテクチャ検討を行いました。 1日目 まず1日目の内容について紹介します。 講義 講義では「アーキテクティングのコツ」について解説いただきました。Webサービス構築におけるフェーズごとのAWSサービス選定の基礎や、システムスケール時の課題と対策について学びました。 講義の内容を簡潔にまとめると以下のようになります。 フェーズ1: プロトタイプ(〜100人) 構成: Route 53 → EC2 → RDS 特徴: シンプル・安価、可用性は重視しない フェーズ2: 一般公開(100人〜10,000人) 構成: Route 53 → ALB → EC2(マルチAZ) → RDS(マルチAZ) 改善点: - Application Load Balancerによる負荷分散 - 複数AZでの冗長化 - Auto Scalingの導入 フェーズ3: 大規模化(10,000人〜1,000,000人) 構成: CloudFront → ALB → Auto Scaling Group → Aurora + リードレプリカ + ElastiCache 改善点: - CloudFront: 静的コンテンツのCDN配信 - Aurora: 高性能DB、リードレプリカで読み取り負荷分散 - ElastiCache: インメモリキャッシュでDB負荷軽減 フェーズ4: 超大規模(1,000,000人以上) 特徴: 計測・分析・改善サイクルの重視 改善点: - アプリケーション最適化 - DBシャーディング、NoSQL活用 - マイクロサービス化 - Infrastructure as Code、CI/CD導入 システム構築時は、最初から過度に作り込まず、要件を満たすシンプルな設計から始めること、そして計測・分析・改善のサイクルを継続することが重要であると学びました。 ハンズオン ハンズオンでは、実際にAWS環境の構築を体験しました。2〜3人のチームで、AWS環境を操作するドライバーと、手順書を確認しながら指示を出すナビゲーターをローテーションしながら進める、モブプログラミング形式で実施されました。 前半のハンズオンでは、以下のようなシンプルなアーキテクチャを構築しました。実際に手を動かすことで、AWS環境構築の理解が深まりました。 後半のハンズオンでは、より実践的なアーキテクチャを構築しました。ALBによる負荷分散や、DBのフェイルオーバー機能による可用性の担保など、前半よりも高度な構成となっています。 また、アーキテクチャを構築して終わりではなく、ECSタスクを停止させた際の挙動なども確認し、実際に障害が発生した場合にどのように可用性が保たれるかを体験できました。 まとめ AWS JumpStart 1日目を通して、単なるAWSサービスの学習にとどまらず、「なぜこのサービスを選定するのか」「どのようにアーキテクチャを設計すべきか」といったアーキテクティングの思考プロセスの基礎を学ぶことができました。 また、モブプログラミング形式で学ぶことで、チーム内で活発に意見交換し、教え合うことで理解をより深めることができました。ここで学んだことを土台に、今後の業務でも要件に合ったアーキテクチャを考えていきたいと思います。 2日目 2日目はより実践的な内容で、与えられたお題に沿ったアーキテクチャを設計する課題に取り組みました。 アーキテクチャの検討(個人) まずは個人でアーキテクチャを検討する時間が設けられました。与えられたお題に対して、以下のような構成を考えました。 マルチAZ構成で冗長化 フロントエンド:React(TypeScript)プロジェクトをECS Fargateで実行、CDNのためにCloudFrontを使用、静的コンテンツはS3から配信 バックエンド:Java/Spring BootアプリケーションをECS Fargateで実行、時間に応じてオートスケーリング データベース:Aurora、レプリカも追加 ロードバランサー:Application Load Balancer(ALB) ログ:CloudWatchとKinesis経由で、S3に保存 DNS:Route 53 1日目で学んだ内容をもとに、基本的なWebサービスのアーキテクチャに必要なことは網羅することができたと思います! アーキテクチャの検討(グループ) まず、メンバーそれぞれが考えたアーキテクチャを共有し、特徴と改善すべき点を共有していきました。 その後各アーキテクチャの良いところを集めて、以下の要素が追加されました。 - キャッシュ:ElastiCache - キュー:SQS - 外部との接続:NAT Gateway - 認証:Cognito これで、我々のアーキテクチャ図は完成しました! NAT Gatewayを経由した外部APIとの連携を丁寧に描けたのが良かったと思っています! 発表会 発表では3チームが選ばれ、それぞれのアーキテクチャについて説明をしていました。 私のチームは選ばれませんでしたが、弊社メンバーのいるチームが選ばれたのでそのアーキテクチャを載せておきます! 個人的には、普段の定例会議でダッシュボードを見る機会が多いので、ダッシュボード作成の機構があるのが良いなと思いました! 懇親会 夜の懇親会では、はじめに24新卒の先輩からAWSに関するLTが行われました。 その中で、シナジーマーケティング株式会社の木山さんからAWSのコスト管理に関するお話をしていただきました。 この話を聞いて、早速個人開発のプロジェクトにコストアラートを設定しておきました! その後は、一緒にアーキテクチャを作ったチームメンバーや他の会社の人たちとたくさん交流することができました! その時に知り合ったメンバーと二次会に行ったり、輪読会を始めたりと、同期の繋がりの良さを改めて実感しました。 まとめ この研修を通じて、実際のアーキテクト業務を体験することができました。 実際のビジネス要件を技術的にどう実現するかなどを深く考えるのはとても楽しかったです。 さらに面白かったのは、完成したアーキテクチャがグループによって全く違ったことです。 中にはサーバーを立てずに、全てをLambdaでサーバーレスに処理するという挑戦的なアーキテクチャもありました。 今後は、「使ったことある」AWSサービスを増やしていきたいです! 実務ではもちろん、個人開発でも色々なAWSサービスを試してみたいと思います! おわりに ここまで、2025年新卒合同研修の前半(第1回〜第3回)についてご紹介しました。 後半では、第4回から第6回の研修内容についてお伝えします。 ぜひ、後半の記事もご覧ください! tech.every.tv
目次 はじめに WAF 導入の背景 WAF 導入にあたり調査・検討したこと ログの運用 Slack 通知の実現 WAF 導入時の考慮点 おわりに はじめに こんにちは。 開発本部開発1部トモニテ開発部所属の庄司( @ktanonymous )です。 先日、トモニテで WAF (Web Application Firewall) を導入しました。 WAF の導入により、これまで以上に安心感を持ってサービス運用に向き合えるようになったと感じています。 本記事では、WAF 導入の背景から、実際に調査・検討した内容、そして導入後の運用についてまとめていきます。 WAF 導入の背景 サービス運営をしている中で、攻撃を受けるというのはよくあることだと思います。 トモニテでも攻撃と思われるアクセスを検知することがありますが、直近ではそのようなアクセスが過去に比べても多い状況が続いていました。 以前には、トモニテで SQL インジェクション攻撃を受けたことに関するブログも公開していますので、そちらもご覧ください。 SQL インジェクションのような攻撃に対しては、アプリケーション側のバリデーションの徹底などによる対応をしていましたが、 DoS 攻撃のように大量のリクエストを送りつけることでシステム負荷を高めるような攻撃に対しては、 社内 Slack でアラートが発報され、都度状況確認するという対応をしていました。 システムの過負荷を検知した時にアラートを発報する監視体制が取れているのは良いのですが、平日・休日・昼夜問わず対応を迫られてしまう状況は、対応負荷も高く健全ではないと感じていました。 そこで、大量アクセスによる過負荷が続いたことも踏まえ、WAF の導入を決定しました。 (なお、トモニテではインフラに AWS を利用しているため、AWS WAF を導入しました。) WAF 導入にあたり調査・検討したこと WAF を導入するにあたっては、運用・金額コストをできるだけ抑えるミニマム構成を前提として、以下の観点で調査・検討を進めました。 ログの運用 WAF を導入する上で、最小コストで運用できるログは何かを考えました。 結論から述べてしまうと、追加コストなく設定できるサンプルリクエストおよび CloudWatch Metrics を利用することにしました。 S3 にログを保存することも考えましたが、そのためには Kinesis Data Firehose を利用する必要があるため、 シンプルに導入できる構成を採用することにしました。 これらは、Terraform で WAF のルールのリソースを作成する際に以下のように visibility_config というフィールドを定義するだけで設定できます。 visibility_config { cloudwatch_metrics_enabled = true metric_name = "metric_name" sampled_requests_enabled = true } WAF のサンプルリクエストでは、実際のリクエストの一部がランダムにピックアップされます。 あらゆるリクエストのサンプリングだけでは WAF のルールで検知されたリクエストを確認するのは難しいですが、サンプルリクエストはメトリクス別にフィルタリングして確認することができるので、WAF がどのようなリクエストを検知したのかを十分に確認することができます。 WAF のサンプルリクエスト Sampled requests の各項目について Metric name visibility_config で設定した metric_name Source IP 該当のルールで検知されたリクエストの送信元 IP アドレス URI 該当のルールで検知されたリクエストの URI Rule Inside rule group 該当のルールが所属するルールグループ Action 該当のルールで検知されたリクエストに対して実行されたアクション Time 該当のルールで検知されたリクエストの送信時刻 Slack 通知の実現 WAF で攻撃と思われるリクエストを検知した時および攻撃が止んだ時に Slack へ通知することで 即座に状況を把握できるようにもしたいと考えました。 そこで、追加のコストを掛けずに通知システムを実現するため、以下のような構成を採用しました。 WAF アラームの通知システム この構成では、WAF のルールの検知状況を CloudWatch Alarm で監視します。 今回の監視対象は、レートベースによる IP アドレスのブロックルールになります。 WAF によってブロックされたということは攻撃と判断されていることになるため、アラームの閾値は 1 件としています。 これにより、攻撃発生時に Slack へ通知されるようになります。 ただし、これだけでは平時のアラームの状態が「データ不足」と認識されてしまうので、 0 件の状態を「正常」であると認識させるために、 noBreaching の設定をしておきます 1 。 この設定を追加しておくことで、攻撃が止みブロックルールの検知数が 0 件に戻った時に、 アラームが「正常」状態に戻り、攻撃が止まったことを Slack へ通知することができるようになります。 (アラームは状態変化によるものなので複数の攻撃を通知することはできませんが、 全ての攻撃が終わったタイミングで「正常」に遷移するため、状況把握という観点では十分だと考えています。) 最終的には、以下のような Terraform コードを実装することで実現しました。 resource "aws_cloudwatch_metric_alarm" "resource_name" { alarm_name = "alarm_name" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = 1 metric_name = "BlockedRequests" namespace = "AWS/WAFV2" period = 300 statistic = "Sum" threshold = 1 alarm_description = "過剰なリクエストのIPアドレスをブロックしました" alarm_actions = [ var.sns_topic_arn, ] ok_actions = [ var.sns_topic_arn, ] treat_missing_data = "notBreaching" # 0 件の状態を「正常」であると認識させるための設定 dimensions = { WebACL = var.web_acl_name, Rule = var.rule_name, Region = var.region, } } WAF 導入時の考慮点 WAF を導入する際、攻撃を防ぐために大量リクエストをブロックしたくなりますが、 いきなりブロックするルールを設定してしまうと、ブロックしてはいけないリクエストまでブロックされてしまう可能性があります。 そのため、まずはカウントのルールを設定してから様子を見てからブロックのルールに変更しました。 また、実際にリソースを作成する前に、開発環境で動作確認を行いました。 作成する予定のリソースと同じ構成のものを開発環境に作成し、 簡単なスクリプトで大量リクエストが発生する状況を再現することで、WAF が意図通りに動作するかを確認しました。 おわりに 今回の記事では、トモニテにおける WAF 導入の背景から、調査・検討内容、実際の運用までを紹介しました。 今回の対応のおかげで、安心感を高めることはできたかと思います。 WAF を導入しただけでセキュリティが完璧になるわけではありませんが、 今後もサービスの安全性向上に向けて、アプリケーション・インフラ両面からセキュリティ対策をおろそかにしないようにしていきたいと思います。 今回の記事が、少しでも皆さんのお役に立てれば幸いです。 最後まで読んでいただき、ありがとうございました。 Configuring how CloudWatch alarms treat missing data ↩