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
.envfiles 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.