How to refer to alternate resources in Terraform? - terraform

Terraform (as of today) has the somewhat disturbing limitation, that you cannot create a resource with an interpolated (calcuted) lifecycle attribute prevent_destroy.
Terraform: How to get a boolean from interpolation?
https://github.com/hashicorp/terraform/issues/3116
The work-around is pretty simple to code, just create 2 resources with "alternating" counts. When you have 1 "production" resource which does not allow destroying you have 0 "testing" resources which can be destroyed. Or the other way round. (See the answer to the stackoverflow question linked
above for details.)
However, this brings up a new question. When I want to refer to "the one of the alternate resources that exists for this execution", how do I do that?
In pseudo code something like
"${local.production ? "${aws_eip.commander_production.public_ip}" : "${aws_eip.commander_testing.public_ip}" }"
This pseudo code cannot work for a couple of reasons:
aws_eip.commander_production is no longer a single resource, it is a list, so you need the * syntax
one of the lists is always empty and Terraform easily complains that it cannot determine the type of an empty list. (I guess because the ternary operator requires that the alternates have the same type)
when you access into an empty list you will get an error (With C semantics the unused alternate would not be evaluated, but Terraform seems to work differently and I got errors when trying to code this)
To work around those I came up with the following hacky solution:
Extend the lists with a dummy element in the end and then refer to the
first element of the extended list. The code for this is pretty
horrible to type, but it seems to work
locals {
dummy = [ "foo" ]
}
output "0101: address" {
value = "${format("Public IP is %s", "${local.production ? "${element("${concat("${aws_eip.commander_production.*.public_ip}", "${local.dummy}")}", 0)}" : "${element("${concat("${aws_eip.commander_testing.*.public_ip}", "${local.dummy}")}", 0)}" }")}"
}
Question: What is a shorter / more elegant way to code this?
Note: I have found one answer myself, but feel welcome to contribute even better ones.

A bit shorter code is
output "0101: address" {
value = "${format("Public IP is %s", "${element("${concat("${aws_eip.commander_production.*.public_ip}", "${aws_eip.commander_testing.*.public_ip}")}", 0)}")}"
}
In plain text: Concatenate the lists and take the first element of the the result. One list has one element and the other one zero, so the result will be what we want, regardless whether the element is in the first or second list.

Related

Is it possible to recover from an error returned by a data source?

Let's take the dns_a_record_set data source, for example, if one does:
data "dns_a_record_set" "test" {
## Purposely passing a DNS that would not resolve
host = "thisDnsDoesNotResolve.org"
}
output "test" {
value = data.dns_a_record_set.test.addrs
}
The error returned is, as expected:
Error: error looking up A records for "thisDnsDoesNotResolve.org": lookup thisDnsDoesNotResolve.org on 1.1.1.1:53: no such host
│
│ with data.dns_a_record_set.test,
│ on main.tf line 1, in data "dns_a_record_set" "test":
│ 1: data "dns_a_record_set" "test" {
But, is it possible to recover from this error and have the test output assigned with a default value?
A naive trial of mine was to use the try function, but that obviously does not work, as the error does not happens when data is accessed, but rather when fetched, as shown in the error above.
output "test" {
## Seems like it doesn't even reach here
value = try(data.dns_a_record_set.test.addrs, ["127.0.0.1"])
}
So, is this even possible to recover from this error?
And, then, how?
The purpose of data resources in Terraform is to declare dependencies on objects managed elsewhere and, as a side-effect, obtain information about them to use in other parts of your module in a similar way as you might with objects that are managed by your current module.
If your module only depends on this DNS record in certain situations then a typical approach would be to use either the count or for_each meta-argument to explicitly declare in which situations your module depends on this object and which situations it doesn't.
It's hard to show a specific example without more information about your underlying motivation for this, but here's a contrived example where the hostname can be directly provided by the module caller as an input variable, and the dependency on that external object is only relevant when that variable is set (non-null):
variable "hostname" {
type = string
default = null # this variable is optional
}
data "dns_a_record_set" "test" {
count = length(var.hostname[*])
host = var.hostname
}
output "ip_addresses" {
value = one(data.dns_a_record_set.test[*].addrs)
}
The above uses two additional language features I didn't mention yet in this comment, which I'll describe briefly for completeness:
Applying the splat operator [*] to a non-list value converts the value to be a list which either has one element (if the value is non-null) or zero elements (if the value is null). Therefore there will be one instance of this data resource if and only if var.hostname is non-null.
The one function does essentially the opposite of that: if given a one-element list it will return the first element, and if given a zero-element list it will return null.
These two features taken together allow you to concisely pivot between values that might be null and lists of zero or one element, which is useful in situations like this where the "null-ness" of a valid decides how many of something should be declared.
Note that the try function only "catches" expression evaluation failures. It isn't relevant to this situation because what failed was the declared dependency on the external object; the error was returned when Terraform asked the provider to retrieve the dns_a_record_set object, not in the downstream expression which referred to that.
(Indeed, Terraform would never actually evaluate that expression in your case, because when there's a dependency from object B to object A, a failure of object A halts processing before reaching object B.)

module.db is a list of object, known only after apply

rds.tf:-
module "db" {
**count = var.environment == "dev" || var.environment == "qa" ? 1 : 0**
source = "../rds"
identifier = var.db_name
engine = var.rds_engine
engine_version = var.rds_engine_version
output.tf:
output "rds_instance_endpoint" {
description = "The connection endpoint"
value = module.db.db_instance_endpoint
}
ERROR:-
Error: Unsupported attribute
on outputs.tf line 28, in output "rds_instance_endpoint":
28: value = module.db.db_instance_endpoint
module.db is a list of object, known only after apply
Can't access attributes on a list of objects. Did you mean to access attribute "db_instance_endpoint" for a specific element of the list, or across all elements of the list?
getting the above error while declaring count in the rds.tf module.
if I remove count then its working fine, not sure what is this error.
The error message in this case is the sentence "Can't access attributes on a list of objects", not the sentence "module.db is a list of object, known only after apply"; the latter is just additional context to help you understand the error message that follows.
In other words, Terraform is telling you that module.db is a list of objects and that it's invalid to use .db_instance_endpoint ("accessing an attribute") on that list.
The root problem here is that you have a module object that may or may not exist, and so you need to explain to Terraform how you want to handle the situation where it doesn't exist.
One answer would be to change your output value to return a set of instance endpoints that would be empty in environments other than the dev and QA environments:
output "rds_instance_endpoints" {
description = "The connection endpoints for any database instances in this environment."
value = toset(module.db[*].db_instance_endpoint)
}
Here I used the "splat" operator to take the db_instance_endpoint attribute value for each instance of the module, which currently means that it will either be a single-element set or an empty set depending on the situation. This approach most directly models the underlying implementation and would give you the freedom to add additional instances of this module in future if you need to, but you might consider the fact that this is a multi-instance module to be an implementation detail which should be encapsulated in the module itself.
The other main option, which does hide that implementation detail of the underlying module being a list, would be to have your output value be null in the situation where there are no instances of the module, or to be a single string when there is one instance. For that, we can slightly adapt the above example to use the one function instead of the toset function:
output "rds_instance_endpoints" {
description = "The connection endpoint for the database instance in this environment, if any."
value = one(module.db[*].db_instance_endpoint)
}
The one function has three possible outcomes depending on the number of elements in the given collection:
If the collection has no elements, it will return null.
If the collection has one element, it will return just the value of that element.
If the collection has more than one element, it will fail with an error message. But note that this outcome is impossible in your case, because count can only be set to zero or one.
Null values can potentially be annoying to deal with if this data is being returned to a calling module for use elsewhere, but sometimes it's the most reasonable representation of the described infrastructure and so worth that additional complexity. For root module output values in particular, Terraform will treat a null value as if the output value were not set at all, hiding it in the messaging from terraform apply, and so this second strategy is often the best choice in situations where your primary motivation is to return this information in the user interface for a human to use for some subsequent action.

Get the index of a set inside of a dynamic block

I'm trying to get the index of a set. Look here for examples: Terraform get list index on for_each
This doesn't seem to work in a dynamic block though. The key and value are the same value still. How do I get the index number of an item in the set in a dynamic block?
dynamic "set" {
for_each = { for idx, item in var.myset: item => idx}
content {
name = "Company----${set.key}"
value = "Company----${set.value}"
}
}
You can't get the index number of an item in a set per Terraform's documentation on dynamic blocks.
key is the map key or list element index for the current element. If the for_each expression produces a set value then key is identical to value and should not be used.
It's probably a smell that there is better way you could approach this problem. But not knowing the full scope of what you are trying to achieve, I also understand that sometimes Terraform requires us to jump through some interesting hoops.
If you are dead set on getting the set index and its values in a dynamic block like you have above then you can convert the set to a map assigning indexes as the keys.
Something like this should work.
zipmap(range(length(var.myset)), var.myset)
Then you can use the resulting map in your for_each block. That should make set.key yield the current index (that was constructed by the range function) and set.value yield the value that was contained in the set.
An important thing to note is that unlike lists, sets are not ordered, so they don't have an index, per se. That's why you can't use the index function like the example above. It's also why I built the indexes using the range function.
It's also possible that you may have to explicitly convert the set to a list - Terraform should do an implicit conversion. But sometimes what it should do and what it does don't always align. And in that case, the statement would be as follows.
zipmap(range(length(var.myset)), toList(var.myset))
This is also explained in Terraform's documentation on types.

How get a value from a puppet resource

I have a problem with my puppet script.
I would like to get a value set in my resource file. I declare a resource like that
define checkxml(
$account = '',
$pwd = template('abc/abc.erb'),
){
if(empty($pwd)){
fail('pwd empty')
}
}
I call it via :
checkxml{"$agtaccount":
account => $agtaccount,
}
I want to get the value of $pwd. The $pwd will get is value by Template. If i try to show the value in my resource definition it's ok, I get the right value, so the Template works fine.
My problem is to get access this value after calling the ressource. I saw the getparam of stdlib but doesn't work for me.
getparam(Checkxml["$agtaccount"],"pwd")
If i try to get the account parameters instead of pwd it's ok. I think as i doesn't declare the pwd i can't get him back
How can i get him ?
Thanks for your help
Ugh, this looks dangerous. First off I'd recommend to steer clear of that function and the concept it embodies. It faces you with evaluation order dependencies, which can always lead to inconsistent manifest behavior.
As for the retrieval of the value itself - that will likely not work if the default is used. That's because on a catalog building level, there is not yet a value that is being bound to the parameter, if that makes any sense.
The resolution of final parameter values is rather involved, so there are lots of things that can go wrong with a manifest that relies on such introspective functionality.
I recommend to retrieve the desired value in a more central location (that depends on your manifest structure) and use it both when declaring the Checkxml["$agtaccount"] resource as well as its other uses (for which you are currently trying to extract it).

How to iterate over an array in Puppet

I would like to iterate over an array that is stored as a Facter fact, and for each element of the array create a new system user and a directory, and finally make API calls to AWS.
Example of the fact: my_env => [shared1,shared2,shared3]
How can I iterate over an array in Puppet?
This might work, depending on what you are doing
# Assuming fact my_env => [ shared1, shared2, shared3 ]
define my_resource {
file { "/var/tmp/$name":
ensure => directory,
mode => '0600',
}
user { $name:
ensure => present,
}
}
my_resource { $my_env: }
It will work if your requirements are simple, if not, Puppet makes this very hard to do. The Puppet developers have irrational prejudices against iteration based on a misunderstanding about how declarative languages work.
If this kind of resource doesn't work for you, perhaps you could give a better idea of which resource properties you are trying to set from your array?
EDIT:
With Puppet 4, this lamentable flaw was finally fixed. Current state of affairs documented here. As the documentation says, you'll find examples of the above solution in a lot of old code.
As of puppet 3.2 this is possible using the "future" parser like so:
$my_env = [ 'shared1', 'shared2', 'shared3', ]
each($my_env) |$value| {
file { "/var/tmp/$value":
ensure => directory,
mode => 0600,
}
user { $value:
ensure -> present,
}
}
See also: http://docs.puppetlabs.com/puppet/3/reference/lang_experimental_3_2.html#background-the-puppet-future-parser
Puppet 3.7 released earlier this month have the new DSL, which one feature is the iteration, check the following URL https://docs.puppetlabs.com/puppet/latest/reference/experiments_lambdas.html#enabling-lambdas-and-iteration
these new features can be enabled with the :
Setting parser = future in your puppet.conf file
or adding the command line switch --parser=future
hope that helps
As of latest Puppet (6.4.2), and since Puppet 4, iteration over arrays is supported in a few ways:
$my_arr = ['foo', 'bar', 'baz']
Each function:
$my_arr.each |$v| {
notice($v)
}
Each function alternative syntax:
each($my_arr) |$v| {
notice($v)
}
To get the index:
Pass a second argument to each:
$my_arr.each |$i, $v| {
notice("Index: $i, value: $v")
}
Comparison with Ruby:
Note that this grammar is inspired by Ruby but slightly different, and it's useful to show the two side by side to avoid confusion. Ruby would allow:
my_arr.each do |v|
notice(v)
end
Or:
my_arr.each { |v|
notice(v)
}
Other iteration functions:
Note that Puppet provides a number of other iteration functions:
each - Repeats a block of code a number of times, using a collection of values to provide different parameters each time.
slice - Repeats a block of code a number of times, using groups of values from a collection as parameters.
filter - Uses a block of code to transform a data structure by removing non-matching elements.
map - Uses a block of code to transform every value in a data structure.
reduce - Uses a block of code to create a new value, or data structure, by combining values from a provided data structure.
with - Evaluates a block of code once, isolating it in its own local scope. It doesn’t iterate, but has a family resemblance to the iteration functions.
Puppet 3 and earlier:
If you have inherited old code still using Puppet 3, the accepted answer is still correct:
define my_type {
notice($name)
}
my_type { $my_arr: }
Note however that this is usually considered bad style in modern Puppet.
itsbruce's answer is probably the best for now, but there is an iteration proposal going through puppetlabs' armatures process for possible implementation in future.
There is a "create_resources()" function in puppet. that will be very helpful while iterating over the list of itmes

Resources