Pull to refresh

В разрезе: новостной агрегатор на Android с бэкендом. Система контроля конфигураций (Puppet)

Reading time 13 min
Views 2.1K
Вводная часть (со ссылками на все статьи)

В ITIL (v3) среди описанных процессов есть 2 особенно интересных: «Процесс управления конфигурациями» и «Процесс управления изменениями», предназначенных для анализа и управления изменениями конфигураций систем. Для продолжения повествования нужно определиться, что такое «система». В это понятие входит огромное количество составляющих, влияющих (прямо или косвенно) на предоставление услуги:

  • серверы

    • настройки безопасности (пользователи, группы, права, межсетевые экраны);
    • установленные приложения и библиотеки;
    • настройки работы приложений (лимиты по дискрипторам, памяти, времени CPU и т.д.);
    • резервное копирование;

  • системы мониторинга за работой прикладного и системного ПО;
  • конфигурационные файлы самого продукта, его компонентов, вспомогательных системных и прикладных приложений
  • ...

Пытаться минимизировать контур системы вашего проекта (тип, резервное копирование не относится к функционированию системы) – значит рыть себе яму, в которую рано или поздно вы провалитесь.


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

  • очень длительному процессу развёртывания/дублирования (все вспоминают, что и как настроено по мере возникновения сбоев и часто сверяют настройки с имеющимся стендом);
  • неожиданно возникают проблемы при эксплуатации развёрнутого продукта (забывают выполнить какие-то настройки, которые на тестовом стенде внесли в последний момент, а на продуктив не перенесли);
  • замедляется процесса тестирования (каждая публикация новой сборки, даже для внутренних тестировщиков, — подвиг как по времени, так и по сложности);
  • и наконец – появляется страх у разработчиков перед выпуском новой версии и как результат – оттягивание выкладки нового релиза (реально серьёзная проблема о которой открыто никто не скажет, но её эффект будет чувствоваться постоянно).

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

Мотивация


Вещи вроде очевидные, но в большинстве групп по разработке на них закрываются глаза – многие аргументируют это тем, что сервер у них один, настроек мало, компетентностью работников (аргумент «у нас работают профессионалы и никуда уходить не собираются» меня просто свалил), малыми рисками и т.д. и т.п.

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

Мотивация для разработчика-одиночки


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

  • т.к. разработка осуществляется не ежедневно, как результат некоторые особенности развёртывания плавно улетучиваются по прошествии пары дней;
  • автоматизация для одиночки – самый надёжный напарник;
  • скорость выполнения работ, обеспечиваемая автоматизацией, позволяет не потерять «запал» и желание развивать проект (это очень важный специфичный момент).

Важная оговорка про ITIL


Не старайтесь брать, из описанных в ITIL процессов, все шаги которые там описаны, — ситуация будет хуже, чем до этого! Я знаю, как подобные шаги внедряются в некоторыъ банковских системах и к чему это приводит (особенно без автоматизации) – бюрократическая машина удушит любой динамично развивающийся проект/систему.

Puppet


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

Откровенно говоря, кривая изучения для Puppet была вовсе не пологой. В связи с тем, что в разрабатываемой системе используется большое количество всяких элементов, каждый из них может быть настроен по-разному и всё это может быть развёрнуто на разных стендах – изучение инструмента потребовалось полное и тщательное. По мере изучения инструмента стали очевидны и некоторые его минусы (в большинстве случаев «by-design») и ограничения (хорошая статья о философии Puppet, поясняющая некоторые архитектурные решения). Так же по мере изучения и уже частичной реализации необходимых скриптов чуть больше узнал про Ansible в котором решены некоторые проблемы, имеющиеся в Puppet (что не отменят возможности наличия своих проблем, отсутствующих в Puppet). Так, что последующее повествование это не реклама Puppet, а описание возможностей и опыта использования.

Немного о Puppet


Puppet использует свой собственный конфигурационный язык (DSL), который был разработан что бы быть доступным для системных администраторов. Язык Puppet позиционируется как не требующий формального знания программирования и его синтаксис был сформирован под влияние формата конфигурационных файлов Nagios.

Основная цель языка Puppet – определение ресурсов (ресурсы в Puppet – файлы, каталоги, службы, лимиты, брандмауэры, пользователи и т.д.). Все другие части языка существуют лишь для добавления гибкости и удобства в том, как определяются ресурсы.

Группы ресурсов могут быть организованы в классы, которые являются более крупными единицами конфигурации. В то время как ресурс может описывать единственный файл/каталог или пакет, класс может описывать всё, что нужно для конфигурирования службы или приложения (включая необходимое количество пакетов, конфигурационных файлов, демонов/служб и задач обслуживания). Более маленькие классы могут объединяться в крупные, которые описывают целую системную роль – «сервер базы данных» или «рабочий узел кластера».

Пример класса с ресурсами внутри:

class apache (String $version = 'latest') {
  package {'httpd':
    ensure => $version, # Using the class parameter from above
    before => File['/etc/httpd.conf'],
  }
  file {'/etc/httpd.conf':
    ensure  => file,
    owner   => 'httpd',
    content => template('apache/httpd.conf.erb'), # Template from a module
  }
  service {'httpd':
    ensure    => running,
    enable    => true,
    subscribe => File['/etc/httpd.conf'],
  }
}

Машины, которые выполняют разные роли должны, в общем случае, получить разный набор классов. Задача конфигурирования того, какие классы будут применены на какие машины – задача узлов Puppet.

Примеры определения узлов Puppet:

node 'www1.example.com', 'www2.example.com', 'www3.example.com' {
  include common
  include apache, squid
}

node /^(foo|bar)\.example\.com$/ {
  include common
}

Факты и база данных Hiera. Перед тем как выполнить код Puppet выполняется сборка информации об узле, собранная информация оформляется в виде предопределённых фактов – переменных, которые можно использовать где угодно в коде. Hiera это встроенная «key-value» база данных. По-умолчанию в качестве источника данных используются файлы формата YAML или JSON, хотя возможно расширение для использования любого источника данных. В связи с её иерархичностью и возможностью изменения данных в зависимости от узла – её использование является неотъемлемой частью работы большинства модулей/классов.

Модули – самодостаточные блоки кода и данных (классы, шаблоны, файлы, и т.д.). Эти повторно используемые, доступные для общего пользования элементы являются основными строительными блоками Puppet.

Тем, кто планирует использовать Puppet в основном придётся заниматься созданием классов и иногда — модулей.

Варианты развёртывания


При внедрении Puppet возможны 2 варианта с централизованным хранением конфигурации и без него:

  • Централизованное хранение конфигурации: преимущества явно прослеживаются при наличии БОЛЬШОГО количества серверов. В этом случае на клиентские машины передаётся информация, касающиеся только самой машины, что так же обеспечивает некоторый уровень безопасности и минимизирует траффик.
  • Децентрализованное хранение конфигурации: обосновано при небольшом количестве серверов, при этом на машин должен быть полный комплект конфигурационных скриптов и файлов и при запуске агентов будет выполнена их компиляция и выполнения части касающейся данной машины. Реализуется обычной cron-задачей, запускаемой каждые 15 минут.

Мой скрипт выглядит примерно так:

#!/bin/sh

PUPPET_BIN='/opt/puppetlabs/bin/puppet'

# Ставим необходимые пакеты для старта
apt-get update &&  apt-get -y install git mc htop apt-transport-https nano wget lsb-release apt-utils curl python

# Первоначально осуществляем установку `puppet-agent`
if [ ! -d /etc/puppetlabs ]; then
  	rm *.deb.* *.deb # possible trash
	wget https://apt.puppetlabs.com/puppetlabs-release-pc1-xenial.deb && dpkg -i puppetlabs-release-pc1-xenial.deb
	apt-get update && apt-get -y install puppet-agent
fi

# Определяем тип `environment`
/opt/puppetlabs/bin/puppet config set environment $PUPPET_ENV
if [ ! -d /etc/puppetlabs/code/environments/$PUPPET_ENV ]; then
  cp -r /etc/puppetlabs/code/environments/production /etc/puppetlabs/code/environments/$PUPPET_ENV
fi

