<?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[Caleb Faruki]]></title><description><![CDATA[Software engineer based in Paris, France. Working as head of technology for LEATHER SPA. Also teaching myself game development on the side.]]></description><link>https://calebfaruki.me</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 04:26:36 GMT</lastBuildDate><atom:link href="https://calebfaruki.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[NextJS recipe: Tracking 404s with Plausible and NextJS App Router]]></title><description><![CDATA[By default, all pages are statically rendered. If you add any backend or middleware logic to your NextJS page, it will be rendered as a server component.
Plausible Analytics requires access to the window object to track 404 events. So create a client...]]></description><link>https://calebfaruki.me/nextjs-plausible-app-router-404</link><guid isPermaLink="true">https://calebfaruki.me/nextjs-plausible-app-router-404</guid><category><![CDATA[plausible]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[404]]></category><dc:creator><![CDATA[Caleb Faruki]]></dc:creator><pubDate>Sun, 17 Mar 2024 17:30:47 GMT</pubDate><content:encoded><![CDATA[<p>By default, all pages are statically rendered. If you add any backend or middleware logic to your NextJS page, it will be rendered as a server component.</p>
<p>Plausible Analytics requires access to the window object to track 404 events. So create a client component.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> Script <span class="hljs-keyword">from</span> <span class="hljs-string">"next/script"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FourOhFourEvent</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> &lt;Script onReady={<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">window</span>.plausible(<span class="hljs-string">'404'</span>, { props: { path: <span class="hljs-built_in">window</span>.location.pathname } })} /&gt;
}
</code></pre>
<p>Then create <code>app/not-found.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> FourOhFourEvent <span class="hljs-keyword">from</span> <span class="hljs-string">"./path/to/component/FourOhFourEvent"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;&gt;
            &lt;FourOhFourEvent /&gt;
            &lt;h1&gt;Not Found&lt;/h1&gt;
        &lt;/&gt;
    )
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Terraform recipe: domain-level redirects using Cloudflare]]></title><description><![CDATA[If you work for an organization of a sufficient size with strong brand recognition, you may encounter the problem of needing to buy any domain names remotely similar to your canonical domain name.
A minor benefit is that visitors can make mistakes ty...]]></description><link>https://calebfaruki.me/terraform-cloudflare-domain-redirects</link><guid isPermaLink="true">https://calebfaruki.me/terraform-cloudflare-domain-redirects</guid><category><![CDATA[Terraform]]></category><category><![CDATA[Infrastructure as code]]></category><dc:creator><![CDATA[Caleb Faruki]]></dc:creator><pubDate>Thu, 04 Jan 2024 14:27:58 GMT</pubDate><content:encoded><![CDATA[<p>If you work for an organization of a sufficient size with strong brand recognition, you may encounter the problem of needing to buy any domain names remotely similar to your canonical domain name.</p>
<p>A minor benefit is that visitors can make mistakes typing your domain and still get where they need to go. A <strong>major</strong> benefit is mitigating phishing attacks.</p>
<p>A classic example that I have recently seen is DHL phishing attacks. They'll send a text message claiming that you need to pay tariffs on an international shipment before it clears customs. You land on a domain name like <strong>dlh.com</strong> instead of <strong>dhl.com</strong> and enter your credit card number to realize their site is broken. Then the real bad news settles in.</p>
<p>Thankfully a totally more legitimate company in another industry owns dlh.com. But the lesson remains important nevertheless.</p>
<p>Lesson aside, how do you manage this at scale when you're using Terraform and Cloudflare? Create a module that implements a Cloudflare Redirect Rule for a Cloudflare Zone. Add a <code>main.tf</code>, <code>variables.tf</code>, and <code>outputs.tf</code> file for said module.</p>
<pre><code class="lang-plaintext"># ./modules/domain_redirect/variables.tf
variable cloudflare_account_id {
  type = string
}

