Follow @nonrecursive to hear about new content or subscribe:

Sweet Tooth Deep Dive

At the end of the last section, you saw that the Sweet Tooth Ansible roles do all the work of deploying your Clojure application. In this section, you’ll learn everything there is to know about those roles so that you can customize, modify, and extend them.

I’ll start by giving you a short refresher on your server setup, 'cause that will help you understand the role of each individual Ansible task in bringing about the end state. Next, I’ll share share some of my design goals so that the decisions I made in writing the Sweet Tooth roles will make sense. After that, we’ll download the roles' git repos and look at every line of code. You’ll learn about a few more Ansible features along the way, including tags and templates. You’ll also learn about Ansible Galaxy, Ansible’s service for sharing roles.

Your Beautiful Little Server Redux

Sweet Tooth installs nginx on a machine, and it configures nginx to forward HTTP requests to your Clojure application. Your Clojure application is packaged as an uberjar. Datomic is involved.

Design Goals

I had the IaC philosophy in mind when I developed the Ansible roles. I also had two other design goals in mind: the scripts should be easy enough to use that someone with no DevOps experience and little Clojure experience can use them, and developers with more experience should be able to easily customize them to fit their needs.

To make the scripts easy to use, I’ve tried to minimize the number of steps you need to take to get the outcome you want. To deploy locally for the first time, you only have to run three commands: ./build.sh, vagrant up, and ./deploy dev. To deploy to a VPS for the first time, you only have to change one line of the file character-sheet-example/infrastructure/ansible/inventories/prod/group_vars/webservers and one line of character-sheet-example/infrastructure/ansible/inventories/prod/host, and then run two commands, ./provision prod and ./deploy prod. After that, if you want to update the app you only have to run ./build.sh and ./deploy prod.

I’ve made the scripts customizable in two ways. First, I’ve used Ansible variables everywhere possible, and given those variables reasonable defaults. For example, most filesystem paths are derived from the clojure_uberjar_webapp_domain variable. In the last section, you set this to an IP address, something like 138.197.66.144, and the app’s log file location is /var/log/138-197-66-144/138-197-66-144.log, but you can change the log file’s location by setting the clojure_uberjar_webapp_app_log_dir variable. You’ll see this pattern repeated everywhere as we go through the Ansible scripts.

Second, I’ve tried to make scripts customizable by allowing you to swap out the default configuration files and scripts that get uploaded to your server with your own files. For example, one step of the deploy process is run a script uploaded to /var/www/app-name/check.sh, where app-name is derived from clojure_uberjar_webapp_domain. The purpose of the script is to do a quick sanity check on your app’s uberjar. You can write your script and then set the clojure_uberjar_webapp_app_check_local_path variable to point to it, running your own custom checks.

Now that you have all the background info you need, let’s look at the code!

Our Playbooks

In the character sheet example, you provision a server with the infrastructure/provision shell script, and you deploy a server with the infrastructure/deploy shell script. These shell scripts just run the infrastructure/ansible/sweet-tooth.yml playbook:

Contents of sweet-tooth.yml
---
- hosts: webservers
  become: true
  become_method: sudo
  roles:
    - "sweet-tooth-clojure.clojure-uberjar-webapp-common"
    - "sweet-tooth-clojure.clojure-uberjar-webapp-nginx"
    - "sweet-tooth-clojure.clojure-uberjar-webapp-datomic-free"
    - "sweet-tooth-clojure.clojure-uberjar-webapp-app"

The webservers declaration is responsible for setting up and configuring the services for serving your app: it creates the directory structure for you app, and it intalls and configures nginx. It also downloads and installs datomic. Eventually, you might host your app server and database server on different machines, but this is a good place to start.

The provision and deploy shell scripts control control which of the roles' tasks get applied using tags.

To understand how these playbooks work, we’re going to go through each line of code for the -common, -app, and -nginx roles (after all that you should know everything you need to understand the Datomic role). As we go through the roles, you’ll see where the tags are defined, and that will make the relationship between tags, tasks, and roles even clearer.

clojure-uberjar-webapp-common

Let’s look at the first role, sweet-tooth-clojure.clojure-uberjar-webapp-common. You can get it from GitHub with git clone https://github.com/sweet-tooth-clojure/ansible-role-clojure-uberjar-webapp-common.git or you can just follow along by browing the files online.

The purpose of this role is only to define a couple variables that are used by the other roles and to do some basic filesystem setup. You can see the variable definitions under defaults/main.yml:

defaults/main.yml
---
# User must define clojure_uberjar_webapp_domain
clojure_uberjar_webapp_app_name: "{{ clojure_uberjar_webapp_domain | replace('.', '-') }}"
clojure_uberjar_webapp_app_user: "{{ clojure_uberjar_webapp_app_name }}"
clojure_uberjar_webapp_app_underscored: "{{ clojure_uberjar_webapp_domain | replace('.', '_') }}"
clojure_uberjar_webapp_app_root: "/var/www/{{ clojure_uberjar_webapp_app_name }}"
clojure_uberjar_webapp_app_config_dir: "{{ clojure_uberjar_webapp_app_root }}/config"

This file takes a user-defined variable, clojure_uberjar_webapp_domain, and derives two other variables from it: clojure_uberjar_webapp_app_name and clojure_uberjar_webapp_app_underscored. I’ve found that this consistency makes it much easier to find the resources related to an app: log files are under /var/log/app-name, the nginx config file is under /etc/nginx/sites-available/app-name.conf, and so on.

It ialso defines the app user (clojure_uberjar_webapp_app_user) and some paths that get referenced by the other roles (clojure_uberjar_webapp_app_root and clojure_uberjar_webapp_app_config_dir).

In the character sheet example, the clojure_uberjar_webapp_domain variable is set in infrastructure/ansible/inventories/{dev,prod}/group_vars/webservers.

Let’s take a look at the tasks, which you can find in _tasks/main.yml.

Create user

The fist task creates a user. It uses the variable clojure_uberjar_webapp_app_user, which is defined in defaults/main.yml. It sticks with the "name things consistently" approach and defaults to clojure_uberjar_webapp_app_name. This user will become the owner of many of the files uploaded by sweet-tooth-clojure.clojure-uberjar-webapp-app. The intention is to add a little extra bit of security above making root the owner of everything, though I’m honestly not sure if I did a great job of that.

Create project directory and Create config directory

The next task, Create project directory, create’s the directory that will hold the application jar file. It also holds the directory created by the next task, Create config directory; this directory holds files used to configure the application, as opposed to e.g. configuring nginx. It’ll store files that set the environment variables for:

  • The HTTP port

  • The database URI

  • Whatever other custom environment variables you want to set

Now let’s look at the sweet-tooth-clojure.clojure-uberjar-webapp-app role.

clojure-uberjar-webapp-app

The role sweet-tooth-clojure.clojure-uberjar-webapp-app configures a server to run a standalone java program as a web server. It:

  • Installs packages needed to run a java program (openjdk-8-jre)

  • Installs an upstart script to run the program as a service

  • Relies on environment variables to configure your program

Let’s go through the code to see how it does this. You can clone the repo with git clone https://github.com/sweet-tooth-clojure/ansible-role-clojure-uberjar-webapp-app.git, or just can just browse the code online. Let’s look at the tasks in tasks/main.yml and refer to the variables in defaults/main.yml when we need to.

The first task is Add java 8 repo:

- name: Add java 8 repo
  apt_repository: repo='ppa:openjdk-r/ppa'
  tags:
    - install

Ubuntu uses the Advanced Packaging Tool (apt) system for managing packages. Packages are stored in repositories, and by default Ubuntu isn’t aware of the repository that has the OpenJDK package. (The OpenJDK package has tools we need to run java programs, so we probably need it.) This task adds the OpenJDK repo to apt’s list of repos.

You’ll notice that this task has a tags key, which we haven’t covered yet. Tags are labels that you can use to filter tasks when you’re applying playbooks, similar to the way tags are used everywhere else in the computer world. The Sweet Tooth roles use tags to avoid unnecessary work. For example, in the character sheet example, the file infrastructure/deploy has the line

ansible-playbook -i inventories/$1 deploy.yml --skip-tags=install

The flag --skip-tags=install does what you’d expect: it tells Ansible not to run any tasks that have the install tag. It makes sense that when you’re deploying an application you don’t need to try to install all of the software packages that should have been installed already when you set up the server.

Install required system packages

For the next task, we actually install openjdk-8 and a few other packages:

- name: Install required system packages
  apt: pkg="{{ item }}" state=installed update-cache=yes
  with_items:
  - openjdk-8-jre
  - wget
  - vim
  - curl
  tags:
    - install

This introduces another new key, with_items. When a task has the with_items key, it means run this task’s module for every element in the with_items sequence, assigning the element to the item variable; it’s a weird yaml-based looping construct. In this case, the module is apt and we’re using it to install each of the listed packages. openjdk-8-jre is the only tool that we absolutely need; the rest are useful for debugging. wget lets use easily download URLs, vim is a lightweight text editor, and curl is great for interacting with HTTP resources.

Notice that one of the arguments to the apt module is state=installed. You’re declaring the end state that you want the server to be in, as opposed to writing imperative code to take the actions which will result in the end state.

Check existence of local env file

Check existence of local env file has a few new keys: local_action, register, ignore_errors, and become.

- name: Check existence of local env file
  local_action: stat path="{{ clojure_uberjar_webapp_app_env_local_path }}"
  register: app_env_local_file
  ignore_errors: True
  become: False
  tags:
    - configure
[source, yaml]

To understand this task, it helps to know that its purpose is to check whether there is a file on your local filesystem (the app env file) which should be used to set environment variables that will be read as configuration by your application. This task stores the result of its check in a variable, and the very next task, Copy app env file, checks that variable before executing.

Now let’s look at each of the arguments to the Check existence of local env file task. local_action is the module we’re using, and it’s unsurprisingly used to run commands on your local machine. Its arguments are stat and path="{{ clojure_uberjar_webapp_app_env_local_path }}", and the result is that the task checks the status of the file at clojure_uberjar_webapp_app_env_local_path.

The register key tells the task where to store the result: the variable app_env_local_file. This is one way that Ansible lets you communicate among tasks: the result of one task is registered in a global variable that can then be read by other tasks. You’ll see in a second that app_env_local_file is read by Copy app env file.

Next, we have to set the ignore_errors key to True because Ansible’s default behavior is to throw an exception and stop applying the playbook if the path given to stat doesn’t exist. become is set to False because we’re running this task locally, and we don’t want to escalate privileges.

Finally, this task has the configure tag. I haven’t had occassion to use this tag explicitly, but hey, it doesn’t hurt, and it kind of serves as documentation.

Copy app env file

Once the task Check existence of local env file has finished, Ansible executes the task Copy app env file. This task obviously copies a file from your local machine to the remote machine, and its larger purpose in deploying and running a Clojure application is to give you a way to set environment variables for each environment (dev, staging, prod, etc). For example, you could set different Google Analytics tracking ids for dev, staging, and prod. Let’s look at from two perspectives: how the task is defined, and the role the task plays in setting up a functioning server.

The task has a couple new keys, when and template:

- name: Copy app env file
  file: src="{{ clojure_uberjar_webapp_app_env_local_path }}" dest="{{ clojure_uberjar_webapp_app_env_path }}"
  tags:
    - configure
  when: app_env_local_file.stat.exists

when defines the conditions for when the current task should run, in this case when app_env_local_file.stat.exists is truthy. The value app_env_local_file.stat.exists was set by the previous task.

We’re giving file two arguments, src and dest. It copies the file located at src on your local machine to dest on the remote machines. Easy peasy!

Now let’s look at the task from the perspective of the role it plays in running a Clojure application. To fully understand this, we’ll need to jump back and forth between the sweet-tooth-clojure.clojure-uberjar-webapp-app files and the character sheet example files.

In the task’s defintion, shown in the snippet above, the value of src is clojure_uberjar_webapp_app_env_local_path. By default, that value is files/env, but in our character sheet example we override it so that we can use a different file for each environment. Let’s walk through the whole process to see how that happens.

Let’s say that you run a playbook by executing the commands

cd character-sheet-example/infrastructure
./deploy dev

You’re trying to deploy your application to your dev environment, and you want to use your dev environment variables. When you call ./deploy dev, the shell script runs this command:

ansible-playbook -i inventories/dev sweet-tooth.yml --skip-tags=install

This says, apply the sweet-tooth.yml playbook with the inventory at inventories/dev. inventories/dev defines the dev inventory with files structured in a way that Ansible understands. (If you need a refresher on this, read Chapter 2).

If you look at character-sheet-example/infrastructure/ansible/inventories/dev/group_vars/webservers, you’ll see this:

# -*- mode: yaml -*-
---
clojure_uberjar_webapp_domain: localhost
clojure_uberjar_webapp_app_env_local_path: files/env/dev.sh

So that’s how we override the default value of clojure_uberjar_webapp_app_env_local_path to point to the dev file. Here’s a look at files/env/dev.sh (located in character-sheet-example/infrastructure/ansible):

export GA_ID="dev google analytics id"

All the file does is export environment variables; in this case, we’re setting a Google Analytics id for the dev environment.

This file gets uploaded to clojure_uberjar_webapp_app_env_path, which defaults to /var/www/localhost/config/.app for the dev inventory. Later on we’ll look at your remote machine handles the file /var/www/localhost/config/.app so that your application can read its environment variables.

One final note: in my own projects, I set git to ignore these environment files. My environment files contain slightly more sensitive information like API keys which shouldn’t get stored in version control. There are more secure ways to achieve the same result (I’ve heard that Ansible Vault is a good solution) but for small hobby sites it works OK. Please yell at me if this is a terrible idea!

Upload http env var file

The next task uploads a one-line file that exports the HTTP_SERVER_PORT environment variable:

- name: Upload http env var file
  template: src="templates/http-env.j2" dest="{{ clojure_uberjar_webapp_app_http_env_path }}"
  tags:
    - configure

This introduces a new key, template, which tells ansible to use the template the module. The template module works similarly to the file module: it copies the file from src on the local machine to dest on the destination machine. It differs from the file module in that the file getting copied is processed by the Jinja2 template system. Let’s look at the file’s contents to see why we might want to do this:

export HTTP_SERVER_PORT={{ clojure_uberjar_webapp_app_http_port }}

(You can find this file under templates/http-env.j2 in the clojure-uberjar-webapp-app repo.)

This file contains the familiar double-braces that we’ve been using in our tasks to interpolate variables. We want to do this so that we can run multiple Clojure applications on the same machine. For example, we could run a staging server on port 9000, and a production server on port 9010.

Later on, you’ll see how this file gets read so that its environment variable is available to your Clojure application.

Upload web app upstart config file

We use Ubuntu’s upstart service to start and stop our Clojure application. From the upstart home page: upstart handles starting of tasks and services during boot, stopping them during shutdown and supervising them while the system is running.

To use upstart, we need to upload a file that tells upstart how to start the Clojure application server, and that file’s template is located at templates/app-upstart.conf.j2:

start on runlevel [2345]

start on (started network-interface
or started network-manager
or started networking)

stop on (stopping network-interface
or stopping network-manager
or stopping networking)

respawn

script
  set -a
  . {{ clojure_uberjar_webapp_app_combined_config_path }}

  {{ clojure_uberjar_webapp_app_command }}
end script

stop on runlevel [016]

The start on and stop on bits are out of scope for this guide, so let’s skip those.

The line respawn means restart this application if it dies for some reason. Very important if you write code as buggy as I do!

The script…​ end script block tells upstart how to start your application. Within that block, we set environment variables with

  set -a
  . {{ clojure_uberjar_webapp_app_combined_config_path }}

