Jordan Sissel. hacker. Loggly, Inc.


Our puppet deployment is:
Uses:
A source of Truth feeds the Model and results in an applied configuration.
I never use ‘import’. I always let puppet determine the path of a class.
Your puppet repository should look like this:
manifests/site.pp
modules/MODULENAME/{manifests,files,templates,plugins}/
# Examples:
modules/apache/manifests/server.pp
modules/apache/templates/httpd.conf.erb
modules/ssh/manifests/server.pp
modules/ssh/files/sshd_config
Puppet lets you abstract things away from the madness of your OS, let it.
puppet:///modules/MODULE/PATHExample:
class ssh::server {
file {
"/etc/ssh/sshd_config":
#source => "puppet:///modules/ssh/etc/ssh/sshd_config", # Bad!
source => "puppet:///modules/ssh/sshd_config"; # Good!
}
}
Avoid trying to expose variables to other classes; instead use custom defines.
# Bad
include apache::server # sets '$configpath'
include apache::package
file {
"${apache::server::configpath}/mysite.conf":
source => "puppet:///...",
require => Class["apache::package"]
notify => Class["apache::server"];
}
Use a custom define to abstract a general-purpose apache config entry:
# In modules/apache/manifests/config.pp
define apache::config($source) {
include apache::server
include apache::package
file {
"/etc/apache/conf.d/$name.conf":
source => $source,
require => Class["apache::package"]
notify => Class["apache::server"];
}
}
# Abstract all the complexity into a reusable define:
apache::config {
"mysite":
source => "puppet:///...";
}
Don’t require resources you haven’t declared in a class.
class apache::server {
include apache::package # defines Package["apache2"]
service {
"apache":
require => Package["apache2"], # Bad!
require => Class["apache::package"], # Good!
... ;
}
}
Split ‘service’ and ‘package’ resources. Require the ‘package’, notify the ‘service’
class apache::package {
# any package/files/users for apache
}
class apache::server {
include apache::package
# any services for apache
}
class nagios::web { # custom define, includes apache::package and ::server
apache::config {
"nagios":
source => "puppet:///modules/nagios/web/nagios.httpd.conf";
}
}
Example:
# modules/foo/manifests/init.pp
class foo {
class bar {
# Bad!
}
}
|
# Good
# modules/foo/manifests/init.pp
class foo { ... }
# modules/foo/manifests/bar.pp
class foo::bar { ... }
|
Dependencies on custom defines:
some::custom::define {
"myname": ... ;
}
file {
"/tmp/x"
require => Some::Custom::Define["myname"];
}
Syntax check all puppet manifests:
find modules manifests -name '*.pp' \
| xargs -t -n1 -P2 sh -c 'puppet --parseonly "$@" || exit 255' -
Syntax check templates:
find modules/*/templates/ -maxdepth 1 -type f -not -name .svn \
| xargs -n1 sh -c 'erb -x -T - $1 | ruby -c 2>&1 | sed -e "s,^,$1: ,"' -
Two main kinds of truth:
| Tag(s): |
role:solr=true role:zookeeper=true solr:levels=4,5 zookeeper:id=3 |
I use extlookup.
# in site.pp:
# '$deployment' fact comes from machine truth. What deployment are we?
$extlookup_precedence = [ "%{deployment}/config", "common" ]
Example production/config.csv (specifying tunables)
package/loggly-frontend,3045.trunk
package/loggly-monitoring-membase,latest
config/s3_enable,true
config/s3_enable_customers,all
Example modules/loggly/manifests/frontend.pp (uses extlookup!)
package {
"loggly-frontend":
ensure => extlookup("package/loggly-frontend");
}
class loggly::frontend {
# include any other required classes
package {
"loggly-frontend": ensure => extlookup("package/loggly-frontend");
}
iptables::rule {
"allow http": ports => 80;
"allow https": ports => 443;
}
apache::site {
"loggly-frontend":
source => template("loggly/frontend/loggly-frontend.httpd.conf.erb");
}
}
Result of previous slide’s config:
loggly-frontend 3044.trunk/etc/iptables.d/…
-A INPUT -s 0.0.0.0/0 -p tcp --syn --dport 80 -j ACCEPT -m comment \
--comment "allow http (any source)"
-A INPUT -s 0.0.0.0/0 -p tcp --syn --dport 443 -j ACCEPT -m comment \
--comment "allow https (any source)"
All features of a role should be defined in that class.
node somehost { } definitionsExample:
Not a frontend? Ok, make sure the frontend is not installed.
if has_role("frontend") {
include frontend
} else {
include frontend::remove
}
A nodeless site.pp is practically empty.
# manifests/site.pp
node default {
include truth::enforcer
}
# modules/truth/manifests/enforcer.pp
class truth::enforcer {
# For each role, include 1 class that is that role.
if has_role("statsserver") {
include loggly::statsserver
} else {
# Ensure this server has no 'statsserver' if we are not a statsserver.
include loggly::stattserver::remove
}
...
}
has_role() is a custom puppet function I wrote role:rolename=true tags.Custom defines let you create your own resource types that wrap other resources.
# Simplified version:
define supervisor::program($command, $user, $notifycmd="", $directory="/",
$ensure="present") {
include supervisor
file {
"/etc/supervisor/conf.d/$name.conf":
ensure => $file_ensure,
content => template("supervisor/program.erb"),
notify => Exec["poke supervisord"];
}
}
supervisor::program {
"loggly-solrserver":
command => "/opt/loggly/solrserver/solrserver.sh",
user => "root", # for ulimit, will drop privs before launching.
subscribe => Class["loggly::config", "loggly::solr"],
require => [User["loggly-solrserver"],
Class["loggly::common", "zeromq::java"],
File["/opt/loggly/solrserver/solrserver.sh",
"/opt/solr/dist/etc/log4j.properties"]];
}
iptables::rule {
"zookeeper client":
roles => ["solr", "zookeeper"],
ports => 2181;
"zookeeper ensemble":
roles => ["zookeeper"],
ports => [2888, 3888];
}
# The '@@' notation means 'export this resource'
# More on exported resources later.
@@nagios::host {
"$fqdn":
address => $ipaddress_eth0,
tag => "deployment::$deployment";
}
@@nagios::check {
"disk space on $fqdn":
command => "check-disk-space",
remote => true,
host => $fqdn,
contacts => "pagerduty",
tag => "deployment::$deployment";
}
Biggest win:
Treat your infrastructure as a collective (or multiple collectives) rather than as individual, standalone hosts
Exporting a check (a custom define):
@@nagios::check {
"httpinput end-to-end test from $fqdn":
command => "endtoend-httpinput", # Command to run
remote => true, # Is an NRPE check
host => $fqdn, # Host to target
contacts => "pagerduty", # Contact
tag => "deployment::$deployment"; # Workaround for bug#5329
}
nagios::command {
"endtoend-httpinput":
command => "/usr/local/bin/endtoend.py http",
remote => true; # register this command with NRPE
}
Collect all checks exported in our deployment (the <<| ... |>> syntax):
# Query by tag to work around puppet bug #5329
Nagios::Check <<| tag == "deployment::$deployment" |>> {
notify => Class["nagios::server"]
}
Results:
% ls /etc/nagios3/checks.d
check-frontend1.example.com-disk space on frontend1.example.com.cfg
check-frontend2.example.com-disk space on frontend2.example.com.cfg
check-ops.example.com-disk space on ops.example.com.cfg
check-proxy1.example.com-disk space on proxy1.example.com.cfg
check-proxy2.example.com-disk space on proxy2.example.com.cfg
...
Code to be released soon.
What if you shutdown an EC2 instance and it no longer needs monitoring?
Can use ‘tidy’ to purge files not updated recently:
tidy {
[ "/etc/nagios3/hosts.d", "/etc/nagios3/checks.d" ]:
age => 1d,
notify => Class["nagios::server"],
recurse => true,
matches => [ "*.cfg" ];
}
Scaling problems:
Overview:
puppet agent you puppet applyGreat for testing things even if you have a puppet master.
% puppet apply -e 'file { "/tmp/x": content => "Hello world\n"; }'
notice: /Stage[main]//File[/tmp/x]/ensure: defined content as
'{md5}f0ef7081e1539ac00ef5b761b4fb01b3'
% cat /tmp/x
Hello world
puppet apply mymanifest.ppLess chicken-and-egg; can start a standalone deployment with simply one
puppet apply.
# Masterless example:
% puppet --modulepath /path/to/modules /path/to/manifests.site.pp
In general: You have to solve problems already solved with the master:
We have a script run via cron doing:
Find me later:
Community: