Import Existing Resources Into Terraform code

One of the questions my friends often ask me is: "Importing resources from the console into the state, then having to cover it in the source code, it's frustrating, isn't it?" On this occasion, someone also asked me about importing in Terraform, so I'd like to share some thoughts:

In reality, my experience is still quite limited, but I've had my fair share of painful tasks like: "We've just set up a series of resources on the console, please convert it into code for us" (of course, the policy doesn't allow for configuration scanning tools, and the client doesn't even remember what they created). So, here are two tips:

  1. Identifying created resources: There are many ways to do this. The most classic method that immediately comes to mind is querying CloudTrail logs. A better way is to use AWS Config, but the bad news is that not every client uses AWS Config. The boss wants to provide a list to check right away, and querying CloudTrail, while accurate, takes more time.

    Tips here: One convenient thing about the cloud is its transparency in costs, pay-as-you-go model, so almost all services and resources you use are reflected in the Bill section. Therefore, a quick way to narrow down the resources that need checking is to look at the Billing section.

  2. If you're not familiar with Terraform, you may try to follow the Terraform workflow: Write code -> Validate code -> Plan -> Apply -> Destroy

However, if you think a bit more "outside the box," the task of importing resources from the console into the state is somewhat reverse to the usual workflow. Therefore, a faster approach is to be a bit "reverse" as well:

Write the most basic code - as simple as possible to locate resources -> import -> Cover the configuration in the source code -> Plan to check.

In this workflow, to save time and effort, there are two powerful helpers:

  • The terraform validate command

  • The terraform state show command

If you're interested in how I usually do it, you can refer to the following article:

Confirm the state before import:

The first thing that you have to do is uing terraform plan command to confirm the current state.

Result:

https://live.staticflickr.com/65535/49617065871_223fd4116f_k.jpg

To make our system’s state be stable, we have to keep the change to the state as small as possible. As you can see in the above image, there are 3 resources to add and 2 resources that have to be change.

Our task now is to make changes to the state in tfstate file of 2 following resources:

  • outer_elb

  • outer_elb[1]

by import 2 resources below in to the terraform code:

  • front_access_sg

  • front_access_vendor_sg

Importing resources:

Step 1/ Remove 2 objects in tfstate file:

You can search for the location of the two in terraform.tfstate.prd file by keywords:

"name": "front_a_sg""name": "front_b_sg"

Step 2/ Import the resources:

  • Log in to the console and find the resources wanting to import.

  • Copy the ids of the resources that you want to import. (in this case, the id of front_a_sg is sg-036ec74316xxxxx and that of front_b_sg is sg-0eeda2xxxx)

  • Run terraform import command to import existing resources to the state file:

  • Import front_a_sg with id sg-036ec74316xxxx:

/usr/local/src/terraform/terraform_0.12 import -var-file=./tfvars/prd.tfvars -state=./tfstate/terraform.tfstate.prd --state-out=./tfstate/terraform.tfstate.prd.new aws_security_group.front_a_sg sg-036ec74316xxxx
  • Import front_b_sg with id: sg-0eeda2xxxx:
/usr/local/src/terraform/terraform_0.12 import -var-file=./tfvars/prd.tfvars -state=./tfstate/terraform.tfstate.prd.new --state-out=./tfstate/terraform.tfstate.prd.new aws_security_group.front_b_sg sg-0eeda2xxxx

Step 3/ Confirm the result by running terraform plan command again.

You should notice that the state change of system now is equal to 0.

Memo: If have index:

/usr/local/src/terraform/terraform_0.14 import -var-file=./tfvars/stg.tfvars -state=./tfstate/terraform.tfstate.stg --state-out=./tfstate/terraform.tfstate.stg aws_db_parameter_group.rds_parameter_group[\"rds_instance_001\"] web-rds-pg

⇒ escaping the double quotes fixes that.

Syntax to use:

terraform import aws_iam_user.api_users[\\"foo\\"] foo

Step by step example:

Actually, sometimes, you do not have to implement all resources logic to import a resource into Terraform state.

For instance, in the case you import EC2 instance, you may only need some required parameters and/or something that you want to set (if not required). I want to import instance id: i-09f00e5bfe5ef9592

This is something I write initially:

terraform {
  backend "s3" {
    bucket = "yen-codejj"
    key    = "terraform.tfstate"
    region = "ap-southeast-1"
  }
}

resource "aws_instance" "this" {
  ami           = "ami-0e97ea97a2f374e3d"
}

and import:

[cloudshell-user@ip-10-130-56-214 import-demo]$ 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 latest version of hashicorp/aws...
- Installing hashicorp/aws v5.58.0...
- Installed hashicorp/aws v5.58.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.
[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform import aws_instance.this i-09f00e5bfe5ef9592
aws_instance.this: Importing from ID "i-09f00e5bfe5ef9592"...
aws_instance.this: Import prepared!
  Prepared aws_instance for import
aws_instance.this: Refreshing state... [id=i-09f00e5bfe5ef9592]

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.

After that, you should utilize terraform state list and terraform state show command:

[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform state list
aws_instance.this
aws_vpc.main
[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform state show aws_instance.this
# aws_instance.this:
resource "aws_instance" "this" {
    ami                                  = "ami-0e97ea97a2f374e3d"
    arn                                  = "arn:aws:ec2:ap-southeast-1:830427153490:instance/i-09f00e5bfe5ef9592"
    associate_public_ip_address          = true
    availability_zone                    = "ap-southeast-1a"
    cpu_core_count                       = 1
    cpu_threads_per_core                 = 1
    disable_api_stop                     = false
    disable_api_termination              = false
    ebs_optimized                        = false
    get_password_data                    = false
    hibernation                          = false
    host_id                              = null
    iam_instance_profile                 = "ec2-admin-role"
    id                                   = "i-09f00e5bfe5ef9592"
    instance_initiated_shutdown_behavior = "stop"
    instance_lifecycle                   = null
    instance_state                       = "running"
    instance_type                        = "t2.micro"
    ipv6_address_count                   = 0
    ipv6_addresses                       = []
    key_name                             = null
    monitoring                           = false
    outpost_arn                          = null
    password_data                        = null
    placement_group                      = null
    placement_partition_number           = 0
    primary_network_interface_id         = "eni-0fb7af3897853f6ab"
    private_dns                          = "ip-172-31-46-55.ap-southeast-1.compute.internal"
    private_ip                           = "172.31.46.55"
    public_dns                           = "ec2-52-77-252-47.ap-southeast-1.compute.amazonaws.com"
    public_ip                            = "52.77.252.47"
    secondary_private_ips                = []
    security_groups                      = [
        "launch-wizard-1",
    ]
    source_dest_check                    = true
    spot_instance_request_id             = null
    subnet_id                            = "subnet-e707d7af"
    tags                                 = {
        "Name" = "test-instance"
    }
    tags_all                             = {
        "Name" = "test-instance"
    }
    tenancy                              = "default"
    vpc_security_group_ids               = [
        "sg-080bed86578a1c4a1",
    ]

    capacity_reservation_specification {
        capacity_reservation_preference = "open"
    }

    cpu_options {
        amd_sev_snp      = null
        core_count       = 1
        threads_per_core = 1
    }

    credit_specification {
        cpu_credits = "standard"
    }

    enclave_options {
        enabled = false
    }

    maintenance_options {
        auto_recovery = "default"
    }

    metadata_options {
        http_endpoint               = "enabled"
        http_protocol_ipv6          = "disabled"
        http_put_response_hop_limit = 2
        http_tokens                 = "required"
        instance_metadata_tags      = "disabled"
    }

    private_dns_name_options {
        enable_resource_name_dns_a_record    = true
        enable_resource_name_dns_aaaa_record = false
        hostname_type                        = "ip-name"
    }

    root_block_device {
        delete_on_termination = true
        device_name           = "/dev/xvda"
        encrypted             = false
        iops                  = 3000
        kms_key_id            = null
        tags                  = {}
        tags_all              = {}
        throughput            = 125
        volume_id             = "vol-0b1aa997b0ec01413"
        volume_size           = 8
        volume_type           = "gp3"
    }
}

You can see all configurations of the resource beautifully. But be careful! You can not just copy all of this to your .tf code file to run because there are also things that are attributes, not arguments.

Go to the docs:

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#attribute-reference

Look for attributes and omit them, we will have arguments configuration. Use terraform validate to find invalid or conflicts if have:

[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform validate
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 14, in resource "aws_instance" "this":
│   14:     cpu_core_count                       = 1
│ 
│ "cpu_core_count": conflicts with cpu_options.0.core_count
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 15, in resource "aws_instance" "this":
│   15:     cpu_threads_per_core                 = 1
│ 
│ "cpu_threads_per_core": conflicts with cpu_options.0.threads_per_core
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 25, in resource "aws_instance" "this":
│   25:     ipv6_address_count                   = 0
│ 
│ "ipv6_address_count": conflicts with ipv6_addresses
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 26, in resource "aws_instance" "this":
│   26:     ipv6_addresses                       = []
│ 
│ "ipv6_addresses": conflicts with ipv6_address_count
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 47, in resource "aws_instance" "this":
│   47:         core_count       = 1
│ 
│ "cpu_options.0.core_count": conflicts with cpu_core_count
╵
╷
│ Error: Conflicting configuration arguments
│ 
│   with aws_instance.this,
│   on test.tf line 48, in resource "aws_instance" "this":
│   48:         threads_per_core = 1
│ 
│ "cpu_options.0.threads_per_core": conflicts with cpu_threads_per_core
╵
[cloudshell-user@ip-10-130-56-214 import-demo]$

Fix all conflicts so that:

[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform validate
Success! The configuration is valid.

Run terraform plan to verify:

[cloudshell-user@ip-10-130-56-214 import-demo]$ terraform plan
aws_vpc.main: Refreshing state... [id=vpc-0a7190e0070ce0e6d]
aws_instance.this: Refreshing state... [id=i-09f00e5bfe5ef9592]

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.
[cloudshell-user@ip-10-130-56-214 import-demo]$

The resource now has been successfully imported to Terraform state!

The terraform code file now similar to this:

terraform {
  backend "s3" {
    bucket = "yen-codejj"
    key    = "terraform.tfstate"
    region = "ap-southeast-1"
  }
}

resource "aws_instance" "this" {
    ami                                  = "ami-0e97ea97a2f374e3d"
    associate_public_ip_address          = true
    availability_zone                    = "ap-southeast-1a"
    disable_api_stop                     = false
    disable_api_termination              = false
    ebs_optimized                        = false
    get_password_data                    = false
    hibernation                          = false
    host_id                              = null
    iam_instance_profile                 = "ec2-admin-role"
    instance_initiated_shutdown_behavior = "stop"
    instance_type                        = "t2.micro"
    ipv6_addresses                       = []
    key_name                             = null
    monitoring                           = false
    placement_group                      = null
    placement_partition_number           = 0
    secondary_private_ips                = []
    security_groups                      = [
        "launch-wizard-1",
    ]
    source_dest_check                    = true
    subnet_id                            = "subnet-e707d7af"
    tags                                 = {
        "Name" = "test-instance"
    }
    tenancy                              = "default"
    vpc_security_group_ids               = [
        "sg-080bed86578a1c4a1",
    ]

    cpu_options {
        amd_sev_snp      = null
        core_count       = 1
        threads_per_core = 1
    }

    credit_specification {
        cpu_credits = "standard"
    }

    enclave_options {
        enabled = false
    }

    maintenance_options {
        auto_recovery = "default"
    }

    metadata_options {
        http_endpoint               = "enabled"
        http_protocol_ipv6          = "disabled"
        http_put_response_hop_limit = 2
        http_tokens                 = "required"
        instance_metadata_tags      = "disabled"
    }

    private_dns_name_options {
        enable_resource_name_dns_a_record    = true
        enable_resource_name_dns_aaaa_record = false
        hostname_type                        = "ip-name"
    }

    root_block_device {
        delete_on_termination = true
        encrypted             = false
        iops                  = 3000
        kms_key_id            = null
        tags                  = {}
        tags_all              = {}
        throughput            = 125
        volume_size           = 8
        volume_type           = "gp3"
    }
}

You can continue to omit other configurations if not necessary to track.

Note:

Reason for deleting configurations in the attributes category:

Others may have a more accurate explanation. But I understand Terraform's attributes and arguments simply as follows:

Terraform is declarative+, so it shouldn't be approached naively like other declaratives, but should be thought of a bit like traditional imperative code:

  • Arguments: These are input values for functions or requests -> With Terraform, it will send requests to the API to create resources, so arguments are the input values for the resources to be created.

  • Attributes: When do we have attributes? Attributes are properties of an object, and for properties to exist, the object needs to be created -> Imagine resources as objects, so when we're just coding, they haven't been created yet, don't exist, and only appear when the object has been created => These are the output values of Terraform.

When covering the configuration, we will be covering the arguments for the resource creation request, so we need to remove the items in the state related to attributes.