[Terraform] terraform-plugin-frameworkでproviderを作成してみる
概要
Terraformと外部サービスを接続するプラグインのことをプロバイダー(provider)と呼びます。
たとえばhashicorp/awsをインストールしてaws_instanceを呼び出すと、EC2インスタンスを作成することができますが、これはawsプロバイダーがTerraformで指定された内容に従ってAWS SDKを実行しているからです。
awsプロバイダーのコードはGitHubで公開されています。
TerraformのプロバイダーはGo言語で作成します。
この記事では、terraform-plugin-frameworkを用いて、Misskeyのノートを管理するresourceを作成してみます。
コードは以下のGitHubレポジトリで公開しています。
https://github.com/maeda6uiui/terraform-provider-misskey
providerの定義
ファイルパスはprovider/internal/provider/provider.goです。
コード全体はGitHubの方を確認していただくとして、ここでは重要そうな部分だけ抜粋します。
func (p *MisskeyProvider) Metadata(
ctx context.Context,
req provider.MetadataRequest,
resp *provider.MetadataResponse) {
resp.TypeName = "misskey"
resp.Version = p.version
}
func (p *MisskeyProvider) Schema(
ctx context.Context,
req provider.SchemaRequest,
resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"server_url": schema.StringAttribute{
Required: true,
MarkdownDescription: "URL of the Misskey server",
},
"access_token": schema.StringAttribute{
Optional: true,
Sensitive: true,
MarkdownDescription: "Access token for the Misskey account",
},
"timeout_seconds": schema.Int32Attribute{
Optional: true,
MarkdownDescription: "Timeout for HTTP requests to the Misskey server",
},
},
}
}
func (p *MisskeyProvider) Configure(
ctx context.Context,
req provider.ConfigureRequest,
resp *provider.ConfigureResponse) {
var model model.MisskeyProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
var accessToken string
if !model.AccessToken.IsNull() && !model.AccessToken.IsUnknown() {
accessToken = model.AccessToken.ValueString()
} else {
accessToken = os.Getenv("MISSKEY_ACCESS_TOKEN")
}
if accessToken == "" {
resp.Diagnostics.AddError(
"Missing Access Token",
"The provider argument 'access_token' or the environment variable 'MISSKEY_ACCESS_TOKEN' must be set.",
)
return
}
model.AccessToken = types.StringValue(accessToken)
var timeoutSeconds int32
if !model.TimeoutSeconds.IsNull() && !model.TimeoutSeconds.IsUnknown() {
timeoutSeconds = model.TimeoutSeconds.ValueInt32()
} else {
timeoutSeconds = 5
}
model.TimeoutSeconds = types.Int32Value(timeoutSeconds)
resp.ResourceData = &model
resp.DataSourceData = &model
}
func (p *MisskeyProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
note.NewNoteResource,
}
}
func (p *MisskeyProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
note.NewNoteDataSource,
}
}
Metadataではこのproviderの情報を定義します。
Schemaでは、Terraformのproviderブロックで受け取る値を定義します。
今回の場合、server_urlが必須なので、ユーザーは以下のようにしてserver_urlを指定する必要があります。
provider "misskey" {
server_url = "https://misskey-dabansky.com"
}
Misskeyを操作するためにはアクセストークンが必要ですが、アクセストークンをハードコードするのは適切ではないため、環境変数から受け取ることができるようにしたいです。
また、タイムアウトについては必須ではなく、指定されなかった場合はデフォルトの値を使うようにしたいです。
このような処理はSchemaでは実現できないので、Configureで実行します。
Schemaで定義されたスキーマに対応するモデル(struct)を定義しておいて、req.Config.Get(ctx, &model)でモデルに値をセットします。
モデルは以下のように定義します。
type MisskeyProviderModel struct {
ServerUrl types.String `tfsdk:"server_url"`
AccessToken types.String `tfsdk:"access_token"`
TimeoutSeconds types.Int32 `tfsdk:"timeout_seconds"`
}
Terraformのステートの実体はJSONファイルですが、tfsdkで指定した名前をキーにしてJSONファイルに値が保存されます。
providerのスキーマの場合はステートには保存されませんが、resourceの場合はtfsdkで指定した値が使用されます。
providerブロックから値を読み込んだら、それをresourceとdata sourceからも参照できるようにします。
以下のようにrespに値をセットします。
resp.ResourceData = &model
resp.DataSourceData = &model
resourceの定義
ファイルパスはprovider/internal/provider/note/resource.goです。
まず、providerブロックから読み込んだ値を取得します。
providerのConfigureでresp.ResourceData = &modelというふうにセットしたので、ここでその値を読み込むことができます。
func (r *NoteResource) Configure(
ctx context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
data, ok := req.ProviderData.(*model.MisskeyProviderModel)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *MisskeyProviderModel, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
misskeyClient := misskey.NewMisskeyHttpClient(
data.ServerUrl.ValueString(),
int(data.TimeoutSeconds.ValueInt32()),
data.AccessToken.ValueString(),
)
r.misskeyClient = misskeyClient
}
読み込んだ値を使って、Misskeyのクライアントを作成します。
このクライアントはMisskeyのHTTP APIを実行するためのものです。
HTTPリクエストを投げるだけなので詳細は省きます。
コードはprovider/internal/provider/misskey/client.goを確認してください。
resourceのスキーマ定義もproviderとほとんど同じですが、少し異なる点があります。
たとえばtextはノートの本文を管理するフィールドですが、Misskeyの場合、ノートの本文をin-placeで更新することはできません。
そのため、textが更新された場合はresourceを再作成するようにしたいです。
それを実現するのが、PlanModifiersです。RequiresReplace()を指定すると、このフィールドが変化した場合にresourceのreplaceが要求されるようになります。
"text": schema.StringAttribute{
Required: true,
MarkdownDescription: "Text of the note",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
次に、ノートに対するCRUD操作を定義する必要があります。Createの例を以下に記載します。
planから値を取得します。
var model NoteModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
無事にノートを作成できると、作成したノートのIDが取得できるので、それをステートに保存します。
model.Id = types.StringValue(respModel.CreatedNote.Id)
resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
同様にして、Read、Update、Deleteを実装します。
Misskeyのノートの場合はin-placeでのupdateは不可能なので、もしUpdateが呼ばれてもエラーになるようにしてあります。
func (r *NoteResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse) {
// Every attribute requires replacement, so this should never be called
resp.Diagnostics.AddError(
"Update Not Supported",
"Misskey note cannot be updated in place, it must be recreated instead",
)
}
main関数
エントリーポイントとなるmain関数を実装します。
ファイルパスはprovider/main.goです。
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/maeda6uiui/terraform-provider-misskey/internal/provider"
)
var (
version string = "0.0.1-alpha1"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/maeda6uiui/misskey",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}
動作確認
misskey_note resourceの動作確認をするため、Terraformのコードを追加します。
動作確認用のコードはexample/resourceディレクトリにあります。
まず、providers.tfでproviderの設定を行います。
動作確認用なので、ステートはローカル環境に保存します。providerブロックではserver_urlのみ指定し、アクセストークンは環境変数MISSKEY_ACCESS_TOKENで指定します。
terraform {
required_providers {
misskey = {
source = "maeda6uiui/misskey"
version = "0.0.1-alpha1"
}
}
backend "local" {
path = "terraform.tfstate"
}
required_version = "~>1.9"
}
provider "misskey" {
server_url = "https://misskey-dabansky.com"
}
main.tfは以下のようになっています。
resource "misskey_note" "test" {
text = <<-EOT
テスト1
テスト2
テスト3
EOT
visibility = "specified"
visible_user_ids = []
}
このディレクトリでterraform applyを実行し、Misskeyのノートが作成されることを確認できました。