[AWS Provider v6] Part 2: Restructuring with Zero Downtime using `moved` blocks + [AWS Provider v6] Part 3: Catching 'Ghost Diffs'

AWS Provider v6 Migration
kubernetesterraformawsmigrationdrift

[AWS Provider v6] Part 2: Restructuring with Zero Downtime using moved blocks

There comes a time after an upgrade when you need to change the resource structure. But what if terraform plan shows a red Destroy? You cannot simply delete a running DB or server.

1. Symptoms

I moved a single EC2 instance existing in the root module into modules/web_server for management convenience.

[Plan Result]

Plan: 1 to add, 0 to change, 1 to destroy.
  - resource "aws_instance" "web" { ... }  # Deleted (IP changes, downtime occurs)
  + resource "aws_instance" "this" { ... } # Created new inside the module

The instance on AWS is the same, but Terraform perceives it as a "different resource" because the Address has changed.

2. Solution: moved block (Terraform Core v1.1+)

In the past, we used the terraform state mv command, but now we record the history in code.

module "web_server" {
  source = "./modules/web_server"
}

# Refactoring Declaration: Map existing resource to inside the module
moved {
from = aws_instance.web
to = module.web_server.aws_instance.this
}

3. Validation

When running plan again after adding the moved block, the following message must appear for success:

"Resource instance has moved." Plan: 0 to add, 0 to change, 0 to destroy.

With this, I changed the logical address without recreating the resource.

4. Rollback

What if an error occurs due to incorrect mapping? Simply comment out the moved block and revert the code to recover immediately. There is no need to find a backup file like with the state mv command.


[AWS Provider v6] Part 3: Catching 'Ghost Diffs'

There are cases where "changes exist" appears every time terraform plan is run, even though the code hasn't been modified at all. If left unchecked, "genuinely important changes" get buried in the noise and become unidentified.

1. Case A: Auto Scaling Group (ASG) Capacity Conflict

[Symptom]
A running ASG increased instances to 5 due to traffic spikes (AWS State). However, the Terraform code is set to desired_capacity = 2, so it tries to terminate 3 healthy servers every deployment.

[Solution: Lifecycle Ignore]
Attributes that change externally (AWS/K8s), like ASG capacity, must be excluded from Terraform's monitoring.

resource "aws_autoscaling_group" "app" {
  desired_capacity = 2 # Applied only during initial creation

lifecycle {
ignore_changes = [
desired_capacity,
target_group_arns
]
}
}

2. Case B: Unmanaged Tags (Tag Drift)

[Symptom]
AWS Backup or EKS automatically adds tags like aws:eks:... or aws-backup-... to resources. Terraform considers these "unknown configurations" and tries to delete them.

[Solution: Provider Level Defense]
Instead of adding ignore_changes to every resource, use ignore_tags supported from AWS Provider v5+.

provider "aws" {
  region = "ap-northeast-2"
   
  # Terraform does not touch tags with specific prefixes
  ignore_tags {
    key_prefixes = ["kubernetes.io/", "aws-backup", "karpenter.sh/"]
  }
}

3. Validation

After applying the settings, executing terraform plan should output "No changes".