Serverless webpage

February 2025
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:

  1. 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.

  1. 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.

  1. 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
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:

  1. Environment Setup:
    • Checkout the code repository
    • Configure AWS credentials from GitHub secrets
    • Set up Node.js 16 and PHP 8.0
  2. 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
  3. Build Process:
    • Generate Laravel application key
    • Compile frontend assets with Laravel Mix
  4. 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:

  1. Create AWS access keys with appropriate permissions
  2. 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