The day has come for us to
put aside our cookbooks, recipes and Chef’s knives, and try on the role of puppet master!
Our initial goal is pretty trivial – give developers a quick and easy way to deploy
an environment. One exclusive requirement – we must use Puppet Enterprise for the auto-configuration.
A few words about the environment we’re going to deploy. It contains two components — FrontEnd (its functions are performed by an IIS server) and BackEnd (it contains a MongoDB database and a Worker service created by developers). Both components, as you can already tell, are implemented on a Windows Server. The source code for the FrontEnd and Worker service content are taken from AWS S3, where it is diligently stored by Jenkins on a nightly basis.
A few words about the environment we’re going to deploy. It contains two components — FrontEnd (its functions are performed by an IIS server) and BackEnd (it contains a MongoDB database and a Worker service created by developers). Both components, as you can already tell, are implemented on a Windows Server. The source code for the FrontEnd and Worker service content are taken from AWS S3, where it is diligently stored by Jenkins on a nightly basis.
Creating a Cloud Formation template
Implementing a Cloud Formation template that will launch two Windows servers is
very simple. A much more interesting challenge is figuring out how to inform
Puppet about which configuration to apply to these servers.
Puppet’s official documentation suggests applying regular expressions to the client hostname, but this does not really work for us, because on AWS Amazon the hostname is assigned automatically and can change after stop-starting the instance, so I would have to create a post-start script to change the hostname and only after that, run the puppet agent.
After digging around in the documentation a bit more, I found just the right thing — Custom External Facts. For those who have worked with Chef Server, facts are the equivalent to attributes. To add our own facts for the Windows machine, we need to create a bat or ps1 file with approximately the following content and put it here: "C:\ProgramData\PuppetLabs\facter\facts.d\"
@echo offPuppet’s official documentation suggests applying regular expressions to the client hostname, but this does not really work for us, because on AWS Amazon the hostname is assigned automatically and can change after stop-starting the instance, so I would have to create a post-start script to change the hostname and only after that, run the puppet agent.
After digging around in the documentation a bit more, I found just the right thing — Custom External Facts. For those who have worked with Chef Server, facts are the equivalent to attributes. To add our own facts for the Windows machine, we need to create a bat or ps1 file with approximately the following content and put it here: "C:\ProgramData\PuppetLabs\facter\facts.d\"
echo node_role=frontend
echo app_version=Build1.2.0
serverRole is, as the name implies, the role
that is assigned to the server, and buildNumber is the version of that
application that will be downloaded from S3 AWS.
A Cloud Formation template will create this file.
A Cloud Formation template will create this file.
DevEnv.tmpl
{"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Developers Stack",
"Parameters" : {
"KeyName" : {
"Description" : "Key-pair name",
"Type" : "String"
},
"SuffixName" : {
"Description" : "Suffix for all created resources",
"Type" : "String"
},
"FrontEndInstanceType" : {
"Type" : "String",
"Default" : "m1.small",
"AllowedValues" : [ "m1.small", "m1.medium", "m1.large", "m1.xlarge"],
"Description" : "EC2 instance type"
},
"BackEndInstanceType" : {
"Type" : "String",
"Default" : "m1.small",
"AllowedValues" : [ "m1.small", "m1.medium", "m1.large", "m1.xlarge"],
"Description" : "EC2 instance type"
},
"PuppetServer": {
"Description" : "Puppet Server URL",
"Type" : "String",
"Default" : "ec2-231-231-123-123.us-west-2.compute.amazonaws.com"
},
"Zone" : {
"Type" : "CommaDelimitedList",
"Description" : "The Availability Zone ",
"Default" : "us-west-2c"
},
"BuildVersion" : {
"Type" : "String",
"Description" : "Version of application build"
},
"RoleName" : {
"Type" : "String",
"Description" : "Instance IAM role",
"Default" : "WebInstance"
},
"SecurityGroup" : {
"Type" : "String",
"Description" : "Default security group for stack",
"Default" : "taws-security-group"
}
},
"Mappings" : {
"WindowsInstanceType" : {
"t1.micro" : { "Arch" : "64" },
"m1.small" : { "Arch" : "64" },
"m1.medium" : { "Arch" : "64" },
"m1.large" : { "Arch" : "64" },
"m1.xlarge" : { "Arch" : "64" }
},
"WindowsRegionMap" : {
"us-east-1" : { "AMI" : "ami-e55a7e8c" },
"us-west-2" : { "AMI" : "ami-1e53c82e" },
"us-west-1" : { "AMI" : "ami-b687b1f3" },
"eu-west-1" : { "AMI" : "ami-5f3ad728" },
"ap-southeast-1" : { "AMI" : "ami-96cd98c4" },
"ap-southeast-2" : { "AMI" : "ami-ab4a2daa" },
"ap-northeast-1" : { "AMI" : "ami-133fa329" },
"sa-east-1" : { "AMI" : "ami-bd3d9ba0" }
}
},
"Resources" : {
"FrontEnd" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"KeyName" : { "Ref" : "KeyName" },
"ImageId" : { "Fn::FindInMap" : [ "WindowsRegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
"InstanceType" : { "Ref" : "FrontEndInstanceType" },
"IamInstanceProfile" : { "Ref" : "RoleName" },
"SecurityGroups" : [{ "Ref" : "SecurityGroup" }],
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : ["",[{"Ref" : "SuffixName"},"-DEV-FrontEnd"]]}}
],
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"<powershell>\n",
"$MsiUrl = \"https://s3-us-west-2.amazonaws.com/mybucket/puppet.msi\"\n",
"$downloadPath = \"c:\\puppet.msi\"\n",
"$webClient = New-Object System.Net.WebClient\n",
"$webClient.DownloadFile($MsiUrl, $downloadPath)\n",
"$process = Start-Process -File $downloadPath -arg \"/qn /norestart\" -PassThru |wait-process\n",
"$PublicHostName = Invoke-RestMethod -Uri http://169.254.169.254/latest/meta-data/public-hostname -Method Get\n",
"Clear-Content 'C:\\ProgramData\\PuppetLabs\\puppet\\etc\\puppet.conf'\n",
"Add-Content 'C:\\ProgramData\\PuppetLabs\\puppet\\etc\\puppet.conf' \"[main]\", \"runinterval=300\", \"certname=$PublicHostName\", \"server=",{ "Ref" : "PuppetServer" },"\", \"environment=",{ "Ref" : "PuppetEnvironment" },"\"\n",
"Add-Content 'C:\\ProgramData\\PuppetLabs\\facter\\facts.d\\facts.bat' \"@echo off\", \"echo node_role=frontend\", \"echo app_version=",{ "Ref" : "BuildVersion" },"\"\n",
"Restart-Service pe-puppet\n",
"$MsiUrl = \"https://s3-us-west-2.amazonaws.com/mybucket/7zip.msi\"\n",
"$downloadPath = \"c:\\7zip.msi\"\n",
"$webClient = New-Object System.Net.WebClient\n",
"$webClient.DownloadFile($MsiUrl, $downloadPath)\n",
"$process = Start-Process -File $downloadPath -arg \"/qn \" -PassThru |wait-process\n",
"</powershell>\n"
]]}}
}
},
"BackEnd" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"KeyName" : { "Ref" : "KeyName" },
"ImageId" : { "Fn::FindInMap" : [ "WindowsRegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
"InstanceType" : { "Ref" : "BackEndInstanceType" },
"IamInstanceProfile" : { "Ref" : "RoleName" },
"SecurityGroups" : [{ "Ref" : "SecurityGroup" }],
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : ["",[{"Ref" : "SuffixName"},"-DEV-BackEnd"]]}}
],
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"<powershell>\n",
"$MsiUrl = \"https://s3-us-west-2.amazonaws.com/mybucket/puppet.msi\"\n",
"$downloadPath = \"c:\\puppet.msi\"\n",
"$webClient = New-Object System.Net.WebClient\n",
"$webClient.DownloadFile($MsiUrl, $downloadPath)\n",
"$process = Start-Process -File $downloadPath -arg \"/qn /norestart\" -PassThru |wait-process\n",
"$PublicHostName = Invoke-RestMethod -Uri http://169.254.169.254/latest/meta-data/public-hostname -Method Get\n",
"Clear-Content 'C:\\ProgramData\\PuppetLabs\\puppet\\etc\\puppet.conf'\n",
"Add-Content 'C:\\ProgramData\\PuppetLabs\\puppet\\etc\\puppet.conf' \"[main]\", \"runinterval=300\", \"certname=$PublicHostName\", \"server=",{ "Ref" : "PuppetServer" },"\", \"environment=",{ "Ref" : "PuppetEnvironment" },"\"\n",
"Add-Content 'C:\\ProgramData\\PuppetLabs\\facter\\facts.d\\facts.bat' \"@echo off\", \"echo node_role=backend\", \"echo app_version=",{ "Ref" : "BuildVersion" },"\"\n",
"Restart-Service pe-puppet\n",
"$MsiUrl = \"https://s3-us-west-2.amazonaws.com/mybucket/7zip.msi\"\n",
"$downloadPath = \"c:\\7zip.msi\"\n",
"$webClient = New-Object System.Net.WebClient\n",
"$webClient.DownloadFile($MsiUrl, $downloadPath)\n",
"$process = Start-Process -File $downloadPath -arg \"/qn \" -PassThru |wait-process\n",
"</powershell>\n"
]]}}
}
}
},
"Outputs" : {
"FrontEndPublicDnsName" : {
"Description" : "Public IP address of FrontEnd",
"Value" : { "Fn::Join" : ["",[{ "Fn::GetAtt" : [ "FrontEnd", "PublicDnsName" ] }]]}
},
"BackEndPublicDnsName" : {
"Description" : "Public IP address of BackEnd",
"Value" : { "Fn::Join" : ["",[{ "Fn::GetAtt" : [ "BackEnd", "PublicDnsName" ]}]]}
}
}
}
Parameters used in the template:
·
KeyName — the name of the access key
·
SuffixName — a suffix that is added to the Name tag (for example,
it can be the developer’s initials)
·
FrontEndInstanceType — shape type for the FrontEnd
·
BackEndInstanceType — shape type for the BackEnd
·
PuppetServer —Puppet server URL
·
Zone — the zone where the servers are created
·
BuildVersion — the version of the application that is downloaded
from с S3
·
RoleName — IAM Role created in advance with ”S3 Read-Only“ permissions
·
SecurityGroup — a Security Group also created in advance
The IAM Role and Security Group can be created with the same template, and this would be the best option. I am not using this in my example for the sake of simplifying.
In the UserData section, Puppet agent and 7zip are downloaded, and puppet.conf and facts.bat are generated.
We are done with Cloud Formation, now on to Puppet configuration.
Configuring
Puppet Server Enterprise
To install Puppet Server Enterprise, we must simply download the installer archive and unpack and launch puppet-server-installer. To enable automatic client registration on the server, we create a /etc/puppetlabs/puppet/autosign.conf file with the following content:
*
On to creating the necessary modules. Modules are similar to the cookbooks in Chef. They are stored in the /etc/puppetlabs/puppet/modules folder.
The simplified structure of a module:
·
my_module/ — the
directory title will be the module’s name
·
manifests/ — contains
the module’s manifests.
·
init.pp — contains
one my_module class. The name of the class must match the module’s class
·
other_class.pp — contains
the other class of the module: my_module::other_class.
·
files/ — contains
the files that will be downloaded by the client
·
lib/ — contains
the plugin and custom facts
·
templates/ — contains
the templates that can be used in the module
·
component.erb — a
manifest that will be available in the module as template('my_module/component.erb').
First, we add the necessary modules from PuppetLabs for installing IIS and for management.
puppet module install dism
puppet module install opentable-iis
Now, we need to fix up the manifest a bit for opentable-iis:
/etc/puppetlabs/puppet/modules/nodes/manifests/init.pp
class iis {
iis::manage_app_pool {"${fqdn}":
enable_32_bit => true,
managed_runtime_version => 'v4.0',
} ->
iis::manage_site {"${fqdn}":
site_path => 'C:\MyAppPath',
port => '80',
ip_address => '*',
host_header => "${fqdn}",
app_pool => "${fqdn}"
}
}
iis::manage_app_pool {"${fqdn}":
enable_32_bit => true,
managed_runtime_version => 'v4.0',
} ->
iis::manage_site {"${fqdn}":
site_path => 'C:\MyAppPath',
port => '80',
ip_address => '*',
host_header => "${fqdn}",
app_pool => "${fqdn}"
}
}
I got seven modules (their number may increase over time).
1.
nodes — the module that will connect the next necessary module
according to the node_role value
/etc/puppetlabs/puppet/modules/nodes/manifests/init.pp
class nodes {
if "${node_role}" == «backend» {
include backend
}
if "${node_role}" == «frontend» {
include frontend
}
}
if "${node_role}" == «backend» {
include backend
}
if "${node_role}" == «frontend» {
include frontend
}
}
2.
getbuild — the module for downloading and unpacking the application
archive from AWS S3.
/etc/puppetlabs/puppet/modules/getbuild/manifests/init.pp
class getbuild {
file { 'c:\config':
ensure => 'directory'
} ->
file { 'c:\Build':
ensure => 'directory'
} ->
exec { 'download_build':
creates => «c:\\config\\${app_version}»,
path => $::path,
command => «powershell.exe -executionpolicy unrestricted start-bitstransfer -sources3-us-west-2.amazonaws.com/mybucket/${app_version} -Destination 'c:\\config\\'»,
} ->
exec { 'app_install':
creates => «c:\\Build\CustomBackendService.exe.config»,
command => "\«c:\\Program Files\\7-Zip\\7z.exe\» x c:\\config\\${app_version} -oC:\\Build ",
}
}
file { 'c:\config':
ensure => 'directory'
} ->
file { 'c:\Build':
ensure => 'directory'
} ->
exec { 'download_build':
creates => «c:\\config\\${app_version}»,
path => $::path,
command => «powershell.exe -executionpolicy unrestricted start-bitstransfer -sources3-us-west-2.amazonaws.com/mybucket/${app_version} -Destination 'c:\\config\\'»,
} ->
exec { 'app_install':
creates => «c:\\Build\CustomBackendService.exe.config»,
command => "\«c:\\Program Files\\7-Zip\\7z.exe\» x c:\\config\\${app_version} -oC:\\Build ",
}
}
3.
mongodb — the module for installing MongoDB
/etc/puppetlabs/puppet/modules/mongodb/manifests/init.pp
class mongodb {
file { 'c:/config':
ensure => directory,
} ->
file { 'c:/config/mongodb.zip':
ensure => file,
mode => '0777',
source => 'puppet:///modules/mongodb/mongodb-win32-x86_64-v2.4-latest.zip',
} ->
file { 'c:/MongoDB':
ensure => directory,
} ->
file { 'c:/MongoDB/bin':
ensure => directory,
} ->
file { 'c:/MongoDB/Data':
ensure => directory,
} ->
file { 'c:/MongoDB/logs':
ensure => directory,
} ->
exec { 'mongodb-unzip':
creates => 'c:/MongoDB/bin/mongod.exe',
command => '«c:\\Program Files\\7-Zip\\7z.exe» e c:\\config\mongodb.zip -oC:\\MongoDB\\bin',
} ->
exec { 'mongodb-install':
creates => 'c:/MongoDB/logs/mongodb.log',
command => '«c:\\MongoDB\\mongod.exe» --dbpath=c:\\MongoDB\\Data --port 27017 --logpath=c:\\MongoDB\logs\\mongodb.log --install --serviceName mongodb --serviceDisplayName «MongoDB Server» --serviceDescription «MongoDB Server»',
} ->
exec { 'mongodb-run':
path => $::path,
command => 'powershell.exe start-service mongodb'
}
}
file { 'c:/config':
ensure => directory,
} ->
file { 'c:/config/mongodb.zip':
ensure => file,
mode => '0777',
source => 'puppet:///modules/mongodb/mongodb-win32-x86_64-v2.4-latest.zip',
} ->
file { 'c:/MongoDB':
ensure => directory,
} ->
file { 'c:/MongoDB/bin':
ensure => directory,
} ->
file { 'c:/MongoDB/Data':
ensure => directory,
} ->
file { 'c:/MongoDB/logs':
ensure => directory,
} ->
exec { 'mongodb-unzip':
creates => 'c:/MongoDB/bin/mongod.exe',
command => '«c:\\Program Files\\7-Zip\\7z.exe» e c:\\config\mongodb.zip -oC:\\MongoDB\\bin',
} ->
exec { 'mongodb-install':
creates => 'c:/MongoDB/logs/mongodb.log',
command => '«c:\\MongoDB\\mongod.exe» --dbpath=c:\\MongoDB\\Data --port 27017 --logpath=c:\\MongoDB\logs\\mongodb.log --install --serviceName mongodb --serviceDisplayName «MongoDB Server» --serviceDescription «MongoDB Server»',
} ->
exec { 'mongodb-run':
path => $::path,
command => 'powershell.exe start-service mongodb'
}
}
4.
api — the module for installing the application on
the FrontEnd
/etc/puppetlabs/puppet/modules/api/manifests/init.pp
class api {
include getbuild
dism { 'IIS-WebServerRole':
ensure => present,
} ->
dism { 'IIS-WebServer':
ensure => present,
require => Dism['IIS-WebServerRole'],
}
}
include getbuild
dism { 'IIS-WebServerRole':
ensure => present,
} ->
dism { 'IIS-WebServer':
ensure => present,
require => Dism['IIS-WebServerRole'],
}
}
5.
worker — the module for installing the application on the BackEnd
/etc/puppetlabs/puppet/modules/worker/manifests/init.pp
class worker {
include getbuild
exec { 'service_install':
creates => «c:\\Build\\Custom.AWS.BackendService.InstallLog»,
command => «c:\\Build\\Custom.AWS.BackendService.exe -install»,
} ->
exec { 'service-run':
path => $::path,
command => 'powershell.exe start-service Custom.AWS.Backend'
}
}
include getbuild
exec { 'service_install':
creates => «c:\\Build\\Custom.AWS.BackendService.InstallLog»,
command => «c:\\Build\\Custom.AWS.BackendService.exe -install»,
} ->
exec { 'service-run':
path => $::path,
command => 'powershell.exe start-service Custom.AWS.Backend'
}
}
6.
frontend — the module that connects all modules necessary for the FrontEnd
to work
/etc/puppetlabs/puppet/modules/frontend/manifests/init.pp
class frontend {
include api
include iis
}
include api
include iis
}
7.
backend — the module that connects all modules necessary for the BackEnd
to work
/etc/puppetlabs/puppet/modules/backend/manifests/init.pp
class backend {
include mongodb
include worker
}
include mongodb
include worker
}
In my manifests, I used the exec resource almost everywhere. With the correctly set creates parameter, this is a fail-proof solution.
More details in the following example:
exec { 'mongodb-unzip':
creates => 'c:/MongoDB/bin/mongod.exe',
command => '"c:\\Program
Files\\7-Zip\\7z.exe" e c:\\config\mongodb.zip -oC:\\MongoDB\\bin',
}
If there is no a c:/MongoDB/bin/mongod.exe executable file, then the archive will be unpacked.
Now, for the sake of convenience, we can create a task in our favorite CI system, for example, Jenkins, insert the script for running the Cloud Formation template into it, and the developers will be able to deploy the environment in one click.
That’s all, folks. I hope this tutorial proves useful.
If any readers out there happen to be Puppet experts, I would be extremely grateful for your feedback.
Thanks for providing this informative information you may also refer.
ReplyDeletehttp://www.s4techno.com/blog/2015/12/21/protect-instances-from-termination-by-auto-scaling/