Wednesday, September 24, 2014

Autoconfiguration using Puppet and AWS Cloud Formation

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.

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 off
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.
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}"
}

}

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
}
}

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 -source
s3-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'
}
}

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'],
}

}

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'
}
}

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
}

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
}



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.
·         Puppet
·         cloudformation
·         autoconfiguration
·         administration


1 comment:

  1. Thanks for providing this informative information you may also refer.
    http://www.s4techno.com/blog/2015/12/21/protect-instances-from-termination-by-auto-scaling/

    ReplyDelete