Serverless webpage

Selftaugth Cloud is a Personal portafolio website engine capable of server-side logic using Laravel and fully deployed as a serverless application runing on AWS costing less than 1$/month of recurring cost.
Journal is a blog engine that you can use to create your own blog.
It is different from most other blog engines in that it is a serverless application, meaning it is very cheap to host and extremely scalable, while still letting us write server-side logic using Laravel.
Full Technology Stack for a Serverless Laravel Deployment
- Laravel renders the website and runs on AWS Lambda.
- Custom pages (like this one) can be created with Blade templates.
- Blog articles are written in static files using Markdown.
- TailwindCSS is used as the CSS framework.
- Assets are compiled using Laravel Mix and deployed to AWS S3.
- CloudFront is used as a CDN to optimize performances.
- The whole stack is deployed using a single configuration file with the Serverless Framework.
How the Serverless Laravel Architecture Operates
When running in production, the entire website is read-only. It reads its content from static files and uses no database. This approach makes it very scalable, extremely cheap to run, and very secure.
To write new articles, admin pages are available but only when running locally. No user accounts are necessary. Simply run the website locally using:
php artisan serve
and start creating or editing posts via the website or your favorite Markdown editor.
Laravel Serverless Configuration Using the Serverless Framework
The entire infrastructure is defined as code using the Serverless Framework. Below is an explanation of the main components in the serverless.yml
configuration file:
Defining Basic AWS Serverless Configuration
service: journal
provider:
name: aws
region: us-east-2
stage: prod
runtime: provided.al2
httpApi:
payload: '2.0'
This sets up the service name, specifies AWS as the provider, defines the deployment region, and configures the runtime for PHP applications using Amazon Linux 2.
Laravel Production Environment Variables in AWS Lambda
environment:
APP_ENV: production
APP_DEBUG: false
MIX_ASSET_URL: https://your-domain.com
These environment variables are injected into the Lambda function to configure Laravel properly in the production environment.
Optimize Lambda Package Size by Excluding Unnecessary Files
package:
exclude:
- 'node_modules/**'
- 'public/storage'
- 'resources/assets/**'
- 'storage/**'
- 'tests/**'
- 'public/assets/**'
This section optimizes the deployment package by excluding unnecessary files, keeping the Lambda deployment package as small as possible.
Main Lambda Function Setup for Running Laravel on AWS
functions:
web:
handler: public/index.php
timeout: 28
layers:
- ${bref:layer.php-80-fpm}
events:
- httpApi: '*'
This defines the main Lambda function that runs the PHP application:
- The
handler
points to Laravel's entry point - The
timeout
is set to 28 seconds (just under API Gateway's 29-second limit) - It uses the
Bref
PHP-FPM layer for PHP 8.0 that is the glue to run php on lambda. - The function responds to all HTTP requests through API Gateway
Supporting AWS Infrastructure for the Laravel Web Application
The configuration also defines several AWS resources:
- S3 Assets Bucket:
Assets:
Type: AWS::S3::Bucket
Properties:
BucketName: bucket-name
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
This bucket stores all static assets like CSS, JavaScript, and images.
- S3 Bucket Policy:
ssetsBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref Assets
PolicyDocument:
Statement:
- Effect: Allow
Principal: '*'
Action: s3:GetObject
Resource: !Join ['/', [!GetAtt Assets.Arn, '*']]
This policy makes the S3 bucket publicly readable, allowing anyone to access assets.
-
CloudFront Website CDN:
- A CDN distribution is configured with two origins:
- Lambda Origin: Serves the dynamic content from AWS Lambda
- S3 Origin: Serves static assets from the S3 bucket
- The distribution is configured with:
- HTTPS enforcement
- HTTP/2 for better performance
- Custom domain configuration (
your-domain.com
) - Gzip compression for assets
- Cookie and header forwarding for the Lambda origin
- Custom error handling
- A CDN distribution is configured with two origins:
WebsiteCDN:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
# Cheapest option by default (https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_DistributionConfig.html)
PriceClass: PriceClass_100
# Enable http2 transfer for better performances
HttpVersion: http2
# Origins are where CloudFront fetches content
Origins:
# The application (AWS Lambda)
- Id: App
DomainName: !Join ['.', [!Ref HttpApi, 'execute-api', !Ref AWS::Region, 'amazonaws.com']]
CustomOriginConfig:
OriginProtocolPolicy: 'https-only' # API Gateway only supports HTTPS
# When using a custom domain, uncomment the configuration below:
# Why? CloudFront does not forward the original `Host` header. We use this
# to forward the website domain name to Laravel via the `X-Forwarded-Host` header.
# Learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
# Laravel picks up this header automatically.
OriginCustomHeaders:
- HeaderName: 'X-Forwarded-Host'
HeaderValue: your-domain.com # our custom domain
# The assets (S3)
- Id: Assets
DomainName: !GetAtt Assets.RegionalDomainName
# Tell CloudFront that this is an S3 origin
S3OriginConfig: {}
# The default behavior is to send everything to AWS Lambda
DefaultCacheBehavior:
AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
TargetOriginId: App # Our Lambda application
# Disable caching for our HTML responses (remove this to use caching)
# https://aws.amazon.com/premiumsupport/knowledge-center/prevent-cloudfront-from-caching-files/
DefaultTTL: 0
MinTTL: 0
MaxTTL: 0
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-forwardedvalues.html
ForwardedValues:
QueryString: true
Cookies:
Forward: all # Forward cookies to use them in our app
# We must *not* forward the `Host` header else it breaks API Gateway
Headers:
- 'Accept'
- 'Accept-Encoding'
- 'Accept-Language'
- 'Authorization'
- 'Origin'
- 'Referer'
# CloudFront will force HTTPS on visitors (which is more secure)
ViewerProtocolPolicy: redirect-to-https
CacheBehaviors:
# Assets will be served under the `/assets/` prefix
- PathPattern: 'assets/*'
TargetOriginId: Assets # the static files on S3
AllowedMethods: [GET, HEAD]
ForwardedValues:
# Laravel Mix uses the query string to provide a unique version hash
# that busts the cache (because it changes on every deploy).
# That's why we need to enable it in the cache key.
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
Compress: true # Serve files with gzip for browsers that support it (https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html)
CustomErrorResponses:
# Force CloudFront to not cache HTTP errors
- ErrorCode: 500
ErrorCachingMinTTL: 0
- ErrorCode: 504
ErrorCachingMinTTL: 0
Aliases:
- your-domain.com
ViewerCertificate:
AcmCertificateArn: 'arn:aws:acm:{region}:{account_id}:certificate/{certificate_id}'
SslSupportMethod: 'sni-only'
MinimumProtocolVersion: TLSv1.1_2016
Automating Laravel Serverless Deployment with GitHub Actions
The project uses GitHub Actions to automate the deployment process. When changes are pushed to the main branch, the CI/CD pipeline automatically deploys the updated website to AWS.
Step-by-Step CI/CD Workflow for Laravel on AWS
The deployment workflow consists of the following steps:
-
Environment Setup:
- Checkout the code repository
- Configure AWS credentials from GitHub secrets
- Set up Node.js 16 and PHP 8.0
-
Dependency Management:
- Cache Node.js dependencies to speed up future builds
- Install the Serverless Framework globally
- Install PHP dependencies with Composer
- Install JavaScript dependencies with npm
-
Build Process:
- Generate Laravel application key
- Compile frontend assets with Laravel Mix
-
Deployment:
- Upload compiled assets to AWS S3 bucket (with deletion of obsolete files)
- Deploy the application to AWS Lambda using the Serverless Framework
This automated pipeline ensures that every change pushed to the main branch is automatically tested and deployed, eliminating manual deployment steps and reducing the possibility of human error.
To set up this CI/CD pipeline for your own project, you'll need to:
- Create AWS access keys with appropriate permissions
- Add the access keys as secrets in your GitHub repository settings
name: Deploy
on:
push:
# The website is only deployed when we push to main
branches: [ main ]
jobs:
# This is the job that deploys the website
deploy:
runs-on: ubuntu-latest
steps:
# Prepare the environment
- uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
# Define the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables in GitHub settings (in the "Secrets" panel)
# These access keys will be used for the deployment
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
# Set up Node.js 16
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
# Cache dependencies
- name: Cache Node.js dependencies
uses: actions/cache@v4
with:
path: ~/.npm # npm cache files are stored in `~/.npm`
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
${{ runner.os }}-
- name: Install serverless
run: sudo npm i -g serverless@3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
# Prepare the Laravel project
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install PHP Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Install JS dependencies
run: npm ci
- name: Compile the assets
run: npm run production
# Deploy the new versions of the assets
# (replace the bucket name with your own bucket)
- name: Deploy the assets
run: aws s3 sync public/assets s3://bucket-name/assets --delete
# Deploy the application
- name: Deploy the application
run: serverless deploy