【Gem Pundit】Punditの内部実装で学んだこと

この記事は、 PLEX Advent Calendar 2024の24日目の記事です。

はじめに

2024年に株式会社プレックスにエンジニアとして新卒入社した佐藤祐飛(@yuhi_junior)と申します。

業務でGem Punditを利用する機会があり、その内部実装を読んで学びが多くあったため本ブログではそちらを共有したいと思います。

Punditとは

PunditはRuby on Railsのための認可ライブラリで、Policy Objectデザインパターンを簡単に実装することができます。

Punditの使用例

class ApplicationController < ActionController::Base
  include Pundit::Authorization
  ...
end
class PostPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def update?
    user.admin? || !post.published?
  end
end
def update
  @post = Post.find(params[:id])
  authorize @post
  ...
end

Punditが内部で行なっていること

Punditがauthorizeメソッドで行っていることは大きく3つ存在します。特に3つ目はRubyメタプログラミングを利用しているので、その部分を中心に解説していきます。

  1. Policyクラスのインスタンスをキャッシュ
  2. Punditユーザーが定義したPolicyクラスのメソッドを実行
  3. authorizeメソッドの引数に渡されたインスタンスに対応するPolicyクラスを動的に特定

内部実装

今回は上記のPunditの利用例についての内部実装を見ていきます。

Pundit::Authorization#authorize

Postモデルの例におけるコントローラー内で実行されているauthorizeメソッドで、recordには@postが代入されます。action_nameメソッドは実行されているコントローラーのアクション名を返します。その結果queryにはupdate?が代入されます。policy_classにはnilが代入されます。具体の実装はPundit::Authorization#punditメソッドが返すオブジェクトのauthorizeメソッドに定義されています。

def authorize(record, query = nil, policy_class: nil)
  query ||= "#{action_name}?"

  @_pundit_policy_authorized = true

  pundit.authorize(record, query: query, policy_class: policy_class) 
end

https://github.com/varvet/pundit/blob/main/lib/pundit/authorization.rb#L84

Pundit::Authorization#pundit

Pundit::Authorization#punditメソッドはPundit::Contextクラスのインスタンスを返します。Pundit::ContextについてはContextとはで解説します。pundit_userはコントローラー内でcurrent_userメソッドを実行した結果が入ります(実装)。policy_cacheには権限判定のキャッシュに利用する空のハッシュが内部的に定義されます(実装)。これはPunditが内部で行なっていることの2つ目に該当します。

def pundit
  @pundit ||= Pundit::Context.new(
    user: pundit_user,
    policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
  )
end

https://github.com/varvet/pundit/blob/main/lib/pundit/authorization.rb#L33

Pundit::Context#authorize

possibly_namespaced_recordには@postが代入されており、pundit_modelメソッドでは今回はそのまま@postを返します(実装)。possibly_namespaced_というプレフィックスがついているのはPunditがPolicy Namespacingという機能を提供しているためです。

policy!メソッドはpossibly_namespaced_recordから動的にPolicyクラスを特定し、そのインスタンスを返し、policyに代入します。その後、policy.update?メソッドを実行し、権限判定を行います(Punditが内部で行なっていることの2つ目に該当)。もし権限がない場合はNotAuthorizedErrorが発生します。

def authorize(possibly_namespaced_record, query:, policy_class:)
  record = pundit_model(possibly_namespaced_record)
  policy =
    ...
    policy!(possibly_namespaced_record)
  end

  raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)

  record
end

https://github.com/varvet/pundit/blob/main/lib/pundit/context.rb#L56

Pundit::Context#policy!

cached_findメソッドに対してrecord&:policy!を引数として渡しています。cached_findメソッドの内部のyieldの第一引数をレシーバとしてpolicy!メソッドが実行されることになります。このcached_findメソッドはPunditが内部で行なっていることの3つ目に該当します。

def policy!(record)
  cached_find(record, &:policy!)
end

https://github.com/varvet/pundit/blob/main/lib/pundit/context.rb#L86

Pundit::Context#cached_find

Pundit::Contextインスタンス作成時に定義されたpolicy_cachefetchメソッドを叩きます。recordをキーとしてキャッシュが存在すればそのPolicyインスタンスを返し、キャッシュが存在しなければブロックが実行されます(実装)。ブロック内部ではまず、policy_finderメソッドを実行して、recordに対応するPolicyを特定するためのPolicyFinderインスタンスを取得します(実装)。その後、Pundit::Context#policy!で述べたとおり、yieldによってPolicyFinderインスタンス.policy!が実行されます。modelには@postがそのまま代入され、Policyクラスのインスタンスが返されます。

def cached_find(record)
  policy_cache.fetch(user: user, record: record) do
    klass = yield policy_finder(record)
    
    ...

    model = pundit_model(record)

    begin
        klass.new(user, model)
    rescue ArgumentError
        raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
    end
  end
end

https://github.com/varvet/pundit/blob/main/lib/pundit/context.rb#L147

Pundit::PolicyFinder#policy!

policy!メソッドは実質policyメソッドを実行しています。findメソッドでいよいよPolicyクラスを特定します。

def policy
  klass = find(object)
  klass.is_a?(String) ? klass.safe_constantize : klass
end

https://github.com/varvet/pundit/blob/main/lib/pundit/policy_finder.rb#L46

Pundit::PolicyFinder#find

findメソッドはfind_class_nameメソッドを実行して、Policyクラス名を特定します。find_class_nameメソッドの内部でsubject.classが実行され、@post.classが返されます。その後、find_class_nameメソッドの結果にSUFFIXが付与され、PostPolicyが返されます。

def find(subject)
  ...
  else
    klass = find_class_name(subject)
    "#{klass}#{SUFFIX}"
  end
end

def find_class_name(subject)
  ...
  else
    subject.class
  end
end

https://github.com/varvet/pundit/blob/main/lib/pundit/policy_finder.rb#L86 https://github.com/varvet/pundit/blob/main/lib/pundit/policy_finder.rb#L110

Contextとは

僕が初めてこのコードを読んだ時はContextって何?Pundit::AuthorizationにそのままContextクラスの内容を定義すれば良いのではないか?と思いました。

Punditのレポジトリを漁ると以下のようなCOMMITを見つけました。

元々はPundit::Authorization#authorizeにおいてPundit.authorizeを叩いていましたが、Pundit::Contextクラスを導入する変更がなされています。また、Pundit.authorizeでもPundit::Context#authorizeを呼び出すように変更されています。

+ def pundit
+   @pundit ||= Pundit::Context.new(
+     user: pundit_user,
+     policy_cache: policies
+   )
+ end

  def authorize(record, query = nil, policy_class: nil)
    query ||= "#{action_name}?"

    @_pundit_policy_authorized = true

