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


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

    Если вам известны способы передачи метаданных для других облачных платформ и известны другие способы управления настройкой виртуальной машины при старте, давайте обсуждать в комментариях.
    EPAM
    154,38
    Компания для карьерного и профессионального роста
    Поделиться публикацией

    Комментарии 3

      +1
      Для убунты и дебиан (с оговорками) на клиентской стороне есть пакет CloudInit. Хавает метаданные из EC2 подобных сервисов и выполняет некоторые операции, вроде ресайза рутового раздела и т.п.
        +1
        CloudInit +1

        у меня в EC2 user-data вот такого типа. работает одинаково хорошо в EC2, RackSpace и CloudStack.
        #cloud-config
        chef:
         install_type: "omnibus"
         server_url: "https://my.chef.server:4000"
         environment: "stage"
         validation_name: "chef-validator"
         validation_key: |
             -----BEGIN RSA PRIVATE KEY-----
             CHEF-VALIDATION-KEY-HERE
             -----END RSA PRIVATE KEY-----
         run_list:
            - "role[db]"
         initial_attributes:
            percona:
              server:
                query_cache_size: "32M"     
        

        При первом старте инстанса, CloudInit сам устанавливает Chef-клиент, создает /etc/chef/client.rb, регистрируется на chef-сервере, устанавливает некоторьіе атрибутьі нодьі и вьіполняет указанньій run_list.

        Обратите внимание на install_type. Патч для «omnibus»-установки я закоммитил в декабре, скорее всего в вашьіх AMI (CloudInit-0.6.*) он отсутствует но в транке проекта он есть ;).
          0
          спасибо за информацию!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое