Right, so you’ve got your pristine, isolated namespaces. Your dev team can’t accidentally kubectl delete the prod database. Everyone’s happy in their little sandboxes. Until, of course, they aren’t. Because at some point, dev needs to test its new fancy service against the prod database, or the logging namespace needs to collect metrics from everywhere. This is where we break the glass and carefully, deliberately, poke holes in our beautiful isolation. Welcome to cross-namespace communication.

The golden rule, the one thing you must burn into your cortex, is this: by default, nothing can talk across namespaces. Kubernetes assumes you want a fortress, not an open-plan office. To get traffic from Pod A in namespace blue to Service B in namespace green, you have to be explicit. The most common and elegant way to do this is by using the Kubernetes DNS service discovery system. It’s smarter than you might think.

The Fully Qualified Domain Name (FQDN) is Your Passport

Every Service you create gets a DNS entry. But it’s not just a name; it’s a full address. Think of it like email. You can just say “Hey, Bob!” if you’re in the same office (namespace). But to reach Bob in another company (namespace), you need his full email address. Kubernetes DNS works the same way.

The magic formula for this email address is: <service-name>.<namespace-name>.svc.cluster.local

Let’s say you have a PostgreSQL Service named postgres-primary in the prod-data namespace. A Pod in the same namespace can simply connect to postgres-primary on port 5432. It’s local. But a Pod in the dev namespace needs to use the full address: postgres-primary.prod-data.svc.cluster.local.

Here’s what that looks like in practice. A dev application would use this connection string:

# pod.yaml (in the 'dev' namespace)
apiVersion: v1
kind: Pod
metadata:
  name: dev-app
  namespace: dev
spec:
  containers:
  - name: app
    image: my-dev-app:latest
    env:
    - name: DATABASE_URL
      value: "postgresql://user:pass@postgres-primary.prod-data.svc.cluster.local:5432/mydb" # The FQDN is the key!

This works right now without any additional configuration. It’s the simplest form of cross-namespace communication. The cluster.local part is the default cluster domain, but it could be different in your setup (cluster.local is to Kubernetes as .com is to the internet).

When FQDNs Aren’t Enough: The Need for Network Policies

Ah, but here’s the catch everyone misses: DNS resolution is not the same as network access. Just because you can look up the IP address of a Service in another namespace doesn’t mean your packet is allowed to get there. This is the number one “it worked on my machine” (where I had no policies) moment.

If you have a Kubernetes network plugin like Calico or Cilium installed, you can define Network Policies. These are firewall rules for your cluster. The default behavior is usually “allow all traffic,” which is why the FQDN trick works immediately in most test clusters. But in a secure, multi-tenant environment, the default is “deny all.”

So, if your dev pod can’t reach the prod-data postgres, even with the FQDN, you need to check the NetworkPolicy. Someone (rightly) probably locked down the prod-data namespace. To allow this traffic, you’d need a policy that explicitly allows ingress from the dev namespace.

# networkpolicy.yaml (in the 'prod-data' namespace)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dev-to-postgres
  namespace: prod-data
spec:
  podSelector:
    matchLabels:
      app: postgres-primary # Selects the specific Postgres pod(s)
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: dev # Allows traffic from the 'dev' namespace
    ports:
    - protocol: TCP
      port: 5432 # Only on the Postgres port

This policy is a bouncer for your prod-data namespace. It says, “Nothing gets in… except traffic from the dev namespace on port 5432.” It’s crucial infrastructure.

The Service Mesh Wild Card

Now, if you really want to get fancy and hate yourself a little (I’m kidding, mostly), you introduce a service mesh like Linkerd or Istio. These tools take cross-namespace communication, sprinkle it with cryptographic fairy dust, and add a layer of observability and control that would make a NASA engineer blush.

With a service mesh, communication often happens through a sidecar proxy. You can define traffic-splitting rules, canary deployments, and mutual TLS (mTLS) between namespaces with incredible granularity. For example, an Istio VirtualService can route traffic from dev to a specific canary version of a service in prod.

# virtualservice.yaml (Istio CRD)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: prod-service-route
  namespace: dev # Note: This is in the *source* namespace in this example
spec:
  hosts:
  - "prodsvc.prod.svc.cluster.local" # The target service
  http:
  - route:
    - destination:
        host: prodsvc.prod.svc.cluster.local
        subset: v1-canary # This directs traffic to a specific subset of pods in 'prod'
      weight: 10 # Send 10% of traffic to the canary
    - destination:
        host: prodsvc.prod.svc.cluster.local
        subset: v1-stable
      weight: 90 # Send 90% to the stable version

This is powerful, but it’s also a whole new level of complexity. It’s the difference of using a simple socket wrench versus a fully automated robotic arm. You need to know when the robotic arm is justified.

The bottom line? Start with the FQDN. It solves 90% of use cases. Then, layer on Network Policies for security. Only when you have a genuine, painful need for fine-grained traffic control, circuit breaking, or automatic mTLS should you invite the beautiful, complex beast that is a service mesh to the party.