За мной, за мной, читатель, и я проведу тебя в чарующий мир автоматизации разворачивания окружения на серверах под управлением Linux семейства RHEL.


Один из наших java-проектов вырос, стал совсем взрослым и сейчас занимает 4 контура:


Dev — контур для команды разработки,
Qa — контур для команды тестирования,
Stage — контур для демонстрации новых фич заказчику,
Production — боевой контур.


Каждый контур содержит два одинаковых сервера с идентичным набором компонентов окружения для нашего приложения:


linux Oracle — операционная система,
jdk — комплект приложений Java,
haproxy — proxy сервер,
nginx — веб-сервер для отдачи статики,
mysql — субд.


Перед командой эксплуатации встал резонный вопрос: как настроить управление окружением на восьми серверах и сохранить оптимистичное отношение к жизни.


После краткого сравнения систем управления конфигурациями был выбран Ansible. В его пользу сыграли простота, гибкость и отсутствие агентов на управляемых серверах.



Теперь немного расскажем об архитектуре ролей Ansible для проекта.


Для установки и первоначальной настройки каждого компонента окружения написана отдельная роль. Последующая кастомизация настроек каждого компонента окружения также выделена в дополнительную роль. Это позволило нам избежать дублирования общих стандартных ролей для остальных проектов.


Playbook c последовательностью выполняемых ролей для подключения новой ноды в контур выглядит так:


- hosts:
     - project_group
  user: username
  become: yes
  gather_facts: true
  roles:
     - rhel_install_new_server
     - rhel_install_java
     - rhel_install_haproxy
     - rhel_install_nginx
     - rhel_install_mysql

А теперь расскажем о каждой роли поподробнее.


Роль rhel_install_new_server


Выполняет общую настройку операционной системы и установку системных утилит.


Файл roles/rhel_install_new_server/tasks/main.yml


---
# tasks file for rhel_install_new_server

# Проверяем и устанавливаем системные обновления:
- name: yum update
  yum:
    name: "*"
    state: latest
    update_cache: yes

# Подключаем EPEL repository:
- name: install EPEL repository
  yum:
    name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
    state: present
    update_cache: yes

# Устанавливаем стандартные утилиты. Список пакетов задан в переменной tools_packages:
- name: install tools
  yum:
    name: '{{ item.name }}'
    state: present
    update_cache: yes
  with_items: '{{ tools_packages }}'

# Выставляем автозагрузку для сетевого интерфейса по умолчанию:
- name: set autoload for default interface
  lineinfile:
    path: "/etc/sysconfig/network-scripts/ifcfg-{{ansible_default_ipv4.interface}}"
    regexp: '^ONBOOT='
    line: 'ONBOOT="yes"'

# Отключаем IPv6 для интерфейса по умолчанию:
- name: disable IPv6 for eth0
  replace:
    path: "/etc/sysconfig/network-scripts/ifcfg-{{ansible_default_ipv4.interface}}"
    regexp: '^(IPV6.*=).*$'
    replace: '\1"no"'

# Отключаем IPv6:
- name: disable IPv6
  blockinfile:
    path: /etc/sysctl.d/disableipv6.conf
    create: yes
    marker: no
    block: |
      net.ipv6.conf.all.disable_ipv6 = 1
      net.ipv6.conf.default.disable_ipv6 = 1

# Отключаем SELinux:
- name: disable SELinux
  selinux:
    state: disabled

# Отключаем локальный firewall:
- name: disable firewall
  systemd:
    name: firewalld
    enabled: no
    state: stopped
  ignore_errors: yes

# Выставляем часовой пояс. Временная зона задана в переменной timezone:
- name: set timezone to UTC
  timezone:
    name: '{{ timezone }}'

# Выставляем синхронизацию времени в крон:
- name: set synchronize time in cron
  cron:
    name: "set synchronize time by ansible"
    minute: 1
    job: "/usr/sbin/ntpdate -u pool.ntp.org >/dev/null 2>&1"

# Чистим старые ядра:
- name: Remove old kernels
  shell: "rpm -q kernel | grep -v `uname -r` | grep -v `/sbin/grubby --default-kernel | sed -r 's#^/boot/vmlinuz-##'` | xargs rpm -e || true"

Файл с переменными roles/rhel_install_new_server/vars/main.yml
---
# vars file for rhel_install_new_server

tools_packages:
  - name: vim
  - name: mc
  - name: less
  - name: sysstat
  - name: iotop
  - name: strace
  - name: traceroute
  - name: screen
  - name: rsync
  - name: curl
  - name: python
  - name: wget
  - name: zlib
  - name: unzip
  - name: bind-utils
  - name: ntp
  - name: ntpdate
  - name: telnet
  - name: nmap
  - name: tcpdump
  - name: logrotate
  - name: net-tools
  - name: bash-completion
  - name: yum-utils
  - name: mtr

timezone: UTC

Роль rhel_install_java — установка Java


На нашем проекте используется пакет jdk версии 8u60. Дополнительно мы заранее скачали и положили в папку roles/rhel_install_java/files/ файлы JCE: US_export_policy.jar и local_policy.jar.


Файл с переменными roles/rhel_install_java/vars/main.yml
---
# vars file for roles/rhel_install_java

java_dst_path: "/opt/dst/java"
download_url: "http://download.oracle.com/otn/java/jdk/8u60-b27/jdk-8u60-linux-x64.rpm"
java_cookie: "Cookie:' gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie'"

Файл roles/rhel_install_java/tasks/main.yml


---
# tasks file for roles/rhel_install_java

# Создадим папку для дистрибутивов:
- name: java | create java dst directory
  file:
    path: "{{ java_dst_path }}"
    state: directory
    recurse: yes
    owner: root
    group: root
    mode: 0755

# Скачиваем дистрибутив jdk:
- name: java | download java
  get_url:
    url: "{{ download_url }}"
    dest: "{{ java_dst_path }}"
    tmp_dest: "{{ java_dst_path }}"
    headers: "{{ java_cookie }}"
    validate_certs: no
    owner: root
    group: root
    mode: 0744
    force: yes
  ignore_errors: True

# Находим скачанный jdk в папке дистрибутивов и заносим имя файла в переменную:
- name: java | register java rpm
  find:
    paths: "{{ java_dst_path }}"
    patterns: "*.rpm"
  register: java_rpm

# Проверим значение переменной:
- debug: msg={{ java_rpm.files.0.path }}

# Установим скачанный пакет:
- name: java | install java rpm
  yum:
    name: "{{ java_rpm.files.0.path }}"
    state: present

# Копируем Java Cryptography Extension: 
- name: java | copy JCE
  copy:
    src: files/{{ item }}
    dest: /usr/java/default/jre/lib/security/
    owner: root
    group: root
    mode: 0644
    backup: yes
  with_items:
    - US_export_policy.jar
    - local_policy.jar

Роль rhel_install_haproxy — установка haproxy


Файл roles/rhel_install_haproxy/tasks/main.yml


---
# tasks file for install_haproxy

# Устанавливаем последнюю версию haproxy:
- name: install the latest version of haproxy
  yum:
    name: haproxy
    state: latest

# делаем бэкап rsyslog.conf:
- name: backup rsyslog.conf
  copy:
    src: /etc/rsyslog.conf
    dest: /etc/rsyslog.conf_orig
    force: no
    remote_src: true

# Включаем udp в rsyslog:
- name: format rsyslog | set UDP options
  blockinfile:
    path: /etc/rsyslog.conf
    block: |
    $ModLoad imudp
    $UDPServerAddress 127.0.0.1
    $UDPServerRun 514
    state: present
    insertafter: '^#\$UDPServerRun.*$'
  notify:
    - restart rsyslog

# создаём файл конфигурации haproxy для rsyslog:
- name: create rsyslog for haproxy
  blockinfile:
    path: /etc/rsyslog.d/haproxy.conf
    content: |
    if $programname == 'haproxy' and $syslogseverity <= '4' then /var/log/haproxy/haproxy.out
    if $programname == 'haproxy' and $syslogseverity  > '4' then /var/log/haproxy/haproxy.log
    & stop
    state: present
    create: yes
  notify:
    - restart rsyslog

# делаем бэкап файла конфигурации logrotate:
- name: backup logrotate file haproxy
  copy:
    src: /etc/logrotate.d/haproxy
    dest: /etc/logrotate.d/haproxy_orig
    force: no
    remote_src: true

# Копируем свой файл конфигурации logrotate для haproxy:
- name: copy logrotate file for haproxy
  copy:
    src: files/logrotate_haproxy
    dest: /etc/logrotate.d/haproxy
    force: yes

# добавляем в автозагрузку и запускаем сервис:
- name: enable and start haproxy
  systemd:
    name: haproxy
    daemon_reload: yes
    enabled: yes
    state: started

Файл rhel_install_haproxy/handlers/main.yml


---
# handlers file for install_haproxy

- name: restart rsyslog
  systemd:
    name: rsyslog
    state: restarted

- name: reload haproxy
  systemd:
    haproxy: name
    state: reloaded

Роль rhel_install_nginx — установка nginx


Файл rhel_install_nginx/tasks/main.yml


---
# tasks file for roles/rhel_install_nginx

# Подключаем официальный репозиторий nginx:
- name: add nginx repo
  yum_repository:
    name: nginx
    description: nginx official repo
    state: present
    baseurl: "http://nginx.org/packages/rhel/{{ansible_distribution_major_version}}/{{ansible_userspace_architecture}}/"
    gpgkey: http://nginx.org/keys/nginx_signing.key
    gpgcheck: yes
    enabled: yes

# Устанавливаем nginx:
- name: install nginx
  yum:
    name: nginx
    state: present
    update_cache: yes

# Создаём системные папки:
- name: create folders
  file:
    path: '/etc/nginx/{{ item }}'
    state: directory
    mode: 0755
  with_items:
    - conf-available
    - sites-available
    - sites-enabled

# Подключаем sites-enabled в конфиг nginx:
- name: enable sites-enabled dir
  lineinfile:
    dest: /etc/nginx/nginx.conf
    state: present
    insertafter: "(.*)include /etc/nginx/(.*)"
    line: " include /etc/nginx/sites-enabled/*.conf;"

# Определяем существует ли default.conf:
- name: stat /etc/nginx/conf.d/default.conf
  stat: path=/etc/nginx/conf.d/default.conf
  register: defaultconf_stat

# Если существует, перемещаем его:
- name: Move default.conf
  command: mv /etc/nginx/conf.d/default.conf /etc/nginx/sites-available/default.conf
  when: defaultconf_stat.stat.exists

# Добавляем в автозагрузку и стартуем вебсервер:
- name: enable and started nginx
  systemd:
    name: nginx.service
    enabled: yes
    state: started

Роль rhel_install_mysql — установка mysql


Здесь пришлось помучиться с первичной авторизацией в mysql.


Файл roles/rhel_install_mysql/vars/main.yml


---
# vars file for roles/rhel_install_mysql

mysql_repo_rpm: https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
mysql_packets:
  - mysql-community-server
  - mysql-community-common
  - MySQL-python

mysql_log: /var/log/mysqld.log

Файл roles/rhel_install_mysql/handlers/main.yml


---
# handlers file for roles/rhel_install_percona_mysql

- name: restarted mysql
  systemd:
    name: mysqld
    state: restarted

Файл roles/rhel_install_mysql/templates/root.cnf.j2


[client]

user=root
password={{ mysql_root_password }}

Файл roles/rhel_install_mysql/tasks/main.yml


--
-
# tasks file for roles/rhel_install_mysql

# Устанавливаем репозиторий yum:
- name: install mysql repo
  yum:
    name: "{{ mysql_repo_rpm }}"
    state: installed

# Устанавливаем пакеты mysql:
- name: install mysql
  yum:
    name: "{{ mysql_packets }}"
    state: installed

# Добавляем mysql в автозагрузку и запускаем его:
- name: enable mysql
  systemd:
    daemon_reload: yes
    name: mysqld.service
    enabled: yes
    state: started

