27

In terraform, is there any way to conditionally use a data source? For example:

 data "aws_ami" "application" {
     most_recent = true
     filter {
         name = "tag:environment"
         values = ["${var.environment}"]
     }
     owners = ["self"]
}

I'm hoping to be able to pass in an environment variable via the command line, and based on that, determine whether or not to fetch this data source.

I know with resources you can use the count property, but it doesn't seem you can use that with data sources.

I would consider tucking this code away in a module, but modules also can't use the count parameter.

Lastly, another option would be to provide a "Default" value for the data source, if it returned null, but I don't think that's doable either.

Are there any other potential solutions for this?

Paul Gear
  • 236
  • 4
  • 12
djt
  • 7,297
  • 11
  • 55
  • 102

1 Answers1

45

You can use a conditional on data sources the same as you can with resources and also from Terraform 0.13+ on modules as well:

variable "lookup_ami" {
  default = true
}

 data "aws_ami" "application" {
   count       = var.lookup_ami ? 1 : 0
   most_recent = true
   filter {
     name   = "tag:environment"
     values = [var.environment]
   }
   owners = ["self"]
}

One use case for this in Terraform 0.12+ is to utilise the lazy evaluation of ternary statements like with the following:

variable "internal" {
  default = true
}

data "aws_route53_zone" "private_zone" {
  count        = var.internal ? 1 : 0
  name         = var.domain
  vpc_id       = var.vpc_id
  private_zone = var.internal
}

data "aws_route53_zone" "public_zone" {
  count        = var.internal ? 0 : 1
  name         = var.domain
  private_zone = var.internal
}

resource "aws_route53_record" "www" {
   zone_id = var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id
   name    = "www.${var.domain}"
   type    = "A"
   alias {
     name                   = aws_elb.lb.dns_name
     zone_id                = aws_elb.lb.zone_id
     evaluate_target_health = false
   }
}

This would create a record in the private zone when var.internal is true and instead create a record in the public zone when var.internal is false.

For this specific use case you could also use Terraform 0.12+'s null to rewrite this more simply:

variable "internal" {
  default = true
}

data "aws_route53_zone" "zone" {
  name         = var.domain
  vpc_id       = var.internal ? var.vpc_id : null
  private_zone = var.internal
}

resource "aws_route53_record" "www" {
   zone_id = data.aws_route53_zone.zone.zone_id
   name    = "www.${data.aws_route53_zone.zone.name}"
   type    = "A"
   alias {
     name                   = aws_elb.lb.dns_name
     zone_id                = aws_elb.lb.zone_id
     evaluate_target_health = false
   }
}

This would only pass the vpc_id parameter to the aws_route53_zone data source if var.internal is set to true as you can't set vpc_id when private_zone is false.


Old Terraform 0.11 and earlier answer:

You can in fact use a conditional on the count of data sources but I've yet to manage to work out a good use case for it when I've tried.

As an example I successfully had this working:

data "aws_route53_zone" "private_zone" {
  count        = "${var.internal == "true" ? 1 : 0}"
  name         = "${var.domain}"
  vpc_id       = "${var.vpc_id}"
  private_zone = "true"
}

data "aws_route53_zone" "public_zone" {
  count        = "${var.internal == "true" ? 0 : 1}"
  name         = "${var.domain}"
  private_zone = "false"
}

But then had issues in how to then select the output of it because Terraform will evaluate any variables in the ternary conditional before deciding which side of the ternary to use (instead of lazy evaluation). So something like this doesn't work:

resource "aws_route53_record" "www" {
   zone_id = "${var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id}"
   name = "www.example.com"
   type = "A"
   alias {
     name                   = "${aws_elb.lb.dns_name}"
     zone_id                = "${aws_elb.lb.zone_id }"
     evaluate_target_health = "false"
   }
}

Because if internal is true then you get the private_zone data source but not the public_zone data source and so the second half of the ternary fails to evaluate because data.aws_route53_zone.public_zone.zone_id isn't defined and equally with the other way around too.

In your case you probably just want to conditionally use the data source so might be able to do something like this:

variable "dynamic_ami" { default = "true" }
variable "default_ami" { default = "ami-123456" }

data "aws_ami" "application" {
  most_recent = true
  filter {
    name = "tag:environment"
    values = ["${var.environment}"]
  }
  owners = ["self"]
}

resource "aws_instance" "app" {
    ami = "${var.dynamic_ami == "true" ? data.aws_ami.application.id : var.default_ami}"
    instance_type = "t2.micro"
}
ydaetskcoR
  • 53,225
  • 8
  • 158
  • 177
  • Thanks for all that info. I had no idea I could use count in a data source. However, id probably want to use it more like you are in your first examples. The problem is, if my dynamic data source doesn't exist yet, then it's query to aws causes a failure. So just putting the ternary in the resource wouldn't be enough. I'll have to experiment with this though. Thanks! – djt Jan 26 '17 at 13:18
  • A good use case would be if you have a resource shared by multiple workspaces and only want it created when deploying a certain workspace. So your "prod" workspace might be provisioning resource X, and your "test" workspace might be referencing X as a datasource. – jschmitter Feb 18 '19 at 22:48
  • Why would the data source need to be conditional there? Surely just whatever uses the data source output? And, as mentioned and pre 0.12, if you made the data source conditional then anything that depends on it (even conditionally) will fail due to the eager evaluation of ternary statements in HCL1. – ydaetskcoR Feb 19 '19 at 08:06