あんしんしてお買い物してもらうためのカートUIリニューアルの裏側

f:id:enigmo7:20211210121849p:plain

こんにちは、BUYMAデザイナーの本田です。 BUYMAでは今年の2月〜7月にかけてカートの大幅なリニューアルを行いました。

PHPからRailsへの洗い替えや、パフォーマンスの改善がメインの施策だったのですが、UXの改善として、 上部に追従する購入ボタンよくある質問エリア を新しく追加しています。

ユーザーからよくある実際に届いている声の中から、

  1. 購入ボタンが遠くて見づらい
  2. キャンセルや返品がわかりにくくて不安
  3. 配送が遅い
  4. エラーがわかりにくい

上記4つの課題を解決するために、どのような想いのもとでデザインに落とし込んだのかについて紹介します。

比較

1. 購入ボタンが遠くて見づらい

BUYMAはカート画面でユーザーに確認していただきたいことが多く、1商品あたりの高さができてしまい、購入ボタンまでスクロール回数が多いことが課題となっていました。

複数商品をカートに入れていても、購入ボタンを見失わないでもらうために

  • 要素の見せ方整理をして、1商品あたりの高さを低くする
  • 購入ボタンを上部へ固定し、スクロール時も追従するようにする

の2つを行いました。

1商品あたりの高さを低く調節

2. キャンセルや返品がわかりにくくて不安

海外で購入した商品を日本へ送るCtoCショッピングサイトという特性上、

  • 偽物ブランドはないか
  • 返品はできるか
  • 送料や関税の負担はだれがするのか

などの質問をユーザーからいただくことが多く、専用の説明LPやガイドラインなどで説明しているのですが、それでも購入前になかなか気づけない・ユーザーの不安を拭えないことが、課題となっていました。

解決策として、LPへ遷移しなくてもカートページ内で補償サービスの内容を理解できるようにしたりユーザーからよくある質問エリアをまとめたエリア設置しました。

LPバナーをテキストへ変更、よくある質問を新設

3. 配送が遅い

海外配送となるため、送料の1番安いプランだと、繁忙シーズンには2週間以上かかってしまうこともあります。一方で、オプションメニューで、DHLやEMSなど海外の速達サービスを利用すると、+1,000円ほどで配送日数は大幅に短縮され、数日でお手元に届きます。

今までのUIですと、オプションメニューを選ぶことで、数日で到着することに気づかないまま、ご購入されるケースも多くありました。そのため、最短のプランがある時は、オプションメニューを選びやすくするUIパーツを追加しました。

最短プランがあるときはオプションメニューを選びやすくするUIパーツを追加

4. エラーがわかりにくい

今回のプロジェクトを機にすべてのエラー文言を洗い出し、どの状況下でエラーが発生してしまうのかを洗い出して整理し、提示するようにしました。

また、ユーザーは次に何をすればいいのか、ネクストアクションをきちんと提示することで、離脱率の低下を狙いました。

エラーパターン

検証

1. 購入ボタン

購入ボタンのクリックの割合は埋め込み:追従=8:2くらいで予想以上に利用されていました。 以前に他のプロジェクトで、商品詳細ページでも追従ボタンを設置してABテストをしていたので、あまり期待はしていなかったのですが、購入をある程度決めているカート画面には適切だったということがわかりました。

2. キャンセルや返品についてわからなくて不安

よくある質問エリアのクリックが全体の約2%で、割合としてはそこまで高くないですが、あんしんしてお買い物をするための後押しに少しでも貢献できたと思うと、デザイナーとして手応えがあったなと思います。

3. 配送が遅い

「最短の配送方法あり」のツールチップが表示されているユーザーの中で、さらに配送選択をしたユーザーの約45%が最短を選択していました。 サービスの特性などにもよるとは思いますが、追加料金を払っても早く商品が届いてほしいユーザーが多いことがわかりました。

まとめ

今回のカートリニューアルはユーザーの今の課題を解決する に加えて、あんしんしてBUYMAでお買い物を楽しんでもらうというビジョンを持ってサービス開発ができたと思ってます。

プロトタイプを定例で関係者全員に見せてフィードバックをいただけたり、プロジェクトチームの方全員と一丸になれたのもいい思い出です。来年も今年学んだことをベースにさらにいろんなことに挑戦していきたいです。

最後まで読んでいただきありがとうございました。 明日の記事の担当は 情シス の 田中さんです。お楽しみに。

株式会社エニグモ すべての求人一覧

hrmos.co

Googleスプレッドシートのオリジナル関数作ってみた

こんにちは、エニグモ嘉松です。

この記事はEnigmo Advent Calendar 2021の17日目の記事です。

はじめに

毎日毎日、それこそ仕事で使わない日は無いくらい、いつもお世話になっている表計算ソフト。 昔はみんなExcelを使っていましたが、最近は社内でもGoogleスプレッドシートを使うことが増えているように感じます。 (因みに私はLotus 1-2-3やNumbersは使ったこと無いです。)

さて、皆さんいろいろな関数を駆使して作業を効率化していることかと思います。

代表的な関数をいくつか紹介すると、

  • SUM

    • 表計算ソフトの関数の代表格といえば何と言ってもSUMですかね。
    • 指定した範囲の合計値を求めてくれます。
    • 「=SUM(A1:A10)」といった形で、合計したいセルの範囲を指定してあげます。
  • COUNTIF

    • 条件に一致するデータの個数を求めてくれます。
    • シンタックスCOUNTIF(範囲, 検索条件)です。
    • 「=COUNTIF(A1:A10, "<100")」といった形で、検索の対象とする範囲と、検索する条件を指定します。
    • 結果としては条件に合致したセルの数を返します。
    • 実は私、ほとんど使ったことないですが、よく聞くので紹介しました。
  • VLOOKUP

    • VLOOKUPを使えると表計算ソフトのスキルは中級くらいのレベルになりますかね。
    • この関数はホントに便利です。もはや無いと生きていけないレベル。
    • シンタックスVLOOKUP(検索値, 範囲, 列番号, 検索方法)です。
    • 範囲で指定した先頭の列を上から順に検索して検索値に一致する値を探します。見つかったセルと同じ行の列番号にあるセルの値を結果として返します。
    • 検索方法FALSEしか使ったことないです。

