Docker+Laravelでタスクスケジューラ

## 目的
通常サーバのタスクスケジューラはcronにて行うが、  

* 少し面倒であること
* Laravelにタスクスケジューラの機能があること

から、画面を作成して画面経由でタスクスケジュール設定を行えるものを作成してみる。

## 前提
下記の環境で実装した。

* PHP7.1.x
* Laravel6.x＋AdminLTE3
* PostgreSQL11.4
* Docker phpfpm

## サンプル仕様
サーバのタスクスケジュールのため、画面操作するためにログインが必要とした。  
タスク自体は別途実装済みであるとして、タスク名と実行スケジュールを設定する画面を
用意する。  
タスクの実行はLaravelにタスクスケジューラをcron経由で実行するものとして、タスクスケジューラの設定を先の画面で設定したものを反映させるものとする。

## タスクの実装
詳細は省略するが、タスクの雛形をartisanで作成できる。  

```
% php artisan make:command タスク名
```

とりあえず、タスクが動いたか確認したいため、下記のタスクを実装した。  

```
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

class Shout extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:shout';

〜省略〜

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        Log::info("start shout!!");
    }
}
```

見ての通り、実行されたらログが出力されるだけのタスクである。

## モデル定義
定義するモデルはScrapingCommandである。
migrateの内容を下記に示す。
 
 ```
 ?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateScrapingCommandsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('scraping_commands', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string("name",200);
            $table->string("cmd",200);
            $table->integer("cron1")->nullable();
            $table->integer("cron2")->nullable();
            $table->integer("cron3")->nullable();
            $table->integer("cron4")->nullable();
            $table->integer("cron5")->nullable();
            $table->integer("flg")->default(0);
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('scraping_commands');
    }
}
 ```

## 画面実装

### ログイン画面
AdminLTEのものをそのまま使用した。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125345/0075d99c-4b98-6ae0-7485-5a2cf8093d84.png)

### タスク一覧画面
登録されているタスクの一覧表示している。  
タスクスケジュールはcronと同じ形式である。スケジュールとは別に有効／無効の設定が可能である。
![img_tsk2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125345/502d0768-f64e-c1cc-2556-1c570d32e092.png)

### タスク設定画面
タスクの設定を行う。コマンド名はartisanで使用するコマンド名(\$signatureに設定しているもの）を設定する。  
設定内容はScrapingCommandモデルに保存される。
![img_tsk3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125345/0ffcf327-a4e2-3490-b34a-53fb83c24c9f.png)

## Laravel側のスケジュール定義
LaravelのタスクスケジュールはApp\Console\Kernelのscheduleメソッドで定義するため、
同メソッドを下記のように修正した。

```
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Log;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\ScrapingCommand;

class Kernel extends ConsoleKernel
{
〜省略〜
    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $lst = ScrapingCommand::where("flg",1)->get();
        foreach($lst as $rec) {
            Log::info("cmd:".$rec->cmd);
            Log::info("cron:".$rec->cronStr());
            $schedule->command($rec->cmd)->cron($rec->cronStr());
        }
    }
〜省略〜
}
```

## Cron側の設定
Cron側はLaravelのタスクスケジューラを実行するだけなので、

```
* * * * * /usr/local/bin/php /svr/app/artisan schedule:run >> /var/log/cron.log 2>&1

(注)/svr/appはLaravelをインストールしたディレクトリ  
```
本来はcrontab -e で上記のファイルを作成することになるが、今回はDockerなので、
rootファイルとして作成しておく。
  
以上で、要望を実装済みであるが、今回はDockerで実装したため、コンテナ上でcronのインストールが必要になる。

そこで、cronのインストールはDockerイメージ作成時に行う。よってDockerファイルは

```
FROM php:fpm

# For composer
RUN apt-get update \
    && apt-get install -y gcc make zlib1g-dev libzip-dev libpng-dev unzip libmcrypt-dev libjpeg-dev libfreetype6-dev \
    && docker-php-ext-install zip \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include \
    && docker-php-ext-install -j$(nproc) gd

# Install composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
  && php composer-setup.php \
  && php -r "unlink('composer-setup.php');" \
  && mv composer.phar /usr/local/bin/composer

# Set composer environment
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

# Install laravel installer
RUN composer global require "laravel/installer"

# PHP's DB setting
RUN apt-get update \
    && apt-get install -y libpq-dev \
    && docker-php-ext-install pdo_mysql pdo_pgsql

# Install Node.js
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \
    && apt-get update \
    && apt-get install -y nodejs

RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN apt-get install -y cron

RUN mkdir /debugbar
RUN chmod go+w /debugbar
WORKDIR /svr
```

とした。  
次にcrontabの登録とcron起動が必要になるが、今回は手動で行うものとした。
よって、上記で作成したイメージからコンテナを起動して、コンテナ上で

```
% cp root /var/spool/cron/crontab
% service cron start
```
を実行する。
rootファイルはあらかじめ/svrにコピーしておくこと。

## 考察
cronの設定って結構面倒なものである。何よりわざわざサーバにログインして作業しないと行けないのである。
この仕組みだと、画面で設定するだけなので大分楽である。タスクはLaravel上で動くため、
DB接続とかログ出力とかの考慮が必要なくなる。  

問題点は

* crontabの設定とcronの起動をコンテナ起動後手動で行わなければならない。  
Dockerイメージでcronの起動もできるらしいが、今回は割愛した
* ５分毎等のスケジュールに対応できてない  
これは実装の問題であるが。。。

サンプルで実装したプロジェクトは[GitHub](https://github.com/kingcony/tasksch)にアップしたので、必要であれば参照していただきたい。