Python で json形式のデータを Azure Blob に アップロード してみました


## 概要
Azure SDK for Python を利用して、json形式のデータを直接 Azure Blob にアップロードしたり、json形式のローカルファイルを Azure Blob にアップロードしたりするための Python プログラムです。

BLOBへの接続には「接続文字列」を使用せず、ServicePrincipal を定義し、アップロード先のコンテナのみにロールを割当てました（Azure Founctions化を考慮して）。


### 実行環境
macOS Big Sur 11.1 
python 3.8.3
Azure CLI 2.28.0

---

## 事前の準備

### ストレージアカウントの登録

```
## 使用するテナントのAzure環境へのログイン
$ az login --tenant <tenant_id>

## 使用サブスクリプションを定義します
$ az account set --subscription '<Subscription名>'

## リソースグループの作成
$ az group create --resource-group rg-UsageCostManage --location japaneast

## ストレージアカウントの作成
$ az storage account create --name usagecostmanage --resource-group rg-UsageCostManage -l japaneast --sku Standard_LRS

## コンテナの作成
$ az storage container create --name test-data
```

### 実行するプログラム用の ServicePrincipal の作成

[この記事](https://jpazureid.github.io/blog/azure-active-directory/azuread-clientsecrets-202104/) にありますように、ServicePrincipal のクライアントシークレットの有効期限はデフォルトで１年、最大２年までの設定となります。そこで Azure CLI から以下のような手順で、有効期限 29年 での ServicePrincial を作成します（現時点では問題なく作成できました）。

```
## 最初は２年で作成します（ロール割当なしで）
$ az ad sp create-for-rbac --name sp_datalakeblob --skip-assignment --years 2

## 作成された内容を取得できることを確認します（いきなり29年で作成した場合、取得できません、、、）
$ az ad sp list --display-name sp_datalakeblob

## その後、29年で再作成します（ロール割当なしで）
$ az ad sp create-for-rbac --name sp_datalakeblob --skip-assignment --years 29
{
  "appId": "xxxxxxxx-xxxx-5757-4774-xxxxxxxxxxxx",      --> AZURE_CLIENT_ID としてローカル環境変数に登録
  "displayName": "sp_datalakeblob",
  "name": "xxxxxxxx-xxxx-5757-4774-xxxxxxxxxxxx",
  "password": "hogehogehogehogehogehogehogehogege",     --> AZURE_CLIENT_SECRET としてローカル環境変数に登録
  "tenant": "zzzzzzzz-cccc-4645-5757-zzzzzzzzzzzz"      --> AZURE_TENANT_ID としてローカル環境変数に登録
}

## 必要なスコープに必要なロールを割り与えます
## 今回はデータを保存するので、スコープ：containers　ロール：Contributor とします
$ APP_ID=$(az ad sp list --display-name sp_datalakeblob --query '[].{ID:appId}' --output tsv)
$ az role assignment create \
    --assignee $APP_ID \
    --role "Storage Blob Data Contributor" \
    --scope /subscriptions/<Subscription_ID>/resourceGroups/rg-UsageCostManage/providers/Microsoft.Storage/storageAccounts/usagecostmanage/blobServices/default/containers/test-data
```

---

## Pythonプログラムの実行

### 実行するプログラム

```python:BlobUpload.py
import os, uuid
import json
from tabulate import tabulate
from pandas import DataFrame, Series
from azure.storage.blob import BlobServiceClient, __version__
from azure.identity import DefaultAzureCredential

CONTAINER_NAME = 'test-data'

# Blob SDK Verion の確認
def BlobVersionCheck():
    try:
        print("\nAzure Blob Storage v" + __version__ + " - Python")

    except Exception as ex:
        print('Exception:')
        print(ex)


# Blobを操作するオブジェクトを取得
def GetBlobServiceObject():
    blob_service_client = BlobServiceClient(
        account_url="https://usagecostmanage.blob.core.windows.net",
        credential=DefaultAzureCredential()
    )
    return blob_service_client


# Containerを操作するオブジェクトを取得
def GetContainerObject(blob_service_client):    
    container_client = blob_service_client.get_container_client(CONTAINER_NAME)
    return container_client


# Blob へのjsonデータ保存
def BlobUploadJson(blob_service_client):
    print("\n\n ＝＝＝ BlobUploadJson 関数の実行 ＝＝＝")
    try:        
        # Create a blob name
        blob_name = str(uuid.uuid4()) + ".json"
        print("\nUploading to Azure Storage as blob (from JSON) :\t" + blob_name)
        
        # Create a blob client using the blob_name as the name for the blob
        blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=blob_name)

        # Data Generate & Upload
        rows = RowData()
        blob_client.upload_blob(json.dumps(rows))

    except Exception as ex:
        print('Exception:')
        print(ex)


# LocalFile へのjsonデータ保存と、そのファイルを Blob へアップロード
def BlobUploadFile(blob_service_client):
    print("\n\n ＝＝＝ BlobUploadFile 関数の実行 ＝＝＝")
    try:        
        # Create a Localfile name
        local_file_name = str(uuid.uuid4()) + ".json"
        print("\nUploading to Local File (from JSON) :\t" + local_file_name)

        # Data Generate
        rows = RowData()

        # json.dump関数でファイルに書き込む
        fw = open(local_file_name,'w')
        json.dump(rows, fw, indent=4)
        fw.close()

        # Create a blob client using the local_file_name as the name for the blob
        blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=local_file_name)
        print("\nUploading to Azure Storage as blob (from FILE) :\t" + local_file_name)

        # Blob Upload the LocalFile
        with open(local_file_name, "rb") as data:
            blob_client.upload_blob(data)

    except Exception as ex:
        print('Exception:')
        print(ex)


# サンプルJSONデータの生成
def RowData():
    row_key = ["Subscription", "UsageCost"]
    row_value = [['app-01', 24095.3648086046965], ['sponsor-03', 100.10], ['lab-01', 15952.818900157803]]

    # 取得したコスト情報を辞書型に変換
    row_dict = [dict(zip(row_key,item)) for item in row_value]

    # コストで降順ソートする
    rows = sorted(row_dict, key=lambda x:x['UsageCost'], reverse=True)
    print(tabulate(rows, headers='keys'))

    return rows


# Blob一覧の取得
def BlobList(container_client):
    print("\n\n ＝＝＝ Blob 一覧の表示 ＝＝＝")
    try:        
        # List the blobs in the container
        print("\nListing blobs...")
        blob_list = container_client.list_blobs()
        for blob in blob_list:
            print("\t" + blob.name)

    except Exception as ex:
        print('Exception:')
        print(ex)


if __name__ == '__main__':
    # Blob SDK Verion の確認
    BlobVersionCheck()

    # Blobを操作するオブジェクトを取得
    blob_service_client = GetBlobServiceObject()

    # LocalFile へのjsonデータ保存と、そのファイルを Blob へアップロード
    BlobUploadFile(blob_service_client)
    
    # Blob へのjsonデータ保存
    BlobUploadJson(blob_service_client)

    # Containerを操作するオブジェクトを取得
    container_client = GetContainerObject(blob_service_client)
    
    # コンテナ一覧の取得
    BlobList(container_client)
    print("")
```

### プログラムの実行と結果
 
```
$ python BlobUpload.py

Azure Blob Storage v12.5.0 - Python 


 ＝＝＝ BlobUploadFile 関数の実行 ＝＝＝

Uploading to Local File (from JSON) :	de92d774-16e0-4c3a-a461-ea21a686ad0d.json
Subscription      UsageCost
--------------  -----------
app-01              24095.4
lab-01              15952.8
sponsor-03            100.1

Uploading to Azure Storage as blob (from FILE) :	de92d774-16e0-4c3a-a461-ea21a686ad0d.json


 ＝＝＝ BlobUploadJson 関数の実行 ＝＝＝

Uploading to Azure Storage as blob (from JSON) :	6070f371-e21e-4487-887a-69bc2bdd9541.json
Subscription      UsageCost
--------------  -----------
app-01              24095.4
lab-01              15952.8
sponsor-03            100.1


 ＝＝＝ Blob 一覧の表示 ＝＝＝

Listing blobs...
	6070f371-e21e-4487-887a-69bc2bdd9541.json
	de92d774-16e0-4c3a-a461-ea21a686ad0d.json
```

---

### まとめ
ServicePrincipal を使用することにより、ロールの割当てをコンテナに限定することでセキュリティを高めれたのでは、、、と思っています。将来的には、AzureFunctions を使用して 必要なデータを BLOB へ アップロードしていく予定です。

### 付録（各種情報の取得）

```
## ストレージアカウントの認証情報の取得
$ az storage account keys list --account-name <StorageAccout名> --resource-group <ResourceGroup名> --output table

## 接続文字列の取得
$ az storage account show-connection-string -g <ResourceGroup名> -n <StorageAccout名> --output tsv
```