migrate: add real-world AWS v3→v6 migration examples

ast-hclwrite-migration
Daniel Schmidt 2 months ago
parent b6e5606f4e
commit 874d047f74
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -0,0 +1,60 @@
# Sample Terraform configuration demonstrating AWS provider v3 patterns
# that need migration to v4.
#
# Run: terraform migrate run -migrations-dir=. "v3to4/*"
# --- S3 Bucket Object Rename ---
# v4 renames aws_s3_bucket_object to aws_s3_object.
# The migration renames the resource type and rewrites all references.
resource "aws_s3_bucket" "assets" {
bucket = "my-app-assets"
# v4 extracts versioning into a standalone aws_s3_bucket_versioning resource.
# The extract_to_resource migration handles this automatically.
versioning {
enabled = true
}
# v4 extracts server_side_encryption_configuration into a standalone resource.
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
# v4 removes the acl argument from aws_s3_bucket.
# Must be migrated to a separate aws_s3_bucket_acl resource.
acl = "private"
}
resource "aws_s3_bucket_object" "config" {
bucket = aws_s3_bucket.assets.id
key = "config.json"
content = jsonencode({ version = "1.0" })
}
resource "aws_s3_bucket_object" "readme" {
bucket = aws_s3_bucket.assets.id
key = "README.md"
source = "README.md"
}
data "aws_s3_bucket_objects" "all_objects" {
bucket = aws_s3_bucket.assets.id
}
# References to aws_s3_bucket_object get rewritten to aws_s3_object
output "config_object_id" {
value = aws_s3_bucket_object.config.id
}
output "readme_etag" {
value = aws_s3_bucket_object.readme.etag
}
output "object_keys" {
value = data.aws_s3_bucket_objects.all_objects.keys
}

@ -0,0 +1,21 @@
{
"name": "v3to4/extract_s3_acl",
"description": "Extract acl argument from aws_s3_bucket, move to aws_s3_bucket_acl. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#s3-bucket-refactor",
"match": {
"block_type": "resource",
"label": "aws_s3_bucket"
},
"actions": [
{
"action": "extract_to_resource",
"name": "acl",
"to": "aws_s3_bucket_acl",
"wire_attribute": "bucket",
"wire_traversal": "id"
},
{
"action": "remove_attribute",
"name": "acl"
}
]
}

@ -0,0 +1,17 @@
{
"name": "v3to4/extract_s3_server_side_encryption",
"description": "Extract aws_s3_bucket.server_side_encryption_configuration into standalone resource. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#s3-bucket-refactor",
"match": {
"block_type": "resource",
"label": "aws_s3_bucket"
},
"actions": [
{
"action": "extract_to_resource",
"name": "server_side_encryption_configuration",
"to": "aws_s3_bucket_server_side_encryption_configuration",
"wire_attribute": "bucket",
"wire_traversal": "id"
}
]
}

@ -0,0 +1,17 @@
{
"name": "v3to4/extract_s3_versioning",
"description": "Extract aws_s3_bucket.versioning into standalone aws_s3_bucket_versioning resource. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#s3-bucket-refactor",
"match": {
"block_type": "resource",
"label": "aws_s3_bucket"
},
"actions": [
{
"action": "extract_to_resource",
"name": "versioning",
"to": "aws_s3_bucket_versioning",
"wire_attribute": "bucket",
"wire_traversal": "id"
}
]
}

@ -0,0 +1,14 @@
{
"name": "v3to4/rename_s3_bucket_object",
"description": "Rename aws_s3_bucket_object to aws_s3_object. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#resource-aws_s3_bucket_object",
"match": {
"block_type": "resource",
"label": "aws_s3_bucket_object"
},
"actions": [
{
"action": "rename_resource",
"to": "aws_s3_object"
}
]
}

@ -0,0 +1,14 @@
{
"name": "v3to4/rename_s3_bucket_objects_data",
"description": "Rename data.aws_s3_bucket_objects to data.aws_s3_objects. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#data-source-aws_s3_bucket_objects",
"match": {
"block_type": "data",
"label": "aws_s3_bucket_objects"
},
"actions": [
{
"action": "rename_resource",
"to": "aws_s3_objects"
}
]
}

