tutorial

How to Monitor Juju Application Deployments with Vigilmon

Juju controller failures, unit hook errors, and broken application relations stall your entire deployment pipeline without firing a single alert. Learn how to monitor Juju controller health, unit agent status, application relations, and cloud substrate connectivity with Vigilmon.

Juju makes deploying complex multi-application stacks declarative and repeatable — but when a charm hook fails, the affected unit silently enters error status and stops responding to config changes. When the Juju controller's DQLite database goes offline, no model operation in the entire deployment succeeds. When a cloud substrate becomes unreachable, new machine provisioning blocks silently and your application scaling grinds to a halt.

Vigilmon gives you external uptime visibility into Juju controller health, unit agent status, application deployment health, and substrate connectivity through HTTP health endpoints and heartbeat monitors. This tutorial walks you through the full setup.


Why Juju Needs External Monitoring

Juju provides juju status for detailed model health, juju debug-log for hook output, and controller dashboards. But polling juju status manually and waiting for someone to notice a unit in error state is not monitoring — it's hoping. External monitoring with Vigilmon adds:

  • Immediate alerting when the Juju controller API becomes unreachable (blocking every model operation across all models)
  • Unit error detection — a charm hook failure puts a unit in error status; Vigilmon catches this and pages you without waiting for a deployment to fail
  • Application blocked status — applications in blocked status have unmet config requirements; catching this early prevents cascading relation failures
  • Substrate connectivity loss — if Juju can't reach the cloud provider API, provisioning fails silently for minutes before the error surfaces in logs
  • Heartbeat verification that Juju cron-style jobs (e.g., relation hook chains, backup charms) complete on schedule

What you'll need

  • A running Juju controller (version 3.x or later with DQLite, or 2.9.x with MongoDB)
  • Juju client configured with credentials to your controller
  • A lightweight health sidecar service (Python or Node.js) with access to run juju CLI commands
  • A free Vigilmon account

Step 1: Build a Juju health endpoint

Juju doesn't expose native HTTP health endpoints, but you can build a sidecar that runs juju CLI commands and exposes their results over HTTP.

# juju_health.py
import subprocess
import json
from flask import Flask, jsonify

app = Flask(__name__)

def run_juju(args):
    result = subprocess.run(
        ['juju'] + args + ['--format=json'],
        capture_output=True, text=True, timeout=30
    )
    if result.returncode != 0:
        return None, result.stderr.strip()
    try:
        return json.loads(result.stdout), None
    except json.JSONDecodeError as e:
        return None, str(e)


@app.route('/health/juju/controller')
def controller_health():
    data, err = run_juju(['show-controller'])
    if err:
        return jsonify(status='down', error=err), 503
    controllers = list(data.values()) if data else []
    if not controllers:
        return jsonify(status='down', error='no controllers found'), 503
    ctrl = controllers[0]
    api_endpoints = ctrl.get('api-endpoints', [])
    return jsonify(
        status='ok',
        controller=ctrl.get('details', {}).get('controller-uuid', 'unknown'),
        api_endpoints=api_endpoints,
        agent_version=ctrl.get('details', {}).get('agent-version', 'unknown'),
    )


@app.route('/health/juju/units')
def unit_health():
    # Check status of all units across the default model
    data, err = run_juju(['status'])
    if err:
        return jsonify(status='down', error=err), 503

    applications = data.get('applications', {})
    error_units = []
    blocked_apps = []

    for app_name, app_data in applications.items():
        app_status = app_data.get('application-status', {}).get('current', '')
        if app_status in ('blocked', 'error'):
            blocked_apps.append({
                'application': app_name,
                'status': app_status,
                'message': app_data.get('application-status', {}).get('message', ''),
            })

        for unit_name, unit_data in app_data.get('units', {}).items():
            agent_status = unit_data.get('juju-status', {}).get('current', '')
            workload_status = unit_data.get('workload-status', {}).get('current', '')
            if agent_status == 'error' or workload_status in ('error', 'blocked'):
                error_units.append({
                    'unit': unit_name,
                    'agent': agent_status,
                    'workload': workload_status,
                    'message': unit_data.get('workload-status', {}).get('message', ''),
                })

    if error_units or blocked_apps:
        return jsonify(
            status='degraded',
            error_units=error_units,
            blocked_apps=blocked_apps,
        ), 503

    total_units = sum(
        len(app_data.get('units', {}))
        for app_data in applications.values()
    )
    return jsonify(
        status='ok',
        applications=len(applications),
        total_units=total_units,
    )


@app.route('/health/juju/model')
def model_health():
    data, err = run_juju(['models'])
    if err:
        return jsonify(status='down', error=err), 503

    models = data.get('models', [])
    error_models = [
        m for m in models
        if m.get('status', {}).get('current', '') not in ('available', '')
    ]

    if error_models:
        return jsonify(
            status='degraded',
            error_models=[
                {'name': m.get('short-name'), 'status': m.get('status', {}).get('current')}
                for m in error_models
            ],
        ), 503

    return jsonify(status='ok', model_count=len(models))


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9081)

Install and start:

pip install flask
# Ensure the juju CLI is in PATH and credentials are loaded for the service user
python juju_health.py &

As a systemd unit:

# /etc/systemd/system/juju-health.service
[Unit]
Description=Juju Health Endpoint

[Service]
ExecStart=/usr/bin/python3 /opt/juju-health/juju_health.py
Restart=always
User=ubuntu
Environment=HOME=/home/ubuntu

[Install]
WantedBy=multi-user.target
systemctl enable --now juju-health

Verify:

curl http://juju-mgmt.example.com:9081/health/juju/controller
# {"status": "ok", "agent_version": "3.5.2", ...}

curl http://juju-mgmt.example.com:9081/health/juju/units
# {"status": "ok", "applications": 8, "total_units": 24}

Step 2: Monitor the Juju controller API endpoint

Juju controllers expose an API on port 17070 (WebSocket-based). While Vigilmon probes HTTP, you can wrap the controller check with a TCP probe to verify port reachability, and use the health sidecar for semantic health.

HTTP health check (via sidecar)

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. URL: http://juju-mgmt.example.com:9081/health/juju/controller
  4. Expected status: 200
  5. Check interval: 1 minute
  6. Save as "Juju Controller Health"

TCP probe for controller API port

  1. Monitors → New Monitor → TCP Port
  2. Host: juju-controller.example.com
  3. Port: 17070
  4. Save as "Juju Controller API Port"

Step 3: Monitor unit agent and application status

Unit errors and blocked applications are the most common Juju failure mode. Add the unit health endpoint:

  1. Monitors → New Monitor → HTTP / HTTPS
  2. URL: http://juju-mgmt.example.com:9081/health/juju/units
  3. Expected status: 200
  4. Check interval: 2 minutes
  5. Save as "Juju Unit and Application Health"

This monitor fires whenever any unit enters error status or any application enters blocked or error status. The response body tells you exactly which unit and why.

Model health

  1. Monitors → New Monitor → HTTP / HTTPS
  2. URL: http://juju-mgmt.example.com:9081/health/juju/model
  3. Expected status: 200
  4. Check interval: 5 minutes
  5. Save as "Juju Model Health"

Step 4: Add a heartbeat monitor for Juju cron jobs

Juju charms often run periodic actions (database backups, relation refresh hooks, certificate renewal). Use Vigilmon's heartbeat monitor to verify these complete on schedule.

Example backup charm action that pings a heartbeat:

#!/bin/bash
# /opt/juju-backup-check.sh
# Run after your Juju backup action completes

HEARTBEAT_URL="https://vigilmon.online/heartbeat/YOUR_HEARTBEAT_TOKEN"

# Trigger backup action and wait for it
juju run postgresql/0 create-backup --wait=10m

if [ $? -eq 0 ]; then
    curl -s -X POST "$HEARTBEAT_URL" > /dev/null
    echo "Backup completed, heartbeat sent"
