Releasing a Laravel app in Lambda using Serverless framework
As the title says, we intend to avoid using an instance to release our app. To do that we are going to use:
- Laravel (https://laravel.com/)
- Bref (https://bref.sh/)
- Serverless (https://www.serverless.com/)
- AWS Lambda (https://aws.amazon.com/lambda/)
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: Releaseon: push: branches: - mainenv: PROJECT : "maurobaptista.com" STAGE : "production" AWS_REGION : "us-east-1" PHP_VERSION : "8.1" COMPOSER_VERSION : "v2"permissions: id-token: write contents: readjobs: 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<?phpif (!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 theiam: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 thepolicy-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.
More
I also use a similar approach to run Workers in Lambda, if you want to know more: Running Laravel workers in AWS Lambda