Engineer's Way

主にソフトウェア関連について色々書くブログです。

ElasticSearchで親-子-孫の3世代ドキュメントを作って、子や孫の内容で検索する

 

f:id:matsnow:20171204030753p:plain

前職の時、AWSのElasticSearchService (Ver5.3) を使っていましたが、日本語どころか英語でもあまり情報がなかったりするので苦労しました。
RDBでよくある、テーブル結合して検索ということがしたかったのですが、手間取ったので方法を残しておきます。

ElasticSearchで3世代ドキュメント構造を持ったindexを作る

たとえば、「商品カテゴリ - 商品 - 商品のレビュアー」みたいに、それぞれのデータに1:nの関係性を持たせたい場合、RDBなら外部キーを使ってJOINすればOKです。 ElasticSearchで、そういった構造を表現したい場合、_typeを使います。

親の方から1,2,3世代とするとき、1世代目、2世代目、3世代目に該当する_typeの名前を決めて、インデックスを設計します。 上の例の場合、名前をcategoryproductreviewerとするなら、以下のようなmappingのJSONをインデックスに適用しておきます。

PUT /market

{
  "mappings": {
    "category": {},
    "product": {
      "_parent": {
        "type": "category" 
      }
    },
    "reviewer": {
      "_parent": {
        "type": "product" 
      }
    }
  }
}

RDBで各テーブルのレコードに外部キーを持たせるのと違う点は、「あくまで1つのindexの中にフラットなデータとして入っている」ということです。 要は_idの重複などを気にしないといけません。

3世代ドキュメント構造を持ったindexにデータを放り込む

  1. 2世代目のドキュメントを放り込む時、_parent: 1世代目の_id(例:_parent: "c1")を追加で渡します。
  2. 3世代目のドキュメントを放り込む時、_parent: 2世代目の_id, _routing: 1世代目の_id(例:_parent: "p1", _routing: "c1")を追加で渡します。

これを_bulkAPIに渡すJSONとして表現するなら

{ "index": { "_index": "market", "_type": "category", "_id": "c1" } }
{"category_name": "clock" }
{ "index": { "_index": "market", "_type": "product", "parent": "c1", "_id": "p1" } }
{"product_name": "clock1" }
{ "index": { "_index": "market", "_type": "reviewer", "parent": "p1", "routing": "c1", "_id": "r1" } }
{"reviewer": "hoge" }

みたいになります。

こうすることで、index内が、図で表すと以下のようなドキュメント構造になります。 f:id:matsnow:20171204030054j:plain

子ドキュメントで検索し、親・孫ドキュメントを含めた一連のドキュメントを取得する

これがあまり情報がなかったのですが、以下のようなクエリを書けば子ドキュメントのデータで検索した上で、3世代分のドキュメントを結合して一気に返してもらうことができます。

{
    "query": {
        "has_child" : {
            "type": "product",
            "query": {
                "bool": {
                    "must": [
                      {
                         "term": {"product_id": "123"}
                      },
                      {
                         "has_child": {
                            "type": "reviewer",
                            "inner_hits": {},
                            "query": {"match_all": {} }
                         }
                      }
                   ]
                }
            },
            "inner_hits": {}
        }
    }
}

boolクエリで、2世代目への検索クエリ(term)と、3世代目の全件取得(has_child.query.match_all)をまとめるのがポイントです。 ※ もちろん、3世代目で更に絞り込みをしたければ、match_allの代わりに別のクエリを使います。

それと、inner_hitsを書くことで、2、3世代目のドキュメント一式を取得できます。 書き忘れると1世代目のドキュメントしか返ってきません。

注意点

公式見解として、今後は1つのindexに対して1つのtypeが推奨されています。
また、親子関係をもたせたいときは5.6から追加されたjoinフィールドを使って欲しいとのこと。
今後のロードマップによると、バージョン8.xあたりでtypeが事実上消滅するようです。(まだまだ先の話でしょうが。。)

www.elastic.co

AWSだと、12/6時点でもまだ5.5までしか選択できないですが、5.6以降がサポートされたら単一type & joinフィールドを使う方向で設計した方が良さそうですね。

Electronでパッケージを作ろうとした時に「May not delete」というエラーが出る

 

electron-packagerでElectronでパッケージを作ろうとした時、以下のエラーが発生しました。  

$  electron-packager . sample --platform=darwin --arch=x64 --overwrite --out build"
npm ERR! May not delete: /private/var/folders/q3/hg9ht5vx5rjddgghnk8z9w480000gn/T/electron-packager/darwin-x64/sample-darwin-x64/Electron.app/Contents/Resources/app/node_modules/.bin

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/hogehoge/.npm/_logs/2017-11-14T10_35_53_001Z-debug.log

