Terraform

Importing Existing Infrastructure Into Terraform – Step by Step

Importing existing infrastructure into terraform

In this tutorial, you will learn ways to import pre-existing cloud resources before you continue to develop the IaC in Terraform. This guide will provide you with an IaC import scenario, which is often faced by teams starting to adopt Terraform for their operations.

What we will cover:

  1. What is Terraform import command?
  2. Terraform import use cases
  3. Importing resources
  4. Importing modules
  5. Importing IAM roles using for_each
  6. Terraform 1.5 import – How to use the import block
  7. Approach towards importing multiple resources
  8. Best practices for Terraform import

What is Terraform import command?

Getting the pre-existing cloud resources under the Terraform management is facilitated by Terraform import. import is a Terraform CLI command which is used to read real-world infrastructure and update the state, so that future updates to the same set of infrastructure can be applied via IaC.

The import functionality helps update the state locally and it does not create the corresponding configuration automatically. However, the Terraform team is working hard to improve this function in upcoming releases.

Terraform import use cases

Terraform is a relatively new technology, and adopting it to manage an organization’s cloud resources might take some time and effort. The lack of human resources and the steep learning curve involved in using Terraform effectively causes teams to start using cloud infrastructure directly via their respective web consoles.

For that matter, any kind of IaC method (CloudFormation, Azure ARM templates, Pulumi, etc.) requires some training and real-time scenario handling experience. Things get especially complicated when dealing with concepts like states and remote backends. In a worst-case scenario, you can lose the terraform.tfstate file. Luckily, you can use the import functionality to rebuild it.

Terraform import is used to add existing infrastructure components into Terraform management. Here are a couple of use cases in which terraform import can be a lifesaver:

  • Bringing unmanaged resources under Terraform management – your company may have started its infrastructure adventure by doing ClickOps or by using some custom scripts. Now, to bring everything under Terraform, terraform import will be the solution.
  • Migrating between Terraform states terraform import can help a lot if you want to split up your state file into multiple files, as managing a lot of things under a single file can be cumbersome.
  • Disaster recovery – if your state file gets corrupted or you don’t have access to it anymore for whatever reason, you can use terraform import to rebuild it.
  • Adopting Terraform in phases – with terraform import you can start by importing a couple of resources, helping you start small with Terraform.

Importing resources

With an understanding of why we need to import cloud resources, let us begin by importing a simple resource – an EC2 instance in AWS. I am assuming the Terraform installation and configuration of AWS credentials in AWS CLI is already done locally. We will not go into the details of that in this tutorial. To import a simple resource into Terraform, follow the step-by-step guide below. 

1. Prepare the EC2 instance

Assuming the Terraform installation and configuration of AWS credentials in AWS CLI is already done locally, begin by importing a simple resource—EC2 instance in AWS. For the sake of this tutorial, we will create an EC2 resource manually to be imported. This could be an optional step if you already have a target resource to be imported.

Terraform: Create EC2 instance in the existing VPC

Go ahead and provision an EC2 instance in your AWS account. Here are the example details of the EC2 instance thus created:

Name: MyVM
Instance ID: i-0b9be609418aa0609
Type: t2.micro
VPC ID: vpc-1827ff72

2. Create main.tf and set provider configuration

The aim of this step is to import this EC2 instance into our Terraform configuration. In your desired path, create `main.tf` and configure the AWS provider. The file should look like the one below.

Importing EC2 instance into Terraform configuration: Example

// Provider configuration
terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "~> 3.0"
   }
 }
}
 
provider "aws" {
 region = "eu-central-1"
}

Run terraform init to initialize the Terraform modules. Below is the output of a successful initialization.

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.0"...
- Installing hashicorp/aws v3.51.0...
- Installed hashicorp/aws v3.51.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!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

3. Write config for the resource to be imported

As discussed earlier, Terraform import does not generate the configuration files by itself. Thus, you need to create the corresponding configuration for the EC2 instance manually. This doesn’t need many arguments as we will have to add or modify them when we import the EC2 instance into our state file.

However, if you don’t mind not seeing colorful output on CLI, you can begin adding all the arguments you know. But this is not a foolproof approach because normally, the infrastructure you may have to import will not have been created by you. So, it is best to skip a few arguments anyway.

In a moment, we will take a look at how to adjust our configuration to reflect the exact resource. For now, append the main.tf file with EC2 config. For example, I have used the below config. The only reason I have included ami and instance_type attribute, is that they are the required arguments for aws_instance resource block.

