Declarative configuration

On this page 7

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.

StageWhat happensWhy it matters
Parse and validateSyntax, types and references are checkedCatches errors before any API call
RefreshCurrent resource attributes are read from the providerDetects manual changes and drift
PlanDesired and actual state are comparedShows the exact diff for review
ApplyOperations run in dependency order, often in parallelOnly the difference is changed
RecordNew identifiers and attributes are storedThe next run starts from accurate data
Stages of a declarative apply cycle

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.

AspectDeclarativeImperative
Unit of descriptionDesired end stateSequence of commands
Repeated runsIdempotent by designIdempotent only if coded carefully
OrderingInferred from dependenciesWritten explicitly by the author
Change reviewPlan or diff before applyRead the script and predict effects
Edge casesHandled by the provider logicHandled by the script author
Typical toolsTerraform, OpenTofu, CloudFormation, Kubernetes manifestsShell scripts, SDK programs, CLI runbooks
Declarative and imperative approaches side by side

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.

FormatTypical useStrengthLimitation
HCLTerraform and OpenTofuExpressions, modules, loopsTool-specific syntax
YAMLKubernetes, CloudFormation, AnsibleReadable, widely supportedIndentation errors, weak typing
JSONARM templates, API payloadsStrict and machine-friendlyVerbose, no comments
BicepAzure resourcesConcise, typed, compiles to ARMAzure only
CUE or JsonnetGenerating large configuration setsStrong typing and reuseExtra learning curve
Common formats and their characteristics

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.

SymptomLikely causeRemedy
Plan shows changes nobody madeConfiguration drift from manual console editsRestrict console access, reconcile regularly
Resource unexpectedly replacedChanged immutable attributeReview plans, use lifecycle protection
Apply fails halfwayAPI limits or dependency not readyRetry, add explicit dependencies
Two runs overwrite each otherMissing state lockingUse a remote backend with locks
Very slow plansOne huge state with thousands of resourcesSplit into smaller state units
Typical problems and how to address them

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.