2025.06.10
PHPのcronバッチをAWSに移行する方法 – ECS/EventBridgeによる自動実行
結論
- AWS環境にバッチを実行する基盤を構築出来た
- ECSタスクのランニングコストとバッチの追加コストを削減出来た
はじめに
こんにちは。次世代システム研究室のT.Tです。
現在、開発運用に携わっているWebサービスのシステム基盤のAWSへの移行を進めています。AWSへの移行は、全体で5フェーズに分けた移行を予定していて、現在は第3フェーズとなるWebサービスのバッチを移行しています。バッチは、移行前のオンプレ環境ではホストOS上のcronジョブでPHPコンテナを実行する構成で、AWS移行後はEventBridgeでPHPコンテナをECSで実行する構成になっています。
本記事では、AWS環境でバッチを実行する基盤を構築した内容についてご紹介します。
1.オンプレ環境のバッチのシステム構成
バッチの移行元のオンプレ環境では、CentOS7のホスト上でcronジョブを実行して、PHPのDockerコンテナを実行する構成で稼働しています。そのコンテナで、Yii2フレームワークのconsoleアプリケーションを実行してバッチ処理しています。
2.AWS環境のバッチのシステム構成
バッチの移行先のAWS環境では、EventBridgeで5分間隔でPHPコンテナのECSタスクを起動して、そのECSタスク内でPHPプロセスを実行して、PHP内に定義されているcronで指定された時間にバッチを処理する構成になっています。
別のバッチの構成案として、EventBridgeのcronで各バッチを実行する時間を指定して、直接PHPコンテナのECSタスクを実行する構成もありますが、この構成と比較した場合に以下のメリットがあります。
- 1個のECSタスクでPHPプロセスを実行することで、ECSのランニングコストを削減出来る
- 日本時間でcronの時刻を設定出来る
- コンテナの更新だけで新しいバッチを追加出来る(CDKで新しくECSタスクを追加しなくて良い)
このメリットについての説明の前に、今回構築したAWS環境のcronジョブの実行基盤の内容を実装を踏まえてご紹介します。その後、メリットの説明に移ります。
3.AWS環境のcronジョブの実現方法
CDKで、5分間隔でPHPコンテナを実行するECSタスクを1個呼び出すEventBridgeを定義します。各バッチの処理はPHPコンテナ内で実行するため、CDKにはバッチを実行する処理は定義されていません。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as events from 'aws-cdk-lib/aws-events'; export interface ConsoleEcsStackProps extends cdk.StackProps { ... } export class ConsoleEcsStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ConsoleEcsStackProps) { super(scope, id, props); const ecsTaskExecutionRole = new iam.CfnRole(...); const securityGroupPrivate = new ec2.CfnSecurityGroup(...); const eventRole = new iam.CfnRole(...); const taskDefinitionProps: ecs.CfnTaskDefinitionProps = { networkMode: ecs.NetworkMode.AWS_VPC, requiresCompatibilities: ['FARGATE'], cpu: "512", memory: "2GB", runtimePlatform: { cpuArchitecture: 'X86_64', operatingSystemFamily: 'LINUX', }, executionRoleArn: ecsTaskExecutionRole.attrArn, family: "console-ecs-task-definition", taskRoleArn: ecsTaskExecutionRole.attrArn, containerDefinitions: [ { essential: true, image: "console", entryPoint: ['php', 'yii', "schedule/run"], name: 'app-console', cpu: 0, logConfiguration: { logDriver: "awsfirelens", secretOptions: [], }, }, ], tags: [ { key: 'Name', value : "console-ecs-task-definition" } ], }; const taskDefinition = new ecs.CfnTaskDefinition(this, "console-ecs-task-definition", taskDefinitionProps); taskDefinition.addDependency(ecsTaskExecutionRole); const rule = new events.CfnRule(this, "console-event-rule", { scheduleExpression: "cron(*/5 * * * ? *)", // UTC+0 state: "ENABLED", targets: [{ id: "console-event-rule-target", arn: "arn:aws:ecs:...", roleArn: eventRole.attrArn, ecsParameters: { taskDefinitionArn: taskDefinition.attrTaskDefinitionArn, taskCount: 1, launchType: 'FARGATE', networkConfiguration: ..., }, }] }); } }
ECSタスク内では、各バッチをcronの設定に基づいてバッチプロセスを起動するScheduleControllerを実行します。ScheduleControllerでは、Yii2のparams.phpファイルからcronの設定と実行するバッチを読み込んで、cronで設定された時間に各バッチを子プロセスとして非同期で実行します。
<?php namespace console\controllers; use yii\console\Controller; use Cron\CronExpression; use Yii; class ScheduleController extends Controller { public function actionRun($runTimeUTC): void { $date = new \DateTime($runTimeUTC, new \DateTimeZone('UTC')); $date->setTimezone(new \DateTimeZone('Asia/Tokyo')); // UTC+9 $runTime = $date->format('c'); // ISO 8601 format $cronJobs = Yii::$app->params["cron_job_list"]; $processes = []; foreach ($cronJobs as $cronJob) { $cron = new CronExpression($cronJob['cron']); $cmd = $cronJob['command']; $dt = new \DateTimeImmutable($runTime); $isDue = $cron->isDue($dt); if ($isDue) { $this->runAsyncBatch($cmd, $lockKey, $processes); } } $this->waitAsyncBatch($processes); } private function runAsyncBatch(string $command, string $lockKey, &$processes): void { $descriptor_spec = [ 1 => ['file', 'php://stdout', 'w'], 2 => ['file', 'php://stderr', 'w'], ]; $process = proc_open($command, $descriptor_spec, $pipes); if (is_resource($process)) { $processes[] = [ 'process' => $process, 'lockKey' => $lockKey, ]; } } private function waitAsyncBatch($processes): void { ... } }
params.phpのバッチの設定
'cron_job_list' => [ [ 'cron' => '*/5 * * * *', 'command' => 'php yii batch1/index', ], [ 'cron' => '*/5 * * * *', 'command' => 'php yii batch2/index', ], [ 'cron' => '0 10 * * *', 'command' => 'php yii batch3/index', ], ]
4.AWS環境のバッチのシステム構成のメリット
ECSのランニングコストの削減
現在運用環境で実行しているバッチでは、CPUやメモリを大量に使用する処理は少なく、ECSの小さめのスペックのタスクでいくつかのバッチを実行出来るリソース使用量です。そのため、一個のECSタスクでPHPの子プロセスとして複数のバッチを同時に処理することで、ECSのランニングコストの削減が期待出来ます。
CDKで、5分間隔で1個のPHPのScheduleControllerを実行するECSタスクを呼び出すようにEventBridgeを設定して、呼び出されたECSタスク内でproc_openでPHPプロセスを非同期で実行することで、この仕組みを実現しています。
先述のparams.phpのバッチの設定では、batch1とbatch2は同時刻に実行されますが、1個のECSタスク内で2つのバッチが実行されるため、2個のECSタスクで実行する構成と比較した場合にランニングコストの削減が期待出来ます。
日本時間でcronの時刻を設定出来る
EventBridgeのcronの書式はUTCにしか対応していないため、cronで特定の時刻を設定する場合に不便です。PHP内で、cronに設定された時間をパースすることで時刻を日本時間で設定出来るようにしています。cronのライブラリには、dragonmantank/cron-expressionを利用しています。
先述のparams.phpのバッチの設定では、日本時間の毎日10時にbatch3が実行されます。
コンテナの更新だけで新しいバッチを追加出来る
バッチの処理とcronの定義は、PHPコンテナに記述するだけで実現出来ます。新しいバッチを追加する場合は、consoleにバッチのControllerを追加して、先述のparams.phpにcronの設定と呼び出すControllerの定義を追加します。これにより、バッチを追加する度にCDKで新しくECSタスクを追加しなくて良いので、システム構成を変更する作業を削減出来ます。
5.まとめ
バッチを実行する基盤をAWS環境に構築出来ました。移行の第3フェーズでは、これからオンプレ環境で稼働しているバッチの移行を進める計画になっています。バッチの内容によっては、今回ご紹介した仕組みだけでは対応出来ない処理があるかも知れないですが、そういった部分も改善しながら移行を進めたいと思います。
次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD