Goglides Dev 🌱

Cover image for Getting Started With Kubernetes | Explain Core Concepts
Roshan Thapa
Roshan Thapa

Posted on

Getting Started With Kubernetes | Explain Core Concepts

Section 1

Introduction

Kubernetes is an open source container orchestration platform, allowing large numbers of containers to work together in harmony, reducing operational burden. It helps with things like running containers across many different machines, scaling up or down by adding or removing containers when demand changes, keeping storage consistent with multiple instances of an application, distributing load between the containers, launching new containers on different machines if something fails.

Google open-sourced the Kubernetes project in 2014. Kubernetes builds upon a decade and a half of experience that Google has with running production workloads at scale, combined with best-of-breed ideas and practices from the community. It's a portable, extensible open-source platform for managing containerized workloads and services, that facilitates both declarative configuration and automation. It has a large, rapidly growing ecosystem. Kubernetes services, support, and tools are widely available.

Kubernetes is a tool you can use if you do not want to develop something specific in order to handle all the aforementioned issues. It can help you pilot your container cluster, hence its name, which means pilot or helmsman in greek.

The Kubernetes Control Plane consists of a collection of processes running on your cluster:

The Kubernetes Master is a collection of three processes that run on a single node in your cluster, which is designated as the master node. Those processes are kube-apiserver, kube-controller-manager and kube-scheduler.

Containers are a great way to package, deploy, and manage applications. However, to build a reliable, scalable containerized application, you need a place to run containers, scale them, update them, and provide them with networking and storage. Kubernetes is the most popular container orchestration system. It simplifies deploying, monitoring, and scaling application components, making it easy to develop flexible, reliable applications. 

Section 2

What Is Kubernetes?

Kubernetes, also known by its abbreviation “k8s”, is an open-source container orchestration system. It manages a “cluster” of multiple hosts that are used to run containers. Originally created by Google, in March of 2016, it was donated to the Cloud Native Computing Foundation (CNCF).

Kubernetes is declarative. This means that you can create and update “resources” that describe which containers to run, how to configure them, and how to route network traffic to them. Kubernetes continuously updates and monitors the cluster to ensure it matches the desired state, including auto-restart, re-scheduling, and replication to ensure applications start and remain running. 

Kubernetes is available as a service from cloud providers including Google, Amazon, and Microsoft, or in a variety of distributions including Red Hat OpenShift, Rancher Kubernetes, and VMWare Tanzu.  

Section 3

Core Kubernetes Concepts

Because Kubernetes is declarative, getting started in Kubernetes mostly means understanding what resources we can create and how they are used to deploy and configure containers in the cluster. To define resources, we use YAML format. The available resources and fields for each resource may change with new Kubernetes versions, so it’s important to double-check the API reference for your version to know what’s available. It’s also important to use the correct apiVersion on each resource to match your version of Kubernetes. This Refcard uses the API from Kubernetes 1.24, released May 2022, but all resources shown are backwards compatible through Kubernetes 1.19. Lastly, the examples featured in this Refcard are available in the associated GitHub repository.

Pod

A Pod is a group of one or more containers. Kubernetes will schedule all containers for a Pod into the same host, with the same network namespace, so they all have the same IP address and can access each other using localhost. The containers in a Pod can also share storage volumes. 

We don’t typically create Pod resources directly. Instead, we have Kubernetes manage them through a controller such as a Deployment or StatefulSet. Controllers provide fault tolerance, scalability, and rolling updates. 

Deployment

A Deployment manages one or more identical Pod instances. Kubernetes will make sure that the specified number of Pods is running, and on a rolling update, it will replace Pod instances gradually, allowing for application updates with zero downtime. Here is an example Deployment:

YAML

apiVersion: apps/v1  
kind: Deployment  
metadata:  
  name: nginx  
spec:  
  replicas: 3  
  selector:  
    matchLabels:  
      app: nginx  
  template:  
    metadata:  
      labels:  
        app: nginx  
    spec:  
      containers:  
      - name: nginx  
        image: nginx:1.23.1-alpine  
        volumeMounts:  
        - mountPath: /usr/share/nginx  
          name: www-data  
          readOnly: true  
        resources:  
          requests: 
            memory: "128Mi"  
            cpu: "50m"  
          limits: 
            memory: "128Mi"  
            cpu: "50m"  
      initContainers:  
      - name: git-clone  
        image: alpine/git  
        args:  
        - "clone" 
        - "https://github.com/book-of-kubernetes/hello-world-static" 
        - "/www/html"  
        volumeMounts:  
        - mountPath: /www  
          name: www-data  
      volumes:  
      - name: www-data  
        emptyDir: {}  
Enter fullscreen mode Exit fullscreen mode

The apiVersion and kind uniquely identify the type of the resource. Deployments are in the apps API group, and here, we are specifying v1 as the version. As a result, Kubernetes knows what fields to expect in the rest of the YAML specification.

For a controller such as a Deployment, the template section specifies exactly what the created Pods should look like. Kubernetes will automatically create three Pods per the replicas field using this template. Kubernetes will monitor these Pods and automatically restart them if the container terminates. Deployments use the matchLabels selector field to determine which Pods to manage. This field must always have the same data as the metadata.labels field inside the template. The Deployment will take ownership of any running Pods that match the matchLabels selector, even if they were created separately, so keep these names unique.

The example above defines one container in the Pod and additionally defines an initContainer. The initContainer runs before the main Pod container starts. In this case, it uses Git to populate a directory. Because this directory is part of a single volume that is specified as a volumeMount in both containers, the resulting files are also mounted into the main container to be served by NGINX. This example uses an initContainer to run Git because Git runs and then exits; if Git was run as a second regular container in the Pod, Kubernetes would interpret this as a container failure and restart the Pod.

Finally, we specify requests and limits for our main container. Kubernetes uses these to ensure that each node in the cluster has enough capacity for its deployed Pods. Requests and limits are also used with quotas so that different applications and users in a multi-tenant cluster don’t interfere with each other. It is good practice to identify the resource needs of each container in a Pod and apply limits.

StatefulSet

Each new Pod in the above Deployment starts with an empty directory, but what about cases where we need real persistent storage? The StatefulSet also manages multiple Pods, but each Pod is associated with its own unique storage, and that storage is kept when the Pod is replaced.

Here is an example StatefulSet:

YAML

apiVersion: apps/v1 
kind: StatefulSet 
metadata: 
  name: postgres 
spec: 
  serviceName: postgres 
  replicas: 2 
  selector: 
    matchLabels: 
      app: postgres 
  template: 
    metadata: 
      labels: 
        app: postgres 
    spec: 
      containers: 
      - name: postgres 
        image: postgres 
        env: 
        - name: POSTGRES_PASSWORD 
          value: "supersecret" 
        - name: PGDATA 
          value: /data/pgdata 
        volumeMounts: 
        - name: postgres-volume 
          mountPath: /data 
  volumeClaimTemplates: 
  - metadata: 
      name: postgres-volume 
    spec: 
      accessModes: 
      - ReadWriteOnce 
      resources: 
        requests: 
          storage: 1Gi 
Enter fullscreen mode Exit fullscreen mode

Like a Deployment, a StatefulSet is in the apps API group, uses a selector to discover its Pods, and uses a template to create those Pods. However, a StatefulSet also has a volumeClaimTemplates section that specifies the persistent storage provided to each Pod.

When this StatefulSet is created, it will create two Pods: postgres-0 and postgres-1. Each Pod will have associated persistent storage. If a Pod is replaced, the new Pod will have the same name and be attached to the same storage. Each of these Pods will be discoverable within the cluster using the combination of the Pod name and the serviceName.
The env field is one way to provide environment variables to containers. The PGDATA variable tells PostgreSQL where to store its database files, so we ensure those are placed on the persistent volume. We also specify the POSTGRES_PASSWORD directly; in a production application, we would use a Secret as described below.

The StatefulSet is one important resource needed to deploy a highly available PostgreSQL database server. We also need a way for the instances to find each other and a way to configure clients to find the current primary instance. In the application example below, we use a Kubernetes Operator to accomplish this automatically.

Service

A Service provides network load balancing to a group of Pods. Every time Kubernetes creates a Pod, it assigns it a unique IP address. When a Pod is replaced, the new Pod receives a new IP. By declaring a Service, we can provide a single point of entry for all the Pods in a Deployment. This single point of entry (hostname and IP address) remains valid as Pods come and go. The Kubernetes cluster even provides a DNS server so that we can use Service names as regular hostnames. Here is the Service that matches our NGINX Deployment above:

YAML

kind: Service  
apiVersion: v1  
metadata:  
  name: nginx  
spec:  
  selector:  
    app: nginx  
  ports:  
  - protocol: TCP  
    port: 80 
Enter fullscreen mode Exit fullscreen mode

Unlike the Deployment and StatefulSet, the Service is in the “core” API group, so we only need to specify the apiVersion as v1. Like the Deployment and StatefulSet we saw above, the Service uses the selector field to discover its Pods, so it automatically stays up to date. For this reason, Services can be created before the Pods exist; this demonstrates an important advantage of the declarative Kubernetes approach.

Services rely on Kubernetes to provide a unique IP address and route traffic, so the way Services are configured can be different depending on how your Kubernetes installation is configured. The most common type of Service is ClusterIP, which is also the default. ClusterIP means the Service has an IP address accessible only from within the Kubernetes cluster, so exposing the Service outside the cluster requires another resource such as an Ingress.

It’s important to know that when network traffic is sent to a Service address and port, Kubernetes uses port forwarding to route traffic to a specific Pod. Only the declared ports are forwarded, so other kinds of traffic (like ICMP ping) will not work to communicate with a Service address, even within the cluster.

Ingress

An Ingress is one approach for routing HTTP traffic from outside the cluster. (An alternate and more advanced approach is a service mesh such as Istio.) To use an Ingress, a cluster administrator first deploys an “ingress controller”. This is a regular Kubernetes Deployment, but it registers with the Kubernetes cluster to be notified when an Ingress resource is created, updated, or deleted. It then configures itself to route HTTP traffic based on the Ingress resources.

The advantage of this approach is that only the ingress controller needs an IP address that is reachable from outside the cluster, simplifying configuration and potentially saving money.

Here is the Ingress example to go with the NGINX Deployment and Service: 

apiVersion: networking.k8s.io/v1  
kind: Ingress  
metadata:  
  name: nginx  
spec:  
  rules:  
  - http:  
      paths:  
      - path: / 
        PathType: Prefix  
        backend:  
          service: 
            name: nginx  
            port:  
              number: 80  
Enter fullscreen mode Exit fullscreen mode

This example routes all traffic in the cluster to a single Service, so it is only useful for a sandbox. In a production cluster, you can use DNS wildcards to route all hostnames in a domain to the ingress controller’s IP address, and then use host rules to route each host’s traffic to the correct application.

PersistentVolumeClaim

The StatefulSet example above tells Kubernetes to create and manage persistent storage for us. We can also create persistent storage directly using a PersistentVolumeClaim.

A PersistentVolumeClaim requests Kubernetes to dynamically allocate storage from a StorageClass. The StorageClass is typically created by the administrator of the Kubernetes cluster and must already exist. Once the PersistentVolumeClaim is created, it can be attached to a Pod by declaring it in the volumes field. Kubernetes will keep the storage while the PersistentVolumeClaim exists, even if the attached Pod is deleted.

YAML

kind: PersistentVolumeClaim  
apiVersion: v1  
metadata:  
  name: web-static-files  
spec:  
  accessModes:  
  - ReadWriteMany  
  resources:  
    requests:  
      storage: 8Gi  
Enter fullscreen mode Exit fullscreen mode