variable origin {
  type = string
  description = "Source hostname"
}

variable destination {
  type = string
  description = "Target hostname"
}
</code></pre>
<pre><code class="lang-plaintext"># ./modules/domain_redirect/main.tf
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~&gt; 4.21.0"
    }
  }
}

resource cloudflare_zone zone {
  account_id = var.cloudflare_account_id
  zone = "${var.origin}"
}

resource cloudflare_record stub {
  zone_id = cloudflare_zone.zone.id
  comment = "Stubbed record needed to trigger DNS resolution and subsequent redirect"
  type = "A"
  name = "@"
  value = "192.0.2.1"
  proxied = true
  ttl = 1
}

resource cloudflare_ruleset redirect {
  zone_id = cloudflare_zone.zone.id
  name = "Hostname redirect to ${var.destination}"
  kind = "zone"
  phase = "http_request_dynamic_redirect"
  rules {
    action = "redirect"
    action_parameters {
      from_value {
        status_code = 301
        target_url {
          value = "https://${var.destination}"
        }
        preserve_query_string = true
      }
    }
    expression  = "http.host eq \"${var.origin}\""
    description = "Redirect all requests from ${var.origin} to ${var.destination}"
    enabled     = true
  }
}
</code></pre>
<p>You'll notice the DNS record above is called a stub. This is because Cloudflare does not execute any rulesets for a given zone until there is at least one root A or CNAME record. In my example above, I use a <a target="_blank" href="https://en.wikipedia.org/wiki/Reserved_IP_addresses#IPv4">reserved IPv4 address</a> belonging to the TEST-NET-1 IP block designated for documentation purposes by the IANA.</p>
<p>And in your parent <code>main.tf</code> file, you can use a <code>for_each</code> loop to map over every redirect you need to implement like so:</p>
<pre><code class="lang-plaintext"># ./main.tf
variable domain_redirects {
  type = map(string)
  default = {
    "example1.com" = "example.com"
    "example2.com" = "example.com"
    "example1.fr" = "example.fr"
  }
}

module domain_redirects {
  cloudflare_account_id = local.cloudflare_account_id
  source = "./modules/domain_redirect"
  for_each = var.domain_redirects
  origin = each.key
  destination = each.value
}
</code></pre>
<p>The benefit of this <code>map(string)</code> type variable is also that you can have multiple destinations in case your company has canonical domains for different use cases (like <code>example.ai</code>) or regions (like <code>example.fr</code>).</p>
<p>Lastly, you'll want to create an output value so that you'll see a more human-readable result for your domain-level redirects. First, you'll need to create an outputs file for your module:</p>
<pre><code class="lang-plaintext">#./module/domain_redirect/outputs.tf
output domain_redirect {
  value = cloudflare_ruleset.redirect.rules[0].action_parameters[0].from_value[0].target_url[0].value
}
</code></pre>
<p>And then add another <code>outputs.tf</code> file to the parent folder where your <code>domain_redirect</code> module is used:</p>
<pre><code class="lang-plaintext"># ./outputs.tf
output domain_redirects {
  value = { for domain, redirect in module.domain_redirects : domain =&gt; redirect.domain_redirect }
  description = "The target URLs for each domain redirect"
}
</code></pre>
<p>After that, you get human readable output when you run your <code>terraform plan</code> command.</p>
<pre><code class="lang-plaintext">module.domain_redirects["example1.com"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example1.com"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example1.com"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

domain_redirects = {
  "example1.com" = "https://example.com"
  "example2.com" = "https://example.com"
  "example1.fr" = "https://example.fr"
}
</code></pre>
<p>That's it!</p>
]]></content:encoded></item><item><title><![CDATA[Terraform surgery: safely rename resources without destroying them]]></title><description><![CDATA[Have you ever run into this scenario while renaming a resource in your terraform code?
Plan: 1 to add, 0 to change, 1 to destroy.

But you didn't add a new resource. And there's still data in your S3 bucket. Destroying the bucket would far from ideal...]]></description><link>https://calebfaruki.me/terraform-renaming-resources-moved-block</link><guid isPermaLink="true">https://calebfaruki.me/terraform-renaming-resources-moved-block</guid><category><![CDATA[Terraform]]></category><category><![CDATA[Infrastructure as code]]></category><dc:creator><![CDATA[Caleb Faruki]]></dc:creator><pubDate>Sat, 30 Dec 2023 15:12:43 GMT</pubDate><content:encoded><![CDATA[<p>Have you ever run into this scenario while renaming a resource in your terraform code?</p>
<pre><code class="lang-swift"><span class="hljs-type">Plan</span>: <span class="hljs-number">1</span> to add, <span class="hljs-number">0</span> to change, <span class="hljs-number">1</span> to destroy.
</code></pre>
<p>But you didn't add a new resource. And there's still data in your S3 bucket. Destroying the bucket would far from ideal. How do we tell Terraform that we don't want to replace the resource but simply rename it?</p>
<p>Let's walk through a straightforward example: a super generic S3 bucket declaration.</p>
<pre><code class="lang-swift">resource aws_s3_bucket s3_bucket {
  bucket = <span class="hljs-string">"assets"</span>
}
</code></pre>
<p>At some point, you apply your terraform changes. But you forgot something. You realize the S3 bucket name is <strong>too generic.</strong> So you rename your S3 bucket to something specific so you can better remember its purpose:</p>
<pre><code class="lang-swift">resource aws_s3_bucket very_specific_s3_bucket {
  bucket = <span class="hljs-string">"assets"</span>
}
</code></pre>
<p>Now you run <code>terraform plan</code> to see the expected changes when you see the dreaded message: <code>Plan: 1 to add, 0 to change, 1 to destroy.</code></p>
<pre><code class="lang-swift">$ terraform plan
<span class="hljs-type">Terraform</span> will perform the following actions:

  # aws_s3_bucket.very_specific_s3_bucket will be created
  + resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"very_specific_s3_bucket"</span> {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = <span class="hljs-string">"assets"</span>
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = <span class="hljs-literal">false</span>
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket.s3_bucket will be destroyed
  # (because aws_s3_bucket.s3_bucket <span class="hljs-keyword">is</span> not <span class="hljs-keyword">in</span> configuration)
  - resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"s3_bucket"</span> {
      - arn                         = <span class="hljs-string">"arn:aws:s3:::assets"</span> -&gt; null
      - bucket                      = <span class="hljs-string">"assets"</span> -&gt; null
      - bucket_domain_name          = <span class="hljs-string">"assets.s3.amazonaws.com"</span> -&gt; null
      - bucket_regional_domain_name = <span class="hljs-string">"assets.s3.amazonaws.com"</span> -&gt; null
      - force_destroy               = <span class="hljs-literal">false</span> -&gt; null
      - hosted_zone_id              = <span class="hljs-string">"..."</span> -&gt; null
      - id                          = <span class="hljs-string">"assets"</span> -&gt; null
      - object_lock_enabled         = <span class="hljs-literal">false</span> -&gt; null
      - region                      = <span class="hljs-string">"us-east-1"</span> -&gt; null
      - request_payer               = <span class="hljs-string">"BucketOwner"</span> -&gt; null
      - tags                        = {} -&gt; null
      - tags_all                    = {
          - <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        } -&gt; null
      - grant {
          - id          = <span class="hljs-string">"..."</span> -&gt; null
          - permissions = [
              - <span class="hljs-string">"FULL_CONTROL"</span>,
            ] -&gt; null
          - type        = <span class="hljs-string">"CanonicalUser"</span> -&gt; null
        }
      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = <span class="hljs-literal">false</span> -&gt; null
              - apply_server_side_encryption_by_default {
                  - sse_algorithm = <span class="hljs-string">"AES256"</span> -&gt; null
                }
            }
        }
      - versioning {
          - enabled    = <span class="hljs-literal">false</span> -&gt; null
          - mfa_delete = <span class="hljs-literal">false</span> -&gt; null
        }
    }
<span class="hljs-type">Plan</span>: <span class="hljs-number">1</span> to add, <span class="hljs-number">0</span> to change, <span class="hljs-number">1</span> to destroy.
</code></pre>
<p>How do you fix this? Start by adding this line:</p>
<pre><code class="lang-swift">moved {
  from = aws_s3_bucket.s3_bucket
  to = aws_s3_bucket.very_specific_s3_bucket
}
</code></pre>
<p>Then run <code>terraform plan</code> to see if your terraform output changes. You should see this output upon running the plan command:</p>
<pre><code class="lang-swift">  # aws_s3_bucket.s3_bucket has moved to aws_s3_bucket.very_specific_s3_bucket
    resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"very_specific_s3_bucket"</span> {
        id                          = <span class="hljs-string">"assets"</span>
        tags                        = {}
        # (<span class="hljs-number">10</span> unchanged attributes hidden)

        # (<span class="hljs-number">3</span> unchanged blocks hidden)
    }
</code></pre>
<p>Now that you've validated your Terraform state will be updated to reflect this resource reference change, you can confidently run <code>terraform apply</code> without fear that your S3 bucket will be accidentally destroyed. Once the changes are applied successfully, you can remove the <code>moved {}</code> block. No subsequent <code>terraform apply</code> is necessary.</p>
<h2 id="heading-but-what-about-terraform-state-mv">But what about terraform state mv?</h2>
<p>An alternative to using the <code>moved { ... }</code> block is the <code>terraform state mv</code> command. But using it runs counter to the intent of terraform. By running this command, you lose the chance to see your changes planned before they're applied. Without that, it's very easy to make mistakes. Whereas using the <code>moved {}</code> block allows you to perform a dry run of your changes and confirm that you will not accidentally make an disruptive change.</p>
<p>If you're still not convinced that the <code>moved {}</code> block is the better solution, consider why you're even using Terraform in the first place: "infrastructure as code". When submitting a pull request for your code changes, it should be clear to the person reviewing the changes <strong>before you actually make the change</strong>. You can always submit a subsequent pull request to delete the moved blocks after you've confirmed that the resource name changes have been successfully applied. If neither of my reasons convince you, Terraform is probably not the right tool for you.</p>
<p>Despite having said that, I'll explain one caveat...</p>
<h2 id="heading-okay-but-what-about-foreach-loops">Okay but what about for_each loops?</h2>
<p>There's one exception to that rule that I've seen so far. And it's not really an exception if you're using the latest stable release of Terraform (which is <strong>v1.6.6</strong> as of this writing).</p>
<p>For versions as recent as <strong>v1.5.7</strong>, the <code>moved { ... }</code> block does not work when applied to resources created using a <code>for_each</code> loop. This isn't ideal if your infrastructure is sufficiently large enough because it's annoying to repeat yourself.</p>
<p>Here's a basic example:</p>
<pre><code class="lang-swift">resource aws_s3_bucket s3_bucket {
  bucket = <span class="hljs-string">"assets"</span>
}
moved {
  from = aws_s3_bucket.s3_bucket
  to = aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"very_specific_s3_bucket"</span>]
}

resource aws_s3_bucket second_s3_bucket {
  bucket = <span class="hljs-string">"other-assets"</span>
}
moved {
  from = aws_s3_bucket.second_s3_bucket
  to = aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"another_s3_bucket"</span>]
}

variable lots_of_buckets {
  type = <span class="hljs-built_in">map</span>(string)
  <span class="hljs-keyword">default</span> = {
    <span class="hljs-string">"very_specific_s3_bucket"</span>: <span class="hljs-string">"assets"</span>,
    <span class="hljs-string">"another_s3_bucket"</span>: <span class="hljs-string">"other-assets"</span>
  }
}
resource aws_s3_bucket all_my_s3_buckets {
  for_each = <span class="hljs-keyword">var</span>.lots_of_buckets
  bucket   = each.value
}
</code></pre>
<p>Here, I've defined two S3 buckets and I've consolidated them because they're configured exactly the same way with the exception of their bucket names.</p>
<p>How will Terraform behave when you do this? You'll get this message:</p>
<pre><code class="lang-swift"><span class="hljs-type">Plan</span>: <span class="hljs-number">2</span> to add, <span class="hljs-number">0</span> to change, <span class="hljs-number">2</span> to destroy.
</code></pre>
<p>But why? I don't know. But suffice it to say there's probably an article about it somewhere else. I'll limit my discussion to when it's ACTUALLY worth using the <code>terraform state mv</code> command.</p>
<p>This is what the output of <code>terraform plan</code> will look like when trying to move distinct resource declarations into a for_each loop:</p>
<pre><code class="lang-swift">$ terraform plan
<span class="hljs-type">Terraform</span> will perform the following actions:

  # aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"very_specific_s3_bucket"</span>] will be created
  + resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"all_my_s3_buckets"</span> {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = <span class="hljs-string">"assets"</span>
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = <span class="hljs-literal">false</span>
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket.very_specific_s3_bucket will be destroyed
  # (because aws_s3_bucket.very_specific_s3_bucket was moved to aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"very_specific_s3_bucket"</span>], which <span class="hljs-keyword">is</span> not <span class="hljs-keyword">in</span> configuration)
  # (moved from aws_s3_bucket.very_specific_s3_bucket)
  - resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"very_specific_s3_bucket"</span> {
      - arn                         = <span class="hljs-string">"arn:aws:s3:::assets"</span> -&gt; null
      - bucket                      = <span class="hljs-string">"assets"</span> -&gt; null
      - bucket_domain_name          = <span class="hljs-string">"assets.s3.amazonaws.com"</span> -&gt; null
      - bucket_regional_domain_name = <span class="hljs-string">"assets.s3.amazonaws.com"</span> -&gt; null
      - force_destroy               = <span class="hljs-literal">false</span> -&gt; null
      - hosted_zone_id              = <span class="hljs-string">"..."</span> -&gt; null
      - id                          = <span class="hljs-string">"assets"</span> -&gt; null
      - object_lock_enabled         = <span class="hljs-literal">false</span> -&gt; null
      - region                      = <span class="hljs-string">"us-east-1"</span> -&gt; null
      - request_payer               = <span class="hljs-string">"BucketOwner"</span> -&gt; null
      - tags                        = {} -&gt; null
      - tags_all                    = {
          - <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        } -&gt; null
      - grant {
          - id          = <span class="hljs-string">"..."</span> -&gt; null
          - permissions = [
              - <span class="hljs-string">"FULL_CONTROL"</span>,
            ] -&gt; null
          - type        = <span class="hljs-string">"CanonicalUser"</span> -&gt; null
        }
      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = <span class="hljs-literal">false</span> -&gt; null
              - apply_server_side_encryption_by_default {
                  - sse_algorithm = <span class="hljs-string">"AES256"</span> -&gt; null
                }
            }
        }
      - versioning {
          - enabled    = <span class="hljs-literal">false</span> -&gt; null
          - mfa_delete = <span class="hljs-literal">false</span> -&gt; null
        }
    }

  # aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"another_s3_bucket"</span>] will be created
  + resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"all_my_s3_buckets"</span> {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = <span class="hljs-string">"other-assets"</span>
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = <span class="hljs-literal">false</span>
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket.another_s3_bucket will be destroyed
  # (because aws_s3_bucket.another_s3_bucket was moved to aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"another_s3_bucket"</span>], which <span class="hljs-keyword">is</span> not <span class="hljs-keyword">in</span> configuration)
  # (moved from aws_s3_bucket.another_s3_bucket)
  - resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"another_s3_bucket"</span> {
      - arn                         = <span class="hljs-string">"arn:aws:s3:::assets"</span> -&gt; null
      - bucket                      = <span class="hljs-string">"other-assets"</span> -&gt; null
      - bucket_domain_name          = <span class="hljs-string">"other-assets.s3.amazonaws.com"</span> -&gt; null
      - bucket_regional_domain_name = <span class="hljs-string">"other-assets.s3.amazonaws.com"</span> -&gt; null
      - force_destroy               = <span class="hljs-literal">false</span> -&gt; null
      - hosted_zone_id              = <span class="hljs-string">"..."</span> -&gt; null
      - id                          = <span class="hljs-string">"other-assets"</span> -&gt; null
      - object_lock_enabled         = <span class="hljs-literal">false</span> -&gt; null
      - region                      = <span class="hljs-string">"us-east-1"</span> -&gt; null
      - request_payer               = <span class="hljs-string">"BucketOwner"</span> -&gt; null
      - tags                        = {} -&gt; null
      - tags_all                    = {
          - <span class="hljs-string">"CreatedBy"</span>   = <span class="hljs-string">"terraform"</span>
        } -&gt; null
      - grant {
          - id          = <span class="hljs-string">"..."</span> -&gt; null
          - permissions = [
              - <span class="hljs-string">"FULL_CONTROL"</span>,
            ] -&gt; null
          - type        = <span class="hljs-string">"CanonicalUser"</span> -&gt; null
        }
      - server_side_encryption_configuration {
          - rule {
              - bucket_key_enabled = <span class="hljs-literal">false</span> -&gt; null
              - apply_server_side_encryption_by_default {
                  - sse_algorithm = <span class="hljs-string">"AES256"</span> -&gt; null
                }
            }
        }
      - versioning {
          - enabled    = <span class="hljs-literal">false</span> -&gt; null
          - mfa_delete = <span class="hljs-literal">false</span> -&gt; null
        }
    }
<span class="hljs-type">Plan</span>: <span class="hljs-number">2</span> to add, <span class="hljs-number">0</span> to change, <span class="hljs-number">2</span> to destroy.
</code></pre>
<p>How do you solve this?</p>
<pre><code class="lang-swift">$ terraform state mv 'aws_s3_bucket.very_specific_s3_bucket' 'aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"very_specific_s3_bucket"</span>]')
<span class="hljs-type">Move</span> <span class="hljs-string">"aws_s3_bucket.very_specific_s3_bucket"</span> to <span class="hljs-string">"aws_s3_bucket.all_my_s3_buckets[\"very_specific_s3_bucket\"]"</span>
<span class="hljs-type">Successfully</span> moved <span class="hljs-number">1</span> object(s).
<span class="hljs-type">Releasing</span> state lock. <span class="hljs-type">This</span> may take a few moments...

$ terraform state mv 'aws_s3_bucket.another_s3_bucket' 'aws_s3_bucket.all_my_s3_buckets[<span class="hljs-string">"another_s3_bucket"</span>]')
<span class="hljs-type">Move</span> <span class="hljs-string">"aws_s3_bucket.another_s3_bucket"</span> to <span class="hljs-string">"aws_s3_bucket.all_my_s3_buckets[\"another_s3_bucket\"]"</span>
<span class="hljs-type">Successfully</span> moved <span class="hljs-number">1</span> object(s).
<span class="hljs-type">Releasing</span> state lock. <span class="hljs-type">This</span> may take a few moments...
</code></pre>
<p>That's about it. Drop me a message on <a target="_blank" href="https://cyberplace.social/@kleb">Mastodon</a> if you have feedback.</p>
]]></content:encoded></item></channel></rss>