Skip to content

Commit 86f315a

Browse files
justin808claude
andauthored
docs: add cpflow vs. Terraform guidance for Control Plane (#751)
* docs: add cpflow vs. Terraform guidance for Control Plane Readers see the official cpln Terraform provider and ask whether the .controlplane/templates/*.yml files should be Terraform instead. Add a focused doc that answers it: the YAML covers two concerns (cpflow's own controlplane.yml orchestration config vs. cpln apply resource manifests), and Terraform only addresses the latter. Includes a decision rule and a concrete HCL mapping of app.yml + rails.yml to cpln_gvc/cpln_workload so the trade-offs (per-PR interpolation, deploy-time image injection, provision-vs-deploy separation) are visible. Linked from .controlplane/readme.md's Project Configuration section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: fix HCL snippet issues in cpflow vs. Terraform appendix Address PR review feedback on the Terraform appendix: - Split the multi-attribute `variable "location"` block onto separate lines (two attributes on one line is an HCL2 parse error). - Use an integer port `number = 3000` instead of the string `"3000"`, matching templates/rails.yml and the cpln_workload schema. - Note that the SECRET_KEY_BASE placeholder is test-only and that production should use a sensitive var or cpln://secret ref. - Flag the 0.0.0.0/0 firewall CIDRs as tutorial defaults to tighten in production. - Point readers at the cpln provider docs for the remaining resource field mappings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: address second-pass review on cpflow vs. Terraform appendix Apply the optional improvements from the second claude[bot] review of the Terraform HCL appendix (#751): - DATABASE_URL: add a production-guidance comment (sensitive var or cpln://secret ref) so the placeholder credentials aren't cargo-culted, matching the note already on SECRET_KEY_BASE. - image_link: clarify that an image bump is a scoped `terraform apply` (plan + state lock), not a "full apply" -- Terraform only diffs the changed argument. Fixed the same overstatement in the closing paragraph. - Expand the single-line `app_name`/`image_link` variable blocks to the idiomatic multi-line form, consistent with `location`. The reviewer's env-must-be-blocks claim was not applied: the official cpln provider docs type container `env` as a Map of String and use the `env = { ... }` map literal in every example, so the doc is already correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b270bd commit 86f315a

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# cpflow vs. Terraform: which manages what?
2+
3+
Control Plane ships an official [Terraform provider](https://registry.terraform.io/providers/controlplane-com/cpln/latest),
4+
so it is reasonable to ask whether the `.controlplane/templates/*.yml` files
5+
should be Terraform instead. The short answer for this app: **no**, because the
6+
YAML here covers two concerns and Terraform only addresses one of them.
7+
8+
## Two kinds of YAML in `.controlplane/`
9+
10+
1. **`controlplane.yml`** — cpflow's own config: app/org aliases,
11+
`app_workloads`, `release_script`, `upstream` (staging→production
12+
promotion), `match_if_app_name_starts_with`, image retention. **This has no
13+
Terraform equivalent.** It drives a Heroku-style deploy workflow, not
14+
infrastructure state.
15+
2. **`templates/*.yml`** — native `cpln apply` resource manifests (GVC,
16+
workloads, secrets, policies, identities, volume sets). These *could* be
17+
rewritten as `cpln_*` Terraform resources.
18+
19+
## What Terraform does not replace
20+
21+
The app lifecycle that is cpflow's whole reason to exist:
22+
23+
- `build-image` / `deploy-image` with sequential image tags
24+
- release-phase migrations (`cpflow run --image latest -- rails db:migrate`)
25+
- staging → production promotion
26+
- **ephemeral per-PR review apps** (the `+review-app-deploy` flow + prefix
27+
matching)
28+
29+
Modeling per-PR environments in Terraform means a workspace and state file per
30+
PR — far heavier than `cpflow setup-app -a qa-react-webpack-rails-tutorial-1234`.
31+
32+
## Decision rule
33+
34+
| Concern | Use |
35+
| --- | --- |
36+
| App build / deploy / release-phase migrations | **cpflow** |
37+
| Ephemeral per-PR review apps | **cpflow** |
38+
| Staging → production promotion | **cpflow** |
39+
| Durable shared infra: orgs, custom domains, mk8s, agents, IAM/policies, external RDS/ElastiCache | **Terraform** (`cpln` provider) |
40+
41+
For a real production system, the two are complementary — Terraform for durable
42+
shared infrastructure, cpflow for the app deploy loop (Control Plane even
43+
publishes a
44+
[GitHub Actions Terraform example](https://github.com/controlplane-com/github-actions-example-terraform)).
45+
This tutorial app has essentially no durable shared infra and exists to
46+
demonstrate the cpflow deploy loop, so it stays entirely on cpflow.
47+
48+
## Appendix: what the templates would look like as HCL
49+
50+
A side-by-side mapping of the current cpflow templates (`templates/app.yml` +
51+
`templates/rails.yml`) to the [`controlplane-com/cpln`](https://registry.terraform.io/providers/controlplane-com/cpln/latest)
52+
provider. The interesting parts are in the comments — they are where the two
53+
models actually diverge.
54+
55+
```hcl
56+
# variables.tf
57+
variable "app_name" { # cpflow injects {{APP_NAME}} per review app;
58+
type = string # in TF this is a -var or a workspace name.
59+
}
60+
variable "location" {
61+
type = string
62+
default = "aws-us-east-2"
63+
}
64+
# cpflow's {{APP_IMAGE_LINK}} is set at *deploy* time by `deploy-image`. In TF the image
65+
# is a plain argument, so each image bump is its own `terraform apply` (plan + state lock)
66+
# rather than a one-shot `deploy-image` call.
67+
variable "image_link" {
68+
type = string
69+
}
70+
71+
# gvc.tf — was templates/app.yml (kind: gvc + kind: identity)
72+
resource "cpln_gvc" "app" {
73+
name = var.app_name
74+
locations = [var.location] # was staticPlacement.locationLinks: [{{APP_LOCATION_LINK}}]
75+
76+
# was spec.env: (a list of {name,value}); in TF it is a flat map.
77+
env = {
78+
# Placeholder credentials mirror templates/app.yml; in production set DATABASE_URL via a
79+
# sensitive var or a cpln://secret ref (like RENDERER_PASSWORD below) — never embed a real password.
80+
DATABASE_URL = "postgres://the_user:the_password@postgres.${var.app_name}.cpln.local:5432/${var.app_name}"
81+
REDIS_URL = "redis://redis.${var.app_name}.cpln.local:6379"
82+
RAILS_ENV = "production"
83+
NODE_ENV = "production"
84+
RAILS_SERVE_STATIC_FILES = "true"
85+
# Placeholder is fine for test apps (matches templates/app.yml); in production set
86+
# SECRET_KEY_BASE via a sensitive var or a cpln://secret ref (see the two lines
87+
# below) — never commit a literal secret to an env map.
88+
SECRET_KEY_BASE = "placeholder_secret_key_base_for_test_apps_only"
89+
RENDERER_PORT = "3800"
90+
RENDERER_LOG_LEVEL = "info"
91+
RENDERER_WORKERS_COUNT = "2"
92+
RENDERER_URL = "http://localhost:3800"
93+
RSC_SUSPENSE_DEMO_DELAY = "true"
94+
# cpln:// secret references are just strings, so they port over verbatim:
95+
RENDERER_PASSWORD = "cpln://secret/${var.app_name}-secrets.RENDERER_PASSWORD"
96+
REACT_ON_RAILS_PRO_LICENSE = "cpln://secret/${var.app_name}-secrets.REACT_ON_RAILS_PRO_LICENSE"
97+
}
98+
}
99+
100+
resource "cpln_identity" "app" { # was the second doc in app.yml (kind: identity)
101+
gvc = cpln_gvc.app.name
102+
name = "${var.app_name}-identity"
103+
}
104+
105+
# rails.tf — was templates/rails.yml (kind: workload)
106+
resource "cpln_workload" "rails" {
107+
gvc = cpln_gvc.app.name
108+
name = "rails"
109+
type = "standard"
110+
identity_link = cpln_identity.app.self_link # was identityLink: {{APP_IDENTITY_LINK}}
111+
112+
container {
113+
name = "rails"
114+
image = var.image_link # was {{APP_IMAGE_LINK}}
115+
cpu = "300m"
116+
memory = "1Gi"
117+
inherit_env = true # pulls the GVC env above
118+
env = { LOG_LEVEL = "debug" }
119+
120+
ports {
121+
protocol = "http" # keep http — Thruster does HTTP/2 on the TLS frontend
122+
number = 3000 # an integer, not a string (matches templates/rails.yml)
123+
}
124+
}
125+
126+
options {
127+
capacity_ai = true
128+
autoscaling {
129+
max_scale = 1 # maxScale 1 ≈ a single Heroku dyno (other fields default)
130+
}
131+
}
132+
133+
firewall_spec {
134+
external {
135+
inbound_allow_cidr = ["0.0.0.0/0"] # mirrors templates/rails.yml; tighten for production
136+
outbound_allow_cidr = ["0.0.0.0/0"] # likewise — scope outbound to known egress in real apps
137+
}
138+
}
139+
}
140+
```
141+
142+
`templates/postgres.yml`, `templates/redis.yml`, and `templates/daily-task.yml`
143+
follow the same pattern (`cpln_workload` + `cpln_secret` + `cpln_policy` +
144+
`cpln_volumeset`). The mechanical translation is straightforward — the field
145+
names line up almost 1:1; see the
146+
[`cpln` provider docs](https://registry.terraform.io/providers/controlplane-com/cpln/latest/docs)
147+
for the exact attribute names of each resource.
148+
149+
What the comments are really showing: the three things cpflow gives you for free
150+
(`{{APP_NAME}}` per-PR interpolation, deploy-time `{{APP_IMAGE_LINK}}`
151+
injection, and the implicit "provision ≠ deploy" separation) all become *your*
152+
problem in Terraform. The image being a plain argument is the big one: in
153+
cpflow, `deploy-image` bumps the running tag without touching infra; in
154+
Terraform, a new image means a `terraform apply` — only that one argument changes,
155+
but the full plan / state-lock cycle still runs. And none of `controlplane.yml`
156+
(release script, upstream promotion, review-app prefix matching) has any
157+
representation above at all.

.controlplane/readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ These YAML files are the same as used by the `cpln apply` command.
195195
3. `Dockerfile`: defines the Docker image used to run the app on Control Plane.
196196
4. `entrypoint.sh`: defines the entrypoint script used to run the app on Control Plane.
197197

198+
Wondering whether to manage these YAML templates with Terraform instead? See
199+
[docs/cpflow-vs-terraform.md](docs/cpflow-vs-terraform.md) for the trade-offs
200+
and a concrete HCL comparison.
201+
198202
## Setup and run
199203

200204
Check if the Control Plane organization and location are correct in `.controlplane/controlplane.yml`.

0 commit comments

Comments
 (0)