AWSPHP

Laravelでbrefを使わないCustom Runtimeを作る

AWS LambdaでLaravelアプリケーションをコンテナイメージで動作させる例についての記事です。
Webアプリケーションではなく、定期実行するバッチやSQSのキューメッセージを処理するのを目的としています。
巷で人気のbrefイメージを使えば自前実装しないで済むのかもしれませんが、いろいろ環境を整えた自作イメージで動かしたい時はbootstrapを自作してしまうほうが手っ取り早いと思います。

本記事の内容はPHP8.2、Laravel10.0において有効性を確認しています。

手順を大分端折って説明していますので、ご容赦ください。

スポンサーリンク

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関数毎に単一の処理をさせる前提なのでこのようにしていますが、若干の修正をすれば受信したリクエストの内容に応じて異なる処理を実行することも容易でしょう。

surface0
管理人
ちなみに、これを書くにあたって、どこかのウェブ記事のコードを参考にさせていただいたのですが、参考元についてはどこかわからなくなってしまったため割愛させていただきます。

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" ]
『AWS Parameter and Secrets Lambda extension』は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を先ほど作成したコマンド名で上書きします。
他はよしなに設定してください。

Lambdaイメージ設定

あとは動作確認しておしまいです。

さいごに

ローカル環境ではLocalStack上で動作確認したいと思いますが、無料枠ではコンテナイメージでLambdaを使用することができないので注意が必要です。
無料枠でなんとかしたい場合は、一旦適当なランタイムでプロキシ関数を作り、それを経由してRIEを叩くという間接的なアプローチが有効です。
下記ページが参考になると思います。

LocalStack を使って dockerise した Lambda を実行する方法 - Qiita
はじめに AWS上で動かしているアプリをどうにかしてローカルで実行できないかと思って 色々試してみてどうにか Lambda を動かすことができたのでそのまとめ。 LocalStack を使った構成 使っている技術など Dock...

以上!

スポンサーリンク
管理人

システムえんじにゃー🐈
趣味はエレキギターなど。作曲したい。
WoWs/プリコネ
記事に関する質問はお気軽にどうぞ。

surface0 (さーふぇす)をフォローする
surface0 (さーふぇす)をフォローする
Rain or Shine

コメント

タイトルとURLをコピーしました