MoneyForward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

dev

マネーフォワードのItamae Tips

こんにちは。 インフラエンジニアの村上です。

マネーフォワードでは、プロビジョニングツールに Itamae を活用しています。

本稿では「Itamaeってこんなこと出来るんだよ」を幾つか紹介します。  

レシピの依存管理をする

レシピはミドルウェア単位等で作ると思いますが、依存関係があるケースありますよね。

Itamaeは、依存管理は include_recipe を使う事になります。 では、「依存先のバージョンアップがあった時に、自分自身も再度インストールしなおしたい」などの場合はどうするのでしょうか。

subscribes という機能がこれらをサポートしてくれます。

サンプルコードは以下になります。 NginxのレシピにOpenSSLやGlibが依存しています。

require File.expand_path('cookbooks/itamae_helper.rb')
APP_NAME="nginx"
APP_VER="1.10.1"

HEADERS_MORE_NGINX_MODULE_VER="0.30"
NGINX_GOODIES_NGINX_STICKY_MODULE_NG_VER="1.2.6"
OPENSSL_VER="1.0.2i"
ZLIB_VER="1.2.8"

include_recipe "../base/base.rb"
include_recipe "./modules/headers-more-nginx-module-#{HEADERS_MORE_NGINX_MODULE_VER}.rb"
include_recipe "./modules/nginx-goodies-nginx-sticky-module-ng-#{NGINX_GOODIES_NGINX_STICKY_MODULE_NG_VER}.rb"
include_recipe "../openssl/openssl-#{OPENSSL_VER}.rb"
include_recipe "../zlib/zlib-#{ZLIB_VER}.rb"

# 依存ライブラリに変更があったら自分自身を一度消して作り直す
%w(openssl zlib).each do |dependency|
  execute "remove self #{APP_NAME}" do
    action :nothing
    command "rm -rf #{version_dir}"
    subscribes :run, "execute[#{dependency} install]", :delay
    only_if "test -e #{version_dir}"
  end
end

# セットアップ
execute "#{APP_NAME} download file" do
  cwd "#{node.build_base_dir}"
  command "wget http://nginx.org/download/#{image_file}"
  not_if "test -e #{version_dir}"
end

## 以後、nginxのセットアップ手順

include_recipeによってレシピの順序付けが可能です。

さらに、 subscribes :run, "execute[#{dependency} install]", :delay を指定する事で、 OpenSSL の Installタスクが呼ばれたら、 command "rm -rf #{version_dir}" をするという実装が出来ます。

※ このレシピでは not_if "test -e #{version_dir}" の時にセットアップが動く様になっているので、rm -rfする事で後続はSkipされずにセットアップが実行されます。

action :nothing をつけるのがコツです。 action :runだと subscribes されてもされてなくてもこのレシピに到達した時点でコードが実行されてしまいます。 Actionnotingだけど、subscribesがきたら、commandが実行される。という挙動にします。

もう一つのコツは、 subscribesのパラメーターは全レシピで一意にしておく必要があります。 私の場合は必ず #{APP_NAME} を含めています。

これによってOpenSSLのバージョンアップが行われたら 「Nginx自身のモジュールを消して、再度セットアップ手順を流す」 事が可能になります。  

ServerSpecはレシピと対になるSpecファイルだけ実行する

Itamaeを書いてるということはServerSpecも書いてますよね。 Roleが増えた時はSpecの管理は、みなさんはどうしてますか?

SpecにもRoleみたいなファイルを作って、対象となるSpecを書いている? Specは全部流れるんだけど、Specファイルの中で条件分岐が書いてあってスキップ処理が動く?

私の場合は以下のようにして、Specファイルを自動で特定出来る様にしました。

前提として * レシピファイルとSpecファイルはネーミングルールを揃えています。 * 例えば nginx-10.2.10.rb -> nginx-10.2.10_spec.rb * ホスト名からRole名が特定出来ます。 * roles/web.rb みたいなレシピが存在しています。

サンプルコード

require 'rake'

task :default => :serverspec

task :serverspec do
  hostname = `hostname`
  environment = ENV['ENVIRONMENT'] # prodとかstgとかが入ります

  if ENV.has_key?('ROLE')
    role_name = ENV.fetch('ROLE')
  else
    role_name = hostname.strip.sub(/\d{2}$/, "").sub(/^#{environment}-/, "") #role_nameを見つける
  end

  load File.expand_path('lib/rspec/itamaeutil.rb')
  targets = []

  # ItamaeのRoleRecipeにぶら下がる全てのレシピファイルの名前を取得し
  # それにマッピングされるSpecファイルを探してテスト対象とする
  # フォルダ名,階層,ファイル名は整合性が取れている必要がある
  if File.exist?("roles/#{role_name}.rb") then
    target_recipes = ItamaeUtils.new("roles/#{role_name}.rb").get_all_recipefiles
    target_recipes.each { |recipe|
      specfile = recipe.sub(/.*cookbooks/,'spec/localhost').sub(/.rb$/,'_spec.rb')
      targets.push(specfile) if File.exist?(specfile)
    }
  else
    targets = ['spec/localhost/**/*_spec.rb']
  end

  # ServerSpec実行
  # lib/rspec/itamaeutil.rb より先に requireするとSpecInfraでエラーになる
  # https://github.com/mizzy/specinfra/blob/master/lib/specinfra/configuration.rb#L57'
  require 'rspec/core/rake_task'

  puts "Test target spec files are ..."
  puts targets.flatten
  puts "------------------------------------------"
  RSpec::Core::RakeTask.new(:serverspec) do |t|
    if ENV['CI_FLAG']
      t.rspec_opts = "--no-color --format RspecJunitFormatter --out report/serverspec/results_#{hostname.chomp}.xml"
    end

    t.pattern = targets
  end
end
require 'itamae'

class ItamaeUtils
  def initialize(recipe)
    @targets = []
    options = {:node_yaml => 'nodes/default.yml'}
    backend = Itamae::Backend.create(:local, options)
    @runner = Itamae::Runner.new(backend, options)
    @runner.load_recipes([recipe]) # recipeを全てロードする
  end

  def get_all_recipefiles() #再帰的にRecipeをロードする
    get_recipefiles(@runner.children)
  end

  private
    def get_recipefiles(children) 
      children.each { |child|
        if child.is_a?(Itamae::Recipe)
          @targets << child.path # Recipeファイル名を取得する
          if child.children.count.positive?
            get_recipefiles(child.children)  
          end
        end
      }
      @targets
    end
end

今回のServerSpecはリモート実行はなくって全てローカル実行の前提になっていますが、 bundle exec serverspec を実行するとホスト名から roles/xxx.rb の roleファイルを探し出し、 そのレシピの include_recipe を解析して、Itamae実行した時に実行されるレシピファイルの全量のリストを取ってきます。

そのリストに紐づくSpecファイルを探して、存在すればテスト対象とする様にしています。

安全策として、roleファイルが見つからない場合は全Specを実行する様にしています。

これで、ServerSpec側はどのテストを呼ぶべきか?について省力化できました。 レシピに紐づくテストを書く事だけに集中できる様になりました。  

Itamae With DockerでEnv等を渡す

ItamaeはDockerに対して実行する事が出来ます。 Itamaeのレシピにホスト名の判定等がある場合は、Docker上でどうすればよいのでしょうか?

実はItamae側からDockerに対してオプションを渡す事が可能です。 これによってDockerコンテナに対してHostnameを適用して、その上でItamaeのレシピを実行する事が可能です。

サンプルコマンド / コード

tty: true       # Itamaeのsudoの為に必要
node_yaml: nodes/default.yml # Itamaeのレシピの為に必要
docker_container_create_options: # Dockerにオプションとして引き渡せる
  name: itamae-docker
  Hostname: dev-docker
  Domainname: sample.com
  Env:
    - sampleenv=hogehoge
bundle exec itamae docker --config itamae-docker.yaml roles/docker/web.rb --image centos:6.8

Itamaeは --config(-c)コマンドライン・オプションや、コマンドラインでは提供していない隠しパラメーターを引き渡す事が出来ます。 (コードではココ)

この際にDocker固有のパラメーターを渡すとSpecInfraにパラメーターが渡り、結果的にDockerに渡ります。 (コードではココ)

今回はあまりWebに出ていないいくつかのTipsを紹介してみました。 是非活用してみて下さい。  

最後に

マネーフォワードでは、エンジニアを絶賛募集しています。 ご応募お待ちしています。

【採用サイト】 ■マネーフォワード採用サイトWantedly | マネーフォワード

【プロダクト一覧】 自動家計簿・資産管理サービス『マネーフォワード』 ■WebiPhone,iPadAndroid

ビジネス向けクラウドサービス『MFクラウドシリーズ』 ■会計ソフト『MFクラウド会計』確定申告ソフト『MFクラウド確定申告』請求書管理ソフト『MFクラウド請求書』給与計算ソフト『MFクラウド給与』経費精算ソフト『MFクラウド経費』入金消込ソフト『MFクラウド消込』マイナンバー管理ソフト『MFクラウドマイナンバー』資金調達サービス『MFクラウドファイナンス』

メディア ■くらしの経済メディア『MONEY PLUS』