20200725のNode.jsに関する記事は5件です。

serverlessを使ってLambdaにmultipart/form-dataでバイナリデータをアップロードする

serverlessを使ってLambdaをデプロイする際、Lambda統合の時にバイナリデータをどうやって送るのか地味にハマったのでメモとして残しておきます。
(たぶんうちの若い子達がココ見るはず..)

serverless便利ですね。コマンド一発でデプロイから各種AWSリソースをいい感じにセットアップしてくれます。いらなくなったら同じくコマンド一発でまるっと削除してくれます。これを使わない手はないですね。

TL;DR

  • serverless.ymlのcustomキー配下にapiBinaryでバイナリメディアタイプを指定する
  • serverless.ymlのpluginsキー配下にserverless-apigw-binaryを追加する
  • Lambdaに送られてくるリクエストはbase64エンコードされたmultipart/form-dataなのでデコードしてaws-lambda-multipart-parserでマルチパートデータをバイナリにする
serverless.yml
custom:
  ...省略
  apigwBinary:
    types:
      - multipart/form-data
...省略
plugins:
  - serverless-apigw-binary

serverless.yml

serverless.ymlの記載内容をまとめると以下のようになるかと思います。

  • S3バケットの設定
  • Lambdaの設定
  • APIGatewayの設定
serverless.yml
service:
  name: FileUploadTest

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true
  apigwBinary:
    types:
      - multipart/form-data    # ← バイナリメディアタイプの指定
  webSiteName: jp.co.onewedge.test.fileuploadtest
  s3Sync:
    - bucketName: ${self:custom.webSiteName}
      localDir: static         # ← プロジェクトのstaticフォルダ配下のファイルをS3にアップロード

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack
  - serverless-apigw-binary    # ← APIGatewayのバイナリメディアタイプでアップロードするために必要
  - serverless-s3-sync         # ← S3に静的ファイルをアップロードするために必要
provider:
  name: aws
  runtime: nodejs12.x
  region: us-east-1
  endpointType: REGIONAL
  apiGateway:
    minimumCompressionSize: 1024 
  environment:
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1

functions:
  fileUpload:
    handler: index.handler
    events:
      - http:
          method: post
          path: doUpload
          cors: false
resources:
  Resources:                   # ←S3バケットでHTMLを配信するための設定
    StaticSite:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.webSiteName}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
          ErrorDocument: error.html
    StaticSiteS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: StaticSite
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
              - s3:GetObject
              Resource:
                Fn::Join: ["", ["arn:aws:s3:::",{"Ref": "StaticSite"},"/*"]]

multipart/form-dataでバイナリデータをアップロードする場合、キモとなるのはAWSコンソールのバイナリメディアタイプとなります。serverlessを利用する場合はserverless-apigw-binaryプラグインの導入と、customキー配下apigwBinaryキーの指定となります。このtypesで指定した値がバイナリメディアタイプとなります。

serverless.yml
custom:
  ...省略
  apigwBinary:
    types:
      - multipart/form-data
...省略
plugins:
  - serverless-apigw-binary

apigateway.png

Lambda実装

Labmda側は、通常のLambda統合のリクエストでデータを取得できます。
この際、event.bodyにはbase64エンコードされたmultipart/form-dataが格納されています。
つまり、そのままでは利用できないので受け取ったevent.bodyはデコードしたあとmultipartの処理を行います。

index.ts
'use strict';
import { APIGatewayProxyHandler, APIGatewayProxyEvent, Context} from 'aws-lambda';
import multipart from 'aws-lambda-multipart-parser';
import { S3 } from 'aws-sdk';
import 'source-map-support/register';


export const handler: APIGatewayProxyHandler = async (event:APIGatewayProxyEvent, _context:Context) => {

  // 受け取ったevent.bodyがbase64エンコードされているのでデコード
  const event2:APIGatewayProxyEvent = event;
  event2.body = Buffer.from(event.body, 'base64').toString('binary');

  // multipart/form-dataをパースする
  const multipartBuffer = multipart.parse(event2, true);

  // クライアント側で特に指定なき場合、アップロードされたファイルはfile.contentとして取り出せる
  // ここではアップロードされたファイルをS3に保管します
  const s3 = new S3();
  const params:S3.Types.PutObjectRequest = {
    Bucket:'jp.co.onewedge.test.fileuploadtest',
    Key: '保存ファイル名',
    Body: multipartBuffer.file.content
  };
  await s3.putObject(params).promise();

  // ... 省略
}

デプロイ

ここまでできたらコマンドラインからいったんデプロイしましょう。
デプロイするとAPIGatewayのエンドポイントがコンソールに表示されたログの中のendpoints欄に表示されるので、メモっておきます。

$ sls deploy
...省略
Service Information
service: FileUploadTest
stage: dev
region: us-east-1
stack: FileUploadTest-dev
resources: xx
api keys:
  None
endpoints:
  POST - https://xxxx.execute-api.us-east-1.amazonaws.com/dev/doUpload  # ←これです
functions:
  fileUpload: fileUpload-dev-doUpload
layers:
  None
...省略

HTML作成

HTMLを作成し、先ほどメモったAPIのエンドポイントに向けてファイルをPostするようにしてください。今回はVue.jsとAxiosで作りました。

index.html
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <div id="app">
      <form >
        <input type="file" id="file" ref="file" @change="handledUploadFile" />
        <button type="button" @click="onClickUpload">アップロード</button>
      </form>
    </div>
    <script type="text/javascript">
      const v = new Vue({
        el: '#app',
        data: function(){
          return {
            file:''
          }
        },
        methods:{
          handledUploadFile: function(){
            this.file = this.$refs.file.files[0];
          },
          onClickUpload: function(){
            const formData = new FormData();
            formData.append('file', this.file);
            axios.post('メモったエンドポイントURL',
              formData,
              {
                headers: {
                  'Content-Type': 'multipart/form-data'
                }
              }
            ).then(function(data){
              console.log(`SUCCESS!!:${data}`);
            }).catch(function(err){
              console.log(`FAILURE!!${err}`);
            });
          }
        }
      })
    </script>
  </body>
</html>

作成したHTMLはstaticディレクトリに配置します。
配置したら再度デプロイしましょう。今度は作成したHTMLがS3にアップロードされるはずです

$ sls deploy

だいぶ端折りましたがこんな感じで動くかと思います。

仲間募集!

株式会社ONE WEDGEでは元気なエンジニア募集中です!一緒に「おもしろい」を作りましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS (TS) で CLI を作る要点メモ

概要

Node.js で CLI を作ったので備忘録として作り方の要点をメモします。
npm 等には公開せず、ローカル環境での実行のみを想定しています。

環境

Node.js 12.13.0

要点

package.jsonbin を指定

コマンドラインから実行するコマンドと、コマンドを実行した際に実行されるスクリプトファイルを指定。

package.json
{
  "bin": {
    "my-cli-name": "bin/cli.js"
  }
}

bin で指定したスクリプトを作成

シェバンで node を指定して、実行したい処理を記述します。

bin/cli.js
#!/usr/bin/env node

require('../dist/app.js');

その他作法

  • プロジェクト内の JSON 等静的テキストファイルは require で読み込むようにする。
    • path.resolve を使うとバンドルされた後や bin ディレクトリからの実行時に __dirname の挙動が変わる。(10割勉強不足)
    • 静的ファイルは require でスクリプト内に埋め込んでしまうのが、時間が無い時は学習コスト掛からず手っ取り早い。
    • テンポラリファイル等の置き場所の解決はどうするのが良いのかまだ判断ついてない。

作ったコマンドのインストール

# プロジェクトのディレクトリで
npm link
# または
npm install -g .

振り返り

cli で動くプログラムをまともに作ったのが初めてだったのだけど、
今回シングルプロセス/シングルスレッド + 同期 I/O で作っちゃったので処理速度が結構遅くなりました。
非同期 I/O と Worker Threads を覚えるとよさそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クライアントアプリから Google Cloud Speech-to-Text を使ってみた

WEBアプリの開発をしていて「ユーザの音声から文字を起こしたい」つまりブラウザで音声認識したいという要件で、いろいろ調べものしたときの備忘メモ。

