AnsibleでMacからVPSへRails環境を構築してみる

# 前置き
[過去の記事](https://qiita.com/items/ee66c13100242f756f03)でCentOSにRailsやMySQLをインストールする方法を投稿しました。読んで下さった方、ありがとうございました:pray:
手順を確立したものの手作業では面倒なので、サーバーの構成管理を自動化とコード化しておきたいと思います。インフラのコード化も触ってみたいと思って初めて触ってみましたが、お詳しい情報は難しく、シンプルな情報は少々古いものが多い印象でしたので、筆者が実施した内容を記事にしてみます。
なお、まずは作業だけを自動化してみたかったので、ディレクトリのベストプラクティスや、変数なども使用しませんでした。雰囲気や形から入っていきます。

# なぜAnsibleにしたのか
ChefやPuppetも候補にありましたが、真面目な理由順に書いていきます。
・ エージェントレス
・ 導入が簡単な印象(YAML)
・ 冪等性
・ 最近注目度が高い
・ [手元にある雑誌](http://gihyo.jp/magazine/SD/archive/2016/201601)に載ってた

# Ansibleの管理サーバーの環境について
| 　 | 　 |
|---|---|
|OS  |MacOS High Sierra 10.13.5|
|Xcode  |9.4.1 (9F2000)  |
|Ruby  |2.3.3p222  |
|Python  |2.7.15  |

# Homebrewのインストールする
こちらの[記事](https://qiita.com/rabbit1013/items/1494cf345ff172c3b9cd)を参考にさせて頂きました。
以下の環境で問題なくインストール出来ました。

1. インストールします。

    ```
    $ /usr/bin/ruby -e "$(curl -fsSL https\://raw.githubusercontent.com/Homebrew/install/master/install)"
    ==> This script will install:
    /usr/local/bin/brew
    /usr/local/share/doc/homebrew
          :
          :
    Press RETURN to continue or any other key to abort
    ==> /usr/bin/sudo /bin/chmod u+rwx /usr/local/bin
    Password:    # パスワードを入力
    ==> Deleting /Library/Caches/Homebrew...
    Already up-to-date.
    ==> Installation successful!
        
        ==> Homebrew has enabled anonymous aggregate user behaviour analytics.
        Read the analytics documentation (and how to opt-out) here:
          https://docs.brew.sh/Analytics.html
        
        ==> Next steps:
        - Run `brew help` to get started
        - Further documentation: 
          https://docs.brew.sh
    ```
1. アップデートしておきます。

    ```
    $ brew update
    Updated 1 tap (homebrew/core).
    ==> Updated Formulae
    offlineimap                              xmake
    ```
1. バージョンの確認と診断もします。
問題なし:thumbsup:

    ```
    $ brew -v
    Homebrew 1.6.8
    Homebrew/homebrew-core (git revision cac0; last commit 2018-06-16)
    $ brew doctor
    Your system is ready to brew.
    ```

# Ansibleをインストールする
1. まず本体をインストールします。
問題なし:thumbsup:

    ```
    $ brew install ansible
    $ ansible --version
    ansible 2.5.4
      :
      :
      python version = 2.7.15 (default, May  1 2018, 16:44:08) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.1)]
    ```

# ファイルの構成について
多数の素敵な説明が溢れているので、今回登場するファイルについて簡単に紹介します。同じディレクトリ階層に配置します。後で細かくみていきます。
[GitHub](https://github.com/devnokiyo/ansible-sample)に公開してます。

- ansible.cfg  
設定ファイルです。
- inventory  
構築する対象サーバーを書きます。今回はIPを書きます。「hosts」というファイル名で紹介されることが多いです。「インベントリーでしょ？」と思ったので、そのまんまファイル名しました。
- playbook.yml  
構築する内容を書きます。  
「○○をインストールする」とか「ユーザを作成する」とかです。

それでは、これよりひとつひとつみていきますね。

# ansible.cfg
書いたのは以下だけです。今回もConoHaのCentOSサーバーを新規に用意して構築します。手動で初めてサーバーにSSHでログインする時、フィンガープリントを登録するか聞かれますね。ansibleは管理サーバーから対象サーバーにSSH経由で操作しているだけなので、仕組みは同じです。登録されていないとエラーになってしまうので、自動的に続行させるようにしました。

```
[defaults]
host_key_checking = False
```

# inventory
構築したいサーバーのIPアドレスを書いてください。
今回は愛機のMacBook ProからConoHaのVPSサーバーの構築を行います。

```
[devserver]
(ConoHa VPSのIPアドレス)
```

# playbook.yml
少々くどくなりそうですが、個々に説明していきます。

1. inventoryの「devserver」を指定します。

    ```
    - hosts: devserver
    ```

1. rootユーザで実行します。

    ```
      remote_user: root
    ```
1. rootのSSHを禁止します。
sshd_configに行を追加しています。
この直後にsshdを再起動してしまうと後続のタスクを実行出来なくなので最後に行います。まさにrootユーザで実行している最中なので:sweat_smile:

    ```
      tasks:
      - name: rootのSSHを禁止します。
        lineinfile:
          dest: /etc/ssh/sshd_config
          regexp: "^PermitRootLogin"
          insertafter: "^#PermitRootLogin"
          line: "PermitRootLogin no"
    ```
1. CentOSに「rails」ユーザを作成します。

    ```
    - name: railsユーザを作成します。
      group:
        name: rails
    - name: railsユーザを作成します。
      user:
        name: rails
        password: "{{ 'Hoge123!'|password_hash('sha512') }}"
        shell: /bin/bash
        group: rails
        groups: wheel
        append: yes
    ```
1. yumでGitをインストールします。

    ```
      - name: Gitをインストールします。
          yum:
            name: git
            state: latest
    ```
1. rbenvをgit cloneします。

    ```
      - name: rbenvをインストールします。Gitから取得します。
          git:
            repo: "https://github.com/rbenv/rbenv.git"
            dest: "/usr/local/src/rbenv"
    ```
1. rbenvのパスを通して再読み込みします。
構築対象のサーバーの環境変数を参照したい場合は、commandではなくshellを使います。

    ```
      - name: rbenvをインストールします。パスを通して再読み込みします。
          shell: |
            echo 'export RBENV_ROOT="/usr/local/src/rbenv/"' > /etc/profile.d/rbenv.sh　
            echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"' >> /etc/profile.d/rbenv.sh
            echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh
            . /etc/profile.d/rbenv.sh
    ```
1. yumでRubyのインストールに必要なパッケージをインストールします。
今度は複数のパッケージを一括でインストールしてみます。

    ```
      - name: Rubyのインストールに必要なパッケージからインストールします。
          yum: name={{ item }} state=latest
          with_items:
            - bzip2
        - openssl-devel
        - readline-devel
        - zlib-devel
        - gcc
        - python-pip                     # このパッケージのみansible用にインストールします。
    ```
1. pipでpexpectをインストールします。
y/Nとかpasswordとかを対話形式で問われた時に自動応答してくれるパッケージです。

    ```
      - name: pexpectをインストールします。    # 対話形式に答えるためのモジュールです。
          pip:
            name: pexpect
    ```
1. ruby-buildをgit cloneします。

    ```
      - name: Rubyのインストールに必要なパッケージからインストールします。
          git:
            repo: "https://github.com/rbenv/ruby-build.git"
            dest: "/usr/local/src/rbenv/plugins/ruby-build"
    ```
1. ruby-buildをインストールします。
今度はshellではなくcommandで行なっています。

    ```
      - name: Rubyのインストールに必要なパッケージからインストールします。
          command: /usr/local/src/rbenv/plugins/ruby-build/install.sh
    ```
1. Ruby本体をインストールします。
リターンコード:0以外が返ってくる時でも「成功」と見なしています。

    ```
      - name: Ruby本体をインストールします。
          command: rbenv install 2.5.1
          register: rbenv_return_code
          failed_when: rbenv_return_code.rc not in [0, 1]
    ```
1. 使用するRubyを指定します。

    ```
      - name: 使用するRubyを指定します。
          command: rbenv global 2.5.1
    ```
1. Railsをインストールします。
commandではなくgem用の構文もあるのですが、オプションの指定方法の調査をサボりました:bow:

    ```
      - name: Railsをインストールします。
          command: gem install -N rails
    ```
1. bundlerをインストールします。
pexpectを利用して対話形式を自動応答します。

    ```
      - name: bundlerをインストールします。
          expect:
            command: gem install bundler
            responses:
              'Overwrite the executable? [yN]': "y"
    ```
1. Rubyを最新にします。
最近は不要になったようですね。

    ```
      - name: Railsをインストールします。
          command: rbenv rehash
    ```
1. yumでMariaDBをアンインストールします。

    ```
      - name: MariaDBをアンインストールします。
          yum:
            name: mariadb-libs
            state: removed
    ```
1. MySQLのリポジトリを登録します。yum localinstallです。

    ```
      - name: MySQLのリポジトリを登録します。
          yum:
            name: http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm
            state: present
    ```
1. yumでMySQLをインストールします。

    ```
      - name: MySQLをインストールします。
          yum:
            name: mysql-community-server
            state: present
    ```
1. MySQLの起動と自動起動の設定をします。

    ```
      - name: MySQLの起動と自動起動の設定をします。
          systemd:
            name: mysqld
            state: started
            enabled: yes
    ```
1. MySQLのrootの初期パスワードを探して、mysql_root_passwordに格納します。

    ```
      - name: MySQLのrootの初期パスワードを探します。
          shell: cat /var/log/mysqld.log | grep "temporary password" | awk '{print $NF}'
          register: mysql_root_password
    ```
1. MySQLの基本的な初期設定を行います。
mysql_secure_installationに関しては、SQLで直接実行する情報が多くありました。
対話形式の自動応答で対応する記述は珍しいのかもしれません。自動応答の文字列は正規表現がデフォルトのようですので、前方一致で手抜きしました。そうです！エスケープすら面倒だったのです。

    ```
      - name: 基本的な初期設定を行います。
          expect:
            command: mysql_secure_installation
            responses:
              'Enter password for user root:': "{{ mysql_root_password.stdout }}"
              'New password:': "Hogehoge123!"
              'Re-enter new password:': "Hogehoge123!"
              'Change the password for root.*:': "y"
              'Do you wish to continue with the password provided.*:': "y"
              'Remove anonymous users.*:': "y"
              'Disallow root login remotely.*:': "y"
              'Remove test database and access to it.*:': "y"
              'Reload privilege tables now.* :': "y"
    ```
1. my.cnfを書き換えて、文字コードの設定を変更します。

    ```
      - name: MySQLの設定ファイルを書き換えます。
          lineinfile:
            path: /etc/my.cnf
            line: "{{ item }}"
          with_items:
            - "character-set-server=utf8mb4    #[mysqld]セクション\n\n"
            - "[client]"
            - "default-character-set=utf8mb4"
    ```
1. MySQLを再起動します。

    ```
      - name: MySQLを再起動します。
          systemd:
            name: mysqld
            state: restarted
    ```
1. Railsアプリ用のDBアカウントを作成します。
「'」のエスケープを忘れずに。私はハマりました。

    ```
      - name: Railsアプリ用のDBアカウントを作成します。
          shell: |
                echo 'GRANT ALL ON *.* TO '\''rails'\''@'\''%'\'' IDENTIFIED BY '\''Hogehoge123!'\'';' | mysql -uroot -pHogehoge123! -vvv
    ```
1. Rails向けのMySQLパッケージをインストールします。

    ```
      - name: Rails向けのMySQLパッケージをインストールします。
          yum:
            name: mysql-devel
            state: latest
      - name: Rails向けのMySQLパッケージをインストールします。
          command: gem install mysql2 -v '0.5.1'
    ```
1. JavaScriptの実行環境をインストールします。

    ```
      - name: JavaScriptの実行環境をインストールします。
          yum: name={{ item }} state=latest
          with_items:
            - nodejs
            - npm
    ```
1. ファイヤーウォールを一時的に停止します。

    ```
      - name: ファイヤーウォールを一時的に停止します。
          systemd:
            name: firewalld
            state: stopped
            enabled: yes
    ```
1. SSHを再起動します。

    ```
      - name: SSHを再起動します。
          systemd:
            name: sshd
            state: restarted

    ```

# いよいよ実行する
まず、Macのターミナルを開き、3ファイルのあるパスへ移動します。

```
$ cd ~/ansible
$ ls
ansible.cfg	inventory	playbook.yml
```

以下のコマンドを実行します。ConoHa VPSサーバー rootのパスワードを聞かれますので、手動で入力します。本来は公開鍵を手動で送っておくなどするそうです。

```
$ ansible-playbook -i inventory playbook.yml --ask-pass
SSH password: 
```
すると、このように自動的に構成されます。

```
PLAY [devserver] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [ConoHa VPSのIP]

TASK [rootのSSHを禁止します。] *********************************************************
changed: [ConoHa VPSのIP]
 :
 :
TASK [SSHを再起動します。] *************************************************************
changed: [ConoHa VPSのIP]

PLAY RECAP *********************************************************************
[ConoHa VPSのIP]              : ok=30   changed=29   unreachable=0    failed=0   

```

構築は完了しているので、[こちら](https://qiita.com/items/3afd5998bcc0887fc88a)のようにRailsのプロジェクトを作って、サーバーを起動してみてください。

# 終わりに
記述が多くなるについてRoleやディレクトリ分けをしたくなってきました。ただ、動かす前に挫折しそうでしたので、動かすところをやってみました。コマンドを手動で記述してしまうと冪等性を自身で確保しなくてはなりませんでした。少しずつ慣れて行きたいと思います。