その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

DynamoDB LocalでもDynamoDB Streamsは使える

TL;DR
  • DynamoDB LocalはDynamoDB Streamsをサポートしている.
  • 本物のDynamoDB Streamsのように,一定期間以上古いレコードについてはトリミングされる (具体的なトリミング期間については未調査).
  • ドキュメントが少ない (見つけられなかった……) のに加え,ライセンス的に逆コンパイルは不可能なのでどういう仕組みで実現されているかがわからない……
本文

DynamoDB Localというローカル環境で動くDynamoDBがあり *1,これはDynamoDBの動作検証やテストに利用するためのミドルウェアなんですが (もちろん本番環境で使うものではない),これがDynamoDB Streamsをサポートしていることはあまり知られていません.僕も一昨日知りました.

Yes, the latest version of DynamoDB Local supports DynamoDB Streams on the same port configured for the DynamoDB service (by default 8000).
https://forums.aws.amazon.com/thread.jspa?threadID=231696

つまりこれはDynamoDB Streamsを使っているようなソフトウェアのテストや動作の検証をDynamoDB Localを使って行えるということです.

以下はgoの場合のサンプルコードですが,内容はAWS上で動いているDynamoDBでDynamoDB Streamsを利用する場合と大差はありません.

dynamoSession, err := session.NewSession(&aws.Config{
    Region:   aws.String("ap-northeast-1"),
    Endpoint: aws.String("http://localhost:8000"),
})
if err != nil {
    panic (err)
}

dynamo := dynamodb.New(dynamoSession)
dynamoStream := dynamodbstreams.New(session)

DynamoDB Localに向けたDynamoDB及びDynamoDB Streamsのクライアントを作ります.

_, err = dynamo.CreateTable(&dynamodb.CreateTableInput{
    AttributeDefinitions: []*dynamodb.AttributeDefinition{
        {
            AttributeName: aws.String("ID"),
            AttributeType: aws.String("S"),
        },
        {
            AttributeName: aws.String("Name"),
            AttributeType: aws.String("S"),
        },
    },
    KeySchema: []*dynamodb.KeySchemaElement{
        {
            AttributeName: aws.String("ID"),
            KeyType:       aws.String("HASH"),
        },
        {
            AttributeName: aws.String("Name"),
            KeyType:       aws.String("RANGE"),
        },
    },
    ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
        ReadCapacityUnits:  aws.Int64(1),
        WriteCapacityUnits: aws.Int64(1),
    },
    TableName: aws.String("User"),
    StreamSpecification: &dynamodb.StreamSpecification{
        StreamEnabled: aws.Bool(true),
        StreamViewType: aws.String("NEW_AND_OLD_IMAGES"),
    },
})

StreamSpecificationを用いてStreamsを有効にしたテーブルを作成します.

_, err = dynamo.PutItem(&dynamodb.PutItemInput{
    TableName: aws.String("User"),
    Item: map[string]*dynamodb.AttributeValue{
        "ID": {
            S: aws.String("ID-1"),
        },
        "Name": {
            S: aws.String("moznion"),
        },
    },
    ReturnConsumedCapacity: aws.String("none"),
})

作成したテーブルに対してitemをputしてみます.これでUser tableにレコードが入っているはず.

table, _ := dynamo.DescribeTable(&dynamodb.DescribeTableInput{
    TableName: aws.String("User"),
})
streamArn := table.Table.LatestStreamArn

stream, _ := dynamoStream.DescribeStream(&dynamodbstreams.DescribeStreamInput{
    StreamArn: streamArn,
})
shards := stream.StreamDescription.Shards

DescribeTableでtable情報を引いて,DynamoDB StreamsのstreamArnを取得します.そのstreamArnを利用してStreamのshardsを引っ張ってきます.

Shard:
	for _, shard := range shards {
		out, err := dynamoStream.GetShardIterator(&dynamodbstreams.GetShardIteratorInput{
			StreamArn:         streamArn,
			ShardId:           shard.ShardId,
			ShardIteratorType: aws.String(dynamodbstreams.ShardIteratorTypeTrimHorizon),
		})
		if err != nil {
			logrus.Error(err)
			continue
		}

		nextItr := out.ShardIterator
		for nextItr != nil {
			record, err := dynamoStream.GetRecords(&dynamodbstreams.GetRecordsInput{
				ShardIterator: nextItr,
			})
			if err != nil {
				continue Shard
			}

			records := record.Records

			// Do something

			nextItr = record.NextShardIterator
		}
	}

shardsを用いて各shardのShardIteratorを取得し,そのshard iteratorを使うことでstreamからレコードを取ってくることが可能となります.後は煮るなり焼くなりできるでしょう.
なおこのコードはあくまでサンプルなので,実際にはshards周りの処理は状況に応じて書き換える必要があるでしょう.

所感

軽く動作検証をした感想ですが,テストやDynamoDB Streamsの動作検証用途であれば十分利用できると思います.「一定期間以上古いレコードはトリミングされる」という挙動もエミュレートしていてすごい (その期間については未検証).
一方で冒頭にも書きましたが,DynamoDB LocalのDynamoSB Streamsはドキュメントが極端に少なく (というかDynamoDB Local自体の情報が少ない気がする……),また具体的にどのような仕組みで動いているのかがわからないという部分がネックと言えばネックとなりえると思います.が,まあDynamoDB Local自体がSQLiteの超テクノロジーで動いているわけだし,まあ……という気持ちではいます.
それにしてもDynamoDB LocalにStreams対応入っているの本当にすごい……

とにかく,DynamoDB LocalはDynamoDB Streamsに対応しているという話でした.

Ref:

docs.aws.amazon.com

*1:中身はSQLiteで,スーパーテクノロジーでエミュレートされている

手っ取り早くウェブアプリケーションにOAuth2認証を導入する

bitly/oauth2_proxyを用いて,ウェブアプリケーションに手っ取り早くOAuth2認証を導入するという話です.
oauth2_proxyは良い感じでOAuth2による認証を肩代わりしてくれる君で,何らかのリバースプロキシの認証機構と組み合わせて利用すると簡単にOAuth2ログインを実現することができます.
今回は例としてKibanaにGoogleのOAuth2ログインを導入してみたいと思います.

構成
+------+              +-------+               +--------------+            +--------+
|      |              |       | ----auth----> |              |            |        |
| user | --request--> | nginx |               | oauth2_proxy | <--auth--> | Google |
|      |              |       | <--response-- |              |            |        |
+------+              +-------+               +--------------+            +--------+
                          |
                       access
                          |
                          v
                      +--------+
                      |        |
                      | kibana |
                      |        |
                      +--------+
流れ
  1. UserがKibanaが動いているドメインで待ち構えているnginxにアクセスする
  2. nginxはoauth2_proxyに認証処理を移譲する
  3. oauth2_proxyはGoogleに認証リクエストを送り,ログインフローに乗せる
  4. oauth2_proxyは認証の結果をnginxに戻す
  5. 認証が成功した場合はKibanaへリクエストを通す.失敗した場合はリジェクトする
  6. (OAuth2認証が成功した場合はその情報をcookieに保存しておき,cookieがexpireするまではOAuth2認証リクエストをバイパスする)
事前準備

https://console.developers.google.comにアクセスしてcredentialsを作り,Client IDとClient secretを取得します.この時,Authorized redirect URIsにリダイレクト先のURLを登録しておきます (e.g. https://kibana.example.com/oauth2/callback).

実際に動くサンプル

docker-composeで実際に動くものを示します.

nginx.conf

gist.github.com

auth_requestディレクティブでOAuth2認証を有効にしています.あとの部分は読むとだいたいわかると思います.このnginx.confの内容については以下の記事が詳しいです (というかパクりました,ありがとうございます).

lamanotrama.hateblo.jp


docker-compose.yml

gist.github.com

<>で囲われた部分については適宜書き換える必要があります.client-id及びclient-secretにはあらかじめ作成しておいたcredentialの内容を入れます.

ここで利用されているhttps-portalはLet's Encryptを利用してSSL/TLS証明書の取得を自動化しつつSSL終端してくれるコンポーネントです.これ利用している理由はGoogleのOAuth2認証がリダイレクトしてくる時にhttpsを利用するからです.もしnginxの側でSSL/TLS化してる場合や,ELB等の上流でSSL終端している場合などは不要です.
なお,STAGE: 'local'と記述しておくとLet's Encryptは利用せずにオレオレ証明書を利用するモードになるのでローカルでの検証等で便利です.つまり本番で使ってはならない.本番運用時にはSTAGE: 'production'などと記述する必要があるでしょう (ドキュメントを参照のこと: https://github.com/SteveLTN/https-portal).


docker-copose upしてアクセスするとこんな感じで動きます (kibana.example.comをそのまま利用する場合はhosts等を適宜書き換えておく必要があります).

f:id:moznion:20171214230153p:plain

Googleのログインページが開き,適宜やっていくと

f:id:moznion:20171214230312p:plain

という感じでKibanaが開きます.便利便利.よかったですね.

AWS Elastic Beanstalkで初期環境構築時にhealth checkが延々通らないためにその一生を終えたくない時に読む

AWS Elastic Beanstalkは摩訶不思議な理由でhealth checkが通らなくなることがあります.
たとえばEBの環境を新規に構築する際に,とりあえず動作するかどうか確認するためにAWSが用意してくれているサンプルアプリをデプロイしようとするでしょう.しかしこれは時に謎の詰まり方をして動かないことがある.恐らくAWSのサンプルアプリが悪いわけではなく,サンプルアプリでなくとも動かない時は動かない.ままなりませんね.
この「環境の初期構築時」というのが鬼門で,初期構築時に詰まると手も足も出ない.「オペレーションの中止」で止めることもできないし,かといって「アプリケーションの削除」も「構築中の環境があるから削除できません」の一点張り,ならば設定を変えようにも一度立ち上がりきった状態でなければ設定の変更すらも許されない.そして数十分待たされるのである.そのように俺たちは年老い……死んでゆく.

というわけでこうです;

ELBを使っている時

ELBごと削除する.これしかない.
するとEBのデプロイメントが異常終了するので,環境をこちらのコントロール下におけるようになる.あとは環境を削除して作り直してみましょう.運が良ければ動く.

ELBを使っていない場合

100年待つ (よく知らないが多分シーケンスの最後まで待つ必要があるだろう……).


ガンバだよ!!

suコマンドでユーザを切り替える時に任意のコマンド実行してからシェルを立ち上げて欲しいんですけどって時

例えば踏み台サーバに個人のアカウントで入ってから,或るuserにsuで切り替わって色々する,みたいなシチュエーションがあると思います.
そんな時に,或るuserにsuする際にあらかじめ任意のコマンドを実行しておいてほしいという時がある.あるのです.

というわけでこうです;

$ su "$LOGIN_USER" -c "$ANY_COMMAND && $LAUNCH_SHELL"

suコマンドの-c (--command) を使うと,そのuserに切り替わってから任意のコマンドを実行することができます.それを利用して「事前実行したい任意のコマンド」を実行してから「実行したいシェル」を起動すると,あたかも事前実行コマンドが実行されてからログインしたように利用できて便利.

取りあえずこれで動く.ヨッシャヨッシャ.

定期的にtcpdumpをある期間だけ実行したいという時

tcpdumpの提供する-Wオプションと-Gオプション,ならびにcrontabを併用するといける.

tcpdump-Wオプションはログローテーションを行う回数で,-Gはそのローテーション期間を秒数で指定できる.
例えば

$ tcpdump -w ./%Y%m%d%H%M%S.pcap -W1 -G60

などとやると,ログローテーション1回,ローテーション期間は60秒となるので,つまり60秒tcpdumpを実行した後にexitする (ローテーション1回指定なので).
ちなみに-wオプションで指定するファイル書き出し先についてはstrptimeと同じフォーマットが利用できるのでこういう時に便利.

あとはcrontabでこのコマンドを仕込んでやるとOK (id:hirose31さんから「crontabでは%をエスケープする必要がある」との指摘があり修正しました).

55 * * * * tcpdump -w /var/pcap/\%Y\%m\%d\%H\%M\%S.pcap -W1 -G600

例えば上記のようなcrontabを書いてやると,毎時55分にtcpdumpを起動して,そこから10分間キャプチャを取る,ということができる.

シナリオとして「毎時0分付近のリクエストの失敗が多い」という問題の調査を行うときなんかに,こういったcronを仕込んでおくと,サクッと欲しい部分のパケットだけ解析できるようになるし,何より不要なパケットキャプチャでディスク容量を食わなくて良い.

訳あってcarton等を利用せずに,しかしきれいなperl環境を保ったままモジュールをインストールしたいんですけどって時

plenvやperlbrew等できれいなperl環境を利用している際に,Perl::Critic (例えばエディッタのsyntax checkingに引っ掛けているようなシチュエーション) やLなどの自分の環境でだけ動かすようなモジュールを環境グローバルに入れたくないという場合が生きているとあるでしょう.かと言って複数人で開発していてcpanfileをrepositoryで管理しているようなプロジェクトだとcarton/carmelでインストールするというのも微妙.そういう時にどうするか,と言う時の話です.

僕は雑な人間なので,そういったモジュールを利用したい時は普通にcpanm L​とかやってしまいがちなんですが (なんと前提が崩れた),真面目にやるときはdirenvを使ってPERL5OPTを設定するようにしています.

例えば

$ cpanm -l mydist L

という風に,-lオプションとともに任意のディレクトリの以下にモジュールをインストールしておいて,

export PERL5OPT="$PERL5OPT -I/path/to/mydist/lib/perl5"

みたいな感じで-Iオプションによってincludeするパスを指定したPERL5OPT.envrcに書き込んでやる.この時指定するパスは,モジュールをインストールした先のパスを指定する.
そうすると,.envrcが置かれているディレクトリ以下ではこのPERL5OPTが有効になるので,-l​オプションと共にインストールしたモジュールが実行時にincludeされ (PERL5OPT-Iに指定したディレクトリ以下が走査される),自動的に利用可能になります.
注意としては,PERL5OPTに書く-I​オプションに与えるパスはフルパスである必要があるということです.これが相対パスだとincludeするパスの位置の決定がperlを実行するディレクトリに依存してしまい正しく動作しなくなります.

これできれいな環境を保ちながら任意のモジュールを任意の場所にインストールしつつ,自分の環境でだけ有効にすることができる.やりましたね.

追記: PERL5LIBを使う方法

ここまでPERL5OPTを使う方法を書いたのですが,冷静に考えてみればPERL5LIBを使ったほうが役割的に正しいのでしっくり来ることに気づきました..envrcに以下のように書くと良い;

export PERL5LIB="/path/to/mydist/lib/perl5:$PERL5LIB"

注意としては,PATHなどと同じでPERL5LIB上では先に書いたものが優先されるということでしょうか.つまり先に書いたパスから評価されてゆき,そこに対象となるモジュールがあった場合はそれが利用されます.適宜,オリジナルの$PERL5LIBを置く位置に気をつけたほうが良いでしょう.

ElasticsearchあるいはKibanaのDockerイメージからX-Packを取り除く方法

ElasticsearchのX-Packはマジ便利なんですが,有償なので (だたしmonitor機能なんかは無償のBasicライセンスで使えて最高) 使わない人も多いでしょう.
そんな場合,Elasticsearch/KibanaをDockerで動かすという時にX-Packを取り除きたくなるというのが人情でしょう.コンテナは軽いと嬉しい.

というわけでそういう場合の法です.

Elasticsearch:

FROM docker.elastic.co/elasticsearch/elasticsearch:5.6.3
RUN bin/elasticsearch-plugin remove x-pack

Kibana:

FROM docker.elastic.co/kibana/kibana:5.6.3
RUN bin/kibana-plugin remove x-pack

プラグインマネジメント機能を介して,docker build時にゴリッとX-Packを消すという構造.

そんでもって,設定のyamlファイルに

xpack:
  monitoring.enabled: false
  security.enabled: false
  watcher.enabled: false
  ml.enabled: false
  graph.enabled: false
  reporting.enabled: false

などと書いてやるとX-Packを無効化した状態で起動することができる.このissueから情報を得ました: Unable to start Kibana 5.3.0 without X-Pack · Issue #27 · elastic/kibana-docker · GitHub

とは言えX-PackのBasicライセンスで利用できるmonitoring機能は普通に便利だったので,別にX-Pack消さんでも良いなというのが最近の感想です.

補足: Kibanaの場合はなんか特殊で,以下のようなyamlを書いてやる必要があった.

xpack:
  monitoring.enabled: false
  grokdebugger.enabled: false
  searchprofiler.enabled: false
  security.enabled: false
  watcher.enabled: false
  ml.enabled: false
  graph.enabled: false
  reporting.enabled: false
  tilemap.enabled: false
  upgrade.enabled: false
  xpack_main.enabled: false

なお現在はX-PackのBasicを有効にしているのでKibanaの設定は

  monitoring.enabled: true
  grokdebugger.enabled: true
  searchprofiler.enabled: true

というふうになっており (差分を心の目で見てください),Elasticsearchの設定は

  monitoring.enabled: true

となっています (差分を心の目で見てください).