サーバーレスでsftpファイル転送

# AzureBlobStorageからsftpを使ってファイル転送
こちらの記事は、Qiita に掲載した [Microsoft Azure Tech Advent Calendar 2018](https://qiita.com/advent-calendar/2018/microsoft-azure-tech "Microsoft Azure Tech Advent Calendar 2018") の企画に基づき、執筆した 内容となります。(2 日目)

Azure Functionsを使ってAzure BlobStorageのアップロードしたファイルを直ちにSFTPサーバーへアップロードするコードを書いてみました。
勿論、最新版の__Functions 2.0__``C#``を使って書きますよ。

##Functionsのプロジェクト作成

###PortalへFunctionsをデプロイ
ポチポチとFunctions作りますよ。

####まずはここから、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/ea91f5e5-aa67-6e37-0cbc-24b1765432a9.png)

``Function``と入力して、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/3b448770-c760-d4f6-44ee-15773d5272f6.png)
``Functions App``を選択

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/799dd1b3-86bc-60c7-5680-97958d234ea8.png)
``作成``を押下

**リージョン**,**ランタイム**を選んでポチっと
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/f875d996-db94-6c8b-fd11-364153f4da0e.png)

立ち上がりました。簡単ですね。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/63fef0d7-5f70-4825-294c-b4345429575c.png)
ここでこのまま``Visula Editor``で作成もできますが、
今回は地味ですが、BlobからダウンロードしたファイルをSFTPで別のサーバーにアップロードするプログラムなので、``Visual Studio``で作成してみます。

####``Visual Studio``で``Functions``を作成
新規作成->プロジェクト
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/a7bb872b-36f3-8a93-a5dc-ed6fe84e4e9e.png)

``Azure Functions``を選択
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/9b38348e-ee22-16ee-5677-f448712454dd.png)

``Azure Functions V2``、``Http trigger``を選択、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/4a022a12-7251-1088-7fc4-2474c5060020.png)


ストレージアカウントは任意のものを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/b9daff41-8230-3098-7e53-8ac371160577.png)

作成完了!
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/ee42a861-9e03-7e8d-e701-1f2bd5cf221c.png)

まずは``デプロイ``してみます。
``発行``を押下し、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/184ffb8c-8c02-5ef3-b4f3-06af47135ff3.png)

``既存のものを選択``、``発行``を押下する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/0d1dc84b-c215-666a-81e2-9d77148f7a95.png)

先ほど``Azure``で作成した、``Functions``が選択できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/1504e1e6-ada2-26cd-6557-eca224f656fe.png)

くるくる発行中のプロセスが回り、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/de34e18e-8a36-3087-f701-ab7eb8f09428.png)

無事発行完了です😊
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/371773be-22f6-1ed3-3789-af6bcb676837.png)

早速見てましょう。
``Azure Portal``にログインしてみます。
先ほどの``blob2sftp.dll``で``Functions``が生成されていることがわかります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/c3db8590-5e25-45b4-a9b4-4d44583c191b.png)

ちょっとした``Azure``の**便利**機能です。
画面を見てみると、右側に``テスト``がありますね。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/6612285a-a151-7530-66eb-9e2e45286a8c.png)
そうなんです。``Http Trigger``ですので、今回作成したFunctionsは``Webhook``のになりますが、そのテストが``Portal``だけでできるんですね。``CURL``や、``Postman``でテストする必要がないですね。

##Implementation(実装)
これで実装開始です。
私個人的には実装とデバック(他人の)の仕事が大好きです。
wktkしながら実装します。


```
実装の手順は簡単ですね。
1.Azure Blobのイベント情報を取得ダウンロード
2.ダウンロードしたファイルをSFTPでアップロード
```

Blobをダウンロードのロジックをダイジェストで解説

```csharp
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
//dynamicで型が取れるのはC#は便利ですねー
dynamic data = JsonConvert.DeserializeObject(requestBody);
var data0 = data[0];

//BlobのURLを解析は少し厄介です...
var url = new Uri(data0.data.url.ToString());
var container = url.AbsolutePath.Split('/')[1];
int startIndex = "/".Length + container.Length + "/".Length;
int length = url.AbsolutePath.Length - startIndex;
var blobname = url.AbsolutePath.Substring(startIndex, length);
var blobNameArray = blobname.Split('/');
var blobFileName = blobNameArray[blobNameArray.Length - 1];
var tempPath = Path.GetTempPath();
var tempFilePath = Path.Combine(Path.GetTempPath(), blobFileName);
var accountName = url.Host.Split('.')[0];
const string accessKey = "<Blob AccessKey>";
var credential = new StorageCredentials(accountName, accessKey);
var storageAccount = new CloudStorageAccount(credential, true);
//blob
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
//container
CloudBlobContainer blobcontainer = blobClient.GetContainerReference(container);
//ダウンロードするファイル名を指定
CloudBlockBlob blockBlob_download = blobcontainer.GetBlockBlobReference(blobname);
//ダウンロード処理
//ダウンロード後のパスとファイル名を指定。
var downloadFile = $"{tempFilePath}" ;
await blockBlob_download.DownloadToFileAsync(downloadFile, System.IO.FileMode.OpenOrCreate);
log.LogInformation("blob download successful.");
```

