AzureBlobStorageからsftpを使ってファイル転送
こちらの記事は、Qiita に掲載した Microsoft Azure Tech Advent Calendar 2018 の企画に基づき、執筆した 内容となります。(2 日目)
Azure Functionsを使ってAzure BlobStorageのアップロードしたファイルを直ちにSFTPサーバーへアップロードするコードを書いてみました。
勿論、最新版の__Functions 2.0__C#
を使って書きますよ。
##Functionsのプロジェクト作成
###PortalへFunctionsをデプロイ
ポチポチとFunctions作りますよ。
Function
と入力して、
Functions App
を選択
立ち上がりました。簡単ですね。
ここでこのままVisula Editor
で作成もできますが、
今回は地味ですが、BlobからダウンロードしたファイルをSFTPで別のサーバーにアップロードするプログラムなので、Visual Studio
で作成してみます。
####Visual Studio
でFunctions
を作成
新規作成->プロジェクト
Azure Functions V2
、Http trigger
を選択、
先ほどAzure
で作成した、Functions
が選択できます。
早速見てましょう。
Azure Portal
にログインしてみます。
先ほどのblob2sftp.dll
でFunctions
が生成されていることがわかります。
ちょっとしたAzure
の便利機能です。
画面を見てみると、右側にテスト
がありますね。
そうなんです。Http Trigger
ですので、今回作成したFunctionsはWebhook
のになりますが、そのテストがPortal
だけでできるんですね。CURL
や、Postman
でテストする必要がないですね。
##Implementation(実装)
これで実装開始です。
私個人的には実装とデバック(他人の)の仕事が大好きです。
wktkしながら実装します。
実装の手順は簡単ですね。
1.Azure Blobのイベント情報を取得ダウンロード
2.ダウンロードしたファイルをSFTPでアップロード
Blobをダウンロードのロジックをダイジェストで解説
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のアップロードをダイジェクトで紹介
//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
PM> Install-Package Renci.SshNet.Async
他のNuGetを入れたとすると、
WindowsAzure.Storage
ですね。
で完成です。
これであとはデバックです。
デバックはローカル実行してみます。
Visula Studio
は最強の開発環境です。
すぐにデバックもできますね。
Functions
がローカルにhttpで立ち上がります。お世辞抜きに素晴らしいです。
この場合は、Postman
でPOSTしてみましょう。
###イベントハンドラの登録です。
実装が終わってテストが完了したら、Blob Storage
へイベントハンドラの登録です。
ハンドラはStorageアカウントの画面から登録です。
エンドポイントのタイプをwebhookに選択し、エンドポイントを選択します。
イベント サブスクリプションの詳細、名前任意
、イベントのスキーマ―はイベント グリッド スキーマ
サブスクライバー エンドポイントでFunctionsのURL
を入力します。
####ハマリポイント①
来ました。
しかも通知で出るメッセージと後から見るメッセージが違うのも何とも😥
デプロイが次のエラーで失敗しました: {"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を返し、検証する必要があるようです。
リクエスト
[{
"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"
}]
結果応答
{
"validationResponse": "<Requestから取得>"
}
なるほど。
一旦以下のように関数を修正して、発行してみます。
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("");
####ハマリポイント②
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
無事完了
#####改めてイベント登録
###blob2sftpのロジックを張り付ける
[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へファイルをアップロード
testfile1.txtをアップロードしました。
SFTPのサーバの状態
ヮ─ヾ(#^∀^#)ノ─ィ☆彡
アップロードできました🤣
##まとめ
Functionsを使ってファイル連携のプログラムを作りました。
I/FとしてBlobは大変活用しやすいオブジェクトストレージですね。
Blob+EventGrid+Functionsを使ったサーバーレスシステム間連携は簡単に実現可能です。