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:
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.
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:
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"
và "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:
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.