Deploying Laravel Artisan on AWS Lambda

July 14, 2019

Introduction

Working with Laravel it's only natural to need artisan for a few tasks. Let it be custom commands tailored to your application or default commands like php artisan migrate, knowing how to use cli-based scripts on Lambda is an important step to gain confidence to go serverless with a project. One might argue that you can run migrate from your own local machine, but if your database is locked behind AWS VPC with private connectivity only you'll have to rely on a tunnel (bastion, jumpbox) so that your local machine connects to an instance running within a trusted location to your private RDS. Where I work, we enforce a security rule that a bastion host will only be created in emergency situations and shall be discarded as soon as possible. Furthermore, not everybody on the development team have access to our AWS Production Account. Running database migrations, on the other hand, can be part of a daily or weekly release and the security requirement would make it a constant annoyance if we wanted to run it from our local machines.

Running Artisan from a Lambda Function is a great mechanism to maintain the security rules as well as allowing developers to keep their autonomy for releases.

Implementation

The AWS SAM template for the Lambda Function will look like the following snippet.

  Listener:
    Type: AWS::Serverless::Function
    Properties:
      Role: !GetAtt LambdaExecutionRole.Arn
      CodeUri: .
      Handler: artisan
      Timeout: 60
      MemorySize: 1024
      Environment:
        Variables:
          ARTISAN_COMMAND: 'my:command'
      Layers:
        - !Sub "arn:aws:lambda:${AWS::Region}:209497400698:layer:php-73:6"
      Runtime: provided
      VpcConfig:
        SecurityGroupIds: [!ImportValue DefaultSecurityGroup]
        SubnetIds: !Split [',', !ImportValue PrivateSubnets]

The important aspects to notice in this snippet are the Handler pointing to the artisan file, the environment variable defining the command signature to invoke, the VPC configuration that grants the ability to establish a connection to the RDS and the layer.

My first (and most obvious) attempt at running artisan commands started by defining a handler such as artisan migrate. The innocent child inside of me got hit hard by a validation rule on the handler attribute that states that a (space character) is not allowed. My workaround to it brings us to the environment variable and changing the the artisan file slightly to accommodate. This is the change made:

$command = $_ENV['ARTISAN_COMMAND'] ?? null;

$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput($command ? ['lambda', $command] : null),
    new Symfony\Component\Console\Output\ConsoleOutput
);

When the Lambda Function gets invoked, the environment variable will be used to indicate to the Laravel Kernel which command is being executed.

The Layer comes from Bref which provides the PHP binary necessary to run PHP on a Lambda Function.

Next relevant part is the command itself. Here's an example of one:

<?php

namespace App\Modules\MyModule\Console;

use Illuminate\Console\Command;

final class TestCommand extends Command
{
    protected $signature = 'my:command';

    public function handle(): void
    {
        $callback = function (array $event) {
            logger()->info(json_encode($event));
        };
        
        lambda($callback);
    }
}

The lambda() global function comes from Bref as well. You can install it with composer require bref/bref and the function will be available. The $event array will contain any parameters sent to the Lambda during the invocation.

It's important to note that you shouldn't register any Laravel-provided commands directly with this approach. Instead, you should write a wrapper that calls the lambda() helper. This is because AWS will only consider your execution successful if there is a callback to the internal services letting AWS know that your execution was successful. Bref does this for us once the registered callback finishes. Here is an example of how to run migrations on your project:

<?php

namespace App\Modules\Migrations\Console;

use Illuminate\Console\Command;
use Illuminate\Database\Console\Migrations\MigrateCommand;
use Illuminate\Foundation\Console\Kernel;

final class LambdaMigrateCommand extends Command
{
    protected $signature = 'lambda:migrate';

    public function handle(Kernel $artisan): void
    {
        $callback = function (array $event) use ($artisan) {
            $artisan->call(MigrateCommand::class);
        };

        lambda($callback);
    }
}

Conclusion

Bref does most of the heavy-lifting for us when running PHP on AWS Lambda. With minimum changes, we're able to let Artisan bootstrap our Laravel app the way it always does and take full advantage of the container dependency injection on an Artisan-registered command. The only thing to always keep an eye is the use of lambda() helper so that Bref can notify Lambda of the successful execution process. Without it, your function will be in an infinite bootstrap state until your Lambda timeout is reached.

Hope this post helps any artisans out there. Don't forget to hit me up on Twitter with any follow-up.

Cheers.