-
Notifications
You must be signed in to change notification settings - Fork 373
docs: add cpflow vs. Terraform guidance for Control Plane #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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" } | ||||||
|
justin808 marked this conversation as resolved.
Outdated
|
||||||
| 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. | ||||||
|
justin808 marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| # 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 = { | ||||||
|
justin808 marked this conversation as resolved.
|
||||||
| DATABASE_URL = "postgres://the_user:the_password@postgres.${var.app_name}.cpln.local:5432/${var.app_name}" | ||||||
|
justin808 marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
Or, if a connection-string form is needed to illustrate the URL shape, use sensitive variables: variable "db_user" { type = string; sensitive = true }
variable "db_password" { type = string; sensitive = true }
# ...
DATABASE_URL = "postgres://${var.db_user}:${var.db_password}@postgres.${var.app_name}.cpln.local:5432/${var.app_name}"Either way, the tutorial teaches the safer pattern rather than asking readers to remember not to copy the placeholder. |
||||||
| 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" | ||||||
|
justin808 marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If the intent is specifically to illustrate that TF can take a sensitive variable, a variable "secret_key_base" { type = string; sensitive = true }
# ...
SECRET_KEY_BASE = var.secret_key_baseEither removes the literal placeholder from the primary code path while still making the mapping clear. |
||||||
| 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" | ||||||
|
justin808 marked this conversation as resolved.
Outdated
justin808 marked this conversation as resolved.
Outdated
|
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| 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. | ||||||
Uh oh!
There was an error while loading. Please reload this page.