UMGUM.COM (лучше) 

Подготовка системы контейнеризации ( Подготовка системного и прикладного окружения для площадки развёртывания тестовых стендов на базе "Docker". )

4 мая 2019  (обновлено 17 июня 2019)

OS: "Linux Debian 9 (Stretch)", "Linux Ubuntu 18.04 LTS (Bionic Beaver)".
Apps: "Bash", "Docker" & etc.

В этой заметке описывается первый из этапов реализации поставленной в вышестоящей публикации задачи автоматизации процедур развёртывания тестовых стендов из docker-контейнеров.

Последовательность дальнейших действий:

1. Подготовка системного окружения.
2. Обеспечение доступа к файловым ресурсам разработчикам web-сайтов.
3. Обеспечение доступа к внешним файловым ресурсам посредством SSH.
4. Установка системы контейнеризации приложений "Docker".
5. Подготовка специализированного docker-образа для фронтального web-прокси.
6. Подготовка docker-образов с PHP-интерпретаторами разных версий.
7. Подготовка docker-образа для запуска NodeJS-приложений.
8. Подготовка конфигурационных файлов контейнеризируемых сервисов.
9. Автоматизация.


Подготовка системного окружения.

Обновляем программное обеспечение и устанавливаем набор полезных утилит:

# apt update && apt full-upgrade
# apt-get install aptitude coreutils sudo acl psmisc net-tools dnsutils host htop iotop bmon tree findutils pigz rsync mc vim pwgen ntpdate dirmngr nfs-client

Удаляем ненужные нам подсистемы:

# aptitude purge snapd lxd lxcfs open-iscsi mdadm

В дальнейшей работе нам понадобится несколько специальных утилит:

# aptitude install bridge-utils bindfs jq members socat cstream

Если устанавливается "Linux Ubuntu", то лучше бы отключить автоматическое обновление компонентов системы - неприятно бывает обнаружить, что через полгода директория "/boot" забита новыми версиями ядра, установка которых явно не разрешалась.

Устанавливаем точное системное время:

# echo "Asia/Novosibirsk" > /etc/timezone
# rm /etc/localtime && ln -sf /usr/share/zoneinfo/Asia/Novosibirsk /etc/localtime

Синхронизируем время к каким нибудь солидным сервером и сохраняем полученные показатели времени в постоянную память BIOS:

# ntpdate 0.asia.pool.ntp.org && hwclock --systohc --utc

Для корректировки отклонений внутренних часов заведём регулярную (через три часа) сверку с общемировым временем:

# vi /etc/crontab

....
0 */3 * * * root ntpdate asia.pool.ntp.org &

Ненужную нам подсистему синхронизации времени "Systemd" отключаем:

# systemctl disable systemd-timesyncd && systemctl stop systemd-timesyncd

Следом отключаем чаще лишний и некстати подсунутый "Systemd" сервис кеширования результатов DNS-запросов:

# systemctl disable systemd-resolved && systemctl stop systemd-resolved

Подставляем вместо ссылки на конфигурационный файл "Systemd-Resolved" с единственным локальным кеширующим DNS-сервером свой файл, описывая там полноценную DNS-конфигурацию:

# rm /etc/resolv.conf
# vi /etc/resolv.conf

nameserver 10.20.8.8
nameserver 8.8.8.8
search example.net

Проверить работу DNS-резольвинга можно элементарно, запросив разрешение в IP-адрес какого-нибудь действительно существующего FQDN посредством следующей команды:

# dig example.net

В свежеустановленном Linux-сервере запросто может не оказаться полноценной поддержки "национальной кодировки" (в нашем случае - русской). Обычно на новых серверах я не трачу время на проверки, а сразу перехожу в режим конфигурирования, добавляя нужные кодировки и удаляя невостребованные:

# dpkg-reconfigure locales

Естественно, кроме всего прочего, активируем пункт "ru_RU.utf8". В качестве варианта "по умолчанию", применяемого для приложений не требующих определённой кодировки, я выбираю "en_US.UTF-8".

Удостоверяемся в успешности изменения перечня поддерживаемых кодировок:

# locale -a

Защита административного входа.

Сразу после инсталляции базовых компонентов операционной системы запрещаем сетевой вход через SSH для суперпользователя.

# vi /etc/ssh/sshd_config

....
# Отключаем возможность удалённого входа для суперпользователя
PermitRootLogin no

# Запрещаем использование "пустых" паролей при подключении
PermitEmptyPasswords no

# Запрещаем передачу пользовательских переменных окружения
PermitUserEnvironment no

# Запрещаем перенаправление портов пользователя и транзит подключений (это конечный сервер, а не "шлюз")
GatewayPorts no
X11Forwarding no
....

Предварительно проверяем корректность изменений конфигурационного файла и перезапускаем OpenSSH-сервер:

# sshd -t && /etc/init.d/ssh reload

Обеспечение доступа к файловым ресурсам разработчикам web-сайтов.

Доступ разработчиков (выделенных членством в системной группе "developer") к файловой структуре тестовых площадок будем обеспечивать с помощью SFTP, функции SSH сервера "OpenSSH" несущей операционной системе, замыкая их в "chroot"-е, с последующим монтированием ("mount --bind") туда появляющихся файловых систем тестовых площадок.

Исходя из того, что сервер "OpenSSH" уже работает и настроен, вносим в конфигурацию лишь необходимые изменения:

# vi /etc/ssh/sshd_config

....
# Запрещаем пользователю передачу своих переменных окружения
PermitUserEnvironment no