# Install puppet modules
$PUPPET_BIN module install puppetlabs-ntp
$PUPPET_BIN module install aco-oracle_java
$PUPPET_BIN module install puppetlabs-firewall
$PUPPET_BIN module install saz-ssh
$PUPPET_BIN module install saz-sudo
$PUPPET_BIN module install saz-limits
$PUPPET_BIN module install thias-sysctl
$PUPPET_BIN module install yo61-logrotate
$PUPPET_BIN module install puppetlabs-apt
$PUPPET_BIN module install puppet-archive

# git pull "deployment" project and go in it only if POVISION_NO_GIT_CLONE set to "true"
if [ ${POVISION_NO_GIT_CLONE:-"false"} = "true" ];
then
	echo "do nothing"
else
	LOCAL_REV=""
	if [ -f local_latest.sha1 ]; then
	  LOCAL_REV=`cat local_latest.sha1`
	fi
	REMOTE_REV=`git ls-remote --tags | grep "latest" | awk '{print $1}'`
	if [ $LOCAL_REV = $REMOTE_REV ]; then
	 exit 0
	fi
	 git fetch --all --tags --prune
	 git checkout -f tags/latest
fi

# replace puppet configs
cp puppet_config/hiera.yaml  /etc/puppetlabs/code/environments/$PUPPET_ENV/

