Eppic Template Debugging
If you’ve ever written a Puppet EPP template and found yourself slogging through unit tests after every little change, there’s a faster, more interactive way: use puppet-debugger
together with the debug::breakpoint()
function to insert breakpoints directly in your manifests and in your .epp
templates.
In this exercise, we’ll walk through:
- Installing the required tools
- Writing an EPP template
- Inserting breakpoints in manifests and templates
- Running and exploring with
puppet-debugger
1. Prerequisites
Make sure you have:
- OpenVox or Puppet installed
- puppet-debugger gem installed
- nwops-debug Puppet module
Install the debugger gem:
gem install puppet-debugger
Install the module in your modulepath:
puppet module install nwops-debug
What is OpenVox?
OpenVox is an open-source fork of Puppet that was created in response to Puppet Inc.’s decision to make Puppet and related tools closed source. When Puppet Inc. moved these tools behind a paywall, the community created OpenVox to maintain open access to Puppet development tools and ensure continued innovation in the configuration management space. Head over to OpenVox for more info.
OpenVox provides:
- Open-source alternative to Puppet
- Continued community-driven development
- Free access to Puppet development tools
- Compatibility with existing Puppet modules and manifests
This fork ensures that developers can continue using Puppet tools without vendor lock-in, while the original Puppet tools are now only available through Puppet Inc.’s commercial offerings.
2. Creating a Puppet Module
We’ll create a small profile_web
module:
mkdir -p ~/.puppetlabs/etc/code/modules/profile_web/{templates,manifests}
touch ~/.puppetlabs/etc/code/modules/profile_web/manifests/init.pp
touch ~/.puppetlabs/etc/code/modules/profile_web/templates/vhost.epp
touch ~/.puppetlabs/etc/code/modules/profile_web/metadata.json
Now open you favorite editor and edit the module.
code ~/.puppetlabs/etc/code/modules/profile_web
# ~/.puppetlabs/etc/code/modules/profile_web/metadata.json
{
"name": "nwops-profile_web",
"version": "1.0.0",
"author": "Corey Osman",
"summary": "A Puppet profile for web server configuration management",
"description": "This module provides a profile for managing web server configurations including virtual hosts, SSL settings, and server aliases.",
"license": "Apache-2.0",
"source": "",
"project_page": "",
"issues_url": "",
"dependencies": [
{
"name": "puppetlabs/stdlib",
"version_requirement": ">= 4.0.0 < 9.0.0"
},
{
"name": "nwops/debug",
"version_requirement": ">= 0.0.3 < 1.0.0"
}
],
"operatingsystem_support": [
{
"operatingsystem": "RedHat",
"operatingsystemrelease": [
"7",
"8",
"9"
]
},
{
"operatingsystem": "CentOS",
"operatingsystemrelease": [
"7",
"8"
]
},
{
"operatingsystem": "Ubuntu",
"operatingsystemrelease": [
"18.04",
"20.04",
"22.04"
]
},
{
"operatingsystem": "Debian",
"operatingsystemrelease": [
"10",
"11"
]
}
],
"requirements": [
{
"name": "puppet",
"version_requirement": ">= 6.0.0 < 9.0.0"
}
]
}
# ~/.puppetlabs/etc/code/modules/profile_web/manifests/init.pp
class profile_web (
String $server_name = 'example.com',
Integer $listen_port = 80,
Array[String] $aliases = [],
Optional[String] $root_dir = '/var/www/html',
Boolean $https = false,
) {
$template_content = epp('profile_web/vhost.epp', {
'server_name' => $server_name,
'listen_port' => $listen_port,
'aliases' => $aliases,
'root_dir' => $root_dir,
'https' => $https
})
# pause compilation here and break into your puppet code
debug::break()
file { "/tmp/${server_name}.conf":
ensure => file,
content => $template_content,
}
}
3. Writing the EPP Template with a Breakpoint
~/.puppetlabs/etc/code/modules/profile_web/templates/vhost.epp
<%-# EPP parameter signature -%>
<% | String $server_name,
Integer $listen_port,
Array[String] $aliases = [],
Optional[String] $root_dir = '/var/www/html',
Boolean $https = false
| -%>
<%# Pause template rendering here %>
<% debug::break() -%>
# Rendered by Puppet EPP
server {
listen <%= $listen_port %><%- if $https { -%> ssl<%- } -%>;
server_name <%= $server_name %><%- if $aliases and !empty($aliases) { -%> <%= $aliases.join(' ') %><%- } -%>;
root <%= $root_dir %>;
<%- if $https -%>
# HTTPS block
ssl_certificate /etc/ssl/certs/<%= $server_name %>.crt;
ssl_certificate_key /etc/ssl/private/<%= $server_name %>.key;
<%- else -%>
# HTTP only
<%- end -%>
}
Here, debug::break()
is the magic that drops you into puppet-debugger
when the template is rendered.
4. Running the Debugger
This exercise is adding a module to your module directory when running the debugger which will automatically look in your modulepath just like puppet apply
does for the profile_web module.
puppet debugger --execute "include profile_web"
While we could also use puppet apply --execute "include profile_web"
that might do harm or make changes to our system. The debugger on the other hand is a simulation only. It works just like apply but never mutates the system.
When Puppet reaches the breakpoint inside the template, you’ll drop into the REPL.
5. Exploring Inside the Debugger
Once paused:
vars # Show all variables in scope
facts # View facts
$server_name # Inspect a single param
$https # See the value of $https or any other variable
exit # Resume rendering or exit if on last debugger session
You can also re-render the template with different values right in the debugger.
New Debugger sessions
Each time you execute that template a new debugger session will be invoked with those new values.
epp('profile_web/vhost.epp', {
'server_name' => 'staging.local',
'listen_port' => 443,
'aliases' => ['www.staging.local'],
'https' => true
})
Ruby Version: 3.4.2
Puppet Version: 8.10.0
Puppet Debugger Version: 1.5.0
Created by: NWOps <corey@nwops.io>
Type "commands" for a list of debugger commands
or "help" to show the help screen.
=> From file: vhost.epp
2: Integer $listen_port,
3: Array[String] $aliases = [],
4: Optional[String] $root_dir = '/var/www/html',
5: Boolean $https = false
6: | -%>
7:
8: <%# Pause template rendering here %>
=> 9: <% debug::break() %>
10:
11: # Rendered by Puppet EPP
12: server {
13: listen <%= $listen_port %><%- if $https { -%> ssl<%- } -%>;
14: server_name <%= $server_name %><%- if $aliases and !empty($aliases) { -%> <%= $aliases.join(' ') %><%- } -%>;
15:
16: root <%= $root_dir %>;
1:>> $https
=> false
2:>> $root_dir
=> "/var/www/html"
Each breakpoint stops the parser and injects a debugger session in a stack life fashion. Type exit
to pop off each breakpoint session. The last session in the stack will exit the debugger
Important
It is important to note that when inside the template you are inside the puppet parser so you can’t execute template code directly. But you can execute any kind of puppet code like function calls, variable interpolation and even calls to other epp templates.
So you can’t execute <%= $server_name %>
but you can execute $server_name
.
From file: init.pp
9: 'server_name' => $server_name,
10: 'listen_port' => $listen_port,
11: 'aliases' => $aliases,
12: 'root_dir' => $root_dir,
13: 'https' => $https
14: })
15:
=> 16: debug::break()
17:
18: file { "/tmp/${server_name}.conf":
19: ensure => file,
20: content => $template_content,
21: }
22: }
6. Why This Workflow Rocks
- Debug both manifests and templates interactively
- Inspect variables on the fly
- Execute functions on the fly
- Catch syntax and logic errors instantly
- No need to run full unit tests for every small edit
Always have unit tests
The debugger is NOT a replacement for unit tests. In complicated modules you will want to test your code across various situations and versions of puppet and unit tests are the best tool for that.
7. Tips
- Always remove breakpoints before commiting code!
- Use conditional breakpoints for development only:
if $facts['env'] == 'dev' { debug::break() }
- You can place multiple
debug::break()
calls in different files to compare scope in each location.
With puppet-debugger
and nwops-debug
, you can make EPP template development fast and fun again.