From 874d047f74bd0e2daa49ca88a7d7a7fb783a6ab2 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Wed, 18 Mar 2026 16:10:47 +0100 Subject: [PATCH] =?UTF-8?q?migrate:=20add=20real-world=20AWS=20v3=E2=86=92?= =?UTF-8?q?v6=20migration=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrate/examples/aws-v3-to-v4/sample.tf | 60 +++++ .../aws-v3-to-v4/v3to4/extract_s3_acl.json | 21 ++ .../extract_s3_server_side_encryption.json | 17 ++ .../v3to4/extract_s3_versioning.json | 17 ++ .../v3to4/rename_s3_bucket_object.json | 14 ++ .../v3to4/rename_s3_bucket_objects_data.json | 14 ++ .../migrate/examples/aws-v4-to-v5/sample.tf | 116 +++++++++ .../flatten_elasticache_cluster_mode.json | 11 + .../v4to5/remove_db_security_group.json | 11 + .../v4to5/remove_ec2_classic.json | 13 + .../v4to5/rename_autoscaling_attachment.json | 11 + .../v4to5/rename_db_instance_name.json | 11 + .../v4to5/rename_elasticache_attrs.json | 13 + .../v4to5/rename_opensearch_kibana.json | 11 + .../v4to5/require_rds_cluster_engine.json | 11 + .../migrate/examples/aws-v5-to-v6/sample.tf | 108 ++++++++ .../v5to6/move_instance_cpu_options.json | 12 + .../v5to6/remove_launch_template_gpu.json | 11 + .../aws-v5-to-v6/v5to6/remove_opsworks.json | 11 + .../v5to6/rename_batch_compute_env.json | 11 + .../v5to6/rename_s3_bucket_region.json | 11 + .../limitations/unsupported_patterns.tf | 197 +++++++++++++++ internal/migrate/examples_test.go | 236 ++++++++++++++++++ 23 files changed, 948 insertions(+) create mode 100644 internal/migrate/examples/aws-v3-to-v4/sample.tf create mode 100644 internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_acl.json create mode 100644 internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_server_side_encryption.json create mode 100644 internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_versioning.json create mode 100644 internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_object.json create mode 100644 internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_objects_data.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/sample.tf create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/flatten_elasticache_cluster_mode.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/remove_db_security_group.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/remove_ec2_classic.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/rename_autoscaling_attachment.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/rename_db_instance_name.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/rename_elasticache_attrs.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/rename_opensearch_kibana.json create mode 100644 internal/migrate/examples/aws-v4-to-v5/v4to5/require_rds_cluster_engine.json create mode 100644 internal/migrate/examples/aws-v5-to-v6/sample.tf create mode 100644 internal/migrate/examples/aws-v5-to-v6/v5to6/move_instance_cpu_options.json create mode 100644 internal/migrate/examples/aws-v5-to-v6/v5to6/remove_launch_template_gpu.json create mode 100644 internal/migrate/examples/aws-v5-to-v6/v5to6/remove_opsworks.json create mode 100644 internal/migrate/examples/aws-v5-to-v6/v5to6/rename_batch_compute_env.json create mode 100644 internal/migrate/examples/aws-v5-to-v6/v5to6/rename_s3_bucket_region.json create mode 100644 internal/migrate/examples/limitations/unsupported_patterns.tf create mode 100644 internal/migrate/examples_test.go diff --git a/internal/migrate/examples/aws-v3-to-v4/sample.tf b/internal/migrate/examples/aws-v3-to-v4/sample.tf new file mode 100644 index 0000000000..c93aa42c2d --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/sample.tf @@ -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 +} diff --git a/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_acl.json b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_acl.json new file mode 100644 index 0000000000..5fa092d19a --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_acl.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_server_side_encryption.json b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_server_side_encryption.json new file mode 100644 index 0000000000..5238083e41 --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_server_side_encryption.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_versioning.json b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_versioning.json new file mode 100644 index 0000000000..20b2b9eed0 --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/v3to4/extract_s3_versioning.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_object.json b/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_object.json new file mode 100644 index 0000000000..bf4bdc2d83 --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_object.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_objects_data.json b/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_objects_data.json new file mode 100644 index 0000000000..b450af8aa9 --- /dev/null +++ b/internal/migrate/examples/aws-v3-to-v4/v3to4/rename_s3_bucket_objects_data.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/internal/migrate/examples/aws-v4-to-v5/sample.tf b/internal/migrate/examples/aws-v4-to-v5/sample.tf new file mode 100644 index 0000000000..1d04f03903 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/sample.tf @@ -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 +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/flatten_elasticache_cluster_mode.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/flatten_elasticache_cluster_mode.json new file mode 100644 index 0000000000..e6ab9c2f73 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/flatten_elasticache_cluster_mode.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_db_security_group.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_db_security_group.json new file mode 100644 index 0000000000..dbbfb67245 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_db_security_group.json @@ -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."} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_ec2_classic.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_ec2_classic.json new file mode 100644 index 0000000000..6a93aaca7d --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/remove_ec2_classic.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_autoscaling_attachment.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_autoscaling_attachment.json new file mode 100644 index 0000000000..953aeb1eb8 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_autoscaling_attachment.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_db_instance_name.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_db_instance_name.json new file mode 100644 index 0000000000..b40547a723 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_db_instance_name.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_elasticache_attrs.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_elasticache_attrs.json new file mode 100644 index 0000000000..c4b1bbab33 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_elasticache_attrs.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_opensearch_kibana.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_opensearch_kibana.json new file mode 100644 index 0000000000..dbb29f0420 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/rename_opensearch_kibana.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v4-to-v5/v4to5/require_rds_cluster_engine.json b/internal/migrate/examples/aws-v4-to-v5/v4to5/require_rds_cluster_engine.json new file mode 100644 index 0000000000..3cf4f40a58 --- /dev/null +++ b/internal/migrate/examples/aws-v4-to-v5/v4to5/require_rds_cluster_engine.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v5-to-v6/sample.tf b/internal/migrate/examples/aws-v5-to-v6/sample.tf new file mode 100644 index 0000000000..61b58ca4b5 --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/sample.tf @@ -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" +} diff --git a/internal/migrate/examples/aws-v5-to-v6/v5to6/move_instance_cpu_options.json b/internal/migrate/examples/aws-v5-to-v6/v5to6/move_instance_cpu_options.json new file mode 100644 index 0000000000..4f0db45792 --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/v5to6/move_instance_cpu_options.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_launch_template_gpu.json b/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_launch_template_gpu.json new file mode 100644 index 0000000000..9532115e60 --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_launch_template_gpu.json @@ -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."} + ] +} diff --git a/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_opsworks.json b/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_opsworks.json new file mode 100644 index 0000000000..2ed55c79dc --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/v5to6/remove_opsworks.json @@ -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."} + ] +} diff --git a/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_batch_compute_env.json b/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_batch_compute_env.json new file mode 100644 index 0000000000..c80eb8cc8f --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_batch_compute_env.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_s3_bucket_region.json b/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_s3_bucket_region.json new file mode 100644 index 0000000000..d86d78c958 --- /dev/null +++ b/internal/migrate/examples/aws-v5-to-v6/v5to6/rename_s3_bucket_region.json @@ -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"} + ] +} diff --git a/internal/migrate/examples/limitations/unsupported_patterns.tf b/internal/migrate/examples/limitations/unsupported_patterns.tf new file mode 100644 index 0000000000..dd41fd64f3 --- /dev/null +++ b/internal/migrate/examples/limitations/unsupported_patterns.tf @@ -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 v3→v4 S3 lifecycle_rule extraction is the most complex migration. +# The nested block structure changes significantly: attribute names change, +# values change type (bool→string), 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 +# +# What would be needed: integration with terraform state commands, or +# generating a shell script of import commands alongside the migration. diff --git a/internal/migrate/examples_test.go b/internal/migrate/examples_test.go new file mode 100644 index 0000000000..4b0ca64ad7 --- /dev/null +++ b/internal/migrate/examples_test.go @@ -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") + } +}