It's been important from the beginning that Terraform's configuration language is declarative, which has meant that the core team has intentionally avoided adding flow-control statements like conditionals and loops to the language.
But in the real world, there are still plenty of perfectly reasonable scenarios that are difficult to express in the current version of Terraform without copious amounts of duplication because of the lack of conditionals. We'd like Terraform to support these use cases one way or another.
I'm opening this issue to collect some real-world example where, as a config author, it seems like an if
statement would really make things easier.
Using these examples, we'll play around with different ideas to improve the tools Terraform provides to the config author in these scenarios.
So please feel free to chime in with some specific examples - ideally with with blocks of Terraform configuration included. If you've got ideas for syntax or config language features that could form a solution, those are welcome here too.
(No need to respond with just "+1" / :+1: on this thread, since it's an issue we're already aware is important.)
Here are 2 examples: https://gist.github.com/chrisferry/780140d709bfad51038c
The RDS and ELB modules have minor differences. SSL cert or no. IOPs or no.
OMG, I could rant on about this issue for a long while. One litany of clear and concise use cases are found in implementing a concept as a terraform module. There are even community modules which exemplify this.. two modules for essentially the same thing, one provides an ELB, one does not.
I tend to want to write terraform source as I do with Saltstack: as a giant jinja template. This affords me a whole lot of flexibility while ensuring the application (Salt) ends up with a machine-readable format. Terraform _sort of_ has this type of pre-processing (with interpolation), but Saltstack's implementation leverages the concept of a pluggable _renderer_ system.. this pre-processor _renders_ the template to give to salt for processing. The renderer can be jinja, mako, or any one of a few different systems (made available as modules). The user/developer experience has been exceptional, and I am thankful for the power it lends, while still providing a declarative system. In contrast, using terraform has felt cumbersome and restrictive in the expression of one's needs (especially when I have gone to encapsulate a working POC into a module).
Thank you for opening this dicussion!
This is a bit of a stretch on the topic of this issue, but a couple of times I've found myself wishing for an iteration construct to allow me to create a set of resources that each map one-to-one to an item in a list.
I've found and then promptly forgotten a number of examples (having dismissed them as impossible), but one that stayed in my mind was giving EC2 instances more memorable local hostnames and then creating Route53 records for each of them.
resource "aws_instance" "app_server" {
# (...)
count = 5
provisioner "remote-exec" {
inline = [
"set-hostname-somehow appname-${join(\"-\", split(\".\", self.private_ip))}"
]
}
}
foreach "${aws_instance.app_server.*}" {
resource "aws_route53_record" "app_server-${item.private_ip}" {
zone_id = "${something_defined_elsewhere}"
name = "appname-${join(\"-\", split(\".\", self.private_ip))}.mydomain.com"
type = "A"
records = ["${item.private_ip}"]
}
}
In my imagination, this creates a set of resources named things like aws_route53_record.app_server-10.0.0.1
which then get created/destroyed as you'd expect when the corresponding instances come and go. In case it wasn't obvious, item
here is imagined as the iteration variable.
While I was sketching this out I also came up with an alternative formulation that might end up leading to a simpler mental model:
resource "aws_instance" "app_server" {
# (...)
count = 5
provisioner "remote-exec" {
inline = [
"set-hostname-somehow appname-${join(\"-\", split(\".\", self.private_ip))}"
]
}
child_resource "aws_route53_record" "hostname" {
zone_id = "${something_defined_elsewhere}"
name = "appname-${join(\"-\", split(\".\", parent.private_ip))}.mydomain.com"
type = "A"
records = ["${parent.private_ip}"]
}
}
In this formulation, rather than generically supporting iteration over all lists we can just create a family of child resources for each "parent" resource. This feels conceptually similar to how per-instance provisioners work. I'm imagining that the child resource would interpolate like ${aws_instance.app_server.0.children.hostname.records}
, or indeed ${aws_instance.app_server.*.children.hostname.name}
to get all of the created hostnames.
When your environment always looks the same (e.g. for a long standing/running app), the declarative language of terraform is expressive enough for most needs. However, my use case (frequent spin ups/tear downs of AWS VPCs of a similar general structure but with plenty of instance/subnet variation) means that terraform is not the "start" of my pipeline. I need to combine some configuration, logic, and templates on the fly each time to define my desired environment before terraform can ingest it.
Right now, I'm planning on writing something custom (rake and erb?) to generate the needed terraform json and go from there because I don't see template-esque logic as a job for terraform's declarative config language, It already can use json as an incoming interchange format, so wouldn't it be more versatile to just leverage any of the existing template renderers out there as @ketzacoatl described? It would keep logic out of the configuration and keep the terraform language simple/clean.
Appreciate this discussion and I'm open to learning about a better way to solve these kinds of problems.
And I just realized why my suggested approach is flawed in some cases. The power of terraform is using derived data at runtime as variables elsewhere. If you render separately ahead of time in some cases, you lose that. The count=5
is an example of something you can't pre-render and then reference easily. Gah, sorry.
The power in terraform, IMHO, is that we have the flexibility to choose how much we do _before_ , _in_ TF, and after TF runs. In most cases, you need to start working with some wrapper to create the JSON you want, when the existing interpolation syntax won't get you what you want. I personally, have avoided this as I would rather keep the _before_ limited to a CI / admin who defines details in the terraform.tfvars
file to pass to TF when it runs. At the same time, this is also where the conditional logic and other interpolation/syntacitic sugar comes in (but is in some cases missing).
Last December in AMS Dockercon I had asked @mitchellh if there would be any plans to add such logic control in Terraform DSL, he explained his view on keeping Terraform as simple as possible maybe adding a little algebraic functionality (which has already been merged) and standard string operations.
I am glad there are second thoughts on this, but it is a decision that needs a lot of input and real world justification, so thatnks @phinze for bringing this up.
I have two real world scenarios I 've faced where an if
statement would have come in handy in Terraform:
rs.add()
itself by connectiong to the master node.if
would greatly mitigate the nastiness in my setupif
would be nice to instruct a remote-exec
provisioner to work its magic on just one node. Right now I just create a dummy resource that depends on .0.id
so that it gets provisioned from there. Ugly. There are ways around it, but still...Other scenario,
A simple scenario -
Optionally use Atlas artifacts to deploy infrastructure - and fallback to just AMI strings (if not using atlas).
Something like -
if ${var.atlas_enabled} {
resource "atlas_artifact" "machine" {
name = "${var.atlas_artifact.name}"
type = "aws.ami"
}
}
then in a resource
resource "aws_instance" "machine" {
if ${var.atlas_enabled} {
ami = "${atlas_artifact.machine.id}"
} else {
ami = "${var.ami_string}"
}
}
:+1:
I like the idea of even a simple
if ${var.atlas_enabled} {
}
There are lots of times where I'm building the same infrastructure for dev/stage/prod but don't need things in dev/stage as are needed in prod.
Even simpler, doesn't break the current syntax and prevents complexity (Inspired by Ansible):
resource "aws_instance" "machine" {
when = "${var.atlas_enabled}"
ami = "${atlas_artifact.machine.id}"
}
resource "aws_instance" "machine" {
when = "not ${var.atlas_enabled}"
ami = "${var.ami_string}"
}
@franklinwise that's pretty nice!
I like that it preserves the declarative syntax.
Preserving the declarative syntax seems like priority number one in my opinion. I like the 'when' solution. :+1:
Yes, I too like this suggested syntax.. when
, and the use of not
here, makes a _lot_ of sense.
This seems inadequate as it only supports the conditional creation of resources. In my usage, I've found myself wanting conditional expressions (not statements) several times.
The use case has been creating heterogenous groups of ec2 instances. I.e., I'd like to create 12 instances where the first 4 are of type m3.large and the remaining are c4.xlarge. I've hacked around this for the time being by using a lookup table and creating an entry for each index, but it's pretty nasty.
For clarification, what I'm looking for is something like:
instance_type = "${if count.index > 3 then c4.xlarge else m3.large}"
It might make sense to consider these use cases separately, if only for the goal of getting the simpler implemented faster than the more complicated and nuanced case. when
/ not
as a field on all resources is a _fantastic_ start. Figuring out the rest seems to be too much to figure out in the immediate.. why block one for the other?
Expanding on the referenced aws spot instances - I'd like to be able to express "spin up N instances in at least M availability zones, bidding the current bid price * X, But fallback to on-demand for any az where the bid price is > Y, or spot instance is unavailable". That seems complex (I have implemented it via a custom ruby script now), so maybe it's less a "we need conditionals" argument and more a "it would be nice if the aws provider abstracted instance types and did the "right thing").
Maybe the whole conditional thing could be handled by some sort of "call to external" hook to work the logic? It's a cop-out, in a way, but perhaps the most flexible in the end. If the suggested "when" could call an arbitrary script, with current context, you could kinda wedge in anything you needed as a shim, in only the spots where declarative is problematic. Can terraform do external calls like that already? I came into this from the side googling for spot instance solutions and seeing if terraform had support, so I'm not super familiar with current functionality there.
:+1: Really like @franklinwise suggestion. I think this is the way to go if control statements are added.
Just to add another use case similar to rafikk's example.
I'm using terraform to bring up a count of 'container instances' or 'nodes' in an AWS ECS cluster with consul running on each one. I want the first three to be run as servers but the rest to be agents.
Currently I have to duplicate the resource block which isn't very neat and any changes have to be done twice which allows too much room for human error.
It would be more flexible if I could do the following:
user_data = "${if count.index < 4 then SERVER_SCRIPT else AGENT_SCRIPT}"
@RJSzynal We have the same thing in our configuration. I really think support conditional logic inside of interpolation blocks is a must.
tl;dr my five cents is that I'm for the high-level idea of better conditional / looping support, but would like to stick to keeping it declarative.
I like the child resource proposal by @apparentlymart (far more than the foreach, which feels too imperative). That being said, for that specific example I feel like the resource isn't really a child. What may be better is a top-level "group" construct, which supports count
-style looping.
I'm also +1 for some basic conditional structure that gets interpolated. I don't like the imperative style "if then else" that's been recommended. I'd rather stick with a more procedural style thing that we already have, e.g., user_data = "${cond(var.use_foo, "foo", "bar")}"
, or perhaps some basic conditions user_data = "${cond(var.count = 1, "foo", "bar")}"
I agree with @rafikk with the lookup table being too verbose:
variable "lookup_table" {
default = {
"true" = "foo"
"false" = "bar"
}
}
variable "use_foo" {}
resource "aws_instance" "my_instance" {
ami = "${lookup(var.lookup_table, var.use_foo)}"
...
}
I'm also not fond of the if block surrounding a resource, as things start looking too imperative. A better approach to stick with the declarative format would be something like:
resource "aws_instance" "my_instance" {
ignore = "${var.disabled}"
}
I think setting count = 0
essentially does this, so this may be redundant. Sorry if I've repeated any ideas that may have already been expressed in earlier comments!
:+1:
@thegedge sorry I just noticed your response to my earlier example even though you posted it a while back.
The special thing I was imagining for "child resources" is that they'd always have an implied dependency on their parent, so if you delete the instance then that always deletes the record along with it... perhaps "child" is the wrong word, but I was going for "this thing only exists to support the thing it's nested inside".
@apparentlymart Ah yes, that would be nice. Could that also be solved with a "also destroy dependent resources" flag to terraform destroy
? Maybe modeling these things as being intimately connected (i.e., an atomic unit) would be useful in other, not immediately obvious ways too.
Anyways, I'm also going to add in an example from my team, since I didn't do that in my last comment and that's what @phinze was interested in seeing! We want our app developers to build things on top of a terraformed "cloud", but we want them to be able to this with a minimal configuration that doesn't require much/any terraform knowledge.
Many of our apps follow a similar recipe: rails app that sometimes needs redis, memcache, an S3 bucket, a place to run the rails app, and/or a database, maybe some other things. We'd like to construct a module that allows our app developers to conditionally select what they need for their app. It would look something like this from the app developer's perspective:
## /my_app/main.tf
module "my_app" {
source = "../app_template"
rails_server = "puma"
background_jobs = "resque"
memcache = true
}
## /app_template/main.tf
...
resource "aws_elasticache_cluster" "memcache" {
ignore = "${!var.memcache}"
...
}
resource "aws_elasticache_cluster" "redis" {
ignore = "${var.background_jobs != "resque"}"
...
}
...
An example I just ran into was attempting to create a list of users on AWS:
variable "devops" {
default = "user1,user2,user3"
}
resource "aws_iam_group" "Administrators" {
name = "Administrators"
path = "/"
}
resource "aws_iam_user" "devops" {
count = "${length(split(",", var.devops))}"
name = "${element(split(",", var.devops), count.index)}"
}
This creates a reasonable create plan
+ aws_iam_user.users.0
arn: "" => "<computed>"
name: "" => "user1"
path: "" => "/"
unique_id: "" => "<computed>"
+ aws_iam_user.users.1
arn: "" => "<computed>"
name: "" => "user2"
path: "" => "/"
unique_id: "" => "<computed>"
+ aws_iam_user.users.2
arn: "" => "<computed>"
name: "" => "user3"
path: "" => "/"
unique_id: "" => "<computed>"
But now user2 leaves.
-/+ aws_iam_user.users.1
arn: "arn:aws:iam::909704556315:user/user2" => "<computed>"
name: "user2" => "user3" (forces new resource)
path: "/" => "/"
unique_id: "AIDAIOESVNFST3M3THMO6" => "<computed>"
And the list gets shuffled down, deleting a legitmate user along the way.
A loop and a way to use a variable in a resource identifier would resolve this.
If you are building a module for any resource like RDS or any resource that change behaviour when additional attributes have been set then a evaluating function is required like the following which is available in cloudformation
"MyDB" : {
"Type" : "AWS::RDS::DBInstance",
"Properties" : {
"AllocatedStorage" : "5",
"DBInstanceClass" : "db.m1.small",
"Engine" : "MySQL",
"EngineVersion" : "5.5",
"MasterUsername" : { "Ref" : "DBUser" },
"MasterUserPassword" : { "Ref" : "DBPassword" },
"DBParameterGroupName" : { "Ref" : "MyRDSParamGroup" },
"DBSnapshotIdentifier" : {
"Fn::If" : [
"UseDBSnapshot",
{"Ref" : "DBSnapshotName"},
{"Ref" : "AWS::NoValue"}
]
}
}
}
AWS Elasticache Redis has a snapshot parameter. Sometimes we want to pass in a snapshot, and we set this param: snapshot_arns = ["${var.redis_snapshot}"]
Other times we do not want to pass in snapshot, however if you pass in an empty string, the elasticache cluster will not be built as its an invalid request. Even using modules and count=0, we cannot get around the conditional nature of this.
The use case that I ran into today was wishing for:
resource "aws_elb" "maybe-ssl-elb" {
listener {
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
if (not empty("${var.elb_server_certificate_arn}")) {
listener {
instance_port = 443
instance_protocol = "https"
lb_port = 443
lb_protocol = "https"
ssl_certificate_id = "${var.elb_server_certificate_arn}"
}
}
}
I have a module which I was using for each environment "./app" now I've split it up into "./app-prod" "./app-stage" etc because some environments use user_data
some do not. And the ones that do not I can't pass in a variable that says null
. An if
statement would allow us to do something like:
if "${var.userdata}" {
user_data = "${var.userdata}"
}
Of course having a null
or something would be good too. I might be missing something obvious.
I wanted to at least drop in an interesting way of doing if ! x.empty y else empty
right now, which we use to choose whether or not we chef provision with a private or public IP:
host = "${replace(self.private_ip, replace(var.bastion_hosts, "/^$/", "/^.*$/"), "")}"
If var.bastion_hosts
is empty, we end up replacing all of self.private_ip
with an empty string. For the connection host
parameter, this means terraform will pick the value for us. If it's non-empty, we search for var.bastion_hosts
, which should never occur in self.private_ip
, so we get the value of self.private_ip
back.
UPDATE:
My assumption that an empty string would have terraform pick the host was wrong, so we had to go full ternary. Here's how you do if foo empty then bar else baz
. If foo is a substring of baz this doesn't work.
${replace(replace(var.baz, replace(var.foo, "/^$/", "/^.*$/"), ""), "/^$/", var.bar)}
nice pattern, thank you for sharing! I'll look forward to the day when this is not necessary, but I imagine it'll do for now (the simple use case, that is).
@thegedge thanks for sharing, neat hack
Some use-cases for looping (along with a proposal for supporting them) in #3310.
+1.
Right now I'm working around this by applying tf files in one folder, or another folder conditionally.
Conditionals would be useful in our dns module, which wraps route53 or ultradns depending on the application lifecycle environment. To work around this, we're writing a terribly ugly workaround:
variable "dns_provider" { default = "none" }
# supporting multiple dns providers requires non-obvious code. It works
# by generating the count attribute from var.dns_provider. If the provider is
# supposed to be used, it will just pass count = 1 through. Otherwise, it
# defaults to count = 0.
# also note: because re2 doesn't support negative lookahead, we can't
# ensure that unexpected values are properly defaulting to count = 0.
resource "aws_route53_record" "record" {
count = "${replace(replace(var.dns_provider, \"route53\", 1), \"/\A(ultradns)|(none)\z/\", 0)}"
# other attrs here...
}
resource "ultradns_record" "record" {
count = "${replace(replace(var.dns_provider, \"ultradns\", 1), \"/\A(route53)|(none)\z/\", 0)}"
# other attrs here...
}
Hi @apparentlymart ,
Please I would like to know if this code block you wrote is what you expect or what is really support in terraform :
resource "aws_instance" "app_server" {
# (...)
count = 5
provisioner "remote-exec" {
inline = [
"set-hostname-somehow appname-${join(\"-\", split(\".\", self.private_ip))}"
]
}
child_resource "aws_route53_record" "hostname" {
zone_id = "${something_defined_elsewhere}"
name = "appname-${join(\"-\", split(\".\", parent.private_ip))}.mydomain.com"
type = "A"
records = ["${parent.private_ip}"]
}
}
I didn't know the child_resource configuration before, nor the python-like syntax ...{join("-", split(".", self.private_ip)...
And I don't see them in the terraform documentation. The inline argument allows bash commands and scripts but not python syntax for me.
I would to be clarified about that.
Thanks.
My bad, sorry,
I see join fucntion in the interpolation section.
@papiveron this issue is discussing possible new Terraform features; the constructs I included in that example were hypothetical, and not actually supported in Terraform today. Sorry for the confusion.
Seems like several people like my idea about using "when" @phinze @rossedman @knuckolls @ketzacoatl . Any expression that terraform supports could go in the when statement. If one wants to support looping then another declarative field of "with_items" could be used.
Here's an example of "with_items" (again, stolen from Ansible), where 'item' is a special keyword like 'var':
resource "aws_instance" "machine" {
ami = "${item.id}"
with_items = "${var.list_of_amis}"
}
and can be combined with "when"
resource "aws_instance" "machine" {
ami = "${item.id}"
when = "${var.custom_ami | length > 0}"
with_items = "${var.list_of_amis}"
}
One use case for me is the ability to use the same code to construct VPCs, but include or not include certain modules based on the value of variables. For example, a simple if... clause (I don't even need the 'then' portion)
if $include_directConnect {
module "direct_connect" {
source = "git::ssh://git@stash:7999/tf_module/direct_connect"
...
}
}
This would allow me to use the same VPC code across different development groups and different environments (dev, qa, stage, prod) without having to remember to comment/un-comment the code every time. It would be a simple matter of setting the variable in that group/environment config file and then just running get & apply.
Very similar to earlier mentioned cases. I'm running into this in module use. I'm creating several VPCs in my AWS cloud. Some with public connectivity, some without. Writing multiple modules to handle that feels very wasteful. Some kind of flow control would make this much cleaner.
Ah, having played some more, I think the count example is insufficient.
Say, I want to do something between 0 and N times. The common way this seems to get done, is via something like count = "${length(split(",", var.private_subnets))}"
However, this results in either needing trailing commas, or being unable to handle the 0 case:
Eg:
${length(split(",", ""))}
is 1${length(split(",", "foo"))}
is 1${length(split(",", "foo,bar"))}
is 2${length(split(",", "")) -1 }
is 0${length(split(",", "foo")) - 1}
is 0${length(split(",", "foo,")) - 1}
is 1${length(split(",", "foo,bar")) - 1}
is 1neither of those is very satisfying
@directionless FYI, the compact
function was somewhat recently added to deal with empty strings when splitting so, for example:
${length(compact(split(",", "")))}
should be 0${length(compact(split(",", "foo")))}
should be 1${length(compact(split(",", "foo,bar")))}
should be 2@thegedge Indeed! I'd just found that in the docs, and testing, that does seem to work.
I'll PR to the modules I need it in.
I'm using count to conditionally execute a resource:
resource "cloudstack_secondary_ipaddress" "clustervip" {
count= "${var.servers.instance_count * length(split(",",var.requiredzones)) * length(split(",",var.servers.hostname)) * var.alwayson}"
virtual_machine = "${element(cloudstack_instance.servers.*.id, count.index)}"
}
Where var.alwayson is just a variable with 0 or 1.
@cihatgenc - What happens if you don't have any cloudstack_instance.servers ? Doesn't this blow up? I'd like to use similar binary logic to determine whether or not to create an aws_vpn_gateway, but can't figure out how that would work. I don't think it could.
@pll - in the example resource above I'm adding an ip to a cloudstack instance. So yes, if there is no server it will fail. I did not use the resource aws_vpn_gateway myself, but it could work if you add a count to the resource aws_vpc
Just to express the idea (don't know if this actually works):
variable "vpc_count" {
default = 1
}
variable "dovpn" {
description = "0 = No, 1 = Yes"
default = 0
}
resource "aws_vpc" "main" {
count = "${var.vpc_count}
cidr_block = "${var.vpc_cidr_block}"
tags {
Name = "${var.vpc_name}"
}
}
resource "aws_vpn_gateway" "vpn_gw" {
count = ${var.vpc_count * var.dovpn}
vpc_id = "${element(aws_vpc.main.*.id, count.index)}"
}
@cihatgenc - Interesting. So, if I create the VPC with a count variable, and I only create 1, then I have a splat list where aws_vpc.main.0.id is the ID of the first VPC in a 1-element list of VPCs? So, if I do not want a VPN gateway attached, I set the count of that to effectively to 0 ( vpc_count =1 * gw_count = 0), then index into the list of VPCs. But doesn't that result in assigning the VPN gateway to the 0th element of my 1-element list?
Sorry, I guess I don't really understand how splats work in this case.. thanks for your help and patience though :)
@pll - No, it will not assign it to the 0th element.
When count = 0, TF will interpret it as "I have to do this resource 0 times", so will do nothing.
When the count = 1, TF will do the resource 1 time on the vpc_id in the 0th element of the list (hence the count.index).
@cihatgenc - Ah, okay. Very cool. Thanks for explaining this to me!
I really like the when syntax as well. There are a few domains that are used only in production, so when I spin up intermediate environments they're not needed at all. My current solution is to have a script generate the json for each environment, so having a when property on a resource and only create when that condition is satisfied is muy bueno.
this would be very useful for me. Is there any chance for this to be implemented soon?
@marekrogala - Fwiw - I've been able work around most of my requirements for conditionals by using boolean logic and splats.
While "neat hacks" are fun, or helpful, or whatever, it's sucks that we fill up our _clean_ Terraform with crazy and unreadable hacks to get "_if, then, else_" logic to fit in place.
I wouldn't call boolean logic a "neat hack". And I wouldn't say that my Terraform code is any less clean than it was or any less readable. In fact, I'd actually it's quite an elegant solution that prevents polluting your code with a lot of 'if, then, else' logic. I go out of my way to avoid using that construct and case statements in any language to begin with.
Sorry if I am not being clear @pll, I am referring to solutions like: ${replace(replace(var.baz, replace(var.foo, "/^$/", "/^.*$/"), ""), "/^$/", var.bar)}
. This is an unreadable hack, in my book - it's doing one thing, but takes a lot of thought to understand the original intention or purpose.
@ketzacoatl - Oh, agreed. That is a complete hack! What I did with my boolean logic hack was to set a variable to 0 or 1, then use count to determine whether or not to actually create the object. I found that pretty elegant.
In a way, what you describe is "clean" (again, in my book), but not when it comes to the implementation - eg, if you have to start thinking about list indexes, simply because you want a conditional/boolean on/off for an object... we have a problem
I think the 'when' syntax is the cleanest most declarative approach that we can take in solving this problem.
As it stands, with my development, I'd have to create several variants of my modules in order to handle the several variations of the implementation. While I embrace and totally agree with being declarative I think some basic logic structures don't complicate the reading and give extremely valuable control to both the module author and consumer.
a for
would be very useful. For example, we want to create a DNS record for each server we start up, and an associated health check for it. The current available method will create one DNS record with all the IPs of the servers. This does not allow for health checks (since they are for the entire record, and creating the health checks is also per a singular IP).
I would like to do something like the following:
for (servers.ips) resource "dns" "record" {
ip = "${current.public_ip}"
...
}
Or something similar.
This is just one example, having a for statement will also simplify lots of our terraform scripts which create lots of very similar instances (we use a module to help with this, but we still need to configure the module over and over again just to change a few params)
The notion of a for
is partially supported.
You can do the following. I oversimplified depending on what data type servers.ips
is.
resource "dns" "record" {
ip = "${lookup(servers.ips, count.index)}"
count = "${length(servers.ips)}"
}
@BSick7 That is actually a cool trick. Thanks, I will give it a try.
If the "thing" created is more complex though then you would need to create a module, but as-long as count can be supplied for modules as well it should work.
@BSick7 Unfortunately
count.index: count.index is only valid within resources
This makes it more difficult (perhaps still possible though), will just have to access dependencies by index also.
@BSick7 I gave it a try. However I have error's due to Cycle dependency (thought I don't think there is one)
Here is the record and hc definition:
resource "aws_route53_health_check" "my-server_hc" {
port = "18083"
type = "HTTP"
resource_path = "/"
failure_threshold = "5"
request_interval = "10"
count = "${length(split(var.instance_public_ips))}"
ip_address = "${element(split(var.instance_public_ips), count.index)}"
tags = {
Name = "my-server-health-check"
}
}
resource "aws_route53_record" "my-server" {
zone_id = "${var.zone_id}"
name = "my-server${var.delimiter}${var.domain_suffix}"
set_identifier = "${var.set_identifier}"
type = "A"
ttl = "${var.ttl}"
weight = 1
health_check_id = "${element(aws_route53_health_check.my-server_hc.*.id, count.index)}"
count = "${length(split(var.instance_public_ips))}"
records = [ "${lookup(split(var.instance_public_ips), count.index)}"]
}
And I get the following error:
* Cycle: module.dns.aws_route53_health_check.my-server_hc (destroy), module.dsp.module.my-server.output.instance_public_ips, module.dsp.output.haproxy-public-ips, module.dns.var.instance_public_ips, module.dns.aws_route53_record.my-server (destroy), module.dsp.module.my-server.aws_instance.cloudinit (destroy), module.dsp.module.my-server.aws_instance.cloudinit
Sharing a hack @BSick7 and I use to enable/disable resources.
We use a string variable to set count = 0
when the string is empty, or count = 1
when the string is non-empty.
An overly simplified example:
variable "foo" {
type = "string"
default = ""
}
resource "aws_route53_record" "foo" {
zone_id = "${aws_route53_zone.primary.zone_id}"
name = "${var.foo}"
type = "A"
ttl = "300"
records = ["${aws_eip.lb.public_ip}"]
# HACK to return 0 if var.foo is empty, or 1 if non-empty.
# Disables this resource when var.foo is not set.
# We're assuming that the # char would be an invalid char in this string (e.g., FQDN)
count = "${length(compact(split("#", var.foo)))}"
}
Credit: @thegedge ^^^
@steve-jansen : You can just set the variable to either 1 or 0 and not worry about using length().
variable "createN" { default = 0 }
resource "aws_instance" "foo" {
count = "${var.createN}"
...
}
I use this to create VGW resources. Some VPCs have a directConnect, and therefore need a VGW created, others don't. Set 'create_vgw = 1' and you get a vgw, set it to 0, you don't.
@pll yep, we use that simple 0/1 too. The string method is just another tool in the toolbox if you don't want to remember to sync the state of two variables in Atlas. Our use case is a module that should or should not run based on a value existing. Both ways work.
We tried something like this at one point:
variable "flag" {
default = {
stage = 1
prod = 0
}
}
variable "env" { default = "stage" }
...
count = "${lookup(var.flag, var.env)}"
But it made Terraform really sad and error barfy. I'll have to try again to see if this has been fixed in a recent release.
@fromonesrc - My experience has been that using lookup() and length() tend to cause the most sadness and barfiness. Which is why I mentioned my simple 0/1 hack. It seems less prone to sadness :)
I want the choice of having a /data directory that's either a mount point for an ebs_block_device, or a simple directory off the root_block_device. The technique I was hoping to use was to skip creation of the ebs_block_device when the volume_size was 0, but I don't see any way to make creation of the ebs_block_device conditional (which is how I landed in this thread).
Maybe instead of inventing brand new declarative language, embed Lua interpreter and allow for specifying pure functions in it? I think it's better option for everyone. Haproxy and Nginx already embed Lua interpreters to give few examples.
Saltstack has the concept of "renderers", they are responsible for "rendering" the text that becomes the code for salt to parse. It seems like an over-abstraction, except it provides a completely pluggable system, so some user-developers use the default (jinja), while others write in python directly or another templating engine (like mako). In practice, it has been very enjoyable to work with saltstack because of this type of flexibility. A lua interpreter would be interesting, if provided along with an analog to salt's renderers. Either way, I would also agree that this _greatly_ simplifies the question of how to provide conditionals and things like that.
In #5278 I proposed embedding lua interpreter with sample syntax. Most importantly it allows for discovery of used variables, so terraform knows when to re-execute them. For example:
instance_type = "#{
if ${count.index} > 3 then
return 'c4.xlarge'
else
return 'm3.large'
end
}"
#{}
defines code to evaluate, and ${}
interpolates used variables. In this case terraform knows to re-evaluate this function each time count.index
change, otherwise use cached result from tfstate.
I am not a huge fan of adding lua interpreter to terraform.
One of the great things about terraform is that it unifies many technologies and providers into one language. It seems like a detractor from simplification.
@sheerun thanks for the Lua suggestion!
For others who want to discuss the merits and details of that, let's do that in the comments of #5278 rather than here, since that's a pretty big architectural shift with lots of implications and I it's better to let this issue keep its original purpose of just collecting use-cases.
@BSick7, please feel free to repeat your feedback on the other issue. :grinning:
There are two separate issues here.
I'm proposing that we implement No. 1 above and move No. 2 to another thread because No 1 is valuable, simple and has a clean obvious way of implementing it. Which is by using "when" declaratively, which many others liked (see my comment on Oct 7, 2015). I'll repost it here for convenience:
resource "aws_instance" "machine" {
when = "${var.atlas_enabled}"
ami = "${atlas_artifact.machine.id}"
}
resource "aws_instance" "machine" {
when = "not ${var.atlas_enabled}"
ami = "${var.ami_string}"
}
If you want to do No. 2, we should look at how ansible does it with jinja2 templates in the string.
@franklinwise - i like this. I agree there are two things here.
I vote to support your proposal.
+1
There are real world needs to address both at some time, however I think
maintaining the original purpose (declarative conditionals) should be the
focus of this thread. I'd hope this is on the roadmap by now.
~Brian
On Thu, Mar 3, 2016 at 12:14 PM, ketzacoatl [email protected]
wrote:
I vote to support your proposal.
—
Reply to this email directly or view it on GitHub
https://github.com/hashicorp/terraform/issues/1604#issuecomment-191942876
.
+1 / support
Dont forget people use the tool because of its simplicity. Keep the tool simple with conditional creation of resources which seems the best solution. Having an option to remove an attribute from the resource instead of removing the complete resource is an idea to keep in mind as it is less typing and less error prone.
I agree simple conditional logic would be better than full-blown interpreter at least for now.
It would be nice if terraform introduced boolean logic ad comparisons into interpolation syntax we can get little more advanced expressions as well (&&
||
==
!=
)
@franklinwise - Regarding your example:
resource "aws_instance" "machine" {
when = "${var.atlas_enabled}"
ami = "${atlas_artifact.machine.id}"
}
One can already accomplish this using count = "${var.true_or_false}"
and setting that value to 0 or 1. Unfortunately, there's no enforced data integrity in the terraform DSL. One could set var.true_or_false to 8 since there's no way to declare variable type or enforce a var is essentially a boolean.
Using your method has some obvious aesthetic advantages. In short, it's clear, concise, and obvious that there's a condition to be met. Whereas using count =
the intent is not obvious at all unless the reader of the code has become quite familiar with the terraform DSL. Even then, it can be tough to decipher.
Implicit in your example though, is enforced data integrity. The value of such a variable can be interpreted to be anything more or less than 0/1, or true/false. Either there has to be type enforcement, or terraform must ignore the actual value and use only that there is or is not value.
As long as that is built in to the implementation, I would wholeheartedly support this solution!
Great idea, thanks for sharing it!
+1
re: @franklinwise's point #1:
- Conditional Creation of Resources. Do or don't create them. (Is Declarative, Is simple)
The when
parameter seems like it would be declarative friendly. I ran into a use case along these lines with a module last week and posted about it in more detail.
For my existing problem, I just need to ignore resources based on an input variable. So count = 0
seems like a work around for now.
What about conditional outputs? I.e. if I don't create resource A, I don't want to output A's IP address.
@rodlogic a good work around is to wrap your terraform apply
call in a bash script and then use terraform output
in some bash conditional.
@rodlogic - If you create resources based on count, create your outputs based on splats.
output "public_ids" {
value = "${join(",",aws_subnet.public.*.id) }"
}
@pll @rodlogic thankfully 0.7 will have better support for splats too! https://github.com/hashicorp/terraform/issues/2821#issuecomment-195051359
@nathanielks any idea when .7 is due out ?
I don't, unfortunately. That's something the core team would be able to answer!
Okay, thanks :)
There were far too many comments for me to read through here, but I'll throw my two cents in.
I don't think a declarative syntax is going to cut it enough to be useful. If we're going to go down this route, my feeling is that we may as well do it "right".
I agree that Salt's approach is very nice. The JSON is put through a renderer before being ingested. This seems like the most flexible approach.
I know some are floating the idea of adding a declarative when
on the resource. As others have said, this works for the resource, but, let's say for example placement_group
on an aws_autoscaling_group
. The placement_group
is only valid if the instance type is > t2.micro
. So what we need is something like (using ERB syntax, but Jinja or anything else would work as well).
resource "aws_autoscaling_group" "my_asg" {
<% unless ['t2.nano', 't2.micro'].include? var.servers.size -%>
placement_group "my_placement_group"
# Other stuff
<% end -%>
}
Or whatever.
@thegranddesign I'd encourage you to take the time :smile:. There's some good stuff.
@nathanielks I read through about half. I've got work that needs done. :wink:
@thegranddesign :+1:
Ok, I finished reading the rest of the comments. when
will definitely handle some but not all use cases as I described in my original comment.
I'll move further discussion to #5278
@thegranddesign I secretly just wanted to find out how fast you can read
One of my favourite parts of terraform is that it's really easy to do a bit of scripting externally and generate terraform JSON.
For this reason, I don't think adding a pre-processor into the core is particularly beneficial.
IMO it makes sense to focus on the logic features which cannot be achieved by a pre-processor.
A when
is already achievable very easily with count, be it as a 1 or 0 from true/false, or as a count * 0/1.
when
is nice but only cleans up the existing syntax. I'd find a not()
operator bloody useful, and an isemtpy()
, as we don't have conditionals and those require regex tricks to work, which is just not nice.
passing empty arrays is hard too, as "" is not an empty, and you ahve to use the convulated compact(split("",var.myvalue))
to turn an empty string into an empty array.
So if it was a vote, i'd ask for more expressive ways of using the existing vars to do conditionals, rather than additional conditional arguments on resources, although if we could get both i'd be jolly happy.
@serialseb, how can I apply this technique to an ebs_block_device? It doesn't have a count
parameter.
@lubars - I believe count
is a meta param and can be applied to anything.
It can only be applied to resources, which somewhat limits it's use as a
"when" condition
On May 9, 2016 22:39, "Paul" [email protected] wrote:
@lubars https://github.com/lubars - I believe count is a meta param and
can be applied to anything.—
You are receiving this because you commented.
Reply to this email directly or view it on GitHub
https://github.com/hashicorp/terraform/issues/1604#issuecomment-217966782
Oh, ebs_block_device. I think my brain interpreted that as aws_ebs_volume... Sorry about that.
You could just create ebs devices and use attachments if you want to use count? Had some issues with doing that with root devices though.
I thought about that - I guess I'd add a count
to both the aws_ebs_volume
and aws_volume_attachment
and make sure they match?
And I've seen the admonition against mixing ebs_block_device with aws_ebs_volume
; is root_block_device
a kind of ebs_block_device
(and could that be the source of the issues you encountered)?
The count would work, order is guaranteed so 0..n volumes, 0..n attachments and they'll match, you element(aws_ebs_volume.*.id,count.index) and off you go. You probably will want the device name to come from a list too as those are required.
I've not investigated that much but i tried having the aws_ebs_volume from a snapshot and an attachment, using the root device name, and it failed on me because the ami already had the volume mount on it. In normal scenarios it may well just work, untested.
Please correct me if I am wrong, but I believe the problem with using volumes attached outside of the aws_instance
block is that if you want to run init on the instance you've created, you don't have any guarantees for when the volumes are actually attached, and there is potential for race conditions or bad assumptions during init.
In short, there are times when you really need to define an EBS volume inline with aws_instance
, and using count
is not _always_ available or reasonable.
Maybe we're kinda polluting this github issue with this conversation? My fault, I'm sorry :/ i think terraform will always wait for the instance to be up before attaching the drive for aws_instance, for asg you can configure that, scripts i tend to loop till device becomes avail
@ketzacoatl I had this exact problem with Azure (classic) using azure_data_disk
- couldn't get disk creation to precede instance creation - even using a [depends_on] clause.
@pll - the example I gave was to focus on the syntax, rather the specific case of creating a machine or not.
Let's say I want a different hard drive configuration for production.
Dev:
Standard EBS Volume
Prod:
Multiple Volumes
resource "aws_volume_attachment" "ebs_att" {
device_name = "/dev/sdh"
volume_id = "${aws_ebs_volume.example.id}"
instance_id = "${aws_instance.web.id}"
when = "${var.env == prod}"
}
resource "aws_instance" "web" {
ami = "ami-21f78e11"
availability_zone = "us-west-2a"
instance_type = "t1.micro"
tags {
Name = "HelloWorld"
}
}
resource "aws_ebs_volume" "example" {
availability_zone = "us-west-2a"
size = 1
when = "${var.env == prod}"
}
Our infrastructure makes use of public and private load balancers in AWS. Public-facing applications (be they browsers or other systems away from our infrastructure) make use of the public load balancers, and applications inside our infrastructure make use of private load balancers to talk to each other. Some applications, as a result, have both a public and a private load balancer, but some do not.
We were trying to come up with a way to only add private load balancers to autoscaling groups if they exist. A conditional would have been great here, but alas we ended up with this:
load_balancers = ["${compact(split(",", concat(aws_elb.elb-public.name, ",", var.private_elb_name)))}"]
where aws_elb.elb-public
is from a reusable module, so that just works, and var.private_elb_name
gets the name of the private load balancer to attach to the ASG during the instantiation of the module.
I have a TF module that creates a security group for a nomad agent. If there was a conditional available such that I could do the equivalent of {% if open_ephemeral_ports %}
, it would allow that one module to add the 20000 to 60000 range to the security group, if the user-developer wants that, rather than having to create a second module that is nearly identical (and making a user-developer choose between multiple modules).
I've expanded on the use of count
to implement conditional logic a bit that some of you may find useful. I've always been unhappy with the need to duplicate code to model different options on the same type of resource. It makes it very difficult to create reusable generic modules. Additions of resources like aws_security_group_rule
and aws_route
have started to help this a lot, but there are still plenty of scenarios where it is a problem. One such scenario would be configuring an autoscaling group for use with an ELB or without.
See the module we have created here: https://github.com/unifio/terraform-aws-asg
We still need to implement each use case separately unfortunately, but leveraging the module from another stack works pretty much the way I would want driven by the value of data passed in and without the need for feature flags.
Using signum
, we were able to simplify the logic required to create the toggle on and off logic for various resources.
resource "aws_autoscaling_group" "asg" {
count = "${signum(length(var.min_elb_capacity)) + 1 % 2}"
.
.
resource "aws_autoscaling_group" "asg_elb" {
count = "${signum(length(var.min_elb_capacity))}"
.
.
We are then using coalesce
to return the ID of the resource that happens to be generated and returning it as a generic output to the calling template.
output "asg_id" {
value = "${coalesce(join(",",aws_autoscaling_group.asg.*.id),join(",",aws_autoscaling_group.asg_elb.*.id))}"
}
If you add support for conditional logic on resources, please add it to fields within resources as well.
I would like to be able to do something like this:
resource "aws_elastic_beanstalk_environment" "myEnv" {
name = "test_environment"
application = "testing"
setting {
onlyif = var.min_nodes != ""
namespace = "aws:autoscaling:asg"
name = "MinSize"
value = "${var.min_nodes}"
}
}
@oillio , I recently learned about coalesce()
- https://www.terraform.io/docs/configuration/interpolation.html#coalesce_string1_string2_ - and it looks like you might also benefit from that function, I think it would meet your intention/need in that example snippet.
How could coalesce be used in this case? I want to avoid setting the MinSize beanstalk value under a given condition...
@oillio, well, I would think you could say "use this variable or the default", with null being the default, but I can see how you might want that to be more explicit, or exclude the setting completely if the variable is not set. I guess that would in part depend on the AWS API.
For this exact example, it would be even easier to just set a default value for the min_nodes variable.
MinSize is not the best example. There are a number of beanstalk fields that don't lend well to default values. For instance, environment variables, imageId, etc.
Additionally, a lot of default values clutters the terraform output result.
The only way coalesce helps you out here is with very specific conditions. It would not be as flexible as you would like it for arbitrary settings.
An example that would work though is as follows:
variable "min_nodes" {
default = ""
}
resource "aws_elastic_beanstalk_environment" "myEnv" {
count = "${signum(length(var.min_nodes)) + 1 % 2}"
name = "test_environment"
application = "testing"
}
resource "aws_elastic_beanstalk_environment" "myEnv_with_min" {
count = "${signum(length(var.min_nodes))}"
name = "test_environment"
application = "testing"
setting {
namespace = "aws:autoscaling:asg"
name = "MinSize"
value = "${var.min_nodes}"
}
}
output "myEnv" {
value = "${coalesce(join(",",aws_elastic_beanstalk_environment.myEnv.*.id),join(",",aws_elastic_beanstalk_environment.myEnv_with_min.*.id))}"
}
The resource that ends up being created, determined by whether min_nodes
is set to anything other than an empty string, would be returned as the output.
Again, this would not be a great fit to model numerous combinations, but if there is one or two specific conditions you are interested in, this will work.
How do I prevent this error? I'm trying to use count to disable creating a subnet and I have an output variable that I want to use for when the subnet is created.
* Resource 'aws_subnet.api_public_z3' does not have attribute 'id' for variable 'aws_subnet.api_public_z3.id'
Where:
variable "zone3_enabled" {
default = "0"
}
output "subnet_data_sql_z3" {
value = "${join("", aws_subnet.data_sql_z3.*.id)}"
}
resource "aws_subnet" "data_sql_z3" {
count = "${var.zone3_enabled}"
vpc_id = "${var.vpc_id}"
cidr_block = "${var.cidr_prefix}.202.0/24"
availability_zone = "${var.vpc_zone_3}"
}
@fwisehc It appears that the following could be happening: assuming count
is {0,1}, when count = 0
invoking join("", aws_subnet.data_sql_z3.*.id)
has a dependency on aws_subnet.data_sql_z3.0.id
, which does not have an attribute id
because it does not exist.
We use the count=${length(compact(split(",", var.foo)))}
trick a lot (I just discovered the ${signum(length(var.bar))}
one) for conditional resource creation but a separate property like "when" mentioned above would make our tf files easier to read
In addition count does not apply to sub-resources: in several cases we would like to be able to create sub-resources conditionally (listeners on ELB are a good example)
I have used cloudformation a lot in the past (and really prefer terraform, no question about that) and the conditions in cloudformation were enough for all the use-cases I remember.
A quick example:
Declare conditions
"Conditions" : {
"CreateBastion" : {"Fn::Equals" : [{"Ref" : "BuildBastion"}, "Yes" ]},
"CreateAdminSubnet" : {"Fn::Not" : [ {"Fn::Equals" : [{"Ref" : "AdminSubnet"}, "" ]} ] }
}
Use it for conditional creation of a resource:
"BastionInstance" : {
"Type" : "AWS::EC2::Instance",
"Condition" : "CreateBastion",
"Properties" : {...}
}
Use it for if / then / else constructs:
"SubnetId" : {"Fn::If" : [ "CreateAdminSubnet",{"Ref" : "AdminSubnet"},{"Ref" : "PublicSubnet"}]}
Hello,
On my side, I don't need so much conditional in the terraform script, but in template, it's essential.
For example, I'm generating n instance for my application server, and an HA proxy right after.
This haproxy needs to get all the instances inserted in a config file... but It's almost impossible to insert multiple line using terraform.
Example :
from a list of instances, you want to generate these line :
server SERVER1 10.100.0.10:1080 check inter 2s fall 3 rise 1 maxconn 10000
server SERVER2 10.100.0.11:1080 check inter 2s fall 3 rise 1 maxconn 10000
server SERVER3 10.100.0.15:1080 check inter 2s fall 3 rise 1 maxconn 10000
each of the server name and the IP being from an instance object of course.
It's driving me nuts, when actually, a simple foreach in the template would have done in in 5 sec.
My solution today : (be careful it's spicy... and the server number doesn't increment actually)
In my terraform file
resource "template_file" "haproxy_config_file" {
template = "${file("${path.module}/templates/provisioner/haproxy.cfg.tpl")}"
vars {
backend_server_lines = "${base64encode(join("ENDBEGIN", openstack_compute_instance_v2.myserver.*.network.0.fixed_ip_v4))}"
instance-count = 0
}
}
In my template file :
${replace(replace(concat(concat("BEGIN",base64decode(backend_server_lines)),"END"),"BEGIN",concat(concat("server SERVER",instance-count + 1)," ")),"END",":1080 check inter 2s fall 3 rise 1 maxconn 10000\n ")}
I'm not sure I should be proud of it, but the line are almost generated... I miss only the server name to be unique... but I can't avoid to be ashamed of my code, I don't think I will be able to read it tomorrow anymore.
@pmithrandir
The simpler solution would be a script to generate the file instead of your creative solution.
@mengesb hi.
A template engine for example...
Just kidding
Terraform allows only very very basic value interpolation in the template_file
resource
https://www.terraform.io/docs/configuration/interpolation.html
If instead it would allow full-blown golang templates like
https://github.com/hashicorp/consul-template#templating-language, it would solve most conditional logic / iteration problems, e.g. one described by @pmithrandir
I have not found any terraform plugin, offering that feature. Lets see if I can write one myself...
Apologies if I am stating the obvious.. using golang templates might be nice for templates, scripts, etc.. but does not cover conditional resources.
Has there been any more though/progress on using "where/when" to define conditional logic for resources?
+1
would be nice to have conditionos
+1 for conditional evaluation. With tf being declarative, I tend to lean towards a single statement ternary approach like val = ${cond(eval_statement, entity_when_true, entity_when_false)}
.
Also +1 for supporting golang template syntax.
I have a situation where I'd like to use the Spotinst service (who provide a terraform plugin I use) to build all environments except production, where I require an ASG production dedicated instances. Because the resource types differ, it's complicated to build terraform in such a way that it builds this tier using different resource types in different environments.
If you're looking for a way to do if-statements in the meantime, I wrote up a blog post that captures a few of the basic techniques that are possible today: Terraform tips & tricks: loops, if-statements, and gotchas. It's not as nice as having built-in language support for conditionals, but it's surprising just how far you can go with creative use of the count
parameter.
Thanks @brikis98 for creating the above page. It has been very helpful for me with this issue and others.
Yep, the page by @brikis98 is very helpful, thank you. There are some cases which are not probably covered by these workaround hacks though.
For example, I'd like to include https listener in aws_elb only if the (in that case mandatory) certificate id variable is not empty/null. Since the listener parameter has a type of object array, the string interpolation functions don't seem to apply to it easily.
@phinze - My kingdom for a case statement:
# Count = 1 only if
# provider = aws AND nodes != 0 AND length(vpc_security_group_ids) is 0
count = "${lookup(map("aws", "${signum(var.nodes)}"), "${var.provider}" , "0") * ( signum(length(var.vpc_security_group_ids)) - 1 ) * -1 }"
or a Ternary Operator
and some logic operators
Fantastic thread, and thank you!
As the Oracle would say ... "Oh, don't worry about it. As soon as you step outside that door, you'll start feeling better. You'll remember you don't believe in any of this logic crap. You're in control of your own DSL, remember?
I think the ongoing lesson is "Don't write your own DSL. It will suck. You'll have to write a parser and editor/IDE support and those will suck, too." Saltstack was mentioned before in the context of preprocessors, but I think the fundamental lesson there is actually that it's domain specific language _isn't_ a DSL, it is just a data-structure. You can encode that data-structure pretty much however you want (jinja pre-processed yaml is the standard, but there are plenty of other options). The important thing is that it renders down to a data-structure which is de-serialized and then drives the functionality. This seems to be a design paradigm that doesn't suck.
@ahammond Which is the design paradigm of Terraform: we fully support JSON for this reason and projects out there do use full languages like Ruby to generate Terraform JSON. I'm not sure if you were saying Terraform didn't do this, but I want to make it clear that Terraform _is_ a data structure currently and is not a DSL.
HCL can definitely be viewed as a DSL, but its really just a human-friendly language for writing data structures. We don't support conditionals or other non-data structure features yet so you can't consider it much more than that...
However, I've also learned the lesson (via Vagrant) of "don't use a full programming language" as well because people do really crazy things that make it difficult to safely load and use configuration.
@mitchellh that's awesome! I'm going to take a look into the JSON format! I'm super-new to Terraform, found this issue while looking for a way to iterate that didn't suck.
FYI to those who are following this issue: Terraform 0.8 will have some basic support for conditionals and boolean operations in the interpolation language, giving us some first-class support for conditionals, albeit only inside interpolations for now.
Although we don't yet have an explicit when
or if
meta-attribute as was discussed above, this new conditional operator makes it easier to achieve that using count
:
# Create this only in production
count = "${var.environment == "production" ? 1 : 0}"
Seems likely that a hypothetical future meta-attribute for explicitly enabling/disabling resources conditionally would build on the new boolean operators; for now, the ternary conditional operator should hopefully tidy up some of the existing tricks people were doing with setting count conditionally.
@apparentlymart This is awesome, thanks!
Very excited to leverage this with TF 0.8.x, thank you for all the great work here!
awaiting for TF .8
IT'S HERE! YAHOOOOOOOOOOOOOOOOOOO!!!
Closing this as it has landed in Terraform 0.8.x :)
@stack72 I assume you refer to the new ternary operator? While that covers many of the use cases, it won't help with optionally specifying attributes, or "sub hashes" inside the resources. For example optionally declaring multiple listeners in an ELB. Would be nice to have an issue for tracking those, too.
But great work, the ternary operator for sure makes things easier especially in generic modules!
Given the age and large potential scope of this issue, I guess it makes sense for us to close it out and capture some more-specific use-cases in other issues, since this one was originally motivated with just collecting information on patterns people were using, and didn't have a tight enough scope that it would ever likely be closed by any real implementation work.
The trick there, of course, is that we previously intentionally consolidated several issues describing other use-cases over here, so there's a bunch of closed issues linked from here that capture some real use-cases we presumably don't want to "lose".
FWIW though, I understand that improved conditional stuff is still on the radar even if this particular issue is closed.
@tmatilai - Would this feature request solve your issue? https://github.com/hashicorp/terraform/issues/7034
@oillio yeah, as far as I understand, that would cover the sub-resource/block case!
Even after that one use case would be conditional individual attributes, although I haven't personally been so much in need of those compared to sub-resources.
Trying to get consistent output of a module using count to dictate the version of a resource to create.
module code:
resource "aws_s3_bucket" "lc_enabled" {
count = "${var.lifecycle_enabled ? 1 : 0}"
bucket = "${var.bucket}"
acl = "${var.acl}"
lifecycle_rule {
id = "log"
prefix = "log/"
enabled = true
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 60
storage_class = "GLACIER"
}
expiration {
days = 90
}
}
versioning {
enabled = "${var.versioning}"
}
}
resource "aws_s3_bucket" "lc_disabled" {
count = "${var.lifecycle_enabled ? 0 : 1 }"
bucket = "${var.bucket}"
acl = "${var.acl}"
versioning {
enabled = "${var.versioning}"
}
}
failing output code:
output "bucket.name" {
value = "${var.lifecycle_enabled == 1 ? aws_s3_bucket.lc_enabled.id : aws_s3_bucket.lc_disabled.id}"
}
Apparently if any ternary values don't exist, the entire ternary will fail silently. the output above does not display at all. However if I'm using lifecycle_enabled = false and change the output to:
output "bucket.name" {
value = "${var.lifecycle_enabled == 1 ? "something" : aws_s3_bucket.lc_disabled.id}"
}
Here because all these values exist the conditional parses correctly. Is this expected behavior? Ultimately I'm looking for a way to consolidate the output so if I'm overlooking a more direct method that'd be great.
Hi @aaroncaito!
That issue is being tracked over in hashicorp/hil#50. It's something I'd like to fix before too long since I know it's annoying and makes the conditional operator not as useful as it could otherwise be. It requires some adjustment to how the interpolation syntax evaluator works, so it's not a quick fix but something that is on my radar to take a look at.
thanks @apparentlymart in this case it'd be even better if we didn't need unique resource names when only one will be created. my output "thing.attribute" {value = "resource.SHARED_NAME.attribute"}
. with no conditional eval required. I'll track that other issue as well as that will help other use cases.
My workaround for the above S3 with LC enabled/disabled use case:
output "bucket.name" {
value = "${coalesce(join("", aws_s3_bucket.lc_enabled.*.id), join("", aws_s3_bucket.lc_disabled.*.id))}"
}
Edit: the coalesce is superfluous:
output "bucket.name" {
value = "${join("", aws_s3_bucket.lc_enabled.*.id, aws_s3_bucket.lc_disabled.*.id)}"
}
However, this usage of join()
with multiple lists is not documented, so I'm not sure it is a good idea:
join(delim, list) - Joins the list with the delimiter for a resultant string.
https://www.terraform.io/docs/configuration/interpolation.html#join_delim_list_
@pdecat your workaround is similar to another workaround I saw recently:
https://github.com/hashicorp/terraform/issues/12453#issuecomment-284273475
using
join
to turn the lists into strings to get past the type check
I have a case where I want to be able to define the AWS instance type in a variable, and - along side it - the size of the root volume. Since on AWS only EBS volumes can have a size, this becomes a problem. If I specify the instance type as a type that doesn't have EBS backed storage then no value will work for the volume_size, and AWS will always complain that you cannot do that.
That is...
resource "aws_instance" "foo" {
...
root_block_device {
volume_size = "0"
}
}
(applies to both aws_instance, and aws_opsworks_instance, although it's strange that they don't share the code)
My workaround? Change the terraform code so that if the parameters within root_block_device
are ineffective (ie 0
in the case of the size) the block device isn't added to the definition, and AWS is happy. It's not great, but recompiling the tool isn't a big deal as I've already got a number of patches applied.
@gerph I am likely to do a similar hack to allow a single aws_launch_configuration to either have name
or name_prefix
— ie, so that normally my LCs can have autogenerated names but I can pin to an old name for a rollback if necessary.
I have two variables lob and role. As of now it is like this:
target = "${var.role == "hzc" ? "TCP:11410" : "HTTP:80/"}"
But my requirement is:
if lob==anything and role==hzc then "TCP:11410
else if lob==kolk and role==kata then "HTTPS:443/index.html"
else "TCP:8443"
How to map this in conditional statement?
@arkaprava-jana I assume by "anything" you mean "is defined" and not the text "anything"? You could nest conditionals to get something like:
target ="${var.role == "hzc" && var.lob != "" ? "TCP:11410" : var.lob == "kolk" && var.role == "kata" ? "HTTPS:443/index.html" : "TCP:8443"}"
Yes you are right. Looking for nested conditional example only.
I'm trying to get clever with the ternary operator and conditionals, but I think I have the wrong idea about them. I have the following code in an ec2 module I've built on top of the ec2_instance resource.
I have a var I've created called "total_instances".
I've set: count = "${var.total_instances}"
I've tried the following:
Name = "${count.index} = 1 ? {var.instance_name}-${var.environment} : {var.instance_name}${count.index+1}-${var.environment}}"
I've also tried:
Name = "${var.total_instances} = 1 ? {var.instance_name}-${var.environment} : {var.instance_name}${count.index+1}-${var.environment}}"
If the count of instances I pass to the module = 1, then I just want the instance to be tagged as "server-ops", but if the count I pass from my main.tf to the module is greater than 1, I want it to assign "server1-ops" as the name.
Any help or clarification would be very helpful, thank you!
@armenr I think the problem is yourr syntax... it should be ${condition ? true : false}
all in one evaluation block. You may also want to use the format() interpolation to combine strings and variables easier but in this case the simplest is probably to only use the ternary to determine if the count is needed or not:
${var.instance_name}${var.total_instances > 1 ? count.index + 1 : ""}-${var.environment}
A format() example would be:
${var.total_instances > 1 ? format("%s-%02d-%s", var.instance_name, (count.index + 1), var.environment) : format("%s-%s", var.instance_name, var.environment)}
@jaygorrell - Thanks! I'll fiddle with this and let you know how that works out. Thank you!
I don't know if I am late to the party, but I am currently facing similar issues to what is being discussed here:
One use case that does not work with the current implementation is assigning pre-allocated Floating IPs vs allocating them. The scenario is like this:
Production has a fixed amount of servers with pre-allocated IP addresses that need to remain the same all the time.
Testing has the same infrastructure but can have more/less servers and Floating IPs are allocated and released on demand.
(I use openstack as an example here, but I imagine there are quite many parallel scenarios regardless of platform)
The following does not work:
resource "openstack_networking_floatingip_v2" "tf_instance_ips" {
count = "${length(var.floating_ips) > 0 ? 0 : var.count}"
pool = "${var.floating_ip_pool_name}"
}
resource "openstack_compute_floatingip_associate_v2" "tf_instance_ips" {
count = "${var.count}"
floating_ip = "${length(var.floating_ips) > 0 ? element(var.floating_ips, count.index) : element(openstack_networking_floatingip_v2.tf_instance_ips.*.address, count.index)}"
instance_id = "${element(openstack_compute_instance_v2.tf_instance.*.id, count.index)}"
}
This will throw
element() may not be used with an empty list
because of https://github.com/hashicorp/terraform/issues/11210
However even when that is fixed, it is much easier to read and cleaner to have something like this:
if "${length(var.floating_ips) > 0}" {
resource "openstack_compute_floatingip_associate_v2" "tf_instance_ips" {
count = "${var.count}"
floating_ip = "${element(var.floating_ips, count.index)}"
instance_id = "${element(openstack_compute_instance_v2.tf_instance.*.id, count.index)}"
}
}
if "${length(openstack_networking_floatingip_v2.tf_instance_ips.*.address) > 0}" {
resource "openstack_networking_floatingip_v2" "tf_instance_ips" {
count = "${length(var.floating_ips) > 0 ? 0 : var.count}"
pool = "${var.floating_ip_pool_name}"
}
resource "openstack_compute_floatingip_associate_v2" "tf_instance_ips" {
count = "${var.count}"
floating_ip = "${element(openstack_networking_floatingip_v2.tf_instance_ips.*.address, count.index)}"
instance_id = "${element(openstack_compute_instance_v2.tf_instance.*.id, count.index)}"
}
}
PS: The workaround for the above use case is to use coalescelist function, but it is sub-optimal for example in cases where type of the input value changes conditionally or when depending on condition different sets of resources need to be spawned, while remaining resources are not different.
resource "openstack_compute_floatingip_associate_v2" "tf_instance_ips" {
count = "${var.count}"
floating_ip = "${element(coalescelist(var.floating_ips, openstack_networking_floatingip_v2.tf_instance_ips.*.address), count.index)}"
instance_id = "${element(openstack_compute_instance_v2.tf_instance.*.id, count.index)}"
}
how do i get something like this in terraform
Properties:
{% if PostgresSnapshot is defined and RDSSnapshot != "" %}
DBSnapshotIdentifier:
Ref: PostgresSnapshot2
{% else %}
DBName: {{ DbName |default('sample', true) }}
MasterUsername:
Ref: MasterU
MasterUserPassword:
Ref: MasterUserPW
{% endif %}
@mthak Something similar to that will be supported by Terraform 0.12 which will be released later this summer: https://www.hashicorp.com/blog/terraform-0-1-2-preview
I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
Most helpful comment
Even simpler, doesn't break the current syntax and prevents complexity (Inspired by Ansible):