Warum sollten Sie Go CDK in Betracht ziehen, auch wenn Sie ein einzelner Cloud-Anbieter sind?

Was ist Go CDK?

Go CDK ist eine Abkürzung für The Go Cloud Development Kit, ein Projekt zur Verwaltung von Diensten mit fast denselben Funktionen, die von großen Cloud-Anbietern mit einer einheitlichen API (früher bekannt als Go Cloud) bereitgestellt werden.

Beispielsweise kann der Prozess zum Speichern / Abrufen eines Objekts in einem Cloud-Speicherdienst mithilfe von Go CDK [^ 1] wie folgt geschrieben werden.

[^ 1]: Die im Beispielcode in diesem Artikel verwendete gocloud.dev ist v0.20.0.

package main

import (
	"context"
	"fmt"
	"log"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	//sparen
	if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
		log.Fatal(err)
	}

	//Erhalten
	data, err := bucket.ReadAll(ctx, "sample.txt")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(data))
}
package main

import (
	"context"
	"fmt"
	"log"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/gcsblob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "gs://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	//sparen
	if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
		log.Fatal(err)
	}

	//Erhalten
	data, err := bucket.ReadAll(ctx, "sample.txt")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(data))
}
package main

import (
	"context"
	"fmt"
	"log"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/azureblob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "azblob://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	//sparen
	if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
		log.Fatal(err)
	}

	//Erhalten
	data, err := bucket.ReadAll(ctx, "sample.txt")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(data))
}

Die Codeunterschiede bei Verwendung verschiedener Cloud-Anbieter befinden sich im Importteil des Treibers und in blob.OpenBucket (). Nur das Schema der angegebenen URL. Es ist wunderbar!

Auf diese Weise erleichtert Go CDK die Implementierung von Multi-Cloud-Anwendungen und hoch Cloud-portablen Anwendungen.

Wenn Sie mehr über Go CDK erfahren möchten, lesen Sie bitte die offiziellen Informationen.

Es ist ein sehr praktisches Go-CDK, aber ab Oktober 2020 scheint der Projektstatus "API ist Alpha, aber produktionsbereit" zu sein [^ 2]. Bitte gehen Sie bei der Einführung auf eigenes Risiko.

Weitere Vorteile von Go CDK neben der Multi-Cloud-Portabilität

Dies ist das Hauptthema.

Ich bin sicher, es gibt Leute, die denken: "Ich benutze nur AWS! Bender Lock-In ist gut!" In diesem Artikel möchte ich die Vorteile der Verwendung von Go CDK auch für solche Personen am Beispiel von "S3-Objektoperationen (Speichern / Abrufen)" vorstellen.

Einfach zu verstehende und zu handhabende API

Die Go CDK-API ist intuitiv, leicht verständlich und einfach zu handhaben.

Das Lesen und Schreiben von Objekten in den Cloud-Speicher mit Go CDK ist unter blob.Bucket verfügbar. NewReader () und NewWriter () Blob.Reader ([[email protected]/blob#Bucket.NewWriter) erhalten von /[email protected]/blob#Bet) Implementiert io.Reader) und blob.Writer Verwenden Sie / blob # Writer) (Implementierung von io.Writer). Es ist sehr intuitiv, dass Sie ein Objekt mit blob.Reader ( io.Reader) abrufen (lesen) und mit io.Writer (io.Writer) speichern (schreiben) können. Auf diese Weise können Sie mit Objekten in der Cloud arbeiten, als würden Sie mit einer lokalen Datei arbeiten.

Lassen Sie uns anhand konkreter Beispiele einen Blick darauf werfen, wie es einfacher zu verstehen ist als die Verwendung des AWS SDK.

Beim Speichern eines Objekts in S3

Für das AWS SDK [Upload () in s3manager.Uploader ](Https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3/s3manager#Uploader.Upload) wird verwendet. Übergeben Sie den Inhalt des hochzuladenden Objekts an die Methode als "io.Reader". Beim Hochladen einer lokalen Datei ist es praktisch, os.File so wie es ist zu übergeben, aber das Problem ist, dass die Daten im Speicher in irgendeiner Form vorliegen. Wenn Sie mit codieren und speichern möchten, wie es ist.

Zum Beispiel ist der Prozess von JSON-codiert und S3 wie er ist im AWS SDK wie folgt.

package main

import (
	"encoding/json"
	"io"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String("ap-northeast-1"),
	})
	if err != nil {
		log.Fatal(err)
	}

	uploader := s3manager.NewUploader(sess)

	data := struct {
		Key1 string
		Key2 string
	}{
		Key1: "value1",
		Key2: "value2",
	}

	pr, pw := io.Pipe()

	go func() {
		err := json.NewEncoder(pw).Encode(data)
		pw.CloseWithError(err)
	}()

	in := &s3manager.UploadInput{
		Bucket: aws.String("bucket"),
		Key:    aws.String("sample.json"),
		Body:   pr,
	}
	if _, err := uploader.Upload(in); err != nil {
		log.Fatal(err)
	}
}

Io.Pipe () zum Verbinden von io.Writer zum Codieren von JSON und io.Reader zum Übergeben an s3manager.UploadInput #Pipe) muss verwendet werden.

Bei Go CDK erfolgt das Schreiben mit blob.Writer ( io.Writer), also json.NewEncoder () Gib es einfach an.

package main

import (
	"context"
	"encoding/json"
	"log"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	data := struct {
		Key1 string
		Key2 string
	}{
		Key1: "value1",
		Key2: "value2",
	}

	w, err := bucket.NewWriter(ctx, "sample.json", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()

	if err := json.NewEncoder(w).Encode(data); err != nil {
		log.Fatal(err)
	}
}

Natürlich können Sie beim Hochladen einer lokalen Datei einfach schreiben. Verwenden Sie einfach io.Copy, als würden Sie von Datei zu Datei kopieren.

package main

import (
	"context"
	"io"
	"log"
	"os"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	file, err := os.Open("sample.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	w, err := bucket.NewWriter(ctx, "sample.txt", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()

	if _, err := io.Copy(w, file); err != nil {
		log.Fatal(err)
	}
}

Übrigens wird der Writer von "s3blob" durch Umschließen von "s3manager.Uploader" implementiert, sodass Sie von der parallelen Upload-Funktion von "s3manager.Uploader" profitieren können.

Beim Abrufen eines Objekts aus S3

Betrachten Sie den Fall, JSON von S3 abzurufen und zu dekodieren.

Verwenden Sie für das AWS SDK s3.GetObject (). S3manager.Downloader, das mit s3manager.Uploader gekoppelt ist, ist das Ausgabeziel. Beachten Sie, dass es in diesem Fall nicht verwendet werden kann, da es io.WriterAt implementieren muss.

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

func main() {
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String("ap-northeast-1"),
	})
	if err != nil {
		log.Fatal(err)
	}

	svc := s3.New(sess)

	data := struct {
		Key1 string
		Key2 string
	}{}

	in := &s3.GetObjectInput{
		Bucket: aws.String("bucket"),
		Key:    aws.String("sample.json"),
	}
	out, err := svc.GetObject(in)
	if err != nil {
		log.Fatal(err)
	}
	defer out.Body.Close()

	if err := json.NewDecoder(out.Body).Decode(&data); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", data)
}

Für Go CDK schreiben Sie es einfach in die entgegengesetzte Richtung des Hochladens.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	r, err := bucket.NewReader(ctx, "sample.json", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	data := struct {
		Key1 string
		Key2 string
	}{}

	if err := json.NewDecoder(r).Decode(&data); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", data)
}

Verwenden Sie s3manager.Downloader, um das abgerufene Objekt in eine lokale Datei zu schreiben. tun können.

package main

import (
	"log"
	"os"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String("ap-northeast-1"),
	})
	if err != nil {
		log.Fatal(err)
	}

	downloader := s3manager.NewDownloader(sess)

	file, err := os.Create("sample.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	in := &s3.GetObjectInput{
		Bucket: aws.String("bucket"),
		Key:    aws.String("sample.txt"),
	}
	if _, err := downloader.Download(file, in); err != nil {
		log.Fatal(err)
	}
}

