chef-soloからchef-zeroへ

長らくchef-solo+knife-soloと連れ添ってきたのですが、
そろそろzeroの学習をしておこうと色々とchef-zero移行を解説なさっているサイトを見て回りました。
たくさん解説されていらっしゃるサイトがあるのですが、一応自分なりにもまとめてみます。

環境の準備

とりあえずrbenv,ruby,bundler、とインストールして、いつもの環境を作ります。
Gemfileはこんな感じの最小構成。こいつでbundle installします。

source "https://rubygems.org"

gem "chef"
gem "knife-zero"

テスト用のレシピを書く。

とりあえずテストとして、apacheをインストールするレシピと、それを適用するjsonファイルを用意します。

$ mkdir -p cookbooks/httpd/recipes
$ echo 'package "httpd" do
>   action :install
> end' > cookbooks/httpd/recipes/default.rb

$ echo '{ "run_list": "recipe[httpd]" }' > web.json

local_mode でプロビジョニングする

chef-soloであれば、cookbook_pathを記述したsolo.rbを用意してchef-soloを実行するところですね。

sudo bundle exec chef-solo -c solo.rb -j web.json

chef-zeroのlocal_modeを使うと、このような感じになるようです。solo.rbが不要になり、少しだけ楽になりましたね。

sudo bundle exec chef-client -z -j web.json

実行後、nodes/(hostname).json ができていました。
内部的にはchef-serverが動いているわけですから、このノード(localhost)の状態がchef-serverの管理下に置かれているということですね。
(Node Objectと言うそうです)

このあたりはchef-server的な仕組みで動いていると思うので、chef-serverを学習した方が理解がはさそうな気がします。

knife-zeroでリモートホストをプロビジョニングする

ローカルにあるレシピを使って、リモートホストをプロビジョニングする場合はknife zeroを使います。

仕組みやチュートリアル含め、開発者様のQiitaを一読すると理解が早いと思います。

先ほど作ったレシピを使って、knifeしてみましょう。

まずはリモートホストをknife zeroの管理に追加します。
mkdir で “許可がありません” とエラーになる場合は、sudoできる権限をつけてあげて –sudo を追加します。

bundle exec knife zero bootstrap remote.com --sudo

nodes/(hostname).jsonが作成されていると思います。
また、node showすると、ノードが管理されている事がわかります。

$ bundle exec knife node show remote.com --local-mode
Node Name:   remote.com
Environment: _default
FQDN:        remote.com
IP:          10.0.2.15
Run List:
Roles:
Recipes:
Platform:    centos 7.0.1406
Tags:

ローカルモードは今後常に有効にするので、設定ファイルを作成して省略できるようにしておきます。
環境によってはsudoを省略するオプションも記述しておいた方が捗りそうです。

mkdir .chef
echo 'local_mode true' > .chef/knife.rb
echo 'knife[:use_sudo] = true' >> .chef/knife.rb
bundle exec knife node show remote.com

それではレシピを適用してみましょう。
knife soloの場合は、nodes/(hostname).json のrun_listを記述してknife solo cookしていました。

bundle exec knife solo cook (hostname)

knife zeroの場合も、nodes/(hostname).json のrun_listを記述して、以下のコマンドを実行するだけです。

bundle exec knife zero converge '(query)' 

knife soloと違い、queryにマッチするホストをすべてプロビジョニングできるようです。
これはかなり便利そうです。

今回の用に単一ホストをプロビジョニングする場合は、このような感じで書きます。

$ bundle exec knife zero converge 'name:remote.com'

remote.com Starting Chef Client, version 12.5.1
remote.com resolving cookbooks for run list: ["httpd"]
remote.com Synchronizing Cookbooks:
remote.com   - httpd (0.0.0)
remote.com Compiling Cookbooks...
remote.com Converging 1 resources
remote.com Recipe: httpd::default
remote.com   * yum_package[httpd] action install
remote.com     - install version 2.4.6-31.el7.centos.1 of package httpd
remote.com
remote.com Running handlers:
remote.com Running handlers complete
remote.com Chef Client finished, 1/1 resources updated in 06 seconds

ちゃんとapacheがインストールされましたね。
knife node show してみるとレシピが追加されている事がわかります。

$ bundle exec knife node show remote.com

Node Name:   remote.com
Environment: _default
FQDN:        remote.com
IP:          10.0.2.15
Run List:    recipe[httpd]
Roles:
Recipes:     httpd, httpd::default
Platform:    centos 7.0.1406
Tags:

細かい点では変わったところもありますが、今までのレシピが全く使えなくなるという事はなく、
確かにchef-solo+knife-soloからの移行は問題なさそう。

MySQL 5.7 お試し編

MySQL 5.7が正式にGAとしてリリースされたようです。
というわけでどこが変わったのか、触って見つつ調査してみようと思います。
ちなみにインストール先は AWS の r3.xlarge (Amazon Linux) です。

インストール/セットアップ

Redhat6向けのyumリポジトリをインストールし、mysql-community-serverをインストール。

sudo rpm -ivh http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm
sudo yum install -y mysql-community-server
sudo service mysqld start

# 原因が把握できていないのですが、mysqlのディレクトリに初期DBが構築されていない事があるようです。
# その場合は、以前のバージョンであれば mysql_install_db を実行して初期DBを構築しますが、
# mysql5.7からは mysqld --initialize に変更されたようです。
mysqld --initialize --datadir=/var/lib/mysql

rootでログインする際の初期パスワードは、下記の様にmysqlのエラーログに出力されるようです。
ノーヒントだったので困った。5.6は .mysql_secret というファイルに保存されていたのに・・

2015-10-22T10:32:43.950929Z 1 [Note] A temporary password is generated for root@localhost: xxxxxxxxxxx

ログインできることを確認したらインストール完了です。
適当にSQLを実行した際、このようなエラーが表示された場合は、 validate_password プラグインが有効になっています。
プラグインを無効化するか、適切なパスワードをrootユーザに設定してください。
(root以外のユーザも、強度の高いパスワードの設定を求められて面倒な為、今回はプラグインを無効化しています。本番環境では有効にしておきましょう)

> alter user root@localhost identified by "xxxxxxxxx";

or

> uninstall plugin validate_password;

とりあえずベンチマーク

http://mysqlserverteam.com/whats-new-in-mysql-5-7-generally-available/

5.6から3倍速くなったぜ!と豪語しているので、
別環境に mysql 5.6 をインストールし、sysbench でベンチマークを取ってみました。

sudo yum install -y sysbench --enablerepo=epel
echo "CREATE DATABASE sbtest; GRANT ALL PRIVILEGES ON sbtest.* TO sbtest@localhost IDENTIFIED BY 'sbtest';" | mysql -u root -p
sysbench --test=oltp --db-driver=mysql --mysql-password=sbtest prepare
sysbench --test=oltp --db-driver=mysql --mysql-password=sbtest run

ほどほどにメモリは割り当てています。

innodb_buffer_pool_size = 20G
innodb_log_file_size = 512M
innodb_flush_method = O_DIRECT
max_connections = 100
join_buffer_size = 128M
sort_buffer_size = 2M
read_rnd_buffer_size = 2M

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

mysql 5.6の結果

sysbench 0.4.12:  multi-threaded system evaluation benchmark

Running the test with following options:
Number of threads: 1

Doing OLTP test.
Running mixed OLTP test
Using Special distribution (12 iterations,  1 pct of values are returned in 75 pct cases)
Using "BEGIN" for starting transactions
Using auto_inc on the id column
Maximum number of requests for OLTP test is limited to 10000
Threads started!
Done.

OLTP test statistics:
    queries performed:
        read:                            140000
        write:                           50000
        other:                           20000
        total:                           210000
    transactions:                        10000  (234.00 per sec.)
    deadlocks:                           0      (0.00 per sec.)
    read/write requests:                 190000 (4446.01 per sec.)
    other operations:                    20000  (468.00 per sec.)

Test execution summary:
    total time:                          42.7349s
    total number of events:              10000
    total time taken by event execution: 42.6917
    per-request statistics:
         min:                                  2.67ms
         avg:                                  4.27ms
         max:                                 17.84ms
         approx.  95 percentile:               5.02ms

