{"id":468,"date":"2025-09-14T07:15:59","date_gmt":"2025-09-14T07:15:59","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=468"},"modified":"2025-09-14T07:15:59","modified_gmt":"2025-09-14T07:15:59","slug":"kubernetes-kind-cluster-tutorial-setup-and-deploy-apps","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=468","title":{"rendered":"Kubernetes Kind Cluster Tutorial: Setup, Deploy Apps, Ingress, LoadBalancer &#038; PVs"},"content":{"rendered":"<p>In this tutorial, you will learn to setup Kubernetes Kind Cluster, deploy apps, and configure Ingress, LoadBalancer &amp; PVs for local development and testing.<\/p>\n<p>We are going to look at:<\/p>\n<ul>\n<li>Install and set up the Kind cluster&nbsp;<\/li>\n<li>Deploying an example Nginx app and exposing it with NodePort<\/li>\n<li>Implementing LoadBalancer on Kind with MetalLB<\/li>\n<li>Implementing Ingress with kind<\/li>\n<li>Persistent volumes in Kind<\/li>\n<li>Enabling feature gate on Kind<\/li>\n<\/ul>\n<p>Overall, we will look at everything you need to develop and test apps on Kubernetes in your local workstation using kind.<\/p>\n<h2 id=\"what-is-kind-cluster\">What is Kind Cluster?<\/h2>\n<p><a href=\"https:\/\/kind.sigs.k8s.io\/?ref=devopscube.com\" rel=\"noreferrer\">Kind<\/a> is a tool that is used to create a portable and lightweight <a href=\"https:\/\/devopscube.com\/building-kubernetes-cluster-the-hard-way\/\" rel=\"noreferrer\">Kubernetes cluster<\/a>. It is the best choice for testing general use cases because it is easy to deploy and delete.<\/p>\n<p>Kind <a href=\"https:\/\/devopscube.com\/run-docker-in-docker\/\" rel=\"noreferrer\">runs on Docker<\/a>. Meaning, when you create a Kind Cluster, <a href=\"https:\/\/devopscube.com\/monitor-docker-containers-guide\/\" rel=\"noreferrer\">Docker containers<\/a> are created and act as cluster nodes.<\/p>\n<p>In the backend, Kind used <a href=\"https:\/\/devopscube.com\/setup-kubernetes-cluster-kubeadm\/\" rel=\"noreferrer\">Kubeadm<\/a> to bootstrap the cluster.<\/p>\n<p>Also, the Kind cluster will not get deleted even after system restarts, and if you are using Docker Desktop, you can pause the containers using Docker Desktop dashboard.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">If you are preparing for <a href=\"https:\/\/devopscube.com\/cka-exam-study-guide\/\" rel=\"noreferrer\">CKA<\/a>, <a href=\"https:\/\/devopscube.com\/ckad-exam-study-guide\/\" rel=\"noreferrer\">CKAD<\/a> or <a href=\"https:\/\/devopscube.com\/cks-exam-guide-tips\/\" rel=\"noreferrer\">CKS<\/a> certifications, you can use the kind cluster for practicing exam scenarios.<\/div>\n<\/div>\n<h2 id=\"setup-prerequisites\">Setup Prerequisites<\/h2>\n<p>Before going further, make sure you have the following prerequisites.<\/p>\n<ul>\n<li><a href=\"https:\/\/devopscube.com\/what-is-docker\/\" rel=\"noreferrer\">Docker<\/a> installed on your system<\/li>\n<li><a href=\"https:\/\/devopscube.com\/kubectl-set-context\/\" rel=\"noreferrer\">kubectl<\/a> installed on your system<\/li>\n<\/ul>\n<p>Follow the steps below to set up a Kind cluster in your system.<\/p>\n<h2 id=\"step-1-install-kind\">Step 1: Install Kind<\/h2>\n<p>Before creating the cluster, you need to install Kind on your system.<\/p>\n<p>Run the following commands based on your OS.<\/p>\n<p>For Linux:<\/p>\n<pre><code class=\"language-bash\">[ $(uname -m) = x86_64 ] &amp;&amp; curl -Lo .\/kind https:\/\/kind.sigs.k8s.io\/dl\/v0.29.0\/kind-linux-amd64\n\n[ $(uname -m) = aarch64 ] &amp;&amp; curl -Lo .\/kind https:\/\/kind.sigs.k8s.io\/dl\/v0.29.0\/kind-linux-arm64\n\nchmod +x .\/kind\n\nsudo mv .\/kind \/usr\/local\/bin\/kind<\/code><\/pre>\n<p>For Mac:<\/p>\n<pre><code class=\"language-bash\">brew install kind<\/code><\/pre>\n<p>For Windows PowerShell:<\/p>\n<pre><code class=\"language-bash\">curl.exe -Lo kind-windows-amd64.exe https:\/\/kind.sigs.k8s.io\/dl\/v0.29.0\/kind-windows-amd64\n\nMove-Item .\\kind-windows-amd64.exe c:\\some-dir-in-your-PATH\\kind.exe<\/code><\/pre>\n<h2 id=\"step-2-create-kind-cluster\">Step 2: Create Kind Cluster<\/h2>\n<p>To create a Kind cluster, you need a YAML config file. Create a YAML file <code>kind-cluster.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">kind: Cluster\napiVersion: kind.x-k8s.io\/v1alpha4\nname: multi-node-cluster\nnodes:\n  - role: control-plane\n    image: kindest\/node:v1.33.0\n    extraPortMappings:\n      - containerPort: 30000\n        hostPort: 30000\n        protocol: TCP\n      - containerPort: 31000\n        hostPort: 31000\n        protocol: TCP\n      - containerPort: 32000\n        hostPort: 32000\n        protocol: TCP\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\n    image: kindest\/node:v1.33.0\n  - role: worker\n    image: kindest\/node:v1.33.0<\/code><\/pre>\n<p>This config file will create a three node cluster, 1 control plane, and two worker nodes.<\/p>\n<p>Under the <code>nodes<\/code> block, you can see the following:<\/p>\n<ul>\n<li><strong>role<\/strong> &#8211; here we specify if the node is control plane or worker.<\/li>\n<li><strong>image<\/strong> &#8211; This is an optional, even without this field, we can use the config file. Use this field if you need a specific Kubernetes version.<\/li>\n<li><strong>extraPortMappings<\/strong> &#8211; Under this block, we have to list the ports that will be needed to expose the workloads outside the cluster as <strong><code>NodePorts<\/code><\/strong>.<\/li>\n<\/ul>\n<p>We are using ports <code>80<\/code> and <code>443<\/code> inside the <strong><code>extraPortMappings<\/code><\/strong> for Ingress.<\/p>\n<p>Run the following command to create a Kind cluster with the configurations in the YAML file.<\/p>\n<pre><code class=\"language-bash\">kind create cluster --config kind-cluster.yaml<\/code><\/pre>\n<p>Once it is created, you can see the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-64.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"814\" height=\"390\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-64.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-64.png 814w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>The cluster context will be added to <a href=\"https:\/\/devopscube.com\/kubernetes-kubeconfig-file\/\" rel=\"noreferrer\">kubeconfig file<\/a> automatically, run the following to verify if kubectl can access the cluster.<\/p>\n<pre><code class=\"language-bash\">kubectl get no<\/code><\/pre>\n<p>You can see the nodes of the Kind cluster.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-65.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"1114\" height=\"144\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-65.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-65.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-65.png 1114w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"testing-kind-cluster\">Testing Kind Cluster<\/h2>\n<p>For testing the cluster, we are going to do the following:<\/p>\n<ul>\n<li>Create a simple Nginx on the cluster and expose it using NodePort.<\/li>\n<li>Expose the application using LoadBalancer<\/li>\n<li>Expose service using Ingress<\/li>\n<\/ul>\n<p>In this section, you will deploy an Nginx application in the Kind cluster and expose it using NodePort.<\/p>\n<h3 id=\"deploy-a-simple-nginx-on-the-kind-cluster\">Deploy a simple Nginx on the Kind Cluster<\/h3>\n<p>To deploy the application, create a YAML file <code>deploy.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: nginx-demo\n  template:\n    metadata:\n      labels:\n        app: nginx-demo\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n<\/code><\/pre>\n<p>This file will create a Nginx deployment. Run the following command to deploy it.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f deploy.yaml<\/code><\/pre>\n<p>Then, run the following command to verify if the pod is created.<\/p>\n<pre><code class=\"language-bash\">kubectl get po<\/code><\/pre>\n<h3 id=\"expose-the-application-using-nodeport\">Expose the Application using NodePort<\/h3>\n<p>Before exposing using NodePort, you should know that we can only use the NodePorts that are specified during cluster creation.<\/p>\n<p>According to our config file, the NodePorts we specified are <code>30000, 31000, and 32000<\/code>.<\/p>\n<p>We are going to expose the application using NodePort <code>32000<\/code> , for that, create a YAML file <code>nodeport-svc.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-nodeport\nspec:\n  type: NodePort\n  selector:\n    app: nginx-demo\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n      nodePort: 32000\n<\/code><\/pre>\n<p>Run the following command to create it.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f nodeport-svc.yaml<\/code><\/pre>\n<p>Then run the following command to check if the service is created.<\/p>\n<pre><code class=\"language-bash\">kubectl get svc<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-69.png\" class=\"kg-image\" alt=\"verifying if the service is created\" loading=\"lazy\" width=\"1968\" height=\"570\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-69.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-69.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/08\/image-69.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-69.png 1968w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, use the URL <code>localhost:32000<\/code> to get the Nginx webpage in the browser.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-69.png\" class=\"kg-image\" alt=\"accessing the nginx web page on the browser\" loading=\"lazy\" width=\"752\" height=\"392\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-69.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-69.png 752w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"expose-applications-in-kind-cluster-using-loadbalancer\">Expose applications in Kind Cluster using LoadBalancer<\/h2>\n<p>To expose the application using LoadBalancer on a Kind cluster, you can either use MetalLB or the <a href=\"https:\/\/devopscube.com\/popular-cloud-providers-for-kubernetes\/\" rel=\"noreferrer\">cloud provider<\/a> Kind.<\/p>\n<p>For this setup, we will be using <code>MetalLB<\/code>. It will act as a software load balancer and assigns a IP address form your local IP pool assigned in your system. <\/p>\n<h3 id=\"install-and-configure-metallb\">Install and Configure MetalLB<\/h3>\n<p>MetalLB is a LoadBalancer for Kubernetes clusters that are running on a local system.<\/p>\n<p>To install MetalLB in the cluster, run the following command.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f https:\/\/raw.githubusercontent.com\/metallb\/metallb\/v0.15.2\/config\/manifests\/metallb-native.yaml<\/code><\/pre>\n<p>Once it&#8217;s installed, run the following command to get the Kind clusters CIDR range.<\/p>\n<pre><code class=\"language-bash\">docker network inspect kind | jq -r '.[0].IPAM.Config[].Subnet'<\/code><\/pre>\n<p>You will get both IPv4 and IPv6 subnets as showb below.<\/p>\n<pre><code class=\"language-bash\">fc00:f853:ccd:e793::\/64\n172.18.0.0\/16<\/code><\/pre>\n<p>From that use the IPv4 range <code>172.18.0.0\/16<\/code>.<\/p>\n<p>Now, create an IP pool for MetalLB with a range from the Kind clusters CIDR, the external IP assigned to the service type LoadBalancer will be taken from this pool. <\/p>\n<p>Create a YAML file <code>metallb-pool.yaml<\/code> and copy the following content.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\u26a0\ufe0f<\/div>\n<div class=\"kg-callout-text\">In the following file, change the IP range <code spellcheck=\"false\" style=\"white-space: pre-wrap;\">172.18.0.100 - 172.18.0.200<\/code> if your CIDR range is other than <code spellcheck=\"false\" style=\"white-space: pre-wrap;\">172.18.0.0\/16<\/code><\/p>\n<p>On Linux\/macOS execute <code spellcheck=\"false\" style=\"white-space: pre-wrap;\">ip addr show<\/code> and look for something like en0 entries. <\/p>\n<p>On Windows run <code spellcheck=\"false\" style=\"white-space: pre-wrap;\">ipconfig<\/code> and check IPv4 Address + Subnet Mask<\/div>\n<\/div>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta1\nkind: IPAddressPool\nmetadata:\n  name: lb-pool\n  namespace: metallb-system\nspec:\n  addresses:\n    - 172.18.0.100 - 172.18.0.200\n---\napiVersion: metallb.io\/v1beta1\nkind: L2Advertisement\nmetadata:\n  name: l2\n  namespace: metallb-system\nspec:\n  ipAddressPools:\n    - lb-pool\n<\/code><\/pre>\n<p>Run the following command to create the IP pool.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f metallb-pool.yaml\n<\/code><\/pre>\n<h3 id=\"expose-application-using-loadbalancer\">Expose Application using LoadBalancer<\/h3>\n<p>We are going to expose the same Nginx application using a service type <code>LoadBalancer<\/code>.<\/p>\n<p>Create a YAML file <code>lb-svc.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-lb\nspec:\n  type: LoadBalancer\n  selector:\n    app: nginx-demo\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n      nodePort: 31000<\/code><\/pre>\n<p>We must specify the NodePort we specified during cluster creation because, if it assigns a random nodeport, we cannot access it outside the cluster.<\/p>\n<p>Run the following command to create the service.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f lb-svc.yaml<\/code><\/pre>\n<p>Now, run the following command to check the service, if an external IP is assigned.<\/p>\n<pre><code class=\"language-bash\">kubectl get svc<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-71.png\" class=\"kg-image\" alt=\"verifying if the service type loadbalancer is created\" loading=\"lazy\" width=\"2000\" height=\"500\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-71.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-71.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/08\/image-71.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-71.png 2040w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>If the external IP is assigned, try accessing it on the browser using the URL <code>localhost:31000<\/code>.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-70.png\" class=\"kg-image\" alt=\"accessing the nginx web page on the browser\" loading=\"lazy\" width=\"746\" height=\"385\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-70.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-70.png 746w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"expose-services-in-kind-cluster-using-ingress\">Expose Services in Kind Cluster using Ingress<\/h2>\n<p>To expose a service in a Kind cluster using <a href=\"https:\/\/devopscube.com\/kubernetes-ingress-tutorial\/\" rel=\"noreferrer\">Ingress<\/a>, first you should make sure that you have created the cluster with ports 80 and 443 inside the extraPortMappings.<\/p>\n<p> Install the <a href=\"https:\/\/devopscube.com\/setup-ingress-kubernetes-nginx-controller\/\" rel=\"noreferrer\">Ingress controller<\/a> by running the following commands.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f https:\/\/kind.sigs.k8s.io\/examples\/ingress\/deploy-ingress-nginx.yaml\n\nkubectl wait --namespace ingress-nginx \\\n  --for=condition=ready pod \\\n  --selector=app.kubernetes.io\/component=controller \\\n  --timeout=90s<\/code><\/pre>\n<p>Now, create a service type ClusterIP for the same Nginx application to map ingress to that service.<\/p>\n<p>Create a YAML file <code>service.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-svc\nspec:\n  type: ClusterIP\n  selector:\n    app: nginx-demo\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80<\/code><\/pre>\n<p>Then, run the following command to create the service.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f service.yaml<\/code><\/pre>\n<p>Now, create an Ingress object for the service.<\/p>\n<p>Create a YAML file <code>ingress.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: nginx-ingress\nspec:\n  ingressClassName: nginx\n  rules:\n  - http:\n      paths:\n      - path: \/\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-svc\n            port:\n              number: 80<\/code><\/pre>\n<p>Then, run the following command to create the Ingress object.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f ingress.yaml<\/code><\/pre>\n<p>Run the following command to check if the ingress is created.<\/p>\n<pre><code class=\"language-bash\">kubectl get ingress<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-80.png\" class=\"kg-image\" alt=\"verifying if the ingress object is created\" loading=\"lazy\" width=\"1414\" height=\"510\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-80.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-80.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-80.png 1414w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>To access it on the web, you need the Ingress controller IP. Run the following command to get the IP assigned to the Ingress controller by MetalLB.<\/p>\n<pre><code class=\"language-bash\">kubectl get svc -n ingress-nginx<\/code><\/pre>\n<p>You can see the IP as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-81.png\" class=\"kg-image\" alt=\"getting the external ip of the ingress controller service\" loading=\"lazy\" width=\"1045\" height=\"292\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-81.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-81.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-81.png 1045w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Use the IP on the browser, and you will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-71.png\" class=\"kg-image\" alt=\"accessing the nginx web page on the browser\" loading=\"lazy\" width=\"753\" height=\"352\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-71.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-71.png 753w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">This Ingress method only works for Linux systems, for Mac and Windows, follow the method below.<\/div>\n<\/div>\n<p>Unlike Linux, in Mac and Windows, the Docker containers runs in a Linux VM by the Docker desktop. Because of this, the container is not exposed to the host system, and you cannot access it using <a href=\"https:\/\/devopscube.com\/ip-address-tutorial\/\" rel=\"noreferrer\">IP<\/a>.<\/p>\n<p>So, we are going to create a temporary Docker container and use curl to check the exposed application.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\u26a0\ufe0f<\/div>\n<div class=\"kg-callout-text\">Update your Ingress controllers IP in below command and run it.<\/div>\n<\/div>\n<pre><code class=\"language-bash\">docker run --rm --network kind curlimages\/curl curl -i http:\/\/172.18.0.101<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-83.png\" class=\"kg-image\" alt=\"sending curl request to a pod in kind cluster in mac\" loading=\"lazy\" width=\"1992\" height=\"990\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/08\/image-83.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/08\/image-83.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/08\/image-83.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/08\/image-83.png 1992w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"using-persistent-volume-in-kind\">Using Persistent Volume in Kind<\/h2>\n<p>Now, let&#8217;s see how we can use Persistent Volume in Kind cluster.<\/p>\n<p>For PV, the Kind cluster uses the local-path-provisioner by default.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">Local path provisioner is a dynamic volume provisioner, which uses the local storage of each node for PVs.<\/div>\n<\/div>\n<p>Let&#8217;s deploy a MySQL StatefulSet with PVC.<\/p>\n<p>Create a YAML file <code>mysql.yaml<\/code> and copy the following content.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: apps\/v1\nkind: StatefulSet\nmetadata:\n  name: mysql\nspec:\n  serviceName: \"mysql\"\n  replicas: 1\n  selector:\n    matchLabels:\n      app: mysql\n  template:\n    metadata:\n      labels:\n        app: mysql\n    spec:\n      containers:\n      - name: mysql\n        image: mysql:latest\n        env:\n        - name: MYSQL_ROOT_PASSWORD\n          value: rootpass\n        ports:\n        - containerPort: 3306\n        volumeMounts:\n        - name: mysql-data\n          mountPath: \/var\/lib\/mysql\n  volumeClaimTemplates:\n  - metadata:\n      name: mysql-data\n    spec:\n      accessModes: [\"ReadWriteOnce\"]\n      resources:\n        requests:\n          storage: 2Gi<\/code><\/pre>\n<p>Run the following command to deploy the StatefulSet.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f mysql.yaml<\/code><\/pre>\n<p>Then, run the following command to check if the StatefulSet pod and volume are created.<\/p>\n<pre><code class=\"language-bash\">kubectl get po, pvc, pv<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-68.png\" class=\"kg-image\" alt=\"checking if the mysql pod and volumes are created\" loading=\"lazy\" width=\"916\" height=\"400\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-68.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-68.png 916w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"enabling-feature-gates-in-kind\">Enabling Feature Gates in Kind<\/h2>\n<p>To enable feature gates in the Kind cluster, you need to add the featureGates paramter with the required featureGates.<\/p>\n<p>For example, in the following cluster config, I have enabled the <strong><code>ImageVolume<\/code><\/strong> feature gate.<\/p>\n<pre><code class=\"language-yaml\">kind: Cluster\napiVersion: kind.x-k8s.io\/v1alpha4\nname: multi-node-cluster\nfeatureGates:\n  ImageVolume: true<\/code><\/pre>\n<p>This will be helpful for you to test new features of Kubernetes.<\/p>\n<p>For detailed information, refer to our blog about <a href=\"https:\/\/devopscube.com\/oci-image-volume-kubernetes-pods\/#testing-in-kind-cluster-optional\" rel=\"noreferrer\">Mounting OCI Image as Volume in Kubernetes Pods<\/a> where we have enabled the feature gate.<\/p>\n<h2 id=\"kind-cluster-management\">Kind Cluster Management<\/h2>\n<p>Below are some of the important commands that will be useful when using the Kind cluster.<\/p>\n<h3 id=\"load-image-into-kind\">Load Image into Kind<\/h3>\n<p>To use a container image you have built locally in a Kubernetes cluster, first you need to push it to an image registry. From there, it pulls the image during deployment.<\/p>\n<p>But in Kind, we can load container images directly, which removes the need for image registries.<\/p>\n<p>Below is the command to load images into Kind.<\/p>\n<pre><code class=\"language-bash\">kind load docker-image &lt;image:tag&gt; --name &lt;cluster-name&gt;<\/code><\/pre>\n<h3 id=\"export-logs\">Export Logs<\/h3>\n<p>Kind has a command that exports all available logs in the cluster to your desired location without exec into the container nodes.<\/p>\n<p>Below is the command to export logs from Kind.<\/p>\n<pre><code class=\"language-bash\">kind export logs --name &lt;cluster-name&gt; &lt;path-to-save&gt;<\/code><\/pre>\n<p>This exports all logs such as container logs, pod logs, kubelet logs, journal logs and even the container images in the cluster.<\/p>\n<h2 id=\"clean-up\">Clean up<\/h2>\n<p>If you no longer need the Kind cluster, run the following command.<\/p>\n<pre><code class=\"language-bash\">kind delete cluster --name multi-node-cluster<\/code><\/pre>\n<p>If your Kind cluster name is not <code>multi-node-cluster<\/code>, update the cluster&#8217;s name before running the above command.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>In this guide, you have learned about creating a Kind cluster and deploying an application, exposing the application using NodePort, LoadBalancer, and Ingress.<\/p>\n<p>Also, you have learned about mounting a persistent volume and enabling feature gates on Kind cluster.<\/p>\n<p>I hope you have found this guide a useful one for running test workload in a simple way using a Kind cluster.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/kubernetes-kind-cluster-tutorial-setup-and-deploy-apps\/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes Kind Cluster Tutorial: Setup, Deploy Apps, Ingress, LoadBalancer &amp; PVs \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/kubernetes-kind-cluster-tutorial-setup-and-deploy-apps\/<\/p>\n","protected":false},"author":1,"featured_media":469,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-468","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/468","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=468"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/468\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/469"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=468"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=468"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=468"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}