Go Integration
Consul Guardian runs as a sidecar next to your Go application. Your app reads configuration from Consul KV using the official client or Viper. Guardian independently watches those same keys, commits every change to Git, and lets you restore any key to a previous value.
No code changes are required in your Go application.
How Go apps typically read from Consul
| Package | Description |
|---|---|
| hashicorp/consul/api | Official Consul client library |
| spf13/viper | Config library with Consul KV backend |
Example: Official Consul client
package main
import (
"fmt"
"log"
"net/http"
consul "github.com/hashicorp/consul/api"
)
func main() {
client, err := consul.NewClient(&consul.Config{
Address: "http://consul:8500",
})
if err != nil {
log.Fatal(err)
}
kv := client.KV()
http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
pair, _, err := kv.Get("config/myapp/db_host", nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if pair == nil {
http.Error(w, "key not found", 404)
return
}
fmt.Fprintf(w, "db_host = %s", string(pair.Value))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Example: Viper with Consul backend
package main
import (
"log"
"github.com/spf13/viper"
_ "github.com/spf13/viper/remote"
)
func main() {
viper.AddRemoteProvider("consul", "http://consul:8500", "config/myapp")
viper.SetConfigType("json")
if err := viper.ReadRemoteConfig(); err != nil {
log.Fatalf("failed to read config from Consul: %v", err)
}
dbHost := viper.GetString("db_host")
dbPort := viper.GetInt("db_port")
maxRetries := viper.GetInt("max_retries")
log.Printf("Config loaded: db=%s:%d retries=%d", dbHost, dbPort, maxRetries)
}
Both approaches work. Your Go app reads from Consul KV at startup or on demand. The problem: when someone pushes a bad config value, there is no built-in way to see what it was before.
Adding Guardian
Docker Compose
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_HTTP_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/,env/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul
volumes:
guardian-data:
Your Go app is untouched. Guardian runs as a separate container.
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 8080
env:
- name: CONSUL_HTTP_ADDR
value: "http://consul.consul.svc.cluster.local:8500"
---
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/,env/"
- name: CONSUL_GUARDIAN_GIT_REPO_PATH
value: "/data/repo"
volumeMounts:
- name: guardian-data
mountPath: /data
volumes:
- name: guardian-data
persistentVolumeClaim:
claimName: guardian-pvc
Drift detection in CI
Guardian's drift detection is useful in Go CI pipelines. You can verify that the live Consul state matches what's committed in Git before deploying:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
check-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Guardian
run: |
curl -sL https://github.com/consul-guardian/consul-guardian/releases/latest/download/consul-guardian-linux-amd64 -o consul-guardian
chmod +x consul-guardian
- name: Check for config drift
run: |
./consul-guardian drift \
--consul-addr ${{ secrets.CONSUL_ADDR }} \
--consul-token ${{ secrets.CONSUL_TOKEN }} \
--prefix config/myapp/
# Exits non-zero if drift is detected
- name: Deploy
if: success()
run: make deploy
If someone manually changed a Consul key outside the normal deployment process, the drift check catches it before your new version goes out. This prevents deploying on top of unknown state.
Restore scenario
A teammate updates config/myapp/db_host to point at a staging database by mistake:
consul kv put config/myapp/db_host "staging-db.internal"
Your Go app starts returning errors because it can't reach the staging DB from production.
Find and restore:
# See what changed
consul-guardian log --key config/myapp/db_host
# Restore the previous value
consul-guardian restore --key config/myapp/db_host --commit abc1234
The correct value is written back to Consul. Your app picks it up on the next read. No restart needed if your app reads config per-request. If your app caches config at startup, a restart or SIGHUP will pick up the restored value.
Zero code changes
Guardian does not modify your Go binary, does not proxy Consul calls, and does not inject middleware. It watches Consul independently. Your app and Guardian are two separate processes that both talk to Consul.