The sartar Terraform provider
The provider lets you describe your monitoring as code: folders, monitors,
scenarios, notification channels and lists, roles and users. It works with
Terraform and with
OpenTofu. Every terraform command on this page works
the same with tofu.
Install
There is nothing to download by hand. Declare the provider, and
terraform init installs it.
terraform {
required_providers {
sartar = {
source = "sartar.app/harskogr/sartar"
version = "~> 2.0"
}
}
}terraform initterraform init finds the provider from the host name in source, downloads
the build for your platform, and verifies its signature and its checksum. It
then records the checksums in .terraform.lock.hcl. Commit that file.
The source must be written in full, host name included. The provider is
served by sartar, not by the public Terraform registry.
Supported platforms
Linux, macOS, Windows and FreeBSD, on amd64 and arm64. Linux, Windows and FreeBSD are also built for 386, and Linux and FreeBSD for arm.
Signing key
Releases are signed with this key. terraform init prints its identifier when
it installs the provider.
Key ID FDC7EE9C5F903C58
Fingerprint 598F C4D4 D2CB 22E0 A197 7BD3 FDC7 EE9C 5F90 3C58Without network access
Download the archive of the provider on a machine that has access:
https://sartar.app/dl/terraform-provider-sartar/<version>/terraform-provider-sartar_<version>_<os>_<arch>.zipThen extract it into the plugin directory of Terraform:
~/.terraform.d/plugins/sartar.app/harskogr/sartar/<version>/<os>_<arch>/Configure
The provider needs the address of the API and an API key. Create the key in the web dashboard: account menu, API keys, Create key. See Before you start.
provider "sartar" {
endpoint = "https://sartar.app/api"
token = var.sartar_token
}
variable "sartar_token" {
type = string
sensitive = true
}| Argument | Environment variable | Value |
|---|---|---|
endpoint |
SARTAR_ENDPOINT |
https://sartar.app/api |
token |
SARTAR_TOKEN |
your API key |
Both arguments are required, in the configuration or in the environment. Keep the key out of your repository. The environment is the simplest way:
export SARTAR_ENDPOINT=https://sartar.app/api
export SARTAR_TOKEN=<your API key>
terraform planWhich key for which command
| Command | Key needed |
|---|---|
terraform plan, terraform import, refresh |
a Read-only key is enough |
terraform apply, terraform destroy |
a Read / write key |
A good habit is to plan with a read-only key, and to keep the read / write key for the apply step of your pipeline.
A first configuration
This creates a folder, a notification channel, and a monitor inside the folder that alerts on the channel.
resource "sartar_folder" "prod" {
name = "Production"
notes = "Customer-facing endpoints."
}
resource "sartar_notification_channel" "slack" {
type = "slack"
name = "ops-alerts"
config = {
webhook_url = var.slack_webhook_url
}
}
variable "slack_webhook_url" {
type = string
sensitive = true
}
resource "sartar_monitor" "api_health" {
name = "api health"
url = "https://api.example.com/health"
period = 60
regions = ["fr"]
folder = "/${sartar_folder.prod.name}"
http_check = {
status_code = "200"
}
notification = {
channel_id = sartar_notification_channel.slack.id
}
}terraform plan
terraform applyWriting the folder as "/${sartar_folder.prod.name}" rather than as plain text
tells Terraform that the monitor depends on the folder. The folder is then
always created first.
Resources
| Resource | What it manages |
|---|---|
sartar_folder |
a folder, and the settings inherited by what it contains |
sartar_monitor |
an HTTP monitor |
sartar_scenario |
a scenario and its steps |
sartar_notification_channel |
a Slack, PagerDuty, Google Chat or webhook alert target |
sartar_notification_list |
a named group of alert targets |
sartar_mailbox |
an inbound e-mail address |
sartar_role |
a named set of permissions |
sartar_user |
a user of the account and their role |
sartar_account_settings |
the default settings of the account |
sartar_account_properties |
the properties of the account |
sartar_folder
| Attribute | Type | Notes |
|---|---|---|
id |
string, computed | uuid of the folder |
name |
string, required | unique in the account, must not contain / |
notes |
string | |
parent_uuid |
string | the parent folder, omitted for a folder at the root |
properties |
map of strings | |
notification |
object | alerting inherited by everything inside, same shape as on a monitor |
resource "sartar_folder" "prod_eu" {
name = "Production EU"
parent_uuid = sartar_folder.prod.id
}sartar_monitor
| Attribute | Type | Notes |
|---|---|---|
id |
string, computed | uuid of the monitor |
name |
string, required | |
url |
string, required | |
period |
number | seconds between two checks |
regions |
set of strings | exactly one region |
enabled |
bool | true by default |
is_manual |
bool | never scheduled, run on demand only |
notes |
string | |
folder |
string | path of the folder, such as /prod/edge. Empty means the root |
calendar |
object | restricts the schedule. See below |
properties |
map of strings | |
http_request |
object | method, body, preserve_cookies, real_hostname, headers |
http_check |
object | status_code, trust_certificate, retry, follow_redirect, and the checks on the response: headers, cookies, body, body_json |
notification |
object | the alert target. See below |
Changing folder to a path that does not exist makes the apply fail. Create
the folder first.
Schedule. Omit calendar to check around the clock.
# Weekdays only, from 08:00 to 18:00.
calendar = {
days = [1, 2, 3, 4, 5] # 0 is Sunday, 6 is Saturday
from = "08:00"
to = "18:00"
}
# Once a day, at a fixed time. `period` is then ignored.
calendar = {
daily_at = "02:30"
}Request and checks.
http_request = {
method = "POST"
body = "{\"ping\":true}"
headers = [
{ header = "Content-Type", value = "application/json" },
]
}
http_check = {
status_code = "2xx"
body_json = [
{ key = "status", operator = "equals", value = "up" },
]
}status_code accepts an exact code such as "200", or a class such as
"2xx". retry and follow_redirect are true by default. Set one to
false to turn it off.
Alert target. Set exactly one of email, channel_id, channel_name,
list_id or list_name.
notification = {
channel_name = "ops-alerts"
checks_before_alert = 2
checks_before_resume = 1
}Omit a block to keep what the monitor inherits from its folders and from the defaults of the account.
sartar_scenario
| Attribute | Type | Notes |
|---|---|---|
id |
string, computed | uuid of the scenario |
name |
string, required | unique in the account |
region |
string | one region for all the steps |
period |
number | seconds between two runs |
enabled, is_manual, notes, folder, calendar |
same as on a monitor | |
notification |
object | same shape as on a monitor |
steps |
list, required | the steps, in order |
A step is either a check or a pause.
resource "sartar_scenario" "checkout" {
name = "checkout flow"
region = "fr"
period = 600
steps = [
{
type = "monitor"
name = "login"
url = "https://example.com/login"
http_request = {
method = "POST"
body = "{\"user\":\"demo\"}"
}
http_check = {
status_code = "2xx"
}
},
{
type = "wait"
min_seconds = 3
max_seconds = 8
},
{
type = "monitor"
name = "dashboard"
url = "https://example.com/dashboard"
http_check = {
status_code = "200"
body = [
{ operator = "contains", value = "Welcome" },
]
}
},
]
}A scenario alerts when the outcome of a run changes, from success to failure or back. It does not alert for each step.
Any change to a scenario recreates all of its steps.
sartar_notification_channel
| Attribute | Type | Notes |
|---|---|---|
id |
string, computed | |
type |
string, required | slack, pagerduty, googlechat or webhook. Changing it replaces the channel |
name |
string, required | |
config |
map of strings, sensitive | depends on the type |
| Type | Keys of config |
|---|---|
slack |
webhook_url |
pagerduty |
routing_key |
googlechat |
webhook_url |
webhook |
url, method, body |
sartar_notification_list
A list delivers an alert to every one of its members. Each member sets exactly
one of channel_id, channel_name or email.
resource "sartar_notification_list" "oncall" {
name = "on-call"
members = [
{ channel_id = sartar_notification_channel.slack.id },
{ email = "alice@example.com" },
]
}An e-mail address must belong to a user of the account. A list that something still alerts to cannot be deleted.
sartar_mailbox
An e-mail address that receives mail for your account. You choose the name, and sartar generates the address.
resource "sartar_mailbox" "signup" {
name = "signup-flow"
}
output "signup_address" {
value = sartar_mailbox.signup.address
}Mailboxes must be enabled on your account.
sartar_role and sartar_user
A role is a named set of permissions. rights maps a kind of object to the
actions allowed on it.
resource "sartar_role" "ci" {
name = "CI scenarios"
description = "Runs scenarios and reads their results"
rights = {
scenario = ["read", "run", "progress"]
}
}
resource "sartar_user" "alice" {
email = "alice@example.com"
role_id = sartar_role.ci.id
}A user can also take one of the built-in roles with
role = "administrator", "editor" or "reader".
The key Terraform runs with can never grant more than it holds itself.
sartar_account_settings and sartar_account_properties
Both exist once per account. Declare each at most once.
resource "sartar_account_properties" "account" {
properties = {
env = "prod"
}
}sartar_account_properties owns the whole map: a key you remove from the
configuration is removed from the account.
sartar_account_settings takes its values as JSON, in settings_json. A key
you remove from it keeps its last value. To change a setting back, state the
value you want.
Data sources
| Data source | Use |
|---|---|
sartar_regions |
the regions your account can use |
sartar_notification_channel |
a channel that Terraform does not manage, found by name |
sartar_mailbox |
a mailbox found by name, to read its address |
data "sartar_notification_channel" "ops" {
name = "ops-alerts"
}
resource "sartar_monitor" "web" {
name = "example.com"
url = "https://example.com"
period = 300
regions = ["fr"]
notification = {
channel_id = data.sartar_notification_channel.ops.id
}
}Import one object
Every resource can be imported by its identifier. For a folder, a monitor or a scenario, this is its uuid. The sartar CLI prints it:
sartar monitor show --name "/prod/api health" --quietterraform import sartar_monitor.api_health <uuid>The two account resources import with the identifier account.
Adopt an existing account
You already have monitors, and you want Terraform to manage them without creating a second copy. The web dashboard writes the configuration for you, and the CLI tells Terraform which existing object each resource is.
You need the sartar CLI for step 3.
1. Export the account
In the web dashboard, open the account menu and choose
Export to Terraform. Save the document as sartar.tf, in a directory of
its own.
Choose a private directory. The state file of Terraform will be written next
to sartar.tf, and it holds every secret of the configuration in clear text.
The export contains everything the provider can manage, except the users.
2. Fill in the secrets
The export replaces secrets, such as webhook addresses, with variables. Give them a value through the environment:
export TF_VAR_slack_alerts_webhook_url='https://hooks.slack.com/services/…'Do this before you apply. Applying a placeholder would overwrite the real value, and the alerts would stop arriving.
3. Generate the import blocks
sartar terraform imports --file sartar.tf > imports.tfThe CLI matches each resource of the document with the object of the same name
in your account. It writes one import block for each match. What it cannot
match with certainty is left out and listed in a comment at the end of the
file. Nothing is guessed.
A read-only key is enough.
4. Read the plan
terraform init
terraform planYou want a plan that imports, and changes nothing. Two differences are expected:
sartar_account_settingsgoes from empty to your values;- a secret you have not filled in yet.
Any other change means the document and the account disagree. Fix the document, then plan again.
5. Apply once
terraform applyThis step needs a read / write key. Afterwards, delete imports.tf. It has
done its job.
From now on the configuration is the reference. Edit the .tf files, plan,
then apply.
Upgrade the provider
Raise the version constraint if needed, then:
terraform init -upgradeThe provider and the platform must agree on the version of the API. When they no longer do, Terraform stops with this message:
Error: Provider and sartar API versions have divergedUpgrading the provider fixes it.
Good to know
- Users and results stay outside Terraform.
terraform destroyremoves neither the people of your account nor the history of your checks. - Secrets are in the state. Protect the state file as you protect the keys themselves.
- Replacing a monitor starts a new history. Terraform tells you in the plan when a change forces a replacement.