# replace hiera db
rm /etc/puppetlabs/code/environments/$PUPPET_ENV/hieradata/*
cp -r $PUPPET_ENV/hieradata/*  /etc/puppetlabs/code/environments/$PUPPET_ENV/hieradata

# replace storyline_* modules
rm -r /etc/puppetlabs/code/environments/$PUPPET_ENV/modules/storyline_*
cp -r modules/*  /etc/puppetlabs/code/environments/$PUPPET_ENV/modules

# copy site.pp
cp $PUPPET_ENV/site.pp /etc/puppetlabs/code/environments/$PUPPET_ENV/manifests/site.pp

#echo "hostname:"
#hostname
$PUPPET_BIN apply /etc/puppetlabs/code/environments/$PUPPET_ENV/manifests/site.pp

echo $REMOTE_REV > local_latest.sha1

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

При запуске указанного скрипта:

  1. устанавливается сам клиент Puppet и необходимые пакеты
  2. инсталлируются необходимые модули Puppet
  3. сверяется изменение номера коммита для метки «latest» (делается при успешном интеграционном тестировании новой версии)
  4. заменяется конфигурация (hiera.yaml) БД Hiera Puppet в текущем окружении (переменная $PUPPET_ENV);
  5. заменяются YAML-файлы с данными для БД Hiera Puppet;
  6. копируются с заменой описания моих модулей;
  7. копируется конфигурация с узлами (серверами моей системы);
  8. вызывается применение всех тех настроек, что были скопированы/установлены ($PUPPET_BIN apply ….)

Список задач, которые выполняет клиент Puppet при запуске огромен (проверка и необходимое выполнение руками была бы просто невозможна):

  • выставляются лимиты на открытые файлы, задействованный объем памяти, свапирование и количество соединений;
  • настраивается ротация логов (как для системы, так и для моего приложения и необходимых ему сервисов);
  • создаются необходимые учётные записи для администрирования с необходимыми группами и полномочиями;
  • устанавливается и настраивается NTP-сервер;
  • устанавливается и настраивается SSH-сервер;
  • устанавливается Oracle JDK;
  • настраивается брандмауэр;
  • устанавливается и настраивается большое количество компонентов, необходимых для функционирования проекта или его компонента на данном конкретном узле.


Примеры из жизни


При разработки системы скриптов Puppet для своего стенда я разрабатывал собственные модули, которые копируются на машину/контейнер, на которой выполнялся Puppet код. Модули содержат в себе данные (в большинстве конфигурационные файлы) и Puppet код для настройки. Основные настройки я выносил из скриптов в Hiera — как результат скрипты получались достаточно универсальными и не зависящими от узлов, на которых они выполняются.

Приведу несколько примеров кода и настроек.

Настройка ngnix (не из пакетов, а из родного репозитария) (спрятал в блок спойлер в связи с размерами. Но для заинтересованных — обязателен к просмотру Очень много нюансов видны при его изучении)
Класс nginx из модуля storyline_infra

class storyline_infra::nginx () {
	$params = lookup({"name" => "storyline_infra.nginx",
	    "merge" => {"strategy" => "deep"}})
	$reverse_port = $params['reverse_port']
	$reverse_url = $params['reverse_url']
	$pid_file = $params['pid_file']
	$init_script = $params['init_script']
	$dir_data = $params['dir_data']
	$dir_logs = $params['dir_logs']
	$version = $params['version']
	$enabled_startup = $params['enabled_startup']
	$enabled_running = $params['enabled_running']
	# topology_configuration
	$enabled_topology_configuration = $params['enabled_topology_configuration']
	$topology_configuration_port = $params['topology_configuration_port']

	# создать соотвествующего пользователя  (при необходимости)
	user { 'nginx':
		ensure => "present",
		managehome => true,
	}
	# создать необходимые каталоги  (при необходимости)
	exec { "nginx-mkdir":
		command => "/bin/mkdir -p /data/db && /bin/mkdir -p /data/logs",
		cwd => "/",
		unless => '/usr/bin/test -d /data/db -a -d /data/logs',
	} ->
	# working dir
	file { [ $dir_logs, $dir_data] :
		ensure => "directory",
		recurse => "true",
		owner => "nginx",
		group=> "nginx",
		require => Exec['nginx-mkdir'],
	}

	# добавить необходимые ключи в менеджер пакетов  (при необходимости)	
	# see by "gpg --verify keyfile"
	apt::key { 'nginx-key':
		id => '573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62',
		source  => 'http://nginx.org/keys/nginx_signing.key',
	} ->
	
	# добавить источник  (при необходимости)
	# deb http://nginx.org/packages/ubuntu/ xenial nginx
	apt::source { 'nginx-repo':
		comment  => 'nginx repo',
		location => "http://nginx.org/packages/ubuntu/",
		release => "xenial",
		repos    => "nginx",
		include  => {
	   		'deb' => true,
			'deb-src' => true,
		},
	} ->

	# осуществить установку пакета (при необходимости)
	package {  'nginx':
		ensure => $version,
		# notify => Exec['disable_nginx'],
	} →
	# выполнить копирование конфигурационного файла из данных модуля (при необходимости). При этом сам файл шаблона содержит подстановочные знаки, которые заменяются переменными класса. Крайне удобный функционал!!!
	file { "/etc/nginx/nginx.conf":
		replace => true,
		content => epp('storyline_infra/nginx.epp'),
		owner => "nginx",
		group=> "nginx",
		notify => Service['nginx'],
	}->
	# выполнить копирование конфигурационного файла из данных модуля (при необходимости)
	file { "/etc/nginx/conf.d/default.conf":
		replace => true,
		content => epp('storyline_infra/nginx_default.epp'),
		owner => "nginx",
		group=> "nginx",
		notify => Service['nginx'],
	}→
	# выполнить копирование конфигурационного файла из данных модуля (при необходимости)
	file { $init_script:
		replace => true,
		content => epp('storyline_infra/nginx_startup.epp'),
		mode=>"ug=rwx,o=r",
		notify => Service['nginx'],
	}→
	#  настроить службу/скрипты запуска (при необходимости)
	service { 'nginx':
  		ensure => $enabled_running,
		enable    => $enabled_startup,
		start 		=> "${init_script} start",
		stop 		=> "${init_script} stop",
		status 		=> "${init_script} status",
		restart 	=> "${init_script} restart",
		hasrestart => true,
		hasstatus => true,
	}
	#  дополнительные настройки nginx (при необходимости)
	if $enabled_topology_configuration {
		file { "/etc/nginx/conf.d/topology.conf":
			replace => true,
			content => epp('storyline_infra/nginx_topology.epp'),
			mode=>"ug=rwx,o=r",
			notify => Service['nginx'],
		}
	}
	#  отключение автозапуска встроенными средствами (при необходимости)
	if $enabled_startup != true {
		exec { "disable_nginx":
			require => Package['nginx'],
			command => "/bin/systemctl disable nginx",
			cwd => "/",
		}
	}

}


Как видите в каждом комментарии перед определением ресурса указано — «при необходимости». Puppet никогда не будет выполнять каких-либо операций если состояние ресурса уже соответствует его определению.

В данном случае видно как с помощью кода «$params = lookup({"name" => "storyline_infra.nginx", "merge" => {"strategy" => "deep"}})» получаются данные из Hiera (примеры её данных я приведу позже), которые в дальнейшем используются для заполнения всех переменных.

Конфигурационный файл Hiera:
---
version: 5
defaults:
  datadir: "hieradata"
  data_hash: yaml_data

hierarchy:
  - name: "1"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "2"
    path: "version.yaml"
  - name: "3"
    path: "common.yaml"

В данном случае видна иерархия (ключ «hierarchy») источник, где каждый источник на более высоком уровне переопределяет значения ключей на боле низком. Это позволяет и иметь ключ, например, «www.server.port» со значением «80» в «common.yaml» и со значением «81» в «nodes/webserver1.yaml» — в итоге мы получим значение данного ключа при выполнении Puppet кода: «81» на узле с именем «webserver1» и «80» на всех остальных.

Hiera's common.yaml
---
limits::entries:
  '*/nofile':
    both: 1048576
  '*/memlock':
    both: unlimited
logrotate::config:
    su_user: root
    su_group: syslog
    compress: true
# sysctl
sysctl::base::purge: false
sysctl::base::values:
  net.core.somaxconn:
    value: '65536'
  vm.swappiness:
    ensure: absent
  fs.file-max:
    value: '500000'
  vm.max_map_count:
    value: '262144'
storyline_base:
  oracle_java:
    version: "8u92"
storyline_infra:
  collectd:
    server_address: "XXX.nlp-project.ru"
    pid_file: '/data/logs/collectd/collectd.pid'
    init_script: '/etc/init.d/collectd'
    dir_data:  '/data/db/collectd'
    dir_logs:  '/data/logs/collectd'
    version: "1.2.0-1"
    enabled_mongodb: false
    mongodb_user: "collectd"
    mongodb_password: "######"
    enabled_storm: false
    enabled_elasticsearch: false
    elasticsearch_port: "####"
    elasticsearch_cluster: "elastic_storyline"
    enabled_startup: false
    enabled_running: true
  influxdb:
    port_http: "####"
    port_rpc: "####"
    pid_file: '/data/logs/influxdb/influxdb.pid'
    init_script: '/etc/init.d/influxdb'
    dir_data:  '/data/db/influxdb'
    dir_logs:  '/data/logs/influxdb'
    version: "present"
    enabled_auth: true
    enabled_startup: false
    enabled_running: true
….


site.pp (файл с определением Puppet узлов)

node "XXX.nlp-project.ru"  {
	include ::limits

	include ::sysctl::base
	include ::logrotate
	include storyline_base::ntp
	include storyline_base::srv_oper
	include storyline_base::ssh
	include storyline_base::oracle_java
…..
	include storyline_infra::monit
	include storyline_base::firewall
}

node "YYYY.nlp-project.ru"  {
	include ::limits
	include ::sysctl::base
	include ::logrotate

	include storyline_base::ntp
	include storyline_base::srv_oper
	include storyline_base::ssh
	include storyline_base::oracle_java
….
	include storyline_infra::zookeeper
	include storyline_components::server_storm

	include storyline_infra::monit
	include storyline_base::firewall
}

Если кого-то заинтересует конкретная реализация какой-либо из задач: пишите – отпишу реализацию в комменте или добавлю в «Tips».

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

Tips


  • При разработке модуля не забывать писать код не только для добавления функции, но и для её отключения. При отсутствии такого функционала при переносе компонента на другой сервер у вас их будет 2: на новом и на старом месте — на старом потребуется удалять руками, что противоречит основной задаче по автоматизации управления конфигурациями;
  • Хорошие книги по Puppet для начинающих – Learning Puppet и Puppet 4 Essentials;
  • Хороший модуль для получения артефактов из nexus sonatype (https://github.com/cescoffier/puppet-nexus);
  • Максимальное количество параметров выносите в файлы-данные Hiera для легкости конфигурации узлов и достижения универсальности кода самих модулей.

Спасибо за внимание!
Tags:
Hubs:
+4
Comments 0
Comments Leave a comment

Articles