AnsibleでNginx + PHP-FPM + MySQL 環境構築（CentOS8）

この記事は[アクシス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/mediaxis) 4日目です。

# はじめに
2020年10月に未経験で入社したムラマツです。
今回の記事は、Ansibleに触れる機会をいただいたこともあり、
復習をかねてAnsibleを使って環境構築することについて重点的に書きます。
Ansibleの概念や詳細の機能などに関しては記事が長くなりすぎるので、
またの機会に投稿していこうと思います。

まだまだ至らぬ点が多々あるかと思いますが、
間違い等ございましたらご指摘いただけると幸いです:bow_tone1:

# 行うこと
AnsibleでNginx + PHP-FPM + MySQLの環境構築

# 前提
【環境】
・CentOS  8.2.2004
・Ansible 2.9.13
・DockerやVagrantなどでweb/dbコンテナ（サーバー）を構築済み

【以下をインストールしていきます】
・Nginx   1.18.0
・PHP     8.0.0
・MySQL   8.0.22



# 本題
webサーバーとdbサーバーに分け、それぞれに接続を行なっていきます。

・inventoryファイル
ファイル名 hosts

```
[web]
webserver

[db]
dbserver

[all:vars]
ansible_ssh_user=deploy
```

・playbookファイル

```site.yml
---
- import_playbook: common.yml
- import_playbook: web.yml
- import_playbook: db.yml
```

```common.yml
---
- hosts: all
  become: yes
  roles:
    - { role: firewalld, tags: firewalld }
```

```web.yml
---
- hosts: web
  become: yes
  roles:
    - { role: nginx,     tags: nginx     }
    - { role: php-fpm,   tags: php-fpm   }
```

```db.yml
---
- hosts: db
  become: yes
  roles:
    - { role: mysqld, tags: mysqld }
```

上記設定で進めて参ります。

## Nginx
まずはNginxからインストールしていきます。

```roles/nginx/tasks/main.yml
---
- name: Repo nginx
  template:
    src: nginx.repo.j2
    dest: /etc/yum.repos.d/nginx.repo

- name: Install nginx
  dnf:
    name: nginx
    disablerepo: AppStream
    state: latest

- name: Transfer nginx.conf
  template:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
  notify: restart nginx

- name: Transfer default.conf.j2
  template:
    src: default.conf.j2
    dest: /etc/nginx/conf.d/default.conf
  notify: restart nginx

- name: Automatic load when boot
  service:
    name: nginx
    enabled: yes

- name: Start nginx
  service:
    name: nginx
    state: started

## php-fpmとの連携確認のため作成
- name: Make hello world in nginx/html
  template:
    src: index.php.j2
    dest: /usr/share/nginx/html/index.php
  notify: restart nginx
```
CentOS8では「AppStream」というRHEL8で発表された新しい概念が導入され、
nginxのリポジトリ設定をしなくてもAppStreamリポジトリからインストールが可能です。

ですが今回はNginx公式のstableリポジトリからインストールを行うため以下のnginx.repoを作成します。


```roles/nginx/templates/nginx.repo.j2
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
```
Ansibleには、```Jinja2```というテンプレートエンジンによるテンプレートファイルを利用し
ファイルを生成してリモートに送れる template モジュール があります。
nginx.conf などの設定ファイルを、変数とテンプレートから生成するといった用途に利用することができます。
srcオプションなど細かい詳細についてはどこかの機会で投稿していきます。

続いて、nginxの設定ファイルを修正していきます。

```roles/nginx/templates/nginx.conf
#省略

http {
    ## 省略
    include             /etc/nginx/conf.d/*.conf;
}
```


```roles/nginx/templates/default.conf.j2
server {
    listen       80;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```
php-fpmとの連携の際に、default.confファイルを修正していきます。

```roles/nginx/handlers/main.yml
- name: Restart nginx
  service:
    name: nginx
    state: restarted
```
Handlerは、Taskの実行時にcahngedが出てた時だけ実行されるジョブの仕組みです。

ここまでで、nginxのインストールから起動まで設定が完了しました。

ですが、このままではfirewallでhttpポートが塞がれているため
firewalldに対しhttpポート開放の設定をしていきます。
そのためfirewalldのroleを作成します。

