Skip to main content

Python Integration

Consul Guardian runs as a sidecar next to your Python application. Your app reads configuration from Consul KV using python-consul2 or any HTTP client. 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 Python application.

How Python apps typically read from Consul

PackageDescription
python-consul2Maintained fork of python-consul
requestsDirect HTTP calls to Consul API

Example: python-consul2

pip install python-consul2
import consul

c = consul.Consul(host="consul", port=8500)

def get_config(key, default=None):
_, data = c.kv.get(key)
if data is None:
return default
return data["Value"].decode("utf-8")

# Read config at startup
DB_HOST = get_config("config/myapp/db_host", "localhost")
DB_PORT = int(get_config("config/myapp/db_port", "5432"))
CACHE_TTL = int(get_config("config/myapp/cache_ttl", "300"))

Example: Flask app

from flask import Flask
import consul

app = Flask(__name__)
c = consul.Consul(host="consul", port=8500)

def load_config():
"""Load all config values from Consul KV."""
keys = {
"config/myapp/db_host": "DB_HOST",
"config/myapp/db_port": "DB_PORT",
"config/myapp/secret_key": "SECRET_KEY",
"config/myapp/debug": "DEBUG",
}
for consul_key, config_key in keys.items():
_, data = c.kv.get(consul_key)
if data is not None:
app.config[config_key] = data["Value"].decode("utf-8")

load_config()

@app.route("/health")
def health():
return {"status": "ok", "db_host": app.config.get("DB_HOST")}

Example: Django settings

# settings.py
import consul

c = consul.Consul(host="consul", port=8500)

def consul_get(key, default=""):
_, data = c.kv.get(key)
if data is None:
return default
return data["Value"].decode("utf-8")

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": consul_get("config/django/db_host", "localhost"),
"PORT": consul_get("config/django/db_port", "5432"),
"NAME": consul_get("config/django/db_name", "myapp"),
"USER": consul_get("config/django/db_user", "postgres"),
"PASSWORD": consul_get("config/django/db_password"),
}
}

CACHE_TTL = int(consul_get("config/django/cache_ttl", "300"))

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:
- "5000:5000"
environment:
- CONSUL_HOST=consul
- CONSUL_PORT=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:

Celery + Consul config

If your Celery workers read broker URLs and task configuration from Consul KV, Guardian protects those values too:

version: "3.8"

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

redis:
image: redis:7

web:
build: .
command: gunicorn app:app --bind 0.0.0.0:5000
environment:
- CONSUL_HOST=consul
depends_on:
- consul
- redis

worker:
build: .
command: celery -A tasks worker --loglevel=info
environment:
- CONSUL_HOST=consul
depends_on:
- consul
- redis

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

volumes:
guardian-data:

Guardian watches both config/ and celery/ prefixes. If someone changes celery/broker_url to a wrong Redis URL, you can restore it in seconds.

Restore scenario

Someone updates the database host to point at a decommissioned server:

consul kv put config/myapp/db_host "old-db.internal"

Your Flask app starts returning 500 errors. Django returns "OperationalError: could not connect to server".

1. Find what changed

consul-guardian log --key config/myapp/db_host

# 2024-03-15T16:00:00Z bad1234 config/myapp/db_host modified
# 2024-02-01T09:00:00Z good567 config/myapp/db_host modified

2. Restore

consul-guardian restore --key config/myapp/db_host --commit good567

3. Restart your app (Python apps typically read config at startup)

docker compose restart myapp
# or: kill -HUP <gunicorn_pid>

The app comes back with the correct database host.

Zero code changes

Guardian does not install a Python package, does not modify your requirements.txt, and does not hook into your WSGI/ASGI pipeline. It is a separate process that watches Consul KV independently. Your Python app and Guardian have no awareness of each other.