TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

590

BASE Advent Calendar 2021 はじめに コスト考慮型学習とは Cost-Sensitive Learningの手法 コスト行列 閾値の調整による誤分類コストの反映 実際のデータセットを用いた例 まとめ 参考文献 はじめに この記事はBASE Advent Calendar 23日目の記事です。 こんにちは、DataStrategyチームの竹内です。 BASEではより良いサービスを提供するために色々なところで機械学習モデルが活用されています。 BASEに限らず、インターネット上のあらゆるサービスに機械学習の技術が活用されるようになって久しい昨今ですが、こうした実際のサービスやビジネス領域に近いところで活用される機械学習モデルにおいては、計算コストやメンテナンスコスト、解釈性やバイアス、データセットシフトなど色々と考えなければいけない特有の要素が存在します。 今回はその中の1つとも言える誤分類コストの非対称性の問題について考え、それに対するアプローチとしてコスト考慮型学習(Cost-Sensitive Learning)について扱っていきたいと思います。 コスト考慮型学習とは コスト考慮型学習とは、データマイニングにおける誤分類時のコスト(誤分類コストに限らず、計算コストなどの他の要素を考慮する場合もあります。)を考慮した学習手法のことです。 機械学習などによる分類モデルを現実の問題に適用する場合、どのデータをどのクラスに誤分類してしまうかで生じるコストが異なる、いわゆる誤分類コストの非対称性の問題に直面することがあります。 1 よく挙げられる例で言えば、がんのような重大な病気を診断する場合、本当に罹患している人に対して健康であると誤診してしまった際の影響は極めて致命的になり得る一方で、健康な人に対して罹患していると誤診してしまった際は追加の検査費用分のコストで済むことになります。 医療診断における非対称な誤分類コスト 健康な人 罹患している人 陰性と診断 - 治療の遅れ、信頼の失墜(コスト大) 陽性と診断 追加の検査費用(コスト小) 早期治療 医療診断の場合ではどの患者についても概ね同様なコスト行列が適用できますが、中にはサンプルごとに誤分類コストが異なる場合もあります。 例えばカードローン審査のような例では、利用客によって申請金額が異なるため誤分類コストが変わってきます。 カードローン審査におけるサンプルごとに異なる誤分類コスト 返済できる人 返済できない人 審査を通す 金利や手数料分の利益 金額分の損失 審査を通さない 適格な申請数の減少 不適格な申請数の減少 このようなサンプルごとに誤分類コストが異なるタスクにおいてモデルの作成や改良を行う際、サンプル数ベースの正答率や再現率、AUCの改善が必ずしも金額ベースの改善につながらない可能性があることに注意する必要があります。 このように現実の問題を分類タスクとして捉え、予測モデルの作成や改良を行う場合、適切な誤分類コストに基づいた性能評価が求められる場合があります。 適切な誤分類コストの設定には機械学習や統計一般の知識だけではなく、その分野特有の知識、いわゆるドメイン知識を十分に活用することが求められます。 Cost-Sensitive Learningの手法 ここからは上の例のような非対称な誤分類コストに対する具体的なアプローチについて扱っていきます。 誤分類コストを考慮したモデルを構成する手法は大きく分けて3つ存在します。 通常の学習を行なったモデルの出力に対する検出閾値を、サンプルの誤分類コストに応じて変更する 学習データセットのクラス比率、あるいは重みを誤分類コストに応じて変更した上で通常の学習を行う 誤差関数などモデルの学習手法そのものに誤分類コストを組み込む 今回は3つアプローチの中から、1つ目の閾値を変更する手法について説明していきます。 コスト行列 手法の説明に入る前に、コスト行列について整理しておきます。 クラス数Mの多クラス分類において、モデルが と予測したデータの真のクラスが であった時の誤分類コストを と表すことにします。 例えば二値分類の場合、クラス1を陽性、クラス0を陰性とすると、 は「陽性と予測したが本当は陰性だった」ため偽陽性、逆に は「陰性と予測したが本当は陽性だった」ため偽陰性のコストを表していることになります。 この時、 を要素としてもつM×Mの行列をコスト行列と呼びます。 例えば二値分類の場合は以下のような2×2の行列となります。 真のクラスが0 真のクラスが1 予測したクラスが0 予測したクラスが1 や には正しく分類した時の利益(負の値)が入りますが、実際はこの部分を0として誤分類コストの方に機会損失として織り込むことができます。(後ほど説明します。) ものすごく大雑把な例ですが、先程のカードローン審査の例で申請者 の申請金額を 円とし、金利を1%とした上で、rejectした場合の申請数の減少による効果を1人あたり大雑把に20000円と見積もる(返済できる人の申請数が減る場合は損失とし、返済できない人の申請数が減る場合は利益とします。)と、以下のようなコスト行列を考えることができます。 返済できる人 返済できない人 審査を通す 審査を通さない 20000 -20000 閾値の調整による誤分類コストの反映 コスト行列を定義したところで、閾値を調整する手法の具体的な説明に入っていきます。 大まかには「間違えたらまずい(誤分類コストが相対的に大きい)」クラスについては、その予測確率がたとえ50%を下回っていたとしても、予測結果としてそのクラスを出力するという手法になります。 最適な検出閾値はコスト行列から具体的に以下のように計算することができます。 データ がクラス に属する確率が であった時、コストの期待値が小さい方に分類することを考えます。 式で表すと、 (クラス と予測した時のコストの期待値) (クラス と予測した時のコストの期待値 が成り立つ時、つまり が成り立つ時にクラス =陽性と判別すれば良いことになります。 陽性である事後確率 を とおくと、 から が得られます。 ここで「間違えて分類した時のコストは常に正しく分類できた時のコストよりも大きい」つまり すべての となる に対して、 が成り立つことを仮定します。 この時上の式から とすると、 が得られます。 つまり正しい事後確率 が得られた時、検出閾値を として がそれ以上であれば陽性、そうでなければ陰性とすることで誤分類コストの期待値を最小化することができます。 また の式からコスト行列を 真のクラスが0 真のクラスが1 予測したクラスが0 0 予測したクラスが1 0 とおいたものも同じ結果が得られることがわかります。 これは先程説明した通り、正しく分類できた時に得られる利益を誤分類した時の機会損失として扱うことに相当します。 また、真のクラスにかかわらず同じ予測に対して常に発生する同じ大きさのコストは相殺することがわかります。 実際のデータセットを用いた例 実際のデータセットで閾値の調整によるコストの変化をみてみたいと思います。 今回はUCIのMushroom Classificationのデータセットを検証用に使わせていただくことにします。 https://www.kaggle.com/uciml/mushroom-classification このデータセットは、様々なキノコの傘の形や色などの特徴量と共に、そのキノコが食用なのかそうでないかのラベルが与えられたデータセットとなります。 今回はこれを用いて、キノコのいくつかの特徴量からそれが食用なのかそうでないかを予測する単純な二値分類のタスクを考えます。 分類器としては単純な決定木を使うことにします。 食用でないキノコのクラスを1、食用であるキノコのクラスを0とすると、サンプルサイズは以下のようになります。 クラス0: 4208 クラス1: 3916 必要なライブラリのimportや前処理など from sklearn.preprocessing import LabelEncoder from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn import metrics import numpy as np import pandas as pd import seaborn as sns from matplotlib import pyplot as plt def plot_confusion_matrix (y_test, y_pred, y_prob, cost_matrix): cm = metrics.confusion_matrix(y_test, y_pred) tn, fp, fn, tp = cm.flatten() accuracy = (tp + tn) / (tn + fp + fn + tp) precision = tp / (tp + fp) recall = tp / (tp + fn) f_score = 2 * recall * precision/(recall + precision) fpr, tpr, thresholds = metrics.roc_curve(y_test, y_prob) roc_auc = metrics.auc(fpr, tpr) total_cost = np.sum(cost_matrix * cm.T) print (f "accuracy: {accuracy*100:.4f}%" ) print (f "precision: {precision*100:.4f}%" ) print (f "recall: {recall*100:.4f}%" ) print (f "f-score: {f_score:.4f}" ) print (f "AUC: {roc_auc:.4f}" ) print (f "total cost: {total_cost}" ) df_cm = pd.DataFrame(cm.T, range ( 2 ), range ( 2 )) sns.set(font_scale= 1.4 ) sns.heatmap(df_cm, annot= True ,annot_kws={ "size" : 16 }, fmt= "" ) plt.xlabel( "true label" ) plt.ylabel( "prediction" ) plt.show() df = pd.read_csv( "mushrooms.csv" ) le = LabelEncoder() for k, v in df.dtypes.items(): df[k] = le.fit_transform(df[k]) # 全特徴量を使用すると完璧に分類できてしまうぐらいタスクが簡単なので、今回は実験用に使用する特徴量を制限します df = df[df.columns[: 4 ]] df 混同行列と各種指標 今回の場合、食用でないキノコを誤って食用だと判別して食べてしまった時の被害と、食用のキノコを誤って食用でないと判別して食べ損なってしまった時の被害では前者の方がより重大であると考え、以下のようなコスト行列を設定することにします。 真のクラスが0 真のクラスが1 予測したクラスが0 0 10 予測したクラスが1 1 0 このコスト行列のもとで決定木による分類を行い、まずは閾値を0.5に設定して混同行列やrecision、recallなどの各種指標とともにコストの総計を計算してみます。 cost_matrix = np.array([ 0 , 10 , 1 , 0 ]).reshape(( 2 , 2 )) x_train, x_test, y_train, y_test = train_test_split(df.drop(columns=[ "class" ]), df[ "class" ], test_size= 0.2 , random_state= 0 ) dtc = DecisionTreeClassifier(max_depth= 5 , random_state= 0 ) model = dtc.fit(x_train, y_train) y_prob = model.predict_proba(x_test)[:, 1 ] y_pred = (y_prob >= 0.5 ).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 コスト行列と混同行列の要素積を取ることで得られたコストの総計(total cost)は2201となりました。 次に閾値を先程の に変えて同じように分類を行ってみます。学習する過程ではコスト行列は使用しないため、再学習せずに同じモデルを使用することができます。 threshold = (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ]) / (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ] + cost_matrix[ 0 , 1 ] - cost_matrix[ 1 , 1 ]) y_pred = (y_prob >= threshold).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 結果としてprecisionが下がる代わりにrecallが上がることでtotal costを2201から690まで下げることができました。 一応閾値に対するtotal costをplotしてみると、 がtotal costを最小化する閾値であることが確認できます。 x = np.linspace( 0 , 0.5 , 5000 ) y = [] for i in x: y_pred = (y_prob >= i).astype( int ) cm = metrics.confusion_matrix(y_test, y_pred) total_cost = np.sum(cost_matrix * cm.T) y.append(total_cost) fig, ax= plt.subplots( 1 , 1 , figsize=( 10 , 8 )) ax.plot(x, y) ax.vlines(threshold, ymin= 0 , ymax= max (y), color= "orange" ) ax.legend([ "total cost" , "p*" ], loc= 'upper center' ) ax.set_xlabel( "threshold" ) ax.set_ylabel( "total cost" ) plt.show() 検出閾値と誤分類コスト まとめ 誤分類コストの非対称性の問題と、それに対するアプローチの1つであるコスト考慮型学習(Cost-Sensitive Learning)について紹介させていただきました。 BASEで活用されている機械学習モデルの一部にも、こうしたコスト考慮型学習のアプローチが用いられています。 実際には正確なコスト行列を設定することが難しい場合もありますが、それでも誤分類コストの非対称性については常に意識する必要があります。 例えばローン審査において機械学習モデルを人手によるチェックのための一時フィルター的な役割で使用する場合には、あらかじめ許容できる件数ベースの偽陽性率の上限を決めた上で、金額ベースの再現率が最も高くなるような閾値を設定する、といった方法が有効かもしれません。 その場合はモデルを開発するエンジニアだけではなく、二次チェックを行うオペレーターともうまくコミュニケーションを取りながら達成すべき目標を明確にしていくステップが必要不可欠となります。 また、実際に運用していく上では誤分類時のコストだけではなく、冒頭で触れた通り計算コストやメンテナンスコスト、解釈性の問題など色々な要素が影響してきます。それらを踏まえた上でドメイン知識を活用し、短期的な利益だけではなく、長期的な利益を見据えて最適なモデルを選択しチューニングしていくことが、ビジネスでの機械学習モデルの活用を求められるデータサイエンティストの役割であると考えます。 参考文献 Elkan, Charles. (2001). The Foundations of Cost-Sensitive Learning. Proceedings of the Seventeenth International Conference on Artificial Intelligence: 4-10 August 2001; Seattle. 1. Ling, Charles & Sheng, Victor. (2010). Cost-Sensitive Learning and the Class Imbalance Problem. Encyclopedia of Machine Learning. 特に現実の応用例を考える際、誤分類コストの非対称性の問題は、必ずと言っていいほど不均衡データの問題と一緒になって現れますが、個人的にはこの二つの問題は分けて考える方が良いかと思っています。というのも不均衡データには不均衡データ特有の、分類器の識別境界にかかるバイアスなど( https://scikit-learn.org/stable/auto_examples/svm/plot_separating_hyperplane_unbalanced.html や https://ieeexplore.ieee.org/document/6137280 などで説明されている)タスクのドメイン(医療診断で使うのか、ローン審査で使うのかなど)とは独立した問題が存在し、誤分類コストが対称であっても生じる可能性がある一方で、誤分類コストの非対称性の問題はタスクのドメインに依存した問題であり、不均衡データでなくても起こり得るからです。ただし、不均衡データの問題を解決するために非対称な誤分類コストを設定したり、誤分類コストの非対称性を考慮するためにunder-samplingやover-samplingなどによって敢えて不均衡な事前分布を設定する(この記事では詳しく紹介できませんでしたが Cost-Sensitive Learningの手法 の2つ目に相当します。)アプローチが取られることはあります。今回の記事ではトピックを絞るため、あまりデータの不均衡性の問題は取り上げていません。(今後機会があれば、2つ目以降の手法と共に記事にしようかと思います。) ↩
この記事は BASE Advent Calendar 2021 の22日目の記事です。 こんにちは!BASEでエンジニアをやっている大津( @cocoeyes02 )です。今回はiikanji-conference-toudanチームの取り組みについてご紹介します! iikanji-conference-toudanチームとは? iikanji-conference-toudanチームとは、BASEから技術イベント・カンファレンスへ登壇する人たちを応援するチームです。登壇を有志による属人的なものにするのではなく、文化として当たり前になっている状態を目指して活動しております。 もともと登壇自体は前から推奨されていましたが、「BASE全体で外部への技術的なブランディングをしっかりやってきたい」という話があり、 iikanji-conference-toudanチームが生まれました! ちなみにiikanji-conference-toudanチーム以外でも、 ブログ や 自社イベント 、 Youtube など精力的に活動していますのでそちらもどうぞ! これまでどんなことしてきたの? チームで応援できそうなことを調査 応援と一口に行っても、様々な応援の形があるかと思います。そこで登壇する人を増やすために我々に何ができるのかをブレストし、まとめました。 ブレストの内容をグルーピングしたり抽象化した様子 特に、登壇のモチベーションのところを深掘りしたいという話になり、実際に登壇に対してどう思っているのかアンケートを取りました。 すると圧倒的に「登壇するネタがない」と思っている人が多かったため、「ブログや社内ドキュメントの内容をベースに登壇を進めたら、登壇してくれるのではないか?」という仮説を立て、登壇を勧めました。 アンケート結果の一部 実際この方法で何人か登壇した人を見たので、登壇を勧めるときの1つの方法として確立しつつあります。 一例として、 @tawamura さんの ElasticsearchとKibela APIを使ってSlackでのCSお問い合わせ対応業務を改善した話 がその1つになります。よければブログや登壇動画も是非ご覧ください! ちなみに、このPHPカンファレンス2021ではBASEから計5人が登壇しました!その時のイベントレポートは以下の記事になります。こちらも是非ご覧ください! devblog.thebase.in 登壇前のトークへのフィードバック より良い登壇ができるように、登壇前にzoom等でトークを聞いてもっと良くなる点などがあればフィードバックします。 例えばトークを聞きながら、以下のようなことについてフィードバックしています。 スライド全体が頭に入ってきやすい構成になっているか スライド等の資料で分かりにくいところは無いか 図や動画を使った方が良いところは無いか 時間オーバーしているのであれば、どこを削るべきか 喋りのスピードが早くて聞いている側が負担にならないか など 技術イベント・カンファレンスでの登壇は、大抵スライド等の資料を使いながらトークする形になります。なので、スライド等の資料のフィードバックが中心になります。 技術イベント・カンファレンスのスポンサー業務 技術イベント・カンファレンスでスポンサーとして名乗り出るのにはさまざまな理由がありますが、登壇している人のモチベーションアップやトークを聞いてもらうきっかけとしてスポンサー業務もやっています! また、カンファレンスによってはスポンサーセッション枠が設けられているスポンサープランもありますので、そちらも積極的に狙っていきます。 例えばPHPカンファレンス2021では、 @tanden さんを中心として「 BASE のお問い合わせ対応の裏側!」という座談会形式のスポンサーツアーの企画・開催をしました。その時の様子は以下の記事に書いてあります! devblog.thebase.in アーカイブ動画をみんなで見る会を開催 近年の技術イベント・カンファレンスでは、各トークのアーカイブ動画がアップロードされていることも珍しくありません。 登壇だけでなく、技術イベントの参加自体もっとわいわいしていきたい。そんな思いから各トークのアーカイブ動画をみんなで見る会の開催もしました! PHPカンファレンス2021のアーカイブ動画を見る会をSlackで募った様子 これからはどんなことをしていきたい? 色んな分野技術イベント・カンファレンスでの登壇を応援していきたい 現在登壇人数で言うと、PHPやGoのカンファレンスでの登壇が多い状況です。ですが、技術イベント・カンファレンスはPHPやGo以外にもたくさんありますので、色んな分野での登壇を応援していきたいと思っています! どの分野の技術イベント・カンファレンスでも日々登壇するチャンスを狙える体制づくりとして、例えば現状技術イベント用のカレンダーを用意して、色々な技術イベント・カンファレンスの日程を追えるようなものを作成しています! 登壇者だけでなく、技術イベントの参加自体ももっとわいわいして行きたい 登壇する人だけにフォーカスするのではなく、技術イベント自体もわいわいして登壇している人のモチベーションアップ&登壇へのハードルを下げるのも狙っていきたいと思います! 技術イベント・カンファレンスを楽しむための施策として、例えば定期的に技術イベントのアーカイブ動画を見る会を開催したりし始めています! 最後に もともとBASEでは、会社全体でOSS(コミュニティ)を応援する文化があり、この記事を書いた直近ではThe PHP Foundationに寄付を行いました。 devblog.thebase.in 我々のチームでは、技術イベント・カンファレンスに登壇して盛り上げていくという形で、OSS(コミュニティ)に貢献できればと思っております。引き続き、色々なアプローチで技術イベント・カンファレンスでの登壇を応援していきます! OSS(コミュニティ)に貢献したい!盛り上げていきたい!と思う方は、ぜひBASEへどうぞ! open.talentio.com 明日は @FUJIIMichiro さんの「UXライティング関連」と、 @shotakeuchi さんの「分類コストを考慮した機械学習モデルの考え方」です!お楽しみに!
この記事は BASE Advent Calendar 2021 の22日目の記事です。 はじめに はじめまして、Owners Success Frontend Shop Frontチームの坂口です。 普段はフロントエンドエンジニアとしてVue.jsを使った開発をメインに行なっているのですが、チームでプロジェクトマネージャーやデザイナーが手軽に動作を確認できるレビュー環境がほしいという話があり、AWS App RunnerとGitHub Actionsを連携して構築をしたのでその話をしたいと思います。 レビュー環境とは レビュー環境というのはGitHubのプルリクエストやブランチごとに動作確認ができる環境で以下のような動きをするものを今回は構築することにしました。 プルリクエストが作成されるとレビュー環境立ち上げ (作成) プルリクエストに紐付いたブランチが更新されるとレビュー環境更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) イメージとしてはHerokuの Review Apps やNetlifyの Deploy Previews などに近いかもしれません。 使用技術 AWS App RunnerとGitHub Actionsを利用して以下のような構成にしました。 AWS App Runner AWS App Runnerは今年5月に発表されたコンテナベースのフルマネージドサービスで、インフラの知識があまりなくても簡単にアプリケーションをデプロイすることができます。 https://aws.amazon.com/jp/apprunner/ 今回構築するにあたってAWS LambdaやAWS ECSなど選択肢はありましたが、簡単にデプロイできるということで採用しました。 準備 事前にGitHub Actionsで利用するAWSのIAMユーザーを作成して、リポジトリのsecretsにアクセスキーとシークレットアクセスキーを用意しておきます。 必要な権限は こちら の公式記事で書いてあるものになります。 ワークフロー内容 ワークフローは先ほど上げた作成、更新、削除に加えて、一時停止、復帰を追加した以下5パターンを用意しました。 プルリクエストにラベルがつくとレビュー環境立ち上げ (作成) ラベルが付いたプルリクエストへ新たにpushされると更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) 毎日21時になると稼働中のレビュー環境を一時停止(一時停止) プルリクエストの /resume とコメントすると復帰(復帰) プルリクエストにラベルがつくとレビュー環境立ち上げ name : review-app-deploy on : pull_request : types : - labeled jobs : deploy-review-app : name : Deploy Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.label.name == 'ラベル名' }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Login to Amazon ECR id : login-ecr uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo review-app_$IMAGE_TAG | cut -c 1-40` docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=service::$SERVICE_NAME" echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name : Deploy to App Runner id : deploy-service uses : awslabs/amazon-app-runner-deploy@main with : service : ${{ steps.build-image.outputs.service }} image : ${{ steps.build-image.outputs.image }} access-role-arn : ${{ secrets.APPRUNNER_ROLE_ARN }} runtime : NODEJS_16 region : ${{ secrets.AWS_REGION }} cpu : 1 memory : 2 port : 80 wait-for-service-stability : false - name : Comment Review App URL run : | SERVICE_URL=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${{ steps.build-image.outputs.service }}']" | jq -r '.[]["ServiceUrl"]' ` gh pr comment $PR_NUMBER --body "Review App URL: https://$SERVICE_URL" 大まかな流れは以下になります。 Dockerイメージビルド Amazon ECRへDockerイメージプッシュ AWS App Runnerのサービス作成 今回、プルリクエストのブランチ名をDockerイメージのタグとAWS App Runnerのサービス名に利用しているのですが、AWS App Runnerのサービス名の 制限 に合わせて文字列を置換しています。 またあとから識別しやすいようにサービス名に review-app_ プレフィックスを付与しています。 IMAGE_TAG = ` echo $BRANCH_NAME | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_NAME = ` echo review-app_ $IMAGE_TAG | cut -c 1-40` サービス作成では、AWS公式GitHub Actionsである aws-actions/configure-aws-credentials を利用していて、CPU、メモリは最低限にしています。 wait-for-service-stability を true にするとサービス作成完了まで待ってくれるのですが、完了まで5分ほどかかってしまいGitHub Actionsの枠を使い切ってしまうことを懸念して false にしています。 さらにAWS App RunnerのURLをプルリクエストのコメントに通知するようにしています。(※サービス作成完了後ではないので、利用可能までに5分程度待機が必要です。) SERVICE_URL = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' ${ { steps.build-image.outputs.service } }'] " | jq -r ' .[]["ServiceUrl"] ' ` gh pr comment $PR_NUMBER --body " Review App URL: https:// $SERVICE_URL " 以下のように特定のラベル(今回は deploy review app )を付与するとワークフローが走り、サービスURLをコメントしてくれるようになります。 ラベルが付いたプルリクエストへ新たにpushされると更新 name : review-app-update on : pull_request : types : - synchronize jobs : update-review-app : name : Update Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo "review-app_$IMAGE_TAG" | cut -c 1-40` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='RUNNING']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=image-tag::$IMAGE_TAG" echo "::set-output name=service-arn::$SERVICE_ARN" - name : Login to Amazon ECR id : login-ecr if : ${{ steps.check-service-exist.outputs.service-arn }} uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image if : ${{ steps.check-service-exist.outputs.service-arn }} env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} . docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} - name : Update App Runner Service id : update-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | aws apprunner start-deployment --service-arn ${{ steps.check-service-exist.outputs.service-arn }} 大まかな流れは作成とほぼ同じになりますが、注意点として aws-actions/configure-aws-credentials 内部でサービスが存在する場合は更新コマンドを実行しているのですが、更新コマンドはサービスの設定を更新してくれるのみでソースを更新してはくれないため、そのまま利用せず、 start-deployment を実行する必要があります。 aws apprunner start-deployment --service-arn ${ { steps.check-service-exist.outputs.service-arn } } また細かいですが、 jq では -r オプションをつけて結果の文字列からダブルクウォートを削除して扱いやすくしています。 SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '&&Status=='RUNNING'] " | jq -r ' .[]["ServiceArn"] ' ` 毎日21時になると稼働中のレビュー環境を一時停止 name : review-apps-pause on : schedule : - cron : '0 12 * * MON-FRI' jobs : pause-review-app : name : Pause Review App runs-on : ubuntu-latest environment : development steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Pause App Runner Services id : pause-services run : | SERVICE_ARN_LIST=`aws apprunner list-services --query "ServiceSummaryList[?Status=='RUNNING']" | jq -r '.[] | select(.ServiceName | test("^review-app_")) | .ServiceArn' ` if [ -n "$SERVICE_ARN_LIST" ] ; then echo $SERVICE_ARN_LIST | while read SERVICE_ARN ; do aws apprunner pause-service --service-arn $SERVICE_ARN done fi AWS App Runnerはサービスが起動している間課金されてしまうため、深夜の誰も利用しない時間では一時停止するようにしています。タイムゾーンはUTCなので注意してください。 on: schedule: - cron: ' 0 12 * * MON-FRI ' 一時停止するサービスはAWS CLIの --query オプションで稼働中のものを抽出した後、jqでサービス名が review-app_ プレフィックスのものを絞り込んでいます。 SERVICE_ARN_LIST = `aws apprunner list-services --query " ServiceSummaryList[?Status=='RUNNING'] " | jq -r ' .[] | select(.ServiceName | test("^review-app_")) | .ServiceArn ' ` プルリクエストに /resume とコメントすると復帰 name : review-app-resume on : issue_comment : types : [ created, edited ] jobs : resume-review-app : name : Resume Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/resume' ) }} env : GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | BRANCH_NAME=`gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName' ` SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='PAUSED']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=service-arn::$SERVICE_ARN" - name : Resume App Runner Service id : resume-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | resume-service --service-arn ${{ steps.check-service-exist.outputs.service-arn }} issue_comment をトリガーにしており、作成や更新のように github.head_ref でブランチ名が取得できないため、コメントされたプルリクエストからブランチを特定するための処理を挟んでいます。 BRANCH_NAME = `gh pr view ${ { github.event.issue.number } } --json headRefName --jq ' .headRefName ' ` 以下のようにコメントすることでワークフローが走り、一時停止中のAWS App Runnerサービスが復帰してくれます。 プルリクエストがclose、またはマージされるとレビュー環境削除 name : review-app-delete on : pull_request : types : - closed jobs : delete-review-app : name : Delete Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Delete App Runner Service id : delete-service run : | SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME']" | jq -r '.[]["ServiceArn"]' ` if [ -n "$SERVICE_ARN" ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo "::set-output name=tag::$SERVICE_NAME" fi - name : Delete ECR Image id : delete-ecr-image if : success() env : ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | TAG=`echo "$BRANCH_NAME" | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` aws ecr batch-delete-image --repository-name $ECR_REPOSITORY --image-ids imageTag=$TAG 削除したいサービスの存在確認をして、サービスとDockerイメージを削除します。 SERVICE_NAME = ` echo " review-app_ $BRANCH_NAME " | cut -c 1-40 | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '] " | jq -r ' .[]["ServiceArn"] ' ` if [ -n " $SERVICE_ARN " ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo " ::set-output name=tag:: $SERVICE_NAME " fi おわりに 今回は公式でGitHub Actionsが用意されているのもあって比較的簡単に連携・レビュー環境の構築ができました。まだサービス作成完了時の通知など改善の余地はたくさんありますが、手軽に構築ができるので、ぜひ一度検討してみてはいかがでしょうか。
この記事は BASE Advent Calendar 2021 の21日目の記事です。 devblog.thebase.in はじめに Payment Devグループの山本( @msysyamamoto )です。 この記事は私たちのグループで行っている読書会の紹介になります。これから読書会を開催しようとしている方や、いまの読書会を改善したいと思っている方の参考になれば幸いです。 こんな感じで行っています 参加者 基本的にはグループのメンバは全員参加になっています(もちろん、業務が忙しい場合などは欠席することは可能です)。現在は14名のメンバが参加しています。 グループの読書会となってはいますが、別のグループからの参加も可能だったりします。 体制 グループのメンバの中の一人が読書会のまとめ役となります。まとめ役は立候補制になっています。もし、立候補する方がいなかった場合は、グループのマネージャからの指名になります。 スケジュール 毎週1回、同じ曜日同じ時間に開催しています。時間は1回あたり1時間となっています。いつも同じ時間に開催することでリズムが生まれ、読書会を続けていきやすくなるのではないかと個人的には思っています。 場所 Zoom で行っています。 読書会は録画をしており、参加できなかった方は後でどのような内容だったかを確認できます。 選書の方法 まとめ役に委ねています。 多くの場合、まとめ役がいくつか本をピックアップし、参加者がそれに投票し読む本を決定します。ピックアップされた本以外で読みたい本があれば、まとめ役でなくとも選択肢に本を追加することができます。 まとめ役に委ねていますので、まとめ役が読書会で読みたい本があれば投票を経ずとも読む本を決定できたりもします。 参考までに今まで読書会で読んできた本を紹介します。2021年12月現在は『DNSがよくわかる教科書』を読み進めているところです。 リーダブルコード オブジェクト指向設計実践ガイド Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 テスト駆動開発 失敗から学ぶRDBの正しい歩き方 Web API: The Good Parts 決済サービスとキャッシュレス社会の本質 DNSがよくわかる教科書 本の難易度やページ数にもよるのですが、今までの実績からして大体1ヶ月から2ヶ月で1冊の本を読み終えています。 会の進め方 事前にやっておくこと 読む本が決定した時点で、読む範囲の決定と、その範囲は誰が発表者になるかを決定しておきます。発表者は1回の会で5人となっています。発表者はランダムで決定されるのではなく、参加者全員が同じくらいの回数発表者になるように決めています。 発表者は会が始まる前まで該当範囲を読み、気づき・疑問を弊社でドキュメントサービスとして利用しているKibelaに書いておきます。要約は書きません。 発表者でない方も事前に該当範囲を読んでおきます。 その会の司会者を決めます。司会者はボットによってランダムに決定されます(まとめ役 ≠ 司会者 となっています。ランダムなのでまとめ役が司会者になる場合もありますが)。 読書会本番 発表者がKibelaに記述した気づき・疑問を説明し、それに対してディスカッションして行きます。発表者は5人ですが、この進め方で毎回ちょうどよい時間に終わるか、少しオーバーするくらいの時間に収まっています。 以上が私たちのグループで開催している読書会の現在時点でのやり方になります。私が読書会に参加するようになったのは2021年7月なのですがその時とのやり方と現在のやり方を比べると、改善されている部分があります。 例えば発表者の決定方法ですが、以前のやり方はランダムで決定していたのですが、連続で発表者になったりする人が出たりするのでよくないよね、ということで今の方法に改善されました。 今のやり方がベストだとは思い込まずに、改善できそうなところはどんどん改善していき、よりよい読書会にできるといいと思っています。 よかったこと この記事の締めくくりとして、私個人の感想にはなるのですが、読書会でよかったと思うことを紹介します。 自分では読まなさそうな本を読むことができる 具体的に言いますと『決済サービスとキャッシュレス社会の本質』がそれにあたります。正直なところ、読書会で読む本のとしてピックアップされるまでその本を知りませんでしたが、決済に関わる仕事をしている身として、興味深く読み進めることができました。 自分とアンテナの張り方が違う人が集まることで、自分の知らなかった本を読むこと・知ることができるので、知識の幅を広げるのにいいと思いました。 コミュニケーションの場になる 私たちのグループは全員が同じプロジェクトを進めているわけではありません。同じプロジェクトに参加しているメンバとはよくコミュニケーションを取るのですが、別のプロジェクトに参加しているメンバとはそうではありません。 また、新型コロナウィルスの影響によりリモートで仕事をする機会が増え、グループのメンバと顔を合わせる機会が減ってきています。 それゆえ、同じグループの中でもお互いのことをよく知らないということがあるのですが、読書会がお互いのことを知るよい場所になっていると思います。
この記事は BASEアドベントカレンダー 21日目の記事です。 まえがき BASE BANK株式会社でエンジニア兼Engineering Program Managerをやっている 松雪( @applepine1125 ) と 永野( @glassmonekey ) です。 BASE BANKでは組織の拡大に伴って表出した課題を解決するために、プロダクトのデリバリー、クオリティに責任を持つEngineering Program Manager(以下EPM)という役割を導入しています。 今回はまだ馴染みのないであろうEPMについてと導入の背景、具体的な働きざまについてご紹介します。 Engineering Program Manager(EPM) とはなにか そもそもEPMという役割自体が日本で馴染みのないものだと思います。 Apple や Amazon 、 Meta(旧FaceBook) 社などで正式なポジションとして存在しており、ざっくり各社のjob descriptionから要素を抽出してみると、主に以 下のような役割を担っていることがわかります。 開発のリードや開発プロセスの改善 設定したリリーススケジュールへの責任とプロダクトのクオリティの担保 プロジェクト、プロダクトの技術的な阻害要因の排除 ステークホルダーと適切なコミュニケーションを取り、自身での意思決定や意思決定のエスカレーションの実施 Product Manager(以下PdM)や外部のステークホルダーと開発チームの間に入り、適切に開発のサイクルを回していくために必要な役割です。 組織によってはこれらの役割をProject Manager(以下PjM)、Engineering Manager(以下EM)、Tech Lead(以下TL)といった役割が担っていたり、もしくは名も無き仕事として誰かが対応しているのではないでしょうか。 PjM、TL、EMとEPMの違い PjM、TL、EMといった、一見EPMと近い役割がすでに存在する中でEPMという役割が新たに生まれたのはなぜでしょうか、PjM、TL、EMとEPMとでは何が違うのでしょうか。 それぞれ重なる部分もありますが、役割としては微妙に異なります。それぞれ見比べてみましょう。 PjMとEPM PjMはPJの完遂に責任を持つ役割です。 PJの要件定義や進捗管理、メンバー集めや、場合によっては予算を獲得してくるなど、PJ完遂のために様々な業務を行います。 EPMとの一番の違いは、技術的なバックグラウンドがなくてもPjMの役割を担える点です。 PjM自身が技術的なバックグラウンドを持っていたり、メンバー内にそういった判断材料を揃えることができるエンジニアがいると開発に関する意思決定がしやすく比較的円滑にPJが進行します。 しかしそういったコミュニケーションや意思決定ができないとPjMはただエンジニアが提示する開発スケジュールを飲み込むしか無く、根本原因にテコ入れできずにスケジュール遅延が発生するいわゆるデスマーチへと突入していってしまいます。 こうならないようにPjMの意思決定を補佐し、協力してプロダクトのリリースを行うEPMのような存在が重要になってきます。 組織によってはPjMを開発面から補佐する役割はTLが担うというケースも多いかもしれません。 TLとEPM 組織によってTLに期待する役割は様々だと思いますが、どの組織でも割と共通しているのはコード品質や設計、アーキテクトの面でチームや組織に影響を及ぼす存在であることかと思います。 EPMでもそういった側面を求められる場面はありますが、そこを主戦場とするメンバーがすでにいるのであればそのメンバーに役割を委譲しつつ、連携しながらプロダクトのデリバリーへの責務を全うするのがEPMの大きな特徴です。 EMとEPM EMは組織によってTL以上に多様な役割を持っています。 参考に、広木氏が提唱する マネジメントの4象限と強め/弱めのEMの定義 と照らし合わせてみると、 主にPeople, Team Managementを行う弱めのEMとEPMは補完関係になるのではないでしょうか。 採用や評価、日々の1on1なども含め、仕事を通した各メンバーの成長やチーム作りと向きあうEMと、メンバーそれぞれのスキルを最大限に生かしてプロダクトのデリバリーとクオリティを担保するEPMとでは、その役割の中で向き合う要素は似ているものの最終的な責任が異なります。 この両方を一人で担うのはスイッチングコストがとても高く難しい仕事となってしまいます。 下図のように、強めのEMがいれば弱めのEM、EPM含め様々なマネージャ業を一手に引き受けてもらうことはできますが、そういったスーパーマンはそうそういないため、弱めのEMとEPMそれぞれで責務を分割してフォーカスするのは有効な打ち手なのではないでしょうか。 なぜEPMが必要なのか ではそのようなEPMはどのようなケースで必要になるのでしょうか? 一つ考えられるケースとしては「目的別組織」のマネジメントの場合ではないでしょうか。 目的別組織とEPM 組織形態としてよく比較されるのが"機能別組織(職能別組織)"と"目的別組織(職能横断型組織)"です。 機能別組織の場合、PJ毎に各チームからメンバーが集まり、PJが終わると解散、各々別のPJへ・・・というのがよく見る流れではないでしょうか。 こういった組織運営を行っている場合、わざわざEPMなどを配置せずとも、PjMやEM、TLがEPMの役割も担うことでとりあえず目の前のPJを完遂させる事はできると思います。 しかし目的別組織になると、自分が所属するチームが受け持つ機能、プロダクト、領域と継続的に向き合えるようになります。すると開発プロセスやステークホルダーとの関わり方の改善も半永久で継続的に行う必要があったり、チームの経験を形式知として蓄積しやすくなるため、それらの活動を主な責務とするEPMを配置することは合理的ではないでしょうか。 特に特定の領域と継続的に向き合い価値提供していくためには、その領域の深いドメイン知識も必要になってきます。仮に開発プロセスやコミュニケーション力が高かったとしても、深いドメイン知識がないと適切なエスカレーションや開発スコープの変更判断などができないためです。 BASE BANKがEPMを据えた背景 ここからは具体例の一つとしてBASE BANKがEPMを据えた理由とBASE BANKでのEPMの働きざまについてもう少し深掘ってみます。 BASE BANKはショップオーナーが売上を立てたあとの活動に関わる機能やプロダクトを受け持っているチームです。 またBASE BANK社を設立した2018年はまだBASE社は目的別組織ではなかったため、BASE BANKはある意味BASE内での実験的な目的別組織(チーム)としてチームを成長させてきました。 そういった事も踏まえ、メンバーが増え受け持つプロダクトも増える中で、上記のようなEPMとしての責務を持つ役割が必要となったのは必然とも言えました。 BASE BANK黎明期 BASE BANKの立ち上げ期はざっくりエンジニアとPMが存在しており、明確に役割を細分化しなくても開発を回していける規模でした。 メンバーが増え、よりチームらしく エンジニアメンバーがjoinし、人数が少しづつ増え、受け持つ機能やプロダクトも増えててくると、ある程度意識して開発プロセスや採用、評価を回していく必要が出てきました。 そこでTeam Managementを行うEM、TLといった役割が生まれ、PMと協力してTeam Management、People Managementを行うようになりました。 このときはまだ、各機能の開発の運営はTLやPMが並行して受け持つ形になっていました。 ピザ2枚 を分け合う以上の規模に 更にメンバーの採用が進み、関わるプロダクトもYELL BANK、BASEカード、振込申請などさらに増え、各プロダクトのグロースにも本格的に着手しなければならなくなりました。 すると、それまで並行して複数プロダクトの開発プロセスを主導していたEMやPdMが本来の責務である組織全体の事業推進やTeam、People Managementに注力せざるを得なくなり、各プロダクトの開発プロセスの主導やPdM、各ステークホルダーとの橋渡し役として開発を主導するEPMという役割が生まれました。 ちなみに、BASE BANKのエンジニアは各々の強みを活かしつつ開発の全てのライフサイクルに関わる フルサイクルエンジニア としての能動的な働きを求められます。 そのため、EPMは主導といってもマイクロマネジメントをするわけではなく、開発チームとしての目的やビジョンを掲げつつ各メンバーと協力して日々の運営や改善をしたり、メンバーがより能動的に動けるようなお膳立てをします。 それぞれのプロダクトごとのEPMとしての役割 では、EPMのプロダクトごとの具体例を紹介します。 現時点では、主にBASE BANKとしては BASEカード と YELL BANK がメインのプロダクトです。 決済のドメイン知識を持つ松雪( @applepine1125 )がBASEカード、 元々YELL BANKの開発をしていた永野( @glassmonekey )がそれぞれEPMとして開発を主導しており、それぞれのEPMの役割について紹介します。 BASEカードにおけるEPM BASEカード のEPMとして、現在以下のような仕事を行っています。 - スプリントの運営 - QAの仕込み - 社内外のステークホルダーとのコミュニケーション - 関係各所へ決済の知識やBASEカードの仕様をインプット - 意思決定のための技術面または仕様面(特にカード決済の知識)の判断材料の提供、場合によっては自身で意思決定 https://cp.thebase.in/basecard cp.thebase.in BASEカードはショップの出金に直接関わる機能のため、内部統制などを責務とする Corporate Solutions Engineer(CSE) やリスクマネジメントチームとの関わりが多く、日頃から密にコミュニケーションを取っています。 また、機能実現のために外部企業が提供しているAPIを利用しており、仕様面の調整や、精算上の証跡を用意するためにBASEの経理との連携も密に行う必要があります。 BASEカードの場合、リアルカードの発行など直近の大まかな開発マイルストーンが割と明確であることから、 実装したい各機能のスケジュール上での優先度をPdMと議論、調整し、開発をすすめる上で発生する技術的な阻害要因を排除すること がEPMとして重要になります。 YELL BANKにおけるEPM 簡単にYELL BANKというサービスを紹介しておくと、BASEを活用されてるショップオーナーの皆様へ簡単に資金調達を提供するサービスです。 thebase.in 最近はスタートアップ企業の隆盛もあり、VCなどから資金調達をすることも馴染み深くなりましたが、BASEのようなロングテールの規模感だとまだまだ未知の市場です。 そのため、YELL BANKの開発では 少しずつ小さな仮説検証すること を重きにおいて開発サイクルを回しています。 特に機能をデリバリーするときはできる限りユーザーストーリー作成し、機能と価値をセットで考えられるように気をつけています。特に以下を意識してメンバーと一緒に作ります。 不確実性はどこになるあるのか? 仮説検証したい内容はどこか? 機能を横断的に作る メンバーと一緒に作ることで、メンバー全員がゴールを意識すること、EPM以外もオーナーシップを持って開発をすることに繋がると考えています。 またバックエンド・フロントエンドで区切るのではなく、横断的に作ることで手戻りはできる限り少なくします。 ユーザーストーリーの分割に関しては アジャイル開発におけるユーザーストーリー分割実践 〜画面リニューアルの裏側〜 をご覧ください。 devblog.thebase.in また、仮説検証を実践するにあたって作っては放置ではいけません。そこでPdM/PMMがPDCAを回しやすくするためのデータ基盤の整備もしています。 過去の実践した事例に関しては Google Apps Script× BigQuery × Googleスプレッドシート × データポータルで簡易CRMを作ってみた や Lookerでショップのサービス活用カルテを作成した話 をご覧ください。 devblog.thebase.in devblog.thebase.in BASE BANKとしてのEPMのこれから 一言にEPMと言っても、プロダクトの特性によって役割や重点の置き方が変わってきます。 今の役割の定義は来年には変わってることも全然あるでしょう。正直模索中です。 現在のEPMはプロダクトのデリバリーやクオリティにフォーカスした役割です。 ただ、プロダクトの成功を目指すためにはそれを支えるメンバーの中長期的な成長も重要な要素です。 そのため今後の可能性としてはメンバーに協力してもらうだけでなく、それを各メンバーの評価として還元するためにPeople Managementも担うようになる可能性も大いにあるでしょう。 おわりに これから更にプロダクトのデリバリー速度やクオリティを高めていくには、現メンバーの成長だけでなく、より多くのメンバーの力が必要です。 BASE BANK自体や各プロダクトへの興味でもEPMへの興味だけでもなんでもいいのでぜひ気軽にお話しましょう! open.talentio.com
この記事は BASE Advent Calendar 2021 の20日目の記事です。 はじめに こんにちは。Owners Experience Backend Group の杉浦です。主にサーバーサイドのアプリケーションの実装をしています。 エンジニアにとって、外部企業から提供されるAPIやCDN(Content Delivery Network)といった『外部サービス』をどう扱うかは悩ましい問題です。 特にシステムの設計段階において「外部サービスをどうやって内部システムに組み込むのか?」という方針は、その後のアプリケーションの生産性に大きく影響します。 仮に、密結合に設計してしまうと「外部サービス」という不確実性に影響されやすくなるため、好ましい状況とはいえません。どのように疎結合を実現するのか?という設計が、外部サービスから不確実性をハンドリングする生命線になります。 そこで、この記事では、2021年11月にリリースを行なった「Akamai Image Video Manager の導入プロジェクト」を例に、外部サービスの不確実性を最小化する「設計戦略」の実践方法を提示します。 本稿は3章立てになっています。 第1章:外部サービスの不確実性という問題 第2章:設計戦略のプランニング 第3章:設計戦略を実行するチームマネジメント 第1章では、外部サービスの不確実性について考察しています。内部サービスがミクロな不確実性への対処が主軸になるのに対して、外部サービスの不確実性がよりマクロな範囲、特に外部企業の経営方針の影響を受けやすいという点に言及しました。 第2章では、不確実性を最小化するための設計戦略を提示します。疎結合な設計はもちろん、外部企業の経営状況を読み解くために財務状況(PL/BS)を踏まえる必要性を提示しました。 第3章では、Akamai Image Video Manager の導入プロジェクトを例に、設計戦略を遂行するためのチームマネジメントについて言及しています。外部サービスの導入・移行プロジェクトには技術的な知識が不可欠なことから、エンジニアである筆者がチームを取りまとめた実践例をとなります。 第1章:外部サービスの不確実性という問題 良い設計をするための1つの重要な論点として「不確実性の最小化」があります。 ここで重要なのが、内部のシステムに対する「不確実性」と、外部のシステムに対する「不確実性」は、それぞれ「似て非なる存在」ということです。 内部システムにおける不確実性は、社内のエンジニアでなんとか対応できる範囲が多いと言えます。あくまで、社内における不確実性に対峙するため、その対処も社内で完結しやすいためです。 一方、外部システムは「存在そのものが不確実性に満ちている」といえます。期待したレスポンスが返却されるか?というミクロな不確実性に加えて、それとは別次元のマクロな不確実性を伴います。 例えば、外部システムが「3年後にもサービスを継続している」という保証はどこにもありません。加えて「値上げをしない」という確証も(契約に入れない限りは)どこにもないのです。 つまり、外部システムの不確実性は、干渉できない範囲に及ぶため、これらの不確実性を考慮した設計を考えることは容易ではないといえます。 第2章:設計戦略のプランニング なぜ戦略なのか? 外部サービスの不確実性に対処するためには、単なるアプリケーションの設計はもちろんですが、その大前提となる「設計戦略」が鍵を握ります。 あえて「戦略」という仰々しい単語を使う理由は、その影響力の時間軸が長いからです。戦略を間違えると、リリース直後は問題が発生しなかったとしても、数年後に苦労します。出発点である戦略を間違えた場合、改修コストへの投資が必要になりますが、こうなると当然、ROIも低下するため、経営的にも好ましくありません。 つまり、方向性を間違えることは将来にわたってビジネスに好ましくない影響を与えることを意味するため、設計の上位概念である「設計戦略」が必要になるということです。 取り替え可能性を考慮して設計する 「外部サービスの取り替え可能性」を考慮した設計こそが、不確実性の最小化にあたって有効な方針となります。 取り替えが可能であれば、別の代替サービスへの移行や、代替機能の開発(内製化)が容易になります。そして、仮に、外部サービスで「値上げ」や「サービス停止」といった不確実な事態が発生した場合に、影響を最小限に抑える最後の砦になります。 ただし、この設計方針は、競合製品が存在する「コモディティーな市場」でしか取り得ない選択肢になります。 逆に、代替が存在しない「独占的なサービス」を利用する場合は、価格決定権は外部サービスの側にあります。さらに、代替サービスの登場は考えにくいため、取り替え可能な状態にする必然性に乏しいといえます。 この意味で、外部サービスの利用を前提とした設計戦略を考える上では、外部サービスのビジネス上の競争環境や、経営状況の見極めが必要になります。 このうち、外部サービスの経営状況は、財務の分析によって明らかにできます。米国企業であれば10K(Security Report)、日本企業であれば有価証券報告書が、そのサービスの置かれた競争環境を示す材料になります。 今回のプロジェクトではAkamai(証券コード:NASDAQ: AKAM)が提供するサービス「Akamai Image Video Manager」の利用を前提としたため、Akamaiが公表した2021年の10Kについて、ざっくりと目を通しました。 https://www.ir.akamai.com/sec-filings 責務を1つのリポジトリに集約する 今回の「Akamai Image Video Manager の導入プロジェクト」では、画像配信基盤を「取り替え可能な状態」にするという設計戦略を採用しました。 具体的な実現方法は、外部サービスを利用した画像配信に関する責務を、1箇所のリポジトリに集約するということです。 従来のBASEにおいては、複数のリポジトリに画像配信の外部サービスのコードが点在しており、責務分離の観点であまり好ましい状態ではありませんでした。 そこで、画像配信に関する実装上の責務を1つのリポジトリに集約することによって、疎結合の実現を目論みました。 第3章:設計戦略を実行するチームマネジメント エンジニア駆動のチームマネジメント 筆者の普段のBASEでの仕事は、コードを読み書きすることなので、プロジェクト関連のマネジメントには関与しません。 ですが、今回の「Akamai Image Video Manager の導入プロジェクト」においては、プロジェクトチームを取りまとめました。 その理由は、このプロジェクトを回すためには、エンジニアリングの専門的な知識が必要だったからです。 一般的なBASEの社内プロジェクトであれば、プロダクトマネージャの元に、複数のエンジニアが参画して、1つの機能開発に携わることが多いといえます。 ですが、このプロジェクトにおいては、画像ファイルの基本的な知識に加えて、キャッシュやCDN、BASEのリポジトリ構成を理解する必要がありました。そこで、普段はコードを書いている筆者が、チームを取りまとめることになったのです。 なお、このプロジェクトでは、筆者は実装を行なっていません(ただしプルリクのレビューは行なっています)。 これは社内事情が影響しており、当初は取りまとめと実装を両立して行う予定だったものの、急遽実装が必要になった別のプロジェクトに追加アサインされたため、時間の都合上、Akamaiの移行プロジェクトは「取りまとめ役」に徹することになったからです。※ ※急ぎの実装とは「Facebookドメイン認証」というプロジェクトで、これに関してもテックブログを書いています。 →「ROI(投資利益率)を意識したエンジニアリング」 答えのない意思決定に対峙する チームの取りまとめにあたって、実装の担当箇所の振り分けと、局所的な意思決定の2つを行いました。 1つ目の実装担当箇所の振り分けとは、(1)改修範囲の調査と見積もりを行い、(2)チームの各メンバーに実装箇所を割りあて、(3)リリースに至る手順を交通整理する、ということです。 2つ目の局所的な意思決定とは、リリースにあたって実装以外で決めなければならないことについて、社内の担当者と調整したうえで決定することです。今回は「ショップに対するコミュニケーション」と「APIの互換性」という2点で、意思決定を行う必要がありました。 「ショップに対するコミュニケーション」とは、BASEの顧客であるショップへの通知のことを意味します。画像配信基盤の移行にあたって、画像表示の仕様が微妙に変化することが避けられなかったため、利用者の方にその内容を事前にお伝えすることが必要でした。 この際に、どのような方法(メールなのか?管理画面なのか?)でお伝えするのかを判断する必要があり、プロダクトマネージャの方の大きな助けを借りつつ方針を決めています。 「APIの互換性」とは、BASEが外部に公開しているAPI(BASE-API)への影響を加味することです。 Akamaiへの移行によって、BASEが提供するAPIの構造には変化はないものの、画像関連の「パス」が変更されるため「潜在的に影響あるかもしれない」という懸念がありました。 従来の画像パスとの互換性を残すか?それとも残さないか?というのは非常に難しい問題で、BASE社内の各方面の責任者の方と相談しつつ、方針の決定に至りました。 これらの経過を経て、2021年11月ごろまでに「Akamai Image Video Manager の導入プロジェクト」は無事に終了しました。 協力していただいた社内の皆様、チームの方々に大感謝しております。 終わりに 筆者の個人的な見解として、インターネット業界では、2020年代を通じて外部システムが提供する機能のボリュームは増え続け、業界全体でAPIなどを通じた外部サービスとの連携が1つの主戦場になると見ています。 近い将来、エンジニアは「外部サービスの不確実性をいかに最小化するか」という問題に、より一層、真剣に対峙する必要が出てくるでしょう。 その際に、このテックブログの内容が少しでも参考になれば幸いです。 最後にいつもの宣伝です。BASEでは各職種で採用を強化しておりますので、お気軽にお声がけください! https://open.talentio.com/r/1/c/binc/homes/4380
この記事は BASE Advent Calendar 2021 の19日目の記事です。 BASE BANK株式会社でエンジニアをしている若野( @sam8helloworld )です。 私が普段見ているサービスではBASEの他のアプリケーションや外部サービスのページから遷移してくることがよくあります。 さらにその時々でユーザに提供する情報や振る舞いを変えたくなることもあります。 今回はそういったケースの中でも 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない という仕様を実現するために私が行った調査検証が記事の内容となります。 仕様を簡単に図で表すと以下のようになります。 アプリケーションの構成の整理 今回の実装を行うに当たって前提となるアプリケーションは以下の3つの登場人物で成り立っています。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 特定のディレクトリ配下(e.g. /shop_admin/xxx)のURLにアクセスしてきた時に、SPAの起点になるindex.htmlをレスポンスとして返すことが責務です。 BFFサーバ SPAから呼ばれて表示要件に応じたjsonをレスポンスとして返すことが責務です。サービスのビジネスロジックはここに集約されている場合が多いです。 SPA BFFサーバにサービスのビジネスロジックを委譲しており、画面の表示やユーザのインタラクションを扱うことが主な責務です。 どこにロジックを持つか問題 さて登場人物がわかったところで、今度はその中でも上記の 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない というロジックを持つべきなのは誰なのか?ということが問題になります。 今回のケースでは以下の要件を満たすことができるかが鍵になります。 ページ遷移してきた時に処理を実行できる 直前のページのURLを取得できる 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない では登場人物を1つずつ検証していきます。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 結論から言うと全ての要件を満たすことはできませんでした。(厳密に言えばやろうとすればできるけど遠回り感すごい。) [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない まず1つ目の要件の「ページ遷移してきた時に処理を実行できる」に関しては、外部サービスからページ遷移してくる都合上必ずバックエンドサーバのControllerでSPAの起点になるindex.htmlを返す処理を行うので以下のようにページ遷移時にAPIを叩くことも可能です。 public class XXController { // 省略 public function index() { // ページ遷移してきた時の処理を行う // $api->call(); $this->render('SPAのindex.html'); } } 次に2つ目の要件の「直前のページのURLを取得できる」に関しては、以下のようにリファラを参照することでブラウザセッション内の直前のページのURLを取得できます。 public class XXController { // 省略 public function index() { $referer = $_SERVER['HTTP_REFERER']; if ($referer === '特定のURL') { // ページ遷移してきた時の処理を行う } $this->render('SPAのindex.html'); } } ※ 注意点: <meta name="referrer" content="no-referrer"> タグがあるなど、リファラを送信しない設定になっているページのURLは取得できません。 最後に3つ目の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、以下の3パターンを場合のサーバサイドの状態を考える必要があります。 外部ページから戻ってきた時 外部ページから進んできた時 ページを更新した時 外部ページから戻ってきた時 例えば以下のように a.example.com -> b.example.com -> c.example.com とリンクを押して遷移して、戻るボタンで b.example.com に戻ってきた場合を考えます。 ブラウザの進むボタン(history forward)や戻るボタン(history back)はリファラの状態も元に戻してしまいます。なので、 c.example.com から遷移してきたにも関わらずバックエンドでリファラを参照するとあたかも a.example.com から遷移してきたように見えてしまうのです。 developer.mozilla.org html.spec.whatwg.org 外部ページから進んできた時 「外部ページから戻ってきた時」とは違って確かに直前のページの意味合いはあっていますが、今度はバックエンドでリンクを踏んだページ遷移と区別がつかないので要件を満たせません。 ページを更新した時 ページを更新した場合も「外部ページから進んで来た時」と同様にリファラはそのままですが、バックエンドではリンクを経由したページ遷移と区別がつかないので要件を満たせません。 ※ セッションやCookieを使えばバックエンドで状態を持てるのでページの更新や2回目以降の遷移かどうかが判断できますが、後述の方法の方がシンプルなので採用していません。 BFFサーバ そもそものSPAから呼ばれてjsonを返すことが責務なので、BFFサーバに画面遷移の状態を判別させるのはお門違いです。 [ ] ページ遷移してきた時に処理を実行できる [ ] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない SPA SPAではなんとか3つの要件全てを満たすことができました。 [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [x] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない 1つ目の要件の「ページ遷移してきた時に処理を実行できる」と3つ目の要件の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、ブラウザのNavigation Timing APIを使うことで要件を満たせます。 Navigation Timing APIではバックエンドで判別できなかった戻る・進む・更新・ページ遷移をそれぞれ状態として取得できるので、ページ遷移の時だけ処理を行うということが可能になります。 developer.mozilla.org ただ注意が必要なことは、バックエンドと違ってSPAではページ遷移という概念が普通のリンクを踏んだページ遷移とvue-routerなどのjsで制御されたルーティングによるページ遷移の2つあるということです。 2つ目の要件の「 直前のページのURLを取得できる」に関しては、 document.referrer がバックエンドのリファラと同じ役割を果たすので要件を満たせます。 mounted() { const referrer = document.referrer if (referrer === '特定のURL') { // ページ遷移してきた時の処理を行う } } Navigation Timing API さて、Navigation Timing APIを使えば要件を満たせると言いましたが、Navigation Timing APIとはどういうものなのでしょうか? Navigation Timing APIの MDN Web Docs を参照すると主に以下の使い方をするAPIであると書かれています。 Collecting timing information(タイミング情報の収集) Determining navigation type(ナビゲーションタイプの決定) さらにDetermining navigation typeでは以下のことが判別できると書いてあります。 Was this a load or a reload? (ロードかリロードか) Was this a navigation or a move forward or backward through history? (ページ遷移かhistory backかhistory forwardか) How many (if any) redirects were required in order to complete the navigation? (遷移が完了するまでに何回りダイレクトしたか) これは上述したクリアすべき要件にピッタリハマりそうです。 Navigation Timing APIを使ってナビゲーションタイプを判別する方法 ナビゲーションタイプを判別する方法は2つあります。 1つは window.performance.navigation.type を参照する方法です。 if ( window . performance . navigation . type === window . performance . navigation . TYPE_NAVIGATE ) { console . log ( 'ページ遷移' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RELOAD ) { console . log ( 'ページ更新' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_BACK_FORWARD ) { console . log ( '戻る・進む' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RESERVED ) { console . log ( 'その他' ) } developer.mozilla.org 2つ目の方法はPerformanceNavigationTimingインタフェースを使用する方法です。 const entries = window . performance . getEntriesByType ( 'navigation' ) for ( const entry of entries ) { if ( entry . type === 'navigate' ) { console . log ( 'ページ遷移' ) } if ( entry . type === 'reload' ) { console . log ( 'ページ更新' ) } if ( entry . type === 'back_forward' ) { console . log ( '戻る・進む' ) } if ( entry . type === 'prerender' ) { console . log ( 'その他' ) } } developer.mozilla.org 2つの判別方法の使い分け 2021年12月現在、主要ブラウザで window.performance.navigation.type を利用することはできますが、参照することは非推奨となっています。 代替の方法として2つ目のPerformanceNavigationTimingインタフェースを利用することが推奨されている状況です。 ただし、PerformanceNavigationTimingはiosのsafariが今のところサポートしていない状況です。 const types = window . PerformanceObserver . supportedEntryTypes if ( types . includes ( 'navigation' )) { // PerformanceNavigationTimingインタフェースに対応 } 上記の supportedEntryTypes でPerformanceNavigationTimingのサポート状況は確認できます。 navigation という文字列を含む配列を返す場合は対応済みなのでPerformanceNavigationTimingインタフェースを使った実装を。 navigation という文字列を含まない配列を返す場合は window.performance.navigation.type を使った実装を行うのが良さそうです。 developer.mozilla.org Navigation Timing APIを使った判別はvue-routerを利用した遷移にも使える? Navigation Timing APIにおける「ページ遷移」「戻る・進む」はあくまでも document オブジェクトが初期化・再構築されるような処理に対して遷移に対して判定されるようです。 www.w3.org html.spec.whatwg.org VueをはじめとしたSPAのルーティングでは document オブジェクトを初期化するのではなく、一部のDOMを更新する仕組みになっているので「ページ遷移」「戻る・進む」は判定できませんでした😢 ただ、今回処理を行いたいのはVueのルーティングではなく外部ページからリダイレクトされてきた時なので要件は満たせるというわけです。 document.referrerと組み合わせて「特定ページからの初めての遷移を判定する」をコード isFromCertainPage() { // safari on ios か確認 if (! window .PerformanceObserver.supportedEntryTypes. includes ( 'navigation' )) { // ページ遷移か確認 if ( window . performance .navigation. type !== window . performance .navigation.TYPE_NAVIGATE) { return false } } // ページ遷移か確認 const entries = window . performance .getEntriesByType( 'navigation' ) for ( const entry of entries) { if (entry. type !== 'navigate' ) { return false } } const expectedReferer = '特定のURL' let referrer = document . referrer . replace ( /\?.*$/ , '' ) // safariのバージョンによってreferrerのトレイリングスラッシュの有無が分かれるので、一律トレイリングスラッシュをつける // https://trac.webkit.org/changeset/280342/webkit/ referrer = referrer. slice (- 1 ) !== '/' ? referrer + '/' : referrer if (!referrer. startsWith (expectedReferer)) { return false } // SPA内での回遊判定 if ( this .$store. state .alreadyVisited) { return false } return true } ※ SPAのルーティングの遷移に関しては現状判定するAPIがないので、状態を持つようにしています。 終わりに Navigation Timing API自体は複雑ではないので、すぐに実装に入れました。ただ、SPAではなぜNavigation Timing APIでページ遷移が判定できないのか?ということを深掘りしていくとブラウザAPIの仕組みが垣間見えて結構面白かったです。 あとは今回みたいにブラウザのAPIを利用したコードのテスト書こうとしたときに、jsdomだとモックだらけになっちゃうし実ブラウザを使ったIntegrationテストにするにもコストかかるしで塩梅が難しいと感じました。ここら辺のテスト詳しい方がいたらぜひ @sam8helloworld まで🙇‍♂️ また、もしBASEBANKに興味のある方は @sam8helloworld や下記のリンクまで open.talentio.com 明日はOwners Experience Backend Groupの杉浦さんです。
この記事は BASE Advent Calendar 2021 の19日目の記事です。 こんにちは、BASE株式会社でバックエンドエンジニアをしている小川です。 私たちのチームでは10月からレスポンス改善PJとして、BASEのレスポンスが遅い処理を改善する施策を行なってきました。 その中で下記の例のようなクエリが重い処理として挙がり、そのクエリではGROUP BYが使われていたのですがDISTINCTを使った方が早くなるのではという意見がありました。 SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; そこで、DISTINCTとGROUP BYでどちらが早いのかをネット上で調べたところ、DISTINCTの方が早い説やGROUP BYの方が早い説、どちらも変わらない説などいろんな説があり、実際のところどうなんだろうと思ったことがきっかけで検証してみることにしました。 DISTINCTとGROUP BYどちらを使った方が良いか検証した結果をご紹介したいと思います。 DISTINCTとGROUP BYの違いについて DISTINCTとGROUP BYは重複行をまとめるという目的で使われると思いますが、それぞれどんな違いがあるのでしょうか。 DISTINCTについて 射影を行う段階で重複を排除 GROUP BYについて 指定された列名でレコードのグループ化を行う。 グループ化したレコードは、集約関数を用いることで集計することができる。 つまり、 DISTINCTは、重複を排除した結果を出力する場合に使用 GROUP BYは、重複を排除した結果に対して集計処理を行いたい場合に使用 という違いがあります。 今回例に挙げたクエリはGROUP BYを使用していましたがグループ化した後、集計処理をせずに重複行を削除するためだけに使われています。 こういった重複行を削除するためだけなら、用途にあったDISTINCTを使うべきだと思います。 次に肝心なDISTINCTとGROUP BYどちらが早いのか見ていきます。 DISTINCTとGROUP BYどちらの方が早い 下記のようなDISTINCTとGROUP BYを使用したクエリを使って、クエリキャッシュを無効にしてそれぞれの実行計画と実行速度を検証してみました。 SELECT DISTINCT c1 FROM t1 WHERE c1 > const; SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; ※c1にはインデックスが貼ってある状態 検証環境 MySQL5.6 検証結果としましては、 実行計画は全く同じで実行速度もほぼ同じ結果となりました。 MYSQL-DISTINCTの最適化 In most cases, a DISTINCT clause can be considered as a special case of GROUP BY. For example, the following two queries are equivalent: Due to this equivalence, the optimizations applicable to GROUP BY queries can be also applied to queries with a DISTINCT clause. Thus, for more details on the optimization possibilities for DISTINCT queries, see Section 8.2.1.14, “GROUP BY Optimization”. こちらのURLのMYSQLのドキュメントにも記載されている通り、オプティマイザによって最適化された結果、内部では同じ処理になり性能は同じということが分かります。 実行速度も検索結果も同じということであれば、DISTINCTとGROUP BYは使用用途によって使い分けてください。 おわりに DISTINCTとGROUP BYについての違いや、速度に関して紹介させていただきましたが参考になったら幸いです。 今回の調査ではレスポンスの改善にはなりませんでしたが、ほんの些細なことでも調べて、検証することによって新たな発見があると思います。 実際に私も検証するまでは、GROUP BYよりも重複行を削除するだけの処理のDISTINCTの方が早いと思っていました。 ぜひ気になったことがあれば検証してみてください! 明日のアドベントカレンダーはyusugiuraさんとShoTakeuchiさんです!お楽しみに!
バックエンドエンジニアの @cureseven です。レスポンス改善プロジェクトという名前で、BASEのレスポンス速度を早くするために10月より動いてきました。 経緯に触れた後、レスポンス改善をどう進めてきたかとおこなった施策を紹介します。 レスポンス改善プロジェクトの経緯 始めにプロジェクトが立ちあがった経緯を少しお話します。 BASEはリリースして9年ほど立ちました。その間、さまざまな機能が追加されたり、ありがたいことに多くのユーザーさんに使っていただいています。その結果、DBに保存しているデータ量が増えました。データ量が多いことによりSelectの実行時間が重くなるなどが起こっていました。 ユーザの方から複数件画面の重さについてのお問い合わせをいただいており、今回プロジェクトとして改善することになりました。 重いリクエストの特定 それではここから、どう進めてきたかとおこなった施策を紹介します。 BASEではNew Relic Oneを導入しており、リクエストごとのレスポンス速度の統計を見たり、Transactionの詳細を見ることができます。私たちはNew Relic APMで重いリクエストを突き止め、施策を実施しました。 New Relic APM Transactionsのページで、 Most time consuming(実行回数 × 平均実行時間) Slowest average responce time(平均実行時間) を見比べながら、平均実行時間が遅すぎるものやたくさんの人が利用していて不便に感じそうなものを中心に見ていくことにしました。 Most time consuming順にエンドポイントをsortしている 今回は購入者が触れる画面ではなく、オーナーさんの作業がサクサクできることにフォーカスして、オーナーさんの使う管理画面の重いものから見ていきました。 最も重い画面についてはある程度見通しがついていましたが、Most time consumingについては予測を立てることが難しいです。New Relic APMを見ることでどんなAPIが重いのかを把握することができました。根拠となる数字を持って優先度を決め施策を実施していくことで、効率よくオーナーさんの問題にアプローチできました。 また、フロントエンドで時間がかかっているかどうかの検証にはBrowserの機能を利用できます。これから使っていくつもりです。 dashboardによる重いリクエストの可視化 オーナーさんからのお問い合わせを受けるより先に社内で重いリクエストがあったことを知るために、Transactionのdashboardを作成し監視することにしました。 dashboardでは注文数の多いshopをいくつかピックして、オーナーさんの使う管理画面に限定したTransactionの95パーセンタイルの重さが目立つリクエストを表示しました。 alert飛ばす設定 dashboardを作成しましたが、張り付いて監視しないでいいように、alertを飛ばす設定をしました。 今回は購入者が触れる画面, オーナーさんの使う管理画面両方において、それぞれにTransaction timeの閾値をセットし、リクエストごとに95パーセンタイルが閾値を超えたときにslack通知するようにしました。 設定に役立ったリンクです。 slackのhookリンクの生成 alertの設定 alert設定のパラメータの説明 通知頻度の設定 レスポンス改善施策の実施 New Relic APMのTransaction traceという機能で、Transactionの詳細を見ることができます。どのメソッドのどのクエリが重いかまで断定できます。 私たちは重いリクエストのTransaction traceを見ることでボトルネックを把握し、リファクタ方針を考えました。リファクタ施策の具体例をいくつか紹介します。 indexを貼った 重いクエリを発見したとき、explainして実行計画がどのようなものかを確認しました。indexが適切に使われていないクエリを発見したので、indexを貼る対応を実施しました。 不要な処理を消した 機能のリニューアルや機能を廃止する中でいらなくなった処理が残ってしまうことがあります。今回、そんな処理がTransaction timeを圧迫してしまっていました。不要な処理を削除することでレスポンスが改善しました。 クエリ改善 今回よく目にしたのが、レコードの存在の有無だけ分かればいい時に、集計したり大量にデータを取ってきたりしているというものでした。レコードの存在有無確認は最初の1件だけとれば良いので、集計処理などをやめることで、クエリ実行時間を短縮できました。 また、画面描画毎に一つずつのデータを取ってきて集計するのではなく、バッチで集計したデータを格納しているテーブルを利用するようにする修正を実施しました。こちらの修正では、95パーセンタイルで15秒ほどかかっていたリクエストが、1秒以下で返ってくるようになりました。 また、注文数をカウントする処理では、画面上は99以上になると「99+」という表示になるところがあります。大量に注文があるshopのクエリがとても重くなってしまっていたため、サブクエリを使って100件以上は数えないようにしました。 バッチ実行の負荷軽減 New Relic APMのTransaction traceは、1分ごとに、基準値より重かった最大Transaction timeの詳細を残しています。 取れたTransaction traceを眺めていると、どうやら15分ごとに基準値に引っかかっていることがわかりました。詳細を見ると、15分おきに出ているTransaction traceではdb接続に決まって6秒ほどかかっていることがわかりました。 原因は15分間隔で実行されるバッチにて大量レコードのtruncateおよびinsertのSQLを実行していることでした。一旦メモリにもった上でループ処理することで急激な負荷を抑えることで、15分おきに出ていた6秒のdb接続時間は解消されました。 dashboardを見ているだけではこのような傾向を読み解くことはできませんでした。Transactionの99パーセンタイル、95パーセンタイル、50パーセンタイルを見比べたり、Transaction traceの出力時間を見たりすることで見えてくるものもありました。 プロジェクトを通して 3ヶ月間対応を行なってきた結果、画面ロードが遅いことに関してお問い合わせいただいていたshopさまから改善されたねの声をいただきました。しかしながらまだまだ遅い画面はあるため、来年以降も改善にむけて動いていこうと思っています。 最後に、レスポンス改善プロジェクトを通して感じたことを記して終わろうと思います。 機能リニューアルなどのタイミングで適切に不要な処理を消すのはもちろんですが、今回のプロジェクトのようにたまにリファクタをするための期間を設けてそのような処理がないか見直すのは大事だなと思いました。大掃除の季節ですし、ソースコードの見直しを実施するいいタイミングなのかもしれません。可読性も上がります。 機能開発のタイミングで貼ったindexが、データが増えることにより効かなくなるときがあります。今回の施策で劇的に効果が上がったのはindexの付与でした。こちらもリリース後年月が経ってから改めて見直すのが良いなと思いました。 あまりにもレスポンスが遅いと素晴らしい機能も使い物にならないので、仕様を少し変更して大幅に改善するものであれば、変更することも検討してみるのがいいなと思いました。 これからも使っていけるサービスであるために、さまざまな実装方法を検討する必要があると改めて感じました。
この記事は BASEアドベントカレンダー2021 18日目の記事です。 UIデザイナーの Yoshioka です。コードベースのデザインツールとして個人的に気になっている UXPin Merge を試してみました。 UXPin Mergeとは UXPinはベクターベースのデザインツールとは違い、HTML/CSSのコードで定義されていることをベースにしているデザインツールです。 UXPin Merge はReactやStorybookのコンポーネントを取り込み、実装されているものと同じコンポーネントを元にデザインができる機能です。 試してみる 今回は簡単にローカルでつないでみました。 ReactComponentsやStorybookを読み込むことができます。 UXPin MergeのReactComponents、Storybookでできることが違います。(詳しくは後述します) また、storybookの場合にはコンポーネントの配置に若干のラグが発生します。 立ち上げる 該当のReactComponentsリポジトリから、ローカルでExperimentalModeを立ち上げます。 これにより、URLが生成されるのでコード変更してどうUXPinで反映されるのか確認することができます。 配置していく コンポーネントがこのようにリスト表示されるので、これをドラッグ&ドロップして配置していきます。 簡単なデザインですが配置してみました。 できること 各コンポーネントをクリックするとコンポーネント定義されているpropsが表示されます。 今回はReactComponents連携したので、コンポーネントをネストすることができます。 (storybook連携ではできない様です) ネストすることのメリットは、下記の様にレイアウトコンポーネントを作成し、その中にコンポーネントを入れ込むことができます。 ネスト内のコンポーネントを削除することもできます。 今回はStackというレイアウトコンポーネントの中に、SelectとButtonを内包しています。 StackのpropsでDirectionを持っているので、レイアウトを切り替えることができます。 レイアウトコンポーネントのMarginなど覚えておくのも大変なので便利です。 できないこと Figmaなどのデザインツールと違ってコンポーネントのDetachはできません。 もちろんテキストや図形など配置できますが、コンポーネント内に配置するなどはできません。 このあたりはプロダクトやデザイナーの好みによりそうですが、自由にデザインデータを作れないことにあまりネガティブな印象はありませんでした。 また、Figmaの様にテキストをアートボード上で編集することはできず、props上で入力する必要があります。 プロトタイプについて まだ上記の様にコンポーネント配置しただけですが、この状態でどのようなプロトタイプが作成されるのかみてみます。 Interactionにはなにも設定していませんが、このようにプロトタイプが作成されます。 Figmaでも似た表現を行うことはできますが、実装が元になっていることでコンポーネントライブラリと差分なくプロトタイプ確認できるのはメリットかなと思います。 デザインツールで起きがちな問題 ここでたまに聞くデザインツールで起きがちな問題を洗ってみます。 このコンポーネントない、などのデザインデータと実装のギャップなどどう解決してよいかわからない沼が出てきます。 コードベースだと解決できるかもしれない 簡単に触った程度ですが、ここまででコードベースのデザインデータにした時のメリットは デザイナー・エンジニアがコンポーネントに対し同じ基準でデザインできることにあります。 これまでの企画・デザイン・コーディングといったフローから、デザイナーとエンジニアが共同でプロトタイプを作ることができることで、既に共通の認識があるのでコミュニケーションコストを抑え開発スピードをあげることができそうです。 現状ではデザイナーがコードへの理解を示すこと、開発環境作りやGitの連携にかなりエンジニアに工数を割いてもらう必要もありますが、検討してみる価値はあります。 来年以降もコードベースのデザインツールを注目していきたいとおもいます!
この記事はBASEアドベントカレンダー2021 17日目の記事です。 はじめに DataStrategyチームの杉です。 ショッピングアプリPay IDではさまざまなショップでの商品購入が可能です。 "探す"タブにはおすすめ機能がついており、利用者にあった商品やショップのレコメンドを行なっています。 おすすめ商品の掲載例 おすすめの商品ではさまざまなアルゴリズムを並行に運用しており、その中のひとつとしてAmazon Personalizeを利用しています。 このアルゴリズムの計算は今まで1日に1回のbatch処理で行なっていました。 しかし、閲覧や購入のログをリアルタイムに利用することでよりマッチしたおすすめ商品を掲載することができるのではという想いでevent trackerを用いたリアルタイムに変化をするレコメンドに挑戦をしました。 この記事では、event trackerをどう実装したかをメインにお伝えしたいと思います。 Amazon Personalizeとは Amazon Personalizeについては以前に@pigooosukeさんが発表をしているので運用しているおすすめの全体の構成やAmazon Personalizeの利用注意点などについてはこちらで見ることができます。 https://speakerdeck.com/pigooosuke/aws-personalize-recsys (現在の構成と異なる部分もあります) Amazon Personalizeを使用することでメンテナンスコストを下げることや精度の高いおすすめを提供することができています。 event trackerについて Amazon Personalizeではevent trakcerを使うことでリアルタイムなイベントを反映させた結果の推論結果を取得することができます。 作成、eventの送信はどちらもboto3で行うことが可能です。 ①event trackerの作成 personalize = boto3.client(service_name= 'personalize' , region_name=AWS_REGION) response = personalize.create_event_tracker( name=EVENT_TRACKER_NAME, datasetGroupArn=dataset_group_arn ) ②eventが発生するたびにput eventを行う personalize_runtime = boto3.client( service_name= 'personalize-runtime' , region_name=AWS_REGION) personalize_events.put_events( trackingId=tracking_id, userId=userId, sessionId=sessionId, eventList=[{ 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '12345' }, { 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '6789' }] ) eventの送信はuser_id単位で送ります。 また、session_idを用いることでその時点では未知なユーザに対してもデータを溜めることが可能です。 推論への影響はすぐに反映され、eventの影響を確認することができます。 event内容によっては必ずしも推論結果に影響するわけではないので、put eventをしても変わらないこともあるということには注意が必要です。 event trackerを導入後の構成 eventとして溜めていくデータとしては2種類あります。 DBに記録された購入情報など 行動ログ これらをリアルタイムにevent trackerに流し込んでいくために以下の構成にしました。 メインのworkerは2台あります ①Kafka->SQS DBの更新イベントと行動ログをKafkaで取得し、SQSへ随時eventを送ります。 ここではRDSに接続をし必要情報の紐付けなども行なっています。 ②SQS->put_event SQSからAmazon Personalizeのevent trakcerへput eventをするような動きをしています。 SQSを挟むことで急激なトラフィックなどが来てもevent trackerに流し込む部分の負荷が高くならないようにしました。 event trackerの注意点 event trackerの検証を行なっているうえで、注意点に出会いました。 いくつかありましたが、その中でも2つご紹介させていただきます。 1. 未知商品に対してはeventを追加しても影響がない データセットには以下の3種類あります。 Item User User-item interaction このItemに入っていない商品は推論結果として出現しません。 そのため、未知商品をeventとして追加をしても推論結果が変わることや推論結果として新しく商品が登場することはありませんでした。 ここで未知商品を反映させるためには商品登録時にPutItemsをする必要があります。 2. "interactions"データセットimportだけではput eventの内容は消えない 今までdailyで計算をする際にデータセットを毎日importすることで上書きをしていました。 これはデータセットの肥大化を防ぐことを目的として行なっておりました。 put eventで追加したデータは"interactions"データセットとして追加されていきます。 そのため、今回リアルタイムに追加をしたeventの内容と、毎日の集計しているデータセットの中身が被ってしまう問題が発生しました。 "interactions"データセットを削除することでevent trakcerに追加した中身を消すことができます。 しかし、"interactions"データセットを削除するためにはevent trackerの削除やfilterの削除も必要です。 そのため、実際に試したところ、約20~30分filterが不在になることとなりました。 (この時間は今回試したデータセットの大きさの場合であり、データセットの大きさなどで変化します。) filterではすでに購入した商品などを除くような処理をいれているため、filterがないことはよくないと判断をし、最終的には"interactions"データセットは初期load以降はevent trackerで補うように変更をし毎日の作り直しをやめました。 さいごに 今回、リアルタイムな閲覧情報などをAmazon Personazlieのevent trackerで導入することで変化するレコメンドについてご紹介をしました。 また、ご紹介した機能は検証中のため、現在全ユーザに公開されている機能ではありません。 これからももっとよりよい精度と新しい発見を提供できるようにレコメンドに磨きをかけていきたいと思います。 最後までお読みいただきありがとうございました。明日はkikuchiさんとcuresevenです!
この記事は New Relic Advent Calendar 2021 16 日目の記事です。 遅刻しちゃいました。本当は発表の裏側的な話にしようかなと思ったのですが、先日のイベントが素晴らしかったので、参加レポートにしました。 はじめに こんにちは!! BASE BANK 株式会社 Dev Division にて Software Developer をしている永野( @glassmonkey )です。 普段はバックエンドエンジニアとして、Go/Python/PHP を主に書いてたりします。 昨日行われた NRUG (New Relic User Group) Vol.1 に参加し、社内での活用事例として「初心者でも簡単に扱えるNew Relic」というコンセプトで発表させていただきました。 nrug.connpass.com 前回のvol.0 では同僚の清水( @budougumi0617 )による参加レポートもあるので合わせてご確認ください。 Goを New Relic で扱いやすくするOSSの紹介もあるので、ぜひ御覧ください。 devblog.thebase.in NRUG (New Relic User Group)とは NRUG(New Relic User Group)はNew Relicを活用するユーザーの集いです。 Slackもあるので、興味のある方はぜひご参加ください。一緒にわいわいしましょう。 招待リンクは こちら です。 発表した内容 内容は私達 BASE BANKチーム が運用しているAPIを New Relic を使って改善したという事例です。 裏コンセプトとして、 NRQL をまともに書けない私でもNew Relicを活用し、数字として繋がるような改善をすることができたというのがあったりはします。 別の切り口でのパフォーマンス改善話を別チームの @cureseven さんから、記事公開の予定なのでそちらも楽しみに!! 公開は18日予定です。 感想 New Relic 三昧の1日で非常に楽しかったです。 特に中の人に直接質問できたのは大変良い機会でした。 リリースノートを眺めるのも楽しいですが、肉声でおすすめの機能が聞けることはイベントの参加の醍醐味だと感じました。 特に CodeStream は分散トレースからログだけでなく、普段使ってるIDEでコードジャンプが実現する素晴らしい機能で感動しました!! 活用していきたいなと思いました。JetBrains系、VS Code、VisualStudioに対応してるので普段使いには困らなさそうです。 www.codestream.com ぜひ一緒にオブザーバビリティをやっていきたい人いたら一緒に働きましょう!! open.talentio.com open.talentio.com
CTOの川口 ( id:dmnlk ) です。 BASE株式会社は本日、 The PHP Foundation への寄付を行いました。 The PHP Foundationとはなんぞや、というのはインフィニットループ様の記事を読むのがとてもわかりやすいのでそちらをご覧ください。 www.infiniteloop.co.jp BASEで動いている多くのコードはPHPであり、そのPHPの継続的な開発を支援するのは当然であると考え今回寄付をしました。 opencollective.com とりあえず今年分はまとめて行いましたが、来年以降も継続的に行っていく予定です。 この記事を見た、PHPを利用している企業の皆様も一度ご検討ください。 余談ですが、OpenCollectiveを初めて使いましたが組織の妥当性検証ができないのが気になりましたが、騙って寄付するメリットもよくわからないですし気にしないこととしました。
この記事は BASE Advent Calendar 2021 の16日目の記事です。 こんにちは。ProductManagementグループに所属している坂東( @naoto bando )です。 2021年はメンバーが10名を超える2つのPJを同時進行する機会があり、色々と考えることや学ぶことの多い年でした。 特にコロナ禍の真っ只中だったということもあり、リモート下におけるコミュニケーションはこれまで以上に気を使う面が多くありました。 本稿ではそういったコロナ禍におけるコミュニケーションの中のランチ会(懇親会)のさいに工夫をしたことを書かせていただきます。 読まれた方が工夫されている点などがありましたら、twitterなどで教えていただけると嬉しいです。 ※これはランチ会後の状態です。楽しそうな雰囲気になりました。 大人数のリモートランチの難しさ リモートであってもなくても、PJにおけるランチ会など懇親会の目的は、 PJメンバーが一定のコミュニケーションをとり、お互いの理解度を深めることにあるかと思います。 そのため、企画者の最低限の役目は「みんなが安心に快適に話せる」環境を作ることです。 オフラインや少人数リモートは楽 これらの状態はオフラインであれば、対面する人やそばにいる人などの2~4名ほどの小さなノードになって対話が進むため、放っておいてもおおよそなんとかなります。 また、だいたいのケースで企画者は全体的に声が聞こえ目が届く状況にあるので、席をシャッフルをするなど、状況を見ながら対応を決めていくことができます。 つまり、場所を提供すればなんとかなります。 オンラインでも3〜4名ほどであれば、比較的みんなが満足に話せる様に感じます。 大人数リモートの罠 しかしながら、オンラインでかつ10名を超えるような規模感になると、次のようなことが問題になります。 声がかぶると対話できないため喋れない 少数の人が喋り続け、会話の輪に入れない 急に話をふられて焦る そのため、話すのが好きな人が話し続ける感じになりがちかなぁ。と感じています。 こうなってくると気持ちも離れ、仕事のことを考えはじめ、いっそうコンテキストがわからなくなっていきます。(まさに私がこの状態になりがち。) これはもともとの目的からすると大変よろしくない状況です。 Remo Conference などを用いれば小さなノードにすることもできますが 別部屋の雰囲気などがわからないので、企画者としては怖いものがあります。 どこかの部屋が静まりかえっていたりしたらと思うと、怖くないですか・・・? どうしたか ここまでのことから、問題は参加者がしゃべる機会やタイミングを失うことで、場への参加を諦めてしまうことにあると言えそうです。 そこで、この問題の解消方法として、以下を満たせばランチ会の目的を果たせると考えました。 それぞれが話す時間を強制的に作る 話のお題をある程度予想できるようにしておく(場のコンテキストから置いてけぼりにならないようにする) また、PJ内でのランチ会は継続的におこなわれるため、適度なランダム性と拡張性を持てるようツールを用いておこないました。 ここからは NETA というツールを使ったランチ会と、自前のLT会ぽいランチ会について紹介していきます。 それぞれに一長一短がありました。 参考になれば幸いです。 NETA を使ってみた NETA はAID-DCC Inc.さんが作成されたオンライン上のコミュニケーションツールです。 このツールは事前に「話すテーマ」と「話す人」を設定しておくと、ランダムで「話すテーマ」と「話す人」を指定してくれます。 プリセットの話すテーマ集も準備されています。 「オンライン飲み」や「アイスブレイク」「合コン」「SONTAKU」(?)などのテーマを選択することで、参加者の名前を記入するだけで開始できます。 実際にやったこと 以下の理由で事前にPJメンバーに対し「他のPJメンバーに聞きたいこと」をアンケートで募集しました。 プリセットの質問は少々PJの雰囲気に合わないかなと感じがし PJメンバーの対話感を出したかった こうすることで、局所的に質問者と返答社の擬似的な対話を作ろうとしました。 PJメンバーへの呼びかけ Googleフォームで簡単に「他のPJメンバーに聞きたいこと」を収集するフォームを作成。 集めた質問集。 「さいきん一番痛かったこと辛かったこと(物理・精神どちらでも可)を教えて下さい」?🤔 質問にも個性がでておもしろいですね。 集計した質問週と参加者をNETAに質問を記入すれば準備完了です。 実際に動かしてみるとこんな感じです。動きがあるので楽しいですね。 やってみて 適度にランダム感があり、楽しく過ごすことが出来ました。 質問にも返答にも個性が出るので、PJメンバーの意外な一面などを見ることができました。 よく出る質問や、よく当たる人などが出てくるため、そういったハプニングも含めて場を楽しめます。 ただ、指名される人はランダム抽出のみであるため、ランチ会も後半になると指名されない人がでてきます。 そのため、最後はルーレットを回して話すテーマを決め、回答者はファシリテータが指名する形になります。 なので、ファシリテータは状況に気を使いながら進行をする必要がありました。 NETAの特徴 何が当たるか、誰が当たるかわからないランダム性を楽しみたいとき すでにPJ内の心理的安全性が保たれているとき 参加者がアドリブに強いとき こんなときはNETAが良さそうです。 ランダムであることの面白さを生かすのであれば、NETAはオンライン飲みなど、よりカジュアルな場面の方が活きるかもしれません。 逆に、どうしても話す人が中心になってインタラクションが生まれにくかったり、 ビジュアル的な説明ができないので、トーク力が出てしまうと言った点が気になりました。 こういったことを活かして、次のような形式を試してみました。 LT会形式にしてみた LT会は(説明するまでもないかもしれないですが)1人の持ち時間が数分の短いプレゼン会です。 気負わず気軽に発表してみましょう。といったものです。 この形式にのっかり、先に質問と書き込むスペースをわたしておき、あとは適当に画像とか用いて質問に答えてもらいました。 事前に準備できて、かつ画像やリンクで表現できるため、口下手であっても情報量を補えるだろうという考えです。 実際にやったこと 事前に準備が必要なため、1週間くらい前にこんな感じでメンバーに依頼。 (この時期疲れがピークだったようで、毎日何かしら数字をミスっていました。。。) メンバーに渡したのは、質問がいくつも書かれた白紙の MIRO 。 この質問リストの中から答えやすいものや、話せるネタがあるものをいくつか選んで書いてもらうようにしました。 参加者全員が自身の土俵で話をしてもらいたかったためです。 質問リストの作成には 100の質問ひろば というサイトが役に立ったような立たなかったような感じでした。 質問ネタに困ったら使ってみてください。 実際に書き込んでもらう際は、全体の流れがわかるようにこんな感じにしてました。 右側のFrameには事前に名前が記入されていて、各自そこに書き込みます。 中央付近のShuffle部分は発表順を決めるために利用しました。 付箋に参加者の名前を書いておき、発表前にMiroの Totally Random というアドオンを使ってシャッフルします。 多少はランダム性があったほうが楽しい気がするんですよね。 やってみて まず、全体としては、発表形式かつ発表順が事前にわかっているため「飯を食う時間の確保」「気持ち的な準備」ができるので進行が非常に楽でした。 発表者としては、事前に画像を用いて書き込めること、答える質問も自由に選定できるため、自分の言いたいことだけを言える楽さがありました。 また、Miroの機能でスタンプで発表にリアクションできるため、よりインタラクティブな場が生じたように感じます。 LT会形式の特徴 参加者それぞれの認識が浅いとき 安定して進行させたいとき こんなときはLT会形式が良さそうです。 進行にランダム性を廃している点と、自己紹介になっている点で、より「人を知ること」にフォーカスした形式となりました。 PJ始まりたてで、自己紹介などからやる必要がある際には、こういった形式のほうがより解像度高くその人を知ることができて良さそうです。 逆に、すでに心理的安全性が保たれていて、話しやすい状況になっている場合、 この形式では対話は生まれにくいので、機会としてはもったいないかもしれません。 まとめ コロナ禍においてPJのあり方や企業への勤め方も大きく変わりました。 また、働き方も多様になり、弊社でもリモート前提で入社される方も増えてきています。 そうすると、対面であっても対面でなくても、情報や心理的の格差がなくパフォーマンスを発揮できるチームビルビルディングが必要性が増していくでしょう。 この時代の流れは今後も続くと思うので、多様性を維持して働きやすい環境というのは、どの会社でも直面している課題なのではないかと思います。 この記事をここまで読んでいただいたみなさまは、きっといろいろな工夫をされていると思うので、 「こんな工夫してるよ!」というのがあればぜひお話を聞きしたいです。 @naoto bando まで気軽にお声がけください。 以上です。明日のアドベントカレンダーはDataStrategyチームの杉さんの記事です。お楽しみですね!
はじめに この記事はBASE Advent Calendar 2021の15日目の記事です。 BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 2021年の5月に入社してから、アサインされるプロジェクトの仕事以外に社内 UI コンポーネントライブラリ「BBQ」のメンテナンスに取り組んでいました。 その中でも特に Storybook 周りの整理をする過程、Storybook の v5 から v6 へのバージョンアップとその自動化のプロセスを以下の記事にまとめました。 Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える TypeScript Compiler API で40の Storybook コンポーネントを storiesOf から CSF(Component Story Format)に置換した 本記事はこの続きにあたります。 BASE の社内 UI コンポーネントライブラリ開発において、ここまでは Storybook というツールをしっかり使うということを目的としていました。 しかし、ここからは Storybook の力を引き出して開発者にその恩恵を感じて貰い、DX を向上させるところまで進めたいと考えています。 本記事では、現状の BBQ の課題を解決しつつ、Storybook をフル活用するために Chromatic の利用を検討し、社内で議論して実際に使った後、導入に至った過程を紹介します。 このため、本記事は「BBQ の課題 → 課題の解決策の検討 → ツールの選定 → ツール導入後の開発フローの変更の紹介」という構成を取ります。なお、Chromatic のインストール方法や具体的な使い方は本記事では扱わず、 別の記事で紹介しています。 社内UIコンポーネントライブラリ「BBQ」の課題 BBQ とは何か BBQ とは、BASE で利用している社内用の UI コンポーネントライブラリです。 Vue2 と TypeScript、scss で記述しており、Storybook で表示確認をしています。Web アプリケーションの BASE とは独立したレポジトリで管理、運用されています。 一例を挙げると、商品が未登録のときに表示される「EmptyBox」というコンポーネントは、この BBQ の中に作られており、import で呼び出すだけで利用できるので各所で再利用が可能です。 商品が購入されていないことを表すUI Storybook ではこのように記述されています。 const Template = (args, { argTypes } ) => ( { components: { EmptyBox } , props: Object .keys(argTypes), render(h) { return ( <div> <section> <h1>・ボタンあり</h1> { /* empty box */ } <bbq-empty-box size= {this .size } icon= {this .icon } > <div slot= "text" > <p><span>まだ商品が登録されていません</span></p> <p><span>商品を登録してあなたのネットショップを作りましょう</span></p> </div> <bbq-button type= "submit" slot= "action" width= {this .buttonSize } icon= "plus" >商品を登録する</bbq-button> </bbq-empty-box> </section> <section style= { { marginTop: '30px' } } > <h1>・ボタンなし</h1> { /* empty box */ } <bbq-empty-box size= {this .size } icon= {this .icon } > <div slot= "text" > <p><span>商品が見つかりません</span></p> </div> </bbq-empty-box> </section> </div> ) } , } ); このように、 BASE のデザインを体現した再利用可能なコンポーネントライブラリが BBQ です。 他にも Button や Modal、Calendar などの多数のコンポーネントがあります。コンポーネントの粒度は Atomic Design の Atoms から Molecules 相当に絞られています。 BBQ の直近の課題 さて、BBQ の直近の課題は大きく以下の2点です。 既存のコンポーネントを改修した際に発生する DOM、CSS に起因する表示崩れを自動で検知できないこと 依存モジュールのバージョンアップに時間がかかること BBQ にはテストがありません。 ビジュアルリグレッションテストやコンポーネントテストがあれば、上記の2点は解決できるはずです。以前はスナップショットテストがあったそうですが、私が入社した時には既に CI 上でもローカルでも動かされていませんでした。 テストがないと、開発者は動いているコードの改修に対して萎縮してしまいます。 自分がバグを仕込まないか戦々恐々としながらコードを書き換えて何度も動作確認をし、最後は勇気を振り絞ってリリースをします。 もしそれでバグが出てしまったらロールバックをしなければなりません。その時間と手間に加えて、原因分析、事象の共有、再発防止策を考えるなど多くのことに時間が割かれてしまいます。 テストがある場合は、ローカルや CI でテストが落ちていればそれを修正するだけでいいのにもかかわらず。 また、共通コンポーネント集はどの企業にとっても素早い開発速度を支える重要なツールであり、デザインや開発のコストを大幅に減らし、ユーザーにリリースを届けるスピードを加速させる大事な資産です。 その資産を守り、健全に育てるためにはやはりテストの導入が必要だと考えました。 そこで、まずは何らかのテストを導入することを BBQ のメンテチームのメンバーに相談しました。 BBQ に対するテスト方法、ツールの選定 BBQ にはメンテナンスチームがあります。メンバーは自分を含めた5人のフロントエンドエンジニアで、プロジェクトの合間にボランティアでメンテナンスをしています。 ライブラリのバージョンアップや、コンポーネント全体の見直しで発見した issue の対応などを行なっています(12月からはアドベントカレンダー 6日目のアクセシビリティの記事 を作成した @rry が加わって6人になりました)。 上記の BBQ の課題をメンテチームに共有し、E2E や VRT(ビジュアルリグレッションテスト)、コンポーネントテストやユニットテストなど色々なテスト方法がある中で、 BBQ に最適なテストは何か議論をしました。 みんなで議論した結果、VRT が BBQ の課題を解決する手段として最も良いと結論づけました。 議論の中で挙げられた検討事項は以下のようなものです。 E2E テストはそもそも今回の課題に見合っていないとして除外。 BBQ はコンポーネントのライブラリ集なのでページをまたぐ画面遷移やバックエンドまたはモックも必要になるようなログインといった粒度の大きなテストは不要だから ユニットテストも除外。BBQ は Vue.js を クラスコンポーネントで記述しており、state を利用したロジックがほとんど。 このため、ロジックをユニットテストに切り出しにくいから コンポーネントテストは、少し議論したけどやはり却下。 BBQ のライフサイクルを考えると、たくさんコンポーネントを追加・改修していくフェーズは終了しており、開発自体は活発ではない。 全てのコンポーネントに対してテストを書くコストは大きい一方で、既存のコンポーネントは数年間運用してバグが出ていないことが確認されており、既存のコンポーネントに対する新しい機能の追加も今はほとんどないため VRT(ビジュアルリグレッションテスト) はちょうど良い。 現在は CSS の修正や、ロジックにあまり関係しない DOM のちょっとした修正がメインであり、開発者は表示崩れの有無を主に確認したいため。 スクリーンショットを撮って差分を表示するだけで実現できるため、コンポーネントテストより記述は少なくなる 以上が VRT を採用した理由です。 E2E やユニットテストを今は採用しなかった理由も記述しました。議論の中では最初に候補から外されたくらいメンバー間で不採用の理由は自明でしたが、本記事は意思決定の過程を紹介する記事なのであえて書き残しておきました。 コンポーネントテストは私から提案しました。BBQ は Input や Modal といったエラー状態や開閉状態を持つコンポーネントが存在するので、それらのテストもでき、ロジックが壊れていないことを担保できると考えたからです。 しかし、メンバーと議論して BBQ に対する理解を深めていく過程で、開発のホットな箇所をこそ守るべきだと考えを改め、VRT が適切だと判断しました。 幸い私は Storybook の VRT ツールである Chromatic の活用経験があったため、BBQ の課題は VRT で解決できるであろうことが予想できました。 課題の解決手段から考えるのではなく、テスト対象のアプリケーションの性質とその課題を考えることが重要だねとチームで確認しあいました。 そして VRT を導入しようとチームで決めた後は、 VRT を実施するツールの検討に入りました。 なお、これは将来に渡ってコンポーネントテストやユニットテストなどを導入しないということではありません。 実際、VRT の導入と並行して、BBQ にバグが出たときの再発防止策としてコンポーネントテストが導入され、過去にバグがあった一部のコンポーネントでデグレが起こらないようにテストが記述されました。 今回は BBQ 全体に関わるテストの導入という文脈であることをご理解頂ければと思います。 VRT を実施するツール。reg-suit と Chromatic VRT とは何か そもそも VRT(ビジュアルリグレッションテスト)とは何でしょうか。 システム開発の文脈でリグレッションという単語は「新規開発に伴って、既存の機能が正常に動作しなくなること」を指します。これはデグレとも呼ばれます。 リグレッションテストは、「コードを書き換えても以前の機能が正常に動作することを担保するテスト」です。それを視覚の面からテストしたものがビジュアルリグレッションテストです。 具体的には、画面(ブラウザ)にページやコンポーネントを表示してスクリーンショットを撮影し、現行の UI と改修後の UI を比較して差分の有無を検出することで実現されます。 reg-suit と Chromatic この VRT を実施するツールとして有名なのは reg-suit です(「レグスイート」と呼んでる人もいますが、suit の発音は「スーツ」です)。 reg-suit について調べた結果、今回は以下の理由から採用を見送りました。 reg-suit は画像の差分を解析して diff を表示してくれるツールであるため、画像は自分たちで別途準備する必要があるため reg-suit には便利な plugin が揃っているが、plugin の選定をしたり、設定ファイルを書いたり、S3 のバケットを準備したりする必要があり、Chromatic と比較すると準備量が多いため 先程記述したように自分は Chromatic の利用経験があったため、コマンドを一行実行するだけで Storybook をビルドして publish できる体験が忘れられませんでした。 しかも全てのストーリーに対してコミットごとの差分を解析、表示してくれる Chromatic は、導入するのも捨てるのも楽だと考えていました。 特に、Chromatic は Storybook のメンテナーが作成したツールであり、BBQ は Storybook で表示確認をしている点も考慮すると、今回は Storybook のエコシステムに乗った方が良いという判断をしました。 reg-suit が悪いわけではなく、こちらも上記のテスト方法の検討結果と同様に BBQ というアプリケーションに対しては Chromatic の方が適切だと判断しただけです。 なお、 Chromatic の具体的な使い方については別の記事をご参照ください。 以上のような意思決定の過程と結論をマネージャーに共有したところ、まずは無料プランで導入し、 BBQ のメンテナンスチームの開発メンバーが Chromatic を使った開発フローを体験した上で、良し悪しを判断しようということになりました。 そこで、実際に Chromatic を一部のブランチに適用していく準備を始めました。 Chromatic を効果的に活用するための準備 ストーリーの数を増やす Chromatic を BBQ に導入する前に、コンポーネントのストーリーのバリエーションを増やしました。 デザイン崩れを検知するためには、コンポーネントの様々なバリエーションを予め表示しておき、カバー範囲を広げないと VRT の効果が半減するからです。 コンポーネントを改修した際に、特定の Props を与えたときにだけ表示される状態の考慮が漏れていて、気づかないところでバグが発生している可能性もあります。 そのようなバグを未然に防ぐために、様々な Props を与えたコンポーネントのストーリーを作成して管理下に置くことが理想です。 具体的には、以下に挙げる Search コンポーネントのように、Storybook の CSF(Component Story Format)の機能を利用して、 Props を部分的に変えて一つのコンポーネントのバリエーションを網羅するようにしました。 import { action } from "@storybook/addon-actions" ; import { Story } from "@storybook/vue" ; import Vue, { VNode } from "vue" ; import README from "./README.md" ; import Search from "./Search.vue" ; export default { title: "Elements/Search/Search(Vue)" , parameters: { notes: { README } , } , } ; type Props = { name: string placeholder: string keyword: string disabled: boolean } const Template: Story<Props> = (args, { argTypes } ) => Vue.extend( { components: { Search } , props: Object .keys(argTypes), render(h) { const { name, placeholder, keyword, disabled } = this .$props return ( <div style= {{ padding: "40px" }} > <bbq-search name= { name } placeholder= { placeholder } disabled= { disabled } vModel= { keyword } onSearch= { (val: any) => this .search(val) } onChange= {this .change } onInput= {this .input } /> </div> ) as VNode } , methods: { input(val: string) { action( 'input' )(val) } , search(val: string) { action( 'search' )(val) } , change(val: string) { action( 'change' )(val) this .$props.keyword = val } , } , } ); export const Default = Template.bind( {} ) Default.args = { name: 'search' , placeholder: '商品名・説明から検索' , keyword: '' , disabled: false , } ; export const NoPlaceholder = Template.bind( {} ) NoPlaceholder.args = { ...Default.args, placeholder: '' , } ; export const WithKeyword = Template.bind( {} ) WithKeyword.args = { ...Default.args, keyword: 'おいしい肉' , } ; export const Disabled = Template.bind( {} ) Disabled.args = { ...Default.args, disabled: true , } ; さまざまな Props を渡す もちろん Search 以外にも Button や Input のような全てのコンポーネントに対してこのような変更を加えています。 Props のバリエーションを増やすことで、結果的にストーリーの数が当初の79から148に増えました。 GitHub Actions でコミットを push するたびにビルドする Chromatic を導入しても常に活用しなければ意味がありません。 そこで、ブランチごとに、またコミットの push のたびに Chromatic をビルドするために GitHub Actions(GHA) を活用しました。 Chromatic 用の GHA のテンプレートは公式で用意されています。 このため、開発者としてやるべきことはこのテンプレートを使うことと、ビルド用のトークンを GitHub レポジトリのシークレットに埋め込むことだけした。これで各ブランチで Chromatic をビルドする準備ができました。 Chromatic の効用をチームで体感する dependabot が作成するブランチに適用する 便利なツールを導入した後はチームで運用し、ツールに対して知見をチームで溜めていくことが次のステップです。 BBQ のチームメンバーが Chromatic を体験してみるにあたり適切な粒度のタスクを探したところ、モジュールのバージョンアップデートがちょうどいいように思われました。 従来、 dependabot によってバージョンアップデートのブランチが作成されたときは、担当しますと手を上げた開発者のローカル環境で手による動作確認がされていました。 具体的には、 Storybook をローカルで立ち上げて全部のコンポーネントを表示、チェックして、以前のバージョンと比較して問題ないことを確認していました。 しかし、この手法は確認に時間がかかる上に、「ビルドは通ってるし目視確認はしたけど本当にバグは出ていないと自信を持ってまでは言えない」という雰囲気がメンテチーム内にありました。 このため、心理的な負担もありモジュールのバージョンアップデートの対応は即座になされるわけではありませんでした。 ただし、脆弱性のあるパッケージのバージョンが上がっていないなどクリティカルな問題があるというわけではなく、dependabot があるパッケージのバージョンアップのブランチを作ってみんな気づいているけど、2〜3週間放置されている間に次のバージョンがリリースされたというような状況です。 パッケージのバージョンアップデートの理想状態は、CI でテストとビルドをしてエラーが出たらアップデートをやめ、CI が Fail しなければ master マージするという運用です。 テストがある場合は人がアドホックに動作確認をする必要がなくなります。しかし、BBQ にはテストがないためそれができていなかったのです。 差分がないから安心してリリースできる この課題は Chromatic で一定程度解決できると予想していました。 BBQ は UI コンポーネント集のため、ロジックに関するライブラリはほとんど入っていません(date-fns くらい)。依存ライブラリは Webpack の loader や eslint、stylelint といったビルド時に必要なツールくらいです(このため、パッケージのアップデートが遅れてもクリティカルな問題にはならなかったのです)。 dependabot が作成したブランチの CI を実行し、Chromatic で Storybook をビルド、表示するようにしました。 Chromatic 上ではブランチの baseline と呼ばれる時点のスクリーンショットとの差分が解析されます ( 詳しくはブランチとベースラインの解説は公式ドキュメントをご覧ください。 )。 テストと同様に、差分があれば Chromatic 上で目視チェックをし、差分がなければ問題ないとして master マージをすれば良いのです。 Chromatic が Storybook をビルドして URL を発行してくれるため、わざわざローカルで Storybook を立ち上げる手間が省けます。 また、差分があるコンポーネントは Chromatic がピックアップしており、差分のがある箇所もハイライトをつけてくれているため、抜け漏れがありません。 これは動作確認の時間の大きな節約になる上に、開発者としても不安なくパッケージアップデートを取り込んだ新バージョンの BBQ リリースできます。 実際にいくつかのパッケージのバージョンを上げましたが、以前と違ってとても楽になりました。 人間の目では気づかない差分もカバーしてくれる Chromatic の試用期間中に、アイコンに変更があるという差分が検知されました。 しかし、元のアイコンと新しいアイコンを見比べてみても全く同じように見えます。エンジニアメンバーが手を加えた覚えはなく、Chromatic のご検知かと疑いながら念のためデザイナーのチームに確認してみました。 すると、デザイナーから「特定の環境下で表示がおかしかったので、少し前に SVG の不要なパスを削除した。見た目には変更がない」とコメントがあり、それはまさにこの差分のことでした。言われてみると、確かに以前のアイコンの線が少し太いように見えます。 Chromatic が検知したアイコンの差分 今回は拡大しているのでなんとなく差が分かりますが、Storybook 上のアイコンは 14px という小さいサイズで表示されているため、目で見ても差分が分かりませんでした。 Chromatic はこんなところまで検知してくれるのかという驚きで、BBQ メンテチームが盛り上がると共に Chromatic に対する信頼感が増しました。 VRT がある開発フロー 開発フローに Chromatic を組み込む Chromatic は開発フローの中で、コードレビューのフェーズで活用しています。 コードと表示に変更があった場合、その表示の変更は意図したものであるかを確認するためのレビューです。コードレビューの中に、デザイン面の差分のレビューを組み込むイメージです。 Chromatic は GitHub と連携可能であるため、PR の画面からビルドの状態を知ることが可能です。下記の画像では「UI Review」と「UI Tests」に緑のチェックマークがついていますが、もし表示に差分があるときは黄色の丸いマークが表示されます。 GitHub の PR の Chromatic の状態のフィードバック Chromatic のサイト上で差分のあるコンポーネントの画像がピックアップされているので、レビュワーはそのコンポーネントに対するレビューをします。 気になる点があれば Chromatic 上でコメントを記入し、問題なければ Chromatic 上で Accept をします。 上記の画像に緑のチェックマークがついているのは、そもそも差分がない場合か、全ての差分に対してレビュワーが Accept をした場合のどちらかです。この画像では差分があったけど Accept しているため、「UI Tests」の説明に「4つの変更が基準として Accept された」と書かれています。 「レビュワーの責任範囲はコードレビューまでで、動作確認や表示確認はブランチの開発者の責務」と考える方もいますが、BBQ では Chromatic の導入に伴い表示確認もレビュワーにお願いすることになります。 ただし、Chromatic が差分を予め検出しているため、レビュワーの負担が大きく増えるようなことはありません。 むしろ、レビュワーが表示の変更点を網羅的に知れるという知識の標準化に役立つ上に、見た目の変更を伝えるために前後比較のスクリーンショットを撮って PR に貼り付けるというレビュイーの手間も省けます。 これが VRT を組み込んだ開発フローです。 Chromatic に対する BBQ メンテチームのメンバーの意見 BBQ メンテチームのみんなに Chromatic を一通り触ってもらった後、マネージャーに Chromatic を触った感想を報告する会が開かれました。そこでは以下の声を聞くことができました。 手元で Storybook を立ち上げていたが、その必要がなくなった 変更点をまとめて Chromatic で見れる 変更があったコンポーネント数が表示される 差分が全て記録されているので安心感がある 人の目でわからない差分を検知してくれた アイコンが変更されていた もちろん懸念点も少しありました。例えば「Chromatic 上のコメントが PR や Slack に通知できず、メールでの通知だけである上に、URL がコメントへのリンクになっていない」というものです。ただ、懸念点がどれもツール側の話なのでこちら側でできることはなく、とりあえず改善要望を出しておきました。 以前から折を見て議論を重ねており、マネージャーにも逐次共有して意見を聞いてきたため、BBQ に VRT を導入することに対する反対や Chromatic ではない別のツールが良いなどの声は出なかったためホッとしました。 以上が、BASE の BBQ という共通 UI コンポーネントライブラリのレポジトリに Chromatic を導入するという意思決定の過程でした。 終わりに フレームワークやライブラリ導入の技術選定に関する記事は枚挙にいとまがないですが、外部ツールを選定するためのプロセスや意思決定に関する記事はあまり見ないなと思い、本記事を書いてみました。 Chromatic の導入を例に挙げていますが、以下の点を意識して読んで貰うとより一般的な視点が得られると思います。 適切なツールを選ぶためには、まず現在開発しているアプリケーションの性質をつぶさに観察すること 社内で使用するツールはマネージャーと二人三脚で選定すること。ツールの調査や社内に使い方を広めるのは現場のエンジニアの役割であるが、導入可否の最終決定者はマネージャーであるため ツールを使い続けるためにチームからフィードバックを受けること。一人が特定のツールの導入を陣頭指揮してもいいが、ツールを使うのはその人だけではないため なお、 Chromatic の具体的な使い方については別の記事で紹介します。 Chromatic の導入や使ってみたいツールの導入に役立てば幸いです。 明日の記事は坂東さんの「10人以上のPJでリモートランチ会をするときの工夫したこと」です。
この記事は BASE Advent Calendar 2021 の14日目の記事です。 こんにちは。UIデザイナーのノムラ( @nomjic )です。2021年の初め頃にデザインリサーチPJを開始して、3〜4名のメンバーでここ一年間、定性リサーチにトライしてまいりました。その内容を本記事に書いていきたいと思います。 デザイン業務の傍らで実施した活動につき、リサーチ内容の深度・精度はいささか低めです。すでにがっつりリサーチに取り組んでいる方よりも、「 定性リサーチ活動を始めようとしている人、始めたいと思っている人 」にとって参考になる記事にできたらいいなぁと思いつつ、書かせていただきます。 デザインリサーチとは 我々の行っている活動を「デザインリサーチ」と呼んでいますが、 定性リサーチ、UXリサーチ、ユーザーリサーチ 、といった言葉に読み替えていただいても問題ありません。ゆくゆくはプロトタイピングやユーザビリティテストも取り入れていきたいと思いますが、現状では「 ユーザーにインタビュー調査をしていろいろ実態を探っています 」という一言でほぼ言い尽くせるリサーチ活動です。 以前に書いたブログ記事 で私の思うところの「デザインリサーチとは」を述べていますので、ご興味お有りでしたら併せてお読みいただけると幸いです。 今年実施した内容・概要 4つのリサーチを行いました。1回のリサーチで数名〜10名弱の方からお話を聞いています。 インタビュワーや書記役は主にPJメンバーが行っていますが、他の社内メンバーに参加してもらうこともあります。 「まずはユーザーとデザイナー(およびPM、エンジニア)が対話する場を作ろう」という形で動き出し、得られた知見を蓄積しつつ、なるべく多くのメンバーを巻き込んで社内に定性リサーチ文化を醸成する、ということを目指してきました。 実施した4つのリサーチについて、以下、概要を述べます。 リサーチその1 「まずは話を聞こう」 リサーチ内容 2021年 2月にインタビューをし、3月にかけて分析をしました。9ショップ・10人のBASEショップオーナーさんから話を聞いています。アパレル系、雑貨系、食品系などさまざまなジャンルのショップから話をお聞きしました。 ※ インタビュー設計や分析の流れは、 以前の記事 に書いたデザインリサーチワークショップの内容に沿っています。 所感 特に仮説等を立てずに「とにかく話を聞いてみよう」と始めたリサーチだったため、得られた成果は少々散漫というか、どう活用するかをイメージしづらいものでした。 とは言え、何はともあれ 「ユーザーに連絡して日程調整し、話を聞かせてもらって分析し、社内に報告する」という一連の流れ をデザイナー主導で行えたというだけでも、一つの大きな成果であったと思っています。 リサーチその2 「属性ベースでセグメント切って深掘り」 リサーチ内容 2021年 5月にインタビューをし、6月にかけて分析をしました。7ショップ・9人のアパレル系BASEショップオーナーさんから話を聞いています。 所感 「インタビュー対象の絞り込み(アパレル限定)」「質問項目の具体性アップ」を行うことで、1Qで得られた成果に比べてより具体的・実用的な情報を得られました。 基本的には「その1」と同じ流れでリサーチを行なったため、だいぶスムーズに一連の流れをこなせています。また、この回ではリサーチPJの外部に協力者を募り、他職種メンバーも巻き込んでいます。(インタビュワーや書記を行ってもらいました。) リサーチその3 「退会したユーザーとも話してみよう」 リサーチ内容 「その2」と並行して、2021年 6月にインタビューと分析を行なっています。BASEから別のECサービスへ移転した3名のショップオーナーさんからお話をお聞きしました。 このリサーチではいくつか仮説を立てて臨みました。結果、かなり具体的で、開発に役立ちそうな情報を得ることができています。 所感 BASEにマッチしなかったユーザーからも意見を集めたい、という想いから実施した「退会したユーザーへのインタビュー」だったのですが、実際に話を聞けた3名は「一度BASEから移転したが、今はBASEに戻ろうとしている」という人たちでした。非常に参考になるお話をたくさん聞けたものの、いささか偏りの大きいリサーチになってしまいました。 リサーチその4 「関心ごとベースでセグメント切って深掘り」 リサーチ内容 2021年8月にショップオーナーさんの関心ごとを問うアンケートを行ったのち、アンケート回答者を対象に 9月にインタビューをし、10月・11月にかけて分析をしました。アンケート結果から「集客・広告に強い関心を持っている」と解釈できる9ショップ・9人のBASEショップオーナーさんから話を聞いています。 所感 このリサーチでは、「傾向」と「課題」の2つの観点で、得られた情報をまとめています。 リサーチも4回目ということで、インタビューおよび分析はだいぶスムーズになってきました。反面、「得られた結果が開発に役立つか?」という面ではまだ不十分である感は否めません。 「その2」同様、この回でもリサーチPJ外からの参加者を募っており、より多くの社内メンバーに定性リサーチを経験してもらえました。 定性リサーチ結果を、どう開発に役立てるか リサーチ結果・知見の社内シェア この一年間で行なったリサーチで得られた情報(議事録、録画データ、インサイト)はスプレッドシート上に一覧表の形でまとめて、社内でシェアしています。また、上記「その1〜4」のリサーチごとに報告会を行うことで、口頭による社内へのシェアも行なっています。 リサーチその2、その4ではリサーチPJ外のメンバーにもインタビューに参加してもらっており、直接的に定性リサーチを体験してもらう形で、ユーザー実態に触れてもらっています。 このように、 シート(ドキュメント)に情報を蓄積する 報告会を実施して知見・成果をシェアする 直接参加して体験もらう という3つのルートで組織内にアウトプットしているのですが、この3つの中で 「直接参加」が圧倒的に意義が大きい(学びが深い) というのが、リサーチPJを率いている身としての本音だったりします。 ドキュメントや報告会では全然伝わらないナマの実態というものがあり、それはプロダクト開発へのマインドにも大きく影響する、と思っています。 (これってライブイベントみたいだなぁ、と思って書いた note記事 などもありますので、ご興味あったらお読みいただけたら幸いです。) 「直接的な体験」と、「蓄積したドキュメント」のバランス 直接参加してもらうのが一番学びが深いのは明らかなのですが、現実に「社内メンバーみんなに参加してもらう」というのは流石に無理があります。(その状態を目指したい、という想いはあるのですが) どうしても参加者が特定のメンバーに限られてしまいます。 組織で活動する以上、その活動の成果は蓄積して「新規メンバーでもアクセスできるように」「当時の担当者が不在でも活かせるように」しなくてはいけません。 いわゆる「属人化を避ける」というやつですね。 なので、直接参加してもらって体験してもらうだけでなく、ドキュメントや映像のような、後から第三者がアクセスできる形で蓄積していかなくてはいけません。 落とし所としては、以下の2つを交互に行うのが良さそうかなと思っています。 実態探索型リサーチ なるべく社内メンバーを広く巻き込みつつ、ユーザーの実態を探るインタビューを行う。 直接的にプロダクト改善に寄与する情報を得ることよりも、「ユーザーを 知ること 」「ユーザーと 対話すること 」に重きを置く。 仮説検証型リサーチ 具体的な仮説を立てて、インタビューにより仮説を検証する。 ユーザーを知ることよりも、プロダクト改善につながる情報を得ることを重視し、ドキュメントとして社内で共有した際に 共感 や 納得感 を呼び起こせる、プロダクト開発メンバーの行動につながりやすいアウトプットを目指す。 そもそも、リサーチ活動の在り方として 現在のデザインリサーチPJは、開発PJや企画PJとは独立した形で、ユーザー実態情報の収集とリサーチノウハウの蓄積を行っています。 この状態では、プロダクトの直接的な改善には繋がりにくく、そのためプロダクトを良くすることに心燃やしているBASE社員たちの関心を集めづらいです。リサーチ活動への参加を促したりリサーチ成果の共有を行っても、期待したほどの反響は得られません。 開発PJや企画PJの内容を鑑みて、それらを評価・検証するような動きをするとか、それらPJの発足の手前段階で関わっていくなど、リサーチ活動の在り方そのものを考えるべきでは?という想いもあり、検討をしているところです。 まとめ いろいろ書きましたが、今の活動形態での良き成果を目指しつつ、活動形態や体制の在り方を検討(MGRと相談)して、より質の高いリサーチ活動を行っていきたいと考えております。 以上です!最後までお読みいただいた方、ありがとうございます。 明日のアドベントカレンダーはフロントエンドエンジニアのkushibikiさんの記事です。お楽しみに!
この記事は BASE アドベントカレンダー 13 日目の記事です。 はじめに こんにちは。 BASE BANK 株式会社 Dev Division にて Software Developer をしている永野( @glassmonekey )です。 普段はバックエンドエンジニアとして、Go/Python/PHP を主に書いてたりします。 最近はチームの分析基盤づくりとかもやってたりします。 そのことについて先日書いたりもしたので、もし良かったらご確認ください。 devblog.thebase.in 私達のチームでは、「BASE」でショップを運営しているショップオーナーが簡単に資金調達をできる「 YELL BANK 」というサービスの開発・運営しています。 thebase.in あるプロジェクトで、ショップオーナーへのコミュニケーション手段に HTML メール を新しく作ることになりました。その際に HTML メール のコーディングも自分たちで行う必要がありました。 今回は その際に行った HTML メール 制作を MJML を使うと楽にできたのでその紹介になります。 HTMLメールの事情 大前提として、 HTML メール を使うとリッチな内容でコミュニケーションを取ることができます。 昨今のメーラーだと基本的には対応しているので使いたくなります。 弊社でも HTML メール の事例で 4月に購入完了メールをリニューアル しました。 では早速 HTML メール を作りましょうと言っても、なかなかそうは行きません。 なぜなら一般に、 HTML メール のマークアップと Webページ のマークアップと毛色が違うからです。 特に、 HTML メール のマークアップは 基本的なCSS はインライン 、 どのような環境でも安定しているテーブルレイアウト が基本となる事情があります。メーラーそれぞれの描画任せが要因だったりするようです。 参考: How to Create HTML Emails Using the Table Element [+ Templates] では HTML メール でどのタグが利用できるのでしょうか。 それに関しては大体は以下のサイトで簡単に調べることができます。 www.caniemail.com 例えば、「BASE」ならぬ <base>タグ だと以下のような対応状況のようです。緑は完全に対応、黄色は部分的に対応、赤は未対応です。 ちなみに対応状況は手動で調べられてるようです。とはいえ最初に見る状況としては大変助かっています。 Because every test is done manually, some features might not have been tested on every email client. Can I email… Email Client Support Scoreboard また製作者の方である @HTeuMeuLeu さんの記事の1つの Outlook is not an Email Client からは各社メーラへの対応状況の複雑さの苦労が垣間見えます。 以上のことから基本的には HTML メール のコーディングは避けたほうが無難です。無理してコーディングする必要がなければ、 Send Grid などの SasS を使うことが良いでしょう。 しかし、コーディングしないといけないときもあるでしょう。そのときに便利なものが MJML となります。 MJMLとは MJML とはレスポンシブな HTML メール を作ることのできるフレームワークです。 mailjet社 が開発しています。 だいたいのことは ドキュメント に載っているので参照ください。 最初は オンラインエディタ で試しながら始めてみるのがおすすめです。 これを使うと複雑なテーブルタグを書かずとも、簡単に HTML メール を書くことができます。 具体例を見てみましょう。 mjml.io 例) < mjml > < mj- body > < mj- section > < mj-column > < mj-image width = "100px" src = "https://mjml.io/assets/img/logo-small.png" ></ mj-image > < mj-divider border - color = "#F45E43" ></ mj-divider > < mj-text font- size = "20px" color = "#F45E43" font-family= "helvetica" > Hello World </ mj-text > </ mj-column > </ mj- section > </ mj- body > </ mjml > 見た目はこのようになります。 ちなみに、実際の HTML はこのような形で出力されています。 MJML で書くと大分簡略化されていることがわかります。 <!doctype html> < html xmlns= "http://www.w3.org/1999/xhtml" xmlns:v= "urn:schemas-microsoft-com:vml" xmlns:o= "urn:schemas-microsoft-com:office:office" > < head > < title > </ title > <!--[if !mso]><!--> < meta http-equiv = "X-UA-Compatible" content = "IE=edge" > <!--<![endif]--> < meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1" > < style type = "text/css" > #outlook a { padding : 0 ; } body { margin : 0 ; padding : 0 ; -webkit- text-size-adjust : 100% ; -ms- text-size-adjust : 100% ; } table , td { border-collapse : collapse ; mso- table -lspace: 0pt ; mso- table -rspace: 0pt ; } img { border : 0 ; height : auto ; line-height : 100% ; outline : none ; text-decoration : none ; -ms- interpolation-mode : bicubic ; } p { display : block ; margin : 13px 0 ; } </ style > <!--[if mso]> <noscript> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> </noscript> <![endif]--> <!--[if lte mso 11]> <style type="text/css"> .mj-outlook-group-fix { width:100% !important; } </style> <![endif]--> < style type = "text/css" > @media only screen and ( min-width : 480px ) { .mj-column-per-100 { width : 100% !important ; max-width : 100% ; } } </ style > < style media = "screen and (min-width:480px)" > .moz-text-html .mj-column-per-100 { width : 100% !important ; max-width : 100% ; } </ style > < style type = "text/css" > @media only screen and ( max-width : 480px ) { table .mj-full-width-mobile { width : 100% !important ; } td .mj-full-width-mobile { width : auto !important ; } } </ style > </ head > < body style = "word-spacing:normal;" > < div style = "" > <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> < div style = "margin:0px auto;max-width:600px;" > < table align = "center" border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "width:100%;" > < tbody > < tr > < td style = "direction:ltr;font-size:0px;padding:20px 0;text-align:center;" > <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> < div class = "mj-column-per-100 mj-outlook-group-fix" style = "font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;" > < table border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "vertical-align:top;" width = "100%" > < tbody > < tr > < td align = "center" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < table border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "border-collapse:collapse;border-spacing:0px;" > < tbody > < tr > < td style = "width:100px;" > < img height = "auto" src = "/assets/img/logo-small.png" style = "border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width = "100" /> </ td > </ tr > </ tbody > </ table > </ td > </ tr > < tr > < td align = "center" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < p style = "border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:100%;" > </ p > <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">   </td></tr></table><![endif]--> </ td > </ tr > < tr > < td align = "left" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < div style = "font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;" > Hello World </ div > </ td > </ tr > </ tbody > </ table > </ div > <!--[if mso | IE]></td></tr></table><![endif]--> </ td > </ tr > </ tbody > </ table > </ div > <!--[if mso | IE]></td></tr></table><![endif]--> </ div > </ body > </ html > MJML入門 では実査に、 MJML を触ってみましょう。 プロジェクト構成 最初にメールを作成するためのプロジェクトを準備しましょう。 今回説明するディレクトリ構成は以下としました。 ├── dist ├── mjml │ ├── section │ └── template ├── package.json └── yarn.lock 各ディレクトリの責務は以下の形です。 今回、メールのコンポーネントは再利用 する/しない ぐらいの区別しかしていません。 MJML によるメール作成の知見が社内で成熟してくれば、変わる可能性はあります。 dist …生成される html を出力します。 mjml … 生成するための MJML ファイル群を置きます。 section… Header や Footer など再利用するコンポーネント用 mjml ファイル群を置く所 template ... 1 メールのバリエーションと対応した mjml ファイル群を置く所 インストール 基本的には npm で mjml-cli を入れたら終わりです。 $ npm install mjml package.json には npm run build などで build できるように、以下を追記しておきます。 " scripts ": { " build ": " mjml ./mjml/template/*.mjml -o ./dist " } 基本構成 基本的には ドキュメント を見つつ、 テンプレート を見て、参考にさせてもらうのが近道です。 基本的には HTML と同じ用なセマンティクスを持っているのでそれに従って記載するといいでしょう。 ローカルで開発時は VS codeの拡張 を入れると previewで確認しつつ編集できるので便利です。 基本構成例 < mjml > < mj- head > < mj-attributes > < mj-text color = "red" /> < mj-class name = "content" color = "black" /> </ mj-attributes > </ mj- head > < mj- body > < mj- section > < mj-column > < mj-text > Hello, World(Red) </ mj-text > < mj-text mj- class = "content" > Hello, World(Black) </ mj-text > </ mj-column > </ mj- section > </ mj- body > </ mjml > 基本的なマークアップの規則としては、いわゆるグリッドレイアウトになります。 メールは複数のセクションから構成されますが、基本的な 1 つのセクションの構成は mj-section > mj-column > コンテンツ要素 から成り立ちます。 主に利用するタグは以下の流れになります。 mj-section … 基本的なコンテンツの区切りを定義します。 mj-column … 複数使うことでモバイルでは 1 列、PC では 2 列といったレスポンシブを実現します。 コンテンツ要素 mj-text … テキスト表示 mj-image … 画像表示 などなど 共通の装飾について 文字色などの装飾は個別の要素に指定すれば変更できます。 < mj-text color = "red" > 赤色 </ mj-text > しかし、余白や文字サイズなどは統一感出したいでしょう。その場合は mj-class を使って予め用意しておくことで同時に複数の要素に共通的な装飾を施すことも可能です。 準備は以下のように mj-attribute に class 名を用意しておきます。 < mj- head > < mj-attributes > < mj-class name = "content" font- size = "14px" font-family= "Arial" line- height = "24px" padding= "0" /> < mj-class name = "annoation" font- size = "12px" font-family= "Arial" line- height = "18px" padding= "0" /> </ mj-attributes > </ mj- head > 利用自体は簡単で mj-class を指定するだけです。 < mj-text mj- class = "content" > 半角スペースで複数のクラスの適用もできます。 < mj-text mj- class = "A B" > ファイル分割について ある程度メールの種類が増えてくると、ヘッダー部分などの共通部分を分離したくなりますよね。 それには、 mj-include を使うことで実現します。 たとえば共通の分割線パーツコンポーネントを section/common/divider.mjml で用意してたとします。 < mj- section > < mj-column > < mj-divider border - width = "4px" border - color = "#F0F1F4" padding= "0" /> </ mj-column > </ mj- section > 呼び出し側としては以下のように相対パスで記述するだけで完了です。 略… < mj-include path= "../section/common/divider.mjml" /> 基本的にはセクション単位で扱うことが多いはずなので、共通のパーツも section 単位で作っておくと再利用がしやすいです。 パーツ分割における注意事項としては、パーツ側のコンポーネント編集時には VS Code の preview が動作しないことが挙げられます。 おそらく preview には mj-body などの要素が必要だが、パーツ側コンポーネントにはそれらが含まれていなからだとは思われます。 なので、最初から分離しようとせずにコーディングがある程度完了してから分離したほうが作業効率は良いです。 余談 余談ですが私自身は MJML のことは知らず、同僚の @sam8helloworld が同僚となる前に教えてくれたという個人的エピソードもあります。大変助かりました。 htmlメールのいい感じのフレームワークとかない? sendgridで素直に作れ?はい。 — エターナル・フィールド (@glassmonekey) 2021年5月6日 HTMLメールはご存知かもですが仕様が独特かつHTMLのルールも古いので人がコーディングするのはただただコスト高いっすね それでもコーディングベースで細かくやりたいならMJML, SaaSで基本ノーコードでやりたいなら無難にsendgrid, mailchimp ,b-dashってところですかね — さむ (@sam8helloworld) 2021年5月6日 おわりに MJML を紹介しましたがいかがだったでしょうか? HTML メール は令和となった現代においてもなかなか難しいので、ぜひ皆さんも MJML をご活用ください。 無論実際のメーラー上の確認は最終的には必要ですが、手元でプレビュー見つつローカルで大半の作業を完結できるので、開発体験は最高でしたし何より工数の削減にも繋がり大変重宝しました。 最後に宣伝ですが、まだまだやれてないことがたくさんあるので、一緒にプロダクトを成長させていく仲間を募集中です。 open.talentio.com 明日は 2021 年のデザインリサーチ振り返りです。楽しみですね〜
この記事はBASE Advent Calendar 2021の12日目の記事です devblog.thebase.in ごあいさつ はじめましての人ははじめまして、こんにちは!フロントエンドエンジニアのがっちゃん( @gatchan0807 )です。 この記事は Denoの公式Docsを読んでみた前編 の続きで、前回得た知識を使って実際にDeno + GitHub Codespaces環境でSlack Botを開発してみよう!の回になります。 もしDenoについて詳しく知りたい方は前回の記事からご覧いただけますと幸いです!🙏 どんなSlack Botを作るのか まず完成品のイメージを共有してから実際に実装する内容に触れていきます。 少し話は脱線するのですが、BASEではコミュニケーションツールにSlackを使っており、その中で独自のSlack Appを作成してChatOpsを実現しています。 主だったところで言うと、STGとは別に複数用意されている検証環境のデプロイや起動、特定の開発中ブランチの適用などをエンジニアはもちろん、PMの方々も日常的に行っています。 下記はあくまでイメージですが、下記のようなコマンド的なテキストをBotにメンションして、デプロイを行ったりしています。 @dev-bot deploy repository inspection1 feat/something-branch-name 詳しくは、以前投稿されたこちらの記事をご覧ください。 BASEにおけるSlack活用術を大公開!〜Slackで始める業務改善〜 - BASEプロダクトチームブログ そういった環境があるため、Slack Botを使ったコマンドの実行に一定のリテラシがある前提で下記のようなサブコマンド付きのSlack Botを作成していきます。 作る予定のBotとそのサブコマンドたち 今回作成するのはご覧の通り、登録されたメンバーの一覧を色んなパターンでランダムにごにょごにょするBotです! @random-bot [command] [group] [member-name] ~~~~~~~ 便利コマンド系 ~~~~~~~ @random-bot (help|h) 👉 ヘルプの表示 @random-bot ping 👉 起動確認 ~~~~~~~ データ登録・削除系 ~~~~~~~ @random-bot create [group-name] 👉 グループ(メンバーを追加する枠)の作成 @random-bot (disband|delete-group|remove-group) [group-name] 👉 グループ(メンバーを追加する枠)の解散 @random-bot list [group-name] 👉 グループ内のメンバー一覧の表示 @random-bot add [group-name] [member-name] 👉 グループにメンバーの追加(メンバー名はテキストもメンションもどちらも登録可能です) @random-bot (delete|remove) [group-name] [member-name] 👉 グループからメンバーの削除 ~~~~~~~ シャッフル実行系 ~~~~~~~ @random-bot random-sort [group-name] 👉 グループ内のメンバー一覧をシャッフルして並び替え @random-bot pick [group-name] [number] 👉 グループ内から指定の人数をランダムでピックアップする @random-bot separate [group-name] [divide-count] [merge-option] 👉 グループ内のメンバーを指定の人数ごとにグループ分けする BASEのSlackで下記のようなユースケースを観測していたので、それらのニーズにマッチしたシャッフルのパターンをいくつか用意しています。 デイリーの今日の一言でのランダム化のニーズ 自分が今所属しているプロジェクトチームでは、デイリーで今日の一言とその日やっていることを共有する時間を取ってコミュニケーションと進捗共有を行っています。 その発表順が固定化されないように、 スッキリす の運勢ランキングが一番良い人からやってみたり、 順番ぎめ.com を使ってランダムに並び替えたりしてみていました。 @random-bot random-sort [group-name] はこのユースケースを満たすために実装していきます。 peer1on1でのランダム化のニーズ 続いて、こちらも自分が所属しているフロントエンドのチームではpeer 1on1という制度を運用しており、ランダムに選ばれた2〜3人で雑談をする時間を週に1回取っています。 peer 1on1の詳しい解説はこちらも過去投稿されたこちらの記事に譲りますが、現在もGASで作られたスプレッドシート上で管理されています。 そのシャッフル、本当にシャッフルですか?何気ない落とし穴にハマった話 - BASEプロダクトチームブログ @random-bot separate [group-name] [per-number] はこのユースケースを満たすために実装していきます。 読書会の司会選出でのランダム化のニーズ 最後に、別のチームで定期的に行われている読書会の司会選出のニーズです。 @random-bot pick [group-name] [number] はこのニーズを満たすために実装していきます。 こちらは標準搭載されている Slackのカスタムレスポンス機能 を利用して、(Slackbotのレスポンスを複数パターン登録して)ランダム機能が実現されています。(これを初めて見た時、正直めっちゃ賢いな…!って思いました) ただ、この方法だと想定外のチャンネルで反応してしまうことがあるので、指定するキーワードには注意ですね⚠ 以上のようなニーズがあったので、今回のSlack Bot作る題材にちょうど良いや!と思ったのが作ろうと思った理由です。 さらに、Slackのリマインダー機能と組み合わせて、決まった時間に自動的にシャッフルした結果を出してくれるようになれば手間も減るので良さそうです🙆 実際にDeno環境でSlack Botを作っていく 前回の記事の最後で、Slack社が発表した Denoで提供されるSlack Appの次世代開発プラットフォーム のベータプラットフォームに参加できればそちらで開発する!と言っていたのですが、残念ながらベータプラットフォームへの参加は叶わなかったので、有志がNode.js用Slackライブラリを Deno向けに書き換えて提供してくれているライブラリ の方を利用していきます。 前回の記事がほとんどの項目をStep by Stepで書いたためにめちゃ長記事になってしまったのを反省したので、詳細は公式ドキュメントに譲り、ハマりどころとポイントをピックアップして書いていくことにします! また、今回のコードの全容は こちらのGitHub に公開しているので、もしご興味あればご覧ください👀 コミットメッセージ込みで試行錯誤の履歴が残っていますし、記事公開後もちょこちょこリファクタしたりする予定です(PRやIssueも歓迎です🙆) 実際に作ってみた所感 実際に作ってみてどうだったのよ?という感想を見に来られている方もいらっしゃるかと思うので、まず最初にそちらを書いておきます。 DenoでのSlack Botの開発はロジック部分は基本的にはNode.js上の開発体験とほぼ変わらない。でも、周辺知識の入れ直しで大変。 特に https://esm.sh や https://skypack.dev などの挙動を読み取りながらライブラリバージョンを指定するの辛かった。 まだまだエコシステムもライブラリも発展途上のため、詰まった時に情報がなくライブラリの中を読みに行って自力でデバッグになるのは大変。 逆にライブラリたちがURL管理なのでライブラリを読みに行く労力は少し減っている部分はある。 Deno Deployはまだまだベータ版。すぐに対応できなさそうな問題にぶち当たったので代替としてCloud Runを使った。 今回根幹として使っていたライブラリがDeno Deploy実行環境から提供されていないAPIを使っていて死ぬという事があった😢 GitHub Codespaces上でSlack Botを開発するのは、体験が良い面もあれば微妙な面もあった。 良い面👍: ポートフォワーディング機能で簡単にHTTPSのURLを公開できるので、SlackのWebHookやEvents APIの指定がngrokとか用意しなくていいのめちゃ楽。 良い面👍: 手元のPCのVSCodeからも、iPadのブラウザ上からも同じ環境を見れるので出先でちょこっと直しやすかった。 微妙な面🤔: ポートフォワーディングのタイムラグが体感1分ぐらいあるので、再起動時とかはSlack Bot側の反応が全くなくなってどこまでデータが届いているのか分からずにちょくちょく集中力が切れる。 微妙な面🤔: Denoのライブラリキャッシュ取得後の反映に微妙にラグがあって何度かファイルの読み込み直しやVSCode/ブラウザの再起動が必要だった。 今回公式ドキュメントから仕入れた知識のみで開発することで、開発自体は問題なく行えるものの所々辛い部分を見てきたので、次回以降は aleph.js や packup 、 Velociraptor 等のDeno周辺ツール群も組み合わせて実装を進めてみたいなと思いました🙆 当初の予定と実際のギャップ 合わせて、実際に作ってみてわかった、初期の自分の中での期待とギャップがあった部分(主に詰まった部分)もまとめていきます。 Deno Deployはimport-map機能にまだ対応していなかった。 Deno Deployで提供されていないAPIを使っているライブラリがあるとデプロイ時に???となる。 利用したライブラリの型情報が古く、 --no-check と一部 any を使ってしまった。 Deno Deployはimport-map機能にまだ対応していなかった これは先に調べておけば…。という話ではあるのですが、現時点ではDeno Deployは import-map には対応しておらず、基本的に deps.ts のパターンで実装する必要があります。 https://deno.com/deploy/docs/runtime-api#future-support ある程度、実装を進めたタイミングでデプロイしようとしてこれに気づいたため、完全に「にゃーん」となりました🐱 今更気づいた。Deno Deployやとimport_mapまだ対応されてへんやーん https://t.co/Nh2rcuXhXM — がっちゃん@えんじにゃ⛺ (@gatchan0807) 2021年12月8日 とはいえ、そこまで労力はかからないのでササッと deps.ts パターンに書き換えたのですが、再度デプロイしたところ次の問題が襲ってきました。 Deno Deployで提供されていないAPIを使っているライブラリがあるとデプロイ時に???となる ※あくまで状況証拠を並べてこれが原因っぽい。という推理のもと書いている点だけご容赦ください🙇 先程の import-map 問題を対応して再度アプリケーションをデプロイしてアクセスしてみたところ、相変わらず502を吐いてページが見れませんでした。 ログを見ると上記のようなエラーを吐いてアプリケーションの起動に失敗していました。 該当ファイルの該当行 を見にいってみたものの、エラーメッセージの reading 'deno' とは異なるデータを参照しているので違いそう…。 該当の 'deno' プロパティはファイルの中だと ${Deno.version.deno} しか使われていないけど、発生行数違うな…? と思いながら、色々調べて改めて https://deno.com/deploy/docs/runtime-api を見たところ、 It's different from Deno but aims to have similar APIs where applicable. の一文を見つけました👀 提供されている同様のAPIリストを見た感じ Deno.version の名前はない(あくまでDenoを模したランタイムなので提供されていない?)のと、上記の発生行が違う疑問は、実行時にコメントを削除されたファイルだとしたら計算が合う点から、 slack_bolt ライブラリの依存先の slack_web_api ライブラリ内の参照が問題で落ちているのだ。と自分の中で結論づけました。 今回利用している slack_bolt ライブラリは今回のアプリケーションの根幹に置いていたものだったこともあって、ある程度実装した上でこれを外して再度実装し直すのは時間がかかりすぎるし、別のライブラリでうまくいく保証は無い。と考え、 結果としてはDeno Deployを利用するのを諦める形で意思決定しました 。 利用したライブラリの型情報が古く、 --no-check と一部 any を使ってしまった 今回利用した slack_bolt には一部依存ライブラリ側での型情報の融通が出来ていない(多分一部のライブラリのバージョンアップ忘れが原因)バグがあったため、 --no-check のフラグを付けて実行し、型情報を無視するようにました( ここ 見る限りv1.17.0からは --no-check=remote が使えて外部ライブラリだけ型チェック無視になるみたいなので、Dockerfileの指定ではすでにこっそりこれを指定して使ってみてる👀) また、最終更新が10ヶ月前のライブラリだったため、現在のレスポンスには実際に返却されているデータが型情報に載っていなかったため、一度 any 化し、 as で型をつけ直すという対応を(型拡張するのをめんどくさがって)行いました。 https://github.com/gatchan0807/deno-slack-random-bot/blob/e6a655c74e25f8e765988e0f9c1f2ac1231737ad/src/app.ts#L21-L24 この辺りは次世代化が発表されたこのタイミングというのもあって、「既存の非公式ライブラリをメンテナンスするよりもSlack公式の次世代開発プラットフォームが全体に公開されるのを待つほうがコスパが良い」という思いになりPR作成は見送りましたが、本来であれば積極的にPR出して行きたいものですね👀 ざっくりとした実装の流れ 最後に、Slack Botが実際に動くようになるまでに行った対応の流れを紹介しておきます。 実際に動いた!の図 Slack Boltの公式チュートリアルを参考に、Slack Appを作成開始する slack.dev 基本的には各ステップのインポート部分の書き方を読み替えてステップを進めていく形で問題ないです🙆 Slackのワークスペースに関しては、権限があれば本番のワークスペースでやっても問題ないです。 ただ、自分はいきなり本番のワークスペースで試すのが怖いのと、管理者画面にどんな設定項目があるのかわからないと困ることがたまにあるので個人のワークスペースを遊び場として作成して、そこで実行できるかチェックしたりしています。 Codespaces環境内に環境変数を指定する 上記の公式チュートリアルの中で、環境変数にSlackのトークンを設定する作業がありますが、exportするだけではせっかく設定した環境変数がCodespacesが再起動されるたびに吹っ飛んでしまうので、提供されているCodespaces secretsの機能を使って環境変数を設定しましょう。 Codespaces の暗号化されたシークレットを管理する - GitHub Docs 下記のように設定できていればOKです🙆 そして、設定した環境変数を適用するにはCodespaceの再起動が必要なので、下記の通りVSCodeコマンドパレットを使って停止→起動を行いましょう。 Using the Visual Studio Code Command Palette in Codespaces - GitHub Docs 再起動後、 $ export | grep SLACK_BOT_TOKEN / $ export | grep SLACK_SIGNING_SECRET を叩いてきちんと環境変数に設定されていることを確認しましょう。 その後、指定のファイルを deno run してみて、問題なく起動できればOKです🙆 Events APIを使ってメンション&サブコマンドの命令を受けられるようにする 上記のチュートリアルを通して、Codespaces上で起動している環境に「リクエストを受け取る→Slackにレスポンスを返す」ことが確認できたので、 app.message メソッドを使って、メンションとサブコマンドの認識ができるようにしていきます。 細かい説明は省いてしまいますが、 app.message は第一引数に正規表現を設定することで正規表現に当てはまるメッセージを検出した場合に、第二引数の関数を呼び出される仕組みになっています。(いわばRouterやEventListnerのようなもの) そして、その関数の引数には各種リクエスト時に受け取ったtimestampやテキストの内容などと、レスポンス用の say 関数が詰まったオブジェクトが渡ってくるので、それらを利用してメンションから受け取ったデータの保存やメンションへの返事などを実装する形になります。 今回利用しているDeno用Bolt SDKの更新が10ヶ月前で止まっているためか、実際に返却されるオブジェクトと event に設定されている型情報が一致していなかったため、一度雑にany化してしまうというTypeScript的には禁忌なことをやってしまっていますが、本来は型情報を拡張して適用するようにしましょう🙏(時間があれば対応します…) import { SubCommandPattern } from "./subcommands.ts"; /** ~~ 省略 ~~ **/ // グループの入れ物の作成コマンド app.message(SubCommandPattern.create, async ({ event, say }) => { const _anyEvent = event as any; const text = _anyEvent.text as string; const user = _anyEvent.user as string; const [_botName, _subcommand, groupName] = text.split(" "); console.log("[INFO] Create: ", _anyEvent.text); await say(`<@${user}> 【 ${groupName} 】グループの作成が完了しました🎉`); }); /** ~~ 省略 ~~ **/ この対応の全貌は↓のコミットに https://github.com/gatchan0807/deno-slack-random-bot/commit/7e27039539013946be6663244ea4dd2762fe5b8d データストアを構築する ここまででSlack Botがコマンドを受け取って返信できるのを確認できたので、続いて作りたいコマンドを実現するために必要なデータの置き場所をつくります。 今回は本番では Deno Deploy Cloud Runを使う関係上、データストアを外に持つ必要があり、個人開発で使った経験があるFirebase(Firestore)を利用して行こうと思います。 Deno Deployの公式ドキュメントに似たような処理の書き方のチュートリアルがあるのですが、FirebaseのSDKバージョンがv8の頃の記法のまま(namespace形式)なので、基本的にはFirebaseの公式ドキュメントを参考にしながら、データ永続化処理を実装していきます。 firebase.google.com この辺り を参考にしながら、Firestoreの環境を構築し、Firebase Configを取得して、Slackのトークンと同じように環境変数に設定しちゃいましょう。 ※ FIREBASE_CONFIG は読み込み時に JSON.parse(Deno.env.get("FIREBASE_CONFIG")) のような形でparseされるので、JSONの記法に合わせてプロパティ名をダブルクォーテーションで囲むのと、改行の削除を行ったうえで環境変数に設定しています。 ライブラリを deps.ts に追加する export { App } from "https://deno.land/x/slack_bolt@1.0.0/mod.ts"; export { initializeApp } from "https://cdn.skypack.dev/@firebase/app@v0.7.10?dts"; export * from "https://cdn.skypack.dev/@firebase/firestore@v3.4.0?dts"; Skypackを利用する場合、 ?dts を設定しないと返ってくるライブラリの型情報がなくなってしまうので、忘れずに設定しましょう。(流石にFirebaseのSDKで型情報がないのは辛い) データ永続化の処理を実装する /** ~~ 省略 ~~ **/ console.log("[INFO] Execute add command:", _anyEvent.text); const docRef = doc(db, "groups", groupName); const docSnap = await getDoc(docRef); if (!docSnap.exists()) { console.info(`[INFO] The specified group name does not found.`); await say(`<@${user}> 【${groupName}】グループは登録リストに見つかりませんでした!`); return; } const groupRef = doc(collection(db, "groups"), groupName); const usersRef = collection(groupRef, "users"); await addDoc(usersRef, { userName: targetUserName, timestamp: timestamp, }); await say( `<@${user}> "${groupName}"グループに【${targetUserName}】を追加しました! Welcome.👏👏👏`, ); /** ~~ 省略 ~~ **/ この対応の全貌は↓のコミットに https://github.com/gatchan0807/deno-slack-random-bot/commit/8281e1a8b1cda83f3828fdc4347001c8c0d8495d ランダム処理を実装する 初期時点では、ランダム処理は雑にコピってきた arr.sort(() => Math.random() - 0.5); を使って実装していましたが、下記の記事で触れられている通り、ランダムといいつつある程度の偏りが発生してしまいます。 devblog.thebase.in そのため、記事に習ってフィッシャーイェーツのアルゴリズムを拝借して実装し直しました🙆 一通り実装できたら、デプロイする Deno Deployにデプロイするプランは残念ながら頓挫してしまったので、代わりにDockerに詰め込んでしまって、GCP上のCloud Runにデプロイするプランに切り替えました。 実際に処理を詰め込んだDockerfileは↓です https://github.com/gatchan0807/deno-slack-random-bot/blob/main/Dockerfile Cloud Runについての詳細な説明は こちらの紹介ページ に譲りますが、今回のユースケースで利用量であればデイリーの無料枠で事足りるレベルなので、Firestoreの無料枠とも相まって基本無料でSlack Botを作成できる見込みです。 - 200 万リクエスト(1 か月あたり) - 360,000 GB 秒のメモリ、180,000 vCPU 秒のコンピューティング時間 - 1 GB の北米からの下り(外向き)ネットワーク(1 か月あたり) cloud.google.com GitHubからの自動デプロイ設定はGUIからポチポチと タイトルのとおりですが、Dockerfileをリポジトリに置いた上で、Cloud Runの設定(もしくはCloud Buildの設定)でGitHubにCloud Build Appのインストールを行えばほぼ自動でGitHubへのPushをトリガーにした自動デプロイ環境を用意できます。 cloud.google.com Cloud Runの環境変数設定を忘れずに 実行環境の環境変数設定もGUIから実施できるので、設定を忘れずに。 忘れてしまうと 指定のポートでアプリケーション起動してないよ 的なエラーが発生してデプロイ失敗してしまいます🙏 cloud.google.com デプロイが完了したら、再度Slack Botの設定画面に行ってRequest URLをCloud Runのサービスに紐付いたURLに書き換えてしまいましょう🙆 Verifiedになれば、 @random-bot くんのバックエンドがCloud Runの本番環境に切り替わり、いつでもBotを利用できるようになっているはずです! これにてSlack Bot完成! まとめ もし前回のDeno基礎知識 + 環境構築編からここまでお付き合い頂いた方がいらっしゃれば、長々とお読みいただいて本当にありがとうございました!! Denoに対しての興味から調査を開始し、Slack Botの作成まで行なって記事にしてきましたが、久しぶりに知らないエコシステムをガッツリ触るということをやったので、とても楽しかったです🙆 全体的な感想としては、2年たったとはいえまだまだDenoも発展途上なんだな!という印象を受け、これから色々な進化を見せてくれるのを追うのが楽しみになりました👀 先にも上げたaleph.jsやpackupなどもこれから触りつつ、Node.jsエコシステムに比べてまだまだ参入余地が沢山あるDenoのコミュニティに機会があれば貢献していきたいなと思いました! 皆さまもぜひ、言語はJavaScript/TypeScriptで馴染みがあるけど、エコシステムは新しいが故にどことなく不思議な感覚になるDenoの世界に飛び込んでいただければと思います! 明日のアドベントカレンダーはBASE BANKのアプリケーションエンジニアの @glassmonekey さんの記事です!お楽しみに!
BASEアドベントカレンダー2021 11日目の記事です。 BASEアドベントカレンダー2021 DataStrategyチームの齋藤( @pigooosuke )です。 DataStrategyチームでは機械学習のモデルや集計結果をAPI経由で配信することが多く、社内の他チームと連携する際にも、どういうリクエストパラメータが存在し、どういうレスポンスが期待されるのかを共有しています。 弊チームでは、AWSのAPI Gatewayを利用しているので、API Gatewayの機能として提供されているドキュメントパーツを利用して共通フォーマットのAPI仕様書を管理しています。 markdown記法によるドキュメント管理に比べ、各APIのリクエストパラメーターやレスポンスモデルの視認性の改善や更新漏れが減るようになりました。 今回は、どのようにAPI仕様書を管理しているのかを紹介したいと思います。 *現在利用している Redoc のデモ画像 Redocのデモ画像 API Gateway ドキュメントパーツ API Gatewayでは各APIリソースに紐づくレスポンスモデル、クエリパラメータ、リクエストヘッダーなどに対して、ドキュメントを設定することができます。かつ、設定されたドキュメントを全て結合して、OpenAPI 3もしくはSwagger形式のjson/yamlを出力することが出来ます。 AWS デベロッパーガイド: API Gateway での API ドキュメントの表現 例えば、 /ysaito/recommend という「おすすめコンテンツを配信するAPI(GET method)」を提供していると仮定して、 このAPIでは、パラメータとして ユーザーID と 最大取得件数 を指定できるとします。 その場合、 /ysaito/recommend に対するドキュメント /ysaito/recommend に含まれるクエリパラメータ ユーザーID(user_id) に対するドキュメント /ysaito/recommend に含まれるクエリパラメータ 最大取得件数(max_size) に対するドキュメント をそれぞれ個別に管理することが出来ます。 今回のケースでは、コンソール画面上では以下のように登録されます。 合わせて、APIレスポンスに関しても、 API Gatewayのモデル管理でモデル登録&APIに紐付けしておくことで出力ファイルにレスポンスモデルを含めることが可能です。もちろん200以外のステータスコードの登録も可能です。 またこのドキュメントのリリースバージョンは、API Gateway本体のデプロイと同様にバージョン管理されているので、必要に応じて、過去のリリースバージョンにロールバックなども容易に可能になっています。 概略 全体のフローとして以下の図の通りです。 API GatewayにAPI定義をTerraformを利用して登録する ビルド用のrepositoryがAPI GatewayからOpenAPI/swagger仕様のAPI定義ファイルを取得する RedocというOpenAPI/swaggerファイルをHTMLファイルに変換するツールでビルドする S3にアップロードし、ドキュメント公開用サーバー上で公開する 処理詳細 API GatewayにAPI定義をTerraformを利用して登録する Document PartsはTerraformで管理することができるため、APIを作成する場合に合わせて登録するようにしています。 Terraform: Resource: aws_api_gateway_documentation_part ビルド用のrepositoryがAPI GatewayからOpenAPI/swagger仕様のAPI定義ファイルを取得する ビルド用のRepositoryのGitHub Actionsでは、AWS CLIにてAPI Gatewayからのドキュメント取得を以下のコマンドで取得しています。 aws apigateway get-export --parameters extensions='apigateway' --rest-api-id `<API ID>` --stage-name prod --export-type oas30 --accepts application/yaml `<出力先path>` RedocというOpenAPI/swaggerファイルをHTMLファイルに変換するツールでビルドする OpenAPI/swaggerファイルをHTML変換するツールはいくつか公開されているのですが、 視認性の観点で Redoc を採用しています。 おわりに 今回は、API仕様書の管理について紹介してみました。 ドキュメントの管理は、「昔の情報から更新されてない」「誰が管理するの?」といった長年の課題ではありますが、 API Gatewayの開発と連動して、自動でドキュメント発行できる仕組みを運用することで負荷軽減に繋がりました。 明日のアドベントカレンダーはフロントエンドエンジニアのがっちゃん( @gatchan0807 )の記事です。お楽しみに!
BASE BANKでエンジニアをしている @budougumi0617 です。 この記事はBASE Advent Calendar 2021 10日目の記事…ではなく、New Relic Advent Calendar 2021 10日目の記事です。 qiita.com TL;DR ソフトウェア開発チームのパフォーマンスを示す 4 つの指標がある https://www.devops-research.com/quickcheck.html デプロイの頻度 変更のリードタイム 変更障害率 サービス復元時間 New Relic Oneを使って「デプロイの頻度」を計測してみた Event APIとNRQLでデプロイ回数をNew Relic Oneのダッシュボードに図示できる https://docs.newrelic.com/docs/data-apis/ingest-apis/introduction-event-api ダッシュボード 自分たちチームのパフォーマンスはどれくらいなのか? 2009年に「10 deploys per day」というタイトルと共にDevOpsというキーワードが産まれて 1 から10年以上が経ちました。 10+ Deploys Per Day: Dev and Ops Cooperation at Flickr from John Allspaw www.slideshare.net 市場へサービスのリリースを繰り返し、より短時間で多くのフィードバックループを回すことがソフトウェア開発チームの生産性向上の鍵なようです。 この事実を具体的な指標にしたのが DevOps Research and Assessment(DORA)チーム の研究から導き出されたソフトウェア開発チームのパフォーマンスを示す 4 つの指標です( Google Cloudブログより引用 )。 デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 変更のリードタイム - commit から本番環境稼働までの所要時間 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間 LeanとDevOpsの科学[Accelerate] や クイックチェックツール では具体的なパフォーマンスレベルも示されています。 performance ( 表はさきほど同様 Google Cloudブログより引用 ) しかし、「 ではあなたのチームはどのレベルなの? 」と聞かれると「多分先週は毎日デプロイしてたはず…」なんて曖昧な回答しかできない状態でした。すでに他社ではこの質問に答えるためのアプローチ方法がいろいろ取られています。 生産性を可視化したい! / SUZURI's four keys https://speakerdeck.com/udzura/suzuris-four-keys-ce85acb4-bb98-4b6c-b6d8-0e62b5253872 普段「推測するな、計測せよ」「可観測性!!Observability!!O11y!!」なんて言っているのにこれではいけない…ということでチームの各指標をちゃんと計測しようと思いました。本記事ではまずは手始めにデプロイの頻度を計測した方法の紹介です。 デプロイ頻度を集計したい デプロイ頻度を計測するには様々なツールがあると思いますが、我々はNew Relicを使ってデプロイ頻度を集計することにしました。 New Relicを使ってデプロイ頻度を集計する理由は次のとおりです。 New Relicで集計している他のメトリクス情報と組み合わせて別の情報を引き出せる可能性がある 新たなツールを導入するよりも日々使うツールは少ないほうがよい 同様に確認が必要なダッシュボードは少ないほうがよい NRQLの便利さとグラフの作りやすさ サービスをデプロイしたという事実を集計する New Relicには Deployment Marker という他のパフォーマンス情報とアプリケーションのデプロイを関連付ける機能があります。 docs.newrelic.com qiita.com Deployment Marker を利用して Deployment イベントを記録すると、APMのグラフにデプロイタイミングを示す線がプロットされます。 New Relic One上でデプロイタイミングを記録しておくことでデプロイがメトリクスにどのような影響を及ぼしたのか解析できます。 Deploymentが一緒に描画されたメトリクス 最初はこの機能を使って記録した Deployment イベントをNRQLで集計すればよいと考えていました。 しかし、当時(そして2021年12月現在も、) Deployment はNRQLによって集計できるイベントはありませんでした。 デプロイ頻度を可視化するには集計してグラフ化する必要があります。そこで我々はデプロイのたびに独自のカスタムイベントを発行することにしました。 Event APIを利用した独自イベントの発行 New RelicのNRQLで集計できる type (NRQLの FROM に指定できるデータ)は Log や Transaction など多岐に渡ります。 そしてNRQLはユーザーが独自に定義した type のデータを集計することも可能です。独自定義のイベント( type )を作れるのがEvent APIです。 docs.newrelic.com Event APIを実行するために必要なもの Event APIを利用するために必要な情報は次の2点です。 License Key Account ID New Relic Oneを利用している場合、Event APIはLicense Keyを確認するだけで実行できます( 以前はInsert Keyが必要だったはずなのですが、2021年12月現在は非推奨 )。 docs.newrelic.com Licence Key自体は次のURLをクリックすると遷移できる「API Keys」の中から確認することができます。 同ページを見れば自分たち組織のアカウントIDも確認できるはずです。 one.newrelic.com Event APIの実行方法 Event APIの基本的な利用方法は先ほど記述したLicense Keyを利用して任意のJSONを curl コマンドで POST するだけです。 docs.newrelic.com 細かい仕様はありますが、任意のキー/バリューを含めることで記録する情報を自由に付与することができます。 今回は次のようなJSONフォーマットで独自イベントを発行することにします。 [ { " eventType ":" Deploy ", " deploy_user ":" ${DEPLOY_USER} ", " service ":" ${SEPLOY_SERVICE_NAME} " } ] たとえひとつのイベントだけでも配列で定義しないとうまくいきません。 時刻情報はAPI実行時の timestamp が自動的に含まれるので自前の定義は不要です。 「誰がデプロイしたのか?」も情報として含めていますが、これを集計すると謎の力学が発生してしまうのであくまで参考値として記録しておくだけにしておきます。 CircleCI上でEvent APIを実行する 上記のJSON定義をNew Relicのエンドポイントに送信すればイベントの記録が始まります。 我々のチームはCircleCIを使ってサービスをデプロイしているので、CircleCIのデプロイ用ワークフローの最後のステップにEvent APIを実行するようにします。 各サービスの .circleci/config.yml の中に次のjobを定義しました。 record-deployment : executor : name : aws-cli/default # curlコマンドが使えれば何でもよい steps : - setenv_prd # 環境変数のセットアップ - run : name : Send Deploy event to New Relic command : | curl -i \ -X POST 'https://insights-collector.newrelic.com/v1/accounts/${NEW_RELIC_ACOUNT_ID}/events' \ -H "X-Insert-Key:${NEW_RELIC_LICENSE_KEY}" \ -H "Content-Type: application/json" \ -d \ "[ { \" eventType \" : \" Deploy \" , \" deploy_user \" : \" ${CIRCLE_USERNAME} \" , \" service \" : \" ${CIRCLE_PROJECT_REPONAME} \" } \ ]" NEW_RELIC_ACOUNT_ID と NEW_RELIC_LICENSE_KEY という環境変数を設定する必要があります。 CIRCLE_USERNAME 、 CIRCLE_PROJECT_REPONAME はCircleCI実行時に自動的に設定される値です。 circleci.com あとはこのjobをデプロイフローの最後に設定しておきます。 デプロイフローの実行完了(デプロイ成功)のタイミングで Deploy というイベントが記録されます。 test-build-deploy : jobs : - test : filters : branches : only : main - build-push : requires : - test # .... - deploy-prd : requires : - approve-deploy-prd - record-deployment : requires : - deploy-prd Event APIを利用した独自イベントの集計 データが取得できるようになったならば後は集計してダッシュボードに掲載します。 New Relic Oneを開いたらQuery Builderに移動して Deploy イベントを集計していきます。 良いグラフができたら右下の「Add to Dashboard」ボタンを使ってダッシュボードにグラフを追加していきます。 参考までに我々が作成したグラフとNRQLのクエリを紹介します。 ダッシュボード 月間デイリーデプロイ数 SELECT count (*) FROM Deploy TIMESERIES 1 day FACET service SINCE 30 days ago 月間デイリーデプロイ数 まずは過去30日の日毎のデプロイ数です。サービス別にカウントしています。 NRQLは独自イベントに付与している独自属性を使って FACET を書くこともできます。 今週のデプロイ総数(先週比較) SELECT count (*) FROM Deploy SINCE this week COMPARE WITH 1 week ago ウィークリーデプロイ数 次に先週比較の数値です。 COMPARE WITH を使うと比較対象との比率まで簡単に示してくれます。 先週と比べて今週のデプロイ頻度は低いのか?気づかないうちにチームのパフォーマンスがブレてていないか?を確認したくて作りました。 週間デプロイ数 最後は直近4週間のデプロイ総数です。 SELECT count (*) FROM Deploy FACET weekOf(timestamp) UNTIL monday SINCE 5 week ago 週別デプロイ数 こちらもコンスタントにデプロイできているか?を確認するために作りました。 終わりに 本記事では「LeanとDevOpsの科学」などで示されているソフトウェア開発チームのパフォーマンスを示す4 つの指標のうち、デプロイ頻度をNew Relicを使って確認する方法を紹介しました。いくつか実際に利用している集計クエリも紹介しました。「こんな見方もできるのは?」「私たちのチームはこういう点をみている」とコメントある方は教えていただけると幸いです。 ソフトウェア開発チームのパフォーマンスの4つの指標ではデプロイ頻度の他に3つのキーが定義されています(再度 Google Cloudブログより引用 )。 デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 変更のリードタイム - commit から本番環境稼働までの所要時間 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間 デプロイ頻度以外については現状まだ計測できていません。 その他のキーについても可視化した際は本開発者ブログで共有させていただきます。 また、New Relic Oneでは最近サービスレベル目標(SLO)を設定、サービスレベル指標(SLI)、Error budgetを管理できるService Level Management機能もPublic betaになりました。 来年は4キーメトリクスの他に、SLIについても可視化していければなと思います。 docs.newrelic.com そして BASE Advent Calendar 2021 10日目の記事も書いたのでよかったら読んでみてください。 devblog.thebase.in 参考 LeanとDevOpsの科学[Accelerate] DevOpsの起源とOpsを巡る対立 エリート DevOps チームであることを Four Keys プロジェクトで確認する | Google Cloud Blog State of DevOps Report 2021を日本語で解説 https://puppet.com/resources/report/2021-state-of-devops-report https://twitter.com/t_wada/status/1420652243330605060 生産性を可視化したい! / SUZURI's four keys NewsPicksにCTOとして入社して1年でDX Criteriaを大幅改善した話 https://ubiteku.oinker.me/2015/07/01/devops%E3%81%AE%E8%B5%B7%E6%BA%90%E3%81%A8ops%E3%82%92%E5%B7%A1%E3%82%8B%E5%AF%BE%E7%AB%8B/ ↩