在Terraform中(強制性地!)定義自己的功能

Terraform是不可或缺的基础设施即代码工具。它具有声明性的语法、可靠的计划输出和丰富的提供商支持等许多令人高兴的特点。

然而,在具有独特的配置描述语言HCL/HIL和Terraform本身的限制下,经常会发出”我想写这样的处理,但却无法写!”或者”强行写会变得混乱不堪!”的叹息(虽然相比以前有所改善……)。

这次将介绍一种能稍微缓解这种困扰的“定义自己的函数”技巧。

※ 注意:本文仅仅是介绍一种“这样就能做到”的方法,并不代表它是否是良好的实践方法,这一点存在较大的讨论空间。

使用中文将以下内容进行改写,只需要给出一种选择:

Terraform 版本

我已经使用下述版本的Terraform进行了确认。

$ terraform -v
Terraform v0.11.7

虽然如此,由于没有太多使用新功能,所以稍旧版本也应该能够运行(但是,请注意在 v0.10.3 中引入了本地值,因此早于此版本的需要进行少许修改)。

有哪些方法可供选择?

在目前的 Terraform 中,定义函数的方式有以下3种选择(如果还有其他选项,请告诉我)。

    1. 从外部数据源调用外部脚本

 

    1. 将插值函数编写为一个模块内的集合

 

    编译自定义的插值函数

每个都有像下表这样的特点:

External data source から外部スクリプトを呼ぶModule 内に interpolation function をまとめる独自 interpolation function をコンパイルする依存関係:broken_heart: スクリプト実行環境も必要:white_check_mark: Terraform のみ:white_check_mark: (独自の)Terraform のみできること:white_check_mark: 何でも:broken_heart: interpolation でできることだけ:white_check_mark: 何でもブロックの記述:broken_heart::broken_heart::white_check_mark: 不要(インラインですぐ使える)ソースビルド:white_check_mark: 不要:white_check_mark: 不要:broken_heart:

第三种方法需要对Terraform本身进行编译,牵涉较多,因此本次不深入讨论。对于感兴趣的人,我只提供源代码的位置。

请提供一个在线的解释。

如果你想要一个通用的函数,也可以尝试实现并向主仓库发起 pull request,这也是一个不错的选择。

让我们试试吧

这次实施的内容已经上传至Github,请根据需要克隆并使用。

请给出以下内容的中文本地化释义,只需要一种选项: https://github.com/tmshn/terraform-how-to/define-custom-functions

尝试实施的处理内容

我們將作為示例,實現一個函數,它接收URL作為參數,並將其解析為方案、主機、端口號、路徑、查詢字串和哈希值。

以下是一个类似的想法。

>>> parse_url('https://blog.example.com/articles?category=bigdata')
{'scheme': 'https',
 'host':   'blog.example.com',
 'port':   '',
 'path':   '/articles',
 'query':  'category=bigdata',
 'hash':   ''}

准备好了

在Terraform的配置文件中,先通过变量声明要解析的URL。

variable "url" {
  type        = "string"
  description = "URL to parse."
  default     = "https://blog.example.com/articles?category=bigdata"
}

选项一:第一种方法是从外部数据源调用外部脚本。

用法

实现目标程序,并将其指定为外部数据源的程序来调用。输入数据通过查询以映射的形式指定。

如果能够成功评估这个条件,那么程序的输出结果将可以通过data.external.url_parts.result进行引用。

data "external" "url_parts" {
  program = ["python", "parse_url.py"]

  # input to the program
  query = {
    url = "${var.url}"
  }

  # result = (...result of the program...)
}

解說

外部数据源是由外部提供商提供的数据来源。分别来解释一下……

    • External プロバイダ……Terraform と外部のプログラムの橋渡しをするためのプロバイダ

今回紹介する data source のみが定義されている

https://www.terraform.io/docs/providers/external/index.html

Data source ……読み取り専用のリソースのこと

別のところで作ったリソースの情報を現在の Terraform 内で使いたい場合などに用いる(たとえば AWS Server certificate を手動でアップロードし、その情報を使うなど)

https://www.terraform.io/docs/configuration/data-sources.html

使用这个外部数据源,您可以执行任意程序来获取数据。

一般情况下,我们通常使用aws命令或gcloud命令来执行操作,以获取Terraform尚未实现的资源信息,但也可以将其作为纯函数来使用。

在Terraform和外部程序之间,交互使用以下协议:

queryブロックで文字列 1 のキーバリューを渡すことができ、それは外部プログラムの標準入力に JSON 形式で渡される
外部プログラムの戻り値は JSON 形式で標準出力に出力することができ、それは Terraform 側 data source の result attribute に設定される
成功時はゼロ、エラー時は非ゼロの実行ステータスを返すこと

你可以将命令行参数作为输入,但除了调用现有命令时,将所有输入汇集到标准输入上会减少混淆。

实施案例

这次我用Python来写了一个脚本,用于执行前面提到的处理。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Small script to parse URL (intended to use from Terraform)."""

import json
import re
import sys


URL_REGEX = re.compile('^(?:(?P<scheme>\\w+):\\/\\/)?(?P<host>[^:\\/]+)(?::(?P<port>\\d+))?(?P<path>[^\\?#]*)(?:\\?(?P<query>[^#]*))?(?:#(?P<hash>.*))?$')


def parse_url(url):
    """Parse URL and returns its parts as dict."""
    try:
        return URL_REGEX.search(url).groupdict(default='')
    except AttributeError:  # If regex did not match
        raise ValueError('invalid url')


def main():
    """Read URL from stdin, parse it, and write result into stdout."""
    input_data = json.load(sys.stdin)

    url = input_data['url']
    result = parse_url(url)

    json.dump(result, sys.stdout)


if __name__ == '__main__':
    main()

选项:将插值函数放在 Module 内的方法2中整合。

用法

如果定义需要调用的模块并将其路径指定为源,同时将其他输入数据(本例中为URL)作为参数指定,那就可以了。

因此,这次我们将定义一个名为 result 的单一输出,以便与 method 1 相匹配,您可以通过 module.url_parts.result 获取该值。

module "url_parts" {
  source = "./parse_url"
  url    = "${var.url}"
}

解释

    • Module ……リソース類をカプセル化するもの。繰り返し現れる共通処理をまとめるために使われる

AWS で Launch Configration と Auto Scaling Group を組み合わせてオートスケーリンググループを作ったり GCP で いろいろ組み合わせてロードバランサを作ったり など、複数のリソースを1つの論理的な単位にまとめるのが主な目的

https://www.terraform.io/docs/configuration/modules.html

Terraform registory にコミュニティが作った様々な module が集まっている

https://registry.terraform.io/

如果我们将“函数”看作是“一系列操作的集合”,那么用模块来实现它是合理的。然而,这有点偏离本来的目的,即“不定义任何资源”。

现在,每个模块的命名空间是完全分离的,并且限制了模块之间的数据交换。作为规范,必须要理解以下内容(这里将调用模块的一方称为”父”,被调用的一方称为”子”):

    • 子 module で variable として定義したものだけが、親 module から値を指定できる

 

    • 子 module で output として定義したものだけが、親 module から値を取得できる

 

    それ以外は、ある module から 別の module の情報を見ることは一切できない(親子 module 間であっても)

顺便提一句,常规设置实际上是写在最上层的根模块(root module)中。通过这种机制,我们可以从 Terraform 外部指定变量或获取输出结果。

一个实施的例子

我们来试着使用这个来实现一个”函数”。

只需执行与前述的Python脚本相同的处理即可。在Terraform中,可以使用replace函数来使用正则表达式,所以我们将尝试直接使用与Python中完全相同的正则表达式来实现。

variable "url" {
  type        = "string"
  description = "[Required] URL to parse."
}
locals {
  url_regex = "/^(?:(?P<scheme>\\w+):\\/\\/)?(?P<host>[^:\\/]+)(?::(?P<port>\\d+))?(?P<path>[^\\?#]*)(?:\\?(?P<query>[^#]*))?(?:#(?P<hash>.*))?$/"
}

locals {
  scheme = "${replace(var.url, local.url_regex, "$scheme")}"
  host   = "${replace(var.url, local.url_regex, "$host")}"
  port   = "${replace(var.url, local.url_regex, "$port")}"
  path   = "${replace(var.url, local.url_regex, "$path")}"
  query  = "${replace(var.url, local.url_regex, "$query")}"
  hash   = "${replace(var.url, local.url_regex, "$hash")}"
}
output "result" {
  value = {
    scheme = "${local.scheme}"
    host   = "${local.host}"
    port   = "${local.port}"
    path   = "${local.path}"
    query  = "${local.query}"
    hash   = "${local.hash}"
  }
}

在定义模块时,通常将变量放在variables.tf文件中,将输出放在outputs.tf文件中,所以这次也按照这个规则进行了。然而,在使用模块时,只需指定目录名称,因此如何分割文件可以根据个人喜好进行调整。

试试看

终于我要开始执行这些了。

$ terraform init  # 初期化処理
$ terraform apply # 実行!

申请已完成,请确认结果。

$ echo 'data.external.url_parts.result' | terraform console
{
  "hash" = ""
  "host" = "blog.example.com"
  "path" = "/articles"
  "port" = ""
  "query" = "category=bigdata"
  "scheme" = "https"
}
$ echo 'module.url_parts.result' | terraform console
{
  "hash" = ""
  "host" = "blog.example.com"
  "path" = "/articles"
  "port" = ""
  "query" = "category=bigdata"
  "scheme" = "https"
}

这些结果都符合我想象中的。

我会尝试其他的网址。

$ terraform apply -var url='tcp://redis.example.com:6379'
$ echo 'data.external.url_parts.result' | terraform console
{
  "hash" = ""
  "host" = "redis.example.com"
  "path" = ""
  "port" = "6379"
  "query" = ""
  "scheme" = "tcp"
}
$ echo 'module.url_parts.result' | terraform console
{
  "hash" = ""
  "host" = "redis.example.com"
  "path" = ""
  "port" = "6379"
  "query" = ""
  "scheme" = "tcp"
}

做得真好!

结束

在本文中,我们将介绍如何使用Terraform定义自己的函数。

    • External data source から外部スクリプトを呼ぶ

 

    Module 内に interpolation function をまとめる

我介绍了两个选项。

请为我提供一个选项,以中文本地化地重新表述以下句子:https://github.com/tmshn/terraform-how-to/define-custom-functions

在文章中,只是通过 terraform console 命令进行了确认,但实际上可能会将其指定为资源的参数。

请用中文本地化地重新表述以下内容(只需提供一种选项):

这种像我们刚才介绍的方式,可以说是“酷炫练习”吗?更倾向于说,这是一种有点奇怪的黑魔法式的做法啊。

对于我个人来说,我甚至对Terraform自身的不完善感到厌倦,质疑它是否真的代表了现代配置管理工具应有的样子。

不过绝对不是一种糟糕的工具,事实上还有一个高效的开发团队和活跃的社区的支持。作为这个社区的一员,我希望能够积极地为Terraform的进一步发展做出贡献,使它成为更好的工具。

那么,各位也祝你们开心地进行Terraforming吧!

在Terraform中,称为“字符串型”的是指包括数值类型和布尔类型在内的标量类型。
广告
将在 10 秒后关闭
bannerAds