Kubernetes and Python

  • Austin Godber
  • @godber

DesertPy - 2/26/2020
PLUG - 7/9/2020

Background

  • Containers
  • Kubernetes

Containers

  • "Single" Lightweight Process (not VM)
  • Isolated
    • Resource Management
    • Resource Access
  • Managed Like Cattle not Pets
  • Docker, Containerd, CRI-O

Containers

Containers aren't magic

<!img src="containers.jpeg">

Kubernetes

Kubernetes is a portable, extensible, open-source platform for managing containerized workloads and services, that facilitates both declarative configuration and automation.

Kubernetes

  • Service discovery and load balancing
  • Storage orchestration
  • Automated rollouts and rollbacks
  • Automatic bin packing (scheduler)
  • Self-healing (resource aware scheduling)
  • Secret and configuration management

"It's a container orchestration system." - Austin

Additionally, Kubernetes is not a mere orchestration system. In fact, it eliminates the need for orchestration. The technical definition of orchestration is execution of a defined workflow: first do A, then B, then C. In contrast, Kubernetes comprises a set of independent, composable control processes that continuously drive the current state towards the provided desired state. It shouldn’t matter how you get from A to C. Centralized control is also not required. This results in a system that is easier to use and more powerful, robust, resilient, and extensible. - Kubernetes Docs

Setup

All of this can be done on a Linux or MacOS desktop if you have the following installed:

  • Python/Virtualenv - To build and test the App
  • Docker - To build and test the container image with the App
  • Minikube - To run a local dev Kuberenetes cluster (runs in VM*)

Python and Kubernetes?

  • Python Applications can be deployed in Kuberenetes
  • Kubernetes Can be managed/controlled by Python

Python Applications can be deployed in Kuberenetes

  • Make the app
  • Write the Dockerfile
  • Build the container Image
  • Create a Kubernetes Deployment
  • Apply the Deployment.

Make the App

In [1]:
!find webapp/
webapp/
webapp/Dockerfile
webapp/app
webapp/app/main.py
webapp/app/static
webapp/__pycache__
webapp/__pycache__/app.cpython-37.pyc

The App

In [2]:
!head webapp/app/main.py
from starlette.applications import Starlette
from starlette.config import Config
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Mount
from starlette.staticfiles import StaticFiles


def homepage(request):
    return PlainTextResponse(f'Hello, world!\nCONFIGA: {CONFIGA}\nCONFIGB: {CONFIGB}')

A Dockerfile

Describes how to build container image:

# special sauce in here, or danger
# pull this thread:
#   https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker
FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8
LABEL maintainer="Austin Godber <godber@uberhip.com>"

RUN pip install starlette aiofiles

COPY ./app /app

Build Image

# Use Docker in Minikube
eval $(minikube -p minikube docker-env)
# Build image
docker build -t starlette .

Sending build context to Docker daemon  7.168kB
Step 1/4 : FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8
 ---> 9c64ae748955
...
Successfully tagged starlette:1

Run Image

docker run -p 8000:80 starlette:1

Create Kubernetes Deployment

A What?

Kubernetes is concept heavy and comprised of many layers of abstraction. So ..

A Deployment creates a Replica Set, which creates Pods, which run one or more Containers that contain your app.

Manifest File

  • Specifies Deployment [link]
  • Specifies Service [link]
    • "a Service is an abstraction which defines a logical set of Pods and a policy by which to access them"

Apply the Manifest and Access Service

# create resources
kubectl apply -f ./manifest.yaml
# expose service and open in browser
# this is a minikube cheat, see Ingress Controllers for prod
minikube service starlette

Finding your Service

How do you find the IP and port on your own?

# The IP is to the Minikube VM
$ minikube ip
192.168.39.2

# The port is the Service LoadBlanacer Port
$ kubectl get service
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)           AGE
kubernetes   ClusterIP      10.96.0.1     <none>        443/TCP           8d
starlette    LoadBalancer   10.97.16.32   <pending>     18000:32105/TCP   47m

See Makefile

Keeping track of all of these various commands and arguments gets fiddle-y. I like to use a Makefile to make iterating faster. See the included Makefile for tips. This is NOT how to do production though. Take a look at the helm and helmfile projects for that.

Controlling Kubernetes with Python

There is an official Kubernetes Python Client here:

https://github.com/kubernetes-client/python

Python Client Docs generated in the README.md:

https://github.com/kubernetes-client/python/blob/master/kubernetes/README.md

Also useful

Kubernetes API Docs can be found here:

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/

Get API Instance

In [3]:
from kubernetes import client, config
from kubernetes.client.rest import ApiException

# use .kube/config, could use token auth
config.load_kube_config()

core_api = client.CoreV1Api()

Create Stub Config Map

In [4]:
configmap = client.V1ConfigMap()
configmap
Out[4]:
{'api_version': None,
 'binary_data': None,
 'data': None,
 'kind': None,
 'metadata': None}

Fill in some details

In [5]:
configmap.metadata = client.V1ObjectMeta(name="starlette")
configmap.data = {}
configmap.data["CONFIGA"] = "True"

What ConfigMaps do we have?

None hopefully ...

In [6]:
!kubectl get configmap
No resources found in default namespace.

Create the ConfigMap

Call the create_namespaced_config_map() method on core_api to create the configmap in Kubernetes

In [7]:
core_api.create_namespaced_config_map(namespace="default", body=configmap)
Out[7]:
{'api_version': 'v1',
 'binary_data': None,
 'data': {'CONFIGA': 'True'},
 'kind': 'ConfigMap',
 'metadata': {'annotations': None,
              'cluster_name': None,
              'creation_timestamp': datetime.datetime(2020, 2, 26, 15, 32, 44, tzinfo=tzutc()),
              'deletion_grace_period_seconds': None,
              'deletion_timestamp': None,
              'finalizers': None,
              'generate_name': None,
              'generation': None,
              'initializers': None,
              'labels': None,
              'managed_fields': None,
              'name': 'starlette',
              'namespace': 'default',
              'owner_references': None,
              'resource_version': '2260570',
              'self_link': '/api/v1/namespaces/default/configmaps/starlette',
              'uid': '2cdda70e-3445-4e4b-afbe-34523acf8012'}}

NOW what ConfigMaps do we have?

In [8]:
!kubectl get configmap
NAME        DATA   AGE
starlette   1      0s

We can list them with the client too

In [9]:
core_api.list_namespaced_config_map(namespace="default")
Out[9]:
{'api_version': 'v1',
 'items': [{'api_version': None,
            'binary_data': None,
            'data': {'CONFIGA': 'True'},
            'kind': None,
            'metadata': {'annotations': None,
                         'cluster_name': None,
                         'creation_timestamp': datetime.datetime(2020, 2, 26, 15, 32, 44, tzinfo=tzutc()),
                         'deletion_grace_period_seconds': None,
                         'deletion_timestamp': None,
                         'finalizers': None,
                         'generate_name': None,
                         'generation': None,
                         'initializers': None,
                         'labels': None,
                         'managed_fields': None,
                         'name': 'starlette',
                         'namespace': 'default',
                         'owner_references': None,
                         'resource_version': '2260570',
                         'self_link': '/api/v1/namespaces/default/configmaps/starlette',
                         'uid': '2cdda70e-3445-4e4b-afbe-34523acf8012'}}],
 'kind': 'ConfigMapList',
 'metadata': {'_continue': None,
              'resource_version': '2260570',
              'self_link': '/api/v1/namespaces/default/configmaps'}}

They can be deleted

In [10]:
core_api.delete_namespaced_config_map(name="starlette", namespace="default", body=configmap)
Out[10]:
{'api_version': 'v1',
 'code': None,
 'details': {'causes': None,
             'group': None,
             'kind': 'configmaps',
             'name': 'starlette',
             'retry_after_seconds': None,
             'uid': '2cdda70e-3445-4e4b-afbe-34523acf8012'},
 'kind': 'Status',
 'message': None,
 'metadata': {'_continue': None, 'resource_version': None, 'self_link': None},
 'reason': None,
 'status': 'Success'}

Gone!

In [11]:
!kubectl get configmap
No resources found in default namespace.

CRUD

Basic CRUD on all Kubernetes Resources.

Keep in mind, we're just modifying desired state here, Kubernetes is doing the work in the background to make sure that the actual state matches the desired state. It doesn't always work out.

Other Roles for Python in Kubernetes

Kubernetes and Python

Thank you!

  • Austin Godber
  • @godber

DesertPy - 2/26/2020
PLUG - 7/9/2020