AWS SES (Simple Email Service) でメール配信
(そもそものモチベーションは、laravelには標準でaws ses のドライバーが存在しているが、テンプレートを生成して、ユーザごとにそれぞれ値が変化するようなメールの送信には対応していないようだ。)
特にマーケティングを目的としたメール配信では、数千, 数万単位での一括で送信を行うため、SESの制限に引っかからないような調整が必要となる。
具体的な制限は以下の通り。
- 日次送信クォータ: 1日あたりの送信件数 (≦ 50,000件)
- 最大送信レート: 1秒間あたりの送信件数 (≦ 14件)
- 一括送信できる送信件数 (≦ 50件)
一括送信(SendBulkEmail)の上限が50件で秒間の送信上限が14件よりも多く、一回上限の50件を送信したら即座に上限に引っ掛かるのか気になる。
何か問題が発生した場合にログを参照したい場合がある。
ただ、ストリームが分散しているためストリーム内を読んで探すのは大変。
その場合は、CloudWatchのログインサイトが便利
以下のようなクエリを指定すると検索結果が出力されて、@logStream
に検索に一致するログのリンクが表示されるので、一発で目的のログへ飛ぶことができる。
fields @timestamp, @message, @logStream, @log
| filter @message like 'DONE' and @message like 'FAIL'
| sort @timestamp desc
| limit 30
ログを検索する際には、期間も指定可能なので、問題発生時の期間をフィルターした上で全てのストリームを検索できる。
ただ、この出力結果は最大でも10000レコードのみのダウンロードとなるので、結局全て見たい場合はダウンロードしてタイムスタンプを元に自分でソートするしかないのかも?
こういうミドルウェアを作成して、 quota のlimitを突破しそうなら一旦job を queueに戻して20秒待機するようにしてみた。
リトライ上限を設定しないとリトライ時に MaxAttemptsExceededException
が発生するので注意
<?php
declare(strict_types=1);
namespace App\Jobs\MailSystem;
use App\Services\Aws\AwsService;
use Illuminate\Support\Facades\Log;
final class SesRateLimitedMiddleware
{
private readonly AwsService $aws;
public function __construct()
{
$this->aws = app()->make(AwsService::class);
}
/**
* If the SES rate limit is exceeded, it will be re-executed after 20 seconds.
*
* @param callable $next
* @return mixed
*/
public function handle(mixed $job, $next)
{
$result = $this->aws->ses->getAccount();
/**
* @var array{
* Max24HourSend: float,
* MaxSendRate: float,
* SentLast24Hours: float,
* } $quota
*/
$quota = $result->get('SendQuota');
Log::info('SES Rate Limited', $quota);
if ($quota['MaxSendRate'] < 3) {
return $job->release(20); // @phpstan-ignore-line
}
return $next($job);
}
}
ざっくりこんな実装で一気に dispatch
すると 突っ込まれた大量のqueue がほぼ同時に発火して ses の limit rate の取得では、対応ができなかった。
(おそらく ses 側の limit rate が更新される前に取得して、制限に引っかかる状態)
$users->chunk(50)->each(
fn (Collection $users, int $index) =>
// @phpstan-ignore-next-line
dispatch(new BulkSendSesMail($email, $users->pluck('id')->toArray(), $index === 0))
);
このため、ループするた度に、x秒遅延を増やすような処理を追記して対応
$users->chunk(50)->each(
fn (Collection $users, int $index) =>
// @phpstan-ignore-next-line
dispatch(new BulkSendSesMail($email, $users->pluck('id')->toArray(), $index === 0))
+ ->delay(now()->addSeconds(3 * $index))
);
送信が成功したステータスなのに、メールが届かない場合
上記の症状に見舞われたことがあった。結論としては、メール本文内で参照する変数をメール送信時に指定していなかったからである。
<!-- メール本文 -->
<p>{{title}}</p>
このようにタイトルが要求される場合にはちゃんと BulkEmailEntries
の template_data
で指定する必要がある。