Container orchestration is quickly becoming a defacto standard among tech firms. As software requirements become more and more complex and service outages are increasingly expensive, the systems we build have to scale up seamlessly and be highly available.
Kubernetes or k8s is one possible tool to fulfill that job. In it’s own words:
Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications.
At the core of k8s there are a few abstractions that we have a look at to get started with orchestrating and automating our containers.
These concepts allow us to engineer products that are resilientand self healing in a concise manner.
The resources necessary for k8s are handled by kubectl - either manually or via manifest files. For automation purposes manifest files are best practice. All Manifest files follow the same structure and can be interpreted in multiple formats. Here an example in yaml
apiVersion: v1 kind: Pod metadata: name: podname namespace: ns labels: key1: value1 key2: value2 spec: ...
The following base elements are specified for each manifest file
- apiVersion - The version of the k8s API
- kind - The type of object that is to be created
- metadata - Metadata including the name, namespace, and labels to identify the resource
- spec - The specification of the object
This allows us to create and destroy resources within a k8s cluster. Now let’s have a closer look at the types of resources we can create.
Pods are the smallest unit of scheduling in k8s. They can contain multiple tighly coupled containers, that share all resources. This enables containers in the same pod to communicate as if they are colocated on the same logical host. This colocation imples the access to the same port ranges, memory, and file access.
The following example describes a pod including a single nginx container exposing port 80 to the other pods.
apiVersion: v1 kind: Pod metadata: name: nginx labels: app: srv spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
Manually deploying pods to k8s clusters is not common practice. Deployments that create ReplicationControllers, which make sure that there is a certain number of pods running at a certain time, handle the life cycle of pods.
Deployments exist to regulate the creation, destruction, and life cycle management of pods. A deployment depicts the target state of the application. If a new version of an application gets deployed to a k8s cluster, deployments take care of the bureaucracy of spinning up new pods and fading out old ones without affecting uptime.
The following deployment spins up 3 replicas of a nginx container
apiVersion: apps/v1 kind: Deployment metadata: name: srv-deploy labels: app: srv spec: replicas: 3 selector: matchLabels: app: srv template: metadata: labels: app: srv spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
The template field of the deployment specification follows the exact syntax of a pod definition. If the manifest is changed and applied to the cluster, in the background k8s spins up another deployment, waiting for the containers to be available, then gradually removes the old pods.
Up and down scaling of the replicas is handled by a ReplicationController (RC). It’s job is to make sure that a specified number of replicas are up and running at any given moment. If a pod crashes or becomes unresponsive, the RC will terminate the dying pod and spawn another one as a replacement.
When creating multiple replicas of a pod, we need an abstraction to handle access in a unified way. Services provide exactly that abstraction.
Services tie a group of pods together to a unified endpoint by using label selectors. They also represent addressable named units within a namespace.
The following manifest shows a service that selects all pods with the app: srv label and then exposes port 80.
apiVersion: v1 kind: Service metadata: name: somesvc spec: selector: app: srv ports: - protocol: TCP port: 80
The service we created is limited to the local k8s network and currently isn’t exposed via a public IP. To expose services to the internet we create a resource called ingress.
To fulfill the promise of multitenancy, a k8s cluster needs to be able to route to multiple endpoints independently. This is where ingress comes in, as the name already implies ingress has the ability of reverse proxying from the internet into the cluster.
The following example routes s1.example.com to the service s1 and s2.example.com to s2.
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test spec: rules: - host: s1.example.com http: paths: - backend: serviceName: s1 servicePort: 80 - host: s2.example.com http: paths: - backend: serviceName: s2 servicePort: 80
Ingress, being the primary endpoint for any incoming connection, also supports TLS.
Now we have a way of deploying our services, scaling them independently, and routing to them from the internet. This covers the very basics of k8s.
Kubernetes is a powerful orchestration tool. With the above mentioned resources we can already automate a service to run in our cluster.
Here a few best practices to adhere by when dealing with k8s resources
- Never deploy pods on their own - always use deployments to manage their lifecycle
- Use one container per pod - this avoids port collisions and keeps your containers independently scalable
- Only use the latest tag for internal docker images, never for external ones
- Create services before deployments are created
- Deployments take some time to ramp up
- Services are created instantly
- Use ingress to expose services to the outside world, not services directly
- Include the orchestration logic in the service’s version control system
- Create a CICD pipeline that deploys to your cluster in an automated fashion
There are additional concepts of vital importance to a k8s cluster. The following is a non-exhaustive list of additional kinds to explore
- Namespace - Used, among other things, to avoid naming conflicts when dealing with multiple environments
- ConfigMap and Secret - Introduce potentially shared static configuration to the pods
- StorageClass and PersistentVolumeClaim - Used to store persistent data and share data among pods
- NetworkPolicy - Used to regulate egress and ingress access to certain nodes and services
- Job and CronJob - Used to run single or periodic jobs on a k8s cluster
For more information, refer to the k8s documentation5