<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Lindbergtech]]></title><description><![CDATA[Lindbergtech]]></description><link>https://lindbergtech.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a9038b71cd87de7e97c19e7/b50ce2c1-01f4-482b-898a-6e815b5bca89.jpg</url><title>Lindbergtech</title><link>https://lindbergtech.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 11:07:42 GMT</lastBuildDate><atom:link href="https://lindbergtech.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What's New in AzureRM 5.0: Upgrading 4.x Without Breaking Prod]]></title><description><![CDATA[By the end of this post your 4.x Azure Terraform config is running green on AzureRM 5.0, through every breaking change the bump throws at it, without breaking production.
So what actually changed in 5]]></description><link>https://lindbergtech.hashnode.dev/what-s-new-in-azurerm-5-0-upgrading-4-x-without-breaking-prod</link><guid isPermaLink="true">https://lindbergtech.hashnode.dev/what-s-new-in-azurerm-5-0-upgrading-4-x-without-breaking-prod</guid><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Carl Lindberg]]></dc:creator><pubDate>Sat, 12 Sep 2026 12:07:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9038b71cd87de7e97c19e7/accb7f0d-9265-4e30-be6c-2c01992f58e5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By the end of this post your 4.x Azure Terraform config is running green on AzureRM 5.0, through every breaking change the bump throws at it, without breaking production.</p>
<p>So what actually changed in 5.0 that matters? Three things:</p>
<ul>
<li><p>How the provider registers resource providers</p>
</li>
<li><p>Breaking changes in resources/some resources being removed</p>
</li>
<li><p>A new preflight check.</p>
</li>
</ul>
<p>I will show you exactly where each one bites and how to get past it. This is not a changelog read-through. It is the upgrade, done for real against a live subscription.</p>
<p>My assumption going into this is that the resource-provider registration rules are the thing that breaks your configuration first. There are two ways to fix that, and I will show you both: the quick and lazy one, and the more controlled one you actually want in production.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/bookend_azurerm-5-whats-new-final-frame.png" alt="The upgrade gauntlet: a 4.x config passing through init, plan, and apply gates and landing green on AzureRM 5.0" style="display:block;margin:0 auto" />

<h2>The upgrade in three phases</h2>
<p><strong>Phase one: get back to</strong> <code>init</code> <strong>and</strong> <code>plan</code><strong>.</strong> The number-one fear when you bump to the latest version is that suddenly you cannot even initialize anymore. We make sure you can init and plan again and keep working with your config.</p>
<p><strong>Phase two: pull failures forward, and dodge the silent trap.</strong> I will show you failures you may not have seen before once you are on 5.0, how to fix them, and what I mean by a silent validation trap you can fall into without noticing.</p>
<p><strong>Phase three: breaking changes inside resources.</strong> Some resources behave a little differently after the bump. That ranges from resources being removed outright to breaking changes in the fields of a resource. Sometimes the fix is to re-import something into state, and I will show you exactly when and how.</p>
<p>Your upgrade path looks a bit like this. You start with your 4.x config. Some resources have changed in place: a removed field, a renamed field, a property that moved. You get to the <code>init</code> step, and that is where you may see things change with resource providers, plus the new preflight check. Then you reach <code>plan</code>, where you may have to do some state imports. Finally <code>apply</code> finishes green and your Azure infra is deployed, ideally without breaking anything in production, especially if you have stateful resources like a storage account where that would be genuinely painful.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/Screenshot-2026-09-12-at-13.11.28.png" alt="The upgrade path from a 4.x config through init and plan to a green apply" style="display:block;margin:0 auto" />

<h3>What a resource provider actually is</h3>
<p>If you do not know what a resource provider is, here is the quick version. Everything that is a resource in Microsoft Azure is registered under a resource provider. A virtual machine lives under <code>Microsoft.Compute</code>. A virtual network lives under <code>Microsoft.Network</code>.</p>
<p>In the past, whenever you created a resource and did not have its resource provider registered, Microsoft would register it for you automatically. Terraform did the same: if the provider was not registered, it registered it for you. But there are cases where you do not want that to happen. Maybe you do not allow certain resource types to be created at all, and that is exactly where you would want to control resource-provider registration yourself.</p>
<h2>Some 4.x code is just gone</h2>
<p>Before we bump anything, one thing you have probably already noticed. If you have visited the AzureRM documentation for <code>azurerm_app_service</code> recently, you would have seen a redirect telling you to use <code>azurerm_linux_web_app</code> or <code>azurerm_windows_web_app</code> instead. You have been able to deploy either of those in place of the now-legacy <code>azurerm_app_service</code>, and it worked.</p>
<p>If you upgrade to 5.0 and you are still using that legacy resource, it does not exist anymore. Your config will not even parse. That is a rewrite, not a rename, and it is a problem you need to resolve before anything else.</p>
<p>The AzureRM upgrade guide documents this in detail: removed resources, removed data sources, and breaking changes inside the resources that survived. It is worth reading through to see if anything you use is affected. See the AzureRM 5.0 upgrade guide for the full list: <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/5.0-upgrade-guide">https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/5.0-upgrade-guide</a></p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/Screenshot-2026-09-12-at-13.16.50.png" alt="azurerm_app_service no longer works after upgrading to 5.x" style="display:block;margin:0 auto" />

<p><em>azurerm_app_service no longer works after upgrading to 5.x</em></p>
<h2>Phase 1: get it to init and plan again</h2>
<p>So you upgrade. What is the first thing that happens? As soon as you bump the provider version to <code>5.0</code> or greater, the first thing that hits you is that <code>skip_provider_registration</code> is no longer a setting in the provider block. If you leave it in, you get an unsupported-argument error inside your provider block.</p>
<p>The easiest fix is to just remove the setting. But does that fix all your problems? Not quite. It depends on your configuration.</p>
<p>Here I have removed that legacy setting and I run an apply that creates an EventGrid namespace. I get an error thrown at me: because I am on the new default behavior, I am now required to tell Terraform which resource providers to register. In my case <code>Microsoft.EventGrid</code> was not registered, and the old legacy behavior would simply have registered it for me and deployed the namespace. With the new hardened default it throws an error instead, and I have to fix it.</p>
<p>How do I fix it? Two ways.</p>
<p>The controlled way is to add <code>resource_providers_to_register</code> and list exactly what you use, for example <code>["Microsoft.EventGrid"]</code>. Now Terraform can deploy resources under that namespace. This is a very deliberate way of deciding what can and cannot be deployed. The cost is that you have to be granular and keep an up-to-date list, which in my opinion is fine. It depends on how much control you want.</p>
<p>The lazy way, if you do not want all that control, is to set <code>resource_provider_registrations = "legacy"</code>. That defaults back to the old AzureRM behavior of auto-registering everything.</p>
<p>My recommendation: try to get the controlled version working. Try to unlock it, because it can increase the security posture of your environment. Maybe you are adopting Azure as an organization and you have not cleared every resource type yet. Maybe you do not want your landing-zone administrators deploying an application gateway that allows internet traffic before you have policies in place. Limiting the resource provider is one way to keep those resources from being deployed until you are ready.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/Screenshot-2026-09-12-at-13.20.53.png" alt="Phase 1: the resource-provider registration error at apply, and the two ways to fix it" style="display:block;margin:0 auto" />

<h2>Phase 2: pull failures forward with preflight</h2>
<p>Now the second phase, where I want to talk about preflight. I will show you how to turn on preflight validation, and then a validation trap you could fall into.</p>
<p>Quickly, if you do not know what preflight is: if you have worked with Terraform for a while, you know a <code>terraform plan</code> can succeed while the <code>apply</code> fails. Some things are checked at <code>plan</code>, mostly whether the syntax is correct and whether the properties you used actually exist. But <code>plan</code> does not always call the cloud provider's API to verify you are following that cloud's rules. A classic example is a storage account name that exceeds the maximum character count. At <code>plan</code> that can pass, because the property exists and the syntax is fine. At <code>apply</code> you actually contact the Azure API and it throws an error. Preflight is meant to catch those things already at <code>plan</code>, so you save time.</p>
<p>How do you enable it? Inside the AzureRM provider block, where we now set <code>resource_providers_to_register</code>, you go into the <code>features</code> block and add <code>enhanced_validation</code>, and inside that set <code>preflight</code> to <code>true</code>.</p>
<p>With preflight enabled, if I set the <code>location</code> on a resource to <code>antarctica</code>. That is not an Azure region as far as I know. I run a <code>terraform plan</code>, not an apply, and I get a <code>ResourceValidationFailed</code> error telling me the location does not exist. Without preflight this would have passed <code>plan</code>, because it is syntactically correct and <code>location</code> is a valid property, and only failed later at <code>apply</code>.</p>
<h3>The silent trap</h3>
<p>That is great. But here is the trap. Not every resource that AzureRM can manage supports preflight validation, and not every setting is covered.</p>
<p>Take the same demo and this time misspell the storage account location as <code>westeruope</code>. Run <code>plan</code> again, and it does not fail. It went straight through. If I ran an <code>apply</code>, it would have failed, and I would have already wasted time on a <code>plan</code> I thought was good.</p>
<p>The reason is that preflight is only supported by a subset of resource types. Unsupported resources are silently skipped. Right now the supported set is six resource types, and storage account is not one of them. That is why my misspelled storage location was never caught.</p>
<p>Two honest notes here. First, preflight is not brand new to the ecosystem. The <a href="https://registry.terraform.io/providers/azure/azapi/latest/docs/guides/feature_preflight">azapi provider</a> has had preflight validation for quite some time, and more generically, across far more resources. AzureRM 5.0 is catching up rather than inventing something. Second, preflight calls Azure during <code>plan</code>, so it needs valid Azure credentials at plan time. If you run Terraform through CI/CD you are most likely using a service principal that authenticates through the Azure CLI anyway, so that is not really a change, but it is worth knowing.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/Screenshot-2026-09-12-at-13.24.35.png" alt="Phase 2: preflight rejecting a bad location at plan, and the silent trap when the resource type is unsupported" style="display:block;margin:0 auto" />

<h2>Phase 3: breaking changes inside resources</h2>
<p>For the third and final phase I want to talk about breaking changes inside the resources you already have, and how to deal with them.</p>
<p>A classic example is the static website inside the AzureRM storage account. Previously your <code>azurerm_storage_account</code> had a <code>static_website</code> property block where you configured a static website hosted in Azure Storage. When you upgrade to 5.0, that entire block goes away from the storage account and becomes its own resource: <code>azurerm_storage_account_static_website</code>, pointing at a storage account ID.</p>
<p>When something like this happens, you may have to run <code>terraform import</code>. Either from the CLI or through configuration-driven <code>import</code> blocks. I point the import at the resource and supply the storage account's resource ID.</p>
<p>Then there are the more obscure changes that are hard to foresee. Here is an <code>azurerm_key_vault</code> from the 4.x era with a property called <code>enable_rbac_authorization</code>. After the upgrade the same resource calls it <code>rbac_authorization_enabled</code>. They flipped the order and changed <code>enable</code> to <code>enabled</code>. These are the kinds of renames that bite you at <code>plan</code>, and again, the fix is to check the upgrade guide's breaking-changes section for the resources you use.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/09/Screenshot-2026-09-12-at-13.27.37.png" alt="Phase 3: a breaking change inside a resource, resolved with terraform import" style="display:block;margin:0 auto" />

