I forked the PlanetScale Terraform Provider

Disclaimer: I am bullish on Planetscale. I also have strong opinions about Infrastructure as Code.

Last week, I was excited to see the PlanetScale Terraform provider v1 (LINK) was released and now includes support for their Postgres product! 🎉
provider-release-announcement.png

Unfortunately, it only includes resources for managing branches (not the databases themselves). See: https://planetscale.com/docs/terraform#conceptual-model

PlanetScale databases themselves (the logical database containers) are not managed directly as Terraform resources in v1.0. Instead:

  • Create databases in PlanetScale first (via the PlanetScale UI or API).
  • Use Terraform to manage branches and related resources (roles, passwords, etc.) within those databases.

In my opinion, this makes the provider totally unusable. Having to diverge from Infrastructure as Code to click-ops the database and then import the default branch breaks any fully automated workflow and results in a less than ideal solution.

After chatting with some folks from PlanetScale (shoutout to @josh and @Josh 😅) they helped me understand the reasoning behind this design choice:

feedback-from-josh.png

TL;DR: The PlanetScale data model and lifecycle for databases and branches doesn't map cleanly onto the Terraform resource model and they didn't want to lock in an approach until they were confident it was the right one.

Challenges

There are three main challenges with how PlanetScale databases/branches behave when designing the Terraform provider:

  1. A default branch is auto-created when a DB is created
  2. A database cannot exist without at least one branch
  3. The default branch can be modified after creation
Note:

For the sake of this article I will use postgres for all examples. Analogous resources would exist for PlanetScale vitess databases/branches.

Together, these raise questions about how to design the Terraform provider:

  1. Who owns the default branch in Terraform?
  2. How is the auto-created branch adopted without import?
  3. How is a change in default branch represented?
  4. What should happen during terraform destroy?

A naive implementation would either lead to resource contention between the database and branch resources or require a two-step create-then-import process for the default branch.

Prior Art

To understand how to best approach this, I investigated at existing Terraform providers that face similar challenges.

The AWS, GitHub contains a handful of resources which must account for pre-existing and default lifecycle behaviors:

User Stories

To capture my goals I came up with the following user stories:

My Solution

I decided to forked the provider repo and experiment with implementing a proposed solution. Here is what I ended up with: PULL REQUEST

Together, these enable all of the desired user stories and I was able to use the modified provider to:

  1. Provision a new database, adopt the default branch, create a new branch, and modify the default branch to point to that new branch in a single apply command
  2. Cleanly tear down all of those resources with a single delete command

Here is my config:

terraform {
  required_providers {
    planetscale = {
      source  = "sidpalas/planetscale"
      version = "1.1.0-postgres-db"
    }
  }
}

provider "planetscale" {

}

resource "planetscale_database_postgres" "db" {
  organization = "sid-devopsdirective"
  name         = "with-branch"
  cluster_size = "PS_5_AWS_ARM"
}

resource "planetscale_postgres_branch" "main" {
  organization      = "sid-devopsdirective"
  database          = planetscale_database_postgres.db.id
  name              = "main"
  cluster_size      = "PS_5_AWS_ARM"
  adopt_if_exists   = true
}

resource "planetscale_postgres_branch" "someotherbranch" {
  organization      = "sid-devopsdirective"
  database          = planetscale_database_postgres.db.id
  name              = "someotherbranch"
  cluster_size      = "PS_5_AWS_ARM"
  on_default_delete = "remove_from_state"
}

resource "planetscale_database_default_branch" "this" {
  organization = "sid-devopsdirective"
  database     = planetscale_database_postgres.db.id
  branch       = planetscale_postgres_branch.someotherbranch.name
}

terraform apply 🚀

$ terraform apply  

...

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

planetscale_database_postgres.db: Creating...
planetscale_database_postgres.db: Still creating... [00m10s elapsed]
...
planetscale_database_postgres.db: Still creating... [01m50s elapsed]
planetscale_database_postgres.db: Creation complete after 1m51s [id=p4wvn79boc9j]
planetscale_postgres_branch.someotherbranch: Creating...
planetscale_postgres_branch.main: Creating...
planetscale_postgres_branch.main: Creation complete after 3s [id=2bqz7zmvhm33]
planetscale_postgres_branch.someotherbranch: Still creating... [00m10s elapsed]
...
planetscale_postgres_branch.someotherbranch: Still creating... [02m10s elapsed]
planetscale_postgres_branch.someotherbranch: Creation complete after 2m20s [id=xgi6gwxpmafo]
planetscale_database_default_branch.this: Creating...
planetscale_database_default_branch.this: Creation complete after 0s [id=p4wvn79boc9j]
╷
│ Warning: existing branch adopted
│ 
│   with planetscale_postgres_branch.main,
│   on main.tf line 21, in resource "planetscale_postgres_branch" "main":
│   21: resource "planetscale_postgres_branch" "main" {
│ 
│ The PostgreSQL branch already exists and was adopted because adopt_if_exists is true.
╵

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

ps-dashboard.png

terraform destroy 🧨

terraform destroy

...

Plan: 0 to add, 0 to change, 4 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

planetscale_database_default_branch.this: Destroying... [id=p4wvn79boc9j]
planetscale_postgres_branch.main: Destroying... [id=2bqz7zmvhm33]
planetscale_database_default_branch.this: Destruction complete after 0s
planetscale_postgres_branch.someotherbranch: Destroying... [id=xgi6gwxpmafo]
planetscale_postgres_branch.someotherbranch: Destruction complete after 0s
planetscale_postgres_branch.main: Destruction complete after 1s
planetscale_database_postgres.db: Destroying... [id=p4wvn79boc9j]
planetscale_database_postgres.db: Destruction complete after 0s
╷
│ Warning: default branch not deleted remotely
│ 
│ The branch is currently the database default branch and cannot be deleted by PlanetScale. The resource was
│ removed from Terraform state because on_default_delete is set to remove_from_state.
╵

Destroy complete! Resources: 4 destroyed.

ps-dashboard-destroy.png

Limitations

There are some important limitations remain in the current implementation:

Input Requested! 🙏

NOTE

The PlanetScale team wants to get to a clean long-term solution here, but wanted to avoid locking in a pattern they would eventually come to regret! My goal with the fork + article was to help make progress towards such a solution!

If you have experience working with PlanetScale and using (or authoring) Terraform providers, I’d love your input on the lifecycle tradeoffs discussed here, especially around default branch management, adoption of pre-existing resources, and destroy behavior for ephemeral environments.

Please share your feedback, edge cases, or preferred UX on the GitHub issue: https://github.com/planetscale/terraform-provider-planetscale/issues/260

Try it out!

I published my version of the provider here if you want to take a look and experiment with its ergonomics! https://registry.terraform.io/providers/sidpalas/planetscale/latest

The source for my fork can be seen here: https://github.com/sidpalas/terraform-provider-planetscale