Threads fairness:
    events (avg/stddev):           10000.0000/0.00
    execution time (avg/stddev):   42.6917/0.00

mysql 5.7

sysbench 0.4.12:  multi-threaded system evaluation benchmark

Running the test with following options:
Number of threads: 1

Doing OLTP test.
Running mixed OLTP test
Using Special distribution (12 iterations,  1 pct of values are returned in 75 pct cases)
Using "BEGIN" for starting transactions
Using auto_inc on the id column
Maximum number of requests for OLTP test is limited to 10000
Threads started!
Done.

OLTP test statistics:
    queries performed:
        read:                            140000
        write:                           50000
        other:                           20000
        total:                           210000
    transactions:                        10000  (203.06 per sec.)
    deadlocks:                           0      (0.00 per sec.)
    read/write requests:                 190000 (3858.12 per sec.)
    other operations:                    20000  (406.12 per sec.)

Test execution summary:
    total time:                          49.2467s
    total number of events:              10000
    total time taken by event execution: 49.2031
    per-request statistics:
         min:                                  2.97ms
         avg:                                  4.92ms
         max:                                 24.68ms
         approx.  95 percentile:               5.40ms

Threads fairness:
    events (avg/stddev):           10000.0000/0.00
    execution time (avg/stddev):   49.2031/0.00

んー、どちらかというと5.7の方がスループットが出ていないような・・
この程度の負荷だと誤差の範囲なのかもしれません。
あとはパラメータチューニングをもっと細かくやるべきなのかも。

JSONテーブルってどんな感じ?

個人的には結構大きいJSONの対応。
PostgreSQLでは既に似たような機能が実装されていたのですが、
RDBMSが複数あるのは微妙ですよね。というわけで採用したことはありませんでした。
そんな中、ついにMySQLに実装されたので、色々と試してみたいと思います。

mysql> create table json_test (id varchar(64) primary key, val json default null);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into json_test values
    ->   ('key1', '{"id":1, "value":"hogehoge"}'),
    ->   ('key2', '{"id":2, "value":"fugafuga", "option":"optionvalue"}'),
    ->   ('key3', '{"id":3, "value":"foobar", "option":null}');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0
[/bash]

これでテーブルとテストデータができあがりましたね。
色々とクエリを発行してみます。
一致検索、LIKE検索と、簡単な構文には対応している様子。


mysql> select * from json_test;
+------+---------------------------------------------------------+
| id   | val                                                     |
+------+---------------------------------------------------------+
| key1 | {"id": 1, "value": "hogehoge"}                          |
| key2 | {"id": 2, "value": "fugafuga", "option": "optionvalue"} |
| key3 | {"id": 3, "value": "foobar"}                            |
+------+---------------------------------------------------------+
3 rows in set (0.00 sec)

mysql> select * from json_test where json_extract(val, '$.id') = 1;
+------+--------------------------------+
| id   | val                            |
+------+--------------------------------+
| key1 | {"id": 1, "value": "hogehoge"} |
+------+--------------------------------+
1 row in set (0.00 sec)

mysql> select * from json_test where json_extract(val, '$.value') like '%hoge%';
+------+--------------------------------+
| id   | val                            |
+------+--------------------------------+
| key1 | {"id": 1, "value": "hogehoge"} |
+------+--------------------------------+
1 row in set (0.00 sec)

mysql> select * from json_test where json_extract(val, '$.option') is null;
+------+--------------------------------+
| id   | val                            |
+------+--------------------------------+
| key1 | {"id": 1, "value": "hogehoge"} |
+------+--------------------------------+
1 row in set (0.00 sec)

mysql> select json_extract(val, '$.option') from json_test;
+-------------------------------+
| json_extract(val, '$.option') |
+-------------------------------+
| NULL                          |
| "optionvalue"                 |
| null                          |
+-------------------------------+
1 row in set (0.00 sec)

という感じで、なかなかそれっぽく動いているようです。
ただ、json内のフィールドとしてはnullとして保存されていても、SQLのnullとは評価されないようですね。。
ドキュメントを読んでもずばりな回答が得られなかったのですが、とりあえずjsonのnullにキャストするとnull値の検索が可能でした。

mysql> select * from json_test where val->"$.option" = cast('null' as json);
+------+----------------------------------------------+
| id   | val                                          |
+------+----------------------------------------------+
| key3 | {"id": 3, "value": "foobar", "option": null} |
+------+----------------------------------------------+
1 row in set (0.00 sec)

そしてjson用の独自構文。

mysql> select * from json_test where val->"$.id" = 2;
+------+---------------------------------------------------------+
| id   | val                                                     |
+------+---------------------------------------------------------+
| key2 | {"id": 2, "value": "fugafuga", "option": "optionvalue"} |
+------+---------------------------------------------------------+
1 row in set (0.00 sec)

sqlでアロー演算子は斬新すぎでは?
なんとか楽にフィールドにアクセスできないか、という事で生まれたのでしょうか・・・
まあ助かりますけど・・・

InnoDB周りの改善だったりとか、他にも色々とあるのですが、今日のエントリではこんなところで。

AWS Lambda Scheduled Eventを試してみる

個人的に心待ちにしていた機能、Scheduled Eventが発表されました。
すでにリリースされているそうなので早速試してみました。

とりあえずサンプルとなるLambda functionを作成。
このサイトへのHTTPリクエストを発行するコードとなっています。

サンプルLambda

そして、Event sources に Scheduled Eventを追加。
とりあえずcronフォーマットで5分毎に実行する形で登録してみます。

lambda_schedule_1

このサイトのアクセスログを関してみたところ、ちゃんと5分ごとにアクセスが来ていることが確認できました。
(こちらのIPアドレスをwhoisすると、ちゃんとamazonからのものでした)

lambda_access

しかし Lambda はすばらしいですね。

EBSのスナップショット作成スクリプト

EBSのスナップショットを作成し、古いスナップショットを自動削除するスクリプトを書いたのでメモがてら残します。
このスクリプトを実行するインスタンスのすべてのボリュームに対して、EXPIRE_DAYS日数経過したスナップショットを削除し、EBSスナップショットを作成します。
あ、credentials は ~/.aws/credentials にある前提です。

#!/bin/sh

# ------------------------------------------
# configuration
EXPIRE_DAYS=4
INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
EXPIRE_DATE=`date +%Y-%m-%dT%H:%M:%S.0000Z -d "-${EXPIRE_DAYS}day"`

# ------------------------------------------
# script start

STATUS=0

for VOL_ID in `aws ec2 describe-volumes --filters "Name=attachment.instance-id,Values=${INSTANCE_ID}" | grep -e '^VOLUMES' | awk '{ print $9 }'`
do
  # delete old snapshots
  for SNAPSHOT in `aws ec2 describe-snapshots --filters "Name=volume-id,Values=${VOL_ID}" | grep ${VOL_ID} | awk '{ print $8","$9 }'`
  do
    SNAPSHOT_ID=`echo $SNAPSHOT | awk -F"," '{ print $1 }'`
    CREATE_DATE=`echo $SNAPSHOT | awk -F"," '{ print $2 }'`

    # delete expired snapshot.
    if [[ "$EXPIRE_DATE" > "$CREATE_DATE" ]]; then
      aws ec2 delete-snapshot --snapshot-id $SNAPSHOT_ID > /dev/null
      RESULT=$?
      if [ $RESULT -ne 0 ]; then
        echo "delete for snapshot failed... ($SNAPSHOT_ID, $CREATE_DATE, ${RESULT})"
        STATUS=1
      fi
    fi
  done

  # create snapshot.
  aws ec2 create-snapshot --volume-id ${VOL_ID} --description "`date +%Y-%m-%d` snapshot instance-id:${INSTANCE_ID}" > /dev/null
  RESULT=$?
  if [ $RESULT -ne 0 ]; then
    echo "create for snapshot failed... (${RESULT})"
    STATUS=2
  fi
done

exit $STATUS