AWS LambdaでLaravelアプリケーションをコンテナイメージで動作させる例についての記事です。
Webアプリケーションではなく、定期実行するバッチやSQSのキューメッセージを処理するのを目的としています。
巷で人気のbrefイメージを使えば自前実装しないで済むのかもしれませんが、いろいろ環境を整えた自作イメージで動かしたい時はbootstrapを自作してしまうほうが手っ取り早いと思います。
手順を大分端折って説明していますので、ご容赦ください。
bootstrapの作成
まずはbootstrapディレクトリに下記の内容のスクリプトを作成します。
ファイル名は『lambda』とし、実行権限を付与します。
シバン行のphpのパスは適宜変更してください。
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
use GuzzleHttp\Client;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Illuminate\Contracts\Console\Kernel;
new class () {
private readonly string $baseUrl;
private readonly Client $client;
public function __construct()
{
$runtimeApi = getenv('AWS_LAMBDA_RUNTIME_API');
if ($runtimeApi === '') {
throw new LogicException('Missing Runtime API Server configuration.');
}
$this->baseUrl = "http://{$runtimeApi}/2018-06-01";
$this->client = new Client();
$this->handle();
}
/**
* @return void
*/
private function handle(): void
{
// CMD で渡されるコマンドライン引数からコマンド名を得る
$argv = $_SERVER['argv'];
if (count($argv) < 2) {
throw new LogicException('No command specified.');
}
/** @var Illuminate\Foundation\Application $app */
$app = require __DIR__.'/app.php';
$kernel = $app->make(Kernel::class);
while (true) {
[$invocationId, $payload] = $this->getNextRequest();
try {
$input = new ArgvInput([...$argv, $payload]);
$output = new BufferedOutput();
$result = $kernel->handle($input, $output);
if ($result !== 0) {
throw new RuntimeException($output->fetch());
}
$this->sendResponse($invocationId, $output->fetch());
} catch (Throwable $e) {
$this->handleFailure($invocationId, $e);
}
}
}
/**
* @return array{invocationId: string, payload: string}
*/
private function getNextRequest(): array
{
$url = "{$this->baseUrl}/runtime/invocation/next";
$response = $this->client->get($url);
$invocationId = $response->getHeaderLine('lambda-runtime-aws-request-id');
$payload = (string)$response->getBody();
return [$invocationId, $payload];
}
/**
* @param string $invocationId
* @param string $response
*
* @return void
*/
private function sendResponse(string $invocationId, string $response): void
{
$url = "{$this->baseUrl}/runtime/invocation/{$invocationId}/response";
$this->client->post($url, [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => $response,
]);
}
/**
* @param string $invocationId
* @param Throwable $exception
*
* @return void
*/
private function handleFailure(string $invocationId, Throwable $exception): void
{
$url = "{$this->baseUrl}/runtime/invocation/{$invocationId}/error";
$data = [
'errorType' => get_class($exception),
'errorMessage' => $exception->getMessage(),
'errorTrace' => $exception->getTrace(),
];
$payload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->client->post($url, [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => $payload,
]);
}
};
リクエストを受け取った時にどのような処理をさせるかについては、artisanコマンドで指定する時と同じコマンド名を、このスクリプトの実行パラメータに指定してやれば良いようになっています。
今回は一つのLambda関数毎に単一の処理をさせる前提なのでこのようにしていますが、若干の修正をすれば受信したリクエストの内容に応じて異なる処理を実行することも容易でしょう。
Lambda用Commandスクリプトを作る
CLIからartisanコマンドで実行するための仕組みを利用して、Lambda用のハンドラーを作成します。
以下はSQSをイベントソースとしてキューメッセージを処理する例ですが、他のトリガーを利用する場合では返却するレスポンスの書式がまた異なったりするので、必要に応じて適宜変更が必要です。
また、$this->output
を結果の返却に使っているので、ログに記録するために標準出力を使いたい場合は、別途でConsoleOutput
インスタンスを用意するなどの工夫が必要です。
<?php
namespace App\Console\Commands\Lambda;
use Illuminate\Console\Command;
use function json_decode;
use function json_encode;
/**
* @package App\Console\Commands\Lambda
*/
class ExampleHandler extends Command
{
protected string $signature = 'lambda:example {event}';
protected $description = 'Lambda用SQSキューメッセージ処理コマンドの例';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$event = json_decode($this->argument('event'), true);
$result = $this->exec($event);
$this->output->write($result);
return 0;
}
/**
* @param array $event
*
* @return string
*/
private function exec(array $event): string
{
foreach ($event['Records'] as $record) {
// メッセージの処理
}
// レスポンスの書式は適宜変更
return json_encode([
'batchItemFailures' => [],
]);
}
}
コンテナイメージの作成
Dockerfile作成例です。
先述のbootstrap/lambdaスクリプトをENTRYPOINT
に指定して以下のような感じでよしなに作ってください。
大抵のケースでAWS Parameter and Secrets Lambda extension
なども必要になると思いますが、それについての説明は割愛します。
FROM php:8.2-cli
# このへんでライブラリやLambda拡張等を導入
COPY . /app
RUN chmod 755 /app/bootstrap/lambda
ENTRYPOINT [ "/app/bootstrap/lambda" ]
RIEを導入する場合
ローカルデバッグ等でRIE(Runtime Interface Emulator)を導入する場合は、まずlambda-entrypointファイルを新規作成します。
#!/bin/bash -eux
readonly lambda_handler=/app/src/bootstrap/lambda
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
exec /usr/bin/aws-lambda-rie ${lambda_handler} "$@"
fi
exec ${lambda_handler} "$@"
環境変数AWS_LAMBDA_RUNTIME_API
が存在しなければRIEを起動するようにしています。
Dockerfileは以下のように加筆・修正します。
# Runtime Interface Emulator
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
COPY --chmod=755 lambda-entrypoint /usr/local/bin/lambda-entrypoint
ENTRYPOINT [ "lambda-entrypoint" ]
AWS Lambdaの設定
先のDockerfileで作成したイメージからLambda関数を作成し、イメージのCMDを先ほど作成したコマンド名で上書きします。
他はよしなに設定してください。
あとは動作確認しておしまいです。
さいごに
ローカル環境ではLocalStack上で動作確認したいと思いますが、無料枠ではコンテナイメージでLambdaを使用することができないので注意が必要です。
無料枠でなんとかしたい場合は、一旦適当なランタイムでプロキシ関数を作り、それを経由してRIEを叩くという間接的なアプローチが有効です。
下記ページが参考になると思います。
以上!
コメント