TL;DR

  • どの環境、どのブラウザでも音声認識を動作するように作るのはなかなか難しい
  • ブラウザのみで完結する音声認識API「Web Speech API」のSpeechRecognition は、Chromeなどかなり限定的なブラウザのみでしか動作しないが、かなりお手軽
  • Google Speech-to-Text ライブラリは、WEB(JavaScript)から利用できるかよく分からない。Google謹製のデモ見ると利用できるように見えるんだけど。→ RESTインタフェースをコールすることはできる、ことは確認済み
  • 最終的には

は確認できました。

今回のコンテンツ

今回は Google Speech-to-Text をクライアントアプリから利用してみるところまで、の備忘です。
Google Speech-to-Textは、ココにデモがありますが、音声ファイルをアップロードして認識してもらったり、ブラウザからマイクを利用して音声認識などができるサービスです1

前提や環境

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G73

% node --version
v10.19.0

% firebase --version
8.6.0
% 

今回は音声データの変換などはMacでやっていますが、プログラム自体はWindowsでも動くと思います。

事前作業

% git clone https://github.com/masatomix/speech_node_samples.git
Cloning into 'speech_node_samples'...
Resolving deltas: 100% (75/75), done.

% cd speech_node_samples/sample_ts 
% ls -lrt
total 1944
-rw-r--r--    1 masatomix  staff    5919  7 17 21:26 tsconfig.json
-rw-r--r--    1 masatomix  staff     890  7 24 01:25 package.json
-rw-r--r--    1 masatomix  staff  109687  7 24 01:25 package-lock.json
drwxr-xr-x    6 masatomix  staff     192  7 24 11:41 src
-rw-r--r--@   1 masatomix  staff    7090  7 24 20:56 README.md
% npm install
...
% 
  • ココ を参考に サービスアカウントファイル firebase-adminsdk.jsonを取得し、上記の場所に配置します。
  • Google Cloud Platform のクイックスタート の冒頭を参考に、Cloud Speech-to-Text API を有効にします。2

Google Speech-to-Text をNode.jsのクライアントアプリケーションから実行する

ライブラリを利用する

すべてのクイックスタートにある「クライアント ライブラリの使用」 をやってみましょう。
まずは音声ファイルを準備します。今回はiPhoneのボイスメモで取得した音声ファイル(sample.m4a)を、Mac上のコンバータafconvert でwavファイルに変換しました。

参考: https://tsukada.sumito.jp/2019/06/11/google-speech-api-japanese/

Windowsをご利用の方は、適宜音声ファイルをご準備ください:-)

% ls -lrt
... 省略
-rw-r--r--@   1 masatomix  staff   39898  7 24 13:31 sample.m4a  ← iPhoneのボイスメモで作成したファイル
% 
% afconvert -f WAVE -d LEI16 sample.m4a sample.wav
% ls -lrt
... 省略
-rw-r--r--@   1 masatomix  staff   39898  7 24 13:31 sample.m4a
-rw-r--r--    1 masatomix  staff  420274  7 24 14:02 sample.wav  ← 変換できた
% 

音声ファイルの変換ができました。以上で、最終的な構成は以下のようになりました。

% ls -lrt
total 1944
-rw-r--r--    1 masatomix  staff    5919  7 17 21:26 tsconfig.json
-rw-r--r--    1 masatomix  staff     890  7 24 01:25 package.json
-rw-r--r--    1 masatomix  staff  109687  7 24 01:25 package-lock.json
-rw-r--r--@   1 masatomix  staff    7090  7 24 20:56 README.md
drwxr-xr-x    6 masatomix  staff     192  7 24 11:41 src
-rw-r--r--@   1 masatomix  staff   39898  7 24 13:31 sample.m4a
-rw-r--r--    1 masatomix  staff  420274  7 24 14:02 sample.wav
-rw-r--r--@   1 masatomix  staff    2335  7 17 15:21 firebase-adminsdk.json

下記のコマンドを実行します。
準備でダウンロードしたfirebase-adminsdk.json を指定する環境変数を定義して、コードsrc/index.tsを実行しています。

% pwd
/xxx/speech_node_samples/sample_ts
% export GOOGLE_APPLICATION_CREDENTIALS="`pwd`/firebase-adminsdk.json"
%
% npx ts-node src/index.ts
{ results: [ { alternatives: [Array], channelTag: 0 } ] }
Transcription: ボイスメモのテストです。
% 

