Thanks Driven Life

日々是感謝

「Serverspec で期待値を直書きしてる部分、Puppet の hiera から持ってこれるけどどうする?」

Puppet や Serverspec に限らず、Chef でも Itamae でも awspec でも自前スクリプトでも何でもいい話なんですが。 つまり テストコードに書く期待値を、構成管理ツールで使っているパラメータファイルから取ってきたほうがいいのか という話題。

結論から言うと「何をテストしたいのか」によって Yes/No が変わるんじゃないかな、と思います。

前置き: 例えばこんなこと

hiera でこう書いてたとして

---

packages:
  - emacs
  - vim

manifest でこう書いてたとすると

$packages = hiera('packages')
package { $packages:
  ensure => latest
}

Serverspec ではこう書くと思います

describe package('vim') do
  it { should be_installed }
end

describe package('emacs') do
  it { should be_installed }
end

ある日、こんな要望がありました

  1. PHP 追加したいから hiera に書いといて
  2. emacs 必要無くなったから hiera から消しといて
 packages:
-  - emacs
   - vim
+  - php-common

このとき、テストコードでは describe package('xxx') の部分を一つずつ直していくことになるでしょう。 数はまだ少ないですが、もしパッケージ数が5個とか10個とかになるとどうでしょうか。 また、パッケージだけならともかく、service だったり iptables だったり、その他細々としたものを書き換える度に

「よっしゃ、manifest 書いてデプロイ完了したぜっってテスト落ちてる…あーはいはい、テストコードの部分を更新し忘れてましたね…」

となることはないでしょうか*1。 その時にアナタはこう思うでしょうか。


「テストコードも hiera を参照してくれれば変更点1箇所で済むのに」


すると、アナタはこんな感じでがんばるかもしれません

hiera = Hiera.new(config: '/path/to/hiera.yaml')
hiera.config[:yaml][:datadir] = '/path/to/hiera/hiera_data'
packages = hiera.lookup('packages', nil, {}) # ['vim', 'php5-common']

packages.each do |pkg|
  describe package(pkg) do
    it { should be_installed }
  end
end

こうすることで

  1. パラメータの変更を hiera に反映する
  2. puppet apply
  3. rspec

という形で サーバの仕様変更におけるパラメータ編集箇所 が1箇所に集約できるメリットが生まれました

本題: これでいいのかどうか

これまでの流れからすると、おそらく Puppet でサーバを構築しつつ Serverspec でテストしているこの人は

さきほど修正した hiera が、ちゃんとサーバに apply されているかどうか

というのを一番チェックしたいと考えていると思います。

f:id:gongoZ:20160115162912p:plain

これ自体は特に問題ないと思っていて、ちゃんとテスト流してて素晴しいですよねみたいな未来です。


ですがこれはあくまで視点の一つで、おそらくもう一つ、下図のような見方をすることもあると考えています。

f:id:gongoZ:20160115162916p:plain

つまり直前にテストしたかどうか、とかは関係なく、純粋に 現時点でのサーバが期待した状態(つまり仕様)を保っているか を中心とするテストです。

この場合、逆に hiera の値をテストコードに使うのはよくなくて

  1. hiera で php を追加するつもりが ruby を追加してしまった
  2. 気づかずに apply
  3. rspec
  4. 本当は php がインストールされていてほしいが、hiera 上は ruby が正義なので PASS

ここもレビューをしっかりしていれば防げるかもしれません。しかし、往々にして人は見逃してしまうものなので、ありえない未来じゃないなと思っています。

まとめ

私が業務で触っている部分では

  • Puppet
  • Terraform

に対して

  • Serverspec
  • infrataster
  • awspec

のテストを用意していて、それぞれのディレクトリも似たような構成にしています。 そのため、hiera ファイルをそのまま使えちゃうので、どうしても「テストコードにも書くのめんどいわー」みたいなことが、無くも無いです。

最終的にはどちらが良い悪い、ということではなくて「何をテストしたいか」っていう当たり前のことを忘れないでいてくれれば10年後の8月また元気で出会えるんじゃないかなと思います。

*1:マニフェストとテストコードをしっかり同時にレビューしてると、こういうのは無さそうですが…

Turnip 2.0.2, turnip_formatter 0.4.0, gnawrnip 0.4.0 リリースしました

新機能追加したわけじゃないです

Turnip 2.0.2

これまで gherkin3 という名前で開発が進んでいた gherkin の version3 が gherkin に rename されました。

https://github.com/cucumber/gherkin/commit/d47add1a5e1a835af8c5dd9f55f7fe7a4916c7dd

これにより、例えば Turnip 2.0.1 の gemspec のように

s.add_runtime_dependency "gherkin", ">= 2.5"

こう書いていると gherkin2 と互換性のない gherkin3 がインストールされるようになった というわけです。 issue で教えてもらいました。感謝 https://github.com/jnicklas/turnip/issues/169

turnip_formatter, gnawrnip 0.4.0

turnip_formatter は Turnip 2.0.2 に、gnawrnipturnip_formatter 0.4.0 に それぞれ追従する形で gemspec 直したりテスト対象の RSpec のバージョン直したりしてました。 たぶん動くと思います。

Gherkin3 について

将来的には Gherkin3 への対応をしようかなと考えてたりします。

Use Gherkin3 · Issue #171 · jnicklas/turnip

いつになるかはわかりませんが。

2015年を振り返って

f:id:gongoZ:20151231190704p:plain

gongo.hatenablog.com

ふりかえり〜

OSS活動

2014年に Turnip のオーナー権を貰って から、ちょくちょく活動しています。 今年の作業としては、RSpec のバージョンを結構新しめ限定にするという Turnip 2.0.0 をリリースできたのが良かったです。

[Proposal] RSpec support policy · Issue #158 · jnicklas/turnip

社外でこのような提案する機会は今までなかったので、良い経験でした。

発表

今年は珍しく発表する機会がありました。

最初で最後だった YAPC Asia はやはり印象深いです。よく、あの規模の会場の壇上にあがって Perl にまったくカスらない内容を発表できたなーと。 発表のネタに繋がる作業は割とマジメに取り組んでいたんですが、結果的に笑い取れたので今年勝利したことにしておきます。

その他

久々に Advent Calendar に参加したり、ちょくちょく Vue.js 触ったり、ぐらいでしょうか。 2014年のふりかえり と見比べると、だいたい同じことしてるなという感想です。

まとめ

2016年もよろしくお願い致します。

Turnip 2.0.1 リリースしました

Release Version 2.0.1 · jnicklas/turnip

修正内容

たとえばこういう feature があったとして

Feature: Feature with background
  Background:
    Given there is a monster
  Scenario: simple scenario
    When I attack it
    Then it should die

RSpec の documentation format でテスト結果を表示した場合、Turnip 2.0.0 までは

Feature with background
  simple scenario
    When I attack it -> Then it should die

こういう形で Background のステップ名が表示されていなかった のですが、2.0.1 より

Feature with background
  simple scenario
    Given there is a monster -> When I attack it -> Then it should die

こんな感じで Background のステップ名も表示されるようになりました。

まとめ

メリークリスマス!!

Turnip 2.0.0 リリースされました

Release Version 2.0.0 · jnicklas/turnip

メジャーバージョンアップとなる 2.0.0 ですが、機能追加されたとか機能削除されたとかではなく

RSpec のサポートバージョンポリシー策定

の一環です。

経緯

github.com

以前は RSpec 2.14.x 系から RSpec 3.x の最新まで全てをサポートする体制でした。 定義されていなかった というのが正確なところですが。

ともかく近年の RSpec の躍進ぶりを追い続けるため、全てのバージョンをサポートしていくのは現状メンテナ一人の状態では無理と判断し、上記のような提案をさせていただきました。

提案の結果

反応としては「まあいいんじゃないのー」といった肯定的なものだったので、安心してばっさりいきました。つまり

  • 最新のバージョン、およびその一つ前のバージョンだけをサポートする
  • それ以前のバージョンはサポート対象外(動くかもしれない)

という形にしました。

最近の RSpec 3.x のリリースは 2,3 ヶ月に1回となっており、まあ約半年ぐらいあれば Turnip ユーザも手元の RSpec のバージョン上げられるだろ、ぐらいの気持ちで進めています。

そんなわけで

Turnip をこれからもよろしくお願い致します。

heroku-docker を使って Emacs & Cask がインストールされた Heroku 環境 (Slug) を作成する Docker イメージ作った

成果物

経緯

Emacs を使っている人は、日頃から

「あー Emacs でも HTTP サーバ立てられるんだし Heroku で起動してーなー」

と考えていると思います。

しかし Emacs ぐらいになると Heroku が標準サポートしている環境には含まれておらず、
いわゆるサードパーティ製 buildpack の導入が必要となります。

Emacs の buildpack もあった*1 のですが

  1. 試しに使ってみたら、なんかエラー出た*2
  2. まあ直せばいいかーと思う
  3. buildpack の修正検証ってものすごいめんどくさいイメージ(ちゃんと調べてない)
  4. モチベーション消えた

みたいな人生を送っていました。

Heroku + Docker

そんなある日、もう一つの可能性である Docker を思い出しました。

Build and Deploy with Docker | Heroku Dev Center

リリースされた当時は

「好きな Docker イメージを Heroku で動かせるのかよすごい!!」

みたいな反応が多数(私も)だったのですが、実際には

  1. 「Heroku と同等の環境を Docker イメージとして配布する」
  2. 「その中で動作確認しろよ」
  3. 「(制限はあるが)そのままデプロイできるフローも作ったぜ」

的なものでした。

デプロイ方法もあるし、ドカドカデバックできるので、これで行こうと決めました。

そんなわけで作りました。

https://github.com/gongo/emacs-heroku-docker

概要は README を読んでもらうとして、つまり必要なことは

  1. 普段通り Emacs Lisp で Web アプリケーションを書く
  2. Cask ファイルを作成し、依存パッケージを書く
  3. Heroku ではお馴染の Procfile に、起動コマンドを書く
  4. コンテナビルドしたり Heroku へリリースするためのファイルを作成
    • docker build したあとのファイルを転送するため、リリースにも使われる
  5. heroku docker:release

みたいな感じです。

試しに作ったのがこちら

https://emacs-heroku-docker-sample.herokuapp.com/

いつか消します

補足

heroku-docker でデプロイする時に注意するところ、備忘録も兼ねて

1. /app 以下のファイルしか転送されない

heroku docker:release で Heroku にデプロイされるファイルは、awesome氏のポストにもある通り

/app以下をtgzで固めてcpコマンドでそれを取り出している.なのでDockerfileに独自の変更を加えるときは注意が必要で/app以下に依存をちゃんと含めるように書く必要がある.

Herokuの'docker:release'の動き | SOTA

という制限(仕様)となっています。

つまり、Dockerfile で素直に apt-get install emacs とかやっても、Heroku 側には Emacs が入っていない slug が転送されてしまう、ということです。 なので今回はソースコードからコンパイルし、 /app/emacs 以下にインストールすることで依存を /app 以下に閉じ込めました。Cask についても同様。

2. 環境変数 export は /app/.profile.d/* 以下に書いておこう

上記のとおり heroku docker:release で転送されるのは、コンテナ内の /app 以下だけ。つまり

ENV PATH /app/emacs/bin:$PATH

みたいなのを書いていても、docker run した環境では使えますが Heroku 上には反映されません

ではどうするか。/app/.profile.d/ 以下に、環境変数設定するファイルを書いておきます。

.profile.d Scripts | Heroku Dev Center

$HOME/.profile.d/ 以下に置いたやつは Dyno 起動時に読まれるので、必要な処理はここに書いておきましょう。

ちなみに heroku-ruby では Dockerfile の ENTRYPOINT として init.sh というものを仕込んでいます。

#!/bin/bash

for SCRIPT in /app/.profile.d/*;
  do source $SCRIPT;
done

exec "$@"

こうすることで

Procfile:

web: cask exec emacs -Q --batch -l app.el

docker run 時:

$ docker run `コンテナ名` cask exec emacs -Q --batch -l app.el

つまり

  • Procfile に書くコマンドと docker run のコマンドを揃えることができる
  • どちらもコンテナ起動時に /app/.profile.d/* 以下を読んでくれる

という感じで、より Heroku の環境に近づけることができる、というわけです。

まとめ

heroku-docker を使うことで、buildpack よりも破壊再構築のサイクルを早く回せると思います。 どんどん作って最高のオレオレ cedar:14 にしよう!!

参考

*1:https://github.com/technomancy/heroku-buildpack-emacs

*2:libgpmが無いとかなんとか

json-reformat.el v0.0.4 リリースしました

Release 0.0.4 · gongo/json-reformat · GitHub

修正内容

v0.0.3 までは、空のハッシュに対して json-reformat-region とかを仕掛けると、
下記のように null になってしまう という 仕様 でした。

{"foo": {}}

// ↓↓↓

{
  "foo": null
}

v0.0.4 からは、ちゃんと空のハッシュのまま整形できるようになりました。

{"foo": {}}

// ↓↓↓

{
    "foo": {
    }
}

空のハッシュであれば "foo": { } という感じで改行入らない方がいいかもしれませんね。
いつか考えます。

今回の内容を仕様としていた理由

json-reformat.el では、JSON テキストのパースを (json-read) で行っています。

;; $ cat foo.json
;; {
;;     "foo": 3,
;;     "bar": "pizza"
;; }

(dolist (type '(plist alist hash-table))
  (let ((json-object-type type))
    (with-temp-buffer
      (insert-file-contents "foo.json")
      (json-read))))

;; => (:bar "pizza" :foo 3)
;; => ((bar . "pizza") (foo . 3))
;; => #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data ("foo" 3 "bar" "pizza" ...))

このように alist plist hash-table の中の好きな形でパースした結果を受け取れます。

v0.0.3 までは plist で実装していたのですが、この時問題となるのが、前述の 空のハッシュ です。

;; $ cat foo.json
;; {
;;     "foo": {},
;;     "bar": null
;; }

(let ((json-object-type 'plist))
  (with-temp-buffer
    (insert-file-contents "foo.json")
    (json-read)))

;; => (:bar nil :foo nil)

このように 空のハッシュも null もパースすると nil になってしまう ということで、
受け取った側としては「どっちかわからんから {} に直すこともできねーなー」となって
最終的に「これは仕様ですわー」みたいな感じにしていました。

光明

そんなことを Tweet してみたところ、 @ さんから以下の reply をいただきました。

なるほど hash-table と思って実際に試してみたところ

;; $ cat foo.json
;; {
;;     "foo": {}
;; }

;; $ cat bar.json
;; {
;;     "bar": null
;; }

(let ((json-object-type 'hash-table))
  (dolist (filename '("foo.json" "bar.json"))
    (with-temp-buffer
      (insert-file-contents filename)
      (json-read))))

;; => #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data ("foo" #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data ( ...)) ...))
;; => #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data ("bar" nil ...))

ちゃんと nil なのか空ハッシュなのか判別できる!!

というわけで、今までの仕様はバグということにできて修正することができました。 @ さんありがとうございました!!

P.S.

Emacs 24.4 から標準実装されている M-x json-pretty-print も似たような問題が起きていますが
M-x json-pretty-print または M-x json-pretty-print-buffer 実行前に

(setq json-object-type 'hash-table)

とかしておくと、空ハッシュが壊れることはありません。