Expose HTTP applications with NGINX Ingress

When you deploy HTTP applications in Kubernetes, you need a way to expose them to the internet with proper routing, domain names, and secure connections. While Cloudfleet provides robust L4 (TCP/UDP) load balancing out of the box through LoadBalancer services, HTTP applications require L7 routing capabilities like host-based routing, path-based routing, and TLS termination.

Cloudfleet gives you the flexibility to choose any ingress controller that fits your needs. Popular options include NGINX Ingress Controller, Traefik, Istio, and many others. This tutorial focuses on NGINX Ingress Controller, one of the most widely used ingress controllers, which sits on top of Cloudfleet’s L4 load balancer and routes incoming HTTP/HTTPS traffic to your applications based on hostnames and paths.

When combined with cert-manager, which automatically provisions and renews TLS certificates from Let’s Encrypt, you get a complete solution for exposing HTTP applications securely to the internet.

In this tutorial, you’ll install both components on your CFKE cluster and deploy a sample HTTP application that’s automatically accessible via HTTPS with a valid TLS certificate. For a service mesh approach with advanced traffic management features, see the Install Istio tutorial.

Prerequisites

Before getting started, ensure you have:

  • A Cloudfleet account. If you don’t have one, you can sign up for free.
  • A running Kubernetes cluster on CFKE, potentially with a Fleet configured for a provider.
  • kubectl installed and configured to connect to your CFKE cluster.
  • helm installed on your local machine (version 3.x or later).
  • A domain name that you can configure DNS records for (required for Let’s Encrypt validation).

Step 1: Install NGINX Ingress Controller

First, add the NGINX Ingress Controller Helm repository and install it:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

Create a nginx-values.yaml file to configure the ingress controller for optimal performance on Cloudfleet:

global:
  image:
    # Use Cloudfleet's Quay mirror for improved reliability
    # By default, these images are pulled from registry.k8s.io which is not recommended for production use
    # Cloudfleet provides a mirror of these images for convenience
    registry: quay.io/cloudfleet

controller:
  # Watch Ingress resources without the ingressClassName field
  watchIngressWithoutClass: true

  image:
    image: ingress-nginx-controller

  admissionWebhooks:
    patch:
      image:
        image: kube-webhook-certgen

  service:
    # Optimize for direct routing to avoid extra network hops
    externalTrafficPolicy: Local

# Optional: Pin the controller to specific provider/region for cost optimization
# Uncomment and adjust based on your fleet configuration
# nodeSelector:
#   cfke.io/provider: hetzner
#   topology.kubernetes.io/region: nbg1

Why this configuration matters on Cloudfleet:

  • Cloudfleet Quay mirror (quay.io/cloudfleet): By default, these images are pulled from registry.k8s.io, which is not recommended for production use. Cloudfleet provides a mirror of these images for improved reliability and convenience. This ensures consistent image availability and faster pulls.
  • Watch ingress without class (watchIngressWithoutClass: true): In Kubernetes 1.18+, Ingress resources can specify which controller should handle them using the ingressClassName field or the kubernetes.io/ingress.class annotation. By default, NGINX Ingress Controller only processes Ingress resources that explicitly reference it. Setting watchIngressWithoutClass: true makes the controller also handle Ingress resources that don’t specify any controller at all. This is useful when you have legacy configurations or when NGINX is the only ingress controller in your cluster, allowing you to skip the class annotation on every Ingress resource.
  • External traffic policy (externalTrafficPolicy: Local): This ensures incoming traffic is routed directly to a pod on the node that received it, preventing extra network hops. See the load balancing documentation to learn how this works in Cloudfleet’s multi-cloud architecture.
  • Provider-specific scheduling (nodeSelector, optional): If you want to optimize for cost and data locality, you can pin the ingress controller to run only on nodes from a specific cloud provider and region. For example, to deploy on Hetzner’s Nuremberg region, uncomment the nodeSelector and set cfke.io/provider: hetzner and topology.kubernetes.io/region: nbg1. This works with any supported cloud provider (AWS, GCP, Azure, etc.) or self-managed nodes. If omitted, the controller will be scheduled across all available nodes in your cluster.

Install the NGINX Ingress Controller:

helm install ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx \
  --create-namespace \
  -f nginx-values.yaml

Wait for the load balancer to be provisioned:

kubectl get svc -n ingress-nginx ingress-nginx-controller --watch

Once you see an EXTERNAL-IP assigned (this may take 1-2 minutes), note the IP address. You’ll use it to configure your DNS records.

Step 2: Configure DNS for your domain

For Let’s Encrypt to validate your domain and issue certificates, you need to create a DNS A record pointing to the ingress controller’s external IP.

If your external IP is 203.0.113.10 and your domain is example.com, create these DNS records:

example.com.        A     203.0.113.10
*.example.com.      A     203.0.113.10

The wildcard record allows you to use subdomains like app.example.com without creating individual DNS entries.

DNS propagation can take a few minutes. You can verify the records with:

dig example.com +short
dig app.example.com +short

Step 3: Install cert-manager

Add the cert-manager Helm repository:

helm repo add jetstack https://charts.jetstack.io
helm repo update

Install cert-manager with the required Custom Resource Definitions (CRDs):

helm install cert-manager jetstack/cert-manager \
  -n cert-manager \
  --create-namespace \
  --set crds.enabled=true

Verify the installation by checking that all cert-manager pods are running:

kubectl get pods -n cert-manager

You should see three pods: cert-manager, cert-manager-webhook, and cert-manager-cainjector, all in the Running state.

Step 4: Create a Let’s Encrypt ClusterIssuer

A ClusterIssuer defines how cert-manager should obtain certificates. You’ll create one for Let’s Encrypt’s production environment using HTTP-01 validation.