SFTPのアップロードをダイジェクトで紹介

```csharp
//setup client
//clientを作成して
using (var client = new SftpClient("<SFTPサーバ アドレス>", "<userid>", "<password>"))
{
    client.Connect();
    // await a file upload
    using (var localStream = File.OpenRead(filePath))
    {
        client.ChangeDirectory("<アップロードパス>");
        await client.UploadAsync(localStream, $"{fileName}");
        // disconnect like you normally would
        client.Disconnect();
    }
}

```

SFTPのモジュールは.NET core標準にはなさそうなので、
を使って実装します。
https://github.com/JohnTheGr8/Renci.SshNet.Async

```powershell
PM> Install-Package Renci.SshNet.Async
```
他のNuGetを入れたとすると、
``WindowsAzure.Storage``ですね。

で完成です。
これであとはデバックです。
デバックはローカル実行してみます。
``Visula Studio``は最強の開発環境です。
すぐにデバックもできますね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/af258501-ece5-4901-71fa-bd1fb16b436b.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/c7f51d04-9d4d-b4a1-6fe4-19912545c864.png)

``Functions``がローカルにhttpで立ち上がります。お世辞抜きに素晴らしいです。
この場合は、``Postman``でPOSTしてみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/b1f007c9-d85e-b07a-a8c5-380e21f5c0ce.png)


###イベントハンドラの登録です。
実装が終わってテストが完了したら、``Blob Storage``へイベントハンドラの登録です。
ハンドラはStorageアカウントの画面から登録です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/43926e03-4bd4-749e-dfbc-884243b77beb.png)

その他のオプションからwebhookを選択
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/8a45ee16-6f29-f90d-32d1-ed7f246a3e4d.png)

エンドポイントのタイプをwebhookに選択し、エンドポイントを選択します。
イベント サブスクリプションの詳細、名前`任意`、イベントのスキーマ―は``イベント グリッド スキーマ``
サブスクライバー エンドポイントで``FunctionsのURL``を入力します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/7067c8e7-e995-0d70-cbb1-70951b960725.png)

``作成``を押下すると、
設定がはじまりますwktk
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/ba2fb7b7-f3c6-cc62-5b84-7d46121e37b6.png)

ｶﾞ━━(ﾟдﾟ；)━━ﾝ!! 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/d950cb10-f88a-5964-576f-3cc8a49d3427.png)


####ハマリポイント①
来ました。
しかも通知で出るメッセージと後から見るメッセージが違うのも何とも😥

```bash
デプロイが次のエラーで失敗しました: {"code":"Url validation","message":"The attempt to validate the provided endpoint https://blob2sftp.azurewebsites.net/api/Function1 failed. For more details, visit https://aka.ms/esvalidation."}

デプロイが次のエラーで失敗しました: {"code":"Provider Registration","message":"The Microsoft.EventGrid resource provider is not registered in subscription 11a6b8e7-8fbc-4fe3-b887-3209d285cfad. To resolve this, register the provider in the subscription and retry the operation."}
```
結局調査の過程は割愛しますが、以下のページの初めのエラーメッセージある、
https://aka.ms/esvalidation

日本語詳細サイト
https://docs.microsoft.com/ja-jp/azure/event-grid/security-authentication#validation-details
へアクセスし確認して解決できました。

#####結論
以下のwebhookは以下のリクエストのJSONを返し、検証する必要があるようです。

リクエスト

```json
[{
  "id": "2d1781af-3a4c-4d7c-bd0c-e34b19da4e66",
  "topic": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "subject": "",
  "data": {
    "validationCode": "<ここの値をオウム返しする>",
    "validationUrl": "https://rp-eastus2.eventgrid.azure.net:553/eventsubscriptions/estest/validate?id=B2E34264-7D71-453A-B5FB-B62D0FDC85EE&t=2018-04-26T20:30:54.4538837Z&apiVersion=2018-05-01-preview&token=1BNqCxBBSSE9OnNSfZM4%2b5H9zDegKMY6uJ%2fO2DFRkwQ%3d"
  },
  "eventType": "Microsoft.EventGrid.SubscriptionValidationEvent",
  "eventTime": "2018-01-25T22:12:19.4556811Z",
  "metadataVersion": "1",
  "dataVersion": "1"
}]
```
結果応答

```json
{
  "validationResponse": "<Requestから取得>"
}
```

なるほど。
一旦以下のように関数を修正して、発行してみます。

```csharp
log.LogInformation("C# HTTP trigger function processed a request.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
var data0 = data[0];
try
{
    var validationCode = data[0].data.validationCode;
    string s = validationCode.ToString();
    var r = $"{{\"validationResponse\":\"{validationCode}\"}}";
    return validationCode != null
        ? (ActionResult)new OkObjectResult(r)
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
catch (Exception e)
{
    log.LogCritical(e.Message);
}
return (ActionResult)new OkObjectResult("");
```

ｶﾞ━━(ﾟдﾟ；)━━ﾝ!! 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/3c5ea76e-d877-d20c-8539-fadb9e31f815.png)

####ハマリポイント②

```
C:\Program Files\dotnet\sdk\2.1.402\Sdks\Microsoft.NET.Sdk.Publish\build\netstandard1.0\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeploy.targets(139,5): error : Web deployment task failed. (発行先にあるファイル 'blob2sftp.dll' は、外部プロセスによってロックされているため変更できません。発行操作を正常に完了するには、アプリケーションを再起動してロックを解除するか、.Net アプリケーションに対する AppOffline 規則ハンドラーを次回の発行時に使用する必要があります。  詳細情報の参照先: http://go.microsoft.com/fwlink/?LinkId=221672#ERROR_FILE_IN_USE) [C:\Users\shtsukam\source\repos\blob2sftp\blob2sftp\blob2sftp.csproj]
  Publish failed to deploy.
```

無論、これくらいで挫けてはいけないです。
ちゃんと調べると載ってます。
https://github.com/projectkudu/kudu/wiki/Dealing-with-locked-files-during-deployment#for-msdeploy-there-is-another-option-to-rename-locked-files

アプリケーション設定に以下の設定が必要でした。
```MSDEPLOY_RENAME_LOCKED_FILES=1```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/982c0f71-c33a-b9cf-af8a-2cce4fde0807.png)


#####気を取り直して発行
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/8da95c47-e99b-d990-d8dc-21ba0ece8a22.png)

無事完了

#####改めてイベント登録

完了
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/8496f09e-cd74-df94-e20d-a76abefc75fb.png)

###blob2sftpのロジックを張り付ける

```csharp
[FunctionName("Function1")]
public static async Task<IActionResult> updatesftp(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    var data0 = data[0];
    try
    {
        var url = new Uri(data0.data.url.ToString());
        var container = url.AbsolutePath.Split('/')[1];
        int startIndex = "/".Length + container.Length + "/".Length;
        int length = url.AbsolutePath.Length - startIndex;
        var blobname = url.AbsolutePath.Substring(startIndex, length);
        var blobNameArray = blobname.Split('/');
        var blobFileName = blobNameArray[blobNameArray.Length - 1];
        var tempPath = Path.GetTempPath();
        var tempFilePath = Path.Combine(Path.GetTempPath(), blobFileName);
        //blob
        CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
        //container
        var containername = url.Host.Split('.')[0];
        CloudBlobContainer blobcontainer = blobClient.GetContainerReference(containername);
        //ダウンロードするファイル名を指定
        CloudBlockBlob blockBlob_download = blobcontainer.GetBlockBlobReference(blobname);
        //ダウンロード処理
        //ダウンロード後のパスとファイル名を指定。
        var downloadFile = $"{tempFilePath}" ;
        await blockBlob_download.DownloadToFileAsync(downloadFile, System.IO.FileMode.OpenOrCreate);
        log.LogInformation("blob download successful.");
        
        await UploadSFTP($"{downloadFile}", blobFileName, log);
        File.Delete(downloadFile);
        log.LogInformation("sftp upload successful.");
    }
    catch (Exception e)
    {
        log.LogCritical(e.Message);
    }
    return (ActionResult)new OkObjectResult("");
}
private static async Task UploadSFTP(string filePath, string fileName, ILogger log)
{
    //setup client
    using (var client = new SftpClient("<ホストIP or FQDN>", "<ユーザ>", "<パスワード>"))
    {
        client.Connect();
        // await a file upload
        using (var localStream = File.OpenRead(filePath))
        {
            client.ChangeDirectory("/home/ubuntu/sftp");
            await client.UploadAsync(localStream, $"{fileName}");
            // disconnect like you normally would
            client.Disconnect();
        }
    }
}
```
発行

###試験

####Blobストレージの状態
空っぽですね
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/17dcea03-1ff1-22ff-6baa-df0d7aabfdab.png)

####SFTPサーバの状態
こちらも空っぽ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/13c4fea0-6442-a0cc-2e29-1b271fb5bfa2.png)

####Blobへファイルをアップロード
testfile1.txtをアップロードしました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/ace70217-142d-3ea8-bc9c-bbec11675ce8.png)

SFTPのサーバの状態
ヮ─ヾ(#＾∀＾#)ﾉ─ｨ☆彡
アップロードできました🤣
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167352/3d7fe7ea-8827-a39d-4eb8-dc221ab90372.png)


##まとめ
Functionsを使ってファイル連携のプログラムを作りました。
I/FとしてBlobは大変活用しやすいオブジェクトストレージですね。
Blob+EventGrid+Functionsを使ったサーバーレスシステム間連携は簡単に実現可能です。