Skip to main content

Any Language / HTTP API

Consul exposes its KV store over a simple HTTP API. Any language that can make HTTP requests can read and write configuration. Guardian watches those same keys independently -- it does not care what language your application uses.

Consul KV HTTP API

Read a key

GET /v1/kv/:key
curl http://consul:8500/v1/kv/config/myapp/db_host?raw
# Returns: db.prod.internal

Without ?raw, the response is JSON with the value Base64-encoded:

curl -s http://consul:8500/v1/kv/config/myapp/db_host | jq .
[
{
"LockIndex": 0,
"Key": "config/myapp/db_host",
"Flags": 0,
"Value": "ZGIucHJvZC5pbnRlcm5hbA==",
"CreateIndex": 42,
"ModifyIndex": 87
}
]

Write a key

curl -X PUT -d "db.prod.internal" http://consul:8500/v1/kv/config/myapp/db_host

List keys under a prefix

curl http://consul:8500/v1/kv/config/myapp/?keys
# ["config/myapp/db_host", "config/myapp/db_port", "config/myapp/cache_ttl"]

Read all keys under a prefix

curl -s http://consul:8500/v1/kv/config/myapp/?recurse | jq .

Language examples

Ruby

require "net/http"
require "json"

def consul_get(key)
uri = URI("http://consul:8500/v1/kv/#{key}?raw")
Net::HTTP.get(uri)
end

db_host = consul_get("config/myapp/db_host")
db_port = consul_get("config/myapp/db_port").to_i

PHP

<?php
function consul_get(string $key): string {
$url = "http://consul:8500/v1/kv/{$key}?raw";
return file_get_contents($url);
}

$dbHost = consul_get("config/myapp/db_host");
$dbPort = (int) consul_get("config/myapp/db_port");

Rust

use reqwest;

async fn consul_get(key: &str) -> Result<String, reqwest::Error> {
let url = format!("http://consul:8500/v1/kv/{}?raw", key);
reqwest::get(&url).await?.text().await
}

Shell script

#!/bin/bash
DB_HOST=$(curl -s http://consul:8500/v1/kv/config/myapp/db_host?raw)
DB_PORT=$(curl -s http://consul:8500/v1/kv/config/myapp/db_port?raw)

echo "Connecting to ${DB_HOST}:${DB_PORT}"

Adding Guardian

Docker Compose template

This template works with any language. Replace myapp with your service:

version: "3.8"

services:
consul:
image: hashicorp/consul:1.17
ports:
- "8500:8500"
command: agent -dev -client=0.0.0.0

myapp:
build: .
ports:
- "8080:8080"
environment:
- CONSUL_ADDR=http://consul:8500
depends_on:
- consul

guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul

volumes:
guardian-data:

Kubernetes template

apiVersion: apps/v1
kind: Deployment
metadata:
name: consul-guardian
spec:
replicas: 1
selector:
matchLabels:
app: consul-guardian
template:
metadata:
labels:
app: consul-guardian
spec:
containers:
- name: guardian
image: ghcr.io/consul-guardian/consul-guardian:latest
env:
- name: CONSUL_GUARDIAN_CONSUL_ADDRESS
value: "http://consul.consul.svc.cluster.local:8500"
- name: CONSUL_GUARDIAN_WATCH_PREFIXES
value: "config/"
- name: CONSUL_GUARDIAN_GIT_REPO_PATH
value: "/data/repo"
volumeMounts:
- name: guardian-data
mountPath: /data
volumes:
- name: guardian-data
persistentVolumeClaim:
claimName: guardian-pvc

Deploy this alongside your existing application. No changes to your app's Deployment needed.

Environment variable approach with consul-template

If your app reads configuration from environment variables (not Consul directly), you can use consul-template to render a .env file from Consul KV. Guardian still protects the source of truth in Consul:

# consul-template.hcl
template {
contents = <<EOF
DB_HOST={{ key "config/myapp/db_host" }}
DB_PORT={{ key "config/myapp/db_port" }}
CACHE_TTL={{ key "config/myapp/cache_ttl" }}
EOF
destination = "/app/.env"
command = "kill -HUP $(cat /app/app.pid)"
}
version: "3.8"

services:
consul:
image: hashicorp/consul:1.17
command: agent -dev -client=0.0.0.0

myapp:
build: .
volumes:
- app-env:/app

consul-template:
image: hashicorp/consul-template:latest
command: -config=/etc/consul-template.hcl
volumes:
- ./consul-template.hcl:/etc/consul-template.hcl
- app-env:/app
depends_on:
- consul

guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul

volumes:
guardian-data:
app-env:

In this setup:

  • consul-template reads from Consul and writes environment files for your app
  • Guardian watches the same Consul keys and maintains Git history
  • Your app reads .env files and knows nothing about Consul

Restore scenario

Regardless of language, the restore flow is the same:

# See change history
consul-guardian log --prefix config/myapp/

# Restore a single key
consul-guardian restore --key config/myapp/db_host --commit abc1234

# Restore an entire prefix
consul-guardian restore --prefix config/myapp/ --commit abc1234

Guardian writes the values back to Consul using CAS. Your app picks up the changes through whatever mechanism it already uses (polling, watch, consul-template, restart).

Zero code changes

Guardian works with any language because it never touches your application. It is a standalone process that watches Consul's KV store over HTTP and commits changes to Git. Whether your app is written in Ruby, PHP, Rust, Elixir, or bash scripts -- Guardian works the same way.