Skip to main content
InfraGap.com Logo
Home
Getting Started
Core Concept What is a CDE? How It Works Benefits CDE Assessment Getting Started Guide Inner Loop vs Outer Loop Environment Drift Local vs Cloud CDEs for Startups
AI & Automation
AI Coding Assistants Agentic AI AI-Native IDEs Agentic Engineering AI Agent Orchestration AI Governance AI-Assisted Architecture Shift-Left AI LLMOps Autonomous Development AI/ML Workloads CDEs for Data Science GPU Computing
Agent Infrastructure
Agent Experience (AX) Agent Egress Control Computer Use Agents Agent Evals Agent Runbooks Agent Client Protocol AGENTS.md MCP Servers Git Worktrees Kubernetes Agent Sandbox Agent Fleets Agent Identity Prompt Injection Defense Agent Observability Context Engineering AI Code Review Bottleneck Headless Agents in CI Spec-Driven Development Agent Readiness Code Provenance
Implementation
Architecture Patterns DevContainers Advanced DevContainers Language Quickstarts IDE Integration CI/CD Integration Platform Engineering Developer Portals Container Registry Multi-CDE Strategies Remote Dev Protocols Nix Environments Hermetic Builds OpenTofu for CDEs Kubernetes Development
Operations
Performance Optimization High Availability & DR Disaster Recovery Monitoring Capacity Planning Multi-Cluster Development Troubleshooting Runbooks Ephemeral Environments Sandbox Environments Workspace Snapshots Database Branching
Security
Security Deep Dive Zero Trust Architecture Secrets Management Vulnerability Management Network Security IAM Guide Supply Chain Security Air-Gapped Environments AI Agent Security MicroVM Isolation Compliance Guide EU AI Act Cyber Resilience Act Data Residency Governance
Planning
Pilot Program Design Stakeholder Communication Risk Management Migration Guide Cost Analysis FinOps GreenOps Vendor Evaluation Training Resources Developer Onboarding Team Structure Platform Maturity Model Open Source CDEs AI Productivity Paradox Build vs Buy DevEx Metrics Productivity Engineering Industry Guides CDEs for Healthcare CDEs for Financial Services CDEs for Government Edge Development WebAssembly in CDEs
Resources
Tools Comparison State of CDEs 2026 Isolation Decision Tool Template Library
Learning Paths
All Paths Platform Engineer Security and Compliance Engineering Manager
Vendor Reviews
GitHub Codespaces Coder Ona Google Workstations Microsoft Dev Box Okteto Eclipse Che DevPod Daytona E2B
Head to Head
Coder vs Codespaces Coder vs Ona Ona vs Codespaces Coder vs Okteto Self-Hosted vs Managed E2B vs Daytona CDE Market Guide CDE vs Alternatives Case Studies Lessons Learned Glossary FAQ Sources & Citations

The Core Concept of CDEs

A development environment defined as code: what that actually means, why it is different from a README or a setup script, and which problem it solves.

The Idea in One Sentence

A development environment is an artifact that can be described in a file, reviewed in a pull request, versioned alongside the application it builds, and rebuilt from scratch by a machine - not a state that each person assembles by hand and then maintains forever.

Everything else about Cloud Development Environments follows from that idea. Where the environment runs, which IDE connects to it, and who pays the compute bill are downstream decisions. The concept itself does not require a cloud at all: a devcontainer that builds on a laptop is environment-as-code, and a hand-configured cloud VM that someone SSHed into and installed packages on is not. The distinction is whether the definition exists as a durable, executable description or only as accumulated state on a disk.

That is worth stating plainly because the vendor conversation almost always starts in the wrong place - with a platform, a pricing page, and a demo. The useful question is narrower: is the environment your team develops in a described thing or an assembled thing? Teams that answer "described" get most of the value regardless of which product they buy. Teams that answer "assembled" do not get the value even after they buy one.

The Failure Mode It Solves

The problem is not that setting up a machine is hard the first time. It is that the setup has no single source of truth, so every copy of it diverges from every other copy the moment it exists. This is environment drift, and it produces a specific and familiar set of costs.

Onboarding becomes archaeology

A new engineer follows the README, hits a step that no longer works, and asks in chat. Someone who solved it eight months ago remembers half the fix. The knowledge that actually makes the project build lives in people, not in the repository.

Bugs that only reproduce on one machine

A test passes locally and fails in CI, or vice versa. The cause is a different patch release of a runtime, a system library the base image happens to carry, or a stale environment variable. Debugging the difference costs more than the original defect.

Upgrades that nobody can coordinate

Moving the team to a new language runtime means every engineer performs the same migration independently, at a different time, with a different result. There is no place to make the change once.

No answer to "what was installed"

When a supply-chain advisory lands, an auditor asks a question, or an incident needs reconstruction, an assembled machine cannot tell you what it contained six weeks ago. A described environment can, because the description is in version control.

Notice that none of these are solved by making laptops faster or by writing a better README. They are all consequences of the same missing property: the environment has no authoritative definition. Adding one is the entire concept.

What "Environment as Code" Actually Includes

"Environment" is vaguer than it sounds, and half-finished attempts usually fail because they codify one layer and leave the rest to chance. A complete definition covers five things. A devcontainer that pins the toolchain but tells engineers to "get a database from somewhere" has codified one of the five.

1. The toolchain

Compilers, interpreters, build tools, linters, formatters, package managers, and the CLIs the project assumes are on PATH. Pinned to exact versions, not ranges. "Node 22" is a range; a specific patch release plus a lockfile is a version. The difference shows up the week a patch release changes behavior.

2. Dependencies, including the invisible ones

Application libraries are usually already pinned by a lockfile. The ones that bite are system-level: the image-processing library, the SSL implementation, the locale data, the fonts a PDF renderer needs. These are the packages nobody remembers installing, which is exactly why they need to be in the definition rather than in someone's shell history.

3. Backing services

Databases, caches, message brokers, object storage, and the stubs standing in for third-party APIs. A definition that stops at the language runtime leaves the hardest part of setup uncodified. This is also where teams make a real architectural choice: run services as sidecar containers inside the environment, or point the environment at shared ephemeral instances provisioned per branch.

4. Access to secrets, not the secrets themselves

The definition should describe how the environment obtains credentials - which identity it assumes, which vault path or secrets manager it reads, which scopes it is granted - and never contain the credential values. Codifying the access path is what makes rotation possible and what keeps the definition safe to review in a public pull request.

5. Resource shape

CPU, memory, disk, and whether an accelerator is attached. On a laptop this is fixed by whatever hardware the company bought. Once the environment is described rather than assembled, resource shape becomes a parameter of the definition - which is what makes it possible to give a compile-heavy service more cores than a documentation site without buying anyone a new machine.

Declarative vs Imperative: Why the Distinction Matters

Writing the setup down is necessary but not sufficient. A bash script writes the setup down and still drifts. The property that matters is whether the file describes the desired end state or the steps taken to reach it.

Imperative: a list of steps

#!/usr/bin/env bash
apt-get install -y postgresql-client
npm install -g pnpm
curl -o /usr/local/bin/tf https://example/tf
cp .env.sample .env

Running it twice may not be safe. Running it a year later installs whatever the upstream repositories serve that day. It cannot tell you what the machine currently looks like, only what someone once tried to do to it. Nothing detects that an engineer edited .env by hand afterwards.

Declarative: a description of the result

{
  "image": "ghcr.io/org/base@sha256:9f2c...",
  "features": {
    "node": { "version": "22.11.0" },
    "postgres-client": { "version": "16" }
  },
  "hostRequirements": { "cpus": 8, "memory": "16gb" }
}

The file states what must be true. The tool decides how to get there, and can apply the same description any number of times with the same result. Because the desired state is knowable, the difference between it and reality is also knowable - which is what drift detection is.

Three properties come from the declarative form, and all three are the reason this distinction is not pedantry:

  • Idempotence. Applying the definition to an environment that already matches it changes nothing. This is what makes it safe to run automatically, on every start, without anyone thinking about it.
  • Diffability. A change to the environment shows up as a reviewable diff in a pull request. Upgrading a runtime becomes one commit that every environment inherits, rather than a message asking everyone to please upgrade.
  • Reproducibility. Given the same definition and the same pinned inputs, you get the same environment - which is the property that lets you throw an environment away rather than repair it. Disposability is a consequence of reproducibility, not a separate feature.

