Preview environments¶
Ephemeral, per-PR deployments of the webapp on Scaleway, provisioned with OpenTofu/Terragrunt from CI.
How to use¶
Add the
previewlabel to a pull request.preview-up.ymlbuilds the webapp Docker image, applies the terragrunt stack and posts a sticky comment on the PR with the environment URL.Every push to the PR rebuilds the image and redeploys the same environment (same URL).
The environment is destroyed when the PR is closed or the
previewlabel is removed. A nightly cron also reaps any preview older than 7 days as a safety net.
Decisions¶
Topic |
Decision |
Rationale |
|---|---|---|
Env keying |
PR number ( |
Stable URL across pushes, one env per PR, destroyed on close. |
Trigger |
Label-gated ( |
Each up run builds a Docker image and seeds a DB: real cost, several minutes. Labeling opts a PR in. |
Hostname |
Scaleway generated domain |
No DNS to manage. The hostname is unknown before apply, so the container runs with |
Container namespace |
Dedicated |
Isolation from preprod; the cleanup cron can list it exhaustively. |
DB seeding |
|
Realistic data on the carte from the preprod sample database. |
Cleanup TTL |
7 days (nightly cron) |
With PR keying + destroy-on-close the cron is only a safety net for missed destroys. A shorter TTL would kill envs of still-open PRs and dead URLs in their PR comment. |
Architecture¶
PR #123 labeled "preview"
│
▼
┌─ preview-up.yml ─────────────────────────────────────────────┐
│ 1. build webapp image → rg.fr-par.scw.cloud/ns-qfdmo/ │
│ webapp:pr-123-<shortsha> │
│ 2. terragrunt run-all apply │
│ infrastructure/environments/preview/pr-123/ │
│ (materialised from _template at runtime) │
│ 3. sticky PR comment with the URL │
└──────────────────────────────────────────────────────────────┘
Per-PR resources (state: lvao-terraform-state/preview/pr-<n>/…):
├── database DB + user + privilege on the EXISTING preprod
│ RDB instance, PostGIS extensions, seeded from
│ the sample DB
├── object_storage throwaway media bucket, force_destroy,
│ 1-day object expiry; container reuses the
│ project-wide SCW key (no bucket-scoped IAM key,
│ see Limitations)
└── container serverless container in the shared
qfdmod-preview namespace, min_scale=0,
tagged preview / preview-pr-<n> /
created-at-<unix>
Shared (one-time):
└── qfdmod-preview container namespace
(infrastructure/environments/preview/namespace)
The image tag embeds the PR head SHA (pr-<n>-<shortsha>) so each push
changes the container’s registry_image, which is what forces Scaleway to
redeploy.
State keys and the environment terragrunt input (pr-<n>) derive from
the materialised directory path through root.hcl — the per-PR stacks
carry no backend overrides.
Teardown paths, all converging on terragrunt run-all destroy plus state
object deletion:
PR closed or
previewlabel removed →preview-down.ymlNightly cron (
preview-cleanup.yml) dispatchespreview-down.ymlfor anything whosecreated-at-<unix>tag is older than 7 daysManual
workflow_dispatchofpreview-down.ymlwith a PR number
Old pr-* image tags are reaped by the existing weekly
scaleway_container_registry_delete_old_tags.yml job (keeps the 30 most
recent tags per image).
Bootstrap (one-time setup)¶
Everything is automated by infrastructure/Makefile. With the Scaleway
credentials exported in your shell (or in the repo root .env):
export SCW_ACCESS_KEY=… # able to manage RDB databases/users,
export SCW_SECRET_KEY=… # object storage, IAM and containers
export SCW_DEFAULT_PROJECT_ID=…
export SCW_DEFAULT_ORGANIZATION_ID=…
export SCALEWAY_DOCKER_SECRET=… # registry push credential
export SAMPLE_DB_URI=… # postgres URI of the preprod sample DB
# (pg_dump source), reachable from CI
make -C infrastructure preview-bootstrap
This runs three idempotent steps, also callable individually:
preview-github-env— creates the GitHubpreviewenvironment and pushes the secrets above, plusPREVIEW_SECRET_KEY(the shared DjangoSECRET_KEYfor previews, generated withopenssl randunless already set in your shell).preview-namespace—terragrunt applyof the sharedqfdmod-previewcontainer namespace (interactive: review the plan before approving).preview-label— creates thepreviewlabel on the repository.
Validation checklist¶
On a test PR:
[ ] Labeling creates the env; the sticky comment links a working URL
[ ] Migrations ran, sample data visible on the carte,
/healthzgreen[ ] A second push updates the same env, same URL
[ ] Removing the label (or closing the PR) destroys all three stacks
[ ] State objects gone from
lvao-terraform-state/preview/pr-<n>/[ ] Cron dry-run (
preview-cleanup.ymlwithdry_run=true) lists an artificially aged container as stale
Limitations / out of scope¶
Webapp only: no Airflow/data-platform previews, no warehouse database (the entrypoint skips
create_remote_db_serverwhenDB_WAREHOUSEis unset).Production deployment is unchanged (Scalingo); the Docker image is used by previews only for now.
Served on the Scaleway-generated domain; no custom URLs.
No bucket-scoped IAM key for the media bucket: the CI Terraform credentials (
SCW_ACCESS_KEY/SCW_SECRET_KEY) lack IAM write permission, so the container reuses those project-wide credentials for S3 access instead of a bucket-scoped key. Any preview container can therefore reach every bucket in the project, not just its own. Revisit once IAM write access is granted (see TODO below) — re-addscaleway_iam_application/scaleway_iam_policy/scaleway_iam_api_keyinpreview_object_storage, scoped to the bucket.
TODO¶
Separate Scaleway project for previews: currently all preview resources (containers, databases, buckets) are created in the same Scaleway project as preprod. A dedicated preview project would provide billing isolation and quota isolation. To implement: create a
previewScaleway project, addSCW_PREVIEW_PROJECT_IDas a GitHub secret in thepreviewenvironment, and update_terragrunt-apply.ymlto pass it asTF_VAR_project_id.Grant IAM write permission to the CI Scaleway key: needed to restore bucket-scoped IAM credentials per preview (see Limitations above). Without it, the IAM isolation between previews and prod/preprod buckets is weaker than intended.