Say I have a Terraform module for creating a AWS EC2 instance.
Now, I want the user to be able to either use the default VPC, or provide another VPC ID.
So I define the following input variables:
# variables.tf
variable "default_vpc" {
description = "Whether or not deploy the instance in the default VPC"
type = bool
}
variable "vpc_id" {
description = "VPC ID to deploy the instance in"
type = string
default = ""
}
Now, in case the user passes false for default_vpc, I want to ensure that he does pass some value in vpc_id. Is that possible?
While there's no way to implement using input variable validation, depending on the Terraform version you can use preconditions.
resource "null_resource" "vpc_precondition_validation" {
lifecycle {
precondition {
condition = (var.default_vpc == false && var.vpc_id != "") || (var.default_vpc == true && var.vpc_id == "")
error_message = "There has been error while validating the inputs for default_vpc and vpc_id variables."
}
}
}
In this case, when we input false for the variable default_vpc and we don't provide a value for the vpc_id variable it's going to print the error message.
When default_vpc is set to true, it will allow the vpc_id variable to be an empty string.
Then in the resource where the vpc_id is required, you can use a ternary condition, assuming the default vpc id attribute is retrieved from a data source:
...
vpc_id = var.default_vpc == false ? var.vpc_id : data.aws_vpc.default.id
...
You could be declaring it as a boolean map like this:
variable "vpc_id" {
type = map(bool)
default = {
true = "default_vpc is true"
false = "default_vpc is false"
}
}
Now you could be using it like this:
var.vpc_id[${var.default_vpc}]
Example:
Related
variable.tf
variable "private_subnets" {
type = list
default = ["subnet-abc1,subnet-abc2"]
}
main.tf
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = "${var.private_subnets}"
tags = {
Name = var.cluster_name,
environment = var.environment
}
}
This the present code i want use varable.tf for subnet this is way i want achive like this subnet-abc1,subnet-abc2
Considering that your variable has a wrongly defined default value, the first thing to fix is that:
variable "private_subnets" {
type = list(string)
default = ["subnet-abc1", "subnet-abc2"]
}
The way you are currently defining the default value, i.e, ["subnet-abc1,subnet-abc2"], is a list, however, it is a list with one element. Each element in a list of strings needs to start and end with double quotes, i.e. "some value". You can read more about lists in [1].
Then, you would just need to fix the code in the main.tf to look like this:
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = var.private_subnets
tags = {
Name = var.cluster_name,
environment = var.environment
}
}
The syntax for subnets in the main.tf file is the old terraform syntax, so this will work without double quotes and ${}.
[1] https://developer.hashicorp.com/terraform/language/expressions/types#list
i have sort issue with split this is the answer
variable.tf
variable "private_subnets" {
default = "subnet-abc1,subnet-abc2"
}
main.tf
resource "aws_db_subnet_group" "rds_subnet_group" {
name = var.cluster_name
subnet_ids = "${split(",", var.private_subnets)}"
tags = {
Name = var.cluster_name,
environment = var.environment
}
I have a VPC cidr in a map variable, which is defined in Terraform. What I am trying to do is to use a specific value in that map variable in order to dynamically create a subnet in Terraform. Any advice how this would be accomplished would be very helpful. Below is how my variables are defined:
VPC CIDR
variable "vpc_cidr" {
default {
us-east-1 = "192.1.0.0/16"
us-west-1 = "192.2.0.0/16"
us-west-2 = "192.3.0.0/16"
}
}
AWS Subnet
resource "aws_subnet" "public_subnets" {
count = "${length(local.availability_zone_names)}"
vpc_id = "${aws_vpc.vpc.id}"
cidr_block = "${cidrsubnet("pulling aws vpc cidr from map variable", newbits, netnum)}"
availability_zone = "${local.availability_zone_names[count.index]}"
map_public_ip_on_launch = true
}
To be honest I am not fully sure about your syntax of defining the variable. I'd rather put it like this (although your version might be also correct):
variable "vpc_cidr" {
type = map
default = {
"us-east-1" = "192.1.0.0/16"
"us-west-1" = "192.2.0.0/16"
"us-west-2" = "192.3.0.0/16"
}
}
and then:
cidr_block = "${cidrsubnet(var.vpc_cidr[YOUR_CURRENT_REGION], newbits, netnum)}"
Not sure if you have YOUR_CURRENT_REGION defined as a variable somewhere. Otherwise you probably need to extract from a data source:
data "aws_region" "current_region" {}
and use current_region instead of YOUR_CURRENT_REGION
Created a list of ranges as per below
subnet_names = ["subnet-lister", "subnet-kryten", "subnet-rimmer", "subnet-cat", "subnet-holly",]
subnet_cidrs = ["192.2.128.0/18", "192.2.0.0/17", "192.2.208.0/20", "192.2.192.0/20", "192.2.224.0/20",]
With this in the subnets.tf
resource "google_compute_subnetwork" "subnet" {
name = "${var.subnet_names}-subnet"
ip_cidr_range = var.subnet_cidrs
network = var.network_name
region = var.subnet_region
And the below in variables.tf (for the module)
variable "subnet_names" {
description = "The name to use for Subnet "
type = list(string)
}
variable "subnet_cidrs" {
description = "The cidr range for for Subnets"
type = list(string)
}
But getting the following message from Terraform.
Error: Incorrect attribute value type
on ..\..\..\Test-Modules\red\dwarf\subnets.tf line 3, in resource "google_compute_subnetwork" "subnet":
3: ip_cidr_range = var.subnet_cidrs
Inappropriate value for attribute "ip_cidr_range": string required.
I'm pretty new to this, can you help me work out what I am going wrong. I've seem someone else use a list for the cidr range (mind you that was for AWS). Does GCP not support this?
It looks like what you're trying to do is actually create several subnets. For that, you should use a map variable and a loop.
variable "subnets" {
type = map(string)
}
resource "google_compute_subnetwork" "subnet" {
for_each = var.subnets
name = each.key
ip_cidr_range = each.value
...
}
Then you can provide the subnets like:
subnets = {
subnet-lister = "192.2.128.0/18",
subnet-kryten = "192.2.0.0/17",
...
}
According to the Terraform Docs, the ip_cidr_range takes only one CIDR-Block, not a list. So you need to create one resource per subnet like this:
resource "google_compute_subnetwork" "subnet" {
count = length(var.subnet_names)
name = "${var.subnet_names[count.index]}-subnet"
ip_cidr_range = var.subnet_cidrs[count.index]
network = var.network_name
region = var.subnet_region
...
I would also recommend to restructure your data a bit, so you can use for_each instead of count (look at Ben's answer). This behaves better in case you later change your configuration and for example insert a new subnet, as described nicely in this post.
I am configuring elasticache cluster using terraform and everything works fine!
Now my requirenment is that i want to do the dynamic configuration inside resource for cluster-mode.
Below is my common code..
resource "aws_elasticache_replication_group" "elasticache_redis_cluster" {
replication_group_id = "cache"
engine_version = "${var.engine_version}"
node_type = "${var.node_type}"
port = "${var.elasticache_port}"
parameter_group_name = "${var.param_group_name}"
security_group_ids = ["${aws_sg.id}"]
subnet_group_name = "${aws_elasticache_subnet_group.subnet_group.id}"
}
Now i want to perform following operation based on the passed parameter.
if (${var.cluster_mode == "enable") {
automatic_failover_enabled = true
cluster_mode {
replicas_per_node_group = 1
num_node_groups = 1
}
}
else {
number_cache_clusters = 2
}
Above code based on matched condition should be appended inside the configuration of the cluster.
Any help will be highly appreciated!
Terraform Conditionals only support ternary assignment of values.
Eg, they can only be in the form of:
resource "cool_thing" "my_resource" {
is_prod_thing = "${var.env == "production" ? true : false}"
}
Values returned from the ternary operation must be the same type, and there is no direct way to internally switch between differing resource configurations.
A possible workaround is to use the count Meta-Parameter to create zero or more resources based on a variable value:
variable "cluster_mode" {
default = "enable"
}
locals {
cluster_count = "${var.cluster_mode == "enable" ? 1 : 0}"
non_cluster_count = "${var.cluster_mode == "enable" ? 0 : 1}"
}
resource "aws_elasticache_replication_group" "elasticache_redis_cluster" {
# Configuration for clustered nodes
count = "${local.cluster_count}"
}
resource "aws_elasticache_replication_group" "elasticache_redis_non_cluster" {
# Configuration for non-clustered nodes
count = "${local.non_cluster_count}"
}
This way you can describe both configurations of the resource that might be needed, and switch which one is created based on the value of cluster_mode.
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?
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"
}