16
3

More than 1 year has passed since last update.

Laravel de Lambda

Last updated at Posted at 2021-12-01

PHP Laravelをサーバーレス化する方法として、AWS SAMを使用した手順はAmazon Web Services ブログで公開されています。
今回はコンテナイメージ化することで、より簡単にAWS Lambda上で動かす手順を記載します。

前提

PHPをサーバーレス化するための必須ライブラリ

Laravel用Package

Laravelのセットアップ

Laravelインストーラー

composer global require laravel/installer

プロジェクト作成

laravel new laravel-function

ログ出力先として標準出力の追加

config/logging.php
    ...
    '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用プロバイダ設定追加

config/app.php
    'providers' => [
        ...
        Aws\Laravel\AwsServiceProvider::class,
        ...
    ],

    'aliases' => [
        ...
        'AWS' => Aws\Laravel\AwsFacade::class,
    ]

Credentials

defaultの認証情報を利用するため、不要な設定は削除

config/queue.php
...
        '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

routes/api.php
...
// 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

app/Jobs/SimpleJob.php
<?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の初期化用シェルを作成

docker/sqs/init.sh
#!/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

設定ファイルを作成

docker/php/xdebug.ini
[xdebug]
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log

デバッグ用Dockerfile

  • API用
debug.Dockerfile
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用
jobs.debug.Dockerfile
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

docker-compose.yml
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を使用
      • 今回は例示として追加
Dockerfile
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を使用
      • 今回は例示として追加
jobs.Dockerfile
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イメージを保存するリポジトリを作成

laravel-api-function
aws ecr create-repository --repository-name laravel-api-function --region ap-northeast-1
laravel-jobs-function
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
  • リポジトリにプッシュ
laravel-api-function
docker push {AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/laravel-api-function:latest
laravel-jobs-function
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ファイルの作成
api.yaml
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ファイルを作成
jobs.yaml
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の確認

/api/greeting
> curl '{ApiEndpoint}/api/greeting'
Hello World
  • レスポンスとしてHello Worldが表示される

jobの動作確認

/api/job
> curl '{ApiEndpoint}/api/job?msg=Success'
OK
  • レスポンスとしてOKが表示される
  • CloudWatch Logs/aws/lambda/laravel-jobs-functionMessage: Successのログが出力される

まとめ

PHP Laravelのコンテナイメージ化を行い、AWS Lambdaにデプロイ、動作確認するまでの手順を簡単に記載しました。
このまま業務に使用できるものではなく、実際にはVPCやRDSProxy等に接続するための設定、実装が必要になるでしょう。
また、Jobを非同期化するとしてもイベントメッセージがPHPに依存したJobクラスのシリアライズデータとなるため抽象化も難しく、APIとJobで実装をきれいに分けるのも困難です。
そのため、最適解ではありませんが、既存システムがPHP Laravelに依存しており、エンジニアに別の言語やフレームワーク等への移行を求める余裕がない場合、あるいはマイクロサービス化ほどの大げさなことにはしたくない場合、負荷を分散させる方法として手段の一つにはなると思います。

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3