Create dynamic block in kubernetes container block using terraform - terraform

I want to create a dynamic block that will able to dynamically create envs for docker container inside kubernetes using terraform.
I already tried creating a var of list and iterate over the envs but I am getting syntax error
Error: Reference to "count" in non-counted context
on kubernetes/kubernetes.main.tf line 68, in resource "kubernetes_deployment" "kube_deployment":
This is due to usage of count out of resource block.
I am looking now to create multiple envs like this
...
env {
name = "NAME"
value = "VALUE"
}
env {
name = "NAME"
value = "VALUE"
}
.
.
.
is there anyway to create this iteration or any hacks to create dynamic envs in container block. I understand that dynamic blocks are only inside resource, data, provider, and provisioner.
I was previously using helm to do this kind of templating but now I want to fully move to terraform.
I would love any directions to solve such issue.
Thanks
resource "kubernetes_deployment" "kube_deployment" {
metadata {
name = var.deployment_name
labels = {
App = var.deployment_name
}
}
spec {
replicas = 1
selector {
match_labels = {
App = var.deployment_name
}
}
template {
metadata {
labels = {
App = var.deployment_name
}
}
spec {
container {
image = var.container_image
name = var.container_name
env {
name = "NAME"
value = "VALUE"
}
port {
container_port = var.container_port
}
}
}
}
}
}

It was actually possible even if inside nested block of type resource, data, provider, and provisione..
here is a working code
resource "kubernetes_deployment" "kube_deployment" {
metadata {
name = var.deployment_name
labels = {
App = var.deployment_name
}
}
spec {
replicas = 1
selector {
match_labels = {
App = var.deployment_name
}
}
template {
metadata {
labels = {
App = var.deployment_name
}
}
spec {
container {
image = var.container_image
name = var.container_name
dynamic "env" {
for_each = var.envs
content {
name = env.value.name
value = env.value.value
}
}
port {
container_port = var.container_port
}
}
}
}
}
}

Related

How to create an Eventarc trigger in terraform for Pub/Sub?

I need to create an eventarc trigger on a Pub/Sub message published. I do not know where to put the Pub/Sub topic ID.
resource "google_eventarc_trigger" "eventarc_trigger" {
name = "test-trigger"
service_account = var.service_account
project = local.project
location = local.region
destination {
workflow = google_workflows_workflow.example.id
}
matching_criteria {
attribute = "type"
value = "google.cloud.pubsub.topic.v1.messagePublished"
}
}
You can define the target transport like that
resource "google_eventarc_trigger" "eventarc_trigger" {
name = "test-trigger"
service_account = var.service_account
project = local.project
location = local.region
destination {
workflow = google_workflows_workflow.example.id
}
matching_criteria {
attribute = "type"
value = "google.cloud.pubsub.topic.v1.messagePublished"
}
transport {
pubsub {
topic = "projects/{PROJECT_ID}/topics/{TOPIC_NAME}"
}
}
}

How to reference instance argument value created with for_each meta-argument in another instance in the same map

Updated with a more illustrative example.
My end goal is to have Terraform create instances of a resource generated with the for_each meta argument in a specific sequence. HCL is known to be a declarative language and when Terraform applies a configuration it can create resources randomly unless you use the depends_on argument or refer from one resource (instance) to another. However, the depends_on argument does not take values that are "calculated", so I don't know how to use it in modules.
For this reason, in order to force Terraform to create instances of a resource in a specific sequence, I decided to try to make the value of a certain argument in an instance it creates "calculated" based on the values of the same argument from another instance.
Below you can find a more practical example based on using one of the providers, but the question is more general and pertains to Terraform as such.
Let's take a test module that instantiates the cloudflare_page_rule resource:
# Module is placed to module\main.tf
terraform {
experiments = [module_variable_optional_attrs]
}
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.0"
}
}
}
variable "zone" {
type = string
description = "The DNS zone name which will be added, e.g. example.com."
}
variable "page_rules" {
type = list(object({
page_rule_name = string
target = string
actions = object({
forwarding_url = optional(object({
url = string
status_code = number
}))
})
priority = optional(number)
status = optional(string)
depends_on = optional(string)
}))
description = "Zone's page rules."
default = []
}
//noinspection HILUnresolvedReference
locals {
page_rule_dependencies = { for p in var.page_rules : p.page_rule_name => p.depends_on if p.depends_on != null }
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/zone
resource "cloudflare_zone" "this" {
zone = var.zone
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/page_rule
//noinspection HILUnresolvedReference
resource "cloudflare_page_rule" "this" {
for_each = var.page_rules != null ? { for p in var.page_rules : p.page_rule_name => p } : {}
zone_id = cloudflare_zone.this.id
target = each.value.target
actions {
//noinspection HILUnresolvedReference
forwarding_url {
status_code = each.value.actions.forwarding_url.status_code
url = each.value.actions.forwarding_url.url
}
}
priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority
status = each.value.status
}
output "page_rule_dependencies" {
value = local.page_rule_dependencies
}
And a configuration that is used to create resources:
terraform {
required_version = ">= 0.15.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.1"
}
}
}
variable "cloudflare_api_token" {
type = string
sensitive = true
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
module "acme_com" {
source = "./module"
zone = "acme.com"
page_rules = [
{
page_rule_name = "page_rule_1"
target = "acme.com/url1"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url1"
}
}
priority = 1
},
{
page_rule_name = "page_rule_2"
target = "acme.com/url2"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url2"
}
}
priority = 2
depends_on = "page_rule_1"
},
{
page_rule_name = "page_rule_3"
target = "acme.com/url3"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url3"
}
}
priority = 3
depends_on = "page_rule_2"
}
]
}
output "page_rule_dependencies" {
value = module.acme_com.page_rule_dependencies
}
In this particular example, I've added the depends_on argument to the page_rules variable (don't confuse this argument with the depends_on meta argument). For the value of the depends_on argument, I specified the name of a page_fule on which another page_fule depends.
Next, I created a local variable page_rule_dependencies, the value of which, after calculations, is the following (you can check this yourself by replacing the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct with priority = each.value.priority and executing terraform apply):
page_rule_dependencies = {
"page_rule_2" = "page_rule_1"
"page_rule_3" = "page_rule_2"
}
Further, in the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct, I refer to the values ​​of the local variable, thereby forming a "reference" to the page_fule instance, on which the current instance depends:
When creating page_rule_1, the value of its argument priority = 1.
When creating page_rule_2, the value of its argument priority = cloudflare_page_rule.this["page_rule_1"].priority + 1.
When creating page_rule_3, the value of its argument priority = cloudflare_page_rule.this["page_rule_2"].priority + 1.
However, I get an Error: Cycle: module.acme_com.cloudflare_page_rule.this["page_rule_3"], module.acme_com.cloudflare_page_rule.this["page_rule_2"], module.acme_com.cloudflare_page_rule.this["page_rule_1"] error.
Either I'm doing something wrong, or it's some kind of Terraform limitation/bug. Is there a way to get rid of this error?
P.S. Resulting graph after terraform graph -draw-cycles | dot -Tsvg > graph.svg or terraform graph -draw-cycles -type=plan | dot -Tsvg > graph-plan.svg (the same result):
P.P.S. I use Terraform v1.1.7.

For_each loop with for expression based on value in map

Since the title is not descriptive enough let me introduce my problem.
I'm creating DRY module code for CDN that contains profile/endpoint/custom_domain.
Variable cdn_config would hold all necessary/optional parameters and these are created based on the for_each loop.
Variable looks like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
}
}
Core of this module is working - in the means that it would create cdn_profile "profile_1" then cdn_endpoint "endpoint_1" will be created and assigned to this profile then cdn_custom_domain will be created and assigned to "endpoint_1" since it's the part of "endpoint_1" map.
Then I realize, what in case I want to create "cdn_custom_domain" only and specify resource ID manually?
I was thinking that adding the optional parameter "standalone" could help, so it would look like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
"endpoint_standalone" = {
custom_domain = {
standalone = true
cdn_endpoint_id = "xxxxx"
}
}
}
}
Having this "standalone" parameter eq true "endpoint_standalone" map should be totally ignored from looping in the azurerm_cdn_endpoint resource creation.
So far this direction is my only guess, clearly, it's not working - if I add "endpoint_standalone" it complains that not all required parameters are specified so it's surely finding it.
resource "azurerm_cdn_endpoint" "this" {
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != "true"}
I would be grateful if you have a solution for this problem.
You are comparing a bool type to a string type, so the logical comparison will always return false:
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != true }
While we are here, we can also improve this for expression:
for_each = { for endpoint, params in var.cdn_config.endpoints : endpoint => params if lookup(params.custom_domain, "standalone", null) != true }

Module composition between two modules using a conditional for_each on both ways