ConfigMap and Secret

In the StatefulSet example above, we specified environment variables directly in the env field. A better approach is to externalize the configuration into a separate ConfigMap or Secret. Both these resources work similarly and can be used to provide either environment variables or file content to containers. The major difference is that a Secret is base-64 encoded to simplify storage of binary content; additionally, as these are separate resources it is possible to configure cluster authorization separately so access to Secrets can be more limited.

To externalize our PostgreSQL environment variables, we’ll use both a ConfigMap and a Secret. First, the ConfigMap:

YAML

--- 
kind: ConfigMap 
apiVersion: v1 
metadata: 
  name: pgsql 
data: 
  PGDATA: /data/pgdata 
Enter fullscreen mode Exit fullscreen mode

Then, the Secret:

  --- 
kind: Secret 
apiVersion: v1 
metadata: 
  name: pgsql 
stringData: 
  POSTGRES_PASSWORD: supersecret 
Enter fullscreen mode Exit fullscreen mode

Besides the kind, the one difference is that we use stringData with the Secret to tell Kubernetes to do the base-64 encoding for us.

To use these externalized environment variables, we replace the env field of the StatefulSet with the following:

YAML

envFrom: 
- configMapRef: 
    name: pgsql 
- secretRef: 
    name: pgsql 
Enter fullscreen mode Exit fullscreen mode

Each of the key-value pairs in the ConfigMap and Secret will be turned into environment variables for the PostgreSQL server running in the container.

Kustomize, Helm, and Operators

Managing all of these different types of resources becomes challenging, even with the ability to externalize configuration using ConfigMap and Secret. Most application components require multiple resources for deployment and often need different configuration for different environments, such as development and production. The Kubernetes ecosystem has a rich set of tools to simplify managing resources:

  • Kustomize is built into Kubernetes itself. It supports applying patches to a set of base YAML resource files. The base file can contain default values, while the patches tailor the deployment to a different environment.
  • Helm is a separate tool that brings templates to YAML resource files. A set of templated resources, known as a Helm chart, can be uploaded to a registry and easily used in many different clusters, with the ability to easily tailor each individual deployment by supplying a set of variables in a YAML file.
  • Operators are a Kubernetes design pattern rather than a single tool. An operator runs in the Kubernetes cluster and registers a CustomResourceDefinition (CRD). It watches the cluster for instances of that custom resource and updates the cluster accordingly. The example application below demonstrates the use of the “Postgres Operator” to rapidly deploy a highly available PostgreSQL database server.

Section 4

Kubernetes Architecture

Kubernetes uses a client-server architecture, as seen here: 

Figure 1: Kubernetes Architecture

Image description

A Kubernetes cluster is a set of physical or virtual machines and other infrastructure resources that are used to run applications. The control plane includes all the services that manage the cluster. The machines that run the containers are called nodes. The control plane services are typically run as containers on the nodes, with multiple instances running in the cluster to provide high availability; this provides an easy way to keep the control plane services up and running and simplifies updates.

Control Plane

The most important control plane service is kube-apiserver, the Kubernetes API server. It accepts connections from clients and provides an API to query and modify the resources running in the cluster. The API server is assisted by:

  • etcd – A distributed key-value store used to record cluster state
  • kube-controller-manager – A monitoring service that decides what changes to make when resources are added, changed, or removed
  • kube-scheduler – A service that decides where to run pods based on the available nodes and their configuration

Node

A node is a physical or virtual machine with the necessary services to run containers. A Kubernetes cluster should have as many nodes as necessary for all the required pods. Each node runs kubelet, which receives commands to run containers and uses the container engine (e.g., containerd or CRI-O) to run them, and kube-proxy, which manages networking rules so connections to service IP addresses are correctly routed to pods. 

As shown in figure 1, each node can run multiple Pods, and each Pod can include one or more containers. All containers in a Pod are in the same network namespace, so those containers share an IP address. 

Section 5

Getting Started With Kubernetes

Now that we have learned about some key Kubernetes resources and we understand the Kubernetes architecture, we’re ready to put our knowledge into practice. We’ll need a development cluster to use for practice. We’ll control this cluster using the command-line utility kubectl.

Creating a Development Cluster

Running a production Kubernetes cluster is a complex job. Unless you’re deeply familiar with Kubernetes configuration, it’s best to use one of the many cloud options or distributions above. Kubernetes can run anything that can be packaged in a container, so insecure public clusters are quickly exploited for Bitcoin mining or other nefarious purposes. 

For a development environment, there are many great options, including Minikube, Microk8s, and Rancher k3s. You can install any of these on an existing machine by following their installation instructions, or even easier, you can use automation from the associated GitHub repository to create a sandbox virtual machine with k3s. The README file in the repository has all the instructions you need.

Once you’ve set up your cluster, you will interact with it using the standard Kubernetes command-line client program kubectl. You can download kubectl from its website; it’s a single binary so there’s no installation needed.

Kubectl

kubectl is a command-line utility that controls the Kubernetes cluster. Commands use this format:

kubectl [command] [type] [name] [flags] 
Enter fullscreen mode Exit fullscreen mode

The parts of the command are as follows:

  • [command] specifies the operation that needs to be performed on the resource. For example, create, get, describe, delete, or scale. 
  • [type] specifies the Kubernetes resource type, such as pod (po), service (svc), deployment (deploy), statefulset (sts), ingress (ing), or persistentvolumeclaim (pvc). Resource types are case-insensitive, and you can specify the singular, plural, or abbreviated forms.
  • [name] specifies the name of the resource, if applicable. Names are case-sensitive. If the name is omitted, details for all resources will be displayed (for example, kubectl get pods will list all pods).
  • [flags] specifies options for the command.

Some examples of kubectl commands and their purpose:

Command/Purpose

kubectl apply -f nginx.yaml 

Create or update the resources specified in the YAML file. Kubernetes records the state of the resource when it was last applied so that it can figure out what changes to make. 

kubectl delete -f nginx.yml 

Delete the resources specified in the YAML file. If any resources do not exist, they are ignored. 

kubectl get pods 

List all Pods in the default namespace. See below for more information on namespaces.

kubectl describe pod nginx 

Show metadata and status for the nginx Pod. The name must match exactly. 

First, let’s use kubectl version to verify that the cluster is running as expected and we can talk to it:

YAML

kubectl version 
[...] 
Client Version: version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.3+k3s1",[...]  
Kustomize Version: v4.5.4 
Server Version: version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.3+k3s1",[...] 
Enter fullscreen mode Exit fullscreen mode

This will report the exact version in use, which is valuable in making sure you are using the correct API documentation. 

Section 6

Run Your First Container

Most of the time when using kubectl, we create YAML resource files, so we can maintain configuration in version-controlled repositories. However, we can create a simple Deployment using kubectl without using a YAML file:

YAML

kubectl create deployment nginx --image=nginx 
deployment.apps/nginx created 
Enter fullscreen mode Exit fullscreen mode

This command will create a Deployment, which will then create a Pod with one container running NGINX. We can use kubectl to get the status of the Deployment:

YAML

kubectl get deploy 

NAME    READY   UP-TO-DATE   AVAILABLE   AGE 

nginx   1/1     1            1           10s 
Enter fullscreen mode Exit fullscreen mode

To see the Pod that the Deployment created, run:

YAML

kubectl get po 

NAME                  READY STATUS  ... AGE 

nginx-8f458dc5b-nd98w 1/1   Running ... 15s 
Enter fullscreen mode Exit fullscreen mode

The reason the Pod has two sets of random characters in the name is that the Deployment has created a ReplicaSet to perform the actual Pod management, and the ReplicaSet created the Pod.

YAML

kubectl delete deploy nginx 

kubectl apply -f ‘nginx-*.yaml’ 

deployment.apps/nginx created 

