concatenate two variables in terraform - terraform

i'm trying to create a kubernetes cluster from kops using terraform,following code is a part of infrastructure, i'm trying to create the name concatenate with two variables, and i'm getting illegal char error line two, error happens because im trying to define the name with concatenate variables. is it possible in terraform?
resource "aws_autoscaling_group" "master-kubernetes" {
name = "master-"${var.zone}".masters."${var.cluster_name}""
launch_configuration = "${aws_launch_configuration.master-kubernetes.id}"
max_size = 1
min_size = 1
vpc_zone_identifier = ["${aws_subnet.subnet-kubernetes.id}"]

With latest terraform 0.12.x terraform format doc , you could do better like:
resource "aws_autoscaling_group" "master-kubernetes" {
name = format("master-%s.masters.%s", var.zone, var.cluster_name)
}

Try this:
resource "aws_autoscaling_group" "master-kubernetes" {
name = "master-${var.zone}.masters.${var.cluster_name}"
# ... other params ...
}

I would say rather concatenating at resource level use the locals first define a variable in locals and then you can utilize it at resource level
Locals declaration
locals {
rds_instance_name = "${var.env}-${var.rds_name}"
}
Resource Level declaration
resource "aws_db_instance" "default_mssql" {
count = var.db_create ? 1 : 0
name = local.rds_instance_name
........
}
This is as simple as its need to be ....

Related

Using terraform to create multiple resources based on a set of variables

I have a set of variables in terraform.tfvars:
resource_groups = {
cow = {
name = "Cow"
location = "eastus"
},
horse = {
name = "Horse"
location = "eastus"
},
chicken = {
name = "Chicken"
location = "westus2"
},
}
my main.tf looks like this:
...
module "myapp" {
source = "./modules/myapp"
resource_groups = var.resource_groups
}
variable "resource_groups" {}
...
./modules/myapp.main.tf look like this:
module "resource_group" {
source = "../myapp.resource_group"
resource_groups = var.resource_groups
for_each = {
for key, value in try(var.resource_groups, {}) : key => value
if try(value.reuse, false) == false
}
}
variable "resource_groups" {}
and ../myapp.resource_group looks like this:
resource "azurerm_resource_group" "resource_group" {
name = var.resource_groups.cow.name
location = var.resource_groups.cow.location
}
variable "resource_groups" {}
My hope is that after terraform plan I would see that three new RGs would be set for addition. Infact I do get three new ones, but they all use the name and location of the cow RG, because I specified var.resource_groups.cow.name The problem is I have tried all kinds of different iterators in place of .cow. and I can't get terraform to use the other variables in the terraform.tfvars file. I have tried square brackets, asterisks, and other wildcards. I am stuck.
I am looking to define a resource in one place and then use that to create multiple instances of that resource per the map of variables.
Guidance would be much appreciated.
Thanks.
Bill
For this situation you'll need to decide whether your module represents one resource group or whether it represents multiple resource groups. For a module that only contains one resource anyway that decision is not particularly important, but I assume you're factoring this out into a separate module because there is something more to this than just the single resource group resource, and so you can decide between these two based on what else this module represents: do you want to repeat everything in this module, or just the resource group resource?
If you need the module to represent a single resource group then you should change its input variables to take the data about only a single resource group, and then pass the current resource group's data in your calling module block.
Inside the module:
variable "resource_group" {
type = object({
name = string
location = string
})
}
resource "azurerm_resource_group" "resource_group" {
name = var.resource_group.name
location = var.resource_group.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
for_each = var.resource_groups
# each.value is the value of the current
# element of var.resource_groups, and
# so it's just a single resource group.
resource_group = each.value
}
With this strategy, you will declare resource instances with the following addresses, showing that the repetition is happening at the level of the whole module rather than the individual resources inside it:
module.resource_group["cow"].azurerm_resource_group.resource_group
module.resource_group["horse"].azurerm_resource_group.resource_group
module.resource_group["chicken"].azurerm_resource_group.resource_group
If you need the module to represent the full set of resource groups then the module would take the full map of resource groups as an input variable instead of using for_each on the module block. The for_each argument will then belong to the nested resource instead.
Inside the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
resource "azurerm_resource_group" "resource_group" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
# NOTE: No for_each here, because we need only
# one instance of this module which will itself
# then contain multiple instances of the resource.
resource_group = var.resource_groups
}
With this strategy, you will declare resource instances with the following addresses, showing that there's only one instance of the module but multiple instances of the resource inside it:
module.resource_group.azurerm_resource_group.resource_group["cow"]
module.resource_group.azurerm_resource_group.resource_group["horse"]
module.resource_group.azurerm_resource_group.resource_group["chicken"]
It's not clear from the information you shared which of these strategies would be more appropriate in your case, because you've described this module as if it is just a thin wrapper around the azurerm_resource_group resource type and therefore it isn't really clear what this module represents, and why it's helpful in comparison to just writing an inline resource "azurerm_resource_group" block in the root module.
When thinking about which of the above designs is most appropriate for your use-case, I'd suggest considering the advice in When to Write a Module in the Terraform documentation. It can be okay to write a module that contains only a single resource block, but that's typically for more complicated resource types where the module hard-codes some local conventions so that they don't need to be re-specified throughout an organization's Terraform configurations.
If you are just passing the values through directly to the resource arguments with no additional transformation and no additional hard-coded settings then that would suggest that this module is not useful, and that it would be simpler to write the resource block inline instead.

