tkak's tech blog

This is my technological memo.

TerraformのProviderを作った

Terraform v2.0から、オリジナルProviderを作るためのフレームワーク機能が追加されている。今回この機能を使ってオレオレProviderを作ったので、やったことをまとめておく。

Go API library

まず、TerraformのProviderを作る前に、自分が使いたいクラウドサービスのGo API libraryを探す。なければ自分で作る。

今回はGMO ConoHaクラウド用のAPI libraryを作った。ConoHaクラウドは今のところオブジェクトストレージ(OpenStack Swift)APIしか提供していなかったので、オブジェクトストレージを操作する機能だけのものになっている。

また、他のクラウドサービス向けのプラグインを作る時の参考用に、簡単なAPI client libraryの型みたいな物も作ってみた。

Terraform Provider

Client libraryが準備できたら、次に、Terraform Providerを作る。terraform-provider-xxxっていう名前のリポジトリを作って、main.goprovider.goconfig.goresource_xxx.goのファイルを用意する。Providerは基本的にこの4種類のファイルで構成されている。(詳細はGitHubを参照)

以下、ちょっとした解説。

main.go

package main
import (
    "github.com/hashicorp/terraform/plugin"
    "github.com/tkak/terraform-provider-conoha/conoha"
)
func main() {
    plugin.Serve(&plugin.ServeOpts{
        ProviderFunc: conoha.Provider,
    })
}

main.goファイルには、plugin.Serve関数を呼ぶだけの処理を書く。オリジナルProviderをプラグインとしてTerraformに組み込むためのもの。

provider.go

// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
    return &schema.Provider{
        Schema: map[string]*schema.Schema{
            "tenant": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
            },
            "user": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
            },
            "password": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
            },
        },
        ResourcesMap: map[string]*schema.Resource{
            "conoha_container": resourceConohaContainer(),
        },
        ConfigureFunc: providerConfigure,
    }
}

provider.goファイルには、helper/schemaライブラリを使って、Provider関数を定義する。ここでは、.tfファイル内で使う、provider用の変数を定義したり、conoha_containerなど、独自のresourceの宣言を行う。例えば、上のProvider関数の定義だとこんな.tfファイルになる。

provider "conoha" {
    user = "hoge"
    password = "hoge123"
    tenant = "hoge"
}

簡単なバリデーションであればhelper/schemaのTypeやRequiredなどを使ってシンプルに書くことができる。

config.go

type Config struct {
    Tenant   string `mapstructure:"tenant"`
    User     string `mapstructure:"user"`
    Password string `mapstructure:"password"`
    Token    string `mapstructure:"token"`
    Endpoint string `mapstructure:"endpoint"`
}

func (c *Config) Client() (*conoha.Client, error) {
...
}

config.goファイルでは、client libraryを使うためのConfig structを定義したり、Client関数を定義したりする。変数の詳細なバリデーションも必要あればここに書いたりする。

resource_conoha_container.go

func resourceConohaContainer() *schema.Resource {
    return &schema.Resource{
        Create: resourceConohaContainerCreate,
        Read:   resourceConohaContainerRead,
        Delete: resourceConohaContainerDelete,

        Schema: map[string]*schema.Schema{
            "name": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
                ForceNew: true,
            },
        },
    }
}

func resourceConohaContainerCreate(d *schema.ResourceData, meta interface{}) error {

    client := meta.(*conoha.Client)

    c := &conoha.Container{
        Name: d.Get("name").(string),
    }

    err := client.CreateContainer(c)
    if err != nil {
        return fmt.Errorf("Error creating container: %s", err)
    }

    d.Set("name", c.Name)
    d.SetId(c.Name)

    return nil
}

...

resource_xxx.goファイルには、resourceで扱う変数の定義やAPI周りのメイン処理を書く。上のresourceConohaContainer()だと、.tfファイルのresource定義はこのようになる。

resource "conoha_container" "example" {
    name = "fuga"
}

また、*schema.ResourceDataSet関数やSetId関数を使うことで、Terraform内で扱う任意の変数やリソースIDなどを設定することができる。

Usage

オリジナルProviderの動かし方の説明も書いておく。

Install

TerraformのProviderは、terraformのバイナリファイルに組み込まれているのではなく、Providerごと別なバイナリファイルになっている。インストールはgo buildしたあとterraform-provider-<provider名>という名前でTerraformの実行ファイルと同じディレクトリ配下($GOPATH/bin配下)に配置する。要は直接go installを叩くのと一緒。そうすると、Terraformコマンドを実行した時にProviderのバイナリファイルが読み込まれる。インストール手順はこんな感じ。

$ go get github.com/tkak/conoha
$ go get github.com/tkak/terraform-provider-conoha
$ go install github.com/tkak/terraform-provider-conoha

terraform apply

あとは、.tfを作って、実行するだけ。今回作ったProviderだと以下のような.tfファイルを作成し、terraform applyコマンドを実行する。

$ vi conoha.tf
---
variable "conoha_user" {}
variable "conoha_password" {}
variable "conoha_tenant" {}

provider "conoha" {
    user = "${var.conoha_user}"
    password = "${var.conoha_password}"
    tenant = "${var.conoha_tenant}"
}

resource "conoha_container" "example" {
    name = "hoge"
}
---
$ terraform plan \
-var "conoha_user=${CONOHA_USER}" \
-var "conoha_password=${CONOHA_PASSWORD}" \
-var "conoha_tenant=${CONOHA_TENANT}"

パスワードなどのパラメータはファイルとしてバージョン管理したくないのでCONOHA_*環境変数で設定して実行する。

ちなみに、環境変数TF_LOG=1を設定すると実行時にデバッグログをはくようにすることもできる。

まとめ

Goは最近勉強しはじめたばかりで、今回作ったライブラリやProviderはまだまだ改良する点があるが、ひとまず動く物を作ることができた。実際作るのにかかった時間はそんなにはなくて、APIの調査やTerraform のコードを読んだりした時間の方がかかった。Mitchell Hashimoto先生曰くGoogle CloudのProviderは8時間しかかからなかったらしい。慣れたらかなり簡単にオリジナルProviderが作れると思う。

プラグインごとの細かい作り込みは必要だけど、いろんなクラウドサービスのProviderを用意すれば、それぞれを共通のファイル形式で一元管理できる。 そんなツールは今までありそうでなかったし、Infrastructure as Codeの痒いところに手が届くツールとして、Terraformは中々優れているものなんじゃないかなと思う。

References