Managing your on-premises infrastructure with HashiCorp - Part 3
Wed, 18 Sep 2024 16:40:15 -0000
|Read Time: 0 minutes
This is the third blog post in a series in which we are introducing Terraform to infrastructure admins and engineers. We go into depth to explain how to use Terraform to manage on-prem physical infrastructure in a datacenter. It is highly recommended that you quickly review the previous posts before proceeding with this one.
- Terraform on-premises infrastructure
- Terraform Dell PowerStore
- Terraform resource dependencies
- Terraform variables
- Terraform Dell PowerMax
Provisioning storage in any storage platform typically requires creating different resources such as hosts, volumes, and more. The actual resources and their name vary between platforms. Sooner or later, you will face with the problem having dependencies between these resources. For example, before mapping a volume to a host, the host must exist. Terraform might try to do the creation of both resources and the mapping in parallel but it might take the storage array a second to complete the creation of the resources. In that case your “terraform apply” will fail.
For this example, we are going to add a new resource to the Terraform. We will add the “host” resource. You can see what attributes the Terraform provider for PowerStore supports by looking at the documentation page. This is in the “docs” folder in GitHub. This screenshot shows part of the schema of the “volume” resource.
As you can see, in PowerStore you can map a volume to “host” or a “host group”. In both cases, you can use the “id” or the “name” to refer to the target.
This is the portion of a new plan to create both resources. Notice the “host_name” attribute in the “powerstore_volume” resource block. It is specifying that the volume must be mapped to the host specified in the first block.
resource "powerstore_host" "test" {
name = "alb-host"
os_type = "Linux"
description = "Creating host"
host_connectivity = "Local_Only"
initiators = [{ port_name = "12:34:56:78:ab:cd:ef:10"},{ port_name = "12:34:56:78:ab:cd:ef:11"}]
}
resource "powerstore_volume" "volume" {
name = "alb-test-vol"
size = 8
capacity_unit = "GB"
host_name = "alb-host"
}
Let’s see what happens when we apply this plan.
Plan: 2 to add, 0 to change, 0 to destroy.
powerstore_host.test: Creating...
powerstore_volume.volume: Creating...
powerstore_host.test: Creation complete after 1s [id=446594c0-8411-4bdc-9b7f]
╷
│ Error: Error creating volume
│
│ with powerstore_volume.volume,
│ on main.tf line 26, in resource "powerstore_volume" "volume":
│ 26: resource "powerstore_volume" "volume" {
│
│ Could not create volume, Invalid host name
Terraform starts the creation of both resources right away, but the volume creation fails because the host creation hadn’t yet been completed. Because of the declarative and idempotent nature of Terraform, if you run it again, the host resource will be there, and it will complete successfully. Notice how the plan realized that only the volume resource had to be added.
Plan: 1 to add, 0 to change, 0 to destroy.
powerstore_volume.volume: Creating...
powerstore_volume.volume: Creation complete after 2s [id=ea134c40-df5f-470c-bdd2]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
The problem shows up also when destroying resources because dependencies also matter in that case. You cannot delete the host if the un-mapping is not complete.
Plan: 0 to add, 0 to change, 2 to destroy.
powerstore_volume.volume: Destroying... [id=ea134c40-df5f-470c-bdd2-ffd22ae15aa0]
powerstore_host.test: Destroying... [id=446594c0-8411-4bdc-9b7f-9d2434eef32c]
powerstore_volume.volume: Destruction complete after 1s
╷
│ Error: Error deleting host
│
│ Could not delete hostID 446594c0-8411-4bdc-9b7f-9d2434eef32c: Host alb-host is attached to one or more volumes. Please detach it before deleting.
Again, if we retry it will realize the mapping and volume are gone and it will execute a plan to destroy only the host resource. However, clearly, we don’t want to be relying on retries.
Plan: 0 to add, 0 to change, 1 to destroy.
powerstore_host.test: Destroying... [id=446594c0-8411-4bdc-9b7f-9d2434eef32c]
powerstore_host.test: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.
Declaring dependencies
Terraform offers two ways of declaring dependencies: implicit and explicit.
Implicit dependencies
This is the preferred method of declaring dependencies according to Terraform. Implicit dependencies are done by including in a resource block, a reference to another resource, or typically, one of its attributes. Notice how the “host_name” attribute in the “volume” resource block is referring to the name of the “host” created in the first block. It refers to it using the “internal name” of the “powerstore_host” resource, which is “test”. The reference is constructed using a dot notation: resource_type . internal_resource_name . attribute. From this reference, Terraform can realize that the volume depends on the host and, therefore it must wait until the host creation is complete, hence the name “implicit”.
resource "powerstore_host" "test" {
name = "alb-host"
os_type = "Linux"
description = "Creating host"
host_connectivity = "Local_Only"
initiators = [{port_name = "12:34:56:78:ab:cd:ef:10"},{ port_name = "12:34:56:78:ab:cd:ef:11"}]
}
resource "powerstore_volume" "volume" {
name = "alb-test-vol"
size = 8
capacity_unit = "GB"
host_name = powerstore_host.test.name
}
Let’s run it and see what happens.
Plan: 2 to add, 0 to change, 0 to destroy.
powerstore_host.test: Creating...
powerstore_host.test: Creation complete after 1s [id=0ffc844e-d6b1-459f-be17]
powerstore_volume.volume: Creating...
powerstore_volume.volume: Creation complete after 1s [id=5c8ffb25-eeb6-4e1b-a62a]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
In the output you can see the sequence of events. Notice how the volume creation is not started until the host creation is complete. With this technique, it successfully completes every time. The same happens with the “Destroy” operation. It takes the steps in reverse, and it doesn’t start the host deletion until the volume has been destroyed.
Explicit
An explicit dependency is when you explicitly call out a dependency between two resources by using the “depends_on” attribute. In the code below, notice the last line where it states that the volume depends on the host creation. As with the earlier examples, you must specify the resource using the internal resource name, which is “test” in this case. Also, it is important to note that the dependency is enclosed in square brackets “[]”. This tells us that this attribute is a list and therefore allows you to put multiple dependencies separated by commas.
resource "powerstore_host" "test" {
name = "alb-host"
os_type = "Linux"
description = "Creating host"
host_connectivity = "Local_Only"
initiators = [{ port_name = "12:34:56:78:ab:cd:ef:10"},{ port_name = "12:34:56:78:ab:cd:ef:11"}]
}
resource "powerstore_volume" "volume" {
name = "alb-test-vol"
size = 8
capacity_unit = "GB"
host_name = "alb-host"
depends_on = [powerstore_host.test]
}
Let’s run it and see what happens.
Plan: 2 to add, 0 to change, 0 to destroy.
powerstore_host.test: Creating...
powerstore_host.test: Creation complete after 2s [id=afd95a69-0cb3-4875-99c5]
powerstore_volume.volume: Creating...
powerstore_volume.volume: Creation complete after 1s [id=0fa06428-3008-4b1d-9b9e]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
As you can see it behaves exactly as it did with the “implicit” dependency and waits until the host is created before the volume creation starts. So, this is another valid way of doing it, but remember that “implicit” declaration is Terraform’s preferred method.
Terraform Graph
In addition to init, plan, apply and destroy, the terraform tool supports other modes of operation. A handy one is “terraform graph” which allows us to visualize the dependencies. The following shows output when we run it for our existing terraform configuration file. It looks a bit complex at first sight, but you can quickly see that it is defining two nodes in a graph, with their respective labels and then it tells us that the volume depends on the host based on the direction of the arrow.
root@alb-terraform:~/powerstore# terraform graph
digraph G {
rankdir = "RL";
node [shape = rect, fontname = "sans-serif"];
"powerstore_host.test" [label="powerstore_host.test"];
"powerstore_volume.volume" [label="powerstore_volume.volume"];
"powerstore_volume.volume" -> "powerstore_host.test";
}
That output by itself can be very hard to read when you have a configuration file with many resources. However, there are tools that allow you to create an actual image of the graph. One such tool is the “dot” command line utility from Graphviz. This is how you install it and how you run it to create a graphical representation of your resources and their dependencies.
apt install graphviz
terraform graph | dot -Tpng >graph.png
This is what the “graph.png” image looks like. It shows both resources and how the volume depends on the host.
Terraform graphs can also be used to graph the entire plan, which includes other information such as the provider and variables.
root@alb-terraform:~/powerstore# terraform graph -type=plan
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] powerstore_host.test (expand)" [label = "powerstore_host.test", shape = "box"]
"[root] powerstore_volume.volume (expand)" [label = "powerstore_volume.volume", shape = "box"]
"[root] provider[\"registry.terraform.io/dell/powerstore\"]" [label = "provider[\"registry.terraform.io/dell/powerstore\"]", shape = "diamond"]
"[root] var.endpoint" [label = "var.endpoint", shape = "note"]
"[root] var.password" [label = "var.password", shape = "note"]
"[root] var.timeout" [label = "var.timeout", shape = "note"]
"[root] var.username" [label = "var.username", shape = "note"]
"[root] powerstore_host.test (expand)" -> "[root] provider[\"registry.terraform.io/dell/powerstore\"]"
"[root] powerstore_volume.volume (expand)" -> "[root] powerstore_host.test (expand)"
"[root] provider[\"registry.terraform.io/dell/powerstore\"] (close)" -> "[root] powerstore_volume.volume (expand)"
"[root] provider[\"registry.terraform.io/dell/powerstore\"]" -> "[root] var.endpoint"
"[root] provider[\"registry.terraform.io/dell/powerstore\"]" -> "[root] var.password"
"[root] provider[\"registry.terraform.io/dell/powerstore\"]" -> "[root] var.timeout"
"[root] provider[\"registry.terraform.io/dell/powerstore\"]" -> "[root] var.username"
"[root] root" -> "[root] provider[\"registry.terraform.io/dell/powerstore\"] (close)"
}
}
root@alb-terraform:~/powerstore# terraform graph -type=plan | dot -Tpng >graph2.png
In the last command, we passed the output through the “dot” tool and created a new image called “graph2.png”. This is what it looks like.
Hopefully this post helps you understand Terraform dependencies.
What we have done so far is great, but we keep creating and changing the same volume because it is hard-coded in our HCL configuration file. It would be better to treat parameters like name and size as variables so that we can make our code more flexible. Let’s explore that in the next post.
Resources
- Part 1: Managing your on-premises infrastructure with HashiCorp Terraform | Dell Technologies Info Hub
- Part 2: Managing your on-premises infrastructure with HashiCorp Terraform | Dell Technologies Info Hub
Author: Alberto Ramos, Principal Systems Engineer