[chef] chefサーバのバックアップ

一通りchefの構築を済ませたので、この辺でchefサーバのバックアップを仕掛けることにしました。
chefも構築を始めるとノウハウの塊になってきて消失するとかなり痛いです。

node,role,data bagのバックアップ

node,role,data bagのバックアップはドキュメントに書いてあるchef_server_backup.rbを使うことにしました。

これを使えば、node,role,data bagは$cwd/.chef/chef_server_backupというディレクトリにそれぞれのオブジェクトがjson形式でバックアップされます。

chef_server_backup.rbの使い方

使い方ってほどでもないのですが、水増しのため解説します。

まずはchef_server_backup.rbを入手します。

cd /path/to/working_dir
curl -O https://raw.github.com/jtimberman/knife-scripts/master/chef_server_backup.rb

そして、knife execchef_server_backup.rbを実行します。
もちろんknifeが正常に動くことが大前提です。

knife exec /path/to/chef_server_backup.rb

以上!

でも、これではせっかく時間をかけて作ったCookbookがバックアップされません。
Cookbookはこれとは別にバックアップを取る必要があります。

Cookbookのバックアップは自作した

いろしろ探したのですが、Cookbookのバックアップツールは見つかりませんでした。
chef_server_backup.rbと同じような感じでできるのが一番かっこいいのですが、残念ながらそれをするためのRuby言語のスキルが僕には圧倒的に不足しています。

ということで、いろいろ悩んだ結果pythonでknifeを使って、cookbookのリストを取得し、それをknifeコマンドでdownloadするベタな方法にしました。
何で悩んだかというとcookbookのバージョンです。
ご存知の通りCookbookは複数のバージョンを保持することができます。それを全てバックアップを取ろうとするとどうやっても僕のスキルではbashで実装できなかったので、pythonで作りました。

恥をしのんでそのスクリプトを晒します。

#!/usr/bin/python
# -*- coding:utf-8 -*-
import subprocess,sys,os,os.path,shutil
import datetime,locale

import smtplib
from email.MIMEText import MIMEText
from email.Utils import formatdate

backup_dir = '/data/chef-repo/.chef/cookbook'
subject = 'Cookbook Backup ERROR'
mail_from = 'from@domain'
mail_to = 'to@domain'

# 現在時刻を取得する関数
def getnow():
  return datetime.datetime.today().strftime("%Y/%m/%d %H:%M:%S")

# メール構築
def create_message(f,to,subject,body):
  message = MIMEText(body)
  message['Subject'] = subject
  message['From'] = f
  message['To'] = to
  message['Date'] = formatdate()
  return message

# メール送信
def sendMail(f, to, message):
  s = smtplib.SMTP()
  s.connect()
  s.sendmail(f, [to], message.as_string())
  s.close()
 
# 処理開始
print '---START {0} ----'.format(getnow())

# 作業用ディレクトリを消す
if os.path.isdir(backup_dir):
  print "Remove {0}".format(backup_dir)
  try:
    shutil.rmtree(backup_dir)
  except OSError:
    error_message = "ERROR: {0} can not delete.".format(backup_dir)
    print error_message
    sendMail(mail_from,mail_to,create_message(mail_from,mail_to,subject,error_message))
    sys.exit(1)

# 作業用ディレクトリの作成
try:
  os.mkdir(backup_dir)
except OSError:
  error_message = "ERROR: {0} can not create.".format(backup_dir)
  print error_message
  sendMail(mail_from,mail_to,create_message(mail_from,mail_to,subject,error_message))
  sys.exit(1)

# cookbookリストを取得する
cmdline = ['/usr/bin/knife', 'cookbook', 'list', '-a']
subproc_args = {
  'stdin':subprocess.PIPE,
  'stdout':subprocess.PIPE,
  'stderr':subprocess.STDOUT,
  'close_fds':True,
}

try:
  p = subprocess.Popen(cmdline, **subproc_args)
except OSError:
  error_message = "Failed to execute command. : {0}".format(cmdline.__str__())
  print error_message
  sendMail(mail_from,mail_to,create_message(mail_from,mail_to,subject,error_message))
  sys.exit(1)

cmd_args = {
  'stdin':subprocess.PIPE,
  'stdout':subprocess.PIPE,
  'stderr':subprocess.STDOUT,
  'close_fds':True,
}

# Cookbookごとにdownload
while True:
  line = p.stdout.readline().split()
  if not line:
    break
  cookbook_name = line.pop(0)
  print cookbook_name
  pp = ""

  # 複数バージョンがある場合はそれらをdownload
  for version in line:
    cmdline = "/usr/bin/knife cookbook download {0} {1} -d {2}".format(cookbook_name, version, backup_dir)
    print "Downloading {0} {1}: ".format(cookbook_name, version),
    try:
      ret = os.system(cmdline)
    except OSError:
      print "ERROR"
      error_message = "Failed to execute command.: {0}".format(cmdline.__str__())
      print error_message
      sendMail(mail_from,mail_to,create_message(mail_from,mail_to,subject,error_message))
      sys.exit(1)

    print "SUCSESS"
    
print '---END {0} ----'.format(getnow())

このスクリプトで/path/to/.chef/cookbookに全てのcookbookをダウンロードして、chef_server_backup.rbで取得したオブジェクトのjsonをまとめてtarで固めてバックアップを取ることにしました。

おしまい。

[chef] clientとnodeの関係

解りづらいclient

chefの要素でclientとnodeがあります。
いろいろ調べる中で、このことを解説しているところがあるのですが、自分は以下のように解釈していました。
Node = 管理対象サーバ
client = Chef Serverとやり取りするインタフェース

この解釈で概ね誤りは無いようなんですが、一つ大きく勘違いをしているところがありました。

Clientの流用は出来ない

どういうことかというと、一つのclient、もっと言えばClientのPrivate Keyを複数のサーバに配布して「Nodeは増えるけど、Clientは増えない」ということは出来なそうな感じです。

chefを使う理由は関するサーバの台数が増えて来て管理が大変なので、一つ例えば「forServer」というclientを作っておいて、そのprivate keyをchef管理下に入るサーバに置いておいて、clientはそれを使ってもらうということにしたかったのです。

chefの管理下に入るには以下の手順が行われます。

  1. 管理対象サーバにchef-clientをセットアップ
  2. chef-serverのclient[chef-validator]のprivate keyをvalidation.pemとして配置。(通常は/etc/chef/validation.pem
  3. 管理対象サーバでchef-clientコマンドを実行
  4. client[chef-validator]によって、chef-serverにnode登録される
  5. 管理対象サーバ用のclientが作られて、管理対象サーバにはclient.pemが配置される(通常は/etc/chef/client.pem

こんな手順でサーバをchef管理下に入れていきますが、出来れば各サーバごとにclientが増えるのは勘弁してほしいところ。
ちなみにあらかじめ作成したclientのprivate keyを配置してclient.rbに以下の行を指定してもダメでした。

client_key   "/etc/chef/client.pem"

どうにかならないものでしょうか。

[chef] attributeの理解

今回はattributeの話。
お約束ですが、この記事の内容は私が一人で分かったつもりになって書いていることなので、誤認やウソが書いてありましたらご指摘してくださると助かります。

attributeとは

Recipeではできるだけ汎用的に書いた方がいいという雰囲気がありますが、それはその通りだと思います。
その汎用性を持たせる機能を担っているのが、providerとこのattributeでは無いでしょうか。
providerはOSなどの環境を吸収してくれて、attributeは変数として使うことができます。

分からなかったのはあちこちでattributeが設定できること

CookbookやRecipeを調べてたり、ドキュメントを読んでいたりすると、あちこちでdefault_attributesやらoverride_attributesを見かけます。
いったいどこでattributeを設定するのが正しいの?と混乱してしまっていました。

その混乱を鎮めたのがドキュメント以下の記述。

Chef > Home > Chef Essentials > Attributes > Precedence

これを見ると設定箇所によってプライオリティがあり、どこで設定するのが適切かということが分かった気がします。

イメージとしては、CookbookのCOOKBOOK/attribute/default.rbではデフォルト値を設定。
roleのdefault_attributesではそのRoleに適した値を設定。
という感じでしょうか。

具体例

具体例を挙げてみます。
シナリオはいつもの通りAWS EC2でchefを使うという想定です。
複数のAWSアカウントを持っていて、横断的にchefを使いたい。
例としてAWSアカウントの各種認証情報をサーバに保管するRecipeを作ります。

template "/path/to/credencial" do
 source "credencial.erb"
 owner "root"
 group "root"
 mode 0644
 variables(
  :cert      => node["ec2"]["cert"],
  :pk        => node["ec2"]["cert"],
  :awsid     => node["ec2"]["awsid"],
  :accesskey => node["s3"]["access_key_id"],
  :secretkey => node["s3"]["secret_key"],
  :region    => node["ec2"]["region"]
 )
end

これは/path/to/credencialというファイルにAPIの証明書やらaccesskey_idなどの情報を記載したファイルのResourceです。
もちろんCOOKBOOK/templates/default/credencial.erbというテンプレートファイルも用意します。
そして、COOKBOOK/attribute/default.rbに以下のように上記resourceで使っているattributeのデフォルト値を設定します。

# s3 key 
default["s3"]["access_key_id"] = ""
default["s3"]["secret_key"] = ""

# ec2 credencial
default["ec2"]["key_location"] = ""
default["ec2"]["cert"] = ""
default["ec2"]["pk"] = ""
default["ec2"]["awsid"] = ""
default["ec2"]["region"] = "ap-northeast-1"

デフォルト値と言ってもそれぞれ、適切な値を入れないと意味がないので、適当に空に設定してみました。

次にRoleを作ります。
account_aというAWS Accountに紐づいたRole[account_a]というroleを作ります。

name "account_a"
description "AWS account_a account servers"
run_list("recipe[Ec2Init]")
default_attributes({
  "s3" => {
    "access_key_id" => "xxxxxxxxxxxxxxxx",
    "secret_key" =>  "xxxxxxxxxxxxxxxxxxx"
  },
  "ec2" => {
    "key_location" => "hoge",
    "cert" => "cert-xxxx.pem",
    "pk" => "pk-xxxx.pem",
    "awsid" => "xxxx"
  }
})

こんな感じで、attributeを便利に使うことができました。

まとめ

attributeが使えるようになって、汎用性がぐっと広がりました。
やってみて分かったんですが、attributeは最終的にnodeに付与されて、node['hoge']['piyo']という感じで指定するんですね。

次回はなにかな?

[Chef] Cookbook,Recipe,Roleの関係

Chef検証再開。

Chefを設計・運用するうえで、どうしても理解しなければいけないのが、CookbookRecipeとRole`の関係。

とりあえず、それを理解するためにこのスライドを見てみた。

Resouceは設定の最小単位

Resouceは各種設定の最小単位と解釈しました。
ファイルを配置する、ソフトウェアをインストールする、ユーザを作る、等など。

これらの設定はOSによってコマンドはまちまちですが、それぞれのコマンドの違いはPlatformやらProviderやらで吸収されて、ResouceではそれらのOSの違いを意識せずに設定を指定できるところが良いところです。

RecipeはResourceのリスト

スライドに

Recipes are lists of Resources

と書いてありました。

僕は、「Recipeは一つの操作をするために必要なこと(Resource)がまとまったもの」と解釈しました。
例えば、apacheをインストールする場合、以下のような設定をする必要があります。

  • apacheのインストール
  • httpd.confの設定
  • 必要に応じて各種設定

それらを一つのRecipeの中に書くことで、apacheのインストールという一つの操作を完了することができます。

[ Cookbook -ne Role ]

分からなかったのが、CookbookとRoleの区別でした。
Roleは単語の意味からWebServerなどの役割を指定して、そのRoleをNodeに適用するとWebServerの機能が構築されてWebServerとして利用できるものと理解しています。

では、Cookbookは?

Cookbookの役割がよくわからなかったので、試しに[Opscode Public Cookbooks](Opscode Public Cookbooks)からApacheのCookbookをダウンロードして中を見てみました。
Cookbook[“apache”]のRecipeのdefaults.rbはapache本体のインストールでした。
そして、defaultではないその他のRecipeはapache modulesのインストールでした。

これで何となくCookbookが分かって来た気がしますが、さらに理解を深めるためcookbook[MySQL]を見てみました。

cookbook[“MySQL”]のRecipesの中にはdefault.rb,client.rb,server.rbがありました。
Recipeのdefault.rbには一行。

include_recipe "mysql::client"

CookbookはRecipeのグループ

これらの調査から、Cookbookはソフトウェアや設定のグルーピングとして使うという理解に至りました。
特定のソフトウェアとそれに付属するソフトウェアや設定などを一つのCookbookにまとめることができそうです。

Roleを書く

ということでまとめという意味でWebServerというRoleがあったと仮定して、そのRoleのファイルを書いてみます。


name "webserver"
run_list "recipe[common]","recipe[apache2]", "recipe[apache2::mod_proxy]","role[product]", "role[datacenter01]"

まだまだ続く

とりあえずここまでなーんとなく分かった気がします。

でも、まだまだ分からないことばかり。
例えば、attributeとか。
さらに調査を続けます。

もし、この記事の内容に理解に誤りがあれば、是非ご指摘ください。お待ちしています。