Declarative configuration is an approach in which an engineer describes the desired end state of a system, such as which networks, servers, databases and permissions should exist, and a tool computes and executes the steps needed to reach that state. In infrastructure as code for the cloud, it is the dominant model: the configuration files say what should exist, not how to create it.
The idea separates intent from procedure. A reconciling engine compares the declared state with the actual state of the platform, calculates the difference and applies only the changes required. Running the same configuration twice should produce no further changes, a property called idempotence, which makes declarative files safe to apply repeatedly, review in version control and reuse across environments.
How declarative configuration reaches the desired state
A declarative tool works in a read, compare, act cycle. It reads the configuration, reads the current state of the real resources, builds a dependency graph and derives an ordered plan of create, update and delete operations. The user never writes that order; the engine infers it from references between resources, for example a subnet that refers to a virtual network.
Most tools keep a record of what they manage. Terraform and OpenTofu store a state file that maps each declared resource to a real resource identifier, while Kubernetes stores the declared objects in its own API server and lets controllers reconcile them continuously. Cloud-native template services such as AWS CloudFormation and Azure Resource Manager keep the state on the provider side.
The following table shows the stages of a typical declarative run and what each stage protects against.
| Stage | What happens | Why it matters |
|---|---|---|
| Parse and validate | Syntax, types and references are checked | Catches errors before any API call |
| Refresh | Current resource attributes are read from the provider | Detects manual changes and drift |
| Plan | Desired and actual state are compared | Shows the exact diff for review |
| Apply | Operations run in dependency order, often in parallel | Only the difference is changed |
| Record | New identifiers and attributes are stored | The next run starts from accurate data |
Declarative compared with imperative configuration
The opposite model is imperative configuration, in which a script or runbook lists the commands to run in order. Imperative scripts are easy to start with but must handle every possible starting condition themselves: a script that creates a virtual machine fails or duplicates it when the machine already exists, unless the author adds explicit checks.
Declarative files trade that procedural control for convergence: whatever the starting point, the engine moves the system toward the same result. The comparison below summarizes the practical differences.
| Aspect | Declarative | Imperative |
|---|---|---|
| Unit of description | Desired end state | Sequence of commands |
| Repeated runs | Idempotent by design | Idempotent only if coded carefully |
| Ordering | Inferred from dependencies | Written explicitly by the author |
| Change review | Plan or diff before apply | Read the script and predict effects |
| Edge cases | Handled by the provider logic | Handled by the script author |
| Typical tools | Terraform, OpenTofu, CloudFormation, Kubernetes manifests | Shell scripts, SDK programs, CLI runbooks |
The boundary is not absolute. Ansible playbooks are executed task by task, yet most of its modules are declarative at the task level, stating that a package should be present rather than installing it unconditionally. Tools such as Pulumi and the AWS CDK use general-purpose programming languages, but they still produce a declarative model that a deployment engine reconciles.
Languages and formats for declarative configuration
Declarative configuration is expressed in a domain-specific language or a data format. The choice affects how much logic is possible, how readable the files remain and how easily they are validated.
| Format | Typical use | Strength | Limitation |
|---|---|---|---|
| HCL | Terraform and OpenTofu | Expressions, modules, loops | Tool-specific syntax |
| YAML | Kubernetes, CloudFormation, Ansible | Readable, widely supported | Indentation errors, weak typing |
| JSON | ARM templates, API payloads | Strict and machine-friendly | Verbose, no comments |
| Bicep | Azure resources | Concise, typed, compiles to ARM | Azure only |
| CUE or Jsonnet | Generating large configuration sets | Strong typing and reuse | Extra learning curve |
Whatever the format, good practice keeps configuration parameterized by environment: one module describes a component, and small variable files set sizes, regions and names for development, staging and production. This keeps environments structurally identical while allowing controlled differences.
Designing declarative configuration in practice
Teams that use declarative configuration well treat the files as the single source of truth. Every change goes through a pull request, the plan output is attached for review, and an automated pipeline applies the approved change. This workflow, often called GitOps when an agent pulls the configuration from a repository, gives an audit trail and a simple rollback path through version history.
- Split configuration into small state units by lifecycle, for example network, shared data services and applications, so one apply cannot touch everything.
- Store state remotely with locking enabled so that two pipelines never write the same state at the same time.
- Pin provider and module versions, because an upgraded provider can change defaults and produce unexpected plans.
- Mark data that must survive with deletion protection or lifecycle rules that prevent accidental destruction.
- Keep secrets out of the files and reference them from a secrets manager instead.
Limitations and common mistakes
Declarative configuration is only as accurate as the provider logic behind it. When a resource attribute cannot be updated in place, the engine may plan a destroy and recreate, which for a database or a load balancer address can mean downtime or data loss. Reading the plan carefully is therefore not optional.
The table below lists frequent problems, their usual cause and the corrective practice.
| Symptom | Likely cause | Remedy |
|---|---|---|
| Plan shows changes nobody made | Configuration drift from manual console edits | Restrict console access, reconcile regularly |
| Resource unexpectedly replaced | Changed immutable attribute | Review plans, use lifecycle protection |
| Apply fails halfway | API limits or dependency not ready | Retry, add explicit dependencies |
| Two runs overwrite each other | Missing state locking | Use a remote backend with locks |
| Very slow plans | One huge state with thousands of resources | Split into smaller state units |
A frequent misconception is that a declarative tool continuously enforces the state. Terraform and similar tools act only when someone runs them; between runs, the real infrastructure can diverge. Continuous enforcement requires a reconciliation loop, such as a Kubernetes controller or a scheduled pipeline that detects and corrects drift.
A non-technical view
Imagine giving a builder a finished floor plan instead of a list of instructions. The plan says there should be three rooms, two windows and one door. The builder looks at the house, sees what is already there and adds only what is missing. If you hand over the same plan tomorrow, nothing changes, because the house already matches it.
Declarative configuration works the same way for computer systems. People describe the result they want, and software figures out the steps. This makes changes predictable, repeatable and easy to review, because everyone can read the plan before anything happens.
A concrete example
A small platform team runs an online booking service in two cloud regions. Its configuration declares a virtual network with three subnets, a managed PostgreSQL database with deletion protection, an autoscaling group of six to twenty application servers and a load balancer. Development, staging and production use the same modules with different variable files.
When the team needs a larger database, an engineer changes one value from 4 to 8 virtual CPUs and opens a pull request. The pipeline produces a plan showing one in-place update and no replacements. A colleague approves it, the change is applied during a quiet hour, and the next scheduled plan reports no differences. A week later, the nightly plan flags a firewall rule someone added by hand; the team removes it and the environment returns to the declared state.