Cycles or selection operators in the template_file - templates

Ran into a problem like this. I can 't figure out how to create a temlate_file of this kind:
[master]
ser1 ansible_host=10.0.0.1
ser2 ansible_host=10.0.0.2
So that the name and address are generated from the change.
I use standard costruction in date.tf:
data "template_file" "inventory" {
count = length(var.domains)
template = file("inventory.tpl")
vars = {
master_ip = join("\n", hcloud_server.rebrain_quest.*.ipv4_address)
key_path = var.privat_key
}
}
But here I can only generate my address.
[master]
10.0.0.1
10.0.0.2
You may need some way like that, but I don 't have anything coming out:
master_ip = join(";", [hcloud_server.rebrain_quest.*.name, ansible_host=, hcloud_server.rebrain_quest.*.ipv4_address])
I have terraform version v0.12.24

Because you are using Terraform 0.12, you should use the templatefile function instead of the template_file data source. Because it's built into the language rather than being offered by a provider, it's free of the data source's limitations like forcing all of the vars values to be strings.
locals {
ansible_inventory = templatefile("${path.module}/inventory.tpl", {
hosts = hcloud_server.rebrain_quest
})
}
Then in the template file:
[master]
%{ for h in hosts ~}
${h.name} ansible_host=${h.ipv4_address}
%{ endfor ~}
The above template is a variation of the example given in the documentation about Terraform's template directive syntax.
Your original example included count = var.domains but the rest of the resource configuration didn't include any mention of count.index so I assumed that wasn't actually needed. However, if you do want to create multiple copies of the template based on the number in var.domains you can do with the following variation:
locals {
ansible_inventory = [
for i in range(var.domains) :
templatefile("${path.module}/inventory.tpl", {
hosts = hcloud_server.rebrain_quest
index = i
})
]
}
The range function here creates a list of integers from zero to var.domains - 1, and so we can use that with for to repeat the template rendering multiple times. I added index = i to the template variables object so that you could in principle use ${i} inside the template to get a similar effect as with count.index in a resource block.

Terraform:
data "template_file" "inventory" {
count = length(var.domains)
template = file("inventory.tpl")
vars = {
hosts = hcloud_server.rebrain_quest
lines = [
for h in hcloud_server.rebrain_quest:
]
}
}
Template:
[master]
%{ for h in hosts ~}
${h.name} ansible_host=${h.ipv4_address}
%{ endfor ~}
But consider using terraform-ansible (there are bunch of them), use hcloud dynamic inventory plugin or use ad-hoc hcloud discover via hcloud_server_info

Thank you all for the answers, I solved this problem with a complex expression:
master_ip = "${join("\n", [for instance in hcloud_server.rebrain_quest : join("", [instance.name, " ansible_host=", instance.ipv4_address])] )}"

Related

Terraform : for_each one by one

I have created a module on terraform, this module creates aws_servicecatalog_provisioned_product resources.
When I call this module from the root I am using for_each to run into a list of objects.
The module runs into this list of objects and creates the aws_servicecatalog_provisioned_product resources in parallel.
Is there a way to create the resources one by one? I want that the module will wait for the first iteration to be done and to create the next just after.
Is there a way to create the resources one by one?
Sadly, there is not such way, unless you remove for_each and create all the modules separately with depends_on.
TF is not a procedural language, and it always will do things in parallel for for_each and count.
I am using terraform templatefile that creates resources with a depends on order, and then terraform creates resources one by one.
Here is the code:
locals {
expanded_accounts = [
{
AccountEmail = example1#example.com
AccountName = example1
ManagedOrganizationalUnit = example_ou1
SSOUserEmail = example1#example.com
SSOUserFirstName = Daniel
SSOUserLastName = Wor
ou_id = ou_id1
},
{
AccountEmail = example2#example.com
AccountName = example2
ManagedOrganizationalUnit = example_ou2
SSOUserEmail = example2#example.com
SSOUserFirstName = Ben
SSOUserLastName = John
ou_id = ou_id2
}
]
previous_resource = [
for acc in local.expanded_accounts :
acc.AccountName
]
resources = { res = local.expanded_accounts, previous = concat([""], local.previous_resource)
}
resource "local_file" "this" {
content = templatefile("./provisioned_accounts.tpl", local.resources)
filename = "./generated_provisioned_accounts.tf"
directory_permission = "0777"
file_permission = "0777"
lifecycle {
ignore_changes = [directory_permission, file_permission, filename]
}
}
provisioned_accounts.tpl configuration:
%{ for acc in res }
resource "aws_servicecatalog_provisioned_product" "${acc.AccountName}" {
name = "${acc.AccountName}"
product_id = replace(data.local_file.product_name.content, "\n", "")
provisioning_artifact_id = replace(data.local_file.pa_name.content, "\n", "")
provisioning_parameters {
key = "SSOUserEmail"
value = "${acc.SSOUserEmail}"
}
provisioning_parameters {
key = "AccountEmail"
value = "${acc.AccountEmail}"
}
provisioning_parameters {
key = "AccountName"
value = "${acc.AccountName}"
}
provisioning_parameters {
key = "ManagedOrganizationalUnit"
value = "${acc.ManagedOrganizationalUnit} (${acc.ou_id})"
}
provisioning_parameters {
key = "SSOUserLastName"
value = "${acc.SSOUserLastName}"
}
provisioning_parameters {
key = "SSOUserFirstName"
value = "${acc.SSOUserFirstName}"
}
timeouts {
create = "60m"
}
%{if index != 0 }
depends_on = [aws_servicecatalog_provisioned_product.${previous[index]}]
%{ endif }
}
%{~ endfor ~}
Why do you want it to wait for the previous creation? Terraform relies on the provider to know what can happen in parallel and will run in parallel where it can.
Setting the parallelism before the apply operation would be how I would limit it artificiality if I wanted to as it's an technical workaround that keeps your Terraform code simple to read.
TF_CLI_ARGS_apply="-parallelism=1"
terraform apply
If you find this is slowing down all Terraform creations but you need this particular set of resources to be deployed one at a time then it might be time to break these particular resources out into their own Terraform config directory and apply it in a different step to the rest of the resources again with the parallelism setting.
You have to remove the for_each and use depends_on for every element if you want to make sure that they are created one after another.
If you want only the first resource to be provisioned before other resources:
Separate the first resource only and use the for_each for the remaining resources. You can put an explicit dependency using depends_on for the remaining resources to depend on the first one. Because for_each expects a set or a map, this input would require some modification to be able to exclude the provisioning of the first resource.
A more drastic approach, if you really need to provision resources one by one, would be to run the apply command with -parallelism=1. This would reduce the number of resources provisioned in parallel to 1. This would apply to the whole project. I would not recommend this, since it would increase drastically the running time for the apply.

Unable to create dynamic terraform outputs for use in terraform_remote_state

I have the following code block for creating various IAM groups
resource "aws_iam_group" "environment-access" {
count = "${length(var.environments)}"
name = "access-${element(var.environments, count.index)}"
}
variable "environments" {
default = ["production", "non-production"]
type = "list"
}
I want to write the outputs of the IAM groups created in order to grab the ARN of each group to use as data via terraform_remote_state where it would look something like the following
Outputs:
access-production = arn:aws:iam::XXXXXXX:group/basepath/access-production
access-non-production = arn:aws:iam::XXXXXXX:group/basepath/access-non-production
I am having trouble creating the dynamic outputs as I am unsure how to dynamically create the output stanzas based on the the resource originally created as using the below code yields an error referencing unknown resource 'aws_iam_group.access-production' referenced.
output "access-production" {
value = "${aws_iam_group.access-production.arn}"
}
output "access-non-production" {
value = "${aws_iam_group.access-non-production.arn}"
}
An initial problem with this requirement is that it calls for having a single dynamic list of environments but multiple separate output values. In order to make this work, you'll need to either make the environment inputs separate values or produce a single output value describing the environments.
# Variant with a fixed set of environments (v0.11 syntax)
variable "production_environment_name" {
type = "string"
default = "production"
}
variable "non_production_environment_name" {
type = "string"
default = "non-production"
}
resource "aws_iam_group" "production_access" {
name = "access-${var.production_environment_name}"
}
resource "aws_iam_group" "non_production_access" {
name = "access-${var.non_production_environment_name}"
}
output "access_production" {
value = "aws_iam_group.production_access.arn"
}
output "access_non_production" {
value = "aws_iam_group.non_production_access.arn"
}
# Variant with dynamic set of environments (v0.11 syntax)
variable "environments" {
type = "list"
default = ["production", "non_production"]
}
resource "aws_iam_group" "access" {
count = "${length(var.environments)}"
name = "access-${var.environments[count.index]}"
}
output "access" {
value = "${aws_iam_group.access.*.arn}"
}
The key here is that the input variable and the output value must have the same form, so that we can make all of the necessary references between the objects. In the second example, the environment names are provided as a list, and the group ARNs are also provided as a list such that the indices correspond between the two.
You can also use a variant of the output "access" expression to combine the two with zipmap and get a map keyed by the environment names, which will probably be more convenient for the caller to use:
output "access" {
value = "${zipmap(var.environments, aws_iam_group.access.*.arn)}"
}
The new features in Terraform 0.12 allow tidying this up a bit. Here's an idiomatic Terraform 0.12 equivalent of the version that produces a map as a result:
# Variant with dynamic set of environments (v0.12 syntax)
variable "environments" {
type = set(string)
default = ["production", "non_production"]
}
resource "aws_iam_group" "access" {
for_each = var.environments
name = "access-${each.key}"
}
output "access" {
value = { for env, group in aws_iam_group.access : env => group.arn }
}
As well as having some slightly different syntax patterns, this 0.12 example has an additional practical advantage: Terraform will track those IAM groups with addresses like aws_iam_group.access["production"] and aws_iam_group.access["non_production"], so the positions of the environment names in the var.environments list are not important and it's possible to add and remove environments without potentially disturbing the groups from other environments due to the list element renumbering.
It achieves that by using resource for_each, which makes aws_iam_group.access appear as a map of objects where the environment names are keys, whereas count makes it a list of objects.

Select where tag end in a or b in Terraform data lookup

I have 3 subnets. They are named:
test-subnet-az-a test-subnet-az-b test-subnet-az-c
I have a datasource like so:
data "aws_subnet_ids" "test" {
vpc_id = "${module.vpc.id}"
tags = {
Name = "test-subnet-az-*"
}
}
This will return a list including all 3 subnets.
How do I return just the first 2, or those ending in a or b?
Terraform data sources are generally constrained by the capabilities of whatever underlying system they are querying, so the filtering supported by aws_subnet_ids is the same filtering supported by the underlying API, and so reviewing that API (EC2's DescribeSubnets) may show some variants you could try.
With that said, if you can use the data source in a way that is close enough to reduce the resultset down to a manageable size (which you seem to have achieved here) then you can filter the rest of the way using a for expression within the Terraform language itself:
data "aws_subnet_ids" "too_many" {
vpc_id = "${module.vpc.id}"
tags = {
Name = "test-subnet-az-*"
}
}
locals {
want_suffixes = toset(["a", "b"])
subnet_ids = toset([
for s in data.aws_subnet_ids.too_many.ids : s
if contains(local.want_suffixes, substr(s, length(s)-1, 1))
])
}
You can place any condition expression you like after if in that for expression to apply additional filters to the result, and then use local.subnet_ids elsewhere in the configuration to access that reduced set.
I used toset here to preserve the fact that aws_subnet_ids returns a set of strings value rather than a list of strings, but that's not particularly important unless you intend to use the result with a Terraform feature that requires a set, such as the for_each argument within resource and data blocks (which is not yet released as I write this, but should be released soon.)

Terraform: put variables in tfvar file not work

I defined a variable map my_role in terraform and set its value in abc.tfvar file as follows. if I assign account id as actual value, it works, if I set account id as a variable, it does not work. Does it mean tfvar file only allow actual value, not variable? By the way, I use terraform workspace. Therefore my_role is different based on workspace I select.
The following works:
my_role = {
dev = "arn:aws:iam::123456789012:role/myRole"
test = ...
prod = ...
}
The following does not work:
my_role = {
dev = "arn:aws:iam::${lookup(var.aws_account_id, terraform.workspace)}:role/myRole"
test = ...
prod = ...
}
The following does not work either:
lambdarole = {
dev = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/myRole"
test = ...
prod = ...
}
does
Have you tried following the example on Input Variables?
You can define your abc.tfvars file with:
variable "my_role" {
type = "map"
default = {
"dev" = "arn:aws:iam::123456789012:role/myRole"
"test" = "..."
"prod" = "..."
}
}
And access it with "${lookup(var.my_role, terraform.workspace)}".
Also, according to the from a file, the variables defined in .tfvars files are automatically loaded if you name the file terraform.tfvars, if not, you have to pass as an argument with -var-file=...
Cannot test it right now, but probably is something in this way.
I am replying when terraform 0.12 version is latest one. Solution is simple, you can create one file say vars.tf and declare variables in it.
Example - variable "xyz" {}
Now create terraform.tfvars and initialize it.
Example - xyz="abcd"
No need to pass any runtime args, it will be picked directly.
Terraform has aws_caller_identity data source. You do not need to mention or hand code account id anywhere. It can be fetched using this source.
In any of your .tf file, just include this source and then you can fetch relevant argument value.
This is how you can do it. Define this in any *.tf file
data "aws_caller_identity" "current" {}
Now where ever you want the value of arn or account id, it can be fetched using :
For account id(For terraform0.12):
data.aws_caller_identity.current.account_id
In your case, it would be like this :
dev = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/myRole"
But in order to this work, you need to define data source like shown above in any *.tf file.
For more help, refer following:
URL : https://www.terraform.io/docs/providers/aws/d/caller_identity.html

Is it possible to turn the access_logs block on and off via the environment_name variable?

I'm looking at using the new conditionals in Terraform v0.11 to basically turn a config block on or off depending on the evnironment.
Here's the block that I'd like to make into a conditional, if, for example I have a variable to turn on for production.
access_logs {
bucket = "my-bucket"
prefix = "${var.environment_name}-alb"
}
I think I have the logic for checking the environment conditional, but I don't know how to stick the above configuration into the logic.
"${var.environment_name == "production" ? 1 : 0 }"
Is it possible to turn the access_logs block on and off via the environment_name variable? If this is not possible, is there a workaround?
One way to achieve this with TF 0.12 onwards is to use dynamic blocks:
dynamic "access_logs" {
for_each = var.environment_name == "production" ? [var.environment_name] : []
content {
bucket = "my-bucket"
prefix = "${var.environment_name}-alb"
}
}
This will create one or zero access_logs blocks depending on the value of var.environment_name.
In the current terraform, the if statement is only a value and can not be used for the block.
There is a workaround in this case. You can set the enabled attribute of the access_log block to false. Note that this is not a general solution but can only be used with the access_log block.
access_logs {
bucket = "my-bucket"
prefix = "${var.environment_name}-alb"
enabled = "${var.environment_name == "production" ? true : false }"
}
See also:
https://www.terraform.io/docs/providers/aws/r/elb.html#access_logs
https://www.terraform.io/docs/providers/aws/r/alb.html#access_logs
https://github.com/hashicorp/terraform/pull/11120
Expanding on Juho Rutila's answer as it's too much to fit in a comment.
This is possible using dynamic blocks from v0.12, but I found the properties had to be included in a nested content block. The for_each statement is also a bit tricky, so I found it sensible to extract that into a local to make the important stuff more readable:
locals {
isProd = var.environment_name == "production" ? [1] : []
// Not necessary, but just illustrating that the reverse is possible
isNotProd = var.environment_name == "production" ? [] : [1]
}
dynamic "access_logs" {
for_each = local.isProd
content {
bucket = "my-bucket"
prefix = "${var.environment_name}-alb"
}
}
You can read more about dynamic blocks here: https://www.terraform.io/docs/configuration/expressions.html#dynamic-blocks
Conditionals in terraform are currently only to be used to determine a value, not to be used as an if statement wrapping a block.
And you can also use conditionals to determine a value based on some logic.
https://www.terraform.io/docs/configuration/interpolation.html#conditionals
Expanding on Juho Rutila’s answer too; I like to use the range function for this use case:
dynamic "access_logs" {
for_each = range(var.environment_name == "production" ? 1 : 0)
contents {
bucket = "my-bucket"
prefix = "${var.environment_name}-alb"
}
}
range(n) produces a n-element list.