AnsibleでRedmineをCentOSにインストール

Ansibleを理解するため、仮想サーバへRedmineをインストールするPlaybookを作成、その内容をまとめました。

## 環境

| ソフトウェア           | バージョン       |
|------------------------|------------------|
| OS(ローカル、リモート) | CentOS 7.5       |
| Redmine                | Redmine 3.4      |
| データベース           | PostgreSQL 9.2.24|
| Webサーバ              | Apache 2.4.6     |
| Ruby                   | 2.4.4            |

## ローカル側ファイル構成

```
/root/ansible
|-- group_vars                    # グループで使用する変数を定義
|   `-- test-servers
|-- hosts                         # インベントリファイル
|-- redmine.yml                   # コマンド実行時に指定するプレイブック
`-- roles
    |-- apache                    # Apache設定用role
    |   |-- tasks
    |   |   `-- main.yml
    |   `-- templates
    |       `-- redmine.conf
    |-- common                    # 共通role（OSの設定等）
    |   `-- tasks
    |       `-- main.yml
    |-- postgresql                # PostgreSQL設定用role
    |   |-- files
    |   |   `-- pg_hba.conf.diff
    |   `-- tasks
    |       `-- main.yml
    |-- redmine                   # Redmine設定用role
    |   |-- tasks
    |   |   `-- main.yml
    |   `-- templates
    |       |-- configuration.yml
    |       `-- database.yml
    `-- ruby                      # Ruby設定用role
        `-- tasks
            `-- main.yml
```

## 各ファイルの内容
### 1. インベントリファイル
/root/ansible/hosts

```hosts
[test-servers]
192.168.11.51

[test-servers:vars]
ansible_user=root
ansible_ssh_pass=P@ssw0rd
```
- `ansible_user`、`ansible_ssh_pass`にリモートサーバのユーザとパスワードを設定する。

### 2. プレイブック
/root/ansible/redmine.yml

```redmine.yml
---
- hosts: test-servers
  pre_tasks:
    - name: Start message
      debug: msg="Redmineのインストールを開始します。"
    
    - name: Create directory on the local side
      local_action:
        module: file
        path: "{{ local_work }}"
        state: directory
        mode: 0777

    - name: Create directory on the remote side
      file:
        path: "{{ remote_work }}"
        state: directory
        mode: 0777
  
  roles:
    - common
    - ruby
    - postgresql
    - redmine
    - apache
  
  post_tasks:
    - name: Delete directory on the remote side
      file:
        path: "{{ remote_work }}"
        state: absent

    - name: End message
      debug: msg="Redmineのインストールが完了しました。"
```
- `pre_tasks`でローカル、リモート側にワークディレクトリを作成する。
- `post_tasks`でリモート側のワークディレクトリを削除する。

### 3. グループの変数定義
/root/ansible/group_vars/test-servers

```test-servers
ruby_download_url: https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
ruby_version: ruby-2.4.4
redmine_svn_url: https://svn.redmine.org/redmine/branches/3.4-stable
db_user: redmine
db_pass: redmine
db_pass_md5: md5c5a58b543b1cad88fa10f629c0e535c1
local_work: /tmp/ansible_work
remote_work: /tmp/ansible_work
```
- PostgreSQLのパスワードとなる`db_pass`,`db_pass_md5`は、任意のパスワードに変更する。
- `db_pass_md5`は、パスワードを暗号化した内容。'str["md5"] + md5[ password + username ]'の形で、コマンドでは`echo "md5$(echo -n 'redmineredmine' | md5sum)"`により作成する。

### 4. 共通role
/root/ansible/roles/common/tasks/main.yml

```main.yml
---
# SELinuxを無効にする
- name: Disable SELinux
  selinux: state=disabled
  register: result_selinux

- block:
  # サーバを再起動する
  - name: Restart the server
    shell: sleep 2 && shutdown -r now
    async: 1
    poll: 0
    ignore_errors: true
  
  # サーバの起動を待つ
  - name: Wait for server startup
    local_action: wait_for host={{ inventory_hostname }} port=22 state=started delay=10
  
  when: result_selinux.reboot_required

# ファイアウォールのhttpポートを許可する
- name: Allow firewall http port
  firewalld:
    service: http
    permanent: true
    state: enabled
    immediate: yes

# パッケージのインストール
# インストール済みのパッケージを更新する
- name: Update installed packages
  yum:
    name: '*'
    state: latest

# 開発ツールのインストール
- name: Install development tools
  yum:
    name: "@Development Tools"
    state: present

# RubyとPassengerに必要なパッケージのインストール
- name: Install necessary packages for Ruby and Passenger
  yum:
    name: 
      - openssl-devel
      - readline-devel
      - zlib-devel
      - curl-devel
      - libyaml-devel
      - libffi-devel
      - mysql-devel
    state: present

# PostgreSQLに必要なパッケージのインストール
- name: Install necessary packages for PostgreSQL
  yum:
    name:
      - postgresql-server
      - postgresql-devel
      - python-psycopg2
    state: present

# Apacheに必要なパッケージのインストール
- name: Install necessary packages for Apache
  yum:
    name:
      - httpd
      - httpd-devel
    state: present

# ImageMagick・日本語フォントのインストール
- name: Installation of ImageMagick, Japanese font
  yum:
    name:
      - ImageMagick
      - ImageMagick-devel
      - ipa-pgothic-fonts
    state: present
```
- 「/etc/sysconfig/selinux」の変更ではなく、`selinux`モジュールにより、SELinuxを無効化する。
- SELinuxの変更により、再起動が必要かどうかは、結果の`reboot_required`で判定する。
- `mysql-devel`は、Rubyのパッケージ管理ツール「bundler」のインストール時に必要。
- PostgreSQLの`python-psycopg2`は、ansibleでPostgreSQLのモジュール実行に必要。

### 5. Ruby設定用role
/root/ansible/roles/ruby/tasks/main.yml

```main.yml
---
# Rubyのインストールを確認する
- name: Confirm installation of Ruby
  stat:
    path: /usr/local/bin/ruby
  register: ruby_state

- block:
  # ソースコードをダウンロードする
  - name: Download the source code
    get_url:
      url: "{{ ruby_download_url }}"
      dest: "{{ remote_work }}/{{ ruby_version }}.tar.gz"
    register: result_get_url
  
  # ダウンロードファイルを確認する
  - name: Check the download file
    stat:
      path: "{{ result_get_url.dest }}"
    register: archive_file_state
  
  # ダウンロードファイルを解凍する
  - name: Unzip the download file
    unarchive:
      src: "{{ result_get_url.dest }}"
      dest: "{{ remote_work }}"
      remote_src: yes
    when: archive_file_state.stat.exists
  
  # Rubyのコンパイル・インストール
  - name: Compiling and installing Ruby
    command: >
      {{ item }}
      chdir={{ remote_work }}/{{ ruby_version }}/
    with_items:
      - 'autoconf'
      - './configure --disable-install-doc'
      - 'make'
      - 'make install'
  
  # bundlerのインストール
  - name: Install bundler
    gem:
      name: bundler
      state: present
      include_doc: no
      user_install: false
  
  when: not ruby_state.stat.exists
```
- Rubyのソースは`curl`コマンドではなく、`get_url`モジュールでダウンロードする。
- `unarchive`モジュールでリモート側のファイルを解凍を行う場合`remote_src: yes`の指定が必要。
- `gem`モジュールで`user_install: false`指定しないと~/.gemへインストールされる。

### 6. PostgreSQL設定用role
/root/ansible/roles/postgresql/tasks/main.yml

```main.yml
---
# PostgreSQLの設定
# initdbが実行されたか確認する
- name: Check if initdb has been executed
  find:
    paths: /var/lib/pgsql/data
    patterns: "*"
  register: result_find

- block:
  # データベースクラスタを新規作成する
  - name: Create a new database cluster
    command: postgresql-setup initdb
  
  # pg_hba.conf ファイルのバックアップ
  - name: Back up pg_hba.conf
    fetch:
      src: /var/lib/pgsql/data/pg_hba.conf
      dest: "{{ local_work }}"
  
  # 差分ファイルをコピーする
  - name: Copy the difference file in pg_hba.conf
    copy:
      src: pg_hba.conf.diff
      dest: "{{ remote_work }}"

  # 差分ファイルを反映する
  - name: Reflect difference file in pg_hba.conf
    shell: patch -t /var/lib/pgsql/data/pg_hba.conf < {{ remote_work }}/pg_hba.conf.diff

  # PostgreSQLのサービスを起動する
  - name: Start PostgreSQL service
    systemd: 
      name: postgresql.service
      state: started
      enabled: True
  
  # ユーザーを追加する
  - name: Add PostgreSQL user
    postgresql_user: 
      name: "{{ db_user }}"
      password: "{{ db_pass_md5 }}"
      state: present
    become: yes
    become_method: su
    become_user: postgres
  
  # データベースを作成する
  - name : Create a database
    postgresql_db: 
      name: redmine
      encoding: UTF-8
      lc_collate: ja_JP.UTF-8
      lc_ctype: ja_JP.UTF-8
      template: template0
      owner: "{{ db_user }}"
      state: present
    become: yes
    become_method: su
    become_user: postgres
  
  when: result_find.matched == 0
```

/root/ansible/roles/postgresql/files/pg_hba.conf.diff

```pg_hba.conf.diff
*** pg_hba.conf.original	2018-10-19 00:00:00.000000000 +0900
--- pg_hba.conf	2018-10-19 00:00:00.000000000 +0900
***************
*** 71,76 ****
--- 71,78 ----
  # "host" records.  In that case you will also need to make PostgreSQL
  # listen on a non-local interface via the listen_addresses
  # configuration parameter, or via the -i or -h command line switches.
+ host    redmine         redmine         127.0.0.1/32            md5
+ host    redmine         redmine         ::1/128                 md5
```
- 修正前のpg_hba.confを、ローカル環境へバックアップする。
- pg_hba.confへのレコードの追加を`blockinfile`モジュールではなく、`patch`コマンドで行う。事前にdiffコマンドで差分ファイルを作成する必要がある。

### 7. Redmine設定用role
/root/ansible/roles/redmine/tasks/main.yml

```main.yml
---
# Redmineのダウンロード
- name: Download Redmine
  subversion:
    repo: "{{ redmine_svn_url }}"
    dest: /var/lib/redmine

# database.ymlを作成
- name: Create database.yml
  template:
    src: database.yml
    dest: /var/lib/redmine/config/database.yml
    force: no

# configuration.ymlを作成
- name: Create configuration.yml
  template:
    src: configuration.yml
    dest: /var/lib/redmine/config/configuration.yml
    force: no

# gemパッケージをインストール
- name: Install gem package
  bundler:
    gem_path: vendor/bundle
    state: present
    chdir: /var/lib/redmine
  register: result_bundler

# セッション改ざん防止用秘密鍵を作成する
- name: Create a secret key for preventing session tampering
  command: bundle exec rake generate_secret_token
  args:
    chdir: /var/lib/redmine
  when: result_bundler.changed

# データベースのテーブルを作成する
- name: Create a database table
  command: bundle exec rake db:migrate
  args:
    chdir: /var/lib/redmine
  environment:
    RAILS_ENV: production
  when: result_bundler.changed

# デフォルトデータを登録する
- name: Register default data
  command: bundle exec rake redmine:load_default_data
  args:
    chdir: /var/lib/redmine
  environment:
    RAILS_ENV: production
    REDMINE_LANG: ja
  when: result_bundler.changed
```

/root/ansible/roles/redmine/templates/configuration.yml

```configuration.yml
production:
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      address: "localhost"
      port: 25
      domain: "{{ ansible_fqdn }}"

  rmagick_font_path: /usr/share/fonts/ipa-pgothic/ipagp.ttf
```

/root/ansible/roles/redmine/templates/database.yml

```database.yml
production:
  adapter: postgresql
  database: redmine
  host: localhost
  username: redmine
  password: "{{db_pass}}"
  encoding: utf8
```
- database.yml,configuration.ymlは、`template`モジュールにより、変数の値を設定したファイルを作成する。
- configuration.ymlの`{{ ansible_fqdn }}`により、"localhost.localdomain"が設定される。

### 8. Apache設定用role
/root/ansible/roles/apache/tasks/main.yml

```main.yml
---
# Passengerのインストールを確認する
- name: Confirm Passenger installation
  command: gem list
  register: gem_list
  changed_when: false
  args:
    chdir: /var/lib/redmine

- block:
  # Passengerのインストール
  - name: Install Passenger
    gem:
      name: passenger
      state: present
      include_doc: no
      user_install: false
  
  # PassengerのApache用モジュールのインストール
  - name: Install Passenger's Apache module
    command: passenger-install-apache2-module --auto --languages ruby
    args:
      chdir: /var/lib/redmine

  when: "'passenger' not in gem_list.stdout"

# Apache用設定内容の確認
- name: Confirm setting contents for Apache
  command: passenger-install-apache2-module --snippet
  register: result_passenger_snippet
  changed_when: false
  args:
    chdir: /var/lib/redmine

# redmine.confの作成
- name: Create redmine.conf
  template:
    src: redmine.conf
    dest: /etc/httpd/conf.d/redmine.conf

# RedmineフォルダをApacheユーザの所有に変更する
- name: Change Redmine folder to owned by Apache user
  file: 
    path: /var/lib/redmine
    owner: apache
    group: apache
    recurse: true

# httpdのサービスを起動する
- name: Start the service of httpd
  systemd: 
    name: httpd.service
    state: started
    enabled: True
```

/root/ansible/roles/apache/templates/redmine.conf

```redmine.conf
<Directory "/var/lib/redmine/public">
  Require all granted
</Directory>

Alias /redmine /var/lib/redmine/public
<Location /redmine>
  PassengerBaseURI /redmine
  PassengerAppRoot /var/lib/redmine
</Location>

{{ result_passenger_snippet.stdout }}

PassengerMaxPoolSize 20
PassengerMaxInstancesPerApp 4
PassengerPoolIdleTime 864000
PassengerStatThrottleRate 10

Header always unset "X-Powered-By"
Header always unset "X-Runtime"
```
- redmine.confは、`template`モジュールにより、`passenger-install-apache2-module --snippet`で出力した内容を反映する。

## コマンドの実行

```
# cd /root/ansible
# ansible-playbook -i hosts redmine.yml --syntax-check

playbook: redmine.yml
# ansible-playbook -i hosts redmine.yml
```
事前に`--syntax-check`でPlaybookの文法チェック

完了後、「http://リモートサーバIPアドレス/redmine」で接続する。