コネヒト開発者ブログ

コネヒト開発者ブログ

Draft Releaseデプロイフローを作成してみました

こんにちは。サーバーサイドエンジニアの岡田です

みなさんはどんなデプロイフローを採用していますでしょうか?今回は自分が取り組んでいたデプロイフローのちょっとした改善を皆さんに紹介したいと思います!

きっかけ

現状コネヒトではgdpを使ったデプロイ方法を採用しており、手元のコマンドからデプロイするのが標準になっています。

tech.connehito.com

元々自分はgit-pr-releaseを使ったワンクリックデプロイを以前の現場で使っており、手元でコマンドを打たなければいけないのが地味にめんどうだなあと感じていました。 そこでワンクリックでもっとラクしてデプロイしたい!と思ったのが今回のデプロイフローを考案したきっかけになります!

それ以外にも手元のコマンドからデプロイする事に感じていた課題感

  • 手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性
  • devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため)
  • 新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる
  • 本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう

そしてこのデプロイフローが生まれた背景

当初はgit-pr-releaseを導入すれば簡単にワンクリックデプロイ出来るようになる!と考えていました。しかしいざgit-pr-releaseを導入しようと調べてみると、難しい背景がいくつか見つかりました。

ちなみにgit-pr-releaseとは

git-pr-releaseとは、本番環境向けの対象ブランチへのリリースPRを作成&リリース内容を一覧にして作成してくれるgemライブラリです。導入も簡単なので使える環境であればぜひ使ってみて欲しいです

こちらの記事が非常に参考になります!

songmu.jp

まずgit-pr-releaseが想定しているブランチモデルは

  • 本番用のmainブランチと開発用のdevelopブランチが並走する形で、developブランチからfeatureブランチを切ってmainブランチへマージしていく、いわゆるgit flowの簡易版の様なフローが前提
  • mainブランチへのマージが本番デプロイへのトリガーとして想定されている
  • 環境構成:本番環境(main)、dev環境(develop)

git-pr-releaseが想定しているブランチモデル

対してコネヒトでは

  • GitHub Flowを踏襲しており、本番用のmainブランチからfeatureブランチを切ってmainブランチへマージしていくフローになっている
  • 最新のmainブランチから切ったtagのpushが本番デプロイへのトリガーになっている
  • 環境構成:本番環境(最新のtag)、dev環境(featureブランチ)

コネヒトのブランチモデル

なのでもしgit-pr-releaseを採用する場合はブランチ戦略自体を見直す必要があり、流石にそれは影響範囲が大きすぎるかつ現実的ではありませんでした。そこで今あるブランチ戦略を変えずにもっと簡単にデプロイを実現する方法として考えたのがこの「Draft Releaseデプロイフロー」です。

そして実際にできたデプロイフローがこちら

いつものようにPRをマージします

Slackにdev環境へのデプロイ完了とドラフトリリースの作成が通知されます

作成されたドラフトリリースを

Publishすると

tagが切られ本番デプロイが走ると言った感じです

ビフォーアフター

既存のgdpを使ったデプロイフロー

  1. mainブランチからfeatureブランチを切る
  2. feature PRをmainブランチへマージ(dev環境へデプロイ)
  3. dev環境へのデプロイが完了したら、手元でgdpコマンドからタグを切ってpush
    1. 手元のmainを最新化(git checkout main && git pull origin main && git pull --tags)
    2. リリース内容確認(gdp deploy -d)
    3. タグを切って本番デプロイ(gdp deploy)
  4. 本番環境へのデプロイが完了したら、手元でgdpを使ってリリースノートを公開
    1. リリースノート内容確認(gdp publish -d)
    2. リリースノート公開(gdp publish)

デプロイフローBefore

今回作成したDraft Releaseデプロイフロー

  1. mainブランチからfeatureブランチを切る
  2. feature PRをmainブランチへマージ(dev環境へデプロイ)
  3. dev環境へのデプロイが完了したら、GitHub上から下書きリリースノートの内容を確認し、問題なければ公開(合わせてタグも切られるのでそのまま本番デプロイ)

デプロイフローAfter

改善ポイント

  • 手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性
    • GUI上から確認できるので、意図せずデプロイしてしまうといったことを防げる
  • devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため)
    • devデプロイ後は別のブランチに切り替えてすぐ作業の続きができるように
  • 新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる
    • 今回の対応で不要に
  • 本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう
    • 必ずリリースノートが作成されるフローに

実際のコード

呼び出し元

ドラフトリリース作成ワークフロー

下記コードを各PJのリポジトリに展開するだけでドラフトリリースが作成されます!

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  deploy:
    # デプロイ処理
    ...

  create_draft_release:
    if: ${{ success() }}
    needs: [ deploy ]
    uses: {Reusable Workflow用リポジトリ}/reusable-workflow/.github/workflows/create_draft_release.yml@main
    secrets:
      slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

呼び出される側

ドラフトリリース作成ワークフロー

ドラフトリリース作成actionの実行と、Slack通知を行っています。

name: Create Draft Release
on:
  workflow_call:
    secrets:
      slack-webhook-url:
        required: true

jobs:
  start:
    runs-on: ubuntu-latest
    steps:
      - name: Action create-draft-release
        id: create-draft-release
        uses: {Reusable Workflow用リポジトリ}/reusable-workflow/.github/actions/create-draft-release@main

      - name: Notify slack of create draft release
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,workflow
          author_name: ${{ github.actor }}
          text: |
            ドラフトリリースが作成されました。<${{ steps.create-draft-release.outputs.draft-release-link }}|${{ steps.create-draft-release.outputs.draft-release-tag }}>
            <${{ github.server_url }}/${{ github.repository }}/releases|リリース一覧>
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.slack-webhook-url }}

実装時のポイント

Reusable Workflowとはいわゆる再利用可能なワークフローの事で、各リポジトリで共通のワークフローを実行することができます! 作成方法はシンプルでon.workflow_callを定義したymlファイルを作成するだけです。

on:
  workflow_call:

今回Reusable Workflowとして実装した理由ですが、Reusable Workflowと後述のaction.ymlの大きな違いとしては実行環境に左右されるかどうかだと考えています。

Reusable Workflowの場合はワークフロー(1つ以上のジョブ)でしか実行できないので前後の処理や実行環境に左右されません。逆にaction.ymlではジョブの一ステップとして実行されるので、必要であれば actions/checkout@v2actions/setup-node@v2 など使って実行環境を整える必要があります。

そして今回のDraft Release作成処理は完全に処理が独立しているので、リポジトリ毎の実行環境に左右されずに横展開しやすいReusable Workflowで実装しました

Reusable Workflow使用時の注意点としては実行されるコードはあくまで呼び出し元で実行されるので

  • actions/checkout を使用する場合は呼び出し元のブランチにチェックアウトされる
  • Reusable Workflowのリポジトリ内にあるファイルは参照できない

と言った点に注意が必要です!上記点を解決するために次のaction.ymlへとつながります

詳しくは公式ドキュメントを確認してください!

docs.github.com

ドラフトリリース作成処理本体

こちらがメインのドラフトリリース作成処理です。実行している処理はシンプルにGitHub CLIを使ったリリースノートの作成と削除を行なっています。(削除はリリースノートの自動生成を使いたいため一度削除→再作成しています)

name: Create Draft Release Action
description: Create draft release with calver tag

outputs:
  draft-release-link:
    description: Create draft release link.
    value: ${{ steps.create-draft-release.outputs.draft-release-link }}
  draft-release-tag:
    description: Create draft release tag.
    value: ${{ steps.create-draft-release.outputs.draft-release-tag }}

runs:
  using: "composite"
  steps:
    - name: Create Draft Release
      id: create-draft-release
      env:
        TZ: "Asia/Tokyo"
        GH_TOKEN: ${{ github.token }}
      shell: bash
      run: |
        # 最新のタグ取得
        latest_tag=$(gh release list --repo '${{ github.repository }}' | awk -F'\\t' '$2=="Latest" { print $3 }')
        echo "latest_tag:${latest_tag}"

        # calver形式での次バージョンのタグ取得
        next_tag=$(/bin/bash ${{ github.action_path }}/calver.sh $latest_tag)
        echo "next_tag:${next_tag}"

        # draft release取得
        draft_tag=$(gh release list --repo '${{ github.repository }}' | awk -F'\\t' '$2=="Draft" { print $3 }' | head -n 1)
        echo "draft_tag:${draft_tag}"

        # draft release が既に存在していれば、削除してから再作成(リリースノートの自動生成を使いたいため)
        if [ "$draft_tag" != "" ]; then
          gh release delete $draft_tag --repo '${{ github.repository }}' -y
        fi
        draft_release_link=$(
          gh release create $next_tag \\
            --repo '${{ github.repository }}' \\
            --title "Release ${next_tag}" \\
            --latest --draft --generate-notes
        )
        echo "draft-release-link=${draft_release_link}" >> "${GITHUB_OUTPUT}"
        echo "draft-release-tag=${next_tag}" >> "${GITHUB_OUTPUT}"
        echo $draft_release_link

実装時のポイント

action.ymlとはいわゆるメタデータ構文と言われているもので、ワークフロー内の処理を切り出したり共通化することができます! 普段よく使っているようなactions/checkoutのようなアクションを作成できるイメージです。作成方法はこちらもシンプルでaction.ymlまたはaction.yamlのファイル名で作成するだけです

