Skip to content

Commit 4830aa3

Browse files
authored
feat(cpflow): add ordered renderer workload (#777)
* feat(cpflow): add ordered renderer workload * fix(cpflow): address renderer rollout review * fix(cpflow): satisfy rolling deploy env lint * fix(cpflow): tighten renderer rollout config * fix(cpflow): align renderer cache and cpflow version
1 parent 0b2d0b4 commit 4830aa3

23 files changed

Lines changed: 310 additions & 67 deletions

.controlplane/Dockerfile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ RUN SECRET_KEY_BASE=precompile_placeholder bundle exec rake react_on_rails:gener
8181
# For Kubernetes and ControlPlane, this is the command on the workload.
8282
ENTRYPOINT ["./.controlplane/entrypoint.sh"]
8383

84-
# Run the Pro Node renderer and Rails in a single container (Option 1 from
85-
# docs/oss/building-features/node-renderer/container-deployment.md).
84+
# Default local/single-container command. Control Plane workload templates
85+
# override this with Rails-only and renderer-only commands so deploy_order can
86+
# roll the renderer before Rails.
8687
# `wait -n` exits on first child failure → container exits → Control Plane
87-
# restarts the whole unit. Keeps the existing Thruster HTTP/2 proxy for Rails.
88+
# restarts the whole unit when a workload intentionally uses this default.
8889
CMD ["bash", "-c", "node renderer/node-renderer.js & RENDERER_PID=$! ; bundle exec thrust bin/rails server & RAILS_PID=$! ; wait -n ; exit 1"]

.controlplane/controlplane.yml

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,35 @@ aliases:
2020
default_location: aws-us-east-2
2121
# Configure the workload name used as a template for one-off scripts, like a Heroku one-off dyno.
2222
one_off_workload: rails
23+
24+
# Templates used when bootstrapping review, staging, or production apps.
25+
# Keep node-renderer before rails so first-time setup matches deploy_order.
26+
setup_app_templates:
27+
# GVC template contains the identity and shared environment.
28+
- app
29+
30+
# Resources
31+
- postgres
32+
- redis
33+
34+
# Workloads, like Dyno types on Heroku
35+
- daily-task
36+
- node-renderer
37+
- rails
38+
2339
# Like the entries in the Heroku Procfile that get deployed when the application code changes
2440
# and the application image updates.
2541
app_workloads:
42+
- node-renderer
2643
- rails
2744
- daily-task
45+
46+
# Deploy the separate React on Rails renderer first, wait for it to become
47+
# ready, then deploy Rails and cron workloads that share the same image.
48+
deploy_order:
49+
- [node-renderer]
50+
- [rails, daily-task]
51+
2852
# Additional workloads that are not affected by deploy-image and promote-app-from-upstream
2953
# These workloads apply to the ps commands
3054
additional_workloads:
@@ -54,21 +78,9 @@ apps:
5478
<<: *common
5579
# QA Apps are like Heroku review apps, but they use `prefix` so you can run a command like
5680
# this to create a QA app for the tutorial app.
57-
# `cpflow setup gvc postgres redis rails -a qa-react-webpack-rails-tutorial-1234`
81+
# `cpflow setup-app -a qa-react-webpack-rails-tutorial-1234`
5882
qa-react-webpack-rails-tutorial:
5983
<<: *common
60-
# Order matters!
61-
setup_app_templates:
62-
# GVC template contains the identity
63-
- app
64-
65-
# Resources
66-
- postgres
67-
- redis
68-
69-
# Workloads, like Dynos types on Heroku
70-
- daily-task
71-
- rails
7284
# match_if_app_name_starts_with is used to identify these "qa" apps.
7385
match_if_app_name_starts_with: true
7486
image_retention_days: 5

.controlplane/docs/cpflow-vs-terraform.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,15 @@ resource "cpln_gvc" "app" {
8888
SECRET_KEY_BASE = "placeholder_secret_key_base_for_test_apps_only"
8989
RENDERER_PORT = "3800"
9090
RENDERER_LOG_LEVEL = "info"
91-
RENDERER_WORKERS_COUNT = "2"
92-
RENDERER_URL = "http://localhost:3800"
91+
RENDERER_WORKERS_COUNT = "1"
92+
RENDERER_SERVER_BUNDLE_CACHE_PATH = "/app/renderer/.node-renderer-bundles"
93+
RENDERER_URL = "http://node-renderer.${var.app_name}.cpln.local:3800"
94+
RENDERER_HOST = "0.0.0.0"
95+
ROLLING_DEPLOY_PREVIOUS_URLS = "https://${var.app_name}.cpln.app/react_on_rails_pro/rolling_deploy"
9396
RSC_SUSPENSE_DEMO_DELAY = "true"
9497
# cpln:// secret references are just strings, so they port over verbatim:
9598
RENDERER_PASSWORD = "cpln://secret/${var.app_name}-secrets.RENDERER_PASSWORD"
99+
ROLLING_DEPLOY_TOKEN = "cpln://secret/${var.app_name}-secrets.ROLLING_DEPLOY_TOKEN"
96100
REACT_ON_RAILS_PRO_LICENSE = "cpln://secret/${var.app_name}-secrets.REACT_ON_RAILS_PRO_LICENSE"
97101
}
98102
}

.controlplane/docs/testing-cpflow-github-actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Testing cpflow GitHub Actions Changes
22

33
Generic reusable-workflow behavior belongs upstream in the
4-
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.1.1/docs/ci-automation.md).
4+
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.2.0/docs/ci-automation.md).
55
Use this repo note only as the canary checklist for
66
`react-webpack-rails-tutorial`.
77

@@ -30,7 +30,7 @@ bin/conductor-exec bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/
3030
```
3131

3232
Leave `CPFLOW_VERSION` unset while testing a commit SHA. After the upstream PR
33-
ships in a release tag, repin wrappers to that tag. Use `v5.1.1` for the
33+
ships in a release tag, repin wrappers to that tag. Use `v5.2.0` for the
3434
promotion-hardening and release-runner timeout fixes; use immutable commit SHAs
3535
only for future unreleased upstream PR tests.
3636

.controlplane/readme.md

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ You can see the definition of Postgres and Redis in the `.controlplane/templates
2323

2424
This repo uses the generated `cpflow-*` GitHub Actions wrappers. Keep the
2525
generic behavior documented upstream in the
26-
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.1.1/docs/ci-automation.md);
26+
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.2.0/docs/ci-automation.md);
2727
this section only lists the values that are specific to this app.
2828

2929
### Review Apps and Staging
@@ -124,10 +124,11 @@ The matching Control Plane resources are:
124124
Bootstrap production the same way before the first promotion, using the
125125
production org and production-only secret values. After bootstrap or any
126126
template change, re-apply the persistent production templates so the `rails`
127-
and `daily-task` workloads keep the same secret-backed env names as staging:
127+
`node-renderer`, and `daily-task` workloads keep the same secret-backed env
128+
names as staging:
128129

129130
```sh
130-
cpflow apply-template app postgres redis daily-task rails \
131+
cpflow apply-template app postgres redis daily-task node-renderer rails \
131132
-a react-webpack-rails-tutorial-production \
132133
--org shakacode-open-source-examples-production \
133134
--yes --add-app-identity
@@ -138,12 +139,13 @@ secrets:
138139

139140
- `SECRET_KEY_BASE`
140141
- `RENDERER_PASSWORD`
142+
- `ROLLING_DEPLOY_TOKEN`
141143
- `REACT_ON_RAILS_PRO_LICENSE`
142144

143145
Generate `SECRET_KEY_BASE` with `openssl rand -hex 64` and
144-
`RENDERER_PASSWORD` with `openssl rand -hex 32`. For real production, prefer
145-
managed Postgres and Redis services and update `DATABASE_URL` and `REDIS_URL`
146-
accordingly.
146+
`RENDERER_PASSWORD` and `ROLLING_DEPLOY_TOKEN` with `openssl rand -hex 32`.
147+
For real production, prefer managed Postgres and Redis services and update
148+
`DATABASE_URL` and `REDIS_URL` accordingly.
147149

148150
Review apps run pull request code, so anything mounted through
149151
`cpln://secret/...` can be read by that code after it starts. Keep the
@@ -283,9 +285,13 @@ Thruster is a small, fast HTTP/2 proxy designed for Ruby web applications. It pr
283285