-   Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
+   pundit.authorize(record, query: query, policy_class: policy_class)
  end
- def authorize(user, possibly_namespaced_record, query, policy_class: nil, cache: {})
-   record = pundit_model(possibly_namespaced_record)
-   policy = if policy_class
-     policy_class.new(user, record)
-   else
-     cache[possibly_namespaced_record] ||= policy!(user, possibly_namespaced_record)
-   end
-   raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
-   record
+ # @see [Pundit::Context#authorize]
+ def authorize(user, record, query, policy_class: nil, cache: {})
+   Context.new(user: user, policy_cache: cache).authorize(record, query: query, policy_class: policy_class)
  end

https://github.com/varvet/pundit/commit/9045680e4faef2ee87a2273c09d7d77d7d024f6e

Contextの導入によってPundit全体のインターフェースを薄くすることに成功しています。具体的には以下の箇所で、userとcacheをContextに保持することで、authorizeメソッドの引数が減り、authorizeメソッドの呼び出し側のコードが簡潔になります。また、PunditにはScopesPolicy Namespacingなどの機能もあり、それらの機能もContextに閉じ込めることで、Punditのインターフェースを薄くすることに成功しています。userとcacheをContextの責務と捉えてDRY原則を体現していると言えます。

-   Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
+   pundit.authorize(record, query: query, policy_class: policy_class)

Punditの内部実装とデザインパターン

Punditの内部実装を読んでいくと、StrategyパターンとService Locatorパターンが使われていることがわかりました。

Policyクラスに定義されているupdate?などの認可メソッドはqueryとして利用されており、StrategyパターンにおけるStrategyに該当します。Pundit::ContextはそのままContextに該当します。

Pundit::PolicyFinderはPolicyクラスを特定するためのクラスであり、Service Locatorパターンに該当します。

まとめ

Gem Punditの内部実装を読んでいきました。Rubyらしさが溢れていて読むのが楽しく、Rubyのコードリーディング力が向上した気がします。今後もOSSのコードリーディングを続けていきたいなと思います。

ActiveJobのloggerメソッドをスタブするはなし

こんにちは、Plex Job開発チームの種井です。

この記事は、 PLEX Advent Calendar 2024の23日目の記事です。

私の書いた前回前々回の記事でSemantic Loggerを使用してRailsアプリケーションから出力されるログを構造化する取り組みについて紹介しました。 今回はSemantic Loggerを使用して開発を行う中で、ログイベントのテストを作成する機会がありました。 その上で、いくらか工夫したところがあったので紹介したいと思います。

また、Plex Jobでは

を使用しているため、サンプルコードについてもRailsおよびRSpecの使用を前提として説明しています。

背景

テストを作成していると、あるログイベントが発生したかどうかをテストしたいことがあります。 そのような場合には、Rails.loggerの各メソッドの呼び出しを検証することになります。 以下は、実装例となります。

class HomeController < ApplicationController
  def index
    # Does some stuff ...
    logger.info "Someone visited the site!"
    # Does some more stuff ...
  end
end
it "logs a message" do
  visit root_path

  expect(page).to have_content "Welcome to my site!"

  expect(Rails.logger).to receive(:info).with("Someone visited the site!")
end

everydayrails.com

Semantic Loggerでは、グローバルにRails.loggerを上書きする仕組み上、ログイベントを検証するにあたって、上記のような方法でログを検証することができません。 Semantic Loggerの公式ドキュメントにもあるように

context 'when it blows up' do
  let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new } # ①

  it 'should should log the error' do
    allow_any_instance_of(MyThing).to receive(:logger).and_return(capture_logger) # ②
    MyThing.new('asdf').do_something!

    # ③
    expect(capture_logger.events.last.message).to include('Here is a message')
    expect(capture_logger.events.last.level).to eq(:error)
  end
end
  • SemanticLogger::Test::CaptureLogEventsインスタンスを作成する
  • ② 対象のメソッドをallow_any_instance_ofを使ってスタブする
  • SemanticLogger::Test::CaptureLogEventsに対してログイベントを検証する

必要があります。

加えて今回はActiveJobを使った非同期処理に対してテストを書く必要があったので、loggerメソッドをスタブしてSemanticLogger::Test::CaptureLogEventsインスタンスへと差し替える必要がありました。

以下は実装例です。

class SampleJob < ApplicationJob
  def perform(message)
    logger.info(message)
  end
end
require "rails_helper"  
  
RSpec.describe SampleJob, type: :job do
  describe "#perform_later" do
    let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new }
      
    it "logs a message" do
        allow_any_instance_of(described_class).to receive(:logger).and_return(capture_logger)
          
        described_class.perform_now('message !!')
        expect(capture_logger.events.last.payload).to eq("message!!")
    end
  end
end

これでもログの出力をテストしたいという当初の目的は果たせますが、allow_any_instance_ofの使用はなるべく避けたいです。(RuboCopにも違反扱いされてしまいます) また、ActiveJobではloggerアクセサが定義されています。そのためRails.loggerではなくloggerメソッドを呼び出してログ出力を行うインターフェースであることも今回実装する上で考慮しなければならいないポイントでした。

今回は、allow_any_instance_ofを使用せずにActiveJobのloggerメソッドをスタブする方法を調査・実装したので紹介したいと思います。

ActiveJobのコードを見てみる

まずは、スタブする対象について調べてみることにします。 ApplicationJobはActiveJob::Baseクラスを継承しているため、そちらを見てみます。

module ActiveJob
    class Base
        include Core
        include QueueAdapter
        include QueueName
        include QueuePriority
        include Enqueuing
        include Execution
        include Callbacks
        include Exceptions
        include Instrumentation
        include Logging
        include ExecutionState
    
        ActiveSupport.run_load_hooks(:active_job, self)
    end
end

github.com

Loggingというモジュールをインクルードしています。

次にLoggingモジュールを見てみます。

module ActiveJob
    module Logging
      extend ActiveSupport::Concern

        included do
          ##
          # Accepts a logger conforming to the interface of Log4r or the default
          # Ruby +Logger+ class. You can retrieve this logger by calling +logger+ on
          # either an Active Job job class or an Active Job job instance.
          cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))

            ...
        end
        ...
    end
end

github.com

cattr_accessorlogger属性が定義されています。cattr_accessorはクラス属性のクラス・アクセサとインスタンス・アクセサの両方を定義するアクセサです。 また、cattr_accessormattr_accessorエイリアスです。

公式ドキュメントの例を借りてmattr_accessorの挙動を簡単に確認してみます。

module HairColors
  mattr_accessor :hair_colors # ①
end

class Person
  include HairColors
end

# ②
HairColors.hair_colors = [:brown, :black, :blonde, :red]
HairColors.hair_colors # => [:brown, :black, :blonde, :red]
Person.new.hair_colors # => [:brown, :black, :blonde, :red]
  • ① mattr_accessorによりhair_colorsクラス変数が定義され、同時にクラスアクセサとインスタンスアクセサの両方が定義される
  • ② アクセサを使って値の参照や代入を行うことができる

ここまでで、AcriveJob::Baseクラスがどのようにloggerを定義しているかがわかりました。

テンプレートメソッドであるperform メソッドはインスタンスメソッドなので、インスタンスアクセサとして定義されたloggerをスタブするとよさそうだとわかりました。

スタブする方法

ActiveJob::Baseクラスがどのようにloggerを定義しているかがわかったところで、早速対象をスタブしていきます。 ActiveJob::Base.new メソッドの返り値をスタブすることで実現することができます。

class SampleJob < ApplicationJob
  def perform(message)
    logger.info(message)
  end
end
require "rails_helper"

RSpec.describe SampleJob, type: :job do
  describe "#perform_later" do
    let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new }
      
    it "logs a message" do

        instance = described_class.new('message !!')
        allow(described_class).to receive(:new).and_return(instance)
        allow(instance).to receive(:logger).and_return(capture_logger)
          
        described_class.perform_now('message !!')

        expect(capture_logger.events.last.payload).to eq("message!!")
    end
  end
end
  • ① あらかじめSampleJobのインスタンスをperform_nowと同じパラメータで作成しておく
  • ② SampleJobクラスの初期化メソッドをスタブし、①のインスタンスを返すようにする
  • ③ SampleJobのloggerインスタンスアクセサをスタブし、SemanticLogger::Test::CaptureLogEventsに差し替える

どちらを使うか?

ここまでで、allow_any_instance_ofを個別のインスタンスを指定する方法に書き換えることができました。 それぞれの書き方を改めて並べてみます。

allow_any_instance_ofを使う方法

require "rails_helper"  
  
RSpec.describe SampleJob, type: :job do
  describe "#perform_later" do
    let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new }
      
    it "logs a message" do
        allow_any_instance_of(described_class).to receive(:logger).and_return(capture_logger)
          
        described_class.perform_now('message !!')
        expect(capture_logger.events.last.payload).to eq("message!!")
    end
  end
end

allow_any_instance_ofを使わない方法

require "rails_helper"

RSpec.describe SampleJob, type: :job do
  describe "#perform_later" do
    let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new }
      
    it "logs a message" do
        instance = described_class.new('message !!')
        allow(described_class).to receive(:new).and_return(instance)
        allow(instance).to receive(:logger).and_return(capture_logger)
          
        described_class.perform_now('message !!')

        expect(capture_logger.events.last.payload).to eq("message!!")
    end
  end
end

allow_any_instance_ofを使う方が、一見するとコードとしては短くなり、使わない方に関してはやや冗長な書きぶりになってしまいます。

今回は、後者のallow_any_instance_ofを使用しない方法を採用することにしました。

というのも、前半に少し触れましたがallow_any_instance_ofrspec-mocks公式のドキュメントでも言及されているように推奨されていません。

  • rspec-mocksのAPIは個々のオブジェクトインスタンス向けに設計されているが、該当の機能はオブジェクトのクラス全体に対して作用するため意図しない挙動や可読性の低下を招いてしまう
  • この機能を必要とする時点で設計上の問題があることが多い。 テストがあまりに多くのことをしようとしすぎているか、テスト対象のオブジェクトが複雑すぎる可能性が高い
  • rspec-mocksの最も複雑な機能で、歴史的に最も多くのバグ報告を受けている

上記のような理由から、Semantic Loggerの公式ドキュメントで紹介されているテストコード上でのログイベントの検証方法を採用せず、個別のインスタンスをスタブすることにしました。

おわりに

allow_any_instance_ofを個別のインスタンスに対してスタブするように書き換えるだけと言えばそうなのですが、ActiveJobのBaseクラスやCoreクラスの仕様をはじめフレームワークの構造をまず理解した上でスタブ対象の特定を行う必要がありました。 同じようなケースでallow_any_instance_ofの書き換えを検討している方の参考になればと思います。

これがkintone開発の理想形!! 開発環境を劇的に改善した話

これがkintone開発の理想形!! 開発環境を劇的に改善した話

この記事は、 PLEX Advent Calendar 2024の21日目の記事です。

はじめに

こんにちは。コーポレートチームの山崎です。

ビジネスを加速させる kintone!!
kintone を加速させるプラグイン・カスタマイズ!!

本記事では、このプラグイン・カスタマイズ開発を加速させる最高の開発環境を作った話をしようと思います。

kintone.cybozu.co.jp

前提

この記事における 「kintone 開発」とは kintone のプラグインおよびカスタマイズの開発を指します。 JavaScript によるフロントエンド開発と捉えてもらって問題ありません。

cybozu.dev

cybozu.dev

背景

以前まではビジネスサイドのメンバーが中心となってkintone開発していました。
開発環境やコードの整備が不十分で「動けばヨシ!!」という思想の元、乱雑なコードが量産されていきました。

alu.jp

その結果、エンジニアには次々とバグ修正の依頼が飛んでくる状況に。
原因の特定や修正に時間がかかり、負担も大きくなっていました。

コーポレートチームのメンバー増員に伴い、この状況を改善するため、エンジニアが主体となって kintone 開発を進めていく方針となりました。

alu.jp

旧開発環境のツラかったところ

一応エンジニアが管理している kintone 用のリポジトリも存在していましたが、かなり不便でした。

バンドラが無い

  • モジュール管理やライブラリの追加ができない
  • 単一の JS ファイルのみで書かれていた

TypeScirpt が書けない

  • Vanila JS しか書けない...
  • 型が欲しい😢

複雑な UI を作るのが辛い

  • React や Vue も導入していなかった
  • createElement して再レンダリングまで自前で実装みたいな...

動作確認が大変

  • kintone が提供しているツールを使っていたが、アップロードに若干の時間がかかり、細かい動作確認時に不便

cybozu.dev

cybozu.dev

プラグインの作成が難しい

  • プラグインを作成する場合、フォルダ構成や必要なファイルが決まっている
  • 若干複雑なので新規プラグインの立ち上げに手間がかかる

cybozu.dev

とても快適に開発できる環境ではなかった...

alu.jp

開発環境を刷新したい!!

上記のツラさから kintone 開発は面倒くさいものという認識が広がりつつありました。

kintone は悪く無い... 石器時代のような環境が悪いんや...

しかし、裏を返せば
難しいことをせずとも、モダンな技術スタックを持ち込むだけで、劇的な効率化が可能ということです!
ローコスト・ハイインパクト!! やるしかない!!

alu.jp