ingress.networking.k8s.io/nginx created 

service/nginx created 
Enter fullscreen mode Exit fullscreen mode

We now have three replicas because that is what was specified in the YAML file:

YAML

kubectl get deploy 

NAME    READY   UP-TO-DATE   AVAILABLE   AGE 

nginx   3/3     3            3           89s 
Enter fullscreen mode Exit fullscreen mode

Also, if you’re running in a cluster with an ingress controller, such as the k3s sandbox created by the associated GitHub repository, traffic to the ingress controller will now be routed to one of our NGINX Pods:

HTML

curl http://localhost 

[...] 

 <h1>Hello, world!</h1> 

[...] 
Enter fullscreen mode Exit fullscreen mode

Scale Applications

Deployments can be scaled up and down from the command line as well as in the YAML file: 

YAML

kubectl scale --replicas=5 deploy/nginx 

deployment.extensions/nginx scaled 
Enter fullscreen mode Exit fullscreen mode

The Kubernetes controller will then work with the scheduler to create or delete Pods as needed to achieve the requested number. This is reflected in the Deployment: 

YAML

kubectl get deploy 

NAME    READY   UP-TO-DATE   AVAILABLE   AGE 

nginx   5/5     5            5           4m46s 
Enter fullscreen mode Exit fullscreen mode

Kubernetes also provides a HorizontalPodAutoscaler (hpa) that can dynamically scale a Deployment up or down based on current resource utilization.

Delete Applications

Once you are done exploring the NGINX example, you can destroy it with the delete command.

YAML

kubectl delete -f ‘nginx-*.yaml’ 

deployment.extensions "nginx" deleted 

ingress.networking.k8s.io "nginx" deleted 

service "nginx" deleted 
Enter fullscreen mode Exit fullscreen mode

Because Kubernetes monitors Pods to maintain the desired number of replicas, we must delete the Deployment to remove the application. Simply deleting a Pod managed by a Deployment will just cause Kubernetes to create another Pod. 

Section 7

Example Application

Let’s put multiple Kubernetes features together to deploy an example to-do application, written in Node.js, together with a highly available PostgreSQL database server. Here is the planned architecture: 

Figure 2: To-do Application Architecture

Image description
As with our NGINX example, we use an Ingress to bring in HTTP traffic from outside the cluster and forward it to a Service. The Service provides a well-known name and consistent IP address for our application. The Service keeps track of our application’s Pods and routes traffic to a Pod IP address.

The application stores the list of to-do items in a PostgreSQL database. The PostgreSQL database also has a Service to provide a well-known name and IP address for the primary database server instance. This name and IP address stays the same even if the primary instance goes down and is replaced by a secondary instance (shown with dashed lines in the figure).

Let’s start by deploying PostgreSQL. We’ll use the Postgres Operator to simplify deploying and configuring a highly available database. First, we deploy the operator itself to the cluster:

YAML

kubectl apply -k github.com/rthway/postgres-operator/manifests 

serviceaccount/postgres-operator created 

clusterrole...io/postgres-operator created 

clusterrole...io/postgres-pod created 

clusterrolebinding...io/postgres-operator created 

configmap/postgres-operator created 

service/postgres-operator created 

deployment.apps/postgres-operator created 
Enter fullscreen mode Exit fullscreen mode

The command kubectl apply -k uses Kustomize to read the file manifests/kustomization.yaml from the Postgres Operator repository. The result is that the operator is now installed into our cluster and the Deployment is listening for a brand-new Kubernetes resource, Postgresql.

This means we can deploy a highly available PostgreSQL database instance to our cluster by creating a PostgreSQL resource.

apiVersion: "acid.zalan.do/v1" 
kind: postgresql 
metadata: 
  name: todo-db 
spec: 
  teamId: todo 
  volume: 
    size: 1Gi 
  numberOfInstances: 3 
  env: 
  - name: ALLOW_NOSSL 
    value: "1" 
  users: 
    todo: 
    - superuser 
    - createdb 
  databases: 
    todo: todo 
  postgresql: 
    version: "14" 
Enter fullscreen mode Exit fullscreen mode

This shows the power of the Kubernetes operator design pattern. The Postgres Operator is able to register a new Postgresql resource type with the Kubernetes API server as a CustomResourceDefinition. As a result, when we apply the todo-db resource above to the cluster, the API server is able to recognize it and validate the YAML we provide.

The Postgres Operator also registers with the Kubernetes API server to watch for new Postgresql resources. When a new resource is detected, the Postgres Operator interacts with the API server to create the necessary Service, StatefulSet, Secret, and other required resources needed for a highly available PostgreSQL database.

The remaining components in our to-do example application are similar to what we’ve seen before. You can inspect the details for yourself in the associated GitHub repository. It’s particularly worth looking to see how the PostgreSQL database configuration and credentials are injected into the Node.js application using environment variables. The database username and password are automatically generated by the Postgres Operator, so they have to be injected from a Secret.

Let’s create all of the resources for our to-do application. Run this command from the todo directory:

YAML

kubectl apply -f '*.yaml' 
deployment.apps/todo created 
postgresql.acid.zalan.do/todo-db created 
ingress.networking.k8s.io/todo-ingress created 
service/todo created 
Enter fullscreen mode Exit fullscreen mode

It may take several minutes for all of the application components to be up and running. Once they are up and running, we can inspect them using kubectl get:

YAML

kubectl get deploy 
NAME              READY UP-TO-DATE AVAILABLE 
postgres-operator 1/1   1          1 
todo              3/3   3          3 
Enter fullscreen mode Exit fullscreen mode

This shows the Deployment for our to-do application and the Deployment for the Postgres Operator. The actual PostgreSQL database needs persistent storage, so the operator does not create a Deployment; instead, it creates a StatefulSet:

YAML

kubectl get sts 
NAME      READY   AGE 
todo-db   3/3     9m38s 
Enter fullscreen mode Exit fullscreen mode

Additionally, there are a number of Services created for our application:

kubectl get svc 
NAME              TYPE        ... PORT(S) 
kubernetes        ClusterIP   ... 443/TCP 
postgres-operator ClusterIP   ... 8080/TCP 
todo-db           ClusterIP   ... 5432/TCP 
todo-db-repl      ClusterIP   ... 5432/TCP 
todo-db-config    ClusterIP   ... <none> 
todo              ClusterIP   ... 5000/TCP 
Enter fullscreen mode Exit fullscreen mode

The todo service handles routing traffic to the application layer, while the todo-db service points to the current primary database instance. The Postgres operator uses the todo-db-repl and todo-db-config services to manage replication from the primary to the secondary instances.

As before, if you’re running in a cluster with an ingress controller, such as the k3s sandbox created traffic to the ingress controller will be routed to our application:

curl http://localhost/todo/ 
[...] 
<title>Todo-Backend client</title> 
[...] 
Enter fullscreen mode Exit fullscreen mode

If you’re running in the sandbox virtual machine from the associated GitHub repository, you should be able to see and use the to-do application by visiting http://localhost:48080/todo/ in your browser.

If you’re having issues, use kubectl get to inspect all the resources created and the associated pods to see if one is failing. You can use kubectl logs to see the output from any pod.

There are more Kubernetes resources for you to explore in this application, including the individual Pods, the PersistentVolumeClaims created by the StatefulSet, and the Secrets used to store database credentials so they can be injected into the Node.js application. Once you’ve finished exploring, you can remove the to-do application from your cluster:

kubectl delete -f '*.yaml' 
deployment.apps "todo" deleted 
postgresql.acid.zalan.do "todo-db" deleted 
ingress...io "todo-ingress" deleted 
service "todo" deleted 
Enter fullscreen mode Exit fullscreen mode

When we delete the PostgreSQL resource, the Postgres Operator automatically deletes the StatefulSet, Services, Secrets, and other resources it created to deploy the highly available PostgreSQL database.

Section 8

Namespace, Resource Quotas, and Limits

Kubernetes uses namespaces to avoid name collisions, control access, and set quotas. When we created resources above, we didn’t specify a namespace, so they all went into the default namespace. Other resources that are part of the cluster infrastructure are in the namespace kube-system. 

To see Pods in kube-system, we can run: 

$ kubectl get po -n kube-system  
NAME                       READY   STATUS 
...  
coredns-7944c66d8d-rmxnr   1/1     Running 
...  
Enter fullscreen mode Exit fullscreen mode

Resource Isolation

A new Namespace can be created from a YAML resource definition:

YAML

apiVersion: v1  
kind: Namespace  
metadata:  
  name: development  
  labels:  
    name: development  
Enter fullscreen mode Exit fullscreen mode

Once we’ve created the Namespace, we can create resources in it using the --namespace (-n) flag, or by specifying the namespace in the resource’s metadata: 

YAML

apiVersion: v1  
kind: Pod  
metadata:  
  name: webserver  
  namespace: development  
...  
Enter fullscreen mode Exit fullscreen mode

By using separate namespaces, we can have many Pods named webserver and not have to worry about name collisions. Also, Kubernetes DNS works with namespaces. Simple host names look for Services in the current namespace, but we can use the full name for Services in other namespaces. For example, we could find the PostgreSQL database we created for our to-do application by using todo-db.default.svc from anywhere in the cluster.

Access Control

Kubernetes supports Role Based Access Control (RBAC).  

Here’s an example that limits developers to read-only access for Pods in the production namespace. First, we create a ClusterRole, a common set of permissions we can apply to any namespace: 

YAML

kind: ClusterRole  
apiVersion: rbac.authorization.k8s.io/v1  
metadata:  
  name: pod-read-only  
rules: 
- apiGroups: [""] # "" is the core API group  
  resources: ["pods"]  
  verbs: ["get", "watch", "list"]
Enter fullscreen mode Exit fullscreen mode

Next, we use a RoleBinding to give members of the developers group permissions in the production namespace:

YAML

kind: RoleBinding  
apiVersion: rbac.authorization.k8s.io/v1  
metadata:  
  name: read-only  
  namespace: production  
subjects:  
- kind: Group  
  name: developers  
  apiGroup: rbac.authorization.k8s.io  
roleRef:  
  kind: ClusterRole  
  name: pod-read-only  
  apiGroup: rbac.authorization.k8s.io  
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can use a ClusterRoleBinding to apply a role to a user or group in all namespaces.

Resource Quotas

By default, Pods have unlimited resources, but they receive resources on a “best effort” basis. To control the number of Pods in a namespace or to limit the resources Pods in a namespace can use, we can apply a ResourceQuota to a namespace:

YAML

apiVersion: v1  
kind: ResourceQuota  
metadata:  
  name: compute-resources  
  namespace: sandbox  
spec:  
  hard:  
    cpu: "5"  
    memory: 10Gi  
Enter fullscreen mode Exit fullscreen mode

Kubernetes will now reject Pods in this namespace unless we apply a limit: 

YAML

apiVersion: v1  
kind: Pod  
metadata:  
  name: webserver  
  namespace: sandbox  
spec:  
  containers:  
  - image: nginx  
    name: nginx  
    resources:  
      limits:  
        memory: "128Mi"  
        cpu: "500m"  
Enter fullscreen mode Exit fullscreen mode

Note that we can request fractions of a CPU and use varying units for memory. Pods that specify limits receive a higher-priority quality of service class compared to “best effort” Pods, so it is good practice to specify limits wherever possible.

Section 9

Conclusion

Kubernetes is the most popular container orchestration framework. It is a powerful and reliable way to run containerized applications in production, providing reliability and scalability. This Refcard has shown a few of the most important resource types in Kubernetes to help you get started deploying applications, and with what you’ve learned here, you can start exploring all that Kubernetes has to offer.

Top comments (0)