MauroBaptista.com

Releasing a Laravel app in Lambda using Serverless framework

Laravel AWS Lambda

As the title says, we intend to avoid using an instance to release our app. To do that we are going to use:

Install Bref

First, we need to add Bref to our project.

composer require bref/bref bref/laravel-bridge --update-with-dependencies

then we publish the package options. It will generate the serverless.yml file.

php artisan vendor:publish --tag=serverless-config

After publishing it, we are going to make some small changes to the serverless.yml file that is in your app root folder:

Configuring Serverless Framework

First, we will add some tags, so we can easily find them in our AWS infrastructure:

provider:
...
stackTags:
ManagerBy: serverless
Role: "lambda deploy"
Project: self:service
Environment: ${opt:stage, self:provider.stage}

Now we will add the bucket where our deployment files will be stored.

provider:
...
deploymentBucket:
name: maurobaptista.com

Then we add some custom variables to easily call it in our serverless functions part:

custom:
stage: ${opt:stage, self:provider.stage}
prefix: ${self:service}-${self:custom.stage}
web: ${self:custom.prefix}-web
artisan: ${self:custom.prefix}-artisan

To reduce the size of the file (not much though) we will add some more folders to exclude from the deployment:

package:
patterns:
...
- '!docker/**'
- '!stubs/**'

In our functions part, we add the name of them, so we can identify them quickly when looking at the Lambda in the AWS console, and also update the bref to use php 8.1:

functions:
web:
name: ${self:custom.web}
...
layers:
- ${bref:layer.php-81-fpm}
...
artisan:
name: ${self:custom.artisan}
...
layers:
- ${bref:layer.php-81} # PHP
- ${bref:layer.console} # The "console" layer

In the last part, we add the bucket deployment plugin:

plugins:
- ./vendor/bref/bref
- serverless-deployment-bucket

Creating S3 bucket

As our deployment files will be in S3, we will need to create the bucket:

Note You must change the command below to suit your AWS information. Also, the bucket must have the same name as you defined in your serverless.yml.

aws --profile benjatech s3api create-bucket --bucket maurobaptista.com --region us-east-1

Setting Github Actions

Now we need to add a way to deploy it in AWS Lambda, for that we are going to use Github Actions.

To do that, we need to create the file: .github\workflows\release.yml with the content:

name: Release
on:
push:
branches:
- main
env:
PROJECT : "maurobaptista.com"
STAGE : "production"
AWS_REGION : "us-east-1"
PHP_VERSION : "8.1"
COMPOSER_VERSION : "v2"
permissions:
id-token: write
contents: read
jobs:
deploy-lambda:
name: Deploy Lambda
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ env.PROJECT }}-github-action-role
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 900
role-session-name: Lambda
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
tools: composer:${{ env.COMPOSER_VERSION }}
coverage: none
- name: Get File from Parameter Store
run: ./deploy/write_to_file.php "$(aws --region ${{ env.AWS_REGION }} ssm get-parameter --with-decryption --name /${{ env.PROJECT }}/${{ env.STAGE }}/env)" .env
- name: Npm install & build
run: |
npm install
npm run build
- name: Install Serverlesss
run: |
npm install serverless -g
npm install serverless-lift -g
npm install serverless-deployment-bucket -g
- name: Composer install
run: composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
- name: Prepare Laravel
run: |
php artisan config:clear
php artisan route:clear
php artisan view:clear
touch ./storage/logs/laravel.log
- name: Deploy Lambda
run: serverless deploy --stage ${{ env.STAGE }}

As you can see above, you need to change some data on the code above. Please, take a look at the env part and adjust it for your needs.

You will also need to add the AWS_ACCOUNT_ID to your repository Github Secrets.

Storing our .env in Parameter Store

As we should never commit our .env file, we are going to store it in Parameter Store in AWS. So, our Github Actions will include the .env file in our deployment zip.

Add the .env for production in a file (e.g. .env.prod) and upload it to AWS Parameter Store. Not that the name must be the same as you set in the Github Actions.

aws --profile benjatech ssm put-parameter --name /maurobaptista.com/production/env --value file://.env.prod --type SecureString

Where the --name is: --name /{your project name}/{stage}/env

Remember to delete the created file, so you do not commit it.

Creating the .env file to deploy

The Github Actions configuration also calls a php script to create the .env file from the data in AWS Parameter Store.

#!/usr/bin/env php
<?php
if (!isset($argv[1])) {
echo 'No data set';
return 1;
}
if (!isset($argv[2])) {
echo 'No filename set';
return 1;
}
$parameter = json_decode($argv[1], true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo 'Invalid Json';
return 1;
}
if (!isset($parameter['Parameter'])) {
echo 'No parameters index found';
return 1;
}
file_put_contents($argv[2], $parameter['Parameter']['Value']);
return 0;

This is a straightforward code that will get the value you pass to it, and then store it into a file.

Creating an Open ID Connect between AWS and Github

To allow our Github Action to connect with our AWS account, we must create the connection for it:

aws --profile benjatech iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 --client-id-list sts.amazonaws.com

If you want to know more: Github Actions update on OIDC-based deployments to AWS

Creating the role that Github Action will assume

As you could see in the action to release, we will make our Github Action assume a role, so we need to create this role.

First, create a JSON file called role.json with the content:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::***:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:maurobaptista/maurobaptista.com:ref:refs/heads/main"
},
"ForAllValues:StringEquals": {
"token.actions.githubusercontent.com:iss": "https://token.actions.githubusercontent.com",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}

Replace the *** with your AWS Account Id, and also on the token.actions.githubusercontent.com:sub replace it with your repository.

Now we create the role in AWS using the file as the content:

aws --profile benjatech iam create-role --role-name maurobaptista.com-github-action-role --assume-role-policy-document file://role.json

Remember to delete the created file, so you do not commit it.

Creating the policy

Let's create the policy.

As we did before, lets create a file called policy.json` with the content:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::maurobaptista.com",
"arn:aws:s3:::maurobaptista.com/*"
]
},
{
"Effect": "Allow",
"Action": [
"apigateway:GET",
"apigateway:HEAD",
"apigateway:OPTIONS",
"apigateway:PATCH",
"apigateway:POST",
"apigateway:PUT",
"apigateway:DELETE",
"apigateway:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResource",
"cloudformation:DescribeStackResources",
"cloudformation:ValidateTemplate",
"cloudformation:UpdateStack",
"cloudformation:ListStacks",
"cloudformation:DeleteChangeSet",
"cloudformation:CreateChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:ListStackResources",
"iam:GetRole",
"iam:AttachRolePolicy",
"iam:CreateRole",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:TagPolicy",
"iam:TagRole",
"lambda:*",
"logs:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": [
"arn:aws:sts::***:assumed-role/maurobaptista.com-github-action-role/*",
"arn:aws:iam::***:role/maurobaptista-production-us-east-1-lambdaRole"
]
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter"
],
"Resource": [
"arn:aws:ssm:us-east-1:***:parameter/maurobaptista.com/production/*"
]
}
]
}

Replace the *** with your AWS Account Id, on the s3 replace it with your bucket name, and also on the iam:PassRole change the first line to have your project name on it, and the second one to have the serverless.yaml service name (first line in the file)

Then we are going to create the policy, remember to take the policy ARN, as we are going to use it next:

aws --profile benjatech iam create-policy --policy-name maurobaptista.com-github-action-policy --policy-document file://policy.json

Remember to delete the created file, so you do not commit it.

Attaching the policy to the role

Replace the role-name with the role name you created, and also replace the policy-arn with the one you just created.

aws --profile benjatech iam attach-role-policy --role-name maurobaptista.com-github-action-role --policy-arn arn:aws:iam::xxx:policy/maurobaptista.com-github-action-policy

Deploying

After you commit all to your main branch, it will trigger an Action in GitHub. Go to your repository page, and click Actions. There you should see a Deploy Lambda job, then go to the Deploy Lambda step (yes, the name is the same). Then you will find the endpoint to access your deployed app.

Possible Errors

If you face the errors that AWS Cloud Formation is in ROLLBACK_IN_PROGRESS (or ROLLBACK_COMPLETE) go to AWS CloudFormation and manually delete the stack.

Performance

This blog is using the approach above. I believe the results are not perfect, but it is good enough.

Performance

More

I also use a similar approach to run Workers in Lambda, if you want to know more: Running Laravel workers in AWS Lambda