When diving into Kubernetes, having a reliable local development environment is crucial. In this post, I’ll walk through setting up a local Kubernetes cluster using KIND (Kubernetes IN Docker) and deploying a simple application to it.

Assumptions

  • Quick Read: link
  • Laptop / Machine to run KIND: (Linux/Mac/Windows)
  • Docker Engine installed and logged in: guide
  • kubectl installed: guide
  • IDE installed: (vim/nvim/Visual Studio Code)
  • Go installed: guide

KIND Installation

The installation steps are straightforward and documented well here.

Setting Up Our Cluster

Let’s start by creating a KIND cluster with one control plane node and two worker nodes running Kubernetes v1.32.0.

We can create this using a simple configuration file and the KIND CLI:

Create the cluster by running:
vim kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

Exit vim by typing ":wq"

w=write (save)

q=quite

Create the cluster by running:

kind create cluster --config kind-config.yaml

View the nodes with:

kubectl config get-contexts

Output

CURRENT   NAME        CLUSTER     AUTHINFO    NAMESPACE
*         kind-kind   kind-kind   kind-kind 

Select Context:

kubectl config use-context kind-kind

Output

Switched to context "kind-kind"

View Nodes:

kubectl get nodes

Output

kind-control-plane   Ready    control-plane   43h   v1.32.0
kind-worker          Ready              43h   v1.32.0
kind-worker2         Ready              43h   v1.32.0

Test Application

We’ll use a simple Flask web application that demonstrates environment variable configuration and secret management.

Here’s the current structure and the application:

touch app.py
touch Dockerfile
.
├── app.py
├── Dockerfile
└── kind-config.yaml

Update the app.py with the below:


# app.py
from flask import Flask, render_template_string
import os

app = Flask(__name__)

@app.route("/")
def home():
    app_message = os.getenv("APP_MESSAGE", "Default Message")
    secret = os.getenv("SECRET", "Not Connected")

    # HTML Template to render in the browser
    html_template = """
    <html>
    <head><title>{{ title }}</title></head>
    <body>
        <h1>{{ app_message }}</h1>
        <p><strong>Vault Secret:</strong> {{ secret }}</p>
    </body>
    </html>
    """
    return render_template_string(html_template,
                                  title="Application Title",
                                  app_message=app_message,
                                  secret=secret)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

We containerize this application using a straightforward Dockerfile:

# DOCKERFILE
FROM python:3.9-slim
WORKDIR /app
COPY app.py /app/
RUN pip install flask
CMD ["python", "app.py"]

Log into Docker (change the username)

docker login -u USERNAME

Nothing fancy here, build the application and tag it with “app:latest”.

docker build . -t USERNAME/app:latest
docker push USERNAME/app:latest

Push it to your local registry.

Deploying to Kubernetes

With our application containerized, we can deploy it to our cluster.

Create the following files:

mkdir app && touch app/deployment.yaml && touch app/service.yaml

mkdir ingress && touch ingress/default.yaml

Navigate to each file and populate it accordingly with the below manifests

Deployment manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: app
        image: matcham89/app:latest
        ports:
        - containerPort: 5000
        env:
        - name: APP_MESSAGE
          value: "Test Application"

Service manifest:

apiVersion: v1
kind: Service
metadata:
  name: app
spec:
  selector:
    app: app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 5000

From within the application directory, deploy the manifests:

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

Setting Up Ingress Controller

KIND provides its own ingress controller, which we can deploy with.

Detailed here.

kubectl apply -f https://kind.sigs.k8s.io/examples/ingress/deploy-ingress-nginx.yaml

Define The Application Ingress

Add this configuration to the default.yaml file created earlier in the ingress folder.

The Ingress configuration maps requests to our previously created service.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: default-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: app.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app
            port:
              number: 80

Apply the maniest:

kubectl apply -f default.yaml

To handle external IPs locally, we’ll need KIND’s cloud provider. Detailed here.

go install sigs.k8s.io/cloud-provider-kind@latest
sudo install ~/go/bin/cloud-provider-kind /usr/local/bin

Once installed, fire up the tool with the below command:

cloud-provider-kind

We must leave the tool running in a terminal whilst we are working on our local cluster with externalIP’s.

Once the tool is “running” you will see some output similar to this:

43 Protocol:TCP} Cluster:[{Address:172.18.0.3 Port:31870 Protocol:TCP} {Address:172.18.0.4 Port:31870 Protocol:TCP}]} 
IPv4_80_TCP:{Listener:{Address:0.0.0.0 Port:80 Protocol:TCP} Cluster:[{Address:172.18.0.3 Port:32131 Protocol:TCP} 
{Address:172.18.0.4 Port:32131 Protocol:TCP}]}] SessionAffinity:None SourceRanges:[]}        

Do not be discouraged if you see some output “server misbehaving” - - this is expected as it boots up.

Running cloud-provider-kind will attach an external IP to our ingress controller. In my case, it looks like this:

ingress-nginx-controller   LoadBalancer   10.96.210.134   172.18.0.6    80:30748/TCP,443:31112/TCP   24h    

Final Configuration

The last step is to update our /etc/hosts file to route traffic to our applications:

sudo vim /etc/hosts

# local k8s dev 
172.18.0.6 app.local

(Be sure to update the IP address with the output from your controller)

What We’ve Accomplished

At this point, we have:

  1. A functioning Kubernetes cluster with one control plane and two worker nodes.
  2. Two deployed applications with their own services.
  3. An ingress controller handling external traffic.
  4. Local DNS resolution for our applications.

You can now access the applications by curling it:

curl app.local 
    <html>
    <head><title>Application Title</title></head>
    <body>
        <h1>Test Application</h1>
        <p><strong>Vault Secret:</strong> Not Connected</p>
    </body>
    </html>

Key Takeaways

  • KIND provides a robust local Kubernetes environment.
  • The built-in ingress controller simplifies local access to applications.
  • Environment variables offer a flexible way to configure applications.
  • Local DNS configuration enables seamless access to your services.

This setup provides a solid foundation for local Kubernetes development. In the next part, we’ll explore further and introduce tools like Vault for secret management.

Updated: