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

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

AWS Lambdaでnodeを動かす時にnode_modulesをどうするか

向かうべき道は色々考えられるが,実際に試してみたのは以下.

1. node_modulesをzipに含めてアップロードする
2. browserifyを使って1つのjsファイルにバンドルする

node_modulesをzipに含めてアップロードする

ローカルでnpm install (or yarn install) してこさえたnode_modulesディレクトリをzipにアーカイブしてそれをLambdaにアップロードするという方法.
zipファイルを作るのは地味に面倒に思えますが,Apexを使うとかなり楽にzipファイルの作成とアップロードができます.しかし得てしてzipファイルの容量が大きくなりがちなのでS3経由でデプロイするなどの方法を採る必要があるかもしれません.
また,ネイティブ拡張を利用しているライブラリが含まれている場合だとLambdaの実行環境 (アーキテクチャ) と同一の環境上で npm insall してnode_modulesを作る必要があるでしょう.
可能な限りnode_modulesの容量を小さくするために npm/yarn install --production でdevDependenciesを外すというのも良い考えです.

pros
  • 手っ取り早い・わかりやすい
cons

browserifyを使って1つのjsファイルにバンドルする

ようこそ地獄へ.
node_modulesをアップロードする方法は容量が大きく,スマートではない……おれはもっと格好良い,小洒落た方法でやりたいんだ……ということでbrowserifyを使って1つのjsファイルに依存をバンドルし,それをアップロードするという方法が思いつくでしょう.

とりあえずLambdaで動くbundled jsを構築するには以下のようにコマンドを実行してやる必要があります.

$ browserify --node --standalone 'your-app-name' index.js -o bundle.js

--node--standalone 'your-app-name' を指定して実行すると,運が良ければ動くjsが生成されます.運が悪いと動かない.動かない時はどうするか? おそらくおとなしく諦めたほうが良いでしょう……人生は短い…… (不毛すぎる)
もちろんネイティブ拡張を利用しているライブラリが含まれている場合だと動かないので,そういうときはnode_modulesをzipにバンドルする手法を採用する必要があります.
あるいはbrowserifyの代わりにwebpackを使うという方法も考えられる.僕はやっていません.

pros
  • node_modulesをバンドルする方法と比較して容量が小さくなる
  • おしゃれ
cons
  • ハマりがち
  • ネイティブ拡張を利用していると使えない

結論

おとなしくnode_modulesをバンドルする方法のほうが楽なのではないか.
あるいは外部モジュールに依存せずにLambda Functionを書くという強い気持ちを持つ (つらい).

EOLなUbuntuを使い続けるとどうなるのか

TL;DR

apt-get関連のコマンドが死ぬ.

例: Ubuntu 16.10の場合

Release end of life | Ubuntu

f:id:moznion:20180126161950p:plain

この図からもわかるようにUbuntu 16.10は現時点でEOLです.使ってはいけません.
しかし生きているとうっかりEOLなバージョンが残っていることもあるでしょう.あったのです……

で,EOLを迎えているUbuntuを使い続けるとどうなるか.特筆すべきは apt-get update が死ぬという点でしょう.

$ sudo apt-get update
...
Err:6 http://security.ubuntu.com/ubuntu yakkety-security/universe Sources
  404  Not Found [IP: 91.189.88.152 80]
...
Err:14 http://archive.ubuntu.com/ubuntu yakkety/universe Sources
  404  Not Found [IP: 91.189.88.149 80]
...
Err:27 http://archive.ubuntu.com/ubuntu yakkety-updates/universe Sources
  404  Not Found [IP: 91.189.88.149 80]
...
Err:37 http://archive.ubuntu.com/ubuntu yakkety-backports/multiverse amd64 Packages
  404  Not Found [IP: 91.189.88.149 80]
