はじめに
弊社では、エンジニアには基本的にMacを支給しています。 今まで、新しく人が入るたびに毎回同じ環境構築を手作業で行っていたのですが、今回これをAnsibleを使って自動化しました。
これでいつでも新しい人を迎えられます。採用のご応募はこちらからどうぞ。笑
Ansibleとは
Ansibleは構成管理ツールの一つです。同種のツールにはChefやPuppetなどがあります。
本来はリモートにあるサーバ等にsshで接続して環境を構築するツールですが、対象ホストをlocalhostにすることで自分自身の構成管理にも使えます。
※ 環境を構築することを「プロビジョニング」と言います。
自動化したこと
- Homebrewで各種パッケージをインストール
- Homebrew Caskで各種GUIアプリケーションをインストール
- oh-my-zshやRictyフォントなどHomebrewだけでカバーできないもののインストール
- ApacheやPHPの設定ファイルの調整
これらをコマンド一発で実行できるようにしました。
Ansibleを使ったMacの環境構築については既に詳しい解説記事がたくさんありますが、ApacheやPHPの設定ファイルの調整の部分はWebであまり実例を見かけなかったので、PHPerの皆さんには参考になるかもしれません。
実際に作成したplaybook
今回作成したplaybook(構成を記述したymlファイル)は弊社のプライベートリポジトリにあるためお見せできませんが、私の個人用のplaybookがだいたい似たような内容になっているので、そちらを実例としてご参照ください。
Ansibleの動作環境を用意
環境構築を自動化するとは言え、最低限Ansible自体の動作環境は事前に必要になります。Homebrewで簡単にインストール可能です。(Homebrwe自体のインストールは事前に済ませておいてください)
$ brew install python
$ brew install ansible
最小限の内容で実際に動かしてみる
必要なファイルを準備
Ansibleを使ってあるホストの構成管理を行うには、
- inventoryファイル(対象のホストを指定する)
- playbookファイル(構成を記述する)
の2つが必要になります。
今回はinventoryファイルをhosts
、playbookファイルをlocalhost.yml
というファイル名で作成します。
これらを作成した状態で
$ ansible-playbook localhost.yml -i hosts
という感じでコマンドを叩くと、プロビジョニングが始まります。
それぞれ以下のような内容で作成しましょう。
inventoryファイル
# 今回はローカルマシンが対象
$ echo 'localhost' > hosts
playbookファイル
$ vi localhost.yml
- hosts: localhost
connection: local
gather_facts: no
sudo: no
tasks:
# update homebrew
- name: update homebrew
homebrew: update_homebrew=yes
この時点では、Homebrewを最新にするだけのplaybookになっています。
この状態で実行してみると、タスクが1つ実行され、ホストの環境が更新される(結果のステータスがchanged
になる)ことが分かると思います。
ちなみに、一度実行した直後にもう一度実行すると、結果のステータスはchanged
ではなくok
となり、実際には変更が加えられません。
このように何度実行しても結果が変わらない性質のことを「冪等性(べきとうせい)」と言います。
Homebrewパッケージをインストール
さて、ここからはplaybookに必要なタスクを書き足していきます。
まずはHomebrewで各種パッケージをインストールしましょう。 localhost.ymlに以下のように追記します。
- hosts: localhost
connection: local
gather_facts: no
sudo: no
+ vars:
+ homebrew_taps:
+ - homebrew/php
+ - homebrew/apache
+
+ homebrew_packages:
+ - { name: ansible }
+ - { name: boris }
+ - { name: brew-cask }
+ - { name: colordiff }
+ - { name: composer }
+ - { name: coreutils }
+ - { name: git }
+ - { name: graphviz }
+ - { name: htop-osx }
+ - { name: httpd22 } # php56より前に置く
+ - { name: hub }
+ - { name: jq }
+ - { name: mysql }
+ - { name: nkf }
+ - { name: node }
+ - { name: openssl }
+ - { name: php56, install_options: with-homebrew-apxs }
+ - { name: php56-intl }
+ - { name: php56-opcache }
+ - { name: php56-apcu }
+ - { name: php56-mcrypt }
+ - { name: php56-xdebug }
+ - { name: php56-xhprof }
+ - { name: phpunit }
+ - { name: python }
+ - { name: reattach-to-user-namespace }
+ - { name: rename }
+ - { name: sl }
+ - { name: sqlite }
+ - { name: tig }
+ - { name: tmux }
+ - { name: tree }
+ - { name: unrar }
+ - { name: wget }
+ - { name: zsh }
+ - { name: zsh-completions }
+
+ # brew tap
+ - name: install taps of homebrew
+ homebrew_tap: tap="{{ item }}" state=present
+ with_items: homebrew_taps
+
# brew update
- name: update homebrew
homebrew: update_homebrew=yes
+
+ # brew instal
+ - name: install homebrew packages
+ homebrew: name="{{ item.name }}" state="{{ item.state|default('latest') }}" install_options="{{ item.install_options|default() }}"
+ with_items: homebrew_packages
やっていること
-
vars:
にHomebrewに追加したいtapとHomebrewでインストールしたいパッケージをリスト化 - homebrew_tapモジュールを使って各tapを追加
- homebrewモジュールを使って各パッケージの最新バージョンをインストール
Homebrew Caskパッケージ(GUIアプリケーション)をインストール
次に、Homebrew Caskで各種GUIアプリケーションをインストールします。
:
:
vars:
homebrew_taps:
+ - caskroom/cask
- homebrew/php
- homebrew/apache
:
:
+ homebrew_cask_packages:
+ - avast
+ - caffeine
+ - clipmenu
+ - cooviewer
+ - dropbox
+ - eclipse-java
+ - evernote
+ - filezilla
+ - firefox
+ - flash-player
+ - google-chrome
+ - google-drive
+ - google-japanese-ime
+ - gyazo
+ - heroku-toolbelt
+ - java
+ - karabiner
+ - kobito
+ - libreoffice
+ - macdown
+ - phpstorm
+ - reaper
+ - slack
+ - skitch
+ - skype
+ - spectacle
+ - sublime-text
+ - vagrant
+ - virtualbox
+ - vlc
+ - xmind
+ - xtrafinder
+
tasks:
:
:
+ # brew cask install
+ - name: update brew-cask
+ homebrew: name=brew-cask state=latest
+ - name: install homebrew cask packages
+ homebrew_cask: name="{{ item }}" state=present
+ with_items: homebrew_cask_packages
やっていること
- インストール対象のtapに
caskroom/cask
を追加 - Homebrew Caskでインストールするアプリのリストを
vars:
に追加 - 事前に
brew-cask
パッケージを最新化 - homebrew_caskモジュールを使って各種アプリをインストール
oh-my-zshをインストール
私はシェルの設定に明るくないのでoh-my-zshで楽をしています。 oh-my-zshのインストールも自動化しましょう。
+ # install oh-my-zsh
+ - name: install oh-my-zsh
+ shell: "sh -c $(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
+ args:
+ creates: ~/.oh-my-zsh
やっていること
- shellモジュールを使ってoh-my-zshのインストールコマンドを実行
-
creates
オプションでファイルの存在チェックを行うことで冪等性を確保(ファイルが存在していればコマンドは実行されない)
Rictyフォントをインストール
Rictyフォントが好きなのでこれも入れます。
vars:
:
:
homebrew_taps:
- caskroom/cask
- homebrew/php
- homebrew/apache
+ - sanemat/font
:
:
+ handlers:
+ - name: run fc-cache
+ shell: fc-cache -vf
:
:
tasks:
:
:
+ # install Ricty font
+ - name: install xquartz
+ homebrew_cask: name=xquartz
+ - name: install ricty
+ homebrew: name=ricty
+ - name: copy generated font file
+ shell: "cp -f $(brew --cellar ricty)/*/share/fonts/Ricty*.ttf ~/Library/Fonts/"
+ args:
+ creates: ~/Library/Fonts/Ricty-Bold.ttf
+ notify: run fc-cache
やっていること
- 必要なtapを追加
-
handlers:に
fc-cache
コマンドを実行するNotifyハンドラを定義 - 事前にxquartzをインストール
- rictyをインストール
- フォントファイルをコピー
-
creates
オプションを使って冪等性を確保 -
notify:
を使って、実行後にrun fc-cache
ハンドラに通知
Apacheを設定
httpd.conf
の設定変更も自動化しましょう。
ファイルの編集は冪等性を保ちながら正確に行うのがなかなか難しく、試行錯誤の末に以下のような設定をしたのですが、割と複雑な感じになってしまっています。 もっと美しく書けるという方はぜひアドバイスいただければ幸いです。
vars:
:
:
+ apache_user: Apacheの実行ユーザ名
:
:
tasks:
:
:
+ # configure apache
+ - name: configure httpd.conf (replace)
+ replace: dest=/usr/local/etc/apache2/2.2/httpd.conf regexp="{{ item.regexp }}" replace="{{ item.replace }}"
+ with_items:
+ - { regexp: '^( *Listen) .*$', replace: '\1 80' }
+ - { regexp: '^( *User) .*$', replace: '\1 {{ apache_user }}' }
+ - { regexp: '^( *DirectoryIndex) .*$', replace: '\1 index.php index.html' }
+ - { regexp: '^( *)# *(AddHandler cgi-script .cgi).*$', replace: '\1\2' } # just comment in
+ - { regexp: '^LoadModule +php5_module +/usr/local/Cellar/.+\n', replace: '' } # remove the line inserted via `brew install php56 --homebrew-apxs`
+ - name: configure httd.conf (insert)
+ lineinfile: dest=/usr/local/etc/apache2/2.2/httpd.conf insertafter="{{ item.insertafter }}" line="{{ item.line }}"
+ with_items:
+ - { insertafter: "LoadModule rewrite_module", line: "LoadModule php5_module /usr/local/opt/php56/libexec/apache2/libphp5.so" }
+ - { insertafter: '^#ServerName ', line: "ServerName localhost:80" }
+ - { insertafter: '^ *AddType ', line: " AddType application/x-httpd-php .php" }
+ - { insertafter: '^ *# *Include .+httpd-vhosts.conf', line: "Include /usr/local/etc/apache2/2.2/extra/httpd-vhosts.my.conf" }
+ - name: create httpd-vhosts.conf
+ copy: src=templates/httpd-vhosts.my.conf dest=/usr/local/etc/apache2/2.2/extra/httpd-vhosts.my.conf
+ args:
+ force: no
やっていること
- Apacheの実行ユーザ名を変数化(他の人とplaybookを共有しやすいように)
-
replaceモジュールを使って、正規表現でファイルの中身を無理やり置換
-
Listen 8080
→Listen 80
-
User daemon
→User Apacheの実行ユーザ名
-
DirectoryIndex index.html
→DirectoryIndex index.php index.html
-
# AddHandler cgi-script .cgi
→AddHandler cgi-script .cgi
(コメントインするだけ) -
LoadModule php5_module /usr/local/Cellar/php56/5.6.x/libexec/apache2/libphp5.so
を削除(php56インストール時に--with-homebrew-apxs
オプションによって自動で挿入された行)
-
-
lineinfileモジュールを使って、「正規表現にマッチした最後の行」の直後に行を挿入(もちろん冪等的に)
-
LoadModule rewrite_module
の次の行にLoadModule php5_module /usr/local/opt/php56/libexec/apache2/libphp5.so
-
#ServerName
の次の行にServerName localhost:80
-
AddType
の次の行にAddType application/x-httpd-php .php
-
# Include /usr/local/etc/apache2/2.2/extra/httpd-vhosts.conf
の次の行にInclude /usr/local/etc/apache2/2.2/extra/httpd-vhosts.my.conf
(httpd-vhosts.conf
ファイルを冪等的に編集するのは大変なので自分用のvhosts設定ファイルを追加)
-
-
copyモジュールを使って自分用のvhosts設定ファイルをコピー
- 初回のプロビジョニング後は自由に変更していいように、
force: no
でファイルが既存かつテンプレートと内容が異なっていても上書きしないように設定
- 初回のプロビジョニング後は自由に変更していいように、
当然ながらhttpd-vhosts.my.conf
を用意しておく必要があります。
$ mkdir templates
$ vi templates/httpd-vhosts.my.conf
<Directory "/usr/local/var/www/htdocs">
AllowOverride All
</Directory>
NameVirtualHost *:80
<VirtualHost *:80>
DocumentRoot "/usr/local/var/www/htdocs"
ServerName localhost
ErrorLog "/usr/local/var/log/apache2/error_log"
CustomLog "/usr/local/var/log/apache2/access_log" common
</VirtualHost>
PHPを設定
同じ要領でphp.ini
を設定します。
+ # configure php
+ - name: configure php.ini
+ replace: dest=/usr/local/etc/php/5.6/php.ini regexp="{{ item.regexp }}" replace="{{ item.replace }}"
+ with_items:
+ - { regexp: '^(max_execution_time) *=.*$', replace: '\1 = 0' }
+ - { regexp: '^(memory_limit) *=.*$', replace: '\1 = 512M' }
+ - { regexp: '^(;date.timezone) *=.*$', replace: '\1 = Asia/Tokyo' }
+ - { regexp: '^(;mbstring.language) *=.*$', replace: '\1 = Japanese' }
+ - { regexp: '^(mysql.default_socket) *=.*$', replace: '\1 = /private/tmp/mysql.sock' }
+ - { regexp: '^(pdo_mysql.default_socket) *=.*$', replace: '\1 = /private/tmp/mysql.sock' }
+ - { regexp: '^(zend_extension *= *opcache.so)', replace: ';\1' } # just comment out
やっていること
max_execution_time = 0
memory_limit = 512M
date.timezone = Asia/Tokyo
mbstring.language = Japanese
mysql.default_socket = /private/tmp/mysql.sock
pdo_mysql.default_socket = /private/tmp/mysql.sock
-
;zend_extension = opcache.so
(コメントアウト)(参考)
xdebugを設定
xdebugの設定ファイルも、vhostsと同様に別ファイル化する戦略をとります。
+ # configure xdebug
+ - name: configure ext-xdebug.ini
+ copy: src=templates/ext-xdebug.my.ini dest=/usr/local/etc/php/5.6/conf.d/ext-xdebug.my.ini
$ vi templates/ext-xdebug.my.ini
xdebug.remote_enable = 1
xdebug.remote_port = 9001
xdebug.profiler_enable = 1
xdebug.profiler_output_dir = /tmp
xdebug.idekey = PHPSTORM
xdebug.max_nesting_level = 500
xdebug.var_display_max_children = -1
xdebug.var_display_max_data = -1
xdebug.var_display_max_depth = -1
やっていること
-
copyモジュールを使って
ext-xdebug.my.ini
の内容をテンプレートと同一に保つ
xhprofを設定
xhprofを使いたいので、これもインストールします。
Homebrewでインストールしたxhprofでは、プロファイル結果のファイルがCellerの下に保存されてブラウザで結果を確認するのが面倒なので、ドキュメントルート直下にxhprof
というディレクトリを作ってその下にシンボリックリンクを置くようにします。
また、xhprofを使うときに書くコードスニペットを毎回忘れてググッているので、スニペットを書いたファイルもこのxhprof
ディレクトリの下に置くようにします。
+ # configure xhprof
+ - name: make xhprod dir in DocumentRoot
+ file: dest=/usr/local/var/www/htdocs/xhprof state=directory
+ - name: symlink xhprof into DocumentRoot
+ file: src="{{ item.src }}" dest="{{ item.dest }}" state=link
+ with_items:
+ - { src: /usr/local/opt/php56-xhprof/xhprof_html, dest: /usr/local/var/www/htdocs/xhprof/xhprof_html }
+ - { src: /usr/local/opt/php56-xhprof/xhprof_lib, dest: /usr/local/var/www/htdocs/xhprof/xhprof_lib }
+ - name: put a snippet file into xhprof dir
+ copy: src=templates/xhprof.snippet dest=/usr/local/var/www/htdocs/xhprof/xhprof.snippet
$ vi templates/xhprof.snippet
<?php
// paset this into bootstrap of your application.
xhprof_enable();
register_shutdown_function(function () {
$runs = new XHProfRuns_Default(sys_get_temp_dir());
$runs->save_run(xhprof_disable(), 'APPLICATION_NAME');
});
やっていること
-
fileモジュールを使って、ドキュメントルート直下に
xhprof
ディレクトリを作成 - 同じくfileモジュールを使って
xhprof_html
,xhprof_lib
ディレクトリのシンボリックリンクを作成 - copyモジュールを使ってスニペットファイルを配置
dotfilesを展開
さて、ここまでで一通り必要な環境は出来てきました。あとは 秘伝のdotfiles を展開したら開発環境の完成です。
ここは結構やり方を迷ったのですが、試行錯誤の末に以下のような方法をとることにしました。
- dotfilesの取得元(
src
)、保存先(dest
)、保存したdotfilesをホームディレクトリ配下へ展開するためのコマンド(symlink_command
)をそれぞれ変数として定義 - 取得元として、Gitリポジトリとローカルのディレクトリパスのいずれかを選択可能(dotfilesをGitHubのリポジトリ等で管理している人とそうでない人で共通のplaybookを使えるようにしたかったので)
- 多くの人は、dotfilesの現物と一緒に、それらをホームディレクトリ配下へシンボリックリンクとして展開するためのシェルスクリプトなんかを管理しているはずだろうと想定
vars:
:
:
+ dotfiles:
+ src:
+ repository: git@github.com:ttskch/dotfiles.git
+ directory: ~
+ dest: ~/dotfiles
+ symlink_command: "cd ~/dotfiles ; sh symlink.sh"
:
:
tasks:
:
:
+ # install dotfiles
+ - name: git clone dotfiles
+ git: repo="{{ dotfiles.src.repository }}" dest="{{ dotfiles.dest }}"
+ register: ret1
+ when: dotfiles.src.repository
+ - name: copy dotfiles
+ copy: src="{{ dotfiles.src.directory }}" dest="{{ dotfiles.dest }}"
+ register: ret2
+ when: dotfiles.src.directory
+ - name: symlink dotfiles
+ shell: "{{ dotfiles.symlink_command }}"
+ when: ret1|changed or ret2|changed
やっていること
- when:を使って、変数の定義内容に応じて取得タスクを選択
- (1)gitモジュールを使ってGitリポジトリをclone
- (2)copyモジュールを使ってディレクトリをコピー
- 1,2それぞれの実行結果をregister:を使って記憶しておき、どちらか一方でも結果が
changed
だったら、シンボリックリンクを展開するコマンドを実行
ここでは私の個人用のdotfilesを使用しています。
おまけ(Homebrew Caskのパッケージが最新に保てない?)
homebrew_caskモジュールのstate
オプションにはpresent
, absent
の2値しかなく、homebrewモジュールのようにlatest
を指定することができません。
なので、このplaybookを実行しても、既存のHomebrew Caskパッケージが最新バージョンに更新されることはありません。
直接brew cask
コマンドを叩けば更新可能なので、その手順をplaybookに書き起こしてもよかったのですが、これ以上playbookを複雑怪奇にしたくなかったのでひとまず現状はGUIアプリの更新は手作業でやることにしています。
具体的な手順は以下のとおりです。
$ brew cask cleanup --outdated
$ brew cask install --force {token}
brew cask install --force {tokne}
でパッケージを再インストールします。({token}
には対象のパッケージ名を入力)
その前にbrew cask cleanup --outdated
を実行していますが、これはダウンロードしたファイル(インストーラやzip)のキャッシュを削除するコマンドで、これをやらないと古いキャッシュから再インストールが実行されて結局バージョンが更新されない、ということが起こります。(--outdated
は削除対象を10日以上前のキャッシュだけに絞るオプション)
おわりに
思いのほか長文になってしまいましたが、Macの開発環境構築をAnsibleで自動化した際に行ったことを簡単な解説を交えながらご紹介しました。
今までは最低限esaに環境構築の手順をメモっておくぐらいしかしていなかったので、コマンド一発で環境が再現できるようになったのはかなり幸福度UPです。
会社などの組織でなくとも、個人の方にも開発環境構築の自動化はおすすめです。「Macbookを買い換える度にいつも同じような環境構築をググりながらやっている」というような人は、この機会にAnsibleで自動化してみてはいかがでしょうか