NSQ is an awesome and extremely simple distributed message queue. You can simply use it by publishing messages — that is arbitrary text, I often use JSON — on a so called "topic". A second process can than attach to a topic to consume messages on a so called "channel". Each messages is forwarded into each channel (at least once). This blag post covers deployment of NSQ in the simplest scenario: one box running everything.
There are three different services you need to run for a minimal NSQ deployment:
nsqd
, nsqlookupd
, and nsqadmin
. nsqd
is the "main service". You are supposed to run it alongside every message producer and it communicates with the configured nsqlookupd
which in turn then enables infrastructure discovery: it can be contacted by consumers to figure out where to connect to. Finally, nsqadmin
is just a Web UI to carry out administrative tasks and get some high-level insights (like total number of processed messages).
# Preparation and Download
Let's first create a new user called nsq
and download the self-contained binary package and unpack it into a sensible location while adjusting the permissions:
useradd -r -s /bin/false nsq
mkdir -p /usr/local/nsq/bin
chown -v nsq:nsq /usr/local/nsq
cd /tmp
wget 'https://s3.amazonaws.com/bitly-downloads/nsq/nsq-1.2.1.linux-amd64.go1.16.6.tar.gz'
tar -xzf nsq-*.tar.gz
rm nsq-*.tar.gz
mv -v nsq-*/bin /usr/local/nsq
rmdir -v nsq-*
chown -v nsq:nsq /usr/local/nsq/bin /usr/local/nsq/bin/*
The plan is to have the binaries in /usr/local/nsq/bin
and any data NSQ needs to write to disk — it does so in case of a shutdown — to /usr/local/nsq/data
.
The next step now is to figure out the command line arguments necessary for the three services to properly run. Before we can actually do that, we need to make a couple of network topology decisions (and I really hope, I made the right ones):
* Use client-side SSL certificates for authentication (I didn't look into nsq's internal auth mechanisms, but it is probable that I wouldn't have trusted them enough anyway).
* Expose nsqadmin and the nsqd itself through NGINX which serves as a reverse proxy (there is a "NGINX with client-side certificate"-sidequest here).
# NGINX: Client-Side Certificates
Again, this is just a sidequest, so I'll leave you with the two scripts I wrote to create the certificate authority (CA) and the individual client-side certificates. Starting with creating the CA:
#!/bin/bash
set -xe
CURVE=secp384r1
# certificate authority (ca) key and certificate (crt)
openssl ecparam -out ca.pem.key -name $CURVE -genkey
openssl req -new -x509 -days 365 -sha512 -key ca.pem.key -out ca.pem.crt -subj '/C=CA/ST=British Columbia/L=Vancouver/O=Foo/OU=Security Department/CN=Bar'
# server key and certificate signing request (csr)
openssl ecparam -out server.pem.key -name $CURVE -genkey
openssl req -new -key server.pem.key -out server.pem.csr -subj "/C=CA/ST=British Columbia/L=Vancouver/O=Foo/OU=Security Department/CN=Bar"
# sign sever certificate with ca
openssl x509 -req -days 365 -sha512 -in server.pem.csr -CA ca.pem.crt -CAkey ca.pem.key -set_serial 01 -out server.pem.crt
The above script is not intended to just be copied&pasted, read it and understand what the different lines do and adjust them to your needs.
There is a rabbit hole within this sidequest: I obviously wanted to use Elliptic-curve cryptography (ECC) because RSA sucks (factoring being sub-exponential and key sizes growing too fast and all that jazz). And with ECC you need to choose an actual curve you want to use. But not all browsers have support for all curves. So I ended up with a relatively lame choice of secp384r1
(don't ask my why I find this choice lame, it's a gut-feeling, probably because all my friends told me to use a brainpool curve).
And then there's a second script I use to create the actual client-side certificates:
#!/bin/bash
set -xe
CURVE=secp384r1
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <client name>"
exit 1
fi
# client key and csr
openssl ecparam -out client_$1.pem.key -name $CURVE -genkey
openssl req -new -key client_$1.pem.key -out client_$1.pem.csr -subj "/C=CA/ST=British Columbia/L=Vancouver/O=Foo/OU=Security Department/CN=$1"
# sign certificate
openssl x509 -req -days 365 -sha512 -in client_$1.pem.csr -CA ca.pem.crt -CAkey ca.pem.key -set_serial 01 -out client_$1.pem.crt
# convert to pkcs
openssl pkcs12 -export -in client_$1.pem.crt -inkey client_$1.pem.key -out client_$1.p12 -name "Foo client-side certificate for $1"
Apart from the client_*.pem.key
(the client private key) and client_*.pem.csr
(the client certificate) files, the above script will also bundle up a handy .p12
file that you can just double click under Windows to install it.
All these scripts need some love: you should probably make the second one safe against spaces in $1
and also somehow increment the serial number after the 365 days. But this blag post is about NSQ!
# How to run NSQ services
Let's start with the lookup service:
/usr/local/nsq/bin/nsqlookupd -http-address 127.0.0.1:4161 -tcp-address 127.0.0.1:4160
Not much to say here, I just specified the host names to make sure to only bind to localhost and not expose this service directly to the internet (that is, without the NGINX reverse proxy). Now that we have a port fixed for nsqlookupd
, we can start the main service:
/usr/local/nsq/bin/nsqd -http-address 127.0.0.1:4151 -tcp-address 127.0.0.1:4150 -lookupd-tcp-address 127.0.0.1:4160 -broadcast-address nsqd.example.com -broadcast-http-port 80
As above, I specified the host name to only bind to localhost as well as the previously configured lookupd address and port. I played around with the last two command line switches and now think it works like this: I told nsqd to announce itself under this publicly reachable host name which will be used by nsqadmin for example. Requests to the plain http port 80 will be redirected to https by NGINX. I didn't specify 443 directly because when I did, some component wanted to speak plain HTTP with an HTTPS endpoint. Again: there's some "opportunity for understanding", but we are already in a rabbit hole of a sidequest.
Ok, so let's finally start the admin UI:
/usr/local/nsq/bin/nsqadmin -http-address 127.0.0.1:4171 -lookupd-http-address 127.0.0.1:4161 -http-client-tls-cert /path/to/client/certs/client_nsqadmin.pem.crt --http-client-tls-key /path/to/client/certs/client_nsqadmin.pem.key
Initially I thought the admin UI performs requests directly to the publicly available nsqd endpoints. But as it turns out, the server-component of the admin UI proxies those requests. That's why it needs a client-side certificate (which can be easily generated by calling ./create-client-certificate.sh nsqadmin
).
The following table gives a quick overview about the different ports and services:
| Service | Port | Protocol | Public Endpoint |
|--- |--- |--- |--- |
| nsqlookupd | 4161 | HTTP | - |
| nsqlookupd | 4160 | TCP | - |
| nsqd | 4151 | HTTP | https://nsqd.exmaple.com |
| nsqd | 4150 | TCP | - |
| nsqadmin | 4171 | HTTP | https://nsqadmin.exmaple.com |
# NGINX: Reverse Proxy Configuration
For completeness sacke I will include the NGINX configuration here as well:
server {
server_name nsqadmin.example.com;
listen [::]:80;
listen 80;
# Let's Encrypt challenges
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt/;
}
location / {
return 308 https://nsqadmin.example.com$request_uri;
}
}
server {
server_name nsqadmin.example.com;
listen 443 ssl;
# Let's Encrypt certificate stuff here
# client side certificate
ssl_client_certificate /path/to/client/certs/ca.pem.crt;
ssl_verify_client optional; # access restriction handled in location block below
access_log /var/log/nginx/nsqadmin.example.com_access.log;
error_log /var/log/nginx/nsqadmin.example.com_error.log info;
charset utf8;
location / {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://127.0.0.1:4171;
}
}
The configuration for nsqd looks exactly the same but with nsqd
instead of nsqadmin
and 4151
instead of 4171
.
# init.d Services
In order to properly turn the above commands into services in init.d, we need to write service scripts able to spawn them into the background and also find the service to properly shut them down. For this, the start-stop-daemon
command seems to be the way to go. Below is the /etc/init.d/nsqd
script, the other two can simply be created by some smart copy&pasting:
#!/bin/bash
### BEGIN INIT INFO
# Provides: nsqd
# Required-Start: $local_fs $network
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: nsqd service
# Description: nsqd is the daemon that receives, queues, and delivers messages to clients.
### END INIT INFO
# Carry out specific functions when asked to by the system
case "$1" in
start)
echo "Starting nsqd..."
start-stop-daemon --start --background --oknodo --user nsq --pidfile /run/nsqd.pid --make-pidfile --name nsqd --exec /usr/local/nsq/bin/nsqd --chdir /usr/local/nsq/data --chuid nsq -- -http-address 127.0.0.1:4151 -tcp-address 127.0.0.1:4150 -lookupd-tcp-address 127.0.0.1:4160 -broadcast-address nsqd.example.com -broadcast-http-port 80
;;
stop)
echo "Stopping nsqd..."
start-stop-daemon --stop --oknodo --user nsq --pidfile /run/nsqd.pid --remove-pidfile --name nsqd --retry 5
sleep 2
;;
*)
echo "Usage: /etc/init.d/nsqd {start|stop}"
exit 1
;;
esac
exit 0
# Testing Functionality
You can now fire up an NSQ tail command:
/usr/local/nsq/bin/nsq_tail -channel foo -topic test -nsqd-tcp-address 127.0.0.1:4150
And use curl to populate the test
topic:
curl -d 'hello NTF' 'http://127.0.0.1:4151/pub?topic=test'
Full-disclosure here: using nsq_tail
with the -lookupd-http-address 127.0.0.1:4161
switch instead of -nsqd-tcp-address 127.0.0.1:4150
doesn't work because nsqd announces itself under it's publicly available hostname (as instructed) and to access that, a client-side certificate would be necessary.