MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

Digdagバッチの基本設計とビルドの実例紹介

f:id:k_osawa:20200110125453p:plain

サーバサイドエンジニアの大澤です。主にETL処理のバッチ開発を行っています。

以前の記事で紹介していますが、マイクロアドではバッチ処理のワークフローを主にDigdagを使用して管理しています。
今回は、Digdagを1年以上使ってきてたどり着いた構成について紹介したいと思います。

Digdagバッチ CI/CDの構成

f:id:k_osawa:20200110134836p:plain

GitHub Enterprise上のバッチを管理しているリポジトリの変化に応じてJenkinsがビルド、テスト、コードチェック、デプロイまで行うようになっています。

バッチはDigdagとDockerを使用して動作するように作っているので、DockerレジストリとDigdagサーバに成果物を登録します。
成果物は対象のブランチに応じて行き先が変化し、masterブランチの変更の場合本番用の環境へ、その他の場合開発用の環境にデプロイされます。

CIの内容はリポジトリ内のJenkinsfile*1で管理していて、各ステップの処理内容はMakefileにまとめています。
Makefileにまとめる事でJenkinsfileコンパクトにでき、開発中に手動でCIと同様の処理を行えるようになります。

下記はJenkinsfileの例です。

pipeline {
    agent any
    stages {
        stage('Build docker for devel') {
            steps {
                sh 'make devel_build_docker'  // 開発用Dockerイメージのビルド
                sh 'make devel_push_docker'  // 開発用Dockerイメージのリリース
            }
        }
        stage('Run unittest') {
            agent {
                docker {  // 開発用のDockerイメージを使用してステージを実行
                    image 'registry.docker.local/batch-project01/batch01:devel'
                }
            }
            steps {
                sh 'make unittest && make codecheck'
            }
        }
        stage('Release to digdag server for devel') {
            steps {
                script {
                    sh 'make devel_push_digdag'  // 開発用のDigdagにバッチをリリース
                }
            }
        }
        stage('Build & Release for production') {
            // masterブランチの時だけstepsが実行される
            when { branch 'master' }
            steps {
                sh 'make build_docker'  // 本番Dockerイメージを作成
                sh 'make push_docker'  // 本番Dockerイメージをリリース
                sh 'make push_digdag_server'  // 本番Digdagにバッチをリリース
            }
        }
    }
}

Digdagバッチの構成

Digdagを使用し始めた当初は、Digdagのワークフローファイルでバッチ処理を書き、補助的にPythonなどの汎用な言語を利用していました。

ただ、複雑なバッチになるとワークフローファイルでは対応しきれずPythonに処理が寄ってしまうことが多くなってしまったので、現在はDigdagワークフローは大きな処理の流れを記述し、実際の処理はPythonで主に記述するようになりました。

下記のような構成になっています。

f:id:k_osawa:20200110124657p:plain

実際のプロジェクトのファイル構成例です。

.
├── wf_batch01.dig  # ワークフローファイル
├── wf_batch02.dig
├── Dockerfile
├── Makefile
├── requirements.txt
├── Jenkinsfile
├── config
│   ├── runtime_environment.dig  # Digdag用共通コンフィグ(プロファイルなど)
│   ├── mail.dig  # メール関連
│   ├── mail_body.txt  # メール本文テンプレート
│   ├── common.conf  # プロファイル間、共通設定ファイル
│   ├── devel.conf # 開発用設定ファイル
│   └── production.conf  # 本番用設定ファイル
├── src
│   ├── interface.py  # 処理の中継
│   ├── dao.py  # データベース操作
│   └── impl.py  # 処理本体
└── tests
      ├── test_impl.py  # ユニットテスト
      └── test_dao.py

バッチ処理の主な記述はPythonに移りましたが、その他にも強力な機能がDigdagには存在するのでそれらの活用はワークフローファイル内に記述しています。

具体的には下記のような用途で使用しています。

  • 大まかな処理の流れの記述
  • バッチ実行時間のスケジュール
  • エラーメールの送信
  • エラー時の自動リトライ
  • session_timeの発行(過去日付でのリカバリを簡易化するため)
timezone: 'Asia/Tokyo'
_export:
    # ワークフロー間の共通部分とプロファイル
    !include : 'config/runtime_environment.dig' 
    !include : 'config/mail.dig'
    docker:
        image: ${docker.image}

schedule:
    hourly>: 10:00

task1:
  # スコープのdigdag変数はすべてPythonで受け取れる(session_timeなど)
  py> process.process1
task2:
  # リトライを考慮して各タスクは冪等になるように設計する
  py> process.process2

_error:
  mail>: config/mail_body.txt
  subject: '[Error]${project_name}/${task_name}'

runtime_environment.digの中身(開発環境用)

project_name: batch01
profile: devel
docker:
    image: registry.docker.local/batch-project01/batch01:devel

Pythonに処理を寄せた事で特にメリットを感じている所としては下記の2点です。

  • ユニットテストを書くことができる
  • HOCON*2でコンフィグファイルを書ける

ちなみにHOCONとはキーをマージする優れた機能を持つ、Scalaでよく使用される多機能なコンフィグファイルフォーマットです。
この機能を使用すると、綺麗に各プロファイル用のファイルを小さくすることができるので、検証が難しくリリース時にトラブルになりやすい本番用ファイルのサイズを小さくでき、ミスを軽減できます。

Pythonに処理を寄せた事でデメリットを感じる部分としては、主な設定ファイルをPythonから読み込んでいるため、ワークフローファイル内から便利なオペレータを直接呼び出せなくなり記述が増える所です。
DigdagのオペレータをPythonから呼び出す必要がある場合はI/Oを記述しているレイヤーからサブタスクを追加する命令を利用して呼び出すようにします。

def s3_wait():
    arg = {'_type': 's3_wait',
                '_command': 's3://flag-bucket/table01'}
    import digdag
    digdag.env.add_subtask(arg)

Dockerのビルド

バッチはDigdagサーバ内で実行されるので、直接サーバのローカルで実行されるとバッチ間でライブラリなどが競合してしまいます。
なので各バッチ毎にDockerイメージを作成し、DigdagのPythonオペレータをDocker環境で実行する機能を使用して環境を分離しています。

下記のようなDockerfileが各バッチリポジトリ内に存在します。

FROM batch-base:0.9.0 AS project-base
COPY requirements.txt  /app/requirements.txt
RUN pip install -r requirements.txt

FROM project-base AS production
# 本番環境向けの設定を取ってくる処理

FROM project-base AS devel
# ユニットテストとコードチェック用のファイルを投入
COPY tests/ /app/tests
COPY src/ /app/src
# 開発環境向けの設定を取ってくる処理

本番と開発用のイメージはJenkinsでビルドされ、完了次第社内のDockerレジストリに登録されます。  

Digdagプロジェクトのビルドとリリース

digdag pushコマンドを使ってバッチのアップロードを行うと、指定したディレクトリ配下のすべてのファイルがDigdagサーバに登録されます。
ただ、バッチのリポジトリ内にはユニットテストなど本番の動作に関係の無いファイルが多数含まれていて、すべてをアップロードする必要はないのでdigdag uploadコマンドを使用しています。*3

digdag uploadコマンドは用意したtar.gzファイルをDigdagサーバーに登録するコマンドで、実行前にプロジェクトに必要なファイルだけを詰めたtar.gzファイルを用意します。

tar zcvf digdag-project.tar.gz config src ./*.dig
digdag upload digdag-project.tar.gz 'batch01' -e $DIGDAG_URL

本番環境の場合はtar.gzを作成するときに本番用のruntime_environment.digをtarファイル内に入れます。

ちなみに多少困る所があり、Digdag UIがGNU tarで圧縮したディレクトリの展開に対応していないので、ファイルの一覧に変な空ファイルが入ることがありますがバッチの動作には特に問題ないです。

まとめ

Digdagはバッチを強力にサポートする機能を持ったワークフローエンジンだと考えていますが、なかなか取り扱いが難しいと感じる所もありマイクロアドでも一年かけてやっと安定して設計できるようになってきたと感じています。

特にif, forや変数の取り扱いと言った処理を制御する記述はあまり得意ではないので、汎用なプログラミング言語に処理の制御を委譲する方針にする事でDigdagのメリットを享受しつつ、思うようなバッチを開発できるようになると思います。  

参考資料

https://docs.digdag.io/architecture.html
https://docs.digdag.io/operators.html