- Cheetah Team
- 16 Dec 2024
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.
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
- Code changes in
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.