<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on alikhil</title>
    <link>https://alikhil.dev/posts/</link>
    <description>Recent content in Posts on alikhil</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>Alik Khilazhev</managingEditor>
    <lastBuildDate>Fri, 24 Apr 2026 00:11:43 +0300</lastBuildDate><atom:link href="https://alikhil.dev/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to Quickly Prepare for Software Engineering Interviews</title>
      <link>https://alikhil.dev/posts/how-to-quickly-prepare-for-software-engineering-interviews/</link>
      <pubDate>Fri, 24 Apr 2026 00:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/how-to-quickly-prepare-for-software-engineering-interviews/</guid>
      <description>&lt;p&gt;A few months ago, I found myself needing to prepare for a series of job interviews within a very limited timeframe. It was a stressful experience, but it ultimately worked out well. I decided to share my notes and reflections in case they’re helpful to others in a similar situation.&lt;/p&gt;
&lt;p&gt;This is especially relevant if you’re not actively job hunting and suddenly receive an interview invitation, leaving you with limited time to prepare but a strong desire to maximize your chances of success.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months ago, I found myself needing to prepare for a series of job interviews within a very limited timeframe. It was a stressful experience, but it ultimately worked out well. I decided to share my notes and reflections in case they’re helpful to others in a similar situation.</p>
<p>This is especially relevant if you’re not actively job hunting and suddenly receive an interview invitation, leaving you with limited time to prepare but a strong desire to maximize your chances of success.</p>
<p><strong>Disclaimer</strong>: The tips described in this post may be more useful for senior engineers with hands-on experience and engineering intuition.</p>
<h2 id="hr-interview">HR Interview</h2>
<p>The internet is full of articles listing all possible HR interview questions. I recommend spending a bit of time on them just to understand what to expect and not be surprised.</p>
<p>However, in my humble opinion, there are two main points to focus on during HR interview preparation.</p>
<p>First, you need a short story that tells your experience briefly. Avoid listing every bullet point from your CV. Instead, focus on highlighting your key achievements. Also, your story must be aligned with the position you are applying to. Yes, you might need to adjust your story for different jobs at different companies.</p>
<p>Second, it’s important to have a clear motivation. Why do you want to change your job, and why this company/role? What kind of job are you looking for?</p>
<h2 id="system-design">System Design</h2>
<p>If you have some experience doing System Design interviews or have never done it, start by learning the <a href="https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery">Delivery framework</a>. Understand each section.</p>
<p>Watch at <a href="https://www.youtube.com/watch?v=Nfa-uUHuFHg">least one video</a> on how it’s done. The more, the better. These videos from <a href="https://www.youtube.com/@hello_interview">Hello Interview channel</a> are really good, though.</p>
<p>If you are applying to a FAANG company, you may search for <a href="https://systemdesign.io/">leaked system design questions</a> from that company and spend some time preparing for them. But there is no guarantee that you will get the same topic, thus I would not recommend spending <strong>all your time</strong> here.</p>
<p>If you can, do a mock interview. Ask a friend or find someone to practice with.
If you can’t, then try to walk through alone, but talk through everything out loud.</p>
<p>During the interview, treat the interviewer as a colleague, ask questions, ensure you understand the problem, and that you have not missed any important requirements before building the design of the system.</p>
<p>Don’t rush.</p>
<h2 id="live-coding">Live coding</h2>
<p><img loading="lazy" src="/images/posts/fast-interview-preparation/leetcode.jpeg" type="" alt="leetcode"  /></p>
<p>This part is really tricky. If the company tends to use LeetCode-style interviews, there is no shortcut here. You need to solve hundreds of them to really feel confident. You may need to refresh your memory on algorithms you feel less confident about (for example, I always forget about corner cases for binary search).</p>
<p>Again, if it’s a big / well-known company, you can try to search for leaked coding interview questions.</p>
<h2 id="behavioral-interview">Behavioral interview</h2>
<p>S.T.A.R (situation task action result) &amp; C.A.R.L (context action result learning)</p>
<p>There are dozens of questions you could be asked in behavioral interviews. And you’re expected to structure your answers using the STAR framework. This means you need to tell a story by defining a context, your actions, and results.  You could go and just prepare a STAR format answer to all such questions, but it will take a lot of time, and it’s suboptimal. This, combined with the fact that the same stories can be used for different questions, makes the situation easier for you. You can prepare 7–10 stories that will cover most of the questions. During preparation, you can write them as text, but don’t read them during the interview. It tends to sound unnatural.</p>
<p>When telling your story using the STAR method, make sure your final sentence clearly highlights a positive outcome. Adjust your tone to emphasize this closing part so it stands out.</p>
<p>The STAR framework is a standard. But also check CARL in some questions, it would be good to tell what you have learned from that story.</p>
<p>Here are some materials that helped me to prepare for a behavioral interview:</p>
<ul>
<li><a href="https://youtu.be/bBvPQZmPXwQ?si=s1ZbiomG5ouanRye">Hello Interview - Behavioral Interview Discussion with Ex-Meta Hiring Committee Member</a>- must watch Behavioral interview, although I would recommend watching it even before the HR interview, because it gives a bunch of helpful tips about self-presentation</li>
<li><a href="https://thebehavioral.substack.com/">https://thebehavioral.substack.com/</a> - Strategies, tips, and resources to prepare for your next behavioral interview from a FAANG+ insider.</li>
</ul>
<h2 id="project-walkthrough">Project Walkthrough</h2>
<p>Some companies have such an interview stage. It’s quite unpopular but still exists.
You’re asked to present a project or problem you worked on. You explain the context, problem, solution, results, and your role in this story. It’s like showing the result of your work to colleagues from different departments/teams.</p>
<p>This stage is very open-ended. You are not given specific instructions, and there is not much information on the internet with recommendations on how to prepare and conduct such interviews.
When I found out I would have this interview, I was initially shocked and unsure how to prepare, as I didn’t know what to expect. It wasn&rsquo;t until I realized that in reality, <strong>it’s you, the interviewee, who rules this interview</strong>. You choose the project, decide what to include and omit, control the level of detail, and you are coming up with the story you know, with all the answers for all possible questions, because it’s your story.</p>
<p>So, make the most of this stage. Prepare your story, make a few slides / notes / architecture sketches.</p>
<p>Don’t dig into details too much. Leave a space for the questions.</p>
<p>And even if there is no dedicated interview, you may be asked to tell in detail about a certain problem/project you were working on. So, be prepared. Have your story!</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>When answering open-ended questions, aim to <strong>tell stories where the scale of the problem matches the level of the role you’re applying for</strong>. For example, if you are asked, “Tell me about a challenging/interesting problem/task you were working on recently.” Optimizing an SQL query by adding an index may be fine for junior roles, but it won’t carry enough weight for senior positions. Interviewers would expect to hear something bigger, challenging, higher stakes, and often involving cross-team collaboration, such as migrating a large system to Kubernetes.</p>
<p><strong>Question back</strong>. You should ask questions to learn more about the company, their culture, the hiring manager’s management style, and what they like or dislike about their work. Prepare a list of questions before the interview.</p>
<p><strong>Start preparing in advance</strong>. Even if you’re not planning to change jobs anytime soon, you can begin investing in your future by:</p>
<ul>
<li>solving one LeetCode problem a day</li>
<li>keeping track of tasks/projects you’ve completed, along with your achievements (many companies require this anyway for performance reviews) – this would be a foundation for your stories in behavioral and project walkthrough interviews.</li>
<li>keeping your CV and LinkedIn up to date.</li>
</ul>]]></content:encoded>
    </item>
    
    <item>
      <title>iOS shortcuts for microbin</title>
      <link>https://alikhil.dev/posts/ios-shortcut-for-microbin/</link>
      <pubDate>Mon, 06 Apr 2026 19:41:44 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/ios-shortcut-for-microbin/</guid>
      <description>&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;Recently, I&amp;rsquo;ve deployed &lt;a href=&#34;https://microbin.eu&#34;&gt;microbin&lt;/a&gt;, minimalistic opensource &amp;ldquo;pastebin&amp;rdquo;-like service, into my homelab to share temporal files/text between devices. It works quite well.&lt;/p&gt;
&lt;p&gt;A few days ago, I started wondering: could I upload files or paste text into Microbin directly from the iOS Share menu?&lt;/p&gt;
&lt;p&gt;The short answer is yes. It could be done using iOS&amp;rsquo;s builtin &amp;ldquo;Shortcuts&amp;rdquo;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hi there!</p>
<p>Recently, I&rsquo;ve deployed <a href="https://microbin.eu">microbin</a>, minimalistic opensource &ldquo;pastebin&rdquo;-like service, into my homelab to share temporal files/text between devices. It works quite well.</p>
<p>A few days ago, I started wondering: could I upload files or paste text into Microbin directly from the iOS Share menu?</p>
<p>The short answer is yes. It could be done using iOS&rsquo;s builtin &ldquo;Shortcuts&rdquo;.</p>
<h2 id="use-cases">Use cases</h2>
<p>I have two general cases of using microbin from my corporate iPhone to my personal devices:</p>
<ol>
<li>Share some text / link</li>
<li>Share file(s)</li>
</ol>
<p>For simplicity, I decided to create dedicated shortcuts for each use case.</p>
<h2 id="paste-text-into-microbin">Paste text into microbin</h2>
<ol>
<li>
<p>Open <strong>Shortcuts</strong> app and create new empty shortcut, rename it into &ldquo;Paste into microbin&rdquo;.</p>
</li>
<li>
<p>Add <strong>Share</strong> action, click at <em>Input</em> and select <strong>Shortcut Input</strong>. New <strong>Receive</strong> action will pop up.</p>
<table>
  <thead>
      <tr>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-1.png" type="" alt="Share action"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-2.png" type="" alt="Share action"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-3.png" type="" alt="Receive action"  /></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</li>
<li>
<p>Click at <em>Nowhere</em> in <strong>Receive &hellip; from</strong> action, enable <strong>Show in Share Sheet</strong> toggle and disable <strong>Show in Search</strong>, click ✅ at the top right.</p>
</li>
<li>
<p>Next to the <strong>if there&rsquo;s no input</strong>, click <em>Continue</em> and select <strong>Ask For</strong> and <strong>Text</strong></p>
</li>
<li>
<p>Delete <strong>Share</strong> action, we don&rsquo;t need it anymore.</p>
<table>
  <thead>
      <tr>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-4.png" type="" alt="Share sheet"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-6.png" type="" alt="Ask for Text"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-13.png" type="" alt="Delete share"  /></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</li>
<li>
<p>Add <strong>Get contents of URL</strong> action.</p>
</li>
<li>
<p>Put your microbin url into <em>URL</em> field. For example <code>https://pub.microbin.eu/upload</code></p>
</li>
<li>
<p>Expand action configuration by clicking ▶︎ button, then:</p>
<ul>
<li>Set <em>Method</em> as <strong>POST</strong></li>
<li>Add <strong>Content-Type</strong> header with value <code>multipart/form-data</code></li>
<li>Select <strong>Form</strong> as <em>Request Body</em> type</li>
<li>Add <strong>content</strong> field as <em>File</em> type (important!) and select <strong>Shortcut Input</strong> as value.
<table>
  <thead>
      <tr>
          <th></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-10.png" type="" alt="Get contents of URL"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-9.png" type="" alt="Configure URL"  /></th>
          <th></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</li>
</ul>
</li>
<li>
<p>That&rsquo;s all. Test it by clicking ▶︎ Play button in the bottom menu. If you did everything correctly you&rsquo;ll see preview with the link.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-11.png" type="" alt="Play button"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/paste-12.png" type="" alt="Preview"  /></th>
          <th></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</li>
</ol>
<h2 id="upload-files-into-microbin">Upload files into microbin</h2>
<p>A shortcut for file upload is configured in very similar way. I won&rsquo;t describe it in detail but mention only notable changes:</p>
<ul>
<li>Duplicate the <strong>Paste into Microbin</strong> shortcut to not start from scratch.</li>
<li>In <strong>Receive</strong> action, select <strong>Files</strong> type and allow <em>multiple files</em></li>
<li>Add <strong>Repeat with each item</strong> action loop to iterate over <em>Selected input</em></li>
<li>In <strong>Get Contents of URL</strong> action everything stays the same, except <em>content</em> field - it gets renamed into <em>file</em></li>
</ul>
<table>
  <thead>
      <tr>
          <th></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/files-1.PNG" type="" alt="Play button"  /></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/files-2.PNG" type="" alt="Preview"  /></th>
          <th></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
<h2 id="preview">Preview</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th></th>
          <th><img loading="lazy" src="/images/posts/ios-shortcuts/preview.PNG" type="" alt="Preview"  /></th>
          <th></th>
          <th></th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
<h2 id="summary">Summary</h2>
<p>If you did everything from above and it did not work, or you don&rsquo;t want to bother with configuring it at all. You can import my shortcuts in your device and override microbin url:</p>
<ul>
<li>
<p>📋 <a href="https://www.icloud.com/shortcuts/8108f92bea454d20b5e68b8bd4f3c8db">Paste Into Microbin</a></p>
</li>
<li>
<p>📤 <a href="https://www.icloud.com/shortcuts/1808fc6036e9436b8534781f7b182ab7">Upload Files to Microbin</a></p>
</li>
</ul>]]></content:encoded>
    </item>
    
    <item>
      <title>What is a CDN and Why It Matters?</title>
      <link>https://alikhil.dev/posts/what-is-a-cdn-and-why-it-matters/</link>
      <pubDate>Sat, 21 Mar 2026 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/what-is-a-cdn-and-why-it-matters/</guid>
      <description>&lt;p&gt;With the rapid growth of GenAI solutions and the continuous launch of new applications, understanding the fundamental challenges and solutions of the web is becoming increasingly important.&lt;/p&gt;
&lt;p&gt;One of the core challenges is &lt;strong&gt;delivering content quickly to the end user&lt;/strong&gt;. This is where a CDN comes into play.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>With the rapid growth of GenAI solutions and the continuous launch of new applications, understanding the fundamental challenges and solutions of the web is becoming increasingly important.</p>
<p>One of the core challenges is <strong>delivering content quickly to the end user</strong>. This is where a CDN comes into play.</p>
<p>A CDN stands for <strong>Content Delivery Network</strong>. Let’s break it down.</p>
<p><em>(Note: Modern CDN providers often bundle additional services such as WAF, DDoS protection, and bot management. Here, we focus on the static content delivery.)</em></p>
<h3 id="content">Content</h3>
<p>Content refers to any asset that needs to be loaded on the user’s device: images, audio/video files, JavaScript, CSS, and more.</p>
<h3 id="delivery">Delivery</h3>
<p>Delivery means that this content is not only available but also delivered efficiently and quickly.</p>
<h3 id="network">Network</h3>
<p>A CDN is a <strong>network</strong> of distributed nodes that cache content. Instead of fetching files directly from the origin server, users receive them from the nearest node, minimizing latency.</p>
<hr>
<h2 id="how-applications-work-without-a-cdn">How Applications Work Without a CDN</h2>
<p>Consider an online marketplace for digital assets, such as a photo stock or NFT platform. The application stores thousands of images on a central server. Whenever users open the app, those images must load quickly.
<img loading="lazy" src="/images/posts/cdn/without-cdn-scheme.png" type="" alt="without-cdn-scheme.png"  />
If the application server is hosted in Paris, users in Paris will experience minimal ping. However:</p>
<ul>
<li>Users in Spain may see about 2× ping time.</li>
<li>Users in the USA may see 6× ping time.</li>
<li>Users in Australia may see 12× ping time.</li>
</ul>
<p>These numbers only reflect simple ICMP ping times. Actual file delivery involves additional overhead such as TCP connections and TLS handshakes, which increase delays even further.</p>
<hr>
<h2 id="how-a-cdn-solves-this-problem">How a CDN Solves This Problem</h2>
<p><img loading="lazy" src="/images/posts/cdn/with-cdn-scheme.png" type="" alt="with-cdn-scheme"  /></p>
<p>With a CDN, each user connects to the <strong>nearest edge node</strong> instead of the origin server. This is typically achieved via GeoDNS. Importantly, only the CDN knows the actual address of the origin server, which also improves security by reducing exposure to direct DDoS attacks.</p>
<p>CDN providers usually operate edge nodes in major world cities. When a request is made:</p>
<ol>
<li>
<p>If the requested file is already cached on the edge node (<strong>cache hit</strong>), it is delivered instantly.</p>
</li>
<li>
<p>If not (<strong>cache miss</strong>), the edge node requests it from the <strong>CDN shield</strong>.</p>
<ul>
<li>
<p>If the shield has the file cached, it is returned to the edge and then served to the user.</p>
</li>
<li>
<p>If not, the shield fetches it from the origin server, caching it along the way.</p>
</li>
</ul>
</li>
</ol>
<p><img loading="lazy" src="/images/posts/cdn/request.png" type="" alt="request.png"  /></p>
<p>For popular websites, the cache hit rate approaches but rarely reaches 100% due to purges, new files, or new users.</p>
<p>The <strong>shield node</strong> plays a critical role. Without it, each cache miss from any edge node would hit the origin server directly, increasing load. Many providers offer shields as an optional feature, and enabling them can significantly reduce origin stress.</p>
<h2 id="measuring-cdn-effectiveness">Measuring CDN Effectiveness</h2>
<p>Beyond cache hits and misses, performance can be measured with concrete indicators:</p>
<ul>
<li>
<p><strong>Time to First Byte (TTFB):</strong> How long it takes for the first data to arrive after a request. CDNs usually reduce TTFB by terminating connections closer to the user.</p>
</li>
<li>
<p><strong>Latency reduction:</strong> The difference in round-trip time between delivery from the origin versus delivery from an edge node.</p>
</li>
<li>
<p><strong>Cache hit ratio:</strong> The percentage of requests served directly from edge caches.</p>
</li>
</ul>
<p>These KPIs provide a real, measurable view of CDN efficiency rather than theoretical assumptions.</p>
<h2 id="which-cdn-is-best">Which CDN is Best?</h2>
<p>The closer the edge node is to the end user, the faster the content loads. The key questions are: <strong>Where are the users located? Which CDN providers have the best edge coverage for those locations?</strong></p>
<p>But don’t rely on maps alone. Measure real performance with Real User Monitoring (RUM) using metrics like TTFB and Core Web Vitals. There are plenty of ready-made tools available. If you’re interested in building your own RUM system, leave a comment or reaction – I can cover that in a follow-up post.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Kubernetes In-Place Pod Resize</title>
      <link>https://alikhil.dev/posts/in-place-pod-resize/</link>
      <pubDate>Sun, 28 Dec 2025 21:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/in-place-pod-resize/</guid>
      <description>&lt;p&gt;About six years ago, while operating a large Java-based platform in Kubernetes, I noticed a recurring problem: our services required significantly higher CPU and memory during application startup. Heavy use of Spring Beans and AutoConfiguration forced us to set inflated resource requests and limits just to survive bootstrap, even though those resources were mostly unused afterwards.&lt;/p&gt;
&lt;p&gt;This workaround never felt right. As an engineer, I wanted a solution that reflected the actual lifecycle of an application rather than its worst moment.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>About six years ago, while operating a large Java-based platform in Kubernetes, I noticed a recurring problem: our services required significantly higher CPU and memory during application startup. Heavy use of Spring Beans and AutoConfiguration forced us to set inflated resource requests and limits just to survive bootstrap, even though those resources were mostly unused afterwards.</p>
<p>This workaround never felt right. As an engineer, I wanted a solution that reflected the actual lifecycle of an application rather than its worst moment.</p>
<p>I opened an <a href="https://github.com/kubernetes/kubernetes/issues/83111">issue</a>  in the Kubernetes repository describing the problem and proposing an approach to adjust pod resources dynamically without restarts. The issue received little discussion but quietly accumulated interest over time (13 👍 emoji reaction). Every few months, an automation bot attempted to mark it as stale, and every time, I removed the label. This went on for nearly six years&hellip;</p>
<p>Until the release of Kubernetes 1.35 where In-Place Pod Resize feature was <a href="https://kubernetes.io/blog/2025/12/19/kubernetes-v1-35-in-place-pod-resize-ga/">marked as stable</a>.</p>
<h2 id="what-in-place-pod-resize-brings">What In-Place Pod Resize Brings</h2>
<p>In-Place Pod Resize allows Kubernetes to update CPU and memory requests and limits without restarting pods, whenever it is safe to do so. This significantly reduces unnecessary restarts caused by resource changes, leading to fewer disruptions and more reliable workloads.</p>
<p>For applications whose resource needs evolve over time, especially after startup, this feature provides a long-missing building block.</p>
<h2 id="impact-on-verticalpodautoscaler">Impact on VerticalPodAutoscaler</h2>
<p>The new <code>resizePolicy</code> field is configured at the pod spec level. While it is technically possible to change pod resources manually, doing so does not scale. In practice, this feature should be driven by a workload controller.</p>
<p>At the moment, the only controller that supports in-place pod resize is the Vertical Pod Autoscaler (VPA).</p>
<p>There are two enhancement proposals enable this behavior:</p>
<ul>
<li>
<p><a href="https://github.com/kubernetes/autoscaler/tree/455d29039bf6b1eb9f784f498f28769a8698bc21/vertical-pod-autoscaler/enhancements/4016-in-place-updates-support">AEP-4016: Support for in place updates in VPA</a> which introduces <code>InPlaceOrRecreate</code> update mode</p>
</li>
<li>
<p><a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/enhancements/7862-cpu-startup-boost">AEP-7862: CPU Startup Boost</a> which is about temporarily boosting pod by giving more cpu during pod startup. This is conceptually similar to the approach proposed in my original issue.</p>
</li>
</ul>
<p>Here is an example of Deployment and VPA using both AEP features:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">my-heavy-java-app:stable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">1000m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1024Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">2000m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">2048Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;autoscaling.k8s.io/v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VerticalPodAutoscaler</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example-vpa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">targetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;apps/v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updatePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">updateMode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;InPlaceOrRecreate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">resourcePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">containerPolicies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">containerName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;app&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">minAllowed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;250m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;512Mi&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">maxAllowed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3000m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;8192Mi&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># The CPU boosted resources can go beyond maxAllowed.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">startupBoost</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Factor&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">quantity</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="w">
</span></span></span></code></pre></div><p>With such configuration pod will have doubled cpu requests and limits during startup. During the boost period no resizing will happen.</p>
<p>Once the pod reaches the <code>Ready</code> state, the VPA controller scales CPU down to the currently recommended value.</p>
<p>After that, VPA continues operating normally, with the key difference that resource updates are applied in place whenever possible.</p>
<h2 id="limitations">Limitations</h2>
<p>Does this feature fully solve the problem described above? Only partially.</p>
<p>First, most application runtimes still impose fundamental constraints. Java and Python runtimes do not currently support resizing memory limits without a restart. This limitation exists outside of Kubernetes itself and is tracked in the OpenJDK project via <a href="https://bugs.openjdk.org/browse/JDK-8359211">an open ticket</a>.</p>
<figure class="align-center ">
    <picture>
        <source type="image/webp" srcset="/images/posts/in-place-pod-resize/runtimes-supporting-in-place-memory-resize_hu_b0b138508605bfb9.webp">
        <img loading="lazy" src="/images/posts/in-place-pod-resize/runtimes-supporting-in-place-memory-resize.png#center"
         alt="In-Place Pod Resize in Kubernetes: Dynamic Resource Management Without Restarts - Tim Allclair &amp; Mofi Rahman, Google"/>
    </picture><figcaption>
            In-Place Pod Resize in Kubernetes: Dynamic Resource Management Without Restarts - Tim Allclair &amp; Mofi Rahman, Google
        </figcaption>
</figure>
<p>Second, Kubernetes does not yet support decreasing memory limits, even with in-place Pod Resize enabled. This is a known limitation documented in the enhancement proposal for <a href="https://github.com/kubernetes/enhancements/tree/758ea034908515a934af09d03a927b24186af04c/keps/sig-node/1287-in-place-update-pod-resources#memory-limit-decreases">memory limit decreases</a>.</p>
<p>As a result, while in-place Pod Resize effectively addresses CPU-related startup spikes, memory resizing remains an open problem.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>In place Pod Resize gives a foundation for cool new features like StartupBoost and makes use of VPA more reliable. While important gaps remain, such as <a href="https://github.com/kubernetes/kubernetes/issues/135670">memory decrease support</a> and <a href="https://github.com/kubernetes/kubernetes/issues/126891">scheduling race condition</a>, this change represents a meaningful step forward.</p>
<p>For workloads with distinct startup and steady-state phases, Kubernetes is finally beginning to model reality more closely.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Contributing to Open Source: Why It Matters and How to Start</title>
      <link>https://alikhil.dev/posts/contributing-to-open-source-why-it-matters-and-how-to-start/</link>
      <pubDate>Thu, 18 Dec 2025 04:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/contributing-to-open-source-why-it-matters-and-how-to-start/</guid>
      <description>&lt;p&gt;Whether you’re curious about open source or wondering how to make a meaningful impact, this post guides you through the process. You’ll learn why contributing is important, discover the different ways to get involved, and find practical steps to take your first contribution.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Whether you’re curious about open source or wondering how to make a meaningful impact, this post guides you through the process. You’ll learn why contributing is important, discover the different ways to get involved, and find practical steps to take your first contribution.</p>
<h2 id="why">Why?</h2>
<p>First of all, why should you contribute to open source?</p>
<h3 id="giving-back">Giving back</h3>
<p>Everyone, from freelance engineers to Big Tech companies and even governments, uses Open Source Software (OSS). Some use it less, others more, but almost everyone depends on it in one way or another. Most of us are <em>consumers</em> of open source.</p>
<p>Contributing to OSS means giving something back. This is especially important given the many cases of projects (e.g <a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/#history-and-challenges">nginx-ingress</a>, <a href="https://github.com/external-secrets/external-secrets/issues/5084">external-secrets</a>) being deprecated due to maintainer burnout, lack of community support, or overwhelming workloads.</p>
<p>It is true that some OSS projects are backed by large companies and maintained by engineers who are paid to work on them. However, the half of open source projects are still maintained by individuals in their spare time for free (<a href="https://www.linuxfoundation.org/blog/open-source-maintainers-what-they-need-and-how-to-support-them">source</a>).</p>
<p>In this sense, contributing to OSS can be seen as a form of digital volunteering. Some companies (<a href="https://www.linkedin.com/posts/matthieublumberg_when-running-a-platform-using-open-source-activity-7382301443698892800-3St-?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABsF7nEBwwglPayi0aSCflAk2mD2nv-HVCA">Criteo</a>, <a href="https://www.futurice.com/blog/year-2015-in-company-sponsored-open-sourcehttps://www.futurice.com/blog/year-2015-in-company-sponsored-open-source">Futurice</a>) even offer paid volunteer time (VPTO) specifically for open-source contributions.</p>
<h3 id="learning">Learning</h3>
<p>Another strong reason to contribute to open source is personal growth.</p>
<p>Every contribution becomes a learning opportunity because it places you inside a real, production-grade codebase rather than a controlled tutorial environment. You learn how projects are structured, how architectural decisions are made, how backward compatibility is maintained, and how trade-offs are handled in practice. Often this means working with unfamiliar tools, languages, or ecosystems, which naturally expands your technical range.</p>
<p>At the same time, open source strongly develops communication and collaboration skills. Issues and pull requests force you to articulate problems clearly, propose solutions in a way others can evaluate, and explain <em>why</em> a particular approach makes sense. Feedback from maintainers and contributors exposes you to different perspectives and constraints, requiring you to adapt, clarify, and sometimes rethink your ideas.</p>
<p>Because most collaboration happens asynchronously and in writing, you also improve your ability to communicate precisely and concisely. Over time, this structured, public collaboration sharpens how you discuss technical topics, handle reviews, and work effectively with distributed teams. These are the same skills required in modern engineering organizations, making open source a highly practical training ground.</p>
<h3 id="networking-and-professional-reputation">Networking and professional reputation</h3>
<p>Open source contribution naturally leads to networking, even if you are not actively trying to “network.” By participating in issues, code reviews, and pull requests, you start interacting with maintainers and contributors from different companies, countries, and levels of seniority. Over time, these repeated interactions build familiarity and trust. People begin to recognize your name, your areas of expertise, and the quality of your work.</p>
<p>Regular contributions can turn these lightweight interactions into professional relationships. Maintainers may invite you to collaborate more closely, grant you additional responsibilities, or even recommend you for roles on their teams. In many cases, job opportunities arise not from formal applications, but from someone already knowing how you work.</p>
<p>Another important, and often underestimated, benefit of open source contribution is visibility. Most professional work is hidden behind NDAs and internal repositories, making it difficult to demonstrate your real impact. Open source work, on the other hand, is public by default. Your commits, pull requests, discussions, and design decisions are all visible and attributable to you.</p>
<p>This public track record allows you to clearly show not only <em>what</em> you built or improved, but also <em>how</em> you collaborate, communicate, and respond to feedback. For recruiters and hiring managers, this is far more convincing than a list of skills on a résumé. In practice, open source contributions often function as a living portfolio and a long-term investment in your professional reputation.</p>
<h2 id="how">How?</h2>
<p>Now that you understand why contributing matters, let’s look at how to get started.</p>
<h3 id="the-natural-way">The Natural way</h3>
<p>You might think that contributing to open source requires skills you do not have, a brilliant idea for a new library, or deep expertise in a specific domain. These beliefs often make OSS contribution feel unreachable.</p>
<p>That is not the case.</p>
<p>You do not need a revolutionary idea or special credentials. If you are an engineer who already writes code, you are capable of contributing.</p>
<p>Here is a simple approach: the next time you are solving a problem using an open source tool and notice that it does not work as expected (a bug) or lacks a feature you need, do not immediately abandon the tool. Use your skills, and an LLM if needed, to investigate and try to fix the issue.</p>
<p>If you succeed, open a pull request to the upstream repository. If you do not, create an issue and share your findings. That is still a contribution, and you will have learned something in the process.</p>
<h3 id="good-first-issue">Good first issue</h3>
<p>If everything you use works perfectly and you do not notice any gaps, you can take a more deliberate approach.</p>
<p>Make a list of projects you like, use, or want to learn more about. Browse their open issues and look for ones labeled “good first issue” or similar. Pick something that matches your current skill level and try to tackle it.</p>
<p>If your list is short or you cannot find suitable issues, there are also curated lists of projects actively looking for contributors.</p>
<ul>
<li><a href="https://goodfirstissue.dev/">https://goodfirstissue.dev/</a></li>
<li><a href="https://forgoodfirstissue.github.com/">https://forgoodfirstissue.github.com/</a></li>
<li><a href="https://goodfirstissues.com/">https://goodfirstissues.com/</a></li>
</ul>
<h2 id="summary">Summary</h2>
<p>Open source is great, but keeping it that way requires people to contribute.
It is not hard or unreachable. Anyone can do it, and the community needs more people like you.
Start small, pick a project you love, and take your first step</p>]]></content:encoded>
    </item>
    
    <item>
      <title>kubectl-find - UNIX-find-like plugin to find resources and perform action on them</title>
      <link>https://alikhil.dev/posts/kubectl-find-plugin-50-stars-anniversary/</link>
      <pubDate>Fri, 17 Oct 2025 19:27:24 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/kubectl-find-plugin-50-stars-anniversary/</guid>
      <description>&lt;p&gt;Recently, I have developed a plugin for &lt;code&gt;kubectl&lt;/code&gt; inspired by UNIX &lt;code&gt;find&lt;/code&gt; utility to find and perform action on resources. And few days ago number of stars in the repo reached 50! I think it&amp;rsquo;s a good moment to tell more about the project.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Recently, I have developed a plugin for <code>kubectl</code> inspired by UNIX <code>find</code> utility to find and perform action on resources. And few days ago number of stars in the repo reached 50! I think it&rsquo;s a good moment to tell more about the project.</p>
<h2 id="the-problem">The problem</h2>
<p>As engineer who works with kubernetes everyday I use kubectl a lot. Actually, more than 50% of my terminal history commands are related to kubernetes. Here is a top 10 commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">1	<span class="m">2079</span>  46.7611%    kubectl
</span></span><span class="line"><span class="cl">2	<span class="m">425</span>   9.55915%    git
</span></span><span class="line"><span class="cl">3	<span class="m">324</span>   7.28745%    helm
</span></span><span class="line"><span class="cl">4	<span class="m">156</span>   3.50877%    <span class="nb">cd</span>
</span></span><span class="line"><span class="cl">5	<span class="m">146</span>   3.28385%    ssh
</span></span><span class="line"><span class="cl">6	<span class="m">130</span>   2.92398%    kctx
</span></span><span class="line"><span class="cl">7	<span class="m">114</span>   2.5641%     kns
</span></span><span class="line"><span class="cl">8	<span class="m">80</span>    1.79937%    gcloud-auth
</span></span><span class="line"><span class="cl">9	<span class="m">78</span>    1.75439%    curl
</span></span><span class="line"><span class="cl">10	<span class="m">66</span>    1.48448%    docker
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><blockquote>
<p>Run <a href="https://superuser.com/a/250230">this command</a> if you are curious what about yours the most popular commands in terminal history.</p>
</blockquote>
<p>I use kubectl to check status of the pods, delete orphaned resources, trigger sync on <code>ExternalSecrets</code> and much more. When I realized half my terminal history was just kubectl commands, I thought — there must be a better way to find things in Kubernetes without chaining pipes with <code>grep</code> / <code>awk</code> / <code>xargs</code>. And I imagined how nice it would be to have a UNIX <code>find</code>-like tool — something that lets you search for exactly what you need in the cluster and then perform actions directly on the matching resources. I searched for a krew plugin like this but there was not any. For that reason, I decided to develop <a href="https://github.com/alikhil/kubectl-find">one</a>!</p>
<h2 id="development">Development</h2>
<p>I used <a href="https://github.com/kubernetes/sample-cli-plugin">sample-cli-plugin</a> as a starting point. Its clean repository structure and straightforward design make it a great reference for working with the Kubernetes API. Additionally, it allows easy reuse of the extensive Kubernetes client libraries.</p>
<p>Almost everything in the Kubernetes ecosystem is written in Go, and this plugin is no exception — which is great, as it allows building binaries for a wide range of CPU architectures and operating systems.</p>
<h2 id="features-overview">Features overview</h2>
<h3 id="filters">Filters</h3>
<h4 id="find-by-resource-name-using-regex">Find by resource name using regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods -r dev
</span></span></code></pre></div><h4 id="find-pods-with-restarted-containers">Find pods with restarted containers</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --restarted
</span></span></code></pre></div><h4 id="find-pods-with-image-matching-regex">Find pods with image matching regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --image bitnami
</span></span></code></pre></div><h4 id="find-pods-by-status">Find pods by status</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --status Pending
</span></span></code></pre></div><h4 id="find-pods-running-on-nodes-matching-regex">Find pods running on nodes matching regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --node <span class="s1">&#39;*spot&#39;</span>
</span></span></code></pre></div><h4 id="find-any-resource-by-custom-condition">Find any resource by custom condition</h4>
<p>Use <code>jq</code> filter to find any resource by any custom condition. <code>kubectl-find</code> uses <a href="https://github.com/itchyny/gojq">gojq</a> implementation of <code>jq</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># find Services with trafficDistribution set to PreferClose</span>
</span></span><span class="line"><span class="cl">kubectl fd svc -j <span class="s1">&#39;.spec.trafficDistribution == &#34;PreferClose&#34;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># find pods with unset nodeSelector</span>
</span></span><span class="line"><span class="cl">kubectl fd pods -j <span class="s1">&#39;.spec.nodeSelector == null&#39;</span> -A
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># find pods with undefined resources</span>
</span></span><span class="line"><span class="cl">kubectl fd pods -j <span class="s1">&#39;any( .spec.containers[]; .resources == {} )&#39;</span> -A
</span></span></code></pre></div><h3 id="actions">Actions</h3>
<p>By default, <code>fd</code> will print found resources to Stdout. However, there flags that you can provide to perform action on found resources:</p>
<ul>
<li><code>--delete</code> - to delete them</li>
<li><code>--patch</code> - to patch with provided JSON</li>
<li><code>--exec</code> - to run command on pods</li>
</ul>
<h2 id="getting-started">Getting started</h2>
<p>Use <a href="https://krew.sigs.k8s.io/">krew</a> to install the plugin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">krew install fd
</span></span></code></pre></div><h2 id="whats-next">What’s next</h2>
<p>I’m currently working on adding:</p>
<ul>
<li>JSON/YAML output format</li>
<li>More filters</li>
<li>Saved queries</li>
</ul>
<p>If you’re tired of writing long <code>kubectl | grep | xargs</code> chains, give <code>kubectl fd</code> a try — it’s already saved me countless keystrokes.</p>
<p>Check out the repo ⭐ <a href="https://github.com/alikhil/kubectl-find">github.com/alikhil/kubectl-find</a>
and share your ideas or issues — I’d love to hear how you use it!</p>
<p><a href="https://github.com/alikhil/kubectl-find">
  <img loading="lazy" src="https://api.star-history.com/svg?repos=alikhil/kubectl-find&amp;type=date&amp;legend=bottom-right" alt="Star History Chart"  /></a></p>]]></content:encoded>
    </item>
    
    <item>
      <title>The Simple Habit That Saves My Evenings</title>
      <link>https://alikhil.dev/posts/the-simple-habit-that-saves-my-evenings/</link>
      <pubDate>Sun, 28 Sep 2025 19:08:35 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/the-simple-habit-that-saves-my-evenings/</guid>
      <description>&lt;p&gt;As a software engineer, I often work on big tasks that require hours of continuous and focused work. However, we have plenty of meetings, colleagues asking us something in Slack, and lunch breaks. Add a colleague who comes to you and calls you for a cup of coffee if you work from the office.&lt;/p&gt;
&lt;p&gt;And usually, we don’t really have such a luxury as &lt;strong&gt;hours&lt;/strong&gt; of uninterrupted time.&lt;/p&gt;
&lt;p&gt;Nevertheless, sometimes we catch &lt;a href=&#34;https://www.amazon.com/Flow-Psychology-Experience-Perennial-Classics/dp/0061339202&#34;&gt;&lt;strong&gt;the flow&lt;/strong&gt;&lt;/a&gt; of productive and focused work at the end of the workday. Imagine you come up with an elegant solution to a problem you’ve been tackling all day, or maybe even the whole past week. You can’t wait to implement and test your solution. And of course, you are so driven by your idea that you decide to continue working despite your working day being over. “20 minutes more and I will finish it,” you think. Obviously, this is not the case; some edge cases and new issues will inevitably arise. You come to your senses only 2–3 hours later—tired, hungry, demotivated, and still struggling with your problem. You just wasted your evening, with nothing to show for it. Worse, you overworked and didn’t recover that night. Thus, you were already exhausted when you started working.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>As a software engineer, I often work on big tasks that require hours of continuous and focused work. However, we have plenty of meetings, colleagues asking us something in Slack, and lunch breaks. Add a colleague who comes to you and calls you for a cup of coffee if you work from the office.</p>
<p>And usually, we don’t really have such a luxury as <strong>hours</strong> of uninterrupted time.</p>
<p>Nevertheless, sometimes we catch <a href="https://www.amazon.com/Flow-Psychology-Experience-Perennial-Classics/dp/0061339202"><strong>the flow</strong></a> of productive and focused work at the end of the workday. Imagine you come up with an elegant solution to a problem you’ve been tackling all day, or maybe even the whole past week. You can’t wait to implement and test your solution. And of course, you are so driven by your idea that you decide to continue working despite your working day being over. “20 minutes more and I will finish it,” you think. Obviously, this is not the case; some edge cases and new issues will inevitably arise. You come to your senses only 2–3 hours later—tired, hungry, demotivated, and still struggling with your problem. You just wasted your evening, with nothing to show for it. Worse, you overworked and didn’t recover that night. Thus, you were already exhausted when you started working.</p>
<p><img loading="lazy" src="/posts/the-simple-habit-that-saves-my-evenings/images/rick-n-morty.jpg" type="" alt="Rick and morty 20 minutes adventure"  /></p>
<p>You can imagine what will follow next. Nothing good, really.</p>
<p><img loading="lazy" src="/posts/the-simple-habit-that-saves-my-evenings/images/rick-n-morty-end.jpg" type="" alt="Rick and morty 20 minutes adventure"  /></p>
<p>I remember this happening back when I worked at a fast-growing startup, KazanExpress, while living in <a href="https://en.wikipedia.org/wiki/Innopolis">Innopolis</a>. Our office was buzzing with energy, and the pace was intense—we often pushed ourselves late into the night. One evening, I felt like I had finally cracked a tricky part of our infrastructure. I thought, “Just 20 more minutes and I’ll finish it.” Of course, those 20 minutes stretched into well over three hours. By the time I left the office, I was tired, hungry, and frustrated—without any real progress to show. The next morning, walking back to the office, I realized how drained I already felt before even starting work. That was when it became clear to me: it’s better to stop, write down the next steps, and come back with a fresh head.</p>
<p>Of course, some might argue, &ldquo;But you are considering only a negative scenario; one could really finish that job in 20 minutes and go home happy&hellip;&rdquo;. Sure, but I think this risk is not worth it. Instead, I would suggest doing another thing.</p>
<p>Rather than trying to complete your task in 20 minutes, take this time to write down your thoughts, and a step-by-step action plan of what you think you need to do to finish your task. Then go home. Rest. A feeling of incompleteness will motivate you to come back and finalize your work the next day. Only you will be full of energy, together with a settled plan. No doubt you’ll accomplish your task before lunch.</p>
<p>Writing down the next steps helps to clear your mind after a  workday. You write and forget about your work until the next morning.</p>
<p>As a bonus, there is a chance that new, better ideas will come while you sleep or rest.</p>
<p>I have been using this trick for more than 5 years now, and it helps me to keep my work and life balanced.
Here are the two main ideas of it:</p>
<ul>
<li>Don’t overwork</li>
<li>Write down the next steps before finishing your workday</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>OAuth2-proxy: protect services in kubernetes</title>
      <link>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</link>
      <pubDate>Tue, 05 Aug 2025 21:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</guid>
      <description>&lt;p&gt;The original post wrote &lt;a href=&#34;https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/&#34;&gt;about oauth2-proxy&lt;/a&gt; over seven years ago was quite popular at the time and attracted a lot of organic traffic to my blog, which still benefits my SEO today. Since the tutorial had become outdated, I decided to rewrite it.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The original post wrote <a href="https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/">about oauth2-proxy</a> over seven years ago was quite popular at the time and attracted a lot of organic traffic to my blog, which still benefits my SEO today. Since the tutorial had become outdated, I decided to rewrite it.</p>
<h2 id="what-we-have">What we have</h2>
<p>We have a Kubernetes cluster with several web services deployed for internal use.</p>
<h2 id="what-we-want-to-achieve">What we want to achieve</h2>
<p>We want to expose our internal web services to the Internet, but restrict access by requiring authorization. Access should be granted only to users authenticated through our Identity Provider (such as Google, GitHub, Keycloak, etc.).</p>
<h2 id="assumptions">Assumptions</h2>
<p>For simplicity, let&rsquo;s assume that both <a href="https://github.com/kubernetes/ingress-nginx">ingress-nginx</a> and <a href="https://cert-manager.io/">cert-manager</a> are already deployed in the cluster.</p>
<p>I will use <a href="https://github.com/pocket-id/pocket-id">Pocket ID</a> as Identity Provider in this tutorial. Configuration slightly differs for different providers. Check the <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/">official documentation</a> for your provider.</p>
<p>For the examples in this guide, I’ll use my <code>alikhil.dev</code> domain:</p>
<p>– <code>pocket-id.k8s.alikhil.dev</code> - will be used for Pocket ID</p>
<ul>
<li>
<p><code>k8s.alikhil.dev</code> - will be used for oauth2-proxy. I recommend to have higher domain for oauth2-proxy service for easier cookie setup.</p>
</li>
<li>
<p><code>*.k8s.alikhil.dev</code> - reserved for services deployed for internal usage</p>
</li>
</ul>
<h2 id="preparation">Preparation</h2>
<h3 id="dns">DNS</h3>
<p>I have added two DNS records:</p>
<ol>
<li><code>A</code> record for k8s.alikhil.dev pointing to ingress-nginx <code>LoadBalancer</code> IP address in the cluster (<code>kubectl get svc -n ingress-nginx | grep LoadBalancer</code>)</li>
<li><code>CNAME</code> record for <code>*.k8s.alikhil.dev</code> pointing to <code>k8s.alikhil.dev</code></li>
</ol>
<h3 id="install-pocket-id">Install Pocket ID</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add anza-labs https://anza-labs.github.io/charts
</span></span><span class="line"><span class="cl">helm upgrade --install pocket-id anza-labs/pocket-id -f ./values/pocket-id.yaml
</span></span></code></pre></div><details>
    <summary>Values for pocket-id</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Specifies whether ingress should be enabled.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress class name.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Annotations to add to the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress host configuration.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- List of TLS configurations for the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span>- <span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="configure-pocketid">Configure PocketID</h3>