npmのバージョンが問題だったらしく、5.3だとダメでした。
内部で実行しているnpm prune --productionがやらかしている模様。

github.com

なお、最新の5.5.1ならOKでした。

API Gateway + CloudFrontの構成でカスタムドメインを使用すると、IAMでのアクセス制限ができない件

 

AWSAPI Gatewayを使ってプロダクション環境でAPIを公開する時、WAFを噛ませて防御したいと思うことがあります。
今のAPI Gatewayには、残念ながらWAFと直接関連付ける仕組みがないので、そういった場合はAPI Gatewayの前段にCloudFrontを置く必要があります。
(API Gatewayも内部的にはCloudFrontを使っているので、二重になってしまいますが仕方ありません)

ドメイン名もビジネス要件に合ったものを使うことになるので、それをRoute53に登録して、CloudFrontと紐づけることになります。
図で示すと以下のようになりますね。

f:id:matsnow:20171031011417p:plain

一方、API Gatewayにはざっくり以下の3つのアクセス制限方法があります。

  1. IAM認証 (v4署名)
  2. カスタムオーソライザー
  3. Cognitoオーソライザー

docs.aws.amazon.com

このうち、IAM認証を先ほどのAPI Gateway + CloudFront + 任意のドメインの構成上で使おうとしても失敗します。
正確には、v4で署名したリクエストをCloudFront(の先にあるAPI Gateway)に送信しても403エラーで返ってきます。
Amazonのサポートにも問い合わせてみましたが、残念ながら現時点の仕様とのことです。

誠に恐れ入りますが、API Gateway の前段に CloudFront ディストリビューションを配置頂いている場合、 API Gateway 上では生成 SDK による API の IAM 認証をご利用頂くことができません。 すでにご確認を頂いております通り、 SDK 内の invokeUrl を CloudFront にて公開頂いているカスタムドメイン名に編集頂くことで、 署名の生成に使用されるドメイン名と実際の API エンドポイントであるドメイン名が不一致となり、API Gateway 上での署名検証が失敗致します。

よって、API Gateway + WAF + カスタムドメインを使いたい場合、APIの認証はカスタムオーソライザーかCognitoオーソライザーでやることになります。 API Gatewayがバージョンアップして、直接WAFと連携させられるようになるのを期待しましょう。

ほかのサーバレス系の記事

matsnow.hatenablog.com matsnow.hatenablog.com

Angular4で開発した複数環境のSPAに、それぞれ別のGoogleタグマネージャーのスニペットを設定する方法

 

背景

本番環境と検証環境で運用しているAngular4で作ったSPAに、Googleタグマネージャー(GTM)のコードを埋め込むことになりました。

Googleタグマネージャーとは

f:id:matsnow:20170929004023p:plain

Googleタグマネージャー自体の概要や使い方は以下が参考になります。

www.h-nanae.com

www.milab-inc.com

埋め込み手順

埋め込む必要のある環境が本番環境だけであれば、単にGTMが吐き出してくれたスニペットをそのままindex.htmlに書けばいいわけですが、検証環境にも別のコンテナのスニペットを埋め込みたいとなった時に、少し詰まりました。

というのも、スニペットはheadタグの中とbodyタグ直下にそれぞれ埋め込む必要があるので、app-rootの外にあるindex.htmlに何とかして環境変数を反映させないといけないからです。

とりあえず、少し考えて、実際に試したのが以下のやり方です。

1) environment.tsへの追記

environment.prod.tsなど、各環境用の定義ファイルに

environment: {
    production: true,
    googleTagManager: 'GTM-XXXX'
};

みたいに書いておきます。(GTM-XXXXのところは GTMのコンテナIDを記述)

2) main.tsにスニペット埋め込み用のコードを記述

main.tsのif (environment.production) の中に以下を追記します。
本来、コンテナのIDが入っている箇所は、テンプレート記法で環境変数に置き換えています。

if (environment.production) {
  enableProdMode();

  /////////////// ここから //////////////////
  const script = document.createElement('script');
  script.text = `
  (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
  })(window,document,'script','dataLayer','${environment.googleTagManager}');`;
  document.head.appendChild(script);

  const noscript = document.createElement('noscript');
  const iframe   = document.createElement('iframe');
  iframe.src     = `https://www.googletagmanager.com/ns.html?id=${environment.googleTagManager}`;
  iframe.height  = '0';
  iframe.width   = '0';
  iframe.setAttribute('style', 'display:none;visibility:hidden');
  noscript.appendChild(iframe);
  document.body.insertBefore(noscript, document.body.firstChild);
  /////////////// ここまで //////////////////
}

これで後はGTMのコンソール上でタグやトリガーを設定してやれば、問題なく環境別にアクセス解析をすることができるようになります。
なお、GoogleAnalyticsでも同じ手法が使えるはずです。

あまり実施する必要に迫られることは無さそうなことですが、一応備忘録として。

CloudFormationでElasticSearchService 5.x系を構築する時の注意点

 

CloudFormationでElasticSearchServiceを構築しようとしたところ、
Createに異様に時間がかかる上、

Creating Elasticsearch Domain did not stabilize.

というエラーが出て構築に失敗してしまうという状況に出くわしました。 (ロールバックにも異様に時間がかかるという、、)

調べてみたところ、バージョン5.x系のElasticSearchを作る場合、 AdvancedOptionsの指定が必須らしいとのこと。

https://forums.aws.amazon.com/thread.jspa?messageID=768527

YAMLだとこうなります。

      ElasticsearchVersion: "5.3"
      AdvancedOptions:
        rest.action.multi.allow_explicit_index: true
        indices.fielddata.cache.size: ""

公式ドキュメントだと「Required: No」となっているので結構な罠ですね、、。

AWS::Elasticsearch::Domain - AWS CloudFormation

Angular4(2+)でカスタムValidationを作る

 

Angular4(2+)で独自のValidationを作ってフォームに適用したい時のやり方を調べましたので、備忘録として残しておきます。

1. Validationディレクティブを作成する。

まず、ディレクティブとして、カスタムValidationを実装したクラスを作ります。 以下は単純な例で、フォームへの入力値が「abc」だったらNG、それ以外はOKとしてます。

import { Directive } from '@angular/core';
import { AbstractControl, FormControl, ValidationErrors } from '@angular/forms';

@Directive({
    selector: '[validateCustom][ngModel],[validatorCustom][formControl]'
})

export class CustomValidator {
    check(control: AbstractControl): ValidationErrors|null {
        return control.value === 'abc' ? {‘custom': true} : null;
    }
}

入力をNGとしたい時に「{‘項目名’: true}」をreturnします。

参考:angular.coreのソース
https://github.com/angular/angular/blob/71f5b73296708014b740fb5dd0145c0599de7a19/packages/forms/src/validators.ts

2. 作ったカスタムValidationを使う。

使いたいコンポーネントの中で以下のように書くことで使用できます。

import ‘CustomValidator’ from ‘../directives/custom-validator';

:
onNgInit() {
  const cv = new CustomValidator();
  this.control = new FormControl('', cv.check);
}

AWS SESでスパム認定されない独自のメールアドレスドメインを登録する

 

独自ドメインでメールを送信する時、受信側にスパムメールフィッシングメールと認定されないように注意しなければなりません。そのために、SPFDKIMといった認証技術を使って、ドメインを認証する必要があります。

SPF/DKIMの普及が迷惑メール対策に効果、Googleが調査結果を公表 -INTERNET Watch Watch

今回はAWS SESを使って、独自ドメインSPFDKIMの認証をしてみます。

1. ドメインの取得

まずはドメインが必要です。
Route53などで取得しましょう。

2. メールドメインの登録&DKIMの設定

  1. AWSコンソールでSESを選択 東京リージョンには残念ながら存在しないので、オレゴンリージョンあたりを選択します。
  2. 「Verify a New Domain」をクリックする。 f:id:matsnow:20170604170356p:plain:w400
  3. ドメイン名を入れ、「Generate DKIM Settings」にチェックを入れ、「Vefiry This Domain」をクリックする。 f:id:matsnow:20170604170418p:plain:w500
  4. Use Route53をクリックする。 f:id:matsnow:20170604170433p:plain:w500
  5. 画面が切り替わった後、そのまま「Create Record Set」をクリックする。 f:id:matsnow:20170604170439p:plain:w500
  6. pending vefiricationの状態になるので、verifyになるまで数10分待つ。 f:id:matsnow:20170604170511p:plain

3. SPFの設定

  1. 「Domains」の中から、追加したドメインをクリックする。
  2. 一番下の「MAIL FROM Domain」を開き、「Set MAIL FROM Domain」のボタンをクリックする。
  3. ダイアログ中の「MAIL FROM Domain」に任意のサブドメインを入力して「Set MAIL FROM Domain」をクリックする。 f:id:matsnow:20170604170533p:plain:w400
  4. 「Publish Records Using Route53」をクリック f:id:matsnow:20170604170606p:plain:w400
  5. Warningの画面になるので、MXとSPFの両方にチェックを入れてボタンをクリック f:id:matsnow:20170604170613p:plain
  6. verifyになるまで待つ。 f:id:matsnow:20170604171628p:plain

4. 設定できていることの確認

Gmailで確認してみます。

  1. 「Domains」から登録したドメインを選び、「Send a Test Email」でテストメールを送信する。 f:id:matsnow:20170604171320p:plain:w500
  2. 受信したメールの詳細を表示する。 f:id:matsnow:20170604170633p:plain
  3. dkim=pass」と「spf=pass」の所に、設定したドメインが記載されていればOK。 f:id:matsnow:20170604170641p:plain

以上でドメインの認証が完了です。

AWS LambdaをNode.jsで簡単に作れるサーバレスアーキテクチャのフレームワークClaudia.jsを実戦で使ってみた

 

f:id:matsnow:20170525010920p:plain:w200

はじめに

今のプロジェクトでAWS Lambdaによるサーバレスアーキテクチャのシステムを開発していますが、フレームワークとしてClaudia.jsというものを選択してみました。

Claudia.jsとは

AWS Lambdaのようなサーバレスアーキテクチャ用のフレームワークとしてはServerless Frameworkが有名ですが、
それ以外にClaudia.jsというフレームワークがあります。 claudiajs.com

以下のような点がServerlessとの大きな違いです。

  • Claudia.jsはAWS Lambda、かつNode.js (JavaScript、TypeScript)に特化している。 特化している分、非常に少ない手間でデプロイまでできる。
  • api-builderやbot-builderなどの追加パッケージを使うことで、簡単にAPIBotを作れる。

なお、サーバレスアーキテクチャということもあり、フレームワークといっても、
フルスタックやMVCなどではなく、むしろビルド・デプロイツールといった色合いが強いです。

Claudia.jsの導入

Claudiaの導入は以下のブログが分かりやすいです。 qiita.com

dev.classmethod.jp

あとは公式のサンプル集を見れば、たいていのものは作れると思います。(手抜き) github.com

実際に使ってみる

Claudia.jsはLambdaやAPIをNode.jsを書いていく上では非常に便利ですが、 実戦投入するに当たって、いくつか考慮する点があります。

1. package.jsonは関数ごとに用意する

公式のサンプルなどでは、ディレクトリは1階層だけで、そこにpackage.jsonが置いていますが、実開発ではディレクトリは複数階層になることが多いと思います。
例えば、以下のようなディレクトリ構成にする場合、api/hello、function/foo、function/hogeの各ディレクトリにpackage.jsonが必要です。

sample  
├── api  
│   └── hello  
│       └── index.ts  
└── function  
    ├── foo  
    │   └── index.ts   
    └── hoge  
        └── index.ts  

その時のpackage.jsonの内容は、例えば「function/foo/package.json」であれば、以下のようになります。
nameがLambda関数の名前、descriptionは説明文、mainはエントリポイントのJSファイルです。

{
  "name": "foo",
  "description": "Lambda関数のサンプル",
  "main": "index.js"
}

なお、node_modulesのディレクトリは下位階層には不要です。 通常のプロジェクトと同じくトップにあればOKです。

2. ロールはあらかじめ作成しておく

Claudia.jsのコマンドでは、とりあえず以下を指定すればLambda関数を作成できます。 sh $ claudia create --region ap-northeast-1 --handler index.handler ただし、この場合、Claudia.jsによってIAMロールが毎回新しく作られてしまいます。 そのため、あらかじめLambda用のIAMロールを作成し、「–role sample-role」のように指定するようにした方が良いです。

3. claudia create –handlerの指定は相対パス

単純なことですが、–handlerで指定するハンドラ関数はディレクトリ構成が影響します。 例えば、src/ の下にindex.jsを入れており、「export handler」としている場合、「–handler src/index.handler」と指定する必要があります。

4. api-builderの中でpromiseを扱うときはpromiseをreturnする

ハマりどころの1つとして、API作成時にPromiseを使う場合、それをreturnしないと、 CloudWatchLogsなどにも何も出力されないまま、APIが終了します。

api.post('/user', function (request) {
    return dynamoDb.put({
        TableName: request.env.tableName,
        Item: {  userid: request.body.userId }
    }).promise();
}); 

公式ドキュメントの「4. Return a Promise out of the API handler」に記載されていますが、 かなりハマりやすい点なので注意が必要です。 https://claudiajs.com/tutorials/external-services.html

まとめ

最初に慣れるまでは若干手間取るかもしれませんが、一度使い方を覚えれば非常に楽に開発が進められる、良いフレームワークだと思います。
とりあえず、今のプロジェクトでは、このままClaudia.jsを使い続けるつもりです。

Linuxでのファイルの正しい配置について

 

続きを読む

Exrm(Elixir Release Manager)を使ったリリースでエラーが出た時の対処法

 

続きを読む