Importing existing cloud infrastructure with Terraform

June 9, 2024    Terraform IaC

Overview

As a Cloud DevOps engineer, you sometimes need to import an existing cloud infrastructure into Terraform. Terraform has a capability to import existing cloud resources. It allows you to take your cloud resources created through ClickOps and import them into Terraform state file to be managed under Terraform IaC.

Before Terraform v1.5.0, the only way to import resources into Terraform was using the terraform import command line. This command has the following limitations:

  • You can only import one resource at a time (painful if you have many resources to import)
  • The Terraform state file is modified immediately, ie. you don’t get to preview the outcome
  • A matching resource block needs to be manually coded, which can be a laborious process

In June 12 2023, HashiCorp released Terraform v1.5.0 and introduced a new config-driven import block. I have recently used this capability as I needed to import many (50+) AWS cloud resources that were created manually.

I should have experimented this feature when it was first released about a year ago because prior to this, I have been utilising the old terraform import command. Long story short, using the Terraform import block saved me a lot of grief and I feel like sharing my experience by writing a blog post about it.

Terraform import block

The Terraform import block allows you to define import operations in code. It addresses all of the terraform import CLI limitations. You can do resources import in bulk and the operations are part of the standard Terraform plan and apply process, which eliminates the risk of unintended state file modification. That means you can now run terraform plan to preview the operation first, and then run terraform apply to execute the import operation.

The import block has the following arguments:

  • to - the imported resource (HCL) address in the state file
  • id - the cloud resource ID string
  • provider (optional) - an optional custom resource provider

To find out what resources are importable and the format, please check HashiCorp cloud provider documentation. An example of import block for Amazon EC2 instance:

import {
  # HCL resource address
  to = aws_instance.web

  # Cloud resource ID
  id = "i-12345678"
}

And the best part is you can now generate the imported resource code automatically. This saves a significant amount of time as you don’t need to spend time writing code to match the imported resources. You simply need to execute a plan operation with the new -generate-config-out parameter to automatically create the matching resource configuration code in a file you specify in the parameter value.

You can review and edit the generated code, and then run an terraform apply to complete the resources import into the state file. Please note that the code generation during import is currently experimental.

A simple demo

For the purpose of demo, I simply created the following AWS resources using the AWS console UI:

  • A security group named tf-import-sg with an inbound rule for port 22 and outbound rule for port 443
  • An S3 bucket named tf-import-s3

Step 1: Create Terraform import configuration

I created the Terraform configuration file, eg. imports.tf, to place the import block for the resources that I want to import.

 1# Security group import
 2import {
 3  to = aws_security_group.tf_import
 4  id = "sg-05af4ed76c86e2b2b"
 5}
 6
 7# S3 bucket import
 8import {
 9  to = aws_s3_bucket.tf_import
10  id = "tf-import-s3"
11}

Step 2: Preview and generate the configuration code

Next step is to run the terraform plan with the -generate-config-out parameter to preview and generate configuration code.

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

The terraform plan output:

wkhoo-macbook:tf-import $ terraform plan -generate-config-out=generated.tf
aws_security_group.tf_import: Preparing import... [id=sg-05af4ed76c86e2b2b]
aws_s3_bucket.tf_import: Preparing import... [id=tf-import-s3]
aws_security_group.tf_import: Refreshing state... [id=sg-05af4ed76c86e2b2b]
aws_s3_bucket.tf_import: Refreshing state... [id=tf-import-s3]