On my root module, I am declaring two modules (paired_regions_network and paired_regions_app), that both iterate a set of regions.
module "paired_regions_network" {
source = "./modules/network"
application_hostname = module.paired_regions_app[each.key].website_hostname
...
for_each = ( var.environment == "TEST" || var.environment == "PROD") ? { region1 = var.paired_regions.region1 } : { }
}
module "paired_regions_app" {
source = "./modules/multi-region"
wag_public_ip = module.paired_regions_network[each.key].wag_public_ip
...
for_each = (var.environment == "TEST" || var.environment == "PROD") ? var.paired_regions : { region1 = var.paired_regions.region1 }
}
output "network_outputs" {
value = module.paired_regions_network
}
output "app_outputs" {
value = module.paired_regions_app
}
The iterated regions are declared as follows:
variable "paired_regions" {
description = "The paired regions"
default = {
region1 = {
...
},
region2 = {
...
}
}
}
From the paired_regions_network module I want to have access to the output coming from the paired_regions_app module, namely the website_hostname value, which I want to assign to the application_hostname parameter, of the paired_regions_network module, as shown above.
output "website_hostname" {
value = azurerm_app_service.was_app.default_site_hostname
description = "The hostname of the website"
}
And from the paired_regions_app module I want to have access to the output coming from the paired_regions_network module, namely the wag_public_ip value, which I want to assign to the parameter with the same name, of the paired_regions_app module, as shown above.
output "wag_ip_address" {
value = azurerm_public_ip.network_ip.ip_address
description = "The Public IP address that will be used by the app gateway"
}
But this causes a dependency cycle, that I can't get rid off. The error is the following:
Error: Cycle: ...
Can I pass the output between the two modules, without causing the dependency cycle?
As per #Marcin's advice, I was able to overcome the issue by creating a third module containing only the Public IP resource. So, the paired_regions_app module would depend on the new module instead of depending on the paired_regions_network module. The paired_regions_network would then depend on both the other two modules. Besides removing the output from the paired_regions_network module, the code changes are as follows:
Root Module
module "paired_regions_ips" {
source = "./modules/public-ip"
...
for_each = ( var.environment == "TEST" || var.environment == "PROD") ? { region1 = var.paired_regions.region1 } : { }
}
module "paired_regions_app" {
source = "./modules/multi-region"
wag_public_ip = length(module.paired_regions_ips) > 0 ? (lookup(module.paired_regions_ips, each.key, "") != "" ? join("/", ["${module.paired_regions_ips[each.key].ip_obj.ip_address}", "32"]) : "" ) : ""
...
for_each = (var.environment == "TEST" || var.environment == "PROD") ? var.paired_regions : { region1 = var.paired_regions.region1 }
}
module "paired_regions_network" {
source = "./modules/network"
wag_public_ip_id = module.paired_regions_ips[each.key].ip_obj.id
application_hostname = module.paired_regions_app[each.key].website_hostname
...
for_each = ( var.environment == "TEST" || var.environment == "PROD") ? { region1 = var.paired_regions.region1 } : { }
}
output "network_outputs" {
value = module.paired_regions_ips
}
output "app_outputs" {
value = module.paired_regions_app
}
The new module
output "ip_obj" {
value = azurerm_public_ip.network_ip
description = "The Public IP address"
}
Some remarks:
Because the paired_regions_ips module has different conditions on the for_each loop when compared to the paired_regions_app module, I had to add some logic when fetching the output from the latter
The new module outputs a public IP object, so that I have access to both its ID (from the paired_regions_network module) and to the IP address (from the paired_regions_app module)

Build dynamic terraform fields for kubernetes_role resource

Please help to understand how to correctly build dynamic rules for resource
In input I want to send vars like this :
role_rules = {
rule01 = {
"api_groups" = ["apps"]
"resources" = ["pods"]
"resource_names" = ["foo"]
"verbs" = ["get", "list", "watch"]
}
rule02 = {
"api_groups" = ["apps2"]
"resources" = ["services"]
"resource_names" = ["foo2"]
"verbs" = ["*"]
}
}
And in a result have two rules for my resource.
I tried to do this in a way like :
resource "kubernetes_role" "this" {
metadata {
name = var.role_name
labels = local.metadata_labels
}
dynamic "rule" {
for_each = local.role_permission_rules
content {
api_groups = try(role.value["api_groups"], "")
resources = try(role.value["resources"], "")
resource_names = try(role.value["resource_names"], "")
verbs = try(role.value["verbs"], "")
}
}
}
locals {
role_permission_rules = {
for rule in keys(var.role_rules):
rule => lookup(var.role_rules, rule)
}
}
But unfortunately, it's not working with a lot of errors that no value on the root module.
Any ideas on how to correct realize such stuff?
I would recommend using lookup instead of try. However, I think you just need to throw it into a list by containing the item in brackets []. Also I would recommend referencing rule.value and not role.value
For example:
dynamic "rule" {
for_each = local.role_permission_rules
content {
api_groups = [lookup(rule.value, "api_groups", null)]
resources = [lookup(rule.value, "resources", null)]
resource_names = [lookup(rule.value, "resource_names", null)]
verbs = [lookup(rule.value, "verbs", null)]
}
}

Resources