Unable to Create Terraform Resource Group when using modules

I am optimizing my terraform code by using modules. When i create a resource group module it works perfectly well but it creates two resource groups
i.e.
Temp-AppConfiguration-ResGrp
Temp-AppServices-ResGrp
instead it should only create
Temp-AppConfiguration-ResGrp
Code Resourcegroup.tf.
resource "azurerm_resource_group" "resource" {
name = "${var.environment}-${var.name_apptype}-ResGrp"
location = var.location
tags = {
environment = var.environment
}
}
output "resource_group_name" {
value = "${var.environment}-${var.name_apptype}-ResGrp"
}
output "resource_group_location" {
value = var.location
}
Variable.tf
variable "name_apptype" {
type = string
default = "AppServices"
}
variable "environment" {
type = string
default = "Temp"
}
variable "location" {
type = string
default = "eastus"
}
Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
I want to pass name_apptype in main.tf when calling resource group module. So that i don't need to update variable.tf every time.
Any suggestions
where i am doing wrong. Plus i am also unable to output the value, i need it so that i could pass resource group name in the next module i want to create.
Thanks
You need to do that in the Main.tf
module "resourcegroup" {
source = "../Modules"
name_apptype = "AppConfiguration"
}
module "resourcegroup-appservices" {
source = "../Modules"
name_apptype = "AppServices"
}
These create a 2 resources groups with the values that you need, additionally you can remove the default value from the name_apptype variable.
If you want to create with the same module both resource groups you need to use count to iterate over an array of names

how to fix terraform unpredict instance creation issue?

I'm getting the below error while running terraform plan and apply
on main.tf line 517, in resource "aws_lb_target_group_attachment" "ecom-tga":
│ 517: for_each = local.service_instance_map
│ ├────────────────
│ │ local.service_instance_map will be known only after apply
│
│ The "for_each" 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 for_each depends on.
My configuration file is as below
variable "instance_count" {
type = string
default = 3
}
variable "service-names" {
type = list
default = ["valid","jsc","test"]
}
locals {
helper_map = {for idx, val in setproduct(var.service-names, range(var.instance_count)):
idx => {service_name = val[0]}
}
}
resource "aws_instance" "ecom-validation-service" {
for_each = local.helper_map
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = "${each.value.service_name}-service"
}
vpc_security_group_ids = [data.aws_security_group.ecom-sg[each.value.service_name].id]
subnet_id = data.aws_subnet.ecom-subnet[each.value.service_name].id
}
data "aws_instances" "ecom-instances" {
for_each = toset(var.service-names)
instance_tags = {
Name = "${each.value}-service"
}
instance_state_names = ["running", "stopped"]
depends_on = [
aws_instance.ecom-validation-service
]
}
locals {
service_instance_map = merge([for env, value in data.aws_instances.ecom-instances:
{
for id in value.ids:
"${env}-${id}" => {
"service-name" = env
"id" = id
}
}
]...)
}
resource "aws_lb_target_group_attachment" "ecom-tga" {
for_each = local.service_instance_map
target_group_arn = aws_lb_target_group.ecom-nlb-tgp[each.value.service-name].arn
port = 80
target_id = each.value.id
depends_on = [aws_lb_target_group.ecom-nlb-tgp]
}
Since i'm passing count as var and its value is 3,i thought terraform will predict as it needs to create 9 instances.But it didn't it seems and throwing error as unable to predict.
Do we have anyway to by pass this by giving some default values for instances count prediction or for that local service_instance_map?
Tried try function but still no luck
Error: Invalid for_each argument
│
│ on main.tf line 527, in resource "aws_lb_target_group_attachment" "ecom-tga":
│ 527: for_each = try(local.service_instance_map,[])
│ ├────────────────
│ │ local.service_instance_map will be known only after apply
│
│ The "for_each" 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 for_each depends on.
My requirement got changed and now i have to create 3 instances in 3 subnets available in that region.I changed the locals as like below But same prediction issue
locals {
merged_subnet_svc = try(flatten([
for service in var.service-names : [
for subnet in aws_subnet.ecom-private.*.id : {
service = service
subnet = subnet
}
]
]), {})
variable "azs" {
type = list(any)
default = ["ap-south-1a", "ap-south-1b", "ap-south-1c"]
}
variable "private-subnets" {
type = list(any)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_instance" "ecom-instances" {
for_each = {
for svc in local.merged_subnet_svc : "${svc.service}-${svc.subnet}" => svc
}
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = "ecom-${each.value.service}-service"
}
vpc_security_group_ids = [aws_security_group.ecom-sg[each.value.service].id]
subnet_id = each.value.subnet
}
}
In your configuration you've declared that data "aws_instances" "ecom-instances" depends on aws_instance.ecom-validation-service. Since that other object won't exist yet on your first run, Terraform must therefore wait until the apply step to read data.aws_instances.ecom-instances because otherwise it would fail to honor the dependency you've declared, because aws_instance.ecom-validation-service wouldn't exist yet.
To avoid the error message you saw here, you need to make sure that for_each only refers to values that Terraform will know before any objects are actually created. Because EC2 assigns instance ids only once the instance is created, it's not correct to use an EC2 instance id as part of a for_each instance key.
Furthermore, there's no need for a data "aws_instances" block to retrieve instance information here because you already have the relevant instance information as a result of the resource "aws_instance" "ecom-validation-service" block.
With all of that said, let's start from your input variables and build things up again while making sure that we only build instance keys only from values we'll know during planning. The variables you have stay essentially the same; I've just tweaked the type constraints a little to match how we're using each one:
variable "instance_count" {
type = string
default = 3
}
variable "service_names" {
type = set(string)
default = ["valid", "jsc", "test"]
}
I understand from the rest of your example that you are intending to create var.instance_count instances for each distinct element of var.service_names. Your setproduct to produce all of the combinations of those is also good, but I'm going to tweak it to assign the instances unique keys that include the service name:
locals {
instance_configs = tomap({
for pair in setproduct(var.service_names, range(var.instance_count)) :
"${pair[0]}${pair[1]}" => {
service_name = pair[0]
}
})
}
This will produce a data structure like the following:
{
valid0 = { service_name = "valid" }
valid1 = { service_name = "valid" }
valid2 = { service_name = "valid" }
jsc0 = { service_name = "jsc" }
jsc1 = { service_name = "jsc" }
jsc2 = { service_name = "jsc" }
test0 = { service_name = "test" }
test1 = { service_name = "test" }
test2 = { service_name = "test" }
}
This matches the shape that for_each expects, so we can use it directly to declare nine aws_instance instances:
resource "aws_instance" "ecom-validation-service" {
for_each = local.instance_configs
instance_type = "t3.micro"
ami = data.aws_ami.ecom.id
subnet_id = data.aws_subnet.ecom-subnet[each.value.service_name].id
vpc_security_group_ids = [
data.aws_security_group.ecom-sg[each.value.service_name].id,
]
tags = {
Name = "${each.value.service_name}-service"
Service = each.value_service_name
}
}
So far this has been mostly the same as what you shared. But this is the point where I'm going to go in a totally different direction: rather than now trying to read back the instances this declared using a separate data resource, I'll just gather the same data directly from the aws_instance.ecom-validation-service resource. It's generally best for a Terraform configuration to either manage a particular object or read it, not both at the same time, because this way the necessary dependency ordering is revealed automatically be the references.
Notice that I included an extra tag Service on each of the instances to give a more convenient way to get the service name back. If you can't do that then you could get the same information by trimming the -service suffix from the Name tag, but I prefer to keep things direct where possible.
It seemed like your goal then was to have a aws_lb_target_group_attachment instance per instance, with each one connected to the appropriate target group based on the service name. Because that aws_instance resource has for_each set, aws_instance.ecom-validation-service in expressions elsewhere is a map of objects where the keys are the same as the keys in var.instance_configs. That means that value is also compatible with the requirements for for_each and so we can use it directly to declare the target group attachments:
resource "aws_lb_target_group_attachment" "ecom-tga" {
for_each = aws_instance.ecom-validation-service
target_group_arn = aws_lb_target_group.ecom-nlb-tgp[each.value.tags.Service].arn
port = 80
target_id = each.value.id
}
I relied on the extra Service tag from earlier to easily determine which service each instance belongs to in order to look up the appropriate target group ARN. each.value.id works here because each.value is always an aws_instance object, which exports that id attribute.
The result of this is two sets of instances that each have keys matching those in local.instance_configs:
aws_instance.ecom-validation-service["valid0"]
aws_instance.ecom-validation-service["valid1"]
aws_instance.ecom-validation-service["valid2"]
aws_instance.ecom-validation-service["jsc0"]
aws_instance.ecom-validation-service["jsc1"]
aws_instance.ecom-validation-service["jsc2"]
...
aws_lb_target_group_attachment.ecom-tga["valid0"]
aws_lb_target_group_attachment.ecom-tga["valid1"]
aws_lb_target_group_attachment.ecom-tga["valid2"]
aws_lb_target_group_attachment.ecom-tga["jsc0"]
aws_lb_target_group_attachment.ecom-tga["jsc1"]
aws_lb_target_group_attachment.ecom-tga["jsc2"]
...
Notice that all of these keys contain only information specified directly in the configuration, and not any information decided by the remote system. That means we avoid the "Invalid for_each argument" error even though each instance still has an appropriate unique key. If you were to add a new element to var.service_names or increase var.instance_count later then Terraform will also see from the shape of these instance keys that it should just add new instances of each resource, rather than renaming/renumbering any existing instances.

Terraform: How can I have one common output for all declared modules?

I have got something like this in my terraform file:
main.tf
module "airflow_tenant_one" {
source = "../modules/airflow_tenant"
name = "one-airflow"
project = var.project
cluster_name = var.cluster_name
region = var.region
kubernetes_endpoint = var.kubernetes_endpoint
tenant_domain = "one-airflow.${var.domain_name}"
}
module "airflow_tenant_two" {
source = "../modules/airflow_tenant"
name = "two-airflow"
project = var.project
cluster_name = var.cluster_name
region = var.region
kubernetes_endpoint = var.kubernetes_endpoint
tenant_domain = "two-airflow.${var.domain_name}"
}
How can I do one common output for all declared modules?
Currently, it looks like:
outputs.tf
output "chart_name_one" {
description = "The name of the chart"
value = module.airflow_tenant_one.chart_name
}
output "chart_name_two" {
description = "The name of the chart"
value = module.airflow_tenant_two.chart_name
}
I asked because in a future it is possible that I will add more modules in my main.tf file. Will be better to have one output declaration for all of them.
The module blocks you shared seem like they are all systematically configured based on some client names, so you can potentially do this using a single module block with for_each if you are using Terraform 0.13.0 or later.
locals {
tenants = toset([
"one",
"two",
])
}
module "airflow_tenant" {
for_each = local.tenants
name = "${each.key}-airflow"
project = var.project
cluster_name = var.cluster_name
region = var.region
kubernetes_endpoint = var.kubernetes_endpoint
tenant_domain = "${each.key}-airflow.${var.domain_name}"
}
output "tenant_chart_names" {
value = {
for name, tenant in module.airflow_tenant : name => tenant.chart_name
}
}
The above will cause there to be one instance of the airflow_tenant module per element of local.tenants, with addresses like this:
module.airflow_tenant["one"]
module.airflow_tenant["two"]
The for_each makes the module behave as a map of instances when you refer to it elsewhere, which is why we're able to project that map in output "tenant_chart_names" to derive a map from tenant name to chart names.
You can add and remove elements of local.tenants over time, in which case Terraform will understand that as an intent to either create a instances or destroy an instances of all of the objects described inside that module.
You can read more about this feature in Multiple Instances of a Module.

Variance in attributes based on count.index in terraform

I'm using Hashicorp terraform to create a MySQL cluster on AWS. I created a module named mysql and want to tag the first instance created as the master. However, per terraform documentation:
Modules don't currently support the count parameter.
How do I work around this problem? Currently, I have these in my files:
$ cat project/main.tf
module "mysql_cluster" {
source = "./modules/mysql"
cluster_role = "${count.index == "0" ? "master" : "slave"}"
}
$ cat project/modules/mysql/main.tf
..
resource "aws_instance" "mysql" {
ami = "ami-123456"
instance_type = "t2.xlarge"
key_name = "rsa_2048"
tags {
Role = "${var.cluster_role}"
}
count = 3
}
This throws an error:
$ project git:(master) ✗ terraform plan
Error: module "mysql_cluster": count variables are only valid within resources
I have the necessary variables declared in the variables.tf files in my mysql module and root module. How do I work around this problem? Thanks in advance for any help!
The way you have count in the module resource would infer that you want 3 modules created, rather than 3 resources within the module created. You can stipulate the count from the module resource but any logic using count.index needs to sit within the module.
main.tf
module "mysql_cluster" {
source = "./modules/mysql"
instance_count = 3
}
mysql.tf
resource "aws_instance" "mysql" {
count = "${var.instance_count}"
ami = "ami-123456"
instance_type = "t2.xlarge"
key_name = "rsa_2048"
tags {
Role = "${count.index == "0" ? "master" : "slave"}"
}
}
Since Terraform 0.13 you can use either for_each or count to create multiple instances of a module.
variable "regions" {
type = map(object({
region = string
network = string
subnetwork = string
ip_range_pods = string
ip_range_services = string
}))
}
module "kubernetes_cluster" {
source = "terraform-google-modules/kubernetes-engine/google"
for_each = var.regions
project_id = var.project_id
name = each.key
region = each.value.region
network = each.value.network
subnetwork = each.value.subnetwork
ip_range_pods = each.value.ip_range_pods
ip_range_services = each.value.ip_range_services
}
Code snipped from
https://github.com/hashicorp/terraform/tree/guide-v0.13-beta/module-repetition
Official documentation
https://www.terraform.io/docs/configuration/modules.html
module doesn't have count. It is available at resources only.

Resources