🤖

Terraformのcustom providerに入門してみた

2022/12/01に公開

この記事はCyberAgent AI tech studio | Go Advent Calendar 2022 1日目の記事です。

TerraformとCustom Provider

近年、クラウドで利用するサービス等はコードにより管理するIaC(Infrasturcture as a Code)で共有されていることが多いと思います。例えばAWSではCloudFormationやCDK、GCPではCloud Deployment Managerなどがあります。
その中でも多くの企業がIaCを実現するために採用している技術が、今回取り上げるTerraformではないでしょうか?

TerraformとはHashiCorp社が開発したオープンソースです。TerraformではHCL(HashiCorp Configuration Language)でインフラの構成を宣言することで該当のサービスのプロビジョニングや変更等を行うことができます。

Terraformを採用することによるメリットとしてはマルチクラウドにおいてHCLの文法を学ぶだけで良い点があります。例えば今までAWSのサービスばかりを書いていたエンジニアが、GCPもTerraformで書くことになった場合でもGCPのサービスの特徴やHCLという文法のキャッチアップに関しては必要がありません。

ここまででなんとなくTerraformについて理解できたかと思いますが、ここで疑問が生じます。なぜTerraformはAWSやGCPのように全く異なるクラウドに対して terraform apply などの共通したコマンドでプロビジョニングや変更等が行えるのでしょうか?

この疑問を解決するためにはTerraformのCustom Providerという概念を理解する必要があります。
この記事では実際にTerraformのCustom Providerに触れることで概念の理解と今後様々なProviderを読むときの助けになることを目的とします。

Terraform Custom Providerとは

そもそも、Custom Providerとはどのようなものなのかを説明します。
Terraformはplanの生成のために必要なGraph Walkなどのコアな処理を行うTerraform CoreとAWSやGCPなどのサービスに依存した処理を担当するProviderのようなTerraform Pluginsというように責務が分かれています。
このProviderはHashiCorp社からリリースされているものもあれば3rdパーティーが提供するものまで様々にあります。利用可能なProviderはTerraform Registryから確認することが可能です。

このことからもわかるように実際に terraform apply のようなコマンドを実行すると内部では、Terraform CoreがRPC通信を介してTerraform PluginsのProviderとコミュニケーションをとり、対応するAPIなどをProviderが実行するといった風に動作します。
以下が公式が発表している構成図です。

実際に触れてみる

ここまででTerraformの大まかな構成と動作の仕組みは理解できたかと思います。
残すは実践ということで実際にCutom Providerを作成してみましょう。
TerraformのCustom Providerを作成する方法は2通りあります。1つがSDKを使用して書く方法です。こちらは既存の方法で昔から存在する多くのProviderはこちらを利用しています。もう1つが新しくできたTerraform Plugin Frameworkというものです。新しく作成する場合はこちらも検討してみてねと公式には書かれています。

今回は比較的リーディングする中で慣れてきたためSDKを使用して作成してみます。

TerraformのProviderを作成する上で定義できる内容は以下の2つです

  • Data source
  • Resource

Data sourceはHCLの以下のようなコードです

data "hoge" "fuga" {
  name = "piyo"
}

Resourceはそれぞれのリソースの状態と実行するコマンドに合わせてCreate, Read, Update, Deleteが実行されます。HCLの以下のようなコードに対して適用されます。

resource "hoge" "fuga" {
  name = "piyo"
}

Providerの定義から始めます。Providerは以下のように*schema.Resourceとして定義されています

package main

import "github.com/hashicorp/terraform-plugin-sdk/helper/schema"

func Provider() *schema.Provider {
	return &schema.Provider{
		ResourcesMap: map[string]*schema.Resource{
			"example_server": UUIDGenerator(),
		},
	}
}

ここでexample_serverというkeyにしている理由はprovider名_xxxにするためです。今回で言うとprovider名がterraform-provider-exampleのためこのようにしています。

UUIDGeneratorの実装は以下の通りです。

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"

	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func UUIDGenerator() *schema.Resource {
	return &schema.Resource{
		Create: func(r *schema.ResourceData, m any) error {
			uc, ok := r.Get("uuid_count").(string)
			if !ok {
				return errors.New("failed to get uuid_count")
			}
			r.SetId(uc)
			url, err := url.JoinPath("https://www.uuidtools.com/api/generate/v1/count/", uc)
			if err != nil {
				return fmt.Errorf("url.JoinPath() err = %w", err)
			}
			resp, err := http.Get(url)
			if err != nil {
				return fmt.Errorf("http.Get() err = %w", err)
			}
			defer resp.Body.Close()
			return nil
		},
		Read: func(r *schema.ResourceData, _ any) error {
			return nil
		},
		Update: func(r *schema.ResourceData, _ any) error {
			return nil
		},
		Delete: func(r *schema.ResourceData, _ any) error {
			r.SetId("")
			return nil
		},
		Schema: map[string]*schema.Schema{
			"uuid_count": {
				Type:     schema.TypeString,
				Required: true,
			},
		},
	}
}

このようにResourceに関するものを定義することでよしなにTerraform Coreが実行してくれます。
注目すべきはr.SetId(string)です。TerraformのリソースにはIdが振られます。もしこのIDが設定されていない場合既に存在しないものとしてリソースが削除されてしまいます。
そのため、CreateではIdをセットし、DeleteではIdを空白にすることでプロビジョニングと削除を実装することができます。

ここまで実装したらビルドしていきます。TerraformのPluginsで使うバイナリの名前にもルールがあります。
ビルド後のバイナリはterraform-provider-プロバイダ名にする必要があります。
今回の例だと go build -o terraform-provider-example になります。
ビルドしたバイナリを以下のパスに移します

~/.terraform.d/plugins/${host_name}/${namespace}/${type}/${version}/${target}

上記の${}で囲っている部分を以下のルールで指定します。

  • host_name-> somehostname.com
  • namespace-> Custom provider name space
  • type-> Custom provider type
  • version-> semantic versioning of the provider (ex: 1.0.0)
  • target-> target operating system

ここまで設定したら実際に使用してみます。

resource "example_server" "hello" {
  uuid_count = "1"
}

terraform {
  required_providers {
    example = {
      version = "~> 1.0.0"
      source  = "path/user/provider"
    }
  }
}

この状態でterraform planをすると以下のようになります

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:

  # example_server.hello will be created
  + resource "example_server" "hello" {
      + id         = (known after apply)
      + uuid_count = "1"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

以上で実装完了です。お疲れ様でした。

まとめ

今回はTerraformの構成から実際に自分でProviderを書くことでどのような仕組みでTerraformがプロビジョニングなどをやっているかをみていきました。
今後デバッグをする際にGoがかければProviderを読めることと、実装できることを知れたのがよかったです。
最後までお読みいただきありがとうございました。

Discussion