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

APL(Alexa Presentation Language)で1文字ずつテキストをアニメーションさせる

$
0
0

APL、特にコマンドについて書いた記事がほとんどないようなので、書きます。
アドベントカレンダーということで、それなりに高度なことをやろうと思います。

やりたいこと

以下を実現したAlexaカスタムスキルを作成しようと思います。
 ・APLを使用し、画面付Echo端末で文字をアニメーション表示する
 ・1文字ずつ微妙に時間をずらして表示させる
 ・表示させる文字は動的に指定できる(今回はランダム)

APL標準コマンドはコンポーネント単位

APL1.1になってかなりいろいろなことができるようになりました。
APL標準コマンドを使うことで、コンポーネントを動かしたり、動的な変更が可能なプロパティの値を制御することができます。
ただ、APL標準コマンドの対象はコンポーネント1個が最小単位となるので、そこは注意が必要です。
例えば、textの値が「こんにちは」であるTextコンポーネントに対しては、この5文字を同時に扱うようなコマンドしか実行できません。

1文字ずつアニメーションさせるには

では、1文字ずつ動かすにはどうしたらよいかというと、単純に「1文字1コンポーネントにする」という方法で対応します。ここでポイントになってくるのは、「共通化」です。
内容がほとんど同じコンポーネントやコマンドが大量に出てきてしまうので、commandsやlayoutsをうまく使って対応するのがベターです。

とりあえず固定文言をアニメーションで出してみる

例えば、以下のようなAPLテンプレートで文字を1文字ずつアニメーションで表示させることができます。
payload参照がないので、datasource側のjsonは空のものでOKです。

template.json
{"type":"APL","version":"1.1","settings":{},"theme":"dark","import":[],"resources":[],"styles":{},"onMount":[],"graphics":{},"commands":{"anime":{"description":"アニメーションで表示する。intervalに設定した時間だけ遅れて出現する。","parameters":[{"name":"interval","type":"number","default":100}],"commands":[{"type":"SetValue","property":"opacity","value":0},{"type":"AnimateItem","easing":"ease-in-out","delay":"${interval}","duration":1000,"value":[{"property":"opacity","to":1},{"property":"transform","from":[{"translateX":30},{"translateY":30},{"rotate":30}],"to":[{"translateX":0},{"translateY":0},{"rotate":0}]}]}]}},"layouts":{"charLayouts":{"description":"animeコマンドを使い、intervalに設定した時間だけ待ってからtextの内容を出現させる。","parameters":[{"name":"interval","type":"number","default":"100"},{"name":"text","type":"string","default":""}],"items":[{"type":"Text","onMount":[{"type":"anime","interval":"${interval}"}],"text":"${text}"}]}},"mainTemplate":{"parameters":["payload"],"items":[{"type":"Container","width":"100vw","height":"100vh","alignItems":"center","direction":"row","justifyContent":"center","items":[{"type":"charLayouts","interval":100,"text":"A"},{"type":"charLayouts","interval":200,"text":"l"},{"type":"charLayouts","interval":300,"text":"e"},{"type":"charLayouts","interval":400,"text":"x"},{"type":"charLayouts","interval":500,"text":"a"}]}]}}

簡単に説明すると、上記APLは以下のような構造になっています。
 ・自作レイアウトとして、layoutsに"charLayouts"を、自作コマンドとしてcommandsに"anime"( = 軽く回転しながらもやっと出現する)を作成して定義している。
 ・"charLayouts"はパラメータ"text"と"interval"を持つ。
 ・"charLayouts"のパラメータ"text"は表示文言になる。
 ・"charLayouts"内では"anime"が使用されている。
 ・"anime"はパラメータ"interval"を持つ。
 ・"charLayouts"のパラメータ"interval"は"charLayouts"内のコマンド"anime"にそのまま渡す。
 ・"anime"にはアニメーションの内容が定義されている。
 ・"anime"のパラメータ"interval"はアニメーションを実行するタイミングになる。
 ・つまり、"charLayouts"を量産し、"interval"の値をちょっとずつずらすことで1文字ずつアニメーションさせることができる。

共通で使用する部分を"charLayouts"として定義し、コマンド部分も"anime"と定義することで、何度も使うレイアウトやコマンドを一回定義するだけで済んでいます。
イメージ的には、何度も出てくる処理を関数化した、みたいな感じです。

上記テンプレートをAlexaスキル側でAPL表示の際に設定すると、"Alexa"という文字が時間差で1文字ずつ同じアニメーションで出現する様子が画面に表示されます。
(Lambda側のソースは割愛)
anime1.gif

今回はもやっと文字を表示させていますが、各文字を画面外のランダムな位置からスライドして登場させたり、回転させながら出現させたり等、工夫次第でいろいろな演出ができます。

文字列部分のコンポーネントを動的に作成

上記の例ではすでに完成済みのテンプレートを読み込ませることで固定の文字列を出力していますが、工夫すれば文字列を動的に変更することができます。
具体的には、lambda側で以下を実施します。
 ・対象の文字列を1文字ずつに分断する。
 ・分断した文字数分だけcharLayoutsコンポーネントを作成し、配列化。
 ・作成したテンプレのitems部分に対し、作成したcharLayoutsコンポーネント配列をにセット。

結構力技なうえにテンプレを直接書き換える、かなり雑な方法ですが参考例として挙げさせていただきます。

aplTemplate.json
{"type":"APL","version":"1.1","settings":{},"theme":"dark","import":[],"resources":[],"styles":{},"onMount":[],"graphics":{},"commands":{"anime":{"description":"アニメーションで表示する。intervalに設定した時間だけ遅れて出現する。","parameters":[{"name":"interval","type":"number","default":100}],"commands":[{"type":"SetValue","property":"opacity","value":0},{"type":"AnimateItem","easing":"ease-in-out","delay":"${interval}","duration":1000,"value":[{"property":"opacity","to":1},{"property":"transform","from":[{"translateX":30},{"translateY":30},{"rotate":30}],"to":[{"translateX":0},{"translateY":0},{"rotate":0}]}]}]}},"layouts":{"charLayouts":{"description":"animeコマンドを使い、intervalに設定した時間だけ待ってからtextの内容を出現させる。","parameters":[{"name":"interval","type":"number","default":"100"},{"name":"text","type":"string","default":""}],"items":[{"type":"Text","onMount":[{"type":"anime","interval":"${interval}"}],"text":"${text}"}]}},"mainTemplate":{"parameters":["payload"],"items":[{"type":"Container","width":"100vw","height":"100vh","alignItems":"center","direction":"row","justifyContent":"center","items":[]}]}}
index.js
// 主要部分のみ記載constLaunchRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='LaunchRequest';},asynchandle(handlerInput){letst='';letrt='';st+='こんにちは!';rt+='エーピーエルのテストです。';st+=rt;// ※実際はスキル実行デバイスがAPL対応しているかどうかのチェックを入れる必要ありconstaplTemplate=require('./apl/aplTemplate.json');varaplData={};// payloadを使用していないので、空でも良い// 連続で呼んだ際、二重に文字が挿入されてしまうので、初期化aplTemplate.mainTemplate.items[0].items=[];consttargetStringArray=['APLはAlexa Presentation Languageの略だぞ!!!','カッコいいアニメーション','令和もよろしくね!'];consttargetString=targetStringArray[Math.floor(Math.random()*targetStringArray.length)];for(leti=0;i<targetString.length;i++){// 1文字ずつcharLayoutsのインスタンスを生成letcharInstance={type:"charLayouts",interval:i*100,// インデックス番号を利用することで表示間隔を微妙にずらしているtext:targetString.substring(i,i+1)};// テンプレートに直接突っ込むaplTemplate.mainTemplate.items[0].items.push(charInstance);}// 画面作成handlerInput.responseBuilder.addDirective({type:'Alexa.Presentation.APL.RenderDocument',version:'1.0',document:aplTemplate,datasources:aplData});returnhandlerInput.responseBuilder.speak(st).reprompt(rt).getResponse();}};

実際はContainerあたりをさらに別のLayoutsにしてしまってそちらに突っ込む(=テンプレ部はいじらないようにする)とか、Sequenceとpayloadをうまく使ってdatasource部にデータを持たせるとかすべきと思います。
ともあれ、こんな感じで1文字ずつdelayを微妙にずらすことができました。
実際にアニメーションを見てみると、1文字ずつ表示されてかなりリッチな感じになっています。

anime2.gif

今回は文字列を出現させるアニメーションでやってみましたが、ほかにもいろいろ応用できると思います。
例えば、文字列中の特定の文字だけ消えるとか、何個かの画像を画面外から降らせるとか、意外と何でもできてしまいます。

APLについてはとにかく記事等が少ない印象がありますが、基本的には公式のリファレンスを見て書いていけば良いので、しっかり目を通し、わからないところはフォーラムや問い合わせフォームで聞くと良いと思います。

なお、APLに凝りすぎるとレスポンスの最大サイズ(24KB)を超えてしまう可能性が出てくるので、注意が必要です。最大サイズを超えた場合はカードが送信されてくるみたいです。
そのため、ある程度慣れてくると共通化はかなり重要になってきます。

宣伝

APLはコマンドもそうですが、コンポーネントの使い方を工夫すれば意外といろいろできます。
私が最近作ったAlexaスキル「方程式ロボ」では、APLの表示可否やAPLコマンドを駆使して画面描写を実施しています。
(ロボ、x^2、x、√,i の部分は画像ですが、それ以外はすべてAPLのコンポーネントやコマンドで実現しています)
もしよろしければ、ためしに呼び出してみてください。スキル内の画面表示を見て、どうやってAPLで表現しているのか考えてみるのも良い勉強になるかと思います。

方程式ロボキャプチャ.PNG

参考URL

APL標準コマンド
APLのユーザー定義コマンド
APLレイアウト


【サーバレス時代の負荷テスト戦略】面倒な負荷テストとはおさらばしよう 〜CircleCIで実現する継続的負荷テストとチューニングTips〜

$
0
0

負荷テストとサーバレス

image.gif

負荷テストに対する考え方は時代とともに変化してきました。従来はサーバスペックやシステムの限界性能を測るという考え方でしたが、クラウドネイティブなシステムではそれに加えて、システムの弾力性(スケールアウトのしやすさ)も考慮する必要があります。

本記事では、負荷テストによるシステムの弾力性の評価と、改善する方法についてツールの具体的な使用方法やアプリケーションのチューニング Tips を交えて説明します。システムの弾力性を評価するために、プロダクション環境でのユーザからのリクエストを想定したロードテストを検討します。

ロードテストでは以下の項目を検証します。

ドリップテスト

ドリップテストは通常、数日間にわたって行われます。通常のバックグラウンド負荷レベルをシミュレートします。遅延またはエラー率の増加が見られる処理を特定します。

スラムテスト

スラムテストは、トラフィックの突然のスパイクをシミュレートします。これにより、オートスケーリングが適切に処理するのが従来困難であったトラフィックに直面したときのシステムの動作を確認できます。

ランプテスト

ランプテストでは、キャンペーン期間中などを想定した日々トラフィックが増加する負荷をシミュレートします。システムが正常にスケールアウトすることを確認します。

負荷テストツールの選定

上記のテストが実行できるように最良の負荷テストツールを選定します。

Apache ab

Apache の ab コマンドはコマンドラインから Web リクエストの負荷をかけることができるシンプルなツールです。手軽に導入できますが、単一のサーバからしか実行できないため、大量のリクエストを送ることができません。こちらの記事が参考になります。

Apache JMeter

Apache JMeter は言わずと知れた負荷テストツールです。マスタスレーブ構成を構築し、マスタとなるサーバにはスレーブの情報(ip アドレスなど)を登録しておきます。GUI でシナリオを作り、マスタからスレーブに指示を送ることでロードテストを実行します。スレーブを増やすことで大量のリクエスト数を送ることができます。
JMeter は確かに多機能で操作性も良く、手に馴染んでいるツールですが負荷をかけるためのサーバを構築したり、GUI からシナリオを作成する手間があります。

Locust

locust

Locust は Python 製の負荷テストツールです。
JMeter 同様、GUI が提供されており、マスタスレーブ構成を取り大量リクエストを生成できます。またテストシナリオを Python で記述でき、柔軟に対応できます。

Locust のシナリオの記述は容易ですが、 JMeter と同様にサーバを構築する必要があります。
アプリケーションがサーバレスで構築できるようになったのに負荷テストのためにサーバを立てなければいけないというのは残念なところです。

宣言的な負荷テストツールをサーバレスで実行するという選択

最近では JAMStackな構成を採用する場合が増え、バックエンドは Web API として提供することが多くなりました。
Web API だけを対象とするならば、画面の表示を想定せずシンプルなテストシナリオを作成できます。
つまり、テストしたい API のエンドポイントと Header, Body などを宣言的に羅列しておくだけ負荷テストを実行できるようなツールがあると幸せです。

Artillery

Artillery は yaml ファイルに宣言的にシナリオを記述し、シンプルなインタフェースで負荷をかけることができる Nodejs 製のツールです。

ローカル実行

npm コマンドでインストールします。

$ npm install -g artillery

ab コマンドのようにワンライナーが用意されています。
以下のコマンドは、10 人の仮想ユーザーを作成し、それぞれが 20 回の HTTP GET リクエストを https://artillery.io/に送信することを意味しています。

$ artillery quick --count 10 -n 20 https://artillery.io/

実際には以下のように script.ymlにテストシナリオを記述して使用します。

script.yml
config:target:'https://artillery.io'phases:-duration:60arrivalRate:20defaults:headers:x-my-service-auth:'987401838271002188298567'scenarios:-flow:-get:url:"/docs"

HTTP 経由で通信する https://artillery.ioのエンドポイントのテストシナリオです。 phasesには、20 人の新しい仮想ユーザーで 60 秒負荷テストを実行することを記述しています。リクエストは平均で 1 秒ごとに到達します。

以下のコマンドでテストシナリオを実行します。

$ artillery run script.yml

以下のように結果が出力されます。

Complete report @ 2019-01-02T17:32:36.653Z
  Scenarios launched:  300
  Scenarios completed: 300
  Requests completed:  600
  RPS sent: 18.86
  Request latency:
    min: 52.1
    max: 11005.7
    median: 408.2
    p95: 1727.4
    p99: 3144
  Scenario counts:
    0: 300 (100%)
  Codes:
    200: 300
    302: 300
項目説明
Scenarios launched直前の 10 秒間に作成された仮想ユーザーの数(または合計)
Scenarios completed直前の 10 秒間(またはテスト全体)でシナリオを完了した仮想ユーザーの数
Requests completed送信された HTTP リクエストとレスポンスまたは WebSocket メッセージの数
RPS sent直前の 10 秒間(またはテスト全体)に完了した 1 秒あたりのリクエストの平均数
Request latencyp99: 500 は 100 リクエスト中 99 リクエストが完了するまでに 500 ミリ秒以下かかったことを意味
Codes受信した HTTP 応答コードの内訳

Lambda を使用して大量のリクエストを生み出す

Artillery だけでは単一のサーバ上から実行するため、大量のリクエストを生成できませんが、方法があります。Artillery をサーバレスな実行環境に乗せてスケールさせる serverless-artilleryが公開されています。

同様のコンセプトを持ったツールにGoadがあります。Goad は、Go で構築された AWS Lambda 搭載の高度に分散された負荷テストツールです。ここでは名前だけの紹介に留めます。

serverless-artillery は Artillery を serverless framework を使用して AWS Lambda にデプロイします。低コストかつ短時間で大量リクエストを生成する負荷テスト環境を構築できます。

デプロイ

デプロイすると CloudFormation にスタックが作成されます。—stage オプションを使用することで複数の環境に対応させることができます。

$ slsart deploy --stage dev

image.png

実行

script.yml に記述されたシナリオを実行します。script.yml 以外の名前のファイルを実行したい場合は -o オプションをつけてシナリオファイルを指定します。

$ slsart invoke --stage dev

CloudWatch にダッシュボードを作成しておくことをおすすめします。

image.png

片付け

デプロイされた AWS Lambda と関連するリソースおよびスタックを削除します。

$ slsart remove

プラグインの追加

Artillery には様々なプラグインが用意されています。

artillery-plugin-cloudwatch

artillery-plugin-cloudwatch を追加することで、テスト結果を AWS CloudWatch に記録できます。
他にも DataDog 用のプラグインなどが用意されています。

image.png

$ npm install --save artillery-plugin-cloudwatch

以下に serverless.yml の一部を抜粋します。

service:serverless-artilleryprovider:name:awsruntime:nodejs8.10region:ap-northeast-1iamRoleStatements:-Effect:"Allow"Action:-"lambda:InvokeFunction"Resource:"Fn::Join":-":"--"arn:aws:lambda"-Ref:"AWS::Region"-Ref:"AWS::AccountId"-"function"-"${self:service}-${opt:stage,self:provider.stage}-loadGenerator*"# must match function name-Effect:"Allow"Action:-"sns:Publish"Resource:Ref:monitoringAlerts-Effect:"Allow"Action:-"cloudwatch:PutMetricData"Resource:-"*"

継続的に負荷テストをするという考え方

従来の開発プロセスでは、アプリケーションを開発し、決められたサーバスペックの VM にデプロイ、最終的に負荷テストを実施することでサーバスペックを決定していました。

クラウド、さらにはサーバレスなアプリケーションの開発プロセスにこの方法は最適でしょうか。

仮想マシン(VM)のスペックの決定、ワークロードに応じた VM 数の調整、障害や災害に備えた可用性の確保などの作業さえもクラウドに任せ、開発者はアプリケーションの開発に専念できるサーバレス。サーバレスはクラウドのあるべき姿を体現しています。素晴らしい技術です。

しかし、その一方で制限がある条件下でのアプリケーション開発は決して容易ではありません。
例えばサーバレスを代表とする Lambda が Web API としてユーザリクエストを受けるアーキテクチャを考えます。この構成がプロダクション環境での性能に耐えうるか、開発プロセスの早い段階で判断したいものです。もし仮にどうしてもプロダクション環境での性能を満たせなかった場合は Fargate に移植するなどの選択肢をとるべきです。

開発プロセスのなるべく早い段階で性能検証をするために CI のプロセスに負荷テストを組み込むアプローチをとります。

CircleCI から負荷テストを実行する

Artillery の宣言的なシナリオとシンプルな実行インタフェースにより、容易に CI に組み込むことができます。以下のようなフローを想定しています。

  1. アプリケーションのソースコードを master ブランチにマージする
  2. アプリケーションが AWS 上にデプロイされる
  3. 構築済みの serverless-artillery を使用して負荷テストが実行される
  4. CloudWatch のダッシュボードを確認する

img

.circleci/config.ymlは以下のようになります。説明の簡単化のためにキャッシュやユニットテストなどは割愛しましたがより詳細なソースはこちらに保管しています。

.circleci/config.yml
version:2jobs:build:docker:-image:circleci/node:7.10working_directory:~/reposteps:-checkout-run:npm install-run:npm run deploy:stgloadtest:docker:-image:circleci/node:7.10working_directory:~/reposteps:-checkout-run:npm --prefix ./loadtester install-run:npm --prefix ./loadtester run loadtestworkflows:version:2workflow:jobs:-build-loadtest:requires:-build

負荷にランダム性を持たせる

CircleCI から継続的に負荷テストを実行できるようになったので開発フェーズにおいて、常にある程度の負荷をかけ続けることができるようになりました。通常のバックグラウンド負荷レベルをシミュレートするドリップテストが継続的に実行できています。さらに、ある程度ユーザリクエストにゆらぎ(ランダム性)を持たせておくとより多くのケースをテストできます。
phasesの項目を動的に変更してみましょう。

script.yml
config:target:https://gatjk9gwc4.execute-api.ap-northeast-1.amazonaws.com/dev/phases:-duration:10# この部分をarrivalRate:1000000# ランダムに変化させるscenarios:-flow:-get:url:"/"

シナリオをランダムに変更する簡単なツールを作成します。

generateSenario.js
functionrand(min,max){min=Math.ceil(min);max=Math.floor(max);returnMath.floor(Math.random()*(max-min))+min;//The maximum is exclusive and the minimum is inclusive}functiongeneratePhases(phases,duration,arrivalRate){constgeneratedPhases=[];for(leti=0;i<rand(phases.min,phases.max);i++){generatedPhases.push({duration:rand(duration.min,duration.max),arrivalRate:rand(arrivalRate.min,arrivalRate.max)});}returngeneratedPhases;}functiongenerateScript(){fs=require("fs");yaml=require("js-yaml");constscript=yaml.safeLoad(fs.readFileSync("./script.yml","utf-8"));constphases={min:1,max:50};constduration={min:10,max:100};constarrivalRate={min:10,max:10000};script.config.phases=generatePhases(phases,duration,arrivalRate);fs.writeFileSync("./converted.yml",yaml.safeDump(script));}generateScript();

シンプルに node コマンドで実行しても良いですが、以下のように npm スクリプトで実行できるようにしておくと CircleCI の設定を記述する際に見通しがよくなります。

$ node generateSenario.js
.circleci/config.yml
version:2jobs:build:docker:-image:circleci/node:7.10working_directory:~/reposteps:-checkout-run:npm install-run:npm run deploy:stgloadtest:docker:-image:circleci/node:7.10working_directory:~/reposteps:-checkout-run:npm --prefix ./loadtester install-run:npm --prefix ./loadtester run generate:senario# <--- これを追加してランダム性を持たせる-run:npm --prefix ./loadtester run loadtestworkflows:version:2workflow:jobs:-build-loadtest:requires:-build

実行結果は以下のようになります。このようにしてランダムに生成された script.ymlを CI から実行します。今回のサンプルソースではスパイクがかかるようなケースを生成しませんでした。ランダムに生成する値をうまく調整することで、通常時の負荷を想定したドリップテストとスパイクアクセスを想定したスラムテストを同時に継続的に実行できます。

script.yml
config:target:'https://gatjk9gwc4.execute-api.ap-northeast-1.amazonaws.com/dev/'phases:-duration:78arrivalRate:6184-duration:94arrivalRate:4583-duration:73arrivalRate:6991-duration:82arrivalRate:2664-duration:92arrivalRate:5239-duration:33arrivalRate:1596-duration:51arrivalRate:9621-duration:94arrivalRate:4155-duration:51arrivalRate:3145-duration:36arrivalRate:802-duration:90arrivalRate:6286-duration:46arrivalRate:5219-duration:29arrivalRate:8020scenarios:-flow:-get:url:/

負荷テスト CI の環境下でアプリケーションを開発する

想定するアプリケーション

シンプルな CRUD 操作ができる API を開発します。
TODO リストアプリケーションです。ユーザからのリクエストを受け、TODO タスクを登録、更新、一覧表示、削除します。リクエストは APIGateway 経由で Lambda が処理します。データベースには DynamoDB を選択します。

アプリケーションのソースコードはこちらに保管しています。

todoapps

テストシナリオ

  • タスクを登録 取得したデータの 1 番目の id を保持する
  • 全件表示
  • 1件表示
  • タスクを更新(id 指定)
  • タスクを削除(id 指定)
  • タスクを登録 データが増大し続けることを想定してさらに登録するシナリオを入れておく

シナリオを作成して CircleCI から実行する

テストシナリオを作成してローカルでテスト実行します。

Artillary の設定では、 capture に引き受ける変数を指定することでレスポンスの値を次のシナリオに引き継ぐことができます。

script.yml
config:target:https://35vbyy1dxd.execute-api.ap-northeast-1.amazonaws.com/devphases:-duration:10arrivalRate:10scenarios:-flow:-post:url:/todosjson:text:"mytodo"capture:json:"$.id"as:"id"-get:url:/todos-get:url:/todos/{{ id }}-put:url:/todos/{{ id }}json:text:"mytodofinished"checked:true-delete:url:/todos/{{ id }}json:id:{{id}}
$ artillary run script.yml

All virtual users finished
Summary report @ 04:14:49(+0900) 2019-12-12
  Scenarios launched:  100
  Scenarios completed: 100
  Requests completed:  500
  RPS sent: 46
  Request latency:
    min: 71.7
    max: 1580.8
    median: 123.4
    p95: 687.9
    p99: 870.6
  Scenario counts:
    0: 100 (100%)
  Codes:
    200: 500 # <-- 正常にシナリオが実行できていることを確認

問題がすぐに検知できる

GitHub にソースをマージして CircleCI を動かしましょう。負荷テストが実行されます。
以下は、負荷テスト実施時の APIGateway のメトリクスです。
リクエスト数、レイテンシー、4xxError, 5xxError を表示しています。

image.png

CI によって自動的に負荷がかけられるようになり、開発プロセスの早い段階からパフォーマンスの問題が検知できるようになりました。まず初めに以下のプログラムの問題を特定しました。TODO タスクの一覧を取得する処理ですが DynamoDB のスキャンを使用しており、全件検索になってしまっています。APIGateway の制限 29 秒以上かかってしまうため 5xx エラーが多発しています。とても使い物になりません。

list.js
'use strict';constdynamodb=require('./dynamodb');module.exports.list=(event,context,callback)=>{constparams={TableName:process.env.DYNAMODB_TABLE,Limit:10// <-- まずは取得する件数を制限してみる};// fetch all todos from the databasedynamodb.scan(params,(error,result)=>{// handle potential errorsif(error){console.error(error);callback(null,{statusCode:error.statusCode||501,headers:{'Content-Type':'text/plain'},body:'Couldn\'t fetch the todo item.',});return;}// create a responseconstresponse={statusCode:200,body:JSON.stringify(result.Items),};callback(null,response);});};

また、DynamoDB のキャパシティユニットが圧倒的に足りません。デフォルト値のままでしたから、当然といえば当然ですが。これも解消しましょう。オートスケーリングの設定をします。

image.png

分散トレーシング

複数の Lambda Function や DynamoDB を使用したアプリケーションでは、分散トレーシングを有効活用することで、そのシステムの振る舞いを可視化できます。
AWS の分散トレーシングサービスである x-ray を使用します。必要なライブラリをインストールします。

$ npm install--save-dev serverless-plugin-tracing
$ npm install--save aws-xray-sdk

serverless.yml に必要な情報を追加します。

serverless.yml
provider:name:awsruntime:nodejs8.10region:ap-northeast-1environment:DYNAMODB_TABLE:${self:service}-${opt:stage, self:provider.stage}tracing:apiGateway:truelambda:trueiamRoleStatements:-Effect:AllowAction:-dynamodb:Query-dynamodb:Scan-dynamodb:GetItem-dynamodb:PutItem-dynamodb:UpdateItem-dynamodb:DeleteItemResource:"arn:aws:dynamodb:${opt:region,self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"-Effect:AllowAction:-xray:PutTraceSegments# <-- 追加-xray:PutTelemetryRecords# <-- 追加Resource:"*"

アプリケーションに aws-xray-sdk を追加し、トレース可能にします。

dynamodb.js
"use strict";+constawsXRay=require("aws-xray-sdk");+constAWS=require('aws-sdk');-constAWS=awsXRay.captureAWS(require("aws-sdk"));letoptions={};// connect to local DB if running offlineif(process.env.IS_OFFLINE){options={region:"localhost",endpoint:"http://localhost:8000"};}constclient=newAWS.DynamoDB.DocumentClient(options);module.exports=client;

この変更をコミットし、GitHub に PUSH します。負荷テストが始まると AWS の X-Ray コンソールに以下のようにマップやトレーシング情報が作成され始めます。

image.png

image.png

image.png

随分と性能改善ができました。

まとめ

serverless-artillery を使用することで低コストかつ短時間で負荷テストの実行環境を用意できました。さらに、CircleCIと組み合わせることで継続的に負荷を与え続けながら開発を進めることも可能です。AWS X-Rayなどの分散トレーシングのサービスを使用することで、高度に分散したアーキテクチャのボトルネックを特定することができました。

サーバレスがクラウドの未来だと信じています。全てがサーバレスになる時代はもうすぐ手の届くところにあります。この先もこの技術領域を開拓して、最良の開発ができるように務めていきたいです。

ローカルPCのNode.jsでaws-sdkの認証を通す

$
0
0

はじめに

ローカルPCでAWSの色々なサービスを操作するツールを作りたいときに、
認証が通せず苦労しましたが、この方法で通せるようです。

ファイルなんか作らずにメモリ内に閉じたかったのですが、無理そうでした。

認証を通すコード

dynamo-utility.ts
importAWS=require('aws-sdk');importFs=require('fs');exportclassDynamoUtility{dynamoDb:AWS.DynamoDB;documentClient:AWS.DynamoDB.DocumentClient;constructor(credential:any){consttmpFile='./credential.json';// 認証ファイルを生成Fs.writeFileSync(tmpFile,JSON.stringify(credential));// 生成したファイルを読み込ませるAWS.config.loadFromPath(tmpFile);// ファイル削除Fs.unlinkSync(tmpFile);// クライアント生成this.documentClient=newAWS.DynamoDB.DocumentClient();this.dynamoDb=newAWS.DynamoDB();}}
environment.ts
// ...credential:{accessKeyId:'xxxxxxxxxxxxxxxxxxxx',secretAccessKey:'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',region:'ap-northeast-1'},// ...

さいごに

役に立ちましたら、記事にいいねをお願いします。

Ubuntuにインストールしたyarnが実行できない

$
0
0

Ubuntuにインストールしたyarnが実行できない

Ubuntu 16.04に最新のyarnをインストールした際、yarnコマンドを実行すると以下のようなエラーが発生しました。

$ yarn -v
/usr/share/yarn/lib/cli.js:46099
  let {
      ^

SyntaxError: Unexpected token {
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:374:25)
    at Object.Module._extensions..js (module.js:417:10)
    at Module.load (module.js:344:32)
    at Function.Module._load (module.js:301:12)
    at Module.require (module.js:354:17)
    at require (internal/module.js:12:17)
    at Object.<anonymous> (/usr/share/yarn/bin/yarn.js:24:13)
    at Module._compile (module.js:410:26)
    at Object.Module._extensions..js (module.js:417:10)

yarnと関係ありそうなものとして、Nodejsがあるので、Nodejsのバージョンを確認します。

$ nodejs -v
v4.2.6

そういえば、レポジトリを更新しない状態でapt install nodejsをすると、古いバージョンがインストールされるようだった気がします。

yarnの公式HPを確認すると、Nodeのバージョンが4.8.0以上となっていました。
いつの間に!
Yarn公式HP

なので、今回はバージョン10系のnodejsがインストールできるように、レポジトリを追加します。
そして、インストール済みのnodejsを削除して、新しいnodejsをインストールします。
コマンドは以下の様になります。

*最新のバージョンをインストールしたい場合は、curlコマンドのurl部分を変更してください。
例) バージョン12系をインストールしたい
-> curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -

$ curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
$ sudo apt remove nodejs
$ sudo apt install nodejs

これで、yarnコマンドが実行できれば、解決です。

$ yarn -v
1.21.1

お疲れさまでした。

Node.jsでローカルにある複数の画像をディレクトリを分けつつリサイズする

$
0
0

ディレクトリ毎に分けた複数の画像を一度にリサイズするべく、スクリプトを書きました。
元の画像の縦横比が全て揃っており、リサイズする画像も元の画像の縦横比を保ったままにすることを前提としています。

環境

  • MacOS
  • Node.jsのバージョン・・・12.8.1
  • npmのバージョン・・・6.10.2

前提

以下がインストール済みであることを前提としています。

  • Node.js
  • npm(又はyarn)

ディレクトリ構造

├── node-extensions
│   └── sharp.js ← 実行するスクリプトファイル。
├── node_modules/
├── package.json
├── src
│   ├── images
│   │   ├── gallery
│   │   │   ├── l ← 元の画像を入れるディレクトリ。この下に年月毎のフォルダを作成。
│   │   │   └── s ← リサイズ後の画像を入れるディレクトリ。スクリプト実行後に生成。

パッケージのインストール・スクリプトの実行

必要なもの

  • sharp・・・画像のリサイズ
  • mkdirp・・・ディレクトリを生成

スクリプトを書く

sharp.js
constsharp=require('sharp');constfs=require('fs');constfsPromises=fs.promises;constglob=require('glob');constpath=require('path');constmkdirp=require('mkdirp');constRESIZE_WIDTH=600;// リサイズ後の画像の幅constORIGINAL_IMG_DIR=glob.sync('../src/images/gallery/l/*/');// 元画像を格納しているディレクトリconstIMG_DATE_DIR=ORIGINAL_IMG_DIR.map(imgDirPath=>imgDirPath.split('/',6)[5]);// 日付のディレクトリconstRESIZED_IMG_PARENT_DIRNAME='../src/images/gallery/s/';// リサイズ後の画像を入れるディレクトリ名constRESIZED_IMG_DIR_ARRAY=IMG_DATE_DIR.map((name)=>RESIZED_IMG_PARENT_DIRNAME+name);// リサイズ後の画像を入れるディレクトリ/**
 * 画像をリサイズしてリサイズ後のディレクトリに格納する
 * @param {string} imgPath リサイズ前の画像パス 
 * @param {string} outputFilePath リサイズ後の画像パス
 */functioncreateResizeImage(imgPath,outputFilePath){sharp(imgPath).resize(RESIZE_WIDTH).toFile(outputFilePath,(err)=>{if(err)console.error(err);return;});}/**
 * リサイズ後の画像を入れるディレクトリを生成
 */RESIZED_IMG_DIR_ARRAY.forEach((pathName)=>{mkdirp.sync(pathName,(err)=>{if(err)console.error(err);return;});});ORIGINAL_IMG_DIR.forEach((dirName,i)=>{constresolvedPath=path.resolve(dirName);fsPromises.readdir(resolvedPath).then((files)=>{console.log(files);files.forEach((file)=>{createResizeImage(resolvedPath+'/'+file,path.resolve(RESIZED_IMG_DIR_ARRAY[i])+'/'+file);});}).catch((err)=>{console.log(err.message);});});

スクリプトを実行する

node-extensionsのディレクトリまで移動して、以下を実行。

node sharp.js

src/images/gallery/l/の下に 201912/というディレクトリの中に画像ファイルを入れている場合。
スクリプト実行後に src/images/gallery/s/201912/のディレクトリが生成されて、リサイズ後の同一ファイル名の画像が格納されます。

Node.js v12でnode-gypでのビルドが通らない場合、v10をインストールすればよい

$
0
0

Node.js v12@Windowsでnode-gypを用いたビルドで以下のようなエラーが出る場合、v10をインストールすればビルドできます
※この例ではwinapiのビルドで失敗の例

v12ではnode-gyp v5.0.5が強制インストールされてしまうことに由来したエラーと睨んでいます。
※なお、MSBuild Toolの2015,2017,2019いずれでもv12ではうまくいきませんでした

gyp ERR! build error 
gyp ERR! stack Error: `C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onExit (C:\Program Files\nodejs\node_modules\npm\node_modules\node-gyp\lib\build.js:194:23)
gyp ERR! stack     at ChildProcess.emit (events.js:210:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Windows_NT 10.0.18363
gyp ERR! command "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js" "rebuild"
gyp ERR! cwd C:\research\idle-poweroff\node_modules\winapi
gyp ERR! node -v v12.13.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok 

NodeでLINE Botを扱うには

$
0
0

この記事はNode.jsアドベントカレンダーの13日目の記事です。

どうも!ハムカツおじさんという名前でtwitterやってます(@hmktsu)🤘
自分でだったり弊社でだったりなどNode.js(TypeScript)を使ってサービスを作っています。

はじめに

現在とあるデータを毎回取得して、その中に特定の文字列ならびに新規データが存在しているならば、LINE Botを使って自分に通知をするというツールを作って運用をしています。

とある商品群のデータがリアルタイムに欲しいので作りました。
Slackを使って通知するという形でも問題はないのですが、エンジニアだけがそのサービスを使いたいわけではない、ましてや普通の人でもという考えがあるので、LINE Botを使うことにしました。

ということで今回はLINE Botを使って通知を自分に送ってみようという内容を書きたいと思います。
なおLINE Botを使うための登録云々については割愛させていただきます🙇‍♂️

LINE Botを無料で使いたい

基本的に開発および少人数であるならば無料でいけると思います。
1000通がフリープランの目処になります。

プラン・料金
LINE Official Account Manager

両方で書かれてることが若干違います。(ターゲットリーチ数×吹き出し数1,000通までなど)
正直自分はどんなに頑張っても1000通は送らないし、吹き出し数計算でも1000通はいかないので今のところ無料で使えています。
1000通送るかどうか、これを1つの指標として考えるといいのではないかと思います。

インストール

では本題のLINE Botで通知を送ってみるために、とりあえずまずはインストールしましょう。

$ npm install --save @line/bot-sdk

アクセストークンの取得

次は送信するためにアクセストークンを取得する必要があります。

typeToken={access_token:string,expires_in:number,token_type:string,}typeTokenError={error:string,error_description:string,}constgetAccessToken=():Promise<Token|TokenError>=>request({url:'https://api.line.me/v2/oauth/accessToken',method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded',},form:{grant_type:'client_credentials',client_id:process.env.CHANNEL_ID,client_secret:process.env.CHANNEL_SECRET,},}).then(body=>JSON.parse(body)).catch(body=>JSON.parse(body.error));

このような関数を作ってあげると簡単です。
client_idclient_secretについてはprocess.envで管理すると楽です。

constsetAccessToken=(token:string=null):line.Client=>{constconfig:line.ClientConfig={channelAccessToken:token,channelSecret:process.env.CHANNEL_SECRET,};constclient=newline.Client(config)returnclient;}

送信する前にアクセストークンは取得する必要があるので、このようにアクセストークンをセットしたらline.Clientを返してくれるような関数も用意しましょう。

1人に送るか?まとめて送るか?

1人に送る場合はpushを、友達になっている人全員に送るにはbroadcastを使います。

constpush=async(to:string=null,messages:line.Message[],):Promise<void>=>{consttoken=awaitgetAccessToken();if('error'intoken){console.warn(`[line push] ${token.error}`);return;}if(!token.access_token){console.warn('[line push] token is null');return;}constclient=setAccessToken(token.access_token);awaitclient.pushMessage(to,messages);}constbroadcast=async(messages:line.Message[],):Promise<void>=>{consttoken=awaitgetAccessToken();if('error'intoken){console.warn(`[line push] ${token.error}`);return;}if(!token.access_token){console.warn('[line push] token is null');return;}constclient=setAccessToken(token.access_token);awaitclient.broadcast(messages);}

基本的に送り方はpushbroadcastも変わりません。

送ることができる内容

Messaging API overview

pushbroadcastも複数のメッセージ形式に対応しています。
プレーンなテキストもビデオもスタンプも色々と対応していますが、自分がよく使うのはTemplate MessageのCarouselImage carouselです。
目的に応じて使い分けるとよいと思います。

Carousel

{"type":"template","altText":"this is a carousel template","template":{"type":"carousel","columns":[{"thumbnailImageUrl":"https://example.com/bot/images/item1.jpg","imageBackgroundColor":"#FFFFFF","title":"this is menu","text":"description","defaultAction":{"type":"uri","label":"View detail","uri":"http://example.com/page/123"},"actions":[{"type":"postback","label":"Buy","data":"action=buy&itemid=111"},{"type":"postback","label":"Add to cart","data":"action=add&itemid=111"},{"type":"uri","label":"View detail","uri":"http://example.com/page/111"}]},{"thumbnailImageUrl":"https://example.com/bot/images/item2.jpg","imageBackgroundColor":"#000000","title":"this is menu","text":"description","defaultAction":{"type":"uri","label":"View detail","uri":"http://example.com/page/222"},"actions":[{"type":"postback","label":"Buy","data":"action=buy&itemid=222"},{"type":"postback","label":"Add to cart","data":"action=add&itemid=222"},{"type":"uri","label":"View detail","uri":"http://example.com/page/222"}]}],"imageAspectRatio":"rectangle","imageSize":"cover"}}

Image Carousel

{"type":"template","altText":"this is a image carousel template","template":{"type":"image_carousel","columns":[{"imageUrl":"https://example.com/bot/images/item1.jpg","action":{"type":"postback","label":"Buy","data":"action=buy&itemid=111"}},{"imageUrl":"https://example.com/bot/images/item2.jpg","action":{"type":"message","label":"Yes","text":"yes"}},{"imageUrl":"https://example.com/bot/images/item3.jpg","action":{"type":"uri","label":"View detail","uri":"http://example.com/page/222"}}]}}

まとめ

簡単にですが、LINE Bot + Node.js(TypeScript)でLINE Botからメッセージを送るにはということを説明させていただきました。

何よりも自分が身近に使っているアプリに対して通知を送ることができるのがとても便利なのでおすすめです。
開発用途で使う分にはほぼ無料でいけるはずなので、何か簡単なものを作るのに最適だと思います

フルスタックの視点から並列非同期処理を俯瞰してみた(JavaScript(Node.js), Kotlin, Go, Java)

$
0
0

はじめに

この投稿はCyberAgent Developers Advent Calendar 2019 14日目の記事ですー

私はフルスタックエンジニアですが、フルスタックの視点から俯瞰してみた時に、プログラミングがどういう見え方をしてるか?というのを伝えられたらと思っています。

いくつか言語を学んでる方は、異なる言語を学ぶ時に、あの言語だとこうできたのに、、、こっちの言語だとこれ便利だな。とか感じたことがあるかと思います。異なる言語でのテクニック・手法・書き方などを並べて解説している記事があまりないと思ったので、あえて異なる言語での同じ手法の例を、1つの記事として見比べてみようと思います。

今回は同時に非同期処理を実行したい場合の例を参考にお伝えできればと思いますが、この記事をでは「並列・並行処理の違い」や、「マルチスレッドとイベントループの違い」など、小難しいことは省きます。
複数言語を記載してるので長文になりますが、読んでもらえたら幸いです。

今回は昔からの定番や、最近私もよく使う下記の言語での例を書いています。

  • JavaScript(Node.jsでの例です)
  • Kotlin(Androidでの例です)
  • Go
  • Java

非同期で同時に処理をなげる

近年マイクロサービスが流行っているかと思いますが、マイクロサービスになると、フロント側に近くづくほど、「あっちのデータはこのAPIから」「こっちのデータはこのAPIから。。。」と、たたくAPI多すぎて、それの結果をまとめて、画面用に加工して。。。ってやるケースは結構増えてると思います。BFF(Backend for frontend)のAPIでやってたりする場合もありますが、クライアント側(JSやスマホアプリ側)でやるケースも少なからずあると思います。
どのケースでもそうですが、叩くAPIが多いほどレスポンス速度や画面の表示速度に影響するので、順番に1つずつAPIを実行するのではなく、APIを同時に処理させた後、結果を加工したいっていうケースも増えてきてると思います。

例えば、3秒かかるAPI、2秒かかるAPI、1秒かかるAPIと3つのAPIを実行する必要があり、それを画面用に加工する処理が1秒あって、結果をくっつけたら画面に表示できる。というケースの場合。
前提としてクラスの呼出などのオーバーヘッドは除く

下記の図のように順番にAPIを実行すると、画面を表示するまでに7秒かかりますが、

順番にAPIを実行した場合、合計7秒かかる図

同時にAPIを実行すると、画面を表示するまでに4秒に短縮できます。

同時にAPIを実行した場合、合計4秒かかる図

このように同時に投げる手法は必要になるケースは増えてきてると思いますが、言語によって結構書き方が違うと思います。
基本は上の図のようなフローは変わらないので、

  • 考え方
  • フローを思い描く
  • 基本的なプログラムの動き(プロセスやスレッドなど)

を身につけてしまえば、言語が変わってもちょっと調べれば、大体想像がつくようになります。もちろん言語によって特性が違うので、そこは使う時に深堀しないと思わぬ不具合に繋がる可能性もありますが、実際に利用してみないとよくわからないとおもうので、ここでは深く書きません。

それでは、実際に同時に処理を投げるプログラムを異なる言語で見比べてみます。

JavaScript(Node.js)の非同期処理

JavaScript(Node.js)では、Promiseを使います。下記ソースみてもらうと、PromiseってPromise.allしかない?と思いますが、メソッドにasyncをつけるとreturnが全てPromiseになります。その、promiseを同時に実行してね、に近いことをしているのが、Promise.allです。
Promise.allでawaitすると、allの配列に指定した関数の結果が全て返却されるまで、Promise.allの行で待機してくれます。test1とtest2は、ほぼ同時に実行されるので、順番に実行するよりも早くレスポンスを返すことが可能です。
なお、JavaScriptですが、Node.jsを前提としています。ブラウザ上のJSだと、chromeとか最近のブラウザだとPromise使えますが、IE11とか古いブラウザ(対応打ち切ってるところ増えてきてますが)だとPromise使えないものもあるので、webpackとかでpolyfillを入れて、古いブラウザでも動くように一度変換が必要です。

JavaScript(Node.js)のソース

constmain=async()=>{console.log('[JavaScript] main start')// 処理を非同期で複数投げ、Promise.allで全ての結果が帰ってくるまで待つconstresult=awaitPromise.all([test1(),test2(),])console.log(result[0])console.log(result[1])console.log('[JavaScript] main end')}consttest1=async()=>{console.log("test1 method")awaitsleep(2000)// APIとかを呼び出していると仮定return123}consttest2=async()=>{console.log("test2 method")awaitsleep(1000)// APIとかを呼び出していると仮定return456}// JavaScriptは便利なThread.sleep的なものがないので近いものを再現しますfunctionsleep(ms){returnnewPromise(resolve=>setTimeout(resolve,ms));}// main関数を実行main()

実行結果

$ npm run main

[JavaScript] main start
test1 method
test2 method
123
456
[JavaScript] main end

補足 Promiseとasync/awaitの関係

Promiseは非同期処理を実行するためのものですが、Node.js V8以前(ベータ版の時期除く)はasync/awaitがなく、非同期処理をする場合、new Promise()を使い、非同期の結果をコールバックチェーンさせてくという、コールバック地獄でした。コールバック地獄はソースの可読性が落ちるので、async/awaitが出来き、可読性が上がり、Promiseとあまり書かなくてよくなりました。TypeScriptを使ってreturnの値を見るとasyncのついた関数の戻り値は、かならずPromiseがつくことがわかるとおもいます。

// TypeScriptconsttestFunc=():Promise<number>=>{return123}

Promise.allは同時実行で使うので残ってますが、基本はPromiseはあんまり使わなくて大丈夫ですが、ちょっと古いライブラリは結果をcallbackで返してくるものがまだあります。そんな時にPromiseで一度包んであげると、async/awaitにできるで、補足として記載します。

サンプルソース

/**
 * callback関数になっているライブラリをasync/awaitにしたい場合の例
 */constmain=async()=>{console.log('[JavaScript] main start')constresult=awaittestFunc()console.log(result)console.log('[JavaScript] main end')}// promiseでcallbackをasync/awaitで実行できるように、callbackで帰ってくるAPIを包んでますconsttestFunc=()=>{returnnewPromise(resolve=>{console.log("testFunc method")callbackMethod(ret=>{resolve(ret)})})}// callbackになっているライブラリの変わりconstcallbackMethod=callback=>{callback(456)}main()

Kotlinの非同期処理

Kotlinは Coroutineを使います。Coroutineは新しいkotlinのversionだと使えますが、ちょっと古いと使えなかったりします。その場合はJava時代からあるThreadとかを使うことになってしまうので、Coroutineが使える新しいKotlinを前提に説明します。
Coroutineは、単純にいうとJavaのThreadを使いやすくした物です。Coroutineを実行した際にThread番号をdebugで見ると異なるThreadで実行されてることがわかると思います。
基本的には、async/awaitの考えかたはJSと一緒で、Coroutineを実行しているメソッドはsuspendをつけ、予備だし側にawaitするか?しないか?判断を委ねたり、runBlockingなどを使って、呼び出したところで同期処理に戻してる感じにします。この例だと、test1とtest2を同時に実行したかったので、doAllのなかで、二つの関数をasyncで呼出し、awaitで2つの結果が終わるのを待ってから、呼出もとに返してます。JSみたいなPromise.allはないので、結果をarrayに筒んであげれば、Promise.all風のメソッドが再現できます。

Kotlinのソース

packagesample.kotlinimportkotlinx.coroutines.*importkotlin.coroutines.resumeimportkotlin.coroutines.suspendCoroutinefunmain(){println("[kotlin] main start")// doAllはsuspendなので、runBlockingをして中の処理がすべて完了するまで待つrunBlocking{valresult=doAll()println(result[0])println(result[1])}println("[kotlin] main end")}/** * JavaScriptPromise.all処理結果と似た様な実装 * JavaでいうとExecutorService Callable */suspendfundoAll():Array<Int>=coroutineScope{valret1=async{test1()}valret2=async{test2()}// Promise.all風// メソッドの実行結果をarrayにいれて返してみると、promise.allっぽくなるarrayOf(ret1.await(),ret2.await())}suspendfuntest1():Int{println("test1 method")delay(2000)// APIとかを呼び出していると仮定return123}suspendfuntest2():Int{println("test2 method")delay(1000)// APIとかを呼び出していると仮定return456}

実行結果

Android StudioでMain.ktを右クリックしてRunを実行。

(kotlincとか入れてコマンドラインからできるようにしようと思ったんですが、coroutineつかってるし設定に時間がなく、時間短縮のためAndroid Studioからやりましたm(_ _)m)

[kotlin] main start
test1 method
test2 method
123
456
[kotlin] main end

補足 Coroutineでもcallback系のAPIをasync/awaitに変換してみる

CoroutineはJavaのThreadよりに楽になったとは言え、機能が多くどれ使ったらいいの?ってなると思います。
今回は1例として、suspendCoroutineを使うと、JSの new Promise()つかってasync/awaitしたのと同じように、コールバックをやめられる例を記載します。

cont.resume(it)
これが、jsの
resolve(ret)
と同じ感じで、resume実行するとreturnされます。

こうすると、この行のように、callbackがasyncにできます。

valfunc=async{testFunc()}valresult=func.await()

違う言語で、書き方違ってもこうやってやりたいことは出来たりします。

サンプルソース

packagesample.kotlinimportkotlinx.coroutines.*importkotlin.coroutines.resumeimportkotlin.coroutines.suspendCoroutinefunmain(){println("[kotlin] main start")// JavaScriptでいうと、awaitっぽい感じ。なの通りでrunBlocking内の処理が終わるまで待ってくれるrunBlocking{valfunc=async{testFunc()}valresult=func.await()println(result)}println("[kotlin] main end")}/** * JavaScriptでいうところのnew Promise()と同じ様な効果。 * suspendCoroutineでcallbackをasync/awaitで実行できるように、callbackで帰ってくるAPIを包んでます */suspendfuntestFunc():Int{println("testFunc method")returnsuspendCoroutine{cont->callbackMethod{cont.resume(it)}}}// callbackになっているライブラリの変わりfuncallbackMethod(ret:(Int)->Unit){ret(456)}

Goの非同期処理

goはgoroutineとchanを使います。goroutineは、goというキーワードで関数を実行すると非同期になります。goだけだと結果をうけとれないので、その非同期の結果を受け取るために chan (channel)を使います。chanは awaitと同じような効果があり <-で awaitしてくれます。それ以外は今までと同じようなフローで動きます。ただし、中身の動きはgoは並行処理というの採用しています。ここではchannelについてはそこまで詳しく書きませんが、goの場合waitGroupというのもあります。詳しく知りたい方は 並列処理と並行処理などで検索してみてください。

今回chanにしたの、今まで紹介してきたJSのPromise.allなどのように、1つ目、2つ目と結果を確実に格納したかったからです。ここでポイントなのが、channelを2つmakeし、第二引数のバッファを1だけ確保している点です。
下記のように1つのchannelで、バッファを2にして、

channel:=make(chanint,2)goTest1(chanel)goTest2(chanel)resultArray=append(resultArray,<-chanel)resultArray=append(resultArray,<-chanel)

のように、書くこともできるのですが、確実に最初の配列にTest1の結果が返ってくる保証がないからです。実際実行すると、処理のタイミングによって配列0にTest2の結果が入ったりします。channelを分けることで確実にTest1の結果はchannel1に入れることができるので、今回のサンプルでは分けました。それを前提に以下のソースをみてもらうと良いかと思います。

Goのソース

packagemainimport("fmt""time")funcmain(){fmt.Println("[go] main start")result:=doAll()fmt.Println(result[0])fmt.Println(result[1])fmt.Println("[go] main end")}funcdoAll()[]int{resultArray:=make([]int,0,2)chanel1:=make(chanint,1)chanel2:=make(chanint,1)// goroutineで非同期処理goTest1(chanel1)goTest2(chanel2)// Promise.all風実行結果を配列に詰め込んでいます。// <- は、jsやkotlinでいうawaitキーワードみたいなものです。Javaだとfuture.get()// 全ての実行結果が買ってくるまで、awaitしてる感じです。resultArray=append(resultArray,<-chanel1)resultArray=append(resultArray,<-chanel2)close(chanel1)close(chanel2)returnresultArray}funcTest1(cchanint){fmt.Println("test1 method")time.Sleep(time.Second*2)// APIとかを呼び出していると仮定c<-123}funcTest2(cchanint){fmt.Println("test2 method")time.Sleep(time.Second*1)// APIとかを呼び出していると仮定c<-456}

実行結果

$ go run src/main.go

[go] main start
test2 method
test1 method
123
456
[go] main end

goroutineは実行順序を保証してくれないので、先にtest2が実行されていますが、awaitして待ってるので、呼び出し側では気にする必要はないです。

Javaの非同期処理

Javaは、古くからある言語のため、非同期処理のやり方がいくつかありますが、今回はJava7くらいから追加された、Executorを使います。いろいろなやり方はありますが、基本的にやってることは同じでJavaのスレッド処理です。今回は非同期の実行結果を受け取りたかったので、Callableクラスを使って実装しています。submitした時に非同期処理が実行され、
future.get()としてる箇所がawaitで、実行結果を待ちます。その結果をListに入れることで、Promise.allのような実行結果を再現しています。Javaの場合Classを作ったりFutureを使ったりと、非同期をするための手続きが他に比べて多いです。Executorを使ったのは、KotlinはJVMで動くので結果中身はJavaなんですが、この記事を執筆している時点の、AndroidのCameraXライブラリを使う場合、Executorのインスタンスを渡す必要があったので、Executorを使ってサンプルを作ろうと思いました。この辺りです。
Google CodelabsGetting Started with CameraX

Javaは昔からある言語で手数は多いですが、KotlinになってもJavaのライブラリは良く呼びますし、そもそもThreadとは?っていうこをを抑えるのには、まだまだ役立つと思っております。

Javaのソース

packagesample;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.*;classJavaApplication{publicstaticvoidmain(String[]args){System.out.println("[Java] main start");List<Integer>result=doAll();System.out.println(result.get(0));System.out.println(result.get(1));System.out.println("[Java] main end");}/**
     * JavaScriptPromise.all処理結果と似た様な実装
     * KotlinのcoroutineScopeでasyncした結果と似た様な実装
     */privatestaticList<Integer>doAll(){List<Future<Integer>>futures=newArrayList<>();List<Integer>result=newArrayList<>();ExecutorServiceexecutor=Executors.newFixedThreadPool(2);try{// この2行がjsやkotlinでいうasyncキーワードとかPromiseと大体動きは同じ思ってくださいfutures.add(executor.submit(newText1Class()));futures.add(executor.submit(newText2Class()));// Promise.all風// Callableを実装したクラスの実行結果をArrayListにいれて返してみると、promise.allっぽくなるfor(Future<Integer>future:futures){result.add(future.get());// future.get() が jsやkotlinでいうawaitキーワードみたいなものです}}catch(Exceptione){e.printStackTrace();}finally{executor.shutdownNow();// Javaは明示的にスレッド止めないと、プロセスが落ちてくれません。}// このループで全ての実行結果が買ってくるまで、awaitしてる感じです。returnresult;}}classText1ClassimplementsCallable<Integer>{@OverridepublicIntegercall()throwsInterruptedException{System.out.println("test1 method");Thread.sleep(2000);// APIとかを呼び出していると仮定return123;}}classText2ClassimplementsCallable<Integer>{@OverridepublicIntegercall()throwsInterruptedException{System.out.println("test2 method");// APIとかを呼び出していると仮定Thread.sleep(1000);return456;}}

実行結果

$ cd src
$ javac sample/Main.java
$ java sample.JavaApplication

[Java] main start
test1 method
test2 method
123
456
[Java] main end

まとめ

今回は4つの言語を使って、それぞれの同時に非同期処理を実行する一例をあげさせていただきました。
JavaScriptの実行に近い形で他の言語のコードも合わせましたが、基本的な考え方はスレッドでの非同期実行や、waitでスレッドの実行完了を待つsignalを受け取る考え方が基本です。JavaScriptは、シングルスレッドのイベントループでスレッドではなく他の言語はスレッドになりますが、 async/awaitの考え方 や 処理フローは言語が異なっても応用できるので、こう言った 考え方を身につけておくことが技術力の向上に繋がると思っています。
今回の非同期はあくまで一例です。言語を覚えると言うより、ある言語を使いアーキテクチャ、手法、ライフサイクル、フローなどの基礎を理解することが重要です。

  • 非同期処理であれば、プロセスや、スレッドおよびスレッドセーフなプログラム
  • ライフサイクルであれば、それぞれのフレームワークのライフサイクルの特徴や動き
  • その言語の得意なこと、苦手なこと
  • ネットワークプロトコルの理解
  • 画面へのデータの流れ方、etc...

上にあげたのも一例ですが、プログラムを通して、そういった基本的なことを理解することが重要です。

最後に

フルスタックになるにはどうしたらいいですか?と時々聞かれますが、真面目に 基礎が大事と言えます。
ただ1点広く深く(応用)覚えようとすると、もちろんそれぞれの言語やアーキテクチャの細かい違いなどがあり、それなりに時間をつかいます。一昔前はエンジニアは細かく領域がわかれていませんでしたが、近年フロント、ネイティブ、バックエンド、インフラエンジニアなどと分かれているのは、それぞれの分野がより高度な知識や経験が必要になり、専属でないとなかなかキャッチアップが大変になってきたからだと思います。

そんな中でも、私は広く深くフルスタックで行こうと思います。もちろん深くいくので、人の2倍3倍は働いていますし勉強もしています。その努力した分は裏切らないし、全分野で深く知っているからこそ提案できる、全体アーキテクチャであったり、ライブラリであったりできることは格段に広がります。そういう開発ができて私はすごく楽しいと思っています。

プログラムを始めたばかりの方は、とりあえず1つの言語で、上にあげたようなプログラムの基礎をしっかりと理解し、自由に使いこなせるようになってから次の言語を学ぶと、おのずと理解しやすくなると思います。また、コンピュータやOSなど、コンピュータサイエンス的なところも後から、「知っててよかった」っていうところは出てくるので、勉強のしかたがわからない方は、基本情報処理技術者試験や応用処理技術者試験などの参考書などをみてもいいかもです。資格は持ってますが、資格自体はあまり役にたった記憶はありません^^;ただ、その過程で勉強した基本的な仕組みは今でも役立っています。

プログラム言語やフレームワークなどは流行りなどもあるので、今まで覚えた物が使えなくなると思うかたもいるかもしれません。ただ、言語やフレームワークは変わっても、基礎は変わらないので、新しいものが登場した時でも、今まで学んできた 考え方はどこかで必ず役立ちます。

私の例でいうと、本当つい最近ですが、AndroidのKotlinでChannel(JavaでいうBlockingQueue)やReentrantLock、TCP/UDP通信などを作って低レイヤーからスレッドセーフな独自ライブラリを作ったんですが、10年前くらいに参画したプロジェクトでスレッドセーフなプログラムや通信周りのプログラムを使ったフレームワークの開発に携わっていたことがあるんです。当時学んだ考え方や設計力が今役立ったなぁと思った今日この頃でした。

どの時代でも技術は流行り廃りはありますが、

努力して学んできた考え方は裏切りません

最後にこの一言で終わりにさせていただきます。

今回作ったサンプルプロジェクトはこちらです。
https://github.com/tanaka-yui/parallel-async-sample

長文にお付き合いいただきありがとうございました。


axiosのヘッダーのconfigでちょっとハマった

$
0
0

某APIを試しててaxiosのconfig指定をミスってたので、自戒の意味を込めて残しておきます。

完全に自分用メモっぽいやつです。

ミスったコードなど

//省略classHoge{constructor(){//省略}//ミスった方methodA(IMAGE_PATH){constfile=fs.createReadStream(IMAGE_PATH);constform=newFormData();form.append('image',file);form.append('entrance','detection');constconfig={'X-ClovaOCR-Service-ID':this.SERVICE_ID,...form.getHeaders()};returnaxios.post(this.URL.RECOGNITION,form,config)}//うまくいった方methodB(IMAGE_PATH){constfile=fs.createReadStream(IMAGE_PATH);constform=newFormData();form.append('image',file);form.append('entrance','detection');constconfig={headers:{'X-ClovaOCR-Service-ID':this.SERVICE_ID,...form.getHeaders(),}}returnaxios.post(this.URL.RECOGNITION,form,config)}}consthoge=newHoge();hoge.methodA();// エラーhoge.methodB();// 成功

axiosのconfigミス

慣れてつかってるうちにconfig=headersっぽいイメージで使ってたみたい。
通常configの中には他にもmethod指定やbodyデータなども入ってくるのでconfig.headersな指定にしないとですね。

constconfig={'X-ClovaOCR-Service-ID':this.SERVICE_ID,...form.getHeaders()};
constconfig={headers:{'X-ClovaOCR-Service-ID':this.SERVICE_ID,...form.getHeaders(),}}

VSCodeのREST ClientでCognitoを利用するためのツールを作ってみた

$
0
0

はじめに

皆さんはVisual Studio Codeの拡張機能の一つのREST Clientは利用していますか?
VSCode上でREST APIを呼び出すツールとして人気があると思います。
しかし、このツールではOAuth2.0がサポートされおらず、Cognitoで認証し、APIをコールすることが現時点ではできません。(2019年12月14日時点)
また、似たツールとしてPostmanが有名ですが、あちらはCognitoで認証してAPIをコールすることができますが、私は次の点でPostmanを利用せずにREST Clientを利用しています。

  • テキストベースでAPIを定義することができるので、Gitで管理することができる。

Gitで管理することで、APIも変更があった場合もそのバージョンでコールするための情報が残っているので、APIのバージョンごとの呼び出しデータを管理する必要がなくなるのは大きかったです。

実装方法

ローカルにCognitoで認証を行いアクセストークンを取得してきてくれる認証サーバをローカルに立ててるだけです。

インストール方法

必要としているディレクトリでnpm i -D rest-client-cognitoでインストールすることができます。
Nodeの開発環境でななければ、グローバルインストールしていただければ利用できると思います。(未検証)

使い方

プロジェクトディレクトリに.envファイルを作成してください。
次の4つの項目を追加してください。

.env
RCC_USER_POOL_ID={CognitoのユーザープールID}
RCC_CLIENT_ID={アプリクライアントID}
RCC_USER_NAME={ログインするユーザー名}
RCC_PASSWORD={ログインするユーザーのパスワード}

次にコンソールでrccコマンドを実行することで、Cognitoの認証サーバをローカルに立ち上げることができます。
rccで立ち上がる認証サーバはデフォルトで3000番ポートで起動するようになっています。
localhost:3000/loginをGETすることでアクセストークンを取得することができます。
(普通ログインといったらPOSTですが、深夜テンションで作成したのですっかりそんなことも忘れて作ってしまいました。。。指摘しないでもらえると助かります。。。)

このAPIで返却されるデータの構造は次の形になります。

.json
{"token":"アクセストーン"}

なので、これをREST Clientで利用する場合は次のように記述することで利用できるようになります。

RestClient.rest
@rccPort = 3000

###

# @name login

GET http://localhost:{{rccPort}}/login

@authToken = {{login.response.body.token}}

あとはauthTokenを色々なAPIで利用してもらえれば、Cognitoからもらった認証情報を用いてAPIをコールできるようになります。

オプション

デフォルトで3000番ポートで起動しますが、だいたいの方は3000番ポートは使っていると思います。
なので、-p--portオプションを利用することで、ポート番号を変更することができるようになっています、
例:rcc -p 4000rcc --port 4000

最後に

簡単な動作確認程度のことしかしていないので、何かありましたらGitの方でコメントお願いします。

linux版 nodeとnpmをインストール

$
0
0

まず確認

Nodejsローカルでもjavascriptを使えるようにしたもの
npmパッケージ管理
n Nodejsのバージョン管理

aptからインストール

インストール先: /usr/bin/
sudo apt install nodejs npm

npmでnをインストール

インストール先: /usr/local/bin/
sudo npm instal -g n

n

nでインストールされるNodeの場所は以下になります。
/usr/local/bin/

インストール可能なバージョンを調べる

最新の20件取得
n lsr

すべて取得
n lsr --all

nodeインストール&削除&バージョン切り替え

インストール
sudo n バージョン

インストール済みNodejs一覧
n ls

バージョン切り替え
sudo n バージョン

削除
n rm バージョン

ヘルプ

npm help npm
n help

npm

npm本体

バージョン一覧
npm info npm versions

アップデート
sudo npm update -g npm

インストール&ダウングレード
sudo npm install -g npm@バージョン

パッケージ(モジュール)管理

package.jsonをつくる
npm init

ローカル
プロジェクト/node_modulesにインストール
npm install パッケージ名

グローバル
/usr/local/bin/npm/node_modulesにインストール
sudo npm install -g パッケージ名

開発環境
・インストールはローカルと同じ
・gitなどで管理する時は必須
・本番環境と開発環境を区別したい場合
・package.jsonの[devDependencies]に追記される。
sudo npm install --save-dev パッケージ名

package.json通りにすべてインストール
npm install

パッケージのインストール場所確認
ローカル
npm root
グローバル
npm root -g

Prettier

最近はやっているフォーマッター 
ローカルインストール
npm install prettier -D

※しかし、これだけではまだ使えない。そこで次のやり方をします。

とても参考になったパスの通し方

ローカル環境にあるツールをバージョン毎に使い分けるnpmにもってこいのテクニックだと思います。
プロジェクトのあるパスのモジュールを参照する必殺技でもなんでもないなんの変哲のない相対参照(./)を使うやり方です。

export PATH=$PATH:./node_modules/.bin

とても気に入ったので、
これ ./ を
今日から執拗に使っていこうかなと思います。

初心者が Googleアシスタント と Vue.js を使ってスマートディスプレイアプリを作ってみた

$
0
0

はじめに

いちあき(@ichiaki_kazu)と言います。初めてのQiita記事です。

僕はこれまでアセンブラやHTMLくらいしか触ってこなかった(ほぼ)ノンプロフリーランスです。
それをフリーランスというのか置いといて…

11月に行った「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」というハンズオンが楽しかったので、勉強も兼ねてハンズオン内容を参考にしつつ自分で作ってみました。

この記事の目的

  • 自身のやったことを整理して定着させる
  • 共に音声アプリの概要を掴んでもらえたらな

もしかしたらこの通りやっても動かないかもしれないので鵜呑みにしないでください。
※知識不足により誤っている部分がある可能性があります。その時は優しく指摘してください…

参考資料

・「Google Nest Hub対応スマートディスプレイスキルを作ろう!【Vue.js】」(2020/1に大阪でも開催)
・Qiita「滑舌チェックスキルをGoogle Nest Hubで実装してみた」

完成物

音声でアプリを呼び出して、出てきたくだものの画像を見て名前を当てるゲームです。
幼児になら使ってもらえるかなって…

今回使ったもの

  • Google Nest Hub:実機動作確認のため
  • Dialogflow:自然言語処理
  • Vue.js(VueCLI):アプリの画面生成
  • Azure Functions(CLI):内部の処理(クイズ部分とか入出力とか)
  • Vuetify:Vueで使えるフレームワーク
  • IntaractiveCanvas:(重要)Googleアシスタントアプリで画面描写に必要なライブラリ

実際に作ってみる

作るに当たってポイント部分だけを解説していきます。
実際に作ってみたい人は「滑舌チェックスキルをGoogle Nest Hubで実装してみた」をまず確認すると良いと思います。

1.Dialogflow(言語処理をする)

Googleアシスタントによって入力された音声の処理を行います。
Intent(インテント)を作成することで、言葉に反応して何かを返します。

1-1.インテント作成

20191213_056_dialogflow.cloud.google.com.png
作成するIntentは4つ

Intent名内容 
Default Welcome Intentアプリ起動の言葉
EndIntentアプリ終了の言葉
MainIntentアプリ起動中の言葉
StartIntentアプリ起動後に開始する言葉

1-2.実際にインテントを作る(例:StartIntent)

アプリ起動後にゲームを開始するための言葉を登録します。
20191213_061_dialogflow.cloud.google.com.png

  • Training phrases:反応する言葉を登録
  • Fulfillment:webhookでやりとりするためチェックを入れる

その他のインテントも同じように作ります。
ちなみに「Response」には反応時返すメッセージを登録できます。
詳しくは参考記事をご覧ください。

2.Vue.js(アプリ画面を作る)

アプリの画面描写部分を作っていきます。
ざっくりとやることは以下の通り

  • プロジェクト作成(vuetifyも入れる・今回はrouterも)
  • index.htmlにIntaractivCanvasのAPI追加
  • App.vueの修正
  • Home.vueの修正
  • HelloWorld.vueの修正

2-1.プロジェクト作成

プロジェクトを作ってvuetifyを入れます。

vue create kudamonoquiz-app
vue add vuetify

今回はプロジェクト作成時にrouterも入れておきました。

2-2.index.htmlにIntaractiveCanvasのAPI追加

画面描写のキモである「InteractiveCanvas」のAPIを引っ張ってきます。

public/index.html(12行目あたり)
<scripttype="text/javascript"src="https://www.gstatic.com/assistant/interactivecanvas/api/interactive_canvas.min.js"></script>

2-3.App.vueの修正

ここではヘッダー部分とrouterへの連携をしています。
routerは初期状態でHome.vueに流れます。

src/App.vue
<template><v-app><v-app-barapp><v-toolbar-titleclass="headline text-uppercase"><span>くだものくいず</span></v-toolbar-title></v-app-bar><v-content><router-view/></v-content></v-app></template>

2-4.Home.vueの修正

コンポーネント「HelloWorld.Vue」を表示するようにします。

src/Home.vue
<template><HelloWorld/></template><script>importHelloWorldfrom'../components/HelloWorld.vue'exportdefault{name:'home',components:{HelloWorld}}</script>

2-5.HelloWorld.vueを修正(ポイント)

app側のメインコンテンツです。

src/Home.vue
<template><v-container><!-- スタートページ --><v-layouttext-centerwrapv-show="target === 'top'"><v-flexxs12md-10><h3class="display-3 font-weight-bold mb-10"></h3></v-flex><v-flexxs12><h1class="display-2 font-weight-bold mb-10">くだものえいご
        </h1></v-flex><v-flexxs12mb-4><v-btncolor="success"@click="start">スタート</v-btn></v-flex></v-layout><!-- 問題表示ページ --><v-layouttext-centerwrapv-show="target === 'kudamono'"><v-flexxs12md-10><h3class="display-3 font-weight-bold mb-10"></h3></v-flex><v-flexxs12mb-4><h1class="display-1 font-weight-bold mb-3">【くだものえいご】
          </h1></v-flex><v-flexxs12mb-4><imgclass="img":src="imgurl"alt="くだもの画像"></v-flex><v-flexxs12mb-4><h5class="display-1 font-weight-bold mb-3">このくだものはなーんだ?
          </h5></v-flex></v-layout><!-- 正解後の表示ページ --><v-layouttext-centercolumnalign-centerv-show="target === 'congratulation'"><v-flexxs12mb-10><h3class="display-3 font-weight-bold"></h3></v-flex><v-flexxs12mb-4><imgclass="img"alt="congratulation"src="../assets/congratulation.png"></v-flex><v-flexxs12mb-4><imgclass="img":src="imgurl"alt="くだもの画像"></v-flex><v-flexxs12mb-4><h2>だいせいかい!<br>これは{{kotae}}{{tango}})だよ!</h2></v-flex><v-flexxs12mb-10><v-btnlargecolor="success"@click="start">スタート</v-btn></v-flex></v-layout></v-container></template><stylescoped>.img{width:300px;}.endimg{width:200px;}</style><script>exportdefault{data(){return{target:'top'}},created(){varme=thisconstcallbacks={onUpdate(data){if('kudamono'indata){me.kotae=data.kudamono.kotae,me.tango=data.kudamono.tango,me.imgurl=data.kudamono.imgurl,me.target=data.kudamono.target}},}interactiveCanvas.ready(callbacks)},methods:{start(){interactiveCanvas.sendTextQuery('スタート');}}};</script>

ざっくり解説するとこのファイルでは3つがポイントだと思います。

  • targetの状態で表示するコンテンツ(開始時・ゲーム時・正解時)を分ける
  • functionから送られてくるdata(画像URLや問題)をセットし、使用する
  • 最初にスタートボタンを押した時 interactiveCanvas.sendTextQuery('スタート');でDialogflowに「スタート」という文字列を送る

特に文字列を送る昨日はinteractiveCanvasならではなので、ポイントかと思います。

2-6.package.json確認

後々動かす際にnpmインストールするのでpackage.jsonの確認をします。

{
  "name": "kudamonoquiz-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "core-js": "^3.4.3",
    "vue": "^2.6.10",
    "vue-router": "^3.1.3",
    "vuetify": "^2.1.0",
    "vuex": "^3.1.2"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.1.0",
    "@vue/cli-plugin-router": "^4.1.0",
    "@vue/cli-plugin-vuex": "^4.1.0",
    "@vue/cli-service": "^4.1.0",
    "sass": "^1.19.0",
    "sass-loader": "^8.0.0",
    "vue-cli-plugin-vuetify": "^2.0.2",
    "vue-template-compiler": "^2.6.10",
    "vuetify-loader": "^1.3.0"
  }
}

おそらくこの状態だと思いますが、念のための確認です。

3.Azure Functions(内部のプログラムを作る)

3-1.CLIツールをインストールする

とりあえずCLIツールをインストールします

npm install -g azure-functions-core-tools 

3-2.プロジェクトを作成する

このあたりは完全にこの記事と同じです。

$ mkdir kudamonoquiz-app-functions
$ cd kudamonoquiz-app-functions
$ func init                             # 選択肢が出てくるのでnodeとjavascriptを選ぶ
$ func new                              # Http triggerを選択し、kudamonoquiz-appという名前で作成する
$ npm init -y
$ npm i -s actions-on-google@2.10.0     # 2.10.0を入れる
$ npm i -s azure-function-express       # azure-function-expressを入れる
$ npm i -s express
$ npm i -s firebase-admin

3-3.functions.jsonを編集

完全にこの記事と同(ry
GETのみに変更して、どこからでもアクセスできるようanonymousにします。

kudamonoquiz-app/functions.json
{"bindings":[{"authLevel":"anonymous",//anonymousにしておく"type":"httpTrigger","direction":"in","name":"req","methods":["post"//getは使わないので消しておく]},{"type":"http","direction":"out","name":"res"}]}

3-4. index.jsを編集

ワードが入力された際にインテントに応じて処理を変えます。

kudamonoquiz-app/index.js
constcreateHandler=require("azure-function-express").createHandler;constexpress=require("express");const{dialogflow,HtmlResponse}=require('actions-on-google');constapp=dialogflow({debug:false});//スタート処理app.intent('StartIntent',async(conv)=>{//問題の生成constkudamonoquiz=[{"imgurl":"/img/banana.jpg","kotae":"バナナ","tango":"banana"},{"imgurl":"/img/cherry.jpg","kotae":"チェリー","tango":"cherry"},{"imgurl":"/img/grape.jpg","kotae":"グレープ","tango":"grape"},{"imgurl":"/img/melon.jpg","kotae":"メロン","tango":"melon"},{"imgurl":"/img/orange.jpg","kotae":"オレンジ","tango":"orange"},{"imgurl":"/img/peach.jpg","kotae":"ピーチ","tango":"peach"},{"imgurl":"/img/remon.jpg","kotae":"レモン","tango":"remon"},{"imgurl":"/img/strawberry.jpg","kotae":"ストロベリー","tango":"strawberry"}];constwordIndex=Math.floor(Math.random()*kudamonoquiz.length);constselKudamono=kudamonoquiz[wordIndex];conv.contexts.set('game',5,selKudamono);conv.ask('このくだものを英語で言ってみてね');selKudamono["target"]="kudamono";conv.ask(newHtmlResponse({data:{kudamono:selKudamono}}));});//ゲーム処理app.intent('MainIntent',async(conv,{any})=>{constcontext=conv.contexts.get('game');if(context.parameters.kotae===any){conv.contexts.delete('game');conv.ask(`大正解。答えは ${context.parameters.kotae}でした! もう一度クイズをするなら「する」終了するなら「終了」と言ってください。`);context.parameters["target"]="congratulation"}else{conv.ask('よくわかりませんでした。もういちど言ってみてね。');}conv.ask(newHtmlResponse({data:{kudamono:context.parameters}}));});//起動時app.intent('Default Welcome Intent',(conv)=>{conv.ask('果物英語をはじめるには、スタートボタンを押してください。');conv.ask(newHtmlResponse({url:'https://{表示させたいホームページのURL}',supperss:true}));});//functionの名前を一致させておくconstexpressApp=express();expressApp.post('/api/kudamonoquiz-app',app);module.exports=createHandler(expressApp);

実際に動かしてみる

プログラムを動かしていきます。

Vue.jsの起動

npmインストール

kudamonoquiz-appでnpmインストールします。

cd ./kudamonoquiz-app
npm install

プログラム実行

npm run serve

これでhttp://localhost:8080にアクセスできます。
トップ画面が表示されます。
僕はこのあとngrokを使いましたがとりあえずこれでも動くと思います。

Azurefunctionsの起動

npmインストール

kudamonoquiz-functionでnpmインストールします。

cd ./kudamonoquiz-function
npm install

index.js内にアプリのURLを記述する

kudamonoquiz-function/kudamonoquiz-app/index.js(抜粋)
//起動時app.intent('Default Welcome Intent',(conv)=>{conv.ask('果物英語をはじめるには、スタートボタンを押してください。');conv.ask(newHtmlResponse({url:'http://localhost:8080',supperss:true}));});

ローカルサーバを起動する

func host start

http://localhost:8080/でアプリが起動します。

Dialogfrowの設定

FulfillmentのwebhookURLを指定する

20191214_062_dialogflow_cloud_google_com.png

テストする

IntegrationからGoogle Assistantを選択
Dialogflow.png

出てきたポップアップで「Auto-preview changes」にチェック入れて「TEST」
Dialogflow2.png

Developから名前(アプリ呼び出しの呼び名)
Develop.png

Deployで「category」を「Games&fun」にする。※InteractiveCanvasに必須
Deploy2.png

InteractiveCanvasをYesにする。
Deploy3.png

Testから実際に動作を確認する。
Androidスマホがある方はそちらからでも確認できます。
Test.png

今回はテストまでなのでデブロイはなしで、ここまでとなります。
ここまで実施すれば実機でテストバージョンとして動作確認もできます。

以上です。
もしかしたら手順漏れなどあるかもしれませんが大体こんな流れです。
興味ある方は是非是非試してください〜

僕も答えられるかわかりませんが、不明点がありましたらお願いします!

宣伝みたいな

【大阪】Google_Nest_Hub対応スマートディスプレイスキルを作ろう!【Vue_js】_-_connpass.png
https://atlabo.connpass.com/event/157824/
2020年の1/23(木)に僕がスマートディスプレイにハマったきっかけのハンズオンが大阪でも開催されます。
興味ある方はおすすめですのでぜひ!※僕もスタッフとして行きます

Firestoreの対話型シェルをいきなり起動する

$
0
0

Firestoreでちょっとしたクエリの実験するときにNode.jsのREPL(Read-Eval-Print-Loop)使って対話型シェルを利用している人は多いと思います。その時、毎回nodeコマンドを実行したあとに.load スクリプト名などとして接続をしている人はまさかいませんね? 人によってやり方は様々だと思いますが私のスクリプトを以下に貼っておきます。

db
#!/bin/bash                                                                                                                                                                     
node--requireawait-outside/repl-i-e"                                                                                                                                                                    
  const firebase = require('firebase-admin');
  var serviceAccount = require('./.service-account.json');
  firebase.initializeApp({
      credential: firebase.credential.cert(serviceAccount)
  });
  var db = firebase.firestore();
"

このスクリプトを開発環境のFirebaseプロジェクトのトップにでも設置しておけば./dbのコマンドでFirestoreへの対話型シェルが出現します。以下のような感じで実行できます。便利でしょ?

$ ./db
> await db.collection('tests').doc('hoge').set({name: 'oreore'});
WriteResult {
  _writeTime: Timestamp { _seconds: 1576307204, _nanoseconds: 63907000 }}>(await db.collection('tests').doc('hoge').get()).data(){ name: 'oreore'}

設置方法

設置方法を簡単に開設しておきます。やるべきことはパッケージのインストールとFirebaseサービスアカウントのキーの配置です。

パッケージのインストール

ここで使うのはこの2つ

npm i firebase-admin --save
npm i await-outside --save

サクッとインストール。

サービスアカウントのキーの配置

FirebaseコンソールにログインしてサイドメニューのSettings>ユーザーと権限からたどって新しい秘密鍵の生成をクリックします。

image.png

ここから得られるjsonを.service-account.jsonという名前で、冒頭のdbスクリプトと同じディレクトリに配置します。このファイルは機密情報を含んでいるので忘れずにgitignoreしときましょう。

あ、dbスクリプトに実行権限を与えることもお忘れなく。

以上

Special Thanks

yarn link実行後、cliでコマンドを入力してもPermission deniedになる場合

$
0
0

実行環境

  • node: v12.13.1
  • yarn: 1.19.2

自前で作成したcliツールをローカルで試したい場合、yarn linkを使用しますが、Permission deniedで実行できないことがあるようです。

$ yarn link$ パッケージ名
bash: /usr/local/bin/パッケージ名: Permission denied

実行権限を付与する必要があるとのこと。

$ chmod +x /usr/local/bin/パッケージ名

で実行可能になる。

CloudFuntionsで「File ./dist/index.js that is expected to define function doesn't exist」と言われた(TypeScript)

$
0
0

Google Cloud FunctionsとTypeScriptを使ってとあるSlackボットを作っていたら、正しく依存関係を指定しているはずなのに「File ./dist/index.js that is expected to define function doesn't exist」というエラーが出てうまくデプロイできず、結構ハマったので原因と解決策を。


先に結論。

GCloud Functions: load error: File ./dist/index.js that is expected to define function doesn't exist - Stack Overflow

ここに書いてあることがすべて

構成

├── .gitignore
├── README.md
├── dist
│   └── index.ts
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

src配下にTypeScriptのファイルが配置されていて、それをコンパイルするとdist配下にJSが吐かれるというよくあるかんじの構成です。

package.jsonのmainにはdist/index.jsが指定されています。

また、.gitignoreにはdistが指定されています。これもTypeScriptのプロジェクトではよくあることかなと思います。

デプロイしようとした

完成したのでデプロイしようとして以下のようなコマンドを実行しました。

tscでコンパイルを行い、2行目のgcloudを使ってCloud Functionsへデプロイを実行しました。

$ npx tsc
$ gcloud functions deploy hoge --runtime nodejs8 --trigger-http--project hoge-proj

すると、「File ./dist/index.js that is expected to define function doesn't exist」というエラーが発生しました。

間違いなくdistの下にindex.tsファイルはあるのにおかしいと思い、いろいろ調べたり悩んだ結果たどり着いたのが上記に貼ったStackOverflowでした。

原因

gcloud functions deployを実行すると.gcloudignoreというファイルが自動的に作成されます。

こんなファイルです

# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
#   $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
node_modules
#!include:.gitignore

.gitやnode_modulesなど、Cloud Functions側にあげる必要のないファイルを.gitignoreと同じような形式で書いておくことで、無視してくれるようになります。

今回原因となったのは一番最後の行、#!include:.gitignoreこれです。

これは.gitignoreの内容を取り込んでくれるものになります(多分)。今回私は.gitignoreにdistを指定していたため、gcloud function deployを実行しても肝心のdist以下がアップロードされずにエラーが出た、というのが原因でした。

解決策

最後の行に!distと書き足すとよいです。これだけでdist以下を読んでくれるようになります。ついでにsrcはあげなくていいのでsrcと書き足しておくといいかもしれません。

まとめ

普通にJS書いてた時はうまくいってたので、油断してたらこんな罠が…と言う感じでした。

FirebaseのFunctionsはfirebase-toolsで初期化するときにTypeScriptを使う選択をすると、自動的にこの辺のファイルとかのテンプレを吐き出してくれるので、困ったことなかったのですが、今回使ったのはGCPの方のFunctionsだったのでハマってしまいました。

調べ方がわるかったのか、なかなか上記StackOverFlowにも辿りつかず、結構苦労しました。

この記事が誰かの役に立つといいなあ。

おわり。


npmパッケージを公開するならnpが便利!

$
0
0

よくある困りごと

npmパッケージを公開・更新する場合、手順を忘れがちですよね(少なくとも私はそうです)。
これから紹介するnpコマンドを使えば、楽にnpmパッケージの公開、更新、さらにはGitHubのtagまで切ってくれる。最高です。

npコマンドの導入

yarn global add np

npを実行すると、バージョンの選択画面が表示され、major, minor, patchのアップデートかどうかの選択肢が出る。

patchを選択した場合
$ np
Publish a new version of パッケージ名 (current: 1.0.3)

Commits:
- コミット内容

Commit Range:
v1.0.3...master

? Select semver increment or specify new version patch  1.0.4

  ✔ Prerequisite check
  ✔ Git
  ↓ Cleanup [skipped]
  ✔ Installing dependencies using Yarn
  ✔ Running tests using Yarn
  ✔ Bumping version using Yarn
  ✔ Publishing package using Yarn
  ↓ Pushing tags [skipped]
    → Upstream branch not found; not pushing.
  ✔ Creating release draft on GitHub

 パッケージ名 1.0.4 published 🎉

参照

https://zellwk.com/blog/publish-to-npm/

Express + Vue.jsでhelloを出してみよう

$
0
0

おはようございますこんにちはこんばんは!

最近Front-endのフレームワークが色々出て流行ってるらしくて、
友達となんかやってみよー!になってはじめました。
最初、どのフレームワークを使おうかと、
'react / vue が人気らしいー何が違う?'とざっと検索してみたら

  • Vue
    • テンプレート形式でアプリの制作したいなら
    • 簡単で「一旦動作」ができるのが好きなら
    • 早くて軽量のアプリが作りたいなら
  • React
    • 大きい規模のアプリを作るなら
    • もっと大きい情報が欲しいなら

こんな差がありました。
簡単なプロジェクトだから、VueにしようーでVueを選びました。
(実は韓国で何もわからずVueの本を買ってきたので、、最初からvueにしようと決めたこともあり、、笑)

back-endはnode.jsのexpress フレームワークを使います(なんでもjsでやってみよう感)

やってみましょう^0^

  1. node.js install
    https://nodejs.org/
    installします。ltsの方が安定的らしくてltsの方インストールしました。

  2. コマンドラインでインストる確認
    $ node -v
    バージョンが出力されたらok

  3. package.jsonでパッケージ管理
    package.json生成
    $npm init
    express 設置 node_modulesというフォルダが生成されます
    $npm install --save express

4.htmlファイルを置いておくpublic directoryをnode_modulesと一緒の段階に生成
生成
$mkdir public
入る
$cd public
index.htmlページ生成
$vi index.html

index.html
<!DOCTYPEhtml><html><head><title>Vue.jsSample</title>
  </head><body><divid="app"><h1>{{message}}</h1>
        <input v-model='message'>
    </div><!--vue.js読み込み--><scriptsrc="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

    <script>
      new Vue({
        el: '#app',
        data: {
          message: 'Hello Vue.js!'
        }
      })
    </script></body>
</html>

v-modelを使ってinputとtextareaのエレメントに両方向のデーターバインディングの生成が出来ます。
javascriptのjqueryだと inputのvalueを持って、、それをh1に適用して、、みたいなことを
nue Vue ~~ だけで出来ます。

5.node.jsを実行するindex.jsを生成(node_modulesとpublicと一緒の段階)
$cd ../
$vi index.js

index.js
constexpress=require('express');constapp=express();constPORT=process.env.PORT=8000;//port8000に指定、変えてもokapp.use(express.static('public'));//app.listen(PORT,()=>{console.log('Server is running at:',PORT);});

6.index.jsのところで実行
$node index.js

console
Serverisrunningat: 8000

表示されたら localhost:8000に接続すると

스크린샷 2019-12-14 오후 9.52.52.png

こういう画面が出て、下のinputに内容を変更するとHello Vue.js!の大文字が変更されます。
とても簡単!

vue.jsの解説なんかより、一応、、画面表示してみよう!になっちゃったんですが、(汗)
これで終わり!

参考

https://jp.vuejs.org/index.html
自分は韓国人なので、https://kr.vuejs.org/index.htmlこちら参照しました (笑)
https://joshua1988.github.io/web_dev/vue-or-react/韓国語です (汗)

ObnizとAWS LambdaとLINEを使って家のエアコンをスマート化した話

$
0
0

きっかけ

僕の家は最寄駅から徒歩20分くらいありまして、その道を毎日歩いて通勤しています。春先や秋などは良いのですが、真夏日だと蒸し暑い中で家と会社を往復しています。
会社に行く時は電車に乗れさえすればエアコンが効いてて快適です!ただ帰りの場合は、家についても中は暑く、時には外よりも蒸し暑い時もあります。
どうせなら駅に着いた瞬間エアコンをつけて、駅から家まで歩いている間に涼しくなってくれればいいのに・・・

作ったもの

LINEから「冷房」などとメッセージを送信すると、家のエアコンの冷房スイッチをONしてくれるbotを作成しました。

out.gif

LINEなので当然、ローカルネットワークではなくインターネット回線越しです。なので家の外でも会社でもどこでも、家のエアコンをつけたり消したりできます!
これ作ったのほんとは夏頃なのですが、冬用に暖房ON機能も追加することで一年中活躍してくれそうです。

LINEのトーク画面はこんな感じ。
カルヴィ.PNG
カルヴィ3.PNG
カルヴィ2.PNG

システム構成・技術解説

構成図.png

システム構成はざっくりこんな感じです。
それぞれ詳細を書いていきます。

LINE Developersでチャネル作成

LINEをBot化するにあたりLINE Developersにてチャネルを作成します。
LINEDEV1.png

せっかくbotにするので、嫁の好きなソシャゲのキャラにアイコンを設定。
(チャネル作成の詳しい手順については割愛します。)
「Messaging API設定」というタブの「Webhook URL」に、エンドポイントとなるAPIを設定する必要があります。今回は、API Gateway & AWS Lambdaで作成します。
また、チャネルアクセストークンとチャネルシークレット(こっちはチャネル基本設定タブの中にあります)も後々必要になるので控えておきます。

エアコン(リモコン)の赤外線パターンを取得

秋月などで販売されてる、赤外線受信モジュールを使用します。
http://akizukidenshi.com/catalog/g/gI-00622/
これをObnizに直接、こんな感じで接続します。
infrared_rec.JPG

あとは赤外線モジュールに向けてリモコン操作することで、その操作の赤外線点灯パターンを取得します。そのためのプログラムを書く必要があるのですが、Obniz公式が赤外線受信モジュール取り扱いのためのページを公開してくれており、そのページの中で、なんとプログラム実行ができてしまいます!
https://obniz.io/ja/sdk/parts/IRSensor/README.md
まずブラウザでObnizにログインしているかどうかをご確認ください。
ログインしているならば、「start(callback(array))」という欄に自身のobnizIDが表示されているはずです。そのボックス内のプログラムを、「TEST RUN」ボタン押下で実行することができます。
obniz_rec.png

「TEST RUN」実行後、エアコンのリモコンを赤外線受信モジュールに向けて操作することで、赤外線点灯パターンの配列が表示されます。冷房・暖房をつけるパターンと停止するパターン、それぞれの配列パターンを取得・控えておきましょう。

ブレッドボードに基盤作成

まず下記のものを購入しましょう。

購入部品は全体的に下記記事を参考にさせていただきました。
https://qiita.com/KAKY/items/55e6c54fa2073cdc0bbe
上記部品たちを下記のように接続!
基板.png
IMG_1623.JPG

見えづらいかもですごめんなさい。
そこまで複雑な配線でもないですが、LEDの足が長い方(アノード:+)はObnizの0番(上記写真でいう赤いジャンパワイヤ)、足が短い方(カソード:-)はObnizの1番(上記写真でいう黒いジャンパワイヤ)に接続するように気をつけてください。
また当たり前ですが、その間に抵抗を挟んでおいてくださいね。(僕は開発中に一度抵抗を挟むのを失念し、LEDを一つ爆発させました。)
LEDキャップは、指向性のあるLEDの光を拡散させる役割で、これをつけておくとわざわざLEDをエアコンに向ける必要がなくなります。これも忘れずにつけておきましょう。

Lambdaへのプログラムアップロード

上記のやり方でゲットした「冷房」「暖房」「停止」それぞれの赤外線パターン配列を控えて、Lambdaに記載するプログラムを書いていきます。
Lambda上でObniz APIをキックするために、またLINE Messaging APIを使用するために、それぞれNode.js用のSDKが必要になりますので、まずローカルPCにインストールしましょう。(僕はMacユーザーなので開発端末はMac想定でいきます。)
※ObnizのLambda連携は、下記公式のドキュメントもありますので、参考にしてみてください。
https://obniz.io/ja/lessons/server_side/lessons_lambda

$cd /path/to/the/work/dir/
$ npm install obniz
$ npm install @line/bot-sdk

そして同じ階層にindex.jsファイルを用意します。

index.js
'use strict';constline=require('@line/bot-sdk');constcrypto=require('crypto');constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN});constObniz=require("obniz");exports.handler=function(event,context){varobniz=newObniz("your-obnizID");letsignature=crypto.createHmac('sha256',process.env.CHANNELSECRET).update(event.body).digest('base64');letcheckHeader=(event.headers||{})['X-Line-Signature'];letbody=JSON.parse(event.body);if(signature===checkHeader){if(body.events[0].replyToken==='00000000000000000000000000000000'){//接続確認エラー回避letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"connect check"}'};context.succeed(lambdaResponse);}else{obniz.onconnect=asyncfunction(){console.log('obniz');lettext=body.events[0].message.text;letres_text='うーん、私難しいことはわからないな〜';if (text.match(/冷房/)){res_text='わかった!冷房をつけておいてあげるわね。ひんやりひんやり〜';}elseif (text.match(/暖房/)){res_text='わかった!暖房をつけておいてあげるわね。ぽっかぽかよ!';}elseif (text.match(/停止/)){res_text='エアコン消しとくわね。節約節約!';}else{res_text='うーん、私難しいことはわからないな〜';}varled=obniz.wired("InfraredLED",{anode:0,cathode:1});obniz.display.clear();obniz.display.print("Connected!");console.log(res_text);if (text.match(/冷房/)){console.log('reibo');led.send([1,1,1,1,1,//...中略...0,0,0,0,0]);obniz.display.clear();obniz.display.print("Reibo ON");}elseif (text.match(/暖房/)){console.log('danbo');led.send([1,1,1,1,1,//...中略...0,0,0,0,0]);obniz.display.clear();obniz.display.print("Danbo ON");}elseif (text.match(/停止/)){console.log('stop');led.send([1,1,1,1,1,//...中略...0,0,0,0,0]);obniz.display.clear();obniz.display.print("OFF");}else{//}constmessage={'type':'text','text':res_text};console.log(message);client.replyMessage(body.events[0].replyToken,message).then((response)=>{letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"completed"}'};context.succeed(lambdaResponse);}).catch((err)=>console.log(err));awaitobniz.wait(5000);obniz.close();}}}else{console.log('署名認証エラー');}};

text.matchの部分は、obnizとの通信部分とMessaging APIとの通信部分のif文をまとめて書くと、なぜかObnizの動作が失敗するので、上記のように分けて書くことで動作させてます。もっといい書き方はある気がします・・・(JSわからない)
また、Lambdaの環境変数の中に、先ほど控えたチャネルアクセストークンとチャネルシークレットを記載するのを忘れずに。
lambda_env.png

Lambdaには本来コンソールから直接プログラムを記載できるのですが、ObnizやLINEのSDKごとアップロードしなければいけないので、zipに固めてアップロードします。
やり方はObniz公式のドキュメントに書いてあります。(さっきのと同じ)
https://obniz.io/ja/lessons/server_side/lessons_lambda
ただ、おそらくzip後のファイルが10MBを越えるため、一度S3バケットにアップロードしリンクURLを指定するやり方をとる必要があるかもしれません。

Webhook設定

作成したLambdaとAPI Gatewayを紐付け、エンドポイントを作成します。
細かい説明は省きますが、メソッドはPOST、認証はNONEにしましょう。(認証挟んだ方がセキュアだと思いますが手抜きしてしまいました。誰かLINE-SDKとの連携方法のやり方教えてください・・・)

作成できたAPIエンドポイントを、LINE DevelopersのWebhook URLに設定して、完成です!

まとめ

これで夏も冬も快適エアコンライフを過ごすことができそうです。おかげさまで嫁にも好評でよかったよかった!
今回は要件満たすためのミニマム実装だったので、赤外線パターンを直接ハードコーディングしたりしてますが、これもLINE上から登録できるようにして、DynamoDBあたりに保存するようにできるといいかもですね。
またせっかくbotにしたので、雑談的な会話も対応させてみたかったのですが・・・docomoの雑談対話APIが終了してしまったので断念。
無料で使える雑談APIどこかにないでしょうかね・・・

macへnode.jsのインストール

$
0
0

macにnode.jsをインストールする時に、3記事くらい参考させていただいてやっとできたので、自分ができたやり方をメモしておく。

まず、インストールの手順としては以下になる。

1.homebrewのインストール
2.nodebrewのインストール(一瞬詰まった)
3.Node.jsのインストール(ここで結構詰まった)

自分は、特に手順3の環境パスを通すところで詰まった。

1.homebrewのインストール

まずは、すでにインストールされているか確認。

$ brew -v
-bash: /usr/local/bin/brew: No such file or directory

上記のメッセージが表示されている場合は、インストールされていない。

インストール

https://brew.sh/index_ja.htmlに書いてあるスクリプトを実行する

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2019/12/11現在は上記

homebrewがインストールできているか確認

$  brew -v
Homebrew 2.2.1
Homebrew/homebrew-core (git revision 9ee2; last commit 2019-12-10)

2.nodebrewのインストール

まずはインストールされているか確認

nodebrew -v
-bash: /usr/local/bin/nodebrew: No such file or directory

上記のメッセージが表示されている場合は、インストールされていない。

インストール

homebrew から nodebrew をインストールする

$ brew install nodebrew

nodebrewがインストールできているか確認

$ nodebrew -v
nodebrew 1.0.1

上記のようにバージョンが表示されていれば、インストールに成功している。

3.Node.jsのインストール

インストールできるバージョンの確認

$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6    

v0.1.0    v0.1.1    v0.1.2    v0.1.3    v0.1.4    v0.1.5    v0.1.6    v0.1.7
v0.1.8    v0.1.9    v0.1.10   v0.1.11   v0.1.12   v0.1.13   v0.1.14   v0.1.15
v0.1.16   v0.1.17   v0.1.18   v0.1.19   v0.1.20   v0.1.21   v0.1.22   v0.1.23
v0.1.24   v0.1.25   v0.1.26   v0.1.27   v0.1.28   v0.1.29   v0.1.30   v0.1.31
v0.1.32   v0.1.33   v0.1.90   v0.1.91   v0.1.92   v0.1.93   v0.1.94   v0.1.95
v0.1.96   v0.1.97   v0.1.98   v0.1.99   v0.1.100  v0.1.101  v0.1.102  v0.1.103
v0.1.104  

v0.2.0    v0.2.1    v0.2.2    v0.2.3    v0.2.4    v0.2.5    v0.2.6    

v0.3.0    v0.3.1    v0.3.2    v0.3.3    v0.3.4    v0.3.5    v0.3.6    v0.3.7
v0.3.8    
(略・・・)

上記コマンドで以下のようなエラーが発生する場合もあるそうです。(自分はならなかった)

取得:https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz
警告:ファイルの作成に失敗しました 
警告:/Users/whoami/.nodebrew/src/v7.10.0/node-v7.10.0-darwin-x64.ta
警告:r.gz:そのようなファイルまたはディレクトリはありません

curl:(23)本文の書き込みに失敗しました(0!= 941)
ダウンロードに失敗しました:https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz

その場合は以下のコードでディレクトリを作成すると良い。

$ mkdir -p〜/ .nodebrew / src

インストール

バージョンを指定してインストールするためには、以下のようにする。

$ nodebrew install-binary v10.16.3

最新版をインストールしたい場合は以下を実行する。

$ nodebrew install-binary latest

インストールされた Node.js のバージョンを確認する

$ nodebrew list
v10.16.3

current: none

使用する Node.js のバージョンを指定する

$ nodebrew use v10.16.3
use v10.16.3

Node.js のパス設定

Node.js のバージョンを確認する。

$ node -v
-bash: node: command not found

※ 上記のようなメッセージが表示されていると、Node.js へのパスが通っていません

以下を実行する。

echo export PATH=$HOME/.nodebrew/current/bin:$PATH >> ~/.bash_profile

node から Node.js のバージョンを確認する(パスの疎通を確認する)

$ node -v
v10.16.3

※ 上記のように、バージョン情報が表示されれば Node.js にパスが通っています。

以上です!

大変お世話になったサイト
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09
あと2つ、なんて調べたか忘れてしまったので、また記載しておきます。

Firebase Admin SDKで一般的なWebサービスの構成にFirebase Authenticationを使った認証処理を組み込む。

$
0
0

概要

Firebase #2 Advent Calendar 2019の14日目の記事です。

Firebase Authenticationを使用した記事は数多くあるのですが、少なくはないユースケースである、Webサービスでのサーバーサイドでの認証も含めたFirebase Authenticationの利用に関する記事が見当たらなかったので、サンプル実装を書いてみました。

今回のFirebase Authenticationの想定ユースケース

  • 自前でのユーザー管理は行いたい
  • 認証部分のみFirebase Authenticationを導入し、Googleアカウントとの紐付けを行いたい。+認証処理の実装の省略したい。

システム構成図と認証フロー

今回作成するFirebase Authenticationを組み込んだWebアプリのシステム構成を図にしました。

認証-Page-2.jpg

以下のフローでFirebase AuthenticationによるセキュアなAPI実装を実現します。

  • ① なんらかの認証トリガー(ログインボタンをクリックなど)から特定の認証プロバイダ(今回はGoogle)の認証画面へリダイレクトする。
  • ②③ Firebase Authenticationが認証処理を行い、IDトークンが払い出される。
  • ④ 何らかの認証が必要な処理を行うHTTPリクエストを送る。
    • その際、Authorizationヘッダーに取得したIDトークンを付与してリクエストする。
  • ⑤ Firebase Admin SDKでIDトークンの検証を行う
  • ⑥ するとユーザーIDやEmailアドレスなどが取得できるので、それを使って自前のUser Tableと突合し、正規のユーザーか判断・ユーザー情報の取得等ができるようになる。

サンプルアプリの仕様

今回作成するサンプルアプリの仕様です。

  • ログインはGoogleの認証画面へのリダイレクトによって行う。
  • ユーザーがその画面に来た時、そのユーザーがログイン済みであればそのユーザーの情報を画面に表示する。
    • その情報は他の一般ユーザーが閲覧することはできない。
  • ユーザーがログイン済みでなければ、ログイン画面(/login)に飛ばす。

サンプルアプリでの採用言語+ライブラリ等

  • クライアントサイド : TypeScript + Axios
  • サーバーサイド : Node.js + TypeScript + Express

クライアントサイドでFirebase Authenticationによるログイン処理とAPIへのリクエストを行う

まずはクライアントサイドの実装からしていきましょう。

事前準備

Firebaseのプロジェクトを作成

先の方々に説明を譲ります。

SDKの取得

npmでFirebase SDKを取得します。

npm i firebase

設定JSONの取得

Firebaseの設定をJSONで取得し、アプリケーションとFirebaseの紐付けを行います。
公式の記事を参考にしましょう。
https://firebase.google.com/docs/web/setup?hl=ja

Firebase Hostingを使ってHTMLのホスティングを行う場合はこの設定JSONは必要ありませんが、ローカルでの確認する際など、あると便利なので一応取得することをオススメします。

実装

Firebase SDKを初期化

firebase.initializeAppに先ほど取得した設定JSONを渡します。

importfirebasefrom"firebase/app";import"firebase/auth";constfirebaseConfig={/* 先ほど取得した設定JSONオブジェクト */}firebase.initializeApp(firebaseConfig);

ログイン処理を実装

リダイレクトによるログインを実装したい場合は以下
プロバイダをGoogleに指定する例です。

constlogin=()=>{constprovider=newfirebase.auth.GoogleAuthProvider();firebase.auth().signInWithRedirect(provider);};

ログイン自体の処理はこれで終わりです。あっけないですね。
これだけで実際にGoogleの認証画面へリダイレクトして、戻ってくる動きが確認できます。

認証情報の取得

ログインした。という情報およびIDトークンを受け取ります。
認証情報はJSのロードから少し時間をおいて取得されるので、認証情報を使用する場合はそれが取得されたことをSubcribeする必要があります。
SubcribeにはonAuthStateChangedメソッドを使用します。

// firebase.auth().getRedirectResultはあまり使い勝手が良くなかった。firebase.auth().onAuthStateChanged(asynccurrentUser=>{if(currentUser){// ここでログインユーザーの情報が参照できる。const{email,uid,displayName}=currentUser;console.log(email,displayName,uid);// IDトークンを取得する。constidToken=awaitcurrentUser.getIdToken();console.log(idToken);}else{/* 
        未ログイン時にはcurrentUserがnullで渡ってくるので
        nullチェックでfalseな分岐に未ログイン時の処理を記述する。
      */window.location.href="/login";}});

getRedirectResultメソッドでもリダイレクト後の認証情報の取得は可能なのですが、それ以外のケース(すでにログイン済みのユーザーがページに訪れた時)などでの使い勝手が良くなかったで採用しませんでした。

認証情報を使ってAPIにアクセスする。

サーバーサイドでのアクセスの検証に用いるため、IDトークンをHTTPヘッダーにつけてリクエストを飛ばします。

firebase.auth().onAuthStateChanged(asynccurrentUser=>{if(currentUser){constidToken=awaitcurrentUser.getIdToken();// 何らかの認証が必要なリクエストをIDトークン付きで飛ばすconstres=awaitaxios.get(BACKEND_SERVICE_BASE_URL+"/secret/userinfo",{headers:{Authorization:idToken}});console.log(res.data.user.secretData);}else{window.location.href="/login";}});

クライアントサイドは以上です。

Firebase Admin SDKを使ってサーバーサイドでIDトークンを検証する。

さて、次はこのアクセスをサーバーサイドで検証するコードを実装してみましょう。

事前準備

SDKの取得

npmでFirebase Admin SDKを取得します。

npm i firebase-admin

秘密鍵の生成

  1. Firebaseのコンソールから「プロジェクトの設定」 -> 「サービスアカウント」 を選択
  2. ページ下部にある「新しい秘密鍵の生成」を押下します。
  3. JSONがダウンロードできるのでそれを任意の場所に保存します。

きちんとした手順は公式を参照しましょう。(丸投げ)
https://firebase.google.com/docs/admin/setup?hl=ja

実装

秘密鍵の適用とSDKの初期化

環境変数FIREBASE_CONFIG先ほど保存したJSONのファイルパスGOOGLE_CLOUD_PROJECTにプロジェクト名を指定し、サーバーを起動させます。

FIREBASE_CONFIG='path/to/secret.json'GOOGLE_CLOUD_PROJECT='projectname' node server.js #server.jsはコンパイル後のjsファイル

SDKを初期化させる際はSDKが上記で設定した環境変数を勝手に見に行くので引数を渡す必要はありません。

importAdminfrom"firebase-admin";constadmin=Admin.initializeApp();

APIでのIDトークンの検証

ExpressでAPIの処理を実装します。
verifyIdTokenメソッドを使い、クライアントから送られてきたAuthorizationヘッダーのIDトークンを検証しuidを取得します。uidはユーザーごとに一意の値になるので、これをキーに別テーブルにてユーザ管理を行えば、Firebase Authenticationと自前のユーザー管理を紐づけることが可能です。

constapp=express();// 諸々のミドルウェアの適用は省略app.get("/secret/userinfo",async(req,res)=>{constidToken=req.header("Authorization");if(idToken){const{uid}=awaitadmin.auth().verifyIdToken(idToken);// uid を使って紐付けられたユーザー情報を取得する処理を行ったりする。constsomeUseInfo=userService.getInfo(uid);res.json(someUseInfo);}// Authorizationヘッダーが無ければ403res.status(403).send();});

以上です。

まとめ

簡単とは行かないまでも、小難しい認証処理をFirebaseに移譲できたのが良かったです。
今回は時間がなく準備できなかったのですが、後々サンプルコードを公開する予定ですので良くわからなかった方は参照ください。

Viewing all 8900 articles
Browse latest View live