DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

プロダクトマネジメント、時代の変化

DIGGLEのベルマです。DIGGLEではプロダクトマネジメントチームとして6月に組織化しました。立ち上げにあたってどのようにプロダクトマネジメントしていくべきかを考えるため、過去の文献や資料をリサーチしました。
今回のブログでは、リサーチを自分の考察も含めてまとめたものを下記にアウトプットすることにしました。

 

DISCLAIMER: 本記事は元が英語であるため、一部ニュアンスが正しく翻訳できていない場合があります

デジタル化の始まり、SaaSが台頭する前の時代

この頃の特徴
  • エンジニアのリソースが少なく、地理的にアクセスしにくい

  • コンピュータサイエンスに特化した教育・研究を行っている国が少ない

                             areppim AG, 2008, https://stats.areppim.com/stats/stats_unitopnbrxengx09.htm

  • 初期開発コストが高い(特に、サーバーの調達コストが高い)

                            2009年10月14日.https://www.rbbtoday.com/article/2009/10/14/62995.html 

  • 大規模な手動テストの実施やバージョン管理システムの未発達のため、開発の所要期間が長い

  • サーバーは物理的で、クラウドは一部の国でしか利用できず、技術スタックも柔軟性に欠ける

  • 製品はパッケージ商品であり、すべての機能が一緒に開発され、ユーザーからのフィードバックは最後にあり、カスタマーサポートが製品の成功の中心でない

  • マーケットはグローバルが対象であり、参入企業は少ない

 

            Scott Brinker, https://chiefmartec.com/2020/04/marketing-technology-landscape-2020-martech-5000/ 

 

この時代のプロダクトマネジメント

 

 

 

以前は2種類のコミュニケーションが行われていました。

マーケット・コミュニケーション

  1. 競合分析、社内SWOT
  2. 市場の展望、それに基づく
    1. インパクトマップ
    2. ステークホルダーマップ
  3. ユーザーのニーズ
    1. ユーザー要件
    2. ユーザージャーニーマップ
  4. 営業コミュニケーションをリードするためのプレイブックを作成する
    1. 営業パイプラインをベースに、お客様にサービスを提供するための契約書を作成する
    2. 営業はCRM、オンライン広告、パイプライン管理ツールを使用する
  5. マーケターは以下の情報を収集する
    1. 上記の情報をもとに、お客様へのセールスピッチを行い
    2. カスタマーサポートは、マーケティングや製品コミュニケーションに問題がある場合、以下の方法で修正します
  6. お客様のオンボーディング
    1. 機能のリリースをサポートする
    2. 既存顧客からのバグ、問題、要求の収集
  7. このコミュニケーションに関わるすべての人はプロフィットセンターである
  8. このコミュニケーションに参加する全員が、お客様(マーケット)と向き合います

プロダクトコミュニケーション

  1. CTOが率いるプロダクトチーム
    1. エンジニアが多いチーム構成
    2. 要件は技術的なもの
    3. CTOとシニアエンジニアは、営業やマーケティングから要件を聞き、迅速かつ効率的に生産することを目的としています
    4. 製品管理ツールはない
  2. DEVチームはCTOが率いる
    1. チームは製品ごとにチームまたは部門に分かれています
    2. プロダクトをまたいで人が動くが、タイムラグが長い
    3. DEVの要件は、シニアエンジニアからチームに伝えられ、実行される
    4. DEVは課題管理ツール、Github、Wikiを使って日々制作している
  3. QAチームはシニアエンジニアが中心
    1. CTOが設定したリリースの基準に合致しているかどうかを確認する
    2. 常にスコープクリープに対抗する
    3. チェック不足でリリースの妨げになったり、品質を低下させたりしている
    4. エクセルシートで行われる
  4. このコミュニケーションに参加する全員がコストセンターである
  5. このコミュニケーションに参加する全員が、顧客と向き合えていないことが多い

デジタル時代、SaaSの時代

特徴
  • エンジニアリングリソースが豊富で、地理的な問題が業務に影響しない

   


                              

                                                       ARWU (Academic Ranking of World Universities), 08/30/2019

  • 開発の初期費用が安い、サーバー、採用、マーケティングがすべてクラウド上にある

  • CI/CDによる自動テストやデプロイの自動化、バージョン管理システムの進化により、現在では開発プロセスは高度にコラボレーションできるようになりました

  • 開発プラットフォームはすべてデジタルで、プラグアンドプレイスタイル。多くの競合他社からオンラインのクラウドベースのサーバーを複数選択でき、技術スタックは市場の需要に追従する

  • プロダクトスタイルはアプリ重視で、各アプリはエンドユーザーの1つの「ジョブ」を解決することに焦点を当てる。カスタマージャーニーを通して常にお客様からのフィードバックを求める。カスタマーサクセスは、プロダクトを通じたカスタマージャーニーの実現を目指して行動する

  • 競争は広範囲に及び、常に進化しローカルである。アプリは簡単に複製でき、ニッチ市場を獲得するために展開できる

 

画像元

この時代のプロダクトマネジメント

                   

 

  1. 社内全員が同じコミュニケーションチャネルで、同じ方向に向かって話している

  2. PdMは製品チームの一員ではありませんが、すべての部門のフィードバックと市場洞察を促進するために

    1. アイデア

    2. ストーリーからサブタスクに

    3. エピックは、ストーリーを組み合わせて大きな画にしたもの

    4. エピックを各機能に落とし込む

  3. PdMは、そのタスクを管理するための独自のツールを持っている

    1. PdMはマーケティング-セールス-CSのマーケティング・サイクルと連動しています

    2. PdMはDEV-QAと連携し、生産サイクルを回す

  4. どのメンバーも収益の創出を担う

    1. DEVとQAは、ユーザーデータとユーザー行動からアイデアを生み出し、ユーザーエクスペリエンスに付加価値を与えることもできます。

    2. DEVは常にシステムパフォーマンスを管理し、それが顧客維持、ひいてはMRR/ARRに影響を与える。

  5. どのメンバーも様々な情報を元にマーケットと対峙する

    1. DEVとQAは、マーケット側からの情報をすべて把握し、プロダクトの議論に積極的に参加します

* 実際の部門役割の定義は、各社で異なります。例:呼び方や分解されることもある。PdM/PMM、商品企画、事業開発など。

 

過去と現在の対比



                          これまで

                       現在

email

チャットツール

paid feedback

instant free feedback

高価なマーケティング

低価格なマーケティング

