はじめに
日常よく使う道具について「いざとなったら拡張を書いてどうにかしてやるぞ!」という自信を持つのは精神衛生上良いことです。
今回はTerraformに最低限の機能を持った自作プラグインを追加してみます。
簡単なTerraformのプラグインアーキテクチャの振り返り
簡単にTerraformのプラグインアーキテクチャについて振り返ってみます。
Terraform はプラグインアーキテクチャを採用しているソフトウェアであり、基本的なリソースのライフサイクル管理の機能はTerraform本体で提供しているのですが、AWS上などの実際の具体的なクラウドインフラ上のプロビジョニングは専用のプラグイン(例: terraform-aws-provider)を呼ぶ構造になっています。
Terraform本体からサブプロセスとしてプラグインを起動した後、プラグイン側でTerraform本体からのgRPC通信を待ち受けTerraform本体と通信(IPC)します。ただしこれらの仕組みは今回プラグインを作る上ではあまり意識する必要はありません。
プロジェクト内だけで動作する超簡単なプラグインを書いてみよう
プラグインを理解するために、プロジェクトローカルで動作するごくごく簡単なプラグインをGoで書いてみます。
以下のように起動して使う予定です。
# "example.txt" ファイルにメッセージを出力する
resource "hello" "example" {
provider = helloworld
file = "example.txt"
}
ソースコードは以下から入手可能です。
Goプロジェクトの実装 〜 バイナリの生成
まずプラグインを構成するGoプロジェクトから見ていきましょう。
file = "..."
で指定したファイル名に Hello, World!
という内容を書き込むだけのTerraformリソースを提供します。
package main
import (
"os"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() *schema.Provider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
// "hello" というリソースを定義
"hello": {
Create: func(d *schema.ResourceData, m any) error {
// "file" という属性を指定可能
filename := d.Get("file").(string)
message := "Hello, World!"
d.SetId(filename)
// "message" という属性を参照可能
d.Set("message", message)
// "file" で指定されたパスへ書き込み
os.WriteFile(filename, []byte(message), 0644)
return nil
},
Read: schema.Noop,
Update: schema.Noop,
Delete: schema.Noop,
Schema: map[string]*schema.Schema{
"file": {
Type: schema.TypeString,
Required: true,
},
},
},
},
}
},
})
}
Goプロジェクトの初期化およびビルドは以下のコマンドで行うことができます。(ビルドされたバイナリの置き場所以外は通常のGoプロジェクトと変わりません)
$ go mod init example.com/my-hello-provider
$ go mod tidy
# Terraformプロジェクトローカルのプラグインフォルダにバイナリを出力する
$ go build -o ./terraform.d/plugins/example.com/my/helloworld/1.0.0/darwin_amd64/terraform-provider-helloworld_v1.0.0
# ※ "darwin_amd64" となっている部分は macOS (Intel) の場合に指定しているのであって、
# 他の環境(OS/CPU)の場合は別の文字列を指定してください
Terraformプロジェクトの作成 〜 生成した自作プラグインのバイナリをプロジェクトから呼んでみる
./terraform.d/plugins/example.com/my/helloworld/1.0.0/darwin_amd64/terraform-provider-helloworld_v1.0.0
に出力されたGoの実行ファイルをTerraformからプラグインとして起動してもらうようにします。
Goのプロジェクトと同じ階層に以下の main.tf
を作成しましょう。
terraform {
required_providers {
helloworld = {
version = "1.0.0"
// `./terraform.d/plugins` 配下で
// `example.com/my/helloworld/${バージョン番号}/${CPUアーキテクチャ}/terraform-provider-helloworld_v${バージョン番号}` というファイルが検索される
source = "example.com/my/helloworld"
}
}
}
provider "helloworld" {}
resource "hello" "example" {
provider = helloworld
file = "example.txt"
}
あとは通常通りTerraformを初期化(terraform init
) & 実行(terraform apply
)すれば自作プラグインが動きます。
$ terraform init
...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$ terraform apply
...
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# hello.example will be created
+ resource "hello" "example" {
+ file = "example.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
hello.example: Creating...
hello.example: Creation complete after 0s [id=example.txt]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ cat example.txt
Hello, World!
プロジェクトローカルなTerraformプラグインをGoで実装して、呼び出せました!
Terraformプラグインの解決方法
自作のTerraformプラグインをローカルで解決させる方法に触れてきましたが、そもそもTerraformはプラグイン名からどうやってプラグインの場所や取得方法を解決しているのでしょうか?
以下のドキュメントに書いてあります。
The default way to install provider plugins is from a provider registry. The origin registry for a provider is encoded in the provider's source address, like registry.terraform.io/hashicorp/aws. For convenience in the common case, Terraform allows omitting the hostname portion for providers on registry.terraform.io, so you can write shorter public provider addresses like hashicorp/aws.
https://developer.hashicorp.com/terraform/cli/config/config-file#provider-installation
基本的には Terraformのレジストリ から動的にダウンロードして解決しているようです。
また、 provider_installation { ... }
ブロックにて明示的にプロバイダ(プラグイン)の場所を指定できるようです。
本記事で述べているようなローカルなプラグインの解決については、以下のルールに基づきます。
If a
terraform.d/plugins
directory exists in the current working directory then Terraform will also include that directory, regardless of your operating system. This behavior changes when you use the -chdir option with the init command. In that case, Terraform checks for the terraform.d/plugins directory in the launch directory and not in the directory you specified with -chdir.https://developer.hashicorp.com/terraform/cli/config/config-file#implied-local-mirror-directories
現在ディレクトリ配下に terraform.d/plugins
があればそれも検索対象としてくれるようです。(他にもOSごとに特定のディレクトリパスを検索してくれるようです)
まとめ
Terraformで小さなプラグインを書く方法を見てきました。もうちょっと発展したプラグインを書けばTerraformの内部の動きの洞察にも繋がりそうです。