@ -0,0 +1,116 @@
# Sample Terraform configuration demonstrating AWS provider v4 patterns
# that need migration to v5.
#
# Run: terraform migrate run -migrations-dir=. "v4to5/*"
# --- EC2-Classic Attribute Removal ---
# v5 removes all EC2-Classic support. These attributes no longer exist.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#ec2-classic-resource-and-data-source-removal
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
security_groups = ["default"]
vpc_classic_link_id = "vpc-abc123"
vpc_classic_link_security_groups = ["sg-abc123"]
vpc_security_group_ids = [aws_security_group.web.id]
}
resource "aws_security_group" "web" {
name = "web-sg"
}
# --- Autoscaling Attachment Rename ---
# v5 renames alb_target_group_arn to lb_target_group_arn.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_autoscaling_attachment
resource "aws_autoscaling_attachment" "asg_alb" {
autoscaling_group_name = "my-asg"
alb_target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/abc123"
}
# --- Elasticache Replication Group ---
# v5 renames several attributes and removes the cluster_mode block.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_elasticache_replication_group
resource "aws_elasticache_replication_group" "redis" {
replication_group_id = "my-redis"
replication_group_description = "Production Redis cluster"
node_type = "cache.r6g.large"
number_cache_clusters = 3
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
cluster_mode {
num_node_groups = 3
replicas_per_node_group = 2
}
}
# --- DB Instance Name Rename ---
# v5 renames 'name' to 'db_name' on aws_db_instance.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_db_instance
resource "aws_db_instance" "postgres" {
identifier = "my-postgres"
engine = "postgres"
engine_version = "14.1"
instance_class = "db.t3.micro"
name = "myappdb"
username = "admin"
password = var.db_password
}
# --- DB Security Group Removal (EC2-Classic) ---
# v5 removes aws_db_security_group entirely.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#ec2-classic-resource-and-data-source-removal
resource "aws_db_security_group" "legacy" {
name = "legacy-db-sg"
}
resource "aws_db_instance" "legacy_db" {
identifier = "legacy"
engine = "mysql"
instance_class = "db.t3.micro"
name = "legacydb"
username = "admin"
password = var.db_password
# This reference will get a FIXME comment when the security group is removed
db_security_groups = [aws_db_security_group.legacy.name]
}
# --- OpenSearch Kibana Rename ---
# v5 renames kibana_endpoint to dashboard_endpoint.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_opensearch_domain
resource "aws_opensearch_domain" "search" {
domain_name = "my-search"
engine_version = "OpenSearch_2.3"
}
output "search_dashboard" {
value = aws_opensearch_domain.search.kibana_endpoint
}
# --- RDS Cluster Engine Now Required ---
# v5 removes the default for engine on aws_rds_cluster.
# The add_attribute action only sets it if not already present.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_rds_cluster
resource "aws_rds_cluster" "aurora" {
cluster_identifier = "my-aurora"
master_username = "admin"
master_password = var.db_password
}
resource "aws_rds_cluster" "aurora_mysql" {
cluster_identifier = "my-aurora-mysql"
engine = "aurora-mysql"
master_username = "admin"
master_password = var.db_password
}
variable "db_password" {
type = string
sensitive = true
}

@ -0,0 +1,11 @@
{
"name": "v4to5/flatten_elasticache_cluster_mode",
"description": "Flatten cluster_mode block into top-level attributes. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_elasticache_replication_group",
"match": {
"block_type": "resource",
"label": "aws_elasticache_replication_group"
},
"actions": [
{"action": "flatten_block", "name": "cluster_mode"}
]
}

@ -0,0 +1,11 @@
{
"name": "v4to5/remove_db_security_group",
"description": "Remove aws_db_security_group (EC2-Classic). See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#ec2-classic-resource-and-data-source-removal",
"match": {
"block_type": "resource",
"label": "aws_db_security_group"
},
"actions": [
{"action": "remove_resource", "text": "FIXME: aws_db_security_group has been removed in v5 (EC2-Classic). Use aws_db_subnet_group with VPC security groups instead."}
]
}

@ -0,0 +1,13 @@
{
"name": "v4to5/remove_ec2_classic",
"description": "Remove EC2-Classic attributes from aws_instance. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#ec2-classic-resource-and-data-source-removal",
"match": {
"block_type": "resource",
"label": "aws_instance"
},
"actions": [
{"action": "remove_attribute", "name": "security_groups"},
{"action": "remove_attribute", "name": "vpc_classic_link_id"},
{"action": "remove_attribute", "name": "vpc_classic_link_security_groups"}
]
}

@ -0,0 +1,11 @@
{
"name": "v4to5/rename_autoscaling_attachment",
"description": "Rename alb_target_group_arn to lb_target_group_arn. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_autoscaling_attachment",
"match": {
"block_type": "resource",
"label": "aws_autoscaling_attachment"
},
"actions": [
{"action": "rename_attribute", "from": "alb_target_group_arn", "to": "lb_target_group_arn"}
]
}

@ -0,0 +1,11 @@
{
"name": "v4to5/rename_db_instance_name",
"description": "Rename 'name' to 'db_name' on aws_db_instance. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_db_instance",
"match": {
"block_type": "resource",
"label": "aws_db_instance"
},
"actions": [
{"action": "rename_attribute", "from": "name", "to": "db_name"}
]
}

@ -0,0 +1,13 @@
{
"name": "v4to5/rename_elasticache_attrs",
"description": "Rename deprecated attributes on aws_elasticache_replication_group. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_elasticache_replication_group",
"match": {
"block_type": "resource",
"label": "aws_elasticache_replication_group"
},
"actions": [
{"action": "rename_attribute", "from": "replication_group_description", "to": "description"},
{"action": "rename_attribute", "from": "number_cache_clusters", "to": "num_cache_clusters"},
{"action": "rename_attribute", "from": "availability_zones", "to": "preferred_cache_cluster_azs"}
]
}

@ -0,0 +1,11 @@
{
"name": "v4to5/rename_opensearch_kibana",
"description": "Rename kibana_endpoint to dashboard_endpoint on aws_opensearch_domain. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_opensearch_domain",
"match": {
"block_type": "resource",
"label": "aws_opensearch_domain"
},
"actions": [
{"action": "rename_attribute", "from": "kibana_endpoint", "to": "dashboard_endpoint"}
]
}

@ -0,0 +1,11 @@
{
"name": "v4to5/require_rds_cluster_engine",
"description": "Add engine attribute to aws_rds_cluster (now required, no default). See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#resource-aws_rds_cluster",
"match": {
"block_type": "resource",
"label": "aws_rds_cluster"
},
"actions": [
{"action": "add_attribute", "name": "engine", "value": "aurora"}
]
}

@ -0,0 +1,108 @@
# Sample Terraform configuration demonstrating AWS provider v5 patterns
# that need migration to v6.
#
# Run: terraform migrate run -migrations-dir=. "v5to6/*"
# --- CPU Options Restructure ---
# v6 moves cpu_core_count and cpu_threads_per_core into a cpu_options block.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_instance
resource "aws_instance" "compute" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "c5.xlarge"
cpu_core_count = 2
cpu_threads_per_core = 1
}
resource "aws_instance" "dynamic_compute" {
ami = var.ami_id
instance_type = var.instance_type
cpu_core_count = var.cpu_cores
cpu_threads_per_core = var.env == "prod" ? 2 : 1
}
# --- Batch Compute Environment Name Rename ---
# v6 renames compute_environment_name to name.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_batch_compute_environment
resource "aws_batch_compute_environment" "batch" {
compute_environment_name = "my-batch-env"
type = "MANAGED"
compute_resources {
type = "FARGATE"
max_vcpus = 16
}
}
# --- S3 Bucket Region Rename ---
# v6 renames 'region' to 'bucket_region'.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_s3_bucket
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
region = "us-east-1"
}
# --- OpsWorks Removal ---
# v6 removes all aws_opsworks_* resources (service discontinued).
# The remove_resource migration removes the block and adds FIXME comments
# to any files that reference it.
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#removal-of-aws_opsworks_-resources
resource "aws_opsworks_stack" "legacy" {
name = "my-legacy-stack"
region = "us-east-1"
service_role_arn = aws_iam_role.opsworks.arn
default_instance_profile_arn = aws_iam_instance_profile.opsworks.arn
}
resource "aws_iam_role" "opsworks" {
name = "opsworks-role"
assume_role_policy = "{}"
}
resource "aws_iam_instance_profile" "opsworks" {
name = "opsworks-profile"
role = aws_iam_role.opsworks.name
}
# This output references the opsworks stack and will get a FIXME comment
output "stack_id" {
value = aws_opsworks_stack.legacy.id
}
# --- Launch Template GPU Removal ---
# v6 removes elastic_gpu_specifications (service discontinued).
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_launch_template
resource "aws_launch_template" "gpu" {
name = "gpu-template"
instance_type = "p3.2xlarge"
image_id = "ami-gpu-123"
elastic_gpu_specifications {
type = "eg1.medium"
}
}
variable "ami_id" {
type = string
default = "ami-0c55b159cbfafe1f0"
}
variable "instance_type" {
type = string
default = "c5.xlarge"
}
variable "cpu_cores" {
type = number
default = 2
}
variable "env" {
type = string
default = "dev"
}

