Steven's Thoughts

Minikube 101

The moment has finally arrived! We will learn a bit about Kubernetes and finally deploy our initial microservice from three weeks ago to our Kubernetes distribution of choice: minikube.

The good thing? The basics of Kubernetes boil down to three new concepts: Pods, Deployments, and Services.

Terminology

Pods allow multiple Docker containers to coexist within the same network space so they can communicate with each other over localhost, almost as if they were services running on the same computer. We will explore multi-container Pods in a future series, but we'll follow the lead of the Bootstrapping Microservices book and stick with single containers Pods for now.

What happens if a Pod fails? Do they restart automatically? No, that's where Deployments come in. Deployments ensure that a certain number of Pods are running at the same time. If a Pod dies, the Deployment removes it and restarts another.

Can we access Pods managed by Deployment via their IP? We can, but their IPs won't stay consistent if a Pod dies and the Deployment replaces it with a new Pod.

That's where Services come in. Services provide a stable IP address to conveniently access the Pods managed by a Deployment. We will be covering Services of type NodePort and LoadBalancer in this week's article.

We are about to dive into the weeds a bit, so with those Kubernetes-specific terms out of the way, let's begin!

Configuration

Every Kubernetes object contains a TypeMeta struct that includes two fields: apiVersion: and kind:. They optionally, but preferrably, include an ObjectMeta field: metadata:, which includes the name: of the Deployment or Service.

Deployments and Services also both include a spec: field: DeploymentSpec for Deployments and ServiceSpec for Services.

Defining a Deployment. (source)

The spec: field of Deployments has two mandatory fields: replicas: and template:. The replicas: field specifies the number of Pods to maintain while the template: field is the PodSpec to instantiate.

A third, optional but recommended, field is selector:. It specifies which Pods the Deployment manages.

For our purposes here, the template: field provides labels: for this template and the PodSpec under spec:.

The primary field we are interested in at the moment is containers:, which is a list of containers belonging to the Pod. We assign each container a name:, specify the image: to use, the imagePullPolicy:, and the environment variables for the container as a list under env:.

Defining a Service. (source)

The spec: field for Services is quite a bit simpler. Similar to the Deployment's selector, we specify a key:value pair of Pods to route requests to.

We then specify the service type and a list of ports to map from the outside world to our Pods.

See, much simpler? Finally, on to our first round with minikube.

Round 1: Learning minikube.

minikube is a great Kubernetes distribution for learning to develop and test microservices locally. The ability to experiment at no expense is, in my opinion, its greatest value.

Creating a minikube cluster is very straightforward:

minikube start [-p clusterName]

That command takes a short while to start the given cluster and configure kubectl to interact with minikube. (Or use minikube kubectl -- if you don't have kubectl installed.)

Note that while kubectl will be configured to interact with Kubernetes, Docker will not. Use the following command to address this issue:

eval $(minikube docker-env -p [instance])

docker build commands will now create images in your minikube registry.

Stopping a minikube cluster is equally straightforward:

minikube stop [-p clusterName]

Round 2: Chapter 3, minikube edition. (source)

If you remember, Chapter 3 ends with a self-contained, containerized video-streaming microservice.

We will now learn how to load that container into the minikube registry, create a Pod from it, deploy it, and expose it via a Service.

This will be be a bit tedious because I'm going to explain the reasoning behind every single step, but that's so you understand what's going on. (And so I don't mistakenly mislead you.)

Step 1: Start our instance of Kubernetes.

minikube start

This command starts our Kubernetes instance. Without the -p flag, this uses the default minikube profile. We prefer using a different one, like FlixTube.

Instead of appending -p FlixTube to every command, we can also use the MINIKUBE_PROFILE environment variable like so:

export MINIKUBE_PROFILE=FlixTube

Either way works, but I find using the .env file much simpler.

  1. minikube start
  2. cd video-streaming
  3. docker build -t video-streaming:1 -f Dockerfile.prod
  4. cd ..
  5. minikube kubectl -- apply -f deploy.yml
  6. minikube kubectl -- delete -f delete.yml
  7. minikube stop

Step 2: Source the environment variables for Docker.

eval $(minikube docker-env)

This lets all docker commands use the minikube image registry by default, so you won't yet see any images with docker image ls.

Step 3: Build the Docker image for the registry.

cd video-streaming
docker build -t video-streaming:1 -f Dockerfile.prod
cd ..

This builds our production Dockerfile for the video-streaming microservice, tags it with the version 1, and pushes it to the minikube image registry.

