Does a ‘perfect’ dev environment exist?
Tags: devops, development, backend, developer
Published: 2024-02-13 22:25:26

That’s likely a loaded question and it comes with a further set of qualifiers:

- How resource intensive is the work? Cloud or local?
- Are we talking about teams or individuals?
- What types of machines are we working with?
- What do you mean by environment? Do you mean your IDE? Internal dev stack?

To avoid the risk of sparking any holy wars, let’s lay some ground rules. Imagine a scenario where we have a team of 15 or so engineers and we’re working on an increasingly complex product where we want to iterate quickly. Our product has a lot of moving pieces and we’re starting to struggle with devs not having shared context for their dev environments. Different setups and inconsistent local environments are slowing us down. Sound familiar?

We’ve experienced it, we have friends who’ve run into it, some of this might even resonate with you.

> Development is a fundamentally creative act and the canvas of development environments needs to enable engineers, not deter them.
> 

This leads us to our quintessential question, what makes the ‘perfect dev environment’? For starters, we believe they should have all of the following characteristics:

- Parametrizable: Save time by encoding common use cases in the dev environment.
- Overridable: Retain flexibility by allowing for changes outside the predefined cases.
- Approachable: Interfacing with your tooling should be intuitive.
- Reliable: The environment should consistently spin up and work as expected.
- Ephemeral: Easily spin up and clean up after yourself.
- Distributable: Ensure everyone can easily access the dev environment.

Let's break these down.

### Parametrizable: Save Time with Common Cases

Parameterization allows you to encode common use cases in your dev environment, letting you flex common interaction points while skipping the boilerplate repetition. Picture your dev environment with a Postgres database. You’ll need to input a username, generate test data, load that data, connect an API to your database, and more. These steps have different inputs however stay consistent.

In the near term, this saves you time in skipping tedium, but in the long term, especially as your setup grows in complexity, it saves mental bandwidth as every dev doesn’t need intimate knowledge of a system to interact with it. Allowing devs to simply configure the parts they need (username, API keys, etc.) while abstracting away the rest (loading data) ensures devs get the exact configuration they need without needing any knowledge of how the environment was set up.

### Overridable: Stay Flexible for Uncommon Cases

So we can handle common cases, but what about the uncommon ones? Can we make changes outside the tyranny of what our environment author has set for us? This leads us to environments being overridable. Without the ability to override your dev environment, you’re stuck with the set of tools already written by someone else. If you aren’t already very familiar with those tools, you’d have to dive into that code to understand it, especially well enough not to break it.

Taking our Postgres example, imagine an internal tool that spins up a Postgres database, web services, and test data. If we wanted to change something that wasn’t yet parameterized — e.g. the flags Postgres starts with — we’d have to dive into Docker, k8s, or whatever else is used to start the dev stack. If you’re using bash scripts, you might break something in your local dev environment requiring more time for debugging. Best case scenario, nothing breaks and you’ve made the edits you expected but you’ve devoted quite a bit of time to achieve that. By allowing your environment to be overridable, you don’t need to deeply understand the system to make changes.

### Approachable: How Intuitive is Your Setup?

Depending on the sophistication of your setup, it may be challenging to interact with your dev environment. Let's say your DevOps team has decided on a CLI and made this tool fully featured with a myriad of wonderful detailed options. As the tool becomes more powerful, this CLI becomes more complex and challenging to use.

When a dev is dumped into a tool that isn’t walk-up intuitive, they’re met with the choice of “Do I learn this tool or do I just hack something that’ll work right now?” Increasing approachability with an intuitive tool helps them pick the former. This is easier said than done as achieving this requires both a product mindset as well as a DevOps/platform mindset, or at the very least great docs which no engineer likes to write.

### Reliable: Your Environment Should Just Work ™️

When spinning up the environments, it needs to just work. These environments need to spin up as expected when presented with different directories, different configurations of dev machines, and more. As many would guess, this is easier said than done. Flaky dev environments not only are frustrating but also result in costly delays.

Scripting languages are challenging for writing safe code. It’s challenging to catch configuration errors in advance, instead, they need to be caught at runtime. You need to implement waits and health checks correctly i.e. docker by default doesn’t wait for ports to become available and k8s has health checks but users need to configure them.

### Ephemeral: Clean and Consistent

An ideal dev environment is also ephemeral. It’s important that you can spin these environments up and clean up after it trivially. This ensures consistency as team members work off a clean slate baseline environment. They’re also resource-efficient and scalable as you can clean up environments not in use and scale multiple instances as needed.

Ephemeral environments are useful for a variety of uses across quick dev iterations and end-to-end testing, they do require sophisticated infrastructure or tooling due to resource management, data persistence, and more, however, some upfront pain pays dividends.

### Distributable: Get Your Tooling to Everyone

The last I’ll mention is the environment being distributable. So you have a highly flexible dev environment that is incredibly easy to spin up for common use cases, but easy to revise for uncommon ones. Sounds great but it’s only valuable if everyone can get their hands on it.

Smaller teams can likely get by as everyone can clone the repo and individually ensure they have the latest. However as the team grows and work becomes more complex, it becomes challenging to distribute the latest environment, ensuring everyone is working on the same thing and it's in sync with relevant tools. The best tooling is most valuable if it’s uniformly adopted by the people it’s meant for.

### So where do we land?

Often, how easy achieving these depends on how good your platform engineer is, their familiarity with bash, compose, k8s, and their product mindset for aforementioned approachability. It’s a tall ask creating the ‘perfect dev environment’ given high demands for optimal developer experience, a fast-moving landscape, and technical barriers.

Luckily, we built Kurtosis so that you don’t have to make that choice. Development environments written into Kurtosis packages achieve all of the above and more. If you’d like to learn more, [check out monorepo and consider dropping a star while you’re there.](https://github.com/kurtosis-tech/kurtosis/tree/main)