DEV Community

Cover image for Superpower REST API DX with Serverless ⚡ and DevOps Best Practices on AWS (🐘 PHP Version)
Davide De Sio
Davide De Sio

Posted on • Edited on

Superpower REST API DX with Serverless ⚡ and DevOps Best Practices on AWS (🐘 PHP Version)

⚡ Enhance the Developer Experience (DX) with Serverless and DevOps Best Practices

In the previous article of this series, I delved into the strategies for turbocharging your development lifecycle through the use of serverless architecture and DevOps best practices. I highly recommend reading it before proceeding, as it provides essential insights into several concepts that are foundational for understanding the subsequent discussions.

While all my code samples were crafted using Node.js within the stack, I consistently emphasized that the choice of language for the stack's components was non-essential. DevOps pillars, indeed, are applicable regardless of the specific technologies involved.

🇧 Bref

In this new article, I aim to demonstrate that by transitioning to a different stack language, such as PHP, which is neither natively supported on AWS Lambda (our serverless compute AWS service), the concepts discussed earlier remain entirely valid and applicable.

To deploy PHP on AWS Lambda, we can leverage the fantastic open source project Bref by Matthieu Napoli, an AWS Serverless Hero renowned for his extensive expertise in deploying PHP applications with serverless architecture on AWS.

I highly recommend taking a look at Bref, as it provides a straightforward solution for deploying your Laravel and Symfony projects in a serverless environment (this could potentially be a story for another article)

🐘 Switch runtime to PHP

Bref provides us PHP runtimes for AWS Lambda,based on Docker containers. Let's use it.

name: aws
deploymentMethod: direct
# Block public access on deployment bucket
deploymentBucket:
  blockPublicAccess: true
# The AWS region in which to deploy (us-east-1 is the default)
region: ${env:AWS_REGION}
runtime: provided.al2 # or provided.al2023
environment:
  # environment variables
  APP_ENV: ${env:APP_ENV}
  DB_HOST: ${env:DB_HOST}
  DB_DATABASE: ${env:DB_DATABASE}
  DB_USERNAME: ${env:DB_USERNAME}
  DB_PASSWORD: ${env:DB_PASSWORD}
## VPC Configuration
vpc:
  securityGroupIds:
    - ${env:SG1}
  subnetIds:
    - ${env:SUBNET1}
    - ${env:SUBNET2}
    - ${env:SUBNET3}
# Enable lambda tracing with xray
tracing:
  lambda: true
layers:
  - ${bref:layer.php-81}

Enter fullscreen mode Exit fullscreen mode

Essentially, we can utilize the same configuration that we employed for Node.js, with the only adjustments being replacing the runtime with provided.al2 and including the php-81 Bref layer.

🚀 Local developement

Bref offers a convenient local development stack built on Docker, which simulates AWS API Gateway for routing and AWS Lambda for compute.
While this setup is straightforward when deploying Laravel and Symfony applications (because routing logic is handled in our PHP app), the scenario we're addressing involves a more advanced use case. As detailed here, we're aiming to deploy a setup where API Gateway routes to atomic functions in response to HTTP events.

Fortunately, Bref offers a solution for this advanced use case through its dev-server package. However, I've discovered that this package hasn't been updated for some time and does not support both API Gateway V1 and V2 architectures.

While it's tempting to lament the absence of certain features, it's important to acknowledge the immense effort required to maintain a complex open-source project like Bref. Rather than dwelling on shortcomings, it's more constructive to express gratitude for the existence of Bref and to actively contribute to updating and improving the package ourselves.

I've patched it by forking the repository and submitting a PR with a comprehensive description of my commits, including extended tests to ensure retro-compatibility. My aim was to contribute something beneficial to the Bref community.

Ultimately, I can include my fork in my composer.json to utilize it as my development server (insted of serverless-offline which does not support PHP).

{
    "require": {
        "bref/bref": "^2.1",
        "league/openapi-psr7-validator": "^0.22.0"
    },
    "require-dev": {
        "bref/dev-server": "dev-master",
        "phpunit/phpunit": "^10.5"
    },
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/eleva/bref-dev-server.git"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

We can run the server with this command:

vendor/bin/bref-dev-server
Enter fullscreen mode Exit fullscreen mode

I prefer to run this on a PHP container, exposing port 8000 of the container to the host for accessibility. However, if you have PHP installed on your machine, you can run it there as well.

The beauty of this setup is that we have a server running on port 8000, which exposes our routes directly from our serverless files. This simulates the behavior of API Gateway without requiring additional routing logic in our endpoints.
Image description

📄 IaC, OpenAPI and doc as code

Once again, developers should begin by defining the requirements and then proceed to write an OpenAPI specification. This approach emphasizes treating documentation as code and decorating infrastructure as code before delving into the actual implementation of function code. By following this structured process, developers can ensure clarity in requirements, maintain consistency across the development lifecycle, and facilitate seamless collaboration within the team.

Here is the sample for our "Hello world" function IaC and DaC.

hello:
  handler: src/function/hello/index.php #function handler
  package: #package patterns
    include:
      - "!**/*"
      - vendor/**
      - src/function/hello/**
  events: #events
    #keep warm event
    - schedule:
        rate: rate(5 minutes)
        enabled: ${strToBool(${self:custom.scheduleEnabled.${env:STAGE_NAME}})}
        input:
          warmer: true
    #api gateway event
    - http:
        path: /hello #api endpoint path
        method: 'GET' #api endpoint method
        cors: true
        caching: #cache
          enabled: false
        documentation:
          summary: "/hello"
          description: "Just a sample GET to say hello"
          methodResponses:
            - statusCode: 200
              responseBody:
                description: "A successful response"
              responseModels:
                application/json: "HelloResponse"
            - statusCode: 500
              responseBody:
                description: "Internal Server Error"
              responseModels:
                application/json: "ErrorResponse"
            - statusCode: 400
              responseBody:
                description: "Request error"
              responseModels:
                application/json: "BadRequestResponse"
Enter fullscreen mode Exit fullscreen mode

As we can observe, the config is essentially the same as with the Node.js version. The main difference lies in switching the handler to a PHP file and including the vendor folder for Composer dependencies. This consistency reinforces the versatility of the concepts discussed.

🐘 Function code in PHP

It's clear that we need to rewrite our "Hello world" function in PHP, leveraging Bref's handy classes to execute code on Lambda. It's assumed we're familiar with the Handler and Context concepts.

I'm presenting the function code before the test code for the sake of article coherence, and because this code essentially serves as a mock function itself. However, it's crucial to emphasize the importance of writing tests before writing the actual code. This ensures that the behavior is well-defined and serves as a guide for implementation.

<?php

namespace App;

use Bref\Event\Handler;
use Bref\Context\Context;

require 'vendor/autoload.php';

class HelloHandler implements Handler
{
    /**
     * @param $event
     * @param Context|null $context
     * @return array
     */
    public function handle($event, ?Context $context): array
    {
        return [
            "statusCode"=>200,
            "headers"=>[
                'Access-Control-Allow-Origin'=> '*',
                'Access-Control-Allow-Credentials'=> true,
                'Access-Control-Allow-Headers'=> '*',
            ],
            "body"=>json_encode([
                "message" =>'Bref! Your function executed successfully!',
                "context" => $context,
                "input" => $event
            ])
        ];

    }
}

return new HelloHandler();

Enter fullscreen mode Exit fullscreen mode

Just go to 0.0.0.0:8000/hello to see our sample function running
Image description

If we go to 0.0.0.0:800 we see a message saying that we have no mapped / route, this confirming our server is reading our serverless config files in which we have mapped only /hello route.
Image description

✅ TDD with PHPunit

Now that we have our OpenAPI documentation and our function running, it's essential to validate our endpoint behavior with a test. We just need to figure out how to do it in PHP instead of using Jest for Node.js.

Let's do it using awesome openapi-psr7-validator repo from thephpleague folks and the evergreen PHPUnit.

<?php 
declare(strict_types=1);

use App\HelloHandler;
use PHPUnit\Framework\TestCase;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;