resource "aws_instance" "myvm" {
 ami           = "unknown"
 instance_type = "unknown"
}

4. Run the import command

Think of it as if the cloud resource (EC2 instance) and its corresponding configuration were available in our files. All that’s left to do is to map the two into our state file. We do that by running the import command as follows.

terraform import aws_instance.myvm <Instance ID>

A successful output should look like this:

aws_instance.myvm: Importing from ID "i-0b9be609418aa0609"...
aws_instance.myvm: Import prepared!
  Prepared aws_instance for import
aws_instance.myvm: Refreshing state... [id=i-0b9be609418aa0609]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

The above command maps the aws_instance.myvm configuration to the EC2 instance using the ID. By mapping I mean that the state file now “knows” the existence of the EC2 instance with the given ID. The state file also contains information about each attribute of this EC2 instance, as it has fetched the same using the import command.

5. Observe state files and plan output

Please notice that the directory now also contains terraform.tfstate file. This file was generated after the import command was successfully run. Take a moment to go through the contents of this file.

Right now our configuration does not reflect all the attributes. Any attempt to plan/apply this configuration will fail because we have not adjusted the values of its attributes. To close the gap in configuration files and state files, run terraform plan and observe the output.

.
.
.
          } -> (known after apply)
          ~ throughput            = 0 -> (known after apply)
          ~ volume_id             = "vol-0fa93084426be508a" -> (known after apply)
          ~ volume_size           = 8 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }

      - timeouts {}
    }

Plan: 1 to add, 0 to change, 1 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply"
now.

The plan indicates that it would attempt to replace the EC2 instance. But this goes completely against our purpose. We could do it anyway by simply not caring about the existing resources, and creating new resources using configuration.

The good news is that Terraform has taken note of the existence of an EC2 instance that is associated with its state. 

6. Improve config to avoid replacement

At this point, it is important to understand that the terraform.tfstate file is a vital piece of reference for Terraform. All of its future operations are performed with consideration for this state file. You need to investigate the state file and update your configuration accordingly so that there is a minimum difference between them.

The use of the word “minimum” is intentional here. Right now, you need to focus on not replacing the given EC2 instance but rather aligning the configuration so that the replacement can be avoided. Eventually, you would achieve a state of 0 difference.

Observe the plan output and find all those attributes that cause the replacement. The plan output will highlight the same. In our example, the only attribute that causes replacement is the AMI ID. Closing this gap should avoid the replacement of the EC2 instance.

Change the value of ami from “unknown” to what is highlighted in the plan output, and run terraform plan again. Notice the output.

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_instance.myvm will be updated in-place
  ~ resource "aws_instance" "myvm" {
        id                                   = "i-0b9be609418aa0609"
      ~ instance_type                        = "t2.micro" -> "unknown"
      ~ tags                                 = {
          - "Name" = "MyVM" -> null
        }
      ~ tags_all                             = {
          - "Name" = "MyVM"
        } -> (known after apply)
        # (27 unchanged attributes hidden)






        # (6 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

This time the plan does not indicate the replacement of the EC2 instance. If you get the same output, you are successful in partially importing our cloud resource. You are currently in a state of lowered risk—if we apply the configuration now, the resource will not be replaced, but a few attributes would change.

7. Improve config to avoid changes

If we want to achieve a state of 0 difference, you need to align your resource block even more. The plan output highlights the attribute changes using ~ sign. It also indicates the difference in the values. For example, it highlights the change in the instance_type value from “t2.micro” to “unknown”.

In other words, if the value of instance_type had been “t2.micro”, Terraform would NOT have asked for a change. Similarly, you can see there are changes to the tags highlighted as well. Let’s change the configuration accordingly so that we close these gaps. The final aws_instance resource block should look as follows:

resource "aws_instance" "myvm" {
 ami           = "ami-00f22f6155d6d92c5"
 instance_type = "t2.micro"
 
 tags = {
     "Name": "MyVM"
 }
}

Run terraform plan again, and observe the output.

aws_instance.myvm: Refreshing state... [id=i-0b9be609418aa0609]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

If you have the same output, congratulations, as you have successfully imported a cloud resource into your Terraform config. It is now possible to manage this configuration via Terraform directly, without any surprises.

Importing Modules

In this section, you will learn how to import resources into the modules. For reference, we use the AWS VPC module (version 3.2.0) that creates 29 resources. Move the terraform.tfstate file to another location, so that Terraform becomes unaware of the existing resources.

This poses a challenge of importing the AWS VPC module into your configuration. Modules wrap multiple AWS resources into a single package that can be reused in various projects. You should expect a large codebase and many resources to be part of the module.

The process of importing resources created using a module is very similar to what we have discussed thus far, with a little difference in running the command. Go through the .tf files included in the module’s source, and identify the resources to be imported.

For example, the AWS VPC module creates a VPC resource. To import this resource, run the command as below.

Terraform Import AWS VPC module example

This command imports the target VPC resource in AWS to our module’s configuration.

terraform import module.vpc.aws_vpc.this <VPC ID>

Running the plan command indicates how many resources will be created. In other words, how many resources are yet to be imported.

      + "Name"        = "my-vpc"
          + "Terraform"   = "true"
        }
      + tags_all        = {
          + "Environment" = "dev"
          + "Name"        = "my-vpc"
          + "Terraform"   = "true"
        }
      + vpc_id          = "vpc-0127895db175d45ff"
    }