action.ymlではパブリックリポジトリにあるアクションはもちろん、同一リポジトリ内のアクションからプライベートリポジトリのアクションも実行可能です。

    # パブリックのaction
    uses: actions/create-draft-release@main
    # 同一リポジトリ内のaction
    uses: ./.github/actions/create-draft-release
    # プライベートリポジトリのaction
    uses: {owner}/{repo}/.github/actions/create-draft-release@main

詳しくは公式ドキュメントと

docs.github.com

こちらの記事が参考になりました!

developer.mamezou-tech.com

calver形式でのタグ作成シェルスクリプト

このシェルスクリプトを作成した背景ですが、前提としてコネヒトではcalver形式でのバージョン管理を採用しています。

タグ作成にあたってできるだけ既存と同じようなcalver形式のタグを作成する必要があったのですが、現状これと同じようなタグを作成するツールが見つかりませんでした。そこで元々のgdpコマンドで行なっていたタグ作成ロジックをそのままコピーしてきて、このシェルスクリプトを作成することにしました。(GitHub Actionsとの相性や実行の手軽さも考えてシェルスクリプトにしました)

#!/bin/bash
set -eu

##############################
## Get next calver
## @ref <https://github.com/Connehito/gdp/blob/main/format.go>
##############################
today=$(date +'%Y%m%d')
regexp="(.*)([0-9]{8})\\.(.+)"

version=${1:-$today}

if [[ $version =~ $regexp ]]; then
    if [[ $today = ${BASH_REMATCH[2]} ]]; then
        minor=${BASH_REMATCH[3]}
        next=$(($minor + 1))
        echo "${BASH_REMATCH[1]}${today}.${next}"
        exit 0
    fi
    echo "${BASH_REMATCH[1]}${today}.1"
    exit 0
fi
echo "${today}.1"
exit 0

おわりに

同じような環境でデプロイフローに悩みを感じでいる方の参考になれば嬉しいです

AWSコスト削減の一環でecrmを使ってECRの不要イメージを削除した話

こんにちは、コネヒト プラットフォームグループの@yosshiです。

昨今、円安の進行によりAWSのコスト増が無視できない状況となっています。
このような状況下で、コスト削減関連のイベントに注目が集まるなど、コスト削減への関心が徐々に高まってきているように感じます。

弊社でもさまざまな方法でコスト削減施策を進めており、今回はその一環で「ecrm」というツールを使用しECRイメージを削除した話をしたいと思います。

弊社では、コンテナのオーケストレーションにAmazon ECSを主に使用しています。 ECSにタスクをデプロイする場合は、イメージをビルドした上でAmazon ECRにpushし、そのECRのイメージを使用しタスクを起動しています。 ECRはイメージが増えれば増えるほど、容量に応じてコストがかかるので、この機会に対応したいと考えていました。

ECRで発生するコスト

ECRではデータストレージの容量に応じて料金がかかります。
ap-northeast-1(アジアパシフィック(東京))のリージョンでは、GB/月当たり 0.10USDの料金が発生します。
※2024/01/17時点の情報。ECRでは上記とは別に転送コストなどが発生します。

参考:https://aws.amazon.com/jp/ecr/pricing/

ecrmとは?

ecrmはAmazon ECRから不要なイメージを安全に削除するOSSです。
github.com

こちらのブログに詳細が書いてありますので、詳しくはこちらをご確認ください。
techblog.kayac.com

ECRには元々ライフサイクルポリシーという機能があり、世代数やイメージプッシュからの日数に基づき古いイメージを自動的に削除することができるのですが、「現在のECSタスクで使用しているもの」という観点でチェックをしてくれるわけではないので、使用中のイメージを削除してしまうリスクがあると感じていました。

そこで調べていたところecrmの存在を知り、試してみることにしました。

試してみる

インストール(macOS で brew を使う場合)

$ brew install fujiwara/tap/ecrm

イメージ削除にあたり、削除対象を決めるための設定ファイルが必要になります。
AWSアカウントのcredentialを適切に環境変数などで設定した状態で 以下コマンドを実行することで、アカウントに存在するECRやECS、Lambdaのリソースを元に、設定ファイル(ecrm.yaml)を自動生成してくれます。

$ ecrm generate

実行すると設定ファイル(ecrm.yaml)が作成されました。
クラスタやタスク定義はワイルドカードのパターンで指定でき、prefixが記号([_/-])で区切れてまとめられそうなものは、適宜まとめたパターンで自動生成してくれます。

ecrm.yaml

clusters:
  - name_pattern: <cluster1>-*
  - name_pattern: <cluster2>-*
    ・
    ・
task_definitions:
  - name_pattern: <task_definition1>-*
    keep_count: 5
  - name_pattern: <task_definition2>-*
    keep_count: 5
    ・
    ・
lambda_functions:
  - name_pattern: <lambda_function1>-*
    keep_count: 5
    keep_aliase: true
  - name_pattern: <lambda_function2>-*
    keep_count: 5
    keep_aliase: true
    ・
    ・
repositories:
  - name_pattern: <repository1>-*
    expires: 30d
    keep_count: 5
    keep_tag_patterns:
      - latest
  - name_pattern: <repository2>-*
    expires: 30d
    keep_count: 5
    keep_tag_patterns:
      - latest
    ・
    ・

この設定ファイル(ecrm.yaml)を弊社の都合に合わせて修正していきます。