final class HelloTest extends TestCase
{
    /**
     * @var HelloHandler
     */
    private HelloHandler $handler;
    /**
     * @var Psr17Factory
     */
    private Psr17Factory $psr17Factory;
    /**
     * @var ServerRequestCreator
     */
    private ServerRequestCreator $creator;

    /**
     * @return void
     */
    public static function setUpBeforeClass(): void
    {
        require_once('src/function/hello/index.php');
    }

    /**
     * @return void
     */
    public function setUp(): void
    {
        $this->handler = new HelloHandler();
        $this->psr17Factory = new Psr17Factory();

        $this->creator = new ServerRequestCreator(
            $this->psr17Factory, // ServerRequestFactory
            $this->psr17Factory, // UriFactory
            $this->psr17Factory, // UploadedFileFactory
            $this->psr17Factory  // StreamFactory
        );
    }

    /**
     * @return void
     * @throws ValidationFailed
     */
    public function testHello(): void
    {
        $event = $this->creator->fromGlobals();
        $response = $this->handler->handle($event,null);
        $response = new Response(200, ["Content-Type"=>'application/json'], $response['body']);
        $responseJson = json_decode($response->getBody()->getContents(),true);

        $yamlFile = "doc/build/openapi.yaml";

        $validator = (new ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();
        $operation = new OperationAddress('/hello', 'get') ;

        $validator->validate($operation, $response);

        $this->assertSame('Bref! Your function executed successfully!', $responseJson['message']);
    }
}

Enter fullscreen mode Exit fullscreen mode

Simply run:

vendor/bin/phpunit
Enter fullscreen mode Exit fullscreen mode

To see test result
Image description

🔐📈 Security by design, monitoring and observability

As discussed in my previous article, we shouldn't miss any of this topic.
Let's recap what we should not forget:

  • use a VPC and subnets in your templates
  • use AWS WAF in front of your API
  • use AWS Cognito as a user pool / identity pool to protect your API usage
  • use AWS CloudWatch for dashboards and alarms (don't forget SlicWatch, this awesome serverless plug-in which automate for you those resource providing)

I suggest also to use this awesome package for logging Bref Logger.
As per documentation

Messages are sent to stderr so that they end up in CloudWatch

🏁 Final Thoughts

In this article we've observed that DevOps concepts are facilitated by a serverless architecture, and this is language-independent. We now have two skeletons available, one using Node.js and the other using PHP. Each skeleton is equipped with different technologies and dependencies tailored to support its respective stack.

As per the language, we can also choose to change the framework used to deploy our architecture: we can use AWS SAM, AWS CDK, Terraform or Pulumi, but those concept could and should be covered as well.

🌐 Resources

You can find a skeleton of this architecture with PHP support open sourced by Eleva here.
It has a hello function, which you can use to start developing your own API in PHP with Bref.

🏆 Credits

A heartfelt thank you to my colleagues:

  • G. Giuranno and W. Folini, as they have actively used solutions based on Bref in Firstance production environment, giving me the opportunity to deep-dive into this stack.
  • A. Fraccarollo and, again, A. Pagani, as the co-authors of CF files and watchful eyes on the networking and security aspect.
  • C. Belloli and L. Formenti to have pushed me to going out from my nerd cave.
  • L. De Filippi for enabling us to make this repo Open Source and explain how we develop Serverless APIs at Eleva.

We all believe in sharing as a tool to improve our work; therefore, every PR will be welcomed.

⏭️ Next steps

If you're interested in learning how to deploy Laravel or Symfony applications in a serverless environment, please leave a comment on this article to request it.

Additionally, I have a pipeline set up to reproduce these concepts with Python and Java. If you'd like me to prioritize work on these, please comment on this article to request it.

📖 Further Readings

Bref Doc

🙋 Who am I

I'm D. De Sio and I work as a Solution Architect and Dev Tech Lead in Eleva.
I'm currently (Apr 2024) an AWS Certified Solution Architect Professional, but also a User Group Leader (in Pavia) and, last but not least, a #serverless enthusiast.
My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.

Top comments (0)