This tutorial shows how to migrate an existing AWS Terraform-managed infrastructure that uses remote backend storage (e.g. S3) for the state to OpenTofu.
How to migrate Terraform Infrastructure to OpenTofu
Install Terraform and OpenTofu
Use your existing Terraform infrastructure definition or create a new one to test the migration procedure.
Initialize OpenTofu providers and local state.
Make Infrastructure changes with OpenTofu
Make changes to the infrastructure using OpenTofu.
Migrate back to Terraform (Optional)
Migrate back from OpenTofu to Terraform to test backward compatibility.
Terraform and OpenTofu (How to Install OpenTofu). An AWS S3 Bucket for state remote backend storage.
7/Dec/2023: correctly upgraded from OpenTofu v1.6.0-alpha4 to OpenTofu v1.6.0-beta2
Use your existing Terraform infrastructure definition or create a new one to test the migration procedure.
Here we will be creating a VPC and a Subnet on AWS with Terraform and once deployed we will be migrating the Terraform infrastructure definition to OpenTofu.
The example uses an AWS S3 bucket as remote backend storage for the Terraform state.
Update your AWS S3 bucket, key, profile, region, and credential files with your account data.
Resources are created with the tag Ver = "Terraform"
, it will be changed to "OpenTofu" after migration.
# Copyright (C) 2018 - 2023 IT Wonder Lab (https://www.itwonderlab.com) # # This software may be modified and distributed under the terms # of the MIT license. See the LICENSE file for details. # -------------------------------- WARNING -------------------------------- # IT Wonder Lab's best practices for infrastructure include modularizing # Terraform/OpenTofu configuration. # In this example, we define everything in a single file. # See other tutorials for best practices at itwonderlab.com # -------------------------------- WARNING -------------------------------- #Define Terrraform Providers and Backend terraform { required_version = "> 1.5" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } # Stores the state as a given key in a given bucket on Amazon S3. backend "s3" { bucket = "ditwonderlab" key = "ditwl_infradmin/aws_basic_ec2/terraform" profile = "ditwl_infradmin" region = "us-east-1" shared_credentials_files = ["~/.aws/credentials"] # See change 690. Before shared_credentials_file = "~/.aws/credentials" } } #----------------------------------------- # Default provider: AWS #----------------------------------------- provider "aws" { shared_credentials_files = ["~/.aws/credentials"] profile = "ditwl_infradmin" region = "us-east-1" //See BUG https://github.com/hashicorp/terraform-provider-aws/issues/30488 } # VPC resource "aws_vpc" "ditlw-vpc" { cidr_block = "172.21.0.0/19" #172.21.0.0 - 172.21.31.254 tags = { Name = "ditlw-vpc" Ver = "Terraform" } } # Subnet resource "aws_subnet" "ditwl-sn-za-pro-pub-00" { vpc_id = aws_vpc.ditlw-vpc.id cidr_block = "172.21.0.0/23" #172.21.0.0 - 172.21.1.255 tags = { Name = "ditwl-sn-za-pro-pub-00" Ver = "Terraform" } }
Create the demo infrastructure.
Initialize the S3 backend and download the Terraform providers:
$ terraform init Initializing the backend... Successfully configured the backend "s3"! Terraform will automatically use this backend unless the backend configuration changes. Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 5.0"... - Installing hashicorp/aws v5.24.0... - Installed hashicorp/aws v5.24.0 (signed by HashiCorp) Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. Terraform has been successfully initialized! ...
Plan & Apply
Plan and apply the changes to the AWS infrastructure (only apply is shown):
$ terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_subnet.ditwl-sn-za-pro-pub-00 will be created + resource "aws_subnet" "ditwl-sn-za-pro-pub-00" { + arn = (known after apply) + assign_ipv6_address_on_creation = false + availability_zone = (known after apply) + availability_zone_id = (known after apply) + cidr_block = "172.21.0.0/23" + enable_dns64 = false + enable_resource_name_dns_a_record_on_launch = false + enable_resource_name_dns_aaaa_record_on_launch = false + id = (known after apply) + ipv6_cidr_block_association_id = (known after apply) + ipv6_native = false + map_public_ip_on_launch = false + owner_id = (known after apply) + private_dns_hostname_type_on_launch = (known after apply) + tags = { + "Name" = "ditwl-sn-za-pro-pub-00" + "Ver" = "Terraform" } + tags_all = { + "Name" = "ditwl-sn-za-pro-pub-00" + "Ver" = "Terraform" } + vpc_id = (known after apply) } # aws_vpc.ditlw-vpc will be created + resource "aws_vpc" "ditlw-vpc" { + arn = (known after apply) + cidr_block = "172.21.0.0/19" + default_network_acl_id = (known after apply) + default_route_table_id = (known after apply) + default_security_group_id = (known after apply) + dhcp_options_id = (known after apply) + enable_dns_hostnames = (known after apply) + enable_dns_support = true + enable_network_address_usage_metrics = (known after apply) + id = (known after apply) + instance_tenancy = "default" + ipv6_association_id = (known after apply) + ipv6_cidr_block = (known after apply) + ipv6_cidr_block_network_border_group = (known after apply) + main_route_table_id = (known after apply) + owner_id = (known after apply) + tags = { + "Name" = "ditlw-vpc" + "Ver" = "Terraform" } + tags_all = { + "Name" = "ditlw-vpc" + "Ver" = "Terraform" } } Plan: 2 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_vpc.ditlw-vpc: Creating... aws_vpc.ditlw-vpc: Creation complete after 5s [id=vpc-0a1bd319d6f8efa64] aws_subnet.ditwl-sn-za-pro-pub-00: Creating... aws_subnet.ditwl-sn-za-pro-pub-00: Creation complete after 2s [id=subnet-08fc86ea7f14ba5e4] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Check that the state has been stored in S3
Initialize OpenTofu to download providers and set the backend.
$ tofu init Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 5.0"... - Reusing previous version of registry.terraform.io/hashicorp/aws from the dependency lock file - Installing hashicorp/aws v5.24.0... - Installed hashicorp/aws v5.24.0 (signed, key ID 0DC64ED093B3E9FF) - Using previously-installed registry.terraform.io/hashicorp/aws v5.24.0 Providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://opentofu.org/docs/cli/plugins/signing/ OpenTofu has made some changes to the provider dependency selections recorded in the .terraform.lock.hcl file. Review those changes and commit them to your version control system if they represent changes you intended to make. OpenTofu has been successfully initialized! ...
We can now use OpenTofu to manage the infrastructure.
Make some changes
Edit the Terraform definition file to replace tag Ver = "Terraform"
with Ver = "OpenTofu"
and apply the change using OpenTofu. You might add or change other infrastructure resources as well (we are adding another subnet ditwl-sn-za-pro-pri-02
).
.... # VPC resource "aws_vpc" "ditlw-vpc" { cidr_block = "172.21.0.0/19" #172.21.0.0 - 172.21.31.254 tags = { Name = "ditlw-vpc" Ver = "OpenTofu" } } # Subnet resource "aws_subnet" "ditwl-sn-za-pro-pub-00" { vpc_id = aws_vpc.ditlw-vpc.id cidr_block = "172.21.0.0/23" #172.21.0.0 - 172.21.1.255 tags = { Name = "ditwl-sn-za-pro-pub-00" Ver = "OpenTofu" } } # Subnet resource "aws_subnet" "ditwl-sn-za-pro-pri-02" { vpc_id = aws_vpc.ditlw-vpc.id cidr_block = "172.21.2.0/23" #172.21.2.0 - 172.21.3.255 tags = { Name = "ditwl-sn-za-pro-pri-02" Ver = "OpenTofu" } }
Plan and Apply the changes with OpenTofu
Apply changes:
$ tofu apply aws_vpc.ditlw-vpc: Refreshing state... [id=vpc-0a1bd319d6f8efa64] aws_subnet.ditwl-sn-za-pro-pub-00: Refreshing state... [id=subnet-08fc86ea7f14ba5e4] OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create ~ update in-place OpenTofu will perform the following actions: # aws_subnet.ditwl-sn-za-pro-pri-02 will be created + resource "aws_subnet" "ditwl-sn-za-pro-pri-02" { + arn = (known after apply) + assign_ipv6_address_on_creation = false + availability_zone = (known after apply) + availability_zone_id = (known after apply) + cidr_block = "172.21.2.0/23" + enable_dns64 = false + enable_resource_name_dns_a_record_on_launch = false + enable_resource_name_dns_aaaa_record_on_launch = false + id = (known after apply) + ipv6_cidr_block_association_id = (known after apply) + ipv6_native = false + map_public_ip_on_launch = false + owner_id = (known after apply) + private_dns_hostname_type_on_launch = (known after apply) + tags = { + "Name" = "ditwl-sn-za-pro-pri-02" + "Ver" = "OpenTofu" } + tags_all = { + "Name" = "ditwl-sn-za-pro-pri-02" + "Ver" = "OpenTofu" } + vpc_id = "vpc-0a1bd319d6f8efa64" } # aws_subnet.ditwl-sn-za-pro-pub-00 will be updated in-place ~ resource "aws_subnet" "ditwl-sn-za-pro-pub-00" { id = "subnet-08fc86ea7f14ba5e4" ~ tags = { "Name" = "ditwl-sn-za-pro-pub-00" ~ "Ver" = "Terraform" -> "OpenTofu" } ~ tags_all = { ~ "Ver" = "Terraform" -> "OpenTofu" # (1 unchanged element hidden) } # (15 unchanged attributes hidden) } # aws_vpc.ditlw-vpc will be updated in-place ~ resource "aws_vpc" "ditlw-vpc" { id = "vpc-0a1bd319d6f8efa64" ~ tags = { "Name" = "ditlw-vpc" ~ "Ver" = "Terraform" -> "OpenTofu" } ~ tags_all = { ~ "Ver" = "Terraform" -> "OpenTofu" # (1 unchanged element hidden) } # (14 unchanged attributes hidden) } Plan: 1 to add, 2 to change, 0 to destroy. Do you want to perform these actions? OpenTofu will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_vpc.ditlw-vpc: Modifying... [id=vpc-0a1bd319d6f8efa64] aws_vpc.ditlw-vpc: Modifications complete after 4s [id=vpc-0a1bd319d6f8efa64] aws_subnet.ditwl-sn-za-pro-pri-02: Creating... aws_subnet.ditwl-sn-za-pro-pub-00: Modifying... [id=subnet-08fc86ea7f14ba5e4] aws_subnet.ditwl-sn-za-pro-pub-00: Modifications complete after 2s [id=subnet-08fc86ea7f14ba5e4] aws_subnet.ditwl-sn-za-pro-pri-02: Creation complete after 2s [id=subnet-053b224aad9c0ee52] Apply complete! Resources: 1 added, 2 changed, 0 destroyed.
The changes on Ver labels and the creation of a new subnet have been done with OpenTofu.
If you want to go back to Terraform, just run terraform init
.
$ terraform init Initializing the backend... Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file - Reusing previous version of registry.opentofu.org/hashicorp/aws from the dependency lock file - Using previously-installed hashicorp/aws v5.24.0 - Using previously-installed registry.opentofu.org/hashicorp/aws v5.24.0 Terraform has been successfully initialized! ...
IT Wonder Lab tutorials are based on the diverse experience of Javier Ruiz, who founded and bootstrapped a SaaS company in the energy sector. His company, later acquired by a NASDAQ traded company, managed over €2 billion per year of electricity for prominent energy producers across Europe and America. Javier has over 25 years of experience in building and managing IT companies, developing cloud infrastructure, leading cross-functional teams, and transitioning his own company from on-premises, consulting, and custom software development to a successful SaaS model that scaled globally.