Is your EC2/VM bill giving you sleepless nights?
Are your EC2 instances under-utilized? Have you been wondering if there was an easy way to maximize the EC2/VM usage?
Are you investing too much in your Control Plane and wish you could divert some of that investment towards developing more features in your applications (business logic)?
Is your Configuration Management system overwhelming you and seems to have got a life of its own?
Do you have legacy applications that do not need Docker at all?
Would you like to simplify your deployment toolchain to streamline your workflows?
Have you been recommended to use Kubernetes as a problem to fix all your woes, but you aren’t sure if Kubernetes is actually going to help you?
Do you feel you are moving towards Docker, just so that Kubernetes can be used?
If you answered “Yes” to any of the questions above, do read on, this article is just what you might need.
There are steps to create a simple setup on your laptop at the end of the article.
Introduction
In the following article, we will present the typical components of a multi-tier application and how it is setup and deployed.
We shall further go on to see how the same application deployment can be remodeled for scale using any Cloud Infrastructure. (The same software toolchain can be used to deploy the application on your On-Premise Infrastructure as well)
The tools that we propose are Nomad and Consul. We shall focus more on how to use these tools, rather than deep-dive into the specifics of the tools. We will briefly see the features of the software which would help us achieve our goals.
- Nomad is a distributed workload manager for not only Docker containers, but also for various other types of workloads like legacy applications, JAVA, LXC, etc.
More about Nomad Drivers here: Nomadproject.io, application delivery with HashiCorp, introduction to HashiCorp Nomad.
- Consul is a distributed service mesh, with features like service registry and a key-value store, among others.
Using these tools, the application/startup workflow would be as follows:
Nomad will be responsible for starting the service.
Nomad will publish the service information in Consul. The service information will include details like:
- Where is the application running (IP:PORT) ?
- What “service-name” is used to identify the application?
- What “tags” (metadata) does this application have?
A Typical Application
A typical application deployment consists of a certain fixed set of processes, usually coupled with a database and a set of few (or many) peripheral services.
These services could be primary (must-have) or support (optional) features of the application.

Note: We are aware about what/how a proper “service-oriented-architecture” should be, though we will skip that discussion for now. We will rather focus on how real-world applications are setup and deployed.
Simple Multi-tier Application
In this section, let’s see the components of a multi-tier application along with typical access patterns from outside the system and within the system.
- Load Balancer/Web/Front End Tier
- Application Services Tier
- Database Tier
- Utility (or Helper Servers): To run background, cron, or queued jobs.

Using a proxy/loadbalancer, the services (Service-A, Service-B, Service-C) could be accessed using distinct hostnames:
- a.example.tld
- b.example.tld
- c.example.tld
For an equivalent path-based routing approach, the setup would be similar. Instead of distinct hostnames, the communication mechanism would be:
- common-proxy.example.tld/path-a/
- common-proxy.example.tld/path-b/
- common-proxy.example.tld/path-c/
Problem Scenario 1
Some of the basic problems with the deployment of the simple multi-tier application are:
- What if the service process crashes during its runtime?
- What if the host on which the services run shuts down, reboots or terminates?
This is where Nomad’s feature of always keep the service running would be useful.
In spite of this auto-restart feature, there could be issues if the service restarts on a different machine (i.e. different IP address).
In case of Docker and ephemeral ports, the service could start on a different port as well.
To solve this, we will use the service discovery feature provided by Consul, combined with a with a Consul-aware load-balancer/proxy to redirect traffic to the appropriate service.
The order of the operations within the Nomad job will thus be:
- Nomad will launch the job/task.
- Nomad will register the task details as a service definition in Consul.
(These steps will be re-executed if/when the application is restarted due to a crash/fail-over) - The Consul-aware load-balancer will route the traffic to the service (IP:PORT)
Multi-tier Application With Load Balancer
Using the Consul-aware load-balancer, the diagram will now look like:

The details of the setup now are:
- A Consul-aware load-balancer/proxy; the application will access the services via the load-balancer.
- 3 (three) instances of service A; A1, A2, A3
- 3 (three) instances of service B; B1, B2, B3
The Routing Question
At this moment, you could be wondering, “Why/How would the load-balancer know that it has to route traffic for service-A to A1/A2/A3 and route traffic for service-B to B1/B2/B3 ?”
The answer lies in the Consul tags which will be published as part of the service definition (when Nomad registers the service in Consul).
The appropriate Consul tags will tell the load-balancer to route traffic of a particular service to the appropriate backend. (+++)
Let’s read that statement again (very slowly, just to be sure); The Consul tags, which are part of the service definition, will inform (advertise) the load-balancer to route traffic to the appropriate backend.
The reason to dwell upon this distinction is very important, as this is different from how the classic load-balancer/proxy software like HAProxy or NGINX are configured. For HAProxy/NGINX the backend routing information resides with the load-balancer instance and is not “advertised” by the backend.
The traditional load-balancers like NGINX/HAProxy do not natively support dynamic reloading of the backends. (when the backends stop/start/move-around). The heavy lifting of regenerating the configuration file and reloading the service is left up to an external entity like Consul-Template.
The use of a Consul-aware load-balancer, instead of a traditional load-balancer, eliminates the need of external workarounds.
The setup can thus be termed as a zero-configuration setup; you don’t have to re-configure the load-balancer, it will discover the changing backend services based on the information available from Consul.
Problem Scenario 2
So far we have achieved a method to “automatically” discover the backends, but isn’t the Load-Balancer itself a single-point-of-failure (SPOF)?
It absolutely is, and you should always have redundant load-balancers instances (which is what any cloud-provided load-balancer has).
As there is a certain cost associated with using “cloud-provided load-balancer”, we would create the load-balancers ourselves and not use cloud-provided load-balancers.
To provide redundancy to the load-balancer instances, you should configure them using and AutoScalingGroup (AWS), VM Scale Sets (Azure), etc.
The same redundancy strategy should also be used for the worker nodes, where the actual services reside, by using AutoScaling Groups/VMSS for the worker nodes.
The Complete Picture

Installation and Configuration
Given that nowadays laptops are pretty powerful, you can easily create a test setup on your laptop using VirtualBox, VMware Workstation Player, VMware Workstation, etc.
As a prerequisite, you will need a few virtual machines which can communicate with each other.
NOTE: Create the VMs with networking set to bridged mode.
The machines needed for the simple setup/demo would be:
- 1 Linux VM to act as a server (srv1)
- 1 Linux VM to act as a load-balancer (lb1)
- 2 Linux VMs to act as worker machines (client1, client2)
*** Each machine can be 2 CPU 1 GB memory each.
The configuration files and scripts needed for the demo, which will help you set up the Nomad and Consul cluster are available here.
Setup the Server
Install the binaries on the server
# install the Consul binary
wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip -O consul.zip
unzip -o consul.zip
sudo chown root:root consul
sudo mv -fv consul /usr/sbin/
# install the Nomad binary
wget https://releases.hashicorp.com/nomad/0.11.3/nomad_0.11.3_linux_amd64.zip -O nomad.zip
unzip -o nomad.zip
sudo chown root:root nomad
sudo mv -fv nomad /usr/sbin/
# install Consul's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/consul.service -O consul.service
sudo chown root:root consul.service
sudo mv -fv consul.service /etc/systemd/system/consul.service
# install Nomad's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/nomad.service -O nomad.service
sudo chown root:root nomad.serviceCreate the Server Configuration
### On the server machine ...
### Consul
sudo mkdir -p /etc/consul/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/consul/server.hcl -O /etc/consul/server.hcl
### Edit Consul's server.hcl file and setup the fields 'encrypt' and 'retry_join' as per your cluster.
sudo vim /etc/consul/server.hcl
### Nomad
sudo mkdir -p /etc/nomad/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/nomad/server.hcl -O /etc/nomad/server.hcl
### Edit Nomad's server.hcl file and setup the fields 'encrypt' and 'retry_join' as per your cluster.
sudo vim /etc/nomad/server.hcl
### After you are done with the edits ...
sudo systemctl daemon-reload
sudo systemctl enable consul nomad
sudo systemctl restart consul nomad
sleep 10
sudo consul members
sudo nomad server membersSetup the Load-Balancer
Install the binaries on the server
# install the Consul binary
wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip -O consul.zip
unzip -o consul.zip
sudo chown root:root consul
sudo mv -fv consul /usr/sbin/
# install the Nomad binary
wget https://releases.hashicorp.com/nomad/0.11.3/nomad_0.11.3_linux_amd64.zip -O nomad.zip
unzip -o nomad.zip
sudo chown root:root nomad
sudo mv -fv nomad /usr/sbin/
# install Consul's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/consul.service -O consul.service
sudo chown root:root consul.service
sudo mv -fv consul.service /etc/systemd/system/consul.service
# install Nomad's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/nomad.service -O nomad.service
sudo chown root:root nomad.service
sudo mv -fv nomad.service /etc/systemd/system/nomad.serviceCreate the Load-Balancer Configuration
### On the load-balancer machine ...
### for Consul
sudo mkdir -p /etc/consul/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/consul/client.hcl -O /etc/consul/client.hcl
### Edit Consul's client.hcl file and setup the fields 'name', 'encrypt', 'retry_join' as per your cluster.
sudo vim /etc/consul/client.hcl
### for Nomad ...
sudo mkdir -p /etc/nomad/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/nomad/client.hcl -O /etc/nomad/client.hcl
### Edit Nomad's client.hcl file and setup the fields 'name', 'node_class', 'encrypt', 'retry_join' as per your cluster.
sudo vim /etc/nomad/client.hcl
### After you are done with the edits ...
sudo systemctl daemon-reload
sudo systemctl enable consul nomad
sudo systemctl restart consul nomad
sleep 10
sudo consul members
sudo nomad server members
sudo nomad node status -verboseSetup the Client (Worker) Machines
Install the binaries on the server
# install the Consul binary
wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip -O consul.zip
unzip -o consul.zip
sudo chown root:root consul
sudo mv -fv consul /usr/sbin/
# install the Nomad binary
wget https://releases.hashicorp.com/nomad/0.11.3/nomad_0.11.3_linux_amd64.zip -O nomad.zip
unzip -o nomad.zip
sudo chown root:root nomad
sudo mv -fv nomad /usr/sbin/
# install Consul's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/consul.service -O consul.service
sudo chown root:root consul.service
sudo mv -fv consul.service /etc/systemd/system/consul.service
# install Nomad's service file
wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/systemd/nomad.service -O nomad.service
sudo chown root:root nomad.service
sudo mv -fv nomad.service /etc/systemd/system/nomad.serviceCreate the Worker Configuration
### On the client (worker) machine ...
### Consul
sudo mkdir -p /etc/consul/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/consul/client.hcl -O /etc/consul/client.hcl
### Edit Consul's client.hcl file and setup the fields 'name', 'encrypt', 'retry_join' as per your cluster.
sudo vim /etc/consul/client.hcl
### Nomad
sudo mkdir -p /etc/nomad/
sudo wget https://raw.githubusercontent.com/shantanugadgil/hashistack/master/config/nomad/client.hcl -O /etc/nomad/client.hcl
### Edit Nomad's client.hcl file and setup the fields 'name', 'node_class', 'encrypt', 'retry_join' as per your cluster.
sudo vim /etc/nomad/client.hcl
### After you are sure about your edits ...
sudo systemctl daemon-reload
sudo systemctl enable consul nomad
sudo systemctl restart consul nomad
sleep 10
sudo consul members
sudo nomad server members
sudo nomad node status -verboseTest the Setup
For the sake of simplicity, we shall assume the following IP addresses for the machines. (You can adapt the IPs as per your actual cluster configuration)
srv1: 192.168.1.11
lb1: 192.168.1.101
client1: 192.168.201
client1: 192.168.202
You can access the web GUI for Consul and Nomad at the following URLs:
Consul: http://192.168.1.11:8500
Nomad: http://192.168.1.11:4646
Login into the server and start the following watch command:
# watch -n 5 "consul members; echo; nomad server members; echo; nomad node status -verbose; echo; nomad job status"Output:
Node Address Status Type Build Protocol DC Segment
srv1 192.168.1.11:8301 alive server 1.5.1 2 dc1 <all>
client1 192.168.1.201:8301 alive client 1.5.1 2 dc1 <default>
client2 192.168.1.202:8301 alive client 1.5.1 2 dc1 <default>
lb1 192.168.1.101:8301 alive client 1.5.1 2 dc1 <default>
Name Address Port Status Leader Protocol Build Datacenter Region
srv1.global 192.168.1.11 4648 alive true 2 0.9.3 dc1 global
ID DC Name Class Address Version Drain Eligibility Status
37daf354... dc1 client2 worker 192.168.1.202 0.9.3 false eligible ready
9bab72b1... dc1 client1 worker 192.168.1.201 0.9.3 false eligible ready
621f4411... dc1 lb1 lb 192.168.1.101 0.9.3 false eligible readySubmit Jobs
Login into the server (srv1) and download the sample jobs
Run the load-balancer job
# nomad run fabio_docker.nomadOutput:
==> Monitoring evaluation "bb140467"
Evaluation triggered by job "fabio_docker"
Allocation "1a6a5587" created: node "621f4411", group "fabio"
Evaluation status changed: "pending" -> "complete"
==> Evaluation "bb140467" finished with status "complete"Check the status of the load-balancer
# nomad alloc status 1a6a5587Output:
ID = 1a6a5587
Eval ID = bb140467
Name = fabio_docker.fabio[0]
Node ID = 621f4411
Node Name = lb1
Job ID = fabio_docker
Job Version = 0
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 1m9s ago
Modified = 1m3s ago
Task "fabio" is "running"
Task Resources
CPU Memory Disk Addresses
5/200 MHz 10 MiB/128 MiB 300 MiB lb: 192.168.1.101:9999
ui: 192.168.1.101:9998
Task Events:
Started At = 2019-06-13T19:15:17Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2019-06-13T19:15:17Z Started Task started by client
2019-06-13T19:15:12Z Driver Downloading image
2019-06-13T19:15:12Z Task Setup Building Task Directory
2019-06-13T19:15:12Z Received Task received by clientRun the service ‘foo’
# nomad run foo_docker.nomadOutput:
==> Monitoring evaluation "a994bbf0"
Evaluation triggered by job "foo_docker"
Allocation "7794b538" created: node "9bab72b1", group "gowebhello"
Allocation "eecceffc" modified: node "37daf354", group "gowebhello"
Evaluation status changed: "pending" -> "complete"
==> Evaluation "a994bbf0" finished with status "complete"Check the status of service ‘foo’
# nomad alloc status 7794b538Output:
ID = 7794b538
Eval ID = a994bbf0
Name = foo_docker.gowebhello[1]
Node ID = 9bab72b1
Node Name = client1
Job ID = foo_docker
Job Version = 1
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 9s ago
Modified = 7s ago
Task "gowebhello" is "running"
Task Resources
CPU Memory Disk Addresses
0/500 MHz 4.2 MiB/256 MiB 300 MiB http: 192.168.1.201:23382
Task Events:
Started At = 2019-06-13T19:27:17Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2019-06-13T19:27:17Z Started Task started by client
2019-06-13T19:27:16Z Task Setup Building Task Directory
2019-06-13T19:27:15Z Received Task received by clientRun the service ‘bar’
# nomad run bar_docker.nomadOutput:
==> Monitoring evaluation "075076bc"
Evaluation triggered by job "bar_docker"
Allocation "9f16354b" created: node "9bab72b1", group "gowebhello"
Allocation "b86d8946" created: node "37daf354", group "gowebhello"
Evaluation status changed: "pending" -> "complete"
==> Evaluation "075076bc" finished with status "complete"Check the status of service ‘bar’
# nomad alloc status 9f16354bOutput:
ID = 9f16354b
Eval ID = 075076bc
Name = bar_docker.gowebhello[1]
Node ID = 9bab72b1
Node Name = client1
Job ID = bar_docker
Job Version = 0
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 4m28s ago
Modified = 4m16s ago
Task "gowebhello" is "running"
Task Resources
CPU Memory Disk Addresses
0/500 MHz 6.2 MiB/256 MiB 300 MiB http: 192.168.1.201:23646
Task Events:
Started At = 2019-06-14T06:49:36Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2019-06-14T06:49:36Z Started Task started by client
2019-06-14T06:49:35Z Task Setup Building Task Directory
2019-06-14T06:49:35Z Received Task received by clientCheck the Fabio Routes
http://192.168.1.101:9998/routes

Connect to the Services
The services “foo” and “bar” are available at:
http://192.168.1.101:9999/foo
http://192.168.1.101:9999/bar
Output:
gowebhello root page
https://github.com/udhos/gowebhello is a simple golang replacement for 'python -m SimpleHTTPServer'.
Welcome!
gowebhello version 0.7 runtime go1.12.5 os=linux arch=amd64
Keepalive: true
Application banner: Welcome to FOO
...
...Pressing F5 to refresh the browser should keep changing the backend service that you are eventually connected to.
Conclusion
This article should give you a fair idea about the common problems of a distributed application and how they can be solved.
Remodeling an existing application deployment as it scales can be quite a challenge. Hopefully the sample/demo setup will help you to explore, design and optimize the deployment workflows of your application, be it On-Premise or any Cloud Environment.