Stackvora.tech

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 LoadBalancer service type is usually preferred. The cloud provider will automatically provision an external load balancer.
  • On-premises/Bare-metal: You might use NodePort in conjunction with an external load balancer (e.g., HAProxy, F5) or a bare-metal load balancer solution like MetalLB. For testing, HostPort can 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 tells cert-manager to use our defined ClusterIssuer to get a certificate for the specified hosts.
  • spec.ingressClassName: nginx: Ensures this Ingress resource is handled by the ingress-nginx controller.
  • spec.tls: Defines the hosts for which TLS should be enabled and the Kubernetes Secret where the certificate will be stored. cert-manager will create and manage this secret.
  • spec.rules.host: The domain name for which this Ingress rule applies. Make sure this matches the host in your tls section.
  • nginx.ingress.kubernetes.io/force-ssl-redirect: "true": An ingress-nginx specific 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 dig or nslookup to verify your domain resolves to the ingress controller's IP.
  • External IP Pending: On cloud providers, the ingress-nginx-controller service might take a few minutes to get an external IP address. Keep running kubectl get svc -n ingress-nginx ingress-nginx-controller until it appears.
  • ClusterIssuer Not Ready: Ensure your email address in the ClusterIssuer is valid. Check kubectl describe clusterissuer letsencrypt-prod for 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 Certificate resource remains in a "Pending" state, check the associated Order and Challenge resources:
    kubectl get challenges
    kubectl describe challenge <challenge-name>
    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., incorrect ingressClassName or firewall issues), or DNS not being correctly pointed to the ingress controller.
  • ingressClassName Mismatch: Ensure the ingressClassName: nginx in your Ingress resource matches the controller you've deployed.
  • Service Not Found: Double-check the service.name and service.port.number in your Ingress backend matches your actual Kubernetes Service.
  • Nginx Controller Logs: If requests are not reaching your application, check the logs of the ingress-nginx controller pods:
    kubectl logs -f -n ingress-nginx ingress-nginx-controller-<pod-hash>
    This can reveal configuration errors, routing issues, or upstream connectivity problems.
  • 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.

Back to the knowledge base · Ask the AI assistant