Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8845 articles
Browse latest View live

aptでlibssl1.0-devをインストールするとrosが削除された

$
0
0

概要

もともとrosが入っていたubuntu18.04のPCにnodejsをインストールしたくて、libssl1.0-devが必要と言われたので、以下のコマンドをうったらros関連を大量にremoveされた。

apt install libssl1.0-dev

Start-Date: 2020-08-06  11:19:07
Commandline: apt install libssl1.0-dev
Requested-By: solitude_under_the_blue_sky (1000)
Install: libcurl4-gnutls-dev:amd64 (7.58.0-2ubuntu3.9, automatic),
 libssl1.0-dev:amd64 (1.0.2n-1ubuntu5.3)
Remove: ros-melodic-image-proc:amd64 (1.15.0-1bionic.20200530.124945),
 ros-melodic-rviz-visual-tools:amd64 (3.8.0-4bionic.20200624.182713),
 ros-melodic-qt-gui-cpp:amd64 (0.4.1-1bionic.20200613.044041),
 ros-melodic-turtlebot3-slam-dbgsym:amd64 (1.2.2-1bionic.20200530.121003),
 ros-melodic-moveit-planners-chomp:amd64 (1.0.4-1bionic.20200630.153721),
 ros-melodic-diff-drive-controller:amd64 (0.17.0-1bionic.20200530.114114),
 ros-melodic-common-tutorials:amd64 (0.1.11-0bionic.20200530.090435),
 ros-melodic-rosmsg:amd64 (1.14.6-1bionic.20200530.031608),
 etc....

nodejsで行う作業が一旦終わって、元の環境に戻したいので、手順のメモ。

初期状態

aptのhistory.logから、上記のようにコマンドを打った際にremoveされたものの一覧を取得し、テキストエディタに貼り付ける。以下の通り。

Remove: ros-melodic-image-proc:amd64 (1.15.0-1bionic.20200530.124945), ros-melodic-rviz-visual-tools:amd64 (3.8.0-4bionic.20200624.182713), ros-melodic-qt-gui-cpp:amd64 (0.4.1-1bionic.20200613.044041), ros-melodic-turtlebot3-slam-dbgsym:amd64 (1.2.2-1bionic.20200530.121003),

あとでapt installする用にプログラムを整理する。
まず、コンマスペースを全て改行に置換。
スペースはLinuxでは\s
ラインフィードは\n

  • [find] ,\s
  • [replace] \n

するとこうなる

Remove: ros-melodic-image-proc:amd64 (1.15.0-1bionic.20200530.124945)
ros-melodic-rviz-visual-tools:amd64 (3.8.0-4bionic.20200624.182713)
ros-melodic-qt-gui-cpp:amd64 (0.4.1-1bionic.20200613.044041)
ros-melodic-turtlebot3-slam-dbgsym:amd64 (1.2.2-1bionic.20200530.121003)

カッコで囲まれた部分はaptで認識されないので、これも正規表現で消去
replaceに何も書かなければ消去される。

  • [find] \(.*\)
  • [replace]

結果がこちら。

Remove: ros-melodic-image-proc:amd64 
ros-melodic-rviz-visual-tools:amd64 
ros-melodic-qt-gui-cpp:amd64 
ros-melodic-turtlebot3-slam-dbgsym:amd64 

あとは「Remove:」を消す。「:amd64」はあってもなくても良いが、一応以下のようにして消した。

  • [find] :amd64
  • [replace]

最終的には以下のようになる。

ros-melodic-image-proc 
ros-melodic-rviz-visual-tools 
ros-melodic-qt-gui-cpp 
ros-melodic-turtlebot3-slam-dbgsym 

これを、history_fix.logとして保存。

その後、history_fix.logが存在するディレクトリで
以下のように端末からapt installを実行。

cat history_fix.log | xargs sudo apt install -y

とりあえず、aptだけはこれでOK。しかしバージョンとかは元のやつが入らないので
コンパイルして動くやつは結局全部コンパイルし直し。

2020年8月15日 お盆のためか、交通量が少ない。


SwitchBotの温湿度計の値をNode.js(BLE)を使って取得する

$
0
0

環境構築

balenaを使ってしまっていますが、debian環境なので install_packagesはそのまま apt install等で置き換えてください。

またNode.jsでBLEを扱うためのnobleというライブラリを今回は使いますが、本家よりも @abandonware/nobleの方がメンテされているので、今回はこちらを使います。

Dockerfile
FROM balenalib/%%BALENA_MACHINE_NAME%%-node:12-buster-build as buildRUN install_packages bluetooth bluez libbluetooth-dev libudev-dev
RUN npm install @abandonware/noble rxjs

FROM balenalib/%%BALENA_MACHINE_NAME%%-node:12-buster-runWORKDIR /workCOPY --from=build ./node_modules ./node_modulesCOPY app.js .CMD ["node", "app.js"]

ソースコード

SwitchBotの温湿度計のMACアドレスを予め取得しておいてください。わからない場合は、まずfilterせずにScanしてみて、それっぽいのをしぼります(filterの記述はソースコードのコメントを参照)。

まず冒頭のRxを準備するソースコード。

app.js
constnoble=require('@abandonware/noble');constSWITCHBOT_MAC_ADDR=process.env.SWITCHBOT_MAC_ADDR||'aa:bb:cc:dd:ee:ff';// コロンあり小文字でconst{fromEvent,forkJoin,interval,empty,}=require('rxjs');const{filter,map,mergeMap,take,}=require('rxjs/operators');// BLEのデバイスがスタンバイ状態になるのを待つストリームconstblePowerdOn=fromEvent(noble,'stateChange').pipe(filter((state)=>{console.log(state);returnstate==='poweredOn';}),map(()=>console.log('[ble]poweredOn')),take(1));// ペリフェラルを発見したらペリフェラルを流すストリーム// MACアドレスが不明な場合はfilterの記述を削除// bleDevices = fromEvent(noble, 'discover')bleDevices=fromEvent(noble,'discover').pipe(filter((peripheral)=>{returnperipheral.address===SWITCHBOT_MAC_ADDR;}));

アドバタイズのデータから温度と湿度とバッテリー残量を取得する処理。
参考: https://qiita.com/c60evaporator/items/7c3156a6bbb7c6c59052#%E3%82%BB%E3%83%B3%E3%82%B5%E5%80%A4%E5%8F%96%E5%BE%97%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90

app.js
constdecodeSensorData=(data)=>{constbatt=data[2]&0b01111111;constisTemperatureAboveFreezing=data[4]&0b10000000;lettemp=(data[3]&0b00001111)/10+(data[4]&0b01111111);if(isTemperatureAboveFreezing<0)temp=-temp;consthumid=data[5]&0b01111111;return{'SensorType':'SwitchBot','Temperature':temp,'Humidity':humid,'BatteryVoltage':batt};};

最後に実際に動かす部分。

app.js
forkJoin({ble:blePowerdOn}).pipe(mergeMap(()=>{console.log('[ble]scanning...');noble.startScanning([]);// 10秒ごとにスキャンする(この中でやる必要はなさそう)interval(10*1000).subscribe(()=>{noble.stopScanning();noble.startScanning([]);});returnbleDevices;}),).subscribe((peripheral)=>{// 取得したセンサー情報を出力するconsole.log(decodeSensorData(peripheral.advertisement.serviceData[0].data));});

出力

image.png

このように値を出力できました。あとは、好きなクラウドにアップロードしたり、画面に出力したりして楽しんでください。

【Alexa】新機能 hostedスキルのコードを理解する

$
0
0

はじめに

これまでβ版だったAlexa-hostedスキルが正式版?となったのでそれを使ってアレクサスキルを作って学習します。この時点で筆者が超初心者のため難しい説明はしません、というか出来ません。

準備

こちらに準備方法を用意しています。
ここからテストが出来る段階まで進めておいてください。

参考

コード全体を理解する

テンプレートのHelloWorldのプログラム部分を省略してみました。

index.js
constAlexa=require('ask-sdk-core');constLaunchRequestHandler={// 省略};constHelloWorldIntentHandler={// 省略};constHelpIntentHandler={};constCancelAndStopIntentHandler={// 省略};constSessionEndedRequestHandler={// 省略};constIntentReflectorHandler={// 省略};constErrorHandler={// 省略};exports.handler=Alexa.SkillBuilders.custom().addRequestHandlers(LaunchRequestHandler,HelloWorldIntentHandler,HelpIntentHandler,CancelAndStopIntentHandler,SessionEndedRequestHandler,IntentReflectorHandler,).addErrorHandlers(ErrorHandler,).lambda();

よく見るとプログラムの実態は下の方に定義してあるだけで、それ以外の部分はconstで定義されていることがわかります。
constで定義されたハンドラプログラム?がaddRequestHandlersまたはaddErrorHandlersで登録されているようです。
つまり新たなハンドラプログラムを書いたときは、ここにも追加する必要があるわけです。addRequestHandlersはその手前を見れば** Alexa.SkillBuilders.custom()**なのでアレクサのスキルビルダーのカスタムスキルからリクエストされるプログラムで、addErrorHandlersはエラーの時に呼ばれるようですね。

まずはここを理解しましょう。

インテントハンドラ

アレクサに対して発音する言葉の単語(インテント)の内容に応じて呼び出される命令セット(ハンドラ)が異なるというイメージです。※イメージの意味は後で説明します。

ハンドラごとの処理の大まかな内訳は

ハンドラハンドラ名処理の内容
LaunchRequestHandlerアレクサが最初に呼ばれたときに実行
HelpIntentHandler説明を求められたときに実行
HelloWorldIntentHandler自分で作ったスキルを実行
CancelAndStopIntentHandler中止または停止を求められた場合に実行
SessionEndedRequestHandlerセッションを終了する時に実行
IntentReflectorHandlerデバッグ時に実行
ErrorHandlerエラーが発生したときに実行

のようになっています。
でもこれ単なるイメージです。

Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent'

Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'
という比較がありますが、ソース中のこの2カ所を入れ替えてみましょう。

入れ替え前

index.js
constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){constspeakOutput='こんにちは、アレクサスキルの世界へようこそ!';returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};constHelpIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.HelpIntent';},handle(handlerInput){constspeakOutput='これはあなたが作ったアレクサのスキルです。。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};

入れ替え後

index.js
constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.HelpIntent';},handle(handlerInput){constspeakOutput='こんにちは、アレクサスキルの世界へようこそ!';returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};constHelpIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){constspeakOutput='これはあなたが作ったアレクサのスキルです。。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};

どうなりましたか?
説明、とこんにちはで本来返す言葉が入れ変わりました。
つまり発音した単語ごとにインテントハンドラが決まっているわけではなく、発音した場合はaddRequestHandlersで追加したインテントハンドラにすべて処理が行き渡っているのです。

では比較文を両方ともHelloWorldIntentにしてみるとどうなるでしょうか?
試してみると最初の比較文は真になって実行されますが、次の比較は行われません。
これは次の章で説明しますが、比較文が真の時にはreturnで返値とともに処理もそこで終わるためです。

ということでインテントハンドラの中身を見てみましょう。

インテントハンドラの中身

「こんにちは」と発音した場合に処理されるインテントハンドラの中身を見てみましょう

index.js
constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){constspeakOutput='こんにちは、アレクサスキルの世界へようこそ!';returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};

実はここにアレクサスキルの基本が詰め込まれています。
まずはcanHandlehandleを見てみましょう。

index.js
constHelpIntentHandler={canHandle(handlerInput){// 処理すべきなら true を返す},handle(handlerInput){// 上記の判断で true になったときに実行される}};

インテントハンドラには必ずcanHandlehandleがセットで登場します。
※なんかもっと良いやり方が将来出るでしょうけど

canHandle内では自分のところで処理すべきか判断し、return で true が返値になれば handle が実行されるようになっています。

なので判断さえできるなら複数の発音単語を処理しても構わないわけです。
サンプルをよく見ると

index.js
constCancelAndStopIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&(Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.CancelIntent'||Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.StopIntent');},handle(handlerInput){constspeakOutput='さようなら';returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}};

のように終了と中止を同じように処理が行われています。

canHandleの中身

canHandle内で行われている判定文を見てみます。

index.js
canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},

Alexa.getRequestTypegetIntentNameをそれぞれ比較して双方が一致しているときにのみ true になっていることがわかります。

getRequestType

getRequestTypeはなぜ呼ばれたのかの種類が入ります。

比較先の文字列内容
LaunchRequestインテントの単語を呼ばれなかったとき
IntentRequestインテントに対応させた単語を呼ばれたとき

LaunchRequestは説明によっては「最初に呼ばれたとき」となっているものもありますが

呼び出し内容Alexa.getRequestTypeに入る中身
ノードテストを開いてLaunchRequest
ノードテストを開いて こんにちはIntentRequest

となることから「最初に呼ばれたとき」という表現からは外しました。最初に呼ばれたときの処理は別途考えなくてはなりませんね。

getIntentName

getIntentNameにはインテントとして設定した値が入ります。
「こんにちは」などの発音に設定したインテントはHelloWorldIntentなので、これと比較することでオリジナルのスキルを作ることが出来ます。

スキルの継続と終了

デバッグではわかりづらいですが、スキルを読んだときは継続する場合と終了する場合があります。

「ノードテストを開いて」と発音した後に「こんにちは」と発音したことで独自のスキルが実行されました。しかし「こんにちは」の後に再度「こんにちは」と発音しても独自のスキルは実行されずにアレクサが反応してしまいます。

これは呼び出しスキル名を発音してインテントを発音しないときの処理が継続状態になっているためです。
先ほどのサンプルをもう一度見てみましょう。

index.js
constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){constspeakOutput='こんにちは、アレクサスキルの世界へようこそ!';returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};

これは「こんにちは」と発音したときに実行される処理ですが、実行処理部分を見るとなにやらコメントになっている部分があって、さらにその後ろに何か書かれています。

つまり 「値を返すときに.repromptをつけておくと処理は継続されますよ」ということです。

最後に

これでアレクサスキルの実行処理コードが理解できるようになったのではないでしょうか?

【今日から始めるAWS】Serverless Frameworkを使ってLINEのbotをつくる

$
0
0

はじめに

30代未経験からエンジニア転職をめざすコーディング初学者のYNと申します。お読みいただきありがとうございます。
前に作ったLINEのオウム返しbotを少しだけ発展させた内容でbotを作りましたので学習ログとして投稿させていただきました。

Bot内容

下記のように、ユーザーが送ったタンパク質摂取量を記録できるbotをつくりました。
↓一応友達登録もできます。(思いついたタイミングで勝手に消去する可能性があります。)
スクリーンショット 2020-08-14 20.27.13.png

スクリーンショット 2020-08-14 19.17.13.png

やったこと

Serverless Flameworkを使って、ローカルPC上でサーバレスのAWSのバックエンドを構築した後デプロイし、LINEのbotをつくりました。
イメージはこんな感じです。
スクリーンショット 2020-08-14 21.36.24.png

手順

  • 事前準備
  • Serverless Flameworkの初期設定
  • DynamoDBでデータベースをつくる
  • API-GatewayでLambda関数を公開する
  • Lambda関数を記述する
  • Serverless Frameworkを使ってローカル環境で動作確認する

事前準備

LINEデベロッパー登録

こちらを参照ください。

Serverless Flameworkのインストール

  • ローカルPCにグローバルインストール
$ npm i -g serverless
  • 動作確認
$ sls -v

(slsserverlessのどちらのコマンドでもOKです)

Serverless Flameworkの初期設定

雛形の作成

今回はnode.jsの雛形を作成します。

$ mkdir line-bot
$ cd line-bot
$ serverless create --template aws-nodejs

下記2つのファイルが作成されます。

  • handler.js => Lambdaで実行する関数の中身を記述します。
  • serverless.yml => AWSで構築するバックエンドの設定を記述します。

クレデンシャル情報の設定

初めてServerless Flameworkを使うとき、まず最初にAWSのクレデンシャル情報を設定する必要があります。
具体的には、(おそらくホームディレクトリにある).aws/credentialsというファイルの中身を設定します。
(以前AWS-CLIをいじったことがある方は、すでに設定されているかもしれません。その場合は上書きすることが出来ます。)
スクリーンショット 2020-08-15 10.04.14.png

まずはIAMコンソールでアクセスキーを作成します。
このアクセスキーは慎重に扱い、くれぐれもGithubなどにupなさらぬよう。。
スクリーンショット 2020-08-15 9.46.07.png

次に、クレデンシャル情報を設定します。

$ serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key

これで、クレデンシャル情報の設定は完了です。
クレデンシャル情報を変更する必要がない限り、この設定は初回のみで大丈夫です。

とりあえず試しにデプロイ

serverless.ymlにデプロイ先のリージョンの設定を追記すればとりあえずデプロイすることができます。

serverless.yml
# 抜粋provider:name:awsruntime:nodejs12.xregion:us-east-2#ここにデプロイ先のリージョンを指定します。(今回はオハイオを選択) 

これで、下記デプロイコマンドを打てばデプロイできます。(簡単!)

$ sls deploy

この状態では、handler.jsに記載されたLambda関数のみがデプロイされた状態になります。
Severless Flameworkを使えば面倒なコンソール処理をしなくてもコマンド一発でAWSバックエンドをデプロイできます。これはserverless.ymlファイルに記載された設定をよみこみ、Cloud FormationというAWSのサービスを使って環境を構築しているということのようです。

DynamoDBでデータベースをつくる

そもそも、なぜDynamoDB?

ユーザー情報を管理するためにはデータベースが必要です。個人的にはSQLの方がユーザーデータを管理しやすいと思っているのですが、LambdaとRDSは相性が悪いという噂を聞いたので、今回はnoSQLであるDynamoDBを使います。

