Closed11

Dataform cli を Cloud Run で動かす

hiracky16hiracky16

動機

  • Dataform をイベント駆動で実行したい
  • Dataform には REST API が用意されておりリクエストとすると実行が可能
  • しかし REST API は Beta 版である(2022/07 時点)
  • また REST API は不安定でレスポンスが返ってこないことがときたまある
  • Dataform cli を実行するためのインフラとして Cloud Run (Jobs) を使ってみる
hiracky16hiracky16

Dockerfile

上の手順で作った example.sql を実行するための Dockerfile を書いてみる。

ただ、dataform init-creds bigquery が実行ができなかった。
コマンド途中に location や service account の json へのパスを入力する手順を Dockerfile 内で突破できなかったためである。
(コマンド途中の標準入力はパイプ使えば突破できるかなと思ったができなかった。誰か突破の仕方教えてほしい…)

Dockerfile
FROM node:slim
MAINTAINER hiracky16
RUN npm i -g @dataform/cli
ENV DATAFORM_PATH /dataform
WORKDIR $DATAFORM_PATH
RUN dataform init bigquery . --default-database
# TODO: 本当は実行したかったがコマンド実行時の入力がクリアできず断念
# 代わりにローカルで作った .df-credentials.json を COPY
# RUN dataform init-creds bigquery
COPY dataform.json .
COPY definitions/* definitions/
COPY includes/* includes/
COPY .df-credentials.json .
RUN dataform compile
ENTRYPOINT ["dataform", "run"]
hiracky16hiracky16

試しに実行

> docker build -t dataform .
...
> docker run dataform
Compiling...

Compiled successfully.

Running...

Dataset created:  dataform.example [view]

できてた。

hiracky16hiracky16

Artifact Registry へ push

  1. リポジトリを作る(dataform って名前にした)
  2. 以下を実行
> docker build -t asia-northeast1-docker.pkg.dev/$PROJECT/dataform/dataform:latest .
...
> docker push asia-northeast1-docker.pkg.dev/$PROJECT/dataform/dataform:latest
...

Artifact Registry を見たらアップされてた。

hiracky16hiracky16

(寄り道)Cloud Run jobs を使ってみる

  • 先日プレビューになった Cloud Run jobs を使ってみる
  • 普通の Cloud Run は HTTP リクエストをトリガーに実行されるが jobs 任意のタイミングでジョブを作成し実行することができる
  • 上記で作った Docker イメージをそのまま jobs に登録して実行してみる

さっき push したイメージを指定してジョブを登録する。

登録したジョブを実行してみると BigQuery に example という view ができる。

hiracky16hiracky16

Cloud Run で実行(node バージョン)

  • dataform cli は npm パッケージなのでアプリも node で書いて Shell を実行すると Go のアプリから node を実行するみたいなテクニカルなことをせずとも済む
Dockerfile.cloudrun
FROM node:slim
MAINTAINER hiracky16
RUN npm i -g @dataform/cli

ENV DATAFORM_PATH /app
WORKDIR $DATAFORM_PATH
RUN dataform init bigquery . --default-database
COPY dataform.json .
COPY definitions/* definitions/
COPY .df-credentials.json .

COPY ./package.json .
RUN npm i express
COPY invoke.js .
CMD ["node", "invoke.js"]
  • node で shell を実行するアプリを作ってみる
    • child_process を使うと shell を実行することができた

参考
https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-nodejs-service

invoke.js
const express = require('express');
const childProcess = require('child_process');
const util = require('util');
const exec = util.promisify(childProcess.exec);

const app = express();

const Response = {
  success: "Success",
  error: "Error"
}

const execShell = async () => {
  const res = await exec('dataform run')
  return res.stderr ? Response.error : Response.success
}

app.get('/', async (req, res) => {
  const response = await execShell();
  res.send(response);
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
  console.log(`helloworld: listening on port ${port}`);
});

これをデプロイして発行された URL にリクエストしてみると、レスポンスが返ってきて、BigQuery にも view が作られていた。

hiracky16hiracky16

tags を指定して実行する

クエリパラメータに ?tag=hogehoge みたいに指定したら dataform の tags を指定できるようにしたい。
tags とは SQLX という記法の config に設定する項目の一つで、dataform のテーブルをとある文脈でまとめることができる。
例えば datawarehoues のクエリだけ実行するみたいな限定ができる。

example.sql をベースに example1.sqlxexample2.sqlx を作る

example1.sqlx
config { type: "view", tags: ["tag1"] }
select 1 as test
example2.sqlx
config { type: "view", tags: ["tag2"] }
select 2 as test

次に node のアプリの方を修正

invoke.js
const express = require('express');
const childProcess = require('child_process');
const util = require('util');
const exec = util.promisify(childProcess.exec);

const app = express();

const Response = {
  success: "Success",
  error: "Error"
}

const execShell = async (tag) => {
  let command = 'dataform run'
  if (!!tag) {
    command = `${command} --tags ${tag}`
  }
  const res = await exec(command)
  if (res.stderr) {
    console.error(res.stderr)
    return Response.error
  }

  console.info(res.stdout)
  return Response.success
}

app.get('/', async (req, res) => {
  const tag = req.query.tag
  const response = await execShell(tag);
  res.send(response);
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
  console.log(`helloworld: listening on port ${port}`);
});

こうすることによって https://dataform-xxxxx?tag=tag1 で example1 が、https://dataform-xxxxx?tag=tag2 で example2 が BQ 上に作成される。
tags によって実行するクエリを限定できたことが確認できる。

hiracky16hiracky16

目的だった Cloud Run サービスへの HTTP リクエストをトリガーに dataform run を実行することができた。
全然関係ないが Cloud Run で Shell を動かすアプリ汎用性あるので他でも使えるかもと思った。

hiracky16hiracky16

.df-credentials.json の扱いについて

  • https://zenn.dev/link/comments/56fd475589f01c にて .df-credentials.json をコマンドベースで生成できずでローカルで生成したものをコピーすることで対応した

  • このファイルの中身はサービスアカウントキーの内容がそのまま記述されておりセキュリティ的にコンテナに含むのは気が引ける

  • Cloud Run では Sercret Manager のシークレットを環境変数やファイルとして扱うことができる

  • コンソール上から設定するとこんな感じ。

  • ポイントが /app/credentials/.df-credentials.json にファイルをマウントするところ

  • dataform のプロジェクト直下 /app にないと動かないため Dockerfile 側でシンボリックリンクを予め作成しておく

    • /app 配下にマウントすると SQL ファイルなども上書きされちゃうので別ディレクトリを指定する必要がある
Dockerfile.cloudrun
FROM node:slim
MAINTAINER hiracky16
RUN npm i -g @dataform/cli@1.22.1

ENV DATAFORM_PATH /app
WORKDIR $DATAFORM_PATH
RUN dataform init bigquery . --default-database
RUN mkdir credentials
COPY dataform.json .
COPY definitions/* definitions/
# COPY .df-credentials.json /tmp/.df-credentials.json
RUN ln -s credentials/.df-credentials.json .df-credentials.json

COPY ./package.json .
RUN npm i express
COPY invoke.js .
CMD ["node", "invoke.js"]```
このスクラップは2022/07/23にクローズされました