Day 13-用了十几天,总算回头看 Language Syntax 文件

本篇介绍 Terraform syntax,为何 .tf 内容是这个格式

课程内容与代码会放在 Github 上: https://github.com/chechiachang/terraform-30-days

赛後文章会整理放到个人的部落格上 http://chechia.net/

追踪粉专可以收到文章的主动推播

https://ithelp.ithome.com.tw/upload/images/20210901/20120327NvpHVr2QC0.jpg


lecture retrospective

写到现在,应该都可以写出能够工作的 terraform。

到目前为止,课程对学生的期待是『能够 google 到社群分享的 .tf,并且会正确的复制贴上,转化成自己的 .tf 内容』。

做到现在,想必也累积了一些问题。之前的内容,或许有些逻辑还不是很清楚,课程都简单带过,许多复杂的部分略过不提;目的先让学生对语言有个基础的了解,有实际操作的经验跟手感,之後做深入的讨论时,更能理解内容。

接下来要继续深入,我们会花一点篇幅,回头细讲细节,把关防文件细细地走过

Terraform syntax

先从这篇出发Terraform Syntax,介绍 Terraform syntax。这里又分为两部分:一个是Configuration Syntax,另一个是Json Configuration Syntax。乍看之下有点疑惑,json 是从哪跑出来的?我们可以从几个角度看这件事:

Terraform 底层 low-level syntax 是由 Hashicorp Configuration Language 定义的

  • HCL 又是什麽东西?不精确的说,他就是过去几堂课我们写的 .tf 的内容语法
    • 精确的说应该反过来,.tf 的语法底层是由 hcl 定义
  • Hashicorp 当初推出 HCL 的目的,而不直接用 json 或 yaml
    • 许多高阶程序语言(ex. ruby, golang, c...) 不易描述多阶层结构资料(ex. json),与宣告式(declarative)的资料内容
      • 而这两者在定义 infrastructure 等 configuration / state 时很重要
      • 这也是许多应用的 conf 不会用高阶语言来直接定义,例如:
        • kubernetes 与 CNCF 专案使用 yaml / json
        • nginx.conf 的语法很有趣,对 hcl 的设计影响很大
    • json 作为 data-interchange format,设计本身已经是人类可读,语法结构非常清晰,也是泛用的 api 沟通格式
      • 然而太复杂的结构可读性变差。例如 json key 深度越多层,可读性大幅下降,必须使用外部 tool parse (ex. 本课程爱用的 jq)
      • json 不易描述更多增益功能性语法,例如:没有 native comment,loop,data type,escape...等
      • yaml 也是有同样的问题
  • HCL 依循 key-value 与阶层 block {} 设计(类似 json)
  • 并增加许多功能性的语法定义,让使用 HCL 语言的应用(ex. terraform )

这里要讲古一下:历史来看, hashicorp 在 2014 年发表 terraform,同时也释出 hcl 语言 spec

  • hashicorp 2014 前的产品采用不同的 conf
    • vagrant 也是 configuration 工具采用 ruby
    • packer 使用 json。然而随着 hcl 逐渐稳定,新版 packer 也支援 hcl 的 template
  • terraform configuration 是第一个套用 hcl spec 的产品,hcl spec 也随 terraform 的更新而演进
  • 後面的产品(consul,vault)都变成 hcl 的形状了

用范例讲解一下上面一大段,回头看 _poc/security_group/security_group.tf

  • 他是 terraform configuration syntax,底下是 hcl
  • 支援 .tf native 格式,也支援 .tf.json json 格式

使用 cli 的 global option -json 来输出 json

  • 比较 hcl 与 json 的输出差异
  • json 乍看之下有点凌乱,使用 jq parse 後,会发现输出内容相同
  • double quote 的跳脱字元 (escape)
terraform plan

  # azurerm_network_security_group.main will be created
  + resource "azurerm_network_security_group" "main" {
      + id                  = (known after apply)
      + location            = "southeastasia"
      + name                = "poc-chechia"
      + resource_group_name = "terraform-30-days"
      + security_rule       = (known after apply)
      + tags                = {
          + "environment" = "poc"
        }
    }

  # azurerm_network_security_rule.main["homeport22"] will be created
  + resource "azurerm_network_security_rule" "main" {
      + access                      = "Allow"
      + destination_address_prefix  = "*"
      + destination_port_range      = "22"
      + direction                   = "Inbound"
      + id                          = (known after apply)
      + name                        = "Port_22"
      + network_security_group_name = "poc-chechia"
      + priority                    = 100
      + protocol                    = "*"
      + resource_group_name         = "terraform-30-days"
      + source_address_prefix       = "17.110.101.57"
      + source_port_range           = "*"
    }

terraform plan -json

{"@level":"info","@message":"azurerm_network_security_group.main: Plan to create","@module":"terraform.ui","@timestamp":"2021-08-18T22:58:26.459366+08:00","change":{"resource":{"addr":"azurerm_network_security_group.main","module":"","resource":"azurerm_network_security_group.main","implied_provider":"azurerm","resource_type":"azurerm_network_security_group","resource_name":"main","resource_key":null},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"azurerm_network_security_rule.main[\"homeport22\"]: Plan to create","@module":"terraform.ui","@timestamp":"2021-08-18T22:58:26.461178+08:00","change":{"resource":{"addr":"azurerm_network_security_rule.main[\"homeport22\"]","module":"","resource":"azurerm_network_security_rule.main[\"homeport22\"]","implied_provider":"azurerm","resource_type":"azurerm_network_security_rule","resource_name":"main","resource_key":"homeport22"},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"Plan: 2 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2021-08-18T22:58:26.461227+08:00","changes":{"add":2,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}

# format with jq

{
  "@level": "info",
  "@message": "azurerm_network_security_rule.main[\"homeport22\"]: Plan to create",
  "@module": "terraform.ui",
  "@timestamp": "2021-08-18T22:58:26.461178+08:00",
  "change": {
    "resource": {
      "addr": "azurerm_network_security_rule.main[\"homeport22\"]",
      "module": "",
      "resource": "azurerm_network_security_rule.main[\"homeport22\"]",
      "implied_provider": "azurerm",
      "resource_type": "azurerm_network_security_rule",
      "resource_name": "main",
      "resource_key": "homeport22"
    },
    "action": "create"
  },
  "type": "planned_change"
}

{
  "@level": "info",
  "@message": "Plan: 2 to add, 0 to change, 0 to destroy.",
  "@module": "terraform.ui",
  "@timestamp": "2021-08-18T22:58:26.461227+08:00",
  "changes": {
    "add": 2,
    "change": 0,
    "remove": 0,
    "operation": "plan"
  },
  "type": "change_summary"
}

.tf 与 .tf.json 的格式转换,我们可以用一个小工具 kvz/json2hcl来做转换

# azure/_poc/security_group/provider.tf

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 2.65.0"
    }
  }

  required_version = ">= 1.0.1"

  # remote Backend
  backend "azurerm" {
    resource_group_name  = "terraform-30-days-poc"
    storage_account_name = "tfstate8b8bff248c5c60c0"
    container_name       = "tfstate"
    key                  = "_poc/security_group/terraform.tfstate"
  }
}

provider "azurerm" {
  features {}
}

安装 json2hcl

# azure/_poc/security_group/provider.tf

curl -SsL https://github.com/kvz/json2hcl/releases/download/v0.0.6/json2hcl_v0.0.6_darwin_amd64 \
  | sudo tee /usr/local/bin/json2hcl > /dev/null && sudo chmod 755 /usr/local/bin/json2hcl && json2hcl -version

v0.0.6

json2hcl -reverse < azure/_poc/security_group/provider.tf
{
  "provider": [
    {
      "azurerm": [
        {
          "features": [
            {}
          ]
        }
      ]
    }
  ],
  "terraform": [
    {
      "backend": [
        {
          "azurerm": [
            {
              "container_name": "tfstate",
              "key": "_poc/security_group/terraform.tfstate",
              "resource_group_name": "terraform-30-days-poc",
              "storage_account_name": "tfstate8b8bff248c5c60c0"
            }
          ]
        }
      ],
      "required_providers": [
        {
          "azurerm": [
            {
              "source": "hashicorp/azurerm",
              "version": "~\u003e 2.65.0"
            }
          ]
        }
      ],
      "required_version": "\u003e= 1.0.1"
    }
  ]
}

然而输入 _poc/security_group/security_group.tf 则会出错

  • json 没有办法辨识 hcl 的功能性语法
    • variable 如:local. each.,是 hcl 送进 terraform 後才能有效 evaluate
    • for_each 在 json 中也不存在
json2hcl -reverse < azure/_poc/security_group/security_group.tf

unable to parse HCL: At 2:25: Unknown token: 2:25 IDENT local.name

我们可以依据hcl readme 补上 double quote "",让格式符合 json 的格式,这样就可以顺利转换

  • 转换成标准 json
  • "${ }" 对 terraform 有额外意义,会自动 evaluate 变数值
cat _poc/security_group/security_group_json.tf

resource "azurerm_network_security_rule" "main" {
  for_each                    = "${local.rules}"
  name                        = "${each.value.name}"
  priority                    = "${each.value.priority}"
  direction                   = "${each.value.direction}"
  access                      = "${each.value.access}"
  protocol                    = "${each.value.protocol}"
  source_port_range           = "${each.value.source_port_range}"
  destination_port_range      = "${each.value.destination_port_range}"
  source_address_prefix       = "${each.value.source_address_prefix}"
  destination_address_prefix  = "${each.value.destination_address_prefix}"
  resource_group_name         = "${local.resource_group_name}"
  network_security_group_name = "${local.name}"
}

json2hcl -reverse < azure/_poc/security_group/security_group_json.tf

{
  "resource": [
    {
      "azurerm_network_security_rule": [
        {
          "main": [
            {
              "access": "${each.value.access}",
              "destination_address_prefix": "${each.value.destination_address_prefix}",
              "destination_port_range": "${each.value.destination_port_range}",
              "direction": "${each.value.direction}",
              "for_each": "${local.rules}",
              "name": "${each.value.name}",
              "network_security_group_name": "${local.name}",
              "priority": "${each.value.priority}",
              "protocol": "${each.value.protocol}",
              "resource_group_name": "${local.resource_group_name}",
              "source_address_prefix": "${each.value.source_address_prefix}",
              "source_port_range": "${each.value.source_port_range}"
            }
          ]
        }
      ]
    }
  ]
}

这也是旧版 terraform 常出现 "${}" 语法的原因。新版 terraform 已经能自动 parse 没有 double quote 的 hcl 语法,并且 validate 时会对有不必要 double quote 的 "${}" 语法跳出 warning。

Review terraform resource

了解 terraform syntax 後,我们要回头重新检视 hcl 中描述的 terraform resources,以及为什麽 resources 会有这些行为

resource block {}

  • hcl parse 後是一层 json node
  • block 内有许多参数 Resource Arguments,包含:
    • resource 远端物件的 arguments
      • 例如:resource.azurerm_linux_virtual_machine 的 name, size ...等是云端物件的参数
      • 这些物件与参数,透过 provider 实作,转成公有云 api 接受的 json payload,向 api 发出 request,变更远端 resource
      • terraform 可以管理或变更这些云端物件参数
    • 另外还有 resource meta-argument (ex. provider, for_each, lifecycle,...)
      • 这些 meta-argument 是 terraform 的参数,也就是上面提到的功能性参数,用来改变 resource 行为,方便使用者透过 terraform 做更高阶的 resource 的管理
      • 例如:在 terraform 内操作回圈控制 resource
      • meta-argument 只在 terraform 内部有效,作用时间与其他参数不同

我们在编写 hcl 内容时应善用 meta-argument,应注意

  • 哪些参数是 terraform 内参数,哪些是 meta-argument
  • terraform cli workflow 的各个阶段,argument 会作用
  • module 间的 depends_onvariable + argument 的使用,会造成 module 彼此的依赖性
    • 造成 apply 时候,terraform 送出 request 的先後顺序有差异
  • for each 与 count 是强大的工具,但使用上需要注意的地方,我们下一章节会说明

Source code

如果对 Terraform 原始码有兴趣,可以看与 hcl config parse

Homework

官方文件阅读测验

  • Terraform: configuration syntax
    • 了解平时使用的 .tf 内容底层是如何定义的
    • 自己的理解,与官方文件描述内容是否相符
    • 被一些单字,官方是如何用英文描述功能,描述元件,描述语法
      • 之後遇到问题 google 找问题时,使用对的单字问对的问题,才搜寻的到解答

References

Next session

meta-argument 只在 terraform 内部有效,细节范例我们下一章节会说明


<<:  Day13 Lab 2 - Object storage API层

>>:  Day 12:「我可不可以 CDN 就好?」- Tailwind 安装及设定

Day1为什麽要做网站勒?

【前言】 其实有蛮多很有趣的的主题,最後会选择做一个平庸的网站,原因是想要圆一个大学时期的後悔 大学...

Elastic Stack第三十重

Logstash Part II 本篇介绍如何搭配filebeat和logstash,把apache...

[Day8]Where子句实作

在HR的EMPLOYEES资料表中,查询2003年6月17日到职的员工姓名及工作部门代码。 SEL...

『比昨天的自己还要好』的菜鸟工程师

回顾 30天咻的一下就过去了,第一次的铁人赛暂时画下小句点,那些期待补的篇幅我没忘(?) 漂向何处 ...

Day 7 - 浅谈Laravel资料库关联的运用

当数据庞大时,我们不会把所有资料都存在同一个资料表,会依照资料类型做分类,例如:使用者资料的user...