All Projects
Open Source2024

Pingdom Terraform Provider

Open-source Terraform Provider in Go that manages Pingdom monitoring resources — checks, contacts, and teams — as infrastructure code, with a layered expand/flatten/normalize model that enforces strict state consistency and eliminates phantom diffs across plan/apply cycles.

GoTerraform Plugin SDKPingdom APIHCL

At a Glance

Resources managed3 types
Check protocolsHTTP / Ping / TCP
Plan stabilityIdempotent
Import supportAll resources

The Problem

Pingdom had no official Terraform Provider, which forced teams to manage monitoring configuration manually or through brittle scripts outside the IaC lifecycle. The deeper problem was correctness: any provider implementation had to survive the full Terraform workflow — plan, apply, import, refresh — without producing false diffs caused by API defaults, collection ordering instability, or type coercions between HCL and the Pingdom API model.

Architecture

The Conversion Model

Every Terraform Provider lifecycle method — Create, Read, Update, Delete, Import — passes through the same two paths. Expand translates HCL config into an API call. Flatten translates API responses back into Terraform state. Both paths share the same internal model, enforcing consistent handling of every field regardless of which lifecycle triggered the operation.

expandConfig → API

  1. 1

    User Config (HCL)

    Terraform schema reads attributes

  2. 2

    Type conversion

    Set → slice, string → int

  3. 3

    Validation

    Enum checks, mutually exclusive fields

  4. 4

    Normalize

    Tag sort, header casing, region format

  5. 5

    Internal Model

    Canonical intermediate representation

  6. 6

    Pingdom API call

    Create / Update with typed body

flattenAPI → State

  1. 1

    API Response

    JSON decoded into Go struct

  2. 2

    Adapter layer

    Normalize API quirks, fill gaps

  3. 3

    Internal Model

    Same canonical representation

  4. 4

    Sort collections

    All slices and sets sorted

  5. 5

    Suppress defaults

    API auto-fields excluded from state

  6. 6

    Terraform State

    Written via d.Set() — stable output

Symmetry guarantee: For every field written in expand, there is a corresponding read in flatten that produces identical output. This symmetry is what makes terraform import followed by terraform plan produce zero diff.

Lifecycle API Flows

Every Terraform lifecycle operation — Create, Read, Update, Delete, Import — ultimately resolves to one or two Pingdom API calls. The sequence diagrams below trace each operation end-to-end. Click any step to see the corresponding Pingdom API spec.

1ResourceCreate(ctx, d, meta)2expand(HCL config → API body)3POST /api/3.1/checks4200 {check: {id, name}}5GET /api/3.1/checks/{id}6200 Full check object7flatten(API response → state)8State written — zero diffTerraform CLIProvider (Go)Pingdom APITerraform CLIProvider (Go)Pingdom API

Normalize: Stability by Design

The normalize layer exists to solve a specific failure mode: terraform plan shows a diff even though the user has not changed their configuration. This happens when the API response is semantically equivalent to what was applied, but textually different. The table below maps each source of instability to its fix.

TypeProblemSolutionWhere
List / SetAPI returns elements in unstable order across readsSort all collections before writing to stateexpand + flatten
MapKey ordering varies between Go map iterationsCanonical key ordering enforced at write timeflatten
API defaultsAPI injects default values not set by the user, producing diffDiffSuppressFunc — suppress if user value ≈ API valueschema
Empty vs nilTerraform treats empty string and nil differently; API may return one for the otherUnified nil handling in flatten — always write empty string, never nilflatten
Region format"region: NA" vs "region:NA" triggers spurious diffNormalize to canonical format in expand before comparisonexpand

Resource Design

The provider manages three Pingdom resource types. Each has distinct complexity. pingdom_check is the most involved: it unifies three check protocols under a single schema, requiring shared fields, type-gated fields, and complex alert routing configuration.

pingdom_checkMost complex

Unifies HTTP, Ping, and TCP checks under a single resource schema with a type discriminator field.

  • ·Common fields shared across all check types
  • ·Type-specific fields gated by type value
  • ·Mutually exclusive field validation (shouldcontain vs shouldnotcontain)
  • ·Alert routing via integrationids, userids, teamids
  • ·API quirks adapter (webhook behavior, port defaults)
pingdom_contactNested blocks

Manages notification contacts with nested SMS and email configuration blocks.

  • ·Nested block schema for sms and email entries
  • ·Severity rules: HIGH vs LOW per notification channel
  • ·Multiple notifications merged and order-stabilized
  • ·Contact ID referenced by teams — deletion must respect dependencies
pingdom_teamReferences

Groups contacts into notification teams, managing member references by ID.

  • ·Member list stored as contact IDs (not names)
  • ·Deletion must handle downstream check references
  • ·Order-stable member list in state across reads

Engineering Challenges

01

Phantom diffs on every plan

Root cause

API responses return collections in unstable order; default values are injected silently.

Fix

Expand/flatten symmetry with sort-on-write ensures identical input always produces identical state. DiffSuppressFunc handles the remaining cases where API output is semantically equivalent but textually different.

02

Import leaves inconsistent state

Root cause

terraform import reads live API state and writes it directly — if flatten is not perfectly canonical, the next plan shows spurious changes.

Fix

Flatten always outputs canonical state: sorted collections, no nil values, API auto-fields excluded. Import uses the same flatten path as Read.

03

Schema is a published API contract

Root cause

Once users have state files referencing your schema, changing field names or types is a breaking change.

Fix

Design the internal model before stabilizing the schema. Internal model changes are private; schema changes are not. Backward compatibility constraints guide every new field decision.

04

Pingdom API behavior is inconsistent

Root cause

The Pingdom API ignores some parameters silently, injects fields the user did not set, and occasionally returns different defaults for GET vs POST.

Fix

An adapter layer isolates all API quirks from resource logic. Retry, fallback, and parameter suppression rules live in one place and are easy to audit.

Outcomes

  • Achieved fully idempotent plan/apply cycles: running terraform plan against an applied configuration always produces zero diff, even across the full range of API default injection and collection reordering behaviors.
  • terraform import produces state that passes plan with no diff — the canonical flatten output is identical to what a fresh apply would write.
  • All three check types (HTTP, Ping, TCP), contact notification targets with nested SMS/email blocks, and team membership management are supported end-to-end.
  • Published as open-source with complete resource coverage, import support, and documented schema for all Pingdom resource types.

Lessons

  • 1A Terraform Provider is a state synchronization system, not an API client library. The hard work is not making API calls — it is designing expand/flatten symmetry so state never drifts.
  • 2DiffSuppressFunc is the right tool for API-induced instability, but it needs to be tested explicitly. A suppressed diff that fires incorrectly is harder to debug than a schema bug.
  • 3Import correctness is a first-class test case, not an afterthought. Running terraform import → plan as part of every resource's test suite catches an entire class of flatten bugs before users hit them.
  • 4Schema design is a public API contract. Fields and their types are hard to change after publication without breaking existing state files — getting the internal model right before stabilizing the schema saves significant pain.