Create Ansible inventory using Terraform - azure

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.

Related

Terraform: Easier way to run a flexible number of objects

Currently I have a powershell script that reads a yaml config file with all the objects I need created and creates a .tfvars file which contains all the variables, maps, lists of maps etc.
It would be something like the following:
global_tags = {
Provisioner = "Terraform"
}
resource_groups = {
myrg1 = {
location = "uksouth",
tags = {
ResourceType = "resourcegroup"
}
}
}
storage_accounts = {
mystorage1 = {
resource_group_name = "myrg1",
location = "uksouth",
account_tier = "Standard",
account_replication_type = "GRS",
tags = {
ResourceType = "storageaccount"
}
containers_list = [
{ name = "test_private_x", access_type = "private" },
{ name = "test_blob_x", access_type = "blob" },
{ name = "test_container_x", access_type = "container" }
]
}
The idea is to then pump each list of maps into each module to create the resources, e.g. main.tf would be just:
module "resourcegroup" {
source = "./modules/azure-resourcegroup"
resource_groups = var.resource_groups
global_tags = var.global_tags
}
module "storageaccount" {
source = "./modules/azure-storageaccount"
depends_on = [module.resourcegroup]
storage_accounts = var.storage_accounts
global_tags = var.global_tags
}
Also, an example of a simple module would be:
resource "azurerm_resource_group" "rg" {
for_each = var.resource_groups
name = each.key
location = each.value.location
tags = lookup(each.value,"tags",null) == null ? var.global_tags : merge(var.global_tags,each.value.tags)
}
The issue is that writing a complex module, say around storage account, isn't too bad if you are just feeding in all the params, but feeding in a list of maps and writing a module to read that list and create multiple flattened lists to perform say 15 different calls (to create containers, shares, network rules etc.) is very complex.
Obviously the reason I want to use for_each loops in the modules is so that my main.tf doesn't have to call the module multiple times with hard coded values for say 50 storage accounts.
Just wondering if I am missing an obvious way to create complicated multiples of each resource type ?
I appreciate I could do separate modules for containers, shares etc and break the complex maps down into simpler ones to pass to the additional modules, but I was trying to just have 1 storage account module that could handle anything and be fed by a complex list of maps so main.tf did not need editing, I could just control the config completely via a .tfvars file

Unable to Create Terraform Resource Group when using modules

I am optimizing my terraform code by using modules. When i create a resource group module it works perfectly well but it creates two resource groups
i.e.
Temp-AppConfiguration-ResGrp
Temp-AppServices-ResGrp
instead it should only create
Temp-AppConfiguration-ResGrp
Code Resourcegroup.tf.
resource "azurerm_resource_group" "resource" {
name = "${var.environment}-${var.name_apptype}-ResGrp"
location = var.location
tags = {
environment = var.environment
}
}
output "resource_group_name" {
value = "${var.environment}-${var.name_apptype}-ResGrp"
}
output "resource_group_location" {
value = var.location
}
Variable.tf
variable "name_apptype" {
type = string
default = "AppServices"
}
variable "environment" {
type = string
default = "Temp"
}
variable "location" {
type = string
default = "eastus"
}
Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
I want to pass name_apptype in main.tf when calling resource group module. So that i don't need to update variable.tf every time.
Any suggestions
where i am doing wrong. Plus i am also unable to output the value, i need it so that i could pass resource group name in the next module i want to create.
Thanks
You need to do that in the Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
module "resourcegroup-appservices" {
source = "../Modules"
name_apptype = "AppServices"
}
These create a 2 resources groups with the values that you need, additionally you can remove the default value from the name_apptype variable.
If you want to create with the same module both resource groups you need to use count to iterate over an array of names

Using Terraform module output as input to local-exec provisioner

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 :)

How do I make terraform skip that block while creating multiple resources in loop from a CSV file?

Hi I am trying to create a Terraform script which will take inputs from the user in the form of a CSV file and create multiple Azure resources.
For example if the user wants to create: ResourceGroup>Vnet>Subnet in bulk, he will provide input in CSV format as below:
resourcegroup,RG_location,RG_tag,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
I have written the following working main.tf file:
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.43.0"
subscription_id = var.subscription
tenant_id = var.tenant
client_id = var.client
client_secret = var.secret
}
#Decoding the csv file
locals {
vmcsv = csvdecode(file("${path.module}/computelanding.csv"))
}
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
To be continued....
The issue I am facing here what is in the second resource group, the user don't want a resource type, suppose the user want to skip the DNS zone in the resource group csvrg2. How do I make terraform skip that block ?
Edit: What I am trying to achieve is "based on some condition in the CSV file, not to create azurerm_dns_zone resource for the resource group csvrg2"
I have provided an example of the CSV file, how it may look like below:
resourcegroup,RG_location,RG_tag,DNS_required,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,1,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,0,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
you had already the right thought in your mind using the depends_on function. Although, you're using a count inside, which causes from my understanding, that once the first resource[0] is created, Terraform sees the dependency as solved and goes ahead as well.
I found this post with a workaround which you might be able to try:
https://github.com/hashicorp/terraform/issues/15285#issuecomment-447971852
That basically tells us to create a null_resource like in that example:
variable "instance_count" {
default = 0
}
resource "null_resource" "a" {
count = var.instance_count
}
resource "null_resource" "b" {
depends_on = [null_resource.a]
}
In your example, it might look like this:
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = null_resource.example
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
resource "null_resource" "example" {
...
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
}
or depending on your Terraform version (0.12+ which you're using guessing your syntax)
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
I hope that helps.
Greetings

Terraform azure-remove subcription details from output

I declared security group in following way:
resource "azurerm_network_security_group" "wan" {
count = "${var.enable_wan_subnet ? 1 : 0}"
provider = "azurerm.base"
name = "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}"
location = "${azurerm_resource_group.this.location}"
resource_group_name = "${azurerm_resource_group.this.name}"
tags = "${
merge(map("Name", format("%s-%s-%s",var.environment_name,"WAN-Subnets", "Security-Group")),
var.tags_global,
var.tags_module)
}"
}
and created output for that security group:
output "security_groups_id_wan" {
value = "${azurerm_network_security_group.wan.*.id}"
depends_on = [
"azurerm_subnet.wan",
]
}
In output i'm getting
Actual output
security_groups_id_wan = [
/subscriptions/111-222-333-4445/resourceGroups/default_resource_group/providers/Microsoft.Network/networkSecurityGroups/DF-DTAP-WAN-Subnet-Security-Group
]
How, from output, to remove all except resource name (DF-DTAP-WAN-Subnet-Security-Group)
Desired output:
security_groups_id_wan = [
DF-DTAP-WAN-Subnet-Security-Group
]
You can just use the Terraform functions and change the output value like this:
output "security_groups_id_wan" {
value = "${slice(split("/",azurerm_network_security_group.wan.*.id), length(split("/",azurerm_network_security_group.wan.*.id))-1, length(split("/",azurerm_network_security_group.wan.*.id)))}"
depends_on = [
"azurerm_subnet.wan",
]
}
With the functions, you can output every resource as you need. For more details, see Terraform Supported built-in functions.
Update
The test with an existing NSG through the Terraform data and the template here:
data "azurerm_network_security_group" "test" {
name = "azureUbuntu18-nsg"
resource_group_name = "charles"
}
output "substring" {
value = "${slice(split("/",data.azurerm_network_security_group.test.id), length(split("/",data.azurerm_network_security_group.test.id))-1, length(split("/",data.azurerm_network_security_group.test.id)))}"
}
The screenshot of the result here:
You built that name yourself with "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}" so why not just output that?
To save repeating yourself you could put that in a local and refer to it in both the resource and the output:
locals {
security_group_name = "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}"
}
resource "azurerm_network_security_group" "wan" {
count = "${var.enable_wan_subnet ? 1 : 0}"
provider = "azurerm.base"
name = "${local.security_group_name}"
# ...
}
output "security_groups_id_wan" {
value = "${local.security_group_name}"
}
Note that you also didn't need the depends_on because a) it's an output, it happens at the end of things anyway and b) you already have an implicit dependency on that resource because you used an interpolation that included the resource.
You can read more about Terraform dependencies via the Hashicorp Learn platform.
Addition to #Charles Xu's answer:Had to convert list to string first
output "subnets_id_wan" {
value = "${slice(split("/",join(",",azurerm_subnet.wan.*.id)), length(split("/",join(",",azurerm_subnet.wan.*.id)))-1, length(split("/",join(",",azurerm_subnet.wan.*.id))))}"
depends_on = [
"azurerm_subnet.wan",
]
}

Resources