In this tutorial I cover setting up a sample project in a local, single-node Kubernetes cluster such as microk8s for local, everyday development without having to build a new image every time code changes.
- A working local Kubernetes cluster using microk8s
- Microk8s docker registry enabled using
- Docker (included with microk8s or install from here)
- (Optional) Python 3 and
This tutorial assumes you already have a Kubernetes cluster up and running on your local workstation and have a basic understanding of the CLI and containers. If you do not have a local Kubernetes cluster, please refer to the installation instructions linked above.
Note: NONE of the code or configs in this tutorial are 'production ready'.
If you did not have
kubectl previously, alias the commands to avoid having to prefix them with
sudo snap alias microk8s.docker docker sudo snap alias microk8s.kubectl kubectl
Kubernetes is a modern platform for automating the deployment, configuration, and scaling of containers and containerized applications. It's built on top of the Docker platform by default and provides developers and sysadmins with flexible and powerful tools for managing their services at any scale. Including workstation scale. 🙂
All configurations and code can be found in this GitHub project:
A simple project for setting up a kubernetes development environment
Alright, ready to get started? We need to have an app to run in a container, so let's define a simple app using the Flask framework in Python:
# test_server.py from flask import Flask app = Flask(__name__) @app.route("/") def kube(): return "Hello Kubernetes!"
This defines a Flask application with a single route,
/. Nothing scary here! 👾
If you'd like to test out the app, install the Flask dependency:
pip install flask
... and run with:
FLASK_APP=test_server.py flask run
Then open http://127.0.0.1:5000/ your browser to see the results.
Next up we need to define a
Dockerfile for our new web service. I'll assume some level of familiarity with Dockerfiles, so I won't break this one down.
FROM python:3.6.7-alpine3.7 RUN mkdir /app WORKDIR /app COPY ./requirements.txt /requirements.txt RUN pip install -r /requirements.txt COPY ./test_server.py /app/test_server.py ENV FLASK_APP=test_server.py ENV FLASK_ENV=development ENTRYPOINT [ "flask", "run", "--host=0.0.0.0" ]
Now we need to build and tag the image with our code in it and push it to the registry. To do this run:
docker build -t localhost:32000/kubetest:v1.0.0 . docker push localhost:32000/kubetest:v1.0.0
Note: depending on which kubernetes distro you are using, the image name may differ, replace
localhost:32000 with whatever the hostname for your local registry is.
You only need to build this image once (unless you add new dependencies), as we'll be injecting modified code into the image later on.
So now we have a Docker image with the latest version of our code in it, ready to run in our shiny new cluster. Normally around now you would fire up
docker run and call it a day, but we're going to do something better than that.
We need to define two types of resources to get our new image running in the cluster, a
Deployment and a
Service. These will seem kind of familiar if you've used
docker-compose before, as they are conceptually very similar.
Deployment looks like:
# kubetest.yaml apiVersion: apps/v1 kind: Deployment metadata: name: kubetest spec: selector: matchLabels: app: kubetest template: metadata: labels: app: kubetest spec: containers: - name: kubetest image: localhost:32000/kubetest:v1.0.0 ports: - containerPort: 5000 imagePullPolicy: Always ...
This defines a basic service using the docker image we just pushed named
kubetest. The important bits are in the
spec structure, where we define a single container entry named
kubetest that uses the image
localhost:32000/kubetest:v1.0.0 and exposes port
5000. See! Just like a
service in docker-compose. 😄 You can try creating a pod now with
kubectl create -f kubetest.yaml, though you won't be able to access the pod via HTTP, so hold off on doing that just yet.
To make your pod accessible from outside the Kubernetes network, we need to define a
Service. This is similar to the
-p option in
docker runor the
ports section in a
# kubetest.yaml ... --- kind: Service apiVersion: v1 metadata: name: kubetest labels: app: kubetest spec: selector: app: kubetest ports: - name: http port: 5000 protocol: TCP type: NodePort
Note: I have the
Service definitions in the same file for simplicity.
Alright, let's go ahead and set this up in our cluster for real now!
noah@kamijou:/opt/kubetest$ kubectl apply -f kubetest.yaml deployment.apps/kubetest created service/kubetest created
We can look at the resulting
noah@kamijou:/opt/kubetest$ kubectl get pods NAME READY STATUS RESTARTS AGE kubetest-574958d5fd-zpvn5 1/1 Running 0 15s
... and the service using:
noah@kamijou:/opt/kubetest$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 8d kubetest NodePort 10.152.183.154 <none> 5000:31065/TCP 20s noah@kamijou:/opt/kubetest$
Pay special attention to the
5000:31065/TCP section in the service list, that
31065 is the port we'll be using to connect to our web service. Let's do that now:
noah@kamijou:/opt/kubetest$ curl -i localhost:31065 HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 18 Server: Werkzeug/0.14.1 Python/3.6.7 Date: Wed, 12 Dec 2018 05:53:10 GMT Hello Kubernetes!
... but wait, I was promised
cookiesbeing able to edit my code within the pod! 😣 How am I supposed to do that!?
Calm down, we'll get to that! 😅
Let's go back to our
kubetest.yaml file and add in a few lines.
apiVersion: apps/v1 kind: Deployment metadata: name: kubetest spec: selector: matchLabels: app: kubetest template: metadata: labels: app: kubetest spec: containers: - name: kubetest image: localhost:32000/kubetest:v1.0.0 ports: - containerPort: 5000 imagePullPolicy: Always + volumeMounts: + - mountPath: /app + name: kubetest-volume + readOnly: true + volumes: + - name: kubetest-volume + hostPath: + path: /home/noah/kubetest + type: Directory
There's a bit to unpack here, but the gist of it is we're creating a directory-based folder mount similar to the
-v /host/path:/container/path syntax in
docker run. Kubernetes handles this slightly different than vanilla docker, so if you'd like to read more on it check out the official documentation on persistent volumes.
Note: change the value for the
path section to point to whatever your
test_server.py file lives. That's super important for this to work right.
Go ahead and run
kubectl apply -f kubetest.yaml again to update our container with the volume mount.
noah@kamijou:~/kubetest$ kubectl apply -f kubetest.yaml deployment.apps/kubetest configured service/kubetest unchanged
Check to make sure the pod is up and running again with
noah@kamijou:~/kubetest$ kubectl get pods NAME READY STATUS RESTARTS AGE kubetest-58f7b7d957-cvdch 1/1 Running 0 63s
Let's take a look at the logs, these are slurped up from the
STDERR of all pods. We only care about the
kubetest service, so lets check that out:
noah@kamijou:~/kubetest$ kubectl logs svc/kubetest * Serving Flask app "test_server.py" (lazy loading) * Environment: development * Debug mode: on * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 124-402-077
Cool, app's still running.
Finally, let's add a new endpoint to the
from flask import Flask app = Flask(__name__) @app.route("/") def kube(): return "Hello Kubernetes!\n" +@app.route("/healthz") +def health(): + return "ok\n", 200
... check the logs again.
noah@kamijou:~/kubetest$ kubectl logs svc/kubetest * Serving Flask app "test_server.py" (lazy loading) * Environment: development * Debug mode: on * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 124-402-077 * Detected change in '/app/test_server.py', reloading * Restarting with stat * Debugger is active! * Debugger PIN: 124-402-077
Flask picked up that there was a change to the test_server.py file and reloaded automagically!
Let's try to hit our new endpoint, remember to make sure the port didn't change on you with
kubectl get svc
noah@kamijou:~/kubetest$ curl -i localhost:30788/healthz HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 3 Server: Werkzeug/0.14.1 Python/3.6.7 Date: Wed, 12 Dec 2018 06:25:33 GMT ok
Woo, we're done!
I hope you found my post helpful! If you have any questions, please drop by in the comments, I'm happy to help.
This is my first post, so any feedback is super welcome. 🙋♂️