Skip to content

Cluster

apiVersion
fleet.nanohype.dev/v1alpha1
kind
Cluster
scope
Namespaced
reconciled by
eks-fleet

Cluster is an order for one EKS cluster. Applying it in a team namespace vends a cluster into a workload account: the Composition renders provider-opentofu Workspaces over the landing-zone substrate, and the cluster's endpoint, CA, OIDC issuer and network identifiers are written back to status as they converge. The substrate stays the source of truth — this is the ordering API on top of it.

The order. Placement (which account and region), the cluster's shape (version, networking, node pools), and the cross-account vend credentials, mirroring the fleet/aws/cluster-stack entrypoint's inputs field-for-field.

What kubectl get shows

ColumnTypeMeaning
REGIONstringRegion the cluster was vended into.
ACCOUNTstringWorkload account the cluster was vended into.
ENDPOINTstringEKS API endpoint — empty until the vend converges.

Spec

  • accountstringrequired

    Workload AWS account id. Records the placement and feeds resource tags. It does not select the account — spec.vendRoleArn does, being the role the entrypoint's provider assumes — so the two must agree, and admission requires this id to appear in that ARN. Leave vendRoleArn empty for a same-account vend, where the hub's own credentials are used and this field stands alone.

  • clusterNamestringrequired

    Cluster base name; the module prefixes it with environment to form the EKS cluster name <environment>-<clusterName>. Required and unique per (account, region, environment) — there is no default, because a shared default collides the moment a second cluster is vended into one account and environment. Must not equal the environment token. Capped at 12 characters so the derived <environment>-<clusterName> keeps the tightest cluster-scoped S3 name (agent-iam's account+region-qualified model-artifacts bucket) within S3's 63-char limit for a 9-character region token like us-east-1.

  • regionstringrequired

    AWS region the cluster is vended into. Any region is valid — the tofu state backend is decoupled from it: state lives in the single hub-account bucket named by spec.stateBucket, in spec.stateRegion, never in a per-region bucket that would have to track this value. So a vend into us-west-1 builds the cluster there but still stores its state in the one hub bucket. A region-lock SCP on the target account's OU can deny actions outside a permitted set, in which case the region named here must be one that SCP permits.

  • stateBucketstringrequired

    S3 bucket holding every vended cluster's tofu state. One bucket in the hub account for the whole fleet, provisioned by landing-zone's fleet-hub component alongside the hub IRSA role — not by a vend, so it must exist before the first Cluster is applied. The per-cluster state key is derived (fleet/<namespace>/<name>/terraform.tfstate), so this names only the bucket. Required and deliberately undefaulted: an S3 bucket name is a global-namespace identifier belonging to one account, so no value here could be correct for a second deployment. Declare the hub's bucket rather than inheriting someone else's.

  • stateRegionstringrequired

    Region the stateBucket lives in. This is the bucket's own region, deliberately NOT spec.region — the S3 backend validates the bucket against it, and decoupling the two lets a vend into any region still init against the one real state bucket. Required alongside stateBucket because the two name one thing: where this fleet's state lives. A region that merely looks plausible is worse than no value — it inits against the wrong place instead of failing.

  • teamstringrequired

    Owning team (tagging + ownership).

  • bootstrapAccessRoleArnstringdefault ""

    For cross-account vends, the hub's Crossplane role ARN — granted cluster-admin via an EKS access entry so the bootstrap Workspace's ambient get-token can reach this cluster's API (get-token can't present the fleet-vend external_id). Empty = same-account, where the creator (the hub) is already admin.

  • clusterPermissionsBoundaryArnstringdefault ""

    Permissions-boundary ARN cluster-stack mints the cluster's IAM roles with — the boundary of whichever fleet role runs the vend, published to SSM by landing-zone: fleet-vend's /eks-fleet/<env>/fleet-vend/vend_permissions_boundary_arn for cross-account (set alongside vendRoleArn), fleet-hub's /eks-fleet/<env>/fleet-hub/hub_permissions_boundary_arn for a same-account hub vend. Enforcement lives in IAM, not here: the fleet roles' CreateRole/AttachRolePolicy gate only allows role writes carrying their exact boundary, so a weaker or missing value can't mint a weaker role — the vend just fails. Empty = ungated (e.g. the local kind hub on user credentials).

  • clusterVersionstringdefault "1.36"

    Kubernetes version.

  • dataKmsKeyArnstringdefault ""

    The spoke's secrets-component data CMK ARN. The bootstrap Workspace's agent-iam module encrypts the model-artifacts + eval-reports buckets at rest with it (SSE-KMS). A spoke prerequisite referenced by ARN — like vendRoleArn and the permissions boundaries — not something the vend creates: cluster-stack provisions only network + cluster (no secrets module), so the order desk supplies the key the spoke's baseline already published to SSM (/platform/<env>/secrets/kms-key-arn). Empty = ungated local/hub validation, where no real artifacts bucket is minted.

  • enableAgentPlatformbooleandefault true

    Install the eks-agent-platform operator via GitOps: when true, the bootstrap labels the in-cluster ArgoCD Secret so eks-gitops reconciles the operator from the per-cluster OIDC/role annotations. False installs the operator out of band (e.g. the e2e harness, to avoid GitOps racing a locally-built image). Either way the cluster is still bootstrapped (Cilium + ArgoCD + the addon catalog): this flag only toggles the operator's GitOps label on the in-cluster ArgoCD Secret — not whether the bootstrap Workspace runs (it always renders once the cluster is Ready).

  • endpointPublicAccessbooleandefault false

    Expose the EKS API on a public endpoint. Clusters vend private by default; public access is explicit opt-in and must carry a non-empty endpointPublicAccessCidrs allowlist (a CEL rule on spec rejects public-with-empty at admission).

  • endpointPublicAccessCidrs[]stringdefault []

    CIDRs allowed to reach the public API endpoint. Required (non-empty) when endpointPublicAccess is true; irrelevant while the endpoint is private.

  • environmentstringdefault "development"

    Environment tier (development | staging | production) — prefixes resource names + tags.

  • gitopsRepoBranchstringdefault "main"

    Branch of the addon-catalog GitOps repo the spoke tracks. Defaults to main.

  • gitopsRepoUrlstringdefault "https://github.com/nanohype/eks-gitops.git"

    Git URL of the addon-catalog GitOps repo the spoke's app-of-apps points at — this org's eks-gitops (or a fork of it). Defaults to the org catalog; override only to vend against a fork. Required non-empty and validated as a git URL by the substrate, so it is always sent (unlike tenantsRepoUrl, which is optional).

  • moduleSourcestringdefault "git::https://github.com/nanohype/landing-zone.git?ref=1b5714dd25b9d12d53aafafaee4c2cc851fa178e"

    Git source for the landing-zone repo the Workspace fetches; tofu runs in the fleet/aws/cluster-stack subdir (set by the composition's entrypoint), which needs the whole repo present. Must be https and pinned to a full 40-character commit sha: the Workspace pod holds no SSH key, and provider-opentofu caches a module by its ref and never re-pulls, so a branch or tag resolves once and every later vend reuses that copy — two clusters ordered from the same spec would run different substrate. Point this at your own fork by changing the host; what is constrained is the shape of the source, not who publishes it. See docs/architecture.md.

  • networkobjectdefault {"mode":"create","create":{"vpcCidr":"10.0.0.0/16","maxAzs":3,"natGateways":1}}

    The VPC the cluster lands in. mode: create (default) provisions a self-owned VPC first; mode: adopt participates in a VPC provisioned elsewhere (a same-account shared VPC, or one shared cross-account via AWS RAM), referencing it by ID. Populate the sub-object matching mode.

    • adoptobjectdefault {"vpcId":"","subnetIds":{"private":[],"public":[]}}

      Adopt-mode inputs — used when mode is adopt.

      2 fields under adopt
      • subnetIdsobjectdefault {"private":[],"public":[]}

        Subnet IDs in the adopted VPC.

        2 fields under subnetIds
        • private[]stringdefault []

          Private subnet IDs (required, non-empty, when mode is adopt).

        • public[]stringdefault []

          Public subnet IDs (empty is valid for a private-only cluster).

      • vpcIdstringdefault ""

        VPC ID to adopt. Required (non-empty) when mode is adopt.

    • createobjectdefault {"vpcCidr":"10.0.0.0/16","maxAzs":3,"natGateways":1}

      Create-mode inputs — used when mode is create.

      7 fields under create
      • centralizedEgressbooleandefault false

        Route private egress through the transit gateway instead of a local NAT gateway (zero NAT gateways). Requires transitGatewayId.

      • ipamNetmaskLengthintegerdefault 0

        Netmask length of the CIDR to allocate from ipamPoolId (16-20; subnets are carved 8 bits smaller than the VPC block, so a base longer than /20 would drop below AWS's /28 minimum). 0 (default) = literal allocation.

      • ipamPoolIdstringdefault ""

        IPAM pool the VPC CIDR is drawn from. Empty (default) = literal allocation from vpcCidr. Cross-account this is the org IPAM env sub-pool shared in over RAM.

      • maxAzsintegerdefault 3

      • natGatewaysintegerdefault 1

      • transitGatewayIdstringdefault ""

        Transit gateway the VPC attaches to. Empty (default) = local NAT egress only. Requires an IPAM-allocated CIDR (set ipamPoolId), so attached prefixes stay non-overlapping.

      • vpcCidrstringdefault "10.0.0.0/16"

        CIDR for the VPC (literal allocation). Mutually exclusive with ipamPoolId.

    • modestringdefault "create"

      createadopt

      create — the stack owns a disposable VPC (subnets, endpoints, egress, and the ELB role tags). adopt — the stack participates in a VPC it does not own; the owner runs the VPC and the subnet tagging, and the cluster references it by adopt.* IDs.

  • observabilityTierstringdefault "floor"

    floorfull

    Which observability substrate this cluster runs. floor is CloudWatch alone: the amazon-cloudwatch-observability addon publishes Container Insights metrics, and the cluster's OpenTelemetry gateway exports application metrics as CloudWatch EMF and logs to CloudWatch Logs. full adds the in-cluster LGTM stack (Loki, Tempo, kube-state-metrics, grafana-operator) and Amazon Managed Prometheus / Grafana, which also requires the managed-monitoring substrate to exist for this cluster. Published as the observability/tier cluster-Secret label the eks-gitops generators select on. Both tiers run the same OpenTelemetry node agent and a gateway on the same telemetry.monitoring.svc endpoint, so a tenant workload is identical either way and only the destinations differ. Defaults to floor — a vended cluster is born light, and opting up is a deliberate act with a substrate prerequisite.

  • operatorPermissionsBoundaryArnstringdefault ""

    Permissions-boundary ARN the bootstrap Workspace mints the agent-platform operator role with. Same value + SSM source as clusterPermissionsBoundaryArn (one fleet role runs both Workspaces, and its IAM gate covers both); a separate field because it feeds the cluster-bootstrap entrypoint's operator_permissions_boundary_arn var. Empty = ungated.

  • portalAccessRoleArnstringdefault ""

    The portal worker's per-account spoke role ARN — granted a read EKS access entry so portal reaches this cluster's API (mint tokens, watch tenants) with the same role it uses for eks:DescribeCluster. Empty = portal not wired for this cluster.

  • systemNodesobjectdefault {"instanceTypes":["m7g.xlarge","m6g.xlarge"],"minSize":2,"maxSize":6,"desiredSize":2,"diskSize":100}

    System node group (Bottlerocket / Graviton arm64).

    • desiredSizeintegerdefault 2

    • diskSizeintegerdefault 100

    • instanceTypes[]stringdefault ["m7g.xlarge","m6g.xlarge"]

    • maxSizeintegerdefault 6

    • minSizeintegerdefault 2

  • tenantsRepoUrlstringdefault ""

    SSH URL of the private tenants GitOps repo. When set, the bootstrap registers a read-only deploy key + ArgoCD repo credential so ArgoCD can pull portal-committed tenant manifests.

  • ttlDaysintegerdefault 0

    Days-to-live for an ephemeral spoke. 0 (default) = persistent: tagged Lifecycle=persistent and never auto-reaped. >0 tags the spoke Lifecycle=ephemeral + Expiry=<vend date + ttlDays> and the hub reaper deletes this Cluster (a clean Crossplane teardown) ttlDays after creation.

  • vendRoleArnstringdefault ""

    Cross-account role the entrypoint's provider assumes to provision into a workload account. Empty = provision with the hub's own credentials (same-account — e.g. the local kind-hub validation).