AzureBlobStorageからsftpを使ってファイル転送
こちらの記事は、Qiita に掲載した Microsoft Azure Tech Advent Calendar 2018 の企画に基づき、執筆した 内容となります。(2 日目)
Azure Functionsを使ってAzure BlobStorageのアップロードしたファイルを直ちにSFTPサーバーへアップロードするコードを書いてみました。
勿論、最新版のFunctions 2.0C#
を使って書きますよ。
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ストレージの状態
SFTPサーバの状態
Blobへファイルをアップロード
SFTPのサーバの状態
ヮ─ヾ(#^∀^#)ノ─ィ☆彡
アップロードできました🤣
まとめ
Functionsを使ってファイル連携のプログラムを作りました。
I/FとしてBlobは大変活用しやすいオブジェクトストレージですね。
Blob+EventGrid+Functionsを使ったサーバーレスシステム間連携は簡単に実現可能です。