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

本文涉及的产品
可观测监控 Prometheus 版,每月50GB免费额度
可观测可视化 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

完成🎉🎉🎉

📚️参考文档

相关实践学习
通过可观测可视化Grafana版进行数据可视化展示与分析
使用可观测可视化Grafana版进行数据可视化展示与分析。
相关文章
|
JSON JavaScript 数据格式
【js jQuery】map集合 循环迭代取值---以及 map、json对象、list、array循环迭代的方法和区别
后台给前台传来一个map    @ResponseBody @RequestMapping(value = "getSys") public Map getSys(){ Map map = orderService.
1690 0
|
3月前
|
XML 存储 JSON
Twaver-HTML5基础学习(19)数据容器(2)_数据序列化_XML、Json
本文介绍了Twaver HTML5中的数据序列化,包括XML和JSON格式的序列化与反序列化方法。文章通过示例代码展示了如何将DataBox中的数据序列化为XML和JSON字符串,以及如何从这些字符串中反序列化数据,重建DataBox中的对象。此外,还提到了用户自定义属性的序列化注册方法。
48 1
|
2月前
|
数据采集 JSON 数据处理
抓取和分析JSON数据:使用Python构建数据处理管道
在大数据时代,电商网站如亚马逊、京东等成为数据采集的重要来源。本文介绍如何使用Python结合代理IP、多线程等技术,高效、隐秘地抓取并处理电商网站的JSON数据。通过爬虫代理服务,模拟真实用户行为,提升抓取效率和稳定性。示例代码展示了如何抓取亚马逊商品信息并进行解析。
抓取和分析JSON数据:使用Python构建数据处理管道
|
1月前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
|
1月前
|
JSON 缓存 前端开发
PHP如何高效地处理JSON数据:从编码到解码
在现代Web开发中,JSON已成为数据交换的标准格式。本文探讨了PHP如何高效处理JSON数据,包括编码和解码的过程。通过简化数据结构、使用优化选项、缓存机制及合理设置解码参数等方法,可以显著提升JSON处理的性能,确保系统快速稳定运行。
|
27天前
|
JSON API 数据安全/隐私保护
拍立淘按图搜索API接口返回数据的JSON格式示例
拍立淘按图搜索API接口允许用户通过上传图片来搜索相似的商品,该接口返回的通常是一个JSON格式的响应,其中包含了与上传图片相似的商品信息。以下是一个基于淘宝平台的拍立淘按图搜索API接口返回数据的JSON格式示例,同时提供对其关键字段的解释
|
2月前
|
JSON JavaScript Java
在Java中处理JSON数据:Jackson与Gson库比较
本文介绍了JSON数据交换格式及其在Java中的应用,重点探讨了两个强大的JSON处理库——Jackson和Gson。文章详细讲解了Jackson库的核心功能,包括数据绑定、流式API和树模型,并通过示例演示了如何使用Jackson进行JSON解析和生成。最后,作者分享了一些实用的代码片段和使用技巧,帮助读者更好地理解和应用这些工具。
123 0
在Java中处理JSON数据:Jackson与Gson库比较
|
2月前
|
JSON JavaScript API
(API接口系列)商品详情数据封装接口json数据格式分析
在成长的路上,我们都是同行者。这篇关于商品详情API接口的文章,希望能帮助到您。期待与您继续分享更多API接口的知识,请记得关注Anzexi58哦!
|
2月前
|
JSON 前端开发 Java
【Spring】“请求“ 之传递 JSON 数据
【Spring】“请求“ 之传递 JSON 数据
87 2
|
3月前
|
存储 JSON Go
在Gin框架中优雅地处理HTTP请求体中的JSON数据
在Gin框架中优雅地处理HTTP请求体中的JSON数据