<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Onder de motorkap - All Subjects Matter</title>
    <link>https://www.allsubjectsmatter.nl/nl/tag/under-the-hood/</link>
    <description>Hoe Remco Loup, CEO/CTO van 333travel, naar problemen kijkt en ze oplost: met data, met teams en met de technologie die past.</description>
    <language>nl</language>
    <image><url>https://www.allsubjectsmatter.nl/images/icon.png</url><title>All Subjects Matter</title><link>https://www.allsubjectsmatter.nl/nl/</link></image>
    <lastBuildDate>Fri, 29 Nov 2024 16:45:40 +0100</lastBuildDate>
    <atom:link href="https://www.allsubjectsmatter.nl/nl/tag/under-the-hood/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Blijf bereikbaar met een dynamic DNS updater voor TransIP</title>
      <link>https://www.allsubjectsmatter.nl/nl/stay-connected-with-a-dynamic-dns-updater-for-transip/</link>
      <guid isPermaLink="false">6749dfb5c2a33e00011cc689</guid>
      <pubDate>Fri, 29 Nov 2024 16:45:40 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Makkelijk toegang tot je pc thuis, ook met een dynamisch IP! Dit script werkt je TransIP DNS-records automatisch bij met je huidige IP, zodat je altijd verbinding hebt. Veilig, aanpasbaar en perfect voor thuiswerken of het hosten van diensten. Blijf moeiteloos bereikbaar!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/stay-connected-with-a-dynamic-dns-updater-for-transip/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In een wereld vol DHCP-adressen is externe toegang tot je pc of server thuis een gedoe. Heb je een provider die je een <strong>dynamisch publiek IP-adres</strong> toewijst, dan heb je een probleem: je IP verandert regelmatig, en daarmee breekt de verbinding met je apparaat thuis via een statisch domein.</p>
<p>Dit gratis <strong>Dynamic DNS (DDNS) updater script</strong> voor TransIP lost dat op. Het actualiseert je DNS-records zodra je externe IP verandert, zodat je domein (bijv. <code>vpn.example.com</code>) altijd naar het juiste IP wijst. Dit script is een must voor iedereen die TransIP&rsquo;s DNS-diensten gebruikt.</p>
<h2 id="key-features-of-the-script">Belangrijkste features van het script</h2>
<ol>
<li><strong>Geautomatiseerde DNS-updates:</strong>
<ul>
<li>Controleert automatisch je huidige externe IP en vergelijkt dat met het DNS A-record van je domein.</li>
<li>Maakt het A-record aan of werkt het bij zodat het overeenkomt met je nieuwste externe IP.</li>
</ul>
</li>
<li><strong>Dynamic DNS voor dynamische IP&rsquo;s:</strong>
<ul>
<li>Ideaal voor situaties waarin je publieke IP regelmatig verandert, zoals een internetverbinding thuis met dynamische DHCP-toewijzing.</li>
</ul>
</li>
<li><strong>Veilige authenticatie:</strong>
<ul>
<li>Gebruikt TransIP&rsquo;s API met een private key voor veilige communicatie.</li>
<li>Genereert kort geldige tokens voor extra veiligheid (maximaal 10 minuten per sessie).</li>
</ul>
</li>
<li><strong>Aanpasbare instellingen:</strong>
<ul>
<li>Configureer eenvoudig domein, subdomein, TTL (time-to-live) en logging.</li>
<li>Pas het script aan naar je eigen wensen.</li>
</ul>
</li>
<li><strong>Logging en foutafhandeling:</strong>
<ul>
<li>Uitgebreide logging houdt elke stap van het updateproces bij.</li>
<li>Ingebouwde foutafhandeling voor een soepelere uitvoering en makkelijker debuggen.</li>
</ul>
</li>
</ol>
<h2 id="how-it-works">Hoe het werkt</h2>
<ol>
<li><strong>Initiële setup:</strong>
<ul>
<li>Maak een TransIP-account aan en genereer een private key voor API-toegang op <a href="https://www.transip.nl/cp/account/api/" data-external>https://www.transip.nl/cp/account/api/</a></li>
<li>Configureer het script met je domeinnaam, subdomein en het pad naar je private key.</li>
</ul>
</li>
<li><strong>Extern IP ophalen:</strong>
<ul>
<li>Het script haalt je huidige externe IP op via een publieke API zoals <code>ipinfo.io</code>.</li>
</ul>
</li>
<li><strong>Token genereren:</strong>
<ul>
<li>Genereert een kort geldig access token via TransIP&rsquo;s authenticatie-endpoint.</li>
</ul>
</li>
<li><strong>DNS-record controleren:</strong>
<ul>
<li>Controleert of het huidige DNS A-record overeenkomt met je externe IP.</li>
<li>Verschilt het, dan werkt het script het record bij; ontbreekt het, dan maakt het een nieuw aan.</li>
</ul>
</li>
<li><strong>DNS updaten:</strong>
<ul>
<li>Gebruikt TransIP&rsquo;s beveiligde API om het DNS A-record bij te werken of aan te maken met je huidige externe IP.</li>
</ul>
</li>
<li><strong>Logging en meldingen:</strong>
<ul>
<li>Logt elke stap en geeft duidelijke meldingen als er iets misgaat.</li>
</ul>
</li>
</ol>
<h2 id="prerequisites">Vereisten</h2>
<p>Voordat je het script gebruikt, zorg dat je het volgende hebt:</p>
<ol>
<li>Een TransIP-account met API-toegang ingeschakeld.</li>
<li>Een geregistreerd domein in je TransIP-account.</li>
<li>Een statische private key voor veilige API-authenticatie.</li>
<li>De benodigde dependencies op je systeem:
<ul>
<li><code>curl</code>, <code>openssl</code> en <code>jq</code>.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt update
sudo apt install -y curl openssl jq</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><h2 id="why-use-this-solution">Waarom deze oplossing?</h2>
<p>In tegenstelling tot betaalde externe DDNS-providers geeft dit script je gratis controle over je eigen DNS-beheer, met privacy en zonder vendor lock-in. Het is op maat gemaakt.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=&amp;#34;your-login-name&amp;#34;  # Fill in your TransIP login name
PRIVATE_KEY_PATH=&amp;#34;/path/transip_private_key.pem&amp;#34;  # Path to your private key file
AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
TOKEN_FILE=&amp;#34;/var/tmp/transip_token.json&amp;#34;  # File to store token and expiration date
DOMAIN=&amp;#34;example.com&amp;#34;  # Your main or test domain
SUBDOMAIN=&amp;#34;vpn&amp;#34;  # The subdomain you want to update (e.g., test.example.com)
RECORD_TYPE=&amp;#34;A&amp;#34;
TTL=60  # Time-to-live for DNS records
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt; $LOG_FILE
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;
    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Set DNS to external DHCP $TIMESTAMP&amp;#34;
    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)
    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;
    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)
    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;
    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;
    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;
    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi
    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to get the current DNS A-record from TransIP
get_dns_record() {
    # Get API response and store status code
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Check if response contains JSON and no errors
    if echo &amp;#34;$RESPONSE&amp;#34; | jq empty 2&amp;gt;/dev/null; then
        RECORD_CONTENT=$(echo $RESPONSE | jq -r &amp;#39;.dnsEntries[] | select(.name==&amp;#34;&amp;#39;$SUBDOMAIN&amp;#39;&amp;#34; and .type==&amp;#34;&amp;#39;$RECORD_TYPE&amp;#39;&amp;#34;) | .content&amp;#39;)
    else
        handle_error &amp;#34;API response does not contain valid JSON or contains an error: $RESPONSE&amp;#34;
    fi

    # Check if we found a valid record
    if [ -z &amp;#34;$RECORD_CONTENT&amp;#34; ]; then
        log_message &amp;#34;No valid DNS record found for $SUBDOMAIN.$DOMAIN. Record may need to be created.&amp;#34;
        return 1
    fi

    echo &amp;#34;$RECORD_CONTENT&amp;#34;
    return 0
}

# Function to update or create DNS record
update_or_create_dns_record() {
    # Check if record exists
    CURRENT_DNS_IP=$(get_dns_record)

    if [ -z &amp;#34;$CURRENT_DNS_IP&amp;#34; ]; then
        log_message &amp;#34;No DNS A-record found for $SUBDOMAIN.$DOMAIN. Creating record.&amp;#34;
        # Create request body for new record (TTL set to 60 seconds)
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;60&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (create): $DNS_ENTRY&amp;#34;

        # Execute POST request to create new DNS entry with better status code handling
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X POST \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API response for creating DNS entry: $RESPONSE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for creation: $HTTP_STATUS_UPDATE&amp;#34;

        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 201 ]; then
            handle_error &amp;#34;API call for creating DNS entry failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        fi

        log_message &amp;#34;New DNS A-record successfully created.&amp;#34;
    else
        log_message &amp;#34;Updating DNS A-record for $SUBDOMAIN.$DOMAIN to IP $CURRENT_IP&amp;#34;
        # Create request body for updating the record
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;$TTL&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (update): $DNS_ENTRY&amp;#34;

        # Execute PATCH request to update the existing DNS record
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X PATCH \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_UPDATE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API update response received: $RESPONSE_UPDATE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for update: $HTTP_STATUS_UPDATE&amp;#34;

        # Accept both 200 and 204 as successful updates
        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 200 ] &amp;amp;&amp;amp; [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 204 ]; then
            handle_error &amp;#34;API call for DNS update failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        else
            log_message &amp;#34;DNS A-record successfully updated to $CURRENT_IP&amp;#34;
        fi
    fi
}

# Get the current external IP
CURRENT_IP=$(curl -s http://ipinfo.io/ip) || handle_error &amp;#34;Failed to get external IP&amp;#34;

# Log the start
log_message &amp;#34;---- Start new run ----&amp;#34;
log_message &amp;#34;Current external IP: $CURRENT_IP&amp;#34;

# Always generate a short lived new token.
generate_new_token


# Try to update or create the DNS record
update_or_create_dns_record

log_message &amp;#34;---- End of run ----&amp;#34;</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre>]]></content:encoded>
    </item>
    <item>
      <title>Let&#39;s Encrypt automatiseren met Certbot en TransIP in Docker</title>
      <link>https://www.allsubjectsmatter.nl/nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/</link>
      <guid isPermaLink="false">6749c93bc2a33e00011cc621</guid>
      <pubDate>Thu, 28 Nov 2024 22:03:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Wil je je SSL-certificaten automatisch updaten en beheren in Docker met Certbot, met je domein gehost bij TransIP? Dan hoef je niet verder te zoeken!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>SSL-certificaten beheren voor domeinen die bij TransIP gehost worden, kan zonder automatisering een gedoe zijn. TransIP biedt gelukkig een degelijke API voor DNS-beheer, maar om die naadloos te integreren met Certbot voor <code>dns-01</code>-validatie is een eigen oplossing nodig. Omdat ik daar geen kant-en-klaar script voor kon vinden, heb ik er zelf één gemaakt dat veilig de authenticatie, DNS-updates en het aanmaken van certificaten regelt.</p>
<p>De volledige API-documentatie vind je hier:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://api.transip.nl/rest/docs.html" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">TransIP API</span><span class="bookmark__description"></span><span class="bookmark__meta"><img src="https://www.allsubjectsmatter.nl/nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/bookmarks/01-icon.ico" sizes="25px" alt="" class="bookmark__icon" loading="lazy" decoding="async"></span></span></a></figure>

<p>Deze handleiding legt de complete setup met Docker uit, inclusief hoe je het API-keypair genereert in de webinterface van TransIP en gebruikt voor geautomatiseerd certificaatbeheer.</p>
<h2 id="how-transip-authentication-works">Hoe de authenticatie bij TransIP werkt</h2>
<p>TransIP gebruikt een RSA-keypair voor API-authenticatie. Zo stel je dat in:</p>
<ol>
<li><strong>Genereer het keypair in TransIP</strong>:
<ul>
<li>Log in op je <a href="https://www.transip.nl/" data-external>TransIP-controlepaneel</a>.</li>
<li>Ga naar <strong>API-instellingen</strong> onder je account en schakel API-toegang in.</li>
<li>Genereer een nieuw API-keypair.</li>
<li><strong>Download/sla de private key</strong> (<code>transip_private_key.pem</code>) veilig op en maak er een secret van voor gebruik in je Docker-setup.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker secret create transip_private_key transip_private_key.pem</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol>
<li><strong>Houd de private key veilig</strong>:
<ul>
<li>Bewaar de private key op een veilige plek nadat je er een secret van hebt gemaakt.</li>
<li>Deze private key is nodig om requests naar de API van TransIP te signen.</li>
</ul>
</li>
<li><strong>Hoe het werkt</strong>:
<ul>
<li>Het script gebruikt de private key om een authenticatie-request te signen.</li>
<li>De API van TransIP antwoordt met een token met korte geldigheid (standaard 10 minuten), die vervolgens gebruikt wordt om de DNS-records te beheren.</li>
<li>Voor het gemak maakt het script bij elke run een nieuwe token aan. Dat maakte het robuuster dan controleren op een actieve token en die hergebruiken, wat het hele script minder betrouwbaar en omslachtiger maakte.</li>
</ul>
</li>
</ol>
<h2 id="what-this-setup-is-going-to-do">Wat deze setup gaat doen</h2>
<ol>
<li><strong>Automatiseert SSL-certificaten</strong>:
<ul>
<li>Automatiseert het aanmaken en vernieuwen van wildcard- en domeinspecifieke certificaten.</li>
</ul>
</li>
<li><strong>Verwerkt DNS-challenges</strong>:
<ul>
<li>Beheert <code>dns-01</code>-challenges door DNS TXT-records aan te maken en te verwijderen via de TransIP API.</li>
</ul>
</li>
<li><strong>Naadloze integratie</strong>:
<ul>
<li>Alles zit in een Docker container voor draagbaarheid en eenvoud.</li>
<li>Je kunt in dit script domeinen verwijderen of toevoegen bij het -d-gedeelte. Ik heb de voorbeelden erin laten staan voor een betere uitleg.</li>
</ul>
</li>
</ol>
<p>We pipen een add- of clean-commando naar het bash-script om de acties uit te voeren die in het script staan. We gebruiken hier dus de &ndash;manual-auth-hook en de manual-cleanup-hook. Zo wordt bijvoorbeeld gegarandeerd dat een fout in het add-gedeelte alsnog netjes wordt opgeruimd.</p>
<p>Je kunt ook een &ndash;force aan dit script toevoegen, maar dan ben je snel door je pogingen bij Letsencrypt heen, want ze staan maar een paar calls per 24 uur toe. Wel zorgt dit ervoor dat de certificaten altijd vernieuwd worden als je iets wilt testen of verifiëren.</p>
<pre class="line-numbers language-yaml"><code class="language-yaml">version: &amp;#34;3.8&amp;#34;

services:
  certbot:
    image: certbot/certbot:latest
    container_name: certbot-transip
    secrets:
      - transip_private_key  # Reference the secret you created earlier
    # adjust these volumes to point to the right .pem and script. right now this dockerfile expects the files to be in the same folder as the docker-certbot-transip.yml
    volumes:
      - &amp;#34;./certbot-transip.sh:/usr/local/bin/certbot-transip.sh:ro&amp;#34;
      - &amp;#34;./letsencrypt:/etc/letsencrypt&amp;#34;
    environment:
    #These Variables are going to be piped in our bash script.
      LOGIN_NAME: &amp;#34;your-transip-login&amp;#34;        # Replace with your TransIP login name
      PRIVATE_KEY_PATH: &amp;#34;/etc/transip_private_key.pem&amp;#34;
      DOMAIN: &amp;#34;example.com&amp;#34;                  # Replace with your domain
      TTL: &amp;#34;60&amp;#34;                              # Optional: Time-to-live for DNS challenge
      CERTBOT_EMAIL: &amp;#34;you@example.com&amp;#34;       # Replace with your valid email as requested by letsencrypt.
    entrypoint:
      - sh
      - -c
      - |
        apk update &amp;amp;&amp;amp; apk add --no-cache bash curl jq bind-tools &amp;amp;&amp;amp; \
        chmod &#43;x /usr/local/bin/certbot-transip.sh &amp;amp;&amp;amp; \
        certbot certonly --manual --preferred-challenges dns \
          --manual-auth-hook &amp;#34;/usr/local/bin/certbot-transip.sh add&amp;#34; \
          --manual-cleanup-hook &amp;#34;/usr/local/bin/certbot-transip.sh clean&amp;#34; \
          --cert-name &amp;#34;$DOMAIN&amp;#34; \
          -d &amp;#34;*.$DOMAIN&amp;#34; \
          -d &amp;#34;example2.$DOMAIN&amp;#34; \
          -d &amp;#34;example3.$DOMAIN&amp;#34; \
          --expand \
          --non-interactive --agree-tos \
          --email &amp;#34;$CERTBOT_EMAIL&amp;#34;

secrets:
  transip_private_key:
    external: true  # Secret must already exist in Docker</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="the-bash-script">Het Bash-script</h2>
<p>Dit is het &ldquo;geavanceerde&rdquo; deel dat zijn magie doet en alles met TransIP laat werken. Ik heb het ook een stuk robuuster gemaakt dan standaard. Dit zou dus hopelijk helpen om een stabiele en goed werkende omgeving op te zetten.</p>
<p>Je kunt dit TransIP-script ook lokaal draaien om certificaten aan te maken, of in een Kubectl- of Helm chart.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=${LOGIN_NAME}
TTL=${TTL:-60}  # Default value 60 if TTL is not set
PRIVATE_KEY_PATH=${PRIVATE_KEY_PATH}
DOMAIN=${DOMAIN}

AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
#log_message() {
#    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt;&amp;gt; $LOG_FILE
#}

# Function to log messages to stdout
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34;
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;

    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Certbot DNS update $TIMESTAMP&amp;#34;

    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)

    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;

    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)

    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;

    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;

    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;

    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi

    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to add or update a TXT record
add_or_update_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    if [ -z &amp;#34;$name&amp;#34; ] || [ -z &amp;#34;$value&amp;#34; ]; then
        log_message &amp;#34;ERROR: Name or value for TXT record is missing.&amp;#34;
        exit 1
    fi

    # Check if the DNS record already exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all existing records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are multiple or no matching records
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    # Ensure MATCHING_COUNT is always an integer
    if ! [[ &amp;#34;$MATCHING_COUNT&amp;#34; =~ ^[0-9]&#43;$ ]]; then
        MATCHING_COUNT=0
    fi

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name. Creating a new record.&amp;#34;
    elif [ &amp;#34;$MATCHING_COUNT&amp;#34; -ge 1 ]; then
        log_message &amp;#34;Multiple or one existing TXT record found for $name. Removing all existing records.&amp;#34;

        # Remove all existing records using the full details of each record
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Log the details of the record to be removed
            log_message &amp;#34;Remove record: name=$existing_name, expire=$existing_expire, content=$existing_content&amp;#34;

            # Make the DELETE call using the full DNS-entry details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Log the response of the DELETE call
            log_message &amp;#34;Response after removing $existing_name: $DELETE_RESPONSE&amp;#34;

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;Record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi

    # Create a new record
    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X POST \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record creation: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be created. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully added for $name with value $value&amp;#34;
    fi
}

# Function to update an existing TXT record
update_txt_record() {
    local name=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X PATCH \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record update: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be updated. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully updated for $name with value $value&amp;#34;
    fi
}

# Function to remove a TXT record for `dns-01` validation
remove_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    # Check if the DNS record exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are existing records that need to be removed
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name to remove.&amp;#34;
    else
        log_message &amp;#34;Removing all existing TXT records for $name.&amp;#34;

        # Loop through all found records and remove them
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Make the DELETE call using the full details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The TXT record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;TXT record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi
}

# Always generate a new token for each run
generate_new_token

# Certbot passes these values when adding and removing the record
if [ &amp;#34;$1&amp;#34; == &amp;#34;add&amp;#34; ]; then
    add_or_update_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34; &amp;#34;$CERTBOT_VALIDATION&amp;#34;
    sleep 25  # Wait for DNS update to propagate
elif [ &amp;#34;$1&amp;#34; == &amp;#34;clean&amp;#34; ]; then
    remove_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34;
fi</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="steps-to-run">Stappen om te draaien</h2>
<ol>
<li><strong>Download de private key</strong>: Sla het bestand met de private key (<code>transip_private_key.pem</code>) uit de API-instellingen van TransIP op op het apparaat waar je dit gaat draaien.</li>
<li><strong>Stel Docker Compose in</strong>: Zet de bestanden <code>docker-compose.yml</code> en <code>certbot-transip.sh</code> in dezelfde map. (of pas het script aan)</li>
<li><strong>Start de container</strong>: Start de container (cd naar het pad waar je yml staat)</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker-compose up --build</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Houd de logoutput in de gaten: de logoutput van de Docker container laat de voortgang van de DNS-challenges en het aanmaken van het certificaat zien.</p>
<p>Meer is het niet!</p>
<p>Binnenkort post ik ook een versie met een werkende Helm chart. Die was wat lastiger om alles correct te laten werken en alle certificaten te laten updaten via verschillende namespaces, waar ik de certificaten op verschillende domeinen beschikbaar moest hebben.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deprecated: Geef je headless (Raspberry Pi) VPN een boost met ProtonVPN en WireGuard</title>
      <link>https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/</link>
      <guid isPermaLink="false">66f3e524e2acb1000109843a</guid>
      <pubDate>Wed, 25 Sep 2024 22:24:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Deprecated: Beheer ProtonVPN op je headless device zoals een Raspberry Pi moeiteloos met dit Python-script. Maak automatisch verbinding met de snelste server via WireGuard voor optimale snelheid en veiligheid. Dit omzeilt error 422 met de protonvpn-cli client, die op dit moment kapot is.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>De API: <a href="https://api.protonvpn.ch/vpn/logicals" data-external>https://api.protonvpn.ch/vpn/logicals</a> is volgens Proton Support officieel stopgezet. Het enige wat hij nog teruggeeft is:</p>
<pre class="line-numbers language-json"><code class="language-json">{&amp;#34;Code&amp;#34;:0,&amp;#34;Error&amp;#34;:&amp;#34;Invalid access token&amp;#34;,&amp;#34;Details&amp;#34;:{}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Dit artikel is daarmee helaas achterhaald. Iemand is een petitie begonnen om deze functionaliteit terug te krijgen.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://protonmail.uservoice.com/forums/932836-proton-vpn/suggestions/49774472-re-enable-access-to-api" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Re-enable access to API</span><span class="bookmark__description">Tot voor kort kon je via de API (https://api.protonvpn.ch/vpn/logicals) servers filteren op onder meer land en drukte, zodat je verbinding maakt met een server die niet overbelast is. Die mogelijkheid is verdwenen. 27 stemmen, 4 reacties.</span><span class="bookmark__meta"><span>Proton VPN feedback</span></span></span></a></figure>

<h2 id="the-deprecated-guide-which-no-longer-functions-as-of-062025-for-historical-reasons">De achterhaalde handleiding die niet meer werkt sinds 06/2025. Voor de historie bewaard.</h2>
<p>Toen ProtonVPN zijn command-line interface (CLI)-tool stopzette, bleven gebruikers van headless systemen zoals de Raspberry Pi zonder een eenvoudige manier om VPN-verbindingen te beheren. Zonder grafische interface is het handmatig wisselen van VPN-servers een gedoe geworden. Ook het selecteren van de snelste VPN uit een pool van servers is onmogelijk geworden.</p>
<p>Om dit probleem op te lossen, heb ik een Python-script ontwikkeld dat automatisch de snelste ProtonVPN-server selecteert op basis van latency en load, en daarna je WireGuard-configuratie voor je bijwerkt. Deze handleiding neemt je mee door het instellen van WireGuard voor ProtonVPN, het installeren van het script en het automatiseren van je VPN-verbinding, zodat je Raspberry Pi altijd verbonden is met de beste server.</p>
<h3 id="why-do-i-need-this-protonss-script">Waarom heb ik dit Protonss-script nodig?</h3>
<p>Dit Python-script automatiseert het verbinden van je Raspberry Pi met de snelst beschikbare ProtonVPN-server, met <strong>WireGuard</strong> als VPN-protocol. Het controleert de latency en load van meerdere VPN-servers via de API van ProtonVPN, kiest de server met de beste prestaties, werkt je WireGuard-configuratie bij met de nieuwe servergegevens en maakt alleen opnieuw verbinding als er een duidelijk snellere server is gevonden (niemand zit eigenlijk te wachten op reconnects). Het script logt het hele proces, wat het debuggen een stuk makkelijker maakt.</p>
<ol>
<li><strong>Geen handmatig VPN-wisselen meer</strong>: Zonder de CLI van ProtonVPN zou het wisselen naar de snelste server handwerk betekenen. Dit script automatiseert dat proces door regelmatig de beschikbare servers te controleren en de verbinding bij te werken op basis van realtime prestaties.</li>
<li><strong>Optimaliseert de verbindingssnelheid</strong>: Het script zorgt dat je altijd verbonden bent met de VPN-server met de laagste latency en load. Dit is vooral handig om snelle verbindingen te behouden in landen met snelle ProtonVPN-servers of servers met weinig verkeer.</li>
<li><strong>Vermindert downtime</strong>: Door het herverbinden te automatiseren, minimaliseert het script mogelijke downtime door slechte serverprestaties of trage verbindingen.</li>
<li><strong>Gemaakt voor de Raspberry Pi</strong>: Als je een headless Raspberry Pi draait (zonder monitor), is dit script ideaal om VPN-taken te automatiseren zonder constante supervisie.</li>
<li><strong>Beveiliging &amp; privacy</strong>: Door ervoor te zorgen dat je Raspberry Pi altijd via een VPN verbinding maakt, beschermt het je data en online activiteit, zeker als je de Pi gebruikt voor torrenting of als netwerkapparaat.</li>
</ol>
<p>Dit script automatiseert dus het vinden van de optimale ProtonVPN-server, met hogere snelheden, meer betrouwbaarheid en gemak voor headless systemen zoals een Raspberry Pi.</p>
<h2 id="lets-dive-in">Aan de slag!</h2>
<p>Eerst moet je WireGuard installeren, een snel en lichtgewicht VPN-protocol dat we gebruiken om verbinding te maken met onze ProtonVPN-server.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># First, update your package list and install WireGuard:
sudo apt update
sudo apt install wireguard curl resolvconf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p><strong>Genereer een WireGuard-configuratie via ProtonVPN</strong></p>
<ul>
<li>Log in op je ProtonVPN-account op hun website.</li>
<li>Ga naar het gedeelte &ldquo;Downloads&rdquo; in je dashboard.</li>
<li>Genereer een nieuw configuratiebestand onder &ldquo;WireGuard Configurations&rdquo;.</li>
<li>Gebruik je torrents, schakel dan <strong>NAT-PMP</strong> in.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_e1bb7171adce48a.webp" alt="" width="560" height="368" data-zoom-src="https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_1345a69245a3f8c.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Sla dit configuratiebestand op je Raspberry Pi op:</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Save the configuration file to your Raspberry Pi:
# Place it in the WireGuard directory and rename it for simplicity:
sudo nano /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p>Zodra je het configuratiebestand in de juiste map hebt geplaatst, controleer je de verbinding.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Verify connection to ProtonVPN:
sudo wg-quick up protonvpn

# This should give an ip address different to the one frome your isp
curl ifconfig.me 

# Verify connection to ProtonVPN:
sudo wg-quick down protonvpn

curl ifconfig.me </code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Als deze twee ip-adressen van ifconfig.me verschillen, ben je verbonden via een VPN. Ook als je je eigen ip-adres niet kent omdat het een DHCP-adres of iets dergelijks is.</p>
<h3 id="installing-our-python-environment-for-protonss">Onze Python-omgeving installeren voor Protonss.</h3>
<p>Bij het ontwikkelen en draaien van Python-scripts op je Raspberry Pi is het verstandig om een <strong>Python virtual environment</strong> te gebruiken. Zo&rsquo;n virtuele omgeving laat je Python-packages geïsoleerd installeren en beheren, zodat de standaard Python-configuratie van je systeem onaangeroerd blijft.</p>
<p>Veranderingen in je omgeving beïnvloeden dan alleen het project waaraan je werkt — zoals het installeren van specifieke packages zoals <code>requests</code> — zonder conflicten te veroorzaken met andere Python-projecten of systeemafhankelijkheden.</p>
<p>Zo stel je een virtuele omgeving in voor ons script:</p>
<ol>
<li><strong>Installeer de virtuele omgeving</strong>: Installeer eerst het package <code>python3-venv</code>, waarmee je virtuele omgevingen kunt aanmaken en beheren.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt install -y python3-venv python3-full</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="2">
<li><strong>Maak een virtuele omgeving</strong>: Na installatie kun je een virtuele omgeving aanmaken. Dit genereert een geïsoleerde omgeving in de door jou opgegeven map (in dit geval <code>~/protonss</code>).</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">python3 -m venv ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="3">
<li><strong>Activeer de virtuele omgeving</strong>: Na het aanmaken moet je de omgeving activeren. Dit zorgt dat je systeem de Python-versie en packages uit de virtuele omgeving gebruikt in plaats van de systeembrede Python-installatie.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ul>
<li>Nu de omgeving actief is, wordt elke Python-package die je installeert (zoals <code>requests</code>) in deze geïsoleerde omgeving geïnstalleerd.</li>
</ul>
<pre class="line-numbers language-bash"><code class="language-bash">pip install requests</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p><strong>Deze virtuele omgeving in de toekomst gebruiken</strong>: Elke keer dat je Python-scripts wilt draaien die van deze virtuele omgeving afhankelijk zijn, moet je hem opnieuw activeren met het commando <code>source ~/venv/bin/activate</code>. Eenmaal actief draaien alle Python-commando&rsquo;s en packages in deze omgeving.</p>
<h3 id="installing-our-protonss-script-to-automate-a-connection-to-the-fastest-vpn">Ons ProtonSS-script installeren om automatisch met de snelste VPN te verbinden.</h3>
<p>We maken een configuratiebestand aan met alle variabelen die het Python-script gebruikt. Daarin definieer je bijvoorbeeld het pad naar je WireGuard-configuratie, het logbestand en het aantal pings om latency te testen.</p>
<pre class="line-numbers language-bash"><code class="language-bash">cd ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Voorbeeld <code>config.ini</code>:</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano config.ini</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-ini"><code class="language-ini">[Settings]
# Path to your WireGuard configuration file
wg_config_path = /etc/wireguard/protonvpn.conf

# Path to your log file
log_file = /var/log/wireguard_updater.log

# Number of pings to measure latency
ping_count = 3

# Maximum score difference before reconnection 0 - 100
# Average score of servers is between 10-40 it seems. so setting this to high results in no switches from servers
score_threshold = 5.0

# Comma-separated list of preferred countries
preferred_countries = US

# Comma-separated list of preferred cities
# If specified, servers must be in these cities (unless they&amp;#39;re in a country without any specified city)
# Preferred cities to connect to. If specified, servers must be in these cities
# (unless they&amp;#39;re in a country without any specified city).
# Example: preferred_cities = Phoenix, Los Angeles, Seattle
# This allows you to target specific cities within your preferred countries.
# If left empty, any city within the preferred countries will be considered.
# Note: City names are case sensitive and must match ProtonVPN&amp;#39;s naming exactly.
# Multiple cities should be comma-separated.
preferred_cities =Phoenix

# Weight for latency in the server score
# latency is subjective since this is probably measured through the vpn and only counts as server latency.
# Checking the latency without a vpn isnt recommended since the proton servers wont reply back without vpn.
# Value cant be higher then 1. 0.3 translates to 30% of the score_treshold.
latency_weight = 0.3

# Weight for server load in the server score
# Load is the best factor 0.7 translates to 70% of the weight of the score_threshold. this cant be higher then 1.
load_weight = 0.7

# Enable or disable pinging servers (True or False)
# Since ping might not be reliable enough for you to keep on switching servers this can be disabled here.
ping_enabled = True

# Specify the interface to use for ping tests. By setting this option you can ping to the fastest vpn server
# this will ensure the lowest latency VPN from your location.
ping_interface = eth0

[Features]
# Choose which type of servers you want to use
# Options: p2p, plus, secure_core, tor
# Default is p2p if not specified
feature_type = p2p

# Feature explanation:
# Each server has exactly ONE feature number:
# 1, 17 = Secure Core servers
# 2 = TOR servers
# 0, 8, 16, 24 = Plus-only servers
# 4, 12, 20, 28 = P2P servers (these are also Plus servers)</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Je kunt de variabelen naar behoefte aanpassen. Laat je <code>preferred_countries</code> leeg, dan controleert het script een HELEBOEL! servers, waardoor het script erg lang gaat draaien!</p>
<h4 id="create-the-python-script"><strong>Maak het Python-script aan</strong></h4>
<p>Nu gaan we het Python-script maken dat je VPN-verbinding automatiseert. Eerst moeten we een paar stappen voorbereiden.</p>
<p>De eerste regel van het script moet een <strong>shebang</strong> zijn, die er altijd voor zorgt dat het script in de juiste Python-omgeving draait:</p>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate
which python</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Dit geeft je het pad dat je op de eerste regel van het script moet zetten.</p>
<pre class="line-numbers language-text"><code class="language-text">/home/pi/venv/bin/python</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Pas het script aan door een <strong>shebang</strong> toe te voegen: zet de volgende regel helemaal bovenaan je Python-script. Vervang <code>/home/{user}/venv/bin/python</code> door het daadwerkelijke pad dat je kreeg toen je eerder het commando <code>which python</code> uitvoerde. Laat de <code>#!</code> aan het begin van de regel staan—daaruit maakt het systeem op welke interpreter het script moet draaien. We hebben het absolute pad nodig, dus ~/ werkt niet.</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-python"><code class="language-python">#!/home/{user}/protonss/bin/python

import requests
import subprocess
import os
import sys
import configparser
import logging
import re
from datetime import datetime
import math
import shutil

# Get the directory of the current script
script_dir = os.path.dirname(os.path.abspath(__file__))

# Load the config file from the same directory as the script
config = configparser.ConfigParser()
config.read(os.path.join(script_dir, &amp;#39;config.ini&amp;#39;))

WG_CONFIG_PATH = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;wg_config_path&amp;#39;)
LOG_FILE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;log_file&amp;#39;)
PING_COUNT = config.getint(&amp;#39;Settings&amp;#39;, &amp;#39;ping_count&amp;#39;)
SCORE_THRESHOLD = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;score_threshold&amp;#39;, fallback=5.0)
PREFERRED_COUNTRIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_countries&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PREFERRED_CITIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_cities&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PING_INTERFACE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;ping_interface&amp;#39;, fallback=None)

# Weighting factors for latency and load from the config
LATENCY_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;latency_weight&amp;#39;, fallback=0.3)
LOAD_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;load_weight&amp;#39;, fallback=0.7)

# Ping option from config
PING_ENABLED = config.getboolean(&amp;#39;Settings&amp;#39;, &amp;#39;ping_enabled&amp;#39;, fallback=True)

# Clean up country codes (remove spaces and convert to uppercase)
PREFERRED_COUNTRIES = [country.strip().upper() for country in PREFERRED_COUNTRIES if country.strip()]

# Clean up city names (remove spaces)
PREFERRED_CITIES = [city.strip() for city in PREFERRED_CITIES if city.strip()]

# Set up logging
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format=&amp;#39;%(asctime)s [%(levelname)s] %(message)s&amp;#39;,
    datefmt=&amp;#39;%Y-%m-%d %H:%M:%S&amp;#39;
)

logging.info(&amp;#39;--- ProtonVPN Auto Connect Script Start ---&amp;#39;)

# Feature definitions
FEATURE_SECURE_CORE = [1, 17]  # Secure Core servers
FEATURE_TOR = [2]         # TOR servers
FEATURE_PLUS = [0, 8, 16, 24]  # Plus-only servers
FEATURE_P2P = [4, 12, 20, 28]    # P2P servers (these are also Plus servers)
# check https://api.protonmail.ch/vpn/logicals for more info. 
# be aware that the secure core and tor server are very limited so check if one is available for your country as stated in the config.ini file.
# FREE servers are not included in the api response. so they are already left out.

def get_feature_description(features):
    # If no feature type is specified in config.ini, default to P2P
    # Features is a number representing server capabilities
    if not features:  # If features is 0 or None
        return &amp;#34;P2P&amp;#34;  # Default to P2P
    elif features in FEATURE_SECURE_CORE:
        return &amp;#34;Secure Core&amp;#34;
    elif features in FEATURE_TOR:
        return &amp;#34;TOR&amp;#34; 
    elif features in FEATURE_P2P:
        return &amp;#34;P2P&amp;#34;
    elif features in FEATURE_PLUS:
        return &amp;#34;Plus&amp;#34;
    else:
        return &amp;#34;P2P&amp;#34;  # Default to P2P for any unrecognized feature

def get_wanted_features():
    &amp;#34;&amp;#34;&amp;#34;Get the wanted feature type based on config priorities&amp;#34;&amp;#34;&amp;#34;
    feature_type = config.get(&amp;#39;Features&amp;#39;, &amp;#39;feature_type&amp;#39;, fallback=&amp;#39;p2p&amp;#39;).lower()
    
    if feature_type == &amp;#39;secure_core&amp;#39;:
        return FEATURE_SECURE_CORE
    elif feature_type == &amp;#39;tor&amp;#39;:
        return FEATURE_TOR
    elif feature_type == &amp;#39;plus&amp;#39;:
        return FEATURE_PLUS
    else:  # Default to P2P
        return FEATURE_P2P

# Function to fetch the list of servers from the ProtonVPN API
def get_servers(full_list=False):
    try:
        url = &amp;#39;https://api.protonmail.ch/vpn/logicals&amp;#39;
        response = requests.get(url)
        response.raise_for_status()
        data = response.json()
        logging.info(&amp;#34;API response data received.&amp;#34;)
        return data[&amp;#39;LogicalServers&amp;#39;]
    except requests.RequestException as e:
        logging.error(f&amp;#39;Error fetching server list: {e}&amp;#39;)
        sys.exit(1)
    except ValueError as e:
        logging.error(f&amp;#39;Error parsing JSON: {e}&amp;#39;)
        sys.exit(1)

# Function to filter P2P servers and servers in preferred countries
def filter_servers(servers):
    filtered_servers = []
    
    # Get feature preferences from config (nu maar één type)
    wanted_features = get_wanted_features()
    
    logging.info(f&amp;#34;Filtering servers with preferences - Countries: {PREFERRED_COUNTRIES or &amp;#39;Any&amp;#39;}, &amp;#34;
                f&amp;#34;Cities: {PREFERRED_CITIES or &amp;#39;Any&amp;#39;}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    
    for server in servers:
        if not isinstance(server, dict):
            logging.error(f&amp;#34;Unexpected type for server: {type(server)} - Value: {server}&amp;#34;)
            continue
            
        features = server.get(&amp;#39;Features&amp;#39;, 0)
        country = server[&amp;#39;ExitCountry&amp;#39;]
        city = server.get(&amp;#39;City&amp;#39;, &amp;#39;&amp;#39;)

        # Check if server features match our wanted features
        if features in wanted_features:
            # Check country and city preferences
            if country in PREFERRED_COUNTRIES:
                if not PREFERRED_CITIES or city in PREFERRED_CITIES:
                    filtered_servers.append(server)
                    logging.info(f&amp;#34;Server matched - Country: {country}, City: {city}, &amp;#34;
                              f&amp;#34;Features: {get_feature_description(features)}&amp;#34;)
            
    if not filtered_servers:
        logging.warning(f&amp;#34;No servers found matching filters. Countries: {PREFERRED_COUNTRIES}, &amp;#34;
                      f&amp;#34;Cities: {PREFERRED_CITIES}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    else:
        logging.info(f&amp;#34;Found {len(filtered_servers)} matching servers&amp;#34;)
        
    return filtered_servers

# Function to measure latency by pinging the server (optional)
def get_latency(hostname):
    if not PING_ENABLED:
        logging.info(f&amp;#34;Ping disabled, skipping latency check for {hostname}.&amp;#34;)
        return 0  # Return 0 latency if ping is disabled

    latencies = []
    for i in range(PING_COUNT):
        try:
            # Add the interface option if it&amp;#39;s specified
            ping_command = [&amp;#39;ping&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;1&amp;#39;, &amp;#39;-W&amp;#39;, &amp;#39;1&amp;#39;, hostname]
            if PING_INTERFACE:
                ping_command.insert(1, &amp;#39;-I&amp;#39;)
                ping_command.insert(2, PING_INTERFACE)

            result = subprocess.run(ping_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if result.returncode == 0:
                output = result.stdout.decode()
                latency_line = [line for line in output.split(&amp;#39;\n&amp;#39;) if &amp;#39;time=&amp;#39; in line][0]
                latency = float(latency_line.split(&amp;#39;time=&amp;#39;)[1].split(&amp;#39; ms&amp;#39;)[0])
                latencies.append(latency)
            else:
                latencies.append(float(&amp;#39;inf&amp;#39;))
        except Exception as e:
            latencies.append(float(&amp;#39;inf&amp;#39;))
            logging.error(f&amp;#39;Ping error for {hostname}: {e}&amp;#39;)

    # Return the lowest (best) latency value
    best_latency = min(latencies)
    
    # Handle inf latency
    if math.isinf(best_latency):
        logging.warning(f&amp;#34;Latency for {hostname} is inf. Assigning high latency value.&amp;#34;)
        best_latency = 1000  # Assign a high latency value

    return best_latency

# Function to select the best server based on a weighted combination of latency and load
def select_best_server(servers):
    best_server = None
    best_score = float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        server_load = server.get(&amp;#39;Load&amp;#39;)
        
        # Handle inf load
        if server_load is None or math.isinf(server_load):
            logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} has inf load. Assigning high load value.&amp;#34;)
            server_load = 100  # Assign a high load value if it&amp;#39;s inf or None
        else:
            server_load = int(server_load)
        
        for srv in server[&amp;#39;Servers&amp;#39;]:
            exit_ip = srv.get(&amp;#39;ExitIP&amp;#39;)
            hostname = exit_ip

            # Get latency, only if ping is enabled
            latency = get_latency(hostname)

            # Combine latency and load using the configured weights
            score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)
            
            logging.info(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
            
            if score &amp;lt; best_score:
                best_score = score
                best_server = server
                best_server_instance = srv

    return best_server, best_server_instance, best_score

# Function to read the current server IP from the WireGuard configuration
def get_current_server_ip():
    try:
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()
        # Extract the current Endpoint IP
        match = re.search(r&amp;#39;Endpoint\s*=\s*([\d\.]&#43;):\d&#43;&amp;#39;, config_text)
        if match:
            logging.info(f&amp;#34;Current server IP found in WireGuard configuration: {match.group(1)}&amp;#34;)
            return match.group(1)
        else:
            logging.warning(&amp;#39;No current Endpoint IP found in WireGuard configuration.&amp;#39;)
            return None
    except Exception as e:
        logging.error(f&amp;#39;Error reading the WireGuard configuration: {e}&amp;#39;)
        return None

# Function to find the current server&amp;#39;s performance in the live server data
def get_current_server_performance(servers, current_ip):
    if current_ip is None:
        logging.warning(&amp;#39;Current server IP is None, skipping performance check.&amp;#39;)
        return None, None, float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        for srv in server[&amp;#39;Servers&amp;#39;]:
            if srv[&amp;#39;ExitIP&amp;#39;] == current_ip:
                server_load = server.get(&amp;#39;Load&amp;#39;)
                if server_load is None or math.isinf(server_load):
                    logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) has inf load. Assigning high load value.&amp;#34;)
                    server_load = 100  # Assign a high load value
                else:
                    server_load = int(server_load)

                latency = get_latency(current_ip) if PING_ENABLED else 0
                score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)

                logging.info(f&amp;#34;Current server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
                return server, srv, score
    logging.warning(f&amp;#34;Current server with IP {current_ip} not found in live data.&amp;#34;)
    return None, None, float(&amp;#39;inf&amp;#39;)

# Function to find the current server in the full ProtonVPN list if not found in filtered data
def find_current_server_full_list(current_ip):
    servers = get_servers(full_list=True)
    return get_current_server_performance(servers, current_ip)

# Function to update the WireGuard configuration file
def update_wg_config(server, server_instance, best_score):
    try:
        endpoint_ip = server_instance[&amp;#39;EntryIP&amp;#39;]
        endpoint_port = 51820  # Default WireGuard port
        endpoint = f&amp;#34;{endpoint_ip}:{endpoint_port}&amp;#34;
        public_key = server_instance[&amp;#39;X25519PublicKey&amp;#39;]
        server_name = server[&amp;#39;Name&amp;#39;]

        # Read the current WireGuard configuration
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()

        # Strip comments that start with # under [Peer] before PublicKey
        config_text = re.sub(r&amp;#39;#.*\n&amp;#39;, &amp;#39;&amp;#39;, config_text, flags=re.M)

        # Update the Endpoint in the configuration
        config_text = re.sub(r&amp;#39;Endpoint\s*=.*&amp;#39;, f&amp;#39;Endpoint = {endpoint}&amp;#39;, config_text)

        # Update the PublicKey in the configuration
        config_text = re.sub(r&amp;#39;PublicKey\s*=.*&amp;#39;, f&amp;#39;PublicKey = {public_key}&amp;#39;, config_text)

        # Add the new comment indicating the chosen server and score
        config_text = re.sub(r&amp;#39;\[Peer\]\n&amp;#39;, f&amp;#39;[Peer]\n# {server_name} - Auto-generated by ProtonVPN Auto Connect - Score: {best_score:.2f}\n&amp;#39;, config_text)

        # Write the updated configuration back to the file
        with open(WG_CONFIG_PATH, &amp;#39;w&amp;#39;) as f:
            f.write(config_text)
        os.chmod(WG_CONFIG_PATH, 0o600)

        logging.info(f&amp;#34;WireGuard configuration updated with server: {server_name} ({server[&amp;#39;ExitCountry&amp;#39;]}), Endpoint: {endpoint}, PublicKey: {public_key}, Score: {best_score:.2f}&amp;#34;)
    except Exception as e:
        logging.error(f&amp;#39;Error updating the WireGuard configuration: {e}&amp;#39;)
        sys.exit(1)

# Function to restart the WireGuard interface
def is_resolvconf_available():
    &amp;#34;&amp;#34;&amp;#34;Check if resolvconf is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;resolvconf&amp;#34;) is not None

def is_wg_quick_available():
    &amp;#34;&amp;#34;&amp;#34;Check if wg-quick is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;wg-quick&amp;#34;) is not None

def restart_wg():
    interface_name = os.path.basename(WG_CONFIG_PATH).split(&amp;#39;.&amp;#39;)[0]
    
    # Check if wg-quick is available before proceeding
    if not is_wg_quick_available():
        logging.error(&amp;#34;wg-quick not found. Ensure WireGuard is installed correctly.&amp;#34;)
        sys.exit(1)

    try:
        # Check if resolvconf is available before proceeding
        if not is_resolvconf_available():
            logging.warning(&amp;#34;resolvconf not found, DNS settings may not be updated correctly.&amp;#34;)

        # Bring down the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;down&amp;#39;, interface_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        # Bring up the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;up&amp;#39;, interface_name], check=True)
        logging.info(f&amp;#39;WireGuard interface {interface_name} started.&amp;#39;)

    except subprocess.CalledProcessError as e:
        logging.error(f&amp;#39;Error starting the WireGuard interface: {e}&amp;#39;)
        sys.exit(1)

# Main function
def main():
    logging.info(&amp;#39;--- Starting server selection process ---&amp;#39;)
    
    # Fetch the list of servers
    servers = get_servers()
    if not isinstance(servers, list):
        logging.error(f&amp;#39;Unexpected type for servers: {type(servers)}&amp;#39;)
        sys.exit(1)

    # Filter the servers
    filtered_servers = filter_servers(servers)
    if not filtered_servers:
        logging.error(&amp;#39;No suitable servers available after filtering.&amp;#39;)
        sys.exit(1)

    # Get the current server&amp;#39;s IP address from the WireGuard configuration
    current_ip = get_current_server_ip()

    # Get the current server&amp;#39;s performance if it exists
    current_server, current_server_instance, current_score = get_current_server_performance(filtered_servers, current_ip)

    # If the current server is not found, check the full list of servers
    if current_server is None:
        logging.info(f&amp;#34;Current server not found in filtered list, checking full server list...&amp;#34;)
        current_server, current_server_instance, current_score = find_current_server_full_list(current_ip)

    # Select the best server based on weighted latency and load
    best_server, best_server_instance, best_score = select_best_server(filtered_servers)
    if not best_server:
        logging.error(&amp;#39;No suitable server found.&amp;#39;)
        sys.exit(1)

    if current_server:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) - Score: {current_score:.2f}&amp;#34;)
    
    logging.info(f&amp;#34;Selected best server {best_server[&amp;#39;Name&amp;#39;]} - Score: {best_score:.2f}&amp;#34;)
    
    if current_server and current_score &amp;lt;= best_score &#43; SCORE_THRESHOLD:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} is within threshold. No switch necessary.&amp;#34;)
    else:
        logging.info(f&amp;#34;Switching to server {best_server[&amp;#39;Name&amp;#39;]} due to better performance.&amp;#34;)
        update_wg_config(best_server, best_server_instance, best_score)
        restart_wg()

    logging.info(&amp;#39;--- ProtonVPN Auto Connect Script End ---&amp;#39;)

if __name__ == &amp;#39;__main__&amp;#39;:
    main()</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="testing-the-script">Het script testen</h3>
<pre class="line-numbers language-bash"><code class="language-bash">chmod &#43;x protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Zodra het script is opgeslagen, geef je het uitvoerrechten en draai je het.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo ./protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">cat /var/log/wireguard_updater.log
tail -f /var/log/wireguard_updater.log</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je kunt het logbestand tailen of catten om de output te controleren. Tail heeft de voorkeur, want dan kun je het blijven volgen terwijl je script draait. Open gewoon een tweede ssh-sessie met de tail om het in de gaten te houden.</p>
<pre class="line-numbers language-log"><code class="language-log">2024-09-25 22:19:10 [INFO] --- ProtonVPN Auto Connect Script Start ---
2024-09-25 22:19:10 [INFO] --- Starting server selection process ---
2024-09-25 22:19:11 [INFO] API response data received.
2024-09-25 22:19:11 [INFO] Current server IP found in WireGuard configuration: 79.135.105.20
2024-09-25 22:19:11 [WARNING] Current server with IP 79.135.105.20 not found in filtered data.
2024-09-25 22:19:11 [INFO] Current server not found in filtered list, checking full server list...
2024-09-25 22:19:12 [INFO] API response data received.
2024-09-25 22:19:13 [INFO] Current server BH#1 - Latency: 26.90 ms, Load: 10%, Score: 15.07
2024-09-25 22:19:14 [INFO] Server AO#1 - Latency: 27.10 ms, Load: 7%, Score: 13.03
2024-09-25 22:19:15 [INFO] Server AO#2 - Latency: 26.60 ms, Load: 14%, Score: 17.78
2024-09-25 22:19:16 [INFO] Server AO#3 - Latency: 27.10 ms, Load: 9%, Score: 14.43
2024-09-25 22:19:17 [INFO] Server AO#4 - Latency: 26.60 ms, Load: 16%, Score: 19.18
2024-09-25 22:19:17 [INFO] Current server BH#1 (IP: 79.135.105.20) - Score: 15.07
2024-09-25 22:19:17 [INFO] Selected best server AO#1 - Score: 13.03
2024-09-25 22:19:17 [INFO] Current server BH#1 is within threshold. No switch necessary.
2024-09-25 22:19:17 [INFO] --- ProtonVPN Auto Connect Script End ---</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">sudo cat /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Je kunt ook controleren of alles werkt door het bestand protonvpn.conf te bekijken of door je externe ip-adres na te gaan.</p>
<pre class="line-numbers language-ini"><code class="language-ini">[Peer]
# BH#3 - Auto-generated by ProtonVPN Auto Connect - Score: 7.74</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je ziet dan iets zoals hierboven. Deze comment wordt bijgewerkt zodra er verbinding wordt gemaakt met een andere VPN, en het ip-adres binnen de peer verandert dan uiteraard mee.</p>
<pre class="line-numbers language-bash"><code class="language-bash">curl ifconfig.me #this wil give the external ip in the cli.</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Als extra stap kun je ook je externe ip controleren. Die moet anders zijn dan het ip-adres dat je provider je geeft.</p>
<h3 id="running-this-script-every-hour">Dit script elk uur draaien</h3>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -e</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">0 * * * * cd /home/{user}/protonss &amp;amp;&amp;amp; /home/{user}/protonss/protonvpn_auto_connect.py &amp;gt;&amp;gt; /home/{user}/protonss/protonvpn_cron.log 2&amp;gt;&amp;amp;1</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Hier hebben we ingesteld dat het elk uur draait, maar je kunt het plannen zoals je wilt.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -l</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Sla de crontab op en controleer dat cron is geïnstalleerd.</p>
<h3 id="optional-block-specific-traffic-if-your-vpn-fails">Optioneel: specifiek verkeer blokkeren als je VPN uitvalt</h3>
<p>Als je er zeker van wilt zijn dat er geen verkeer je device verlaat als de VPN-verbinding uitvalt, kun je <code>iptables</code> gebruiken om verkeer te blokkeren wanneer de WireGuard-interface down is.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Controleer dat je iptables hebt geïnstalleerd, of installeer het met het bovenstaande commando.</p>
<pre class="line-numbers language-bash"><code class="language-bash">ifconfig -a</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">protonvpn: flags=209&amp;lt;UP,POINTOPOINT,RUNNING,NOARP&amp;gt;  mtu 1420
        inet 10.2.1.3  netmask 255.255.255.255  destination 10.2.1.3
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (UNSPEC)
        RX packets 612  bytes 101768 (99.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 765  bytes 101144 (98.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Bepaal eerst de naam van je netwerkinterface. Als je je bestand protonvpn.conf hebt genoemd, heet de interface protonvpn.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -A OUTPUT -p tcp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable
sudo iptables -A OUTPUT -p udp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je kunt elke regel aan je iptables toevoegen. De 2 regels hieronder zorgen ervoor dat poort 51413 wordt geweigerd zodra de netwerkinterface met de naam protonvpn niet beschikbaar is. Er is een regel voor UDP- en voor TCP-verkeer.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -L OUTPUT -v -n</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Nadat je alle benodigde regels hebt toegevoegd, kun je ze met dit commando controleren.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables-persistent</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>En maak de regels persistent, zodat je ze niet bij elke reboot opnieuw hoeft toe te voegen.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo netfilter-persistent save</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><h3 id="conclusion">Conclusie</h3>
<p>Met dit Python-script is je Raspberry Pi altijd verbonden met de snelste beschikbare ProtonVPN-server, zodat je de best mogelijke VPN-prestaties hebt zonder handmatig ingrijpen. Of je je Pi nu gebruikt voor netwerken, torrenten of algemene online beveiliging: deze automatisering zorgt voor optimale prestaties en betrouwbare privacybescherming.</p>
<h3 id="update20-12-2024">Update - 20-12-2024</h3>
<p>Preferred_cities toegevoegd als optie om de selectie voor de VS te verkleinen. Dat land bevat anders ruim 2000 servers om te controleren. De performance-check kan zo in elk geval wat korter worden :-).</p>
<p>En ik heb uitgevonden wat de Feature-sectie van elke server in de API betekent. Hieronder leg ik het uit. Ik heb mijn code daarnaar aangepast om er correct mee te werken. Let op: de configuratie die je eerder hebt gemaakt moet overeenkomen met het type server waarmee je verbindt. Als je configuratie niet is ingesteld om met een Secure Core te verbinden, selecteer dan ook geen Secure Core in dit script. Sluit aan bij de ProtonVPN-configuratie die je hebt aangemaakt.</p>
<p>Uitleg van de features:</p>
<p>Elke server heeft precies één feature-nummer:</p>
<ul>
<li>1, 17 = Secure Core-servers</li>
<li>2 = TOR-servers</li>
<li>0, 8, 16, 24 = Plus-only servers</li>
<li>4, 12, 20, 28 = P2P-servers (dat lijken ook Plus-servers te zijn)</li>
</ul>
<pre class="line-numbers language-json"><code class="language-json">        {
            &amp;#34;Name&amp;#34;: &amp;#34;US-AZ#105&amp;#34;,
            &amp;#34;EntryCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;ExitCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;Domain&amp;#34;: &amp;#34;node-us-260.protonvpn.net&amp;#34;,
            &amp;#34;Tier&amp;#34;: 2,
            &amp;#34;Features&amp;#34;: 12,</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Na wat meer onderzoek naar vragen over de gratis servers van ProtonVPN blijkt dat ProtonVPN de publieke API-toegang tot hun free server-endpoints heeft verwijderd. Het /vpn/servers-endpoint dat eerder informatie over gratis servers had moeten leveren, geeft nu een 404-error:</p>
<pre class="line-numbers language-json"><code class="language-json">{    &amp;#34;Code&amp;#34;: 404,    &amp;#34;Error&amp;#34;: &amp;#34;Path not found&amp;#34;,    &amp;#34;Details&amp;#34;: {}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Op dit moment is alleen het Plus-server-endpoint via de publieke</p>
<p>API bereikbaar: url = &lsquo;<a href="https://api.protonmail.ch/vpn/logicals" data-external>https://api.protonmail.ch/vpn/logicals</a>&rsquo;</p>
<p>Dit is waarschijnlijk een bewuste keuze van ProtonVPN. Helaas kan dit script daardoor de gratis servers niet ondersteunen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Azure &#43; MySQL, MariaDB, PostgreSQL: prestaties vergeleken</title>
      <link>https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/</link>
      <guid isPermaLink="false">6605e7eafc993e0001aef0ea</guid>
      <pubDate>Fri, 29 Mar 2024 00:04:45 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Een kijkje in de databaseprestaties van Azure, tijdens een reis langs MySQL, MariaDB en PostgreSQL. Ontdek welke database de race aanvoert.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h3 id="introduction">Inleiding</h3>
<p>In deze post wil ik databaseperformance binnen Azure belichten, met een directe vergelijking tussen MySQL, MariaDB en PostgreSQL. Er is veel literatuur over het onderwerp, maar praktische tests die de performance in de praktijk meten, zijn schaars.</p>
<p>Veel discussies draaien om de verschillen tussen deze databases in plaats van om hun daadwerkelijke performance. Toen ik dit onderzoek begon, stonden niet de verschillen voorop, maar de zoektocht naar een performante database die een goed Azure-alternatief kon zijn voor onze MariaDB-monolith. Aanvankelijk dachten we dat de overstap van MariaDB naar MySQL een simpele vervanging was, met nauwelijks impact op de performance. Daar hadden we het mis!</p>
<blockquote class="pull-quote"><p>&ldquo;There are many opinions about performance, But we needed facts&rdquo;</p>
</blockquote>
<p>We zagen een enorme performance-regressie bij elke query. Performance is cruciaal voor ons, en het was verwarrend om te zien dat MySQL zo ver achterbleef bij MariaDB. Toen we dit ontdekten, hebben we zelf een MariaDB-VM in Azure opgezet. Die was weer wel performant, maar bracht veel extra onderhoud en beheer met zich mee: uptime garanderen, hot standby, backups.</p>
<p>Daarom ging ik PostgreSQL onderzoeken, in de hoop op betere performance dan MySQL bood. Met deze introductie: de tests die we hebben gedaan, met een complexe query en een set kleine queries, om de performanceverschillen tussen de drie databases en hun implementaties binnen Azure te meten. We hebben ook Docker-tests toegevoegd om te kunnen vaststellen of de performanceverschillen aan de databases liggen of dat Azure de oorzaak is.</p>
<h2 id="testing-our-complex-query">Onze complexe query testen</h2>
<p>Om de databases te beoordelen draaiden we een complexe query die data uit meerdere tabellen aggregeerde, een realistische workload voor ons systeem. Om vertrouwelijkheidsredenen hebben we de tabel- en kolomnamen in deze beschrijving aangepast. Deze query moest laten zien hoe elke database omgaat met data-aggregatie en complexe joins.</p>
<pre class="line-numbers language-sql"><code class="language-sql">WITH AggregatedSkateData AS (
SELECT
    e.id AS eventID,
    e.theme_cid AS themeCID,
    s.id AS skateID,
    p.firstName || &amp;#39; &amp;#39; || p.lastName AS participantName,
    l.label AS locationLabel,
    s.priceOffered,
    s.inspectionCompleted,
    s.registrationDate,
    s.eventStartDate,
    org.name AS organizerName,
    &amp;#39;https://skateeventimages.net/&amp;#39; || im.path AS organizerImageURL,
    s.registrationCode,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 0 OR pa_last.id IS NULL THEN sp.id END) AS partsPending,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 1 THEN sp.id END) AS partsRequested,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 2 THEN sp.id END) AS partsConfirmed,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 8 THEN sp.id END) AS partsDenied,
    MAX(CASE WHEN ti_unscanned.id IS NOT NULL THEN 1 ELSE 0 END) AS hasUnscannedTickets,
    s.isPublic
FROM
    skateEvents s
    INNER JOIN events e ON s.eventID = e.id
    INNER JOIN participants p ON p.id = e.participantID
    LEFT JOIN organizers org ON org.id = e.organizerID
    LEFT JOIN images im ON im.id = org.profileImageID
    LEFT JOIN locations l ON l.id = s.locationID
    LEFT JOIN skateParts sp ON sp.eventID = s.id
    LEFT JOIN partApprovals pa ON pa.id = sp.partApprovalID
    LEFT JOIN ticketsUnscanned ti_unscanned ON ti_unscanned.id = sp.ticketID AND ti_unscanned.scannedAt IS NULL
    LEFT JOIN partApprovalActions pa_last ON pa_last.id = (
        SELECT pa2.id
        FROM partApprovalActions pa2
        WHERE pa2.partID = pa.id
        ORDER BY pa2.id DESC
        LIMIT 1
    )
WHERE
    s.registrationDate IS NOT NULL
    AND e.cancelledAt IS NULL
    AND e.deletedAt IS NULL
    AND s.isArchived = false
    AND s.deletedAt IS NULL
GROUP BY
    e.id, e.theme_cid, s.id, p.id, p.firstName, p.lastName, org.id, im.id, l.id, l.label, s.priceOffered, s.inspectionCompleted, s.registrationDate, s.eventStartDate, org.name, im.path, s.registrationCode, s.isPublic
)
SELECT * FROM AggregatedSkateData
WHERE
    TO_CHAR(s.registrationDate, &amp;#39;YYYY-MM&amp;#39;) = &amp;#39;2024-03&amp;#39;
    OR partsDenied &#43; partsRequested &#43; partsPending &amp;gt; 0
ORDER BY s.registrationDate DESC</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="azure-complicated-query">Azure, complexe query</h3>
<p>We hebben deze tests binnen dezelfde Azure-zone gedraaid voor containers en databases, om interzone-latency te vermijden. Hieronder de specificaties en resultaten, voor de transparantie. Opvallend: bij MariaDB en MySQL hadden de performancespecificaties nauwelijks invloed op de resultaten, mits we de eerste (burstable) tier vermeden. PostgreSQL was de uitzondering, met duidelijke verbeteringen bij de runtime van grote queries.</p>
<p>Met HeidiSQL via de Azure VPN hebben we onze beste resultaten van vijf runs bepaald.</p>
<p><strong>MySQL Flexible, Zone 2 (Standard_E4ds_v5, 450 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 13,875 sec</p>
<p><strong>MariaDB VM, Zone 2 (Standard B4s v2 (4 vcpus, 16 GiB memory))</strong><br>
Found rows: 130<br>
Duration for 1 query: 10,625 sec<br>
23,4% beter dan MySQL</p>
<p><strong>Postgres Flexible, Zone 3 (Standard_E4ds_v50 p10, 500 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,953 sec<br>
71,5% beter dan MySQL</p>
<h3 id="local-docker-complicated-query">Lokale Docker, complexe query</h3>
<p>De hardware waarop we lokaal hebben getest:</p>
<p>12th Gen Intel(R) Core(TM) i9-12900H, 2900 Mhz, 14 Core(s), 20 Logical Processor(s)<br>
Installed Physical Memory (RAM) 32,0 GB</p>
<p><strong>MySQL 8.3 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 12,437 sec</p>
<p><strong>MariaDB 11.4 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 9,484 sec</p>
<p><strong>Postgres 16.2 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,969 sec</p>
<p>Daarmee zijn de tests met de complexe query afgerond. Geweldig om te zien dat Postgres hier zo goed presteert! Voor onze overstap naar Azure draaiden we op MariaDB. Het was behoorlijk vervelend om zo&rsquo;n slechte performance in MySQL te zien zodra we met de nieuwe setup gingen testen.</p>
<h2 id="testing-our-set-small-querys">Onze set kleine queries testen</h2>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_c11062909b9cd6ee.webp" alt="" width="560" height="171" data-zoom-src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_44990348cc6259d6.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>De volgende test vuurt meer dan 20.000 transacties per seconde af op elke database, waarmee we een feed in JSON bouwen die een bepaalde tijd nodig heeft om af te ronden. Met veel queries belasten we de database beter, wat ons een gemiddelde runtime per database geeft.
Er zit ook wat PHP-logica in, want dit is een PHP-script dat we draaien om de feed te genereren. Maar dat draait altijd op dezelfde hardware en hetzelfde docker-image, dus het verschil in runtime komt van de database.
Hiervoor hebben we onze containers naar dezelfde zone verplaatst als waar de database draait, om ook mogelijke problemen met zone-latency uit te sluiten.</p>
<p>We draaien dit script binnen ons Laravel-framework. Onze PHP draait tegen <a href="https://openswoole.com/" data-external>Swoole</a> voor de best mogelijke performance binnen PHP 8.</p>
<h3 id="azure-small-querys">Azure, kleine queries</h3>
<p>In deze test komen ook aspecten van containerperformance kijken.
We draaien onze containers en database weer in dezelfde zone in Azure (West-Europa). Binnen Azure draaien we met een stuk minder performance dan we lokaal in onze docker-omgeving tot onze beschikking hebben.</p>
<p>Daarom kunnen we deze resultaten alleen gebruiken om de verschillen binnen docker en binnen Azure zelf te vergelijken, niet tussen die twee. Die met elkaar vergelijken zegt niets door dit hardwareverschil. We willen alleen het verschil in runtime tussen de databases zien, en Docker is de baseline om te checken of de verschillen overeenkomen met wat we in Azure zien.</p>
<p>Onze node size in Azure is een standard_b2s, 2 vCPU&rsquo;s met 4 GiB RAM.</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2</strong><br>
24,59s runtime (spot nodes)</p>
<p><strong>API, Zone 2 -&gt; MySQL Flexible, Zone 2</strong><br>
27,87s runtime (spot nodes)</p>
<p><strong>API, Zone 3 -&gt; Postgres Flexible, Zone 3</strong><br>
24,92s runtime (spot nodes)</p>
<h3 id="local-docker-small-querys">Lokale Docker, kleine queries</h3>
<p>Voor de kleine query-tests binnen lokale Docker gebruiken we een systeem met een 12th Gen Intel(R) Core(TM) i9-12900H CPU, draaiend op 2900 MHz met 14 cores en 20 logical processors, aangevuld met 32 GB RAM.</p>
<p>Deze Docker images zijn zo geconfigureerd dat ze memory-bound zijn, wat betekent dat de prestaties vooral afhankelijk waren van het beschikbare geheugen en niet beperkt werden door de CPU. De CPU-instellingen stonden op onbeperkte prestaties, zodat Docker zoveel CPU-bronnen kon gebruiken als nodig.</p>
<p>Voor elke testrun tegen een database herstartte ik de Docker container om de verbinding te verversen. Na de herstart hield ik een pauze van 30 seconden aan om er zeker van te zijn dat de container volledig gestabiliseerd en idle was. Die pauze is belangrijk voor consistentere testomstandigheden. Zodra de container klaar was, startte ik het feed generator script om de feed op te bouwen, om zo te beoordelen hoe goed onze verschillende database-setups kleine queries aankunnen onder deze gecontroleerde omstandigheden.</p>
<p><strong>API -&gt; MariaDB 11.4 Docker</strong><br>
22.88s runtime</p>
<p><strong>API -&gt; MySQL 8.3 Docker</strong><br>
23.74s runtime</p>
<p><strong>API -&gt; Postgres 16.2 Docker</strong><br>
20.77s runtime</p>
<h2 id="mariadb-query-cache">MariaDB query cache</h2>
<p>In ons eerdere gebruik van MariaDB binnen onze monolithische setup maakten we veelvuldig gebruik van de query cache. Deze functionaliteit speelde een cruciale rol bij het leveren van prestaties voor herhaalde queries. Die efficiëntie houdt stand totdat de cache herbouwd moet worden, meestal getriggerd door updates of inserts op de tabellen van de gecachte query. We doen een tweede run op onze complexe query met de query cache ingeschakeld.</p>
<p><strong>Local MariaDB Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 0,000 sec</p>
<p>De prestatiewinst van de query cache is echter niet overal even groot. Bij bijvoorbeeld gemiddeld 20.000 transacties per seconde met kleine queries zakte de runtime enigszins in. Deze variatie laat zien dat de query cache niet altijd hetzelfde niveau van optimalisatie oplevert, zeker onder hoge belasting. Maar daar later meer over.</p>
<p><strong>API -&gt; Local MariaDB Docker (query cache enabled)</strong><br>
17.30s runtime</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2 (query cache enabled)</strong><br>
22.72s runtime (spot nodes)</p>
<p>De query cache in MariaDB werkt door de result set van SELECT queries op te slaan, samen met de querytekst. Wanneer een identieke query opnieuw wordt uitgevoerd (met dezelfde tekst en onder dezelfde database-userrechten), kan MariaDB de query-uitvoerfase overslaan en het resultaat direct uit de cache halen. Dit kan de uitvoertijd van vaak gedraaide queries die geen data wijzigen drastisch verlagen.</p>
<p>Wees voorzichtig met het gebruik van de query cache van MariaDB. Ondanks de voordelen is de query cache deprecated en standaard uitgezet in nieuwere versies van MariaDB 10.1.7 (en in MySQL vanaf versie 5.7.20), vanwege zorgen over de impact op prestaties in omgevingen met veel gelijktijdige transacties. Voor specifieke scenario&rsquo;s, met name met een overwegend read-workload, kan de query cache nog steeds aanzienlijk betere prestaties opleveren. In MySQL 8 en hoger is hij volledig verwijderd.</p>
<h2 id="observation-on-azures-flexible-servers">Observatie over de Flexible Servers van Azure</h2>
<p>Deze tests laten zien dat de prestaties van databases op de flexible servers van Azure aanzienlijk zijn verbeterd en dicht bij de prestaties van lokale Docker-omgevingen liggen. Dat is een flinke verbetering ten opzichte van de eerdere Azure database single servers, die doorgaans minder prestaties leverden door een vorm van overhead die voortkwam uit de manier waarop ze in de Azure-cloud draaiden.</p>
<h2 id="azure-latency-anomaly">Azure latency-anomalie</h2>
<p>Als bonus hebben we nog wat extra tests gedaan om de interzone-latency te meten bij het verbinden van Zone 1 of 2 met een PostgreSQL-database in Zone 3 binnen Azure.</p>
<p><strong>API, Zone 2 -&gt; Azure PostgreSQL Flexible, Zone 3</strong><br>
De runtime was 44.23 seconden.</p>
<p>Interessant: de runtime verdubbelde opnieuw, een fenomeen dat we uitsluitend zien bij communicatie tussen Zone 1-2 en Zone 3, of andersom. Dit patroon suggereert dat elke container die een databaseverbinding nodig heeft onder deze omstandigheden aanzienlijke latency ondervindt, terwijl de latency vrijwel verwaarloosbaar is bij interactie tussen Zone 1 en Zone 2.</p>
<p>Voor wie het gemist heeft: ik heb eerder een latency-anomalie in Zone 3 besproken. De gangbare wijsheid is dat je de databaseverbinding binnen dezelfde zone houdt om latency te minimaliseren. Onze tests laten echter zien dat de latency-impact minimaal is tussen Zone 1 en 2, maar aanzienlijk wordt zodra je van Zone 1 of 2 naar Zone 3 gaat.</p>
<p>Deze tests bevestigen opnieuw dat het latency-probleem niet exclusief is voor MySQL Flexible services, maar ook PostgreSQL Flexible services binnen Azure raakt, wat wijst op een breder patroon in het database-aanbod van Azure.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/azure-western-europe-zone-3-latency-issue-exploration/"><span class="bookmark__body"><span class="bookmark__title">Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</span><span class="bookmark__description">Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-thumb_hu_27e6839ff9d3a962.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="conclusion">Conclusie</h2>
<p>Vanuit prestatieoogpunt is het duidelijk dat de keuze voor de juiste database cruciaal is en moet aansluiten bij de specifieke snelheids- en efficiëntiebehoeften van je app. De resultaten laten zien dat MySQL vaak trager is. MariaDB presteert goed en heeft een handige query cache, maar Azure ondersteunt MariaDB niet meer. Daarmee is PostgreSQL de meest logische keuze voor prestatiegerichte gebruikers in Azure, want het blinkt uit in het verwerken van zowel complexe queries als grote aantallen simpele queries, zoals de tests hierboven laten zien.</p>
<p>Hopelijk helpt deze analyse bij het kiezen van de juiste databasetechnologie en onderstreept ze hoe belangrijk zorgvuldige planning is voor het verbeteren van je app-prestaties. Voor wie Azure gebruikt en topprestaties zoekt, is PostgreSQL in mijn testresultaten de beste optie.</p>
<p>Er zijn genoeg andere redenen om je database te kiezen. Maar in deze post wilde ik me alleen richten op performance. Dat was onze grootste zorg, want we zijn bezig met de overstap van een monolithische server naar Azure. Bij een database-switch komt nog meer kijken. Het voordeel hier is dat alle drie de databases zich redelijk makkelijk laten integreren, welke je ook kiest.</p>
<p>Postgres is hier het best compatibel dankzij zijn strikte routines. De gecompliceerde query hierboven is geschreven voor Postgres en draait in MariaDB zonder één wijziging. Voor MySQL hoef je alleen de TO_CHAR in de laatste regel te vervangen door DATE_FORMAT.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Latency-anomalie: een poging om Azure-performanceproblemen in West-Europa te ontrafelen</title>
      <link>https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/</link>
      <guid isPermaLink="false">65e6f9da5ce24800015cc7ae</guid>
      <pubDate>Tue, 05 Mar 2024 12:16:52 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Een groot Azure-performanceprobleem: de onverwachte latency-anomalie in Zone 3 van de regio West-Europa zet clouddeploymentstrategieën onder druk]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="introduction">Introductie</h2>
<p>We zijn begonnen aan een overstap van een traditionele VM-setup bij TransIP naar de schaalbare cloudinfrastructuur van Microsoft Azure. Die reis draait net zo goed om ontdekking als om prestaties. Met de expertise van <a href="https://www.pionative.com/" data-external>Pionative</a> als leidraad verhuizen we onze omgeving niet alleen; we proberen haar in Azure opnieuw op te bouwen, waarbij we elke stap zorgvuldig testen en analyseren. Ons doel? Optimale prestaties in deze nieuwe architectuur, en eventuele bottlenecks of verborgen problemen in onze nieuwe cloudarchitectuur zo vroeg mogelijk op het spoor komen.</p>
<h2 id="azure-regions-and-zones">Azure-regions en zones</h2>
<p>De infrastructuur van Azure beslaat de hele wereld, georganiseerd in regions voor gelokaliseerde diensten, en verder onderverdeeld in zones voor hoge beschikbaarheid. <strong>Regions</strong> zijn specifieke geografische gebieden waar Azure-diensten draaien, terwijl <strong>zones</strong> fysieke locaties binnen die regions zijn, elk met eigen voorzieningen om storingen te isoleren.</p>
<h2 id="the-western-europe-region">De region Western Europe</h2>
<p>De region Western Europe in Azure is opgebouwd uit drie aparte availability zones. Deze zones bieden een extra laag redundantie en fouttolerantie door datacenters binnen de region fysiek van elkaar te scheiden. Deze opzet moet garanderen dat zelfs bij een volledige uitval van één zone de diensten in de andere zones gewoon doordraaien — een stevige disaster recovery-oplossing voor bedrijven die in Europa actief zijn. Waar Zone 1, 2 en 3 geografisch precies liggen, is mij niet bekend.</p>
<h2 id="performance-anomaly-in-the-western-europe-region">Prestatie-anomalie in de region Western Europe</h2>
<p>Ons testprogramma bracht ons bij een anomalie die het middelpunt van onze Azure-verkenning in deze post vormt: een duidelijk latency-probleem in Zone 3 van de region Western Europe. Deze ontdekking kwam niet meteen; ze kwam uit dagen prestatietesten en scherpe observatie van patronen die afweken van wat we verwachtten. Dat komt vooral doordat we Azure nog maar net aan het leren zijn, en door het prestatieverschil met onze huidige setup.</p>
<p>Eerst hebben we onze eigen pods, code en prestatie-instellingen binnen de node pool uitgesloten. Database-prestatie-instellingen en dat soort zaken.</p>
<h2 id="methodology-and-findings">Methodiek en bevindingen</h2>
<p>Onze uiteindelijke aanpak was behoorlijk grondig om dit resultaat te verifiëren. We deployden twee node pools in elk van de drie zones van Microsoft Azure binnen de region Western Europe, om nodespecifieke verschillen uit te sluiten. Al die pools hadden exact dezelfde prestatie-instellingen.</p>
<p>Uit uitgebreide tests — waaronder een hele reeks database-aanroepen en de cron jobs die essentieel zijn voor de functionaliteit van onze applicatie — zagen we een consistent en verrassend patroon. Ongeacht de gerapporteerde capaciteit of drukte van de zone.</p>
<blockquote>
<p>Interacties met de database waren aanzienlijk trager zodra de database óf de verbindende pods in Zone 3 stonden.</p>
</blockquote>
<h2 id="performance-results">Prestatieresultaten</h2>
<p>Hieronder onze situatie met een cronjob die lokaal in Docker tegen onze huidige setup zo&rsquo;n 32 seconden nodig heeft. Die setup is niet vergelijkbaar met MS Azure, maar gaf ons wel een prestatiematrix en een doel. Het script (de cronjob) dat we draaiden vuurt zo&rsquo;n 32.000 queries af om een feed te bouwen van alle producten op onze website. Ja, dat is veel, maar daar gaat het hier niet om. We wilden het prestatieprobleem blootleggen. We weten dat dit script geoptimaliseerd kan worden om beter te presteren.</p>
<p><strong>MySQL-database in Zone 2</strong><br>
Zone 1 – pool 1: 33 seconden<br>
Zone 1 – pool 2: 32 seconden</p>
<p>Zone 2 –  pool 1:  32 seconden<br>
Zone 2 –  pool 2:  32 seconden</p>
<p>Zone 3 –  pool 1: 115 seconden<br>
Zone 3 –  pool 2: 117 seconden</p>
<p>En toen, als klap op de vuurpijl, hebben we de setup omgedraaid met een database in Zone 3. Zie hieronder de resultaten die we hoopten te zien om dit onderzoek af te sluiten.</p>
<p><strong>MySQL-database in Zone 3</strong><br>
Zone 1: 120 seconden<br>
Zone 2: 122 seconden<br>
Zone 3: 33 seconden</p>
<p>We zagen dezelfde consistentie terugkomen, elke keer opnieuw, ook toen we minder maar grotere database-aanroepen draaiden.</p>
<p><strong>MySQL-database in Zone 2</strong><br>
16 DB-queries, single threaded.</p>
<p>Zone 1: Totale tijd voor queries uit log: 9,57 ms<br>
Zone 2: Totale tijd voor queries uit log: 9 ms<br>
Zone 3: Totale tijd voor queries uit log: 49,96 ms</p>
<h2 id="website-performance-in-azure">Websiteprestaties in Azure</h2>
<p>Als we onze database in Zone 2 draaien met de API-pod in Zone 1, daalt de initiële laadtijd van onze website met minstens 400 milliseconden bij de eerste page load, vergeleken met de configuratie waarin de API-pod in Zone 3 staat.</p>
<p>Dat betekent dat de setup met de database in Zone 2 en de API-pod in Zone 1 een veel snellere initiële responstijd voor de website oplevert dan wanneer de API-pod in Zone 3 staat en de database in Zone 2.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_239c52cf6c075a2e.webp" alt="" width="560" height="129" data-zoom-src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_5c52b5671bc6fe37.webp" loading="lazy" decoding="async">
  </picture><figcaption>De performance van de eerste call op onze website, in een optimale setup in Azure.</figcaption></figure>

<p>Er zit hier een foutmarge, maar uit deze resultaten kunnen we concluderen dat je, als je je API geografisch over twee zones wilt verdelen, nooit consistent kunt presteren met Zone 3. Zones 1 en 2 zijn de juiste keuze.</p>
<p>We moeten eerlijk zijn: deze prestatie-anomalie doet zich alleen voor bij een pod met een databaseverbinding. Onze website (die geen databaseverbinding heeft) in Zone 3 draaien had vrijwel geen meetbaar effect, zolang de API-pod in Zone 1 draaide en de database in Zone 2 stond.</p>
<h2 id="diving-deeper-in-zone-3s-latency-issues">Dieper duiken in de latency-problemen van Zone 3</h2>
<p>Dit prestatieverschil in Zone 3 was voor ons verbijsterend. Volgens de best practices van Microsoft zou de fysieke nabijheid van applicatieservers bij hun databases binnen dezelfde regio de latency moeten minimaliseren. Onze bevindingen schetsten echter een ander beeld: de keuze van een zone binnen een regio kan flink invloed hebben op de performance van je applicatie.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_f23e2d3a977b47e3.webp" alt="" width="560" height="235" data-zoom-src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_dda4b94313ff73ba.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Ons onderzoek was behoorlijk grondig. We hebben mogelijke variabelen uitgesloten, zoals onze eigen codebase, PHP-configuratie, MySQL-performance en meer. De aanhoudende latencyproblemen, vooral bij single-threaded cron jobs in PHP, wezen uiteindelijk richting een structureel probleem in de netwerkarchitectuur van Zone 3, of ergens in de deployment.</p>
<h2 id="collaboration-and-learning">Samenwerking en leren</h2>
<p>Ons traject, gedragen door de samenwerking met <a href="https://www.pionative.com/" data-external>Pionative</a>, onderstreept hoe waardevol het is om samen te werken als je door de complexiteit van de cloud moet navigeren. Hoe dieper we in deze latency-anomalie doken, hoe duidelijker werd dat we niet alleen onze eigen deployment wilden optimaliseren, maar onze bevindingen ook wilden teruggeven aan de bredere Azure-community.</p>
<blockquote class="pull-quote"><p>Deze latency-impact is te groot om te negeren in onze productieomgeving.</p>
</blockquote>
<h2 id="implications-and-next-steps">Implicaties en vervolgstappen</h2>
<p>Deze anomalie in Zone 3 roept discussies op over de optimale zonekeuze en deploymentstrategieën binnen de regio West-Europa van Azure. Is dit de reden dat Zone 3 de dunstbevolkte zone is? Online is er hier zo te zien weinig over te vinden. Daarom heb ik deze post geschreven: het kostte ons letterlijk dagen om dit prestatieverschil op te sporen.</p>
<blockquote>
<p>Dit gaat invloed hebben op onze productieomgeving. We draaien onze DB in Zone 1 of 2, en de API moet in een van deze zones staan. Zone 3 kan gebruikt worden voor pods die met de API praten.</p>
</blockquote>
<p>Deze latency-anomalie in Zone 3 was voor ons meer dan een uitdaging. We zagen het als een kans om te leren, aan te passen en onze kennis te verbreden. Terwijl we blijven samenwerken, testen en onze aanpak verfijnen, groeit ons Azure-traject uit tot een zeer interessante technische verkenning die onze toolkit voor de toekomst verruimt.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hotfixes met Docker-artifacts in een Git Flow-strategie</title>
      <link>https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/</link>
      <guid isPermaLink="false">65d330e818945800013cc243</guid>
      <pubDate>Tue, 20 Feb 2024 09:53:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Leer hotfixes in Kubernetes beheersen met Docker. Ontdek hoe je kritieke fixes snel deployt in een CI/CD-pipeline met een strakke branching-strategie. Ideaal voor het oplossen van urgente problemen zonder de flow te verstoren. Duik erin.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Deze post is een vervolg op GIT deployment-strategieën binnen Kubernetes-omgevingen. Lees eerst deze post:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/"><span class="bookmark__body"><span class="bookmark__title">CI/CD pipeline with Docker and Git branching strategy</span><span class="bookmark__description">Explore advanced CI/CD with Docker &amp; Git. Master deploying Docker artifacts via Git branching. Dive into strategies for DEV, STAGING, LIVE environments. Perfect your pipeline now!</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-thumb_hu_4b0a9042ec0e16fc.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>In een CI/CD-pipeline die draait om Docker artifacts, met de focus op één enkele branch, vraagt een efficiënte hotfix-strategie om een kleine afwijking van de standaardaanpak. Zo zorgen we dat kritieke fixes snel gedeployd worden. Laten we onze aanpak voor hotfixes in deze Docker-gerichte omgeving uitwerken, op basis van de branch-architectuur uit de post hierboven.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Grafische weergave van onze branching- en deploymentstrategie</figcaption></figure>

<h2 id="identifying-the-need-for-a-hotfix">Wanneer is een hotfix nodig?</h2>
<p>Hotfixes zijn cruciaal voor de stabiliteit en betrouwbaarheid van elke software in productie. Ze zijn meestal nodig in situaties die directe actie vragen. Weten wanneer je een hotfix inzet, is essentieel om je CI/CD-pipeline goed te beheren, zeker als je werkt met Docker artifacts en een gestroomlijnde branch-strategie met één hoofdbranch. Dit zijn onze scenario&rsquo;s waarin een hotfix nodig kan zijn:</p>
<ol>
<li><strong>Beveiligingslekken:</strong> Een van de meest urgente redenen voor een hotfix is het ontdekken van een beveiligingslek in je applicatie. Of het nu gaat om blootgestelde data, risico op ongeautoriseerde toegang of een andere kwetsbaarheid die kwaadwillenden kunnen misbruiken: je moet snel handelen om het lek te dichten en je gebruikers en data te beschermen.</li>
<li><strong>Kritieke bugs die de gebruikerservaring raken:</strong> Als een serieuze bug door je testproces heen glipt en in productie belandt, kan dat de gebruikerservaring flink raken. Dat kan van alles zijn: een kapotte kernfunctionaliteit waardoor gebruikers de applicatie niet meer kunnen gebruiken, of ernstige performanceproblemen die leiden tot timeouts of crashes. Raakt het probleem een groot deel van je gebruikers? Dan is een hotfix op zijn plaats.</li>
<li><strong>Compliance-problemen:</strong> Sommige applicaties hebben te maken met wettelijke of regelgevende eisen. Als er een compliance-probleem wordt geconstateerd dat je juridische positie of de privacy van gebruikers kan raken, moet dat snel met een hotfix worden opgelost om boetes of verlies van vertrouwen te voorkomen.</li>
<li><strong>Risico op dataverlies:</strong> Situaties waarin gebruikersdata verloren kan raken of corrupt kan worden, vragen om directe actie. Of het nu een fout is in hoe data wordt opgeslagen, bijgewerkt of opgehaald: deploy dan een hotfix om verlies of corruptie van gebruikersdata te beperken.</li>
<li><strong>Falende betalingen of transacties:</strong> Voor applicaties die betalingen of andere transacties verwerken, zijn problemen die transacties blokkeren of tot financiële discrepanties leiden kritiek. Zulke problemen schaden niet alleen het vertrouwen van gebruikers, maar kunnen ook financiële gevolgen hebben voor zowel gebruikers als de dienstverlener. Een hotfix is dan noodzakelijk.</li>
</ol>
<h2 id="deploying-a-hotfix">Een hotfix deployen</h2>
<p>Als er een kritiek probleem in de productieomgeving wordt geconstateerd, is het doel om zo snel mogelijk een hotfix te ontwikkelen, testen en deployen. Omdat MAIN onze hoofdbranch voor development is en we Docker artifacts genereren voor deployment, hebben we een proces nodig dat snelle interventie mogelijk maakt zonder het lopende werk te verstoren.</p>
<ol>
<li><strong>Een hotfix-branch aanmaken:</strong> In plaats van te branchen vanaf je Production branch (LIVE bestaat in deze strategie immers niet meer in de Git-repository), maak je de hotfix-branch vanaf de commit die op dat moment in productie draait. Zo is de hotfix gebaseerd op exact de versie die in de productieomgeving draait. De specifieke commit vind je via tags of Docker image-labels die gekoppeld zijn aan de productie-deployment.</li>
<li><strong>De hotfix ontwikkelen en testen:</strong> De kritieke fix wordt ontwikkeld binnen deze hotfix-branch. De urgentie vraagt om een gerichte aanpak: pak het probleem aan met minimale wijzigingen om testen en deployen te versnellen. Ook onder tijdsdruk is het naleven van de Definition of Done (DoD) cruciaal voor de stabiliteit en betrouwbaarheid van de hotfix. Dat kan een compactere versie van de DoD zijn, met prioriteit voor de kritieke tests en checks die garanderen dat de fix geen nieuwe problemen introduceert.</li>
<li><strong>Een Docker artifact voor de hotfix maken:</strong> Zodra de hotfix aan de afgesproken DoD-criteria voldoet, bouwt een geautomatiseerd proces een Docker image van de hotfix-branch. Dit Docker artifact krijgt een passende tag, zodat duidelijk is waar het voor dient en hoe urgent het is.</li>
<li><strong>De hotfix deployen:</strong> Het nieuwe Docker artifact wordt direct in de productieomgeving gedeployd, waarbij de gebruikelijke route via DEV en STAGING naar PRODUCTION wordt overgeslagen. Deze deployment is alleen te rechtvaardigen door de kritieke aard van de fix en de tests die erop zijn uitgevoerd.</li>
<li><strong>Terugmergen en consistentie bewaken:</strong> Als de hotfix succesvol in productie draait, is het essentieel om de wijzigingen terug te mergen naar de MAIN branch en eventuele andere relevante branches of tags. Pas deze stap zorgt ervoor dat de fix wordt opgenomen in het lopende developmentwerk en toekomstige releases, en dat je geen regressie krijgt!!</li>
</ol>
<h2 id="ensuring-rapid-deployment-and-minimizing-disruption">Snel deployen en zo min mogelijk storing</h2>
<ul>
<li><strong>Geautomatiseerde processen:</strong> Automatisering speelt een sleutelrol in deze strategie, van het maken van het Docker artifact tot de deployment. Geautomatiseerde tests, builds en deployments zorgen dat het hotfix-proces zo snel en efficiënt mogelijk verloopt.</li>
<li><strong>Communicatie en monitoring:</strong> Snelle deployment van hotfixes vraagt om heldere communicatie binnen het team en nauwe monitoring van de productieomgeving na de deployment, om te verifiëren dat de fix werkt en geen nieuwe problemen introduceert.</li>
<li><strong>Documentatie en review:</strong> Het documenteren van het hotfix-proces en het doen van post-mortem-reviews helpt om de hotfix-strategie te verfijnen, zodat het team bij toekomstige problemen nog effectiever kan reageren.</li>
</ul>
<h2 id="conclusion">Conclusie</h2>
<p>In een CI/CD-pipeline met Docker en Git Flow als hoofdstrategie vraagt een hotfix om een strakke aanpak: gebruik de automatisering die er al is en focus op snelle, gerichte fixes. Door hotfix-branches te baseren op de commit die in productie staat, fixes te bouwen en testen volgens een goed gedefinieerde Definition of Done, en de Docker-artifacts direct naar productie te deployen, lost een team kritische problemen snel op zonder de stabiliteit en integriteit van de applicatie in gevaar te brengen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>CI/CD-pipeline met Docker en een Git branching-strategie</title>
      <link>https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/</link>
      <guid isPermaLink="false">65d3035a18945800013cc216</guid>
      <pubDate>Tue, 20 Feb 2024 09:50:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Een kijkje in CI/CD met Docker &amp; Git. Docker-artifacts deployen via Git branching onder de knie krijgen. Strategieën voor DEV-, STAGING- en PROD-omgevingen. Maak je pipeline beter!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In deze post ga ik in op het beheren van een CI/CD-pipeline met Docker, met de nadruk op een Git branching-strategie met een development- (DEV), staging- (STAGING) en production-branch (LIVE). Het doel is duidelijk maken welke rol elke branch speelt in het proces, en vooral hoe Docker-artifacts binnen dat stramien worden aangemaakt en beheerd.</p>
<h2 id="docker-deployment-using-git-flow-with-multiple-branches">Docker-deployments met Git Flow en meerdere branches</h2>
<p>De kern van onze branching-strategie bestond uit drie branches:</p>
<ol>
<li><strong>DEV</strong>: Hier gebeurt alle ontwikkeling. Elke nieuwe feature, bugfix of verbetering start in een feature branch, afgesplitst van DEV. Is het werk af, gereviewd en voldoet het aan de Definition of Done (DoD), dan wordt het gemerged naar DEV.</li>
<li><strong>STAGING</strong>: Deze branch spiegelt een pre-productieomgeving. Hier worden geïntegreerde features uit DEV als geheel getest en door stakeholders geaccepteerd, zodat we zeker weten dat alles samenwerkt.</li>
<li><strong>MASTER</strong>: De branch die de huidige staat van onze live-omgeving weerspiegelt. Alleen grondig geteste en door stakeholders goedgekeurde wijzigingen uit STAGING halen het tot Productie.</li>
</ol>
<figure class="bookmark"><a class="bookmark__link" href="https://dev.to/preethamsathyamurthy/git-branching-and-branching-strategy-4mci" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Git Branching and Branching Strategy</span><span class="bookmark__description">In this post, we will go over why branching is required, the difference between development, staging…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/01-icon_hu_4e78872e34382e92.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>DEV Community</span><span>Preetham</span></span></span></a></figure>

<p>Dit was onze variant op een Git Flow-strategie, die we tot nu toe gebruikten. Zodra je met artifacts gaat werken, verandert dat allemaal terug naar een normale Git Flow-structuur. We hebben nog steeds omgevingen die draaien (Dev, Staging en Prod). Het echte verschil is dat code nu één keer gecommit wordt, in plaats van gemerged te worden naar een branch die op Staging of in Productie draait.</p>
<p>In plaats daarvan hebben we nu één branch, die we Main noemen. Daar wordt na een geslaagde code merge met Docker images een runnable artifact aangemaakt. Alles wat we op Staging of in Productie deployen is een specifieke versie van een eerder aangemaakt artifact.</p>
<p>Het grootste verschil: STAGING en PROD zijn geen branches meer waar je code in merged. Het zijn omgevingen geworden die een specifieke versie van een Docker image draaien. Dat brengt eigen uitdagingen met zich mee, zeker bij een hotfix. Hotfixen krijgt een eigen post, die je na deze kunt lezen. Ik loop het nu eerst stap voor stap door, zonder de hotfix erbij. Het is al omvangrijk genoeg zo.</p>
<h2 id="what-is-an-artifact">Wat is een artifact?</h2>
<p>Een Docker-artifact, vaak door elkaar gehaald met een Docker image, is een lichtgewicht, zelfstandig uitvoerbaar package dat alles bevat wat nodig is om een stuk software te draaien: de code, een runtime, libraries, omgevingsvariabelen en configuratiebestanden. Simpel gezegd: een snapshot van je applicatie én de omgeving eromheen, samen verpakt, klaar om overal gedeployed en gedraaid te worden waar Docker is geïnstalleerd. Zo weet je dat alles in verschillende omgevingen consistent werkt.</p>
<h2 id="the-importance-of-dod-when-merging-code">Het belang van de DoD bij het mergen van code</h2>
<p>Een volledige en heldere Definition of Done (DoD) speelt in dit nieuwe proces een sleutelrol, zeker nu alle wijzigingen in één enkele branch gemerged worden die een Docker image moet opleveren die runnable en deployable is:</p>
<ul>
<li><strong>Kwaliteitsborging</strong>: De DoD zorgt dat alle wijzigingen — nieuwe features, verbeteringen en hotfixes — die terug in MAIN gemerged worden aan een consistent kwaliteitsniveau voldoen. Denk aan code quality checks, succesvol doorlopen geautomatiseerde tests en het volgen van security best practices.</li>
<li><strong>Regressie voorkomen</strong>: Door de DoD toe te passen op alle merges naar MAIN voorkomt het team regressies en zorgt het dat nieuwe wijzigingen bestaande functionaliteit niet breken. Cruciaal voor de integriteit en stabiliteit van de codebase.</li>
<li><strong>Deployments soepeler laten verlopen</strong>: Een duidelijke en volledige DoD maakt deployments makkelijker, omdat alleen volledig gecontroleerde, deploy-klare wijzigingen in MAIN gemerged worden. Zo verspil je geen resources aan het bouwen van Docker images met bugs of die helemaal niet werken, en dat levert efficiëntere en betrouwbaardere artifacts op.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Hier een grafische weergave van onze Git Flow-strategie en hoe die door de verschillende omgevingen loopt.</figcaption></figure>

<h2 id="creating-and-managing-artifacts">Artifacts aanmaken en beheren</h2>
<p>De eerste voorwaarde om dit proces te laten werken: zodra een afgesplitste branch terug in MAIN gemerged wordt, start dat een geautomatiseerd proces in onze CI/CD-pipeline. Dit gebeurt er:</p>
<ol>
<li><strong>Geautomatiseerde tests</strong>: Eerst draaien de geautomatiseerde tests om te checken of de nieuwe wijzigingen aan onze kwaliteitsnormen voldoen. Cruciaal voor een stabiele branch.</li>
<li><strong>Docker-artifact aanmaken</strong>: Zijn alle tests geslaagd, dan wordt er een Docker image (ons artifact) van de MAIN-branch gebouwd. MAIN is immers de enige branch die de laatste staat van alle afgeronde ontwikkeling vertegenwoordigt. Van daaruit genereren we artifacts die &ldquo;shippable&rdquo; producten kunnen worden.</li>
<li><strong>Waarom MAIN voor Docker-artifacts?</strong>: Door artifacts op dit moment aan te maken vangen we integratieproblemen vroeg op. Omdat elke feature die in MAIN gemerged wordt als release-klaar wordt beschouwd, heb je door hier een artifact te genereren altijd een geteste en potentieel deploybare versie van je product bij de hand.</li>
</ol>
<h2 id="transferring-from-branches-to-environments">Van branches naar omgevingen</h2>
<p>In traditionele Git-workflows staan branches als STAGING en PROD voor verschillende fasen in het ontwikkelproces, van testen tot productie. In een Docker-gecentreerde CI/CD-pipeline verschuift de focus: deze branches dienen niet langer voor code-integratie, maar als referentie voor deployment-omgevingen.</p>
<h2 id="deploying-to-staging">Deployen naar STAGING</h2>
<ul>
<li><strong>Docker-artifacts aanmaken</strong>: zodra de ontwikkeling klaar is en gemerged is naar de MAIN-branch, wordt er automatisch een Docker-artifact aangemaakt. Dat artifact is een snapshot van je applicatie op dat moment, met alle wijzigingen die gemerged zijn.</li>
<li><strong>Deployen in plaats van mergen</strong>: de rol van de STAGING-omgeving is om dit Docker-artifact te testen onder omstandigheden die sterk lijken op productie. In plaats van code van MAIN naar een STAGING-branch te mergen, deploy je het Docker-artifact naar een STAGING-omgeving. De code zelf beweegt dus niet tussen branches; het is de gecompileerde en gecontaineriseerde versie van je applicatie die getest wordt.</li>
<li><strong>Het doel van STAGING</strong>: de STAGING-omgeving is essentieel voor uitgebreide tests, waaronder performance- en security-assessments. Het is een laatste check om te verifiëren dat alles werkt zoals verwacht in een productie-achtige omgeving, zonder je echte gebruikers direct te raken.</li>
</ul>
<h2 id="going-to-production">Naar productie</h2>
<ul>
<li><strong>Goedkeuring van stakeholders</strong>: zodra je Docker-artifact grondig getest is in STAGING en goedgekeurd is door de stakeholders, is het klaar voor productie. Deze stap is cruciaal om te garanderen dat alle features, fixes en updates aan de nodige standaarden en verwachtingen voldoen.</li>
<li><strong>Deployen naar productie</strong>: de overgang van STAGING naar PROD betekent dat je het geteste Docker-artifact deployt naar je productieomgeving. Dit deploymentproces garandeert consistentie: het exacte artifact dat in STAGING getest is, wordt naar PROD gedeployd. Zo zijn er geen verschillen tussen geteste en gedeployde code.</li>
</ul>
<h2 id="key-takeaways">Belangrijkste punten</h2>
<ul>
<li><strong>Geen branch-merges meer voor deployment</strong>: in deze gestroomlijnde workflow worden STAGING- en PROD-branches niet meer gebruikt om code te mergen. In plaats daarvan verwijzen ze naar specifieke deployment-omgevingen waar Docker-artifacts voor tests en productie gedeployd worden.</li>
<li><strong>Focus op artifacts</strong>: de nadruk ligt op het aanmaken, testen en deployen van Docker-artifacts. Deze artifacts vertegenwoordigen stabiele versies van je applicatie, klaar voor STAGING en uiteindelijk voor PROD.</li>
<li><strong>Consistentie en betrouwbaarheid</strong>: door hetzelfde Docker-artifact van STAGING naar PROD te deployen, weet je zeker dat wat getest is exact is wat er uitgebracht wordt. Dat vergroot de betrouwbaarheid en voorspelbaarheid van je deployments.</li>
</ul>
<p>Deze aanpak vereenvoudigt het deploymentproces, verkleint het risico op fouten en zorgt ervoor dat elke release grondig getest wordt in een omgeving die productie zo nauw mogelijk benadert.</p>
<h2 id="are-multiple-branches-still-needed">Zijn meerdere branches nog nodig?</h2>
<p>In deze nieuwe workflow hebben de STAGING- en PROD-branches in de Git branching strategy geen actieve functie meer en zijn ze dus overbodig geworden. Ze hielpen om bij te houden welke versies van de code naar welke omgeving gedeployd waren. Dat probleem hebben we nu opgelost door een specifieke versie van een artifact te draaien.</p>
<h2 id="conclusion">Conclusie</h2>
<p>Door Docker op te nemen in onze Git branching strategy stroomlijnen we het hele traject van development tot deployment: elke merge naar MAIN triggert een Docker-image met de potentie om naar productie te gaan. De sleutel van deze strategie is het vroeg in de developmentcyclus aanmaken van Docker-artifacts, zodat continuous integration en delivery mogelijk blijven zonder in te leveren op kwaliteit en stabiliteit.</p>
<p>Door de veranderingen aan de Git-branches te begrijpen en het proces van het aanmaken en deployen van Docker-artifacts, kunnen we met vertrouwen omgaan met de complexiteit van CI/CD. Zo leveren we alle nieuwe features en fixes zo efficiënt en betrouwbaar mogelijk af bij de gebruikers.</p>
<h2 id="hotfixing">Hotfixes</h2>
<p>We hebben het nog niet gehad over hotfixes. Zoals ik al zei, verdient dit een eigen post. Je leest hier alles over hotfixes en de valkuilen die daarbij komen kijken.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/"><span class="bookmark__body"><span class="bookmark__title">Hotfixing with Docker Artifacts in a Single Branch Strategy</span><span class="bookmark__description">Master hotfixes in Kubernetes with Docker. Learn to rapidly deploy critical fixes in a CI/CD pipeline focusing on streamlined branching. Perfect for tackling urgent issues without disrupting flow. Dive in now.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-thumb_hu_31f1067c4e9c7ce6.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
    <item>
      <title>Problemen met het tonen van afbeeldingen vanaf onze Static server</title>
      <link>https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/</link>
      <guid isPermaLink="false">6551ee1b3a3fb5000134167b</guid>
      <pubDate>Sun, 12 Nov 2023 12:50:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[We zagen dat in de browsers die we gebruiken (Edge en Chrome) meldingen in de console stonden van afbeeldingen die niet laadden. ERR_HTTP2_PROTOCOL_ERROR]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="some-background-to-understand-our-issue-better">Wat je moet weten om ons probleem te snappen</h2>
<p>We werken aan een nieuwe website met veel nadruk op afbeeldingen. We hebben al <a href="http://www.333travel.nl/" data-external>www.333travel.nl</a>, dat als reisproduct natuurlijk al zwaar leunt op afbeeldingen. Onze nieuwe website laadt zelfs afbeeldingen met een nog hogere resolutie, omdat de nadruk op het luxe gevoel ligt. Deze site is gericht op de luxe reiziger: <a href="http://www.travelplatinum.nl/" data-external>www.travelplatinum.nl</a>. Het project dat onze afbeeldingen serveert, static.333travel.nl, draait al een paar jaar zonder gemelde problemen voor de 333travel-website.</p>
<h2 id="the-issue">Het probleem</h2>
<p>Het probleem werd gemeld door onze thuiswerkers. Zij merkten dat bij het werken vanuit huis veel afbeeldingen op TravelPlatinum soms niet laadden (en dat een refresh het meestal oploste). Ze gaven ook aan dat alles gewoon werkte vanuit kantoor. Dat was voor ons het startpunt van een zoektocht naar de boosdoener.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-8526b527-c281-496a-9251-1028b11d3560_hu_1bb3200cf0522d93.webp" alt="" width="560" height="382" data-zoom-src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-8526b527-c281-496a-9251-1028b11d3560_hu_220750cfd07c6574.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>We zagen in de browsers die we gebruiken (Edge en Chrome) meldingen in de console van de mislukte afbeeldingen: ERR_HTTP2_PROTOCOL_ERROR. Met die melding in ons achterhoofd begonnen we in de logs op de server te zoeken. We draaien Nginx als webserver.</p>
<p>We vonden snel een spoor dat bij elke afbeelding opdook die volgens ons niet goed geladen was. We konden het probleem zelf nog niet reproduceren.</p>
<p>0.0.0.0 - - [06/Nov/2023:15:45:02 +0100] &ldquo;GET /web-images/10/2023/653775730fc66/Jazan&mdash;Al-qahar-mountains-1200.webp HTTP/2.0&rdquo; 206 183064 &ldquo;<a href="https://www.travelplatinum.nl/" data-external>https://www.travelplatinum.nl/</a>&rdquo; &ldquo;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0&rdquo;</p>
<p>Let op de 206 hier. Dat betekent partial content.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206" data-external>https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206</a></p>
<p>Dat was vreemd, want onze webserver ondersteunde helemaal geen partial loading van afbeeldingen. We sturen nooit headers met iets als een range, terwijl dat nodig is om partial content te kunnen leveren.</p>
<h2 id="our-attempt-to-fix-the-issue">Onze poging om het probleem op te lossen</h2>
<p>Dus daar doken we als eerste in. Ik kwam erachter dat Nginx standaard een header meestuurt die Content-Type: bytes teruggeeft. Ik snap niet waarom ik zou willen dat onze image server met een ok-antwoord reageert op een 206-request. We hebben dit uitgezet met de volgende setting in ons server block.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
}</p>
<p>We vroegen de mensen die het probleem hadden om het te melden als het weer gebeurde. Helaas was dat snel het geval. De foutmelding in onze logging was wel veranderd. Edge en Chrome bleven dezelfde nutteloze ERR_HTTP2_PROTOCOL_ERROR in de console tab spuwen. In de timeline zagen we ook dat deze afbeeldingen op een bepaald punt stopten met laden.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-1cdc5aec-ad55-4bfd-8a7c-2593b57282be_hu_597e0975f3b81981.webp" alt="" width="560" height="247" data-zoom-src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-1cdc5aec-ad55-4bfd-8a7c-2593b57282be_hu_ccaba7f5f6ea432e.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>0.0.0.0 - - [08/Nov/2023:14:22:27 +0100] &ldquo;GET /web-images/06/2020/5ed7ad48d3d79/mexico-yucatan-chichen-itza-ik-kil-cenote.jpeg HTTP/2.0&rdquo; 200 0 &ldquo;<a href="https://vue-boss.333office.nl/" data-external>https://vue-boss.333office.nl/</a>&rdquo; &ldquo;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0&rdquo;</p>
<p>We krijgen nu een header met een lengte van 0 bytes en een response met http 200 (ok). Het wordt hier echt vreemd. De header geeft bij inspectie in de browser wél het juiste aantal bytes terug in de response header. Waarom zegt de log van Nginx dan dat hij met een size van 0 antwoordt? Hier werd het raar en we wisten eigenlijk niet wat we verder moesten doen. We moesten het probleem eerst zelf kunnen reproduceren voordat we echt konden troubleshooten.</p>
<p>Ik probeerde een fout te simuleren met ab (apache benchmark). Ik begon voorzichtig met 500 requests en 50 gelijktijdige requests. Het probleem werd bij mij nooit getriggerd. Daarna ging ik in kleine stapjes omhoog tot 10.000 requests met 700 gelijktijdige requests, zonder het gemelde probleem ook maar één keer te zien. De serverload leek alles ook prima aan te kunnen. Het ging om een afbeelding van zo&rsquo;n 500kb die ik daar opvroeg.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/image_hu_e4a7c89173cb9060.webp" alt="" width="560" height="339" data-zoom-src="https://www.allsubjectsmatter.nl/nl/sudden-issues-loading-images-from-our-static-project/images/image_hu_52a1f89bab47586c.webp" loading="lazy" decoding="async">
  </picture></figure>

<h2 id="the-facts">De feiten</h2>
<ol>
<li>Een traag netwerkprofiel instellen in de network tab bij het inspecteren simuleerde het probleem voor ons niet.</li>
<li>De fout werd nog steeds alleen gemeld door 2 gebruikers die vanuit huis werkten.</li>
<li>Op kantoor hadden ze geen enkel probleem.</li>
<li>De site 333travel.nl laden gaf helemaal geen probleem. Elke afbeelding werkte daar ook voor die 2 thuiswerkers.</li>
<li>Testen via een 5g-verbinding op allerlei plekken die we konden bedenken simuleerde het probleem voor mij geen enkele keer.</li>
<li>ab (apache benchmark) met 10.000 requests en 700 gelijktijdige requests triggerde het probleem nooit.</li>
</ol>
<p>Er was nog één verschil, en dat was het domein dat de afbeeldingen serveerde. 333travel kreeg een Cross origin header van same_site terug, wat klopt, want het domein is hetzelfde. Dus ik voegde een header toe aan de static.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
add_header &lsquo;Access-Control-Allow-Origin&rsquo; &lsquo;*&rsquo;;<br>
}</p>
<p>Zo krijgt elke bron die afbeeldingen van onze static.333travel.nl wil gebruiken hetzelfde antwoord. Dit loste niets op van het gemelde probleem.</p>
<p>We hadden nog steeds moeite om het probleem zelf te simuleren. We waren er vrij zeker van dat het alleen getriggerd werd op een of andere slechte verbinding bij een bezoek aan TravelPlatinum. Een bezoek aan 333travel, dat de afbeeldingen van dezelfde static server serveert, liet het probleem dat we op TravelPlatinum ervaren nooit zien.</p>
<h2 id="simulating-a-bad-wifi-connection">Een slechte wifi-verbinding simuleren</h2>
<p>We wilden zo graag een slechte wifi-verbinding simuleren. Ik verplaatste mezelf naar een plek buiten ons kantoor waar de wifi slecht was, en eindelijk zag ik het probleem met eigen ogen. Eindelijk iets om mee te testen. Toch bleef het raar dat we een of andere trage/slechte wifi nodig hadden om dit te simuleren. Je zou zeggen dat je profiel op slow 3g zetten in de developer tools van de browser hetzelfde gedrag zou geven.</p>
<p>Nu konden we eindelijk gaan testen vanuit onze development-omgeving. We begonnen te vermoeden dat het project zelf iets raars deed, misschien iets met javascript dat de verbinding sloot of zo. Dus begonnen we een stripped-down versie van de TravelPlatinum-website te bouwen om zonder javascript te testen.</p>
<p>Het probleem dat afbeeldingen niet laadden was er nog steeds..</p>
<p>We besloten exact dezelfde html van 333travel.nl over te nemen om daarmee te testen en zo een gelijke omgeving te creëren. 333travel deed het immers bij elke refresh nog prima.</p>
<p>Het probleem dat afbeeldingen niet laadden was er nog steeds..</p>
<p>We wilden dit nog beter kunnen simuleren, want onze slechte wifi-plek was niet bepaald een bureau; we zaten op een trap het probleem te debuggen. Ik besloot een 2<sup>nd</sup> wifi-hotspot op te zetten in ons kantoor met een beperkte verbinding van 3mbit om te zien waar dat ons zou brengen. Dit was het! We konden nu bij elke refresh een heleboel kapotte afbeeldingen op TravelPlatinum simuleren! We komen in elk geval ergens. Bedenk wel dat de 333travel-website nog steeds elke afbeelding zonder problemen laadde.</p>
<p>We lieten de nginx-logs op dat moment voor wat ze waren, omdat we erachter waren gekomen dat we in die richting niets gingen oplossen. Nginx gaf nog steeds een ok terug op een header met 0 bytes. Als nginx dat ok vindt met de juiste headers op zijn plek, gaat dat bericht ons op dat moment niet helpen het probleem op te lossen.</p>
<h2 id="testing-with-firefox">Testen met Firefox</h2>
<p>Op het moment dat ik deze wifi-hotspot aanmaakte, waren we net zover dat we afbeeldingen konden testen met alleen de html image tag en verder niets geladen in het project. Ook dat brak onze afbeeldingen. Ik stelde toen voor om Firefox er even bij te pakken, gewoon om te zien of die zich anders gedroeg, want ik vermoedde dat het browsergerelateerd was — hoe de browser reageert op wat er gebeurt. Of in elk geval zou hij misschien een andere foutmelding geven. Iets waar we onze zoektocht naar de oplossing op konden richten.</p>
<p>We zagen dat Firefox de afbeeldingen inderdaad regel voor regel laadde op de 3mbit-verbinding. Maar plotseling stopte het laden, met kapotte afbeeldingen als gevolg die voor zo&rsquo;n 50% geladen waren of helemaal niet. Eindelijk zagen we eens iets anders! Edge/Chrome lieten nog steeds helemaal geen afbeelding zien en gaven me een nutteloze foutmelding. Nu drong eindelijk tot me door dat de static de afbeeldingen daadwerkelijk aan het versturen was en de browser ze ontving, maar dat de verbinding op de een of andere manier te vroeg gesloten werd.</p>
<h2 id="the-solution">De oplossing</h2>
<p>En dat was het moment waarop ik het probleem eindelijk vond. Een header die ik al tijden geleden op globaal niveau in Nginx had gezet tijdens een performance-tuningsessie.</p>
<p>send_timeout 2;</p>
<p>Wat send timeout volgens mij doet, is de verbinding X seconden openhouden na het niet ontvangen van data. De standaard is 60 seconden en ik had hem op 2 seconden gezet zodat we geen 60 seconden lang open verbindingen zouden hebben die niets doen.</p>
<p>Waarom zou dat het probleem zijn, terwijl ik Firefox duidelijk de afbeelding regel voor regel zag laden? Er wordt dan toch constant data verstuurd? Waarom zou deze setting dan gerelateerd zijn aan het sluiten van de verbinding.</p>
<table>
	<thead>
			<tr>
					<th>Syntax:</th>
					<th><strong>send_timeout</strong> <em>time</em>;</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Default:</td>
					<td>send_timeout 60s;</td>
			</tr>
			<tr>
					<td>Context:</td>
					<td>http, server, location</td>
			</tr>
	</tbody>
</table>
<p><em>Sets a timeout for transmitting a response to the client. The timeout is set only between two successive write operations, not for the transmission of the whole response. If the client does not receive anything within this time, the connection is closed.</em></p>
<p>Nou, het blijkt dat dit op tragere verbindingen wél getriggerd wordt. Nginx interpreteert dit als een stalled request. En het sluit de hele verbinding naar in ons geval static.333travel.nl. De gedeeltelijk geladen afbeelding en elke afbeelding daarna die niet geladen werd, wordt niet opnieuw geladen omdat de verbinding met static gesloten is.</p>
<p>Waarom het simuleren van een trage verbinding in de developer tools dit probleem niet triggert, weet ik nog steeds niet zeker. Ik heb een paar theorieën: een slechte wifi-verbinding zorgt ook voor meer latency en verloren packets die opnieuw verstuurd moeten worden. Maar de echte reden ken ik niet. Ik weet niet hoe deze browsers een trage verbinding simuleren.</p>
<p>Deze send_timeout op 8 seconden zetten zoals hieronder loste ons probleem volledig op.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
add_header &lsquo;Access-Control-Allow-Origin&rsquo; &lsquo;*&rsquo;;<br>
send_timeout 8;<br>
}</p>
<h2 id="conclusion">Conclusie</h2>
<p>We hebben veel geleerd, en we hebben het probleem opgelost. Raar blijft wel dat dit al die tijd alleen op ons TravelPlatinum-project getriggerd werd. We balanceerden blijkbaar op de grens met de send_timeout-setting, omdat de afbeeldingen op TravelPlatinum voor meer load zorgen.</p>
<p>Nog iets belangrijks om te onthouden: een trage verbinding instellen in je developer tools is geen echte simulatie van een trage verbinding. Je kunt simuleren wat je wilt, maar je hebt echte praktijksituaties nodig om echt te meten of alles goed werkt.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Handige commando&#39;s</title>
      <link>https://www.allsubjectsmatter.nl/nl/useful-commands/</link>
      <guid isPermaLink="false">66e14258aea2140001bf043f</guid>
      <pubDate>Mon, 11 Sep 2023 09:21:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Handige commando&#39;s en scripts die technische taken makkelijker maken, zoals logs beheren met Notepad&#43;&#43; en regex, of DNS-checks voor domeinen automatiseren.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/useful-commands/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="notepad-regexp">Notepad++ regexp</h2>
<p>Bijvoorbeeld om je php-fpm access/slow log te controleren. Gebruik mark in Notepad++ om alle regels te markeren die trager zijn dan x/y.</p>
<pre class="line-numbers language-text"><code class="language-text">10000ms en trager 

^.*\s(\d{5,}\.\d{3}ms).*

5000ms en trager 

^.*\s(5\d{3}\.\d{2,}ms|\d{5,}\.\d{2,}ms).*</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="dns-tools-to-automatically-check-the-transfer-of-your-dns">DNS-tools om de overdracht van je DNS automatisch te controleren</h2>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt update
sudo apt install dnsutil

nano check_nameservers.sh</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

DOMAIN=&amp;#34;jouwdomein.com&amp;#34;
EXPECTED_NS1=&amp;#34;ns1.cloudflare.com.&amp;#34;
EXPECTED_NS2=&amp;#34;ns2.cloudflare.com.&amp;#34;

# Get the current nameservers
CURRENT_NS=$(dig NS $DOMAIN &#43;short)

# Check if the current nameservers match the expected ones
if [[ $CURRENT_NS == *&amp;#34;$EXPECTED_NS1&amp;#34;* &amp;amp;&amp;amp; $CURRENT_NS == *&amp;#34;$EXPECTED_NS2&amp;#34;* ]]; then
    echo &amp;#34;Nameservers are correctly set to Cloudflare.&amp;#34;
else
    echo &amp;#34;Nameservers have not yet switched to Cloudflare.&amp;#34;
fi</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">chmod &#43;x check_nameservers.sh

watch -n 1 ./check_nameservers.sh</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre>]]></content:encoded>
    </item>
    <item>
      <title>Docker Nginx HTTP/3 en Brotli</title>
      <link>https://www.allsubjectsmatter.nl/nl/docker-nginx-http3-and-brotli/</link>
      <guid isPermaLink="false">64183271dba1d1003d7c539e</guid>
      <pubDate>Mon, 20 Mar 2023 11:20:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Hier vind je een werkende Dockerfile die ik heb gemaakt als basis voor mijn projecten. Dit is een recente Nginx met gecompileerde HTTP/3-ondersteuning en Brotli-ondersteuning voor betere compressie. Ik plaats het hier om het met iedereen te delen.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="update-13-11-2024">Update (13-11-2024)</h2>
<p>Op de een of andere manier was ik de stap kwijt om een nieuw image aan te maken en daar alle builder-items uit te kopiëren. Het veranderde niets aan de werking van het image, maar de grootte is nu veel kleiner: van ruim 2gb naar onder de 100mb. Ergens tijdens mijn eerdere debuggen is die stap verloren gegaan. De laatste update staat hieronder dus opnieuw.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/azure-western-europe-zone-3-latency-issue-exploration/"><span class="bookmark__body"><span class="bookmark__title">Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</span><span class="bookmark__description">Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/docker-nginx-http3-and-brotli/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/docker-nginx-http3-and-brotli/bookmarks/01-thumb_hu_27e6839ff9d3a962.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="introduction">Introductie</h2>
<p>Hieronder vind je een werkende Dockerfile die ik heb gemaakt, met de onderstaande compatibiliteitseisen die ik zelf had. Ik plaats dit hier als eigen archief, en natuurlijk om te delen. Ik heb de volgende ondersteuning toegevoegd.</p>
<ul>
<li>HTTP/2 HPACK</li>
<li>HTTP/3</li>
<li>BoringSSL</li>
<li>Brotli-ondersteuning</li>
</ul>
<p>Deze versie van Nginx ondersteunt HTTP/3 quic en Brotli-compressie als belangrijkste voordelen, plus wat andere fixes. Tegenwoordig geen Brotli-ondersteuning hebben is gewoon een stap terug in de DeLorean.</p>
<p>Meer info over hoe <a href="https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/" data-external>HPACK</a> je HTTP/2 versnelt. En hier lees je meer details over <a href="https://blog.ononoki.org/enable-http3-for-nginx/" data-external>BoringSSL</a>.</p>
<h2 id="brotli-compared-to-gzip">Brotli vergeleken met GZIP</h2>
<p>Zoals gezegd is dit een Nginx-versie die Brotli ondersteunt. Brotli is erg nuttig omdat het een beter compressie-algoritme is dan GZIP. Ben je nog niet overtuigd, kijk dan de video hieronder.</p>
<figure class="embed"><iframe src="https://www.youtube-nocookie.com/embed/BMCvMWqoivs" title="How File Compression Increases Your Website Speed: BROTLI vs GZIP Algorithms" loading="lazy" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></figure>

<h2 id="nginx-base-docker-file">Nginx basis Dockerfile</h2>
<p>Dit is de basis van mijn dockerfile die alle bovengenoemde dingen regelt. Je kunt dit natuurlijk aanpassen of andere versies van alpine of Nginx gebruiken, zolang het bouwen van de andere modules maar niet breekt. Ik heb Nginx, NJS en BoringSSL ook naar de nieuwste versies geüpdatet.</p>
<p>Ik gebruik dit image in plaats van de officiële Nginx om al mijn webprojecten te draaien. Zie dit als een basisimage waarin je al je draaiende webserver-projecten definieert. Je krijgt er &ldquo;gratis&rdquo; Brotli-compressie en HTTP/3-ondersteuning bij, zodat je je sites sneller en actueler kunt maken.</p>
<pre class="line-numbers language-docker"><code class="language-docker">##################################################
# Nginx with Quiche (HTTP/3) and Brotli
# VERSION 1.05 / 13-11-2024
##################################################
# This build compiles Nginx with Brotli and
# HTTP/3 support. It uses a multi-stage build to
# minimize the final image size by copying only
# the necessary artifacts from the builder image.
# Based on Alpine for a lean and efficient image.
##################################################

# Builder Stage
FROM alpine:latest AS builder

# Set the versions of Nginx, NJS, and BoringSSL
ENV NGINX_VERSION=1.27.2
ENV NJS_VERSION=0.8.7
ENV BORINGSSL_VERSION=c59bf8bf189dcbde868e04efcd53b705ed155231
ENV BORINGSSL=&amp;#34;/tmp/boring-nginx&amp;#34;

# Build-time metadata
ARG BUILD_DATE
ARG VCS_REF

# Install dependencies for building Nginx
RUN addgroup -S nginx \
  &amp;amp;&amp;amp; adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
  &amp;amp;&amp;amp; apk update \
  &amp;amp;&amp;amp; apk upgrade \
  &amp;amp;&amp;amp; apk add --no-cache ca-certificates openssl \
  &amp;amp;&amp;amp; update-ca-certificates \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .build-deps \
      gcc libc-dev make pcre-dev zlib-dev linux-headers gnupg \
      libxslt-dev gd-dev geoip-dev perl-dev \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .brotli-build-deps \
      autoconf libtool automake git g&#43;&#43; cmake go perl rust cargo patch \
      libxml2-dev byacc flex libstdc&#43;&#43; libmaxminddb-dev lmdb-dev file openrc

# Install and build BoringSSL
RUN mkdir $BORINGSSL \
  &amp;amp;&amp;amp; cd $BORINGSSL \
  &amp;amp;&amp;amp; git clone https://boringssl.googlesource.com/boringssl \
  &amp;amp;&amp;amp; cd boringssl \
  &amp;amp;&amp;amp; git checkout -q ${BORINGSSL_VERSION} \
  &amp;amp;&amp;amp; mkdir build \
  &amp;amp;&amp;amp; cd build \
  &amp;amp;&amp;amp; cmake -DBUILD_SHARED_LIBS=1 .. \
  &amp;amp;&amp;amp; make

# Prepare BoringSSL for Nginx compilation
RUN mkdir -p &amp;#34;$BORINGSSL/boringssl/.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cd &amp;#34;$BORINGSSL/boringssl/.openssl&amp;#34; \
  &amp;amp;&amp;amp; ln -s ../include include \
  &amp;amp;&amp;amp; cd &amp;#34;$BORINGSSL/boringssl&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;build/crypto/libcrypto.so&amp;#34; &amp;#34;.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;build/ssl/libssl.so&amp;#34; &amp;#34;.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;.openssl/lib/libssl.so&amp;#34; /usr/lib/ \
  &amp;amp;&amp;amp; cp &amp;#34;.openssl/lib/libcrypto.so&amp;#34; /usr/lib/

# Clone Nginx modules
RUN mkdir /usr/src \
  &amp;amp;&amp;amp; cd /usr/src \
  &amp;amp;&amp;amp; git clone --depth=1 --recursive --shallow-submodules https://github.com/google/ngx_brotli \
  &amp;amp;&amp;amp; git clone --branch $NJS_VERSION --depth=1 --recursive --shallow-submodules https://github.com/nginx/njs

# Build Nginx with modules
RUN cd /usr/src \
  &amp;amp;&amp;amp; wget -qO nginx.tar.gz https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \
  &amp;amp;&amp;amp; tar -zxC /usr/src -f nginx.tar.gz \
  &amp;amp;&amp;amp; rm nginx.tar.gz \
  &amp;amp;&amp;amp; cd /usr/src/nginx-$NGINX_VERSION \
  &amp;amp;&amp;amp; mkdir /root/.cargo \
  &amp;amp;&amp;amp; echo $&amp;#39;[net]\ngit-fetch-with-cli = true&amp;#39; &amp;gt; /root/.cargo/config.toml \
  &amp;amp;&amp;amp; ./configure --prefix=/etc/nginx \
      --sbin-path=/usr/sbin/nginx \
      --modules-path=/usr/lib/nginx/modules \
      --conf-path=/etc/nginx/nginx.conf \
      --error-log-path=/var/log/nginx/error.log \
      --http-log-path=/var/log/nginx/access.log \
      --pid-path=/var/run/nginx.pid \
      --lock-path=/var/run/nginx.lock \
      --http-client-body-temp-path=/var/cache/nginx/client_temp \
      --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
      --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
      --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
      --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
      --user=nginx \
      --group=nginx \
      --with-pcre-jit \
      --with-http_ssl_module \
      --with-http_realip_module \
      --with-http_addition_module \
      --with-http_sub_module \
      --with-http_dav_module \
      --with-http_flv_module \
      --with-http_mp4_module \
      --with-http_gunzip_module \
      --with-http_gzip_static_module \
      --with-http_random_index_module \
      --with-http_secure_link_module \
      --with-http_stub_status_module \
      --with-http_auth_request_module \
      --with-http_xslt_module=dynamic \
      --with-http_image_filter_module=dynamic \
      --with-http_geoip_module=dynamic \
      --with-http_perl_module=dynamic \
      --with-threads \
      --with-stream \
      --with-stream_ssl_module \
      --with-stream_ssl_preread_module \
      --with-stream_realip_module \
      --with-stream_geoip_module=dynamic \
      --with-http_slice_module \
      --with-mail \
      --with-mail_ssl_module \
      --with-compat \
      --with-file-aio \
      --with-http_v2_module \
      --with-http_v3_module \
      --add-module=/usr/src/ngx_brotli \
      --add-module=/usr/src/njs/nginx \
      --with-cc-opt=&amp;#34;-I$BORINGSSL/boringssl/include -Wno-error&amp;#34; \
      --with-ld-opt=&amp;#34;-L$BORINGSSL/boringssl/build/ssl -L$BORINGSSL/boringssl/build/crypto&amp;#34; \
      --with-select_module \
      --with-poll_module \
      --build=&amp;#34;docker-nginx-http3-$VCS_REF-$BUILD_DATE ngx_brotli-$(git --git-dir=/usr/src/ngx_brotli/.git rev-parse --short HEAD) njs-$(git --git-dir=/usr/src/njs/.git rev-parse --short HEAD)&amp;#34; \
  &amp;amp;&amp;amp; touch &amp;#34;$BORINGSSL/boringssl/.openssl/include/openssl/ssl.h&amp;#34; \
  &amp;amp;&amp;amp; make -j$(getconf _NPROCESSORS_ONLN) \
  &amp;amp;&amp;amp; make install \
  &amp;amp;&amp;amp; rm -rf /etc/nginx/html/ \
  &amp;amp;&amp;amp; mkdir /etc/nginx/conf.d/ \
  &amp;amp;&amp;amp; mkdir -p /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; install -m644 html/index.html /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; install -m644 html/50x.html /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; ln -s /usr/lib/nginx/modules /etc/nginx/modules \
  &amp;amp;&amp;amp; strip /usr/sbin/nginx* \
  &amp;amp;&amp;amp; strip /usr/lib/nginx/modules/*.so \
  &amp;amp;&amp;amp; rm -rf /etc/nginx/*.default /etc/nginx/*.so \
  &amp;amp;&amp;amp; rm -rf /usr/src \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .gettext gettext \
  &amp;amp;&amp;amp; mv /usr/bin/envsubst /tmp/ \
  \
  # Determine runtime dependencies
  &amp;amp;&amp;amp; runDeps=&amp;#34;$( \
      scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
      | awk &amp;#39;{ gsub(/,/, &amp;#34;\nso:&amp;#34;, $2); print &amp;#34;so:&amp;#34; $2 }&amp;#39; \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
    )&amp;#34; \
  &amp;amp;&amp;amp; echo &amp;#34;$runDeps&amp;#34; &amp;gt; /nginx-run-deps.txt \
  \
  # Cleanup
  &amp;amp;&amp;amp; apk del .brotli-build-deps .build-deps .gettext \
  &amp;amp;&amp;amp; rm -rf /root/.cargo \
  &amp;amp;&amp;amp; rm -rf /var/cache/apk/* \
  &amp;amp;&amp;amp; mv /tmp/envsubst /usr/local/bin/ \
  &amp;amp;&amp;amp; mkdir -p /etc/ssl/private \
  &amp;amp;&amp;amp; openssl req -x509 -newkey rsa:4096 -nodes -keyout /etc/ssl/private/localhost.key -out /etc/ssl/localhost.pem -days 365 -sha256 -subj &amp;#39;/CN=localhost&amp;#39; \
  &amp;amp;&amp;amp; ln -sf /dev/stdout /var/log/nginx/access.log \
  &amp;amp;&amp;amp; ln -sf /dev/stderr /var/log/nginx/error.log

# Final Stage
FROM alpine:latest

# Copy Nginx and related files from the builder stage
COPY --from=builder /usr/sbin/nginx /usr/sbin/nginx
COPY --from=builder /etc/nginx /etc/nginx
COPY --from=builder /usr/lib/nginx /usr/lib/nginx
COPY --from=builder /usr/share/nginx /usr/share/nginx
COPY --from=builder /usr/local/bin/envsubst /usr/local/bin/envsubst
COPY --from=builder /var/log/nginx /var/log/nginx
COPY --from=builder /var/cache/nginx /var/cache/nginx
COPY --from=builder /var/run /var/run
COPY --from=builder /etc/ssl /etc/ssl
COPY --from=builder /usr/lib/libssl.so* /usr/lib/
COPY --from=builder /usr/lib/libcrypto.so* /usr/lib/
COPY --from=builder /nginx-run-deps.txt /nginx-run-deps.txt

# Install runtime dependencies
RUN apk add --no-cache ca-certificates openssl libstdc&#43;&#43; libgcc \
  &amp;amp;&amp;amp; apk add --no-cache $(cat /nginx-run-deps.txt) \
  &amp;amp;&amp;amp; rm /nginx-run-deps.txt \
  &amp;amp;&amp;amp; update-ca-certificates \
  \
  # Forward logs to Docker
  &amp;amp;&amp;amp; ln -sf /dev/stdout /var/log/nginx/access.log \
  &amp;amp;&amp;amp; ln -sf /dev/stderr /var/log/nginx/error.log

# Create nginx user and group
RUN addgroup -S nginx \
  &amp;amp;&amp;amp; adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx

# Expose ports
EXPOSE 80 443

STOPSIGNAL SIGTERM

CMD [&amp;#34;nginx&amp;#34;, &amp;#34;-g&amp;#34;, &amp;#34;daemon off;&amp;#34;]</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="nginxconf-sample">Nginx.conf-voorbeeld</h2>
<p>Hier vind je een voorbeeld van een nginx.conf die je een idee geeft hoe ik mijn nginx-omgeving draai.</p>
<p>Het belangrijkste verschil in dit bestand ten opzichte van de normale nginx.conf zijn de brotli-compressie-instellingen.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">user nginx;
worker_processes auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 1024;
}

http {
	##
	# Basic Settings
	##
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;

	proxy_connect_timeout  300s;
	proxy_send_timeout  300s;
	proxy_read_timeout  300s;
	fastcgi_send_timeout 300s;
	fastcgi_read_timeout 300s;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##
    log_format  main  &amp;#39;$remote_addr - $remote_user [$time_local] &amp;#34;$request&amp;#34; &amp;#39;
                      &amp;#39;$status $body_bytes_sent &amp;#34;$http_referer&amp;#34; &amp;#39;
                      &amp;#39;&amp;#34;$http_user_agent&amp;#34; &amp;#34;$http_x_forwarded_for&amp;#34;&amp;#39;;

    access_log  /var/log/nginx/access.log  main;

	##
	# Gzip Settings
	##
	gzip on;
	gzip_vary on;
	gzip_proxied any;
	gzip_comp_level 6;
	gzip_buffers 16 8k;
	gzip_http_version 1.1;
	gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml&#43;rss text/javascript;

	##
	# Brotli Settings (modify the brotli_comp_level to match your optimal cpu load vs compression)
	##
	brotli on;
	brotli_static on;
	brotli_comp_level 11;
	brotli_types
	text/plain
	text/css
	text/xml
	text/javascript
	text/x-component
	application/xml
	application/xml&#43;rss
	application/javascript
	application/json
	application/atom&#43;xml
	application/vnd.ms-fontobject
	application/x-font-ttf
	application/x-font-opentype
	application/x-font-truetype
	application/x-web-app-manifest&#43;json
	application/xhtml&#43;xml
	application/octet-stream
	font/opentype
	font/truetype
	font/eot
	font/otf
	font/woff
	font/woff2
	image/svg&#43;xml
	image/x-icon
	image/vnd.microsoft.icon
	image/bmp;

	##
	# Virtual Host Configs
	##
	include /etc/nginx/conf.d/*.conf;
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="siteconf-sample">Site.conf-voorbeeld</h2>
<p>Let op: hoe hoger je de brotli-compressie instelt, hoe cpu-intensiever het wordt. TTFB (time to first byte) kan een probleem worden als de compressieratio te hoog staat in combinatie met een trage kubernetes-omgeving, hoge loads, of alles daartussenin. Test dus met realistische scenario&rsquo;s en pas het daarop aan. Dit is niet alleen een Brotli-probleem; Gzip heeft dezelfde nadelen als je de compressieniveaus te hoog zet, terwijl de afname in bestandsgrootte het niet waard is.</p>
<p>Je kunt dit bestand gebruiken om alle HTTP/3-instellingen hieronder over te nemen. Dit optimaliseert en activeert HTTP/3-ondersteuning voor je site.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">server {
       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;

        # Certificate locations for HTTPS.
	    ssl_certificate /etc/ssl/certs/wildcard-cert-23.crt;
        ssl_certificate_key /etc/ssl/private/wildcard-cert-23.key;

        # Enable SSL settings for performance.
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        # Enable SSL settings for security.
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        # Base settings (root and hostname).
        root /var/www/html;
        index index.php index.html index.htm;
        server_name kubernetes.servername.nl;

        # HTTP/2 settings
        http2_push_preload on;
        # /End of HTTP/2.

        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	    }

        location ~ \.php$ {
                try_files $uri =404;
                # Bunch of PHP and FPM settings
                fastcgi_split_path_info ^(.&#43;\.php)(/.&#43;)$;
                fastcgi_pass your-php-fpm-instance:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info
                
                # Add a bunch of other headers based on your desired settings.
                add_header X-protocol $server_protocol always;   
                add_header X-Cache $upstream_cache_status always;
                add_header X-Frame-Options SAMEORIGIN always;
                add_header X-Content-Type-Options nosniff always;
                add_header X-XSS-Protection &amp;#34;1; mode=block&amp;#34;;
                add_header Strict-Transport-Security &amp;#34;max-age=31536000; includeSubdomains; preload&amp;#34; always;
                add_header Referrer-Policy strict-origin-when-cross-origin;
                add_header Pragma public;
                add_header Cache-Control &amp;#34;public&amp;#34;;
        }
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="multiple-h3-connections-from-a-single-nginx">Meerdere H3-verbindingen vanaf één Nginx</h2>
<p>Hier leg ik uit hoe je meerdere server blocks binnen Nginx met HTTP/3 kunt laten werken. Er zitten wat haken en ogen aan, omdat http3 UDP gebruikt met de instelling reuseport, waardoor je poort 443 niet meerdere keren kunt toewijzen bij gebruik van HTTP/3.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Dit deel van de header meldt dat onze HTTP/3-verbinding poort 443 accepteert. Heb je een enkele nginx-container met meerdere geconfigureerde server blocks, dan krijg je problemen met het hergebruik van poort 443.</p>
<p>Met HTTP/3 kun je een poort maar één keer definiëren binnen een server block. Heb je meerdere server blocks, dan kun je je website via andere poorten dan 443 laten communiceren met behulp van deze alt-svc-header, die de browser vertelt dat hij HTTP/3 accepteert en op welke poort.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;

add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:444&amp;#34;; ma=86400, h3=&amp;#34;:444&amp;#34;; ma=86400&amp;#39; always;</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p>Let op de verschillen in de h3-poorten hier. Zo kun je meerdere server blocks h3 laten gebruiken door unieke poorten op te geven.</p>
<p>Het enige wat je hoeft te doen is je instance naar de juiste poort laten luisteren die in je alt-svc-header staat. Als er een succesvolle verbinding mogelijk is, zal de browser proberen over te stappen op h3. Hieronder een voorbeeld van hoe je kunt luisteren op poort 443 en 444 met HTTP/3.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">server {

        server_name http3-port-443

       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-nginx"><code class="language-nginx">server {

        server_name http3-port-444

       	# HTTP/3 and Quic Listen
      	listen 444 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:444&amp;#34;; ma=86400, h3=&amp;#34;:444&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Zo eenvoudig is het om je reverse proxy of meerdere server blocks een H3-verbinding te laten opzetten. Je moet wel de juiste port forwards instellen in je firewall/netwerk.</p>
<p>Met docker configureer je dit zoals hieronder weergegeven. Bij het opgeven van UDP moet je in docker elke poort apart specificeren. Hieronder mijn voorbeeld met UDP-poorten 443 tot en met 449.</p>
<pre class="line-numbers language-yaml"><code class="language-yaml">services:
  ############################################################
  # proxy webservice to connect to multiple nginx instances
  ############################################################
  reverse-proxy:
    image: custom-build-nginx:latest
    restart: unless-stopped
    ports:
      - &amp;#39;80:80&amp;#39;
      - &amp;#39;443:443&amp;#39;
      - &amp;#39;443:443/udp&amp;#39;
      - &amp;#39;444:444/udp&amp;#39;
      - &amp;#39;445:445/udp&amp;#39;
      - &amp;#39;446:446/udp&amp;#39;
      - &amp;#39;447:447/udp&amp;#39;
      - &amp;#39;448:448/udp&amp;#39;
      - &amp;#39;449:449/udp&amp;#39;
    volumes:</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="update-03-04-2024">Update (03-04-2024)</h2>
<p>Wat versienummers toegevoegd aan mijn dockerfile om versies beter te kunnen vergelijken. Ik heb de build van gisteren ook opgeschoond en besloten om een ENV toe te voegen voor het instellen van de boringssl-versie. Omdat hun build mijn dockerfile kapotmaakte, updaten we die maar handmatig. Verder heb ik nginx geüpdatet naar 1.25.4 en NJS naar 0.8.3 en de build geverifieerd.</p>
<h2 id="update-02-04-2024">Update (02-04-2024)</h2>
<p>Pas recent kwam ik erachter dat een update van BoringSSL van rond 28 februari mijn build van BoringSSL had gebroken. Hieronder de bijgewerkte dockerfile die weer compileert. Ik moest libssl als shared library bouwen om het weer te laten werken. Ik ga dit image nog wat verder opruimen, maar het werkt in elk geval weer.</p>
<h2 id="update-18-03-2024">Update (18-03-2024)</h2>
<p>Ik heb een sectie toegevoegd die uitlegt hoe je meerdere H3-verbindingen kunt gebruiken als je een Nginx hebt met meer dan één server block. Dit is ook de sleutel als je Nginx als Reverse Proxy gebruikt en gebruik wilt maken van H3.</p>
<h2 id="update-04-09-2023">Update (04-09-2023)</h2>
<p>Ik kwam erachter dat deze build niet meer compileerde door een verandering in de brotli-broncode? Het had misschien te maken met updates van GitHub die ik niet had vastgelegd in deze Dockerfile. Ik heb het bestand volledig omgebouwd zodat het werkt met de bijgewerkte Nginx 1.25.2, die HTTP/3-ondersteuning &ldquo;ingebouwd&rdquo; heeft. Ook heb ik ervoor gezorgd dat we weer native Brotli-ondersteuning hebben. Dit zou vanaf nu moeten blijven werken.</p>
<h2 id="update-30-08-2023">Update (30-08-2023)</h2>
<p>Ik heb de modsec-ondersteuning die er eerst in zat verwijderd. Die gaf willekeurige errors en omdat ik hem toch niet gebruik, heb ik hem eruit gehaald. Misschien komt hij later terug.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Het ultieme ZSH- en Windows Terminal-look and feel</title>
      <link>https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/</link>
      <guid isPermaLink="false">64180e15dba1d1003d7c5261</guid>
      <pubDate>Wed, 08 Feb 2023 10:57:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[In deze post leg ik mijn eigen setup uit: hoe ik mijn Windows Terminal gebruik, plus wat andere tips. Dus als je iemand indruk wilt maken terwijl je in je favoriete koffietent werkt, en ze wilt laten denken dat je bij Nasa werkt.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="introduction">Introductie</h2>
<p>In deze post leg ik mijn persoonlijke setup uit: hoe ik mijn Windows Terminal gebruik, plus wat andere tips. Dus als je iemand wilt imponeren terwijl je werkt in je favoriete koffiezaak, door ze het gevoel te geven dat je voor Nasa werkt — dan zit je hier goed! Grapjes terzijde: als je dag in dag uit met iets werkt, mag het er op z&rsquo;n minst een beetje mooi uitzien, toch?</p>
<p>Ik begin met een lijst van alles wat ik geïnstalleerd heb, daarna leg ik uit wat het allemaal doet.</p>
<h2 id="zsh-and-the-p10k-theme">ZSH en het p10k-thema</h2>
<figure class="bookmark"><a class="bookmark__link" href="https://github.com/ohmyzsh/ohmyzsh" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - ohmyzsh/ohmyzsh: 🙃 A delightful community-driven (with 2,100&#43; contributors) framework for managing your zsh configuration. Includes 300&#43; optional plugins (rails, git, macOS, hub, docker, homebrew, node, php, python, etc), 140&#43; themes to spice up your morning, and an auto-update tool so that makes it easy to keep up with the latest updates from the community.</span><span class="bookmark__description">🙃 A delightful community-driven (with 2,100&#43; contributors) framework for managing your zsh configuration. Includes 300&#43; optional plugins (rails, git, macOS, hub, docker, homebrew, node, php, pyth…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/01-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>ohmyzsh</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/01-thumb_hu_cd46afdcd6f277f0.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/romkatv/powerlevel10k" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - romkatv/powerlevel10k: A Zsh theme</span><span class="bookmark__description">A Zsh theme. Contribute to romkatv/powerlevel10k development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/02-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>romkatv</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/02-thumb_hu_e0ce4be87804921b.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://spacevim.org/quick-start-guide/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Quick start guide | SpaceVim</span><span class="bookmark__description">A quick start guide which will tell you how to install and configure SpaceVim, also provides a list of resources for learning SpaceVim.</span><span class="bookmark__meta"><span>SpaceVim</span><span>Shidong Wang</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/03-thumb_hu_fd18ea080be99d12.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>De bovenstaande items heb ik geïnstalleerd voor de look and feel van mijn shell. Ik gebruik Windows Terminal en daardoor ziet mijn terminal er goed uit en worden de juiste gegevens onderscheiden door kleuren in de shell.</p>
<p><strong>ohmyzsh</strong> is het framework waarmee ik mijn ZSH-configuratie beheer. Het thema dat ik geïnstalleerd heb is p10k. Dit thema is enorm flexibel aan te passen.</p>
<p><strong>SpaceVim</strong> is een van de beste visuele editors om alles in Linux te bewerken wat je nodig hebt. Kijk zelf maar via de link.</p>
<p>Hier een screenshot van hoe mijn persoonlijke shell eruitziet. Ik deel hieronder ook de JSON-config van mijn Windows Terminal. Het font dat ik gebruik is de standaard nerd font die bij p10k zit. Dat is de Meslo Gorgeous monospace Nerd font.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-1_hu_98a6094c3d3897a5.webp" alt="" width="560" height="342" data-zoom-src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-1_hu_f6fef50f1a2399a5.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>De kleuren die ik gebruik heten Atom One Dark. Een subtiel pastelkleurig font.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image_hu_6020c119054673fb.webp" alt="" width="400" height="70" data-zoom-src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image_hu_6020c119054673fb.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>In het JSON-bestand hieronder vind je een paar leuke extra&rsquo;s om je ervaring nog verder aan te passen.</p>
<h2 id="kali-linux">Kali Linux</h2>
<p>Ik heb Kali Linux geïnstalleerd als seamless overlay. Dat betekent dat je Kali visueel native binnen Windows kunt draaien. Zie de screenshot hieronder om te zien hoe netjes dat eruitziet! Via de link hieronder lees je hoe je dit met Kali aan de praat krijgt.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.kali.org/docs/wsl/win-kex/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Win-KeX | Kali Linux Documentation</span><span class="bookmark__description">Windows Subsystem for Linux 2 &amp; Win-KeX</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/04-icon_hu_5cd2a9b225aaa8ff.webp" alt="" class="bookmark__icon" width="32" height="32" loading="lazy" decoding="async">
  </picture><span>Kali Linux</span></span></span><span class="bookmark__thumb"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/04-thumb.svg" sizes="250px" alt="" loading="lazy" decoding="async"></span></a></figure>

<pre class="line-numbers language-json"><code class="language-json">&amp;#34;commandline&amp;#34;: &amp;#34;wsl -d kali-linux kex --sl --wtstart -s&amp;#34;,</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-2_hu_727fbce17435cc82.webp" alt="" width="560" height="351" data-zoom-src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-2_hu_b5bbf743fe36003f.webp" loading="lazy" decoding="async">
  </picture><figcaption>Naadloze integratie met Kali en Windows</figcaption></figure>

<h2 id="external-ssh-in-windows-terminal">Externe SSH in Windows Terminal</h2>
<p>Je kunt met Windows Terminal ook makkelijk inloggen op (externe) servers die tokens vereisen.</p>
<pre class="line-numbers language-json"><code class="language-json">&amp;#34;commandline&amp;#34;: &amp;#34;ssh -i \&amp;#34;~/.ssh/id_rsa\&amp;#34; username@ip-address&amp;#34;,</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Kijk via de link hieronder hoe je dit configureert.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://blog.anurut.com/ssh-with-private-key-in-windows-terminal/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">How to SSH into a remote server using private key and Windows Terminal</span><span class="bookmark__description">SSH into a remote computer in just one click using a private key and Windows Terminal | Super easy.</span><span class="bookmark__meta"><span>./anurut&#39;s-blog</span><span>Anurut</span></span></span></a></figure>

<h2 id="windows-terminal-config">Windows Terminal-config</h2>
<p>Hier is de JSON voor de Windows Terminal, met als laatste deel het kleurenthema dat ik gebruik, en de instellingen voor de look and feel. Er staan ook voorbeelden in voor het seamless draaien van Kali en een SSH-sessie met tokens.</p>
<pre class="line-numbers language-json"><code class="language-json">    &amp;#34;profiles&amp;#34;: 
    {
        &amp;#34;defaults&amp;#34;: 
        {
            &amp;#34;antialiasingMode&amp;#34;: &amp;#34;cleartype&amp;#34;,
            &amp;#34;backgroundImage&amp;#34;: null,
            &amp;#34;backgroundImageAlignment&amp;#34;: &amp;#34;bottom&amp;#34;,
            &amp;#34;backgroundImageOpacity&amp;#34;: 0.8,
            &amp;#34;colorScheme&amp;#34;: &amp;#34;Atom One Dark&amp;#34;,
            &amp;#34;cursorShape&amp;#34;: &amp;#34;vintage&amp;#34;,
            &amp;#34;experimental.retroTerminalEffect&amp;#34;: false,
            &amp;#34;font&amp;#34;: 
            {
                &amp;#34;face&amp;#34;: &amp;#34;MesloLGS NF&amp;#34;
            },
            &amp;#34;opacity&amp;#34;: 75,
            &amp;#34;useAcrylic&amp;#34;: true
        },
        &amp;#34;list&amp;#34;: 
        [
            {
                &amp;#34;commandline&amp;#34;: &amp;#34;wsl -d kali-linux kex --sl --wtstart -s&amp;#34;,
                &amp;#34;guid&amp;#34;: &amp;#34;{55ca431a-3a87-5fb3-83cd-11ececc031d2}&amp;#34;,
                &amp;#34;hidden&amp;#34;: false,
                &amp;#34;icon&amp;#34;: &amp;#34;C:\\Users\\icons\\kali-menu.png&amp;#34;,
                &amp;#34;name&amp;#34;: &amp;#34;Kali Seamless overlay&amp;#34;,
                &amp;#34;startingDirectory&amp;#34;: &amp;#34;%USERPROFILE%&amp;#34;
            },
            {
                &amp;#34;commandline&amp;#34;: &amp;#34;ssh -i \&amp;#34;~/.ssh/id_rsa\&amp;#34; username@ip-address&amp;#34;,
                &amp;#34;guid&amp;#34;: &amp;#34;{0caa0dad-35be-5f56-a8ff-afceaeaa6103}&amp;#34;,
                &amp;#34;hidden&amp;#34;: false,
                &amp;#34;name&amp;#34;: &amp;#34;External test server&amp;#34;
            }
        ]
    },
    &amp;#34;schemes&amp;#34;: 
    [
        {
            &amp;#34;background&amp;#34;: &amp;#34;#282C34&amp;#34;,
            &amp;#34;black&amp;#34;: &amp;#34;#000000&amp;#34;,
            &amp;#34;blue&amp;#34;: &amp;#34;#61AFEF&amp;#34;,
            &amp;#34;brightBlack&amp;#34;: &amp;#34;#5C6370&amp;#34;,
            &amp;#34;brightBlue&amp;#34;: &amp;#34;#61AFEF&amp;#34;,
            &amp;#34;brightCyan&amp;#34;: &amp;#34;#56B6C2&amp;#34;,
            &amp;#34;brightGreen&amp;#34;: &amp;#34;#98C379&amp;#34;,
            &amp;#34;brightPurple&amp;#34;: &amp;#34;#C678DD&amp;#34;,
            &amp;#34;brightRed&amp;#34;: &amp;#34;#E06C75&amp;#34;,
            &amp;#34;brightWhite&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;brightYellow&amp;#34;: &amp;#34;#D19A66&amp;#34;,
            &amp;#34;cursorColor&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;cyan&amp;#34;: &amp;#34;#56B6C2&amp;#34;,
            &amp;#34;foreground&amp;#34;: &amp;#34;#CCCCCC&amp;#34;,
            &amp;#34;green&amp;#34;: &amp;#34;#98C379&amp;#34;,
            &amp;#34;name&amp;#34;: &amp;#34;Atom One Dark&amp;#34;,
            &amp;#34;purple&amp;#34;: &amp;#34;#C678DD&amp;#34;,
            &amp;#34;red&amp;#34;: &amp;#34;#E06C75&amp;#34;,
            &amp;#34;selectionBackground&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;white&amp;#34;: &amp;#34;#ABB2BF&amp;#34;,
            &amp;#34;yellow&amp;#34;: &amp;#34;#D19A66&amp;#34;
        }
    ],
    &amp;#34;themes&amp;#34;: [],
    &amp;#34;useAcrylicInTabRow&amp;#34;: true</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="addins-which-make-your-shell-look-and-function-even-better">Addins die je shell er nog beter uit laten zien en nog beter laten werken</h2>
<p>Hieronder vind je nog wat links die je shell-ervaring nog beter maken. Het is een verzameling tools die ik dagelijks gebruik. Aan de bookmarks kun je al zien wat ze doen. Ik zou aanraden om elke link door te lezen en te bepalen of dit ook je leven makkelijker maakt.</p>
<p>Zelf vind ik Exa het leukst. Dit is een vervanging voor het &rsquo;ls&rsquo;-commando, met veel meer zichtbaarheid dankzij kleuren en iconen voor mappen en dergelijke. Ook htop gebruik ik al jaren als mijn go-to process manager.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://github.com/BurntSushi/ripgrep" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - BurntSushi/ripgrep: ripgrep recursively searches directories for a regex pattern while respecting your gitignore</span><span class="bookmark__description">ripgrep recursively searches directories for a regex pattern while respecting your gitignore - GitHub - BurntSushi/ripgrep: ripgrep recursively searches directories for a regex pattern while respec…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/06-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>BurntSushi</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/06-thumb_hu_b211a5b1ef787f02.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/nvbn/thefuck" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - nvbn/thefuck: Magnificent app which corrects your previous console command.</span><span class="bookmark__description">Magnificent app which corrects your previous console command. - GitHub - nvbn/thefuck: Magnificent app which corrects your previous console command.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/07-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>nvbn</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/07-thumb_hu_b2b48ab666833f4.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/rupa/z" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - rupa/z: z - jump around</span><span class="bookmark__description">z - jump around. Contribute to rupa/z development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/08-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>rupa</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/08-thumb_hu_4400b46cb8d5382e.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/ogham/exa" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - ogham/exa: A modern replacement for ‘ls’.</span><span class="bookmark__description">A modern replacement for ‘ls’. Contribute to ogham/exa development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/09-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>ogham</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/09-thumb_hu_67f6b8479bab6f37.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://htop.dev/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">htop - an interactive process viewer</span><span class="bookmark__description"></span><span class="bookmark__meta"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/10-icon.ico" sizes="25px" alt="" class="bookmark__icon" loading="lazy" decoding="async"><span>an interactive process viewer</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/10-thumb_hu_745f32083f045968.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
  </channel>
</rss>
