tutorial

How to Monitor OpenTofu: State Backend Health, Registry Availability, and IaC Pipeline Uptime with Vigilmon

OpenTofu plan/apply failures silently break infrastructure delivery. Learn how to monitor state backend health, registry availability, and CI/CD pipeline success rates with Vigilmon external probes and heartbeat monitors.

OpenTofu is the open-source continuation of Terraform, used by platform teams to provision and manage cloud infrastructure declaratively. When OpenTofu breaks — a corrupted state file, an unreachable registry, or a flapping CI pipeline — your infrastructure delivery stops. Unlike HTTP services, IaC failures don't return 500 errors to users; they silently prevent deployments until an engineer notices a stuck pipeline.

Vigilmon gives you external visibility into the services OpenTofu depends on: state backends, provider registries, CI runners, and your own infrastructure management APIs. This tutorial shows how to build a complete monitoring layer for your IaC platform.


What Can Go Wrong with OpenTofu

OpenTofu depends on several external services that can fail independently:

  • State backend (S3, GCS, Azure Blob, Terraform Cloud, GitLab-managed state) — lock conflicts or connectivity failures block all plan/apply operations
  • Provider registry (registry.opentofu.org, private registries) — unavailability prevents tofu init and blocks pipeline starts
  • Cloud provider APIs — AWS, GCP, or Azure API outages cause apply failures that look like OpenTofu bugs
  • CI/CD pipeline runners — silently killed runners leave tofu apply mid-execution with locked state

Step 1: Monitor State Backend Health

Your state backend is the most critical OpenTofu dependency. A locked or unreachable state file blocks all operations across all pipelines for that workspace.

S3 State Backend Health Endpoint

// health/opentofu-state.js
const express = require('express');
const { S3Client, HeadObjectCommand, ListObjectsV2Command } = require('@aws-sdk/client-s3');

const app = express();
const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
const STATE_BUCKET = process.env.TF_STATE_BUCKET;
const STATE_KEY_PREFIX = process.env.TF_STATE_KEY_PREFIX || 'terraform/';

