DockerのPython Imageとparse-crontabを使ってシンプルな定時処理batchを作る

この記事は [Docker Advent Calendar 2015](http://qiita.com/advent-calendar/2015/docker "Docker Advent Calendar 2015") 14日目の記事です。

# Summary

* Pythonで定時処理batchを作りたい場合はparse-crontabを使うと便利&瞬殺で出来るお.
* parse-crontabとDockerの公式Python Imageでbatch実行環境を速攻で作れちゃうぜ!

# 本題

このネタはPyCon JP 2015のTalk Session「[野球Hack!~Pythonを用いたデータ分析と可視化 ](http://www.slideshare.net/shinyorke/hackpython-pyconjp "野球Hack!~Pythonを用いたデータ分析と可視化")」の後半戦で披露した、「Dockerとparse-crontabで野球データを取得するbatchを作ったぜ！」ネタの抜粋&詳細解説バージョンとなります.

基本的には私の都合&思いで書いてる部分が多いので意見ツッコミ提案お待ちしておりますm(_ _)m

また、このネタを作るにあたり、

**[Pythonで定時実行処理を実装する（GAUJIN.JP/ごうじん）](http://gaujin.jp/index.php?itemid=27 "Pythonで定時実行処理を実装する")**

上記エントリーを参考にさせていただきました.

※PyCon JP 2015発表の時点で引用の承諾を頂いております.

## お前だれよ

### 自己紹介

* [@shinyorke(Twitter)](https://twitter.com/shinyorke "@shinyorke")
* 八重洲方面で働くWeb Engineerであり、野球データをHackする「野生の野球アナリスト」でもある.
* Python, Agile, Lean Startupなどが好物.
* 「野球」「Python」でググると過去の成果がたくさん出てきます.

### Dockar歴とか

* 初めて触ったのが2014年冬
* 一度挫折→キッカケがあって2015年夏に再チャレンジ→ハマりにハマり、今ではDockerナシでの開発が苦痛に.
* 上述の通り、今年のPyConの発表でDockerを少しだけ使いました.
* 今冬〜来春にかけて構築する野球データ分析基盤はDocker上で動かす予定です.
* 何が言いたいってDockerメッチャ好き.

## 背景

PyCon JP 2015、、、の前に行った[XP祭り2015の発表](http://www.slideshare.net/shinyorke/agile-baseball-science-52692504 "Agile Baseball Science - アジャイル脳によく効く野球のハナシ")で野球データが必要となり、開発を始めました.

* 野球データのサイト（毎日更新）をスクレイピング&データセットを吐き出すスクリプトをPythonで実装.
* 毎朝定期的に実行してデータセットを取るマイクロサービスが必要に.
* そうだ！Docker上で作る&imageごとクラウドサービスにコピって定時実行すればいいじゃん！（名案）.

## 課題

### Docker HubのPythonイメージにcrontabが含まれていない

* 実行すべき処理をPythonで書いて、あとはcrontabを仕込んで放っておけばいい.と、最初は思っていた.
* だがしかし、使う予定だった[Docker HubのPython image](https://hub.docker.com/_/python/ "Python(Docker Hub)")にはcrontabが含まれていない！
* ワイ「Dockerの思想上、確かにそうよねー（でもこのままじゃサービス作れないどうしよう...orz）」

## 転機！〜Pythonだけでcrontabが動かせるだと？

* 適当にググった所、ごうじんさんのブログ「[Pythonで定時実行処理を実装する（GAUJIN.JP/ごうじん）](http://gaujin.jp/index.php?itemid=27 "Pythonで定時実行処理を実装する")」に遭遇
* 手元で写経して実験→うまく動いた＼(^o^)／
* Dockerに突っ込んで動かしても問題ない！！！ちゃんと動いてる！！！！これはイケる！！！！！

ちょうどPyCon JP 2015の発表を作ってたのもあり、その場でTwitterで「転載させて！」とお願い→ありがたいことに快諾していただきました.

## sample&解説

野球のコードはちょっと複雑（&あんまり解説したくないw）ので、sampleを作りました.

[python-crontab-docker-example(GitHub)](https://github.com/Shinichi-Nakagawa/python-crontab-docker-example "python-crontab-docker-example")

推奨環境はPython3.4.x以上です.

ちなみに、現時点（2015/12/14）の最新バージョンPython 3.5.1で問題なく動いております！

### Job設定&実行

batchを実行するJobControllerと、次の実行時間&実行までのインターバルを管理するJobSettingsを定義します.

JobController.run()の引数はお馴染みcrontabの設定(* * * * *)です.

```py:scheduler/job.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import functools
import logging
from crontab import CronTab
from datetime import datetime, timedelta
import math

__author__ = 'Shinichi Nakagawa'


class JobController(object):
    """
    ジョブ実行Controller
    """

    @classmethod
    def run(cls, crontab):
        """
        処理実行
        :param crontab: job schedule
        """
        def receive_func(job):
            @functools.wraps(job)
            def wrapper():

                job_settings = JobSettings(CronTab(crontab))
                logging.info("->- Process Start")
                while True:
                    try:
                        logging.info(
                            "-?- next running\tschedule:%s" %
                            job_settings.schedule().strftime("%Y-%m-%d %H:%M:%S")
                        )
                        time.sleep(job_settings.interval())
                        logging.info("->- Job Start")
                        job()
                        logging.info("-<- Job Done")
                    except KeyboardInterrupt:
                        break
                logging.info("-<- Process Done.")
            return wrapper
        return receive_func


class JobSettings(object):
    """
    出力設定
    """

    def __init__(self, crontab):
        """
        :param crontab: crontab.CronTab
        """
        self._crontab = crontab

    def schedule(self):
        """
        次回実行
        :return: datetime
        """
        crontab = self._crontab
        return datetime.now() + timedelta(seconds=math.ceil(crontab.next()))

    def interval(self):
        """
        次回実行までの時間
        :return: seconds
        """
        crontab = self._crontab
        return math.ceil(crontab.next())
```

### batch本体

JobControllerをimport、実行すべき処理をPoolしてあとは並列実行します.

sampleは毎週金曜日夜のタモリ倶楽部および、毎日18:00の野球を教える感じで作っています.

注意するポイントとしては、

* DockerのPythonイメージはUTCタイム(デフォ)なので、日本時間では設定しちゃダメ.
* 日本のタイムスケジュールに合わせるなら-9.0hしましょう.

ってことぐらいですね.

```py:batch.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from multiprocessing import Pool
from scheduler.job import JobController

__author__ = 'Shinichi Nakagawa'


# Docker ImageのTimezoneがUTCなので注意！
@JobController.run("20 15 * * 5")
def notice_tmr_club():
    """
    タモリ倶楽部の時間だお(東京)
    :return: None
    """
    logging.info("タモリ倶楽部はじまるよ！！！")


# Docker ImageのTimezoneがUTCなので注意！(大切なので2回言いました)
@JobController.run("00 9 * * *")
def notice_baseball():
    """
    やきうの時間を教えるお
    :return: None
    """
    logging.info("やきうの時間だあああ！！！！")


def main():
    """
    crontabを動かすmethod
    :return: None
    """
    # ログ設定(Infoレベル、フォーマット、タイムスタンプ)
    logging.basicConfig(
        level=logging.INFO,
        format="time:%(asctime)s.%(msecs)03d\tprocess:%(process)d" + "\tmessage:%(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    # crontabで実行したいジョブを登録
    jobs = [notice_tmr_club, notice_baseball]

    # multi process running
    p = Pool(len(jobs))
    try:
        for job in jobs:
            p.apply_async(job)
        p.close()
        p.join()
    except KeyboardInterrupt:
        logging.info("exit")


if __name__ == '__main__':
    main()
```

### Dockerfile

こちらは単純です.

GitHubのコードをincludeして立ち上げて終了.

```text:Dockerfile
# Python crontab sample

FROM python:3.5.1

MAINTAINER Shinichi Nakagawa <spirits.is.my.rader@gmail.com>

# add to application
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app/
RUN pip install -r requirements.txt
ADD ./scheduler /app/scheduler/
ADD *.py /app/
```

### docker-compose

docker-compose.ymlにdocker起動の設定を書きます.

といってもbatch.pyを動かして終了です.

```yaml:docker-compose.yml
batch:
  build: .
  dockerfile: ./Dockerfile
  command: python batch.py
  container_name: python_crontab_example

```

### 起動！！！

docker-compose up(又はdocker run)してこんな感じで動けばOK.

```
$ docker-compose up
Creating python_crontab_example
Attaching to python_crontab_example
python_crontab_example | time:2015-12-13 13:45:09.463   process:9       message:->- Process Start
python_crontab_example | time:2015-12-13 13:45:09.464   process:8       message:->- Process Start
python_crontab_example | time:2015-12-13 13:45:09.465   process:9       message:-?- next running        schedule:2015-12-18 15:20:00
python_crontab_example | time:2015-12-13 13:45:09.465   process:8       message:-?- next running        schedule:2015-12-14 09:00:00

```

## まとめ&次の方へ

### まとめ

* コンテナの凝集性を高める意味でも、「crontabに依存しない言語独自のcrontab」というアプローチはアリなのかもしれない
* 他の言語でもあれば真似出来そう.
* とはいえ用途とかユースケースは割と限定される&総合的にもっと賢い方法があったら知りたい.
* というわけでご意見ご感想お待ちしています.

### 次の方へ

@tamai さんよろしくお願いします.