Make the simple trivial, and the complex possible
This is part of a series about some of our design principles and, more generally, about composable infrastructure.
I’ll start with a list taken verbatim from a design document I contributed to while working on Upsun a few months back:
To be clear, this was much more of a reminder to myself than anything handed down as orders. The work on Upsun was fascinating for me. For the first time in a very long while, we were capable of releasing ourselves from the shackles of another article of faith Never Break BC.
This last one, backwards compatibility, is possibly the most important one when you care about robustness and uptime. And we do care.
But Upsun is a new product offering. No existing production customers yet. We can go wild.
Things that just work
As a PaaS, a lot of our work focuses on making an infrastructure that just works and comes with a bunch of bells and whistles—features our users won’t have to think about ever again. Like making sure backups can actually be restored. That a service that shouldn’t be exposed to the internet isn’t. And that a service that should be is. Or that everything hums and scales.
All of that is the invisible baseline. If you do your job nicely, users become oblivious to it. That’s the hard stuff. Day two operations. And more than anything else, that’s what we care about.
But part of the work is to deliver magic on another level—the initial onboarding.
I know how much I enjoyed it the first time I typed
vagrant up and I had a VM running a minute later. And as much “cringe” as it was for me initially, piping a raw GitHub URL to bash
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)", and having a beautiful setup of ZSH with everything you might have hoped for, was a tantalizing experience.
But as always with software, everything has a tradeoff. In a later blog post, I’ll go into the Curse of Defaults. For now, I’ll just say that we chose to produce more friction in onboarding. Not because we like to torture people, but because we prefer having users complete a bit more upfront work and much less work later on.
So, we chose to have an explicit configuration of the application. You can’t just type
upsun up. Users must add a configuration file—a file that will explicitly define the behavior of the infrastructure that will get deployed—to the repository, commit, and push it.
Here comes the YAML
Now, we aren’t bad people, so the most minimal thing that would deploy isn’t that complex.
It won’t do much though. You can push this and you’ll have a container running in the ether. But it won’t be serving any http requests. To have some joy you would add:
Voilà. If you have an index.html at the root of your repo, when pushing it to Upsun, you’ll get a URL back. It’ll be live. And everything will be dandy. By the way, if you’re way too tired (I know I often am) to write YAML by hand, you could also use “upsun ify” and the CLI will generate one for you. As I said, we aren’t bad people.
From this little snippet, you can already figure out quite a bit. And possibly more about why we like explicitness.
- Applications is a plural. *
- An application has an explicit name and an explicit type.
But you could ask. Why an explicit configuration? When you look at it, it’s simple. Can’t we figure this out? I mean “index.html” what could it be? “thingamebob.burp”?
This is about making things as simple as they could be, but no simpler.
- If you don’t have an explicit runtime, either Node.js has to be locked at some ancient version or you risk breakage when it’s implicitly upgraded.
- If you didn’t give the app a name, you wouldn’t be able to deploy a second one in the same environment. And you wouldn’t be able to have nifty routing rules for your multi-app/microservices setup.
- And why a
locationsblock? Maybe there could be one by default. Unclear. But then it would have to be the root of the repo. And it sounds like an unsafe default. We err on the side of caution. In earlier versions, you didn’t have default routes either. But it seemed reasonable enough to imagine that if all you deploy is a single app, you probably would like for it to have an ingress route.
There are defaults that are actually set here. And you’d see everything we set as defaults when you push the repo:
- For example “As no routes configuration was detected, a single default route will be deployed.” If you had two apps, we’d error-out and force you to make a routing choice.
- Or “Environment configuration: app (type: nodejs:20, cpu: 0.5, memory: 224)”
That’s the other side of the explicitness story. It’s often fine to make some reasonable choices. But it’s vital to give the user feedback on those. So you know you can override them.
There are even more choices you could make. The “locations” block is quite a beast. You can do URL rewriting there. Configure static asset caching. Control buffering and custom headers (best served with websocket routes). But that’s probably for day two of configuration. This is what make the complex possible is about. When you have explicit configuration, you also have a way to override any default behavior. But you can also postpone complexity for when you will need it.
When all you need is simplicity, the configuration should be trivial. And, hopefully, the YAML above is not overwhelming.
But when you want to use the full power of the platform—run multiple apps with complex relationships between them, run a bunch of databases and message queues—we want that to be as simple as possible too. We need to strike a balance.
Too simple now is too complex later
If you look at a service where deploying a single app requires no configuration at all, you will also discover that either it’s impossible, or difficult, to deploy multiple apps in a single command. You’ll discover that creating a consistent preview environment—that contains all of the data of all of the services—is either hard, or impossible. Sometimes the apparent simplicity of today is the horrendous complexity of tomorrow.
These design principles are our guiding lights. We try to adhere to them. Make everything as simple as it can be. But not simpler. Even if there is friction to pay. We always think about day two. About how we make things such as that we would never have to break BC. But these days we are still in Beta! So we are still allowed to make changes. And we’d love feedback on how you find our configuration format.