トランザクション処理など複数の並行処理で値や処理が失敗したことを共有するために必要
並行処理の数が少ない場合は、チャネルを利用して自分で実装することもできるが、並行処理で行うごルーチンの数が多い場合、コンテキストを使うと簡単に共有できる。
type Context interface {
Deadline() (deadline time.Time, ok bool) // コンテキストが自動でキャンセルされる時刻と時刻を設定しているかどうかのブール値
Done() <-chan struct{} // キャンセルされているかどうか
Err() error // キャンセルされた理由
Value(key interface{}) interface{}
}
ctx
にすべきリソース解放
キャンセル処理付きのコンテキストを生成した場合、リソース解放をすべき。
→ defer cancel()
(全ての処理が正常終了すると、キャンセル処理が呼ばれないため)
並行処理でのエラーハンドリング
コンテキストを使った並行処理でエラーハンドリングする際に、実装が難しくなる。
→ errgroup
パッケージのWithContext
関数を使うと簡易化
WithContext
関数で生成されたゴルーチンのうち、一つでもエラーが発生した場合にキャンセルが実行されてcontext.Done
のチャネルを受け取れる。
ctx, cancel := context.WithTimeout(context.Background(), downloadTimeout)
defer cancel()
eg, ctx := errgroup.WithContext(ctx)
for _, size := range times {
eg.Go(func() error {
// do something
})
}
子コンテキストから親コンテキストを辿って値を探す
同じキーで保存した場合、一番近いコンテキストの値になる
キーの型 Tips
キーは、「新しく名前をつけた型を宣言すると、呼び出すパッケージで同じ規定型かつ同じ値を持っていてもイコールにならない」ことを利用すると、他のパッケージによって値が勝手に更新されることを防ぐことができます。
package external
type requestIDKey struct{}
func GetRequestID(ctx context.Context) (int, bool) {
r, ok := ctx.Value(requestIDKet{}).(int)
if ok {
return r, true
}
return 0, false
}
func WithRequestID(ctx context.Context, reqID int) context.Context {
return context.WithValue(ctx, requestIDKey{}, reqID)
}
メモリの読み書きを行う場所は、スタックとヒープに分かれる。
ガベージコレクタ(GC)を用いてヒープのメモリを管理し、しかるべきタイミングで解放しているが、Go では GC 時に STW(Stop The World)が発生するものを採用しているので、GC が動いている間はプログラムの実行が止まってしまう。
そのため、パフォーマンスの観点でスタックかヒープにメモリを確保するかは重要。
go build -gcflags "-m"
でビルドすると見ることができ、スタックとヒープのどちらに確保されるかがわかる。特別な理由がない場合はレシーバ
はポインタとして定義したほうが良い。
他の場所から値を書きかえらないように(イミュータブル)したい場合、実体で定義する。
type Time struct {
wall uint64
ext int64
loc *Location
}
func (t Time) Unix() int64 {
return t.unixSec()
}
ポインタとは、型とともに定義することでメモリ上のアドレス
とそのアドレス位置からのどの範囲
を読み取ればいいかわかるもの。
もし同じアドレス位置を指していたとしても別の型で定義された場合、扱う範囲が異なるため不正な値を読み取ってしまう。
unsafe.Pointer
とはあるポインタ型から任意のポインタ型にキャストを可能にする仕組み。
unsafe.Pointer
を活用した例として、string 型から[]byte 型へのキャストがある。string と[]byte はキャストによって変換できるが、変換の際に文字列データのコピーが発生する。
unsafe.Pointer
を使うとコピーせず、キャストができる。文字列とスライスの構造体がData
とLen
フィールドまで一致しているためこのテクニックが使える。
s := "hello"
b := *(*[]byte)(unsafe.Pointer(&s))
type String struct {
Data uintptr
Len int
}
type Slice struct {
Data uintptr
Len int
Cap int
}
type MyError {
Op string
Table string
}
func (e *MyError) Error() string {
return fmt.Sprintf("myerror: %s %s", e.Op, e.Table)
}
再利用
するものやerrors.Is()
などでエラー判別したい場合は、エラーをパッケージ単位で変数として持つと良いpackage sql
var ErrNoRows = errors.New("sql: no rows in result set")
単にバケツリレーで発生したエラーを返していくだけでは、「どのような処理でそのエラーが発生したのか」という文脈が失われてしまう。
→ ラッピングすることで「どこ」で行われたかの情報を付随させることができる
Unwrap メソッドを実装した error の値を返す。
return fmt.Errorf("handle signup request* %w", err)
ラッピングされたエラーから、エラー型固有の情報を利用したい場合はerrors.As
を、単にエラー同士が等しいかどうかを確認したい場合はerrors.Is
関数を使うと良い。
var me *MyError
if errors.As(err, &me) {
fmt.Printf("op: %s, table: %s", me.Op, me.Table)
}
errors.Unwrap
を呼び出し、取り出したエラーに対してまた 1 から検証するif errors.Is(err, sql.ErrNoRows) {
// do something
}
golang.org/x/sync/errgroup
パッケージは「最初に発生したエラー」のみ取得できるが、並行している各処理が返すエラーを全てハンドリングしたい場合は、github.com/hashicorp/go-multierror
パッケージを用いると良い。
var group multierror.Group
for _, name := range names {
group.Go(func() error {
// do something
})
}
if err := group.Wait(); err != nil {
for _, e := range err.Errors {
// do something
}
}
req, err := http.NewRequest(http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
データの競合を回避するためにsync.Mutex
を使うことができるが、もっとシンプルに書く方法がある。
var cnt uint32
var mu sync.RWMutex
for i := 0; i < times; i++ {
go func() {
mu.Lock()
cnt++
mu.Unlock()
}()
}
sync/atomic
パッケージのatomic.AddInt64()
などvar cnt uint32
for i := 0; i < times; i++ {
go func() {
atomic.AddInt32(&cnt, 1)
}()
}
sync
パッケージのsync.Map{}
var m sync.Map
go func() {
m.Store("cnt", 1)
}
m.Load("cnt")
time.Ticker
はインターバルで処理したい時に便利
t := time.NewTicker(150 * time.Millisecond)
for {
select {
case <-t.C:
fmt.Println(time.Now())
}
}
gRPC はコンテンツの送受信するために Protocol Buffers のようなシリアライザを使用し、バイナリベースのプロトコルである HTTP/2 で通信するため、curl などの CLI ツールでリクエストを送信し、レスポンスの内容を目で確認することができない。
→ 簡単にリクエストを構築し、送受信ができる gRPC クライアントEvans
通常、gRPC の API 定義ファイルを protoc で読み込むことになるが、protoc がユーザ環境にインストールされていなければならない。
→ Protocol Buffers パーサの github.com/jhump/protoreflect を使用することで protoc ヘの依存を取り除いている
gRPC Server Reflection Protocol
→ サーバがどのような RPC を受け付けているかを知るための RPC を Protocol Buffers を用いて定義している。
srv := grpc.NewServer()
// do something
reflection.Register(srv)
API 定義ファイルがない場合、自動生成された型が手に入らないため、動的にメッセージを構築する必要がある。
→ github.com/jhump/protoreflect のdynamic
パッケージのdynamic.Message
を使用。
Marshal すると、内部で型アサーションを行い、proto.Marshaler
を実装している場合、Marshal メソッドを呼び出している。
文字列の出力がシンプルである場合、単純な文字列比較によりテストしますが、コマンドヘルプの内容のテストや、複数行にわたる出力など、複雑な出力が行われる場合はしばしばゴールデンファイルテストパターンを使う。
期待する出力が書き込まれたファイルをゴールデンファイルと呼び、テスト実行時に-update
フラグを指定されている場合、ゴールデンファイルをテストによって得られた出力で上書きする。
期待通りの出力であればバージョン管理システムにコミットする。
マルチコンパイル、Github Releases や Homebrew での配布までを1コマンドで実行
→ GoReleaser
builds:
-
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm
- '386'
changelog:
sort: asc
brews:
- tap:
owner: shellingford330
name: homebrew-test
url_template: "http://github.com/shellingford330/test..."
install: |
bin.install "evans"
awesome-go
にプルリクエストを送ってみるインフラも IaS によってソフトウェア開発のプラクティスの恩恵を受けることができるようになった。
その中でもテストは恩恵があまり得られない。テストには大きく以下のケースがある。
この中で2つ目の YAML などの任意のフィールドの値が正しいかどうか(k8s の場合、レプリカ数が正しいか)についてのテストが難しい。
このようにインフラの設定・状態をコードで示すことを、(Hashicorp では)Policy as Code と呼ぶ。
PaC を実現するsteinがある。
stein の大まかな仕組みは、JSON や YAML といった設定ファイルのルールを HCL という言語で記述し、Go で書かれた stein アプリケーションが設定ファイルがルールに準拠しているかチェックするという流れになっている。
rule "replicas" {
conditions = [
"${jsonpath("spec.replicas") > 3}"
]
}
PaC はユースケースによって異なるルールを設定できる必要があるため、3つ目の「式の評価」ができる柔軟性の高い特徴が最適。
HCL を Go の構造体に落とし込むには、github.com/hashicorp/hcl2
を使う。
HCL では中間表現を持つことで、デコード前に色々な処理を挟めるようになっている。中間表現に以下のようなコンテキスト情報を介してデコードすることで、スキーマ(独自 DSL の構文)側で定義済みの変数、もしくは関数として展開することができる。
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"name": cty.StringVal("babarot"),
},
Functions: map[string]function.Function{
"upper": stdlib.UpperFunc,
}
}
message = "HELLO, ${upper(name)}!"
# ==> "HELLO, BABAROT!"
ルールファイル(ポリシー)は独自 DSL(HCL)で記述されていて、以下の手順で Go にパースされる。
var policySchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "rule",
LabelName: []string{"name"}
},
}
}
Bazel は Google が開発したオープンソースのビルドツールである。
Bazel のメリット
WORKSPACE
というファイルを配置WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
<!-- ビルドに必要なルールをダウンロード -->
http_archive(
name = "io_bazel_rules_go",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.20.1/rules_go-v0.20.1.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.20.1/rules_go-v0.20.1.tar.gz",
],
sha256 = "842ec0e6b4fbfdd3de6150b61af92901eeb73681fd4d185746644c338f51d4c0",
)
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
go_rules_dependencies()
go_register_toolchains()
<!-- BUILD.bazelを自動生成するためのルールをダウンロード -->
http_archive(
name = "bazel_gazelle",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.0/bazel-gazelle-v0.19.0.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.0/bazel-gazelle-v0.19.0.tar.gz",
],
sha256 = "41bff2a0b32b02f20c227d234aa25ef3783998e5453f7eade929704dcff7cd4b",
)
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load 文は extension を読み込むための記述。http_archive
という命令は他のリポジトリをアーカイブファイルとしてダウンロードし、それを解凍してビルドルールで使えるようにするための命令。rules_go
は Go で書かれたソフトウェアのビルドのために必要なルール群をまとめたリポジトリ。
go_register_toolchains
はビルドで用いる Go のツールチェインを登録するための命令。
go_rules_dependencies
は Go のビルドルール自体が必要としている外部依存を登録するためのルール。
BUILD.bazel
各パッケージに BUILD ファイルを自動生成するためにgithub.com/bazelbuild/bazel-gazelle
が提供されている。
gazelle_dependencies
を用いて gazelle 自身が必要としている外部依存を登録。
プロジェクトのルートディレクトリに配置した BUILD.bazel ファイルに以下のように記述する。
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:prefix github.com/example/project
gazelle(name = "gazelle")
その後、コマンドを実行して BUILD ファイルを自動生成する。その際、Bazel に依存モジュール(go.mod)の内容を認識させる必要がある。
$ bazel run //:gazelle -- update-repos -from_file=go.mod
これにより、WORKSPACE ファイルにgo_repository
という Go の依存モジュールを登録するための命令が記述される。
また、同時に main.go と同じディレクトリに存在する BUILD.bazel に以下の記述が追記される。
go_binary(
name = "...",
embed = [..."],
visibility = ["..."].
)
go_binary がバイナリを生成するためのルールで、これをもとに実行可能なバイナリが bazel-bin ディレクトリ配下に生成される。
$ bazel build //:wrench
## ビットコインメッセージの変換関数の生成
type packageInfo struct {
name string
files []*ast.File
}
func makePackageInfo(path string) (*packageInfo, error) {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedSyntax,
Tests: false,
}
packageList, err := packages.Load(cfg, path)
p := packageList[0]
return &packageInfo{name: p.Name, files: p.Syntax}, nil
}
golang.org/x/tools/go/packages
にあるpackage.Load
関数を使うと、パッケージ全体の AST が取得できる。
packages.Load
関数の引数のpackages.Config
は
type typeInspector struct {
typeName string
typeSpec *ast.TypeSpec
}
func (p *packageInfo) findTypeSpec(typeName string) (*ast.TypeSpec, error) {
ti := &typeInspector{typeName: typeName}
for _, file := p.files {
if file == nil {
continue
}
ast.Inspect(file, ti.Inspect)
if ti.typeSpec != nil {
return ti.typeSpec, nil
}
}
return nil, errors.New("not found type")
}
構造体の情報は TypeSpec という type 宣言の情報を保持している AST ノードにある。。目的の AST を見つけるために見つけるためにast.Instpect
という関数を使用。ast.Inspect 関数の引数としてルートの AST ノードと AST を調べる関数を渡すと、再起的にノードを巡回する。
func (t *typeInspector) inspect(node ast.Node) bool {
decl, ok := node.(*ast.GenDecl)
if !ok || decl.Tok != token.TYPE {
return true
}
for _, spec := range decl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
if typeSpec.Name().String() != t.typeName {
continue
}
t.typeSpec = typeSpec
return false
}
return true
}
抽象構文木のノードを表す ast.Node を引数に渡す。構造体宣言を抽出したいので、*ast.GenDecl
(generic declaration node)かどうかをキャストで判定し、Tok が TYPE である宣言ノードを抽出。
GenDecl で TYPE であれば、Specs に Spec と言われる単一のノード情報のスライスを保持している。
func makeStructureInfo(ts *ast.TypeSpec, typeName string) (*StructureInfo, error) {
structType, ok := interface{}(ts.Type).(*ast.StructType)
for _, fi := range structType.Fields.List {
fi, err := makeFiledInfo(fi)
// フィールド情報を格納
}
}
func makeFiledInfo(field *ast.Field) (*filedInfo, error) {
typeName, err := exprToTypeName(filed.Type) // 型を文字列に変換する関数
fieldName := filed.Names[0].String()
tag := filed.Tag
if _, isSlice := filed.Type.(*ast.ArrayType); isSlice {
// doSomething
}
}
TypeSpec が構造体ではない場合(type Hoge int
など)は除外する。Fields.List
でフィールド情報を取得。
外部サービスの SDK を利用している場合、SDK の中にはあらかじめそのサービスに適したリトライを備えていることが多い。自前のリトライ処理と SDK のリトライ処理を重ねるとリトライする回数が定数倍に増える。
冪等性に対応している必要がある。
リトライに成功しないと無限にリトライされて処理資源を占有し続けるため、リトライする上限数を決めるべき
functional-optionsという実装パターン。
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
Exponetial backoff:乗数倍で待機時間を増やしていく
例:50ms, 100ms, 200ms, 400ms, ...
ただ、この方法には問題がある。同時に多くのリクエストが失敗した際、同じ戦略で待機時間が計算されるので同じタイミングにリトライされて、一気にリクエスト先に負荷をかけてしまう。
間隔の算出に jilter(タイミングのずれ)を取り入れて待機時間に揺らぎを持たせることで、リクエストが集中することを緩和する。
パッケージ/ソフトウェアを提供するときに、ユーザの要望によって新しく機能をつけたいときにパッケージ本体に機能を追加するとどんどん肥大化したり、柔軟性がないという問題がある。ユーザが自由にカスタマイズできるように動的にプラグインを入れられると便利。
例)認証系のパッケージでインフラ層が MySQL にしか対応していなく、PostgreSQL や AWS RDS に対応したいとき、本体のパッケージでは動作する部分が書いてあるだけなのでユーザが実装したプラグインに差し替えることができるため、柔軟性が高い。
ソフトウェア本体とプラグインをまとめて1つのバイナリにビルドする方法
動的ライブラリとしてプラグインを作成する方法(実行時に動的にロード)
ソフトウェア本体とプラグインとが別のプロセスとして起動し、お互いに通信することで動作する方法
Go 標準のplugin
パッケージを使用する。サンプルコードはこちら。
package main
import "fmt"
func Greet() {
fmt.Println("Hello!")
}
$ go build -buildmode=plugin -o en.go ./plugin/en
package main
func main() {
if err := greet(os.Args[1]); err != nil {
panic(err)
}
}
func greet(lang string) error {
p, err := plugin.Open(lang + ".so")
if err != nil {
return err
}
v, err := p.Lookup("Greet")
if err != nil {
return err
}
f, ok := v.(func())
if !ok {
return errors.New("Greet must be a function type")
}
f()
return nil
}
Lookup メソッドは、プラグインでエクスポートされている変数と関数を名前で取得できる。
$ go build -o greet greet.go
$ ./greet en
Hello!
ソフトウェア本体とプラグインが別のプロセスで動作してお互いに通信するタイプのもの。
github.com/hashicorp/go-plugin
というツールがよく使われる。
サンプルコードはこちら
サンプルコードはこちら
作成する Action の定義ファイル。
name: "Print tilte"
description: "Print tilte with the given title"
author: "Hirofumi Suzuki"
inputs:
title:
description: "A title."
required: true
outputs:
number:
description: "Given title"
runs:
using: "docker"
image: "Dockerfile"
branding:
icon: 'calendar'
color: 'orange'
入出力
入力値は環境変数のINPUT_VARIABLE_NAME
の形式で渡される。
例)「INPUT_TITLE」という環境変数にセットされる。
出力値は標準出力に::set-output name=title::value
の形式で出力することで、他のステップから${{ steps.id.outputs.title }}
から呼び出せる。
func main() {
title := os.Getenv("INPUT_TITLE")
fmt.Fprintf(os.Stdout, "::set-output name=title::%s\n", title)
}
モックサーバ
標準ライブラリのhttptest.NewServer
関数を使うと、任意の http.Handler インターフェースの ServeHTTP メソッドが呼び出される HTTP サーバを簡単に作成することができる。
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do something
}))
defer ts.Close()
fmt.Println(ts.URL) // モックサーバのURL
bytes.Buffer
テストなどでio.Writer
インターフェースへの出力を確認したい場合、bytes.Buffer
が便利。
var outStream bytes.Buffer
README.md ファイルを公開する前に作成。
[Draft a release]
というボタンから action を公開する。
Envoy は C++で実装されたネットワークプロキシであり、「設定の大部分を API 経由で動的に変更できる」という特徴をもっている。この API を xDS API という。
Envoy はプロキシとしての設定を以下のような単位に分割する。
xDS API は x Discovery Service の略であり、Cluster や Listener などの設定を、プロキシに対して動的に配信するための API である。
xDS API で設定を変更するサーバーを Control Plane という。
xDS API は Protocol Buffers を用いて gRPC のサービスとして定義されている。例えば、CLuster の設定を配布するためのサービスである Cluster Discovery Service(CDS)が定義されている。他に、Listener Discovery Service(LDS)、Route Discovery Service(RDS)と言った gRPC のサービスも定義されており、Cluster や Listener などの各設定を1つの gRPC サービスで配信するための Aggregated Discovery Service(ADS)というサービスも存在する。
go-controle-plane は xDS API の Go 実装であり、Go を用いて Controle Plane を開発するためのライブラリである。
import (
cache "github.com/envoyproxy/go-controle-plane/pkg/cache/v2"
server "github.com/envoyproxy/go-controle-plane/pkg/server/v2"
"google.golang.org/grpc"
)
snapshotCache := cache.NewSnapshotCache(true, cache.IDHash{}, logger)
server := server.NewServer(context.Background(), snapshotCache, &callbacks{})
grpcServer := grpc.NewServer()
lis, err := net.Listen("tcp", ":8081")
discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, server)
err := grpcServer.Serve(lis)
NewSnapshotCache 関数ではキャッシュ用の変数を作成し、1番目の引数では ADS を用いるかどうか、2番目の引数(cache.IDHash{}
)は Controle Plane に接続している個々のプロキシの ID を識別するための変数。
サーバの起動後は、キャッシュを更新することで gRPC サーバから各プロキシに設定を配布できる。
clusters := []*api.Cluster{
{
Name: "cluster-1",
// 略
}
}
clusterResources := make([]types.Resource, len(clusters))
for i, cluster := clusters {
clusterResources[i] = types.Resource(cluster)
}
// 略
snapshot := cache.NewSnapshot("version", endpointResources, clusterResources, routeResources, listenerResources, runtimeResources)
err := snapshotCache.SetSnapshot("node-id", snapshot)
SetSnapshot メソッドでキャッシュの更新が行われると、内部的には gRPC サーバからプロキシへの設定の配布が発火される実装になっている。1番目の引数の ID で特定されるプロキシ(Node)に対して設定を配布する。
go-controle-plane は「xDS API サーバとしての最小限の実装」に徹しており、「どのようにして Cluster や Listener などの設定を作成するかどうか」という点については個々の Control Plane の実装者に委ねている。例えば、Custom Resource で xDS API で定義されている任意の設定を記述し、任意のプロキシに対してこの設定を配布することが可能。
kind(Kubernetes in Docker)は Docker を用いてローカル環境に Kubernetes クラスタを構築するためのツールである。
クラスターをローカル環境に作成
$ kind create cluster --name <cluster name> --image kindest/node:v1.21.1
Skaffold は Kubernetes をターゲットとして開発された、ビルドやデプロイを支援するためのコマンドラインツール。Skaffold dev コマンドは自動デプロイ機能でソースコードの変更を検知して設定ファイルに従ってアプリケーションをビルドし、クラスタにデプロイするもの。
$ skaffold dev --filename=./dev/skaffold/skaffold.yaml
apiVersion: skaffold/v2beta11
kind: Config
build:
artifacts:
- image: boots
docker:
dockerfile: ./Dockerfile
deploy:
kubectl:
manifests:
- ./dev/skaffold/pod.yaml
デプロイ対象の Kubernetes クラスタが kind で作成されたものだった場合、kind の load docker-image コマンドを用いてクラスタに Docker コンテナのイメージをロードする。
apiVersion: v1
kind: Pod
metadata:
name: bootes
namespace: bootes
spec:
containers:
- name: bootes
image: bootes
Terraform は大きく次の二つのコンポーネントによって構成されている。
で構成されている。
Terraform Plugin はさらに二つの構成要素がある。
Terrfaorm Provider はディストリビューション方法で3つに分けられる。
Provider の開発をサポートするTerraform Plugin SDKが HashiCorp 社から提供されている。
Provider とは別に SDK を開発することを推奨。
Provider ブロックのスキーマ(設定すべき項目)を実装する必要がある。その各情報をもとに外部サービスへアクセスをする。
package miro
import (
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func Provider() *schema.Provider {
return &schema.provider{
Schema: map[string]*schema.Schema{
"access_token": {
Type: schema.TypeString,
Description: "Access key for Miro API",
Required: true,
Sensitive: true,
},
},
ResourceMap: map[string]*schema.Resource{},
DataSourceMap: map[string]*schema.Resource{},
}
}
Required を true に設定したフィールドが指定されていなければ、terraform plan コマンド or terraform apply コマンドなどの実行は失敗する。
Resource とは1つの外部リソースの単位(Google Compute Engine インスタンスや GitHub の Team)に対応していて、Data Source とは外部リソースを Resource のように Terraform で扱う読み取り専用のもの。
Provider の ConfigureContextFunc で Provider の初期化のための関数を指定できる。
import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
)
func Provider() *schema.Provider {
return &schema.Provider{
ConfigureContextFunc: providerConfigureFunc,
}
}
func providerConfigureFunc(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) {
key := data.Get("access_token").(string)
var diags diag.Diagnostics
return miro.NewClient(key), diags
}
package main
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() *schema.Provider {
return miro.Provider()
},
})
}
Terraform は、デフォルトで読み込むディレクトリ(.terraform/providers/
)に使用する Provider のバイナリがなければ、Terraform Registry からインストールしようとする。
terraform {
required_providers {
miro = {
source = "Miro-Ecosystem/miro"
}
}
}
provider "miro" {
access_token = var.access_token
}
$ terraform init
* miro-ecosystem/miro: version = "~> 99.0.0"
Terraform has been successfully initialized!
.tf ファイルの Resource ブロックのスキーマを定義する。
func resourceBoard() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Description: "Name of the Board",
Type: schema.TypeString,
Required: true,
},
},
},
}
このスキーマを使って Miro の Board API の CRUD を行う次の Context Functions を実装。
func resourceBoard() *schema.Resource {
return &schema.Resource{
CreateContext: resourceBoardCreate,
}
}
func resourceBoardCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*miro.Client)
var diags diag.Diagnostics
name := data.Get("name").(string)
req := &miro.CreateBoardRequest{
Name: name,
}
board, err := c.Boards.Create(ctx, req)
if err != nil {
return diag.FromErr(err)
}
data.SetId(board.Id)
return resourceBoardRead(ctx, data, meta)
}
引数の meta は実装した ConfigureContextFunc の戻り値である。
data.SetId
でこの Board を Terraform で一意に識別する ID を設定。
func resourceBoard() *schema.Resource {
return &schema.Resource{
ReadContext: resourcBoardRead,
}
}
func resourceBoardRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*miro.Client)
var diags diag.Diagnostics
board, err := c.Boards.Get(ctx, data.Id())
if err != nil {
return diag.FromErr(err)
}
if board == nil {
data.SetId("")
return diag
}
if err := data.Set("boards", board); err != nil {
return diag.FromErr(err)
}
diag.SetId(board.ID)
return diags
}
data.SetId("")
とすることで Terraform の管理下から外すことができる。
Resource のブロックの種類をキーにして、ResourceMap へ登録。
func Provider() *schema.Provider {
return &schema.Provider{
ResourceMap: map[string]*schema.Resource{
"miro_board": resourceBoard(),
}
}
}
resource "miro_board" "test" {
name = "Test Board"
}
Terraform Registry に GPG の公開鍵を登録し、対応する秘密鍵で Terraform Provider のバイナリを署名してから、アップロードする。