The 60 Puppet questions interviewers actually ask, with direct answers, runnable manifests, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Puppet is an open-source configuration management tool, first released by Luke Kanies in 2005, that keeps servers in a defined state. You write manifests in Puppet's declarative language describing resources like packages, files, and services, and Puppet figures out the steps to reach that state and enforces it on every run. It's model-driven and idempotent: applying the same manifest twice changes nothing the second time. In interviews, Puppet questions probe the run lifecycle (facts gathered, catalog compiled, resources applied, report sent), the master-agent architecture, and design patterns like Roles and Profiles, not trivia. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're still building fundamentals, the official Puppet Core documentation from Puppet by Perforce is the canonical reference; increasingly the first DevOps round runs as a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Puppet Full Course: Learn Puppet Training, Puppet Tutorial for Beginners
Video: Puppet Full Course: Learn Puppet Training, Puppet Tutorial for Beginners (edureka!, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Puppet certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Puppet is a configuration management tool that keeps servers in a defined state. You describe what a system should look like in manifests, and Puppet enforces that state on every run, correcting anything that drifted since last time. It's declarative, so you specify the end result and Puppet works out the steps.
The problem it solves is consistency at scale: managing hundreds of servers by hand leads to snowflake machines that behave differently. Puppet makes configuration repeatable, versioned, and self-correcting, so a new server matches the fleet the moment it registers.
# a tiny manifest: install nginx and keep it running
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
enable => true,
}Key point: A one-line definition plus the word 'drift correction' beats a feature list. Interviewers open with this to hear how you frame the tool's purpose.
Declarative means you describe the end state you want, not the ordered steps to get there. You say the package should be installed and the service running; Puppet inspects the current state and works out whether to install, upgrade, or leave it alone. You describe the destination, not the route.
This contrasts with imperative or procedural tools where you write the exact commands in order. The declarative model is what makes Puppet idempotent and lets the same manifest safely run again and again.
Key point: The follow-up is almost always 'how is that different from a shell script?'. Have the idempotency point ready.
Idempotency means applying the same configuration repeatedly produces the same result: after the first run reaches the desired state, later runs change nothing. Puppet reports zero changes when a node already matches its catalog, and only acts again when something has drifted away from that desired state.
It matters because agents run on a schedule (every 30 minutes by default). Without idempotency, each run would repeat actions, restart services needlessly, and never settle. Idempotency is what makes continuous enforcement safe.
Key point: Interviewers love a concrete example: a file resource writing the same content twice makes no second change. Say that out loud.
A resource is the smallest unit of configuration: a single piece of system state like a package, a file, a user, or a service. Every resource has a type, a title, and attributes describing its desired state, and Puppet reads all three to decide what, if anything, to change.
Resources are the fundamental building block; everything else (classes, modules, manifests) organizes collections of them. Puppet ships many built-in resource types and you can add more with modules.
# resource type { 'title': attribute => value }
file { '/etc/motd':
ensure => file,
content => "Managed by Puppet\n",
owner => 'root',
mode => '0644',
}Key point: Being able to The three parts (type, title, attributes) from memory indicates someone who has actually written manifests.
The everyday set is package, file, and service, so common the trio has a name of its own: package-file-service. Add user, group, cron, exec, notify, and mount to that base, and you can express most of the system configuration a normal server needs without reaching for anything custom.
Each type has its own attributes. A package cares about ensure and provider; a service cares about ensure and enable; a file cares about content, source, owner, and mode.
Key point: If you mention exec, immediately say it's a last resort. Reaching for exec first is a common junior tell.
It's the canonical Puppet idiom for managing a piece of software: install the package, manage its config file, and run the service, wired so a config change restarts the service. The three resources chain together with require and notify, capturing the full install-configure-run lifecycle in a handful of lines.
It captures the whole lifecycle in a few lines and is idempotent. Interviewers ask you to write it because it exercises resources, attributes, and ordering at once.
package { 'nginx':
ensure => installed,
}
file { '/etc/nginx/nginx.conf':
ensure => file,
source => 'puppet:///modules/nginx/nginx.conf',
require => Package['nginx'],
notify => Service['nginx'],
}
service { 'nginx':
ensure => running,
enable => true,
}Key point: Writing this cleanly from memory, with require and notify in the right place, is a strong freshers-level signal.
A manifest is a file with a .pp extension containing Puppet code: resources, classes, and logic. It's the unit you write and Puppet reads, kept in version control like any source file. The main manifest for a node defines what that node should look like once Puppet finishes applying it.
Manifests live inside modules or in the site manifest. They're plain text, kept in version control, and compiled into a catalog per node.
# site.pp assigns configuration to nodes
node 'web01.example.com' {
include profile::webserver
}
node default {
include profile::base
}A class is a named, reusable block of resources you define once and include wherever it's needed. Defining a class alone doesn't apply it; you have to declare it with include or a resource-like class declaration to actually put it in the catalog. A class is included at most once per node.
Classes group related configuration (everything for NTP, say) so you can assign it to many nodes without repeating resources. A class is included at most once per node.
class ntp {
package { 'ntp': ensure => installed }
service { 'ntpd':
ensure => running,
enable => true,
require => Package['ntp'],
}
}
# apply it to a node
include ntpKey point: The distinction the key signal is: defining a class versus declaring it. Defining alone does nothing until it's included.
A module is a self-contained bundle of everything needed to manage one thing: manifests, files, templates, and metadata, in a fixed directory layout Puppet knows how to autoload. Modules are how Puppet code is packaged and shared, including on the Puppet Forge.
The layout matters because Puppet autoloads classes by path. A class named nginx::config must live at nginx/manifests/config.pp.
nginx/
manifests/
init.pp # class nginx
config.pp # class nginx::config
files/ # static files served to nodes
templates/ # .epp or .erb templates
metadata.json # name, version, dependenciesKey point: Knowing that manifests/init.pp maps to the class named after the module shows real hands-on time.
The Puppet server (master) stores manifests and compiles catalogs on demand. Each managed node runs an agent that connects over HTTPS, sends its facts, receives a compiled catalog built specifically for it, applies the resources locally, and returns a report of what changed. The server never touches the node directly.
It's a pull model: agents initiate the connection on a schedule, the server doesn't push. This scales because the server only compiles when asked, and agents self-heal even if they were offline during a change.
# trigger an agent run manually instead of waiting for the interval
sudo puppet agent --test
# see the run without changing anything
sudo puppet agent --test --noopKey point: Say 'pull, not push' explicitly. It's the one-word difference from Ansible that interviewers are checking you know.
Watch a deeper explanation
Video: Learn Configuration Management Using Puppet (edureka!, YouTube)
The agent gathers facts with Facter and sends them to the server. The server compiles a catalog from the manifests and those facts. The agent receives the catalog, applies each resource in dependency order, and sends a report back to the server.
This four-stage cycle (facts, compile, apply, report) repeats every run interval. Understanding it is how you debug: a failure sits in exactly one of those stages.
The Puppet agent run, stage by stage
When a run misbehaves, ask which stage: a bad fact, a compile error, an apply failure, or a reporting gap.
Key point: Naming the four stages in order is the whole point. Debugging questions later assume you can place a failure in one of them.
Facter is Puppet's system inventory tool. It gathers facts, the key details about a node like operating system, IP address, memory, kernel, and hostname, and makes them available as variables inside your manifests so your code can adapt to the machine it's running on.
Facts drive conditional logic: install a different package on Debian than on Red Hat by reading the os fact. You can also write custom facts for values Facter doesn't collect by default.
# branch on the OS family fact
case $facts['os']['family'] {
'Debian': { $pkg = 'apache2' }
'RedHat': { $pkg = 'httpd' }
default: { fail('Unsupported OS') }
}
package { $pkg: ensure => installed }Key point: Use the $facts['os']['family'] hash syntax, not the old top-scope $osfamily. The modern form signals current knowledge.
A catalog is the compiled document the server produces for one specific node: the full list of resources, their desired states, and the dependencies between them, with all logic and conditionals already resolved. It's the concrete, node-specific plan the agent executes, not the manifests themselves.
The agent doesn't read your manifests directly; it applies the catalog. That separation is why compilation errors happen on the server and application errors happen on the agent.
Key point: The insight to volunteer: the agent never sees your raw manifests, only the compiled catalog. It explains where different errors surface.
ensure describes the desired existence or state of a resource. For a package it can be installed, latest, absent, or a specific version. For a file it can be file, directory, link, or absent. For a service it can be running or stopped.
It's the most common attribute because it answers the basic question: should this thing exist, and in what form? Puppet compares current state to the ensure value and acts only if they differ.
| Resource type | Common ensure values | Meaning |
|---|---|---|
| package | installed, latest, absent, '1.2.3' | install, keep newest, remove, or pin a version |
| file | file, directory, link, absent | manage as a file, folder, symlink, or delete it |
| service | running, stopped | start and keep up, or stop and keep down |
| user | present, absent | create the account or remove it |
Key point: Knowing that ensure => latest upgrades on every run (and why that can surprise people) is a nice bit of depth.
By declaring relationships between resources rather than relying on file order. The metaparameters before, require, notify, and subscribe wire resources together, and Puppet applies them in the resulting dependency order, so a config file can be guaranteed to exist before the service that reads it starts.
require and before set ordering; notify and subscribe add a refresh (restart the service when the config file changes). Chaining arrows (-> and ~>) express the same relationships between resource references.
# two ways to say the same thing
file { '/etc/app.conf':
ensure => file,
notify => Service['app'],
}
# chaining arrow form: apply file, then refresh service
File['/etc/app.conf'] ~> Service['app']Key point: Being able to convert between metaparameters and chaining arrows on the spot is a common live exercise.
They set up the same refresh relationship from opposite ends. notify placed on a file tells a service to refresh when that file changes. subscribe placed on the service says the same thing from the service's side, watching the file. The result is identical; only the direction you write it differs.
Refresh means the target's refresh action runs (a service restarts). Use whichever direction reads more naturally: notify from the thing that changed, or subscribe from the thing that reacts.
# equivalent: config change restarts the service
file { '/etc/app.conf': notify => Service['app'] }
service { 'app':
ensure => running,
subscribe => File['/etc/app.conf'],
}Key point: The word to use is refresh. the key signal is it because notify does not always mean restart; it triggers the resource's refresh action.
Variables a dollar sign and hold a value assigned once per scope; you can't reassign a variable in the same scope, which surprises people from other languages comes first. Scopes are nested: a top scope, then node scope, then each class or defined type gets its own scope inside that.
Reference a variable from another class with its fully qualified name ($ntp::server). This explicit scoping avoids the confusing dynamic scoping that older Puppet versions allowed.
$greeting = 'hello'
# $greeting = 'hi' # error: cannot reassign in same scope
class web ( $port = 80 ) {
notify { "listening on ${port}": }
}Key point: Say variables are immutable within a scope. Candidates who think Puppet variables work like other languages get tripped up here.
Four main forms: if/elsif/else for boolean logic, case for matching one value against several options, selectors for choosing a value inline, and unless as the inverse of if. All of them commonly branch on facts, so your manifest installs the right package or picks the right path per operating system.
Selectors are the concise choice when you're just picking a value; case and if suit blocks of resources. Puppet has no loops in the old sense but supports iteration with each and map.
# selector: pick a value based on a fact
$conf_dir = $facts['os']['family'] ? {
'Debian' => '/etc/apache2',
'RedHat' => '/etc/httpd',
default => '/etc/http',
}puppet apply runs a manifest locally, with no server involved; it compiles and applies on the same machine. It's great for testing, single nodes, or a masterless setup. puppet agent is the daemon that contacts a Puppet server, pulls a catalog, and applies it.
In short: apply is standalone, agent is the client half of master-agent. Interviews use this to check you understand that Puppet can run without a central server.
# masterless: compile and apply one manifest locally
sudo puppet apply /etc/puppetlabs/code/site.pp
# agent: pull from the configured server and apply
sudo puppet agent --testKey point: masterless (puppet apply driven by version control) matters.
site.pp is the main entry-point manifest where node definitions live. A node block matches a node's certificate name and lists the classes or roles that node should get, and a node default block catches anything unmatched. It's the first place the compiler looks to decide what a given machine should become.
Modern practice keeps site.pp thin: each node gets exactly one role via node classification, and the role includes profiles. Many teams classify nodes in the console or with an external node classifier instead of long node blocks.
node 'db01.example.com' {
include role::database
}
node /^web\d+\.example\.com$/ { # regex match
include role::webserver
}
node default {
include role::base
}Templates generate file content dynamically by mixing static text with variables and a bit of logic. Puppet uses EPP, its own template language, and still supports the older ERB. You render a template into a file resource's content, so one template produces the right config for every node it runs on.
Use them when a config file needs values that vary per node: a port, a hostname, a list of upstreams. Instead of shipping many near-identical files, you ship one template and feed it data.
# manifest side: render an EPP template with parameters
file { '/etc/app/app.conf':
ensure => file,
content => epp('app/app.conf.epp', {
'port' => $port,
'host' => $facts['networking']['hostname'],
}),
}Key point: Knowing EPP is the current default and ERB is the legacy Ruby option is enough at this level.
The first time an agent contacts the server, it generates a key pair and sends a certificate signing request. Until an admin signs that request on the server, the agent can't get a catalog. Signing establishes trusted, encrypted communication over HTTPS.
You sign requests with puppetserver ca sign. Autosigning can automate this in trusted environments, but signing by hand (or with policy-based autosigning) is the secure default because a signed cert is a trusted node.
# on the server: list and sign pending requests
sudo puppetserver ca list
sudo puppetserver ca sign --certname web01.example.comKey point: Security-minded interviewers ask about blanket autosigning. The safe answer: avoid it in production, or use policy-based autosigning with a shared secret.
The Forge is the public repository of Puppet modules, both community-authored and Puppet-supported. Instead of writing a module for common software like Apache or MySQL from scratch, you install a vetted one from the Forge and configure it through its parameters, which saves real time and inherits others' hard-won fixes.
You install modules with puppet module install or, better for teams, pin them in a Puppetfile managed by r10k or Code Manager so every environment gets the same versions.
# quick install
puppet module install puppetlabs-apache
# team practice: pin in a Puppetfile
# mod 'puppetlabs-apache', '11.1.0'Key point: Say you pin module versions in a Puppetfile. Installing modules ad hoc on the server is a junior habit interviewers probe for.
include ntp adds a class to the catalog and can be called many times safely; every include of the same class is the same single instance, and parameters come from Hiera or defaults. The resource-style form, class { 'ntp': servers => [...] }, lets you pass parameters explicitly but can be declared only once per node.
The practical rule: prefer include and let Hiera supply parameters, because two resource-style declarations of the same class collide with a duplicate-declaration error. Reach for the resource-style form only when you must set parameters in code and you control that it happens once.
# safe anywhere, parameters via Hiera or defaults
include ntp
# explicit parameters, but only once per node
class { 'ntp':
servers => ['10.0.0.1', '10.0.0.2'],
}Key point: The trap is declaring the same class resource-style twice. Saying 'prefer include, let Hiera parameterize' shows you've hit that duplicate-declaration wall.
No-op (--noop) is a dry run: Puppet compiles the catalog and reports every change it would make, without actually changing anything on the node. It's how you preview the effect of a manifest before it touches production, turning a risky apply into a safe, reviewable diff you can read first.
Use it after a code change to confirm the diff is what you expect, and to audit drift: run no-op on a node and see what Puppet would correct. It turns a risky apply into a safe preview.
# preview changes without applying them
sudo puppet agent --test --noopKey point: No-op is the answer to 'how do you safely test a change on a live server?'. Reach for it before anyone asks.
For candidates with working experience: language mechanics, data separation, and the patterns that separate users from designers.
It's the standard way to organize Puppet code into two layers. A profile wraps a component module (or several) with your site-specific data: a profile::webserver configures Apache with your ports and vhosts. A role is a business-level description that includes one or more profiles: role::frontend includes profile::webserver and profile::base.
Each node gets exactly one role. This keeps node classification simple (one role per node), pushes technology config into profiles, and leaves reusable component modules generic. It's the most-tested Puppet design question.
# profile: component module plus site data
class profile::webserver {
class { 'apache': default_vhost => false }
apache::vhost { 'app':
port => 443,
docroot => '/var/www/app',
}
}
# role: only includes profiles
class role::frontend {
include profile::base
include profile::webserver
}Key point: The rule to state plainly: roles include only profiles, profiles include component modules, nodes get one role. Interviewers grade this almost verbatim.
Watch a deeper explanation
Video: Puppet Tutorial for Beginners: Puppet Tutorial (Intellipaat, YouTube)
Hiera is Puppet's hierarchical data lookup. It stores configuration data in YAML files arranged in a hierarchy (per node, per environment, then a common default) and looks values up from most specific to most general. Puppet automatically binds Hiera data to class parameters.
Separating data from code means one module works everywhere: the code says 'listen on this port', and Hiera supplies 80 in one environment and 8080 in another. You change data without touching logic, which keeps modules reusable and reviews clean.
# data/common.yaml
profile::webserver::port: 80
# data/nodes/web01.example.com.yaml
profile::webserver::port: 8080Key point: The phrase the question needs is automatic parameter lookup: a class parameter named profile::webserver::port is filled from Hiera without an explicit lookup call.
Watch a deeper explanation
Video: Puppet Tutorial: Puppet Configuration Management Tool (Simplilearn, YouTube)
Hiera walks its hierarchy top to bottom and, by default, returns the first match it finds. The hierarchy is ordered most specific to least specific: a per-node file, then per-role or per-OS, then a common file at the bottom. A more specific layer overrides a general one.
The hierarchy is defined in hiera.yaml, where each level uses facts to build a file path. You can also merge across layers (hash or array merges) instead of first-match when you want values to combine.
# hiera.yaml
version: 5
hierarchy:
- name: 'Per-node data'
path: "nodes/%{trusted.certname}.yaml"
- name: 'Per-OS family'
path: "os/%{facts.os.family}.yaml"
- name: 'Common defaults'
path: 'common.yaml'Key point: Mention merge behavior (first-found versus hash/array merge). Knowing you can combine layers, not just override, indicates real Hiera experience.
A defined type is a reusable block you can declare many times per node with different titles, like a resource. A class can be included only once per node. Use a defined type when you need multiple instances: three vhosts, five cron jobs, ten users.
You define it with the define keyword and give it parameters; each declaration gets a unique title. Under the hood it behaves like a custom resource type built from other resources.
define app::vhost ( String $docroot, Integer $port ) {
file { "/etc/nginx/sites/${title}.conf":
ensure => file,
content => epp('app/vhost.epp', { 'root' => $docroot, 'port' => $port }),
}
}
app::vhost { 'blog': docroot => '/var/www/blog', port => 80 }
app::vhost { 'shop': docroot => '/var/www/shop', port => 8080 }| Class | Defined type | |
|---|---|---|
| Declarations per node | Once | Many, with unique titles |
| Keyword | class | define |
| Typical use | One-time configuration | Repeated items (vhosts, users) |
| Declared with | include or class {} | type-style: app::vhost { ... } |
Key point: The one-liner the question needs: use a class for something a node has once, a defined type for something it has many of.
A parameterized class declares typed parameters with optional defaults, so the same class configures differently depending on the values passed at declaration time. Parameters are what make a module reusable across environments without editing its code: one class, many configurations, driven by data instead of copies.
Values come from three places, in order: an explicit resource-style class declaration, automatic Hiera lookup by the fully qualified parameter name, then the default in the class signature. Preferring Hiera keeps declarations clean and data centralized.
class ntp (
Array[String] $servers = ['pool.ntp.org'],
Boolean $enabled = true,
) {
# ... uses $servers and $enabled
}
# explicit values
class { 'ntp': servers => ['10.0.0.1'], enabled => true }
# or let Hiera fill ntp::servers automatically via include ntpKey point: Naming the resolution order (explicit, then Hiera, then default) is the depth interviewers are checking for.
Puppet has a real type system: core types like String, Integer, Float, Boolean, Array, and Hash, plus abstract types like Optional, Enum, Variant, and Pattern. You annotate class and defined-type parameters with these to validate input at compile time, so bad data fails fast with a clear message.
Typed parameters catch mistakes early: pass a string where an Integer is required and compilation fails with a clear message instead of a strange runtime bug. Enum and Pattern constrain allowed values further.
class web (
Integer[1, 65535] $port,
Enum['http', 'https'] $scheme = 'https',
Optional[String] $doc_root = undef,
) {
# compilation fails if $port is out of range or $scheme is unexpected
}Key point: Showing a constrained type like Integer[1, 65535] or Enum turns a plain answer into one that indicates production discipline.
With iteration functions on arrays and hashes: each for side effects like declaring one resource per item, map to transform a list, filter to select from it, and reduce to fold it to a single value. These replaced the old create_resources approach for most cases and read far more clearly in modern code.
The common pattern is looping over a hash of data (often from Hiera) and declaring one resource per entry. That's how you turn a list of users or vhosts in data into resources in code.
$users = ['asha', 'ben', 'cara']
$users.each |String $name| {
user { $name:
ensure => present,
shell => '/bin/bash',
}
}exec runs an arbitrary command and is the escape hatch for something no resource type covers. It's appropriate as a last resort, and only when you make it idempotent with creates, onlyif, or unless so it doesn't run on every Puppet run.
The risk is that exec is not declarative: it runs a command instead of describing state, so it breaks idempotency if you forget a guard, and it hides intent. Reviewers treat unguarded exec as a red flag and ask whether a real resource type would fit.
exec { 'build-cache':
command => '/usr/bin/app rebuild-cache',
creates => '/var/cache/app/index', # skip if this file exists
path => ['/usr/bin', '/bin'],
}Key point: The moment you mention exec, mention its guards (creates, onlyif, unless). An unguarded exec is the fastest way to look junior.
An environment is an isolated set of modules, manifests, and data, so you can serve different code to different nodes: a production environment, a testing environment, per-feature branches. Each environment is a directory the server loads independently, which means a change in testing can't affect production nodes until you promote it.
The common workflow ties environments to Git branches with r10k or Code Manager: a branch named feature-x becomes an environment named feature-x, letting you test a change on a few nodes before merging to production. This is how teams promote code safely.
# run an agent against a specific environment for testing
sudo puppet agent --test --environment testingKey point: Linking environments to Git branches via r10k is the workflow answer interviewers hope for. It shows you've done real code promotion.
Both deploy Puppet code from version control into environments. You define modules and versions in a Puppetfile and map Git branches to environments; the tool clones the branches, installs the pinned modules, and lays out the environment directories on the server.
r10k is the standalone tool; Code Manager is the integrated version in Puppet Enterprise with an API and webhooks. Either way the point is the same: no more editing modules on the server by hand, everything flows from Git.
# Puppetfile
forge 'https://forge.puppet.com'
mod 'puppetlabs-apache', '11.1.0'
mod 'puppetlabs-stdlib', '9.6.0'
mod 'internal_app',
git: 'git@github.com:acme/internal_app.git',
ref: 'v2.3.0'Key point: The keyword is Git-driven deployment. Editing code directly on the Puppet server is the anti-pattern these tools exist to kill.
EPP is Puppet's native template format; it uses Puppet language syntax and takes explicit typed parameters, so a template declares exactly what data it needs. ERB is the older format that embeds Ruby, using top-scope variable access rather than passed parameters.
Prefer EPP in new code: explicit parameters make templates easier to reason about and validate, and you're writing Puppet, not Ruby. ERB is still common in older modules, which is why you should recognize both.
| EPP | ERB | |
|---|---|---|
| Language inside | Puppet | Ruby |
| Data passing | Explicit typed parameters | Implicit scope variables |
| Status | Current default | Legacy, still supported |
| File extension | .epp | .erb |
Key point: Say EPP takes explicit parameters. That single fact is the cleanest way to show you know why it's preferred.
Two ways. External facts are the simplest: drop a script or a static YAML/JSON file in a facts.d directory and its output becomes a fact, no Ruby needed. Custom facts written in Ruby live in a module's lib/facter directory and can run logic to compute a value.
Use external facts for quick, language-agnostic values (a script that echoes a data-center name). Reach for Ruby custom facts when the value needs real logic or must be confined to certain platforms.
# module_name/lib/facter/app_role.rb
Facter.add('app_role') do
setcode do
File.exist?('/etc/app/frontend') ? 'frontend' : 'backend'
end
endKey point: Knowing external facts (no Ruby) versus custom facts (Ruby in lib/facter) is the distinction interviewers probe here.
Resource defaults set default attribute values for a resource type within a scope, using the capitalized type name with no title. Every File declared after it inherits those defaults unless it overrides them. Modern style prefers per-resource explicitness, but you'll see defaults in existing code.
Resource collectors use the space-ship syntax to select already-declared resources by attribute and act on them, often to add relationships or override values in bulk. They're occasionally handy and occasionally a source of confusing action-at-a-distance.
# resource default: applies to File resources in this scope
File {
owner => 'root',
mode => '0644',
}
# collector: realize every File whose mode is 0600
File <| mode == '0600' |>Key point: Mention that resource defaults can cause surprises across a large scope. Awareness of the downside is what makes the answer senior-leaning.
A virtual resource is declared but not applied until you realize it, which lets you define a resource in one place and activate it selectively. Exported resources go further: a node declares a resource meant for a different node, which then collects it. This needs PuppetDB to store the exported resources.
The classic exported-resource use is service discovery: each web node exports its address, and the load balancer collects all of them to build its backend pool. It's node-to-node coordination without hardcoding.
# on each web node: export a load-balancer member
@@haproxy::balancermember { $facts['networking']['fqdn']:
listening_service => 'web',
server_names => $facts['networking']['hostname'],
ipaddresses => $facts['networking']['ip'],
ports => '8080',
}
# on the load balancer: collect them all
Haproxy::Balancermember <<| listening_service == 'web' |>>Key point: The trigger phrase is service discovery, and the requirement is PuppetDB. Naming both marks you as someone who has run this in anger.
PuppetDB is a data store that collects everything Puppet generates: catalogs, facts, reports, and exported resources. The server writes to it on each run, and you query it for inventory, reporting, and cross-node features. It's what turns a fleet of independent agents into something you can ask questions about centrally.
It enables exported resources, fast fact queries across the fleet, and reporting dashboards. Tools like puppetdb query and the PuppetDB API let you answer questions like 'which nodes run this package version?' without touching each node.
Key point: PuppetDB maps to a concrete feature (exported resources or fleet-wide fact queries). Naming it without a use case indicates memorized.
After a run, Puppet prints a summary of resources: how many were changed, failed, or skipped. A truly idempotent, converged node reports zero changed resources on a second consecutive run. Any resource that changes every run is a bug, usually an unguarded exec or a file whose content is nondeterministic.
The practical test: apply twice in a row. If the second run reports changes, hunt down the resource that isn't idempotent. Reports also flag corrective changes (drift Puppet fixed) versus intentional ones.
Key point: The two-consecutive-runs test is the answer to 'how do you know your manifest is idempotent?'. State it as a method, not a guess.
A dependency cycle happens when resources require each other in a loop: A requires B, B requires C, and C requires A. Puppet can't pick an order that satisfies all of them, so catalog application fails outright with a dependency-cycle error that names the resources tangled in the loop.
Fix it by finding the loop (Puppet lists the resources, and --graph produces a visual) and removing the relationship that doesn't belong. Usually one require or notify is redundant or points the wrong way. Rethink which resource genuinely depends on which.
# generate dependency graphs to spot the cycle
sudo puppet agent --test --graph
# .dot files land under the client graph directory; render with graphvizKey point: Knowing --graph exists and produces a visual is the practical detail that separates reading about cycles from debugging one.
You commit a change to a Git branch, and that branch maps to a Puppet environment. r10k deploys the branch: it clones the code and installs the Puppetfile's pinned modules into the environment on the server. You test by pointing a canary node at that environment with a no-op run, then merge to the production branch, which r10k deploys for the fleet.
The whole point is that code flows from Git to environments automatically, with a test environment gating the change before it reaches every node. Nobody edits modules on the server by hand.
From commit to production with r10k
Environments plus r10k are what let you promote code branch by branch instead of editing the server directly.
Key point: the question needs the Git-branch-to-environment mapping named explicitly, plus the canary no-op step before merge. That sequence is the modern Puppet delivery answer.
Layer it. puppet parser validate and puppet-lint catch syntax and style. rspec-puppet runs unit tests that compile a catalog and assert resources are present with the right parameters, without touching a real machine. Litmus or Beaker run acceptance tests that actually apply code to a container or VM.
In a pipeline, lint and unit tests run on every commit, acceptance tests on merge, and no-op runs verify the change on a canary node before full rollout. Saying this progression signals you've worked in a real Puppet delivery pipeline.
# spec/classes/ntp_spec.rb (rspec-puppet)
require 'spec_helper'
describe 'ntp' do
it { is_expected.to compile.with_all_deps }
it { is_expected.to contain_package('ntp').with_ensure('installed') }
it { is_expected.to contain_service('ntpd').with_ensure('running') }
endKey point: Naming the ladder (lint, rspec-puppet, acceptance, no-op canary) is what turns 'I test my code' into a credible pipeline answer.
advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
The server receives the agent's facts, loads the node's environment (modules, manifests, hiera.yaml), evaluates the classes assigned to that node, resolves every parameter (explicit values, then Hiera, then defaults), and produces a fully realized graph of resources with dependencies. The output is the catalog: no logic left, just concrete resource states and ordering.
Compilation is where most non-apply errors live: a missing class, a Hiera lookup with no value and no default, a type mismatch, a duplicate resource declaration. Because facts feed compilation, the same manifest can compile to different catalogs on different nodes.
Key point: The production insight: compilation is data-dependent (facts and Hiera), so a manifest that compiles on one node can fail on another. Say that and you sound like you've debugged it.
Watch a deeper explanation
Video: Puppet Full Course: Learn Puppet In 1 Hour, Puppet Tutorial For Beginners (Simplilearn, YouTube)
Choose Puppet for large, long-lived fleets that need continuous state enforcement: agents re-apply on a schedule and correct drift with no human in the loop, and the declarative model plus PuppetDB gives you strong reporting and inventory. Choose Ansible for orchestration, ad hoc tasks, and environments where installing an agent is a non-starter, since it's agentless over SSH and pushes on demand.
The honest answer is they overlap and many shops run both: Puppet for baseline server state, Ansible for deployment and one-off orchestration. Picking on continuous-enforcement versus on-demand-push, and on agent versus agentless, is the reasoning the question needs.
| Dimension | Puppet | Ansible |
|---|---|---|
| Enforcement | Continuous, scheduled pull | On-demand push, no self-heal between runs |
| Agent | Requires an agent | Agentless, uses SSH |
| Model | Declarative end-state | Procedural task list |
| Strong fit | Baseline state on large fleets | Orchestration, deploys, ad hoc runs |
| Reporting | PuppetDB, rich inventory | Lighter out of the box |
Key point: Refusing to declare one universally 'better' and instead giving the decision axes (enforcement model, agent, reporting) is the mature answer.
Compilation is the bottleneck, so scale the compilers: run multiple compile servers behind a load balancer, all backed by a shared CA and PuppetDB. Tune the agent run interval and use splay so agents don't all check in at once and stampede the servers. Keep catalogs small and Hiera lookups efficient because both cost compile time.
Beyond that: cache Forge modules, watch PuppetDB sizing since it stores every report, and consider environment caching so the server isn't re-reading code on each request. The framing the question needs is that scaling Puppet means scaling and protecting catalog compilation.
Key point: Leading with 'compilation is the bottleneck' frames the whole answer correctly. Everything else is protecting or scaling that step.
The tension is real: enforced runs fix drift automatically, but a bad manifest then enforces a bad state fleet-wide. The controls are staged environments (test on a branch environment first), no-op canary runs before enforcing, and rolling out via a small node subset before the whole fleet.
Some teams run production in no-op and only enforce after review of the diff, trading immediacy for a human gate. Others enforce continuously but gate code with strong CI. The right coverage names the trade-off and the guardrails, rather than treating auto-enforcement as unconditionally safe.
Key point: the question needs the trade-off named (auto-fix versus blast radius) plus concrete guardrails, not a claim that enforcement is always safe.
When you're managing something no existing type covers and you want it to behave like a first-class resource: idempotent, with a clear model of desired versus current state, not a fragile exec. The type defines the interface (properties and parameters); the provider implements how to read and set state on a given platform.
It's real Ruby work and worth it for something you manage repeatedly across many nodes: a proprietary appliance's config, a specific API-driven object. For a one-off, an exec with guards or a defined type wrapping existing resources is usually the right call instead.
# lib/puppet/type/app_widget.rb
Puppet::Type.newtype(:app_widget) do
ensurable
newparam(:name, namevar: true)
newproperty(:color)
end
# a matching provider reads and sets the widget's real stateKey point: The senior tell is knowing when NOT to: a custom type is Ruby maintenance, so justify it by reuse across many nodes, not novelty.
Never in plain manifests or plain Hiera. The classic approach is hiera-eyaml, which encrypts values in Hiera files so the data stays in Git but is unreadable without the key; the server decrypts at lookup time. Modern setups often back Hiera with an external secret store like Vault through a Hiera backend, so secrets never live in the repo at all.
Either way the principles hold: encrypt at rest, keep decryption keys off the repo and on the server or secret manager, rotate them, and limit who can read them. Saying 'eyaml or a Vault backend, and keys stay out of Git' is the answer the question needs.
# hiera-eyaml: the value is encrypted, safe to commit
profile::db::password: >
ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXY...==]Key point: The disqualifying answer is plaintext passwords in Hiera. Lead with eyaml or a Vault-backed backend and the rule that keys never enter Git.
Assess first: which Puppet version, is it node-block classification with no Roles and Profiles, is data hardcoded in manifests instead of Hiera, are there unguarded execs and dynamic scope. Then migrate incrementally: introduce Roles and Profiles as a wrapping layer, extract hardcoded data into Hiera, add rspec-puppet tests around modules before refactoring them, and set up r10k so deployment leaves the server.
The failure mode to name is a big-bang rewrite that stalls. Wrap and ratchet: new nodes use the new pattern, old ones migrate module by module behind tests, and CI prevents backsliding.
Key point: the technical value is incrementalism plus tests-first. A rip-and-replace answer signals someone who hasn't lived through a migration.
Read the error and place it in a stage: compile-time (missing class, Hiera miss, type error, duplicate declaration) or apply-time (a resource failed on the node). Reproduce with puppet agent --test on one affected node for the full message, and diff the merge to find what changed. If it's a compile error, it reproduces on the server; check the environment and Hiera data the new code expects.
Then contain the blast radius: roll back the environment or revert the merge so the fleet stops failing, fix on a branch environment, verify with a no-op run on a canary, and re-deploy. The structure (identify stage, reproduce, contain, fix, verify) matters more than any single command.
Key point: The methodology is; the first move is always placing the failure in compile versus apply.
They solve different layers. Terraform provisions infrastructure: it creates the servers, networks, and cloud resources and tracks them in state. Puppet configures what runs inside those servers: packages, files, services, and their ongoing state. Terraform is about existence of infrastructure; Puppet is about configuration on it.
They complement more than compete: Terraform stands up the VMs, Puppet configures and maintains them. On immutable-infrastructure teams that rebuild images instead of mutating servers, Puppet's role shrinks (config baked into images) while Terraform's stays. Framing them by layer, not as rivals, is the accurate answer.
| Terraform | Puppet | |
|---|---|---|
| Concern | Provisioning infrastructure | Configuring inside servers |
| State | Tracks resource state file | Enforces desired state each run |
| Typical output | VMs, networks, cloud resources | Packages, files, services |
| Relationship | Creates the machine | Configures and maintains it |
Key point: Saying 'Terraform provisions, Puppet configures' and calling them complementary is the framing that indicates real infrastructure experience.
In immutable infrastructure you don't mutate running servers; you build a new image and replace them. Puppet's continuous drift correction matters less there, but Puppet still earns its place at build time: run it during image creation (with Packer, say) to configure the golden image declaratively, using the same modules the mutable fleet uses.
So the honest answer is that immutable infrastructure narrows Puppet's job from lifelong enforcement to image construction, and the two models often coexist: immutable for stateless app tiers, Puppet-enforced for long-lived stateful or legacy systems.
Key point: The nuanced take: immutable infra doesn't kill Puppet, it moves it to build time. That reframing is what senior interviewers are digging for.
An ENC is any external system that tells the Puppet server which classes, parameters, and environment a node should get, instead of encoding that in node blocks or site.pp. The server calls the ENC (a script or the Puppet Enterprise console) with the node name and gets back its classification as data.
Use one when classification should live outside Puppet code: a CMDB, an inventory system, or the PE console owns the mapping of node to role. It decouples 'what a node is' from the Puppet codebase, which suits large orgs where a different team manages node inventory.
# an ENC returns YAML like this for a given node
environment: production
classes:
role::database:
parameters:
datacenter: 'us-east'Key point: The concept to land is decoupling classification from code. Naming a real source (CMDB, PE console) shows you know why an ENC exists.
Resource types abstract the what from the how: a package resource means 'install this software' regardless of OS, and a provider implements it with apt on Debian, yum or dnf on Red Hat, and so on. Facter feeds the OS facts that let the right provider and your conditionals pick platform-specific values.
The abstraction leaks where platforms genuinely differ: service names (ntpd versus ntp), config paths, and packages with different names. That's why real modules branch on the os fact for those specifics even though the resource type is portable. Knowing both the abstraction and its limits is the point.
$service_name = $facts['os']['family'] ? {
'Debian' => 'ntp',
'RedHat' => 'ntpd',
default => 'ntp',
}
service { $service_name: ensure => running, enable => true }Key point: Providers are the mechanism behind cross-platform types. Mentioning them by name, and where the abstraction leaks, is the depth the technical value is.
Bolt is Puppet's agentless task runner. It runs ad hoc commands, scripts, and tasks over SSH or WinRM without needing a Puppet agent or server, and it can also apply Puppet manifests directly. It fills the orchestration and one-off-task gap that pull-based Puppet doesn't cover well.
So they're complementary: Puppet enforces ongoing desired state on a schedule; Bolt handles imperative, on-demand work like a deployment step, a restart across a group, or bootstrapping nodes. Teams often use Bolt for orchestration alongside Puppet for state, similar to how others pair Ansible with Puppet.
# run a command across a group of nodes, no agent required
bolt command run 'systemctl restart nginx' --targets web_nodesKey point: Positioning Bolt as the imperative, on-demand complement to declarative pull-based Puppet is the framing that shows current tooling awareness.
Puppet requires that each resource in a catalog be unique by type and title, because two declarations of File['/etc/nginx.conf'] would give conflicting desired states with no way to reconcile them. The compiler rejects it with a duplicate-declaration error rather than guessing which wins.
The fix is design, not a flag. One module or profile owns a given file, and everything else that needs to influence it does so through that owner: parameters, Hiera data, template fragments, or the concat pattern that assembles a file from ordered fragments each declared once. If two modules both want a file, that's a signal the ownership boundary is wrong.
# the concat pattern: many owners, one assembled file, no clash
concat { '/etc/hosts':
ensure => present,
}
concat::fragment { 'hosts-header':
target => '/etc/hosts',
content => "127.0.0.1 localhost\n",
order => '01',
}Key point: The senior framing: a duplicate-resource error is usually a design smell about ownership, not a syntax problem. Mentioning concat for shared files indicates production scar tissue.
Track whether agents are actually running and converging: alert on nodes that haven't reported within a couple of run intervals (a silent agent means unmanaged drift), and on nodes reporting failures or high change counts. PuppetDB and the reports it stores are the source; many teams feed those metrics into their monitoring stack or a dashboard.
Also watch the server side: compile latency and error rates on the compilers, PuppetDB query performance and disk growth, and CA certificate expiry. The mindset to convey is that Puppet is production infrastructure and needs the same observability as anything else you run.
Key point: The signal here is treating Puppet as monitored production infrastructure. Silent agents (no reports) are the failure mode juniors forget to alert on.
Puppet wins when you manage a large, long-lived fleet that needs continuous drift correction, because agents pull and re-enforce state on a schedule without anyone triggering a run. It trades a steeper setup (a Puppet server, agents, and certificate signing) for that always-on enforcement and a strict declarative model. Ansible skips agents and pushes over SSH, which is simpler to start but doesn't self-heal between runs. Chef leans on Ruby and a procedural feel. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.
| Tool | Model | Transport | Language | Best at |
|---|---|---|---|---|
| Puppet | Declarative, pull | Agent over HTTPS | Puppet DSL (Ruby-based) | Continuous drift enforcement on large fleets |
| Ansible | Procedural, push | Agentless over SSH | YAML playbooks | Quick orchestration and ad hoc runs |
| Chef | Procedural, pull | Agent (chef-client) | Ruby DSL | Teams comfortable writing Ruby |
| SaltStack | Declarative, push or pull | Agent (minion) or SSH | YAML plus Jinja | Fast event-driven automation at scale |
Prepare in layers, and practice out loud. Most Puppet rounds move from concept questions to writing a manifest to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical Puppet interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Puppet questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works