Building a Kubernetes Cluster on Google Cloud Platform with kubeadm

Ever wondered how Kubernetes clusters actually work under the hood? In this guide, I'll walk you through building your own Kubernetes cluster on Google Cloud Platform from scratch using kubeadm. This hands-on experience will give you a deeper understanding of Kubernetes architecture and help you appreciate what managed services like GKE do behind the scenes.
Whether you're preparing for a certification, building your homelab, or just curious about container orchestration, this step-by-step tutorial has got you covered!
What You'll Need
Don't worry—the setup is straightforward! Here's what you'll need before we begin:
- Google Cloud Account: You'll need an active GCP account with billing enabled. If you're new to GCP, they offer free credits to get started!
- gcloud CLI: The Google Cloud SDK on your local machine (get it here)
- Quick setup:
gcloud auth loginto authenticate
- Quick setup:
- Appropriate Permissions: Your GCP account should be able to:
- Create and manage projects
- Spin up Compute Engine instances
- Configure VPC networks and firewall rules
- Some Familiarity With:
- Basic Linux commands
- SSH connections
- Kubernetes fundamentals (though we'll explain as we go!)
Let's Build This Thing
Ready to get your hands dirty? We'll build this cluster step by step, starting with the infrastructure and working our way up to a fully functioning cluster. I'll explain what each command does and why it matters.
Step 1: Setting Up Your GCP Project
First things first—let's create a dedicated GCP project for our cluster and make it the active project. Run these commands on your local machine:
gcloud projects create my-k8s-cluster
gcloud config set project my-k8s-clusterStep 2: Crafting Your Network Infrastructure
Set up a custom VPC network with regional BGP routing and create a subnet in the us-central1 region. Run these commands on your local machine:
gcloud compute networks create k8s-vpc \
--subnet-mode=custom \
--bgp-routing-mode=regional
gcloud compute networks subnets create k8s-subnet \
--network=k8s-vpc \
--range=10.0.0.0/24 \
--region=us-central1Step 3: Locking Down Security with Firewall Rules
Configure firewall rules to enable SSH access from anywhere and allow all internal communication between cluster nodes. Run these commands on your local machine:
gcloud compute firewall-rules create k8s-allow-ssh \
--network=k8s-vpc \
--allow=tcp:22 \
--source-ranges=0.0.0.0/0
gcloud compute firewall-rules create k8s-allow-internal \
--network=k8s-vpc \
--allow=tcp,udp,icmpStep 4: Spinning Up Your Control Plane Node
Provision an e2-medium VM instance running Ubuntu 22.04 LTS to serve as the cluster's control plane. Run this command on your local machine:
gcloud compute instances create k8s-control-plane \
--machine-type=e2-medium \
--subnet=k8s-subnet \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--zone=us-central1-aStep 5: SSH Into Your Control Plane
Connect to the control plane VM using the gcloud CLI. Run this command on your local machine:
gcloud compute ssh k8s-control-plane --zone=us-central1-aStep 6: Preparing the Control Plane Environment
Installing containerd on Ubuntu for a Kubernetes cluster requires a few specific OS-level tweaks before the actual installation to ensure networking and resource management (cgroups) work correctly.
Here is the step-by-step guide to getting it running. All commands in this section run on the control plane node.
System Configuration Prerequisites
Kubernetes requires specific kernel modules and network settings to allow the container runtime to handle bridged traffic.
Load Kernel Modules
Create a configuration file to load the necessary modules on boot (on control plane):
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilterConfigure Sysctl
Enable IP forwarding and bridge networking. The sysctl command applies these changes immediately without requiring a reboot (on control plane):
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --systemInstalling containerd
You can install containerd directly from the official Ubuntu repositories (on control plane):
sudo apt update
sudo apt install -y containerdConfiguring containerd for Kubernetes
This is the most critical step. By default, containerd does not use the systemd cgroup driver, which Kubernetes strongly recommends for stability.
Generate Default Config
Create the directory and generate a clean configuration file (on control plane):
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/nullEnable Systemd Cgroup Driver
You need to change SystemdCgroup = false to true. You can do this manually with an editor or use this sed command (on control plane):
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.tomlRestart containerd
Apply the new configuration (on control plane):
sudo systemctl restart containerd
sudo systemctl enable containerdInstalling kubeadm, kubelet, and kubectl
Add the Kubernetes package repository and install the necessary components. Note that the directory /etc/apt/keyrings is typically already present on Ubuntu 22.04 systems. The packages are marked as held to prevent automatic updates (on control plane):
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubeletStep 7: Bootstrapping the Kubernetes Control Plane
Bootstrap the Kubernetes control plane with the specified pod network CIDR range (on control plane):
sudo kubeadm init --pod-network-cidr=10.244.0.0/16Once initialization completes successfully, configure kubectl access for your user account (on control plane):
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/configThe initialization will output a join token for adding worker nodes. While you can save it, generating a fresh token later is straightforward.
Deploying Your Pod Network Plugin
Deploy Flannel as the CNI (Container Network Interface) plugin to enable pod-to-pod communication throughout the cluster (on control plane):
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.ymlSupercharging Your kubectl Experience
Enable kubectl command auto-completion for the current session and persist it for future sessions. The first command activates it immediately, while the second adds it to your bash profile (on control plane):
source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> ~/.bashrcOptionally, create a shorthand alias k for kubectl with the same completion behavior (on control plane):
alias k=kubectl
complete -o default -F __start_kubectl kStep 8: Bringing Worker Nodes Into the Mix
With the control plane ready, provision an additional VM instance that will function as a worker node for running workloads. Run this command on your local machine:
gcloud compute instances create k8s-worker-1 \
--machine-type=e2-medium \
--subnet=k8s-subnet \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--zone=us-central1-aStep 9: SSH Into Your Worker Node
Connect to the worker node VM using the gcloud CLI. Run this command on your local machine:
gcloud compute ssh k8s-worker-1 --zone=us-central1-aStep 10: Preparing the Worker Node Environment
All commands in this section run on the worker node. Repeat all the installation steps from Step 6 on the worker node: System Configuration Prerequisites, Installing containerd, Configuring containerd for Kubernetes, and Installing kubeadm, kubelet, and kubectl.
Step 11: Joining Worker to Control Plane
Generate a fresh join command from the control plane (on control plane):
kubeadm token create --print-join-commandExecute the output command on the worker node to register it with the cluster (on worker node).
Step 12: Taking Your Cluster for a Test Drive
Now that your cluster is fully operational, verify everything is working by deploying a simple nginx web server. All kubectl commands run on the control plane.
Creating and Running an Nginx Pod
Deploy an nginx pod to your cluster (on control plane):
kubectl run nginx-test --image=nginx:latest --port=80Verifying the Pod is Running
Check the status of your pod (on control plane):
kubectl get podsYou should see output similar to:
NAME READY STATUS RESTARTS AGE
nginx-test 1/1 Running 0 30sGetting Detailed Pod Information
View more details about the running pod (on control plane):
kubectl describe pod nginx-testExposing the Pod to the World
If you want to access nginx from within the cluster, create a service (on control plane):
kubectl expose pod nginx-test --type=NodePort --port=80Check the service and assigned port (on control plane):
kubectl get services nginx-testAccessing Nginx from Your Computer
To access the nginx server from your local machine, you'll need to open the NodePort in GCP's firewall and get the external IP of one of your nodes.
First, create a firewall rule to allow traffic on NodePort range (30000-32767), run this on your local machine:
gcloud compute firewall-rules create k8s-allow-nodeport \
--network=k8s-vpc \
--allow=tcp:30000-32767 \
--source-ranges=0.0.0.0/0Get the external IP address of your worker node or control plane (on your local machine):
gcloud compute instances list --filter="name~'k8s-'"Note the EXTERNAL_IP from the output. Now get the NodePort assigned to your service (on control plane):
kubectl get service nginx-test -o jsonpath='{.spec.ports[0].nodePort}'This will output a port number between 30000-32767. Access nginx by opening your browser to:
http://EXTERNAL_IP:NODEPORTFor example, if your external IP is 34.123.45.67 and NodePort is 31234, visit:
http://34.123.45.67:31234You should see the "Welcome to nginx!" page.
Cleaning Up the Test Resources
Remove the nginx pod and service when you're done testing (on control plane):
kubectl delete service nginx-test
kubectl delete pod nginx-testIf you created the NodePort firewall rule, remove it as well (on your local machine):
gcloud compute firewall-rules delete k8s-allow-nodeportStep 13: Tearing Down Your Cluster
Tear down all created resources in reverse order. Start with the VMs, then remove firewall rules, subnet, and VPC. Run these commands on your local machine:
gcloud compute instances delete k8s-control-plane k8s-worker-1 \
--zone=us-central1-a
gcloud compute firewall-rules delete k8s-allow-ssh k8s-allow-internal
gcloud compute networks subnets delete k8s-subnet \
--region=us-central1
gcloud compute networks delete k8s-vpcIf you want to remove the entire project and all its contents, run:
gcloud projects delete my-k8s-clusterA Word About Production Readiness
Before we wrap up, let's have an honest conversation: this cluster is perfect for learning and testing, but it's not production-ready. Think of this as your training ground—a safe space to experiment, break things, and learn!
Final Thoughts
Congratulations on building your own Kubernetes cluster! You've gained hands-on experience with container orchestration that many developers never get. Use this setup to experiment, test your applications, and deepen your understanding of how Kubernetes really works.
When you're ready for production, you'll appreciate managed services even more—but you'll also understand what's happening behind the scenes. And that knowledge? Invaluable.
Happy clustering! Feel free to reach out if you have questions or want to share your experience. Let's keep learning together!