社内向け管理ツールにGoogleログインを導入した話

はじめに

こんにちは!エンジニアインターンをしている湯村です。
今回、社内向け管理ツールにGoogleログインを導入したので、その話をしたいと思います。

タスクの背景

現在crispyでは、SlackやKibelaなどのツールごとにメンバーのアカウントを作成しています。
社内向け管理ツールも同様に、管理ツール用に作成したアカウントのメールアドレスとパスワードでログインしていました。

現状はそれでも問題ないのですが、今後どんどんメンバーが増えてくると、アカウントの作成・削除をそれぞれのツールで行っていると手続きが煩雑になってしまいます。
そこで、今後はメンバーのアカウントをGoogle Workspaceで一元管理し、作業を簡略化しようと考えています。

その一環として、今回は社内向け管理ツールにGoogleアカウントでログインできるように実装を行いました。

OAuth2.0

Googleログインに限らず、FacebookやTwitterなど、ソーシャルアカウントでのログインにはOAuth(オーオース)という仕組みが使われていることが多いです。
現在のバージョンはOAuth2.0です。

ソーシャルログインでは、クライアントアプリケーション(今回の場合、管理ツール)がAPIを叩いてリソースサーバー(今回の場合、Google)にアクセスし、取得したユーザーのデータを使ってクライアントアプリケーションにログインしています。

この際、APIさえ叩けば誰でもユーザーのデータが取得できる、という状態だと、悪意のあるアプリケーションがユーザーの個人情報を取得してしまう恐れがあるので、そのような事態を回避するために、アクセストークンが使われています。
Googleログインをする際、「続行するにあたり、Googleはあなたの名前、メールアドレス、言語設定、プロフィール写真を(アプリケーション名)と共有します。」という画面が出るのを見たことがあると思います。

ここでアクセスを許可すると、アプリケーションにアクセストークンが付与され、そのアクセストークンを持った状態でAPIを叩くとGoogleアカウントのユーザーデータが参照できるという仕組みになっています。

この、「アクセストークンの要求とそれに対する応答」を標準化したものがOAuth2.0です。

実装した内容

それではいよいよ実装した内容について説明していきます。
crispyの社内向け管理ツールではRailsを使用しているので、今回はomniauth-google-oauth2というgemを使用し、基本的にこちらのREADMEに従ってGCPの設定とアプリケーションのコードの追加を行いました。 github.com

GCPの設定

アプリケーションからGoogleログインのAPIを叩くことができるようにするために、GCPの設定を行います。
「APIとサービス」から「OAuth同意画面」と「認証情報」の二項目を設定します。

OAuth同意画面

アプリケーションがOAuthを使用するために必要な情報を登録します。
アプリケーション名や連絡先メールアドレスなどの必須項目を入力してください。

認証情報

認証情報を作成し、クライアントIDとクライアントシークレットを発行します。
ここで発行したIDとシークレットは、後ほどAPIが叩けるように実装をする際に必要となります。

アプリケーション側の実装

元々deviseを使用してログイン機能を実装していたので、そこに追記する形で実装していきました。

設定ファイルを記述

deviseのOmniauthableモジュールを使うと簡単に書くことができるのですが、今回は既に別のモデルでOmniauthableモジュールを使用していたため使用することができませんでした。
代わりにconfig/initializers/omniauth.rbというファイルを新しく作成し、そこに設定を記述しました。

Rails.application.config.middleware.use(OmniAuth::Builder) do
  provider(
    :google_oauth2,
    Rails.application.secrets.dig(:google, :client_id),
    Rails.application.secrets.dig(:google, :client_secret),
  )
end

client_idclient_secretには、先ほどGCPで発行されたものを設定します。

ルーティングを追加

既にログイン/ログアウトのルーティングは定義済みだったため、omniauthのコールバックのルーティングのみ追加しました。

get 'auth/google_oauth2/callback', to: 'sessions#google_oauth'

コントローラとモデルにメソッドを追加

こちらもログイン/ログアウトのメソッドは存在するため、oauthのためのメソッドのみ追加します。

# コントローラ
def google_oauth
  access_token = request.env['omniauth.auth']
  @user = User.from_omniauth(access_token)

  if @user&.persisted?
    sign_in_and_redirect(@user, event: :authentication)
  else
    session['devise.google_data'] = access_token.except('extra') # NOTE: Removing extra as it can overflow some session stores
    redirect_to new_session_path, alert: @user.errors.full_messages.join("\n")
  end
end
# モデル
class << self
  def from_omniauth(access_token)
    data = access_token.info
    User.find_by!(email: data['email'])
  end
end

あとはviewを整えて、実装完了です!

まとめ

Googleログインはgemを使うと簡単に実装できて便利なので、ぜひ導入を検討してみてください。

crispyでは、インターン生でも社員と変わらず幅広いタスクを任せてもらえます!
社内向け管理ツールの機能開発・改善だけではなく、アプリで使用するAPIの実装や、インフラ関連のタスクを行うこともあります。
開発未経験でも丁寧に教えていただけるので、安心して開発に取り組めます。
エンジニアとして成長するにはとてもいい環境だと思います!
Wantedlyで正社員・インターン共に募集しているので、覗いてみてください!

RSpecもスタブも知らなかった私が、RSpec実装時に外部リクエスト部分をスタブ化した話

はじめに

こんにちは。crispyでエンジニアインターンをしている雨宮佳音です。crispyが運営するHEIMには、商品の価格情報を取得するため、外部サイトにアクセスを行う処理があります。RSpecもスタブも知らなかった私が、RSpec実装において外部リクエスト処理をスタブ化したので、そこで学んだことをまとめていきます。

背景

HEIMの商品価格周りのコード改修後に、テストでエラーが発生しました。原因を調査してみると、外部サイトの影響でテストが失敗しているようでした。

crispyに入るまでは小規模な個人開発の経験しかなかったため、当時の私はRSpecやスタブどころか、テストで何をするのかもよくわかっていませんでした。そこで、RSpecの記事やHEIMの既存のコードからテストについて勉強し、外部サイトの状況に依存しないテストに変更するため、スタブ化を行いました。

スタブ化とは

テストでエラーが発生したとき、初めはなぜそれがエラーになっているのかがわかりませんでした。前に同じコードでテストを走らせたときには成功したのに、どうして失敗してしまうことがあるのか疑問に思いました。社員の方に質問したところ、該当のテストコードがスタブ化されていないことにより、テストが失敗することがあると教えていただきました。そこで初めて聞いた「スタブ」という言葉について、様々な記事を読んで理解を深めました。

スタブとは、テスト内で使用される「あらかじめ中身が決められたオブジェクト」のことです。そしてスタブ化とは、テスト実行時に、他のメソッド・外部サイトに飛んでオブジェクトを取ってくるのではなく、事前にオブジェクトの中身を宣言することです。これを行う理由は大きく3つあります。

機能実装前にテストを実装するため

大規模なシステム開発を行ったとき、一部の完成済みプログラムの動作検証を行うためです。完成済みプログラムが、未完成プログラムのオブジェクトを必要とする場合を想定します。スタブ化を行わなければ、未完成プログラムを仕上げてからでないと、完成済みプログラムのテストを実行できません。ここでスタブを用いれば、オブジェクトを代用してテストを実行できます。

テストを別メソッドの動作に依存させないため

テストを他のプログラムの不確かさに依存させないためです。例えば、テスト対象(calculateメソッド)とは別のメソッド(numberメソッド)からオブジェクトを取得して、処理を行うプログラムを想定します。

def number
  return 5
end

def calculate(number)
  result = number + 10
end

numberメソッドのコードが適切でない場合、calculateメソッドのテストに影響を与えてしまいます。テストをnumberメソッドの結果に依存させないようスタブ化することで、calculateメソッドだけの確かさを検証できます。

テストを外部サイトの状況に依存させないため

テストを外部サイトの状況に依存させないためです。外部サイトからデータを取得して、処理を行うプログラムを想定します。例えば、このプログラムのテスト実行時に、外部サイトがサーバー落ちしていた場合、プログラム自体に問題がなくてもテストが成功しません。外部サイトから取得するデータをあらかじめ宣言しておけば、対象のプログラム自体を検証できます。 また、外部サイトにアクセスすると発生する、通信の時間を短縮することもできます。

スタブとモック

テストについて勉強していくうちに、「モック」という言葉も出てきました。似たような働きをするため、これらの違いを理解するのにはかなり時間を費やしました。スタブとモックは、どちらもテストを円滑に進めるのに使う道具ですが、役割が異なります。

スタブは、テスト対象に対して渡す、あらかじめ中身が決められたオブジェクトのことです。スタブ化を行うことで、ある処理がそのオブジェクトを受け取ったとき、期待する結果を返すかどうかを確認できます。

一方モックは、テスト対象からデータを受け取り、それを検証するオブジェクトのことです。モック化を行うことで、ある処理が期待する引数・呼び出し回数で呼ばれているかを確認できます。

今回は、テスト対象が商品の価格情報を受け取ったとき、期待する結果を返すかどうかを確認したかったため、モックではなくスタブを使うことにしました。

実際にスタブ化してみる

スタブについて理解したところで、先ほどのコードを例に、スタブ化をしてみます。

def calculate(number)
  result = number + 10
end
it 'calculate correctly' do
  allow(number).to receive(5)
  expect(result).to eq (15)
end

calculateメソッドが正しく実行されるかどうかのテストです。calculateメソッド内ではnumberメソッドの戻り値が使用されています。ここでは、numberメソッドの戻り値の不確かさに依存しないようにするため、「numberメソッドの戻り値は5だよ」と事前に宣言をしています。

Typhoeus使用時のスタブ化

次に、実際のHTTPリクエストを例に、スタブ化を行います。ここではHEIMで使用しているTyphoeusを使い、商品情報を取得するプログラムのテストを書いています。TyphoeusはRSpec独自のスタブを使う必要がなく、簡単にスタブ化ができます。

def get_product_name(url)
  product_information = Typhoeus.get(url)
  # ここにproduct_informationのデータから商品名(product_name)を取得する処理を書く
  return product_name
end

product_informationのデータから商品名(product_name)を取得する処理は、ここでは割愛します。 続いてテストコードです。

it 'get_product_name correctly' do
  response = Typhoeus::Response.new
  Typhoeus.stub('www.example.com').and_return(response)
  expect(get_product_name).to eq ('#URLに該当する商品名')
end

テストにおいて

Typhoeus.get('www.example.com')

が実行されたときには、responseを返すことを事前に宣言し、スタブ化しています。こうすることで、テスト実行時にTyphoeus.getがうまくいかなくても、それ以下の、商品名を取得するコードの確かさを検証することができます。

まとめ

HEIMのテスト実装では、Typhoeusを使った方法でスタブ化を行い、失敗していたテストが成功するようになりました。新たなスキルを身につけてcrispyに貢献できたことで、インターン生としての成長を実感しています。ここで学んだことを活かせるよう、現在別のテスト実装のタスクも任せていただいています!

crispyでは、HEIM、3rdmallの開発を一緒に盛り上げてくれるエンジニアを募集しています。インターン生でもHEIM・3rdmallを作り上げる重要なタスクを任せていただけるので、たくさんのスキルを身につけて、成長できます!興味がある方は、ぜひWantedlyのページから応募してください!

駆動表(外部表)を考慮したMySQLクエリチューニング

はじめに

こんにちは、crispyでエンジニアインターンをしている壷谷太樹です。

弊社では、Amazonのレビュー調査や価格比較ができるサービス、「サードモール」を運営しています。この度、サードモールの社内向け分析用ダッシュボードの作成を担当しました。

一度ダッシュボードを完成させましたが、分析する内容によっては多くの集計クエリが必要になり、ページ表示が遅くなるパフォーマンスが著しく低いクエリが出てしまいました。

この記事では、スロークエリの原因調査・解決を通して、普段のパフォーマンス改善の流れをお伝えできればと思います!

パフォーマンスが低かったクエリについて

ダッシュボードの実装にあたり、ユーザが分析したAmazonの商品を集計するクエリを作成しました。 クエリは正常に動作したものの、1クエリに平均51.5秒かかっていました。

SELECT product_variants.*, ...
FROM product_variants
    INNER JOIN analysis_counts ON ...
    INNER JOIN products ON ...
    INNER JOIN product_prices ON ...
    INNER JOIN analysis_results ON ...
WHERE product_prices.ec_site_id = 1 
ORDER BY analysis_counts.analysis_count DESC 
LIMIT 50;

参考までに、各テーブルの概要は以下のようになります。また現象のイメージが付きやすいよう、実際のテーブル構造から一部変更しています。

テーブル名 概要
products 商品情報のマスターテーブル
product_variants 商品を(カラー・サイズなどの)バリエーションに分類したテーブル
product_prices 商品価格に関するテーブル
analysis_counts ユーザがAmazonレビュー調査を行った回数に関するテーブル
analysis_results ユーザがAmazonレビュー調査を行った結果に関するテーブル