Create a file named letsencrypt-issuer.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Let's Encrypt production server
    server: https://acme-v02.api.letsencrypt.org/directory

    # Email for ACME account registration (replace with your email)
    email: your-email@example.com

    # Store the ACME account private key in this secret
    privateKeySecretRef:
      name: letsencrypt-prod

    # Use HTTP-01 challenge
    solvers:
    - http01:
        ingress:
          class: nginx

Important: Replace your-email@example.com with your actual email address. This email is required for your ACME account registration with Let’s Encrypt.

Apply the ClusterIssuer:

kubectl apply -f letsencrypt-issuer.yaml

Verify the ClusterIssuer is ready:

kubectl get clusterissuer letsencrypt-prod

You should see READY status as True.

While the HTTP-01 challenge method works well for most use cases, DNS-01 validation is the more robust and flexible approach and should be preferred where possible. DNS-01 validation offers several key advantages:

  • Wildcard certificate support: DNS-01 is the only method that allows you to issue wildcard certificates (e.g., *.example.com), enabling a single certificate to secure all subdomains.
  • No ingress dependency: DNS-01 doesn’t require your application to be publicly accessible on port 80, making it ideal for internal services, clusters behind firewalls, or services that aren’t yet deployed.
  • More reliable: DNS-01 doesn’t depend on your ingress controller being properly configured or your load balancer being ready, reducing potential points of failure during certificate issuance.
  • Better for automation: DNS-01 works seamlessly in automated environments where services may not be immediately accessible via HTTP.

DNS-01 validation is supported for many DNS providers including AWS Route 53, Google Cloud DNS, Azure DNS, Cloudflare, and many others. For detailed configuration instructions for your DNS provider, see the cert-manager DNS-01 documentation.

For this tutorial, we’ll continue using HTTP-01 validation for simplicity, but consider migrating to DNS-01 for production workloads.

Step 5: Deploy a sample application with HTTPS

Now you’ll deploy a sample application and configure an Ingress resource that automatically provisions a TLS certificate.

Create a file named sample-app.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app
  namespace: demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - name: hello
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hello-service
  namespace: demo
spec:
  selector:
    app: hello
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
  namespace: demo
  annotations:
    # Tell cert-manager to issue a certificate
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  # Use the NGINX Ingress Controller
  ingressClassName: nginx
  tls:
  - hosts:
    - app.example.com
    # cert-manager will create this secret with the TLS certificate
    secretName: hello-tls
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hello-service
            port:
              number: 80

Important: Replace app.example.com with your actual domain name in both the tls.hosts and rules.host fields.

Apply the configuration:

kubectl apply -f sample-app.yaml

Step 6: Monitor certificate issuance

cert-manager will automatically request a certificate from Let’s Encrypt. This process typically takes 1-2 minutes.

Watch the certificate request:

kubectl get certificate -n demo --watch

You should see the certificate progress through states: RequestingIssuedReady.

To see detailed information about the certificate issuance process:

kubectl describe certificate hello-tls -n demo

If there are any issues, check the cert-manager logs:

kubectl logs -n cert-manager -l app=cert-manager

Step 7: Test your HTTPS application

Once the certificate is ready, test your application over HTTPS:

curl https://app.example.com

You should see the default nginx welcome page HTML.

Verify the certificate is valid:

curl -vI https://app.example.com 2>&1 | grep -A 10 "SSL connection"

You can also visit the URL in your browser. You should see the nginx welcome page with a valid HTTPS certificate issued by Let’s Encrypt and a green padlock icon.

Understanding automatic certificate renewal

One of cert-manager’s most valuable features is automatic certificate renewal. Let’s Encrypt certificates are valid for 90 days, and cert-manager automatically renews them 30 days before expiration.

To view certificate expiration details:

kubectl get certificate hello-tls -n demo -o yaml | grep -A 5 "status:"

You can also check the actual certificate stored in the secret:

kubectl get secret hello-tls -n demo -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates

This will show the notBefore and notAfter dates for the certificate.

Deploying additional applications

To add HTTPS to other applications, simply create an Ingress resource with the cert-manager annotation. The process is the same:

  1. Create your Deployment and Service
  2. Create an Ingress with:
    • ingressClassName: nginx in the spec
    • cert-manager.io/cluster-issuer: letsencrypt-prod annotation
    • A tls section with your hostname and desired secret name
    • Matching rules for your hostname

cert-manager will automatically handle certificate issuance and renewal for each application.

Troubleshooting

Certificate stuck in “Requesting” state

Check the CertificateRequest and Order objects:

kubectl get certificaterequest -n demo
kubectl describe certificaterequest <request-name> -n demo

Common issues include:

  • DNS not properly configured (verify with dig or nslookup)
  • Firewall blocking HTTP traffic on port 80 (Let’s Encrypt needs HTTP access for validation)
  • Rate limits from Let’s Encrypt

Ingress controller not responding

Verify the ingress controller is running:

kubectl get pods -n ingress-nginx

Check the controller logs:

kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller

Conclusion

You have successfully deployed NGINX Ingress Controller and cert-manager on your Cloudfleet Kubernetes Engine cluster. Your applications now benefit from:

  • Automatic TLS certificate provisioning from Let’s Encrypt
  • Zero-downtime certificate renewal every 60 days
  • Production-grade ingress routing with NGINX
  • Cloudfleet’s managed load balancing with optimized traffic routing

This setup eliminates manual certificate management while providing enterprise-grade security for your applications. You can now deploy any number of applications with automatic TLS certificates by simply adding the cert-manager annotation to their Ingress resources.

For advanced configurations including DNS-01 validation for wildcard certificates, other certificate authorities, and production monitoring, refer to the cert-manager documentation and NGINX Ingress Controller documentation.