# Парсим лог файл mysql и получаем из него пароль рута, назначенный при установке и который нам необходимо сменить. Заносим пароль из файла лога в переменную mysql_root_temp_password.
- name: take default root password from mysql log file
  shell: >
    awk -F': ' '$0 ~ "temporary password"{print $2}' {{ mysql_log }} | tail -1
  register: mysql_root_temp_password

# Проверяем, что мы действительно получили временный пароль:
- name: check mysql_root_temp_password
  debug: msg={{ mysql_root_temp_password.stdout }}
  when: mysql_root_temp_password is defined

# Назначаем временный пароль как действительный пароль для рута:
- name: Set temp root pass as root password
  set_fact:
    mysql_root_password: "{{ mysql_root_temp_password.stdout }}"
  when: mysql_root_temp_password is defined

# Копируем шаблон с новым паролем рута:
- name: Copy the root credentials as .my.cnf file
  template:
    src: root.cnf.j2
    dest: "~/.my.cnf"
    mode: 0600

# Проверяем, валиден ли текущий пароль рута:
- name: register password expire
  shell: mysql --defaults-file=~/.my.cnf -e "SELECT NOW();"
  register: password_expired
  ignore_errors: True

- name: check password_expired
  debug: msg={{ password_expired.stdout }}
  when: password_expired is defined

# Наконец-то устанавливаем постоянный пароль для рута на localhost:
- name: ALTER USER root@localhost
  shell: mysql --defaults-file=~/.my.cnf --connect-expired-password -e "ALTER USER root@localhost IDENTIFIED BY '{{ mysql_root_password }}';"
  when: password_expired.stdout.find("expired") != -1

# Устанавливаем постоянный пароль для остальных учётных записей рута:
- name: Update MySQL root password for all root accounts
  mysql_user:
    name: root
    host: "{{ item }}"
    password: "{{ mysql_root_password }}"
    state: present
    priv: "*.*:ALL,GRANT"
  with_items:
    - "{{ ansible_hostname }}"
    - 127.0.0.1
    - ::1
    - localhost
  when: mysql_root_temp_password is defined

# Удалим анонимного пользователя:
- name: Ensure Anonymous user(s) are not in the database
  mysql_user:
    name: ''
    host: "{{ item }}"
    state: absent
  with_items:
    - localhost
    - "{{ ansible_hostname }}"

# Удалим тестовую базу данных:
- name: Remove the test database
  mysql_db:
    name: test
    state: absent

На данном этапе мы получили «чистое» окружение, настройки которого необходимо кастомизировать под наш проект. Сделаем это с помощью дополнительной роли project_configuration.


Роль project_configuration — кастомизация окружения


Перед выполнением роли проверим, что все необходимые компоненты установлены в системе.


Файл roles/project_configuration/tasks/main.yml


######### check java

- name: register installed java
  shell: java -version
  args:
    warn: no
  register: java_present
  failed_when: java_present.rc > 1
  changed_when: no
  tags: check_installed

- debug:
    msg: "{{ java_present.rc }}"
  tags: check_installed

- fail: msg="Please install java first"
  when: java_present.rc == 1
  tags: check_installed

######### check haproxy

- name: register installed haproxy
  shell: rpm -q haproxy
  args:
    warn: no
  register: haproxy_present
  failed_when: haproxy_present.rc > 1
  changed_when: no
  tags: check_installed

- debug:
    msg: "{{ haproxy_present.rc }}"
  tags: check_installed

- fail: msg="Please install haproxy first"
  when: haproxy_present.rc == 1
  tags: check_installed

- include: tasks/project_haproxy.yml
  when: haproxy_present.rc == 0
  tags: check_installed

######### check nginx

- name: register installed nginx
  shell: rpm -q nginx
  args:
    warn: no
  register: nginx_present
  failed_when: nginx_present.rc > 1
  changed_when: no
  tags: check_installed

- debug:
    msg: "{{ nginx_present.rc }}"
  tags: check_installed

- fail: msg="Please install nginx first"
  when: nginx_present.rc == 1
  tags: check_installed

- include: tasks/project_nginx.yml
  when: nginx_present.rc == 0
  tags: check_installed

######### check mysql

- name: register installed mysql
  shell: rpm -q mysql57-community-release
  args:
    warn: no
  register: mysql_present
  failed_when: mysql_present.rc > 1
  changed_when: no
  tags: check_installed

- debug:
    msg: "{{ mysql_present.rc }}"
  tags: check_installed

- fail: msg="Please install mysql first"
  when: mysql_present.rc == 1
  tags: check_installed

- include: tasks/project_mysql.yml
  when: mysql_present.rc == 0
  tags: check_installed

При успешном выполнении проверок подключаются файлы:
tasks/project_haproxy.yml
tasks/project_nginx.yml
tasks/project_mysql.yml


В них мы прописываем установку конфигурационных файлов с помощью шаблонов, установку ssl сертификатов, заведение необходимых системных пользователей, создание баз данных и т.д.


Как результат: после всех манипуляций мы получили рабочее окружение для нашего приложения. Теперь можно приступать к процедуре деплоймента приложения, но это уже совсем другая история, которой мы поделимся в следующей статье.

Комментарии (14)


  1. 0x12ee705
    09.02.2018 15:03

    #Отключаем SELinux
    #Отключаем локальный firewall

    плохая практика даже в тестовом окружении, т.к со временем формируется привычка и отключаешь в Prod


    1. navion
      09.02.2018 16:11

      Это будет справедливо, когда разработчики начнут массово писать правила для SELinux.
      Сейчас же из всех серверов приложений они есть лишь для WAS, а сам Red Hat для JBoss их написать не удосужился.


      1. 0x12ee705
        09.02.2018 16:21

        пример: накатываете случайно роль на prod, в итоге получаете дырявое решето смотрящее наружу, Вас ломают, а Вы, а мы не виноваты это разработчики во всем виноваты.


        1. navion
          09.02.2018 20:10

          Дырявое решето — это если не ставить патчи, а так лишь отключена дополнительная фича безопасности.


  1. ALexhha
    09.02.2018 15:24

    Я так и не понял, что хотели сказать статьей — «Смотрите мы умеем использовать ansible»? Или я просто не заметил скрытый смысл? Так как кроме банальных тасков/ролей не увидил ничего интересного, может просто не туда смотрел


    1. hamnsk
      09.02.2018 20:17

      а что вы хотели увидеть? ну так для примера?


      1. ALexhha
        09.02.2018 20:55

        Какие то уникальные «трудности» и пути их решения, а в итоге получаем стандартную установку nginx/haproxy/mysql.

        Просто не понятно — для кого адресована статья. Если для новичков, то они все равно ничего не поймут, так как нет никакой вводной по ansible. Ну а более опытным пользователем она будет не интересна, имхо


  1. scor2k
    09.02.2018 17:39

    А почему не используете docker? В вашем случае настройка сервера свелась бы к установке docker, docker-compose, ctop :) и пару ролей на деплой продукта…


    1. hamnsk
      09.02.2018 20:17

      тогда уже лучше kubernetes, считаю что необходимо писать по с заделкой на контейнеризацию его и возможность скейлиться.

      Думаю этот плейбук используется для раскатки софта разработчиком, чтобы посмотреть как оно все получилось. Либо в очень маленьком проекте


      1. ALexhha
        09.02.2018 20:58

        тогда уже лучше kubernetes

        очень спорное утверждение. Порог вхождения у кубернетиса намного больше. Да и чем вы собрались управлять собственно


      1. scor2k
        12.02.2018 06:40

        Kubernetes для двух хостов — сильно избыточно. Если хватает обычных плейбуков — то кластер тут точно не нужен.


    1. eastbanctech Автор
      12.02.2018 06:47

      Спасибо за комментарий. Да, мы собираемся переходить на kubernetes.


  1. relgames
    11.02.2018 21:07

    reload haproxy

    Это сработает только для frontend записей. Сами столкнулись с этой проблемой. Нужно делать restart.


    1. eastbanctech Автор
      12.02.2018 06:44

      Спасибо, проверим.