<h2>Green on 5.0</h2>
<p>Once you have addressed everything above and enabled the new settings, you run a <code>terraform apply</code> to make sure your state agrees with what is in your cloud environment, that all the new providers are registered, and that the versions are correct. You want to land where your infrastructure matches your configuration. Once you are there, it is happy days.</p>
<hr />
<p><em>Originally published on</em> <a href="https://www.lindbergtech.com/whats-new-in-azurerm-5-0-upgrading-4-x-without-breaking-prod"><em>lindbergtech.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Terraform State, Explained]]></title><description><![CDATA[This is the blog version of my video on Youtube of the same title
By the end of this post you will understand what is actually inside your Terraform state file, and you will know how to run it so a te]]></description><link>https://lindbergtech.hashnode.dev/terraform-state-explained</link><guid isPermaLink="true">https://lindbergtech.hashnode.dev/terraform-state-explained</guid><category><![CDATA[Azure]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Carl Lindberg]]></dc:creator><pubDate>Thu, 27 Aug 2026 13:24:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9038b71cd87de7e97c19e7/d9619eec-0628-435b-b934-9a5ddcb53e40.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the blog version of my video on Youtube of the same title</p>
<p>By the end of this post you will understand what is actually inside your Terraform state file, and you will know how to run it so a teammate's apply never corrupts it and a secret never leaks out of it.</p>
<p>Here is the whole idea in one line. State is just Terraform's map between the config you wrote and the resources that really exist in the cloud. Once you see it that way, seven habits keep that map shared, locked, encrypted, and honest. I will show you every one of them.</p>
<p>I am an Azure MVP, and in my day job I work on a team that collaborates on running Terraform against real infrastructure in the cloud, in my case Microsoft Azure. So this is not the tutorial version. It is the setup I actually run for real teams, and everything below is what I have learned first hand doing it. It works whether you are solo or on a team of ten.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/bookend_terraform-state-explained-final-frame-1.png" alt="" style="display:block;margin:0 auto" />

<h2>What Terraform state actually is</h2>
<p>You can think of Terraform state as a snapshot of the infrastructure Terraform manages. That is everything from resource attributes and properties to metadata and tags.</p>
<p>This matters because state is what you <em>think</em> your infrastructure looks like when you read your Terraform configuration. At least, that is the goal. You want to be able to look at your config and your state and have confidence that this is what is really out there in the live infrastructure.</p>
<h2>The mental model: three ideas</h2>
<p>You can explain state with three ideas. Get these and the rest of the post is just habits built on top.</p>
<p><strong>Idea one: the state file itself.</strong> It is just a file, on your machine or somewhere else, and it keeps track of your resources. That could be a virtual machine or a storage account. It can also track things you might not think of as resources, like a role assignment or a role definition, because those are things Terraform can manage too.</p>
<p><strong>Idea two: local versus remote state.</strong> You can keep the state file on your own computer as a local state file, and run <code>terraform plan</code> and <code>apply</code> against it with no problem at all. You usually see local state in videos and tutorials like this, because we are doing demos and labs, so there is no reason to keep it anywhere else. But the moment you work at a company or on a team, a local file breaks down. Your co-worker cannot reach a file that lives on your laptop. That is why real teams use remote state: a shared state file in a backend both of you can work against.</p>
<p><strong>Idea three: remote state locking.</strong> When you run an operation against remote state, whether that is a plan or an apply, Terraform puts a lock on the state file. If your co-worker tries to run anything against the same state while you hold the lock, they get an error telling them the state is locked. When you finish, the lock is released and then they can take it. This is what stops two people from overriding each other's changes and corrupting the file. Picture ten people on a team: it is important that not all ten of you are writing to the state at the same time, or the file has no way to keep straight what you actually want your infrastructure to be.</p>
<p>With that model in place, here are the seven practices that turn it into a safe, team-ready workflow.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/Screenshot-2026-08-25-at-11.00.33.png" alt="" style="display:block;margin:0 auto" />

<h2>Practice 1: Prefer remote state with locking and isolation</h2>
<p>Not just remote state, but remote state that supports locking and isolation. If you use Microsoft Azure like I do, Azure Storage gives you this out of the box. You do not have to think about it.</p>
<p>Usually there is a file called <code>backend.tf</code>. Inside the usual <code>terraform</code> block, alongside the providers, there is a <code>backend</code> block pointed at <code>azurerm</code> for example. That tells Terraform we are using Azure as our remote backend. You could just as easily point it at Google, AWS, or HashiCorp's Terraform Cloud. The block names a resource group, a storage account, a container (could be called <code>tfstate</code>), and a key. When you run <code>init</code>, Terraform picks this up and knows to use Azure remote state.</p>
<p>One note from experience. When I am working with a team we often use a Terraform wrapper, a small CLI application that wraps certain Terraform operations. That is a bit much to cover here and it is not really about state, but it is a good option and maybe a topic for a future post. When we do that we will usually have defined an empty backend config: <code>backend = {}</code> and the backend config gets passed inline by the wrapper.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/Screenshot-2026-08-25-at-11.05.41.png" alt="" style="display:block;margin:0 auto" />

<h2>Practice 2: Enable strong encryption for state</h2>
<p>Once you are on remote state, make sure your provider encrypts it. Because I use Azure, I know Azure Storage is encrypted at rest by default.</p>
<p>One thing I would add from the real world. If you are using Azure Storage for state, do not hand out access with SAS keys. Use Entra ID authentication to grant people access to the storage account instead.</p>
<p>Encryption at rest does not mean everything is automatically fine, though. To see why, we need to talk about secrets in state, which is the next practice.</p>
<h2>Practice 3: Minimize secrets stored in state</h2>
<p>Terraform state can hold sensitive values. You might set a password on a virtual machine, or a connection string to a database. Those land in the state file.</p>
<p>If I use a <code>random_password</code> resource with a length of 24 characters, and its <code>result</code> is referenced when setting a Key Vault secret, both of those are recorded in plain text in state. If I go into the storage account that holds my state, open the container, and click edit on the state file, I can read the contents. It is encrypted at rest, but I am authenticated, so I am allowed to see it.</p>
<p>At this point you might say there is a <code>sensitive</code> property you can set to <code>true</code>. That is correct, and you should. But read what it actually does. Hover over it and the tooltip says it controls whether the output contains sensitive material and should be hidden <em>in the UI</em>. That is the key difference. It says the output should be <em>hidden</em>. It does not say the value should be <em>encrypted</em>, or <em>not recorded at all</em>.</p>
<p>So <code>sensitive = true</code> marks the output as sensitive, which is genuinely useful. If you are running in a pipeline like GitHub Actions or Azure DevOps, you do not want the password visible to anyone who can open the run and read the output. But if someone has access to the state itself, they can still read the value in clear text.</p>
<p>If you want to keep a secret from ever being recorded in state in the first place, there is a resource block for that now, called <code>ephemeral</code>. I have made a dedicated video on ephemeral blocks if you want to go deeper on how that works.</p>
<h2>Practice 4: Use config-driven state changes</h2>
<p>Prefer declarative changes over manual surgery, so state edits stay auditable and reviewable. There are two ways to change state beyond plain <code>plan</code> and <code>apply</code>.</p>
<p>The first is CLI-driven, using inline commands: <code>terraform state mv</code> to move something, or <code>terraform state rm</code> to delete something.</p>
<p>The second is config-driven, using Terraform blocks that are the equivalent of those CLI commands. Take a real example. Say a Key Vault secret was originally named <code>.db</code>. Just looking at that, it is not obvious it is the database password, especially since <code>terraform state list</code> will not show you all the properties. So the administrator renames it from <code>.db</code> to <code>.database_password</code>, and adds a <code>moved</code> block that says the resource formerly called <code>.db</code> is now called <code>.database_password</code>.</p>
<p>This is the way I recommend you do it, because it gets checked into a pull request where someone can review it, and it is tracked in version control. You <em>could</em> achieve the same rename by running <code>terraform state mv</code> by hand. If you do, Terraform connects to state, acquires the lock (remember the locking from earlier, it matters here too), and moves the resource. But that hand-run move leaves no trace. With the <code>moved</code> block, when you run <code>apply</code>, Terraform shows you that the resource has moved from the old name to the new name and asks you to confirm, and the change lives in the diff.</p>
<p>Same outcome either way, but only one of them survives code review.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/Screenshot-2026-08-25-at-11.13.19.png" alt="" style="display:block;margin:0 auto" />

<h2>Practice 5: Review plans and automate applies</h2>
<p>Route plans and applies through CI/CD, not from local machines and laptops. That is how you enforce approvals and add checks and controls before your code reaches production.</p>
<p>There are several ways to do CI/CD. I use GitHub these days, so all I need is a workflow file under <code>.github/workflows</code>. It runs pretty much the same commands I would run locally, just inside a GitHub Actions runner instead.</p>
<p>The real benefit is what you can insert around those commands. For example, Open Policy Agent, which lets you check for blast radius. The short version is that you can use OPA to enforce policy on the changes in your environment. Maybe you want to block configurations that would destroy or redeploy a lot of resources. That is a blast-radius policy, and you can add it as a step in CI. If the check does not pass, the pipeline fails. I have a dedicated Policy as Code video if you want the full story. For state, the point is simply that this gating is something you want once you are automating applies.</p>
<h2>Practice 6: Version and modularize your configuration</h2>
<p>This practice is not strictly about state, but it is closely related, because it is how you think about implementing changes and writing to state safely.</p>
<p>Here is the flow I use on any team. I make code changes, commit, push to GitHub, and open a pull request. Opening the PR automatically runs <code>plan</code>, so a reviewer can glance at the plan and confirm nothing weird happened. Once the PR is approved, we merge to main, and that automatically kicks off <code>plan</code> and <code>apply</code> against the real environments.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/Screenshot-2026-08-25-at-11.16.47.png" alt="" style="display:block;margin:0 auto" />

<h2>Practice 7: Monitor and remediate drift</h2>
<p>Quick definition first, in case you are new to the term. You have real infrastructure, in my case in Azure, and it should match whatever is in your Terraform state file. Drift is when someone changes a property directly, through the portal or the Azure CLI, and suddenly the real infrastructure no longer matches state. That is worth monitoring for, because if you are going to use infrastructure as code, you want to actually use the tools rather than mix and match.</p>
<p>One thing you could do is run a scheduled plan that applies or imports to bring reality back in line with state. But be careful, because that has real risks. A scheduled apply could change properties on your production resources without you knowing. It is already bad that someone changed something outside of infrastructure as code. It can be worse if something silently changes back, because some properties, when altered, redeploy the resource. If it is a stateless resource like an Azure Function, that may not be the end of the world. But if it is a database or a storage account, you could lose whatever data was in there, and that is really bad.</p>
<p>I will be honest with you: I do not have a specific way that I monitor drift. What we actually do is enforce the four-eye principle. Every change has to be checked into version control, opened as a PR, and reviewed by a co-worker. When you do that, the plan pipelines from the previous practice will show changes you did not expect, or resources changing that were not part of your work. When that happens, you look at previous PRs or the activity logs, figure out if someone made a temporary change, and go talk to that person. It is a semi-automated monitoring system. It is perhaps not the best, but we have not really needed anything more than that. Yours might differ.</p>
<img src="https://storage.ghost.io/c/0e/ef/0eef31db-1c67-40b3-8177-ab4c05a5a032/content/images/2026/08/Screenshot-2026-08-25-at-11.18.59.png" alt="" style="display:block;margin:0 auto" />

<h2>Wrap-up</h2>
<p>That is state end to end: the map between your configuration and reality, kept shared, locked, encrypted, and honest across a whole team.</p>
<p>The result of these seven habits is a state setup the whole team can run against without stepping on each other, without pasting secrets around, and without one bad apply taking down your software or your infrastructure.</p>
<p>I will leave you with the question I keep coming back to. Do you actually monitor drift? And is there a practice I did not mention here that you think belongs on this list? I would genuinely like to know.</p>
<hr />
<p><em>Originally published on</em> <a href="https://www.lindbergtech.com/terraform-state-explained"><em>lindbergtech.com</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>