Quickstart: Scaleway
Two modules in one root of your own, applied in two steps: opentofu/scaleway builds the cluster, then the bootstrap installs Flux and the catalog. No Scaleway apply has run in the socle’s CI.
Before you start
Section titled “Before you start”From prerequisites:
-
identity validated and the quota ticket answered: 4
COMPUTE3-X8C-16Gnodes per cluster, up to 10, and a dedicated control plane forprod; -
a Project for this environment, and its ID;
-
an IAM application for the apply, with
KubernetesFullAccess,VPCFullAccess,PrivateNetworksFullAccess,VPCGatewayFullAccess,IPAMFullAccess,InstancesFullAccessandObservabilityFullAccesson the Project,IAMManageron the Organization, and its key exported:Terminal window export SCW_ACCESS_KEY=SCWXXXXXXXXXXXXXXXXXexport SCW_SECRET_KEY=...export SCW_DEFAULT_ORGANIZATION_ID=...export SCW_DEFAULT_PROJECT_ID=... -
a versioned Object Storage state bucket (prerequisites);
-
OpenTofu 1.13 or later, and
kubectl.
Write your tfvars
Section titled “Write your tfvars”The root, main.tf. Step 1 is the foundations alone:
terraform { required_version = ">= 1.13"
backend "s3" { bucket = "my-tofu-state" key = "socle/scaleway/prod/terraform.tfstate" region = "fr-par" endpoints = { s3 = "https://s3.fr-par.scw.cloud" } use_lockfile = true skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true }}
variable "socle_version" { type = string }variable "project_id" { type = string }variable "cluster_endpoint_public_access_cidrs" { type = list(string) }variable "kube" { type = any default = {}}
provider "scaleway" { project_id = var.project_id region = "fr-par" zone = "fr-par-1"}
module "foundations" { source = "oci://ghcr.io/do-now-io/socle/opentofu-modules//opentofu/scaleway?tag=${var.socle_version}"
project_id = var.project_id region = "fr-par" cluster_name = "acme-prod" owner = "platform" environment = "prod"
# Required, no default: Scaleway has no release channel. kubernetes_version = "1.35"
# Required, no default, 0.0.0.0/0 refused: the control plane is always public. # List the runner's egress and the operators'. cluster_endpoint_public_access_cidrs = var.cluster_endpoint_public_access_cidrs
# Required: when patch upgrades may land, in UTC. Production last. maintenance_window = { day = "saturday", start_hour = 3 }
# Required: what the in-cluster Crossplane identity may manage in this Project. crossplane_permission_sets = ["ObjectStorageFullAccess"] crossplane_key_expires_at = "2027-10-01T00:00:00Z"}
output "cluster_id" { value = module.foundations.cluster_id }output "gateway_egress_cidrs" { value = module.foundations.gateway_egress_cidrs }And prod.tfvars:
# The only line an upgrade touches.socle_version = "0.1.0" # x-release-please-version
project_id = "22222222-2222-2222-2222-222222222222"cluster_endpoint_public_access_cidrs = ["203.0.113.0/24"]
# Only what differs from the catalog's defaults.kube = {}The minimal example is the same foundations call, with a relative source; CI plans it.
Step 1, the cluster.
tofu inittofu apply -var-file=prod.tfvarsThe outputs include crossplane_access_key and the sensitive crossplane_secret_key. Nothing in the cluster uses them yet: leave the key in state, never copy it into a Secret. oidc_issuer_url and workload_identity_pool are null on Scaleway.
Step 2, Flux and the catalog. Add to main.tf:
# Kapsule has no exec credential plugin: this exec hands the API server# SCW_SECRET_KEY, the same variable the scaleway provider reads, at call time.# No token is stored in state.provider "helm" { kubernetes = module.foundations.helm_kubernetes}
# The bootstrap requires the aws provider for its EKS add-ons, and OpenTofu# configures it even when cloud = "scaleway" plans no AWS resource. This block# satisfies it without calling AWS.provider "aws" { region = "eu-west-1" access_key = "unused" secret_key = "unused" skip_credentials_validation = true skip_requesting_account_id = true skip_metadata_api_check = true}
module "socle" { source = "oci://ghcr.io/do-now-io/socle/opentofu-modules//opentofu/bootstrap?tag=${var.socle_version}"
cloud = "scaleway" cluster_name = "acme-prod" environment = "prod" owner = "platform" region = "fr-par"
socle_version = var.socle_version kube = var.kube}No Cilium and no CoreDNS here: Kapsule operates both, and the bootstrap refuses cilium.
For External-DNS, first create its Secret with a key that may write the zone:
kubectl create namespace external-dnskubectl -n external-dns create secret generic external-dns-scaleway \ --from-literal=SCW_ACCESS_KEY=SCWXXXXXXXXXXXXXXXXX \ --from-literal=SCW_SECRET_KEY=...kube = { external_dns = { enabled = true, domain_filters = ["acme.example"] }}Then apply again:
tofu inittofu apply -var-file=prod.tfvarsCheck it converged
Section titled “Check it converged”Fetch a kubeconfig. cluster_id is region/uuid:
scw k8s kubeconfig install "$(tofu output -raw cluster_id | cut -d/ -f2)" region=fr-parkubectl get nodes # 4 nodes, 2 per zone, Readykubectl -n flux-system get ocirepository socle # the pulled digest, SourceVerifiedkubectl -n flux-system get resourceset # socle-root and one per module, Readysocle-root Ready means the catalog converged. Expect no Gateway: nothing implements Gateway API on Scaleway yet (limits).
Next steps
Section titled “Next steps”- Configure the catalog through
kube, and enable a module. - Upgrade:
socle_versionmoves the modules and the artifact;kubernetes_versionmoves the cluster, environment by environment. - Limits, troubleshooting, uninstall.