Metabaseのグラフをslackへ通知するbotをServerless Framework + Puppeteerで作ってみた

f:id:kaminashi-developer:20201012002731p:plain こんにちは。株式会社KAMINASHIでPMをやっている@gtongy1です。
みなさんはMetabaseをご存知ですか?
見た目の良さによる直感的なダッシュボードの構築、クエリ記述時の補完機能、グラフの種類数の豊富さ、ツール自体は無料で使える等便利なBIダッシュボードツールです。
カミナシでもMetabaseをフルに活用して、日々分析時に利用しています。
そんなMetabaseですが唯一欠点があって、それはslackとの連携が難しい所です。
slackへ通知を行う時にグラフで表現できるのはまだ棒グラフだけとか、マルチバイトの文字列は文字化けしてしまうとか、実運用に回す時にはちょっと辛いなと感じるところがあります。
そこで、今回Metabaseのグラフをslackへ通知するbotをServerless Framework + Puppeteerで作ってみました。
最終的に以下のようにslack上でMetabaseのグラフを通知することが出来るようになります。

slack-notified
slackへの通知後の画面

作成したコード

今回作成したbotリポジトリです。

github.com

構成

f:id:kaminashi-developer:20201011001751p:plain
構成図

  • Serverless Frameworkを用いてAWS Lambdaの構成管理
  • AWS Lambda上でPuppeteerを起動し、Metabaseのダッシュボードに対してスクリーンショットを撮る
  • 撮影後の写真をslackへ投稿
  • CloudWatch Eventsにより、Lambda関数を定期実行

上記の流れで、今回のbotを実現させています。

Serverless Frameworkを用いてAWS Lambdaの構成管理

Serverless FrameworkはServerlessアプリの構成管理、デプロイを行うツールです。
以下のコマンド実行により、初期プロジェクトを作成出来ます。

$ npm install -g serverless
$ serverless create -t aws-nodejs-typescript --name project-name -p /path/to/project-name

一度作成してしまえば後は、serverless invoke local -f funcNameで関数を実行出来るようになります。
localでのLambda関数の実行だったり、構成管理のデプロイだったりコマンドにうまく吸収されていて、取っ掛かりのハードルが下がるのが良いですね。

また今回の構成では、CloudWatch Eventで平日に定期的に通知を行うような関数を作成しています。

import type { Serverless } from 'serverless/aws'

const serverlessConfiguration: Serverless = {
  // ...
  functions: {
    metabaseSlackNotify: {
      handler: 'handler.metabaseSlackNotify',
      events: [
        {
          schedule: 'cron(0 0 ? * MON-FRI *)'
        }
      ]
    }
  }
}
import { ScheduledHandler } from 'aws-lambda'
export const metabaseSlackNotify: ScheduledHandler = async () => {}

cron形式の記述によって平日に通知を行う設定を追加しています。
handler側では、特にイベントを引数から受け取ることはなく、ただ純粋に関数を実行するような形となっています。

MetabaseのダッシュボードURLの取得方法

Metabaseでは、Embeddingという機能が存在します。

www.metabase.com

この機能を利用することにより、作成したダッシュボードをアプリケーション内に埋め込むためのURLが発行することが出来ます。

このURLへのリンク先には、ダッシュボード全体の画面が表示されます。
表示されたブラウザの全体をスクリーンショットを撮り、この画像をslackに通知することで、今回のbotを実現させています。
ダッシュボードを公開し埋め込みのURLを取得するためには、管理者画面側からとダッシュボード画面側の両方から設定を行う必要があります。
詳細はマニュアルを確認ください。

metabase-setting-admin
管理者画面の設定
ダッシュボード共有設定
ダッシュボードの公開

設定を行った後に、

const payload = {
    resource: { dashboard: dashboard.id },
    params: {},
    exp: Math.round(Date.now() / 1000) + 10 * 60
}
const token = jwt.sign(payload, process.env.METABASE_SECRET_KEY)
const embedUrl = process.env.METABASE_SITE_URL + '/embed/dashboard/' + token + '#bordered=true&titled=true'

のような形で認証後のtokenを取得し、URL経由でダッシュボードを表示することが可能となります。

Puppeteerでスクリーンショットを撮って画像を作成

この後で、Puppeteerを使ってスクリーンショットを撮ります。

export const metabaseSlackNotify: ScheduledHandler = async () => {
    // ...
    // フォントの読み込み。
    // raw.githack.comを経由することで、ローカルのファイルを参照することなくgithub上に上がっているフォント読み込める
    const readFontProcess = chromeLambda.font(
      'https://raw.githack.com/minoryorg/Noto-Sans-CJK-JP/master/fonts/NotoSansCJKjp-Regular.ttf'
    )
    await readFontProcess
    const buffer = await screenshotMetabaseGraph(embedUrl)
}
export const screenshotMetabaseGraph = async (
  embedUrl: string,
  defaultViewport: { width: number; height: number } = { width: 2000, height: 100 }
): Promise<Buffer> => {
  // ...
  const page = await browser.newPage()
  await page.goto(embedUrl)
  // ダッシュボードが読み込みを完了するまで画面内で待機
  await page.waitFor(5000)
  // fullPageをtrueに設定して、画面内全ての項目を表示出来るように
  const buffer = await page.screenshot({ fullPage: true })
  await browser.close()
  return buffer
}

実装のうちで

  • 画面全体を撮影する方法
  • ファイルサイズ超過でLambdaへデプロイ出来ない
    • 純粋なPuppeteerを使うのではなく、chrome-aws-lambdaを利用。
      • ファイルサイズ250MBの制限があって、Puppeteerが250MB少し超えるサイズで使えないが、50MB以下まで下がった状態でPuppeteerを使える
  • Puppeteerから撮った後の画像が文字化けを起こす
    • chromeLambda.fontによってフォントを事前に読み込み
    • raw.githack.comを経由することでローカルのファイルを参照することなく読み込み可能

あたりに躓きました。PuppeteerとLambdaの組み合わせやフォント当たりは少し工夫が必要な感じですね。

serverless-layersを使って、node_modulesのlayer化

dev.classmethod.jp

const serverlessConfiguration: Serverless = {
  custom: {
    'serverless-layers': {
      layersDeploymentBucket: 'bucketName'
    }
  },
  plugins: ['serverless-layers'],
}

これで、node_modules自体のlayer化を行うことが出来ます。
chrome-aws-lambdaがサイズ大きくて気になっていたのですっきりしますね。

AWS Systems Managerを利用して環境変数の外出し

環境変数は外部から注入したいので、AWS Systems Managerを利用して環境変数を外出しします。

import type { Serverless } from 'serverless/aws'

const serverlessConfiguration: Serverless = {
  provider: {
    environment: {
      METABASE_SITE_URL: '${ssm:/metabase-slack-notify/METABASE_SITE_URL}',
      METABASE_SECRET_KEY: '${ssm:/metabase-slack-notify/METABASE_SECRET_KEY}',
      METABASE_SESSION_ID: '${ssm:/metabase-slack-notify/METABASE_SESSION_ID}',
      SLACK_TOKEN: '${ssm:/metabase-slack-notify/SLACK_TOKEN}',
      SLACK_CHANNELS: '${ssm:/metabase-slack-notify/SLACK_CHANNELS}'
    }
  },
}

SSMパラメータを設定

等の設定を行っておけば、外部に環境変数の依存を外出し出来ます。
ローカルでlambdaを実行する時は

$ serverless invoke local \ 
   -f metabaseSlackNotify \
   --region region-name \
   -e METABASE_SITE_URL="" \
   -e METABASE_SECRET_KEY="" \
   -e METABASE_SESSION_ID="" \
   -e SLACK_TOKEN="" \
   -e SLACK_CHANNELS=""

-e ENV_NAMEの指定により、環境変数を上書き出来ます。

デプロイ

いざ、デプロイです!以下コマンドを実行し、本番環境へのデプロイを行います。

$ serverless deploy -v --region region-name --stage production

上記の実行に成功するとCloudFormation側でスタックが生成され、リソースが新規作成されます。 またデプロイ後にLambda関数の実装だけ変更し、関数の中身のロジックを変更する場合には

$ serverless deploy function -v --region region-name --stage production -f function-name

を実行することで関数のみを更新することが出来ます。

実行結果

実際の実行結果です。

f:id:kaminashi-developer:20201011011006p:plain
CloudWatch上の実行ログ

9時頃に成功のログが出力されていて

f:id:kaminashi-developer:20201011011928p:plain
slackへの通知

正しくslack上でも通知されていますね!

終わりに

Metabaseのグラフをslackへ通知するbotをServerless Framework + Puppeteerで作ってみました。
今回初めてServerless Frameworkを使ってみましたが、特に癖も少なくすんなり使えた印象。周辺のエコシステムが充実しているあたりにサーバーレスの成長をひしひしと感じました。

またslackにMetabaseのグラフの通知を飛ばすというところで、今回データによる改善のまさに第一歩目を踏み出せました。
実際に使ってくれているユーザーの行動に対してバイアスをかけないためにも、Metabaseからこれからデータを拾い続け事業を前に進める施策や改善を最速で回していきます!

カミナシでは実際に現場へ赴き、ヒアリングを重ね必要な機能や体験を磨き込み、リリース後反応を分析することにより次の一手を常に考える体制です。
そんな環境で、チャレンジしてみたい熱量持ったエンジニアの方を募集しています。エントリーお待ちしております!!