First Kubernetes deployment
Learning objectives
- You know the difference between a Kubernetes deployment and a Kubernetes service.
- You can create an application that runs on Minikube.
Let's create our first Kubernetes deployment (even though it's on Minikube, it's still Kubernetes). For this, we create an application, package it as a Docker image, and then deploy it to Kubernetes. The final project structure of the application will be as follows.
tree --dirsfirst
.
├── kubernetes
│ ├── my-app-deployment.yaml
│ └── my-app-service.yaml
└── my-app
├── app.js
└── Dockerfile
Application and Dockerfile
As the application, we'll use a simple Deno application that runs on the hostname 0.0.0.0
and the port 7777
and returns a string as a response to requests.
The hostname
0.0.0.0
is used as a wildcard that matches all hostnames. This is relevant for exposing the application later.
Create a folder called my-app
. Create the file app.js
in the folder, and place the following code to it.
The application code is as follows.
const helloMessage = `Hello from server: ${Math.floor(10000 * Math.random())}`;
const handleRequest = async (request) => {
return new Response(helloMessage);
};
Deno.serve({ hostname: "0.0.0.0", port: 7777 }, handleRequest);
With the above code, when the application starts, it creates the string helloMessage
that starts with Hello from server:
. The string has a random number at the end. When a request is received, the application returns a response with the helloMessage
string.
Next, create the file Dockerfile
to the folder my-app
and place the following configuration to it.
FROM denoland/deno:alpine-1.42.2
EXPOSE 7777
WORKDIR /app
COPY . .
RUN deno cache app.js
CMD [ "run", "--allow-net", "--unstable", "app.js" ]
This bases our image on the official Deno image, exposes the port 7777
, copies the files from the current directory to the image, caches the application, and runs it.
Building a Docker image for Minikube
By default, Docker images are built for the Docker daemon running on the host machine. Minikube, however, runs its own daemon inside its virtual machine. To be able to access Docker images in Minikube, we need to build so that Minikube can access them.
To build an image for Minikube, we use the minikube image build
command that is given the name of the image followed by -t
, the path to the Dockerfile followed by -f
, and the local directory (simply .
). The command builds the image and makes it available for Minikube.
When inside the folder my-app
that hosts the Dockerfile
and app.js
that we created above, we build an image called my-app
image with the following command.
minikube image build -t my-app -f ./Dockerfile .
(this takes a while...)
Successfully tagged my-app:latest
Once the image has been built, we can check whether it's available in Minikube with the minikube image list
command.
minikube image list
registry.k8s.io/pause:3.9
registry.k8s.io/pause:3.6
registry.k8s.io/kube-scheduler:v1.26.1
registry.k8s.io/kube-proxy:v1.26.1
registry.k8s.io/kube-controller-manager:v1.26.1
registry.k8s.io/kube-apiserver:v1.26.1
registry.k8s.io/etcd:3.5.6-0
registry.k8s.io/coredns/coredns:v1.9.3
gcr.io/k8s-minikube/storage-provisioner:v5
docker.io/library/my-app:latest
docker.io/denoland/deno:alpine-1.42.2
It indeed is -- we see the row docker.io/library/my-app:latest
in the output.
It is also possible to build the images with Docker and copy them to Minikube. For additional approaches, see Using Local Docker Images With Minikube.
Creating a deployment configuration
Deployments in Kubernetes are used to manage the lifecycle of pods (which run images), including creating, updating, and deleting them. Deployments are configured using yaml
files; we'll create one for our application as well.
Create a folder caled kubernetes
as a sibling folder to the folder my-app
. Create the file my-app-deployment.yaml
into the folder kubernetes
and place the following configuration in it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-deployment
labels:
app: my-app
spec:
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest
imagePullPolicy: Never
ports:
- containerPort: 7777
The above configuration creates a deployment called my-app-deployment
that runs a pod with a container called my-app
. The container is based on the image my-app:latest
that we built earlier. The container exposes the port 7777
that the application listens to.
At this point, the project structure should be as follows.
tree --dirsfirst
.
├── kubernetes
│ └── my-app-deployment.yaml
└── my-app
├── app.js
└── Dockerfile
Deploying the application
To deploy the application, we use the kubectl apply
command that takes the path to the deployment configuration file as an argument (after -f
). Before we do that, let's check out our current deployments -- this can be done with the command kubectl get deployments
.
kubectl get deployments
No resources found in default namespace.
As expected, there are no deployments yet. Let's deploy our application using the kubectl apply
command as follows.
kubectl apply -f kubernetes/my-app-deployment.yaml
deployment.apps/my-app-deployment created
Now, when we look at our deployments again, we see the one we just created.
kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
my-app-deployment 1/1 1 1 57s
The 1/1 in the READY
column means that there is one pod running and that it's ready to serve requests, while the UP-TO-DATE
column shows that the deployment is up to date with the configuration. We can list our pods with the kubectl get pods
command.
kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-deployment-85bcb74bcd-mc2r8 1/1 Running 0 98s
As we see, the pod is up and running. We can take a peek at the logs of the pod by running the command kubectl logs
, which is followed by the name of the pod we wish to look into (above, my-app-deployment-85bcb74bcd-mc2r8
).
kubectl logs my-app-deployment-85bcb74bcd-mc2r8
Listening on http://localhost:7777/
It seems that the application is running and listening for request. When we access the address http://localhost:7777
, we see nothing however. This is because we haven't exposed the deployment to the world yet -- it is just running within Minikube.
Exposing the application
To expose the deployment to the world, we need a service. Services are used to expose the pods of a deployment to the world. Perhaps unsurprisingly, they are also configured using yaml
.
Create the file my-app-service.yaml
into the folder kubernetes
and place the following configuration in it.
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
type: LoadBalancer
ports:
- port: 7777
targetPort: 7777
protocol: TCP
selector:
app: my-app
The above configuration creates a service called my-app-service
that exposes the port 7777
of the pods of the deployment my-app-deployment
to the world. The type
of the service is LoadBalancer
, which means that Minikube will create a load balancer for us -- in effect, we could have multiple pods and Minikube would handle the load balancing for us.
Let's deploy the service with the kubectl apply
command.
kubectl apply -f kubernetes/my-app-service.yaml
service/my-app-service created
Now, when we look into our services with the kubectl get services
command, we see the one we just created.
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 162m
my-app-service LoadBalancer 10.104.69.51 <pending> 7777:30788/TCP 11s
Accessing the application
Now that we have a service, we can access the application, given an address. To find an address connected to our service, we use Minikube's service command.
Running the command minikube service my-app-service --url
creates a tunnel to the service called my-app-service
, which is the one that is presently exposing our application.
minikube service my-app-service --url
http://192.168.49.2:32217
With the above output, we can access the application locally at the address http://192.168.49.2:32217
, as shown in Figure 1.
Cleaning up
Once we've gotten everything working, it's time to clean up. We can delete the deployment and the service with the kubectl delete
command, which is followed by the type of the resource and the name of the resource.
As an example, to delete the service and the deployment, we would run the following commands.
kubectl delete -f kubernetes/my-app-service.yaml
service "my-app-service" deleted
kubectl delete -f kubernetes/my-app-deployment.yaml
deployment.apps "my-app-deployment" deleted