Eva
Eva

Reputation: 593

Multi-level maps with a list over template in Terraform

I have a requirement to generate around ten YAML files and use kubernetes_manifest resource to apply them. Most of the content in the YAML is constant, only a few parameters change. Some YAML files have a repetitive ports section, in the below example I have http and https under the ports section. In some cases, I have three http, https and sql. Based on the inputs I should be able to generate a yaml file out of the template file. The below code works fine when I have only one section under ports like only http. I have confusion as to how to loop the ports section. should i create a map of map? I am using Terraform v1.2.2. I request you to help me correct my issue or suggest an alternate idea/solution achieve my goal

Expected File after generation

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: test
  namespace: test
spec:
  hosts:
  - 'api.facebook.com'
  ports:
  - name: http
    number: 8080
    protocol: TCP
  - name: https
    number: 443
    protocol: TCP
  resolution: NONE

Template file

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: ${service_entry}
  namespace: ${namespace}
spec:
  hosts:
  - ${jsonencode(hosts)}
  ports:
  %{ for name, number, protocol in service_entry ~}
  - name: ${name}
    number: ${number}
    protocol: ${protocol}
  %{ endfor ~}
  resolution: ${resolution}

main.tf

resource "kubernetes_manifest" "service-entry" {
  for_each = var.service_entry
  manifest = yamldecode(templatefile("${path.module}/templates/service_entry.yaml.tpl", {
    service_entry_name = each.value.service_entry
    namespace          = each.value.namespace
    hosts              = each.value.hosts
    name               = each.value.name
    number             = each.value.number
    protocol           = each.value.protocol
    resolution         = each.value.resolution
  }))
}

varibles.tf

variable "service_entry" {
  type = map(object({
    service_entry = string
    namespace     = string
    hosts         = string
    name          = list(string)
    number        = list(string)
    protocol      = list(string)
    resolution    = string
  }))
  default = {}
}

tfvars Below tfvars should generate two yaml files. the first YAML would have two sections under ports http https and the second YAML would have three sections under ports http https and sql

service_entry = {
  app1 = {
    service_entry = "test"
    namespace     = "test"
    hosts         = "api.facebook.com"
    resolution    = "NONE"
    name          = ["http", "https"]
    number        = ["8080", "443"] 
    protocol      = ["TCP", "TCP"]
 },
  app2 = {
    service_entry = "example"
    namespace     = "example"
    hosts         = "api.facebook.com"
    resolution    = "NONE"
    name          = ["http", "https", "sql"]
    number        = ["8080", "443", "5432"] 
    protocol      = ["TCP","TCP","TCP"]
    }
}

Current error

 Error: Error in function call
│
│   on ../../modules/service_entry/main.tf line 3, in resource "kubernetes_manifest" "service-entry":
│    3:   manifest = yamldecode(templatefile("${path.module}/templates/service_entry.yaml.tpl", {
│    4:     service_entry_name = each.value.service_entry_name
│    5:     namespace          = each.value.namespace
│    6:     hosts              = each.value.hosts
│    7:     name               = each.value.name
│    8:     number             = each.value.number
│    9:     protocol           = each.value.protocol
│   10:     resolution         = each.value.resolution
│   11:   }))
│     ├────────────────
│     │ each.value.hosts will be known only after apply
│     │ each.value.namespace will be known only after apply
│     │ each.value.name will be known only after apply
│     │ each.value.number will be known only after apply
│     │ each.value.protocol will be known only after apply
│     │ each.value.resolution will be known only after apply
│     │ each.value.service_entry_name will be known only after apply
│     │ path.module is "../../modules/service_entry"
│
│ Call to function "templatefile" failed:
│ ../../modules/service_entry/templates/service_entry.yaml.tpl:11,32-33:
│ Invalid 'for' directive; For directive requires 'in' keyword after names.,
│ and 1 other diagnostic(s).
╵
ERRO[0005] 1 error occurred:
    * exit status 1