なぜ遅くなっていたのか?

クエリを調べてみる

作成したクエリでは、SQLの基本と言える構文のみを使っていたため調査は難航しました。

クエリを調べたところ、

  • ORDER BYをなくすと200ms程度で動作する
  • JOINするテーブルが1~2つであれば通常の速度で動作する(JOINするテーブルによって速度は変わります)

ということがわかりました。

このことから、ORDER BYJOINを併用していることでパフォーマンスの低下が起きていると推測し、これらをキーワードに検索していくと原因に繋がる情報を見つけることができました。

ORDER BYするテーブルが駆動表になっていない

調べていくと、ORDER BYJOINを併用する場合、ORDER BYするカラムのテーブルが駆動表になっていないとパフォーマンスが落ちるということがわかりました。駆動表は外部表ともいい、JOINする順番が最初の軸となる表(テーブル)になります。

ORDER BYするカラムのテーブルが駆動表でないことでクエリのパフォーマンス低下に繋がるのは、MySQLの結合アルゴリズムが関係しています。

MySQLのJOINは、以下のようなループでテーブルを結合していくNested Loop Join(NLJ)アルゴリズムが使われています。

for row in t1 {
  for row in t2 {
    for row in t3 {
      ...
    }
  }
}

NLJ アルゴリズムでは、外側のループから内側のループに一度に 1 つずつ行を渡すため、一般に内側のループで処理されるテーブルを何回も読み取ります。

そのため、何回も読み取られる内側のループ(内部表のテーブル)でORDER BYでソートすることで、速度の低下に繋がるようです。

駆動表になっていないのか確認する

実際にORDER BYするテーブルが駆動表になっていないのか確認してみます。以下はクエリに対するEXPLAINの結果で、一番上にあるテーブルが駆動表になります。

id select_type table ...
1 SIMPLE analysis_results
1 SIMPLE products
1 SIMPLE product_variants
1 SIMPLE analysis_counts ※駆動表にしたいテーブル
1 SIMPLE product_prices

今回のクエリでは、

ORDER BY analysis_counts.analysis_count DESC

のようにソートしており、ORDER BYするカラムのテーブルであるanalysis_countsが駆動表であるべきです。しかしこの結果から、駆動表はanalysis_resultsであることがわかります。

テーブルをJOINする順番は結合オプティマイザが自動で判断するため、今回のように意図しないテーブルが駆動表になっているケースがあります。

改善案

パターン① STRAIGHT_JOIN

STRAIGHT_JOINは、左側のテーブルが常に右側のテーブルの前に読み取られるINNER JOINです。

以下の場合、analysis_counts STRAIGHT_JOIN ...のように左側のテーブルに指定することで、結合オプティマイザは駆動表と判断してくれます。

...
FROM product_variants
    INNER JOIN analysis_counts ON ...
    STRAIGHT_JOIN products ON ...
    INNER JOIN product_prices ON ...
    INNER JOIN analysis_results ON ...
...

パターン② USE INDEX

USE INDEX (index_list)を指定することによって、テーブル内の行検索に、指定されたインデックスの 1 つのみを使用するようMySQLに指示できます。また、index_listPRIMARYを指定することでインデックスとして主キーを指定できます。

今回はanalysis_countsの主キーを検索時のインデックスに指定したいので以下のようなSQLになります。これにより結合オプティマイザは駆動表と判断してくれます。

...
FROM product_variants
    INNER JOIN analysis_counts USE INDEX (PRIMARY) ON ...
    INNER JOIN products ON ...
    INNER JOIN product_prices ON ...
    INNER JOIN analysis_results ON ...
...

パターン③ JOIN_ORDER

MySQL8.0以降では、JOIN_ORDERヒントを使うことができるようです。

今回はバージョン8.0未満の環境であったため詳細は割愛しますが、STRAIGHT_JOINではINNER JOIN(内部結合)にのみ対応しているので、LEFT JOINを使う場合などに有効かと思います。

改善結果

今回のダッシュボードの実装では、「MySQLの言語に依存するSTRAIGHT_JOINより、インデックスを指定することを明示的にする方が駆動表になるテーブルがわかりやすい」という判断で、パターン②のUSE INDEXを使って駆動表を指定する実装をしました。

結果、平均51.5秒かかっていたクエリを平均329.5ms(約156.3倍の速度)にパフォーマンス改善することができました。

おわりに

