ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

「やわらかConsul」というワークショップをした

社で、徐々にConsulへの依存度が上がってきたので、Consulパーソンをより増やすべくワークショップを開催した。

gistを公開します。

サンプルプロジェクトは以下。Vagrantのプロビジョナでポンポンとステップを進められます。

github.com

Consul、機能が多いけど、クールなこともできるので、知っていることはどんどんドキュメントにしていこうと思いました。

OpenStack 上にある MySQL のデータを、ファイルベースでサーバAからサーバBに復旧する手順

  • ある日、 OpenStack にあるサーバAが起動しなくなった :sob:
  • OpenStack なのでディスクイメージは見ることができる

という状態。起動しないので mysqldump のようなコマンドは使えないが、以下のように復旧した。

同じアーキ、同じOSでサーバを作り直す

Puppetなどでサーバ構成管理していればなんちゃないですね。していない場合は... ......

サーバAの母艦ノードに入る

いわゆる、compute nodeというもの。

拡張の種類にもよるが、 nova show などで確認できるはず。また、一緒に instance_name というものも確認できる、控えておく。

$ nova show test001
+--------------------------------------+------------------------------------------------------------------+
| Property                             | Value                                                            |
+--------------------------------------+------------------------------------------------------------------+
...
| OS-EXT-SRV-ATTR:hypervisor_hostname  | comp-node0014.example.cloud                                      |
| OS-EXT-SRV-ATTR:instance_name        | foocloud01-0000063b                                              |
...

入ったら、 guestmount というすごい便利そうなコマンドがあって、それでサーバAのブートディスクをマウントできる。

root@comp-node0014:/# instance_name=foocloud01-0000063b
## /var/lib/nova/instances/${instance_name}/disk にあるはず
root@comp-node0014:/# mkdir /tmp/${instance_name}-work
root@comp-node0014:/# guestmount --ro -a /var/lib/nova/instances/${instance_name}/disk -m /dev/ubuntu-vg/root /tmp/${instance_name}-work

マウントできたら、 chroot なりなんなりでサーバAのファイルシステムがどうなっていたか確認できる。

ここで、mysqlの既存のデータを持ってくる。

# chroot /tmp/${instance_name}-work
root@comp-node0014:/# cd /var/lib
root@comp-node0014:/var/lib# tar czvf mysql.tgz mysql/                                                                                                                   
mysql/
mysql/ibdata1
mysql/ib_logfile1
mysql/auto.cnf
...
root@comp-node0014:/var/lib# exit

## 見える箇所にコピー
# cp /tmp/${instance_name}-work/var/lib/mysql.tgz ./
## umount忘れない
# umount /tmp/${instance_name}-work

サーバBのmysqlデータを置き換える

このデータをアップロードした前提で、以下は新サーバでの操作。

## mysqlを止める
ubuntu@serverB:~$ sudo systemctl stop mysql
## 持って行ったら /var/lib/mysql を差し替え
ubuntu@serverB:~$ sudo mv /var/lib/mysql /var/lib/mysql.bak
ubuntu@serverB:~$ tar xzf mysql.tgz 
ubuntu@serverB:~$ sudo mv ./mysql/ /var/lib/

ただ置き換えただけでは認識しないので、 mysql_upgrade の実行。

ubuntu@serverB:~$ sudo mysql_upgrade -u root -p
Enter password: 
Looking for 'mysql' as: mysql
Looking for 'mysqlcheck' as: mysqlcheck
Running 'mysqlcheck with default connection arguments
Warning: Using a password on the command line interface can be insecure.
Running 'mysqlcheck with default connection arguments
Warning: Using a password on the command line interface can be insecure.
mysql.columns_priv                                 OK
mysql.db                                           OK
mysql.event                                        OK
...

sudo しているのは、 /var/lib/mysql/mysql_upgrade_info というファイルの読み書きができるユーザでこのコマンドを実施する必要があるため。mysql的なユーザは関係ない。

終わったら、mysqlを立ち上げ、mysqlクライアントからSQLなどでデータの復旧を確認する。

ubuntu@serverB:~$ mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 23
Server version: 5.6.25-0ubuntu0.15.04.1 (Ubuntu)

Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show tables;
ERROR 1046 (3D000): No database selected
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| foobar             |
| mysql              |
| performance_schema |
+--------------------+
4 rows in set (0.00 sec)

お疲れ様でした。

参考です

Hacker TackleでHashicorpツールについて話した

hackertackle.github.io

話しました

www.slideshare.net

最近とにかくPackerとConsulを触っていたのでそのお話をし、その後、なぜ僕がこのようにYak刈りに突っ込んでいくかのお話をしました、が、当日は50分を若干オーバーし、後半(後半が一番大事なのに)は駆け足となってしまいました......すいません。

