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.
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)".
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.
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
.
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.
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
}
s3manager.Downloader
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.
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")
}
}
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.