<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Operations on Distorted Scribbles</title>
        <link>https://blog.l3zc.com/en/tags/operations/</link>
        <description>Recent content in Operations on Distorted Scribbles</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Fri, 10 Apr 2026 01:12:33 +0800</lastBuildDate><atom:link href="https://blog.l3zc.com/en/tags/operations/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>First impressions of Terraform IaC</title>
            <link>https://blog.l3zc.com/en/2026/04/iac-with-terraform/</link>
            <pubDate>Thu, 09 Apr 2026 07:13:35 +0000</pubDate>
            <guid>https://blog.l3zc.com/en/2026/04/iac-with-terraform/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/cover_hu_a8e83f97ed61569c.webp&#34; alt=&#34;Featured image of post First impressions of Terraform IaC&#34; /&gt;&lt;p&gt;Terraform is an IaC tool. IaC stands for ‘Infrastructure as Code’: we write our infrastructure as declarative code, then use &lt;code&gt;terraform apply&lt;/code&gt; to deploy it. With the same configuration, you will always get exactly the same infrastructure every time (Nix OS users rejoice).&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-use-terraform&#34;&gt;Why use Terraform&#xA;&lt;/h2&gt;&lt;p&gt;Traditional infrastructure management mostly relies on manual work and the dashboards provided by various cloud vendors, which brings the following pain points:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Pain point&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Explanation&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to reproduce&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Configuring things by clicking around in a dashboard makes it easy to miss or misconfigure something, and hard to reproduce later&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Environment drift&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Manual changes gradually cause production and test environments to diverge, so in extreme cases testing is fine but production falls over&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to scale&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Adding a new environment requires repeating lots of manual steps, which is time-consuming and error-prone&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to audit&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;There is no change history, &lt;del&gt;so when things go wrong it is harder to pass the buck&lt;/del&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to collaborate&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Infrastructure ends up controlled by a small number of ‘people who know’, and anyone who wants to change something has to go through them, which is inefficient&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;To solve these problems, the concept of ‘Infrastructure as Code’ &lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; was introduced, and Terraform is one of the best-known solutions.&lt;/p&gt;&#xA;&lt;p&gt;Take a realistic use case: suppose you buy a new GCP account with $300 trial credit every year. It is cheap, but every year you have to go back into the GCP console and recreate your machines. With Terraform, if you want to deploy the same setup again, you just replace the API token after switching accounts, then run &lt;code&gt;terraform apply&lt;/code&gt;. In a few minutes, you can recreate exactly the same machines, VPCs, S3, firewall rules, and so on as in your previous account.&lt;/p&gt;&#xA;&lt;p&gt;Another example is managing and migrating Cloudflare DNS and Tunnel. You only need to copy the old Tunnel’s Ingress Rule to the new Tunnel’s Ingress Rule. Even if you later migrate to other providers such as AliDNS or Route 53, you can still copy the data across as-is &lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;This becomes especially useful when working with other people. Combined with Git, every change leaves a trace, merge conflicts are far less worrying, PRs can automatically generate previews of changes, and if something goes wrong you can roll back to the previous version immediately. None of this is really possible with the traditional approach of people directly operating a provider’s dashboard by hand.&lt;/p&gt;&#xA;&lt;h2 id=&#34;installing-terraform&#34;&gt;Installing Terraform&#xA;&lt;/h2&gt;&lt;p&gt;Terraform is written in Go, and the compiled output is naturally a single executable file, so installation is very straightforward. On Windows, you can install it directly with Winget:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;winget install Hashicorp.Terraform&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you do not want to use Winget, you can also use Scoop or another package manager, or place the precompiled binary into &lt;code&gt;$PATH&lt;/code&gt; to complete the installation.&lt;/p&gt;&#xA;&lt;p&gt;If you are on Linux, just use the appropriate package manager. If you install it by placing the binary into &lt;code&gt;$PATH&lt;/code&gt;, remember to use &lt;code&gt;sudo chmod +x terraform&lt;/code&gt; to make the file executable.&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-basic-terraform-concepts&#34;&gt;Two basic Terraform concepts&#xA;&lt;/h2&gt;&lt;h3 id=&#34;providers&#34;&gt;Provider(s)&#xA;&lt;/h3&gt;&lt;p&gt;As mentioned earlier, Terraform is an Infrastructure as Code tool. As a tool, it is not tied to any specific platform. Instead, it connects to different platforms through Provider(s). To see what Provider(s) are available, you can browse &lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/browse/providers&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Terraform’s Registry&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image_hu_463ebc576c22c691.webp&#34; alt=&#34;Terraform has a rich Provider(s) ecosystem&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;state-management&#34;&gt;State management&#xA;&lt;/h3&gt;&lt;p&gt;Terraform stores the state information from each infrastructure change operation in a state file. By default, this is saved as the &lt;code&gt;terraform.tfstate&lt;/code&gt; file in the current working directory &lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;, though you can also configure a different backend such as S3 or Postgres. Every time you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform compares the state declared in the current configuration files with the existing state file, calculates the differences, works out the correct order of operations, and then tells the Provider to apply those changes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraforms-resource-import-problem&#34;&gt;Terraform’s resource import problem&#xA;&lt;/h2&gt;&lt;p&gt;Importing existing resources has long been one of Terraform’s most criticised pain points. Hashi Corp. seems to have stuck to a stubborn and frankly silly idea for years: all your infrastructure should have been created with Terraform from the very beginning, so there is no such thing as a resource import problem.&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Time&lt;/th&gt;&#xA;          &lt;th&gt;Version&lt;/th&gt;&#xA;          &lt;th&gt;Progress&lt;/th&gt;&#xA;          &lt;th&gt;Problem&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2014–2022&lt;/td&gt;&#xA;          &lt;td&gt;v0.x–v1.4&lt;/td&gt;&#xA;          &lt;td&gt;Only &lt;code&gt;terraform import&lt;/code&gt;, one resource at a time, and it did not generate any configuration&lt;/td&gt;&#xA;          &lt;td&gt;After importing, you still had to hand-write the HCL &lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2023.06&lt;/td&gt;&#xA;          &lt;td&gt;v1.5&lt;/td&gt;&#xA;          &lt;td&gt;Introduced the &lt;code&gt;import&lt;/code&gt; block and the &lt;code&gt;-generate-config-out&lt;/code&gt; parameter, so it could generate configuration&lt;/td&gt;&#xA;          &lt;td&gt;But you still had to write them one by one, and still had to provide the existing resource IDs yourself&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2024.01&lt;/td&gt;&#xA;          &lt;td&gt;v1.7&lt;/td&gt;&#xA;          &lt;td&gt;The &lt;code&gt;import&lt;/code&gt; block gained support for &lt;code&gt;for_each&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Batch import at last, but you still had to obtain the IDs yourself&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Second half of 2024&lt;/td&gt;&#xA;          &lt;td&gt;v1.12&lt;/td&gt;&#xA;          &lt;td&gt;Introduced the &lt;code&gt;terraform query&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt; blocks, finally enabling automatic resource discovery&lt;/td&gt;&#xA;          &lt;td&gt;But this feature has to be implemented by each Provider, and many Providers simply have not caught up&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The community has been complaining about this for years, yet the question ‘I already have a pile of existing resources — how do I import them into Terraform?’ was never taken seriously. As an Infrastructure as Code tool, Terraform’s design philosophy is declarative configuration and idempotence &lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;. Hashi Corp. firmly believes that ‘the state declared in code is the only source of truth, so resources should be created from scratch with Terraform’. But in reality, most companies already have a large amount of legacy infrastructure. Having resources first and code later is the norm. Hashi Corp. acknowledged this contradiction very late; the &lt;code&gt;terraform query&lt;/code&gt; in &lt;code&gt;v1.12&lt;/code&gt; was really the first time the official tooling took the issue seriously — though as for Provider ecosystem support&amp;hellip; well, it has been rough.&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-file-structure&#34;&gt;Terraform file structure&#xA;&lt;/h2&gt;&lt;p&gt;Terraform’s file structure is very simple. When it runs, the main program blindly reads all &lt;code&gt;.tf&lt;/code&gt; files in the working directory. As long as the required information is present, you can name the files whatever you like. For example, my file structure looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;❯ tree -a -I .git&#xA;.&#xA;├── .editorconfig&#xA;├── .github&#xA;│   ├── dependabot.yml&#xA;│   └── workflows&#xA;│       ├── terraform-apply.yml&#xA;│       ├── terraform-plan.yml&#xA;│       └── your-fork.yml&#xA;├── .gitignore&#xA;├── .terraform.lock.hcl&#xA;├── cf_dns_zones.tf&#xA;├── cf_tunnel.tf&#xA;├── dns_example_com.tf&#xA;├── dns_example_net.tf&#xA;├── dns_example_top.tf&#xA;├── dns_example_cn.tf&#xA;├── main.tf                 # Basic configuration (terraform block)&#xA;├── moved.tf                # Moved resources&#xA;├── provider.tf             # Configuration for each Provider&#xA;├── README.md&#xA;├── rename_resources.ps1&#xA;└── variables.tf            # Custom variables&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt; is used to store the basic configuration:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  required_providers {&#xA;    cloudflare = {&#xA;      source  = &#34;cloudflare/cloudflare&#34;&#xA;      version = &#34;~&gt; 5&#34;&#xA;    }&#xA;&#xA;    tencentcloud = {&#xA;      source  = &#34;tencentcloudstack/tencentcloud&#34;&#xA;      version = &#34;&gt;= 1.81.43&#34;&#xA;    }&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;provider.tf&lt;/code&gt; is used to store the configuration for each Provider:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;provider &#34;cloudflare&#34; {}&#xA;provider &#34;tencentcloud&#34; {}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is left empty here because we can pass credentials through environment variables instead of writing them directly into the configuration file. For example, the Cloudflare Provider accepts &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; as an alternative to the &lt;code&gt;api_token&lt;/code&gt; variable.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt; is used to declare custom variables:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;variable &#34;cloudflare_zone_example_com&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.com&#34;&#xA;  type        = string&#xA;}&#xA;&#xA;variable &#34;cloudflare_zone_example_top&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.top&#34;&#xA;  type        = string&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These variables can be passed in through environment variables prefixed with &lt;code&gt;TF_VAR_&lt;/code&gt;. Terraform will also automatically read variables from the &lt;code&gt;terraform.tfvars&lt;/code&gt; file. The main purpose of variables is to be referenced from other configuration files, for example:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;example_cname&#34; {&#xA;  content = &#34;${cloudflare_zero_trust_tunnel_cloudflared.Production_Tunnel.id}.cfargotunnel.com&#34;&#xA;  name    = &#34;example.example.com&#34;&#xA;  proxied = true&#xA;  tags    = []&#xA;  ttl     = 1&#xA;  type    = &#34;CNAME&#34;&#xA;  zone_id = var.cloudflare_zone_id_example_com  # the cloudflare_zone_example_com variable is referenced here&#xA;  settings = {&#xA;    flatten_cname = false&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With these basic settings in place, we can run &lt;code&gt;terraform init&lt;/code&gt; to initialise the Terraform environment and lock dependency versions. After that, the rest is just declaring our resources.&lt;/p&gt;&#xA;&lt;h2 id=&#34;importing-existing-resources-into-terraform&#34;&gt;Importing existing resources into Terraform&#xA;&lt;/h2&gt;&lt;p&gt;As mentioned earlier, Terraform uses state management. This is great, but it also creates a problem during initialisation: by default, Terraform’s state file is obviously empty.&lt;/p&gt;&#xA;&lt;p&gt;At this point, what we need to do is import the state of the existing resources from the cloud provider into Terraform, so that Terraform can take over and manage our infrastructure seamlessly. As you have probably noticed from the earlier rant, resource import in Terraform is a sore point. Taking DNS records hosted on Cloudflare as an example, if the Provider supports it, you can use &lt;code&gt;terraform query&lt;/code&gt;, introduced in Terraform 1.12. In most cases, though, Providers have not implemented this new feature, and Cloudflare is one of those that does not support it.&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, even if Hashi Corp. has not taken the problem seriously, companies that use Terraform heavily to manage infrastructure have come up with their own solutions. Cloudflare maintains an import tool called &lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cf-terraforming&lt;/a&gt;, which saves a lot of manual effort. First, install &lt;code&gt;cf-terraforming&lt;/code&gt;. This tool is written in Go, so you either need a Go environment to install it, or you can place the official precompiled binary into &lt;code&gt;$PATH&lt;/code&gt; and make it executable.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The tool is fairly straightforward to use. First, you need the following environment variables:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# If you use an API Token&#xA;export CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&#xA;&#xA;# If you use an API Key&#xA;export CLOUDFLARE_EMAIL=&#39;user@example.com&#39;&#xA;export CLOUDFLARE_API_KEY=&#39;1150bed3f45247b99f7db9696fffa17cbx9&#39;&#xA;&#xA;# Specify the zone ID of the domain to import; this is not needed for account-level resources (such as Cloudflare Tunnel)&#xA;export CLOUDFLARE_ZONE_ID=&#39;81b06ss3228f488fh84e5e993c2dc17&#39;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote class=&#34;alert alert-tip&#34;&gt;&#xA;        &lt;div class=&#34;alert-header&#34;&gt;&#xA;            &lt;span class=&#34;alert-icon&#34;&gt;💡&lt;/span&gt;&#xA;            &lt;span class=&#34;alert-title&#34;&gt;Tip&lt;/span&gt;&#xA;        &lt;/div&gt;&#xA;        &lt;div class=&#34;alert-body&#34;&gt;&#xA;            &lt;p&gt;The commands here assume you are using Bash. If your shell is not compatible with Bash syntax, you will need to adjust them. For example, in PowerShell on Windows, the syntax for setting an environment variable is:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;$env:CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&lt;/code&gt;&lt;/pre&gt;&#xA;        &lt;/div&gt;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;Usually, you only need to set &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; and &lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;. When creating the API token in the console, remember to grant it the necessary permissions. In this case, we are only importing DNS records, so giving it permission to edit zone DNS is enough.&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-1_hu_15298d4bd366a377.webp&#34; alt=&#34;Grant the permissions required to operate on the resources&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Right, all the preparation is done. Now we can start generating the configuration files.&lt;/p&gt;&#xA;&lt;p&gt;First, import the domain configuration in the account, namely &lt;code&gt;cloudflare_zone&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_zone&#34; &gt; zone.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This step generates a file called &lt;code&gt;zone.tf&lt;/code&gt; in the current directory, containing content in the following format:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&#xA;&#xA;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At this point, the domain resources have been imported, but their internal configuration has not. Next, import the DNS records under the domain:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --zone $CLOUDFLARE_ZONE_ID \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; &gt;&gt; dns.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This step generates a configuration file called &lt;code&gt;dns.tf&lt;/code&gt; in the current directory, containing content in the following format:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_5deb14xxxxxb629bf123xxxxxxxc8f_0&#34; {&#xA;  content  = &#34;67.24.33.108&#34;&#xA;  name     = &#34;example.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;81c7f2de8dfxxxxxx52629xxxxxxfc&#34;&#xA;  settings = {}&#xA;}&#xA;&#xA;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89xxxxx0bf9cxxxxxx9a_1&#34; {&#xA;  content  = &#34;35.27.108.33&#34;&#xA;  name     = &#34;terraform.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;8xxxxxx7644e428526xxxxxx&#34;&#xA;  settings = {}&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you need to import multiple domains, just set the &lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt; environment variable separately each time and rerun the command.&lt;/p&gt;&#xA;&lt;p&gt;The generated configuration file here can be used directly — it is the Terraform configuration file we need later. However, at this point we have only generated the configuration file; Terraform’s state is still empty. If you run &lt;code&gt;terraform apply&lt;/code&gt; now, Terraform will blindly treat all the declarations we just imported as new resources and throw a pile of ‘Alredy Exists’ errors. So next, we need to import the generated configuration into Terraform’s &lt;code&gt;terraform.tfstate&lt;/code&gt; state.&lt;/p&gt;&#xA;&lt;p&gt;Terraform introduced the &lt;code&gt;import&lt;/code&gt; block in version 1.5, which is much more modern than typing import commands one line at a time. The process is to generate an &lt;code&gt;.tf&lt;/code&gt; file containing &lt;code&gt;import&lt;/code&gt; blocks. The next time you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform will automatically perform the import for you.&lt;/p&gt;&#xA;&lt;p&gt;Generate the &lt;code&gt;import&lt;/code&gt; blocks for &lt;code&gt;cloudflare_zone&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_zone&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Generate the &lt;code&gt;import&lt;/code&gt; blocks for &lt;code&gt;cloudflare_dns_record&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This step generates &lt;code&gt;import.tf&lt;/code&gt; in the current directory. It contains the import information needed to tell Terraform which cloud-provider resource ID corresponds to each &lt;code&gt;resource&lt;/code&gt; block generated in the previous step. This ID is the code the cloud provider uses internally to identify a resource. You would not normally see it in the control panel; you only get it by requesting it through the API. Terraform needs this ID during import to confirm that the local definition matches the cloud resource, ensuring strict idempotence.&lt;/p&gt;&#xA;&lt;p&gt;Right, now let us run &lt;code&gt;terraform plan&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform plan&#xA;cloudflare_dns_record.minio_a: Refreshing state... [id=xxxxxxxxxxx53]&#xA;cloudflare_zero_trust_tunnel_cloudflared_config.raspberrypi: Refreshing state...&#xA;......&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;5deb14c21xxxxxxx20f1c8f&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89c149exxxxxxxxxxxba13xxxxxa_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;89cxxxxxxxxxxxxxxxxxx09a&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;81xxxxxxxxxxxxxxxxxxxxxfc&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;&#xA;────────────────────────────────────────────────────────────────────────────────────────────────────────&#xA;&#xA;Note: You didn&#39;t use the -out option to save this plan, so Terraform can&#39;t guarantee to take exactly&#xA;these actions if you run &#34;terraform apply&#34; now.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If the numbers for Add, Change, and Destroy are all 0, then the import has gone correctly. Just run &lt;code&gt;terraform apply --auto-approve&lt;/code&gt;, and the resources will be imported.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform apply --auto-approve&#xA;cloudflare_dns_record.push_a: Refreshing state... [id=REDACTED]&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Import complete [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Import complete [id=REDACTED/REDACTED]&#xA;&#xA;Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;state-storage-and-continuous-integration&#34;&gt;State storage and continuous integration&#xA;&lt;/h2&gt;&lt;p&gt;One of IaC’s core strengths is that it makes Git-based collaboration and CI easy, but before that there is another problem to solve: where exactly should &lt;code&gt;terraform.tfstate&lt;/code&gt; live? Nobody wants to painstakingly import state for each environment only to lose it every time they switch.&lt;/p&gt;&#xA;&lt;p&gt;Terraform currently supports the following state backends:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;local&lt;/li&gt;&#xA;&lt;li&gt;remote&lt;/li&gt;&#xA;&lt;li&gt;azurerm&lt;/li&gt;&#xA;&lt;li&gt;consul&lt;/li&gt;&#xA;&lt;li&gt;cos&lt;/li&gt;&#xA;&lt;li&gt;gcs&lt;/li&gt;&#xA;&lt;li&gt;http&lt;/li&gt;&#xA;&lt;li&gt;Kubernetes&lt;/li&gt;&#xA;&lt;li&gt;oci&lt;/li&gt;&#xA;&lt;li&gt;oss&lt;/li&gt;&#xA;&lt;li&gt;pg&lt;/li&gt;&#xA;&lt;li&gt;s3&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;If you do not have any special requirements, you can choose &lt;code&gt;s3&lt;/code&gt; as I do. Cloudflare R2 has a free tier, after all, so you might as well use it.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  backend &#34;s3&#34; {&#xA;    bucket = &#34;terraform&#34;&#xA;    key    = &#34;terraform.tfstate&#34;&#xA;    region = &#34;auto&#34;&#xA;    endpoints = {&#xA;      s3 = &#34;https://REDACTED.r2.cloudflarestorage.com&#34;&#xA;    }&#xA;&#xA;    # R2 does not need this AWS validation&#xA;    skip_credentials_validation = true&#xA;    skip_metadata_api_check     = true&#xA;    skip_region_validation      = true&#xA;    skip_requesting_account_id  = true&#xA;    use_path_style              = true&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For the S3 backend, it is recommended to store credentials in the two environment variables &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;export AWS_ACCESS_KEY_ID=&#39;REDACTED&#39;&#xA;export AWS_SECRET_ACCESS_KEY=&#39;REDACTED&#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once configured, run &lt;code&gt;terraform init -migrate-state&lt;/code&gt; and the state will be stored successfully in the cloud. After that, no matter where you edit the configuration or run &lt;code&gt;terraform apply&lt;/code&gt;, you will not need to worry about Terraform state getting out of sync.&lt;/p&gt;&#xA;&lt;p&gt;Next comes the GitHub CI configuration. It is actually very simple: on each &lt;code&gt;git push&lt;/code&gt;, just trigger &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform fmt&lt;/code&gt;, and &lt;code&gt;terraform apply&lt;/code&gt;. Here is my &lt;code&gt;.github/workflows/apply.yml&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: &#34;Terraform Apply&#34;&#xA;&#xA;on:&#xA;  push:&#xA;    branches:&#xA;      - main&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: &#34;${{ secrets.CLOUDFLARE_API_TOKEN }}&#34;&#xA;  AWS_ACCESS_KEY_ID: &#34;${{ secrets.AWS_ACCESS_KEY_ID }}&#34;&#xA;  AWS_SECRET_ACCESS_KEY: &#34;${{ secrets.AWS_SECRET_ACCESS_KEY }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  terraform:&#xA;    name: &#34;Terraform Apply&#34;&#xA;    runs-on: ubuntu-latest&#xA;    permissions:&#xA;      contents: read&#xA;    concurrency:&#xA;      group: terraform-apply&#xA;      cancel-in-progress: false&#xA;    steps:&#xA;      - name: Checkout&#xA;        uses: actions/checkout@v6&#xA;&#xA;      - name: Setup Terraform&#xA;        uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform Init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Apply&#xA;        run: terraform apply -input=false -auto-approve&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For PRs, CI should automatically attach the output of &lt;code&gt;terraform plan&lt;/code&gt; to each PR:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: Terraform Plan&#xA;&#xA;on:&#xA;  pull_request:&#xA;    paths:&#xA;      - &#34;**/*.tf&#34;&#xA;      - &#34;.github/workflows/terraform-plan.yml&#34;&#xA;&#xA;permissions:&#xA;  contents: read&#xA;  pull-requests: write&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}&#xA;  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}&#xA;  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  plan:&#xA;    name: Terraform Plan&#xA;    runs-on: ubuntu-latest&#xA;    steps:&#xA;      - uses: actions/checkout@v6&#xA;&#xA;      - uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform fmt&#xA;        id: fmt&#xA;        run: terraform fmt -check -recursive&#xA;        continue-on-error: true&#xA;&#xA;      - name: Terraform Init&#xA;        id: init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Validate&#xA;        id: validate&#xA;        run: terraform validate -no-color&#xA;&#xA;      - name: Terraform Plan&#xA;        id: plan&#xA;        run: terraform plan -input=false -no-color&#xA;        continue-on-error: true&#xA;&#xA;      - name: Post Plan to PR&#xA;        uses: actions/github-script@v8&#xA;        with:&#xA;          github-token: ${{ secrets.GITHUB_TOKEN }}&#xA;          script: |&#xA;            const { data: comments } = await github.rest.issues.listComments({&#xA;              owner: context.repo.owner,&#xA;              repo: context.repo.repo,&#xA;              issue_number: context.issue.number,&#xA;            });&#xA;            const botComment = comments.find(c =&gt;&#xA;              c.user.type === &#39;Bot&#39; &amp;&amp; c.body.includes(&#39;&lt;!-- terraform-plan --&gt;&#39;)&#xA;            );&#xA;&#xA;            const planOutput = `${{ steps.plan.outputs.stdout }}`.substring(0, 65000);&#xA;&#xA;            const body = `&lt;!-- terraform-plan --&gt;&#xA;            #### Terraform Plan&#xA;&#xA;            | Step     | Result                            |&#xA;            | -------- | --------------------------------- |&#xA;            | fmt      | \`${{ steps.fmt.outcome }}\`      |&#xA;            | init     | \`${{ steps.init.outcome }}\`     |&#xA;            | validate | \`${{ steps.validate.outcome }}\` |&#xA;            | plan     | \`${{ steps.plan.outcome }}\`     |&#xA;&#xA;            &lt;details&gt;&lt;summary&gt;Expand Plan details&lt;/summary&gt;&#xA;&#xA;            \`\`\`terraform&#xA;            ${planOutput}&#xA;            \`\`\`&#xA;            &lt;/details&gt;`;&#xA;&#xA;            if (botComment) {&#xA;              await github.rest.issues.updateComment({&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                comment_id: botComment.id,&#xA;                body&#xA;              });&#xA;            } else {&#xA;              await github.rest.issues.createComment({&#xA;                issue_number: context.issue.number,&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                body&#xA;              });&#xA;            }&#xA;&#xA;      - name: Fail if plan failed&#xA;        if: steps.plan.outcome == &#39;failure&#39;&#xA;        run: exit 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-2_hu_c922b7d4de19a009.webp&#34; alt=&#34;Every PR will have Plan output&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;ongoing-workflow&#34;&gt;Ongoing workflow&#xA;&lt;/h2&gt;&lt;p&gt;At this point, Terraform’s initial ‘takeover’ is complete, and after that you move into day-to-day maintenance. At this stage there are really only three things to do:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Create new resources&lt;/li&gt;&#xA;&lt;li&gt;Modify existing resources&lt;/li&gt;&#xA;&lt;li&gt;Delete resources that are no longer needed&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;There are only two common operations: &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;. If you are working alone, you can usually just commit small changes directly. If you are working in a team, though, each change should follow the principle of using a PR whenever possible rather than committing directly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;creating-infrastructure&#34;&gt;Creating infrastructure&#xA;&lt;/h3&gt;&lt;p&gt;Suppose you want to add a new DNS record, create a new Tunnel, or create a new object storage bucket. The process looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  C1[Create a new branch&#xA;such as feat/add-minio-record] --&gt; C2[Add a new resource block]&#xA;  C2 --&gt; C3[terraform fmt + validate]&#xA;  C3 --&gt; C4[terraform plan]&#xA;  C4 --&gt; C5{Only the expected new resources?}&#xA;  C5 -- No --&gt; C6[Fix the configuration and rerun plan]&#xA;  C6 --&gt; C4&#xA;  C5 -- Yes --&gt; C7[Submit PR]&#xA;  C7 --&gt; C8[After merge, CI apply]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ideal &lt;code&gt;plan&lt;/code&gt; output is:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;X to add&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to change&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to destroy&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;If you only meant to add something but &lt;code&gt;to destroy&lt;/code&gt; appears, do not rush in. Usually it means a bad reference, a wrong variable, or that you accidentally changed a resource address. Check carefully to see what went wrong.&lt;/p&gt;&#xA;&lt;h3 id=&#34;modifying-and-deleting-infrastructure&#34;&gt;Modifying and deleting infrastructure&#xA;&lt;/h3&gt;&lt;p&gt;The process for modifying resources is similar to creating them, but there is one extra step: evaluate whether the change will trigger a rebuild.&lt;/p&gt;&#xA;&lt;p&gt;That is because many Provider fields are &lt;code&gt;ForceNew&lt;/code&gt;. You think you are only changing one field, and Terraform replies: ‘Right then, delete and recreate it.’ In a DNS scenario like this, that is not a huge issue, but for something like a cloud instance, deleting and recreating it can obviously cause real damage.&lt;/p&gt;&#xA;&lt;p&gt;It is best to follow this order:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  M1[Modify .tf] --&gt; M2[terraform plan]&#xA;  M2 --&gt; M3{replace/destroy appears?}&#xA;  M3 -- No --&gt; M7[Confirm the scope of impact]&#xA;  M7 --&gt; M8[terraform apply]&#xA;  M3 -- Yes --&gt; M4[Pause and review the change]&#xA;  M4 --&gt; M5[Add lifecycle protection if needed]&#xA;  M5 --&gt; M6[Schedule a maintenance window]&#xA;  M6 --&gt; M8&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For production environments, being a bit slower when creating or modifying things is not a big problem. What matters most is correctness. Go a bit slower; do not make mistakes. IaC is not a speed contest — it is about predictability.&lt;/p&gt;&#xA;&lt;p&gt;If you are deleting resources instead (for example, retiring a DNS record or cleaning up an abandoned Tunnel), follow the process below &lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  D1[Confirm the resource is no longer needed; check dependencies in services, monitoring, and scripts] --&gt; D2[Delete the resource block or adjust count/for_each]&#xA;  D2 --&gt; D3[terraform plan]&#xA;  D3 --&gt; D4{Does to destroy match expectations?}&#xA;  D4 -- No --&gt; D5[Revert the change and continue checking dependencies]&#xA;  D5 --&gt; D1&#xA;  D4 -- Yes --&gt; D6[Prepare a rollback plan and choose a low-traffic window]&#xA;  D6 --&gt; D7[PR approved]&#xA;  D7 --&gt; D8[terraform apply]&#xA;  D8 --&gt; D9[Availability check after deletion]&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;suggestions-for-day-to-day-collaboration&#34;&gt;Suggestions for day-to-day collaboration&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;Put credentials in environment variables or CI secrets; do not write them into &lt;code&gt;.tf&lt;/code&gt; or the repository&lt;/li&gt;&#xA;&lt;li&gt;Enable protection policies for critical resources to prevent accidental deletion&lt;/li&gt;&#xA;&lt;li&gt;Split directories by resource type&lt;/li&gt;&#xA;&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt; regularly to check for and correct infrastructure drift &lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;, so you do not end up with manual dashboard changes by mistake&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Although this workflow may look a bit cumbersome, every change is recorded, auditable, and reversible — and most importantly, reproducible. That is where IaC delivers its real value.&lt;/p&gt;&#xA;&lt;h2 id=&#34;references&#34;&gt;References&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://candinya.com/posts/manage-cloudflare-dns-with-terraform/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Using Terraform to manage DNS records on CloudFlare - Candinya&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/tutorials/automation/github-actions&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Automate Terraform with GitHub Actions&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/files/tfquery&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Query configuration files&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/block/tfquery/list&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;list block reference&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cloudflare/cf-terraforming&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developers.cloudflare.com/terraform/advanced-topics/import-cloudflare-resources/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Import Cloudflare resources&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Cloudflare Provider - Terraform Registry&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_code&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Infrastructure as Code&lt;/a&gt; refers to a method of defining and deploying the required infrastructure using machine-readable configuration files.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;Each provider uses different field names and formats in its configuration files, but these can be converted fairly easily with a script.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;One especially important thing to note is that the state file may contain sensitive information stored in plain text, such as database passwords and API keys, so you must never commit the &lt;code&gt;.tfstate&lt;/code&gt; file to a public code repository.&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;HashiCorp Configuration Language, a declarative configuration language developed by HashiCorp, designed to balance machine readability with human readability.&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:5&#34;&gt;&#xA;&lt;p&gt;Idempotence means that when a computer system or interface receives the same request multiple times, the effect is the same as if it had been executed once. No matter how many times it runs, the system’s final state remains consistent.&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:6&#34;&gt;&#xA;&lt;p&gt;If you only want Terraform to stop managing a resource, rather than actually destroying it in the cloud, you should use the &lt;code&gt;terraform state rm&lt;/code&gt; command instead of deleting the resource block from the code and then running &lt;code&gt;apply&lt;/code&gt;, otherwise the real resource in the cloud will be destroyed as well.&amp;#160;&lt;a href=&#34;#fnref:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:7&#34;&gt;&#xA;&lt;p&gt;Infrastructure drift refers to a situation where infrastructure is modified in reality through non-IaC means such as clicking around in a console, causing the actual state to differ from the state declared in code.&amp;#160;&lt;a href=&#34;#fnref:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item></channel>
</rss>
