<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Terraform on ferkakta.dev</title><link>https://ferkakta.dev/tags/terraform/</link><description>Recent content in Terraform on ferkakta.dev</description><generator>Hugo</generator><language>en-US</language><copyright>Copyright fizz.</copyright><lastBuildDate>Fri, 27 Mar 2026 00:00:00 -0500</lastBuildDate><atom:link href="https://ferkakta.dev/tags/terraform/index.xml" rel="self" type="application/rss+xml"/><item><title>One module block per service per tenant</title><link>https://ferkakta.dev/one-module-block-per-service-per-tenant/</link><pubDate>Fri, 27 Mar 2026 00:00:00 -0500</pubDate><guid>https://ferkakta.dev/one-module-block-per-service-per-tenant/</guid><description>&lt;p&gt;Every tenant on my platform gets three services: an API server, an auth service, and a frontend. Each one is a single module block in Terraform that creates a Kubernetes deployment, a ClusterIP service, an ALB ingress, IRSA for AWS access, ESO-synced secrets from SSM, and a feature flag discovery mechanism. The module is the same for all three services. The variables are different.&lt;/p&gt;
&lt;p&gt;I extracted it into an open source module because I kept explaining the design decisions to people who asked &amp;ldquo;how do you deploy services to EKS?&amp;rdquo; and the answer was always &amp;ldquo;let me show you the module.&amp;rdquo; The module is the answer.&lt;/p&gt;</description></item><item><title>Every tool I've ever used is a CloudFormation frontend</title><link>https://ferkakta.dev/cloudformation-frontends/</link><pubDate>Thu, 26 Mar 2026 18:00:00 -0500</pubDate><guid>https://ferkakta.dev/cloudformation-frontends/</guid><description>&lt;p&gt;I was reading a job description that wanted CloudFormation experience, and I had the thought that derails the actual task: I&amp;rsquo;ve spent my entire career using tools that compile down to CloudFormation and don&amp;rsquo;t mention it until something breaks. I&amp;rsquo;ve just never framed it that way.&lt;/p&gt;
&lt;p&gt;My career is a parade of progressively nicer frontends for the same underlying control plane — but one at a time.&lt;/p&gt;
&lt;p&gt;The first one was the AWS console. Click, wait, refresh, click. Then CloudFormation itself, which was an improvement in the way that a paper map is an improvement over asking for directions — technically correct, nearly unusable in practice. Then Serverless Framework, which promised to abstract the whole stack into a YAML file and a deploy command. Then Terraform, which promised cloud-agnostic infrastructure as code with a state model that actually worked.&lt;/p&gt;</description></item><item><title>from feature_flags import *</title><link>https://ferkakta.dev/from-feature-flags-import-star/</link><pubDate>Wed, 25 Mar 2026 21:00:00 -0500</pubDate><guid>https://ferkakta.dev/from-feature-flags-import-star/</guid><description>&lt;p&gt;A colleague needed a feature flag enabled on one tenant. &lt;code&gt;FEATURE_FLAG_ENABLE_AGENTS=True&lt;/code&gt; — one environment variable, one pod. I added it to the K8s secret manually, restarted the pod, and he was unblocked in two minutes.&lt;/p&gt;
&lt;p&gt;Then I realized: the next terraform apply would overwrite that secret without the flag. The ExternalSecret syncs from SSM, and the flag wasn&amp;rsquo;t in SSM through any path terraform knew about. My manual fix had a shelf life of one deploy.&lt;/p&gt;</description></item><item><title>Zero-touch multi-tenant deploys: removing myself from the critical path</title><link>https://ferkakta.dev/zero-touch-multi-tenant-deploys-eks-terraform/</link><pubDate>Mon, 02 Mar 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/zero-touch-multi-tenant-deploys-eks-terraform/</guid><description>&lt;p&gt;I had provisioned two tenants when I realized the deploy process didn&amp;rsquo;t scale to three. Each tenant on &lt;a href="https://ramparts.dev"&gt;ramparts&lt;/a&gt; runs three services &amp;ndash; &lt;code&gt;api-server&lt;/code&gt;, &lt;code&gt;web-client&lt;/code&gt; (the React frontend), &lt;code&gt;tenant-auth&lt;/code&gt; &amp;ndash; each with its own Docker image in ECR. Deploying a release meant running &lt;code&gt;gh workflow run deploy-tenant.yml -f tenant_name=acme -f action=apply -f update_images=true&lt;/code&gt;, then doing it again for the next tenant. With 3 services resolving per run and N tenants, I was the bottleneck. Not Terraform, not GitHub Actions, not ECR. Me, remembering which tenants existed and typing their names correctly.&lt;/p&gt;</description></item><item><title>IAM trust policies silently accept wildcards in principals — and silently deny everything</title><link>https://ferkakta.dev/iam-trust-policy-wildcards/</link><pubDate>Thu, 26 Feb 2026 10:00:00 -0600</pubDate><guid>https://ferkakta.dev/iam-trust-policy-wildcards/</guid><description>&lt;p&gt;I needed a cross-account IAM role in a management account that workloads in a separate devops account could assume to send email via SES. Two types of callers: one shared service with a stable role name, and N dynamically-created per-tenant roles following a naming convention like &lt;code&gt;myapp-apiserver-*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The shared service was straightforward — exact ARN in the trust policy principal. For the per-tenant roles, I wrote what looked correct:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Principal&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;:&lt;/span&gt; { &lt;span style="color:#f92672"&gt;&amp;#34;AWS&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;arn:aws:iam::111111111111:role/myapp-apiserver-*&amp;#34;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;terraform apply&lt;/code&gt; succeeded. The role was created. Every assume-role call was denied.&lt;/p&gt;</description></item><item><title>The Over-Mighty Subject: why your site repos have too much power</title><link>https://ferkakta.dev/over-mighty-subjects-terraform-credential-scope/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://ferkakta.dev/over-mighty-subjects-terraform-credential-scope/</guid><description>&lt;p&gt;Josh Marshall &lt;a href="https://talkingpointsmemo.com/edblog/elon-musk-and-the-the-threat-of-the-over-mighty-subject-part-i/sharetoken/21fb0dac-112d-4a9d-bcc0-f5bf844b16bb"&gt;borrows a phrase from medieval history&lt;/a&gt; to describe a modern political problem: the Over-Mighty Subject. A feudal lord whose personal wealth, private army, and territorial control grew so large that he rivaled the crown itself. Not a rebel — still nominally a subject — but operating with enough independent power that the sovereign&amp;rsquo;s authority became theoretical.&lt;/p&gt;
&lt;p&gt;I had three of them in my infrastructure. They were Terraform roots for static sites.&lt;/p&gt;</description></item><item><title>I replaced $489/mo in AWS Client VPN with a $3 t4g.nano running Headscale</title><link>https://ferkakta.dev/headscale-aws-open-source-terraform-module/</link><pubDate>Sat, 21 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/headscale-aws-open-source-terraform-module/</guid><description>&lt;p&gt;A finops sprint surfaced $489/mo in AWS Client VPN charges. Three endpoints across two accounts, plus connection-hour fees. For a VPN that four people used. I had provisioned two of them.&lt;/p&gt;
&lt;p&gt;At the time, they felt indispensable — secure customer access, familiar tooling, predictable behavior.
In reality, they were architectural inertia.&lt;/p&gt;
&lt;p&gt;I replaced all three with a single t4g.nano running &lt;a href="https://github.com/juanfont/headscale"&gt;Headscale&lt;/a&gt; — the open-source Tailscale coordination server. Total cost: ~$3/mo.&lt;/p&gt;
&lt;p&gt;I genericized the Terraform and open-sourced the module.&lt;/p&gt;</description></item><item><title>Self-healing race conditions: when your CI/CD fails on purpose</title><link>https://ferkakta.dev/self-healing-race-conditions-github-actions-concurrency/</link><pubDate>Fri, 20 Feb 2026 11:00:00 -0500</pubDate><guid>https://ferkakta.dev/self-healing-race-conditions-github-actions-concurrency/</guid><description>&lt;p&gt;Three app repos build Docker images and push them to ECR. On merge, each fires a &lt;code&gt;repository_dispatch&lt;/code&gt; to an infra repo&amp;rsquo;s orchestrator workflow. The orchestrator resolves ALL service images — not just the one that triggered it — and deploys every tenant via Terraform.&lt;/p&gt;
&lt;p&gt;What happens when two repos merge at the same time?&lt;/p&gt;
&lt;h2 id="the-sequence"&gt;The sequence&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;T=0:&lt;/strong&gt; &lt;code&gt;web-client&lt;/code&gt; and &lt;code&gt;tenant-auth&lt;/code&gt; both merge to &lt;code&gt;releases/0.0.2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=2m:&lt;/strong&gt; &lt;code&gt;tenant-auth&lt;/code&gt; build finishes first, fires dispatch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=2.5m:&lt;/strong&gt; Orchestrator Run A starts. Tries to resolve all 3 service images.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=2.5m:&lt;/strong&gt; &lt;code&gt;web-client&lt;/code&gt; image doesn&amp;rsquo;t exist yet — still building. Run A fails at image resolution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=4m:&lt;/strong&gt; &lt;code&gt;web-client&lt;/code&gt; build finishes, fires its own dispatch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=4m:&lt;/strong&gt; Orchestrator Run B starts. Run A already finished (failed), so the concurrency group is free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=4m:&lt;/strong&gt; Run B resolves all 3 images. Both new ones exist now. Deploy succeeds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The end state is correct. Both changes deployed. One workflow run failed. Nobody had to do anything.&lt;/p&gt;</description></item><item><title>Cross-repo auto-deploy with GitHub Actions: the orchestrator pattern</title><link>https://ferkakta.dev/cross-repo-auto-deploy-orchestration-github-actions/</link><pubDate>Fri, 20 Feb 2026 10:00:00 -0500</pubDate><guid>https://ferkakta.dev/cross-repo-auto-deploy-orchestration-github-actions/</guid><description>&lt;p&gt;Two repos merged within seconds of each other. The first orchestrator run failed — &lt;code&gt;web-client&lt;/code&gt;&amp;rsquo;s ECR image didn&amp;rsquo;t exist yet because the build was still running. The GitHub Actions log showed a red X, an error annotation, and a Slack notification I didn&amp;rsquo;t need to read.&lt;/p&gt;
&lt;p&gt;Four minutes later, the second run deployed both changes. No retry logic. No manual intervention. Nobody touched anything.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d spent my day building a cross-repo deploy pipeline for a multi-tenant platform — three app repos pushing Docker images to ECR, one infra repo deploying the new tenant service images to EKS. The race condition was the first real test. It failed exactly the way I wanted it to.&lt;/p&gt;</description></item><item><title>Your terraform apply is silently rolling back your container images</title><link>https://ferkakta.dev/state-aware-ecr-image-resolution-github-actions/</link><pubDate>Tue, 17 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/state-aware-ecr-image-resolution-github-actions/</guid><description>&lt;p&gt;Every &amp;ldquo;deploy to EKS with GitHub Actions&amp;rdquo; tutorial solves the same problem: build an image, push to ECR, deploy it. The tutorial ends at &amp;ldquo;your pod is running.&amp;rdquo; Nobody talks about day two.&lt;/p&gt;
&lt;h2 id="the-silent-rollback"&gt;The silent rollback&lt;/h2&gt;
&lt;p&gt;Day two: you have a running EKS cluster with three services per tenant. You need to change an IAM policy. You open a PR, touch one line of Terraform, run &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Your IAM policy updates. Your container images also update — to whatever was hardcoded in &lt;code&gt;variables.tf&lt;/code&gt; as the default. That default was correct three months ago. Your services just rolled back to a three-month-old image and nobody noticed because the deployment succeeded.&lt;/p&gt;</description></item><item><title>Terraform module for multi-provider DNS: define once, deploy to Route53 + Cloudflare</title><link>https://ferkakta.dev/terraform-module-for-multi-provider-dns-define-once-deploy-to-route53--cloudflare/</link><pubDate>Mon, 16 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/terraform-module-for-multi-provider-dns-define-once-deploy-to-route53--cloudflare/</guid><description>&lt;p&gt;I manage 10 domains across Route53 and Cloudflare. When I set up &lt;a href="https://fizz.today/til-cloudflare-registrar-locks-your-nameservers-and-how-to-escape-with-multi-provider-dns/"&gt;multi-provider DNS&lt;/a&gt; on my first domain, every record had to be defined twice — once for each provider. The APIs are different enough that you can&amp;rsquo;t just copy-paste.&lt;/p&gt;
&lt;p&gt;The duplication got old fast. So I wrote a module.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;Route53 and Cloudflare represent the same DNS data differently:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MX records&lt;/strong&gt;: Route53 bundles priority into the value string (&lt;code&gt;&amp;quot;10 mx1.example.com&amp;quot;&lt;/code&gt;). Cloudflare splits it into a separate &lt;code&gt;priority&lt;/code&gt; field.&lt;/p&gt;</description></item><item><title>ElastiCache auth-token to RBAC migration has a Terraform provider bug</title><link>https://ferkakta.dev/elasticache-auth-token-to-rbac-migration/</link><pubDate>Fri, 13 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/elasticache-auth-token-to-rbac-migration/</guid><description>&lt;p&gt;Needed to migrate a shared ElastiCache Redis cluster from a single auth token to per-user RBAC. Breaking change — every service on the cluster goes dark if you get the sequencing wrong.&lt;/p&gt;
&lt;h2 id="the-terraform-provider-bug"&gt;The Terraform provider bug&lt;/h2&gt;
&lt;p&gt;Step one: don&amp;rsquo;t touch the real cluster. Built a throwaway copy and ran the migration there first.&lt;/p&gt;
&lt;p&gt;Good thing — the Terraform AWS provider has a bug in the auth-token removal step. It tells you the auth token was removed. Updates its state file. The plan shows no changes. But the underlying API call silently fails. The token is still active on the cluster.&lt;/p&gt;</description></item><item><title>SimpleAD is Samba 4 — you can create users with ldapadd instead of ClickOps</title><link>https://ferkakta.dev/simplead-ldap-user-creation-terraform/</link><pubDate>Thu, 12 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/simplead-ldap-user-creation-terraform/</guid><description>&lt;p&gt;If you&amp;rsquo;ve tried to fully automate Amazon WorkSpaces provisioning with Terraform, you&amp;rsquo;ve hit the wall: SimpleAD has no AWS API for creating directory users.&lt;/p&gt;
&lt;h2 id="what-every-guide-tells-you"&gt;What every guide tells you&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Enable WorkDocs in the console, then use the WorkDocs API to create users&lt;/li&gt;
&lt;li&gt;Launch a domain-joined EC2 instance with RSAT tools and create users manually&lt;/li&gt;
&lt;li&gt;RDP into a Windows management machine and use the AD admin console&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these break the Terraform workflow. Everything is automated except the one step that creates the user your WorkSpace actually needs.&lt;/p&gt;</description></item><item><title>90 AWS resources in 5 minutes — automating multi-tenant SaaS tenant lifecycle</title><link>https://ferkakta.dev/multi-tenant-saas-tenant-lifecycle/</link><pubDate>Tue, 10 Feb 2026 09:00:00 -0600</pubDate><guid>https://ferkakta.dev/multi-tenant-saas-tenant-lifecycle/</guid><description>&lt;p&gt;I recorded our entire tenant lifecycle — create, test, destroy — with no edits. Here&amp;rsquo;s what 5 minutes of infrastructure automation looks like when there are no tickets, no handoffs, and no &amp;ldquo;can someone set up the database.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="what-happens-on-tenant-create"&gt;What happens on &lt;code&gt;tenant create&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;One GitHub Actions workflow backed by Terraform + a Kubernetes operator:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validates the tenant name, resolves container images from the latest release branch&lt;/li&gt;
&lt;li&gt;Provisions ACM wildcard cert + Route53 DNS records&lt;/li&gt;
&lt;li&gt;Creates the &lt;code&gt;Tenant&lt;/code&gt; CRD → operator provisions PostgreSQL databases on shared RDS, seeds credentials to SSM&lt;/li&gt;
&lt;li&gt;Terraform deploys ExternalSecrets, Deployments, Ingress — 3 services per tenant&lt;/li&gt;
&lt;li&gt;SSM parameters auto-seeded: Redis credentials, auth URLs, signing keys — ~40 config values per tenant&lt;/li&gt;
&lt;li&gt;Zero static credentials anywhere — IRSA for everything, secrets injected at runtime from SSM via External Secrets Operator&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;About 5 minutes from nothing to 90 AWS resources and running pods.&lt;/p&gt;</description></item></channel></rss>