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.
At a Glance
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
User Config (HCL)
Terraform schema reads attributes
- 2
Type conversion
Set → slice, string → int
- 3
Validation
Enum checks, mutually exclusive fields
- 4
Normalize
Tag sort, header casing, region format
- 5
Internal Model
Canonical intermediate representation
- 6
Pingdom API call
Create / Update with typed body
flattenAPI → State
- 1
API Response
JSON decoded into Go struct
- 2
Adapter layer
Normalize API quirks, fill gaps
- 3
Internal Model
Same canonical representation
- 4
Sort collections
All slices and sets sorted
- 5
Suppress defaults
API auto-fields excluded from state
- 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.
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.
| Type | Problem | Solution | Where |
|---|---|---|---|
| List / Set | API returns elements in unstable order across reads | Sort all collections before writing to state | expand + flatten |
| Map | Key ordering varies between Go map iterations | Canonical key ordering enforced at write time | flatten |
| API defaults | API injects default values not set by the user, producing diff | DiffSuppressFunc — suppress if user value ≈ API value | schema |
| Empty vs nil | Terraform treats empty string and nil differently; API may return one for the other | Unified nil handling in flatten — always write empty string, never nil | flatten |
| Region format | "region: NA" vs "region:NA" triggers spurious diff | Normalize to canonical format in expand before comparison | expand |
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 complexUnifies 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 blocksManages 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_teamReferencesGroups 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
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.
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.
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.
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.