Building a Serverless Lumen API with AWS Lambda and DynamoDB

Building a Serverless Lumen API with AWS Lambda and DynamoDBPietro IglioBlockedUnblockFollowFollowingMay 18In my previous post, I explained how to run Lumen Laravel serverless on AWS with a custom PHP runtime.

In this post I’ll show how to build a serverless, RESTful API in Lumen Laravel to add, delete, update, and retrieve sample movie data stored in Dynamodb.

This article is not meant to be an accurate step-by-step tutorial: I have only included the main steps to reach the goal.

In case you think I’m missing some fundamental details, please comment below.

ArchitectureLumen is a “micro-framework”, meaning it’s a smaller, faster, leaner version of Laravel, which is the full web application framework.

Lumen has the same foundation as Laravel, but is built for microservices.

AWS Lambda is a service that lets you run code without provisioning or managing servers.

AWS Lambda executes your code only when needed and scales automatically, from a few requests per day to thousands per second.

Although Lambda does not support PHP natively, it has been recently extended to support AWS Lambda runtime API and layers capabilities.

Because of this, it is possible to build a native PHP runtime for Lambda functions.

AWS DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.

It’s a fully managed, multiregion, multimaster database with built-in security, backup and restore, and in-memory caching for internet-scale applications.

PrerequisitesYou should have a recent version of PHP, Java Runtime Environment (JRE), and Composer.

JRE is needed to run DynamoDB locally.

It’s not a strict requirement, but I recommend testing the API locally before creating the serverless version.

I’m assuming you are familiar with basic REST concepts and the Lumen/Laravel framework.

The complete source code for this tutorial is available here.

You can download and inspect the whole package while reading the rest of this article.

Getting StartedThe easiest way to start is to clone my lambda-lumen-test project, which can be used as scaffolding for a Lumen application that can run serverless on AWS:git clone https://github.

com/code-runner-2017/lambda-lumen-test.

gitcd lambda-lumen-test/src/php/lumencomposer updatecp .

env.

example .

envNext, we download DynamoDB so that we can run it locally before using the AWS service.

You can download it here along with instructions to run it locally.

I recommend running the local DynamoDB instance on port 8001, to avoid conflicts with the Lumen app:java -Djava.

library.

path=.

/DynamoDBLocal_lib -jar DynamoDBLocal.

jar -port 8001AWS Deployment SetupWe customize the template.

yaml file in order to use PHP 7.

3 from Stackery and pass the environment variables that are used by the Lambda function:AWSTemplateFormatVersion: 2010-09-09Description: Sample API using DynamodbTransform: AWS::Serverless-2016-10-31Resources: phpserver: Type: AWS::Serverless::Function Properties: FunctionName: !Sub ${AWS::StackName}-phpserver Description: Sample API using Dynamodb CodeUri: src/php Runtime: provided Handler: index.

php MemorySize: 1024 Timeout: 30 Tracing: Active Layers: – !Sub arn:aws:lambda:eu-west-1:887080169480:layer:php73:2 Policies: AmazonDynamoDBFullAccess Events: api: Type: Api Properties: Path: /{proxy+} Method: ANY Environment: Variables: DYNAMODB_CONNECTION: aws_iam_role DYNAMODB_DEBUG: false DYNAMODB_REGION: eu-west-1In the provided template I’m using region eu-west-1.

You can change the last line to use another region.

Adding Required PackagesFor this tutorial, we’re going to use the baopham/laravel-dynamodb package that provides a simple API for accessing DynamoDB.

In addition, we’re adding the ramsey/uuid package, which generates universally unique identifiers (UUID) that we’ll use as primary keys for our DynamoDB items:$ composer require ramsey/uuid$ composer require baopham/dynamodb$ composer updateWe don’t need to add the AWS SDK for PHP as it’s already included in the base project.

In order to complete the setup, I have modified /lumen/app/bootstrap.

php to register the authentication provider and the DynamoDB service provider:$app->register(AppProvidersAuthServiceProvider::class);$app->register(BaoPhamDynamoDbDynamoDbServiceProvider::class);I’ve also modified the same file to load the DynamoDB configuration, stored in config/dynamodb.

php:$app->configure('dynamodb');Creating DynamoDB TablesYou have many options here:creating the table using the AWS DynamoDB console.

creating tables using the CloudFormation script.

using an artisan command that I have added to the tutorial source code.

In this tutorial I’m using the last option, although I recommend the second one on a real project.

You can view the table definition in the/src/php/lumen/database/dynamodb/tables.