今回のダッシュボードの実装を通して、意識していないとスロークエリが発生すること、MySQL特有のSTRAIGHT_JOIN、結合アルゴリズムなどについて理解できた良い機会になりました。

またこの実装では、普段より多く仕様の提案・相談をして、要件の決定から実装まで担当しました。裁量を持って担当でき、成長を実感できて楽しかったです!

crispyでは、HEIMや、サードモールの開発を一緒に盛り上げてくれるエンジニアを募集しています!興味がある方は、ぜひWantedlyのページから応募してください!

スタートアップにおすすめ!ヤマハ小規模向けWiFiルーターや無線アクセスポイントの紹介

f:id:takamario:20220222133538p:plain

はじめに

CTO 兼 情シス担当の高丸です。

スタートアップの初期は、社内ネットワークを組む人がおらず(そもそも複雑な社内ネットワークにする必要もなく)「エンジニアなんだからFAXも直せるでしょ」という形で、ネットワークチョットデキルのエンジニアやCTOが、情報システム部の役割を果たすことがあると思います。

我々も、インターネット企業である以上、インターネットが使えなくなってしまっては仕事になりません。 インターネットが使えて当たり前の人々しかいません(笑)

高速で安定したオフィスインターネット環境構築のための、ちょっとしたこだわりをご紹介します。

小規模向けルーターとは

今回のお話は、あくまでも社員数十名程度、同階2エリア以下を想定しています。

つまりは、一般家庭用のWiFiルーターを卒業したレベルを想定しています。
社員100名以上の規模だったり、会議室が多かったり、2階以上のフロアだったりすると、より機器の台数も多い複雑なネットワークになりますが、今回のお話では対象外です。

一般家庭向けと言えば、光回線の通信会社から提供されるレンタルルーターだったり、 BufflaloやNEC・IO DATA・TP-Link・ASUSなどのWiFiルーターを使うことが多いかと思います。

しかし、そういった一般家庭向けのWiFiルーターは、ぱたっとある程度の接続数・帯域になると通信が不安定になったりすることが多いです。
家庭向けなので、例として、4人家族が各人2端末ずつと、最近だと家の中のIoT機器5台使うとすると、多くても20台ぐらいの接続数を想定として作られているのかなと思います。

したがって、オフィスの人数が10人を超えてくると通信が不安定になるということであり、 そのタイミングで仕事ができないとなると、人数とともに会社の勢いも乗ってきた大事なときに事業活動に影響が出るということです。

これは何としてでも解決しなければいけない問題です。

構成

さて、我々のオフィス環境ですが、ほぼWiFiメインの通信で、有線LANを使うことがなく、特にサーバーというものも設置しておりません。 社内のファイルのやりとりは、Google Drive等で管理しているので、いわゆるDXができている状態です。

ですので、社内ネットワークを構築する際は、

  • いかに安定したWiFiにするか
  • いかに部屋の隅々まで届くようにするか
  • 十分な速度が出るか

光回線としては、NTTの「フレッツ 光ネクスト ギガマンション・スマートタイプ(速度最大1Gbps)」です。 (回線を速いやつにした方がいいだろっ!というのはさておき……)

プロバイダーは「ぷらら」で、元々の、マンションの一角で事業を初めた以来の回線環境が続いており、 まさに今は、一般家庭用のWiFiルーターを卒業したレベルです。

現状のオフィスは、大きい1フロアを、内装工事でパーティションで取り付けて会議室を1つ作った、同階2エリアです。
執務エリアの真ん中に無線アクセスポイント(以下:無線AP)を置きたかったため、その無線APが遠くならないよう、会議エリア側にも1台設置しました。

おすすめのヤマハルーター、無線アクセスポイント

我々が使用しているヤマハのネットワーク機器は以下の通りです。
(生産完了品もあります)

実にシンプルです。 人数的にそれほど機器が必要にならないのも当然なのですが、ヤマハネットワーク機器の安定性があってこその台数だと思います。

RTXシリーズのルーターではなく、NVR510を使用しているのは、ひかり電話のためです。 NVR510は、VoIPに対応しているので、ひかり電話もそのまま使うことができます。(要設定)

オフィスネットワーク図
オフィスネットワーク図

実際に構築してみたところ、十分すぎる電波強度で、全員が1個の無線APにつないでも通信は安定していました。むしろ、会議エリアでも執務エリアの無線APを拾いやすい状態でした。(もしかしたらWLX202は1台でも行けたかも……) ですので、無線APの電波強度は混線しないようにかなり落としました。

個人的に結構気に入っているのが、スイッチのSWX2200-8PoEで、 有線LANでつなぐことがないので不要なようにも見えるのですが、PoE対応しているので、無線APの電源がPoE対応のLANケーブルで給電することができ、余計な配線をする必要がなくなりました。

小型ONU対応でスリムに

NTT回線だけの話になりますが、現在の飯田橋のオフィスに移転する前は、NTTから提供されるレンタルルーター(PR-400NE)を使用しておりました。

PR-400NEは家庭向けなので、ONUとルーターと無線アクセスポイントが合わさったものになっています。 ですが、ONUの後のLANケーブルを直接ルーターに差し込む裏技を使うことで、NTTのルーターはONUとしてだけ機能させていました。

飯田橋オフィスに移転してからは、レンタルルーターではなく小型ONUに替えることで、直接NVR510にONUの回線をつなぐことができ、スリムな配線になりました。

まとめ

(個人的な好みが強いのですが)ヤマハのネットワーク機器を使った小規模オフィス向けWiFi環境を紹介しました。

インターネット企業であれば、社内インターネット環境への投資はするべきだと思います。 オフィス規模大きくなったり、人数が増えていっても、ヤマハのネットワーク機器はそれに耐えうる実績と安定性と使いやすさがありますので、ぜひ導入してみてはいかかでしょうか?

また、そんなインターネット環境が安定したオフィスのcrispyで働いてみたいと思った方は、ぜひWantedlyからお声がけください!

Google Cloud Functionsを使用したECサイトのクローリング技術について

f:id:crispyblog:20220208115826p:plain

はじめに

バックエンドエンジニアをしています斎藤です。

弊社ではAmazon、楽天、Yahoo!などの各ECサイトでの価格情報やレビュー情報を取り扱っているため、クローリングが必要不可欠になってきています。 しかしユーザーのリクエスト時にクローリングをしてしまうとレスポンスまでに時間がかかってしまい、ページ表示まで時間がかかりユーザービリティを損ないます。 そのためにクローリングの処理はGCPサービスにあるGoogle Cloud Functionsを採用して非同期で情報取得をしています。 今回はそのCloud Functionsの使い方について記してみようと思います。

Cloud Functionsとは

詳しくはググってもらった方がわかりやすいと思いますが、端的な説明をGoogleブログから引用すると

Google Cloud Functions は、サーバー管理なしでコードを実行するスケーラブルな従量課金制 Functions as a Service(FaaS)プラットフォームです。

と言うことらしいです。

サーバー管理なし=サーバーレスになるわけですが、GCP以外のAWS、Azureでも似たようなサービスは存在していますが、弊社はGCPでWebサーバーなどを構築しているため連携の取りやすいCloud Functionsを採用しています。

Google Cloud Functionsの特徴

Google Cloud Fuctions
トリガー - HTTP Request
- Google Cloud Pub/Sub
- Google Cloud Storage
- Cloud Firestore
-Firebase
(Realtime Database, Cloud Storage, アナリティクス, Auth)
- Cloud Logging
用途 - システムとの連携
- サーバーレスでのバックエンドアプリケーション
- リアルタイムデータ処理
- インテリジェントなアプリケーション
(Google AIサービスの利用)

トリガーとしてHTTP Requestや他のGCPサービスのイベントを使うことができます。 HTTP Requestでトリガーを受けることができるのは、どのような開発言語・システムであっても使えるので便利です。 またGCPサービスだと、例えばGoogleCloudStorageで使う場合は、ファイルがアップロードされたタイミングでファイル形式をチェックしてフォルダを振り分けるような使い方ができたり、FirebaseのAuthであればアカウントの作成・削除時に完了・削除メールの配信するシステムを構築することができます。

Cloud Functionsのメリット

  • サーバーレスなのでサーバーの管理などが不要
  • マネージドなのでスケール耐性を意識しないで良い
  • 実行時間でしか課金されないので、単発で動くプログラムなどは頻度によっては安く済む

Cloud Functionsのデメリット

  • 初回起動が遅いので、反応速度が早くなければいけないものなどには向いていない (最小インスタンス設定をすることで改善できます。 https://cloud.google.com/blog/ja/products/serverless/cloud-functions-supports-min-instances)
  • 使用するGCPのトリガーによって複数回呼び出される場合があるため仕様を確認して設計が必要。特に加算処理(カウンターや課金・ポイント処理など)は注意が必要
  • 使える言語は制限がある。(現在はNode.js/Python/Go/Java/.NET/Ruby/PHP)

Cloud Functionsの関数

Cloud Functionsの関数には、HTTP 関数とイベント ドリブン関数の 2 種類があり、さらに、イベント ドリブン関数は Cloud Functions のどのランタイムを対象に記述されているかによって、バックグラウンド関数または CloudEvent 関数のいずれかを使用することができます。 それぞれの関数を使用することで、同期/非同期ともに処理することができるので便利です。

HTTP関数

  • HTTPリクエストをトリガーとする
  • 関数の実行は同期的でレスポンス=関数の実行終了

→トラフィックを処理するために迅速にスケールアップ

バックグラウンド関数

  • Cloud Pub/SubやCloud Storageなどのインフライベントをトリガーとする
  • 関数の実行は非同期的で処理終了の通知はコールバック関数を使用する

→緩やかにスケールアップ

なぜCloud Functionsを選んだのか

弊社の開発言語としてはRailsを使用しているため、一般的な非同期処理のやりかたとしてはSidekiq、Delayed Job、Resqueなどが採用されるかと思いますが、Cloud functions + Cloud Tasksの構成にすることでフルマネージドで稼働ができ、実行時間でしか課金されないためコストが安く済ませることができます。また、開発言語に縛りがないため開発の幅が広がり機能に応じて最適な言語を選択しやすい部分があります。

弊社での使用例

  • 各ECサイトの商品価格および送料情報の取得
  • 各ECサイトの商品レビュー取得
  • ECサイトの商品レビュー分析
  • Lighthouseのレポート送信
  • Webからの画像投稿から不要なEXIF情報の削除
  • GitHubからSlackへのメンション変換

構成図

各ECサイトの商品価格および送料情報の取得の場合

f:id:crispyblog:20220202113142j:plain
商品価格取得の構成図

急にCloud Tasksが登場しましたが、リクエスト制御する上で HTTP関数を使っているため、同期処理になってしまいます。 ここで非同期化するためにCloud Tasksにリクエストを任せてしまいます。 各サイトの運用状況によっては500エラーになることがあるため、そうなってくると即時にリトライしてもエラー解消していないまま再アクセスとなってしまうのでタイマーを設定したいところなのですが、Cloud Functionsだけでタイマー使用するとその間インスタンスが起動しっぱなしになってもったいないです。 エラーを受け付けた場合は一度Cloud Functionsは終了し、Cloud Tasksに再リクエストをすることでタイマー替わりとなり、再度リクエスト処理することが可能です。

Cloud Functionsの使いづらいところ

  • 当初Node.jsの使えるバージョン(v10)が低かったが、どんどん上がるので対応していかないといけない
  • Cloud Loggingにログが出力されるが、出力内容が1行ごとになるのでわかりづらい(Cloud FunctionsというよりLoggingの使いづらいところですね)
  • 時間の経過と共にメモリ利用率が上がりがち
  • 関数呼び出しよりは遅くなってしまうのでリアルタイム性を求められる処理は向いていない

Cloud Functionsのいいところ

  • ローカル環境で動作確認をするための環境が整っている
  • デプロイがローカルマシンからCLI (gcloud コマンドを使用)でもWeb上のCloud Consoleからでも可能
  • オートスケーリングなのでスケーラビリティについて悩む必要がない
  • トリガーの種類が多く、GCPサービスを使用している場合は連携した機能を作りやすい

まとめ

上記のような構成をすることで、ユーザーには意識せずともデータ処理のリクエストを出すことができ、Webサーバー自体は自身のリクエストのみを捌くことに集中することができます。 Cloud Functions稼働によるコスト(Cloud Tasksも)はかかりますが、処理自体は低スペックでも処理できてしまうことがほとんどであるため、Webサーバー単体で動作させるよりはローコストなサーバーレスサービスで処理を実行することができるかと思います。 デメリットでもあげましたが、起動に時間がかかる部分とリクエスト処理をCloud Tasks、Cloud Functionsを経由させている時間もかかってくるため、リアルタイムでデータが必要な処理には向いていないので、適材適所で採用すれば有用かなと思います。

エンジニア募集中!

Cloud FunctionsやECサイトのデータを扱うことに興味がある人は、ぜひWantedlyのページから応募してください! 話を聞くだけでもお待ちしております!