モジュールバンドラを導入しよう

察しの良い方はお気づきかもしれませんが、上述したツラい点のほとんどは、モジュールバンドラを導入することで解決できます。
開発環境刷新に取り掛かったのは 2024年3月頃、モジュールバンドラ界隈は Vite が台頭していました。

blog.tech-monex.com

kintone は多数の開発 Tips を公開していますが、
当時 Vite を用いた開発に関する記事は存在しておらず、自分で頑張って作りました...

という話を書こうと思っていたのですが、最近 kintone が下記記事を公開していたので割愛します。公式を見てください。

cybozu.dev

とにかく...!!
Vite を導入したことで現代フロントエンジニアが満足に開発できる環境が整いました!!

  • ライブラリを追加できるようになった
  • React で簡単に複雑な UI を作成できるようになった
  • TypeScript で書けるようになった

alu.jp

誰でも簡単に kintone 開発ができるようにしたい!!

最低限のフロントエンド開発環境は整ったものの、kintone開発経験が無いエンジニアにとっては、いくつかの障壁が残っていました。

  • React は書けるけど、kintone 上でどう動かすの?
  • どうやって kintone にアップロードするの?
  • カスタマイズとプラグインって作り方違うの!?
  • etc...

alu.jp

カスタマイズ・プラグインの雛形を CLI から作成

kintone 開発リポジトリは Turborepo を使ったモノレポ構成になっています。(これも導入した)
そして、Turborepo にはジェネレータを定義してコード生成する機能が存在します。

turbo.build

プラグイン・カスタマイズの雛形を用意しておき、ジェネレータを定義して、CLI から一発で作成できるようにしました。

雛形にはkintone上でReactコンポーネントを表示する部分まで用意しているので、kintoneとの繋ぎ込みを気にせず、実装そのものに集中できるようになりました!

➜ yarn create:customize

? カスタマイズ名を入力してください。例: project-name customize-name
? 対象のkintoneアプリのIDを入力してください 100
? 開発用アプリがあればIDを入力してください 999
? 機能の概要を入力してください。例: ブログ記事用のテストだよ
>>> Changes made:
  • /apps/100/customize-name/package.json (add)
  • 6 files added
 -> /apps/100/customize-name/README.md
 -> /apps/100/customize-name/tsconfig.json
 -> /apps/100/customize-name/vite.config.ts
 -> /apps/100/customize-name/src/App.tsx
 -> /apps/100/customize-name/src/main.tsx
 -> /apps/100/customize-name/src/vite-env.d.ts (addMany)

>>> Success!
✨  Done in 38.38s.

CLI から GCS 経由でニアリアルタイムにアップロード

kintone から提供されている customize-uploader はアップロードに若干の時間がかかるため、細かい変更を反映させて動作確認するのは少し手間でした。

cybozu.dev

ホットリロードのような開発体験を目指して下記スクリプトを追加しました。

  • nodemon で変更検知&ビルド
  • gsutil で GCS にアップロード
  • concurrently で上記2つのコマンドを同時実行

kintone はURL指定でカスタマイズを利用することができるので、GCSにアップロードしたファイルを指定することで、高速かつ簡単にアップロードできるようになりました!

ただし、プラグインのアップロードにはブラウザの操作が必要なため、公式提供の plugin-uploader を使用しています。

nodemon.io

cloud.google.com

github.com

最高の開発環境ができた

難しい実装はせずに、モダン技術を取り込むだけでストレスの無い最高の開発環境が完成しました!!
やはり、流行りの技術をキャッチアップするのはとても大切だと感じました。

alu.jp

開発環境刷新後の効果

本当に誰でも kintone 開発できるようになった

業務委託の方や入社したばかりメンバーがスムーズに開発を始めることができるようになりました。
「誰でも簡単に kintone 開発ができるようにしたい!!」という目標を達成することができたし、「フロントエンド開発ができる = kintone 開発ができる」という状況を作ることができたのはとても大きいです。

大幅に開発効率が上がった

kintone 開発特有の障壁を取り除く事で、大幅に開発効率が上がりました。(冗談抜きで百倍効率化できたと感じる)
また、kintone 開発をできるエンジニアが増えたので、チーム全体としてのアウトプット量も増やすことができました。

kintone を利用した業務オペレーションの選択肢が広がった

旧環境では難しかったリッチな UI を簡単に作成できるようになったことで、業務オペレーションの選択肢が大幅に増えました。
ビジネスサイドのメンバーが考えるアイデアをより多く実現できるようになり、会社の成長に大きく貢献できるようになりました。

まとめ

最終的な開発環境の構成を簡単にまとめておきます。

  • Turborepo によるモノレポ構成
  • Vite によるバンドル
  • TypeScript で開発可能
  • CLI から雛形を生成可能
  • GCS 経由でニアリアルタイム更新反映

面倒臭いコト・効率が悪いコトを技術で解決する。エンジニア冥利に尽きますね。
みなさんの kintone 開発 tips もコメントで教えていただけると嬉しいです!

さいごに

現在プレックスではソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーを募集しています。

一緒に働いてみたいと思った方がいましたら、是非ご連絡をお待ちしています!

dev.plex.co.jp

オンプレRedashの運用Tips8選

こんにちは、プレックスの石塚です。この記事は、 PLEX Advent Calendar 202420日目の記事です。

プレックスではBIツールとして2年ほど前からRedashを使用しています。今回の記事ではRedashの簡単な紹介とオンプレで運用する上でのTipsを8つ紹介します。

以前はMetabaseというBIツールを使用していたのですが、データ活用が思ったより進まず、Redashに移行する流れとなりました。このあたりの話もいずれ機会があればブログにまとめたいと思います。

Redashとは?

RedashPython製のオープンソースとして作られたBIツールです。自分が個人的に感じるRedashの最大の特徴はエンジニアフレンドリーであることです。下記の画像のように直接SQLを書いて、それを即座にグラフの形にビジュアライズできるため、エンジニアにとって直感的に使えるBIツールとなっています(APIの利用も非常に簡単です)。

そんな便利なRedashですが、2020年にDatabricksに買収されたことをきっかけに、クラウド版のサービス終了とOSS開発の停止という状況になってしまいました。そのため、現時点でRedashを使うためには、自社でサーバーをホスティングしてオンプレの環境で動かすことが必要です。

redash.io

※2023年4月にコミュニティ主導のプロジェクトとして、OSS開発を再開するというアナウンスがありました。まだメジャーバージョンのリリースはありませんが、コントリビューションを見る限り着々と開発が進んでいるようです。

github.com

オンプレRedashの運用Tips8選

以前はクラウド版があったため、運用について考える部分が少なかったのですが、今はオンプレで動かすしか選択肢がないということで、運用を楽にするためのTipsをいくつか紹介していきます。Redashのバージョンは 10.1.0 を想定しています。

