マイクロサービスとSOA

似ているよう異なる二つの概念をまとめる。

SOA(サービス指向アーキテクチャ)

  • いくつかのサービスのコレクションによってシステムを実現する構造
  • ソフトウェアを繋ぐミドルウェアであるESB(エンタープライズサービスバス)によって、サービス間の結合を実現する
  • サービス間で同じリソース、ストレージを共有する

マイクロサービス

  • いくつかのサービスのコレクションによってシステムを実現する構造
  • REST API のようなシンプルで軽い手段でサービス間の結合を実現する
  • サービスごとにリソース、ストレージが分離されている

ポイントは通信手段、リソースやストレージを共有するか否かというところかな。
マイクロサービスの方は、それぞれのサービスがより独立しているという認識。

Elasticsearch のCRUD

いつでもElasticsearchでCRUDが実行できるように自分用のメモです。
ElasticsearchはREST APIを提供しているので、馴染みのある感覚でデータの操作を実行できます。
かといってもCurlで実行するのは辛いのでKibanaのコンソールを使います。

使用バージョン

  • Elasticsearch 7.7.1
  • Kibana 7.7.1

Create

  • Documentの新規作成はPUTもしくはPOSTを利用します。

PUTの場合

PUT /member/_doc/1
{ 
  "name": "Beats", 
  "age" : 26, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
} 
{
  "_index" : "member",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

またこっちの形式でも結果は同じ

PUT /member/_create/1
{ 
  "name": "Beats", 
  "age" : 26, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}

POSTの場合

POSTを用いる場合はIDを指定しなくても良いです。指定がない場合は自動採番されます。

POST /member/_doc/
{ 
  "name": "Beats", 
  "age" : 26, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
} 
{
  "_index" : "member",
  "_type" : "_doc",
  "_id" : "4_1FfHcBun3pb7RqaiAK",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}

またはこっちの形式でも結果は同じ

POST /member/_create/1
{ 
  "name": "Beats", 
  "age" : 26, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}

READ

GETの場合

GET /member/_doc/1
{
  "_index" : "member",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "Beats",
    "age" : 26,
    "birthday" : "1994-10-29",
    "blog_url" : "https://beatsbeats.hatenablog.com/",
    "job" : [
      "monk",
      "developer"
    ],
    "favorite_song" : {
      "title" : "fav_song",
      "created_at" : "2000-01-01"
    }
  }
}

こっちの形式を使えばインデックスの名前などのメタデータを除いた形で取得可能です。

GET /member/_source/1
{
  "name" : "Beats",
  "age" : 26,
  "birthday" : "1994-10-29",
  "blog_url" : "https://beatsbeats.hatenablog.com/",
  "job" : [
    "monk",
    "developer"
  ],
  "favorite_song" : {
    "title" : "fav_song",
    "created_at" : "2000-01-01"
  }
}

UPDATE

フィールドを指定して更新する場合は以下の形式のリクエストを実行します。

POST /member/_update/1
{ 
  "doc": {
    "name": "Beeeats", 
    "age" : 27, 
    "birthday": "1995-10-29",
    "blog_url": "",
    "job": ["developer"], 
    "favorite_song": {"title":"fav_song","created_at":"2000-01-03"}
  }
}
{
  "_index" : "member",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

Document全体を新たなDocumentに更新する場合は以下の形式のリクエストを実行します。

PUT /member/_doc/1
{ 
  "name": "Beats", 
  "age" : 27, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}
POST /member/_doc/1
{ 
  "name": "Beats", 
  "age" : 27, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
}

DELETE

DELETE /member/_doc/1
{
  "_index" : "membe",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 3,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}

Elasticsearch の基本用語

Elasticsearch に入門したということで基本の用語についてまとめました。

Document

  • Elasticsearch に格納するデータの一つの単位を指します。
  • RDS でいうレコードに相当します。
  • JSON形式のオブジェクトです。
  • Documentは一つ以上のFieldを持ちます。
  • IDは指定がなければ自動採番されます。

Field

  • Documentを表現するJSONオブジェクトの、KeyとValueの組み合わせを指します。
  • Fieldにはいくつかのデータ型が存在します。

www.elastic.cowww.elastic.co

以下に基本的な型のみ列挙しておきます。

text

  • 文字列を表す型です。
  • text型は格納される際に単語ごとに分割され、転置インデックスが構成されます。

keyword

  • 文字列を表す型です。
  • text型と異なる点として、単語が分割されません。
  • メールアドレスやURLなどの完全一致時のみヒットして欲しいような値を格納します。
  • keyword型でhttps://beatsbeats.hatenablog.com/を格納した場合、beatshatenablogで検索しても検索でヒットしません。

long,short,integer,float...etc

  • 数値を表す型です。(公式ページに詳しく記載があります。)

date

  • 日付を表す型です。
  • 下記のいずれかの形式を指定できます。
  • UNIXエポック(1970/1/1 0:00:00からのミリ秒表記):1420070400001
  • 日付のみ:"2015-01-01"
  • 日付+時刻:"2015-01-01T12:10:30Z"

boolean

  • 真偽を表す型です。

object

  • JSONオブジェクトを表す型です。(ネスト構造)

array

  • 配列を表す型です。
  • 配列の各要素は同じ型になります。

また、一つのFiledが複数のデータ型を持つことも可能です。(マルチフィールド型) 明示的に型を定義していない状態で文字列を格納した場合には、自動的にtextとkeywordが定義されます。 こちらは一見便利ですが、インデックスが増えて容量の増加に繋がります。

Documentの簡単な例
{ 
  "name": "Beats", 
  "age" : 26, 
  "birthday": "1994-10-29",
  "blog_url": "https://beatsbeats.hatenablog.com/",
  "job": ["monk","developer"], 
  "favorite_song": {"title":"fav_song","created_at":"2000-01-01"}
} 

Index

  • Documentを格納する場所を表します。
  • RDSでいうテーブルに相当します。
    • 元々はスキーマかデータベースに相当すると言われていましたが、後述のDocument typeが廃止になったためインデックス単位でデータの分別する他、データ格納場所を分ける方法がないためIndexがテーブルに相当すると言えます。
  • Documentがそのまま格納されているのではなく、検索しやすいように様々な形式で格納されている。

Document type

  • Documentの種別を表します。
  • 廃止となった概念です。詳細は下記の記事にあります。

www.elastic.co

Mapping

  • Documentの各Fieldのデータ型を定義した情報を表します。
  • Mappingを事前に定義していなくても、ドキュメント格納時に自動的にElasticsearch側でMappingの定義をしてくれます。



以下は実際にドキュメントをどのように格納しているのかを示す用語なので図も交えてまとめました。

Node

f:id:beatsbeats:20210207200138p:plain

  • Elasticsearch が動作するプロセスを指します。
  • 実体は一つのJVMインスタンスです。
  • 一つのサーバー上で一つのNodeが動作するのが基本ですが、複数のNodeを一つのサーバー上で動作させることも可能です。
  • 一意の名(node.name)を持ちます。
  • Nodeは役割ごとに4つの属性を持ちます。(詳細は別記事にて)

Cluster

f:id:beatsbeats:20210207200210p:plain

  • 互いに通信し合う複数のNodeの集合を指します。
  • 一意のCluster名(cluster.name)を持ちます。

Shard

f:id:beatsbeats:20210207200235p:plain

  • Elasticsearchはデータを複数のノードで分散保持することがですが、分散した際のそれぞれのパートを指します。
  • Shardの数は事前(Index作成時)に設定しておく必要があります。(作成後に増やせないので注意)

f:id:beatsbeats:20210207200258p:plain

Replica

f:id:beatsbeats:20210207200319p:plain

  • Shardの複製を指します。
  • 複製元のShardをPrimary Shardと呼び、複製されたShardをReplica Shardと呼びます。
  • Replica Shardは自動的にPrimary Shardとは別のNodeに作成されます。
  • Primary Shardが配置されているNodeの障害時には、ReplicaがPrimaryに昇格します。
  • Replica Shardの数はいつでも変更することが可能です。

参考

book.impress.co.jp

nextpublishing.jp

開発途中でプロジェクト名を変更した時は一括置換すること

僕の場合 Go を使用することが多いのですが、ディレクトリ名変更語にプロジェクト直下でgo buildをした時に古いプロジェクト名でバイナリファイルが作られ続け割と長い時間ハマりました。
これはgo.modのモジュール名を変え忘れたままだったことが原因でした。
かなり基本的なことですが二度と忘れないようにと記事にしました。

初歩的なミスに限ってなかなか原因に気づけないですね。。。

CDK で DynamoDB Stream を Lambdaのトリガーにする時の注意点

タイトルを実現するために、半日近く溶かしたので備忘録として残しておきます。
CDKの細かい説明は省きます。

正常に動作するコードがこちら

import { DynamoEventSource } from '@aws-cdk/aws-lambda-event-sources';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as lambda from '@aws-cdk/aws-lambda';
.
.
.
const Table = dynamodb.Table.fromTableAttributes(...,{
    // tableArn: でもOK
    // tableArn tablaName のどちらか片方必須
    tableName: {table名},
    tableStreamArn: {tableのStreamArn}'
});
const Event =  new DynamoEventSource(Table, {
    startingPosition: lambda.StartingPosition.LATEST,
});

const fn = new lambda.Function(...,{
.
.
.
    events:[Event]
});

別のコンストラクタのスタックとして作成したDynamoDBを取得してStreamArnを使ってイベントソースを作成するには、fromTableAttributes()でテーブルを取得しないといけないようです。
fromTableArn()fromTableName()で取得するとストリームが設定されていないですよと怒られます。
ドキュメント内を解決策は探しても見つからず結局GitHubのIssueが解決の糸口となりました...
1/26 追記
あとは// tableArn tablaName のどちらか片方必須というのも味噌です。

公式→issue→ブログくらいの順番で探すのが良さそうですね。

VPC内にElasticsearchを置く時の注意点

VPC内のプライベートサブネットにElasticsearchを置いたはいいけど、LambdaからAPI叩けないしKibanaもアクセスできなくて困ったときのメモです。

解決策

  • BastionServerとして同VPC内にEC2(パブリックサブネット)を置く

   - EC2にSSH接続してダイナミックフォワードを行う(ダイナミックフォワードあまりわかっていない...)

  • Lambdaを同じVPC内に置く

   - 仕様によってプライベートかパブリックかを選択する

  • それぞれのリソースに適切なセキュリティグループ(後述)を設定する

セキュリティグループ設定

EC2

Lambda

  • 今回はインバウンドの設定は不要(仕様による)

Elasticsearch

ハマりポイント

ElasticsearchのアクセスコントロールでEC2のプライベートIPを許可しようとしたが以下のエラーが出ました。

UpdateElasticsearchDomainConfig: {"message":"You can’t attach an IP-based policy to a domain that has a VPC endpoint. Instead, use a security group to control IP-based access."}

解決策が見つからず2時間くらいはまりましたが、ちゃんと公式に書いてありました。

VPCs ではセキュリティグループを通じてドメインへのアクセスを管理できます。多くのユースケースでは、このセキュリティ機能の組み合わせで十分となり、ドメインにオープンなアクセスポリシーを安心して適用できます。

Amazon Elasticsearch Service ドメインの VPC サポート - Amazon Elasticsearch Service

どうやらセキュリティグループでの制限で十分のようです。

つまりVPC内にElasticsearchを置くケースだとオープンアクセスを選択しておいて、アタッチしているセキュリティグループのみでアクセスの制限を行う形になります。

追記(2021/1/16)

CDKでElasticsearchを作成する時は明示的にオープンアクセスを設定しなければいけないみたいです。
以下がオープンアクセスのポリシーステートメントです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "{domain ARN}/*"
    }
  ]
}

またKibanaにアクセスするためにSOCKSプロキシを用いたダイナミックフォワード方式ではなく、ローカルフォワード方式でSSHトンネルを掘るときには、EC2にアタッチするセキュリティグループにローカルマシンからの443ポートアクセスを許可し、8157ポートを閉じておきましょう。
コマンドもこちらに残しておきます。

$ ssh -i xxxxxxxx.pem ec2-user@{dns} -N -L 9200:{elasticsearch-domain}:443

https://localhost:9200/_plugin/_kibanaへアクセスすると、Kibanaが表示されるはずです。

参考

github.com

aws.amazon.com

docs.aws.amazon.com

VPC内にLambdaを置く時の注意点

LambdaをVPC内に置こうとしてエラーが発生した時のメモです。

マネコンからVPC、サブネット、セキュリティグループを指定して保存しようとすると以下のエラーが出ました。

The provided execution role does not have permissions to call CreateNetworkInterface on EC2

ネットワークインターフェイスを作る権限がないみたい。
Lambda VPC ネットワークインターフェースググると公式がヒット。

Lambda は、関数のアクセス許可を使用してネットワークインターフェイスを作成および管理します。VPC に接続するには、関数の実行ロールに次のアクセス許可が必要です。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-vpc.html

AWS管理ポリシーであるAWSLambdaVPCAccessExecutionRoleをLambdaの実行ロールにアタッチしてあげなければいけません。 公式を参照する癖をつけたいですね。