Table of Contents
Services and dependencies work together in Kubernetes to ensure that applications have the required network access and resources, enabling seamless communication and efficient operation within the cluster environment. Creating and managing services is essential in Kubernetes to enable network access to sets of pods. Application dependencies ensure your apps have everything they need to run correctly and efficiently.
This article is the second part of the series "Platform Engineering Workflows" and focuses on adding and managing services and dependencies in Kubernetes. It follows the first part, which covers environment variables and configuration.
The article provides a step-by-step guide on how to set up a demo Kubernetes deployment and create a service. Additionally, it demonstrates the process of introducing a dependency like a MySQL database and establishing a connection with the existing deployment.
You'll also explore best practices for managing services and dependencies in Kubernetes, from utilizing namespaces and service registries to implementing health checks and setting up autoscaling.
Platform Engineering + Kubernetes Series
- Platform Engineering on Kubernetes for Accelerating Development Workflows
- Adding Environment Variables and Changing Configurations in Kubernetes
- Adding Services and Dependencies in Kubernetes
- Adding and Changing Kubernetes Resources
- Enforcing RBAC in Kubernetes
- Spinning up a New Kubernetes Environment
Adding Services and Dependencies to Kubernetes
Kubernetes services and dependencies are very important in any large-scale Kubernetes application. Services allow pods to communicate with each other and external systems, while dependencies are the various resources your application requires to function correctly. Whether they include a supporting library, a database, or an external application, correctly managing dependencies ensures the smooth operation of your application.
To understand the roles that services and dependencies play, you'll create a demo application in Node.js that depends on a MySQL database, then Dockerize the Node.js application. You'll then create a deployment manifest file to add the application to your cluster and a service to make the application available via an endpoint.
Creating the Demo Application
You'll create a simple server application developed on Node.js that will be deployed on Kubernetes with a MySQL dependency. If the app is able to connect to the database, it will display a message saying "Connected to the database." If it can't make a connection, it will display a different message saying "Cannot connect to the database."
Start by creating a directory titled nodeapp
and cd
into the directory:
mkdir nodeapp
cd nodeapp
Initialize a new Node project and package.json
file in the directory:
npm init -y
Create a server.js
file:
vim server.js
Paste the following into the server.js
file:
const express = require('express');
const mysql = require('mysql');
const app = express();
const port = 8080;
let dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: 'mydb'
};
let connection;
const databaseConnectionError = () => {
connection = mysql.createConnection(dbConfig);
connection.connect(function(err) {
if(err) {
console.log('Error when connecting to db:', err);
setTimeout(databaseConnectionError, 2000);
}
});
connection.on('error', function(err) {
console.log('db error', err);
if(err.code === 'PROTOCOL_CONNECTION_LOST') {
databaseConnectionError();
} else {
throw err;
}
});
}
databaseConnectionError();
app.get('/', (req, res) => {
connection.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
if (error) {
res.send('Cannot connect to the database');
} else {
res.send('Connected to the database');
}
});
});
app.listen(port, () => {
console.log(`Node app listening at http://localhost:${port}`)
});
The above script contains the database and server configuration. The database config allows the application to connect to the database from the application (provided that the details are properly set), while the server config allows the application to receive requests from users through the endpoint configured.
Finally, install the express
and mysql
packages:
npm install express mysql
Creating the Docker Image
The application needs to be Dockerized so that you can use it in your Kubernetes cluster.
First, set up a Dockerfile
that will be used to create a Docker image:
vim Dockerfile
Next, paste in the following:
# Use an official Node.js runtime as the base image
FROM node:18.14.2
# Set the working directory in the container to /app
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install the application dependencies
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
# Make the application's port available to the outside world
EXPOSE 8080
# Define the command that should be executed when the container starts
CMD [ "node", "server.js" ]
For the purposes of this tutorial, you'll be using minikube to create a local cluster on your local machine.
Since this tutorial uses minikube, you won't need to deploy your Docker image on Docker Hub. However, if you opt to use a Kubernetes cluster that is not hosted on your local machine, then you'll have to deploy your Docker image.
If you are using a minikube cluster, before you build your application image, run the following command:
eval $(minikube docker-env)
This is to ensure that Docker and minikube are using the same Docker daemon.
Run the following command to build the Docker image:
docker build -t nodeapp:v1 .
Creating a Kubernetes Deployment and Service
Now that you've built the Docker image for your application, you can use it in your Kubernetes cluster. To add your application to the Kubernetes cluster, you have to create a deployment.
Move out of the nodeapp
directory and create a new directory titled nodeapp-k8s
:
cd .. && mkdir nodeapp-k8s
Move into the nodeapp-k8s
directory and create a new deployment called nodeapp-deployment
:
vim nodeapp-deployment.yaml
Paste in the following code:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodeapp-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nodeapp
template:
metadata:
labels:
app: nodeapp
spec:
containers:
- name: nodeapp
image: nodeapp:v1
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: mysql-service
- name: DB_USER
value: root
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
For more information about Kubernetes deployments, visit the official documentation.
To avoid potential pod failures, create the database secret before adding the deployment to your cluster. This ensures the secret is available when the deployment's pods start, facilitating a smooth deployment process.
To create the secret, execute the following command:
kubectl create secret generic mysql-pass --from-literal=password='yourpassword'
Run the following command to add the deployment to your cluster:
kubectl apply -f nodeapp-deployment.yaml
You can use the following command to confirm that your deployment is running:
kubectl get deployment
You should get a response similar to the one below:
NAME READY UP-TO-DATE AVAILABLE AGE
nodeapp-deployment 1/1 1 1 12s
You now need to expose your deployed application so that it will be available via an endpoint.
Create a nodeapp-service.yaml
file:
vim nodeapp-service.yaml
Paste the following into the file:
apiVersion: v1
kind: Service
metadata:
name: nodeapp-service
spec:
selector:
app: nodeapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
Run the following command to apply nodeapp-service
to your cluster:
kubectl apply -f nodeapp-service.yaml
Open a new terminal and use the following command to get the URL for nodeapp-service
:
minikube service nodeapp-service --url
This command is necessary because
nodeapp-service
is a LoadBalancer service type. In a cloud-based Kubernetes cluster, an external IP is automatically generated and available for communication with the service. However, when running Kubernetes on minikube, this external IP is not accessible. Therefore, you need to run the above command to retrieve the IP you can use locally to communicate with your service. Note that this command is not needed in a cloud-based Kubernetes cluster.
In a separate terminal, try to access the URL, replacing the address below with the one provided by the minikube service
command:
curl http://127.0.0.1:56789
Once you ping the URL using the curl command, you'll get the following response:
Cannot connect to the database%
You can see that your Node application depends on a database dependency to function properly. Without the database, the application won't be able to function.
Deploying the Database Dependency
You'll need to deploy the database dependency so that your application can function properly.
In the nodeapp-k8s
directory, create a new file named mysql-deployment.yaml
:
vim mysql-deployment.yaml
Paste the following content into the file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-deployment
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7.11
env:
- name: MYSQL_DATABASE
value: mydb
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
Apply the deployment file to the cluster:
kubectl apply -f mysql-deployment.yaml
Now, run the following command:
kubectl get deployment
You should see the following:
NAME READY UP-TO-DATE AVAILABLE AGE
mysql-deployment 1/1 1 1 2m
nodeapp-deployment 1/1 1 1 20m
Next, create a service for mysql-deployment
by creating a new file titled mysql-service.yaml
:
vim mysql-service.yaml
Paste in the following code:
apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
selector:
app: mysql
ports:
- protocol: TCP
port: 3306
type: ClusterIP
Run the following command to apply the service:
kubectl apply -f mysql-service.yaml
Now, run kubectl get service
to check that the MySQL service has been deployed properly:
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 44m
mysql-service ClusterIP 10.109.17.243 <none> 3306/TCP 11s
nodeapp-service LoadBalancer 10.108.177.235 <pending> 80:32530/TCP 30m
After a few minutes, ping the nodeapp-service
URL again, using the address provided by the minikube service
command earlier:
curl http://127.0.0.1:56789
You should get the following response:
Connected to the database%
This tutorial demonstrated a simple use case for service dependencies, underscoring the importance of their correct configuration and deployment. To ensure that your deployments are as efficient as possible and optimize overall performance, it's essential to have a comprehensive understanding of all service dependencies and ensure their correct setup prior to deploying your services.
Best Practices for Managing Services and Dependencies in Kubernetes
To ensure efficient and maintainable management of services and dependencies within Kubernetes, there are some best practices you should follow.
Use Correct Organization with Namespaces
Namespaces in Kubernetes allow you to divide cluster resources between multiple users or groups of users (such as teams, projects, or clients), applications, or environments. They provide a scope for the names of Kubernetes objects like pods and deployments, which must be unique within a namespace, but not across namespaces.
To create a new namespace, you can use the kubectl create namespace
command:
kubectl create namespace my-namespace
Then, in your Kubernetes manifest files, you can specify the namespace for each resource:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-namespace
...
Alternatively, you can specify the namespace resources that can be deployed when using kubectl
, as seen below:
kubectl -n team-a-ns apply -f team-a-app.yaml
kubectl -n team-b-ns apply -f team-b-app.yaml
This practice helps maintain order, especially in large clusters with many users or teams. It also adds isolation, as resources in one namespace can't directly interact with resources in another.
Use a Service Registry for Better Service Discovery
Using a service registry enhances service discovery capabilities. Kubernetes' built-in service registry mechanism, service discovery, allows pods to communicate with each other through services. It enables consistent access to services in dynamic environments where pods are frequently created and destroyed, eliminating the need to track changing IP addresses. It also provides built-in load balancing, distributing network traffic to prevent overload on specific service instances and improving overall system efficiency. Moreover, service discovery ensures high availability by automatically redirecting traffic to available pods if one fails, guaranteeing uninterrupted service access and promoting fault tolerance.
Here's an example of a service manifest that creates a service:
apiVersion: v1
kind: Service
metadata:
name: example-service
spec:
selector:
app: example-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
In this example, any pod with the label app: example-app
can be discovered through the example-service
service.
Use a Service Mesh to Manage Interservice Communication
A service mesh is an infrastructure layer that manages service-to-service communication over the network. It provides valuable features such as traffic management, service discovery, load balancing, security, and observability. With a service mesh like Istio or Linkerd, you can control load balancing, service discovery, and more on a per-service level.
For example, if you're using Istio, you can set up a traffic rule that splits traffic between two versions of a service like this:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: example-service
spec:
hosts:
- example-service
http:
- route:
- destination:
host: example-service
subset: v1
weight: 60
- destination:
host: example-service
subset: v2
weight: 40
The above config sends 60 percent of the traffic to v1 of example-service
and 40 percent to v2.
Use DNS-Based Service Discovery
Kubernetes employs DNS-based service discovery, which enables applications to discover services using their DNS names rather than IP addresses. This simplifies service management by providing consistent DNS names even if the underlying IP addresses change.
When you create a service, a unique DNS name is assigned in the following format:
service-name.namespace-name.svc.cluster.local
For example, when a pod within a Kubernetes cluster needs to communicate with a service, it's more efficient to utilize the service's DNS name rather than its IP address. This is because when a service is redeployed, it might receive a new IP address. If pods relied on the old IP address to communicate with the service, they would encounter connection issues after the redeployment. However, using the service's DNS name establishes a consistent point of contact that remains unchanged across deployments as long as the service's name remains the same. This ensures uninterrupted communication between the pod and the service, regardless of any redeployments.
Choose the Right Resource Type: Deployment vs. StatefulSet for Dependencies
When managing dependencies in Kubernetes, choosing the right type of resource for each dependency is essential. Deployments are best for stateless applications, while StatefulSets are best for stateful applications.
For example, a MySQL database would be better suited to a StatefulSet, as each replica needs its own unique identity and storage. On the other hand, a Node.js web server would be better suited to a deployment, as each replica is interchangeable.
Use Pod Affinity and Anti-Affinity
Pod affinity and anti-affinity enable you to define rules regarding the preferred or undesired scheduling of pods onto specific nodes. These rules can be based on node labels or the presence of other pods running on the nodes.
For example, you can use pod anti-affinity to ensure that two pods of the same type don't get scheduled on the same node:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
...
template:
...
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-app
topologyKey: "kubernetes.io/hostname"
This rule prevents the scheduler from placing two pods with the label app: my-app
on the same node.
Balance Traffic across Pods
A service in Kubernetes acts as a load balancer, distributing traffic across the pods associated with the service. The default mode uses the round robin method, but for more complex scenarios, you can use session affinity to ensure all requests from a particular client are directed to the same pod:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
sessionAffinity: ClientIP
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
Implement Health Checks
Kubernetes allows you to specify liveness and readiness probes in your pod specifications. These probes are used to check your containers' health and ensure they're ready to receive traffic.
Here's an example of a liveness probe that checks an HTTP endpoint:
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
containers:
- name: example-container
image: example-image
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
In this example, the kubelet sends an HTTP GET request to the /healthz
endpoint of the container every 5 seconds, 20 seconds after the container starts. If the endpoint returns a failure, the kubelet restarts the container.
Configure Autoscaling in Your Cluster
Kubernetes has built-in autoscaling through the Horizontal Pod Autoscaler (HPA). The HPA adjusts the number of replicas in a deployment or StatefulSet based on observed CPU utilization or custom metrics.
Here's an example of an HPA configuration:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: example-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: example-deployment
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
In this example, the HPA increases or decreases the number of replicas of example-deployment
to maintain an average CPU utilization of 50 percent. It won't reduce the number of replicas below 1 or increase it above 10.
Additionally, you can apply an HPA using the kubectl
command, as seen below:
kubectl autoscale deployment example-hpa --min=2 --max=5 --cpu-percent=80
Conclusion
This tutorial explored how to incorporate services and dependencies into your applications within a Kubernetes environment. You deployed a demo Node.js application and integrated a MySQL database as a dependency, emphasizing the significance of services and dependencies in ensuring seamless operation within your cluster. The article also provided some best practices for managing services and dependencies in Kubernetes, encompassing key aspects such as namespaces, service registries, autoscaling, and more.
Understanding Kubernetes services and dependencies and managing them effectively is crucial for any platform engineering workflow. They are integral to setting up robust, scalable applications and functional applications in Kubernetes.