PHP Laravelをサーバーレス化する方法として、AWS SAMを使用した手順はAmazon Web Services ブログで公開されています。
今回はコンテナイメージ化することで、より簡単にAWS Lambda
上で動かす手順を記載します。
前提
PHPをサーバーレス化するための必須ライブラリ
Laravel用Package
Laravelのセットアップ
Laravelインストーラー
composer global require laravel/installer
プロジェクト作成
laravel new laravel-function
ログ出力先として標準出力の追加
...
'channels' => [
...
'stdout' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stdout',
],
],
],
...
必須ライブラリの追加
bref
composer require bref/laravel-bridge --update-with-dependencies
worker.php
の作成
SQSからイベントメッセージを受信するためのエントリーポイントを作成
php artisan vendor:publish --tag=serverless-worker
aws
composer require aws/aws-sdk-php-laravel
composer require aws/aws-sdk-php
config/aws.php
の作成
php artisan vendor:publish --provider="Aws\Laravel\AwsServiceProvider"
AWS用プロバイダ設定追加
'providers' => [
...
Aws\Laravel\AwsServiceProvider::class,
...
],
'aliases' => [
...
'AWS' => Aws\Laravel\AwsFacade::class,
]
Credentials
defaultの認証情報を利用するため、不要な設定は削除
...
'sqs' => [
'driver' => 'sqs',
// 'key' => env('AWS_ACCESS_KEY_ID'),
// 'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
...
処理の実装
API
...
// API 例
Route::get('/greeting', function () {
return 'Hello World';
});
// 非同期Job呼び出し用
Route::get('/job', function (Request $request) {
$message = $request->get("msg");
\App\Jobs\SimpleJob::dispatch($message);
return 'OK';
});
Job
SQS経由で非同期に実行するJob
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class SimpleJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/** @var string */
private $message;
public function __construct(string $message)
{
$this->message = $message;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Log::info('Message: ' . $this->message);
}
}
ローカル実行環境
localstack
localstackの初期化用シェルを作成
#!/bin/sh
awslocal sqs create-queue --queue-name localstack_dead_letter_queue
awslocal sqs create-queue --queue-name LaravelQueue \
--attributes '{"RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:ap-northeast-1:000000000000:localstack_dead_letter_queue\",\"maxReceiveCount\":3}"}'
# Lambdaの作成
awslocal lambda create-function \
--function-name 'laravel-lambda-job-bridge' \
--runtime=provided.al2 \
--role=dummyrole \
--handler=worker.php
awslocal lambda create-event-source-mapping \
--event-source-arn arn:aws:sqs:ap-northeast-1:000000000000:LaravelQueue \
--function-name "laravel-lambda-job-bridge"
xdebug
設定ファイルを作成
[xdebug]
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
デバッグ用Dockerfile
- API用
FROM bref/php-81-fpm-dev
COPY --from=bref/extra-redis-php-81:0.11.19 /opt /opt
COPY --from=bref/extra-xdebug-php-81:0.11.19 /opt /opt
COPY docker/php/xdebug.ini /opt/bref/etc/php/conf.d/zz-xdebug.ini
COPY --from=composer:2.1.12 /usr/bin/composer /usr/bin/composer
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
- JOB用
FROM bref/php-81
COPY --from=bref/extra-redis-php-81:0.11.19 /opt /opt
COPY --from=bref/extra-xdebug-php-81:0.11.19 /opt /opt
COPY docker/php/xdebug.ini /opt/bref/etc/php/conf.d/zz-xdebug.ini
COPY --from=composer:2.1.12 /usr/bin/composer /usr/bin/composer
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
docker-compose
version: "3.5"
services:
web:
image: bref/fpm-dev-gateway
ports:
- '8000:80'
volumes:
- .:/var/task
links:
- php
environment:
HANDLER: public/index.php
DOCUMENT_ROOT: public
php:
build:
context: .
dockerfile: debug.Dockerfile
env_file:
- .env.example
environment:
LOG_CHANNEL: stdout
QUEUE_CONNECTION: sqs
SQS_PREFIX: 'http://localstack:4566/000000000000'
SQS_QUEUE: LaravelQueue
AWS_DEFAULT_REGION: ap-northeast-1
XDEBUG_MODE: develop,debug
PHP_IDE_CONFIG: serverName=docker
volumes:
- ~/.aws:/.aws
- .:/var/task:ro
# sqs
localstack:
image: localstack/localstack:latest
environment:
HOSTNAME_EXTERNAL: localstack
DEFAULT_REGION: ap-northeast-1
DATA_DIR: /tmp/localstack/data
SERVICES: sqs
ports:
- "4566:4566"
volumes:
- ./docker/sqs:/docker-entrypoint-initaws.d
job:
build:
context: .
dockerfile: jobs.debug.Dockerfile
depends_on:
- localstack
env_file:
- .env.example
environment:
LOG_CHANNEL: stdout
QUEUE_CONNECTION: sqs
SQS_PREFIX: 'http://localstack:4566/000000000000'
SQS_QUEUE: LaravelQueue
AWS_CONFIG_FILE: /.aws/config
AWS_SHARED_CREDENTIALS_FILE: /.aws/credentials
AWS_DEFAULT_REGION: ap-northeast-1
XDEBUG_MODE: develop,debug
PHP_IDE_CONFIG: serverName=docker
entrypoint:
- php
- artisan
- queue:work
volumes:
- ~/.aws:/.aws
- .:/var/task:ro
ローカル実行
docker-compose up -d
動作確認
> curl 'http://localhost:8000/api/greeting'
Hello World
> curl 'http://localhost:8000/api/job?msg=Success'
OK
Dockerイメージの作成
Rest API用
-
Dockerfile
-
bref/php-81-fpm
を使用 - Redisを利用する場合、brefphp/extra-php-extensionsから
extra-redis-php-81
を使用- 今回は例示として追加
-
FROM bref/php-81-fpm
COPY --from=bref/extra-redis-php-81:0.11.18 /opt /opt
COPY . /var/task
CMD [ "public/index.php" ]
docker build -t {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/laravel-api-function:latest .
Job 用
-
jobs.Dockerfile
-
bref/php-81
を使用 - Redisを利用する場合、brefphp/extra-php-extensionsから
extra-redis-php-81
を使用- 今回は例示として追加
-
FROM bref/php-81
COPY --from=bref/extra-redis-php-81:0.11.18 /opt /opt
COPY . /var/task
CMD [ "worker.php" ]
docker build -f jobs.Dockerfile -t {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/laravel-jobs-function:latest .
ECR
dockerイメージを保存するリポジトリを作成
aws ecr create-repository --repository-name laravel-api-function --region ap-northeast-1
aws ecr create-repository --repository-name laravel-jobs-function --region ap-northeast-1
dockerイメージをリポジトリにプッシュ
- ECRログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com
- リポジトリにプッシュ
docker push {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/laravel-api-function:latest
docker push {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/laravel-jobs-function:latest
SQS
動作確認用のSQSキューを作成
aws sqs create-queue --queue-name LaravelQueue
デプロイ
CloudFormation
API Gateway
からLambda Function
を起動する
- CloudFormationファイルの作成
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
LogGroupName:
Type: String
Default: /aws/api-gateway/laravel
FunctionName:
Type: String
Default: laravel-api-function
QueueName:
Type: String
Default: LaravelQueue
Resources:
ApiFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Ref FunctionName
MemorySize: 1024
Timeout: 300
Code:
ImageUri: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/laravel-api-function:latest'
ImageConfig:
Command:
- public/index.php
PackageType: Image
Environment:
Variables:
APP_ENV: development
LOG_LEVEL: debug
LOG_CHANNEL: stdout
QUEUE_CONNECTION: sqs
SQS_PREFIX: !Sub 'https://sqs.${AWS::Region}.amazonaws.com/${AWS::AccountId}'
SQS_QUEUE: LaravelQueue
Role: !GetAtt Role.Arn
Role:
Type: AWS::IAM::Role
Properties:
RoleName: RoleForLaravelApiFunction
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Sid: ""
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: LaravelApiFunctionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
- Effect: Allow
Action:
- sqs:SendMessage
- sqs:GetQueueAttributes
Resource:
- !Sub 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${QueueName}'
HttpAPI:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: !Sub '${AWS::StackName}-apigw'
ProtocolType: HTTP
DefaultStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
StageName: $default
ApiId: !Ref HttpAPI
AutoDeploy: true
AccessLogSettings:
DestinationArn: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${LogGroupName}'
Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }'
DependsOn:
- ApiGatewayLog
ApiGatewayLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref LogGroupName
RetentionInDays: 3
GetGreeting:
Type: AWS::ApiGatewayV2::Route
Properties:
RouteKey: "GET /api/greeting"
ApiId: !Ref HttpAPI
Target: !Join
- "/"
- - "integrations"
- !Ref GETIntegration
GetJob:
Type: AWS::ApiGatewayV2::Route
Properties:
RouteKey: "GET /api/job"
ApiId: !Ref HttpAPI
Target: !Join
- "/"
- - "integrations"
- !Ref GETIntegration
GETIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref HttpAPI
PayloadFormatVersion: "2.0"
IntegrationType: AWS_PROXY
IntegrationMethod: GET
IntegrationUri: !GetAtt ApiFunction.Arn
Permission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref ApiFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub
- 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpAPI}/*'
- HttpAPI: !Ref HttpAPI
- CloudFormationデプロイ
aws cloudformation deploy --template api.yaml --stack-name laravel-api-function --capabilities CAPABILITY_NAMED_IAM
SQS
をトリガーに起動する
- CloudFormationファイルを作成
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
FunctionName:
Type: String
Default: laravel-jobs-function
QueueName:
Type: String
Default: LaravelQueue
Resources:
JobFunction:
Type: "AWS::Lambda::Function"
Properties:
FunctionName: !Ref FunctionName
MemorySize: 1024
Code:
ImageUri: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/laravel-jobs-function:latest'
ImageConfig:
Command:
- worker.php
PackageType: Image
Environment:
Variables:
APP_ENV: development
LOG_LEVEL: debug
LOG_CHANNEL: stdout
QUEUE_CONNECTION: sqs
SQS_PREFIX: !Sub 'https://sqs.${AWS::Region}.amazonaws.com/${AWS::AccountId}'
SQS_QUEUE: !Ref QueueName
Role: !GetAtt Role.Arn
Role:
Type: AWS::IAM::Role
Properties:
RoleName: RoleForLaravelJobFunction
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Sid: ""
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: LaravelJobFunctionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
- Effect: Allow
Action:
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource:
- !Sub 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${QueueName}'
SQSEvent:
Type: AWS::Lambda::EventSourceMapping
Properties:
BatchSize: 10
EventSourceArn: !Sub 'arn:aws:sqs:ap-northeast-1:${AWS::AccountId}:${QueueName}'
FunctionName: !GetAtt JobFunction.Arn
Enabled: true
- CloudFormationデプロイ
aws cloudformation deploy --template jobs.yaml --stack-name laravel-jobs-function --capabilities CAPABILITY_NAMED_IAM
動作確認
ApiEndpointの取得
> aws apigatewayv2 get-apis
{
"Items": [
{
"ApiEndpoint": "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
"ApiId": "xxxxxxxxxx",
"ApiKeySelectionExpression": "$request.header.x-api-key",
"CreatedDate": "2021-12-02T00:00:00+00:00",
"DisableExecuteApiEndpoint": false,
"Name": "laravel-api-function-apigw",
"ProtocolType": "HTTP",
"RouteSelectionExpression": "$request.method $request.path",
"Tags": {}
}
]
}
apiの確認
> curl '{ApiEndpoint}/api/greeting'
Hello World
- レスポンスとして
Hello World
が表示される
jobの動作確認
> curl '{ApiEndpoint}/api/job?msg=Success'
OK
- レスポンスとして
OK
が表示される -
CloudWatch Logs
の/aws/lambda/laravel-jobs-function
にMessage: Success
のログが出力される
まとめ
PHP Laravel
のコンテナイメージ化を行い、AWS Lambda
にデプロイ、動作確認するまでの手順を簡単に記載しました。
このまま業務に使用できるものではなく、実際にはVPCやRDSProxy等に接続するための設定、実装が必要になるでしょう。
また、Jobを非同期化するとしてもイベントメッセージがPHPに依存したJobクラスのシリアライズデータとなるため抽象化も難しく、APIとJobで実装をきれいに分けるのも困難です。
そのため、最適解ではありませんが、既存システムがPHP Laravel
に依存しており、エンジニアに別の言語やフレームワーク等への移行を求める余裕がない場合、あるいはマイクロサービス化ほどの大げさなことにはしたくない場合、負荷を分散させる方法として手段の一つにはなると思います。