@ -0,0 +1,12 @@
{
"name": "v5to6/move_instance_cpu_core_count",
"description": "Move cpu_core_count into cpu_options block. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_instance",
"match": {
"block_type": "resource",
"label": "aws_instance"
},
"actions": [
{"action": "move_attribute_to_block", "name": "cpu_core_count", "block_name": "cpu_options", "to": "core_count"},
{"action": "move_attribute_to_block", "name": "cpu_threads_per_core", "block_name": "cpu_options", "to": "threads_per_core"}
]
}

@ -0,0 +1,11 @@
{
"name": "v5to6/remove_launch_template_gpu",
"description": "Remove discontinued elastic_gpu_specifications and elastic_inference_accelerator from aws_launch_template. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_launch_template",
"match": {
"block_type": "resource",
"label": "aws_launch_template"
},
"actions": [
{"action": "add_comment", "text": "FIXME: elastic_gpu_specifications has been removed in v6 (service discontinued). Remove the block manually."}
]
}

@ -0,0 +1,11 @@
{
"name": "v5to6/remove_opsworks_stack",
"description": "Remove aws_opsworks_stack (service discontinued). See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#removal-of-aws_opsworks_-resources",
"match": {
"block_type": "resource",
"label": "aws_opsworks_stack"
},
"actions": [
{"action": "remove_resource", "text": "FIXME: aws_opsworks_stack has been removed in v6 (OpsWorks discontinued). Migrate to ECS, EKS, or another deployment service."}
]
}

@ -0,0 +1,11 @@
{
"name": "v5to6/rename_batch_compute_env",
"description": "Rename compute_environment_name to name. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_batch_compute_environment",
"match": {
"block_type": "resource",
"label": "aws_batch_compute_environment"
},
"actions": [
{"action": "rename_attribute", "from": "compute_environment_name", "to": "name"}
]
}

@ -0,0 +1,11 @@
{
"name": "v5to6/rename_s3_bucket_region",
"description": "Rename region to bucket_region on aws_s3_bucket. See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown#resource-aws_s3_bucket",
"match": {
"block_type": "resource",
"label": "aws_s3_bucket"
},
"actions": [
{"action": "rename_attribute", "from": "region", "to": "bucket_region"}
]
}

