Cómo trabajar con el SDK de Azure Storage para Go

Todos los años intento aprender un nuevo lenguaje de programación. No a nivel experto, ni mucho menos, pero si ser capaz de comprender y lanzarme a la piscina cuando un cliente necesita que le muestre una funcionalidad en su lenguaje preferido. Este año ha tocado aprender Go y quería compartir contigo cómo trabajar con los blobs Azure Storage y este lenguaje.

Aplicación de ejemplo

Si estás leyendo este artículo es posible que tú sí sepas de Go y necesites saber cómo interactuar con este servicio de la plataforma. Para mostrarte lo mínimo que necesitas saber he creado una interfaz de línea de comandos, un CLI vamos, con Cobra, Viper, y el SDK de Azure Storage para Go. Con esta serás capaz de lanzar los siguientes comandos:

# Test commands
# Create container
go run . create container --name code
go run . create container --name pics --public
# List containers
go run . list containers
# Upload file
go run . create blob -n main.go -c code -p main.go
go run . create blob -n licencia -c code -p LICENSE

# List blobs
go run . list blobs -c code
# Download blob
go run . download blob -c code -n main.go -p prueba_main.go
go run . download blob -c code -n licencia -p prueba_licencia
# Delete blob
go run . delete blob -c code -n main.go

Crear el CLI

Para crear el CLI con Cobra de forma sencilla, he usado Cobra Generator. Para instalarlo en tu local puedes usar el siguiente comando:

go install github.com/spf13/cobra-cli@latest

Además, si quieres que sea sencillo su uso puedes modificar tu bash/zsh para añadir el path donde este se ha instalado y poder usarlo sin tener que especificar la ruta completa.

export PATH=$PATH:/Users/$USER/go/bin

Una vez lo tengas puedes lanzar los siguientes comandos para generar la estructura que he utilizado para este ejemplo:

# Create folder
mkdir az-storage-golang-demos
# Create cobra app
cobra-cli init --viper
# Add commands
cobra-cli add create
cobra-cli add container -p createCmd
cobra-cli add blob -p createCmd
cobra-cli add list
cobra-cli add containers -p listCmd
cobra-cli add blobs -p listCmd
cobra-cli add download
cobra-cli add delete

Añade las siguientes librerías a tu proyecto:

go get github.com/manifoldco/promptui
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
go get github.com/Azure/azure-sdk-for-go/sdk/storage/azblob

La primera de ellas la utilizo para pedir al usuario información por pantalla. Las otras dos son para poder autenticarnos e interactuar con Azure Storage.

Ahora vamos a ver cada uno de los comandos, cómo han sido implementados.

Paquete internal

Antes de poder ejecutar cualquiera de los comandos anteriores, es necesario recuperar las credenciales/token para interactuar con la cuenta de almacenamiento de Azure. Para ello he creado un paquete llamado internal que sabe cómo crear el objeto que nos dará acceso a nuestro recurso una vez autenticados:

package internal
import (
	"fmt"
	"github.com/manifoldco/promptui"
	"github.com/spf13/viper"
)
func CheckCredentials() {
	// Get config values if there is a config file in $HOME
	storageAccountName := viper.GetString("storageAccountName")
	if storageAccountName == "" {
		// Set config file
		viper.AddConfigPath(".")
		viper.SetConfigFile("config.yaml")
		viper.ReadInConfig()
	}
	// try again
	storageAccountName = viper.GetString("storageAccountName")
	// if not, prompt for azure storage account name
	if storageAccountName == "" {
		// Prompt for azure storage account name
		prompt := promptui.Prompt{
			Label: "Type your storage account name",
		}
		result, err := prompt.Run()
		if err != nil {
			fmt.Printf("Prompt failed %v\n", err)
			return
		}
		fmt.Printf("You choose %q\n", result)
		viper.AddConfigPath(".")
		viper.SetDefault("storageAccountName", result)
		viper.WriteConfig()
	}
	// Print config values
	println("You are playing with " + storageAccountName)
}

Para comprobar si tenemos las credenciales necesarias, para acceder a la cuenta de almacenamiento, hago uso de Viper, el cual recupera la configuración por defecto de $HOME/.nombre_del_proyecto.yaml. En mi caso, sino lo encuentro en esa ubicación me fijo en el archivo config.yaml que he creado como parte del proyecto y recupero el nombre de la cuenta. En el caso de no estar en ningún archivo de configuración le pido el nombre al usuario y la guardo en un archivo config.yaml que genero.

Por otro lado, dentro del mismo paquete internal, tengo otro archivo donde recupero el cliente de Azure Storage:

package internal
import (
	"fmt"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"github.com/spf13/viper"
)
func GetBlobServiceClient() (*azblob.Client, error) {
	// Create a default Azure credential
	credential, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		return nil, err
	}
	// Create blob client
	return azblob.NewClient(fmt.Sprintf("https://%s.blob.core.windows.net", viper.GetString("storageAccountName")), credential, nil)
}

Esto se puede «complicar» más, ya que para este ejemplo estoy teniendo en cuenta que ya estoy autenticada con Azure CLI y aprovecho el token obtenido para este. En el caso de no estar logada la aplicación me devolverá un error como el siguiente:

error al no estar logada con Azure CLI usando el SDK para golang

Con estos dos archivos, ya puedo trabajar con los comandos que cree al inicio.

Crear un contenedor

Para crear un contenedor dentro de una cuenta de almacenamiento podemos hacerlo de la siguiente forma:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"context"
	"log"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
	"github.com/spf13/cobra"
	"azure-storage-golang-demos/internal"
)
// containerCmd represents the container command
var containerCmd = &cobra.Command{
	Use:   "container",
	Short: "It creates a container",
	Long:  `It creates a container in a storage account.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		var containerName string
		containerName, _ = cmd.Flags().GetString("name")
		isPublic, _ := cmd.Flags().GetBool("public")
		ctx := context.Background()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		// Create a container
		containerClient := serviceClient.ServiceClient().NewContainerClient(containerName)
		if isPublic {
			log.Println("Creating public container")
			access := container.PublicAccessTypeContainer
			// access := container.PublicAccessTypeBlob
			createContainerOptions := container.CreateOptions{
				Access: &access,
			}
			_, err = containerClient.Create(ctx, &createContainerOptions)
		} else {
			_, err = containerClient.Create(ctx, nil)
		}
		if err != nil {
			log.Fatal("Error creating container: " + err.Error())
		}
		log.Printf("Container %s created", containerName)
	},
}
func init() {
	createCmd.AddCommand(containerCmd)
	// Here you will define your flags and configuration settings.
	containerCmd.Flags().StringP("name", "n", "", "Name of the container")
	containerCmd.MarkFlagRequired("name")
	containerCmd.Flags().BoolP("public", "p", false, "Make the container public")
}

En este ejemplo, si te fijas, he querido mostrarte además cómo crear contenedores privados (por defecto) como públicos, ya sea a nivel de contenedor o blob.

Listar contenedores

Una vez que ya tenemos algunos contenedores creados, podemos listarlos:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"azure-storage-golang-demos/internal"
	"context"
	"log"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"github.com/fatih/color"
	"github.com/rodaine/table"
	"github.com/spf13/cobra"
)
// containersCmd represents the containers command
var containersCmd = &cobra.Command{
	Use:   "containers",
	Short: "List containers in your storage account",
	Long:  `You can see all containers in your storage account and the type of access configure.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		// List containers
		pager := serviceClient.NewListContainersPager(&azblob.ListContainersOptions{
			Include: azblob.ListContainersInclude{Metadata: true, Deleted: true},
		})
		// Define output table
		headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
		columnFmt := color.New(color.FgYellow).SprintfFunc()
		table := table.New("Name", "Last modified")
		table.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
		for pager.More() {
			resp, err := pager.NextPage(ctx)
			if err != nil {
				log.Fatal("Error listing containers: " + err.Error())
				return
			}
			for _, _container := range resp.ContainerItems {
				table.AddRow(*_container.Name, _container.Properties.LastModified)
			}
		}
		table.Print()
	},
}
func init() {
	listCmd.AddCommand(containersCmd)
}

Y el resultado debería de ser como el siguiente:

go run . list containers

Subir un archivo

Otra de las tareas más importantes, quizás la que más, es la de subir archivos a una cuenta de almacenamiento:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"azure-storage-golang-demos/internal"
	"context"
	"log"
	"os"
	"github.com/spf13/cobra"
)
// blobCmd represents the blob command
var blobCmd = &cobra.Command{
	Use:   "blob",
	Short: "Create a new blob",
	Long:  `It creates a new blob in your storage account.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		// Get flags values
		containerName := cmd.Flag("container").Value.String()
		blobName := cmd.Flag("name").Value.String()
		path := cmd.Flag("path").Value.String()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		// Try to open the file you want to upload
		file, err := os.Open(path)
		if err != nil {
			log.Fatal("Error reading file: " + err.Error())
		}
		// Upload the file
		_, err = serviceClient.UploadFile(ctx, containerName, blobName, file, nil)
		if err != nil {
			log.Fatal("Error creating blob: " + err.Error())
		}
		log.Printf("File '%s' uploaded", blobName)
	},
}
func init() {
	createCmd.AddCommand(blobCmd)
	// Flags
	blobCmd.Flags().StringP("name", "n", "", "Name of the blob")
	blobCmd.MarkFlagRequired("name")
	blobCmd.Flags().StringP("path", "p", "", "Local path of the blob")
	blobCmd.MarkFlagRequired("path")
	blobCmd.Flags().StringP("container", "c", "", "Container name")
	blobCmd.MarkFlagRequired("container")
}

Listar blobs

Ahora que ya puedes tener algunos blobs en tus contenedores, también es posible que necesites listarlos en base al contenedor donde se encuentran:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"azure-storage-golang-demos/internal"
	"context"
	"log"
	"github.com/fatih/color"
	"github.com/rodaine/table"
	"github.com/spf13/cobra"
)
// blobsCmd represents the blobs command
var blobsCmd = &cobra.Command{
	Use:   "blobs",
	Short: "List blobs in a container",
	Long:  `List blobs in a specific container.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		// Get flags values
		containerName := cmd.Flag("container").Value.String()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		// Get blobs in the container
		pager := serviceClient.NewListBlobsFlatPager(containerName, nil)
		// Define output table
		headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
		columnFmt := color.New(color.FgYellow).SprintfFunc()
		table := table.New("Name", "Last modified")
		table.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
		// List blobs
		for pager.More() {
			resp, err := pager.NextPage(ctx)
			if err != nil {
				log.Fatal("Error listing containers: " + err.Error())
				return
			}
			for _, _blob := range resp.Segment.BlobItems {
				table.AddRow(*_blob.Name, _blob.Properties.LastModified)
			}
		}
		// Print output table
		table.Print()
	},
}
func init() {
	listCmd.AddCommand(blobsCmd)
	//Flags
	blobsCmd.Flags().StringP("container", "c", "", "Name of the container")
}

Descargar un blob

Casi llegando al final, otra tarea que solemos necesitar es la descarga:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"azure-storage-golang-demos/internal"
	"context"
	"log"
	"os"
	"github.com/spf13/cobra"
)
// downloadCmd represents the download command
var downloadCmd = &cobra.Command{
	Use:   "download",
	Short: "Download a blob",
	Long:  `You can download a blob from your storage account. You need to specify the container name, the blob name and the path where you want to save the file.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		// Get flags values
		containerName := cmd.Flag("container").Value.String()
		blobName := cmd.Flag("name").Value.String()
		path := cmd.Flag("path").Value.String()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		file, err := os.Create(path)
		if err != nil {
			log.Fatal("Error reading file: " + err.Error())
		}
		// Download the blob
		_, err = serviceClient.DownloadFile(ctx, containerName, blobName, file, nil)
	},
}
func init() {
	rootCmd.AddCommand(downloadCmd)
	// Flags
	downloadCmd.Flags().StringP("name", "n", "", "Name of the blob")
	downloadCmd.MarkFlagRequired("name")
	downloadCmd.Flags().StringP("path", "p", "", "Local path of the blob")
	downloadCmd.MarkFlagRequired("path")
	downloadCmd.Flags().StringP("container", "c", "", "Container name")
	downloadCmd.MarkFlagRequired("container")
}

Eliminar un blob

Y, por último, para eliminar un blob puedes hacerlo a través de la siguiente implementación:

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
	"azure-storage-golang-demos/internal"
	"context"
	"fmt"
	"log"
	"github.com/spf13/cobra"
)
// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
	Use:   "delete",
	Short: "Delete a blob from a container",
	Long:  `You can delete blobs in a container.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		internal.CheckCredentials()
	},
	Run: func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		// Get flags values
		containerName := cmd.Flag("container").Value.String()
		blobName := cmd.Flag("name").Value.String()
		// Get blob service client
		serviceClient, err := internal.GetBlobServiceClient()
		if err != nil {
			log.Fatal("Error trying to get service client: " + err.Error())
		}
		serviceClient.DeleteBlob(ctx, containerName, blobName, nil)
		fmt.Printf("Blob %s deleted from container %s successfully", blobName, containerName)
	},
}
func init() {
	rootCmd.AddCommand(deleteCmd)
	// Here you will define your flags and configuration settings.
	deleteCmd.Flags().StringP("name", "n", "", "Name of the blob")
	deleteCmd.MarkFlagRequired("name")
	deleteCmd.Flags().StringP("container", "c", "", "Container name")
	deleteCmd.MarkFlagRequired("container")
}

Como ves, en cada uno de los comandos, utilizo el método PreRun para comprobar que tengo las credenciales adecuadas y después hago uso de internal.GetBlobServiceClient() para recuperar el cliente.

El código del ejemplo lo tienes en mi GitHub.

¡Saludos!