Terraform 系列 - 使用 for-each 对本地 json 进行迭代

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
简介: Terraform 系列 - 使用 for-each 对本地 json 进行迭代

概述

前文 Grafana 系列 - Grafana Terraform Provider 基础 介绍了使用 Grafana Terraform Provider 创建 Datasource.

现在有这么一个现实需求:

有大量的同类型 (type) 的 datasource 需要批量添加,而且这些 datasource 的基本信息是以 json 的格式已经存在。

需要对 json 进行解析 / 精简 / 重构等操作并将 json 作为 Terraform 的 datasource.

Json 的格式可能类似于这样:

[
    {
        "env_name": "dev",
        "prom_url": "http://dev-prom.example.com",
        "es_url": "http://dev-es.example.com:9200",
        "jaeger_url": "http://dev-jaeger.example.com"
    },
    {
        "env_name": "test",
        "prom_url": "http://test-prom.example.com",
        "es_url": "http://test-es.example.com:9200",
        "jaeger_url": "http://test-jaeger.example.com"
    }
]
JSON

📝Notes:

举一反三,后面的解决方案也适用于其他任意 Json 格式。

该如何实现?🤔

解决方案

通过 Terraform 的 locals jsondecode for 循环 和 for_each 实现。

具体如下:

  • 构造一个 local 变量
  • local 变量从 .json 文件中读取并内容并通过 jsondecode + file 将 json 文件解码为 object
  • 使用 for 循环,将 object 根据当前需求调整,将例子中 env_name 作为 key, 将其他作为 value
  • 批量创建资源时,通过 for_each, 进行批量创建。

基本概念

locals

locals表达式 指定一个名称,所以你可以在一个模块中多次使用这个名称,而不用重复表达式。

如果你熟悉传统的编程语言,把 Terraform 模块比作函数定义可能会很有用:

一旦声明了一个本地值,你可以在 表达式 中以local.<NAME> 的形式引用它。

本地值有助于避免在配置中多次重复相同的值或表达式,只有在一个单一的值或结果被用于许多地方的情况下,才可以适度地使用本地值。能够在一个中心位置轻松地改变数值是本地值的关键优势

file 函数

file读取指定路径下的文件内容,并将其作为 string 返回。

1
2
> file("${path.module}/hello.txt")
Hello World
BASH

jsondecode 函数

jsondecode将一个给定的 string 解释为 JSON,返回该字符串的解码结果。

该函数以如下方式将 JSON 值映射到 Terraform 语言 type

JSON type Terraform type
String string
Number number
Boolean bool
Object object(...)的属性类型根据此表确定
Array tuple(...)的元素类型根据此表确定
Null Terraform 语言的 null

Terraform 语言的自动类型转换规则意味着你通常不需要担心一个给定的值到底会产生什么类型,只需以直观的方式使用结果即可。

> jsondecode("{\"hello\": \"world\"}")
{
  "hello" = "world"
}
> jsondecode("true")
true
BASH

jsonencode 执行相反的操作,将一个 string 编码为 JSON。

for 表达式

一个 for 表达式通过转换另一个复杂类型的值来创建一个复杂类型的值。输入值中的每个元素可以对应于结果中的一个或零个值,并且可以使用一个任意的表达式来将每个输入元素转化为输出元素。

例如,如果 var.list 是一个字符串的列表,那么下面的表达式将产生一个全大写字母的字符串的元组:

[for s in var.list : upper(s)]
HCL

这个 for 表达式遍历了 var.list 中的每个元素,然后评估表达式 upper(s),将s 设置为每个相应的元素。然后它用所有执行该表达式的结果按相同的顺序建立一个新的元组值。

一个 for 表达式的输入(在 in 关键字之后给出)可以是一个列表,一个集合,一个元组,一个 map,或者一个对象 (object)。

上面的例子显示了一个只有一个临时符号 sfor表达式,但是一个 for 表达式可以选择声明一对临时符号,以便也使用每个项目的键或索引:

[for k, v in var.map : length(k) + length(v)]
HCL

对于 map 或对象类型,像上面那样,k符号是指当前元素的键或属性名称。你也可以对列表和 map 使用双符号形式,在这种情况下,额外的符号是每个元素的索引,从 0 开始,常规的符号名称是 iidx,除非选择一个很有帮助的更具体的名称:

[for i, v in var.list : "${i} is ${v}"]
HCL

索引或关键符号总是可选的。如果你在 for 关键字后面只指定一个符号,那么这个符号将总是代表输入集合的每个元素的值。

for表达式周围的 括号 的类型决定了它产生的结果的类型。

上面的例子使用 [],产生一个 元组 。如果你用{}代替,结果是一个 对象 ,你必须提供两个结果表达式,用=> 符号分开:

{for s in var.list : s => upper(s)}
HCL

这个表达式产生一个对象,其属性是来自 var.list 的原始元素,其相应的值是大写版本。例如,产生的值可能如下:

{
  foo = "FOO"
  bar = "BAR"
  baz = "BAZ"
}
HCL

单独的 for 表达式只能产生一个对象值或一个元组值,但 Terraform 的自动类型转换规则意味着你通常可以在期望使用列表、map 和集合 (set) 的地方使用其结果。

一个 for 表达式也可以包括一个可选的 if 子句来过滤源集合中的元素,产生一个比源值更少元素的值:

[for s in var.list : upper(s) if s != ""]
HCL

for 表达式中过滤集合的一个常见原因是根据一些标准将一个源集合分成两个独立的集合。例如,如果输入的 var.users 是一个对象的映射,其中每个对象都有一个属性is_admin,那么你可能希望产生包含管理员和非管理员对象的单独映射:

variable "users" {
  type = map(object({
    is_admin = bool
  }))
}
locals {
  admin_users = {
    for name, user in var.users : name => user
    if user.is_admin
  }
  regular_users = {
    for name, user in var.users : name => user
    if !user.is_admin
  }
}
HCL

因为 for 表达式可以从无序类型(map、对象、集合 set)转换为有序类型(列表、元祖),Terraform 必须为无序集合的元素选择一个隐含的排序。

对于 map 和对象,Terraform 通过键或属性名称对元素进行排序,使用词法排序。

对于字符串的集合,Terraform 按其值排序,使用词法排序。

for表达式机制是为了在表达式中从其他集合值中构建集合值,然后你可以将其分配给期待复杂值的单个资源参数。

for_each 元参数

默认情况下,一个 资源块 配置 一个真实的基础设施对象 (同样,一个 模块块 将一个子模块的内容纳入一次配置)。然而,有时你想管理几个类似的对象(比如一个固定的计算实例池),而不需要为每个对象单独写一个块。Terraform 有两种方法可以做到这一点: countfor_each

如果一个资源或模块块包括一个 for_each 参数,其值是一个 map 或字符串集合,Terraform 为该 map 或字符串集合的每个成员创建一个实例。

版本说明: for_each是在 Terraform 0.12.6 中添加的。Terraform 0.13 中增加了对for_each 的模块支持;以前的版本只能在资源中使用它。

** 注意:** 一个特定的资源或模块块不能同时使用 countfor_each

for_each是 Terraform 语言定义的一个元参数。它可以与模块和每一种资源类型一起使用。

for_each 元参数接受一个 map 或字符串集合,并为该 map 或字符串集合的每个项目创建一个实例。每个实例都有一个独特的基础设施对象与之相关联,每个实例都在应用配置时被单独创建、更新或销毁。

Map:

resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  name     = each.key
  location = each.value
}
HCL

字符串集合:

resource "aws_iam_user" "the-accounts" {
  for_each = toset(["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
}
HCL

在设置了 for_each 的区块中,表达式中还有一个each 对象,所以你可以修改每个实例的配置。这个对象有两个属性:

  • each.key - 这个实例对应的 map 键(或集合成员)。
  • each.value - 该实例对应的 map 值。(如果提供了一个集合,这与 each.key 相同。)

for_each 被设置时,Terraform 区分了区块本身和与之相关的多个 资源或模块实例 。实例由提供给for_each 的值中的一个 map 键(或集合成员)来识别。

  • <TYPE>.<NAME>module.<NAME> (例如,azurerm_resource_group.rg) 代表这个块。
  • <TYPE>.<NAME>[<KEY>]module.<NAME>[<KEY>] (例如,azurerm_resource_group.rg["a_group"], azurerm_resource_group.rg["another_group"], etc.) 代表独立的实例

这与没有 countfor_each的资源和模块不同,它们可以在没有索引或键的情况下被引用。

String & Template

字符串是 Terraform 中最复杂的一种文字表达,也是最常用的一种。

Terraform 同时支持字符串的引号语法和 heredoc 语法。这两种语法都支持用于插值和操作文本的模板序列。

带引号的字符串是一系列由双引号字符(")划定的字符。

有两个不使用反斜线的特殊转义序列:

Sequence Replacement
$${ 字面意思是${,不会开始一个插值序列。
%%{ 字面意思是%{,不会开始一个模板指令序列。

${ ... }序列是一个 插值,它评估标记之间给出的表达式,如果有必要,将结果转换为字符串,然后将其插入到最终的字符串中:

"Hello, ${var.name}!"
HCL

在上面的例子中,命名的对象 var.name 被访问,其值被插入到字符串中,产生的结果类似 “Hello, Juan!”。

%{ ... } 序列是一个 指令 ,它允许有条件的结果和对集合的迭代,类似于条件和for 表达式。

以下指令被支持:

  • %{if <BOOL>}/%{else}/%{endif}指令根据一个 bool 表达式的值在两个模板之间进行选择:
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"
HCL
  • else部分可以省略,在这种情况下,如果条件表达式返回false,结果就是一个空字符串。
  • %{for <NAME> in <COLLECTION>}/%{endfor}指令在给定的集合或结构值的元素上进行迭代,对每个元素评估一次给定的模板,将结果串联起来:
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT
HCL

实战

需求:

有大量的同类型 (type) 的 datasource 需要批量添加,而且这些 datasource 的基本信息是以 json 的格式已经存在。

需要对 json 进行解析 / 精简 / 重构等操作并将 json 作为 Terraform 的 datasource.

Json 的格式可能类似于这样:

[
    {
        "env_name": "dev",
        "prom_url": "http://dev-prom.example.com",
        "es_url": "http://dev-es.example.com:9200",
        "jaeger_url": "http://dev-jaeger.example.com"
    },
    {
        "env_name": "test",
        "prom_url": "http://test-prom.example.com",
        "es_url": "http://test-es.example.com:9200",
        "jaeger_url": "http://test-jaeger.example.com"
    }
]
JSON

解决方案

  • 构造一个 local 变量
  • local 变量从 .json 文件中读取并内容并通过 jsondecode + file 将 json 文件解码为 object
  • 使用 for 循环,将 object 根据当前需求调整,将例子中 env 作为 key, 将其他作为 value
  • 批量创建资源时,通过 for_each, 进行批量创建。

串起来, 最终如下:

locals {
  # 将 json 文件转换为 对象  
  user_data = jsondecode(file("${path.module}/env-details.json"))
  # 构造一个 map
  # key 是 env_name
  # value 又是一个 map, 其 key 是 grafana datasource type, value 是 url
  envs = { for env in local.user_data : env.env_name =>
    {
      prometheus = env.prom_url
      # 利用 ${} 构造新的 url
      jaeger     = "${env.jaeger_url}/trace/"
      es         = env.es_url
    }
  }
}
resource "grafana_data_source" "prometheus" {
  # 通过 for_each 迭代
  for_each = local.envs
  type = "prometheus"
  name = "${each.key}_prom"
  uid  = "${each.key}_prom"
  url  = each.value.prometheus
  json_data_encoded = jsonencode({
    httpMethod = "POST"
  })
}
resource "grafana_data_source" "jaeger" {
  for_each = local.envs
  type = "jaeger"
  name = "${each.key}_jaeger"
  uid  = "${each.key}_jaeger"
  url  = each.value.jaeger
}
resource "grafana_data_source" "elasticsearch" {
  for_each = local.envs
  type          = "elasticsearch"
  name          = "${each.key}_es"
  uid           = "${each.key}_es"
  url           = each.value.es
  database_name = "[example.*-]YYYY.MM.DD"
  json_data_encoded = jsonencode({
    esVersion = "6.0.0"
    interval = "Daily"
    includeFrozen              = false
    maxConcurrentShardRequests = 256
    timeField                  = "@timestamp"
    logLevelField   = "level"
    logMessageField = "message"
  })
}
TERRAFORM

完成🎉🎉🎉

📚️参考文档

相关文章
|
JSON JavaScript 数据格式
【js jQuery】map集合 循环迭代取值---以及 map、json对象、list、array循环迭代的方法和区别
后台给前台传来一个map    @ResponseBody @RequestMapping(value = "getSys") public Map getSys(){ Map map = orderService.
1644 0
|
1月前
|
JSON 前端开发 Java
Json格式数据解析
Json格式数据解析
|
2月前
|
存储 JSON Apache
揭秘 Variant 数据类型:灵活应对半结构化数据,JSON查询提速超 8 倍,存储空间节省 65%
在最新发布的阿里云数据库 SelectDB 的内核 Apache Doris 2.1 新版本中,我们引入了全新的数据类型 Variant,对半结构化数据分析能力进行了全面增强。无需提前在表结构中定义具体的列,彻底改变了 Doris 过去基于 String、JSONB 等行存类型的存储和查询方式。
揭秘 Variant 数据类型:灵活应对半结构化数据,JSON查询提速超 8 倍,存储空间节省 65%
|
3月前
|
XML 机器学习/深度学习 JSON
在火狐浏览器调ajax获取json数据时,控制台提示“XML 解析错误:格式不佳”。
在火狐浏览器调ajax获取json数据时,控制台提示“XML 解析错误:格式不佳”。
31 0
在火狐浏览器调ajax获取json数据时,控制台提示“XML 解析错误:格式不佳”。
|
2天前
|
XML JSON API
转Android上基于JSON的数据交互应用
转Android上基于JSON的数据交互应用
|
9天前
|
JSON JavaScript Java
从前端Vue到后端Spring Boot:接收JSON数据的正确姿势
从前端Vue到后端Spring Boot:接收JSON数据的正确姿势
21 0
|
11天前
|
JSON 数据格式 Python
Python标准库中包含了json模块,可以帮助你轻松处理JSON数据
【4月更文挑战第30天】Python的json模块简化了JSON数据与Python对象之间的转换。使用`json.dumps()`可将字典转为JSON字符串,如`{&quot;name&quot;: &quot;John&quot;, &quot;age&quot;: 30, &quot;city&quot;: &quot;New York&quot;}`,而`json.loads()`则能将JSON字符串转回字典。通过`json.load()`从文件读取JSON数据,`json.dump()`则用于将数据写入文件。
16 1
|
11天前
|
JSON 数据格式 Python
Python处理JSON数据
【4月更文挑战第30天】该内容介绍了Python处理JSON数据的三个方法:1)使用`json.loads()`尝试解析字符串以验证其是否为有效JSON,通过捕获`JSONDecodeError`异常判断有效性;2)通过`json.dumps()`的`indent`参数格式化输出JSON数据,使其更易读;3)处理JSON中的日期,利用`dateutil`库将日期转换为字符串进行序列化和反序列化。
23 4
|
14天前
|
存储 JSON 数据处理
|
16天前
|
JSON 数据可视化 定位技术
python_将包含汉字的字典数据写入json(将datav的全省数据中的贵州区域数据取出来)
python_将包含汉字的字典数据写入json(将datav的全省数据中的贵州区域数据取出来)
19 0