Green Up 2015

Last year I wrote about Green Up and my use of varnish to cache the API server that was created to service the various applications Xenon Apps created for the Green Up Vermont NPO.

This year we're running the same setup for them, and this past weekend I got together with the other developers and reworked the server for the API. It's running a pretty small stack:

And that's it. The machine is small, running with only 256MB of RAM. It might surprise you that this is the same machine that serviced the 300 or so users of the application last year.

Varnish

Varnish is performing the same duties as last year, acting as backend director and load balancer for the running instances of the C APIs. The full configuration file uses 2 backends right now and also enables the CORS headers for varnish during the delivery phase. Here's the full vcl file:

backend default {
    .host = "127.0.0.1";
    .port = "31337";
    .probe = {
	.url ="/api/";
	.interval = 10s;
	.timeout = 2s;
	.window = 5;
	.threshold = 3;
    }
}

backend backup {
    .host = "127.0.0.1";
    .port = "31338";
    .probe = {
	.url ="/api/";
	.interval = 10s;
	.timeout = 2s;
	.window = 5;
	.threshold = 3;
    }
}

sub vcl_recv {
	if ( req.request == "POST" || req.request == "PUT" ) {
		#Invalidate only the neccesary things per endpoint
		if ( req.url ~ "(?i)/api/comments" ) {
			ban("req.url ~ (?i)/api/comments");
			ban("req.url ~ (?i)/api/pins");
		}
		if ( req.url ~ "(?i)/api/pins" ) {
			ban("req.url ~ (?i)/api/comments");
			ban("req.url ~ (?i)/api/pins");
		}
		if ( req.url ~ "(?i)/api/heatmap" ) {
			ban("req.url ~ (?i)/api/heatmap");
		}
		if( req.url ~ "(?i)/api/debug" ) {
			ban("req.url ~ (?i)/api/debug");
		}
		#Don't cache POST or PUT (no sense in doing so)
		return(pass);
	}
}

director backup_director round-robin {
	{
		.backend = default;
	}
	{
		.backend = backup;
	}
}

sub vcl_fetch { 
	set req.backend = backup_director;
}

sub vcl_deliver {
	set resp.http.Access-Control-Allow-Origin = "*";
	set resp.http.Access-Control-Allow-Methods = "GET, POST, PUT, DELETE";
	set resp.http.Access-Control-Allow-Headers = "Content-Type";
	
	if (obj.hits >= 0)
	{
		set resp.http.X-Cache = "HIT";
	}
	else
	{
		set resp.http.X-Cache = "MISS";
	}
}

You'll notice the invalidation on the POST requests is exactly the same as it was last year. And also the header X-Cache is handy for debugging if you're recieving a cached result or not.

API Servers

Something that is a little bit different this year is the way which the API servers are setup. Last year my system admin hat wasn't quite so feathered, this year, I've got a new set of tools to apply. Specifically, subsystem scripts.

I started out by deciding how best to organize the multiple copies of the API servers that would run, deciding on a simple directory structure inside /var/run/ directory. I created a green-serv folder, and then for each instance of the API running I created a folder based on the port it would listen in on. So in the end I had 3 folders setup:

user@server:/var/run/green-serv# ls
31337  31338  80  

This was done to make it easier to start up multiple instances of the server itself. If we take a look at the script I wrote for the service:

#!/bin/sh
#
# Subsystem file for green-serv
#
# chkconfig: 036 95 05
# description: green-serv
#

# To install: 
#   chmod +x, mv to /etc/init.d/green-serv, then chkconfig --add green-serv

RETVAL=0
prog="green-serv"
PORT="80"
if [ $# -eq 2 ]; then
	PORT=$2
fi
PID_FILE=/var/run/green-serv/$PORT/GREENSERV_PID.pid
PORT_FILE=/var/run/green-serv/$PORT/GREENSERV.port
CMD=/var/run/green-serv/$PORT/green-serv
LOGFILE=/var/log/green-serv.log

start() {
	echo -n $"Staring $prog:"
	echo $PORT > $PORT_FILE
	nohup $CMD >> $LOGFILE 2>&1 &
	echo
}

stop() {
	echo -n $"Stopping $prog:"
	if [ -f $PID_FILE ]; then kill `cat $PID_FILE`; fi
	echo
}

if [ "$(id -u)" != "0" ]; then
	echo "This script must be run as root" 1>&2
	exit 1
fi

case "$1" in
	start)
		start
		;;
	stop)
		stop
		;;
	restart)
		stop
		start
		;;
	*)
		echo $"Usage: $0 {start [port]|stop [port]|restart}"
		RETVAL=1
esac
exit $RETVAL

You might notice that unlike your common service file such as apache or mysql, this one can take an additional argument! Specifically, doing something like this: service green-serv start 3000 would start up a server instance on port 3000 (as long as that directory exists and has the server binary).

A couple changes I made today on the API server was to have it write its own process id to a file, and to load which port to run on from a file within the same directory as the binary. This helped facilate this script to be an easy one to write.

In addition, I updated the Makefile to have an install command to make deploying any changes out to the various run folders easy:

install:
	mkdir -p /var/run/green-serv/80
	for portdir in `ls -d /var/run/green-serv/*/`; do \
		cp green-serv $$portdir;\
	done
	chmod +x green-serv.d
	cp green-serv.d /etc/init.d/green-serv

This is pretty standard, and only handles just an 80 port as far as creating goes, but it does support updating the binaries for any directory in the green-serv run folder. The only thing to make note of is the weird $$portdir -- which if you're familiar with bash will make you assume that I had a typo and wrote 2 dollar signs instead of one. Nope, within a Makefile you need two dollar signs in order to differentiate the make and bash variables from one another.

UFW

The last thing to setup was a firewall. The uncomplicated firewall (ufw) is, as its name implies, uncomplicated. A few simple commands are all you really need:

cat /etc/ufw/applications.d/varnish
[Varnish]
title=Web Proxy
description=Proxy caching server
ports=80/tcp

sudo ufw allow OpenSSH
sudo ufw allow Varnish
sudo ufw default deny
sudo ufw enable

And you're pretty much all set. Ports that aren't used by the applications listed in /etc/ufw/applications.d will be denied by default. Note that you probably want to triple check that you've allowed SSH through so you don't kill your connection to your server. A good rule of thumb is to enable the ports you're using for SSH, then enable the firewall and try to ssh to the server from a different shell and if it doesn't work, disable the firewall and try again with different settings.

The firewall is in place to make sure that the API servers are only accessed through Varnish and not from the outside world. This enables us to be sure that varnish will handle all the load and we won't tax our servers from the outside world by anything.

Summary

In conclusion, the API server is ready to go for round two of Green Up day and we're all hoping that it will go as smoothly as it did last year!