app.get('/health/opentofu/state', async (req, res) => {
  try {
    // Check bucket accessibility
    const listRes = await s3.send(new ListObjectsV2Command({
      Bucket: STATE_BUCKET,
      Prefix: STATE_KEY_PREFIX,
      MaxKeys: 1,
    }));

    // Check for stale locks (lock files older than 30 minutes indicate a stuck pipeline)
    const lockObjects = listRes.Contents?.filter(obj => obj.Key.endsWith('.lock')) || [];
    const staleLocks = lockObjects.filter(lock => {
      const ageMinutes = (Date.now() - lock.LastModified.getTime()) / 60_000;
      return ageMinutes > 30;
    });

    if (staleLocks.length > 0) {
      return res.status(503).json({
        status: 'degraded',
        reason: 'stale_state_locks',
        locks: staleLocks.map(l => ({ key: l.Key, age_minutes: Math.round((Date.now() - l.LastModified.getTime()) / 60_000) })),
      });
    }

    return res.status(200).json({
      status: 'ok',
      bucket: STATE_BUCKET,
      objects: listRes.KeyCount,
    });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

app.listen(3009, () => console.log('OpenTofu health sidecar running on :3009'));

GCS State Backend Health Endpoint

const { Storage } = require('@google-cloud/storage');
const storage = new Storage();

app.get('/health/opentofu/state/gcs', async (req, res) => {
  try {
    const bucket = storage.bucket(process.env.TF_STATE_BUCKET);
    const [files] = await bucket.getFiles({ prefix: process.env.TF_STATE_PREFIX, maxResults: 5 });

    return res.status(200).json({ status: 'ok', accessible: true, files: files.length });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

Step 2: Monitor Provider Registry Availability

OpenTofu resolves providers from registry.opentofu.org (or your private registry) during tofu init. Registry downtime blocks all pipeline starts.

Registry Health Probe

The OpenTofu registry exposes a service discovery endpoint:

# Public OpenTofu registry health
curl -i https://registry.opentofu.org/.well-known/terraform.json

# Private registry (e.g. Terraform Enterprise / Spacelift)
curl -i https://your-private-registry.example.com/.well-known/terraform.json

Configure Vigilmon to probe this URL directly — no sidecar needed. If your pipelines run in a private network, deploy the sidecar proxy:

app.get('/health/opentofu/registry', async (req, res) => {
  const REGISTRY_URL = process.env.TOFU_REGISTRY_URL || 'https://registry.opentofu.org';
  try {
    const response = await fetch(`${REGISTRY_URL}/.well-known/terraform.json`, {
      signal: AbortSignal.timeout(10_000),
    });
    if (!response.ok) {
      return res.status(503).json({ status: 'down', http_status: response.status });
    }
    const data = await response.json();
    return res.status(200).json({ status: 'ok', modules_v1: data['modules.v1'] });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

Step 3: Monitor Plan/Apply Pipeline Success Rates

Pipeline failures are where IaC uptime gets tricky. A failed tofu apply doesn't send an HTTP error — it exits with a non-zero code in your CI system. You need to export pipeline health to an HTTP endpoint that Vigilmon can probe.

CI Pipeline Health Exporter (GitLab CI Example)

Add a pipeline status endpoint to your internal tooling API:

# pipeline_health.py — expose recent pipeline success/failure rates
from flask import Flask, jsonify
import gitlab, os, time

app = Flask(__name__)
gl = gitlab.Gitlab(os.environ['GITLAB_URL'], private_token=os.environ['GITLAB_TOKEN'])

PIPELINE_PROJECT_ID = int(os.environ['GITLAB_PROJECT_ID'])
FAILURE_THRESHOLD = float(os.environ.get('PIPELINE_FAILURE_THRESHOLD', '0.3'))  # 30% failure rate

@app.route('/health/opentofu/pipelines')
def pipeline_health():
    project = gl.projects.get(PIPELINE_PROJECT_ID)
    # Last 20 pipelines for the IaC repo
    pipelines = project.pipelines.list(per_page=20, order_by='id', sort='desc')

    total = len(pipelines)
    if total == 0:
        return jsonify(status='unknown', reason='no_recent_pipelines'), 200

    failed = sum(1 for p in pipelines if p.status == 'failed')
    failure_rate = failed / total

    if failure_rate > FAILURE_THRESHOLD:
        return jsonify(
            status='degraded',
            reason='high_pipeline_failure_rate',
            failure_rate=round(failure_rate, 2),
            failed=failed,
            total=total,
        ), 503

    return jsonify(status='ok', failure_rate=round(failure_rate, 2), total=total), 200

if __name__ == '__main__':
    app.run(port=3010)

GitHub Actions Health Exporter

@app.route('/health/opentofu/actions')
def actions_health():
    import requests as req
    headers = {'Authorization': f"Bearer {os.environ['GITHUB_TOKEN']}"}
    url = f"https://api.github.com/repos/{os.environ['GITHUB_REPO']}/actions/workflows/{os.environ['WORKFLOW_ID']}/runs"
    r = req.get(url, headers=headers, params={'per_page': 20}, timeout=10)
    if r.status_code != 200:
        return jsonify(status='down', reason='github_api_error'), 503

    runs = r.json().get('workflow_runs', [])
    total = len(runs)
    if total == 0:
        return jsonify(status='unknown'), 200

    failed = sum(1 for run in runs if run['conclusion'] == 'failure')
    failure_rate = failed / total

    if failure_rate > 0.3:
        return jsonify(status='degraded', failure_rate=round(failure_rate, 2), failed=failed, total=total), 503

    return jsonify(status='ok', failure_rate=round(failure_rate, 2), total=total), 200

Step 4: Configure Vigilmon Monitors for OpenTofu

  1. Log in to vigilmon.onlineMonitors → New Monitor
  2. Choose HTTP / HTTPS

Set up the following monitors:

| Monitor | URL | Interval | Priority | |---|---|---|---| | State backend (S3/GCS) | /health/opentofu/state | 2 minutes | P1 | | Provider registry | /health/opentofu/registry | 5 minutes | P2 | | Pipeline success rate | /health/opentofu/pipelines | 5 minutes | P2 |

For each monitor:

  • Expected status: 200
  • Response body contains: "status":"ok"
  • Response time threshold: 10 000ms for registry checks (cross-region DNS can be slow)
  • Alert channel: Slack #infra-oncall for P1, #infra-alerts for P2

Step 5: Heartbeat Monitor for Scheduled Drift Detection

If you run OpenTofu on a schedule (e.g., nightly drift detection with tofu plan to confirm infrastructure matches state), configure a Vigilmon heartbeat to alert when the job stops running.

  1. In Vigilmon → Monitors → New Monitor → Heartbeat
  2. Name: opentofu-drift-detection
  3. Expected interval: 24 hours
  4. Grace period: 2 hours
  5. Copy the heartbeat URL

Add the Vigilmon ping at the end of your drift detection script:

#!/bin/bash
# drift-check.sh — run nightly via cron or CI schedule
set -euo pipefail

cd /infra/terraform
tofu init -input=false
tofu plan -input=false -detailed-exitcode

EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
  echo "No drift detected"
elif [ $EXIT_CODE -eq 2 ]; then
  echo "Drift detected — sending alert"
  curl -X POST "$SLACK_WEBHOOK_URL" -d '{"text":"OpenTofu drift detected"}'
fi

# Signal Vigilmon that the drift check ran — regardless of drift, the JOB must run
curl -s "$VIGILMON_HEARTBEAT_URL" > /dev/null

Summary

OpenTofu reliability spans multiple layers — the state backend, the provider registry, your CI pipelines, and scheduled drift detection:

| Monitor Type | What It Covers | |---|---| | HTTP: state backend | S3/GCS accessibility, stale lock detection | | HTTP: registry | Provider download availability | | HTTP: pipeline health | Plan/apply success rate trends | | Heartbeat: drift detection | Scheduled job liveness |

Get started free at vigilmon.online — your IaC uptime monitors are live in under two minutes.

Monitor your app with Vigilmon

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

Start free →