284286
To enable Thruster with HTTP/2 on Control Plane, two configuration changes are required:
285287

286-
#### 1. Dockerfile CMD (`.controlplane/Dockerfile`)
288+
#### 1. Container Startup Command
287289

288-
The Dockerfile must use Thruster to start the Rails server:
290+
The Dockerfile `CMD` is the local and single-container fallback. Control Plane
291+
workload templates can override it with explicit `args:`; this app does that for
292+
both `rails` and `node-renderer`.
293+
294+
The fallback command still uses Thruster to start the Rails server:
289295

290296
```dockerfile
291297
# Use Thruster HTTP/2 proxy for optimized performance
@@ -294,9 +300,9 @@ CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
294300

295301
**Note:** Do NOT use `--early-hints` flag as Thruster handles this automatically.
296302

297-
#### 2. Workload Port Protocol (`.controlplane/templates/rails.yml`)
303+
#### 2. Workload Port Protocol (`.controlplane/templates/rails.yml` and `node-renderer.yml`)
298304

299-
The workload port should remain as HTTP/1.1:
305+
The Rails workload port should remain as HTTP/1.1:
300306

301307
```yaml
302308
ports:
@@ -310,16 +316,25 @@ ports:
310316
- Setting `protocol: http2` causes a protocol mismatch and 502 errors
311317
- Thruster automatically provides HTTP/2 to end users through its TLS termination
312318

319+
The separate `node-renderer` workload is different: React on Rails Pro's Node
320+
Renderer listens with cleartext HTTP/2 (h2c), so its workload port intentionally
321+
uses `protocol: http2`. Keep its probes as `tcpSocket` because HTTP/1.1-only
322+
health probes cannot speak to that listener.
323+
313324
### Important: Dockerfile vs Procfile
314325

315326
**On Heroku:** The `Procfile` defines how dynos start:
316327
```
317328
web: bundle exec thrust bin/rails server
318329
```
319330
320-
**On Control Plane/Kubernetes:** The `Dockerfile CMD` defines how containers start. The Procfile is ignored.
331+
**On Control Plane/Kubernetes:** workload `args:` define how `rails` and
332+
`node-renderer` start. The Dockerfile `CMD` is the fallback for local or
333+
single-container runs. The Procfile is ignored.
321334
322-
This is a common source of confusion when migrating from Heroku. Always ensure your Dockerfile CMD matches your intended startup command.
335+
This is a common source of confusion when migrating from Heroku. For deployed
336+
workloads, update the matching template in `.controlplane/templates/`; keep the
337+
Dockerfile fallback aligned for local and single-container use.
323338
324339
### Verifying HTTP/2 is Enabled
325340
@@ -567,17 +582,60 @@ The system uses Control Plane's infrastructure to manage these deployments, with
567582
each review app getting its own resources as defined in the controlplane.yml
568583
configuration.
569584

585+
### Separate React on Rails renderer workload
586+
587+
Control Plane runs the React on Rails Pro Node Renderer as its own
588+
`node-renderer` workload. `.controlplane/controlplane.yml` includes
589+
`node-renderer` in `app_workloads` and sets `deploy_order` so
590+
`cpflow deploy-image` and `cpflow promote-app-from-upstream` deploy the
591+
renderer first, wait for it to become healthy, and then deploy `rails` plus
592+
`daily-task`.
593+
594+
The renderer workload uses the same application image as Rails, runs
595+
`react_on_rails_pro:pre_seed_renderer_cache` at container boot, and then starts
596+
`yarn node-renderer`. Rails gets `RENDERER_URL` from
597+
`.controlplane/templates/app.yml` and reaches the renderer at
598+
`http://node-renderer.<app>.cpln.local:3800`.
599+
600+
`ROLLING_DEPLOY_TOKEN` lives in the app secret dictionary alongside
601+
`RENDERER_PASSWORD`. `ROLLING_DEPLOY_PREVIOUS_URLS` points at the app's
602+
Control Plane rolling-deploy endpoint so the boot seed can pull the draining
603+
bundle before Rails rolls.
604+
605+
### Coordinating existing staging and production apps
606+
607+
For existing persistent apps, roll the topology change separately from the new
608+
React on Rails RC when possible:
609+
610+
1. Populate `ROLLING_DEPLOY_TOKEN` in the staging and production app secret
611+
dictionaries. Use the same value for Rails and the renderer within each app.
612+
2. Re-apply templates to staging first:
613+
`cpflow apply-template app postgres redis daily-task node-renderer rails -a react-webpack-rails-tutorial-staging --org shakacode-open-source-examples-staging --yes --add-app-identity`.
614+
3. Deploy staging with `cpflow deploy-image -a react-webpack-rails-tutorial-staging --org shakacode-open-source-examples-staging`.
615+
cpflow 5.2.0 honors `deploy_order`, so `node-renderer` deploys and waits
616+
before `rails`.
617+
4. After staging is healthy, re-apply the same template list to production.
618+
5. Promote production with the generated workflow. Production promotion also
619+
honors `deploy_order`, so the renderer rolls before Rails.
620+
621+
Do not apply the production template change until the production app secret
622+
dictionary has `ROLLING_DEPLOY_TOKEN`; the app template references that secret
623+
for both Rails and the renderer.
624+
625+
React on Rails docs reference:
626+
<https://reactonrails.com/docs/pro/rolling-deploy-adapters/#deploy-the-renderer-before-rails>
627+
570628

571629
### Updating Generated cpflow Workflows
572630

573631
Keep the reusable-workflow mechanics in the upstream
574-
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.1.1/docs/ci-automation.md).
632+
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.2.0/docs/ci-automation.md).
575633
For this repo, the update loop is:
576634

577635
1. Update the bundled `cpflow` gem to the desired release.
578636
2. Refresh generated wrappers from that release with `--staging-branch master`.
579637
3. Keep generated refs on the same release tag as the bundled `cpflow` gem.
580-
This branch pins refs to `v5.1.1`, which includes upstream promotion
638+
This branch pins refs to `v5.2.0`, which includes upstream promotion
581639
hardening and the release-runner timeout fix. Use a full commit SHA only for
582640
short-lived upstream testing and leave `CPFLOW_VERSION` unset in that case.
583641
4. Keep app names and GitHub settings aligned with `.controlplane/controlplane.yml`.

.controlplane/shakacode-team.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
Deployments are handled by Control Plane configuration in this repo and GitHub Actions.
66

7+
### Renderer Workload
8+
- `node-renderer` is an app workload and deploys before `rails` through `deploy_order`.
9+
- `node-renderer` runs the React on Rails Pro boot seed before `yarn node-renderer` so the new renderer cache is warm before Rails rolls.
10+
- Rails reaches the renderer through `RENDERER_URL=http://node-renderer.<app>.cpln.local:3800`.
11+
- Keep `ROLLING_DEPLOY_TOKEN` populated in the app secret dictionary; Rails and the renderer use it for rolling-deploy bundle pulls.
12+
- For existing staging/production apps, populate `ROLLING_DEPLOY_TOKEN`, apply templates with `node-renderer`, validate staging, then promote production. Do not combine the production template cutover with missing secrets.
13+
714
### Review Apps
815
- Add a comment `+review-app-deploy` to any PR to deploy a review app
916
- Leave `REVIEW_APP_PREFIX` unset for the standard path. The workflow infers
@@ -19,8 +26,8 @@ Deployments are handled by Control Plane configuration in this repo and GitHub A
1926
in this repository.
2027
- Review apps run pull request code. Keep `CPLN_TOKEN_STAGING`,
2128
`qa-react-webpack-rails-tutorial-secrets`, database credentials, renderer
22-
credentials, and license values limited to review/staging use. Never mount
23-
production secrets into review apps.
29+
credentials, rolling-deploy tokens, and license values limited to
30+
review/staging use. Never mount production secrets into review apps.
2431

2532
### Staging Environment
2633
- **Automatic**: Any merge to the `master` branch automatically deploys to staging
@@ -57,6 +64,10 @@ so those values do not need to be set just to test review apps. Set them only
5764
when testing a fork or clone against a different Control Plane org, review-app
5865
prefix, or public workload.
5966

67+
App secret dictionaries for review, staging, and production must include
68+
`SECRET_KEY_BASE`, `RENDERER_PASSWORD`, `ROLLING_DEPLOY_TOKEN`, and
69+
`REACT_ON_RAILS_PRO_LICENSE`.
70+
6071
Production promotion uses a protected GitHub Environment named `production`:
6172

6273
- Environment secret `CPLN_TOKEN_PRODUCTION`
@@ -104,20 +115,20 @@ Use `setup-app` for first-time bootstrap because it creates the app secret
104115
policy and identity binding. Use `cpflow apply-template` for later template
105116
updates to existing persistent apps. Production promotion compares both GVC env
106117
names and app workload container env names against staging before copying the
107-
image, so keep production `rails` and `daily-task` env references in sync with
108-
the templates:
118+
image, so keep production `rails`, `node-renderer`, and `daily-task` env
119+
references in sync with the templates:
109120

110121
```sh
111-
cpflow apply-template app postgres redis daily-task rails \
122+
cpflow apply-template app postgres redis daily-task node-renderer rails \
112123
-a react-webpack-rails-tutorial-production \
113124
--org shakacode-open-source-examples-production \
114125
--yes --add-app-identity
115126
```
116127

117128
Advanced optional settings are documented upstream in the
118-
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.1.1/docs/ci-automation.md).
129+
[`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.2.0/docs/ci-automation.md).
119130

120-
Current workflow wrappers pin `control-plane-flow` release tag `v5.1.1`, which
131+
Current workflow wrappers pin `control-plane-flow` release tag `v5.2.0`, which
121132
includes promotion hardening and the release-runner timeout fix. Keep release
122133
tags as the steady-state configuration; use a full commit SHA only for
123134
short-lived upstream testing and leave `CPFLOW_VERSION` unset in that case.

.controlplane/templates/app.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,26 @@ spec:
2424
# set to a secure random value using: openssl rand -hex 64
2525
# Production apps should configure this manually after app creation via a secret.
2626
value: 'placeholder_secret_key_base_for_test_apps_only'
27-
# Pro Node renderer settings. The renderer process runs in the same
28-
# container as Rails (see .controlplane/Dockerfile CMD).
27+
# Pro Node renderer settings. Control Plane runs the renderer in the
28+
# separate node-renderer workload and Rails reaches it over same-GVC DNS.
2929
- name: RENDERER_PORT
3030
value: '3800'
3131
- name: RENDERER_LOG_LEVEL
3232
value: info
3333
- name: RENDERER_WORKERS_COUNT
34-
value: '2'
34+
# One worker keeps the separate tutorial renderer at minimum baseline cost.
35+
value: '1'
36+
- name: RENDERER_SERVER_BUNDLE_CACHE_PATH
37+
# Shared by the boot-seed rake task and the Node renderer process.
38+
value: /app/renderer/.node-renderer-bundles
3539
- name: RENDERER_URL
36-
value: http://localhost:3800
40+
# Internal same-GVC endpoint for the separate React on Rails Pro Node Renderer workload.
41+
value: http://node-renderer.{{APP_NAME}}.cpln.local:3800
42+
- name: RENDERER_HOST
43+
value: 0.0.0.0
44+
- name: ROLLING_DEPLOY_PREVIOUS_URLS
45+
# The renderer boot seed pulls the still-live bundle manifest before Rails rolls.
46+
value: https://{{APP_NAME}}.cpln.app/react_on_rails_pro/rolling_deploy
3747
# Enable the artificial Suspense demo delay so the streaming fallback is
3848
# visible on the review-app. Off by default in production deployments.
3949
- name: RSC_SUSPENSE_DEMO_DELAY
@@ -46,6 +56,8 @@ spec:
4656
# pull request code, so every mounted secret must be review-safe.
4757
- name: RENDERER_PASSWORD
4858
value: cpln://secret/{{APP_SECRETS}}.RENDERER_PASSWORD
59+
- name: ROLLING_DEPLOY_TOKEN
60+
value: cpln://secret/{{APP_SECRETS}}.ROLLING_DEPLOY_TOKEN
4961
- name: REACT_ON_RAILS_PRO_LICENSE
5062
value: cpln://secret/{{APP_SECRETS}}.REACT_ON_RAILS_PRO_LICENSE
5163
# Part of standard configuration

0 commit comments

Comments
 (0)