競合比較は難しい

競合比較サイトが無料でみられる

ストーリー 引き継ぎ時間が長く、面倒くさい

idea >> story >> epic >> feature

フィードバックは1つの大きなカテゴリー

既存顧客の声と潜在顧客の声を分離

全てのアプリケーションを内製化

3rd partyによる積極的な自動化



その結果、このような変化が生まれました。

 

  1. より迅速で効果的なコミュニケーション*を実現します。

  2. マーケットの変化に機敏に対応するプロダクトマネジメント

    1. CS/Salesからのインプットが重要に

      1. 既存顧客とのベータテスト

    1. マーケットベンチマーク

    2. マーケットに合わせた機能

  3. 最小限のアプリをプロダクトスイートとして開発することができる

    1. 市場シェアと普及率の向上

    1. サブスクリプションモデルに移行できる価格が目立つ

 

* コミュニケーションに深みがない、つまり返信は早いけれども、積極的なコミュニケーションに欠けることもあります。

 

この記事で使用している用語について
  • SaaS - サービスとしてのソフトウェア
  • テスト/QA - 顧客にリリースする前にソフトウェアをテストすること
  • リポジトリ - 一連のソースコードと、それらのファイルに対する変更の履歴
  • Git - リポジトリの分散バージョンコントロールのためのソフトウェア
  • バージョン管理システム - ソフトウェアチームがソースコードの変更を長期的に管理するためのソフトウェアツール。
  • デプロイ - ソフトウェアシステムを一般顧客が使用できるようにするためのすべての活動。
  • 技術スタック - ソフトウェアを構築し実行するために使用する技術の組み合わせ。
  • ユーザージャーニーマップ - ソフトウェアを利用するユーザの流れを視覚的に表現した図
  • CTO - 最高技術責任者
  • 課題管理ツール - チケットトラッキングとアジャイルプロジェクトマネジメントを可能にする課題追跡製品
  • スコープクリープ - プロジェクト開始後に、プロジェクトのスコープが変更されることを指します。
  • コストセンター - 利益を直接的に増やさないが、それでも組織のコストがかかる組織内の部門や機能
  • ジョブ - 人々がなぜそのような行動をとるのかを理解するための、ジョブ理論というフレームワークにおける用語
  • ハイパーローカル - 明確に定義されたコミュニティとその住民に向けられた主要な焦点
  • Ideas-Story-Epic-Feature - エンドユーザーの視点で書かれた、ソフトウェアシステムの機能に関する非形式的な自然言語による記述
  • PdM - プロダクトマネージメントまたはプロダクトマネージャー
  • MRR/ARR - 月間定期収入/年間定期収入

 

DIGGLEの考える開発効率の上げ方

こんにちは。DIGGLEエンジニアリングマネージャーのzakkyです。 先月は埼玉県民の日(11/14)で学校が休みだったので、有給を取って家族写真を撮りに行ってきました。娘も息子も可愛かったです。

ということで、開発生産性 Advent Calendar 2022の23日目の記事となります。

前回のブログ投稿から約半年立ちましたが、その間に役職が変わりまして、エンジニアリングマネージャーになりました。 今回は、マネージャーになって最初の記事という事で、技術的な話からは少し離れて、DIGGLEの開発効率向上施策について書きたいと思います。

そもそも開発効率ってどうやったら向上するんだろう

開発効率向上と一言で言っても色々な視点や施策があるかと思います。

ざっと思いつくだけでも以下があるかと思います。

  • CI/CDの導入・高速化
  • IaC化
  • よく使うコマンドをaliasとして展開
  • 自動生成系のツールの利用(無ければ作成)

上記はDIGGLE内で実施している一例なのですが、今回は先ほどお伝えした通り、上記のような技術的な話からは少し離れ、開発チームとして行っている開発効率向上施策にフォーカスして書いていきたいと思います。

差し込み作業の発生頻度を減らす

皆さんも経験があるかと思いますが、今の作業に差し込みで新しい作業が発生するとガクッと作業効率が落ちることがあると思います。 他の業種に比べてエンジニアは特にここの部分が顕著で、いかに作業に集中してもらえるかが肝になると考えています。

弊社では、そんなエンジニアの効率を下げる差し込み対応を、スクラムマスターに集約することで、他のメンバーが作業に集中できる状況を作るようにしています。

スクラムマスターには負荷になってしまいますが、トータルとしてチームの生産性は向上することとなります。 また、余談となりますが、スクラムマスターを輪番にすることで、各メンバーの自律化を促す施策も行っています。 こちらについて、詳しくは下記の記事で語っておりますので参照ください。

www.wantedly.com

自律したフルスタックエンジニアで構成する

弊社エンジニアはバックエンドやフロントエンドの垣根なく全員フルスタックに開発を行っています。 また、全員が仕様の確認&調整、CSなど他チームとの連携なども含めて自律して行うことができるので、開発要件1件につきエンジニア1人を割り振れば事足りることが多いです。(開発工数次第で複数人で開発することもあります)

これにより、作業工程間での連携で引継ぎ工数が掛かってしまったり、バックエンド(またはフロントエンド)側のタスクだけが積みあがって滞留してしまうといった事を防げます。

また、自律したエンジニアで構成されたチームとすることで、一般的にメンバーが増えれば増えるほど増加していくマネジメントコストの増加を抑えることができます。 そしてマネージャーはマネジメントに追われることなく、開発へリソースを回すことができるようになります。

実際、今回マネージャーとなりましたが、マネジメントに関する工数はほぼ掛かっておりません。優秀なエンジニアに囲まれて嬉しい限りです。

チューター制度を採用することにより素早い立ち上がりをサポートする

せっかく新しいエンジニアの方に入社いただいても、実際に開発できるようになるまでに時間が掛かってしまっては意味がありません。 ということで、弊社では新規参画エンジニアの方にはチューターを付けて、立ち上げをサポートする仕組みがあります。

チューターには、業務内容の説明や、各作業方法の伝達など細かなサポートをお願いしています。 どのようにサポートするかについては、それぞれのエンジニアの裁量にお任せしているので、チューターとなるエンジニアごとに多少の違いはありますが、だいたい1カ月ほどで上記の自律したフルスタックエンジニアになれるようにサポートをお願いしています。

もちろん個人差もありますし、1カ月で「はいおしまい」ということではなく、チーム全体として新規参画エンジニアを受け入れる土壌を作る。ということに重きを置いています。

開発効率向上施策が打てる土壌を持つ

