<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Leonid Koftun</title><link>https://blog.sldk.de/posts/</link><description>Recent content in Posts on Leonid Koftun</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>&lt;a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0&lt;/a></copyright><lastBuildDate>Sun, 10 Dec 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.sldk.de/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>AWS API Gateway Authorizer Patterns</title><link>https://blog.sldk.de/2023/12/aws-api-gateway-authorizer-patterns/</link><pubDate>Sun, 10 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2023/12/aws-api-gateway-authorizer-patterns/</guid><description>Introduction AWS API Gateway Authorizers are a powerful tool to secure API Gateway endpoints.
The configuration of Authorizers can be a bit overwhelming at first. There are many options which have an impact on the security, performance and programming model of your API Gateway.
This article is a collection of three patterns that I have identified from my experience with AWS API Gateway Authorizers, from discussions with peers and colleagues and from existing documentation online.</description><content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>AWS API Gateway Authorizers are a powerful tool to secure API Gateway endpoints.</p>
<p>The configuration of Authorizers can be a bit overwhelming at first. There are many options which have an impact
on the security, performance and programming model of your API Gateway.</p>
<p>This article is a collection of three patterns that I have identified from my experience with AWS API Gateway Authorizers,
from discussions with peers and colleagues and from existing documentation online.</p>
<p>Please note that this is not a comprehensive guide to API Gateway Authorizers. It&rsquo;s a collection of abstract concepts
that can be used to reason about the different patterns:</p>
<ol>
<li><a href="#no-authorizer-pattern"><strong>No-Authorizer Pattern</strong></a></li>
<li><a href="#binary-authorizer-pattern"><strong>Binary Authorizer Pattern</strong></a></li>
<li><a href="#context-authorizer-pattern"><strong>Context Authorizer Pattern</strong></a></li>
</ol>
<p>Note that the naming of the patterns is not official. I made them up to make it easier to talk about them.</p>
<h2 id="no-authorizer-pattern">No-Authorizer Pattern</h2>
<p>The first pattern consists of an API Gateway and integration Lambdas in the background.</p>
<p><img src="/img/generated/no-authorizer.svg" alt="diagram"></p>
<p>There is no authorizer Lambda configured in the API Gateway. All requests to the API Gateway are authorized by default
and forwarded to the backend Lambdas. The Lambdas are responsible for authorization directly.</p>
<p>In the diagram above, there is an exemplary &ldquo;External User Service&rdquo; that is queried by the backend Lambdas to authorize
the request.</p>
<h3 id="when-to-use">When to use</h3>
<ul>
<li>When there is a only handful of Lambdas to integrate with the API Gateway</li>
<li>When it&rsquo;s possible to re-use the same authorization code across Lambdas in a maintainable way (same code repository or
shared library)</li>
<li>When the authorization call does not take a considerable amount of time to execute</li>
<li>When authorization logic should be testable together with the business logic</li>
<li>When each invocation should use the up-to-date authorization information</li>
</ul>
<h2 id="cacheable-vs-non-cacheable-authorizers">Cacheable vs Non-Cacheable Authorizers</h2>
<p>Before we dive into the next patterns, let&rsquo;s talk about caching.</p>
<p>API Gateway Authorizers can be configured to cache the authorization result for a given request for a given amount of
time.
This is useful when the authorization call takes a considerable amount of time to execute and the authorization result
is not expected to change within the cache time.</p>
<p>Caching the authorization result can significantly improve the response times of the authorizer but it comes with a
trade-off.
The cached authorization result is not updated until the cache expires. This means that the authorization result may not
be in sync with the actual authorization state of the user.</p>
<p>This needs to be taken into account when choosing the right authorizer pattern.</p>
<h3 id="example">Example</h3>
<ol>
<li>There&rsquo;s a role based authorization system with 2 roles: <code>admin</code> and <code>user</code></li>
<li>User A has an admin role and requests a resource in our AWS backend</li>
<li>The authorization result (&ldquo;user A has admin role and is allowed to do admin things&rdquo;) is cached for a given amount of
time</li>
<li>In the meantime, user A&rsquo;s role is changed to <code>user</code></li>
<li>User A requests the same resource again where the authorization result is cached =&gt; The cached authorization result
is used and user A is allowed to do admin things even though he shouldn&rsquo;t be allowed to</li>
</ol>
<h2 id="binary-authorizer-pattern">Binary Authorizer Pattern</h2>
<p>This pattern consists of an API Gateway, a single authorizer and integration Lambdas.</p>
<p><img src="/img/generated/binary-authorizer.svg" alt="diagram"></p>
<p>The authorizer is responsible for the authorization based on a binary decision (e.g. &ldquo;does the request contain a valid
token&rdquo;). In the diagram above, an exemplary &ldquo;External User Service&rdquo; is queried by the authorizer to authorize the request.</p>
<p>Authorized requests are forwarded to backend Lambdas. The Lambdas assume that the requester is authorized to perform the
call and can focus on the business logic.</p>
<h3 id="when-to-use-1">When to use</h3>
<ul>
<li>When separation of concerns between authorization and business logic is needed (code, test, deploy)</li>
</ul>
<h3 id="when-to-cache">When to cache</h3>
<ul>
<li>When the authorization call takes a considerable amount of time to execute</li>
<li>When the security requirements allow the use of caching to improve response times of Authorizer and Lambda invocations</li>
</ul>
<h2 id="context-authorizer-pattern">Context Authorizer Pattern</h2>
<p>This pattern consists of an API Gateway, a single authorizer and integration Lambdas.</p>
<p><img src="/img/generated/context-authorizer.svg" alt="diagram"></p>
<p>The authorizer is responsible for the authorization based on a binary decision (e.g. &ldquo;does the request contain a valid
token&rdquo;)
and for providing the authorization information to the backend Lambdas.</p>
<p>The Lambdas assume that the requester is authorized to perform the call and receive additional context information to
make more nuanced authorization decisions.</p>
<p>In the diagram above, an exemplary &ldquo;External User Service&rdquo; is queried by the authorizer to authorize the request and
get the role of the user. The role is then forwarded to the backend Lambdas as context information.</p>
<h3 id="when-to-use-2">When to use</h3>
<ul>
<li>When the authorization conditions are complex and require evaluation on a per-Lambda basis</li>
<li>When it&rsquo;s possible to re-use the same authorization evaluation code across Lambdas in a maintainable way
(same code repository or shared library)</li>
</ul>
<h3 id="when-to-cache-1">When to cache</h3>
<ul>
<li>When the authorization call takes a considerable amount of time to execute</li>
<li>When the security requirements allow the use of caching to improve response times of Authorizer and Lambda invocations</li>
</ul>
<h2 id="choosing-the-right-authorizer-pattern">Choosing the right authorizer pattern</h2>
<p>Here are some questions to ask yourself when choosing the right authorizer pattern.</p>
<p>Please note that these are based on the identity source being a authorization token.
This does not take into account other identity sources like request parameters where caching behaves differently (I hope
to cover this in a future article).</p>
<ol>
<li>
<p>Does my use-case need strict security requirements?</p>
<ul>
<li>Yes:
<ul>
<li>No-Authorizer Pattern</li>
<li>Non-Cacheable Binary Authorizer Pattern</li>
<li>Non-Cacheable Context Authorizer Pattern</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Do my authorizer calls take a considerable amount of time to execute?</p>
<ul>
<li>Yes:
<ul>
<li>Cacheable Binary Authorizer Pattern</li>
<li>Cacheable Context Authorizer Pattern</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Do all my Lambdas require the same authorization information?</p>
<ul>
<li>Yes:
<ul>
<li>Binary Authorizer Pattern</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Do my Lambdas require different authorization information?</p>
<ul>
<li>Yes:
<ul>
<li>Context Authorizer Pattern</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>We have looked at different patterns for API Gateway Authorizers and learned when to use which pattern.</p>
<p>There is no one-size-fits-all solution. It&rsquo;s important to understand the trade-offs of each pattern and choose the right
one for your use-case.</p>
<h2 id="further-reading-and-mentions">Further reading and mentions</h2>
<ul>
<li><a href="https://www.linkedin.com/pulse/aws-lambda-authorizer-patterns-caching-harshit-pandey/">AWS Lambda Authorizer Patterns and Caching</a></li>
<li><a href="https://stackoverflow.com/q/53813947/1510659">API Gateway Authorizer and Logout (Performance/Security Considerations)</a></li>
</ul>
]]></content></item><item><title>E2E email testing with temp-mail</title><link>https://blog.sldk.de/2022/05/e2e-email-testing-with-temp-mail/</link><pubDate>Wed, 25 May 2022 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2022/05/e2e-email-testing-with-temp-mail/</guid><description>The Problem You have a web app that sends out emails using a service like SendGrid or similar when a user signs up. It&amp;rsquo;s actually not important how the mail delivery is implemented. You want to test that an email is delivered at all using an automated test runner like Cypress.
This is what the sign-up implementation could look like.
Note that we don&amp;rsquo;t want to reconfigure the app under test to use a different mail API or SDK (like MailTrap) to reroute our whole mailing traffic via a different service.</description><content type="html"><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>You have a web app that sends out emails using a service like SendGrid or similar when a user signs up. It&rsquo;s actually not important how the mail delivery is implemented. You want to test
that an email <em>is</em> delivered at all using an <em>automated</em> test runner like Cypress.</p>
<p>This is what the sign-up implementation could look like.</p>
<p><img src="/img/generated/e2e-email-sequence.svg" alt="diagram"></p>
<p>Note that we don&rsquo;t want to reconfigure the app under test to use a <strong>different</strong> mail API or SDK (like <a href="">MailTrap</a>) to reroute our whole mailing traffic via a different service. We don&rsquo;t do that because that essentially prevents us from truly testing the app end-to-end.</p>
<h2 id="the-solution">The solution</h2>
<p>Let&rsquo;s first have a look at our high level test spec.</p>
<ol>
<li><strong>Given</strong> a new email address and password</li>
<li><strong>When</strong> I sign up to my app</li>
<li><strong>Then</strong> I want to receive an email</li>
</ol>
<p>In Step 2 we will need to provide a valid email
for our app. In step 3 we will need to check that an email arrived so we need some sort of mailbox. Let&rsquo;s define some requirements for our mailbox.</p>
<ol>
<li>The mailbox should give us <strong>unique</strong> addresses to use per test.</li>
<li>The mailbox should require <strong>minimal manual configuration</strong>.</li>
<li>The mailbox should be accessible via an <strong>API</strong>.</li>
<li>The mailbox should be <strong>cheap af</strong>.</li>
</ol>
<p><strong><a href="https://1secmail.com">1secmail.com</a> to the rescue!</strong></p>
<p>The web version looks quite hideous but it has a really simple <a href="https://www.1secmail.com/api/">API</a>.</p>
<p>It basically functions as a catch-all email mailbox. So you don&rsquo;t need to <em>create</em> temp addresses.
You can <em>use</em> any temp address in your tests (e.g. during sign-up).
You can then use the temp address as <em>key</em> to fetch email for the temp address.</p>
<p>So our automated test looks like this under the hood.</p>
<ul>
<li>Generate a temp email address like <code>test-user-${unique-suffix}@1secmail.com</code> (no API call necessary)</li>
<li>Use the generated temp email address in the test to sign up to the app under test</li>
<li>Check for mails using the 1secmail API</li>
</ul>
<p>Here&rsquo;s an example axios implementation of the API call:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-typescript" data-lang="typescript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">suffix</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">faker</span>.<span style="color:#a6e22e">random</span>.<span style="color:#a6e22e">numeric</span>(<span style="color:#ae81ff">8</span>)