DynamoDBの設定

serverless.ymlファイルに設定を追記してDynamoDBの設定を行います。

まずは、AWS全体の設定についてです。

serverless.yml
# AWS周りの設定provider:name:awsruntime:nodejs12.xregion:us-east-2 stage:devenvironment:#環境変数をここに定義できます。今回は「DYNAMODB_TABLE」という環境変数を定義しています。DYNAMODB_TABLE:${self:service}-${self:provider.stage}#「self」とは、このファイルに記載されているクラスそのものです。iamRoleStatements:#ここでは、Lambda関数にDynamoDBへのアクセス権限を与えています。-Effect:AllowAction:-dynamodb:Query#条件検索-dynamodb:Scan#全件取得-dynamodb:GetItem#一件取得-dynamodb:PutItem#一件登録-dynamodb:UpdateItem#修正-dynamodb:DeleteItem#削除Resource:"arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}*"

そして、DynamoDBの設定をします。

serverless.yml
# DynamoDBの設定resources:Resources:Protein:Type:AWS::DynamoDB::TableProperties:TableName:${self:provider.environment.DYNAMODB_TABLE}-protein#作成するデーブル名です。どんな名前でもいいのですが、「line-bot-dev-protein」としています。AttributeDefinitions:-AttributeName:userIdAttributeType:S-AttributeName:sentAtAttributeType:NKeySchema:-AttributeName:userIdKeyType:HASH-AttributeName:sentAtKeyType:RANGEProvisionedThroughput:ReadCapacityUnits:1WriteCapacityUnits:1
  • まずは適当なテーブル名を付けます。
    今回は「line-bot-dev-protein」としました。

  • AttributeDefinitionsKeySchemaは非常に大事です。
    主キー(HASH)とソートキー(RANGE)を組み合わせて、一意に定まるように設定します。それ以外の記録情報(今回はタンパク質摂取量)は定義する必要はありません。ここら辺がSQLとの大きな違いですね。
    スクリーンショット 2020-08-15 11.59.52.png

テーブルをデプロイする

追記したserverless.ymlをデプロイするとテーブルが作成されているのが分かります。

$ sls deploy

スクリーンショット 2020-08-15 12.06.11.png

API-GatewayでLambda関数を公開する

今回、Lambdaで実装したい関数をlogProteinとして、

  1. LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
  2. DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
  3. ユーザーにメッセージを返す

という内容としますが、Lambda関数を記述する前に、API-Gatewayを使ってLambda関数を使って外部から関数を呼び出せるようにします。

serverless.ymlのLambda設定部分を記述します。

serverless.yml
# Lambdaの設定functions:logProtein:handler:handler.logProtein# handler.jsにlogProteinという言う関数を定義してLambdaにデプロイします。environment:# Lambda関数で有効な環境変数を定義。handler.jsで参照する。TableName:${self:provider.environment.DYNAMODB_TABLE}-proteinevents:-http:path:protein#アクセスするときのPathmethod:post#HTTPメソッドを指定

logProteinの概形を記述します。

handler.js
"use strict";module.exports.logProtein=async(event,context)=>{};

この状態でデプロイすると、

$ sls deploy

API-Gatewayにdev-line-botというAPIが作成されており、/proteinというエンドポイントにlogProteinというLambda関数と統合されていることが分かります。
スクリーンショット 2020-08-15 13.27.38.png

Lambda関数を記述する

前に作ったLINEのオウム返しbotを発展させて、
1. LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
2. DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
3. ユーザーにメッセージを返す
という内容を記述します。

前回からの変更部分にコメントを加えていきます。

handler.js
"use strict";constline=require("@line/bot-sdk");constcrypto=require("crypto");constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN,});constAWS=require("aws-sdk");// AWSのSDKをインポートconstdynamo=newAWS.DynamoDB.DocumentClient();// DynamoDBと接続constTableName=process.env.TableName;// 接続先のテーブル名を設定。ymlファイルに記述された環境変数を参照している。module.exports.logProtein=async(event,context)=>{constbody=JSON.parse(event.body);constsignature=crypto.createHmac("sha256",process.env.CHANNELSECRET).update(event.body).digest("base64");constcheckHeader=(event.headers||{})["X-Line-Signature"];if(checkHeader===signature){if(body.events[0].replyToken==="00000000000000000000000000000000"){letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"connect check"}',};context.succeed(lambdaResponse);}else{try{constuserId=body.events[0].source.userId;constsentAt=body.events[0].timestamp;constprotein=Number(body.events[0].message.text);// ユーザーからのリクエストに含まれるWebhookイベントオブジェクトの情報をよみこむ。// https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objectsを参照constyesterday=sentAt-24*3600*1000;// リクエストが送られてきた24時間前のタイムスタンプを指定。constputParams={TableName,Item:{userId,sentAt,protein,},};constputResult=awaitdynamo.put(putParams).promise();// リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む。詳細下記。constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};constresult=awaitdynamo.query(queryParams).promise();consttotalProtein=result.Items.map((item)=>item.protein).reduce((a,b)=>a+b);// ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む。詳細下記。constmessage1={type:"text",text:`この24時間で${totalProtein}gのタンパク質を摂取したぞ`,};constmessage2=totalProtein<100?{type:"text",text:`引き続き、高タンパク/低脂質/低糖質の食事を心掛けろ`,}:{type:"text",text:`タンパク質の摂りすぎも禁物だ。過剰摂取は腸内環境を悪化させる危険があるぞ`,};returnclient.replyMessage(body.events[0].replyToken,[message1,message2])//ユーザーにメッセージを返信する.then((response)=>{letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"completed"}',};context.succeed(lambdaResponse);}).catch((err)=>console.log(err));}catch(error){return{statusCode:error.statusCode,body:error.message,};}}}else{console.log("署名認証エラー");}};
  • リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む
    DynamoDBにデータを1件書き込むためには、putメソッドを使います。
    詳細は公式ドキュメントを参照ください。

  • ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む
    今回は、「条件に合致するデータすべて」を取得するために、queryメソッドを使っています。
    今回の条件とは、「①任意のユーザーの」「②24時間以内の」すべてのデータとなるため、下記のようにパラメータを設定しました。
    詳細は公式ドキュメントを参照ください。

constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};

最後に、LINEのMessaging APIで使っているアクセストークンとチャンネルシークレットをymlファイルに追記します。
(Githubにあげる場合は、ymlファイルに書かずにLambdaコンソールで直接入力するのがいいと思います。)

serverless.yml
provider:name:awsruntime:nodejs12.xregion:us-east-2stage:devenvironment:DYNAMODB_TABLE:${self:service}-${self:provider.stage}ACCESSTOKEN:your-access-token#ここにアクセストークンを記述CHANNELSECRET:your-channel-secret#ここにチャンネルシークレットを記述

これで、デプロイしてLINEのWebhookのurlを設定すれば完成です。

Serverless Frameworkを使ってローカル環境で動作確認する

アプリ自体は、上記手順の後に、デプロイしてLINEのWebhookのurlを設定すれば完成なのですが、Serverless Flameworkの素晴らしい点は、なんと言ってもローカルで動作確認できる点です。
以下のように、AWSバックエンドでAPI-Gateway/Lambda/DynamoDBの動作をローカルで確認することができます。

ローカル開発用のライブラリをインストール

ライブラリをインストールし、プラグインの設定serverless.ymlに追記します。

$ yarn add -D serverless-dynamodb-local serverless-offline
serverless.yml
plugins:-serverless-dynamodb-local-serverless-offline

動作確認用データのseedファイルを作成

ルートディレクトリにseedsフォルダを作成してprotein.jsonを作成します。

seeds/protein.json
[{"userId":"Hanako","sentAt":1597126641204,"protein":50},{"userId":"Taro","sentAt":1597126631204,"protein":60},{"userId":"Jiro","sentAt":1597126241204,"protein":70}]

ローカル開発のための設定

下記の設定をserverless.ymlに追記し、handler.jsのSDKインポート部分を修正します。

serverless.yml
custom:serverless-offline:httpPort:8083# http://localhost:8083にAPIのエンドポイントを設定するdynamodb:stages:devstart:port:8082# http://localhost:8082でデータベースと接続するinMemory:truemigrate:trueseed:trueseed:protein:sources:# データベースと接続し、seedファイルのデータを書き込む-table:${self:provider.environment.DYNAMODB_TABLE}-proteinsources:[./seeds/protein.json]
handler.js
constoptions=process.env.LOCAL?{region:"localhost",endpoint:"http://localhost:8082"}:{};// 環境変数LOCALがtrueの場合は、ローカルにデータベースを作成してhttp://localhost:8082で接続するconstdynamo=newAWS.DynamoDB.DocumentClient(options);

その後、下記コマンドによりローカルでAPIの動作確認をすることができます。
(環境変数LOCALにtrueを代入したのち、ローカル環境を構築します。)

$ LOCAL=true sls offline start

今回、AWSのバックエンドの動作確認をローカルですることができますが、LINEのWebhookの動作確認をオフラインですることが出来ないので、handler.jsを少し書き換えます。

handler.js
"use strict";constline=require("@line/bot-sdk");constcrypto=require("crypto");constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN,channelSecret:process.env.CHANNELSECRET,});constoptions=process.env.LOCAL?{region:"localhost",endpoint:"http://localhost:8082"}:{};constAWS=require("aws-sdk");constdynamo=newAWS.DynamoDB.DocumentClient(options);constTableName=process.env.TableName;module.exports.logProtein=async(event,context)=>{constbody=JSON.parse(event.body);constsignature=crypto.createHmac("sha256",process.env.CHANNELSECRET).update(event.body).digest("base64");constcheckHeader=(event.headers||{})["X-Line-Signature"];// if (checkHeader === signature) {if(true){// cryptの検証をしないif(body.events[0].replyToken==="00000000000000000000000000000000"){letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"connect check"}',};context.succeed(lambdaResponse);}else{try{constuserId=body.events[0].source.userId;constsentAt=body.events[0].timestamp;constprotein=Number(body.events[0].message.text);constyesterday=sentAt-24*3600*1000;constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};constputParams={TableName,Item:{userId,sentAt,protein,},};constputResult=awaitdynamo.put(putParams).promise();constresult=awaitdynamo.query(queryParams).promise();consttotalProtein=result.Items.map((item)=>item.protein).reduce((a,b)=>a+b);constmessage1={type:"text",text:`この24時間で${totalProtein}gのタンパク質を摂取したぞ`,};constmessage2=totalProtein<100?{type:"text",text:`引き続き、高タンパク/低脂質/低糖質の食事を心掛けろ`,}:{type:"text",text:`タンパク質の摂りすぎも禁物だ。過剰摂取は腸内環境を悪化させる危険があるぞ`,};// ユーザーへの返信をしない// return client//   .replyMessage(body.events[0].replyToken, [message1, message2])//   .then((response) => {//     let lambdaResponse = {//       statusCode: 200,//       headers: { "X-Line-Status": "OK" },//       body: '{"result":"completed"}',//     };//     context.succeed(lambdaResponse);//   })//   .catch((err) => console.log(err));// ユーザへの返信の代わりにレスポンスを返す letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:JSON.stringify([message1,message2]),};context.succeed(lambdaResponse);}catch(error){return{statusCode:error.statusCode,body:error.message,};}}}else{console.log("署名認証エラー");}};

エンドポイントにリクエストを送って動作確認

postmanなどを使ってhttp://localhost:8083/dev/proteinにリクエストを送ればローカルで動作確認することができます。
スクリーンショット 2020-08-15 15.22.06.png

最後に

ずいぶん長くなってしまいましたが、初学者にもやさしいServerless Flameworkを使ったAWSバックエンド開発の素晴らしさを伝えたかったです。
お読みいただきありがとうございました。

参考にさせていただいた記事

参考、というか下記の内容をそのままコピーした感じになってしまいました。
初学者にも分かりやすくまとめて頂いています。感謝です。

ツンデレのGoogleアシスタントをLINEボットにする

$
0
0

何か「OK、Google」したいときに、Googleアシスタントを立ち上げるのも寂しいし、ロボットから味気ない応答をもらっても寂しい。。。

Googleアシスタントは、テキスト入力してテキストで応答をもらえるのでその機能を使うこととし、さらに応答をツンデレっぽく語尾変換してみました。
LINEアプリのフレンドから応答が来れば、より本物感が出てきます。

構成はこんな感じです。

image.png

毎度の通り、GitHubに上げておきます。

poruruba/Chatbot_Tsundere
 https://github.com/poruruba/Chatbot_Tsundere

GoogleアシスタントAPIを有効にする

こちらを参考に進めます。

googlesamples/assistant-sdk-nodejs
 https://github.com/googlesamples/assistant-sdk-nodejs
 ※大変参考になったgoogleassistant.jsがあります。これをもとにカスタマイズ済みのため使わなくても大丈夫です。

actions-on-google/actionis-on-google-testing-nodejs
 https://github.com/actions-on-google/actions-on-google-testing-nodejs
 ※便利なファイルgenerate-credentials.jsを使うだけです。

まずは、GoogleのActions Consoleから、プロジェクトを作成します。

Actions Console
 https://console.actions.google.com/

Project Nameは適当に、「Tsundere」とでもしておきます。
LanguageはJapanese、CountryはJapanにします。
Create Projectボタンを押下します。

image.png

次の画面が間違いやすいのですが、一番下に、「Are you looking for device registration? Click here」となっているので、Click hereをクリックします。

image.png

「Register Model」ボタンを押下します。

image.png

適当に入力しましょう。何でもいいです。
 Product name:FirstProduct
 Manufacturer name:FirstManufactuerer
 Device type:Speaker

image.png

Download OAuth 2.0 credentials ボタンを押下して、JSONファイルを取得します。Nextボタンを押下します。JSONファイルはクレデンシャルファイルであり、後で使います。credentials.jsonという名前に変えておきます。

image.png

適当に、All 7 traitsを選択しておきます。Save Traitsボタンを押下します。

image.png

image.png

次に、Google Developer Consoleから、「Google Assistant API」を有効化します。

Google Developer Console
 https://console.developers.google.com/apis/dashboard

image.png

+APIとサービスを有効化 をクリック

image.png

検索にAssistantを入力して、Google Assistant APIを選択

image.png

有効にする ボタンを押下します。これでOKです。

デバイスクレデンシャルファイルの作成

actions-on-google/actionis-on-google-testing-nodejs からZIPをダウンロードしたファイルの中にある有用なJSファイルを使います。

$ unzip actions-on-google-testing-nodejs-master.zip
$ cd actions-on-google-testing-nodejs-master
$ npm install

さきほどダウンロードしたcredentials.jsonを同じフォルダにコピーし、以下を実行します。

$ node generate-credentials.js credentials.json

途中で、URLが表示されますので、コピーペースとしてブラウザから開きます。
さきほどプロジェクトを作成したときのGoogleアカウントでログインすると、許可を求められますので、許可します。そうすると、認可コードのような文字列が表示されます。それをコピーします。
nodeを実行したコマンドラインに戻って、さきほどの認可コードのようなものをペーストします。
そうすると、test-credentials.jsonというファイルが出来上がります。次に使うときのために、devicecredentials.jsonと名前を変えておきます。

Googleアシスタントをローカル実行できるか試してみる

まずは、nodeでローカル実行できるか試してみましょう。

$ mkdir assistant_test
$ cd assistant_test
$ npm init -y
$ npm install google-auth-library google-proto-files grpc

さきほど生成したdevicecredentials.jsonを同じフォルダにコピーしておきます。

vi index.js

index.js
'use strict';constGoogleAssistant=require('./googleassistant');constdeviceCredentials=require('./devicecredentials.json');constCREDENTIALS={client_id:deviceCredentials.client_id,client_secret:deviceCredentials.client_secret,refresh_token:deviceCredentials.refresh_token,type:"authorized_user"};constassistant=newGoogleAssistant(CREDENTIALS);assistant.assist("ありがとう").then(({text})=>{console.log(text);// Will log the answer}).catch(error=>{console.error(error);});

ユーティリティである googleassistant.jsは以下です。
使用しているライブラリのバージョンが新しいと動かないところがあったので直しておきました。grpcもDeprecatedのようですが、とりあえず動いているのでそのままにしています。

googleassistant.js
'use strict';constpath=require('path');constgrpc=require('grpc');constprotoFiles=require('google-proto-files');const{GoogleAuth,UserRefreshClient}=require('google-auth-library');constPROTO_ROOT_DIR=protoFiles.getProtoPath('..');constembedded_assistant_pb=grpc.load({root:PROTO_ROOT_DIR,file:path.relative(PROTO_ROOT_DIR,protoFiles.embeddedAssistant.v1alpha2)}).google.assistant.embedded.v1alpha2;classGoogleAssistant{constructor(credentials){GoogleAssistant.prototype.endpoint_="embeddedassistant.googleapis.com";this.client=this.createClient_(credentials);this.locale="ja-JP";this.deviceModelId='default';this.deviceInstanceId='default';}createClient_(credentials){constsslCreds=grpc.credentials.createSsl();// https://github.com/google/google-auth-library-nodejs/blob/master/ts/lib/auth/refreshclient.tsconstauth=newGoogleAuth();constrefresh=newUserRefreshClient();refresh.fromJSON(credentials,function(res){});constcallCreds=grpc.credentials.createFromGoogleCredential(refresh);constcombinedCreds=grpc.credentials.combineChannelCredentials(sslCreds,callCreds);constclient=newembedded_assistant_pb.EmbeddedAssistant(this.endpoint_,combinedCreds);returnclient;}assist(input){constconfig=newembedded_assistant_pb.AssistConfig();config.setTextQuery(input);config.setAudioOutConfig(newembedded_assistant_pb.AudioOutConfig());config.getAudioOutConfig().setEncoding(1);config.getAudioOutConfig().setSampleRateHertz(16000);config.getAudioOutConfig().setVolumePercentage(100);config.setDialogStateIn(newembedded_assistant_pb.DialogStateIn());config.setDeviceConfig(newembedded_assistant_pb.DeviceConfig());config.getDialogStateIn().setLanguageCode(this.locale);config.getDeviceConfig().setDeviceId(this.deviceInstanceId);config.getDeviceConfig().setDeviceModelId(this.deviceModelId);constrequest=newembedded_assistant_pb.AssistRequest();request.setConfig(config);deleterequest.audio_in;constconversation=this.client.assist();returnnewPromise((resolve,reject)=>{letresponse={input:input};conversation.on('data',(data)=>{if(data.device_action){response.deviceAction=JSON.parse(data.device_action.device_request_json);}elseif(data.dialog_state_out!==null&&data.dialog_state_out.supplemental_display_text){response.text=data.dialog_state_out.supplemental_display_text;}});conversation.on('end',(error)=>{// Response ended, resolve with the whole response.resolve(response);});conversation.on('error',(error)=>{reject(error);});conversation.write(request);conversation.end();});}}module.exports=GoogleAssistant;

以下、実行してみましょう。以下の感じです。

$ node index.js
(node:31667) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
他にも何かお手伝いできることがあればおっしゃってくださいね?

LINEボットの登録

LINE Developer Consoleからボットを登録します。

LINE Developer Console
 https://developers.line.biz/console/

英語表記になっている場合は、右下の選択から日本語を選択してください。
プロバイダをまだ作成していなければ作成し、すでに作成済みであれば、それを選択します。

image.png

「+新規チャネル作成」をクリックします。
ボットは、Messaging APIです。

image.png

チャネルアイコン、チャネル名、チャネル説明、大業種、小業種、メールアドレスを入力してください。
チャネルアイコンが一番大事です。これで、見た目の印象がガラッと変わります!

次に、チャネル基本設定にある「チャネルシークレット」をメモっておきます。
次に、Messaging API設定で、チャネルアクセストークン(長期)において、発行ボタンを押下して、秘匿の文字列を生成しておきます。これも後で使います。
LINE公式アカウント機能において、応答メッセージがありますが、これは無効にしておきます。
さらに、Webhook URLに今回立ち上げるサーバのURLとエンドポイントをくっつけて入力します。

 https://【立ち上げるサーバのURL】/tsundere

とりあえず、ここまでにして、次にLINEボット用のサーバを立ち上げます。

LINEボット用のサーバを立ち上げる

環境一式をGitHubに上げておきましたので、詳細はそちらを参照してください。

$ cd server
$ npm install
$ mkdir cert
$ mkdir credentials

作成したフォルダcertにHTTPSのためのSSL証明書を置きます。その後、app.jsの147行目あたりの以下の記載の部分を、SSL証明書ファイル名に変更しておきます。

app.js
key:fs.readFileSync('./cert/server.key'),cert:fs.readFileSync('./cert/server.crt'),ca:fs.readFileSync('./cert/secom.crt')

もし、起動するポート番号を変える場合には以下のようにします。

vi .env

SPORT=10443

また、作成しておいたデバイスクレデンシャルは、credentialsフォルダにコピーしておきます。

重要なのは、api\controllers\tsundere\index.jsです。

index.js
'use strict';constHELPER_BASE=process.env.HELPER_BASE||'../../helpers/';constResponse=require(HELPER_BASE+'response');constRedirect=require(HELPER_BASE+'redirect');constconfig={channelAccessToken:process.env.LINE_CHANNEL_ACCESS_TOKEN||'【チャネルアクセストークン】',channelSecret:process.env.LINE_CHANNEL_SECRET||'【チャネルシークレット】',};constLineUtils=require(HELPER_BASE+'line-utils');constapp=newLineUtils(config);constGoogleAssistant=require('./googleassistant');constdeviceCredentials=require('./../../../credentials/devicecredentials.json');consttsundere=require('./tsundere');constCREDENTIALS={client_id:deviceCredentials.client_id,client_secret:deviceCredentials.client_secret,refresh_token:deviceCredentials.refresh_token,type:"authorized_user"};constassistant=newGoogleAssistant(CREDENTIALS);app.message(async(event,client)=>{varresponse=awaitassistant.assist(event.message.text);console.log(response);vartext=tsundere(response.text);console.log(text);constecho={type:'text',text:text};returnclient.replyMessage(event.replyToken,echo);});exports.handler=app.lambda();

以下の部分を、LINE Developer Consoleで払い出された値を記載しておきます。

・【チャネルアクセストークン】
・【チャネルシークレット】

LINEの処理はユーティリティ(line-utils.js)にまとめてあります。
app.messageに受信したメッセージが届くので、それに対してGoogleアシスタントに問い合わせています。

間に以下の処理を入れています。これが、ツンデレの語尾変換です。

index.js
vartext=tsundere(response.text);

以下、中身です。

tsundere.js
'use strict';vartable=[["こんにちは","ねえ"],["あなた","あんた"],["どういたしまして","別にあんたのためにやったわけじゃないし。。"],["またいつでもどうぞ","まったく世話が焼けるわ"],・・・・["ありがとうございます","おだてたって何もないわよ"],["ありますか","あるわよね"],];functionDecode(s){for(vari=0;i<table.length;i++){varregExp=newRegExp(table[i][0],"g");s=s.replace(regExp,table[i][1]);}returns;}module.exports=Decode;

変数tableに語尾変換元と変換後の文字列をたくさん追加すれば、よりそれっぽくなります。

それでは、起動させます。

$ node app.js

もう一度、LINE Developer ConsoleのMessaging APIに戻ります。
Webhook URLの部分です。特に問題なければ、「検証」ボタンを押下するとOKが返ってくるはずです。

image.png

LINEアプリに友達登録する

同じMessaging APIのところに、QRコードが表示されています。
このQRコードを、お手持ちのLINEアプリで友達追加してみましょう。

友達追加ができたら、さっそく会話してみましょう。
たとえば、「ありがとう」と伝えてあげてください。

image.png

終わりに

自分でやってみて、ちょっとにやりとしてしまいました。。。
LINEからだけでなく、POSTでも受け付けるようにしていますので、そちらも使ってみてください。

https://【立ち上げるサーバのURL】/assistant-talk

以下もぜひ参考にしてください。
SwaggerでLambdaのデバッグ環境を作る(1)
LINE Beaconを自宅に住まわせる

以上

Terraformを使ってAPI Gatewayとlambda(aws-serverless-express)でAPIを構築する

$
0
0

はじめに

最近サーバーサイドやインフラを勉強中の新米エンジニアです。
勉強のためにnodejsとexpressでWebAPIを構築してローカルでモックとして利用することが結構あるのですが、お盆休みを使ってデプロイ方法を勉強しました。
AWSのAPI GatewayとLambdaを利用します。業務でALBとLambdaの組み合わせは使っているのですが、個人でやるならコスト的にAPI Gatewayかなと思い選定しました。

前提

  • terraform&aws cliの環境構築済み
  • nodejsの環境構築済み

API Gateway

https://aws.amazon.com/jp/api-gateway/

フルマネージド型サービスの Amazon API Gateway を利用すれば、開発者は規模にかかわらず簡単に API の作成、公開、保守、モニタリング、保護を行えます。API は、アプリケーションがバックエンドサービスからのデータ、ビジネスロジック、機能にアクセスするための「フロントドア」として機能します。API Gateway を使用すれば、リアルタイム双方向通信アプリケーションを実現する RESTful API および WebSocket API を作成することができます。API Gateway は、コンテナ化されたサーバーレスのワークロードやウェブアプリケーションをサポートします。

APIの作成や管理が容易に行えるAWSのマネージドなサービスです。ECSやEC2を使わなくてもAPIの構築ができて、APIのコール数や転送データでの従量課金なのでコストも低く抑えられます。API構築するならEC2やECSよりもなるべくこちらを利用したいですね。
色んな使い方がありそうですが、今回はパスやメソッドごとの処理はLambda側で行うので、API Gatewayでは入り口を1つ用意してあげるだけになります。

Lambda

https://aws.amazon.com/jp/lambda/
言わずもがなですね。
サーバーの管理が不要でコードを実行できて、実行時間とリクエスト数で課金ですが100万リクエスト/月の無料枠があります。個人で使う分にはコストを意識する必要はほぼないと思います。

今回は実行環境をnodejs12.xにして、aws-serverless-expressを利用して作成したLambda関数を使います。

aws-serverless-express

https://github.com/awslabs/aws-serverless-express
webアプリケーションフレームワークであるexpressがLambdaで動くすごいやつです。expressを使ったnodejsのプロジェクトを少しいじる以外はほぼそのままでLambdaで動かせます。
リポジトリのREADMEに書かれている通りにやれば実はコマンド叩くだけでデプロイまでできちゃうのですが、terraformの勉強を兼ねていたのでterraformで書いてます。

アーキテクチャ

ざっくり下の図のような感じになります。
API Gatewayのエンドポイントは1つで、そこに来たリクエストをaws-serverless-expressとexpressで実装したLambdaにプロキシする。
アーキテクチャ.png

実装

作業ディレクトリの作成

$ mkdir ./serverless-express-app
$ cd serverless-express-app

まずはlambdaで動かすnodejsのコードを準備していきます。
lambda用のnodejsプロジェクトディレクトリをserverless-express-app配下に作成

$ mkdir ./lambda
$ cd lambda

プロジェクトを作成。全部Enterで進めます。

$ npm init
$ npm i express
$ npm i aws-serverless-express

エントリポイントとなるindex.jsを作成

index.js
constawsServerlessExpress=require('aws-serverless-express');constapp=require('./app');constserver=awsServerlessExpress.createServer(app);exports.handler=(event,context)=>awsServerlessExpress.proxy(server,event,context);

index.jsの2行目でrequireしているapp.jsを作成する。ほぼ普通にexpress書く感じですね。

app.js
constserverlessExpress=require('aws-serverless-express/middleware');varexpress=require('express');varapp=express();app.use(serverlessExpress.eventContext());app.get('/',(req,res)=>{res.send({message:"Hello World"});});module.exports=app

ここまでで今回必要なlambdaのコードは完成です。
気になる方はlambdaディレクトリをzip化してマネジメントコンソールからlambda関数を作成してテストしてみてください。うまくいってれば{\"message\":\"Hello World\"}が返ってきているはずです。

それではlambdaのコードができたので、terraformを書いていきます。
tfファイルはserverless-express-appディレクトリ配下に作成してください。

aws.tf
provider "aws" {
  version = "~> 3.0"
  region = "ap-northeast-1"
}
lambda.tf
data "archive_file" "your_function_name" {
  type        = "zip"
  source_dir  = "lambda"
  output_path = "./your_function_name.zip"
}

resource "aws_lambda_function" "your_function_name" {
  filename         = data.archive_file.your_function_name.output_path
  function_name    = "your_function_name"
  role             = aws_iam_role.your_lambda_role.arn
  handler          = "index.handler"
  source_code_hash = data.archive_file.your_function_name.output_base64sha256
  runtime          = "nodejs12.x"

  memory_size = 128
  timeout     = 60
}

resource "aws_iam_role" "your_lambda_role" {
  name = "your_lambda_role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
POLICY
}

resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.your_function_name.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_api_gateway_rest_api.your_api.execution_arn}/*/*/*"
}
api.tf
resource "aws_api_gateway_rest_api" "your_api" {
  name = "your_api"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_method" "your_api_method_root" {
  rest_api_id   = aws_api_gateway_rest_api.your_api.id
  resource_id   = aws_api_gateway_rest_api.your_api.root_resource_id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_resource" "your_api_resource" {
  path_part   = "{proxy+}"
  parent_id   = aws_api_gateway_rest_api.your_api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.your_api.id
}

resource "aws_api_gateway_method" "your_api_method" {
  rest_api_id   = aws_api_gateway_rest_api.your_api.id
  resource_id   = aws_api_gateway_resource.your_api_resource.id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "your_api_proxy" {
  rest_api_id             = aws_api_gateway_rest_api.your_api.id
  resource_id             = aws_api_gateway_resource.your_api_resource.id
  http_method             = aws_api_gateway_method.your_api_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.your_function_name.invoke_arn
}

resource "aws_api_gateway_integration" "your_api_proxy_root" {
  rest_api_id             = aws_api_gateway_rest_api.your_api.id
  resource_id             = aws_api_gateway_rest_api.your_api.root_resource_id
  http_method             = aws_api_gateway_method.your_api_method_root.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.your_function_name.invoke_arn
}

resource "aws_api_gateway_deployment" "your_deployment" {
  depends_on = [aws_api_gateway_integration.your_api_proxy]

  rest_api_id = aws_api_gateway_rest_api.your_api.id
  stage_name  = "dev"
}

aws_api_gateway_integrationのtypeを"AWS_PROXY"に設定した場合、integration_http_methodは"POST"にしないといけないみたいですね。結構はまりました。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_integration#argument-reference

tfファイルが準備できたのでデプロイします。

$ terraform init
$ terraform apply

AWSマネジメントコンソールからAPI GatewayとLambdaを確認しましょう。
確認できたら、APIのステージからURLを確認し、アクセスしてみます。
スクリーンショット 2020-08-15 17.18.26.png
下のスクリーンショットのようにレスポンスが返ってきていればOKです。
スクリーンショット 2020-08-15 17.20.38.png

最後に

terraformの勉強と思ってやってみたのですが、最終的なコード量の割にかなり時間がかかりました。まだまだ勉強する必要がありますね。これくらいはさくっと書けるようになりたいです。
指摘などあればよろしくお願いします!

【Node.js】package.jsonのscriptsをnpsとnps-utilsを利用して、綺麗に整理してマルチプラットフォーム対応

$
0
0

はじめに

  • npmやyarnを利用した開発の際に、以下の点が気になったため、良い方法があるか調査。
    • package.jsonへ定義するscriptsの量や内容が多くなってしまい、管理が大変
    • 柔軟性や拡張性が欲しいため、json以外のファイルで記述したい。
    • マルチプラットフォームで利用できる環境にしたい。
  • 調査の結果、npsnps-utilsの2つのライブラリを発見。
  • 今回は、上記2つのライブラリの概要や手順、設定テンプレートを記述する。

手順

プロジェクト準備

  • npmやyarnを利用している既存Node.jsプロジェクトを利用する。
    • ※新規の場合、package.jsonでscriptsを管理するものを各自作成
  • 今回は、下記のpackage.jsonの例を置き換える。
置き換え対象のpackage.json
{"scripts":{"dev":"NODE_ENV=development nodemon index.js","test":"NODE_ENV=test jest --coverage","lint":"eslint . --cache""build":"NODE_ENV=production run-s build:{clean,js}","build:clean":"rm -rf ./dist","build:js":"esbuild src/index.js --bundle --outfile=dist/out.js"},"devDependencies":{  ................,"esbuild":"^0.6.12","nodemon":"^2.0.4","jest":"^26.4.0","eslint":"^7.7.0","npm-run-all":"^4.1.5"}}

導入

  • 上記プロジェクトへ以下2つのライブラリへインストール
    • nps : npm scripts管理ツール
    • 利点
      • scriptsの詳細を別ファイルにすることで、管理や保守が容易になる。
      • jsonではなく、jsやyamlで記述できるため、柔軟性や拡張性が高い。
    • nps-utils : npsを強化するためのパッケージツール
    • 利点
      • マルチプラットフォーム対応ライブラリ(rmコマンド対応、環境変数対応)が多数組み込まれているため、一つで完結する。
      • 並列実行サポートもされている。
# nps,nps-utilの導入
npm install--save-dev nps nps-utils

初期化

  • 下記のコマンドで設定ファイルを作成する。
    • ※今回はjsで作成する
# 設定ファイルの作成。デフォルトはpackage-scripts.js
./node_modules/.bin/nps init

# yamlで作成する場合。package-scripts.ymlが作成
./node_modules/.bin/nps init --type yml
  • 作成後、下記の2つのファイルが入っている構造になっていることを確認する。
.
├── package.json
└── package-scripts.js
└── .......

設定テンプレート

  • まず、初期化で作成したpackage-scripts.jsの中身を下記にする。
package-scripts.js
// クロスプラットフォーム対応(Mac,Windows,Linux)const{series,// 連続実行の簡易化rimraf,// rmコマンドcrossEnv// 環境変数設定}=require('nps-utils');module.exports={scripts:{dev:{default:crossEnv('NODE_ENV=development nodemon index.js')},test:{default:crossEnv('NODE_ENV=test jest --coverage')},lint:{default:'eslint . --cache'},build:{default:crossEnv(`NODE_ENV=production ${series.nps('build.clean','build.js',)}`,),clean:rimraf('dist'),js:'esbuild src/index.ts --bundle --outfile=dist/out.js',}}};
  • 最終的なpackage.jsonの中身を下記にする。
    • ※devDependenciesにはインストール済みのnps等が入っている。
最終package.json
{"scripts":{"dev":"nps dev","test":"nps test","lint":"nps lint","build":"nps build"},"devDependencies":{  ................,"esbuild":"^0.6.12","nodemon":"^2.0.4","jest":"^26.4.0","eslint":"^7.7.0","npm-run-all":"^4.1.5","nps":"^5.10.0","nps-utils":"^1.7.0",..........}}

実行

  • 各種実行は、下記で行う。
    • ※基本的に、npm scriptsで行なっている方法と変わらない。
# npm run スクリプト名# dev
npm run dev
# test
npm run test

参考

【Node.js】APIサーバでマイクラ鯖をコントロールしよう #2

$
0
0

はじめに

前回の記事はこちらから

Node.js+TypeScript+Expressを使って、Minecraftサーバをコントロールしていきます!
今回は基本的な部分である 起動・停止・再起動の機能を追加したのと、問題があった際にプレイメンバー(友人たち)と一緒に入っているDiscordのグループにアラートを送る機能、またログをDBに保存させる機能の3点を追加しました。

このソースコードはGitHubに公開しているので、修正点やアドバイス等があれば是非プルリクをお願いします。またMITライセンスにしておりますので、どうぞご自由にお使いください!

GitHub ー smpny7

仕様

基本的には以下のことができるAPIサーバを作成する。(予定)

  • サーバ稼働状況やバージョンの取得
  • オンラインメンバーの一覧取得
  • Discordへのエラー通知 ← 今回
  • DB (SQLite) へのログの保存 ← 今回
  • サーバの起動・停止・再起動 ← 今回
  • サーバアイコンの取得
  • プレイヤー画像の取得
  • ワールドデータの日常的なGoogleDriveへのバックアップ ...など

プログラムと説明

index.ts
// --- Start Server ------------------------------------------------------------router.post('/api/run/start',(req:express.Request,res:express.Response)=>{constschema=Joi.object({user:Joi.string().required(),})constvalidation=schema.validate(req.body)if(validation.error){post("不正なリクエストを拒否しました!","公式サイト・アプリ以外からチャレンジされた可能性があります。",3)res.status(400).send('Bad request')return}statusAsync().then(()=>{startAsync(req.body.user).then(()=>{res.send('GREAT')}).catch((err)=>{if(err=='failed due to run interval')post("起動コマンドを拒否しました","前回の処理の実行から"+process.env.WAIT_SECONDS_FROM_LAST_PROCESS+"秒経過していないため、コマンドを拒否しました。",2)elsepost("起動コマンドを拒否しました","サーバが既に起動しているため、起動コマンドを拒否しました。サーバとの同期ができていない恐れがあります。[Err: startAsync()]",2)res.status(400).send('Bad request')})}).catch(()=>{post("起動コマンドを拒否しました","既に起動しているため、起動コマンドを拒否しました。サーバとの同期ができていない恐れがあります。[Err: statusAsync()]",2)res.status(400).send('Bad request')})})// -----------------------------------------------------------------------------// 中略

まず、Joiを用いてバリデーションを実装しました。
これはプロセスを実行したユーザのログを取るため、プレイヤー名を取得するようにしています。

今回Minecraftサーバをコントロールする上で、起動後すぐに停止コマンドを受け付けてしまうと、ワールド展開中にサーバの終了などを行う可能性があるため、よろしくないのです。また、複数人が同時にプロセスを実行する可能性もあるため、リクエストの間隔を設定することにしました。リクエストの間隔は初期値60秒にしてますが、.envファイルで変更可能です。
リクエストのログは./db/log.sqliterunテーブルに保存されます。

またDiscordへのwebhook通知はcurlを使いました。

webhook.ts
require('dotenv').config()constexec=require('child_process').execconstdateformat=require('dateformat')constnow=newDate()constsqlite=require('sqlite3').verbose()constdb=newsqlite.Database('db/log.sqlite')// ######   SETTINGS   #########################################################constcolor_success=6815520constcolor_warning=16768300constcolor_error=16724530constcolor_notify=12895428constwebhook_icon=process.env.WEBHOOK_ICONasstringconstwebhook_copyright=process.env.WEBHOOK_COPYRIGHTasstringconstwebhook_url=process.env.WEBHOOK_URLasstring// #############################################################################exportfunctionpost(title:string,description:string,status:number){// 1: Success (green)// 2: Warning (yellow)// 3: Error (red)// 4: Notify (gray)switch(status){case1:varcolor=color_successbreakcase2:varcolor=color_warningbreakcase3:varcolor=color_errorbreakcase4:default:varcolor=color_notify}constjson=`
        {
            "embeds": [ {
                "title": "${title}",
                "description": "${description}",
                "timestamp": "${dateformat(now,'yyyy-mm-dd HH:MM:ss+09:00')}",
                "color": "${color}",
                "footer": {
                    "text": "© ${dateformat(now,'yyyy')}${webhook_copyright}",
                    "icon_url": "${webhook_icon}"
                }
            } ]
        }
    `exec(`curl -H "Content-Type: application/json" -X POST -d '${json}' "${webhook_url}"`,(err:string,stdout:string,stderr:string)=>{db.serialize(()=>{db.run('CREATE TABLE IF NOT EXISTS webhook (id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(80), description VARCHAR(255), url VARCHAR(80), created_at DATETIME, err TEXT, stdout TEXT)')conststmt=db.prepare('INSERT INTO webhook (title, description, url, created_at, err, stdout) VALUES (?, ?, ?, ?, ?, ?)')if(err){stmt.run([title,description,webhook_url,dateformat(now,'yyyy-mm-dd HH:MM:ss'),stderr,stdout])}else{stmt.run([title,description,webhook_url,dateformat(now,'yyyy-mm-dd HH:MM:ss'),'',stdout])}stmt.finalize()})})}

以下のような関数でDiscordに通知を送ることができました!

test.ts
post("ここにタイトル","ここに本文を記入します!色は Error(赤)・Warning(黄)・Success(緑)から選べます。その他の設定は`.env`ファイルから設定できます。",3)

Discordに通知がきます(一応Apple信者です)
screenshot.png

次にシェルコマンドを実行する部分です。

run.ts
require('dotenv').config()constexec=require('child_process').execconstdateformat=require('dateformat')constsqlite=require('sqlite3').verbose()constdb=newsqlite.Database('db/log.sqlite')exportfunctionstatusAsync():Promise<any>{returnnewPromise((resolve,reject)=>{exec(`pgrep -f ${process.env.GAME_SCREEN_NAME}`,(err:string,stdout:string,stderr:string)=>{if(!stdout)resolve('OK')elsereject('NG')})})}exportfunctionstartAsync(req:string):Promise<any>{returnnewPromise((resolve,reject)=>{runIntervalCheck(req).then((row)=>{if(row){exec(`cd ${process.env.GAME_DIR}&& screen -UAmdS ${process.env.GAME_SCREEN_NAME} java -Xmx1024M -Xms1024M -jar ${process.env.GAME_JAR} nogui`,(err:string,stdout:string,stderr:string)=>{if(err){runLog(req,'start','failed to start')reject('failed to start')}else{runLog(req,'start','success')resolve('success')}})}else{runLog(req,'start','failed due to run interval')reject('failed due to run interval')}}).catch((err)=>{console.log(err)})})}// 中略
run.ts
// runLog()functionrunLog(user:string,command:string,result:string){db.serialize(()=>{db.run('CREATE TABLE IF NOT EXISTS run (id INTEGER PRIMARY KEY AUTOINCREMENT, user VARCHAR(80), command VARCHAR(80), result VARCHAR(80), created_at DATETIME)')conststmt=db.prepare('INSERT INTO run (user, command, result, created_at) VALUES (?, ?, ?, ?)')constnow=newDate()stmt.run([user,command,result,dateformat(now,'yyyy-mm-dd HH:MM:ss')])stmt.finalize()})}
run.ts
// runIntervalCheck()functionrunIntervalCheck(user:string){returnnewPromise((resolve,reject)=>{db.serialize(()=>{db.all('SELECT COUNT(*) FROM sqlite_master WHERE TYPE="table" AND name="run"',(err:string,row:any)=>{if(row===undefined){resolve(false)return}elseif(row[0]['COUNT(*)']){db.all('SELECT created_at FROM run WHERE command="reservation" ORDER BY created_at DESC LIMIT 1',(err:string,date:any)=>{if(err){reject(err)}else{constnow_interval=newDate()now_interval.setSeconds(now_interval.getSeconds()-parseInt(process.env.WAIT_SECONDS_FROM_LAST_PROCESSasstring))resolve(date[0]['created_at']<dateformat(now_interval,'yyyy-mm-dd HH:MM:ss'))return}})}else{resolve(true)}})db.run('CREATE TABLE IF NOT EXISTS run (id INTEGER PRIMARY KEY AUTOINCREMENT, user VARCHAR(80), command VARCHAR(80), result VARCHAR(80), created_at DATETIME)')conststmt=db.prepare('INSERT INTO run (user, command, created_at) VALUES (?, ?, ?)')constnow_reservation=newDate()stmt.run([user,'reservation',dateformat(now_reservation,'yyyy-mm-dd HH:MM:ss')])stmt.finalize()})})}

runLog()関数で、実行結果をDBに保存していきます。
runIntervalCheck()関数では、前回の実行から決められた時間経過しているかをbool型で返します。
前回の処理から規定時間経過しているか、二重起動にならないかをチェックし、どちらもOKだった場合に同期処理でコマンドを実行します。

最後に、今回追加された.envファイルの環境変数の説明です。

.env
SERVER_URL='example.com'
SERVER_PORT=25565

GAME_DIR='/home/user/minecraft/'
GAME_JAR='server.jar'
GAME_SCREEN_NAME='minecraft'

WAIT_SECONDS_TO_STOP=30
WAIT_SECONDS_TO_RESTART=10
WAIT_SECONDS_FROM_LAST_PROCESS=60

WEBHOOK_ICON='https://example.com/icon.png'
WEBHOOK_COPYRIGHT='Minecraft Management System'
WEBHOOK_URL='https://discordapp.com/api/webhooks/xxxxxxxxxx'
環境変数名
GAME_DIRserver.propertieseula.txtなどゲームデータが存在するディレクトリ
GAME_JARMinecraft公式サイトからダウンロードできるjarファイル名
GAME_SCREEN_NAMEscreenで実行する際のセッション名
WAIT_SECONDS_TO_STOP

WAIT_SECONDS_TO_RESTART
screenshot.pngここの秒数↑
WAIT_SECONDS_FROM_LAST_PROCESS処理実行後、次のリクエストを受け付けない時間
WEBHOOK_ICONwebhook通知の際、送信元を表すアイコン (URL)
WEBHOOK_COPYRIGHTwebhook通知の際、送信元を表す名前
WEBHOOK_URLDiscordで発行したwebhookのURLを入力

Discordのwebhookは受け付けられるJSONの縛りが厳しいので、うまくいかない場合はDBに残っているエラーログを参照してください。
(あとでこっそり.envでメモリサイズを変更できるように修正しておきます)

次回はサーバアイコンとプレイヤー画像の取得機能を実装したいと思います。ぜひご覧ください!

参考

https://minecraft.server-memo.net/minecraftserver-3/#Minecraft


Node.js で Google Cloud Storage に署名付きURLを使ってファイルアップロードする方法

$
0
0

GCSにクライアントからアップロードする

画像、ドキュメント、映像、音声など様々な種類のファイルをクラウドストレージにアップロードするシーンは多々あります。そんなとき クライアント => バックエンド => クラウド の順番にアップロードしていると通信に時間がかかります。そこで、署名付きURLを使って クライアント => クラウド で直接アップロードするといい感じです。

AWSのS3は結構ドキュメントなり記事なりが充実していますが、GCS x Node.js のものがほとんどなく苦労したので残しておきます。

1.署名付きURLを発行(バックエンド)

署名付きURLはGCSのサービスクレデンシャルが必要なのでバックエンドで行います。フレームワークにexpressを使っています。

signed_url.ts
importexpressfrom"express";exportconstpost_signed_url=async(req:express.Request,res:express.Response)=>{const{Storage}=require('@google-cloud/storage');constclient=newStorage();constoptions={version:'v4',action:'write',expires:Date.now()+15*60*1000,contentType:'application/octet-stream',};consturl=awaitclient.bucket("your_bucket_name").file(req.body.file_name).getSignedUrl(options);res.send(url)}

ポイントはcontentTypeです。上記のように'application/octet-stream'にしないといけません。

2.署名付きURLを取得(クライアント)

1で作ったAPIを叩いて署名付きURLを取得します。
file_nameにはアップロードするファイルの名前を入れます。

get_signed_url.ts
post_signed_url(){this.$axios.post('/api/post_url',{file_name:"your_uploading_file.png"}).then((res:any)=>{this.signed_url=res.data[0]})}

3.アップロードできるようにバケットのCORSを設定します。

上記で取得したurlをそのまま使ってもCORSではじかれてしまうので設定します。

3.1. 好きなディレクトリで下記ファイルをつくります。ファイル名は自由です。

cors.json
[{"origin":["http://localhost:3000"],"responseHeader":["Content-Type","Authorization","Content-Length","User-Agent","x-goog-resumable"],"method":["GET","POST","PUT","DELETE","OPTIONS"],"maxAgeSeconds":3600}]

3.2 さっき作ったファイルを指定してバケットに設定します。

$ gsutil cors set cors.json gs://your_bucket_name

以下のコマンドで設定を確認できます。

$ gsutil cors get gs://your_bucket_name

4.クライアントからアップロードします。

変数 signed_url には2で取得したものを設定します。

client.js
upload_file(){constfile=document.querySelector('#file').files[0];if(signed_url!=null&&target!=undefined){this.$axios.put(signed_url,file,{headers:{'Content-Type':'application/octet-stream'}}).then((res:any)=>{console.log(res)})}

クライアント側でもContent-Typeを指定のようにしないといけません。どんなファイル形式でも。

以上です!

【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~ 【Cover】

$
0
0

ワイ 「この記事のカバーです」
https://qiita.com/kiwatchi1991/items/58b53c5b4ddf8d6a7053

ワイ 「暇やし久々Webアプリつくるか」
ワイ 「ニートでカネないから0円で運用できる構成にするで」

ワイ 「AWSとかGCPとかはクラウド破産が怖いから手ださない」
ワイ 「なんでAWSとかは課金に上限設定できないんやろか」

成果物

https://minna.itsumen.com

ワイ 「3日でつくった」

リポジトリ

フロントエンド

https://github.com/yuzuru2/minna_frontend

バックエンド

https://github.com/yuzuru2/minna_backend

構成

フロントエンド: Netlify(月間100GBまで転送量無料)
バックエンド: Netlify Functions(月間125000回まで無料 いわゆるFaaS)
データベース: MongoDB Atlas(ストレージ512MBまで無料 いわゆるDaaS)
SNSログイン: Firebase Authentication(使い放題)

ワイ 「最近流行のSPA(シングルページアプリケーション)でつくる」
ワイ 「静的ファイルはNetlifyに置いて」
ワイ 「Netlify Functionsでサーバ側の処理する」

ワイ 「データベースは」
ワイ 「本当はRDB使いたいが」
ワイ 「0円でそこそこ使えるRDBを使えるDaaSを知らん」
ワイ 「知っている人いたらラインでもツイッターでもいいんでDMください」

ワイ 「データベースはMongoDB Atlas使う」
ワイ 「Firebase Firestore使おうか迷ったけど」
ワイ 「ページングしにくいんだよねFireStore・・・」

ワイ 「MongodbはRDBのJoinぽいこともできて」
ワイ 「検索にわりと柔軟性がある」
ワイ 「だから今回はMongoDB Atlas使う」

使用ライブラリ周り

フロントエンド

  • TypeScript
  • Parcel
  • React
  • Redux

バックエンド

  • TypeScript
  • webpack
  • Express
  • mongoose
  • firebase-admin
  • netlify-lambda

ワイ 「割とよくある構成ですな」

~今回はここまで~

ワイのGitHubとか

GitHub: https://github.com/yuzuru2
YouTube: https://www.youtube.com/channel/UCuRrjmWcjASMgl5TqHS02AQ
Qiita: https://qiita.com/yuzuru2
LINE: https://line.me/ti/p/-GXpQkyXAm
Twitter: https://twitter.com/yuzuru_program
成果物まとめ: https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378

DMやプルリクやお仕事待ってます^^

【nodemailer】お前らのOAuth2は間違っている

$
0
0

・はじめに

個人開発でGmailを使おうとしたのですが、「安全性の低いアプリをブロックしました(ドヤッ」って通知&設定の自動変更がウザかったのでOAuth2を導入しました。その過程で「gmailのOAuth2認証をnodemailerからちゃんと通したい」だけなのに結構回り道をしたので忘備録です。

まあ実はnodemailer公式ドキュメント読めば秒で解決するけど公式ドキュメント読まない人もいるからね。仕方ないね。(ブーメラン


ちなみにGoogleさんからIDとかシークレットとかリフレッシュトークンを取得する方法についてはnode.js 上の nodemailer で OAuth 2.0 を使って gmail からメールを送るが一番詳しいと思います。それ以前にOAuthって何ぞやって人は各自ググってください。要はTwitterのアレです。




・よくないかもしれない実装

maybeBadExample.js
varnodemailer=require('nodemailer')// メッセージvarmessage={from:'送信元',to:'送信先',subject:'タイトル',text:'本文'};// 認証情報varauth={type:'OAuth2',user:'ユーザ名@gmail.com',clientId:'クライアントID',clientSecret:'クライアントシークレット',refreshToken:'リフレッシュトークン'};// トランスポートvartransport={service:'gmail',auth:auth};vartransporter=nodemailer.createTransport(transport);transporter.sendMail(message,function(err,response){console.log(err||response);});

Node.jsでGmailからメールをOAuth認証で送信する方法から引用
↑晒しあげるのは流石にどうかと思ったけど「nodemailer gmail oauth2」で検索すると2番目に出てくるので責任は重い。恨むならGoogleを恨んで




var使ってるとかそういうのは本筋じゃないのでさて置くとしても、このコードをそのまま流用してmodule.exportなりrequire()なりすると間違いなくGoogleさんに怒られます。

というのも後半のnodemailer.createTransport()これはOAuth認証を通すメソッドなのですが、これだとリクエストの度に新しいアクセストークンを要求してしまいます。アクセストークン自体は発行してから30分ほど有効なので、まあ軽い気持ちで都度都度新しいトークンを要求したらGoogleさんも当然オコですよね。衛宮士郎かよ。


でまあ対策としてはnodemailer.createTransport()で作成したインスタンスを適当な場所にキャッシュしておきましょう。あとは引数とかrequire()とかグローバル変数で渡せばOK

実はnodemailer君、最初に一回createTransport()してしまえばアクセストークンの管理は全部自動でやってくれますイェイ!優秀!!!



一応コード例としてクラス内でcreateTransport()した例を置いておきます。

Authed.js
classAuthed{constructor(){letnodemailer=require('nodemailer');this.transporter=nodemailer.createTransport({host:'smtp.gmail.com',port:465,secure:true,// SSLauth:{type:'OAuth2',user:'送信元アドレス',clientId:'クライアントID',clientSecret:'シークレット',refreshToken:'リフレッシュトークン'}});}}module.exports=newAuthed();





ちなみに呼び出し側はこんな感じ、require()がキャッシュを持つ仕様を利用してOAuth認証済みのインスタンスを呼び出してます。

// メッセージvarmessage={from:'送信元',to:'送信先',subject:'タイトル',text:'本文'};lettransporter=require("./Authed").transporter;transporter.sendMail(message,function(err,response){console.log(err||response);});




・まとめ

つまり何が言いたいんだってばよって結論を3行でまとめると
・nodemailer.createTransport()は一回でOK
・アクセストークンはnodemailerが隠蔽してくれる
・公式ドキュメント読め

って感じです。

というかOAuthの仕組みを知ってれば、アクセストークンが一切出てこない時点で違和感あると思うんですけどどうなんでしょうね。今回の調べ物を通して、やはりいくらフレームワークが便利だとはいえ、その前提知識は必要なんだなって思いを新たにした次第です。

ちなみに個人開発の進捗ですが、こんなお気持ちポエム書いてるぐらいには順調です。9月にはクローズドベータ、10月にはリリースの予定なのでもうしばらくお待ちください。

それではまた次回

javascriptの標準入力

$
0
0

javascriptで標準入力からデータを受け取る

・標準入力からデータを受け取る

例)「hello」というデータが渡される

process.stdin.resume();process.stdin.setEncoding('utf8');varlines=[];;//標準入力から受け取ったデータを格納する配列varreader=require('readline').createInterface({ //readlineという機能を用いて標準入力からデータを受け取るinput:process.stdin,output:process.stdout});reader.on('line',(lines)=>{ //line変数には標準入力から渡された一行のデータが格納されているlines.push(lines); //ここで、lines配列に、標準入力から渡されたデータが入る});reader.on('close',()=>{ //受け取ったデータを用いて処理を行うconsole.log(lines) //helloが出力される}

受け取ったデータの処理

●整数化する
例)「1」が標準入力から与えられている場合

(...)reader.on('close',()=>{ //受け取ったデータを用いて処理を行うvarn=parseInt(lines[0]);//「1」を整数として処理できるようになる}

●データをコンマで分解する
例)「1,2」「3,4」というデータが与えられた場合

(...)reader.on('close',()=>{ //受け取ったデータを用いて処理を行うvarArr1=lines[0].split(',')//Arr1配列に「1」「2」が格納されるvarnum_Arr1=Arr1.map(function(str){returnNumber(str);//num_Arr1配列内に、Arr1配列を整数化したものを格納});varArr2=lines[1].split(',') //Arr2配列に「3」「4」が格納されるvarnum_Arr2=Arr2.map(function(str){//num_Arr2配列内に、Arr2配列を整数化したものを格納returnNumber(str);)});}

map()メソッドとは

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します

●応用編
※Teratailで質問した際にいただいた回答をQiitaでまとめています
https://teratail.com/questions/256288

(...)reader.on('close',()=>{lines.map(e=>e.split(',')).flat().map(e=>+e);})

・上記のコードで行っていること
①「e」(elementの略)という仮配列に、「1,2」「3,4」をコンマで分割した値をいれる。
※e配列の中身:[[1 2][3 4]]

.flat()メソッドを用いて配列の深さを揃える
※e配列の中身:[1 2 3 4]

③mapメソッドを用いて、e配列に整数化したe配列を代入する
単項算術演算子 (+、-、++、--) はすべて、必要に応じてオペランドを数値に変換するという特性がある為、「(e => +e);」という処理で整数化を行える。


mapメソッドによって以上の処理をした中身がlines配列に代入される。
lines配列の中身は
[1 2 3 4]に更新される。

【Alexa】hostedスキル起動中の変数を保存する

$
0
0

はじめに

Alexaスキル起動中に使用する変数を保存、読み出しする方法を記載します。

準備

新機能 hostedスキルで作成するの章を進めてスキルをテスト出来るところまで進めてください。
参考

スキルを開いた後、「こんにちは」など挨拶をした回数をカウントするスキルを作ってみましょう。
なお筆者はこの時点でnode.jpの知識がほとんどありません。

まずはグローバル変数countを使って「こんにちは」と発音した回数を数えてみましょう。

index.js
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,// session persistence, api calls, and more.constAlexa=require('ask-sdk-core');letcount;constLaunchRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='LaunchRequest';},handle(handlerInput){constspeakOutput='こんにちは などの挨拶の回数をカウントします。';count=0;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){count+=1;constspeakOutput=`挨拶の回数は ${count}回です。`;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constHelpIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.HelpIntent';},handle(handlerInput){constspeakOutput='これはあなたが作ったアレクサのスキルです。。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constCancelAndStopIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&(Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.CancelIntent'||Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.StopIntent');},handle(handlerInput){constspeakOutput=`さようなら。挨拶の回数は ${count}回でした。`;returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}};constSessionEndedRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='SessionEndedRequest';},handle(handlerInput){// Any cleanup logic goes here.returnhandlerInput.responseBuilder.getResponse();}};// The intent reflector is used for interaction model testing and debugging.// It will simply repeat the intent the user said. You can create custom handlers// for your intents by defining them above, then also adding them to the request// handler chain below.constIntentReflectorHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest';},handle(handlerInput){constintentName=Alexa.getIntentName(handlerInput.requestEnvelope);constspeakOutput=`あなたが言ったのは ${intentName}ですね`;returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};// Generic error handling to capture any syntax or routing errors. If you receive an error// stating the request handler chain is not found, you have not implemented a handler for// the intent being invoked or included it in the skill builder below.constErrorHandler={canHandle(){returntrue;},handle(handlerInput,error){console.log(`~~~~ Error handled: ${error.stack}`);constspeakOutput=`すみません、よく聞き取れませんでした。`;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};// The SkillBuilder acts as the entry point for your skill, routing all request and response// payloads to the handlers above. Make sure any new handlers or interceptors you've// defined are included below. The order matters - they're processed top to bottom.exports.handler=Alexa.SkillBuilders.custom().addRequestHandlers(LaunchRequestHandler,HelloWorldIntentHandler,HelpIntentHandler,CancelAndStopIntentHandler,SessionEndedRequestHandler,IntentReflectorHandler,// make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers).addErrorHandlers(ErrorHandler,).lambda();

新しく出てきた命令だけ説明します。

let.js
letcount;

グローバル変数としてcountを定義しています。グローバル変数なのでここで定義すればほかの場所でも扱えるようになります。

letは変数定義を行うときに使用します。letは変更可能な変数に、constは値固定の変数に使用します。
constでの定義は値固定となりますが、その変数がオブジェクトの場合、そのオブジェクトのプロパティは変更することが出来ます。

これを実行してみます。

「ノードテストを開く」「こんにちは」で本来であれば、挨拶をした回数をカウントされるはずですが、カウントされ・・・ますね・・・

本来であればhandlerInput.attributesManager.getSessionAttributes();handlerInput.attributesManager.Requestattributes();handlerInput.attributesManager.Persistentattributes();
などを使用して変数の保存と読み込みをするのですが・・・

次回、この件も含めて調査してから修正したいと思います。

handlerInput.attributesManager.getSessionAttributes()を使用した例を下記に示します。

index.js
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,// session persistence, api calls, and more.constAlexa=require('ask-sdk-core');constLaunchRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='LaunchRequest';},handle(handlerInput){constspeakOutput='こんにちは などの挨拶の回数をカウントします。';constattr=handlerInput.attributesManager.getSessionAttributes();attr.count=0;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constHelloWorldIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='HelloWorldIntent';},handle(handlerInput){constattr=handlerInput.attributesManager.getSessionAttributes();letcount=attr.count;count+=1;attr.count=count;constspeakOutput=`挨拶の回数は ${count}回です。`;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constHelpIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.HelpIntent';},handle(handlerInput){constspeakOutput='これはあなたが作ったアレクサのスキルです。。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};constCancelAndStopIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&(Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.CancelIntent'||Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.StopIntent');},handle(handlerInput){constattr=handlerInput.attributesManager.getSessionAttributes();letcount=attr.count;constspeakOutput=`さようなら。挨拶の回数は ${count}回でした。`;returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}};constSessionEndedRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='SessionEndedRequest';},handle(handlerInput){// Any cleanup logic goes here.returnhandlerInput.responseBuilder.getResponse();}};// The intent reflector is used for interaction model testing and debugging.// It will simply repeat the intent the user said. You can create custom handlers// for your intents by defining them above, then also adding them to the request// handler chain below.constIntentReflectorHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest';},handle(handlerInput){constintentName=Alexa.getIntentName(handlerInput.requestEnvelope);constspeakOutput=`あなたが言ったのは ${intentName}ですね`;returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};// Generic error handling to capture any syntax or routing errors. If you receive an error// stating the request handler chain is not found, you have not implemented a handler for// the intent being invoked or included it in the skill builder below.constErrorHandler={canHandle(){returntrue;},handle(handlerInput,error){console.log(`~~~~ Error handled: ${error.stack}`);constspeakOutput=`すみません、よく聞き取れませんでした。`;returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};// The SkillBuilder acts as the entry point for your skill, routing all request and response// payloads to the handlers above. Make sure any new handlers or interceptors you've// defined are included below. The order matters - they're processed top to bottom.exports.handler=Alexa.SkillBuilders.custom().addRequestHandlers(LaunchRequestHandler,HelloWorldIntentHandler,HelpIntentHandler,CancelAndStopIntentHandler,SessionEndedRequestHandler,IntentReflectorHandler,// make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers).addErrorHandlers(ErrorHandler,).lambda();

Next.js × TypeScript × Firebaseでポートフォリオ

$
0
0

はじめに

初投稿です。
Next.js / TypeScript / Firebaseを使ってポートフォリオサイトを作ってみました。
作ったあとでNextのルーティング機能とかいらなかったなと思ったのは秘密...

デプロイしたのでとりあえず公開。
作り方を初学者の方向けにまとめてみました!ご意見いただけると嬉しいです!

随時更新します!

お前誰?

こういう者です...(成果物)
最近は自作キーボードを作ったり、スタートアップ企業で2社ほどお手伝いさせていただいています!

誰向けの記事?

React, Next.jsのチュートリアルくらいはやったことある!という初学者の方向けです。

目次

  1. Hello World
  2. _app.tsx / _document.tsx
  3. Material-UI
  4. 画像
  5. デプロイ

参考資料

1. Hello World

Next.jsをインストール

~/workspace/portfolio
$npm init -y$npminstall--save react react-dom next
$npminstall--save-dev typescript @types/react @types/node
$mkdir pages
$touch pages/index.tsx

補足:@types/~は型定義ファイル

git始めます

※gitでコード管理していない人はとばしてください!

~/workspace/portfolio
$git init
# 忘れずに.gitignoreを書き変えましょう$vi .gitignore
.gitignore
.DS_store

# next.js build output
.next
.next/

# Logs
npm-debug.log*

.vscode/

# node.js
node_modules/
npm-debug.log

# dotenv environment variables file (build for Zeit Now)
.env
.env.build

追記し終わったら...

~/workspace/portfolio
$git add .$git commit
$git remote add origin https://github.com/hoge/fuga.git
$git push origin master

では、HelloWorldしましょう。

pages/index.tsx
import{NextPage}from'next';constHome:NextPage=()=>(<h1>Hello world!</h1>);exportdefaultHome;

デバッグサーバを立てます。

~/workspace/portfolio
$npm run dev

localhost:3000にアクセスしてみましょう。
出来たてホヤホヤのHelloWorldがいるはずです!

2. _app.tsx / _document.tsx

Next.jsで開発するなら、この2つのファイルをpages/ディレクトリに置いてください。
まずはこちらの記事を参考に書いてみましょう!

これらのファイルの詳しい説明は下記のブログが分かりやすかったので引用させていただきました。

_document.tsxを書くと吐き出されるHTMLファイルの構成を変えることができ、_app.tsxを書くとすべてのRouteコンポーネントがここで書いたコンポーネントによってラップされるようになります。
                       TypeScriptでNext.js 9を触った感想ーkohei.dev

3. Material-UI

Google Material Design のガイドラインに沿ったデザインフレームワーク。
多機能かつ洗練されていて素晴らしいと思います。
たまに痒いところに手が届かないですが...

早速インストール!

~/workspace/portfolio
$npminstall @material-ui/core @material-ui/icons @material-ui/styles @material-ui/lab

Hello Worldしていきましょう!

pages/index.tsx
import{NextPage}from'next';import{Button}from'@material-ui/core';constHome:NextPage=()=>(<Buttonvariant="contained"color="primary">
      Hello World
    </Button>);exportdefaultHome;

Hello Worldボタンができました。

tsconfig.json

さて、こちらのページを見ながらMaterial-UI用のtsconfig書いていきましょう

tsconfig.json
{"compilerOptions":{"target":"es6","lib":[+"es6"+"dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"strict":true,"forceConsistentCasingInFileNames":true,"noEmit":true,"esModuleInterop":true,"module":"esnext","moduleResolution":"node","resolveJsonModule":true,"isolatedModules":true,"jsx":"preserve",+"noImplicitAny":true,+"noImplicitThis":true,+"strictNullChecks":true},"exclude":["node_modules"],"include":["next-env.d.ts","**/*.ts","**/*.tsx"]}

font-awesome

Material-UIのアイコンだけで物足りない!という人はfont-awesomeを使ってみてください。
もちろんMaterial-UIのIconコンポーネントが対応しています。

font-awesome

スッスッ

※上下スクロールのシングルページにしたのでやめました
react-swipeable-viewsでページがスッスッと動くUIを作れます。
詳しくはMaterial-UIのTagsコンポーネントをみてね

~/workspace/portfolio
$npm i --save react-swipeable-views @types/react-swipeable-views

4. 画像

前提

next.jsではpublicという名前のディレクトリに静的ファイルを置く必要があります。
公式リンク

publicを作成して、その配下に画像をおきましょう。

~/workspace/portfolio
$mkdir public
$mv ~/hoge.png public/
プラグイン

next-imagesというプラグインがあります
画像の表示にはこちらを使ってみました。

~/workspace/portfolio
$npminstall--save next-images @types/next-images

↓インストールできたら、以下で画像表示ができるはず!

next.config.js
constwithImages=require('next-images');module.exports=withImages();

デプロイ

タイトル通り、デプロイはFirebaseで行います。
今回はFirebase Hostingに静的ページとしてデプロイしましょう。

まずは、firebase-toolsを導入。

~/workspace/portfolio
# firebaseと接続するツール$npm i -g firebase-tools

# projectIDを記述$vi .firebaserc

続いてFirebase Consoleにアクセスし、プロジェクトを作成。
プロジェクトのIDを取得してください!
取得できたら書き加える...

.firebaserc
{"projects":{"default":"xxxxxxxxx"}}
~/workspace/portfolio
$firebase login
# このコマンドでプロジェクトが表示されたら成功!$firebase projects:list 

package.jsonのscriptsにexportコマンドを追加。

package.json
{"scripts":{"test":"echo \"Error: no test specified\"&& exit 1","dev":"next","deploy":"firebase deploy","build":"next build","start":"next start",//exportを追加"export":"next export"},}

Firebase Hostingの設定を書きます。

firebase.json
{"hosting":{"public":"out","ignore":["firebase.json","**/.*","**/node_modules/**"]}}
補足
next.jsの静的ファイル置き場のディレクトリ名はpublicでした。
Firebase Hostingでデフォルトの公開ファイル置き場もpublicなので書き換えます。

https://nextjs.org/learn/excel/static-html-export

ビルドしたファイルをまとめて静的ページにしてデプロイするためのフォルダを作りましょう。

~/workspace/portfolio
$mkdir out

できたら準備完了。いよいよデプロイ。

~/workspace/portfolio
$npm run build && npm run export$firebase deploy

表示されたHosting URLにアクセスしてみましょう!

参考資料

UI

Reactでポートフォリオを作ってみた
爆速デザイン

環境

環境構築
コミッターのquiita

実装 / アーキテクチャ

Next.js on Cloud Functions for Firebase with Firebase Hosting
Next.jsとFirebaseで更新の手間がかからないポートフォリオサイトを作ってみた
Typescript + React、Firebase Hosting & Functions、Github GraphQL APIを使って自分のポートフォリオサイトを作った。
firebase上でtypescriptで書かれたnextjsを動かす
ちょっと真面目なNext.js【Firebase Hosting編】

JavaScriptでJSONファイルを同ディレクトリ上に生成する

$
0
0

結論からいうとnodeを使って生成します。fsモジュールを使います。
「JavaScript JSONファイル 生成」とJavaScriptを使ったキーワードでググったところ某スクールの違う、そうじゃない、な記事やあまり関係のない記事が出てきてしまったため対抗馬としてJavaScriptを前面に押して記事にしました。(結局は他の記事と同じようにnodeを使いますが)

特に初学者は「ググっても出てこないじゃないか」、と困っているかと思います。記事の前提をクリアして手を動かしてみてください。

追記:もっと良い書き方あれば教えてもらえるとハッピーになります。

【この記事の前提】 nodeをインストールしよう

nodeとはサーバーサイドのJSです。ブラウザ以外のところでもJavaScriptを実行できるよ、という実行環境になります。
nodeとセットで語られるnpmは聞いたことがある人が多いと思いますがnodeで動く様々なモジュールを引っ張ってくるためのパッケージマネージャーです。

nodeをインストール

初心者は以下を参考に導入すると良いかと思います。
MacにNode.jsをインストール
windows10にNode.jsをインストールする

インストールの仕方はいくつかあり、今後JSフレームワークを使いたい人はanyenv経由でnodenvを導入してnodeのバージョン切り替えを楽にプロジェクトに応じてできるようにしておくと良いかと思います。
anyenv から入れた nodenv で Node.js を入れたときのメモ
MacにNode.jsをインストール(anyenv + nodenv編)
↑環境構築に少しは慣れている人向けです。自分はシェルをzsh環境へ変えるところから沼にハマってしまいしかもつまづいたところをメモに残してなかったのでどうやったのか思い出せない(バージョン切り替えできなくて残念でした)

node -v

でバージョンが表示されればOKです。

任意のディレクトリに今回のためのファイルを用意

% mkdir createObj //createObjフォルダを作る
% cd createObj //移動する
% touch create.js //jsファイルを作る

最小単位でfsモジュールを使ってJSONファイルを生成する

fsモジュールはnodeパッケージマネージャ(npmやyarn)からインストールしてこなくても最初からnodeの機能として備わっているためすぐに使うことができます。
create.jsのファイル編集します。

create.js
constfs=require('fs');consttestObj={test:'dammy',};constcreateFile=(pathName,source)=>{consttoJSON=JSON.stringify(source);fs.writeFile(pathName,toJSON,(err)=>{if(err)rej(err);if(!err){console.log('JSONファイルを生成しました');}});};createFile('newObj.json',testObj);

fs.writeFileでpathNameで指定したファイルを生成しています。
コマンドライン(ターミナル)で node create で実行すると newObj.json のファイルが同ディレクトリ上で生成されます。

% node create 
newObj.json
{"test":"dammy"}

JSONの中身を変えて再度実行するとファイルが上書きされます。

create.js
consttestObj={test:'second dammy',};

工夫1 ファイルのディレクトリ場所を変える

下の形で相対パスを指定すればOKです。

createFile('./任意のフォルダ/newObj.json', testObj);

工夫2 ファイルを上書き保存されないようにする

ディレクトリチェックを行えるfs.statSync()メソッドを用いてファイル重複を確認します。
同ディレクトリ上に既存ファイルがあった場合はreturnでメッセージを返します。

create.js
constfs=require('fs');consttestObj={test:'dammy',};constcreateFile=(pathName,source)=>{constisExist=dupliCheck(pathName);//booleanを挟むif(isExist)returnconsole.log('既存のパスが見つかりました');//エラーメッセを返すconsttoJSON=JSON.stringify(source);fs.writeFile(pathName,toJSON,(err)=>{if(err)throwerr;if(!err){console.log('JSONファイルを生成しました');}});};//新たに追加した関数constdupliCheck=(pathName)=>{try{fs.statSync(pathName);returntrue}catch(err){//パスが存在しない場合エラーを返すif(err.code==='ENOENT')returnfalse}};createFile('new.json',testObj);

工夫3 データ加工してJSONファイルを生成する

僕はこれがやりたくてfsモジュールを使いました。
エクセルからデータを加工すると手間がかかるので既存オブジェクトの値を元に計算した結果をオブジェクトに格納し生成したかったです。

ここでサンプルを用意しました。(なお、手っ取り早くデータが欲しいのでベストプラクティスみたいなところは意識してないんでそこらへんは許して欲しいンゴ。独学者のやり方なので良い方法あったら教えてください。)

サンプルデータの簡単な説明

例として食材データObjectに含む栄養データが1日あたりの摂取する推奨量に対してどのくらい割合を満たしているのかを算出したものをJSONデータとしてアウトプットします(dailyRationSample.jsonとして)

外部データを用意する

% mkdir seed
% touch seed/sampleStuff.js
% touch seed/sampleDaily.js

module.exports で外部データとして引っ張ってこれるようにしています。

sampleStuff.js
module.exports=[{id:1,Name:'アマランサス 玄穀 ',Protein:12.7,Iron:9.4,},{id:2,Name:'あわ 精白粒 ',Protein:11.2,Iron:4.8,},{id:3,Name:'あわ あわもち ',Protein:5.1,Iron:0.7,},];
sampleDaily.js
module.exports=[{name:'Protein',volume:60,},{name:'Iron',volume:7.5,},];

create.jsにデータを取り込んでオブジェクトを生成してファイルとしてアウトプットする

sampleStuff.jsの食材データの栄養素であるプロテイン(Protein)と鉄分(Iron)を sampleDaily.js の1日あたりの推奨量の大きさである volume とで割って割合を出してみましょう。

create.js
constfs=require('fs');conststuff=require('./seed/sampleStuff');//外部データを引っ張っているconstdaily=require('./seed/sampleDaily');//外部データを引っ張っている//新たに追加した関数constcreateObj=()=>{constobjArray=[],directArr=['id','Name'],divisionArr=['Protein','Iron'];stuff.map(stuffObj=>{constsingleObj={};directArr.map(keyName=>{singleObj[keyName]=stuffObj[keyName];});divisionArr.map(keyName=>{constdailyObj=daily.find(item=>keyName===item.name);const{volume:denomi}=dailyObj,//分割代入したvolumeの名前をdenomi(分母)にしているmolecule=stuffObj[keyName],//molecleは分子ratio=Math.round(molecule/denomi*1000)/1000;singleObj[keyName]=ratio;});objArray.push(singleObj);});returnobjArray;}constcreateFile=(pathName)=>{constisExist=dupliCheck(pathName);if(isExist)returnconsole.log('既存のパスが見つかりました');consttargetObj=createObj();consttoJSON=JSON.stringify(targetObj,null,4);//行数が多いのでオプション指定して改行しておくfs.writeFile(pathName,toJSON,(err)=>{if(err)throwerr;if(!err){console.log('JSONファイルを生成しました');}});};constdupliCheck=(pathName)=>{try{fs.statSync(pathName);returntrue}catch(err){if(err.code==='ENOENT')returnfalse}};createFile('dailyRatioSample.json');

上記を実行するとdailyRatioSample.jsonが生成されます。

dailyRatioSample.json
[{"id":1,"Name":"アマランサス 玄穀 ","Protein":0.212,"Iron":1.253},{"id":2,"Name":"あわ 精白粒 ","Protein":0.187,"Iron":0.64},{"id":3,"Name":"あわ あわもち ","Protein":0.085,"Iron":0.093}]

蛇足(記事更新)

JSON整形は別記事で説明しようと思いましたがやっぱりここで説明
下記のようにJSON.stringfy(オブジェクト, 置換オプション, 字下げ数)を指定して整形ことができます。

const toJSON = JSON.stringify(targetObj, null, 4);
つまり
const toJSON = JSON.stringify(オブジェクト, 置換オプション, 字下げ数);

「置換オプションってなんだよ」というところですが僕も使ったことがありません。(使うタイミングにまだ巡り合っていない)
JSON.stringify() - MDN Web Docs - Mozillaのサイトで構文を確認すると

JSON.stringify(value[, replacer[, space]])

となっています。また下の記事の引用では下記のような説明となっています。

"もし関数である場合、文字列化の間に出会った値とプロパティを変換します。もし配列である場合は、最終的な文字列のオブジェクトに含まれるプロパティの集合を指定します。" 引用:JSON.stringifyを改めて調べる。

JSONに含まれる関数と配列を置き換えできるようです。


息子の可愛さを普及するために、AWS + LINEでBotを作った話〜形態素解析導入編〜

$
0
0

我が家の息子が可愛すぎる。

可愛すぎるので、LINE Botを作成し布教したが、少し問題が・・・

はじめに

息子の可愛さを普及するために、AWS + LINEでBotを作った話の続編となります。元ソースや環境はこちらに記載してあります。

問題

こちらをご覧ください。
Bef.gif

「かわいい」と送信した際には、会話ができているが、「かわいいね」や「超!!かわいい!!!」など、「かわいい」という文字列にアドオンされた場合、該当するメッセージがないと判断され、「〜ってなんでちゅか?」と分からないフリをするかわいい。(親バカ)

原因

原因となる部分はこちら

constbody=JSON.parse(event.body)constevents=body.events[0]constmessage=events.message.textconstparam={TableName:'ReplyMapping',Key:{type:messageここ!!}}constresult=awaitdynamodb.get(param).promise()letmsg,imgif(result.Item){constgetRandomList=(list)=>list[Math.floor(Math.random()*list.length)]msg=getRandomList(result.Item.msg)img=getRandomList(result.Item.img)}else{msg=`${message}ってなんでちゅか?`img=''}

メッセージとtypeが完全一致した時に、DynamoDBからレコードを取得している。
今回の事象は、「かわいい」というレコードは登録されているが、「かわいいね」や「超!!かわいい!!!」といったレコードが登録されていないため発生している。

対策

じゃあ、メッセージを形態素解析して、どういった種類のメッセージか。解析するようにしたら良いのでは?

実装

kuromoji.jsを使って実現してみよう。
※ kuromoji.jsとは、javaのオープンソース日本語形態素解析エンジンkuromojiのjs移植版である。

ラッパークラスを作ってみた。

KuromojiWrapper.js
'use strict'constpath=require('path')constDIR='./node_modules/kuromoji/dict'constkuromoji=require('kuromoji')module.exports=classKuromojiWrapper{constructor(){}asyncinit(){this.tokenizer=awaitnewPromise((resolve,reject)=>{kuromoji.builder({dicPath:DIR}).build((err,tokenizer)=>{if(err)reject(err)resolve(tokenizer)})})}exec(text){returnthis.tokenizer.tokenize(text)}get(text){constres=this.exec(text)// 第一優先: 感動詞constprop1=res.find((o)=>o.pos==='感動詞')if(prop1)returnprop1.basic_form// 第二優先: 形容詞constprop2=res.find((o)=>o.pos==='形容詞')if(prop2)returnprop2.basic_formreturntext}}

this.tokenizer.tokenize(text)で形態素解析を行う。
例えば、超!かわいいね!という文字列の場合、kuromojiからのレスポンスは下記となる。

[{word_id:70940,word_type:'KNOWN',word_position:1,surface_form:'超',pos:'接頭詞',pos_detail_1:'名詞接続',pos_detail_2:'*',pos_detail_3:'*',conjugated_type:'*',conjugated_form:'*',basic_form:'超',reading:'チョウ',pronunciation:'チョー'},{word_id:91640,word_type:'KNOWN',word_position:2,surface_form:'!',pos:'記号',pos_detail_1:'一般',pos_detail_2:'*',pos_detail_3:'*',conjugated_type:'*',conjugated_form:'*',basic_form:'!',reading:'!',pronunciation:'!'},{word_id:1717990,word_type:'KNOWN',word_position:3,surface_form:'かわいい',pos:'形容詞',pos_detail_1:'自立',pos_detail_2:'*',pos_detail_3:'*',conjugated_type:'形容詞・イ段',conjugated_form:'基本形',basic_form:'かわいい',reading:'カワイイ',pronunciation:'カワイイ'},{word_id:92590,word_type:'KNOWN',word_position:7,surface_form:'ね',pos:'助詞',pos_detail_1:'終助詞',pos_detail_2:'*',pos_detail_3:'*',conjugated_type:'*',conjugated_form:'*',basic_form:'ね',reading:'ネ',pronunciation:'ネ'},{word_id:91640,word_type:'KNOWN',word_position:9,surface_form:'!',pos:'記号',pos_detail_1:'一般',pos_detail_2:'*',pos_detail_3:'*',conjugated_type:'*',conjugated_form:'*',basic_form:'!',reading:'!',pronunciation:'!'}]

この中から、posが感動詞となっているものを第一優先。形容詞となっているものを第二優先として、抜き出す。
※ ここは正直適当。優先順位を持たせることができたらとりあえずOK

呼び出し方としてはこう。
※ node.jsってconstructorでawaitできないのね・・・ちょっと強引に外部メソッドで実現。

sample.js
constKuromojiWrapper=require('./KuromojiWrapper')constmain=async()=>{// イニシャライズconstanalysis=newKuromojiWrapper()// 外部initawaitanalysis.init()constres=analysis.get('超!かわいいね!!')console.log(res)}main()// 実行結果// かわいい

既存の処理に組み込む

ソース全文

index.js
'use strict'constline=require('@line/bot-sdk')constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN})constcrypto=require('crypto')constAWS=require('aws-sdk')constdynamodb=newAWS.DynamoDB.DocumentClient({region:'ap-northeast-1'})constKuromojiWrapper=require('./KuromojiWrapper')exports.handler=async(event)=>{returnnewPromise(async(resolve,reject)=>{constsignature=crypto.createHmac('sha256',process.env.CHANNELSECRET).update(event.body).digest('base64')constcheckHeader=(event.headers||{})['X-Line-Signature']constbody=JSON.parse(event.body)if(signature!==checkHeader){reject('Authentication error')}constevents=body.events[0]constmessage=events.message.text// ここから 追加constanalysis=newKuromojiWrapper()awaitanalysis.init()constkey=analysis.get(message)// ここまで 追加constparam={TableName:'ReplyMapping',Key:{type:key// ここもいじった}}constresult=awaitdynamodb.get(param).promise()letmsg,imgif(result.Item){constgetRandomList=(list)=>list[Math.floor(Math.random()*list.length)]msg=getRandomList(result.Item.msg)img=getRandomList(result.Item.img)}else{msg=`${message}ってなんでちゅか?`img=''}constreplyText={type:'text',text:msg}constreplyImage={type:'image',originalContentUrl:img,previewImageUrl:img}client.replyMessage(events.replyToken,replyText).then((r)=>{returnclient.pushMessage(events.source.userId,replyImage)}).then((r)=>{resolve({statusCode:200,headers:{'X-Line-Status':'OK'},body:'{"result":"completed"}'})}).catch(reject)})}

結果

Aft.gif

固定文言じゃなくても、単文レベルならそれっぽくできた!!!

ただ・・レスポンス遅くね?

まとめ

それっぽいロジックは組み込めたけど・・・

それっぽいロジックを組み込むことができたが、考えれば考えるほど様々な処理を入れたくなる。(例えば年齢ロジックとか、天気とか)
これらを詰め合わせると、とんでもないことになりそうな予感。

パフォーマンス劣化

パフォーマンスがかなり劣化。
ローカルで動かした時より圧倒的に劣化しているので、NW周りとか、Lambdaのメモリーとかが関係しているのかな。

【SRE/JS】Cloud Run 周りの基礎

$
0
0

ローカル

イメージを作ってから、docker run -p 8080:8080 {Image ID}
コンテナ イメージをローカルでテストする  |  Cloud Run のドキュメント  |  Google Cloud

実装(JS)

コンテナ ランタイムの契約に記載されているように、コンテナは、Cloud Run によって定義され、PORT 環境変数で指定されているポートで受信リクエストをリッスンする必要があります。
Cloud Run のトラブルシューティング(フルマネージド)  |  Cloud Run のドキュメント  |  Google Cloudより引用

リクエストをリッスンすることをお忘れなく。

constexpress=require('express');constapp=express();constport=process.env.PORT||8080;app.listen(port,()=>{console.log('Hello world listening on port',port);});

デプロイ

手順

  1. イメージ作成、プッシュ
  2. デプロイコマンド実行
    Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング  |  Firebase

イメージ作成

docker build -t gcr.io/{PROJECT_ID}/js-cloud-run .
docker push gcr.io/{PROJECT_ID}/js-cloud-run

コマンドでのデプロイ

gcloud run deploy js-cloud-run \--image gcr.io/yojo-linebot-test/js-cloud-run \--port 8080 \--platform managed \--region asia-northeast1 \ # リージョン--allow-unauthenticated--revision-suffix v1 # つけたいrevisionの名前をつける

リビジョンに名前をつけてデプロイしたい場合は、--revision-suffixを指定します。
このコマンドで付与した revisonには、デプロイ完了時点では、トラフィックは飛ばないようになっています。

トラフィックの割合をコマンドで管理したい場合は、gcloud run services update-trafficコマンドに --to-revisionsオプションを付与することで実行できそう。

サンプルコマンド
gcloud run services update-traffic myservice --to-revisions=myservice-s5sxn=10,myservice-cp9kw=90
gcloud run services update-traffic  |  Cloud SDK のドキュメント  |  Google Cloud

ブルーグリーンデプロイメント

Cloud Runでは、下記のように設定することで、トラフィックの分割を容易に実施することができます。
image.png
image.png

ロールバック、段階的なロールアウト、トラフィックの移行  |  Cloud Run のドキュメント  |  Google Cloud

TODO

  • ホットリロード環境の構築
  • Cloud Run の CI/CDパイプライン構築

Azure Static Web AppsのAPI作成でLINE BOTを作る

$
0
0

前に書いた記事でAzure Static Web Appsを簡単に試してみましたが、後半の手順でAPIの作成も出来ました。

参考: たぶん10分で試せる。Azure Static Web AppsにWebサイトをデプロイして独自ドメイン設定とFunctionsでAPI公開まで

今回はこれを使ってLINE BOTを作ってみようと思います。

ちなみにStatic Web AppsのAPIの中身はAzure Functionsになる模様です。

参考: Azure Functions による Azure Static Web Apps プレビューでの API のサポート

環境

  • macOS Catalina
  • Node.js v12系

現状だとローカル実行やAzure FunctionsがNode.js v12までしか対応してなさそうでした。

APIエンドポイントを作ってみる(おさらい)

こんな手順です。

  • まずは参考記事をもとに、Azure Static Web Appで静的サイトを作成してみる
  • apiフォルダを作成し、さらにlinebotフォルダを作成
  • linebotフォルダ内にindex.jsfunction.jsonを作成
index.js
module.exports=asyncfunction(context,req){context.res={body:{text:"Hello from the API"}};};
function.json
{"bindings":[{"authLevel":"anonymous","type":"httpTrigger","direction":"in","name":"req","methods":["get"],"route":"linebot"},{"type":"http","direction":"out","name":"res"}]}

この状態でプッシュしてしばらくするとAPIが有効になります。

https://<APP ID>.azurestaticapps.net/api/linebotにアクセスするとAPIが見えます。

ローカル実行まで

まずはローカルで実行できる環境を整えます。

Azure Functionsの拡張機能のインストールと設定

VScodeにAzure Functionsの拡張機能をインストールします。

インストールが完了するとAzureマークがVSCodeのサイドバーに表示されます。選択するとサインインしましょう的なボタンがあるのでサインインします。

サインインすると少し読み込み待ち

Create New Project...のボタンを選択し、Browseを選択。

作成したapiフォルダを選択

言語選択でJavaScriptを選択

スクリーンショット 2020-08-17 1.27.33.png

テンプレート選択でHTTP triggerを選択

スクリーンショット 2020-08-17 1.27.28.png

関数名をwebhookとし、承認レベルをanonymousにします。

こんな感じでapiフォルダ内にwebhookフォルダやその他ファイルが作成されます。

Azure Functions拡張機能のタブに戻るとこのような表示になっています。

core toolsでローカル実行

Azure Functions Core Toolsというツールを使ってローカル実行をする模様です。

VScodeのコマンドパレットを開き(macだとcommand+shift+p)、core toolsと入力しインストールします。

スクリーンショット 2020-08-17 1.43.35.png

バージョン選択ですが、レコメンドされてるのでAzure Functions v3を選択します。

スクリーンショット 2020-08-17 1.43.44.png

しばらくするとインストールが完了します。

VSCodeのメニューのRun > Start Debuggingを選択するとローカルサーバーが起動します。

スクリーンショット 2020-08-17 1.55.28.png

ちなみに、このときにNode.js v14を利用してたらエラーが出ました。執筆時点ではv12までの対応らしいのでv12に切り替えたらうまく行きました。

ターミナルではこんな感じで作成したwebhooklinebotのエンドポイントが有効になったような表示になります。

ブラウザでアクセスしてみるとちゃんと表示されました。

LINE BOTプログラムの作成

現状だとルートディレクトリにapiフォルダindex.htmlがありますが、apiフォルダ内にpackage.jsonがあるのでそこでnpm installを実行していきます。

  • ルートからapiフォルダへ
$ ls
api        index.html
$ cd api
$ ls
host.json           local.settings.json package.json        webhook
linebot             package-lock.json   proxies.json
  • apiフォルダでモジュールのインストール
npm i @line/bot-sdk express azure-function-express

通常だとAzure Functionsのお作法で書く必要がありますが、azure-function-expressを利用することで、Azure Functionsっぽい書き方に依存せずに通常のexpressの利用っぽい雰囲気で書くことが出来ます。ひらりんさんの記事が参考になりました!

参考: LINE Bot を Azure Functions (Node.js) で作る際のオウム返しテンプレ

  • api/webhook/index.jsにコードを記述

'use strict';constline=require('@line/bot-sdk');constcreateHandler=require("azure-function-express").createHandler;constexpress=require('express');constconfig={channelSecret:'作成したBOTのチャンネルシークレット',channelAccessToken:'作成したBOTのチャンネルアクセストークン'};constapp=express();app.get('/api/webhook',(req,res)=>res.send('Hello LINE BOT!(GET)'));//ブラウザ確認用(無くても問題ない)app.post('/api/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);//ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。if(req.body.events[0].replyToken==='00000000000000000000000000000000'&&req.body.events[1].replyToken==='ffffffffffffffffffffffffffffffff'){res.send('Hello LINE BOT!(POST)');console.log('疎通確認用');return;}Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);asyncfunctionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}returnclient.replyMessage(event.replyToken,{type:'text',text:event.message.text//実際に返信の言葉を入れる箇所});}module.exports=createHandler(app);

大元はこちらの1時間でLINE BOTを作るハンズオンの記事のコードになっています。

参考記事をもとにLINE BOTのチャンネルシークレットとアクセストークンもコードに記載しておきます。

参考: 1時間でLINE BOTを作るハンズオン

おうむ返し

ngrokでトンネリングして試してみましょう。

コードが出来たら先ほどと同様にRun > Start Debuggingでローカルサーバーを起動させます。

今回(デフォ?)7071ポートで起動したのでngrokで7071ポートにトンネリングさせます。

$ npx ngrok http 7071

これは別のターミナルで実行しておいた方が良いと思います。

こんな感じでhttps://35027c542caa.ngrok.ioというURLが発行されました。

このURLを使ってLINE DevelopersでWebhook URLの設定を行います。

今回はエンドポイントが/api/webhookになっているので、この場合はhttps://35027c542caa.ngrok.io/api/webhookとなります。

この状態(ローカルサーバー起動中 + ngrok起動中 + Webhook URL登録済)でLINE BOTに話しかけると無事におうむ返しをしてくれました。

ひとまずローカルで実行できて一安心。。

デプロイして永続化する

デプロイは簡単で、もともとGitHub連携ありきでAzure Static Web Appsを作っているのでローカルで作ったものをプッシュすればOKです。

裏側でGitHub Actionsでビルドされる模様なので少し待ちましょう。

しばらくするとポータルの画面にもwebhook関数が表示されます。

https://<APP ID>.azurestaticapps.net/api/webhookにブラウザでアクセスするとHello LINE BOT!の表示が見えるようになると思います。

最終的にこのアドレスをLINE BOTの管理画面からWebhook URLに登録します。

これでデプロイも完了し永続化まで出来ました。

念のため最後におうむ返しがちゃんと動くか確認しましょう。

おまけ 環境変数の利用

ソースコードのチャンネルシークレットチャンネルアクセストークンをコードに直書きしてましたが環境変数置き換えも出来ます。

//省略constconfig={channelSecret:process.env.CHANNEL_SECRET,channelAccessToken:process.env.CHANNEL_ACCESS_TOKEN,};//省略

ポータルの構成の箇所から環境変数の追加ができます。

パスワードやトークンはここを活用すると良さそうです。

最後にチェックをして保存を押すことで保存されるのですが、これを忘れると値が保存されないので注意です。

まとめと所感

Static Web AppsでLINE BOTを作成することが出来ました。

当たり前ですがMSエコシステムということでVSCodeを使わないと厳しいかもしれないですね。
この辺は趣味趣向ありそうです。

内部はAzure Functionsを使ってるっぽいですが、Azure Functionsのリソースは一個も作っていないので金額とかは特に発生せずに使えてるのかな......

Static Web Appsの管理画面がシンプルすぎて稼働状況もいまいちわからないですが、とりあえずしばらく稼働させてみます。

Visual Studioのコードと同じDockerを使ってNode.jsアプリを開発する

$
0
0

このガイドでは、Ubuntu Linuxデスクトップ上でVisual Studio Codeを使ってNode.jsアプリを開発し、Alibaba Cloud上でDockerを使って同じアプリをデプロイしていきます。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

よくある質問

Node.jsとは?

Node.jsはオープンソースでクロスプラットフォームなJavaScriptのランタイム環境で、サーバー上でJavaScriptを実行することができます。

Express とは?

Expressは、最小限で柔軟性の高いNode.jsのWebアプリケーションフレームワークで、Webとモバイルアプリケーションのための堅牢な機能セットを提供しています。

Dockerとは?

Dockerは非常に人気のあるコンテナプラットフォームで、アプリケーションやサービスを簡単にパッケージ化、デプロイ、消費することができます。

Docker Hubとは?

Docker Hubは、Dockerユーザーやパートナーがコンテナイメージを作成、テスト、保存、配布するクラウドベースのリポジトリです。

Visual Studio Codeとは?

Visual Studio Codeは、デスクトップ上で動作し、Windows、MacOS、Linuxに対応した軽量かつ強力なソースコードエディタです。JavaScript、TypeScript、Node.jsをビルトインでサポートしており、他の言語(C++、C#、Java、Python、PHP、Goなど)やランタイム(.NET、Unityなど)に対応した拡張機能の豊富なエコシステムを備えています。Visual Studio Codeでは、Dockerを使ったアプリケーションのデプロイを簡単に行うことができ、プロジェクトの種類に応じて適切なDockerファイルの生成と追加をサポートしています。

Alibaba Cloud Simple Application Serverとは?

Simple Application Serverは、アプリケーションの起動と管理、ドメイン名解決の設定、ウェブサイトの構築、監視、保守を数回クリックするだけで行えるオールインワンのソリューションを提供します。プライベートサーバーの構築が格段に簡単になり、初心者がアリババクラウドを始めるのに最適な方法です。

アプリケーションの開発例

Ubuntu Linuxデスクトップに前提条件をインストール

私はバージョン16.04のUbuntu Linuxデスクトップを持っています。記載されている手順は、すべてのバージョンでほとんど同じです。

Node.js

node.jsが既にインストールされているかどうかを確認します。

node –v

バージョン番号が表示されない場合は、以下の手順でインストールしてください。

sudo apt-get install curl python-software-properties
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash –
sudo apt-get install nodejs

ノードとnpmのバージョンを確認します。

node –v
npm –v

Docker

DockerにはEnterprise版とCommunity版があります。今回はコミュニティ版のDocker CEをインストールします。

以前のバージョンがインストールされているかどうかを確認し、インストールされている場合はアンインストールしてください。

sudo apt-get remove docker docker-engine docker.io containerd runc

aptパッケージを更新します。

sudo apt-get update

aptがHTTPSでリポジトリを使用できるようにします。

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
           gnupg-agent \
    software-properties-common

GPGキーを追加

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add –

安定版リポジトリを設定してインストールします。

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

dockerをインストールします。

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

hello-worldイメージを実行して、Dockerが正しくインストールされているかどうかを確認します。

sudo docker run hello-world

このコマンドはイメージをダウンロードしてコンテナ内で実行し、そのコンテナ内でHello Worldを印刷します。

しかし、上記のコマンドはsudoまたはrootユーザのアクセスで実行されています。

次にインストールするVisual Studio CodeとそのDockerエクステンションをIDEとして使用する予定なので、非rootユーザで実行できるようにする必要があります。VS Codeは非rootユーザで動作し、拡張機能を使うことでコンテナ化されたアプリケーションの構築、管理、デプロイが容易になります。

Dockerグループを作成します。

sudo groupadd docker

ユーザーをdockerグループに追加します。

sudo usermod -aG docker $USER

ログアウトして再度ログインするか、グループのメンバーシップが再評価されるように再起動してください。

sudo reboot

再起動後、sudoなしでdockerコマンドが実行できることを確認します。

docker run hello-world

Visual Studioコード

VS Code をインストールする最も簡単な方法は、Snap パッケージとしてインストールすることです。スナップはすべての主要な Linux ディストリビューションで使用することができ、ほとんどの Ubuntu デスクトップにプリインストールされています。もしあなたのデスクトップにない場合は、https://docs.snapcraft.io/installing-snapdからインストールすることができます。

VSコードをインストールします。

sudo snap install --classic code

アプリケーション/プログラミングから同じものを選択するか、linuxターミナルでコードを入力してVisual Studioのコードを開きます。

「表示」→「拡張機能」に移動します。

Docker」を検索して、拡張機能をインストールします。拡張機能の詳細は https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-dockerを参照してください。

Node.jsプロジェクトの作成

アプリケーション/プログラミングから同じものを選択するか、linuxターミナルでコードを入力してVisual Studioのコードを開きます。

フォルダを開くを選択し、documentsディレクトリに移動します。ここでプロジェクトを作成します。

VS Codeにはシェルコマンドを実行するためのターミナルが統合されています。そこから直接Node.jsを実行することができます

Visual Studio Code のトップメニューから Terminal -> New Terminal を選択して Terminal を開きます。

Express用のテンプレエンジンをインストールするためにExpress Generatorをインストールします。

sudo npm install -g express-generator

新しいエクスプレスアプリケーションを足場にします。

express nodeexpress-alibaba-docker-tutorial --view=pug

これにより、nodeexpress-alibaba-docker-tutorialという名前の新しいフォルダが作成され、左のdocumentsの下にあるアプリケーションの内容を見ることができます。

コードの編集と変更

ここに独自の開発コードを追加します。ここでは、いくつかの簡単な変更を行います。

routes フォルダの下にある index.js を選択します。

index.jsの中のres.renderを含むコードの行に移動します。

以下のように変更してください。

res.render('index', { title: 'Docker on Simple Application Server', data:'Alibaba Cloud' });

フォルダビューの下にあるindex.pugを選択します。

Welcome toを含む行のデータタイトルを変更します。

アプリケーションの実行

VSCodeターミナルで、ディレクトリをアプリケーションフォルダに変更します。

cd nodeexpress-alibaba-docker-tutorial

package.jsonファイルに存在するアプリケーションの依存関係をインストールします。

npm install

image.png

Package.jsonファイルには、Node.jsアプリケーションを実行するためのスタートスクリプトも含まれています。

npm start

ブラウザを開き、localhost:3000にアクセスしてアプリケーションを表示します。ポート番号はスタートスクリプトから呼び出されるbinフォルダ内のファイルwwwの中に設定されています。

Visual Studio Codeのトップメニューから「ファイル」→「フォルダを閉じる」を選択し、VS Codeでアプリケーションを閉じます。

アプリケーションのDockerize例

パブリックリポジトリは、誰もが利用できるDockerイメージをホスティングするために使用することができます。Docker hubはパブリックリポジトリであり、利用するためのパブリックなDockerイメージのリストを見つけることができます。あとはそれらのイメージを引っ張ってきて、それを元にコンテナを起動し始めるだけです。Docker Hub上の公開リポジトリに公開することで、イメージを利用可能にしています。

Docker Hubアカウントとリポジトリの作成

https://hub.docker.com/にサインアップし、アカウントを作成したらログインします。

トップメニューからリポジトリを選択します。

アカウントを作成したばかりなので、リポジトリはありません。リポジトリの作成ボタンを選択してリポジトリを作成します。これがあなたの画像が保存されるリポジトリです。

image.png

次の画面では、リポジトリの名前と説明を指定します。ここでは下の画像のようにnodeexpress-alibaba-docker-tutorialという名前を指定します。

リポジトリを公開し、dockerhub上で検索できるようにしています。また、リポジトリを非公開にすることもできます。1つのプライベートリポジトリは無料で、課金プランを利用すればそれ以上の数のリポジトリを作成することができます。プライベートリポジトリは公開リポジトリと同じように動作しますが、閲覧や検索ができません。

image.png

Click on the Create button and your repository is ready.

image.png

画面の右端にあるコマンドに注目してください。このコマンドを実行して画像をこのリポジトリにプッシュする必要があります。

画像は [registry or username]/[image name]:[tag]の形式である必要があります。

注:Alibaba Cloudは、https://www.alibabacloud.com/product/container-registryで利用できる安全なコンテナレジストリも提供しています。

アプリケーションをDockerizeする

フォルダを開くを選択し、documentsフォルダの下にあるnodeexpress-alibaba-docker-tutorialフォルダに移動し、アプリケーションを開きます。

Visual Studio Codeのトップメニューから「ターミナル」→「新規ターミナル」を選択してターミナルを開きます。

Dockerがインストールされて実行されていることを確認します。

docker –version

Visual Studio CodeのトップメニューからView->Command Paletteを選択してCommand Paletteを開きます。

ワークスペースにDockerファイルを追加と入力し、Dockerを選択して実行します。Dockerファイルをワークスペースに追加するコマンドを実行します。

image.png

コマンドパレットでアプリケーションプラットフォームの選択を求められますので、Node.jsを選択します。

image.png

また、アプリケーションがリスンするポートを指定します。

image.png

これにより、ソースファイルの場所やコンテナ内でアプリを起動するコマンドなど、アプリの環境を記述するDockerfileと共にプロジェクトに特定のファイルが追加されます。

コマンドパレットでDocker: Build Imageを実行してイメージをビルドします。先ほど作成したDockerfileを選択し、Docker hubリポジトリの作成時に指定したフォーマットでイメージに名前を付けます。名前は......... arnab74/nodeexpress-alibaba-docker-tutorial:firsttry

image.png

ターミナルパネルが開き、Dockerコマンドが実行されます。ビルドが完了すると、DockerエクスプローラーのImagesの下にイメージが表示されます。

コマンドパレットでdocker runと入力し、Docker:Images runを選択してコンテナをビルドします。

image.png

コマンドパレットで、イメージグループ arnab74/nodeexpress-alibaba-docker-tutorialを選択します。

image.png

コマンドパレットで、Image firsttryを選択します。

ターミナルに表示された生成されたコマンドを実行します。

コマンドは'docker run'で、2つのフラグがあります。

-p : これはコンテナ上のポートを公開し、ホスト上のポートにマッピングします。ここでマッピングされたホスト上のポートも同様に3000です。

-d : バックグラウンドでコンテナを実行します。

詳細は https://docs.docker.com/engine/reference/commandline/run/を参照してください。

実行中のコンテナを検査します。

Docker ps

ブラウザを http://your_server_ip:3000または http://localhost:3000に移動してアプリケーションを表示します。

実行中のアプリケーションコンテナを停止するには、実行中のコンテナを右クリックして、DOCKER explorer の Containers の下にある stop を選択します。

image.png

または、ターミナルで以下のコマンドを実行することができます。

docker stop [CONTAINER ID]

CONTAINER ID]をDocker psコマンドで取得した自分のCONTAINER IDに置き換えてください。

お試し追加ステップ

VSCode の docker 拡張機能によって自動的に生成される dockerfile を改良しました。

https://hub.docker.com/_/node/から現在の nodejs LTS バージョンに基づいてアプリケーションのベースイメージを更新します。

コンテナをrootで実行することは避けなければなりません。Docker Nodeイメージには、rootではないノードのユーザが含まれています。ユーザーと同じにして、そのホームディレクトリを作業ディレクトリに設定します。

新しいDockerfileは以下のようになります。

FROM node:10.16-alpine
ENV NODE_ENV production
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
USER node
RUN npm install --production --silent && mv node_modules ../
COPY --chown=node:node . .
EXPOSE 3000
CMD npm start

前述のプロセスに従って、新しいイメージをビルドし、arnab74/nodeexpress-alibaba-docker-tutorial:latestという名前で実行してテストします。

Docker Hubに画像をアップロードする

Docker Explorerで、レジストリセクションのConnect Registryを選択します。

image.png

利用可能なオプションで、レジストリプロバイダとしてDocker Hubを選択します。

image.png

ユーザー名とパスワードを入力してログインします。

コマンドパレットでdockerタグを書き、Docker Images:Tagを選択します。

次のコマンドパレット画面で画像グループを選択します。

arnab74/nodeexpress-alibaba-docker-tutorial

次の画面でタグ付けする画像を選択

latest

新しいタグを提供

arnab74/nodeexpress-alibaba-docker-tutorial:0.1

コマンドパレットでdocker pushを書き、Docker Images:Pushを選択します。

次のコマンドパレット画面で画像グループを選択します。

arnab74/nodeexpress-alibaba-docker-tutorial

次の画面でDocker hubにプッシュするImageを選択します。

0.1

ターミナル上にDocker Pushコマンドが生成されるので、それに従ってください。

以下のようにターミナル上で上記の手順を行うこともできます。

docker login --username=arnab74
docker images

docker tag [IMAGE ID] arnab74/nodeexpress-alibaba-docker-tutorial:0.2

IMAGE ID を最新のタグ付けされた画像の画像 ID に置き換えてください。

docker push arnab74/nodeexpress-alibaba-docker-tutorial:0.2

アップロードされた画像は、Docker hubのサイトのリポジトリやDocker Explorerのレジストリで確認できるはずです。

シンプルなアプリケーションサーバにアプリケーションをデプロイする

並行記事Developing Node.js App in Visual Studio and Deploying on Simple Application Serverをご覧いただき、Alibaba Cloud上にCentOS7でSimple Application Serverインスタンスを作成し、インスタンスへのパスワードを作成し、デスクトップ端末を使ってログインする方法をご紹介します(記事のようにputtyではなく)。

サーバーに前提条件をインストールする

yum

yumは、CentOSでソフトウェアパッケージを取得、インストール、削除、クエリ、管理するための主要なツールです。まず、ソフトウェアリポジトリを最新のバージョンにアップデートするために使います。

yum -y update

Docker

古いバージョンがある場合は、アンインストールしてください。

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

必要なパッケージをインストールします。

sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2

インストールするリポジトリの設定

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

Dockerのインストール

sudo yum install docker-ce docker-ce-cli containerd.io

Dockerの起動

sudo systemctl start docker

Dockerを非rootユーザーで動作させます。

sudo groupadd docker
sudo usermod -aG docker $USER
sudo reboot
Start Docker again
sudo systemctl start docker

コマンドが sudo なしで実行されているかどうかを確認します。

docker run hello-world

アプリケーションのデプロイ

コンテナをビルドしてアプリケーションを実行します。マシンのローカルにイメージがない場合は、Dockerがリポジトリからイメージを引っ張ってきます。

docker run -d -p 80:3000 arnab74/nodeexpress-alibaba-docker-tutorial:0.1

ここでは、ホストに80番ポートを使用しました。そのため、簡易アプリケーションサーバの公開IP(サーバ管理画面で利用可能)を利用してサイトを閲覧する際には、ポート番号を使用する必要はありません。

http://public_ipにアクセスしてサイトをテストします。アプリケーションが起動しているのが確認できるはずです。

結論

これらは、Ubuntu Linux上でビジュアルコードを使用してNode.jsアプリケーションを開発し、アプリケーション用のDockerイメージを作成し、Dockerハブリポジトリを作成してDockerハブにイメージをプッシュし、最後にCentOSを使用してAlibaba CloudのSimple Application Server上にDockerハブイメージをデプロイするための基本的なステップです。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

Vue 3.0とgRPCを使ってTodoListを作ってみた

$
0
0

gRPCとは?

gRPCはオープンソース、RPCフレームワークをベースとして、最初はGoogleが開発されました。
インターフェース記述言語としてProtocol Buffersを使用し、protobufは構造化データをシリアル化するためのメカニズムです。
protoファイルでサービスとそのデータ構造を定義するだけで、gRPCがさまざまな言語でプラットフォームのクライアントとサーバーのStubsを自動的に生成します。
profobufを使用すると、JSONではなくバイナリを使用して資料を転送しています。
これにより、gRPCがはるかに高速で信頼性の高いものになります。
gRPCの他の主要な機能のいくつかは、双方向ストリーミングとフロー制御、BlockingまたはNonBlockingバインディング、および認証機能です。
gRPCはHTTP/2を使用して、シングルTCPコネクションの中で複数のストリームを開始することができます。
gRPCの詳細については、こちらをご覧ください:https://grpc.io/

gRPC V.S. REST

FeaturegRPCREST
Portocol HTTP/2 (早い)HTTP/1.1 (遅い)
Payload Protobuf (バイナリ、小さい)JSON (テキスト、大きい)
API構造 厳格、必要 (.proto)ゆるい、選択
Code生成 内蔵 (protoc)他のツール (Swagger)
安全性 TLS/SSLTLS/SSL
ストリーミング 双方向ストリーミングクライアント -> サーバーリクエストだけ
ブラウザのサポート制限あり (grpc-webは必要)ほぼ全部

Protobuf と gRPC を使えば、REST API の GET、PUT やヘッダーなどを気にする必要はありません、そしてgRPCフレームワークによって生成されたStubsにはデータモデル用の記述が全部書いてるので、直接引用するだけで使えます。

開発環境とツール

  • Protoc v3.12.4 -- Protobuf コンパイラー Stubs を生成する為に使ます。
  • Node.js v14.2.0 -- バックエンドとVueのビルドに使います。
  • Docker v19.03.12 -- envoyを動かす為に使ます。
  • envoy v1.14 -- 普通WebからのHTTP/1.1をHTTP/2に変換する為のプロキシ。
  • Vue.js 3.0.0-rc.5 -- 今回は Vue 3 を使ってフロントエンドを作成します。
  • Docker Compose v1.26.2 -- 全部をDocker化する為に使います、なくでも動けます。

フォルダ構成

dya2g02.png

全体の流れ

  1. Protoファイルの作成
  2. Node.jsでバックエンド作成
  3. Envoy proxyの設定
  4. Client stubsの生成
  5. Clientの作成
  6. Docker化

コードを書いてみましょう

1. Protoファイルの作成

ProtoファイルはgRPCの心臓と呼ばれる部分、ここでRequestとResponseとサービスを定義することによって、後でStubsファイルを自動的に生成することができます。
Protoファイルの構成は大体四つの部分に分けている。
1. Protoのバージョンを定義する
2. Packageの名前
3. サービス定義
4. メッセージ定義

todo.proto
syntax="proto3";packagetodo;servicetodoService{rpcaddTodo(addTodoParams)returns(todoObject){}rpcdeleteTodo(deleteTodoParams)returns(deleteResponse){}rpcgetTodos(getTodoParams)returns(todoResponse){}}// RequestmessagegetTodoParams{}messageaddTodoParams{stringtask=1;}messagedeleteTodoParams{stringid=1;}// ResponsemessagetodoObject{stringid=1;stringtask=2;}messagetodoResponse{repeatedtodoObjecttodos=1;//ここはArrayの中身にtodoObjectが複数ありますのこと。}messagedeleteResponse{stringmessage=1;}

2. Node.jsでバックエンド作成

環境設定:

npm:

bash
npm init -y
npm i uuid grpc @grpc/proto-loader
npm i -D nodemon

yarn:

bash
yarn init -y
yarn add uuid grpc @grpc/proto-loader
yarn add -D nodemon
package.json
{"name":"server","version":"1.0.0","description":"A Node.js gRPC API Server","main":"server.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1","start":"nodemon server.js"//ここでnodeをnodemonに書き換える},"author":"","license":"MIT","devDependencies":{"nodemon":"^2.0.4"},"dependencies":{"@grpc/proto-loader":"^0.5.5","grpc":"^1.24.3","uuid":"^8.3.0"}}

サーバーのコード内容:

server.js
// proto ファイルのパスconsttodoProtoPath='./todo.proto';// npm packageを導入constgrpc=require('grpc');constprotoLoader=require('@grpc/proto-loader');const{v4:uuidv4}=require('uuid');// grpcの初期化constpackageDefinition=protoLoader.loadSync(todoProtoPath,{keepCase:true,longs:String,enums:String,defaults:true,oneofs:true,},);// packageを指定consttodoProto=grpc.loadPackageDefinition(packageDefinition).todo;// Todosの保存、リースダートしたら資料が消えますletTodos=[];constaddTodo=(call,callback)=>{consttodoObject={id:uuidv4(),task:call.request.task,};console.log(call.request);Todos.push(todoObject);console.log(`Todo: ${todoObject.id} added!`);callback(null,todoObject);};constgetTodos=(call,callback)=>{console.log('Get tasks');console.log(Todos);callback(null,{todos:Todos});};constdeleteTodo=(call,callback)=>{Todos=Todos.filter((todo)=>todo.id!==call.request.id);console.log(`Todo: ${call.request.id} deleted`);callback(null,{message:'Success'});};constgetServer=()=>{constserver=newgrpc.Server();// サービスを登録、名前はprotoファイルと同じなので省略できますserver.addService(todoProto.todoService.service,{addTodo,getTodos,deleteTodo});returnserver;};if(require.main===module){constserver=getServer();server.bind('0.0.0.0:9090',grpc.ServerCredentials.createInsecure());server.start();console.log('Server running at port: 9090');}

3. Envoy proxyの設定

Envoy proxyはサーバーとクライアントの中央にいるサービスです、主にはHTTP/1.1のコネクションをHTTP/2に変換するの役割です。

Dockerのイメージ設定ファイル

Dockerfile
FROM envoyproxy/envoy:v1.14-latestCOPY ./envoy.yaml /etc/envoy/envoy.yamlCMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

Envoyの設定ファイル

envoy.yaml
admin:access_log_path:/tmp/admin_access.logaddress:socket_address:{address:0.0.0.0,port_value:9901}static_resources:listeners:-name:listener_0address:socket_address:{address:0.0.0.0,port_value:8080}filter_chains:-filters:-name:envoy.filters.network.http_connection_managertyped_config:"@type":type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManagerstat_prefix:ingress_httpcodec_type:AUTOroute_config:name:local_routevirtual_hosts:-name:local_servicedomains:["*"]routes:-match:{prefix:"/"}route:cluster:todo_servicemax_grpc_timeout:0scors:allow_origin_string_match:-prefix:"*"allow_methods:GET, PUT, DELETE, POST, OPTIONSallow_headers:keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeoutmax_age:"1728000"expose_headers:custom-header-1,grpc-status,grpc-messagehttp_filters:-name:envoy.filters.http.grpc_web-name:envoy.filters.http.cors-name:envoy.filters.http.routerclusters:-name:todo_serviceconnect_timeout:0.25stype:logical_dnshttp2_protocol_options:{}lb_policy:round_robinload_assignment:cluster_name:cluster_0endpoints:-lb_endpoints:-endpoint:address:socket_address:# docker-composeを使うときにserverに書き換えますaddress:host.docker.internalport_value:9090

gRPCサービスは9090 portで動かして、Envoyは8080 portでWebからのHTTP/1.1をHTTP/2に変換して9090に送るそいう仕組みです。

4. Client stubsの生成

protocをインストール : Protocol Buffer Compiler Installation

bash
# Linux$ apt install-y protobuf-compiler
$ protoc --version# MacOS using Homebrew$ brew install protobuf
$ protoc --version

先にVueのProjectを作成します。
vue-cliを使います。

bash
vue create client

そしてStubsを作ります
このコメンドで ./client/src に二つのJSファイルを生成する

  • todo_pb.js // メッセージのType定義
  • todo_grpc_web_pb.js // gRPCクライアント
bash
protoc -I server todo.proto \--js_out=import_style=commonjs,binary:client/src \--grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/src

5. Clientの作成

クライアントのTodoコンポーネントの中にtodo_pb.jsとtodo_grpc_web_pb.jsを導入して、todoServiceClient()を使ってlocalhost:8080のEnvoy proxyに接続します。

Todo.vue
import{ref}from'vue'// クライアントが使う部分だけを導入するimport{getTodoParams,addTodoParams,deleteTodoParams}from"../todo_pb.js";import{todoServiceClient}from"../todo_grpc_web_pb.js";importCloseIconfrom'./CloseIcon'exportdefault{components:{CloseIcon},setup(){consttodos=ref([])constinputField=ref('')// 新しクライアントのインスタンスを作成constclient=newtodoServiceClient("http://localhost:8080",null,null);constgetTodos=()=>{letgetRequest=newgetTodoParams();client.getTodos(getRequest,{},(err,response)=>{if(err)console.log(err);console.log(response.toObject());todos.value=response.toObject().todosList;});}getTodos()constaddTodo=()=>{letaddRequest=newaddTodoParams();addRequest.setTask(inputField.value);client.addTodo(addRequest,{},(err)=>{if(err)console.log(err);inputField.value="";getTodos();});}constdeleteTodo=(todo)=>{letdeleteRequest=newdeleteTodoParams();deleteRequest.setId(todo.id);client.deleteTodo(deleteRequest,{},(err,response)=>{if(err)console.log(err);if(response.getMessage()==="Success"){getTodos();}});}return{todos,inputField,addTodo,deleteTodo}}}

完成の参考 : Github

動かしてみましょう

Back-Endを立ち上げて:

bash
$ cd ./server
$ npm start

enovy proxy:

bash
$ docker build -t envoy:v1 ./enovy
$ docker run --rm-it-p 8080:8080 envoy:v1

Front-End:

bash
$ cd ./client
$ yarn dev

成功すればこんな感じです:
giphy.gif

6. Docker化

Docker Compose一発で動かす為にDocker化します。
各フォルダにDockerfileと.dockerignore入れます。

./serverフォルダ

Dockerfile
FROM node:lts-alpine# make the 'app' folder the current working directoryWORKDIR /app# copy both 'package.json' and 'package-lock.json' (if available)COPY package*.json ./# install project dependenciesRUN npm install# copy project files and folders to the current working directory (i.e. 'app' folder)COPY . .EXPOSE 9090CMD [ "node", "server.js" ]
.dockerignore
.git
.gitignore

node_modules

README.md

./clientフォルダ

Dockerfile
FROM node:lts-alpine# install simple http server for serving static contentRUN npm install-g http-server

# make the 'app' folder the current working directoryWORKDIR /app# copy both 'package.json' and 'package-lock.json' (if available)COPY package*.json ./COPY yarn.lock ./# install project dependenciesRUN yarn install# copy project files and folders to the current working directory (i.e. 'app' folder)COPY . .# build app for production with minificationRUN yarn run build

EXPOSE 3000CMD [ "http-server", "-p", "3000", "dist" ]
.dockerignore
.git
.gitignore

node_modules

README.md

./docker-composer.ymlを作成します。

docker-compose.yml
version:'3'services:web:build:./clientimage:todo-grpc-vue-client:v1ports:-3000:3000restart:unless-stoppednetworks:-grpc-todolistproxy:build:./envoyimage:todo-grpc-envoy:v1ports:-8080:8080restart:unless-stoppednetworks:-grpc-todolistserver:build:./serverimage:todo-grpc-server:v1restart:unless-stoppednetworks:-grpc-todolistnetworks:grpc-todolist:driver:bridge

Envoyの設定ファイルのサーバーアドレス修正します

envoy.yaml
endpoints:-lb_endpoints:-endpoint:address:socket_address:# host.docker.internalをserverに書き換えるaddress:serverport_value:9090

そしてビルドして立ち上げます。

bash
# イメージをビルド$ docker-compose build
# 特定のイメージをリビルドします$ docker-compose build --no-cache[service]
# 立ち上げる$ docker-compose up
# 背景で立ち上げる$ docker-compose up -d# ログを確認$ docker-compose logs

Front-Endに入ります:http://localhost:3000

最後

Qiitaで初めての投稿です、よろしくお願いします。
Githubでソースコードを公開しています、
なんが詰まったところがあれば参考してください:Github

Viewing all 8845 articles
Browse latest View live