diadia

興味があることをやってみる。自分のメモを残しておきます。

nodejs expressでhttps通信を開発環境でできるようにするう

参考資料:

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. 不動産という商品の単価が高い
  2. 売上に対する原価が低い
  3. 企画によって商品価格をあげることができる

売上に対する原価が低いの補足:売買の場合は不動産を仕入れるので原価がかかるが、高価な設備や機材などは必要なく、製造業や建築業、サービス業と比べると原価がかからないと言える。

地価公示とは、国土交通省が毎年1回標準地として定める不動産の価格を調査して公的に発表すること。

宅地建物取引業は免許制度となっているが、賃貸業・管理業は免許制度ではないので参入ハードルが高くない。

nodejs をすこし使えるようにするためのメモ

参考: Node.js入門 - とほほのWWW入門

今これを使うのは、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

// 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についての解説資料

https://wa3.i-3-i.info/word13464.html

mac ショートカット

目的

横になったり、リラックスした姿勢でPCを操作するためにはマウスを使わないほうが都合が良い。マウスをできるだけ排除した操作ができるような方法を調べメモしていく。

ショートカットキーカテゴリ

  1. macosによるOS系のもの
  2. ブラウザ(chrome)
  3. 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の結果から処理を分岐させることができるようになり、統一するとフロントエンド側が書きやすくなる。
これを実装するにはエラーハンドリングについて少し考える必要があり、それに関する良い記事がある。

https://medium.com/@ceoroman9/django-rest-framework-apiview-class-advance-usage-permissions-response-and-exception-handling-d4321a08a83f

関連するソースコードはこちら:

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 accessVPC 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ベースのポリシー

https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-ac.html#es-ac-types-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": "おはよう"  
    }
  }
}
// この検索結果はヒットする。
// つまり既存のデータを新インデクスに移行した場合、新インデックスのルールに従ってデータが格納されることが確認された。