日々の開発の中で、「開発者として実施したいけど、目先の作業に押し流されてできないこと」があると、それが積み重なって負債となり、開発効率を低下させる要因となります。

例えば、以下のようなものがあるかと思います。

  • ライブラリやミドルウェアを最新バージョンに上げたい
  • CIが遅いから高速化したい
  • リファクタリングがしたい
    • unusedなコードがあるから消したい
    • 冗長なロジックを見つけたから共通化したい など

弊社では、上記のような開発効率向上施策に全体工数の何%を使用して良いかを事前に経営層と相談し、工数を確保しておく仕組みを取っています。(およそ10~20%程度)

これによりプランニング時に、「次のSprintではRailsのバージョンを上げたい」であったり、「CIを高速化しないと作業効率が悪すぎるから今やってしまおう」といった事が行いやすくなっています。

※無論、予定していた機能リリースが遅延したりしては元も子もないので、杓子定規で測るのではなく、都度都度柔軟に対応しています。

メンテナンスする資料をむやみに増やさない

README、新規参画者向けのキャッチアップ資料、各種議事録など、ドキュメント化は率先して行っています。

ただ、エンジニアの本分はコードを書くことですので、資料を作成する際には必ず以下を確認するようにしています。

  • なぜ必要なのか
  • 用途は何なのか
  • 似たような資料、もしくは代替しうる資料は他に存在しないか
  • メンテナンスし続けることができるか(※議事録や調査資料など揮発性の高い資料は除く)

特に「メンテナンスし続けることができるか」が重要だと思っています。 作ってはみたけど、メンテナンスせずに放置してしまうと、新規参画者へ間違った情報を与えてしまったり、逆に悪影響を及ぼすことすらありますし、メンテナンスするトリガーが明確か、工数を確保できるかも考えていくと、そこまで重要ではない資料は作成しない方向へ進むことが多いです。

そして、作ることに決めた資料は、チームとしてメンテナンスする意識を持ち、メンテナンスコストを認識・許容するようにしています。

環境面(PCや開発環境)でストレスを与えない

ディスプレイが小さかったり、PCのスペックが低かったりすると開発効率は当然落ちます。 また、開発環境(OSやIDEなど)を制限されることで慣れない環境を余儀なくされるケースもあります。 私も前職や前々職では貸与PCのスペックが低かったり、ディスプレイが貸与されなかったりと不便を感じたこともありました。

弊社では、ハイスペックPCの貸与はもちろん、OSやIDEの縛りもありませんので自分の好きなように開発環境を作ってパフォーマンスを出して貰うことが可能です。 実際、私はwindows + WSLで開発していますし、MacやUbuntuを使って開発しているメンバーもいます。

さいごに

上記のような開発効率向上施策は、その場その場で我々が考えて作り上げたものになります。そのため、「これが最適解である」ということではなく、あくまで現時点(2022年末時点)での形となります。 そして、今後も色々と改善が行われて変わっていくものだと思います。

また良いものが見つかったらご紹介させていただきますので続報お待ちください。

We're hiring!

DIGGLEでは、現在の仕組みを改善してより良いものを一緒に作っていけるメンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Ruby on Rails で Google API を利用するための承認フローを実装する

こんにちは!DIGGLE エンジニアの miyakawa です。
この記事は Ruby on Rails Advent Calendar 2022 の22日目の記事です。

qiita.com

はじめに

弊社のプロダクト DIGGLE には、Google ドライブ や Google スプレッドシートからデータをインポートする機能があり、この機能を実現するために Google Drive API を利用しています。

Google Drive API 等の Google API を利用して各種 Google サービス上のユーザーのデータを取得・操作する場合、OAuthによってアクセストークンを発行・利用する必要があります。

↓の画像のやつです

今回は Ruby on Rails のアプリにこちらの OAuth 承認フローを組み込む方法についてお話しします。

※この記事では触れないこと

  • OAuth について
  • Google API 個別の利用方法
  • Google OAuth の検証・公開の手順

ステップ1: OAuth 同意画面の設定

まず Google Cloud の Web コンソールから OAuth 同意画面の設定を行います。
OAuth 同意画面とはユーザーが OAuth の承認をする際に表示される画面のことを指します。

下記のURLからアプリ情報等を入力していきます。

https://console.cloud.google.com/apis/credentials/consent

ひとまずは開発環境向けなので適当に項目を埋めていって問題ありません。
(本番環境向けに公開する際には正しく情報を入れる必要があります。利用する OAuth スコープによっては Google によるアプリの検証が必要になります。)

ステップ2: OAuth クライアント ID の作成

OAuth 同意画面の設定が終わったら下記 URL から OAuth クライアント ID を作成します。

https://console.cloud.google.com/apis/credentials

アプリケーションの種類は「ウェブ アプリケーション」を選択し、承認済みのリダイレクト URI に次のステップで作成する OAuth 用 Controller の callback アクションの URI を入力します。

OAuth クライアント ID を作成するとクライアント ID とクライアントシークレットが発行されるので控えておきます。

ステップ3: Rails アプリに OAuth 承認機能を実装する

※ 既に Rails アプリの土台があることを前提にしています。

gem のインストール

今回は Google 公式の OAuth クライアントライブラリである googleauth gem を使用します。

github.com

余談

googleauth gem では signet という汎用的な OAuth 2.0 クライアントライブラリを利用しています。
もし Google 以外のサービス向けに OAuth のクライアント実装が必要な場合はこちらの利用を検討してみてください。

OAuth アクセストークンの Model 作成

今回の実装では、取得した OAuth アクセストークン(アクセストークン、リフレッシュトークン)を RDB(PostgreSQL)に保存します。

マイグレーションと Model は下記のようになります。

class CreateGoogleOauthTokens < ActiveRecord::Migration[7.0]
  def change
    create_table :google_oauth_tokens do |t|
      t.references :user, null: false, foreign_key: true
      t.string :access_token, null: false
      t.string :refresh_token, null: false
      t.timestamps
    end
  end
end
class GoogleOauthToken < ApplicationRecord
  belongs_to :user

  validates :access_token, presence: true
  validates :refresh_token, presence: true
end

※ 上記コードは説明用のため省略していますが、実際は access_tokenrefresh_token は暗号化して保存するように実装するべきです。

Token Store の作成

Token Store とは googleauth gem における OAuth アクセストークンの保存場所です。先の通り、今回は RDB です。

googleauth gem では以下二つの Token Store が実装されていますが、RDB 向けの実装は用意されていません。

  • Google::Auth::Stores::FileTokenStore
  • Google::Auth::Stores::RedisTokenStore

そのため、自アプリの RDB 向けの Token Store を実装します。

require 'googleauth/token_store'

class DBTokenStore < Google::Auth::TokenStore
  def load id
    token = GoogleOauthToken.find_by(user_id: id)
    return nil if token.nil?
    JSON.dump({
      access_token: token.access_token,
      refresh_token: token.refresh_token
    })
  end

  def store id, token
    token_hash = JSON.parse(token)
    token = GoogleOauthToken.find_or_initialize_by(user_id: id)
    token.update!(
      access_token: token_hash['access_token'],
      refresh_token: token_hash['refresh_token'],
    )
  end

  def delete id
    token = GoogleOauthToken.find_by(user_id: id)
    token&.destroy!
  end
end

内容はシンプルで、loadstoredelete の各メソッドに DB への保存、更新、削除の処理を定義したものとなっています。

OAuth 用 Controller の作成

最後に Controller の作成です。

class GoogleOauthController < ApplicationController
  before_action :set_authorizer

  def authorize
    credentials = authorizer.get_credentials(current_user.id)
    if credentials.nil?
      redirect_to authorizer.get_authorization_url(request: request)
    else
      redirect_back fallback_location: root_path
    end
  end

  def callback
    cred, = authorizer.handle_auth_callback(current_user.id, request)
    redirect_to root_path
  rescue Signet::AuthorizationError
    render :authorization_error
  end

  private

  def set_authorizer
    # クライアント ID とクライアントシークレットを環境変数で渡しておく
    client_id = Google::Auth::ClientId.new(ENV.fetch('GOOGLE_OAUTH_CLIENT_ID'), ENV.fetch('GOOGLE_OAUTH_CLIENT_SECRET'))
    # Google API の OAuth スコープのリスト。例として Google Drive の読み込み専用スコープを指定。
    scopes = ['https://www.googleapis.com/auth/drive.readonly']
    token_store = DBTokenStore.new
    # callback アクションの URL
    callback_url = 'http://localhost:3000/google_oauth/callback'

    @authorizer = Google::Auth::WebUserAuthorizer.new(client_id, scopes, token_store, callback_url)
  end
end

routes.rb の設定もお忘れなく。

...
resources :google_oauth, only: [] do
  collection do
    get :authorize
    get :callback
  end
end
...

※ callback アクションの URL が、ステップ2の 承認済みのリダイレクト URI に設定したものと異なる場合はリダイレクト時にエラーとなるため再度設定を確認しておいてください。

これで、authorize アクションのURLにアクセスすることで Google OAuth の認可プロセスを行うことが可能になります。

コードの説明

まず authorizer についてですが、これによってアクセストークンの取得・保存、認証先リダイレクトURLの作成などを行うことができます。
そして、今回利用している Google::Auth::WebUserAuthorizer は authorizer の Rack アプリケーション向けアダプタです。

@authorizer = Google::Auth::WebUserAuthorizer.new(client_id, scopes, token_store, callback_url)


authorize アクションは、Google OAuth の認証/認可の画面へリダイレクトする役割をもちます。

callback アクションは、ユーザーが Google OAuth の認可を終えた後のリダイレクト先であり、リクエストに付与された認証情報を Token Store に保存します。

(参考)Google API の OAuth スコープ

Google API のサービス個別に OAuth のスコープが用意されており、下記ページでスコープの一覧を確認できます。

OAuth 2.0 Scopes for Google APIs  |  Authorization  |  Google Developers

ステップ4: アクセストークンを利用して Google API を使う

アクセストークンを取得できたら、それを利用して Google API を使うことができます。

以下は Google 製の Google ドライブ用クライアントライブラリ google-apis-drive_v3 を使う例です。

require 'google/apis/drive_v3'

drive = Google::Apis::DriveV3::DriveService.new
# 先述の @authorizer を利用して認証情報をセット
drive.authorization = @authorizer.get_credentials(current_user.id)

# ドライブトップ上のファイルを取得
files = drive.list_files()
files.items.each do |file|
  puts file.title
end

おわりに

今回は Ruby on Rails のアプリに Google の OAuth 承認フローを実装する方法についてお話ししました。

他にも関連するトピックとして、Drive API の小話や Google OAuth 利用のためのアプリの検証の方法などあるので、また機会があればお話しできればと思います。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

React Testing Libraryのrerenderを薄くラップすることで使い勝手を向上させた話

こんにちは。DIGGLE エンジニアの伊藤です。
ソフトウェアテスト Advent Calendar 2022の 20 日目の記事です。 Advent Calendar 初参加になります。

普段はフロントエンドを中心に業務を行なっています。
今回は DIGGLE で実施されているフロントエンドの単体テスト、 特にコンポーネントのテストで使っているフレームワークの使い勝手を向上させたお話をさせていただきます。

はじめに

DIGGLE ではフロントエンドで React を使っており、 テストフレームワークとして下記を利用しています。

Jest でモデルなどの React を介さない部分のテストを行い、React Testing Library を使って React コンポーネントのスナップショットテストを実施しています。React コンポーネントのテストで React Testing Library を使うか Enzyme を使うかといった違いはあるかもしれませんが、一般的によくある構成だと思います。

上記2つのフレームワークを用いることで DIGGLE でテストしたい項目を概ね満たすことができたのですが、 state を外部からコンポーネントに渡すテストを書こうとした際に上手く書けない問題がありました。

stateを外部からコンポーネントへ渡せない問題

例えば、React Testing Library を使って下記の様な Dropdown のコンポーネントをテストした際、

import { render } from '@testing-library/react';

---

it('Dropdownをクリックした時のテスト', async () => {
  const [value, setValue] = React.useState();
  render(<TestDropdown value={value} onClick={(e, v) => setValue(v)} option={...}/>);

// onClickした時のvalueの変化をテストしたい
});

としてuserEventでクリックイベントを発火させても value に値が反映されませんでした。
React Testing Library での関数は React の関数ではないので、React のフックを使おうとすると怒られるということで、 考えてみれば当然の話でした。
(Storybook では useState を使えたため、React Testing Library でも使えるのでは?と勘違いしたことが原因ですが、 おかげで Storybook と React Testing Library のスタンスの違いを感じることができました。)

上記の問題解消のためには、Jest + React Testing Library 構成の中で state を外部から props で渡すコンポーネントに関して「useState をモックしたい」という話になるのですが、

結論から先に書いてしまうと React Testing Library の rerender を活用することで問題を解決しました。

解決にあたって

解決にあたって候補になった方法は下記の2点でした

  1. rerender*1 を使って値を更新して擬似的に useState を表現する
  2. @testing-library/react-hooks というライブラリを使う

どちらの方法も甲乙つけがたいのですが、最終的には「1. rerender を使って値を更新して擬似的に useState を表現する」を採用することにしました。

「2. @testing-library/react-hooks というライブラリを使う」を採用しなかった理由は、

  • @testing-library/react-hooks はカスタムフックのテストに対する解決方法という色が強そう
    • DIGGLE ではカスタムフックの利用が活発でなく、導入の旨味が薄い
      • 展開のコストも考えると、急いで入れる必要はなさそう
  • React v18.0 対応を見てみると、 @testing-library/react-hooks の ver がサッと上がらない様子だった
    • 急いで入れて旨味を感じられず、管理の手間が増えるということになりかねない
  • 「1. rerender を使って値を更新して擬似的に useState を表現する」を実施する場合は薄くラップする必要があるとわかっていたため、「2. @testing-library/react-hooks というライブラリを使う」は必要になったタイミングでラッパーの中で切り替えれば良い

と考えたためです。

ただ、「1. rerender を使って値を更新して擬似的に useState を表現する」の方法にも問題はあり、 なにもせずに利用しようとするとほぼ同じ記述を 2 回することになってしまいます。

「rerender を使って値を更新して擬似的に useState を表現する」の問題点

愚直にテストを記述すると下記の形になります。( it 関数の中身だけを記載しています)

let rerender = _.noop;

const handleClick = jest.fn((value) => {
  rerender(<TestDropdown value={value} onClick={(e, v) => handleClick(v)} />);
});

const { rerender as TLRerender } = render(
  <TestDropdown value={value} onClick={(e, v) => handleClick(v)} />
);

rerender = TLRerender;

// onClickした時のvalueの変化をテスト

rerender を活用することにより、onClick が発火した際にvを与えながら再描画をhandleClick経由で行える様になります。

「useState をモックしたい」は上記で満たすことができたものの、TestDropdown をほぼ同じ形で2回書かないといけないことがわかると思います。

今回の様なシンプルな props ならまだ耐えられますが、 大量に props を渡す必要のあるコンポーネントに対するテストや onClick、onBlur など複数イベントに rerender の関数をセットしたい際に記述が煩雑になりそうなことがわかります。

問題点の解消 - rerender を薄くラップする

タイトルの通り、rerender を薄くラップすることで同じ記述を 2 回しなければならない問題を解決しました。

用意したラッパーは下記になります。

import { render as TLRender } from '@testing-library/react';
import _ from 'lodash';

export default class RerenderHelper {
  #rerender = _.noop;

  #render = _.noop;

  #firstRenderValue = null;

  constructor(render = _.noop, firstRenderValue = null) {
    this.#render = render; // render関数
    this.#firstRenderValue = firstRenderValue; // 初期描画時に利用されるvalueの値
  }

  // 再描画
  rerenderComponent = jest.fn((value) => {
    const r = this.#render(value, (v = value) => this.rerenderComponent(v));
    this.#rerender(r);
  });

  // 初期描画
  firstRender() {
    const { rerender } = TLRender(
      this.#render(this.#firstRenderValue, (v = this.#firstRenderValue) =>
        this.rerenderComponent(v)
      )
    );
    this.#rerender = rerender;
  }
}

やっていることはシンプルで、 同じ記述をしている部分を #render として持たせ、初期描画と再描画処理でそれぞれ呼び出しています。

また、#render を (value, setValue) => {} という形の関数で渡せる様にすることで、 使い勝手を useState に寄せています。

使い方は下記になります。

import Rerender from 'helpers/rerender';

---

it('Dropdownをクリックした時のテスト', async () => {
  const r = new Rerender((value, setValue) => {
    return (
      <TestDropdown value={value} onChange={(v) => setValue(v)} option={...}/>
    );
  });
  r.firstRender();

  // onClickした時のvalueの変化をテスト
});

上記ラッパーでできることは限られているものの、カスタムフックを使っていない環境であれば問題ないと思います。

まとめ

今回は DIGGLE のフロントエンドテストの環境を、フレームワークの一機能をラップすることで向上させた事例について紹介しました。

フロントエンドに限らずテストではさまざまなモックがされるかと思います。
モックする箇所や基準は様々だと思いますが、必要な部分に絞って慎重にモックを適用していく一例として今回の事例が参考になれば幸いです。

モックの濫用を防ぐ意味でも、カスタムフックの活用が活発になるまでのつなぎとして役割を果たしてくれると思います。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

*1:rerender自体の説明は公式にあるためここでは割愛させていただきます https://testing-library.com/docs/react-testing-library/api/#rerender

RailsのJOIN方法の違いでソートしたときに意図した結果を取得できてなかった話

こんにちは。DIGGLEのエンジニアのhondaです。
Ruby on Rails Advent Calendar 2022の12日目の記事です。
Advent Calendar初参加です。

はじめに

Railsで開発している方にはN+1問題というのはおなじみだと思います。(説明は割愛します)
そのためincludesやeager_load, joinsなどのN+1問題を起こさないためのメソッドに関する理解は必須だと思います。
自分もそれぞれの意味を把握して使い分ける程度にはわかっているつもりだったのですが、その理解が甘かったのでソートした時に意図した結果を取得できていませんでした。 今回はそのときの失敗談と解決した方法をお話します。

状況設定

今回は説明のために簡略化した↓のテーブル定義を使います。 データベースはpostgresqlを使用しています。

ER

要件はitemsをユーザーにリスト形式で表示します。
このときユーザーは任意のitem_tag_fieldsを指定してitem_tagsのvalueでソートした結果を取得できます。
なお、item_tagsのtag_field_idとitem_idに対してunique_indexを貼っています。 ユーザーに見せるインターフェースは以下のようなイメージになります。

item list

ID name tag_field_A(ソート可能) tag_field_B(ソート可能)
1 item_name_A value_A_1 value_B_3
2 item_name_B value_A_2 value_B_2
3 item_name_C value_A_3 value_B_1

以下では↑のようなレコードがDBに保存されていることを想定します。

どのような失敗をしたのか

例えばtag_field_id = 1でソートしたいとしたとき以下のような処理を書いたとします。 簡略化のためにtag_field_idのパラメータ化やsanitizeなどの処理は省略します。

