データサイエンティストでもできるサーバーレスなWebフォーム開発
見出し画像

データサイエンティストでもできるサーバーレスなWebフォーム開発

電通デジタルで機械学習エンジニアをしている今井です。
前回の記事は「GoogleColabで統計的因果探索手法BMLiNGAMを動かしてみた」でした。
Advent Calendar 14日目となる本記事では、AWSでサーバーレスなWebフォーム開発を行うための方法について紹介します。

データサイエンティストの日常

データサイエンティスト業務に従事してる多くの方は以下のような経験をしたことがあるのではないでしょうか?

図1

依頼者がエンジニア職であれば、作成したテンプレートクエリなどを渡して「パラメータ部分を適宜修正して使ってください」というコミュニケーションで完結しますが、大抵の場合は非エンジニア職のため都度手を動かす必要があります。

こんなとき、パラメータ入力用のWebフォームを作成すれば依頼者の入力に応じて自動集計されるようなシステムがあればいいなと思うことでしょう。

入力用UIだけであればGoogleフォームなどで簡単に用意できるかもしれませんが、入力に応じたパラメータ置換や自動集計をシステムで組むには相応のバックエンド開発が必要となります。
また仮に入力フォーマットとして単純なテキストや選択肢だけでなく、Excelのような表形式データなども使えるようにするにはフロントエンド開発も必要となるでしょう。

多くのデータサイエンティストはPythonに慣れていることからDjangoやFlaskを使って開発できないかと考え、「フロントエンドとバックエンドをDjango/ Flaskで実装し、動かすサーバーを用意し、アプリケーションをデプロイし、...」と意外と開発が大変そうだとなり、気がつけば手動での集計を続けてしまっている、というのはあるある話かと思います。

そこで、本記事ではAWSの各種サービスと必要最低限のPython, HTML, JavaScriptを使用してサーバーレスなWebフォーム開発を行うための方法を紹介します。

S3 static website hostingによるフロントエンド開発

はじめにS3 static website hostingを使用したフロントエンド開発について紹介します。

static website hostingはHTMLファイルをS3バケットにアップロードするだけで静的なWebページを公開できるサービスです。
はじめに次のようなHTMLファイル(index.html)を用意します。

<html>
   <head>
       <meta charset="utf-8"/>
       <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
   </head>
   <body>
       <a href="https://note.com/dd_techblog">Dentsu Digital Tech Blog</a>
   </body>
</html>

次にS3バケットを作成し、オブジェクトタブから index.html を選択してアップロードします。
アップロードが完了したら、index.html をクリックし、オブジェクトアクション>公開するを選択します。​

スクリーンショット 2020-11-13 14.42.02

次に、プロパティタブ>静的ウェブサイトホスティングを編集する>有効にすると遷移し、インデックスドキュメントに index.html を入力します。

スクリーンショット 2020-11-13 14.45.11

最後にアクセス許可タブ>パブリックアクセスをすべてブロックのチェックを外すと、https://bucket-name.s3-region.amazonaws.com/index.html にWebページが公開されます。

スクリーンショット 2020-11-16 18.13.25

次に、index.html にJavaScript(本記事ではVueJSを使用)を追記し、バックエンドとデータの受け渡しを行う方法を紹介します。
まずはVueJSを使うために index.html を次のように修正します。

<body>
   <div id="app">
       <a href="https://note.com/dd_techblog">Dentsu Digital Tech Blog</a>
   </div>

   <script src="https://unpkg.com/vue"></script>
   <script>
       new Vue({
           el: "#app"
       })
   </script>
</body>

次にバックエンドに渡すパラメータ値(ここでは"message")を入力できるようにします。

<div id="app">
   <div class="form-group text-left">
       <label for="message">メッセージを入力してください</label>
       <input class="form-control" type="text" v-model="message">
   </div>
   入力されたメッセージは {{ message }} です。
</div>

<script src="https://unpkg.com/vue"></script>
<script>
   new Vue({
       el: "#app",
       data: {
           message: null
       }
   })
</script>

スクリーンショット 2020-11-16 18.22.16

次に登録ボタンを押したらバックエンドにパラメータ値を送るようにします。
https://api-gateway-endpoint.execute-api.region.amazonaws.com/api/ については後ほど説明します。

<div id="app">
   <div class="form-group text-left">
       <label for="message">メッセージを入力してください</label>
       <input class="form-control" type="text" v-model="message">
   </div>

   <div class="form-group">
     <button class="form-control btn-primary" v-on:click="registerParams">登録</button>
   </div>
</div>

<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
   new Vue({
       el: "#app",
       data: {
           message: null
       },
       methods: {
           registerParams() {
               var params = new URLSearchParams()
               params.append("message", this.message)
               axios.post("https://api-gateway-endpoint.execute-api.region.amazonaws.com/api/register", params)
               .then(response => {
                   alert(response.data)
               })
           }
       }
   })
</script>

スクリーンショット 2020-11-16 18.45.40

このようにHTMLとして入力項目を作成し、それらをparams.append(key, value)に追加ことで任意のパラメータ値をバックエンドに送ることができます。
また、次のようにすることで登録後に入力値をClearすることもできます。

methods: {
   registerParams() {
       var params = new URLSearchParams()
       params.append("message", this.message)
       axios.post("https://api-gateway-endpoint.execute-api.region.amazonaws.com/api/register", params)
       .then(response => {
           alert(response.data)
           this.reset()
       })
   },
   reset() {
       this.message = null
   }
}

Chaliceによるバックエンド開発

次にChaliceを使用したバックエンド開発について紹介します。

ChaliceはAPI GatewayとLambdaを組み合わせたサーバーレス環境を自動で構築できるサービスです。
Chaliceのインストール方法などについてはこちらのGitHubリポジトリを参照ください。

はじめにChalice用のプロジェクト(techblog-sample)を作成します。

$ chalice new-project techblog-sample

を実行すると、以下のようなフォルダが作成されます。
techblog-sample/
├ .chalice
├ app.py
└ requirements.txt

app.pyは次のようになっています。

from chalice import Chalice
app = Chalice(app_name='techblog-sample')
@app.route('/')
def index():
   return {'hello': 'world'}

次に、以下のコマンドで techblog-sample をAWSにデプロイします。

$ cd techblog-sample/
$ chalice deploy

実行が無事終わると、下記のようなログが出力されます。
このログのRest API URLが index.html でpostするためのURLとなります。

Creating deployment package.
Creating IAM role: techblog-sample-dev
Creating lambda function: techblog-sample-dev
Creating Rest API
Resources deployed:
 - Lambda ARN: arn:aws:lambda:region:account_id:function:techblog-sample-dev
 - Rest API URL: https://api-gateway-endpoint.execute-api.region.amazonaws.com/api/

Lambdaにアクセスし、API Gatewayがトリガーされたtechblog-sample-dev関数がデプロイされているのを確認します。

スクリーンショット 2020-11-17 18.46.00

次にWebフォームで入力されたパラメータを受け取るための処理をapp.pyに追記します。

from urllib.parse import parse_qs
@app.route('/register', methods=['POST'], content_types=['application/x-www-form-urlencoded'], cors=True)
def register():
   parsed = parse_qs(app.current_request.raw_body.decode())
   print(parsed)
   return 'メッセージ「{}」が登録されました'.format(parsed['message'][0])

app.pyを修正し $ chalice deploy を実行すると、API GatewayとLambdaに自動で反映されます。
また、もし必要なPythonライブラリがあれば、requirements.txtに追加しておくことで自動でpip installが実行されます。

@app.routeを '/register' とすることで、https://api-gateway-endpoint.execute-api.region.amazonaws.com/api/register のURLにpostできるようになります。
また、print(parsed) によりCloudWatchで受け取ったパラメータ一覧を確認できます。

スクリーンショット 2020-11-17 19.00.58

parsedで入力パラメータを抽出できるので、あとはよしなにクエリを置換したり実行したりという処理を追記すれば、登録ボタンをトリガーにした自動集計が可能になります。

付録. Excelファイルを入力フォームに使用する方法

次のようなExcelファイルを抽出するための index.html は以下のようになります。

スクリーンショット 2020-11-17 19.29.13

<body>
   <div id="app">
       <div class="form-group text-left">
           <label for="excel_input">Excelファイル</label>
           <input type="text" class="form-control" v-model="excel_input" placeholder="ファイルを選択してください" :disabled="true">
           <input type="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" v-on:change="uploadExcel($event)">
       </div>
   </div>

   <script src="https://unpkg.com/vue"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.14.1/xlsx.full.min.js"></script>
   <script>
       new Vue({
           el: "#app",
           data: {
               excel_input: []
           },
           methods: {
               uploadExcel(event, element) {
                   var input = event.target
                   var reader = new FileReader()
                   reader.readAsBinaryString(input.files[0])
                   reader.onload = () => {
                       var file_data = reader.result
                       var wb = XLSX.read(file_data, {type: "binary"})
                       var row_obj = XLSX.utils.sheet_to_json(wb.Sheets["target_sheet_name"])
                       for (let i in row_obj) {
                           this.excel_input.push(JSON.stringify({
                               "number": row_obj[i]["number"],
                               "english": row_obj[i]["english"]
                           }))
                       }
                   }
               }
           }
       })
   </script>
</body>

スクリーンショット 2020-11-18 10.57.22

次回の記事は「電通デジタル社内システム「EASI」のデザイン原則を公開します」です。よろしくお願いします。