読者です 読者をやめる 読者になる 読者になる

stop したり init スクリプトを削除したりすると apache2 cookbook なんかが仕込む delayed な reload アクションがこけて converge 全体が失敗する

また Chef のしょうもない話で恐縮なのですが、表題のようなことに悩んでおりました。

実はこれ、単に次の起動時に立ち上がってきて欲しくないという要求から始まったもので、それだけなら stop も削除もせず、立ち上げたまま chkconfig off でも service リソースで止めるでもしておくだけでいいわけですが、なんかこー…勝手に上がって欲しくないときって、あらかじめ明示的に止めたいじゃないですか、私の手で。特に docker なんかだと supervisord 使いたいじゃないですか。使わない(使って欲しくない)のが明らかなら upstart の config ファイルや init ファイルも消したい……はやり過ぎかもしれませんが、そういうときにどうするかっていうのを

delayed されたものをキャンセルしてえー

とか結構長いこと悩んで放置していたのですが、どこだったか誰かのレシピ見てて

あ…… web_app リソースの後に stop する delayed なアクションをセットするんだ……

という学びがあった次第です。

web_app 'oogatta' do
  docroot   /var/www/oogatta
  server_name    'oogatta'
end

bash 'cleanup_services' do
  code <<-EOH
    if [[ -e /etc/init.d/httpd ]]; then
      service httpd stop
      chkconfig httpd off
      pkill httpd
    fi
  EOH
  action :nothing
end

log 'set cleanup timer' do
  level    :info
  notifies :run, 'bash[cleanup_services]', :delayed
end

気がついてみると実に当たり前ですね。

EC2 の Amazon Linux を launch するとき authorized_keys に公開鍵が注入されるユーザを変更したい

変更したくないですか? 私はしたかった。いい加減 recipe 作り込んだし、 ec2-user じゃねえだろって思ってた。例えば oogatta とかにしたかった。でもわからなかった。

今日、 @ainame と話してたら「それは CloudInit です」と教わった。 CloudInit …!

その通りだった。 /etc/cloud/cloud.cfg.d/00_defaults.cfg を開くと

# A set of users which may be applied and/or used by various modules
# when a 'default' entry is found it will reference the 'default_user'
# from the distro configuration specified below
users:
  - default

と書いてあり、その通り system_info の中を見ると default_user がいた。

# System and/or distro specific settings
# (not accessible to handlers/transforms)
system_info:
  # This will affect which distro class gets used
  distro: amazon
  distro_short: amzn
  # Default user name + that default users groups (if added/used)
  default_user:
    name: ec2-user
    lock_passwd: true
    gecos: EC2 Default User
    groups: [ wheel ]
    sudo: [ "ALL=(ALL) NOPASSWD:ALL" ]
    shell: /bin/bash

めっちゃ ec2-user って書いてある……。

さっそくこの一歩前の recipe で

if platform?('amazon')
  bash 'switch default user' do
    code <<-EOH
  sed -i -e '{s/name: ec2-user/name: oogatta/;}' /etc/cloud/cloud.cfg.d/00_defaults.cfg
    EOH
  end
end

ぶち込んでやりましたら、望んでいたとおりになった……。 lock_passwd とか passwordless sudo とかそのままでいいのかってのはあとで調整として、これで packer とか test-kitchen とか ssh 接続をラップするツールを横断している時に(今はどのユーザとどの鍵で入れるんだっけ……?)といちいち認知負荷を強いられることがなくなり、今夜は健やかな夜を過ごせそうです。 @ainame サンタさん、ありがとう。

これでミッチェル先生が packer の ssh 鍵交換周りで本気出してない理由わかったす。

Rails の n.months って Date の加減演算の引数になると月ごとの日数考慮するんだなあ

知らなかったというか、それについて真面目に考えたことが無かった。

コード