ちゃんと音声認識できていますね!
あもちろん結果は音声ファイルによって異なります :-)

コードの中身

コードを見ておきましょう。さきほどのクイックスタートのほぼまんまですが。

src/index.ts
import speech from '@google-cloud/speech'
import fs from 'fs'

async function main() {
  // Creates a client
  const client = new speech.SpeechClient()

  // The name of the audio file to transcribe
  const fileName = './sample.wav'

  // Reads a local audio file and converts it to base64
  const file: Buffer = fs.readFileSync(fileName)
  const audioBytes: string = file.toString('base64')

  // The audio file's encoding, sample rate in hertz, and BCP-47 language code
  // https://cloud.google.com/speech-to-text/docs/reference/rest/v1/RecognitionConfig
  const config = {
    enableAutomaticPunctuation: true,
    encoding: 'LINEAR16',
    languageCode: 'ja-JP',
    model: 'default',
  }
  const request: any = {
     audio: {
      content: audioBytes,
    },
    config: config,
  }

  // Detects speech in the audio file
  const [response]: Array<any> = await client.recognize(request)
  console.log(response)
  const transcription = response.results.map((result: any) => result.alternatives[0].transcript).join('\n')
  console.log(`Transcription: ${transcription}`)
}

if (!module.parent) {
  main().catch(console.error)
}

流れとしては

  • ファイルsample.wavを読み込んで
  • Base64 エンコードして文字列化
  • パラメタconfig 情報とともに、Base64 文字列をライブラリの recognize メソッドを呼び出す
  • 結果を得る

というシンプルなモノです。
まずこれで「音声ファイルをもとに、クライアントアプリケーションから、ライブラリを用いて音声認識する」ことができました。

REST インタフェースを呼んでみる

つづいて@google-cloud/speech のライブラリ経由でなく、RESTインタフェースを直接呼び出してみます。
ちなみにRESTインタフェースの仕様はココ

追加の準備として、Firebase や Google Cloud Platformのサインアップ の 「Firebaseのプロジェクト内に、アプリを作成する」を実施し、firebaseConfigの情報を下記のように保存しておきます。

% cat ./src/firebaseConfig.ts 
export default {
  apiKey: 'xx',
  authDomain: 'xx',
  databaseURL: 'xx',
  projectId: 'xx',
  storageBucket: 'xx',
  messagingSenderId: 'xx',
  appId: '1:xx',
}
%  ↑こんなファイルを手動で作る

さあ実行です。

% export GOOGLE_APPLICATION_CREDENTIALS= 
//  環境変数は不要なのでリセット
% npx ts-node ./src/index2.ts
Transcription: ボイスメモのテストです。
% 

またまたちゃんと音声認識できていそうです!

コードの中身

さきほどとコードの構成はほぼおなじではありますが、今度はライブラリは使わずRESTインタフェースを直接呼び出しています。

src/index2.ts
import fs from 'fs'
import request from 'request'
import firebaseConfig from './firebaseConfig'

const createRequestPromise = (option: any): Promise<Array<any>> => {
  const promise: Promise<any> = new Promise((resolve, reject) => {
    request(option, function (err: any, response: any, body: string) {
      if (err) {
        reject(err)
        return
      }
      if (response.statusCode >= 400) {
        reject(new Error(JSON.stringify(body)))
      }
      resolve(body)
    })
  })
  return promise
}

function main() {
  const API_KEY = firebaseConfig.apiKey

  // The name of the audio file to transcribe
  const fileName = './sample.wav'

  // Reads a local audio file and converts it to base64
  const file: Buffer = fs.readFileSync(fileName)
  const audioBytes: string = file.toString('base64')

  // The audio file's encoding, sample rate in hertz, and BCP-47 language code
  const config = {
    enableAutomaticPunctuation: true,
    encoding: 'LINEAR16',
    languageCode: 'ja-JP',
    model: 'default',
  }
  const request: any = {
    audio: {
      content: audioBytes,
    },
    config: config,
  }

  const option = {
    uri: `https://speech.googleapis.com/v1p1beta1/speech:recognize?key=${API_KEY}`,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    json: request,
  }

  createRequestPromise(option)
    .then((response: any) => {
      const transcription = response.results.map((result: any) => result.alternatives[0].transcript).join('\n')
      console.log(`Transcription: ${transcription}`)
    })
    .catch((error) => console.log(error))
}

if (!module.parent) {
  main()
}

これで「音声ファイルをもとに、クライアントアプリケーションから、RESTを用いて音声認識する」ことができました。

以下蛇足

ちなみにMacでは afinfo コマンドなどで音声データの確認が可能です。

$ afinfo sample.m4a
File:           sample.m4a
File type ID:   m4af
Num Tracks:     1
----
Data format:     1 ch,  48000 Hz, 'aac ' (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame
                no channel layout.
estimated duration: 4.335187 sec
audio bytes: 36206
audio packets: 206
bit rate: 65908 bits per second
packet size upper bound: 275
maximum packet size: 275
audio data file offset: 44
not optimized
audio 208089 valid frames + 2112 priming + 743 remainder = 210944
format list:
[ 0] format:      1 ch,  48000 Hz, 'aac ' (0x00000000) 0 bits/channel, 0 bytes/packet, 1024 frames/packet, 0 bytes/frame
Channel layout: Mono
----

afplay コマンドは音声を再生できたりします。

% afplay sample.wav
% (再生されてます)

参考: https://qiita.com/fromage-blanc/items/32e2ba83b79151e5ecb9

べんりですね。

まとめ

  • 音声ファイルをもとに、クライアントアプリケーションから、ライブラリを用いて音声認識する ことができました。
  • 音声ファイルをもとに、クライアントアプリケーションから、RESTを用いて音声認識する ことができました。

次回は、マイクを使った音声認識をやってみましょう。

おつかれさまでしたー。

関連リンク


  1. コレを見たら「Webアプリから簡単に音声認識できるじゃん」って思えたのですが、なかなかうまくいきませんでした、、。 

  2. リンク先には有効化した後、サービスアカウント〜、JSONとして秘密鍵を〜とかあるんですが、多分やんなくて大丈夫 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure IoT Data Pipeline - EventHub から Functions で PostgreSQL へ

はじめに

Screen Shot 2020-07-25 at 14.05.54.png

Azure を使って、センサ(Mosquitto)からのデータを PostgreSQL まで持っていくようなデータパイプラインをつくりました1

ここでは、EventHub > Functions > PostgreSQL のところを設定するにあたってやったことをつらつらと書きます

ちなみに Mosquitto > IoT Hub 周りは、Azure IoT Hub に Mosquitto™ から MQTT なげてみる に書いてあります

やったこと

やったことは、こんな感じの流れになります

  1. EventHub はできていて、データが流れてきている状態からスタート2
  2. PostgreSQL を設定する
  3. Functions を設定する

EventHub から流れてくるデータ

こんな感じの電流値センサからのデータです

{
  "id": "11253",
  "mid": "M_180331",
  "name": "60Min_W-PDP-I4B-A1 出力回路23 電流 計測",
  "time": "2020/05/05 08:00:00",
  "value": "0",
  "unit": "A",
  "EventProcessedUtcTime": "2020-07-25T05:06:10.8586288Z",
  "PartitionId": 0,
  "EventEnqueuedUtcTime": "2020-07-25T05:06:10.777Z",
  "IoTHub": {
    "MessageId": null,
    "CorrelationId": null,
    "ConnectionDeviceId": "iot-gw-tk10",
    "ConnectionDeviceGenerationId": "637294479848408560",
    "EnqueuedTime": "2020-07-25T05:06:10"
  }
}

PostgresSQL を設定する

ここはあまり書くことがありません。以下のドキュメントに従って進めるだけです

Quickstart: Create an Azure Database for PostgreSQL server in the Azure portal

  • PostgreSQLの名前やパスワードだけ控えておきます
  • 途中、データベースに外部からアクセスできるように、IPアドレスのフィルタを設定します。Azure の ネットワークセキュリティとは独立した設定になるようなので注意が必要です(ネットワークセキュリティの方では穴を開けてなくても、こちらを開けると穴が開いてしまいます)

psql をインストールして接続

(PostgreSQL全体ではなく) psql だけをインストール(Mac の場合)

brew install libpq

インストールしたところにパスをとおす。私の場合は以下

/usr/local/Cellar/libpq/12.3/bin

psql で接続する

psql --host=*****.postgres.database.azure.com --port=5432 --username=*****@***** --dbname=postgres

データを受け取るテーブルを作成

テーブル作成SQL
CREATE TABLE public.iotdata
(
    deviceid integer NOT NULL,
    createdate timestamp without time zone NOT NULL DEFAULT now(),
    data jsonb
);

データをカラムに分けた方がいいんですが、まずは動かすことに注力して、データは JSON のまま jsonb のカラムに放り込む方向性

postgres=> CREATE DATABASE iotdemo;

postgres=> \c iotdemo

iotdemo=> \c
psql (12.3, server 10.11)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
You are now connected to database "iotdemo" as user "*****@*****".
iotdemo=> CREATE TABLE public.iotdata
iotdemo-> (
iotdemo(>     deviceid integer NOT NULL,
iotdemo(>     createdate timestamp without time zone NOT NULL DEFAULT now(),
iotdemo(>     data jsonb
iotdemo(> );
CREATE TABLE
iotdemo=> 
iotdemo=> 
iotdemo=> \dt
         List of relations
 Schema |  Name   | Type  |  Owner  
--------+---------+-------+---------
 public | iotdata | table | *****
(1 row)

Azure Functions を設定する

Azure Functions の理解

他のクラウドで Functions を触ったことがあればそれほど違和感はない3 。初めてであれば、いきなり EventHub をターゲットにせずに、ファンクション自体の理解のために、最初に クイック スタート:HTTP 要求に応答する関数を Azure で作成する をやっておくと流れが掴めるのでおすすめ

開発環境

以下から選ぶ感じ4

前提条件

ファンクションを書く前に、このあたりを事前に準備しておく

  • Azure Functions Core Tools バージョン 2.7.1846 以降の 2.x バージョン
  • Azure CLI バージョン 2.4 以降5
  • node はアクティブ LTS およびメンテナンス LTS バージョン (8.11.1 および 10.14.1 を推奨)6

なんか、準備だけで、だいぶ、面倒くさくなってきてるが.... 進める

ローカルで書く場合のプロジェクト構成

ファンクションは FunctionApp > Function という階層になっているようだ

  • 開発言語や実行環境(Win/Linux)などは FunctionApp 単位で定義
  • FunctionApp に複数の Functions が存在可能
  • クラウドへのデプロイ(publish)は FunctionApp 単位
  • node の場合は node_modules ごとデプロイすることになる7
  ├── FunctionApp
  │   ├── EventHubTrigger # ファンクション#1
  │   │   ├── function.json
  │   │   └── index.js
  │   ├── HTTPTrigger # ファンクション#2
  │   │   ├── function.json
  │   │   └── index.js
  :
  :
  ├── host.json
  ├── local.settings.json
  ├── node_modules
  │   ├── buffer-writer
  │   │   ├── LICENSE
  :
  :

トリガーとバインドの概念

ここで、Azure Functions でのトリガーとバインドの概念 を読む。今回は、トリガーとしての設定になる。EventHub にデータがきたらそれをトリガーにファンクションが動いて、DBに書き込む

ファンクションの設定

準備が整ったので、いよいよはじめるよ

ストレージ作成

まず、作ったファンクションをおく場所であるストレージを作る

$ az storage account create --name ***** --location japaneast --resource-group *****-poc --sku Standard_LRS
$ az storage account show --name ***** -o table
AccessTier    CreationTime                      EnableHttpsTrafficOnly    Kind       Location    Name     PrimaryLocation    ProvisioningState    ResourceGroup    StatusOfPrimary
------------  --------------------------------  ------------------------  ---------  ----------  -------  -----------------  -------------------  ---------------  -----------------
Hot           2020-07-23T09:07:56.927438+00:00  True                      StorageV2  japaneast   *****  japaneast          Succeeded            *****-poc      available

FunctionApp をつくる

クラウド側に FunctionApp をつくる

$ az functionapp create --resource-group *****-poc --consumption-plan-location japaneast --runtime node --runtime-version 10 --functions-version 2 --name *****-func-e2p --storage-account *****
$ az functionapp list -o table
Name                 Location    State    ResourceGroup        DefaultHostName                        AppServicePlan
-------------------  ----------  -------  -------------------  -------------------------------------  --------------------------
*****-func-e2p     Japan East  Running  *****-poc          *****-func-e2p.azurewebsites.net     JapanEastPlan
$ az functionapp config appsettings list --name *****-func-e2p --resource-group *****-poc
$ func azure functionapp list-functions *****-func-e2p --show-keys

ローカル側で FunctionApp を初期化

func init e2p --javascript
├── e2p
│   ├── host.json
│   ├── local.settings.json
│   └── package.json

host.jsonversion3 はファンクションランタイムバージョンではない。単純にhost.json のスキーマのバージョンなので、2 のままで良い

ここで、e2pにおりて、ファンクションをつくる

今回は、Azure Event Hub trigger で作成

$ func new
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions activity
4. Durable Functions HTTP starter
5. Durable Functions orchestrator
6. Azure Event Grid trigger
7. Azure Event Hub trigger
8. HTTP trigger
9. IoT Hub (Event Hub)
10. Azure Queue Storage trigger
11. SendGrid
12. Azure Service Bus Queue trigger
13. Azure Service Bus Topic trigger
14. SignalR negotiate HTTP trigger
15. Timer trigger
Choose option: 7
Azure Event Hub trigger
Function name: [EventHubTrigger] 
Writing /Users/*****/azure/e2p/EventHubTrigger/index.js
Writing /Users/*****/azure/e2p/EventHubTrigger/function.json
The function "EventHubTrigger" was created successfully from the "Azure Event Hub trigger" template.

function.jsonindex.js のテンプレが作成されるので、function.json のパラメータを 接続するEventHub に合わせて書き換える

function.json
{
  "bindings": [
    {
      "type": "eventHubTrigger",
      "name": "eventHubMessages",
      "direction": "in",
      "eventHubName": "*****",
      "connection": "*****_RootManageSharedAccessKey_EVENTHUB",
      "cardinality": "many",
      "consumerGroup": "$Default"
    }
  ]
}

ここで、よくわからなかったのが、connection の値8。結論としては、これは Event Hubs 名前空間 > 共有アクセス ポリシー にある主キー の値のこと。実際には 主キー をそのまま書くのではなく、ファンクションの 構成 > アプリケーション設定 に環境変数のような感じで書いておくのがベストプラクティスのようだ(以下参照)9

image-20200724135710900.png

ファンクションを書く

やっと、index.js に中身を書く... 今回書いたのは超シンプルにこんな感じ

index.js
module.exports = async function (context, eventHubMessages) {

    var pg = require('pg');

    const config = {
      host: '*****.postgres.database.azure.com',
      user: '*****@*****',     
      password: '*****',
      database: 'iotdemo',
      port: 5432,
      ssl: true
    };

    var client = new pg.Client(config);

    eventHubMessages.forEach((message, index) => {
      context.log("Processed message: " + JSON.stringify(message));
      context.log("id: " + message.id)

      const sql = 'insert into iotdata(deviceid, data) values(' + message.id + ',' + '\'' + JSON.stringify(message) + '\');';
      console.log("SQL: " + sql);

      client.connect()
      client.query(sql, (err, res) => {
        if (err) {
          console.error(err.stack)
        } else {
          console.log(res)
          console.log('insert completed successfully!');
        }
        client.end()
      })

    });

   context.log("eventHubMessages: " +  JSON.stringify(eventHubMessages));

};

pg パッケージをインストールする

$ npm install pg

とやっておいて、node_modules ができるのを確認する。このフォルダも一緒にクラウドにあげることになる

クラウドにアップロードする

publishhost.json のあるところで叩く10

$ func azure functionapp publish *****-func-e2p
$ func azure functionapp publish *****-func-e2p
You're trying to use v3 tooling to publish to a non-v3 function app (FUNCTIONS_EXTENSION_VERSION is set to ~2).
You can pass --force to force update the app to v3, or downgrade to v1 or v2 tooling for publishing.
*****:e2p *****$ func azure functionapp publish *****-func-e2p --force
Getting site publishing info...
Creating archive for current directory...
Uploading 1.21 KB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in *****-func-e2p:
    EventHubTrigger - [eventHubTrigger]

これで、ひとまず動くはず。

確認する

ポータルの App Service > 関数 > モニタ > ログ でログが確認できる

2020-07-25T05:06:11Z   [Information]   Executing 'Functions.EventHubTrigger' (Reason='', Id=e1066a47-60af-4d8b-a9df-8d1c5a31a76e)
2020-07-25T05:06:11Z   [Information]   Processed message: {"id":"11253","mid":"M_180331","name":"60Min_W-PDP-I4B-A1 出力回路23 電流 計測","time":"2020/05/05 08:00:00","value":"0","unit":"A","EventProcessedUtcTime":"2020-07-25T05:06:10.8586288Z","PartitionId":0,"EventEnqueuedUtcTime":"2020-07-25T05:06:10.777Z","IoTHub":{"MessageId":null,"CorrelationId":null,"ConnectionDeviceId":"iot-gw-tk10","ConnectionDeviceGenerationId":"637294479848408560","EnqueuedTime":"2020-07-25T05:06:10"}}
2020-07-25T05:06:11Z   [Information]   id: 11253
2020-07-25T05:06:11Z   [Information]   eventHubMessages: [{"id":"11253","mid":"M_180331","name":"60Min_W-PDP-I4B-A1 出力回路23 電流 計測","time":"2020/05/05 08:00:00","value":"0","unit":"A","EventProcessedUtcTime":"2020-07-25T05:06:10.8586288Z","PartitionId":0,"EventEnqueuedUtcTime":"2020-07-25T05:06:10.777Z","IoTHub":{"MessageId":null,"CorrelationId":null,"ConnectionDeviceId":"iot-gw-tk10","ConnectionDeviceGenerationId":"637294479848408560","EnqueuedTime":"2020-07-25T05:06:10"}}]
2020-07-25T05:06:11Z   [Information]   Executed 'Functions.EventHubTrigger' (Succeeded, Id=e1066a47-60af-4d8b-a9df-8d1c5a31a76e)

あとは、PostgreSQL に SQL なげてみればデータ入っているはず

今後

  • Mosquitto のところを、IoT Edge にして、コンテナ化して遊ぶ
  • Streaming Analytics でいろいろアノマリーを検知して遊ぶ
  • PowerBI で可視化して遊ぶ
  • センサの数を増やし、データをよりリアルタイムにして遊ぶ

参考にしたサイト


  1. こんなリッチな構成が本当に必要なのかと思わなくはない 

  2. IoT Hub > Streaming Analytics > EventHub はポータル上でマウスでコチコチやれば比較的簡単につながりますのでやってみてください 

  3. Firebase のファンクションの方が使いやすいな... という違和感を除く 

  4. ただし、今回は言語を JavaScript にしてるので、PostgreSQL と繋げるためにパッケージを入れる必要があることから、ローカルでやることになるのであった 

  5. さあ、brew upgrade azure-cli をどうそ 

  6. この記述は古いと思われる。Function のバージョンが 3.x であれば node 12 が使えるはず 。私はここで、homebrew で入れた最新の14 を消して、nvm を入れ、12 を入れる羽目になる。Happy Yac Shaving?! 

  7. 今回 PostgreSQLと接続するために pg パッケージを使うので これは必須 

  8. Webポータルからファンクションをつくると、GUIで選択できるのでそれほど困らないが、ローカルでファンクション作ると何を書けばいいのかわかりにくい。コンポーネントごとに毎回キーを指定しないといけないのは、Azure の ダウンサイドだな... がんばれ Azure IAM... 

  9. CLIでは az functionapp config appsettings list -g *****-poc --name *****-func-e2p みたいな感じで確認・設定できる 

  10. さもなくば、Unable to find project root. Expecting to find one of host.json in project root. と怒られる 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでGCEのインスタンスを作成・削除する

作成

const Compute = require('@google-cloud/compute');
const compute = new Compute();
const option = { .......... };
const zone = compute.zone('asia-northeast1-b');
const [vm, operation] = await zone.createVM(vmName, option);
await operation.promise();
console.log(vmName + ' created!');

option の中身はGCPのコンソールでポチポチやった時のパラメータ(下記参照)をコピーして使うと良い

image.png

image.png

削除

const compute = new Compute();
const zone = compute.zone('asia-northeast1-b');
const vm = await zone.vm(vmName);
const [operation] = await vm.delete();
await operation.promise();
console.log(vmName + ' deleted!');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む