BigQueryの新機能を使ってクエリ料金を1/1000にする

こんにちは、エンジニアの大迫です。

Kaizen Platformでは、以前からGoogle BigQueryを利用して、ウェブサイトの行動ログや広告の配信レポートなど様々なデータを保存・活用できるような仕組みを整え、お客様のウェブサイトや広告クリエイティブの改善に取り組んできました。特にここ最近では、非エンジニア向けにBigQueryやSQLの社内勉強会が行われたり、 @ikedayu によりProduction以外のメンバーでも気軽にデータ分析ができる仕組みが作られたりして全社的にBigQueryの利用が広がっています。

その一方で、データを活用できる人が増えた結果として、BigQueryのクエリ料金も増えていく傾向になっています。 せっかくエンジニア以外でも分析できる仕組みがあるのに、クエリコストが気になってクエリ書くのが怖くなってしまってはもったいないので、こちらの記事にあるように @ikedayu によってBigQueryコストの可視化をしながら必要のないクエリが定期的に実行されてないか、不必要に多くのデータをスキャンしていないかといった傾向を把握して改善する仕組みを回し始めたりしています。

developer.kaizenplatform.com

そんなある日、BigQueryのドキュメントを読んでいたところ、 Clustered Tableというβ機能の存在を知りました。これを活用するとKaizen PlatformのBigQueryの利用料の大きな割合を占めるクエリ料金を削減できる可能性があったので調べてみたところ、この機能を適切に活用すると、クエリにもよるものの、クエリ料金を大きく削減できそうなことが分かったので、調べた結果を簡単にまとめたいと思います。

続きを読む

Railsで不要なテーブルと古いmigrationファイルを削除する

はじめまして、ハートレイルズの境 (@kazsakai) です。

色々あって今は長野県の伊那という、地理的には日本列島の中心らへんだけどあらゆる大都市から満遍なく遠い片田舎に暮らしています。(ちなみにアニメ聖地巡礼発祥の地だそうで)

Kaizen Platformさんの社員ではなくパートナーという立場ではありますが、ほぼ最初期くらいから開発に関わっているエンジニアの一人として、今回こちらのブログにお邪魔させていただきます。

Rails の不要テーブルと migration ファイルを整理したい

Kaizen Platformさんのプロダクトは日々着実に拡大を続けていて、githubの社内リポジトリ数も今や200を超えていますが、そんなKaizenのプロダクトも最初期には単一のRailsリポジトリからスタートしました。

最初期のプロダクト名「planBCD」にちなんだそのRailsリポジトリplanbcdは、少しずつリファクタリングされながら今でもサービスの中核に残り続けています。

しかし、このplanbcdはさすがにもう5年を超える歳月を経たリポジトリだけあって、その間に内部のデータ構造も様々な変遷を経ていて、MySQLのテーブル周りもかなり散らかった状態になっていました。

具体的には、

  • もう使ってないはずのテーブルがdb/schema.rbに載っている
  • 新しくDBを作ってrake db:migrateするとmigrationファイル群の色んなところでエラーが出て動かない

等々の課題があって、致命的というわけではないけれども放っておきたくもない状態にありました。

前々から何とかしたいとは思っていたのですが、最近新しい機能をリリースして一息ついたタイミングでこの問題を解消する機会に恵まれましたので、今回はどうやってこれらを解消したのかを紹介したいと思います。

不要なテーブルを削除する

解消前の時点でplanbcdにはMySQLのテーブルが165個ありましたが、このうち現在はもう使われていなさそうなものが多数存在していました。 これらが残っていてもコードやDBを眺める際にノイズにしかならないので、使われていないものは削除することにしました。

Rails で使っていないテーブルを探す

まず本当に使われていないかどうかを調べるために、ちょっとしたスクリプトを走らせてみます。

Railsで普通にActiveRecordを使ってテーブルを作成・操作すると、レコードのINSERTUPDATEのときに自動的にupdated_atカラムにその時点の日付が入ります。 (update_columnupdate_allメソッドを使うと入らないのですが、Kaizenさんでこれらのやや行儀の悪いメソッドを使う人は僕以外あまりいないので、大体このカラムが信用できそうでした)

これを利用して、各テーブルの最終更新日時と、ついでにレコード数を調べてみます。

railsコンソール上で、

ActiveRecord::Base.connection.tables.map do |table_name|
  count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM #{table_name}")
  column_names = ActiveRecord::Base.connection.columns(table_name).map(&:name)
  time_column = column_names.include?('updated_at') ? :updated_at : nil
  time_column ||= column_names.include?('created_at') ? :created_at : nil
  last_updated_at = nil
  if time_column && count > 0
    sql = "SELECT #{time_column} FROM #{table_name} ORDER BY #{time_column} DESC LIMIT 1"
    last_updated_at = ActiveRecord::Base.connection.select_value(sql)
  end
  { 
    table_name: table_name,
    count: count,
    time_column: time_column,
    last_updated_at: last_updated_at,
  }
end

のようなスクリプトを走らせて、

=> [{:table_name=>"account_statements", :count=>22323, :time_column=>:updated_at, :last_updated_at=>2016-03-02 03:02:34 UTC},
    {:table_name=>"acct_areas", :count=>2, :time_column=>:updated_at, :last_updated_at=>2015-05-14 00:40:46 UTC},
    {:table_name=>"acct_contact_roles", :count=>146, :time_column=>:updated_at, :last_updated_at=>2015-12-04 23:36:44 UTC},
    ...]

のようにテーブル名、レコード数、最終更新日時を取得してみました。

ここで全テーブル名の取得等にActiveRecord::Base.connectionを使っていますが、ここにはRailsのコンソールやスクリプトで生 SQL を叩いたり (executeselect_all) テーブルのスキーマを変えたり (add_columncreate_index) できるメソッドが詰まっているので、覚えておくとちょっとしたときに使えて便利です。

こうして取得したテーブル一覧を眺めて、本当に使われていないかどうかの判断の材料とします。 例えばレコード数は大量にあるのに最終更新日時が数年前で止まっているテーブルなどは高確率でもう使われていないと思っていいでしょう。

ただ実際にはこれだけで判断するわけではなく、Railsのコードと、あとテーブルの役割や歴史的経緯から見てこれはまだ要るはず、これはもう要らないはずと総合的に判断していきました。

こうした結果、66個の消してもよさそうなテーブルのリストができあがりました。

いきなり消さずにリネームして様子を見る

165個中66個のテーブルを消せそうでしたが、いきなりこの数のテーブルを消すのは怖いので、削除の前にもう一段階挟むことにします。

まず、こんな風に削除予定のテーブルを別の名前にリネームするmigrationファイルを用意しました。

class RenameLegacyTables < ActiveRecord::Migration
  def change
    rename_table :acct_areas,         :zzz_acct_areas
    rename_table :acct_contact_roles, :zzz_acct_contact_roles
    # ...略...
  end
end

もしテーブルを消して問題が起こるようならリネームするだけでも同じ問題が起こるはずなので、こうしてリネームして様子を見ることにします。

今回、リネーム後の名前は分かりやすければなんでも良かったのでzzz_をつけてみました。 ただrename_tableではテーブルと同時にそのテーブルに張られたインデックスもリネームされて、MySQLの名前の上限64字を超えるエラーが幾つか出てしまったので、そのあたりは別途インデックスの方もリネームしていました。 ここはもうちょっと短いプレフィックスか、名前が長くならないような命名の方が良かったかもしれません。

ともかくこうして削除予定のテーブルをリネーム後、自動テストの全パスとステージング環境での確認を経て、本番環境へと反映します。 リネームした状態で本番環境でしばらく運用して、問題が起こらなければテーブル削除する予定でした。

が、やはり想定外の問題の一つや二つは起こるもので、Chartioで書いていたグラフの一つが動かなくなるという問題が発生してしまいました。 Chartioのクエリで今回削除しようとしていたテーブルの幾つかが参照されていて、そこに気づかないままリネームしてしまったのです。

幸い、Kaizen社内で参照する指標のためのグラフでユーザー影響は無かったのですが、リネームだけにしておいたためすぐ復旧することができました。 テーブル削除していたら、バックアップからの復旧となりもう少し手数がかかっていたと思います。

こうして一部のテーブルは元に戻したものの、以降一週間ほど様子を見ても特に問題は起こらず、残りのテーブルは本当に消してもよさそうなので、実際に削除へと進みます。

テーブルをバックアップを取って削除する

本当に消してもよさそうなテーブルが分かってきたので、今度はそれらをこんなmigrationファイルで削除します。

class DropLegacyTables < ActiveRecord::Migration
  def backup_table(name, timestamp)
    local_d = "#{Rails.root}/dump"
    Dir.mkdir(local_d) unless File.directory? local_d
    dumpfile = "#{local_d}/#{timestamp}-#{Rails.env}-#{name}.dump.gz"

    options = connection.raw_connection.query_options.slice(:database, :username, :password, :host, :socket)
    cmds = ['mysqldump',
            "-u #{options[:username]}",
            "--password='#{options[:password]}'",
            options[:socket].present? ? "-S #{options[:socket]}" : "-h #{options[:host]}",
            options[:database],
            name,
            "| gzip > #{dumpfile}"]

    cmd = cmds.join(' ')
    puts "executing: #{cmd.gsub(/(--password)=[^ ]*/, '\1=xxxx')}"
    system(cmd) or raise "command failed!"
  end

  def up
    legacy_tables = [
      :zzz_acct_areas,
      :acct_contact_roles,
      # ...略...
    ]
    legacy_tables.select! do |table_name|
      table_exists?(table_name)
    end

    # backup tables by mysqldump
    timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
    legacy_tables.each do |table_name|
      backup_table table_name, timestamp
    end

    # rename foreign keys before drop tables
    legacy_tables.each do |table_name|
      foreign_keys(table_name).each do |fk|
        remove_foreign_key table_name, name: fk.name
      end
    end

    # then drop tables
    legacy_tables.each do |table_name|
      drop_table table_name
    end
  end

  def down
    puts "rollback of DropLegacyTables does't restore legacy tables."
  end
end

ここでは念のため削除前にテーブル毎にmysqldumpでバックアップをしています。

また、他のテーブルと外部キー制約のリレーションがあるテーブルもあるので、それら外部キー制約は先にremove_foreign_keyで削除しています。

最後にdrop_tableでテーブルを削除することで、不要テーブルの削減は完了です。

migration ファイルを整理する

さて、不要テーブルの削除は終わったものの、db/migrate/以下には削除済みのテーブルの分を含めて大量のmigrationファイルが残っていました。

この時点でおよそ500個弱のmigrationファイルがあって、単に数が多いだけならそれほど害は無いのですが、

  • 新しくDBを作ってrake db:migrateするとmigrationファイル群の色んなところでエラーが出て動かない

という問題があって、新しく参加したエンジニアが手元に開発環境を構築するときには、他の既に動いている環境のDBからmysqldumpしてもらってくるしかないという状況でした。

このエラーの原因のほとんどは単純で、例えばmigrationファイルに

class AddPricingPlanIdToUsers < ActiveRecord::Migration
  def up
    add_column :users, :pricing_plan_id, :integer

    plan_enterprise = PricingPlan.where(label: 'enterprise_jp_v1').first
    User.where(role: User::Role::STANDARD).find_each do |user|
      user.pricing_plan_id = plan_enterprise.id
      user.save!
    end
  end

  def down
    remove_column :users, :pricing_plan_id
  end
end

のように、カラムを追加したついでにモデルクラスを使って値を設定しているところでひっかかっていました。 これを書いた当時は動いていたはずですが、その後の経緯でクラスそのものが無くなったりして動かないコードになってしまったというわけです。

migrationファイルで将来変わりうるクラスや定数を使うのは悪いコードで、本当は

  def up
    add_column :users, :pricing_plan_id, :integer

    plan_enterprise_id = connection.select_value('SELECT id FROM pricing_plans WHERE label = "enterprise_jp_v1"')
    role = 1  # User::Role::STANDARD
    execute <<-SQL
      UPDATE users SET users.pricing_plan_id = #{plan_enterprise_id} WHERE role = #{role}
    SQL
  end

のように生のSQLで設定するようなコードにすべきでした。

とは言え既にある500個近いmigrationファイルのうち、現在動かないコードが何箇所もあって、最初から全て通るように修正するのはそれなりに手間がかかりそうです。

そもそも古いmigrationファイルのコードを見ることは経験上ほとんどなく、残っていても考古学的な価値くらいしかないので、手間をかけて修正するほどでもないと考えて、今回これらを一気に削除することにしました。

削除すると言ってもdb:migrateしたときにちゃんとDBが最新の状態になるようにしたいので、削除する代わりに最新のdb/schema.rb相当のmigrationファイルを用意することにします。

まずは本番のサーバーでrake db:schema:dumpを実行して最新のdb/schema.rbを取得します。

ActiveRecord::Schema.define(version: 20180626172837) do

  create_table "active_admin_comments", force: :cascade do |t|
    t.string   "resource_id",   limit: 255,   null: false
    t.string   "resource_type", limit: 255,   null: false
    # ...略...

このdefine内が最新のテーブルのスキーマ情報になっていて、このコードはそのままmigrationファイルで使えるので、これを新たに作ったmigrationファイルの中にコピーします。

class SchemaSnapshot < ActiveRecord::Migration
  def up
    return if table_exists? :active_admin_comments

    create_table "active_admin_comments", force: :cascade do |t|
      t.string   "resource_id",   limit: 255,   null: false
      t.string   "resource_type", limit: 255,   null: false
      # ...略...

ここで最初にテーブルが既に存在したらスキップする処理を入れておきます。

この新しいmigrationファイルは新たにDB作成してdb:migrateしたときのためのもので、既存のDBがある環境ではここでスキップさせるようにします。

これで新しい環境でこのmigrationファイルが実行されるとdb/schema.rb相当の状態になるところまで進みますが、db/schema.rbには載らないものの動作に必要な情報が古いmigrationファイルに残っている場合があります。

具体的には、絵文字対応のために一部カラムをutf8mb4に変更していたり、必要な初期レコードをテーブルに投入したり、ストアドファンクションを定義したりといったコードです。

これらを古いmigrationファイルから洗い出して、新しいmigrationファイルの中に

    # set utf8mb4 columns
    execute "ALTER TABLE users CHANGE `username` `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
    execute "ALTER TABLE users CHANGE `comment` `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
    # ...略...

    # init membership_roles
    execute <<-SQL
      INSERT INTO membership_roles
        (id, name, ancestry, receive_emails, created_at, updated_at, is_inheritable, is_root_organization_only, type)
      VALUES
        (1, 'owner',     NULL,  TRUE,  NOW(), NOW(), TRUE,  TRUE,  'MembershipRole::Organization'),
        (2, 'admin',     '1',   TRUE,  NOW(), NOW(), FALSE, FALSE, 'MembershipRole::Organization'),
        (3, 'member',    '1/2', FALSE, NOW(), NOW(), FALSE, FALSE, 'MembershipRole::Organization')
    SQL    
    # ...略...

のように手作業で移植して、ようやく一つの巨大なmigrationファイルが完成しました。

これを古いmigrationファイルを全削除した代わりに配置すれば完了です。

ちなみにこの古いmigrationファイルを捨ててdb/schema.rbから新しいmigrationファイルを作るのはおそらくsquasher gemでもできそうだったのですが、それ自体は手作業でもすぐできるのと、db/schema.rbに載らない情報の移植はどのみち必要だったので、今回はこうして手作業で済ませました。 もっと素直な内容のmigrationであればsquasher gemでまとめた方が簡単かもしれません。

最後に

KaizenのRailsリポジトリの一つでは今回こんな風に古いテーブルやmigrationファイルの整理を行っていました。 約3分の1のテーブルと大量のmigrationファイルが消えたので、随分とすっきりしたように思います。

動作に支障は無いけれども古くて整理した方がよいものはテーブル周りに限らず放置しがちで、蓄積していくと徐々に、新しい何かを作る際に開発しづらい環境になってしまいます。 もちろん新機能の開発も重要なのですが、その一方でこうした過去の負債もバランス良く清算して継続的に開発しやすい環境を作ることも大事です。

実際のところ他社さんではこうしたところにはなかなか工数を取れないのですが、その点、こうやって過去の負債を解消するための工数もしっかり確保できるのは、Kaizen Platformさんの素敵なところの一つと思います。

BigQueryのコスト可視化ダッシュボードをGoogle Apps Script/Google Sheets/Google Sitesを使ってお手軽に作る

4月からKaizen Platformにジョインしたアプリケーションエンジニアのikedayu@つくばです。業務ではデータ解析や解析基盤構築を主に行っています。

Kaizen Platformでは、許可を求めるな、謝罪せよの文化が深く浸透していて、エンジニア一人ひとりの裁量が大きいのが特徴です。自分は、その裁量でしばしば自由研究的に身近な問題に挑戦していて、今回は、その1つについて紹介させてもらいます。

TL;DR

  • ↓のようなBigQueryコストを可視化するダッシュボードを作ります
  • 「日付ごとのコストの推移」と「IAMユーザーごとのコスト」を可視化することでコスト削減の方策が立てやすくなります
  • G Suiteだけで完結するので、(G Suiteを使っている企業であれば) すぐに導入可能です

f:id:kaizenplatform:20180816121446p:plain

課題: BigQueryのコストはざっくりとしかわからない

Kaizen Platformでは他のたくさんの会社と同じように様々な用途でBigQueryを使っています。 サービスで利用したり、ChartioなどのBIツールや、自分のようなデータ解析者が直接BigQueryを叩くなど、エンジニア/非エンジニア、人類/非人類に関わらず使用方法は多岐に渡ります。 こういった使い方をしていて、会社の規模が大きくなってくると、当然「あれ、今月のBigQuery料金高すぎ…?」という状態になったりします。しかし、GCPのコンソールだと誰が各日でどのくらい使っているのかがわからない、という課題がありました。

f:id:kaizenplatform:20180816163504p:plain

「課金警察」の導入

その対策の1つとしてKaizen Platformでは↓のGunosyさんがやっているような課金警察を導入しており、高コストなQueryは検知することができていました。 data.gunosy.io

ただ課金警察は威力のあるワンパンチを検知することには向いているのですが、毎日コツコツ積み重なる後から効いてくるジャブを検知するのは難しいという課題がありました。

「費用管理」機能の前に

GCPもアカウントごとにコストを管理できる機能を提供しているのですが、アカウントごとに設定するのが大変&どの程度の閾値を設定すべきか検討をつけるのが難しいという問題があります。

費用管理  |  BigQuery  |  Google Cloud

ということで、とりあえず可視化してみることにしました。

G Suiteを使おう

可視化にあたって色々な選択肢がありましたが、今回は自由研究的な性質が強かったので、予算的な面や運用的な面を考えなくて良いG Suiteを選択しました。(ちなみにKaizen Platform的には前述したように裁量が大きいので「サーバー立てて運用したいです」という話でも余裕でOKだと思いますが、今回は単純に自分が考えたくなかった、という話です。笑)

構成はこんな感じです。コア部分はGoogle Apps ScriptとGoogle Sheetsで、Google Sitesはグラフを埋め込んでいるだけになります。

f:id:kaizenplatform:20180816163433p:plain

この「1. BQ(BigQuery) APIでJob取得」「2. SS(Spreadsheet) APIで書き出し」「3.グラフを作成/埋め込み」を簡単に解説させていただきます。

[Google Apps Script] BigQuery APIでJob取得

Google Apps ScriptにはBuilt-inでBigQuery APIにアクセスするObjectが用意されているので、認証を全く考えることなくAPIに楽々アクセスできます。 BigQuery Service  |  Apps Script  |  Google Developers

APIアクセス部分はこんな感じで書いています。

utility.getJobsFromBqApi = function(maxResults, minCreationTime, nextPageToken){
  maxResults = maxResults || 1000
  minCreationTime = minCreationTime || null
  nextPageToken = nextPageToken || null

  const projectId = 'myProjectId'
  var options = {
   'maxResults': maxResults,
   'stateFilter': 'done',
   'projection': 'full',
   'allUsers': true,
   'minCreationTime': minCreationTime,
   'pageToken': nextPageToken
  }
  
  const response = BigQuery.Jobs.list(projectId, options)

  return response
}

注意点: 一度にJobを全て取得/処理できない

Google Apps Scriptは一度に実行できるJobの実行時間の上限が5分になっています。このためBigQueryの使い方にもよりますが、サービスやBIツールでの利用がある場合は一度にできないケースが多いと思います。このため、 データ管理用のスプレッドシートに state シートを作成し、下記をそれぞれ保存しておく作りになっています。

  • tmpLatestTime: 全件取得できなかったときに次回 minCreationTime パラメータとして使うUnixTimestamp
  • nextPagetoken: 全件取得できなかったときに次回 pageToken パラメータとして使うtoken

f:id:kaizenplatform:20180816133417p:plain

ちなみにlatestTimeは取得したデータの最新日時(UnixTimestamp)を保存しています。

claspでコード管理

今まではGoogle Apps Scriptをコード管理するのはGoogleDrive上で行わなければならず、Google Apps Script APIを使っていい感じにしてくれるthird party libraryはあったのですが、今年、公式CLIの clasp が発表されたので最近書くGoogle Apps Scriptコードは全てこれで管理してます。動作が不安定な時もありますが、継続的に開発がされていてどんどん修正されていますし、大体満足して使ってます。

github.com

[Google Apps Script] スプレッドシートにデータ書き出し

BigQuery APIから取得したJobデータをスプレッドシートに書き出します。要所要所の実装だけ切り出すとこんな感じになります。

var SheetManager = function(){
  this.thisMonth = Utilities.formatDate(new Date(), "UTC", "YYYYMM")
  this.spreadSheet = SpreadsheetApp.openById(SSID)
  this.priceSheet = this.spreadSheet.getSheetByName(this.thisMonth)
}

SheetManager.prototype.setPrices = function(prices){
  priceArr = this.convertPricesToArr(prices)
  this.priceSheet.getRange(2, 1, priceArr.length, priceArr[0].length).setValues(priceArr);
}

SheetManager.prototype.convertPricesToArr = function (prices){
  var priceArr = []

  Object.keys(prices).forEach(function(date) {
    var emailPrice = Object.keys(prices[date]).forEach(function(email) {
      priceArr.push([date, email, prices[date][email]]);
    });
  });
  // Sortしてから出力
  priceArr.sort(utility.sortFunction)
  return priceArr
}

注意点: スプレッドシートのセルの上限は40000

最初は取得したJob情報をスプレッドシートに書き出していたのですが、すぐにエラーが発生し始めました。これはスプレッドシートのセルの数に40000という上限があったためです。このため取得の度にアグリゲーションを行っています。 コードを抜粋して紹介すると下のような感じです。アグリゲーションに関してはObjectを使った愚直な実装をしています。またGoogle SheetsとGoogle Apps ScriptでTimeZoneが違うので Utilities.formatDate を使うところもポイントです。

var CostManager = function(){
  this.sheetManager = new SheetManager()
}

CostManager.prototype.getCurrentPrices = function(){
  this.prices = this.sheetManager.getCurrentPrices()
}

CostManager.prototype.updateJobs = function(){
  var that = this
  this.jobs.forEach(function(job){
    const stats = job['statistics']
    const totalBytes = stats['query'] ? stats['query']['totalBytesBilled'] : 0
    const timeStamp = parseInt(stats['creationTime'])
    const date = Utilities.formatDate(new Date(timeStamp + 1000*60*60*13), "UTC", "YYYY-MM-DD")
    const teraBytes = parseInt(totalBytes, 10)/Math.pow(1000, 4)
    const price = teraBytes * 5
    const email = job['user_email']
    
    // もし金額が発生していればpricesをupdate
    if(price > 0){
      if(!that.prices[date]) { that.prices[date] = {} }
      if(!that.prices[date][email]){ that.prices[date][email] = 0 }
      that.prices[date][email] += price
    }
  })
}

SheetManager.prototype.getCurrentPrices = function(){
  data = this.priceSheet.getDataRange().getValues().slice(1)
  const prices = {}
  data.forEach(function(d){
    var date = Utilities.formatDate(d[0], "JST", "YYYY-MM-DD")
    var email = d[1]
    var price = d[2]

    if(!prices[date]) {prices[date] = {}}
    prices[date][email] = price
  })

  return prices
}

これで無事Jobデータを書き出すことが出来ました。

[Google Sheets/Google Sites] グラフを作成/埋め込み

もうここまで来れば、あとはGoogle SheetsとGoogle Sitesをいじっていくのみになります。 ダッシュボードに表示させるデータはある程度絞りたいので今回は当月分の表示をさせることにします。 最初は月ごとにGoogle Sitesのグラフを切り替える方法について悩んだのですが、これが一番お手軽かなと思っています。まず先ほど紹介した「state」シートに対象月(今月)を入れるようGoogle Apps Scriptで書きます。

f:id:kaizenplatform:20180816142240p:plain

次に「current」シートを作り、INDIRECT 関数で対象月のデータを読み込んできます。(月ごとにスプレッドシートを作成してデータを書き込んでいます) INDIRECT 関数は初めて使ったのですが、文字列をevalしてくれるような関数で便利でした。

INDIRECT - ドキュメント エディタ ヘルプ

読み込んできたら、「current」シートでグラフを作成します。 こうすることによって、「state」シートの対象月を書き換えるだけで、データ及びグラフが対象月のデータに切り替わります。

f:id:kaizenplatform:20180816142056p:plain

あとは、Google Sitesのグラフ埋め込み機能を使うだけです。

f:id:kaizenplatform:20180816144240p:plain

これで月ごとに自動で切り替わるダッシュボードができました。めでたしめでたしです!

f:id:kaizenplatform:20180816121446p:plain

解決/まとめ/補足

Google Apps Script/Google Sheets/Google Sitesを使って、アカウントごと/日付ごとのBigQueryコストを可視化できるダッシュボードを作成できました。

可視化してわかったこと

社内で公開してみたところ、課金警察だけではわからなかった傾向や、利用料の多いアカウントが判明しました。また、アカウント管理の面でも、割とざっくりとアカウントを分けていたので、今後SRE中心に整理していこう、ということになり、コスト削減の流れに貢献することができて良かったです!

ちなみにプロトタイプはPythonで作りました。

最初はJupyter&Matplotlibでグラフをさっと作ってみて完成形のイメージを固めました。プロトタイプ -> 本番プロダクト作成、という流れは定番ですが、今回も完成形のイメージに関しては手戻りがなかったのでとても有用だな、と改めて感じました。

f:id:kaizenplatform:20180816150411p:plain

反省

最初、お手軽に作ろうと考えていたのですが、作ってみたら意外と考えることが多くて、あんまりお手軽じゃなくなってしまったな、と思っています。笑 この記事でも全ての手順を紹介しきれなかったので、もし不明点等ありましたら、Twitterなどで質問していただければ幸いですmm

最後に

Kaizen Platformは、今回のような自由研究的なことにもどんどん挑戦させてもらえる面白い環境なので、またどんどん挑戦していきたいと思ってます。そんな面白い環境で働いてみたい方は採用ページを是非チェックしてみてください!ではでは。

Kaizen Platform採用サイト

Datadog APMを導入してRailsアプリケーションのボトルネックを調べる

はじめまして。
7月にApplication Engineerとして入社した徳田 (id:hazeblog) です。
この記事では、先日社内に導入したDatadogのAPM(Application Performance Monitoring)機能と、APMを使ったアプリケーションの調査の様子を紹介します。

続きを読む

リモートワークと心理的安全性と雑談、あるいは小咄 (こばなし) の話

Kaizen Platform で Product Manager / Engineering Manager をしている @takus です。

Kaizen Platform は、誰もが好きな時に好きな場所でその人ならではの才能を発揮しながら働ける、そのような 「21 世紀の新しい雇用と働き方の創出」というビジョンを実現しようと、日々努力を重ねています。そのため、私たちの会社自体が「こんな働き方ができたらいいのに」という理想を追求する実験台となるべく、創業当初からリモートワークにも積極的に取り組んでいます。

リモートワーク自体、今では特に珍しい働き方ではなくなってきてると思いますが、実際にリモートワークをやってみると、様々な困難にぶち当たった経験のある方は多いのではないでしょうか? 長年リモートワークをやっている弊社でも、まだまだベストプラクティスを模索しながら、ちょっとずつ改善を続けている状況です。

その中でも続けていてよい仕組みだと認識してることの 1 つに、雑談する機会を意図的に作る ということがあります。本投稿では、このリモートワークにおける雑談に焦点をあて、その雑談の機会を作る小咄という仕組みについて紹介します。

TL;DR

  • 良いチームには良い関係性が不可欠、そのために雑談は重要
  • リモートワークが多いチームでは雑談する機会を仕組みとして担保している
  • Kaizen メンバーの小咄は普通に面白い
続きを読む