# Устанавливаем режим проверки права доступа пользователя к целевым объектам
StrictModes yes

# Включаем дополнительную функциональность SFTP-сервиса в OpenSSH
# (отключаем перевод на внешнее приложение, активируя втроенные возможности)
# Subsystem sftp /usr/lib/openssh/sftp-server
Subsystem sftp internal-sftp
....

# Запираем пользователей группы "developer" в пределах их площадок разработки и тестирования
Match Group developer User *
  AllowTcpForwarding no
  AllowAgentForwarding no
  X11Forwarding no
  ChrootDirectory /var/opt/devops/chroot/%u
  ForceCommand internal-sftp -u 0007

Параметром "Match" мы выделяем всех пользователей группы "developer", принудительно переводим на работу только в режиме SFTP (запрещая работу в интерактивном "shell"-е), изолируя при этом в "chroot"-е, путь к которому формируется частично автоматически, на основе имени пользователя.

Важный параметр "-u 0007" команды "ForceCommand" переопределяет системные установки "umask 0022", разрешающие членам группы только чтение общих файловых ресурсов, расширяя привилегии группы до уровня владельца файла. Этим мы добиваемся гарантированного доступа к разделяемым файлам как для тестируемых приложений, так и для разработчиков, могущих в разное время перехватывать владение файлами внутри структуры, не ломая при этом логики разрешений доступа, явно запрещая при этом доступ всем остальным (полностью снимая разрешения для "other").

Обращаю особое внимание на то, что конструкция "Match" должна располагаться обязательно после всех остальных определений - после неё может быть или конец файла или другая конструкция "Match". У конструкции "Match" нет определения завершения и всё, что будет ниже неё - будет воспринято как входящее в эту конструкцию.

Проверяем корректность конфигурации и применяем её:

# sshd -t && /etc/init.d/ssh reload

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

Самым простым способом заводим пользователя-разработчика без права использования командной строки и задаём ему пароль:

# groupadd --force --system developer
# useradd --shell /bin/false --create-home --gid developer developer-one && passwd developer-one

Обеспечение доступа к внешним файловым ресурсам посредством SSH.

Принимая во внимание то, что при подключении к Git-репозиториям наверняка будет использоваться аутентификация посредством SSH-keys, считаю полезным этот же подход использовать и для копирования файлов с исходных серверов. Ну, а раз уж мы применяем аутентификацию посредством SSH-keys в двух случаях загрузки данных, то этот же подход есть смысл распространить и на выгрузку срезов "баз данных" - реализовав её в виде SSH-подключения к исходном серверу, запроса "дампа" через наверняка доступный сетевой интерфейс "локальной петли" и выгрузки получаемого потока в SSH-туннеле на сервер тестирования, с возможным урезанием полосы пропускания утилитой "cstream" и "ionice".

На стороне сервера тестирования, от которой будут осуществляться обращения к хранилищам данных исходных серверов и Git-репозитариям, создаём выделенного для взаимодействий с упомянутыми удалёнными ресурсами пользователя (обычно я отталкиваюсь от имени сервера тестирования, элементарно добавляя к нему говорящий префикс, например "get-$(cat /etc/hostname)" - так понятнее, для чего предназначен аккаунт) и генерируем набор SSH-ключей для аутентификации:

# useradd --shell /bin/false --create-home --home-dir /home/get-hostname --gid www-data get-hostname
# mkdir -p /home/get-hostname/.ssh
# ssh-keygen -t rsa -b 2048 -f /home/get-hostname/.ssh/id_rsa -P "" -C "User for requests data for HOSTNAME"

Проверяем корректность данных созданного SSH-ключа:

# ssh-keygen -l -f /home/get-hostname/.ssh/id_rsa

Возможно это слегка небезопасно, но в этой схеме я разрешаю подключаться SSH-пользователю "get-hostname" к удалённым серверам без запроса на подтверждение приёма "fingerprint" их SSH-ключей - иначе при добавлении каждого нового источника данных придётся проделывать лишнюю ручную работу:

# vi /home/get-hostname/.ssh/config

StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel ERROR

Обращаю внимание, что файлы SSH-ключей должны быть доступны только какому-то определённому пользователю, но не группе таковых - иначе в некоторых строгих конфигурациях OpenSSH-клиент откажется с ними работать (заявив при этом что-то вроде "Permissions are too open" или, как ни странно, "Permission denied"):

# chown -R get-hostname /home/get-hostname/.ssh
# chmod -R go-rwx /home/get-hostname/.ssh

Разумеется, на стороне исходного серверов, откуда забираются данные, потребуется завести одноимённого пользователя:

# useradd --shell /bin/bash --create-home --home-dir /home/get-hostname --gid www-groupname get-hostname

Если такой пользователь уже имеется и его функционал шире требующегося, то доступ к нужным файловым ресурсам ему можно предоставить посредством добавления в соответствующую группу:

# usermod --append --groups www-groupname get-hostname

Для наладки аутентификации прежде всего получаем содержимое открытого (публичного) ключа пользователя "get-hostname" командой "cat /home/get-hostname/.ssh/id_rsa.pub" (с сервера тестирования) и добавляем его в перечень ключей доступа "/home/get-hostname/.ssh/authorized_keys" на стороне исходного web-сервера, а также на серверах Git-репозиториев в перечень SSH-keys, аутентифицировавшимся посредством которых разрешён доступ к ресурсам.

Обращаю внимание, для для специализированных групп пользователей на web-серверах иногда ограничивается функционал при подключении через SSH. Тогда нужно сделать для нашего пользователя исключение - например так мы выводим пользователя "get-hostname" из под принудительного перевода в режим работы только с SFTP:

# vi /etc/ssh/sshd_config

....
Match Group www-groupname User *,!get-hostname
  ForceCommand internal-sftp
....

Установка системы контейнеризации приложений "Docker".

Одно из клёвейших открытий последних последних лет для меня - "Docker". Это программное обеспечение для автоматизации развёртывания и управления приложениями в средах с поддержкой контейнеризации, позволяющее "упаковать" приложение со всем его окружением и зависимостями в контейнер, который может быть применён на любой современной Linux-системе с поддержкой "cgroups" в ядре и изоляцией "пространств имён (namespaces)".

Написан на языке Go. Изначально использовал возможности LXC, с 2015 года применял собственную библиотеку, абстрагирующую виртуализационные возможности ядра Linux - "libcontainer". С появлением "Open Container Initiative" начался переход от монолитной к модульной архитектуре.

Для экономии дискового пространства проект использует файловую систему "Aufs" с поддержкой технологии каскадно-объединённого монтирования: контейнеры используют образ базовой операционной системы, а изменения записываются в отдельную область. Также поддерживается размещение контейнеров в файловой системе "Btrfs" с включённым режимом копирования при записи.

В состав программных средств входит сервер контроля контейнеров "docker" и сервер запуска контейнеров как таковых "containerd", а также клиентские средства, позволяющие из интерфейса командной строки управлять образами и контейнерами (включая API, позволяющий в стиле REST управлять контейнерами программно).

Инструментарием "Docker"-а обеспечивается полная изоляция запускаемых контейнеров на уровне файловой системы (у каждого контейнера собственная корневая файловая система), на уровне процессов (процессы имеют доступ только к собственной файловой системе контейнера, а ресурсы разделены средствами "libcontainer"), на уровне сети (каждый контейнер имеет доступ только к привязанному к нему сетевому "пространству имён" и соответствующим виртуальным сетевым интерфейсам).

По состоянию на Май 2019 несколько поколений "Docker" стали устаревшими и неподдерживаемыми: их дистрибутивы как правило называются "docker", "docker.io" или "docker-engine". Есть смысл профилактически зачистить систему от них:

# apt remove docker docker-engine docker.io containerd runc

Данные, по умолчанию располагающиеся в "/var/lib/docker/" могут быть наследованы новыми версиями, возможно с их автоматической конвертацией под следующие мажорные релизы.

Современный "Docker" выпускается в двух вариантах: "Community Edition" (бесплатная) и "Enterprise Edition" (проприетарная, платная). Мы будем работать со свободно распространяемым дистрибутивом, именуемым в APT-системе "docker-ce".

В централизованных репозиториях стабильных версий "Debian/Ubuntu" актуальные пакеты "Docker" отсутствуют - оно и понятно, технология переднего края, непрерывно развивающаяся. Единственный простой способ добычи дистрибутива в подключении репозитория разработчиков и установке оттуда.

Прежде всего обеспечим наличие в системе утилит, необходимых для ручного подключения дополнительных APT-репозиториев:

# apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common

Перед подключением нового APT-репозитория скачаем и применим PGP-ключ, которым подписано содержимое репозитория:

# curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | sudo apt-key add -

Создаём выделенный конфигурационный файл с описанием подключаемого APT-репозитория:

# echo -e "# Official APT-repository Docker-CE\ndeb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable" >> /etc/apt/sources.list.d/docker.list

Обновляем сведения о доступном программном обеспечении и устанавливаем базовый набор подсистем "Docker":

# apt-get update && apt-get install docker-ce docker-ce-cli containerd.io

Обращаю внимание на то, что в качестве прослойки абстракции связей контейнеров "Docker-CE" с ядром "Linux" сейчас используется открытая реализация "Open Container Initiative (containerd.io)", сменившая использовавшуюся в предыдущем поколении "Docker" самодельную подсистему "docker-containerd".

Конечно, при остром желании можно установить "Docker" из DEB-пакетов, скачав их из хранилища дистрибутивов, потеряв при этом удобства автоматического обновления.

Сразу после установки APT-пакетов "Docker" автоматически будут запущены два сервиса.

Прослойка изоляции и абстракции ("cgroups" и "namespaces", а также "aufs|btrfs") между ядром Linux и контейнерами:

# systemctl status containerd.service

Централизованный сервис управления "виртуальными контейнерами" как таковыми:

# systemctl status docker.service

Система контейнеризации "Docker" вполне управляема непривилегированным пользователем - для этого достаточно включить его в специализированную группу "docker":

# usermod --append --groups docker username

Подготовка специализированного docker-образа для фронтального web-прокси.

Предлагаемый разработчиками "Nginx" docker-образ меня полностью устраивает и далее он будет использоваться, но от фронтального web-прокси мне потребуется возможность ограничения доступа к ряду директорий аутентификацией самым простым способом, с использованием системной пользовательской базы, посредством механизма "Linux PAM". В этом самодельном docker-образе мы обеспечим на стороне операционной системы возможность приёма от "Nginx" соответствующих запросов, создав PAM-профиль "/etc/pam.d/nginx", разрешив web-серверу читать данные из "/etc/shadow" и добавив в конфигурацию "Nginx" загрузку модуля-посредника.

Собираем контейнер с "Nginx v1.14", поддерживающим "simple HTTP Authentication via PAM":

# mkdir -p /usr/local/etc/devops/images/nginx
# cd /usr/local/etc/devops/images/nginx

Конфигурационные файлы "Nginx" в изначальном варианте, хотя бы и из соображений унификации, лучше использовать те, что в официальной стабильной сборке:

# mkdir -p ./nginx-1.14-auth-pam/etc
# docker pull nginx:stable && docker create --name tmp-nginx-stable nginx:stable
# docker cp tmp-nginx-stable:/etc/nginx ./nginx-1.14-auth-pam/etc
# docker rm tmp-nginx-stable

Задаём порядок построения "образа" с "Nginx v1.14":

# vi ./Dockerfile-nginx-1.14-auth-pam

FROM ubuntu:bionic

LABEL maintainer="NSU, Andrey Narozhniy"

ENV HOME /root
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y apt-utils \
  && apt-get install --no-install-recommends -y ca-certificates \
  && apt-get install --no-install-recommends -y \
    nginx-light libnginx-mod-http-auth-pam tzdata \
  && apt-get remove apt-utils ca-certificates -y \
  && apt-get purge --auto-remove -y \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && echo "@include common-auth" > /etc/pam.d/nginx \
  && echo "auth required pam_succeed_if.so quiet user ingroup developer" >> /etc/pam.d/nginx \
  && echo "auth optional pam_faildelay.so delay=3000000" >> /etc/pam.d/nginx \
  && usermod --append --groups shadow www-data \
  && useradd --system --no-create-home --home-dir /nonexistent --shell /bin/false nginx \
  && usermod --append --groups shadow nginx \
  && ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log \
  && rm -rf /etc/nginx

COPY nginx-1.14-auth-pam/etc/nginx /etc/nginx

RUN sed -i -e '/^pid.*/a \\nload_module modules/ngx_http_auth_pam_module.so;' /etc/nginx/nginx.conf

EXPOSE 80 443

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

Собираем "образ", явно отключив кеширование на основе слоёв от предыдущих попыток:

# docker build --no-cache --tag selfmade:nginx-1.14-auth-pam --file ./Dockerfile-nginx-1.14-auth-pam . >> ./build-nginx-1.14-auth-pam.log

Удостоверяемся в появлении нового docker-образа:

# docker images

REPOSITORY TAG             ... SIZE
selfmade   nginx-1.14-auth-pam 80MB
nginx      stable              109MB

Подготовка docker-образов с PHP-интерпретаторами разных версий.

Возможно я чего-то не понимаю в подходах "докеризации", но мне очень не нравится контейнер PHP-FPM, предлагаемый самими разработчиками PHP-интерпретатора - его конфигурационные файлы вынесены в неудобное место, функционал урезан и некоторых необходимых мне модулей нет - потому делаем "образы" самостоятельно:

# mkdir -p /usr/local/etc/devops/images/php-fpm

Собираем контейнер с "PHP v7.0":

# cd /usr/local/etc/devops/images/php-fpm
# vi ./Dockerfile-php-7.0-fpm

FROM debian:stretch-slim

LABEL maintainer="NSU, Andrey Narozhniy"

ENV HOME /root
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y apt-utils \
  && apt-get install --no-install-recommends -y \
    ca-certificates git curl bzip2 file graphicsmagick-imagemagick-compat tzdata msmtp \
  && apt-get install --no-install-recommends -y \
    php7.0-fpm php7.0-cgi php7.0-cli php7.0-opcache php7.0-common \
    php7.0-mysql php7.0-pgsql php7.0-mbstring php7.0-xml php7.0-json php7.0-gd php7.0-curl php7.0-ldap php7.0-mcrypt \
    php7.0-soap php7.0-xmlrpc php7.0-intl php7.0-pspell php7.0-zip php7.0-bz2 \
    php-memcache php-mongodb php-pclzip php-geoip \
  && apt-get remove apt-utils ca-certificates -y \
  && apt-get purge -y --auto-remove \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && mkdir -p /run/php && chown www-data:www-data /run/php \
  && ln -s /usr/bin/msmtp /usr/sbin/sendmail \
  && ln -sf /dev/stdout /var/log/msmtp.log \
  && ln -sf /dev/stderr /var/log/php7.0-fpm.log

EXPOSE 9000

# in the regular way start PHP-FPM
CMD [ "/usr/sbin/php-fpm7.0", "-F", "-O", "-c", "/etc/php/7.0/fpm/php.ini", "-y", "/etc/php/7.0/fpm/php-fpm.conf" ]

# docker build --no-cache --tag selfmade:php-7.0-fpm --file ./Dockerfile-php-7.0-fpm . >> ./build-php-7.0-fpm.log

Собираем контейнер с "PHP v7.2":

# cd /usr/local/etc/devops/images/php-fpm
# vi ./Dockerfile-php-7.2-fpm

FROM ubuntu:bionic

LABEL maintainer="NSU, Andrey Narozhniy"

ENV HOME /root
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y apt-utils \
  && apt-get install --no-install-recommends -y \
    ca-certificates git curl bzip2 file graphicsmagick-imagemagick-compat tzdata msmtp \
  && apt-get install --no-install-recommends -y \
    php7.2-fpm php7.2-cgi php7.2-cli php7.2-opcache php7.2-common \
    php7.2-mysql php7.2-pgsql php7.2-mbstring php7.2-xml php7.2-json php7.2-gd php7.2-curl php7.2-ldap \
    php7.2-soap php7.2-xmlrpc php7.2-intl php7.2-pspell php7.2-zip php7.2-bz2 \
    php-memcache php-mongodb php-pclzip php-geoip php-libsodium \
  && apt-get remove apt-utils ca-certificates -y \
  && apt-get purge -y --auto-remove \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && mkdir -p /run/php && chown www-data:www-data /run/php \
  && ln -s /usr/bin/msmtp /usr/sbin/sendmail \
  && ln -sf /dev/stdout /var/log/msmtp.log \
  && ln -sf /dev/stderr /var/log/php7.2-fpm.log

