Skip to content
The last release, 0.1.0, predates this site: this is the documentation of main.

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.

Enforced: no variable. Default: a variable changes it.

Position
EKS Standard, no Auto Modeenforced
No VPC CNI, kube-proxy or CoreDNS add-onenforced
Pod Identity only, no IAM OIDC providerenforced
force_update_version = falsedefault
Secrets encrypted with a module KMS key, rotateddefault
Public endpoint restricted by CIDR (0.0.0.0/0 refused), private onenforced
A public and a private subnet per AZ, at least two AZsenforced
A NAT Gateway and private route table per AZdefault
S3 gateway endpoint; ecr.api, ecr.dkr, sts, ec2, logs interface endpointsenforced on a module VPC
Standard support onlyenforced
Log groups owned by the module, 90 days, KMS; VPC flow logsdefault
Control-plane logs off: CloudWatch bills them by the gigabyte. cluster_log_types = ["audit", "authenticator"] when your audit asks for the API server’s recorddefault
Two Spot Graviton bootstrap nodes, untainted, not autoscaleddefault
Bootstrap nodes on AL2023, IMDSv2 hop limit 1, encrypted gp3, private subnetsenforced
Crossplane’s role and permissions boundary, only when crossplane is setdefault
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_cidr defaults to 10.0.0.0/16, split into one equal block per subnet.
  • authentication_mode = "API": access entries, not the aws-auth ConfigMap.
  • bootstrap_cluster_creator_admin_permissions = true, set explicitly (see Measured). AWS accepts it only at creation.
  • The node role carries AmazonEKSWorkerNodePolicy, the three actions of AmazonEC2ContainerRegistryPullOnly inline, and the Cilium operator’s ENI policy. No AmazonEKS_CNI_Policy.
  • Subnet tags kubernetes.io/role/elb, kubernetes.io/role/internal-elb and kubernetes.io/cluster/<name> = owned.
  • Every resource carries owner, environment, socle-version, plus additional_tags.
  • 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.
  • 2026-09-14, real EKS, aws provider 6.x: with bootstrap_cluster_creator_admin_permissions unset, the applying principal got no access entry and kubectl was refused; set to true, 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.
NameVersion
terraform>= 1.10
aws>= 6.0, < 7.0

No modules.

NameType
aws_acm_certificate.gatewayresource
aws_acm_certificate_validation.gatewayresource
aws_cloudwatch_log_group.clusterresource
aws_cloudwatch_log_group.flow_logsresource
aws_eip.natresource
aws_eks_cluster.socleresource
aws_eks_node_group.bootstrapresource
aws_eks_pod_identity_association.crossplaneresource
aws_flow_log.socleresource
aws_iam_policy.crossplane_boundaryresource
aws_iam_role.clusterresource
aws_iam_role.crossplaneresource
aws_iam_role.flow_logsresource
aws_iam_role.noderesource
aws_iam_role_policy.crossplaneresource
aws_iam_role_policy.flow_logsresource
aws_iam_role_policy.node_cilium_operatorresource
aws_iam_role_policy.node_ecr_pullresource
aws_iam_role_policy_attachment.clusterresource
aws_iam_role_policy_attachment.noderesource
aws_internet_gateway.socleresource
aws_kms_key.logsresource
aws_kms_key.secretsresource
aws_launch_template.bootstrapresource
aws_nat_gateway.socleresource
aws_route53_record.gateway_certificate_validationresource
aws_route_table.privateresource
aws_route_table.publicresource
aws_route_table_association.privateresource
aws_route_table_association.publicresource
aws_security_group.vpc_endpointsresource
aws_subnet.privateresource
aws_subnet.publicresource
aws_vpc.socleresource
aws_vpc_endpoint.interfaceresource
aws_vpc_endpoint.s3resource
aws_caller_identity.currentdata source
aws_iam_policy_document.cilium_operatordata source
aws_iam_policy_document.node_ecr_pulldata source
aws_region.currentdata source
aws_route53_zone.gatewaydata source
NameDescriptionTypeDefaultRequired
availability_zonesAZs 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/ayes
cluster_endpoint_public_access_cidrsCIDRs 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/ayes
cluster_nameName shared by the VPC, the EKS cluster and every resource this module creates around them.stringn/ayes
environmentStamped on every billable resource. Also what the factory's upgrade rings (dev/staging/prod) key off.stringn/ayes
kubernetes_versionEKS 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 validation
block 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.
stringn/ayes
ownerStamped on every billable resource so cost can be attributed and orphans can be found.stringn/ayes
additional_tagsExtra tags merged onto every resource this module creates, on top of owner/environment/socle-version.map(string){}no
bootstrap_node_capacity_typeSPOT 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_countHow 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.
number2no
bootstrap_node_instance_typesInstance 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)
[
"t4g.xlarge",
"m7g.xlarge",
"m6g.xlarge",
"c7g.xlarge",
"c6g.xlarge",
"r6g.xlarge"
]
no
cluster_log_typesControl 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_gatewayCreate 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.
booltrueno
create_vpcCreate the VPC, or attach to one the consumer already manages.booltrueno
crossplaneGive 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({
allowed_services = optional(list(string), [])
})
nullno
force_update_versionForce 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 currently
rolled back, so this module does not assume AWS enforces the check
either.
boolfalseno
gateway_certificateThe ACM certificate the socle's two Gateways terminate TLS with, at the
load balancer: domain and *.domain, validated by DNS in the public
Route 53 zone named domain — found by name, no zone ID to copy. zone
names that zone instead when domain is a subdomain of it, such as
domain = "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 bootstrap
module creates no Gateway: the socle never serves a route in clear text.
object({
domain = string
zone = optional(string)
})
nullno
log_retention_daysRetention 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.number90no
private_subnet_idsExisting 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_idsExisting 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_enabledEnvelope-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.booltrueno
secrets_encryption_kms_key_arnExisting KMS key for Secrets envelope encryption. Leave unset and the module creates its own key. Ignored when secrets_encryption_enabled is false.stringnullno
vpc_cidrCIDR 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_enabledRecord 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.booltrueno
vpc_idExisting 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.stringnullno
NameDescription
bootstrap_node_groupThe 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_jsonIAM 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_certificateBase64-encoded cluster CA certificate, for building a kubeconfig.
cluster_endpointThe control plane's API endpoint — the access path the socle and its automation use.
cluster_nameName of the EKS cluster.
crossplane_permissions_boundary_arnARN 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_arnARN 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_arnARN 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_kubernetesDrop-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_urlThe 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_idsPrivate subnet IDs, one per AZ.
public_subnet_idsPublic subnet IDs, one per AZ.
regionRegion the cluster and VPC were created in, as resolved from the provider.
service_cidrThe 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).
tagsThe standard tag set applied to every billable resource this module creates.
vpc_idID of the VPC the cluster is attached to, whether this module created it or not.