Looker + hubot によるデータ業務の自動化

はじめに

🔺YAMAP 分析チームの松本です。 Looker Advent Calendar 2020YAMAP エンジニア Advent Calendar 2020 3日目の記事です。 よろしくお願いします。

YAMAP では登山地図の情報をより充実・正確にするために、ユーザーデータを活用した 伊能忠敬プロジェクト に取り組んでいます。 具体的な一例としては、ユーザーさんの軌跡を分析して新規ルートを抽出し、登山地図に反映しています。

このルート抽出処理は Python で実装しており、従来は分析者が手元のマシンで実行していました。 また、地図上のどこに対してルート抽出を試みるかは、データ基盤のデータを Looker で可視化して決めています。

f:id:hidetakafm:20201203152227p:plain

今夏にはじめたルート抽出の取り組みも軌道に乗り、最近では自動化・効率化の機運が高まっていました。

どう自動化するか

Python ルート抽出処理については、もともと運用していた分析システム(Python / GKE)に組み込みました。 あとは Looker のデータから抽出処理を kick する所を自動化すれば OK ですが、ここでは 2 つの方法を検討しました。

  1. Looker Action Hub でカスタムアクションを開発する
  2. Looker から Slack へ抽出候補 csv を投稿させる。hubot で csv を読み取り、抽出処理を kick する

今回は以下の理由から、2 の方法を採用しました。

  • 自動化の仕組みから GKE クラスタに対して kubectl コマンドを実行するため ※1
    • ルート抽出処理の kick は k8s job をデプロイする形で行いたく、そのために kubectl コマンドで実行したかった
    • これは 1 の方法では実現できなさそうに思われた
  • 一連のオペレーションは人間が ChatOps として実行できるようにもしておきたかったため
    • YAMAP ではユーザーさんからルート追加要望を承る場合もあり、その場合はオペレーターがルート抽出を kick できると便利

ということで、以下のような構想になります。

f:id:hidetakafm:20201203202938p:plain

自動化した結果

  • hubot の名前は "yak" にしました。ヤクはヒマラヤ地方の高地に生息する牛さんです
    • f:id:hidetakafm:20201203175818p:plain
  • Looker から csv をスケジュール配信します。コメントに hubot のコマンドを書いておきます
    • f:id:hidetakafm:20201203180256p:plain
  • yak がファイルをパースして、kubectl コマンドでルート抽出処理を kick します
    • f:id:hidetakafm:20201203180317p:plain
  • 分析システム(yama)が処理完了を教えてくれます
    • f:id:hidetakafm:20201203180411p:plain
    • この後、地図エディタで成果物を人間がチェックして、登山者の皆様にお届けしています

やったね 🎉

まとめと展望

Looker + hubot で、YAMAP 伊能忠敬プロジェクトのオペレーションの一部を自動化しました。 Looker には Action Hub という便利な仕組みも用意されていますが、今回のように事情がある場合は hubot などチャットボットを利用することが出来ます。

今回は僕が所属する分析チームのオペレーションを自動化しましたが、今後は hubot や Action Hub も活用して社内のデータ業務を自動化し、データ活用を加速したいと思います。

それにしても YAMAP では様々なデータ活用が広がっており、それを柔軟に支えてくれる Looker は本当に素晴らしいツールだと思います! (hubot アドベントカレンダーみたいになってしまいスミマセンでした 😅)

実装(ご参考まで)

何もコードがないのは寂しいので、hubot の yama extract-route コマンドの正常系の実装だけ載せておきます。

'use strict';
const util = require('util');
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec);
const fs = require('fs');
const csv = require('csv');
const request = require('request');

module.exports = (robot) => {
  robot.respond(/yama extract-route/i, async (res) => {
    const fileInfo = res.message.rawMessage.files[0],
          tmpFilePath = `tmp/${fileInfo.timestamp}_${fileInfo.name}`;
    let data = [];
    const parser = csv.parse((err, row) => {
      data = row;
    });
    request
      .get(fileInfo.url_private_download)
      .auth(null, null, true, process.env.HUBOT_SLACK_TOKEN)
      .pipe(fs.createWriteStream(tmpFilePath))
      .on('finish', () => {
        fs.createReadStream(tmpFilePath)
          .pipe(parser)
          .on('end', async () => {
            // process head 10 rows.
            for (let row of data.slice(1, 4)) {
              const result = await exec(`LANDMARK=${row[1]} k8s/scripts/deploy/run_route_extraction`);
              res.send(result.stdout);
            }
          });
      });
  });
}

補足

今回採用しなかった Looker Action Hub を改めて調べると、GCP の CloudStorage に csv を PUT するアクションが用意されていました。

CloudStorage の PUT イベントに対して CloudFunction をトリガすることで、※1 はクリアできる可能性があることに、この記事を書きながら気づきました...