nodejs expressでhttps通信を開発環境でできるようにするう
参考資料:
- 【とっても簡単】開発中のNode.js ExpressでのSSLのhttps設定 | アールエフェクト
- localhostで開発中にGoogle Chromeで「この接続はプライバシーが保護されません」が出たときの対処法【NET::ERR_CERT_INVALID】 - Qiita
djangoの開発環境でhttps通信させるのが面倒そうだったのでnodejsでhttps通信できる環境を調べたところ案外かんたんにできそうなのでこちらを試してみた。 これにはcognitoを使う場合https通信をしないと認証できないことを知った背景がある。。。
手順
1.開発準備
// projectディレクトリを作成する mkdir nodejs_https_sample cd nodejs_https_sample // package.json等作成 npm init // expressをインストール npm install express
2. http通信対応するスクリプトをとりあえず作る
// ファイル名:index.js const express = require('express') const app = express() const port = 3000 app.get('/', function(req, res){ res.send('Hello World!') } ) app.listen(port, () => console.log(`Listening on port ${port}!`))
この状態だとhttp://localhost:3000/にアクセスすればHello Worldが表示されるが、 https://localhost:3000/にアクセスすると表示できない。これをできるようにするには以下の手順を実行する。
3. httpsに対応させる
1.証明書を作成する
// プロジェクトルート以下にcertディレクトリを作成する mkdir cert cd cert
certディレクトリ以下に privatekey.pemとcert.pemを作成する。
openssl req -x509 -newkey rsa:2048 -keyout privatekey.pem -out cert.pem -nodes -days 365
このコマンドについてはこちらを参照するとよい。 https://reffect.co.jp/node-js/node-js-https#i
2.index.jsを修正する
// index.js // 以下のように書き換える const express = require('express') const app = express() const port = 3000 const fs = require('fs'); const option = { key: fs.readFileSync('./cert/privatekey.pem'), cert: fs.readFileSync('./cert/cert.pem'), } const server = require('https').createServer(option, app) app.get('/', (req, res) => res.send('Hello World!')) server.listen(port, () => console.log(`Listening on port ${port}!`))
不動産業界の本を読んでまとめる
不動産業は取引業と賃貸業・管理業に分けられる。
不動産業に大きな影響を与える法律は、宅建業法、建築基準法、マンション管理適正化法、都市計画法。
2018年のデータでは名目GDP538兆円の11.4%に当たる61兆円を占める。
全産業の売上高1535兆円のうち46兆円が不動産がシェアしている。これは全体の3%。
不動産業は従業員一人あたりの付加価値額が高い。他の産業より高い。 付加価値額が高い理由は
- 不動産という商品の単価が高い
- 売上に対する原価が低い
- 企画によって商品価格をあげることができる
売上に対する原価が低いの補足:売買の場合は不動産を仕入れるので原価がかかるが、高価な設備や機材などは必要なく、製造業や建築業、サービス業と比べると原価がかからないと言える。
地価公示とは、国土交通省が毎年1回標準地として定める不動産の価格を調査して公的に発表すること。
宅地建物取引業は免許制度となっているが、賃貸業・管理業は免許制度ではないので参入ハードルが高くない。
nodejs をすこし使えるようにするためのメモ
今これを使うのは、cognitoで認証できた結果をdjangoにつなぐことができるか確認する事が必要。 記事通りやれば認証までは行けるからそこからdjangoにつなげさえすればかなりOKな感じになるんだと思う。
ハローワールドをやってみる
var http = require("http"); var server = http.createServer(function (req, res) { res.write("HELLO WORLD"); res.end(); }).listen(8080);
このファイルをsample1.jsとして以下を実行する
node sample1
そしてlocalhost:8080/ にアクセスするとちゃんとハローワールドできている。
expressを利用するケース
expressはフレームワークのことらしい。
Express は、Web アプリケーションとモバイル・アプリケーション向けの一連の堅固な機能を提供する最小限で柔軟な Node.js Web アプリケーション・フレームワークです。
Express - Node.js Web アプリケーション・フレームワーク
手順
mkdir express_test cd express_test npm init # package.jsonにexpress を登録 npm install express
以下のファイルをindex.jsと命名し作成する
var express = require('express'); var app = express(); app.listen(8080); app.get('/test1', function(req, res) { res.send('TEST1\n'); }); app.post('/test2', function(req, res) { res.send('TEST2\n'); });
以下のコマンドでサーバーを立てる
node index
開発時expressをホットリロードにする
nodemonを使えばホットリロードができる。
// nodemonをインストール npm install -g nodemon // ホットリロードで起動 nodemon index
// npm startでnodemonを起動させる // package.jsonのscriptsのところで以下のように書いておく "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon index" }, $ npm start
nodejsのでバッグ
デバッグの種類が多くてなにを使えばよいのか分からなかった。
とりあえずtyprにはnodeじゃなくてpwa-nodeとかpwa-chromeがあることが分かった。 pwa-chromeの方は最初にnodeを起動しておいてからじゃないとデバッグを起動しても動かないことだけは確認した。この辺なかなか難しい感じがする。。。pwa-chromeだとcognitoのテストが使えないし。
デバッグについて
デバッグの方法論
デバッグ方法のやりかたをまとめておくというより、統合開発環境のデバッグモードを使ってフェーズごとに効率よく作業する方法を整理したい。
フェーズ
- 開発時
- テスト
- プロダクトがエラーを吐き出したとき
- なんかよく分からんけどアプリが落ちるとき
開発時
コードを実行する ↓ エラーが出る ↓ 出たところを見る ↓ エラー原因を特定する ↓ 原因を排除する -> 再び'コードを実行する'へ
このフローだとエラーが出た際に正確にエラー原因を理解してエラーに対応したコードを書かなければ何度もコードを実行して時間がかかってしまう。 この場合には、コードを実行する際にデバッグモードで実行する。エラーが出る手前でbreakpointを設置し、エラーが出るスコープ内で、修正したコードをコンソールで実行する。こうすることで修正したコードは確実にエラーが出ないので修正したのにも関わらずエラーが出て時間をロスしてしまうことを回避する事ができる。
テストフェーズ
テストフェーズでは自動テストコードを走らせることが良いと思うが、それがない場合にはデバッグモードを実行しコードの品質を確認する。
- 意図した条件分岐を通るか
- 条件分岐先にある関数が適切に動くか
意図した条件分岐を通るか
ここをチェックしたい場合、以前まではデータベースのデータを調整してそのデータを流してその条件分岐を通るかどうか試していた。この場合データの構造がシンプルな場合にはDBのデータをいじれば済むが、それがコストがかかる場合がある。このケースでは、適切なデータを呼び出し、そのデータに条件分岐が通るべき修正を加え、コードを走らせる。そしてブレクポイントを適当に複数セットし、条件分岐が機能しているか確認すると良いだろう。
条件分岐先にある関数が適切に動くか
これに関しても単体テストを実行すればそれで済む話なのだけれども、職場環境によってはない場合がある。この場合には通らせたい条件分岐はTrueに変更、その他はFalseにすれば関数を実行することができる。関数実行時に適切なデータがほしければ、上で書いたようにコード中で修正すればその問題もないだろう。
プロダクトがエラーを吐き出したとき
web系に関しての対処法ではあるが、chromeのdevelopperツールのコンソールログを確認する。こちらでエラーが出ていなければサーバー側で問題があると考えられるのでツールのネットワークを開く。APIの結果で500が出ているものを見つけ、当該APIに対応するロジックの部分をデバッグモードで実行する。エラーメッセージもよく確認し、コンソールで通るコードを作成、修正でよい。
なんかよく分からんけどプロダクトのアプリが落ちるとき
具体的な対策が今は見つけられていない。IDEをつかうというよりはログを詳細にとりおかしいところを見つけることで対応している。
VSCODE pipenvの環境からデバッグを実行したいときには
.vscode/settings.jsonにpythonPathの項目を追加する。
{ "python.autoComplete.extraPaths": [ "/Users/chiaki/.local/share/virtualenvs/cognito_sample-4m7Vb3k3/lib/python3.9/site-packages/", ], "python.pythonPath": "/Users/chiaki/.local/share/virtualenvs/cognito_sample-4m7Vb3k3/bin/python" }
pythonPathの値は、仮想環境に入ったあとのpythonパスを参照すること。
手順;
# `仮想環境に入る` pipenv shell # which python
settings.jsonにpythonPathを追加すると仮想環境に入っていなくてもデバッグを起動すると仮想環境でデバッグが実行される。
javascriptファイルを実行する方法
javascriptで書かれたファイルを実行する方法を今更ながら知ったので記録しておく。
ブラウザで開いて実行する方法
今まで.jsファイルは該当の ファイルを選択してブラウザで開いてconsole.logとかを確認していた。
nodejsを使って実行する方法
それとは別にnode hoge.jsとすれば、javascriptのコードを走らせ、ターミナルにconsole.logの結果を表示させることができる。
この場合のnode は
python hoge.py
のpythonと同じ役割なのだろうと思う。
visual studio codeのドキュメントの中ではnode.jsをruntimeだと 定義している。 ランタイムとはある言語で書かれたファイルを実行するものに必要なものとのことだった。 ここにきてruntimeという概念を認知した。
Node.js is a platform for building fast and scalable server applications using JavaScript. Node.js is the runtime and npm is the Package Manager for Node.js modules.
Build Node.js Apps with Visual Studio Code
runtimeについての解説資料
mac ショートカット
目的
横になったり、リラックスした姿勢でPCを操作するためにはマウスを使わないほうが都合が良い。マウスをできるだけ排除した操作ができるような方法を調べメモしていく。
ショートカットキーカテゴリ
- macosによるOS系のもの
- ブラウザ(chrome)
- visual studio code
ウィンドウの切替ショートカット
Mac - ウィンドウの切替ショートカット - 覚えたら書く
command + tab
Chrome タブを切り替える
【Mac版Chrome】タブを切り替えるショートカットキー【隣のタブへ移動】 | Tipstour
Option + Command + 左右キー
CHROMEタブを閉じる
command + W
ショートカット|タブを閉じる・開く・タブの切り替え(Windows/Mac) | BIGLOBEハンジョー
CHROME閉じたタブを再度開く
command + Shift + T
CHROMEの戻る、進む
# 戻る command + [ # 進む command + ]
CHROME クリック可能な項目間の移動
順方向 tab 逆方向 tab + shift
visual studio code ターミナルを開く
ctrl + `
visual studio code breakpointの切り替え
F9
https://code.visualstudio.com/docs/nodejs/nodejs-tutorial#_debugging-hello-world
visual studio code デバッグで実行
command + shift + d
https://code.visualstudio.com/docs/editor/debugging#_run-view
visual studio code デバック中次のbreakpointまで実行
F5
DjangoをDockerのドキュメントを見て環境構築
Docker自体はqiitaの記事を見て環境構築したことがあるが、ドキュメントが存在し、それ通りにやったら案外かんたんにできてしまったのでメモ。 引っかかったところはsettings.pyのデータベースのセッティングだったのでそこを中心にメモしておく。
ドキュメント;
クィックスタート: Compose と Django — Docker-docs-ja 19.03 ドキュメント
これ通りやると、動かない。データベースの接続にはpassword等の値とsettings.pyの設定値を対応させるためにdocker-compose.ymlにenviriionmentを定める。
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'HOST': 'db', 'PORT': 5432, 'PASSWORD': 'postgres', } }
DATABASEの名前はpostgres, userはpostgres, passwordはpostgres。これを起動するコンテナに設定しないとdbに接続できなくてエラーが出てしまう。
// dokcer-compose.yml version: "3" services: db: image: postgres ports: - "5432" environment: - POSTGRES_DB=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres web: build: . command: python3 manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" depends_on: - db
drf エラーハンドリング
リソースに変更を加えるapiを作る際に、以下の要望があった。 apiを叩いたら成功したか失敗したかの結果をTrue, Falseで欲しい。この場合drfでResponseオブジェクトを返せば良い。
return Response({"success":True}) # または return Response({"success":False})
こんな感じで返せば良い。こうするとフロントエンドでsuccessの結果から処理を分岐させることができるようになり、統一するとフロントエンド側が書きやすくなる。
これを実装するにはエラーハンドリングについて少し考える必要があり、それに関する良い記事がある。
関連するソースコードはこちら:
django-rest-framework/exceptions.py at master · encode/django-rest-framework · GitHub
具体例な解決案
クライアントの操作に誤りがある場合には、djangoが自動的にエラーをすくいとり、エラーがあった旨のレスポンスを自動的に返す。それを使っている以上responseにはsuccessキーは使えない。
エラーをキャッチしてなおかつsuccessキーを返すには以下のようにすると良い。 一言添えるならば、drf上ではエラーが発生したらexception_handlerが呼び出され、handle_exceptionが作動する。こいつがdrfのエラーをどう処理するか定めているものなので、そこを自分好みにカスタマイしてみてはどうか?ってことだ。
from rest_framework.views import APIView from rest_framework.exceptions import APIException, NotFound class BookTagView(APIView): def get_exception_handler(self): default_handler = super().get_exception_handler() def handle_exception(exc, context): if isinstance(exc, KeyError): return Response({'success': False, 'error': { 'message': exc.detail, } }, status_code=exc.status_code) elif isinstance(exc, NotFound): return Response({'success': False, 'error': { 'message': exc.detail, } }, status_code=exc.status_code) else: # unknown exception return default_handler(exc, context) return handle_exception def post(self, request): data = request.data try: book_title = data["book_title"] tag = data['tag'] except KeyError: raise KeyError() try: book = Book.objects.get(title=book_title) except NotFound: raise NotFound() book.tags.add(tag) book.save() return Response({"success": True}) class KeyError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = _('送信データのkeyにはbook_title, tagがないのでエラーとなります') default_code = 'key_error'
Amazon ES
Fine-Grained Access Control in Amazon Elasticsearch Service - Amazon Elasticsearch Service
Amazon ES Security
ESのセキュリティは3つから構成されている。
Network -> Public accessか VPC accessで設定できる
Domain access policy -> resource-based access policy
Fine-grained access control -> fine-grained access control evaluates the user credentials and either authenticates the user or denies the request. If fine-grained access control authenticates the user, it fetches all roles mapped to that user and uses the complete set of permissions to determine how to handle the request.
Remote Reindex in Amazon Elasticsearch Service - Amazon Elasticsearch Service
指定のURLからのみのアクセスを行う
パブリックアクセスで以下をJSON内で指定すると指定のURLのみからアクセスできる設定になる
IPベースのポリシー
"Condition": { "IpAddress": { "aws:SourceIp": "[アクセス元IPアドレス]/32" } }
ただ、これをIPを調べて登録してもうまく行かなかった。
https://checkip.amazonaws.com/のipを使うとうまく行った。理由はわからない。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/anonymous-not-authorized-elasticsearch/
elasticsearch alias
インデックスにはaliasをつけられるらしい。
aliasの確認
GET sample1/_alias GET <index_name>/_alias
aliasがない場合には空が表示される
// 結果 { "sample1" : { "aliases" : { } } }
aliasの追加、更新
Create or update index alias API | Elasticsearch Reference [7.12] | Elastic
PUT sample1/_alias/my_alias PUT <index_name>/_alias/<alias_name>
実行結果
{ "sample1" : { "aliases" : { "my_alias" : { } } } }
aliasの複数インデックスへの追加の注意点
ある名前のaliasは一つのインデックスのみに使えるわけではなくて、複数のインデックスに設定できる。 つまりalias Xをもつindex Aとindex Bが存在することを許容する。
しかしながら書き込み時は注意すること。書き込めるインデックスは複数設定できないっぽいので複数のインデックスにalias経由でデータを格納しようとするとエラーが出てしまう。
Is it possible to write to multiple indexes with an ElasticSearch alias? - Stack Overflow
Update index alias API | Elasticsearch Reference [7.10] | Elastic
searchをした場合当aliasをもつ複数のインデックスから結果を表示することはできる。
同一エイリアスを設定し、そのエイリアスを使ってインデックスに書き込みを実現したい場合は以下のように "is_write_index"を設定する必要があるようだ。当然true に設定するのは一つのインデックスのみ。
POST /_aliases { "actions": [ { "add": { "index": "sample1", "alias": "my_alias", "is_write_index": false } }, { "add": { "index": "sample2", "alias": "my_alias", "is_write_index": true } } ] }
matchとmatch_phraseクエリの検索結果の違いをメモ
以下のsample1インデックスを作成し、matchやmatch_phraseを使って観察してみる。
インデックスはhtmlタグが存在している場合にタグを削除するhtml_strip, 日本語文章を日本語単語に分けるkuromoji_tokenizerを使っている。
PUT sample1 { "settings": { "analysis": { "analyzer": { "kuromoji_standard": { "char_filter": [ "html_strip", "icu_normalizer" ], "tokenizer": "kuromoji_tokenizer", "type": "custom" } } } }, "mappings":{ "properties":{ "content":{ "type": "text", "analyzer":"kuromoji_standard" } } } }
ドキュメントの登録
以下3つの文章を登録する。あとで”消費者センター”で検索をかけるための文章になっている。
PUT sample1/_doc/1 { "content":"私は消費者センターに連絡する" } PUT sample1/_doc/2 { "content":"カロリーを消費する" } PUT sample1/_doc/3 { "content":"消費する者はコンシューマー。中央はセンターという。" }
また、analyzeAPIの結果では"消費者センター"の単語は以下にように分けられているを確認した。
GET sample1/_analyze { "analyzer": "kuromoji_standard", "text":"消費者センター" } ///////////////////////////////////// 結果 ///////////////////////////////////// { "tokens" : [ { "token" : "消費", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "者", "start_offset" : 2, "end_offset" : 3, "type" : "word", "position" : 1 }, { "token" : "センター", "start_offset" : 3, "end_offset" : 7, "type" : "word", "position" : 2 } ] }
"消費"、"者"、"センター"で分けられている。
この3単語が検索のキーワードになることが分かったので、
”カロリーを消費する”なら”消費"で転置インデックスされていることが推定できるし、
”消費する者はコンシューマー。中央はセンターという。”であれば、”消費”, ”者”, ”センター”で転置インデックスされていることが推定できると思われる。
matchによる検索を試す
結果は”消費者センター”と一致した"私は消費者センターに連絡する"の他、検索にかけた単語すべて(”消費”, ”者”, “センター“)を満たす"消費する者はコンシューマー。中央はセンターという。"
もヒットする。さらに、”者”, ”センター” は一致しないが、”消費”のみ一致した"カロリーを消費する"
もヒットする事がわかる。
これらからmatchはor検索を実行していると思われる。
GET sample1/_search { "query":{ "match":{ "content":"消費者センター" } } } /////////////////////////////// 結果 ////////////////////////////////////// { "took" : 0, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 0.8630463, "hits" : [ { "_index" : "sample1", "_type" : "_doc", "_id" : "3", "_score" : 0.8630463, "_source" : { "content" : "消費する者はコンシューマー。中央はセンターという。" } }, { "_index" : "sample1", "_type" : "_doc", "_id" : "1", "_score" : 0.8630463, "_source" : { "content" : "私は消費者センターに連絡する" } }, { "_index" : "sample1", "_type" : "_doc", "_id" : "2", "_score" : 0.2876821, "_source" : { "content" : "カロリーを消費する" } } ] } }
match_phraseによる検索を試す
結果は"私は消費者センターに連絡する"のみヒットした。 "消費する者はコンシューマー。中央はセンターという。"といった検索時に使われる単語が全て含まれた文章は排除されているのに注目したい。これを使えば、検索時に使用したキーワードがまるまる入った文章のドキュメントだけを拾ってこれるのでは?と思う。 https://www.elastic.co/guide/en/elasticsearch/reference/7.x/query-dsl-match-query-phrase.html#query-dsl-match-query-phrase
Full text queries | Elasticsearch Reference [7.x] | Elastic
GET sample1/_search { "query":{ "match_phrase":{ "content":"消費者センター" } } } ///////////////////////////// 結果 /////////////////////////////////////// { "took" : 0, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 0.8630463, "hits" : [ { "_index" : "sample1", "_type" : "_doc", "_id" : "1", "_score" : 0.8630463, "_source" : { "content" : "私は消費者センターに連絡する" } } ] } }
既存のデータをコピーしたり新しいインデックスにデータをコピーしたい
次にデータ移行の際に移行するデータは新インデックスのルール(マッピング)に従ってデータを格納し、検索できるのか検証する。 要するに既存のデータを新インデックスに移し替えると新インデックスのtokenizerやchar_filterに従って検索できるのかを確認する。
// おはようが検索できないインデックスを作成する PUT my_sample { "settings":{ "analysis": { "analyzer": { "my_kuromoji_analyzer": { "type": "custom", "tokenizer": "kuromoji_tokenizer" } } } }, "mappings":{ "properties": { "content":{ "type":"text", "analyzer": "my_kuromoji_analyzer" } } } }
// データ格納 PUT my_sample/_doc/1 { "content":"<p>おは<span>よう</span></p>" } // これでは”おはよう”は検索できないことを確認できた
次に新たなインデックスを作成する。そのインデックスにデータを流しこむ。
PUT my_new_sample { "settings": { "index": { "analysis": { "analyzer": { "kuromoji_normalize": { "char_filter": [ "html_strip", "icu_normalizer" ], "tokenizer": "kuromoji_tokenizer", "filter": [ "kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "ja_stop", "kuromoji_stemmer", "lowercase" ] } } } } }, "mappings":{ "properties":{ "content":{ "type": "text" } } } }
// my_sampleのデータをmy_new_sampleに移行する(コピー) POST _reindex { "source": { "index": "my_sample" }, "dest": { "index": "my_new_sample" } }
このmy_new_sampleインデックスではおはようは検索できるかを確認する
GET my_new_sample { "query":{ "match":{ "content": "おはよう" } } } // この検索結果はヒットする。 // つまり既存のデータを新インデクスに移行した場合、新インデックスのルールに従ってデータが格納されることが確認された。