<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Blog on Nikola Milojević</title>
        <link>https://nikola.milojevic.me/blog/</link>
        <description>Recent content in Blog on Nikola Milojević</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sun, 17 Mar 2024 17:14:15 +0100</lastBuildDate>
        <atom:link href="https://nikola.milojevic.me/blog/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Docker, Kubernetes and the Walking Dead</title>
            <link>https://nikola.milojevic.me/blog/docker-init/</link>
            <pubDate>Sun, 17 Mar 2024 17:14:15 +0100</pubDate>
            <guid>https://nikola.milojevic.me/blog/docker-init/</guid>
            <description>&lt;p&gt;Yesterday, I was rewriting the screenshot utility that is used across MailerLite and MailerSend. I wrote it several years ago using Express.js and Puppeteer. It was a quick hack as we needed a microservice that would return a PNG or PDF of a page via API. We noticed some problems with microservice as we have grown, such as memory leaks and occasional unresponsiveness. We also wanted to rewrite it in Golang so that it could make use of goroutines and concurrency.  Yesterday, while I was rewriting it in Go and using the exceptional library &lt;a href=&#34;https://github.com/go-rod/rod&#34;&gt;go-rod&lt;/a&gt;, I noticed that we still had occasional zombie Chromium processes.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Yesterday, I was rewriting the screenshot utility that is used across MailerLite and MailerSend. I wrote it several years ago using Express.js and Puppeteer. It was a quick hack as we needed a microservice that would return a PNG or PDF of a page via API. We noticed some problems with microservice as we have grown, such as memory leaks and occasional unresponsiveness. We also wanted to rewrite it in Golang so that it could make use of goroutines and concurrency.  Yesterday, while I was rewriting it in Go and using the exceptional library <a href="https://github.com/go-rod/rod">go-rod</a>, I noticed that we still had occasional zombie Chromium processes.</p>
<p>Then it hit me - we&rsquo;re running Docker and Kubernetes, and the containers in Docker don&rsquo;t have a process management system in place. This means that if a process in a container spawns child processes, they exist without waiting for termination. Thus, child processes become zombie processes, and they pile up and cause instability. Without an init system, there is no process to clean up zombie processes.</p>
<p>Since Docker 1.13.0, they have added the &ndash;init flag when you run Docker via the CLI. This is acceptable for our local development purposes, however, in the production environment, we are utilizing GKE and required a solution to address the issue. There are a couple of ways to run it in k8s:</p>
<ol>
<li>Use tini/dumb-init or any other init system as ENTRYPOINT in your Dockerfile, with CMD as your program:</li>
</ol>
<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">debian:stable-slim</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">RUN</span> apt install tini<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">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;tini&#34;</span> <span class="s2">&#34;--&#34;</span><span class="p">]</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="o">[</span><span class="s2">&#34;/app/screenshoter&#34;</span><span class="err">
</span></span></span></code></pre></div><ol start="2">
<li>Use init container that starts before your main app in Kubernetes deployment:</li>
</ol>
<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">Pod</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">screenshoter</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">initContainers</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">init-tini</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># sample image with tini</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">krallin/ubuntu-tini:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;tini&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;true&#34;</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">screenshoter</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">screenshoter:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/app/screenshoter&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>After init container completes, any zombie process will be reaped by <code>tini</code>.</p>
<ol start="3">
<li>(theoretically) set <code>shareProcessNamespace: true</code> in <code>securityContext</code> in Kubernetes. I haven&rsquo;t tried this and I do not plan to, as I only touch <code>securityContext</code> when there is an absolute need for it.</li>
</ol>
<p>I chose to go with option number 1 as it was the simplest and I like to have same setup locally and on all other environments, meaning same Docker image with init system both locally with Docker (actually, Orbstack) and in dev/prod in GKE.</p>
<p><a href="https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals">If you read more carefully than me</a>, you will find out that even NodeJS doesn&rsquo;t recommend running NodeJS processes as PID 1 as they would not respond to SIGINT. This isn&rsquo;t only applicable for NodeJS, but for any process like this, as we can see that it also happens on Golang app.</p>
]]></content>
        </item>
        
        <item>
            <title>Be curious</title>
            <link>https://nikola.milojevic.me/blog/be-curious/</link>
            <pubDate>Sun, 28 Feb 2021 00:35:46 +0100</pubDate>
            <guid>https://nikola.milojevic.me/blog/be-curious/</guid>
            <description>&lt;p&gt;It seems that in my circle of friends, as you grow older, it is harder for you to learn new things and to actually be motivated to learn something new. This doesn’t only relate to tech - but general things in life. For me, that isn’t the case, and my simple hack for this is to &lt;strong&gt;be more curious than judgmental&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Not knowing something isn’t stupid - not wanting to learn new things is.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>It seems that in my circle of friends, as you grow older, it is harder for you to learn new things and to actually be motivated to learn something new. This doesn’t only relate to tech - but general things in life. For me, that isn’t the case, and my simple hack for this is to <strong>be more curious than judgmental</strong>.</p>
<p>Not knowing something isn’t stupid - not wanting to learn new things is.</p>
]]></content>
        </item>
        
        <item>
            <title>His Airness - Apple Silicon M1</title>
            <link>https://nikola.milojevic.me/blog/apple-silicon/</link>
            <pubDate>Thu, 11 Feb 2021 22:13:56 +0100</pubDate>
            <guid>https://nikola.milojevic.me/blog/apple-silicon/</guid>
            <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;
&lt;p&gt;On Nov 10th Apple confirmed the rumors that they are cutting ties with Intel CPUs and starting using their own - Apple Silicon ARM-based CPUs. While that came as a surprise to some, Apple has been working for years to make this transition. They don&amp;rsquo;t want to rely on Intel more, and Intel has arguably slowed down the evolution of CPUs in the last few years.&lt;/p&gt;
&lt;p&gt;Long story short, Apple has announced two M1 powered MacBooks as a start - MacBook Air with passive cooling and with all physical keys and MacBook Pro with active cooling and Touch Bar.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<h2 id="intro">Intro</h2>
<p>On Nov 10th Apple confirmed the rumors that they are cutting ties with Intel CPUs and starting using their own - Apple Silicon ARM-based CPUs. While that came as a surprise to some, Apple has been working for years to make this transition. They don&rsquo;t want to rely on Intel more, and Intel has arguably slowed down the evolution of CPUs in the last few years.</p>
<p>Long story short, Apple has announced two M1 powered MacBooks as a start - MacBook Air with passive cooling and with all physical keys and MacBook Pro with active cooling and Touch Bar.</p>
<p>I wanted to wait for the second iteration of Apple Silicon chips and MacBooks as I thought that the transition process won&rsquo;t be that smooth. I remember the transition from PowerPC to Intel - it wasn&rsquo;t nice and I expected general problems: slowness, not-so-fast adoption, and just regular bugs. After the first reviews popped in they were surprisingly positive - even from hard-core Apple haters. Again, I opted-in to wait. I had my MacBook Pro 2018 15.4&quot; with i7 and 16GB RAM, why would I need a new computer? Well, again, I was wrong, as the IT professionals, devs, SREs, started showing numbers from new MacBooks in their regular work and the general feeling is that we have something magical here.</p>
<p>I was hooked. Pulled the trigger and ordered <strong>MacBook Air M1 with 16GB RAM</strong>.</p>
<h2 id="software">Software</h2>
<p>Big Sur came out with a shiny new design which I don&rsquo;t like completely. There is much-unused space, rounded corners of the Windows, etc. But, this is not about Apple&rsquo;s software, more of third party software that I daily use. I will just list the ones that have native versions. You can always use <a href="https://isapplesiliconready.com/">https://isapplesiliconready.com/</a> to check if some software not listed here has a native ARM version.
Native:</p>
<ul>
<li>Brave</li>
<li>Slack</li>
<li>VSCode (Insiders version)</li>
<li>Firefox</li>
<li>iTerm</li>
<li>Telegram</li>
<li>TablePlus</li>
<li>PyCharm</li>
<li>Goland</li>
<li>Fantastical</li>
<li>Paw</li>
<li>Bear</li>
<li>Things</li>
<li>iAWriter</li>
<li>Parallels Preview</li>
<li>nodejs (npm)</li>
<li>python</li>
<li>vim</li>
<li>postgres</li>
<li>brew</li>
</ul>
<p>Non-native (for now):</p>
<ul>
<li>Dropbox</li>
<li>Stay (support incoming in 1.4)</li>
<li>Spotify</li>
<li>Signal</li>
<li>Golang and all tools related to it (kubectl, helm, etc)</li>
</ul>
<p>Golang 1.16 is coming out this or the next week and then all tools related to it will support aarch64.</p>
<h2 id="rosetta-2">Rosetta 2</h2>
<p>When the app isn&rsquo;t complied natively, it goes through a transition layer called Rosetta 2. Rosetta has to &ldquo;translate&rdquo; the app for Silicon to &ldquo;understand&rdquo; and the first start of the app lasts 5-20s. Each next start of the app is like a native app, no matter if you restart your computer or not.</p>
<h2 id="brew---to-arm-or-not-to-arm">Brew - to ARM or not to ARM</h2>
<p>With 3.0.0 version, Brew officially supports installations in <code>/opt/homebrew</code>. For now, the preferred way is to have it running both, one in the (good) ol&rsquo; <code>/usr/local</code> directory, and the other in <code>/opt/homebrew</code>. After that you should put both in the path, but <code>/opt/homebrew</code> first.</p>
<h2 id="virtualization">Virtualization</h2>
<p>When Apple announced the M1 the biggest problem with it is how to run virtualization, aka Windows and Linux in virtual machines. Unfortunately, I have to use Windows for a few things - managing my company&rsquo;s bank account and some government access. Windows 10 has insider preview for ARM64, and virtualization software Parallels has Technical Preview with support for arm64 of W10/Ubuntu.</p>
<p>My experience with it has been silky smooth - it passes through my USB token, Java e-banking software installs correctly and everything works. Kudos Parallels devs!</p>
<h2 id="docker-technical-preview">Docker technical preview</h2>
<p>Docker is a go-to environment for almost any dev these days. Its performance on Mac has historically been a major pain, mostly because of the way how it goes around macOS limitation to use the filesystem. Read no further, it is still awful, but this time a bit better because everything works faster because of the new SoC.</p>
<p>Other than that - you will need arm64 images of your apps to achieve better performance. You can still run the amd64 image without any problems via QEMU emulation (Docker does it automatically if it doesn&rsquo;t find arm64 version of the container), but it will be slower.</p>
<p>I have set up our images at The Remote Company to be built on GitHub Actions for both amd64 and arm64 images and they work pretty much great. Other than that, there are still performance issues vs the Linux version of Docker.</p>
<h2 id="why-air-instead-of-pro-for-power-users">Why Air instead of Pro for power users?</h2>
<p>This machine is cold. Colder than the heart of my mother-in-law. <em>Hopefully she won&rsquo;t read this, but my wife will. I regret nothing! Please take care of my cat.</em> It has no fans so it is silent. Of course, I generally run things that require a short burst of performance, not constant performance like rendering a video, then I would suggest you go with MacBook Pro as its performance is a little bit better as it doesn&rsquo;t throttle under high load for a long time.</p>
<p>Other than that, I dislike touch bar from the bottom of my heart. Air still has physical keys and that is what nudged me to it.
Apple has been stubborn for years with the touch bar that 99% of power users are annoyed with, but the reports say that with incoming 14&quot; and 16&quot; MacBook Pros they will finally go back to physical keys.</p>
<p>Good business model - introducing old things as new that users will like.</p>
<h2 id="keyboard">Keyboard</h2>
<p>Let&rsquo;s face it, all Apple keyboards after MacBook Pro 2015 have been abysmal. The butterfly keyboard is a failure that broke in all 3 of its iteration. Last year Apple announced that they are going back to the old-style keyboard on MacBook Pro 2020, and they have continued with that practice on M1 MacBooks. The new Magic keyboard is amazing - it feels even better than the old favorite MacBook Pro 2015.</p>
<p>I guess anything is better than the piece of plywood called MacBook Pro 2016-2020 keyboard. May it rest in peace or for someone, literally in pieces.</p>
<h2 id="battery-life">Battery life</h2>
<p>I almost forgot one big improvement. Battery life on M1 is crazy. I regularly get 12-16 hours, depending on the usage, and I don&rsquo;t even use Safari. Knowing that you can go a full day without charging the battery or even two working days is a great thing to have.</p>
<h2 id="npm-performance-benchmark">NPM performance benchmark</h2>
<p>Now, a few benchmarks. I compared nodejs compilation time on both of my machines - MacBook Pro 2018 (i7 6c, 12t, 16GB RAM) and MacBook Air M1. Both have nodejs v15.8.0 natively installed and both have the tests running the same commands in docker image <code>node:v15.8.0-buster</code>.</p>
<p><em>Native runs:</em></p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">Command</th>
					<th style="text-align: left">Time</th>
					<th style="text-align: left">Device</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">npm install</td>
					<td style="text-align: left">40.3s</td>
					<td style="text-align: left">MBP 15 2018</td>
			</tr>
			<tr>
					<td style="text-align: left">npm install</td>
					<td style="text-align: left">9.8s</td>
					<td style="text-align: left">MBA M1 2020</td>
			</tr>
			<tr>
					<td style="text-align: left">npm run dev</td>
					<td style="text-align: left">25s</td>
					<td style="text-align: left">MBP 15 2018</td>
			</tr>
			<tr>
					<td style="text-align: left">npm run dev</td>
					<td style="text-align: left">12.8s</td>
					<td style="text-align: left">MBA M1 2020</td>
			</tr>
	</tbody>
</table>
<p><em>Docker runs:</em></p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">Command</th>
					<th style="text-align: left">Time</th>
					<th style="text-align: left">Device</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">npm install</td>
					<td style="text-align: left">3m22s</td>
					<td style="text-align: left">MBP 15 2018</td>
			</tr>
			<tr>
					<td style="text-align: left">npm install</td>
					<td style="text-align: left">1m47s</td>
					<td style="text-align: left">MBA M1 2020</td>
			</tr>
			<tr>
					<td style="text-align: left">npm run dev</td>
					<td style="text-align: left">1m5s</td>
					<td style="text-align: left">MBP 15 2018</td>
			</tr>
			<tr>
					<td style="text-align: left">npm run dev</td>
					<td style="text-align: left">37s</td>
					<td style="text-align: left">MBA M1 2020</td>
			</tr>
	</tbody>
</table>
<p>You can conclude yourself, but in most of the tests, Silicon has been more than 50% faster.</p>
<h2 id="problems">Problems</h2>
<p>The only problem that I had is that my USB-c screen stopped showing the correct color range as Macbook now recognizes it as a TV, so it has a limited color output. I use Caldigit TS3+ and LG38WK95-c and my workaround was to buy a USB type-c to HDMI adapter and connect it to Caldigit and colors are now fine. A little bit frustrating, but a software problem that I hope some future Big Sur version will fix.</p>
<h2 id="conclusion">Conclusion</h2>
<p>MBA is the best value for money in the market currently. ~1600€ for a 16GB/512SSD version that outperforms even desktop CPUs in a small and portable package as Macbook 13&quot; is truly astonishing and I&rsquo;m excited to see how will new Pro machines perform with improved M1s and more RAM as M1 is limited to 16GB.</p>
<p>Bravo, Apple!</p>
]]></content>
        </item>
        
        <item>
            <title>There&#39;s some fear in letting go</title>
            <link>https://nikola.milojevic.me/blog/remote-work/</link>
            <pubDate>Sat, 24 Oct 2020 13:52:55 +0200</pubDate>
            <guid>https://nikola.milojevic.me/blog/remote-work/</guid>
            <description>&lt;p&gt;I kinda hate myself for writing this post like a revelation in the times of pandemic and talking about remote work while it is &lt;em&gt;hot topic&lt;/em&gt; . In reality, I have been working 100% remotely for more than 2 years (I previously worked for more than 8 years in the offices) and right now I feel I can tell my experiences about remote work and why I will probably never go back to a regular office job.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>I kinda hate myself for writing this post like a revelation in the times of pandemic and talking about remote work while it is <em>hot topic</em> . In reality, I have been working 100% remotely for more than 2 years (I previously worked for more than 8 years in the offices) and right now I feel I can tell my experiences about remote work and why I will probably never go back to a regular office job.</p>
<p>Around 2 years ago I got the offer to join MailerLite as a remote contractor. I was over-thinking whether I should start working remotely, what would it mean for me, my habits, social life, and my career progress. There were too many unknowns at that point as I have only done some freelance gigs remotely, but have been involved with distributed teams all over the world in previous companies. I took the red pill and my remote journey began.</p>
<p>All my doubts were cleared right away.</p>
<p>The first one - traffic. I hate the traffic in Belgrade. The city isn’t building its infrastructure (roads, bridges) as fast as it is growing and it is showing. On a good day, it took around 30 minutes of driving to work, which is not a problem. But, in the majority of days going to work and back home took on average 2 hours. I say 2 hours because you have to take into account that you have to find a parking space, probably 10 minutes from the office, and then walk to the office which all adds to 2 hours of lost time each day. If you calculate - it means that you are losing <strong>22 days per year</strong> in the traffic.</p>
<p>What could you do with that extra <em>time</em> that you have? I remember one analogy when I talked to one of my friends as he said: “It is like when people are telling that smokers could put all money they spend on cigarettes aside and can buy a new car after some time of saving. Then you see people who stopped smoking and after some time you don’t see them with a new car”. That wasn’t the case for me. I rediscovered hobbies that I have stopped practicing because of lack of time - started playing guitar more, recording new music, and most importantly, I had time to read more non-business related books. You can become dull and get into a work-home-sleep routine because of the lack of time.</p>
<p>Micromanagement was the other thing that I thought could happen in remote work. Fortunately, it didn’t. It is just a matter if you trust your employees or not. If you don’t trust your employees, you either didn’t hire the right people for your company or you are suffering from trust issues and adapting to the situation. Usually, it is the second thing. People who slack off because there aren’t any measures of control because they are working remotely show their faces and bad habits really fast and you can easily filter them.</p>
<p>When working remotely, <strong>you are your own manager</strong>. You must plan your time and goals, and how much you give yourself to work. Not too little and not too much, balance, as always, is the key.</p>
<p>There are so many tools to make communication while working remotely easy. We utilize Slack, have regular calls on Zoom, write our monthly feedback on 15five, and much more. If we need to brainstorm, we get on a video call or write in Slack.</p>
<p>There is one potential downside of working remotely for one target group - junior engineers. I will explain why I don’t think it is a rule but purely depends on the person. While junior engineers can always sit next to their mentors in a regular office job, here it is a bit different. Your company has to have really good documentation and processes so they don’t get lost. But, on the other hand, at MailerLite we are always looking for persons that are “self-starters” and can pick-up things themselves without or with minimal interference from other colleagues. In my experience, juniors can struggle at the beginning for the first (half of) year, but after that, they are usually stronger with knowledge and work ethics than juniors in regular offices. They are more independent in work and can easily learn new things and implement them.</p>
<p>The key thing in remote work is overcommunication. I can’t stress enough how much it is important. As a pitfall of remote work, you must write and overshare because there is no such thing as over-communication. Why do we do that? We are a remote-first company, but we have an office in Vilnius if someone wants to work from it. A couple of people are working from the office, but as I have been there a couple of times it is the most silent office I have ever been in. The communication is done via Slack or conference calls with everyone, so no one is left out, as this can be a problem when you have a combination of office and remote work in your company.</p>
<p>Productivity, personally, is sky-rocketing once I switched to the remote. I get at least 50% more stuff done as there are no distractions, unnecessary small-talks, listening to office gossips, braggings, and other things that can happen in the office.</p>
<p>Freedom is also an important thing. Sometimes I have to do something personal in the morning, or the middle of the day. Sometimes, I can go to the gym during the day and go back refreshed and start working. I can go to my hometown when I want to and work from there. I can travel wherever I want and work from there. I have become more social as I have more time for my friends and family and am less stressed. No one is looking at how much time you are spending working - but if you did the task or not which is the only important thing.</p>
<p>The biggest indicator that remote work is a benefit for both an employee and the company is that Microsoft and Google are now permanently remote if you want to. Pandemic nudged them in that direction, but they have seen the benefits and adapted.</p>
<p>Times are changing. Gosh, what a cliche sentence. <strong>Times are always changing, it is up to us if we want to adapt</strong>. Remote work has been around for a long time, but most of the companies resisted the change. I’m happy that companies are now forced to change because it requires a change of mindset, but there is a quote from my favourite TV series of all time, Twin Peaks, <em>“There’s some fear in letting go”</em>.</p>
]]></content>
        </item>
        
        <item>
            <title>How to boost email throughput</title>
            <link>https://nikola.milojevic.me/blog/unbound-email/</link>
            <pubDate>Sun, 18 Oct 2020 15:15:41 +0200</pubDate>
            <guid>https://nikola.milojevic.me/blog/unbound-email/</guid>
            <description>&lt;p&gt;At the core of our business at MailerLite are, as our name says, emails — in particular email sending. We send over &lt;strong&gt;30 million&lt;/strong&gt; emails per day, and a lot of our servers are MTAs doing their job.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>At the core of our business at MailerLite are, as our name says, emails — in particular email sending. We send over <strong>30 million</strong> emails per day, and a lot of our servers are MTAs doing their job.</p>
<p>To maximize the throughput of email servers you can do a few things, optimise the resources, optimise MTA, but there is one more thing that you can optimise and that is obvious but overlooked - DNS tweaking. I won’t go into the whole flow of sending and receiving the message, but the DNS part. When you want to send the email, MTA has to do at least two DNS queries - to get the MX record of the server you are trying to send the email to, and to get an A record of that server and there are even more — it could be getting PTR records. You can imagine how time costly is this operation when you are sending 30 million emails per day. To get a clearer picture — it is 21000 emails per minute. If you multiply it with the number of DNS queries that each email sending has to do, you come to crazy numbers.</p>
<p>To mitigate that overhead of running those DNS queries to an external DNS server, we came up with the idea of using a local recursive, validating, and caching DNS server. That is where <a href="https://nlnetlabs.nl/projects/unbound/about/">Unbound</a> comes into the picture.</p>
<p><strong>Recursive</strong> part of Unbound gets the request from the client to traverse the path of DNS to get the answer to the question.</p>
<p>We will assume that the question in this example is “What is the A record of mailerlite.com”:</p>
<ul>
<li>Client sends a query “what is the A record of mailerlite.com” to the server configured in resolv.conf</li>
<li>DNS server (Unbound) checks if it has that record in its cache</li>
<li>If the server wasn’t asked before for that record it won’t be found in the cache so it has to look it up on the root servers</li>
<li>Unbound asks the root servers for the record</li>
<li>Root server replies to TLD servers for .com</li>
<li>Unbound asks another query — this time to the .com TLD servers</li>
<li>This time - TLD server replies with referral to the authoritative name server for mailerlite.com at CloudFlare (we host all our domains at CF)</li>
<li>Once more, Unbound sends another query for A record of mailerlite.com to CF</li>
<li>CF replies with the A record of mailerlite.com which contains the IP address for it</li>
<li>Unbound answers with the IP address of mailerlite.com to the client</li>
</ul>
<p>Sounds like a longer process than it should be, right? Imagine doing this each time we want to send an email. Yikes, it would kill our performance.</p>
<p>That is where <strong>caching</strong> part of Unbound comes into place. If the query has been asked before, Unbound will respond from its cache, if not — it will recursively resolve the query if it hasn’t seen it before. This operation saves a lot of time as cached results are returned in a couple of <strong>micro</strong>seconds while usual DNS queries (recursive) can take hundreds of <strong>mili</strong>seconds or more.</p>
<p>The following is the speed comparison when the query from cache and from publicly available DNS servers:</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">DNS Server</th>
					<th style="text-align: left">avg response</th>
					<th style="text-align: left">min response</th>
					<th style="text-align: left">max response</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">Unbound - local</td>
					<td style="text-align: left">160.12µs</td>
					<td style="text-align: left">106.05µs</td>
					<td style="text-align: left">252.481µs</td>
			</tr>
			<tr>
					<td style="text-align: left">8.8.8.8</td>
					<td style="text-align: left">25.78ms</td>
					<td style="text-align: left">8.05ms</td>
					<td style="text-align: left">62.24ms</td>
			</tr>
			<tr>
					<td style="text-align: left">8.8.4.4</td>
					<td style="text-align: left">54.64ms</td>
					<td style="text-align: left">8.03ms</td>
					<td style="text-align: left">355.17ms</td>
			</tr>
			<tr>
					<td style="text-align: left">208.67.222.222</td>
					<td style="text-align: left">32.63ms</td>
					<td style="text-align: left">10ms</td>
					<td style="text-align: left">37.17ms</td>
			</tr>
			<tr>
					<td style="text-align: left">156.154.71.1</td>
					<td style="text-align: left">27.20ms</td>
					<td style="text-align: left">10ms</td>
					<td style="text-align: left">30.11ms</td>
			</tr>
			<tr>
					<td style="text-align: left">216.146.35.35</td>
					<td style="text-align: left">112.74ms</td>
					<td style="text-align: left">10ms</td>
					<td style="text-align: left">146.77ms</td>
			</tr>
	</tbody>
</table>
<p>Grafana graphs:</p>
<p><img src="/images/blog/unbound-01.png" alt="Grafana 01" title="Cache hit/miss ratio"></p>
<p>This one shows the cache hit/miss ratio. For the last 24 hours, we had 432 million hits and only 9.28 million misses. That means that only 2.14% of the queries went to external DNS servers to check the records and that the other 97.86% was answered from the cache in matter of microseconds.</p>
<p><img src="/images/blog/unbound-02.png" alt="Grafana 02" title="Response time distribution"></p>
<p><img src="/images/blog/unbound-03.png" alt="Grafana 03" title="DNS query types"></p>
<p>Unbound is a fantastical piece of software, and as you have seen it provides quite a boost and also increases our email throughput.</p>]]></content>
        </item>
        
        <item>
            <title>Managing K8S secrets in GCP</title>
            <link>https://nikola.milojevic.me/blog/helm-secrets-skaffold-gcp/</link>
            <pubDate>Sat, 03 Oct 2020 11:10:40 +0200</pubDate>
            <guid>https://nikola.milojevic.me/blog/helm-secrets-skaffold-gcp/</guid>
            <description>&lt;p&gt;Managing secrets in Kubernetes is not a straightforward thing.&lt;/p&gt;
&lt;p&gt;Creating them manually via command line is a bad practice and makes problem managing, updating, and adding passwords. There are quite a few tools that address this problem, the most popular of them is Hashicorp Vault. While I very much like what Hashicorp is doing and what Vault provides, it generates a big overhead in managing it and most of the systems/companies don&amp;rsquo;t need it so I wanted do use something simple, manageable and secure for all of our workloads at &lt;a href=&#34;https://www.remotecompany.com&#34;&gt;The Remote Company&lt;/a&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Managing secrets in Kubernetes is not a straightforward thing.</p>
<p>Creating them manually via command line is a bad practice and makes problem managing, updating, and adding passwords. There are quite a few tools that address this problem, the most popular of them is Hashicorp Vault. While I very much like what Hashicorp is doing and what Vault provides, it generates a big overhead in managing it and most of the systems/companies don&rsquo;t need it so I wanted do use something simple, manageable and secure for all of our workloads at <a href="https://www.remotecompany.com">The Remote Company</a>.</p>
<p>A little of the background - we use Helm charts for each of our apps. They are deployed to GKE with Github Actions, utilizing Skaffold as a build/deploy tool, so we wanted something compatible with it. Queue the <a href="https://github.com/zendesk/helm-secrets">Helm Secrets plugin from Zendesk</a> - wonderful implementation of an even more wonderful <a href="https://github.com/mozilla/sops">Mozilla SOPS</a>. SOPS is an editor from Mozilla able to encrypt the data with AWS KMS, GCP KMS, Azure Key Vault, and PGP. Originally, it was written in Python, but Mozilla rewrote it in Golang making it easier to build and redistribute as Golang compiles everything into a single binary. Helm secrets is a helm plugin that uses SOPS in the background to encrypt/decrypt the data, but also automatically applies the secrets to Kubernetes cluster and attaches it to the deployment on demand (we will cover it a bit later).</p>
<p>The instructions that follow are how you would manage it from macOS.</p>
<p>The tools that we will be using are:</p>
<ul>
<li>Helm</li>
<li>Helm secrets</li>
<li>Skaffold</li>
<li>GCloud SDK</li>
<li>Mozilla SOPS</li>
<li>Kubectl</li>
</ul>
<h2 id="dependency-setup">Dependency setup</h2>
<p>Install dependencies via brew.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew cask install google-cloud-sdk
</span></span><span class="line"><span class="cl">brew install kubectl helm skaffold sops
</span></span><span class="line"><span class="cl">helm plugin install https://github.com/zendesk/helm-secrets
</span></span></code></pre></div><p>Login to our Google Cloud project. Follow the instructions on setting up your project and compute region and zone.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud auth login
</span></span><span class="line"><span class="cl">gcloud auth application-default login
</span></span></code></pre></div><h2 id="creating-kms-key">Creating KMS key</h2>
<p>Create Google KMS key used for encryption and decryption of the content</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud kms keyrings create sops --location global
</span></span><span class="line"><span class="cl">gcloud kms keys create sops-key --location global --keyring sops --purpose encryption
</span></span><span class="line"><span class="cl">gcloud kms keys list --location global --keyring sops
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># You should see similar output to</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">NAME                                                                  PURPOSE          PRIMARY_STATE
</span></span><span class="line"><span class="cl">projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key ENCRYPT_DECRYPT  ENABLED
</span></span></code></pre></div><h2 id="setup-secrets">Setup secrets</h2>
<p>You should have a helmchart in your project. In your helmchart folder, create <code>.sops.yaml</code> file.</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">creation_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">gcp_kms</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key&#34;</span><span class="w">
</span></span></span></code></pre></div><p>In order to easily diff in git, you would need <code>.gitattributes</code> in the same folder with sopsdiffer.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="l">secrets.yaml diff=sopsdiffer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="l">secrets.*.yaml diff=sopsdiffer</span><span class="w">
</span></span></span></code></pre></div><p>In our helmchart, we will need secrets file. When you start, you can add secrets as plain text in that file, later on, we will encrypt them with SOPS. In this example, we will create <code>secrets.dev.yaml</code> in our helmchart folder.</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">secrets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">first-password</span><span class="p">:</span><span class="w"> </span><span class="l">very-secure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">second-password</span><span class="p">:</span><span class="w"> </span><span class="l">much-locked</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">third-password</span><span class="p">:</span><span class="w"> </span><span class="l">so-encrypted</span><span class="w">
</span></span></span></code></pre></div><p>Now we can encrypt the secrets file. The file will be encrypted with KMS key.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm secrets enc secrets.dev.yaml
</span></span></code></pre></div><p>It should look something like this:</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">secrets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">first-password</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:0DKw4JAoAv8kg139TfEatrrhdVEhBKFtFYpuNBVlxDqd4qc6u6SaK1MbsGUqHLuuMhyCuUNl2MepmMwm2QsgVqx7euBIeqKSe6ZUYdiAKuwFrGpLzQJ==,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">second-password</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:Mv1Frwz6CT2Jnq5EmBgZJl6sn7cYzXFceLSlOiu5djguJi6cRJAW90cpIkwpcuvYc3ulyZagoB7vfO2nL17IOa3hZ4GZRhzXZXqo0wDAgXFjucR1L4B==,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">third-password</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:B9FxMPPSOisPrlnsDfwilwtagxVfWbqCwWJw7XsfqElRqitvCpkB3anvj1uW0hpruJ96LwAJDwplqu8iaZmz14OyeKMLREuCpwRx5b4OhEUbRGTtESA==,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sops</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kms</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="nt">gcp_kms</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">resource_id</span><span class="p">:</span><span class="w"> </span><span class="l">projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">created_at</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2020-09-04T11:21:03Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enc</span><span class="p">:</span><span class="w"> </span><span class="l">somevalue=</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">azure_kv</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="nt">hc_vault</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="nt">lastmodified</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2020-09-30T15:12:35Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mac</span><span class="p">:</span><span class="w"> </span><span class="l">ENC[AES256_GCM,data:somevalue==,type:str]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgp</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="nt">unencrypted_suffix</span><span class="p">:</span><span class="w"> </span><span class="l">_unencrypted</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">3.6.1</span><span class="w">
</span></span></span></code></pre></div><p>You can see that our secrets are now encrypted and they can be referenced in helm template.</p>
<h2 id="referencing-secrets-in-helm-values">Referencing secrets in helm values</h2>
<p>First, we need to create a template for helm so it can iterate over your secrets and add them automatically. Here&rsquo;s the example of the template:</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="w"> </span><span class="l">include &#34;app.fullname&#34; . }}</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="l">include &#34;app.labels&#34; . | nindent 4 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Opaque</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="l">range $key, $val := .Values.secrets }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>{{<span class="w"> </span><span class="nt">$key }}</span><span class="p">:</span><span class="w"> </span>{{<span class="w"> </span><span class="l">$val | b64enc | quote }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span></code></pre></div><p>This template goes over secrets file, looks for the secrets key and then adds all the keys referenced with their values to a Secret object in k8s.</p>
<p>Usually, your secrets are in env and you would have a template in helm that can iterate over your env variables and attach them to the deployment. Here is one example of a template for k8s deployment:</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="w"> </span><span class="l">include &#34;app.fullname&#34; . }}</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="l">include &#34;app.labels&#34; . | nindent 4 }}</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="w"> </span><span class="l">.Values.replicaCount }}</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="l">include &#34;app.selectorLabels&#34; . | nindent 6 }}</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="l">include &#34;app.selectorLabels&#34; . | nindent 8 }}</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="l">with .Values.imagePullSecrets }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">imagePullSecrets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">toYaml . | nindent 8 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">securityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">toYaml .Values.podSecurityContext | nindent 8 }}</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="w"> </span><span class="l">.Chart.Name }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">securityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">toYaml .Values.securityContext | nindent 12 }}</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="s2">&#34;{{ .Values.image }}&#34;</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="w"> </span><span class="l">.Values.imagepullPolicy }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>{{- <span class="l">if .Values.volumeMounts }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">range .Values.volumeMounts }}</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="w"> </span><span class="l">.name }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span>{{<span class="w"> </span><span class="l">.mountPath }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>{{- <span class="l">if .subPath }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">subPath</span><span class="p">:</span><span class="w"> </span>{{<span class="w"> </span><span class="l">.subPath }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span>{{<span class="w"> </span><span class="l">.readOnly }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>{{- <span class="l">end }}</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="l">range .Values.env }}</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="s2">&#34;{{ .name }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{ .value }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">range .Values.envExtra }}</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="s2">&#34;{{ .name }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{ .value }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">range .Values.secretEnv }}</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="s2">&#34;{{ .name }}&#34;</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="s2">&#34;{{ .secretKeyRefName }}&#34;</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="s2">&#34;{{ .secretKeyRefKey }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>{{- <span class="l">end }}</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">containerPort</span><span class="p">:</span><span class="w"> </span>{{<span class="w"> </span><span class="l">.Values.service.port }}</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">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">/probe</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="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><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">/probe</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="l">http</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="l">toYaml .Values.resources | nindent 12 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>{{- <span class="l">if .Values.volumeMounts }}</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="l">range .Values.volumeMounts }}</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="w"> </span><span class="l">.name }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secret</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="w"> </span><span class="l">.name }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>{{- <span class="l">with .Values.nodeSelector }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodeSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">toYaml . | nindent 8 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>{{- <span class="l">with .Values.affinity }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">affinity</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">toYaml . | nindent 8 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>{{- <span class="l">with .Values.tolerations }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tolerations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{{- <span class="l">toYaml . | nindent 8 }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>{{- <span class="l">end }}</span><span class="w">
</span></span></span></code></pre></div><p>You can see env and secretEnv values, which we will use to define env and secret env variables in our values file.</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">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">SOME_GREAT_ENV</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;notasecretvariable&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">secretEnv</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">COOL_ENV</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretKeyRefName</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">secretKeyRefKey</span><span class="p">:</span><span class="w"> </span><span class="l">first-password</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">SLIGHTLY_LESS_COOL_ENV</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretKeyRefName</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">secretKeyRefKey</span><span class="p">:</span><span class="w"> </span><span class="l">second-password</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">CHILL_ENV</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretKeyRefName</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">secretKeyRefKey</span><span class="p">:</span><span class="w"> </span><span class="l">third-password</span><span class="w">
</span></span></span></code></pre></div><p>As you can see, we added a simple env which is not a secret, and then used secretEnv to add a couple of variables that we have in our secret. <code>secretKeyRefName</code> in this case will be the name of the helm release, as our secrets template will create an object under that name. Next, we will just use the key that we entered into our <code>secrets.dev.yaml</code> to let Kubernetes know which env should be referenced to which secret. After that, we are ready to go for deployment.</p>
<h2 id="deploying-with-skaffold">Deploying with Skaffold</h2>
<p>I won&rsquo;t get into the whole flow with Github Actions, Helm charts, and Skaffold, but will show you how you can simply tell Skaffold to utilize Helm secrets automatically.</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">skaffold/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">Config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">profiles</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">dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">artifacts</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">our-repo/our-image</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">docker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">dockerfile</span><span class="p">:</span><span class="w"> </span><span class="l">docker/Dockerfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tagPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">gitCommit</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">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">helm</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">releases</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">chartPath</span><span class="p">:</span><span class="w"> </span><span class="l">helmchart</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">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">values</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">our-repo/our-image</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">valuesFiles</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">helmchart/values.dev.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">helmchart/secrets.dev.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">useHelmSecrets</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">wait</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">flags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">global</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="nt">upgrade</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;--debug&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--install&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--timeout&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;6000s&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>A couple of important things here:</p>
<ul>
<li>You need to add <code>useHelmSecrets: true</code> so Skaffold knows that you want to utilize Helm secrets plugin and that it can apply the secrets automatically</li>
<li><code>valuesFiles</code> needs to contain both a path to values and secrets file in order for a build to succeed.</li>
<li>Your Google IAM user must have KMS Encrypter role (if you want to encrypt the data), and KMS Decrypter role (used for CIs as they don&rsquo;t need to encrypt the data, just decrypt).</li>
<li>You would need to login to your GKE cluster before running Skaffold.</li>
</ul>
<p>Afterward you would just call <code>skaffold run -p dev</code> and watch the tools do its work. If you want to edit, or add some value, you would simply run <code>helm secrets edit helmchart/secrets.dev.yaml</code> and it would open the decrypted file in your <code>$EDITOR</code>. You can add data, change passwords, etc, and save the file and it would automatically get encrypted so you can commit it to git and push it to your CI.</p>
<p>This sounds like a lot of steps to manage &ldquo;such a simple thing&rdquo; as secrets, but you only need to do this when you start a project and forget about it. If you have any questions - let me know in the comments.</p>
]]></content>
        </item>
        
        <item>
            <title>Caddy server and landing pages</title>
            <link>https://nikola.milojevic.me/blog/caddy-landing-pages/</link>
            <pubDate>Sat, 19 Sep 2020 20:46:32 +0200</pubDate>
            <guid>https://nikola.milojevic.me/blog/caddy-landing-pages/</guid>
            <description>&lt;p&gt;Caddy is an open-source web server that aims for a more secure web as it automatically configures LetsEncrypt for your domains (it also has the support for ZeroSSL starting with v2.2.0 Caddy). It is fast and written in Go, as well as the underlying library for issuing and renewing certificates - &lt;a href=&#34;https://github.com/caddyserver/certmagic&#34;&gt;certmagic&lt;/a&gt;. Besides its speed and auto-ssl, it is also very light, easy to configure and use in Docker/Kubernetes, and very extensible.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Caddy is an open-source web server that aims for a more secure web as it automatically configures LetsEncrypt for your domains (it also has the support for ZeroSSL starting with v2.2.0 Caddy). It is fast and written in Go, as well as the underlying library for issuing and renewing certificates - <a href="https://github.com/caddyserver/certmagic">certmagic</a>. Besides its speed and auto-ssl, it is also very light, easy to configure and use in Docker/Kubernetes, and very extensible.</p>
<p>One thing that differentiates Caddy from other webservers is its ask endpoint. Basically - it asks the endpoint if it is allowed to issue the certificate for the particular domain and only then issues the certificate for it. This is useful for us at <a href="https://www.mailerlite.com">MailerLite</a> and our <a href="https://www.mailerlite.com/features/landing-pages">landing pages</a> feature which allows users to easily create landing pages for them via drag and drop editor and they can use their custom domains for it. As we are also aiming for a more secure web, we are giving out the free certificates for each custom domain users set.</p>
<p>The company of our size has a lot of custom domains used in our landing pages, so we wanted to use some other storage rather than files for certificates. Fortunately, there is a <a href="https://github.com/gamalan/caddy-tlsredis">plugin</a> that utilizes Redis for that operation. It is really easy to add plugins to your Caddy installation with Caddy builder. You can find our implementation <a href="https://github.com/nklmilojevic/caddy2-custom/blob/master/Dockerfile">here</a>. This Dockerfile also includes Cloudflare DNS plugin so you can use dns-01 verification instead of http-01 (useful when running certificates on your local network).</p>
<p>Back to the implementation, we utilize the following caddy configuration:</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="err">email</span> <span class="err">team@mailerlite.com</span>
</span></span><span class="line"><span class="cl">	<span class="err">on_demand_tls</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">		<span class="err">ask</span> <span class="err">{$CADDY_ASK_HOST</span><span class="p">}</span><span class="err">:</span><span class="p">{</span><span class="err">$CADDY_ASK_PORT</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="err">storage</span> <span class="err">redis</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="err">host</span>          <span class="err">{$CADDY_CLUSTERING_REDIS_HOST</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="err">port</span>          <span class="p">{</span><span class="err">$CADDY_CLUSTERING_REDIS_PORT</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="err">aes_key</span>       <span class="p">{</span><span class="err">$CADDY_CLUSTERING_REDIS_AESKEY</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">	<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></span><span class="line"><span class="cl"><span class="err">:</span><span class="mi">8080</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">respond</span> <span class="err">/healthz</span> <span class="nt">&#34;200 ok&#34;</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="err">:</span><span class="mi">443</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="err">tls</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">		<span class="err">on_demand</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="err">reverse_proxy</span> <span class="p">{</span><span class="err">$CADDY_LANDINGS_HOST</span><span class="p">}</span><span class="err">:</span><span class="p">{</span><span class="err">$CADDY_LANDINGS_PORT</span><span class="p">}</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="err">transport</span> <span class="err">http</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">			<span class="err">keepalive</span> <span class="err">30s</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="err">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span></code></pre></div><p>As you can see, we are utilizing env variables to set our ask endpoint, as well the other config, redis host, and finally, our microservice that servers landing pages. Our stack for landing services contains a couple of microservices - Caddy, Redis, NodeJS landings microservice, and Golang ask endpoint microservice. I won&rsquo;t get into depth on how we deploy the apps to Kubernetes in the post, but in a nutshell, we are using Helm, Helm Secrets, Skaffold, and GitHub Actions.</p>
<p>To get a better overview of how the flow is, you can check it out in the following image:</p>
<p><img src="/images/blog/caddy-flow.png" alt="Caddy flow"></p>
<p>Overall, we are really satisfied with how easy and performant Caddy is, and in version 2.2.0 you can even have <a href="https://caddyserver.com/docs/metrics#monitoring-caddy-with-prometheus-metrics">native Prometheus integration</a> so you could increase your observability in Grafana, for example.</p>
<p>Let me know in the comments if you have any questions about the implementation.</p>
]]></content>
        </item>
        
        <item>
            <title>ePorezi na macOS sa MUP sertifikatom</title>
            <link>https://nikola.milojevic.me/blog/eporezi-macos-mup/</link>
            <pubDate>Wed, 15 Jan 2020 12:06:51 +0100</pubDate>
            <guid>https://nikola.milojevic.me/blog/eporezi-macos-mup/</guid>
            <description>&lt;p&gt;&lt;em&gt;Ažurirano 11.06.2021. sa novim linkovima.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Posle puno pitanja i pojedinačnog objašnjavanja na Slacku Digitalne zajednice, rešio sam da napišem kratko uputstvo kako sam namestio da aplikacija ePorezi radi na macOSu sa sertifikatima MUPa.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Napomena: Sve ovo koristite na sopstvenu odgovornost. Modul je dobijen od PKS CA i nije prepravljan, već je u izvornom obliku u kome sam ga i dobio. Ovo bi sve trebalo da radi i za sertifikate koje je izdala PKS, ali nisam imao mogućnosti da proverim.&lt;/em&gt;&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><em>Ažurirano 11.06.2021. sa novim linkovima.</em></p>