items = Item.eager_load(item_tags: :item_tag_field).order(Arel.sql(<<~SQL))
  CASE item_tags.item_tag_field_id
  WHEN 1 THEN item_tags.value
  ELSE NULL
  END
SQL

> items.first.name # => item_name_A
> items.last.name # => item_name_C
> items.count # => 3

この結果だけ見ると上手くいっているように見えなくもないですが、実は問題をはらんでいます。
eager_loadにしているのはorder by句に指定しているitem_tagsテーブルとのjoinを確実に行うことを意図して書いていました。
変更前はincludesで書かれていたのもあってeager_loadにしておけば変なことにはならないだろうという漠然とした考えもありました。

# 変更前のコード
Item.includes(item_tags: :item_tag_field).order(:id)

一旦は問題がないと思ってしまった...

実はこのコードを書いた段階では自分はitems.countの部分は正しく動かないだろうと思っていました。
理由は発行されるSQLにありました。 実際にSQLを見てもらうとわかりやすいと思います。
items.to_sqlでSQLの文字列を出力して整形してカラム名をわかりやすく変更し、実行した結果が↓のものになります。

SELECT "items"."id" AS item_id,
       "items"."name" AS item_name,
       "item_tags"."id" AS item_tag_id,
       "item_tags"."item_tag_field_id" AS item_tag_field_id,
       "item_tags"."value" AS value,
       "item_tag_fields"."name" AS item_tag_field_name
FROM "items"
LEFT OUTER JOIN "item_tags" ON "item_tags"."item_id" = "items"."id"
LEFT OUTER JOIN "item_tag_fields" ON "item_tag_fields"."id" = "item_tags"."item_tag_field_id"
ORDER BY CASE item_tags.item_tag_field_id
             WHEN 1 THEN item_tags.value
             ELSE NULL
         END
;
 item_id |  item_name  | item_tag_id | item_tag_field_id |   value   | item_tag_field_name 
---------+-------------+-------------+-------------------+-----------+---------------------
       1 | item_name_A |           1 |                 1 | value_A_1 | tag_field_A
       2 | item_name_B |           3 |                 1 | value_A_2 | tag_field_A
       3 | item_name_C |           5 |                 1 | value_A_3 | tag_field_A
       2 | item_name_B |           4 |                 2 | value_B_2 | tag_field_B
       3 | item_name_C |           6 |                 2 | value_B_1 | tag_field_B
       1 | item_name_A |           2 |                 2 | value_B_3 | tag_field_B
(6 rows)

ご覧の通り6件の結果が出力されています。 items : item_tags は 1:Nの関係にあるのでjoinするときに取得件数に影響がでます。 なので、自分としてはitems.count == 6になるのではないかと思っていました。 しかし、実際には↓のような処理がおこなわれました。

# countした時に別のsqlが発行される
> items.count
SELECT COUNT(DISTINCT "items"."id") FROM "items" LEFT OUTER JOIN "item_tags" ON "item_tags"."item_id" = "items"."id" LEFT OUTER JOIN "item_tag_fields" ON "item_tag_fields"."id" = "item_tags"."item_tag_field_id"
=> 3

joinしたテーブルが1:N関係にあるときはCOUNT(DISTINCT items.id)しているので重複レコード分はカウントしないようになっています。
これを見て「ああ、Railsがよしなにやってくれるんだな」と思い手元でテストした感じも動いてそうだったのでこの書き方で問題ないと思っていました。

正しく値を取得できないときがある

しかし、このコードはページング処理を行うときに問題が発生します。
kaminariを使って2件づつ取得する処理をおこなうとします

# 1ページ目は問題なさそうに見える
page1 = items.page(1).per(2)
page1.pluck(:id) # => [1, 2]
page1.current_page # =>1
page1.total_pages # => 2
page1.total_count # => 3

# 2ページ目にレコードが2件ある!?
page2 = items.page(2).per(2)
page2.pluck(:id) # => [2, 3]
 
# 3ページ目!?
page3 = items.page(3).per(2)
page3.pluck(:id) # => [1, 3]

ご覧のとおりページネーション(ORDER BYとLIMT OFFSET)を使うと意図していないレコードが取れてしまいます。 自分でテストした段階では入れていたデータがよくなかったのか、この問題に気づけませんでした...

解決策

このバグは開発終盤の方に見つかったのもあってか、お恥ずかしい話、自分はいい修正方法が思いついていませんでした。
しかし、弊社のハイパーエンジニアことokazakiさんにこのことを相談したところ、 order byするときは必要なレコードに絞ってjoinすれば問題ないという至極真っ当なアドバイスをもらいました。(なぜ気づかなかったのか...)

また、関連テーブル先のデータはeager_loadで取得するのではなくpreloadで取得するようにしてORDER BYを含むSQLを発行する際に余計なテーブルとjoinしないようにしました。
例の如く簡略化のためにtag_field_idのパラメータ化やsanitizeなどの処理は省略します。

join_clause = <<~SQL
  LEFT OUTER JOIN item_tags ON items.id = item_tags.item_id
  AND item_tags.item_tag_field_id = 1
SQL

items = Item.joins(join_clause).preload(item_tags: :item_tag_field).order('item_tags.value')

items.to_sql 
=begin
SELECT "items".*
FROM "items"
LEFT OUTER JOIN item_tags ON items.id = item_tags.item_id
AND item_tags.item_tag_field_id = 1
ORDER BY item_tags.value
=end

# ページネーションも問題なし

page1 = items.page(1).per(2)
page1.pluck(:id) # => [1, 2]

page2 = items.page(2).per(2)
page2.pluck(:id) # => [3]

page3 = items.page(3).per(2)
page3.pluck(:id) # => []

これで上手くソートもページネーションもできました。よかったよかった。
普段ActiveRecord::Relationを使っていると意外と頑張ってやってくれるので大変重宝しているのですが、 1:N関係にあるレコードでページネーションするときは注意が必要というお話でした。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

ふりかえり(レトロスペクティブ)を活用してチームの自己組織化を促す

UnsplashRavi Palweが撮影した写真

フロントエンドエンジニア兼デザイナーの大澤です。
今回は主にふりかえりによってDIGGLEの開発チームがいかに自己組織化を達成したか、その肝である「ふりかえり」をどのように実施したかについてお伝えします。

なぜ自己組織化を求めるのか

自己組織化したチームには以下の利点があります。

  • 少ないルール
  • 少ない管理コスト
  • 問題が放置されづらい(誰かが拾って対応できる人に渡される)
  • 互いがサポートする中で生まれる安心感
  • 良い意味で交代可能(穴は誰かが埋めてくれるという信頼がある) -> 有給取りやすい
  • そもそもスクラムに必要

DIGGLEではスクラムを採用したので自己組織化は必須でした。
ただ仮にスクラムを採用しなかったとしても上記の利点は魅力的です。
個人的には、ルールを守るために注意を割かれるのが嫌い(そんなことより仕事自体に集中したい)なので、少ないルールで動けるチームであると嬉しいと考えていました。

実際に得たこと

ふりかえりを起点とした改善行動によってDIGGLEの開発チームは、おおよそ自己組織的になったと思います。
上記のメリットに加えて、以下のことも達成できました。

  1. 安定したベロシティ&バーンアップの上昇
  2. 全員がスクラムマスターになれる(輪番制)
  3. 上記をフルリモート環境で運用できている

これらは継続的なふりかえりによる問題発見と改善によってもたらされたと考えています。

過去の失敗経験と学び

狭い経験内の話ではありますが、DIGGLEほど自己組織化に成功したチームはありませんでした。
過去に自己組織化に失敗した事例を思い返すと、以下の理由が思い当たります。

  1. スクラムやアジャイルのプラクティスを綺麗に回すことだけに気を取られる(形式重視)
  2. 既にあるチームや組織の慣習から離れられない
  3. プラクティスの適用を急いでしまう

特に3番目は、1と2が合わさると発生しやすくなります。
素早くプラクティスを適用して、悪い/非生産的(と思っている)文化からの脱却を図ろうとします。しかし、急いで適用するためプラクティスは浸透しません。メンバーは怒られないために仕方なくやっている状況となり、主体性に欠けた受け身のチームが出来上がります。

自分なりの回答: 自己組織化は時間がかかる

自己組織化はチームが主体的に改善を繰り返していく中で達成できます。改善のほとんどは小さなものですが、この積み重ねが大きな変化をもたらします。チームが自分達のものだと強く認識でき、結果として自己組織化が促されます。

自己組織化は一朝一夕で達成できるものではなく、継続的に取り組んだからこそ得られる状態です。
そして、チームの改善行動を促す仕組みこそが「ふりかえり(レトロスペクティブ)」なのです。

シンプルに始める

DIGGLEでは以下のことを重視しました。

  1. 必要なスクラムプラクティスから少しずつ取り入れる
  2. ふりかえりでの問題発見と改善行動の繰り返しの中で「少しずつ」改善する

スクラムを取り入れた当時、必要最小限と考えたプラクティスは以下のものです。

  • スプリント(タイムボックスの役割)
  • プランニング(タスク見積もり)
  • ふりかえり(問題発見と改善)

初期はデイリースクラムもレビューもリファインメントもありませんでした。
今でこそこれらのイベントも実施していますが、加わったのはふりかえりの中で問題が見えてきた後になります。
問題が顕在化してから適用することで「なぜやるのか」が明確になります。

問題を率直に共有することの重要性

「部屋の中の象」という言葉があります。
誰の目にも明らかな問題が存在するのに、誰もそのことに触れない状況のことを指します。もし部屋に象がいたら、それこそが「本物の問題」です。ふりかえりで共有される問題が「本物の問題」でなければ、ふりかえりの効果が極めて限定的になります。

以下のような状況では率直な共有がされません。

  • 問題報告者が面倒ごとを押し付けられる
  • 問題を共有したけど無視される
  • 切断処理(「君の問題だよね?」)
  • 心理的安全性を欠く

上記のような状況を引き起こさないためにファシリテーター(スクラムの場合はスクラムマスター)が率先して状況を切り開く必要があります。
と言っても自分から問題に突っ込むというより、メンバーに率直さと積極性を促すことが重要です。呼び水となる問いかけをしましょう。ふりかえりの場ではチーム全員が話して良いという雰囲気を作り出すことで、本物の問題を引っ張り出すきっかけになります。

ふりかえり手法について

DIGGLEではKPTを用いていますが、問題を共有して改善行動に繋げることができれば何でも良いと思っています。
大事なことは以下の通りです。

  • 定期的に開催する
  • 対象を限定する(ex: 直前のスプリント)
  • 思いつく限り書き出す(書き出すハードルは低めにする)
  • 具体的な改善行動につなげる(次のスプリントで実行できると望ましい)

具体的な改善行動まで繋げられると理想です。とはいえ、最も重要なことはチームメンバーが感じている問題を共有することにあります。問題が共有されていれば、後はどうとでもなります。

反対に最も怖いのが「問題が見えない」ことです。チームメンバーが萎縮したり、無力感を感じていると必要な共有がされません。ふりかえりには心理的安全性が必要です。ファシリテーターはこれを維持する努力をしましょう。

「ふりかえりではどんな問題も話し合える」と感じてもらえるのが理想です。

結果の出ないふりかえりになる覚悟を持つ

ここまで色々書いてきましたが、いつだってうまくいくわけではありません。
例えば、以下のような状況になって十分な成果を出せないときもあります。

  • 議論が長引いて、残りの問題について話し合う時間が不足する。結果、適当に終わらせてしまう。
  • 疲労などの影響で注意が逸れて必要な議論が不足する(面倒になってしまう)
  • 自信を失って話し合いを打ち切ってしまう

毎度完璧で効果的なふりかえりができるなんてことはありません。うまくいかない日は必ずあります。
そんな日があったとしても、改善プロセスを繰り返すことこそが重要なのです。
「次こそはもっと良くしよう」「小さくても良いから次の改善に繋げよう」という意識を持ち続ける限り、チームを成長させることができます。

諦めずに続けていきましょう。

まとめ

DIGGLEではふりかえりを改善プロセスの中心に据えることで自己組織化を達成できました。
この結果としてスクラムの運用とチームの独立性を保てていると思います。

もちろん今後がどうなるかは分かりません。
しかし、ふりかえりを中心にした改善プロセスが回り続けることは変わらないと思っています。

We're hiring!

DIGGLEでは主体性を発揮してチームで最高のプロダクトを作りたいエンジニアやデザイナーを募集しております!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

herp.careers

検証用スクリプトの実行環境として AWS Cloud9 を採用した話

こんにちは。DIGGLE エンジニアの miyakawa です。
普段はバックエンドとインフラを中心に開発しています。

今回は、前回の記事で触れたレポート機能の数値検証の「実行環境」についてお話したいと思います。

レポート機能の数値検証の内容については前回の記事をご覧ください。 diggle.engineer

数値検証の実行環境

まず結論として、現在は数値検証の実行環境として AWS の Cloud9 というサービスを利用しています。

Cloud9 はクラウドベースの統合開発環境(IDE)を提供してくれるサービスです。

docs.aws.amazon.com

IDE なので開発環境としての用途が一般的かと思いますが、弊チームでは検証用スクリプトの共有実行環境として活用しています。

次に Cloud9 を採用した経緯についてお話しします。

実行環境選定の経緯

ローカル環境での課題

当初、数値検証の実行は開発メンバーのローカル環境 (PC)上で行っていました。

数値検証には本番環境のデータベースのデータを複製したデータベース(以降、数値検証用 DB)を用いますが、セキュリティ上ネットワーク外部からの直接アクセスはできないようになっています。

そこで、ローカル環境から数値検証用 DB へアクセスするために、EC2 インスタンスによる踏み台サーバを用意し AWS Systems Manager の Session Manager を使ってセキュアにログイン、数値検証用DBまでポートフォワード接続できる環境を構築していました。

しかしながら、このローカル環境で数値検証を行う方法では以下の問題がありました。

  • 数値検証の実行完了まで数時間かかり、その間ローカル環境のリポジトリのブランチは固定かつ PC リソースも消費されるため開発作業に支障が発生する
  • 40〜60分ほどで Session Manager の接続が切れて数値検証の実行が途中で止まってしまう*1

二つ目の問題については、数値検証スクリプトに途中の段階からリトライするオプションがあるため、実行が止まるたび都度リトライすることで対処していましたがかなりの手間となっていました。

ローカル環境以外の方法の模索

ローカル環境を使う運用は難しいと判断し、それ以外の方法を検討し始めました。

ここで一度数値検証の実行環境の要件をまとめます。

  • Rake タスクを実行できる or Docker コンテナを実行できる
  • リポジトリの任意のブランチで実行することができる
  • AWS のプライベートネットワークにある数値検証用 DB にアクセスできる

これを踏まえて検討した結果、以下のような案が出ました。

  • EC2 インスタンスにローカル環境と同様の環境(git + Docker)を用意して数値検証を実行する
  • 上記に加えてジョブ管理ツール(Rundeck など)を導入して Web ベースで作業できるようにする
  • Github Actions のワークフローとして実行する*2
    • 数値検証用 DB アクセスのために EC2 インスタンスで self-hosted runners を構築する

いずれも AWS 上に EC2 インスタンスを作成するという点は同じで費用面でのコストに差はありませんでした。
後は工数との兼ね合いかと考えていたところで、AWS のソリューションアーキテクトの方と相談の機会があり本件についても相談してみました。

すると
「Cloud9 が適しているのはないか」
とのご助言が...!

それまで Cloud9 は使ったことがあってもあくまで開発環境用というイメージしかなかったため目から鱗でした。

早速 Cloud9 による環境を検証したところ、次に述べる Cloud9 の利点から、先に検討していた各案よりも優れていると判断し最終的に採用へ至りました。

Cloud9 の利点

環境作成が容易

Cloud9 の EC2 環境では EC2インスタンスをベースにクラウド IDE 環境を構築できます。
AWS コンソールからネットワーク(VPC)や接続方法等の最低限の情報を設定するだけで、すぐに開発を始められる EC2 インスタンスが作成されます。
(Cloud9 の環境作成方法の詳細については こちら を参照ください)

IDE の機能を除いたとしても、一から EC2 インスタンスをセットアップする場合と比べて多くの利点があり、セットアップの工数を減らすことができます。
例えばですが、

  • (接続方法として選択した場合)Session Manager の設定が自動で行われる
  • git、docker といった開発に必要なツール群がプレインストールされている

などが挙げられます。

IAM ユーザによる共有

Cloud9 の環境は作成時点では作成者のみが利用できる状態です。 そこに IAM ユーザを招待する形で他の開発者に環境を共有することができます。

仮に素の EC2 インスタンスへの Session Manager によるアクセスを IAM ユーザごとに可否設定する場合は各 IAM ポリシーを設定する必要が出てきますが、Cloud9 であればクラウド IDE 上で操作するだけなので非常に管理が楽になります。

また CloudTrail から証跡(いつ、どの IAM ユーザが Cloud9 を利用したか)も確認できるため、万が一の時のためにも安心です。

費用削減の機能がある

開発・検証用の環境は常に利用するわけではないため、夜間停止などの仕組みを入れたくなると思います。

EC2 インスタンスであれば AWS Systems Manager Automation を利用するなど一工夫が必要になります。

一方で Cloud9 にはセッションがなくなった後に一定時間が経過後 EC2 インスタンスを自動で停止する機能があります(停止するまでの時間は選択可能です)。
これにより、Cloud9 を使用する時のみ EC2 インスタンスが起動している状態となるので、費用の無駄を防ぐことができます。

Cloud9 環境セットアップ時の注意点

Docker Compose はインストールされていない

前節で docker がプリインストールされていると言いましたが Docker Compose はインストールされていません。

Docker Compose が必要な場合は、Docker の公式ドキュメント を参考にインストールしましょう。

ディスクサイズの拡張

Cloud9 のデフォルトのディスクサイズ*3は 10GB でその内大半はシステムで利用されており、環境作成時点でユーザが利用可能な領域は 2GB 程度です。
これでは多少大きめの Docker イメージをいくつかプルするだけであっという間にストレージが枯渇してしまいます。

そのため、環境を作成した時点でディスクサイズを拡張しておくことをおすすめします。

docs.aws.amazon.com

まとめ

今回は数値検証の実行環境として Cloud9 を採用するまでの経緯について紹介しました。

Cloud9 は他にもいろいろと活用できそうに思っているので、今後も新しいユースケースが出てきたら紹介していければと思います。

We're hiring!

DIGGLEでは最高のプロダクトを一緒に作ってくれるエンジニアを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

*1:主にポートフォワードの接続が切れるという状況。調査したところ Session Manager のセッションタイムアウト(https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-preferences-timeout.html)が原因ではないことは分かりましたが、根本の原因は分かっていません

*2:弊チームでは CI として Github Actions を利用しています

*3:Cloud9 の環境は EC2 インスタンスで構築されているので、ディスクサイズ = EBSのボリュームサイズを示します