Step 4: Create your Deployment configuration.

This is where the Terminology and Configuration sections come in handy. We need to create a video-streaming-deployment and a video-streaming-service to access our service.

The configuration:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: video-streaming-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: video-streaming
  template:
    metadata:
      labels:
        app: video-streaming
    spec:
      containers:
        - name: video-streaming
          image: video-streaming:1
          imagePullPolicy: Never
          env:
            - name: PORT
              value: "4000"
---
apiVersion: v1
kind: Service
metadata:
  name: video-streaming-service
spec:
  selector:
    app: video-streaming
  type: NodePort
  ports:
    - protocol: TCP
      port: 80
      targetPort: 4000
      nodePort: 30000

Notice spec.ports[0].nodePort. That's the port we will access our service through.

Step 5: Apply your Deployment configuration.

minikube kubectl -- apply -f deploy.yml

Step 6: Test your microservice. Bask in its glory.

minikube ip

Visit http://[ip]:30000/video and watch the sample video play.

Marvelous!

Step 7: Destroy your Deployment.

minikube docker -- delete -f deploy.yml

Not destroying your Deployments and Services wastes disk space.

Step 8: Stop your Kubernetes instance.

minikube stop

This shuts minikube down for now.

Step 9: Repeat these steps until your comfortable.

Repetition is the key to mastery. Try these steps yourself without looking at this guide.

Did you succeed? Can you recite them from memory as if you were teaching them to someone?

Congrats if you can! You're well on your way to mastering the minikube Kubernetes distribution.

Round 3: Chapter 3, LoadBalancer edition. (source)

The Round 2 version was pretty cool, but Services of type NodePort aren't ideal for production use. We need Services of type LoadBalancer for production deployments.

Fortunately, minikube has us covered here as well.

Step 1: Update video-streaming-deployment's imagePullPolicy.

We won't have the Docker images on our production machines, so we will need to pull the image if it doesn't already exist. That involves a tiny change to the imagePullPolicy of each of our containers.=:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: video-streaming
spec:
  replicas: 1
  selector:
    matchLabels:
      app: video-streaming
  template:
    metadata:
      labels:
        app: video-streaming
    spec:
      containers:
        - name: video-streaming
          image: video-streaming:1
          imagePullPolicy: IfNotPresent
          env:
            - name: PORT
              value: "4000"

Notice we use IfNotPresent instead of Never.

Step 2: Update video-streaming-service's type.

Production Kubernetes environments will require Services to use type LoadBalancer to distribute traffic instead of NodePort. One tiny change makes that possible:

apiVersion: v1
kind: Service
metadata:
  name: video-streaming-service
spec:
  selector:
    app: video-streaming
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 4000

Step 3: Tunnel traffic to our local LoadBalancer.

Earlier we accessed the NodePort directly via the IP given to us by minikube ip. That works locally during development, but it will fail in Production.

A LoadBalancer gives us a static IP that routes traffic to all healthy Pods in our Production environment, but minikube only gives us a Cluster IP by default.

Fortunately, minikube also provides a way to expose the LoadBalancer IP to our dev machine so we can test our services locally with the same configuration we will use in Production.

How? In another terminal, run the following command:

minikube tunnel

You will likely be prompted for your password, but then you'll see output similar to this:

Status:
        machine: flixtube
        pid: 129134
        route: 10.96.0.0/12 -> 192.168.58.2
        minikube: Running
        services: [video-streaming]
    errors:
                minikube: no errors
                router: no errors
                loadbalancer emulator: no errors

Return to your original tunnel and execute the following command:

minikube kubectl -- get services

This will show you an External IP you can access from your browser:

NAME              TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
kubernetes        ClusterIP      10.96.0.1       <none>          443/TCP        3d19h
video-streaming   LoadBalancer   10.96.104.195   10.96.104.195   80:31953/TCP   37m

The steps you just followed will let you access your microservices application as if it's deployed to GCP, AWS, LiNode, or any provider of your choice whenever you are ready.

Conclusion

The book Bootstrapping Microservices recommends trying to develop with both docker compose and Kubernetes to decide which you prefer.

Personally, I'm still undecided. docker compose uses less memory, but using minikube to become more comfortable with Kubernetes is an enticing option.

Whatever you choose, keep developing!

And next week, we'll simplify our life by learning a bit about Infrastructure as Code with Terraform. That will save us countless headaches having to learn the UI of whichever hosting provider we choose.

#microservices #docker #minikube