Virtual Day
Building the ecosystem for the Cloud Operating Model with Azure, Cisco, F5 and GitLab Register

Manage Terraform State

Import Terraform configuration

In this guide, you will import an existing Docker container and image into an empty Terraform workspace. By doing so, you will learn strategies and considerations for importing real-world infrastructure into Terraform.

The default Terraform workflow involves creating and managing infrastructure entirely with Terraform.

  1. Write Terraform configuration that defines the infrastructure you want to create.

  2. Review the Terraform plan to ensure the configuration will result in the expected state and infrastructure.

  3. Apply the configuration to create your Terraform state and infrastructure.

Once you create infrastructure with Terraform, you can update the configuration, and plan and apply those changes. Eventually you use Terraform to destroy the infrastructure when it is no longer needed. This workflow assumes that Terraform will create brand new infrastructure.

Typical Terraform workflow: Write config, review plan, apply

However, you may need to manage infrastructure that wasn’t created by Terraform. Terraform import solves this problem by loading supported resources into your Terraform workspace’s state. The import command doesn’t automatically generate the configuration to manage the infrastructure, though. Because of this, importing existing infrastructure into Terraform is a multi-step process.

Bringing existing infrastructure under Terraform’s control involves five main steps:

  1. Identify the existing infrastructure to be imported.
  2. Import infrastructure into your Terraform state.
  3. Write Terraform configuration that matches that infrastructure.
  4. Review the Terraform plan to ensure the configuration matches the expected state and infrastructure.
  5. Apply the configuration to update your Terraform state.

Terraform import workflow: Identify infrastructure, import into state, write config, review plan, apply

In this guide, first you will create a Docker container with the Docker CLI. Next, you will import it into a new Terraform workspace. Then you will update the container’s configuration using Terraform before finally destroying it when you are done.

»Prerequisites

In order to follow this guide you will need the following.

  1. The Terraform CLI.

  2. Docker installed and running.

  3. The git CLI.

»Create a Docker container

Create a container named hashicorp-learn using the latest NGINX image from Docker Hub, and publish that container’s port 80 (HTTP) to your local host system’s port 8080. You will import this container in this guide.

$ docker run --name hashicorp-learn --detach --publish 8080:80 nginx:latest
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
afb6ec6fdc1c: Pull complete
dd3ac8106a0b: Pull complete
8de28bdda69b: Pull complete
a2c431ac2669: Pull complete
e070d03fd1b5: Pull complete
Digest: sha256:883874c218a6c71640579ae54e6952398757ec65702f4c8ba7675655156fcca6
Status: Downloaded newer image for nginx:latest
e7ba41fd94e51c501533241e4cffd307fbda81c5b402c372d989c4578518d2e5

Verify that the container is running with docker ps.

$ docker ps --filter "name=hashicorp-learn"
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                  NAMES
e7ba41fd94e5        nginx:latest        "/docker-entrypoint.…"   About a minute ago   Up 59 seconds       0.0.0.0:8080->80/tcp   hashicorp-learn

Visit the address 0.0.0.0:8080 in your web browser to see the NGINX default index page.

Now you have a docker image and container to import into your workspace and manage with Terraform.

»Import the container into Terraform

Now, clone the example repository.

$ git clone https://github.com/hashicorp/learn-terraform-import.git

Next, change into that directory.

$ cd learn-terraform-import

In this directory, you will find two Terraform configuration files that make up the configuration you will use in this guide.

  • main.tf file configures the docker provider
  • docker.tf file will contain the configuration necessary to manage the Docker container you created in the previous step

Initialize your Terraform workspace with terraform init.

$ terraform init

Next, define an empty docker_container resource in your docker.tf file, which represents a Docker container with the Terraform resource ID docker_container.web.

resource "docker_container" "web" {}

Next, run docker ps to find the name of the container you want to import - in this case, the container you created in the previous step.

$ docker ps --filter "name=hashicorp-learn"
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
d45091b71212        nginx:latest        "nginx -g 'daemon of…"   18 minutes ago      Up 18 minutes       0.0.0.0:8080->80/tcp   hashicorp-learn

Now run terraform import to attach the existing Docker container to the docker_container.web resource you just created. Terraform import requires this Terraform resource ID and the full Docker container ID. In the following example, the command docker inspect -f {{.ID}} hashicorp-learn returns the full SHA256 container ID.

$ terraform import docker_container.web $(docker inspect -f {{.ID}} hashicorp-learn)
docker_container.web: Importing from ID "d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee"...
docker_container.web: Import prepared!
  Prepared docker_container for import
docker_container.web: Refreshing state... [id=d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee]

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.

Now verify that the container has been imported into your Terraform state by running terraform show.

$ terraform show
# docker_container.web:
resource "docker_container" "web" {
    command           = [
        "nginx",
        "-g",

## Output truncated…

    ports {
        external = 8080
        internal = 80
        ip       = "0.0.0.0"
        protocol = "tcp"
    }
}

This state contains everything that Terraform knows about the docker container you just imported. However, Terraform import does not create the configuration for the resource.

»Create configuration

You’ll need to create Terraform configuration before you can use Terraform to manage this container.

Run terraform plan. Terraform will show errors for the missing required arguments image and name. Terraform cannot generate a plan for a resource that is missing required arguments.

$ terraform plan

Error: Missing required argument

  on docker.tf line 1, in resource "docker_container" "web":
   1: resource "docker_container" "web" { }

The argument "name" is required, but no definition was found.


Error: Missing required argument

  on docker.tf line 1, in resource "docker_container" "web":
   1: resource "docker_container" "web" { }

The argument "image" is required, but no definition was found.

There are two approaches to update the configuration in docker.tf to match the state you imported. You can either accept the entire current state of the resource into your configuration as-is or cherry-pick the required attributes into your configuration one at a time. You may find both of these approaches useful in different circumstances.

  • Using the current state is often faster, but can result in an overly verbose configuration since every attribute is included in the state, whether it is necessary to include in your configuration or not.
  • Cherry-picking the required attributes can lead to more manageable configuration, but requires you to understand which attributes need to be set in the configuration.
Try either or both of these approaches using the tabs below.

To use current state as configuration, you will:

  1. Copy your Terraform state into a configuration file.

  2. Run terraform plan to identify and remove read-only configuration arguments.

  3. Re-run terraform plan to confirm the configuration is correct.

  4. Run terraform apply to finish synchronizing your configuration, state, and infrastructure.

Use terraform show to copy your Terraform state into your docker.tf file.

$ terraform show -no-color > docker.tf

Inspect the docker.tf file to see that its contents have been replaced with the output of the terraform show command you just ran.

Now run terraform plan. Terraform will show warnings and errors about a deprecated argument ('links'), and several read-only arguments (ip_address, network_data, gateway, ip_prefix_length, id).

$ terraform plan

Warning: "links": [DEPRECATED] The --link flag is a legacy feature of Docker. It may eventually be removed.

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {



Error: "ip_prefix_length": this field cannot be set

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {



Error: "ip_address": this field cannot be set

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {



Error: "network_data": this field cannot be set

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {



Error: "gateway": this field cannot be set

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {



Error: : invalid or unknown key: id

  on docker.tf line 2, in resource "docker_container" "web":
   2: resource "docker_container" "web" {

These read-only arguments are values that Terraform stores in its state for Docker containers but that it cannot set via configuration since they are managed internally by Docker. Terraform can set the links argument with configuration, but still throws a warning because it is deprecated and may not be supported by future versions of the Docker provider.

Remove all six of these attributes from your docker.tf configuration file before continuing with the next step.

         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
         "PKG_RELEASE=1~buster",
     ]
-    gateway           = "172.17.0.1"
     group_add         = []
     hostname          = "0cc3203b4634"
-    id                = "0cc3203b46342f0adf7ed7a30d41311aed65e5e8632d29ca5e6107ee7be39f16"
     image             = "sha256:2622e6cca7ebbb6e310743abce3fc47335393e79171b9d76ba9d4f446ce7b163"
-    ip_address        = "172.17.0.2"
-    ip_prefix_length  = 16
     ipc_mode          = "private"
-    links             = []
     log_driver        = "json-file"
     log_opts          = {}
     max_retry_count   = 0
     memory            = 0
     memory_swap       = 0
     name              = "hashicorp-learn"
-    network_data      = [
-        {
-            gateway          = "172.17.0.1"
-            ip_address       = "172.17.0.2"
-            ip_prefix_length = 16
-            network_name     = "bridge"
-        },
-    ]
     network_mode      = "default"
     privileged        = false
     publish_all_ports = false

When importing real infrastructure, consult the provider documentation to learn what each argument does. This will help you to determine how to handle any errors or warnings from the plan step. For instance, the documentation for the links argument is in the Docker provider documentation.

Now verify that the errors have been resolved by re-running terraform plan.

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

docker_container.web: Refreshing state... [id=d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  ## docker_container.web will be updated in-place
  ~ resource "docker_container" "web" {
      + attach            = false
        command           = [
            "Nginx",
            "-g",
            "daemon off;",
        ]

## Output truncated...

        ports {
            external = 8080
            internal = 80
            ip       = "0.0.0.0"
            protocol = "tcp"
        }
}

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

## Output truncated...

The plan should now execute successfully. Notice that the plan indicates that Terraform will update the container in place to add the attach, logs, must_run, and start attributes.

Terraform uses these attributes to create Docker containers, but Docker doesn’t store them. As a result, terraform import didn’t load their values into state. When you plan and apply your configuration, the Docker provider will assign the default values for these attributes and save them in state, but they won’t affect the running container.

Apply the changes and finish the process of syncing your Terraform configuration and state with the Docker container they represent. Remember to confirm the apply step with a yes.

$ terraform apply
docker_container.web: Refreshing state... [id=d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # docker_container.web will be updated in-place
  ~ resource "docker_container" "web" {
      + attach            = false
        command           = [
            "nginx",
            "-g",

# Output truncated…

        ports {
            external = 8080
            internal = 80
            ip       = "0.0.0.0"
            protocol = "tcp"
        }
    }

Plan: 0 to add, 1 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

docker_container.web: Modifying... [id=d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee]
docker_container.web: Modifications complete after 0s [id=d45091b7121266f0c0e69dd9985acdefd110a66bcedbd03797e3606fb0a7d7ee]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Now your configuration file, Terraform state, and the container are all in sync, and you can use Terraform to manage the Terraform container as you normally would. Because this apply step changed the container’s state rather than destroying and recreating it, the container ID didn’t change, and the container continued running normally during the process.

Since the approach shown here loads all of the attributes represented in Terraform state, your configuration includes optional attributes whose values are the same as their defaults. Which attributes are optional, and their default values, will vary from provider to provider, and can be found in the provider documentation.

Optionally, you can remove all of these attributes, keeping only the required attributes and those for whom your container differs from the default values. After removing these unnecessary attributes, your configuration should match the following.

resource "docker_container" "web" {
    image = "sha256::602e111c06b6934013578ad80554a074049c59441d9bcd963cb4a7feccede7a5"
    name  = "hashicorp-learn"

    ports {
      external = 8080
      internal = 80
    }
}

At this point, running terraform plan or terraform apply will show no changes, and your configuration only includes the minimum set of attributes needed to recreate the container as-is.

If you want to try the other method for generating configuration before moving on, use the following steps to revert the changes you made in the previous section, then switch to the other tab.

  1. Remove everything inside the "docker_container" "web" block in docker.tf, so that the file only contains resource "docker_container" "web" { }.

  2. Remove the container from your terraform workspace's state by running: terraform state rm "docker_container.web".

  3. Import the container to Terraform state again by running the command terraform import docker_container.web $(docker inspect -f {{.ID}} hashicorp-learn).

Otherwise, proceed with the next step to verify your configuration.

»Verify import

Regardless of which method you used, your Docker container is now managed by Terraform. Use the Docker CLI to inspect the container.

$ docker ps --filter "name=hashicorp-learn"
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
fac6b3ddb49d        nginx:latest        "nginx -g 'daemon of…"   11 minutes ago      Up 11 minutes       0.0.0.0:8080->80/tcp   hashicorp-learn

Note the "Status" — the container has been up and running since it was created, so you know that it was not restarted when you imported it into Terraform. The ID has not changed either — this is the same container you created at the beginning of this guide.

Visit 0.0.0.0:8080 in your web browser to verify that the container is still working as intended.

»Create image resource

In some cases, you can bring resources under Terraform's control without using the terraform import command. This is often the case for resources that are defined by a single unique ID or tag, such as Docker images.

In your docker.tf file, the docker_container.web resource specifies the SHA256 hash ID of the image used to create the container. This is how docker stores the image ID internally, and so terraform import loaded the image ID directly into your state. However the image ID is not as human readable as the image tag or name, and it may not match your intent. For example, you might want to use the latest version of the "nginx" image.

Retrieve the image's tag name by running the following command. Replace the image ID with the image ID from docker.tf.

$ docker image inspect sha256:4392e5dad77dbaf6a573650b0fe1e282b57c5fba6e6cea00a27c7d4b68539b81 -f {{.RepoTags}}
[nginx:latest]

Then add the following configuration to your docker.tf file to represent this image as a resource.

resource "docker_image" "nginx" {
  name         = "nginx:latest"
}

Run terraform apply to create an image resource in state. Remember to confirm the apply step with a yes.

$ terraform apply
docker_container.web: Refreshing state... [id=023afc10768ab8eeaf646d6a3ac47b52a15af764367ded41702ef9cf5b91a976]

## Output truncated

Terraform will perform the following actions:

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id     = (known after apply)
      + latest = (known after apply)
      + name   = "nginx:latest"
    }

## Output truncated

Apply complete! Resources: 1 added, 0 changed, 0 destroyed!

Now that Terraform has created a resource for the image, you can reference it in your container’s configuration. Change the image value for docker_container.web to reference the new image resource.

resource "docker_container" "web" {
  name  = "hashicorp-learn"
  image = docker_image.nginx.latest

# File truncated...
}

Since docker_image.nginx.latest will match the hardcoded image ID you replaced. Running terraform apply at this point will show no changes.

$ terraform apply
docker_image.nginx: Refreshing state... [id=sha256:4392e5dad77dbaf6a573650b0fe1e282b57c5fba6e6cea00a27c7d4b68539b81nginx:latest]
docker_container.web: Refreshing state... [id=e7ba41fd94e51c501533241e4cffd307fbda81c5b402c372d989c4578518d2e5]

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

»Manage the container with Terraform

Now that Terraform manages the Docker container, use Terraform to change the its configuration.

In your docker.tf file, change the container's external port from 8080 to 8081.

resource "docker_container" "web" {
  name  = "hashicorp-learn"
  image = docker_image.nginx.latest

  ports {
    external = 8081
    internal = 80
  }
}

Apply the change. This will cause Terraform to destroy and recreate the container with the new port configuration. Remember to confirm the apply step with a yes.

$ terraform apply
docker_container.web: Refreshing state... [id=75278f99c53a6b39e94127d2c25f7dee13f97a4af89c52d74bff9dc783b3cce1]

## Output truncated

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

## Output truncated

docker_container.web: Destroying... [id=75278f99c53a6b39e94127d2c25f7dee13f97a4af89c52d74bff9dc783b3cce1]
docker_container.web: Destruction complete after 1s
docker_container.web: Creating...
docker_container.web: Creation complete after 1s [id=023afc10768ab8eeaf646d6a3ac47b52a15af764367ded41702ef9cf5b91a976]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Now verify that the container has been replaced with a new one with the new configuration by running docker ps or visiting 0.0.0.0:8081 in your web browser.

$ docker ps --filter "name=hashicorp-learn"
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
023afc10768a        4392e5dad77d        "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        0.0.0.0:8081->80/tcp   hashicorp-learn

Notice that the container ID has changed. Because changing the port configuration required destroying and recreating it, this is a completely new container.

»Destroy infrastructure

You have now imported your Docker container and the image used to create it into Terraform.

Destroy the container and image by running terraform destroy. Remember to confirm the destroy step by responding yes when prompted.

$ terraform destroy
docker_image.nginx: Refreshing state... [id=sha256:9beeba249f3ee158d3e495a6ac25c5667ae2de8a43ac2a8bfd2bf687a58c06c9nginx:latest]
docker_container.web: Refreshing state... [id=3fe1cb2e5326c31bac9250f6d09eade77945ee07ccea025d6424d91a89f98557]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # docker_container.web will be destroyed
  - resource "docker_container" "web" {
      - attach            = false -> null
      - command           = [
          - "nginx",
          - "-g",

## Output truncated…

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

docker_container.web: Destroying... [id=3fe1cb2e5326c31bac9250f6d09eade77945ee07ccea025d6424d91a89f98557]
docker_container.web: Destruction complete after 1s
docker_image.nginx: Destroying... [id=sha256:9beeba249f3ee158d3e495a6ac25c5667ae2de8a43ac2a8bfd2bf687a58c06c9nginx:latest]
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

Finally, run docker ps to validate that the container was destroyed.

$ docker ps --filter "name=hashicorp-learn"
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

»Limitations and other considerations

There are several important things to consider when importing resources into Terraform.

  • Terraform import can only know the current state of infrastructure as reported by the Terraform provider. It does not know:

    • whether the infrastructure is working correctly
    • the intent of the infrastructure
    • changes you've made to the infrastructure that aren't controlled by Terraform — for example, the state of a Docker container's filesystem.
  • Importing involves manual steps which can be error prone, especially if the person importing resources lacks the context of how and why those resources were created in the first place.

  • Importing manipulates the Terraform state file, you may want to create a backup before importing new infrastructure.

  • Terraform import doesn’t detect or generate relationships between infrastructure.

  • Terraform doesn’t detect default attributes that don’t need to be set in your configuration.

  • Not all providers and resources support Terraform import.

  • Just because infrastructure has been imported into Terraform does not mean that it can be destroyed and recreated by Terraform. For example, the imported infrastructure could rely on other unmanaged infrastructure or configuration.

Following Infrastructure as Code (IaC) best practices such as immutable infrastructure can help prevent many of these problems, but infrastructure created by hand is unlikely to follow IaC best practices.

Tools such as Terraformer to automate some manual steps associated with importing infrastructure. However, these tools are not part of Terraform itself, and not endorsed or supported by HashiCorp.

»Next steps

Now that you have imported infrastructure into Terraform, you may like to: