Rails開発環境をAnsibleでサクッと構築する（Ruby, MySQL, Node.js, Redis, ElasticSearch）

# !TL;DR
Ansible知ってるandソースだけ欲しい人はここで終了です。
https://github.com/Blue-Pix/eclair

なぜeclairという名前で作ったかというと、Ansibleの語源からインスピレーションを得ました。

# [Ansible](https://www.ansible.com/) is 何？
インフラのオートメーションツール。
ChefとかPuppetと並んで引き合いに出されます。
![ansible-logo.png](https://qiita-image-store.s3.amazonaws.com/0/83183/689024b0-da1d-31f2-3f8e-c7c514d18047.png)

# 環境構築 is 時間の無駄
・プロジェクトごとに毎回Vagrant構築するのしんどい
・テキストベースで環境構築マニュアル作るのしんどい
・コマンド1つ1つメモるのか
・構築するものが多い（Ruby, Node.js, MySQL, ElasticSearch, Redis, ...etc.）
・プロジェクトごとに細かいバージョン違って死ぬ

# Why not Chef ?

ざっくりChefとAnsibleについて調べた結果、

- 導入コストが少なそうだったから
- エージェントレスだから（対象サーバーにPythonさえ入ってれば構築できる）
- シンプルだから
- Pythonとyamlで書くから（僕はRubyが苦手です）

です。

### Ansible
- yamlで書く
- エージェント不要
  - ssh経由で構築を行う
  - Pythonがインストールされてる環境ならOK
- build and scrapに向いてる

### Chef
- Rubyで書く
- Chef-Serverとかknifeとか特有のツールがいる
- 複数台のホスト管理に向いてる（というか前提？）

サクッと開発環境を作るならAnsible一択かなあと思います。

# 今回の目的

`macOS High Sierra`で`CentOS7.2`のVagrantを立てて、
以下をインストールします。

- Ruby 2.4.2
- MySQL5.7
- Node.js 8.1.2
- Redis stable
- ElasticSearch 5.6.8

なぜこのバージョンかと言うと、携わるプロジェクトがこの構成だったからです。
バージョンはそれぞれ好みのものに変えれる用に作ってはありますが、
検証はしてないので互換性がないやつは普通にエラー出ると思います。

`MySQL`は`5.6`, `5.7`に対応させてありますが、`5.1`とかはたぶん無理です。
パスワード周りが`5.6`と`5.7`で地味に違うのめんどくさいよね。
`Ruby`は`rbenv`, `Node.js`は`n`を使って入れます。

RailsアプリはGit clone済みで、Vagrantにマウントする想定です。

OSはバージョン以上に深刻な問題で、`CentOS7`系以外では無理です。
どうしても`Ubuntu`で開発したいんだ俺は！！という方はせっせと
`yum`を`apt-get`に書き換えたりして対応してください。
設定ファイルの位置とかsystemdとか細かく違うので同じCentOSと言えど6系も無理です。

# VMの構築

Vagrantはインストールしておいてくださいね。

```
$ vagrant box add CentOS7.2 https://github.com/CommanderK5/packer-centos-template/releases/download/0.7.2/vagrant-centos-7.2.box
$ vagrant init CentOS7.2
```

```ruby:VagrantFile
# コメントアウトを外す
config.vm.network "private_network", ip: "192.168.33.10"

# フォワードしたいポートを追記する
config.vm.network "forwarded_port", guest: 3000, host: 30000
config.vm.network "forwarded_port", guest: 3035, host: 30350

# マウントするフォルダを追記する
config.vm.synced_folder "./app/my_app", "/home/vagrant/my_app", owner: "vagrant", group: "vagrant"
```

マウントするRailsアプリと今回の環境構築のフォルダは
同じ階層に置いてます。プロジェクトごとにフォルダ作って、
アプリと構成管理をそれぞれ`git clone`してくる流れで開発してます。

`VagrantFile`は元々ある`VagrantFile.example`をコピーして作ってください。
ここで1回起動しておきます。

```
$ vagrant up
```

`Ansible`はssh経由でリモートホストの環境を構築するので、
sshでこのVMにつなげるように手配して置く必要があります。

VMのホスト名を`hoge`として、sshの接続情報をmacのホームディレクトリに記述します。
`~/.ssh/config`に書くか、プロジェクトごとに別ファイルを切るかはお好みだと思います。
どこに書こうが特に支障はありません。

```
$ vagrant ssh-config --host hoge >> ~/.ssh/hoge-config
```

sshでログインできるか確認

```
$ ssh hoge
Last login: Sat Jan  6 19:39:59 2018 from 10.0.2.2
[vagrant@localhost ~]$
```

構築終わり。
開発はデフォルトのvagrantユーザーで進めるので、
別ユーザーでやりたい場合は適宜作ってください。

# Ansibleのインストール

`brew`一発です。

```
$ brew install ansible

$ ansible --version
ansible 2.4.2.0

$ ansible-playbook --version
ansible-playbook 2.4.2.0
```

# Ansibleの初期設定

どのソフトをインストールして設定ファイルをこう書き換えて〜
みたいなことは`Playbook`と呼ばれるファイルに記述するのですが、
それとはまた別のレイヤーで、どのホストに対してどういう構成を当てるかや、
変数の宣言などをあらかじめしておかなければならないのでそこの設定です。

## inventory file

対象となるホストを記述するファイルです。
`yaml`ではなく`INI`形式。
ファイル名に決まりはないですが、`hosts`という名前が一般的です。
グループ名とホスト名の対ですが、グループ名は省略できます。

```ini:hosts
hoge
```

WebサーバーやらDBサーバーやら一緒くたにするので
グループ化する必要はないのでホスト名だけ。
Docker的な思考でサービスごとに分離したい時や、
本番環境で運用する場合はこの辺り整備する必要があります。
今回はローカルでの開発用なので思考停止してください。

Ansibleを本番で使おうとする場合は、
もっともっともっともっと考え抜いて設計しなければいけません。


## ansible.cfg

Ansibleの設定ファイルです。
`ansible`コマンドを使うときにオプションとしても指定できますが、
ファイルで定義していた方が楽なので。

```ini:ansible.cfg
[defaults]
inventory = hosts
retry_files_enabled = False
 
[privilege_escalation]
become = True
 
[ssh_connection]
control_path = %(directory)s/%%h-%%r
ssh_args = -o ControlPersist=15m -F /Users/hoge/.ssh/hoge-config -q
scp_if_ssh = True
```

>[defaults]
>inventory: 先ほど定義したホスト情報のファイル名を指定
>retry_files_enabled: 構成に失敗した時に`.retry`ファイルを作るかどうか
>
>[privilege_escalation]
>become: 管理者権限で実行するかどうか
>
>[ssh_connection]
>control_path: コントロールソケットを保持するパス
>ssh_args: sshコマンドの引数
>scp_if_ssh: scpでファイルを転送するかどうか

重要なのはsshの接続情報をちゃんと参照できるようにするのと、
`inventory`でホスト情報を指定すること。
また、`become: True`で管理者権限でサーバーをいじるようにすること。
`vagrant`ユーザーでは触れないところがあるので、基本はroot権限で
インストールやら何やかんやをして、必要に応じて`vagrant`ユーザーに
戻るという手順を取ることにする。


▼参考
https://dev.classmethod.jp/server-side/ansible/ansible-tutorial-2017/

▼他にもたくさんオプションがあるので詳しくは公式を
http://docs.ansible.com/ansible/latest/intro_configuration.html

▼スケルトンがあるのでそれを使うのもあり
https://raw.githubusercontent.com/ansible/ansible/devel/examples/ansible.cfg

## site.yml
Playbookというやつです。
ここにどの`host`に対してどの`task`を実行するかを定義します。

```yaml:site.yml
- hosts: hoge
  roles:
    - init
    - mysql
    - ruby
    - nodejs
    - redis
    - elasticsearch
```

`task`はベタ書きするのではなく、`role`という単位にまとめてます。
1`role`1`middleware`のイメージです。
こうすることで見通しも良くなるし、
`role`ごとに再利用可能になり、
このプロジェクトでは`redis`使わないとかなったら

```yaml
- redis
```
だけコメントアウトすればよくなります。

# Ansibleの実行

`hosts`, `ansible.cfg`, `site.yml`が書けたら、
以下のコマンドを打つだけで構築が完了します。

```
$ ansible-playbook site.yml
```

Rails開発環境できましたね。

これ以降はそれぞれの`role`の`task`について
Ansibleの解説になるので興味ない人は読み飛ばしちゃってください。

# 変数について

Ansibleにおいて変数を定義できる場所はたくさんあります。
ゆえに散らばらせるとたくさん上書きされてよくわからないことになります。
シンプルにするために以下のように自分ルールを定めます。

- `role`に依存するものは`roles/xxx/vars/main.yml`に定義する。

```yaml:roles/redis/vars/main.yml
__version: stable
__port: 6379
__data_dir: /var/lib/redis
__log_dir: /var/log/redis
__log_level: notice
```

versionなんかほとんどのミドルウェアで使う変数名だと思うので、
グローバルに定義すると上書きが起こってしまい不安です。
ミドルウェアごとにプレフィックスをつけるのもありっちゃありですが、
そもそもスコープを`role`内に絞るのが自然かと思います。

あ、プレフィックスに`__`を付けているのはpythonicな習慣です。
あらかじめ定義してあり、代入は禁止ということを表したいのでこういう書き方を取りました。
というのも、`task`の中で標準出力を変数に格納する場面があるのですが、
タスク実行中に定義された変数と、あらかじめ定義してある変数をごっちゃにしたくないのです。

また、変数を使う時には基本ブラケットで括るのですが（`{{ __version }}`）、
`when`オプションの中などではWarningが出るため、生で書かなければいけない時があります。

```yaml
when: __version == 5.6
```

`__`がないと変数であることが非常に分かりくいので可読性のためにもつけることにしてます。

```yaml
when: version == 5.6
```

- `role`をまたいで使うものは`site.yml`に定義する。

```yaml:site.yml
- hosts: hoge
  roles:
    - init
    - mysql
    - ruby
    - nodejs
    - redis
    - elasticsearch
  vars:
    __working_user: vagrant
```

`__working_user`はいろんな`role`で参照しているのでグローバルにここで定義します。


色々とやり方はありますが自分好みの構成はこれです。
変数の管理とか、ディレクトリ構成は以下の記事が参考になると思います。
https://qiita.com/kkitta/items/27f8aca89e55b719cb6f
https://qiita.com/ArimaRyunosuke/items/1f9d840311584d8160bc
https://qiita.com/kotarella1110/items/79af4485bd7985935d6b

または公式の[Best Practice](http://docs.ansible.com/ansible/latest/playbooks_best_practices.html)に倣う。


# Rolesの作成

ではでは具体的な`task`の記述について。

## init

`.bash_profile`の配置と、ロケール設定です。

色々とインストールをしていく中で`PATH`がカオスなことにならないように、
以下の記事のようなフラグメント化を行なっています。
[.bash_profileはフラグメント化してプロビジョニングしやすくしよう！](http://www.bunkei-programmer.net/entry/2015/05/22/004837)

```yaml:roles/init/tasks/main.yml
- name: deploy .bash_profile
  template:
    src: .bash_profile
    dest: ~/.bash_profile
    mode: 0644
  become_user: "{{ __working_user }}"
```

>`roles/init/templates`にデフォルトの`.bash_profile`を用意して、
vagrantユーザーのホームに置く。実行時はrootユーザーなので、
`become_user`でvagrantユーザーに成りすます。

```yaml:roles/init/tasks/main.yml
- name: mkdir .bash.d
  file:
    path: ~/.bash.d
    state: directory
    mode: 0755
  become_user: "{{ __working_user }}"
```

>ディレクトリの作成には[Files](http://docs.ansible.com/ansible/2.4/list_of_files_modules.html)モジュールの`state: directory`を使う。
`shell: mkdir ~ `だと自分で冪等性を管理しなくてはいけないので面倒。
Ansible組み込みのモジュールを使うと自動的に冪等性を保証してくれるので積極的に使う。(yumなど)

```yaml:roles/init/tasks/main.yml
- name: stop and disable firewalld
  service:
    name: firewalld
    state: stopped
    enabled: no
```

>ファイアウォールを止める。
`systemctl`コマンドは[System](http://docs.ansible.com/ansible/2.4/list_of_system_modules.html)モジュールの`service`で扱える。

```yaml:roles/init/tasks/main.yml
- name: set LANG={{ __locale }} in /etc/locale.conf
  lineinfile:
    dest: /etc/locale.conf
    regexp: "^LANG="
    line: "LANG={{ __locale }}"
    backup: yes
```

>ロケールを日本語にする。
すでにある設定ファイルなどの中身を書き換える時は[Files](http://docs.ansible.com/ansible/2.4/list_of_files_modules.html)モジュールの`lineinfile`で正規表現を使って置換する。
一応`backup: yes`で破壊的な変更を防ぐ。


## MySQL

個人的にMySQL入れるのが一番めんどくさい。
パスワードポリシーとかバージョンによって違ったり、
`community server`どうやって入れるんだっけとか毎回調べてる気がする。
Ansibleで書くときも厄介なのがパスワード問題。

```yaml:roles/mysql/tasks/main.yml
- name: check if pre-installed mariadb exists
  yum:
    list: mariadb-libs
  register: mariadb
```

>デフォルトでmariadbが入っているかどうかチェックします。
結果は`mariadb`という変数に格納します。

```yaml:roles/mysql/tasks/main.yml
- name: uninstall pre-installed mariadb
  yum:
    name: mariadb-libs
    state: absent
```

> 最初から入ってるmariadbをアンインストールする。
yumモジュールを使うのでアンイストール済みかどうかは勝手にチェックしてくれる。

```yaml:roles/mysql/tasks/main.yml
- name: remove pre-installed mariadb's file
  file:
    path: /var/lib/mysql
    state: absent
  when: mariadb.results[0].yumstate == 'installed'
```

>mariadbをアンインストールするときに、変な影響が出ないように関連するファイルも削除する。
`when`オプションを付けているのは、冪等性のため。
2回目以降実行するときに、新しくインストールしたMySQLのファイルを削除してしまうのを防ぐため。

```yaml:roles/mysql/tasks/main.yml
- name: add yum repository
  yum: 
    name: "{{ __repo_url }}"
    state: present
```

>`mysql57-community-release-el7-11.noarch.rpm`のyumリポジトリを追加する。

```yaml:roles/mysql/tasks/main.yml
- name: intall yum-utils
  yum:
    name: yum-utils
    state: present
```

>バージョンによってリポジトリを切り替えなければいけないため、
`yum-config-manager`コマンドを使えるように`yum-utils`をインストールする。

```yaml:roles/mysql/tasks/main.yml
- name: switch yum repository (for version 5.6)
  shell: |
    yum-config-manager --disable mysql57-community
    yum-config-manager --enable mysql56-community
  when: __version == 5.6 and mariadb.results[0].yumstate == 'installed'

- name: switch yum repository (for version 5.7)
  shell: |
    yum-config-manager --disable mysql56-community
    yum-config-manager --enable mysql57-community
  when: __version == 5.7 and mariadb.results[0].yumstate == 'installed'
```

>バージョンによってリポジトリを切り替える。
`shell`で処理するので、2回目以降不要な`changed`が出ないように、
mariadbをアンインストールした時(=初回実行時)のみ実行するようにする。

>ちなみに！！！！
`when`オプションの中で変数を`{{ }}`で括ろうとすると以下のエラーが出る。

>```
 [WARNING]: when statements should not include jinja2 templating delimiters
such as {{ }} or {% %}.
```
見にくいが`{{ }}`なしで変数名を記述する。

```yaml:roles/mysql/tasks/main.yml
- name: install MySQL{{ __version }}
  yum:
    name: "{{ item }}"
    state: present
  with_items:
    - mysql
    - mysql-devel
    - mysql-server
    - mysql-utilities
    - MySQL-python
```

>色々インストールします。複数個処理するときは`with_items`でスッキリ書く。

```yaml:roles/mysql/tasks/main.yml
- name: start and enable server
  service:
    name: mysqld
    state: started
    enabled: yes
```

>１回起動しておきます。自動起動の設定も`enabled`で有効に。

```yaml:roles/mysql/tasks/main.yml
- name: check if my.cnf.bk already deployed
  stat:
    path: "/etc/my.cnf.bk"
  register: default_my_conf_backup
```

>以降の処理の冪等性を保つため、`my.cnf`のバックアップが作られているかどうかを確認します。作成済みであれば、これ以降の処理は実行済みとみなしてスキップします。

```yaml:roles/mysql/tasks/main.yml
- name: get default MySQL root users's password
  shell: |
    less /var/log/mysqld.log | grep password | awk '{ print $NF }'
  register: mysql_default_password
  when: not default_my_conf_backup.stat.exists and __version == 5.7
```

>rootの初期パスワードがログに吐かれているので取り出します。5.6の場合はファイルが違うのでスキップ。

```yaml:roles/mysql/tasks/main.yml
- name: change password policy
  shell: |
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "SET GLOBAL validate_password_length='{{ __password_length }}';"
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "SET GLOBAL validate_password_policy='{{ __password_policy }}';"
  when: not default_my_conf_backup.stat.exists and __validate_password and __version == 5.7

```

>先ほど取り出した初期パスワードでログインして、シェル上からSQLステートメントを発行します。`connect-expired-password`オプションを付けないとログインに失敗するので注意。
ここではパスワードポリシーを変更する場合のみ、指定のポリシーに変更する処理を行っています。

>また、バージョン5.6だとそもそも`validate_password`プラグインが有効になっていないのでスキップする。

```yaml:roles/mysql/tasks/main.yml
- name: disable validate password
  shell: |
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mysql_default_password.stdout }}';"
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "uninstall plugin validate_password;"
  when: not default_my_conf_backup.stat.exists and not __validate_password and __version == 5.7
```

>パスワードポシリーを無効化したい場合のタスクです。`uninstall plugin validate_password`で無効化できるのですが、先に初期パスワードをリセットしないとこのステートメントを実行することはできません。
適当なパスワードに再設定してもいいのですがシンプルさを保つため、
前述のファイルから取り出した初期パスワードでそのまま上書きます（笑）
これでもパスワードはリセットされたと認識してくれます。

```yaml:roles/mysql/tasks/main.yml
- name: change root user password (for version 5.6)
  shell: |
    /usr/bin/mysqladmin -u root password "{{ __root_new_password }}"
  when: not default_my_conf_backup.stat.exists and __version == 5.6

- name: change root user password (for version 5.7)
  shell: |
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ __root_new_password }}';"
  when: not default_my_conf_backup.stat.exists and __version == 5.7
```

>rootユーザーのパスワードを指定のパスワードに変更します。
5.6と5.7でやり方が違う。5.7は前述のタスクでファイルから取り出したパスワードを使う。

```yaml:roles/mysql/tasks/main.yml
- name: backup default my.cnf
  shell: |
    mv /etc/my.cnf /etc/my.cnf.bk
  when: not default_my_conf_backup.stat.exists
```

>`my.cnf`のバックアップを作ります。

```yaml:roles/mysql/tasks/main.yml
- name: deploy my.cnf
  template:
    src: "my.cnf_{{ __version }}.j2"
    dest: "/etc/my.cnf"
    owner: root
    group: root
    mode: 0644
  when: not default_my_conf_backup.stat.exists
  notify: restart mysqld
```

>自分好みにカスタマイズした`my.cnf`を配置します。バージョンによって使えないオプションとかあるので注意。
配置したら設定を反映させるため、`notify`を使って再起動を通知します。
`handler`は`roles/xxx/handlers/main.yml`に書く。

>```yaml:roles/mysql/handlers/main.yml
- name: restart mysqld
  service:
    name: mysqld
    state: restarted
```

```yaml:roles/mysql/tasks/main.yml
- name: create database [{{ __project_db_name }}]
  mysql_db:
    login_user: root
    login_password: "{{ __root_new_password }}"
    name: "{{ __project_db_name }}"
    state: present
    encoding: "{{ __project_db_encoding }}"
  when: __project_db_create
```

>オプションでアプリのデータベースを作成します。今時のフレームワークはマイグレーションの仕組みがそれぞれあるので自分でデータベースを作成するのは稀かもしれませんが。
`mysql_db`という便利なモジュールがあるのでそれを使います。

```yaml:roles/mysql/tasks/main.yml
- name: create MySQL user [{{ __project_db_user_name }}] for database [{{ __project_db_name }}]
  mysql_user:
    login_user: root
    login_password: "{{ __root_new_password }}"
    user: "{{ __project_db_user_name }}"
    password: "{{ __project_db_user_password }}"
    host: localhost
    priv: "{{ __project_db_name }}.*:ALL,GRANT"
    state: present
  when: __project_db_create
```

>さっき作ったデータベース用のユーザーを作成します。
同じく`mysql_user`というモジュールを活用します。（便利！）

## Ruby

```yaml:roles/ruby/tasks/main.yml
- name: install git and other dependencies
  yum:
    name: "{{ item }}"
    state: latest
  with_items:
    - git
    - nss
    - nss-util
    - nspr
    - curl
```

>Ruby入れるために色々インストールします。

```yaml:roles/ruby/tasks/main.yml
- name: install rbenv
  git:
    repo: https://github.com/rbenv/rbenv.git
    dest: ~/.rbenv
  become_user: "{{ __working_user }}"
```

>`rbenv`インストールします。`git`もモジュールあるよ！

```yaml:roles/ruby/tasks/main.yml
- name: set global rbenv path
  template:
    src: 001_rbenv.sh
    dest: ~/.bash.d/001_rbenv.sh
    mode: 0755
  become_user: "{{ __working_user }}"
```

>`rbenv`パス通します。
中身はこんな感じ。

>```sh:roles/ruby/templates/001_rbenv.sh
export PATH=~/.rbenv/bin:~/.rbenv/shims:$PATH
eval "$(rbenv init -)"
```

```yaml:roles/ruby/tasks/main.yml
- name: install dependencies for ruby build
  yum: 
    name: "{{ item }}"
    state: present
  with_items:
    - gcc
    - openssl-devel
    - libyaml-devel
    - libffi-devel
    - readline-devel
    - zlib-devel
    - gdbm-devel
    - ncurses-devel
```

>Rubyビルドするために色々インストールします。

```yaml:roles/ruby/tasks/main.yml
- name: install ruby-build
  git:
    repo: https://github.com/sstephenson/ruby-build.git
    dest: ~/.rbenv/plugins/ruby-build
  become_user: "{{ __working_user }}"
```

>`ruby-build`インストールします。

```yaml:roles/ruby/tasks/main.yml
- name: make app directory (if rails new)
  file:
    path: "{{ __app_dir }}"
    state: directory
    mode: 0755
  become_user: "{{ __working_user }}"
  when: __rails_new
```

>既存のRailsアプリがない場合、指定のバージョンでスケルトン作るのでアプリ用のディレクトリ作ります。

```yaml:roles/ruby/tasks/main.yml
- name: check current ruby version
  shell: |
    /bin/bash -lc "ruby -v | grep -oP '^ruby \d{1}.\d{1}.\d{1}' | cut -f 2 -d ' '"
  args:
    chdir: "{{ __app_dir }}"
  register: current_ruby_version
  become_user: "{{ __working_user }}"
  changed_when: False
```

>Rubyのバージョンチェックします。初回実行時はシステムデフォルトのバージョン(2.0とか)が取れるので、それでRubyインストールするかどうかを判断します。
一点注意すべきなのは、`shell`は`/bin/sh`を使うので、`bash`で通したPATHとかを使おうとしてもエラーになることです。`/bib/bash -lc "コマンド"`で明示的にbashシェルで実行します。

```yaml:roles/ruby/tasks/main.yml
- name: shell relogin
  shell: |
    /bin/bash -lc  "source ~/.bash_profile"
  become_user: "{{ __working_user }}"
  when: current_ruby_version.stdout != __version
```

>ログインし直してパス通します。

```yaml:roles/ruby/tasks/main.yml
- name: install ruby
  shell: |
    /bin/bash -lc "rbenv install {{ __version }}"
    /bin/bash -lc "gem install bundler"
    /bin/bash -lc "rbenv rehash"
    /bin/bash -lc "rbenv local {{ __version }}"
  args:
    chdir: "{{ __app_dir }}"
  become_user: "{{ __working_user }}"
  when: current_ruby_version.stdout != __version
```

>指定のバージョンのRubyインストールします。で、アプリのルートで使います。

```yaml:roles/ruby/tasks/main.yml
- name: check rails new succeeded
  stat:
    path: "{{ __app_dir }}/Gemfile.lock"
  become_user: "{{ __working_user }}"
  register: rails_new_gemfile
  when: __rails_new
```

>Railsアプリのスケルトンを作る場合のみ、すでにスケルトン作成済みかどうかチェックします。

```yaml:roles/ruby/tasks/main.yml
- name: init bundler
  shell: |
    /bin/bash -lc "bundle init"
  args:
    chdir: "{{ __app_dir }}"
  become_user: "{{ __working_user }}"
  when: __rails_new and not rails_new_gemfile.stat.exists

- name: gem install rails
  lineinfile:
    path: "{{ __app_dir }}/Gemfile"
    regexp: '# gem "rails"'
    line: 'gem "rails", "{{ __rails__version }}"'
  become_user: "{{ __working_user }}"
  when: __rails_new and not rails_new_gemfile.stat.exists
```

>スケルトン用の`Gemfile`作って、指定のバージョンのrailsを追記します。

```yaml:roles/ruby/tasks/main.yml
- name: rails new
  shell: |
    /bin/bash -lc "gem install bundler"
    /bin/bash -lc "rbenv rehash"
    /bin/bash -lc "bundle install --path vendor/bundle --jobs=4"
    /bin/bash -lc "bundle exec rails new -f {{ __app_dir }}"
  args:
    chdir: "{{ __app_dir }}"
  become_user: "{{ __working_user }}"
  when: __rails_new and not rails_new_gemfile.stat.exists
```

>`rails new`します。

```yaml:roles/ruby/tasks/main.yml
- name: bundle install (if app already exists)
  shell: |
    /bin/bash -lc "bundle install"
  args:
    chdir: "{{ __app_dir }}"
  become_user: "{{ __working_user }}"
  when: current_ruby_version.stdout != __version and not __rails_new
```

>すでにRailsアプリをgit clone済み（マウント済み）でスケルトンを作らない場合、初回`bundle install`します。

## Node.js

```yaml:roles/nodejs/tasks/main.yml
- name: check if nodejs already installed
  stat:
    path: /usr/local/bin/node
  register: nodejs_bin
```

>すでにインストール済みかチェック。

```yaml:roles/nodejs/tasks/main.yml
- name: download and setup nodejs source
  shell: |
    curl -sL https://rpm.nodesource.com/setup_8.x | bash -
  when: not nodejs_bin.stat.exists
```

>インストールされていない初期状態であれば、`curl`でソースを持ってきてビルドします。

```yaml:roles/nodejs/tasks/main.yml
- name: install latest nodejs temporary
  yum:
    name: "{{ item }}"
    state: present
  with_items: 
    - nodejs
  when: not nodejs_bin.stat.exists
```

>yum経由でインストールします。この時点では一時的なのでバージョンとかなんでもいいんです。

```yaml:roles/nodejs/tasks/main.yml
- name: install n
  npm:
    name: n
    state: present
    global: yes
  when: not nodejs_bin.stat.exists
```

>`npm`コマンドが使えるようになったので、バージョン管理のための`n`というパッケージをインストールします。

```yaml:roles/nodejs/tasks/main.yml
- name: install version specific nodejs using n
  shell: |
    n {{ __version }}
  when: not nodejs_bin.stat.exists
```

>この`n`を使って好きなバージョンをインストールしちゃいましょう。

```yaml:roles/nodejs/tasks/main.yml
- name: set global npm path
  template:
    src: 201_nodejs.sh
    dest: ~/.bash.d/201_nodejs.sh
    mode: 0755
  become_user: "{{ __working_user }}"
  when: not nodejs_bin.stat.exists
```

>`NODE_PATH`をセットする設定を`.bash_profile`に追加します。（直接追加してるわけではないけど）
中身はこれです。

>```sh:roles/nodejs/templates/201_nodejs.sh
export NODE_PATH=$(npm root -g)
```

```yaml:roles/nodejs/tasks/main.yml
- name: install yarn
  npm:
    name: yarn
    state: present
    global: yes
```

>`yarn`よく使うので入れちゃいます。

## Redis

```yaml:roles/redis/tasks/main.yml
- name: check if source already downloaded
  stat:
    path: /tmp/redis-{{ __version }}
  register: redis_source
```

>インストール済みかチェックする。

```yaml:roles/redis/tasks/main.yml
- name: download and unarchive source
  unarchive:
    src: http://download.redis.io/releases/redis-{{ __version }}.tar.gz
    dest: /tmp
    copy: no
  when: not redis_source.stat.exists
```

>まだインストールしてなかったらソース持ってきて解凍する。`unarchive`モジュール便利！

```yaml:roles/redis/tasks/main.yml
- name: make and install
  make:
    chdir: /tmp/redis-{{ __version }}
    target: install
  when: not redis_source.stat.exists
```

>`make`, `make install`もモジュールでできちゃいます。

```yaml:roles/redis/tasks/main.yml
- name: check if binary source already deployed
  stat:
    path: /usr/local/bin/redis-server
  register: redis_server_bin_check
```

>`redis-server`コマンドにパスが通ってるかチェックする。

```yaml:roles/redis/tasks/main.yml
- name: deploy binary source
  shell: |
    cp redis-server redis-cli redis-sentinel redis-benchmark redis-check-rdb redis-check-aof /usr/local/bin
  args:
    chdir: /tmp/redis-{{ __version }}/src
  when: not redis_server_bin.stat.exists
```

>通ってなかったら諸々パス通します。

```yaml:roles/redis/tasks/main.yml
- name: check if config file deployed
  stat:
    path: /etc/redis/{{ __port }}.conf
  register: redis_conf
```

>設定ファイル配置済みかチェックする。

```yaml:roles/redis/tasks/main.yml
- name: make config directory
  file: 
    path: /etc/redis
    state: directory
    mode: 0755
```

>redisの設定ファイル置き場作る。

```yaml:roles/redis/tasks/main.yml
- name: deploy config file
  shell: |
    mv redis.conf /etc/redis/{{ __port }}.conf
  args:
    chdir: /tmp/redis-{{ __version }}
  when: not redis_conf.stat.exists
```

>設定ファイル置く。`/etc/redis/ポート番号.conf`という形にしてる。

```yaml:roles/redis/tasks/main.yml
- name: edit config file
  lineinfile:
    path: /etc/redis/{{ __port }}.conf
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    state: present
  with_items:
    - {regexp: "^dir ", line: "dir {{ __data_dir }}"}
    - {regexp: "^logfile ", line: "logfile {{ __log_dir }}/{{ __port }}.conf"}
    - {regexp: "^loglevel ", line: "loglevel {{ __log_level }}"}
    - {regexp: "^daemonize ", line: "daemonize yes"}
    - {regexp: "^port ", line: "port {{ __port }}"}
    - {regexp: "^supervised ", line: "supervised systemd"}
```

>設定ファイルまとめて書き換えてます。テンプレート化してもいいんだけど、バージョンによってどこまで差分出てくるかよく分からないので。。。

```yaml:roles/redis/tasks/main.yml
- name: create usergroup for redis daemon
  group:
    name: redis
    state: present
    system: yes

- name: create user for redis daemon
  user:
    name: redis
    group: redis
    state: present
    createhome: no
    system: yes
```

>redis用のグループとユーザー作ります。

```yaml:roles/redis/tasks/main.yml
- name: create data and log directory
  file:
    path: "{{ item }}"
    state: directory
    owner: redis
    group: redis
    mode: 0755
  with_items:
    - "{{ __data_dir }}"
    - "{{ __log_dir }}"
```

>データとログの置き場作ります。

```yaml:roles/redis/tasks/main.yml
- name: check if service already daemonized
  stat:
    path: /etc/systemd/system/redis.service
  register: redis_service
```

>デーモン化されているかチェックする。

```yaml:roles/redis/tasks/main.yml
- name: daemonize service
  template:
    src: redis.service.j2
    dest: /etc/systemd/system/redis.service
    owner: root
    group: root
    mode: 0755
  when: not redis_service.stat.exists
```

>デーモン化して`systemd`に管理させます。
テンプレートはこんな感じ。

>```jinja:roles/redis/templates/redis.service.j2
[Unit]
Description=Redis

>[Service]
Type=notify
ExecStart=/usr/local/bin/redis-server /etc/redis/{{ __port }}.conf
ExecStop=/usr/local/bin/redis-cli -p {{ __port }} shutdown
User=redis
Group=redis

>[Install]
WantedBy=multi-user.target
```

```yaml:roles/redis/tasks/main.yml
- name: reload daemon
  shell: |
    systemctl daemon-reload
  notify: start and enable server
  when: not redis_service.stat.exists
```

>再起動して終了。

## Elasticsearch

```yaml:roles/elasticsearch/tasks/main.yml
- name: install JDK-1.8.0
  yum:
    name: "{{ item }}"
    state: present
    enablerepo: "*debug*"
  with_items:
    - java-1.8.0-openjdk-devel
    - java-1.8.0-openjdk-debuginfo
```

>JDKをインストールします。

```yaml:roles/elasticsearch/tasks/main.yml
- name: deploy yum repository info 
  template:
    src: elasticsearch.repo
    dest: /etc/yum.repos.d/elasticsearch.repo
    mode: 0644
```

>Elasticsearchのリポジトリを登録します。
中身はこんな感じ。

>```roles/elasticsearch/templates/elasticsearch.repo
[elasticsearch-5.x]
name=Elasticsearch repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
```

>他のバージョン使いたいときは適宜作ってくださいますよう。

```yaml:roles/elasticsearch/tasks/main.yml
- name: install 
  yum:
    name: elasticsearch-{{ __version }}
    state: present
```

>指定のバージョンをインストールします。

```yaml:roles/elasticsearch/tasks/main.yml
- name: check if kuromoji plugin already installed
  stat:
    path: /usr/share/elasticsearch/plugins/analysis-kuromoji
  register: kuromoji_check
```

>`kuromoji`プラグインがインストール済みかチェックします。yumとかで入れるわけではないのでこの辺は自前で冪等性を担保する必要があります。

```yaml:roles/elasticsearch/tasks/main.yml
- name: install kuromoji plugin
  shell: |
    ./elasticsearch-plugin install analysis-kuromoji
  args:
    chdir: /usr/share/elasticsearch/bin/
  when: not kuromoji_check.stat.exists
```

>`kuromoji`プラグインのインストール。

```yaml:roles/elasticsearch/tasks/main.yml
- name: edit  elasticsearch.yml
  lineinfile:
    path: /etc/elasticsearch/elasticsearch.yml
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
  with_items:
    - { regexp: "^http.port: ", line: "http.port: {{ __port }}" }
    - { regexp: "^bootstrap.memory_lock: ", line: "bootstrap.memory_lock: true" }
    - { regexp: "^path.logs: ", line: "path.logs: /var/log/elasticsearch" }
    - { regexp: "^network.host: ", line: "network.host: 127.0.0.1" }
```

>設定ファイルをよしなに書き換えます。

```yaml:roles/elasticsearch/tasks/main.yml
- name: unlock memory limit for elasticsearch user
  lineinfile:
    path: /etc/security/limits.conf
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
  with_items:
    - { regexp: "^elasticsearch soft memlock unlimited", line: "elasticsearch soft memlock unlimited" }
    - { regexp: "^elasticsearch hard memlock unlimited", line: "elasticsearch hard memlock unlimited" }

- name: add LimitMEMLOCK option for elasticsearch.service
  lineinfile:
    path: /usr/lib/systemd/system/elasticsearch.service
    regexp: "^LimitMEMLOCK"
    line: "LimitMEMLOCK = infinity"
```

>Elasticsearchがメモリを最大限使えるように解放します。

```yaml:roles/elasticsearch/tasks/main.yml
- name: edit  jvm.options
  lineinfile:
    path: /etc/elasticsearch/jvm.options
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
  with_items:
    - { regexp: "^-Xms", line: "-Xms{{ __jvm_xms }}" }
    - { regexp: "^-Xmx", line: "-Xmx{{ __jvm_xmx }}" }
  notify: start and enable service
```

>jvmのヒープサイズを引き下げます。スペックにもよりますが、多分大半の場合メモリ不足になってElasticsearchが起動できなくなります。自分のマシンのスペックに応じて色々と変えてみてください。

# 以上でした
他によく使うミドルウェアってなんですかねー。
次はLAMP+Laravelでも作ろうかなー。