AnsibleでAmazonLinux2 + Laravel + mysql5.7 + nginx + SSL環境を構築する。

# Ansibleってなんじゃ？
>Ansible（アンシブル）は、レッドハットが開発するオープンソースの構成管理ツールである。サーバを立ち上げる際、>あらかじめ用意した設定ファイルに従って、ソフトウェアのインストールや設定を自動的に実行する事が出来る
出典: フリー百科事典『ウィキペディア（Wikipedia）』

らしいです。今までポチポチクリックして設定していた部分を自動化できるなんてすごいですね。

# 実行環境
- macOS  
mojave 10.14.6  
- ansible  
2.8.3

# 下準備
1.ansibleインストール
macの場合は以下のコマンドでインストールする事ができます。

```
brew install ansible

ansible --version
ansible 2.8.3
```

2.ansible実行先サーバ構築
今回はAWSを使用します。AmazonLinux2でサーバを一つ立ち上げ、pemキーとpublicIPを控えておいてください。
またletsencrptでssl化する対象ドメインも用意してください

#　今回作成するディレクトリ構成
```
├── ansible.cfg
├── hosts
├── playbook.yml
├── roles
│   ├── common #サーバで共通で実行するタスク
│   │   └── tasks
│   │       └── main.yml
│   ├── composer #composerのインストールやらなにやら
│   │   └── tasks
│   │       └── main.yml
│   ├── git_clone #リポジトリをクローンしてくる処理
│   │   └── tasks
│   │       ├── main.yml
│   │       └── templates
│   │           └── env.j2
│   ├── letsencrypt #指定されたドメインをssl化
│   │   └── tasks
│   │       ├── main.yml
│   │       └── templates
│   │           ├── laravel-ssl.conf.j2
│   │           └── letsencrypt.j2
│   ├── mysql
│   │   └── tasks
│   │       ├── main.yml
│   │       └── templates
│   │           ├── init_my.cnf.j2
│   │           └── my.cnf.j2
│   ├── nginx
│   │   └── tasks
│   │       ├── main.yml
│   │       └── templates
│   │           ├── laravel.conf.j2
│   │           └── nginx.conf.j2
│   ├── php
│   │   └── tasks
│   │       ├── main.yml
│   │       └── templates
│   │           └── php-fpm.conf.j2
│   └── restart_nginx
│       └── handlers
│           └── main.yml
└── vars #変数ファイル
    └── variables.yml
```
今回のディレクトリ構成は4つに分類する事ができます。
####hosts
サーバの接続情報を定義します。sshキーの配置場所の指定もここで行います。

####vars
variables.ymlにrolesで使う変数の情報を記述します。例えばnginxのドメイン名の指定など、gitのclone先のリポジトリ情報などはここに記載します。

####roles
このディレクトリにインストールするパッケージや処理を項目ごとに分けて配置します。

####playbook.yml
rolesや、変数ファイルを読み込んだり、ssh接続するユーザーの指定を記述します。
ここで書かれたものを元に構築されるというイメージですね。

ansibleではrolesというディレクトリ配下に処理を記述することにより、構築するリソースを分ける事ができます。
分けられたリソースはplaybook.yml内で実行したい処理のみ記述する事ができます。
簡単に言えばrolesはモジュールのようなもので、使いたい処理のみplaybook.ymlにかけるのでかなり見通しが良くなります。


# 早速作る
##1.hostsファイルを作成
サーバに接続するための情報を記述します。

```ansible/hosts
[localhost]
127.0.0.1 ansible_connection=local

[aws] #ここに記載されたIPに対して接続します。複数指定することも可能です。
111.111.111.111
111.111.111.111

[aws:vars] #[aws]で使う変数を定義しています。
ansible_user=ec2-user #サーバ接続するユーザー
ansible_ssh_private_key_file=/path/to/example.pem #サーバに接続するためのpemキーを指定
```

##２.variablesファイル作成
varsディレクトリの下にvariables.ymlを作成します。
ここにしっかり情報を指定しないとansible実行時にエラーが起きます。

