Published

- 4 min read

ลองใช้ GCP RuntimeConfig

img of ลองใช้ GCP RuntimeConfig

วันนี้จะมาลองใช้ GCP RuntimeConfig ซึ่งในปัจจุบันยังคงสถานะเป็น beta อยู่ ซึ่งน่าลองมา adopt กับ project ที่ต้องการความ flexible ในการอ่านค่า config แบบ runtime ซึ่งประโยชน์ของมันนั้นมีหลากหลายมากครับ เช่น เก็บ status การ build จาก CICD, ทำ runtime variable

บทความนี้จะ focus ไปที่ runtime variable โดยใช้ lib ที่ชื่อว่า gocloud นะครับ (https://gocloud.dev/howto/runtimevar/)
ซึ่งเจ้า gocloud นี้คือ Cloud Development Kit (CDK) ที่มาช่วยในการเขียน code ที่เชื่อมต่อกับ cloud เจ้าต่างๆ (AWS, GCP, AZURE) ได้อย่างง่ายดาย ซึ่งเรียกว่า vendor-neutral ​generic API ซึ่งความดีงามของมันคือมันเป็น generic API แค่เราเปลี่ยน connection URL ให้ตรงกับ provider นั้นๆ เช่น Blob (S3, GCS), PubSub (Kafka, RabbitMQ)

ตัวอย่าง PubSub implementation ของ RabbitMQ และ Kafka

   import (
	"context"

	"gocloud.dev/pubsub"
	_ "gocloud.dev/pubsub/rabbitpubsub"
)

// pubsub.OpenTopic creates a *pubsub.Topic from a URL.
// This URL will Dial the RabbitMQ server at the URL in the environment
// variable RABBIT_SERVER_URL and open the exchange "myexchange".
topic, err := pubsub.OpenTopic(ctx, "rabbit://myexchange")
if err != nil {
	return err
}
defer topic.Shutdown(ctx)
   import (
	"context"

	"gocloud.dev/pubsub"
	_ "gocloud.dev/pubsub/kafkapubsub"
)

// pubsub.OpenTopic creates a *pubsub.Topic from a URL.
// The host + path are the topic name to send to.
// The set of brokers must be in an environment variable KAFKA_BROKERS.
topic, err := pubsub.OpenTopic(ctx, "kafka://my-topic")
if err != nil {
	return err
}
defer topic.Shutdown(ctx)

กลับมาเรื่อง runtimevar ซึ่งเจ้า gocloud มี interface ในการเรียกใช้งานในการ watch ค่า config ที่อยู่บน GCP RuntimeConfig หรือเราอาจดึงค่าล่าสุดของ config มาได้เลย แต่ควรไปอ่าน Limitation และ Quota ของ provider นั้นๆ ด้วยนะครับ ซึ่งในที่นี่ GCP RuntimeConfig จะมีอยู่ตาม link นี้เลย https://cloud.google.com/deployment-manager/quotas#cloud-runtime-configuration-api แต่เท่าไปดู code บน GitHub มา เจ้า gocloud นั้น open connection โดยใช้เป็นการ watch config ที่เราส่ง Connection URL เข้าไป ผมเลยคิดว่าเราไม่ต้องกังวลเรื่อง quota มากนักครับ จะเห็นได้ตามรูปด้านล่าง

ต่อไปก็จะเป็น code ที่จะแสดงการทำงานในการ watch ค่า config จาก GCP RuntimeConfig แบบ runtime ครับ

   package main

import (
	"context"
	"fmt"
	"os"
	"sync"
	"time"

	"gocloud.dev/runtimevar"
	_ "gocloud.dev/runtimevar/gcpruntimeconfig"
)

var (
	prj = os.Getenv("PROJECT")
	cfg = os.Getenv("CONFIG")
	vrb = os.Getenv("VARIABLE") // TODO: make this variable to point to the key of yml file on runtimeconfig or make a list of variables instead
)

func main() {
	fmt.Println("starting program...")

	rc := &RuntimeConfig{
		config: make(map[string]string, 1),
	}

	rc.watch()
	defer rc.variable.Close()
	periodicallyPrint(rc)

	var st time.Duration = 120 * time.Second
	fmt.Printf("ending program in %v...\n", st)
	time.Sleep(st)

	os.Exit(0)
}

// periodicallyPrint periodically prints the config value.
func periodicallyPrint(rc *RuntimeConfig) {
	ticker := time.NewTicker(1 * time.Second)
	go func() {
		for range ticker.C {
			fmt.Printf("(goroutine)config key: %+v, value: %+v\n", vrb, rc.read(vrb))
		}
	}()
}

type RuntimeConfig struct {
	variable *runtimevar.Variable
	config   map[string]string
	rw       sync.RWMutex
}

// initVariable initializes the variable in the RuntimeConfig.
func (rc *RuntimeConfig) initVariable(ctx context.Context) error {
	v, err := runtimevar.OpenVariable(ctx, "gcpruntimeconfig://projects/"+prj+"/configs/"+cfg+"/variables/"+vrb+"?decoder=string")
	if err != nil {
		return err
	}

	rc.variable = v
	return nil
}

// write writes the value for the given key in the RuntimeConfig.
func (rc *RuntimeConfig) write(key string, value string) {
	rc.rw.Lock()
	rc.config[key] = value
	rc.rw.Unlock()
}

// read returns the value associated with the given key in the RuntimeConfig.
func (rc *RuntimeConfig) read(key string) string {
	rc.rw.RLock()
	defer rc.rw.RUnlock()
	return rc.config[key]
}

// watch Call Watch in a loop from a background goroutine to see all changes,
// including errors.
//
// You can use this for logging, or to trigger behaviors when the
// config changes.
//
// Note that Latest always returns the latest "good" config, so seeing
// an error from Watch doesn't mean that Latest will return one.
//
// Maybe add time.Sleep(3 * time.Second) after finished this function to make sure config is
// loaded or call this function after finished executing variable.Latest(ctx) and store the value in a variable.
func (rc *RuntimeConfig) watch() {

	// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.
	// The URL Host+Path are used as the GCP Runtime Configurator Variable key;
	// see https://cloud.google.com/deployment-manager/runtime-configurator/
	// for more details.

	if rc.variable == nil {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		err := rc.initVariable(ctx)
		if err != nil {
			fmt.Printf("Error initializing variable: %v", err)
			return
		}
	}

	go func() {
		for {
			snapshot, err := rc.variable.Watch(context.Background())
			if err == runtimevar.ErrClosed {
				// variable has been closed; exit.
				return
			}
			if err == nil {
				// Casting to a string here because we used StringDecoder.
				rc.write(vrb, snapshot.Value.(string))
			} else {
				fmt.Printf("Error loading config: %v", err)
				// Even though there's been an error loading the config,
				// variable.Latest will continue to return the latest "good" value.
			}
		}
	}()
}

ซึ่งหลักๆ ที่เน้นไป focus คือ function watch() ครับ ซึ่งผมได้ทำการ watch config แล้วถ้าหากมีการ update config ขึ้นมาก็จะมีการเรียก function write เพื่อที่จะไป update runtime variable ซึ่งผมเก็บไว้ในรูปแบบ map[string]string โดยมี sync.RWMutex เป็นตัวช่วยแก้ปัญหา race condition ครับ ซึ่งที่เลือกใช้ sync.RWMutex แทน sync.Mutex เพราะว่า rate ในการ write ซึ่งเราตั้งค่าเป็น period (ค่อนข้างมี gap time ในการ write) และจะเน้นไปด้านการ read data ซะส่วนใหญ่

ตัวอย่างการทำงานของ code

แวะไปชม source code ได้ตามนี้ครับ https://github.com/3ackdoor/gcp-runtimeconfig

References

https://gocloud.dev/
https://cloud.google.com/deployment-manager/runtime-configurator\