<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Message</span> <span style="color:#f92672">=</span> {
  <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>
  <span style="color:#66d9ef">from</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span>
  <span style="color:#a6e22e">subject</span>: <span style="color:#66d9ef">string</span>
  <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>
}

<span style="color:#66d9ef">async</span> <span style="color:#a6e22e">getMessages</span>(
  <span style="color:#a6e22e">login</span>: <span style="color:#66d9ef">string</span>,
  <span style="color:#a6e22e">domain</span>: <span style="color:#66d9ef">string</span>
)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">Message</span><span style="color:#960050;background-color:#1e0010">[]</span>&gt; {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">axios</span>.<span style="color:#66d9ef">get</span>&lt;<span style="color:#f92672">Message</span><span style="color:#960050;background-color:#1e0010">[]</span>&gt;(
    <span style="color:#e6db74">&#39;https://www.1secmail.com/api/v1/&#39;</span>,
    {
      <span style="color:#a6e22e">params</span><span style="color:#f92672">:</span> {
        <span style="color:#a6e22e">login</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`test-user-</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">suffix</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
        <span style="color:#a6e22e">domain</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1secmail.com&#39;</span>,
      },
    }
  )
  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">data</span>
}
</code></pre></div><p>Let&rsquo;s double check if this approach fulfils our defined requirements.</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> We get <strong>unique</strong> temporary addresses per test.</li>
<li><input checked="" disabled="" type="checkbox"> The mailbox is catch-all so <strong>no configuration</strong> is necessary.</li>
<li><input checked="" disabled="" type="checkbox"> There&rsquo;s a <strong>simple API</strong>.</li>
<li><input checked="" disabled="" type="checkbox"> It&rsquo;s <strong>free</strong>.</li>
</ul>
<p>Note: the public nature of this (anyone can read any temp mailbox) makes this unsuitable for tests where some sort of sensitive data is exchanged.</p>
<h3 id="why-not-aliases">Why not aliases?</h3>
<p>You could create an email address at a public email provider like Protonmail.</p>
<p>You could use unique aliases for each test like <code>test-user+timestamp@protonmail.com</code>.</p>
<p>I know they do have some sort of public API for you to use in your automated tests.</p>
<p>You can get probably get away with this for quite some time. My previous setup involved
a prepared email address with aliases at Protonmail. Unfortunately they terminated my account
due to a violation of their TOS - apparently you&rsquo;re not allowed to use automated software
to spam their system. Who would have thought.</p>
<h3 id="why-not-mailtrap">Why not MailTrap?</h3>
<p>MailTrap looks promising because it&rsquo;s supposed to be an email testing service but it&rsquo;s not
suited for real E2E tests on production if you&rsquo;re not willing to pay premium cash.
You don&rsquo;t get any public email address up until the Business plan which costs 50$ / month!</p>
<p>In other words, the lower tier plans only allow you to include MailTrap&rsquo;s SDK into your app
or use SMTP to send mail directly to MailTrap. This is not E2E testing because you&rsquo;re modifying
the app&rsquo;s configuration.</p>
<p>Basically it&rsquo;s useless and I was disappointed enough to write them a mail when I found out about this&hellip;</p>
]]></content></item><item><title>Handling secrets in Flux v2 repositories with SOPS</title><link>https://blog.sldk.de/2021/03/handling-secrets-in-flux-v2-repositories-with-sops/</link><pubDate>Wed, 03 Mar 2021 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2021/03/handling-secrets-in-flux-v2-repositories-with-sops/</guid><description>This is part 2 of my series on &amp;ldquo;GitOps with Flux v2&amp;rdquo;.
If you&amp;rsquo;re not familiar with what Flux is and how it helps you build GitOps workflows on Kubernetes, feel free to read part 1 here: &amp;ldquo;Introduction to GitOps on Kubernetes with Flux v2&amp;rdquo;.
In today&amp;rsquo;s guide we will look at Mozilla SOPS and learn how to incorporate it with Flux v2 to store encrypted secrets in our GitOps repositories and have Flux decrypt them automatically during deployments.</description><content type="html"><![CDATA[<p>This is part 2 of my series on &ldquo;GitOps with Flux v2&rdquo;.</p>
<p>If you&rsquo;re not familiar with what Flux is and how it helps you build GitOps workflows on Kubernetes, feel free to read
part 1 here: <a href="https://blog.sldk.de/2021/02/introduction-to-gitops-on-kubernetes-with-flux-v2/">&ldquo;Introduction to GitOps on Kubernetes with Flux v2&rdquo;</a>.</p>
<p>In today&rsquo;s guide we will look at <a href="https://github.com/mozilla/sops">Mozilla SOPS</a> and learn how to incorporate it with
<a href="https://toolkit.fluxcd.io/">Flux v2</a> to store
encrypted secrets in our GitOps repositories and have Flux decrypt them automatically during deployments.</p>
<p>To follow along, you will need access to a Flux-enabled Kubernetes cluster.</p>
<h2 id="introduction-to-sops">Introduction to SOPS</h2>
<p>If SOPS is old news to you, you can skip ahead to <a href="#enabling-sops-in-flux-v2">&ldquo;Enabling SOPS in Flux v2&rdquo;</a> ⏩</p>
<p>Suppose you want to store a database password in a Kubernetes yaml and check that in to source control.</p>
<p>You could base64-encode the password and upload it to a <strong>private</strong> git repository that only your team has access to&hellip;
If your repository is compromised, the attacker could easily read the password
without any further measures in place to protect it. That&rsquo;s where SOPS comes in.</p>
<p>SOPS stands for &ldquo;<strong>S</strong>ecrets <strong>OP</strong>eration<strong>S</strong>&rdquo;.
SOPS allows us to <em>encrypt</em> and <em>decrypt</em> certain parts of our Kubernetes yamls with <a href="https://en.wikipedia.org/wiki/Pretty_Good_Privacy">PGP</a>. This means
that we can even make our yaml declarations containing secret data public and not worry about unauthorized parties
peeking into the contents of the secret values because they would require the respective PGP private
keys to decrypt the data.</p>
<p>ℹ️ Note that SOPS can also handle different file formats (JSON, ENV, INI, etc). In the Kubernetes context, we mainly use it with yaml files as that&rsquo;s what we work with most of the time.</p>
<h3 id="sops-basics">SOPS basics</h3>
<p>In this section we&rsquo;ll learn how to use SOPS on your local machine.</p>
<p>First you&rsquo;ll have to create a PGP key with OpenGPG. The following command will guide you through the creation process.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">gpg --full-generate-key</code></pre>
<p>⚠️ Make sure <strong>not</strong> to specify a passphrase in the prompt if you&rsquo;re going to use this key with Flux.
You can use the default selections for most other parameters and just supply your ID information (name + email).</p>
<p>Verify that the PGP key pair was created and note the secret ID:</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">gpg --list-secret-keys ${your_email_address}
out:sec   rsa4096 2021-02-04 [SC]
out:CFF53C2B937EAFD676F75C48F70573E9355BF63B
out:uid           [ultimate] Leonid Koftun &lt;leonid.koftun@gmail.com&gt;
out:ssb   rsa4096 2021-02-04 [E]</code></pre>
<p>Let&rsquo;s create a simple Kubernetes secret yaml to demonstrate how to use SOPS with the new GPG key.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">cat &lt;&lt;EOF &gt; secret.yaml
out:apiVersion: v1
out:kind: Secret
out:metadata:
out:    name: my-database-secret
out:    namespace: awesome-namespace
out:stringData:
out:    database-password: Password123
out:EOF</code></pre>
<p>Now we&rsquo;ll create a SOPS config file in our working directory.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">cat &lt;&lt;EOF &gt; .sops.yaml
out:---
out:creation_rules:
out:- encrypted_regex: &#39;^(data|stringData)$&#39;
out:  pgp: &gt;-
out:    CFF53C2B937EAFD676F75C48F70573E9355BF63B
out:EOF</code></pre>
<p>This configuration tells SOPS to only encrypt values under the &lsquo;data&rsquo; and &lsquo;stringData&rsquo; keys inside of yaml files and
to use our previously generated PGP key for the actual encryption.</p>
<p>We can now run sops:</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash"># You can encrypt the file in-place
sops --encrypt --in-place secret.yaml
# Or write to a new file
sops --encrypt secret.yaml &gt; encrypted-secret.yaml</code></pre>
<p>The result of the encrypted yaml will look like this:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Secret</span>
<span style="color:#f92672">metadata</span>:
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-database-secret</span>
    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">awesome-namespace</span>
<span style="color:#f92672">stringData</span>:
    <span style="color:#f92672">database-password</span>: <span style="color:#ae81ff">ENC[AES256_GCM,data:k8GGkwr4AE/CdlM=,iv:tecWFmg0INNY1vRfpdGLsDc+APd6UmKk6AS//U0OjI4=,tag:EEm7DHO7muNgpLOwUZh1Lw==,type:str]</span>
<span style="color:#f92672">sops</span>:
    <span style="color:#f92672">kms</span>: []
    <span style="color:#f92672">gcp_kms</span>: []
    <span style="color:#f92672">azure_kv</span>: []
    <span style="color:#f92672">hc_vault</span>: []
    <span style="color:#f92672">lastmodified</span>: <span style="color:#e6db74">&#39;2021-02-22T21:45:13Z&#39;</span>
    <span style="color:#f92672">mac</span>: <span style="color:#ae81ff">ENC[AES256_GCM,data:jhzW3o+XcFZgkvGzMb05GpM3hu1dmhRE74woFIeYQOtOy1jCXCp9WgyHar3XDp+1TkOOaN0myfRMe6uz/WmyyKXMPZNf5i4MlB053UUeL2RFMjaGjFlEgq7kG+aoke7+JVN3vTLCiP9fMb4aV3wfPy3hMp5d10wSmPhcfG6/Fww=,iv:07ToHHvJL5tzI5RZLEkfFj+tqJ1y/3XOlADB9TAIuS0=,tag:xlZWGsK5Isrr6GEpBU7YyA==,type:str]</span>
    <span style="color:#f92672">pgp</span>:
    -   <span style="color:#f92672">created_at</span>: <span style="color:#e6db74">&#39;2021-02-22T21:45:13Z&#39;</span>
        <span style="color:#f92672">enc</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74">            -----BEGIN PGP MESSAGE-----
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">            hQIMAzkIo7JC/ReAAQ/+KV60yKpfRK/qiVaRLbHwu6iTNy59O23vS4+Qt5tv8B9n
</span><span style="color:#e6db74">            xW4IxTt4IiXeqZMDt2byJUh9KtncnNKectM5gdxDsFdh4QeChFgVYZsgl0CWG3bY
</span><span style="color:#e6db74">            JBq6Tc/X4udagIuKYIqE+kXiiSwH3+YlKO5VbvcKJPbKg+daeWQvQvXgfghzRZpL
</span><span style="color:#e6db74">            6auVpdw2E8K59gtADi5T+0JvCy8WXYRWu9JOP9TSQjMx1LRLXmiVITbDQFRokZmb
</span><span style="color:#e6db74">            7sGbweCVw6ixEDVDbNZx6rh/sH6MGV+yVVd/KMAUWWkKfvezZgbU4M4i7Bfp4sKT
</span><span style="color:#e6db74">            fp9I21UL1SERHV+6h+jsU379OYb6QdUQ3s+UU7PqejWzuojG4SaHxwPUCT05Doo7
</span><span style="color:#e6db74">            Bf7syjGta6BJcDgMW8CTfqO/58wpBCI5m1xqri9KYF1/E3T7qP4P1vYO4XLXKJK+
</span><span style="color:#e6db74">            Q6ee2pgb3W+y8qI0ev2QYh+lFjqH0mk7Z2L5E3JK/Jwsl+mG3jQBPo4fw/N4Iq1j
</span><span style="color:#e6db74">            WIel4uL9PgPPAgt13Z6UaaYjmYkfiaJIK0rVaY9ArQwU+ShkqHC9nFKB4wxRrGRA
</span><span style="color:#e6db74">            jJU/6pDLCGOfiHOrGC873qGpOnhnpEZlwxQEQClzFo+uug0bL9fMmD+UBgxaoT1C
</span><span style="color:#e6db74">            rrXs0tVkgVlyeYDgIpjuwsZEflfxjy/vuH49VeZETn8iQ+4dpqAXFcQyoAAFNWzS
</span><span style="color:#e6db74">            XgF3Cl2zDs/SIoRMwvZw+GlaWAX9yBLGjxjJGyA5oXkx03i1sL9+5B154O/iPm5q
</span><span style="color:#e6db74">            QwRTqwmZ22XmtMycV4cJP4tMC/kPKvCHyv3JUO28FkXfhovh+VHCpghzAFySKxc=
</span><span style="color:#e6db74">            =Pf3r
</span><span style="color:#e6db74">            -----END PGP MESSAGE-----</span>            
        <span style="color:#f92672">fp</span>: <span style="color:#ae81ff">CFF53C2B937EAFD676F75C48F70573E9355BF63B</span>
    <span style="color:#f92672">encrypted_regex</span>: <span style="color:#ae81ff">^(data|stringData)$</span>
    <span style="color:#f92672">version</span>: <span style="color:#ae81ff">3.6.1</span>
</code></pre></div><p>Note that the <code>stringData.database-password</code> is no longer readable and the added <code>sops</code> block. The latter contains metadata
for SOPS to allow decrypting the secret back to plain-text with the correct private key.</p>
<p>The encrypted yaml file could now be checked into git and distributed. If we try to <code>kubectl apply</code> the encrypted yaml
directly to our cluster, it will fail because the secret is not a valid Kubernetes secret in this form.</p>
<p>We have to decrypt it before deploying it like so:</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">sops --decrypt encrypted-secret.yaml | kubectl apply -f -</code></pre>
<p>The added security of encryption adds at least two extra steps to our DevOps workflow:</p>
<ol>
<li>We need to make sure we only add encrypted secrets to source control.</li>
<li>We need to decrypt secrets in our CI/CD scripts before we can deploy to a cluster.</li>
</ol>
<p>The latter can be automated nicely with Flux v2. Let&rsquo;s see how it&rsquo;s done.</p>
<h2 id="enabling-sops-in-flux-v2">Enabling SOPS in Flux v2</h2>
<p>Now that we know how SOPS works offline, we will be able to apply the same principe to
our GitOps repositories with Flux.</p>
<p>Flux supports SOPS out of the box, we just need to supply it with correct PGP private keys and
its controllers will decrypt SOPS-protected yamls during reconciliation.</p>
<h3 id="create-a-kubernetes-secret-with-your-private-pgp-key">Create a Kubernetes secret with your private PGP key</h3>
<p>We want to export our PGP private key and store that in a Kubernetes secret that Flux can use on the cluster.</p>
<p>We can do that with the following commands:</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash"># Find the ID of the private key.
gpg --list-secret-keys ${your_email_address}
out:sec   rsa4096 2021-02-04 [SC]
out:CFF53C2B937EAFD676F75C48F70573E9355BF63B
out:uid           [ultimate] Leonid Koftun &lt;leonid.koftun@gmail.com&gt;
out:ssb   rsa4096 2021-02-04 [E]

# Export the PGP secret to a new k8s secret
# in the flux-system namespace.
gpg --export-secret-keys \
out:  --armor CFF53C2B937EAFD676F75C48F70573E9355BF63B |
out:kubectl create secret generic sops-gpg \
out:  --namespace=flux-system \
out:  --from-file=sops.asc=/dev/stdin</code></pre>
<p>🐔 🥚 Note that this manual step will likely need to be a part of your Flux bootstrapping routine. This
secret can not be installed by Flux itself because of the implied chicken-egg problem.</p>
<h3 id="setup-cluster-side-decryption-in-flux">Setup cluster-side decryption in Flux</h3>
<p>The following examples are based on my recent <a href="https://blog.sldk.de/2021/02/introduction-to-gitops-on-kubernetes-with-flux-v2/">post about GitOps with Flux v2</a> where
we have bootstrapped the <code>flux-system</code> namespace and deployed a new namespace from our GitOps repository.</p>
<p>Our GitOps repo looks something like this. You can browse it on <a href="https://github.com/sladkoff/home-cluster/tree/110d76e47c597903a0b40357b606fe680cee5769">Github</a>.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">.
├── cluster
│   ├── awesome-namespace
│   │   └── namespace.yaml
│   └── flux-system
│       ├── gotk-components.yaml
│       ├── gotk-sync.yaml
│       └── kustomization.yaml
└── README.md
</code></pre></div><p>To enable SOPS for our GitOps repository we need to edit the <code>gotk-sync.yaml</code> resource:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">---
<span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">source.toolkit.fluxcd.io/v1beta1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GitRepository</span>
<span style="color:#f92672">metadata</span>:
  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">flux-system</span>
<span style="color:#f92672">spec</span>:
  <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">1m0s</span>
  <span style="color:#f92672">ref</span>:
    <span style="color:#f92672">branch</span>: <span style="color:#ae81ff">master</span>
  <span style="color:#f92672">secretRef</span>:
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">url</span>: <span style="color:#ae81ff">ssh://git@github.com/sladkoff/home-cluster</span>
---
<span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.toolkit.fluxcd.io/v1beta1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
<span style="color:#f92672">metadata</span>:
  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">flux-system</span>
<span style="color:#f92672">spec</span>:
  <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">10m0s</span>
  <span style="color:#f92672">path</span>: <span style="color:#ae81ff">./cluster</span>
  <span style="color:#f92672">prune</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#f92672">sourceRef</span>:
    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GitRepository</span>
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">validation</span>: <span style="color:#ae81ff">client</span>
  <span style="color:#75715e"># Enable decryption</span>
  <span style="color:#f92672">decryption</span>:
    <span style="color:#75715e"># Use the sops provider</span>
    <span style="color:#f92672">provider</span>: <span style="color:#ae81ff">sops</span>
    <span style="color:#f92672">secretRef</span>:
      <span style="color:#75715e"># Reference the new &#39;sops-gpg&#39; secret</span>
      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">sops-gpg</span>
</code></pre></div><p>Nice. All that is left to do is test this by checking-in an encrypted secret like the one we created earlier
and push our changes to the remote git repository.</p>
<p>➡️ Example commit on <a href="https://github.com/sladkoff/home-cluster/commit/15ed3210297449bfc77740bd3d75c1d8912ea2b8">Github</a>.</p>
<p>Once Flux reconciliation is done, you should see the new <code>my-database-secret</code> inside the <code>awesome-namespace</code>.</p>
<h2 id="summary">Summary</h2>
<p>We can use SOPS to encrypt and decrypt Kubernetes secrets. We learnt how to do this manually on our local machine
and automatically on a k8s cluster with Flux v2.</p>
<p>If any of this helped you in any way, feel free to <a href="https://www.buymeacoffee.com/sldk">buy me a coffee</a> ☕</p>
<p>You can also give me feedback in the comments, on <a href="https://twitter.com/sladkovik">Twitter</a> or <a href="https://www.instagram.com/sladkoff2/">Instagram</a>.</p>
<p>Thanks 🙏</p>
]]></content></item><item><title>Setting up a Grafana Kiosk on a Raspberry Pi with Ansible</title><link>https://blog.sldk.de/2021/02/setting-up-a-grafana-kiosk-on-a-raspberry-pi-with-ansible/</link><pubDate>Fri, 26 Feb 2021 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2021/02/setting-up-a-grafana-kiosk-on-a-raspberry-pi-with-ansible/</guid><description>I was bored, I had a spare TV screen and a Raspberry Pi 3. I also love watching colorful charts on Grafana 📈
As I&amp;rsquo;m currently learning about Ansible, I decided to write a playbook that sets up a Headless Raspberry Pi in Grafana Kiosk mode.
This quick guide shows you how to create a Kiosk of your own.
You&amp;rsquo;ll need:
A Raspberry Pi with an empty SD card Ansible on your local machine A display or TV to connect to your Raspberry A Grafana instance that is reachable by the Raspberry Installing the OS I used the GUI rpi-imager to bootstrap my SD card with Raspberry Pi OS Lite (32bit) for my Raspberry.</description><content type="html"><![CDATA[<p>I was bored, I had a spare TV screen and a Raspberry Pi 3.
I also love watching colorful charts on <a href="https://grafana.com/">Grafana</a> 📈</p>
<p>As I&rsquo;m currently learning about <a href="https://www.ansible.com/">Ansible</a>,
I decided to write a playbook that sets up a Headless Raspberry Pi in Grafana Kiosk mode.</p>
<p>This quick guide shows you how to create a Kiosk of your own.</p>
<p>You&rsquo;ll need:</p>
<ul>
<li>A Raspberry Pi with an empty SD card</li>
<li>Ansible on your local machine</li>
<li>A display or TV to connect to your Raspberry</li>
<li>A Grafana instance that is reachable by the Raspberry</li>
</ul>
<h2 id="installing-the-os">Installing the OS</h2>
<p>I used the GUI <a href="https://github.com/raspberrypi/rpi-imager">rpi-imager</a> to bootstrap my SD card with
<strong>Raspberry Pi OS Lite (32bit)</strong> for my Raspberry. I think it&rsquo;s quick and easy and offers you some distro options
to choose from.</p>
<p>You can get the tool from the AUR if you&rsquo;re using Arch:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">yay -S rpi-imager
</code></pre></div><p>Here&rsquo;s a screenshot of the GUI (select an image, select SD card, write!):</p>
<p><img src="/img/posts/grafana-kiosk-ansible/rpi-imager-screenshot.png" alt="rpi-imager"></p>
<p>To make the raspberry auto-connect to your WiFi on boot, create a new file <code>wpa_supplicant.conf</code> inside the <code>boot</code>
directory of your flashed SD card with the following contents.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=&lt;Insert 2 letter ISO 3166-1 country code here&gt;

network={
 ssid=&#34;&lt;Name of your wireless LAN&gt;&#34;
 psk=&#34;&lt;Password for your wireless LAN&gt;&#34;
}
</code></pre></div><p>To enable remote access over ssh after boot, create an empty file called <code>ssh</code> inside the <code>boot</code> directory as well.</p>
<h2 id="installing-grafana-kiosk">Installing grafana-kiosk</h2>
<p><a href="https://github.com/grafana/grafana-kiosk">grafana-kiosk</a> is a simple wrapper script that starts a fullscreen Chrome session
and opens a configured Grafana URL with optional authentication.
This Grafana URL usually points to a <a href="https://grafana.com/docs/grafana/latest/dashboards/playlist/">Grafana Playlist</a>
which switches between different Grafana dashboards.</p>
<p>I wrote a simple <a href="https://github.com/sladkoff/ansible-role-grafana-kiosk">Ansible role</a> to set up Headless Raspberry Pi Grafana Kiosks ™️.</p>
<h3 id="ansible-playbook">Ansible Playbook</h3>
<p>Here&rsquo;s my personal <a href="https://github.com/sladkoff/ansible-playbook-grafana-kiosk">Ansible playbook</a>.</p>
<p>It assumes that you have a fresh Raspberry with no graphical environment. Like what we&rsquo;ve set up during &ldquo;<a href="#installing-the-os">Installing the OS</a>&rdquo;.</p>
<p>The playbook will install and configure the following things to autostart on boot:</p>
<ul>
<li>openbox</li>
<li>chromium</li>
<li>grafana-kiosk</li>
</ul>
<p>To get started on your own RPI, you can fork <a href="https://github.com/sladkoff/ansible-playbook-grafana-kiosk">my repo</a> and
apply at least the following changes:</p>
<h4 id="rolesssh-keystasksmainyml">roles/ssh-keys/tasks/main.yml</h4>
<p>This role contains tasks to disable password login via ssh and upload ssh keys to the RPI. You can either get rid of
this if you don&rsquo;t need it or provide your own ssh keys:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># roles/ssh-keys/tasks/main.yml</span>
---
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy SSH Key</span>
  <span style="color:#f92672">ansible.posix.authorized_key</span>:
    <span style="color:#f92672">user</span>: <span style="color:#ae81ff">pi</span>
    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
    <span style="color:#f92672">key</span>: <span style="color:#ae81ff">&lt;URL-OR-FILE-TO-YOUR-SSH-KEYS&gt;</span>
    <span style="color:#f92672">validate_certs</span>: <span style="color:#66d9ef">False</span>
  <span style="color:#f92672">notify</span>:
    - <span style="color:#ae81ff">Restart sshd</span>

- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Disable Password Authentication</span>
  <span style="color:#f92672">lineinfile</span>:
    <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/etc/ssh/sshd_config</span>
    <span style="color:#f92672">regexp</span>: <span style="color:#e6db74">&#39;^PasswordAuthentication&#39;</span>
    <span style="color:#f92672">line</span>: <span style="color:#e6db74">&#34;PasswordAuthentication no&#34;</span>
    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
    <span style="color:#f92672">backup</span>: <span style="color:#66d9ef">yes</span>
  <span style="color:#f92672">notify</span>:
    - <span style="color:#ae81ff">Restart sshd</span>
</code></pre></div><h4 id="hostsyaml">hosts.yaml</h4>
<p>Provide the IP of your RPI in the <code>hosts.yaml</code>:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># hosts.yaml</span>
<span style="color:#f92672">grafana_kiosk</span>:
  <span style="color:#f92672">hosts</span>:
    <span style="color:#f92672">grafana_kiosk_rpi_tv</span>:
      <span style="color:#f92672">ansible_host</span>: <span style="color:#e6db74">&#34;&lt;YOUR-RASPBERRY-IP-HERE&gt;&#34;</span>

<span style="color:#f92672">all</span>:
  <span style="color:#f92672">children</span>:
    <span style="color:#f92672">grafana_kiosk</span>:
</code></pre></div><h4 id="filesmy-grafana-kiosk-configyaml">files/my-grafana-kiosk-config.yaml</h4>
<p>Provide a configuration for the grafana-kiosk utility script, this is where you specify the path to the Grafana instance
and Playlist ID that you want to display on your kiosk:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># my-grafana-kiosk-config.yaml</span>
<span style="color:#f92672">general</span>:
  <span style="color:#f92672">kiosk-mode</span>: <span style="color:#ae81ff">full</span>
  <span style="color:#f92672">autofit</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#f92672">lxde</span>: <span style="color:#66d9ef">false</span>

<span style="color:#f92672">target</span>:
  <span style="color:#f92672">login-method</span>: <span style="color:#ae81ff">local</span>
  <span style="color:#f92672">username</span>: <span style="color:#ae81ff">pi-grafana-kiosk</span>
  <span style="color:#f92672">password</span>: <span style="color:#ae81ff">pi-grafana-kiosk-password</span>
  <span style="color:#f92672">playlist</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#f92672">URL</span>: <span style="color:#ae81ff">https://&lt;YOUR-GRAFANA-HOST-HERE&gt;/playlists/play/&lt;YOUR-GRAFANA-PLAYLIST-ID&gt;?kiosk&amp;autofitpanels</span>
  <span style="color:#f92672">ignore-certificate-errors</span>: <span style="color:#66d9ef">false</span>

</code></pre></div><p>⏯ Run the playbook in the repo directory</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ansible-galaxy install -r requirements.yaml
ansible-playbook install-grafana-kiosk -i hosts.yaml --ask-pass
</code></pre></div><p>You&rsquo;ll need to connecto the RPI to a screen and reboot it. If all went well, you should be greeted with your Grafana Playlist
after booting.</p>
<p>If there are still issues with my Ansible voodoo, feel free to create a PR on Github 😸</p>
]]></content></item><item><title>Introduction to GitOps on Kubernetes with Flux v2</title><link>https://blog.sldk.de/2021/02/introduction-to-gitops-on-kubernetes-with-flux-v2/</link><pubDate>Fri, 19 Feb 2021 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2021/02/introduction-to-gitops-on-kubernetes-with-flux-v2/</guid><description>Today we&amp;rsquo;re having a look at how to set up a GitOps pipeline for your Kubernetes cluster with Flux v2.
We will first go through some core concepts of Flux and then create our first GitOps workflow.
You will need access to a Kubernetes cluster, a shell interface and a Github account to follow this guide. Note that you can use any git provider (Gitlab, Bitbucket, custom) but you&amp;rsquo;ll have to modify the provided example commands.</description><content type="html"><![CDATA[<p>Today we&rsquo;re having a look at how to set up a GitOps pipeline for your Kubernetes cluster with <a href="https://toolkit.fluxcd.io/">Flux v2</a>.</p>
<p>We will first go through some core concepts of Flux and then create our first GitOps workflow.</p>
<p>You will need access to a Kubernetes cluster, a shell interface and a Github account to follow this guide. Note that you
can use any git provider (Gitlab, Bitbucket, custom) but you&rsquo;ll have to modify the provided example commands.</p>
<h2 id="what-is-gitops">What is GitOps?</h2>
<blockquote>
<p>GitOps is a way of managing your infrastructure and applications so that whole system is described declaratively and version controlled (most likely in a Git repository), and having an automated process that ensures that the deployed environment matches the state specified in a repository.</p>
<p>&ndash; <a href="https://toolkit.fluxcd.io/core-concepts/#gitops">https://toolkit.fluxcd.io/core-concepts/#gitops</a></p>
</blockquote>
<p>In Kubernetes practice this means that <code>git</code> is used over <code>kubectl</code> (<code>helm</code>, etc) to perform operations tasks against the cluster.</p>
<p>Pushing to <code>master</code> triggers a deployment to the cluster. We can work with branches and Merge Requests to diff and
review changes to the desired cluster state. We can audit past cluster states with <code>git log</code>. We can rollback a change
using <code>git revert</code>.</p>
<h2 id="what-is-flux-v2">What is Flux v2?</h2>
<p>Flux v2 is a Toolkit for building GitOps workflows on Kubernetes.</p>
<p>In simplified terms, Flux v2 is deployed to a Kubernetes cluster and configured to watch git repositories containing
Kubernetes manifests.</p>
<p><img src="/img/posts/fluxcd/gitops-toolkit.png" alt="GitOps Toolkit"></p>
<p>As Flux watches all our repos and pulls the changes into the cluster, we can focus on writing our Kubernetes manifests.
We don&rsquo;t have to worry about client-side tooling and pushing the changes from every git repo into the cluster.
This is what GitOps is about.</p>
<p>We will come back to the individual Flux components once we&rsquo;ve installed them in the upcoming steps.</p>
<h2 id="installing-flux-v2">Installing Flux v2</h2>
<p>We&rsquo;ll follow the official docs and use the <code>flux</code> CLI tool.</p>
<p>You can install it with:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">curl -s https://toolkit.fluxcd.io/install.sh | sudo bash

<span style="color:#75715e"># enable completions in ~/.bash_profile</span>
. &lt;<span style="color:#f92672">(</span>flux completion bash<span style="color:#f92672">)</span>
</code></pre></div><p>The <code>flux bootstrap</code> command will ensure that:</p>
<ol>
<li>A new GitOps repository for our manifests is created on Github</li>
<li>A <code>flux-system</code> namespace with all Flux components is configured on our cluster</li>
<li>The Flux controllers are set up to sync with our new git repository</li>
</ol>
<p>ℹ️ Note that there&rsquo;s also <a href="https://toolkit.fluxcd.io/guides/installation/#bootstrap-with-terraform">Terraform Provider</a> as an alternative installation method.</p>
<p>The following snippet shows an annotated bootstrap command.
Feel free to play around with <code>flux bootstrap --help</code> to get to know all available options.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">export GITHUB_TOKEN<span style="color:#f92672">=</span>&lt;your-gitlab-token-here&gt; <span style="color:#75715e"># 1</span>

flux bootstrap <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span>  github <span style="color:#ae81ff">\ </span>                     <span style="color:#75715e"># 2</span>
  --owner &lt;your-github-user&gt; <span style="color:#ae81ff">\ </span> <span style="color:#75715e"># 3</span>
  --repository cluster-name <span style="color:#ae81ff">\ </span>  <span style="color:#75715e"># 4</span>
  --personal <span style="color:#ae81ff">\ </span>                 <span style="color:#75715e"># 5</span>
  --private <span style="color:#ae81ff">\ </span>                  <span style="color:#75715e"># 6</span>
  --path cluster <span style="color:#ae81ff">\ </span>             <span style="color:#75715e"># 7</span>
  --branch master               <span style="color:#75715e"># 8</span>
</code></pre></div><ol>
<li>We&rsquo;re going to need to give <code>flux</code> access to our Github account so that it can manage our GitOps repository on our behalf.
You can generate a new Personal Access Token in <a href="https://github.com/settings/tokens">Github Settings</a>.</li>
<li>We need to tell Flux that we&rsquo;re using Github as git provider. You can also use Gitlab and generic git servers.</li>
<li>The Github username.</li>
<li>The name of our git repository.</li>
<li>(Optional) We&rsquo;re creating a personal Github repo.</li>
<li>(Optional) We&rsquo;re starting out with a private Github repo.</li>
<li>(Optional) A path inside the repository to be watched by Flux.</li>
<li>(Optional) The git branch to be watched by Flux.</li>
</ol>
<p>The bootstrap command is interactive and will let you know about the progress and state of the installation.</p>
<p>This is all you need to get up and running with Flux. Note that the boostrap command is idempotent
and can also be used with an existing GitOps repository.</p>
<h2 id="inspecting-the-flux-components-">Inspecting the Flux components 🕵</h2>
<h3 id="the-flux-system-namespace">The flux-system namespace</h3>
<p>Let&rsquo;s take a look at what was deployed to the k8s cluster for us.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash"># Let&#39;s have a look at the Flux controllers...
kubectl get pods -n flux-system
out:NAME                                      READY   STATUS    RESTARTS   AGE
out:helm-controller-c858b9c65-v8pgr           1/1     Running   28         17d
out:kustomize-controller-6767b8fd78-8ptd4     1/1     Running   30         17d
out:notification-controller-db467b4fb-pt9tc   1/1     Running   29         17d
out:source-controller-7b4d748b45-2blvd        1/1     Running   28         17d
# And the custom resource definitions...
kubectl get crd -o name | grep flux
out:customresourcedefinition.apiextensions.k8s.io/alerts.notification.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/buckets.source.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/gitrepositories.source.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/helmcharts.source.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/helmreleases.helm.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/helmrepositories.source.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/kustomizations.kustomize.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/providers.notification.toolkit.fluxcd.io
out:customresourcedefinition.apiextensions.k8s.io/receivers.notification.toolkit.fluxcd.io</code></pre>
<p>Great! The Flux controllers are running and we have a bunch of new CustomResourceDefinitions available to us.
Read ahead to learn more about how those play together.</p>
<h3 id="the-gitops-repository">The GitOps repository</h3>
<p>To better understand how Flux works, let&rsquo;s now take a look at the directory structure of our bootstrapped GitOps
repository on Github. We&rsquo;re going to see that the bootstrap command has created some Kubernetes yamls for us.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash">git clone git@github.com:${username}/cluster-name.git
cd cluster-name
tree
out:.
out:├── cluster
out:│   └── flux-system
out:│       ├── gotk-components.yaml
out:│       ├── gotk-sync.yaml
out:│       └── kustomization.yaml
out:└── README.md
out:
out:2 directories, 4 files</code></pre>
<p>Cool. We&rsquo;ll go through the individual files.</p>
<h4 id="the-cluster-directory">The cluster directory</h4>
<p>This directory was created because we used <code>flux bootstrap ... --path cluster ...</code> during bootstrapping.
We&rsquo;ll see in a second that this folder is also special because it&rsquo;s being watched by the Flux <code>kustomization-controller</code>.</p>
<p>Any Kubernetes yaml that we throw in this directory will be deployed to our cluster. It also means that we can have
different files and directories in our repository that will never make it to the cluster. This is useful because we could
have one single git repository for our infrastructure declarations and divide it into sub-paths for different k8s clusters
or environments. For example, we could bootstrap multiple Flux environments and use paths like
<code>dev-cluster</code>, <code>stage-cluster</code>, <code>prod-cluster</code>.</p>
<p>That&rsquo;s just one way to organize. It&rsquo;s nice that we have this flexibility.</p>
<h4 id="the-flux-system-directory">The flux-system directory</h4>
<p>This directory represents the <code>flux-system</code> k8s namespace and contains Kubernetes declarations.</p>
<p>I like to continue this pattern and create a subdirectory for each namespace that I deploy with Flux.</p>
<p>Example:</p>
<pre><code>└── cluster
    ├── flux-system
    ├── monitoring
    ├── cool-app-namespace
    └── monitoring
</code></pre><p>This isn&rsquo;t required. You&rsquo;re flexible and can structure the <code>/cluster</code> directory like you want!</p>
<h4 id="kustomizationyaml">kustomization.yaml</h4>
<p>As we can see Flux created a simple <code>kustomization.yaml</code> for us. If you&rsquo;re not familiar with <a href="https://kustomize.io/">Kustomize</a>
you can just ignore this for now. All it does is include the other two files in the <code>cluster/flux-system</code> directory.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># cluster/flux-system/kustomization.yaml</span>
<span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.config.k8s.io/v1beta1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
<span style="color:#f92672">resources</span>:
- <span style="color:#ae81ff">gotk-components.yaml</span>
- <span style="color:#ae81ff">gotk-sync.yaml</span>
</code></pre></div><h4 id="gotk-componentsyaml">gotk-components.yaml</h4>
<p>This file contains the <strong>G</strong>it<strong>O</strong>ps <strong>T</strong>ool<strong>K</strong>it components 🤯 -
in here you will find the <em>Deployments</em> and <em>Services</em> for the Flux controllers and all the <em>CustomResourceDefinitions</em>
that we&rsquo;ve already seen when we inspected the <code>flux-system</code> namespace.</p>
<p>This file is generated by the <code>flux bootstrap</code> command and you shouldn&rsquo;t have to deal with it manually.</p>
<h4 id="gotk-syncyaml">gotk-sync.yaml</h4>
<p>The file <code>gotk-sync.yaml</code> is more interesting to us. This file declares our first two custom resources for Flux.</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># cluster/flux-system/gotk-sync.yaml</span>
---
<span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">source.toolkit.fluxcd.io/v1beta1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GitRepository</span>
<span style="color:#f92672">metadata</span>:
  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">flux-system</span>
<span style="color:#f92672">spec</span>:
  <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">1m0s</span>
  <span style="color:#f92672">ref</span>:
    <span style="color:#f92672">branch</span>: <span style="color:#ae81ff">master</span>
  <span style="color:#f92672">secretRef</span>:
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">url</span>: <span style="color:#ae81ff">ssh://git@github.com/your-user-name-here/cluster-name</span>
---
<span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.toolkit.fluxcd.io/v1beta1</span>
<span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
<span style="color:#f92672">metadata</span>:
  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">flux-system</span>
<span style="color:#f92672">spec</span>:
  <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">10m0s</span>
  <span style="color:#f92672">path</span>: <span style="color:#ae81ff">./cluster</span>
  <span style="color:#f92672">prune</span>: <span style="color:#66d9ef">true</span>
  <span style="color:#f92672">sourceRef</span>:
    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GitRepository</span>
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">flux-system</span>
  <span style="color:#f92672">validation</span>: <span style="color:#ae81ff">client</span>
</code></pre></div><p>Let&rsquo;s take a closer look at these two&hellip;</p>
<h4 id="the-flux-system-gitrepository">The flux-system GitRepository</h4>
<p>The first custom resource is of type <em>GitRepository</em>. A GitRepository is one of the possible <em>Sources</em> that are picked
up by the Flux <code>source-controller</code>.
Once a repository is registered with this controller, the Flux toolkit is able to watch it for changes
and notify other Flux controllers to apply the contents to the cluster.</p>
<p>This specific GitRepository definition tells Flux to check the <code>master</code> branch of our new GitOps repository
from <abbr title="ssh://git@github.com/${username}/cluster-name">Github</abbr>. every <code>1m0s</code> using a <code>flux-system</code> secret for authentication
(this secret contains your Github Personal Access Token that we created during bootstrapping).</p>
<p>Note that we could create more GitRepository resources and effectively sync any number of GitOps repositories with our
Flux-enabled cluster.</p>
<p>Example use-case: You have multiple teams that want to deploy to one cluster. You want each team to maintain their own
infrastructure repository. You could set up Flux on the cluster such that it pulls each team&rsquo;s repository.</p>
<p>Read more in the <a href="https://toolkit.fluxcd.io/components/source/gitrepositories/">Flux Docs</a>.</p>
<h4 id="the-flux-system-kustomization">The flux-system Kustomization</h4>
<p>The second custom resource in this file is a (Flux-) <strong>Kustomization</strong>.</p>
<p>⚠️ Not to be confused with a <a href="https://kustomize.io/"><strong>Kustomize</strong> resource</a>.</p>
<p>A Flux Kustomization describes a <em>path</em> inside of a <em>Source</em> that Flux should apply to the cluster. It&rsquo;s assumed
that there are Kubernetes yamls and/or Kustomize yamls located in the directory at the named path.</p>
<p>The Flux <code>kustomize-controller</code> is going to periodically apply the yamls at the given location to our cluster.</p>
<p>Here&rsquo;s a simplified walk-through of how the <code>kustomize-controller</code> is going to install our k8s resources.</p>
<ul>
<li>Checkout the configured branch from
<pre><code>ssh://git@github.com/${username}/cluster-name
</code></pre></li>
<li>Change the working directory to the specified path: <code>./cluster</code></li>
<li>Build a root <code>Kustomize</code> yaml at this location if no explicit one is available:
<pre><code>kustomize create --autodetect --recursive &gt; kustomization.yaml
</code></pre></li>
<li>Apply the result:
<pre><code>kubectl apply -k kustomization.yaml`
</code></pre></li>
</ul>
<p>I think it&rsquo;s good to know how <code>kustomize</code> is being used to build the final Kubernetes manifest from our GitOps repo.
The commands above can be used for testing locally. This is especially helpful when you mix plain Kubernetes yamls with
Kustomize yamls as this has an impact on the recursive auto-detection.</p>
<h2 id="deploying-own-resources">Deploying own resources</h2>
<p>Let&rsquo;s do a really simple example on how to deploy stuff to our cluster.</p>
<p>All we want to do is create a new namespace &ldquo;awesome-namespace&rdquo;.</p>
<pre class="command-line" data-user="leo" data-host="sldk.de" data-filter-output="out:"><code class="language-bash"># Create a new directory for our namespace
mkdir cluster/awesome-namespace

# Create the Kubernetes namespace descriptor
cat &lt;&lt;EOF &gt; cluster/awesome-namespace/namespace.yaml
out:---
out:apiVersion: v1
out:kind: Namespace
out:metadata:
out:  name: awesome-namespace
out:EOF

# Commit and push the change
git add .
git commit -m &#34;Add &#39;awesome-namespace&#39;&#34;
git push</code></pre>
<p>Lean back and watch as the new namespace is shipped to our cluster 🛳️</p>
<hr>
<h2 id="summary">Summary</h2>
<p>We installed the Flux GitOps toolkit to a Kubernetes cluster and deployed a Kubernetes manifest by only using <code>git</code>.
Along the way we learnt about the Flux components.</p>
<p>I hope this gave you an introduction on what Flux v2 is, how it works and how you can use it to enhance your CI/CD workflows.</p>
<p>I&rsquo;m planning to write more about this topic in the future. We&rsquo;ll take a look at how to
integrate <a href="https://github.com/mozilla/sops">SOPS</a> for secret handling, how to deploy
<a href="https://toolkit.fluxcd.io/components/source/helmcharts/">Helm Charts</a> and how to set up Flux notifications.</p>
<p>If you read this far, hit me up in the comments, on <a href="https://twitter.com/sladkovik">Twitter</a> or <a href="https://www.instagram.com/sladkoff2/">Instagram</a> to let me know how I did.</p>
<p>Thanks 🙏</p>
]]></content></item><item><title>How I build my resume from Markdown</title><link>https://blog.sldk.de/2021/02/how-i-build-my-resume-from-markdown/</link><pubDate>Mon, 15 Feb 2021 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2021/02/how-i-build-my-resume-from-markdown/</guid><description>I was recently inspired to update my Software Engineering resume. The problem with that was that up until now my resume was a Word docx document that I would manually export as PDF and distribute. I used to always get frustrated when I had to touch this document because I hate formatting text in Word and/or Google Docs.
After stumbling upon this Techlead video on YouTube I was convinced that I didn&amp;rsquo;t need any fancy layouts for my CV anyway and that I would go with a minimalist plain-text approach.</description><content type="html"><![CDATA[<p>I was recently inspired to update my Software Engineering resume.
The problem with that was that up until now my resume was a Word <code>docx</code> document that I would manually export as PDF and distribute.
I used to always get frustrated when I had to touch this document because I <strong>hate</strong> formatting text in Word and/or Google Docs.</p>
<p>After stumbling upon this <a href="https://www.youtube.com/watch?v=xpaz7nrNmXA">Techlead video on YouTube</a> I was convinced that I didn&rsquo;t
need any fancy layouts for my CV anyway and that I would go with a minimalist plain-text approach.</p>
<p>If you&rsquo;re like me and you don&rsquo;t need pie charts on your resume, you might be asking:</p>
<blockquote>
<p>Why not write my resume in good old Markdown? Why not just make it open-source on Github as well?
Why not use Github Actions to build a distributable PDF version of it? 🤔</p>
</blockquote>
<p>Hey, that&rsquo;s what I did! And I find it pretty cool. So I recommend you do it, too. Here&rsquo;s why.</p>
<h2 id="the-opensource-markdown-resume-">The Opensource Markdown Resume ™️</h2>
<h3 id="easy-to-write-and-maintain">Easy to write and maintain!</h3>
<p>It&rsquo;s one file. It&rsquo;s simple <strong>af</strong>. I only used headings and bullet lists and decided that it was enough to get my points across.</p>
<p>You can see it on <a href="https://github.com/sladkoff/resume/edit/master/README.md">Github</a> and fork it or whatever.</p>
<h3 id="automated-pdf-export">Automated PDF export!</h3>
<p>I found this <a href="https://github.com/BaileyJM02/markdown-to-pdf">markdown-to-pdf</a> Github Action that exports - you guessed it - Markdown to PDF.
It renders the PDF with the default Github styling, so the result (<a href="https://github.com/sladkoff/resume/releases/tag/2021-02-13">preview available here</a>) looks very similar to the <a href="https://github.com/sladkoff/resume">web version</a>.</p>
<p>Here&rsquo;s the full annotated <a href="https://raw.githubusercontent.com/sladkoff/resume/master/.github/workflows/release.yml">Github Action</a> that I&rsquo;m using now:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#f92672">on</span>:
  <span style="color:#f92672">push</span>:
    <span style="color:#f92672">tags</span>:
      - <span style="color:#e6db74">&#39;*&#39;</span>                                                 <span style="color:#75715e"># (1)</span>

<span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload Release Asset</span>

<span style="color:#f92672">jobs</span>:
  <span style="color:#f92672">build</span>:
    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload Release Asset</span>
    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
    <span style="color:#f92672">steps</span>:
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Checkout code</span>
        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Remove swag line                             </span> <span style="color:#75715e"># (2)</span>
        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">sed -i &#39;1d&#39; README.md</span>
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build PDF from Markdown                      </span> <span style="color:#75715e"># (3)</span>
        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">BaileyJM02/markdown-to-pdf@v1</span>
        <span style="color:#f92672">with</span>:
          <span style="color:#f92672">input_dir</span>: <span style="color:#ae81ff">.</span>
          <span style="color:#f92672">output_dir</span>: <span style="color:#ae81ff">out</span>
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Rename pdf</span>
        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">cp out/README.pdf ./leonid_koftun_resume.pdf</span>
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create Release                               </span> <span style="color:#75715e"># (4)</span>
        <span style="color:#f92672">id</span>: <span style="color:#ae81ff">create_release</span>
        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/create-release@v1</span>
        <span style="color:#f92672">env</span>:
          <span style="color:#f92672">GITHUB_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
        <span style="color:#f92672">with</span>:
          <span style="color:#f92672">tag_name</span>: <span style="color:#ae81ff">${{ github.ref }}</span>
          <span style="color:#f92672">release_name</span>: <span style="color:#ae81ff">Release ${{ github.ref }}</span>
          <span style="color:#f92672">draft</span>: <span style="color:#66d9ef">false</span>
          <span style="color:#f92672">prerelease</span>: <span style="color:#66d9ef">false</span>
      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload Release Asset                         </span> <span style="color:#75715e"># (5)</span>
        <span style="color:#f92672">id</span>: <span style="color:#ae81ff">upload-release-asset</span>
        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-release-asset@v1</span>
        <span style="color:#f92672">env</span>:
          <span style="color:#f92672">GITHUB_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
        <span style="color:#f92672">with</span>:
          <span style="color:#f92672">upload_url</span>: <span style="color:#ae81ff">${{ steps.create_release.outputs.upload_url }}</span>
          <span style="color:#f92672">asset_path</span>: <span style="color:#ae81ff">./leonid_koftun_resume.pdf</span>
          <span style="color:#f92672">asset_name</span>: <span style="color:#ae81ff">leonid_koftun_resume.pdf</span>
          <span style="color:#f92672">asset_content_type</span>: <span style="color:#ae81ff">application/pdf</span>
</code></pre></div><h4 id="annotations">Annotations</h4>
<ol>
<li>Build PDF when any tag is pushed</li>
<li>(Optional) This is just &lsquo;pre-processing&rsquo; to remove some HTML content that I don&rsquo;t want to be rendered to PDF (the first line of the Markdown contains some links that only make sense on the Github web view)</li>
<li>The actual <a href="https://github.com/BaileyJM02/markdown-to-pdf">markdown-to-pdf</a> step with minimal config</li>
<li>Creates a Github release for the tag</li>
<li>Uploads the generated PDF to the tagged release</li>
</ol>
<h3 id="the-opensource-aspect">The opensource aspect</h3>
<p>As you probably noticed by now, my resume is publicly available on Github.</p>
<p>Potential employers and recruiters can grab a copy there. My peers and colleagues can review it and tell me what I suck at the most (come at me).</p>
<h3 id="any-issues-with-this">Any issues with this?</h3>
<p>Yes.</p>
<ul>
<li>If you want to use fancy column layouts and colors and unicorns 🦄 in your resume, then this probably isn&rsquo;t for you (you could make it complicated and add CSS and templating but that kind of defeats the purpose of simplicity in the <code>.md</code> approach IMO).</li>
<li>If you&rsquo;re afraid of me or one of my cats copying some lines from your own personal resume, I do not recommend that you make it public.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<blockquote>
<p>Keep your resume simple by using Markdown, publish it on Github if you want to be a cool kid.</p>
</blockquote>
<p>What do you think of this nonsense? #hmu on <a href="https://twitter.com/sladkovik">Twitter</a> or <a href="https://www.instagram.com/sladkoff2/">Instagram</a>. 🤙</p>
]]></content></item><item><title>Hosting static sites generated with Hugo</title><link>https://blog.sldk.de/2020/01/hosting-static-sites-generated-with-hugo/</link><pubDate>Sat, 25 Jan 2020 00:00:00 +0000</pubDate><guid>https://blog.sldk.de/2020/01/hosting-static-sites-generated-with-hugo/</guid><description>I&amp;rsquo;m using Hugo to generate a static website / blog from markdown.
This means that I have some source files on my local machine and I run hugo to generate static html/css files. Once I have these static files I needed to decide how to publish them to the internet.
This post is a subjective comparison of possible hosting solutions when working with Hugo.
Gitlab Pages My initial plan was to use Gitlab Pages because my source files are already checked into a private git repository on Gitlab.</description><content type="html"><![CDATA[<p>I&rsquo;m using <a href="https://gohugo.io/">Hugo</a> to generate a static website / blog from markdown.</p>
<p>This means that I have some source files on my local machine and I run <code>hugo</code> to generate static html/css files.
Once I have these static files I needed to decide how to publish them to the internet.</p>
<p>This post is a subjective comparison of possible hosting solutions when working with Hugo.</p>
<h2 id="gitlab-pages">Gitlab Pages</h2>
<p>My initial plan was to use <a href="https://about.gitlab.com/product/pages/">Gitlab Pages</a> because my source files are already checked into a private git repository on Gitlab.</p>
<p>I used a simple <code>.gitlab-ci.yaml</code> found on <a href="https://gitlab.com/pages/hugo:">https://gitlab.com/pages/hugo:</a></p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#f92672">image</span>: <span style="color:#ae81ff">registry.gitlab.com/pages/hugo:latest</span>

<span style="color:#f92672">variables</span>:
  <span style="color:#f92672">GIT_SUBMODULE_STRATEGY</span>: <span style="color:#ae81ff">recursive</span>

<span style="color:#f92672">pages</span>:
  <span style="color:#f92672">script</span>:
  - <span style="color:#ae81ff">hugo</span>
  <span style="color:#f92672">artifacts</span>:
    <span style="color:#f92672">paths</span>:
    - <span style="color:#ae81ff">public</span>
  <span style="color:#f92672">only</span>:
  - <span style="color:#ae81ff">master</span>
</code></pre></div><p>This would run on my master branch and publish the generated content to Gitlab Pages.
The setup is simple and everything was in one place, a custom domain can be used and we get a Let&rsquo;s Encrypt TLS certificate as well.</p>
<p>What made me look for an alternative solution? Two things:</p>
<ul>
<li>For some reason I could not get custom 404 pages to work with this setup. When accessing a non-existent URL it would always
load a generic Gitlab 404 page instead of my custom Hugo one. It may be related to this <a href="https://gitlab.com/gitlab-org/gitlab-pages/issues/183">issue</a>.</li>
<li>The loading times seemed atrociously slow for what I was serving 🐌. 5-10 seconds on an empty cache was quite a deal-breaker.</li>
</ul>
<h2 id="github-pages">Github Pages</h2>
<p>Everybody and their mom has a Github account. Github also has <a href="https://pages.github.com/">Pages</a>. So I tried out hosting my hugo content there.</p>
<p>The setup is not so elegant anymore. I followed the instructions on <a href="https://gohugo.io/hosting-and-deployment/hosting-on-github/">https://gohugo.io/hosting-and-deployment/hosting-on-github/</a> and ended up with <strong>two</strong> git repos.
One for my source files (I used my existing Gitlab repo) and one for the generated content under <code>/public</code> which needs to be set up as a git submodule.</p>
<p>This could still be automated with Gitlab CI pretty easily with something like (not tested):</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#f92672">image</span>: <span style="color:#ae81ff">registry.gitlab.com/pages/hugo:latest</span>

<span style="color:#f92672">variables</span>:
  <span style="color:#f92672">GIT_SUBMODULE_STRATEGY</span>: <span style="color:#ae81ff">recursive</span>

<span style="color:#f92672">github-pages</span>:
  <span style="color:#f92672">script</span>:
  - <span style="color:#ae81ff">hugo</span>
  - <span style="color:#ae81ff">cd public &amp;&amp; git -am &#34;Build for Github Pages&#34; &amp;&amp; git push</span>
  <span style="color:#f92672">artifacts</span>:
    <span style="color:#f92672">paths</span>:
    - <span style="color:#ae81ff">public</span>
  <span style="color:#f92672">only</span>:
  - <span style="color:#ae81ff">master</span>
</code></pre></div><p>This tells Gitlab to build the static page and then push the git submodule to Github as well.</p>
<p>Github Pages were subjectively faster but there are downsides:</p>
<ul>
<li>I still didn&rsquo;t get a custom 404 page 😥</li>
<li>I don&rsquo;t like having one repo for the source and one for the generated content</li>
<li>Github Pages works only with <strong>public</strong> repositories (unless you have a paid account AFAIK)</li>
</ul>
<h2 id="netlify">Netlify</h2>
<p>I haven&rsquo;t heard of <a href="https://www.netlify.com/">Netlify</a> before. I think it came up in some Gitlab issue while I was looking for alternatives.</p>
<p>I used their web UI to set everything up but I read that there&rsquo;s also a CLI available which should be cool.
Anyway, I just had to connect my Gitlab account and select the source repository which I had already been working on.</p>
<p>Netlify automagically recognized that the contents of the repo had to be build with hugo and suggested that for the build options:</p>

    <img src="/img/posts/netlify_screenshot.png"  alt="Netlify wizard"  class="center"  style="border-radius: 8px;"  />


<p>Whenever I push to <code>master</code> in Gitlab a hook on Netlify builds and publishes the site. I could get rid of the <code>.gitlab-ci.yml</code> altogether.</p>
<p>I like about this setup:</p>
<ul>
<li>I have <strong>one private</strong> git repo on Gitlab</li>
<li>I haven&rsquo;t had issues with loading times on Netlify so far</li>
</ul>
<p>I understand that there could be one big downside to this approach for other people.
On Gitlab / Github you get a cool <code>xyz.gitlab.io</code> / <code>xyz.github.io</code> domain for free.
A free <code>xyz.netfliy.com</code> domain is not so cool for a tech blog - so I&rsquo;d say that you <strong>need</strong> a custom domain if you choose to go that route.</p>
<h2 id="conclusion">Conclusion</h2>
<p>If you&rsquo;re using hugo to generate your static site, you can use Gitlab Pages, Github Pages and Netlify as a hosting provider quite easily.</p>
<p>Gitlab turned out to have slow loading times. Github requires a weird repo / branching setup. Netlify works nicely together with Gitlab but
you&rsquo;ll probably want a custom domain.</p>
<table>
<thead>
<tr>
<th></th>
<th>Loading Times (cold cache)</th>
<th>Private Repo</th>
<th>Domains</th>
<th>TLS</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Gitlab Pages</strong></td>
<td>~5s ⛔</td>
<td>Yes</td>
<td>.gitlab.io, custom</td>
<td>Let&rsquo;s Encrypt</td>
</tr>
<tr>
<td><strong>Github Pages</strong></td>
<td>&lt;1s</td>
<td>No ⛔</td>
<td>.github.io, custom</td>
<td>Let&rsquo;s Encrypt</td>
</tr>
<tr>
<td><strong>Netlify</strong></td>
<td>&lt;1s</td>
<td>Yes</td>
<td>.netlify.com, custom</td>
<td>Let&rsquo;s Encrypt</td>
</tr>
</tbody>
</table>
]]></content></item></channel></rss>