1. パスワードログインを無効にする

Redashではパスワードログイン、Googleログイン、SAMLの3つの認証方法をサポートしています。パスワードログインを有効にしてしまうと、Redash側で退職者の整理などのアカウント管理をしなければならなくなるため、運用のコストが増えてしまいます。

https://redash.io/help/user-guide/users/authentication-options/

ログイン方法はRedashの画面上( /settings/general )から設定が可能です。

2. グループを使ったデータソースの管理

Redashでは権限をグループ×データソース×アクション( Full Access or View Only )で管理できます。複数の事業部やチームからの利用を想定している場合は、初期からグループの設計をしておくとよいでしょう。

https://redash.io/help/user-guide/users/permissions-groups/

3. redashbotを導入する

redashbotは、Slack上でRedashのURLをメンションするとグラフを展開してくれるツールです。Slack標準のリマインダーを使って、定期でグラフを流すといったことも可能です。

https://raw.githubusercontent.com/yamitzky/redashbot/refs/heads/main/images/screenshot.png

以前はRedashと同じインスタンス内で docker run で動かしていたのですが、たまにプロセスが落ちることがあったため、Redashを動かしている docker-compose.ymlrestart: always オプションを付けて組み込んだところ、落ちなくなりました。

github.com

4. クエリの同時実行数を調整する

Redashの手動でのクエリの同時実行数はデフォルトで2になっています。1つの事業部で使用している場合は問題にならないかもしれませんが、複数の事業部で複数のデータソースに対してクエリを実行したいケースでは、クエリの同時実行数を上げたい場合があります。

これは docker-compose.yml から adhoc_worker環境変数である WORKERS_COUNT の数字で指定することで調整可能です。

  adhoc_worker:
    <<: *redash-service
    command: worker
    environment:
      QUEUES: "queries"
      WORKERS_COUNT: 4

5. Redashサイトの死活監視

オンプレで運用しているRedash上で、LIMITを付け忘れるなど取得するデータ量の多いクエリを投げてしまうと、サービスが落ちてしまうことが多々あります。プレックスではこれをすぐに検知できるように、Google Cloud MonitoringのUptime checksを使用して、Slackに通知しています。設定方法は下記のドキュメントを参照していただきたいのですが、簡単に死活監視を実現できます。

公開の稼働時間チェックを作成する  |  Cloud Monitoring  |  Google Cloud

死活監視の上で工夫しているポイントとしては、通知先をRedashの利用者が集まるSlackのチャンネルにしていることです。以前は開発者しかいないエラー通知部屋に通知していたのですが、利用するメンバーにもサービスが落ちていることが周知できた方が良い、落ちる原因がほとんどクエリ起因なのでクエリ実行者に自覚してもらいたい、ということで変更しました。

また通知のメッセージには直近のクエリの実行時間、結果行数を調査するクエリのURLを付与しており、原因のクエリの調査をしやすくしています。Redash内部のPostgreSQLに対して発行できるクエリなので、参考までに貼っておきます。

SELECT
    '<a target="_blank" href="https://{domain}/queries/' || q.id || '">' || q.id || '</a>' AS id,
    q.name,
    q.user_id,
    u.name AS user_name,
    q.is_archived,
    q.is_draft,
    q.tags,
    qr.id AS query_result_id,
    qr.runtime AS seconds,
    JSON_ARRAY_LENGTH(qr.data::json->'rows') AS rows,
    qr.retrieved_at + INTERVAL '9 hours' AS retrieved_at_jst
FROM
    query_results AS qr
JOIN
    queries AS q
ON
    q.query_hash = qr.query_hash
JOIN
    users AS u
ON
    u.id = q.user_id
WHERE
    qr.retrieved_at > NOW() - INTERVAL '1 hour'
ORDER BY retrieved_at_jst
LIMIT 1000;

6. 定期実行の死活監視

Redashには定期実行の機能がサポートされているのですが、プレックスで運用していたところ、定期実行が実行されていないというケースが稀にありました。そのため、サイト以外にも定期実行の死活監視も行っています。

方法としては、Railsのrakeタスクで定期実行をセットしたクエリの /api/queries/<id>/results APIを叩いて、クエリの最終実行時間(retrieved_at)がセットした時間内に行われているかをチェックしています。

https://redash.io/help/user-guide/integrations-and-api/api/

余談ですが、定期実行が失敗する根本の原因はRQ Schedulerのバージョンが古いことにあるようでした。コンテナの中に入って直接RQ Schedulerをバージョンアップしたところ、この問題は解決することができました。

https://github.com/getredash/redash/issues/5797 https://dev.classmethod.jp/articles/fix-redash-scheduler-error-by-rq-scheduler/

7. クエリのアーカイブ

Redashはクエリの検索機能の精度があまり良くなかったり、クエリを整理するためのフォルダ機能などがないためかクエリが乱立して探しづらいといった特徴があります。プレックスも例に漏れず、運用を開始して2年弱で約1,000個のクエリが作成されており、クエリを探すコストはどんどん上がっています。

命名やタグ付けのルールも定めていますが、いまいち浸透しきっていない状況です。そのため、2週間に1度、期間中に作成されたクエリを全部チェックして不要なクエリがあればアーカイブするという力技の作業を行っています。

UnpublishedなクエリのアーカイブなどはRedashの内部APIを使用すれば自動化できそうなので、部分的にやっていきたいと思っています。

8. いざという時のためのトラブルシューティング

実はRedashには内部のクエリの実行状況やキューの状況を確認できる管理画面が存在します。 /admin/status のURLから管理者権限を持っているユーザーのみアクセスできます。

クエリが実行中のまま終わらなくなってしまった、キューにクエリが溜まりすぎていて新しいクエリが実行できない等トラブルが発生した際の状況確認に上の管理画面は有用です。一方で管理画面から実行中のクエリを停止させる、キューの中身をクリアするといった機能は提供されていないため、Redashの内部APIを叩くかサーバーに入って作業する必要があります。

内部APIを叩く一例として、実行中のタスクを取得して、それを停止させるサンプルを載せておきます。

% curl -s "https://{domain}/api/admin/queries/rq_status?api_key={api_key}" | jq .queues.queries
{
  "name": "queries",
  "started": [
    {
      "id": "e891f8ff-10b4-4b6f-a6c6-982cda1efa6f",
      "name": "redash.tasks.queries.execution.execute_query",
      "origin": "queries",
      "enqueued_at": "2024-12-18T01:51:43.744",
      "started_at": "2024-12-18T01:51:43.764",
      "meta": {
        "data_source_id": 6,
        "org_id": 1,
        "scheduled": false,
        "query_id": "adhoc",
        "user_id": 1
      }
    }
  ],
  "queued": 0
}
% curl -X DELETE "https://{domain}/api/jobs/e891f8ff-10b4-4b6f-a6c6-982cda1efa6f?api_key={api_key}"
null%