php file:<?phpreturn [ [ 'TableName' => 'movies', 'AttributeDefinitions' => [ [ 'AttributeName' => 'id', 'AttributeType' => 'S', ], ], 'KeySchema' => [ [ 'AttributeName' => 'id', 'KeyType' => 'HASH', ], ], 'ProvisionedThroughput' => [ 'ReadCapacityUnits' => 10, 'WriteCapacityUnits' => 20, 'OnDemand' => false, ], ], // add further tables here];A custom artisan command is available to create tables in DynamoDB.

Before running it, you need to configure the environment to point to the right DynamoDB instance.

If you want to create the tables in the local instance first, you have to set these values in the .

env file:DYNAMODB_CONNECTION=localDYNAMODB_LOCAL_ENDPOINT=http://localhost:8001Now you can create the tables running this command:$ php artisan dynamodb:create-tablesand you can drop them running:$ php artisan dynamodb:delete-tablesIf you want to use the same command to create tables on the cloud AWS instance, you can set:DYNAMODB_CONNECTION=awsDYNAMODB_KEY=<your access key>DYNAMODB_SECRET=<your secret key>DYNAMODB_REGION=<your region>Defining API RoutesNow we are ready to define our routes.

This is what we need the API to do, along with the endpoint that we are going to define according to the REST style:Get all movies (GET /api/movies)Get one movie (GET /api/movies/<movieId>)Add a new movie (POST/api/movies)Edit a movie (PUT/api/movies)Delete a movie (DELETE /api/movies/<movieId>)In Lumen all routes are defined in the routes/web.

php file:$router->group(['prefix' => 'api'], function () use ($router) { $router->get('movies', ['uses' => 'MovieController@getAll']); $router->get('movies/{id}', ['uses' => 'MovieController@getById']); $router->post('movies', ['middleware' => 'auth', 'uses' => 'MovieController@create']); $router->delete('movies/{id}', ['middleware' => 'auth', 'uses' => 'MovieController@delete']); $router->put('movies/{id}', ['middleware' => 'auth', 'uses' => 'MovieController@update']);});The ['prefix' => 'api'] option is needed so that all endpoints are defined under /api.

The “auth” middleware (['middleware' => 'auth']) is used so that the “create”, “delete” and “update” methods can be invoked by authenticated users only.

Anonymous users can request the list of movies or details about a specific movie.

All routes invoke methods of the MovieController, explained in the next section.

Creating the ControllerIn the app/Http/Controllers dir, copy ExampleController.

php to MovieController and define the methods connected with the declared routes:<?phpnamespace AppHttpControllers;use AppModelsMovieModel;use IlluminateHttpRequest;use IlluminateSupportStr;class MovieController extends Controller{ public function getAll(MovieModel $model, Request $request) { } public function getById(MovieModel $model, string $id) { } public function create(Request $request) { } public function update(MovieModel $model, Request $request, string $id) { } public function delete(MovieModel $model, string $id) { }}For some methods, we are injecting a Request instance so we can access the HTTP request, for example to retrieve the POST body.

We’re also injecting a MovieModel instance, so we can easily access the DynamoDB table used to store movie data.

The model is defined as follows:class MovieModel extends DynamoDbModel{ // Set this to be able to mass assign attributes, eg.

->all() method protected $fillable = ['title', 'year']; protected $table = 'movies';}}Implementing the Controller LogicIn a real world implementation, it’s recommended to not code any logic into the controller: the controller should only route requests to services.

To keep this tutorial simple, however, I’m going to implement all the CRUD logic within the controller itself.

Let’s start with the getAll method, which maps the /api/movies endpoint.

We want to give the ability to retrieve all movies with an optional filter to match the movie title:public function getAll(MovieModel $model, Request $request) { $title = $request->input('title'); if ($title == null) { return $model->all(); } else { return $model->where('title', '=', $title)->get(); } }Next, the getById method:public function getById(MovieModel $model, string $id) { return $model->find($id);}And so on.

You can find the whole implementation here.

Enabling AuthenticationWe want to introduce a very simple authentication mechanism based on a secret token that must be provided either as a parameter or as a request header.

There are two options here:using Lumen authenticationusing AWS API Gateway authenticationThere are some differences.

If you use API GW authentication, the Lambda function is not invoked at all if the user is not providing the right credentials.

In addition, you can control access to the API defining IAM users within AWS.

In this tutorial, however, I’m going to use Lumen authentication, with a simple shared secret key between Lambda and the client.

In order to enable authentication, I have uncommented the following lines in the bootstrap/app.

php file:$app->routeMiddleware([ 'auth' => AppHttpMiddlewareAuthenticate::class,]);.

$app->register(AppProvidersAuthServiceProvider::class);and modified src/php/lumen/app/Providers/AuthServiceProvider.

