-
Notifications
You must be signed in to change notification settings - Fork 374
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
base: master
Are you sure you want to change the base?
Changes from all commits
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" } | ||||||||||
| 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" | ||||||||||
|
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. This is the one spot in the appendix that could teach a bad pattern. Hardcoding a secret value in a GVC Two lines below,
Suggested change
Even for a tutorial placeholder, showing the |
||||||||||
| 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" | ||||||||||
|
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
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! 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. HCL port numbers are integers, not strings — the
Suggested change
Passing a string may cause a type-mismatch error on |
||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| 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. | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typeanddefaulton the same line without either separator is a parse error thatterraform validatewill reject. A reader who copies this snippet verbatim will get an immediate failure.