Pourquoi devriez-vous envisager d'adopter Go CDK même si vous êtes un fournisseur de cloud unique

Qu'est-ce que Go CDK

Go CDK est une abréviation de The Go Cloud Development Kit, qui est un projet visant à gérer des services avec presque les mêmes fonctions fournies par les principaux fournisseurs de cloud avec une API unifiée (anciennement connue sous le nom de Go Cloud).

Par exemple, le processus d'enregistrement / de récupération d'un objet dans un service de stockage cloud peut être écrit comme suit en utilisant Go CDK [^ 1].

[^ 1]: Le gocloud.dev utilisé dans l'exemple de code de cet article est 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()

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

	//Avoir
	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()

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

	//Avoir
	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()

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

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

	fmt.Println(string(data))
}

Les différences de code lors de l'utilisation de différents fournisseurs de cloud se trouvent dans la partie importation du pilote et blob.OpenBucket (). Seul le schéma de l'URL donnée. C'est merveilleux!

De cette manière, Go CDK facilite la mise en œuvre d'applications multi-cloud et d'applications hautement portables dans le cloud.

Si vous souhaitez en savoir plus sur Go CDK, veuillez vous référer aux informations officielles.

C'est un Go CDK très pratique, mais en octobre 2020, le statut du projet semble être "L'API est alpha mais prête pour la production" [^ 2]. Veuillez être à vos risques et périls lors de son introduction.

Autres avantages de Go CDK en plus de la portabilité multi-cloud

C'est le sujet principal.

Je suis sûr qu'il y a des gens qui pensent: "Je n'utilise qu'AWS! Le verrouillage de Bender, c'est bien!" Dans cet article, je voudrais présenter les avantages de l'utilisation de Go CDK même pour ces personnes, en utilisant comme exemple "manipulation d'objets S3 (sauvegarde / récupération)".

API facile à comprendre et à gérer

L'API Go CDK est conçue pour être intuitive, facile à comprendre et à utiliser.

La lecture et l'écriture d'objets sur le stockage cloud avec Go CDK sont disponibles sur blob.Bucket NewReader () et NewWriter () Blob.Reader ([] obtenu par /[email protected]/blob#Bucket.NewWriter) ʻO.Reader](implémenté https://pkg.go.dev/io#Reader)) et [blob.Writer](https://pkg.go.dev/[email protected]) Utilisez / blob # Writer) (implémentez [ʻio.Writer](https://pkg.go.dev/io#Writer)). Il est très intuitif que vous puissiez obtenir (lire) un objet avec blob.Reader (ʻio.Reader) et le sauvegarder (écrire) avec ʻio.Writer (ʻio.Writer`). Cela vous permet de travailler avec des objets dans le cloud comme si vous travailliez avec un fichier local.

Voyons comment il sera plus facile à comprendre que d'utiliser le kit AWS SDK, avec des exemples concrets.

Lors de l'enregistrement d'un objet dans S3

Pour le kit SDK AWS, [ʻUpload () dans [ s3manager.Uploader](https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3/s3manager#Uploader) ](Https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3/s3manager#Uploader.Upload) sera utilisé. Passez le contenu de l'objet à télécharger à la méthode en tant que ʻio.Reader. Lors du téléchargement d'un fichier local, il est pratique de transmettre ʻos.File` tel quel, mais le problème est que les données en mémoire sont sous une forme ou une autre. Si vous souhaitez encoder avec et enregistrer tel quel.

Par exemple, le processus de codage JSON et S3 tel quel est le suivant dans AWS SDK.

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 () ](https://pkg.go.dev/io) pour connecter ʻio.Writer pour encoder JSON et ʻio.Reader pour passer à s3manager.UploadInput` #Pipe) doit être utilisé.

Avec Go CDK, l'écriture se fait avec blob.Writer (ʻio.Writer), donc [ json.NewEncoder () `](https://pkg.go.dev/encoding/json#NewEncoder) Passez-le simplement.

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)
	}
}

Bien sûr, vous pouvez simplement écrire lors du téléchargement d'un fichier local. Utilisez simplement ʻio.Copy` comme si vous copiez d'un fichier à un autre.

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)
	}
}

En passant, le Writer de s3blob est implémenté en enveloppant s3manager.Uploader, vous pouvez donc bénéficier de la fonction de téléchargement parallèle de s3manager.Uploader.

Lors de la récupération d'un objet depuis S3

Prenons le cas de l'obtention du JSON à partir de S3 et du décodage.

Pour le kit SDK AWS, utilisez s3.GetObject (). S3manager.Downloader, qui est associé à s3manager.Uploader, est la destination de sortie. Notez qu'il ne peut pas être utilisé dans ce cas car il doit implémenter ʻio.WriterAt`.

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)
}

Pour Go CDK, écrivez-le simplement dans le sens inverse du téléchargement.

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)
}

Utilisez s3manager.Downloader pour écrire l'objet récupéré dans un fichier local. peut faire.

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)
	}
}

Dans Go CDK, c'est OK si vous l'écrivez dans le sens opposé au moment du téléchargement.

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)
	}
}

Cependant, bien que cette méthode soit simple, elle présente certains inconvénients. Dans le cas de s3manager.Downloder, au lieu de demander ʻio.WriterAt à la destination de sortie, il a une fonction de téléchargement parallèle et a d'excellentes performances, mais dans le cas de Go CDK, le téléchargement parallèle ne peut pas être effectué tel quel. Si vous souhaitez télécharger en parallèle avec Go CDK, vous devez l'implémenter vous-même en utilisant [NewRangeReader ()`](https://pkg.go.dev/[email protected]/blob#Bucket.NewRangeReader). il y a.

Exemple d'implémentation de téléchargement parallèle dans Go CDK (plié car il est long)
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
}
  • Reportez-vous à l'implémentation de s3manager.Downloader

Exécution locale facile

Go CDK est en cours de développement pour fournir une implémentation locale pour tous les services. Par conséquent, vous pouvez facilement remplacer le fonctionnement du service cloud par l'implémentation locale. Par exemple, sur un serveur local pour le développement, il est pratique de remplacer tous les services par des implémentations locales, car ils peuvent être exploités sans accéder à AWS ou GCP.

Pour le package gocloud.dev / blob qui gère le stockage dans le cloud, [ fileblob](https: // pkg. Une implémentation est fournie qui lit et écrit les fichiers locaux (go.dev/[email protected]/blob/fileblob).

Voici un exemple de commutation de la destination de sortie du JSON codé vers S3 et local en fonction de l'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)
}

Si vous l'exécutez tel quel, sample.json sera sauvegardé dans S3, mais si vous l'exécutez avec l'option -local, il sera sauvegardé dans le ʻoutput / sample.json local. À ce stade, la propriété de l'objet est enregistrée sous le nom ʻoutput / sample.json.attrs. En conséquence, les propriétés de l'objet enregistré peuvent être obtenues sans aucun problème.

Testabilité extrêmement améliorée

Le code qui appelle des API de services externes comme AWS doit toujours se soucier de la manière d'en faire une implémentation testable, mais avec Go CDK, vous n'avez pas à vous en soucier. Normalement, vous résumeriez un service externe en tant qu'interface pour implémenter une simulation, et le remplaceriez par une simulation dans les tests ... mais Go CDK a déjà correctement extrait chaque service et fourni son implémentation locale. Puisqu'il est fait, vous pouvez simplement l'utiliser tel quel.

Par exemple, envisagez de tester une structure qui implémente une interface pour télécharger du JSON encodé vers le stockage cloud, telle que:

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

Dans le cas du kit AWS SDK, des interfaces pour divers clients de service sont fournies, de sorte que la testabilité est garantie en les utilisant. Pour s3manager, l'interface est fournie dans un package appelé 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
}

Avec une telle implémentation, vous pouvez tester sans accéder réellement à S3 en mettant une maquette appropriée dans jsonUploader.uploader. Cependant, cette implémentation fictive n'est pas officiellement fournie, vous devrez donc l'implémenter vous-même ou trouver un package externe approprié.

Dans le cas de Go CDK, il devient une structure avec une haute testabilité simplement en l'implémentant telle quelle.

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
}

Pour les tests, il est utile d'utiliser l'implémentation en mémoire blob appelée memblob.

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")
	}
}

Sommaire

Nous avons présenté les avantages autres que la prise en charge multi-cloud et la portabilité cloud en introduisant Go CDK. En raison de la nature de la gestion de plusieurs fournisseurs de cloud de manière unifiée, il existe bien sûr des faiblesses telles que l'impossibilité d'utiliser des fonctions spécifiques à un fournisseur de cloud spécifique, je pense donc que le SDK de chaque fournisseur de cloud sera utilisé correctement selon les exigences.

Go CDK lui-même est encore en développement, donc j'aimerais m'attendre à plus de fonctionnalités à l'avenir.

Recommended Posts

Pourquoi devriez-vous envisager d'adopter Go CDK même si vous êtes un fournisseur de cloud unique
Si vous souhaitez créer Word Cloud.