Puppet as a declarative and idempotent language
The first important thing to understand is how Puppet differs from normal scripting or coding languages. Puppet is declarative, meaning you describe the state you want the system to be in. For example, you could describe that your system should have a user called username
with UID 1234
, a configuration file should not exist, and a kernel setting should be at a particular value. In comparison to most languages where you have to describe the process to get to the state, Puppet’s approach brings us closer to how customers request services. They don’t want to know how it’s done, just that it will meet their requirements. These resource definitions can be saved in your version control system. Often, this approach is described as being part of Infrastructure as Code.
Puppet is idempotent, meaning that it will only make the changes required to get into the declared state. Meanwhile, most procedural languages will run steps every time and, typically, require various checks such as if
statements to be added to make checks to avoid duplication. This is particularly powerful as what is called enforcement can be run with the Puppet language, ensuring the state you declared has been reached, and is capable of detecting whether a change happened because of you updating the state you wished the machine to be in or whether it was a change that happened on the machine itself moving away from the desired state. This can greatly assist with audits and avoid any configuration drifts in an estate and ensure change is managed and deliberate.
Puppet is OS-independent; the language is focused on the state, not the underlying implementation of how particular OSes install a package or add a user. This gives us a universal language that is independent of any underlying implementations, allowing for less duplication of code, avoiding the need to use layers of case
/if
statements to detect differences, and allowing multiple language implementations such as PowerShell for Windows and Bash for Unix-based systems. Additionally, it makes it easier to recover after failures in applying code. If in a procedural language, a step fails, it might not be safe to run the script in full again depending on how well the check steps have been coded. In contrast, Puppet code is able to resume only performing the steps it needs to reach the correct state.
A simple example of Puppet code to create a user would look like this:
user { 'david' uid => '123' }
In contrast, a shell script might have a section like this:
if ! getent passwd david; then useradd -u 123 david elif ! $(uid david) == 123; then usermod -u 123 david fi
In the preceding shell example, we have to check whether a user exists, and if not, create one. If it does exist, then does it have the right UID? If not, we change it. This script only covers OSes that can use useradd
and usermod
. To achieve compatibility with multiple OSes, we would need a test to detect the OS type and produce a section of code like this for every OS or group of OSes and their required commands. Often, it would be more practical to write in multiple languages and scripts to cover a broader base of OS flavors, that is, if we wanted to cover both Unix and Windows, for example.
This compares to the Puppet declaration, which will work on multiple OSes without change as Puppet will detect the required commands and perform all the necessary state checks as part of that.
This example is all just for a single resource with a single attribute. You can quickly see how the shell script example will not scale as it becomes increasingly complex with almost endless checks and options.