Tuesday, June 09, 2026

Calico on Hyperconverged Kubernetes

In a previous blog I showed how to install and configure a three node high available Kubernetes cluster. That cluster was using Flannel as the CNI for networking. While Flannel is a perfectly acceptable choice there are other alternatives to a CNI on Kubernetes. One of those options is Calico which is an open-source networking and network security solution for Kubernetes. It acts as a Container Network Interface (CNI) plug-in, assigning routable IP addresses to pods and enforcing secure communication rules (Network Policies) to dictate how containers interact with each other and the outside world. It provides the following key features and capabilities:

  • Network Policies: It provides granular "zero-trust" access controls, allowing administrators to limit traffic between pods, namespaces, and external endpoints.
  • High-Performance Data Planes: Calico supports multiple data planes including the standard Linux kernel (iptables), Windows HNS, and high-performance eBPF.
  • Advanced Networking: It can operate natively over Layer 3 using BGP (Border Gateway Protocol) routing for maximum performance without overlay network overhead. It also supports overlay networks (like VXLAN or IP-in-IP) if underlying infrastructure requires it.
  • Traffic Security: It supports automatic WireGuard encryption for in-cluster pod traffic to secure data on the wire.
  • Observability: Provides deep insights into workload traffic and metrics so developers can quickly identify and troubleshoot network issues.

Calico is popular because it scales well, operates on virtually any Kubernetes platform and is engineered for low CPU consumption. It replaces or enhances the default Kubernetes networking stack, offering a unified platform for both connection management and security.

Calico sounds like a great CNI option so let's get started by installing it on our Kubernetes cluster. First let's confirm flannel is no longer running on our cluster by looking at the currently running pods.

$ kubectl get pods -n kube-flannel No resources found in kube-flannel namespace.

Calico can be installed via Helm or yaml manifest. I always prefer yaml manifest so the following steps will show that method. Now let's determining the version of Calico to use by visiting the official releases (here)[https://github.com/projectcalico/calico/releases]. As of this writing the latest version is v3.32.0 and that is what we will use.

We can curl the v3.32.0 release for the following manifests:

$ curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/v1_crd_projectcalico_org.yaml % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2.90M 100 2.90M 0 0 6.65M 0 0 $ curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/tigera-operator.yaml % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 19285 100 19285 0 0 247.7k 0 0 $ curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/custom-resources.yaml % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1046 100 1046 0 0 11334 0 0

The manifest we pull down will work as is for the deployment however we need to make one change to the custom-resources.yaml to ensure the CIDR block identified in the ipPools reflects that of our pod network we defined when we created our Kubernetes cluster (10.244.0.0/16). We can use sed to make that inline change.

$ sed -i "s|cidr: 192.168.0.0/16|cidr: 10.244.0.0/16|" custom-resources.yaml $ cat custom-resources.yaml |grep cidr cidr: 10.244.0.0/16

Now that we have adjusted the manifests we are now ready to deploy Calico on the cluster. We will start by creating the v1_crd_projectcalico_org.yaml on the cluster.

$ kubectl create -f v1_crd_projectcalico_org.yaml customresourcedefinition.apiextensions.k8s.io/apiservers.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/gatewayapis.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/goldmanes.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/imagesets.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/installations.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/istios.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/managementclusterconnections.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/tigerastatuses.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/whiskers.operator.tigera.io created customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/bgpfilters.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created Warning: unrecognized format "cidr" customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/stagedglobalnetworkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/stagedkubernetesnetworkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/stagednetworkpolicies.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/tiers.crd.projectcalico.org created customresourcedefinition.apiextensions.k8s.io/clusternetworkpolicies.policy.networking.k8s.io created

Next we can deploy the Tigera operator.

$ kubectl create -f tigera-operator.yaml namespace/tigera-operator created serviceaccount/tigera-operator created clusterrole.rbac.authorization.k8s.io/tigera-operator-secrets created clusterrole.rbac.authorization.k8s.io/tigera-operator created clusterrolebinding.rbac.authorization.k8s.io/tigera-operator created rolebinding.rbac.authorization.k8s.io/tigera-operator-secrets created deployment.apps/tigera-operator created

And finally we can deploy the custom-resources manifest.

$ kubectl create -f custom-resources.yaml installation.operator.tigera.io/default created apiserver.operator.tigera.io/default created goldmane.operator.tigera.io/default created whisker.operator.tigera.io/default created

We can observe the state of the deployment with the following command.

$ kubectl get tigerastatus -w NAME AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE apiserver True False False 0s All objects available calico False False True 20s Pod calico-system/calico-node-crn9x is running but not ready goldmane False False True 20s Error creating / updating resource: StorageError: key not found, Code: 1, Key: projectcalico.org/networkpolicies/calico-system/calico-system.goldmane, ResourceVersion: 0, AdditionalErrorMsg: <nil> ippools True False False 21s All objects available tiers True Waiting for Tigera API server to be ready whisker False False True 20s Error creating / updating resource: StorageError: key not found, Code: 1, Key: projectcalico.org/networkpolicies/calico-system/calico-system.whisker, ResourceVersion: 0, AdditionalErrorMsg: <nil>

Once the deployment completes we should see that all the Tigera components are now available.

$ kubectl get tigerastatus NAME AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE apiserver True False False 85s All objects available calico True False False 70s All objects available goldmane True False False 50s All objects available ippools True False False 106s All objects available tiers True False False 80s All objects available whisker True False False 70s All objects available

Further we can confirm all the pods are running as well in both the calico-system and tiger-operator namespace.

$ kubectl get pods -A| egrep "calico-system|tigera-operator" calico-system calico-apiserver-6ccd896b97-nvgvs 1/1 Running 0 5m25s calico-system calico-apiserver-6ccd896b97-prpn8 1/1 Running 0 5m25s calico-system calico-kube-controllers-687c745c79-wdgqm 1/1 Running 0 5m24s calico-system calico-node-bdxb9 1/1 Running 0 5m24s calico-system calico-node-crn9x 1/1 Running 0 5m24s calico-system calico-node-f6hrg 1/1 Running 0 5m24s calico-system calico-typha-56dffbdfbf-hjtkx 1/1 Running 0 5m24s calico-system calico-typha-56dffbdfbf-xjsbr 1/1 Running 0 5m16s calico-system csi-node-driver-b4vlk 2/2 Running 0 5m24s calico-system csi-node-driver-bzr2k 2/2 Running 0 5m24s calico-system csi-node-driver-lcq5r 2/2 Running 0 5m24s calico-system goldmane-6885dcb7d-b4jrv 1/1 Running 0 5m25s calico-system whisker-7c95549979-cjhh8 2/2 Running 0 5m16s tigera-operator tigera-operator-85dbff4478-dmmsr 1/1 Running 0 5m54s

At this point we have a complete install of Calico on our Kubernetes cluster but we also needs ingress access for our applications. In Calico the ingress is called a GatewayApi and we can create one by using the following manifest.

$ cat <<EOF >gatewayapi.yaml apiVersion: operator.tigera.io/v1 kind: GatewayAPI metadata: name: default EOF

Once we have the manifest we can create it on the cluster.

$ kubectl create -f gatewayapi.yaml gatewayapi.operator.tigera.io/default created

We can see from the status that we now have the GatewayApi available.

$ kubectl get tigerastatus NAME AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE apiserver True False False 20h All objects available calico True False False 20h All objects available gatewayapi True False False 7s All objects available goldmane True False False 20h All objects available ippools True False False 20h All objects available tiers True False False 20h All objects available whisker True False False 20h All objects available

We can also see additional GatewayApi resources are now at our disposal.

$ kubectl api-resources | grep gateway.networking.k8s.io backendtlspolicies btlspolicy gateway.networking.k8s.io/v1 true BackendTLSPolicy gatewayclasses gc gateway.networking.k8s.io/v1 false GatewayClass gateways gtw gateway.networking.k8s.io/v1 true Gateway grpcroutes gateway.networking.k8s.io/v1 true GRPCRoute httproutes gateway.networking.k8s.io/v1 true HTTPRoute referencegrants refgrant gateway.networking.k8s.io/v1beta1 true ReferenceGrant tcproutes gateway.networking.k8s.io/v1alpha2 true TCPRoute tlsroutes gateway.networking.k8s.io/v1alpha3 true TLSRoute udproutes gateway.networking.k8s.io/v1alpha2 true UDPRoute

Now let's put those apis to work for us. The first thing we need to do is create a gateway. In Calico, Gateway API is the standard used to manage traffic rather than legacy Kubernetes Ingress. It utilizes the Gateway custom resource to configure load balancing and routing, backed by an Envoy proxy managed by the Tigera Operator. Below is an example of a Gateway that will allow web traffic on port 8080.

$ cat <<EOF >http-gateway.yaml apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: http-deployment-gateway spec: gatewayClassName: tigera-gateway-class listeners: - name: http protocol: HTTP port: 8080 EOF

Let's create our example on our cluster.

$ kubectl create -f http-gateway.yaml gateway.gateway.networking.k8s.io/http-deployment-gateway created

Now let's create a new namespace where we will run a web server we will call it http-deployment-gateway to match that of the gateway we just created.

$ kubectl create namespace http-deployment-gateway namespace/http-deployment-gateway created

Next let's define a manifest that does three things, creates the content the web server will deliver, the deployment for the web server which will mount up our Configmap content and finally a service. Notice the web server will run on port 8080, the service will reference 8080 and the content we are serving up is a basic "Hello Calico!" html file.

$ cat <<EOF >http-deployment.yaml apiVersion: v1 kind: ConfigMap metadata: name: app-v1-html namespace: http-deployment-gateway data: index.html: | <html><body><h1>Hello Calico!</h1></body></html> --- apiVersion: apps/v1 kind: Deployment metadata: name: app-v1 namespace: http-deployment-gateway spec: replicas: 1 selector: matchLabels: app: http-deployment-gateway version: v1 template: metadata: labels: app: http-deployment-gateway version: v1 spec: containers: - name: busybox image: busybox:latest command: ["sh", "-c", "httpd -f -p 8080 -h /var/www/html"] ports: - containerPort: 8080 volumeMounts: - name: html mountPath: /var/www/html volumes: - name: html configMap: name: app-v1-html --- apiVersion: v1 kind: Service metadata: name: app-v1 namespace: http-deployment-gateway spec: selector: app: http-deployment-gateway version: v1 ports: - port: 8080 targetPort: 8080 EOF

One we have the manifest let's create it on the cluster which created three objects.

$ kubectl create -f http-deployment.yaml configmap/app-v1-html created deployment.apps/app-v1 created service/app-v1 created

We can confirm the pod is running by listing the pods in the http-depoyment-gateway namespace.

$ kubectl get pods -n http-deployment-gateway NAME READY STATUS RESTARTS AGE app-v1-6d98784d66-8486g 1/1 Running 0 15s

Now that we have our web server running we need to provide access to it. In Calico, the HTTPRoute kind is a standard Kubernetes Gateway API resource used to define Layer 7 routing rules for incoming HTTP and HTTPS traffic. It is used alongside the Calico Ingress Gateway (which is built on Envoy Gateway) to manage advanced traffic routing, like path-based matching, traffic splitting, and redirects. The below manifest is an example for web server running on port 8080. Here we are just simply redirecting all traffic to the backend application.

$ cat <<EOF >http-route-gateway.yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: http-route-gateway spec: parentRefs: - name: http-deployment-gateway namespace: default rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: app-v1 namespace: http-deployment-gateway port: 8080 weight: 100 EOF

Once we have the manifest let's create it on the cluster.

$ kubectl create -f http-route-gateway.yaml httproute.gateway.networking.k8s.io/http-route-gateway created

Now we need to generate a ReferenceGrant. In Calico, a ReferenceGrant is a native Kubernetes Gateway API resource (gateway.networking.k8s.io). It acts as a security safeguard that explicitly permits cross-namespace references. It is primarily used with the Calico Ingress Gateway. Below is an example ReferenceGrant to allow traffic into our service.

$ cat <<EOF >http-reference-grant.yaml apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: name: http-deployment-gateway namespace: http-deployment-gateway spec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: default to: - group: "" kind: Service

Once we have the manifest we can create it on the cluster.

$ kubectl create -f http-reference-grant.yaml referencegrant.gateway.networking.k8s.io/http-deployment-gateway created

At this point we are ready to test out everything we have pieced together above. First we need to obtain the svc name and assign it to a variable.

$ HTTPGATEWAY=$(kubectl get svc -n tigera-gateway -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n'|grep http) $ echo $HTTPGATEWAY envoy-default-http-deployment-gateway-f4240cdd

Now we can open up in a terminal with cluster access and run the following port-forward command.

$ kubectl port-forward -n tigera-gateway svc/$HTTPGATEWAY 30135:8080 Forwarding from 127.0.0.1:30135 -> 8080 Forwarding from [::1]:30135 -> 8080

In a secondary terminal on the same host where we ran the port-forward command we can curl the localhost url on port 30135. The results should be the web servers html payload we configured in the configmap above.

$ curl http://localhost:30135/ <html><body><h1>Hello Calico!</h1></body></html>

At this point I feel pretty confident my Calico environment is working and again this is not anything bleeding edge but just good review for us in understanding how Calico works in Kubernetes.