Terraform: managing state and workspaces
Introduction
In this tutorial, we will learn how to manage Terraform state and move sample resources between modules while using Terraform workspaces. This tutorial assumes you are already familiar with Terraform state and workspaces but we can quickly review those features.
State is simply information that Terraform stores about current infrastructure to map resources to configuration code.
Workspaces allow switching between multiple states of a single configuration with a single backend. A sample use case is deploying to a test environment before pushing changes to production. This is useful when using a CI/CD pipeline like GitHub Actions:
# Sample GitHub Actions workflow
jobs:
deploy-infra-test:
name: Deploy infra to test
with:
environment: test
terraform_workspace: test
deploy-infra-production:
name: Deploy infra to production
needs:
- deploy-infra-test
with:
environment: production
terraform_workspace: production
Please note that this tutorial is a simple example using a Terraform provider for Docker to demonstrate how to move Terraform resources from one module to another without breaking the infrastructure. In real-life scenarios we work with resources on Cloud providers such as AWS and updating such resources and their state requires special care.
Problem statement
Let’s assume we currently have some resources (Docker containers and local files) defined in a resources/infra
module and we’d like to move Docker resources to another module resources/infra-v2
into relevant workspaces (e.g. test
). We can use this GitHub repository for this example.
terraform-workspace-sample main$ tree
.
├── README.md
├── providers
│ ├── docker-module
│ │ ├── main.tf
│ │ └── variables.tf
│ └── file-module
│ ├── main.tf
│ └── variables.tf
└── resources
├── infra
│ ├── containers.tf
│ └── files.tf
└── infra-v2
└── containers.tf
After creating a test
workspace and running terraform apply
we can check out our local Docker container up and running:
$ cd resources/infra
$ terraform init
Terraform has been successfully initialized!
$ terraform workspace new test
Created and switched to workspace "test"!
$ terraform apply
...
module.file1.local_file.foo: Creating...
module.file1.local_file.foo: Creation complete after 0s [id=371aae854f336b541fd8bc75b6e6c8d6ba56d10a]
module.nginx1.docker_image.nginx_image: Creating...
module.nginx1.docker_image.nginx_image: Still creating... [10s elapsed]
module.nginx1.docker_image.nginx_image: Creation complete after 10s [id=sha256:8dd77ef2d82eade8dcf2c08ea032bd9cba04c9d28ace2ccf08ad6804c27bf14fnginx:latest]
module.nginx1.docker_container.nginx[0]: Creating...
module.nginx1.docker_container.nginx[0]: Creation complete after 0s [id=8d0d55518aa02f0dd0528eed2bc85b2b5f77e8c93bbb27429f6270dabc8be49b]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
docker_container_id = "8d0d55518aa02f0dd0528eed2bc85b2b5f77e8c93bbb27429f6270dabc8be49b"
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8d0d55518aa0 8dd77ef2d82e "/docker-entrypoint.…" 53 seconds ago Up 52 seconds 0.0.0.0:8090->80/tcp nginx-server-test-0
$ terraform state list
module.file1.local_file.foo
module.nginx1.docker_container.nginx[0]
module.nginx1.docker_image.nginx_image
Note the last command above which gives us a list of current resources in this module. And if we visit http://localhost:8090 we should see the Nginx welcome page:
We can also create those resources in another workspace (e.g. prod
):
$ terraform workspace new prod
$ terraform apply
...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
docker_container_id = "fe9b3e0a2a4cd4b19b74aa8dc8934ccc156c18b9e5efef30bdbedb8ac12133c0"
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fe9b3e0a2a4c 8dd77ef2d82e "/docker-entrypoint.…" 3 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp nginx-server-prod-0
8d0d55518aa0 8dd77ef2d82e "/docker-entrypoint.…" 10 minutes ago Up 10 minutes 0.0.0.0:8090->80/tcp nginx-server-test-0
We now have 2 Nginx servers running on different ports (8080, 8090) and in separate workspaces (test
and prod
). Now let’s move the nginx-server-prod-0
container to another module: infra-v2
.
Updating state
There are 2 steps we need to follow in order to move these resources:
- Update the state by removing that resource using
terraform state rm
- Import the resource to the destination module using
terraform import
We can always be cautious and create a backup of the current state:
$ terraform state pull > terraform.tfstate.prod
And as we already have the resources list in each workspace we can simply remove the resource from the current prod
workspace state:
$ terraform workspace list
default
* prod
test
$ terraform state list
module.file1.local_file.foo
module.nginx1.docker_container.nginx[0]
module.nginx1.docker_image.nginx_image
$ terraform state rm 'module.nginx1.docker_container.nginx[0]'
Removed module.nginx1.docker_container.nginx[0]
Successfully removed 1 resource instance(s).
It’s time to visit the new module! This module only has one file resources/infra-v2/containers.tf
which is basically a copy of resources/infra/containers.tf
as we need the same configuration code in order to import the existing resources. Now all we need is an ID of the resource we want to import — which is the Docker container ID in this case:
$ cd resources/infra-v2
$ terraform init
$ terraform workspace new prod
# You can run `docker ps` with `--no-trunc` to get the full container ID:
$ docker ps -aq --no-trunc -f name=nginx-server-prod-0
fe9b3e0a2a4cd4b19b74aa8dc8934ccc156c18b9e5efef30bdbedb8ac12133c0
$ terraform import 'module.nginx1.docker_container.nginx[0]' fe9b3e0a2a4cd4b19b74aa8dc8934ccc156c18b9e5efef30bdbedb8ac12133c0
module.nginx1.docker_container.nginx[0]: Importing from ID "fe9b3e0a2a4cd4b19b74aa8dc8934ccc156c18b9e5efef30bdbedb8ac12133c0"...
module.nginx1.docker_container.nginx[0]: Import prepared!
Prepared docker_container for import
module.nginx1.docker_container.nginx[0]: Refreshing state... [id=fe9b3e0a2a4cd4b19b74aa8dc8934ccc156c18b9e5efef30bdbedb8ac12133c0]
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.
And we check the current Terraform state:
$ terraform state list
module.nginx1.docker_container.nginx[0]
We can follow the same steps to move the other container nginx-server-test-0
in the test
workspace. Once we have moved all the resources we need, we can delete the old Terraform configuration (e.g. resources/infra/containers.tf
).
And if run terraform plan
in the new module, we see Terraform already knows about the containers and won’t try to add new resources. There’s just an issue with this particular provider that signals Terraform to update the containers when we run terraform plan
which can be ignored.
One final point is that there may be limitations in terms of importing some resources. For instance, we cannot import a Docker image into another module or workspace as the Docker provider does not have import functionality for images and it only supports importing a container. So in our example we let Terraform create the image in the new module and then destroy the resources under infra.
$ terraform import 'module.nginx1.docker_image.nginx_image' 8dd77ef2d82eade8dcf2c08ea032bd9cba04c9d28ace2ccf08ad6804c27bf14f
module.nginx1.docker_image.nginx_image: Importing from ID "8dd77ef2d82eade8dcf2c08ea032bd9cba04c9d28ace2ccf08ad6804c27bf14f"...
╷
│ Error: resource docker_image doesn't support import
│
Recap
This was a simple example using a Terraform provider for Docker to demonstrate how to move Terraform resources from a module to another. In real-life scenarios we may work with Cloud providers such as AWS and updating resources and their state requires extra attention.
We also learned that there may be limitations in terms of importing some resources and we must first check if a provider supports importing a particular resource. For instance, it is possible to import most AWS resources such as an S3 bucket:
$ terraform import aws_s3_bucket.bucket bucket-name
That’s all.
Happy coding!