Article Content
Take a quick gander at the footer of this site and you'll find that it was built with SvelteKit, Kiro, and the "SvelteKit Serverless Toolkit". Kiro is a topic for another day β the main focus areas of today's post are SvelteKit, and this mysterious little "toolkit" that I mention.
TL;DR: take a look at this GitHub repository. It enabled me to get this app hosted in a few minutes, for less than $2 a month (most of that is the hosted zone). This whole article is essentially just prosaic fluff around that for the extra-curious; there's a README in the repo that'll tell you how to use it.
Preamble
It started a few years back. I had a SvelteKit app deployed on Vercel, which was great (and probably is the easiest way to get your SvelteKit app hosted), but I like using my IaaS providers (because I'm a nerd like that). So I sought to deploy where I knew best β AWS. If you're building a fully static site, grab yourself an S3 bucket and a CloudFront distribution and Bob's probably your uncle (I haven't done it before). Alas, I like my SSR, and that then entails compute resources, and probably path-based routing for static/dynamic bits, yada yada. But all I wanted was to quickly drop my silly little personal apps on the cloud! I guess I wanted to have my cake and eat it.
There were a few solutions available. The most broadly recognised seemed to be sveltekit-adapter-aws. S3 for static content, SSR Lambda behind an API Gateway, fronted by a CloudFront Distribution. Pretty sensible. Maybe it was just because I'm the most butterfingered AWS CDK user in the universe, but I couldn't get it to play nice. Also, more generally, I preferred to be able to extend my infrastructure in Terraform anyway. It was only when I stumbled upon this post by Sean W. Lawrence that the initiative came to me.
In sum, Sean used adapter-node, set up a Polka HTTP server on a Lambda function, and then used a Bash script to automate the process of building the app, copying it into the Lambda function, and having the CDK pick that up for deployment, which entailed very similar infrastructure to that shown in sveltekit-adapter-aws. Lovely stuff.
However. I like me my Terraform. And I thought, hey, if we're using Bash scripts now, why not just build my own full-blown Terraform-oriented template/toolkit from Sean's example?
So that's what I did. Voila, welcome to blog.ishkur.uk.
About the Template/Toolkit
So, the use case for this thing was:
I just want to get a small, relatively low-traffic SvelteKit app running on AWS, and I want to use my good friend Terraform to do it.
If that's you, great, strap in. If that's not you, maybe still think about it β the whole idea is that it's totally modifiable/extensible, which I'll get back to in a sec.
The architecture generally pinches what we saw in the sveltekit-adapter-aws, so it uses an SSR Lambda behind a greedy API gateway proxy (i.e., it essentially just forwards the raw request on to the Lambda, regardless of the method), an S3 bucket for all the static bits, and a CloudFront distribution that picks the origin based on whether your path wants static or dynamic content.
graph TB
User[User]
Terraform[Terraform-wielding Engineer β‘]
subgraph "AWS"
R53["(Optional) Route 53 Hosted Zone"]
CDN[CloudFront Distribution]
S3[Static Assets S3 Bucket]
APIGW[Greedy HTTP Proxy API Gateway]
Lambda[SSR Lambda]
end
Terraform .-> AWS
User <-. "queries" .-> R53
R53 <-. "resolves" .-> CDN
User -- "HTTP request" --> CDN
CDN -- "1. /__data.json" --> APIGW
CDN -- "2. /*.* (static files)" --> S3
CDN -- "3. /* (everything else)" --> APIGW
APIGW -- "forwards *" --> Lambda
I'll talk about some of the technical intricacies in the Techy Tidbits section.
But yes, the use of Lambda is great for small-scale stuff. If you're seeing constant, high-volume traffic, ECS/EC2/another non-ephemeral compute might be better. Lambda can get surprisingly expensive under such circumstances. Which is why I say this isn't really for production use β but it can be modified to be that way. Just adding a proper pipeline in would get it a decent chunk of the way there.
The whole reason that I built a template/toolkit rather than an adapter was, ironically, so it was easier to adapt. If you want to change the infra, easy. Nothing fancy about it, just change it as you need. If you want to change the build/deployment process, same story. You don't have to go poking around the internals of a dependency you pulled in to change how things work.
It features some other things I thought would be convenient as well, such as:
- A script for running this whole workflow locally, with a Docker compose stack.
- Husky pre-commit hooks with various Prettier, ESLint, NPM etc. configs
- Configs for getting setup with Playwright testing
I would absolutely welcome suggestions/tweaks, particularly if they're additive and generic. No harm in adding optional utilities if it could potentially make getting an app hosted easier.
How do I use it?
I won't go into reams of depth about how to use the template, since that's all information you can find in the README, but for the sake of articulating here just how easy it is to get started, the gist is:
- Clone the repo
- Drop your app into the
srcfolder - Authenticate against AWS (if it's the default profile, you're sorted, if you're using an AWS SSO profile or otherwise, you'll need to configure that in Step 4)
- Fill out the
.env.devfile with your desired configs (many of which you can leave empty if you want β you can always change them later) bash deploy.sh, and if you're happy with the Terraform plan, give it ayes(alternatively you could run thedeploy.shscript with the--auto-approveflag, like in Terraform, but, y'know, caution).
It'll then build your app, copy it over to the package that'll be deployed to Lambda, spin up all your infrastructure, and even do a cache invalidation on updates if you like (caching has deliberately been left unset β that's really not something for me to decide or even begin to assume for your project. For this site though, I have some very aggressive CDN and client-level caching in place).
At the end, it'll run a simple status check on the front-most domain (either your CloudFront distribution URL or a domain that you own and specified in your configs), and let you know how it went. Whole script run takes a few seconds to a couple minutes. It's quite cathartic.
Techy Tidbits
There was a handful of curiosities that came up as I was building this. This section is pure indulgence for those with nerd-brains like mine, so do feel free to skip if uninterested...
You may notice in the architecture diagram above that the CloudFront ordered cache goes like:
__data.json--> API Gateway*.*--> S3*--> API Gateway
So, for those unfamiliar with SvelteKit (I must admit I'm not a pro myself, just a tinkerer), there's an internal data endpoint for each route, called
__data.json. A lot of frameworks with SSR use something akin to that for client-side app hydration. It's the serialised data your frontend needs to render that specific route, and it's what enables page navigation without full reloads. But of course, if you're looking at our second rule (*.*) β that catches all requests for things with a file extension β AKA static files. Which works for everything except this. If you don't give the__data.jsonprecedence over your static route, it'll get swallowed by the S3 origin, and client-side navigation will fail.As a side note, careful with that endpoint. I'd seriously recommend writing some proper tests for what it spits out. It can return some sensitive stuff if you don't build your
loadfunctions properly.The API Gateway API key is generated by Terraform, so it is therefore stored in state (but treated as sensitive). It's used in a header added to requests by CloudFront, so potential wrong'uns can't hit your API directly.
I had to employ a few hacks here and there, to get Terraform to run as cleanly as it does. I honestly haven't thought of any decisively better approaches, but I'm absolutely open to them if you have them. Some of those:
This gnarly son of a gun here:
resource "aws_s3_object" "assets" { for_each = fileset(var.static_assets_source_path, "**") bucket = aws_s3_bucket.bucket.id key = each.value source = "${var.static_assets_source_path}${each.value}" etag = filemd5("${var.static_assets_source_path}${each.value}") # Gets the file extension (i.e., the string after the last ".") and finds its respective MIME type from a map of content types. # Not the most pulchritudinous; prettier approaches are welcome. content_type = lookup(local.content_type_map, element(split(".", each.value), length(split(".", each.value)) - 1), "binary/octet-stream") }...This is accompanied by a
content_type_map, which maps common file extensions to a MIME type, so the browser doesn't get a bunch ofbinary/octet-streams (as is the default) and flip out.I also had to use a couple
local-execs, which I'll spare you (I was quite liking the flow of this article). The first of those essentially just polled to verify whether an ACM certificate had been validated with DNS. I'm sure there must be a better way to do this one, but I haven't found the answer.aws_acm_certificate_validationsounded to me like it would do the job, but no dice. Suggestions very welcome.The second
local-execI had to use to prevent a cyclical dependency between the API Gateway, and the SSR Lambda. The API Gateway needed to know the ARN of the Lambda function to send requests to, but the Lambda function β due to a requirement ofadapter-nodeβ needed the invocation URL of the API Gateway. There isn't a decoupled resource for AWS Lambda environment variables, unfortunately β so an after-the-factupdate-function-configurationAPI call with thelocal-execseemed like the best tool for the job.
I guess more broadly, some would clutch their pearls at the idea of controlling the Lambda deployment package and all of the individual static files with Terraform. I can understand that; it definitely feels like Terraform is being used as a CI/CD tool more than an IaC tool. But I think it actually works quite well for cases like this β it can check file hashes and pretty cleanly determine when to actually update stuff.
Alternatives
I mentioned this briefly before, but I'd like to make sure it's clear that this is certainly not the only way to go about things. I've already mentioned Vercel, sveltekit-adapter-aws, and this post (which was the most significant influence of mine when building this). The other one is SST. These folks seem to be the winners in this territory at the moment. They offer a nice way to deploy to AWS serverlessly, and from what I can tell, they also offer a nice wrapper around AWS services so you can easily link them up with your app. Really cool stuff, I may well have a play.
But insofar as deploying a SvelteKit app to AWS with Terraform, and having proper control over it (i.e., it's a template, rather than a package), I daresay that this solution of mine does fill its own niche? ππ
You can be the judge.
So, with all that said and done, I built this thing a couple years back. Didn't think too much of it at the time. I came back to it the other day to get this site deployed, gave it a brief dust off, so to speak, and I was honestly quite surprised to see my site just appear on the cloud, behind this domain, in about 5 minutes*. I sincerely hope it can offer you the same value as it did me β I'd definitely implore you to give it a go and see what you make of it.
*yes, okay, I had to set up some DNS delegation bits and what-have-you, so yes I'm lying a bit, but that's beside the point π