Skip to main content

Command Palette

Search for a command to run...

Using Kubernetes to deploy a 3-tier containerized application infrastructure

Published
•10 min read•View as Markdown
S
I'm energetic, ambitious person who has developed a mature and responsible approach to any task that I undertake, or situation that I am presented with. I am excellent at working with others to achieve a certain objective on time and with excellence. Customer Engineer| Al/ ML |AI Infrastructure | Cloud Migration |Technical Solution| Vertex AI| Cloud Database |Cloud Networking |DevOps Engineer| Technical Blogger| Generative AI| Google Cloud Ready Facilitator 🌐Linux Linux Professional Institute Certificate Technical Writer \ Cloud Networking Cloud Computing \ Cloud Infrastructure Cloud Consultant \ Customer Engineer 🌐Virtualization - VMware, vSphere, vCenter Server 🌐Programming Skill Technical Skills Proficiency in languages like Java, Python, Scala, or JavaScript. System Administration: Experience with Linux/Unix systems, Windows Server. Networking: Understanding of network protocols, routing, VPC, Subnets, Firewalls, VPNs, Load Balancers, switching, and firewall configurations. Cloud Platforms: Experience with AWS, Azure, or Google Cloud Platform. Databases: Knowledge of SQL and NoSQL databases like MySQL, PostgreSQL, MongoDB. Scripting: Ability to write scripts for automation using Bash, PowerShell, or similar. Monitoring and Logging: Familiarity with tools like Nagios, Prometheus, Grafana, ELK Stack. Configuration Management: Experience with tools like Ansible, Puppet, Chef. DevOps: Knowledge of CI/CD pipelines, Jenkins, Docker, Kubernetes. Security: Understanding of security best practices and tools, Cloud security best practices, IAM, Security Groups, Compliance. Infrastructure as Code: Terraform, CloudFormation, Ansible Compute Services: EC2, GCE, Azure VMs. Storage Solutions: S3, GCS Customer Service Skills:- Communication: Strong verbal and written communication skills. Problem-Solving: Ability to diagnose and resolve technical issues efficiently. Interpersonal Skills: Building and maintaining relationships with clients. Training and Education: Ability to conduct training sessions for clients. Project Management: Managing customer projects and ensuring timely delivery. Knowledge/experience in configuring and supporting devices such as Cisco, Juniper, Checkpoint, etc. Knowledge Cloud Migration, Presale, Data Center relocation, Go-to-Market Strategy. Certifications: AWS Certified Solutions Architect Microsoft Certified: Azure Solutions Architect Expert Google Professional Cloud Architect Certified Kubernetes Administrator (CKA)

Intro

In my last article, we discussed containerized application orchestration using Docker Swarm. Now we’re going to deploy that same architecture with another leading orchestration platform: Kubernetes.

If you need a refresher, review the previous article.

What we’re building

  1. A Kubernetes environment with one (1) master/control plane node and three (3) worker nodes, using minikube.

  2. A Kubernetes application deployment that will deploy a 3-tier application architecture (Web/Apache, Node.js, and Postgres DB).

Note: We won’t have any application source code in this demo. We’ll focus more on the architecture and infrastructure of the application.

Prerequisites

  • Review of the last article about Docker Swarm.

  • Familiarity with Linux systems and commands.

  • Familiarity with Docker containers and orchestration methods, such as Docker Compose and Swarm.

  • Docker installed on your system.

  • Access to a command line tool.

What is Kubernetes?

Similar to Docker Swarm, Kubernetes is one of the leading container orchestration tools, but for larger and more complex application workloads. It offers much more flexibility, scalability, reliability, and specificity on how to deploy workloads.

So…Kubernetes is better than Docker Swarm, right?

Not necessarily! Although Docker Swarm is more lightweight, it’s often considered to be easier to use and scale. Like anything in the tech world, it really depends on your use case and what you're trying to build.

Some key differences

Kubernetes adds a layer of abstraction on top of Docker, so it has its own set of APIs to create networks, containers, and volumes. Like docker, we can access the API engine through the kubectl CLI.

Pods:
Pods are the basic unit of deployment. They are pretty much like services/containers in Swarm, but with a layer of abstraction on top (you’ll hear a lot of this). You can have multiple pods within a node and multiple containers within a pod. It can be a little confusing, but just know, when we mention ‘pods,’ think ‘containers.’

Services:
In Swarm, a service referred to container/task definitions and deployments. In Kubernetes, a service refers to a network and stable endpoint/address for pods to either communicate internally or with outside services. There are three main types of services: ClusterIP, NodePort, and LoadBalancer.

Let’s get started!

Install minikube and Hyperkit

Before start building our containerized application, we need to create a local dev environment. minikube is a great tool that allows us to implement multi-node Kubernetes clusters locally on our machines and comes with kubectl already installed.

Install minikube on your system however you see fit, but for me (macOS), the Homebrew package manager was the most straightforward method and also allows us to install the virtual machine environment we’ll need.

brew install minikube

To run a multi-node cluster locally, minikube needs a virtual machine driver, so we need to install a VM tool, such as Hyperkit, VirtualBox, or Parallels. I’ll be using Hyperkit, but feel free to choose your own.

brew install hyperkit

Cluster setup

If you remember from Docker Swarm, our container workloads run on ‘nodes,’ which can be physical or virtual machines/servers that provide high availability and fault tolerance. In Swarm, nodes are assigned roles of manager or worker. The principle is the same in Kubernetes, but nodes are split into roles of the control plane — which contain all the master nodes — and workers. Let’s set up our cluster in minikube:

  1. One (1) master/control plane node.

  2. Three (3) worker nodes.

By default, minikube will assign our local machine as a master/control plane node and all others as workers.

minikube start --driver hyperkit --nodes 4

Now, we can use kubectl commands to get our nodes

kubectl get nodes

Restrict pod assignments

We only want pods to be scheduled on worker nodes, so we need to add special instructions to the control plane in the form of a taint. A Taint allows a node to repel a set of pods by telling the scheduler not to schedule any pods unless they have a matching toleration.

kubectl taint nodes node_name key1=value1:NoSchedule

Deploy the frontend web tier

Similar to Docker Compose, an application’s entire infrastructure can be deployed using a single YAML file; however, Kubernetes YAML files can be a little more involved and detailed. Although a single YAML file can be used, the deployment configurations might be easier to manage if we break them out into a few organized files.

We’ll start with the frontend web tier and deploy our pods that contain our Apache web servers.

I’ve organized my ‘kube_app’ application directory into ‘frontend,’ ‘backend,’ and ‘database’ tiers.

Frontend web Service

We’ve been working with Docker Swarm so, you might be familiar with the term, ‘services.’ In Docker, a service is the image or image type run on a container, such as Apache, NGINX, Node, Postgres, etc… It is essentially a description and role of a container/task that will be deployed in a cluster.

A Kubernetes service refers to a network and stable endpoint/address for pods to either communicate internally or with outside services. There are three main types of services: ClusterIP, NodePort, and LoadBalancer.

A ClusterIP creates virtual IPs to allow pods to communicate with each other. A default ClusterIP is given to our cluster, so the control plane and worker nodes can communicate with each other.

kubectl get services

A NodePort allows the control plane to expose and allocate specific ports for outside access. This is the type of service we’ll need to access our website on port 80.

Create a new YAML file named frontend-kube-app.yml.

apiVersion: v1
kind: Service
metadata:
  name: web-httpd-service
spec:
  type: NodePort
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  selector:
    app: web-httpd
---

Each Kubernetes object, or kind, refers to a certain API group, so it’s important to know which apiVersion we should point to. An easy way to find this is to look at the api-resources and find the version of the object.

kubectl api-resources

Under metadata, we can give our service a name (required).

We specify the type as Nodeport and expose port 80.

The concept of selector and labels are very important. The service uses the selector to allow traffic to pods with matching labels.

The --- indicates a break and the end of the object.

Tip: Instead of trying to go back and forth between documentation, you can get details about each section of an object from the explain command. For example, you can use dot-notation to drill down on specific areas, such askubectl explain service.spec and it will give you an explanation of each option that falls under that category.

Frontend web deployment

Now we’re ready to create a Deployment! Deployments are more akin to Docker services. It’s the set of instructions and specifications for the pods including the image, number of replicas, ports, restart policies, etc…

In the same file, under the service object:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-httpd-deployment
spec:
  selector:
    matchLabels:
      app: web-httpd
  replicas: 10
  template:
    metadata:
      labels:
        app: web-httpd
    spec:
      containers:
        - name: web-httpd
          image: httpd:2.4.55
          ports:
            - containerPort: 80

We’re keeping this pretty simple, but you can see how more detailed specs can make this a little more complicated.

As we can see, pod deployments are under the kind: Deployment, with its own API Group. We should also notice the selector section where we've given it a matchlabel of the same name as the web-httpd-service selector, so they know to connect to each other.

Under template, we can provide the container information, such as the name, image, and port.

Deploy the pods and service

We’re ready to deploy our first pods! This is the easy part. All we have to do is apply the configuration and point to our YAML file.

kubectl apply -f ./frontend/frontend-kube-app.yml

We can run kubectl get all -o wide to get all the service and pod details for our cluster.

Alright! All ten of our pods are up and running, and we can see that none of them have been distributed on the master/control plane node.

We also see our NodePort service and its port mapping. Let’s see if we can access the web server by going to any of our node's IP addresses with the NodePort port (the number on the right). We can get a node's IP by running kubectl get nodes -o wide.

It works! We’ve just deployed our first Kubernetes pods to our cluster!

Deploy the backend application tier

Now that we have our first tier, out of the way, it’s pretty much rinse-and-repeat — but with varying specs to fit your needs.

Since we don’t have any application source code or need to connect anything at this point, we’ll just create a simple Deployment for the app backend. In the backend directory, create a new YAML file called backend-kube-app.yml.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs-app-deployment
spec:
  selector:
    matchLabels:
      app: nodejs-app
  replicas: 4
  template:
    metadata:
      labels:
        app: nodejs-app
    spec:
      containers:
        - name: nodejs-app
          image: node:19-alpine3.16
          command: ["sleep", "100000"]

We’ll enter a command of sleep for testing purposes to keep the container open and running.

Run the apply command again and get the results.

kubectl apply -f ./backend/backend-kube-app.yml
kubectl get all -o wide

Great! 4/4 pods we provisioned are running successfully.

Deploy the backend database tier

We’re ready for the final phase, and deploy the database (Postgres) for our app. The database can be a little more involved so let’s break down what we’ll need:

  1. A ConfigMap to store secrets such as usernames and passwords.

  2. A PersistantVolume and PersistantVolumeClaim to define and allocate storage for the A database.

  3. A Service to connect the database

  4. A Deployment to define and deploy the database pod to our cluster.

Create a ConfigMap

It’s always best practice to separate data from code. A good way to do this in development is through a ConfigMap. Kubernetes has its own API group for this purpose.

In the database directory, create a new YAML file called postgres-config.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres-db
data:
  POSTGRES_DB: postgresdb
  POSTGRES_USER: admin
  POSTGRES_PASSWORD: mypass

Here, we’ll store environment variables such as the DB name, user, and password. Again, take special notice of the labels, since this is what our Deployment will look for to connect to the config.

Apply the ConfigMap to the cluster.

kubectl apply -f ./database/postgres-config.yml

Create a PersistantVolume & PersistantVolumeClaim

Since pods are ephemeral — meaning once they’re gone, so is all the data they carried — we’ll need persistent storage in place for our database. Here, we’ll define the capacity, access, and the hostPath of the volume.

Once we create the volume, we need a PersistantVolumeClaim, which defines how the users request and consume PV resources.

Create a new YAML file named postgres-pvc-pv.yml.

kind: PersistentVolume
apiVersion: v1
metadata:
  name: postgres-pv-volume  # Sets PV's name
  labels:
    type: local  # Sets PV's type to local
    app: postgres-db
spec:
  storageClassName: manual
  capacity:
    storage: 5Gi # Sets PV Volume
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/mnt/data"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: postgres-pv-claim  # Sets name of PVC
  labels:
    app: postgres-db
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteMany  # Sets read and write access
  resources:
    requests:
      storage: 5Gi  # Sets volume size

Apply the PV and PVC to the cluster.

kubectl apply -f ./database/postgres-pvc-pv.yml

Create the Service

For now, we’ll just create a simple NodePort service and expose port 5432.

Create a new YAML file named database-kube-app.yml.

apiVersion: v1
kind: Service
metadata:
  name: postgres # Sets service name
  labels:
    app: postgres-db # Labels and Selectors
spec:
  type: NodePort # Sets service type
  ports:
    - port: 5432 # Sets port to run the postgres application
  selector:
    app: postgres-db
---

Create the Deployment

Finally, we’ll create the deployment options for our Postgres database (in the same file).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-db-deployment  # Sets Deployment name
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-db
  template:
    metadata:
      labels:
        app: postgres-db
    spec:
      containers:
        - name: postgres-db
          image: postgres:10.1 # Sets Image
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 5432  # Exposes container port
          envFrom:
            - configMapRef: # Maps env variable from ConfigMap
                name: postgres-config
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgres-volume
      volumes:
        - name: postgres-volume
          persistentVolumeClaim:
            claimName: postgres-pv-claim # Maps claim from PersistantVolumeClaim

We can map our environment variables envFrom and volumes from our config and PVC (this is why names and labels are important!).

Apply the service and Deployment to the cluster.

kubectl apply -f ./database/postgres-pvc-pv.yml

Test database connection

For good measure, let’s test and see if we can connect to the database.

kubectl exec -it [pod-name] --  psql -h localhost -U admin --password -p 5432 postgresdb

Enter the DB password and type \l to list all the databases.

And we’re in!

5 views

More from this blog

C

CloudGrad

96 posts