ternary conditional operators in terraform - amazon-web-services

I am working quite a bit with terraform in hopes of building an in-house solution to standing up infra. So far, I have written most of the terraform code and am building out the Ansible code for post-processing of the instances that are stood up. I am shuttling over the dynamic inventory from terraform to Ansible using this little Go app that can be found here, https://github.com/adammck/terraform-inventory. All of that works well.
As I get more into the terraform code, I am trying to use a ternary conditional operator on the ssh key for Linux instances. The goal is to "reuse" this resource on multiple instances.
My resource looks like this ..
resource "aws_key_pair" "key" {
key_name = var.ssh_key
count = var.create_ssh_key ? 1 : 0
public_key = file("~/.ssh/${var.ssh_key}")
}
I've included the [count.index] within the key argument here ...
resource "aws_instance" "linux" {
ami = var.linux_ami
instance_type = var.linux_instance_type
count = var.linux_instance_number
subnet_id = data.aws_subnet.itops_subnet.id
key_name = aws_key_pair.key[count.index].key_name
...
$ terraform validate comes back clean.
$ terraform plan -var-file response-file.tfvars -var "create_ssh_key=false" does not.
The std error is as follows ...
$ terraform plan -var-file response-file.tfvars -var "create_ssh_key=false"
╷
│ Error: Invalid index
│
│ on instances.tf line 16, in resource "aws_instance" "linux":
│ 16: key_name = aws_key_pair.key[count.index].key_name
│ ├────────────────
│ │ aws_key_pair.key is empty tuple
│ │ count.index is 0
│
│ The given key does not identify an element in this collection value.
What am I missing?
Thanks for the feedback!

if count in aws_key_pair is 0, there is no key to reference later on at all.
So you have to check for that and use null to eliminate key_name in such a case:
resource "aws_instance" "linux" {
ami = var.linux_ami
instance_type = var.linux_instance_type
count = var.linux_instance_number
subnet_id = data.aws_subnet.itops_subnet.id
key_name = var.create_ssh_key ? aws_key_pair.key[0].key_name : null

I think the root problem in your example here is that your resource "aws_key_pair" "key" block and your resource "aws_instance" "linux" block both have different values for count, and so therefore it isn't valid to use the count.index of the second to access an instance of the first.
In your case, you seem to have zero key pairs (it says aws_key_pair.key is empty tuple) but you have at least one EC2 instance, and so your expression is trying to access the zeroth instance of the key pair resource, which then fails because it doesn't have a zeroth instance.
If you are using Terraform v0.15 or later then you can use the one function to concisely handle both the zero- and one-instance cases of the key pair resource, like this:
resource "aws_instance" "linux" {
# ...
key_name = one(aws_key_pair.key[*].key_name)
# ...
}
Breaking this down into smaller parts:
aws_key_pair.key[*].key_name is a splat expression which projects from the list of objects aws_key_pair.key into a list of just strings containing key names, by accessing .key_name on each of the objects. In your case, because your resource can only have count 0 or 1, this'll be either a zero-or-one-element list of key names.
one then accepts both of those possible results as follows:
If it's a one-element list, it'll return just that single element no longer wrapped in a list.
If it's a zero-element list, it'll return null which, in a resource argument, means the same thing as not specifying that argument at all.
The effect, then, will be that if you have one key pair then it'll associate that key pair, but if you have no key pairs then it'll leave that argument unset and thus create an instance that doesn't have a key pair at all.

Related

The argument "ami" is required, but no definition was found

I am new to terraform, I am learning modules. I am stuck in a problem.
I have modules folder as root which contain two folders EC2 and IAM and they have terraform code in them. In the same modules folder I have main.tf file which is a module file for calling EC2 instance terraform code.
For your information, EC2 instance folder contain two files one for instance resource and second is for defining variables.
My EC2 file looks like this.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
count = "${var.ec2_count}"
ami = "${var.ami}"
instance_type = "${var.instance_type}"
tags = {
Name = "App_Instance"
}
}
My Variable.tf file looks like this.
variable "ec2_count" {
type = number
default = 1
}
variable "ami" {
type = string
}
variable "instance_type" {
type = string
}
My main.tf file looks like this.
module "EC2" {
source = "./EC2"
}
Now I want that when I type terraform plan command, it should take input at the command prompt but it is showing me below error.
PS: I don't want to pass the value of the variable in the module.
C:\Users\PC\Desktop\AWS_Modules\Modules>terraform plan
╷
│ Error: Missing required argument
│
│ on main.tf line 1, in module "EC2":
│ 1: module "EC2" {
│
│ The argument "ami" is required, but no definition was found.
╵
╷
│ Error: Missing required argument
│
│ on main.tf line 1, in module "EC2":
│ 1: module "EC2" {
│
│ The argument "instance_type" is required, but no definition was found.
Since you are new to modules, there are a few things to consider:
Where are you defining the variables
How are you passing the values to variables required by the module
Ideally, you would pass the values to variables when calling the module:
module "EC2" {
source = "./EC2"
ami = "ami-gafadfafa"
instance_type = "t3.micro"
}
However, since you said you do not want to do that, then you need to assign the default values to variables on the module level. This can be achieved with:
variable "ec2_count" {
type = number
default = 1
}
variable "ami" {
type = string
default = "ami-gafadfafa"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
This way, if you do not provide values when calling the module, the instance will be created and you are still leaving the option to provide the values if someone else were to use your module.
The third option is to have a separate variables.tf file in the root module (i.e., the same directory where you are calling the module from) and not define the values for variables. It would be basically a copy+paste of the file you have on the module level without defining any default values. Here is an example:
# variables.tf on the root module level
variable "ec2_count" {
type = number
}
variable "ami" {
type = string
}
variable "instance_type" {
type = string
}
However, in this case, it is impossible not to use the input variable names when calling the module. For this to work with providing the values on the CLI, the code would have to change to:
module "EC2" {
source = "./EC2"
ec2_count = var.ec2_count
ami = var.ami
instance_type = var.instance_type
}
There is no other way the module can know how to map the values you want it to consume without actually setting those variables to a certain value when calling the module.

What is needed to make Terraform tolist() to work for my code after the replacement of list()

This is a simple question and I am trying to understand what documentation I can use to better understand this but this is what is happening. I'm looking to update my code of Terraform and I am running into an issue when it comes to using tolist(). My snippet of code:
subnet_ids = var.zone_awareness_enabled ? var.subnet_ids : tolist(var.subnet_ids[0])
When running my terraform plan, I get this error:
| subnet_ids = var.zone_awareness_enabled ? var.subnet_ids : tolist(var.subnet_ids[0])
│ ├────────────────
│ │ var.subnet_ids[0] is "subnet-234324df3dfd"
│
│ Invalid value for "v" parameter: cannot convert string to list of any single type.
When using list(), I had no problems but this is confusing since it is giving me the correct subnet but it's unable to process the request. Would appreciate any help.
Depending on the requirements it could only be a simple change to the code to look like the following:
subnet_ids = var.zone_awareness_enabled ? var.subnet_ids : [ var.subnet_ids[0] ]
On the other hand, if you take a look at the tolist documentation, it says the following:
Pass a set value to tolist to convert it to a list. Since set elements are not ordered, the resulting list will have an undefined order that will be consistent within a particular run of Terraform
So it is meant to convert a set (with no indices) to a list but it cannot convert a string to a list.

Terraform use data resource to get Cluster Snapshot?

I have a global Aurora RDS cluster that takes automated snapshot everyday. My DB instance looks like this :-
new-test ( Global Database )
new-test-db ( Primary Cluster )
new-test-db-0 ( Writer Instance )
I have enabled automated snapshots for the db. What i am trying to achieve is to get the ARN for my snapshot using data resource. My ARN is something like this :-
arn:aws:rds:us-west-2:123456789101:cluster-snapshot:rds:new-test-db-2022-08-23-08-06
This is what my data resource looks like :-
data "aws_db_cluster_snapshot" "db" {
for_each = toset(var.rds_sources)
db_cluster_identifier = each.key
most_recent = true
}
where var.rds_sources is a list of strings. But when i try to access the arn using :-
data.aws_db_cluster_snapshot.db[*].db_cluster_snapshot_arn
I keep running into
Error: Unsupported attribute
│
│ on ../main.tf line 73, in resource "aws_iam_policy" "source_application":
│ 73: cluster_data_sources = jsonencode(data.aws_db_cluster_snapshot.db[*].db_cluster_snapshot_arn)
│
│ This object does not have an attribute named "db_cluster_snapshot_arn".
Which is weird since the attribute is laid out in the official docs. Thank you for the help.
This is my provider file :-
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.75"
}
archive = "~> 2.2.0"
}
required_version = "~> 1.2.6"
}
Since the data source is using for_each, the result will be a map of key value pairs. In terraform, there is a built-in function values [1] which can be used to fetch the values of a map. The return value is a list, so in order to get all the values for all the keys the splat operator is used [2]. Then, since the data source is returning multiple attributes and only one is required (namely db_cluster_snapshot_arn), the final expression needed is as follows:
jsonencode(values(data.aws_db_cluster_snapshot.db)[*].db_cluster_snapshot_arn)
[1] https://www.terraform.io/language/functions/values
[2] https://www.terraform.io/language/expressions/splat

Data resource of Network_interface throws invalid count argument Terraform

I am trying to get the network interface ids of a VPC endpoint using the data resource of aws_network_interface, the code for which looks like
resource "aws_vpc_endpoint" "api-gw" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.aws_region}.execute-api"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.datashop_sg.id]
private_dns_enabled = true
subnet_ids = [data.aws_subnet.private-1.id]
}
data "aws_network_interface" "endpoint-api-gw" {
count = length(aws_vpc_endpoint.api-gw.network_interface_ids)
id = tolist(aws_vpc_endpoint.api-gw.network_interface_ids)[count.index]
}
I get the following error
Error: Invalid count argument
│
│ in data "aws_network_interface" "endpoint-api-gw":
│ count = length(aws_vpc_endpoint.api-gw.network_interface_ids)
│
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work
│ around this, use the -target argument to first apply only the resources that the count depends on.
I have also tried the for_each and it gives similar error of it is dependent on resources. I am running out of ideas. It would be of great if someone can help
The error is clear:
count = length(aws_vpc_endpoint.api-gw.network_interface_ids)
is only known after apply. You can't do this. count value must be known at plan time. You have to run your TF in two stages:
Execute TF with -target to deploy only aws_vpc_endpoint.api-gw using option.
Execute it again, to deploy the rest.
Otherwise, you have to re-factor you code, and fully eliminate the dependency of the count on aws_vpc_endpoint.api-gw.network_interface_ids.

Use different value if another resource is created

I have a Terraform module that I would like to modify. Currently my module creates a service account. I would like to modify it so that someone could pass in an existing service account OR if one is not passed in, then the module creates a service account is it would have originally.
Originally my service account looked like this:
resource "google_service_account" "scheduler" {
account_id = "${var.prefix}-scheduler"
project = var.project
}
I've added the following variable to my variables.tf file:
variable "service_account_email" {
default = null
description = "Existing service account for running ... jobs. If null a new service account will be created."
}
What I originally thought to do was to add some locals
locals {
service_account_count = var.service_account_email == null ? 1 : 0
service_account_email = var.service_account_email == null ? google_service_account.scheduler.email : var.service_account_email
}
Then I could change my service account to look like
resource "google_service_account" "scheduler" {
count = local.service_account_count
account_id = "${var.prefix}-scheduler"
project = var.project
}
And then wherever I would have referenced google_service_account.scheduler.email I can instead reference local.service_account_email .. It doesn't look like I'm able to do this, however, for a few reasons.
I get the following error if I try to use the locals block that mentioned above:
│ Because google_service_account.scheduler has "count" set, its attributes must be accessed on specific instances.
│
│ F`or example, to correlate with indices of a referring resource, use:
│ google_service_account.scheduler[count.index]
╵
If I change it so that I'm using google_service_account.scheduler[count.index].email instead, I get the following error:
│ Because google_service_account.scheduler has "count" set, its attributes must be accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ google_service_account.scheduler[count.index]
╵
Now I'm sort of stuck, because I can't force any resources that would originally have referenced google_service_account.scheduler.email to instead reference the var.service_account_email variable that is being passed in for cases where we would prefer to use an existing service account.
Since you are using count, you have to use [0] to access your resource:
service_account_email = var.service_account_email == null ? google_service_account.scheduler[0].email : var.service_account_email