【Laravel8】scheduleで実行する時だけ、DBにアクセスしようとするとConnection refusedが発生する


背景

ローカルにLaravel+Dockerの開発環境を作ってWEBアプリを開発をしており、定期的にDBに保存したデータを元にして外部サービスのAPIを叩き情報を取得する必要があった為、コマンドスケジューラで取得処理を実装してたところ、DBに接続できないエラーが発生してかなり詰まってしまい、(探し方の問題なのか詰まるほどでもない初歩的な問題だからか)関連する記事や解決方法を見つけるのに苦労したので備忘録として残したくてまとめました。

開発環境

PHP 7.4.12
Laravel 8.12
Docker 20.10.5
Docker Compose 1.28.5

起きたこと

やりたいことはめちゃくちゃシンプルで、DBから外部サービスのAPIを叩くのに必要な情報を取得して、APIから返された結果をDBに保存する処理をコマンドとして登録し、そのコマンドをスケジューラで定期的に叩くというもの。起きたことを分かりやすくする為に不要な記述を極力省いてますので、ツッコミ所が多いかもですがご了承頂けると幸いです。

まずは公式ドキュメントを参考にArtisanコマンドを作成

$ php artisan make:command SampleGetApi

作成されたSampleGetApi.phphandle内にDBからデータを取得してAPIを叩く処理を実装(分かりやすくするためにシンプルにしてます。)

app/Console/Commands/SampleGetApi.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\User;

class SampleGetApi extends Command
{
    protected $signature = 'sample:getApi';
    protected $description = 'This command is a sample.';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        // 処理の対象になるデータ取得
        $users = User::where(['status' => 1])->get();

        /*
          ~ APIから取得する処理 ~ 
        */


        /*
          ~ DBに保存する処理 ~ 
        */

        return 0;
    }
}



スケジューラに作成したコマンドを定期的に実行するように設定

app/Console/Kernel.php
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        // コマンドを登録
        Commands\SampleGetApi::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // 5分おきにコマンドを実行するよう設定
        $schedule->command('sample:getApi')->everyFiveMinutes();
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}


あとはcronにお決まりのphp artisan schedule:runを設定

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

すると、コマンドは実行されるのですがUser::where()-> ~の箇所で下記のエラーが発生。

[previous exception] [object] (Doctrine\\DBAL\\Driver\\PDO\\Exception(code: 2002): SQLSTATE[HY000] [2002] Connection refused at /work/backend/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Exception.php:18)

エラーの内容で調べてみたところ、ほとんどがDBへの接続情報の設定がおかしいのが問題みたいでした。

しかし、大半がphp artisan migrateがそもそも通らないという状態なのに対して、僕の環境だとCLIとコマンドスケジューラ両方でphp artisan migrateを実行しても問題なく通り、LaravelのControllerに書いたDBへの読み書き処理も正しく動き、php artisan tinkerで試しても、CLIから作成したsample:getApiコマンドを実行しても問題なく通るのに、なぜかコマンドスケジューラでsample:getApiを実行すると上記のエラーが出るという謎の状態でした。 

結論

色々試した結果、DBの接続情報に原因があり、 通常の処理で使う場合CLIやタスクスケジュールで実行する場合 とで使われてる接続情報が異なっていた為、そこに気づかずに通常の処理で使われてる接続情報でDBにアクセスしようとしてエラーが発生してました。なぜこの設定が異なっているのか理由や原因まで追えてないのですが、もしこの辺について詳しい方やこの事象の原因についてご存知の方、もっと良い解決方法あるよーという方がいらっしゃいましたら、コメントを頂けると大変嬉しいです。

(個人的な仮説としては、CLIでDBに接続する場合は、host名の部分がdocker-compose.ymlで設定したホスト名になってたりするのが原因なんじゃないかと考えてます。)

解決方法

僕がとった解決方法としては、tinkerで使われてる接続情報を確認して、その内容をconfig/database.phpconnectionsにスケジューラ用の接続情報として追加して、スケジューラで操作するときは、追加した接続先に切り替えて繋ぐようにしたところ、うまく動作しました。(これが正しいのか怪しいですが)

手順としては、まずtinkerを起動してconfig('database.connections.mysql');で接続情報を表示してメモする

$ php artisan tinker
Psy Shell v0.10.4 (PHP 7.4.12 — cli) by Justin Hileman
>>> config('database.connections.mysql');   // このコマンドを叩いて表示される情報をメモ
=> [
     "driver" => "mysql",
     "url" => null,
     "host" => "db",
     "port" => "3306",
     ...
     // その他接続情報が表示
   ]

>>>

その後、出力された情報をconfig/database.phpconnectionsの設定に新しく追加
(分かりやすいようにmysql_scheduleの名前で設定しました。)

config/database.php
<?php

use Illuminate\Support\Str;

return [


       ~~~~~~省略~~~~~~~~~


    'connections' => [

         // 通常の処理で使われてる接続情報
         'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
                         ...
                     // その他接続情報が表示
        ],


                 // スケジューラ用の設定として出力された情報を追加
        'mysql_schedule' => [
            'driver' => 'mysql',
            'url' => null,
            'host' => 'db',
            'port' => '3306',
            ...
                          // その他接続情報が表示
        ],


    ],


];

そして、作成したコマンド内でDBに対して操作する際にDB::connection()で接続先を指定してからデータを取得するようにするとうまく動作しました。

app/Console/Commands/SampleGetApi.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\User;

class SampleGetApi extends Command
{
    protected $signature = 'sample:getApi';
    protected $description = 'This command is a sample.';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        // DB::connection()で追加した接続情報の設定を指定する
        $users = DB::connection('mysql_schedule')->table('users')->where(['status' => 1])->get();
        /*
          ~ APIから取得する処理 ~ 
        */

        /*
          ~ DBに保存する処理 ~ 
        */

        return 0;
    }
}



参考