From monolithic to Kubernetes
Intro
The aim of this mini guide is to introduce the concept of container in order to build microservices managed through kubernetes.
The highest aim is to focus on shipping your ideas and making your users happy.
Back to the past
- many hours to deploy;
- single source code;
- single point of failure.
I remember a project in which i was a developer 3 years ago. In a single source code we had PHP+AltoRouter in order to expose HTTP routes and Smarty Template Engine to pass PHP variables to HTML. Javascript was a hell, but this is an other story.
So if i wanted to change a front-end label or a critical logic in PHP i had to make changes in the same source code.
Microservices
Interviewer: "How do you coordinate all those pieces that are out of there?"
Adrian Cockcroft (ex Cloud Architect on Netflix): "You don't coordinate, right?" [2]
Microservices are:
- Modular;
- Easy to deploy;
- Scale Independently.
12 factor
In order to start building microservices we must read at least the 12factor; a bible where we can find 12 fondamental principles to apply while developing software-as-a-service apps.
12factor is Written by the founder and CTO of Heroku, Adam Wiggins (https://www.linkedin.com/in/adam-wiggins-a7623845/).
12 principles
I. Codebase: One codebase tracked in revision control, many deploys
II. Dependencies: Explicitly declare and isolate dependencies
III. Config: Store config in the environment
IV. Backing services: Treat backing services as attached resources
V. Build, release, run: Strictly separate build and run stages
VI. Processes: Execute the app as one or more stateless processes
VII. Port binding: Export services via port binding
VIII. Concurrency: Scale out via the process model
IX. Disposability: Maximize robustness with fast startup and graceful shutdown
X. Dev/prod parity: Keep development, staging, and production as similar as possible
XI. Logs. Treat logs as event streams
XII. Admin processes: Run admin/management tasks as one-off processes
What is a container?
In order to garantee portability and heterogeneous technology stacks we need containerized microservices. We can imagine a container like an app on a phone full of others etherogeneous apps.
Container is a technology that makes it easy to run and distribute applications across different operating environments. They are similar to Virtual Machine but much lighter weight.
Container ensures *:
- Indipendent packages;
- Namespace isolation.
Most used container technologies:
Containers with Docker
Problem: easly run different versions of nginx on a linux server.
Without container: In a linux virtual machine we usually have a unique nginx service and in order to start/stop/restart it we use
service nginx <command>
In order to change nginx configs we need to edit "/etc/nginx/nginx.conf" file but it's not a safe editing because it has a complex structure.
With docker containers:-)
version: '3'
services:
web1:
image: nginx:1.17.2
volumes:
- ./mysite.template:/etc/nginx/conf.d/mysite.template
ports:
- "8080:80"
environment:
- NGINX_HOST=foobar.com
- NGINX_PORT=80
command: /bin/bash -c "envsubst < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
web2:
image: nginx:1.9.8
volumes:
- ./mysite2.template:/etc/nginx/conf.d/mysite2.template
ports:
- "8081:80"
environment:
- NGINX_HOST=foobar2.com
- NGINX_PORT=80
command: /bin/bash -c "envsubst < /etc/nginx/conf.d/mysite2.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
In a single file called docker-compose.yml (https://docs.docker.com/compose/gettingstarted/) we have achieved both goals:
- running different version of nginx;
- easy configure through environments variable.
Packaging containers is like 5% of the problem. The real problems are:
- app configuration;
- service discovery;
- managing updates;
- monitoring.
While we can build all those things on top of Docker, it's better delegating all these complexity to a platform.
This is where Kubernetes comes in.
Previously we focused on deploying in a virtual machine which lock you into a limited workflows. Kubernetes allows us to abstract the individual machines and treat the entire cluster like a single logical machine.
Kubernetes!
Assumption
First of all, you can't build microserivces with a waterfall organization.
Where/When is born?
Kubernetes was founded by Joe Beda, Brendan Burns and Craig McLuckie.
First announced by Google in mid-2014 influenced by Google's Borg system. Borg is a Cluster OS Google uses for managing internal workloads.
Kubernetes is a production-ready, open source platform designed with Google's accumulated experience in container orchestration, combined with best-of-breed ideas from the community. Kubernetes coordinates a highly available cluster of computers that are connected to work as a single unit.
Every Kubernetes Node runs at least:
- Kubelet, a process responsible for communication between the Kubernetes Master and the Node; it manages the Pods and the containers running on a machine.
- A container runtime (like Docker, rkt) responsible for pulling the container image from a registry, unpacking the container, and running the application.
But what is a Pod?
Let's start talking from bottom of Kubernetes. The smallest unit is the Pod. A pod is a collection of one or many containers and volumes.
- Every containers in a Pod share the same namespace;
- A Pod has an unique ip address.
Containers should only be scheduled together in a single Pod if they are tightly coupled and need to share resources such as disk.
I really recommend to read this paper [1] where a founder of Kubernetes talks about distribuited system patterns and multi-container application patterns.
Let's go inside a pod yml.
cat pods/healthy-monolith.yaml
apiVersion: v1
kind: Pod
metadata:
name: "secure-monolith"
labels:
app: monolith
spec:
containers:
- name: nginx
image: "nginx:1.9.14"
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
volumeMounts:
- name: "nginx-proxy-conf"
mountPath: "/etc/nginx/conf.d"
- name: "tls-certs"
mountPath: "/etc/tls"
- name: monolith
image: "udacity/example-monolith:1.0.0"
ports:
- name: http
containerPort: 80
- name: health
containerPort: 81
resources:
limits:
cpu: 0.2
memory: "10Mi"
livenessProbe:
httpGet:
path: /healthz
port: 81
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /readiness
port: 81
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 1
volumes:
- name: "tls-certs"
secret:
secretName: "tls-certs"
- name: "nginx-proxy-conf"
configMap:
name: "nginx-proxy-conf"
items:
- key: "proxy.conf"
path: "proxy.conf"
Create the pod
kubectl create -f pods/health-monolith.yaml
Examine pods
kubectl get pods
Monitoring and health checks in Kubernetes [6]
Liveness probes
The kubelet uses liveness probes to know when to restart a Container. For example, liveness probes could catch a deadlock, where an application is running, but unable to make progress. Restarting a Container in such a state can help to make the application more available despite bugs.
Liveness config example (this is a section inside the pod yml):
livenessProbe:
httpGet:
path: /healthz
port: 81
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 5
Imagine two people, the name of the first is Kubelet and the second one is Pod1. Periodically Kubelet tries to call Pod1: "Hey Pod1, are you alive?".
If Pod1 doesn't answer, Kubelet reanimate him.
The calls are obviously in HTTP protocol and the reanimation is a restart of the Pod. This is the liveness check of kubelet.
Readiness probes
The kubelet uses readiness probes to know when a Container is ready to start accepting traffic. A Pod is considered ready when all of its Containers are ready. One use of this signal is to control which Pods are used as backends for Services. When a Pod is not ready, it is removed from Service load balancers.
Readiness config example (this is a section inside the pod yml):
readinessProbe:
httpGet:
path: /readiness
port: 81
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 1
App config and security overview in Kubernetes
Kubernetes provides two ways to inject configs:
- configMaps: used for not sensitive data (es. nginx.conf)
- secrets: used for sensitive data (es.tls certificates)
Let's try to create a configMap for proxy.conf file of nginx and check it.
Now, same things but for secrets.
Once configMaps/Secrets are creates we need to attach it, we do it through the "volumeMounts" section in pod yml:
volumeMounts:
- name: "nginx-proxy-conf"
mountPath: "/etc/nginx/conf.d"
- name: "tls-certs"
mountPath: "/etc/tls"
We need also, always in pod yml, to declare volumes:
volumes:
- name: "tls-certs"
secret:
secretName: "tls-certs"
- name: "nginx-proxy-conf"
configMap:
name: "nginx-proxy-conf"
items:
- key: "proxy.conf"
path: "proxy.conf"
How to expose pods
Deployment
Deployment yml has the same template as pod yml.
Pods are suitable only for dev purpose, deployment, instead, also for production.
The following are typical use cases for Deployments:
- Create a Deployment to rollout a ReplicaSet. The ReplicaSet creates Pods in the background. Check the status of the rollout to see if it succeeds or not.
- Declare the new state of the Pods by updating the PodTemplateSpec of the Deployment. A new ReplicaSet is created and the Deployment manages moving the Pods from the old ReplicaSet to the new one at a controlled rate. Each new ReplicaSet updates the revision of the Deployment.
- Rollback to an earlier Deployment revision if the current state of the Deployment is not stable. Each rollback updates the revision of the Deployment.
- Scale up the Deployment to facilitate more load.
- Pause the Deployment to apply multiple fixes to its PodTemplateSpec and then resume it to start a new rollout.
- Use the status of the Deployment as an indicator that a rollout has stuck.
- Clean up older ReplicaSets that you don’t need anymore.
Let's go scaling!
These are the 2 scaling modes available:
- Horizontal Pod Autoscaler based on the following rule
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]
Algorithm detail -> https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#algorithm-details
- Manual using CLI
Multiple instances benefits
PRO: Once you have multiple instances of an Application running, you would be able to do Rolling updates without downtime.
Rolling updates allow Deployments' update to take place with zero downtime by incrementally updating Pods instances with new ones. By default, the maximum number of Pods that can be unavailable during the update and the maximum number of new Pods that can be created, is one.
Namespaces
Namescapaces allow us to share projects/environments inside the same cluster.
An interesting feature is limit cpu usage of a namespace https://kubernetes.io/docs/tasks/administer-cluster/manage-resources/cpu-constraint-namespace/.
Es. As a cluster administrator, you might want to impose restrictions on the CPU resources that Pods can use. For example:
- Each Node in a cluster has 2 CPU. You do not want to accept any Pod that requests more than 2 CPU, because no Node in the cluster can support the request;
- A cluster is shared by your production and development departments. You want to allow production workloads to consume up to 3 CPU, but you want development workloads to be limited to 1 CPU. You create separate namespaces for production and development, and you apply CPU constraints to each namespace.
Scaffolding a Kubernetes cluster
Kubernetes addons
https://kubernetes.io/docs/concepts/cluster-administration/addons/#service-discovery
General Configuration Tips
- When defining configurations, specify the latest stable API version;
- Configuration files should be stored in version control before being pushed to the cluster. This allows you to quickly roll back a configuration change if necessary. It also aids cluster re-creation and restoration;
- Write your configuration files using YAML rather than JSON. Though these formats can be used interchangeably in almost all scenarios, YAML tends to be more user-friendly;
- Group related objects into a single file whenever it makes sense. One file is often easier to manage than several. See the guestbook-all-in-one.yaml file as an example of this syntax;
- Note also that many
kubectl
commands can be called on a directory. For example, you can callkubectl apply
on a directory of config files; - Don’t specify default values unnecessarily: simple, minimal configuration will make errors less likely;
- Put object descriptions in annotations, to allow better introspection.
References
1) https://static.googleusercontent.com/media/research.google.com/it//pubs/archive/45406.pdf
2) https://classroom.udacity.com/courses/ud615
4) https://github.com/udacity/ud615
5) https://kubernetes.io/docs/home/
6) https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
Notes:
*ensure!==it always do it. To ensure it you need to correctly use docker.