<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Blogs on Stano Bočinec | juniq</title>
        <link>/blog/</link>
        <description>Recent content in Blogs on Stano Bočinec | juniq</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Sat, 26 Aug 2023 13:12:54 +0200</lastBuildDate>
        <atom:link href="/blog/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Finer Kubernetes pod scheduling with topology spread constraints</title>
            <link>/blog/kubernetes-pod-scheduling/</link>
            <pubDate>Wed, 17 Nov 2021 21:01:26 +0100</pubDate>
            
            <guid>/blog/kubernetes-pod-scheduling/</guid>
            <description>Reviewing a PR to upgrade ingress-nginx my brain got caught on thinking more about Kubernetes pod scheduling. Curiosity to find the optimal solution to meet company&amp;rsquo;s resiliency requirements for the ingress controller led me to discover a fresh new K8s feature - Pod Topology Spread Constraints. In this blog post, I&amp;rsquo;m going to show you an example of how to fine tune Kubernetes scheduling using the constraints to spread your workload in a more resilient way.</description>
            <content type="html"><![CDATA[<p>Reviewing a PR to upgrade <a href="https://github.com/kubernetes/ingress-nginx/">ingress-nginx</a> my brain got caught on thinking more about Kubernetes pod scheduling. Curiosity to find the optimal solution to meet company&rsquo;s resiliency requirements for the ingress controller led me to discover a fresh new K8s feature - <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/">Pod Topology Spread Constraints</a>. In this blog post, I&rsquo;m going to show you an example of how to fine tune Kubernetes scheduling using the constraints to spread your workload in a more resilient way.</p>
<h1 id="default-scheduling">Default scheduling</h1>
<p>I run a low-traffic beta workload as Kubernetes deployment with 2 replicas waiting in a task backlog to be optimized before going GA. The deployment with low resource requests and limits runs in a GKE cluster with 10+ nodes in a single node pool in 2 availability zones. To make it more resilient, I&rsquo;d like to have every pod replica scheduled on a different K8s node and in a different availability zone. The Kubernetes scheduler initially schedules resources well, even without additional configuration. Though combined with other external factors (other workloads, node scale up/down events, preemptible node restarts, node-pool upgrades, &hellip;), pod replicas sometimes end up scheduled on the same node in a single availability zone. What&rsquo;s indeed not what I want for my workload to be resilient.</p>
<p>Let&rsquo;s go straight to the example. Let&rsquo;s have a GKE cluster with 2 nodes in 2 availability zones (4 nodes total) and our workload defined as a deployment with 2 replicas:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">edu</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">revisionHistoryLimit</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dummy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">image</span>: <span style="color:#ae81ff">k8s.gcr.io/pause:3.2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">50m</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">50Mi</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">500m</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">500Mi</span>
</span></span></code></pre></div><p>Create a namespace, apply the manifest, check the pods and, depending on the star constellation, you might get (un)lucky to have all replicas scheduled on the same node:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>$ kubectl get pod -n edu -o custom-columns<span style="color:#f92672">=</span>NAME:.metadata.name,NODE:.spec.nodeName
</span></span><span style="display:flex;"><span>NAME                             NODE
</span></span><span style="display:flex;"><span>fine-spread-app-7589d49465-29wr9   gke-default-0c04c813-0d1z
</span></span><span style="display:flex;"><span>fine-spread-app-7589d49465-8l7v7   gke-default-0c04c813-0d1z
</span></span></code></pre></div><p>If the node <code>gke-default-0c04c813-0d1z</code> goes down, all pod replicas are gone too, making the deployment unavailable until rescheduled elsewhere. This is something that is outside your control by default. The good thing is, you might easily prevent it.</p>
<h1 id="inter-pod-anti-affinity">Inter-pod anti-affinity</h1>
<p>To ensure the multiple pod replicas are each scheduled on a different node, you can use <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity">inter-pod anti-affinity</a> constraints. Let&rsquo;s add it to the existing manifest (please check the referenced doc on how it works in details):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">edu</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">revisionHistoryLimit</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">affinity</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">podAntiAffinity</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">preferredDuringSchedulingIgnoredDuringExecution</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">podAffinityTerm</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">labelSelector</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">app.kubernetes.io/name</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">topologyKey</span>: <span style="color:#ae81ff">kubernetes.io/hostname</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dummy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">image</span>: <span style="color:#ae81ff">k8s.gcr.io/pause:3.2</span>
</span></span></code></pre></div><p>Applying the manifest, the pods now got rescheduled to different nodes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>$ kubectl get pod -n edu -o custom-columns<span style="color:#f92672">=</span>NAME:.metadata.name,NODE:.spec.nodeName
</span></span><span style="display:flex;"><span>NAME                             NODE
</span></span><span style="display:flex;"><span>fine-spread-app-856975d9d4-2ptg8   gke-default-0c04c813-0d1z
</span></span><span style="display:flex;"><span>fine-spread-app-856975d9d4-dp9sg   gke-default-0c04c813-7pfr
</span></span></code></pre></div><p>In case you have more nodes with free resources available in the cluster than the requested number of replicas, this indeed helps the app resiliency. This is also what the PR I mentioned in the intro was trying to address. Reviewing it, I got curious whether I can improve it further. As the cluster spreads across 2 different availability zones, can we schedule each pod replica in a different AZ? Yes, we can - as the <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#an-example-of-a-pod-that-uses-pod-affinity">doc explains</a>, simply modify the <code>podAntiAffinity</code> constraints by changing the <code>topologyKey</code> to prevent scheduling the replicas in the same zone:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">edu</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">revisionHistoryLimit</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">affinity</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">podAntiAffinity</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">preferredDuringSchedulingIgnoredDuringExecution</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">podAffinityTerm</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">labelSelector</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">app.kubernetes.io/name</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">topologyKey</span>: <span style="color:#ae81ff">topology.kubernetes.io/zone</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dummy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">image</span>: <span style="color:#ae81ff">k8s.gcr.io/pause:3.2</span>
</span></span></code></pre></div><p>For the example deployment with 2 replicas, this solution fully meets the requirement to have the pods scheduled, each on a different node and in a different AZ. But what if we need more replicas, e.g. 4? The <code>podAntiAffinity</code> example might again end up scheduling multiple pods on the same node. Searching for the solution, I first thought I will simply add another <code>podAffinityTerm</code> to the <code>podAntiAffinity</code> spec. Though, checking the doc I noticed there might be a better solution - a fresh new Kubernetes feature providing much more flexibility.</p>
<h1 id="pod-topology-spread-constraints">Pod Topology Spread Constraints</h1>
<p>The rather recent Kubernetes version <code>v1.19</code> added a new feature called <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/">Pod Topology Spread Constraints</a> to &ldquo;<em>control how Pods are spread across your cluster among failure-domains such as regions, zones, nodes, and other user-defined topology domains. This can help to achieve high availability as well as efficient resource utilization.</em>&rdquo;. This fits perfectly with this post’s goal of better controlling the pod scheduling in our cluster.</p>
<p>Using the Pod Topology Spread Constraints allows us to further improve the existing deployment - simply change back the <code>podAntiAffinity</code> <code>topologyKey</code> to <code>kubernetes.io/hostname</code> and add <code>topologySpreadConstraints</code> section to the deployment <code>template.spec</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">edu</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">revisionHistoryLimit</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">affinity</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">podAntiAffinity</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">preferredDuringSchedulingIgnoredDuringExecution</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">podAffinityTerm</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">labelSelector</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">app.kubernetes.io/name</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#ae81ff">fine-spread-app</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">topologyKey</span>: <span style="color:#ae81ff">kubernetes.io/hostname</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dummy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">image</span>: <span style="color:#ae81ff">k8s.gcr.io/pause:3.2</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">topologySpreadConstraints</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">maxSkew</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">topologyKey</span>: <span style="color:#ae81ff">topology.kubernetes.io/zone</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">whenUnsatisfiable</span>: <span style="color:#ae81ff">ScheduleAnyway</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">labelSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">fine-spread-app</span>
</span></span></code></pre></div><p>Apply the manifest, the pods are now scheduled based on the requirements (Note, we have also increased the replicas count):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>$ kubectl get pod -n edu -o custom-columns<span style="color:#f92672">=</span>NAME:.metadata.name,NODE:.spec.nodeName
</span></span><span style="display:flex;"><span>NAME                                 NODE
</span></span><span style="display:flex;"><span>fine-spread-app-6c97884994-cptjm   gke-default-92685fea-81d3
</span></span><span style="display:flex;"><span>fine-spread-app-6c97884994-f4w22   gke-default-0c04c813-rbwf
</span></span><span style="display:flex;"><span>fine-spread-app-6c97884994-qr58f   gke-default-92685fea-7pfr
</span></span><span style="display:flex;"><span>fine-spread-app-6c97884994-s4qj8   gke-default-0c04c813-0d1z
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Wait, <span style="color:#66d9ef">do</span> we need to combine podAntiAffinity and topologySpreadConstraints? No!
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Multiple topologySpreadConstraints </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The TSC feature documentation <span style="color:#f92672">[</span>provides clarifying comparison with the _PodAffinity_/_PodAntiaffinity_<span style="color:#f92672">](</span>https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/#comparison-with-podaffinity-podantiaffinity<span style="color:#f92672">)</span>:
</span></span><span style="display:flex;"><span>&gt; * For <span style="color:#e6db74">`</span>PodAffinity<span style="color:#e6db74">`</span>, you can try to pack any number of Pods into qualifying topology domain<span style="color:#f92672">(</span>s<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>&gt; * For <span style="color:#e6db74">`</span>PodAntiAffinity<span style="color:#e6db74">`</span>, only one Pod can be scheduled into a single topology domain.
</span></span><span style="display:flex;"><span>&gt;
</span></span><span style="display:flex;"><span>&gt; For finer control, you can specify topology spread constraints to distribute Pods across different topology domains - to achieve either high availability or cost-saving. This can also help on rolling update workloads and scaling out replicas smoothly.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>It means we can fully replace the <span style="color:#e6db74">`</span>podAntiAffinity<span style="color:#e6db74">`</span> spec with multiple <span style="color:#e6db74">`</span>topologySpreadConstraints<span style="color:#e6db74">`</span> items as the doc further explains: 
</span></span><span style="display:flex;"><span>&gt; When a Pod defines more than one topologySpreadConstraint, those constraints are ANDed: The kube-scheduler looks <span style="color:#66d9ef">for</span> a node <span style="color:#66d9ef">for</span> the incoming Pod that satisfies all the constraints.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>So let<span style="color:#960050;background-color:#1e0010">&#39;</span>s constraint:
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```</span>yaml
</span></span><span style="display:flex;"><span>apiVersion: apps/v1
</span></span><span style="display:flex;"><span>kind: Deployment
</span></span><span style="display:flex;"><span>metadata:
</span></span><span style="display:flex;"><span>  name: fine-spread-app
</span></span><span style="display:flex;"><span>  namespace: edu
</span></span><span style="display:flex;"><span>spec:
</span></span><span style="display:flex;"><span>  replicas: <span style="color:#ae81ff">4</span>
</span></span><span style="display:flex;"><span>  revisionHistoryLimit: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  selector:
</span></span><span style="display:flex;"><span>    matchLabels:
</span></span><span style="display:flex;"><span>      app.kubernetes.io/name: fine-spread-app
</span></span><span style="display:flex;"><span>  template:
</span></span><span style="display:flex;"><span>    metadata:
</span></span><span style="display:flex;"><span>      labels:
</span></span><span style="display:flex;"><span>        app.kubernetes.io/name: fine-spread-app
</span></span><span style="display:flex;"><span>    spec:
</span></span><span style="display:flex;"><span>      containers:
</span></span><span style="display:flex;"><span>      - name: dummy
</span></span><span style="display:flex;"><span>        image: k8s.gcr.io/pause:3.2
</span></span><span style="display:flex;"><span>      topologySpreadConstraints:
</span></span><span style="display:flex;"><span>      - maxSkew: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        topologyKey: topology.kubernetes.io/zone
</span></span><span style="display:flex;"><span>        whenUnsatisfiable: ScheduleAnyway
</span></span><span style="display:flex;"><span>        labelSelector:
</span></span><span style="display:flex;"><span>          matchLabels:
</span></span><span style="display:flex;"><span>            app.kubernetes.io/name: fine-spread-app
</span></span><span style="display:flex;"><span>      - maxSkew: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        topologyKey: kubernetes.io/hostname
</span></span><span style="display:flex;"><span>        whenUnsatisfiable: ScheduleAnyway
</span></span><span style="display:flex;"><span>        labelSelector:
</span></span><span style="display:flex;"><span>          matchLabels:
</span></span><span style="display:flex;"><span>            app.kubernetes.io/name: fine-spread-app
</span></span></code></pre></div><p>Apply the manifest, and see the pods being distributed, as in the previous example - each on a different node across the 2 availability zones. And this is the level of pod scheduling control I wanted to achieve to show you an example of how to spread workload to make it more resilient.</p>
<h1 id="summary">Summary</h1>
<p>Workload scheduling in Kubernetes is an interesting topic to explore if you want to better utilize your servers, make the workload more resilient, or prevent unexpected surprises. I wrote this post to show you a practical example of how to control the scheduling using Pod (Anti) Affinities and Pod Topology Spread Constraints. The former is available since the early K8s versions, but sometimes might not be flexible enough for your needs. Pod Topology Spread Constraints, available since <code>v1.19</code> when <code>EvenPodsSpread</code> <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/">feature gate reached GA stage</a>, provides much more flexibility. There is, of course, much more to explore beyond format of this introductory post  - check the referenced Kubernetes documentation to learn more.</p>
<p>Let me <a href="https://www.juniq.net">know</a> if you liked the article or interested to read more about different Kubernetes features! 👋</p>
]]></content>
        </item>
        
        <item>
            <title>DIY global cloud VPN service using WireGuard and GCP</title>
            <link>/blog/diy-vpn-wireguard-gcp/</link>
            <pubDate>Tue, 18 Feb 2020 17:00:00 +0100</pubDate>
            
            <guid>/blog/diy-vpn-wireguard-gcp/</guid>
            <description>Do you need to secure your internet connection occasionally while travelling, connecting to the internet over untrusted networks (cafes, airports, &amp;hellip;)? Tired of growing number of subscriptions or searching for a trusted VPN provider? Skilled enough to build your private VPN server but don&amp;rsquo;t want to host the service on your home network?
I found myself in a similar situation. Then a couple of announcements came during the last weeks of January 2020:</description>
            <content type="html"><![CDATA[<p>Do you need to secure your internet connection occasionally while travelling, connecting to the internet over untrusted networks (cafes, airports, &hellip;)? Tired of growing number of subscriptions or searching for a trusted VPN provider? Skilled enough to build your private VPN server but don&rsquo;t want to host the service on your home network?</p>
<p>I found myself in a similar situation. Then a couple of announcements came during the last weeks of January 2020:</p>
<ul>
<li><em>&ldquo;Linus pulled in net-next about a half hour ago. So WireGuard is now officially upstream. Yeah!&rdquo;</em> by <a href="https://lists.zx2c4.com/pipermail/wireguard/2020-January/004906.html">Bruno Wolf III</a> on the WireGuard mailing list.</li>
<li><em>&ldquo;I am beyond excited to finally announce Secret Manager - a secure and convenient method for storing API keys, passwords, certificates, and other sensitive data on @GCPcloud. It’s available for everyone today in beta&rdquo;</em> by <a href="https://twitter.com/sethvargo/status/1220035296018018310">@sethvargo</a> on Twitter</li>
</ul>
<p>The news combined with my intense affair with Google Cloud Platform (GCP) sparked an idea to build a solution for this - let&rsquo;s create a pool of short-lived WireGuard VPN servers in GCP cloud with minimum effort and cost. Starting a VPN instance in any supported location worldwide only when I need it. Most of the internet is running through HTTPS anyway and unless I want to escape <a href="https://en.wikipedia.org/wiki/Internet_censorship_and_surveillance_by_country">internet censorship</a> or access a service not being served in the current country, VPN is no more a daily necessity.</p>
<h2 id="the-idea">The idea</h2>
<p>The idea is the following:</p>
<ol>
<li>Store both sensitive data (DNS API keys, WireGuard private keys,&hellip;) and WireGuard configuration inside the <a href="https://cloud.google.com/secret-manager/">Google Secret Manager</a></li>
<li>Create a free (US-only) or paid GCE VM instance, without a static IP address to enjoy</li>
<li>Use <a href="https://cloud-init.io/">cloud-init</a> config to install &amp; setup the WireGuard VPN and update DNS record automatically on VM boot</li>
<li>Create multiple VMs (VPN instances) in various locations but run only 1 at a time to minimize costs</li>
<li>Use the <a href="https://cloud.google.com/console-app/">Cloud Console mobile app</a> to start the instance in a region you just need</li>
<li>Enjoy</li>
</ol>
<blockquote>
<p>This article can introduce you to the following topics:</p>
<ul>
<li>Install a WireGuard VPN server with minimal 3rd party tooling or complicated scripts</li>
<li>Store sensitive info required at runtime in a Google Secret Manager secure vault</li>
<li>Spawn a VPN server VM in an automated way in a location of your choice worldwide</li>
</ul>
</blockquote>
<p><strong>WARNING</strong></p>
<ul>
<li>The article is intended for tech-savvy readers with basic experience with DNS &amp; Cloud providers as it&rsquo;s not going to provide you a step-by-step guide for all the topics covered.</li>
<li>If you search for a solution to provide you with complete privacy, total anonymity and bullet-proof security, this provides you with none of those. Using a VPN is not a magic bullet, but definitely brings a few benefits.</li>
<li>Consult with a security professional all the pros and cons of using a VPN.</li>
</ul>
<h2 id="requirements">Requirements</h2>
<p>I built this concept using services I like for their simplicity and features offered in their free plan: GCP as a cloud, Cloudflare as a DNS provider. Here is a complete list of building blocks and skills recommended to try the concept:</p>
<ul>
<li>your own domain name</li>
<li>a DNS hosting for the domain allowing to update DNS records using API - I&rsquo;m using <a href="https://www.cloudflare.com/plans/">Cloudflare&rsquo;s Free Plan</a></li>
<li><a href="https://cloud.google.com/free">Google Cloud Platform</a> account</li>
<li>basic <a href="https://cloud.google.com/docs/overview/">GCP experience</a> - create, start, stop VM, create IAM account and configure firewall rules</li>
<li>(optional) <a href="https://cloud.google.com/sdk/">Google Cloud SDK CLI</a> installed - most of the implementation steps will provide a gcloud example</li>
<li>(optional) Shell scripting / Python basics - if you want to understand a few simple scripts and oneliners used in the examples</li>
<li>(optional) <a href="https://cloud.google.com/console-app">Cloud Console Mobile App</a> - to control VPN instances from your smartphone</li>
</ul>
<p>&#x1f4a1; Feel free to use providers and services based on your preference and experience.</p>
<h2 id="lets-play">Let&rsquo;s play</h2>
<p>Enough theory, let&rsquo;s play. This is the agenda:</p>
<ul>
<li>GCP quickstart</li>
<li>Create a limited GCP IAM Service Account</li>
<li>Create a DNS record</li>
<li>Create WireGuard configuration</li>
<li>Create firewall rules to allow the WireGuard traffic</li>
<li>Create WireGuard secrets using Google Secret Manager</li>
<li>Create a VM and your first VPN instance</li>
<li>Test, play, enjoy!</li>
</ul>
<h3 id="gcp-quickstart">GCP quickstart</h3>
<ol>
<li>Read the <a href="https://cloud.google.com/docs/overview/">GCP overview</a> if this domain is completely new for you</li>
<li>Sign in to the <a href="https://cloud.google.com/">GCP Cloud console</a> using a Google account. If you don&rsquo;t already have one, sign up for a new account.</li>
<li><a href="https://cloud.google.com/billing/docs/how-to/manage-billing-account#create_a_new_billing_account">Create a new billing account</a> to use the account (required only for new users). You must have a valid Cloud Billing Account to use GCP even if you are in your <a href="https://cloud.google.com/free/docs/gcp-free-tier#free-trial">free trial period</a> or you only use Google Cloud resources that are covered by the <a href="https://cloud.google.com/free/docs/gcp-free-tier#always-free">Always Free program</a>.</li>
<li>Select or create a Cloud project</li>
<li>Install <a href="https://cloud.google.com/sdk/install">Cloud SDK cli</a> if you prefer to follow the <code>gcloud</code> examples in this article</li>
<li>(Optional, but highly recommended) <a href="https://cloud.google.com/billing/docs/how-to/budgets#create-budget">Set budget alerts</a> to avoid any unexpected payments.</li>
</ol>
<h3 id="create-a-limited-gcp-iam-service-account">Create a limited GCP IAM service Account</h3>
<p>Create a new Cloud IAM service account and grant it only a role to access the Secret manager secrets for the project. The SA account will be used later to create every new VPN VM instance with a restricted identity. This is important for security reasons:</p>
<ul>
<li>the default Compute Engine service account grants the instance more permissions than is required. Google <a href="https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#best_practices">recommends</a> that each instance that needs to call a Google API should run as a service account with the minimum permissions necessary for that instance to do its job.</li>
<li>The role grants instance rights to only access (view) secrets stored in the Secret Manager vault. The VM (or a potential attacked) will not be allowed to edit or list the secrets stored in the Secret Manager, nor to access any other resources using API.</li>
</ul>
<p>Steps:</p>
<ol>
<li><a href="https://cloud.google.com/secret-manager/docs/quickstart#enable_api">Enable the Secret Manager API</a> for the project</li>
<li><a href="https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating">Create a service account</a> using a console or use gcloud CLI:
<pre tabindex="0"><code>gcloud iam service-accounts create secret-accessor \
--description &#34;SA for accessing Secret Manager secrets&#34; \
--display-name &#34;secret accessor&#34;
</code></pre></li>
<li><a href="https://cloud.google.com/secret-manager/docs/quickstart#assign-iam-roles">Grant</a> the service account <code>secretmanager.secretAccessor</code> IAM role:
<pre tabindex="0"><code>gcloud projects add-iam-policy-binding project-123 \
--member serviceAccount:secret-accessor@project-123.iam.gserviceaccount.com \
--role roles/secretmanager.secretAccessor
</code></pre></li>
</ol>
<h3 id="setup-dns">Setup DNS</h3>
<p>To setup the system and enable the dynamic DNS update, we need the following:</p>
<ul>
<li>A DNS record</li>
<li>API token and another provider specific info (e.g. Cloudflare Zone ID, the DNS record Cloudflare ID) to enable dynamic DNS updates</li>
</ul>
<p>Steps:</p>
<ol>
<li>Create an <a href="https://www.cloudflare.com/learning/dns/dns-records/dns-a-record/">A DNS record</a> that will VPN clients use to connect to the VPN server. As we do know the IP address of the VM instance yet, point the record to any IP address, e.g. any from the <a href="https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses">private ranges of the IPv4 addresses</a>: <code>vpn.example.com</code> -&gt; <code>172.27.27.2</code>. Example instructions for <a href="https://support.cloudflare.com/hc/en-us/articles/360019093151-Managing-DNS-records-in-Cloudflare#h_60566325041543261564371">adding a DNS record on Cloudflare</a></li>
<li>Create an <a href="https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys">API token</a> or any provider specific key that will allow you to update DNS record remotely using API.</li>
<li>Get other details required for the API requests to work</li>
</ol>
<h3 id="create-a-wireguard-configuration">Create a WireGuard configuration</h3>
<p>There are tons of howto guides on the internet how to create a WireGuard configuration fitting your needs. Here are at least 2 links:</p>
<ul>
<li><a href="https://www.wireguard.com/quickstart/#quick-start">Official WireGuard Quickstart guide</a></li>
<li><a href="https://github.com/pirate/wireguard-docs#config-reference">Unofficial WireGuard documentation - Configuration reference</a></li>
</ul>
<p>I will further use following WireGuard configuration used in every VM instance created (<code>wg0.conf</code>):</p>
<pre tabindex="0"><code>[Interface]
Address = 172.30.0.1/24
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s 172.30.0.0/24 -o ens4 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -o wg0  -j ACCEPT; iptables -t nat -D POSTROUTING -s 172.30.0.0/24 -o ens4 -j MASQUERADE
ListenPort = 51820
PrivateKey = abcdef123456serverPrivateKey==

# Client Alice
[Peer]
PublicKey = abcdef123456aliceKey=
AllowedIPs = 172.30.0.2/32

# Client Bob
[Peer]
PublicKey = abcdef123456bobKey=
AllowedIPs = 172.30.0.3/32
</code></pre><p>This configuration exaplanation:</p>
<ul>
<li>WireGuard server listens on UDP port 51820</li>
<li>creates iptables NAT &amp; forwarding firewall rules on start</li>
<li>binds to <code>ens4</code> network interface that&rsquo;s being created and enabled by default on the <code>ubuntu-minimal-1910</code> VM image from the <code>ubuntu-os-cloud</code> GCE family that I use for this setup.</li>
<li>uses following keypair for the VPN server: <code>abcdef123456serverPrivateKey==</code>/<code>abcdef123456serverPublicKey==</code></li>
<li>configures 2 peers/clients - Alice (IP: <code>172.30.0.2/32</code>) and Bob (IP: <code>172.30.0.3/32</code>)</li>
</ul>
<h3 id="store-config-and-secrets-in-the-secret-manager-vault">Store config and secrets in the Secret Manager vault</h3>
<p><a href="https://cloud.google.com/secret-manager/docs/overview">Secret Manager</a> is a pretty cool, new product in the GCP portfolio (currently in Beta) that complements the existing <a href="https://cloud.google.com/secret-manager/docs/overview">Cloud KMS</a> solution for data encryption. It allows storing sensitive data <a href="https://cloud.google.com/secret-manager/docs/encryption">encrypted at rest using AES-256</a>. Customer managed encryption keys are not yet supported.</p>
<p>Let&rsquo;s create and store following secrets for this example setup:</p>
<ol>
<li>WireGuard configuration created in the previous section: <code>gcloud beta secrets create wg_config --data-file wg0.conf --replication-policy automatic</code></li>
<li>Cloudflare API token: <code>echo -n 'my_cloudflare_api_token==' | gcloud beta secrets create wg_cf_api_token --data-file - --replication-policy automatic</code></li>
<li>Cloudflare Zone ID: <code>echo -n 'my_cloudflare_zone_id_123456' | gcloud beta secrets create wg_cf_zone_id --data-file - --replication-policy automatic</code></li>
<li>Hostname of the VM WireGuard clients will conect to: <code>echo -n 'vpn.example.com' | gcloud beta secrets create wg_hostname --data-file - --replication-policy automatic</code></li>
</ol>
<p>As an alternative, <a href="https://console.cloud.google.com/security/secret-manager">Secrets Manager cloud console</a> can be used to create the secrets if you prefer GUI.</p>
<h3 id="create-a-cloud-init-configuration">Create a cloud-init configuration</h3>
<p><a href="https://cloudinit.readthedocs.io/en/latest/">cloud-init</a> is a tool that helps to install &amp; configure a cloud (GCE) VM to the desired state. In short, the cloud-init example:</p>
<ul>
<li>installs required packages - iptables &amp; wireguard</li>
<li>enables IP forwarding</li>
<li>sets up a script executed on every boot to:
<ul>
<li>read and store WireGuard config from the Secret Manager</li>
<li>read and use other sensitive data we saved in the Secret Manager</li>
<li>create or update Cloudflare DNS record with the VM&rsquo;s public IP address using Cloudflare API on boot</li>
</ul>
</li>
<li>updates OS packages</li>
<li>reboots the VM after the install is finished</li>
</ul>
<pre tabindex="0"><code>#cloud-config
package_upgrade: true
packages:
  - iptables
  - wireguard
write_files:
  - content: |
      net.ipv4.ip_forward=1
    path: /etc/sysctl.d/99-wireguard.conf
    permissions: &#39;0644&#39;
  - content: |
      #!/usr/bin/env bash
      set -e

      # get latest wireguard configuration from the secret manager and store it locally
      gcloud beta secrets versions access latest --secret wg_config &gt; /etc/wireguard/wg0.conf
      chmod 0600 /etc/wireguard/wg0.conf

      # get other sensitive data
      hostname=&#34;$(gcloud beta secrets versions access latest --secret wg_hostname)&#34;
      cf_api_token=&#34;$(gcloud beta secrets versions access latest --secret wg_cf_api_token)&#34;
      cf_zone_id=&#34;$(gcloud beta secrets versions access latest --secret wg_cf_zone_id)&#34;

      # create or update the Cloudflare DNS record
      if ip=$(curl -fs ifconfig.me); then
              echo &#34;Checking if DNS record $hostname exists in Cloudflare&#34;
              dns_check=$(curl -sX GET &#34;https://api.cloudflare.com/client/v4/zones/${cf_zone_id}/dns_records?type=A&amp;name=${hostname}&#34; \
              -H &#34;Authorization: Bearer $cf_api_token&#34; \
              -H &#34;Content-Type: application/json&#34; \
              | python3 -c &#34;import sys, json; f=json.load(sys.stdin); print(&#39;{};{}&#39;.format(f[&#39;result&#39;][0][&#39;content&#39;],f[&#39;result&#39;][0][&#39;id&#39;])) if &#39;result&#39; in f else print(&#39;EE&#39;)&#34;)
              IFS=&#39;;&#39; read -ra response &lt;&lt;&lt; &#34;${dns_check}&#34;
              if [[ &#34;${response[0]}&#34; == &#34;${ip}&#34; ]]; then
                      echo &#34;DNS record ${hostname} OK: ${ip}. No need to update.&#34;
              else
                      echo &#34;Updating existing A record $hostname to $ip&#34;
                      curl -o /dev/null -fsX PUT &#34;https://api.cloudflare.com/client/v4/zones/${cf_zone_id}/dns_records/${response[1]}&#34; \
                      -H &#34;Authorization: Bearer ${cf_api_token}&#34; \
                      -H &#34;Content-Type: application/json&#34; \
                      -d &#34;{\&#34;type\&#34;: \&#34;A\&#34;, \&#34;name\&#34;: \&#34;${hostname}\&#34;, \&#34;content\&#34;: \&#34;${ip}\&#34;, \&#34;proxied\&#34;: false}&#34;
              fi
      else
              echo &#34;Problem detecting current IP address. Exiting&#34;
      fi
    path: /usr/local/bin/wg_dns_updater.sh
    permissions: &#39;0755&#39;
  - content: |
      [Unit]
      Description=wg-dns-updater script
      After=network.target

      [Service]
      Type=oneshot
      RemainAfterExit=yes
      ExecStart=/usr/local/bin/wg_dns_updater.sh

      [Install]
      WantedBy=multi-user.target
    path: /etc/systemd/system/wg-dns-updater.service
    permissions: &#39;0644&#39;
runcmd:
  - [ sysctl, -p, /etc/sysctl.d/99-wireguard.conf ]
  - [ systemctl, daemon-reload ]
  - [ systemctl, enable, --now, --no-block, wg-quick@wg0.service ]
  - [ systemctl, enable, --now, wg-dns-updater.service ]
power_state:
  mode: reboot
  delay: 1
  message: Rebooting after installation
</code></pre><p>I recommend everyone to review the config and change it to fit personal needs.</p>
<h3 id="create-a-gce-vm">Create a GCE VM</h3>
<p>Now the best part - glueing everything together. I will create multiple VMs using the cloud-init config in regions I need. I use the following bash script to create the instances:</p>
<pre tabindex="0"><code>#!/usr/bin/env bash
NAME=${1:-vpn-ue1b}
ZONE=${2:-us-east1-b}
gcloud compute instances create &#34;$NAME&#34; \
  --machine-type f1-micro \ # f1-micro is powerful enough to run WireGuard server
  --image-family ubuntu-minimal-1910 \ # Ubuntu 19.10 provides WireGuard in the universe repo
  --image-project ubuntu-os-cloud \
  --metadata-from-file user-data=cloud-init-config.yaml \ # use the prepared cloud-init config here
  --scopes cloud-platform \
  --service-account secret-accessor@project-123.iam.gserviceaccount.com \ # use the prepared secret-accessor service account
  --tags wg \ # use network tags for the firewall rules
  --zone &#34;$ZONE&#34; # zone where to create the instance
</code></pre><p>And then execute the script:</p>
<pre tabindex="0"><code>./vm-create.sh vpn-ue1b us-east1-b
./vm-create.sh vpn-uc1b us-central1-b
./vm-create.sh vpn-uw1b us-west1-b
</code></pre><p>It&rsquo;s important to remind the DNS record is updated on every VM boot. Therefore shut down all VMs after creation and start only a single VM in a region you just need.</p>
<p>Pro tips:</p>
<ul>
<li>you can have up to 3 VMs created in the US zones and still qualify for the free tier if you have only single one running at a time.</li>
<li>use <code>--preemptible</code> argument when creating the VM to have it automatically shut down within 24 hours. The only con of this is that a preemptible instance is excluded from the free tier so you will be charged for running it.</li>
<li>WARNING: only 1 GB of network egress from North America to all region destinations per month (excluding China and Australia) is included in the free tier.</li>
</ul>
<h3 id="configure-firewall">Configure firewall</h3>
<p>Create a firewall rule to enable incoming traffic to <code>udp/51820</code> to the WireGuard VM(s):
<code>gcloud compute firewall-rules create wg --direction IN --target-tags wg --allow udp:51820 --source-ranges 0.0.0.0/0</code></p>
<p>As the VM instances are tagged by a network tag, this <a href="https://cloud.google.com/vpc/docs/add-remove-network-tags#using_tags_with_firewall_rules_and_routes">allows applying firewall rules and routes to a specific instance or set of instances</a>.</p>
<h3 id="test-vpn-connection">Test VPN connection</h3>
<p>Let&rsquo;s now test if the VPN connection works. Install a WireGuard client on a platform of your choice and configure it to connect to the VPN server:</p>
<pre tabindex="0"><code>[Interface]
Address = 172.30.0.2/32
PrivateKey = abcdef123456alicePrivateKey==

[Peer]
PublicKey = abcdef123456serverPublicKey==
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
</code></pre><h3 id="install-the-cloud-console-mobile-app">Install the Cloud Console Mobile app</h3>
<p>I recommend to install the <a href="https://cloud.google.com/console-app">Cloud Console Mobile app</a> to start and stop WireGuard server instances from your smartphone. You can thus fire up any of the pre-installed instances in the region you just need using your smartphone, very handy thing!</p>
<h2 id="final-notes">Final notes</h2>
<p>In this post, I have shown you how to build a self-managed, on-demand VPN infrastructure ready to tunnel your private internet traffic using WireGuard and Google Cloud Platform. You can now own and run the whole infrastructure on-demand with minimum effort and costs end scape the VPN subscription and fake reviews hell.</p>
<p>If you like the article or have any feedback, feel free to <a href="/">let me know</a> or share the post.</p>
<p>Also, consider <a href="https://www.wireguard.com/donations">donating to WireGuard project</a> to help with the development of this great OSS tool &#x2764;&#xfe0f;</p>
]]></content>
        </item>
        
        <item>
            <title>Hello, the world of words!</title>
            <link>/blog/hello-the-world-of-words/</link>
            <pubDate>Mon, 18 Nov 2019 22:55:02 +0100</pubDate>
            
            <guid>/blog/hello-the-world-of-words/</guid>
            <description>Every blog should have some proper introduction. So this is it - my own creative playground. I will try to occasionally put down thoughts and share knowledge on mostly tech-related topics that exceeds 280 characters. Let&amp;rsquo;s see how it will work and how much joy it will spark &amp;#x1f4a5;.
Happy reading!</description>
            <content type="html"><![CDATA[<p>Every blog should have some proper introduction. So this is it - my own creative playground. I will try to occasionally put down thoughts and share knowledge on mostly tech-related topics that exceeds 280 characters. Let&rsquo;s see how it will work and how much joy it will spark &#x1f4a5;.</p>
<p>Happy reading!</p>
]]></content>
        </item>
        
    </channel>
</rss>
