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

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

訳あって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

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

Elastic BeanstalkでImmutable deployを試す

やり方についてはドキュメントを読むと良い.

docs.aws.amazon.com

option_settings:
  aws:elasticbeanstalk:command:
    DeploymentPolicy: Immutable
    HealthCheckSuccessThreshold: Ok
    IgnoreHealthCheck: false
    Timeout: "600"

設定ファイルでやるならばこういった設定を書き,.ebextensions/ 以下に deploy.config みたいな名前で放り込んでデプロイするとImmutable deployがされる.

最初は素朴なhealth checkベースのrolling deployを試していたのだけど,JavaのApp serverと相性が悪いのか (私はJavaを書いています) 上手くhealth checkが通らない時があり,また場合によってはJavaのプロセスが2個立ち上がったりしてしまってまともにデプロイできなかったので,ものは試しとImmutable deployを試してみたところ上手く行った.

基礎的な動作・挙動についてはドキュメントを参照してもらうとして,immutable deployの利点は

  • 新しいインスタンスが立ち上がって,その上にデプロイされるので環境がデプロイごとにクリーン
  • 既存インスタンスに対する変更が行われるわけではないのでデプロイ中も安定する

というものが挙げられる.これはディスポーザブルな構成が持つ良い特徴そのもののように思う.
一方で懸念すべき点としては (公式ドキュメントにもあるが)

  • デプロイ完了までにかかる時間がかなり増える
  • デプロイ中一時的にインスタンス数が2倍になる
    • オンデマンド制限に引っかかる可能性がある
    • もちろんお金がかかる

と言うものがある.
前者については「一台だけ新しいインスタンスを立ててデプロイする」->「health checkが通るかどうか確認する」->「health checkが通るのであれば残りのインスタンスも立ててデプロイする」->「health checkが通るかどうか確認する」->「health checkが通るのであれば新しいインスタンス群をauto scaling groupに突っ込む」->「古いインスタンス群をauto scaling groupからterminateする」という手順を踏むのでそれは時間が長くなるだろうなという感じ.急ぎのデプロイの時は中々ヒリヒリしそうなので,場合によってはhealth checkをスキップしたり,別のデプロイ手法を試したりする必要がありそうだと思った.
後者についてはもうそういうもんだと割り切るしかあるまい……2倍の数のインスタンスが必要となるのはデプロイ時だけなので寛容な心が必要であろう (オンデマンド制限については考えなければならないだろうが).

とはいえEC2も秒単位の時間課金になったので,かつてよりはImmutable deployもやりやすくなったんじゃないでしょうか.良い時代になりましたね.
それにしても日本語化されたドキュメント上で「Immutable」を「変更不能」と訳すの,気持ちはわかるんだがそれはどうなんだ……

相手のサーバにHTTP(S)で接続できるかどうかを確認するときにリトライしながらやりたいんですけどって時

例えばCIでテストのためのストレージのコンテナを上げる際,そのストレージが上がりきるまで待たなければテストには使えないわけですが「上がりきるまで何秒待てばよいのか」というのは一概に決められるものではありません.
そうした際,実際にストレージに対してHTTP(S)のリクエストを投げてみて,繋がったら「上がりきった」とみなして処理を次に進めるというようなことがよく書かれます.そういった要件を満たす為には接続に失敗したらリトライをしつつ一定回数試行するという処理を書かなくてはなりません.
つまりはそういう話です.

wgetの場合

  • retry-connrefused
  • tries
  • waitretry

を利用すると出来る.
retry-connrefused が無いと connection refusedに対応できずにリトライが不可能となるので,今回のような要件を満足するためには付ける必要があります.

e.g.

$ wget --tries 10 --waitretry 10 --retry-connrefused localhost:5000

この例だと

  • 最大10回リトライ
  • 1回失敗するごとに1秒ずつリトライ間隔が伸びていきながらリトライ (つまりn回失敗したあとのwaitはn秒)

というような動きをします. tries には最大リトライ回数を指定します.
waitretry は失敗するごとに1秒ずつリトライ間隔を伸ばしてゆく際の上限値となります (例えば,値が1だと常に1秒間隔をあけるようになる.0だと一切待たない).
場合によっては timeout を設定する必要があるでしょう.

なおこのコマンドはTravis CIのドキュメントにも書かれている由緒正しいコマンドから拝借しました.

curlの場合

  • retry-connrefused
  • retry
  • retry-delay
  • retry-max-time

