Repository navigation
feat(catalog): the karpenter module — nodes for the workloads on aws, its role and interruption queue through Crossplane - #116
Merged
Conversation
Contributor
|
📖 Docs preview: https://do-now-io.github.io/socle/pr/116/ |
Demmonius
force-pushed
the
issue-53-karpenter
branch
4 times, most recently
from
October 17, 2026 09:46
a0d503e to
3da2aa3
Compare
… IAM exception, queues and rules for Crossplane What the karpenter module (#53) needs from the foundations, and nothing that names it: - an instance profile for the bootstrap nodes' role, <cluster>-bootstrap-node: the identity of every node of the cluster. Karpenter's EC2NodeClass names it, so its nodes are the bootstrap nodes' peers (kubelet, ECR, Cilium's ENI calls, the access entry EKS made) and it never creates a profile. A profile, not a role: OpenTofu still makes no role for a module. - with ec2 in crossplane.allowed_services, and only then, the boundary lets a role pass that one role to EC2 and read instance profiles; iam:* stays denied on every other resource (AWS-21). Without ec2 the boundary is unchanged. - Crossplane's role may manage SQS queues and EventBridge rules (with their targets) under <cluster>-, never read or send a message (AWS-22). - the node_instance_profile output. The alternatives weighed — a second node role made by the foundations, or the node role, profile and access entry made by Crossplane — are in AWS-21. Refs #53.
… its role and interruption queue through Crossplane A pod that fits on no node gets one, Spot and Graviton first; an empty or under-used node goes. aws only, off by default, needs crossplane. - oci/catalog/karpenter: steps namespace, workload, nodes. The chart 1.15.0 from ECR Public, CRDs CreateReplace on install and upgrade, two replicas never on a node Karpenter manages, AWS_REGION set (pods cannot reach IMDS on one hop). The default EC2NodeClass and NodePool from nodes/, put in by the karpenter-nodes Kustomization's patches: the bootstrap nodes' instance profile, subnets and security group on the tags the foundations and EKS already lay down, AL2023, Cilium's startup taint, c/m/r gen 6+, WhenEmptyOrUnderutilized. - oci/clusters/aws/karpenter/patch.yaml, step cloud: the controller's Role (AWS's own controller policy for 1.15.0, minus instance-profile writes and zonal shift; PassRole on the nodes' role, to EC2 only), its Pod Identity association, the interruption queue and the five EventBridge rules with their targets; the controller's ServiceAccount, the association's subject, created by the step (the chart's off); and karpenter-access, which holds the workload until the role, the association and the queue are Ready and a Job under that ServiceAccount has been given the role's credentials (#97). - Uninstall order (KARPENTER-04): the NodePool and EC2NodeClass in a Flux Kustomization with WaitForTermination, deleted in flux-operator's first stage; the controller's HelmRelease in the child ResourceSet karpenter-controller, deleted after — so Karpenter removes its nodes before it goes. - kube.karpenter: enabled, node_pool (capacity_types, architectures, cpu, memory, consolidate_after, ami), values, values_secret; every refusal with a failing tofu test. The aws root warns when ec2, ssm, pricing, sqs or eks is missing from aws.crossplane.allowed_services. - crossplane on aws: provider-aws-sqs and provider-aws-cloudwatchevents. - monitoring: Karpenter's capacity and performance dashboards, the socle's five rules. - e2e: on floci, the role, queue and rules through Crossplane and the workload withheld, then the controller with static keys, the valuesFrom order, the EC2NodeClass reconciled against floci's EC2, the nodes' objects gone before the controller. karpenter-module-eks, platform: eks, is the sandbox's proof, by hand: a node launched, Cilium on it, consolidated away. - gcp: not offered — GKE Autopilot provisions its own nodes (KARPENTER-05). Refs #53.
…ttling registry, module e2e jobs get 25 minutes The requests guard pulls every catalog chart. ECR Public, where the karpenter chart lives, answers anonymous pulls from GitHub runners with 429 toomanyrequests, and the cache key moves with every template change, so each such change pulled everything again and the required kubeconform check failed on the registry. A chart is cached under its URL, name and version, so the last cache is restored whatever the key (restore-keys); the retries wait 20 s then 40 s instead of 5 s then 10 s. With six AWS providers in Crossplane, velero's e2e job ran 17.8 to 20.3 min against a 20-minute budget, the destroy cancelled: module jobs get 25. Refs #53.
Demmonius
force-pushed
the
issue-53-karpenter
branch
4 times, most recently
from
October 17, 2026 14:52
16b0613 to
42809ab
Compare
…hrough the Internet Gateway, nothing in from the internet Hugo's ask: two pools, always. A workload costly in network traffic goes public, its egress through the Internet Gateway, with no NAT Gateway to pay per gigabyte; everything else stays private. - karpenter: EC2NodeClasses and NodePools `private` (the default, private subnets) and `public` (tainted socle.do-now.io/node-pool=public, public subnets, a public IP per node); kube.karpenter.private_pool and public_pool replace node_pool, same keys, validated per pool (KARPENTER-03). - aws foundations: the security group <cluster>-public-nodes — in from the VPC's CIDR, out anywhere — and the cluster security group admitting it back (on floci, which reports no cluster security group, that rule lands on the group itself); the vpc_cidr and public_nodes_security_group outputs (AWS-23). - public nodes register labelled node.kubernetes.io/exclude-from-external-load-balancers, through an AL2023 NodeConfig: Karpenter keeps kubernetes.io labels off the kubelet's flags, and labelled later the node was targeted for a few seconds, long enough for EKS's controller to open a NodePort to 0.0.0.0/0 in its security group and never close it. - bootstrap: on aws Cilium masquerades what leaves the VPC (ipv4NativeRoutingCIDR = the VPC's CIDR, wired by the aws root as cluster_network.pod_cidr, now required on aws; egress through ens+): a pod's secondary ENI address never crosses an Internet Gateway. Measured on the aws sandbox, 2026-10-17: without masquerading a pod on a public node reached nothing; with it, its node's public IP, while a private pod left through the NAT Gateway; on the cluster security group a public node's NodePort answered from the internet; with its own group and the kubelet's label it was never a target and stayed closed. Refs #53.
Demmonius
force-pushed
the
issue-53-karpenter
branch
from
October 17, 2026 15:06
42809ab to
9ab26c5
Compare
Demmonius
marked this pull request as ready for review
October 17, 2026 15:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue #53. Four commits, rebased on
main(78ec831):feat(aws)), as Catalog: Karpenter as a socle module (aws) #53 asked when the boundary answer touches a foundations concern;feat(catalog));ci);feat(karpenter)).Proven on the aws sandbox (see Measured). Design:
docs/catalog/karpenter.md,docs/decisions/karpenter.md(KARPENTER-01…05),docs/decisions/aws.md(AWS-21, AWS-22, AWS-23; main took AWS-20).The blocking question: IAM under the boundary
Karpenter's controller needs
iam:PassRoleon its nodes' role; AWS's setup also lets it create and tag instance profiles. The Crossplane boundary deniesiam:*.EC2_LINUXaccess entry. The foundations give it an instance profile of the same name. The EC2NodeClasses name it withspec.instanceProfile, so Karpenter creates no profile and only reads it (GetInstanceProfile,ListInstanceProfiles). A profile, not a role: OpenTofu still creates no role for a module.ec2is incrossplane.allowed_services. It allowsiam:PassRoleon that one role, withiam:PassedToService = ec2.amazonaws.com, plus the two profile reads;iam:*stays denied everywhere else. Nothing in the foundations names Karpenter.CreateInstanceProfile, PassRole of any role it makes, andeks:CreateAccessEntryfor Crossplane.Roleunder/socle/<cluster>/. Its policy is AWS's own controller policy for 1.15.0, statement for statement (getting-started CloudFormation), minus the instance-profile writes and the zonal-shift read. The client allowsec2,ssm,pricing,sqsandeks; the aws root warns when one is missing.What
The
karpentercatalog module, aws only, off by default.oci/catalog/karpenter, stepsnamespace,workload,nodes:oci://public.ecr.aws/karpenter, CRDsCreateReplaceon install and upgrade;AWS_REGIONset, because pods cannot reach IMDS on one hop;eksControlPlane: true, because AL2023's nodeadm needs the service range.Two NodePools, always (KARPENTER-03), put in by the
karpenter-nodesKustomization's patches:private, the default: private subnets, egress through the NAT Gateway, the cluster security group;public, taintedsocle.do-now.io/node-pool=public:NoSchedule: public subnets, a public IP per node, egress straight through the Internet Gateway, so no NAT Gateway bill for traffic-heavy workloads. A pod asks for it with that toleration and anodeSelectoronsocle.do-now.io/node-pool.Both: Spot and Graviton first, families c/m/r gen 6+,
WhenEmptyOrUnderutilized, Cilium's startup taintnode.cilium.io/agent-not-ready, AL2023, IMDSv2 on one hop, an encrypted gp3 root volume.kube.karpenter.private_poolandpublic_pooltype each pool's capacity types, architectures, limits, consolidation delay and AMI alias, validated at plan. No new subnet tag.oci/clusters/aws/karpenter/patch.yaml, stepcloud:karpenter-access. It waits for the role, the association and the queue, then for asts get-caller-identityJob under that ServiceAccount to complete.Monitoring: Karpenter's capacity and performance dashboards (upstream, v1.15.0) and five socle rules:
The public pool, and what it took (AWS-23)
Each point below was found on the sandbox, not assumed.
enableIPv4Masquerade,ipv4NativeRoutingCIDRthe VPC's CIDR,egressMasqueradeInterfaces: ens+). Cilium refuses masquerading in ENI mode without the interfaces: the agents crashed. The aws root wires the VPC's CIDR ascluster_network.pod_cidr, now required on aws. Inside the VPC nothing changes; the NAT Gateway and the flow logs now see nodes, not pods.0.0.0.0/0in the security group of every node it targets. On the cluster security group, a public node's NodePort answered from the internet. The fix: the public nodes get their own security group (<cluster>-public-nodes: the VPC's CIDR in, nothing from the internet, not tagged for the cluster; the cluster security group admits it back. floci reports no cluster security group, so that one rule then lands on the group itself) and the labelnode.kubernetes.io/exclude-from-external-load-balancers.kubernetes.iolabels off the kubelet's flags, so the kubelet sets it at registration through an AL2023NodeConfigin the public EC2NodeClass'suserData. With both, a public node was never a target and its NodePort stayed closed.A Gateway's traffic reaches pods on public nodes through the private nodes.
Uninstall order (KARPENTER-04)
flux-operator 0.60.0 deletes a ResourceSet's objects in two stages (
deleteAllStaged): Flux objects first, waiting up to 60 s for them, then the rest. So:deletionPolicy: WaitForTermination;karpenter-controller.Nodes go before the controller. The docs still say to turn the module off and wait for
nodeclaimsto be empty beforetofu destroy, because of the 60 s cap.GCP
Not offered (KARPENTER-05). GKE Autopilot provisions, sizes and removes a node for every pod. There is no node pool to scale and no NAP to turn on, and Karpenter has no GCP provider.
tofu planrefuses the module on gcp; the docs, the README and the gcp status page say so.Outside the module, each generic
opentofu/awsec2; Crossplane may manage queues and EventBridge rules (and targets) under<cluster>-, never read or send a message; the public nodes' security group (the VPC in, anywhere out) and the cluster security group admitting it; outputsnode_instance_profile,vpc_cidr,public_nodes_security_group;tofu testasserts eachopentofu/bootstrapcluster_network.pod_cidrrequired on awsopentofu/clusters/awspod_cidrwired from the foundations; acheckwarning when a needed service is missing; the tfvars exampleoci/clusters/aws/crossplaneprovider-aws-sqsandprovider-aws-cloudwatcheventsv2.8.1; crossplane's e2e asserts them Healthy.github(cicommit)restore-keys, safe because a chart is cached under its URL, name and version: ECR Public 429s anonymous runner pulls; pull retries wait 20/40 s; module e2e jobs get 25 min (velero hit 20 with six providers)siteChecks run locally, on the rebased head (every
pr-static.yamlstep)tofu fmt,validate,tflintonopentofu/aws,opentofu/bootstrapandopentofu/clusters/aws.tofu test: 44/44 inopentofu/aws, 324/324 inopentofu/bootstrap.yamllint --strict,actionlint,check-catalog-clouds.sh,check-catalog-pages.sh(17 modules, no cycle),check-version.sh.vmalert -dryRun;upstream-monitoring.sh check.Measured
On the aws sandbox, eu-west-3, 2026-10-17, cluster
socle-sbx-53, applied from this branch's root. Two full cycles, the second on the final code: artifact0.0.0-issue-53-karpenter.42809ab, whoseoci/is the head's.ValidationSucceeded(Karpenter's dry-run RunInstances, so the PassRole works under the boundary),publicfinding its security group by name.karpenter-module-ekspassed (4 min 26 s):10.0.0.0/16;terminatedwithin a minute, then the controller went;tofu destroy: 61 destroyed. Nothing left: no cluster, VPC, instance, EIP, NLB, security group, instance profile, role, rule or log group.CI, on the head
9ab26c5: all 57 checks green, the 5protect-mainrequired checks included, and the 17 floci e2e jobs (karpenter (aws)androot (aws)among them).Fixed on the way:
nullnow).*Classfirst and waits for it whateverwait(fluxcd/pkgssa). floci's EC2NodeClass is never Ready, so the e2e sets the pools at enable.Decisions to confirm
ec2, not a karpenter flag. Any module role allowedec2could pass the nodes' role to EC2, and nothing more.Closes #53.