In Go CDK ist es in Ordnung, wenn Sie es entgegen der Zeit zum Hochladen schreiben.

package main

import (
	"context"
	"io"
	"log"
	"os"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	file, err := os.Create("sample.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	r, err := bucket.NewReader(ctx, "sample.txt", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	if _, err := io.Copy(file, r); err != nil {
		log.Fatal(err)
	}
}

Obwohl diese Methode einfach ist, weist sie einige Nachteile auf. Im Fall von "s3manager.Downloder" verfügt es nicht über "io.WriterAt" für das Ausgabeziel, sondern über eine parallele Downloadfunktion und eine hervorragende Leistung. Im Fall von Go CDK kann der parallele Download jedoch nicht so ausgeführt werden, wie er ist. Wenn Sie parallel zu Go CDK herunterladen möchten, müssen Sie es selbst mit NewRangeReader () implementieren. es gibt.

Beispiel für die Implementierung eines parallelen Downloads in Go CDK (falten, weil es lang ist)
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"sync"

	"gocloud.dev/blob"
	_ "gocloud.dev/blob/s3blob"
)

const (
	downloadPartSize    = 1024 * 1024 * 5
	downloadConcurrency = 5
)

func main() {
	ctx := context.Background()

	bucket, err := blob.OpenBucket(ctx, "s3://bucket")
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	file, err := os.Create("sample.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	d := &downloader{
		ctx:         ctx,
		bucket:      bucket,
		key:         "sample.txt",
		partSize:    downloadPartSize,
		concurrency: downloadConcurrency,
		w:           file,
	}

	if err := d.download(); err != nil {
		log.Fatal(err)
	}
}

type downloader struct {
	ctx context.Context

	bucket *blob.Bucket
	key    string
	opts   *blob.ReaderOptions

	partSize    int64
	concurrency int

	w io.WriterAt

	wg     sync.WaitGroup
	sizeMu sync.RWMutex
	errMu  sync.RWMutex

	pos        int64
	totalBytes int64
	err        error

	partBodyMaxRetries int
}

func (d *downloader) download() error {
	d.getChunk()
	if err := d.getErr(); err != nil {
		return err
	}

	total := d.getTotalBytes()

	ch := make(chan chunk, d.concurrency)

	for i := 0; i < d.concurrency; i++ {
		d.wg.Add(1)
		go d.downloadPart(ch)
	}

	for d.getErr() == nil {
		if d.pos >= total {
			break
		}

		ch <- chunk{w: d.w, start: d.pos, size: d.partSize}
		d.pos += d.partSize
	}

	close(ch)
	d.wg.Wait()

	return d.getErr()
}

func (d *downloader) downloadPart(ch chan chunk) {
	defer d.wg.Done()
	for {
		c, ok := <-ch
		if !ok {
			break
		}
		if d.getErr() != nil {
			continue
		}

		if err := d.downloadChunk(c); err != nil {
			d.setErr(err)
		}
	}
}

func (d *downloader) getChunk() {
	if d.getErr() != nil {
		return
	}

	c := chunk{w: d.w, start: d.pos, size: d.partSize}
	d.pos += d.partSize

	if err := d.downloadChunk(c); err != nil {
		d.setErr(err)
	}
}

func (d *downloader) downloadChunk(c chunk) error {
	var err error
	for retry := 0; retry <= d.partBodyMaxRetries; retry++ {
		err := d.tryDownloadChunk(c)
		if err == nil {
			break
		}

		bodyErr := &errReadingBody{}
		if !errors.As(err, &bodyErr) {
			return err
		}

		c.cur = 0
	}
	return err
}

func (d *downloader) tryDownloadChunk(c chunk) error {
	r, err := d.bucket.NewRangeReader(d.ctx, d.key, c.start, c.size, d.opts)
	if err != nil {
		return err
	}
	defer r.Close()

	if _, err := io.Copy(&c, r); err != nil {
		return err
	}

	d.setTotalBytes(r.Size())

	return nil
}

func (d *downloader) getErr() error {
	d.errMu.RLock()
	defer d.errMu.RUnlock()

	return d.err
}

func (d *downloader) setErr(err error) {
	d.errMu.Lock()
	defer d.errMu.Unlock()

	d.err = err
}

func (d *downloader) getTotalBytes() int64 {
	d.sizeMu.RLock()
	defer d.sizeMu.RUnlock()

	return d.totalBytes
}

func (d *downloader) setTotalBytes(size int64) {
	d.sizeMu.Lock()
	defer d.sizeMu.Unlock()

	d.totalBytes = size
}

type chunk struct {
	w     io.WriterAt
	start int64
	size  int64
	cur   int64
}

func (c *chunk) Write(p []byte) (int, error) {
	if c.cur >= c.size {
		return 0, io.EOF
	}

	n, err := c.w.WriteAt(p, c.start+c.cur)
	c.cur += int64(n)

	return n, err
}

type errReadingBody struct {
	err error
}

func (e *errReadingBody) Error() string {
	return fmt.Sprintf("failed to read part body: %v", e.err)
}

func (e *errReadingBody) Unwrap() error {
	return e.err
}
  • Siehe die Implementierung von s3manager.Downloader

Einfache lokale Ausführung

Go CDK wird entwickelt, um eine lokale Implementierung für alle Dienste bereitzustellen. Daher können Sie den Betrieb des Cloud-Dienstes problemlos durch die lokale Implementierung ersetzen. Auf einem lokalen Server für die Entwicklung ist es beispielsweise praktisch, alle Dienste durch lokale Implementierungen zu ersetzen, da sie ohne Zugriff auf AWS oder GCP betrieben werden können.

Für das Paket gocloud.dev / blob, das den Cloud-Speicher verwaltet, [ fileblob](https: // pkg. Es wird eine Implementierung bereitgestellt, die lokale Dateien liest und schreibt (go.dev/[email protected]/blob/fileblob).

Das Folgende ist ein Beispiel für das Umschalten des Ausgabeziels von codiertem JSON auf S3 und lokal, abhängig von der Option.

package main

import (
	"context"
	"encoding/json"
	"flag"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"gocloud.dev/blob"
	"gocloud.dev/blob/fileblob"
	"gocloud.dev/blob/s3blob"
)

func main() {
	var local bool
	flag.BoolVar(&local, "local", false, "output to a local file")
	flag.Parse()

	ctx := context.Background()

	bucket, err := openBucket(ctx, local)
	if err != nil {
		log.Fatal(err)
	}
	defer bucket.Close()

	data := struct {
		Key1 string
		Key2 string
	}{
		Key1: "value1",
		Key2: "value2",
	}

	w, err := bucket.NewWriter(ctx, "sample.json", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()

	if err := json.NewEncoder(w).Encode(data); err != nil {
		log.Fatal(err)
	}
}

func openBucket(ctx context.Context, local bool) (*blob.Bucket, error) {
	if local {
		return openLocalBucket(ctx)
	}

	return openS3Bucket(ctx)
}

func openLocalBucket(ctx context.Context) (*blob.Bucket, error) {
	return fileblob.OpenBucket("output", nil)
}

func openS3Bucket(ctx context.Context) (*blob.Bucket, error) {
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String("ap-northeast-1"),
	})
	if err != nil {
		return nil, err
	}

	return s3blob.OpenBucket(ctx, sess, "bucket", nil)
}

Wenn Sie es so ausführen, wie es ist, wird sample.json in S3 gespeichert. Wenn Sie es jedoch mit der Option -local ausführen, wird es in der lokalen output / sample.json gespeichert. Zu diesem Zeitpunkt wird die Eigenschaft des Objekts als "output / sample.json.attrs" gespeichert. Dadurch können die Eigenschaften des gespeicherten Objekts problemlos abgerufen werden.

Überwiegend verbesserte Testbarkeit

Code, der APIs von externen Diensten wie AWS aufruft, muss sich immer darum kümmern, wie eine testbare Implementierung erstellt werden kann, aber mit Go CDK müssen Sie sich darüber keine Gedanken machen. Normalerweise abstrahieren Sie einen externen Dienst als Schnittstelle zum Implementieren von Mock und ersetzen ihn beim Testen durch Mock. Go CDK hat jedoch jeden Dienst bereits ordnungsgemäß abstrahiert und seine lokale Implementierung bereitgestellt. Da es fertig ist, können Sie es einfach so verwenden, wie es ist.