In practice most real definitions are a declarative outer shell with a small imperative escape hatch inside it, and that is fine. The failure is when the imperative part grows until the declarative part is only a wrapper around a five-hundred-line install script. A useful rule of thumb: anything in the imperative section should be safe to run twice.

Documentation, Script, Definition

Most teams are already on one of three rungs. Knowing which one clarifies what the next step actually buys.

ApproachExecuted byFails whenCan it answer "what is installed?"
README instructionsA human, by handImmediately, and silently - prose has no testNo
Setup scriptA shell, onceWhen upstream changes, or on a machine in a different starting stateNo - only what it attempted
Declarative definitionA tool, on every startWhen inputs are unpinned or a mutable tag is usedYes - the definition is the answer

The third rung is not automatically reproducible. A definition that references a floating tag such as ubuntu:latest, or that runs a package manager without a lockfile, produces a different environment every week while looking perfectly declarative. Pinning inputs by digest or lockfile is what turns a description into a guarantee. This is the single most common reason a team adopts devcontainers and still gets drift.

The Mechanisms

Several tools implement the concept, and they operate at different layers rather than competing for the same job. Most real setups use two: one that describes the machine the environment runs on, and one that describes the environment itself.

Terraform

Describes the infrastructure the environment sits on - the instance, the network, the disk, the identity it assumes. It keeps a state file recording what it created, which is how it can compute a plan before changing anything. It is the outer layer: it does not care what tools are inside the box.

OpenTofu

A Linux Foundation project that forked from Terraform after HashiCorp moved Terraform to the Business Source License in 2023. It keeps the same configuration language and provider ecosystem under an open source license, and has added features of its own such as state encryption. For teams whose objection is licensing rather than capability, it is a drop-in concern rather than a rewrite. See the OpenTofu overview.

Pulumi

Takes the same declarative model but expresses it in general-purpose languages - TypeScript, Python, Go, C#, Java - rather than a dedicated configuration syntax. The trade is real in both directions: you gain loops, types, and unit tests over your infrastructure code, and you give up the constraint that kept configuration simple. Teams that already maintain shared internal libraries tend to like it; teams that want infrastructure to stay boring often do not.

DevContainers

An open specification for describing the environment itself in a devcontainer.json file that lives in the repository: base image, features to layer on, ports to forward, editor extensions, and lifecycle commands. It is the layer closest to the developer and the easiest place to start, because the file works locally and in a hosted workspace without change. See DevContainers and advanced patterns.

Nix

Goes furthest on reproducibility by making every package a pure function of its inputs, so an identical definition yields a bit-for-bit identical result years apart without depending on a container registry. The cost is a distinct language and a genuine learning curve, which is why it tends to be adopted by teams that have already been burned by drift rather than by teams starting out. See Nix environments.

Kubernetes manifests

Where the environment is a pod rather than a VM, the pod spec is itself a declarative definition of resource shape, volumes, service accounts, and network policy. This is common when a platform team already runs a cluster, since the scheduling, quota, and isolation machinery is work they do not have to build again. See Kubernetes development.

What the Concept Does Not Fix

Environment-as-code moves work rather than removing it. The setup that used to be duplicated across every laptop becomes a definition that someone has to own, keep current, and debug when it breaks for everyone at once. That is a good trade, but it is a trade: the definition is now a shared dependency with a blast radius, and a bad merge to it blocks the whole team rather than one person.

It also does not, by itself, make anything faster. A described environment still has to be built, and if the definition is written carelessly - unpinned inputs, layers that invalidate on every commit, a build that compiles from source - it can be considerably slower to start than the laptop it replaced. Speed comes from caching and prebuilding the described result, which is a separate piece of engineering.

And it says nothing about where the environment runs. Putting it in a cloud adds a different set of benefits and a different set of costs, which is a decision to make on its own evidence rather than one that falls out of adopting the concept. Those are covered in the mechanics and in the benefits and trade-offs.