Kubernetes: expose a service with ingress-nginx and TLS
Exposing applications running within a Kubernetes cluster to external traffic is a fundamental requirement for most deployments. While Kubernetes Services…
Exposing applications running within a Kubernetes cluster to external traffic is a fundamental requirement for most deployments. While Kubernetes Services can expose applications via NodePort or LoadBalancer types, Ingress provides a more sophisticated solution, offering HTTP routing, TLS termination, and name-based virtual hosting. This article details how to set up the popular ingress-nginx controller, configure Ingress resources, and secure traffic with TLS using cert-manager and Let's Encrypt.
We'll focus on a common setup where ingress-nginx acts as the entry point, routing requests to internal services, and cert-manager automates the acquisition and renewal of TLS certificates from Let's Encrypt.
1. Deploying the ingress-nginx Controller
The ingress-nginx controller is a robust Ingress implementation built around the NGINX web server. It operates by watching the Kubernetes API for Ingress resources and then configuring NGINX accordingly.
1.1. Add the Helm Repository
First, add the official ingress-nginx Helm chart repository:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
1.2. Install the Controller
Install the controller into its own namespace, typically ingress-nginx. The exact service type for the controller will depend on your Kubernetes environment:
- Cloud Providers (AWS EKS, GCP GKE, Azure AKS): A
LoadBalancerservice type is usually preferred. The cloud provider will automatically provision an external load balancer. - On-premises/Bare-metal: You might use
NodePortin conjunction with an external load balancer (e.g., HAProxy, F5) or a bare-metal load balancer solution like MetalLB. For testing,HostPortcan also be used, but it's not recommended for production.
For a cloud environment, the default installation uses LoadBalancer:
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.service.externalTrafficPolicy=Local \
--version 4.10.0 # Specify a recent, stable version
The --set controller.service.externalTrafficPolicy=Local flag is recommended to preserve the client source IP address when running on cloud providers, which can be useful for logging and security. Check the official chart documentation for the latest recommended version and values.
Verify the deployment and get the external IP/hostname:
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx ingress-nginx-controller
Wait until the ingress-nginx-controller service shows an external IP address or hostname. This is the address you'll point your domain's A record (or CNAME) to.
2. Deploying cert-manager for TLS
cert-manager automates the management and issuance of TLS certificates from various sources, including Let's Encrypt. It will handle certificate provisioning and renewal directly within your Kubernetes cluster.
2.1. Install cert-manager
Install cert-manager using Helm. It's often installed in its own namespace, cert-manager.
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.5 \
--set installCRDs=true
The --set installCRDs=true flag is crucial as cert-manager relies on Custom Resource Definitions (CRDs).
Verify the installation:
kubectl get pods -n cert-manager
2.2. Configure a ClusterIssuer
A ClusterIssuer (or Issuer) resource tells cert-manager how to obtain certificates. We'll use Let's Encrypt with the HTTP-01 challenge, which requires cert-manager to create a temporary pod and Ingress rule to prove domain ownership.
Create a file named letsencrypt-clusterissuer.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# You must replace this with your email address
email: your-email@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource that will be used to store the ACME account's private key.
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginx
For testing, you can use the Let's Encrypt staging server to avoid rate limits:
server: https://acme-staging-v02.api.letsencrypt.org/directory
Apply the ClusterIssuer:
kubectl apply -f letsencrypt-clusterissuer.yaml
Check its status:
kubectl describe clusterissuer letsencrypt-prod
It should eventually show Ready: True.
3. Defining Your Application and Service
Before creating an Ingress, you need a Kubernetes Deployment (or similar workload) and a Service to expose it internally.
Let's assume a simple NGINX deployment and service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
labels:
app: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: nginx-container
image: nginx:1.25.3-alpine # Specify a recent, stable NGINX image
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
Apply these resources:
kubectl apply -f app-deployment-service.yaml
4. Creating the Ingress Resource with TLS
Now, define the Ingress resource that uses the ingress-nginx controller and requests a TLS certificate from cert-manager.
Create a file named app-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
# Use the ClusterIssuer we created
cert-manager.io/cluster-issuer: letsencrypt-prod
# Recommended ingress-nginx annotation for redirecting HTTP to HTTPS
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx # This specifies which Ingress controller to use
tls:
- hosts:
- app.example.com # Replace with your domain
secretName: app-tls-secret # cert-manager will store the TLS certificate here
rules:
- host: app.example.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service # Name of your internal Service
port:
number: 80 # Port your internal Service exposes
Key points:
metadata.annotations.cert-manager.io/cluster-issuer: letsencrypt-prod: This tellscert-managerto use our definedClusterIssuerto get a certificate for the specified hosts.spec.ingressClassName: nginx: Ensures this Ingress resource is handled by theingress-nginxcontroller.spec.tls: Defines the hosts for which TLS should be enabled and the Kubernetes Secret where the certificate will be stored.cert-managerwill create and manage this secret.spec.rules.host: The domain name for which this Ingress rule applies. Make sure this matches the host in yourtlssection.nginx.ingress.kubernetes.io/force-ssl-redirect: "true": Aningress-nginxspecific annotation that automatically redirects all HTTP traffic to HTTPS.
Apply the Ingress resource:
kubectl apply -f app-ingress.yaml
5. Verifying TLS Certificate Issuance
After creating the Ingress, cert-manager will detect the request for a certificate. It will create a CertificateRequest, an Order, and then an Challenge resource.
Monitor the process:
kubectl get certificate -w
kubectl describe certificate app-tls-secret
Once the certificate is issued, the Certificate status will show Ready: True, and a Kubernetes Secret named app-tls-secret will be created containing the TLS certificate and private key.
kubectl get secret app-tls-secret -o yaml
You should see tls.crt and tls.key data encoded in base64.
6. DNS Configuration
The final step is to configure your domain's DNS. You need to create an A record (or CNAME record) that points your application's hostname (e.g., app.example.com) to the external IP address or hostname of your ingress-nginx-controller service.
# Example DNS A record configuration
# Host Type Value
# app.example.com A XXX.XXX.XXX.XXX (external IP of ingress-nginx-controller service)
Allow some time for DNS propagation. Once propagated, you should be able to access your application securely via https://app.example.com in your browser.
Common Pitfalls and Troubleshooting
- DNS Not Propagated: If you get "DNS_PROBE_FINISHED_NXDOMAIN" or similar, your DNS records haven't updated yet. Use
digornslookupto verify your domain resolves to the ingress controller's IP. - External IP Pending: On cloud providers, the
ingress-nginx-controllerservice might take a few minutes to get an external IP address. Keep runningkubectl get svc -n ingress-nginx ingress-nginx-controlleruntil it appears. ClusterIssuerNot Ready: Ensure your email address in theClusterIssueris valid. Checkkubectl describe clusterissuer letsencrypt-prodfor specific error messages. Common issues include incorrect ACME server URLs or network connectivity problems from your cluster to Let's Encrypt.- Certificate Not Issued: If the
Certificateresource remains in a "Pending" state, check the associatedOrderandChallengeresources:
Look for events or conditions indicating why the HTTP-01 challenge failed. This is often due to the ingress controller not being able to route the challenge request (e.g., incorrectkubectl get challenges kubectl describe challenge <challenge-name>ingressClassNameor firewall issues), or DNS not being correctly pointed to the ingress controller. ingressClassNameMismatch: Ensure theingressClassName: nginxin your Ingress resource matches the controller you've deployed.- Service Not Found: Double-check the
service.nameandservice.port.numberin your Ingress backend matches your actual Kubernetes Service. - Nginx Controller Logs: If requests are not reaching your application, check the logs of the
ingress-nginxcontroller pods:
This can reveal configuration errors, routing issues, or upstream connectivity problems.kubectl logs -f -n ingress-nginx ingress-nginx-controller-<pod-hash> - Firewall Rules: Ensure that any cloud provider or on-premises firewall allows incoming traffic on ports 80 and 443 to the external IP of your
ingress-nginx-controller.