Expose Your Application
Your application is running on the cluster (see Deploy Your Application). Now you need users to reach it.
This golden path takes you from a running pod to a publicly accessible, TLS-secured application with automatic DNS and TLS management.
By the end of this path, your application will be:
- Reachable at
https://your-domain.example.com - Secured with a TLS certificate from Let’s Encrypt (auto-renewed)
- DNS records created and managed automatically
- HTTP traffic redirected to HTTPS by default
How it works
Three components work together, all managed by Skyscrapers on the platform side:
| Component | What it does |
|---|---|
| Traefik (or AWS Load Balancer Controller) | Routes external traffic to your pods |
| cert-manager | Issues and renews TLS certificates via Let’s Encrypt |
| ExternalDNS | Creates DNS records in Route53 automatically from your Ingress |
You create an Ingress resource. The rest happens automatically.
Choose your ingress controller
The platform supports two ingress controllers. Choose based on your needs:
Use Traefik (recommended for most cases) when you need:
- CORS headers at the ingress level
- Rate limiting without additional AWS costs
- Complex proxy timeout or buffering requirements
- Cookie-based session affinity
- Middleware composition (chain multiple behaviors)
Use AWS Load Balancer Controller when you need:
- Native OIDC or Cognito authentication at the load balancer
- AWS WAF, Shield, or GuardDuty integration
- An AWS-managed service with SLA guarantees
- An origin for CloudFront, when you’re already using CloudFront in front of your application
For a full comparison and decision guidance, see the Ingress overview.
Note
ingress-nginx is sunsetting. Do not use it for new ingresses. If you’re currently using it, see the Ingress overview for migration guidance.
The simple path: Traefik with automatic TLS and DNS
This is the golden path, one Ingress resource gives you TLS, DNS, and HTTPS redirection automatically.
Step 1: Add an Ingress to your Helm chart
Add templates/ingress.yaml to your Helm chart:
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType | default "Prefix" }}
backend:
service:
name: {{ include "my-app.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
tls:
- secretName: {{ include "my-app.fullname" . }}-tls
hosts:
{{- range .Values.ingress.hosts }}
- {{ .host | quote }}
{{- end }}
{{- end }}Step 2: Add ingress values
In your Helm chart’s values.yaml, add the defaults:
ingress:
enabled: false
className: traefik
annotations: {}
hosts: []Then in your environment’s helmrelease-patch.yaml, enable and configure it:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: my-app
namespace: flux-apps
spec:
values:
ingress:
enabled: true
className: traefik
hosts:
- host: my-app.example.com
paths:
- path: /
pathType: PrefixStep 3: Push and verify
Push your changes. Flux deploys the Ingress, then:
- cert-manager sees the
cert-manager.io/cluster-issuer: letsencrypt-prodannotation and requests a certificate from Let’s Encrypt using DNS-01 validation via Route53 - External DNS sees the Ingress hostname and creates an A record in Route53 pointing to the Traefik load balancer
- Traefik routes traffic to your pods and automatically redirects HTTP to HTTPS
Verify:
# Check the Ingress is created
kubectl get ingress -n flux-apps
# Check the certificate was issued
kubectl get certificate -n flux-apps
# If the certificate is stuck, check challenges
kubectl get challenges -n flux-apps
# Test it
curl -I https://my-app.example.comCertificate issuance typically takes 1-2 minutes. DNS propagation may take a few minutes more.
That’s it for the standard case. The rest of this page covers common variations.
Internal-only access
For services that should only be reachable within the VPC (not from the internet), use the internal Traefik instance:
ingress:
enabled: true
className: traefik-internal
hosts:
- host: my-app-internal.example.com
paths:
- path: /
pathType: PrefixNote
Internal ingresses still get public DNS records (resolving to private IPs). The certificate will appear in public Certificate Transparency logs. This is expected behavior, the service is still only reachable from within the VPC.
Using the AWS Load Balancer Controller instead
If you need AWS-native load balancing (OIDC auth, WAF, etc.), use the ALB controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:123456789:certificate/abc-123
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
spec:
ingressClassName: alb
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80Important
The ACM certificate referenced in alb.ingress.kubernetes.io/certificate-arn must exist before you create the Ingress. Unlike the Traefik path, certificates are not issued automatically: set it up beforehand via IaC (Terraform) or ask your Skyscrapers customer lead.
Key differences from Traefik:
| Traefik | ALB Controller | |
|---|---|---|
| TLS certificates | cert-manager (Let’s Encrypt, auto-renewed) | ACM (AWS Certificate Manager, via Terraform, ask your customer lead in Skyscrapers) |
| IngressClassName | traefik or traefik-internal | alb |
| Target routing | Via Traefik proxy pods | Direct to pod IPs (target-type: ip) |
| Cost | Included in platform | Per ALB + LCU usage |
| CORS | Native middleware | Requires CloudFront or app-level handling |
| Rate limiting | Native middleware | Requires AWS WAF |
Important
When using ALB, you can group multiple Ingress resources into a single ALB to reduce costs:
annotations:
alb.ingress.kubernetes.io/group.name: shared-alb
alb.ingress.kubernetes.io/group.order: "1"For the full ALB configuration reference, see ALB Ingress.
Common Traefik patterns
Traefik handles CORS, rate limiting, redirects, and similar behaviors through Middlewares: separate Kubernetes resources that you create in your namespace and reference from your Ingress via an annotation:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: my-namespace-cors@kubernetescrdThe reference format is <namespace>-<name>@kubernetescrd. You can chain multiple middlewares by comma-separating them; they execute in order.
All patterns are documented with full examples in the Traefik reference:
- CORS headers
- Rate limiting
- IP allowlisting
- Basic authentication
- OAuth / OIDC protection via the platform-provided OAuth2-Proxy
- Redirects
- URL rewriting
- Session affinity (configured on the Service, not the Ingress)
- Custom TLS configuration
- Timeouts and buffering
Delegated domains
If your domain’s DNS is hosted externally (not in the same AWS account), cert-manager can still issue certificates. Create a CNAME in your external DNS:
_acme-challenge.api.example.com IN CNAME _acme-challenge.api.production.eks.example.org.Where production.eks.example.org is the cluster’s domain. Then create the Ingress as normal, cert-manager follows the CNAME to validate ownership.
Wildcard certificates
For multiple subdomains under the same domain, use a wildcard certificate to avoid hitting Let’s Encrypt rate limits:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: flux-apps
spec:
secretName: wildcard-example-com-tls
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod
dnsNames:
- "example.com"
- "*.example.com"Then reference the same secret across multiple Ingresses:
tls:
- secretName: wildcard-example-com-tls
hosts:
- "*.example.com"Note
A TLS Secret is scoped to a single namespace. To use the same wildcard in another namespace, cert-manager will request a new certificate.
Disabling external-dns
If you’re using an external CDN (CloudFront, Cloudflare) and manage DNS yourself, disable automatic DNS record creation:
annotations:
external-dns.alpha.kubernetes.io/exclude: "true"Disabling HTTPS redirection
By default, Traefik redirects all HTTP traffic to HTTPS. If you need to serve plain HTTP (rare), create an IngressRoute with a higher priority than the default redirection rule, see Disable automatic HTTPS redirection.
Troubleshooting
Certificate not issuing
# Check certificate status
kubectl describe certificate my-app-tls -n flux-apps
# Check if challenges are stuck
kubectl get challenges -n flux-apps
# Check cert-manager logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-managerWarning
cert-manager has a limit of 60 parallel challenges. If this fills up with unprocessable challenges (DNS misconfiguration), further certificate issuance is blocked. Monitor with kubectl get challenges -A.
DNS not resolving
# Check if external-dns created the record
kubectl logs -n infrastructure -l app.kubernetes.io/name=external-dns | grep my-app.example.com
# Verify the record in Route53
aws route53 list-resource-record-sets --hosted-zone-id ZXXXXX | grep my-appIngress not routing traffic
# Check the Ingress is created and has an address
kubectl get ingress -n flux-apps
# Check Traefik is seeing the Ingress
kubectl logs -n traefik -l app.kubernetes.io/name=traefik | grep my-appFurther reading
- Ingress overview, Full comparison of Traefik vs ALB with migration guidance
- Traefik Ingress, Complete Traefik reference
- ALB Ingress, Complete ALB reference
- cert-manager, cert-manager configuration details