<p>Go to <a href="https://pocket-id.k8s.alikhil.dev/signup/setup">https://pocket-id.k8s.alikhil.dev/signup/setup</a> and set initial configuration for Pocket ID.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-setup.png" type="" alt="Initial setup page"  /></p>
<p>Then add your passkey.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-pass.png" type="" alt="Add passkey"  /></p>
<p>Create <strong>developers</strong> group and add yourself to the list of members.
<img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-groups.png" type="" alt="Create group"  /></p>
<p>After that, go to <strong>OIDC clients</strong> page and create one for oauth2-proxy. Set proper callback url.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-client.png" type="" alt="PocketID client creation"  /></p>
<p>Save generated <strong>Client ID</strong> and <strong>Client Secret</strong> for later use.</p>
<h2 id="installing-oauth2-proxy">Installing oauth2-proxy</h2>
<p>I am using raw k8s secrets in this tutorial, but I highly recommend storing secrets in Vault or similar services and use <a href="https://external-secrets.io/latest/">External Secretes Operator</a> to deliver them to kubernetes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create secret generic oauth2-proxy-secrets --from-literal<span class="o">=</span>client-id<span class="o">=</span><span class="nv">$CLIENT_ID</span> --from-literal<span class="o">=</span>client-secret<span class="o">=</span><span class="nv">$CLIENT_SECRET</span> --from-literal<span class="o">=</span>cookie-secret<span class="o">=</span><span class="k">$(</span>openssl rand -base64 <span class="m">32</span> <span class="p">|</span> head -c <span class="m">32</span> <span class="p">|</span> base64<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
</span></span><span class="line"><span class="cl">helm install oauth2-proxy oauth2-proxy/oauth2-proxy -f values/oauth2-proxy.yaml
</span></span></code></pre></div><details>
    <summary>Adjust domains in values for oauth2-proxy</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Oauth client configuration specifics</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cookieName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;general-oauth2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default configuration, to be overridden</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">configFile</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    email_domains = [ &#34;*&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    upstreams = [ &#34;file:///dev/null&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_provider_button = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    allowed_groups = [ &#34;developers&#34;, &#34;admins&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_secure = false
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_domains = [&#34;.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34;]
</span></span></span><span class="line"><span class="cl"><span class="sd">    whitelist_domains = [ &#34;*.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_samesite = &#34;lax&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_per_request = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_expire = &#34;15m&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_access_token = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider = &#34;oidc&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider_display_name = &#34;PocketID&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    reverse_proxy = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    scope = &#34;openid profile email groups&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    session_store_type = &#34;redis&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_xauthrequest = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    silence_ping_logging = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_auth_preflight = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_upstream_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    insecure_oidc_allow_unverified_email = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    oidc_issuer_url = &#34;https://pocket-id.k8s.alikhil.dev&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    redirect_url = &#34;https://k8s.alikhil.dev/oauth2/callback&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    # to reduce log amount
</span></span></span><span class="line"><span class="cl"><span class="sd">    request_logging = false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Only used if API capabilities (networking.k8s.io/v1) allow it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Used to create an Ingress record.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/cors-allow-origin</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/enable-cors</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Secrets must be manually created in the namespace.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Configure the session storage type, between cookie and redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sessionStorage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Can be one of the supported session storage cookie|redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">passwordKey</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;redis-password&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">clientType</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;standalone&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Enables and configure the automatic deployment of the redis subchart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># provision an instance of the redis sub-chart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">architecture</span><span class="p">:</span><span class="w"> </span><span class="l">standalone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">master</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">128Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1Gi</span><span class="w">
</span></span></span></code></pre></div></details>
<h2 id="testing">Testing</h2>
<h3 id="install-whoami">Install whoami</h3>
<p>To check oauth2-proxy we need a dummy service. I will use <a href="https://artifacthub.io/packages/helm/cowboysysop/whoami">whoami</a> helm chart for this.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm repo add cowboysysop https://cowboysysop.github.io/charts/
</span></span><span class="line"><span class="cl">helm install whoami cowboysysop/whoami
</span></span></code></pre></div><details>
    <summary>Values for whoami helm chart</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># put oauth2-proxy domain here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># service-name.namespace-name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">whoami-cert</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="perform-test">Perform test</h3>
<p>Go to whoami url and check if oauth2-proxy redirects you to Pocket ID like in the demo:</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/demo.gif" type="" alt="Demo"  /></p>
<h2 id="takeaways">Takeaways</h2>
<p>Later, when you need to protect any service in Kubernetes with oauth2-proxy, you simply need to add two annotations to your Ingress resource:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Note on cable management for standing desk</title>
      <link>https://alikhil.dev/posts/cable-management-for-standing-desk/</link>
      <pubDate>Fri, 11 Jul 2025 20:00:00 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/cable-management-for-standing-desk/</guid>
      <description>&lt;p&gt;Hey!
Eventually, my desk got pretty messy with all the cables and other stuff piled up on it. Also, my cats started playing with and gnawing on wires. So, I decided to reorganize everything on my desk. Check it out!&lt;/p&gt;
&lt;!---more---&gt;
&lt;p&gt;I&amp;rsquo;ve got a medium-sized table, about 120 by 80 cm. It&amp;rsquo;s wide (deep) but not too long. I need to keep everything in order and clean so I can work comfortably.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hey!
Eventually, my desk got pretty messy with all the cables and other stuff piled up on it. Also, my cats started playing with and gnawing on wires. So, I decided to reorganize everything on my desk. Check it out!</p>
<!---more--->
<p>I&rsquo;ve got a medium-sized table, about 120 by 80 cm. It&rsquo;s wide (deep) but not too long. I need to keep everything in order and clean so I can work comfortably.</p>
<p>Here&rsquo;s what my setup looked like before I made all the changes I mentioned.</p>
<p><img loading="lazy" src="/images/posts/cable-management/before-front.jpg" type="" alt="setup before reorganization"  /></p>
<p>As you can see, I&rsquo;ve got a work laptop, a Mac mini, a monitor on its own stand, a journal, AirPods, and a few other things. I also bought a USB switch at some point because I got tired of plugging and unplugging my webcam manually in my <a href="https://alikhil.dev/posts/how-to-use-3-computers-without-kvm/">KVM-less setup</a>.</p>
<h2 id="heres-what-ive-done">Here&rsquo;s what I&rsquo;ve done</h2>
<ul>
<li>I mounted the monitor to the arm, which freed up space below the screen.</li>
<li>I set up my Mac mini under the table.</li>
<li>I&rsquo;ve got my MacBook pro mounted under the table.</li>
<li>I installed the USB hub into the monitor arm. It&rsquo;s a solid connection point, but the switch button on it is a bit uncomfortable to press. So,</li>
<li>Put the extended switch button under the table, close to the chair.</li>
<li>There&rsquo;s a mounted headphones holder under the table.</li>
<li>The cables that ran next to each other were routed through the sleeves.</li>
<li>Bind cables together with hook and loops tie</li>
</ul>
<h2 id="list-of-things-that-ive-used">List of things that I&rsquo;ve used</h2>
<ul>
<li><a href="https://a.aliexpress.com/_Ewdgm1c">The cable sleeve</a> is super handy for binding the cables together!</li>
<li><a href="https://a.aliexpress.com/_EvYuv3M">This cable sleeve</a> is a bit pricey, but it lets you pull cables out from the middle without damaging it.</li>
<li><a href="https://a.aliexpress.com/_EyRlHBM">MacBook under desk mount support</a>. Antiscratch stickers came loose at the second day. So don’t recommend this one.</li>
<li><a href="https://a.aliexpress.com/_EuW6yMS">Mac Mini under desk support</a></li>
<li><a href="https://a.aliexpress.com/_EJvesWI">Hook and loops tie</a></li>
<li><a href="https://a.aliexpress.com/_EQ3DlES">Cable clips</a></li>
<li><a href="https://www.flexispot.es/monitor-mount-fits-10-27.html">Adjustable monitor support arm</a></li>
</ul>
<h2 id="what-ive-got">What I&rsquo;ve got</h2>
<style>
td, th {
   border: none!important;
}
</style>
<table>
  <thead>
      <tr>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><img loading="lazy" src="/images/posts/cable-management/before-front.jpg" type="" alt="front picture before"  /></td>
          <td><img loading="lazy" src="/images/posts/cable-management/after-front.jpg" type="" alt="front picture after"  /></td>
      </tr>
      <tr>
          <td><img loading="lazy" src="/images/posts/cable-management/before-under-table.jpg" type="" alt="under table picture before"  /></td>
          <td><img loading="lazy" src="/images/posts/cable-management/after-under-table.jpg" type="" alt="under table picture after"  /></td>
      </tr>
      <tr>
          <td><img loading="lazy" src="/images/posts/cable-management/before-side.jpg" type="" alt="side picture before"  /></td>
          <td><img loading="lazy" src="/images/posts/cable-management/after-side.jpg" type="" alt="side picture after"  /></td>
      </tr>
  </tbody>
</table>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to Use 3 Computers with One Monitor, Keyboard and Mouse – Without a KVM Switch</title>
      <link>https://alikhil.dev/posts/how-to-use-3-computers-without-kvm/</link>
      <pubDate>Mon, 23 Jun 2025 15:00:00 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/how-to-use-3-computers-without-kvm/</guid>
      <description>&lt;p&gt;Hi there! Today, I want to share how I organize my three-computer setup (MacBook Air, Mac mini, and Raspberry Pi) without a KVM switch, using a single keyboard, mouse, and monitor.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hi there! Today, I want to share how I organize my three-computer setup (MacBook Air, Mac mini, and Raspberry Pi) without a KVM switch, using a single keyboard, mouse, and monitor.</p>
<h2 id="my-devices">My devices</h2>
<h3 id="computers">Computers</h3>
<ul>
<li>Macbook Air M2 14&quot; – work Laptop.</li>
<li>Mac Mini M1 – my personal computer.</li>
<li><a href="https://www.clockworkpi.com/uconsole">ClockworkPi uConsole</a>, based on <a href="https://www.raspberrypi.com/products/compute-module-4/">RPI CM4</a> is a portable device for Linux tinkering.</li>
</ul>
<h3 id="peripheral-devices">Peripheral devices</h3>
<ul>
<li>The <a href="https://www.amazon.com/UGREEN-Bluetooth-Ergonomic-Prevention-Compatible/dp/B0CM6FSPY3">UGREEN Vertical Wireless</a> - can connect up to three devices: Two via Bluetooth and one via a 2.4GHz USB dongle.</li>
<li>The <a href="https://www.keychron.com/products/keychron-k15-max-alice-layout-qmk-wireless-custom-mechanical-keyboard?srsltid=AfmBOoo83yQiVI4ctH1noZzohk5v1fqAPMx_RbL86q7PxBWGvmSdUtw1">Keychrone K15 Max</a> allows me to connect up to three devices via Bluetooth.</li>
<li>The <a href="https://www.dell.com/es-es/shop/monitor-dell-27-4k-uhd-con-usb-c-s2722qc/apd/210-bbrq/monitores-y-accesorios">Monitor Dell 27 4K UHD con USB-C (S2722QC)</a> with two HDMI inputs and one USB-C port which allows me to connect it up to three devices.</li>
</ul>
<p>As you can see, all peripheral devices could connect to all my computers. At least in theory.</p>
<h2 id="connecting-devices">Connecting devices</h2>
<p>Historically my devices were numbered as follows:</p>
<ol>
<li>uConsole</li>
<li>Macbook Air</li>
<li>Mac Mini</li>
</ol>
<h3 id="keyboard">Keyboard</h3>
<p>The keyboard connection scheme is straightforward: uConsole connects via Bluetooth to the first device, the MacBook Air, as the second device, and the Mac Mini as the third device.</p>
<p>Later, I can use the <code>Fn + 1/2/3</code> hotkey to switch the keyboard between devices.</p>
<p><img loading="lazy" src="/images/posts/kvm/kvm-keyboard.png" type="" alt="Switching devices for keyboard"  /></p>
<h3 id="mouse">Mouse</h3>
<p>The first connection of the Ugreen mouse is reserved for the 2.4 GHz USB dongle. I plugged it into my uConsole, and then my MacBook Air and Mac mini connected as the second and third devices, respectively.</p>
<p>To switch devices with the mouse, press the red button on the bottom.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Dk_LNbkvECs?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<h3 id="monitor">Monitor</h3>
<p>The MacBook Air is connected to the USB-C port, which charges the computer.</p>
<p>The Mac Mini connects via HDMI-1, and the uConsole connects via HDMI-2.</p>
<h4 id="easiest-way-to-switch-devices">Easiest way to switch devices</h4>
<p>The easiest way to switch devices on the monitor is to press the small, round button in the bottom right corner to open the settings menu.</p>
<p><img loading="lazy" src="/images/posts/kvm/kvm-monitor-2.jpg" type="" alt="Bottom corner buttons"  /></p>
<p>Then, choose the desired port.</p>
<p><img loading="lazy" src="/images/posts/kvm/kvm-monitor.jpg" type="" alt="Monitor menu"  /></p>
<p>However, <strong>&ldquo;easiest&rdquo;</strong> does not mean <strong>&ldquo;most comfortable&rdquo;</strong>. Changing the monitor&rsquo;s input source this way requires pressing these small buttons several times. You also need to keep track of which devices are connected to HDMI-1 and HDMI-2.</p>
<h4 id="the-comfortable-way-to-switch-devices">The comfortable way to switch devices</h4>
<p>Instead of pressing buttons on the monitor and navigating its menu, I could press a hotkey on my keyboard to trigger the monitor to change input sources via its API (Display Data Channel/Command Interface Standard (DDC/CI).).</p>
<p>I have explained in detail how one could do it on a Mac in my another post - <a href="https://alikhil.dev/posts/monitor-input-source-control-mac/">Monitor input source control on Mac</a>.</p>
<p>I won&rsquo;t go into detail here. I&rsquo;ll just say that on my Mac Mini and MacBook Air, I have configured the following:</p>
<p><strong>Mac Mini (#3)</strong></p>
<ul>
<li>On <code>CMD + F1</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/raycast/switch-monitor-to-uconsole.sh">uConsole</a></li>
<li>On <code>CMD + F2</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/raycast/switch-monitor.sh">Macbook Air</a></li>
</ul>
<p><strong>Macbook Air (#2)</strong></p>
<ul>
<li>On <code>CMD + F1</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/raycast/switch-monitor-to-uconsole.sh">uConsole</a></li>
<li>On <code>CMD + F3</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/raycast/switch-monitor.sh">Mac mini</a></li>
</ul>
<p>For <strong>uConsole Linux machine (#1)</strong> I used <a href="https://github.com/rockowitz/ddcutil">ddcutil</a> and configured the following hotkeys:</p>
<ul>
<li><code>Ctrl + F2</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/linux/switch-to-macbook.sh">Macbook Air</a></li>
<li><code>Ctrl + F3</code> triggers switch to <a href="https://github.com/alikhil/configs/blob/master/linux/switch-to-mac-mini.sh">Mac Mini</a></li>
</ul>
<h2 id="switch-sequence">Switch sequence</h2>
<p>Once all the devices are connected and the hotkeys are configured, the following sequence of actions is needed to switch to device <code>#N</code> (where N is 1, 2, or 3):</p>
<ol>
<li>Switch the monitor input source by pressing <code>CMD + F#N</code>.</li>
<li>Switch the keyboard device by pressing <code>Fn + #N</code>.</li>
<li>Switch the mouse by pressing red button on the bottom of it until the indicator is under <code>#N</code> is blinking.</li>
</ol>
<p>For example, let&rsquo;s assume I am currently using my third device – Mac Mini, to switch to Macbook Air (#2) I do:</p>
<ol>
<li>Press <code>CMD + F2</code></li>
<li>Press <code>Fn + 2</code></li>
<li>Press red button on the bottom of the mouse until indicator under 2 is on.</li>
</ol>
<p>Here is a demo video:</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/NgVESzIzpw4?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<h2 id="pros-and-cons">Pros and Cons</h2>
<h3 id="pros">Pros</h3>
<ul>
<li>I don&rsquo;t need a special KVM switch to switch between devices.</li>
<li>I use my keyboard to switch (for two-thirds of the KVM).</li>
</ul>
<h3 id="cons">Cons</h3>
<ul>
<li>Switching the mouse takes longer since there is only one way iteration over the connected devices.</li>
<li>Three actions are needed to switch all peripheral devices, as opposed to one action with a KVM.</li>
<li>I still need to manually reconnect other peripheral devices, e.g. web camera.</li>
</ul>
<h2 id="final-thoughts">Final thoughts</h2>
<p>It has some limitations, but if you have a keyboard and mouse that support multiple connected devices, as well as a monitor, you won&rsquo;t need a special KVM switch to switch between computers.</p>
<p>Thank you for reading this!</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Why Graceful Shutdown Matters in Kubernetes</title>
      <link>https://alikhil.dev/posts/graceful-shutdown/</link>
      <pubDate>Sun, 01 Jun 2025 20:31:06 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/graceful-shutdown/</guid>
      <description>&lt;p&gt;Have you ever deployed a new version of your app in Kubernetes and noticed errors briefly spiking during rollout? Many teams do not even realize this is happening, especially if they are not closely monitoring their error rates during deployments.&lt;/p&gt;
&lt;p&gt;There is a common misconception in the Kubernetes world that bothers me. The official Kubernetes &lt;a href=&#34;https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/&#34;&gt;documentation&lt;/a&gt; and most guides claim that &amp;ldquo;if you want zero downtime upgrades, just use rolling update mode on deployments&amp;rdquo;. I have learned the hard way that this simply it is not true - rolling updates alone are &lt;strong&gt;NOT enough&lt;/strong&gt; for true zero-downtime deployments.&lt;/p&gt;
&lt;p&gt;And it is not just about deployments. Your pods can be terminated for many other reasons: scaling events, node maintenance, preemption, resource constraints, and more. Without proper graceful shutdown handling, any of these events can lead to dropped requests and frustrated users.&lt;/p&gt;
&lt;p&gt;In this post, I will share what I have learned about implementing proper graceful shutdown in Kubernetes. I will show you exactly what happens behind the scenes, provide working code examples, and back everything with real test results that clearly demonstrate the difference.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Have you ever deployed a new version of your app in Kubernetes and noticed errors briefly spiking during rollout? Many teams do not even realize this is happening, especially if they are not closely monitoring their error rates during deployments.</p>
<p>There is a common misconception in the Kubernetes world that bothers me. The official Kubernetes <a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/">documentation</a> and most guides claim that &ldquo;if you want zero downtime upgrades, just use rolling update mode on deployments&rdquo;. I have learned the hard way that this simply it is not true - rolling updates alone are <strong>NOT enough</strong> for true zero-downtime deployments.</p>
<p>And it is not just about deployments. Your pods can be terminated for many other reasons: scaling events, node maintenance, preemption, resource constraints, and more. Without proper graceful shutdown handling, any of these events can lead to dropped requests and frustrated users.</p>
<p>In this post, I will share what I have learned about implementing proper graceful shutdown in Kubernetes. I will show you exactly what happens behind the scenes, provide working code examples, and back everything with real test results that clearly demonstrate the difference.</p>
<h2 id="the-problem-hidden-errors-during-pod-termination">The Problem: Hidden Errors During Pod Termination</h2>
<figure class="align-center ">
    <picture>
        <source type="image/webp" srcset="/images/posts/k8s-graceful-shutdown_hu_bc6cd8ddc1517085.webp">
        <img loading="lazy" src="/images/posts/k8s-graceful-shutdown.png#center"
         alt="ChatGPT: draw funny picture of Kubernetes pod gracefully shutting down"/>
    </picture><figcaption>
            ChatGPT: draw funny picture of Kubernetes pod gracefully shutting down
        </figcaption>
</figure>
<p>If you are running services on Kubernetes, you have probably noticed that even with rolling updates (where Kubernetes gradually replaces pods), you might still see errors during deployment. This is especially annoying when you are trying to maintain &ldquo;zero-downtime&rdquo; systems.</p>
<p>When Kubernetes needs to terminate a pod (for any reason), it follows this sequence:</p>
<ol>
<li>Sends a SIGTERM signal to your container</li>
<li>Waits for a grace period (30 seconds by default)</li>
<li>If the container does not exit after the grace period, it gets brutal and sends a SIGKILL signal</li>
</ol>
<p>The problem? Most applications do not properly handle that SIGTERM signal. They just die immediately, dropping any in-flight requests. In the real world, while most API requests complete in 100-300ms, there are often those long-running operations that take 5-15 seconds or more. Think about processing uploads, generating reports, or running complex database queries. When these longer operations get cut off, that&rsquo;s when users really feel the pain.</p>
<h3 id="when-does-kubernetes-terminate-pods">When Does Kubernetes Terminate Pods?</h3>
<p>Rolling updates are just one scenario where your pods might be terminated. Here are other common situations that can lead to pod terminations:</p>
<ul>
<li>
<p><strong>Horizontal Pod Autoscaler Events</strong>: When HPA scales down during low-traffic periods, some pods get terminated.</p>
</li>
<li>
<p><strong>Resource Pressure</strong>: If your nodes are under resource pressure, the Kubernetes scheduler might decide to evict certain pods.</p>
</li>
<li>
<p><strong>Node Maintenance</strong>: During cluster upgrades, node draining causes many pods to be evicted.</p>
</li>
<li>
<p><strong>Spot/Preemptible Instances</strong>: If you are using cost-saving node types like spot instances, these can be reclaimed with minimal notice.</p>
</li>
</ul>
<p>All these scenarios follow the same termination process, so implementing proper graceful shutdown handling protects you from errors in all of these cases - not just during upgrades.</p>
<h2 id="lets-test-it-basic-vs-graceful-service">Let&rsquo;s Test It: Basic vs. Graceful Service</h2>
<p>Instead of just talking about theory, I built a small lab to demonstrate the difference between proper and improper shutdown handling. I created two nearly identical Go services:</p>
<ul>
<li><strong>Basic Service</strong>: A standard HTTP server with no special shutdown handling</li>
<li><strong>Graceful Service</strong>: The same service but with proper SIGTERM handling</li>
</ul>
<p>Both services:</p>
<ul>
<li>Process requests that take about 4 seconds to complete (intentionally configured for easier demonstration)</li>
<li>Run in the same Kubernetes cluster with identical configurations</li>
<li>Serve the same endpoints</li>
</ul>
<p>I specifically chose a 4-second processing time to make the problem obvious. While this might seem long compared to typical 100-300ms API calls, it perfectly simulates those problematic long-running operations that occur in real-world applications. The only difference between the services is how they respond to termination signals.</p>
<p>To test them, I wrote a simple k6 script that hammers both services with requests while triggering rolling restart of service&rsquo;s deployment. Here is what happened:</p>
<h3 id="basic-service-the-error-prone-one">Basic Service: The Error-Prone One</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">checks_total.......................: 695    11.450339/s
</span></span><span class="line"><span class="cl">checks_succeeded...................: 97.98% 681 out of 695
</span></span><span class="line"><span class="cl">checks_failed......................: 2.01%  14 out of 695
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">✗ status is 200
</span></span><span class="line"><span class="cl">  ↳  97% — ✓ 681 / ✗ 14
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">http_req_failed....................: 2.01%  14 out of 696
</span></span></code></pre></div><h3 id="graceful-service-the-reliable-one">Graceful Service: The Reliable One</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">checks_total.......................: 750     11.724824/s
</span></span><span class="line"><span class="cl">checks_succeeded...................: 100.00% 750 out of 750
</span></span><span class="line"><span class="cl">checks_failed......................: 0.00%   0 out of 750
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">✓ status is 200
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">http_req_failed........... ........: 0.00%  0 out of 751
</span></span></code></pre></div><p>The results speak for themselves. The basic service dropped 14 requests during the update (that is 2% of all traffic), while the graceful service handled everything perfectly without a single error.</p>
<p>You might think &ldquo;2% it is not that bad&rdquo; — but if you are doing several deployments per day and have thousands of users, that adds up to a lot of errors. Plus, in my experience, these errors tend to happen at the worst possible times.</p>
<h2 id="so-how-do-we-fix-it-the-graceful-shutdown-recipe">So How Do We Fix It? The Graceful Shutdown Recipe</h2>
<p>After digging into this problem and testing different solutions, I have put together a simple recipe for proper graceful shutdown. While my examples are in Go, the fundamental principles apply to any language or framework you are using.</p>
<p>Here are the key ingredients:</p>
<h3 id="1-listen-for-sigterm-signals">1. Listen for SIGTERM Signals</h3>
<p>First, your app needs to catch that SIGTERM signal instead of ignoring it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Set up channel for shutdown signals</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">stop</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Signal</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">signal</span><span class="p">.</span><span class="nf">Notify</span><span class="p">(</span><span class="nx">stop</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Interrupt</span><span class="p">,</span><span class="w"> </span><span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Block until we receive a shutdown signal</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">&lt;-</span><span class="nx">stop</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Shutdown signal received&#34;</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>This part is easy - you are just telling your app to wake up when Kubernetes asks it to shut down.</p>
<h3 id="2-track-your-in-flight-requests">2. Track Your In-Flight Requests</h3>
<p>You need to know when it is safe to shut down, so keep track of ongoing requests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Create a request counter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">inFlightRequests</span><span class="w"> </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/process&#34;</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// Increment counter when request starts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">inFlightRequests</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// do not forget to decrement when done!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="nx">inFlightRequests</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// Your normal request handling...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">4</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">  </span><span class="c1">// Simulating long-running work</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span></code></pre></div><p>This counter lets you check if there are still requests being processed before shutting down. it is especially important for those long-running operations that users have already waited several seconds for - the last thing they want is to see an error right before completion!</p>
<h3 id="3-separate-your-health-checks">3. Separate Your Health Checks</h3>
<p>Here is a commonly overlooked trick - you need different health check endpoints for liveness and readiness:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Track shutdown state</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">isShuttingDown</span><span class="w"> </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Readiness probe - returns 503 when shutting down</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/ready&#34;</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">isShuttingDown</span><span class="p">.</span><span class="nf">Load</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Shutting down, not ready&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Ready for traffic&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Liveness probe - always returns 200 (we are still alive!)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/alive&#34;</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;I&#39;m alive&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span></code></pre></div><p>This separation is crucial. The readiness probe tells Kubernetes to stop sending new traffic, while the liveness probe says &ldquo;do not kill me yet, I&rsquo;m still working!&rdquo;</p>
<h3 id="4-the-shutdown-dance">4. The Shutdown Dance</h3>
<p>Now for the most important part - the shutdown sequence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Step 1: Mark service as shutting down</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">isShuttingDown</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Step 2: Let Kubernetes notice the readiness probe failing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">5</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Step 3: Wait for in-flight requests to finish</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">inFlightRequests</span><span class="p">.</span><span class="nf">Load</span><span class="p">()</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">1</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Step 4: Finally, shut down the server gracefully</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">cancel</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span><span class="w"> </span><span class="mi">10</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">defer</span><span class="w"> </span><span class="nf">cancel</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">server</span><span class="p">.</span><span class="nf">Shutdown</span><span class="p">(</span><span class="nx">ctx</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">log</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;Forced shutdown: %v&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>I&rsquo;ve found this sequence to be optimal. First, we mark ourselves as &ldquo;not ready&rdquo; but keep running. We pause to give Kubernetes time to notice and update its routing. Then we patiently wait until all in-flight requests finish before actually shutting down the server.</p>
<h3 id="5-configure-kubernetes-correctly">5. Configure Kubernetes Correctly</h3>
<p>Do not forget to adjust your Kubernetes configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Use different probes for liveness and readiness</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/alive </span><span class="w"> </span><span class="c"># Always returns OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/ready </span><span class="w"> </span><span class="c"># Returns 503 during shutdown</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Give pods enough time to shut down gracefully</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w"> </span><span class="c"># Stops routing traffic after 2 failed checks (6 seconds)</span><span class="w">
</span></span></span></code></pre></div><p>This tells Kubernetes to wait up to 30 seconds for your app to finish processing requests before forcefully terminating it.</p>
<h2 id="tl-dr-quick-tips">TL; DR; Quick Tips</h2>
<p>If you are in a hurry, here are the key takeaways:</p>
<ol>
<li>
<p><strong>Catch SIGTERM Signals</strong>: Do not let your app be surprised when Kubernetes wants it to shut down.</p>
</li>
<li>
<p><strong>Track In-Flight Requests</strong>: Know when it is safe to exit by counting active requests.</p>
</li>
<li>
<p><strong>Split Your Health Checks</strong>: Use separate endpoints for liveness (am I running?) and readiness (can I take traffic?).</p>
</li>
<li>
<p><strong>Fail Readiness First</strong>: As soon as shutdown begins, start returning &ldquo;not ready&rdquo; on your readiness endpoint.</p>
</li>
<li>
<p><strong>Wait for Requests</strong>: Do not just shut down - wait for all active requests to complete first.</p>
</li>
<li>
<p><strong>Use Built-In Shutdown</strong>: Most modern web frameworks have graceful shutdown options; use them!</p>
</li>
<li>
<p><strong>Configure Terminaton Grace Period</strong>: Give your pods enough time to complete the shutdown sequence.</p>
</li>
<li>
<p><strong>Test Under Load</strong>: You will not catch these issues in simple tests - you need realistic traffic patterns.</p>
</li>
</ol>
<h2 id="wrap-up-is-it-worth-the-extra-code">Wrap Up: Is It Worth the Extra Code?</h2>
<p>You might be wondering if adding all this extra code is really worth it. After all, we&rsquo;re only talking about a 2% error rate during pod termination events.</p>
<p>From my experience working with high-traffic services, I would say absolutely yes - for three reasons:</p>
<ol>
<li>
<p><strong>User Experience</strong>: Even small error rates look bad to users. Nobody wants to see &ldquo;Something went wrong&rdquo; messages, especially after waiting 10+ seconds for a long-running operation to complete.</p>
</li>
<li>
<p><strong>Cascading Failures</strong>: Those errors can cascade through your system, especially if services depend on each other. Long-running requests often touch multiple critical systems.</p>
</li>
<li>
<p><strong>Deployment Confidence</strong>: With proper graceful shutdown, you can deploy more frequently without worrying about causing problems.</p>
</li>
</ol>
<p>The good news is that once you have implemented this pattern once, it is easy to reuse across your services. You can even create a small library or template for your organization.</p>
<p>In production environments where I have implemented these patterns, we have gone from seeing a spike of errors with every deployment to deploying multiple times per day with zero impact on users. that is a win in my book!</p>
<h2 id="further-reading">Further Reading</h2>
<p>If you want to dive deeper into this topic, I recommend checking out the article <a href="https://learnk8s.io/graceful-shutdown">Graceful shutdown and zero downtime deployments in Kubernetes</a> from learnk8s.io. It provides additional technical details about graceful shutdown in Kubernetes, though it does not emphasize the critical role of readiness probes in properly implementing the pattern as we have discussed here.</p>
<hr>
<p>For those interested in seeing the actual code I used in my testing lab, I&rsquo;ve published it on <a href="https://github.com/alikhil/gracefull-shutdown-lab">GitHub</a> with instructions for running the demo yourself.</p>
<p>Have you implemented graceful shutdown in your services? Did you encounter any other edge cases I didn&rsquo;t cover? Let me know in the comments how this pattern has worked for you!</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Remote LAN access with WireGuard and Mikrotik</title>
      <link>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</link>
      <pubDate>Sun, 13 Apr 2025 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</guid>
      <description>&lt;p&gt;Recently I have configured out how to access my home and cloud network remotely using WireGuard and Mikrotik Hex S router. With this step-by-step tutorial I will show you (and perhaps my future self) how to do it.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Recently I have configured out how to access my home and cloud network remotely using WireGuard and Mikrotik Hex S router. With this step-by-step tutorial I will show you (and perhaps my future self) how to do it.</p>
<h2 id="requirements">Requirements</h2>
<h3 id="what-i-have">What I have</h3>
<ul>
<li>
<p>A Mikrotik Hex S router with a dynamic public IP address.</p>
</li>
<li>
<p>Services in a cloud VM (Ubuntu 22) with a static public IP address.</p>
</li>
<li>
<p>Services in a VM on my home network.</p>
</li>
<li>
<p>Clients - laptops and phones - that need to access the services in my home and cloud network.</p>
</li>
</ul>
<h3 id="what-i-want">What I want</h3>
<ul>
<li>
<p>Clients outside of my home network should be able to access services both on my home and cloud network.</p>
</li>
<li>
<p>Only traffic to my home and cloud network should be routed through the VPN.</p>
</li>
<li>
<p>Clients inside my home network should be able to access services on my cloud network without additional configuration.</p>
</li>
<li>
<p>No external centralized service should be used.</p>
</li>
<li>
<p>No open ports on my home router.</p>
</li>
</ul>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/final.png" type="" alt="Final configuration"  /></p>
<h2 id="implementation">Implementation</h2>
<blockquote>
<p>Nowadays, there are plenty of VPN solutions like zero-tier and tailscale. However, I think they are too bloated for my humble needs and WireGuard is more than enough for that.</p>
</blockquote>
<p>Because of last requirement, it&rsquo;s obvious that traffic to home network should be routed though my cloud server.<br>
So, I will use WireGuard to create a tunnel between mikrotik router and cloud server.<br>
This way, I can access my home network from anywhere without exposing any ports on my home router.</p>
<h3 id="i-initial-configuration-of-wireguard-on-cloud-vm">I. Initial configuration of WireGuard on cloud VM</h3>
<blockquote>
<p><em>For the sake of reproducibility and simplicity I will use <a href="https://www.wireguard.com/">vanilla wireguard</a> and configure it on OS level, not in docker container.</em></p>
</blockquote>
<h4 id="1-install-wireguard-on-ubuntu-2204">1. Install wireguard on Ubuntu 22.04</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install wireguard -y
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Edit the configuration file</span>
</span></span><span class="line"><span class="cl">sudo nano /etc/sysctl.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Find and uncomment the line:</span>
</span></span><span class="line"><span class="cl">net.ipv4.ip_forward<span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then apply the changes:</span>
</span></span><span class="line"><span class="cl">sudo sysctl -p
</span></span></code></pre></div><h4 id="2-create-public-and-private-key">2. Create public and private key</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo su -
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /etc/wireguard
</span></span><span class="line"><span class="cl"><span class="c1"># Generate private and public keys</span>
</span></span><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee privatekey <span class="p">|</span> wg pubkey &gt; publickey
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check the keys</span>
</span></span><span class="line"><span class="cl">cat privatekey
</span></span><span class="line"><span class="cl">cat publickey
</span></span></code></pre></div><h4 id="3-create-the-configuration-file">3. Create the configuration file</h4>
<p>I will use <code>172.16.10.0/24</code> subnet for Wireguard network.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Add the following lines:</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Interface<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">Address</span> <span class="o">=</span> 172.16.10.1/24
</span></span><span class="line"><span class="cl"><span class="nv">ListenPort</span> <span class="o">=</span> <span class="m">27277</span> <span class="c1"># You can choose any port you want</span>
</span></span><span class="line"><span class="cl"><span class="nv">Privatekey</span> <span class="o">=</span> &lt;your_private_key&gt; <span class="c1"># from previous step</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># These are placeholder lines for next steps, keep them commented for now</span>
</span></span><span class="line"><span class="cl"><span class="c1"># # Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.0/24,192.168.0.0/16</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = mikrotik public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># # client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = first client device public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change the permissions of the configuration file</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> wg0.conf
</span></span></code></pre></div><h4 id="4-start-the-wireguard-interface">4. Start the WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable service to start on boot</span>
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> wg-quick@wg0.service
</span></span><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start the service</span>
</span></span><span class="line"><span class="cl">systemctl start wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check wg status</span>
</span></span><span class="line"><span class="cl">wg
</span></span></code></pre></div><p>It should show the interface is up like this</p>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/wg.png" type="" alt="WireGuard status"  /></p>
<p>More commands to check the status of the service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check the status of the service</span>
</span></span><span class="line"><span class="cl">systemctl status wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the service if needed</span>
</span></span><span class="line"><span class="cl">systemctl restart wg-quick@wg0
</span></span></code></pre></div><h3 id="ii-mikrotik-configuration">II. Mikrotik configuration</h3>
<p>I will use Mikrotik command line interface (CLI) to configure the router. You can use Winbox or WebFig if you prefer.</p>
<h4 id="1-create-wireguard-interface">1. Create WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create WireGuard interface and it will automatically generate private and public keys</span>
</span></span><span class="line"><span class="cl">/interface wireguard add listen-port<span class="o">=</span><span class="m">13231</span> <span class="nv">mtu</span><span class="o">=</span><span class="m">1420</span> <span class="nv">name</span><span class="o">=</span>cloud-wg
</span></span><span class="line"><span class="cl"><span class="c1"># Add the IP address to the interface</span>
</span></span><span class="line"><span class="cl">/ip add <span class="nv">address</span><span class="o">=</span>172.16.10.3/24 <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="nv">network</span><span class="o">=</span>172.16.10.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Print the keys, you will need only the public key later</span>
</span></span><span class="line"><span class="cl">/interface/wireguard/print
</span></span></code></pre></div><h4 id="2-create-the-peer">2. Create the peer</h4>
<p>Here we add cloud vm as a peer to the Mikrotik router&rsquo;s wireguard. The public key of the cloud server is needed here.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># In allowed-address we add IP ranges which will be routed through this peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.21.0.0/16 - subnet of docker containers on cloud server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.16.10.0/16 - subnet of wireguard network</span>
</span></span><span class="line"><span class="cl">/interface wireguard peers add allowed-address<span class="o">=</span>172.16.10.0/24,172.21.0.0/16 <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-address<span class="o">=</span>cloud.vm.ip.address <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-port<span class="o">=</span><span class="m">27277</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="se">\
</span></span></span><span class="line"><span class="cl">    public-key<span class="o">=</span><span class="s2">&#34;PUT CLOUD SERVER PUBLIC KEY HERE&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span>peer1 persistent-keepalive<span class="o">=</span>25s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Here is a tricky part</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Routing rule for subnet of wireguard network is added automatically on adding IP address to the interface</span>
</span></span><span class="line"><span class="cl"><span class="c1"># But we need to add explicitly the route for docker containers subnet</span>
</span></span><span class="line"><span class="cl"><span class="c1"># You may need to adjust the distance and routing-table values</span>
</span></span><span class="line"><span class="cl">/ip route <span class="nv">disabled</span><span class="o">=</span>no <span class="nv">distance</span><span class="o">=</span><span class="m">1</span> dst-address<span class="o">=</span>172.21.0.0/16 <span class="nv">gateway</span><span class="o">=</span>cloud-wg routing-table<span class="o">=</span>main <span class="nv">scope</span><span class="o">=</span><span class="m">30</span> suppress-hw-offload<span class="o">=</span>no target-scope<span class="o">=</span><span class="m">10</span>
</span></span></code></pre></div><h3 id="3-add-wireguard-peer-vm">3. Add WireGuard peer (VM)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.0/24,192.168.0.0/16
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> mikrotik public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><p>You should see the Mikrotik peer in the list of peers.
<img loading="lazy" src="/images/posts/mikrotik-wg/wg-show-2.png" type="" alt="WireGuard peers"  /></p>
<details>
<summary>More commands to tests connection between cloud vm and mikrotik wg</summary>
<h5 id="run-in-mikrotik-cli">Run in mikrotik CLI</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-ping.png" type="" alt="WireGuard ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud vm</span>
</span></span><span class="line"><span class="cl">put <span class="o">[</span>resolve google.com server 172.21.0.114<span class="o">]</span>
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-resolve.png" type="" alt="Wireguard resolve"  /></p>
<h5 id="run-in-cloud-server">Run in cloud server</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of Mikrotik router</span>
</span></span><span class="line"><span class="cl">ping 172.168.10.3
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-ping.png" type="" alt="VM ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on home adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @192.168.11.2
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-resolve.png" type="" alt="VM resolve"  /></p>
<h5 id="run-on-my-mac-connected-to-home-network">Run on my Mac connected to home network</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-ping.png" type="" alt="mac ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @172.121.0.114
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-resolve.png" type="" alt="mac resolve"  /></p>
</details>
<h2 id="iv-add-first-client-device-as-a-peer">IV. Add first client device as a peer</h2>
<p>I recommend you to use your smartphone as first client device, because it can work from both home WiFi and mobile data. This way you can test the connection from both networks.</p>
<p>Also, install on your smartphone:</p>
<ul>
<li>WireGuard app (<a href="https://apps.apple.com/us/app/wireguard/id1441195209">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.wireguard.android&amp;hl=en&amp;pli=1">Android</a>)</li>
<li>Network debugging app (<a href="https://apps.apple.com/bo/app/inettools-ping-dns-port-scan/id561659975">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.ulfdittmer.android.ping&amp;hl=en">Android</a>)</li>
</ul>
<h3 id="1-create-keys-for-the-client">1. Create keys for the client</h3>
<p>Install Wireguard app for your client OS.</p>
<p>Then, generate public and private keys on the device, for that create config from scratch in the app and then click on <strong>Generate keypair</strong> button.</p>
<p>Or you can generate keys on the cloud server and then copy them to the client device.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee peer-privatekey <span class="p">|</span> wg pubkey &gt; peer-publickey
</span></span></code></pre></div><h3 id="2-add-the-client-as-a-peer-to-wireguard-config-on-cloud-server">2. Add the client as a peer to WireGuard config on cloud server</h3>
<p>On the cloud server, edit the WireGuard config file and add the client as a peer.</p>
<p>Each time increment previous peer address by 1.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># client peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.2/32
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> put public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><h3 id="3-continue-configuring-the-client-device">3. Continue configuring the client device</h3>
<p>You already have public and private keys for the client device, other configuration parameters are:</p>
<h4 id="interface">Interface</h4>
<ul>
<li><strong>Address</strong> - It&rsquo;s address of peer in wireguard subnet. Put the same address you set in <code>AllowedIPs</code> field in previous step.</li>
<li><strong>DNS servers</strong> - if you have adguard/pihole running on the cloud server, you can use it as a DNS server. Put it&rsquo;s IP address here.</li>
<li><strong>MTU</strong> and <strong>ListenPort</strong> - you can leave them empty, they will be set automatically.</li>
</ul>
<h4 id="peer">Peer</h4>
<ul>
<li><strong>Endpoint</strong> - cloud server IP address and port (27277)</li>
<li><strong>Public key</strong> - public key of the cloud server</li>
<li><strong>AllowedIPs</strong> - here we put all subnets that we want to access from this current client device</li>
</ul>
<p>Here is an example of the configuration file for the client device:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Interface]</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateKey</span> <span class="o">=</span> <span class="s">private-key-of-client-device</span>
</span></span><span class="line"><span class="cl"><span class="na">Address</span> <span class="o">=</span> <span class="s">172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="na">DNS</span> <span class="o">=</span> <span class="s">172.21.0.114 # IP of adguard container on cloud server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Peer]</span>
</span></span><span class="line"><span class="cl"><span class="na">PublicKey</span> <span class="o">=</span> <span class="s">public-key-of-cloud-server</span>
</span></span><span class="line"><span class="cl"><span class="na">AllowedIPs</span> <span class="o">=</span> <span class="s">192.168.0.0/16, 172.16.10.0/24, 172.21.0.0/16 # subnets of home, cloud and wireguard network</span>
</span></span><span class="line"><span class="cl"><span class="na">Endpoint</span> <span class="o">=</span> <span class="s">ip.of.cloud.servner:27277</span>
</span></span></code></pre></div><p>That&rsquo;s it! Disconnect your device from the home Wi-Fi, switch to mobile data and connect to the VPN.</p>
<p>Then try to:</p>
<ul>
<li>ping the cloud server and Mikrotik router IP addresses in wireguard subnet.</li>
<li>check ports of services in docker containers on cloud and home server VM.</li>
</ul>
<style>
td, th {
   border: none!important;
}
</style>
<table>
  <thead>
      <tr>
          <th>ping cloud</th>
          <th>ping home</th>
          <th>check adguard port</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-vm.jpeg" type="" alt="ping cloud"  /></td>
          <td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-home.jpeg" type="" alt="ping home"  /></td>
          <td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-port-scan.jpeg" type="" alt="port scan"  /></td>
      </tr>
  </tbody>
</table>
<h2 id="v-bonus-dns-configuration">V. Bonus. DNS configuration</h2>
<p>Since I have 2 adguard instances and I use them as DNS servers everywhere, I will add DNS records for accessing my services:</p>
<ul>
<li>*<strong>.cloud.domain.com</strong> - pointing to traefik docker container on cloud server</li>
<li>*<strong>.home.domain.com</strong> - pointing to traefik docker container on home server</li>
</ul>
<p>Thus, I can access my services using domain names instead of IP addresses.</p>
<img src="/images/posts/mikrotik-wg/dns.png" width="30%">
<h2 id="vi-final-thoughts">VI. Final thoughts</h2>
<p>I hope this tutorial was helpful for you. I will keep it updated if I find any issues or improvements.</p>
<p>If you have any questions or suggestions, feel free to leave a comment below.</p>
<p>Credits to <a href="https://www.laroberto.com/">@laroberto</a> for their <a href="https://www.laroberto.com/remote-lan-access-with-wireguard/">guide on LAN access with WireGuard</a>. I followed it to set up the initial configuration and then adapted it to my needs.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Storing and using secrets in Mikrotik RouterOS</title>
      <link>https://alikhil.dev/posts/saving-and-using-secrets-in-mikrotitk-routeros/</link>
      <pubDate>Sun, 30 Mar 2025 16:01:25 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/saving-and-using-secrets-in-mikrotitk-routeros/</guid>
      <description>&lt;p&gt;Recently I have replaced my stock ISP router with &lt;a href=&#34;https://mikrotik.com/product/hex_s&#34;&gt;Mikrotik Hex S&lt;/a&gt;. I have been using it for a while and I am very happy with it. It is a very powerful device which can be programmed and automated with built-in scripting language.&lt;/p&gt;
&lt;p&gt;When I started writing my first scripts I faced a problem: how to store and use secrets in my scripts. I have found a solution and I want to share it with you.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Recently I have replaced my stock ISP router with <a href="https://mikrotik.com/product/hex_s">Mikrotik Hex S</a>. I have been using it for a while and I am very happy with it. It is a very powerful device which can be programmed and automated with built-in scripting language.</p>
<p>When I started writing my first scripts I faced a problem: how to store and use secrets in my scripts. I have found a solution and I want to share it with you.</p>
<h2 id="the-problem">The problem</h2>
<p>Let&rsquo;s say I want to write a script that will send me telegram notifications. To do so I need to store my telegram bot token and chat id. Since I keep my RouterOS configuration in a git repository, I don&rsquo;t want to hardcode my secrets in the script.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="p">:</span><span class="n">global</span> <span class="n">sendTelegramMessage</span> <span class="k">do</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">botToken</span> <span class="s2">&#34;1234567890:ABCDEFGHIJKLMN&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">chatId</span> <span class="s2">&#34;10000000&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">message</span> <span class="s2">&#34;$1&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># telegram notification</span>
</span></span><span class="line"><span class="cl">    <span class="o">/</span><span class="k">tool</span> <span class="n">fetch</span> <span class="n">url</span><span class="o">=</span><span class="s2">&#34;https://api.telegram.org/bot$botToken/sendMessage\?chat_id=$chatId&amp;text=$message&#34;</span> <span class="n">keep</span><span class="o">-</span><span class="n">result</span><span class="o">=</span><span class="n">no</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="the-solution">The solution</h2>
<p>RouterOS has low level feature <code>/ppp secret</code> which can be used to store secrets. However, it could be inconvenient and a bit messy to use it directly in scripts. Instead, I would like to have more high level API to store and use secrets.</p>
<p>And, I have one. There is post in <a href="https://forum.mikrotik.com/viewtopic.php?p=916159#p916159">mikrotik forum</a> by user with nickname <strong>Amm0</strong>. Ammo has shared a script of global function which can be used to store and retrieve secrets like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$SECRET set mySecret password=&#34;mySecretPassword&#34;
</span></span><span class="line"><span class="cl">:put [$SECRET get mySecret]
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mirotik-secrets/terminal.png" type="" alt="Terminal Screenshot"  /></p>
<p>Now, I modify my script to use this function to keep it clean and secure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="p">:</span><span class="n">global</span> <span class="n">sendTelegramMessage</span> <span class="k">do</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">botToken</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">set</span> <span class="n">botToken</span> <span class="s2">&#34;$[$SECRET get TELEGRAM_TOKEN]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">chatId</span> <span class="s2">&#34;$[$SECRET get CHAT_ID]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">message</span> <span class="s2">&#34;$1&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># telegram notification</span>
</span></span><span class="line"><span class="cl">    <span class="o">/</span><span class="k">tool</span> <span class="n">fetch</span> <span class="n">url</span><span class="o">=</span><span class="s2">&#34;https://api.telegram.org/bot$botToken/sendMessage\?chat_id=$chatId&amp;text=$message&#34;</span> <span class="n">keep</span><span class="o">-</span><span class="n">result</span><span class="o">=</span><span class="n">no</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><details>
  <summary>global SECRET function source code</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">### $SECRET
</span></span><span class="line"><span class="cl">#   get &lt;name&gt;
</span></span><span class="line"><span class="cl">#   set &lt;name&gt; password=&lt;password&gt;
</span></span><span class="line"><span class="cl">#   remove &lt;name
</span></span><span class="line"><span class="cl">#   print
</span></span><span class="line"><span class="cl">:global SECRET
</span></span><span class="line"><span class="cl">:set $SECRET do={
</span></span><span class="line"><span class="cl">    :global SECRET
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # helpers
</span></span><span class="line"><span class="cl">    :local fixprofile do={
</span></span><span class="line"><span class="cl">        :if ([/ppp profile find name=&#34;null&#34;]) do={:put &#34;nothing&#34;} else={
</span></span><span class="line"><span class="cl">            /ppp profile add bridge-learning=no change-tcp-mss=no local-address=0.0.0.0 name=&#34;null&#34; only-one=yes remote-address=0.0.0.0 session-timeout=1s use-compression=no use-encryption=no use-mpls=no use-upnp=no
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    :local lppp [:len [/ppp secret find where name=$2]]
</span></span><span class="line"><span class="cl">    :local checkexist do={
</span></span><span class="line"><span class="cl">        :if (lppp=0) do={
</span></span><span class="line"><span class="cl">            :error &#34;\$SECRET: cannot find $2 in secret store&#34;
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET
</span></span><span class="line"><span class="cl">    :if ([:typeof $1]!=&#34;str&#34;) do={
</span></span><span class="line"><span class="cl">        :put &#34;\$SECRET&#34;
</span></span><span class="line"><span class="cl">        :put &#34;   uses /ppp/secrets to store stuff like REST apikeys, or other sensative data&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET print - prints stored secret passwords&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET get &lt;name&gt; - gets a stored secret&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET set &lt;name&gt; password=\&#34;YOUR_SECRET\&#34; - sets a secret password&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET remove &lt;name&gt; - removes a secret&#34;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET print
</span></span><span class="line"><span class="cl">    :if ($1~&#34;^pr&#34;) do={
</span></span><span class="line"><span class="cl">        /ppp secret print where comment~&#34;\\\$SECRET&#34;
</span></span><span class="line"><span class="cl">        :return [:nothing]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET get
</span></span><span class="line"><span class="cl">    :if ($1~&#34;get&#34;) do={
</span></span><span class="line"><span class="cl">        $checkexist
</span></span><span class="line"><span class="cl">       :return [/ppp secret get $2 password]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET set
</span></span><span class="line"><span class="cl">    :if ($1~&#34;set|add&#34;) do={
</span></span><span class="line"><span class="cl">        :if ([:typeof $password]=&#34;str&#34;) do={} else={:error &#34;\$SECRET: password= required&#34;}
</span></span><span class="line"><span class="cl">        :if (lppp=0) do={
</span></span><span class="line"><span class="cl">            /ppp secret add name=$2 password=$password
</span></span><span class="line"><span class="cl">        } else={
</span></span><span class="line"><span class="cl">            /ppp secret set $2 password=$password
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">        $fixprofile
</span></span><span class="line"><span class="cl">        /ppp secret set $2 comment=&#34;used by \$SECRET&#34;
</span></span><span class="line"><span class="cl">        /ppp secret set $2 profile=&#34;null&#34;
</span></span><span class="line"><span class="cl">        /ppp secret set $2 service=&#34;async&#34;
</span></span><span class="line"><span class="cl">        :return [$SECRET get $2]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET remove
</span></span><span class="line"><span class="cl">    :if ($1~&#34;rm|rem|del&#34;) do={
</span></span><span class="line"><span class="cl">        $checkexist
</span></span><span class="line"><span class="cl">        :return [/ppp secret remove $2]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    :error &#34;\$SECRET: bad command&#34;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div></details>
<h2 id="conclusion">Conclusion</h2>
<p>The good thing about this approach is that secrets storing and retrieving mechanism encapsulated and can be easily changed in the future without changing the scripts. Also, it is easy to use and understand.</p>
<p>Keep your secrets safe and happy scripting!</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Unintended Side Effects of Using http.DefaultClient in Go</title>
      <link>https://alikhil.dev/posts/go-global-http-client-gotcha/</link>
      <pubDate>Wed, 19 Mar 2025 19:20:45 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/go-global-http-client-gotcha/</guid>
      <description>&lt;p&gt;The Internet is plenty of articles that telling why you should not be using &lt;code&gt;http.DefaultClient&lt;/code&gt; in Golang (&lt;a href=&#34;https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779&#34;&gt;one&lt;/a&gt;, &lt;a href=&#34;https://vishnubharathi.codes/blog/know-when-to-break-up-with-go-http-defaultclient/&#34;&gt;two&lt;/a&gt;) but they refer to &lt;code&gt;Timeout&lt;/code&gt; and &lt;code&gt;MaxIdleConns&lt;/code&gt; settings.&lt;/p&gt;
&lt;p&gt;Today I want to share with you another reason why you should avoid using &lt;code&gt;http.DefaultClient&lt;/code&gt; in your code.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The Internet is plenty of articles that telling why you should not be using <code>http.DefaultClient</code> in Golang (<a href="https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779">one</a>, <a href="https://vishnubharathi.codes/blog/know-when-to-break-up-with-go-http-defaultclient/">two</a>) but they refer to <code>Timeout</code> and <code>MaxIdleConns</code> settings.</p>
<p>Today I want to share with you another reason why you should avoid using <code>http.DefaultClient</code> in your code.</p>
<h2 id="the-story">The Story</h2>
<p>As an SRE at Criteo, I both read and write code. Last week, I worked on patching <a href="https://github.com/updatecli/updatecli">Updatecli</a> — an upgrade automation tool written in Go.</p>
<p>The <a href="https://github.com/updatecli/updatecli/pull/4432">patch</a> itself was just ~15 lines of code. But then I spent three days debugging a strange authorization bug in an unrelated part of the code.</p>
<p>It happened because of code like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">client</span><span class="p">.</span><span class="nx">Client</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Since <code>http.DefaultClient</code> is a reference, not a value:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">DefaultClient</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">Client</span><span class="p">{}</span><span class="w">
</span></span></span></code></pre></div><p>The code above is effectively the same as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Later, in a third-party library, I found this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">opts</span><span class="p">.</span><span class="nx">Client</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">opts</span><span class="p">.</span><span class="nx">Client</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-fix">The Fix</h2>
<p>To prevent this, I had to change the code to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Client</span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>As a result, the patched client with the authorization transport got injected into the third-party library, causing unexpected failures.</p>
<p>Bugs like this are hard to catch just by reading the code, since they involve global state mutation. But could they be detected by linters?</p>
<p>What do you think? How do you find or prevent such issues in your projects?</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Forwarding SMS to Telegram</title>
      <link>https://alikhil.dev/posts/forwarding-sms-to-telegram/</link>
      <pubDate>Sat, 26 Oct 2024 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/forwarding-sms-to-telegram/</guid>
      <description>&lt;p&gt;After extensive travel, I’ve accumulated several mobile numbers and, naturally, physical SIM cards. Switching them out each time became tedious, even after buying a basic Nokia with two SIM slots, which only helped temporarily. When a friend asked if I could set up a Spanish number for account registrations, I realized it was time to automate the process.&lt;/p&gt;
&lt;p&gt;If you’re dealing with multiple SIM cards and want to receive SMS in Telegram, I have a straightforward approach. You’ll need a Linux machine that’s always online, connected to the internet, and about $10.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After extensive travel, I’ve accumulated several mobile numbers and, naturally, physical SIM cards. Switching them out each time became tedious, even after buying a basic Nokia with two SIM slots, which only helped temporarily. When a friend asked if I could set up a Spanish number for account registrations, I realized it was time to automate the process.</p>
<p>If you’re dealing with multiple SIM cards and want to receive SMS in Telegram, I have a straightforward approach. You’ll need a Linux machine that’s always online, connected to the internet, and about $10.</p>
<h2 id="what-youll-need">What You’ll Need</h2>
<ul>
<li>A physical SIM card</li>
<li>A USB modem that’s supported by the <a href="https://wammu.eu/phones/">Gammu library</a></li>
<li>A Telegram bot token, chat or channel ID</li>
<li>A Linux machine with a free USB port</li>
<li>Docker and Docker Compose installed</li>
</ul>
<h2 id="finding-the-right-modem">Finding the Right Modem</h2>
<blockquote>
<p>If you have a USB modem at home, check if it’s supported by <a href="https://wammu.eu/phones/">Gammu</a>.</p>
</blockquote>
<p>For our purposes, we don’t need an expensive 4G modem with advanced features. Any basic 2G/3G modem will work, and these are easy to find at a discounted price on sites like eBay or Wallapop.</p>
<p>Search for “Huawei USB modem,” sort by price, and look for unlocked options or ones with compatible firmware.</p>
<p>For instance:
<img loading="lazy" src="/images/posts/sms-to-telegram/ebay.png" type="" alt="Ebay Screenshot"  /></p>
<p>Next, go to the <a href="https://wammu.eu/phones/">Gammu</a> website and look up the device. Make sure it appears on the list and that “SMS” is included in the &ldquo;Supported features&rdquo; column:</p>
<p><img loading="lazy" src="/images/posts/sms-to-telegram/e3131.png" type="" alt="E3131"  /></p>
<p>If the device meets these requirements, it’s good to go!</p>
<h2 id="setup-instructions">Setup Instructions</h2>
<blockquote>
<p>Before starting the setup, it’s best to connect the modem with the SIM card already inserted to your PC and check that it’s functioning properly.</p>
</blockquote>
<h3 id="identify-device-path">Identify Device Path</h3>
<p>Run the following command to identify the device path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tree /dev/serial/by-id/
</span></span></code></pre></div><p>You should see a paths similar to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">/dev/serial/by-id
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if00-port0 -&gt; ../../ttyUSB0
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if02-port0 -&gt; ../../ttyUSB1
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if03-port0 -&gt; ../../ttyUSB2
</span></span></code></pre></div><p>Choose a path that ends with <code>ttyUSB0</code>, in my case it&rsquo;s <code>/dev/serial/by-id/usb-HUAWEI_HUAWEI_Mobile-if00-port0</code>.</p>
<h3 id="running-the-service">Running the Service</h3>
<p>Using Docker Compose, set up your configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gammu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/alikhil/sms-to-telegram:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">bind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/serial/by-id/usb-HUAWEI_HUAWEI_Mobile-if00-port0</span><span class="w"> </span><span class="c"># Change this to your device path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/modem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">privileged</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">BOT_TOKEN=&lt;put your telegram bot token here&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PIN=&lt;your sim card pin&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">CHAT_ID=&lt;telegram chat/channel ID&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DEVICE=/dev/modem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PROTOCOL=at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">NET_ADMIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">SYS_MODULE</span><span class="w">
</span></span></span></code></pre></div><p>Save the configuration to a <code>docker-compose.yml</code> file and run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs -f gammu
</span></span></code></pre></div><p>If everything is set up correctly, you should see the following log messages:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:50:56 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Created POSIX RW shared memory at 0x7fcf90b21000
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:50:56 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Starting phone communication...
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:30 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Ignoring incoming SMS info as not a Status Report in SR memory.
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Read <span class="m">1</span> messages
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Received IN20241004_195517_00_Celerity_00.txt
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>13<span class="o">]</span>: Starting run on receive: /etc/sms_to_telegram.sh IN20241004_195517_00_Celerity_00.txt
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Process finished successfully
</span></span></code></pre></div><p>To test SMS reception, you can use free online SMS-sending services (search for &ldquo;send SMS for free&rdquo;) or try logging into Telegram, your bank account, etc.</p>
<p><img loading="lazy" src="/images/posts/sms-to-telegram/screenshot.png" type="" alt="Telegram Screenshot"  /></p>
<h2 id="how-it-works">How It Works</h2>
<p>The <a href="https://wammu.eu/libgammu/">Gammu library</a> provides a unified interface for working with phones and modems from various manufacturers.</p>
<p>On top of that, there&rsquo;s the <a href="https://wammu.eu/smsd/">Gammu SMS Daemon</a>, which receives SMS messages and triggers a custom script—in our case, <a href="https://github.com/alikhil/sms-to-telegram/blob/main/sms_to_telegram.sh">script</a> to send the messages to Telegram.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Thanks to <a href="https://github.com/kutovoys">@kutovoys</a> for the idea and <a href="https://github.com/kutovoys/sms-to-telegram">Docker image</a>!</p>
<p>This is a simple, affordable, and scalable solution—especially if you’re into self-hosting.</p>
<blockquote>
<p>This post was originally written for <a href="https://vas3k.club/post/25926/">vas3k.club</a>.</p>
</blockquote>]]></content:encoded>
    </item>
    
    <item>
      <title>Monitor input source control on Mac</title>
      <link>https://alikhil.dev/posts/monitor-input-source-control-mac/</link>
      <pubDate>Wed, 16 Oct 2024 20:31:06 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/monitor-input-source-control-mac/</guid>
      <description>&lt;p&gt;If you as me have single monitor and 2 Mac devices (for example, I have corporate Macbook and personal Mac Mini) you may want to use the same monitor for both devices. And you may want to switch between them without unplugging and plugging cables or selecting input source using monitor buttons.&lt;/p&gt;
&lt;p&gt;In this post I will show you how to configure hotkeys for that.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>If you as me have single monitor and 2 Mac devices (for example, I have corporate Macbook and personal Mac Mini) you may want to use the same monitor for both devices. And you may want to switch between them without unplugging and plugging cables or selecting input source using monitor buttons.</p>
<p>In this post I will show you how to configure hotkeys for that.</p>
<h2 id="hardware">Hardware</h2>
<p>You will need a monitor with <strong>multiple input sources</strong>.
For example, I have Dell S2722QC tt has 2 HDMI ports and 1 USB-C port where:</p>
<ul>
<li>Macbook Air connected to port HDMI-2</li>
<li>Mac Mini connected to port USB-C-1</li>
</ul>
<h2 id="software">Software</h2>
<p>There is app called <a href="https://github.com/waydabber/BetterDisplay">BetterDisplay</a> that has a lot of powerful features. But for our case we need only one feature - <strong>change display inputs using DDC</strong>.</p>
<p>Install it on both Macs. You will have 14 days trial period with all PRO features.</p>
<p><strong>Enable Accessibility for BetterDisplay in System Settings -&gt; Privacy &amp; Security -&gt; Accessibility.</strong></p>
<p>Then try to switch input source by clicking on BetterDisplay icon in the menu bar -&gt; <em>DDC Input Source -&gt; Select next port.</em></p>
<table>
  <thead>
      <tr>
          <th></th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><img loading="lazy" src="/images/posts/menu-shot.png" type="" alt="Menu Shot"  /></td>
          <td><img loading="lazy" src="/images/posts/input-choose.png" type="" alt="Input Choose"  /></td>
      </tr>
  </tbody>
</table>
<p>If it works, you can continue to the next step.
Otherwise check if your monitor supports DDC protocol and ensure Accessibility is enabled for BetterDisplay.</p>
<h3 id="paid-option">Paid option</h3>
<p>If you are ready to pay 19$/19€ x2 for both Macs you can <a href="https://betterdisplay.pro/buy/">buy BetterDisplay</a>. And then configure hotkeys in the app settings <em>Settings -&gt; Keyboards -&gt; Custom keyboard shortcuts -&gt; DDC Input Source</em>.</p>
<p>Click &ldquo;Record Shortcut&rdquo; and press the key combination you want to use, for example <code>CMD + F1</code> and <code>CMD + F2</code>.</p>
<p><img loading="lazy" src="/images/posts/shortcuts.png" type="" alt="alt text"  /></p>
<h3 id="free-option">Free option</h3>
<p>If you like me don&rsquo;t want to pay for 40$ for single feature there is a hacky way to do it.</p>
<p>We need an app that can handle hotkeys and run shell commands. I use <a href="https://www.raycast.com/">Raycast</a>, so called &ldquo;Spotlight on steroids&rdquo; and it can handle custom hotkeys. Or you can use any other app you like.</p>
<h4 id="configuring-shell-command">Configuring shell command</h4>
<p>Before configuring <strong>Raycast</strong> we need to know <code>ddc</code> value for each input source. To do so, go to <em>Settings -&gt; Displays -&gt; &ldquo;Your monitor name&rdquo; -&gt; DDC Input Sources</em>, and save IDs from <strong>Value</strong> column for each input source:</p>
<p><img loading="lazy" src="/images/posts/ddc-input-sources.png" type="" alt="alt text"  /></p>
<p>In my case it&rsquo;s <code>18</code> for HDMI-2 and <code>25</code> for USB-C-1.</p>
<p>Then create a directory <code>~/raycast-scripts</code> and put there a script <code>change-input-source.sh</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># See full documentation here: https://github.com/raycast/script-commands</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required parameters:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># @raycast.schemaVersion 1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># @raycast.title Switch Monitor Input Source</span>
</span></span><span class="line"><span class="cl"><span class="c1"># @raycast.mode silent</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Optional parameters:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># @raycast.icon 🖥️</span>
</span></span><span class="line"><span class="cl"><span class="c1"># @raycast.packageName Raycast Scripts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DEST_ID</span><span class="o">=</span><span class="m">18</span> &lt;PUT YOUR DDC ID HERE&gt;
</span></span><span class="line"><span class="cl"><span class="nv">DEST_NAME</span><span class="o">=</span><span class="s2">&#34;Home Mac Mini&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># PUT YOUR ACTUAL HOSTNAME</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="sb">`</span>hostname<span class="sb">`</span> <span class="o">==</span> <span class="s2">&#34;Aliks-Mac-mini.local&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">DEST_ID</span><span class="o">=</span><span class="m">25</span> &lt;PUT YOUR SECOND DDC ID HERE&gt;
</span></span><span class="line"><span class="cl">  <span class="nv">DEST_NAME</span><span class="o">=</span><span class="s2">&#34;Work Macbook Air&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Switching monitor input source to </span><span class="nv">$DEST_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">/Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay <span class="nb">set</span> -ddc<span class="o">=</span><span class="nv">$DEST_ID</span> -vcp<span class="o">=</span>inputSelect
</span></span></code></pre></div><p>After that try to run it in terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x ~/raycast-scripts/change-input-source.sh
</span></span><span class="line"><span class="cl">~/raycast-scripts/change-input-source.sh
</span></span></code></pre></div><p>If it works, you can continue to the next step.</p>
<h4 id="configuring-raycast">Configuring Raycast</h4>
<ol>
<li>Open Raycast and go to <em>Settings -&gt; Extensions -&gt; Search for Scripts</em></li>
<li>Click on <em>Add Script Directory</em> and select <code>~/raycast-scripts</code></li>
<li>Click on <em>Record Shortcut</em> for newly added script and press the key combination you want to use, for example <code>CMD + F1</code> on first Mac and <code>CMD + F2</code> on second.</li>
</ol>
<p><img loading="lazy" src="/images/posts/raycast-settings.png" type="" alt="alt text"  /></p>
<p>And that&rsquo;s it! Now you can switch input source using hotkeys.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Build own drone.io docker image</title>
      <link>https://alikhil.dev/posts/build-own-drone-image/</link>
      <pubDate>Fri, 20 Sep 2019 10:30:00 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/build-own-drone-image/</guid>
      <description>&lt;p&gt;¡Hola, amigos!&lt;/p&gt;
&lt;p&gt;In this post, I will quickly descibe how you can build your own &lt;a href=&#34;https://drone.io&#34;&gt;drone.io&lt;/a&gt; docker image.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>¡Hola, amigos!</p>
<p>In this post, I will quickly descibe how you can build your own <a href="https://drone.io">drone.io</a> docker image.</p>
<p>Drone is very popular container native CI/CD platform. Not long time ago, there was release of new <a href="https://blog.drone.io/drone-1/">1.0 version</a> of drone. Which brang a lot of cool features and new <a href="https://discourse.drone.io/t/licensing-and-subscription-faq/3839">license</a>. The license tells that we can use Enterprise version of drone for free without any limits by building our own docker image if we are individuals or startup (read the licence for more detail).</p>
<p>So, how to build it?</p>
<h2 id="instructions">Instructions</h2>
<p>First, clone the drone repo to your local machine.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone git@github.com:drone/drone.git
</span></span></code></pre></div><p>Second, checkout to version of drone you want to build. For example, I want to build <a href="https://github.com/drone/drone/tag/v1.3.1">v1.3.1</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git checkout v1.3.1
</span></span></code></pre></div><p>We will use single dockerfile to build the image. To do so, we need to add extra step to existing dockerfile which is in <code>docker</code> directory. Let&rsquo;s say we want to build docker image for <code>linux</code> OS and <code>amd64</code> architecture, then we will edit <code>docker/Dockerfile.server.linux.amd64</code>.</p>
<!-- Original drone docker images built in drone itself. To check how they built you can check `drone.yml`. -->
<p>If you check the dockerfile you will see, that binaries are just copied into docker image during the build and they are built outside of the docker build. So, the step we will add to dockerfile is <code>go build</code> step.</p>
<p>To build the binary, we need to know what version of go is used for building binary in original docker image. We can find it in <code>drone.yml</code> <strong>build</strong> step. For version 1.3.1 of drone <code>golang:1.12.9</code> docker image is used for building binaries.</p>
<p>Then, we use same image to build binary in our dockerfile:</p>
<p><strong>docker/Dockerfile.server.linux.amd64</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">golang:1.12.9</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">builder</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/go/src/github.com/drone/drone</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> GOOS linux<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> GOARCH amd64<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> CGO_ENABLED <span class="m">1</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> REPO github.com/drone/drone<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> GO111MODULE on<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> go build -tags nolimit -ldflags <span class="s2">&#34;-extldflags \&#34;-static\&#34;&#34;</span> -o release/linux/<span class="si">${</span><span class="nv">GOARCH</span><span class="si">}</span>/drone-server <span class="si">${</span><span class="nv">REPO</span><span class="si">}</span>/cmd/drone-server<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">alpine:3.9</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">alpine</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apk add -U --no-cache ca-certificates<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">alpine:3.9</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">80</span> <span class="m">443</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">VOLUME</span><span class="w"> </span><span class="s">/data</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> GODEBUG <span class="nv">netdns</span><span class="o">=</span>go<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> XDG_CACHE_HOME /data<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> DRONE_DATABASE_DRIVER sqlite3<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> DRONE_DATABASE_DATASOURCE /data/database.sqlite<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_RUNNER_OS</span><span class="o">=</span>linux
</span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_RUNNER_ARCH</span><span class="o">=</span>amd64
</span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_SERVER_PORT</span><span class="o">=</span>:80<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_SERVER_HOST</span><span class="o">=</span>localhost
</span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_DATADOG_ENABLED</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DRONE_DATADOG_ENDPOINT</span><span class="o">=</span>https://stats.drone.ci/api/v1/series<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>builder /go/src/github.com/drone/drone/release/linux/amd64/drone-server /bin/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;/bin/drone-server&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>Also we need to delete .dockerignore file from root of the repo.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rm .dockerignore
</span></span></code></pre></div><p>Then we build docker image like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t alikhil/drone:1.3.1 -f docker/Dockerfile.server.linux.amd64 .
</span></span></code></pre></div><p>That&rsquo;s all! Now you can use own newly built docker image instead of official one if your use case meet license conditions.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Deploy SPA application to Kubernetes</title>
      <link>https://alikhil.dev/posts/deploy-spa-to-k8s/</link>
      <pubDate>Sat, 08 Jun 2019 22:30:00 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/deploy-spa-to-k8s/</guid>
      <description>&lt;p&gt;Hello, folks!&lt;/p&gt;
&lt;p&gt;Today I want you to share with you tutorial on how to deploy your SPA application to Kubernetes. Tutorial is oriented for those don&amp;rsquo;t very familiar with docker and k8s but want their single page application run in k8s.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hello, folks!</p>
<p>Today I want you to share with you tutorial on how to deploy your SPA application to Kubernetes. Tutorial is oriented for those don&rsquo;t very familiar with docker and k8s but want their single page application run in k8s.</p>
<h3 id="dockerize-the-application">Dockerize the application</h3>
<p>I expect that you have docker installed in your machine. If it isn&rsquo;t you can install it by following <a href="https://docs.docker.com/install/">official installation guide</a>.</p>
<p>As SPA project I will use <a href="https://github.com/gothinkster/vue-realworld-example-app">vue-realworld-example-app</a> as SPA project. You can your own SPA project if you have one.</p>
<p>So, I have cloned it, installed dependencies and built:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/gothinkster/vue-realworld-example-app
</span></span><span class="line"><span class="cl">yarn
</span></span><span class="line"><span class="cl">yarn build
</span></span></code></pre></div><p>Next step is to decide how our application will be served. There are bunch of possible solutions but I decided to use <a href="https://nginx.org/en/">nginx</a> since it recommends itself as one of the best http servers.</p>
<p>To serve SPA we need to return all requested files if they exist or otherwise fallback to index.html. To do so I wrote the following nginx config:</p>
<p><strong>nginx.conf</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># ...
</span></span></span><span class="line"><span class="cl"><span class="c1"># other configs
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kn">root</span> <span class="s">/app</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">alias</span> <span class="s">/app/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">/index.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Full config file can be found in <a href="https://github.com/alikhil/vue-realworld-example-app/blob/master/nginx.conf">my fork of the repo</a></p>
<p>Then, we need to write <strong>Dockerfile</strong> for building image with our application. Here it is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">nginx</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/root</span>/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> ./dist /app<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> ./nginx.conf /etc/nginx/conf.d/default.conf<span class="err">
</span></span></span></code></pre></div><p>We assume that artifacts of build placed in the <code>dist</code> directory and so that during the docker build the content of <code>dist</code> directory copied into containers <code>/app</code> directory.</p>
<p>Now, we are ready to build it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t alikhil/my-spa:0.1 .
</span></span></code></pre></div><p>And run it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -p 8080:80 alikhil/my-spa:0.1
</span></span></code></pre></div><p>Then if we open http://localhost:8080 we will see something similar to:</p>
<img width="1279" alt="Screen Shot 2019-06-09 at 14 21 09" src="https://user-images.githubusercontent.com/7482065/59158361-e792f580-8ac1-11e9-9cd0-de309c697c12.png">
<p>Cool! It works!</p>
<p>We will need to use our newly builded docker image to deploy to k8s. So, we need to make it available from the k8s cluster by pulling to some docker registry. I will push image to <a href="http://hub.docker.com/">DockerHub</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker push alikhil/my-spa:0.1
</span></span></code></pre></div><h3 id="deploy-to-k8s">Deploy to k8s</h3>
<p>To run the application in k8s we will use <code>Deployment</code> resource type. Here it is:</p>
<p><strong>deployment.yaml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">alikhil/my-spa:0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">150m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">250Mi</span><span class="w">
</span></span></span></code></pre></div><p>Then we create deployment by running <code>kubectl apply -f deployment.yaml</code> and newly created pods can found:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ kubectl get pods
</span></span><span class="line"><span class="cl">NAME                      READY   STATUS    RESTARTS   AGE
</span></span><span class="line"><span class="cl">my-spa-84b6dcd48d-mhv9f   1/1     Running   <span class="m">0</span>          18s
</span></span></code></pre></div><p>Then we need to expose our app to the world. It can be done by using service of type NodePort or via Ingress. We will do it with Ingress. For that we will need service:</p>
<p><strong>service.yaml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span></code></pre></div><p>And ingress itself:</p>
<p><strong>ingress.yaml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa-ing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">my-spa.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa-cert-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">my-spa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f ingress.yaml -f service.yaml
</span></span></code></pre></div><p>And here it is! Our SPA runs in the k8s!</p>
<img width="1253" alt="Screen Shot 2019-06-10 at 22 39 00" src="https://user-images.githubusercontent.com/7482065/59221731-a1788780-8bd0-11e9-806b-734048d3a291.png">]]></content:encoded>
    </item>
    
    <item>
      <title>Tips for language learners</title>
      <link>https://alikhil.dev/posts/how-to-start-learning-new-language/</link>
      <pubDate>Sun, 21 Oct 2018 22:42:44 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/how-to-start-learning-new-language/</guid>
      <description>&lt;p&gt;If you expected only post on IT topics on this blog, I am sorry :(
Today I&amp;rsquo;ll share you my experience in learning a new language. I am practicing these techniques and tips for mastering Español, but I am sure that you can apply them to most of the other languages.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>If you expected only post on IT topics on this blog, I am sorry :(
Today I&rsquo;ll share you my experience in learning a new language. I am practicing these techniques and tips for mastering Español, but I am sure that you can apply them to most of the other languages.</p>
<h2 id="gamification">Gamification</h2>
<p>If you are only starting to learn then this will be helpful for you in double. Most of the people give up learning after several weeks or even days after they begin. It&rsquo;s because of their motivation. It becomes lower with time. And in the beginning when you know almost nothing and understand that you should work hard and work a lot to learn. It really demotivates. And I think elements of gamification will help to increase motivation and turn learning process to habit. You can gamify your learning in any way you want. I recommend:</p>
<ul>
<li><a href="https://www.duolingo.com/">Duolingo</a> or <a href="https://lingualeo.com/">Linguoleo</a> - good for learning the basics of the language</li>
<li><a href="https://tinycards.duolingo.com/">Tinycard</a> - for learning/memorizing new words</li>
</ul>
<h2 id="listen">Listen</h2>
<p>As soon as you have learned basics, start listening to podcasts. There are some free audio podcasts oriented for beginners. For example, in Spanish, it&rsquo;s a <a href="https://www.notesinspanish.com/">Notes in Spanish</a>.</p>
<p>Create a playlist with up to 20 songs you like on language you learn and listen to them regularly. And keep listening until you will understand what a song about. You don&rsquo;t have to translate all the words in each song. In fact, It is not very useful. But opposite, when you learn new words from textbook, tinycard or from anywhere else and you hear this word while you are listening to your playlist you learn better.</p>
<blockquote>
<p>&ndash; Oh, wait. I know what this word means &hellip;</p>
</blockquote>
<h2 id="watch">Watch</h2>
<p>If you are fun of TV-shows, you can start watching them in the language you learn. With subtitles or without them. Of course, it requires some basic knowledge. If you care that you will miss some key points of the story and prefer to fully understand it, you can watch sitcoms which don&rsquo;t have almost anything in common between series.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Oauth2 Proxy for Kubernetes Services</title>
      <link>https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/</link>
      <pubDate>Sun, 20 May 2018 21:30:36 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/</guid>
      <description>&lt;p&gt;Hello, folks!&lt;/p&gt;
&lt;p&gt;In this post, I will go through configuring &lt;a href=&#34;https://github.com/bitly/oauth2_proxy&#34;&gt;Bitly OAuth2 proxy&lt;/a&gt; in a kubernetes cluster.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hello, folks!</p>
<p>In this post, I will go through configuring <a href="https://github.com/bitly/oauth2_proxy">Bitly OAuth2 proxy</a> in a kubernetes cluster.</p>
<h3 id="upd-5082025">UPD 5/08/2025</h3>
<p>There is a fresh tutorial about <a href="https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/">oauth2-proxy</a></p>
<hr>
<p>A few days ago I was configuring <a href="https://en.wikipedia.org/wiki/Single_sign-on">SSO</a> for our internal dev-services in <a href="https://github.com/KazanExpress">KE Technologies</a>.</p>
<p>And I spent the whole to make it work properly, and at the end I decided that I will share my experience by writing this post, hoping that it will help others(and possibly me in the future) to go through this process.</p>
<!-- toc -->
<h1 id="what-do-we-want">What do we want?</h1>
<p>We have internal services in our k8s cluster that we want to be accessible for developers.
It can be <em>kubernetes-dashboard</em> or <em>kibana</em> or anything else.</p>
<p>Before that we used Basic Auth, it&rsquo;s <a href="https://banzaicloud.com/blog/ingress-auth/">easy to setup</a> in ingresses. But this approach has several disadvantages:</p>
<ol>
<li>We need to share a single pair of <em>login</em> and <em>password</em> for all services among all developers</li>
<li>Developers will be asked to enter credentials each time when they access service first time</li>
</ol>
<p>What we want is that developer will log in once and will have access to all other services without additional authentication.</p>
<p>So, a possible scenario could be:</p>
<ol>
<li>Developers open <a href="https://kibana.example.com">https://kibana.example.com</a> which is internal service</li>
<li>Browser redirects them to <a href="https://auth.example.com">https://auth.example.com</a> where they sign in</li>
<li>After successful authentication browser redirects them to <a href="https://kibana.example.com">https://kibana.example.com</a></li>
</ol>
<h1 id="preparation">Preparation</h1>
<h2 id="update">Update</h2>
<h3 id="upd-10073018">UPD 1.0(07/30/18)</h3>
<p>Using kube-lego for configuring Let&rsquo;s Encrypt certificates is depricated now. Consider using <a href="https://github.com/jetstack/cert-manager">cert-manager</a> instead.</p>
<h3 id="upd-20082418">UPD 2.0(08/24/18)</h3>
<p>Initialy, when I was writing this post I was using old version of nginx 0.9.0, because it did not work correctly on newer version. Now, I found the problem and it have been fixed in 0.18.0 release. But ingress exposing private services should be updated(<a href="https://github.com/helm/charts/issues/5958#issuecomment-408457931">more details</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="l">https://auth.example.com/oauth2/start?rd=https://$host$request_uri$is_args$args</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-proxy.svc.cluster.local:4180/oauth2/auth</span><span class="w">
</span></span></span></code></pre></div><h2 id="kubernetes">Kubernetes</h2>
<p>First of all, we need a Kubernetes cluster. I will use the newly created cluster in <strong>Google Cloud Platform</strong> with version <strong>1.8.10-gke.0</strong>. If you have a cluster with configured ingress and https you can skip this step.</p>
<p>Then we need to install <a href="https://github.com/kubernetes/ingress-nginx"><strong>nginx ingress</strong></a> and <a href="https://github.com/jetstack/kube-lego"><strong>kube lego</strong></a>. Let&rsquo;s do it using helm:</p>
<h3 id="init-helm">Init helm</h3>
<p>With RBAC:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># giving default accout admin role:</span>
</span></span><span class="line"><span class="cl"><span class="nv">ACCOUNT</span><span class="o">=</span><span class="k">$(</span>gcloud info --format<span class="o">=</span><span class="s1">&#39;value(config.account)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create clusterrolebinding owner-cluster-admin-binding <span class="se">\
</span></span></span><span class="line"><span class="cl">    --clusterrole cluster-admin <span class="se">\
</span></span></span><span class="line"><span class="cl">    --user <span class="nv">$ACCOUNT</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl -n kube-system create sa tiller
</span></span><span class="line"><span class="cl">kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount<span class="o">=</span>kube-system:tiller
</span></span><span class="line"><span class="cl">helm init --service-account tiller
</span></span></code></pre></div><p>without RBAC:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm init
</span></span></code></pre></div><h3 id="install-nginx-ingress">Install nginx-ingress</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm install stable/nginx-ingress --name nginx-ing --namespace nginx-ing <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set controller.image.repository<span class="o">=</span>gcr.io/google_containers/nginx-ingress-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set controller.image.tag<span class="o">=</span><span class="s2">&#34;0.9.0-beta.15&#34;</span>
</span></span><span class="line"><span class="cl">    --set rbac.create<span class="o">=</span><span class="nb">true</span> <span class="c1"># if RBAC is enabled in the cluster</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># see all options here: https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/values.yaml</span>
</span></span></code></pre></div><p>After it&rsquo;s installed we can retrieve controller IP address:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl --namespace nginx-ing get services -o wide -w nginx-ing-nginx-ingress-controller
</span></span></code></pre></div><p>and create DNS record to point our domain and subdomains to this IP address.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">A       example.com     xxx.xxx.xx.xxx
</span></span><span class="line"><span class="cl">CNAME   *.example.com   example.com
</span></span></code></pre></div><h3 id="install-kube-lego">Install kube-lego</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm install --name kube-lego stable/kube-lego --namespace kube-lego <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set config.LEGO_SUPPORTED_INGRESS_CLASS<span class="o">=</span>nginx <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set config.LEGO_SUPPORTED_INGRESS_PROVIDER<span class="o">=</span>nginx <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set config.LEGO_DEFAULT_INGRESS_CLASS<span class="o">=</span>nginx <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set config.LEGO_URL<span class="o">=</span>https://acme-v01.api.letsencrypt.org/directory <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set rbac.create<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set image.tag<span class="o">=</span>0.1.5 <span class="se">\
</span></span></span><span class="line"><span class="cl">    --set config.LEGO_LOG_LEVEL<span class="o">=</span>debug
</span></span></code></pre></div><h3 id="test-it">Test it!</h3>
<p>Let&rsquo;s run simple HTTP server as service and expose it using nginx ingress:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl run simple-http --image<span class="o">=</span>strm/helloworld-http --port<span class="o">=</span><span class="m">80</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl expose deployment simple-http --name example-service --port<span class="o">=</span><span class="m">80</span> --target-port<span class="o">=</span><span class="m">80</span> --type<span class="o">=</span>NodePort
</span></span></code></pre></div><p><strong>example-ing.yaml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">service.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">example-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="s2">&#34;service.example.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">ing-tls</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f example-ing.yaml
</span></span></code></pre></div><p>Wait for a few seconds and open <a href="https://service.example.com">https://service.example.com</a> and you should see something similar to this:</p>



<div class="figure " >
  
    <img class="fig-img" src="https://user-images.githubusercontent.com/7482065/40230488-575867a4-5aa0-11e8-907c-8878b0b951e6.png"  alt="Example">
  
   
    <span class="caption">Example</span>
  
</div>

<h2 id="github-application">GitHub application</h2>
<p>In this post, we will use GitHub accounts for authentication.</p>
<p>So, go to <a href="https://github.com/settings/applications/new">https://github.com/settings/applications/new</a> and create new OAuth application</p>
<p>Fill <strong>Authorization callback URL</strong> field with <a href="https://auth.example.com/oauth2/callback">https://auth.example.com/oauth2/callback</a> where <em>example.com</em> is your domain name.</p>



<div class="figure [classes]" >
  
    <img class="fig-img" src="https://user-images.githubusercontent.com/7482065/40223994-7f7c7f94-5a8d-11e8-97db-9ca6e0809e0a.png"  alt="GitHub Application">
  
   
    <span class="caption">GitHub Application</span>
  
</div>

<p>After creating an application you will have <strong>Client ID</strong> and <strong>Client Secret</strong> which we will need in next step.</p>
<h1 id="deploy-oauth-proxy">Deploy OAuth Proxy</h1>
<p>There are a lot of docker images for OAuth proxy, but we can not use them because they do not support domain white-listing. The problem is that such functionality has not implemented yet.</p>
<p>Actualy there are several PRs that solve that problem but seems to be they frozen for an unknown amount of time.</p>
<p>So, the only thing I could do is to merge one of the PRs to current master and build own image.</p>
<p>You also can use my image, but if you worry about security just clone <a href="https://github.com/alikhil/oauth2_proxy">my fork</a> and build image yourself.</p>
<p>Let&rsquo;s create a namespace and set it as current:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create ns oauth-proxy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kns oauth-proxy <span class="c1"># I am using kubectx tool -&gt; https://github.com/ahmetb/kubectx</span>
</span></span></code></pre></div><h2 id="deploy-secret">Deploy secret</h2>
<p><strong>secret.yml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">github-client-id</span><span class="p">:</span><span class="w"> </span><span class="l">base64(YOUR_CLIENT_ID)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">github-client-secret</span><span class="p">:</span><span class="w"> </span><span class="l">base64(YOUR_CLIENT_SECRET)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cookie-secret</span><span class="p">:</span><span class="w"> </span><span class="l">base64(random_string)</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f secret.yml
</span></span></code></pre></div><h2 id="deploy-deployment">Deploy deployment</h2>
<p><strong>oauth-proxy.deployment.yml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">alikhil/oauth2_proxy:2.2.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">provider=github</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">email-domain=*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">upstream=file:///dev/null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">http-address=0.0.0.0:4180</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">whitelist-domain=.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- --<span class="l">cookie-domain=.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># - --cookie-expire duration: expire timeframe for cookie (default 168h0m0s)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># - --cookie-name string: the name of the cookie that the oauth_proxy creates (default &#34;_oauth2_proxy&#34;)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># - --cookie-refresh duration: refresh the cookie after this duration; 0 to disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># - --cookie-secret string: the seed string for secure cookies (optionally base64 encoded)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">OAUTH2_PROXY_CLIENT_ID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">github-client-id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">OAUTH2_PROXY_CLIENT_SECRET</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">github-client-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">OAUTH2_PROXY_COOKIE_SECRET</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">cookie-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">4180</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f oauth-proxy.deployment.yml
</span></span></code></pre></div><h2 id="deploy-service">Deploy service</h2>
<p><strong>oauth-service.yml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">4180</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">4180</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f oauth-service.yml
</span></span></code></pre></div><h2 id="deploy-ingress">Deploy ingress</h2>
<p><strong>oauth-ing.yml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">auth.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">4180</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">auth.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-proxy-tls</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f oauth-ing.yml
</span></span></code></pre></div><h2 id="test-it-1">Test it!</h2>
<p>You can update ingress that we used while configuring nginx-ingress or create a new one:</p>
<p><strong>example-ing.yml</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">https://auth.example.com/oauth2/auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="l">https://auth.example.com/oauth2/start?rd=https://$host$request_uri$is_args$args</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">service.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">example-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">service.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">ing-tls</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f example-ing.yml
</span></span></code></pre></div><p>Then visit service.example.com and you will be redirected to GitHub authorization page:</p>



<div class="figure " >
  
    <img class="fig-img" src="https://user-images.githubusercontent.com/7482065/40262777-feb43e2a-5b12-11e8-92dd-d7d066d61c50.png"  alt="GitHub Authorization page">
  
   
    <span class="caption">GitHub Authorization page</span>
  
</div>

<p>And once you authenticate, you will have access to all your services under ingress that point to auth.example.com until cookie expires.</p>
<p>And that&rsquo;s it! Now you can put any of your internal services behind ingress with OAuth.</p>
<h2 id="resources">Resources</h2>
<p>Here is a list of resources that helped me to go through this proccess first time:</p>
<ul>
<li><a href="https://eng.fromatob.com/post/2017/02/lets-encrypt-oauth-2-and-kubernetes-ingress/">https://eng.fromatob.com/post/2017/02/lets-encrypt-oauth-2-and-kubernetes-ingress/</a></li>
<li><a href="https://www.midnightfreddie.com/oauth2-proxy.html">https://www.midnightfreddie.com/oauth2-proxy.html</a></li>
<li><a href="https://thenewstack.io/single-sign-on-for-kubernetes-dashboard-experience/">https://thenewstack.io/single-sign-on-for-kubernetes-dashboard-experience/</a></li>
</ul>]]></content:encoded>
    </item>
    
    <item>
      <title>Say hello to Hugo</title>
      <link>https://alikhil.dev/posts/say-hello-to-hugo/</link>
      <pubDate>Thu, 17 May 2018 16:03:44 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/say-hello-to-hugo/</guid>
      <description>&lt;p&gt;Today I migrated my blog to &lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt; engine. So, it&amp;rsquo;s my first post here, yaay!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Today I migrated my blog to <a href="https://gohugo.io">Hugo</a> engine. So, it&rsquo;s my first post here, yaay!</p>
<p>There were only 2 posts in my last blog and I decided to not migrate the one about creating blog in Jekyll. Since it is not actual now.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Go Quickstart</title>
      <link>https://alikhil.dev/posts/go-quickstart/</link>
      <pubDate>Fri, 06 Apr 2018 16:22:35 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/go-quickstart/</guid>
      <description>&lt;p&gt;Hi folks!
It&amp;rsquo;s been a long time since I have published the last post, but now I came back with short quickstart guide in &lt;strong&gt;Go&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this tutorial, we will configure Go environment in VS Code and write our first program in Go.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hi folks!
It&rsquo;s been a long time since I have published the last post, but now I came back with short quickstart guide in <strong>Go</strong>.</p>
<p>In this tutorial, we will configure Go environment in VS Code and write our first program in Go.</p>
<h3 id="install-go">Install Go</h3>
<p>The first thing that you need to do it&rsquo;s to install Go on your computer. To do so, download installer for your operating system from  <a href="https://golang.org/dl/">here</a> and then run the installer.</p>
<h3 id="configure-gopath">Configure GOPATH</h3>
<p>By language convention, Go developers store all their code in a single place called <em>workspace</em>. Go also puts dependency packages in the workspace. So, in order to Go perform correctly, we need to set <code>GOPATH</code> variable with the path to the workspace.</p>
<h4 id="macos-and-linux">MacOS and Linux</h4>
<p>Set the <code>GOPATH</code> envar with workspace</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GOPATH</span><span class="o">=</span><span class="nv">$HOME</span>/go
</span></span></code></pre></div><p>Also, we need to add <code>GOPATH/bin</code> to <code>PATH</code> in order to run compiler Go programs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:<span class="nv">$GOPATH</span>/bin
</span></span></code></pre></div><h3 id="configure-vs-code">Configure VS Code</h3>
<p>Install <a href="https://github.com/Microsoft/vscode-go">official Go extension</a>.</p>
<p>Install delve debugger:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go get -u github.com/derekparker/delve/cmd/dlv
</span></span></code></pre></div><p>I recommend you to add the following lines to your VS Code user settings:</p>
<p><strong>settings.json</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;go.autocompleteUnimportedPackages&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;go.formatTool&#34;</span><span class="p">:</span> <span class="s2">&#34;gofmt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h4 id="windows">Windows</h4>
<p>Create <code>GOPATH</code> envar:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">set</span> <span class="nv">GOPATH</span><span class="o">=</span>c:<span class="se">\U</span>sers<span class="se">\%</span>USERNAME%<span class="se">\g</span>o
</span></span></code></pre></div><p>Also, we need to add <code>GOPATH\bin</code> to <code>PATH</code> in order to run compiler Go programs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">set</span> <span class="nv">PATH</span><span class="o">=</span>%PATH%<span class="p">;</span>%GOPATH%<span class="se">\b</span>in
</span></span></code></pre></div><h3 id="create-project">Create project</h3>
<p>Move to your <code>GOPATH/src</code> directory. Create a directory for your project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> <span class="nv">$GOPATH</span>/src
</span></span><span class="line"><span class="cl">mkdir -p github.com/alikhil/hello-world-with-go
</span></span></code></pre></div><p>Open it using vscode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">code github.com/alikhil/hello-world-with-go
</span></span></code></pre></div><h3 id="hello-world">Hello World</h3>
<p>Let&rsquo;s create a file named <code>program.go</code> and put the following code there:</p>
<p><strong>program.go</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;¡Hola, mundo!&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="run-the-program">Run the program</h3>
<p>Finally, to run the program by pressing the <code>F5</code> button in VS Code and you should see the message printed to <em>Debug Console</em>.</p>
<p>That&rsquo;s all! My congratulations, you have just written your first program in Go!</p>
<h3 id="troubleshooting">Troubleshooting</h3>
<p>If you fail to run your program and there is some message like <strong>&ldquo;Cannot find a path to <code>go</code>&rdquo;</strong>.
Try to add to your <code>PATH</code> envar with path directory where <code>go</code> binary is stored.</p>
<p>For example in MacOS I have added following line to my <code>~/.bash_profile</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span>/usr/local/go/bin:<span class="nv">$PATH</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
  </channel>
</rss>