このようにRedashは内部で使用しているAPIAPIキー( /users/me から生成可能)があれば叩けるようになっています。もちろんドキュメントは用意されていないので、Githubのコードを直接読んで、自己責任での実行をお願いします。キューの中身を削除する場合は直接Redisに入ってredis-cliからクリアするといったことも可能です。

おわりに

いくつかオンプレでRedashを運用するためのTipsをご紹介させていただきました。少しでも自社でRedashを運用している方の参考になれば幸いです。

コミュニティ主導のプロジェクトとして再稼働したRedashですが、これからの開発や新しいバージョンのリリースにも期待したいですね。

Webパフォーマンス改善に向き合っていくお話

こんにちは、Plex Job開発チームの高岡です。

この記事は、 PLEX Advent Calendar 2024の16日目の記事です。

以前投稿した入社エントリーでReactにおけるパフォーマンスチューニングについてお話する旨を記載してから、早半年が経過しました...
そのお話をする前に、パフォーマンス改善に向き合うための基礎知識をまとめましたので、こちらからお話ししていきたいと思います。 Reactでのパフォーマンス改善の具体的なお話は気長にお待ちいただけますと嬉しいです 🙏

対象読者

  • パフォーマンスに興味を持ち始めた方
  • Webパフォーマンスの指標を勉強したい方

背景

Plex Job開発チームでは、Next.jsのSSG(静的サイト生成)を利用する中で、ページ数が多くVercelのビルド時間を超過したり、デプロイ完了までの時間が長くなってしまうといった技術的な課題に直面しています。 この課題を解決するためには、ビルドプロセスのチューニングが重要ですが、そもそも以下のような疑問が生じています。

  • SSGはパフォーマンスにおいてどのような効果をもたらし、その効果はどの程度なのか
  • SSR(サーバーサイドレンダリング)に変更することで問題を解消でないのか

これらの疑問を解消するためには、パフォーマンスの計測と評価を適切に行い、その結果を基にアプローチを決定する必要があります。
そこで、今回Webパフォーマンスの指標とその計測方法に関してまとめ、今後の改善に役立てることを目的としています。

Webパフォーマンスとは

MDNでは下記のように記載されています。

ウェブパフォーマンスとは、サイトが読み込まれるまでの時間、操作可能・応答可能になるまでの時間、そしてユーザーが操作する際のコンテンツのスムーズさを意味します。スクロールはスムーズか、ボタンはクリックしやすいか、ポップアップはすぐに読み込まれて表示されるか、表示の際にスムーズにアニメーションするか。

MDN Web Performance

Webパフォーマンスを改善する目的は、大きく下記の2つの理由があります。

これらの要素はユーザーの離脱を防ぎ、コンバージョン率の向上に寄与します。そのほか、検索エンジンでの上位表示により新規ユーザーの獲得機会が増加し、ビジネスの成長を促進します。

Webパフォーマンスがどのように重要かを理解したところで、次は具体的に何を基準に改善を進めるべきかを見ていきます。Googleが提唱する「Core Web Vitals」という指標が、その指針となります。

現在のCore Web Vitals

Core Web Vitalsとは3つのユーザー中心のパフォーマンス指標です。ページの読み込みパフォーマンス、インタラクティブ性、視覚的安定性に関する実際のユーザー体験に焦点を当てています。これらの指標を理解し、最適化することで、優れたユーザー体験を提供することができます。 それぞれの指標について簡単に説明します。

Largest Contentful Paint (LCP)

LCP は、ウェブページの読み込みパフォーマンスを評価するための指標の1つです。ユーザーがページを訪れた際に、視覚的に最も大きなコンテンツ(画像やテキストブロックなど)が表示されるまでの時間を測定します。良好なLCPは2.5秒以下とされています。

Largest Contentful Paint

これまでは、初回ペイント後の読み込み体験をより正確に把握するために、First Meaningful Paint(FMP)Speed Index(SI) などのパフォーマンス指標が推奨されていました。しかし、これらの指標は計測が複雑で説明が難しく、結果にばらつきが生じやすいため、メインコンテンツがいつ正確に読み込まれたのかを特定することが困難でした。

そこで、ページのメインコンテンツが読み込まれたタイミングをより正確に測定するために、最も大きな要素がレンダリングされたタイミングを確認する方法としてLCPがCore Web Vitalsの指標に加えられました。

▼ LCPの要素になり得るもの

  • img要素(GIFやアニメーションPNGのようなアニメーションコンテンツには、最初のフレーム表示時間が使用されます。)
  • svg要素内のimage要素
  • video要素(ポスター画像のロード時間と動画の最初のフレーム表示時間のどちらか早い方を使用します。)
  • CSSurl()関数を使用して読み込まれた背景画像を持つ要素
  • テキスト・ノードまたは他のインライン・レベルのテキスト要素の子を含むブロック・レベル要素。

今後、調査が進むにつれて要素が追加される可能性があるそうです。

詳細はこちら

Cumulative Layout Shift (CLS)

CLS は、ウェブページの視覚的な安定性を評価するための指標の1つです。ユーザーがページを読み込んでいる間に発生する予期しないレイアウトの変動(シフト)の総合スコアを測定します。良好なCLSスコアは0.1以下とされています。

Cumulative Layout Shift

CLSは、ページのライフサイクル全体で発生したすべてのレイアウトシフトのスコアを合計して算出されます。各レイアウトシフトのスコアは、影響率と距離の割合の積になります。

レイアウトシフトスコア = 影響の割合 × 距離の割合

下記の図を例に、それぞれ影響率と距離の割合を説明します。

影響の割合は、そのフレームと前のフレームのすべての不安定な要素の可視領域を、ビューポートの合計領域の割合として組み合わせたものです。
1つのフレームでビューポートの半分を占有する要素があり、レイアウトシフト後に、要素はビューポートの高さの25%下方に移動します。赤い点線の長方形は、両方のフレームにおける要素の可視領域の結合を示しています。この場合、可視領域はビューポートの合計の75%であるため、影響の割合は0.75です。距離の割合は、不安定な要素が移動した際のビューポートからの相対距離を測定します。下方に25%移動したので、0.25になります。
この例では、影響の割合が0.75、距離の割合が0.25であるため、レイアウトシフトスコアは0.75 × 0.25 = 0.1875です。

当初、レイアウトシフトスコアは影響率のみに基づいて計算されていました。距離の割合は、大きな要素がわずかにシフトした場合に過度なペナルティが適用されないように導入されたそうです。

