コネヒト開発者ブログ

コネヒト開発者ブログ

GitHub Actions & ecspresso を用いたデプロイフローの改善

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

本日はコネヒト株式会社で取り組んでいる Super X という活動の一環である「デプロイフロー改善」の取り組みを紹介しようと思います。

本記事で紹介した改善以外にも、デプロイフロー改善チームで取り組んだことは別エントリでお届けする予定です✨


目次


はじめに

そもそも Super X って何?と思われるかと思うので、取り組みのご紹介をしようと思います!

Super X はエンジニア組織として追っている目標のプロジェクト名で、開発組織の中で課題となってる負債の解消などに取り組んでいく活動です!

今回私は 「デプロイフロー改善」を上期のテーマとして選び、活動を行ってまいりました。

抱えていたデプロイフローでの課題

※以下、「開発環境」は一般的に言うステージング環境を指しています。

弊社では過去に ecs-deploy を用いたデプロイを ecspresso に変更する取り組みがありました。(ref. GitHub Actions & ecspressoによるデプロイフロー構築

上記取り組みで ecspresso 導入がされたものの、以下の課題が残っていました。

  1. 開発環境と本番環境で別々の Docker イメージが利用されている
  2. ロールバックの方法が確立してない

1.については開発環境で検証した Docker イメージを本番環境でも利用した方が安心であるのと、本番環境デプロイ時に再ビルドが発生するので余計な時間がかかっているのを解消したいと思っていました。

2.については、ecspresso を使えば簡単にロールバックできるものの、その運用方法が社内で確立していなかったので、この際に他リポジトリに展開できるまで運用方法を確立しようという話になりました。

各課題の解決方法

開発環境で検証した Docker イメージを本番環境のデプロイで利用する

前提として、ecspresso を用いたデプロイフローはざっくり下記のようになっております。

1. 最新コードのチェックアウト
2. イメージをビルドし、ECR へプッシュ
3. ecspresso を利用して ECS タスク定義の更新・デプロイ

今回の改善では開発環境で上記のデプロイフローを行い、本番環境では下記のデプロイフローを行うことを目指します。

1. 最新コードのチェックアウト
2. ecspresso を利用して、開発環境で作成したイメージを参照するように ECS タスク定義の更新・デプロイ

このとき Docker イメージにつけるタグ名としては以下を満たす必要があると考えました。

  • 開発環境デプロイ時に利用できるものであること
  • タグ名が :latest などに固定されないこと(ecspresso のロールバックを考慮する)

上記2点を考慮すると、コミットハッシュをタグにするのがいいのではないか、という話になりました。

コミットハッシュならば、Github Actions 内で github.sha で取得ができます。 取得したコミットハッシュをイメージのタグ名として、イメージのビルド・ECR へのプッシュを行い、ecspresso でそのイメージを利用する形にすれば、開発環境で検証したイメージを本番環境デプロイ時に流用できる設計となりました。

デプロイフローのイメージ図

これにより、本番デプロイの actions は以下のようになり、だいぶシンプルになりました(一部抜粋)。

on:
  push:
    tags:
      - '*'

env:
  AWS_ROLE_ARN: hogehoge

permissions:
  id-token: write
  contents: read
  actions: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # 通知など前処理
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: aws-region
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - uses: kayac/ecspresso@v1
        with:
          version: latest
      - name: Deploy to Amazon ECS
        env:
          IMAGE_TAG: ${{ github.sha }}
        run: |
          ecspresso deploy --config .ecspresso/production/config.yaml
      # 後続処理

ロールバック方法の確立

デプロイが完了し、タスクは正常に起動したもののアプリケーションの動作に問題が発生したので戻したい場合には、ecspresso rollback コマンドを使用します。

ドキュメントによると、ロールバックをすると下記動作が走ります。

  1. 現在サービスに設定されているタスク定義の「ひとつ前」のリビジョンを見つける
  2. ひとつ前のリビジョンのタスク定義をサービスに設定する

ref. ecspresso advent calendar 2020 day 6 - rollback

なので理論上、有効なリビジョンのタスク定義がある場合は無限にロールバック可能です。

このとき、--deregister-task-definition オプションをつけるとロールバックを行ったタスク定義を登録解除 します。 つまり、1回異常が起きたタスク定義はもう使わない、という思想のようで、ecspresso 作者の方も特別な事情がない場合はこのオプションをつけることをおすすめしています。

このオプションをつけることで、無限に戻れはするが、戻った際に過去ロールバックをした定義は使われない状態が作れます。 なのでロールバック時はオプションを指定したコマンドを実行するように修正しました。

ecspresso rollback --deregister-task-definition --config .ecspresso/production/config.yaml

ちなみに ecspresso Roadmap to v2 を見ると、このオプションがデフォルトで true になるようなので、いずれオプション指定しなくても良くなりそうですね🙌

その他工夫した点

デプロイジョブの並列化

場合によっては複数の ECS 環境にデプロイを行いたい場合があるかもしれません。その際にジョブを直列で書くと、1つの環境でデプロイが終わらないと次のデプロイが始まりません。

そんな時はジョブを並列化するとデプロイ時間が短縮されます。

deploy_1:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ
deploy_2:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ
deploy_3:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ

上記のように needs: build でビルド作業が終わったら各ジョブが走るようにします。

デプロイ並列化

actions の詳細を見ても上図のように並列実行できていることがわかります。

並列化したジョブの結果を取得する workflow-conclusion-action の利用

デプロイをする actions では最後にデプロイ結果を通知するジョブを用意しています。デプロイのジョブを並列化した場合、どれか1つでもジョブが失敗したら通知としては失敗と通知してほしい気持ちになります。

通常ジョブの成功/失敗は job.status で取得できますが、これは1つのジョブの成否になるので、ジョブが分かれた場合、前段のジョブが成功したのか、失敗したのかはわかりません。

そんな時は workflow-conclusion-action が便利です。

notify_end:
  needs: [ deploy_1, deploy_2, deploy_3 ]
  runs-on: ubuntu-latest
  if: always()
  steps:
    - uses: technote-space/workflow-conclusion-action@v2
    - name: Notify slack of deployment result
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ env.WORKFLOW_CONCLUSION }}
        fields: repo,commit,ref,workflow,message
        author_name: ${{ github.actor }}
        text: |
          Deployment has ${{
            (env.WORKFLOW_CONCLUSION == 'success' && 'succeeded') ||
            (env.WORKFLOW_CONCLUSION == 'failure' && 'failed') ||
            'cancelled'
          }} to hogehoge.
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

このように各デプロイ全て終わった後に実行し、結果を env.WORKFLOW_CONCLUSION で取得すると、どれか一つでもデプロイが失敗した場合は失敗の通知をしてくれます。

まとめ

今回は ecspresso を用いたデプロイの更なる改善方法をご紹介しました。

仕組みが整っていると、普段あまり意識しなくても流れに乗ってしまえばできてしまうデプロイですが、これを機に CI 周りの理解だけでなく、ECR、ECS などインフラ側の理解も進んだので、非常に良い機会でした。

今回ご紹介した方法が、日頃の開発の何か参考になれば幸いです。

PR

コネヒト株式会社では絶賛エンジニア募集しております!

今回のような活動に興味を持った方、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

参考

ecspresso handbook

オンボーディング改善に機械学習を活用する〜トピックモデルによる興味選択編〜

みなさんこんにちは。MLチームのたかぱい(@takapy0210)です。

ここ1年くらいPokémon UNITE というゲームにハマっていまして、何回か大会にも出場しているのですが、先日出場した大会の「おじさんの部 26歳以上の部」で準優勝することができました🎉
若い頃の部活に近い感覚で、チームメンバーで勝利の喜びを噛み締めたり、負けた悔しさを共有したりなど、生活に刺激を与えてくれる存在になっています。

さて本日は、コネヒトの運営するママリのオンボーディング改善に機械学習を活用した事例をお話をしようと思います。

今回実施したオンボーディング改善には大きく分けて以下2つのステップがあります。

ステップ1:興味選択にどのようなトピックを掲示したら良いか?
 → 後述するTwitterの例でいうところの「Pokémon」や「Business news」など

ステップ2:選択したトピックに関連するアイテムをどのように計算(推薦)するか?
 → 「Pokémon」を選んだユーザーに対して、どんなアイテムを推薦するのか

本エントリでは主にステップ1の内容についてお話しできればと思います。(ステップ2に関しては別エントリでお届けする予定です!)


目次


はじめに

世の中にリリースされているサービスの多くは、登録時に何かしらのオンボーディングがあると思います。

例えばTwitterのオンボーディングでは、以下のように「Twitterで見たいものは何か?」を選ぶフローがあり、ここでユーザーごとに興味関心のあるトピックを選択することで、パーソナライズを実現しようとしています。

このように、サービスを使い始めたばかりのユーザーに対しては、順当にユーザーの興味関心に適合したアイテムを推薦することで、サービスへの信頼性を高める戦略をとることは非常に重要だと考えています。

そこでママリにおいても、オンボーディング時にユーザーの興味関心を教えてもらうことで、新規ユーザーのUX向上に貢献できるのではないか、という仮説のもと、今回のプロジェクトが始まりました。(以降、このプロジェクトのことを”興味選択”と呼びます)

オンボーディング改善に取り組んだ背景

これまでのママリでは、新規ユーザーに対してはお子さんの年齢や妊娠週数別にルールベースでアイテムの推薦を行っていました。

これにはいくつか課題がありますが、中でもユーザーの興味関心を拾えていない部分が大きいと考えていました。

例えば、妊娠初期のユーザーでも、以下のように興味関心は1人1人異なります。

  • 妊娠中、母体に訪れる症状(例:つわりなど)に関心のあるユーザー
  • 仕事関連(例:産休など)に関心のあるユーザー
  • お金関連(例:出産にかかる費用や保険など)に関心のあるユーザー etc …

上記のような興味関心は今までのルールベースの推薦では考慮できておらず、同じ属性(妊娠初期など)の新規ユーザーには一様なアイテムが推薦されている状態でした。

このような課題を解消するために、オンボーディングに興味選択を組み込むことになりました。

なぜ機械学習を使う必要があったのか

例えば、妊娠初期のユーザーがどのようなアイテムに興味関心があるのか?を知る方法はいくつかあると思います。

一番手軽に知る方法、検索のログを分析することでしょうか。
検索のログを妊娠初期ユーザーでフィルタリングし、検索頻度の高い単語 = 妊娠初期に気になるトピック、と仮定することができます。

しかし、検索のログで拾えるものは一般的に、顕在化されている関心であることが多く、潜在的な関心を拾うのは難しいと考えられます。

そこで、検索だけでなくアイテムのクリックログから、それぞれのクラスタごとによく見られているアイテムを抽出し、潜在的なニーズを探るためにトピックモデルを活用しました。

顕在的なニーズは検索ログベースで抽出し、潜在的なニーズは機械学習を使って抽出することで、興味選択で表示するトピックに網羅性を持たせ、さまざまな悩みを持つユーザーに寄り添ったオンボーディングを目指しました。

分析手順

今回はユーザーを複数のクラスタ(妊娠初期や妊娠中期、生後Nヶ月、など)に分割し、それぞれのクラスタが閲覧しているアイテム(=質問)毎にトピックモデルを活用し、各クラスタでどのようなトピックが現れるのかを分析しました。

トピックモデルとは

文書が複数の潜在的なトピックから確率的に生成されると仮定したモデルです。

ここでいう「トピック」とは話の主題のことで、同じ話題について話していても、人によって解釈が変わることもあります。
特徴として、トピックモデルの「トピックの数」と「出力されたトピックのラベル」は人間が決める必要があるということが挙げられます。

例えば、トピックの数を3と指定してモデリングした場合、そのモデルに何かしらの文書を渡すと、出力として3つのトピックそれぞれの確率分布が得られます。
また、トピックモデルの出力が以下のようになった場合、Topic1はスポーツ、Topic2は経済、Topic3はエンタメといった解釈は人間が行う必要もあります。

こうした「トピックへの意味づけ」や「最適なトピックの数」に関しても、併せて紹介していこうと思います。

トピックモデルのイメージ図

今回はトピックモデルの代表とも言えるLDAを、gensimというライブラリを用いて実装しました。

トピックモデルの概要についてはAlbertさんの記事を見ていただくとよりイメージがしやすいと思います。

www.albert2005.co.jp

gensimを用いたトピックモデリング

モデリングする際は日本語であればトークナイズ済みのデータが必要になります。(以下のようなイメージです)

LDAの学習に使用するデータイメージ

データが用意できれば、LDAの学習は以下のように実装できます

import gensim
import multiprocessing

texts = df['content_token']
dic = gensim.corpora.Dictionary(texts)
bow_corpus = [dic.doc2bow(doc) for doc in texts]

lda_model = gensim.models.LdaMulticore(
    bow_corpus,
    num_topics=18,
    id2word=dic,
    workers=multiprocessing.cpu_count(),
    passes=10,
    random_state=0
)

前述したように学習後のトピックに対する意味(ラベル)づけは人間が行う必要があります。
その際は以下のようにすることで、トピックに紐づく単語を取得することが可能です。

num_words = 30
topic_list = []
word_list = []
weight_list = []

