Terraform v0.13 - Check if password or secret provided, use a randomly generated one if not - terraform

I'm working to fine-tune some of my Terraform modules, specifically around the google_compute_vpn_tunnel, google_compute_router_interface, and google_compute_router_peer resources. I'd like to make things similar to AWS, where pre-shared keys and tunnel interface IP addresses are randomized by default, but can be overridden by the user (provided they are within a certain range).
The random option is working fine. For example, to create a 20-character random password, I do this:
resource "random_password" "RANDOM_PSK" {
length = 20
special = false
}
But, I only want to use this value if an input variable called vpn_shared_secret was not defined. Seems like this should work:
variable "vpn_shared_secret" {
type = string
default = null
}
locals {
vpn_shared_secret = try(var.vpn_shared_secret, random_password.RANDOM_PSK.result)
}
resource "google_compute_vpn_tunnel" "VPN_TUNNEL" {
shared_secret = local.vpn_shared_secret
}
Instead, it seems to ignore the vpn_shared_secret input variable and just go with the randomly generated one each time.
Is try() the correct way to be doing this? I'm just now learning Terraform if/else and map statements.

How about the coalesce() function?
The coalesce function takes any number of arguments, and returns the first argument that isn't null or an empty string.
locals {
vpn_shared_secret = coalesce(var.vpn_shared_secret, random_password.RANDOM_PSK.result)
}

Related

Conditional outputs on data sources in terraform

I have a sql server terraform module that outputs the name of a sql server for the databases to get created in. However, some environments should use an external server outside of the terraform project instead. Most datacenters we have do not have this external server, just a few.
I've set up the external server using data sources as usual, and made both the output, normal server and datasource conditional on a variable thats passed in like this:
variable "use_external_sql_server" {
type = bool
}
resource "azurerm_mssql_server" "sqlserver" {
count = var.use_external_sql_server ? 0 : 1
name = "sql-interal-sql_server"
....
}
data "azurerm_mssql_server" "external_sql_server" {
count = var.use_external_sql_server ? 1 : 0
name = "sql-${var.env}-${var.location}"
resource_group_name = "rg-${var.env}-${var.location}"
}
output "sql_server_name" {
value = var.use_external_sql_server ? data.azurerm_mssql_server.external_sql_server.name : azurerm_mssql_server.sqlserver[0].name
depends_on = [
azurerm_mssql_server.sqlserver,
data.azurerm_mssql_server.external_sql_server
]
}
However, I'm running into issues with the output. It requires data.azurerm_mssql_server.external_sql_server to exist to evaulate the condition, even if "use_external_server" is false. This is not ideal as I have to manual create dummy servers to fix this condition, so that that conditional can evaulate to true.
Is there a way to do this conditional without having to have "data.azurerm_mssql_server.external_sql_server" actually exist?
You could get rid of the conditional in the output and just use a try.
try evaluates all of its argument expressions in turn and returns the result of the first one that does not produce any errors.
This is a special function that is able to catch errors produced when evaluating its arguments, which is particularly useful when working with complex data structures whose shape is not well-known at implementation time.
You could then possibly write something like
output "sql_server_name" {
value = try(data.azurerm_mssql_server.external_sql_server[0].name, azurerm_mssql_server.sqlserver[0].name, "")
depends_on = [
azurerm_mssql_server.sqlserver,
data.azurerm_mssql_server.external_sql_server
]
}

Terraform output from several resources generated with count

I have these outputs:
output "ec2_id" {
value = aws_instance.ec2instance[*].id
}
output "ec2_name" {
value = aws_instance.ec2instance[*].tags["Name"]
}
output "ec2_mgmt_eip" {
value = aws_eip.eip_mgmt_ec2instance[*].public_ip
}
I want to make an output like:
"<instanceName>: <instanceID> -> <publicIP>"
(all data in same line for same ec2 instance).
In any non-declarative language i can use something like for (var i=0; i<length(myarray);i++) and use "i" as index for each list, in every index concatenate in a new string, but in terraform I can't find how to do it.
Thanks!
Even though you got the answer in the comments, I will add an example. So, the thing you want does exist in terraform as it also has for loops [1]. A for loop along with the right syntax will give you a desired output, which is a map in terraform:
output "ec2_map" {
value = { for i in aws_instance.ec2instance: i.tags.Name => "${i.id}:${i.public_ip}" }
}
The output you said you want is quite similar to this. Also, there is no concept of "same line" in terraform. In this case, since this is a map, the keys will be instance names and value will be a combination of instance id and the public IP, but that will be a string.
[1] https://www.terraform.io/language/expressions/for

How to set a puppet class variable from within a hiera_hash each loop?

hiera data
ae::namespace_by_fqdn_pattern:
'((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((client))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com': '/test/blah/regression/client'
'((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((server))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com': '/test/blah/regression/server'
class
class ae {
$namespace = hiera('ae::namespace')
$target_host_patterns = hiera('ae::target_host_patterns')
hiera_hash('ae::namespace_by_fqdn_pattern').each |String $pattern, String $ns| {
if $facts['networking']['fqdn'].match($pattern) {
$ae::namespace = "${ns}"
}
}
<snip>
... yields
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Illegal attempt to assign to 'ae::enforcerd_namespace'. Cannot assign to variables in other namespaces (file: /etc/puppetlabs/code/environments/ar/modules/ae/manifests/init.pp, line: 21, column: 13) on node dfw-ubuntu1604-client02.pp-devcos.us-central1.gcp.dev.blah.com
... anyone here know how to do this correctly? trying to conditionally override that $ae::namespace variable but i'm too puppet-ignorant to know how to get it working : (
the loop and the pattern matching bits work. just can't figure out how to correctly set that class variable from within the hiera_hash().each loop.
How to set a puppet class variable from within a hiera_hash each loop?
You cannot. The associated block of an each() call establishes a local scope for each iteration. Variable assignments within apply to that local scope, and therefore last only for the duration of one execution of the block. You cannot anyway assign a new value to a variable during its lifetime, so even if you could assign to a class variable from within an each() call, it would be difficult to use that capability (and your approach would not work).
There are several ways you could approach the problem without modifying the form of the data. You could leverage the filter() function, for example, but my personal recommendation would be to use the reduce() function, something like this:
$namespace = lookup('ae::target_host_patterns').reduce(lookup('ae::namespace')) |$ns, $entry| {
$facts['networking']['fqdn'].match($entry[0]) ? { true => $entry[1], default => $ns }
}
That does pretty much exactly what your original code seems to be trying to do, except that the selected namespace is returned by the reduce() call, to be assigned to a variable by code at class scope, instead of the lambda trying to assign it directly. Note also that it takes care not only of testing the patterns but of assigning the default namespace when none of the patterns match, as it needs to do because you can only assign to the namespace variable once.
so the solution i landed on was to change the hiera data to:
ae::namespace : '/test/blah/regression'
ae::namespace_patterns: ['((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((client))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com', '((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((server))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com']
ae::namespace_by_pattern:
'((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((client))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com': '/test/paypal/regression/client'
'((^dfw-oel6)|(^dfw-oel7)|(^dfw-ubuntu1604))-((server))([0-9]{2}).pp-devcos-ae.us-central1.gcp.dev.blah.com': '/test/paypal/regression/server'
then the class code to:
$pattern = hiera_hash('ae::namespace_patterns').filter |$pattern| {
$facts['networking']['fqdn'] =~ $pattern
}
if length($pattern) {
$namespace = hiera('ae::namespace_by_pattern')[$pattern[0]]
} else {
$namespace = hiera('ae::namespace')
}
definitely still open to better answers. just what my own hacking produced as workable so far through much trial and error.

Get type of a variable in Terraform

Is there a way to detect the type of a variable in Terraform? Say, I have a module input variable of type any, can I do some kind of switch, depending on the type?
variable "details" {
type = any
}
local {
name = var.details.type == map ? var.details["name"] : var.details
}
What I want to archive is, to be able to pass either a string as shorthand or a complex object with additional keys.
module "foo" {
details = "my-name"
}
or
module "foo" {
details = {
name = "my-name"
age = "40"
}
}
I know this example doesn't make much sense and you would like to suggest to instead use two input vars with defaults. This example is just reduced to the minimal (non)working example. The end goal is to have a list of IAM policy statements, so it is going to be a list of lists of objects.
Terraform v0.12.20 introduced a new function try which can be used to concisely select between different ways of retrieving a value, taking the first one that wouldn't produce an error.
variable "person" {
type = any
# Optional: add a validation rule to catch invalid types,
# though this feature remains experimental in Terraform v0.12.20.
# (Since this is experimental at the time of writing, it might
# see breaking changes before final release.)
validation {
# If var.person.name succeeds then var.person is an object
# which has at least the "name" attribute.
condition = can(var.person.name) || can(tostring(var.person))
error_message = "The \"person\" argument must either be a person object or a string giving a person's name."
}
}
locals {
person = try(
# The value of the first successful expression will be taken.
{name = tostring(var.person)}, # If the value is just a string
var.person, # If the value is not a string (directly an object)
)
}
Elsewhere in the configuration you can then write local.person.name to obtain the name, regardless of whether the caller passed an object or a string.
The remainder of this answer is an earlier response that now applies only to Terraform versions between v0.12.0 and v0.12.20.
There is no mechanism for switching behavior based on types in Terraform. Generally Terraform favors selecting specific types so that module callers are always consistent and Terraform can fully validate the given values, even if that means a little extra verbosity in simpler cases.
I would recommend just defining details as an object and having the caller explicitly write out the object with the name attribute, in order to be more explicit and consistent:
variable "details" {
type = object({
name = string
})
}
module "example" {
source = "./modules/example"
details = { name = "example" }
}
If you need to support two different types, the closest thing in the Terraform language would be to define two variables and detect which one is null:
variable "details" {
type = object({
name = string
})
default = null
}
variable "name" {
type = string
default = null
}
local {
name = var.name != null ? var.name : var.details.name
}
However since there is not currently a way to express that exactly one of those two must be specified, the module configuration you write must be ready to deal with the possibility that both will be set (in the above example, var.name takes priority) or that neither will be set (in the above example, the expression would produce an error, but not a very caller-friendly one).
terraform v1.0+ introduces a new function type() for this purpose. See https://www.terraform.io/language/functions/type

Is it possible to generate a variable name in terraform

So i want to get the variable in the terraform remote state, however we have a number of different one per environment on the shared route53
So for a given environement, we want to pull the zone id out as such;
zone_id = data.terraform_remote_state.route_53.route53_zone_${var.environment}_id
How would I do this please.
In general, it is not possible to use arbitrary dynamic strings as variable names.
However, in this particular case the outputs from terraform_remote_state are collection values and so you can use the index syntax to access a dynamically-built key from your map value:
data.terraform_remote_state.outputs.route53["route53_zone_${var.environment}_id"]
With that said, if possible I would recommend structuring the output values better so that the Route53 zone ids are given as a map by environment, so that this can be obtained in a more intuitive way.
For example, you could make your route53 output be a map of objects whose keys are the environment names:
data.terraform_remote_state.outputs.route53[var.environment].zone_id
output "route53" {
value = tomap({
production = {
zone_id = aws_route53_zone.production.id
}
staging = {
zone_id = aws_route53_zone.staging.id
}
})
}
Or, if you have a variety of different per-environment settings you could structure it as a single output value that is a map of all of those per environment settings keyed by environment name:
data.terraform_remote_state.outputs.environments[var.environment].route53_zone_id
output "environments" {
value = tomap({
production = {
ec2_vpc_id = aws_vpc.production.id
route53_zone_id = aws_route53_zone.production.id
}
staging = {
ec2_vpc_id = aws_vpc.staging.id
route53_zone_id = aws_route53_zone.staging.id
}
})
}
This doesn't change anything about the ultimate result, but grouping things by your environment keys in your outputs is likely to make your intent clearer to future maintainers of these configurations.
(You might also consider whether it'd be better to have a separate configuration/state per environment rather than managing them altogether, but that is a big topic in itself.)

Resources