Foundations
opentofu/aws builds the VPC, the EKS cluster, its
bootstrap nodes, the log groups and the identities nothing in the cluster can
create. The bootstrap nodes are ACTIVE only once Cilium runs on them, so the
root opentofu/clusters/aws applies this
module together with the bootstrap.
What is decided for you
Section titled “What is decided for you”Enforced: no variable. Default: a variable changes it.
| Position | |
|---|---|
| EKS Standard, no Auto Mode | enforced |
| No VPC CNI, kube-proxy or CoreDNS add-on | enforced |
| Pod Identity only, no IAM OIDC provider | enforced |
force_update_version = false | default |
| Secrets encrypted with a module KMS key, rotated | default |
Public endpoint restricted by CIDR (0.0.0.0/0 refused), private on | enforced |
| A public and a private subnet per AZ, at least two AZs | enforced |
| A NAT Gateway and private route table per AZ | default |
S3 gateway endpoint; ecr.api, ecr.dkr, sts, ec2, logs interface endpoints | enforced on a module VPC |
| Standard support only | enforced |
| Log groups owned by the module, 90 days, KMS; VPC flow logs, whose role cannot recreate the group after a destroy (AWS-20) | default |
Control-plane logs off: CloudWatch bills them by the gigabyte. cluster_log_types = ["audit", "authenticator"] when your audit asks for the API server’s record | default |
| Two Spot Graviton bootstrap nodes, untainted, not autoscaled | default |
| Bootstrap nodes on AL2023, IMDSv2 hop limit 1, encrypted gp3, private subnets | enforced |
Crossplane’s role and permissions boundary, only when crossplane is set | default |
Under the hood
The variables behind the defaults: secrets_encryption_enabled,
secrets_encryption_kms_key_arn, create_nat_gateway (only with
create_vpc = false), log_retention_days, cluster_log_types,
vpc_flow_logs_enabled (all traffic, ten-minute aggregation),
bootstrap_node_* (six 4 vCPU Graviton families, 20 GiB volumes, one node
unavailable at a time), crossplane = null.
Plain engineering, no decision behind it:
vpc_cidrdefaults to10.0.0.0/16, split into one equal block per subnet.authentication_mode = "API": access entries, not theaws-authConfigMap.bootstrap_cluster_creator_admin_permissions = true, set explicitly (see Measured). AWS accepts it only at creation.- The node role carries
AmazonEKSWorkerNodePolicy, the three actions ofAmazonEC2ContainerRegistryPullOnlyinline, and the Cilium operator’s ENI policy. NoAmazonEKS_CNI_Policy. - Subnet tags
kubernetes.io/role/elb,kubernetes.io/role/internal-elbandkubernetes.io/cluster/<name> = owned. - Every resource carries
owner,environment,socle-version, plusadditional_tags.
What is deliberately absent
Section titled “What is deliberately absent”- IPv6 and security groups for pods.
- EKS add-ons: the bootstrap module creates them once Cilium runs.
- Load balancers: EKS’s in-tree controller gives the shared Gateways their NLBs; this module only creates their ACM certificate.
- IRSA: the OIDC issuer URL is an output, unused.
- Workload identities other than Crossplane’s: each catalog module declares its own; the CSI drivers’ roles are the bootstrap’s.
- Fargate and Karpenter: Karpenter is to be a catalog module (#53).
- GuardDuty.
- A DynamoDB endpoint, a region variable, any credential as input.
Measured
Section titled “Measured”- 2026-09-14, real EKS, aws provider 6.x: with
bootstrap_cluster_creator_admin_permissionsunset, the applying principal got no access entry andkubectlwas refused; set totrue, it gets cluster-admin, which lets the same apply install Cilium and Flux. - 2026-09-14, real EKS: a refresh after the first apply showed no drift.
Reference
Section titled “Reference”Requirements
Section titled “Requirements”| Name | Version |
|---|---|
| terraform | >= 1.13 |
| aws | >= 6.0, < 7.0 |
| time | >= 0.13, < 1.0 |
Modules
Section titled “Modules”No modules.
Resources
Section titled “Resources”Inputs
Section titled “Inputs”| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| availability_zones | AZs the VPC's public and private subnets are spread across. Every VPC gets at least one public and one private subnet per AZ — never a flat, all-public layout — to satisfy ISO 27001 A.8.22 and SOC 2 CC6 network segregation. There is no all-public escape hatch: the private tier is structural, whether or not a client's workloads use it. | list(string) | n/a | yes |
| cluster_endpoint_public_access_cidrs | CIDRs allowed to reach the public EKS API endpoint. Required, no default: the endpoint is restricted by CIDR rather than left open or made fully private-only (AWS's own recommended pattern for this case), and there is no globally correct default to authorize on a client's behalf. | list(string) | n/a | yes |
| cluster_name | Name shared by the VPC, the EKS cluster and every resource this module creates around them. | string | n/a | yes |
| environment | Stamped on every billable resource. Also what the factory's upgrade rings (dev/staging/prod) key off. | string | n/a | yes |
| kubernetes_version | EKS control plane version. Required, no default: the module accepts whatever version it is given rather than enforcing the policy ceiling itself. The n-1 policy ceiling / n-2 compatibility floor is decided and bumped by the socle Kargo pipelines, not by this module — a validationblock cannot call the AWS API to know what "current" is, and the decoupled socle-release/Kubernetes-version pipelines are the actual owners of that decision. | string | n/a | yes |
| owner | Stamped on every billable resource so cost can be attributed and orphans can be found. | string | n/a | yes |
| additional_tags | Extra tags merged onto every resource this module creates, on top of owner/environment/socle-version. | map(string) | {} | no |
| bootstrap_node_capacity_type | SPOT by default: the several instance types spread the two nodes over independent pools, and EKS replaces a node at risk before draining it. ON_DEMAND for a cluster that must not lose a bootstrap node to a reclaim, at about two and a half times the price. | string | "SPOT" | no |
| bootstrap_node_count | How many bootstrap nodes, spread over the private subnets' AZs. Two by default: when one is reclaimed or lost the other carries the whole socle until it is replaced, CoreDNS keeps a replica, and Karpenter's chart places its two replicas on different nodes in different zones. One is accepted on a cluster that can live with neither. Nothing scales this group, so it is one number rather than min, max and desired. | number | 2 | no |
| bootstrap_node_instance_types | Instance types of the bootstrap node group, which carries Cilium's operator, CoreDNS, Flux and later Karpenter — not the client's workloads, which Karpenter's nodes carry. Several on Spot, so the nodes come from independent capacity pools; on demand, the first is used. The default is six Graviton families of 4 vCPU and 8 to 32 GiB: each one alone carries the whole socle, Karpenter included, with half its CPU left, so losing a node to a reclaim is routine. All must share an architecture, which the AMI follows. | list(string) | [ | no |
| cluster_log_types | Control plane log types shipped to CloudWatch Logs. None by default: ingestion is billed by the gigabyte, and the audit stream records every request to the API server — one controller writing in a loop made it about $40 a day on an idle cluster (#80). A client whose audit expects the API server's record of who did what (ISO 27001 A.8.15, SOC 2 CC7) turns on at least audit and authenticator; the log group, its retention and its key exist either way, so the first line is already covered. | list(string) | [] | no |
| create_nat_gateway | Create one NAT Gateway per AZ. One per AZ, never a single shared one, to avoid cross-AZ data transfer charges — not a toggle for disabling NAT outright, which the private-subnet decision above rules out. Exists only for the create_vpc = false case, where the consumer's existing VPC already manages its own NAT. | bool | true | no |
| create_vpc | Create the VPC, or attach to one the consumer already manages. | bool | true | no |
| crossplane | Give the catalog's crossplane module its AWS identity — the one the socle cannot make for itself: an IAM role, bound through EKS Pod Identity to crossplane-system/provider-aws, allowed to create roles under /socle/<cluster_name>/ only, and only carrying the permissions boundary this module writes. allowed_services is that boundary: the AWS services (IAM action prefixes, such as route53 or s3) any module's role may be granted. Empty grants nothing; iam, sts, organizations, account, sso and identitystore are refused. Which resources of those services a module reaches is its own role's policy. Null, the default, creates nothing. The client root passes the boundary's ARN into kube.crossplane. | object({ | null | no |
| force_update_version | Force the control plane version update even if Upgrade Insights reports blocking findings. Default false: Upgrade Insights is a mandatory pre-check, never sufficient alone (it only sees the client's own removed-API usage, over a rolling 30-day audit-log window that both misses infrequent calls and over-reports fixed ones) — but AWS's own blocking of update-cluster-version on ERROR findings is currentlyrolled back, so this module does not assume AWS enforces the check either. | bool | false | no |
| gateway_certificate | The ACM certificate the socle's two Gateways terminate TLS with, at the load balancer: domain and *.domain, validated by DNS in the publicRoute 53 zone named domain — found by name, no zone ID to copy. zonenames that zone instead when domain is a subdomain of it, such asdomain = "sbx.acme.example" in zone = "acme.example". Every route published through a Gateway — ArgoCD's included — is then <name>.<domain>. Null, the default, creates nothing, and the bootstrapmodule creates no Gateway: the socle never serves a route in clear text. | object({ | null | no |
| log_retention_days | Retention for the log groups this module creates — the control plane's and the VPC flow logs'. Set explicitly because a log group left to AWS never expires. | number | 90 | no |
| private_subnet_ids | Existing private subnet IDs, one per AZ in availability_zones. Required when create_vpc is false — this module carves its own subnets out of vpc_cidr only when it also creates the VPC. | list(string) | [] | no |
| public_subnet_ids | Existing public subnet IDs, one per AZ in availability_zones. Required when create_vpc is false — same reasoning as private_subnet_ids. | list(string) | [] | no |
| secrets_encryption_enabled | Envelope-encrypt Kubernetes Secrets via KMS. On by default: essentially free (~$1/month per key, negligible per-request cost), and standard on Kubernetes 1.28+ already. | bool | true | no |
| secrets_encryption_kms_key_arn | Existing KMS key for Secrets envelope encryption. Leave unset and the module creates its own key. Ignored when secrets_encryption_enabled is false. | string | null | no |
| vpc_cidr | CIDR for the VPC when this module creates it. Arbitrary default (not a research decision) — a /16 large enough for any socle estate. | string | "10.0.0.0/16" | no |
| vpc_flow_logs_enabled | Record accepted and rejected traffic on the VPC this module creates. On by default, aggregated over ten-minute windows to keep the volume down: it is the only record of who talked to whom, and the segmentation this VPC is built around is unauditable without it. | bool | true | no |
| vpc_id | Existing VPC to attach to. Required when create_vpc is false, and incoherent to set when create_vpc is true — this module cannot both create a VPC and attach to a different one. | string | null | no |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
| bootstrap_node_group | The bootstrap node group, once its nodes have joined. node_count is what the bootstrap module checks before it installs anything that needs a node; referencing it is also what orders those releases after the group. |
| cilium_operator_policy_json | IAM policy the Cilium operator needs in ENI mode — the bootstrap module installs Cilium on this cluster before Flux. The bootstrap nodes' role already carries it; any other node role the operator may be scheduled on needs it too. |
| cluster_ca_certificate | Base64-encoded cluster CA certificate, for building a kubeconfig. |
| cluster_endpoint | The control plane's API endpoint — the access path the socle and its automation use. |
| cluster_name | Name of the EKS cluster. |
| crossplane_permissions_boundary_arn | ARN of the permissions boundary every role Crossplane creates must carry — what kube.crossplane.permissions_boundary takes, and what the client root passes for you. Null when crossplane is not set. |
| crossplane_role_arn | ARN of the IAM role the catalog's crossplane module's AWS providers run as, through Pod Identity. Null when crossplane is not set. |
| gateway_certificate_arn | ARN of the issued ACM certificate the socle's Gateways terminate TLS with — what the bootstrap module's gateway_certificate_arn takes, and what the client root passes for you. Null when gateway_certificate is not set. |
| helm_kubernetes | Drop-in value for the helm provider's kubernetes attribute, so a root configures it in one line. Carries no credential: the exec plugin obtains a short-lived token from the caller's ambient AWS credentials at call time, exactly as the aws provider itself authenticates. |
| oidc_issuer_url | The cluster's OIDC issuer. Checklist requirement, not this module's identity mechanism — Pod Identity is, IRSA is absent, and nothing here provisions an OIDC trust relationship against it. Null on an emulated cluster that reports no identity (floci), so an apply there still converges. |
| private_subnet_ids | Private subnet IDs, one per AZ. |
| public_subnet_ids | Public subnet IDs, one per AZ. |
| region | Region the cluster and VPC were created in, as resolved from the provider. |
| service_cidr | The Kubernetes service range EKS chose for this cluster (172.20.0.0/16 or 10.100.0.0/16, by VPC CIDR). The bootstrap module gives CoreDNS its .10 address, the one every node's kubelet is told to use. Null on an emulated cluster that reports none (floci). |
| tags | The standard tag set applied to every billable resource this module creates. |
| vpc_id | ID of the VPC the cluster is attached to, whether this module created it or not. |