My earliest infrastructure instructions lived in documents.
1. Open the cloud console.
2. Create a virtual network.
3. Add three subnets.
4. Create a security group.
5. Launch a virtual machine.
6. Attach the security group.
7. Copy the public address into the application configuration.
The document was useful until somebody followed it.
After that, the real infrastructure existed in the cloud platform while the document remained a description of what somebody had once intended to create.
| Document | Real environment |
|---|---|
| Three subnets | Four subnets |
Perhaps an engineer added the fourth subnet during an incident. Perhaps one of the original subnets was deleted. Perhaps the instructions were updated without changing the environment. Perhaps the environment changed without updating the instructions.
The document could tell me what to click. It could not prove what existed.
A shell script improved matters:
cloud network create bfstore-dev
cloud subnet create \
--network bfstore-dev \
--name bfstore-dev-private-a \
--cidr 10.20.10.0/24
Now the instructions were executable. They could be stored in Git, reviewed and run consistently.
But another problem appeared when I ran the script again:
network already exists
subnet already exists
operation failed
The script described a sequence of creation commands. It did not describe the enduring infrastructure outcome.
I could add checks. Then I would need more checks for values, replacements, deletions, concurrent writers and partial failures. Soon the script would need to know which resources already existed, whether they matched, which changes were possible in place and how to recover when the world changed halfway through.
The script was becoming a small infrastructure controller.
Infrastructure as Code gives that problem a more deliberate model. Instead of recording only the actions used to create infrastructure, I record the intended infrastructure itself.
resource "example_network" "bfstore_dev" {
name = "bfstore-dev"
cidr = "10.20.0.0/16"
}
A declarative tool combines several forms of evidence:
configuration
+
prior state
+
provider observations
│
▼
plan
│
▼
apply
│
▼
new state
That gives me the central idea:
Infrastructure as Code makes infrastructure intent executable, reviewable and repeatable. Declarative infrastructure compares that intent, remembered resource identity and the existing world to calculate a route between them.
The configuration is not merely a list of commands. It becomes part of the system’s account of what the infrastructure should be.
One qualification matters from the beginning:
Terraform is convergent when it runs. It is not normally a continuously running controller by itself.
A Kubernetes controller remains active and repeatedly reconciles resources. The Terraform CLI plans when somebody or something invokes it, applies when instructed, writes state and exits. Scheduled pipelines, remote-run platforms and drift-monitoring services can run it repeatedly, but that recurrence belongs to the operating system built around Terraform.
A declaration does not maintain itself.
1. From instructions to infrastructure intent
Infrastructure as Code means representing infrastructure through machine-readable files that participate in an engineering workflow.
Those files can be version controlled, reviewed, validated, tested, released, audited and reused. A virtual network created through a console click may be perfectly functional. The problem is not that clicking is inherently impure. The problem is that the reason for the decision can disappear into the resulting resource.
A network may exist, but the cloud object alone may not explain:
- why its address range was selected;
- which environment owns it;
- who approved it;
- how another environment should reproduce it;
- what should happen if it changes;
- which controls are expected to remain attached.
Code can preserve more of that intention.
resource "example_network" "bfstore_dev" {
name = "bfstore-dev"
cidr = "10.20.0.0/16"
tags = {
project = "bfstore"
environment = "dev"
managed_by = "terraform"
}
}
The cloud object remains real infrastructure. The code is the repeatable declaration used to manage it.
That does not automatically make the infrastructure good.
Infrastructure as Code can reproduce a poor design with exceptional consistency. Code does not guarantee secure defaults, least privilege, sensible network boundaries, tested recovery, affordable capacity or safe deletion. It gives engineering practices somewhere to attach. The design still needs judgement.
Imperative automation still has a place
An imperative approach tells the system which actions to perform:
create network
create subnet
create route table
attach route table
create firewall rule
Imperative automation remains useful when:
- the operation is finite;
- the sequence itself matters;
- the target has no useful declarative interface;
- the work is diagnostic or corrective;
- an operator needs direct control;
- the desired result cannot be represented completely.
It is not the primitive ancestor that declarative tooling arrived to banish.
Most declarative systems eventually execute imperative API calls. The difference lies in who calculates them.
| Style | Who chooses the actions? |
|---|---|
| Imperative | The engineer or script |
| Declarative | The engineer describes the outcome; the tool calculates actions |
Declarative infrastructure is valuable because the declaration remains meaningful after the original command has finished.
A direct command says:
Do this once.
A declaration says:
This is what we are prepared to keep claiming should be true.
2. Declarative configuration describes a resource graph
A declarative configuration describes resources and relationships.
resource "example_network" "bfstore_dev" {
name = "bfstore-dev"
cidr = "10.20.0.0/16"
}
resource "example_subnet" "private_a" {
name = "bfstore-dev-private-a"
network_id = example_network.bfstore_dev.id
cidr = "10.20.10.0/24"
}
The subnet refers to:
example_network.bfstore_dev.id
That reference carries a value and reveals a dependency. The tool can infer that the network must exist before the subnet can be created.
network
│
▼
subnet
Declarative configuration therefore captures part of the infrastructure topology, not only isolated values.
Dependencies should be real, not decorative. Sometimes an operational dependency is not visible through an ordinary attribute reference, so an explicit dependency is necessary:
depends_on = [
example_policy.network_ready,
]
That mechanism should be used carefully. Adding explicit dependencies everywhere recreates a shell script in graph form and prevents independent work from happening concurrently.
Providers translate declarations into remote operations
Terraform itself does not contain native knowledge of every cloud service. Providers translate Terraform’s resource model into external API operations.
Terraform configuration
│
▼
provider
│
▼
cloud or platform API
│
▼
remote resource
A provider defines resource types, configurable arguments, computed attributes, lifecycle behaviour and import identity.
That means provider versions affect infrastructure behaviour. A configuration can remain unchanged while a provider upgrade changes validation, default handling, diff calculation or update logic.
Provider upgrades are infrastructure changes.
A responsible workflow should include:
- version constraints;
- dependency-lock updates;
- release-note review;
- representative plans;
- module compatibility tests;
- state-backup verification;
- controlled rollout.
terraform {
required_providers {
example = {
source = "vendor/example"
version = "~> 4.2"
}
}
}
The version constraint defines acceptable provider versions. The dependency lock file records the selected provider versions and checksums.
Those are not the same thing.
The .terraform.lock.hcl file locks provider selections. It does not currently lock remote module versions. Remote module sources still need explicit versioning where reproducibility matters.
Desired state is a management contract
A remote resource may expose hundreds of properties. The provider may support only some of them. The platform may generate values, apply defaults or expose read-only attributes.
| Property class | Example |
|---|---|
| Configured argument | Name, CIDR, retention period |
| Computed attribute | Resource ID, assigned address |
| Platform default | Provider-selected or API-selected value |
| Immutable property | A change may require replacement |
| Ignored difference | A change the configuration deliberately does not manage |
| Sensitive value | A value requiring restricted handling |
Declarative infrastructure is not a complete copy of the remote object. It is a contract describing which properties the configuration intends to manage.
That contract has an authority boundary. Two active states should not normally manage the same remote object or repeatedly write the same property.
One remote object should normally have one Terraform resource instance in one active state as its owner.
Two states can lock themselves perfectly and still overwrite one another because state locking does not lock the cloud resource.
3. A plan proposes the route between intent and reality
One of Terraform’s strongest features is the plan.
A plan answers:
Given this configuration, this prior state, these provider rules and the remote objects currently observed, what actions does Terraform propose?
A simplified plan may contain:
+ create network bfstore-dev
~ update tags on security group catalog
- destroy obsolete subnet bfstore-dev-public-b
-/+ replace database instance
The plan separates intention from execution. It gives an engineer a chance to inspect consequences before applying them.
Declarative syntax is calm. Infrastructure consequences may not be.
Changing one string may require replacing a network, its subnets and everything that depends on them. The plan is where the tool explains the route between the declaration and the existing world.
A plan can contain unknown values
Not every final value is available during planning. Generated IDs, remote defaults and values that depend on resources being created may appear as:
(known after apply)
A plan is therefore not always a complete table of final infrastructure values. It is an action graph containing:
- known values;
- unknown placeholders;
- provider assumptions;
- dependencies;
- proposed lifecycle actions.
A plan is stronger than guessing. It is not a prophecy sealed in amber.
The world can change after the plan is calculated. Another operator may change a resource, a quota may be consumed or a provider API may return a different result. Apply can still fail or discover that an assumption no longer holds.
Speculative plans and execution plans are different artefacts
A pull-request plan may be speculative. It predicts what a later apply would do, but it may not be the exact artefact eventually executed.
A saved execution plan can be produced locally:
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan
That allows the reviewed plan to be passed directly to apply.
Remote-run platforms may use different mechanics. A speculative plan may be review-only, while the approved run calculates or stores a separate applyable plan.
The workflow should answer one explicit question:
Is the plan being reviewed the plan that will be applied?
If the answer is no, the later plan needs its own review or an equivalent trust boundary.
Plan files are sensitive
Saved plan files can contain configuration, variable values, resource values and sensitive material even when terminal output redacts it.
They should be treated like state:
- do not commit them to version control;
- restrict access;
- encrypt storage where appropriate;
- retain them only as long as needed;
- audit who can retrieve them.
A plan can be a useful change artefact without being a harmless document.
Replacement order is part of the story
Replacement can occur in more than one order:
destroy old
create new
or, when configured and feasible:
create new
move dependants
destroy old
Terraform’s create_before_destroy lifecycle setting can influence that ordering:
lifecycle {
create_before_destroy = true
}
It cannot make every replacement safe. Unique names, fixed addresses, attached storage, quotas, dependent systems and provider behaviour may prevent overlap.
The plan must be read as an operational story:
- Which resource exists first?
- Can both exist simultaneously?
- Which identity moves?
- When do dependants switch?
- What data migration is required?
- Which failure leaves the environment in a recoverable state?
4. State remembers resource identity
Consider this configuration:
resource "example_network" "bfstore_dev" {
name = "bfstore-dev"
cidr = "10.20.0.0/16"
}
The platform may identify the real network as:
network-7f31c2
Terraform state records the mapping:
example_network.bfstore_dev
│
▼
network-7f31c2
Configuration describes the intended resource. State records which real object Terraform believes satisfies that declaration.
State is not merely a cache of convenient output values. It is part of Terraform’s memory of resource identity.
That makes it operationally important.
State can contain sensitive information
State may contain:
- remote resource identifiers;
- configured values;
- computed attributes;
- dependency information;
- outputs;
- provider metadata;
- values needed to calculate future changes.
Marking a value sensitive hides it from ordinary display. It does not necessarily remove it from state or plan files.
Current Terraform also supports stronger mechanisms in supported contexts:
| Mechanism | Effect |
|---|---|
sensitive = true |
Redacts ordinary display; value may remain in state and plans |
ephemeral = true |
Omits eligible values from state and plan |
| Write-only argument | Passes a value to a supporting resource without persisting it |
| Backend encryption | Protects stored state according to backend capability |
Ephemeral variables and eligible ephemeral values require Terraform 1.10 or later. Provider-supported write-only arguments require Terraform 1.11 or later.
These features reduce persistence, but they do not remove the need for secure secret delivery, narrow permissions and careful provider selection.
A sensitive marker is not a cryptographic force field.
Remote state needs security and recovery controls
Local state can be suitable for learning or isolated experiments. Shared environments need a backend designed for collaboration.
A production state system should address:
- restricted access;
- encryption in transit and at rest;
- version history;
- backups;
- restore testing;
- audit;
- retention;
- locking;
- disaster recovery.
Treating state as a disposable build artefact can leave the infrastructure running while the management system forgets what it owns.
That is a particularly expensive form of amnesia.
Locking is backend-dependent
Terraform locks state for supported operations when the selected backend supports locking. Not every backend provides the same mechanism, and some require explicit configuration.
A production workflow should verify that locking exists and works.
State locking serialises Terraform writers against one state. It does not prevent:
- a console user changing a resource;
- another state managing the same object;
- another management system calling the same API;
- an external service modifying a managed attribute.
Those are ownership and drift problems, not state-lock problems.
Disabling locking in normal automation should be exceptional and deliberate.
Drift is a comparison, not a moral judgement
Drift is the difference between declared and observed infrastructure.
Suppose configuration declares three replicas and an operator changes the remote platform to five. A later plan may propose restoring three.
That does not tell us whether the manual change was wrong.
It may have been:
- an emergency mitigation;
- a temporary diagnostic action;
- a provider workaround;
- a controlled migration;
- an accidental change;
- evidence that the repository is stale.
The tool can identify a difference. The organisation must decide which side represents the intended truth.
A manual change should eventually be codified, reverted or explicitly recorded as temporary. Otherwise a later apply may remove it without understanding why it existed.
Terraform can only compare attributes exposed by providers and included in its management contract. Unsupported, ignored or externally derived properties may not appear as actionable drift.
Refresh-only operations update knowledge
Terraform normally refreshes its in-memory understanding of managed resources while planning. A refresh-only plan or apply can update state to match remote observations without proposing ordinary infrastructure mutation.
The standalone terraform refresh command is deprecated in favour of refresh-only planning and application.
This can be useful when the intent is:
Update Terraform’s recorded knowledge of changes made elsewhere.
It is not a substitute for resolving ownership. Recording drift does not explain who should manage the property next.
5. Resource lifecycle has real consequences
Declarative infrastructure makes absence meaningful.
If a resource leaves the configuration, Terraform may propose destroying the remote object.
A version-controlled deletion is still a deletion.
Git history is not a database backup.
Important resources may need:
- platform deletion protection;
- lifecycle checks;
- policy gates;
- specialist approval;
- final backups;
- restore verification;
- handover procedures.
Partial apply is possible
Terraform apply is not a database transaction across cloud APIs.
create network: success
create subnet: success
create gateway: failure
The environment is partially changed. Terraform records completed actions in state where possible. A later plan can continue reconciliation or reveal what requires deliberate repair.
This is why provider operations need to be repeatable and why recovery needs to be designed before a destructive change.
Infrastructure does not have global rollback
Reverting Git can restore an earlier declaration. Applying that declaration may propose reverse actions.
It cannot guarantee restoration of the earlier world.
- Deleted data may be gone.
- A public address may have been reallocated.
- A resource name may be unavailable.
- A replacement database may contain new data.
- An external system may have consumed a generated endpoint.
- Provider behaviour may have changed.
Git revert
│
▼
earlier declaration
not
earlier reality
Infrastructure recovery may require migration, backup restoration, compensation or operator repair.
Apply success is not capability success
Terraform can successfully create a network, cluster, database and load balancer while the application remains unusable.
Possible causes include:
- incorrect routes;
- DNS delay or error;
- blocked traffic;
- invalid credentials;
- missing schema;
- failing health checks;
- unavailable dependencies.
The provider API result proves that API operations succeeded. It does not prove that the intended capability works.
Post-apply verification may include:
- connectivity tests;
- service health checks;
- policy checks;
- backup checks;
- logging and alert verification;
- application smoke tests;
- recovery evidence.
A successful apply is not the final proof.
6. Modules and state boundaries encode ownership
A module groups related infrastructure decisions behind an interface.
module "bfstore_dev_network" {
source = "app.example.com/platform/network/example"
version = "2.1.0"
name = "bfstore-dev"
cidr = "10.20.0.0/16"
availability_zones = [
"zone-a",
"zone-b",
"zone-c",
]
}
Internally, the module may create networks, subnets, routes, gateways, flow logs and tags.
The caller supplies a smaller set of intentional inputs. The module encodes an approved implementation pattern.
A module can make the secure, observable and supportable path the easy path.
It can also become a black box.
Users still need to understand:
- what it creates;
- which resources are public;
- which data is durable;
- how upgrades work;
- what replacement means;
- what happens during deletion;
- who supports it.
A module is an internal product. It needs documentation, versioning, tests and ownership.
Variables create a supported interface
Variables should express meaningful decisions:
variable "environment" {
type = string
validation {
condition = contains(
["dev", "staging", "prod"],
var.environment
)
error_message = "Environment must be dev, staging or prod."
}
}
A module with fifty loosely related switches may expose all of its internal wiring while still calling itself an abstraction.
A strong interface makes supported decisions explicit.
Outputs are APIs
Outputs connect infrastructure layers:
output "network_id" {
value = example_network.bfstore_dev.id
}
Infrastructure outputs are APIs. They should be intentionally named, documented and kept stable where practical.
Direct cross-state access can create broad coupling and broad read permissions. Provider-specific data sources or deliberately published configuration may be safer than granting another layer access to an entire state snapshot.
A consumer should receive only the information it needs.
State boundaries should follow lifecycle and authority
There is no universal perfect state boundary.
A useful boundary tends to group resources that:
- change together;
- share ownership;
- share permissions;
- need coordinated planning;
- have similar lifecycles;
- fail and recover together.
One enormous state gives simple references but a large blast radius. Many tiny states reduce some coupling but create more interfaces, handovers and ordering dependencies.
The boundary should be a deliberate architectural decision.
Workspaces are not strong isolation by themselves
Terraform CLI workspaces provide multiple state instances for one configuration.
They do not automatically provide:
- separate cloud accounts;
- separate credentials;
- separate backends;
- separate approvals;
- separate access policies;
- strong production isolation.
A workspace separates state instances. It does not separate authority unless the surrounding backend, credentials and policy do so.
Important environments often benefit from separate root configurations, backends, accounts or projects, credentials and approval paths.
Environment names are labels. State, identity and account boundaries provide enforcement.
7. Existing resources need controlled adoption and handover
Infrastructure often exists before Terraform arrives. Adoption should be treated as a migration.
Modern Terraform supports configuration-driven import:
import {
to = example_network.bfstore_dev
id = "network-7f31c2"
}
resource "example_network" "bfstore_dev" {
name = "bfstore-dev"
cidr = "10.20.0.0/16"
}
This allows import intent to participate in ordinary planning and review.
A safe adoption process includes:
- inventorying the real resource;
- writing the intended configuration;
- declaring the import;
- planning;
- reconciling unexpected differences;
- proving that the plan does not recreate or destroy the object;
- moving future changes through the IaC workflow.
Import transfers management responsibility. It does not manufacture a correct configuration automatically.
Refactors should preserve identity
Moving a resource address can look like deletion and creation unless Terraform is told that the identity moved.
moved {
from = example_network.old_name
to = module.network.example_network.main
}
A moved block makes the refactor reviewable and preserves the relationship to the existing remote object.
This is safer than relying on undocumented state surgery during a module restructure.
Handover should be explicit
Sometimes a resource should continue existing but leave one state.
A configuration-driven removal can express that intention:
removed {
from = example_network.bfstore_dev
lifecycle {
destroy = false
}
}
That tells Terraform to stop managing the resource without destroying it.
The remote object remains, but the old configuration no longer owns it.
A handover should include evidence that another owner has accepted responsibility. Otherwise the result is an unmanaged orphan.
Reading a resource does not make the reader its owner. Removing it from one state does not prove another system has adopted it.
8. IaC needs a secure delivery system
Infrastructure as Code becomes trustworthy when the files participate in a controlled change and recovery system.
A possible delivery flow is:
branch
│
▼
format and validate
│
▼
static analysis and tests
│
▼
plan
│
▼
review and policy
│
▼
approval
│
▼
apply
│
▼
verify
│
▼
store evidence
Each stage answers a different question.
| Stage | Question |
|---|---|
| Format | Is the representation consistent? |
| Validate | Is the configuration structurally acceptable? |
| Static analysis | Does the declaration violate known rules? |
| Test | Does the module behave as intended under tested inputs? |
| Plan | What does Terraform propose to change? |
| Review | Does that proposal match the engineering intention? |
| Policy | Is the proposal inside organisational guardrails? |
| Apply | Can the remote systems perform the actions? |
| Verify | Did the resulting infrastructure provide the required capability? |
Validation has several levels
Terraform offers different forms of validation.
terraform fmtnormalises formatting.terraform validatechecks syntax and internal consistency.- Variable validation and resource preconditions reject unsupported inputs or states.
terraform testexercises module behaviour, using real or mocked providers according to the test design.checkblocks evaluate assertions during operations.
A check block reports a warning rather than automatically blocking the operation. A surrounding pipeline may choose to treat that warning as a gate, but the Terraform language behaviour should not be overstated.
Configuration validation is not architecture validation. A valid CIDR can still be wrong. A valid public ingress rule can still be unsafe. A valid database can still be impossible to restore.
Policy should explain the supported path
Policy as code can evaluate declarations or plans:
- production databases must not be public;
- storage must be encrypted;
- internet ingress needs approval;
- backups must meet retention rules;
- required tags must exist;
- destructive changes need specialist review.
An opaque policy that only says denied teaches engineers to resent the guardrail rather than understand it.
Good policy explains the risk, the expected remediation and the exception process.
Automation identity should be temporary and narrow
Long-lived cloud keys in CI create broad, durable risk.
Where possible, automation should exchange its workload identity for short-lived credentials:
pipeline identity
│
▼
federated trust
│
▼
short-lived cloud role
│
▼
Terraform operation
Permissions can be separated by:
- environment;
- infrastructure layer;
- plan versus apply;
- branch or repository;
- account or project;
- break-glass workflow.
Perfectly minimal provisioning permissions can be difficult to maintain. That is not a reason to stop trying.
State and plan access are not ordinary review permissions
A plan reviewer may need to inspect proposed changes. A state administrator may be able to read infrastructure identity and sensitive values.
Those are different privileges.
State and saved plans should receive narrower access than ordinary source review. Their storage, audit and retention deserve explicit design.
Reproducibility has boundaries
The same configuration, variables and provider versions do not freeze the cloud platform.
External conditions can differ:
- regional capacity;
- quotas;
- available zones;
- API behaviour;
- organisation policy;
- platform defaults;
- image availability;
- current prices.
IaC improves reproducibility. It does not make remote systems timeless.
Inputs and versions should be explicit where differences matter. The workflow should still verify the result.
9. A possible bfstore network design
A simplified root module might declare:
terraform {
required_version = ">= 1.11"
required_providers {
example = {
source = "vendor/example"
version = "~> 4.2"
}
}
backend "remote" {}
}
provider "example" {
region = var.region
}
module "network" {
source = "app.example.com/platform/network/example"
version = "2.1.0"
name = "bfstore-dev"
environment = "dev"
cidr = "10.20.0.0/16"
availability_zones = [
"zone-a",
"zone-b",
"zone-c",
]
private_subnets = {
zone-a = "10.20.10.0/24"
zone-b = "10.20.20.0/24"
zone-c = "10.20.30.0/24"
}
public_subnets = {
zone-a = "10.20.110.0/24"
zone-b = "10.20.120.0/24"
zone-c = "10.20.130.0/24"
}
enable_flow_logs = true
tags = {
project = "bfstore"
environment = "dev"
managed_by = "terraform"
}
}
The root module supplies environment-specific intent. The reusable module encodes the approved network pattern.
That pattern should document more than resource names. It should define:
- route behaviour;
- egress architecture;
- DNS assumptions;
- logging destination;
- security defaults;
- replacement risk;
- supported expansion;
- data-retention implications;
- ownership.
A bfstore network change could follow this path:
- Create a branch.
- Update the root configuration or module version.
- Run formatting, validation, static analysis and tests.
- Calculate a speculative development plan.
- Review creates, updates, unknowns, replacements and deletions.
- Produce or approve an applyable plan through the delivery system.
- Apply to development.
- Verify routes, DNS, flow logging and workload connectivity.
- Repeat the evidence process for staging.
- Apply to production only after compatibility, backup and recovery checks.
The same module may move through several environments. State, credentials, approvals and evidence remain separate.
Infrastructure as Code review checklist
| Concern | Question |
|---|---|
| Intent | Which infrastructure outcome should remain reproducible? |
| Invocation | Who runs Terraform later to detect or correct drift? |
| Ownership | Which team and active state own each remote object? |
| Management contract | Which remote properties are intentionally controlled? |
| State | Where is identity stored, encrypted, versioned and restored? |
| Locking | Does the backend support locking, and is it enabled? |
| Plan | Is the reviewed plan the one that will be applied? |
| Unknowns | Which values remain unresolved until apply? |
| Plan security | Who can read saved plans and how long are they retained? |
| Destruction | What data or capability disappears? |
| Replacement | Can old and new resources coexist safely? |
| Drift | How are out-of-band changes detected and resolved? |
| Adoption | Are imports represented and reviewed as configuration? |
| Refactoring | Do moved blocks preserve resource identity? |
| Handover | Is ownership accepted before a resource leaves state? |
| Modules | Are interfaces, versions and lifecycle consequences documented? |
| Environments | Are state, credentials, accounts and policy genuinely separated? |
| Secrets | Which values remain in state, plans or provider APIs? |
| Toolchain | Which Terraform, provider and module versions are expected? |
| Policy | Which security, resilience and cost rules run before apply? |
| Verification | What proves the resulting capability works? |
| Recovery | How are partial applies, lost state and failed replacements repaired? |
| Evidence | Can the organisation reconstruct who proposed, approved and applied the change? |
Infrastructure as Code is not complete when a file exists.
It becomes useful when the file participates in a trustworthy change and recovery system.
The mental model I am keeping
My original model was:
Infrastructure as Code
write cloud CLI commands
in a script
The stronger model is:
DECLARED INFRASTRUCTURE
│
▼
CONFIGURATION
│
┌─────────────┴─────────────┐
│ │
▼ ▼
PRIOR STATE REMOTE OBSERVATION
│ │
└─────────────┬─────────────┘
▼
COMPARISON
│
▼
PLAN
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
create update destroy
│ │ │
└──────────┼──────────┘
▼
APPLY
│
▼
NEW STATE
│
▼
VERIFY
Configuration answers:
What infrastructure do we intend to manage?
State answers:
Which real resources does Terraform believe belong to that configuration?
The provider answers:
How do those declarations map to remote APIs?
The plan answers:
Which actions connect observed reality to declared intent?
Review answers:
Do those actions match the change we believe we are making?
Policy answers:
Is the proposed infrastructure inside the organisation’s guardrails?
Apply answers:
Can the remote systems perform the change?
Verification answers:
Did the resulting infrastructure provide the required capability?
And the operating workflow answers the question declarative configuration cannot answer alone:
Who checks whether the result still matches the intention later?
Terraform supplies the graph, state, comparison and execution machinery.
The surrounding platform decides when that machinery runs, which plan is trusted, who owns drift and how destructive or partial outcomes are recovered.
Infrastructure as Code does not turn cloud infrastructure into pure software.
Networks still carry traffic. Databases still contain irreplaceable data. Machines still run out of memory. APIs still fail halfway through operations. Resources still cost money while nobody is looking at them.
What IaC changes is the quality of the conversation around those resources.
The infrastructure gains a reviewable declaration, a remembered identity, a proposed change set and a repeatable route towards the intended design.
Imperative automation records what we once told the platform to do.
Declarative infrastructure records what we are prepared to keep claiming should be true.
That is a much more durable form of intent.
References and further reading
Terraform documentation
Introduces Terraform configuration, providers, resources, state, planning and infrastructure workflows.
Terraform plan command
Documents speculative plans, saved execution plans, refresh behaviour and plan-file sensitivity.
Terraform state
Explains resource identity, one-to-one bindings and why Terraform requires state.
Terraform state locking
Documents backend-dependent state locking during write operations.
Terraform import blocks
Documents configuration-driven adoption of existing remote objects.
Terraform moved blocks
Explains how to preserve resource identity during address and module refactors.
Terraform removed blocks
Documents reviewable removal of resources from state without necessarily destroying them.
Manage sensitive data in Terraform
Covers sensitive, ephemeral and write-only values and their storage implications.
Terraform testing
Documents terraform test, test files, assertions and provider behaviour.
Terraform check blocks
Explains non-blocking continuous assertions evaluated during Terraform operations.
Terraform workspaces
Documents CLI workspaces and their limits as an isolation mechanism.
Terraform dependency lock file
Explains provider dependency selections, checksums and reproducible installation.
Terraform modules
Documents reusable modules, inputs, outputs and composition.
NIST: Infrastructure as Code
Discusses repeatability, security and the treatment of infrastructure definitions as code.
Martin Fowler: Infrastructure as Code
Explores repeatable infrastructure definition, version control and automation as engineering practices.