else
    echo "Backup failed — heartbeat NOT sent, Vigilmon will alert"
fi

In Vigilmon:

  1. Monitors → New Monitor → Heartbeat
  2. Interval: 25 hours (alert if daily backup misses by more than an hour)
  3. Copy the heartbeat URL and add it to your backup script

Step 5: Monitor cloud substrate connectivity

If Juju can't reach your cloud provider API (OpenStack Keystone, AWS EC2 API, LXD API, MAAS API), new unit provisioning fails silently. Add a TCP or HTTP monitor for your substrate's API endpoint.

OpenStack / MAAS substrate

# Add to your health sidecar
@app.route('/health/juju/substrate')
def substrate_health():
    # Attempt to list machines — fails immediately if cloud substrate is unreachable
    data, err = run_juju(['machines'])
    if err and 'cannot connect' in err.lower():
        return jsonify(status='down', error='substrate_unreachable', detail=err), 503
    if err:
        return jsonify(status='degraded', error=err), 503

    machines = data.get('machines', {})
    provisioning_errors = {
        m_id: m for m_id, m in machines.items()
        if m.get('juju-status', {}).get('current') == 'provisioning error'
    }

    if provisioning_errors:
        return jsonify(
            status='degraded',
            reason='provisioning_errors',
            machines=list(provisioning_errors.keys()),
        ), 503

    return jsonify(status='ok', machine_count=len(machines))

Add the monitor in Vigilmon:

  1. Monitors → New Monitor → HTTP / HTTPS
  2. URL: http://juju-mgmt.example.com:9081/health/juju/substrate
  3. Expected status: 200
  4. Save as "Juju Cloud Substrate Connectivity"

Step 6: Configure alert channels

Email alerts

  1. Alert Channels → Add Channel → Email
  2. Enter your platform engineering team's on-call email
  3. Assign to all Juju monitors

Slack/PagerDuty webhook

Vigilmon sends this payload when a monitor goes down:

{
  "monitor_name": "Juju Unit and Application Health",
  "status": "down",
  "url": "http://juju-mgmt.example.com:9081/health/juju/units",
  "started_at": "2026-03-10T14:45:00Z",
  "duration_seconds": 120
}

Route this to Slack or PagerDuty for immediate operator response. When a unit enters error status, the fix is usually juju resolve <unit> after investigating the hook failure with juju debug-log.

Recommended alert configuration

| Monitor | Urgency | First responder action | |---------|---------|------------------------| | Juju Controller Health | Critical | Check controller machine health; restart juju-controller service | | Controller API Port (17070) | Critical | Network/firewall investigation | | Unit and Application Health | High | juju status; juju resolve <unit> if hook error | | Model Health | Medium | juju models; investigate failing model | | Substrate Connectivity | High | Check cloud provider API; check Juju credential validity | | Heartbeat (backup/cron) | Medium | Check Juju action logs |


Full monitor summary

| Monitor | Type | Endpoint | What it catches | |---------|------|----------|-----------------| | Controller Health | HTTP | :9081/health/juju/controller | Controller API unavailable | | Controller API Port | TCP | controller:17070 | Port-level unreachability | | Unit & App Health | HTTP | :9081/health/juju/units | Unit error / app blocked | | Model Health | HTTP | :9081/health/juju/model | Model-level errors | | Substrate Connectivity | HTTP | :9081/health/juju/substrate | Cloud API unreachable | | Backup Heartbeat | Heartbeat | heartbeat URL | Backup action not running |


What's next

  • SSL certificate monitoring — if your Juju controller runs on HTTPS (port 17070 with TLS), Vigilmon will alert you before the cert expires, preventing client authentication failures
  • Per-model monitors — deploy separate health sidecars in each Juju model for isolation between development, staging, and production deployments
  • Charm upgrade verification — use Vigilmon's heartbeat monitor to confirm that charm upgrade hooks complete within expected time windows

Get started free at vigilmon.online — no credit card, your first monitors are live in under a minute.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →