Ansible: миграция RTFM 2.8 – logrotate, unattended-upgrades и Let’s Encrypt для Bastion хоста

By | 01/29/2018
 

Прыдущая часть – AWS: миграция RTFM 2.7 – CloudFormation и Ansible – наcтройка NAT (там же ссылки на предыдущие посты).

В этой части продолжим настройку Bastion хоста.

Задача – добавить:

  • роль logrotate: ротация логов NGINX (в дальнейшем логи будут сбрасываться через CloudWatch Logs агента)
  • роль unattended-upgrades: автоапдейты системы
  • роль Let’s Encrypt: получение и обновление сертификатов для сайтов
  • роль NGINX Amplify: мониторинг хоста и NGINX (он же будет установлен и на Services, для мониторинга PHP-FPM)
  • роль common: установка стандартных утилит – curl, mailx etc

Запишу тут команды для тестов.

Ansible:

  • cd ~/Work/RTFM/Github/rtfm-blog-ansible-provision
  • ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml
  • ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml
  • ansible -i hosts rtfm-dev --private-key=Work/RTFM/Bitbucket/aws-credentials/rtfm-dev.pem -a "COMMANDHERE"

CloudFormation:

  • cd ~/Work/RTFM/Github/rtfm-blog-cf-templates
  • aws cloudformation --region eu-west-1 validate-template --template-body file://rtfm-blog-cf-template.json
  • aws cloudformation --region eu-west-1 update-stack --stack-name rtfm-dev --template-body file://rtfm-blog-cf-template.json --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32 ParameterKey=JenkinsIP,ParameterValue=34.***.***.43/32

Ссылки на коммиты файлов, получившиеся в результате написания этого поста:


Ansible

Роль logrotate

Начнём с роли для logrotate, используем плейбук manala.logrotate.

Установка зависимостей – requirements.yml

Для установки плейбуков в Jenkins во время выполнения задач – создаём файл requirements.yml, в который добавляем зависимость:

- src: manala.logrotate

Обновляем файл provision.groovy, добавляем функцию для установки ролей:

...
def ansibleRolesInstall() {

    docker.image('williamyeh/ansible:master-ubuntu16.04').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${ANSIBLE_GITHUB_BRANCH}", url: "${ANSIBLE_GITHUB_REPO_URL}"

        stage('Roles install') {

            sh "ansible-galaxy install --ignore-certs --roles-path roles --role-file requirements.yml"
        }
    }
}
...

Добавляем её вызов в rtfm-blog-ansible-provision.groovy:

...
    def provision = load 'ciscripts/provision.groovy'

    provision.ansibleRolesInstall()
...

Теперь добавим её выполнение.

В rtfm-blog-ansible-provision.yml добавляем manala.logrotate:

- hosts: all
  become:
    true
  roles:
    - nginx
    - nat
    - manala.logrotate

Проверяем локально, загружаем роль:

ansible-galaxy install --ignore-certs --role-file requirements.yml
- downloading role 'logrotate', owned by manala
- downloading role from https://github.com/manala/ansible-role-logrotate/archive/1.0.1.tar.gz
- extracting manala.logrotate to /home/setevoy/.ansible/roles/manala.logrotate
- manala.logrotate (1.0.1) was installed successfully

Выполняем проверку с рабочей машины:

ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml
playbook: rtfm-blog-ansible-provision.yml

Вроде ОК – пушим, запускаем билд в Jenkins:

Проверяем на хосте:

root@ip-10-0-1-177:/home/admin# logrotate --version
logrotate 3.11.0

Добавим настройки для ротации логов – в файл rtfm-blog-ansible-provision.yml добавляем:

- hosts: all
  become:
    true
  roles:
    - role: nginx
    - role: nat
    - role: manala.logrotate
      manala_logrotate_configs:
        - file: nginx
          config:
            - /var/log/nginx/*.log:
              - size: 100M
              - missingok
              - rotate: 5
              - compress
              - delaycompress:
              - notifempty
              - create: 0640 www-data adm
              - sharedscripts 
              - daily
              - postrotate
                  systemctl reload nginx.service
              - endscript

Параметры можно посмотреть тут>>>.

Проверяем синтаксис:

ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml
playbook: rtfm-blog-ansible-provision.yml

Запускаем:

ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml

На Dev хосте проверяем:

root@ip-10-0-1-177:/home/admin# cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
size 100M
missingok
rotate 5
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
daily
postrotate systemctl reload nginx.service
endscript
}

ОК, файл настроек есть, проверяем логи сейчас:

root@ip-10-0-1-177:/home/admin# ls -l /var/log/nginx/
total 4
-rw-r--r-- 1 root root   0 Jan 28 13:14 access.log
-rw-r--r-- 1 root root 148 Jan 28 13:14 dev.rtfm.co.ua-access.log
-rw-r--r-- 1 root root   0 Jan 28 13:14 dev.rtfm.co.ua-error.log
-rw-r--r-- 1 root root   0 Jan 28 13:14 error.log

Запускаем logrotate:

root@ip-10-0-1-177:/home/admin# logrotate -f /etc/logrotate.conf

И ещё раз логи:

root@ip-10-0-1-177:/home/admin# ls -l /var/log/nginx/
total 12
-rw-r--r-- 1 root     root   0 Jan 28 13:14 access.log
-rw-r----- 1 www-data adm    0 Jan 28 13:17 dev.rtfm.co.ua-access.log
-rw-r--r-- 1 root     root 148 Jan 28 13:14 dev.rtfm.co.ua-access.log.1
-rw-r--r-- 1 root     root   0 Jan 28 13:14 dev.rtfm.co.ua-error.log
-rw-r----- 1 www-data adm   63 Jan 28 13:17 error.log
-rw-r--r-- 1 root     root  63 Jan 28 13:16 error.log.1

Ротация работает, можно продолжать.

Роль unattended-upgrades

Используем jnv.unattended-upgrades.

Добавляем зависимость в requirements.yml:

- src: manala.logrotate
- src: jnv.unattended-upgrades

Добавляем вызов и настройки в rtfm-blog-ansible-provision.yml:

...
    - role: jnv.unattended-upgrades
      unattended_mail: notify@domain.kiev.ua
      unattended_automatic_reboot: true
      unattended_automatic_reboot_time: 05:00
      unattended_clean_interval: 10

Полный список переменных для параметров – тут>>>.

Устанавливаем роль локально для проверки:

ansible-galaxy install --ignore-certs --role-file requirements.yml

Проверяем:

ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml

Запускаем:

ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml

Проверяем на хосте:

root@ip-10-0-1-177:/home/admin# cat /etc/apt/apt.conf.d/50unattended-upgrades | grep -v "//" | grep -v "^$"
Unattended-Upgrade::Origins-Pattern {
"origin=Debian,codename=${distro_codename},label=Debian-Security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::Mail "notify@domain.kiev.ua";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "05:00";

Готово.

Роль Let’s Encrypt

Используем thefinn93.letsencrypt.

Добавляем в requirements.yml:

...
- src: thefinn93.letsencrypt

Устанавливаем локально:

ansible-galaxy install --ignore-certs --role-file requirements.yml

Обновляем rtfm-blog-ansible-provision.yml, добавляем вызов перед ролью nginx, с параметрами:

- hosts: all
  become:
    true
  roles:
    - role: thefinn93.letsencrypt 
      letsencrypt_email: letsencrypt@domain.kiev.ua 
      letsencrypt_cert_domains: 
        - "{{ inventory_hostname }}" 
      letsencrypt_webroot_path: /var/www/html/
      letsencrypt_renewal_command_args: '--renew-hook "systemctl restart nginx"'
    - role: nginx
    - role: nat
...

Проверяем:

ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml

Выполняем:

ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml
...
TASK [thefinn93.letsencrypt : Attempt to get the certificate using the webroot authenticator] ****
ok: [dev.rtfm.co.ua]
TASK [thefinn93.letsencrypt : Attempt to get the certificate using the standalone authenticator (in case eg the webserver isn't running yet)] ****
ok: [dev.rtfm.co.ua]
TASK [thefinn93.letsencrypt : Fix the renewal file] ****
changed: [dev.rtfm.co.ua] => (item={'value': False, 'key': u'hsts'})
ok: [dev.rtfm.co.ua] => (item={'value': u'webroot', 'key': u'authenticator'})
changed: [dev.rtfm.co.ua] => (item={'value': u'certonly', 'key': u'verb'})
changed: [dev.rtfm.co.ua] => (item={'value': False, 'key': u'noninteractive_mode'})
changed: [dev.rtfm.co.ua] => (item={'value': False, 'key': u'os_packages_only'})
changed: [dev.rtfm.co.ua] => (item={'value': False, 'key': u'uir'})
TASK [thefinn93.letsencrypt : Fix the webroot map in the renewal file] ****
changed: [dev.rtfm.co.ua] => (item=dev.rtfm.co.ua)
TASK [thefinn93.letsencrypt : Install renewal cron] ****
changed: [dev.rtfm.co.ua]
TASK [thefinn93.letsencrypt : Create directory for `ssl_certificate` and `ssl_certificate_key`] ****
skipping: [dev.rtfm.co.ua]
TASK [thefinn93.letsencrypt : Symlink certificates to `ssl_certificate` and `ssl_certificate_key`] ****
skipping: [dev.rtfm.co.ua]
TASK [nginx : Install Nginx] ****
ok: [dev.rtfm.co.ua]
...

Проверяем:

root@ip-10-0-1-177:/home/admin# letsencrypt certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
-------------------------------------------------------------------------------
Found the following certs:
Certificate Name: dev.rtfm.co.ua
Domains: dev.rtfm.co.ua
Expiry Date: 2018-04-28 13:32:36+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem
Private Key Path: /etc/letsencrypt/live/dev.rtfm.co.ua/privkey.pem
-------------------------------------------------------------------------------

На самом деле думаю, что Ansible будет выполнять только установку Let’s Encrypt клиента, а управлять сертификатами я буду уже вручную, потому что при создании стека для rtfm-production – домен будет направлен на старый EC2, нынешний, и авторизация не пройдёт (хотя можно выполнить DNS-верификацию). Но тогда не запустится NGINX, в конфигах которого будут указаны пути к SSL сертификатам…

Посмотрим, пока всё в процессе настройки и планирования – можно оставить так.

Роль NGINX Amplify

Для установки мониторинга – создадим новую роль.

Добавляем каталоги:

mkdir -p roles/amplify/{tasks,files}

В посте NGINX: Amplify – SaaS мониторинг от NGINX установка выполнялась из скрипта, тут используем репозиторий NGINX.

На Dev хосте проверяем ОС:

admin@ip-10-0-1-177:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.1 (stretch)
Release:        9.1
Codename:       stretch

Создаём в roles/amplify/tasks/ файл main.yml, выполняем импорт ключа, добавление репозитория и установку агента:

- name: Add the NGINX Apt signing key 
  apt_key: 
    url: http://nginx.org/keys/nginx_signing.key 
    state: present

- name: Add NGINX Amplify repository
  apt_repository:
    repo: deb http://packages.amplify.nginx.com/debian/ stretch amplify-agent
    state: present

- name: Install Amplify agent
  apt:
    name: nginx-amplify-agent
    update_cache: yes

В файле rtfm-blog-ansible-provision.yml добавляем выполнение роли после роли nginx:

...
    - role: nginx
    - role: amplify
    - role: nat
    - role: manala.logrotate
      manala_logrotate_configs:
...

Проверяем, запускаем:

ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml
ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml
...
TASK [amplify : Add the NGINX Apt signing key] ****
changed: [dev.rtfm.co.ua]
TASK [amplify : Add NGINX AMplify repository] ****
changed: [dev.rtfm.co.ua]
TASK [amplify : Install Amplify agent] ****
changed: [dev.rtfm.co.ua]

На хосте проверяем файл:

admin@ip-10-0-1-177:~$ file /etc/amplify-agent/agent.conf.default
/etc/amplify-agent/agent.conf.default: C++ source, ASCII text

ОК, теперь надо добавить API ключ.

В roles/amplify/tasks/main.yml добавляем копирование файла настроек и с помощью lineinfile обновление в нём ключа:

...
- name: Copy default Amplify config
  command: cp /etc/amplify-agent/agent.conf.default /etc/amplify-agent/agent.conf

- name: Add Amplify API key
  lineinfile:
    path: /etc/amplify-agent/agent.conf
    regexp: '^api_key ='
    line: 'api_key = {{ api_key }}'

В Jenkins он будет передаваться через переменные в билде, пока передаём его через --extra-vars, выполняем:

ansible-playbook --limit=rtfm-dev --extra-vars '{"api_key":"111222333"}' --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml

Проверяем:

root@ip-10-0-1-177:/home/admin# cat /etc/amplify-agent/agent.conf | grep api_key
api_key = 111222333

Аналогично можно сразу добавить имя хоста, которое будет отображаться в Amplify dashboard:

...
- name: Add hostname for Amplify dashboard
  lineinfile:
    path: /etc/amplify-agent/agent.conf
    regexp: '^hostname ='
    line: 'hostname = {{ inventory_hostname }}'

Документация по установке Amplify агента – тут>>>.

Сюда же добавляем запуск агента и его добавление в автозапуск:

...
- name: Start Amplify service
  systemd:
    name: amplify-agent
    state: started

- name: Enable Amplify service
  systemd:
    name: amplify-agent
    enabled: yes

Запускаем выполнение, проверяем Amplify:

Хост dev.rtfm.co.ua появился, всё гуд.

Далее в roles/amplify/files добавляем файл stub_status.conf:

server {
    listen 127.0.0.1:80;
    server_name 127.0.0.1;
    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}

В файл roles/amplify/tasks/main.yml – его копирование и перезагрузку конфигов NGINX:

...
- name: Copy stub_status.conf
  copy:
    src: files/stub_status.conf
    dest: /etc/nginx/conf.d/stub_status.conf

- name: Reload NGINX service
  systemd:
    name: nginx
    state: reloaded

Ещё раз запускаем, проверяем:

Данные пошли.

Осталось обновить скрипт для Jenkins – добавить передачу API_KEY.

Добавляем параметр:

Обновляем provision.groovy, функция ansiblePlaybookApply(), добавляем --extra-vars api_key=${AMPLIFY_API_KEY}:

...
def ansiblePlaybookApply(ansibleHostLimit='1', ansiblePlaybookFile='2', ansiblePemFile='3') {
...
        stage('Ansible playbook apply') {

            sh "chmod 400 credentials/${ANSIBLE_EC2_PEM_FILE}"
            sh "set +x && ansible-playbook --limit=${ansibleHostLimit} --extra-vars api_key=${AMPLIFY_API_KEY} --private-key=credentials/${ansiblePemFile} ${ansiblePlaybookFile}"
        }
    }
}
...

set +x – что бы Jenkins не выводил ключ в логе билда.

Пушим, запускаем, проверяем:

Amplify:

С этим вроде всё.

Роль common

Для установки всего прочего – добавляем роль common:

mkdir -p roles/common/{tasks,templates}

Добавляем roles/common/tasks/main.yml, в цикле описываем установку необходимых пакетов:

- name: Install common packages
  apt: name={{item}} state=present
  with_items:
       - mailutils
       - curl

Добавляем вызов роли в rtfm-blog-ansible-provision.yml:

...
    - role: jnv.unattended-upgrades
      unattended_mail: notify@domain.kiev.ua
      unattended_automatic_reboot: true
      unattended_automatic_reboot_time: 05:00
      unattended_clean_interval: 10
    - role: common

Собственно – на этом пока и всё.

Сохраняем, пушим.

Теперь, для проверки, можно удалить весь стек, и пересоздать его заново:

aws cloudformation delete-stack --stack-name rtfm-dev

Запускаем создание стека в Jenkins.

Запускаем Ansible в Jenkins.

Готово:

Можно переходить к ролям для DB и Services.