В последнее время я все больше и больше поклоняюсь концепции неизменных серверов при автоматизации нашей инфраструктуры в Zapier . Концепция проста: никогда не обновляйте и не изменяйте серверы на живых серверах, вместо этого просто создавайте новые серверы с примененными обновлениями и выбрасывайте старые. В основном вы получаете все преимущества неизменности в программировании на уровне инфраструктуры, и вам никогда не придется беспокоиться о смещении конфигурации. И что еще лучше, мне больше не нужно бояться, что, несмотря на обширные тесты, кто-то может протолкнуть изменение манифеста в виде марионетки, из-за которого все наши веб-серверы ломаются (уверен, что мы можем откатить изменения и восстановить, но потенциальный сбой все еще существует) беспокоиться о).
Очевидно, вам нужен хороший инструмент, чтобы это произошло. Некоторое недавнее дурачение с упаковщиком позволило мне собрать настройку, которой я до сих пор был немного доволен.
Узлы
В нашем инфраструктурном проекте у нас есть node.yaml, который определяет имена узлов и группы безопасности AWS, к которым они принадлежат. Это довольно просто и используется для множества других инструментов (например, vagrant ).
1
2
3
4
5
6
7
8
9
|
elasticsearch: group: logging zookeeper: group: zookeeper redis: group: redis size: m2.2xlarge |
Рейк-файл
Мы используем этот файл node.yaml вместе с rake для создания шаблонов упаковщиков для создания новых AMI. Это избавляет меня от необходимости управлять кучей шаблонов упаковщика, поскольку они в основном имеют одинаковые функции.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
require 'erb' require 'yaml' namespace :packer do task :generate do current_dir = File.dirname(__FILE__) nodes = YAML.load_file( "#{current_dir}/nodes.yml" ) nodes.each_key do |node_name| include ERB::Util template = File.read( "#{current_dir}/packs/template.json.erb" ) erb = ERB. new (template) File.open( "#{current_dir}/packs/#{node_name}.json" , "w" ) do |f| f.write(erb.result(binding)) end end end end |
Это используется в сочетании с простым шаблоном erb, который просто вводит в него имя узла.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
{ "builders" : [{ "type" : "amazon-ebs" , "region" : "us-east-1" , "source_ami" : "ami-10314d79" , "instance_type" : "t1.micro" , "ssh_username" : "ubuntu" , "ami_name" : "<%= node_name %> {{.CreateTime}}" , "security_group_id" : "packer" }], "provisioners" : [{ "type" : "shell" , "script" : "packs/install_puppet.sh" }, { "type" : "shell" , "inline" : [ "sudo apt-get upgrade -y" , "sudo sed -i /etc/puppet/puppet.conf -e \"s/nodename/<%= node_name %>-$(hostname)/\"" , "sudo puppet agent --test || true" ] }] |
Это создаст шаблон упаковщика для каждого узла, который будет
- Создать AMI в США-Восток-1
- Использует Ubuntu Server 13.04 AMI для начала
- Устанавливает группу безопасности для упаковщика в EC2. Мы создаем это и разрешаем доступ к группе безопасности puppetmaster. В противном случае упаковщик создаст случайную временную группу безопасности, которая не будет иметь доступа ни к каким другим группам (если вы будете следовать, по крайней мере, рекомендациям)!
- устанавливает куклу
- Запускает puppet один раз для настройки системы
Мы также никогда не включаем puppet agent (по умолчанию он не запускается), поэтому он никогда не запрашивает обновления. Мы также можем удалить куколку с сервера после ее завершения, чтобы AMI не запекла ее.
Сценарий
У Packer есть приятная функция, позволяющая пользователю задавать команды оболочки и файлы оболочки для запуска. Это хорошо для начальной загрузки, но не так хорошо для управления уровнем конфигурации, для которого больше подходит puppet. Поэтому наши шаблоны упаковщиков вызывают скрипт оболочки, который гарантирует, что мы не используем старую версию старых дистрибутивов ruby linux, по умолчанию устанавливаем и устанавливаем puppet. В рамках установки также указывается имя главного сервера puppet (если вы используете VPC вместо EC2 classic, вам это не нужно, поскольку вы можете просто назначить внутренний dns «puppet» puppetmaster).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
sleep 30 , wget http: //apt.puppetlabs.com/puppetlabs-release-raring.deb sudo dpkg -i puppetlabs-release-precise.deb sudo apt-get update sudo apt-get remove ruby1. 8 -y sudo apt-get install ruby1. 9.3 puppet -y sudo su -c 'echo "" "[main] logdir=/var/log/puppet vardir=/var/lib/puppet ssldir=/var/lib/puppet/ssl rundir=/var/run/puppet factpath=$vardir/lib/facter templatedir=$confdir/templates [agent] server = ip- 10 -xxx-xx-xx.ec2.internal report = true certname=nodename "" " >> /etc/puppet/puppet.conf' |
Строим Это
Теперь все, что нам нужно сделать, чтобы создать новый AMI для redis, это запустить packer build packs/redis.json
и boom! Сервер создан, настроен, отображен и завершен. Теперь просто создайте несколько заданий в jenkins, чтобы генерировать их на основе определенных триггеров, и вы на шаг ближе к автоматизации вашей неизменной инфраструктуры.
Убираться
Конечно, каждый генерируемый вами AMI будет стоить вам копейки в день или что-то подобное. Это может показаться небольшим, но как только у вас будет 100 ревизий каждой AMI, это будет стоить вам! Итак, в качестве последнего шага я создал простой скрипт для очистки старых изображений. Это оказалось простой задачей, потому что мы включили метку времени Unix в имя AMI.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import os import boto from fabric.api import task class Images(object): def __init__(self, **kwargs): self.conn = boto.connect_ec2(**kwargs) def get_ami_for_name(self, name): (keys, AMIs) = self.get_amis_sorted_by_date(name) return AMIs[ 0 ] def get_amis_sorted_by_date(self, name): amis = self.conn.get_all_images(filters={ 'name' : '{}*' .format(name)}) AMIs = {} for ami in amis: (name, creation_date) = ami.name.split( ' ' ) AMIs[creation_date] = ami # remove old ones! keys = AMIs.keys() keys.sort() keys.reverse() return (keys, AMIs) def remove_old_images(self, name): (keys, AMIs) = self.get_amis_sorted_by_date(name) while len(keys) > 1 : key = keys.pop() print( "deregistering {}" .format(key)) AMIs[key].deregister(delete_snapshot=True) @task def cleanup_old_amis(name): '' ' Usage: cleanup_old_amis:name={{ami-name}} '' ' images = Images( aws_access_key_id=os.environ[ 'AWS_ACCESS_KEY_ID' ], aws_secret_access_key=os.environ[ 'AWS_SECRET_ACCESS_KEY' ] ) images.remove_old_images(name) |
Установите это как задание после сборки на задание jenkins, которое генерирует AMI, и вы всегда будете уверены, что у вас есть только самое последнее. Вы также можете настроить это так, чтобы последние 5 AMI оставались рядом для целей архивирования.
Что дальше?
Я признаю, что все еще немного свеж с этой концепцией. В идеале я был бы очень рад довести нашу инфраструктуру до точки, где каждый месяц (или неделя!) Серверы перерабатываются с новыми копиями. Серверы, которые являются более быстрыми, как веб-серверы или очереди, это просто. С хранилищами данных это может быть немного сложнее, так как вам нужна эффективная стратегия для загрузки реплик первичных экземпляров, продвижения реплик к основным и удаления старых праймериз.
Последняя проблема — решить, какой уровень изменчивости разрешен. Развертывания, очевидно, хороши, поскольку они не изменяют конфигурацию сервера, но как насчет добавления / удаления пользователей? Используем ли мы подход «все или ничего» или разрешаем обновлять мелкие детали, такие как открытые ключи SSH, без полной перестройки сервера?