Node JS- Getting Started
Node.js is a highly popular, scalable, open-source server. It uses Google Chrome V8 JavaScript engine at its core. It was developed by Ryan Dahl based on the concept of non-blocking IO in 2009.
In a layman’s view, the NodeJS is the Chrome’s V8 engine converted into a backend server. In this process, we have removed the DOM related libraries for the browser display. As part of the back-end needs, we have added many crucial libraries for the IO operations with filesystem, database and the network systems etc. These libraries enables us to keep these major time consuming IO operations asynchronous and non-blocking.
As a result of these IO libraries, we are able to keep the major overhead of a back end server away from the single processing thread. And, in turn, it allows the processing thread to be free and run the programs as fast as we observer in the browsers.
The removal of the delays due to IO operations makes NodeJS highly scalable for the data intensive applications compared to its multi-threaded server counterpart. The article on ‘Single Vs Multi-Thread Model‘ explains this difference using a simulation.
Apart from being scalable, the other key advantage is its ease of learning. With the same language in use for both the front-end and the back-end, it becomes easy to shuffle and manage the teams. A good end to end knowledge, greatly improves the interest and the contribution from the team members as well. No wonder why, we have such a large community base and a huge set of libraries in the NPM registry!
Before we move on to the next topic, here is the summary of the key benefits including what we have discussed so far.
Key Benefits of NodeJS
- Highly Scalable: The non-blocking IO implementation makes the NodeJS server highly scalable for the data intensive applications. It enables us to handle significantly high amount of traffic with minimal front-end servers.
This is the one of the core reason behind its fast paced growth and wide level of acceptance. Most of the leading enterprises including the likes of LinkedIn, Uber, Twitter, Microsoft, Netflix use NodeJS for many of their high volume applications. - Easy Learning Curve : The usage of the same language for both client and server side code makes the learning easy in NodeJS.
- Lesser Compatibility Issue : The server-side compilation allows us to use the latest ECMA scripts syntax, worrying much less about clients browser versions. The ECMA transpilers can take care of many of the compatibility issues. The frameworks like Angular, React and Vue are some the example providing easy to use frontend frameworks using ECMA scripts.
- Huge Open-source Libraries : It’s a highly popular framework enjoying a huge community base. And, it has million plus open-source libraries available in it’s npm registry.
NodeJS IDEs
It is well supported by a good set of tools and IDEs. Some of the prominent IDEs for NodeJS development are :
Desktop IDE | Cloud Based |
---|---|
Atom by GitHub | Eclipse Cloud IDEs by Eclipse |
Visual Studio Code by MicroSoft | AWS Cloud9 by AWS |
IntelliJ IDEA or WebStorm by JetBrains | |
Komodo IDE by ActiveState | |
Sublime Text by Sublime HQ | |
Brackets by Adobe |
Kubernetes – POD
A Pod is a basic unit of deployment that encloses one or more containers in it. The Pod API object is used to create a single instance of a Pod.
An application may need multiple things for its operation as shown in the diagram. A container that run the app may not support all of them; a Docker container does not provide an in-built support for external storage for instance.
The Pod that encloses the containers, builds the features needed for the underlying application instances. It fulfills the needs of the app and the needs of framework to manage the app instance as shown.
As shown in the diagram, each Pod gets a separate virtual IP as its address and acts like a separate virtual machine. With the virtual networking model K8s keeps the applications secured and independent from the from outside world. It also helps K8s automate internal traffic routing and service discovery.
Lifecycle , Restart Policy , Hooks & Probes
As the system plans to create an instance of a Pod, the system asks the scheduler to find a suitable Node. As we find the node, the kubelet on the Node is handed over the Pod spec to create and manage it. The kubelet ensures the Pod and the containers within it run as per the specifications. And, keeps updating their status to the Master node.
This diagram briefly consolidates Pod and Container life-cycle events to show the order of their occurrence in a normal course of action. Apart from the order, it shows their relationship and significance.
Creating , Exploring & Debugging a POD
A simple POD
test-pod.yaml
apiVersion: v1 kind: Pod metadata: name: demo-app label: app: demo-app spec: containers: - name: hello-echo image: k8s.gcr.io/echoserver:1.10 ports: - containerPort: 8080
kind – Says its a POD API object.
name – Gives a unique name to these objects
labels – Allows us to give a group name to multiple instances of the Pod
image – specifies the app build IMAGE file to be used
port – Provides the port at which the app will run
After creating this file, we can use the following command to deploy it:
kubectl apply -f test-pod.yaml
Since its simple POD , we can also conveniently deploy it using a command line as follows :
kubectl run hello-echo \ --image=k8s.gcr.io/echoserver:1.10 \ --port=8080 \ --generator=run-pod/v1
Deploy the Pod
Lets deploy it using the command line on a minikube environment and explore:
$ kubectl run hello-echo --image=k8s.gcr.io/echoserver:1.10 --port=8080 --generator=run-pod/v1 pod/hello-echo created $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES hello-echo 1/1 Running 0 48s 160.74.10.26 minikube <none> <none>
As we see, the Pod is running on the minikube Node with its virtual IP 160.74.10.26 .
Inside the Pod,we can access the application using localhost or the Pod’s IP. It is to be noted that, there can be multiple Pods on a Node, the localhost inside a Pod refers to the Pod IP , not to the Node’s ip.
exec : Login to container workspace & access the application
As we can not access this IP from outside,let us login to the container and try accessing the application.
#Command to login to the container workspace $ kubectl exec -it hello-echo -- /bin/bash root@hello-echo:/# #Access the application inside the cluster using localhost or pod ip root@hello-echo:/# curl http://localhost:8080 | head -4 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 404 0 404 0 0 28230 0 --:--:-- --:--:-- --:--:-- 28857 Hostname: hello-echo
Logs : Explore the container logs
Here we are accessing the logs using only the Pod name. If there multiple containers, we can specify the container name after the pod name.
$ kubectl logs hello-echo Generating self-signed cert Generating a 2048 bit RSA private key writing new private key to '/certs/privateKey.key' ----- Starting nginx 127.0.0.1 - - [07/Jul/2020:07:13:51 +0000] "GET / HTTP/1.1" 200 416 "-" "curl/7.47.0" 127.0.0.1 - - [07/Jul/2020:07:15:17 +0000] "GET / HTTP/1.1" 200 416 "-" "curl/7.47.0"
Get : Explore a detailed runtime Pod state and status history
The ‘get’ command using ‘-o yaml’ provides the important information about the selected object as stored in the system.
We can explore the detailed runtime Pod status as below:
$ kubectl get pod hello-echo -o yaml ----- kind: Pod metadata: creationTimestamp: "2020-07-07T07:10:29Z" labels: run: hello-echo name: hello-echo namespace: default --------- spec: containers: - image: k8s.gcr.io/echoserver:1.10 imagePullPolicy: IfNotPresent name: hello-echo -------
Status part in the same YAML output shows the status history of the Pod. The latest container status is also shown at the end.
status: conditions: - lastProbeTime: null lastTransitionTime: "2020-07-30T15:13:05Z" status: "True" type: Ready - lastProbeTime: null lastTransitionTime: "2020-07-30T15:13:05Z" status: "True" type: ContainersReady - lastProbeTime: null lastTransitionTime: "2020-07-30T15:12:56Z" status: "True" type: Initialized - lastProbeTime: null lastTransitionTime: "2020-07-30T15:12:56Z" status: "True" type: PodScheduled containerStatuses: - image: k8s.gcr.io/echoserver:1.10 name: hello-echo ready: true started: true state: running: startedAt: "2020-07-30T15:13:04Z"
Describe : Provides a more formatted Pod status & associate events
The describe provides a more concise and formatted Pod status information and the list associated event.
#Lines has been removed from the output for clarity $ kubectl describe pod hello-echo | less Name: hello-echo Namespace: default Node: minikube/160.74.10.26 Labels: run=hello-echo Status: Running IP: 160.74.10.26 Containers: hello-echo: Container ID: docker://7507054f5a3f34c60d080323e5f1f0c2f9b8c588e85a423e51f31b383bb011f3 Image: k8s.gcr.io/echoserver:1.10 Port: 8080/TCP State: Running QoS Class: BestEffort Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 45m default-scheduler Successfully assigned default/hello-echo to minikube Normal Pulled 45m kubelet, minikube Container image "k8s.gcr.io/echoserver:1.10" already present on machine Normal Created 45m kubelet, minikube Created container hello-echo Normal Started 45m kubelet, minikube Started container hello-echo
Delete the POD
$ kubectl delete pod hello-echo pod "hello-echo" deleted
Conclusion
A Pod API object creates a single instance of a Pod. Creation and a deletion of each instance needs to be done manually.
The API objects like ReplicaSet, Deployment, Job, StatefulSets etc add features to automate the scaling, version upgrades of the Pod through configuration in the API objects. These API object use the Pod specification as a template to create the Pod instances.
The above paragraph just says, never use a Pod for deploying your application. Choose an appropriate Deployment API object to simplify your deployment.
Pod & Container Life-Cycle
This diagram consolidates Pod and Container life-cycle events to show the order of their occurrence in a normal course of action. Apart from the order, it shows how these life-cycle events are dependent on each other to get a better understanding of their significance.
Reference from Official Documents
Here are the related links from the Kubernetes official site for more information.
Kubernetes- Features
Kubernetes automates our deployment activities. It lets us define our desired system using its templates called the Kube API objects. As discussed in our previous article, it simplifies the infrastructure provisioning, network configuration and manages the containers at scale.
The diagram provides a summary of the features it provides. The diagram has our application at the center and shows various features the framework provides for its easy management.
Summary of the Features
Horizontal Scaling : Kubernets allows us to scale our applications through simple configurations. Moreover, the framework takes care of the distribution of the loads. And, it also allows us to add new servers to meet the extra resource requirements.
Auto Scaling : K8s enables us to auto-scale based on our usage. We may configure our application to add new instances as the current usage exceeds 90% of CPU or 85% of memory for instance. This is quite useful in addressing peak loads and unexpected in spikes in application usage.
Auto Recovery : Kubernetes keeps monitoring the state of the system at frequent intervals. In case of any deviation, it quickly starts its restoration process to bring the actual state back to the desired state. As it restores, its does this with its in-built best practices. For instance, it can stop the traffics towards the failed server or application instances and re-start it as the instance are restored.
Auto Placement : The best-practices in K8s ensures the optimal utilization of the resources. As part of this, every time we plan to create new a instance of our application, it uses a scheduler to evaluate the most suitable server.
Auto Deployment: It supports rolling update deployment which can automate our deployment and version upgrades. It ensures the availability of the application, even during deployment, as it migrates the instances one by one. Moreover, if the new instances fail to come up, it can stop deployment process. But, we need to ensure new versions are backward compatible to fully utilize a rolling deployment. Hence, to address compatibility concerns it supports other deployments as well.
Auto Discovery & Service Exposure: A traditional setup involves a lot of effort to address the discovery and service exposure. However, K8s automatically handles complexities using its meta-data and configuration in its API objects.
Flexible Storage Options : It supports plugging in a wide range of storage options including local and cloud storage.
Portable Applications : It provides an elegant solutions to externalize the configuration and confidential information. Thus, it makes it easy to build portable applications.
Flexible Server Choice : Kubernetes is portable. We can install it on a bare-metal, virtual machines or cloud servers. We can use it as a private cloud, move it to a public cloud or use it as a hybrid.
Its being a container management system, it also enables us to easily plug-in other cloud-native solutions.
Supporting Tools & Solutions
Kubernetes is one of the best tools for automating our deployment activities at scale. Its main focus is to manage the deployment infrastructure and the deployed applications.
We need a right set of supporting tools to effectively utilize this in our development process. Many of these tools are open-source and some are part of the K8s add-on. Here are some of those key supporting tools:
- CI\CD & Image Registry :
- Kubernetes starts after we are ready with our containerized builds. A private image registry (like nexus, JFrog, Docker etc) and a suitable CI\CD tool like Jenkins act as the input for our deployment process.
- Logging and Monitoring Tools :
- Essential tools for monitoring the health, critical issues, alerts and useful matrices.
- Example Tools – K8s dashboard, Prometheus, Grafana,Weave Scope, EFK stack, cAdvisor, Kube-state-metrics
- Tracing Tools :
- They usually provide visual tracing which is very very useful in tracing transactions and performance bottlenecks in a micro-service based applications in distributed environments.
- Example Tools – OpenTracing, ZipKin, DynaTracing and Jaeger
- Right Ingress Solution :
- These solution can greatly enhance the power of Ingress load balancing, performance and security features for external client communications.
- Example solutions – NGNIX, Kong, AWS ALB , Ambassador, Gloo etc.
Kubernetes – Routing Services with Ingress
Kubernetes Ingress simplifies the routing of our external traffic (http & https) to our internal services. It can handle traffics from multiple domains for us.
Ingress supports implementations from multiple vendors such as NGNix, Kong, HAProxy, Ambassador and many others. They add many useful features to manage the communication with the outside world. They include features like static page caching, TLS termination, size compression, session affinity etc.
The Ingress module mainly consists of an Ingress API object that defines the requirements. And, an Ingress controller that fulfills the Ingress.
How does it simplify routing ?
Suppose we have two domains for two of our applications as shown.
Ingress lets us develop our features as separate services. For publishing, we can simply keep adding our services (service IP and port ) against the desired url paths as shown. And, its that simple! The Ingress controller is capable handling the rest for us.
How does the controller handle the Ingress routing request ?
To make the traffics from both the domains reach the Ingress Host, we can assign both the domains as host names to our Ingress Host IP. It is know as virtual hosting.
As the traffic from both the domains hit Ingress host, the Ingress controller would read the destination url in the request header. Based on the traffic routing rules, it would rout our requests to appropriate K8s services.
What other benefits do we get out of this routing feature ?
First, it separates the design of our back-end services from the our desired presentation. The reward feature and the beta version, as shown, even being deployed separately, are exposed under the same game app. It’s as if they are deployed together.
Second, it lets us share our Layer 4 Load Balancers for multiple services. A load balancer can manage the apps and expose them as internal services on different ports. Ingress can expose these services to outside, on the desired domains and url paths.
A sample Ingress object
The Ingress object shows the routing rules for the above example. A couple of path has been removed to keep it short.
The annotation under the metadata specifies the Ingress controller to be used. Here, we have used – nginx.ingress.kubernetes.io . The ngnix supports to import different features separately. ‘rewrite-target’ is one such feature.
As we can deploy multiple Ingress Controllers, it is important to specify this to point to the correct one.
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: ingress-routing-example annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: fungames.com http: paths: - path: / backend: serviceName: service-G1 servicePort: 80 - path: /reward backend: serviceName: service-G2 servicePort: 8080 - host: prettygifts.com http: paths: - path: /shop backend: serviceName: service-P1 servicePort: 8090 - http: paths: - path: / backend: serviceName: service-D servicePort: 8090
Secure a Service using TLS termination
This is another feature supported by Ingress. Using TLS termination, we can create a secured tls channel between the client and the Ingress load balancer.
The ingress and the secret API object shows the configuration for the TLS termination. The highlighted lines tell the host-name to secure and the secret to use. The keys tls.crt and tls.key in the secret are the certificate and private key for tls communication.
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: ingress-tls-demo namespace: my-prortal spec: tls: - hosts: - ssl.demoservice.com secretName: tls-secret rules: - host: ssl.demoservice.com http: paths: - path: /cart backend: serviceName: cart-service servicePort: 8080
apiVersion: v1 kind: Secret metadata: name: tls-secret namespace: my-prortal data: tls.crt: [base64 encoded cert] tls.key: [base64 encoded key] type: kubernetes.io/tls
Demo : How to expose a K8s Services using Ingress.
As we discussed above, its as simple as adding routing rule to our service. We will try this out using a simple hello-app application from google-samples.
Step 1: Deploy Ingress Controller
This maps the url to our service as per the mappings in the Ingress object.
Ingress controllers are not started by default, we need to start it on our cluster. Use the below command to enable and verify it on a minikube.
$ minikube addons enable ingress * The 'ingress' addon is enabled $ kubectl get pods -n kube-system | grep ingress nginx-ingress-controller-6fc5bcc8c9-4ztcn 1/1 Running 0 2m36s
Step 2: Deploy the services on a NodePort and ensure its accessible
As we will map this service, we will ensure its working.
Follow the steps under these two tabs to deploy and verify the service.
$ kubectl create deployment hello --image=gcr.io/google-samples/hello-app:1.0 deployment.apps/hello created $ kubectl expose deployment hello --type=NodePort --port=8080 service/hello exposed
$ kubectl get service hello NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hello NodePort 10.109.247.168 <none> 8080:32321/TCP 78s //Easy way to get the url $ minikube service hello --url http://160.74.10.26:32321 //Verify if the service is working $ curl $(minikube service hello --url) Hello, world! Version: 1.0.0 Hostname: web-557f59c6cf-rt9cv
Step 3: Create host name for our minikube
We are creating a host name that hits our ingress controller deployed on minikube.
Check our minikube ip and add a host name to /etc/hosts as shown.
$ minikube ip 160.74.10.26 //Add this entry to /etc/hosts 160.74.10.26 greetings.org
Step 4: Create the Ingress Object, add the routing rule and deploy
We will add a mapping for our service to expose it on a desired url.
test-ingress.yaml
apiVersion: networking.k8s.io/v1beta1 # for versions before 1.14 use extensions/v1beta1 kind: Ingress metadata: name: example-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: greetings.org http: paths: - path: /say-hello backend: serviceName: hello servicePort: 8080
We have configured to access our hello app at http://greetings.org/say-hello.
Let us deploy using ‘apply’ command and verify :
$ kubectl apply -f ingress.yaml ingress.networking.k8s.io/example-ingress created $ kubectl get ingresses NAME HOSTS ADDRESS PORTS AGE example-ingress greetings.org 160.74.10.26 80 3m36s $ kubectl describe ingresses example-ingress |head -20 Name: example-ingress Namespace: default Address: 160.74.10.26 Default backend: default-http-backend:80 (<none>) Rules: Host Path Backends ---- ---- -------- greetings.org /say-hello hello:8080 (160.78.0.7:8080)
We can verify our rules as well as the Host address where the Ingress is deployed as highlighted.
Step 6 – Lets verify if we are able access our app on our rout path
Time to test our Ingress Routing!
$ curl greetings.org/say-hello Hello, world! Version: 1.0.0 Hostname: hello-6649fc6cf7-tnhcn
Our routing is working ! We are able to access our service on our configured path.
Conclusion
Sitting at the edge server the Ingress can manage lot of features required as part of our communication with the outside world.
The major one being the Ingress (HTTP & HTTPS) traffic routing. This feature allows us to build applications using simpler services. It enables us to share our load balancers between multiple services. In simple terms, it provides the flexibility to expose any of our service on any url path available.
We can use the Ingress Controllers from multiple vendors. As a reverse proxy they provide many features for managing traffic. The features include TLS routing, session affinity, static page caching, Denial of Service, size compression etc.
Kubernetes – Job
A Kubernetes Job can run multiple instances of a Pod to complete a set of tasks. It can run them in sequence or in parallel or in a mixed mode. If all the instances complete successfully, the Job controller marks the Job as complete.
How to plan your execution ?
The Job includes one Pod template. It only creates multiple instances of the Pod and allows you to run them in sequence, parallel or in a mix mode. Each instance acts as worker node, as shown, to execute a part of the task. As regards the tasks, a file , a database or a queue might be holding these tasks. Here are the possible approaches that we can take:
- Single Pod instance per Job – A single Pod can fetch and process all task in a queue in a loop.
- Parallel Pod instances per Job – Multiple instances can be fetch and share the tasks from a given queue, to process them in parallel. Each instance gets over as it finds no more task.
- Sequential Pod instances per Job – This can be a case of a known set of dependent steps that needs to be executed one after the other.
- ‘Process Applications’, ‘Archive Rejected’ and ‘Daily Report’ can be 3 steps in a Job for which we can use 3 Pods in a sequence.
Kubernetes Jobs supports the parallel and the Sequential execution of pods using two simple configurations, as specified below:
spec: parallelism: 3
spec: completions: 3
Let us start with creating a Job with a single Pod.
Create a Job with a Single Pod
demo-job.yaml
apiVersion: batch/v1 kind: Job metadata: name: demo-job spec: template: metadata: name: worker-pod spec: containers: - name: worker-pod image: busybox args: - /bin/sh - -c - "for task in 1 2 3 4 5; do echo Task -${task} completed!; done;echo 'Done !'" restartPolicy: OnFailure
demo-job.yaml is a simple Job that simply mocks few tasks and prints the completion statements. Lets deploy the Job using the below command and explore the output :
kubectl apply -f demo-job.yaml
$ kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/demo-job 1/1 3s 37s NAME READY STATUS RESTARTS AGE pod/demo-job-5v8mx 0/1 Completed 0 37s $ kubectl logs demo-job-5v8mx Task -1 completed! Task -2 completed! Task -3 completed! Task -4 completed! Task -5 completed! Done !
As we notice, the Job has created a Pod based on the template. The Pod ideally implements the logic to fetch and execute a set of tasks as mentioned above. Here, we have simply printed some statements to mock those tasks. As the worker Pod completes, the Job finishes successfully showing 1\1 worker Pod has completed.
How to automate the cleanup of the completed Jobs ?
The Job controller retains the Job and its Pods to preserve the logs for verification and error analysis, even after its completion. You may delete them separately or delete the Job to delete them all.
To automate the cleanup, we can make use of the TTL mechanism as shown below which will do the cleanup 600 seconds after the completion of the Job. We need to enable the TTL mechanism for this.
apiVersion: batch/v1 kind: Job metadata: name: demo-job spec: ttlSecondsAfterFinished: 600 template: . . . . . .
How to stop a Job in progress ?
A Job does not provide any option to stop or pause it in between. You can only delete the Job, which will delete its Pods too. If you are not storing the logs in an external storage, you will loose your logs.
To handle failure conditions, it provides with two parameters – backoffLimit which set max retries & activeDeadlineSeconds which sets a deadline to complete.
The below Job tells –
- Stop the Job if the container or the Pod is restarted for 3 times.
- The default value of backoffLimit is 6
- Stop the Job if it exceeds 300 seconds.
- The Job controller will terminate all the active Pods and mark the Job as failed.
apiVersion: batch/v1 kind: Job metadata: name: demo-job spec: completions: 2 backoffLimit: 3 activeDeadlineSeconds: 300 template: metadata: name: worker-pod spec: containers: - name: worker-pod image: busybox args: - /bin/sh - -c - echo Started processing;'sleep 10;echo Task completed; restartPolicy: OnFailure
Execution Patterns – Sequential, Parallel & Mixed
A Kubernetes Job allows you to run a set of Pods in parallel or run them in a sequence or run in a mixed mode. All these patterns would be useful to solve different use cases as mentioned above.
Run Workers in Parallel
To run workers in parallel, use :
spec: parallelism: 3
If we set this value to our demo-job, we can see 3 Pods running in parallel, as shown:
$ kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/demo-job 0/1 of 3 11s 11s NAME READY STATUS RESTARTS AGE pod/demo-job-5ddwc 1/1 Running 0 11s pod/demo-job-7pd5g 1/1 Running 0 11s pod/demo-job-vq2dk 1/1 Running 0 11s
parallelism of N, starts N Pods in parallel. This internally sets the ‘completions’ to N.
Run Workers in Sequence
To run worker Pods in sequence, use :
spec: completions: 5
The ‘completions’ value fixes the max number of Pods in a Job. If we do not specify any value for ‘parallelism’, it retains the default value of 1, and all the Pods run in a sequence. As we can see below, the Job is creating the 3rd Pod after the 2nd one is complete.
$ kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/demo-job 2/5 29s 29s NAME READY STATUS RESTARTS AGE pod/demo-job-46d57 0/1 Completed 0 15s pod/demo-job-b9h96 0/1 ContainerCreating 0 2s pod/demo-job-wz52v 0/1 Completed 0 29s
Mix Parallel & Sequential
K8s allows us to mix the parallel and sequential execution together. In such a case, the ‘completions’ provides max number of Pods to run where as the ‘parallelism’ says how many to run in parallel. A sample Job definition is given below.
apiVersion: batch/v1 kind: Job metadata: name: demo-job spec: completions: 10 parallelism: 3 template: metadata: name: worker-pod spec: containers: - name: worker-pod image: busybox args: - /bin/sh - -c - echo Started processing;sleep 10;echo Task completed; restartPolicy: OnFailure
Conclusion
As we have seen, the Jobs supports the core features to deploy task specific applications that completes after the task is over.
The summary of the key points about a Job are:
- A Job can run its Pod instances in sequence or in parallel or in a mixed.
- The Pods has to stop successfully for a Job to succeed.
- As a result, it is not allowed to have a restartPolicy: Always.
- It can only use restartPolicy values – OnFailure or Never
- backoffLimit & activeDeadlineSeconds – key attribute to stop a failed Job.
- Jobs are not deleted by default to preserve the logs, we need to take measures to clean up.
- As a closely related module, K8s provides CronJob that comes with a scheduler.
- It can be used for automating the Jobs at desired intervals.
K8s – Deployment
A Deployment object is used to define the desired specification of your application instance including the scaling requirements.
The key in the deployment module lies in the way the Deployment Controller manages deployment objects. The deployment controller transitions the existing state of your application instances to the new desired state as per your latest deployment specifications, thus providing you an automated deployment process.
The transitioning is done in a controlled way using rolling over mechanism; thus it ensures minimal impact on the availability of the application during a deployment.
We will look at these with some examples in the sections below.
Key Features of Deployment
- Supports the deployment of scalable applications
- Manages the changes to the POD template
- Manages the changes to the scaling requirements
- Supports the auto scaling of the applications
- Manages Deployment History and enables you to Roll-back to historical versions
- Provides Rolling Update of the application instances
- Supports Pausing and Resuming of the rolling deployments
The following sections describes how K8s provides these features in more detail.
Deploying an Application
demo-deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: demo-deploy labels: app: demo-deploy spec: replicas: 4 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: k8s.gcr.io/echoserver:1.9 ports: - containerPort: 8080
This is our sample deployment named : demo-deploy
The key components of the deployment are :
- replicas – Defines the scaling
- template -Defines the desired specification of the pod instances
- selector – Defines the criteria to select and keep track of the instances deployed across the cluster nodes. Here, the label is ‘app: demo-app’
Lets deploy and explore the resultant components :
$ kubectl apply -f demo-deploy.yaml --record=true deployment.apps/demo-deploy created $ kubectl get deployment,rs,pod NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demo-deploy 4/4 4 4 42s NAME DESIRED CURRENT READY AGE replicaset.apps/demo-deploy-595d5466b5 4 4 4 42s NAME READY STATUS RESTARTS AGE pod/demo-deploy-595d5466b5-5v8kj 1/1 Running 0 42s pod/demo-deploy-595d5466b5-hhtrr 1/1 Running 0 42s pod/demo-deploy-595d5466b5-jf4ks 1/1 Running 0 42s pod/demo-deploy-595d5466b5-w6sgx 1/1 Running 0 42s
Observation
- The Deployment has created a Replicaset . Each deployment is managed by a separate replicaset.
- The Replicaset used the template to create 4 pods as per the scaling requirement
- Each replicaset shares the same hash code with its associated pods.
- And, the pods, also, have another hash code to avoid the naming conflict.
- Replicaset uses the selector criteria to select and monitor the pods
To look at the details of our deployment and it’s associated events we can use the following command. Let us look at the top part for now:
$ kubectl describe deployments | head -15 Name: demo-deploy Namespace: default CreationTimestamp: Wed, 08 Jul 2020 13:36:42 +0000 Labels: app=demo-deploy Selector: app=demo-app Replicas: 4 desired | 4 updated | 4 total | 4 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: Labels: app=demo-app
Understanding Rolling Update
Now let us do a failed deployment by setting an invalid image to the deployment:
$ kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.9.xy --record=true deployment.apps/demo-deploy image updated
The status of various components of the deployment is shown below. Now lets look at the ‘RollingUpdateStrategy‘ as highlighted under the pod description above and try to match the outcome below:
$ kubectl get deployment,rs,pod NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demo-deploy 3/4 2 3 6m58s NAME DESIRED CURRENT READY AGE replicaset.apps/demo-deploy-595d5466b5 3 3 3 6m58s replicaset.apps/demo-deploy-fd86759fb 2 2 0 4s NAME READY STATUS RESTARTS AGE pod/demo-deploy-595d5466b5-5v8kj 1/1 Terminating 0 6m58s pod/demo-deploy-595d5466b5-hhtrr 1/1 Running 0 6m58s pod/demo-deploy-595d5466b5-jf4ks 1/1 Running 0 6m58s pod/demo-deploy-595d5466b5-w6sgx 1/1 Running 0 6m58s pod/demo-deploy-fd86759fb-jjfr7 0/1 ImagePullBackOff 0 4s pod/demo-deploy-fd86759fb-nqgwf 0/1 ImagePullBackOff 0 4s
- Only one pod (25% max unavailable out of scale=4) of the existing replicaset is terminating.
- 2 pods (1 in place of terminating pod and 1 as part of 25% max surge) is coming up in the new replicaset.
- And, since this is a rolling update and the new pod are not going to come up to the running status due to image issue, no more of pods of the existing replicaset will be taken down and the deployment will be stuck at this stage.
- Advantage : The application will be still available with 75% of the pods because of the rolling update strategy.
Now fix the image version and do a redeployment as follows :
kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.10 --record=true
Running the following command shows the deployment is successful and the latest replicaset is having all the 4 pods ready.
$ kubectl rollout status deployment.v1.apps/demo-deploy Waiting for deployment "demo-deploy" rollout to finish: 1 old replicas are pending termination... Waiting for deployment "demo-deploy" rollout to finish: 3 of 4updated replicas are available... deployment "demo-deploy" successfully rolled out $ kubectl get rs NAME DESIRED CURRENT READY AGE demo-deploy-595d5466b5 0 0 0 11m demo-deploy-5f6f56cd49 4 4 4 118s demo-deploy-fd86759fb 0 0 0 5m2s
Exploring Deployment History and doing a Roll Back
Now, lets say, the new version has some serious bug and we want to rollback and we want to check the history of the deployment:
$ kubectl rollout history deployment.v1.apps/demo-deploy deployment.apps/demo-deploy REVISION CHANGE-CAUSE 1 kubectl apply --filename=pod.yaml --record=true 2 kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.9.xy --record=true 3 kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.10 --record=true
The change cause has been added due to –record=true flag used during the deployments.
A simple ‘rollout undo‘ will take the deployment back to a failed revision(revision=2) and to take it to an older version we can specify the revision as follows :
$ kubectl rollout undo deployment.v1.apps/demo-deploy --to-revision=1 deployment.apps/demo-deploy rolled back $ kubectl rollout history deployment.v1.apps/demo-deploy deployment.apps/demo-deploy REVISION CHANGE-CAUSE 2 kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.9.xy --record=true 3 kubectl set image deployment.v1.apps/demo-deploy demo-app=k8s.gcr.io/echoserver:1.10 --record=true 4 kubectl apply --filename=pod.yaml --record=true
Now the history shows that the roll back has used the revision 1 and renamed it to revision 4 after the rollback. Moreover, as we can see, it has reused the saved ‘pod-template-hash‘, the identity (595d5466b5) given by that deployment revision.
$ kubectl rollout history deployment.v1.apps/demo-deploy --revision=4 deployment.apps/demo-deploy with revision #4 Pod Template: Labels: app=demo-app pod-template-hash=595d5466b5 Annotations: kubernetes.io/change-cause: kubectl apply --filename=pod.yaml --record=true Containers: demo-app: Image: k8s.gcr.io/echoserver:1.9
Other Key Features – Pause, Restart, Scale , Autoscale
As we have seen so far, the deployment manages the desired state changes and keeps track of the each state history to support the requirement to roll back.
The above examples showed how we can change the image version. For major changes we can also deploy the updated version of deployment file where the name of the deployment remains the same:
# deploying an updated version of the app $ kubectl apply -f demo-app-v-1.1.yaml deployment.apps/demo-deploy configured
For scaling, autoscaling, pausing, resuming , updating comments to a deployment history, here are some useful commands:
# To pause a deployment in the middle kubectl rollout pause deployment.v1.apps/demo-deploy # To resume a paused deployment kubectl rollout resume deployment.v1.apps/demo-deploy # To scale a deplyment kubectl scale deployment.v1.apps/demo-deploy --replicas=8 # Command to autoscale a deployment as the cpu usage increases beyond 90% kubectl autoscale deployment.v1.apps/demo-deploy --min=10 --max=20 --cpu-percent=90 # To add a comment to the latest deployment history change-cause manually kubectl annotate deployment.v1.apps/demo-deploy kubernetes.io/change-cause="successful roll-over to image-1.10"
Conclusion
The deployment is used to manage the desired state changes of the deployed applications. And, the applications used in deployment are long running stateless applications.
The default restart policy of a pod in a deployment is – Always.
The other key deployment supported by Kubenetes are :
- Job & CronJob : Meant for managing jobs which are meant to run only till completion.
- ReplicaSets : Meant for managing long running stateful applications.
- Deamonset : Meant for managing applications which are required to present on all the desired nodes. These are not required to support scaling requirements as in the other deployments.
K8s – Secrets
K8s Secrets allow you to store and manage your confidential data such as passwords , ssh keys , authentication tokens separately in a cluster level object that the PODs can fetch for their usage at the time of deployment.
- Secret being an cluster level API object, provides a safer place and the ability to apply better access control
- Better compared to hard coding such data in Pod definition or in a container image
- Allows you to manage your confidential from a central location.
- We can apply policies to restrict user access to view the secrets
- We can apply data encryption on the secret data stored on ETCD.
- It works very similar to ConfigMap and you can access the data as env variables or volumes.
- It stores data in in-memory, tmpfs volumes at Node level.
- Thus if a node crashes , neither the node nor the associated containers hold on to the confidential data.
Creating Secrets
Using ‘create secret ‘ command
Similar to ConfigMaps we can use ‘create secret’ command to create the key value pairs using :
- —from-literal : Literal key becomes key & literal value becomes value
- —from-file : File name (or the file-key, if specified) becomes the key and the content becomes the value
- —from-env-file : Each entry in side the file creates a literal value.
The following is an example secret being created using two literals(run this on a K8s cluster, say a minikube) :
# my-db-secrete using --from-literal kubectl create secret generic my-db-secret \ --from-literal=username='admin' \ --from-literal=password='pa$sw@rd1)%'
The resultant secrete can be retrieved using :
kubectl get secret my-db-secret -o yaml
The secrete in the output would look as below :
apiVersion: v1 data: password: cGEkc3dAcmQxKSU= username: YWRtaW4= kind: Secret metadata: creationTimestamp: "2020-06-30T13:32:40Z" name: my-db-secret namespace: default resourceVersion: "1016" selfLink: /api/v1/namespaces/default/secrets/my-db-secret uid: b17cf73b-dbeb-4a7d-9b6b-14ac8649d4ae type: Opaque
As we can see the values are shown encrypted with Base64 encryption. This is the default encryption and you can change the encryption using K8s EncryptionConfiguration .
Using the secret inside a POD
Again the usage is also very similar to ConfigMaps.
Import as Env variables
Let us use the following POD to import the ‘my-db-secret’ created above into the env variables :
secret-test-pod.yaml
apiVersion: v1 kind: Pod metadata: name: secret-test-pod spec: containers: - name: test-container image: k8s.gcr.io/echoserver:1.4 envFrom: - secretRef: name: my-db-secret
Follow the steps for testing :
# Step - 1 : Create the secret my-db-secret kubectl create secret generic my-db-secret \ --from-literal=username='admin' \ --from-literal=password='pa$sw@rd1)%' #Step 2: Create the pod and check the status kubectl apply -f secret-test-pod.yaml kubectl get pod,secret -o wide #Step 3: When the pod is running , # log in to its container shell and check the env variables kubectl exec -it secret-test-pod -- /bin/bash printenv
In the output, you should find the following two environment variables:
password=pa$sw@rd1)% username=admin
Summary of importing Secret as Env variables
- Each literal and each file entry in a secret, is imported as a separate environment variable
- For literals, the value of the variable is the literal’s value
- For the files, the value of the variable is the content of the file.
The above example is for importing all the keys in a secret. The comparison of the syntax to read few particular keys is as shown below :
All entries in a Secret
envFrom: - secretRef: name: my-db-secret
Specific entries in a secret
env: - name: USERNAME valueFrom: secretKeyRef: name: my-db-secret key: username - name: PASSWORD valueFrom: secretKeyRef: name: my-db-secret key: password
Import into volume
secret-vol-pod.yaml
apiVersion: v1 kind: Pod metadata: name: secret-vol-pod spec: containers: - name: test-container image: k8s.gcr.io/echoserver:1.4 volumeMounts: - name: secret-volume readOnly: true mountPath: "/secret-volume" volumes: - name: secret-volume secret: secretName: my-db-secret
The syntax for importing secrets has been highlighted in the above pod definition.
Let us follow the below steps to try this out with an example:
# Step - 1 : Create the sample secret 'my-db-secret' kubectl create secret generic my-db-secret \ --from-literal=username='admin' \ --from-literal=password='pa$sw@rd1)%' #Step 2: Create the pod and check the status kubectl apply -f secret-test-pod.yaml kubectl get pod,secret -o wide #Step 3: When the pod is running , # log in to its container shell and move to the mount path kubectl exec -it secret-test-pod -- /bin/bash cd secret-volume
The contents of the mount path would be as follows:
#The mount path has two link file for two literal entries root@secret-vol-pod:/secret-volume# ls -l total 0 lrwxrwxrwx 1 root root 15 Jun 30 15:48 password -> ..data/password lrwxrwxrwx 1 root root 15 Jun 30 15:48 username -> ..data/username #Content of username - its literal value in secret root@secret-vol-pod:/secret-volume# cat username admin #Content of password- its literal value in secret root@secret-vol-pod:/secret-volume# cat password pa$sw@rd1)%
Summary of importing a Secret using a volume
- Each data key in the secret will be listed as a linked file under the mount path.
- For literals – the content of the linked file would be the literal’s value
- For files – the content of the linked file would be the content of the file in the secret
The above example imports all the keys in a secret into a volume. Comparison of the syntax to import only few specific keys is as follows:
For all keys in a secret
volumes: - name: secret-volume secret: secretName: my-db-secret
For specific keys in a secret
volumes: - name: secret-volume secret: secretName: my-db-secret items: - key: username path: USERNAME - key: password path: PASSWORD
K8s – Services
PODs are instantiated across various nodes by the scheduler and each POD gets it’s unique virtual IP. Moreover, these instances need to be scaled up or down as per our load requirements and, also, need to be replaced in case of failures.So, the set of the backing POD ips for an application may keep changing.
The service API objects in K8s enable us to:
- keep track of the dynamic sets of backing PODs for an application
- provide service urls for load balanced application access
- expose the application to internal and\or external clients
Besides this it also provides ability to customize the default mechanism to address custom needs.
K8s Service – There are 4 basic types of service
K8s supports 4 basic types of service :
- ClusterIP : Only for internal access
- NodePort : It builds on top of the clusterIP service and provides exposure to external clients
- LoadBalancer : A load balancer in front of NodePort service
- ExternalName: A service for access external endpoints
As all the other services are like some variation of the cluster IP service, we will explore this in detail and then move on to see how other one vary from this.
1. ClusterIP : Used only for internal access.
The service API object is shown on the left side the diagram and when this gets deployed the following things happen internally.
- K8S assigns a cluster IP to the service
- The service create an Endpoints object based on spec.selector and keeps track of the backing POD IPs.
- kube-proxy creates the routing rules from cluster ip (@port) to endpoint ips(@ target ports) for load balancing purpose.
- Service Discovery: With the help of DNS service,if available, a dns entry for network access is also created as shown. This helps the clients not to worry about the clusterIP that gets assigned dynamically to the service.
Different options for customization:
- Custom Endpoints : Say, some clustered MySql instances are running outside the K8s cluster. And, you want to access this DB cluster from applications running inside K8s.
- You have the option to create a cluster service with no spec.selector
- Create an Endpoints object with the same name as the cluster service.
- Specify the targeted DB IPs in the Endpoints object.
- ClusterIP: None – This creates a HeadlessService
- No cluster IP is assigned and, hence, no routing rules in kube-proxy.
- The service adds separate DNS entries for each of the backing PODs, instead of the regular single DNSentry for the service.
- Specify a ClusterIP: You can hardcode a specific ip from the allowed range of cluster ips. The service will use this as the cluster IP as long as it is not taken up by another service.
Kube-proxy manages the load balancing (L4 type) as part of the ClusterIP service. In case you need to go for advanced L4 load balancing, you have the option of going with Headless Service and implement your own load balancing using the endpoint addresses available in the associated EndPoint object.
2. NodePort : The easiest option to provide external access.
- K8s creates the regular ClusterIP service as discussed earlier.
- K8s assigns a NodePort and creates proxy routing rules for routing requests from NodePorts on K8s Nodes to the ClusterIP service.
- This enables traffics coming at the assigned NodePort on any K8s node to get routed to one of the endpoint PODs.
Although its quite an easy approach to expose your service to the external clients it comes with some serious drawbacks :
- You need to open firewall access on the NodePort for a set of your K8s Nodes. Which is a serious security concern.
- The clients need to be intimated about any changes in the node IPs as well as the assigned NodePort. Which leads to maintenance issues.
A LoadBalancer service helps in solving these two issues by adding a LoadBalancer at the top. Let us see, how?
3. LoadBalancer: Allows you to add a load balancer on top of a NodePort service.
- The load balancer which connects to the NodePort service internally and provides a single point of access to the external clients.
- From security point of view, you need not have to expose your kubernetes nodes directly to your client.
- Keeping track of the available Nodes and any changes to the service NodePort is now the responsibility of the load balancer.
But, the load balancer solution has its own drawbacks:
- Adding an external load-balancer, adds to your cost.
- Moreover, if you have to expose multiple services , you can not expose them all on the same port(80) on a single load-balancer. So, you have to go for multiple load-balancers which will increase your cost many folds.
Luckily to overcome this cost issue of using multiple load balancers, K8s provides a nice solution called Ingress. We will look into this solution in a separate article.
4. External Name: A K8S service for external endpoints.
apiVersion: v1 kind: Service metadata: name: my-db-service namespace: test spec: type: ExternalName externalName: my.test.database.com
- No Cluster ID is assigned, no Endpoints is used and no proxying is setup by K8s.
- DNS name against the externalName contains the list of endpoints
- The redirection happens at the DNS level
When a K8s application tries to access my-db-service, it gets modified with the contents of my.test.database.com
This is useful in pointing to set of external applications or database cluster (not managed by K8s).
The Journey Begins
Thanks for joining me!
Good company in a journey makes the way seem shorter. — Izaak Walton