Although I have heard a lot about Hashicorp’s Consul, I have not had the opportunity to work with it and get acquainted with the basics. In this post, I will share some of the basics I have learned, hopefully giving you a bit of a head start when you embark on this journey yourself.
Want to watch a video about this instead?
What is Consul?
Basically, Consul is a networking tool. It provides service discovery and allows you to store and retrieve configuration values. On top of that, it provides service-mesh capability by controlling and encrypting service-to-service traffic. Although that looks simple enough, in complex and dynamic infrastructure spanning multiple locations such as on-premises and cloud, this can become extremely complicated. Let’s stick to the basics and focus on three things:
- Installation on Kubernetes
- Using the key-value store for configuration
- Using the service catalog to retrieve service information
We will use a small Go program to illustrate the use of the Consul API. Let’s get started… ššš
Installation of Consul
I will install Consul using the provided Helm chart. Note that the installation I will perform is great for testing but should not be used for production. In production, there are many more things to think about. Look at the configuration values for hints: certificates, storage size and class, options to enable/disable, etc… That being said, the chart does install multiple servers and clients to provide high availability.
I installed Consul with Pulumi and Python. You can check the code here. You can use that code on Azure to deploy both Kubernetes and Consul in one step. The section in the code that installs Consul is shown below:
consul = v3.Chart("consul",
config=v3.LocalChartOpts(
path="consul-chart",
namespace="consul",
values={
"connectInject": {
"enabled": "true"
},
"client": {
"enabled": "true",
"grpc": "enabled"
},
"syncCatalog": {
"enabled": "true"
}
}
),
opts=pulumi.ResourceOptions(
depends_on=[ns_consul],
provider=k8s
)
)
The code above would be equivalent to this Helm chart installation (Helm v3):
helm install consul -f consul-helm/values.yaml \ --namespace consul ./consul-helm \ --set connectInject.enabled=true \ --set client.enabled=true --set client.grpc=true \ --set syncCatalog.enabled=true
Connecting to the Consul UI
The chart installs Consul in the consul namespace. You can run the following command to get to the UI:
kubectl port-forward services/consul-consul-ui 8888:80 -n consul8:80 -n consul
You will see the screen below. The list of services depends on the Kubernetes services in your system.

The services above include consul itself. The consul service also has health checks configured. The other services in the screenshot are Kubernetes services that were discovered by Consul. I have installed Redis in the default namespace and exposed Redis via a service called redisapp. This results in a Consul service called redisadd-default. Later, we will query this service from our Go application.
When you click Key/Value, you can see the configured keys. I have created one key called REDISPATTERN which is later used in the Go program to know the Redis channels to subscribe to. It’s just a configuration value that is retrieved at runtime.

The Key/Value pair can be created via the consul CLI, the HTTP API or via the UI (Create button in the main Key/Value screen). I created the REDISPATTERN key via the Create button.
Querying the Key/Value store
Let’s turn our attention to writing some code that retrieves a Consul key at runtime. The question of course is: “how does your application find Consul?”. Look at the diagram below:

Above, you see the Consul server agents, implemented as a Kubernetes StatefulSet. Each server pod has a volume (Azure disk in this case) to store data such as key/value pairs.
Your application will not connect to these servers directly. Instead, it will connect via the client agents. The client agents are implemented as a DaemonSet resulting in a client agent per Kubernetes node. The client agent pods expose a static port on the Kubernetes host (yes, you read that right). This means that your app can connect to the IP address of the host it is running on. Your app can discover that IP address via the Downward API.
The container spec contains the following code:
containers:
- name: realtimeapp
image: gbaeke/realtime-go-consul:1.0.0
env:
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
- name: CONSUL_HTTP_ADDR
value: http://$(HOST_IP):8500
The HOST_IP
will be set to the IP of the Kubernetes host via a reference to status.hostIP. Next, the environment variable CONSUL_HTTP_ADDR
is set to the full HTTP address including port 8500. In your code, you will need to read that environment variable.
Retrieving a key/value pair
Here is some code to read a Consul key/value pair with Go. Full source code is here.
// return a Consul client based on given address
func getConsul(address string) (*consulapi.Client, error) {
config := consulapi.DefaultConfig()
config.Address = address
consul, err := consulapi.NewClient(config)
return consul, err
}
// get key/value pair from Consul client and passed key name
func getKvPair(client *consulapi.Client, key string) (*consulapi.KVPair, error) {
kv := client.KV()
keyPair, _, err := kv.Get(key, nil)
return keyPair, err
}
func main() {
// retrieve address of Consul set via downward API in spec
consulAddress := getEnv("CONSUL_HTTP_ADDR", "")
if consulAddress == "" {
log.Fatalf("CONSUL_HTTP_ADDRESS environment variable not set")
}
// get Consul client
consul, err := getConsul(consulAddress)
if err != nil {
log.Fatalf("Error connecting to Consul: %s", err)
}
// get key/value pair with Consul client
redisPattern, err := getKvPair(consul, "REDISPATTERN")
if err != nil || redisPattern == nil {
log.Fatalf("Could not get REDISPATTERN: %s", err)
}
log.Printf("KV: %v %s\n", redisPattern.Key, redisPattern.Value)
... func main() continued...
The comments in the code should be self-explanatory. When the REDISPATTERN key is not set or another error occurs, the program will exit. If REDISPATTERN is set, we can use the value later:
pubsub := client.PSubscribe(string(redisPattern.Value))
Looking up a service
That’s great but how do you look up an address of a service? That’s easy with the following basic code via the catalog:
cat := consul.Catalog()
svc, _, err := cat.Service("redisapp-default", "", nil)
log.Printf("Service address and port: %s:%d\n", svc[0].ServiceAddress,
svc[0].ServicePort)
consul is a *consulapi.client obtained earlier. You use the Catalog() function to obtain access to catalog service functionality. In this case, we simply retrieve the address and port value of the Kubernetes service redisapp in the default namespace. We can use that information to connect to our Redis back-end.
Conclusion
It’s easy to get started with Consul on Kubernetes and to write some code to take advantage of it. Be aware though that we only scratched the surface here and that this is both a sample deployment (without TLS, RBAC, etc…) and some sample code. In addition, you should only use Consul in more complex application landscapes with many services to discover, traffic to secure and more. If you do think you need it, you should also take a look at managed Consul on Azure. It runs in your subscription but fully managed by Hashicorp! It can be integrated with Azure Kubernetes Service as well.
In a later post, I will take a look at the service mesh capabilities with Connect.
One thought on “Getting started with Consul on Kubernetes”