A new blog site!

9 Dec 2024 / engineering / swift / vapor

I've migrated the old website to a brand new one! It's still written in Swift, but this time it's not static site generation. I've taken it much farther! Expect a series of blog posts about creating the new website.

Motivation

I really wanted to do something with server side swift. I've played with Vapor quite a few times but now was my chance to really sink my teeth into it. The problem was I wasn't sure exactly what I wanted to build. It started with just a WebAuthn demonstration. Eventually I decided to challenge myself. What if I could create a website without using HTML, CSS, or JavaScript? As I had continued success with this venture I eventually figured out what I wanted to do, I wanted to move my blog over. This time, I wanted to do more than just static site generation.

Building for Scale

I have no illusions, my blog will barely get any visitors. However, I wanted to create something that could scale as large as necessary. There are so many considerations:

There's far too much to cover in a single blog post, so I'll start by sharing what I'm doing for hosting. Originally, I wanted to use Swift Cloud. This seemed like a perfect starting point, but unfortunately, I just couldn't make it work. For starters, it only supports AWS. It's been a hot minute since I did anything in AWS and in the time I've spent away it got a lot more complicated. As much as I would've loved to move in this direction the truth is it was completely overwhelming and I wasn't convinced I could manage costs the way I wanted.

Luckily, the vapor docs have a list of deployment options and they're very helpful! I looked through everything there and landed on what felt both simple to work with, something I had confidence I could control cost on, and something that could scale. Fly.io checked all those boxes! Their solution is absolutely delightful.

Managing Fly Configuration

Fly has a CLI you can use and it's one of the things I truly love about using them. The definition for your infrastructure lives in a fly.toml file. Now honestly, I'm not a huge TOML fan. I love that YAML is a superset of JSON and TOML just moved in a direction that doesn't feel natural to me. It's designed to prioritize humans, but the syntax (while simple) feels a little hard for me to reach for. No matter what configuration format is used I usually have a gripe with config files because I have to go look up syntax or use JSONSchema to figure out what I can do. One of the amazing things about the fly CLI is that it modifies your fly.toml file! This means I can use fly --help and have all the discoverability I crave without having to fuss with a config file.

To that end, this is what my configuration looks like:

app = 'aprincipalengineer'
primary_region = 'den'

[build]

[deploy]
  release_command = 'migrate -y'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'suspend'
  auto_start_machines = true
  min_machines_running = 1
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 2

[[metrics]]
  port = 8080
  path = '/metrics'
  https = false

As you can see there's a brilliantly simple configuration for auto scaling. I can easily control the minimum number of machines (and scale to 0, which I'll probably do after a while). You may notice there's no max_machines_running. This is basically something you control by provisioning machines. The more you provision, the higher your max is. In my case, I have 2 machines provisioned. I'm only charged for them as they are used and it's hard for me to believe I'll ever have enough traffic to worry about scaling past that.

Other fly apps and services I run in parallel

I'll probably need an entire blog post for each of these, but here's a quick rundown of what I'm running and how it went.

Persistence

So to do this site justice I wanted my blogs, users, and WebAuthn public keys in a database. Vapor has several drivers. I usually prefer SQLite for testing and Postgres for production. This is because the Vapor SQLite driver has an in-memory database and Postgres doesn't. As it happens, Fly has a Postgres solution that's trivially easy to get set up. I chose the development profile for it, I don't really want to pay for 3 machines in a cluster with replication for the blog that receives very low traffic.

Distributed Caching

I also needed Redis for session management. Fly provides an Upstash Redis Integration. Once again, this was stupidly simple to add. One click in the dev console and you've now got Redis ready to rock. Upstash has some advantages I really liked, too. For example, they've got high availability. Your distributed cache is backed by block storage in the cloud, so you've got a lot more durability than you would with other solutions. This discovery meant that I could trust Redis to hold onto things that I would normally be a little hesitant to trust it with.

Secrets Management

For secrets management there's some ease of use with Fly itself using its CLI. You can run fly secrets set KEY=value and it'll set environment variables on all your machines. The downside is that this has you managing secrets from your local machine through the CLI. While convenient, I felt like for secrets that were actually secret I prefer something a bit more robust. Doppler was an amazingly simple integration. They've even got a dedicated sync and instructions for working with Fly. On top of that, they allow login with GitHub which makes my life so much easier. Doppler has a great interface, super simple integration, and even lets me generate ECDSA keys at will. Bonus points, their free tier is based on the number of users managing secrets. This means that I don't have to pay!

Logging

For logging a good friend of mine recommended Axiom. Now Fly has its own log explorer, but it's nowhere near as sophisticated. If you want something that feels like Splunk without paying for Splunk Axiom is straight up awesome. Their free tier allows for 0.5tb of logs! That's way more than enough for my use cases. I won't pretend to be a Splunk or log querying expert, but their autocomplete makes it really easy to use their query language and I was able to set up a dataset and a dashboard with no issues. I haven't yet set up monitors, but I might as time goes on. At first I thought I'd have to write my own swift-log LogHandler. However, as it turns out Fly has a solution! You can use the Fly Log Shipper which supports a wide array of solutions as well as anything custom you might want to do. Setting up Axiom was as simple as setting some environment variables. I loved how easy it was!

Authorization

This'll have to be a topic for a much more detailed post. Vapor provides a few authentication solutions out of the box. I went with both session authentication and JWTs. As great as that is, I needed an authorization solution. Sadly, there's just no good Authorization libraries for Vapor. There's some almost good ideas floating around, but they're unmaintained and not very feature rich. In my quest to create scalable solutions I decided to go big with OpenFGA. I'm not going to lie, this was a steeper learning curve that I thought it would be. I have a lot of AuthN/AuthZ experience, so I figured I'd fly through development with it. However, wrapping my head around their ReBAC modeling and database took a bit. However it's all working now! I'm running OpenFGA in a docker container as its own machine. It's only accessible to my private network in my Fly organization and it simply uses the same Postgres app as my server does.

Metrics

Once again, I was able to very easily integrate into the Fly ecosystem. You may have spotted in the TOML file above, I had a [[metrics]] section. Fly automatically collects metrics and reports to a managed Grafana instance. I added the swift-metrics library from Apple along with SwiftPrometheus backend for it. Then I created a Vapor route for metrics and told Fly it could use it. That's all it took!

Distributed Tracing

I haven't fully finished setting this up yet, there's just been so much to do! Sadly, Fly doesn't provide distributed tracing with their managed Grafana instance. I hope that they eventually do, and I think it's somewhere on their roadmap. For now, this is likely something I'll need to set up myself. I intend to use Grafana cloud and configure an Otel collector.

Email

Nothing super special here. I ended up going with SendGrid because why not? As it turns out, the vapor community has a SendGrid plugin that was trivial to pull in and use. I'll give an honorable mention to Twilio here, because when I open sourced the code backing this website I forgot my send grid API key was in my docker-compose file. I got a huge influx of emails from various tools that scanned the repo and discovered my mistake but most impressive was Twilio. They sent me a message that said they saw the SendGrid API key they gave me was leaked and they automatically revoked it. This was a great touch! Thank you Twilio!

Keep an eye out for more!

There's a ton to share about how this all came together. Now that I've finally built a flow for creating and editing blog posts I can get to work sharing all the exciting tech that went into migrating the blog! Expect a series of posts that go into detail. I'm also hoping to apply to some Swift conferences to talk about the experience. I truly believe that Swift on Server is production ready and competitive with so many other solutions out there!