for n, values in lda_model.show_topics(num_topics=18, num_words=num_words, formatted=False): 
    for word, weight in values: 
        topic_list.append(n)
        word_list.append(word)
        weight_list.append(round(float(weight) * 100, 2))
        
topic_df = pd.DataFrame()
topic_df['topic'] = topic_list
topic_df['word'] = word_list
topic_df['weight'] = weight_list

このようなDataFrameを取得することができ、トピックの理解に役立ちます。

取得できるDataFrame

可視化してみると、より分かりやすくなると思います。

import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

fig, axes = plt.subplots(2, 5, figsize=(36, 20))
topic = 0
for ax in axes.ravel()[0:]:
    sns.barplot(x="weight", y="word", data=topic_df.query('topic==@topic'), color='#da8886', ax=ax)
    ax.set_title(f'Topic: {topic}')
    topic += 1
plt.show()

トピックに紐づく単語(一部)

最適なトピック数を探索する

前述したように、トピックの数は人間が指定しなければなりません。

トピックモデルのトピック数は、一般的にCoherenceとPerplexityの値から最適なトピック数に当たりをつけることができます。

  • Coherence :トピックが人間にとって分かりやすいかといった品質を表す指標。高い方が良いとされている。
  • Perplexity:モデルの予測性能を表す指標。低い方が良いとされている。

上の2つの値は、以下のようにして探索することができます。

# 探索
start = 10
limit = 50
step = 1
coherence_vals = []
perplexity_vals = []
for n_topic in tqdm(range(start, limit, step)):
    lda_model = gensim.models.LdaMulticore(bow_corpus,
                                           num_topics=n_topic,
                                           id2word=dic,
                                           workers=multiprocessing.cpu_count(),
                                           passes=10,
                                           random_state=0)
    perplexity_vals.append(np.exp2(-lda_model.log_perplexity(bow_corpus)))
    coherence_model_lda = gensim.models.CoherenceModel(model=lda_model, texts=texts, dictionary=dic, coherence='c_v')
    coherence_vals.append(coherence_model_lda.get_coherence())

# グラフの描画
fig, ax1 = plt.subplots(figsize=(15,8))
# coherence & perplexity
x = range(start, limit, step)
ax1.plot(x, coherence_vals, 'o-', color=c1)
ax1.set_xlabel('Num Topics')
ax1.set_ylabel('Coherence', color='darkturquoise'); ax1.tick_params('y', colors='darkturquoise')
ax2 = ax1.twinx()
ax2.plot(x, perplexity_vals, 'o-', color=c2)
ax2.set_ylabel('Perplexity', color='slategray'); ax2.tick_params('y', colors='slategray')
# 表示
ax1.set_xticks(x)
fig.tight_layout()
plt.show()

coherenceとperplexityのプロット

上記の例だと26あたりのトピック数が良さそうだ、という判断をすることができます。

プロダクトへの活用

上記で分析した結果などを参考に、PdMと議論しながら最終的にユーザーに表示するトピックを選定していきました。

2022年09月時点では以下のようなトピックがオンボーディングで表示されています(※ユーザーの属性情報によって表示されるトピックは異なります)

オンボーディング時のスクリーンショット

最後に

今回はユーザーをクラスタ別で分析したときに、どのようなコンテンツの興味があるのか?を分析することで、オンボーディングを改善した事例をご紹介しました。

冒頭で述べた通り、続編として「選択したトピックに関連するアイテムをどのように計算(推薦)するか?」の内容についても、後日紹介できればと思います。

オンボーディング改善の内容は、PyCon 2022でも詳細をお話する予定なので、興味がある方は是非観にきてください! (登壇日時は10月14日(金)の17時10分〜17時40分に決まりました!)

2022.pycon.jp

We Are Hiring !!

コネヒトでは一緒に働く仲間を募集しています!

www.wantedly.com

機械学習に関しては、過去の取り組み事例などを以下にまとめていますので、是非見てみてください!

tech.connehito.com

そして興味持っていただけた方はカジュアルにお話しましょう! (TwitterのDMでもMeety経由でも、気軽にご連絡ください)

参考文献

コネヒトはiOSDC Japan 2022に協賛いたします!

こんにちは、iOSアプリエンジニアのyanamura(@yanamura_)です!

本日は、iOSアプリ開発者の祭典iOSDC Japan 2022に協賛するお知らせです。

コネヒトはiOSDC Japan 2022に協賛いたします!

iOSDC Japan 2022に、シルバースポンサーとして協賛いたします。

iosdc.jp

スポンサーするにあたって、コネヒトは「人の生活になくてはならないものをつくる」というミッションを掲げているので、技術コミュニティについても同様に、サポートして一緒に盛り上げていくことができたら、と思っております。

イベント概要

  • 開催 2022年9月10日(土)~9月12日(月)
  • 場所 早稲田大学 理工学部西早稲田キャンパス63号館・オンライン(ニコニコ生放送)
  • 対象 iOS関連技術およびすべてのソフトウェア技術者
  • タイムテーブル https://fortee.jp/iosdc-japan-2022/timetable
  • 主催 iOSDC Japan 2022 実行委員会 (実行委員長 長谷川智希) / WASEDA-EDGE人材育成プログラム
  • 共催 GTIE (Greater Tokyo Innovation Ecosystem), 早稲田大学 総合研究機構 グローバル科学知融合研究所

iOSDCトークンはこちら!

#connehito-techvision

これはiOSDCチャレンジで使うトークンです!

今年はオンラインだけではなく、オフラインでも開催される予定です。久しぶりのオフライン開催なので私も会場に足を運びたいと思っています!会場でお会いした際にはぜひお話しましょう!

リモートワークでもIP制限があるツールを利用するためにSquidで簡易Proxyを用意した話

こんにちは。
コネヒトのテクノロジー推進部でインフラエンジニアをしている @laugh_k です。

先日リモートワークにおいても、共通の固定IPからのアクセスを実現するために Squidで簡易的なProxyを用意したので、そのときの話をまとめます。

TL;DR

  • ビジネスサイドのあるチームで社外のツールを利用しようとしたが、IP制限が必要だった
  • IP制限のあるツールはリモートワーク前提の状況と相性が悪い
  • VPN サービスの契約も検討したが、人数も限られていたため、Squid を使った簡易的な Proxy サーバをたてて対応した
  • 割り切った利用方法であれば、今でもEC2インスタンスはサクッと環境構築できるので便利

きっかけ

元はビジネスチームより「IP制限がある外部サービスを利用したいので簡易的なVPNを用意できないか」とインフラチームに来た相談がきっかけです。

要件を確認していくと「会社として共通の固定IPから外部サービスにアクセスできるようにする」ということを満たせればよさそうでした。そのため、VPNという手段に囚われずに要件に応えた結果 Squid を用いた Proxy サーバを用意することになりました。