set -a, is what ensures that your app can read the vars. You can read a little more about what set -` does in this StackExchange thread. The next line sources environment variables from a file that combines every configuration file that you’ve uploaded. (If you’re wondering what that period does at the beginning of the line, here’s a good explanation.) The configuration file’s contents will look something like this:

export GA_ID="dev google analytics id"
export DB_URI=datomic:free://localhost:4334/localhost
export HTTP_SERVER_PORT=3000

After we set environment variables, there’s this line: {{ clojure_uberjar_webapp_app_command }}, which will actually kick off the java process that runs your application. By default, that variable expands to something like:

/usr/bin/java -Xms300m -Xmx300m \
-Ddatomic.objectCacheMax=64m \
-Ddatomic.memoryIndexMax=64m \
-jar /var/www/localhost/localhost.jar server \
>> /var/log/localhost/localhost.log 2>&1
This command starts a JVM that executes the jar file at
_/var/www/localhost/localhost.jar_. The flags `-Xms300m` and
`-Xmx300m` set the minimum and maximum memory usage. The other flags
are Datomic-specific.

We pass the Clojure application one argument, server because I coded that application so that it will start an HTTP server if the first argument is server. You can see this in character sheet example’s source code, in the file src/backend/character_sheet/core.clj:

(ns character-sheet.core
  (:gen-class)
  (:require [datomic.api :as d]
            [com.stuartsierra.component :as component]
            [com.flyingmachine.datomic-booties.core :as datb]
            [character-sheet.system :as system]
            [character-sheet.config :as config]))

(defmacro final
  [& body]
  `(do (try (do ~@body)
            (catch Exception exc#
              (do (println "ERROR: " (.getMessage exc#))
                  (clojure.stacktrace/print-stack-trace exc#)
                  (System/exit 1))))
       (System/exit 0)))

(defn system
  []
  (system/new-system (config/full)))

(defn -main
  [cmd & args]
  (case cmd
    "server"
    (component/start-system (system))

    "db/install-schemas"
    (final
      (let [{:keys [db schema data]} (config/db)]
        (d/create-database db)
        (datb/conform (d/connect db)
                      schema
                      data
                      config/seed-post-inflate)))

    "deploy/check"
    ;; ensure that all config vars are set
    (final (config/full))))

Ignore final; it just does some error handling. The main (ah ha ha!) thing to note is that the -main function’s first argument, cmd, corresponds to the first command line argument. The -main function switches on cmd and evaluates the appropriate expression. If the argument is "server", it executes a function that starts a server.

The last bit of the config file specifies that the Clojure application should send standard out and standard error to /var/log/localhost/localhost.log.

This upstart file gets copied to /etc/init/{{ clojure_uberjar_webapp_app_service_name }}.conf on the remote machine because that’s where upstart expects to find its config files.

Make app log directory

This task creates a directory to store the application’s log file and ensures that the application has permission to write to the file.

Copy uberjar

This task copies the uberjar from your local machine to the remote machine. By defaul, it looks for the file at files/app.jar on your local.

This is the first task that’s tagged deploy. You might remember that we first provision our machines before deploying to them; tasks tagged deploy don’t get run when we provision our machines, because it’s possible that the programs that deploy tasks depend on haven’t been installed. For example, if we ran all the deploy tasks while provisioning, then Ansible would try to install the Clojure app’s Datomic schemas, but Datomic wouldn’t have been installed yet.

combine configs

This task uses the assemble module to concatenate all the files in the config directory into one file. The combined file gets sourced by the upstart script, as described in the Upload web app upstart config file section.

Copy check script and Run check

This task copies a shell script that you can use to do a quick sanity test on your app. Here’s the shell script’s template:

for f in {{ clojure_uberjar_webapp_app_config_dir }}/.[a-z]*; do
  source "$f";
done

java -Xms200m -Xmx400m  -jar {{ clojure_uberjar_webapp_app_jar_name }} deploy/check

The here’s how it renders for the dev deployment of the character sheet app (it gets saved to /var/www/localhost/check.sh):

for f in /var/www/localhost/config/.[a-z]*; do
  source "$f";
done

java -Xms200m -Xmx400m  -jar localhost.jar deploy/check

The script gets called in the next task, Run check. The script sources files that set environment variables, and then runs your uberjar with one argument: deploy/check. You need to set up your application to respond to that argument. If you look at character-sheet-example/src/backend/character_sheet/core.clj, you’ll see what gets run:

(ns character-sheet.core
  (:gen-class)
  (:require [datomic.api :as d]
            [com.stuartsierra.component :as component]
            [com.flyingmachine.datomic-booties.core :as datb]
            [character-sheet.system :as system]
            [character-sheet.config :as config]))

(defmacro final
  [& body]
  `(do (try (do ~@body)
            (catch Exception exc#
              (do (println "ERROR: " (.getMessage exc#))
                  (clojure.stacktrace/print-stack-trace exc#)
                  (System/exit 1))))
       (System/exit 0)))

(defn system
  []
  (system/new-system (config/full)))

(defn -main
  [cmd & args]
  (case cmd
    "server"
    (component/start-system (system))

    "db/install-schemas"
    (final
      (let [{:keys [db schema data]} (config/db)]
        (d/create-database db)
        (datb/conform (d/connect db)
                      schema
                      data
                      config/seed-post-inflate)))

    "deploy/check"
    ;; ensure that all config vars are set
    (final (config/full))))

If youlook at the -main function, you’ll see that it takes one argument, cmd, and a list optional arguments, args. cmd is the first argument sent to the program on the command line, so with our check.sh script the argument is deploy/check and the value of cmd is "deploy/check". The -main function checks the value of cmd, and you can see in the final line of the snippet that it executes (final (config/full)). The config/full function validates that all required environment variables are set, and throws an exception if any are missing.

Thus, the check.sh script ensures that all environment variables are set. If the script fails, then Ansible stops applying tasks, which is good! If it didn’t stop, it would try to restart your application server with a bad build, and your site would be unavailable or you’d get weird errors.

The last thing to note about the Run check task is that it sends three notifications:

- name: Run check
  command: chdir={{ clojure_uberjar_webapp_app_root }}/ bash ./check.sh
  tags:
    - deploy
    - check
  notify:
    - install schemas
    - restart web app

Notifications deserve their own section, so imma type out three equals signs because that what asciidoc uses to designate a new section heading:

Notifications

Notifications are a mechanism for signaling that some task should be run once and only once after all other tasks have finished. Tasks that respond to notifications are called handlers, and when defined as part of a role they live in the file handlers/main.yml. These tasks are defined in the same way as other tasks.

I often use notifications to signal that some service should be restarted. For example, there might be multiple tasks that should trigger an nginx restart: upgrading the program and uploading a new config file, for example. If neither trigger happens, you don’t want to restart nginx, and if one of them happens, you do. If both triggers happen, nginx will only be restarted once because handlers only run once no matter how many notifications they receive.

In the Run check task above, the notifications are:

  notify:
    - install schemas
    - restart web app

This will notify handlers with the names install schemas, combine configs, and restart web app.

I’ve defined install schemas as a handler so that it’s more modular. For example, the datomic role that we’ve been using defines an install schemas handler, but for your own app you might want to use postgresql. If your postgresql role defines an install schemas handler, then everything will be just peachy.

install schemas

You can find this defined in the clojure-uberjar-webapp-datomic-free repo in the file handlers/main.yml.

The Install schemas task runs your application, passing it one argument: db/install-schemas. It’s up to your application to handle it correctly. You can check out character-sheet-example/src/backend/character_sheet/core.clj again to see what it does. I won’t go into the details here; basically, it ensures that all the schemas you’ve defined exist in the Datomic database.

restart web app

This task runs at the end of your deployment. It tells upstart to restart your application. Sadly, there’s usually some downtime when you restart, but it’s still good enough for me! If you come up with a clover way to do zero-downtime deployments that don’t eat up all your machine’s memory, please let me know!

clojure-uberjar-webapp-nginx

nginx is super best very good web server, and now you are using it, making you super best very good devopser.

You can clone the repo of nginx tasks with git clone https://github.com/sweet-tooth-clojure/ansible-role-clojure-uberjar-webapp-nginx.git, or just can just browse the code online. Let’s look at the tasks in _tasks/main.yml.

Install required system packages

Not much new here: this just installs nginx and openssl. openssl is needed if you want to serve sites using ssl. Crazy how that works out.

Disable default site

nginx’s default site configuration doesn’t provide anything useful, and it sometimes makes it harder to debug issues. Kill it!

nginx base config

We replace nginx’s default base config with one that I like better. Explaining what each of the config settings does it out of scope.

nginx app config

This task is kind of insane. Let’s look at the code:

- name: nginx app config
  template:
    # "no-ssl.conf.j2" if clojure_uberjar_webapp_nginx_use_ssl isn't defined or is false
    src: "templates/app-nginx-{{ 'no-' if not (clojure_uberjar_webapp_nginx_use_ssl|d(False)) else '' }}ssl.conf.j2"
    dest: "{{ clojure_uberjar_webapp_nginx_sites_available }}"
  tags:
    - configure
  notify:
    - "nginx config changed"

That src key look cuh-ray-zay. The idea is that it’s deciding whether to use the app-nginx-no-ssl.conf.j2 template, or the app-nginx-ssl.conf.j2 template. It uses Jinja’s weirdo fake programming language to check whether the variable clojure_uberjar_webapp_nginx_use_ssl has been set to a truthy value, and uses that to pick which config template to use.

Let’s look at the no-ssl config file that’s been rendered and saved on the vagrant VM at /etc/nginx/sites-available/localhost.conf. If you’ve never looked at an nginx config file, it might be daunting; just stick with me and it’ll make sense.

upstream localhost_upstream { (1)
    server 127.0.0.1:3000;
    # put more servers here for load balancing
    # keepalive(reuse TCP connection) improves performance
    keepalive 32;
}

server {
    server_name localhost; (2)

    location /static/ {  # static content
        alias /var/www/localhost/public/;
    }

    location / {
        proxy_pass  http://localhost_upstream; (3)

        # tell http-kit to keep the connection
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        proxy_set_header x-forwarded-host  $host;
        proxy_set_header x-forwarded-proto $scheme;
        proxy_set_header x-forwarded-port  $server_port;


        access_log  /var/log/nginx/localhost.access.log;
        error_log   /var/log/nginx/localhost.error.log;
    }
}

To make sense of this file, it helps to take a step back and consider nginx’s role in handling HTTP requests. nginx’s job is to return some response for an HTTP request. Config files tell nginx how to handle HTTP requests.

For example, you might have a single nginx server for two domains, ilovehats.com and iloveheads.com. Your nginx configuration files might tell nginx to serve content from /var/www/ilovehats.com for one domain, and to server content from /var/www/iloveheads.com for the other.

In our case, we need to tell nginx to forward requests to our application server, or the upstream server. You define the upstream at ➊, giving it the name localhost_upstream. The next line, server 127.0.0.1:3000 tells nginx what address and port to use when it forwards requests. The Clojure application is running on the same machine and listening to port 3000, hence server 127.0.0.1:3000.

server_name localhost , at ➋, tells nginx to use this server configuration when the HTTP host is localhost. For my Community Picks site, this reads server_name www.communitypicks.com.

proxy_pass http://localhost_upstream, at ➌, tells nginx to handle requests by forwarding them to the server named localhost_upstream - the server that’s defined at ➊.

If you’re browsing the vagrant example, overall flow is:

  1. Your browser sends an HTTP requests with a host header whose value is localhost.

  2. The vagrant VM receives the request and sends it to nginx

  3. nginx examines the HTTP host to determine which server configuration to use

  4. It sees that the host is localhost and uses the configuration shown above

  5. The configuration directs nginx to forward the request to localhost_upstream. nginx forwards the request to the Clojure app

  6. The Clojure app handles the request and sends a response to nginx

  7. nginx forwards the response to your browser

The rest of the file sets some useful headers and sets log file locations.

The last task for this role simple creates a symlink at /etc/nginx/sites-enabled/localhost.conf pointing to /etc/nginx/sites-available/localhost.conf. This follows an nginx convention; the intention is to make it easy for you to disable a site by deleting its symlink in /etc/nginx/sites-enabled and the re-enable it just by re-symlinking.

It’s Up to You Now

And that’s it for our exhaustive explanation of the Ansible roles. I hope I’ve given you everything you need to get your own app online! Not only that, I hope you’ll feel comfortable customizing these roles and even writing your own.

If you do end up writing your own, remember that Vagrant is your friend. Treat it like a REPL: log into the VM and fool around until you get things working the way you want, then go back and record what you did in an Ansible playbook or a role. After that, recreate the VM and run your playbooks against it to make sure everything’s working the way it should. And have fun!

When you’re done, you’ll be better equipped to share your beautiful dark Clojure babies with the world. I can’t wait to see them! Good luck!