Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

VirtualBox+Vagrant+Chef(knife-solo) 環境構築(Windows 7+Cygwin) 苦戦記

前回記事
VirtualBox+Vagrant 環境構築(Windows 7+Cygwin) 苦戦記 - Status Code 303 - See Other

前回記事概要

Windowsでコンソールとして Cygwin を利用し、VirtualBox + Vagrant で仮想環境を作成できるようにした。
このとき、苦戦した内容と解決に至るまでをメモった。

この記事で記述すること

Chef を導入し、前回の仮想環境上にインフラ環境をセットアップする。
しかし、Cygwin で行うために少し面倒な設定が必要になる。

環境

前回に引き続き、Windows + Cygwin 環境で行う。

ソフトウェアについて

VirtualBoxVagrantCygwin

前回記事参照。

Chef?

Chef 社が提供。インフラ設定をコードで記述、設定したいサーバにコード内容を実行するためのツール。
サーバの構築内容をコードとして管理することができる。(Infrastructure as Code)
Chef には大きく2通りの形態があり(Chef-solo/Chef-Server + Chef-Client)、
今回は小規模向けである Chef Solo を用いる。この中でも拡張ユーティリティの knife-solo を用いる。
その他の Chef の種類や特徴については下記を。
あなたに合ったChefはどれ? 〜 おすすめ構成確認チャート #getchef - クリエーションライン株式会社

なんのため?

  • 本番を想定したテスト環境を作成したり、その上で簡単なテストしたりするため。
  • 継続的デリバリー(CD)を実現するため。

 (参考:DevOps時代の開発者のための構成管理入門(終):継続的デリバリ/デプロイを実現する手法・ツールまとめ (1/2) - @IT)

環境構築

今回使用環境

Operating System

  • Windows 7 Professional Service Pack1(64bit)

Console

Application

導入

  • Chef Development Kit Version: 0.13.21
  • chef-client version: 12.9.41
  • berks version: 4.3.2
  • kitchen version: 1.7.3

Ruby gems

  • berkshelf (4.3.2)
  • knife-solo (0.6.0)

手順

前提環境

※自身のCドライブが小さいためデータをEドライブに保存しています。そこは適宜ご自身の環境に置き換えてください。

Chef インストール 手順

gem を使ってインストールする方法もあるようだが、現在ではインストーラを使うのが主流らしい。
基本的に手順は以下を参考にさせてもらった。
Chef for Windows
Windows7にChef(11.6.0)とVagrant(1.7.2)を入れてプロビジョニングしてみた - カタカタブログ

つまずいたところ

インストール先の変更

インストーラがインストール先の選択が出る。ここで「E:\opscode\」を選択。
f:id:kouki_hoshi:20160622013513p:plain
そうするとインストールは完了するが、Cygwin 上で実際に使ってみるとなぜか動かない。
調べるとどうやら、インストーラのバグらしい。(c:\opscode にあることを前提としたコードがある?)
Windows can't install into anything other than C:\opscode · Issue #68 · chef/chef-dk · GitHub

しかたないので、シンボリックリンクを作成し C:\opscode を E:\opscode に関連付けしてから再インストールした。
(再インストールしなくても動くかもだけど念のため)
※ 以下のコマンドに限り、管理者モードで起動したコマンドプロンプトを使用。

> mklink /d C:\opscode E:\opscode

[Windows] シンボリックリンクの作成と削除

Cygwin上のパスを制御する

CygwinLinux 環境をWindows上に作るだけあってそのフォルダ構成が異なる。
これが原因でエイリアスを設定しないと動かない。(コマンドが実行できない)

$ chef -v
C:\opscode\chefdk\embedded\bin\ruby.exe: No such file or directory -- /cygdrive/c/opscode/chefdk/bin/chef (LoadError)

関連するエイリアスを全て設定に書き込み、その設定を現在のセッションに適用する。
(下記ではEドライブだが、Cドライブのファイル名で指定してもOK)

$ echo "alias chef='E:/opscode/chefdk/bin/chef'" >> ~/.bashrc
$ echo "alias knife='E:/opscode/chefdk/bin/knife'" >> ~/.bashrc
$ echo "alias gem='E:/opscode/chefdk/embedded/bin/gem'" >> ~/.bashrc
$ source ~/.bashrc

これで次回から Cygwin を起動するたびに設定される。

knife solo の障害

Chef コマンドは動くようになった。次は knife solo コマンドを使うため下記を実行する。

gem install knife-solo
gem install berkshelf
chef gem install knife-solo

インストール完了後 knife solo コマンドを動かすがどうも動かない。knife configure も同様だ。
どうやら gem で使用するライブラリ(net-ssh)との依存関係が原因のようだ。

$ knife solo init .
C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems/specification.rb:2288:in `raise_if_conflicts': Unable to activate knife-solo-0.5.1, because net-ssh-3.1.1 conflicts with net-ssh (< 3.0, ~> 2.7) (Gem::ConflictError)
        from C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems/specification.rb:1408:in `activate'
        from C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems.rb:200:in `rescue in try_activate'
        from C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems.rb:193:in `try_activate'
        from C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:126:in `rescue in require'
        from C:/opscode/chefdk/embedded/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:40:in `require'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.5.1/lib/chef/knife/cook.rb:1:in `<top (required)>'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:100:in `load'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:100:in `block in load_commands'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:100:in `each'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:100:in `load_commands'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:110:in `load_command'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife/core/subcommand_loader.rb:124:in `command_class_from'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:153:in `subcommand_class_from'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:214:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/knife.rb:148:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/knife:25:in `<top (required)>'
        from E:/opscode/chefdk/bin/knife:61:in `load'
        from E:/opscode/chefdk/bin/knife:61:in `<main>'

knife solo ユーティリティの既知の障害みたい。 Unable to activate knife-solo-0.5.1 · Issue #811 · chef/chef-dk · GitHub
直す方法を探したが、ダウングレード記事が多くあまり気が進まなかった。
ここでしばらく諦めモードに。

しかし、ver 0.6.0 でこの問題が直ったためアップデートして 0.5.1 -> 0.6.0 にアップデートすると動いた

$ knife configure
WARNING: No knife configuration file found
Where should I put the config file? [E:/cygwin/home/hoshikouki/.chef/knife.rb]
Please enter the chef server URL: [https://hoshikouki-PC.flets-east.jp:443]
Please enter an existing username or clientname for the API: [hoshikouki]
Please enter the validation clientname: [chef-validator]
Please enter the location of the validation key: [/etc/chef-server/chef-validator.pem]
Please enter the path to a chef repository (or leave blank):
*****

You must place your client key in:
  E:/cygwin/home/hoshikouki/.chef/hoshikouki.pem
Before running commands with Knife

*****

You must place your validation key in:
  E:/etc/chef-server/chef-validator.pem
Before generating instance data with Knife

*****
Configuration file written to E:/cygwin/home/hoshikouki/.chef/knife.rb
rsync コマンド失敗

Cookbook を作成し、debian-jessieホストを以下のコマンドで設定。

$ vagrant ssh-config --host debian-jessie >> ~/.ssh/config

リモート先で Chef 実行しようと knife solo bootstrap コマンド(prepare + cook コマンド)を実行。

$ knife solo bootstrap debian-jessie
Bootstrapping Chef...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 19602  100 19602    0     0  17371      0  0:00:01  0:00:01 --:--:-- 17377
debian 8 x86_64
Getting information for chef stable 12.9.41 for debian...
downloading https://omnitruck-direct.chef.io/stable/chef/metadata?v=12.9.41&p=debian&pv=8&m=x86_64
  to file /tmp/install.sh.8361/metadata.txt
trying wget...
sha1    2f8503b7177437418337e7ad01064b77aefdafad
sha256  3f54dd6121acaea52620a58cd06f4192cd7b0e6c82f2047f10876494c93ac89d
url     https://packages.chef.io/stable/debian/8/chef_12.9.41-1_amd64.deb
version 12.9.41
downloaded metadata file looks valid...
downloading https://packages.chef.io/stable/debian/8/chef_12.9.41-1_amd64.deb
  to file /tmp/install.sh.8361/chef_12.9.41-1_amd64.deb
trying wget...
Comparing checksum with sha256sum...
Installing chef 12.9.41
installing with dpkg...
(Reading database ... 63807 files and directories currently installed.)
Preparing to unpack .../chef_12.9.41-1_amd64.deb ...
Unpacking chef (12.9.41-1) over (12.9.41-1) ...
Setting up chef (12.9.41-1) ...
Thank you for installing Chef!
Running Chef on debian-jessie...
Uploading the kitchen...
WARNING: Local cookbook_path 'E:/cygwin/home/hoshikouki/cookbooks' does not exist
WARNING: Local role_path '.\roles' does not exist
WARNING: Local data_bag_path '.\data_bags' does not exist
WARNING: Local environment_path '.\environments' does not exist
Generating solo config...
rsync: link_stat "/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160605-18680-1tgoomz" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1196) [sender=3.1.2]
ERROR: RuntimeError: Failed to launch command ["rsync", "-rL", "--chmod=ugo=rwX", "--rsh=ssh vagrant@debian-jessie", "--delete-after", "-zt", "--exclude=revision-deploys", "--exclude=.git", "--exclude=.hg", "--exclude=.svn", "--exclude=.bzr", "/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160605-18680-1tgoomz", ":~/chef-solo/solo.rb"]

最後の行から察するに rsync か。またかい。
原因の切り分けをするために、knife solo prepare コマンド実行。(リモートに Chef solo 環境作成)
処理は成功。次に、knife solo cook コマンド(リモートに cookbook 適用)を実行すると同じエラーが出た。
どうやらここが原因らしい。

そして、調べてみると -VV (Vが2個) がデバッグモードになるらしく、これを使って調べてみた。

$ knife solo cook -VV debian-jessie
INFO: Using configuration from E:/cygwin/home/hoshikouki/chef-repo/.chef/knife.rb
Starting 'Run'
Running Chef on debian-jessie...
Checking Chef version...

(長いので中略)

sent 124 bytes  received 11 bytes  270.00 bytes/sec
total size is 0  speedup is 0.00
DEBUG: 'encrypted_data_bag_secret' not set
DEBUG: ["rsync", "-rL", "-v", "--chmod=ugo=rwX", "--rsh=ssh vagrant@debian-jessie", "--delete-after", "-zt", "--exclude=revision-deploys", "--exclude=.git", "--exclude=.hg", "--exclude=.svn", "--exclude=.bzr", ".\\environments/", ":~/chef-solo/environments"]
building file list ... done

sent 124 bytes  received 11 bytes  270.00 bytes/sec
total size is 0  speedup is 0.00
Generating solo config...
DEBUG: ["rsync", "-rL", "-v", "--chmod=ugo=rwX", "--rsh=ssh vagrant@debian-jessie", "--delete-after", "-zt", "--exclude=revision-deploys", "--exclude=.git", "--exclude=.hg", "--exclude=.svn", "--exclude=.bzr", "/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160607-9868-1vtkfpf", ":~/chef-solo/solo.rb"]
building file list ...
rsync: link_stat "/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160607-9868-1vtkfpf" failed: No such file or directory (2)
done

sent 81 bytes  received 11 bytes  184.00 bytes/sec
total size is 0  speedup is 0.00
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1196) [sender=3.1.2]
C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/knife-solo/tools.rb:4:in `system!': Failed to launch command ["rsync", "-rL", "-v", "--chmod=ugo=rwX", "--rsh=ssh vagrant@debian-jessie", "--delete-after", "-zt", "--exclude=revision-deploys", "--exclude=.git", "--exclude=.hg", "--exclude=.svn", "--exclude=.bzr", "/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160607-9868-1vtkfpf", ":~/chef-solo/solo.rb"] (RuntimeError)
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:279:in `rsync'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:233:in `upload'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:258:in `write'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:229:in `generate_solorb'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:97:in `block in run'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:212:in `time'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:79:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:421:in `block in run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/local_mode.rb:44:in `with_server_connectivity'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:420:in `run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:219:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/knife.rb:148:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/knife:25:in `<top (required)>'
        from E:/opscode/chefdk/bin/knife:61:in `load'
        from E:/opscode/chefdk/bin/knife:61:in `<main>'

やはり rsync のようだ。最初は "Program Files の半角スペースが原因でコマンドが認識できていないのかと思ったが、
配列からコマンドを作成するこの記述方法なら問題なさそうだ。・・・原因分からねえ。

そして、ある時閃いた。

  • cygwin 直下のこのパスってtmpフォルダなんてあったっけ? →残念あった
  • Program Files 以下って書き込み権限あったっけ?     →なかった

ユーザの書き込み権限を「書き込み」許可すると動いた。(tmp フォルダ上で右クリック->プロパティのセキュリティタブ)
どうやら「/cygdrive/C/Program Files/cygwin64/tmp/solo.rb20160607-9868-1vtkfpf」に書き込みができなくてダメだったようだ。
ユーザの書き込み権限をエクスプローラから「書き込み」許可すると先ほどの場所は抜けた。

謎のエラー

rsync は直ったようだが、まだエラーが残っていた。次の問題部分。

DEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout:
        DEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout: from /usr/bin/chef-solo:51:in `<main>'
from /usr/bin/chef-solo:51:in `<main>'DEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout:


C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:315:in `cook': chef-solo failed. See output above. (RuntimeError)
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:99:in `block in run'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:212:in `time'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:79:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:421:in `block in run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/local_mode.rb:44:in `with_server_connectivity'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:420:in `run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:219:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/knife.rb:148:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/knife:25:in `<top (required)>'
        from E:/opscode/chefdk/bin/knife:61:in `load'
        from E:/opscode/chefdk/bin/knife:61:in `<main>'
  • VV オプション付けるとメッセージが違う。不思議。
Generating solo config...
Running Chef: sudo chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json
/opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/config_fetcher.rb:47:in `read': Is a directory @ io_fread - /home/vagrant/chef-solo/solo.rb (Errno::EISDIR)
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/config_fetcher.rb:47:in `read_local_config'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/config_fetcher.rb:36:in `read_config'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application.rb:107:in `load_config_file'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application.rb:85:in `configure_chef'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application.rb:47:in `reconfigure'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/solo.rb:210:in `reconfigure'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application.rb:56:in `run'
        from /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/chef-solo:25:in `<top (required)>'
        from /usr/bin/chef-solo:51:in `load'
        from /usr/bin/chef-solo:51:in `<main>'
ERROR: RuntimeError: chef-solo failed. See output above.

/home/vagrant/chef-solo/solo.rb はディレクトリだよ?
パスを見るに 仮想環境上のようだ。乗り込んで確かめてみると、
たしかに「/home/vagrant/chef-solo/solo.rb」はディレクトリだった。
なんでこんなことになっていたのか・・・。謎のディレクトリを削除してみると、進んだ。

$ sudo rm -rf /home/vagrant/chef-solo/solo.rb
リソースが見つからない。

Cookbook に作成した hello リソースが見つからないらしい。

/usr/bin/chef-solo:51:in `<main>'
[2016-06-07T15:43:59+00:00] ERROR: Cookbook hello not found. If you're loading hello from another cookbook, make sure you configure the dependency in your metadata
DEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout: [2016-06-07T15:43:59+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)

[2016-06-07T15:43:59+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:315:in `cook': chef-solo failed. See output above. (RuntimeError)
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:99:in `block in run'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:212:in `time'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:79:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:421:in `block in run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/local_mode.rb:44:in `with_server_connectivity'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:420:in `run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:219:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/knife.rb:148:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/knife:25:in `<top (required)>'
        from E:/opscode/chefdk/bin/knife:61:in `load'
        from E:/opscode/chefdk/bin/knife:61:in `<main>'

ためしに dstat リソースを新しく作成して同様の手順を行う。しかし、結果は同じ。

DEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout: [2016-06-07T15:54:55+00:00] ERROR: Cookbook dstat not found. If you're loading dstat from another cookbook, make sure you configure the dependency in your metadata
[2016-06-07T15:54:55+00:00] ERROR: Cookbook dstat not found. If you're loading dstat from another cookbook, make sure you configure the dependency in your metadataDEBUG: sudo -p 'knife sudo password: ' chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json -l debug stdout:

[2016-06-07T15:54:55+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:315:in `cook': chef-solo failed. See output above. (RuntimeError)
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:99:in `block in run'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:212:in `time'
        from C:/Users/hoshikouki/AppData/Local/chefdk/gem/ruby/2.1.0/gems/knife-solo-0.6.0/lib/chef/knife/solo_cook.rb:79:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:421:in `block in run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/local_mode.rb:44:in `with_server_connectivity'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:420:in `run_with_pretty_exceptions'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/knife.rb:219:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/application/knife.rb:148:in `run'
        from C:/opscode/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/bin/knife:25:in `<top (required)>'
        from E:/opscode/chefdk/bin/knife:61:in `load'
        from E:/opscode/chefdk/bin/knife:61:in `<main>'

作成したリソースを Chef が認識していないようだ。

  1. Berkshelf が悪さしている? (違った)
    chef (11.6.0) + knife-solo (0.3.0.pre5)は相性が悪いっぽい? - じゅにゃくんのはてブロ。
  2. cookbook の設定を書く場所が悪かった (ビンゴ)
    knife-soloによるChefの実行 - Qiita
以下を .chef/knife.rb (knife configure を実行したフォルダ直下)に記述。

cookbook_path    ["cookbooks", "site-cookbooks"]
node_path        "nodes"
role_path        "roles"
environment_path "environments"
data_bag_path    "data_bags"
#encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"

※ これを ~/.chef/knife.rb (ホームディレクトリ直下)に書いてたから動かなかった

そして、 knife solo cook が成功。(gitリソースまで作成してから行ってます)

$ knife solo cook debian-jessie
Running Chef on debian-jessie...
Checking Chef version...
Installing Berkshelf cookbooks to 'E:/cygwin/home/hoshikouki/.berkshelf/knife-solo/741e61ac3fba69a26087f2d0c26ffc30e967a5f2'...
Resolving cookbook dependencies...
Uploading the kitchen...
WARNING: Local cookbook_path 'E:/cygwin/home/hoshikouki/.berkshelf/knife-solo/741e61ac3fba69a26087f2d0c26ffc30e967a5f2' does not exist
Generating solo config...
Running Chef: sudo chef-solo -c ~/chef-solo/solo.rb -j ~/chef-solo/dna.json
Starting Chef Client, version 12.9.41
[2016-06-07T17:10:27+00:00] WARN: The cookbook(s): dstat exist in multiple places in your cookbook_path. A composite version has been compiled.  This has been deprecated since 0.10.4, in Chef 13 this behavior will be REMOVED. at /opt/chef/embedded/lib/ruby/gems/2.1.0/gems/chef-12.9.41/lib/chef/cookbook_loader.rb:89:in `load_cookbooks'
Installing Cookbook Gems:
Compiling Cookbooks...
Converging 3 resources
Recipe: dstat::default
  * apt_package[dstat] action remove (up to date)
  * apt_package[git] action install (up to date)
Recipe: hello::default
  * log[hello world!!!!!] action write


Running handlers:
Running handlers complete

Ruby on Rails チュートリアル2章

概要

Ruby の Web Framework である Ruby on Railsチュートリアルについてまとめる。
対象読者としては、チュートリアルを一度は読んだけどチュートリアルからいちいちコマンドなどを再確認するのが面倒な場合など。

チュートリアル
第1章 ゼロからデプロイまで | Rails チュートリアル

前回記事
Ruby on Rails チュートリアル1章 - Status Code 303 - See Other

使用環境

※ 全部基本的に無料

IDE (Integrated Development Environment)

Cloud 9 (Cloud9 - Your development environment, in the cloud)

  • クラウド上で作業できる開発環境、最近はこんなサービスもあるんですね・・
  • 基本的にコンソール上で作業するので、GUI しか使えない人は厳しいかも?
  • ファイルの編集には vi を使用

バージョン管理

Git

  • おなじみのバージョン管理ツール
  • SVN(subversion)との違いは、ローカルでも自由に作業できたり、ネット不通でも開発できる環境だったり。

Gitリポジトリ

Bitbucket

  • 少人数開発であればプライベート環境(他人に公開しない)が無料で使える。
  • Github と基本的に提供機能は同じ。

事前準備 (前回記事とほぼ同じ)

今回は、前回のアプリは扱わないため、新しくプロジェクトを作成する。
なお、今回は チュートリアルに習い、プロジェクト名を toy_app として記述する。

プロジェクト作成
$ cd ~/workspace
$ rails _4.2.2_ new toy_app
$ cd toy_app/
Gemfile 置換
$ rm ~/workspace/toy_app/Gemfile
$ vi ~/workspace/toy_app/Gemfile

以下をコピペで貼付けて作成。

source 'https://rubygems.org'

gem 'rails',        '4.2.2'
gem 'sass-rails',   '5.0.2'
gem 'uglifier',     '2.5.3'
gem 'coffee-rails', '4.1.0'
gem 'jquery-rails', '4.0.3'
gem 'turbolinks',   '2.3.0'
gem 'jbuilder',     '2.2.3'
gem 'sdoc',         '0.4.0', group: :doc

group :development, :test do
  gem 'sqlite3',     '1.3.9'
  gem 'byebug',      '3.4.0'
  gem 'web-console', '2.0.0.beta3'
  gem 'spring',      '1.1.3'
end

group :production do
  gem 'pg',             '0.17.1'
  gem 'rails_12factor', '0.0.2'
end
本番環境用のgemを除いたローカルgemをインストール
$ bundle install --without production
Gitの管理下におく
$ git init
$ git add -A
$ git commit -m "Initialize repository"
Bitbucket 登録

リボジトリ作成 (bitbucket のサイトで作成)
次に以下のコマンドで作成

$ git remote add origin git@bitbucket.org:${USER_NAME}>/toy_app.git
$ git push -u origin --all 
Applicationコントローラにhelloを追加
デフォルトページを変更する

2章内容(名前変更必要)

作成するデータ定義

ユーザ情報 (users)

  • id :integer
  • name :string
  • email :string

マイクロポスト (microposts)

  • id :integer
  • content :text
  • user_id :integer

Userリソース作成

Railsのscaffoldを用いて作成する。

$ rails generate scaffold User name:string email:string

Rakeを使用してデータベースをマイグレート(データベース更新・usersデータモデル作成)
※ rake db:migrate でもいける場合もあるがシステム依存らしい

$ bundle exec rake db:migrate

サーバ起動して追加リソースを見る

サーバ起動

ローカル

$ rails server  # ローカル環境
$ rails server -b $IP -p $PORT  # Cloud 9
リソース先にアクセス

下記のRestful APIが自動生成される。
アクセス可能なページ(URL)一覧

メソッドURLアクション用途
GET/usersindexすべてのユーザーを表示するページ
GET/users/1showid=1のユーザーを表示するページ
GET/users/newnewユーザーを新規作成するページ
GET/users/1/editeditid=1のユーザーを編集するページ
その他利用可能なアクション。
メソッドURLアクション用途
POST/userscreateユーザーを作成するアクション
PATCH/users/1updateid=1のユーザーを更新するアクション
DELETE/users/1destroyid=1のユーザーを削除するアクション

(筆者追記) 動作実行前との差分を知りたければ $ git status を実行する。

リソースの自動生成について

挙動説明

プログラムとDBがどのように連携しているかの説明。本記事は手順書的な意味合いが強いので割愛。

挙動確認・変更方法

上記リソースのアクセス先ルーティングを変更したい場合、routers.rbを変更する。

vi config/routes.rb
Rails.application.routes.draw do
  resources :users
  # The priority is based upon order of creation: first created -> highest priority.
  # See how all your routes lay out with "rake routes".

  # You can have the root of your site routed with "root"
  root 'users#index' # 'application#hello から変更
(以下省略)

もし、さきほどの Restful API の各挙動を知りたければ、app/controllers/users_controller.rb を参照する。

$ less app/controllers/users_controller.rb
自動生成したリソースの欠点
  • データ検証がされていない
  • 誰でも作成・参照・更新・削除可能
  • 業務要件を満たすテストが存在しない
  • レイアウトやスタイルはデフォルト
  • 理解が困難

micropostsリソース作成

microposts リソース作成

基本的にUserリソースと同じ。

$ rails generate scaffold Micropost content:text user_id:integer
$ bundle exec rake db:migrate
動作確認

挙動変更

マイクロポストの文字数に制限をかける。

vi app/models/micropost.rb
class Micropost < ActiveRecord::Base
  validates :content, length: { maximum: 140 }  # 追加
end

データモデル同士の関連付け

※ユーザとユーザ ID が一致した microposts を事前に作成しておくこと。

user.rb を編集する
$ vi app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts  # 追加
end
micropost.rb を編集する
vi app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user  # 追加
  validates :content, length: { maximum: 140 }  # 追加
end
関連付け確認方法

rails console を使用して確認する。

$ rails console
>> first_user = User.first
>> first_user.microposts

コンソール実行例 (この例では、ユーザが二人いて二人目がデータ持っていた)

2.3.0 :003 > second_user = User.second
  User Load (0.4ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1
 => #<User id: 8, name: "XYZ", email: "xyz@aaa.com", created_at: "2016-07-02 07:37:50", updated_at: "2016-07-02 07:37:50"> 
2.3.0 :004 > second_user.microposts
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 8]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Micropost id: 2, content: "あああああ", user_id: 8, created_at: "2016-07-02 07:44:23", updated_at: "2016-07-02 07:44:23">, #<Micropost id: 3, content: "AAAAAAAAAAAA", user_id: 8, created_at: "2016-07-02 07:44:56", updated_at: "2016-07-02 07:44:56">]>

Ctrl + D で終了する。
※ なお、user.rb で設定していないと例外が発生する。

2.3.0 :002 > second_user.microposts
NoMethodError: undefined method `microposts' for #<User:0x007faa00082788>
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activemodel-4.2.2/lib/active_model/attribute_methods.rb:433:in `method_missing'
        from (irb):2
        from /usr/local/rvm/gems/ruby-2.3.0/gems/railties-4.2.2/lib/rails/commands/console.rb:110:in `start'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/railties-4.2.2/lib/rails/commands/console.rb:9:in `start'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/railties-4.2.2/lib/rails/commands/commands_tasks.rb:68:in `console'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/railties-4.2.2/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/railties-4.2.2/lib/rails/commands.rb:17:in `<top (required)>'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `block in require'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:240:in `load_dependency'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:274:in `require'
        from /home/ubuntu/workspace/toy_app/bin/rails:9:in `<top (required)>'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `load'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `block in load'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:240:in `load_dependency'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activesupport-4.2.2/lib/active_support/dependencies.rb:268:in `load'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/commands/rails.rb:6:in `call'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/command_wrapper.rb:38:in `call'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:180:in `block in serve'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:153:in `fork'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:153:in `serve'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:128:in `block in run'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:122:in `loop'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application.rb:122:in `run'
        from /usr/local/rvm/gems/ruby-2.3.0/gems/spring-1.1.3/lib/spring/application/boot.rb:18:in `<top (required)>'
        from /usr/local/rvm/rubies/ruby-2.3.0/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /usr/local/rvm/rubies/ruby-2.3.0/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from -e:1:in `<main>'2.3.0 :003 > 

継承階層

作成したモデル(Users, Microposts) は ActionController::Base を継承する。
このため、作成したモデルはデータベースにアクセスしカラムをあたかもRubyの属性のように扱える。
また、作成したモデルコントローラ(UsersController, MicropostController) は ApplicationController を継承する。
このため、ApplicationControllerで定義したルールはすべてのコントローラに反映される。

デプロイ

ローカルに変更をコミットし、bitbucket に push する

$ git status
$ git add -A
$ git commit -m "Finish toy app"
$ git push

herokuにデプロイ。

$ git push heroku    # デプロイ
$ heroku run rake db:migrate   # マイグレーション

Ruby on Rails チュートリアル1章

概要

Ruby の Web Framework である Ruby on Railsチュートリアルについてまとめる。
対象読者としては、チュートリアルを一度は読んだけどチュートリアルからいちいちコマンドなどを再確認するのが面倒な場合など。

チュートリアル
第1章 ゼロからデプロイまで | Rails チュートリアル

使用環境

※ 全部基本的に無料

IDE (Integrated Development Environment)

Cloud 9 (Cloud9 - Your development environment, in the cloud)

  • クラウド上で作業できる開発環境、最近はこんなサービスもあるんですね・・
  • 基本的にコンソール上で作業するので、GUI しか使えない人は厳しいかも?
  • ファイルの編集には vi を使用

バージョン管理

Git

  • おなじみのバージョン管理ツール
  • SVN(subversion)との違いは、ローカルでも自由に作業できたり、ネット不通でも開発できる環境だったり。

Gitリポジトリ

Bitbucket

  • 少人数開発であればプライベート環境(他人に公開しない)が無料で使える。
  • Github と基本的に提供機能は同じ。

1章内容

Ruby on Rails の知識・チュートリアルの基本的な取り決めなど

Rails こんなすごい

チュートリアル実施の前提知識

  • 基本的に前提知識はなくても大丈夫。もちろん知っているに越した事はない。
  • 個人的には、プログラミングを過去に書いた事ある人ならできる内容
    足りない所は適宜補完する(Ruby, HTML, CSS, JavaScript, SQLなど)
  • 記法の説明、流し読みすれば OK
    • コンソール上のコマンドの書き方
    • パスの記述
    • プログラムの書き方。省略記号をそのままソースコードに記述しないこと!
とりあえず動かしてみる

上記使用環境の利用方法説明がひたすら続く
※ 当然だけど、各サービスのWeb画面もいつかは変化するので適宜読み替える
※ 事前にローカル環境もしくは Cloud 9 のどちらで開発するか決めること。個人的に Cloud 9 の方が環境依存なくて良いと思う。

  1. Ruby の導入 (※Cloud9 は不要)
    Windows では ruby がデフォルトで入ってないので導入する。
    Mac OS はデフォルトで入ってるけど、バージョン古いかも。(私の環境では Ruby 2.0.0)
    Ruby のインストール確認方法

    $ ruby -v
    ruby 2.0.0p481 (2014-05-08 revision 45883) [universal.x86_64-darwin13]


  2. Rails をインストール

    $ gem install rails -v 4.2.2


  3. プロジェクト用フォルダ作成、そこにプロジェクト作成

    $ APP_NAME=アプリ名をココにいれて!
    $ mkdir ~/workspace
    $ cd ~/workspace
    $ rails _4.2.2_ new ${APP_NAME}


  4. Gemfile の置換

    $ rm ~/workspace/${APP_NAME}/Gemfile
    $ vi ~/workspace/${APP_NAME}/Gemfile

    以下をコピペで貼付けて作成。

    source 'https://rubygems.org'
    
    gem 'rails',                '4.2.2'
    gem 'sass-rails',           '5.0.2'
    gem 'uglifier',             '2.5.3'
    gem 'coffee-rails',         '4.1.0'
    gem 'jquery-rails',         '4.0.3'
    gem 'turbolinks',           '2.3.0'
    gem 'jbuilder',             '2.2.3'
    gem 'sdoc',                 '0.4.0', group: :doc
    
    group :development, :test do
      gem 'sqlite3',     '1.3.9'
      gem 'byebug',      '3.4.0'
      gem 'web-console', '2.0.0.beta3'
      gem 'spring',      '1.1.3'
    end


  5. Rails サーバを起動する
    サーバ起動はそれ専用の端末になってしまうため、別タブ・別ウィンドウで起動すること。

    $ APP_NAME=アプリ名をココにいれて!
    $ cd ~/workspace/${APP_NAME}
    $ rails server

    Cloud 9 の場合

    $ APP_NAME=アプリ名をココにいれて!
    $ cd ~/workspace/${APP_NAME}
    $ rails server -b $IP -p $PORT

    なお、APP_NAME を2回いれるのが面倒な場合は、以下のコマンドを実行する事。
    これによって、ターミナル起動時に設定が読み込まれるため、別タブに移動しても同様の操作が可能になる。
    Mac OSXの場合

    echo "APP_NAME=hello_app" >> ~/.bash_profile

    Linux環境の場合

    echo "APP_NAME=hello_app" >> ~/.bashrc


  6. 動作確認
    ローカル環境なら
    http://localhost:3000/ にアクセスし、以下のようなデフォルトページが表示されれば OK。
    f:id:kouki_hoshi:20160611163613p:plain
    Cloud 9 であれば、[Share]ボタンをクリックして、環境設定中の[Application]にある URL にアクセスする。
    f:id:kouki_hoshi:20160611163120p:plain

デフォルトページに Hello World 表示

  1. コントローラにweb表示時の挙動を記述。

    $ vi ~/workspace/${APP_NAME}/app/controllers/application_controller.rb

    hello という挙動を定義する。(hello, worldって表示するだけ)

    class ApplicationController < ActionController::Base
      # Prevent CSRF attacks by raising an exception.
      # For APIs, you may want to use :null_session instead.
      protect_from_forgery with: :exception
      def hello                               # hello メソッド追加 (3行)
        render text: "hello, world!"  # hello, world を表示
      end
    end


  2. 現在のアプリケーションのルーティング情報を設定する。

    vi ~/workspace/${APP_NAME}/config/routes.rb

    アプリケーションルート(http://localhost:3000/など)に hello の挙動を設定する。

    Rails.application.routes.draw do
      # The priority is based upon order of creation: first created -> highest priority.
      # See how all your routes lay out with "rake routes".
    
      # You can have the root of your site routed with "root"
      root 'application#hello'         # ApplicationController クラスの hello メソッドを呼び出す
    (以下省略)



  3. 再度動作確認。hello, world って書かれてればOK。
    f:id:kouki_hoshi:20160611165624p:plain

開発におけるソースコード管理からデプロイまで

以下は、Ruby on Rails 以外に関わる内容のため、概要のみ。

ソースコードのバージョン管理

初回作業

Git 設定

  1. Git インストール (※ Cloud 9 では不要)
  2. Git 設定
    バージョン情報で誰が更新したかの情報などに使われる。

    $ git config --global user.name "Your Name"
    $ git config --global user.email your.email@example.com
    $ git config --global push.default matching


Bitbucket 設定
  1. Bitbucket 登録
    Bitbucket — The Git solution for professional teams
  2. 公開鍵作成(※ 既に存在する場合・Cloud 9 では不要)

    $ ssh-keygen -t rsa
    Generating public/private rsa key pair.
    Enter file in which to save the key (/Users/XXXXXX/.ssh/id_rsa): 
    Created directory '/Users/XXXXXX/.ssh'.
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in /Users/XXXXXX/.ssh/id_rsa.
    Your public key has been saved in /Users/XXXXXX/.ssh/id_rsa.pub.
    The key fingerprint is:
    be:e9:16:45:e1:56:d3:e3:01:61:0e:ac:3e:61:42:18 XXXXXX@YYYYY-ZZZZZ.local
    The key's randomart image is:
    +--[ RSA 2048]----+
    |                 |
    |                 |
    | .   .           |
    |    E+           |
    | o =   o S       |
    |  + B o.         |
    | o = B  .        |
    |  o * o .        |
    |   o . Bo+       |
    +-----------------+

    公開鍵の内容出力

    $ cat ~/.ssh/id_rsa.pub

    公開鍵を Bitbucket ->[アカウントの管理]->[SSH キー] に登録


ローカル環境の Git と Bitbucket 連携
  1. Git レポジトリ作成

    $ git init
    $ git add -A
    $ git commit -m "コミット内容をココに記述する"


  2. BitBucket にリモートリポジトリ作成

  3. ローカルリポジトリとリモートリポジトリの関連付け

    $ git remote add origin git@bitbucket.org:${USER_NAME}/${REPOSITORY_NAME}.git
    $ git push -u origin --all


開発作業手順

  1. トピックブランチ作成 + 現ブランチ確認

    $ git checkout -b ${BRANCH_NAME}
    $ git branch 


  2. ソースコード修正

  3. 現ブランチの状態確認
    以下の例では、変更状態にあることが示されている。

    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
            modified:   toy_app/app/controllers/application_controller.rb
    
    no changes added to commit (use "git add" and/or "git commit -a")


  4. 変更をステージ/コミット
    ステージ - 変更をコミット対象にする(コミットはまだされない)
    コミット - 変更をブランチの最新状態にする
    特定のファイルをステージ段階にする

    $ git add ${TARGET_FILES}

    全てのファイルをステージ段階にする

    $ git add -A

    ステージ対象をコミット

    $ git commit -m "コミット内容"

    全変更対象をステージ段階をすっとばしコミット (ただし新しく追加したファイルは適用されない)

    $ git commit -a -m "コミット内容"


  5. トピックブランチの変更をマスタにマージ

    $ git checkout master
    $ git merge ${BRANCH_NAME}


  6. トピックブランチ削除

    $ git branch -d ${BRANCH_NAME}


  7. ローカルリポジトリの内容をリモートに反映する

    $ git push


デプロイ

ここでは、Heroku を利用したデプロイを記述する。

初回作業

  • Gemfile 設定追加
    DB に PostgreSQL を利用するため、この内容を Gemfile に記述。

    $ vi ~/workspace/${APP_NAME}/Gemfile

    末尾に4行追加。

    source 'https://rubygems.org'
    
    gem 'rails',        '4.2.2'
    gem 'sass-rails',   '5.0.2'
    gem 'uglifier',     '2.5.3'
    gem 'coffee-rails', '4.1.0'
    gem 'jquery-rails', '4.0.3'
    gem 'turbolinks',   '2.3.0'
    gem 'jbuilder',     '2.2.3'
    gem 'sdoc',         '0.4.0', group: :doc
    
    group :development, :test do
      gem 'sqlite3',     '1.3.9'
      gem 'byebug',      '3.4.0'
      gem 'web-console', '2.0.0.beta3'
      gem 'spring',      '1.1.3'
    end
    
    # 下記4行追加 
    group :production do
      gem 'pg',             '0.17.1'
      gem 'rails_12factor', '0.0.2'
    end

    ローカル環境で追加した内容(production)を反映しないように bundler に設定
    (さらに、この変更をコミット)

    $ bundle install --without production
    $ git commit -a -m "Update Gemfile.lock for Heroku"


  • Heroku アカウント登録
    Heroku

  • Heroku Toolbelt インストール (Cloud 9 の場合不要)
    Heroku Command Line | Heroku Dev Center

  • Heroku CLI がインストール済か確認

    $ heroku version
    heroku-toolbelt/3.43.3 (x86_64-linux) ruby/2.3.0
    heroku-cli/5.2.14-a56a9ae (linux-amd64) go1.6.2
    You have no installed plugins.


  • Heroku に新しいインスタンスを作成
    Heroku ログイン + 認証鍵追加 + インスタンス作成

    $ heroku login
    $ heroku keys:add
    $ heroku create


アプリケーションを Heroku にデプロイする

  1. デプロイ方法

    $ git push heroku master


  2. デプロイしたアプリケーションの挙動確認
    heroku create 時のコマンドからアクセスすべき ホストが分かるので、
    これを元にアクセスして確認する。(以下の例では、http://damp-fortress-5769.herokuapp.com/ になる)

    $ heroku create
    Creating damp-fortress-5769... done, stack is cedar
    http://damp-fortress-5769.herokuapp.com/ | git@heroku.com:damp-fortress-5769.git
    Git remote heroku added


VirtualBox+Vagrant 環境構築(Windows 7+Cygwin) 苦戦記

概要

この記事で記述すること

Windows+Cygwin 上で Vagrant 環境を作成したときに苦戦した内容をメモったもの。

環境

今回は、Windows + Cygwin 環境で行う。
※ 構築 PC の CPU が仮想化技術をサポートしていない場合、多分できません。(下記参照)

ソフトウェアについて

VirtualBox

仮想化環境を提供するソフトウェア。その他、有名なソフトウェアには VMWare がある。
Oracle が提供している。基本的に無料で使える。

Vagrant

Virtual Box を使いやすくするためのソフトウェア。OSS であるため無料で使える。

Cygwin

Windows 上に Linux のコンソールっぽい環境を提供するソフトウェア。DOS が使えないので、結構使ってる。

Windows

た・・大変申し訳ありませんが、多分その方にはこの記事の内容はおそらく理解できないです。(^^;

なんのため?

  • 本番を想定したテスト環境を作成したり、その上で簡単なテストしたりするため。
  • 継続的デリバリー(CD)を実現するため。

 (参考:DevOps時代の開発者のための構成管理入門(終):継続的デリバリ/デプロイを実現する手法・ツールまとめ (1/2) - @IT)

環境構築

今回使用環境

Operating System

  • Windows 7 Professional Service Pack1(64bit)

Console

  • Cygwin (mintty 2.2.2)
    • 英語だけど文献いっぱいあるからなんとかなる。(cygwin 導入 - Google 検索)
    • 個人的に使いやすいと思ってるから使ってる

Vagrant を使うのに必要なアプリケーション

導入

構築手順

Cygwin Install

Cygwin にアクセスし、下記画面の場所からダウンロードする。
f:id:kouki_hoshi:20160610001654p:plain
ダウンロードできたら、起動してインストールする。ココで注意点としては、Choose Downloaded Sites で
末尾が【.jp】のものを選ぶこと。他でやるとネットワークが遅くて時間がかかってしまう。
f:id:kouki_hoshi:20160610001835p:plain
あとは、インストールした後にパッケージで sshrsync を Skip ステータスから変更してインストールする。
f:id:kouki_hoshi:20160610002033p:plainf:id:kouki_hoshi:20160610002030p:plain

で。事前準備は完了

VirtualBox + Vagrant Install

これらの環境セッティングは世の中にいっぱいあるので、それらに任せる。
注意点として、Ruby のパッケージ管理ツール gem からの Vagrant 提供は終了しているらしいので、
インストーラでインストールすること。(参考:Vagrant 1.1.x のインストール - oooooooo)

なお、私はこのサイトを参考にさせてもらった。操作も少なく、かなり簡単。
Windows上でVirtualBox+Vagrant+CentOSによる仮想環境構築 - Qiita

起動失敗

仮想化設定

問題なくインストールは終了したのだが、起動しても仮想マシンが起動しない。
「仮想化支援機能(VT-z/AMD-V)を有効化できません。・・・・」ってアラートが出る。
(同一事象:仮想化支援機構(VT-x/AMD-V)を有効化できません | Futurismo)

調べてみると、マザボBIOS から CPU の仮想化設定を有効にしないといけないそうだ。
システム設定 | VirtualBox Mania
CPU は Intel core i7-3770K。仕様を調べるとサポートしてた。(Intel® Core™ i7-3770K Processor (8M Cache, up to 3.90 GHz) 仕様)
f:id:kouki_hoshi:20160610005349p:plain

で。BIOS から探し出して有効化すると起動。
もし、サポートしてない場合は・・諦めるか、CPU 取り替えましょう。

rsync 実行が失敗

インストール後は Cygwin のコマンドで仮想化などの操作を実行できる。

また、仮想化用の OS は box と呼ばれており、以下のサイトで入手できる。

私は、Raspberry Pi の OS が raspbian jessie なので、そのテスト環境 Debian jessie を入れることに。
(実際は少し違うかもしれないけど)

仮想マシン起動コマンド

$ vagrant box add "debian-jessie" https://github.com/holms/vagrant-jessie-box/releases/download/Jessie-v0.1/Debian-jessie-amd64-netboot.box
$ vagrant init debian-jessie
$ vagrant up

で、エラーがでると。

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'debian/jessie64' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default: Warning: Remote connection disconnect. Retrying...
    default: Warning: Remote connection disconnect. Retrying...
==> default: Machine booted and ready!
GuestAdditions 5.0.20 running --- OK.
==> default: Checking for guest additions in VM...
==> default: Rsyncing folder: /cygdrive/e/cygwin/home/hoshikouki/ => /vagrant
There was an error when attempting to rsync a synced folder.
Please inspect the error message below for more info.

Host path: /cygdrive/e/cygwin/home/hoshikouki/
Guest path: /vagrant
Command: rsync --verbose --archive --delete -z --copy-links --chmod=ugo=rwX --no-perms --no-owner --no-group --rsync-path sudo rsync -e ssh -p 2222 -o ControlMaster=auto -o ControlPath=C:/Program Files/cygwin64/tmp/ssh.363 -o ControlPersist=10m -o StrictHostKeyChecking=no -o IdentitiesOnly=true -o UserKnownHostsFile=/dev/null -i 'E:/cygwin/home/hoshikouki/.vagrant/machines/default/virtualbox/private_key' --exclude .vagrant/ /cygdrive/e/cygwin/home/hoshikouki/ vagrant@127.0.0.1:/vagrant
Error: ssh: Could not resolve hostname files/cygwin64/tmp/ssh.363: Name or service not known
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(226) [sender=3.1.2]

rsync コマンドが失敗したみたい。
「ControlPath=C:/Program Files/cygwin64/tmp/ssh.363」で半角スペースが混入してるのが問題になっている。
どうやら cygwin 環境で実行すると起こる問題らしく、ソースコードを直接編集して対処。
Windows + vagrant 1.8.1 + rsync 3.1.1 で発生する、config.vm.synced_folderの失敗の直し方 - Qiita

ruby のアプリケーションはインタプリタだから最悪手で調整できるのはありがたい。
もし、壊したら再度インストールすればいいんだし。(^-^;

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'debian/jessie64' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default: Warning: Remote connection disconnect. Retrying...
    default: Warning: Remote connection disconnect. Retrying...
==> default: Machine booted and ready!
GuestAdditions 5.0.20 running --- OK.
==> default: Checking for guest additions in VM...
==> default: Rsyncing folder: /cygdrive/e/cygwin/home/hoshikouki/ => /vagrant
==> default: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> default: flag to force provisioning. Provisioners marked to run always will still run.

以下コマンドで動作確認。

$ vagrant ssh

rsync を使って共有する場所は、デフォルト設定では、カレントディレクトリになるっぽい。
実際に共有するフォルダは独自に設定したいので カレントディレクトリにある Vagrantfile に以下の設定を追加する。

  config.vm.synced_folder "./vagrant_data/", "/vagrant"

Basic Usage - Synced Folders - Vagrant by HashiCorp

さらなる応用的情報

ドキュメント

英語だけど、本家ということもあり最も信用できる情報。
Documentation - Vagrant by HashiCorp

プラグイン sahara

OS の現在の状態を管理できる Vagrantプラグイン
これを使えば途中の状態をセーブできるため、便利らしい。(まだ使ったことがない・・)

オブジェクト返却メソッドの異常系実装

概要

今回は、メソッド仕様の話。
オブジェクト値を返却するメソッドにおいて、入力値・プログラム状態によって異常な結果になったときにそれを示したい。
その方法として、以下の3通りが考えられる。

これらの使いどころやそれぞれの利点について記述する。

詳細内容

実装内容

コンソールアプリケーションでユーザが入力した文字列からコマンド(コード内に定義)を実行する。
実行例

ls
カレントのファイルリスト出力
ls /usr/local/bin
/usr/local/bin ディレクトリのファイルリスト出力
rm /usr/local/bin
/usr/local/binの削除
aaa
不正なコマンド aaa

基本実装

※ この後実装するため getCommandメソッドを空実装にしている。

package com.example1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	/**
	 * コマンド定義.
	 *
	 */
	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		};
		public abstract void execute(String... params);
	}

	/**
	 * メインロジック.標準入力からコマンドを入力して,登録された処理を実行する.
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			getCommand(elements[0]).execute(elements);
		}
	}
	/**
	 * 入力文字列から処理するコマンドを選択する.
	 * @param cmd コマンド文字列
	 * @return コマンド実体
	 */
	public static Command getCommand(String cmd) {
		return null; // TODO: 空実装
	}
}

getCommandメソッドの異常系処理、それに伴うロジック部の対応

登録されていないコマンドを指定した場合の処理を3通りの方法を実装する。

null を返却する場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return null;
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			Command cmd = getCommand(elements[0]);
			if(cmd != null) {
				cmd.execute(elements);
			}else {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

例外を発生させる場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		throw new IllegalArgumentException(cmd + " is not defined.");
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			try {
				getCommand(elements[0]).execute(elements);
			}catch (IllegalArgumentException e) {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

NULL オブジェクトを返却する場合

NULLオブジェクトの定義

	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		},
		// NULL オブジェクト追加
		UNDEFINED {
			@Override
			public void execute(String... params) {
				System.out.println("不正なコマンド " + params[0]);
			}
		};
		public abstract void execute(String... params);
	}

getCommandメソッド修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return Command.UNDEFINED;
	}

ロジック部修正

変更不要

それぞれの実装利点/欠点

null 返却

利点

  • 結果が存在しないことを意図的に示せる。
  • オブジェクトを作成しないため、メモリ領域の節約になる。

欠点

  • メソッド利用者の処理で null チェックが必要になると、利用者の処理が複雑化する。
  • メソッド利用者が意図しない場所で NullPointerException が発生する可能性がありデバッグに大きなコストがかかる可能性あり。

例外発生

利点

  • 処理が続行できない場合、その後の処理を中断できるためメソッド利用者のコードがシンプルになる。
  • チェック例外をスローすれば、異常時対応をメソッド利用者に通知できる。

欠点

  • 実行時例外をスローすると、メソッド利用者が意図せず処理が中断することがある。
  • チェック例外をスローすると、異常時対応を強制するため、メソッド利用者のコードが複雑化する。

NULL オブジェクト返却

利点

  • null チェックが必要ないため、利用者のコードがシンプルになる。
  • 関連した処理に格納されるため、コードが見やすくなる
  • 利用者のコードが複数あっても、お決まりの処理の記述が其々で不要なため、冗長性がなくなり保守性が向上する。

欠点

  • 事前に定義する必要がある。

まとめ

null 返却の利点は基本的に大きな利点にならないため、意図的に null を返す場合は少ない。
また、null を返すことは思わぬバグを生む可能性があるため余程の意図がない限り行うべきではない。

基本的には、NULL オブジェクトで実装する方が保守性が良くなる。
しかし、全メソッドに NULL オブジェクトを作成しても、多くの NULL オブジェクト定義ができるだけで、あまり効果はない。
つまり、それほど重要でない処理にまでこのような実装をする必要はない。

例外を返す場合、その処理が「本質的にどういう処理ものであるべきか」に基づいて決定すべき。
その処理が仕様として、起こりえなかったり・明らかに不自然であれば、例外で実装するのが良い。
例外仕様設計 - Status Code 303 - See Other

また、異常時に返す値・その意図をドキュメントに示す方が良い(特に null を返す場合)。
そして、これらを意識することによって複数人開発においては以下の利点が見込まれる。

  • バグの少ない品質の良いコード
  • デバッグに要する時間が少ない保守面で優れたコード
  • 開発者に異常系を意識したコーディングの促進


一通りコードが書けるようになった人は、次はメソッド仕様として
このようなことを考えてコーディングできるようにしてみよう!

if-else 文と条件演算子(?:)の使い分け

概要

プログラミングを行ううえで、複数の選択があり基本的にどちらでも実装できる処理は数多くある。
これらは常にどっちを使うではなく、これらはプログラムが対象とする処理の内容に応じて使い分けるべきである。

今回は、その中でも分岐を表す方法として if-else 文と条件演算子の使い分けについて記述する。

詳細説明

記述法の違い

ここでは、ある条件に応じて、文字列変数 val を分岐させる処理を記述する。

if-else文
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = null;
	if(condition) {
		// 真の場合の処理
		val = "真の場合の値";
	}else {
		// 偽の場合の処理
		val = "偽の場合の値";
	}
条件演算子
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = condition ? "真の場合の値" : "偽の場合の値"; // 値を分岐

それぞれの利点/欠点

if-else 文

  • void 型命令も記述可能
  • 処理を複数記述できる

条件演算子

  • 乱用しなければ読みやすい
  • 事前に変数宣言する必要がなし(変数が冗長にならない)

どっちを使うべきか?

if-else 文は処理を分岐させることができ、条件演算子は値を分岐させるために用いる。
そして、一般的には if-else 文の方が値の分岐に関わらず処理も記述できるためできる事が多い。

逆に 条件演算子にできないことは、値に応じた処理内容が変わるとき。
ここから、一つの使い分け条件ができる。

条件演算子
データを選択する必要があるが、概念的にどの分岐があっても処理が同じ
if-else 文
分岐によって、全く異なる処理を選択

例1(条件演算子を使うべきだと思う例)

使用している Linux OS から使用すべきパッケージマネージャを判別し、
パッケージをインストールさせようとしているとする。
今回は CentOS/RedHat系なら rpm、そうでなければ apt-get を利用するとする。

if-else文の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = null;
	if(rpmUses.contains(getOsName())) {
		pkgmngr = "rpm -ivh";
	}else {
		pkgmngr = "apt-get install";
	}
	installPackage(pkgmngr, "zabbix-server-mysql");
条件演算子の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = rpmUses.contains(getOsName()) ? "rpm -ivh" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

これらは、パッケージマネージャのプログラム名(文字列データ)を選択する責任を担っている。
概念的にやってることはどの分岐でも同じなので、後者の条件演算子で記述すべきと思う。

もし、RedHat の場合は別のパッケージマネージャ、例えば yum を使うことになったら、下記のようになる。

	List<String> rpmUses = Arrays.asList("centos");
	List<String> yumUses = Arrays.asList("redhat");
	String os = getOsName();
	String pkgmngr = rpmUses.contains(os) ? "rpm -ivh" : 
			 yumUses.contains(os) ? "yum install" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

ここで、ネストするときは行を分けて記述すると処理内容が分かりやすい。
(よく条件演算子は可読性が悪いというけれど、書式を工夫すれば問題ないと思う)
しかしこれ以上増やすと見苦しいソースコードになるので、3~4個くらいが限界か。

それ以上の分岐を書くときは、列挙型などを使用して記述する方が良い。
試しに、fedora のときは dnf というパッケージマネージャを利用することにすると。
私なら、処理内容は次のように修正する。

まず、各 OS の列挙型に使用するパッケージマネージャ情報を定義。

private static enum LinuxOS {
	CENT_OS("centos", "rpm -ivh"),
	RED_HAT("redhat", "yum install"),
	FEDORA("fedora", "yum install"),
	OTHER("other", "apt-get install");
	
	private String os;
	private String pkgmngr;
	private LinuxOS(String os, String pkgmngr) {
		this.os = os;
		this.pkgmngr = pkgmngr;
	}
	static LinuxOS get(String osName) {
		for(LinuxOS linux : values()) {
			if(linux.os.equals(osName)) {
				return linux;
			}
		}
		return OTHER;
	}
}

パッケージマネージャを取得するロジック。

	String pkgmngr = LinuxOS.get(getOsName()).pkgmngr;
	installPackage(pkgmngr, "zabbix-mysql");

ロジック自体の処理が見辛くなることはなくなるし、今後新しいパッケージマネージャが増えても列挙型を増やすだけで対応可能。
また、OS で使用するパッケージマネージャを変更したければ、OS の列挙型の中身を修正するだけで対応できる。

例2 (if文を使うべきだと思う例)

ユーザログイン画面でパスワード認証するページを想定する。
認証に成功すればメンバーページに遷移し、逆に失敗すれば認証ページを再び表示する。

if-else文の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
			gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
			gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
条件演算子の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

この例では、認証失敗・成功で表示ページを切り替えるだけであるから、データの分岐に見えるかもしれない。
しかし、個人的には、この場合はif文で実装にすべきだと思っている。

なぜなら、分岐による処理の内容は本質に異なるから。
成功時に処理したい内容、認証失敗時に処理したい内容、これらは当然異なる処理になりやすい。

例えば、セキュリティ対策の一環として、あまりにログイン処理失敗が多い場合はログに記録しておきたい場合もあるし、
ログインに成功すれば、その場でセッションを新しくする処理も必要になる。
これらは、処理が本質的に異なるため条件演算子で対応すると複雑になりやすい。

以下は、仮に認証失敗時にログを取得する場合をさきほどの実装から追加した場合。

	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
		gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
		writeLog(new Date() + " id->" + id); // 一行追加
		gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(!pwd.equals(getPassword(id))) {
		writeLog(new Date() + " id->" + id); // 認証失敗時、結局if文を使う
	}
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

おまけに条件演算子の実装は、失敗時に次に遷移するページを表示する前に
ログを出力するということが分かり辛いと思う。(前者に比べて)

この処理は端的に言うと「ログインが成功したときは~~、失敗したなら~~。」
というログインが成功したかが基準となり、どちらかの処理が選択される構造。
これを端的に表現できるのは、if文の強みでもある。

まとめ

条件演算子はダメで if-else 文のみという現場もあるらしいが、ナンセンスな話だと思う。
たしかに条件演算子は書き方によっては複雑になるが、フォーマットを工夫すれば見やすくなる。
なにより、分岐が多くなれば条件演算子に関わらず、見辛くなるのは if-else 文もあまり変わらない。

今回は if-else 文と条件演算子の違いについて記述したが、これらですら書き方一つでコードの読みやすさは変わる。
文法レベルだけの解釈だけでなく、現在書いている処理の意味に応じて柔軟に対応できた方が良いだろう。
このようなことでも、意識してプログラミングできる/できないが、プログラマとして実力差につながると思う。

謝辞

これらの記事を書くきっかけを作ってくれた、H氏。いつもいい刺激を頂きありがたい限り。

Seasar2 (導入編)

概要

今回は、DI コンテナという少し特殊な技術を用いる。
この技術を使うと、プログラムをコンパイルせずに、プログラム外部にあるリソースファイルを使いプログラムの挙動を制御できる。
また、デザインパターンで言う Factory や Singleton パターンを容易に実装できるため、
オブジェクトのライフサイクルに気を取られず、どのようなクラスを作成するかに注力できるようになる。

DI って?

プログラミングのスタイルとして、まずインタフェースを決定し、その後詳細を実装していくスタイルがある。
このスタイルは、インタフェースでコンポーネントの仕様を決め、それぞれの仕様に従ったクラスを実装するという
流れでプログラミングを行うため、各コンポーネントが互いに依存しない(疎な)コンポーネント群になりやすい。

そして、この考えを発展させたものとして DI (Dependency Injection) という設計パターンがある。
これは、それぞれのコンポーネント間が疎になることにより、コンポーネントを自由に取り替えることができるようになる。
そのため、どのコンポーネントを使うかをプログラム内で記述するのではなく、外部リソースを使って決定させるようにする。
これによって、外部リソースを変更するだけで、プログラムをコンパイルし直さなくても、プログラムの挙動を変えることができる。

これによって、以下の利点が生まれる。

  • コンポーネントの再利用促進
  • オブジェクト使用コードとオブジェクト生成コードの分離
  • テスト単純化
  • 保守性向上

今回用いる DI コンテナ

Java では、有名なものには Spring DI, Dagger, Google Guice などがあるらしいが、
参考:Dependency Injection Options For Java | Keyhole Software
今回は Seasar2 を使う。(Seasar2 - Seasar2)

Seasar2 は、世界的に見るとシェアは低く、新規機能開発も現時点では終了している(バグ改修のみ)ため、
今ある以上のことが今後できる見込みはない。

しかし、

  • 少ない設定で使用できる
  • 専用プラグインが充実している
  • 国産のため日本語文献が多い

といった利点があるため、規模の小さい開発や DI の実践演習としては有効な選択肢になりやすい。
なお、Searsar2 では、DI コンテナ以外にも、DB連携機能、アスペクト指向プログラミングなどの機能も提供している。

導入方法

日本語版の WindowsEclipse (Mars, ver4.5.1)を使用している場合を示す。
(画像は別環境でのスクリーンショットなため、Mac 英語版ですが...)

SeaSar2 用のプラグインの導入

次のプラグインを導入。

Eclipseツールバーより、 [ヘルプ] -> [新規ソフトウェアのインストール...]を選択。
上記テキストボックスの「作業対象」に http://eclipse.seasar.org/updates/3.3/ を追加する。
f:id:kouki_hoshi:20160316201117p:plain
そして、以上から、Dolteng, Kijimuna, SAStruts Plugin, Resource Synchronizer をインストールする。

なお、SAStruts Plugin のインストールがうまくいかなかった場合(私はなんかエラーが出た...)
http://download.eclipse.org/releases/mars/"Eclipse のバージョンによってURL 変える感じ。
を対象に入れて、「Web, XML, Java EE and OSGi Enterprise Development」をインストールすると、
SAStruts Plugin もインストールできた。
参考:SAStrutsPlugin がインストールできない場合の対応方法 - KusoBoze is here.

次に、作業対象に「http://eclipse.seasar.org/updates/3.2/」を追加する。
そして、DbLauncher をインストールする。
f:id:kouki_hoshi:20160316201124p:plain

ライブラリ追加

あとは、Seasar2 - Downloads にアクセスして、
S2Tiger は便利な拡張機能があるので、入れておいた方が良さそう。
また、DBを使うなら S2JDBC-GEN も入れた方が良い。
f:id:kouki_hoshi:20160316201130p:plain
インストール方法:Seasar - DI Container with AOP -

これで導入フェーズは終了。
次回からはサンプルの構築に移る予定。

参考文献

Seasar2徹底入門 SAStruts/S2JDBC対応

Seasar2徹底入門 SAStruts/S2JDBC対応