SASを使ったAzure Blob StorageへのアップロードをGoで実装する

[SENSYN ROBOTICS](https://www.sensyn-robotics.com/)(センシンロボティクス)の中山です。
Webアプリやそのインフラ周り、Web側とドローンの接続を行うデバイスドライバ的なモジュールを担当しています。

今回は[ARM64をターゲットとしたGoのクロスコンパイル](https://qiita.com/nakayamaatsushi/items/31a8e7f51fc550202e75)の続きというか、Goで何を作ったのかという話です。

# 何を作ったのか?

前回と同じく、[SENSYN DRONE HUB](https://www.sensyn-robotics.com/solution/sensyndronehub)という、人間の手を一切介さずに運用できるドローンの中で動くソフトウェアを作りました。

DRONE HUBは、指定の時刻に自動的に基地から離陸し、事前に作成しておいたルートを飛行して動画・静止画を撮影し、基地に返ってきます。返ってきたら自動的に充電して次回の飛行に備えつつ、撮影したデータをクラウドに同期します。

この、データをクラウドに同期する機能は、ドローン内部の静止画や動画をAzure Blob Storageにアップロードすることで実現しています。機能自体はGoで作成しており、[Azure Storage Blob SDK for Go](https://github.com/Azure/azure-storage-blob-go)を使ってShared Access Signatures(SAS=アクセスに制限を加えたURI)にファイルをアップロードします。

# なぜSASを使うのか

当初、ドローン内部の設定ファイル等にAzure Blob Storageのアカウントキーを置いて、ドローンからAzure Blob Storageにフルアクセスさせるという案もありました。しかし、万が一、ドローンが盗難に遭ったときにAzure Blob Storageへのフルアクセス権も一緒に盗まれる可能性があるというのは許容できるリスクではありません。

そこで、特定のパスへのアップロード権限を限られた時間だけ持つSASを返すWeb APIをAzure Functionsに作成しました。このWeb APIでSASを取得してからデータのアップロードを行います。こうすることで、ストレージアカウントがドローンから漏れるリスクはなくなります。

もちろん、Web APIのAPI keyはドローン内部に保持する必要はあり、こちらが漏れるリスクはあります。しかし、このWeb APIで取得できるSASにはデータの閲覧権限を与えていないため、データが流出するリスクはありません。また、API keyは機体ごとに発行しており、機体に異常があればすぐに無効化できるようになっています。

こういったセキュリティ面を考慮して、SASを利用しています。

# 実装

SASを利用するようにしたことで、処理のフローは以下のようになりました。

1. 飛行の開始時刻・終了時刻と動画・静止画のタイムスタンプを比較して、飛行中の撮影データを抽出する
2. ファイルごとにWeb APIを呼び出してSASを取得する
3. SASに対して[Azure Storage Blob SDK for Go](https://github.com/Azure/azure-storage-blob-go)経由でアップロードする

問題は[Azure Storage Blob SDK for Go](https://github.com/Azure/azure-storage-blob-go)を使ってSASにアップロードする方法でした。公式ドキュメントを見ると、アカウントキーを指定してアップロードするサンプルはあるのですが、SASを使う方法がよく分かりません。試行錯誤した結果、以下のようなコードでアップロードできることが分かりました。

```golang
import (
	"context"
	"io"
	"log"
	"mime"
	"net/url"
	"path/filepath"
	"os"
	"time"

	"github.com/Azure/azure-storage-blob-go/azblob"
	"github.com/pkg/errors"
)

const (
	defaultBufferSize = 4 * 1024 * 1024
	maxBuffers     	  = 4
)

func Upload(sasUri string, file os.FileInfo, content io.Reader) (string, error) {
	u, errParse := url.Parse(sasUri)
	if errParse != nil {
		return sasUri, errors.Wrapf(errParse, "failed to parse SAS URL: %s", sasUri)
	}

	pipeline := azblob.NewPipeline(azblob.NewAnonymousCredential(), azblob.PipelineOptions{
		Retry: azblob.RetryOptions{
			TryTimeout: 30 * time.Minute,
		},
	})
	blockBlobURL := azblob.NewBlockBlobURL(*u, pipeline)
	ctx := context.Background()

	log.Printf("Uploading...: %s to %s\n", file.Name(), sasUri)
	_, errUpload := azblob.UploadStreamToBlockBlob(ctx, content, blockBlobURL,
		azblob.UploadStreamToBlockBlobOptions{
			BufferSize: defaultBufferSize,
			MaxBuffers: maxBuffers,
			BlobHTTPHeaders: azblob.BlobHTTPHeaders{
				ContentType: mime.TypeByExtension(filepath.Ext(file.Name())),
			},
		})
	if errUpload != nil {
		return sasUri, errors.Wrapf(errUpload, "failed to upload to %s", sasUri)
	}
	log.Printf("Done: %s to %s\n", file.Name(), sasUri)
	return sasUri, nil
}
```

ポイントは、アカウントキー用の`azblob.NewSharedKeyCredential(accountName, accountKey)`の代わりに`azblob.NewAnonymousCredential()`を使うところです。それ以外は通常のアップロードと同じです。

# まとめ

* Azure Blob Storageのアカウントキーが漏洩する可能性があるときは、Shared Access Signatures(SAS=アクセスに制限を加えたURI)を使うと安全にできる
* SASを使って[Azure Storage Blob SDK for Go](https://github.com/Azure/azure-storage-blob-go)経由でファイルをアップロードするときは、`azblob.NewSharedKeyCredential(accountName, accountKey)`の代わりに`azblob.NewAnonymousCredential()`を使う