Previous Parts
- 2024-01-02: Introduction to Puppet: Part 2
- 2023-11-30: Introduction to Puppet: Part 1
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 https://tlehman.blog/p/introduction-to-puppet # refined in https://tlehman.blog/p/introduction-to-puppet-part-3 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 ip-172-26-8-230.us-west-2.compute.internal 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:
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:
- set_node_parameters
- create_settings_scope
- evaluate_capability_mappings
-
evaluate_main
- evaluate_main calls Puppet::Parser::Resource#evaluate
- which calls Puppet::Resource::Type#evaluate_code(resource) (note, this evaluation is of the compile time types of resources in the manifest. An example resource is Node[default]{:name=>"default"}, which is the top-level resource in our manifest.
- evaluate_main calls Puppet::Parser::Resource#evaluate
- evaluate_site
- evaluate_ast_node
- evaluate_node_classes
- evaluate_applications
- evaluate_capability_mappings
- evaluate_generators
- validate_catalog(CatalogValidator::PRE_FINISH)
-
finish
- from our example manifest, here are the finished resources:
[1] pry(#<Puppet::Parser::Compiler>)> resources => [Class[main]{:name=>"main"}, Node[default]{:name=>"default"}, 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"}]
- 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.
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.
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.