Testen Sie beispielsweise eine Struktur, die eine Schnittstelle zum Hochladen von codiertem JSON in den Cloud-Speicher implementiert, z.

type JSONUploader interface {
	func Upload(ctx context.Context, key string, v interface{}) error

Im Fall des AWS SDK werden Schnittstellen für verschiedene Service-Clients bereitgestellt, sodass die Testbarkeit durch deren Verwendung gewährleistet ist. Für s3manager wird die Schnittstelle in einem Paket namens [ s3manageriface] bereitgestellt (https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3/s3manager/s3manageriface).

type jsonUploader struct {
	bucketName string
	uploader   s3manageriface.UploaderAPI
}

func (u *jsonUploader) Upload(ctx context.Context, key string, v interface{}) error {
	pr, pw := io.Pipe()

	go func() {
		err := json.NewEncoder(pw).Encode(v)
		pw.CloseWithError(err)
	}()

	in := &s3manager.UploadInput{
		Bucket: aws.String(u.bucketName),
		Key:    aws.String(key),
		Body:   pr,
	}
	if _, err := u.uploader.UploadWithContext(ctx, in); err != nil {
		return err
	}

	return nil
}

Mit einer solchen Implementierung können Sie testen, ohne tatsächlich auf S3 zuzugreifen, indem Sie ein geeignetes Modell in "jsonUploader.uploader" einfügen. Diese Scheinimplementierung wird jedoch nicht offiziell bereitgestellt, sodass Sie sie selbst implementieren oder ein geeignetes externes Paket finden müssen.

Im Fall von Go CDK wird es zu einer Struktur mit hoher Testbarkeit, indem es einfach so implementiert wird, wie es ist.

type jsonUploader struct {
	bucket *blob.Bucket
}

func (u *jsonUploader) Upload(ctx context.Context, key string, v interface{}) error {
	w, err := u.bucket.NewWriter(ctx, key, nil)
	if err != nil {
		return err
	}
	defer w.Close()

	if err := json.NewEncoder(w).Encode(v); err != nil {
		return err
	}

	return nil
}

Zum Testen ist es zweckmäßig, die speicherinterne "blob" -Implementierung mit dem Namen memblob zu verwenden.

func TestUpload(t *testing.T) {
	bucket := memblob.OpenBucket(nil)
	uploader := &jsonUploader{bucket: bucket}

	ctx := context.Background()

	key := "test.json"

	type data struct {
		Key1 string
		Key2 string
	}

	in := &data{
		Key1: "value1",
		Key2: "value2",
	}

	if err := uploader.Upload(ctx, key, in); err != nil {
		t.Fatal(err)
	}

	r, err := bucket.NewReader(ctx, key, nil)
	if err != nil {
		t.Fatal(err)
	}

	out := &data{}
	if err := json.NewDecoder(r).Decode(out); err != nil {
		t.Fatal(err)
	}

	if !reflect.DeepEqual(in, out) {
		t.Error("unmatch")
	}
}

Zusammenfassung

Mit der Einführung von Go CDK haben wir andere Vorteile als Multi-Cloud-Unterstützung und Cloud-Portabilität eingeführt. Aufgrund der Art und Weise, wie mehrere Cloud-Anbieter einheitlich behandelt werden, gibt es natürlich Schwachstellen, z. B. die Unfähigkeit, Funktionen zu verwenden, die für einen bestimmten Cloud-Anbieter spezifisch sind. Daher denke ich, dass das SDK jedes Cloud-Anbieters entsprechend den Anforderungen ordnungsgemäß verwendet wird.

Go CDK selbst entwickelt sich noch, daher hoffe ich, dass es in Zukunft mehr Funktionen haben wird.

Recommended Posts

Warum sollten Sie Go CDK in Betracht ziehen, auch wenn Sie ein einzelner Cloud-Anbieter sind?
Wenn Sie Word Cloud erstellen möchten.