<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Thomas Skowron's Blog</title><link>https://thomas.skowron.eu/blog/</link><description>Recent content on Thomas Skowron's Blog</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 18 Feb 2017 11:19:40 +0000</lastBuildDate><atom:link href="https://thomas.skowron.eu/blog/" rel="self" type="application/rss+xml"/><item><title>Why the Cloud Isn't Getting More Expensive (Yet)</title><link>https://thomas.skowron.eu/blog/why-the-cloud-isnt-getting-more-expensive/</link><pubDate>Fri, 27 Feb 2026 16:03:14 +0100</pubDate><guid>https://thomas.skowron.eu/blog/why-the-cloud-isnt-getting-more-expensive/</guid><description>&lt;p>When Hetzner, a popular German hosting provider, recently announced price hikes of more than 30% for their services&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, this did not go unnoticed. The reasons are pretty clear: Hardware component prices jumped massively. When I accidentally bought the wrong RAM sticks in November and I needed to return them, in a two-week span the price doubled. I imagine the shop must&amp;rsquo;ve felt quite happy with my mistake – although I doubt that shops really have feelings.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/why-the-cloud-isnt-getting-more-expensive/L1005051.jpg" alt="an emergning pattern">
&lt;/figure>
&lt;p>Germany is not the first place I would start a hosting provider: Electricity is expensive, finding enough land for data centers is difficult, permitting is always a struggle, and personnel costs aren&amp;rsquo;t that cheap, but through fierce competition in the early 2000s a scene of reliable, low-cost providers emerged. The shrewd ones that managed to work within these thin margins have stuck around.&lt;/p>
&lt;p>When AWS started building their cloud offering, their pricing structure wasn&amp;rsquo;t beholden to the same logic. SmugMug, their first enterprise customer, was happy to switch to S3 without saving any money. Passing off the responsibility to a third party at cost parity made sense to them. Of course this was 2006: All hardware was still getting dramatically cheaper. Between 2006 and 2018 hard disks dropped in price more than 90%.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> Not even the floods in Thailand in 2011 could meaningfully disrupt this trend.&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>&lt;/p>
&lt;p>In the early days, AWS passed some of these savings on, possibly to deter competitors from realizing that the cloud was a good business to be in. That deterrence, by the way, seems to have worked: All other successful entrants emerged from their own needs for cloud services. The price cuts stopped around 2014 and from then on, prices have found stable ground: Their mindshare was established, other big name providers had similar pricing, and customers wouldn&amp;rsquo;t be switching en masse to smaller providers.&lt;/p>
&lt;p>AWS did launch some new products that allowed a cheaper on-ramp, like Lambda, which allowed to serve code without paying a flat rate for the substrate it&amp;rsquo;s running on, along with other &amp;ldquo;serverless&amp;rdquo; services that often start out at 0 USD, but have a sharp cost curve once used heavily.&lt;/p>
&lt;p>With their AWS Graviton architecture, they also presented some cheaper EC2 options: A &lt;a href="https://instances.vantage.sh/aws/ec2/t4g.nano?currency=USD&amp;amp;duration=monthly">&lt;code>t4g.nano&lt;/code>&lt;/a> is just around 3 USD/month. Also, it only has ½ GB of memory, which makes it unusable for anything that isn&amp;rsquo;t engineered specifically to run within these confines.&lt;/p>
&lt;p>The sharply rising hardware prices in recent months are attributed to the AI boom, or bubble, as some prefer. There is some circumstantial evidence: AI companies are raising insane capital, which they use to buy GPUs from a limited manufacturing pool. GPUs need memory chips, therefore GPUs and memory are getting expensive. Everybody who is not an AI unicorn needs to buy these with their real, hard earned cash, which may seem massively unfair.&lt;/p>
&lt;p>But unless you plan to build a gaming rig, this is just whinging, because chances are, you have already been paying a premium. Apple is charging 400 USD for a 16 GB RAM upgrade (to 32 GB). And if you are like me and don’t find this completely outrageous yet, AWS charges for a mere 7.5 GB memory upgrade (between &lt;code>t4g.nano&lt;/code> and &lt;code>t4g.large&lt;/code>) over 550 USD per year!&lt;/p>
&lt;p>Of course, buying instead of renting gives you the magic of depreciation: In an earnings call, AWS has reported that they run hardware for five years, meaning that even at current memory prices they have a margin of more than 15x.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> With a 3 year reserved instance, it shrinks into a pitiful 6x. Assuming AWS is paying retail price, which I am sure they have figured out a way not to.&lt;/p>
&lt;p>For all the thin-margin hosting companies, this bears an existential risk: Their customer-facing price per GB of RAM often hovers around 10 EUR (~12 USD) per year, which only starts to break even after more than 2.5 years. Assuming the memory prices stay flat.&lt;/p>
&lt;p>The market does not show any signs of relief: The number of memory manufacturers has shrunk to just three, and new contenders will find it difficult. DDR5 memory has extremely tight timing tolerances, and the big ones are utilizing EUV lithography to manufacture those chips. CXMT, a Chinese manufacturer of memory, has started a DDR5 line, but as Chinese entities are barred from importing current-generation lithography equipment, they are still struggling with engineering challenges.&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>&lt;/p>
&lt;p>Chinese labs are working on their own EUV technology&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>, which may reach maturity in 2030, but everybody would prefer to be Nvidia instead of just another memory manufacturer, so it’s not clear how new capacities would be utilized. Cutting edge hardware takes years or decades to get into production while predictions are imprecise.&lt;/p>
&lt;p>This gold rush sparks a lot of interest in competition for Nvidia, as nobody likes to pay 25k USD for a single H100: Google invested early into TPUs, and Cerebras offers powerful hardware for training and inference you can buy. Still, any of them need to utilize manufacturing capacity which is massively constrained and sold to the highest bidder. Fat margins insulate from icy market conditions, but attract hungry competition.&lt;/p>
&lt;p>Even if AI company valuations don’t stay as high, LLMs are here to stay and will continue to devour a lot of the available hardware resources. But if capital dries up, purchase orders may disappear overnight, triggering a sell-off. OpenAI already slashed their spending plans by over 800 billion USD.&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>&lt;/p>
&lt;p>The three big hyperscalers are growing massively in revenue not least because of the huge spend by AI companies, so they don’t have to worry about decreased margins in other areas – at least for the moment. The big question will be if the revenue streams from AI services will be sufficient, once the products mature.&lt;/p>
&lt;p>In the tech industry we have been massively spoiled that costs for hardware only ever went down, and somebody else had to do all the hard work to get there. Sloppy software engineering could just gobble up all the resources. But what happens if the free ride is over?&lt;/p>
&lt;p>The cloud revolution created generations of software developers who do not know how to run on hardware, so they have to rent it from others. The AI revolution may cause developers to not know how to build software, thus having to rent it by the token. The question isn&amp;rsquo;t really whether the cloud will get more expensive: It&amp;rsquo;s how many layers of rent you will be willing to pay.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://www.hetzner.com/pressroom/statement-price-adjustment/">https://www.hetzner.com/pressroom/statement-price-adjustment/&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;a href="https://www.backblaze.com/blog/hard-drive-cost-per-gigabyte/">https://www.backblaze.com/blog/hard-drive-cost-per-gigabyte/&lt;/a> and &lt;a href="https://mkomo.com/cost-per-gigabyte">https://mkomo.com/cost-per-gigabyte&lt;/a>&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>&lt;a href="https://www.bbc.com/news/technology-15534614">https://www.bbc.com/news/technology-15534614&lt;/a>&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Assuming a current market price of 25 USD per GB of RAM and a five year depreciation schedule, while charging roughly 75 USD per GB surcharge per year.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>&lt;a href="https://www.tomshardware.com/pc-components/dram/chinas-cxmt-reportedly-delays-mass-production-of-ddr5-chips-to-late-2025-state-backed-manufacturer-could-still-be-disruptive-market-force">https://www.tomshardware.com/pc-components/dram/chinas-cxmt-reportedly-delays-mass-production-of-ddr5-chips-to-late-2025-state-backed-manufacturer-could-still-be-disruptive-market-force&lt;/a>&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>&lt;a href="https://www.reuters.com/world/china/how-china-built-its-manhattan-project-rival-west-ai-chips-2025-12-17/">https://www.reuters.com/world/china/how-china-built-its-manhattan-project-rival-west-ai-chips-2025-12-17/&lt;/a>&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>&lt;a href="https://www.cnbc.com/2026/02/20/openai-resets-spend-expectations-targets-around-600-billion-by-2030.html">https://www.cnbc.com/2026/02/20/openai-resets-spend-expectations-targets-around-600-billion-by-2030.html&lt;/a>&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Licenses: Trust and Promises</title><link>https://thomas.skowron.eu/blog/licenses-trust-and-promises/</link><pubDate>Fri, 04 Jul 2025 14:02:11 +0200</pubDate><guid>https://thomas.skowron.eu/blog/licenses-trust-and-promises/</guid><description>&lt;p>Fundamentally there are two types of perspectives inside Open Source: true believers and opportunists. Also those who still secretly think it&amp;rsquo;s a disease – but let&amp;rsquo;s leave those aside for the moment.&lt;/p>
&lt;p>I have wandered in the &amp;ldquo;true believer&amp;rdquo; FOSS fundamentalist circles for a long time, and it never felt like a practical ideology to me, so I&amp;rsquo;d consider myself an opportunist, despite having written Open Source software (OSS) for free and for money, having contributed to codebases of others and donated my own money to it and made others direct their cash.&lt;/p>
&lt;p>Either way I think that Open Source is a huge chance, irrespective of whether you are building it for yourself, for your company or just harvesting stuff made by others. And it does not have to be extremist either: Even if it&amp;rsquo;s not really Open Source, meaning under an &lt;a href="https://opensource.org/licenses">OSI license&lt;/a>, published openly, but even if it&amp;rsquo;s software that the rest of your organisation can access and send pull requests to, or if it&amp;rsquo;s code that you send to your customer after the invoice got paid. These small things can have an immense effect on the impact that your developers can have and the trust you build in and outside of your organisation.&lt;/p>
&lt;p>Even if your customers might not understand the value that you are sharing the source code with them, it might push you to produce better artifacts.&lt;/p>
&lt;p>At its core Open Source might be seen as a legal contract: You are getting a piece of code and technically speaking you can run it as long as you want. Practically speaking you will want to receive updates at some point, either to get new features or fix security problems. The very most sophisticated consumers of software may become contributors themselves: It may be more practical to develop patches in-house than to contract either the original author or switch to a different software.&lt;/p>
&lt;p>Since standard licenses are often quite simplistic – grant permissions and exclude warranty – the social contract is crucial. And all the projects that are altering the legal contracts unilaterally are putting a strain on the social aspect.&lt;/p>
&lt;p>Future expectations are central to that deal, even if they are not codified: Ten years ago when adoption of Open Source software packages was debated inside of companies, doubts about the longevity were a major contention point. This aspect has become less important, because Open Source as a whole got more accepted and stable. License choice and compatibility are more central aspects of due diligence these days.&lt;/p>
&lt;p>With the ongoing license change wave (e.g. Redis, CockroachDB, Terraform) the function and reality of OSS must be questioned: &lt;em>Why&lt;/em> are companies deciding to publish their source code? And &lt;em>why&lt;/em> do customers want Open Source?&lt;/p>
&lt;h2 id="universal-toolmaking">Universal Toolmaking&lt;/h2>
&lt;p>Tool making is a profitable business, for producers and consumers alike. People are proud of the tools they own: sometimes because a cheap tool is making one unreasonably productive or because they have bought something of extraordinary quality that enriches their enjoyment in the process. ASAKA, a Japanese tool maker, exists since 1661, primarily manufacturing spades and shovels. Their business model works for over 350 years despite not having an end-user agreement that dictates that you may not use their tools in for-profit endeavors.&lt;/p>
&lt;p>Derek Collison, founder of NATS, &lt;a href="https://changelog.com/podcast/641">bemoaned in an episode of Changelog&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> that some mega corporations have policies that block them from paying at least for certain kind of Open Source tooling. This framing makes the complaint look extremely reasonable: A huge player having an almost comically bad policy that takes the fruits of a much smaller entity and not paying anything.&lt;/p>
&lt;p>From the consuming companies&amp;rsquo; perspective they seem to be legally in the clear: The Open Source license allows them to use it as they see fit; they can even make private changes to the code since it&amp;rsquo;s licensed under Apache 2 license. Why should they pay for something they are already allowed to do? Sure, they didn&amp;rsquo;t pay for the shovel, because it&amp;rsquo;s been gifted to them.&lt;/p>
&lt;p>Customers of software businesses are often subject to absurd licensing rules: Limitations on concurrent usage, agreement to invasive audits or telemetry, and chiefly &amp;ldquo;no warranty&amp;rdquo;, aka the software vendor not guaranteeing that the product performs any function. Open Source corrects this relationship somewhat: The customer can terminate the business relationship if they are unhappy. Even if you have bought a perpetual license, only some form of OSS give you the ability to change the software after the fact.&lt;/p>
&lt;p>Upstreaming changes comes with its own troubles: Some vendors are not thrilled about (potential) customers just submitting patches. Many of them would rather make a service contract sale and develop the patch in-house from scratch, instead of inheriting a maintenance burden. But by rejecting these contributions upstreams are risking the customer creating a fork. Some vendors are not integrating the customers&amp;rsquo; patches but instead engineering their own solutions that fulfill the requirement. From the outside it looks like the original authors are the only contributors, but in the long run it is a unsustainable business practice if the customers are not paying.&lt;/p>
&lt;p>More integrated solutions are creating more value and are often able to achieve higher margins: A shovel manufacturer will have a slimmer margin than a construction company. A scrum ensues when the lower value producer (creator of Open Source Software) cannot even collect their small margin, because the upstack vendor (a public cloud company) is paying zero while selling something more useful.&lt;/p>
&lt;p>Cloud providers or even hyperconverged infrastructure vendors have little problems collecting money from the value they provide, mainly because they have gauged correctly how much of the value they are allowed to capture. Although they are at risk of these dynamics shifting, as I alluded to in my &lt;a href="https://thomas.skowron.eu/blog/go-big-or-go-onprem/">Go Big or go On-Prem piece&lt;/a>. Some Open Source Software companies are able to earn good money, but measured against the value they are creating, the capture ratio is rather low.&lt;/p>
&lt;p>From the perspective of support contracts I find the &amp;ldquo;Business Software License&amp;rdquo; (BUSL) by default, Open Source after two years construct not violently unreasonable: Many software and infrastructure contracts are negociated for a multi-year period, and after that expires they have an escape hatch: Whatever they originally bought will become Open Source after the contract ends. If the vendor turns out to be a dick or gets sold to one, you have a kind of insurance and can keep using what you&amp;rsquo;ve already got.&lt;/p>
&lt;h2 id="execution-matters">Execution matters&lt;/h2>
&lt;p>From a customer&amp;rsquo;s perspective some OSS vendors are making massive mistakes in their execution and later blaming greedy non-payers for their ills: I still find it baffling how disappointing support and sales sometimes can be.&lt;/p>
&lt;p>Open Source allows for a low-friction start: You download a package and start going. When you have to buy software, even if you have access to a trial version (which often isn&amp;rsquo;t even long enough to cover the sales process), it slows any adopter down by weeks or months.&lt;/p>
&lt;p>A customer of mine has been using the Open Source version of a database software. When we hit some issues, we tried to find a solution through support, which wasn&amp;rsquo;t able to assist us in any meaningful way. Their paid cloud offering was an underdocumented mess and wasn&amp;rsquo;t even competitive against running the open version on-prem ourselves. The sales process was slow and didn&amp;rsquo;t resolve any of our questions. In their newest version they finally decided to cripple the Open Source version and shifted focus to their enterprise offering. OSS hasn&amp;rsquo;t failed here: It&amp;rsquo;s been their strategy and execution.&lt;/p>
&lt;p>When companies close down their source it is easy to be cynical and claim a money grab or see their open efforts as a way to lure customers into their captivity. It definitely tarnishes the reputation of these companies, because it will be much harder to believe them that they will not alter the deal even further. The customers, especially these of VMWare or Oracle, already know this asymmetry of power. And the aftermath of the Terraform agreement change and the emergence of a decentralized OpenTofu shows that this kind of rug-pull may have unintended consequences.&lt;/p>
&lt;p>Companies like Tailscale are showing how it&amp;rsquo;s done: Their product is building upon Wireguard (an Open Source VPN software), and an eager technologist might argue that you could just use that. But their execution makes it so easy to pick them instead: You can create an account using self-service, much of the software they are offering is Open Source, you can even use a different backend. If you want to go further, their support staff can actually help and their sales is proactive. They are pushing themselves to provide more value to their customers, so they don&amp;rsquo;t have to lock you in.&lt;/p>
&lt;p>If you are a &amp;ldquo;pure play&amp;rdquo; software company, launching your product as Open Source and hoping to make it a sustainable business through e.g. enterprise support contracts, I do still think that it&amp;rsquo;s possible, but your fundamentals need to be absolutely solid: Your sales staff needs to be aware what they are up against, your support needs to know how to effectively help. Your strategy must be somewhat transparent or consistent. Your investors need to know that monetisation strategies may not be straight forward. Don&amp;rsquo;t trade a boost in quarterly numbers for reputation.&lt;/p>
&lt;p>If you are on the other side and are enjoying the benefits of Open Source, think about where you already spend your money: Are you maybe paying for a cloud account that doesn&amp;rsquo;t have good support and doesn&amp;rsquo;t finance the underlying OSS? Maybe paying for the hosted solution offered by the producer of the software makes more sense? If you are spending big bucks for a support or enterprise contract of an Open Source package, codify that the software will actually stay Open Source.&lt;/p>
&lt;p>Whether you are a producer or a consumer of Open Source Software: Be mindful not to overextract value. If you do, the market may overcorrect.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&amp;ldquo;But we are starting to see a very disturbing trend where customers that everyone would recognize - they’re in the Fortune 50 - that are using NATS to power production-level services or functions or products, not only had never reached out for any type of commercial agreement with us, but actually have policies that say &amp;lsquo;If you’re in the CNCF and you’re incubating and graduating, we’re not paying for it, period.&amp;rsquo;&amp;rdquo;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Go Big or go On-Prem</title><link>https://thomas.skowron.eu/blog/go-big-or-go-onprem/</link><pubDate>Mon, 02 Jun 2025 15:38:00 +0200</pubDate><guid>https://thomas.skowron.eu/blog/go-big-or-go-onprem/</guid><description>&lt;p>Probably my favourite thing about the &lt;a href="https://world.hey.com/dhh/we-have-left-the-cloud-251760fb">&amp;ldquo;Cloud Exit&amp;rdquo; from Basecamp&lt;/a> that DHH is so vociferously writing about is that it has shifted the predisposition about Cloud usage. For many years it seemed there was no choice: you created an AWS, Google Cloud or Azure&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> account and started creating resources. Who the hell is Dell? What&amp;rsquo;s a hypervisor?&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/go-big-or-go-onprem/wa-11.jpeg" alt="clouds covering the valley">
&lt;/figure>
&lt;p>But economic realities have kicked in and people are starting to wonder why they spend as much on their Cloud bill as it would cost to buy a full rack of servers while employing even more operations staff.&lt;/p>
&lt;h2 id="on-cloud-prem-cloud">On-Cloud, Prem-Cloud&lt;/h2>
&lt;p>On-Prem never really went away; depending on the survey, it seems that at least half of computing workloads are still hosted in a private data centre instead of the public cloud. And of course, you might argue what &amp;ldquo;Cloud&amp;rdquo; even means: is it about the technology, the economic aspect, or culture? If you can autonomously provision your workloads on a cluster that&amp;rsquo;s managed by somebody else and is running on machines inside your organisation, it feels like Cloud to you, but has a pay-to-own cost structure.&lt;/p>
&lt;p>Conversely, if your organisation uses the services of a Cloud provider, but you don&amp;rsquo;t do DevOps and software cannot be deployed without operator intervention, you might be spending Cloud-money without embracing the culture and reaping the benefits.&lt;/p>
&lt;p>&lt;a href="https://oxide.computer">Oxide&lt;/a> advertises themselves as &amp;ldquo;The cloud you own&amp;rdquo; as their product is a rack-scale computer that can be provisioned like a public cloud account, using e.g. Terraform. You don&amp;rsquo;t rent it, you buy it once and run it as long as it&amp;rsquo;s feasible – and I bet that&amp;rsquo;s longer than most people would expect.&lt;/p>
&lt;p>And then there are combinations which never really made sense to me&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>: buying a bunch of servers and then paying heaps of yearly licence fees for every layer in the software stack – hypervisor, operating system, storage system, etc. Especially since none of those vendors is able or willing to solve your problems in case the shit hits the fan.&lt;/p>
&lt;p>My biggest gripe is the low-levelness of many solutions that e.g. AWS offers: it takes astonishing complexity to get anything going. Configuring a setup on EC2 might be quite easy, but it means that you don&amp;rsquo;t get many of the benefits. You want to add a load balancer? Prepare to configure VPCs and subnets and manage multiple availability zones, and oh: don&amp;rsquo;t forget to add CloudTrail and CloudWatch!&lt;/p>
&lt;p>Of course, with all these tools stacked up, your bill will too. Your trivial workload incurs a non-trivial cost. Also, most of the cloud setups I encounter are massively overprovisioned, just in case. It&amp;rsquo;s great that your development team doesn&amp;rsquo;t need to count CPU cycles, and can just write Python, but when you only utilise 10% of already expensive compute, you&amp;rsquo;re not getting any of the savings.&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> Development environments could be shut off overnight, when nobody is using them, but getting this process in place seems to cost more than it saves in usage.&lt;/p>
&lt;p>Even solutions like Elastic Container Service still require a lot of prefabrication: you don&amp;rsquo;t have to manage EC2 instances anymore, but now one has to figure out how to connect your container registry. It might make it easier to scale or to add redundancy, but the complexity is just moved around at best.&lt;/p>
&lt;p>Kubernetes might be more provider-agnostic&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>, but I don&amp;rsquo;t think that anybody has ever made the argument that it is non-complex. And boy, do I see many setups that do not warrant its complexity. If the number of microservices outnumbers the number of developers, &lt;a href="https://www.youtube.com/watch?v=y8OnoxKotPQ">you already have a problem&lt;/a>.&lt;/p>
&lt;h2 id="above-the-cloud">Above the Cloud&lt;/h2>
&lt;p>Some people said &amp;ldquo;fuck all of that&amp;rdquo; and started moving into more upstack, but lightweight solutions, like AWS Lambda. If your databases, storage and load balancers are already run by your cloud provider, you can go a step further and eliminate your internal complexity. Write a piece of JavaScript, Go or Python, upload it to AWS, and they host it for you.&lt;/p>
&lt;p>As with everything, there are trade-offs: at the low end, cost is lower than spinning up any EC2 instance, but it can eat through any IT budget at the high end. You can start quicker, but you are locked into your provider. You can use a familiar programming language, but not necessarily any framework.&lt;/p>
&lt;p>Heroku was truly ahead of their time: they certainly made mistakes, in technology as in business, but connecting your Git repo and getting your software deployed in minutes was truly groundbreaking. Unfortunately, in 2015 one half was building the next Instagram so they thought they&amp;rsquo;d outgrow Heroku within seconds and reached for more grown-up cloud solutions, the others were still SFTP-ing PHP scripts to their server. Maybe the world simply wasn&amp;rsquo;t ready yet.&lt;/p>
&lt;h2 id="its-all-about-the-benjamins">It&amp;rsquo;s all about the Benjamins&lt;/h2>
&lt;p>The new generation of more integrated Cloud offerings has better chances: borrowing money isn&amp;rsquo;t as cheap as it was, companies don&amp;rsquo;t want to hire additional Cloud operators, and developers shall be more productive. People are reaching for more managed tooling: Vercel is already a big hit, despite the underlying framework, Next.js, being an absolute nightmare. Quality of integration is more important than quality of your individual components.&lt;/p>
&lt;p>With services like fly.io, the operational burden stays low while offering a lot of options. If you&amp;rsquo;ve got a Dockerfile, you can get your service running in mere minutes and it&amp;rsquo;s almost trivial to scale to multiple regions while not being more expensive than AWS et al.&lt;/p>
&lt;p>I absolutely get it when people do the maths and conclude that buying and running their own hardware makes sense. I also agree that paying for an integrated service is great. But shelling out top dollar for furniture that you need an expert to assemble and to keep together?&lt;/p>
&lt;p>As I wrote in my &lt;a href="https://thomas.skowron.eu/blog/deutsche-cloud">previous piece&lt;/a>, the big cloud providers have one enormous advantage: unified and centralised billing. CFOs across the globe still think they made a killer deal when the Cloud vendor offers them 20% off or gives them a five or six-figure discount, but these are not acts of charity – they just make a slight dent in the long run. Make no mistake: they still have obscene margins, and there is a lot of room for negotiation. Especially egress is a money printer.&lt;/p>
&lt;p>If they don&amp;rsquo;t want to make you a seemingly generous offer, they either think that you&amp;rsquo;re not going to grow or that your organisation is too incapable to switch to a different vendor.&lt;/p>
&lt;p>It&amp;rsquo;s much easier to add another ELB than to get the company credit card to sign up with a new provider. As a side note, if you are not letting your staff create new resources autonomously without an architecture review board getting involved, it doesn&amp;rsquo;t make sense to be in the Cloud at all. A strategy to prevent getting too complacent can be personal budgets for services that developers can use freely to spend with other providers without going through a massive purchasing process.&lt;/p>
&lt;p>Computer hardware has become much faster &lt;strong>and&lt;/strong> cheaper, but many of the providers do not pass the savings on to the customer. AWS used to reduce prices on a regular schedule, but those times are over. EC2 prices have at best stayed flat, while Amazon runs their hardware for much longer these days. It&amp;rsquo;s a marker of their success that they are dominating the market, but also that CTOs have become a bit blind to the fundamentals. Even I was stumped when I realised that the smallest VM instance at Hetzner now offers 4GB of RAM, at just under 4 EUR (USD ~4.50) per month!&lt;/p>
&lt;p>It&amp;rsquo;s a matter of incentives, too: technologists are not really pushed towards being cost-efficient. Setting up a Kubernetes cluster may be driven more by the desire to be able to put it on the CV than to improve reliability or speed.&lt;/p>
&lt;p>The technology sector, as unconventional as it can be, has largely kept sales quite traditional: lower base salary, bigger commission. Which kind of makes sense – when sales gets a win, the whole company profits. With technical roles, one could argue the same: when a developer cuts the Cloud bill, everybody saves. But all these savings will end up, at best, in the shareholders&amp;rsquo; pockets. Only when it&amp;rsquo;s almost too late, management will start rampaging and trying to cut down, starting to put every line item in question.&lt;/p>
&lt;p>I think a more balanced leadership approach needs to be adopted that emphasises more mid-term thinking: the long term is overvalued in my opinion – some C-level staff try to imagine a wild future vision, where they are AI-first or whatever makes them look smart or gets them through their next funding round. The short term is dominated by reactive firefighting. In between, you can push for a better discount with your Cloud provider, move some workloads either on-prem or onto servers you own, push for better utilisation or move to higher value, cheaper maintenance all-in-one providers. Employ people who are smart and adaptable, not those who just have the right things on the CV you are currently using. Chances are, your business will need to change more than it needs the current crop of tech.&lt;/p>
&lt;p>AWS likes to emphasise in their training materials the dangers of &amp;ldquo;undifferentiated heavy lifting&amp;rdquo; in which you are doing things that don&amp;rsquo;t improve your product, but require your attention. If you look at the heavy lifting that you need to do to satisfy AWS&amp;rsquo;s well-architected framework, one could argue that you shouldn&amp;rsquo;t be a customer there.&lt;/p>
&lt;p>&lt;em>How is your strategy going? Are you going more upstack or doubling down on on-prem? I&amp;rsquo;d love to hear from your experiences!&lt;/em>&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>God help us&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Despite some people&amp;rsquo;s best efforts to explain it to me like I am five.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>A 10% utilisation also means that you are not paying a 5x premium on the compute – you are paying 50x!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>It isn&amp;rsquo;t though for most.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Deutsche Cloud</title><link>https://thomas.skowron.eu/blog/deutsche-cloud/</link><pubDate>Thu, 15 May 2025 16:23:00 +0200</pubDate><guid>https://thomas.skowron.eu/blog/deutsche-cloud/</guid><description>&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/deutsche-cloud/flag-wanderer.jpeg" alt="Man wrapped in german flag looking at clouds">
&lt;/figure>
&lt;p>Due to the unique political situation we are living in, there is an increased demand for European technology solutions. Call it derisking, sovereignty, or nationalism. Everybody seems to be at least pretending to look for new solutions from within.&lt;/p>
&lt;p>The German IT industry is classically laggard: while the rest of the world is beginning to understand that the benefits of cloud computing are technological, not economic, in Germany, all the rage seems to be in public cloud these days. In my local government, everybody seems to have learned the word &amp;ldquo;hyperscaler&amp;rdquo; recently and doesn&amp;rsquo;t hesitate to repeat it relentlessly with limited understanding.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>When Microsoft builds a data center in Germany, everybody seems to ignore all the nasty details and declares it EU-cloud, congratulating themselves on getting the deal.&lt;/p>
&lt;p>A particularly strange public sentiment I&amp;rsquo;ve encountered is the admiration of the Schwarz Group IT division &amp;ldquo;Schwarz Digits&amp;rdquo; and their brand &amp;ldquo;STACKIT&amp;rdquo;. The Schwarz Group is the holding company of one of the biggest retailers in Germany. In 2022, they entered the limelight by starting a public cloud offering that they built based on their own needs and opened to the general public. A retailer building their own IT services and selling them as a service? How 2002!&lt;/p>
&lt;p>I don&amp;rsquo;t mind people taking affairs into their own hands and building their own thing: buy hardware to run your software on? Sounds great, I&amp;rsquo;m in! But if you look at it for more than a few seconds, the details are massively underwhelming.&lt;/p>
&lt;p>The service palette itself is a little bit sparse: aside from quite low-level features like virtual machines and block storage, they claim to run a handful of databases and other managed services. They offer only one message queue and it&amp;rsquo;s RabbitMQ, an odd choice.&lt;/p>
&lt;p>There is no transactional email service, and their workspace offering is just Google Workspace in a trench coat.&lt;/p>
&lt;p>The underpinnings are based on OpenStack: a sign of either cluelessness or insanity. I just hope they aren&amp;rsquo;t also paying for VMware.&lt;/p>
&lt;p>Pricing is a bit opaque, despite their affirmation to be transparent, but the calculator produces prices somewhere between two and five times more expensive than the competition. All cloud providers are able to cut special deals with significant discounts, so real costs may vary. It&amp;rsquo;s certainly a different strategy than their retail one, where affordability is paramount.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/deutsche-cloud/eu01.png" alt="EU01 Region Layout">
&lt;/figure>
&lt;p>The operational side doesn&amp;rsquo;t impress either. They currently have one region: &amp;ldquo;Heilbronn/Deutschland&amp;rdquo; or &amp;ldquo;eu01&amp;rdquo;, as their documentation likes to call it. They claim it consists of at least three availability zones (AZs), but then backtrack and say that the &lt;em>one&lt;/em> region they operate has &lt;em>exactly&lt;/em> three AZs.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/deutsche-cloud/lb-ipv6.png" alt="Load Balancer without IPv6 support">
&lt;p class="small">Screenshot from STACKIT docs&lt;/p>
&lt;/figure>
&lt;p>Network and power look disappointing too: while Meta, AWS, and Google started building their own network gear ages ago, STACKIT doesn&amp;rsquo;t even seem to fully support IPv6. Their colocation offer hints at a very old-school 19-inch rack setup with a pitiful 2 kW supply for 47U. At least you get a three-year contract, that&amp;rsquo;s a good thing, right?&lt;/p>
&lt;p>I am cherry-picking here, obviously, but the colocation market in Germany is fiercely competitive.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/deutsche-cloud/colocation.png" alt="Colocation Offering">
&lt;/figure>
&lt;p>Still interested? You can&amp;rsquo;t just sign up and get going: you have to apply for an account in true bureaucratic fashion. If you&amp;rsquo;re outside of the German-speaking part of the world, you will be subject to extended interrogation. Even if you sign up with a valid VAT ID, your account creation will have to be manually approved first. Mine is still pending. (&lt;strong>Update:&lt;/strong> My account application got rejected after they asked me to submit a credit report from my bank. I am worthy enough to have credit cards or finance a house, but not to open a public cloud account.)&lt;/p>
&lt;p>I&amp;rsquo;m sure the next generation of developers will patiently wait to incorporate a company and wait for your confirmation email, instead of using Google Cloud&amp;rsquo;s free tier.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/deutsche-cloud/european-hyperscaler.png" alt="European Hyperscaler">
&lt;p class="small">Screenshot from LinkedIn&lt;/p>
&lt;/figure>
&lt;p>In late 2024, they hired the former managing director of Google Cloud Germany to become the CEO of STACKIT. In a LinkedIn post, he described STACKIT as a &amp;ldquo;sovereign European hyperscaler&amp;rdquo;. Their &amp;ldquo;About Us&amp;rdquo; page, on the other hand, adds &amp;ldquo;Swabian&amp;rdquo;&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> and &amp;ldquo;German&amp;rdquo; to their labels. As if wrapping yourself in one flag wouldn&amp;rsquo;t suffice, they wrap themselves in multiple.&lt;/p>
&lt;p>I wish them all the best, honestly. They run open source infrastructure to some extent and have opened up what could otherwise just have been an internal project. But the total lack of depth and the marketing focus on some self-defined sovereignty seems like a total miss. A narrative often presented in Germany is that Americans can advertise themselves very well without substance. I must say that some people in Germany could accidentally become number one in that regard.&lt;/p>
&lt;p>It&amp;rsquo;s not 2002 anymore. There is plenty of competition on the market, some of it EU-based (EU regions of any public cloud), truly European (e.g., OVH), or German (e.g., Hetzner). There are more batteries-included solutions too (e.g., Fly.io). And increasingly, on-prem: with ever-larger cloud bills and the realization that owning instead of renting can be significantly cheaper, it will not be an easy battle for those who lack ambition.&lt;/p>
&lt;p>I think a major strategic advantage of the big public cloud providers is (billing) integration: not even a vigilant CFO will mind a 20 Euro line item on a cloud bill, while an invoice from a better or cheaper new vendor may raise questions. Joining the cloud market requires a gigantic amount of capital: you need the bones, like VMs and storage, but you also need enough higher-stack services to satisfy the needs of a large number of customers. I think that Fly.io took the smarter route by partnering with select providers of high-value services, like Tigris or Upstash, while centralizing billing. Fly.io also own their hardware, but not their data centers.&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>&lt;/p>
&lt;p>If you don&amp;rsquo;t compete on variety, depth, or value, what&amp;rsquo;s left is to be proud of your heritage. If someone on my street started offering lukewarm, stale beer for 10 Euro, I wouldn&amp;rsquo;t be proud, I would experience &lt;a href="https://en.wiktionary.org/wiki/Fremdscham">Fremdscham&lt;/a>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://www.rhein-erft-kreis.de/aktuelles/meldungen/2024-Hyperscaler-Ansiedlung-in-Bedburg-und-Bergheim.php">https://www.rhein-erft-kreis.de/aktuelles/meldungen/2024-Hyperscaler-Ansiedlung-in-Bedburg-und-Bergheim.php&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Swabia is a region in southern Germany, probably most famous for an unintelligible accent and penny-pinching abilities.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>They can still build data centers later, if they get wildly successful, since they own so much of their own stack.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Keeping your OpenStreetMap Database Replicated</title><link>https://thomas.skowron.eu/blog/osm2pgsql-logical-replication/</link><pubDate>Wed, 16 Apr 2025 14:14:00 +0200</pubDate><guid>https://thomas.skowron.eu/blog/osm2pgsql-logical-replication/</guid><description>&lt;p>The physical world is changing all the time, and so is OpenStreetMap (OSM). Luckily, you can just tap the source by using the replication system. &lt;a href="https://osm2pgsql.org">osm2pgsql&lt;/a> can import your OSM data into PostgreSQL and also update from that replication stream.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>If you want to scale out your database because you want capacity and/or redundancy, you need a strategy to orchestrate this process. One option is to run the import and update process on each Postgres node.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/osm2pgsql-logical-replication/b1.png" alt="importer on each node">
&lt;/figure>
&lt;p>This has multiple drawbacks: due to the unique way the OpenStreetMap data format is structured, you need to keep a large state file to be able to update your imported database on each node.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> Also, the update process itself is quite resource-intensive.&lt;/p>
&lt;h2 id="one-importer-multiple-replicas">One importer, multiple replicas&lt;/h2>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/osm2pgsql-logical-replication/b2.png" alt="one importer">
&lt;/figure>
&lt;p>A better alternative is to run one osm2pgsql instance that performs the initial import and continuous updates while all other PostgreSQL instances are read replicas. This can be performed using the built-in logical replication, which started shipping with PostgreSQL 10.&lt;/p>
&lt;p>This offloads the update process to one node, while all replicas get a preprocessed Postgres logical replication stream, just applying the data updates.&lt;/p>
&lt;p>To get your database ready for replication, a few steps are required. In the following guide, we assume that you want to publish the tables containing points, lines, roads, and polygons, which are required for e.g. rendering using Mapnik.&lt;/p>
&lt;h2 id="1-replica-identities">1. Replica Identities&lt;/h2>
&lt;p>Logical replication propagates changes to the downstream databases at a high level. To be able to apply an &lt;code>UPDATE&lt;/code>, it must be clear which rows this applies to. Since logical replication, as opposed to streaming replication, can be partial – not all tables or columns or even rows have to be replicated&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> – this requires a row identity.&lt;/p>
&lt;p>Usually, you can just use a &lt;code>PRIMARY KEY&lt;/code>, which most tables have. Alas, this is not the case with a default osm2pgsql import. You can either &lt;a href="https://osm2pgsql.org/doc/manual.html#using-an-additional-id-column">add one using a flex config&lt;/a> or do it yourself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_point&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">COLUMN&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#999">SERIAL&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">PRIMARY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_line&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">COLUMN&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#999">SERIAL&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">PRIMARY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_roads&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">COLUMN&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#999">SERIAL&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">PRIMARY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_polygon&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">COLUMN&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#999">SERIAL&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">PRIMARY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then you can indicate to PostgreSQL that you want to use these as replica identities:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_point&lt;span style="color:#bbb"> &lt;/span>REPLICA&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">IDENTITY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DEFAULT&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_line&lt;span style="color:#bbb"> &lt;/span>REPLICA&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">IDENTITY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DEFAULT&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_roads&lt;span style="color:#bbb"> &lt;/span>REPLICA&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">IDENTITY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DEFAULT&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_polygon&lt;span style="color:#bbb"> &lt;/span>REPLICA&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">IDENTITY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DEFAULT&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="2-publication">2. Publication&lt;/h2>
&lt;p>You can now mark these tables as available for replication. Make sure that your &lt;code>wal_level&lt;/code> is sufficient: it must be set to &lt;code>logical&lt;/code> in your &lt;code>postgresql.conf&lt;/code>.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>&lt;/p>
&lt;p>To create the publication:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>PUBLICATION&lt;span style="color:#bbb"> &lt;/span>osm_pub&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">FOR&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_point,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_line,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_roads,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>planet_osm_polygon;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="3-prepare-for-subscribing">3. Prepare for Subscribing&lt;/h2>
&lt;p>The logical replication facility only copies data, not the schema. Therefore, you need to dump the schema for the tables you want to replicate and create it on your downstream nodes.&lt;/p>
&lt;p>Get the schema from your upstream:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pg_dump &lt;span style="color:#b84">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span> -t planet_osm_point &lt;span style="color:#b84">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span> -t planet_osm_line &lt;span style="color:#b84">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span> -t planet_osm_roads &lt;span style="color:#b84">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span> -t planet_osm_polygon &lt;span style="color:#b84">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span> --schema-only &amp;gt; ddl.sql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Beware of &lt;code>TRIGGER&lt;/code> and &lt;code>GRANT&lt;/code>: these apply to your upstream but possibly not your read replicas.&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> I use a little &lt;code>sed&lt;/code> to remove them from the script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#b84">&amp;#39;/^CREATE TRIGGER/ d&amp;#39;&lt;/span> ddl.sql
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#b84">&amp;#39;/^GRANT SELECT/ d&amp;#39;&lt;/span> ddl.sql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lastly, import the schema on your downstream(s):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>psql -f ddl.sql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="4-subscribe">4. Subscribe&lt;/h2>
&lt;p>Your downstreams are ready to subscribe. You must be able to reach your upstream from your downstreams.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>SUBSCRIPTION&lt;span style="color:#bbb"> &lt;/span>osm_sub&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">CONNECTION&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#b84">&amp;#39;host=your.upstream.local port=... user=... password=... dbname=...&amp;#39;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>PUBLICATION&lt;span style="color:#bbb"> &lt;/span>osm_pub;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PostgreSQL will start importing table by table into your replica and then keep up with updates. The initial import may take a while.&lt;/p>
&lt;p>You can observe the process from your PostgreSQL log:&lt;/p>
&lt;pre>&lt;code>logical replication apply worker for subscription &amp;quot;osm_sub&amp;quot; has started
&lt;/code>&lt;/pre>
&lt;p>And the &lt;code>pg_subscription&lt;/code> table:&lt;/p>
&lt;pre>&lt;code>SELECT * FROM pg_subscription;
oid | subdbid | subskiplsn | subname | subowner | subenabled | subbinary | substream | subtwophasestate | subdisableonerr | subpasswordrequired | subrunasowner | subfailover | subconninfo | subslotname | subsynccommit | subpublications | suborigin
-----+---------+------------+---------+----------+------------+-----------+-----------+------------------+-----------------+---------------------+---------------+-------------+-----------------------------------------------------------------------------------------+--------------------+---------------+-----------------+-----------
5555 | 1234 | 0/0 | osm_sub | 10 | t | f | f | d | f | t | f | f | host=10.1.1.2 port=5432 user=y password=x dbname=osm | osm_sub_1234 | off | {osm_pub} | any
(1 row)
&lt;/code>&lt;/pre>
&lt;p>If the upstream is unavailable, replication will pause and resume as soon as it becomes available again.&lt;/p>
&lt;h2 id="closing-thoughts">Closing Thoughts&lt;/h2>
&lt;p>I originally started writing this article five years ago when I first built this system, but I didn’t feel qualified enough to finalize it. The partners I built this with were much more experienced in streaming replication, but I stubbornly pushed for logical replication since the underlying mechanism has the aforementioned benefits and &lt;em>feels&lt;/em> much more modern.&lt;/p>
&lt;p>This approach definitely has drawbacks, too—most notably that you cannot promote a new upstream/primary, which is possible if you use streaming replication and something like &lt;code>repmgr&lt;/code>. But since the database can be recreated at any point by just re-importing from source (which has the nice side-effect of getting rid of index bloat), this is quite acceptable in my opinion.&lt;/p>
&lt;p>After shipping this to a customer, and also running it myself for multiple years, it has been running without adding any drama. I am only moonlighting as a DBA sometimes, so simple and effective solutions that don&amp;rsquo;t wake me up at 3 am are my favorites—and this is definitely one of those.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>It must be noted that PostgreSQL is kind of a bad match for such workloads, and an OLAP-optimized DB would probably be a better fit, but PostGIS is extremely powerful when you want to work with geospatial data. I worked on geospatial systems that would make a database unnecessary, but losing access to PostGIS made that unfeasible – at least for my resources and skill level.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Almost 100GB at the time of writing.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>If you already know exactly which tables or expressions you need on your replicas, you can restrict the data that will be published: adjust the &lt;code>CREATE PUBLICATION&lt;/code> in section 2.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>&lt;a href="https://www.postgresql.org/docs/current/runtime-config-wal.html#RUNTIME-CONFIG-WAL-SETTINGS">https://www.postgresql.org/docs/current/runtime-config-wal.html#RUNTIME-CONFIG-WAL-SETTINGS&lt;/a>&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Triggers are used by osm2pgsql, but you do not need them on your read replicas, since these will not run the import/update process. Grants are for permissions, but your permission set on replicas may be different from the upstream.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Typst is Weird and I Really Like It</title><link>https://thomas.skowron.eu/blog/typst-is-weird-and-i-really-like-it/</link><pubDate>Tue, 01 Apr 2025 15:55:00 +0200</pubDate><guid>https://thomas.skowron.eu/blog/typst-is-weird-and-i-really-like-it/</guid><description>&lt;p>For reasons not entirely understood to humankind, some software developers have an aversion to &lt;a href="https://en.wikipedia.org/wiki/WYSIWYG">WYSIWYG&lt;/a>: Typing into Microsoft Word&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and immediately seeing what the document looks like seems strangely alien. In some cases, this is warranted: If you write a 300-page thesis, modern desktop software is not able to cope with it, while being perfectly able to play back 4K video that is being streamed live from some remote data centre.&lt;/p>
&lt;p>As every self-respecting nerd in their adolescence, I experimented with LaTeX, a typesetting system that generates documents from plain-text input: it&amp;rsquo;s this promised land of perfect typography which has an aura of a scientific mindset. But to be honest, the whole thing is something for frizzy, grey-haired hippies: The syntax is horrifying, the tooling is byzantine, and adding a character that is not in the English alphabet can lead to a total meltdown.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> Also, the typographical output is not as good as many people claim it to be.&lt;/p>
&lt;p>For publishing on the web, &lt;a href="https://daringfireball.net/projects/markdown/">Markdown&lt;/a> came into dominance. And it&amp;rsquo;s not bad if your name is John Gruber and you never need to author a table. I do not want to be too harsh on Markdown; this post has been composed in it!&lt;/p>
&lt;p>But it gets tricky when you write a &lt;em>&amp;ldquo;document&amp;rdquo;:&lt;/em> be it a manual, tech documentation, an invoice or similar. You want a table of contents? Variables that you can reference? Insert a formula? Tough luck, you will be scripting.&lt;/p>
&lt;p>&lt;a href="https://asciidoctor.org">Asciidoc&lt;/a> has an acceptable syntax and promises more features, but it&amp;rsquo;s often unclear what is part of the language and what is part of one of the implementations. Tables of contents, for example, are handled very differently depending on whether you use Asciidoctor for generating an HTML file or Asciidoctor-PDF for creating a PDF. Especially the latter often feels half-baked.&lt;/p>
&lt;p>I need to generate PDF documents from structured data on a regular basis, so I cobbled together a workflow consisting of YAML files for data input, a &lt;a href="https://jinja.palletsprojects.com/en/stable/">Jinja&lt;/a> template that produces HTML, and then a little bit of Python code that combines those two and feeds it to &lt;a href="https://weasyprint.org">WeasyPrint&lt;/a>, which generates a PDF file. It works, but I don&amp;rsquo;t really love it.&lt;/p>
&lt;p>A while ago, I stumbled upon &lt;a href="https://typst.app">Typst&lt;/a>, which looked to me like an online editor for scientific writing but is, in fact, a typesetting system. It has a reasonable markup language, generates PDFs, is fast, and doesn&amp;rsquo;t litter your file system with intermediate files.&lt;/p>
&lt;p>It allows a lot of scripting inside the markup, which makes it a perfect match for data-driven documents.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>Jon Doe&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#000080">sandwiches&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- Chicken Teriyaki&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- Philly Cheese Steak&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- Salami&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-typst" data-lang="typst">#let data = yaml(sys.inputs.path)
= Sandwich Documentation
Hi #data.name! Our records indicate that your favourite
sandwiches are
#data.sandwiches.join(&amp;#34;, &amp;#34;, last: &amp;#34; and &amp;#34;).
&lt;/code>&lt;/pre>&lt;p>glued together with a&lt;/p>
&lt;pre>&lt;code>typst w doc.typ --input &amp;quot;path=sandwich.yaml&amp;quot;
&lt;/code>&lt;/pre>
&lt;p>gives you a PDF document that looks something like this:&lt;/p>
&lt;p>&lt;img src="doc.svg" alt="Visualisation of a Typst output">&lt;/p>
&lt;p>And while normal text looks very reasonable if you are used to the usual markup languages, you can use all sorts of tricks to generate a specific output. You have a number that you want to display as currency? Try this:&lt;/p>
&lt;pre tabindex="0">&lt;code>#let as_currency(value, round: true) = {
let v = value
if round {
v = calc.round(decimal(value), digits: 2)
}
let s = data.currency + &amp;#34;\u{00A0}&amp;#34; + str(v)
if s.ends-with(regex(&amp;#34;\.\d&amp;#34;)) {
s += &amp;#34;0&amp;#34;
} else if not s.ends-with(regex(&amp;#34;\.\d*&amp;#34;)) {
s += &amp;#34;.00&amp;#34;
}
s
}
&lt;/code>&lt;/pre>&lt;p>If you spend too much time in Typst, your document may look more like a program than text sprinkled with markup, but that&amp;rsquo;s maybe what you wanted and what you&amp;rsquo;re most familiar with. A kind of strangeness that&amp;rsquo;s beautiful – and fun.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Better document processors are, of course, also available.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>WTF is &lt;code>{&amp;quot;o}&lt;/code>?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Journaling for Productivity</title><link>https://thomas.skowron.eu/blog/journaling-for-productivity/</link><pubDate>Thu, 27 Mar 2025 11:43:11 +0200</pubDate><guid>https://thomas.skowron.eu/blog/journaling-for-productivity/</guid><description>&lt;p>A technique that helped me focus is journaling, by hand, on paper. I can assure you that the first time I heard about journaling in a professional context, I must&amp;rsquo;ve rolled my eyes violently. But I&amp;rsquo;ve been doing it consistently for over a year, and I feel that it has finally stuck. I miss it when I leave my notebook somewhere.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/journaling-for-productivity/L1004123.jpg" alt="Red Notebook">
&lt;/figure>
&lt;p>The strategy is quite simple: I note the date and the project I am working on and start writing down what I&amp;rsquo;m doing or what I&amp;rsquo;m thinking about. When I feel that I am running in circles, I write down my conundrum. When I&amp;rsquo;ve achieved something, I write it down.&lt;/p>
&lt;p>When I used to have a meeting, I&amp;rsquo;d pull out a sheet of paper, write down my notes, and then put it to the side. The effect was a stack of loose sheets with random notes. What meeting? When? Nobody really knows. My poor in-meeting notetaking skills, compounded with a total lack of structure, made it difficult to keep track. With a chronological log, it&amp;rsquo;s much simpler: There is a date on the sheet. I don&amp;rsquo;t lose it. When I flip through the pages, I know the mental context I&amp;rsquo;ve been in.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/journaling-for-productivity/4D2A5240.jpg" alt="TODO">
&lt;/figure>
&lt;p>What I write down isn&amp;rsquo;t official or polished or ready to be shared with anybody, but this provides me with a certain kind of freedom. It allows me to forget, because it&amp;rsquo;s already written down.&lt;/p>
&lt;h2 id="deciding-on-something">Deciding on Something&lt;/h2>
&lt;p>&amp;ldquo;Something with A didn&amp;rsquo;t feel right, B wasn&amp;rsquo;t our preferred choice either, so I went with C,&amp;rdquo; is a common template for summarizing a decision. The more time that has passed since the decision, the murkier the arguments become.&lt;/p>
&lt;p>Now, when I do solution discovery, I just write down everything in real-time. When I need to compile a report later or revisit my decision because some externality has changed, I go back to my notes.&lt;/p>
&lt;p>It also takes away the pressure to remember non-critical details. As soon as it&amp;rsquo;s written down, my mind is allowed to drop it, and I can move on to the next thing immediately.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> If a customer asks what I&amp;rsquo;ve billed for, I don&amp;rsquo;t have to reconstruct it by combing through emails or my Git commits; I can simply go through my journal log.&lt;/p>
&lt;p>It&amp;rsquo;s not perfect; the sorting is chronological, and then there is a project title, but there is no search function – it&amp;rsquo;s just paper, after all. But I don&amp;rsquo;t really want something digital. I own an iPad Pro with a Pencil, but it&amp;rsquo;s another attention-grabbing device. I have looked at the reMarkable tablets, but my handwriting has gotten rather messy, and I don&amp;rsquo;t see the point in digitizing my notes unless it gets to a machine-readable state.&lt;/p>
&lt;p>My paper notebook doesn&amp;rsquo;t need software updates, it can&amp;rsquo;t display notifications, I don&amp;rsquo;t have to pay a monthly fee, and the worst thing that can happen is that I run out of ink.&lt;/p>
&lt;p>It took me a while to make it stick. It wasn&amp;rsquo;t a success right away, and it certainly takes discipline, like any other habit. For me, the right format and a nice pen made it more pleasant.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> Compared to more radical productivity ideas like committing yourself to a monastery or waking up at five and taking cold showers, I can assure you it&amp;rsquo;s cheaper and definitely worth a try, but do give it a bit of time to sink in!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>There is a concept called &lt;em>attention residue&lt;/em> (&lt;a href="https://www.sciencedirect.com/science/article/abs/pii/S0749597809000399">Leroy 2009&lt;/a>) that describes how switching to a new task is hindered by the previous one. Writing a thought down helps me to disengage from the old task.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>If you want to replicate my &amp;ldquo;setup&amp;rdquo;: It&amp;rsquo;s a LEUCHTTURM1917 Hardcover Medium (A5), Fox Red, checkered and a Parker Jotter Stainless Steel with Golden Finish.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Getting Back into Writing on My Own Terms</title><link>https://thomas.skowron.eu/blog/getting-back-to-writing-publicly/</link><pubDate>Thu, 13 Mar 2025 13:11:00 +0200</pubDate><guid>https://thomas.skowron.eu/blog/getting-back-to-writing-publicly/</guid><description>&lt;p>For the last five years, I&amp;rsquo;ve probably been writing more than ever, which you wouldn&amp;rsquo;t guess based on my publication schedule on my very own website (spoiler: it&amp;rsquo;s been four years!). Most of it has happened internally somewhere: multi-page proposals, architecture concepts, or just pull request texts and Slack messages of epic length.&lt;/p>
&lt;p>In the last few years of my work, there were only a couple of weeks when I spent working with somebody at the same place at the same time. Some collaboration happens on calls, and these can be really productive for actively collaborating on some code or nailing a particularly nasty bug. Nevertheless, I feel that for aligning, advocating, or documenting, they tend to have a high signal loss. Recording meetings can feel like a solution, but someone will not revisit an hour-long meeting by watching a video; they will rather hit you up on Slack, ugh.&lt;/p>
&lt;p>Text, on the other hand, is easily searchable, can be stored along with code, rewritten as needed, and stored for less than a penny.&lt;/p>
&lt;h2 id="_why_">&lt;em>Why?&lt;/em>&lt;/h2>
&lt;p>One of my favorite methods to drive documentation is asking &lt;em>why&lt;/em> relentlessly: &lt;em>Why&lt;/em> do we encounter this issue? &lt;em>Why&lt;/em> are we solving it? &lt;em>Why&lt;/em> is this the approach we took?&lt;/p>
&lt;p>Instead of writing a commit message or pull request that explains what has been done, I write &lt;em>why&lt;/em> those changes were necessary. The &lt;em>what&lt;/em> can probably be summarized by some babbling LLM or, more optimally, by looking at the content. The same applies to code comments: code shouldn&amp;rsquo;t really need comments about what is being done, as this should be self-explanatory by virtue of naming and structure. A &lt;code>start.before(end)&lt;/code> makes an inline comment unnecessary, which would be needed for code containing copious amounts of boolean algebra.&lt;/p>
&lt;p>The aspects of &lt;em>why&lt;/em> in code are also much less prone to getting stale than the &lt;em>what&lt;/em>. Often, implementations change over time, but the comments stay, leading subsequent readers astray.&lt;/p>
&lt;p>The &lt;em>why&lt;/em> also makes it easier to spot your own bullshit. If you propose an architecture and the &lt;em>why&lt;/em> starts to feel awkward and long-winded, you&amp;rsquo;re probably offering the wrong thing, and it gives you the opportunity to catch it before somebody else does.&lt;/p>
&lt;h2 id="diagram-noun-ugly-image">Diagram [noun], ugly image&lt;/h2>
&lt;p>I think this explains my skepticism about architecture diagrams: they mask everything besides the &lt;em>what&lt;/em>. They reduce complex plans into a 2D plane while losing all the fidelity that makes such a diagram necessary in the first place. Even simple assumptions about time relationships in diagrams can break down, which happened to me when I got a diagram created by somebody native in a right-to-left language. After a call, it turned out that I did not need to build a machine that reverses time; I just started to read from the wrong side.&lt;/p>
&lt;p>Often, architecture diagrams create an aura of &amp;ldquo;the difficult grown-up work has been done; now draw the rest of the fucking owl, you coding monkey.&amp;rdquo; I&amp;rsquo;ve been guilty of such diagrams myself, one so horrifying that I cannot bring myself to offer it to the general public, as it may make me liable for people being sick. It helped me think about a problem in time-space, but it didn&amp;rsquo;t offer any sufficient explanation to anybody else. Just recently, I revisited it to re-familiarize myself with it after several years, but I had to read the code since this was much easier to parse than my own drawing.&lt;/p>
&lt;pre>&lt;code> ┌─────────────────┐
│ Transport (net) │
├─────────────────┴──────────────────────────────────┐
│ ┌───────────────┐┌───────────────┐┌───────────────┐│ ┌──────────────┐
┌──│ │ REST ││ Websocket ││ STUN/TURN ││──┐ │ Background │
│ │ └───────────────┘└───────────────┘└───────────────┘│ │ └──────────────┘
│ └────────────────────────────────────────────────────┘ │ │
│ │ │
│ ┌──────────────────┐ ┌──────────────────┐ │ │
└─────▶│ Models │◀─────│ Controller │◀─────┴────────────┘
└──────────────────┘ └──────────────────┘
│
┌────────────┴─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Database │ │ External │
└──────────────────┘ └──────────────────┘
&lt;/code>&lt;/pre>
&lt;p>I do not have a fundamental issue with diagrams, I think. Sometimes I even sprinkle some diagrams into code&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, but it has to be like a table of contents, not a pretend-authoritative piece of imagery that inevitably drifts from reality.&lt;/p>
&lt;p>If you ever tried to generate diagrams from code, you will have realized that they aren&amp;rsquo;t as pretty or as useful as you hoped. I don&amp;rsquo;t think it&amp;rsquo;s a matter of technique, but rather that the reality of systems is more messy than architecture design wants it to be.&lt;/p>
&lt;p>Even AWS fell into that trap with CloudFormation Designer, which was supposed to be a simple way of architecting your cloud systems by drawing diagrams. It feels natural at first sight: you drag and drop high-level, prefabricated components onto a canvas. And while the declarative aspect definitely took off with the likes of Terraform, diagram-based design got mostly abandoned.&lt;/p>
&lt;p>Text is just so unreasonably effective. And that&amp;rsquo;s despite the fact that mass literacy is a relatively recent phenomenon: it took until the 1970s for the majority of the world&amp;rsquo;s population to be considered literate&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Even if the end product isn&amp;rsquo;t the text, but a machine, a software, or a &lt;a href="https://youtu.be/K3xppn2STXk">YouTube video explaining the merits of a reasonably priced semi-offroad motorcycle&lt;/a>, you just &lt;em>know&lt;/em> when the writing is good.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://monodraw.helftone.com">Monodraw&lt;/a> is an amazing piece of software for creating diagrams suitable for among-code documentation.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;a href="https://ourworldindata.org/literacy">https://ourworldindata.org/literacy&lt;/a>&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Don't UUID Yourself</title><link>https://thomas.skowron.eu/blog/dont-uuid-yourself/</link><pubDate>Fri, 10 Dec 2021 12:37:00 +0000</pubDate><guid>https://thomas.skowron.eu/blog/dont-uuid-yourself/</guid><description>&lt;p>For a long time, sequential IDs were good enough for everything. Just use &lt;code>AUTO_INCREMENT&lt;/code> or &lt;code>SERIAL&lt;/code>, job done, right? But then you realize that your competitor might now figure out how many customers you have, because they just have to create a new user and look at their ID. Or someone might enumerate all of your customers, starting at one and adding one until they are finished.&lt;/p>
&lt;p>As an alternative UUID (esp. UUID v4) has become increasingly popular and the reasons are pretty obvious: It&amp;rsquo;s a 128 bit space, so guessing an ID is much harder, they are not sequential, so you can&amp;rsquo;t enumerate them. And most importantly, it makes your software look so much more enterprisey.&lt;/p>
&lt;p>UUIDs incorporate a trade-off that they are made for machines, but are also human-readable. But I&amp;rsquo;m not sure that they are always the right choice.&lt;/p>
&lt;p>128 bit are just 16 byte, but due to the hex encoding and the separation into groups, they&amp;rsquo;re in fact 36 characters long. Thus they have a 125% overhead, but that&amp;rsquo;s not too bad when you consider that they are suitable for human consumption. But about the last point: I find it rather difficult to skim through lists of UUIDs:&lt;/p>
&lt;p>&lt;img src="uuid-line.png" alt="UUIDs">&lt;/p>
&lt;p>They don&amp;rsquo;t give any indication about the kind of data. When you&amp;rsquo;re developing or debugging you try to make sense and find patterns, but UUIDs to - at least - my brain are mostly noise.&lt;/p>
&lt;p>Also it may be difficult if you have a large collection and start to get UUIDs that look similar. The following IDs might look equal:&lt;/p>
&lt;ul>
&lt;li>&lt;code>c0d72339-f3fd-438f-934f-283821683226&lt;/code>&lt;/li>
&lt;li>&lt;code>c0d72339-f3f9-4e8f-934f-283821683226&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>but they&amp;rsquo;re different. In a heated debugging situation this might turn out to be very annoying.&lt;/p>
&lt;h2 id="could-there-be-an-alternative">Could there be an alternative?&lt;/h2>
&lt;p>If you want to keep human-readability and have a similar address space, an alternative might be random, prefixed alpha-numeric strings. Prefixes help distinguish different kind of identifiers: user IDs could be prefixed with &amp;ldquo;&lt;code>usr-&lt;/code>&amp;rdquo;, payment IDs with &amp;ldquo;&lt;code>pay-&lt;/code>&amp;rdquo; and so forth.&lt;/p>
&lt;p>A 6 bit per ASCII char alphabet consisting of &lt;code>ABCDEFGHJKLMNPQRSTUVWXYZ23456789&lt;/code> is case insensitive and even eliminates confusing characters like &lt;code>0&lt;/code> and &lt;code>O&lt;/code> as well as &lt;code>1&lt;/code> and &lt;code>l&lt;/code>. To match at least 128 bit of UUID, you&amp;rsquo;d need 22 chars in that alphabet. Adding a type-specific prefix, with a length of four chars (as in the example above), would result in 26 chars instead of the UUID&amp;rsquo;s 36. Such a string would be more readable and distinguishable
and also have a smaller overhead.&lt;/p>
&lt;p>If you&amp;rsquo;re already using UUIDs, stick with them, they&amp;rsquo;re nice. But when you start off with something new consider if you want to add nine SLOC to your code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#b84">&amp;#34;math/rand&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">const&lt;/span> alphanum = &lt;span style="color:#b84">&amp;#34;ABCDEFGHJKLMNPQRSTUVWXYZ23456789&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">func&lt;/span> &lt;span style="color:#900;font-weight:bold">randomAlphaNumToken&lt;/span>(prefix &lt;span style="color:#458;font-weight:bold">string&lt;/span>, length &lt;span style="color:#458;font-weight:bold">int&lt;/span>) &lt;span style="color:#458;font-weight:bold">string&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> b &lt;span style="font-weight:bold">:=&lt;/span> &lt;span style="color:#999">make&lt;/span>([]&lt;span style="color:#458;font-weight:bold">byte&lt;/span>, length)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">for&lt;/span> i &lt;span style="font-weight:bold">:=&lt;/span> &lt;span style="font-weight:bold">range&lt;/span> b {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> b[i] = alphanum[rand.&lt;span style="color:#900;font-weight:bold">Intn&lt;/span>(&lt;span style="color:#999">len&lt;/span>(alphanum))]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> prefix &lt;span style="font-weight:bold">+&lt;/span> &lt;span style="color:#999">string&lt;/span>(b)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>:= Need Not Allocate</title><link>https://thomas.skowron.eu/blog/-need-not-allocate/</link><pubDate>Thu, 10 Jun 2021 13:35:00 +0000</pubDate><guid>https://thomas.skowron.eu/blog/-need-not-allocate/</guid><description>&lt;p>From time to time I work with planet-wide geospatial data, which in itself is nothing new, but I was in discontent with the ingestion stack I was using, so I built a stream processor. Everything was going well, my prototype was around five times faster than the software I was previously using. Then, I realized the classic GIS mistake: I forgot to reproject the coordinates.&lt;/p>
&lt;p>For those unfamiliar with projections: It&amp;rsquo;s just a way of describing coordinates on a globe. Projections define numeric boundaries and can describe a geographical region in different ways, e.g. when you need equal distances or equal angles in a region. It depends on the context which projection you&amp;rsquo;ll want to use.&lt;/p>
&lt;p>My source data came in EPSG:4326, but I needed EPSG:3857 for rendering. Transforming the coordinates is a trigonometric function with some constants, but doing this for billions of values takes some consideration. The mathematical transformation is quick, but allocating is not.&lt;/p>
&lt;p>Go doesn&amp;rsquo;t make you think too much about allocation: Slices grow automatically, memory is acquired as you go and garbage is collected. I&amp;rsquo;ve seen some very high traffic Go applications that run smootly with a small memory footprint without anyone having spent any thought on memory optimization. But every luxury has its limits: When you deal with long data streams, allocations can stack up.&lt;/p>
&lt;p>I knew it wouldn&amp;rsquo;t be viable to decode the coordinates from my stream format, put them into a slice and recode them again, so I had to do some in-place trickery, resulting in this code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">func&lt;/span> (f &lt;span style="font-weight:bold">*&lt;/span>Feature) &lt;span style="color:#900;font-weight:bold">projectPoint&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> offset &lt;span style="font-weight:bold">:=&lt;/span> f.geometryPos
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> floatX &lt;span style="font-weight:bold">:=&lt;/span> math.&lt;span style="color:#900;font-weight:bold">Float64frombits&lt;/span>(binary.LittleEndian.&lt;span style="color:#900;font-weight:bold">Uint64&lt;/span>(f.Geometry[offset : offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">8&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> floatY &lt;span style="font-weight:bold">:=&lt;/span> math.&lt;span style="color:#900;font-weight:bold">Float64frombits&lt;/span>(binary.LittleEndian.&lt;span style="color:#900;font-weight:bold">Uint64&lt;/span>(f.Geometry[offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">8&lt;/span> : offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">16&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> floatX3857 &lt;span style="font-weight:bold">:=&lt;/span> &lt;span style="color:#900;font-weight:bold">degToRad&lt;/span>(floatX) &lt;span style="font-weight:bold">*&lt;/span> earthRadius
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> floatY3857 &lt;span style="font-weight:bold">:=&lt;/span> math.&lt;span style="color:#900;font-weight:bold">Log&lt;/span>(math.&lt;span style="color:#900;font-weight:bold">Tan&lt;/span>(&lt;span style="color:#900;font-weight:bold">degToRad&lt;/span>(floatY)&lt;span style="font-weight:bold">/&lt;/span>&lt;span style="color:#099">2&lt;/span>&lt;span style="font-weight:bold">+&lt;/span>math.Pi&lt;span style="font-weight:bold">/&lt;/span>&lt;span style="color:#099">4&lt;/span>)) &lt;span style="font-weight:bold">*&lt;/span> &lt;span style="color:#999">float64&lt;/span>(earthRadius)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> binary.LittleEndian.&lt;span style="color:#900;font-weight:bold">PutUint64&lt;/span>(f.Geometry[offset:offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">8&lt;/span>], math.&lt;span style="color:#900;font-weight:bold">Float64bits&lt;/span>(floatX3857))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> binary.LittleEndian.&lt;span style="color:#900;font-weight:bold">PutUint64&lt;/span>(f.Geometry[offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">8&lt;/span>:offset&lt;span style="font-weight:bold">+&lt;/span>&lt;span style="color:#099">16&lt;/span>], math.&lt;span style="color:#900;font-weight:bold">Float64bits&lt;/span>(floatY3857))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f.geometryPos &lt;span style="font-weight:bold">+=&lt;/span> wkbPtSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you&amp;rsquo;re sensitive to allocation, you might look at those &amp;ldquo;&lt;code>:=&lt;/code>&amp;rdquo; and have an urge to elimiate those by inlining those operations. But beware, this makes the code less readable and in fact, doesn&amp;rsquo;t have &lt;em>any&lt;/em> performance benefit.&lt;/p>
&lt;p>A small benchmark confirms this: &lt;code>projectPoint()&lt;/code> doesn&amp;rsquo;t allocate at all, because the compiler knows that nothing will escape.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>In short: Think about the memory, don&amp;rsquo;t reallocate slices if you don&amp;rsquo;t need to, but check if the compiler is maybe already smart enough, so you don&amp;rsquo;t need to compromise readability.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Use &lt;code>b.ReportAllocs()&lt;/code> in your benchmark runs.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Load Balancing Without Giving Away the Keys</title><link>https://thomas.skowron.eu/blog/loadbalancing-without-giving-away-the-keys/</link><pubDate>Mon, 31 Aug 2020 15:49:00 +0000</pubDate><guid>https://thomas.skowron.eu/blog/loadbalancing-without-giving-away-the-keys/</guid><description>&lt;p>Distributing the load of a web application or especially an API endpoint using a load balancer (LB) is highly useful: You can get better performance, make software rollouts smoother and withstand node failure.&lt;/p>
&lt;p>If an application node becomes unavailable due to failure or a planned maintenance, the LB notices that it does not respond and stops send traffic to it.&lt;/p>
&lt;video width="100%" height="200" autoplay loop playsinline muted>
&lt;source src="lb-pure.mp4">
&lt;source src="lb-pure-av1.mkv">
&lt;/video>
&lt;p>The LB itself can go down, too, but since LBs can be made stateless, failover and redundancy can be achieved more easily through e.g. the usage of CARP&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> or anycast. Many cloud providers offer managed load balancers.&lt;/p>
&lt;p>Since HTTPS has become the standard even for non-private data transfers, the management of TLS certificates and keys needs to be considered as well. Often software operators opt to decrypt the TLS on load balancer level and send traffic via unencrypted HTTP to the nodes, compromising confidentiality.&lt;/p>
&lt;p>TLS can also be handled by the nodes itself, but this bears two challenges: All the nodes need the same key and certificate and also, if you&amp;rsquo;re acquiring the certificates from Let&amp;rsquo;s Encrypt or any ACME-compatible CA, they need to serve the appropriate authorization challenge.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;p>For serving HTTPS directly from a Go application, there is the &lt;a href="https://github.com/caddyserver/certmagic">certmagic&lt;/a> library, which also powers the automatic TLS management in &lt;a href="https://caddyserver.com">Caddy&lt;/a>. By default, it manages key generation, cert acquisition and wraps the HTTP server with TLS. The storage for the keys is rather flexible: Normally they&amp;rsquo;re stored on disk, but you can add a different adapter or write your own. There is a third-party S3 adapter, which allows you to store your certs/keys/challenges on Amazon S3. If you configure your nodes to use the same S3 bucket, it doesn&amp;rsquo;t matter which node will be asked by the load balancer to serve the challenge, all of them can answer, because they&amp;rsquo;re pulling the challenge via API.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/loadbalancing-without-giving-away-the-keys/keys-on-s3.png" alt="Keys and certs on S3">
&lt;/figure>
&lt;p>Personally I found it unacceptable to store all the keys in plain text on some else&amp;rsquo;s hard drive. Perfect forward secrecy prevents from decoding HTTPS traffic before a breach, but with a third party service it&amp;rsquo;s impossible to determine whether such an event took place.&lt;/p>
&lt;p>This is the reason why I built a &lt;a href="https://github.com/thomersch/certmagic-generic-s3">custom certificate storage for certmagic&lt;/a>, which stores the keys, certs and challenges on any S3-compatible store (e.g. Backblaze, Digital Ocean Spaces or your own Minio instance), but encrypts it using &lt;a href="https://nacl.cr.yp.to/secretbox.html">NaCl&amp;rsquo;s secret box&lt;/a>&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> before sending it off.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/loadbalancing-without-giving-away-the-keys/keys-on-s3-encr.png" alt="Keys and certs on S3">
&lt;/figure>
&lt;p>You don&amp;rsquo;t have to trust any of the provider&amp;rsquo;s claims about ”at rest“ encryption and you don&amp;rsquo;t have to implement any highly available storage yourself. You just have to spend a few pennies per month for any S3 storage, without lock-in.&lt;/p>
&lt;p>This allows you to do HTTPS without having to break encryption mid-way and you can offload TLS to the nodes, instead of the LB having to do all the work.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://www.freebsd.org/doc/handbook/carp.html">FreeBSD Manual on CARP&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>To be pedantic, only if you&amp;rsquo;re using either TLS-ALPN or HTTP authorization. You can also use DNS validation, if your DNS software or zone provider supports this.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>More specifically, the &lt;a href="https://pkg.go.dev/golang.org/x/crypto@v0.0.0-20200820211705-5c72a883971a/nacl/secretbox?tab=doc">secretbox implementation in Go&lt;/a>&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Printing with SwiftUI</title><link>https://thomas.skowron.eu/blog/printing-with-swiftui/</link><pubDate>Wed, 08 Jul 2020 16:12:00 +0000</pubDate><guid>https://thomas.skowron.eu/blog/printing-with-swiftui/</guid><description>&lt;p>This post combines one of the world&amp;rsquo;s oldest information technologies with one of the newer ones: Printing on paper and SwiftUI. SwiftUI is Apple&amp;rsquo;s rather new framework to declaratively design user interfaces, which runs on all their platforms. It has a few rough edges, but it is very powerful, because it allows mixing and matching with classic &lt;code>NSViews&lt;/code> on macOS, so you can even use them for print (or PDF) output.&lt;/p>
&lt;p>macOS has always had rather great printing support and for a first-class Mac app this is a feature one shouldn&amp;rsquo;t miss.&lt;/p>
&lt;p>Integrating the print workflow into a new app is not too hard and starts with connecting the “Print&amp;hellip;“ menu item&amp;rsquo;s action to an outlet. The menu configuration is not part of SwiftUI, but lives by default in a dedicated storyboard &lt;code>Main.storyboard&lt;/code>.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/printing-with-swiftui/storyboard.png" alt="Storyboard">
&lt;/figure>
&lt;p>With an option click drag (aka &lt;em>right click drag&lt;/em>) you can connect the print menu entry to an outlet in your &lt;code>AppDelegate&lt;/code> class.&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/printing-with-swiftui/outlets.png" alt="Outlets">
&lt;/figure>
&lt;p>Your delegate will look something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">@IBAction&lt;/span> &lt;span style="font-weight:bold">func&lt;/span> &lt;span style="color:#900;font-weight:bold">applicationPrint&lt;/span>(&lt;span style="font-weight:bold">_&lt;/span> sender: NSMenuItem) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inside this method you&amp;rsquo;ll need to setup your view that you will want to print, set up the Print layout info and then hand over to the user to make a decision about the output format and device.&lt;/p>
&lt;p>First of all, inside your delegate, set up an &lt;code>NSPrintInfo&lt;/code> instance and configure it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">let&lt;/span> &lt;span style="color:#008080">printInfo&lt;/span> = NSPrintInfo()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>printInfo.scalingFactor = &lt;span style="color:#099">0.7&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I used a smaller scaling factor, because the font sizes suitable for screens were too large for printing in my particular case.&lt;/p>
&lt;p>Unless you want to print the whole window with e.g. all input elements and text fields (in which case you can simply pass &lt;code>window!.contentView&lt;/code> to the print routine), you need to initialize your printable view and populate it with some data. Let&amp;rsquo;s assume you have a SwiftUI view called &lt;code>EntryListView&lt;/code> and it takes a list of string as data:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">let&lt;/span> &lt;span style="color:#008080">entryListView&lt;/span> = EntryListView(data: [&lt;span style="color:#b84">&amp;#34;This is a string&amp;#34;&lt;/span>, &lt;span style="color:#b84">&amp;#34;And another one&amp;#34;&lt;/span>, &lt;span style="color:#b84">&amp;#34;One more&amp;#34;&lt;/span>])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Since this is a standalone SwiftUI view, it needs to be contained in an &lt;code>NSHostingView&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">let&lt;/span> &lt;span style="color:#008080">printContainerView&lt;/span> = NSHostingView(rootView: entryListView)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>printContainerView.frame.size = CGSize(width: &lt;span style="color:#099">800&lt;/span>, height: &lt;span style="color:#099">600&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This view is now wrapped and can be passed to a print operation alongside the print info configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">let&lt;/span> &lt;span style="color:#008080">printOperation&lt;/span> = NSPrintOperation(view: printContainerView, printInfo: printInfo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>printOperation.printInfo.isVerticallyCentered = &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>printOperation.printInfo.isHorizontallyCentered = &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>printOperation.runModal(&lt;span style="font-weight:bold">for&lt;/span>: window, delegate: &lt;span style="font-weight:bold">self&lt;/span>, didRun: &lt;span style="font-weight:bold">nil&lt;/span>, contextInfo: &lt;span style="font-weight:bold">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If the user selects “Print&amp;hellip;” from the menu bar or hits “⌘P”, a print dialog will popup with your view ready to be printed:&lt;/p>
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/printing-with-swiftui/printdialog.png" alt="Print Dialog">
&lt;/figure></description></item><item><title>Migrating Foreign Keys in PostgreSQL</title><link>https://thomas.skowron.eu/blog/migrating-foreign-keys-in-postgresql/</link><pubDate>Thu, 21 May 2020 08:30:48 +0000</pubDate><guid>https://thomas.skowron.eu/blog/migrating-foreign-keys-in-postgresql/</guid><description>&lt;p>Applications often need to work with external IDs, e.g. UUIDs of third party services or &lt;a href="https://en.wikipedia.org/wiki/Stock_keeping_unit">SKUs&lt;/a>. In such cases, an external ID should be stored just once in a mapping table and from there on only referenced by an internal (e.g. serial) foreign key. Without a dedicated mapping table, you will carry such external identifiers through all tables. This may seem reasonable at the beginning, but may soon cause trouble, e.g. if you want to add a second type of external service or when you realize, that those seemingly unique identifiers are not as unique as assumed.&lt;/p>
&lt;p>Even though I should know better, this kind of mistake happens to me from time to time anyway. Or it happens to others and I have to save the day.&lt;/p>
&lt;p>Because PostgreSQL is not the wild west, if you reference a field in a different table, this will be enforced through a constraint, which is fantastic for data consistency, but annoying if you misdesigned your data structure and have to change your foreign keys. But, don&amp;rsquo;t worry, it&amp;rsquo;s still possible to change this inside a single transaction with those four-ish steps:&lt;/p>
&lt;ol start="0">
&lt;li>Create the new key ID column, make it unique (e.g. &lt;code>serial&lt;/code>)&lt;/li>
&lt;li>Identify all tables which reference the (old) ID:&lt;br>
1.1. Note the table name&lt;br>
1.2. Find the referencing column&lt;br>
1.3. Determine the foreign key constraint name (see &lt;code>pg_constraint&lt;/code> table)&lt;/li>
&lt;li>For each referencing table:&lt;br>
2.1. Drop the existing constraint (from 1.3.)&lt;br>
2.2. Update the referencing key values to the new ones&lt;br>
2.3. Add new constraint for the new base table ID&lt;/li>
&lt;li>Drop primary key constraint from base table&lt;/li>
&lt;li>Create new primary key constraint on base table&lt;/li>
&lt;/ol>
&lt;h2 id="example">Example&lt;/h2>
&lt;p>Let&amp;rsquo;s assume your application tracks users, which have external identifiers. At the moment you got a user table, which has this external ID as the primary key:&lt;/p>
&lt;h3 id="user">&lt;code>user&lt;/code>&lt;/h3>
&lt;table>
&lt;tr>
&lt;th>external_user_id (primary key)&lt;/th>
&lt;th>user_name&lt;/th>
&lt;/tr>
&lt;tr>
&lt;td>12312&lt;/td>
&lt;td>Alice&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>91823&lt;/td>
&lt;td>Bob&lt;/td>
&lt;/tr>
&lt;/table>
&lt;p>Thus, all tables that reference any user have those long, external identifiers:&lt;/p>
&lt;h3 id="user_permissions">&lt;code>user_permissions&lt;/code>&lt;/h3>
&lt;table>
&lt;tr>
&lt;th>user_id&lt;/th>
&lt;th>can_read&lt;/th>
&lt;th>can_write&lt;/th>
&lt;/tr>
&lt;tr>
&lt;td>12312&lt;/td>
&lt;td>true&lt;/td>
&lt;td>true&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>91823&lt;/td>
&lt;td>false&lt;/td>
&lt;td>true&lt;/td>
&lt;/tr>
&lt;/table>
&lt;p>Now, if you want to gain flexbility and add an internal ID, you can start off with adding a &lt;code>serial&lt;/code> column to users:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">user&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">column&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#999">serial&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">UNIQUE&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>user&lt;/code> would now look like this:&lt;/p>
&lt;table>
&lt;tr>
&lt;th>external_user_id (primary key)&lt;/th>
&lt;th>user_name&lt;/th>
&lt;th>id&lt;/th>
&lt;/tr>
&lt;tr>
&lt;td>12312&lt;/td>
&lt;td>Alice&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>91823&lt;/td>
&lt;td>Bob&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;/table>
&lt;p>Now, for every referencing table, you need to strip the constraint, update the values to the new serial ID and then add the new constraint:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DROP&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">CONSTRAINT&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions_user_id_...id;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">UPDATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">SET&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#bbb"> &lt;/span>(&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">SELECT&lt;/span>&lt;span style="color:#bbb"> &lt;/span>id&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">FROM&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">user&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">WHERE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>external_user_id&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions.user_id&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>);&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">CONSTRAINT&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_permissions_user_id_...id&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">FOREIGN&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>(user_id)&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">REFERENCES&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">user&lt;/span>&lt;span style="color:#bbb"> &lt;/span>(id);&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>user_permissions&lt;/code> looks now like this:&lt;/p>
&lt;table>
&lt;tr>
&lt;th>user_id&lt;/th>
&lt;th>can_read&lt;/th>
&lt;th>can_write&lt;/th>
&lt;/tr>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>true&lt;/td>
&lt;td>true&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>false&lt;/td>
&lt;td>true&lt;/td>
&lt;/tr>
&lt;/table>
&lt;p>Finally, you can now change &lt;code>user.external_user_id&lt;/code> without constraints, you just have to swap out the primary key in &lt;code>user&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">user&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DROP&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">CONSTRAINT&lt;/span>&lt;span style="color:#bbb"> &lt;/span>user_pkey;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">ALTER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">user&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">ADD&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">PRIMARY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">KEY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>(id);&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At this moment, you can even put the external identifier into a different table, if you wish, thus possibly separating the concerns even better.&lt;/p>
&lt;h2 id="bonus-content-for-django-users">Bonus Content for Django Users&lt;/h2>
&lt;p>Swapping out primary keys is too difficult for the automatic Django migration assistant, so you&amp;rsquo;ll need to get your hands dirty. Obviously, you can do all those steps as raw SQL migrations, but then &lt;code>makemigrations&lt;/code> will still nag you about changes in your model, thus you got to use &lt;code>SeparateDatabaseAndState&lt;/code>, apply the changes with SQL and then tell Django what the effect is. For an example, see my code in &lt;a href="https://github.com/thomersch/openstreetmap-calendar/commit/7890094c28dc1990eee87ffe65e5b2b926a46add">osmcal&lt;/a>.&lt;/p>
&lt;h3 id="extra-bonus">Extra Bonus&lt;/h3>
&lt;p>If you&amp;rsquo;re working with raw SQL migrations, you might run into trouble and all you&amp;rsquo;ll get is some context-free SQL error message. For better debugging, you can use the &lt;code>sqlmigrate&lt;/code> subcommand which will display the SQL queries instead of directly executing them. That way you can step through the statements and debug them step by step.&lt;/p>
&lt;pre>&lt;code>./manage.py sqlmigrate &amp;lt;app name&amp;gt; &amp;lt;migration name&amp;gt;
&lt;/code>&lt;/pre></description></item><item><title>Making of the OpenStreetMap Calendar</title><link>https://thomas.skowron.eu/blog/making-of-the-osm-calendar/</link><pubDate>Fri, 15 May 2020 12:28:17 +0000</pubDate><guid>https://thomas.skowron.eu/blog/making-of-the-osm-calendar/</guid><description>&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/making-of-the-osm-calendar/osmcal.png" alt="Screenshot of osmcal">
&lt;/figure>
&lt;p>Wikis have changed the landscape of information technology: No matter what organisation you‘re in and what topic you‘re working on, chances are that there is a Wiki, in which you can collaborate with others. Mostly without asking permission, you can put in notes, make lists and refine the material of others. Same with the OpenStreetMap wiki: Some pages are the standard source of information, like &lt;em>“How to map a…”&lt;/em>, others are things relevant to just a single person, like pages where mappers list all the street they‘ve already mapped. And then there are things that have started with a good idea and gotten out of hand…&lt;/p>
&lt;p>While collaborating on texts is good in most Wiki software, when it gets to tables and templates and structures, things get confusing, rather quickly. Sadly one of those examples is the Wiki calendar, which involves fiddling around with the custom templates, writing markup and someone rotating past events by hand. When we were talking about the OSM Wiki during events, people have been complaining repeatedly about the calendar. Some communities don‘t even announce their events in it, because they have shifted to Meetup or Facebook for better usability. There have been discussions how to solve it, maybe some sort of plugin or extension, but nothing has happened.&lt;/p>
&lt;h2 id="inception">Inception&lt;/h2>
&lt;p>So, in early summer of 2019 &lt;a href="https://youtu.be/MRuS3dxKK9U?t=89">I was mad as hell and couldn‘t take it anymore&lt;/a>, so I sat down and started writing a &lt;a href="https://osmcal.org">standalone, special purpose web-based calendar for the OSM community. With maps!&lt;/a>&lt;/p>
&lt;p>Initially I wanted to utilize the infrastructure of the German OpenStreetMap Local Chapter, FOSSGIS, but they were busy generating rules about what I got to provide before getting a precious openstreetmap.de subdomain, so I went on my own and the domain &lt;a href="https://osmcal.org">osmcal.org&lt;/a> was born.&lt;/p>
&lt;p>The software is based on the incredibly powerful Django framework which helps with HTML templates, form handling, databases, authentication and much more. It‘s all &lt;a href="https://github.com/thomersch/openstreetmap-calendar">open source&lt;/a>, of course, as it should be. The issue tracker is on GitHub and already attracts a small community of people with bug reports, feature requests or general questions.&lt;/p>
&lt;h2 id="progress">Progress&lt;/h2>
&lt;p>While osmcal started with a minimal feature set, development has continued and with the input from community members some features have shipped since then:&lt;/p>
&lt;ul>
&lt;li>In October 2019, the possibility to “join“ events has been added, so organizers get a list of participants and as a participant you can see who else is going to come&lt;/li>
&lt;li>In December 2019, iCalendar subscriptions have been made available&lt;/li>
&lt;li>Since February 2020, event organizers can create surveys for participation, so they can ask people for e.g. their t-shirt sizes, their dietary preferences or skill level.&lt;/li>
&lt;/ul>
&lt;p>Special thanks to all reporters, especially &lt;a href="https://github.com/jbelien">@jbelien&lt;/a>, &lt;a href="https://github.com/adrienandrem">@adrienandrem&lt;/a> and &lt;a href="https://github.com/qeef">@qeef&lt;/a> for their collaboration and testing!&lt;/p>
&lt;h2 id="future">Future&lt;/h2>
&lt;p>Of course, at the moment, in-person meetings are out of the question, but this doesn’t mean that there are no events at all. Some are postponed, but many are happening online and those need to be scheduled and communicated as well. Thus, now it’s about time to integrate time zone support, which I initially deemed unnecessary as every event would have a location, but that has been disproven by the increased need of online events.&lt;/p>
&lt;p>Also, I want more communities to integrate osmcal into their websites and wiki pages, so their lives get easier by not having to create and maintain their events on multiple platforms.&lt;/p>
&lt;p>I was asked to consider applying for an &lt;a href="https://wiki.openstreetmap.org/wiki/Microgrants/Microgrants_2020">OpenStreetMap Microgrant&lt;/a>, so I submitted the aforementioned ideas with a request for funding. If you like to see those features and want to support further development, consider expressing your support on my &lt;a href="https://wiki.openstreetmap.org/wiki/Microgrants/Microgrants_2020/Proposal/OpenStreetMap_Calendar">grant application&lt;/a>.&lt;/p>
&lt;h2 id="making-more-connections">Making more connections&lt;/h2>
&lt;p>One new functionality that will be coming soon is community tagging, so organizers can create non-OSM-related events and have a dedicated feed with all their events. But more on that when it’s done. Stay tuned.&lt;/p></description></item><item><title>Faster Map Making With osmium</title><link>https://thomas.skowron.eu/blog/fast-mapmaking-with-osmium/</link><pubDate>Sun, 22 Mar 2020 17:29:08 +0000</pubDate><guid>https://thomas.skowron.eu/blog/fast-mapmaking-with-osmium/</guid><description>&lt;p>It&amp;rsquo;s sunday and I am fiddling around to make some maps, maybe to put them up my own wall and that&amp;rsquo;s the moment where I usually stuggle to get data out of OpenStreetMap into QGIS: Overpass is slow and won&amp;rsquo;t give you too much data, Shapefiles are ok, but this only works for limited regions and filtering and exporting OSMPBFs into GeoJSON is annoying.&lt;/p>
&lt;p>Of course it would be better to have the data in a local PostgreSQL, but in order to get it there, you need to think about what data you want to load, but since you&amp;rsquo;re still iterating and fumbling with the data, this is not yet clear, so you&amp;rsquo;d need to have some cycle of loading, purging and reloading again. Annoying.&lt;/p>
&lt;p>Then I remembered that you can use osmium to get &lt;em>all&lt;/em> the data into PostgreSQL, without having to filter, which is not fast enough for tile servers, but easily fast enough for some experimentation.&lt;/p>
&lt;p>Just get osmium tool and fire up:&lt;/p>
&lt;pre>&lt;code>osmium export -f pg -o taiwan.pg taiwan-latest.osm.pbf
&lt;/code>&lt;/pre>
&lt;p>Create a Postgres database and a table for the data:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">DATABASE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>mapmaking;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#a61717;background-color:#e3d2d2">\&lt;/span>&lt;span style="font-weight:bold">c&lt;/span>&lt;span style="color:#bbb"> &lt;/span>mapmaking&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>EXTENSION&lt;span style="color:#bbb"> &lt;/span>postgis;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">TABLE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>taiwan&lt;span style="color:#bbb"> &lt;/span>(&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>geom&lt;span style="color:#bbb"> &lt;/span>GEOMETRY,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>tags&lt;span style="color:#bbb"> &lt;/span>JSONB&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>);&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>And now suck the data into the table:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-psql" data-lang="psql">\copy taiwan FROM &amp;#39;taiwan.pg&amp;#39;&lt;/code>&lt;/pre>
&lt;p>Now you got all the geometries in one column, the other holds all the tags. Now, you can fire up QGIS and use that table with some rudimentary querying action:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">SELECT&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>geom,&lt;span style="color:#bbb"> &lt;/span>tags&lt;span style="font-weight:bold">-&amp;gt;&amp;gt;&lt;/span>&lt;span style="color:#b84">&amp;#39;ele&amp;#39;&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">AS&lt;/span>&lt;span style="color:#bbb"> &lt;/span>ele,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>tags&lt;span style="font-weight:bold">-&amp;gt;&amp;gt;&lt;/span>&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>,&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>tags&lt;span style="font-weight:bold">-&amp;gt;&amp;gt;&lt;/span>&lt;span style="color:#b84">&amp;#39;name:en&amp;#39;&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="font-weight:bold">AS&lt;/span>&lt;span style="color:#bbb"> &lt;/span>name_en&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">FROM&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>taiwan&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="font-weight:bold">WHERE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>tags&lt;span style="font-weight:bold">-&amp;gt;&amp;gt;&lt;/span>&lt;span style="color:#b84">&amp;#39;natural&amp;#39;&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;peak&amp;#39;&lt;/span>;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Enjoy!&lt;/p>
&lt;p>By the way, my work-in-progress greyscale map looks like this:&lt;/p>
&lt;div class="img-screen-max-height">
&lt;figure class="centered">
&lt;img src="https://thomas.skowron.eu/blog/fast-mapmaking-with-osmium/taiwan.png" alt="Taiwan">
&lt;/figure>
&lt;/div></description></item></channel></rss>