[chef] knifeでroleで使われているrecipeを検索

chefの運用が始まって、サーバの設定のほとんどをchefでやるようになりましたが、その中で不便なことが出てきました。
recipeを変更してそれをサーバに反映させるときに、「はて?このrecipeはどこで使ってたっけ?」となってしまうのです。
今の環境では能動的にサーバの設定変更をしたいので、chef-clientはdaemonで起動しておらず、手動でchef-clientを実行してサーバに反映させています。
その場合にrecipeを変更した後、どのサーバでchef-clientを実行すればよいか探すことになり、場合によっては反映し忘れということも起こってしまいます。

knifeのsearchでrun_listを検索

そこで、knifeのsearch機能に着目してrun_listを検索することにしました。
使い方はこちらを参照。

Dashboard – Chef – Home – Chef Essentials – Search

knifeのsearchは具体的にはsolrのindexの検索です。
こんな感じで、run_listを検索しました。

$ knife search role "run_list:*MySQL*" -i
10 items found

db-server

stg-db
.
.
.

“-i”は検索結果のidのみを表示するオプションです。
これでrecipe[MySQL]を使っているroleを検索することができました。
ちなみに

$ knife search role "run_list:recipe[MySQL]"

としてもエラーが出てしまい検索できませんでしたので、上記のようなクエリーになったわけですが、多分もっとスタイリッシュなクエリーがあるはずです。それにはもっと勉強が必要なので今日はこれまで。

PythonでAWS(EC2)操作

最近は仕事の9割がAWS(Amazon Web Service)環境となっていて、毎日のようにインスタンスを立ち上げてchefでサーバの構築をしています。
数が少ないうちはWEBのManagement Consoleでぽちぽちやっていたんですが、いい加減面倒。なにが面倒ってサーバによってはデータ領域をEBSにしたりSecurityGroupを作ったり。
さらに作業を面倒にしているのが複数のAWSアカウント。プロジェクトによってAWSアカウントを分けていたりして、その度にサインアウトしてサインインして、もう!という感じでした。

環境変数を変えながらec2-api-toolで頑張ってました

僕の作業環境はMacなので、最初のうちは(というか今でも)それぞれのアカウントの認証情報を環境変数に設定するshell scriptを用意して、ec2-api-toolsを使って操作していました。
例えば以下のように。

$ #account-01の認証情報(aws_access_key_idとか)が書いてあるファイルをsourceで読み込む
$ source account-01.rc
$ ec2-describe-instances
   .
   .
   .

SecurityGroupを確認したり設定したりする時はこれが便利。
しかし、Instanceを起動したりEBSを作ったりはこれではまだまだ手間がかかるのでスクリプトで一発で作れるようにしてみました。

botoを使ってpythonでインスタンス起動

スクリプトでやるメリットは

  • インスタンス起動時にcloud-initでホスト名を設定するためにuser_dataを設定する
  • インスタンス起動と同時にtag:Nameにホスト名を設定する
  • インスタンスのホスト名から判断して適切なSecurityGroupを設定する
  • インスタンスのホスト名から判断してEBSが必要な場合はEBSを作って、EBSのTag:Nameに名前を付けて、インスタンスにAttachする

と、こんなところです。
将来的にはchefへの登録もこの流れにのせられるかなと思っています。

実はスクリプトでのインスタンス起動は前からbash+ec2-api-toolでやってたのですが、その時は単独アカウントと1種類のサーバのみだったので、管理アカウントが増えた今回はpythonで作り直すことにしました。

なぜpythonかって?そりゃ好きだからです。
これまではサーバ管理用スクリプトはbashでムリがあるものはperlでやっていたのですが、正直perlにはあまり良い思い出が無く、1年ぐらい前にpythonを使い始めて、それ以降はこの手のスクリプトはpythonで作るようになっています。

まずは上記を実装すべくPython用のEC2のSDKであるbotoを使って作ってみました。
コード全てを載せるのもアレなので、ハマったところやドヤ顔できる部分を抜粋します。

複数アカウントの認証関連情報はiniで管理

今回の目的の一つである複数アカウント対応は、iniファイルを使って認証関連情報を管理することにしました。
僕にpythonを勧めてくれた後輩君に「pythonで設定ファイルなら何が良い?xml?ini?」と聞いたら「ini」と即答。iniはConfigParserで一発で使えるとのこと。確かにラクチンでした。

例えば以下のようなiniファイルを用意します。

$ cat hoge.ini
[account-01]
aws_access_key_id=xxxxxxxxxxxxxxxxxxxxxxx
aws_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
account_id=xxxxxxxxxxx

[account-02]
aws_access_key_id=xxxxxxxxxxxxxxxxxxxxxxx
aws_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
account_id=xxxxxxxxxxx

これをpythonで使うにはこうします。

import ConfigParser

# オブジェクト作成
ini = ConfigParser.SafeConfigParser()

# ini読み込み
ini.read("/path/to/hoge.ini")

# account-01のaws_access_key_idを取り出す
aws_access_key_id = ini.get("account-01","aws_access_key_id")

簡単です。

こんな感じで各アカウントの情報をiniにまとめて引数でそれぞれの情報をセットするようにしました。

botoの基本的な使い方

botoのドキュメントはこちら。http://docs.pythonboto.org/
今回はこの中のEC2を使ってインスタンスの起動をしています。

ec2 APIに接続します。

import boto.ec2
region = "ap-northeast-1"
auth = {
  "aws_access_key_id":ini.get('account-01', 'aws_access_key_id'),
  "aws_secret_access_key": ini.get('account-02',aws_secret_access_key')
}

# region指定でec2に接続
conn = boto.ec2.connect_to_region(region, **auth)

っと、ここまで書いて疲れちゃいましたので続きはまた今度。

[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で固めてバックアップを取ることにしました。

おしまい。