diff --git a/.controlplane/docs/cpflow-vs-terraform.md b/.controlplane/docs/cpflow-vs-terraform.md new file mode 100644 index 00000000..9c6a0da9 --- /dev/null +++ b/.controlplane/docs/cpflow-vs-terraform.md @@ -0,0 +1,142 @@ +# cpflow vs. Terraform: which manages what? + +Control Plane ships an official [Terraform provider](https://registry.terraform.io/providers/controlplane-com/cpln/latest), +so it is reasonable to ask whether the `.controlplane/templates/*.yml` files +should be Terraform instead. The short answer for this app: **no**, because the +YAML here covers two concerns and Terraform only addresses one of them. + +## Two kinds of YAML in `.controlplane/` + +1. **`controlplane.yml`** — cpflow's own config: app/org aliases, + `app_workloads`, `release_script`, `upstream` (staging→production + promotion), `match_if_app_name_starts_with`, image retention. **This has no + Terraform equivalent.** It drives a Heroku-style deploy workflow, not + infrastructure state. +2. **`templates/*.yml`** — native `cpln apply` resource manifests (GVC, + workloads, secrets, policies, identities, volume sets). These *could* be + rewritten as `cpln_*` Terraform resources. + +## What Terraform does not replace + +The app lifecycle that is cpflow's whole reason to exist: + +- `build-image` / `deploy-image` with sequential image tags +- release-phase migrations (`cpflow run --image latest -- rails db:migrate`) +- staging → production promotion +- **ephemeral per-PR review apps** (the `+review-app-deploy` flow + prefix + matching) + +Modeling per-PR environments in Terraform means a workspace and state file per +PR — far heavier than `cpflow setup-app -a qa-react-webpack-rails-tutorial-1234`. + +## Decision rule + +| Concern | Use | +| --- | --- | +| App build / deploy / release-phase migrations | **cpflow** | +| Ephemeral per-PR review apps | **cpflow** | +| Staging → production promotion | **cpflow** | +| Durable shared infra: orgs, custom domains, mk8s, agents, IAM/policies, external RDS/ElastiCache | **Terraform** (`cpln` provider) | + +For a real production system, the two are complementary — Terraform for durable +shared infrastructure, cpflow for the app deploy loop (Control Plane even +publishes a +[GitHub Actions Terraform example](https://github.com/controlplane-com/github-actions-example-terraform)). +This tutorial app has essentially no durable shared infra and exists to +demonstrate the cpflow deploy loop, so it stays entirely on cpflow. + +## Appendix: what the templates would look like as HCL + +A side-by-side mapping of the current cpflow templates (`templates/app.yml` + +`templates/rails.yml`) to the [`controlplane-com/cpln`](https://registry.terraform.io/providers/controlplane-com/cpln/latest) +provider. The interesting parts are in the comments — they are where the two +models actually diverge. + +```hcl +# variables.tf +variable "app_name" { type = string } # cpflow injects {{APP_NAME}} per review app; + # in TF this is a -var or a workspace name. +variable "location" { type = string default = "aws-us-east-2" } +variable "image_link" { type = string } # cpflow's {{APP_IMAGE_LINK}} is set at *deploy* + # time by `deploy-image`. In TF the image is a + # plain argument, so every deploy is a full apply. + +# gvc.tf — was templates/app.yml (kind: gvc + kind: identity) +resource "cpln_gvc" "app" { + name = var.app_name + locations = [var.location] # was staticPlacement.locationLinks: [{{APP_LOCATION_LINK}}] + + # was spec.env: (a list of {name,value}); in TF it is a flat map. + env = { + DATABASE_URL = "postgres://the_user:the_password@postgres.${var.app_name}.cpln.local:5432/${var.app_name}" + REDIS_URL = "redis://redis.${var.app_name}.cpln.local:6379" + RAILS_ENV = "production" + NODE_ENV = "production" + RAILS_SERVE_STATIC_FILES = "true" + SECRET_KEY_BASE = "placeholder_secret_key_base_for_test_apps_only" + RENDERER_PORT = "3800" + RENDERER_LOG_LEVEL = "info" + RENDERER_WORKERS_COUNT = "2" + RENDERER_URL = "http://localhost:3800" + RSC_SUSPENSE_DEMO_DELAY = "true" + # cpln:// secret references are just strings, so they port over verbatim: + RENDERER_PASSWORD = "cpln://secret/${var.app_name}-secrets.RENDERER_PASSWORD" + REACT_ON_RAILS_PRO_LICENSE = "cpln://secret/${var.app_name}-secrets.REACT_ON_RAILS_PRO_LICENSE" + } +} + +resource "cpln_identity" "app" { # was the second doc in app.yml (kind: identity) + gvc = cpln_gvc.app.name + name = "${var.app_name}-identity" +} + +# rails.tf — was templates/rails.yml (kind: workload) +resource "cpln_workload" "rails" { + gvc = cpln_gvc.app.name + name = "rails" + type = "standard" + identity_link = cpln_identity.app.self_link # was identityLink: {{APP_IDENTITY_LINK}} + + container { + name = "rails" + image = var.image_link # was {{APP_IMAGE_LINK}} + cpu = "300m" + memory = "1Gi" + inherit_env = true # pulls the GVC env above + env = { LOG_LEVEL = "debug" } + + ports { + protocol = "http" # keep http — Thruster does HTTP/2 on the TLS frontend + number = "3000" + } + } + + options { + capacity_ai = true + autoscaling { + max_scale = 1 # maxScale 1 ≈ a single Heroku dyno (other fields default) + } + } + + firewall_spec { + external { + inbound_allow_cidr = ["0.0.0.0/0"] + outbound_allow_cidr = ["0.0.0.0/0"] + } + } +} +``` + +`templates/postgres.yml`, `templates/redis.yml`, and `templates/daily-task.yml` +follow the same pattern (`cpln_workload` + `cpln_secret` + `cpln_policy` + +`cpln_volumeset`). The mechanical translation is straightforward — the field +names line up almost 1:1. + +What the comments are really showing: the three things cpflow gives you for free +(`{{APP_NAME}}` per-PR interpolation, deploy-time `{{APP_IMAGE_LINK}}` +injection, and the implicit "provision ≠ deploy" separation) all become *your* +problem in Terraform. The image being a plain argument is the big one: in +cpflow, `deploy-image` bumps the running tag without touching infra; in +Terraform, a new image is a `terraform apply` that diffs the whole workload. And +none of `controlplane.yml` (release script, upstream promotion, review-app +prefix matching) has any representation above at all. diff --git a/.controlplane/readme.md b/.controlplane/readme.md index 735f133a..8eb17627 100644 --- a/.controlplane/readme.md +++ b/.controlplane/readme.md @@ -163,6 +163,10 @@ These YAML files are the same as used by the `cpln apply` command. 3. `Dockerfile`: defines the Docker image used to run the app on Control Plane. 4. `entrypoint.sh`: defines the entrypoint script used to run the app on Control Plane. +Wondering whether to manage these YAML templates with Terraform instead? See +[docs/cpflow-vs-terraform.md](docs/cpflow-vs-terraform.md) for the trade-offs +and a concrete HCL comparison. + ## Setup and run Check if the Control Plane organization and location are correct in `.controlplane/controlplane.yml`.