50分で180枚目のところぐらいまでは話せたので、今後はこれをベロシティの基準にしたいと思います...(?

当日の話

正直発表前はそわそわして集中できず、発表後は放心演義といった趣きだったのですが、松本さん、ばりかた勉強会の花田さんのお話などを聞いていました。

懇親会では、おひさしぶりに id:repeatedly さんとお話できたり、 id:kyon_mm さん、 id:daiksy さんといった以前よりお話したいと思っていた方々と初めて話せたり、非常に楽しく過ごせました!

運営の皆様ありがとうございました。福岡でもこんな感じのキレキレなイベントをもっと増やしたいですね。

その他の登壇者の皆さんへリンク

hb.matsumoto-r.jp

daiksy.hatenablog.jp

packer-provisioner-serverspec というPackerプラグインを書いた

以前、Qiitaでこういう記事を書いたんですが:

qiita.com

これと同じコンセプトで、最低限の設定で作成後のイメージにServerspecを走らせるためのプラグインを書きました。

github.com

インストールは、今のところ自分でビルドする必要があります(もうちょいちゃんとしたらバイナリを配りたい)。

# Needs Go >= 1.3
$ git clone https://github.com/udzura/packer-provisioner-serverspec.git
$ cd packer-provisioner-serverspec
$ make install

設定はこんな感じで。

{
    "builders" : [
        {
            "type": "amazon-ebs",
            "ssh_username": "centos",
            "ssh_pty": true,
            "...": "..."
        }
    ],

    "provisioners": [
        {
            "type": "shell",
            "script": "/path/to/your-provisoner.sh"
        },
        {
            "type": "serverspec",
            "source_path": "/path/to/your/serverspec-root"
        }
    ]
}

今はα版ということ(にしておく)で、いろいろな条件が固定です。

  • sshのユーザはsudoできないといけない
  • "ssh_pty": true でないといけない(Packer 0.8からだそうです。参考
  • rakeタスクは rake spec でないといけない
  • ディレクトリ構成はこんな感じがよさそう
/path/to/your/serverspec-root
├── Gemfile # Gemfileとlock、必ずしも必要なし
├── Gemfile.lock
├── Rakefile
└── spec
    ├── localhost
    │   ├── cloud_init_spec.rb
    │   ├── hosts_spec.rb
    │   ├── packages_spec.rb
    │   └── users_spec.rb # and so on...
    └── spec_helper.rb

Serverspec自体のインストールは omnibus を使っています。 @sawanoboly さんは神です。おかげで、CentOSUbuntuであればひとまず動きそうです。あ、Ubuntuはちゃんと検証していません......

rake spec が終わったらserverspecパッケージとアップしたserverspecのディレクトリは綺麗に消します。

そんな感じで、機能はまだ全然足りないですが、お試しください。


PR

こういうPackerの深い話、具体的にはprovisionerだけでなくbuilderまで自作した事例を、 HackerTackleというイベントで話します。ついでに、 Consulの話とかなんか抽象的な話もしちゃいます。

hakat.connpass.com

なんと! 明日 なんですが、会場かなり余裕があるので、今から登録しても参加できると思います!

九州に住んでいない人も、今から飛行機を取れば全然間に合います!

という感じで、深い話ができ、なおかつ最前線(自分を棚にあげる)のエンジニアに質問できたり、がっつり議論できる貴重な場だと思います。博多でお待ちしております!

Faradayの話 - OpenStack クライアント開発日記 (3)

今日はFaradayの話をする。

github.com

yao の開発の基本方針は、「必要のない依存をしない」だけれど、Faradayは数少ない依存gemに含まれている。

github.com

理由は、

  • Faraday自体の依存gemが少ない
  • 利用することでの、コードをスッキリさせるメリットが非常に大きい

と考えたから。

具体的には、FaradayはRackよろしくMiddlewareで機能追加をするパターンを採用しているため、ちょっとした仕事をさせるためのコードが、短く、わかりやすいものにできる。以下は具体例。

ログイントークンの保持と再発行

OpenStackのAPI利用の流れは、

  • username、パスワード、テナント名(AWSでいうVPCひとつひとつみたいな感じ)を指定し、ログイン用エンドポイントを叩く
  • そのエンドポイントから、ログイン用のトークンと、各サービス(compute、networkなど)それぞれのエンドポイントURL一覧をもらえる
  • トークンとエンドポイント情報を利用してその後のAPIを叩く

という感じになっている。 OpenStack API Quick Start の通りである。

トークンは、 "ac4e1c05084ba4365c1c38bfb1350000" みたいなMD5ハッシュ風の値で、これをヘッダに X-Auth-Token: "ac4e1c05084ba4365c1c38bfb1350000" と含めばOK。なお、この認証方式、微妙にOAuth2と違うので既存のMiddlewareを使いまわせない...

今回は以下のようなMiddlewareを作った。

class Faraday::Request::OSToken
  def initialize(app, token)
    @app = app
    @token = token
  end

  def call(env)
    if @token.expired?
      @token.reflesh(Yao.default_client.default)
    end

    env[:request_headers]['X-Auth-Token'] = @token.to_s
    @app.call(env)
  end
end
Faraday::Request.register_middleware os_token: -> { Faraday::Request::OSToken }

ご覧の通り、これはなんというRack Middleware...という感じである。

ポイントは、 @token 自体は普通にTokenオブジェクトなので、expireしたかどうかの情報も所持している。なので、「Tokenがexpireしていたら、ハンドラーの中で勝手に再発行する」といった実装が可能になっているところ。

レスポンス JSON をそのままダンプする

OpenStackのドキュメントなどに想定されるレスポンスはあるんだけど、実際のものを見た方が理解が早い場合が多い。

こういうレスポンスを保存するやつとしては vcr/vcr · GitHub が有名だけど、リクエストも保存したりなど重量級なのでこんぐらいのコードで対応する。

class Faraday::Response::OSResponseRecorder < Faraday::Response::Middleware
  def on_complete(env)
    require 'pathname'
    root = Pathname.new(File.expand_path('../../../tmp', __FILE__))
    path = [env.method.to_s.upcase, env.url.path.gsub('/', '-')].join("-") + ".json"

    puts root.join(path)
    File.open(root.join(path), 'w') do |f|
      f.write env.body
    end
  end
end
Faraday::Response.register_middleware os_response_recorder: -> { Faraday::Response::OSResponseRecorder }

レスポンスのMiddlewareの場合、Rackっぽくなくて、 on_complete(env) が順次呼ばれ続けるというイメージ。その中でenvに副作用を起こせばいい。

このMiddlewareを有効にすると、通常通りAPIアクセスをしつつ、 PROJECT_ROOT/tmp 以下にそのままレスポンスのJSONが残る。

$ jq . < tmp/POST--v2.0-tokens.json                                                                                    
{
  "access": {
    "token": {
      "issued_at": "2015-08-31T03:58:36.073232",
      "expires": "2015-09-01T03:58:36Z",
      "id": "31a5166533fd49f3b11b1cdce2000000",
      "tenant": {
        "description": "development environment",
        "enabled": true,
        "id": "b598bf98671c47e1b955f8c9660e0000",
        "name": "dev"
      }
    },
    "serviceCatalog": [
      {
        "endpoints": [
....

エラー処理

OpenStack APIのエラーの扱いは、原則として 200..299 の範囲外のものであれば、ステータスコードに対応したエラーになる(300系はない。ないよね?)。

なので、アダプター側で共通化できる。

class Faraday::Response::OSErrorDetector < Faraday::Response::Middleware
  # TODO: Better handling, respecting official doc
  def on_complete(env)
    # Faraday::Env の組み込みメソッド
    # 本来、APIごとに正常なステータスコードの指定があるのだが、後でやる
    return if env.success?

    raise Yao::ServerError.detect(env)
  end
end
Faraday::Response.register_middleware os_error_detector: -> { Faraday::Response::OSErrorDetector }
module Yao
  class ServerError < ::StandardError
    def self.detect(env)
      case env.status
      when 400
        if env.body && env.body["computeFault"]
          ComputeFault.new(extract_message(env.body), env)
        elsif env.body && env.body.to_a.join.include?('NetworkNotFound')
          NetworkNotFound.new(extract_message(env.body), env)
        else
          BadRequest.new(extract_message(env.body), env)
        end
      when 401
        Unauthorized.new(extract_message(env.body), env)
      when 404
        if env.body && env.body["itemNotFound"]
          ItemNotFound.new(extract_message(env.body), env)
        else
          NotFound.new("The resource could not be found.", env)
        end
      when 405
        BadMethod.new(extract_message(env.body), env)
      # ... こんな感じで判定を頑張る
      end
    end
  end
end

エラー種類の判定は、まあ頑張ってメタプログラミングしなくていいかと思ってこうしてる。この辺多分最近Go言語を書いてることが強く影響してそう。。

エラーについては、より注意深くハンドリングしたい場合もあるだろうが、その場合はRequest pathとか(というかenv自体)そういう情報も入れてるので、rescueしてそういうのを見るという設計方針で良さそう。

こんな感じでMiddlewareは簡単に書けて助かる。という話でした。

お知らせ

ノリで 0.0.2 をリリースしました。

yao | RubyGems.org | your community gem host

ConoHaのAPI でも動くんじゃないかという気がする。試してないけど。