を利用するとできる.
retry-connrefused が必要な理由はwgetのそれと同様です.なお,retry-connrefused2016-12-21にリリースされたバージョン7.52.0から入ったわりと新しい機能なので古いcurlだと使えません.新しいものにアップデートしましょう.

e.g.

$ curl --retry 10 --retry-delay 1 --retry-connrefused localhost:5000

この例では最大10回リトライしつつ (retry で指定),リトライ間隔は常に1秒 (retry-delay で指定) というふうになります.

retry 単体で利用すると失敗する度にretry間隔が倍々になり (つまり失敗回数をnとすると2^(n-1)秒待つ),
retryretry-delay を組み合わせると固定長秒数のretry間隔を設定できます.
また retryretry-max-time を組み合わせると,retry間隔は倍々で増えてゆき,そのリトライ間隔が retry-max-time を超えるとギブアップする (retry で指定された回数に満たなくても) という挙動をします.

どちらを使ったら良いのか

お好みで.双方微妙にリトライ間隔の取扱いが異なるので用途にマッチしたほうを使えば良いと思います.
とは言えcurlは環境によって古い (そしてアップグレードできない) ということがあるのでwgetを使っておけば安心なんでしょうか.

Groovyで動的にメソッドを生やす

インスタンスにメソッドを生やす

class EmptyCat {}
def cat = new EmptyCat()
cat.metaClass.nyan = { return 'meow' }
cat.nyan() // => 'meow'

インスタンスmetaClass からMetaClass (HandleMetaClass) を引っ張ってきて,それ経由で生やしたいメソッドの名前を指定し,Closureを代入してやると良い.

インスタンスメソッドとして生やす

上記がインスタンス自体にメソッドを生やしていたのに対し,こちらはクラスに対してメソッド追加の処理を行い,そのクラスがinstantiateされた際にそのメソッドを利用可能にするという方法.

class EmptyHuman {}
EmptyHuman.metaClass.greet = { return '無……' }
def human = new EmptyHuman()
human.greet() // => '無……'

クラスの metaClass からMetaClass (ExpandoMetaClass) を引っ張ってきて,それ経由で生やしたいメソッドの名前を指定し,Closureを代入してやると良い.

クラスメソッドとして生やす

class EmptyDog {}
EmptyDog.metaClass.static.wan = { return 'bow' }
EmptyDog.wan() // => 'bow'

クラスの metaClass.static からMetaClass (ExpandoMetaClass) を引っ張ってきて,それ経由で生やしたいメソッドの名前を指定し,Closureを代入してやると良い.

所感

Groovyらしい機能という感じ.finalなインスタンスであろうとfinalなクラスであろうとこのような機能を実現することができ,脱出ハッチが用意されているという謎の安心感はある.
こうした広義のメタプログラミングが簡単にできるのは便利である一方で,使う場所を誤るあるいは多用しすぎると途端にコードが魔界と化す予感がある (これはメタプログラミング全般に言えることです).節度を持った利用を心がける必要がありそうだが,しかしこういう処理を容易に書けるように言語側がアフォードするのは中々男らしい,という感想です.

CircleCI 2.0でElasticsearchを起動しつつテストする

例えばElasticsearchを使ったプロジェクトがあったとして,それをCircleCIで継続的にテストしたいとする.CircleCI 2.0はコンテナベースのCI環境なので,そのプロジェクトが採用している言語のコンテナの上でElasticsearchを動かす必要がある.どうするか.

circleci.com

CircleCIのドキュメントに記されている通り,docker imageは複数起動することができる.一番目に指定したdocker imageはprimary containerとして取り扱われ,stepsに書く手続きは全てそのコンテナ上で実行される.それ以降にdocker imageを指定した場合はそのimageがprimary containerと共通のネットワーク上で立ち上がり,そのコンテナ上で公開されているポートについてはprimary containerのlocalhost経由でアクセスすることが可能となる.これで手っ取り早く依存しているストレージをdockerで立ててテストすることができる (今回はたまたまElasticsearchの話題だったが,これはRedisでもmemcachedでも,docker imageがあるものであればなんでも応用が効く).

つまり,例えばjavaのプロジェクト上でElasticsearchのテストを行いたい場合は以下のようなconfig.ymlを書くと良い;

# https://circleci.com/docs/2.0/language-java/
version: 2
jobs:
  build:
    docker:
      - image: circleci/openjdk:8-jdk
      - image: docker.elastic.co/elasticsearch/elasticsearch:5.6.1

    working_directory: ~/awesome-project

    steps:
      # do something!

この場合,JDK8のコンテナがprimary containerとなり,Elasticsearchはsecondary container (という言い方が正しいのかどうかはわからないが) として立ち上がる.

さて,Elasticsearchのデーモンの設定を手っ取り早くいじりたいんだけど (要はdocker run -e'xxx=yyy'みたいな感じで制御したいんだけど) みたいな時にどうすれば良いのかというと

circleci.com

にあるように,environmentを設定すると良い.
例えば,

      - image: docker.elastic.co/elasticsearch/elasticsearch:5.6.1
        environment:
          http.host: '0.0.0.0'
          http.port: 9200
          xpack.security.enabled: false

というふうにしてやると,localhost:9200をlistenしつつ,xpackのセキュリティ設定 (要はBASIC認証) をバイパスしてElasticsearchを立ち上げることが可能となる.

ところで.CircleCIの弱いインスタンスを使っているとElasticsearchがOOMしてずっこけるという事態に出くわす.出くわすのです.僕は出くわした.そういう時にどうすればいいかというと,

  • CircleCIを良いプランにする (本質的な改善!!!)
  • Elasticsearchのヒープサイズを低くする

という方法が考えられる.色々な理由から前者が難しい場合,後者に頼るほか無いのでこうしてやるしかない;

      - image: docker.elastic.co/elasticsearch/elasticsearch:5.6.1
        environment:
          ES_JAVA_OPTS: '-Xms256m -Xmx256m'

プロダクションでは考えがたい蛮行だがまあテストだし……というわけで動く.
これでテストがハチャメチャに遅くなって支障をきたすようであれば調整する必要があるだろう……ガンバだよ!!!

SSL/TLS化しているサイトにリクエストを投げたら証明書の検証にしくじっているという時

表題のような状況のトラブルシュートについて記します.

moznion.hatenablog.com

というかこれの続きです.怪奇現象など存在しない.

背景

  • ブラウザとかcurlとかからはリクエストが通るのにJavaからは通らない
  • Let's encryptの証明書を使っている
    • Let's encryptでSSL/TLS化された他のサイトにはリクエストが通る
  • 当該サーバにSSHする権利がない (あとで出来るようになった)

検討したこと

  • 認証局が違うのではないか
  • Cipherがおかしいのではないか (鍵長が短いとか)
    • 証明書側
    • リバースプロキシ側
    • Java*1
  • 中間証明書がなんかおかしいのではないか

調査方法

とりあえずcipherは調べたけど特におかしいところは見当たりませんでした.
認証局についてはブラウザで確認してみたら双方同じRoot CA,中間CAで,それぞれfingerprintも同一だし問題なかろういう結論に.
というわけで中間証明書周りを疑いはじめる (というかid:hdkshjmさんとid:uzullaさんから示唆を受ける).しからば検証.

$ openssl version
OpenSSL 1.0.2l  25 May 2017
$ openssl s_client -connect $TARGET_HOST:443 -showcerts < /dev/null

とすると末尾に

Verify return code: 21 (unable to verify the first certificate)

というのが出てくる.証明書の検証にしくじっているのはまず間違いなさそう.
では認証チェーンのチェックをしてみましょう (一部ドメイン情報に編集を入れています).

$ openssl s_client  -connect moznion.net:443 < /dev/null | grep "Certificate chain" -A 10
Certificate chain
 0 s:/CN=moznion.net
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---
(以下略)
$ openssl s_client -connect $TARGET_HOST:443 < /dev/null | grep "Certificate chain" -A 10
Certificate chain
 0 s:/CN=<host>
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
(以下略)

正常に動いている https://moznion.net と比較すると中間証明書がすっぽ抜けている事がわかります.

こうなるともはや何も難しいことはありません.nginxの ssl_certificateサーバ証明書だけが突っ込まれており,中間証明書が結合されていないという典型的なやつでした.
というわけでLet's encryptが提供してくれる fullchain.pemssl_certificate に食わせてやると果たして正しく動作しましたとさ.めでたしめでたし.

教訓

  • おちついてトラブルシューティングをする.ちゃんとやればわかる.
  • opensslコマンドをちゃんと知っておく.
  • サーバをセットアップした人にちゃんとヒアリングすると楽.

その他

curlやブラウザには当該中間証明書 (すっぽ抜けてたやつ) がバンドルされているからリクエストが通った?
Javaにはそれがなかった? あるいはJavaはチェーンの検証が厳密?