あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

React+Amplify+Cognito認証のアプリでExcelをS3にアップロードしLambda(C#)で読み込む

はじめに

前の記事で、React+Amplify+Cognito認証のアプリで画像ファイルをS3にアップロードするまで行いました。

www.aruse.net

この記事では、Excel ファイルを S3 にアップロードし、S3 のトリガーで Lambda 関数(C#)を起動し、Excel ファイルを読み込んだ結果をログに出力するまでを行います。

前の記事の続きになるので、Amplify、Cognito、S3 などの環境設定については前の記事を参考にしてください。

Lambda 関数(C#)を作成

Visual Studio 2019 Community Edition に AWS Toolkit for Visual Studio をインストールして、S3 のテンプレートを使用して雛形を作成します。

そして、NPOI を Nuget よりインストールします。

C# で Excel を読み込むライブラリについては、以下の記事を参考にしてください。

www.aruse.net

実際に Excel を読み込む Lambda 関数は以下のようになります。S3 のイベントで呼び出され、S3Event に必要な情報が渡されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using NPOI.SS.UserModel;
using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Util;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AWSLambda1
{
  public class Function
  {
    IAmazonS3 S3Client { get; set; }

    public Function()
    {
      S3Client = new AmazonS3Client();
    }

    public Function(IAmazonS3 s3Client)
    {
      this.S3Client = s3Client;
    }
    
    // S3 のイベントで呼び出される関数
    public async Task FunctionHandler(S3Event evnt, ILambdaContext context)
    {
      var s3Event = evnt.Records?[0].S3;
      if(s3Event == null)
      {
        return;
      }

      try
      {
        // S3 ファイルを取得する。s3Eventにバケット名とオブジェクトキーが含まれている。
        var res = await this.S3Client.GetObjectAsync(s3Event.Bucket.Name, s3Event.Object.Key);

        // 日時をファイル名につけるの重要!
        var filePath = "/tmp/" + (DateTime.Now).ToString("yyyyMMdd_HHmmss_") + s3Event.Object.Key;
        await res.WriteResponseStreamToFileAsync(filePath, false, System.Threading.CancellationToken.None);

        // Create で xls,xlsx に対応
        IWorkbook workbook = WorkbookFactory.Create(filePath);
        ISheet worksheet = workbook.GetSheetAt(0);

        // 最終行数を取得
        int lastRow = worksheet.LastRowNum;

        for (int i = 0; i <= lastRow; i++)
        {
          // 行取得
          IRow row = worksheet.GetRow(i);

          // セルの値取得
          ICell cell1 = row?.GetCell(0);
          ICell cell2 = row?.GetCell(1);

          // ログに出力
          LambdaLogger.Log(cell1?.StringCellValue + "," + (cell2?.NumericCellValue).ToString());
        }
        
        // Excel クローズ
        workbook.Close();

        // ファイル削除
        File.Delete(filePath);
      
      }
      catch(Exception e)
      {
        context.Logger.LogLine($"Error getting object {s3Event.Object.Key} from bucket {s3Event.Bucket.Name}. Make sure they exist and your bucket is in the same region as this function.");
        context.Logger.LogLine(e.Message);
        context.Logger.LogLine(e.StackTrace);
        throw;
      }
    }
  }
}

基本的にはコメントを付けたとおりなのですが、1点補足しておきます。

S3 のファイルを /tmp にコピーしてきていますが、これは S3 ファイルを直接プログラムで読み込むことができないので、/tmp に一時的にコピーしています。なお、この/tmp は上限が512MBになっているのでご注意ください。

今回は NPOI を Nuget でインストールしたため、Zip ファイルで Lambda 関数をアップロードする必要があります。詳しくは、以下の記事を参考にしてください。

www.aruse.net

なお、Lambda 関数の実行ロールには、S3 と CloudWatch へのアクセス権限が必要なので、IAM から権限付与しておいてください。

Lambda 関数に S3 イベントトリガーを設定する

Lambda 関数ができあがったので、イベントトリガーとして S3 のイベントトリガーを設定します。

Lambda 関数の画面で、S3 のバケットの PUT イベントをトリガーとして追加します。トリガーも有効化します。

f:id:fnyablog:20190715084526p:plain:w480

React の実装

前の記事では、画像ファイルをアップロードするのでPhotoPickerを使用しましたが、今度は Excel ファイルをアップロードするのでDropzoneというライブラリを使用します。

下記のように、プロジェクトにインストールします。

$ yarn add react-dropzone @types/react-dropzone

App.tsxは以下のようになります。

import React from 'react';
import { withAuthenticator } from "aws-amplify-react";
import { Storage, Auth } from 'aws-amplify';
import Dropzone from 'react-dropzone';
import './App.css';

const App: React.FC = () => {

  const putFiles = (files : any[]) => {
    for (const file of files) {
      Storage.put(file.name, file,{
        contentType: file.type
      })
      .then (result => console.log(result)) 
      .catch(err => console.log(err));
    }
  }

  return (
    <>
      <button onClick={() => Auth.signOut()}>サインアウト</button>

      <Dropzone onDrop={putFiles}>
        {({getRootProps, getInputProps}) => (
          <section>
            <div className="fileArea" {...getRootProps()}>
              <input {...getInputProps()} />
              <p>Drag 'n' drop some files here, or click to select files</p>
            </div>
          </section>
        )}
      </Dropzone>
  </>
  );
}

export default withAuthenticator(App);

App.cssは以下のようになります。

.App {
  text-align: center;
}

.fileArea {
  width: 100%;
  height: 200px;
  background-color: skyblue;
}

動作させてみる

今回の Excel ファイルは以下のようなデータが入っているものになります。

f:id:fnyablog:20190715090225p:plain:w240

では、React アプリを起動します。

$ yarn start

以下の画面が表示されるので、Excel ファイルをドラッグ&ドロップしてアップロードします。

f:id:fnyablog:20190715090319p:plain:w480

CloudWatch のログを確認すると、以下のように S3 のイベントから Lambda 関数が起動して、想定したデータが出力されていることがわかります。

f:id:fnyablog:20190715090638p:plain:w480

デプロイ先でも動作させてみる

念のため、自動デプロイを行ってみて動作するかも確認します。

リモートリポジトリに変更を反映させます。

$ git add .
$ git commit -m "Add excel feature."
$ git push origin master

反映ができたようなので、Excel ファイルをアップロードしてみます。

f:id:fnyablog:20190715091532p:plain:w480

正しくログが出力されました。

f:id:fnyablog:20190715091810p:plain:w480

おわりに

React+Amplify+Cognito認証のアプリで Excel ファイルを S3 にアップロードして Lambda(C#) で読み込んで、CloudWatch にログを出力することができました。

実際にはかなり苦戦しましたが、できるようになってよかったです。

参考になれば幸いです。