I'm trying to define a module testing framework for terraform and my approach is to use Pester, called from a local-exec provisioner in order to verify build is correct.
To this end I was hoping to be able to use output from the module, e.g:
output "windows_ip_address" {
value = module.windowsservers.network_interface_private_ip
}
... as an input for a local-exec provisioner. e.g:
module "windowsservers" {
source = "../../"
vm_hostname = "host${random_id.ip_dns.hex}-windows" // line can be removed if only one VM module per resource group
resource_group_name = azurerm_resource_group.test.name
is_windows_image = true
admin_username = var.admin_username
admin_password = var.admin_password
vm_os_simple = "WindowsServer"
vnet_subnet_id = azurerm_subnet.subnet1.id
}
resource "null_resource" "run-pestertest" {
provisioner "local-exec" {
#command = "..\\test_azurerm_compute.ps1 -vmhostname test -vmip ${module.windowsservers.network_interface_private_ip}"
command = "echo ${module.windowsservers.network_interface_private_ip}"
interpreter = ["pwsh", "-Command"]
}
depends_on = [module.windowsservers]
triggers = {
always_run = "${timestamp()}"
}
}
...but i'm getting:
Error: Invalid template interpolation value: Cannot include the given value in a string template: string required.
I thought by using depends_on i'd be able to force terraform to graph it out in such a way that the "windowsserver" module would be inacted prior to null_resource - but I think maybe there is something fundamentally incorrect with what i'm doing!
Thanks
Dan
I apologize if this is a silly question, but have you verified the module output you want to use (module.windowsservers.network_interface_private_ip) is in fact typed as a string? Perhaps it's a list, or something else .. You can try "forcing" it to be a string in a locals block and see if that either fixes the error or changes it to indicate perhaps the output type isn't actually a string ..
locals = {
module_private_ip = "${tostring(module.windowsservers.network_interface_private_ip)}"
}
I only mention the locals block because it looks like you use it in multiple places, and using the locals means only one place it's used, and once place that could be spitting out the error about invalid type.
I've also used the locals block as a trick to deal with dependencies between modules as TF doesn't always seem to handle that well..
and I apologize for posting as an "answer", but I don't have the karma to post comments yet :)
Related
Here is my shortened code:
variable.tf:
variable "vm_avd" {
type = map(any)
default = {
avd-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
avd-gpu = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
variable "vm_server" {
type = map(any)
default = {
srv-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
if.tf:
resource "azurerm_network_interface" "if" {
count = var.vm_count
name = var.private_ip_address_allocation == "Static" ? "if_${var.if_name}_${replace(var.private_ip_address, ".", "")}_${format("%02d", count.index)}" : "if_${var.if_name}_${format("%02d", count.index)}"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "if_ipconfig"
subnet_id = var.subnet_id
private_ip_address_version = var.private_ip_address_version
private_ip_address_allocation = var.private_ip_address_allocation
private_ip_address = var.private_ip_address
}
}
output "if_name" {
value = azurerm_network_interface.if[*].name
}
output "if_id" {
value = azurerm_network_interface.if[*].id
}
main.tf:
module "vm" {
for_each = merge(var.vm_server, var.vm_avd)
vm_name = each.key
vm_count = each.value["vm_count"]
network_interface_ids = module.if[each.key].if_id
}
module "if" {
for_each = merge(var.vm_server, var.vm_avd)
vm_count = each.value["vm_count"]
if_name = each.key
subnet_id = module.snet[each.value["subnet_id"]].snet_id
}
Is there a possibility to reference in a for_each loop the correct count object for dependent resources?
I'm creating Terraform modules for Azure and I want to build a structure that is highly flexible and easy to fill for everyone.
Therefore I build a root file with a single module call for each resource and I have a single main variables file which should be filled with values which are looped through.
All related resource variables are defined in the same variables object, so for a VM a variable VM is created and filled with VM details, NIC details and extension details. The single modules run through the same variables for VM, NIC and extensions which is, in general, running fine.
I can reference the subnet_id for the network interfaces nicely but as soon as I use a count operator in the resource I don't know how the correctly get the network interface ID of the correct interface for the correct VM.
Now I ran into the problem that I use for_each in my module call and I want to have the option to use count in my resource definition too. It is working but when I build e.g. virtual machines, I get the problem that I cannot reference to the correct network interfaces.
Atm all my interfaces are connected to the same VM and the 2nd VM cannot be build. Same goes for extensions and any possible multiple objects tho.
I tried several module calls but the first VM got all the interfaces every time. I tried with the Star character in the NIC module call or with the network interface ID.
If this is not possible at all, I thought of a solution with building the network interface ID itself with its original form.
I also checked if there is a possibility to build an array with the for_each and count elements but I couldn't find any way to build arrays out of number variables in terraform, like the normal "for 1 to 5 do".
If you know of any way to do this, I would be grateful too.
Maybe it is a typo when you try to simplify the question, but in your variables you have:
variable vm_avd { default = { avd-std = {...}} }
variable vm_server { default = { avd-std = {...}} }
in your main tf you have:
merge(var.vm_server, var.vm_avd)
since the key is the same, one will override the other, if you are using the default value, it might result in the behaviour you are suggesting, consider changing the key name to server-std?
I got the solution with simply providing the whole element array, e.g. from VM, to the variable for the ID and the reference the correct VM with the count index:
virtual_machine_id = var.vm_id[count.index]
As i run through the same Count in other ressources, I get the correct order of the elements for referencing :)
I am trying to create Ansible inventory file using local_file function in Terraform (I am open for suggestions to do it in a different way)
module "vm" config:
resource "azurerm_linux_virtual_machine" "vm" {
for_each = { for edit in local.vm : edit.name => edit }
name = each.value.name
resource_group_name = var.vm_rg
location = var.vm_location
size = each.value.size
admin_username = var.vm_username
admin_password = var.vm_password
disable_password_authentication = false
network_interface_ids = [azurerm_network_interface.edit_seat_nic[each.key].id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
output "vm_ips" {
value = toset([
for vm_ips in azurerm_linux_virtual_machine.vm : vm_ips.private_ip_address
])
}
When I run terraform plan with the above configuration I get:
Changes to Outputs:
+ test = [
+ "10.1.0.4",
]
Now, in my main TF I have the configuration for local_file as follows:
resource "local_file" "ansible_inventory" {
filename = "./ansible_inventory/ansible_inventory.ini"
content = <<EOF
[vm]
${module.vm.vm_ips}
EOF
}
This returns the error below:
Error: Invalid template interpolation value
on main.tf line 92, in resource "local_file" "ansible_inventory":
90: content = <<EOF
91: [vm]
92: ${module.vm.vm_ips}
93: EOF
module.vm.vm_ips is set of string with 1 element
Cannot include the given value in a string template: string required.
Any suggestion how to inject the list of IPs from the output into the local file while also being able to format the rest of the text in the file?
If you want the Ansible inventory to be statically sourced from a file in INI format, then you basically need to render a template in Terraform to produce the desired output.
module/templates/inventory.tmpl:
[vm]
%{ for ip in ips ~}
${ip}
%{ endfor ~}
alternative suggestion from #mdaniel:
[vm]
${join("\n", ips)}
module/config.tf:
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/templates/inventory.tmpl",
{ ips = module.vm.vm_ips }
)
filename = "${path.module}/ansible_inventory/ansible_inventory.ini"
file_permission = "0644"
}
A couple of additional notes though:
You can modify your output to be the entire map of objects of exported attributes like:
output "vms" {
value = azurerm_linux_virtual_machine.vm
}
and then you can access more information about the instances to populate in your inventory. Your templatefile argument would still be the module output, but the for expression(s) in the template would look considerably different depending upon what you want to add.
You can also utilize the YAML or JSON inventory formats for Ansible static inventory. With those, you can then leverage the yamldecode or jsondecode Terraform functions to make the HCL2 data structure transformation much easier. The template file would become a good bit cleaner in that situation for more complex inventories.
I'm new at terraform and I created a custom azure policies on module structure.
each policy represents a custom module.
One of the modules that I have created is enabling diagnostics logs for any new azure resource created.
but, I need a storage account for that. (before enabling the diagnostics settings how can I implement "depends_on"? or any other methods?
I want to create first the storage account and then the module of diagnostics settings.
on the main.tf (where calling all the other modules) or inside the resource (module)?
Thanks for the help!! :)
this below code represents the main.tf file:
//calling the create storage account name
module "createstorageaccount" {
source = "./modules/module_create_storage_account"
depends_on = [
"module_enable_diagnostics_logs"
]
}
this one represents the create storage account module
resource "azurerm_resource_group" "management" {
name = "management-rg"
location = "West Europe"
}
resource "azurerm_storage_account" "test" {
name = "diagnostics${azurerm_resource_group.management.name}"
resource_group_name = "${azurerm_resource_group.management.name}"
location = "${azurerm_resource_group.management.location}"
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
environment = "diagnostics"
}
}
depends_on = [
"module_enable_diagnostics_logs"
]
In most cases, the necessary dependencies just occur automatically as a result of your references. If the configuration for one resource refers directly or indirectly to another, Terraform automatically infers the dependency between them without the need for explicit depends_on.
This works because module variables and outputs are also nodes in the dependency graph: if a child module resource refers to var.foo then it indirectly depends on anything that the value of that variable depends on.
For the rare situation where automatic dependency detection is insufficient, you can still exploit the fact that module variables and outputs are nodes in the dependency graph to create indirect explicit dependencies, like this:
variable "storage_account_depends_on" {
# the value doesn't matter; we're just using this variable
# to propagate dependencies.
type = any
default = []
}
resource "azurerm_storage_account" "test" {
name = "diagnostics${azurerm_resource_group.management.name}"
resource_group_name = "${azurerm_resource_group.management.name}"
location = "${azurerm_resource_group.management.location}"
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
environment = "diagnostics"
}
# This resource depends on whatever the variable
# depends on, indirectly. This is the same
# as using var.storage_account_depends_on in
# an expression above, but for situations where
# we don't actually need the value.
depends_on = [var.storage_account_depends_on]
}
When you call this module, you can set storage_account_depends_on to any expression that includes the objects you want to ensure are created before the storage account:
module "diagnostic_logs" {
source = "./modules/diagnostic_logs"
}
module "storage_account" {
source = "./modules/storage_account"
storage_account_depends_on = [module.diagnostic_logs.logging]
}
Then in your diagnostic_logs module you can configure indirect dependencies for the logging output to complete the dependency links between the modules:
output "logging" {
# Again, the value is not important because we're just
# using this for its dependencies.
value = {}
# Anything that refers to this output must wait until
# the actions for azurerm_monitor_diagnostic_setting.example
# to have completed first.
depends_on = [azurerm_monitor_diagnostic_setting.example]
}
If your relationships can be expressed by passing actual values around, such as by having an output that includes the id, I'd recommend preferring that approach because it leads to a configuration that is easier to follow. But in rare situations where there are relationships between resources that cannot be modeled as data flow, you can use outputs and variables to propagate explicit dependencies between modules too.
module dependencies are now supported in Terraform 13, this is currently at the release candidate stage.
resource "aws_iam_policy_attachment" "example" {
name = "example"
roles = [aws_iam_role.example.name]
policy_arn = aws_iam_policy.example.arn
}
module "uses-role" {
# ...
depends_on = [aws_iam_policy_attachment.example]
}
Using depends_on at resource level is different from using depends_on at inter-module level i found very simple way to do to it at module level
module "eks" {
source = "../modules/eks"
vpc_id = module.vpc.vpc_id
vpc_cidr = [module.vpc.vpc_cidr_block]
public_subnets = flatten([module.vpc.public_subnets])
private_subnets_id = flatten([module.vpc.private_subnets])
depends_on = [module.vpc]
}
i created dependencies directly with module simple as simplest no complex relation reequired
I have an aws instance defied like so
resource "aws_instance" "an_instance" {
count = "${var.instance_count}"
......
}
which works just fine, BUT when I add this snippet
resource "aws_ebs_volume" "on_host_1_1" {
availability_zone = "${aws_instance.an_instance[1].availability_zone}"
snapshot_id = "snap-abcdca8ee59112345f"
tags = "${local.all_tags}"
}
I get the following error:
Error reading config for aws_ebs_volume[on_host_1_1]: parse error at 1:31: expected "}" but found "."
Any ideas what is wrong?
Terraform v0.11.14
+ provider.aws v2.25.0
You need to use the proper syntax for referencing a specific element of a list. You can see the documentation here. Specifically note the section mentioning:
To reference a particular instance of a resource you can use resource.foo.*.id[#] where # is the index number of the instance.
Therefore, your resource with the proper syntax would be:
resource "aws_ebs_volume" "on_host_1_1" {
availability_zone = "${aws_instance.an_instance.*.availability_zone[1]}"
snapshot_id = "snap-abcdca8ee59112345f"
tags = "${local.all_tags}"
}
which will give you the behavior you desire. The reason this works is because the splat operator * properly signifies to Terraform that the resource output is a list, and not a single element type.
With terraform 0.12, there is a templatefile function but I haven't figured out the syntax for passing it a non-trivial map as the second argument and using the result to be executed remotely as the newly created instance's provisioning step.
Here's the gist of what I'm trying to do, although it doesn't parse properly because one can't just create a local variable within the resource block named scriptstr.
While I'm really trying to get the output of the templatefile call to be executed on the remote side, once the provisioner can ssh to the machine, I've so far gone down the path of trying to get the templatefile call output written to a local file via the local-exec provisioner. Probably easy, I just haven't found the documentation or examples to understand the syntax necessary. TIA
resource "aws_instance" "server" {
count = "${var.servers}"
ami = "${local.ami}"
instance_type = "${var.instance_type}"
key_name = "${local.key_name}"
subnet_id = "${element(aws_subnet.consul.*.id, count.index)}"
iam_instance_profile = "${aws_iam_instance_profile.consul-join.name}"
vpc_security_group_ids = ["${aws_security_group.consul.id}"]
ebs_block_device {
device_name = "/dev/sda1"
volume_size = 2
}
tags = "${map(
"Name", "${var.namespace}-server-${count.index}",
var.consul_join_tag_key, var.consul_join_tag_value
)}"
scriptstr = templatefile("${path.module}/templates/consul.sh.tpl",
{
consul_version = "${local.consul_version}"
config = <<EOF
"bootstrap_expect": ${var.servers},
"node_name": "${var.namespace}-server-${count.index}",
"retry_join": ["provider=aws tag_key=${var.consul_join_tag_key} tag_value=${var.consul_join_tag_value}"],
"server": true
EOF
})
provisioner "local-exec" {
command = "echo ${scriptstr} > ${var.namespace}-server-${count.index}.init.sh"
}
provisioner "remote-exec" {
script = "${var.namespace}-server-${count.index}.init.sh"
connection {
type = "ssh"
user = "clear"
private_key = file("${local.private_key_file}")
}
}
}
In your question I can see that the higher-level problem you seem to be trying to solve here is creating a pool of HashiCorp Consul servers and then, once they are all booted up, to tell them about each other so that they can form a cluster.
Provisioners are essentially a "last resort" in Terraform, provided out of pragmatism because sometimes logging in to a host and running commands on it is the only way to get a job done. An alternative available in this case is to instead pass the information from Terraform to the server via the aws_instance user_data argument, which will then allow the servers to boot up and form a cluster immediately, rather than being delayed until Terraform is able to connect via SSH.
Either way, I'd generally prefer to have the main body of the script I intend to run already included in the AMI so that Terraform can just run it with some arguments, since that then reduces the problem to just templating the invocation of that script rather than the whole script:
provisioner "remote-exec" {
inline = ["/usr/local/bin/init-consul --expect='${var.servers}' etc, etc"]
connection {
type = "ssh"
user = "clear"
private_key = file("${local.private_key_file}")
}
}
However, if templating an entire script is what you want or need to do, I'd upload it first using the file provisioner and then run it, like this:
provisioner "file" {
destination = "/tmp/consul.sh"
content = templatefile("${path.module}/templates/consul.sh.tpl", {
consul_version = "${local.consul_version}"
config = <<EOF
"bootstrap_expect": ${var.servers},
"node_name": "${var.namespace}-server-${count.index}",
"retry_join": ["provider=aws tag_key=${var.consul_join_tag_key} tag_value=${var.consul_join_tag_value}"],
"server": true
EOF
})
}
provisioner "remote-exec" {
inline = ["sh /tmp/consul.sh"]
}