OpenTofu の Lua/Go ですぐに自作関数を定義できる機能を試す

## はじめに

ちょうど一年前の2024年4月に [OpenTofu](https://opentofu.org/) から、HCL中の式で簡単にGoやLuaで自作したカスタム関数を呼び出せる機能およびプロバイダがリリースされていたので、本記事でそれを試していきたいと思います。

https://opentofu.org/docs/v1.7/intro/whats-new/#provider-defined-functions

元々 OpenTofu / Terraform 双方でカスタム関数自体の機能は提供されていますが、ちょっとした関数を定義するだけでも自作プロバイダを用意する必要があり面倒です。

> Provider-defined functions are **supported in Terraform 1.8 and later.**
> https://developer.hashicorp.com/terraform/plugin/framework/v1.13.x/functions/concepts

> **As of OpenTofu 1.7.0**, providers may define their own functions to be available during execution.
> https://opentofu.org/docs/language/functions/#provider-defined-functions

今回紹介する機能はそのままTerraformコード中にカスタム関数のコードを書けばいいだけなので、とても簡単に使うことができます。


## 実際に試してみる

（この記事のソースコードは https://gitlab.com/ksaitou/2025-03-15_opentofu-immediate-func にあります）

### 環境準備

`.opentofu-version` ファイルを用意した上で、 [tenv](https://github.com/tofuutils/tenv) でOpenTofuをインストールして試します。

```.opentofu-version
1.9.0
```

```sh
# OpenTofu のインストール
$ tenv tofu install
```

今回試用するものをプロバイダとして定義します。残念ながら去年(2024-04)の発表以降開発はアクティブではないようです。

```terraform:main.tf
terraform {
    required_providers {
        lua = {
            source = "opentofu/lua"
            version = "0.0.2"
        }
        go = {
            source = "opentofu/go"
            version = "0.0.3"
        }
    }
}
```

```sh
# プロジェクトの初期化
$ tofu init
```

### Lua (`opentofu/lua`)

まずLua関数の実行から行っていきます。

https://github.com/opentofu/terraform-provider-lua

```terraform:lua.tf
locals {
    lua_add = <<EOT
function add( input )
    return input.foo + input.bar
end

return add
EOT
}

output "example" {
    value = provider::lua::exec(local.lua_add, {"foo": 100, "bar": 200})
}
```

Luaのソースコードからは関数を返せばそれを `lua::exec` で呼べるようです。

上記を実行すると、きちんと計算結果が表示されます。

```sh
$ tofu plan
Changes to Outputs:
  + example = 300
```

### Go (`opentofu/go`)

つづいて、Go関数の実行も試してみます。

https://github.com/opentofu/terraform-provider-go

```go:lib.go
package lib

type FooBar struct {
	Former int `tf:"foo"`
	Latter int `tf:"bar"`
}

func Add(foobar FooBar) int {
	return foobar.Former + foobar.Latter
}
```

```terraform:go.tf
provider "go" {
    go = file("./lib.go")
}

output "test" {
    value = provider::go::add({"foo": 300, "bar": 400})
}
```

`package lib` 上で定義したエクスポートされた関数（大文字から始まる関数）をその関数名を用いて呼び出し可能なようです。

上記を実行すると、きちんと計算結果が表示されます。

```sh
$ tofu plan
Changes to Outputs:
  + test    = 700
```

#### なぜGoのソースコードをいきなり実行できるのか？

`opentofu/go` では `./lib.go` として外部のGoファイルをスクリプト言語のように指定していますが、本来Go言語はビルドしないと動かないはずです。どうなっているのでしょう？

プロバイダのソースコードを見てみましょう。

```go:https://github.com/opentofu/terraform-provider-go/blob/main/main.go

package main

import (
// ...
	"github.com/traefik/yaegi/interp"
	"github.com/traefik/yaegi/stdlib"
)
// ...
						&tfprotov6.SchemaAttribute{
							Name:     "go",
							Type:     tftypes.String,
							Required: true,
						},
// ...
				codeVal := cfg["go"]
				var code string
				err = codeVal.As(&code)

// ...
				interpreter := interp.New(interp.Options{})
				if err := interpreter.Use(stdlib.Symbols); err != nil {
// ...
				_, err = interpreter.Eval(code)
// ...
				exports := interpreter.Symbols("lib")
				libExports := exports["lib"]
```

ざっと読む限り、`go` プロパティに指定したコードをGoのインタプリタ実装である [yaegi](https://github.com/traefik/yaegi) に渡しているようです。

つまり、ここで書いているGoはビルドされて動く通常のGoではありません。通常のGoプロバイダのように何でも自由に実装できると考えないほうがよさそうです。そういう用途であれば真っ当にGoでプロバイダを実装することになるでしょう。

プロバイダの実装については、以下の私のエントリも参考にしてください。

https://qiita.com/ssc-ksaitou/items/493482b163e8dc4d5939

## この機能（プロバイダ）は Terraform でも使えるのか？

これらのプロバイダがTerraformでも同じように使えるのか試してみます。

プロバイダがTerraformレジストリ側には存在していないので、プロバイダの参照について `registry.opentofu.org/` を `source` の先頭につけてあげれば参照可能です。

```terraform:main.tf
terraform {
    required_providers {
        lua = {
            source = "registry.opentofu.org/opentofu/lua"
            version = "0.0.2"
        }
        go = {
            source = "registry.opentofu.org/opentofu/go"
            version = "0.0.3"
        }
    }
}
```

Terraformもインストールしましょう。

```.terraform-version
1.11.2
```

```sh
# Terraform のインストール
$ tenv terraform install
```


これで参照自体はできるのですが、以下のようにそれぞれ実行可能かどうかは異なってしまいました。

- `opentofu/lua`: OpenTofu上と変わらず**実行可能**
- `opentofu/go`: `There is no function named "provider::go::add".` と言われ**エラーで終了**
  - そもそもプロバイダが任意の関数名を定義できるのがOpenTofuだけの機能の模様……

OpenTofuもTerraformと同じプロトコル([`tfprotov6`](https://github.com/hashicorp/terraform-plugin-go/tree/main/tfprotov6))を話すのですが、動的な関数部分についてはOpenTofu側のみの拡張のようです。

両方で実行したい場合はLuaに限定したほうがよさそうです。

## まとめ

OpenTofuで使えるインスタントな自作関数定義機能を使ってみました。

リポジトリを見る限り、去年の発表時点から開発が止まってしまっているので実際の利用はためらうところですが、今後に期待です。