Configure a cluster
A cluster is one main.tf, in a directory of its own, that calls the socle’s
root for its cloud.
Before you start: your cloud’s prerequisites are met (AWS, GCP). AWS and GCP have a root; on Azure and Scaleway, the foundations take a tfvars file of their own (Azure, Scaleway).
1. One directory per cluster
Section titled “1. One directory per cluster”clusters/ dev/main.tf prod/main.tfEach has its own state key, so a plan on dev never touches prod. Commit the
.terraform.lock.hcl tofu init writes next to it.
2. Write its main.tf
Section titled “2. Write its main.tf”On AWS
Section titled “On AWS”terraform { backend "s3" { bucket = "acme-tofu-state" key = "socle/aws/acme-prod.tfstate" region = "eu-west-3" use_lockfile = true }}
locals { socle_version = "0.1.0" # x-release-please-version}
module "socle" { source = "oci://ghcr.io/do-now-io/socle/opentofu-modules//opentofu/clusters/aws?tag=${local.socle_version}"
socle_version = local.socle_version
aws = { region = "eu-west-3" cluster_name = "acme-prod" owner = "platform" environment = "prod" kubernetes_version = "1.34" availability_zones = ["eu-west-3a", "eu-west-3b", "eu-west-3c"] cluster_endpoint_public_access_cidrs = ["203.0.113.0/24"] gateway_certificate = { domain = "acme.example" } }
kube = { argocd = { domain = "argocd.acme.example" } kyverno = { enabled = true } }}On GCP
Section titled “On GCP”terraform { backend "gcs" { bucket = "acme-prod-tofu-state" prefix = "socle/gcp/acme-prod" }}
locals { socle_version = "0.1.0" # x-release-please-version}
module "socle" { source = "oci://ghcr.io/do-now-io/socle/opentofu-modules//opentofu/clusters/gcp?tag=${local.socle_version}"
socle_version = local.socle_version
gcp = { project_id = "acme-prod" region = "europe-west1" cluster_name = "acme-prod" owner = "platform" environment = "prod" create_network = true maintenance_window = { start_time = "2026-01-03T02:00:00Z" end_time = "2026-01-03T14:00:00Z" recurrence = "FREQ=WEEKLY;BYDAY=SA" } gateway_certificate = { dns_zone = "acme-example", domains = ["acme.example", "*.acme.example"] } }
kube = { argocd = { domain = "argocd.acme.example" } kyverno = { enabled = true } }}One socle cluster per project: every module’s Workload Identity principal names the project, never the cluster (GCP prerequisites).
The blocks
Section titled “The blocks”| Block | What it holds |
|---|---|
socle_version | A released version, never latest or main; the only line an upgrade touches, source included. |
aws | The AWS foundations’ variables. Keys without a default, kubernetes_version among them, are required. Every option: prod.tfvars.example, written as tfvars; the same keys go in the module block. |
gcp | The GCP foundations’ variables. Keys without a default, maintenance_window among them, are required; there is no Kubernetes version, the release channel decides. Every option: prod.tfvars.example. |
kube | Only what differs from the catalog’s defaults, in snake_case. Defaults: Inputs; modules: catalog. |
3. Pass chart values
Section titled “3. Pass chart values”Every module with a chart takes values, any value of its chart. Yours win
over the socle’s defaults and over the module’s named attributes:
kube = { argocd = { domain = "argocd.acme.example" values = { configs = { cm = { "accounts.alice" = "apiKey, login" } rbac = { "policy.csv" = "g, alice, role:admin" } } } values_secret = "argocd-values" }}values lands in the OpenTofu state, so secrets are refused there at plan.
Put them in a Secret with a values.yaml key, in the module’s namespace, and
name it in values_secret:
kubectl -n argocd create secret generic argocd-values --from-file=values.yamlkubectl -n argocd label secret argocd-values reconcile.fluxcd.io/watch=EnabledThe Secret is merged last and optional; the label applies a change at once. OpenTofu never reads it.
4. Adjust what precedes Flux, if you need to
Section titled “4. Adjust what precedes Flux, if you need to”On AWS the root also takes cilium, coredns and eks_addons, every key
optional (Inputs),
beside aws and kube in the module block:
cilium = { hubble = true } coredns = { values = { replicaCount = 3 } } eks_addons = { efs_csi = true }Leave cilium.enabled at true on a real EKS: the cluster has no other
network.
On GCP there is nothing to adjust: GKE Autopilot runs the network (Dataplane V2), the cluster DNS and the nodes, and the root takes none of these blocks.
5. Plan and apply
Section titled “5. Plan and apply”cd clusters/prodtofu inittofu plantofu applyA misspelt name, a wrong type, a module not offered on your cloud or a secret
in values fails the plan
(Troubleshooting). A green apply
is not convergence: check
kubectl -n flux-system get resourceset socle-root
(Enable a module).