# Terraform, OpenTofu and Pulumi

Manage monitors and contacts as code. The JustMonitorIt provider works with
Terraform (1.0 or later) and OpenTofu, and its Pulumi version uses the same
code. It talks to the [REST API](rest.md) with an API token, so the same
roles, validation, plan limits and audit log apply.

## Set up

1. **Create a token** with the `member` role (or `admin`) under
   **Settings → API tokens**. See [Authentication](authentication.md).
2. **Require the provider.** It is installed from JustMonitorIt's own
   registry, so the source starts with `dash.justmonitorit.com`:

   ```hcl
   terraform {
     required_providers {
       justmonitorit = {
         source  = "dash.justmonitorit.com/justmonitorit/justmonitorit"
         version = "~> 0.1"
       }
     }
   }

   provider "justmonitorit" {}
   ```

3. **Pass the token** in the `JUSTMONITORIT_API_TOKEN` environment variable
   (or `api_token` in the provider block, from a sensitive variable), then
   run `terraform init`. The provider works on the organisation of its token.

`terraform init` checks each download against a checksum list signed with
the registry key (ID `160A38870F4B46E0`, fingerprint
`356C A0E2 8CBD 74A8 2B7C AE77 160A 3887 0F4B 46E0`; the public key is at
[`/_registry/signing-key.asc`](https://dash.justmonitorit.com/_registry/signing-key.asc))
and records the checksums in `.terraform.lock.hcl`.

## Monitors

Set exactly one kind: `http`, `http_adv`, `tcp`, `icmp`, `ssl_expiry`,
`domain_expiry`, `imap`, `smtp` or `heartbeat`. The fields of each are
those of the [monitor kinds](monitor-kinds.md), with the same defaults.

```hcl
resource "justmonitorit_contact" "ops" {
  name  = "Ops team"
  email = { address = "ops@example.com" }
}

resource "justmonitorit_monitor" "homepage" {
  name        = "Homepage"
  interval_s  = 60
  locations   = { regions = ["eu"] }
  http        = { url = "https://example.com/", keyword = "Welcome" }
  labels      = ["web", "production"]
  contact_ids = [justmonitorit_contact.ops.id]
}

resource "justmonitorit_monitor" "api" {
  name              = "Status API"
  interval_s        = 30
  locations         = { regions = ["eu", "na"] }
  degraded_after_ms = 2000
  http_adv = {
    url     = "https://api.example.com/health"
    headers = [{ name = "X-Api-Key", secret_value = var.health_key }]
    assertions = [
      { type = "json_path", path = "$.status", op = "equals", json_value = jsonencode("ok") },
    ]
  }
  contact_ids = [justmonitorit_contact.ops.id]
}

resource "justmonitorit_monitor" "domain" {
  name          = "example.com registration"
  domain_expiry = { domain = "example.com", expiry_warn_days = 45 }
}

resource "justmonitorit_monitor" "backup" {
  name       = "Nightly backup"
  interval_s = 86400
  heartbeat  = { grace_s = 1800 }
}

output "backup_ping_url" {
  value     = justmonitorit_monitor.backup.ping_url
  sensitive = true
}
```

- **Locations**: `locations = { regions = [...] }` (every location in
  those regions) or `locations = { codes = [...] }` (2-12 locations; the
  `justmonitorit_locations` data source lists them). `domain_expiry`
  monitors leave `locations` and `interval_s` out: they look the domain up
  once a day.
- **Heartbeats**: `interval_s` is the period between the job's pings
  (60-604800 seconds), and there are no `locations`. The computed
  `ping_url` (sensitive: anyone who has it can ping the monitor) is what
  the job requests; followed by `/fail` it reports a failed run. In Pulumi
  it is the `pingUrl` output.
- **Changing the kind** replaces the monitor (a new id, new history).
- **Removing an attribute** puts its default back.
- **`cert_expiry_warn_days = 0`** turns the certificate warning off (the
  API's `null`).

## Contacts

Set exactly one channel: `email`, `sms`, `pushover`, `discord`,
`pagerduty`, `slack` or `webhook`.

```hcl
resource "justmonitorit_contact" "pagerduty" {
  name      = "PagerDuty"
  pagerduty = { routing_key = var.pagerduty_routing_key, region = "eu" }
  notify_on = ["down", "recovered", "degraded"]
}
```

Email and SMS contacts start `unverified`: the recipient confirms them before
alerts are sent, as when you add them in the web app. Creating them sends a
code, and an organisation can send 10 codes a day, so a configuration with
more than 10 new email or SMS contacts needs a second `apply` the next day.

A `webhook` contact stays `unverified` until its host has a TXT record that
proves you control it. The contact's `webhook_txt_name` and
`webhook_txt_value` give the record, so your DNS provider's resource can
publish it in the same configuration:

```hcl
resource "justmonitorit_contact" "webhook" {
  name    = "Ops webhook"
  webhook = { url = "https://hooks.example.com/justmonitorit?token=${var.webhook_token}" }
}

# For example with Cloudflare:
resource "cloudflare_dns_record" "justmonitorit" {
  zone_id = var.zone_id
  name    = justmonitorit_contact.webhook.webhook_txt_name
  type    = "TXT"
  content = "\"${justmonitorit_contact.webhook.webhook_txt_value}\""
  ttl     = 300
}
```

Then press **Check again** on the contact in the web app (or `POST
.../contacts/{id}/verify/send`) once the record is live.

Attach contacts with a monitor's `contact_ids`. Leave `contact_ids` out and
a new monitor gets every contact with **auto attach**, and Terraform leaves
its contacts alone afterwards. `paused` works the same way: set it to manage
pausing in code, leave it out to pause and resume in the web app.

## How it behaves

- **Secrets** (`basic_auth.password`, `bearer_token`, `secret_value`,
  `user_key`, `webhook_url`, `routing_key`) are write-only in the API: the
  provider keeps them in the state, marked sensitive. Protect your state as
  you protect the secrets. A secret changed in the web app is not detected.
- **Values the API normalises**, such as `https://Example.com` (stored as
  `https://example.com/`), stay as you wrote them in the state; a later
  change in the web app still shows as a difference.
- **Changes made elsewhere** show in `terraform plan`, and `apply` puts the
  configuration back.
- **Concurrent edits**: an update fails, without changing anything, if the
  monitor or contact was changed after Terraform last read it. Run
  `apply` again.
- **Rate limits**: requests over the per-minute write limit wait and retry
  for up to 5 minutes, so large applies just take longer. A longer limit
  fails at once and says when to retry.
- **Import**: `terraform import justmonitorit_monitor.homepage mon_…` (or
  `con_…` for a contact). Set any secrets in the configuration and apply
  once after importing.

Data sources: `justmonitorit_locations`, `justmonitorit_monitor` (with its
live `status` and `uptime_24h`) and `justmonitorit_contact`.

## OpenTofu

Use the same `source`. OpenTofu checks the same signature.

## Pulumi

The Pulumi provider has the same resources, with Pulumi names
(`justmonitorit:Monitor`, `justmonitorit:Contact`) and camelCase properties
(`intervalS`, `httpAdv`, `secretValue`). Install its plugin once, then use it
from a YAML program:

```sh
pulumi plugin install resource justmonitorit v0.1.0 \
  --server https://dash.justmonitorit.com/_registry/pulumi
pulumi config set --secret justmonitorit:apiToken jmi_...
```

```yaml
name: monitoring
runtime: yaml
resources:
  homepage:
    type: justmonitorit:Monitor
    properties:
      name: Homepage
      intervalS: 60
      locations:
        regions: [eu]
      http:
        url: https://example.com/
```

For TypeScript, Python, Go or C#, generate a local SDK from the Terraform
provider: `pulumi package add terraform-provider dash.justmonitorit.com/justmonitorit/justmonitorit`.
