• Cheetah Team
  • 16 Dec 2024
Site Architecture, Deployment, and CI/CD

Site Architecture

Site architecture on AWS: Amplify to serve static site, Lambda for backend logic, and SES for emailing.


Welcome to Cheetah Byte! What a better way to kick off our blog than this site’s very own architecture!

When deciding on a site tech stack, we had one thing in mind: ease of maintainability.

This started with a static site framework, where we chose Hugo, since anyone who knows HTML and Markdown can maintain and develop it.

We took the extra step of going Serverless, taking away the hassle of maintaining a webserver, database routing, authentication, etc. In addition, there’s no site to hack, just a bunch of HTML and CSS.

CI/CD pipelines to build and deploy code on a push to main makes the development cycle painless!

With no backend, how does the Contact Form work? How did we deploy the serverless site? With AWS, Terraform, and GitHub workflow actions.

Code Repository

Static Site

Hugo powers our site generation, converting Markdown files and templates into a tightly packaged, multi-page site.

Being static and built on Go, the site is lighting fast out-of-the-box.

Frontend Stack: Hugo, tailwindCSS, Alpine.JS

Serverless Stack

Our serverless architecture leverages three key AWS services:

AWS Amplify

Amplify handles our static site hosting, where we leverage

  • Automatic HTTPS certificates
  • Branch-based deployments
  • Built-in CI/CD pipeline

The setup is minimal - just connect your GitHub repository and Amplify handles the rest.

AWS Lambda (Contact Form)

Since we do not have a backend, we use Lambda for what we call Just-in-Time computing of our Contact Form submissions.

This was coded in JavaScript, where we add functionality for spam, form validation, among other things (code link).

The Lambda function is deployed using Terraform (excerpt follows):

resource "aws_lambda_function" "contact_form" {
  filename      = "contact-form.zip"
  function_name = "${var.lambda_function_name}Function"
  role         = aws_iam_role.contact_form_role.arn
  handler      = "index.handler"
  runtime      = "nodejs18.x"
}

resource "aws_lambda_function_url" "contact_form_url" {
  function_name      = aws_lambda_function.contact_form.function_name
  authorization_type = "NONE"

  cors {
    allow_origins = ["${var.website_url}"]
    allow_methods = ["POST"]
    allow_headers = ["content-type"]
    max_age       = 300
  }
}

We made a ContactFormRole and ContactFormPolicy, and associated with this lambda function, which allowed it interact with the next service we used.

The terraform code is only ran once for initial setup, where the build automation uses the AWS CLI exclusively to update the function.

AWS SES

Simple Email Service (SES) handles our Contact Form emails, where the service is called by our Lambda function.

We had to verify both the source and recipient addresses, where the recipient address is just a distribution group in Outlook: aws ses verify-email-identity --email-address contact@cheetahbyte.com

We then get an email from AWS, where we click the link to verify the email.

Infrastructure as Code with Terraform

While AWS Amplify handles site hosting with minimal configuration, the Lambda Contact Form required a more sophisticated setup.

This is where Terraform became essential to our project.

Why Terraform? The Contact Form functionality needed several AWS resources to work together:

  • Lambda function with proper runtime and handler
  • Function URL with CORS configuration
  • SES permissions and configurations set at the Lambda function level
  • GitHub Actions deployment user for updating

This is really time-consuming, error-prone, and tedious to document, but with Terraform, we have reusable scripts that can be used to deploy, modify, and destroy infrastructure on the cloud. The scripts themselves serve as a form of living documentation.

See the Terraform section of our repository.

CI/CD

We maintain two separate CI/CD pipelines:

Amplify GitHub Push

This makes life really easy, where we just push to main, wait 40 seconds, and our changes are up!

It detects the repo’s amplify.yaml, and builds according to this script developed by the Hugo team that was adapted.

We had great pleasure in having our CEO make a subtle HTML change, and only needing to press a single button on IntelliJ to push her code!

Lambda Github Workflow Action

Our Lambda deployments use GitHub Actions, where we must initially deploy our Terraform scripts, and then make an initial commit to trigger our automation.

  • Workflow
    • Code changes in contact-form/ trigger workflow
    • Dependencies are installed and tested
    • Code is packaged into ZIP
    • Lambda function is updated
    • Deployment is verified

Lambda CI/CD Pipeline

name: Deploy Lambda Function

on:
  push:
    branches:
      - main
    paths:
      - '${{ vars.LAMBDA_CODE_PATH }}/**' # detects changes in specific code path (i.e. contact-form/)

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... deployment steps

Since this action is happening outside of AWS (unlike Amplify), we created an IAM User, where we then made a policy to allow updating of this Lambda function, and then associated it with this new user.

Our workflow uses GitHub variables and secrets for configuration:

  • LAMBDA_FUNCTION_NAME
  • LAMBDA_CODE_PATH
  • AWS_REGION
  • secrets.AWS_ACCESS_KEY_ID
  • secrets.AWS_SECRET_ACCESS_KEY

Note: this pipeline is reusable for updating any lambda function, where you add the code to its own directory, and give the IAM user appropriate permissions for that new function.

  1. Lambda Contact Form Code
  2. Deploying Hugo on Amplify
  3. AWS Lambda Function URLs
  4. AWS SES Setup Guide

Contact Us

Message sent successfully!
Failed to send message. Please try again.