```ansible/vars/variables.yml
---
# nginx
virtual_server_name: example.com #nginx指定するServername
virtual_root_path: "{{git_repo_dest}}/public" #nginxで指定するrootディレクトリ
# db
db_name: ansible_test #作成するDBの名前
db_user: root #作成するdbのユーザー名
db_password: root #作成するdbパスワード
#git
git_repo_url: https://github.com/username/repository.git #clone元repositoryのURL
git_repo_dest: /www/test #どこにgit cloneしたフォルダを配置するか指定
git_branch_name: master # cloneしてくるブランチ指定
#Laravel .env
MAIL_DRIVER: sendmail
MAIL_HOST: locahost
MAIL_PORT: 25
MAIL_USERNAME: null
MAIL_PASSWORD: null
MAIL_FROM_ADDRESS: test@test.com
MAIL_FROM_NAME: test@test.com
#SSL(let's encrypt) #let's encryptで有効期限を知らせるメール通知先
ssl_email: test@test.com
```

##3.rolesファイル作成
rolesファイルは決められたディレクトリ名で作成しなければ正常に動作しないので注意が必要です。

※例

```
├── nginx #role名 playbookでこのディレクトリ名を指定します。
│   └── tasks 
│       ├── main.yml #この名前じゃなきゃだめ
│       └── templates #テンプレート置き場
│           ├── laravel.conf.j2
│           └── nginx.conf.j2
```

タイトルの構成になるように作っていきます。

また、templateファイルを作ってますが、

###1.common
サーバで共通で実行するタスクを定義します。

```
├── common
│   └── tasks
│       └── main.yml
```

```ansible/roles/common/tasks/main.yml
#ホスト名設定
- name: set hostname
  hostname:
    name: "{{virtual_server_name}}"
- name: amzn2-core.repo priority down
  replace:
    path: /etc/yum.repos.d/amzn2-core.repo
    regexp: 'priority=10'
    replace: 'priority=99'
  when: ansible_facts['distribution'] == "Amazon"
- name: amzn2-extras.repo priority down
  replace:
    path: /etc/yum.repos.d/amzn2-extras.repo
    regexp: 'priority = 10'
    replace: 'priority=99'
  when: ansible_facts['distribution'] == "Amazon"
- name: check exists EPEL
  shell: amazon-linux-extras list | grep epel
  register: epel_check
  failed_when: epel_check.rc not in [0,1]
  when: ansible_facts['distribution'] == "Amazon"
- name: install EPEL when amazon linux
  shell: amazon-linux-extras install -y epel
  when: ansible_facts['distribution'] == "Amazon" and epel_check.rc == 0
- name: install EPEL when other linux
  yum:
    name: ['epel-release','https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm']
  when:
    - ansible_facts['distribution'] == "CentOS"
    - ansible_facts['distribution_major_version'] == "7"
#必須パッケージインストール
- name: yum basic install
  yum:
    name: ['zip', 'unzip', 'git', 'mysql', 'nginx', 'nodejs','git']
    state: latest
#グループ作成
- name: create group
  group:
    name: laravel
    state: present
# nginxを作成したグループに参加させる
- name: add nginx to laravel group
  user:
    name: "{{ item.name }}"
    append: yes
    groups: "{{ item.group }}"
  with_items:
    - {name: 'nginx', group: 'laravel'}
    - {name: 'ec2-user', group: 'laravel'}

```
###4.php
php関連のタスクを定義します。templatesにはnginxとphpをつなぐために必要なphp-fpmファイルのテンプレートを配置します。

```
|── php
│   └── tasks
│       ├── main.yml
│       └── templates
│           └── php-fpm.conf.j2
```

```ansible/roles/php/tasks/template/php-fpm.conf.j2
# PHP-FPM FastCGI server
# network or unix domain socket configuration

upstream php-fpm {
        server unix:/run/php-fpm/www.sock;
}
```

```ansible/roles/php/tasks/main.yml
---
- name: remi repository install
  yum:
    name: [http://rpms.famillecollet.com/enterprise/remi-release-7.rpm]
- name: yum php install
  yum:
    name: ['php72', 'php-cli' ,'php-gd', 'php-mysqlnd', 'php-mbstring', 'php-mcrypt', 'php-pdo', 'php-xml', 'php-fpm','php-json','php-xmlrpc','php-pecl-zip']
    state: latest
    enablerepo: remi,remi-php72
- name: modify php.ini
  replace:
    dest: /etc/php.ini
    regexp: "{{ item.regexp }}"
    replace: "{{ item.replace }}"
  with_items:
    - { regexp: "^;date.timezone =", replace: "date.timezone = Asia/Tokyo" }
    - { regexp: "^expose_php = On", replace: "expose_php = Off" }
    - { regexp: "^upload_max_filesize = .*$", replace: "upload_max_filesize = 256M" }
    - { regexp: "^memory_limit = .*$", replace: "memory_limit = 1024M" }
- name: modify php-fpm config file
  replace:
    dest: /etc/php-fpm.d/www.conf
    regexp: "{{ item.regexp }}"
    replace: "{{ item.replace }}"
  with_items:
    - { regexp: "^user = apache", replace: "user = nginx" }
    - { regexp: "^group = apache", replace: "group = nginx" }
    - { regexp: "^listen = 127.0.0.1:9000", replace: "listen = /var/run/php-fpm/php-fpm.sock" }
    - { regexp: "^;listen.owner = nobody", replace: "listen.owner = nginx" }
    - { regexp: "^;listen.group = nobody", replace: "listen.group = nginx" }
- name: make php-fpm.conf
  template:
    src: php-fpm.conf.j2
    dest: /etc/nginx/conf.d/php-fpm.conf
- name: start php-fpm
  systemd:
    name: php-fpm.service
    state: restarted
    daemon_reload: yes
    enabled: yes
```

###5.nginx
ngix関連のタスクを定義します。

```
├── nginx
│   └── tasks
│       ├── main.yml
│       └── templates
│           ├── laravel.conf.j2
│           └── nginx.conf.j2
```
```ansible/roles/nginx/tasks/tempalates/laravel.conf.j2
server {
  root {{virtual_root_path}};
  listen 80;
  server_name {{virtual_server_name}};

location / {
    client_max_body_size 10m;
    index index.php index.html;
    try_files $uri $uri/ /index.php?$query_string;
  }
  location ~ \.php$ {
    fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
    fastcgi_index index.php;
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }
    location /.well-known/ {
    default_type "text/plain";
  }
}
```
```ansible/roles/nginx/tasks/tempalates/nginx.conf.j2
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    # サーバ情報の非表示
    server_tokens off;
    # クリックジャッキング
    add_header X-Frame-Options SAMEORIGIN;
    # XSS対策
    add_header X-XSS-Protection "1; mode=block";
    # IEで発生するコンテンツタイプSniffing対策
    add_header X-Content-Type-Options nosniff;
    sendfile off;
    etag off;
    if_modified_since off;

    keepalive_timeout  65;

    gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
```

```ansible/roles/nginx/tasks/main.yml

- name: make nginx config file for laravel
  template:
    src: laravel.conf.j2
    dest: /etc/nginx/conf.d/laravel.conf
- name: change nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify:
    - restart nginx
- name: start nginx
  systemd:
    name: nginx.service
    state: restarted
    daemon_reload: yes
    enabled: yes
```
###6.mysql
mysql関連のタスクを定義します。

```
├── mysql
│   └── tasks
│       ├── main.yml
│       └── templates
│           ├── init_my.cnf.j2
│           └── my.cnf.j2
```
```ansible/roles/mysql/tasks/templates/init_my.cnf.j2
[client]
user = root
password = {{ mysql_default_password.stdout }}
connect-expired-password
```
```mysql/tasks/templates/my.cnf.j2
[client]
user = {{db_user}}
password = {{db_password}}
default-character-set = utf8mb4

[mysqld]
character-set-server = utf8mb4
skip-character-set-client-handshake
default-storage-engine = INNODB
collation-server = utf8mb4_general_ci
init-connect = SET NAMES utf8mb4
explicit_defaults_for_timestamp
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
symbolic-links = 0
log-error = /var/log/mysqld.log
pid-file = /var/run/mysqld/mysqld.pid

[mysqldump]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4
```
```mysql/tasks/main.yml
---
- block:
  - name: remove mariadb-libs
    yum:
      state: absent
      name: mariadb-libs
  - name: install mysql repository
    yum:
      name: https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
      validate_certs: yes
  - name: disable mysql80-community-server
    shell: yum-config-manager --disable mysql80-community
  - name: enable mysql57-community-server
    shell: yum-config-manager --enable mysql57-community
  - name: install mysql-community-server
    yum:
      state: present
      name: ['mysql-community-server','MySQL-python']
  - name: running and enabled mysqld
    service:
      name: mysqld
      state: started
      enabled: yes
  - name: check .my.cnf exists
    stat:
      path: /root/.my.cnf
    register: mycnf_file
  - name: get temporary password
    shell: cat /var/log/mysqld.log | grep "temporary password" | awk '{print $11}'
    register: mysql_default_password
    when: not mycnf_file.stat.exists
  - name: deploy init .my.cnf
    template:
      src: init_my.cnf.j2
      dest: /root/.my.cnf
      owner: root
      group: root
      mode: 0644
    when: not mycnf_file.stat.exists
  - name: change password validation to the easy way
    shell: |
      mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "SET GLOBAL validate_password_length=4;"
      mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "SET GLOBAL validate_password_policy=LOW;"
    when: not mycnf_file.stat.exists
  - name: change root user password
    shell: |
      mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';"
    when: not mycnf_file.stat.exists
  - name: deploy changed .my.cnf
    template:
      src: my.cnf.j2
      dest: /root/.my.cnf
      owner: root
      group: root
      mode: 0644
  - name: deploy changed .my.cnf
    template:
      src: templates/my.cnf.j2
      dest: /etc/my.cnf
  - name: remove all anonymous user
    mysql_user:
      name: ''
      state: absent
      host: localhost
  - name: create database
    mysql_db:
      name: "{{db_name}}"
      state: present
      encoding: utf8mb4
  - name: create user
     mysql_user:
       name: vagrant
       password: vagrant
       priv: '*.*:ALL,GRANT'
       state: present
  - name: restart mysqld
    service:
      name: mysqld
      state: restarted
  when: ansible_facts['distribution'] == "Amazon"
```
###5.git_clone
リポジトリからgitcloneします。

```
├── git_clone
│   └── tasks
│       ├── main.yml
│       └── templates
│           └── env.j2
```

```ansible/roles/git_clone/tasks/templates/env.j2
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://{{virtual_server_name}}
AUTH_URL=https://{{virtual_server_name}}

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE={{db_name}}
DB_USERNAME={{db_user}}
DB_PASSWORD={{db_password}}

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER={{MAIL_DRIVER}}
MAIL_HOST={{MAIL_HOST}}
MAIL_PORT={{MAIL_PORT}}
MAIL_USERNAME={{MAIL_USERNAME}}
MAIL_PASSWORD={{MAIL_PASSWORD}}
MAIL_FROM_ADDRESS={{MAIL_FROM_ADDRESS}}
MAIL_FROM_NAME={{MAIL_FROM_NAME}}
MAIL_ENCRYPTION=null

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
```

```git_clone/tasks/main.yml
---
- name: check_repos
  stat:
    path: "{{ git_repo_dest }}"
  register: check_repos
- name: git clone
  git:
    repo: "{{ git_repo_url }}"
    dest: "{{ git_repo_dest }}"
    version: "{{ git_branch_name }}"
- block:
  - name: disable git check permissions
    shell: "cd {{ git_repo_dest }}; git config core.filemode false"
  - name: change project permissions
    file:
      path: "{{ git_repo_dest }}"
      recurse: yes
      state: directory
      owner: nginx
      group: laravel
      mode: '777'
  when: not check_repos.stat.exists
- name: composer install
  become: no
  shell: "cd {{ git_repo_dest }}; composer install "
- name: check_env
  stat:
    path: "{{ git_repo_dest }}/.env"
  register: check_env
- name: create .env
  template:
    src: env.j2
    dest: "{{ git_repo_dest }}/.env"
    owner: nginx
    group: laravel
    mode: 0777
  when: not check_env.stat.exists
- name: generate APP_KEY
  become: no
  shell: "cd {{ git_repo_dest }};php artisan key:generate"
  when: not check_env.stat.exists
- name: migration DB
  shell: "cd {{ git_repo_dest }}; php artisan migrate"
  register: migrate_result
  changed_when: "migrate_result.stdout != 'Nothing to migrate.'"
  when: ansible_facts['distribution'] == "Amazon"
```

###7.composer
```ansible/roles/composer/tasks/main.yml
- name: check composer
  stat: path=/usr/local/bin/composer
  register: composer_bin
  tags: composer
- block:
  - name: download composer
    get_url:
      url: https://getcomposer.org/installer
      dest: /tmp/installer
  - name: install composer
    shell: cat /tmp/installer | php -- --install-dir=/usr/local/bin
  - name: rename composer.phar to composer
    shell: mv /usr/local/bin/composer.phar /usr/local/bin/composer
  - name: make composer executable
    file:
      path: /usr/local/bin/composer
      mode: a+x
      state: file
  when: not composer_bin.stat.exists
  tags: composer
```

###6.lets'encrypt
lets'encryptでssl証明書を取得する処理を定義します。

```
├── letsencrypt
│   └── tasks
│       ├── main.yml
│       └── templates
|           |── letsencrypt.j2
│           └── laravel-ssl.conf.j2
```

```ansible/roles/letsencrypt/tasks/templates/laravel-ssl.conf.j2
server {
  listen 80;
  server_name {{virtual_server_name}};
  return 301 https://$host$request_uri;
}
server {
  listen 443;
  server_name {{virtual_server_name}};
  root {{virtual_root_path}};

  client_max_body_size 10m;
  ssl                  on;
  ssl_certificate     /etc/letsencrypt/live/{{virtual_server_name}}/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/{{virtual_server_name}}/privkey.pem;
  ssl_prefer_server_ciphers     on;
  ssl_protocols     SSLv3 TLSv1 TLSv1.1 TLSv1.2;

  location / {
    index index.php index.html;
    try_files $uri $uri/ /index.php?$query_string;
  }
  location ~ \.php$ {
    fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
    fastcgi_index index.php;
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }
    location /.well-known/ {
    default_type "text/plain";
  }
}
```
```ansible/roles/letsencrypt/tasks/templates/letsencrypt.j2
* * 1 * * root letsencrypt renew --post-hook "service nginx restart"
```

```ansible/roles/letsencrypt/tasks/main.yml
---
- name: install python-simplejson
  yum:
    name: ['python-simplejson','openssl']
    state: present
- name: install letsencrypt
  yum:
    name: ['letsencrypt']
    state: present
- name: check letsencrypt folder
  stat:
    path: "/etc/letsencrypt/live/{{virtual_server_name}}"
  register: letsencrypt
- name: Create SSL/TLS certificates
  shell: "letsencrypt certonly --webroot -w {{ virtual_root_path }} -d {{ virtual_server_name }} --email {{ ssl_email }} --agree-tos --keep-until-expiring --non-interactive --debug"
  when: not letsencrypt.stat.exists
- name: add virtualhost.conf for certificates settings
  template:
    src: laravel-ssl.conf.j2
    dest: /etc/nginx/conf.d/laravel.conf
  when: not letsencrypt.stat.exists
  notify:
    - restart nginx
- name: add cron for automatically cert update
  template:
    src: letsencrypt.j2
    dest: /etc/cron.d/letsencrypt
    owner: root
    group: root
    mode: '0600'
  when: not letsencrypt.stat.exists
```

##7.playbookを作成する。
適当なディレクトリにplaybook.ymlを作成し編集します。
rolesには上で作ったリソースを定義します。作りたくないタスクがあればコメントアウトで実行しないように設定する事もできます。

```
ansible
├── playbook.yml
```

```ansible/playbook.yml
---
- hosts: aws #hostsファイルに記述したホストを指定します。
  user: ec2-user #接続ユーザ
  become: yes # root権限でコマンド実行
  vars_files: #変数ファイル指定。後ほど作成します。
    - ./vars/variables.yml
  roles: #rolesディレクトリに今まで作ったファイルを読み込んでいます。
    - common
    - php
    - mysql
    - composer
    - git_clone
    - nginx
    - letsencrypt
```

これらのファイルを作成した後以下のコマンドでansibleを実行します。

###playbook.ymlの構文があってるかチェック※実際にはansibleは実行されません。
```
ansible-playbook -i hosts playbook.yml --syntax-check
#大丈夫だと以下が返ってくる。
playbook: playbook.yml
```
###playbook実行

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

これで自動構築が始まると思います。

# 終わりに
まだ環境ごとに作りたいリソースを分ける事ができなかったり、状況に応じての処理が甘いですが、laravelの実行環境を構築する事ができます。

ansibleはアプリケーションよりの自動化ソフトなので、インフラ構築などはterraformなどを使うとよいと思います。