@ -0,0 +1,197 @@
# Patterns that CANNOT be automated with the current migration system.
# Each section describes what would be needed to support it.
# ============================================================================
# LIMITATION 1: S3 Lifecycle Rule Extraction with Structural Rewriting
# ============================================================================
# The v3v4 S3 lifecycle_rule extraction is the most complex migration.
# The nested block structure changes significantly: attribute names change,
# values change type (boolstring), and sub-blocks are restructured.
#
# What would be needed: a "transform" action that can rewrite nested block
# structure and map values (e.g., enabled=true status="Enabled").
#
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-4-upgrade.html.markdown#s3-bucket-refactor
resource "aws_s3_bucket" "with_lifecycle" {
bucket = "my-bucket"
lifecycle_rule {
id = "expire-old"
enabled = true # v4 changes to: status = "Enabled"
prefix = "logs/" # v4 changes to: filter { prefix = "logs/" }
expiration {
days = 90
}
noncurrent_version_expiration {
days = 30 # v4 renames to: noncurrent_days = 30
}
transition {
days = 30
storage_class = "STANDARD_IA"
}
}
}
# ============================================================================
# LIMITATION 2: Dynamic Blocks with for_each
# ============================================================================
# When resources use dynamic blocks, the iterator variable creates indirect
# references that can't be tracked by simple token-level prefix matching.
# The migration system doesn't understand that `each.value.ami` refers to
# the same thing as `var.instances["web"].ami`.
#
# What would be needed: expression-level analysis that understands for_each
# iterator bindings and can rename attributes inside dynamic block content.
resource "aws_instance" "dynamic_fleet" {
for_each = var.instances
ami = each.value.ami # If "ami" is renamed to "image_id",
instance_type = each.value.instance_type # this can't be auto-updated because
# the rename_attribute action only
# touches the block's own attributes,
# not the map values feeding for_each.
}
variable "instances" {
type = map(object({
ami = string # This key name won't be updated
instance_type = string
}))
default = {
web = {
ami = "ami-web123"
instance_type = "t3.micro"
}
}
}
# ============================================================================
# LIMITATION 3: Conditional Resource Creation with count
# ============================================================================
# When a removed resource is conditionally created with count, the migration
# system removes it but can't update count-dependent references like
# aws_db_security_group.legacy[0].name. The reference rewriting works at the
# resource type level but doesn't handle indexed references specially.
#
# What would be needed: reference-aware removal that can find and comment
# out indexed references (resource.name[0].attr, resource.name[*].attr).
resource "aws_db_security_group" "legacy" {
count = var.use_classic ? 1 : 0
name = "legacy-sg"
}
resource "aws_db_instance" "uses_classic" {
identifier = "my-db"
engine = "mysql"
instance_class = "db.t3.micro"
# This indexed reference won't be caught by remove_resource's
# ReferencesPrefix check because it looks for "aws_db_security_group"
# as a traversal root, but this expression uses a complex index operation.
db_security_groups = var.use_classic ? [aws_db_security_group.legacy[0].name] : []
}
variable "use_classic" {
type = bool
default = false
}
# ============================================================================
# LIMITATION 4: Provider Configuration Changes
# ============================================================================
# v5 renames several provider-level attributes. While rename_attribute works
# on provider blocks, authentication precedence changes and new required
# fields can't be expressed as simple migrations.
#
# What would be needed: provider blocks matched via {"block_type": "provider",
# "label": "aws"} work with rename_attribute. But behavioral changes like
# auth credential precedence reordering are not expressible.
#
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-5-upgrade.html.markdown#changes-to-authentication
provider "aws" {
region = "us-east-1"
# v5 renames these (automatable):
# shared_credentials_file shared_credentials_files
# s3_force_path_style s3_use_path_style
shared_credentials_file = "~/.aws/credentials"
s3_force_path_style = true
# v5 changes auth credential resolution order (NOT automatable):
# Previously: env vars > shared credentials > IAM role
# Now: env vars > shared credentials > SSO > IAM role
# If your config relied on the old precedence, no migration can fix this.
}
# ============================================================================
# LIMITATION 5: Value Type Changes Requiring Expression Rewriting
# ============================================================================
# Some v6 changes convert between types in ways that require understanding
# the full expression, not just pattern-matching a literal value.
#
# replace_value can handle: true "Enabled", "" null, 0 true
# replace_value CANNOT handle: expressions, variables, or interpolations.
#
# See: https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/guides/version-6-upgrade.html.markdown
resource "aws_wafv2_web_acl" "example" {
name = "my-acl"
scope = "REGIONAL"
# replace_value can fix: enable_machine_learning = true false
# But this conditional can't be pattern-matched:
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "my-metric"
}
}
# ============================================================================
# LIMITATION 6: Cross-Resource Wiring After Extraction
# ============================================================================
# When extract_to_resource pulls a nested block into a new resource, it creates
# a basic wiring attribute (e.g., bucket = aws_s3_bucket.X.id). But if other
# resources reference attributes of the *extracted* nested block through the
# parent, those references break silently.
#
# Example: if another resource references aws_s3_bucket.main.versioning[0].enabled,
# that reference won't exist after extraction. The migration system doesn't
# rewrite these cross-resource nested attribute references.
#
# What would be needed: deep reference analysis that tracks nested block
# attribute paths and rewrites them to point at the new standalone resource.
resource "aws_s3_bucket" "main" {
bucket = "my-bucket"
versioning {
enabled = true
}
}
# After extraction, this reference path breaks because
# aws_s3_bucket.main.versioning no longer exists.
output "versioning_status" {
value = aws_s3_bucket.main.versioning[0].enabled
}
# ============================================================================
# LIMITATION 7: Import Commands Required After Extraction
# ============================================================================
# After extract_to_resource creates new standalone resources, Terraform will
# see them as new resources to create. The user must run terraform import
# for each extracted resource to adopt existing infrastructure.
#
# This is a fundamental limitation: HCL migration only handles the config
# files, not the state. Users need to run:
# terraform import aws_s3_bucket_versioning.main <bucket-name>
#
# What would be needed: integration with terraform state commands, or
# generating a shell script of import commands alongside the migration.

@ -0,0 +1,236 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package migrate
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform/internal/ast"
)
// TestExamples_AWS_v3to4 verifies the v3→v4 example migrations run against the sample.
func TestExamples_AWS_v3to4(t *testing.T) {
examplesDir := filepath.Join("examples", "aws-v3-to-v4")
migrations, err := DiscoverMigrations(examplesDir)
if err != nil {
t.Fatal(err)
}
migrations = FilterMigrations(migrations, "v3to4/*")
if len(migrations) == 0 {
t.Fatal("no v3to4 migrations found")
}
src, err := os.ReadFile(filepath.Join(examplesDir, "sample.tf"))
if err != nil {
t.Fatal(err)
}
f, err := ast.ParseFile(src, "sample.tf", nil)
if err != nil {
t.Fatal(err)
}
mod := ast.NewModule([]*ast.File{f}, "", true, nil)
for _, m := range migrations {
if err := Execute(m, mod); err != nil {
t.Fatalf("migration %s failed: %v", m.Name, err)
}
}
got := string(mod.Bytes()["sample.tf"])
// resource "aws_s3_bucket_object" should be renamed to "aws_s3_object"
if strings.Contains(got, `resource "aws_s3_bucket_object"`) {
t.Error("expected resource aws_s3_bucket_object renamed to aws_s3_object")
}
if !strings.Contains(got, `resource "aws_s3_object" "config"`) {
t.Error("expected aws_s3_object resource")
}
// data "aws_s3_bucket_objects" should be renamed to "aws_s3_objects"
if strings.Contains(got, `data "aws_s3_bucket_objects"`) {
t.Error("expected data aws_s3_bucket_objects renamed to aws_s3_objects")
}
if !strings.Contains(got, `data "aws_s3_objects"`) {
t.Error("expected data aws_s3_objects resource")
}
// Resource references should be rewritten
if !strings.Contains(got, "aws_s3_object.config.id") {
t.Error("expected references rewritten to aws_s3_object")
}
// NOTE: data source references (data.aws_s3_bucket_objects...) are NOT
// rewritten because the traversal root is "data", not the resource type.
// This is a known limitation.
// Versioning should be extracted to new resource
if !strings.Contains(got, `resource "aws_s3_bucket_versioning" "assets"`) {
t.Error("expected aws_s3_bucket_versioning resource extracted")
}
// acl should be removed with FIXME comment
if strings.Contains(got, `acl = "private"`) {
t.Error("expected acl attribute removed")
}
if !strings.Contains(got, "FIXME") {
t.Error("expected FIXME comment for acl removal")
}
}
// TestExamples_AWS_v4to5 verifies the v4→v5 example migrations run against the sample.
func TestExamples_AWS_v4to5(t *testing.T) {
examplesDir := filepath.Join("examples", "aws-v4-to-v5")
migrations, err := DiscoverMigrations(examplesDir)
if err != nil {
t.Fatal(err)
}
migrations = FilterMigrations(migrations, "v4to5/*")
if len(migrations) == 0 {
// Print all found migrations for debugging
allMigs, _ := DiscoverMigrations(examplesDir)
t.Fatalf("no v4to5 migrations found after filter. All discovered: %d", len(allMigs))
}
src, err := os.ReadFile(filepath.Join(examplesDir, "sample.tf"))
if err != nil {
t.Fatal(err)
}
f, err := ast.ParseFile(src, "sample.tf", nil)
if err != nil {
t.Fatal(err)
}
mod := ast.NewModule([]*ast.File{f}, "", true, nil)
for _, m := range migrations {
if err := Execute(m, mod); err != nil {
t.Fatalf("migration %s failed: %v", m.Name, err)
}
}
got := string(mod.Bytes()["sample.tf"])
// EC2-Classic attributes should be removed
if strings.Contains(got, "vpc_classic_link_id") {
t.Error("expected vpc_classic_link_id removed")
}
// vpc_security_group_ids should survive
if !strings.Contains(got, "vpc_security_group_ids") {
t.Error("expected vpc_security_group_ids preserved")
}
// Autoscaling attachment renamed (check attribute assignment, not comments)
if strings.Contains(got, "alb_target_group_arn =") {
t.Error("expected alb_target_group_arn renamed to lb_target_group_arn")
}
if !strings.Contains(got, "lb_target_group_arn") {
t.Error("expected lb_target_group_arn present")
}
// Elasticache attributes renamed
if strings.Contains(got, "replication_group_description") {
t.Error("expected replication_group_description renamed to description")
}
if strings.Contains(got, "number_cache_clusters") {
t.Error("expected number_cache_clusters renamed")
}
// cluster_mode block should be flattened
if strings.Contains(got, "cluster_mode {") {
t.Error("expected cluster_mode block flattened")
}
if !strings.Contains(got, "num_node_groups") {
t.Error("expected num_node_groups at top level")
}
// DB instance name renamed
if strings.Contains(got, `name = "myappdb"`) {
t.Error("expected 'name' renamed to 'db_name'")
}
// aws_db_security_group should be removed
if strings.Contains(got, `resource "aws_db_security_group"`) {
t.Error("expected aws_db_security_group removed")
}
if !strings.Contains(got, "FIXME: aws_db_security_group") {
t.Error("expected FIXME comment for removed resource")
}
// RDS cluster without engine should get one added
// (the first cluster has no engine, the second already has engine="aurora-mysql")
if !strings.Contains(got, "aurora-mysql") {
t.Error("expected existing aurora-mysql engine preserved")
}
}
// TestExamples_AWS_v5to6 verifies the v5→v6 example migrations run against the sample.
func TestExamples_AWS_v5to6(t *testing.T) {
examplesDir := filepath.Join("examples", "aws-v5-to-v6")
migrations, err := DiscoverMigrations(examplesDir)
if err != nil {
t.Fatal(err)
}
migrations = FilterMigrations(migrations, "v5to6/*")
if len(migrations) == 0 {
t.Fatal("no v5to6 migrations found")
}
src, err := os.ReadFile(filepath.Join(examplesDir, "sample.tf"))
if err != nil {
t.Fatal(err)
}
f, err := ast.ParseFile(src, "sample.tf", nil)
if err != nil {
t.Fatal(err)
}
mod := ast.NewModule([]*ast.File{f}, "", true, nil)
for _, m := range migrations {
if err := Execute(m, mod); err != nil {
t.Fatalf("migration %s failed: %v", m.Name, err)
}
}
got := string(mod.Bytes()["sample.tf"])
// cpu_core_count and cpu_threads_per_core should be moved into cpu_options
// (check attribute assignments, not comments)
if strings.Contains(got, "cpu_core_count =") {
t.Error("expected cpu_core_count moved to cpu_options block")
}
if strings.Contains(got, "cpu_threads_per_core =") {
t.Error("expected cpu_threads_per_core moved to cpu_options block")
}
if !strings.Contains(got, "cpu_options") {
t.Error("expected cpu_options block created")
}
// Batch compute environment rename (check attr assignment, not comments)
if strings.Contains(got, "compute_environment_name =") {
t.Error("expected compute_environment_name renamed to name")
}
// OpsWorks should be removed (check resource block, not comments)
if strings.Contains(got, `resource "aws_opsworks_stack"`) {
t.Error("expected aws_opsworks_stack removed")
}
if !strings.Contains(got, "FIXME: aws_opsworks_stack") {
t.Error("expected FIXME comment for opsworks removal")
}
// S3 bucket region renamed (check attr assignment, not comments)
if strings.Contains(got, "region = ") && strings.Contains(got, `"my-data-bucket"`) {
// The S3 bucket should have bucket_region, not region
if !strings.Contains(got, "bucket_region") {
t.Error("expected region renamed to bucket_region on aws_s3_bucket")
}
}
// elastic_gpu_specifications can't be auto-removed (it's a block, not attribute),
// so the migration adds a FIXME comment instead
if !strings.Contains(got, "FIXME: elastic_gpu_specifications") {
t.Error("expected FIXME comment for elastic_gpu_specifications")
}
}
Loading…
Cancel
Save