Terraform will perform the following actions:

  # aws_s3_bucket.tf_import will be imported
  # (config will be generated)
    resource "aws_s3_bucket" "tf_import" {
        arn                         = "arn:aws:s3:::tf-import-s3"
        bucket                      = "tf-import-s3"
        bucket_domain_name          = "tf-import-s3.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-import-s3.s3.ap-southeast-2.amazonaws.com"
        hosted_zone_id              = "Z1WCIGYICN2BYD"
        id                          = "tf-import-s3"
        object_lock_enabled         = false
        region                      = "ap-southeast-2"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "754c7d875755bfb329f9bd5cf61f8eacb3db030406e6765caf8e869da5036366"
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = false
            mfa_delete = false
        }
    }

  # aws_security_group.tf_import will be imported
  # (config will be generated)
    resource "aws_security_group" "tf_import" {
        arn         = "arn:aws:ec2:ap-southeast-2:308825204162:security-group/sg-05af4ed76c86e2b2b"
        description = "Demo of security group Terraform import"
        egress      = [
            {
                cidr_blocks      = [
                    "0.0.0.0/0",
                ]
                description      = ""
                from_port        = 443
                ipv6_cidr_blocks = []
                prefix_list_ids  = []
                protocol         = "tcp"
                security_groups  = []
                self             = false
                to_port          = 443
            },
        ]
        id          = "sg-05af4ed76c86e2b2b"
        ingress     = [
            {
                cidr_blocks      = [
                    "10.0.0.0/8",
                ]
                description      = ""
                from_port        = 22
                ipv6_cidr_blocks = []
                prefix_list_ids  = []
                protocol         = "tcp"
                security_groups  = []
                self             = false
                to_port          = 22
            },
        ]
        name        = "tf-import-sg"
        owner_id    = "308825204162"
        tags        = {
            "Name" = "tf-import-sg"
        }
        tags_all    = {
            "Name" = "tf-import-sg"
        }
        vpc_id      = "vpc-0be2f04ae84a21152"
    }

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.
╵

Step 3: (Optional) Review and edit the generated code

You can review the generated configuration code and edit it as necessary before proceeding to import.

wkhoo-macbook:tf-import $ cat generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "sg-05af4ed76c86e2b2b"
resource "aws_security_group" "tf_import" {
  description = "Demo of security group Terraform import"
  egress = [{
    cidr_blocks      = ["0.0.0.0/0"]
    description      = ""
    from_port        = 443
    ipv6_cidr_blocks = []
    prefix_list_ids  = []
    protocol         = "tcp"
    security_groups  = []
    self             = false
    to_port          = 443
  }]
  ingress = [{
    cidr_blocks      = ["10.0.0.0/8"]
    description      = ""
    from_port        = 22
    ipv6_cidr_blocks = []
    prefix_list_ids  = []
    protocol         = "tcp"
    security_groups  = []
    self             = false
    to_port          = 22
  }]
  name                   = "tf-import-sg"
  name_prefix            = null
  revoke_rules_on_delete = null
  tags = {
    Name = "tf-import-sg"
  }
  tags_all = {
    Name = "tf-import-sg"
  }
  vpc_id = "vpc-0be2f04ae84a21152"
}

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

Step 4: Import the resources

Run the terraform apply command to import the resources.

terraform apply

The terraform apply output:

wkhoo-macbook:tf-import $ terraform apply -auto-approve
aws_s3_bucket.tf_import: Preparing import... [id=tf-import-s3]
aws_security_group.tf_import: Preparing import... [id=sg-05af4ed76c86e2b2b]
aws_security_group.tf_import: Refreshing state... [id=sg-05af4ed76c86e2b2b]
aws_s3_bucket.tf_import: Refreshing state... [id=tf-import-s3]

Terraform will perform the following actions:

  # aws_s3_bucket.tf_import will be imported
    resource "aws_s3_bucket" "tf_import" {
        arn                         = "arn:aws:s3:::tf-import-s3"
        bucket                      = "tf-import-s3"
        bucket_domain_name          = "tf-import-s3.s3.amazonaws.com"
        bucket_regional_domain_name = "tf-import-s3.s3.ap-southeast-2.amazonaws.com"
        hosted_zone_id              = "Z1WCIGYICN2BYD"
        id                          = "tf-import-s3"
        object_lock_enabled         = false
        region                      = "ap-southeast-2"
        request_payer               = "BucketOwner"
        tags                        = {}
        tags_all                    = {}

        grant {
            id          = "754c7d875755bfb329f9bd5cf61f8eacb3db030406e6765caf8e869da5036366"
            permissions = [
                "FULL_CONTROL",
            ]
            type        = "CanonicalUser"
        }

        server_side_encryption_configuration {
            rule {
                bucket_key_enabled = true

                apply_server_side_encryption_by_default {
                    sse_algorithm = "AES256"
                }
            }
        }

        versioning {
            enabled    = false
            mfa_delete = false
        }
    }

  # aws_security_group.tf_import will be imported
    resource "aws_security_group" "tf_import" {
        arn         = "arn:aws:ec2:ap-southeast-2:308825204162:security-group/sg-05af4ed76c86e2b2b"
        description = "Demo of security group Terraform import"
        egress      = [
            {
                cidr_blocks      = [
                    "0.0.0.0/0",
                ]
                description      = ""
                from_port        = 443
                ipv6_cidr_blocks = []
                prefix_list_ids  = []
                protocol         = "tcp"
                security_groups  = []
                self             = false
                to_port          = 443
            },
        ]
        id          = "sg-05af4ed76c86e2b2b"
        ingress     = [
            {
                cidr_blocks      = [
                    "10.0.0.0/8",
                ]
                description      = ""
                from_port        = 22
                ipv6_cidr_blocks = []
                prefix_list_ids  = []
                protocol         = "tcp"
                security_groups  = []
                self             = false
                to_port          = 22
            },
        ]
        name        = "tf-import-sg"
        owner_id    = "308825204162"
        tags        = {
            "Name" = "tf-import-sg"
        }
        tags_all    = {
            "Name" = "tf-import-sg"
        }
        vpc_id      = "vpc-0be2f04ae84a21152"
    }

Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
aws_security_group.tf_import: Importing... [id=sg-05af4ed76c86e2b2b]
aws_security_group.tf_import: Import complete [id=sg-05af4ed76c86e2b2b]
aws_s3_bucket.tf_import: Importing... [id=tf-import-s3]
aws_s3_bucket.tf_import: Import complete [id=tf-import-s3]

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

Step 5: (Optional) Verify the import

Verify the resources are imported successfully into Terraform state.

wkhoo-macbook:tf-import $ terraform state list
aws_s3_bucket.tf_import
aws_security_group.tf_import

Success! The S3 bucket and security group were imported successfully and now managed by Terraform.

With a few simple steps, I imported the resources into Terraform state file easily. This is just a simple demo with 2 simple AWS resources but the steps are the same for any other importable resources. You will still need to craft the import configuration file, which takes less time compared to writing the resource configuration code.

Clean up

As the S3 bucket and security group have been imported and managed by Terraform, you can run terraform destroy to delete the resources.

Some considerations

  • The import block is only available in Terraform v1.5.0 and later version. You will get the following error if you try with an older version of Terraform:
╷
│ Error: Unsupported block type
│
│   on imports.tf line 12:
│   12: import {
│
│ Blocks of type "import" are not expected here.
  • Terraform import block currently does not support interpolation in the ID field. It must be a string value. So you can’t do something like this:
locals {
  bucket_name = "tf-import-s3"
}
import {
  to = module.s3.aws_s3_bucket.tf_import
  id = local.bucket_name
}
  • You can only define the import block in the root (parent) module. If you define the import block inside a module, you will get this error:
╷
│ Error: Invalid import configuration
│
│   on s3/main.tf line 2:
│    2: import {
│
│ An import block was detected in "module.s3". Import blocks are only allowed in the root module.
╵

There is an open GitHub issue for the feature request. You can still use the terraform import CLI to import a resource into a Terraform module.

  • Lastly, you would still take a backup of your Terraform state file just in case before importing so that you can roll back to the previous state quickly.

Summary

In this post, I talked about an overview of the Terraform import feature which you can use to import existing cloud infrastructure resources under Terraform IaC management. I have demonstrated how you can import an S3 bucket and a security group easily into Terraform state using the Terraform config-driven import block. I also highlighted some considerations when using the Terraform import block feature.

The terraform import command can import existing cloud resource but will not generate the imported resource configuration. You will need to write the code. The Terraform import block will import the existing resources into Terraform state and provide the capability to generate the resource configuration, which is very handy in my view even though it is an experimental feature.