EXPOSE 9000

# in the regular way start PHP-FPM
CMD [ "/usr/sbin/php-fpm7.2", "-F", "-O", "-c", "/etc/php/7.2/fpm/php.ini", "-y", "/etc/php/7.2/fpm/php-fpm.conf" ]

# docker build --no-cache --tag selfmade:php-7.2-fpm --file ./Dockerfile-php-7.2-fpm . >> ./build-php-7.2-fpm.log

Можно заметить, что в сборке контейнера PHP-интерпретатора включен Git-клиент. Это не совсем хорошо в плане безопасности - в идеале web-серверы должны только отвечать на запросы, сами ничего себе не себе не загружая, но разработчики используют "Compose" для сборки современных CMS, и без него им некомфортно.

Пробно запускаем контейнер с PHP-FPM:

# docker run -d --rm --name test-php-fpm selfmade:php-7.2-fpm

Убеждаемся, что PHP-интерпретатор в FCGI-обёртке запущен внутри контейнера:

# docker top test-php-fpm

UID      PID   PPID ... CMD
root     19616 19588    php-fpm: master process (/etc/php/7.2/fpm/php-fpm.conf)
www-data 19669 19616    php-fpm: pool www
www-data 19670 19616    php-fpm: pool www

После запуска контейнера очень полезно посмотреть, какие файлы в нём появились или изменились относительно родительского "образа" - их не должно быть слишком много, и важно не допустить бессмысленного избыточного создания данных внутри контейнера:

# docker diff test-php-fpm

C /run
C /run/php
A /run/php/php7.2-fpm.pid
A /run/php/php7.2-fpm.sock

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

# docker logs test-php-fpm

NOTICE: fpm is running, pid 1
NOTICE: ready to handle connections
NOTICE: systemd monitor interval set to 10000ms

Останавливаем контейнер (если при запуске употреблялся ключ "--rm", то после остановки контейнер будут автоматически удалён):

# docker stop test-php-fpm

Подготовка docker-образа для запуска NodeJS-приложений.

Методика запуска на исполнение приложений в "NodeJS" отличается от традиционно применяемой в интерпретируемых средах программирования вроде "Perl" или PHP. В случае использования последних web-сервер при нахождении исполняемого файла, по тем или иным критериям подходящего, отправляет его код на обработку в определённый произвольный преднастроенный интерпретатор, причём в идеальном случае интерпретатор как таковой может и не иметь непосредственного доступа к исполняемому коду проекта, получая его лишь от web-сервера. В противовес "классическому" в web-е подходу приложение "NodeJS" считается монолитным и представляется единым сервисом, которому на этапе запуска предоставляется вся необходимая кодовая база и одна точка входа, через которую принимаются запросы и выдаются ответы.

Следствием монолитности архитектуры является необходимость передавать "NodeJS" на этапе запуска сразу весь код приложения и учитывать, что привычный для программирования на интерпретируемых языках подход с изменением кода проекта "на лету" в "NodeJS" не работает - после каждого изменения приложение нужно перезапускать.

Предоставляемый разработчиками "NodeJS" docker-образ несёт в себе лишь саму среду исполнения и служит основой для сборки "образа", предназначающегося для запуска конкретного web-приложения. На сервисах публикации проще и эффективнее собирать docker-образ с упакованным прямо в него кодом web-приложения, но для среды тестирования удобнее иметь унифицированный "образ", запускающий с примонтированного "тома" любое полностью готовое (не нуждающееся в сборке зависимостей) web-приложение имеющее корректный "package.json".

Собираем контейнер с "NodeJS v8.16":

# mkdir -p /usr/local/etc/devops/images/nodejs
# cd /usr/local/etc/devops/images/nodejs
# vi ./Dockerfile-nodejs-8.16

FROM node:8.16

LABEL maintainer="NSU, Andrey Narozhniy"

ENV HOME /var/www
ENV DEBIAN_FRONTEND noninteractive

RUN npm install --no-optional --global nodemon

EXPOSE 8000

# in the regular way start NodeJS with "package.json"
CMD [ "npm", "start" ]

# docker build --no-cache --tag selfmade:nodejs-8.16 --file ./Dockerfile-nodejs-8.16 . >> ./build-nodejs-8.16.log

Подготовка конфигурационных файлов контейнеризируемых сервисов.

После перебора вариантов передачи конфигурации контейнерам стенда тестирования я остановился на полном замещении директорий конфигурационных файлов внутри контейнеров своим набором файлов, основанным на дистрибутивном (извлечённом из тех же контейнеров), перекрываемым при желании специфичными индивидуальными.

Обобщённая начальная схема конфигурационных директорий:

# tree

./conf
├─ front
│  └─ etc
│     └─ nginx
├─ bunch
│  └─ etc
│     ├─ memcached
│     ├─ mysql
│     ├─ nginx
│     └─ php
│        ├─ 7.0
│        └─ 7.2
└─ sitename
   └─ etc
      └─ nginx

