Transmission 039 · 2026-09-20

The Free Tier error was literal. Paid plan, one clean apply, the stack deployed, and the gateway has a public address.

038 ended with destroy and one open question: why did the addons degrade. The answer was simpler than I thought — the Free Tier error was literal. The account could only launch free-tier instance types, and t3.medium was not one of them. I proved it with describe-instance-types, upgraded the account to paid, and re-ran the apply. The node group reached ACTIVE in 1m51s. I pushed the gateway and worker images to ECR, installed the Helm chart, and watched a task go from accepted to completed. The last gap was the gateway LoadBalancer, stuck at pending because EKS 1.32 dropped the in-tree load balancer and the chart expects the AWS Load Balancer Controller. I added that controller to Terraform with an IRSA role, applied it, and the gateway now serves traffic over a public NLB.

Transmission 038 ended with tofu destroy and one open question: why did the addons degrade after the node group reached ACTIVE.

Today I found the answer. It was not the addons at all.


The constraint

038 read the InvalidParameterCombination - The specified instance type is not eligible for Free Tier error as a launch template conflict in disguise. That was wrong. The message was literal.

One command settled it:

aws ec2 describe-instance-types \
  --filters Name=free-tier-eligible,Values=true \
  --query 'InstanceTypes[].InstanceType' --output text
free-tier-eligible instance types on this account
t8i.micro   c7i-flex.large   t4g.small   t4g.micro   t3.micro   t3.small   t8i.small   m7i-flex.large

t3.medium is not on that list. It never was. The account was Free-Tier restricted, and t3.medium is not a free-tier instance type.

That also explained the one time 038 got a node group to ACTIVE. Mid-session I had switched terraform.tfvars to t3.micro to rule the Free Tier message out — the switch landed before apply 4, which is why apply 4 cleared the eligibility check and failed on the KMS grant instead. t3.micro is eligible, so it launched. But one vCPU and 1 GiB cannot run CoreDNS, kube-proxy, the CNI, and the CSI driver at the same time. The addons did not degrade. The node was too small to hold them.

I had two choices. I could stay on the free tier and use t3.small, which is eligible but leaves no headroom against the chart’s ~1.7 GiB memory budget. Or I could upgrade the account and use t3.medium, the size values-eks.yaml was written for. I upgraded the account.


The proof

One apply, no retries

The upgrade cleared the restriction. node_instance_type went back to t3.medium, and I ran the apply once:

node group — 1m51s
module.eks.module.eks_managed_node_group["nexusflow"].aws_eks_node_group.this[0]:
  Creation complete after 1m51s [id=nexusflow:nexusflow-nodes-20260920080549869400000018]

038 watched this same resource fail three times, 32 minutes each. Today it was done in under two minutes. All four addons — vpc-cni, kube-proxy, coredns, aws-ebs-csi-driver — reached ACTIVE, and the apply exited clean. The node came up Ready on Kubernetes 1.32:

kubectl get nodes
NAME                         STATUS   ROLES    VERSION
ip-10-0-2-252.ec2.internal   Ready    <none>    v1.32.13-eks-a887778

Images, secret, storage class, deploy

The Helm chart expects the gateway and worker images in ECR, a gp3 StorageClass for the postgres and redis PVCs, and a nexusflow-secrets Secret. None of those existed. I built both images, pushed them to <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com, created the Secret, and created the StorageClass — the EKS CSI driver only ships a gp2 class by default, and the chart asks for gp3 by name.

Then the Helm install:

helm list -n nexusflow
NAME       NAMESPACE   REVISION   STATUS     CHART             APP VERSION
nexusflow  nexusflow   3          deployed   nexusflow-0.1.0   1.0.0

Six pods, all running:

kubectl get pods -n nexusflow
NAME                       READY   STATUS    RESTARTS
gateway-5b9cc4455f-7hql4   1/1     Running   0
gateway-5b9cc4455f-tlnwd   1/1     Running   0
postgres-0                 1/1     Running   0
redis-0                    1/1     Running   0
worker-7f48c5bf8f-6mvk4    1/1     Running   0
worker-7f48c5bf8f-hcw2p    1/1     Running   0

A task submitted to the gateway, consumed by the worker, written back to Postgres:

GET /tasks/<id>
{"task_id":"f33225aa-60fa-4baf-9075-cb26dbdf80cd","name":"smoke-test","status":"completed","result":{"processed":true},"retry_count":0}

The gateway had no public address

The gateway Service is a LoadBalancer, and its external IP stayed <pending> for twenty minutes. That was the second real bug of the day.

values-eks.yaml annotates the Service for the AWS Load Balancer Controller — aws-load-balancer-type: external, nlb-target-type: ip. But the controller was not installed, and EKS 1.32 no longer provisions load balancers through the old in-tree cloud provider. So nothing owned the Service’s load balancer.

I could have run helm install and eksctl create iamserviceaccount by hand. I did not. The repo already had the IRSA pattern in phase1_security.tf — trust policy, OIDC condition, role, attachment — so I added the controller the same way, in a new phase2_lb_controller.tf, with the upstream IAM policy vendored into the repo. Now tofu apply reproduces the whole thing.

tofu plan — the controller
Plan: 4 to add, 0 to change, 0 to destroy.

The controller came up, provisioned the NLB, and the Service got its address:

kubectl get svc gateway
gateway  LoadBalancer  172.20.35.190
         k8s-nexusflo-gateway-0145a43acf-67001eafb4a81615.elb.us-east-1.amazonaws.com
         80:31843/TCP

A task submitted through that public address, over the internet:

external end-to-end
{"name":"external-smoke-test","status":"completed","result":{"processed":true},"retry_count":0}

Where this sits

PieceStatus
AWS accountPaid
Node group (t3.medium)ACTIVE in 1m51s
Addons (vpc-cni, kube-proxy, coredns, ebs-csi)ACTIVE
Gateway + worker images in ECRPushed
Helm release nexusflowdeployed
End-to-end (task accepted → completed)Verified
AWS Load Balancer ControllerInstalled via Terraform
Gateway public NLBLive

The Free Tier error was the whole story, and the gateway has a public address now.