Plan: 28 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply"
now.

The above output indicates 28 more resources to be imported to (re)build a perfectly consistent terraform.tfstate file. One less as we have successfully imported the VPC resource. Referring to the plan output, identify the resources in AWS and repeat the process for import.

Importing IAM roles using for_each

Let’s suppose you want to manage multiple IAM roles with Terraform that already exist. In this example, I created two roles in my AWS account that I want to import using for_each.

Terraform Import Module

Now, let’s define the Terraform configuration for the roles:

provider "aws" {
 region = "eu-west-1"
}

locals {
 roles = ["import_role1", "import_role2"]
}

resource "aws_iam_role" "import_roles" {
 for_each = toset(local.roles)
 name     = each.value
 assume_role_policy = jsonencode(
   {
     Statement = [
       {
         Action = "sts:AssumeRole"
         Principal = {
           Service = "ec2.amazonaws.com"
         }
         Effect = "Allow"
       },
     ]
     Version = "2012-10-17"
 })
 description = "Allows EC2 instances to call AWS services on your behalf."
}

We have to ensure we use the same role policy we have used inside our AWS account and the same description. 

Now that we have the configuration in place let’s run a plan first to see that resources are not yet in the state:

terraform plan

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_iam_role.import_roles["import_role1"] will be created
    }

  # aws_iam_role.import_roles["import_role2"] will be created
  + resource "aws_iam_role" "import_roles" {
}

Plan: 2 to add, 0 to change, 0 to destroy.

Now we can import the resources:

terraform import "aws_iam_role.import_roles[\"import_role1\"]" import_role1
aws_iam_role.import_roles["import_role1"]: Importing from ID "import_role1"...
aws_iam_role.import_roles["import_role1"]: Import prepared!
  Prepared aws_iam_role for import
aws_iam_role.import_roles["import_role1"]: Refreshing state... [id=import_role1]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

terraform import "aws_iam_role.import_roles[\"import_role2\"]" import_role2
aws_iam_role.import_roles["import_role2"]: Importing from ID "import_role2"...
aws_iam_role.import_roles["import_role2"]: Import prepared!
  Prepared aws_iam_role for import
aws_iam_role.import_roles["import_role2"]: Refreshing state... [id=import_role2]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Ensure you escape the quotation marks when you are specifying the role key while doing the import.

Now, let’s run another plan:

terraform plan                                                             
aws_iam_role.import_roles["import_role1"]: Refreshing state... [id=import_role1]
aws_iam_role.import_roles["import_role2"]: Refreshing state... [id=import_role2]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

As you can see, the resources have been imported successfully.

Terraform 1.5 import- How to use the import block

In Terraform 1.5, a new import mechanism is available. A new top-level import block can be defined in your code to allow import operations. As this block is added in the code, import will not be a state operation, from now on, as for every other resource, it becomes a plannable operation. 

Terraform import S3 bucket example

Let’s use a concrete example to see the new import in action.

For this, I have created two s3 buckets manually, and I want to import them in my state:

  • import-bucket-tf15
  • import-bucket-tf15-2
provider "aws" {
 region = "eu-west-1"
}

import {
 # ID of the cloud resource
 # Check provider documentation for importable resources and format
 id = "import-bucket-tf15"
 # Resource address
 to = aws_s3_bucket.this
}

import {
 # ID of the cloud resource
 # Check provider documentation for importable resources and format
 id = "import-bucket-tf15-2"
 # Resource address
 to = aws_s3_bucket.this2
}

The import block, as you can see above, takes two parameters:

  • id → The id of the resource used in your cloud provider
  • to → The resource address that will be used in Terraform

Next, if you want to generate the configuration automatically, you can run the following command:

terraform plan -generate-config-out=generated_resources.tf

This will result in:

aws_s3_bucket.this: Preparing import... [id=import-bucket-tf15]
aws_s3_bucket.this2: Preparing import... [id=import-bucket-tf15-2]
aws_s3_bucket.this2: Refreshing state... [id=import-bucket-tf15-2]
aws_s3_bucket.this: Refreshing state... [id=import-bucket-tf15]

Terraform will perform the following actions:

  # aws_s3_bucket.this will be imported
  # (config will be generated)
    resource "aws_s3_bucket" "this" {
       ommited
        }
    }

  # aws_s3_bucket.this2 will be imported
  # (config will be generated)
    resource "aws_s3_bucket" "this2" {
        ommited
    }

Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
│ Warning: Config generation is experimental
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Terraform has generated configuration and written it to generated_resources.tf. Please review the configuration and edit it as necessary before adding it to version
control.

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.



As you can see, at the moment of writing this article, config generation is still experimental, so you should review it carefully before making any changes to your Terraform configuration.

The resulting file will be similar to this:

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "import-bucket-tf15"
resource "aws_s3_bucket" "this" {
 bucket              = "import-bucket-tf15"
 bucket_prefix       = null
 force_destroy       = null
 object_lock_enabled = false
 tags                = {}
 tags_all            = {}
}

# __generated__ by Terraform from "import-bucket-tf15-2"
resource "aws_s3_bucket" "this2" {
 bucket              = "import-bucket-tf15-2"
 bucket_prefix       = null
 force_destroy       = null
 object_lock_enabled = false
 tags                = {}
 tags_all            = {}
}

Now, if you run a terraform plan, this is the output you are going to see:

Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.

After you are done with all the changes, you can simply run a terraform apply to add these resources to the state:

Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.

Approach towards importing multiple resources

As you have seen, the process of importing simple resources is pretty straightforward. The key here is to understand how Terraform state works. However, things can get quite tedious when importing complex deployments.

In the case of complex deployments, the team should have a clear identification of resources they want to manage using Terraform. A diagram representing the entire landscape helps to plan and to get the parts of deployment under Terraform’s management.

Planning output is key. If it is well-formatted, you will build a perfectly consistent state file. Prioritize and mitigate configurations that cause replacement of target resources, followed by configurations that cause changes. Import the resources as they are to satisfy the state file until there are no gaps. Any enhancement or code restructuring can be focused on and implemented once you achieve 0 gaps.

The difference between Terraform import and Terraform state mv

With terraform import you are importing existing infrastructure resources inside of your state to manage them with Terraform. However, if you want to keep an object created with Terraform and manage it as a different resource (having a different name or moving it under a different module inside your configuration), you will need to use terraform state mv.

Best practices for Terraform import

There are a couple of things you should consider before using terraform import:

  • Understand your existing infrastructure and see how it would fit your Terraform configuration
  • Create your Terraform configuration first – before importing your resources, it will be really helpful to create your HCL code and ensure it actually reflects the settings and properties that have been set up for your IaC resources
  • Use version control – before doing imports, ensure your state file is managed remotely and also that you have versioning enabled for it. This also applies to your code configuration as well.
  • Run a plan after import – it is very important to understand if the import worked properly and if the code configuration you’ve built really reflects the resource you have imported

Key points

Although this article provides a comprehensive overview of possible approaches towards importing resources into Terraform, there is always room for some particular and unusual scenarios. If you have any questions or would like to share your common practices, feel free to tell us by leaving a comment below!

We encourage you also to explore how Spacelift makes it easy to work with Terraform. If you need any help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this. It supports Git workflows, policy as code, programmatic configuration, context sharing, drift detection, and many more great features right out of the box. You can check it for free by creating a trial account or booking a demo with one of our engineers.

Note: New versions of Terraform will be placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that will expand on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6. OpenTofu retained all the features and functionalities that had made Terraform popular among developers while also introducing improvements and enhancements. OpenTofu is the future of the Terraform ecosystem, and having a truly open-source project to support all your IaC needs is the main priority.

Manage Terraform Better with Spacelift

– Manage Terraform state and complex workflows effectively.

– Identify and remediate drift.

– Collaborate efficiently with context sharing, policy as code, programmatic configuration, resource visualization, and lots more.

Start free trial
Terraform CLI Commands Cheatsheet

Initialize/ plan/ apply your IaC, manage modules, state, and more.

Share your data and download the cheatsheet