設定した条件 (削除対象外とするもの)

  • 現在使用中のイメージ
    • ECSクラスタで現在実行中のタスクに含まれるイメージ
    • ECSサービスで指定されているタスク定義に含まれるイメージ
  • ECSタスク定義で指定されているイメージ(タスク定義最新リビジョンから5世代分
  • Lambda関数で指定されているイメージ(Lambda最新バージョンから5世代分)
  • 90日以内に作成されたイメージ
  • タグ名にlatest, releaseがついているイメージ ※弊社ではリリース対象に左記のようなタグ名をつけています

上記の下線部は可変にできるので、適宜適切な内容に置き換えてください。 設定ファイル(ecrm.yaml)は以下のようになりました

clusters:
  - name_pattern: <cluster1>-*
  - name_pattern: <cluster2>-*
    ・
    ・
task_definitions:
  - name_pattern: <task_definition1>-*
    keep_count: 5
  - name_pattern: <task_definition2>-*
    keep_count: 5
    ・
    ・
lambda_functions:
  - name_pattern: <lambda_function1>-*
    keep_count: 5
    keep_aliase: true
  - name_pattern: <lambda_function2>-*
    keep_count: 5
    keep_aliase: true
    ・
    ・
repositories:
  - name_pattern: <repository1>-*
    expires: 90d
    keep_count: 5
    keep_tag_patterns:
      - *latest*
      - *release*
  - name_pattern: <repository2>-*
    expires: 90d
    keep_count: 5
    keep_tag_patterns:
      - *latest*
      - *release*
    ・
    ・

設定ファイル(ecrm.yaml)の編集が完了したら、どの程度イメージが削減できそうか確認します。

削除対象は以下コマンドで確認できます。

$ ecrm plan

実行したところ以下のようになりました。

REPOSITORY     |   TYPE    |    TOTAL     |    EXPIRED    |    KEEP      
---------------------------+-------------+--------------+---------------+--------------
<repository1>  | Image     | 256 (38 GB)  | -187 (27 GB)  | 69 (11 GB)      
<repository2>  | Image     | 227 (44 GB)  | -209 (41 GB)  | 18 (3.2 GB)  
<repository3>  | Image     | 92 (13 GB)   | -82 (12 GB)   | 10 (1.5 GB)  
<repository4>  | Image     | 109 (307 GB) | -57 (181 GB)  | 52 (126 GB)  
<repository5>  | Image     | 63 (67 GB)   | -52 (53 GB)   | 11 (14 GB)   
<repository6>  | Image     | 251 (2.2 GB) | -236 (2.0 GB) | 15 (190 MB)  
<repository7>  | Image     | 771 (67 GB)  | -760 (66 GB)  | 11 (862 MB)  
<repository8>  | Image     | 150 (75 GB)  | -135 (67 GB)  | 15 (7.8 GB)  
<repository9>  | Image     | 65 (22 GB)   | -60 (20 GB)   | 5 (1.7 GB)   
<repository10> | Image     | 138 (29 GB)  | -128 (27 GB)  | 10 (2.1 GB)  
<repository11> | Image     | 116 (21 GB)  | -104 (19 GB)  | 12 (2.2 GB)  
<repository12> | Image     | 927 (129 GB) | -910 (126 GB) | 17 (2.8 GB)  
<repository13> | Image     | 284 (55 GB)  | -231 (44 GB)  | 53 (11 GB)   
<repository14> | Image     | 57 (546 MB)  | -21 (186 MB)  | 36 (360 MB) 

トータルのイメージに対して削除対象がどの程度あるか(EXPIRED)、残るイメージがどの程度あるか(KEEP)がわかります。

実際に削除を実行する際には以下コマンドを実行します。

$ ecrm delete

実行した後、実際に削除されたかどうかを確認したいので再度ecrm planを実行します。

$ ecrm plan

実行結果

REPOSITORY       |    TYPE     |    TOTAL    | EXPIRED |    KEEP      
-----------------+-------------+-------------+---------+--------------
<repository1>    | Image       | 69 (11 GB)  |         | 69 (11 GB)   
<repository2>    | Image       | 18 (3.2 GB) |         | 18 (3.2 GB)  
<repository3>    | Image       | 10 (1.5 GB) |         | 10 (1.5 GB)  
<repository4>    | Image       | 52 (126 GB) |         | 52 (126 GB)  
<repository5>    | Image       | 11 (14 GB)  |         | 11 (14 GB)   
<repository6>    | Image       | 15 (190 MB) |         | 15 (190 MB)  
<repository7>    | Image       | 11 (862 MB) |         | 11 (862 MB)  
<repository8>    | Image       | 15 (7.8 GB) |         | 15 (7.8 GB)  
<repository9>    | Image       | 5 (1.7 GB)  |         | 5 (1.7 GB)   
<repository10>   | Image       | 10 (2.1 GB) |         | 10 (2.1 GB)  
<repository11>   | Image       | 12 (2.2 GB) |         | 12 (2.2 GB)  
<repository12>   | Image       | 17 (2.8 GB) |         | 17 (2.8 GB)  
<repository13>   | Image       | 53 (11 GB)  |         | 53 (11 GB)   
<repository14>   | Image       | 36 (360 MB) |         | 36 (360 MB)  

EXPIREDの列がブランクとなり、対象の不要イメージが削除されたことがわかります。

結果

今回のケースでは685GBのイメージの削減に成功しました。 現時点のAWSのコストベース(GB/月当たり 0.10USD)で換算すると、月間68.5USDの削減となります。
※2024/01時点のAWS ECRのコスト

感想

今回はecrmを使用してECRイメージを削除した話をしました。

金額としては小さいかもしれませんが、もともと使っていないイメージに対する余分なコストだったので、このタイミングで簡単に削減できてよかったなと思います。

コスト削減に注目が集まっている状況だと思うので、各社のコスト削減の参考になれば幸いです。

今回は単発でイメージの削除を行いましたが、今後は自動で定期実行する仕組みなども合わせて検討していきたいと考えています。

2023年のコネヒト開発組織を振り返る

こんにちは。CTOの永井(shnagai)です。

早いもので今年も残すところ数日ですね。

アドベントカレンダー最終日ということで、技術的なトピックやチームでの工夫はこれまでの記事でみんなが思う存分書いてくれたので、今回は2023年のコネヒト開発組織の1年を自分の視点で振り返っていこうと思います。

この記事は、コネヒトAdvent Calendar2023の25日目の記事です。

adventar.org

開発組織のアップデートと特に目立った技術的なトピックという構成で書いてます。

開発組織のリフレーミングと頼れる多くの仲間のジョイン

FY23のスタートに合わせて、開発組織のリフレーミングを行いました。

それぞれのチームの関係を可視化

期初のタイミングでチームトポロジーを参考に、それぞれのチームの構造を改めて可視化しました。

自分はチームトポロジーの、ストリームアラインドチームが出す価値の総量が開発組織のバリューという考えに非常に感銘を受けており、プロダクトの先にいるユーザーファーストの考えをうまく組織に当てはめた考えだなと思っています。

ママリや自治体向け事業等の事業毎にストリームアラインドチームを配置して、事業部と密に連携しながら開発するスタイルをコネヒトでは採用しています。

コンプリケイテッドサブシステムチームは技術的難易度の高い領域を受け持ち、プラットフォームチームでは、ストリームアラインドチームの認知負荷を減らしプラットフォームエンジニアリングでプロダクトの価値最大化に貢献することをミッションにしています。

下記は、期初戦略で全社に説明した資料の一部です。

※現在はメンバー増によりストリームアラインドが増えています。

ピープルマネジメントをEMに集約 〜エンジニアがよりプロダクト・事業に集中出来る体制へ〜

元々コネヒトでは、各開発グループにグループリーダー(以後、GL)を配置し、そのGLがチームのマネジメント4象限(ピープル/プロダクト/プロジェクト/テクノロジー)を全て担う構造にしていました。

だいたい2,3名のチームにそれぞれGLがいる構造だったのですが、今の組織フェーズ的にはエンジニアがよりプロダクトや事業に集中する時間を作るほうが良いと判断して、ピープルマネジメント業務をチーム横断でEM 2名に集約しました。

全てがうまくいっているわけではありませんが、ピープルマネジメントを集約したことでメンバーからはよりプロダクトや事業に集中出来ているというフィードバックをもらっており、一定うまくいっている施策だなと思っています。

制度やロールは常に見直しながらアップデートしていくのが良いと思っており、これからも組織拡大に合わせて柔軟に見直していき、アジリティの高い開発が出来るようにしていきたいと思っています。

きれいなことばかり書きましたが、テックリードという役割を設けるトライもしたのですが、中々イメージの言語化や定義が追いつかずに、役割を担ってくれたメンバーと対話しながら、半年でまた別のロールにしたりと頼れる同僚とともに試行錯誤しながらやっています。

3人に1人を目指して 多くの仲間のジョイン

今年は、7名の新しいエンジニアがコネヒトにジョインしてくれました。

コネヒトでは、テックビジョンにおいて「3人に1人」という戦術を掲げており、会社としてやりたい事業を全て実現し、テクノロジーやエンジニアリングの恩恵を社内も受け、そしてエンジニア以外でもテクノロジーを使いこなせるような世界を目指しています。

今年ジョインしてくれた仲間の職種も多岐に渡るのですが、それぞれが所属するチームや開発組織でバリューを発揮してくれており全体として活気づいています。

社内のエンジニア比率が高まっていく中で、生成AIの技術革新もあいまってテックビジョンに掲げる世界をより実現フェーズに持っていくために新たな動きも走り始めています。

全社向けに生成AIの活用とテクノロジーを利用した業務改善を推進する「Run with Tech 」プロジェクトで、こちらは、また、どこかの機会で紹介出来ればと思っています。

続いて、今年特に印象的に残った技術的なトピックについても紹介していければと思います。

検索システムの内製化完了

今年技術的なトピックで一番大きなトピックといえば検索システムの内製化が完了したことがまず挙げられます。

これまで、SaaSの検索システムを内部のAPIから呼び出す形式で使っていたものを、フルリプレイスする形で0から検索システムを構築しました。

時系列で見る検索システム内製化の軌跡

  • 2021年10月 構想開始
  • 2022年3月 経営承認
  • 2022年4月 開発開始
  • 2022年8月 最初の機能(新着質問順)のリプレイス完了
  • 2023年6月 全ての機能のリプレイス完了

主な狙いは、検索システムを内部で持つことによるユーザー体験の強化とSaaS入れ替えによる費用削減の2点でした。

時系列で書くとすんなり言ったように見えますが、すべての機能において既存とのABテストを行いママリにおける検索体験のユーザー毀損がないことを細かく確認しながらリプレイスを行いました。

新着順、人気順、サジェスト、関連キーワードと多岐に渡る機能のABテストはSaaS提供と同品質をゴールにしている関係で中々にヒリヒリするもので、メインで担当していた同僚達のやり切る力には脱帽です。

技術的な要素を少し解説すると、検索システム内製化には下記技術要素があります。

  • 内部APIから呼ばれる検索API(Go)
  • 検索エンジンにはOpenSearchを採用
  • 技術的難易度が一番高かったデータ同期の仕組みにはAWS GlueとStepFunctionsでニアリアルタイムな基盤を構築   tech.connehito.com

担当したメンバーは全員検索システムは未経験だったので、みんなでペンギン本を読んで輪読会をしたり、壁にぶつかるたびに議論して一歩ずつ前に進んでいきました。私自身もメンバーとして当初コミットしていて、検索システムは奥深くエンジニアとしてまた一つ成長できた良い機会だったと感じています。

tech.connehito.com

もちろん、リプレイスをして終了ではなく、ようやく検索システムをママリの武器に出来るスタートラインに立てたので、昨今のLLM全盛の時勢も鑑みながら、キーワード検索にとらわれずよりユーザーにとってママリの検索がより使いやすいものであり続けられるように技術的な検証や新たなトライも既に始まっています。

tech.connehito.com

「終わらせることを始めよう」を合言葉に最終的に3ヶ月計画を前倒しし、検索システム内製化という成果に対して社内表彰もされたプロジェクトとなりました。

ユーザーへの価値提供という点では、まさにこれから真価を問われることになりますが、テクノロジーでプロダクトを伸ばす先端事例になるような取組みなので今後がより楽しみです。

Let’s Go

コネヒトのテックビジョンでは、「バックエンドのシステムにGoを積極的に導入する」という戦略を掲げていおりその戦術名が「Let's Go」です。

これまでも、前述の検索APIしかり新規のシステムでのGoの採用は何度か行ってきました。それらを通しての、組織としての知見の習得はそこそこ進んできたかと感じています。

このLet’s Goの作戦は、aboyがボールを持って中心に進めてくれているのですが、今年の大きな進歩として、既存のPHPで書かれたコードベースをGoに置き換えるというプロジェクトが走り出しました。

まずは、Goの特徴である並列処理やワーカー実装のしやすさを活かせる非同期のバッチ処理のリプレイスを実施していますが、メインシステムを適材適所でGoに置き換える動きがスタートしているのは開発組織として大きな一歩だなと感じています。

中期戦略では、2024年中にバッチ処理を全てGoに置き換えるというチャレンジングな目標を掲げており、その実現に向けて日々開発を進めています。

また、Let’s Go Talkという社主催のイベントも定期的に実施しており、Go好きがゆるく繋がれるようなコミュニティも作っていきたいと思っているので興味のある方は是非ご参加ください。

コネヒト/Connehito Inc. - connpass

まとめ

2024年も引き続きプロダクトや事業を伸ばしていくことに注力しつつ、テックビジョンに掲げる「Beyond a Tech Company」の実現に向けてCTOというラベルを活かして色々と策を打っていこうと思います。

テックビジョンの存在が羅針盤からより会社の中で身近なものになっていけば、コネヒトとしてユーザーや社会に約束するありたい世界観の実現に近づいていくと自分は信じています。

最後にですが、エンジニアはもちろん、幅広い職種で採用もしていますので興味持たれた方は是非ご応募いただきカジュアルにお話出来るとうれしいです。

hrmos.co

trivyとGithub Actionsを使用しTerraform設定ファイルのセキュリティスキャンを実行する仕組みを作りました

この記事はコネヒトアドベントカレンダー21日目の記事です。

コネヒト Advent Calendar 2023って?
コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。
コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、
ママの一歩を支えるアプリ「ママリ」などを 運営しています。

adventar.org

はじめに

コネヒトのプラットフォームグループでインフラ関連を担当している@yosshiです。 今年の7月に入社してから早いもので半年が経ちました。時が経つのは本当に早いですね。

今回のブログでは、セキュリティスキャンツールであるtrivyを使って、自動的にIaC (Infrastructure as Code)スキャンを実行する仕組みを構築した話をしたいと思います。

弊社ではインフラ構成をTerraform利用して管理するようにしており、それをモノレポの構成で運用しています。

インフラリソースの作成・変更・削除をする際には必ず相互レビューを必須としているものの、人的なチェックのみに依存しているためファイルの設定ミスやセキュリティ上の見落としが潜在的なリスクとなっていました。

これらを事前に検知するツールを調べていたところtrivyの存在を知り、今回導入に至りました。

trivyとは

  • コンテナイメージやアプリケーションの依存ライブラリ・OSのパッケージなどを迅速にスキャンし、セキュリティリスクを効率的に検出するセキュリティツールです。
  • 当初はコンテナのセキュリティ問題に焦点を当てたツールとして開発されたようですが、後にTerraformやKubernetesなどの設定ファイルのチェック機能も追加されました。
  • 設定ファイルのチェックでは、設定ミスやセキュリティのベストプラクティスに沿っていない構成などを検知することができます。

参考:https://github.com/aquasecurity/trivy

(参考)スキャン可能な対象 2023/12時点

  • コンテナイメージ
  • ファイルシステム
  • リモートGitリポジトリ
  • 仮想マシンイメージ
  • Kubernetes
  • AWS

(参考)検出可能な内容 2023/12時点

  • OSパッケージとソフトウェア依存関係(SBOM)
  • 既知の脆弱性(CVE)
  • IaCの問題と設定ミス
  • 機密情報と秘密
  • ソフトウェアライセンス

上記の通りtrivyでは、Terraformのコードだけでなくさまざまなセキュリティスキャン行うことができます。

trivy自体の詳しい説明はここでは割愛するので、詳しくは公式サイトや他の方の記事などをご確認いただけると幸いです。

Github Actionsでの実装

では早速ですが実装内容の説明に移りたいと思います。 今回はTerraformコードのスキャンをCIに組み込んでいます。 弊社では CIツールとしてGithub Actionsを利用しているため、今回もこちらを利用します。

スキャン実行は、trivy公式で用意しているGithub Actions用のツール(tricy-action)があるので、こちらをそのまま利用しています。

背景

まず、前提条件となる弊社のディレクトリ構造を説明します。

弊社では、サービスで共通利用するリソース(base_system)と各サービスで利用するリソース(product_system)とでディレクトリを分けており、 product_sytem以下にはサービスごと関連するリソースが紐づいています。以下のようなイメージです。

.
├── base_system
│   ├── common
│   │   └── terraform
│   ├── privilege
│   │   └── terraform
│   .
│   .
└── product_system
    ├── (サービス1)
    │   └── terraform
    ├── (サービス2)
    │   └── terraform
    .
    .
    .

実装方針と内容

実装したGithub Actionsのコードは以下の通りです。

主に以下のことをやっています。

  1. シェルスクリプト(sync-updated-dirs-to-work-dir.sh)の実行
    • mainブランチとプルリクエスト中のブランチのコードの差分を検知
    • 差分となっているディレクトリを作業用のディレクトリに同期
  2. 作業ディレクトリに対してスキャン実行
  3. 検出されたスキャン結果をPRのコメントに残す。

Github Actionsのコードは以下の通りです。

name: trivy-scan

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  trivy_scan:
    name: Run Trivy Scan
    runs-on: ubuntu-latest
    steps:
      - name: Clone repo
        uses: actions/checkout@v4

      - name: fetch origin/main for getting diff
        run: git fetch --depth 1 origin $GITHUB_BASE_REF

      - name: Sync Updated Dir to Work Dir
        env:
          GITHUB_TOKEN: ${{ secrets.github_token }}
        run: |
          bash utils/scripts/sync-updated-dirs-to-work-dir.sh

      - name: Trivy Scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          severity: 'HIGH,CRITICAL'
          scan-ref: scan_work_dir
          output: trivy-scan-result.txt

      - name: Format Trivy Scan Result
        run: |
          if [ -s trivy-scan-result.txt ]; then
            # ファイルに内容がある場合
            echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
          else
            # ファイルが空の場合
            echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
          fi

      - name: Comment PR with Trivy scan results
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          recreate: true
          GITHUB_TOKEN: ${{ secrets.github_token }}
          path: formatted-trivy-result.md

詳しく見ていきます。

まずは変更差分となったディレクトリを調べるため、Github Actionsの中でシェルスクリプトを実行しています。

ここでは一時的なディレクトリを用意し、プルリクエスト上で変更が発生したディレクトリの内容を作業用のディレクトリに同期するという作業を行なっています。

Github Actionsの関連部分は以下です。

- name: Sync Updated Dir to Work Dir
   run: |
     bash utils/scripts/sync-updated-dirs-to-work-dir.sh

sync-updated-dirs-to-work-dir.shはrsyncをwrapしたもので、前述の通りプルリクエスト上で変更が発生したterraformディレクトリを、スキャン実行する一時的な作業用ディレクトリに同期する処理をしています。1

細かな実装は内部事情に特化したものとなっているため割愛しますが、例えば、base_system/common/terraform, base_system/privilege/terraform, product_system/(サービス1)/terraformのディレクトリで変更が発生している場合、このスクリプトを実行することで作業用ディレクトリ(scan_work_dir)が作成され、以下のようなディレクトリ構造となります。

.
├── base_system
│   ├── common
│   │   └── terraform
│   └── privilege
│   │   └── terraform
│   .
│   .
├── product_system
│   ├── (サービス1)
│   │   └── terraform
│   ├── (サービス2)
│   │   └── terraform
│   .
│   .
└── scan_work_dir (ここのディレクトリにtrivyによるスキャンを実行する)
    ├── base_system
    │   ├── common
    │   │   └── terraform
    │   └── privilege
    │   │   └── terraform
    └── product_sytem
        └── (サービス1)
            └── terraform

次に同期してきた作業用ディレクトリに対してスキャンを実行します。
ここでは前述の通り、公式が用意しているtricy-actionを利用しています。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: scan_work_dir
   output: trivy-scan-result.txt

オプションの内容は以下の通りです。

  • scan-type: 'config'
    • configはTerraformなどの設定ファイルをスキャンする際に使用する。
  • scan-ref: scan_work_dir
    • 作業用ディレクトリ scan_work_dir を指定している。
  • output: trivy-scan-result.txt
    • スキャンした結果をテキストファイルtrivy-scan-result.txtに出力する。
    • このテキストファイルは次のアクションでフォーマットを整形して出力するために使用している。
  • serverity
    • 脆弱性の深刻度で'CLITICAL'と'HIGH'を指定しています。
    • ここのレベルは、CVSS2によって定量化された脆弱性の深刻度をもとに設定されています。

他のオプションなどについてはtricy-actionのページをご確認ください。

次に、前のアクションで出力したテキストファイルを見やすく整形した上で、marocchino/sticky-pull-request-commentを使用しプルリクエストのコメント欄に出力しています。

プルリクエスト更新時には、既存の脆弱性に関するコメントを削除した上で新規コメントを残して欲しかったので、その点でmarocchino/sticky-pull-request-comment(recreate: trueのオプション指定)を利用することで楽に実装することができました。

# スキャンした結果を整える
- name: Format Trivy Scan Result
  run: |
    if [ -s trivy-scan-result.txt ]; then
      # ファイルに内容がある場合
      echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
    else
      # ファイルが空の場合
      echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
    fi

- name: Comment PR with Trivy scan results
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    recreate: true
    GITHUB_TOKEN: ${{ secrets.github_token }}
    path: formatted-trivy-result.md

スキャン結果

  • 脆弱性が検知されなかった場合

  • 脆弱性が検知された場合

「詳細」部分を開くと以下のような形で表示されています。

今回のケースだとCritical0件、High61件の脆弱性が検知されていることがわかります。

base_system/privilege/terraform/xxx.tf (terraform)
=====================================================================
Tests: 75 (SUCCESSES: 14, FAILURES: 61, EXCEPTIONS: 0)
Failures: 61 (HIGH: 61, CRITICAL: 0)

HIGH: IAM policy document uses sensitive action 'autoscaling:Describe*' on wildcarded resource '*'
════════════════════════════════════════
You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals.

See https://avd.aquasec.com/misconfig/avd-aws-0057
────────────────────────────────────────
 base_system/privilege/terraform/xxx.tf:376
   via base_system/privilege/terraform/xxx.tf:376 (aws_iam_policy.xxx.xxx)
    via base_system/privilege/terraform/xxx.tf:375-438 (aws_iam_policy.xxx.xxx)
     via base_system/privilege/terraform/xxx.tf:373-439 (aws_iam_policy.xxx)
────────────────────────────────────────
 373   resource "aws_iam_policy" "xxx" {
 ...
 376 [     Version = "2012-10-17",
 ...
 439   }
────────────────────────────────────────
・
・
・(以下省略)

上記の例では、重要度「HIGH」の脆弱性が検知されています。

IAMポリシーは最小特権で付与するべきなので、ワイルドカード使うのはリスクがありますよ、という指摘のようです。

補足

検出された脆弱性の中で、この内容は指摘する対象から除外したい、というケースもあると思います。

その場合は.trivyignoreというファイルをトップディレクトリに置くことで勝手に参照して検出対象から除外してくれます。

以下のような形で記載します。

.trivyignore

AVD-AWS-0057
AVD-AWS-XXXX

(参考)

以下のようにtrivyignoresのオプションを利用することで、トップディレクトリに配置するだけでなく別のディレクトリに配置しているファイルを参照させたり、.trivyignore以外の別のファイル名を指定することもできるようです。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: trivy_temp_dir
   output: trivy-scan-result.txt
   trivyignores: test-trivyignore

まとめ

今回はtrivyとGitHub Actionsを活用し、Terraformでのセキュリティ上のリスクを効果的に検知する仕組みを構築しました。

今回検知されたものについては、優先度の高いものから順次改善していきたいと思います。

また、冒頭に説明した通り、trivyでは他にも様々なものを検知してくれる機能があるので、 Terraformの設定だけでなく色々な場面での活用を検討していきたいと思います。


  1. 変更が発生したディレクトリの抽出には git diff origin/main --name-only の結果をパースしています。
  2. CVSS( Common Vulnerability Scoring System ) = 共通脆弱性評価システム

ママリiOSアプリで今年取り組んだ3つの改善について

「コネヒト Advent Calendar 2023」の20日目のブログです!

adventar.org

iOSエンジニアのyoshitakaです。 コネヒトに入社してそろそろ1年が経ちます。

コネヒトに入社してからはママリiOSアプリの開発を担当しています。

今回はママリiOSアプリで行った3つの改善をまとめました。

  1. フォルダ構成の変更
  2. 開発中アプリの配布フローの改善
  3. 画面遷移の改善

それぞれどんな課題があったか、どんな改善をしたかを紹介します。

フォルダ構成の変更

課題に感じていたこと

既存のフォルダ構成は機能追加をする際に対象となるコードが見つけにくい状況でした。

前提

  • ママリiOSアプリのアーキテクチャーはMVVMを採用している
  • View・ViewModelModelでモジュール分割している
    • 今回改善したのはView・ViewModel側のフォルダ構成

    ※ モジュール分割の取り組みについてはこちらの記事を見てください。 tech.connehito.com

どんな改善をしたか

変更前のフォルダ構成のイメージがこちらです。

App
├── ViewController
│   ├── 機能AのViewController
│   ├── 機能AのViewController
│   ├── 機能BのViewController
│   └── ...
├── View
│   ├── 機能AのView
│   ├── 機能AのView
│   ├── 機能BのView
│   ├── 機能AB共通のView
│   └── ...
└── ViewModel
    ├── 機能AのViewModel
    ├── 機能AのViewModel
    ├── 機能BのViewModel
    └── ...

アーキテクチャーがわかりやすい構成になっていると思います。

しかし、機能追加をする際に対象となるコードにたどり着くのに時間がかかっていました。

改善案は大きく二つありました。

  1. 現在のフォルダ構成の配下に機能ごとのフォルダを作る
    • メリット: 変更が少なく済む
    • デメリット: 機能ごとのコードが分散する
  2. App配下に機能ごとのフォルダ作り、機能フォルダ内でさらにViewとViewModelのフォルダを作る
    • メリット: 機能ごとのコードがまとまる
    • デメリット: フォルダ構成の変更が大きい

機能改善する際ViewとViewModelをセットで修正することがよくあるので、2のApp配下で機能ごとにフォルダ分けをすることにしました。

改善したフォルダ構成イメージがこちらです。

App
├── Features
│   ├── 機能A
│   │   ├── View:ViewController・DataSource・View置き場
│   │   └── ViewModel
│   ├── 機能B
│   ├── 機能C
│   └── ...
└── Common:共通化されたView・ViewModel置き場
    ├── View
    └── ViewModel

複数の機能で使われているものはCommonフォルダにまとめるようにしました。

フォルダ構成は好みもあるかと思いますが、整理したことで不要なファイルもお掃除でき、良い改善だったと思います。

開発中アプリの配布フローの改善

課題に感じていたこと

特定の開発中ブランチから開発中アプリのを配布する際に、CIでは実行できず、常にローカルでfastlaneを実行する必要がありました。

前提

  • CI/CDはBitriseとfastlaneを使っている
  • 開発中アプリはFirebase Distributionを使い社内に配布している
  • Firebase Distributionへの配布方法は以下の2つ
    • PullRequestをメインブランチにマージすると自動で配布される
    • ローカルでfastlaneを実行して配布する

どんな改善をしたか

CIでfastlaneのアプリ配布のワークフローを実行できるようにすることで、ローカルでのfastlane実行を不要にしました。

トリガーはGitHub Actionsを使い、ブランチを指定して実行するとBitriseのアプリ配布のワークフローが実行されるようにしました。

改善方法は別の記事にまとめております。

tech.connehito.com

この改善により、ローカル環境に依存しないアプリ配布ができるようになり、開発スピードのUPに繋がっています。

画面遷移の改善

課題に感じていたこと

アプリ全体の各ViewConrollerに画面生成と遷移のロジックが実装されていて、コードの見通しが悪く、ViewControllerの肥大化要因にもなっていました。

前提

  • 画面遷移のアーキテクチャーは採用していない
  • 各ViewControllerの任意の箇所でViewControllerの生成と遷移処理を行っている

どんな改善をしたか

画面生成と遷移処理のロジックをなるべく1箇所にまとめる仕組みを作りました。

具体的には画面生成と遷移処理部分で以下のような変更をしました。

let viewController = QuestionContainerViewController.instantiate(
    id: question.id
)
navigationController?.pushViewController(viewController, animated: true)

pushScreen(screen: .questionContainer(
    questionId: question.id
)

Screenというenumを作り、Screenの値を元にViewControllerを生成するようにしました。

enum Screen {
    case questionContainer(
        questionId: Int
    )
    ...
}

extension UIViewController {
    func makeViewController(from screen: Screen) -> UIViewController {
        switch screen {
        case .questionContainer(let questionId):
            return QuestionContainerViewController.instantiate(
                id: questionId
            )
        ...
        }
    }

    func pushScreen(screen: Screen) {
        let viewController = makeViewController(from: screen)

        self.navigationController?.pushViewController(viewController, animated: true)
    }
}

画面遷移のアーキテクチャーパターンの採用も検討しましたが、実装コストとメリットが見合わず、今回はよりライトにできる方法を採用しました。

画面遷移のアーキテクチャー検討について以前外部イベントで発表した資料がありますので、興味がある方はご覧ください。

www.docswell.com

この改善は現在も段階的に置き換えを進めているところですが、すでに実装完了している部分だけでもコードの見通しが良くなり、またテストコードも書きやすくなりました。

まとめ

どの改善も日々の開発業務のスピードを上げることができていると実感する部分が多くありました。

来年はより難易度の高い改善にも取り組めたらと思っております!

ドメインモデリングを通じて起きたチームの変化 ~ytake氏のワークショップ~

この記事はコネヒト Advent Calendarのカレンダー 13日目の記事です。

adventar.org

2023/10/06にytakeさんをお招きして、社内メンバーを対象にドメインモデリングの講義を開催しました。 今回は当日の様子と、その後の変化について紹介したいと思います。

現時点では、定量的に測れる変化までは至っていませんが、業務上定性的な変化が起きていると感じています。 今回、本記事で例示するのは以下の2つです。

  • チーム内でモデルと命名の話題が増えた
  • 「境界づけられたコンテキスト」を基準に設計・実装に反映する

本記事は、以下の構成でお送りします。

ワークショップの目的

ワークショップの目的は、以下の2点です。

  1. コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める
  2. 技術コミュニティと新しい学びのループを作る

コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める

日々の業務では、個人個人が持つ情報差分や専門職種の特性などから作られるコミュニケーションのコンテキストに違いが生まれます。 その違いが大きいと、コミュニケーションのコストが高くなったり、思わぬ手戻りが発生することで、ボトルネックとなってしまうことがあります。 どの程度のボトルネックかは、個人個人の暗黙的な感覚に依存してしまうため、 まずはワークショップを通じた現状認識と課題の発見を目的としました。

技術コミュニティと新しい学びのループを作る

コネヒトにはスマイル制度という「技術コミュニティになくてはならない開発組織をつくる」をコンセプトにしたアウトプット支援制度があります。 インプットとアウトプットのループを作ることで、コネヒトも技術コミュニティもWin-Winな関係を築いていく狙いがあります。

tech-vision.connehito.com

これまでは研修参加や書籍の購入など、個人で利用されるケースが多かったのですが、複数人で同じことを学び、発信する事例としてこの制度を利用しました。 本制度の多様な活用事例を生み出すことで、より技術コミュニティとWin-Winになること2つめの目的と設定しています。

講義の内容

当日の講義は、座学とワークショップを組み合わせた形式で、合計4時間の内容でした。参加者は事前に目的を共有し希望者を募りました。 ytakeさんに参加者と予定時間を相談の上、半日で収まる内容にチューニングしていただきました。

タイムライン

座学(120min)

  • 14:00 ~ 14:05: イベント概要おさらい
  • 14:05 ~ 14:15: 自己紹介/チェックインタイム
  • 14:15 ~ 15:00: ytakeさん自己紹介・座学
  • 15:00 ~ 15:10: 休憩
  • 15:10 ~ 16:00: 座学
  • 16:00 ~ 16:10: 休憩

イベントストーミング形式ワークショップ(120min)

  • 16:10 ~ 17:00: ワークショップ
  • 17:00 ~ 17:10: 休憩
  • 17:10 ~ 17:50: ワークショップ
  • 17:50 ~ 18:00: チェックアウト

参加者内訳

  • エンジニア:12名
  • デザイナー:1名
  • PdM/PMM: 2名

なお、イベントストーミングは、Alberto Brandolini氏が考案した協働的にドメインモデルを発見していく手法です。 EventStormingのサイトを参照すると以下のような記載があります。

The adaptive nature of EventStorming allows sophisticated cross-discipline conversation between stakeholders with different backgrounds, delivering a new type of collaboration beyond silo and specialisation boundaries.

翻訳:EventStorming の適応的な性質により、異なる背景を持つ関係者間で専門分野を超えた洗練された会話が可能になり、サイロや専門分野の境界を超えた新しいタイプのコラボレーションが実現します。`

今回はこのようなコラボレーションを実現するために、エンジニアとPdM/PMM、デザイナーの3つの職種のメンバーに参加してもらい、この方式を採用しました。

当日の様子

座学

当日は以下の内容を中心にお話ししていただきました。

  • ドメインモデルとはなに?
  • 分析するための考え方
  • ユースケース

座学の間も適宜質問を受けていただき、理解を深めながらお聞きすることができました。 ここで教えていただいた「コンテキストの境界」が今後の変化に繋がっていきます。

わいわいと講義を受けています

イベントストーミング

今回はママリアプリで起こる出来事を題材にイベントストーミングを開催しました。 みんなでわいわいと、出来事や関連する要素を書き出していき、ytakeさんのファシリテーションを受けながら、認識の違いを発見していきました。

グルーピングしたものをもとに議論しています

付箋の色分けは以下の通りです。

  • 画面やUIが絡むようなもの: 青
  • システム的なもの: ピンク
  • サービスの仕様、重要な出来事: オレンジ

当日のサンプル

その後の変化

このようにモデリングを利用し、認識の違いを発見する方法を学んだことで起きたエピソードを2つ紹介します。

チーム内でモデルと命名の話題が増えた

ミーティングで話す用語は、どのように整理すると自然なのかというコミュニケーションが以前より増え、用語集を作る動きがチーム内で生まれています。 筆者の所属するチームが動画コンテンツの改修を担当しているため、まずは動画施策の用語の整理から始めています。

用語集の整理

職種を問わず共同作業ができるように、Notionのデータベースを利用しています。

「境界づけられたコンテキスト」を基準に設計・実装に反映する

コネヒトが管理する動画には、外部サイトへのリンクがあるものないものがあります。この違いをRestAPIリソースとして個別に分けるかの議論がなされました。

miroで整理した様子

ワークショップ開催前は、設計においてアクターが誰かという話題は出てこなかったのですが、ワークショップを通じて、「コンテキストの境界」が重要であることを学んだことで、 この議論では、アクターが同じため同一のリソースとして定義しておこうと判断することができるようになりました。

この他にも

  • チームの担当領域に絞りイベントストーミングを実施する
  • 特定施策のユースケースを書き出してみる

など少しずつモデリング実施する機会が増えてきています。 それらの事例も、今後の変化に繋がっていくと思うので、また別の機会に紹介したいと思います。

まとめ

今回のワークショップでは、まずはモデリングのエッセンスを学ぶことにフォーカスしておりました。 本来の目的であるリリースの機動力を高める目的の実現には、まだまだ課題が多くあり、モデリングの実践とコードへの設計・実装への反映を定期的に繰り返す必要があると感じています。

ytakeさんから「目の前のものに騙されないように本質はなにか分析しましょう!100回くらい分析を繰り返しましょう!」というアドバイスをいただきました。 今後はより日常的にモデリングを実践できるように、常にMiro*1を開いてミーティングに参加しようと思います。

目指せモデリング100回!

最後となりますが、ytakeさん講義の開催ありがとうございました! モデリングにお悩みの場合、最初の一歩として、ytakeさんに相談することをおすすめしたいと思います。

*1:コネヒトではオンラインホワイトボードツールとして利用しています。https://miro.com/ja/

レコメンドで使用する類似アイテムをAmazon Bedrockとitem2vecで計算・比較検証してみた

みなさんこんにちは。MLエンジニアのたかぱい(@takapy0210)です。

最近、久しぶりに機動戦士ガンダムSEEDを見直しました。(来年には劇場版の公開もあります)

地球連合軍第7機動艦隊に所属するパイロットであるムウさんの

「君は出来るだけの力を持っているだろう?なら、出来ることをやれよ」

というセリフが好きです。
相手をリスペクトしつつ、でもお前はもっとできるだろ?という期待も込もった、良い言葉だなと感じます。

さて本日は、レコメンドで使用頻度の高い類似アイテムの計算処理を2パターンで実施し、どんな差分がでるのか?を検証した結果をお話ししようと思います。

この記事はコネヒト Advent Calendarのカレンダー 10日目の記事です。

adventar.org


目次


背景

コネヒトの運営するコミュニティサービスママリでは、様々な部分でレコメンデーション機能が提供されています。 今後もレコメンデーションロジックの改善を継続的に行っていく中で、類似するアイテムをどのように計算するのか?は重要な課題の1つです。

類似アイテムを計算する1つのHowとして、昨今話題になっているLLMを使う方法が考えられます。
LLMを用いることで様々なEmbedding(ベクトル)を取得することができ、このベクトルを用いることでアイテム間の類似度を計算することができます。

本記事では以下の2パターンで算出したベクトルを用いて、類似アイテム(ここで言うアイテム=質問)を抽出し、比較・考察してみようと思います。

  • Amazon Bedrockの埋め込みモデルで取得したベクトル
  • item2vecで計算したベクトル

前半でそれぞれのベクトル取得方法を簡単に説明し、後半では実際にいくつかの質問を用いてどのような類似アイテムが算出できるのか?を見ていこうと思います。

Amazon Bedrockの埋め込みモデルでベクトルを取得する

Amazon Bedrock(以下、Bedrock)とは、テキスト生成AIをはじめとする基盤モデル (Foundation Model) を提供するAWSのサービスです。Bedrockで使用できる基盤モデルには、Amazon自身が開発提供するTitanやAnthropicのテキスト生成AIであるClaudeなどがあります。

aws.amazon.com

以下のようなコードで、Bedrockの埋め込みモデルを使ってテキストのベクトルを取得することができます。

import json
import boto3

bedrock_runtime_client = boto3.client('bedrock-runtime', region_name="ap-northeast-1")

def get_bedrock_embedding(input_str, bedrock_runtime_client):
    bedrock_body = {
        "inputText": input_str
    }
    body_bytes = json.dumps(bedrock_body).encode('utf-8')
    response = bedrock_runtime_client.invoke_model(
        accept="*/*",
        body=body_bytes,
        contentType="application/json",
        modelId="amazon.titan-embed-text-v1",
    )
    response_body = json.loads(response.get("body").read())

    embedding = response_body.get("embedding")
    
    return embedding

text = "帝王切開で出産、退院時に痛み止めもらった方、いつぐらいまで飲んでましたか?"
vec = get_bedrock_embedding(input_str=text, bedrock_runtime_client=bedrock_runtime_client)

今回、Bedrockで取得したEmbeddingはOpenSearchに格納し、その機能を利用して類似質問を抽出しています。 OpenSearchへの格納方法などは、以下のブログに書いていますので、興味のある方はこちらもご覧ください。

tech.connehito.com

item2vecで計算したベクトルを取得する

item2vecとは、自然言語処理におけるword2vecの概念をアイテム推薦などに適用したものです。
word2vecは単語をベクトルとして表現し、これらのベクトルを使って単語間の意味的な関係を捉えることができますが、item2vecでは、この考えを商品や映画、曲などの「アイテム」に適用することができます。*1

今回はママリの質問閲覧ログデータを用いてitem2vecの学習を行いました。学習にはgensimライブラリを用いています。

学習に使用したデータは以下のようなイメージです。

user_id question_id event_dt
100 19064176 2023-10-15
100 19073732 2023-10-16
100 19037730 2023-10-16
101 18892007 2023-10-15
101 18891679 2023-10-16
... ... ...

まずは、このデータをgensimのモデルに入力できるように、系列データに変更していきます。

series_df = pd.DataFrame(df.groupby(['user_id', 'event_dt'])['question_id'].apply(list)).reset_index()
series_df = series_df.rename(columns={'question_id': 'order_question'})

こうすることで、以下の様なDataFrameを取得することができます。

user_id event_dt order_question
100 2023-10-15 [19084888, 18932714, 18925535 …]
100 2023-10-16 [19060467, 19055834, 19047868 …]
101 2023-10-15 [19096491, 19011148, 19095622 …]
101 2023-10-15 [18921942, 18921942, 18921939 …]
... ... ...

最後にこのデータをgensimに渡し、item2vecモデルの学習を行います。

import multiprocessing
from gensim.models import Word2Vec

corpus = series_df['order_question'].values.tolist()
cpu_count = multiprocessing.cpu_count()

model = Word2Vec(
    corpus,
    vector_size=50,
    window=5,
    hs=1,
    min_count=1,
    sg=1,
    workers=cpu_count,
    seed=42
)

類似アイテムの抽出は以下の様に行うことができます。

for i in model.wv.most_similar(target_q_id, topn=3):
    print(f"類似質問ID:{i[0]}, 類似度:{round(i[1], 4)}")

比較検証結果

上記2種類のロジックで取得したベクトルを用いて、どのような類似質問が取得できるのか類似度TOP3を取得して比較します。(今回はコサイン類似度を用いています)
「入力クエリ欄」に記載した質問内容に興味のあるユーザーに対して、どんなものが推薦されるのか?という想定で検証してみます。(Bedrockの場合は入力クエリがテキスト、item2vecの場合入力クエリは質問IDになりますが、ここでは分かりやすいようにテキストで統一して記述します)

※以下で掲示している質問文は一部改変しております

パターン1:ディズニーランドの情報が知りたいユーザー

入力クエリ

ディズニー詳しい方や最近行った方回答お願いします! 10月中旬の平日にディズニーランドへ行くのですが、朝何時から開園並んで何時に入園できましたか? ハロウィンのパレードはプレミアアクセスを購入予定ですが、何時までに入れば買える可能性ありますか?

取得結果

Bedrockの類似アイテムTOP:3

①:10月ディズニーインパについて。 今平日でも入園待ちですごいことになっているみたいですね。10時とかだとスムーズに入園できそうですが、その頃だともうプライオリティパスは取れないですかね?取れても夜の時間とかでしょうか? ハニーハントかモンスターズインクを取る予定です!

②:10/3ディズニー、平日、ハロウィンの情報です!どなたかの参考になれば嬉しいです。6:30ランド到着で一般前から10列目くらいで入場できました。プライオリティパスは入場してすぐで10:10~11:10の回でした!ハモカラ1時間前に地蔵で前から3列目、うちはそのままスプブ待ちでチップ、デール停車位置で前から2列目とれました。

③:皆さんディズニー行く時何時に行きますか?シーもハロウィンだと混むんでしょうか…? 何かYouTubeみて旦那が、7時半には並んだ方がいい!とか言ってますが、子連れでその時間から並ぶって無理あるでしょと思うのですが…笑朝イチじゃないとショーやプライオリティパスもすぐなくなってしまいますか? 来週水曜にディズニーシーに行く予定です。。 全然詳しくないので教えて頂きたいです。

item2vecの類似アイテムTOP:3

①:皆さんの意見を聞かせてください! 何年振りかにディズニーランドに行きます!2歳と4歳の子供を連れていきます! 13日は平日なので多少は空いてる?!チケット料金が安いです。 14日だった場合は親が一緒にこれるので私と旦那の負担が少ない?休日なので混んでる&チケット料金が高いです。 皆さんだったらどちらに行きますか?? 1つ気になるのが13日がシーが早く閉まる?のでランドが混みそうと言う事です。。 ディズニー初心者過ぎて…皆さんの力を貸してください。

②:ここ何年もディズニーに行ってなく、最後に行ったのは10年近く前、、、 その頃は紙のファストパスの時代でしたが今はアプリでとる?んですよね? 5歳3歳1歳の子供連れで久しぶりのディズニーで不安すぎます。 ディズニーランドに行くのですが1日の流れどのようにするのがいいですかね? まったくの無知なのでこうした方がいいよなどあったら教えてほしいです!

③:ここ数日でディズニー行った方教えて下さい! 平日で混んでますか?乗り物の待ち時間どのくらいでしょうか?コロナ禍のガラガラ時以降行ってなくて。。

パターン2:帝王切開後の身体への影響が気になるユーザー

入力クエリ

帝王切開の方にお聞きしたいです。もうすぐ産後1ヶ月になります。骨盤ガタガタで尾てい骨も痛いし足あげるのも痛いし、ゆっくりしか歩けません。みなさんは骨盤ベルトしていましたか?

取得結果

Bedrockの類似アイテムTOP:3

①:臨月になりお尻と足の付け根痛いです。 頭がおりてきていたり、靭帯が緩んだりと原因はいろいろあるみたいですが、これは出産が近いのかな…?? とにかく痛みに対しては骨盤ベルトで抑えてます。 同じように股関節の痛みを感じた方、どの位の期間で出産になりましたか??

②:赤ちゃんがもう産まれても大丈夫な状態らしく、たくさん歩くように言われました。 昨日と今日、いつもより多めに歩いたのですが、先ほどから恥骨がいつもにまして割れるような痛みがきてしまいました。 その前から恥骨は痛かったのですが、歩くのも困難になるほどです。それに加えて、股関節も痛いです。 昨日の検診では赤ちゃん、まだ全然降りてきてないと言われましたが赤ちゃんがおりてきてるんでしょうか?

③:産後50日過ぎましたが、恥骨がまだ痛過ぎます。いつになったら解放されますか? 妊娠後期から恥骨激痛で歩くのやっとで、産んだら解放されるかと思いきや、全然痛い・・・ 骨盤矯正は、先週から行き始めましたが、産後も恥骨痛あった人、いつから無くなりましたか?

item2vecの類似アイテムTOP:3

①:出生後、しばらく哺乳瓶で、その後スムーズに直母授乳に移れますか? 25日の月曜日に帝王切開にて出産しました。 術後の私の回復具合、我が子の体調などなどいろんなことを考慮して、しばらくは哺乳瓶でミルクまたは搾乳した母乳をあたえていくことになり、その方法で1週間近く経ちました。 最初の3日間はミルクを使用、4日目以降搾乳した母乳のみ飲ませています。 同じように、最初は哺乳瓶でその後母乳に移られた方、どうでしたか??特に抵抗なく直母に移れるのもでしょうか?

②:帝王切開で出産、退院時に痛み止めもらった方いつぐらいまで飲んでましたか?

③:出産して入院中なんですが、特に足首から下のむくみがすごく、象の足みたいになってるんですがしばらくはこんな感じなんですかね?

パターン3: ドラム式洗濯機を買うか迷っているユーザー

入力クエリ

縦型洗濯機からドラム式洗濯機に乗り換えた方にお聞きしたいです! 電気代と水道代はどのくらい高くなりましたか? 大体で構わないので教えてください。

取得結果

Bedrockの類似アイテムTOP:3

①:現在、縦型の洗濯機を使っていますが、ドラム式に買い替えることにしました。 明日、電気屋さんに行くのですが、 全く無知なので、おすすめの洗濯機を教えてください! また金額も20万ぐらいかな…と思っているのですが、どのぐらいするのでしょうか?

②:みなさんは洗濯機って縦型使ってますか? それともドラム式洗濯乾燥機ですか? うちは縦型ですが、ドラム式ほしいです。でも高い...

③:洗濯機買って5年経つんですが、縦型で容量が8キロ。 子どもが2人になり洗濯物が増えて、週末は1日に2回まわすこともよくあります。 容量が少ないのがストレスで買い替えたい気持ちもあるのですが、まだ5年だしな〜と迷っています。 もし次買うならドラム式がいいですが高い。。。ドラム式を使われてる方は洗濯後乾燥までされてる方が多いんですかね?縦型、ドラムどちらがおすすめですかね??皆さんどちら使われてるかも教えて欲しいです☺️

item2vecの類似アイテムTOP:3

①:マイホームを購入した時にサービスでつけてもらったオプションや家具家電など参考にさせてもらいたいので教えて下さい

②:コンビニで2,000円の買い物をするのに、 ①楽天カードで支払い ②楽天ペイで支払い(クレカからチャージ) どちらがお得なんでしょうか?

③:年少1人、1歳1人のお子様が居るご家庭の月の食費を教えてください!また旦那さんのお昼ご飯代も込みかどうかも教えて貰えるとありがたいです

パターン4:離乳食の2回食をいつから始めるか気になっているユーザー

入力クエリ

生後5ヶ月半から離乳食を始めました。 2回食にするのは離乳食が始まって2ヶ月後である7ヶ月半くらいかな?と思っていたんですが、6ヶ月の時点で2回食検討中、またはされている話も聞いたりします。 みなさん2回食はいつから始められましたか?

取得結果

Bedrockの類似アイテムTOP:3

①:離乳食についてです。 みなさんいつから2回食に移行しましたか? 5ヶ月から始めてもう1ヶ月と1週間が過ぎました。調べると2回食を始めるのはだいたい7ヶ月からか、始めて1ヶ月経ったらと見ます。 もちろん子どもの様子によって進めていくものだとは思いますが、2回食にするのがすごく気が乗らなくて笑

②:6ヶ月から離乳食を始めた方、いつから2回食にしましたか?5ヶ月で始める時より早めに2回食にした方がいいとかあるんでしょうか、、進め方がよく分からず困っています。

③:5ヶ月から離乳食始めました。 今7週目で(6ヶ月の2週目)いつから2回食を始めるか悩んでます。今日から始めようかな、、と。 みなさん2回目はいつ頃から、何時頃あげてましたか?

item2vecの類似アイテムTOP:3

①:10ヶ月検診で肥満気味と言われました。 男の子で身長73センチの11キロです。離乳食を始めたのが6ヶ月後半からで遅く、量もあまり食べなかったのですが、1ヶ月くらい前から本格的に3回食にして量は小鉢2つ程度です。量は測ったことなかったのですが、 今回測ってみたら25ml+30ml+50mlの冷凍パックしたものを解凍し味噌汁に野菜をプラスしたおかゆとじゃがいもと野菜を潰してコンソメで味付けのポテトサラダで、105g?食べているのかなと思いました。ただ、毎回このくらい食べる時もあれば半分食べたかなくらいで残す時もあります。 本やネットには離乳食の間隔は4時間あけて18時以降は食べさせないと書いてあり、先生には1度に量を食べれなければ間食としてご飯をあげてくださいと言われましたが、その場合はあける時間間隔は無視してもいいのでしょうか。 正直、間食をあげるタイミングもわからないです。 10ヶ月のお子さんがいらっしゃる方は、毎食どのくらい食べているのでしょうか?

②:まだ自分でコップを持って飲めない赤ちゃん、コップ飲みの練習ってどうやりましたか? 現在生後6ヶ月で、生後5ヶ月の頃から離乳食デビューに合わせて麦茶でのコップ飲みを始めました。 ダイソーで売っているトレーニングコップを使用していて、取っ手の部分を握ったりはしますがそのまま自分の口に持っていくのはまだできません。なので私がコップを持って飲ませていますが、麦茶がたくさん口に入るのかむせてしまったりしてイマイチ上手くあげられません。 自分で持って飲んでむせたり溢したりしてだんだんコップに慣れるイメージなのですが、親が飲ませていて練習になるんでしょうか? 1ヶ月続けてもなんの進展もないので不安になってしまいました。 ぜひ教えてください。

③:離乳食をあげてる時間教えてください。 現在生後6ヶ月で5ヶ月の頃から離乳食あげてますがまだ一回食です。 離乳食はモリモリ食べすぎてるくらいなのですが、二回食にするのがめんどくさくて… でもそろそろ二回食を考えなきゃな〜と思ってるので、何時に離乳食をあげているのかみなさんの離乳食スケジュール教えてください。 三回食をもう始めている場合は今後のために三回食の時間も教えて欲しいです。 うちは今10時に離乳食をあげています。 離乳食とミルクはバラバラです。

パターン5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー

入力クエリ

オムツのサイズアップについてです。 生後1ヶ月で、まだ新生児サイズを使っているのですが、太ももにかなり跡がついています。 ですが、お腹周りはゆるゆるです。 サイズアップした方がいいのかな?と思い、試しに試供品のSサイズを使ってみたら、太ももはぴったりですがお腹の方はオムツがかなり上まで来てゆるゆるです。 このような場合でもサイズアップしたほうがいいのでしょうか?

取得結果

Bedrockの類似アイテムTOP:3

①:オムツの新生児サイズからSサイズへのサイズアップっていつ頃でしたか? また、どんな感じになったらサイズアップした方が良いのか教えて下さい。

②:オムツのサイズについて質問させてください! 生後2ヶ月の男の子がいます。 私の母乳外来受診のため、授乳の様子も見てもらえるということで息子も一緒に受診したのですが、付けているオムツが小さいからサイズアップしたほうがいいと言われました。 体重は約5800gでテープのSサイズを使ってます。 最近ようやく太もも周りがちょうどよくなってきたかなぁと自分的には思ってた矢先助産師さんに小さいと言われたのですが、やはりMサイズへ変えるべきでしょうか?

③:オムツのサイズ別の使用量について。 2400gで生まれ、1ヶ月半新生児用を使用しメリーズファーストプレミアムを5袋使いました。 1ヶ月半となり、4000gになったのでオムツをSサイズにしました! 皆さんはSサイズ、Mサイズ、Lサイズをそれぞれ何ヶ月頃から使用し、何袋使いましたか?? もちろん、赤ちゃんの体型それぞれなのは承知で参考までに。 また、何ヶ月の何キロからSサイズorMサイズからテープオムツをパンツオムツにしましたか??

item2vecの類似アイテムTOP:3

①:生後1ヶ月と半月です。 夜間の授乳が5~6時間くらい初めて空いたのですが、こうやって徐々に夜の時間を延ばしていってもいいんですかね? 最近、日中はずっと起きています。。。早めに昼夜の感覚をつけたいと思ってます!

②:生後1ヶ月 4キロです。 アマゾンセールでおむつを買いたいです。 今使っているものの次のサイズをストックしておくつもりです。 6キロからのMサイズを購入しますがテープタイプとパンツタイプだとどちらが良いのでしょうか?

③:現在生後1ヶ月、まもなく2ヶ月になる息子の育児について相談させてください。ご機嫌に起きている時で、あやすのも授乳も終えている時は皆さんどうされてますか? ご機嫌で1人でおしゃべりしてる時はそのままベッドやハイローチェアで眠るのを待っていても良いのでしょうか?

考察

それぞれのパターンについて簡単にまとめてみます。

パターン Bedrock item2vec
1:ディズニーランドの情報が知りたいユーザー 「プレミアアクセス」や「時間状況」などの単語表現をうまく拾い、類似したアイテムが推薦されている ディズニーランド全般に関する幅広い情報が推薦されている
2:帝王切開後の身体への影響が気になるユーザー 骨盤や股関節の痛みといった、身体への影響に関連するアイテムが推薦されている どちらかと言えば帝王切開に関連したアイテムが推薦されている
3:ドラム式洗濯機を買うか迷っているユーザー 洗濯機を選ぶ基準やおすすめの洗濯機に関連するアイテムが推薦されており、購入を検討しているユーザーには有用そう 家庭生活全般(主にお金関連)に関連するアイテムが推薦されている
4:離乳食の2回食をいつから始めるか気になっているユーザー 離乳食の2回食移行に関連したアイテムが推薦されている 赤ちゃんの健康管理や飲み物の練習など、子育ての幅広い側面をカバーしたアイテムが推薦されている
5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー オムツのサイズに重点を置いたアイテムが推薦されている パターン4の時と同様に子育ての幅広い側面をカバーしたアイテムが推薦されている

Bedrockによる推薦は、細かいテキスト内容部分も考慮された類似アイテムが計算できていそうです。(テキストをベクトル化して類似度を計算しているので当たり前と言えば当たり前ですが)
一方でitem2vecによる推薦は、ドンピシャな推薦というよりは、同一トピック内ではあるものの、もう少し幅広いアイテムが計算されていそうです。

おわりに

本記事では、Bedrockとitem2vecを用いて類似アイテムの計算と比較検証を行ってみました。

レコメンデーションには大きく分けて「探索 (Exploration)」と「活用 (Exploitation)」があると言われています。

探索とは、何らかのアルゴリズムによってユーザの嗜好に最も合うであろうアイテムが決定される中で、あえて嗜好に最も合うもの"ではない"アイテムを推薦することを言います。 探索の主な目的は新しい知見を得ることです。これにより、ユーザーの好みや興味の変化に適応することができます。
逆に、現時点で最もユーザの嗜好に合うと計算されたアイテムをそのまま推薦することを活用と言います。
一般的にこの2つはトレードオフの関係になっています。

今回の考察結果から、item2vecの推薦はユーザーの興味トピックとしては近いが、そのトピックの中から幅広いアイテムを抽出しているため、どちらかというと探索向けの推薦として利用できそうだなと感じました。
一方でBedrockによる推薦は、興味を持った具体的なアイテムと似ているものをピンポイントで推薦するため、活用向けの推薦に利用できそうだなと感じました。

今後も推薦の目的や状況に合わせたレコメンデーションロジックの検証・改善を繰り返し、より良いユーザー体験を届けていきたいと思います。