...
Reading package lists...
W: The repository 'http://security.ubuntu.com/ubuntu yakkety-security Release' does not have a Release file.
W: The repository 'http://archive.ubuntu.com/ubuntu yakkety Release' does not have a Release file.
W: The repository 'http://archive.ubuntu.com/ubuntu yakkety-updates Release' does not have a Release file.
W: The repository 'http://archive.ubuntu.com/ubuntu yakkety-backports Release' does not have a Release file.
E: Failed to fetch http://security.ubuntu.com/ubuntu/dists/yakkety-security/universe/source/Sources  404  Not Found [IP: 91.189.88.152 80]
E: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/yakkety/universe/source/Sources  404  Not Found [IP: 91.189.88.149 80]
E: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/yakkety-updates/universe/source/Sources  404  Not Found [IP: 91.189.88.149 80]
E: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/yakkety-backports/multiverse/binary-amd64/Packages  404  Not Found [IP: 91.189.88.149 80]
E: Some index files failed to download. They have been ignored, or old ones used instead.
$ echo $?
100

ワーオ,repositoryが404になっていますね.それが起因してコマンドが100でexitしています.

curlを打ってみましょう.

$ curl -I http://security.ubuntu.com/ubuntu/yakkety-security/universe
HTTP/1.1 404 Not Found

無慈悲にdistのパッケージがremoveされてる!! マジかよ.

解決するには
  • Ubuntuのバージョンを上げる
小話

インスタンス起動時にapt-get updateを走らせるような設定にしていたため,毎回起動時に失敗してインスタンスごと死んでしまい,そしてそれを埋めるために更に新しいインスタンスが自動で立ちあがって死に……という無間地獄が行われていてすごい状況だった.

結論

apt-get系のコマンドがしくじるだけでなく様々な悪いことが起きる.
EOLなUbuntuを使うのをやめよ.

GitHub releasesのフィードを購読する

生きているとOSSのライブラリを使ったり,OSSのソフトウェアを使用することになるでしょう.
そうなってくると内部実装や変更点を逐一知りたくなるというのが人情というものです.
GitHubでコードが公開されているのであれば,「リポジトリwatchする」というのは有効な方法に思えますが,しかしwatchしているリポジトリが増えてくると現状のGitHubのタイムラインは即座に崩壊し,容易に取りこぼしが生じてしまうでしょう.これはGitHubのタイムラインの問題の一つだと思っていて,なんとかなって欲しい点はあります (例えばタイムラインを分割できるとか……).

というわけでどうするか.もちろんAtomフィードです.

https://github.com/{author}/{repos}/releases.atom

と指定してやると当該repositoryのGitHub releasesをAtomフィードで取得することが可能となります.これで(必ずreleaseを切るプロジェクトであれば)変更をトラックできるようになって便利です.場合によっては https://github.com/{author}/{repos}/tags.atom を利用するのでも良いかもしれませんね.

というわけでreleasesフィードをSlackのRSSリーダーAppを介してチャンネルに流すようにしたので,関係するメンバーに周知できるようになって便利になりました.良かった良かった.

PerlのYAMLライブラリ性能比較

なんと2018年の記事です.皆様無事明けられておりますでしょうか.

さてYAML::XSには2017年に色々と変更が入り,実用するにあたり非常に便利な機能が色々と導入されました (具体的に言うと,$YAML::XS::LoadBlessed$YAML::XS::Booleanです).また安定化が図られました *1
というわけで個人的に,最近PerlYAMLをserialize/deserializeするにあたってはYAML::XSを使うことが多くなってきたわけですが,そこでふと各YAMLライブラリの性能について気になったのでベンチマークを取ってみたという次第です.以下はその記録です.

追記

とのことでしたので,YAML::PPについても記載しました.

ベンチマークコード
#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use Benchmark qw(cmpthese);
use YAML ();
use YAML::Tiny ();
use YAML::Syck ();
use YAML::XS ();
use YAML::PP::Loader;
use YAML::PP::Dumper;

my $yaml_text = do { local $/; <DATA> };

my $yaml_pp_loader = YAML::PP::Loader->new;

# Deserialize
cmpthese(10000, {
    'YAML' => sub {
        YAML::Load($yaml_text);
    },
    'YAML::Tiny' => sub {
        YAML::Tiny::Load($yaml_text);
    },
    'YAML::Syck' => sub {
        YAML::Syck::Load($yaml_text);
    },
    'YAML::XS' => sub {
        YAML::XS::Load($yaml_text);
    },
    'YAML::PP' => sub {
        $yaml_pp_loader->load_string($yaml_text);
    },
});

my $hashref = YAML::XS::Load($yaml_text);

my $yaml_pp_dumper = YAML::PP::Dumper->new;

# Serialize
cmpthese(10000, {
    'YAML' => sub {
        YAML::Dump($hashref);
    },
    'YAML::Tiny' => sub {
        YAML::Tiny::Dump($hashref);
    },
    'YAML::Syck' => sub {
        YAML::Syck::Dump($hashref);
    },
    'YAML::XS' => sub {
        YAML::XS::Dump($hashref);
    },
    'YAML::PP' => sub {
        $yaml_pp_dumper->dump_string($hashref);
    },
});

__DATA__
# From: https://github.com/kubernetes/kubernetes/blob/7bbab6234f99af9adb700ff30968794084b6ee12/examples/mysql-wordpress-pd/wordpress-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
        - image: wordpress:4.8.0-apache
          name: wordpress
          env:
            - name: WORDPRESS_DB_HOST
              value: wordpress-mysql
            - name: WORDPRESS_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-pass
                  key: password.txt
          ports:
            - containerPort: 80
              name: wordpress
          volumeMounts:
            - name: wordpress-persistent-storage
              mountPath: /var/www/html
          volumes:
            - name: wordpress-persistent-storage
              persistentVolumeClaim:
                claimName: wp-pv-claim
Deserialize結果
              Rate   YAML::PP       YAML YAML::Tiny YAML::Syck   YAML::XS
YAML::PP     251/s         --       -29%       -86%       -97%       -98%
YAML         351/s        40%         --       -80%       -96%       -97%
YAML::Tiny  1767/s       604%       403%         --       -81%       -87%
YAML::Syck  9524/s      3696%      2613%       439%         --       -30%
YAML::XS   13699/s      5360%      3803%       675%        44%         --
Serialize結果
              Rate       YAML   YAML::PP YAML::Tiny   YAML::XS YAML::Syck
YAML         588/s         --       -74%       -84%       -96%       -97%
YAML::PP    2299/s       291%         --       -38%       -84%       -87%
YAML::Tiny  3731/s       535%        62%         --       -74%       -79%
YAML::XS   14286/s      2330%       521%       283%         --       -19%
YAML::Syck 17544/s      2884%       663%       370%        23%         --

なるほどという結果です.
DeserializeはYAML::XSが最も速く,SerializeはYAML::Syckが高速という結果になりました.
もちろん対象とするYAMLのデータ構造に依存する結果ではありますが,性能を考えるのであればYAML::XSかYAML::Syckを使っておけば良さそうという感じですね.


それぞれ,

  • YAML: Pure Perl実装.これでしか解釈できない記法が(なぜか!)ある.
  • YAML::Tiny: Pure Perl実装.PPなのでシンプル.
  • YAML::Syck: libsyckのバインディング.XSで比較的高速.*2
  • YAML::XS: libyamlのバインディング.XSで比較的高速.libyamlという安心感がある.現代では安定している.
  • YAML::PP: Pure Perl実装.YAML 1.2をサポートするというモチベーションらしい.

という特徴があるので (そして各々のライブラリで解釈できる構文が異なる場合があり,これは業界ではヤムルの地獄と呼ばれています),状況に応じて使い分ける必要はありそうです.こちらからは以上です.



*1:昔はよく動かなくなっていた印象がある

*2:しかしlibsyckってどこで開発してるんですか?

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年待つ (よく知らないが多分シーケンスの最後まで待つ必要があるだろう……).


ガンバだよ!!