{"id":890,"date":"2025-11-21T06:33:51","date_gmt":"2025-11-20T22:33:51","guid":{"rendered":"https:\/\/vinta.ws\/code\/?p=890"},"modified":"2026-02-18T01:20:34","modified_gmt":"2026-02-17T17:20:34","slug":"stop-paying-for-kubernetes-load-balancers-use-cloudflare-tunnel-instead","status":"publish","type":"post","link":"https:\/\/vinta.ws\/code\/stop-paying-for-kubernetes-load-balancers-use-cloudflare-tunnel-instead.html","title":{"rendered":"Stop Paying for Kubernetes Load Balancers: Use Cloudflare Tunnel Instead"},"content":{"rendered":"<p>To expose services in a Kubernetes cluster, you typically need an Ingress backed by a cloud provider's load balancer, and often a NAT Gateway. For small projects, these costs add up fast (though someone may argue small projects shouldn't use Kubernetes at all).<\/p>\n<p>What if you could ditch the Ingress, Load Balancer, and Public IP entirely? Enter <strong>Cloudflare Tunnel<\/strong> (by the way, it costs <strong>$0<\/strong>).<\/p>\n<h2>How Cloudflare Tunnel Works<\/h2>\n<p>Cloudflare Tunnel relies on a lightweight daemon called <code>cloudflared<\/code> that runs within your cluster to establish secure, persistent outbound connections to Cloudflare's global network (edge servers). <strong>Instead of accepting incoming connections, your server runs cloudflared to dial out and establish a secure tunnel with Cloudflare.<\/strong> Then it creates a bidirectional tunnel that allows Cloudflare to route requests to your private services while blocking all direct inbound access to your origin servers.<\/p>\n<p>So basically Cloudflare Tunnel acts as a <strong>reverse proxy<\/strong> that routes traffic from Cloudflare edge servers to your private services: Internet -&gt; Cloudflare Edge Server -&gt; Tunnel -&gt; <code>cloudflared<\/code> -&gt; Service -&gt; Pod.<\/p>\n<p>ref:<br \/>\n<a href=\"https:\/\/developers.cloudflare.com\/cloudflare-one\/networks\/connectors\/cloudflare-tunnel\/\">https:\/\/developers.cloudflare.com\/cloudflare-one\/networks\/connectors\/cloudflare-tunnel\/<\/a><\/p>\n<h2>Create a Tunnel<\/h2>\n<p>A tunnel links your origin to Cloudflare's global network. It is a logical connection that enables secure, persistent outbound connections to Cloudflare's global network (Cloudflare Edge Servers).<\/p>\n<ul>\n<li>Go to <a href=\"https:\/\/one.dash.cloudflare.com\/\">https:\/\/one.dash.cloudflare.com\/<\/a> -&gt; Networks -&gt; Connectors -&gt; Create a tunnel -&gt; Select cloudflared<\/li>\n<li>Tunnel name: <code>your-tunnel-name<\/code><\/li>\n<li>Choose an operating system: <code>Docker<\/code><\/li>\n<\/ul>\n<p>Instead of running any installation command, simply copy the token (starts with <code>eyJ...<\/code>). We will use it later.<\/p>\n<p>ref:<br \/>\n<a href=\"https:\/\/developers.cloudflare.com\/cloudflare-one\/connections\/connect-networks\/deployment-guides\/kubernetes\/\">https:\/\/developers.cloudflare.com\/cloudflare-one\/connections\/connect-networks\/deployment-guides\/kubernetes\/<\/a><\/p>\n<h2>Configure Published Application Routes<\/h2>\n<p>First of all, <strong>make sure you host your domains on Cloudflare<\/strong>, so the following setup can update your domain's DNS records automatically.<\/p>\n<p>Assume you have the following Services in your Kubernetes cluster:<\/p>\n<pre class=\"line-numbers\"><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: my-blog\nspec:\n  selector:\n    app: my-blog\n  type: NodePort\n  ports:\n    - name: http\n      port: 80\n      targetPort: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\nspec:\n  selector:\n    app: frontend\n  type: NodePort\n  ports:\n    - name: http\n      port: 80\n      targetPort: http<\/code><\/pre>\n<p>You need to configure your published application routes based on your Services, for instance:<\/p>\n<ul>\n<li>Route 1:\n<ul>\n<li>Domain: <code>example.com<\/code><\/li>\n<li>Path: <code>blog<\/code><\/li>\n<li>Type: <code>HTTP<\/code><\/li>\n<li>URL: <code>my-blog.default:80<\/code> =&gt; format: <code>your-service.your-namespace:your-service-port<\/code><\/li>\n<\/ul>\n<\/li>\n<li>Route 2:\n<ul>\n<li>Domain: <code>example.com<\/code><\/li>\n<li>Path: <code>(leave it blank)<\/code><\/li>\n<li>Type: <code>HTTP<\/code><\/li>\n<li>URL: <code>frontend.default:80<\/code> =&gt; format: <code>your-service.your-namespace:your-service-port<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>Deploy <code>cloudflared<\/code> to Kubernetes<\/h2>\n<p>We will deploy <code>cloudflared<\/code> as a Deployment in Kubernetes. It acts as a connector that routes traffic from Cloudflare's global network directly to your private services. You don't need to expose any of your services to the public Internet.<\/p>\n<pre class=\"line-numbers\"><code class=\"language-yaml\">apiVersion: v1\nkind: Secret\nmetadata:\n  name: cloudflared-tunnel-token\nstringData:\n  token: YOUR_TUNNEL_TOKEN\n---\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: tunnel\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: tunnel\n  template:\n    metadata:\n      labels:\n        app: tunnel\n    spec:\n      terminationGracePeriodSeconds: 25\n      nodeSelector:\n        cloud.google.com\/compute-class: \"autopilot-spot\"\n      securityContext:\n        sysctls:\n          # Allows ICMP traffic (ping, traceroute) to resources behind cloudflared\n          - name: net.ipv4.ping_group_range\n            value: \"65532 65532\"\n      containers:\n        - name: cloudflared\n          image: cloudflare\/cloudflared:latest\n          command:\n            - cloudflared\n            - tunnel\n            - --no-autoupdate\n            - --loglevel\n            - debug\n            - --metrics\n            - 0.0.0.0:2000\n            - run\n          env:\n            - name: TUNNEL_TOKEN\n              valueFrom:\n                secretKeyRef:\n                  name: cloudflared-tunnel-token\n                  key: token\n          livenessProbe:\n            httpGet:\n              # Cloudflared has a \/ready endpoint which returns 200 if and only if it has an active connection to Cloudflare's network\n              path: \/ready\n              port: 2000\n            failureThreshold: 1\n            initialDelaySeconds: 10\n            periodSeconds: 10\n          resources:\n            requests:\n              cpu: 50m\n              memory: 128Mi\n            limits:\n              cpu: 200m\n              memory: 256Mi<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"https:\/\/developers.cloudflare.com\/cloudflare-one\/networks\/connectors\/cloudflare-tunnel\/configure-tunnels\/cloudflared-parameters\/run-parameters\/\">https:\/\/developers.cloudflare.com\/cloudflare-one\/networks\/connectors\/cloudflare-tunnel\/configure-tunnels\/cloudflared-parameters\/run-parameters\/<\/a><\/p>\n<pre class=\"line-numbers\"><code class=\"language-bash\">kubectl apply -f cloudflared\/deployment.yml<\/code><\/pre>\n<p>That's it! Check the Cloudflare dashboard, and you should see your tunnel status as <strong>HEALTHY<\/strong>.<\/p>\n<p>You can now safely delete your Ingress and the underlying load balancer. You don't need them anymore. Enjoy your secure, cost-effective cluster!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What if you could ditch the Ingress, Load Balancer, and Public IP entirely? Enter Cloudflare Tunnel. It costs $0.<\/p>\n","protected":false},"author":1,"featured_media":891,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38],"tags":[16,151,114,123],"class_list":["post-890","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-about-devops","tag-amazon-web-services","tag-cloudflare","tag-google-cloud-platform","tag-kubernetes"],"_links":{"self":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts\/890","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/comments?post=890"}],"version-history":[{"count":0,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts\/890\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/media\/891"}],"wp:attachment":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/media?parent=890"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/categories?post=890"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/tags?post=890"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}