# Digitalocean Terraform

> Write, review, and debug Terraform/OpenTofu configuration for DigitalOcean using the digitalocean/digitalocean provider. Bundles the complete provider reference (70 resources, 61 data sources) plus cross-cutting gotchas and composed multi-resource stacks. Use this whenever the user is working with DigitalOcean infrastructure as code — Droplets, VPCs, Cloud Firewalls, Load Balancers, DOKS/Kubernetes clusters, managed databases (Postgres/MySQL/Valkey/MongoDB/Kafka/OpenSearch), Spaces object storage, App Platform, CDN, reserved/floating IPs, DNS records, volumes, snapshots, container registry, monitoring and uptime alerts, or GradientAI — even if they don't name the provider explicitly. Also use when they mention .tf files that reference `digitalocean_*` resources, a DIGITALOCEAN_TOKEN, doctl-managed infrastructure being moved to Terraform, or ask why a DigitalOcean plan shows perpetual drift.

- Skill: `dbjpanda/digitalocean-terraform` (Agent Skill, multi-file: 140 files)
- Install (CLI): `npx skillmds@latest add dbjpanda/digitalocean-terraform`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dbjpanda/digitalocean-terraform/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: dbjpanda (https://skillmd.com/u/dbjpanda)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dbjpanda/digitalocean-terraform

---


# DigitalOcean Terraform provider

Everything here is derived from the official `digitalocean/digitalocean` provider
docs at **v2.99.1**, bundled verbatim under `references/provider-docs/`. Those files
are authoritative on argument names, types, defaults, and import syntax — prefer them
over recollection, because this provider changes often and several of its resources
have near-identical siblings that take different arguments.

## Start here

**Never write `digitalocean_*` HCL from memory.** The failure mode is not a clear
error — it is configuration that looks correct, passes review, and then either fails
at apply or produces a plan that never converges. Confirm arguments first; it costs
one command.

```bash
# What exists? (fuzzy, matches resources and data sources)
python3 scripts/dodocs.py list database

# Exact arguments and attributes for one resource
python3 scripts/dodocs.py args droplet

# Just the example, or just the import syntax
python3 scripts/dodocs.py example loadbalancer
python3 scripts/dodocs.py import database_db

# Which resources mention an argument at all?
python3 scripts/dodocs.py search vpc_uuid

# Data source rather than resource, when both names exist
python3 scripts/dodocs.py args kubernetes_cluster --kind data
```

Run from the skill directory. Names work with or without the `digitalocean_` prefix,
and a wrong guess suggests near matches instead of failing silently.

## The files

| File | Read it when |
|---|---|
| `references/gotchas.md` | **Before writing or reviewing any config.** Cross-cutting traps: perpetual drift, forced replacement, deprecated arguments, Spaces credentials. Short, and prevents most real-world breakage. |
| `references/patterns.md` | You need more than one resource wired together — web tier, database + firewall, DOKS, Spaces + CDN, App Platform, projects, imports. |
| `references/resource-index.md` | You know the goal but not the resource name. All 131 entries with one-line purposes, grouped by subcategory. |
| `references/deprecations.md` | Auditing an inherited config, or a plan won't converge. All nine deprecated arguments quoted from the provider's compiled schema — these messages are what `terraform validate` prints, and several are more specific than the markdown docs. |
| `references/provider-docs/` | You need exact arguments. Upstream docs verbatim: `resources/<name>.md`, `data-sources/<name>.md`, plus `index.md` for provider configuration. |

## Provider setup

```hcl
terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.99"
    }
  }
}

provider "digitalocean" {
  token = var.do_token # or the DIGITALOCEAN_TOKEN environment variable
}
```

The token grants full account access, so keep it in an environment variable or a
secret manager — never committed, and never a default value on a variable.

**Spaces is the exception that catches everyone.** Spaces resources authenticate with
S3-style keys, not the API token. Without `spaces_access_id` / `spaces_secret_key`
(or `SPACES_ACCESS_KEY_ID` / `SPACES_SECRET_ACCESS_KEY`), Spaces resources fail while
every other resource applies normally.

Other provider arguments worth knowing: `requests_per_second` (throttle to avoid rate
limits on large applies), `http_retry_max`, `api_endpoint`, `spaces_endpoint`. Full
list in `references/provider-docs/index.md`.

## Where things live

The provider's own subcategories, with what each covers:

| Area | Covers |
|---|---|
| **Networking** (33) | VPC, peering, NAT gateway, Cloud Firewall, Load Balancer, certificates, reserved IP/IPv6 (+assignments), DNS domains and records, CDN, BYOIP, Partner Network Connect |
| **Databases** (28) | Managed clusters, users, DBs, connection pools, DB firewalls, replicas, online migration, log sinks, and per-engine config resources (mysql, postgresql, valkey, redis, mongodb, kafka, opensearch) |
| **Account** (13) | Projects and project resources, tags, SSH keys, account/region/size lookups |
| **Spaces Object Storage** (12) | Buckets, objects, policies, CORS, logging, access keys |
| **Droplets** (8) | Droplets, autoscale pools, custom images, image lookups |
| **Dedicated Inference** (8) | Dedicated inference endpoints, tokens, GPU model config, sizes |
| **NFS Storage** (7) | NFS shares, access points, snapshots, attachments |
| **Backups & Snapshots** (5) | Droplet and volume snapshots |
| **Kubernetes** (4) | DOKS clusters, node pools, version lookups |
| **Volumes Block Storage** (3) | Volumes and volume attachments |
| **Monitoring** (3) | Metric alerts, uptime checks, uptime alerts |
| **Container Registry** (3) | Registry and Docker credentials |
| **GradientAI** (2) | GradientAI agents and knowledge bases |
| **App Platform** (2) | `digitalocean_app` and its data source |

Full per-resource breakdown with purposes: `references/resource-index.md`.

## The traps that actually bite

These cause the majority of broken DigitalOcean configurations. Details and fixes in
`references/gotchas.md`.

1. **Attach exactly one way.** Volumes, reserved IPs, reserved IPv6s, and floating
   IPs can each be attached inline on the Droplet *or* through a separate
   `*_attachment` / `*_assignment` resource. Using both makes every plan show
   changes forever. Setting `volume_ids` also means Terraform claims the Droplet's
   *entire* volume set.
2. **Floating IPs are now reserved IPs.** `digitalocean_floating_ip*` is deprecated;
   use `digitalocean_reserved_ip*`.
3. **`ssh_keys` cannot change after creation.** It takes key IDs or fingerprints, not
   key material, and editing it destroys and recreates the Droplet.
4. **Don't create a DOKS cluster and its Kubernetes resources in one module.**
   Terraform evaluates provider blocks before resources exist. Split into two applies.
5. **Database `*_config` resources don't unset remote settings on destroy.** They only
   leave state. Reset values explicitly first.
6. **Managed Redis was discontinued (30 June 2025).** Use Valkey for anything new.
7. **`certificate_id` can never converge — it is a drift bug, not a style nit.**
   `digitalocean_certificate.id` is the certificate's *name*, not its UUID, so the
   field permanently disagrees with the API. Attach by `certificate_name`, and rotate
   with a new name plus `create_before_destroy`.
8. **Set `disable_lets_encrypt_dns_records = true`** when a load balancer has a
   Let's Encrypt cert and you also manage that hostname's DNS record in Terraform —
   otherwise DigitalOcean's auto-created record fights yours.
9. **`force_destroy` on a Spaces bucket defaults to `false`,** so destroy fails on a
   non-empty bucket.
10. **Projects take URNs** (`.urn`), not IDs.
11. **Singular data sources error on zero or multiple matches.** Use the plural forms
    (`digitalocean_droplets`, `digitalocean_images`, …) for lists.
12. **Renaming a volume's `filesystem_type` to `initial_filesystem_type` destroys the
    volume and its data.** The replacement is `ForceNew` and never read back, so on an
    existing volume it always diffs `null → "ext4"` and forces replacement. Delete the
    deprecated argument instead of renaming it (gotchas.md §12b).

## Working approach

**Writing new configuration**

1. Find the resource — `dodocs.py list <term>` or `references/resource-index.md`.
2. Read its real arguments — `dodocs.py args <name>`. Note which are `(Required)` and
   which say "Changing this forces a new resource".
3. If more than one resource is involved, check `references/patterns.md` for the
   composed shape before assembling it yourself.
4. Scan `references/gotchas.md` for anything touching the resources you used.
5. Look up regions, sizes, and versions with data sources rather than hardcoding
   slugs — availability changes per account and region.
6. Verify: `terraform fmt`, then `terraform validate`, then `terraform plan`.

**Reviewing or debugging existing configuration**

- *Plan never converges / always shows changes* → check three things, in this order:
  the attach-once rule (gotchas.md §1), `certificate_id` on a load balancer or CDN
  (§8 — the field is structurally incapable of converging), and `*_config` resources
  (§5). A config can have more than one cause at once, so keep looking after the
  first hit.
- *Cleaning up deprecated arguments on a live resource* → check gotchas.md §12b
  first. The obvious fix for a volume's `filesystem_type` destroys the volume. Read
  the plan for `must be replaced` before applying, and stop on a `-/+` against
  anything holding data.
- *Unexpected destroy/recreate* → look for `ssh_keys` edits, or an argument the docs
  mark as forcing a new resource. `dodocs.py args <name>` shows these.
- *Auth failures on only some resources* → missing Spaces credentials.
- *Copied from a blog post* → check the deprecated-argument table in gotchas.md §9;
  `private_networking`, `algorithm`, `certificate_id`, and app `routes`/`cors` are
  the usual survivors.

**Verification honesty:** `terraform validate` checks syntax and types but does not
contact the API, so it cannot confirm that a region supports a size or that a slug
exists. Only `terraform plan` (with credentials) does. Say which one you actually
ran rather than implying more coverage than you have.

## Version note

Bundled docs are v2.99.1 (published 2026-08-06). If the user pins a different major
or a much newer minor version, arguments may differ — check the upstream repo at
`github.com/digitalocean/terraform-provider-digitalocean` under `docs/`, or the
registry, and say so rather than assuming these docs still apply.

