Здравствуй, читатель. Вот и подоспела третья часть моей статьи, которая подытожит цикл (часть 1 и часть 2) статей для начинающих. Эта часть будет посвящена конкретному примеру применения Chef в облаке Amazon. Как я уже упоминал – это достаточно популярный сценарий. Для простоты понимания, будет рассмотрен случай с двумя ec2-instance (виртуальные сервера Amazon), один из которых будет выполнять роль Chef-сервера, а второй – узла.
AWS и Chef
Сразу же уточню, что запускать instance мы будем используя AWS CloudFormation. Можно было бы, конечно, запустить и управлять ними вручную, но какой смысл в такой автоматизации?
CloudFormation можно разделить на 2 понятия:
— template, представляющий собой json-файл, в котором описываются все ресурсы, которые необходимы нам для запуска instance;
— stack, представляющий собой сами ресурсы AWS, описанные в template.
Для тех, кто начинает знакомство с AWS, Amazon предоставляет уже готовые sample template, которые освещают большинство аспектов, необходимых при работе с AWS. Ссылка на шаблоны будет приведена в конце статьи.
Рассмотрим, что собой представляет template. В базовом случае он состоит из 4 блоков: Parameters, Mappings, Resources, Outputs.
Блок Parameters описывает переменные и их значения, которые будут переданы в stack во время создания оного. Значения параметров можно вводить при создании ресурсов, а можно задавать при помощи поля default в описании параметра. Параметром может быть любая информация, начиная от пароля и кончая сетевым портом или путем к директории. Для получения значения параметра, в шаблоне используется функция Ref.
Блок Mappings содержит в себе набор ключей с соответствующими параметрами и их значениями. Чаще всего можно видеть, как mapping-и используются для определения регионов AWS и соответствующих им виртуальным образам (instance). Для получения значения того или иного mapping – используется функция Fn::FindInMap, в которой указывается ключ и параметры, по которым производится поиск того или иного значения.
Блок Resources описывает наши ec2-instance либо другие ресурсы AWS. Именно в этом разделе объявляются образы для Chef-сервера и клиентского узла. В описании необходимо указать тип ресурса (например, AWS::EC2::Instance), также возможно указать metadata, в которой можно указать описание нашего узла или директивы по pre-install процедуре (например, если необходимо установить какой-либо пакет при запуске образа). Основной частью блока является блок Properties, в котором детально описывается запускаемый образ. В этом блоке можно задать тип образа, который будет запущен (например, Amazon Linux 32-bit), принадлежность запускаемого образа к той или иной Security Group (по сути, это firewall, с определяемыми правилами для трафика, в котором действие по умолчанию — deny). Однако, самой главной частью блока Properties – является User Data. Именно тут мы опишем скрипт, который будет превращать наш безликий instance в Chef-сервер или Chef-клиент.
Template, который я использую, приведен ниже под катом, рассмотрим его и я прокомментирую его.
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template for stack",
"Parameters" : {
"KeyName" : {
"Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
"Type" : "String",
"MinLength" : "1",
"MaxLength" : "255",
"AllowedPattern" : "[\x20-\x7E]*",
"ConstraintDescription" : "can contain only ASCII characters."
},
"HostKeys" : {
"Description" : "Public Key",
"Type" : "String"
},
"SecretAccessKey" : {
"Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
"Type" : "String"
},
"InstanceType" : {
"Description" : "Chef Server EC2 instance type",
"Type" : "String",
"Default" : "m1.small",
"AllowedValues" : [ "t1.micro","m1.small"],
"ConstraintDescription" : "must be a valid EC2 instance type."
},
"SSHLocation" : {
"Description" : " The IP address range that can be used to SSH to the EC2 instances",
"Type": "String",
"MinLength": "9",
"MaxLength": "18",
"Default": "0.0.0.0/0",
"AllowedPattern": "(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})",
"ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
}
},
"Mappings" : {
"AWSInstanceType2Arch" : {
"t1.micro" : { "Arch" : "64" },
"m1.small" : { "Arch" : "64" }
},
"AWSRegionArch2AMI" : {
"us-east-1" : { "32" : "ami-d7a18dbe", "64" : "ami-bba18dd2", "64HVM" : "ami-0da96764" },
"us-west-2" : { "32" : "ami-def297ee", "64" : "ami-ccf297fc", "64HVM" : "NOT_YET_SUPPORTED" },
"us-west-1" : { "32" : "ami-923909d7", "64" : "ami-a43909e1", "64HVM" : "NOT_YET_SUPPORTED" }
}
},
"Resources" : {
ChefClient" : {
"Type" : "AWS::EC2::Instance",
"Metadata" : {
"Description" : "Chef Client",
"AWS::CloudFormation::Init" : {
"config" : {
"packages" : {
"yum" : {
"git" : []
}
}
}
}
},
"Properties": {
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
"InstanceType" : { "Ref" : "InstanceType" },
"SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
"KeyName" : { "Ref" : "KeyName" },
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"#!/bin/bash -vn",
"yum update -y aws-cfn-bootstrapn",
"function error_exitn",
"{n",
" cfn-signal -e 1 -r "$1" '", { "Ref" : "WaitHandle" }, "'n",
" exit 1n",
"}n",
"yum update -yn",
"yum install git -yn",
"/sbin/service iptables stopn",
"/sbin/service ip6tables stopn",
"/sbin/chkconfig iptables offn",
"/sbin/chkconfig iptables offn",
"yum install git -yn",
"/usr/bin/curl -L https://www.opscode.com/chef/install.sh | bashn",
"cd /root/n",
"/usr/bin/git git://github.com/opscode/chef-repo.gitn",
"/bin/mkdir -p /root/chef-repo/.chefn",
"/bin/mkdir -p /etc/chefn",
"/bin/mkdir /root/.awsn",
"/bin/touch /root/.aws/confign",
"/bin/echo '[default]' >> /root/.aws/confign",
"/bin/echo 'region = ", {"Ref" : "AWS::Region" }, "' >> /root/.aws/confign",
"/bin/echo 'aws_access_key_id = ", { "Ref" : "HostKeys" }, "' >> /root/.aws/confign",
"/bin/echo 'aws_secret_access_key = ", { "Ref" : "SecretAccessKey" }, "' >> /root/.aws/confign",
"/usr/bin/aws s3 cp s3://storage/admin.pem /root/chef-repo/.chefn",
"/usr/bin/aws s3 cp s3://storage/chef-validator.pem /root/chef-repo/.chefn",
"/usr/bin/aws s3 cp s3://storage/knife.rb /root/chef-repo/.chefn",
"/usr/bin/aws s3 cp s3://storage/client.rb /etc/chefn",
"/usr/bin/aws s3 cp s3://storage/json_attribs.json /etc/chefn",
"/bin/cp -p /root/chef-repo/.chef/chef-validator.pem /etc/chef/validation.pemn",
"/usr/sbin/ntpdate -q 0.europe.pool.ntp.orgn",
"/bin/echo 'nchef_server_url "", { "Ref" : "ChefServerURL" }, ""' >> /etc/chef/client.rbn",
"/bin/echo 'nchef_server_url "", { "Ref" : "ChefServerURL" }, ""' >> /root/chef-repo/.chef/knife.rbn",
"/usr/bin/chef-clientn",
"/opt/aws/bin/cfn-signal -e 0 -r "ChefClient setup complete" '", { "Ref" : "WaitHandle" }, "'n"
]]}}
}
},
"WaitHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"WaitCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : "ChefClient",
"Properties" : {
"Handle" : {"Ref" : "WaitHandle"},
"Timeout" : "1200"
}
},
"ChefServer" : {
"Type" : "AWS::EC2::Instance",
"Metadata" : {
"Description" : "Bootstrap ChefServer",
"AWS::CloudFormation::Init" : {
"config" : {
"packages" : {
"yum" : {
"wget" : []
}
}
}
}
},
"Properties": {
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
"InstanceType" : { "Ref" : "InstanceType" },
"SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
"KeyName" : { "Ref" : "KeyName" },
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"#!/bin/bashn",
"cfn-init --region ", { "Ref" : "AWS::Region" },
" -s ", { "Ref" : "AWS::StackId" }, " -r ChefServer ", " -c orderby ",
" --access-key ", { "Ref" : "HostKeys" },
" --secret-key ", {"Ref" : "SecretAccessKey"}, " || error_exit 'Failed to run cfn-init'n",
"yum update -y aws-cfn-bootstrapn",
"function error_exitn",
"{n",
" cfn-signal -e 1 -r "$1" '", { "Ref" : "WaitHandle" }, "'n",
" exit 1n",
"}n",
"yum update -yn",
"/sbin/service iptables stopn",
"/sbin/service ip6tables stopn",
"/sbin/chkconfig iptables offn",
"/sbin/chkconfig ip6tables offn",
"#Install ChefServer packagen",
"cd /root/n",
"/usr/bin/wget https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-server-11.0.10-1.el6.x86_64.rpmn",
"/bin/rpm -ivh /root/chef-server-11.0.10-1.el6.x86_64.rpmn",
"/usr/bin/wget https://s3.amazonaws.com/storage/default.rbn",
"/bin/cp -f default.rb /opt/chef-server/embedded/cookbooks/runit/recipes/default.rbn",
"#Configure ChefServern",
"su - -c '/usr/bin/chef-server-ctl reconfigure'n",
"su - -c '/usr/bin/chef-server-ctl restart'n",
"#AWS creds installationn",
"/bin/mkdir /root/.awsn",
"/bin/touch /root/.aws/confign",
"/bin/echo '[default]' >> /root/.aws/confign",
"/bin/echo 'region = ", {"Ref" : "AWS::Region" }, "' >> /root/.aws/confign",
"/bin/echo 'aws_access_key_id = ", { "Ref" : "HostKeys" }, "' >> /root/.aws/confign",
"/bin/echo 'aws_secret_access_key = ", { "Ref" : "SecretAccessKey" }, "' >> /root/.aws/confign",
"#Upload files for clientn",
"/usr/bin/aws s3 cp /etc/chef-server/admin.pem s3://storage/n",
"/usr/bin/aws s3 cp /etc/chef-server/chef-validator.pem s3://storage/n",
"#Chef client and dirs for itn",
"/usr/bin/curl -L https://www.opscode.com/chef/install.sh | /bin/bashn",
"/bin/mkdir /root/.chefn",
"/bin/mkdir /etc/chefn",
"/bin/mkdir /etc/chef/cookbooksn",
"/bin/mkdir /etc/chef/rolesn",
"#Knife client config files from S3n",
"/bin/cp /etc/chef-server/admin.pem /etc/chef/client.pemn",
"/usr/bin/aws s3 cp s3://storage/knife_admin.rb /root/.chef/knife.rbn",
"#Roles and cookbooks from S3n",
"/usr/bin/aws s3 cp s3://storage/roles/ /etc/chef/roles/ --recursiven",
"/usr/bin/aws s3 cp s3://storage/cookbooks/ /etc/chef/cookbooks/ --recursiven",
"#Cookbooks from communityn",
"/usr/bin/knife cookbook site download cronn",
"/usr/bin/knife cookbook site download jenkinsn",
"/usr/bin/knife cookbook site download ntpn",
"/usr/sbin/ntpdate -q 0.europe.pool.ntp.orgn",
"yum remove ruby -yn",
"yum install ruby19 -yn",
"#Unpack and move cookbooksn",
"/bin/mv /root/*.tar.gz /etc/chef/cookbooksn",
"for i in `/bin/ls /etc/chef/cookbooks/*.tar.gz`; do /bin/tar zxf $i -C /etc/chef/cookbooks/; /bin/rm -f $i; donen",
"for i in `/bin/ls /etc/chef/cookbooks`; do /usr/bin/knife cookbook upload $i; donen",
"#Upload cookbooks and rolesn",
"/usr/bin/knife cookbook upload * -c '/root/.chef/knife.rb'n",
"/usr/bin/knife role from file /etc/chef/roles/*.rbn",
"/bin/echo -e "*/5 * * * * root /usr/bin/knife exec -E 'nodes.find(\"!roles:BaseRole\") { |n| puts n.run_list.add(\"role[BaseRole]\"); n.save}' -c '/root/.chef/knife.rb'" >> /etc/crontabn",
"/bin/echo -e "*/5 * * * * root /usr/bin/knife exec -E 'nodes.find(\"env_role:master AND !roles:master\") { |n| puts n.run_list.add(\"role[master]\"); n.save}' -c '/root/.chef/knife.rb'" >> /etc/crontabn",
"/bin/echo -e "*/5 * * * * root /usr/bin/knife exec -E 'nodes.find(\"env_role:slave AND !roles:slave\") { |n| puts n.run_list.add(\"role[slave]\"); n.save}' -c '/root/.chef/knife.rb'" >> /etc/crontabn",
"/opt/aws/bin/cfn-signal -e 0 -r "ChefServer setup complete" '", { "Ref" : "WaitHandle" }, "'n"
]]}}
}
},
"WaitHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"WaitCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : "ChefServer",
"Properties" : {
"Handle" : {"Ref" : "WaitHandle"},
"Timeout" : "1200"
}
},
"WebServerSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Enable HTTP access via port 80 and SSH access",
"SecurityGroupIngress" : [
{"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "8080", "ToPort" : "8080", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "443", "ToPort" : "443", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}}
]
}
},
}
Из template видно, что у нас объявлены пять параметров. Два из них имеют в себе значения по умолчанию – тип создаваемого instance (в данном случае это m1.small) и подсеть IP адресов, с которых будет разрешен SSH-доступ к узлу. При создании stack-а необходимо будет указать 3 параметра – ключ для SSH-доступа к узлам (создается отдельно в AWS Console), ключ доступа и секретный ключ (оба формируются при регистрации учетной записи доступа к AWS).
В mapping-ах описаны два типа instance, оба с архитектурой 64-bit. А также, в AWSRegionArch2AMI – описаны ID виртуальных образов, которые соответствуют instance с ОС Amazon Linux (данные ID можно получить в AWS Console).
Далее, у нас описываются ресурсы Chef-сервера и Chef-клиента. В обоих случаях через раздел Metadata, прежде чем запускать команды из User Data раздела, устанавливается wget (на всякий случай, на самом деле образы Amazon Linux должны содержать такие пакеты). Создаваемые ресурсы определяются переменными ImageId и InstanceType (в данном случае это заданные ранее параметры, в данном случае Amazon Linux, m1.small и 64-bit архитектура). Далее идет основное тело ресурса – User Data. Она представляет собой тело bash-скрипта конфигурации, который исполняется пошагово после того, как наш instance инициализируется.
Вкратце, для узла, который будет Chef-сервером проводятся следующие действия:
- отключение iptables (дабы не возникали проблемы с drop-ом пакетов);
- установка Open Source Chef Server;
- подмена default.rb (да уж, баг Amazon Linux образов в том, что они представляются не как redhat, чем являются по сути, а как amazon, и сервис Chef-server не может полноценно работать);
- авто-конфигурация и перезапуск сервера;
- создание файла конфигурации для работы AWS консоли;
- загрузка файлов admin.pem и chef-validator.pem на хранилище (они понадобятся нам на клиенте);
- установка chef-client на узле (да-да, Chef-сервер не имеет своего knife);
- получение файлов client.pem и knife.rb для knife, работающего на сервере (эдакий starter-kit из первой части статьи);
- воссоздание структуры директории chef-repo, в которой хранятся наши cookbook-и, файлы ролей и т.п.;
- загрузка и установка cookbook-ов на сервер (наши cookbook-и находится в хранилище, а часть берется с community);
- загрузка и установка ролей на сервер;
- добавление периодического задания, которое будет каждые 5 минут опрашивать все узлы и устанавливать базовую роль на каждый новый.
Я понимаю, что это может выглядеть сумбурным и непонятным объяснением, однако на детальное рассмотрение каждой части скрипта ушло бы много места. Поэтому – если есть вопросы – смело пишите мне в ЛС или же в комментарии.
Возвращаемся к нашему template-у. Для узла, который будет Chef-клиентом, проводятся следующие действия:
- отключение iptables (дабы не возникали проблемы с drop-ом пакетов);
- установка Chef Client;
- создание файла конфигурации для работы AWS консоли;
- загрузка файлов конфигурации клиента с хранилища;
- добавление в конфигурацию клиента адреса Chef-сервера (он может изменятся даже при перезапуске образов, либо при запуске stack заново);
- добавление периодического задания для запуска Chef client.
Стоит отметить, что благодаря такой опции, как json_attribs, мы можем создать метку для узла, которая будет определять его роль в инфраструктуре. Это сделано для того случая, когда среди Chef-клиентов могут быть узлы, принимающие различные инфраструктурные роли.
Следующие ресурсы – WaitHandle и WaitCondition – описывают условия, при которых процесс создания stack может быть приостановлен. Если WaitHandle получает сигнал об успешном завершении процесса в течении тайм-аута, указанного в WaitCondition – то процесс создания stack продолжается/завершается.
Следующий объявляемый ресурс – это Security Group – firewall для наших узлов. В группе описывается пробрасываемые порты и source address пакетов.
Последний блок – Outputs – служит для того, чтобы после успешного запуска stack и instance получить на выходе какие-либо переменные, интересующие нас. Например, доменное имя для того, чтобы получить доступ к instance.
В итоге мы получаем универсальный template и возможность «развернуть» нашу скромную инфраструктуру (если же интересует большее количество instance — используйте Auto-Scaling Group) исполнением одной команды в консоли управления AWS. Результат запуска можно посмотреть в разделе CloudFormation.
Что дальше? Дальше Вы получаете возможность контролировать узлы путем knife, cookbook-ов и ролей. Можно использовать community cookbook, писать свои, писать wrapper-ы к другим cookbook-ам. Возможностей много и зависит все от конечной задачи.
В этом цикле статей я постарался, пусть и поверхностно, описать процесс автоматизации управления парком ПК и взаимодействии с облачными ресурсами AWS. Надеюсь, для начинающих DevOps эти статьи будут интересны.
Если буду возникать вопросы и предложения – смело пишите в диалоги или комментарии к моим статьям. Спасибо всем, кто уделил время прочтению статей.
До скорых встреч!
Ссылки:
- Документация по AWS — aws.amazon.com/documentation/
- AWS CloudFormation — aws.amazon.com/documentation/cloudformation/
- AWS EC2 — aws.amazon.com/documentation/ec2/
- AWS Sample Templates — aws.amazon.com/cloudformation/aws-cloudformation-templates/
- AWS Console — docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/getting-started.html
Автор: MistiC