もう結構前のことですが、AWS Lambda に zip ではなくコンテナイメージでデプロイ出来るようになったとのことです。
ので試してみました。残骸はこちら。
コンテナイメージは AWS から提供されている Amazon Linux2 ベースのものをカスタマイズすると簡単に作成できます。
あるいは Lambda Runtime Interface Clients を使えば自前の debian や alpine ベースのイメージでも実装できます。例えば NodeJS だと次の npm モジュールを使って実装できます。
ただ、AWS 提供のコンテナイメージも、Lambda Runtime Interface Clients も、PHP の実装はありません。PHPer なので PHP で試したいので、PHP 用のカスタムランタイムのコンテナイメージを作成しました。
PHP 用のカスタムランタイムのコンテナイメージ
カスタムランタイムは次の API を使用して実装します。
PHP 用に次のスクリプトを bootstrap というファイル名で作成しまいた。
<?php require __DIR__ . '/../vendor/autoload.php'; new class () { private string $baseUrl; public function __construct() { $runtimeApi = getenv('AWS_LAMBDA_RUNTIME_API'); if (strlen($runtimeApi) == 0) { throw new LogicException('Missing Runtime API Server configuration.'); } $this->baseUrl = "http://$runtimeApi/2018-06-01"; // CMD で渡されるコマンドライン引数からハンドラ名を得る $argv = $_SERVER['argv']; if (count($argv) < 2) { throw new LogicException('No handler specified.'); } $appRoot = getcwd(); $handlerName = $argv[1]; // ハンドラ名をファイルとして require 戻り値をクロージャーとして得る $function = require "$appRoot/$handlerName"; do { list ($invocationId, $payload) = $this->getNextRequest(); try { // クロージャーを実行 $response = $function($payload); $this->sendResponse($invocationId, $response); } catch (Throwable $ex) { $this->handleFailure($invocationId, $ex); } } while (true); } private function getNextRequest(): array { $url = "$this->baseUrl/runtime/invocation/next"; $client = new GuzzleHttp\Client(); $response = $client->get($url); $invocationId = $response->getHeaderLine('lambda-runtime-aws-request-id'); $payload = json_decode($response->getBody(), true); return [$invocationId, $payload]; } private function sendResponse(string $invocationId, $response): void { $url = "$this->baseUrl/runtime/invocation/$invocationId/response"; $payload = json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $client = new GuzzleHttp\Client(); $client->post($url, [ 'headers' => [ 'Content-Type' => 'application/json', ], 'body' => $payload, ]); } private function handleFailure(string $invocationId, Throwable $exception): void { $url = "$this->baseUrl/runtime/invocation/$invocationId/error"; $data = [ 'errorType' => get_class($exception), 'errorMessage' => $exception->getMessage(), ]; $payload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $client = new GuzzleHttp\Client(); $client->post($url, [ 'headers' => [ 'Content-Type' => 'application/json', ], 'body' => $payload, ]); } };
コマンドラインで指定されたファイルを require してそのファイルが返したクロージャーを実行しています。require するファイルは次の要領で作成します。
<?php return function ($payload) { // なにかする return $result; };
イメージのための Dockerfile は次のような内容です。ENTRYPOINT で前述の bootstrap を実行し、CMD で require するファイル名を指定します。
FROM php:alpine COPY --from=composer /usr/bin/composer /usr/bin/composer COPY composer.* /home/app/ WORKDIR /home/app/ RUN composer install --prefer-dist --no-dev --no-progress -o -a COPY bin/ /home/app/bin/ COPY handlers/ /home/app/handlers/ ENTRYPOINT [ "/home/app/bin/bootstrap" ] RUN chmod +x /home/app/bin/bootstrap CMD [ "handlers/index.php" ]
なお Lambda の定義時に ENTRYPOINT や CMD はオーバーライドできます。例えば次のように Terraform テンプレートで指定できます(この例では Dockerfile で指定してるとおりなので意味ないですが)。
resource "aws_lambda_function" "func" { function_name = "${var.tag}-func" role = aws_iam_role.lambda.arn timeout = 10 memory_size = 128 package_type = "Image" image_uri = "${aws_ecr_repository.php.repository_url}:${var.docker_tag}" image_config { command = ["handlers/index.php"] entry_point = ["/home/app/bin/bootstrap"] working_directory = "/home/app/" } }
なので Web アプリケーション用に作ったコンテナイメージに Lambda 用の bootstrap を仕込んでおき、デフォルトの ENTRYPOINT と CMD は Web アプリケーション用、Lambda の定義時には ENTRYPOINT や CMD を差し替える、といったことが可能です(Web アプリケーション用と Lambda 用に別のイメージを作る必要は無い)。
Lambda Runtime Interface Emulator でローカル実行
Lambda Runtime Interface Emulator でローカル環境でのテスト実行が可能です。
Lambda Runtime Interface Emulator は aws-lambda-rie
というワンバイナリの実行可能ファイルです。あらかじめイメージに Lambda Runtime Interface Emulator を仕込んでおいても良いし、あるいは実行時にホストから差し込んでも OK です。
# ホストから aws-lambda-rie を差し込んで実行する例 wget https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie chmod +x ./aws-lambda-rie docker build -t ore-no-image . docker run --rm -p 9000:8080 \ -v "$PWD/aws-lambda-rie:/aws-lambda-rie:ro" \ --entrypoint /aws-lambda-rie ore-no-image \ bin/bootstrap handlers/index.php
次のように Lambda が実行できます。
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"hello":"php"}' #=> {"statusCode":200,"headers":{"Content-Type":"text/plain","x-php-version":"8.0.6"},"body":{"payload":{"hello":"php"}}}
さいごに
試しに PHP でやってみましたが、あえて AWS Lambda で PHP を動かすことはまず無いと思います。 (Lambda Runtime Interface Clients の PHP 実装も出来て Packagist で公開されれば話は別かもしれないですが)
Lambda 以外でも使用しているイメージを Lambda に流用したい、というケースが多いと思うので(例えば ECS Service で実行する Web アプリケーションのイメージを Lambda でも使いたい、とか)、 AWS 提供のイメージをカスタマイズするよりは、Docker Hub のオフィシャルのイメージから作成した独自のイメージに Lambda Runtime Interface Clients を入れる、という形で、構築が容易で素早く開始できる ECS Run Task のような感覚で使えそうです。