詳細はこちら

Interaction to Next Paint (INP)

INPは、ユーザーのインタラクション(例えばクリックやタップ)に対するページの全体的な応答性を評価するための指標です。200ミリ秒未満だとページの応答性が良好とされています。 INPの意図は、インタラクションの最終的な効果(ネットワークフェッチや他の非同期操作によるUIの更新など)をすべて測定することではなく、次の描画がブロックされている時間を測定することです。なので、時間のかかるインタラクションでは、最初の視覚的なフィードバックを素早く提示することが重要です。

Interaction to Next Paint

INPは、First Input Delay(FID)の後継指標で、どちらも応答性の指標ですが、FIDはページでの最初の操作の入力遅延のみを測定していました。INPは、入力遅延からイベントハンドラの実行時間、ブラウザが次のフレームをペイントするまでのページ上のすべてのインタラクションをモニタリングすることで、FIDを改善します。

詳細はこちら

その他のCore Web Vitalsに関わる重要な数値

前述したCore Web Vitals以外にも重要な指標がいくつかあるので、その指標を説明していきます。

First Contentful Paint (FCP)

FCPは、ユーザーがページに初めて移動してから、ページのコンテンツのいずれかの部分が画面上にレンダリングされるまでの時間を測定します。この指標の「コンテンツ」とは、テキスト、画像(背景画像を含む)、svg 要素、白色以外の canvas 要素を指します。

上記の画像の2番目のフレームまでの時間がFCPを指します。 LCPとは違って、読み込みのごく初期の段階しか捉えません。
ページにスプラッシュ画面や読み込みインジケーターが表示されている場合、それらの表示までを計測しており、実際に使用が可能になっているわけではなく、FCPが短いからといって、必ずしもユーザー体験が優れているとは言えません。

詳細はこちら

Total Blocking Time (TBT)

TBTは、FCP後にメインスレッドが入力に応答できないほど長くブロックされた合計時間を測定します。この時間が長いほど、ユーザーがページとインタラクションを取るまでの遅延が大きくなります。200ミリ秒未満が良好とされる値です。

詳細はこちら

Time to First Byte (TTFB)

TTFBは、ユーザーがリクエストを送信してからサーバーが最初のバイトをブラウザに返すまでの時間を測定します。下記の画像のstartTimeからresponseStartまでの経過時間になります。
これはクリティカルレンダリングパスより前の工程です。

HTMLの最初のリクエストはいくつかのステップがあり、各ステップにかかる時間を短縮することで、TTFBを短縮することができます。
ページの読み込みの速さに関しては、TTFBだけが注目すべき指標ではありませんが、TTFBが高いと、LCPやFCPなどの指標で指定された良好な値に到達するのが難しくなります。

詳細はこちら

Speed Index(SI)

Speed Indexは、Lighthouseレポートのパフォーマンスセクションで追跡される5つの指標のうちの1つです。ページの読み込み中にコンテンツが視覚的に表示される速度を測定します。スコアは3.4秒以下が良好とされています。
個人的には、その他の指標と比べると定義が少し曖昧に感じています。改善の指標としては、具体的な改善ポイントがあるその他の指標を目安にする方が良いかと思います。

詳細はこちら

パフォーマンス計測方法

すでに使用されている方も多いかと思いますが、普段使用しているパフォーマンスを計測する3つのツールの概要を説明します。

Google Search Console

ウェブパフォーマンスに関する主な指標では、モバイル・PCともに、低速(不良)、改善が必要、良好のステータス別にURLをレポートします。
このレポートは、実際のユーザーデータで測定される3つの指標(LCP、INP、CLS)に基づいています。レポート内のURLは、ユーザーエクスペリエンスが類似するページにグループ化されているため、個別のURLでは確認できません。

詳細はこちら

PageSpeed Insights

GoogleのPageSpeed Insightsでは、実際のページを読み込んで課題を検出します。 下記のページにアクセスして、分析するURLを入力するとパフォーマンスを計測できます。
https://pagespeed.web.dev/

Vercelを利用している場合は、Vercelが提供しているSpeed Insightsを利用することが可能です。それぞれのパスやルートごとに数値が算出可能で、特定の日付での絞り込みを行えるので利便性が高いです。

計測している指標や算出方法は下記のドキュメントに詳しく記載されていますので、ご参考ください。
Speed Insights Metrics

Google Chrome DevTools

開発者が頻繁に使用する検証ツールです。パフォーマンスの計測に使用される主なパネルについて説明します。

Lighthouse

LighthouseはPageSpeed Insightsと類似の機能を持ち、パフォーマンス以外にもアクセシビリティSEOの観点からの計測が可能です。NetworkパネルでブロッキングされたリソースやSourcesパネルのOverride機能でAPIレスポンスやCSSの書き換えを反映した状態で計測が可能です。
計測値は様々な要因で変動するため、複数回の結果の中央値を確認することを推奨します。

Lighthouse の概要  |  Chrome for Developers

Performance

Performanceパネルでは、ページの読み込みやユーザー操作に伴うイベントを詳細に分析できます。タイムラインの記録と再生により、ページロードや操作時の問題箇所を特定し、フレームごとのレンダリング時間やスクリプト実行時間を確認することでボトルネックを見つけることができます。また、メモリ使用状況の監視やJavaScriptのパフォーマンス分析も行えます。

パフォーマンス パネル: ウェブサイトのパフォーマンスを分析する  |  Chrome DevTools  |  Chrome for Developers

Network

Networkパネルでは、ネットワークを通じたリソースのタイムラインを詳細に確認できます。個別のリソースの詳細では、サーバーへのリクエスト時間やレスポンス時間をさらに細かく見ることができます。リクエスト数が多い場合は、カテゴリごとに絞り込みや名前でフィルターをかけることができ、操作性を高めて効率的に分析できます。

ネットワーク パネル: ネットワークの負荷とリソースを分析する  |  Chrome DevTools  |  Chrome for Developers

まとめ

今回の記事では、Webパフォーマンスの重要な指標とその指標の計測方法について紹介しました。パフォーマンス改善にはトレードオフが伴うため、エンジニアとして最適な実装を判断するためには、基礎知識をしっかりと身につけておくことが不可欠です。
フレームワークが進化し続ける中で、新機能を導入する際にはその機能が「何を改善し、ユーザー体験にどのような恩恵があるか、そしてそのトレードオフは何か」を正しく理解するための基盤として、今回の知識が役立ちます。
次回以降の記事では、計測した数値に基づいて仮説を立て、具体的な改善策を実施します。その過程でReactのパフォーマンスチューニングについても詳しく解説する予定ですので、お楽しみに!

▼ 参考文献
web.dev https://www.amazon.co.jp/Webフロントエンド-ハイパフォーマンス-チューニング-久保田-光則/dp/4774189677

