Pull to refresh
EPAM
Компания для карьерного и профессионального роста

Автоматическое конфигурирование виртуальных машин в облаках при помощи метаданных

Reading time 11 min
Views 9.8K

In God we trust, the rest we automate
— unknown DevOps Engineer

Использование виртуализации и облачных платформ позволяет в десятки раз сократить время, затрачиваемое на запуск и обслуживание IT инфраструктуры. Один человек может манипулировать десятками, сотнями и даже тысячами виртуальных серверов, с легкостью их запускать, останавливать, клонировать, изменять конфигурацию оборудования и создавать на их основе готовые образы систем. Если все ваши сервера имеют одинаковую конфигурацию, то особых проблем нет, можно один раз вручную настроить сервер, сделать на его основе образ и запускать столько машин, сколько вам необходимо. Если же у вас большое количество разных операционных систем с разным набором программного обеспечения или если вам необходимо быстро запускать и останавливать сложные кластерные конфигурации, то обслуживание даже нескольких десятков таких серверов будет занимать очень много времени. Можно, конечно иметь набор разных скриптов и образов на все случаи жизни, которые необходимо будет сопровождать и обновлять, но более рационально использовать один скрипт и несколько образов, а все необходимые параметры передавать при старте системы. Многие платформы для облачных вычислений предлагают, так называемый, механизм метаданных (metadata) или пользовательских данных (user-data), используя этот механизм, вы можете передать скрипту все необходимые данные по настройке конкретной виртуальной машины или даже передать сам скрипт, чтобы он выполнился при старте.



В той или иной мере в данной статье будут рассмотрены следующие облачные платформы:
  • Amazon EC2
  • Eucalyptus
  • Nimbula Director
  • VMWare vCloud Director


1. Обзор принципа работы пользовательских данных для разных платформ и примеры их использование через CLI или простые скрипты

1.1 Amazon EC2

В Amzon параметр user-data может быть задан в свободной форме, при запуске виртуальной машины и потом его можно получить по определенной ссылке:

curl 169.254.169.254/latest/user-data

IP адрес 169.254.169.254 является виртуальным и все запросы к нему перенаправляются на внутренний API EC2 сервиса в соответствии с IP адресом источника.

Стандартные образы систем, предоставляемые Amazon, имеют встроенную возможность выполнять Bash и Power Shell скрипты переданные через user-data. Если user-data начинается shebang (#!), то система попытается выполнить скрипт, используя указанный в нем интерпретатор. Изначально такая возможность была реализована в отдельном пакете cloud init для Ubuntu, но сейчас он входит во все стандартные образы систем, включая Windows.

Для Windows систем можно указать, как выполнение обычных консольных команд,

<script>
     netsh advfirewall set allprofiles state off
</script>


так и код на Power Shell:

<powershell>
       $source = "http://www.example.com/myserverconfig.xml"
       $destination = "c:\myapp\myserverconfig.xml"
       $wc = New-Object System.Net.WebClient
       $wc.DownloadFile($source, $destination)
</powershell>


Эту функциональность можно использовать вместе с шаблонами Cloud Formation и запускать целые стеки серверов, указав необходимые user-data:

{
 "AWSTemplateFormatVersion" : "2010-09-09",
 "Parameters" : {
     "AvailabilityZone" : {
      "Description" : "Name of an availability zone to create instance",
      "Default" : "us-east-1c",
      "Type" : "String"
    },
    "KeyName" : {
      "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instance",
      "Default" : "test",
      "Type" : "String"
    },
    "InstanceSecurityGroup" : {
      "Description" : "Name of an existing security group",
      "Default" : "default",
      "Type" : "String"
    }
  },

  "Resources" : {
    "autoconftest" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
      "AvailabilityZone" : { "Ref" : "AvailabilityZone" },
      "KeyName" : { "Ref" : "KeyName" },
      "SecurityGroups" : [{ "Ref" : "InstanceSecurityGroup" }],
      "ImageId" : "ami-31308xxx",
      "InstanceType" : "t1.micro",
      "UserData" : { "Fn::Base64" : { "Fn::Join" : ["",[
            "#!/bin/bash","\n",
            "instanceTag=WebServer","\n",
            "confDir=/etc/myconfig","\n",
            "mkdir $confDir","\n",
            "touch $confDir/$instanceTag","\n",
            "IPADDR=$(ifconfig eth0 | grep inet | awk '{print $2}' | cut -d ':' -f 2)","\n",
            "echo $IPADDR myhostname","\n",
            "hostname myhostname","\n" ]]
      }
  }
}


Если же запуск скрипта при старте машины вам не подходит, к примеру, вы хотите чтобы вашими образами пользовались другие люди, и они не хотят разбираться в вашем коде, то можно свой скрипт установить в систему, добавить в автозагрузку, и создать образ системы. И предоставить пользователям образа описание возможных параметров, которые можно задать в user-data. Например, список параметров ключ=значение разделенных точкой с запятой:

graylogserver=«192.168.1.1»;chefnodename=«chef_node_name1»;chefattributes=«recipe1.attribute1=value1,recipe1.attribute2=value2,customparameter1=value1»;chefserver=«192.168.1.38:4000»;chefrole=«apache,mysql,php»;

получать всю строку на Bash можно так:
function get_userdata {
	user_data=$(curl -w "%{http_code}" -s http://169.254.169.254/latest/user-data)
	result_code=${user_data:(-3)}
	if [ -z "$user_data" ] || [ $result_code != "200" ]
	then
		echo "$CurrentDate: Couldn't receive user-data. Result code: $result_code"
		return 1
	else
		export user_data=${user_data%%$result_code}
		return 0
	fi
}


и затем, из полученного списка получить нужное значение:
function get_userdata_value {
	IFS=';'
	for user_data_list in $user_data
	do
		user_data_name=${user_data_list%%=*}
		if [ $user_data_name = $1 ]
		then
			user_data_value=${user_data_list#*=} 
			user_data_value=$(echo $user_data_value | tr -d '\"')
			return 0
		fi
	done
	return 1
}


После этого можно продолжить настройку системы в соответствии с полученными данными. Необязательно хранить все скрипты внутри образа, достаточно иметь простой стартовый скрипт, который считывает user-data и затем скачивает и запускает всё необходимое или же передает управление Chef или Puppet.

Аналогичную функциональность можно реализовать на Power Shell.

1.2 Eucaliptus
Этот продукт совместим с Amazon AWS, и механизм user-data в нем реализован так же.

1.3 Nimbula
Этот продукт относительно молодой, но быстроразвивающийся, он предназначен для создания приватных облачных систем и использует KVM виртуализацию. Его основатели выходцы из Amazon и у них заявлена совместимость с Amazon, но, несмотря на это, совместимость не полная. У них есть поддержка механизма user-data, через виртуальный IP, но задаются они в виде ключ=значение.
Список всех ключей можно получить по ссылке:
192.0.0.192/latest/attributes или 169.254.169.254/latest/attributes

пример:
curl 169.254.169.254/latest/attributes
nimbula_compressed_size
nimbula_decompressed_size
chefserver
hostname


Получить значение конктерного ключа:
curl 169.254.169.254/latest/attributes/chefserver
192.168.1.45:4000


Таким образом, передать целый скрипт для выполнения через user-data нельзя, необходимо создавать свой образ системы со встроенным стартовым скриптом.

Пример кода на Bash:
function get_value {
   user_data_value=$(curl curl -w "%{http_code}" -s http://169.254.169.254/latest/attributes/"$1")
   result_code=${user_data_value:(-3)}
   if [ -z "$user_data_value" ] || [ $result_code != "200" ]
   then
        echo "$CurrentDate: $1 variable is not set, skip it, return code: $result_code" >> $LogFile
        return 1
   else
        user_data_value=${user_data_value%%$result_code}
       return 0
   fi
}


1.4 VMWare vCloud Director
Начиная с версии 1.5 в vCloud Director появился механизм использование метаданных в рамках vApp (контейнера для виртуальных машин). Данные задаются в формате ключ=значение. Чтобы задать метаданные, необходимо создать XML с их описанием:

<Metadata xmlns="http://www.vmware.com/vcloud/v1.5">
    <MetadataEntry>
              <Key>app-owner</Key>
              <Value>Foo Bar</Value>
    </MetadataEntry>
    <MetadataEntry>
              <Key>app-owner-contact</Key>
              <Value>415-123-4567</Value>
    </MetadataEntry>
    <MetadataEntry>
              <Key>system-owner</Key>
              <Value>John Doe</Value>
    </MetadataEntry>
</Metadata>


И, затем, выполнить POST запрос по URL для соответствующего vApp:
$ curl -i -k -H «Accept:application/*+xml;version=1.5» -H «x-vcloud-authorization: jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -H «Content-Type:application/vnd.vmware.vcloud.metadata+xml» -X POST 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata -d @metadata-request.

Прочитать все метаданные можно GET запросом:
$ curl -i -k -H «application/*+xml;version=1.5» -H «x-vcloud-authorization: jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -X GET 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata.

Для того чтобы прочитать значение конкретного ключа, запрос должен быть вида:
$ curl -i -k -H «application/*+xml;version=1.5» -H «x-vcloud-authorization: jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -X GET 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata/asset-tag

Ответ выдается в виде XML.

Подробнее о работе метаданных в vCloud можно узнать здесь: blogs vmware

2. Работа с пользовательскими данными при помощи систем управления, таких как: Chef и Puppet

2.1 Chef
Выбор того, как Chef клиент попадает на машину за вами, вы можете его устанавливать вручную и затем создавать свои образы систем, либо вы можете его устанавливать автоматически при старте системы. Оба способа имеют свои преимущества и недостатки: первый способ уменьшает время затрачиваемое на конфигурацию машины во время старта, второй способ дает вам возможность всегда устанавливать актуальную версию клиента или устанавливать строго необходимую вам версию клиента, в зависимости от потребностей. В любом случае мы должны передать список ролей и рецептов, которые должны быть выполнены на машине, этот список мы можем получить через used-data и сконфигурировать Chef клиент при старте системы. Так же в случае, если мы устанавливаем и конфигурируем клиент при старте системы, нам необходимо скачать ключ validation.pem для соответствующего Chef сервера (данные о котором тоже можно передать через user-data)

Пример Bash скрипта, который получает список ролей:

rolefile="/etc/chef/role.json"

function get_role {
    get_value "chefrole"
    if [ $? = 0 ]
    then
		chefrole=$user_data_value
    else
		echo "$CurrentDate: Couldn't get any Chef role, use base role only."
		chefrole="base"
    fi
	commas_string=${chefrole//[!,]/}
	commas_count=${#commas_string}
	echo '{ "run_list": [ ' > $rolefile
	IFS=","
	for line in $ep_chefrole
	do
		if [ $commas_count = 0 ]
		then
		echo "\"role[$line]\" " >> $rolefile
		else
		echo "\"role[$line]\", " >> $rolefile
		fi
		commas_count=$(($commas_count-1))
	done
	echo ' ] }' >> $rolefile
} 


А затем создает конфигурационный файл для клиента:
function set_chef {
    if [ -d $chef_dir ] && [ -e $chef_bin ]
    then
		service $chef_service stop
		sleep 10
		echo -e "chef_server_url \"http://$1\"" > $chef_dir/client.rb
		echo -e "log_location \"$chef_log\"" >> $chef_dir/client.rb
		echo -e "json_attribs \"$rolefile\"" >> $chef_dir/client.rb
		echo -e "interval $chef_interval" >> $chef_dir/client.rb
		echo "$CurrentDate: Writing $chef_dir/client.rb"
		service $chef_service start
    else
		echo "$CurrentDate: Chef directory $chef_dir or chef binary $chef_bin does not exist. Exit."
		exit 1
    fi
} 


Параметр json_attributes задает путь к JSON файлу со списком ролей и рецептов.

После того, как мы передали управление Chef клиенту, он зарегистрируется на сервере, скачает список рецептов и начнет их выполнение, но есть несколько нюансов:
  • выполнение некоторых рецептов может занять много времени, нам необходимо знать когда конфигурация системы закончится и как она закончилась успешно или нет
  • что если мы не хотим выполнять рецепты с атрибутами по умолчанию, а хотим изменить какие-то атрибуты, например, хотим установить LAMP, но так, чтобы Apache работал на порту 8080, а не 80


Для решения первой проблемы существует cookbook от Opscode, который называется chef_handler. Он предоставляет механизм, называемый Exception and Report Handlers, который вызывается после того как Chef клиент закончил выполнение рецептов. Используя этот cookbook мы можем проверять результат последнего выполнения клиента и выполнять какие-либо действия. Можно отправлять письмо о результатах выполнения (пример описанный в документации Opscode) или записывать результат выполнения на Chef сервер, чтобы проверять это значение своими приложениями и отображать статус выполнения.

Пример рецепта:

устанавливаем значения атрибутов по умолчанию
default['lastrun']['state'] = "unknown"
default['lastrun']['backtrace'] = "none"


указываем, что нужно выполнять
include_recipe "chef_handler"
chef_handler "NodeReportHandler::LastRun" do
    source "#{node.chef_handler.handler_path}/nodereport.rb"
    action :nothing
end.run_action(:enable) 


то, что выполняем
module NodeReportHandler
    class LastRun < Chef::Handler
	def report
	    if success? then
		node.override[:lastrun][:state] = "successful"
		node.override[:lastrun][:backtrace] = "none"
	    else
		node.override[:lastrun][:state] = "failed"
		node.override[:lastrun][:backtrace] = "#{run_status.formatted_exception}"
	    end
	node.save
      end
    end
end


В результате, у нас изначально задается значения атрибутов lastrun.state и lastrun.backtrace как 'unknown' и 'none' и затем, по резльтутам завершения выполнения клиента, мы получим либо запись 'successfull' либо 'failed' с описанием ошибки в lastrun.backtrace.
Этот рецепт должен быть в списке выполнения первым, чтобы охватывать ошибки при выполнении любых рецептов.

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

Рецепт для получения user-data, на примере Amazon:
получаем целую строку
# Get whole user-data string
    def GetUserData(url)
      uri = URI.parse(url)
      http = Net::HTTP.new(uri.host, uri.port)
      http.open_timeout = 5
      http.read_timeout = 5
      proto = url.split(":", 2)
      if proto[0] == "https"
	    http.use_ssl = true
	    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
      end
      request = Net::HTTP::Get.new(uri.request_uri)
	begin
            http.open_timeout = 5
            http.read_timeout = 5
            request = Net::HTTP::Get.new(uri.request_uri)
            response = http.request(request)
            result = response.body.to_s
	    if response.is_a?(Net::HTTPSuccess)
		Chef::Log.info("Successfuly get user-data.")
		return result
	    else
		return false
	    end
	rescue Exception => e
	    Chef::Log.info("HTTP request failed.")
	    return false
	end
    end


получаем значение конкретного параметра
# Get specified user-data value
    def GetValue(user_data,attribute)
             user_data.split(";").each do |i|
	attribute_name=i.split("=", 2)
	if attribute_name[0] == attribute
		return attribute_name[1].strip
	end
	end
	return false
   end


Теперь, когда мы можем получить значение для конкретного параметра из переданных данных, мы можем их задать:
сhefnodename=«chef_node_name1»;chefattributes=«recipe1.attribute1=value1,recipe1.attribute2=value2,customparameter1=value1»;chefserver=«192.168.1.38:4000»;chefrole=«apache,mysql,php»

В параметре chefattributes мы передали список атрибутов, которые мы хотим изменить, задаются они в формате «cookbookname.attributename=value». Если мы хотим поменять порт по умолчанию для Apache нам нужно задать chefattributes=apache.port=8080.

Рецепт, который считывает это значение и сохраняет его:
chefattributes = GetValue("#{node[:user_data]}","chefattributes")
if chefattributes != false
       сhefattributes.split(",").each do |i|
       attribute_name=i.split("=")
       recipe_name=attribute_name[0].split(".", 2)
       node.override[:"#{recipe_name[0]}"][:"#{recipe_name[1].strip}"]="#{attribute_name[1].strip}"
      Chef::Log.info("Save node attributes.")
      node.save
else
    Chef::Log.info("Couldn't get Chef attributes. Skip.")
end


Этот рецепт нужно выполнять перед выполнением остальных рецептов.

Недостатки описанных выше рецептов. Операция node.save отправляет на сервер для сохранения весь JSON массив для конкретной ноды, включая информацию собранную Ohai. Если у вас тысячи машин и все они постоянно будут пытаться перезаписать свои атрибуты на сервере, это может плохо сказаться на его производительности. Это же относится к использованию гибкого и мощного поиска, предоставляемого Chef, операция поиска очень трудоемкая и в случае обслуживания тысяч машин, это создаст большую нагрузку на сервер. В таком случае нужно использовать другие способы, которые здесь описываться не будут.

2.2 Puppet
Использование Puppet для получения user-data аналогично использованию Chef. Адрес Puppet сервера и другие необходимые данные для конфигурирования агента мы получаем при помощи стартового скрипта. Для передачи своих фактов на сервер удобно использовать дополнение Facter.

Вот пример скрипта на ruby, который получает из user-data необходимые данные и отправляет их на сервер в виде дополнительных фактов, для данной машины:

require 'facter'
 
user_data = `curl http://169.254.169.254/latest/user-data`
user_data = user_data.split(";")
 
user_data.each do |line|
    user_data_key_value = line.split('=', 2)
    user_data_key = user_data_key_value[0]
    user_data_value = user_data_key_value[1]
    Facter.add(user_data_key) do
        setcode { user_data_value }
    end
end
 
instance_id = `curl http://169.254.169.254/latest/meta-data/instance-id`
Facter.add('instance-id') do
    setcode { instance_id }
end


Конечно, может показаться, что это все просто, и незачем городить какие-то сложные схемы, можно создать необходимый набор образов с предустановленным софтом, а все изменения выполнять своими скриптами, которые ходят по SSH и вонсят изменения в конфигурационные файлы. В данной статье описаны элементарные шаги. Если нам необходимо запустить кластер Hadoop, MySQL или кластер из Front-End, Back-End, App, DB серверов, чтобы все машины автоматически сконфигурировались и чтобы можно было динамически удалять или добавлять произвольное количество машин в кластер при автоматическом масштабировании, без описанных выше приемов не обойтись.

Если вам известны способы передачи метаданных для других облачных платформ и известны другие способы управления настройкой виртуальной машины при старте, давайте обсуждать в комментариях.
Tags:
Hubs:
+5
Comments 3
Comments Comments 3

Articles

Information

Website
www.epam.com
Registered
Founded
1993
Employees
over 10,000 employees
Location
США
Representative
vesyolkinaolga