Introduction to Puppet: Part 3

16 page views   |   693 words   |  

Previous Parts

In the last post we got an error from 'puppet apply' that looked for a non-existent module. What we will do now is go through the manifest and find a minimal subset that works, and then iteratively build it up from there. The feedback loop is pretty small, so I'll share what I find with you. By the end of this series, you will have a way to spin up an instance of your own blog, and have more experience with managing infrastructure with Puppet.

Minimum Viable Manifest

This blog_setup.pp manifest does the trick. When run on Ubuntu 22.04, it install ruby 3, rails 7 
# introduced in
# refined in
node default {
  # Ensure the required packages are installed
  package { ['ruby', 'bundler', 'libyaml-dev']:
    ensure => installed,

  exec { 'gem install rails -v 7.1.2':
    cwd     => '/',
    path    => ['/usr/bin'],

  exec { 'rails new blog':
    cwd     => '/home/ubuntu',
    creates => '/home/ubuntu/blog',
    path    => ['/usr/bin', '/usr/local/bin'],
    user    => 'ubuntu',

  exec { 'bundle install':
    cwd     => '/home/ubuntu/blog',
    path    => ['/usr/bin'],
    user    => 'ubuntu',

Then, to compile and run this manifest:
sudo puppet apply blog_setup.pp

And the blog app will be set up, in Part 4 we will set up nginx, the systemd service, and the Post models that will make it into a functional blog app and not just a "rails new". You can skip ahead if you don't care about the implementation details, which I'll put below.

What is happening under the hood when you run 'puppet apply'

$ sudo puppet apply blog_setup.pp
Notice: Compiled catalog for in environment production in 0.62 seconds
Notice: /Stage[main]/Main/Node[default]/Exec[gem install rails -v 7.1.2]/returns: executed successfully
Notice: /Stage[main]/Main/Node[default]/Exec[bundle install]/returns: executed successfully
Notice: Applied catalog in 3.41 seconds

Catalog compilation

When you run 'puppet apply', it executes the puppet compiler in Ruby, you can see the entry point in the Puppet::Parser::Compiler.compile method.

At the end of the compile method, a new Puppet::Parser::Compiler is instantiated. Where's the manifest? If you look at the compile method, you will notice a node argument passed in. The Puppet::Node::Environment object keeps track of the manifest, the environment name (e.g. "production") and the module path:

[10] pry(Puppet::Parser::Compiler)> node.environment
=> <Puppet::Node::Environment:10540 @name="production" @manifest="/home/ubuntu/puppet-intro-blog/blog_setup.pp" @modulepath="/usr/share/puppet/modules" >

When compile is called on the new Puppet::Parser::Compiler object, these methods are called in order, but they are wrapped with profiling methods that we will ignore for now, also we will only look at a few of the steps, the rest are all available to the interested github user:

  1. set_node_parameters
  2. create_settings_scope
  3. evaluate_capability_mappings
  4. evaluate_main
  5. evaluate_site
  6. evaluate_ast_node
  7. evaluate_node_classes
  8. evaluate_applications
  9. evaluate_capability_mappings
  10. evaluate_generators
  11. validate_catalog(CatalogValidator::PRE_FINISH)
  12. finish
    • from our example manifest, here are the finished resources:
    • [1] pry(#<Puppet::Parser::Compiler>)> resources
      => [Class[main]{:name=>"main"},
       Package[ruby]{:name=>"ruby", :ensure=>"installed"},
       Package[bundler]{:name=>"bundler", :ensure=>"installed"},
       Package[libyaml-dev]{:name=>"libyaml-dev", :ensure=>"installed"},
       Exec[gem install rails -v 7.1.2]{:command=>"gem install rails -v 7.1.2", :cwd=>"/", :path=>["/usr/bin"]},
       Exec[rails new blog]{:command=>"rails new blog", :cwd=>"/home/ubuntu", :creates=>"/home/ubuntu/blog", :path=>["/usr/bin", "/usr/local/bin"], :user=>"ubuntu"},
       Exec[bundle install]{:command=>"bundle install", :cwd=>"/home/ubuntu/blog", :path=>["/usr/bin"], :user=>"ubuntu"}]
  13. prune_catalog

The stages where exec's are executed

After compilation, run stages are used to order the execution of resources. There's the main stage and then the setup stage, and you can also create custom stages (like "pre"), see run stages documentation for more detail.

The Exec type is worth reading in detail, as that is the one we've used the most in this minimum viable manifest. It represents the execution of an external command (e.g. bundle install), but it also must be idempotent.

Catalog application

Finally, the catalog is applied by running Puppet::Configurer#apply_catalog, which calls Puppet::Resource::Catalog.apply. This apply method creates a Puppet::Transaction and then calls evaluate on that.

This is a view of what the Puppet compiler is doing and how manifests are compiled and applied. In part 4 I'll get into how to build out more blog functionality, and also get into puppet's module system.

#puppet #programming