Sovereign Infrastructure as Code: OVHcloud and Scaleway Terraform Providers, From Zero to Production
Infrastructure as Code

Sovereign Infrastructure as Code: OVHcloud and Scaleway Terraform Providers, From Zero to Production

October 23, 20268 min readTerraformOVHcloudScaleway

A hands-on guide to industrialising sovereign infrastructure with the OVHcloud and Scaleway Terraform providers: state backends, IAM, managed Kubernetes and a self-hosted observability stack.

Choosing a European cloud provider is a procurement decision. Making it reliable, reproducible and auditable is an engineering one. In practice, the gap between "we signed with OVHcloud" and "we run production there" is filled almost entirely by Infrastructure as Code, identity management and observability. This article walks through that path concretely: the real coverage of the ovh/ovh and scaleway/scaleway Terraform providers, how to bootstrap a sovereign state backend, and how to run a self-hosted observability stack that doesn't leak your telemetry to a US-controlled SaaS.

Sovereignty is an operational property, not a logo

A sovereign infrastructure is one where you can answer three questions without hesitation: where does the data physically sit, who legally controls the operator, and who technically holds the credentials. IaC is what makes the third question answerable. Without it, sovereignty degrades into a handful of consoles clicked by three people, with no history and no reproducibility — the exact opposite of what an auditor (or a DORA/NIS2 assessment) wants to see.

One nuance worth stating plainly: "European provider" and "SecNumCloud-qualified" are not synonyms. Qualification applies to a specific perimeter of a specific offering, not to a company as a whole. If your compliance target requires it, verify the exact scope of the qualified offer rather than assuming the whole catalogue is covered — and design your Terraform code so that moving a workload between a standard public cloud region and a qualified perimeter is a module swap, not a rewrite.

What the two providers actually cover

Both providers are mature enough for production, but they have different shapes. The Scaleway provider is a single, coherent provider covering nearly the whole catalogue. The OVHcloud story is split: ovh/ovh handles the account-level and managed-service layer (Kubernetes, managed databases, IAM, DNS, load balancers, vRack), while Public Cloud instances, networks and volumes are often driven through the OpenStack provider, since OVHcloud Public Cloud is an OpenStack deployment.

DomainOVHcloudScaleway
Compute (VMs)openstack_compute_instance_v2 (or newer ovh_cloud_project_instance)scaleway_instance_server
Managed Kubernetesovh_cloud_project_kube + ..._nodepoolscaleway_k8s_cluster + scaleway_k8s_pool
Private networkingvRack + OpenStack networks (ovh_vrack_cloudproject)scaleway_vpc / scaleway_vpc_private_network
Managed databasesovh_cloud_project_database (PG, MySQL, Kafka, OpenSearch…)scaleway_rdb_instance, scaleway_redis_cluster
Object storage (S3 API)S3-compatible endpoints, often managed via the AWS providerscaleway_object_bucket
IAMovh_iam_policy, ovh_iam_resource_groupscaleway_iam_application, _policy, _api_key
DNSovh_domain_zone_recordscaleway_domain_record

The practical consequence: do not build a fake "cloud-agnostic" module that abstracts both. The resource models, networking semantics and lifecycle rules differ enough that the abstraction will leak within weeks. Build two provider-specific modules exposing the same outputs (kubeconfig, VPC id, DB endpoint) and keep the consuming code generic. That's the portability that actually survives contact with production.

Bootstrapping: a state backend that never leaves Europe

Terraform state contains secrets, IPs and topology. Storing it on a US-hosted bucket defeats the whole exercise. Both providers expose S3-compatible object storage, which works with the native s3 backend provided you disable every AWS-specific check.

terraform {
  required_version = ">= 1.10"

  required_providers {
    ovh      = { source = "ovh/ovh",           version = "~> 2.0" }
    scaleway = { source = "scaleway/scaleway", version = "~> 2.48" }
    helm     = { source = "hashicorp/helm",    version = "~> 2.17" }
  }

  backend "s3" {
    bucket = "m2c-tfstate-prod"
    key    = "platform/par/terraform.tfstate"
    region = "fr-par"
    endpoints = { s3 = "https://s3.fr-par.scw.cloud" }

    use_path_style              = true
    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
    skip_metadata_api_check     = true
    skip_s3_checksum            = true

    use_lockfile = true   # native S3 locking, requires conditional writes
  }
}

use_lockfile (Terraform 1.10+) replaces the DynamoDB table with a lock object written using S3 conditional writes. Test it against your provider's endpoint before relying on it: conditional-write support varies between S3-compatible implementations. If it isn't supported, fall back to the pg backend on a managed PostgreSQL instance — fully sovereign, with real transactional locking, and arguably easier to back up than a lock table.

Encrypt the state at rest regardless: server-side encryption on the bucket plus, ideally, client-side encryption through Terraform's state encryption or an external secret store. Enable object versioning and a lifecycle rule to retain a few dozen versions — it is your only recourse after a bad terraform apply -replace.

Identities: the hard part

This is where European providers are still behind the hyperscalers. OVHcloud authenticates through an application key / application secret / consumer key triplet, which is long-lived and, by default, broad. Scaleway uses IAM applications with API keys. Neither offers the generalised OIDC federation that lets a GitHub Actions job assume a role with no stored secret — check the current state of the offering before designing around it, but assume you will be managing static credentials.

The mitigations that work:

  • One IAM application (Scaleway) or one consumer key with a narrow IAM policy (OVHcloud) per pipeline and per environment. Never a shared admin key.
  • One project per environment (prod, staging, dev) so that blast radius is enforced by the platform, not by HCL discipline.
  • Credentials stored in Vault or OpenBao, injected at runtime, rotated on a schedule with a Terraform-managed key rotation resource where available.
  • Read-only credentials for plan on pull requests, write credentials only on the protected branch job.

A real cluster, in code

On OVHcloud, a production managed Kubernetes cluster attached to a vRack private network looks like this:

resource "ovh_cloud_project_kube" "prod" {
  service_name = var.ovh_project_id
  name         = "prod-gra"
  region       = "GRA11"
  version      = "1.31"

  private_network_id = var.openstack_private_network_id

  update_policy = "MINIMAL_DOWNTIME"

  customization_apiserver {
    admissionplugins {
      enabled  = ["NodeRestriction"]
      disabled = ["AlwaysPullImages"]
    }
  }
}

resource "ovh_cloud_project_kube_nodepool" "workers" {
  service_name   = var.ovh_project_id
  kube_id        = ovh_cloud_project_kube.prod.id
  name           = "workers"
  flavor_name    = "b3-16"
  desired_nodes  = 3
  min_nodes      = 3
  max_nodes      = 9
  autoscale      = true

  template {
    metadata { labels = { "m2c.io/pool" = "workers" } }
  }
}

Two gotchas. First, private_network_id expects the OpenStack network identifier of a vRack-attached network, not the vRack id — a classic first-day mistake. Second, update_policy governs how OVHcloud applies patch upgrades to the control plane and nodes; pick it deliberately and align it with your maintenance windows rather than discovering it during an incident.

The Scaleway equivalent is more self-contained:

resource "scaleway_vpc_private_network" "prod" {
  name   = "prod-par"
  region = "fr-par"
}

resource "scaleway_k8s_cluster" "prod" {
  name                        = "prod-par"
  version                     = "1.31"
  cni                         = "cilium"
  private_network_id          = scaleway_vpc_private_network.prod.id
  delete_additional_resources = false

  autoscaler_config {
    scale_down_delay_after_add = "10m"
    estimator                  = "binpacking"
  }
}

resource "scaleway_k8s_pool" "workers" {
  cluster_id  = scaleway_k8s_cluster.prod.id
  name        = "workers"
  node_type   = "PRO2-S"
  size        = 3
  min_size    = 3
  max_size    = 9
  autoscaling = true
  autohealing = true
}

Note delete_additional_resources = false: left at its permissive value, destroying the cluster can also remove load balancers and block volumes created by the cloud controller manager. On production, set it explicitly to false and clean up deliberately.

Self-hosted sovereign observability

Running infrastructure in Europe while shipping every metric, log and trace to a SaaS backed by a non-European company is an obvious inconsistency — and one that an auditor will find. The good news is that the open-source observability stack is now production-grade and cheap to operate on object storage.

A pragmatic reference stack:

SignalComponentLong-term storage
MetricsPrometheus (Agent mode) + Mimir or Thanos, or VictoriaMetricsS3 bucket (fr-par / GRA)
LogsGrafana Alloy / Promtail → LokiS3 bucket, chunks + index
TracesOpenTelemetry Collector → TempoS3 bucket
Dashboards & alertingGrafana + AlertmanagerManaged PostgreSQL for Grafana state

Everything is wired to the same sovereign object storage you already use for state. The Loki configuration is typical:

loki:
  storage:
    type: s3
    bucketNames:
      chunks: m2c-loki-chunks
      ruler:  m2c-loki-ruler
    s3:
      endpoint:         s3.fr-par.scw.cloud
      region:           fr-par
      s3ForcePathStyle: true
      insecure:         false
  limits_config:
    retention_period: 720h

Three operational points matter more than the component choice. Retention: object storage is cheap, egress and API calls are not — tune chunk sizes and compaction rather than retention alone. High availability: run the write path replicated, but accept that a single-region observability stack goes blind if that region goes down; if your SLA requires it, replicate metrics to a second region of the other provider. And isolation: the observability cluster should not run on the cluster it observes. A small, separately managed cluster — ideally on the second provider — is the cheapest insurance policy in the stack.

Delivering it: GitOps for infrastructure

Pin provider versions strictly (~> on minor, never floating majors) and commit the lock file; both providers ship breaking changes between majors. Run plan on every pull request with read-only credentials, post the plan as a comment, and gate apply behind an approved merge. Atlantis works well here because it doesn't require cloud-native OIDC; so does a GitLab CI job with Vault-injected secrets.

Watch out for API rate limiting on large applies — reducing -parallelism to 5 or 10 is often the difference between a flaky pipeline and a deterministic one. And run drift detection on a schedule: on providers where some attributes force replacement (changing a cluster's private network, for instance), you want to discover drift in a nightly plan, not during an emergency change.

Production checklist

  • State in a European bucket, versioned, encrypted, with working locking (tested, not assumed).
  • One project and one scoped credential per environment; no shared admin keys; rotation automated.
  • Provider versions pinned and lock file committed.
  • Per-provider modules with identical outputs — no premature abstraction.
  • Observability self-hosted, on its own cluster, with telemetry stored in the same jurisdiction.
  • Documented, tested restore procedure for state and for managed database backups.
  • Verified compliance perimeter: which regions, which offering, which qualification actually applies.

None of this is exotic. It is the same discipline you would apply on a hyperscaler, minus a few conveniences (OIDC federation, the sheer breadth of managed services) and plus a meaningful property: you know exactly where your data and your credentials live.

← Back to blog