ちなみに、Googleスプレッドシートの関数の一覧はこちら。

Google スプレッドシートの関数リスト - ドキュメント エディタ ヘルプ

何十、何百の関数が用意されていて、使ったことも無い関数が殆どなのですが、自作のオリジナル関数を作ることも出来ます。

えっ、関数を自分で作る?そんな事できるの?と思われた方も多いかとも思いますが、私も最近知りました(笑)

では、これからオリジナル関数を自作する方法を紹介します。

消費税を求めるオリジナル関数を作ってみる

オリジナル関数、どう作るか?

そのためにはGoogle Apps Scriptでスクリプトを書く必要があります。

Google Apps Script(GAS)とは、Googleが開発・提供しているプログラミング言語です。

JavaScriptをベースに開発されたそうです。

スクリプトエディタを起動する

Googleスプレッドシートを開いたら、タブから「拡張機能」>「Apps Script」を押下します。

f:id:enigmo7:20211210112651p:plain
スクリプトエディタを起動

新しいタブが追加されて、以下のようなスクリプトエディタが起動されます。

f:id:enigmo7:20211210113818p:plain
スクリプトエディタが起動

スクリプトを書く

スクリプトエディタが起動されたら、ここにオリジナル関数のスクリプトを書きます。

ここでは消費税を求める関数myTAX関数を作ってみます。

/* 消費税を返す関数myTAX */
function myTAX(price){
  const taxRate = 0.1; //消費税率
  return price * taxRate;
}

functionのあとのmyTAXは関数の名前です。

priceは、関数の引数です。この関数の引数は1つなので、このように指定します。

const taxRate = 0.1; //消費税率taxRateという定数を作成して、税率の0.1(10%)を代入してあげます。

そしてreturn price * taxRate;で引数で指定されたpriceに消費税率taxRateを掛けて、その結果をreturnで返してあげます。

スクリプトができたら、フロッピーディスクのアイコンをクリックして保存してあげます。 (いまの若い方はフロッピーディスクなんて知らないと思いますが、これでイメージ付くのかな?)

オリジナル関数の使い方

では、作った関数を使ってみましょう。

使い方は簡単で、標準の関数と同様に=の後に関数の名前と引数を指定するだけです。

f:id:enigmo7:20211210120142p:plain
オリジナル関数の使い方

はい、このように消費税の金額が求められました。

f:id:enigmo7:20211210120700p:plain
オリジナル関数の結果

B2のセルの右下の小さな四角を下に引っ張れば、B2のセルがコピーされて、下の行の消費税が算出されます。

f:id:enigmo7:20211210121320p:plain
関数のコピー

消費税の算出だけであれば、セルに直接=A2 * 0.1とすればできます。

ただ、オリジナル関数を作って消費税を算出するメリットは、消費税の税率が変わった時にスクリプトの消費税率を変えてあげるだけで、変更後の消費税が取得できます。

  const taxRate = 0.12; //消費税率

こんな感じですね。

ABテストの信頼度を算出するオリジナル関数を作ってみた

実は、というか当然ですが私は消費税を算出するオリジナル関数を作りたかったわけではありません。

我々は日常的にABテストを実施しています。

例えばクーポンを配布した時に、配布したグループと配布しなかったグループで注文率に差があるが、どの程度差がでるか、クーポンを配布することで注文は増えるけど利益はどうなのか?といったようなことです。

この時、以下のような結果が出たとしましょう。

ABテストではクーポンの配布を実施したグループをトリートメントグループ、クーポンの効果を検証するために敢えてクーポンを配布しないグループをコントロールグループと呼びます。

対象者 注文者 注文率
トリートメントグループ 179,120 1,677 0.94%
コントロールグループ 11,800 98 0.83%

この結果からクーポンを配布したトリートメントグループの注文率が0.94%とコントロールグループの0.83%を上回っているので「クーポンの効果があった」と判断したくなります。

ただ、世の中、誤差は付き物です。 サイコロを10回振った時に1が10回連続で出たとしても、必ずしもそのサイコロがイカサマとは決めつけられません。偶然10回連続で1が出る確率は少なからずあります。

同様にクーポンのABテストでも偶然トリートメントグループの注文率が高かった、ということは無くはないです。

そんな事言ったらABテストを行う意味がないではないか、というとそうではありません。 今回の結果が偶然なのか、それとも偶然ではないのかを統計的な手法を使って判断することが可能です。 その方法を統計の用語で検定と呼びます。

検定の方法はいくつかあるのですが、今回はカイ二乗検定という検定方法を使って検定を行います。

詳細は省略しますがカイ二乗検定の方法は、以下の順序で行います。

①実績値を集計

注文あり 注文なし 合計
トリートメントグループ 1,677 177,443 179,120
コントロールグループ 98 11,702 11,800
合計 1,775 189,145 190,920

②期待値を算出

注文あり 注文なし 合計
トリートメントグループ 1,665 177,455 179,120
コントールグループ 110 11,690 11,800
合計 1,775 189,145 190,920

③実績値と期待度数の乖離値を求める

④乖離値を合算してカイ二乗値を求める

カイ二乗値からp値を求める

⑥p値から有意差を求める

一般的に有意差が95%を超えると有意な差がある、つまり偶然ではないと判断します。

このように有意差を求めるには多くの手順が必要となります。

そこでオリジナル関数の出番です。

今回は①〜④のカイ二乗検値を求めるところまでをオリジナル関数で行い、⑤〜⑥は標準で用意されている関数を使いました。

function myGETCHISQ(a, b, c, d){

  // 実績値
  var o = [
    a - b,
    b,
    c - d,
    d,
  ];
  
  var s = o[0]+o[1]+o[2]+o[3];

  // 期待値
  var e = [
    (o[0]+o[1]) * (o[0]+o[2]) / s,
      (o[0]+o[1]) * (o[1]+o[3]) / s,
        (o[2]+o[3]) * (o[0]+o[2]) / s,
          (o[2]+o[3]) * (o[1]+o[3]) / s,
            ];
  
  // カイ二乗検値
  var chisq = 0;
  for (i=0; i<=3; i++){
    chisq += (o[i] - e[i]) * (o[i] - e[i]) / e[i];
  }

  return chisq;
}

この関数のシンタックスは、以下のとおりです。

myGETCHISQ(試行回数A, 注文数A, 試行回数B, 注文数B)

上記の例だと、

=myGETCHISQ(179120, 1665, 11800, 98)

となります。もちろん引数にはセルの値を参照させることもできますよ。

この関数で求められたカイ二乗検値を標準で用意されているCHISQ.DIST.RT関数に与えてあげることでp値が求められます。

CHISQ.DIST.RT(カイ二乗値, 自由度)

そして、1からp値を引いて100を掛けてあげれば有意差が求まります。

因みにこの例の有意差は75.36%で95%を下回っているので、有意な差が出ているとは言えない、となります。

最後に

今回はGoogle Apps Script(GAS)を使ってGoogleスプレッドシートのオリジナル関数を作る方法を紹介しました。

また、より実践的な例としてABテストの信頼度を算出するオリジナル関数を作ってみました。

GASを使うと日頃の作業を上手に、そして簡単に効率化することが出来ます。

是非参考にしていただければと思います。

明日の記事の担当はデザイナーの本田さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

受注リストRails化プロジェクトについて

こんにちは、Webエンジニアの平井です。 この記事は Enigmo Advent Calendar 2021 の16日目の記事です。

現在、私はBUYMAのSellチームに所属していて出品者関連システムの実装を担当しています。 今季最も注力した受注リストページのRails化プロジェクトについて、プロジェクトの概要、プロジェクトを進める上で工夫した点、失敗した点について書きたいと思います。

Rails化プロジェクトとは

もともとBUYMAPHPで実装されたシステムで、その実装をRuby on Railsで書き換えるのを弊社ではRails化と呼んでいます。 Rails化する目的は以下になります。

受注リストページについて

受注リストページはBUYMAで発生した受注が一覧で表示されていて、出品者がその受注情報を確認したり、受注に対して発送通知などの アクションを実施するために用意されたページです。

出品者にとって利用頻度が高いページで、施策で修正する頻度が高いもののPHPの古いフレームワークで実装されているため修正のコストがかかるのがネックでした。

また、絞り込み機能が乏しく、出品者が目的に沿った取引を絞り込めずに各アクションを実施するのが大変という企画サイドの要望も有りました。 ですので、今回はRails化に加えて、検索機能の充実化も開発スコープに含めてプロジェクトを進めました。

システム概要

クライアントサイド

クライアントサイドは、実装スピードを上げるため大部分はerbで実装しました。ただ、検索フォームに関しては共通コンポーネントを利用したかったため Reactを使いました。

サーバーサイド

すべてRuby on Railsです。また、BUYMAでは検索サーバーにSolrを利用していて、この受注リストでも例外なく 受注の検索はSolrを使っています。ただ、旧受注リストではSolrを直接利用していたためSolr特有のパラメーター生成処理など も実装されていました。そこで、検索エンジニアの方にAPIを開発してもらい新受注リストからはそのAPIを利用して検索処理を実装しています。 APIを利用したため、Solrの専門的な知識が不要でアプリケーション側の実装の負担がかなり減りました。

工夫した点

ドキュメントの作成

Rails化プロジェクトの難しいところは、PHPで書かれたソースコードを読み正しく仕様を理解することです。 旧受注リストには、出品者が利用する様々な機能が実装されていますがまとまったドキュメントがなかったのでソースコードを 呼んで仕様を理解する必要があります。 ドキュメントがあれば非エンジニアとのコニュニケーションがスムーズに進むのでこのタイミングでドキュメントを作成しました。 また、実装とドキュメント作成を別々で行うとどうしてもドキュメント作成が後回しになってしまうため、実装しながらドキュメントも同時に作成するように 工夫しました。

ドキュメントを作成した事で、デザイナーやQA担当の方とのコニュニケーションがスムーズに進んだと思います。

フィーチャーフラグの利用

リリースの負担を減らすために、フィーチャーフラグを利用しました。 リリースブランチに受注リストRails化に関わる修正をマージし、該当ブランチを毎週リリースすることで新受注リスト公開時のリリース負担を減らすことが出来ました。

追加機能の優先度付け

リリーススケジュールに関しては、12月のセール時期より前に新受注リストをリリースしたいという企画側の要望が有りました。 そこで、企画サイドとコニュニケーションを取り追加機能の優先度を決めて、リリース目標日までに実装すべき機能とそうでない機能を切り分けました。 優先度を決めたのが功を奏し、目標としていたスケジュール日通りに基本的な機能をリリースすることが出来ました。

ユーザーインタビュー

企画的な内容になりますが、企画サイドの担当者の方が、新しく実装する機能やデザインについて多くの出品者に対してインタビューを実施してくれました。 リリース後に追加機能に関してネガティブな意見がほとんどなかったのもこのインタビューのおかげだと思います。

失敗した点

テスト環境でのパフォーマンス監視

そもそもRails化プロジェクトの目的としてレスポンスタイムの向上があるのですが、テスト環境でのパフォーマンス監視が不十分だっため 旧受注リストよりも、新受注リストのほうがレスポンスタイムが遅くなってしまいました。 原因としては, N+1とerb上でのrenderメソッドの多用です。 現在、絶賛修正中です。

最後に

いかがでしたでしょうか。現在弊社で進められているRails化プロジェクトについて書かせて頂きました。 今回プロジェクトで学んだ点を活かし、今後もBUYMAの改善を進めていきたいと思います。

明日の記事の担当は。データ活用推進室の嘉松さんです。


株式会社エニグモ すべての求人一覧

hrmos.co

MLOpsはじめました

この記事は Enigmo Advent Calendar 2021 の15日目の記事です。

はじめに

寒さが身にしみる今日この頃、みなさん如何お過ごしでしょうか。
最近、○○エンジニアという肩書きがよく分からなくなってきたエンジニアの伊藤です。

アドベントカレンダーの時期になると年末になったんだなという実感が湧きますね。
今回は今年一番注力してやってきたMLOpsについて書いていこうかと思います。

MLOpsとは?については他にも色々と記事があると思うのでここでは触れずに、 導入に向けてどういった手順でどのような取り組みを行ってきたかを中心にご紹介できればと思います。

なぜMLOpsなのか?

弊社でも機械学習を活用したプロジェクトがここ最近増え始めています。
そういった中で課題も色々と見えてきました。

  • 属人化
    • 実装内容を把握しているのが個人に依存している
    • レビューがされていない
    • 使用されているバージョンやライブラリがプロジェクトによってバラバラ
  • 開発効率の低下
    • 同じような処理が色々なところにある
    • テストコードがない
  • 心温まる運用
    • モデルの作成、デプロイは手動
  • オブザーバビリティの欠如

こういった状況を踏まえつつ、 今後も増えてくるであろう機械学習プロジェクトを見据えて、MLOpsを導入することになりました。

全体的な流れ

MLOps導入まではざっと下記のような流れで進めました。

  1. 調査
  2. MLOpsの定義、方針/目標の設定
  3. アーキテクチャの設計
  4. ML基盤の構築
  5. 開発体制やルール等の策定
  6. 実装
  7. CI/CD/CTの検討、設計、実装
  8. 監視周りの検討、設定

4以降についてはきっちり分けて進めていた訳ではなく、走りながら(実装、構築しながら)並行で進めていくことが多かったです。 以降で各項目からいくつかピックアップして詳細については記載します。

調査

私自身、MLOpsについては何となく分かっていたつもりでしたが、先人たち(先行他社)の取り組みや関連資料を読み漁りました。 ここで特に気を付けていたのは下記の点です。

  • 機械学習プロジェクトにまつわる課題としてはどういったものがあるのか?
    • 現時点で弊社としての課題感はないが、今後課題になりうるものがないか?
  • 先人たちの取り組みを真似るのではなく参考にする
    • 各課題に対してどういった考えで、どのようなアプローチをとっているのか
    • 個人的な考えですが、組織の規模感や文化といったものが違うのに同じような取り組みをしても失敗すると思っています

MLOpsの定義、目標の設定

MLOpsはバズワードだなんて意見もあります。 MLOpsという単語自体が一人歩きしている感もあるかなと思います。 なので一通り調査が終わったあとは弊社におけるMLOpsを定義することにしました。

今更ながら弊社のメンバー構成を紹介すると下記の通りで、スモールスタートにはちょうど良い人数かなという印象です。

  • データサイエンティスト(以降、DS) ✕ 2名
  • エンジニア(以降、Ops) ✕ 2名

エニグモにおけるMLOps

「DS(ML)とエンジニア(Ops)がお互いの役割を理解し、強調し合うことで、 機械学習モデルの実装から運用までのライフサイクルを円滑に進めるための仕組みづくりやその考え方」

f:id:pma1013:20211208132316p:plain

方針

続いて方針についてです。3つ挙げました。

  1. 小さく始めて、大きく育てる

  2. 同じ言葉で話せるようにする

  3. マネージドサービスの活用

小さく始めて、大きく育てる

  • 最初からあれも、これもと色々なことを詰め込みすぎない
    • いつまで立っても設計が固まらず、システムの構築や運用が始まらないというのは一番避けたい
  • MLOpsを取り巻く環境はまだまだ成熟しておらず、ノウハウや技術も発展途上の段階
    • 柔軟性が高くスケールする基盤であることを第一としてシステムを設計、構築する

同じ言葉で話せるようにする

  • スキルセットやメンタルモデルが異なるメンバー同士で作業を進めていく上ではどうしてもコニュニケーションコストは高くなる
  • エンジニア同士では暗黙知としているようなルールやお作法についてもきちんと明文化し、使用するツールやフレームワークなども共通化
    • ガチガチのルールで縛るのは好きではないので、バランスには注意

マネージドサービスの活用

  • 限られた人的リソースの中で対応していくにはどうしても人がボトルネックになりがち&人的コストはスケールさせるのが難しい
    • お金で解決できるようなところは積極的に利用する
  • 何でもかんでもマネージドという訳ではなくバランスが大事
    • コスト(人とお金) X システムとしての柔軟性は意識する

目標

f:id:pma1013:20211208141457p:plain

最後に具体的に何をしていくか、どこを目指すかの目標についてです。 ひとまず今期(2021年)としては下記を目標として挙げました。

DSが開発しやすい環境作り

  • UIベースでの実験管理
  • 再現性の確保(今期はデータではなく環境面にフォーカス)
  • 各種ルール化(明文化)
    • コーディング規約、レビュー規則、ログ仕様、開発フロー
  • 実験段階から本番環境へのデプロイがシームレスに可能
  • ドキュメントの充実化

開発コスト削減

データの可視化と監視

  • モデルの性能指標など、可視化して確認したいデータが見たい時に見られる状態
  • 過不足のない監視、アラート

変化に強い基盤構築

  • 汎用性が高く、スケール可能な基盤であること

チームとして1つの目標を目指す上で、言葉の定義や、目標の明文化というのはとても大事だと思っているので、 この辺については特に丁寧に行いました。

また目標としてやるべきことを明確化することと合わせて、やらないこともあえて決めておきました。
どうしても作業を進めていく中で「こんなこともできるとうれしいよね」みたいなことは出てきます。
方針にも挙げていますが、まずは小さく始めることを第一としていたので、こういった迷うケースでも チームとしてブレずに進めたのは良かったなと思います。

アーキテクチャの設計

続いてアーキテクチャの設計についてです。

ツール選定

アーキテクチャを設計する上ではまずはMLワークフローの中心となるツール選定を行いました。 MLOpsを取り巻くツールはMLOps Toysに集約されるようにとても多岐に渡ります。
この1つ1つを調査、検証していてはとても時間がかかるのでツール選定については時間をかけずに、 下記の観点で一番相性の良さそうなKubeflowを選択しました。

  • 利用事例が多い
  • 開発が活発
  • 柔軟性がある
    • まずはPipelinesのみ導入し、その後必要に応じて他のコンポーネント導入が可能
  • Kubernetesの構築、運用実績がある

アーキテクチャ

分析等に扱うデータはBigQueryに集約している関係で、そのデータを扱うML系のプロジェクトもGCP上で構築することが多いです。 そのため今回新たに構築するML基盤についてもGCP上で構築しています。 またKubeflow PipelinesについてはそのマネージドサービスであるAI Platform Pipelinesを利用することとしました。

f:id:pma1013:20211208143338p:plain

  • ML基盤としてGKEを利用
    • 3パターンのnodeプールを用意
    • ML nodeプール
      • MLパイプラインでの処理を実行するpod用のnodeプール
        • 0 to Nのノードスケール
        • 使用するマシンタイプは短時間で高速で処理が完了するようにCPU,メモリともに潤沢なものを選択
    • API nodeプール
      • 予測結果のサービング用API向け
        • 1 to Nのノードスケール
        • 使用するマシンタイプはCPUメインの比較的軽めのものを選択
  • GCPマネージドサービスとして下記を利用
    • AI Platform Pipelines
    • BigQuery
    • Cloud Storage
    • Cloud Datastore
    • Container Registry
  • 各種メトリクスの可視化、監視にはDatadogを利用

ML基盤の構築

ML基盤としては上記の通り、GKEをインフラとしつつ、AI Platform Pipelinesを利用しています。 基本的には全てIaCとしてterraformで管理、運用していますが、AI Platform Pipelinesは未対応だったため対象外として扱っています。 また、システムに名前があった方が良いよねということで、 メンバーそれぞれで候補となる名前をいくつか挙げて、投票で一番多かったCapellaという名前に決まりました。

CI/CD/CTの検討、設計、実装

CI、CD、CTともに全てGitlab CIを利用するようにしました。
Cloud Buildなども検討しましたが、Gitlab CIの方がより柔軟性が高かったのと、元々別のシステムでもGitlab CIを利用していたというのが理由です。

CI/CD

  • テスト環境

f:id:pma1013:20211208190442p:plain

  • 本番環境

f:id:pma1013:20211208190522p:plain

  • ポイント
    各工程の細かい処理については置いておいて、ポイントについてここでは記載します。
    • 成果物はテスト、本番環境で共通
      • Kubeflow Pipelinesにおける成果物してはパイプライン(コンパイル済みファイル)とそこで実行されるコンポーネントのコンテナイメージとなります。
      • 本番環境ではテスト環境で作成された成果物を使用してパイプラインのデプロイを行います。
    • integrationテストとして実際にKubeflow Pipelines上でコンポーネントの動作検証を自動テスト
      • 作業を進める中でコンテナイメージ単体としては問題なく動作するが、Kubeflow Pipeline上でパイプラインとして実行した際に問題となるケースがいくつかあったため組み込みました。

CT

パイプラインの実行トリガーをどこに持たせるかは正直一番悩んだところです。 Kubeflow Pipelines側に標準実装されているスケジュール機能を利用するのが良いのかなとも思いましたが、下記の理由からGitlabパイプラインスケジュールを利用して定期実行するようにしています。

  • CI/CD用に作成したツールを使い回せる
  • gitブランチと紐付けてスケジュール実行が可能
    • master → テスト環境
    • production → 本番環境
  • CI/CDジョブとシームレスに扱うことができる
  • CI/CD/CTがすべてGitlabで管理されているという分かりやすさ
  • Kubeflow Pipelines側のスケジュール機能で実行した場合に、MLパイプラインの実行ステータスの通知をどうするかの検討が別途必要だった
    • Gitlab側であればパイプラインステータスを判断してSlack連携が可能なことは確認済みだった

なお、下記がGitlabパイプラインスケジュールのUIになります。 内容としても直感的に分かりやすく、cronジョブベースでの定期実行と、設定した変数で処理が分岐できるようにしています。

f:id:pma1013:20211209093618p:plain

おわりに

ここまで調査/検討から始まり導入までについて、ざっとご紹介させていただきました。 書ききれていない内容もありますが、少しでもどういった取り組みをしているのかが伝われば幸いです。

また今回構築したシステムについてはまだまだ未熟で、これからどう成長させていくかが大事かなと思っております。 そんな訳でエニグモではMLプロジェクトを一緒に盛り上げていくメンバーについても募集中です! この記事を見て少しでも興味を持っていただけたら、まずは雑談からでもOKですので気軽にご相談ください。

明日の記事はエンジニアの平井くんです!きっとワクワクするような内容だと思うのでめちゃくちゃ楽しみですね!!!

株式会社エニグモ すべての求人一覧

hrmos.co

シェルスクリプトの実装に潜む 4 つの罠

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

この記事は Enigmo Advent Calendar 2021 の 14 日目の記事です。

みなさんはシェルスクリプトを実装する機会はどのくらいの頻度でありますでしょうか?

私は社内ツールや個人で利用するちょっとしたツールを作成する際に、シェルスクリプトを実装することがあります。 とはいえ、普段の業務では Ruby on Rails を用いて実装をすることが多いので、 シェルスクリプトを実装する機会自体はそれほど多いものではありません。

その為、シェルスクリプト実装時に割と基本的な罠に陥ってしまうことがありました。 

そこでこの記事ではシェルスクリプトの実装に潜む 4 つの罠を共有したいと思います。

1. shebang の記載漏れ

# bad

echo "FOO"

# good

#!/usr/bin/env bash
echo "FOO"

ファイルの 1 行目に shebang を記載することで、プログラムを実行するシェルを明示的に指定することが推奨されます。 というのも、シェルによってはサポートされている機能が異なることがあるため、 スクリプトを実行するシェルを明示的に指定することで意図しない挙動を避けることができます。

ここで 1 つの疑問が発生しました。

シェルスクリプトにおいてshebang の記述は下記の通り複数ありますが、好ましい記述方はどれなのでしょうか?

#!/bin/bash
#!/usr/bin/env bash

一般的には#!/usr/bin/env bash の形式を利用することが推奨されるようです。

絶対パス等で指定をした場合、実行環境によっては指定の場所に実行可能なコマンドが存在しない可能性が考えられます。 上記の方法で記載すると、 $PATH から実行可能なコマンド(この場合 bash)を捜査してくれるのでポータビリティという点でメリットがあります。

さて、少し話が逸れてしまったので閑話休題、 2 つ目の罠に話を移します。

2. 適切な set options を指定しない

# bad

#!/usr/bin/env bash
echo "FOO"

# good

#!/usr/bin/env bash
set -eu
echo "FOO"

-e (errexit)

こちらのオプションを指定することで、シェルスクリプトの途中でエラーが発生した場合その場で直ちにスクリプトを終了します。

大抵のケースではエラーが発生した場合、その場でスクリプトを終了したいことが多いと思いますので有効にしておくことが推奨されます。

ちなみに、エラーを許容したい場合には下記のように記述することで該当のコマンドのみエラーを許容することが可能です。

command1 || true
echo "This line is executed anyway"

-u (nounset)

こちらは変数宣言に関するオプションです。

デフォルトでは宣言していない変数を利用した場合、空文字として扱われてしまいます。 その為、typo 等で未宣言の変数を利用したとしても特にエラーにはならないので、意図しない挙動を招くことがあります。 そういったケースを避けるためこちらのオプションを有効にしておくことをお勧めします。

3. 1行で宣言と代入を行う

# bad
export BAR="$(command2)"

# good
BAR="$(command2)"

export BAR

上記のスクリプトでは command2 の結果が無視されてしまいます。 set -e を指定している場合、本来エラーが発生した箇所でスクリプトは終了します。 ただ、上記のケースでは command2 の結果に関わらず後続の処理が実行されてしまいます。

その為、代入と宣言の箇所を分けることが推奨されます。

4. Double quotes で変数を囲わない

# bad
file="sample file.txt"
ls $file

DIR="$(dirname $0)/.."

# good
file="sample file.txt"
ls "$file"

DIR="$(dirname "$0")/.."

double quotes を利用しないと該当の文字列中にスペースやタブ等を含むケースで、意図しない結果を招くことがあります。 例えば、上記のようにファイル名にスペースを含むケースだと、double quotes で該当の変数を囲わない場合スペースによって samplefile.txt という 2 つの単語に分割されてしまいます。 結果として、 sample file.txt というファイルではなく samplefile.txt を引数に ls コマンドが実行されてしまいます。

このような、意図しない挙動を避ける為、シェルスクリプトでは変数を double quotes で囲むことが推奨されます。

さて、ここまでシェルスクリプトの実装に潜む 4 つの罠について記載してきました。 シェルスクリプトを実装したことがある開発者にとっては、これらのどれもが一度は経験してきた問題なのではないでしょうか?

これらの問題が一般的ではある一方で、全てをマニュアルで人間が気をつけることは難しく労力を要する作業かと思います。 ということで、 shellcheck というツールを利用することをお勧めします。

shellcheck とは?

シェルスクリプト用の静的 Lint ツールです。 こちらを利用することで、上記で説明してきたエラー(罠)を簡単に検知することが可能です。

コマンドを実行すると、下記の通り問題のある箇所を明瞭に指摘してくれます。

>>> shellcheck ./tmp.sh

In ./tmp.sh line 1:
set -eu
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.


In ./tmp.sh line 5:
echo ${baz}
     ^----^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean: 
echo "${baz}"

For more information:
  https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

さて、これだけでも記憶を頼りにマニュアルで全てを確認することに比べ、 簡便になったとは思います。

しかし、こちらのコマンドを修正の度に毎度手動実行することは非常に骨の折れる作業であり、非人間的です。 ということで、CI として設定して自動で実行しましょう。

今回はサンプルとして昨年の Advent Calendar 2020 の記事に記載した自作の OATHTOOL ラッパーコマンドshellcheck を CI として追加してみます。

こちらのレポジトリは GitHub 上に存在するので、今回は GitHub Actions を用いて設定していきます。

設定ファイルは下記。

# .github/workflows/main.yml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  shellcheck:
    name: Shellcheck
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run ShellCheck
      uses: ludeeus/action-shellcheck@master
      with:
       scandir: './bin'

shellcheck は既に GitHub Action としてマーケットプレースに存在するので、設定自体は簡単です。 上記のように設定ファイルを 1 つ追加するだけで設定は完了です。

github.com

無事に CI として shellcheck が走るようになりました。

最後まで読んでいただきありがとうございました。

明日の記事の担当は検索エンジニアの伊藤 明大さんです。お楽しみに。

参考


株式会社エニグモ すべての求人一覧

hrmos.co

移行パターン(ストラングラー、抽象化ブランチ、同時実行)を活用してブラックフライデーを乗り切った負荷対策

エンジニアの木村です。この記事は Enigmo Advent Calendar 2021 の 13日目の記事です。 いろいろやってますが、BUYチームという購入UXに関わる機能開発を担当するチームのマネージャーもやっています。11月末に行われるブラックフライデーサイバーマンデーといった大型キャンペーンに備えた開発もそのチームで担当したのですが、今日はそれに備えて行った負荷対策の1つの取り組みについてお話しします。

悔しい思いをした去年のブラックフライデー

これは昨年のブラックフライデー開始直後のBUYMAオフィシャルアカウントのツイートになります。。悲しいですね。下のリンクから当時のBUYMAの状況を表すタイムラインが見れますが、どういう状況だったか推察いただけると思います。

バイマ OR buyma until:2020-11-27_00:50:00_JST -filter:links -filter:replies - Twitter Search

ユーザーのログイン状態管理が重い

原因の1つは、ユーザーのログイン状態の管理をサービスのメインDBで行っているところでした。スケールが難しいRDBです。最近はアクセス集中時にユーザーのログイン済み比率も増え、そのユーザーのアクセスでは毎リクエストそのDBのテーブルへSELECTが走るのでメインDBが高負荷になり、メインDBなのであらゆる機能へ影響するためサイトとしては繋がりにくくなるという状況でした。もちろん、以前から問題視されていた仕組みではありましたが、ログイン状態の管理という重要な機能でもあり、原作者もおらず、別の仕組みへの移行の難易度が高くずっと放置してしまっていました。

したがって、DBのリソースをログイン管理のために温存する必要があるため、動的コンテンツ(HTMLやAPI)配信にもCDNを入れてキャッシュしたり、DB負荷が高い機能はアクセス集中が予測される時間帯には停止するようにスケジュールしたりなどいろんな手を尽くしていま。しかし、それでもブラックフライデーだけは耐えることができませんでした。

セッションの保存をRDBからredisへ

そこで、ログイン状態の保存先をRDBから、より読み取りレイテンシーが小さく、読み取りの集中にも耐久性のあるRedisへと移行するプロジェクトが始まりました。その移行の際に行った工夫が記事の本題となります。この工夫が、たまたま後日読んだこちらの本で紹介されている移行パターンそのものだったのでこちらの本の用語を使って説明していきます。

利用パターン1:抽象化によるブランチ

現代のソフトウェア開発では1つのコードベースでブランチを切って複数人で並行して行うのは普通ですが、あまりブランチを切ってから長い時間が経つと差分が大きく、レビューやマージが大変になります。そこで、実装を改良しようとしている機能の抽象を作り、既存の実装と並行してその抽象の新しい実装を別に作り、さらにあとで切り替えるという方法が抽象化によるブランチと呼ばれる移行パターンです。大きな変更であっても、寿命の長いブランチを作らずに、他で並行して行われる開発と影響しにくいように開発が可能です。今回の事例で1ステップずつ手順を説明していきます。

今回は抽象というよりはファサードとなるクラスを前に立てて新実装へ切り替えているので、モノリス内でのストラングラーパターンと言っても良いかもしれません。

ステップ0:変更前

変更前のアプリケーションはこのような構成でした。ActiveRecordのモデルを介してDBのテーブルを呼び出すというオーソドックスな作りかと思います。

f:id:enigmo7:20211203164956p:plain

ステップ1:ファサードを作成し、既存のクライアントの向き先を変更

最初に行ったのは、実装の変更を呼び出し元へ影響するのを抑えるため、ログインステータスの操作を抽象化するためにファサードとなるクラスを設けたことです。また、このファサードを利用するように、順次呼び出し元の向け先もこのファサードへ切り替えました。

f:id:enigmo7:20211203164959p:plain

ステップ2:並行して新しい実装を作成

従来の実装を稼働させつつ、新しい実装を横に作成します。実装のソースはデプロイされますが、この段階では事実上、従来の実装だけが動いたままです。

f:id:enigmo7:20211203165002p:plain

ステップ3:ファサード内で新しい実装を使うように切り替え

ファサード内部を変更して、処理の流れを従来の実装から新しい実装へと切り替えます。この切り替えは後述する 同時実行パターンカナリアリリース を使って段階的に行いました。

f:id:enigmo7:20211203165004p:plain

利用パターン2:同時実行

新旧の実装を切り替える時に、それぞれの実装で処理結果が変わらないことを担保する必要があるのですが、通常、デプロイ前にテストを頑張るという手段になると思います。ただ、テスト尽くしても本番環境で起こりうるシナリオは全て網羅することは困難です。特に、今回の変更はユーザーのログイン状態の管理になるので、不具合があると誤ってユーザーをログアウトさせてしまったり、あるいはBANしたユーザーを誤ってログインさせてしまったりなど重大な問題となってしまいます。

そこで、新実装を本番へ出してしまい、旧実装と同時に新実装も呼び出しつつ、旧実装の結果を呼び出し元へは返しておきながら、それぞれの実行結果を比較して新実装の結果が信頼できるか検証するという 同時実行 というパターンを利用しました。こちらの図のとおり、ファサード側でDB、Redisそれぞれの結果を処理が通るたびに比較検証し、差分があればログに出しておくようにしました。また、ファサードの処理のタイミング以外でもバッチで定期的にそれぞれの内容をスキャンし、差分有無の検証も行いました。差分が出れば原因を調査し、原因を潰していくという作業を差分が出なくなるまで続けました。

f:id:enigmo7:20211203225922p:plain

カナリアリリースも利用

最初に同時実行モードに入る前に、気になったのはRedisの負荷でした。ほかの機能で導入実績はありましたし、負荷テストはしていたものの、やはり本番のトラフィックを一気に向けるのは勇気が要りました。そこで、徐々に10%ずつ新実装へトラフィックを向けるようにカナリアリリースを行いました。また、同時実行により十分に検証を行い、新しい実装へと切り替えるタイミングでも、予期しないユーザーへの影響の可能性を考え、そのタイミングでも10%ずつ新実装のみの結果を利用するようにしています。カナリアリリースの仕組みは、cookieベースでトラフィックを振り分ける既存のABテストの仕組みを利用しています。

心穏やかに移行完了

以上の工夫によりあまりドキドキすることなく、心穏やかに移行を終えることができました。数ヶ月ほど時間はかかりましたが、抽象化ブランチの仕組みにより、切ってから長時間経った差分の大きいブランチをマージする必要も無かったですし、同時実行によりバグを十分出し切った上で、しかもカナリアリリースで少しずつリリースできたためです。時間がかかるというデメリットはあったものの、特に品質に気を遣う変更には有用かと思います。

負荷対策の効果

ついに迎えたブラックフライデー当日ですが、サイトのパフォーマンスを落とすことなく乗り切ることができました。こちらは、開始直後11/26の0時台の企画メンバーが集うslackのチャンネルの様子です。例年なら不安定になるはずのサイトがサクサク動いていて盛り上がっていました。

f:id:enigmo7:20211203234506p:plain

もちろん、今回紹介した移行以外にも負荷対策や様々なパフォーマンス改善策を重ねた結果なのですが、それらについてはまた別の機会に紹介しようと思います。

Terraformにまつわる運用tips的なもの

この記事は Enigmo Advent Calendar 2021 の12日目の記事です。

こんにちは。BUYMAの検索やMLOps基盤周りを担当している竹田です。
この一年間はTerraformを業務で利用することが多かったため、普段気を付けていることなどを運用tipsとして紹介したいと思います。

Terraform

Terraformは言わずと知れたInfrastructure as Code (IaC)を実現するためのツールです。
先日v1 🎉 になり、安定してきた印象があります。 f:id:enigmo7:20211207145027p:plain

Terraformに限った話ではありませんが、コード管理されている安心感は何ものにも変えがたいものがあります。 f:id:enigmo7:20211207151117p:plain

本記事はTerraform自体の説明は割愛し、ある程度利用したことのある方を対象としています。
個人の主観を多分に含んでいる、およびGoogleCloudに寄った内容のため他クラウドベンダーとは若干記述の毛色が違うといった可能性があります。あくまで参考程度としていただけると幸いです。
また、 terraform Advent Calender もあるようですので、より多くの情報を得たいという場合はそちらも参考にしていただくと良いかと思います。

定期的なバージョンアップ

Terraformはv1化により安定してきたように思いますが、クラウドベンターのproviderは変化のサイクルが早く、新機能の追加やベータ版のGA化など定期的に追従した方が良いです。

  • 気付かない内に有用な機能がリリースされており、無効の状態で運用を継続している可能性がある
  • 新機能があった場合に気付きを得られる
  • 項目の定義方法が変更になっている場合がある

設定理由を残す

インフラ面のライフサイクルとして定義変更の頻度が少ないこともあり、いざ定義を変更しようとした際に設定理由を忘れていることが多々あります。
「この機能は何々の理由で有効・無効にした」といった内容を何かしら文章として残しておくことをお勧めします。

terraform applyの際にエラーとなってしまう場合がある

主に認証周りや、providerサイドで項目のvalidationがされていないものについて発生することがあります。terraform planした際には正常終了するものの、いざterraform applyするとエラーになるというものです。
すでにコード管理上にcommitしていると二度手間になるため、開発環境やテスト可能な環境でterraform applyまで実施しておくことをお勧めします。

書き方のtips

tfファイルの記法は若干クセがあるため、どの書き方が適切なのか、どう書くとやりたいことを実現できるのか分からない状況に遭遇することが稀にあります。
以下に一部例を挙げてみます。


変数参照

ハッシュ記述value["preemptible"]なのかシンボル的記述value.preemptibleなのか迷います。大抵はどちらでも問題ありません。
ただし、混在すると見にくくなったり、記述箇所によっては意図があるように感じたりする場合があるため、統一感を持たせた方が良いように思います。
↓こんな混在した書き方ができてしまう

  node_config {
    preemptible     = each.value["preemptible"]
    machine_type    = each.value.machine_type
    disk_type       = each.value["disk_type"]
    oauth_scopes    = each.value.oauth_scopes

dynamicブロック

環境毎にリソース内の定義有無を設定できるため非常に便利なdynamicブロックですが、リソースによっては後で削除できないパターンが存在します。
例えば GoogleCloud のノードプールに設定可能なtaint定義などが該当します。taintの説明はこちらを参照ください。
なお、ここで例として挙げているtaintの変更は、変更の際にノードプール自体が作り直されてしまいますのでご注意ください。

taint定義を以下のようにdynamicブロック定義していたとします。

    dynamic "taint" {
      for_each = contains(keys(local.gke_nodepools), "taints") ? local.gke_nodepools.taints : []
      content {
        key    = taint.value.key
        value  = taint.value.value
        effect = taint.value.effect
      }
    }

variablesは以下のようになっており、terraform applyにより実環境にtaintが設定されていたとします。

    locals {
      gke_nodepools = {
        taints = [
          {
            key    = "testkey"
            value  = "testvalue"
            effect = "NO_SCHEDULE"
          }
        ]
      }

あるタイミングでtaint定義が不要となり、削除したいと考えてvariablestaints項目自体を除去してterraform planを実施します。すると

No changes. Infrastructure is up-to-date.

削除差分が出ると思っていたのに差分が一切検出されませんでした 😢

これはdynamicブロック対象のオブジェクトを定義しない場合に、そもそも定義が存在しないものと解釈されて空の内容では更新してくれないためです。
こういった場合は、taint定義を以下のようにします。

    taint = [
      for item in local.gke_nodepools.taints :
      {
        key    = item.key
        value  = item.value
        effect = item.effect
      }
    ]

続いて、variablesを空に設定します。

    locals {
      gke_nodepools = {
        taints = []
      }

このように定義することで、taint定義を空で更新することが可能となります。
以下はterraform plan時の差分結果です。

          ~ taint             = [ # forces replacement
              - {
                  - effect = "NO_SCHEDULE"
                  - key    = "testkey"
                  - value  = "testvalue"
                },
            ]

最後に

システム規模としては小〜中規模のため、規模面で困ったことは今のところありません。
大規模システムでは自動化などを考慮する必要がありそうなので、その辺りの悩みがない分は楽かもしれませんね。
今後はTerraform以外にもIaC関連のツールを利用するかもしれませんが、しっかり追従していきたいなと思います!

明日の記事担当はエンジニアマネージャーの木村さんです。お楽しみに!


株式会社エニグモ すべての求人一覧

hrmos.co