おわりに

現在プレックスではソフトウェアエンジニアフロントエンドエンジニアを募集しています。 この記事を見て一緒にパフォーマンス改善を行いたい方がいればお気軽にご連絡をお願いいたします!

dev.plex.co.jp

BigQueryでリージョンだけ異なるテーブルを生成する方法

この記事は、 PLEX Advent Calendar 2024の15日目の記事です。

はじめに

こんにちは。コーポレートチームの石川です。

今回は BigQuery(以下、BQ)で異なるプロジェクトのデータセットを利用したい場合や、同一プロジェクト内で異なるリージョンにデータセットを作成したい場合の対応方法を記載します。

背景

社内プロジェクトの一部で、Heroku 上で Rails を動かしつつ、BQ のテーブルを利用し、EXTERNAL_QUERY で Cloud SQL を参照しているシステムがありました。

その際、Heroku・BQ・Cloud SQL のリージョンが異なり、本来2~3秒で終わるデータ取得処理が約16秒ほどかかる事象が発生しました。

そのため、リージョンを統一することで速度改善が見込めるかを検証する必要があり、今回の内容に至りました。

結論

BigQuery Data Transfer Service で解決!

機能に制限はあるものの、異なるプロジェクト間でデータセットを同期したり、同一プロジェクト内で異なるリージョンのデータセット同期することが可能です。さすが Google

BigQuery Data Transfer Service とは  |  Google Cloud

設定方法

1. データセットを生成したい BQ(転送先)にアクセスする

初めての場合、データ転送ページから「作成」を押すと BigQuery Data Transfer API を有効にするか確認されるため、有効にします。

2. 転送設定を行う

転送元・転送先の情報は以下の通りです。

プロジェクト情報

## 転送元
プロジェクト名: source-project-444601
リージョン: asia-northeast1
データセット: source
テーブル: user,order

## 転送先
プロジェクト名: destination-project-444601
リージョン: us-east4
データセット: destination
テーブル:  user,orderテーブルが同期される

3. データ転送ができたか確認

データ転送設定後、「今すぐ転送を実行」を選択します。何もしない場合は、cron 設定が実行されるまで転送されないので注意してください。

手動で実行後、指定した source データセットのテーブルが、異なるリージョンの状態で同期されれば成功です!

4. 同一プロジェクトでリージョンを変更する

次に、同一プロジェクト内でリージョンを変更するケースを説明します。この場合、転送元と転送先のデータセット名が異なる必要があるので注意してください。

プロジェクト情報

## 転送元(先ほどの転送元)
プロジェクト名: destination-project-444601
リージョン: us-east4
データセット: destination
テーブル:  user,orderテーブル

## 転送先
プロジェクト名: destination-project-444601
リージョン: asia-northeast1
データセット: destination_asia
テーブル: user,orderテーブルが同期される

5. データ転送ができたか確認

転送が正しく行われたかを確認します。

まとめ

以上、BQ のデータ転送に関する対応方法を紹介しました。

他にも様々な方法があるかもしれませんが、既存のデータセットを汚さず、GUI 上で操作できるこの方法が一番手軽ではないかと感じました!

さいごに

現在プレックスではソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーを募集しています。

とても働きやすい環境なので、一緒に働いてみたいと思った方がいましたら、是非ご連絡をお待ちしています!

dev.plex.co.jp

自社のデータ基盤を支えるAirbyteの良いところ

この記事は、 PLEX Advent Calendar 2024 の14日目の記事です。

こんにちは、株式会社プレックスのコーポレートチームの金山です。

この記事では「Airbyte」というデータ基盤で使われるツールについて紹介したいと思います。

Airbyteとは

Airbyteとは、オープンソースで公開されているデータ同期ツールです。

airbyte.com

Airbyteは、様々なデータソースからデータを抽出(Extract)し、データウェアハウスやデータレイクなどにロード(Load)するために使用されます。

例えば、ウェブアプリで使用しているPostgreSQLのデータを、分析用のBigQueryに同期するといったことが可能です。 今回はこのAirbyteを実際に使ってみて感じた良いところを紹介したいと思います。

豊富なコネクタ

Airbyteのコネクタとは、特定のデータソースまたは転送先と接続してデータをやり取りするためのモジュールです。

これらのコネクタを利用して、様々なデータソースからデータを抽出し、指定した転送先にロードします。

代表的なコネクタは以下の通りです。

以下のページで対応しているコネクタの一覧を確認できます。

airbyte.com

ただし、日本製品向けのコネクタが少ないのがデメリットです。

コネクタが提供されていない場合は、独自のカスタムコネクタを開発することで対応可能です。

わかりやすい管理画面

Airbyteの管理画面は、必要最低限の機能にまとめられており、直感的で使いやすいのが特徴です。

データ同期までのステップが非常にわかりやすく、初めて触る人でも簡単に操作できます。簡単な設定とボタンひとつで同期できたときは感動しました。

デモサイトが公開されているので、実際のUIや操作感を確認してみてください。

airbyte.com

選べる実行環境

Airbyteは公式提供のクラウドサービスもあります。

  • フルマネージドサービス
  • オートスケーリング対応
  • 2週間無料トライアル
  • 従量課金制(データ同期量に応じて課金される)

インフラ設定無しですぐに使えるのは魅力的ですが、料金が高いです。

なので、基本的には自社で用意したインフラにオープンソース版のAirbyteをインストールして利用することになると思います。

シンプルな構成ならDocker Composeでデプロイ、スケーラブル対応ならKubernetesでデプロイします。

無料でクラウド版を使ってみた

Airbyte クラウドを利用して実際にNotionのデータをBigQueryに同期してみました。

まずはデータソースを設定します。

データソースのコネクタ一覧からNotionを選択。設定ページに移動するので、認証方法を選択してデータソースを設定します。

続いて転送先を設定します。

転送先のコネクタ一覧からBigQueryを選択。設定ページに移動するので、プロジェクトIDやデータセットIDを定義して転送先を設定します。

転送先の設定が完了するとデータソースのスキーマの取得が始まります。 スキーマの取得が終わったら転送先に同期したいスキーマを選択します。

最後にコネクションの設定です。 同期頻度(間隔かcronか)や同期モード(差分更新か全件更新か)を設定します。

コネクションの設定が完了すると同期が始まります。

同期が完了しました。

BigQueryに同期されたことが確認できました。

まとめ

以上、Airbyteについてご紹介しました。

データ同期ツールは他にもFivetranやStitchなどの選択肢がありますが、Airbyteはオープンソースで自由度が高く、自社環境でコスト効率よく運用したい方におすすめです。

この記事を読んでAirbyteに興味を持ってもらえたら幸いです。