Files
spinifex/docs/terraform-workbooks/eks-https-ingress
Josh Siviour 67092d40be fix(workbooks): split EKS demo workloads into separate root module (#370)
The kubernetes provider in the eks-quickstart, eks-addons-access and
eks-https-ingress workbooks was configured from the managed cluster
resource (host = aws_eks_cluster.this.endpoint). On destroy the endpoint
resolved empty, the provider fell back to localhost:80, and refreshing
kubernetes_deployment_v1.hello / kubernetes_service_v1.hello failed with
'dial tcp [::1]:80: connect: connection refused'.

Move the kubernetes provider and the hello Deployment + Service into a
nested workloads/ root module with its own state. It reads cluster
identity from the parent via terraform_remote_state and the live
endpoint/CA from a data aws_eks_cluster source, so the provider is only
ever configured while the cluster exists. Apply parent then workloads;
destroy workloads then parent.
2026-06-16 21:48:56 +10:00
..

title, description, category, tags, resources
title description category tags resources
3-Node EKS behind an HTTPS ALB (ACM TLS) Provision an HA-shaped EKS cluster with public + private API endpoints and three workers, deploy a demo app, and publish it through an internet-facing ALB that terminates TLS with an ACM-imported certificate, using Terraform on Spinifex. Terraform Workbooks
terraform
eks
kubernetes
acm
tls
alb
elbv2
vpc
workbook
title url
Terraform AWS Provider https://registry.terraform.io/providers/hashicorp/aws/latest
title url
Terraform Kubernetes Provider https://registry.terraform.io/providers/hashicorp/kubernetes/latest
title url
Spinifex Repository https://github.com/mulgadc/spinifex
title url
OpenTofu https://opentofu.org/

Terraform: 3-Node EKS behind an HTTPS ALB

The largest EKS workbook, and the clearest demo. An HA-shaped cluster with public + private API endpoints and three workers, running a demo app published over HTTPS through an internet-facing ALB that terminates TLS with an ACM-imported certificate.

Table of Contents


Overview

This workbook is sized for a 3-node Spinifex deployment and is built to show off. It brings together the pieces from the smaller EKS examples, deploys a demo app across three workers, and fronts it with a TLS-terminating load balancer. When apply finishes, fetch the ALB's public IP and open https://<ip> — the nginxdemos/hello page reports which pod answered, and refreshing moves between the three replicas spread across the cluster. That single page demonstrates a multi-node Kubernetes cluster load-balancing behind HTTPS.

  • Public + private API endpoints. endpoint_public_access serves kubectl and the Kubernetes provider from your host (CIDR-restricted); endpoint_private_access exposes an in-VPC private endpoint the workers use to join.
  • Private workers + NAT. The three workers sit in private subnets. They join the cluster over the in-VPC private endpoint, and a NAT gateway gives them outbound access to pull the demo container image.
  • CoreDNS installed as a managed addon.
  • TLS via ACM. A self-signed certificate is generated with the tls provider and imported into ACM (Spinifex ACM supports ImportCertificate, not RequestCertificate). The ALB's HTTPS listener references the ACM ARN and forwards to the workers' NodePort.
  • The app, deployed by Terraform. The Kubernetes provider deploys the Deployment + NodePort Service, and one security-group rule lets the ALB reach the NodePort.

3-node EKS — public+private API endpoints, ALB in public subnets terminating TLS, three workers in private subnets behind a NAT gateway

What you'll learn:

  • Running an EKS cluster with both public and private API endpoints
  • Placing workers in private subnets that join over the private endpoint and egress via NAT
  • Importing a certificate into ACM and terminating TLS on an ALB HTTPS listener
  • Wiring an ALB target group to managed node-group workers via a NodePort, including the SG rule that allows it
  • Deploying a load-balanced workload with the Kubernetes provider

What gets created

Resource Name Purpose
VPC eks-https-vpc Isolated network (10.32.0.0/16)
Public Subnets eks-https-public-a/-b Host the ALB and NAT gateway
Private Subnets eks-https-private-a/-b Host the workers
Internet Gateway eks-https-igw Internet for the public subnets
NAT Gateway (+ EIP) eks-https-nat Outbound for the private workers (image pulls)
Route Tables eks-https-public-rt, eks-https-private-rt Public via IGW; private via NAT
IAM Roles eks-https-cluster-role, eks-https-node-role Control-plane and worker roles
EKS Cluster eks-https Public (CIDR-restricted) + private endpoints
Node Group workers Three t3.large workers in the private subnets
Addon coredns Cluster DNS
ACM Cert eks-https-ingress Self-signed cert imported into ACM
ALB + HTTPS Listener eks-https-alb Internet-facing, terminates TLS on :443
Target Group eks-https-tg Instance targets on the NodePort
SG Ingress Rule eks-https-alb-nodeport Lets the ALB SG reach the worker NodePort
K8s Deployment + Service hello nginxdemos/hello, 3 replicas, NodePort

Spinifex specifics

  • ACM supports ImportCertificate, DescribeCertificate, ListCertificates, DeleteCertificate — there is no RequestCertificate/DNS-validation flow, hence the self-signed import.
  • The ALB HTTPS listener resolves the cert from ACM. Valid SSL policies are ELBSecurityPolicy-2016-08 (default) and ELBSecurityPolicy-TLS13-1-2-2021-06.
  • The worker SG is auto-managed (vpc_config.security_group_ids is ignored) and admits only intra-cluster traffic. The ALB→NodePort rule is added by looking the SG up by its deterministic name and admitting the ALB SG.
  • Worker discovery uses the spinifex:eks-cluster tag Spinifex stamps on node-group instances; the target-group attachment count is the known node_desired_size.
  • Leave addon_version unset (see the addons workbook for why pinning it can't satisfy both the provider and the catalog).

Prerequisites:

  • A 3-node Spinifex deployment, installed and running
  • The Spinifex eks-node image available on the cluster
  • OpenTofu or Terraform, plus kubectl and the AWS CLI

Instructions

Step 1. Get the Template

git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
cd spinifex-tf
git sparse-checkout set docs/terraform-workbooks
cd docs/terraform-workbooks/eks-https-ingress

Or create a main.tf file and paste the full configuration below.

Step 2. Deploy

The cluster (with its ALB and ACM cert) and the demo app are two root modules with separate state. Apply the cluster first:

export AWS_PROFILE=spinifex
tofu init
tofu apply

Expect several minutes: the control plane bootstraps, three workers launch and join over the private endpoint, the NAT gateway comes up, CoreDNS installs, and the ALB is provisioned. Then deploy the demo app from the nested workloads/ module:

cd workloads
tofu init
tofu apply

The ALB target group already points at the workers' NodePort; once these pods are running the targets turn healthy.

Why two modules? The Kubernetes provider in workloads/ reads the cluster endpoint from a live data "aws_eks_cluster" source, so it's only ever configured while the cluster exists. Keeping it out of the cluster module means destroy never tries to refresh a workload against a cluster that's already gone — the failure mode where the provider falls back to http://localhost:80 and reports connection refused. Always destroy workloads/ before the cluster.

Keep AWS_PROFILE=spinifex exported for both applies — the Kubernetes provider authenticates with aws eks get-token against the Spinifex STS endpoint.

Tighten access in production. api_public_access_cidr and alb_ingress_cidr both default to 0.0.0.0/0. Set them to your own CIDR:

export TF_VAR_api_public_access_cidr="203.0.113.10/32"
export TF_VAR_alb_ingress_cidr="203.0.113.10/32"

Step 3. Open the Demo over HTTPS

The ALB DNS name (*.elb.spinifex.local) won't resolve from your host, so fetch its public IP:

ALB_IP=$(aws elbv2 describe-load-balancers --names eks-https-alb \
  --query 'LoadBalancers[0].AvailabilityZones[].LoadBalancerAddresses[].IpAddress' \
  --output text)

The certificate is self-signed, so a browser will warn (proceed anyway) and curl needs -k:

curl -k https://$ALB_IP
curl -k https://$ALB_IP   # refresh: the "Server name" changes between pods

Open https://$ALB_IP in a browser and refresh to watch requests land on different pods across the three nodes.

Step 4. Inspect (optional)

aws eks update-kubeconfig --name eks-https --region ap-southeast-2
kubectl get nodes
kubectl get pods -o wide          # three replicas, one per node

TG_ARN=$(aws elbv2 describe-target-groups --names eks-https-tg \
  --query 'TargetGroups[0].TargetGroupArn' --output text)
aws elbv2 describe-target-health --target-group-arn "$TG_ARN"

Cleanup

Destroy in reverse — the demo app first (while the cluster is still up), then the cluster:

cd workloads
tofu destroy
cd ..
tofu destroy

Troubleshooting

Targets Unhealthy / ALB Returns 5xx

The ALB health-checks / on the NodePort. Targets turn healthy once the demo pods are running and the SG rule admits the ALB. Check, in order:

kubectl get pods -o wide                                  # pods Running?
aws ec2 describe-security-groups \
  --filters "Name=group-name,Values=eks-cluster-eks-https-nodegroup-sg" \
  --query 'SecurityGroups[0].IpPermissions'               # NodePort rule present?
aws elbv2 describe-target-health --target-group-arn "$TG_ARN"

If pods are ImagePullBackOff, the workers can't reach Docker Hub — confirm the NAT gateway is available:

aws ec2 describe-nat-gateways --query 'NatGateways[].[NatGatewayId,State]'

Workers Never Join

The workers join over the private endpoint. Confirm it's enabled and the node group is progressing:

aws eks describe-cluster --name eks-https --query 'cluster.resourcesVpcConfig.endpointPrivateAccess'
aws eks describe-nodegroup --cluster-name eks-https --nodegroup-name workers --query 'nodegroup.status'

Target-Group Attachment Errors at Apply

The attachment indexes the discovered worker IDs by node_desired_size. If apply errors with an index out of range, the workers weren't all tagged/running when the aws_instances lookup ran. Re-run tofu apply — the node group will be ACTIVE by then:

aws ec2 describe-instances --filters "Name=tag:spinifex:eks-cluster,Values=eks-https" \
  --query 'Reservations[].Instances[].[InstanceId,State.Name]' --output text

kubernetes provider: connection refused / Unauthorized

The provider runs aws eks get-token against the Spinifex STS endpoint. If the cluster was still CREATING when the provider first connected, re-run tofu apply. Otherwise confirm:

aws sts get-caller-identity
aws eks describe-cluster --name eks-https --query 'cluster.status'

ACM Import / HTTPS Listener Errors

aws acm list-certificates
aws acm describe-certificate --certificate-arn "$(tofu output -raw certificate_arn)" \
  --query 'Certificate.Status'

A CertificateNotFound on the listener means the ACM ARN didn't resolve — check the cert exists and belongs to the same account/profile.

Provider Connection Refused

sudo systemctl status spinifex.target
curl -k https://localhost:9999/