Таким образом, файлы базовой конфигурации для "Nginx" будут взяты из директории "./conf/bunch/etc/nginx/", но они могут быть перезаписаны оптимизированными конкретно для тестируемого сайта файлами из директории "./conf/sitename/etc/nginx/".

Прежде всего получаем дистрибутивные варианты конфигурационных файлов:

# docker create --name tmp-nginx-stable selfmade:nginx-1.14-auth-pam
# mkdir -p /usr/local/etc/devops/conf/front/etc/nginx
# docker cp tmp-nginx-stable:/etc/nginx /usr/local/etc/devops/conf/front/etc
# docker rm tmp-nginx-stable

# docker pull nginx:stable && docker create --name tmp-nginx-stable nginx:stable
# mkdir -p /usr/local/etc/devops/conf/bunch/etc/nginx
# docker cp tmp-nginx-stable:/etc/nginx /usr/local/etc/devops/conf/bunch/etc
# docker rm tmp-nginx-stable

# docker create --name tmp-php-7.0-fpm selfmade:php-7.0-fpm
# mkdir -p /usr/local/etc/devops/conf/bunch/etc/php
# docker cp tmp-php-7.0-fpm:/etc/php/7.0 /usr/local/etc/devops/conf/bunch/etc/php
# docker rm tmp-php-7.0-fpm

# docker create --name tmp-php-7.2-fpm selfmade:php-7.2-fpm
# mkdir -p /usr/local/etc/devops/conf/bunch/etc/php
# docker cp tmp-php-7.2-fpm:/etc/php/7.2 /usr/local/etc/devops/conf/bunch/etc/php
# docker rm tmp-php-7.2-fpm

# docker pull mysql:5.7 && docker create --name tmp-mysql-5.7 mysql:5.7
# mkdir -p /usr/local/etc/devops/conf/bunch/etc/mysql
# docker cp tmp-mysql-5.7:/etc/mysql /usr/local/etc/devops/conf/bunch/etc
# docker rm tmp-mysql-5.7

Далее приведём конфигурационные файлы контейнеризированных сервисов к желаемому нам виду.

Подготовка конфигурации фронтального web-прокси "Nginx".

Фронтальный web-прокси на базе "Nginx":

# vi /usr/local/etc/devops/conf/front/etc/nginx/nginx.conf

user www-data www-data;
worker_processes 8;
....

# Подключаем модуль "simple HTTP Authentication via PAM"
load_module modules/ngx_http_auth_pam_module.so;

http {
  ....
  # Запрещаем web-серверу сообщать о себе подробные данные
  server_tokens off;

  # Запрещаем просмотр содержимого директории, если не указан целевой файл
  autoindex off;

  # Снимаем ограничение на размер тела запроса
  client_max_body_size 0;

  # Подключаем конфигурационные файлы тестовых стендов
  include /etc/nginx/bunch.d/*.conf;
....

Для первоначального запуска "Nginx" нужно подготовить как минимум один файл конфигурации "сайта по умолчанию" (который мы будем в дальнейшем использовать для отображения посредством SSI журналов событий подсистем площадок тестирования), а также заранее озаботится подкладыванием SSL-ключей в директорию "/etc/ssl/nginx", если таковые используются:

# vi /usr/local/etc/devops/conf/front/etc/nginx/conf.d/default.conf

server {
  server_name _;
  listen 80 default_server;

  # (никаких ресурсов этот HTTP-сервер не обслуживает, лишь проксируя или перенаправляя запросы)

  # Проксируем LetsEncrypt-запросы обслуживающему их серверу
  location ~ ^/.well-known/acme-challenge/ {
    # (очевидно нужно указывать внешний относительно контейнера адрес, IP или FQDN)
    proxy_pass http://10.20.30.40:880;
    proxy_set_header X-Scheme http;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Proto http;
  }

  # Все необслуженные запросы перенаправляем на HTTPS-сервер
  location / {
    rewrite ^ https://$host$request_uri permanent;
    #deny all; # (опционально можно не перенаправлять запросы, а запрещать доступ)
  }
}

server {
  server_name _;
  listen 443 ssl http2 default_server;
  include ./ssl_wildcard.conf;

  # Конфигурация сервиса отображения журналов событий
  location ~ ^/log {
    # Ограничиваем доступ, разрешая таковой только из LAN
    satisfy all;
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    allow 192.168.0.0/16;
    deny all;

    # Включаем SSI
    ssi on; ssi_silent_errors on;
    root /var/www;
    index index.html;
  }

  # Проксируем Webhook-запросы обслуживающему их серверу
  location ~ ^/webhook/ {
    # (очевидно нужно указывать внешний относительно контейнера адрес, IP или FQDN)
    proxy_pass https://10.20.30.40:8443;
    proxy_ssl_verify off;
    proxy_ssl_server_name on;
    proxy_ssl_session_reuse on;
    proxy_set_header X-Scheme https;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Ssl on;
  }

  # Все необслуженные запросы отвергаем
  location / { deny all; }
}

Пример файла конфигурации подключения ключей SSL-сертификатов:

# vi /usr/local/etc/devops/conf/front/etc/nginx/ssl_wildcard.conf

ssl_dhparam /etc/ssl/nginx/dhparam.pem;
ssl_certificate /etc/ssl/nginx/wildcard.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/wildcard.example.net.key.decrypt;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl on;

В случае использования SSL-сертификатов "Let`s Encrypt" для какого-то определённого сайта потребуется подложить индивидуальный конфигурационный файл, примерно такого вида:

# vi /usr/local/etc/devops/conf/bunch/test-site.example.net/etc/nginx/ssl_www.test-site.example.net.conf

ssl_dhparam /etc/ssl/nginx/dhparam.pem;
ssl_certificate /etc/letsencrypt/live/www.test-site.example.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.test-site.example.net/privkey.pem;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl on;

Наладка "PAM + HTTP Authentication" на фронтальном web-прокси "Nginx".

При сборке доработанного для фронтального web-прокси "Nginx" мы уже установили позволяющий проводить аутентификацию пользователей группы "developer" посредством "Linux PAM" модуль "libnginx-mod-http-auth-pam" и обеспечили на стороне системы возможность приёма от "Nginx" соответствующих запросов, создав PAM-профиль, разрешив web-серверу читать данные из "/etc/shadow" внутри контейнера и внеся в конфигурацию web-сервера команду загрузки модуля.

Теперь доработаем конфигурацию сайта, включив в ней аутентификацию для защищаемых от несанкционированного доступа ресурсов (не забыв сделать это как для HTTP-блока, так и HTTPS):

# vi /usr/local/etc/devops/conf/front/etc/nginx/conf.d/default.conf

....
  location ~ ^/log/ {
    # Включаем обязательную HTTP-аутентификацию
    auth_pam "Restricted area";
    auth_pam_service_name "nginx";
    ....
  }
}

Пробно запускаемся на свободном сетевом порту:

# docker run --rm -p 8080:80 -v /usr/local/etc/devops/conf/front/etc/nginx:/etc/nginx:ro -d --name test-nginx selfmade:nginx-1.14-auth-pam

Функциональность PAM + "Nginx HTTP Authentication" требует наличия аккаунтов соответствующих пользователей (напоминаю, что ранее мы ограничились обслуживанием только системной группы "developer"). Проще всего создавать таковые внутри контейнера, передавая туда необходимый минимум данных, извлечённых из системных файлов несущей системы:

# USRN="developer-one" && UPASSWORD=$(cat /etc/shadow | grep -i "^${USRN}:" | awk -F ':' '{print $2}') && docker exec -ti test-nginx /bin/bash -c "groupadd --force --system developer && useradd --no-create-home --home-dir /nonexistent --shell /bin/false --gid developer ${USRN} && usermod -p '${UPASSWORD}' ${USRN}"

Пробуем обратиться к web-ресурсу, тестируем аутентификацию, возможно для отладки заходим внутрь контейнера и по завершению игрищ останавливаем временный контейнер:

# docker exec -ti test-nginx bash
# docker stop test-nginx

Подготовка конфигурации web-сервера "Nginx" тестового стенда.

Изменения в конфигурации web-сервера "Nginx" тестового стенда:

# vi /usr/local/etc/devops/conf/bunch/etc/nginx/nginx.conf

user www-data www-data;
worker_processes 4;

http {
  ....
  # Снимаем ограничение на размер тела запроса
  client_max_body_size 0;

  # (VERY IMPORTANT: use built-in Docker DNS-resolver)
  resolver 127.0.0.11 ipv6=off valid=30s;
....

В конфигурацию упакованного в docker-контейнер "Nginx" новые сайты принято добавлять через директорию "/etc/nginx/conf.d/". В дальнейшем создание простейшего конфигурационного файла будет автоматизировано, а пока удаляем "сайт по умолчанию":

# rm /usr/local/etc/devops/conf/bunch/etc/nginx/conf.d/default.conf

Подготовка конфигурации PHP-интерпретатора тестового стенда.

Изменения в конфигурации PHP-интерпретатора (справка):

# vi /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm/php.ini

....
cgi.fix_pathinfo = 0
....
short_open_tag = On
....
; (после v.5.4 PHP-интерпретатору нужно явно указывать временную зону)
date.timezone = Asia/Novosibirsk
....
memory_limit = 1024M
....
max_execution_time = 300
max_input_time = 300
post_max_size = 50M
upload_max_filesize = 50M
max_file_uploads = 100
max_input_vars = 10000
....
pcre.jit = 0
....
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 50000
opcache.validate_timestamps = 1
opcache.use_cwd = 1
opcache.revalidate_path = 1
opcache.revalidate_freq = 0
....

Для CMS "Bitrix" в дополнение к вышеприведённым важны следующие параметры как для PHP-FPM, так и для PHP-Cli:

# vi /usr/local/etc/devops/conf/bunch/etc/php/7.0/cli/php.ini
# vi /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm/php.ini

....
pcre.backtrack_limit = 100000
pcre.recursion_limit = 100000
....
mbstring.default_charset = UTF-8
mbstring.internal_encoding = UTF-8
mbstring.detect_order = "UTF-8"
mbstring.encoding_translation = on
mbstring.func_overload = 2
mbstring.strict_detection = on
....

Конфигурационные файлы FPM-пулов я всегда полностью переписываю:

# rm /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm/pool.d/www.conf
# vi /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm/pool.d/tcp-unix.conf

; Инстанс PHP-FPM, работающий через TCP-подключение
[tcp]

; Указываем пользователя и группу, от имени которых запускается сервис
user = www-data
group = www-data

; Принимаем подключения через TCP
listen = 0.0.0.0:9000

; Деактивируя параметр разрешаем подключение по TCP откуда угодно (подсеть изолирована группой тестирования)
;listen.allowed_clients =

; Режим запуска инстанса
pm = dynamic
; Количество процессов, запускаемых при старте PHP-FPM
pm.start_servers = 2
; Максимальное количество процессов, которые могут быть запущены для обработки запросов
pm.max_children = 32
; Параметры количества запущенных неактивных процессов (находящихся в ожидании запросов)
pm.min_spare_servers = 2
pm.max_spare_servers = 5
; Количество запросов, после которого процесс будет перезапущен (для компенсации "утечек памяти" в скриптах)
pm.max_requests = 1024

; Разрешаем чтение системных переменных окружения (default: Yes)
clear_env = No

; Инстанс PHP-FPM, работающий через локальный файловый "сокет"
[unix]

user = www-data
group = www-data

; Принимаем подключения через локальный файловый "сокет"
listen = /run/php/php-fpm.sock

; Задаём владельца файлового "сокета" и режим доступа к нему
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Режим запуска инстанса
pm = dynamic
pm.start_servers = 2
pm.max_children = 32
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 1024

; Разрешаем чтение системных переменных окружения (default: Yes)
clear_env = No

Возможно для некоторых тестируемых сайтов понадобится изменить в конфигурации параметры (следует иметь в виду, что не все параметры можно переопределить динамически - нужно сверяться с документацией ):

# vi /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm/pool.d/tcp-unix.conf

....
; Блок переопределения параметров PHP-интерпретатора
php_admin_value[memory_limit] = 1024M

Конфигурация PHP от версии к версии меняется незначительно, так что при появлении новой достаточно просто скопировать уже имеющийся набор файлов:

# mkdir -p /usr/local/etc/devops/conf/bunch/etc/php/7.2
# cp -r /usr/local/etc/devops/conf/bunch/etc/php/7.0/fpm /usr/local/etc/devops/conf/bunch/etc/php/7.2/fpm

Подготовка конфигурации СУБД "MySQL" тестового стенда.

Изменения в конфигурации СУБД:

# vi /usr/local/etc/devops/conf/bunch/etc/mysql/mysql.conf.d/mysqld.cnf

....
[mysqld]
....

# Задаём фиксацию сообщений журнала событий с датой в общесистемном часовом поясе (по умолчанию: UTC)
log_timestamps = 'SYSTEM'

# Для совместимости с грязнокодными CMS выключаем строгое следование SQL-стандартам
sql_mode = ''
explicit_defaults_for_timestamp = 1

# Включаем приём подключений через файловый "сокет"
socket = /var/run/mysqld/mysqld.sock

# Разрешаем подключение по TCP откуда угодно (подсеть изолирована группой тестирования)
bind-address = 0.0.0.0

# Отключаем ведение "бинарного" журнала транзакций
# (это значение параметра применяется только на тестовых серверах, для достижения максимальной производительности)
skip-log-bin

# Отключаем режим согласования с подсистемой ядра "Linux asynchronous I/O"
# (это понадобится в случае запуска БД на ФС не поддерживающих AIO)
# (это значение параметра применяется только на тестовых серверах, для достижения максимальной совместимости)
innodb_use_native_aio = 0  # (default: 1)

# Включаем поддержку нового формата файлов хранения данных InnoDB
innodb_file_format = barracuda

# Явно указываем создавать для каждой таблицы отдельные файлы описаний (.frm) и данных (.ibd) на диске, а не сваливать всё в один (по умолчанию "ibdataX")
innodb_file_per_table = 1 # (default: 0)

# Включаем поддержку длинных индексных ключей
innodb_large_prefix = 1 # (default: 0)

# Указываем сохранять журнал транзакций InnoDB независимо от транзакций как таковых, параллельно
# (это значение параметра применяется только на тестовых серверах, для достижения максимальной производительности)
innodb_flush_log_at_trx_commit = 0 # (default: 1; optimal: 2)

# Отключаем процедуру двухэтапного сохранения данных
# (это значение параметра применяется только на тестовых серверах, для достижения максимальной производительности)
innodb_doublewrite = 0 # (default: 1)

# Пробуем включить режим сброса данных на диск без контроля успешности
# (это значение параметра применяется только на тестовых серверах, для достижения максимальной производительности)
innodb_flush_method = nosync # (default: fsync)

# Увеличиваем количество параллельных потоков чтения/записи (по умолчанию: 4)
innodb_read_io_threads = 16 # (default: 4)
innodb_write_io_threads = 16 # (default: 4)

# Увеличиваем размер буфера запросов и журнала транзакций
innodb_buffer_pool_size = 2G # (default: 128MB; optimal: 60% RAM)
innodb_log_file_size = 512M # (default: 5MB)
innodb_log_buffer_size = 256M # (default: 8MB)
....

Подготовка конфигурации инстанса "Memcached" тестового стенда.

Текстовый файл с переменными-параметрами для запуска "Memcached":

# mkdir -p /usr/local/etc/devops/conf/bunch/etc/memcached
# touch /usr/local/etc/devops/conf/bunch/etc/memcached/memcached.conf

-m 128
-c 1024
-t 4
-s /var/run/memcached/memcached.sock
-a 770

Подготовка конфигурации почтовой подсистемы тестового стенда.

Текстовый файл с параметрами утилиты отправки почты "msmtp":

# vi /usr/local/etc/devops/conf/bunch/etc/msmtprc

# Set default values for all following accounts.
defaults
auth off
tls off
logfile /var/log/msmtp.log

# Default Account
account default
host mx.example.net
port 25
from www-site@example.net

Автоматизация.

В этой заметке мы провели подготовительные работы в рамках реализации поставленной в вышестоящей публикации задачи автоматизации процедур развёртывания тестовых стендов из docker-контейнеров - автоматизация как таковая рассматривается в следующих публикациях.


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )