Skip to content

feat(scaleway): the Scaleway foundations OpenTofu module - #20

Draft
UlysseCarpentier wants to merge 4 commits into
docs/scaleway-crossplane-iacfrom
feat/scaleway-foundations-module
Draft

UlysseCarpentier wants to merge 4 commits into
docs/scaleway-crossplane-iacfrom
feat/scaleway-foundations-module

Conversation

@UlysseCarpentier

@UlysseCarpentier UlysseCarpentier commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

The Scaleway sprint deliverable — closes #4. One flat root module at opentofu/scaleway/: VPC, Private Network, Public Gateways, the Kapsule cluster, its pools and the identities the Flux-pulled socle needs.

Stacked on #18, which is itself stacked on #17, so this PR's diff shows only the module. The research it implements lives in those two. GitHub retargets as each merges.

Verified locally

Check Result
tofu fmt -recursive clean
tofu validate clean, no warnings
tofu test 37 passed, 0 failed — 5 defaults runs, 32 validation runs against 32 validation blocks
terraform-docs --output-check up to date, module and example
tofu plan, real Scaleway Project 22 to add, 0 to change, 0 to destroy
Module carries no lock file confirmed
Example carries one confirmed

TFLint and Trivy run in CI. integration.yaml's Scaleway leg is plan-only — Scaleway publishes no emulator, which the conformance checklist already records as an exception.

Research → code

Every default traces to a document. Two findings are enforced in code rather than merely written down:

  • expander = "least_waste", because Scaleway ships random, which picks which pool to grow by coin flip — and multi-AZ always means more than one pool.
  • A validation refuses a node type whose generation does not exist in the chosen zones. The Zen 5 ranges (COMPUTE3/BASIC3) and POP2 never share an Availability Zone, so this catches at plan time what would otherwise fail midway through building a pool. Two test cases cover both directions.

Enforced, not exposed: Cilium, full isolation, one gateway per zone, one security group and one placement group per zone. Required with no default: kubernetes_version, maintenance_window, cluster_endpoint_public_access_cidrs (which rejects 0.0.0.0/0), crossplane_permission_sets. Derived: the control plane offer, dedicated in production and mutualized elsewhere, with a precondition that refuses a production cluster on the free one.

Two exceptions this module declares rather than hides

It issues a credential. Every other foundations module refuses to, and the checklist says modules authenticate through ambient credentials or workload identity federation. Scaleway has neither, so an in-cluster Crossplane provider can only hold a long-lived API key. Mitigations: scoped to one Project, bound to the gateways' egress addresses by an IAM request.ip condition, optional expiry, sensitive output. oidc_issuer_url and workload_identity_pool are returned as null rather than omitted, so one output surface holds across four clouds.

There is no TFLint ruleset for Scaleway — not from terraform-linters, not from anyone. .tflint.hcl carries the terraform preset alone and the module leans on tofu test.

Prerequisites

docs/scaleway/prerequisites.md, matching the AWS and GCP module PRs. Two of its sections have no counterpart on the other clouds:

  • There is nothing to enable. Scaleway has no API-enablement step, so GCP's whole "enable these services" section has no equivalent. What replaces it as the thing that blocks a first apply is quota — per instance type, below what a production socle needs, with no published figure at all for the Zen 5 generation, and raised only by support ticket. Identity validation gates it in turn.
  • "No keys, ever" is not available here. There is no workload identity federation and no OIDC trust for CI, so the runner holds a long-lived key just as the in-cluster Crossplane provider does. The document gives the two mitigations that exist: an IP condition on the policy, and an expiry on the key.

It also fixes two permission set names the example had wrong: VPCFullAccess does not cover Private Networks, and PublicGatewaysFullAccess does not exist. The real names are PrivateNetworksFullAccess and VPCGatewayFullAccess.

Review notes

  • Cross-cloud naming (additional_tags, create_vpc, vpc_id, kubernetes_version, cluster_endpoint_public_access_cidrs) follows the AWS module. That check only really passes once feat(gcp): the GCP foundations OpenTofu module #11 and feat(aws): the AWS foundations OpenTofu module #16 merge.
  • delete_additional_resources defaults to false, keeping a client's volumes and load balancers when a cluster is torn down, at the price of orphaned billable resources. The opposite default is defensible; this is the one I would most like a second opinion on.
  • Quota is an onboarding blocker the module cannot solve — documented in both READMEs.

Acceptance criteria (#4)

  • Conformance checklist: fmt/validate, tests, docs, structure, bounded provider range, no backend block, no lock in the module
  • TFLint and security scan — run in CI on this PR
  • Clean tofu plan on the minimal example against a test account — needs credentials and a quota raise
  • Research recommendations traceable to a variable, default or constraint
  • Reviewed and approved by a maintainer

🤖 Generated with Claude Code

UlysseCarpentier and others added 4 commits September 14, 2026 14:47
The sprint deliverable. VPC, Private Network, Public Gateways, the Kapsule
cluster, its pools and the identities the Flux-pulled socle needs, in one
flat root module built to the conformance checklist.

Every default traces to a research document. Cilium and full isolation are
enforced rather than exposed. The control plane offer derives from
environment — dedicated in production for the SLA and the audit log,
mutualized elsewhere — and a precondition refuses a production cluster on
the free one. The Kubernetes version, the maintenance window and the
allowed-IP list are required with no default, the last one rejecting
0.0.0.0/0 outright since the control plane cannot be made private.

Two findings from the research are enforced in code rather than written
down. The autoscaler expander defaults to least_waste because Scaleway
ships random, which picks a pool to grow by coin flip once there is more
than one — and multi-AZ always means more than one. And a validation
refuses a node type whose generation does not exist in the chosen zones:
the Zen 5 ranges and POP2 never share an Availability Zone, so this catches
at plan time what would otherwise fail while building a pool.

Two exceptions this module declares rather than hides. It issues a
credential, which every other foundations module refuses to do, because
Scaleway has no workload identity federation and an in-cluster Crossplane
provider can hold nothing else; the mitigations are a one-Project scope, an
IAM condition binding the key to the gateways' egress, and an optional
expiry. And there is no TFLint ruleset for Scaleway from anyone, so the
module leans on tofu test instead.

One Public Gateway per zone the pools span, because the gateway is zoned
with no HA of its own. One security group and one placement group per zone
for the same reason.

Verified locally: tofu fmt, tofu validate, 38 tofu test cases passing, and
terraform-docs --output-check clean on the module and the example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow warned about this itself: "Scaleway has no emulator yet — its
apply runs offline, which is only meaningful while the module holds no
resources." The module now holds resources, so that stopped being true the
moment it landed, and the leg failed.

Give every leg an explicit mode. AWS, GCP and Azure apply against their
floci emulator and destroy afterwards. Scaleway runs `tofu test`, which is
plan-only: it exercises the module under fixture credentials, asserting the
recommended defaults resolve as intended and that every validation block
rejects what it should.

The conformance checklist asks that each leg declare which it does and that
legs which cannot apply say why in the run summary. Both are now true, and
the summary states plainly that convergence is not proven — a disposable
Scaleway Project, or floci support for Scaleway, is what that would take.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AWS and GCP module PRs each carry a docs/<cloud>/prerequisites.md. This
is the Scaleway one, and two of its sections have no equivalent on the other
clouds.

There is nothing to enable: Scaleway has no API-enablement step, so the
whole "enable these services" section of the GCP document has no counterpart
here. What replaces it as the thing that blocks a first apply is quota —
per instance type, below what a production socle needs, with no published
figure at all for the Zen 5 generation, and raised only by support ticket.
Identity validation gates it in turn: without it most production instance
types have no quota whatsoever.

And the credentials section cannot say what GCP's says. "No keys, ever" is
not an available position on Scaleway: there is no workload identity
federation and no OIDC trust for CI, so the runner holds a long-lived key
just as the in-cluster Crossplane provider does. The document gives the two
mitigations that do exist — an IP condition on the policy, and an expiry on
the key.

Also correct the minimal example, which named two permission sets that do
not exist: VPCFullAccess does not cover Private Networks, and there is no
PublicGatewaysFullAccess. The real names are PrivateNetworksFullAccess and
VPCGatewayFullAccess, verified against Scaleway's permission sets reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tofu test` plans the module as a root module. It never touches the example,
which is the surface a consumer actually calls — its variables, its wiring
and its outputs go unexercised.

So plan it as well, in the same leg. The credentials are fixtures and the
state is empty, so the plan creates nothing and reads nothing back: no API
call leaves the runner, and forks can run it, exactly like the emulator
legs. Verified to produce the same 22 resources a real Scaleway Project does.

The env block is scoped to the step rather than shared with the job, so the
AWS leg's own TF_VAR_* additions land without a conflict.

This lines Scaleway up with AWS and GCP, which both stop at a plan too — for
their own documented reasons, in their case an emulator that cannot finish
the apply.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant