在Terraform中(強制性地!)定義自己的功能
Terraform是不可或缺的基础设施即代码工具。它具有声明性的语法、可靠的计划输出和丰富的提供商支持等许多令人高兴的特点。
然而,在具有独特的配置描述语言HCL/HIL和Terraform本身的限制下,经常会发出”我想写这样的处理,但却无法写!”或者”强行写会变得混乱不堪!”的叹息(虽然相比以前有所改善……)。
这次将介绍一种能稍微缓解这种困扰的“定义自己的函数”技巧。
※ 注意:本文仅仅是介绍一种“这样就能做到”的方法,并不代表它是否是良好的实践方法,这一点存在较大的讨论空间。
使用中文将以下内容进行改写,只需要给出一种选择:
Terraform 版本
我已经使用下述版本的Terraform进行了确认。
$ terraform -v
Terraform v0.11.7
虽然如此,由于没有太多使用新功能,所以稍旧版本也应该能够运行(但是,请注意在 v0.10.3 中引入了本地值,因此早于此版本的需要进行少许修改)。
有哪些方法可供选择?
在目前的 Terraform 中,定义函数的方式有以下3种选择(如果还有其他选项,请告诉我)。
-
- 从外部数据源调用外部脚本
-
- 将插值函数编写为一个模块内的集合
- 编译自定义的插值函数
每个都有像下表这样的特点:
第三种方法需要对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吧!