```roles/firewalld/tasks/main.yml
---
- name: Install firewalld
  dnf:
    name: firewalld
    state: latest

- name: Setting firewalld
  template:
    src: firewalld.conf.j2
    dest: /etc/firewalld/firewalld.conf
  notify: reload firewalld

- name: Port release firewall
  firewalld:
    service: http
    permanent: true
    state: enabled
  notify: reload firewalld

- name: Allow mysql
  firewalld:
    service: mysql
    permanent: true
    state: enabled
  notify: reload firewalld
```
今回はpublicゾーンに対してhttpサービスを追加していきます。

```roles/firewalld/templates/firewalld.conf.j2
DefaultZone=public
CleanupOnExit=yes
Lockdown=no
IPv6_rpfilter=no
IndividualCalls=yes
LogDenied=off
FirewallBackend=iptables
FlushAllOnReload=yes
RFC3964_IPv4=yes
AllowZoneDrifting=yes
```

```roles/firewalld/handlers/main.yml
- name: reload firewalld
  service:
    name: firewalld
    state: reloaded
```

以下のコマンドを使用して Playbook を実行します

```
$ ansible-playbook -i hosts common.yml web.yml
```

localhostに接続して問題ないか確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594409/4d5124de-33cb-9b88-15ae-9d52f140c753.png)


## PHP
続いてPHPをインストールしていきます。

```roles/php-fpm/tasks/main.yml
---
- name: Install Remi
  rpm_key:
    state: present
    key: https://rpms.remirepo.net/RPM-GPG-KEY-remi2018

- name: Install EPEL
  rpm_key:
    state: present
    key: https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-8

- name: Add EPEL Repository
  dnf:
    name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
    state: present

- name: Add Remi Repository
  dnf:
    name: https://rpms.remirepo.net/enterprise/remi-release-8.rpm
    state: present

- name: Install php
  dnf:
    name: "{{ php_packages }}"
    state: present

- name: Configuration file
  template:
    src: php-fpm.conf.j2
    dest: /etc/php-fpm.d/www.conf

- name: Start php-fpm
  systemd:
    name: 'php-fpm'
    state: started
    enabled: yes
```
デフォルトの状態では、CentOS8の標準リポジトリからPHP7.2がインストールされるようになっていたため、RemiリポジトリからPHP8.0がインストールされるように設定しています。

```roles/php-fpm/vars/main.yml
---
php_packages:
  - '@php:remi-8.0'
  - php-mbstring
  - php-xml
  - php-pdo
  - php-mysqlnd
```
今回はmysqlと接続確認をするため必要なパッケージをインストールしています。
variableに記述することで他に必要なパッケージをインストールする際、
```vars```に追記するだけでインストールできるようにしています。


```roles/php-fpm/vars/main.yml
[www]
# nginxに変更
user =  nginx
group = nginx

listen = /var/run/php-fpm/php-fpm.sock

listen.allowed_clients = 127.0.0.1

# nginxに変更
listen.owner = nginx
listen.group = nginx


## 省略
```
Nginxとphp-fpmの通信は、TCPではなくUNIXドメインソケット通信で行いました。
UNIXドメインソケット通信はTCPよりもスループットが優れているとのことです。
ソケット通信に関しては、以下の記事がとても参考になりました。
[調べなきゃ寝れない！と調べたら余計に寝れなくなったソケットの話](https://qiita.com/kuni-nakaji/items/d11219e4ad7c74ece748) 

Nginxのconfファイルも修正していきます。

```roles/nginx/templates/default.conf.j2
server {
    listen       80;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
    
    location ~ \.php$ {
        root           /usr/share/nginx/html;
        fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}
```
fastcgi_passをunixドメインソケット通信に変更しています。

fastcgi_passについては下記の記事を参考にさせていただきました。
[nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する](https://qiita.com/kotarella1110/items/634f6fafeb33ae0f51dc) 

加えてNginxとphp-fpmが連携できているかを確認するためindex.phpファイルを作成します。

```roles/nginx/templates/index.php.j2
<?php
  echo 'Hello world';
?>
```

以下のコマンドを使用して Playbook を実行します

```
$ ansible-playbook -i hosts common.yml web.yml
```

記載したHello worldが画面上に表示されていればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594409/632c0546-4c1f-c760-085f-55bf031ac8ab.png)

## MySQL
最後にMySQLをインストールしていきます。
今回は、rootユーザーの初期パスワードを変更し、その後新規ユーザーを作成していきます。
冪同性を保たれるように初期パスワードは１回目の実装で削除するよう設定しています。
　・```regexp```: 書き換えたい行にマッチする正規表現


```roles/mysqld/tasks/main.yml
---
- name: Install RPM-GPG-KEY-mysql
  rpm_key:
    state: present
    key: https://repo.mysql.com/RPM-GPG-KEY-mysql

- name: Download epel-release
  dnf:
    name: https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm
    state: present

- name: Install mysql-packages
  dnf:
    name: ["mysql-community-server", "mysql-community-devel"]
    disablerepo: AppStream
    state: present

- name: Install mysql plugin for python
  dnf:
    name: python3-PyMySQL
    state: present

- name: Configuration my.cnf
  template:
    src: my.cnf.j2
    dest: /etc/my.cnf
    owner: root
    group: root
    mode: 0644

- name: Start mysqld
  systemd:
    name: mysqld
    state: started
    enabled: yes

- name: Get root password
  shell: >
    grep "password" /var/log/mysqld.log | awk '{print $(NF)}'
  register: root_password

- name: Update expried root user password
  command: >
    mysql --user root --password={{ root_password.stdout }} --connect-expired-password \
    --execute="ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mysql_root_password }}';"
  when: root_password.stdout != ""

- name: Delete init pass
  lineinfile:
    path: /var/log/mysqld.log
    regexp: 'A temporary password is generated for root@localhost'
    state: absent

- name: Create mysql client user
  mysql_user:
    update_password: on_create
    name: "{{ item.user }}"
    password: "{{ item.password }}"
    host: "{{ item.host }}"
    priv: "{{ item.privileges }}"
    state: present
    login_unix_socket: /var/lib/mysql/mysql.sock
    login_password: "{{ mysql_root_password }}"
    login_user: root
  with_items:
    - { user: "sample", password: "{{ mysql_client_password }}", host: "%", privileges: "*.*:ALL,GRANT" }
```
itemを使用して今後新しくユーザーを作成する場合でも
with_itemsの項目を増やすだけで作成できるようにしています。

```roles/mysqld/vars/main.yml
---
mysql_root_password: PASSWORD
mysql_client_password: PASSWORD
```
セキュアな情報はvaultを使って暗号化した方がいいかと思いますので、
以下のコマンドで暗号化します。
※複数ファイル選択もできます。詳細は以下にて。
[Ansible公式ドキュメント](https://docs.ansible.com/ansible/2.9_ja/user_guide/vault.html) 


```
$ ansible-vault encrypt roles/mysqld/vars/main.yml
```


MySQLと接続確認するためindex.phpファイルを編集していきます。
今回はPDOを使用して簡単な接続確認を試みました。

```roles/nginx/templates/index.php.j2
<?php

const DB_HOST = 'mysql:dbname=;host=db;charset=utf8';
const DB_USER = 'sample';
const DB_PASSWORD = 'PASSWORD';

try {
  $pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD);
  echo '接続成功';
} catch (PDOException $e) {
  echo '接続失敗' . $e->getMessage() . "\n";
  exit();
}

$sql = 'select host, user from mysql.user';

$stmt = $pdo->query($sql);

$result = $stmt->fetchall();

echo '<pre>';
var_dump($result);
echo '</pre>';
```

それでは、Playbookを実行します

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

localhostに接続して下記のようなmysqlとの接続確認やwith_itemsに追加したユーザー（今回であればsample）が確認できればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594409/4fcd896d-7869-aee4-40a1-f04ddd8e1d47.png)

これでとても殺風景な画面が出来上がりました:sunny:

``group_vars``を使ってコードをよりシンプルにできていないところや
Ansible以前にCentOSやyumリポジトリに関して勉強不足な点が多々出てきました。
まだまだ課題ばかりですが、とても頼もしい先輩についていけるように頑張ります。

# 参考
[Ansibleベストプラクティス](https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_best_practices.html) 

[調べなきゃ寝れない！と調べたら余計に寝れなくなったソケットの話](https://qiita.com/kuni-nakaji/items/d11219e4ad7c74ece748) 

[nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する](https://qiita.com/kotarella1110/items/634f6fafeb33ae0f51dc) 

# 最後に
これからもよろしくお願いします。