---
- 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"
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:
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:
---
# 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:
-
Your browser sends an HTTP requests with a host header whose value is localhost.
-
The vagrant VM receives the request and sends it to nginx
-
nginx examines the HTTP host to determine which server configuration to use
-
It sees that the host is localhost and uses the configuration shown above
-
The configuration directs nginx to forward the request to localhost_upstream. nginx forwards the request to the Clojure app
-
The Clojure app handles the request and sends a response to nginx
-
nginx forwards the response to your browser
The rest of the file sets some useful headers and sets log file locations.
nginx link app config
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!