☁ Cloud & DevOps

Development, staging and production: environments that match

Why separate environments are needed, how to keep them close to production, config via variables, realistic test data and twelve-factor principles.

Almost every software project talks about "development", "test" and "production". But these environments are often so different from each other that a test passed in one says nothing about the other. The developer uses SQLite, production PostgreSQL; staging runs on a small machine with year-old data; configuration is changed by hand in each place. A good environment setup is what makes tests and releases truly reliable.

What each environment is for

  • Development: the developer's computer or a personal environment. It must be quick to start and easy to destroy and recreate.
  • Test or integration: where the pipeline runs automated tests on every change, often in temporary environments that last a few minutes.
  • Staging: a copy as faithful as possible to production, where the release candidate is verified before it reaches users.
  • Production: the real environment, with real users and data.

Not every project needs four levels. For a small application development and production may be enough, as long as the pipeline runs serious tests. But if a staging environment exists, it must serve a purpose: a staging that doesn't resemble production only provides a false sense of security.

Parity between environments

Environments should differ as little as possible. The most insidious differences are of three kinds.

Software differences

Using a different database in development, a different language version or a different operating system makes problems surface only in production. Containers are the most effective tool for narrowing this gap: the same image, with the same versions, runs everywhere. Supporting services, such as databases, queues and caches, can also run in containers identical to those in production.

Configuration differences

It is normal for environments to have different addresses, credentials and sizes. What matters is that these differences live only in configuration and not in code. An application full of conditions like "if we're in production do this" behaves differently precisely where it hasn't been tested.

Data differences

An application tested with ten rows of data can turn out to be extremely slow with ten million. Staging should have data volumes and variety similar to production. Simply copying the production database into staging, however, brings real personal data into a less protected environment. The correct alternatives are anonymized or synthetic data, generated to reproduce the characteristics of the real data without containing information about real people.

Configuration in the environment: the twelve-factor principles

The twelve-factor app methodology, published by the developers of the Heroku platform, collects twelve principles for applications that are easy to deploy. Some concern environments directly:

Principle In practice
One codebase, many deploys The same repository is deployed to every environment
Config in the environment Addresses and credentials come from environment variables, not from code
Backing services as attached resources Databases and queues are swapped by changing an address, with no code changes
Separate build, release, run The artifact is built once and combined with each environment's configuration
Dev/prod parity Tools, versions and release timing as close as possible
Logs as event streams The application writes to standard output and the environment decides where to collect them

A simple test of whether configuration is properly separated: could you make the code public right now without exposing any credential? If the answer is no, something needs to move.

Temporary environments

A useful evolution is an ephemeral environment for every change request. When a developer opens a pull request, the pipeline automatically creates a complete environment running that version, reachable at a dedicated address. Reviewers can actually try it, not just read the code. When the request is closed the environment is destroyed.

This approach requires infrastructure as code and automatically generated test data, but it removes the bottleneck of a single staging environment contested by several people.

Protecting production from the other environments

Separating environments also means preventing them from touching each other by mistake:

  • distinct credentials for each environment, so a test script cannot write to the real database;
  • separate cloud accounts or projects for production and non-production, where possible;
  • clear visual cues, such as a colored banner in the staging interface, to avoid mixing up windows;
  • email and payments disabled or redirected in test environments, so as not to send messages or charges to real customers.

In my work the last point is the one most often forgotten: all it takes is a staging environment connected to the real email service to send a test newsletter to thousands of people.

In short

  • Each environment has a purpose: fast development, automated tests, verification in staging, real production.
  • Minimize software, configuration and data differences between environments.
  • Differences must live in configuration, read from environment variables, never in code.
  • Use anonymized or synthetic data in staging, not copies of production personal data.
  • Separate credentials and accounts, and disable real email and payments in test environments.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also