6
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-02

AzureBlobStorageからsftpを使ってファイル転送

こちらの記事は、Qiita に掲載した Microsoft Azure Tech Advent Calendar 2018 の企画に基づき、執筆した 内容となります。(2 日目)

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

Functionsのプロジェクト作成

PortalへFunctionsをデプロイ

ポチポチとFunctions作りますよ。

まずはここから、

image.png

Functionと入力して、
image.png
Functions Appを選択

image.png
作成を押下

リージョン,ランタイムを選んでポチっと
image.png

立ち上がりました。簡単ですね。
image.png
ここでこのままVisula Editorで作成もできますが、
今回は地味ですが、BlobからダウンロードしたファイルをSFTPで別のサーバーにアップロードするプログラムなので、Visual Studioで作成してみます。

Visual StudioFunctionsを作成

新規作成->プロジェクト
image.png

Azure Functionsを選択
image.png

Azure Functions V2Http triggerを選択、
image.png

ストレージアカウントは任意のものを選択します。
image.png

作成完了!
image.png

まずはデプロイしてみます。
発行を押下し、
image.png

既存のものを選択発行を押下する。
image.png

先ほどAzureで作成した、Functionsが選択できます。
image.png

くるくる発行中のプロセスが回り、
image.png

無事発行完了です😊
image.png

早速見てましょう。
Azure Portalにログインしてみます。
先ほどのblob2sftp.dllFunctionsが生成されていることがわかります。
image.png

ちょっとしたAzure便利機能です。
画面を見てみると、右側にテストがありますね。
image.png
そうなんです。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は最強の開発環境です。
すぐにデバックもできますね。

image.png

image.png

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

image.png

イベントハンドラの登録です。

実装が終わってテストが完了したら、Blob Storageへイベントハンドラの登録です。
ハンドラはStorageアカウントの画面から登録です。
image.png

その他のオプションからwebhookを選択
image.png

エンドポイントのタイプをwebhookに選択し、エンドポイントを選択します。
イベント サブスクリプションの詳細、名前任意、イベントのスキーマ―はイベント グリッド スキーマ
サブスクライバー エンドポイントでFunctionsのURLを入力します。
image.png

作成を押下すると、
設定がはじまりますwktk
image.png

ガ━━(゚д゚;)━━ン!!
image.png

ハマリポイント①

来ました。
しかも通知で出るメッセージと後から見るメッセージが違うのも何とも😥

デプロイが次のエラーで失敗しました: {"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("");

ガ━━(゚д゚;)━━ン!!
image.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

気を取り直して発行

image.png

無事完了

改めてイベント登録

完了
image.png

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ストレージの状態

空っぽですね
image.png

SFTPサーバの状態

こちらも空っぽ
image.png

Blobへファイルをアップロード

testfile1.txtをアップロードしました。
image.png

SFTPのサーバの状態
ヮ─ヾ(#^∀^#)ノ─ィ☆彡
アップロードできました🤣
image.png

まとめ

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

6
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5