NGINX.COM
Web Server Load Balancing with NGINX Plus

[Editor – The Chef cookbook referenced in this blog relies on the NGINX Plus Status and Upstream Conf modules (enabled by the status and upstream_conf directives). Those modules are replaced and deprecated by the NGINX Plus API in NGINX Plus Release 13 (R13) and later, and are not available in NGINX Plus R16 and later. For the solution to continue working, update the cookbook components that refer to the two deprecated modules.]

In an earlier blog post, we talked about using Ansible to install NGINX or NGINX Plus. As for many other types of software out there, there lots of alternatives when it comes to configuration management software. Along with Ansible, one of the most popular is Chef. Both tools have their fans, and there are plenty of articles that compare them. Here we’ll focus on showing how to use Chef to install and configure NGINX and NGINX Plus.

Opscode, the company behind Chef, provides an extensive collection of cookbooks that are easy to install onto your Chef server with a single command. Out of the box, the base cookbook for NGINX is a very powerful tool for installing and configuring NGINX. It can be rather overwhelming for newer Chef users, however, so in this post we’ll go over how to use it to install an NGINX Plus package and set up a basic configuration. (You can also use this cookbook to install NGINX via source code – with or without third‑party modules – but we will not be covering that process in this post.)

Editor – For information about using other DevOps automation tools with NGINX and NGINX Plus, check out these related blogs:

This post assumes that you have a basic understanding of Chef, its configuration files (recipes, environments, roles, and so on) and its associated tools, mainly knife. If you need to review, check out the tutorials and documentation at learn.chef.io.

Preparing Your Chef Environment

To help make it quick and easy to get a test environment up and running, we’re doing our work in a Chef Zero environment. Chef Zero is a simple, easy to install, in‑memory Chef server that is useful for testing chef‑client tasks (similar to chef‑solo), as well as tasks that require a full Chef server. For full installation instructions, see the Chef Zero homepage on GitHub.

Here is the process for installing chef‑zero on a basic Ubuntu 14.04 installation and getting everything in place:

  1. Use apt to install the prerequisite packages:

    ~$ sudo apt-get install ruby-dev make git
    [sudo] password for username:
    Reading package lists... Done
    Building dependency tree
    Reading state information... Done
    ...
     
    Need to get 52.6 MB of archives.
    After this operation, 188 MB of additional disk space will be used.
    Do you want to continue? [Y/n] y
    Get:1 http://us.archive.ubuntu.com/ubuntu/ trusty-updates/main libasan0 amd64 4.8.4-2ubuntu1~14.04 [63.0 kB]
    ...
    Setting up ruby1.9.1 (1.9.3.484-2ubuntu1.2) ...
    Processing triggers for libc-bin (2.19-0ubuntu6.6) ...
    
  2. Use gem to install chef‑zero:

    ~$ sudo gem install chef-zero
    Fetching: mixlib-log-1.6.0.gem (100%)
    Fetching: hashie-3.4.2.gem (100%)
    ...
    Successfully installed chef-zero-4.3.2
    7 gems installed
  3. Download and install the Chef package. We’re pulling the package directly from the Opscode servers instead of using apt, because the Ubuntu distributions have a rather outdated version of the Chef package. (The Chef package in the example might even be slightly outdated; to be sure you’re getting the latest package, check the Chef website.)

    Download the package:

    ~$ wget https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/10.04/x86_64/chef_12.4.3-1_amd64.deb
    --2015-10-19 11:33:44-- https://opscode-omnibus-packages.s3.amazonaws.com/ubuntu/10.04/x86_64/chef_12.4.3-1_amd64.deb
    Resolving opscode-omnibus-packages.s3.amazonaws.com (opscode-omnibus-packages.s3.amazonaws.com)... 54.231.2.233
    Connecting to opscode-omnibus-packages.s3.amazonaws.com (opscode-omnibus-packages.s3.amazonaws.com)|54.231.2.233|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 38821266 (37M) [application/x-www-form-urlencoded]
    Saving to: 'chef_12.4.3-1_amd64.deb'
     
    100%[==========================================================================
    ===============================================================================
    ===============>] 38,821,266 9.96MB/s in 4.6s
     
    2015-10-19 11:33:50 (8.03 MB/s) - 'chef_12.4.3-1_amd64.deb' saved [38821266/38821266]

    Install the package:

    ~$ sudo dpkg -i chef_12.4.3-1_amd64.deb
    Selecting previously unselected package chef.
    (Reading database ... 64482 files and directories currently installed.)
    Preparing to unpack chef_12.4.3-1_amd64.deb ...
    Unpacking chef (12.4.3-1) ...
    Setting up chef (12.4.3-1) ...
    Thank you for installing Chef!
  4. Set two GitHub global variables so that you can clone repos. The values do not actually have to match an existing GitHub user, as we are only cloning public repositories, and here I’m using my name and email address. If you plan on cloning any private repos or pushing any commits, specify values that match your GitHub account.

    ~$ git config --global user.name "Damian Curry"
    ~$ git config --global user.email "damian.curry@nginx.com"
  5. Clone the chef‑zero repo. The repo contains the necessary configuration files to get moving quickly, even though we used gem to install it. It also contains some cookbooks, node definitions, and environment definitions that we will not be using, but are good examples to have if you are new to Chef.

    ~$ git clone https://github.com/chef/chef-zero.git
    Cloning into 'chef-zero'...
    remote: Counting objects: 3115, done.
    remote: Total 3115 (delta 0), reused 0 (delta 0), pack-reused 3115
    Receiving objects: 100% (3115/3115), 548.87 KiB | 0 bytes/s, done.
    Resolving deltas: 100% (1727/1727), done.
    Checking connectivity... done.
  6. Start the chef‑zero process that we’ll work with.

    By default, Chef Zero listens on port 8889, but the knife configuration file bundled with the chef‑zero repo defaults to port 4000. To keep file changes to a minimum, we tell the chef‑zero instance to listen on port 4000. We also include the ‑d flag to make Chef Zero run as a daemon. If you run into any issues, you can try running the process in the foreground for troubleshooting purposes.

    ~$ sudo chef-zero -p 4000 -d

Downloading and Configuring the NGINX Cookbook

Now we install and configure the NGINX cookbook on our Chef server.

  1. Change directory to the playground directory inside the repo we cloned as ~/chef-zero in the previous section. This enables knife commands to locate the knife.rb configuration file, which resides in the ~/chef-zero/playground/.chef subdirectory.

    ~$ cd chef-zero/playground/
  2. Download the cookbook and its requirements to your local system. (The term install in the command string is kind of misleading, because the command doesn’t actually install NGINX or anything else; instead it just downloads the files.)

    ~/chef-zero/playground$ knife cookbook site install nginx
    Installing nginx to /home/username/chef-zero/playground/cookbooks
    Checking out the master branch.
    Creating pristine copy branch chef-vendor-nginx
    Downloading nginx from the cookbooks site at version 2.7.6 to /home/username/chef-zero/playground/cookbooks/nginx.tar.gz
    ...
    Cookbook yum version 3.8.1 successfully installed
  3. For some reason, the knife cookbook command we ran in the previous step downloads cookbook versions that are incompatible for rsyslog. If we don’t fix it, the incompatibility will cause the following error later on when we upload the cookbooks in Step 4 of Bootstrapping and Preparing the Node for NGINX Plus Installation:

       # DO NOT RUN NOW: example to show error
       ~/chef-zero/playground/cookbooks$ knife cookbook upload *
       Uploading apache2      [1.0.0]
       Uploading apt          [2.9.2]
       Uploading bluepill     [2.4.1]
       ERROR: Cookbook bluepill depends on cookbooks which are not
       ERROR: currently being uploaded and cannot be found on the 
       ERROR: server.
       ERROR: The missing cookbook(s) are: 'rsyslog' version '~> 2.0'

    To avoid the error, run this command to pull an earlier version of the rsyslog cookbook:

    ~/chef-zero/playground$ cd cookbooks
    ~/chef-zero/playground/cookbooks$ knife cookbook site install rsyslog 2.0.0
    Installing rsyslog to /home/username/chef-zero/playground/cookbooks
    Checking out the master branch.
    Pristine copy branch (chef-vendor-rsyslog) exists, switching to it.
    ...
    Cookbook rsyslog version 2.0.0 successfully installed
  4. Create a new recipe file for NGINX Plus installation, based on the existing recipe in the NGINX cookbook for installing the open source NGINX software from a prebuilt binary, package.rb. After we make a couple of changes to the NGINX cookbook, we’ll upload it to the Chef server. The new recipe, plus_package.rb, creates all of the necessary certificates and apt repos needed for access to the NGINX Plus package.

    Create the plus_package.rb file in the ~/chef-zero/playground/cookbooks/nginx/recipes directory with these contents:

    #
    # Cookbook Name:: nginx
    # Recipe:: plus_package
    # Author:: Damian Curry <damian.curry@nginx.com>
    #
    # Copyright 2008-2013, Chef Software, Inc.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
     
    include_recipe 'nginx::ohai_plugin'
     
    directory '/etc/ssl/nginx' do
      owner  'root'
      group  'root'
      mode   '0755'
      action :create
    end
     
    file '/etc/ssl/nginx/nginx-repo.key' do
      owner   'root'
      group   'root'
      mode    '0644'
      content node.attribute['nginx']['nginx_repo_key']
    end
     
    file '/etc/ssl/nginx/nginx-repo.crt' do
      owner   'root'
      group   'root'
      mode    '0644'
      content node.attribute['nginx']['nginx_repo_crt']
    end
     
    # Ensure file does not exist (certificate for the NGINX Plus repository was 
    # changed in June 2016)
    file '/etc/ssl/nginx/CA.crt' do
      action :delete
    end
    
    case node['platform_family']
    when 'rhel'
      version_long = node[:platform_version]
      version = version_long[0]
      case version
      when '5'
        package 'openssl' do
          action :install
        end
      when '6', '7'
        package 'ca-certificates' do
          action :install
        end
      end
    
      remote_file "/etc/yum.repos.d/nginx-plus-#{version}.repo" do
        source "https://cs.nginx.com/static/files/nginx-plus-#{version}.repo"
        owner 'root'
        group 'root'
        mode 0700
      end
    
    when 'debian'
      include_recipe 'apt::default'
     
      apt_repository 'nginx_plus' do
        uri          'https://plus-pkgs.nginx.com/ubuntu'
        distribution node['lsb']['codename']
        components   %w(nginx-plus)
        deb_src      false
        key          'http://nginx.org/keys/nginx_signing.key'
      end
    end
     
    package node['nginx']['package_name'] do
      notifies :reload, 'ohai[reload_nginx]', :immediately
      not_if 'which nginx'
    end
     
    directory node['nginx']['dir'] do
      owner     'root'
      group     node['root_group']
      mode      '0755'
      recursive true
    end
     
    directory node['nginx']['log_dir'] do
      mode      node['nginx']['log_dir_perm']
      owner     node['nginx']['user']
      action    :create
      recursive true
    end
     
    directory File.dirname(node['nginx']['pid']) do
      owner     'root'
      group     node['root_group']
      mode      '0755'
      recursive true
    end
     
    directory "#{node['nginx']['dir']}/conf.d" do
      owner 'root'
      group node['root_group']
      mode  '0755'
    end
     
    service 'nginx' do
      supports :status => true, :restart => true, :reload => true
      action   :enable
    end
     
    include_recipe 'nginx::commons_script'
     
    template 'nginx.conf' do
      path     "#{node['nginx']['dir']}/nginx.conf"
      source   node['nginx']['conf_template']
      cookbook node['nginx']['conf_cookbook']
      owner    'root'
      group    node['root_group']
      mode     '0644'
      notifies :reload, 'service[nginx]', :delayed
    end
     
    if node['nginx']['default_site_enabled'] == 'true'
      template "#{node['nginx']['dir']}/conf.d/default.conf" do
        source   'default-site.erb'
        owner    'root'
        group    node['root_group']
        mode     '0644'
        notifies :reload, 'service[nginx]', :delayed
      end
    else
      file "#{node['nginx']['dir']}/conf.d/default.conf" do
        action :delete
      end
    end
     
    if node['nginx']['plus_status_enable'] == 'true'
      template 'nginx_plus_status' do
        path   "#{node['nginx']['dir']}/conf.d/nginx_plus_status.conf"
        source 'nginx_plus_status.erb'
        owner  'root'
        group  node['root_group']
        mode   '0644'
        notifies :reload, 'service[nginx]', :delayed
      end
    end
  5. Create the template file, nginx_plus_status.erb, that is referenced in the last stanza of the plus_package.rb recipe. Put it in the ~/chef-zero/playground/cookbooks/nginx/templates/default directory with these contents:

    upstream example {
        zone example 64k;
        server [::1]:8080;
    }
    server {
        listen <%= node['nginx']['plus_status_port'] -%>;
        status_zone status-page;
        access_log  <%= node['nginx']['log_dir'] %>/status.access.log;
        location = /status.html { 
            root /usr/share/nginx/html; 
        }
        location = / {
            return 301 /status.html;
        }
        location /status {
            status;
            status_format json;
            access_log off;
        }
        location /upstream_conf {
            upstream_conf;
        }
    }
  6. Create a role to use when installing NGINX Plus. Roles in Chef consist of four main lists:

    • run_list – List of the recipes to run on a node
    • default_attributes – List of attributes and the values for the recipes to set during deployment
    • env_run_list – (Not used in this example) List of recipes to run against the node based on which environment it is assigned to
    • override_attributes – (Not used in this example) List of attributes and the values for the recipes to set during deployment that will override the default attributes that are defined in some recipes

    A role is a good way to define a baseline set of characteristics for a node with a specific function. You can then modify the role by adding more attributes to one or both of the default_attributes and override_attributes lists on each individual node.

    Create the nginx_plus.rb role definition file with these contents and place it in the ~/chef-zero/playground/roles directory:

    name "nginx_plus"
    description "An example role to install NGINX Plus"
    run_list "recipe[nginx]"
    default_attributes "nginx" => { "install_method" => "plus_package",
                                    "package_name" => "nginx-plus",
                                    "init_style" => "init",
                                    "default_site_enabled" => "true",
                                    "nginx_repo_key" => "-----BEGIN PRIVATE KEY-----nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbYwum24BwEY8Ynnqc0  ...  +BCnoMSzbvLWhZbpSrdmD9nOj1KkPcWn4ArSv6prlYItUwWbNtFLw/E=n-----END PRIVATE KEY-----",
                                    "nginx_repo_crt" => "-----BEGIN CERTIFICATE-----nMIIDrDCCApSgAwIBAgICBs8wDQYJKoZIhvcNAQEFBQAwXjELMAkGA1UEBhGTg  ...  X2XnbovinLlYPfdi7BhlXTI9u78+tqbo0YVsSBiDV49hcIA=n-----END CERTIFICATE-----" }

    You will notice that I have added the contents of the nginx-repo.key and nginx-repo.crt files as the values of the nginx_repo_key and nginx_repo_crt attributes. If you use the example as the basis for your nginx_plus.rb file, replace the values in the example with the key and certificate for your NGINX Plus subscription or trial. I prefer recording the key and certificate in attributes this way instead of recording them in files inside of the cookbook, because it makes things more portable, but you can use either method.

    It is also possible to define the run_list and default_attributes for an individual host or an environment, inside either the node or environment definition, but I prefer to define them in a role. Then you can apply the role to any node where you want to install NGINX Plus, followed by another recipe that configures NGINX Plus for the specific function the node is to perform.

    Notes:

    • You must define the init_style attribute as init, because its default value of runit does not work with NGINX Plus.
    • When you copy in the key and certificate from your Subscriptions page at the NGINX Plus customer portal, each one is a block of about 20 lines of text with a hard linebreak at the end of each line. For Chef to parse the key and certificate properly, you must convert the blocks to a single line each by replacing every hard linebreak with the linebreak character, n (backslash‑n). For brevity, the example shows the converted form of only the first and last couple lines from the original blocks.

Bootstrapping and Preparing the Node for NGINX Plus Installation

Next we need to make sure that our node has the correct role in its run list. You can set up the run list when bootstrapping the node or after. To keep the process as simple as possible and thus avoid errors, I prefer to set up the run list after the node has been bootstrapped. Also, I always like to have a local copy of the node definitions so I can modify them on disk and then push the changes to Chef, which lets me keep track of changes using Git.

  1. Run this knife bootstrap command to associate the node with the Chef environment without actually executing any run lists against it. Once the node has been associated with Chef, we will define its run list.

    In this example I am using the ‑N flag to set the name of the node to chef-test. You can set any name you like; if you omit the ‑N flag, the name defaults to the IP address or FQDN provided as the last parameter to the command (127.0.0.1 in this example).

    ~/chef-zero/playground/cookbooks$ cd ..
    ~/chef-zero/playground$ knife bootstrap -N chef-test -x username --sudo 127.0.0.1
    Creating new client for chef-test
    Creating new node for chef-test
    Connecting to 127.0.0.1
    username@127.0.0.1's password:
    127.0.0.1 knife sudo password:
    Enter your password:
    127.0.0.1
    127.0.0.1 -----> Existing Chef installation detected
    ...
    127.0.0.1 Chef Client finished, 0/0 resources updated in 1.122875381 seconds
  2. Create a local copy of the node’s definition.

    ~/chef-zero/playground$ knife node show chef-test --format json > nodes/chef-test.json
  3. Edit the local node definition, adding the nginx_plus role to its run list. Here is the definition after you have modified it:

    {
      "name": "chef-test",
      "chef_environment": "_default",
      "run_list": [
      	      "role[nginx_plus]"
      ]
    ´
      "normal": {
        "tags": [
     
        ]
      }
    }
  4. With all of the required files in place, push them to the Chef server:

    ~/chef-zero/playground$ knife role from file roles/nginx_plus.rb
    Updated Role nginx_plus!
     
    ~/chef-zero/playground$ knife node from file nodes/chef-test.json
    Updated Node chef-test!
     
    ~/chef-zero/playground$ cd cookbooks
     
    ~/chef-zero/playground/cookbooks$ knife cookbook upload *
    Uploading apache2      [1.0.0]
    Uploading apt          [2.8.2]
    ...
    Uploaded 12 cookbooks.
  5. Run the chef‑client in the foreground so you can make sure everything works. The first time the cookbook runs, it can take a while as it needs to run an apt‑get update operation:

    ~/chef-zero/playground/cookbooks$ sudo chef-client
    Starting Chef Client, version 12.4.3
    resolving cookbooks for run list: ["nginx"]
    Synchronizing Cookbooks:
      - nginx
      - apt
      ...
    Compiling Cookbooks...
    Recipe: ohai::default
    ...
    Recipe: nginx::plus_package
     
    ...
    Running handlers:
    Running handlers complete
    Chef Client finished, 34/41 resources updated in 81.395229372 seconds

At this point NGINX Plus is installed with a very basic configuration, without any server blocks defined in the http block. This is not the default NGINX Plus configuration, but rather the default configuration from the official Chef NGINX cookbook. Here are the contents of the /etc/nginx/nginx.conf file:

user www-data;
worker_processes  1;
 
error_log  /var/log/nginx/error.log;
pid        /run/nginx.pid;
 
events {
    worker_connections  1024;
}
 
http {
    include       /etc/nginx/mime.types;
 
    default_type  application/octet-stream;
    access_log    /var/log/nginx/access.log;
 
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
 
    keepalive_requests 100;
    keepalive_timeout  65;
 
    gzip  on;
    gzip_http_version 1.0;
    gzip_comp_level 2;
    gzip_proxied any;
    gzip_vary off;
    gzip_types text/plain text/css application/x-javascript text/xml     
               application/xml application/rss+xml 
               application/atom+xml text/javascript 
               application/javascript application/json text/mathml;
    gzip_min_length  1000;
    gzip_disable     "MSIE [1-6].";
 
    variables_hash_max_size 1024;
    variables_hash_bucket_size 64;
    server_names_hash_bucket_size 64;
    types_hash_max_size 2048;
    types_hash_bucket_size 64;
 
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Because the basic default site has no virtual servers (server configuration blocks) defined, when you access it you see the NGINX welcome page:

Welcome to NGINX screen

Modifying the Default Options for the NGINX Cookbook

Now, let’s change some of the config variables by defining more attributes in the configuration file for the NGINX Plus role (~/chef-zero/playgrounds/roles/nginx_plus.rb). To match the default configuration in NGINX Plus packages, we are disabling the default site, setting the worker_processes directive to auto, changing the username from www‑data to nginx, and turning off the gzip module. We’re also enabling the NGINX Plus live activity monitoring (status) dashboard and API and setting them to listen on port 8080.

Here is what the modified role looks like, with the changes highlighted in bold:

name "nginx_plus"
description "An example role to install NGINX Plus"
run_list "recipe[nginx]"
default_attributes "nginx" => { "install_method" => "plus_package",
                                "package_name" => "nginx-plus",
                                "init_style" => "init",
                                "default_site_enabled" => "false",
                                "worker_processes" => "auto",
                                "user" => "nginx",
                                "gzip" => "off",
                                "plus_status_enable" => "true",
                                "plus_status_port" => "8080",
                                "nginx_repo_key" => "-----BEGIN PRIVATE KEY-----nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbYwum24BwEY8Ynnqc0  ...  +BCnoMSzbvLWhZbpSrdmD9nOj1KkPcWn4ArSv6prlYItUwWbNtFLw/E=n-----END PRIVATE KEY-----",
                                "nginx_repo_crt" => "-----BEGIN CERTIFICATE-----nMIIDrDCCApSgAwIBAgICBs8wDQYJKoZIhvcNAQEFBQAwXjELMAkGA1UEBhGTg  ...  X2XnbovinLlYPfdi7BhlXTI9u78+tqbo0YVsSBiDV49hcIA=n-----END CERTIFICATE-----" }

There are many other NGINX configuration options that you can define as attributes in the same way. We aren’t covering them here, but they are all documented in the README.md that is bundled with the cookbook. Keep in mind that the different attributes were designed for the open source NGINX software, so not all of them apply to NGINX Plus.

Now we go ahead and upload the modified role and rerun Chef to see the changes. To avoid errors, we need to manually stop NGINX Plus before running the chef‑client command.

~/chef-zero/playground$ knife role from file roles/nginx_plus.rb
Updated Role nginx_plus!
 
~/chef-zero/playground$ sudo service nginx stop
 
~/chef-zero/playground$ ps axu | grep nginx
username 10493  0.0  0.2  11748  2144 pts/0    S+   17:24   0:00 grep --color=auto nginx
 
~/chef-zero/playground$ sudo chef-client
Starting Chef Client, version 12.4.3
resolving cookbooks for run list: ["nginx"]
Synchronizing Cookbooks:
  - apt
  -
...
Compiling Cookbooks...
...
Recipe: nginx::commons_conf
  * template[nginx.conf] action create
   
...
Running handlers:
Running handlers complete
Chef Client finished, 5/30 resources updated in 3.877185478 seconds

At this point you can display the NGINX Plus dashboard by pointing a browser at port 8080 on your NGINX Plus server’s IP address. There’s not much information on the dashboard’s main, Server zones, or Upstreams tabs, because there are no running backend servers for it to monitor (the current entries are for the live activity monitoring module itself). So in the next section we’ll define a configuration of backend servers that takes advantage of some NGINX Plus features.

Before configuring backend servers using Chef, the NGINX Plus dashboard shows only the live activity monitoring module itself

Creating a Cookbook for a DockerUI Deployment

The base NGINX cookbook does not include variables for creating a server configuration block, so we need to create a basic recipe to create the site configuration file for NGINX Plus. As an example, I am going to configure NGINX Plus as a frontend for DockerUI, adding SSL, basic htpasswd authentication, and a custom log format that makes it easy to track which users are accessing DockerUI.

  1. The demo-deploy cookbook that we are creating depends on a couple of other cookbooks, which we need to download:

    • htpasswd – Creates and manages htaccess files. This is not an official Chef cookbook, but it is a good option. It depends on the Python cookbook, so we need to install that as well.

    • selfsigned_certificate – Creates and manages self‑signed certs.

    Run these five commands to download the required files and verify they are in place:

    $ cd ~/chef-zero/playground/cookbooks
     
    ~/chef-zero/playground/cookbooks$ git clone https://github.com/redguide/htpasswd.git
    Cloning into 'htpasswd'...
    remote: Counting objects: 234, done.
    remote: Total 234 (delta 0), reused 0 (delta 0), pack-reused 234
    Receiving objects: 100% (234/234), 32.88 KiB | 0 bytes/s, done.
    Resolving deltas: 100% (96/96), done.
    Checking connectivity... done.
     
    ~/chef-zero/playground/cookbooks$ knife cookbook site install python
    Installing python to /home/username/chef-zero/playground/cookbooks
    Checking out the master branch.
    Creating pristine copy branch chef-vendor-python
     
    ...
    Uploaded 15 cookbooks.
     
    ~/chef-zero/playground/cookbooks$ git clone https://github.com/cgravier/selfsigned_certificate.git
    Cloning into 'selfsigned_certificate'...
    remote: Counting objects: 169, done.
    remote: Total 169 (delta 0), reused 0 (delta 0), pack-reused 169
    Receiving objects: 100% (169/169), 105.59 KiB | 0 bytes/s, done.
    Resolving deltas: 100% (59/59), done.
    Checking connectivity... done.
     
    ~/chef-zero/playground/cookbooks$ ls
    apache2  apt  bluepill  build-essential  htpasswd  nginx  ohai  packagecloud  php  python  rsyslog  runit  selfsigned_certificate  yum  yum-epel
  2. Create the directory structure that Chef expects, as the first step in creating the new demo-deploy cookbook. This is a very basic cookbook at this time, so we only need to create directories for templates and recipes:

    ~/chef-zero/playground/cookbooks$ mkdir -p demo-deploy/templates/default
     
    ~/chef-zero/playground/cookbooks$ mkdir -p demo-deploy/recipes
  3. Create three files that the demo-deploy cookbook needs:

    • metadata.rb – Defines the details of the cookbook
    • dockerui.rb – Controls the deployment
    • dockerui.conf.erb – Provides a template for the actual NGINX configuration file

    We also create an empty file called default.rb just because Chef expects to find a file with that name.

    Here are the contents of ~/chef-zero/playground/cookbooks/demo-deploy/metadata.rb:

    name              'demo-deploy'
    maintainer_email  'damian.curry@nginx.com'
    license           'Apache 2.0'
    description       'Deploys NGINX Plus demos'
    version           '0.0.1'
     
    recipe 'demo-deploy::dockerui',  'Installs dockerui demo'
     
    depends 'selfsigned_certificate',             '~> 0.1.3'
    depends 'htpasswd',        '~> 0.2.5'

    Here are the contents of ~/chef-zero/playground/cookbooks/demo-deploy/recipes/dockerui.rb. Notice that in the third stanza we are using the htpasswd cookbook to add a user named dockerui with the password dockerui in the file /etc/nginx/htpassword. In a real world deployment, you would store the password in an encrypted data bag instead of as plain text.

    #
    # Cookbook Name:: demo-deploy
    # Recipe:: dockerui
    # Author:: Damian Curry <damian.curry@nginx.com>
    #
    # Copyright 2008-2013, Chef Software, Inc.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
     
    node.default['htpasswd']['built-in']['lang'] = ['ruby']
     
    include_recipe 'htpasswd'
    include_recipe 'nginx'
    include_recipe 'selfsigned_certificate'
     
    htpasswd '/etc/nginx/htpassword' do
      user 'dockerui'
      password 'dockerui'
    end
     
    template '/etc/nginx/conf.d/dockerui.conf' do
      source 'dockerui.conf.erb'
      owner  'root'
      group  'root'
      mode   '0644'
      notifies :reload, 'service[nginx]', :delayed
    end
     
    bash 'install docker.io' do
      user 'root'
      cwd '/root/'
      code 'curl -sSL https://get.docker.com/ | sh'
      not_if 'which docker'
    end
     
    bash ‘start docker container' do
      user 'root'
      cwd '/root/'
      code 'docker run -d -p 8970:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock dockerui/dockerui'
      not_if 'ps -ef | grep docker-proxy | grep -v grep'
    end

    Here are the contents of ~/chef-zero/playground/cookbooks/demo-deploy/templates/default/dockerui.conf.erb:

    log_format main "$status $ssl_client_verify/$remote_user@$remote_addr $request / $bytes_sent bytes  ->$upstream_addr";
        upstream dockerui {
            zone dockerui 64k;
            server <%= node['ipaddress'] %>:8970;
        }
        server {
            listen 80;
            return 301 https://$http_host;
        }
        server {
            listen 443 ssl;
            ssl_certificate /usr/var/ssl/certs/server.crt;
            ssl_certificate_key /usr/var/ssl/certs/server.key;
            access_log  /var/log/nginx/access.log main;
     
            location / {
                auth_basic on;
                auth_basic_user_file /etc/nginx/htpassword;
                proxy_pass http://dockerui;
                
                health_check match=dockerui;
                proxy_set_header Host $host;
                proxy_buffering off;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
            }
        }
     
        match dockerui {
            status 200;
            body ~ "dockerui";
        }
  4. Upload the cookbooks to Chef.

    ~/chef-zero/playground/cookbooks$ knife cookbook upload *
    Uploading apache2        [1.0.0]
    Uploading apt            [2.8.2]
    ...
    Uploading yum-epel       [0.6.3]
    Uploaded 16 cookbooks.
  5. Modify the NGINX Plus role definition file (~/chef-zero/playground/roles/nginx_plus.rb) so that the run list includes the recipe for our DockerUI deployment. The text to add appears in bold:

    name "nginx_plus"
    description "An example role to install NGINX Plus"
    run_list "recipe[nginx]","recipe[demo-deploy::dockerui]"
    default_attributes "nginx" => { "install_method" => "plus_package",
                                    "package_name" => "nginx-plus",
                                    "init_style" => "init",
    				"default_site_enabled" => "false",
                                    "worker_processes" => "auto",
                                    "user" => "nginx",
                                    "gzip" => "off",
    				"plus_status_enable" => "true",
                                    "plus_status_port" => "8080",
                                    "nginx_repo_key" => "-----BEGIN PRIVATE KEY-----nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbYwum24BwEY8Ynnqc0  ...  +BCnoMSzbvLWhZbpSrdmD9nOj1KkPcWn4ArSv6prlYItUwWbNtFLw/E=n-----END PRIVATE KEY-----",
                                    "nginx_repo_crt" => "-----BEGIN CERTIFICATE-----nMIIDrDCCApSgAwIBAgICBs8wDQYJKoZIhvcNAQEFBQAwXjELMAkGA1UEBhGTg  ...  X2XnbovinLlYPfdi7BhlXTI9u78+tqbo0YVsSBiDV49hcIA=n-----END CERTIFICATE-----" }
  6. Update the role:

    ~/chef/zero/playground/cookbooks$ cd ../
     
    ~/chef-zero/playground$ knife role from file roles/nginx_plus.rb
    Updated Role nginx_plus!
  7. Apply the updated role to our node:

    ~/chef-zero/playground$ sudo chef-client
    Starting Chef Client, version 12.4.3
    resolving cookbooks for run list: ["nginx", "demo-deploy::dockerui"]
    Synchronizing Cookbooks:
      - demo-deploy
      - bluepill
      - apt
      - nginx
      - build-essential
      - packagecloud
      - runit
      - yum-epel
      - ohai
      - rsyslog
      - selfsigned_certificate
      - htpasswd
      - yum
      - python
    Compiling Cookbooks...
    ...

Now that DockerUI is running and the NGINX Plus configurations are in place, when we refresh the NGINX Plus dashboard we see a server zone and upstream group for it.

chef-dashboard-dockerui-server

If you send an HTTP request to the server’s IP address, NGINX Plus redirects it to an HTTPS URL and prompts for a username and password. When you provide the username and password defined in the dockerui.rb recipe, you are brought to the DockerUI web interface. The first line of this snippet from the access log shows the redirect:

10.100.10.1 - - [11/Dec/2015:17:31:22 -0800] "GET / HTTP/1.1" 301 184 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36"
200 NONE/dockerui@10.100.10.1 GET / HTTP/1.1 / 1711 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /vendor.css HTTP/1.1 / 127994 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /dockerui.css HTTP/1.1 / 1352 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /angular.js HTTP/1.1 / 209191 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /dockerui.js HTTP/1.1 / 96665 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /vendor.js HTTP/1.1 / 652438 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /dockerapi/version HTTP/1.1 / 345 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /dockerapi/containers/json?all=1 HTTP/1.1 / 564 bytes  ->10.100.100.23:8970
200 NONE/dockerui@10.100.10.1 GET /dockerapi/images/json?all=0 HTTP/1.1 / 431 bytes  ->10.100.100.23:8970

The snippet also shows the custom format for log entries that we defined in Creating a Cookbook for a DockerUI Deployment by including this log_format directive in the dockerui.conf.erb file:

log_format main "$status $ssl_client_verify/$remote_user@$remote_addr $request / $bytes_sent bytes  ->$upstream_addr";

The custom format adds the second field with the $remote_user variable to report the username that made the request, making it easier to track which users are accessing the DockerUI web interface. Because we placed the log_format directive in the server block for HTTPS traffic, the format is used only for the lines in the snippet after the first one. The first line uses the default format for log entries because it records the redirect operation that happens in the server block for HTTP traffic.

Summary

We’ve barely scratched the surface of what is possible with Chef, but I hope you have a better understanding of how you can implement Chef to deploy NGINX Plus. There are many more cookbooks and modules that can handle almost any administrative function that you need.

Editor – Check out these related blogs about other DevOps automation tools for NGINX and NGINX Plus:

Ready to put what you’ve learned about Chef and NGINX Plus into practice? Start your free 30‑day trial of NGINX Plus or contact us today.

Hero image
Free O'Reilly eBook: The Complete NGINX Cookbook

Updated for 2024 – Your guide to everything NGINX



About The Author

Damian Curry

Damian Curry

Community & Alliances Technical Director

About F5 NGINX

F5, Inc. is the company behind NGINX, the popular open source project. We offer a suite of technologies for developing and delivering modern applications. Together with F5, our combined solution bridges the gap between NetOps and DevOps, with multi-cloud application services that span from code to customer.

Learn more at nginx.com or join the conversation by following @nginx on Twitter.