DEPRECATED - This has a race condition!

Please see this post for an updated version which works!

Following on from the post the other day on setting up DNS forwarding to Consul with SystemD, I wanted also to show how to get Consul up and running under Alpine Linux, as it’s a little more awkward in some respects.

To start with, I am going to setup Consul as a service - I didn’t do this in the Ubuntu version, as there are plenty of useful articles about that already, but that is not the case with Alpine.

Run Consul

First, we need to get a version of Consul and install it into our system. This script downloads 1.5.1 from Hashicorp’s releases site, installs it to /usr/bin/consul, and creates a consul user and group to run the daemon with:

CONSUL_VERSION=1.5.1

curl -sSL https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip -o /tmp/consul.zip

unzip /tmp/consul.zip
sudo install consul /usr/bin/consul

sudo addgroup -S consul
sudo adduser -S -D -h /var/consul -s /sbin/nologin -G consul -g consul consul

Next, we need to create the directories for the configuration and data to live in, and copy the init script and configuration file to those directories:

consul_dir=/etc/consul
data_dir=/srv/consul

sudo mkdir $consul_dir
sudo mkdir $data_dir
sudo chown consul:consul $data_dir

sudo mv /tmp/consul.sh /etc/init.d/consul
sudo chmod +x /etc/init.d/consul

sudo mv /tmp/consul.json $consul_dir/consul.json

The init script is pretty straight forward, but note that I am running the agent in this example in dev mode; don’t do this in production:

#!/sbin/openrc-run
CONSUL_LOG_FILE="/var/log/${SVCNAME}.log"

name=consul
description="A tool for service discovery, monitoring and configuration"
description_checkconfig="Verify configuration file"
daemon=/usr/bin/$name
daemon_user=$name
daemon_group=$name
consul_dir=/etc/consul
extra_commands="checkconfig"

start_pre() {
    checkpath -f -m 0644 -o ${SVCNAME}:${SVCNAME} "$CONSUL_LOG_FILE"
}

depend() {
    need net
    after firewall
}

checkconfig() {
    consul validate $consul_dir
}

start() {
    checkconfig || return 1

    ebegin "Starting ${name}"
        start-stop-daemon --start --quiet \
            -m --pidfile /var/run/${name}.pid \
            --user ${daemon_user} --group ${daemon_group} \
            -b --stdout $CONSUL_LOG_FILE --stderr $CONSUL_LOG_FILE \
            -k 027 --exec ${daemon} -- agent -dev -config-dir=$consul_dir
    eend $?
}

stop() {
    ebegin "Stopping ${name}"
        start-stop-daemon --stop --quiet \
            --pidfile /var/run/${name}.pid \
            --exec ${daemon}
    eend $?
}

Finally, a basic config file to launch consul is as follows:

{
    "data_dir": "/srv/consul/data",
    "client_addr": "0.0.0.0"
}

Now that all our scripts are in place, we can register Consul into the service manager, and start it:

sudo rc-update add consul
sudo rc-service consul start

You can check consul is up and running by using dig to get the address of the consul service itself:

dig @localhost -p 8600 consul.service.consul

Setup Local DNS with Unbound

Now that Consul is running, we need to configure a local DNS resolver to forward requests for the .consul domain to Consul. We will use Unbound as it works nicely on Alpine. It also has the wonderful feature of being able to send queries to a specific port, so no iptables rules needed this time!

The config file (/etc/unbound/unbound.conf) is all default values, with the exception of the last 5 lines, which let us forward DNS requests to a custom, and insecure, location:

#! /bin/bash

sudo apk add unbound

(
cat <<-EOF
server:
    verbosity: 1
    root-hints: /etc/unbound/root.hints
    trust-anchor-file: "/usr/share/dnssec-root/trusted-key.key"
    do-not-query-localhost: no
    domain-insecure: "consul"
stub-zone:
    name: "consul"
    stub-addr: 127.0.0.1@8600
EOF
) | sudo tee /etc/unbound/unbound.conf

sudo rc-update add unbound
sudo rc-service unbound start

We can validate this works again by using dig, but this time removing the port specification to hit 53 instead:

dig @localhost consul.service.consul

Configure DNS Resolution

Finally, we need to update /etc/resolv.conf so that other system tools such as ping and curl can resolve .consul addresses. This is a little more hassle on Alpine, as there are no head files we can push our nameserver entry into. Instead, we use dhclient which will let us prepend a custom nameserver (or multiple) when the interface is brought up, even when using DHCP:

#! /bin/bash

sudo apk add dhclient

(
cat <<-EOF
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
        domain-name, domain-name-servers, domain-search, host-name,
        dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
        netbios-name-servers, netbios-scope, interface-mtu,
        rfc3442-classless-static-routes, ntp-servers;
prepend domain-name-servers 127.0.0.1;
EOF
) | sudo tee /etc/dhcp/dhclient.conf

sudo rm /etc/resolv.conf # hack due to it dhclient making an invalid `chown` call.
sudo rc-service networking restart

The only thing of interest here is the little hack: we delete the /etc/resolv.conf before restarting the networking service, as if you don’t do this, you get errors about “chmod invalid option resource=…”.

We can varify everything works in the same way we did on Ubuntu; curl to both a .consul and a public address:

$ curl -s -o /dev/null -w "%{http_code}\n" http://consul.service.consul:8500/ui/
200

$ curl -s -o /dev/null -w "%{http_code}\n" http://google.com
301

End

This was a bit easier to get started with than the Ubuntu version as I knew what I was trying to accomplish this time - however making a good init.d script was a bit more hassle, and the error from chmod took some time to track down.