なぜSquidを使ったProxyをEC2で用意したか

Squid を使ったProxyの選定

社外から共通の固定IP経由でWebサービスにアクセスする方法は色々ありますが、今回は以下の理由で VPN よりも Proxy を用意したほうが良いだろうと判断しました。

  • 今回の件は全社員の話ではなく、ビジネスサイドの特定のチームメンバーが共通の固定IPを利用できればよかった
  • 使いたいツールの案件が終了すれば、固定IPの管理自体が不要になる
  • 要件の規模に対してVPNの用意は環境を構築する側にとっても、利用する側にとっても少々大げさだと感じた

Proxy の用意に当たっては Squid の利用を決めました。

www.squid-cache.org

選定理由としては、特段「Squid でなければいけない理由」はなく、単純に環境準備を担当した筆者自身が Squid を用いた Proxy の運用経験があったことによります。Proxy が実現可能であれば他のツール、ソフトウェアでも問題ないでしょう。

EC2 の選定

当初は Proxy を ECS on Fargate 上につくりサーバレスな状態にすることも考えました。しかし、今回は以下の理由でインスタンス1台とEIPの組み合わせだけでいけるEC2のパターンの方がお手軽感があったためEC2(OS: ubuntu 22.04 LTS)を選定しています。

  • 準備にかけられる時間がかなり限られていた
  • 必要な案件が終わればProxy自体不要になるので、最終的に環境ごと捨てられる見込みがあった

既にIaC化されているなど、社内に実績があるのであれば ECS on Fargate と NLB, NAT Gateway を組み合わせて環境をつくるものアリだと考えています。

構成

Proxy SwitchcyOmega のようなブラウザ拡張を入れてもらい、IP制限のあるツールの利用にのみProxyサーバを利用してもらう前提で以下のような構成にしました。

Proxyの簡単な構成イメージ

  • 専用のネットワーク環境(VPC, subnet, EIP など)を用意
  • EC2 インスタンスをEIPに関連付けて1台起動
  • 最低限の Ansible Playbook を書きつつ、Squid のインストールと設定をする
  • Proxy へのアクセス制限は Security Group に任せる形にして、Squid 側では設けない(詳細は後述)

アクセス制限

Proxy は関係者のみが使えるようにアクセス制限をかけています。

理想で言えばアクセス制限はなんらかの認証基盤を介したものにできるとよいのですが、今回の案件限定と割り切り、2022-08-29時点では利用メンバー環境のIPベースのものとしています。

またIPベースのアクセス制限も Security Group のみで行うようにし、Squid の設定では制限を設けていません。これはシンプルな設定にすることだけでなく、設定変更の頻度が多くなりそうな場合に自動化をやりやすくする狙いもあります。

Ansible Playbook

実際に使った Ansible Playbook も紹介します。( inventory は SSH ログイン情報のみなので省略)

ファイル構成は次の通りです。

.
|-- inventory.yaml
|-- main.yaml
`-- templates
    `-- etc/squid/conf.d/allows.conf.j2

main.yaml は内容は次の通りで、標準の方法で squid を install して最小限の設定を施すものとしています。

---
- hosts: all
  become: true
  tasks:
    - name: install squid
      apt:
        name: squid
        state: present

    - name: squid configuration
      template:
        src: etc/squid/conf.d/allows.conf.j2
        dest: /etc/squid/conf.d/allows.conf
        owner: root
        group: root
        mode: 0644
        validate: /usr/sbin/squid -k parse -f %s
      notify:
        - restart squid

  handlers:
    - name: restart squid
      service:
        name: squid
        state: restarted

templates/etc/squid/conf.d/allows.conf.j2 は次の通りです。

acl accesslist src all
http_access allow accesslist

# proxy からのアクセスを隠ぺいする
forwarded_for off
header_access X-Forwarded-For deny all
header_access Via deny all
header_access Cache-Control deny all

# キャッシュをoff
no_cache deny all

前述のとおり、アクセス制限は Security Group で行うため、Squid 側には設けていません。

また、内容を見るとわかりますが、結果的には templates ではなく files でよいです。templates となっているのは「後から必要になった場合もすぐに対応できるように」と選んで書き始めたことによります。

適用する際は SSH ログイン情報をそろえて次の通りに実行すればよいです。

$ ansible-playbook -i inventory.yaml main.yaml

その他の準備

Proxy そのももの用意以外に準備したことも紹介します。

使い方のアナウンス

原則としてブラウザ拡張は Proxy SwitchcyOmega を利用してもらう前提とし、利用方法を Notion にまとめました。

実際に共有した利用方法ページの一部(1)

その際に、Proxy SwitchOmegaの設定は個別にやってもらうのではなく、あらかじめインポート可能なファイルにしてインポートして使ってもらうようにしています。設定ファイルは Google Drive で共有します。

実際に共有した利用方法ページの一部(2)

この対応により、Proxy利用メンバーにはIP制限のあるサービスでのみProxyを経由したアクセスができる環境をつくりました。

利用メンバーの自宅 IP のヒアリングとアクセス許可

前述したとおりProxyにはアクセス制限をかけているので、利用メンバーの許可設定が必要です。

やり方は至ってシンプルで、利用メンバー一人一人に自分のインターネット環境のIPアドレスを確認してもらい、共有してもらいます。そのIPアドレスからのアクセスを Proxy 環境で利用している Security Group で許可をするだけです。

このアクセス制限のやり方だと結局利用メンバーのIPアドレスの管理は発生してしまいますが、社内のみで完結できます。社内での手間は発生しますが、外部サービスに対して個別に問い合わせをするよりはスムーズに管理ができています。

一方で、固定回線ではなくモバイルWifi環境から業務をするメンバーもいるため、それなりに自宅ネットワークのIPアドレス変更が起こるケースが発生しています。このあたりの問題に対しては今のところ人力で対応しているものの、頻度によっては Security Group の許可設定を変更可能にする Slackbot の用意などの手段も検討しています。

用意してみた雑感

リモートワーク状態で、IPアドレス制限のあるサービスを共通のProxy経由で利用する方法を紹介しました。

今回紹介したProxyの用意方法は、正直なところ完璧なシステムというものではありません。サーバーレスな環境で運用負荷を減らしたり、認証基盤を用いたアクセス制限をするなど改善できる点は多くあると認識しています。

一方で、今回の要件「一時的に特定のメンバーがIP制限付きのサービスを利用したい」という問題に、限られた時間と労力で解決するにはちょうど良いボリューム感であったとも感じています。

リモートワークにおいても共通の固定IPが必要になるケースは発生しうると思います。その際、限定的な条件であれば今回紹介したような簡易的な Proxy を用意するだけでも十分に対処できることもあるので、よろしければご参考にしてみてください。

ニアリアルタイムで同期される検索基盤を構築 ~AWS Glueによるデータ同期編~

皆さん,こんにちは!MLエンジニアの柏木(@asteriam)です.

ここ最近は検索エンジン内製化プロジェクトに携わっていて,検索エンジニアとして,検索基盤の主にデータ連携・同期の実装を1から構築したりしていました.7月中旬にABテストまで持っていくことが出来たので,ひとまず安心しているところです.ここからはユーザーの検索体験向上のために検索品質の改善に力を入れていく予定です!

はじめに

今回新しく検索基盤をAWSのマネージドサービスを活用して構築しました!本エントリーでは,タイトルにもあるように,検索基盤の肝であるDBから検索エンジンへのデータ同期をAWS Glueを用いてニアリアルタイムで実施したお話になります.我々は以下の構成で今回の検索基盤を構築しています.

  • 検索エンジン:Amazon OpenSearch Service
  • データベース:Amazon Aurora
  • データ同期(ETL):AWS Glue
  • ワークフロー・パイプライン:AWS Step Functions

また,Step Functionsを使ったワークフロー・データ同期パイプラインに関する内容も別ブログで紹介したいと思います.全量データ同期に伴うreindexの仕組みや運用を意識したシンプルなワークフロー構成の紹介などをしようと思っています.

今回は以下の内容を紹介していこうと思います.

  • AWS Glueで実現するデータ同期とは
  • OpenSearchへのデータ連携でハマったところ
  • 今後の取り組み

※ 今回はGlueのDBやOpenSearchとの細かい設定方法や検索エンジン内製化プロジェクトの発足した背景などに関しては,触れないのでご了承下さい🙏


目次


AWS Glueで実現するデータ同期とは

ここからは実際にGlueを用いて検索エンジンへのデータ同期をどのようにして構築したかを紹介していきます.

全体アーキテクチャ

まずは今回の検索基盤の全体アーキテクチャを紹介しておこうと思います.

  • 太線矢印が「データの流れ
  • 点線矢印が「制御の流れ

検索基盤全体のアーキテクチャー概略図

中央にあるOpenSearch(検索エンジン)に対して,2つのパイプラインを流し込んでデータを同期しています.

  1. 全量データ用の同期パイプライン
    • 全量データの同期は,差分データ同期で取り逃がしたデータを拾ったり,バックアップ的な意味合いで定期的に実行しています.ただ1回の同期に時間がかかるので,頻繁には実施できません.
    • 特定のテーブルの全データを対象に総入れ替え(洗い替え)を行います.
  2. 差分データ用の用の同期パイプライン
    • 差分データの同期は,よりリアルタイムにアプリ上で生成されたデータを検索できるようにするために構築しています.
    • 直近x分以内に更新があったデータのみを対象に同期を行います.

全量データ用のパイプラインはさらに上段で実行スケジュールを管理するワークフローが存在しています.この辺りのワークフローやパイプラインの話は別ブログで紹介します.

AWS Glueによるデータ同期

Glueは様々なデータソースからターゲットソースに向けてサーバーレスでETLができるマネージドサービスになります.バックエンドでSparkが動いているので,高速なデータ処理が可能となっています.Glue Studioという新しいUIがあり,見やすく簡単に設定できてめっちゃ使いやすいです!また,使い勝手も悪くないと思います.

今回AWS Glueを選択した理由

AuroraからOpenSearchへのデータ同期の方法はGlueの他にも,AWS Database Migration Service (AWS DMS) を使う方法がありますが,今回の差分データ抽出や今後発生しうる複雑なデータ抽出が発生した場合に,柔軟に対応できるというところでGlueを選択しました.

前提としてマネージドサービスで運用したいという気持ちがあります.これは運用していく人数も限られている中で,データ同期を行うという本筋以外の周辺の管理運用に時間やコストを割けないというのもあります.

Glueはコネクターを使って簡単にOpenSearchへデータを同期できたり,組み込みの変換処理・フィルターが用意されていたりと便利な機能が色々とあるので,AWSでETL処理をするなら最初の選択肢としてありだと思います.

データ同期の流れ

さてさて今回は上記理由からGlueを選択することにし,データソースのDBであるAuroraからデータを抽出し,Glueを用いてターゲットソースの検索エンジンであるOpenSearchにデータを同期しています.

DB-Glue-検索エンジンの繋がり

Glueを使った流れは以下のようになります.

  1. データベースからデータをクロールする
  2. クロールしたらデータカタログにメタデータを管理する
  3. データカタログのメタデータを元にデータソースからデータを抽出する
  4. ジョブを実行してターゲットにデータを同期する

手動やスケジュールなどでGlue Jobを実行したタイミングで3, 4が実行される

  • Auroraへの接続
    • JDBC接続
    • インスタンス名・データベース名・ユーザー名・パスワードを設定
  • OpenSearchへの接続

こちらの公式ドキュメントにも設定の仕方が分かりやすく書いてあるので参考にしてみて下さい.

Glue Jobの設定

Glue Jobの設定はGlue Studioという新しいUIを使って簡単に設定することができます. 決めるのはデータソースとターゲットだけです.ここで事前に上に書いたコネクターを有効化しておく必要があります.

  • データソース:Data Catalog
  • ターゲットソース:Elasticsearch Connector

全量と差分データの同期フローのDAGは以下のようになり,差分処理が入るかどうかの違いになります.

全量と差分データの同期フローのDAG

Glue Jobには最初から用意されている組み込みのTransform処理があり,それを繋ぐだけで簡単に処理を追加することができます.また,カスタム処理も作ることができ,今回の差分処理はカスタム処理を使っています.

# 青枠の部分を抜粋しています.
from datetime import datetime, timedelta, timezone

from awsglue.dynamicframe import DynamicFrame, DynamicFrameCollection
from awsglue.transforms import DropFields, SelectFromCollection


# 現在時刻からX分前の時刻を取得
JST = timezone(timedelta(hours=+9), 'JST')
now = datetime.now(JST)
lastXmin = now + timedelta(minutes=-X)

# 差分データ抽出用のクエリ
query = f'''
select * from myDataSource
where modified > '{lastXmin}'
'''


def sparkSqlQuery(glueContext, query, mapping, transformation_ctx) -> DynamicFrame:
    """対象のクエリに対して,sparksqlを実行する"""
    for alias, frame in mapping.items():
        frame.toDF().createOrReplaceTempView(alias)
    result = spark.sql(query)

    return DynamicFrame.fromDF(result, glueContext, transformation_ctx)

# Script generated for node Difference Data Transform
def DiffDataTransform(glueContext, dfc) -> DynamicFrameCollection:
    """差分データを抽出し変換する"""
    # DynamicFrameCollectionをDynamicFrameにする
    dyf = dfc.select(list(dfc.keys())[0]).toDF()
    diff_data = DynamicFrame.fromDF(dyf, glueContext, "diff_data")

    return DynamicFrameCollection({"CustomTransform0": diff_data}, glueContext)

SQL_node3 = sparkSqlQuery(
    glueContext,
    query,
    mapping={"myDataSource": DropFields_node2},
    transformation_ctx="SQL_node3"
)

# Script generated for node Difference Data Transform
DifferenceDataTransform_node4 = DiffDataTransform(
    glueContext,
    DynamicFrameCollection(
        {"SQL_node3": SQL_node3}, glueContext
    )
)

# Script generated for node Select From Collection
SelectFromCollection_node5 = SelectFromCollection.apply(
    dfc=DifferenceDataTransform_node4,
    key=list(DifferenceDataTransform_node4.keys())[0],
    transformation_ctx="SelectFromCollection_node5",
)

ニアリアルタイムの同期のために

今回の要件として,差分同期はニアリアルタイムのデータ同期が求められハードルが高いものになっています.これを実現するために,GlueのJob bookmarkというオプション機能を使用しています.

Job bookmark機能とは,ジョブの実行状態を保持する機能で,処理済みデータを再度処理しないようにすることで,重複処理や重複データを防ぐことができます.これによりJobの実行時間が劇的に早くなります!ただ,ここはかなりハマったので次の章でも紹介します.

最終的には,Job bookmark機能とworker数(この辺はインスタンスタイプの調整も必要)を調整することでニアリアルタイムなデータ同期を可能にしています!

数分に1回動かすスケジュール実行は以下の2通り考えましたが,最終的に2番目の方法で実装することにしました.

  1. GlueのJob Schedulerを使う方式
    • メリット
      • オーバーヘッドが少なくGlueのみでデータ同期を完結できる
    • デメリット
      • 最低5分間隔でしか設定できない
        • X分間隔のジョブを複数作ることで回避できるが複雑で管理も大変
      • エラー通知がGlueの設定からはできない(CloudWatch Logsを使えばできそう)
  2. Step FunctionsとEventBridgeを使う方式
    • メリット
      • エラーハンドリングや通知設定がStep Functions内でできる
      • EventBridge経由のスケジューリングになり,柔軟な設定が可能
    • デメリット
      • 複数のサービス間でオーバーヘッドが生じる

OpenSearchへのデータ連携でハマったところ

検証中にハマった問題(初歩的なものもあります笑)を紹介したいと思います.

  • Too Many Requestsの問題
    • これは何も考えずdev環境でOpenSearchにデータを投げたら,発生したエラーです.この原因は単純に同期しようとしているデータ量が多い,またOpenSearch側のインスタンスタイプのスペック不足によるものだったので,dev環境の方もスペックを上げたところ解消されました.
  • Job bookmark機能が言うことを聞かない問題
    • 予想以上に処理時間がかかった
      • 1回のジョブ実行時はブックマークを作ることになるので,通常の実行となります.そのため,それなりに実行時間がかかるということを知らなかったので,効いていないと勘違いしました.差分の同期に関しては,dry-run的な形で一度動かしておく必要があることを学びました.
    • KeysとKeysSortOrderの設定
      • この問題は,データカタログからデータを抽出するところで設定するadditional_optionsのKeysSortOrderをdescにすると意図した通りに上手く動かないという問題です.OpenSearchへデータ同期するのに,重複データを排除するためにupsert形式で入れているのですが,upsertが効いていないことに気づいて発見しました.
      • これはタイムスタンプのように昇順で増加する場合,ascで指定する必要があったみたいです.直感的には,新しい順で並ぶdescにすると思ったのですが,それだと上手く機能しないことが分かりました.(これに全然気づかなくて苦労しました…😅)
        • 正:additional_options={"jobBookmarkKeys": ["id", "modified"], "jobBookmarkKeysSortOrder": "asc"}
          • Keysは複数取ることができ,id(レコードにおける一意のキー)とmodified(更新日)を指定しています.
        • この設定は,Glue Jobの最初のフェーズでデータカタログからデータを抽出する部分でオプションとして設定します(コード例を下に載せておきます).
# Script generated for node Data Catalog table
DataCatalogtable_node1 = glueContext.create_dynamic_frame.from_catalog(
    database="<データカタログに登録されているDB名>",
    table_name="<上記DBに登録されているテーブル名>",
    transformation_ctx="DataCatalogtable_node1",
    additional_options={"jobBookmarkKeys": ["id", "modified"], "jobBookmarkKeysSortOrder": "asc"}
)

Glue→OpenSearch間でエラーが発生した場合,なかなか原因を調査して特定するのが難しいなと感じました.CloudWatch Logsに出力するロギングオプションを有効化したりもしていますが,それでも大変です.今後の取り組みでも紹介していますが,Spark UIを立ち上げて詳細を追っていく必要がありそうで,この辺りはもう少し簡単にできるようになると嬉しいなと思います.

今後の取り組み

データ同期部分に関して,今後取り組んでいきたいことを紹介します.

  • Glueのジョブ実行時間を収集して見える化
    • Start-up timeやExecution timeを計算することで,処理時間が遅くなっているかどうかを知ることでサービスへの影響をプロアクティブに把握することができます
  • Glueジョブのモニタリング
  • データ基盤の安定した運用
    • 検索エンジンにとってデータが安定して同期されることが必要不可欠なため,どんな状態でも止まらず安定してデータ同期を続けられる仕組みとそれを計測する方法についても定義していきたいと思っています

今後複数のテーブルを同時に連携していく必要があったり,機械学習によるランキング学習など負荷が発生したり処理時間の問題など色々な課題が生まれてくることが考えられるので,そのためにも状態の把握や監視を進められると良いと思っています.

おわりに

最後に,コネヒトではプロダクトを成長させたいMLエンジニアを募集しています!!(切実に募集しています!)
もっと話を聞いてみたい方や,少しでも興味を持たれた方は,ぜひ一度カジュアルにお話させてもらえると嬉しいです.(僕宛@asteriamにTwitterDM経由でご連絡いただいてもOKです!)

www.wantedly.com

AWS Distributed Load Testingを使うと手軽にAWS内での負荷試験が出来るという話

こんにちは。インフラエンジニアの永井(shnagai)です。

今回は、現在進めているプロジェクトでの負荷試験で、AWS Distributed Load Testing を使って比較的手軽にAWS内での負荷試験を行うことが出来たのでその内容を紹介しようと思います。

内容はざっくり下記3点です。

  • これまで使ってきた負荷試験ツールとその悩み
  • AWS Distributed Load Testingとは
  • 実際の負荷試験の様子

これまで使ってきた負荷試験ツールの悩み

新規システムを開発し、サービスに導入する際には、負荷試験が必要になるケースも多いと思います。

負荷試験は、開発したシステムが想定リクエストに対して性能面で問題なく稼働出来るかをユーザに提供する前にチェックする目的で行うのが一般的です。

内容としては、レイテンシやステータスコードのエラー数等をレポートし、それらが基準として定めたパフォーマンスを満たしているかチェックします。もし想定外の結果が出た際は、負荷試験で露見したボトルネックを潰し、再度計測して基準値を満たせるまでそれを続けます。

負荷試験を行うツールは代表的なものがいくつかあり、ライトなものだとab - Apache HTTP server benchmarking tool、複雑なテストシナリオやレポーティングを求めると、Apache JMeter™ , Tsung ,Gatling - Professional Load Testing Tool等があります。

コネヒトでもケースに応じてこれらのツールを都度検討しつつ負荷試験を実施してきました。

ここでは各ツールの比較はしませんが、個人的な所感だとabは簡単な単一リクエストの負荷試験用途に便利で、その他のツールはそれぞれ特性はありつつも、一度手に馴染んで環境を作ってしまえば負荷試験ツールとして十分に便利だと思っています。

ただ、負荷試験環境を作るのに毎回そこそこのコストがかかるのが個人的にはもったいないなといつも思っていて、実際に下記のような悩みポイントを毎回抱えていました。

  • 負荷試験の環境準備には2つあり、負荷をかけられる側とかける側がある。負荷をかける側の準備は本来の開発とは関係ない部分なので、出来るだけ低コストに抑えたいのですがテストの規模やツールによってはその準備に大きなコストがかかるケースがある。負荷をかける側がボトルネックになり期待したパフォーマンスが出ないケースも十分に想定されるのでおざなりには出来ない。 端的にいうと大きめのリクエストをクライアント側をボトルネックにせずに回す環境作るの大変、、、
  • 負荷試験の頻度が年に数回なので、nヶ月後にやろうとした時に環境含めたキャッチアップコストがそこそこ高い

以降ではこれらの負荷試験環境の準備コストにかかる悩みを一定解消してくれた AWSマネージドの負荷試験ツールであるAWS Distributed Load Testingを紹介していきます。

※参考までに、今回のツール選定前にざっくりかかげた負荷試験ツールの要件のキャプチャを貼っておきます。

AWS Distributed Load Testingとは

AWS SAに負荷試験について相談したところ、下記ブログを教えてもらいAWS Distributed Load Testingの存在を知りました。

大規模な負荷テストを実行可能。「Distributed Load Testing on AWS」 を試してみる - builders.flash☆ - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

自分なりの解釈で要約すると、下記の特徴があるサービスかなと思っています。

  • CloudFormation一発実行で負荷試験の実行環境を作ってくれるサービス
    • 負荷試験環境の構成管理不要
  • 作られるサービスにはフロントエンドとバックエンドに分かれる
    • フロントエンドは、負荷試験の設定やレポートを補完閲覧するためのWebアプリケーション
    • バックエンドは、負荷試験でいうところのクライアント環境(負荷掛け機)
  • 単純なエンドポイントへのHTTPリクエストの他、Jmeterのシナリオファイルをインポート出来る
    • Jmeterのシナリオファイル=複雑なテストシナリオを実行可能
  • クライアントはFargateタスクなので、クライアント側のCPU負荷をウォッチすることでリソース不足であれば簡単に設定画面から追加可能
  • クライアント側で計測出来る項目に、 Average response time(s) Requests Per Second err数(ステータスコードあり) Percentileレイテンシ 等があり1テスト毎にレポート
    • 上のレポートを一覧で見ることも可能(項目は固定だが)

これまで都度苦労してきた負荷試験クライアントの準備が、用意されているCloudFormation一発で作成/削除出来るという手軽さが個人的には一番刺さりました。

また、Jmeterのシナリオを使えるので複雑なテストシナリオの実行が可能かつレポートも今回求めているものを満たしていると判断したので、今回の負荷試験ツールとして正式採用することを決めました。

実際の負荷試験の様子

ここでは具体の設定方法には触れず、どのようなフローで負荷試験を回したかを紹介します。

具体の設定方法は、クラメソさんのやってみたシリーズが詳しいのこちらを参照していただくのがよいかなと思います。

AWSの負荷テストソリューションを試してみた | DevelopersIO

負荷試験のフロー

  1. ローカルのJmeterでシナリオを作る
  2. CloudFormationを実行して負荷試験環境を作る(ほぼワンクリック)
    • VPC内にリソース作ることも可能なのが嬉しいポイント
  3. jmxファイルをZIPに固めて、Distributed Load Testing上でアップロード

    • csv等を使う場合は、一緒にzipに固める
      • シナリオで読み込ませるファイルのパスをアップロード時の相対パスにする必要があるので注意
  4. General Settingsにある負荷テストの条件を設定して実行

  5. 実行後の結果を確認し再度設定を変えて基準値を満たすまで実施
    • 負荷がけ機(クライアント)側のリソース状況はECSクラスタ関連のCloudWatchで確認
    • 負荷をかけられるサービス側はそれぞれのAPIやデータソース側のメトリクスを確認
    • 負荷テストの実行結果のレポートはこんな感じでテスト毎に出力されます 実行結果としてレポートされる項目の一覧は下記です。
    • Average response time : テストによって生成されたすべてのリクエストの平均応答時間 (秒)
    • Average latency : テストによって生成されたすべてのリクエストの平均レイテンシー (秒)
    • Average connection time : テストで生成されたすべてのリクエストについて、ホストへの接続にかかった平均時間 (秒)
    • Average bandwidth : テストで生成されたすべてのリクエストの平均帯域幅
    • Total Count : リクエストの総数
    • Success Count : 成功したリクエストの総数
    • Error Count : エラーの総数
    • Requests Per Second : テストで生成されたすべてのリクエストの 1 秒あたりの平均リクエスト数
    • Percentile : テストの応答時間のパーセンタイル値 (最大応答時間は 100 %、最小応答時間は 0 %)
  6. 負荷試験の結果、基準値を満たせたらCloudFormationのスタック削除で環境がまるっと削除されるので後片付け終了

負荷試験のまとめとして、実行結果と付随情報を一覧出来るようにまとめて最終的なレポートは自作しました。どの負荷試験ツールを使ってもこれは一緒で、実際のサービス側のリソース状況(CPU,mem等)と負荷試験レポートの相関関係が一望出来ないと意図しない結果の時にボトルネックがわかりにくくなるため一覧出来るようにしています。

ただ、Distributed Load Testingのレポートベースで作れるので作成コストは非常に小さく済みました。

今回は、AWS Distributed Load Testingを使って比較的簡単にAWS内で負荷試験を実施した事例を紹介しました。

個人的には、負荷試験の環境準備にかかるコストが劇的に減った感覚があるので非常におすすめのサービスです。

最後に宣伝です! コネヒトでは一緒に成長中のサービスを支えるために働く仲間を様々な職種で探しています。 少しでも興味もたれた方は、是非気軽にオンラインでカジュアルにお話出来るとうれしいです。

コネヒト株式会社

hrmos.co

A/Bテスト標準化へ取り組んだ話

みなさんこんにちは!機械学習チームのたかぱいです。

半年ほど前からA/Bテストの標準化に取り組んでいたので、本日はその背景やプロセスについてご紹介しようと思います。

尚、以下メルカリさんの事例を参考にさせていただいています(この場を借りて御礼申し上げます。ありがとうございます!)

note.com

標準化に取り組んだ背景

コネヒトでは日常的にさまざまなチームでA/Bテストが行われていました。

しかし、以下のような課題があると感じていました。

  • A/Bテストに関する知識にバラつきがある
  • チームごとにA/Bテストのドキュメントの書体が異なるので、読み解くのにコストがかかる
  • 「どのような実験」が「どのくらいの期間行われていたか(いるか)」という情報が一目で把握できない

etc...

上記のような課題は時間が経てば経つほど負債が大きくなると判断し、一度テコ入れした方が良いと思い、標準化に取り組みました。(後述する書籍の日本語版が発売になったことも大きなトリガーでした)

ここで言う「標準化」 is 何?

標準化によって得たい効果は、以下のようなものを想定していました。

  • 実験計画書のテンプレートをつくり、レビュー文化も導入することで、人依存による実験設計・検証品質の分散を減らす
  • 書体が揃うことで過去のドキュメントを読み解くコストが下がる
    • いつ、どんなものが誰によって検証されたかを振り返りやすく・見やすくする
  • 実験文化そのものの醸成を促す
  • 他のチームでどんな実験をやっていて、どんなメトリクスが使われているのかを共有知とする
  • 過去に何がうまくいって、何がうまくいかなかったのかを振り返ることができる
    • 未来へKnowledgeリレーを繋いでいきたい

そのため、まずは誰でも使える標準化テンプレートを作成し、それに沿って実験を行うフローにアップデートしようと考えました。

以降で、標準化プロセスでポイントだと感じた以下2点について紹介できればと思います。

  1. さまざまな職種の方と輪読会を行い、A/Bテストに対する視座を揃える
  2. 先んじて活用事例を作成し、今後社内で行われる実験で利用するイメージを沸かせる

最終的に作成したテンプレートは冒頭で述べたメルカリさんのテンプレートに若干アレンジを加えたものになりました。(最後にサンプルを載せています)

1. さまざまな職種の方と輪読会を行い、A/Bテストに対する視座を揃える

まず最初に行ったことは、いろんな職種の方を交えて輪読会をしました。

読んだ本は界隈で有名な A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは という書籍です。

社内slackで参加者の募集を行い、ML、インフラ、サーバーサイド、ネイティブエンジニアやPdMといったいろいろな専門性を持った方々に参加していただきました。

個人的にはPdMを含めた様々な職種の方に参加してもらえたのはとても良かったなと思っています。

例えば、本を読んでいく中で「インフラでこの辺の数値って計測できるんですか?」といった議論や、「ネイティブでこの辺のログって取得できるんですか?」といった議論をその場で行うことができました。
また、PdMにも参加してもらったので「今のコネヒトだとこの辺の検証はどうやっているのか」や「この辺注意した方が良いよね」といった共通認知を取ることもできました。

本を読み終えたあとは、輪読会で議論した内容をもとにテンプレートに落とし込み、関係者にレビューしてもらいブラッシュアップしていきました。

レビュー時の様子

2. 先んじて活用事例を作成し、今後社内で行われる実験で利用するイメージを沸かせる

無事にテンプレートが作成できても、実際に使うまでにはハードルがあると思います。

そのため、機械学習のプロジェクトで過去に行ったABテストをこのテンプレートに落とし込み、いくつかサンプルを作成しました。

これにより少しでも活用イメージを沸かせてもらい、「自分も使ってみようかな」というアクションを促しました。

作成したテンプレート

冒頭で紹介したメルカリさんの事例にあるように、「Background」「Test setting」「Metrics Details」「Action plan」という項目は、細部は異なりますが概ね同じ項目を採用しました。

大きな差分としては「Result」という項目を追加しています。

この項目は、実験が終了した後に記載する項目で、実験が複数のメトリクスにどの程度の影響を与えたかを記録しておく項目です。
数値が改善した理由(成功した理由)や、数値に変化がなかった or 改悪した理由(失敗した理由)などの考察も記入します。
ここの記載内容をもとに、A/Bテストの成功 or 失敗を判断し、事前に決めておいたAction plan**に則ってNext actionに移行します。

Resultは以下のような項目でテンプレート化しました。

# Result

## OEC metricsの結果
- hoge

## Guardrail metricsの結果
- hoge

## Debugging metricsの結果
- hoge

## 考察
- hoge

## Next action
- hoge

この項目を追加した意図としては、実験計画書とその実験の結果や得られた知見をセットで記録しておくことで、後から振り返りやすくしたい、という想いがあります。

標準化に取り組んでどうだったか?

運用を始めて半年ほど経過しましたが、このテンプレートを使って数十件の実験計画書が作成されています。

実験計画書を作成する際も項目に沿って埋めるだけで良くなったので、準備コストは削減できているなぁと感じています。

また「こんな検証してるんだな〜」ということも分かりやすくなり、各チームの簡易的な活動の可視化にもなっていると感じています。

最後に

前提として、実験の成功率は高くないと思っています。

BingやGoogleでも、アイデアの成功率は約10%〜20%程度、Slackでも30%程度しかポジティブな結果を得られないことが公開されています。

つまり、70%以上は失敗するわけですが「失敗=負け」だとは思っていません。失敗から何も学べない状況こそが本当の敗北だと思っています。

そうならないためにも、失敗前提で標準的なプロセスに則ってサイクルを簡単に回していける仕組みを作っていくはとても大切だと思います。

実験計画書を溜めておくことによるメリットを享受できるのはもう少し先になると思いますが、確実に未来へ資産を残していけていると思うので、形骸化しないように今後もブラッシュアップしていこうと思います!

We Are Hiring !!

コネヒトでは一緒に働く仲間を募集しています!

もしご興味があれば、カジュアルにお話しさせてください!

www.wantedly.com