Upvotes: 1

Views: 1049

Answers (2)

adR
adR

Reputation: 528

Building on top of the accepted answer by @Marcin, here is example usage. NOTE: You need to create seperate service entries for services with hosts that don't use/expose the same ports. E.g. hosts that don't support HTTPS/TLS cannot be in the same service entry as hosts that do.

// ./modules/istio/manifests/serviceEntry.yaml.tpl
${yamlencode(
{
    apiVersion = "networking.istio.io/v1beta1"
    kind = "ServiceEntry"
    metadata = {
      name = service_entry_name
      namespace = namespace    
    }  
    spec = {
      "hosts" = [hosts]
      ports = [ for idx in range(length(name)):
            {
               name: name[idx]
               number: number[idx]
               protocol: protocol[idx]            
            }
      ]
    }    
}
)}


// /modules/istio/variables.tf
variable "service_entry" {
  type = map(object({
    service_entry = string
    namespace     = string
    hosts         = list(string)
    name          = list(string)
    number        = list(number)
    protocol      = list(string)
    resolution    = string
  }))
  default = {}
}


// ./modules/istio/main.tf
resource "k8s_manifest" "service-entry" {
  for_each = var.service_entry
  content = templatefile("${path.module}/manifests/serviceEntry.yaml.tpl", {
    service_entry_name = each.value.service_entry
    namespace          = each.value.namespace
    hosts              = each.value.hosts
    name               = each.value.name
    number             = each.value.number
    protocol           = each.value.protocol
    resolution         = each.value.resolution
  })
}


// ./main.tf
module "istio_service_entry_dp" {
  source = "./modules/istio"

  depends_on = [
    module.event_hub_ingress,
    module.checkpoint_storage_account,
    module.postgres_server
  ]

  providers = {
    k8s = k8s.data_plane
  }

  service_entry = {
    sa = {
      service_entry = "storage-account-spark"
      namespace     = var.datapipeline_namespace
      hosts = [data.terraform_remote_state.datahub_infrastructure.outputs.csv_fapp.fqdn,
        data.terraform_remote_state.datahub_infrastructure.outputs.dp_adl_storage_account.fqdn,
        data.terraform_remote_state.datahub_infrastructure.outputs.csv_upload_storage.fqdn,
      module.checkpoint_storage_account.dp_checkpoint_storage.fqdn]
      resolution = "DNS"
      name       = ["https"]
      number     = [443]
      protocol   = ["HTTPS"]
    },
    eventhub = {
      service_entry = "event-hub-spark"
      namespace     = var.datapipeline_namespace
      hosts         = ["${var.event_hub_ingress_namespace}.servicebus.windows.net"]
      resolution    = "DNS"
      name          = ["https", "AMQP", "AMQP-5672", "kafka"]
      number        = [443, 5671, 5672, 9093]
      protocol      = ["HTTPS", "TCP", "TCP", "TLS"]
    },
    db = {
      service_entry = "postgres-db"
      namespace     = var.datapipeline_namespace
      hosts         = [module.postgres_server.fqdn]
      resolution    = "DNS"
      name          = ["tcp"]
      number        = [5432]
      protocol      = ["TCP"]
    }
  }
}

Upvotes: 1

Marcin
Marcin

Reputation: 238867

The easiest way to work with templates, in your case yaml, is to wrap everything in yamlencode. So your service_entry.yaml.tpl can be:

${yamlencode(
{
    apiVersion = "networking.istio.io/v1beta1"
    kind = "ServiceEntry"
    metadata = {
      name = service_entry_name
      namespace = namespace    
    }  
    spec = {
      "hosts" = [hosts]
      ports = [ for idx in range(length(name)):
            {
               name: name[idx]
               number: number[idx]
               protocol: protocol[idx]            
            }
      ]
    }    
}
)}

This generates valid yaml file and you don't have to fight against the strange templating syntax.

Upvotes: 2

Related Questions