<p>Posle puno pitanja i pojedinačnog objašnjavanja na Slacku Digitalne zajednice, rešio sam da napišem kratko uputstvo kako sam namestio da aplikacija ePorezi radi na macOSu sa sertifikatima MUPa.</p>
<p><em>Napomena: Sve ovo koristite na sopstvenu odgovornost. Modul je dobijen od PKS CA i nije prepravljan, već je u izvornom obliku u kome sam ga i dobio. Ovo bi sve trebalo da radi i za sertifikate koje je izdala PKS, ali nisam imao mogućnosti da proverim.</em></p>
<p>Uslovi da bi vam ovo proradilo:</p>
<ul>
<li>Lična karta izdata <strong>posle</strong> 18.8.2014. godine</li>
<li>macOS</li>
<li>modifikovana ePorezi aplikacija koju možete skinuti <a href="https://github.com/OpenSerbianEID/ePorezi/releases/download/1.2.2-5/ePorezi.zip">ovde</a></li>
<li>Instalirana Java</li>
<li>Library za middleware koji MUP koristi koji možete skinuti <a href="https://github.com/nklmilojevic/eporezi-macos/raw/master/libnstpkcs11.dylib">ovde</a></li>
</ul>
<p>Instalirajte Javu, iskopirajte middleware u <code>/usr/local/lib</code> i prebacite modifikovanu verziju ePorezi za macOS u <code>Applications</code> folder. Na Catalini je potrebno da držite ctrl i pritisnete desni klik i open prilikom prvog otvaranja aplikacije zbog novih sigurnosnih pravila na ovoj iteraciji OSa.</p>
<p>To bi trebalo da bude to. Ubacite ličnu karticu i zakačite smart card čitač i trebalo bi sve da radi.</p>
<p>Moguće greške i problemi:</p>
<ul>
<li>Ako piše da čitač ili lična karta nisu povezani, proverite da li je čitač prepoznat od strane macOSa. Isto tako, proverite kada Vam je izdata lična karta jer datum mora biti posle 18.8.2014.</li>
<li>Probajte da odete u <code>/usr/local/lib</code> i da otvorite <code>libnstpkcs11.dylib</code> sa desnim klikom dok držite ctrl i da pritisnite open.</li>
</ul>
]]></content>
        </item>
        
    </channel>
</rss>
