Without getting to far into the weeds, Nginx is forcing my hand in order to accomplish some magic with vhosts and the map directive.

Is there an elegant (relative) solution to sharing a variable across multiple define calls, which allows each define call to append it's data to the global variable? In software this would be known as a singleton.

-- The weeds --

Nginx has a map directive which dictates which upstream server pool the request should be passed to, like so:

map $http_host $upstream_pool {


    blog.example.com     blog_example_com;
    repo.example.com     repo_example_com;

    default example_com;

As you can see, any requests for blog.example.com will be passed to the blog_example_com upstream server pool (by way of proxy_pass).

The problem is the map directive syntax is that it can only be included within the main http block (nginx.conf), whereas vhost specific directives such as upstream and location can be included in the server block of a vhost config.

My nodes.pp manifest looks something like this:

 service-a-1.example.com inherits project_dev {

     nginx::vhost { 'mothership': }
     nginx::vhost { 'mothership_blog': }
     nginx::vhost { 'repo': }

As you can see, after a successful puppet run, I should end up with 3 distinct vhost config files in /etc/nginx/vhost.d/ dir.

The problem I am having is that in order for the map directive to work, I need to know which vhosts were loaded, so I can add their respective upstream ids to the map directive, which I have defined in the primary config: /etc/nginx/nginx.conf within the http block (of which there can only be one).

-- What I have tried --

I have a global.pp file which does some "bootstrapping" and in this file I added a $singleton = '' syntax, then in the nginx::vhost define, I added this syntax:

$tpl_upstream_pool_labels = inline_template("<% tpl_upstream_pools.keys.sort.each do |role| %><%= role %>_<%= tpl_domain_primary %>_<%= tpl_domain_top_level %>|<% end %>")

$singleton = "${singleton}${tpl_upstream_pool_labels}"

notify { "\n--------------------- Nginx::Conf::Vhost::Touch | Timestamp: ${timestamp} | Pool labels: ${singleton} -------------------------\n": }

Which should result in a pipe delimited list of upstream ids. As mentioned earlier, in the nodes.pp manifest, I make three calls to nginx::vhost, and would expect the $singleton global variable to be appended for every call, however it is not, it only contains the last call's data.

I also tried to hack my way around this by writing a temp file like so:

$temp_file_upstream_pool_labels_uri = "/tmp/puppet_known_upstreams_${timestamp}.txt"

exec { "event_record_known_upstream_${name}" :            
    command     => "touch ${temp_file_upstream_pool_labels_uri} && echo ${tpl_upstream_pool_labels} >> ${temp_file_upstream_pool_labels_uri}",            
    provider    => 'shell'

Then in the nginx::conf::touch define, where the primary config nginx.conf is to be written by puppet, I tried this:

$temp_file_upstream_pool_labels_uri = "/tmp/puppet_known_upstreams_${timestamp}.txt"

$contents = file($temp_file_upstream_pool_labels_uri)

Which should, in theory, load the contents of the file into the $contents variable. But when I run puppet using this approach I get an error that the file does not exist. I ensured that the nginx::conf::touch call is not made until after all the vhosts were considered, but still to no avail.

  • Question is straight forward, however in the interests of solving my problem, I also included 'the weeds'. – Mike Purcell Sep 17 '14 at 14:21
  • 1
    OK, now your question makes sense. From what I understand, the usual solution for this is concat, though I have no experience with it myself. – Michael Hampton Sep 17 '14 at 15:16
  • Concat looks promising, however it's only for 3.x, and I am currently on 2.7.x – Mike Purcell Sep 17 '14 at 18:26
  • 1
    Hmmm. I don't think 2.7.x is supported anymore. Might be time to upgrade. – Michael Hampton Sep 17 '14 at 18:27
  • Lol. And spend another 3-4 months porting over all the modules? Don't have that kind of time nor resources. – Mike Purcell Sep 17 '14 at 18:28

The problem with global variables in Puppet is that you cannot actually append to them. The += syntax is allowed, but it creates a local copy of the global, with the right hand side value appended.

I have implemented a pattern that does what you want, but I'm not proud of it and cannot recommend using it. Still, since you're asking, here goes:

class nginx::conf {
    $global = ''
    file { '/etc/nginx/nginx.conf':
        content => template(...)

The variable from the class can be appended, at least in 2.7.x.

define nginx::vhost(...) {
    include nginx::conf

    $nginx::conf::global += "new content here"
    nginx::override_conf_content { "for-vhost-$name": }

The magic happens in the mysterious final line, which instantiates yet another define.

define nginx::override_conf_content() {
    include nginx::conf
    $global = $nginx::conf::global
    File<| title == '/etc/nginx/nginx.conf' |> { 
        content => template(...)

The content property for nginx.conf is overridden with the result of another evaluation of the template, with the new variable value.

Full disclosure, I'm not sure why this works. It probably shouldn't, and may rely on an obscure bug. It may stop working in future versions.

Note that I called it $global instead of $singleton, because that's what it is. Singletons can be used to implement the semantics of globals, but they are not the same thing.

Finally, even though I can feel your pain facing the Puppet 3 update, you should really take the time and get it underway. We all will likely not be able to afford running 2.x much longer.

  • Interesting. I ended up having to add a static mapping for dev and prod pools to a config.yaml file just to get past the issue and move onto bigger things. Puppet and I have a love / hate relationship; I love that it keeps servers in check, but I hate the fact that simple design principles are not avail. I get that it's based upon DSL, and being a software guy I am procedural based, but 2.7 should have at least afforded us the ability to accomplish these simple feats. Moving onto puppet 3.x will be a huge undertaking, but if we get funded, I think we can make it happen. Thx for the post. – Mike Purcell Sep 19 '14 at 0:53

Adding another answer to keep the issues separate.

The approach that combines exec and file() is flawed because those work on very different levels.

The exec resource is added to the catalog and sent to the agent, where it is evaluated after compiling has finished. Since you are trying to gather information during compile time (before you collect it using the file() function), you cannot rely on any resources.

It should be possible to build what you have in mind by relying on the generate function instead of exec resources.

  • Confirmed. When I tried to write to a file, the contents wouldn't be available until I ran it a 2nd time. Then I started running into permission issues (SELinux enabled), so then I decided to create an empty file in the modules/nginx/files dir, but this made no sense as I now had a file whose sole purpose was to act as a container for transitory data. In short, total hack. – Mike Purcell Sep 19 '14 at 0:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.