Статьи

Неизменяемые серверы с упаковщиком и марионеткой

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

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

Узлы

В нашем инфраструктурном проекте у нас есть node.yaml, который определяет имена узлов и группы безопасности AWS, к которым они принадлежат. Это довольно просто и используется для множества других инструментов (например,  vagrant ).

elasticsearch: 
  group: logging
 
zookeeper:
  group: zookeeper
 
redis:
  group: redis
  size: m2.2xlarge

Рейк-файл

Мы используем этот файл node.yaml вместе с rake для создания шаблонов упаковщиков для создания новых AMI. Это избавляет меня от необходимости управлять кучей шаблонов упаковщика, поскольку они в основном имеют одинаковые функции.

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, который просто вводит в него имя узла.

{
  "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).

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и бум! Сервер создан, настроен, отображен и завершен. Теперь просто создайте несколько заданий в jenkins, чтобы генерировать их на основе определенных триггеров, и вы на шаг ближе к автоматизации вашей неизменной инфраструктуры.

Убираться

Конечно, каждый генерируемый вами AMI будет стоить вам копейки в день или что-то подобное. Это может показаться небольшим, но как только у вас будет 100 ревизий каждой AMI, это будет стоить вам! Так как последний шаг я взбитый простой  fabfile  скрипт для очистки старых изображений. Это оказалось простой задачей, потому что мы включили метку времени Unix в имя AMI.

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, без полной перестройки сервера?