php as follows:public function boot(){ $this->app['auth']->viaRequest('api', function ($request) { $apiToken = $request->input('api_token'); if ($apiToken === null) { $apiToken = $request->header('api_token'); } if ($apiToken && $apiToken == env('API_TOKEN')) { return new GenericUser(['id' => 1, 'name' => 'User']); } else { return null; } });}The code checks for the api_token in the input params or request headers, and compares the value against the API_TOKEN env variable.

In the environment setup used locally, the value is set to ‘secret’, whereas when you deploy to the cloud you can set the API_TOKEN value in the Lambda environment for stronger authentication.

The method returns a generic user with ID 1.

You can change this logic in a real project situation.

Environment SetupWe want to use the local DynamoDB instance when running locally, and the real AWS instance when running as Lambda.

Thus, we define 1 DYNAMODB_CONNECTION in the local .

env file.

When running as Lambda, we use the aws_iam_role value instead, which will override the .

env value.

We also need to set the local DynamoDB endpoint and the API_TOKEN, which we can override again to define the Lambda environment variables.

Therefore, we add the following lines to the .

env file:DYNAMODB_CONNECTION=localDYNAMODB_LOCAL_ENDPOINT=http://localhost:8001API_TOKEN=secretRunning it locallyLet’s try to run the API locally:php -S localhost:8000 -t publicTip: in case you want to set specific options for PHP, such as enabling some extensions, you can define your own php.

ini and pass to the command line:php -c php.

ini -S localhost:8000 -t publicWe can now use curl to invoke the API:# Create a moviecurl -s -X POST -H 'Content-Type:application/json' http://localhost:8000/api/movies?api_key=secret -d '{"title": "Blade Runner", "year": 1982, "director": "Ridley Scott"}'147c940b-a818-4421-9206-2a1bcdddce7e# Retrieve the moviecurl -s http://localhost:8000/api/movies/147c940b-a818-4421-9206-2a1bcdddce7e{"created_at":"2019-02-16T07:46:04+00:00","id":"147c940b-a818-4421-9206-2a1bcdddce7e","title":"Blade Runner","updated_at":"2019-02-16T07:46:04+00:00","year":1982,"director":"Ridley Scott"}# Retrieve all moviescurl -s http://localhost:8000/api/movies…and so on.

Deploying to AWSNow we’re ready to upload your API to AWS to create the Lambda function.

We need the following command line tools:the latest version of AWS CLI (Windows users can download it here)AWS SAM (available here)I’m not going through the details of installing and configuring AWS CLI and SAM.

I assume you are familiar with them, and you have already configured AWS CLI with your account credentials.

Create an S3 bucket “yourbucketname”, choosing any name you like.

Now, we’re ready to package and deploy the Lambda function:$ sam package — template-file template.

yaml — output-template-file serverless-output.

yaml — s3-bucket yourbucketname$ sam deploy –template-file serverless-output.

yaml –stack-name my-first-serverless-lumen-api –capabilities CAPABILITY_IAMReplace yourbucketname with the name of your S3 bucket.

A whole CloudFormation stack has been deployed, which has created and configured all services needed to get your API working, including API Gateway, Lambda, and DynamoDB.

Testing the Cloud APIWe need to retrieve the public API URL.

We log into the AWS console again and select “Services” > “Lambda” > “Functions”.

Then we click on “my-first-serverless-lumen-api”, then we click on “API Gateway” in the Designer view.

Once we click on “API Gateway,” the API Endpoint section appears in the lower part of the above figure.

It should be something like this:https://ua12345ai.

execute-api.

eu-west-1.

amazonaws.

com/Prod/{proxy+}Replace {proxy+} with “hello” and yourhostname with the value of your API Endpoint:https://yourhostname/Prod/moviesBefore invoking the API, we need to set the environment variablesDYNAMODB_CONNECTION=awsDYNAMODB_KEY=<your access key>DYNAMODB_SECRET=<your secret key>DYNAMODB_REGION=<your region>Now we can use same curl commands seen above to create, update, read, and delete movies.

ConclusionAs previously said, this article is meant to be a useful reference for developing AWS Lambda APIs that make use of DynamoDB.

I like the simplicity and elegance of Laravel/Lumen, and you can use them in serverless architectures without any particular limitation.

As a side note, in a real project I would recommend using Bref, which I am considering covering in a future article.

While the Stackery runtime is still the official AWS runtime for PHP, Matthieu Napoli and the rest of the team are doing an excellent job on serverless PHP.

Thanks for reading!.I hope you found this article helpful.

Please share your comments.

.

. More details

Leave a Reply