Step 6 - Put Your App on the Internet - VIScon 2025 Workshop

October 11, 2025

Put Your App on the Internet

Now that your k3s cluster is running, it's time to expose your application to the world. In this guide, you'll install cert-manager for automatic SSL certificates, then create an Ingress resource that routes traffic to your application with HTTPS enabled. By the end, your app will be accessible via a secure HTTPS URL with a valid Let's Encrypt certificate.

Understanding the Architecture

Before we dive in, let's understand how traffic flows to your application:

  1. DNS: Your domain points to your VM's IP address
  2. Traefik: k3s's built-in ingress controller receives incoming HTTP/HTTPS requests
  3. Ingress Resource: Defines routing rules (which domain goes to which service)
  4. cert-manager: Automatically obtains and renews SSL certificates from Let's Encrypt
  5. Your Application: Receives the routed traffic

Prerequisites

Before starting, ensure you have:

  • A working k3s cluster (completed in Step 4)
  • kubectl configured to access your cluster
  • Your application deployed in Kubernetes (Deployment + Service)
  • Your VM connected via SSH

For this workshop, you'll use: dj-viscon-workshop-X.vsos.ethz.ch (replace X with your participant number)

Verify your cluster is ready:

kubectl get nodes

You should see your node in Ready status.

Part 1: Install cert-manager

cert-manager is a powerful Kubernetes add-on that automates the management and issuance of TLS certificates. It will handle requesting, renewing, and managing certificates from Let's Encrypt automatically.

Step 1: Install cert-manager

cert-manager provides official Kubernetes manifests for easy installation. Apply the latest version:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml

This single command creates:

  • A new cert-manager namespace
  • Custom Resource Definitions (CRDs) for certificates and issuers
  • The cert-manager controller and webhook components

Step 2: Verify cert-manager Installation

cert-manager creates three main deployments. Wait for all pods to be ready:

kubectl get pods -n cert-manager

You should see three pods running:

  • cert-manager-xxxxx
  • cert-manager-cainjector-xxxxx
  • cert-manager-webhook-xxxxx

All should show 1/1 in the READY column and Running status. This may take 1-2 minutes.

Verify the installation is healthy:

kubectl get deployments -n cert-manager

All deployments should show READY 1/1 and AVAILABLE 1.

Step 3: Create a Let's Encrypt Issuer

cert-manager needs an "Issuer" to know where to request certificates from. We'll create a ClusterIssuer for Let's Encrypt, which provides free SSL certificates trusted by all browsers.

Important: Replace your-email@example.com with your actual email address—Let's Encrypt uses this for certificate expiration notifications.

First, create a file for your ClusterIssuer configuration:

vim letsencrypt-issuer.yaml

Add the following configuration:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@example.com  # CHANGE THIS to your actual email
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik

Understanding the ClusterIssuer:

  • server: Let's Encrypt's ACME server endpoint (production)
  • email: Your email for certificate expiration notifications
  • privateKeySecretRef: Where cert-manager stores its private key
  • solvers: How cert-manager proves you control the domain (HTTP-01 challenge using Traefik)

Save the file, then apply it to your cluster:

kubectl apply -f letsencrypt-issuer.yaml

You should see:

clusterissuer.cert-manager.io/letsencrypt-prod created

Verify your issuer is ready:

kubectl get clusterissuer

The issuer should show True under the READY column.

Understanding cert-manager

cert-manager automates the entire certificate lifecycle using the HTTP-01 challenge:

  1. Certificate Request: When you create an Ingress with cert-manager annotations, it detects the request
  2. HTTP Challenge: cert-manager creates a temporary route to serve a validation file at http://your-domain/.well-known/acme-challenge/
  3. Domain Validation: Let's Encrypt verifies you control the domain by fetching this file
  4. Certificate Issuance: Once validated, Let's Encrypt issues a valid SSL certificate
  5. Secret Storage: The certificate is stored as a Kubernetes Secret
  6. Automatic Renewal: Certificates are automatically renewed before expiration

Part 2: Deploy with Ingress

Now that cert-manager is installed and configured, let's expose your application to the internet.

Step 4: Verify Your Application is Running

First, make sure your application is deployed and accessible within the cluster.

Check your deployments:

kubectl get deployments

You should see both your frontend and backend deployments running. Check your services:

kubectl get services

You should see:

  • furnet-frontend service of type ClusterIP on port 8080
  • furnet-backend service of type ClusterIP on port 8000

Step 5: Understand the Ingress Resource

An Ingress is a Kubernetes resource that manages external access to services in your cluster. Let's break down what we need:

Key Components:

  • Metadata: Name, labels, and annotations for configuration
  • IngressClassName: Tells Kubernetes which ingress controller to use (Traefik)
  • TLS Configuration: Defines which domains need HTTPS and where to store certificates
  • Rules: Maps domains and paths to backend services

Step 6: Create the Ingress Manifest

Create a new file for your Ingress configuration using your favorite editor (e.g., vim):

vim ingress.yaml

Why a separate file? Keeping your Ingress in its own file makes it easy to modify routing without touching your application deployment.

Step 7: Add Metadata and Annotations

Start by defining the basic structure and metadata:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: furnet-ingress
  labels:
    app: furnet
  annotations:
    # Traefik annotations for k3s
    traefik.ingress.kubernetes.io/redirect-entry-point: websecure

    # cert-manager annotations for automatic TLS certificate provisioning
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

Understanding the annotations:

  • traefik.ingress.kubernetes.io/redirect-entry-point: websecure

    • Automatically redirects HTTP (port 80) traffic to HTTPS (port 443)
    • Ensures all traffic uses secure connections
    • Users visiting http://your-domain.com will be redirected to https://your-domain.com
  • cert-manager.io/cluster-issuer: "letsencrypt-prod"

    • Tells cert-manager to automatically create a certificate
    • Uses the letsencrypt-prod ClusterIssuer we created earlier
    • cert-manager watches for Ingresses with this annotation and handles certificate issuance

Step 8: Specify the Ingress Class

Add the ingress class specification:

spec:
  # Note: k3s uses traefik as the default ingress class
  # You can omit ingressClassName or explicitly set it to "traefik"
  ingressClassName: traefik

What is ingressClassName?

  • Kubernetes supports multiple ingress controllers in one cluster
  • This field tells Kubernetes which controller should handle this Ingress
  • k3s comes with Traefik pre-installed, so we use traefik

Fun fact: You could run NGINX and Traefik side-by-side and route different applications through different controllers!

Step 9: Configure TLS/SSL

Add the TLS configuration:

  tls:
  - hosts:
    - dj-viscon-workshop-0.vsos.ethz.ch  # CHANGE THIS to your participant number
    secretName: furnet-tls-cert

Understanding TLS configuration:

  • hosts: List of domains that need SSL certificates

    • Replace 0 with your participant number (1, 2, 3, etc.)
    • This must match your actual domain
  • secretName: furnet-tls-cert

    • Where cert-manager will store your certificate
    • This is a Kubernetes Secret that will be automatically created
    • Traefik reads this secret to serve HTTPS traffic

What happens here?

When you create this Ingress:

  1. cert-manager sees the annotation and TLS config
  2. It creates a Certificate resource automatically
  3. Let's Encrypt validates you own the domain (HTTP-01 challenge)
  4. The certificate is stored in the furnet-tls-cert secret
  5. Traefik uses this certificate for HTTPS connections

Step 10: Define Routing Rules

Add the routing rules to direct traffic to both your frontend and backend:

  rules:
  - host: dj-viscon-workshop-0.vsos.ethz.ch  # CHANGE THIS to your participant number
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: furnet-backend
            port:
              number: 8000
      - path: /
        pathType: Prefix
        backend:
          service:
            name: furnet-frontend
            port:
              number: 8080

Breaking down the routing:

  • host: dj-viscon-workshop-0.vsos.ethz.ch

    • Only requests to this domain will be routed by this rule
    • Replace with your actual workshop domain
  • http.paths: List of path-based routing rules

    • Order matters! More specific paths should come first

Backend API Path (/api):

  • path: /api

    • Matches all paths starting with /api
    • Examples: /api/posts, /api/users
  • pathType: Prefix

    • Match based on URL path prefix
  • backend.service.name: furnet-backend

    • Routes API requests to the backend service
    • The backend service should be running on port 8000

Frontend Path (/):

  • path: /

    • Matches everything else (the catch-all route)
    • Must come after the /api path
  • backend.service.name: furnet-frontend

    • Routes all other requests to the frontend service
    • The frontend service should be running on port 8080

Why order matters:

Traefik evaluates paths in order. If we put / first, it would match everything (including /api requests) and the backend would never receive traffic. By putting /api first, we ensure API requests go to the backend, while everything else goes to the frontend.

Step 11: Complete Ingress Manifest

Here's your complete Ingress configuration with both frontend and backend routing:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: furnet-ingress
  labels:
    app: furnet
  annotations:
    # Traefik annotations for k3s
    traefik.ingress.kubernetes.io/redirect-entry-point: websecure

    # cert-manager annotations for automatic TLS certificate provisioning
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

spec:
  # Note: k3s uses traefik as the default ingress class
  # You can omit ingressClassName or explicitly set it to "traefik"
  ingressClassName: traefik

  tls:
  - hosts:
    - dj-viscon-workshop-0.vsos.ethz.ch  # CHANGE THIS to your actual domain
    secretName: furnet-tls-cert

  rules:
  - host: dj-viscon-workshop-0.vsos.ethz.ch  # CHANGE THIS to your actual domain
    http:
      paths:
      # Backend API - must come first (more specific path)
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: furnet-backend
            port:
              number: 8000
      # Frontend - catch-all path (less specific)
      - path: /
        pathType: Prefix
        backend:
          service:
            name: furnet-frontend
            port:
              number: 8080

Important: Replace dj-viscon-workshop-0.vsos.ethz.ch with your assigned domain in both places (tls.hosts and rules.host).

What this configuration does:

  • All requests to https://your-domain.com/api/* → Backend service on port 8000
  • All other requests to https://your-domain.com/* → Frontend service on port 8080
  • Both routes are served over HTTPS with the same certificate

Step 12: Apply the Ingress

Save the file and apply it to your cluster:

kubectl apply -f ingress.yaml

You should see:

ingress.networking.k8s.io/furnet-ingress created

Step 13: Verify the Ingress

Check that your Ingress was created:

kubectl get ingress

You should see something like:

NAME              CLASS     HOSTS                                    ADDRESS         PORTS     AGE
furnet-ingress    traefik   dj-viscon-workshop-0.vsos.ethz.ch       10.43.123.45    80, 443   10s

Get detailed information:

kubectl describe ingress furnet-ingress

Look for:

  • The TLS configuration
  • The routing rules
  • Events showing cert-manager activity

Step 14: Watch Certificate Issuance

cert-manager detected your Ingress and started requesting a certificate. Watch the process:

kubectl get certificate

You should see a certificate named based on your Ingress:

NAME               READY   SECRET             AGE
furnet-tls-cert    False   furnet-tls-cert    20s

Check the certificate details:

kubectl describe certificate furnet-tls-cert

What to look for:

  • Events showing "Created new CertificateRequest"
  • "Waiting for CertificateRequest to complete"
  • Eventually: "Certificate issued successfully"

This takes 1-3 minutes. Let's Encrypt needs to:

  1. Create an HTTP challenge
  2. Verify your domain ownership
  3. Issue the certificate
  4. Store it in the secret

Step 15: Verify the Certificate Secret

Once the certificate shows READY True, check the secret:

kubectl get secret furnet-tls-cert

View the certificate details:

kubectl describe secret furnet-tls-cert

You should see tls.crt and tls.key entries. These are your SSL certificate and private key!

Step 16: Test Your Application

Now let's test both the frontend and backend through the Ingress!

Test the Frontend:

Open your browser and navigate to your domain:

https://dj-viscon-workshop-0.vsos.ethz.ch

Expected results:

  • Your frontend application loads over HTTPS
  • The browser shows a lock icon (secure connection)
  • No certificate warnings
  • The certificate is issued by "Let's Encrypt"

Test the Backend Health Endpoint:

Test the backend health endpoint by visiting it in your browser or using curl:

curl https://dj-viscon-workshop-0.vsos.ethz.ch/health

Or open in your browser:

https://dj-viscon-workshop-0.vsos.ethz.ch/health

You should see a response from your backend health endpoint. Note that health endpoints are typically exposed at the root level for monitoring purposes.

Test the certificate:

Click the lock icon in your browser and view the certificate details. You should see:

  • Issued to: your domain
  • Issued by: Let's Encrypt
  • Valid for: 90 days

What's happening:

  • Requests to / → Frontend (React app)
  • Requests to /api/* → Backend (FastAPI)
  • Both use the same HTTPS certificate
  • Both are routed through a single Ingress

Understanding HTTP to HTTPS Redirect

Thanks to the redirect-entry-point: websecure annotation we configured, Traefik automatically redirects all HTTP traffic to HTTPS. Try visiting:

http://dj-viscon-workshop-0.vsos.ethz.ch

You should be automatically redirected to HTTPS. This ensures users always access your application securely, even if they type http:// in their browser.

Troubleshooting

Problem: Certificate stays in "False" state

Check the certificate request:

kubectl get certificaterequest
kubectl describe certificaterequest

Common causes:

  • Domain doesn't point to your VM IP
  • Firewall blocking port 80 (needed for HTTP-01 challenge)
  • Invalid email in ClusterIssuer

Check cert-manager logs:

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

Problem: "503 Service Unavailable"

Your Ingress is working, but one of the backend services isn't reachable.

Check which service is failing:

  • If it's the homepage: check the frontend service
  • If it's the API: check the backend service

Check your services:

kubectl get service furnet-frontend
kubectl describe service furnet-frontend

kubectl get service furnet-backend
kubectl describe service furnet-backend

Check your pods:

kubectl get pods
kubectl logs <frontend-pod-name>
kubectl logs <backend-pod-name>

Ensure:

  • Service selector matches your pod labels
  • Service port matches container port
  • Pods are in Running state
  • For backend: API endpoints are correctly exposed on port 8000
  • For frontend: Static files are served on port 8080

Problem: "404 Not Found"

Traefik is working, but can't find a route.

Check your Ingress:

kubectl describe ingress furnet-ingress

Ensure:

  • Host matches your domain exactly
  • Path is correct
  • Backend service name is correct

Problem: Certificate warnings in browser

Check if cert-manager created the certificate:

kubectl get certificate
kubectl describe certificate furnet-tls-cert

If the certificate exists but shows warnings:

  • Verify the domain in the certificate matches your URL
  • Check if you're using the correct ClusterIssuer (prod, not staging)

Problem: Can't access application at all

Check Traefik is running:

kubectl get pods -n kube-system | grep traefik

Next Steps

Congratulations! Your application is now live on the internet with HTTPS.

Continue to Step 7: Achievements and Future Steps to review what you've accomplished and explore ideas for taking your DevOps skills even further.