class HogeTest < Test::Unit::TestCase
  def test_hoge
    Timecop.freeze Date.new(2014, 11, 1) do
      assert_equal Date.new(2014, 11, 1), Date.today
      assert_equal Date.new(2014, 12, 1), Date.today >> 1
      assert_equal Date.today + 1.months, Date.today >> 1
      assert_equal Date.today, Date.today + 1.months - 30.days
      assert_equal 30.days, 1.months
    end

    Timecop.travel Date.new(2014, 12, 1) do
      assert_equal Date.new(2014, 12, 1), Date.today
      assert_equal Date.new(2015, 1, 1),  Date.today >> 1
      assert_equal Date.today + 1.months, Date.today >> 1
      assert_equal Date.today, Date.today + 1.months - 31.days
      assert_equal 30.days, 1.months
    end

    assert_equal Date.new(2015, 1, 1),  Date.new(2015, 1, 1) + 2.months - 59.days
    assert_equal Date.new(2015, 2, 1),  Date.new(2015, 2, 1) + 3.months - 89.days
  end
end
$ RAILS_ENV=test rbbe rails r hoge.rb

ぽてっと 1.months って書いた場合はいつも固定で30日が来るんですが、加算の左辺が11月の場合30日になって、12月の場合31日になる。2015年の1-2月の2ヶ月だと59日で、2-4月の3ヶ月だと89日。

いつも定数的に使ってるけど、同じスコープ内で同じ表記して違う値が戻ってくるんだなあ。31日から1ヶ月後とやって次の月に31日が無い場合30日に戻るので、「次の月の同じ日に行く!(その日が無かったら有効なとこまで戻る!)」指向なのでありました。

ActiveSupport::Duration 同士の加減算では年月日の情報が保持されている

ちなみに 1.month + 1.days こんな感じに左辺としても使うと呼ばれるのは Duration#+ でこれはもちろん30日固定…なんだけど、この加算で生成された Durationインスタンスは内部的に「月が1で日が1です」って情報を抱えていて、これを Date に対して加算しようとするとやっぱり30日になったり31日になったり28日になったりする。

    duration = 1.months + 1.days
    assert_equal Date.new(2014,11,1) + duration, Date.new(2014,11,1) + 31.days
    assert_equal Date.new(2014,12,1) + duration, Date.new(2014,12,1) + 32.days

これは、自分アホなんで、間違いなく定数として使ってバグらすパターンですね。

実装

Date#+ はその実 Rails が拡張した Date#plus_with_duration で、これが最終的にネイティブの Date>> を使っておられる。

TimeDate はいかにも Ruby ネイティブの少女っぽいふりして Rails によってすっかり女に変えられているので、裏表あって怖い。

スタートアップ的何かについて考えていることのメモ

企業でもチームでもプロジェクトでも何と呼んでもいいけど人間が複数集まって新しく何かを始める時なんかに…

手法の前に人間なぜなら

どんな方面にも手法がいろいろ確立されているもので。たとえば Lean でもいいし、 UX だったら Empathy とか。ここで言う手法とは要するに抽象された、情報密度の高い、言葉。一言で多くのことを伝え合える。速くて、間違いが無い。さらに多くの副手法(副概念)を内包できる。

ただ、このように一言で多くのことを伝え合うには、一言で多くのことを伝え合える人間で場が占められていなければならない。その言葉を、集団でさらに抽象しようとする場合なんかは、99.99999%でもだめで、確実に100%でなければならない。

誰が教育するのか? スタートアップ的 something ではこの言葉のレベルの教育(トップダウンでも、相互教育でもいいけど)の余裕は無い。ことここに関しては、いきなり100%の要求から始まる。大規模な企業間プロジェクトのローンチみたいに全員缶詰の合宿に1ヶ月かけられればいいけど、ここはそうではない。

従って、スタートアップ的 something においては「任意に規定する必要な言葉の集合を理解している人間のみで組織すること」が、最も重要な要因となっているように見える。さらに言えば「プロジェクトの成功のためにメンバーが理解していなければならない任意の言葉の集合を定義すること」もそうだ。プログラミングでも、デザインでも、セールスフォースでも、なんでもいいけど、 harsh なことを言うと、クズが一人でもいるとスタートアップ的 something の成功率は著しく下がる。キャラも意見も多少違っていても「任意に規定する必要な言葉の集合」は留保無く共有できなければならないし、これできていれば自分たちなりの抽象をしていってプレタポルテな手法から抜け出せる。

よくどこかの成功手法を持ってきて既存の something に当てはめようとすることがあるけど、その手法が確立された北米だとかの現場が、この辺りの人間の面をかなり strict に実践してることが多くて、要するに、ゼロレイテンシで話し合える概念の用意が無いところにゼロ教育で新概念持ちこんできてもすり合わせだけで半年ですよね。ってのとか、実際に思う。

スタートアップ的 something における活動方針

  • 取ったら最初のロスとデタッチのコストが発生するので、できる限り取らないようにしよう。
  • 「一言で説明できたら本なんていらないじゃないですか」と容赦なく言おう。
  • なんとかしてデタッチする。「教えれば…」とか「いい人なんだけど…」を考えないようにしよう。
  • 逆に自分がデタッチされてもくよくよしない。共通概念セットはリーダーによって現場によって大きく異なるので、質の問題じゃ無いかもしれない(質の問題のこともあるだろうけど)。

自分は甘ちゃんなので、人間と人間が集まってやる行いが果たしてこれでいいのか? という気がずっとしてたんだけど、おそらく、これでビジネスが立ち上がってから、立ち上がってから社会の公器としての組織の役割を果たしたらいいんじゃねーのってことなんだと思う。企業というものの、フェーズ毎の社会的役割の分離も進んでいるのだ。

成功したら、パートのおばさんをたくさん雇用しよう。地元に工場を作ろう。あるいは法人税いっぱい払おう。

この論は理想状態の設定が目的。理想状態と現実的な次のゴールの両方が、記憶領域の浅いところにあるといいんだと思う。

IntelliJ ファミリーで OSX の Dvorak キーボードを使うと記号絡みのショートカットがおかしい

ずーっとこれ悩んでて、これのために OSX のキーボードは US にしつつ、 Karabiner で Dvorak に変換していました。でも気持ち悪いですよね。 Karabiner で配列をがらっと入れ替えると、再起動やスリープから復帰した後のログイン画面、 Karabiner 自体のアップデートをかけるときの OSX のパスワードダイアログなど「今 Karabiner が上がってるかどうか」が不可視なときに、自分、異常に混乱する時がありまして…。

Issue を何クリックかしたら書いてあった…。

Keymap shows characters for keys only for english keyboard layout correctly : IDEA-63779

/Applications/IntelliJ\ IDEA\ 14.app/Contents/bin/idea.vmoptions

を開いて、末尾にでも

-Dide.non.english.keyboard.layout.fix=true

を追加したら問題無くなりました。

Test-Kitchen で成果物の受け入れテストもやる…のはどうやるの

bats 好きなんですよ。なので Test-Kitchen でめっちょテスト書きます。なんとなくふわっとした複合的な要件は Serverspec で書きます。どちらにせよ、アプリじゃ無くてインフラなので、普段の25倍くらいは書きたいところです。

そんでいっぱしのレシピが書けて、クックブックが仕上がって、 git push して、そのあといろんなやり方で実際の環境、成果物をつくりますよね。自分の場合 docker のコンテナと IAM と Vagrant BOX にします。 VPS や自前のハードウェアに展開することが全く無いので、実際の論理マシンに直接 Chef を流すことが今はないのです。

なので、作るのは今 packer を gradle で動かして作ってます。

そこまではいいんですが、じゃあそこで例えば Vagrant の Box ができあがったとして、これもう一回、レシピ作ったときと同じテストにかけたいですよね…。

みんなどうやってるんだろう…。

小生、いまいち検索力が追いつかず、しばし考えた結果、 .kitchen.yml がたとえばこうあったとして

---
provisioner:
  name: chef_solo

platforms:
  - name: hoge
    driver_plugin: vagrant
    driver_config:
      box: oogatta/hoge
      provider: virtualbox
suites:
  - name: default
    run_list:
      - recipe[unko::default]
    attributes:

あったとして、これに

---
provisioner:
  name: chef_solo

platforms:
  - name: hoge
    driver_plugin: vagrant
    driver_config:
      box: oogatta/hoge
      provider: virtualbox
  - name: ukeire
    driver_plugin: vagrant
    driver_config:
      box: ukeire-box # localでaddしたテスト用
      provider: virtualbox

suites:
  - name: default
    run_list:
      - recipe[unko::default]
    attributes:
    excludes:
      - ukeire
  - name: ukeire
    run_list:
    attributes:
    excludes:
      - hoge

受け入れ用の、レシピのないスイートを加えてみることにしました。

当然これ、こうしただけではテスト走りません。

テストは test/integration の下にスイート名のディレクトリを探して実行されるのですが、だからって同じテストのファイルを2箇所に置いてもしようがないですからためしにリンクしてみたら、なんとそれでうまく走りました。

$ cd test/integration
$ ln -s default ukeire

しかし、スイートとレシピ名ごっちゃになってよく混乱しますよね。ここはレシピ名なんだっけ…スイート名なんだっけ…っていう。でもこれで

$ vagrant box add --name ukeire-box fugafuga.box
$ kitchen test ukeire

できます。

終わりです。

Vagrant のプラグインで使えるフックポイント

vagrant-aws と Route 53

Vagrant そんな途方も無くすごいかって言うとそういう話じゃないですけど、毎日便利に使わせていただいてます。これまでは VirtualBox か VMware 、遊びで docker だったんですが、こないだ諸事情あって仕事の開発環境をインターネット上に出したくなり、 vagrant-aws プラグインを使いました。そうしたら思っていたより、はるかに便利でした。

もう一個諸事情あって、そこで立ち上がった EC2 のインスタンスの Public IP が、 vagrant up をしたときに勝手に Route 53 の所定のレコードセットに入って欲しいです。ドメインとワイルドカード証明書をボスに買ってもらったので、これで https 開発です。

探したらなぜか無かったので、 vagrant-aws-route53 というのを書きました。

フックポイント

この「なんかのイベントの前や後」に実行するためのフックポイント、結構雑なんですがコード読め的な感じだったので読みました。

原理としては、プラグインのクラスの中で Vagrant::Plugin::V2::Plugin.action_hook に特定の文字列とブロックを渡してフックポイントを定義します。ブロックの引数に、そのフックポイントの Vagrant::Action::Hook インスタンスが渡ってくるので、 #before とか #after とか呼び出して、 "middleware" のスタックにおれさまのクラスを差し込みます。ちなみに "middleware" は #call メソッドに応答するクラスであればなんでもいいはず…です。多分。いや、これ、嘘でした。 #call#initialize にわたってくる application#call してあげて middleware スタックの実行を継続してあげないとだめです。下の場合は vagrant-aws プラグインの RunInstanceStartInstance のそれぞれの後にさしこんでます。

ちなみにフックの名前( :assign_ip_to_route53 ってところ)を渡してる引数、いまどこにも使われてないそうです。

action_hook :assign_ip_to_route53, :machine_action_up do |hook|
  require_relative './action/set_ip'
  hook.after VagrantPlugins::AWS::Action::RunInstance, VagrantPlugins::AwsRoute53::Action::SetIp
  hook.after VagrantPlugins::AWS::Action::StartInstance, VagrantPlugins::AwsRoute53::Action::SetIp
end

次にこれはフックを呼び出すですが、 Vagrant::Environment#hook を呼ぶとその名前で走るようです。フックポイント用のスタックを定義するのは呼び出す側では無くて、フックする方なんですね。

def hook(name, opts=nil)
  @logger.info("Running hook: #{name}")
  opts ||= {}
  opts[:callable] ||= Action::Builder.new
  opts[:runner] ||= action_runner
  opts[:action_name] = name
  opts[:env] = self
  opts.delete(:runner).run(opts.delete(:callable), opts)
end

ちなみに、これを使わず直接 Vagrant::Action::Runner#run してるところがあって、 Vagrant::Machine#action_raw です。

def action_raw(name, callable, extra_env=nil)
  # Run the action with the action runner on the environment
  env = {
    action_name: "machine_action_#{name}".to_sym,
    machine: self,
    machine_action: name,
    ui: @ui,
  }.merge(extra_env || {})
  @env.action_runner.run(callable, env)
end

つまるところこれは Provider のアクションが全部ここにきます。 up とか destory とか。なので上のように :machine_action_up が使えるわけでした。 Provider 次第ですが、この粒度の中でさらに上記のように middleware のスタックの任意の場所に差し込むので、ちょっとしたコマンド追加でやりたいようなことはできるだろということなんではないかと想像します。

ちなみに #hook でスタティックに定義されているのは、 :environment_plugins_loaded :environment_load :environment_unload がありました。