C#の .NET Framework を使ったアプリで Azure ストレージにデータを入出力

# はじめに
大した内容ではないのだが、必要パッケージのインストール情報やサンプルコードが１つにまとまった記事を自分で書いておくと、後で同じ事をやろうとした時に楽なので記事を書いておいた。

# 実施環境
■ OS
　Windows 10 Pro　

■ 各種SW ＆ フレームワークのバージョン
　Visual Studio 2019
　.NET Framework 4.7.2

# 本記事内で試している事
 .NET Framework を使った C# のWindowsアプリケーションで、`Azure BLOB`へのデータのアップロード・ダウンロードを試してみる。

記事の流れは以下の通り。

1. Azure Storage BLOB の準備
2. Visual Studioで C# の環境準備
3. アップロード・ダウンロードのサンプルコード

## 1. Azure Storage BLOB の準備

#### ストレージアカウントを作成して、適当なコンテナを用意
　※匿名アクセスレベルは、『プライベート(匿名アクセスはありません)』としておく。

#### Blobを操作するためのロール設定
接続したいコンテナに対して、ユーザーのロールを割り当てる必要がある。
ロールの割り当て方は、該当のコンテナにおいて左側のサブメニューより `アクセス制御(IAM)`を選択し、`追加 > ロールの割り当ての追加` より **[ストレージ BLOB データ共同作成者]** のロールを付与する。

#### ※アクセスキーを使った接続について
アクセスキーを使用した接続であれば、ユーザーの作成等は不要になるのだが、この方法ではストレージアカウント内のBlob、File、Queue、Tableは全て読取も書込も可能になってしまい、セキュリティ的にはかなり乱暴なやり方になってしまいそう。

`ストレージアカウント> 左側のサブメニューよりセキュリティとネットワーク > アクセスキー` より確認できる。


## 2. Visual Studioで C# の環境準備
Visual Studio 2019 を立ち上げて、新しいプロジェクト ⇒ コンソールアプリ(.NET Framework) を作成。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/bec5b684-e146-7876-102e-43c631744472.png)

#### 必要なパッケージのインストール
`NuGetパッケージの管理 ` より必要そうなパッケージをインストール。
　※1. パッケージソースは `nuget.org`
　※2. インストールバージョンは `最新の安定版` を選択
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/9eac0df6-7db3-bb2b-f86c-b602962d2775.png)

※ アクセスキーやSASを使った認証であれば、`Azure.Identity`は不要。
`Azure.Storage.Blobs`をインストールすると、依存関係で`Azure.Core`も自動でインストールされる。


## 3. アップロード・ダウンロードのサンプルコード

### ストレージアカウントのアクセスキーを利用して Blob 接続
　※コンテナに対するロールの割り当てが不要だが、何でもできてしまう権限レベルになるため基本は非推奨な気がする！
```C#:
using Azure.Storage.Blobs;

namespace Azure_blob_test
{
    class Program
    {
        static void Main(string[] args)
        {
            // Azure Blob Storage Account の接続文字列　※【セキュリティとネットワーク】のアクセスキーより確認可能
            string connectionString = @"<接続文字列>";

            // データをアップロードする Azure Blob Storage のコンテナ名
            string containerName = @"container1";

            // アップロード元のローカルファイルのパス
            string filePath = @"sample.txt";

            try
            {
                // Program クラスのインスタンスを作成
                Program program = new Program();

                //program.UploadWithAccesskey(connectionString, containerName, filePath);
                //program.DownloadWithAccesskey(connectionString, containerName, filePath);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

        }

        private void UploadWithAccesskey(string connection, string containerName, string input_filepath)
        {
            // BlobContainerClient の取得
            BlobContainerClient containerClient = new BlobContainerClient(connection, containerName);

            // BlobClient の取得 （クライアントという名前が、Blob自体はファイル末端の様なイメージ）  ※ ディレクトリをファイルで上書きしてしまう事もできるし、ディレクトリがなければ作ってしまう。
            BlobClient blob = containerClient.GetBlobClient("upload.txt");

            // BlobClient を通じて、ローカルファイルを Azure Blob Storage へアップロード
            blob.Upload(input_filepath);
        }

        private void DownloadWithAccesskey(string connection, string containerName, string input_filepath)
        {
            // BlobContainerClient の取得
            BlobContainerClient containerClient = new BlobContainerClient(connection, containerName);

            // BlobClient の取得
            BlobClient blob = containerClient.GetBlobClient("sample2.txt");

            // BlobClient を通じて、zure Blob Storage のファイルをローカルにダウンロード
            blob.DownloadTo("download.txt");
        }
    }
}
```

