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/applyoperations - Provider registry (
registry.opentofu.org, private registries) — unavailability preventstofu initand blocks pipeline starts - Cloud provider APIs — AWS, GCP, or Azure API outages cause
applyfailures that look like OpenTofu bugs - CI/CD pipeline runners — silently killed runners leave
tofu applymid-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
- Log in to vigilmon.online → Monitors → New Monitor
- 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 000msfor registry checks (cross-region DNS can be slow) - Alert channel: Slack
#infra-oncallfor P1,#infra-alertsfor 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.
- In Vigilmon → Monitors → New Monitor → Heartbeat
- Name:
opentofu-drift-detection - Expected interval: 24 hours
- Grace period: 2 hours
- 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.