Environments

We suggest starting out with a minimum of three environments which operate in complete isolation: local, preview, and production. Each of these environments utilizes Docker for your web application / service such that all the environments run identically, with exception to their intentional configuration differences.

Achieving environment isolation from the very beginning is critical in order to avoid expensive tech debt that is common among startup teams. Maintaining environment isolation over time as services are added is also quite easy because the patterns for the infrastruction definitions and application configurations are well-established from the beginning.

An important aspect to note is that all environments log their errors to New Relic, and of course in an isolated fashion via separate “applications” in New Relic. See error logging and alerting for more detail.

Local

The local environment is a collection of Docker containers which are orchestrated via Docker Compose. Additionally, all AWS services are simulated locally via LocalStack such that every developer’s local environment is wholly owned.

The only software that is truly required to be installed on the native machine is git, Docker and Docker Compose. This means that onboarding a new developer + computer is exceptionally easy, and we provide detailed instructions on how to set up the local environment.

Lastly, as you would expect, all code changes made locally via each developer’s IDE of choice are reflected immediately into the running application by nature of the chosen tech stack (e.g. Webpack watcher).

Preview

The preview environments are unique in that there is actually one per branch that has an open pull request (PR). Rather than have the common problem of development teams competing for limited staging resources, every developer gets their own automatically when opening a PR.

In fact, the preview environment which is created for each open PR is also used in the CI/CD pipeline for that PR to perform integration and E2E tests against. Regardless of if those tests pass or fail, the preview environment remains alive after the pipeline completes and thus can be used for manual testing, presenting a demo, sharing with the team and stakeholders, etc.

But.. how?

It is important to know that the preview environments are not physically isolated from each other akin to how local and production are, for example. Doing so would be impractical due to the massive cost, time to provision, etc. Instead, each preview environment runs on shared resources (used only by previews) in such a way that they are guaranteed not to collide and are optimized for cost.

For example, there is a single preview database which is entirely isolated from local and production. Within that single database, there are multiple “databases” (one per preview environment). As such, each preview environment uses a completely isolated logical database within a single shared physical database.

As a more complicated example, consider S3. All buckets that exist for production also exist for previews (prefixed with “preview-”), but those buckets are shared across all PRs. So, once again we have shared physical resources and need a way to isolate them logically. The logical isolation, in this case, is a bit harder and relies on the application to do the right thing. However, the easiest way to solve for this is to use UUIDs (or similar globally unique IDs) for the objects you are storing. As such, every object across all environments (not just across all the preview environments) will be unique and therefore will not suffer from possible collision in any context (S3 or otherwise). Another option is to prefix object paths with the preview environment ID that is made available within the code (e.g. “preview-102/some_image.jpg”).

Production

The production environment is easy to think about and is exactly what you would expect it to be. It is physically isolated from all other environments, yet operates identically due to the intentional parity achieved via Docker containerization, with exception to configuration differences of course.