### `DefaultAzureCredential` を使う
コンテナに対して権限を持っているユーザーであれば、恐らくこの方法最も一般的である 
```C#:
using Azure.Identity;
using Azure.Storage.Blobs;

namespace Azure_blob_test
{
    class Program
    {
        static void Main(string[] args)
        {        
            // アップロード元のローカルファイルのローカルパス
            string localFilePath = @"sample.txt";
    
            // コンテナのエンドポイント ⇒ コンテナのプロパティより確認可能
            string blobServiceEndpoint = @"";
        
            try
            {
                // Program クラスのインスタンスを作成
                Program program = new Program();
    
                // DefaultAzureCredential を使う方法
                //program.UploadWithAzureCredential(blobServiceEndpoint, localFilePath);
                //program.DownloadWithAzureCredential(blobServiceEndpoint, @"downloadTo.txt");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }        
        }
    
        private void UploadWithAzureCredential(string blobServiceEndpoint, string input_filepath)
        {
            BlobContainerClient containerClient = new BlobContainerClient(new Uri(blobServiceEndpoint), new DefaultAzureCredential());
            // Default Azure Credential は以下の認証方法を順に試みて、最初に成功したものを使用するらしい。
            /*
             ・環境変数（Azure_TENANT_ID、 AZURE_CLIENT_ID、 AZURE_CLIENTSCRET）
             ・Managed Identity
             ・Visual Studio の認証情報（ローカル開発環境で Visual Studio がインストールされている場合）
             ・Azure CLI の認証情報（ローカル開発環境で Azure CLI がインストールされている場合）
            */
    
            // アップロード先（Blobパス）
            BlobClient blob = containerClient.GetBlobClient("uploadWithAzureCredential.txt");
            blob.Upload(input_filepath);        
        }

        private void DownloadWithAzureCredential(string blobServiceEndpoint, string outputLocalFilePath)
        {
            BlobContainerClient containerClient = new BlobContainerClient(new Uri(blobServiceEndpoint), new DefaultAzureCredential());

            // ダウンロードしたいファイル（Blobパス）
            BlobClient blob = containerClient.GetBlobClient("uploadWithAzureCredential.txt");
            blob.DownloadTo(outputLocalFilePath);
        }
    }
}
```

※ 1つ注意点としては、ユーザーの権限が所有者であっても、なぜかこの接続方法の場合はパーミッションエラーが起きてしまったため、**[ストレージ BLOB データ共同作成者]** のロールを付与する必要がありました。



#### Shared Access Signature（SAS）を利用して Blob 接続
Microsoftのアカウントを持っていない人にでも手軽に接続をさせたい時に便利。

コンテナ単位にSASの発行の発行ができるのだが、署名方法として以下の２つがある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/b6e40f2d-0bc3-4285-c58d-4d22c8be688f.png)

`アカウントキー`による署名は基本的に所有者しかできず、発行SASの有効期限やSAS接続による権限も自由に設定可能。
一方 `ユーザーの委任キー`は誰でも行えるが、有効期限が7日以内だったり、デフォルトではSAS接続による権限も読み取りのみしか設定できなかったりする。

**※クライアントがユーザー委任 SAS を使用して Blob Storage リソースにアクセスすると、Azure Storage への要求は、SAS の作成に使用された AzureAD 資格情報で承認されるらしいです。**

```C#:
using Azure.Storage.Blobs;

namespace Azure_blob_test
{
    class Program
    {
        static void Main(string[] args)
        {        
            // アップロード元のローカルファイルのローカルパス
            string localFilePath = @"sample.txt";
    
            // コンテナのエンドポイント ⇒ コンテナのプロパティより確認可能
            string blobServiceEndpoint = @"";
        
            try
            {
                // Program クラスのインスタンスを作成
                Program program = new Program();
    
                // SAS （アカウントキー）を使う　※ コンテナに対する アクセス制御(IAM) 設定は不要？
                //program.UploadWithSas(localFilePath, @"upload_with_sas.txt");
                //program.DownloadWithSas(@"upload_with_sas.txt", "download_with_sas.txt");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }        
        }
    
        private void UploadWithSas(string input_filepath, string output_filename)
        {
            // コンテナまでのURLの後に `/{0}` のblob パスを付けている
            string sas_url = string.Format(@"共有アクセストークンより発行可能！", output_filename);
            
            BlobClient blob = new BlobClient(new Uri(sas_url));
            blob.Upload(input_filepath);
        }

        private void DownloadWithSas(string filename, string download_filename)
        {
            // コンテナまでのURLの後に `/{0}` のblob パスを付けている        
            string sas_url = string.Format(@"共有アクセストークンより発行可能！", filename);

            BlobClient blob = new BlobClient(new Uri(sas_url));
            blob.DownloadTo(download_filename);
        }
    }
}
```

※ ユーザー自体に適切なロールが付いていても、SASの発行の際にアクセス許可を付け忘れるとアップロードできないので注意！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/ed9e896e-1067-ce3c-6210-fc734df12c22.png)

## 最後に
本当に基本的な blobへの接続の仕方は確認できたのだが、アカウントを２つ持っている時の `DefaultAzureCredential` の動きや サービスプリンシパルを使った接続方法などは試せていないため、また確認できたら追記したいと思います。