Artificial Unintelligence.

DS II - Making a Mockery (of a Platform).

☕️☕️☕️ 15 min read

In the last post we discussed a simple requirement to store tweets and make them available to other applications. It quickly became clear that it wasn’t as simple as it looked - in fact, it was so complicated that it actually called for an entire platform to be built, replete with a full data plane and separate control plane.

Mocking up the platform side of things is the subject of this post, in which we’ll discuss getting a dev environment set up. Once you land in an organisation, the first thing you often want to do is replicate their production environment as closely as possible, ideally locally on your own machine. This allows you to test your application in an environment that mirrors the one it will ultimately run in.

“But my code is already unit tested!”

Cool guy coding.
No number of unit tests will make you this guy.

This is a common objection to this approach when you present it to traditional software engineers. But if those tests are like most I’ve seen, I suspect that they might say something very deep and subtle like assert MyClass==MyClass. This is roughly equivalent to the conversation at every bad first date you’ve ever been on. Your tests will agree with a set of statements which only a psychopathic piece of code would fail to agree with, and which tell you nothing about the inner workings of the subject under examination; much less its performance in your particular environment, how it will get along with the rest of the apps if invited to a party, whether it will try to monopolise your limited resources, or knows how to clean up after itself.

Sorry, we were talking about software weren’t we…

Even if you’ve gone to the trouble of finding a proper test framework and using some sample data (because this is about data science, remember?) the scale or velocity of that data may be completely different in the real world - this is often the problem you are trying to solve. So unit testing doesn’t really cut the mustard, and actually the local testing approach we demonstrate here isn’t very thorough either, but it doesn’t require you to go and buy resources on GCP or AWS, so that’s a plus, and you can always do that later.

What’s in the box? About the images we’re using.

This is a mock platform, so it is going to be missing things. While you’re welcome to talk to me on Twitter about all the things it is missing, I expect that there will be a vast number. What we are including however, is;

  • gcr.io/etcd-development/etcd:v3.3.13
  • landoop/fast-data-dev
  • fluent/fluentd:v1.3-debian-1

Oh? You went and ran Docker pull didn’t you? Sorry, Minikube runs in a VM with a wholly separate filesystem. You may want to delete those if you’re short of disk space. Minikube creates and deletes VMs all the time, so add the above images to a cache using minikube cache add ${IMAGE} to avoid re-downloading frequently.

The etcd and fluentd images are provided by the organisations developing both tools. The Landoop Kafka image is one that I see out in the industry semi-regularly, it contains a full Kafka setup which includes Zookeeper and a Schema Registry, alongside some GUI tools to inspect your cluster and make sure it is giving the expected results. Feel free to work something up from the official Confluent images or similar if that’s more your speed, but be prepared to spend a great deal of time kludging around with unwieldly arguments with Kafka’s CLI tools (I’m generally a CLI fan, but only where they have been designed for humans, or indeed designed at all).

Pre-requisites

I will assume you have Docker installed and are reasonably proficient at using it. There are plenty of tutorials to help with that, so I’ll suggest a Google search if you hit issues. You will also need to install Minikube and Kubectl, which we’ll use to mock Kubernetes (but only gently, so as not to hurt its feelings). I strongly advise enabling autocompletion for Kubectl (and most CLI’s generally), and future you will be greatful to be one of the few engineers without RSI if you do so.

I’m running everything on Linux, and you can look up instructions for Windows. (Personally, my advice is “perhaps consider a different career path, I hear accounting is nice and has something to do with numbers, so it must be just like AI - right?” Ironically, I’m relying on readers not taking this advice to keep my potential readership relatively high - because Windows is still quite dominant.)

You’ll also need to ensure you have git installed - presumably you’re familiar with it as well, otherwise its back to Coursera with you. Or you can enjoy the documentation (that link points to a parody, but I can’t tell the difference).

Getting started - Minikube

If you’ve recovered from reading the parody Git manpages, you can clone the repo if you’re following along.

Bring minikube up using minikube start --memory=8192 --cpus=2 --extra-config=apiserver.enable-admission-plugins=PodSecurityPolicy, and wait for… a long time… (Note that there is advice floating about online suggesting you can use something like --extra-config=apiserver.GenericServerRunOptions.AdmissionControl=.... - this was deprecated, and leads to fierce and incomprehensible errors, as does the attempted use of things like PersistentVolumeLabel. Meanwhile, various admission plugins are enabled by default, so the only one we need to focus on is PodSecurityPolicy.)

(**EDIT: September 19:** starting this week I've had some trouble with this approach. Haven't had time to research properly; initially I suspected that minikube may have updated something to require `--extra-config=apiserver.authorization-mode=RBAC`. But having done a bit more reading (per this [link](https://github.com/kubernetes/minikube/issues/3818)), I think I neglected to configure some security policies authorizing the core services - this naturally prevents the cluster from coming up. I've updated the github repo for this post to include the relevant policies in `psp.yaml`, which needs to be copied to `~/.minikube/files/etc/kubernetes/addons/psp.yaml`, and I've updated `k8s-configure.sh` to include this step. I may write an additional post exploring psp.yaml and providing more detail on how it works, it gave me some insight to fix up my podsecuritypolicy settings which I've improved in `devSecurity.yaml`, as I don't believe these were being applied as intended.

Once it is up, you should have a mock K8s cluster running in a VM, which you can describe using your kubectl commands such as kubectl describe-cluster. If that all works, we’re good to go.

Kubernetes Concepts and setup

Kubernetes has a few entities you’ll need to familiar with, notably - nodes, pods, services, controllers and containers. The documentation on the main website is somewhat unsatisfactory when it comes to describing the design succinctly, and I advise you to consult the architecture doc for detail. Summarising, containers are just Docker containers (at least for our purposes), they run in pods on nodes, which are hosts, and are exposed by services which do network related stuff like load balancing, DNS (including discovery etc.), controllers do deployment and scaling. The pod is kind of the interesting part of Kubernetes that differentiates it from Docker compose in many ways.

Running an application on Kubernetes

Kubernetes is so flexible that the number of configurables can become overwhelming, so it helps to have a checklist of considerations when you’re crafting a deployment. The basics you want to include are;

  1. A namespace
  2. Memory and CPU limits
  3. A container image
  4. A service to handle load balancing
  5. The number of replicas you’ll require
  6. (Optional, stateful applications only) Any volumes you’ll need.

N.B. - it should be noted that management love checklists for the same reason you like this one, they’re just overwhelmed by more things; if you show your boss this checklist (and pass it off as your own) you will be able to run a meeting about Standardising Processes which will assure you of a tidy bonus while simultaneously earning you the undying contempt of your colleagues. Good luck!

Kubernetes pods

Applications are deployed in pods, and each instance of the application container should be in its own pod. Pods do not handle replication. Instead, think of them as a way of packaging the application for runtime/production. A production environment might mandate that all applications use a particular set of subsidiary apps/containers. Many of these might operate separately from the application itself, and this is called the Sidecar Pattern. Some of the common things included are proxies that inspect network traffic before forwarding it to the container it was intended for, but there are a variety of use cases around application monitoring, networking, and other stuff that ops people care about and we aren’t going to talk about.

Side note: pods are the thing that enable service mesh frameworks like Istio to fly. Istio (for example) installs a hook into K8s which modifies its default pod deployment behaviour and adds additional stuff. I may write a post about this later - one interesting implication is that we could consider taking REST traffic from our existing apps and proxying it to Kafka traffic to ameliorate the deficiencies REST suffers in persistence. There are also technologies such as Cilium, which bundles in additional security and might be worth evaluating for its Kafka interop. I only mention to sketch some of the flexibility available via Kubernetes.

Controllers, services… and everything else

Examples of controllers include replica sets (which is what we’re using here), stateful sets, daemon sets, deployments and so on. These are just all different ways of getting some set of pods out onto some set of nodes in a cluster and keeping some number there under different conditions (the desired number being something that might change according to various scaling behaviours that we’ll discuss much later). The documentation is sufficent to explain what these do, so I will not cover them in depth beyond mentioning their existence.

A very serious hacker.
Crash override fails on ClusterIP.

As mentioned above; services do networking things, and in this series we are going to talk purely about ClusterIP services, which do not expose external IPs. If you have the privelege of having customers who might be interested in your app, you would be looking at load balancing, creating static public IP addresses, and other things that would require care and thought. You should not do any of these things unless you understand the security implications. One more time. Do not do any of those things unless you understand the security implications.

If you want to add to the K8s API, you’ll be pleased to hear that its entities are customisable. Customised resources that don’t fit within the node/service/pod/container/controller paradigm are quite possible. This is an approach pursued by tools such as Kubeflow which adds various machine learning tooling to K8s. This is quite useful, and worth keeping in mind if you plan to run some such service at scale.

Defining Kubernetes entities

apiVersion: v1
kind: Service
metadata:
  name: kafka-service
  namespace: dev
  labels:
    app: kafka
    phase: dev
spec:
  selector:
    app: kafka
  ports:
  - port: 9092
    name: kafka
  - port: 8081
    name: schema-registry
  - port: 3030
    name: kafka-gui
  - port: 2181
    name: zookeeper
  clusterIP: None
---
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  namespace: dev
  name: kafka
  labels:
    app: kafka
    phase: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
      - name: kafka
        image: landoop/fast-data-dev
        env:
        - name: CONNECT_HEAP
          value: "1G"
        - name: ADV_HOST
          value: "kafka-service.dev"
        resources:
          limits:
            memory: "3000Mi"
            cpu: "1"

As you can see to your right (or above for those poor unfortunates trying to read this long thin piece of text on mobile), Kubernetes stuff is defined in YAML (which stands - in the great recursive acronym tradition pioneered by the likes of GNU - for YAML Ain’t Markup).

Note that we’ve created -

  1. A kafka-service to handle ingress and egress to the Kafka cluster - it is a ClusterIP service, which means no access from outside the cluster, and we’ve gone with an approach which uses selectors to route traffic to the pod via K8s DNS.
  2. A replica set with a single replica.

YAML is an irritating language to define things in; it is highly sensitive to spaces, has an awkward way of defining lists or arrays (basically a - followed by a space). Tabs are a no go in YAML and will break everything in hard to detect ways. If that’s unclear and generally unpleasant to wrap your head around… then good, I’m pleased you’re getting a feel for the format.

While YAML looks fine visually, and the intention is quite clear, it is painful to type and prone to errors. If something does go wrong, look for the use of tabs where spaces were intended, or the absence of spaces between -s or :s. The only element that bears commenting on is the labels elements - these are just a way to subset your various K8s entities for subsequent selection and manipulation. You can do things like applying services to pods based on labels, which is what is happening above.

You’ll also note the almost complete inability to abstract anything away here - if you want some common feature (e.g. a set of labels or something) across several services, you’re going to need to either use an additional tool (helm might help), write it out explicitly in each YAML file, or do something with Kubectl to add it to your request (you can do this for namespaces).

Pay attention to which particular K8s API you’re referring to in the apiVersion, as this matters, and dictates which part of the K8s REST API your commands are directed at.

I was originally going to discuss it in the next post, but decided that presenting broken k8s configs as correct is fairly cruel to anyone just trying to find a template for Kafka. Many systems (Kafka in this case) require an environment variable added the Kubernetes manifest along the lines of ADV_HOST=kafka-service.dev. This lets Kafka know which address it is listening on - Confluent explain it better than I can here. If you hit connectivity issues with a system running in Docker containers, checking that the app in the container knows which address to advertise itself at is a good first step in resolving them.

Kubernetes cluster security

We should first take steps to make sure our new cluster is secure. Sadly the only secure computer is one that is turned off, at the bottom of the ocean. This being incompatible with, well… basically every other requirement we have - we will have to make do with the clusterSecurity.yaml file in the repo, which ensures that;

  • No priveleged mode containers run.
  • No containers can run as root.
  • Containers can only access NFS volumes and persistent volume claims (for stateful set deployments). In other words, they shouldn’t be touching your local storage.
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: minikubesecurity
spec:
  privileged: false
  runAsUser:
    rule: MustRunAsNonRoot
  seLinux:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  volumes:
  - 'nfs'
  - 'persistentVolumeClaim'

This is important because one of the major issues people hit when first using Docker in anger is around the use of the root user in the containers. This is bad practice and often a material security risk in Docker (one of the few), so we want to ensure that if we’re doing it inadvertantly in dev, we get an error immediately rather than encountering mysterious issues at deployment (where we hope that the cluster admin has disabled the capability, as we have!)

Apply your security policies using kubectl create -f clusterSecurity.yaml.

The final point to be made on securing this cluster is to take note that none of the services expose public IP addresses. To access (for example) the landoop UIs, we’d run something like kubectl --namespace=dev port-forward service/kafka-service :3030, which forwards the port from our local machine over SSH. This is desirable, because we have not configured security on these GUIs, and by keeping them internal to the cluster we can piggyback off kubectl’s authentication mechanisms and simplify our setup. Clearly, not appropriate for production usage.

Kubernetes Services

We now have a functioning cluster, so let’s get visibility of it using the Kubernetes Dashboard (we’ll do better than this for monitoring, but we’re still bootstrapping and need some visibility while we do so) using minikube dashboard. On a real cluster we’d deploy pods and a service to handle this, but minikube makes things a bit simpler and lets us avoid creation of service accounts and worrying about authentication and so on.

Without any further stuffing about, let’s get it hooked up.

  1. Create a dev user and namespace with kubectl apply -f dev-cluster.yaml
  2. Run kubectl apply -f ${FILE} for each of the components we need (etcd, Fluentd, Kafka) for our dev platform and watch them appear on the dashboard.

Done. There are many improvements that could be made to this setup, but for writing an application it works as a minimum viable platform. If we were deploying this in the real world, we would have a vastly expanded range of things to think about;

  1. How does our logging actually work? Fluentd needs somewhere to land its data, and Kafka isn’t a good choice (for reasons that we’ll cover later in the series). We could use Elasticsearch and Kibana (or something similar like Prometheus and Grafana) to visualise our logs from Fluentd.
  2. Authentication for all these front ends we’re creating, and then Authorization to determine what permissions people should have.
  3. Where are we going for level two support? If we can’t fix something ourselves who do we bring in and how much money do we have to pay them?
  4. What network connectivity do we need? Load balancing? CDNs?
  5. While we’re talking about load, what about autoscaling?

6 - 100. Everything else.