Layered Runs
In complex infrastructure setups, certain Terraform operations cannot be executed in a single step. Instead, they require multiple plan and apply cycles across different layers of your infrastructure. Terrateam’s Layered Runs feature allows you to define these dependencies and execute operations in the correct order, ensuring that each layer is applied only after its dependencies have been successfully applied.
Understanding Layered Runs
Layered Runs are designed to handle scenarios where infrastructure components are interdependent. For example, a network must be established before deploying a database that relies on that network, and the database must be set up before an application can connect to it. These layers must be applied in sequence to avoid errors and ensure a stable deployment.
Terrateam helps manage these dependencies through the depends_on
configuration, which specifies the relationship between different directories or environments in your repository.
How It Works
The depends_on
key within the when_modified
configuration allows you to specify dependencies between different layers of your infrastructure. When changes are detected in a lower layer, Terrateam ensures that the corresponding plan and apply operations are executed first. If the apply operation is successful, the next layer’s plan and apply operations are triggered.
The following code block is a configuration snippet to be placed within your .terrateam/config.yml
file:
dirs: network: when_modified: file_patterns: ["${DIR}/*.tf"] database: when_modified: depends_on: 'dir:network' file_patterns: ["${DIR}/*.tf"] application: when_modified: depends_on: 'dir:database' file_patterns: ["${DIR}/*.tf"]
This configuration ensures that:
- If the
network
layer is modified, it will be planned and applied first. - After the
network
apply is successful, thedatabase
layer is then planned and applied. - Finally, if the
database
layer is successfully applied, theapplication
layer is planned and applied.
flowchart LR network["network"] database["database"] application["application"] network --> database database --> application
Advanced Dependency Configuration
The depends_on
key in your when_modified
configuration allows for complex dependency management by using logical operators with tag queries, and directory references. Understanding these components will help you set up more flexible and powerful configurations.
Logical Operators
Logical operators such as or
and and
can be used to combine multiple dependencies within the depends_on
key. This allows you to specify conditions where changes in any one of several directories will trigger the necessary plan and apply operations. See the tag queries documentation for details.
Example snippet for .terrateam/config.yml
:
when_modified: depends_on: 'dir:network or dir:database'
In this example, changes to either the network
or database
directory will trigger the operation.
Directory References
dir:
This prefix is used to define an absolute path to a directory within your repository. It is useful when you have a clear and fixed structure for your directories.
when_modified: depends_on: 'dir:network'
relative_dir:
This prefix allows you to specify a path relative to the current directory. It is particularly useful when your directory structure might change, or when you need to maintain flexibility in your dependency definitions.
when_modified: depends_on: 'relative_dir:../network'
Use Cases
Sequential Infrastructure Deployment
In scenarios where your infrastructure is composed of multiple interdependent layers (e.g., network, database, application), it is crucial to ensure that these layers are deployed in the correct sequence. This is necessary both when rebuilding the entire infrastructure from scratch (e.g., in a disaster recovery situation) and when making ongoing modifications to any of the layers.
Example snippet for .terrateam/config.yml
:
dirs: network: when_modified: file_patterns: ["${DIR}/*.tf"] database: when_modified: depends_on: 'dir:network' file_patterns: ["${DIR}/*.tf"] application: when_modified: depends_on: 'dir:database' file_patterns: ["${DIR}/*.tf"]
flowchart LR network["network"] database["database"] application["application"] network --> database database --> application
Complex Dependency Management with Multiple Layers
In more advanced setups, you might have multiple layers that depend on one or more preceding layers. For example, consider a scenario where two application environments (app1
and app2
) both depend on a shared database
, which in turn depends on a network
layer. Changes to the network
layer should trigger updates to both database
and subsequently both app1
and app2
.
Example snippet for .terrateam/config.yml
:
dirs: network: when_modified: file_patterns: ["${DIR}/*.tf"] database: when_modified: depends_on: 'dir:network' file_patterns: ["${DIR}/*.tf"] app1: when_modified: depends_on: 'dir:database' file_patterns: ["${DIR}/*.tf"] app2: when_modified: depends_on: 'dir:database' file_patterns: ["${DIR}/*.tf"] shared_resources: when_modified: depends_on: 'dir:network or dir:database' file_patterns: ["${DIR}/*.tf"]
flowchart TD network["network"] database["database"] app1["app1"] app2["app2"] shared_resources["shared_resources"] network --> database database --> app1 database --> app2 network --> shared_resources database --> shared_resources
Maximizing Reusable Configuration
By structuring the repository such that environments follow a common pattern, the Terrateam configuration can be simplified such that each layer is defined once.
For example, the following repository shows four environments, asia
, europe
, us-east
, and us-west
. Inside each of these directories are the services: application
, database
, networking
, and block_storage
.
.└── envs ├── asia │ ├── application │ │ └── main.tf │ ├── database │ │ └── main.tf │ └── networking │ └── main.tf ├── europe │ ├── application │ │ └── main.tf │ ├── block_storage │ │ └── main.tf │ ├── database │ │ └── main.tf │ └── networking │ └── main.tf ├── us-east │ ├── application │ │ └── main.tf │ ├── database │ │ └── main.tf │ └── networking │ └── main.tf └── us-west ├── application │ └── main.tf ├── block_storage │ └── main.tf ├── database │ └── main.tf └── networking └── main.tf
We define the layers to be from bottom to top:
networking
database
andblock_storage
application
Example snippet for .terrateam/config.yml
:
dirs: 'envs/*/database': when_modified: depends_on: 'relative_dir:../networking' 'envs/*/block_storage': when_modified: depends_on: 'relative_dir:../networking' 'envs/*/application': when_modified: depends_on: 'relative_dir:../database or relative_dir:../block_storage'
flowchart TD networking["networking"] database["database"] block_storage["block_storage"] application["application"] networking --> database networking --> block_storage database --> application block_storage --> application
That is, if envs/asia/database
changes, then the envs/asia/application
layer will run. If us-west/networking
changes then the us-west/database
and us-west/block_storage
layers will be triggered, followed by us-west/application
.
Structuring the repository this way allows for a single configuration to apply to every environment. Note that not all environments have the same services, only two of them have block_storage
, but that is not a problem because the configuration will only apply to those services that exist.
Best Practices
- Ensure that the
depends_on
relationships between layers are well-defined and reflect the true dependencies in your infrastructure. This helps avoid circular dependencies and ensures a smooth execution flow. - Utilize tag queries in conjunction with
depends_on
to target specific directories or environments, ensuring that only the necessary layers are triggered for a plan or apply operation. - Structure your infrastructure into distinct, modular layers that can be managed independently. This modularity simplifies dependency management and allows for more granular control over Terraform operations.