We use infrastructure as code to configure and provision cloud infrastructure, which consists of different resources connected in various ways:
- We set up virtual networks and partition them into multiple subnets.
- We configure network security groups, each with multiple different rules allowing or denying traffic to or from other security groups.
- We provision Kubernetes clusters with associated private container registries, workloads, load balancers, networking, storage, and identities.
- And much more.
The cloud infrastructure that we provision seldom lives in complete isolation. Resources are connected through implicit and explicit dependencies between them.
In Terraform, you can express dependencies between resources in several different ways. In this blog post, we will explore how Terraform resource dependencies work, together with examples and best practices for managing resource dependencies.
- What are Terraform dependencies?
- What are Terraform resource dependencies?
- Types of Terraform resource dependencies
- Best practices for Terraform resource dependencies
What are Terraform dependencies?
Terraform dependencies are the relationships between resources or modules that determine the order in which they are created, updated, or destroyed. Dependencies ensure that Terraform provisions infrastructure components in a safe and logical sequence. Proper dependency management is critical to avoid race conditions and ensure reliable provisioning in complex infrastructure setups.
Resource dependencies describe how different resources and data sources in your Terraform configurations are related to each other.
Take a look at the following two small Terraform configurations using the AzureRM provider. The first Terraform configuration looks like this:
resource "azurerm_resource_group" "default" {
name = "rg-spacelift-resources"
location = "swedencentral"
}
resource "azurerm_storage_account" "backup" {
name = "stspaceliftbackup"
resource_group_name = azurerm_resource_group.default.name
location = azurerm_resource_group.default.location
account_tier = "Standard"
account_replication_type = "LRS"
}
The second Terraform configuration looks like this:
resource "azurerm_resource_group" "default" {
name = "rg-spacelift-resources"
location = "swedencentral"
}
resource "azurerm_storage_account" "backup" {
name = "stspaceliftbackup"
resource_group_name = "rg-spacelift-resources"
location = "swedencentral"
account_tier = "Standard"
account_replication_type = "LRS"
}Both Terraform configurations look similar at first glance. Each includes an Azure resource group containing an Azure storage account (blob and file storage service).
The difference between the two configurations is in how the values to the resource_group_name and location attributes of the storage account are set.
In the first example, we use references to attributes of the resource group, while in the second example we use static strings. Both the references and the static strings result in the same value.
If you try to provision both of the Terraform configurations, you will find that the second example will fail. This is because we have not properly told Terraform how the two resources are related to each other.
It is important to properly configure resource dependencies to tell Terraform in which order to create, update, and destroy resources. If Terraform does not know there is a dependency between two resources, it will attempt to create both resources in parallel, which usually leads to unpredictable errors.
A storage account on Azure must be placed in a resource group, but if that resource group does not exist when you create the storage account, an error will occur.
Resource dependencies describe relationships between resources and data sources in your Terraform configurations. Terraform uses resource dependencies to build the dependency graph that it uses during terraform apply to create and update resources in the proper order.
Some dependencies will appear in a single Terraform configuration and across multiple Terraform configurations.
These are:
- Implicit dependencies
- Explicit dependencies
- Dependencies across Terraform configurations
The following sections explain each type of dependency and provide examples of how they work.
Implicit dependencies in Terraform
An implicit dependency in a Terraform configuration occurs when attributes from one resource or data source are referenced in the configuration of a different resource or data source.
We saw an example of this in the previous section:
resource "azurerm_resource_group" "default" {
name = "rg-spacelift-resources"
location = "swedencentral"
}
resource "azurerm_storage_account" "backup" {
name = "stspaceliftbackup"
# reference the resource group name
resource_group_name = azurerm_resource_group.default.name
# reference the resource group location
location = azurerm_resource_group.default.location
account_tier = "Standard"
account_replication_type = "LRS"
}Implicit dependencies are the preferred way of handling dependencies between Terraform resources.
Explicit dependencies in Terraform
An explicit dependency in a Terraform configuration is when you specify how two resources are related using the depends_on meta argument. Explicit dependencies allow you to describe relationships between resources that Terraform could not know about organically.
An explicit dependency is required if you are provisioning an AWS S3 bucket together with an AWS EC2 instance. In the instance userdata script, you use the AWS CLI to read the properties of the S3 bucket and perform some actions with it.
Without adding an explicit dependency, you might try to write your configuration like this:
resource "aws_s3_bucket" "spacelift" {
bucket_prefix = "spacelift-"
}
resource "aws_instance" "spacelift" {
# aws_ami data source omitted for brevity
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
user_data_base64 = file("${path.module}/scripts/instance.sh")
}The userdata script has no implicit reference to the aws_s3_bucket resource (it is just a static script), yet there is a hidden dependency because the script assumes the S3 bucket exists.
To tell Terraform about this hidden dependency, we can add an explicit dependency using depends_on to the aws_instance resource:
resource "aws_instance" "spacelift" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
user_data_base64 = file("${path.module}/scripts/instance.sh")
# add an explicit dependency
depends_on = [
aws_s3_bucket.spacelift,
]
}Now Terraform will create the S3 bucket first, and once it is provisioned it will move on to provision the EC2 instance.
The depends_on meta-argument is an array and you can add multiple resources if a resource depends on many other resources. A pseudo example of what this looks like:
resource "dependee" "a" { … }
resource "dependee" "b" { … }
resource "dependee" "c" { … }
resource "dependent" "x" {
depends_on = [
dependee.a,
dependee.b,
dependee.c,
]
}Dependencies across Terraform configurations
The third common example of dependencies are dependencies across different Terraform configurations.
You have various options for handling these types of dependencies. The simplest method is to configure a variable in your Terraform configuration that takes the attribute value of a dependent resource.
If you have a Terraform configuration that needs the ID of an AWS VPC resource created somewhere else you can add a vpc_id variable to it:
variable "vpc_id" {
type = string
description = "AWS VPC ID for shared networking"
}
resource "aws_subnet" "web" {
# use the variable to add a subnet to the VPC
vpc_id = var.vpc_id
cidr_block = "10.100.100.0/24"
}Another common approach is to expose a resource attribute as an output in one Terraform configuration and read the output in a different Terraform configuration. For this, you need to use remote state storage, and both Terraform configurations must have access to the same state file.
In the producer Terraform configuration, we output the value of a VPC ID (the following code block is truncated for brevity):
terraform {
backend "s3" {
bucket = "spacelift"
key = "producer/terraform.tfstate"
region = "eu-west-1"
}
}
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
}
output "vpc_id" {
value = aws_vpc.default.id
}In the consumer Terraform configuration, we use the terraform_remote_state data source to read the state of the producer configuration and specifically reference the VPC ID output:
data "terraform_remote_state" "producer" {
backend = "s3"
config = {
bucket = "spacelift"
key = "producer/terraform.tfstate"
region = "eu-west-1"
}
}
resource "aws_subnet" "web" {
# reference the vpc_id output from the producer state file
vpc_id = data.terraform_remote_state.producer.outputs.vpc_id
cidr_block = "10.0.100.0/24"
}A third option for dependencies between different Terraform configurations involves using platform-specific features.
On Spacelift, you can set up stack dependencies to manage how different stacks are related to each other. You can also pass data between the stacks. Apart from sharing data (e.g., VPC IDs) between stacks, you also make sure related stacks are triggered and updated in the right order if needed.
Similarly, on HCP Terraform (formerly Terraform Cloud) you can read data from one workspace in a different workspace using the tfe_outputs data source from the TFE provider (Terraform Enterprise). You can combine this feature with run triggers to automatically trigger dependent workspaces when an upstream output changes.
Sharing data using platform-specific features is the preferred way of handling resource dependencies across different Terraform configurations.
Resource dependencies are a necessary evil when working with Terraform, or infrastructure as code in general. Here are best practices to keep in mind when working with resource dependencies:
1. Minimize the number of dependencies for efficiency reasons
Introducing Terraform resource dependencies increases the amount of time it takes for Terraform to provision or update all resources in a given Terraform configuration.
When there are dependencies between resources, Terraform will create the resources in sequential order. When there are no dependencies between resources, Terraform will create the resources in parallel.
Some dependencies can’t be avoided (e.g., anything on Azure must be placed in a resource group, so you must create the resource group first). You should not introduce explicit resource dependencies that are not necessary (e.g,. creating two independent Kubernetes clusters should be done in parallel).
2. Keep dependencies between different Terraform configurations to a minimum
Introducing many dependencies between different Terraform configurations means you will need to coordinate updates to dependent resources for all Terraform configurations. This can quickly become complicated. For this reason, you should try to minimize the amount of dependencies across Terraform configurations.
Dependencies between different Terraform configurations should primarily involve resources with long lifecycles (shared VPC networks, shared firewalls, shared Kubernetes clusters, etc). Avoid configuring dependencies on resources with short lifecycles that are updated and possibly replaced often.
To simplify management of dependencies between Terraform configurations, you can use platform-specific features such as stack dependencies on Spacelift.
3. Avoid sharing state using the terraform_remote_state data source
A common option for sharing state between different Terraform configurations involves the terraform_remote_state data source. Sometimes this is the only reasonable option you have, but you should consider alternatives if they are available.
This is because the producer Terraform configuration must provideread access to its state file for all consumer Terraform configurations. The state file can contain sensitive information, so it is unwise to provide full read access to share a few output values.
4. Use implicit dependencies when possible
Use implicit dependencies in your Terraform configuration, if possible. This allows you to utilize Terraform to make sure resources are constructed in the correct order. Explicit dependencies require you to decide the order in which resources are created, updated, and destroyed.
5. Avoid circular dependencies
It is surprisingly easy to accidentally introduce circular dependencies in your Terraform configurations.
One common situation is when you start with a large root module with many resources and later refactor this configuration into separate modules. During the refactoring, you may inadvertently introduce circular dependencies that are not apparent as long as the existing infrastructure is not destroyed.
However, if you try to set everything up from scratch, this will become apparent.
Another situation where this is common is when you use hardcoded values of IDs or other important values that really should be references to resource attributes (i.e., implicit references).
6. Use the Terraform graph command to visualize dependencies
To visualize what Terraform knows about your resource dependencies, you can use the terraform graph CLI command.
This CLI command outputs a graph using the DOT format. Use the Graphviz tool to convert this format into a PNG image; see the Graphviz documentation for how to install this tool on your system.
As an example, consider the following Terraform configuration that includes an AWS VPC with three subnets and outputs the VPC ID (some of the code is omitted for brevity):
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "frontend" {
vpc_id = aws_vpc.default.id
cidr_block = "10.0.100.0/24"
}
resource "aws_subnet" "backend" {
vpc_id = aws_vpc.default.id
cidr_block = "10.0.110.0/24"
}
resource "aws_subnet" "db" {
vpc_id = aws_vpc.default.id
cidr_block = "10.0.120.0/24"
}
output "vpc_id" {
value = aws_vpc.default.id
}Run the following terraform graph command in the directory of your root module to create a PNG rendition of the dependency graph:
$ terraform graph -type=plan | dot -Tpng > graph.pngThe resulting PNG image looks like this:
The arrows in the image indicate where you have dependencies and in which order Terraform will create and update your resources.
The three aws_subnet resources all point at the aws_vpc resource. The same is true for the vpc_id output. This means that the aws_vpc resource is created first, followed by the three subnets and the output that are created in parallel.
The image also includes some additional details about the providers used and how resources depend on the specific provider instances.
Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs
- Build self-service infrastructure – You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to Integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. For more information on configuring private workers, refer to the documentation.
Spacelift can also optionally manage the Terraform state for you, offering a backend synchronized with the rest of the platform to maximize convenience and security. You can also import your state during stack creation, which is very useful for engineers who are migrating their old configurations and states to Spacelift.
For more information, refer to this blog post, which shows in detail Spacelift’s remote state capabilities.
Check Spacelift for free by creating a trial account or booking a demo with one of our engineers.
Your Terraform configurations will always contain different types of resource dependencies.
- An implicit dependency is when a resource or data source references attributes of another resource or data source. With an implicit dependency, you tell Terraform that there is a dependency between these two resources.
- An explicit dependency is when you tell Terraform that one resource or data source depends on a different resource or data source through the use of the
depends_onmeta-argument. In thedepends_onmeta-argument you provide a list of resource addresses. Terraform will make sure that all resources listed in depends_on are created or updated before it creates or updates the resource where you configureddepends_on. - Apart from dependencies in a single Terraform configuration you can also have dependencies across different Terraform configurations. Here are your options for handling these dependencies:
- Use variables to get the required values as input to your Terraform configurations.
- Expose resource attributes as output values in a producer Terraform configuration and read these attributes from a consumer configuration using the
terraform_remote_statedata source. - Use platform-specific features to configure dependencies between Terraform configurations (e.g. stack dependencies on Spacelift).
Keep dependencies between different Terraform configurations to a minimum and favor the use of implicit dependencies in your Terraform configurations.
Visualize what Terraform knows about your resource dependencies using the terraform graph CLI command, this can be useful for debugging purposes.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Manage Terraform better with Spacelift
Build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.
Frequently asked questions
What’s the difference between implicit and explicit dependencies in Terraform?
In Terraform, explicit dependencies are defined manually using the depends_on argument, while implicit dependencies are inferred automatically based on resource references.
What are Terraform module dependencies?
Terraform module dependencies refer to the order in which Terraform provisions resources across modules, based on how outputs and inputs are linked. A module depends on another when it consumes its outputs as inputs, establishing an implicit dependency.
Should I use depends_on on modules?
In most cases, you should not use depends_on with modules. It is primarily intended for resources, not modules, and its behavior with modules is limited and potentially unreliable.
If you need to enforce a dependency between resources in separate modules, it’s preferable to pass outputs from one module as inputs to the other. This creates an implicit dependency and ensures correct ordering based on data flow.
Can lifecycle arguments replace dependencies?
No, lifecycle arguments in Terraform cannot replace dependencies. Lifecycle settings like create_before_destroy or ignore_changes control resource behavior during updates and deletions, but they do not establish or manage resource ordering. Dependencies, whether implicit (via references) or explicit (using depends_on), ensure Terraform builds resources in the correct order.
How do I visualize the dependency graph?
You can visualize the dependency graph in Terraform using the command:
terraform graph | dot -Tpng > graph.png
This generates a DOT-format graph of resources and their relationships, which you can convert into an image with Graphviz.
