<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Infrastructure-as-Code on zharif.my</title>
        <link>https://zharif.my/categories/infrastructure-as-code/</link>
        <description>Recent content in Infrastructure-as-Code on zharif.my</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zharif.my/categories/infrastructure-as-code/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>GitHub Organization as Code with Terraform</title>
        <link>https://zharif.my/posts/github-management-plane/</link>
        <pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate>
        
        <guid>https://zharif.my/posts/github-management-plane/</guid>
        <description>&lt;img src="https://images.unsplash.com/photo-1556075798-4825dfaaf498?w=800&amp;h=400&amp;fit=crop" alt="Featured image of post GitHub Organization as Code with Terraform" /&gt;&lt;h2 id=&#34;why-this-matters&#34;&gt;Why This Matters
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;ve ever tried to explain to your team that &amp;ldquo;we can&amp;rsquo;t create a new repo right now, I&amp;rsquo;m at dinner,&amp;rdquo; you understand why GitHub management should be code. Every infrastructure change in my homelab goes through code review — including how we manage GitHub itself.&lt;/p&gt;
&lt;p&gt;The problem: GitHub&amp;rsquo;s web UI is fine for 3 repos, painful for 30+. You can&amp;rsquo;t track who changed what, can&amp;rsquo;t enforce naming conventions, and can&amp;rsquo;t ensure consistency across repositories.&lt;/p&gt;
&lt;p&gt;The solution: treat your GitHub organization like database infrastructure. Define everything in YAML, let Terraform handle the drift, and sleep better at night.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scale&lt;/strong&gt;: This setup manages 40+ repositories across my organization with full configuration, teams, secrets, and custom properties.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All Terraform resources support &lt;code&gt;lifecycle { create_before_destroy = true }&lt;/code&gt; for zero-downtime deployments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;architecture&#34;&gt;Architecture
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;github-management-plane&lt;/code&gt; repository manages my GitHub organization through Terraform — the same configuration-driven pattern as my homelab infrastructure:&lt;/p&gt;
&lt;pre class=&#34;mermaid&#34;&gt;
  graph TB
    G[github-management-plane]
    R[Repository Module]
    S[Secrets/Variables Module]
    O[Organization Custom Properties]
    
    G --&amp;gt; R
    G --&amp;gt; S
    G --&amp;gt; O
    
    Repos[&amp;#34;All Repositories&amp;#34;]
    Vars[&amp;#34;Actions Variables&amp;#34;]
    Secs[&amp;#34;Actions Secrets&amp;#34;]
    
    Repos --&amp;gt; tf-infra-homelab[&amp;#34;tf-infra-homelab&amp;#34;]
    Repos --&amp;gt; tf-module-proxmox-talos[&amp;#34;tf-module-proxmox-talos&amp;#34;]
    Repos --&amp;gt; applications-homelab[&amp;#34;applications-homelab&amp;#34;]
    Repos --&amp;gt; ...[&amp;#34;24+ repositories&amp;#34;]
&lt;/pre&gt;

&lt;h2 id=&#34;what-this-module-does&#34;&gt;What This Module Does
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Repository management&lt;/strong&gt; — create and configure all repositories via YAML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team management&lt;/strong&gt; — define teams and members&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization custom properties&lt;/strong&gt; — classify repositories&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actions secrets/variables&lt;/strong&gt; — manage organization-wide secrets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue labels&lt;/strong&gt; — standardize labels across repositories&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;quick-start&#34;&gt;Quick Start
&lt;/h2&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;module &amp;#34;repositories&amp;#34; {
  source   = &amp;#34;./modules/repository&amp;#34;
  for_each = local.filtered_repo_configurations
  
  configuration = each.value
  organization  = var.github_organization
}&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id=&#34;repository-management&#34;&gt;Repository Management
&lt;/h2&gt;&lt;p&gt;All repositories are defined as YAML configurations:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# configurations/repository/tf-infra-homelab.yaml
name: tf-infra-homelab
description: A terraform infrastructure repository for managing my homelab environment
enabled: true
archived: false
visibility: private
type: terraform-infrastructure

topics:
  - homelab
  - proxmox

enabled_features:
  vulnerability_alerts: true
  issues: true
  wiki: false
  projects: false
  discussions: false&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;repository-types&#34;&gt;Repository Types
&lt;/h3&gt;&lt;p&gt;The module supports different repository types with defaults:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;locals {
  repository_types = {
    terraform-infrastructure = {
      license_template = &amp;#34;mit&amp;#34;
      auto_init       = true
      topics        = [&amp;#34;terraform&amp;#34;, &amp;#34;homelab&amp;#34;]
    }
    terraform-module = {
      license_template = &amp;#34;mit&amp;#34;
      auto_init       = true
      topics         = [&amp;#34;terraform&amp;#34;, &amp;#34;proxmox&amp;#34;]
    }
    python-docker-application = {
      license_template = &amp;#34;mit&amp;#34;
      auto_init       = true
      topics         = [&amp;#34;python&amp;#34;, &amp;#34;docker&amp;#34;]
    }
    generic = {
      auto_init = true
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;repository-resource&#34;&gt;Repository Resource
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &amp;#34;github_repository&amp;#34; &amp;#34;this&amp;#34; {
  name        = var.configuration.name
  description = var.configuration.description
  visibility  = var.configuration.visibility
  
  allow_rebase_merge    = true
  allow_squash_merge  = true
  delete_branch_on_merge = true
  
  vulnerability_alerts = var.configuration.enabled_features.vulnerability_alerts
  has_discussions     = var.configuration.enabled_features.discussions
  has_issues         = var.configuration.enabled_features.issues
  has_projects       = var.configuration.enabled_features.projects
  has_wiki           = var.configuration.enabled_features.wiki
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;organization-custom-properties&#34;&gt;Organization Custom Properties
&lt;/h2&gt;&lt;p&gt;Custom properties allow classification and filtering:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;locals {
  organization_custom_properties = {
    &amp;#34;can-be-public&amp;#34; = {
      description   = &amp;#34;To indicate whether the repository can be made public&amp;#34;
      value_type  = &amp;#34;single_select&amp;#34;
      required   = true
      allowed_values = [&amp;#34;true&amp;#34;, &amp;#34;false&amp;#34;]
      default_value = &amp;#34;false&amp;#34;
    }
    
    &amp;#34;managed-by&amp;#34; = {
      description = &amp;#34;To identify who manages the repository&amp;#34;
      value_type = &amp;#34;single_select&amp;#34;
      required = true
      allowed_values = [
        &amp;#34;github-management-plane&amp;#34;,
        &amp;#34;manual-management&amp;#34;,
      ]
      default_value = &amp;#34;manual-management&amp;#34;
    }
    
    &amp;#34;repository-type&amp;#34; = {
      description = &amp;#34;To indicate the type of repository&amp;#34;
      value_type = &amp;#34;single_select&amp;#34;
      required = true
      allowed_values = [
        &amp;#34;generic&amp;#34;,
        &amp;#34;repository-template&amp;#34;,
        &amp;#34;golang-linux-package&amp;#34;,
        &amp;#34;golang-docker-application&amp;#34;,
        &amp;#34;python-docker-application&amp;#34;,
        &amp;#34;python-package&amp;#34;,
        &amp;#34;terraform-infrastructure&amp;#34;,
        &amp;#34;terraform-module&amp;#34;,
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Apply them:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &amp;#34;github_organization_custom_properties&amp;#34; &amp;#34;managed_properties&amp;#34; {
  for_each   = local.organization_custom_properties
  
  property_name = each.key
  value_type  = each.value.value_type
  required   = each.value.required
  description = each.value.description
  default_value = each.value.default_value
  allowed_values = each.value.allowed_values
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;secrets-and-variables&#34;&gt;Secrets and Variables
&lt;/h2&gt;&lt;p&gt;Actions secrets and variables are managed through Bitwarden:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# configurations/secrets_variables/global.yaml
variables:
  - name: RUNNER
    value: self-hosted
    visibility: all

secrets:
  - name: BWS_ACCESS_TOKEN
    is_manual: true
    visibility: private&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;variable-resource&#34;&gt;Variable Resource
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &amp;#34;github_actions_organization_variable&amp;#34; &amp;#34;managed_variables&amp;#34; {
  for_each = { for v in var.configuration.variables : v.name =&amp;gt; v }
  
  variable_name = each.value.name
  visibility   = each.value.visibility
  value        = each.value.value
}&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;secret-resource&#34;&gt;Secret Resource
&lt;/h3&gt;&lt;p&gt;For secrets, two patterns:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;# Manual secrets (value set outside Terraform)
resource &amp;#34;github_actions_organization_secret&amp;#34; &amp;#34;managed_secrets_manual&amp;#34; {
  for_each = { for v in var.configuration.secrets : v.name =&amp;gt; v if v.is_manual }
  
  secret_name = each.value.name
  visibility = each.value.visibility
  plaintext_value = &amp;#34;NONE&amp;#34;
}

# Synced secrets (from Bitwarden)
resource &amp;#34;github_actions_organization_secret&amp;#34; &amp;#34;managed_secrets_sync&amp;#34; {
  for_each = { for v in var.configuration.secrets : v.name =&amp;gt; v if !v.is_manual }
  
  secret_name     = each.value.name
  visibility     = each.value.visibility
  plaintext_value = data.bitwarden-secrets_secret.secrets[each.value.name].value
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Get secrets from Bitwarden:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;data &amp;#34;bitwarden-secrets_secret&amp;#34; &amp;#34;secrets&amp;#34; {
  for_each = { for v in local.all_secrets : v.name =&amp;gt; v if !v.is_manual }
  id      = each.value.bw_secret_id
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;team-management&#34;&gt;Team Management
&lt;/h2&gt;&lt;p&gt;Teams are defined in the main module:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &amp;#34;github_team&amp;#34; &amp;#34;organization_administrators&amp;#34; {
  name        = &amp;#34;organization-administrators&amp;#34;
  description = &amp;#34;Team with administrative access to the organization&amp;#34;
  privacy     = &amp;#34;closed&amp;#34;
}

resource &amp;#34;github_team_members&amp;#34; &amp;#34;organization_administrators_members&amp;#34; {
  team_id = github_team.organization_administrators.id
  
  members = {
    &amp;#34;your-username&amp;#34; = {
      role = &amp;#34;maintainer&amp;#34;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;issue-labels&#34;&gt;Issue Labels
&lt;/h2&gt;&lt;p&gt;Labels are standardized across repositories:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;locals {
  shared_labels = {
    &amp;#34;bug&amp;#34; = {
      color   = &amp;#34;d73a4a&amp;#34;
      description = &amp;#34;Bug report&amp;#34;
    }
    &amp;#34;enhancement&amp;#34; = {
      color   = &amp;#34;a2eeef&amp;#34;
      description = &amp;#34;New feature&amp;#34;
    }
    &amp;#34;documentation&amp;#34; = {
      color   = &amp;#34;0075ca&amp;#34;
      description = &amp;#34;Documentation improvements&amp;#34;
    }
  }
}

resource &amp;#34;github_issue_labels&amp;#34; &amp;#34;this&amp;#34; {
  repository = var.configuration.name
  
  label = merge(
    local.shared_labels,
    try(var.configuration.labels, {})
  )
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;my-repository-configuration&#34;&gt;My Repository Configuration
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the list of repositories managed:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-txt&#34;&gt;configurations/repository/
├── tf-infra-homelab.yaml          # Homelab infrastructure
├── tf-infra-github-management-plane.yaml  # GitHub management
├── tf-module-proxmox-lxc.yaml    # LXC module
├── tf-module-proxmox-vm.yaml    # VM module
├── tf-module-proxmox-talos.yaml  # Talos module
├── tf-module-proxmox-docker.yaml # Docker module
├── applications-homelab.yaml   # Kustomize apps
├── cf-worker-terraform-registry.yaml  # TF registry worker
├── cf-worker-apt-repository.yaml   # APT repository worker
├── template-terraform-basic.yaml   # Module template
├── template-cloudflare-worker-python.yaml  # Worker template
└── ... (24 total)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each is just a YAML file — adding a new repository is adding a file.&lt;/p&gt;
&lt;h2 id=&#34;configuration-structure&#34;&gt;Configuration Structure
&lt;/h2&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-txt&#34;&gt;github-management-plane/
├── configurations/
│   ├── repository/           # Repository definitions
│   ├── secrets_variables/   # Actions secrets/variables
│   └── rulesets/           # Branch protection (future)
├── modules/
│   ├── repository/         # Repository module
│   ├── secrets_variables/  # Secrets module
│   └── organization/        # Org properties module
├── main.tf                 # Orchestration
├── locals.tf              # Configuration loading
└── providers.tf          # Provider setup&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This mirrors the homelab infrastructure structure.&lt;/p&gt;
&lt;h2 id=&#34;outputs&#34;&gt;Outputs
&lt;/h2&gt;&lt;p&gt;The module doesn&amp;rsquo;t have specific outputs since it manages the organization passively.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Branch protection rulesets&lt;/strong&gt; — via rulesets API (when Terraform provider supports it)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository invitations&lt;/strong&gt; — managing outside collaborators&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security advisories&lt;/strong&gt; — automated security scanning&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;what-most-people-get-wrong&#34;&gt;What Most People Get Wrong
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;GitHub Terraform is just for repos&amp;rdquo;&lt;/strong&gt; — It&amp;rsquo;s org-wide: teams, custom properties, secrets, variables. The whole thing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;One Terraform run is enough&amp;rdquo;&lt;/strong&gt; — Git rate limits apply. Use &lt;code&gt;ratenode&lt;/code&gt; provider or caching.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Manual changes are fine if you revert&amp;rdquo;&lt;/strong&gt; — Terraform drift will catch you. Enable &lt;code&gt;logento&lt;/code&gt; or run &lt;code&gt;terraform refresh&lt;/code&gt; regularly.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;when-to-use--when-not-to-use&#34;&gt;When to Use / When NOT to Use
&lt;/h2&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Use GitHub Management Plane&lt;/th&gt;
          &lt;th&gt;Do it manually&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;20+ repositories&lt;/td&gt;
          &lt;td&gt;1-5 repos&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Team collaboration&lt;/td&gt;
          &lt;td&gt;Personal projects&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Audit requirements&lt;/td&gt;
          &lt;td&gt;Quick experiments&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Secret/variable management&lt;/td&gt;
          &lt;td&gt;Ad-hoc scripts only&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This makes GitHub management declarative — every change goes through code review.&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Terraform-Driven Homelab Architecture</title>
        <link>https://zharif.my/posts/homelab-terraform-architecture/</link>
        <pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate>
        
        <guid>https://zharif.my/posts/homelab-terraform-architecture/</guid>
        <description>&lt;img src="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&amp;h=400&amp;fit=crop" alt="Featured image of post Terraform-Driven Homelab Architecture" /&gt;&lt;h2 id=&#34;the-problem-space&#34;&gt;The Problem Space
&lt;/h2&gt;&lt;p&gt;Homelabs evolve. You start with one Docker container, add some LXCs, then Kubernetes, and suddenly your infrastructure is a house of cards held together by scripts you wrote two years ago and don&amp;rsquo;t remember.&lt;/p&gt;
&lt;p&gt;This architecture solves that through:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Everything in code&lt;/strong&gt; — from VM provisioning to Kubernetes bootstrap&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioned modules&lt;/strong&gt; — each update is a code review opportunity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-service via Backstage&lt;/strong&gt; — templated provisioning, no Slack threads&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Numbers&lt;/strong&gt;: 3 Proxmox nodes, 2 production clusters (Docker Swarm + Talos K8s), ~50 resources defined across 24+ YAML configurations.&lt;/p&gt;
&lt;p&gt;Running in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 Proxmox nodes (alpha, charlie, foxtrot)&lt;/li&gt;
&lt;li&gt;Docker Swarm clusters with Keepalived HA&lt;/li&gt;
&lt;li&gt;Talos Kubernetes clusters with Flux GitOps&lt;/li&gt;
&lt;li&gt;GPU passthrough for hardware acceleration&lt;/li&gt;
&lt;li&gt;Multi-network topology (dmz + vmbr1)&lt;/li&gt;
&lt;li&gt;Private container registry (Harbor)&lt;/li&gt;
&lt;li&gt;Private Terraform registry (Cloudflare Workers)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Start with the basic template. All custom modules derive from it — maintaining consistency across the infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;module-hierarchy&#34;&gt;Module Hierarchy
&lt;/h2&gt;&lt;pre class=&#34;mermaid&#34;&gt;
  graph TB
    subgraph &amp;#34;Root Module&amp;#34;
        A[tf-infra-homelab]
    end
    
    subgraph &amp;#34;Compute Modules&amp;#34;
        B[tf-module-proxmox-lxc]
        C[tf-module-proxmox-vm]
        D[tf-module-proxmox-talos]
        E[tf-module-proxmox-docker]
    end
    
    subgraph &amp;#34;Application Layer&amp;#34;
        F[applications-homelab]
    end
    
    subgraph &amp;#34;Platform&amp;#34;
        G[Proxmox VE]
    end
    
    A --&amp;gt; B
    A --&amp;gt; C
    A --&amp;gt; D
    A --&amp;gt; E
    D --&amp;gt; F
    E --&amp;gt; F
    B --&amp;gt; G
    C --&amp;gt; G
    D --&amp;gt; G
    E --&amp;gt; G
&lt;/pre&gt;

&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Module&lt;/th&gt;
          &lt;th&gt;Purpose&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;terraform-basic-template&lt;/td&gt;
          &lt;td&gt;Foundation for all modules&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tf-module-proxmox-lxc&lt;/td&gt;
          &lt;td&gt;LXC container provisioning&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tf-module-proxmox-vm&lt;/td&gt;
          &lt;td&gt;Full VM provisioning&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tf-module-proxmox-docker&lt;/td&gt;
          &lt;td&gt;Docker Swarm clusters&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tf-module-proxmox-talos&lt;/td&gt;
          &lt;td&gt;Talos Kubernetes clusters&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tf-infra-homelab&lt;/td&gt;
          &lt;td&gt;Root orchestration&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;applications-homelab&lt;/td&gt;
          &lt;td&gt;Kustomize deployments&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;github-management-plane&lt;/td&gt;
          &lt;td&gt;GitHub org management&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;the-dependency-graph&#34;&gt;The Dependency Graph
&lt;/h2&gt;&lt;pre class=&#34;mermaid&#34;&gt;
  graph TB
    T[terraform-basic-template]
    L[tf-module-proxmox-lxc]
    V[tf-module-proxmox-vm]
    DT[tf-module-proxmox-docker]
    TT[tf-module-proxmox-talos]
    RH[tf-infra-homelab]
    AH[applications-homelab]
    P[ProxmoxVE]
    
    T --&amp;gt; L
    T --&amp;gt; V
    L --&amp;gt; DT
    V --&amp;gt; DT
    L --&amp;gt; TT
    V --&amp;gt; TT
    DT --&amp;gt; RH
    TT --&amp;gt; RH
    RH --&amp;gt; P
    DT --&amp;gt; AH
    TT --&amp;gt; AH
&lt;/pre&gt;

&lt;p&gt;Key observations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Template is foundational&lt;/strong&gt; — all modules derive from the same template&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LXC and VM are leaf modules&lt;/strong&gt; — no dependencies on other custom modules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker and Talos are composite&lt;/strong&gt; — build on LXC/VM modules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root module is orchestrational&lt;/strong&gt; — composes modules based on configurations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applications deploy post-provisioning&lt;/strong&gt; — GitOps ties into Docker/Talos clusters&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;configuration-driven&#34;&gt;Configuration-Driven
&lt;/h2&gt;&lt;p&gt;All infrastructure is defined in YAML configurations, not ad-hoc Terraform runs:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-txt&#34;&gt;configurations/
├── docker/
│   ├── dev-docker-lxc.yaml
│   └── prod-docker-lxc.yaml
├── kubernetes/
│   ├── dev-k8s.yaml
│   └── prod-k8s.yaml
└── virtual_machine/
    └── ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each config has an &lt;code&gt;enabled&lt;/code&gt; flag for gradual rollout:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: prod-k8s
enabled: true  # Set to false to disable without deletion

cluster:
  name: prod-k8s
  datastore:
    id: nas
    node: alpha&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;whats-running&#34;&gt;What&amp;rsquo;s Running
&lt;/h2&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Cluster&lt;/th&gt;
          &lt;th&gt;Type&lt;/th&gt;
          &lt;th&gt;Nodes&lt;/th&gt;
          &lt;th&gt;VIP&lt;/th&gt;
          &lt;th&gt;Purpose&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;prod-docker-lxc&lt;/td&gt;
          &lt;td&gt;Docker Swarm&lt;/td&gt;
          &lt;td&gt;3x medium (8vCPU/32GB)&lt;/td&gt;
          &lt;td&gt;192.168.61.20&lt;/td&gt;
          &lt;td&gt;Container workloads&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;prod-k8s&lt;/td&gt;
          &lt;td&gt;Talos K8s&lt;/td&gt;
          &lt;td&gt;3x CP (4vCPU/8GB) + 3x worker (10vCPU/48GB)&lt;/td&gt;
          &lt;td&gt;192.168.62.20&lt;/td&gt;
          &lt;td&gt;Kubernetes workloads&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both clusters span all 3 Proxmox nodes for high availability.&lt;/p&gt;
&lt;h2 id=&#34;design-principles&#34;&gt;Design Principles
&lt;/h2&gt;&lt;p&gt;This architecture follows specific principles:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Principle&lt;/th&gt;
          &lt;th&gt;Implementation&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Single configuration object&lt;/td&gt;
          &lt;td&gt;All modules use unified &lt;code&gt;configuration&lt;/code&gt; input&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Host pools&lt;/td&gt;
          &lt;td&gt;Resilience through multi-node distribution&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Versioned modules&lt;/td&gt;
          &lt;td&gt;Each module has explicit versions&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;YAML configurations&lt;/td&gt;
          &lt;td&gt;Infrastructure as data, not ad-hoc apply&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Private registry&lt;/td&gt;
          &lt;td&gt;Distribution without Terraform Cloud cost&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Secrets integration&lt;/td&gt;
          &lt;td&gt;Bitwarden for credential storage&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;GitOps&lt;/td&gt;
          &lt;td&gt;Flux bootstrapped during cluster creation&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Multi-network&lt;/td&gt;
          &lt;td&gt;Separate DMZ and backend networks&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;GPU passthrough&lt;/td&gt;
          &lt;td&gt;Device mapping in host pool&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;what-most-people-get-wrong&#34;&gt;What Most People Get Wrong
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;More modules = better architecture&amp;rdquo;&lt;/strong&gt; — I started with 10+ modules. Consolidated to 5. Over-modularization creates maintenance overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;YAML = Terraform&amp;rdquo;&lt;/strong&gt; — Terraform is the engine, YAML is the fuel. Don&amp;rsquo;t embed YAML in &lt;code&gt;.tf&lt;/code&gt; files; load from external files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;GitOps replaces Terraform&amp;rdquo;&lt;/strong&gt; — They work together: Terraform provisions, Flux manages apps. Both are declarative.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;related-posts&#34;&gt;Related Posts
&lt;/h2&gt;&lt;p&gt;Each component has its own detailed post:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Post&lt;/th&gt;
          &lt;th&gt;Focus&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/talos-kubernetes-proxmox&#34; &gt;Talos Kubernetes on Proxmox&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;tf-module-proxmox-talos deep dive — image factory, machine config, bootstrap&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/docker-swarm-proxmox&#34; &gt;Docker Swarm on Proxmox&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;tf-module-proxmox-docker deep dive — Keepalived HA, provisioning&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/lxc-vm-modules&#34; &gt;LXC &amp;amp; VM Modules&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;tf-module-proxmox-lxc + tf-module-proxmox-vm basics&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/backstage-homelab&#34; &gt;Backstage Integration&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Catalog generation, software templates&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/terraform-registry-cloudflare-workers&#34; &gt;Private Terraform Registry&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Module distribution via Cloudflare Workers&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://zharif.my/posts/github-management-plane&#34; &gt;GitHub Management Plane&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Managing GitHub org via Terraform&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</description>
        </item>
        <item>
        <title>Private Terraform Registry on Cloudflare Workers</title>
        <link>https://zharif.my/posts/terraform-registry-cloudflare-workers/</link>
        <pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate>
        
        <guid>https://zharif.my/posts/terraform-registry-cloudflare-workers/</guid>
        <description>&lt;img src="https://images.unsplash.com/photo-1592659762303-90081d34b277?w=800&amp;h=400&amp;fit=crop" alt="Featured image of post Private Terraform Registry on Cloudflare Workers" /&gt;&lt;h2 id=&#34;the-alternatives&#34;&gt;The Alternatives
&lt;/h2&gt;&lt;p&gt;When you need to share Terraform modules across projects:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Option&lt;/th&gt;
          &lt;th&gt;Cost&lt;/th&gt;
          &lt;th&gt;Complexity&lt;/th&gt;
          &lt;th&gt;Trade-off&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Terraform Cloud&lt;/td&gt;
          &lt;td&gt;$20+/month&lt;/td&gt;
          &lt;td&gt;Low&lt;/td&gt;
          &lt;td&gt;Free tier doesn&amp;rsquo;t support custom modules&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Nexus/Artifactory&lt;/td&gt;
          &lt;td&gt;Free&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;Java-based, heavy for this use case&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Custom (this)&lt;/td&gt;
          &lt;td&gt;Free&lt;/td&gt;
          &lt;td&gt;Medium&lt;/td&gt;
          &lt;td&gt;Implement registry protocol yourself&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The killer: Terraform Cloud&amp;rsquo;s free tier doesn&amp;rsquo;t support private module registries. You need the paid plan. For homelab use, that&amp;rsquo;s 10x the cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this handles&lt;/strong&gt;: service discovery, version listing, download URL redirects, and zipball proxy — the full registry protocol.&lt;/p&gt;
&lt;h2 id=&#34;worker-endpoints&#34;&gt;Worker Endpoints
&lt;/h2&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Endpoint&lt;/th&gt;
          &lt;th&gt;Purpose&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/.well-known/terraform.json&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Service discovery — tells Terraform where the API lives&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/v1/modules/:ns/:name/:provider/versions&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Lists available versions (from GitHub tags)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/v1/modules/:ns/:name/:provider/:version/download&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Returns the download URL&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/archive/:ns/:name/:provider/:version&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Proxies the actual module download&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;the-implementation&#34;&gt;The Implementation
&lt;/h2&gt;&lt;p&gt;Let me walk through the actual code. The worker is written in Python using Pyodide, which means full CPython 3.12 in WebAssembly.&lt;/p&gt;
&lt;h3 id=&#34;service-discovery&#34;&gt;Service Discovery
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;async def handle_well_known(req, env):
    &amp;#34;&amp;#34;&amp;#34;Handle /.well-known/terraform.json&amp;#34;&amp;#34;&amp;#34;
    response = {
        &amp;#34;modules.v1&amp;#34;: f&amp;#34;{env.get(&amp;#39;BASE_URL&amp;#39;, &amp;#39;https://terraform.example.com&amp;#39;)}/v1/modules/&amp;#34;
    }
    return json_response(response)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple enough — returns a JSON object telling Terraform where to find the modules API.&lt;/p&gt;
&lt;h3 id=&#34;version-listing&#34;&gt;Version Listing
&lt;/h3&gt;&lt;p&gt;This is where things get interesting. I needed to fetch GitHub tags and filter for valid semver:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;def filter_semver_tags(tags: list[str]) -&amp;gt; list[str]:
    &amp;#34;&amp;#34;&amp;#34;Filter tags to only valid semver versions.&amp;#34;&amp;#34;&amp;#34;
    valid_tags = []
    for tag in tags:
        if _SEMVER_RE.match(tag):
            valid_tags.append(tag)
    return sorted(valid_tags, key=parse_version, reverse=True)

# Semver pattern from actual implementation
_SEMVER_RE = re.compile(r&amp;#34;^v?\d&amp;#43;\.\d&amp;#43;\.\d&amp;#43;(?:[.&amp;#43;-].&amp;#43;)?$&amp;#34;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The regex ensures only proper semver tags (&lt;code&gt;v1.0.0&lt;/code&gt;, &lt;code&gt;2.3.1&lt;/code&gt;, etc.) are returned. Prerelease versions like &lt;code&gt;v1.0.0-beta&lt;/code&gt; are supported too.&lt;/p&gt;
&lt;h3 id=&#34;authentication-the-github-app-dance&#34;&gt;Authentication: The GitHub App Dance
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s where it gets clever. Instead of requiring users to generate GitHub tokens, the worker handles authentication server-side using a GitHub App:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;class GitHubAppAuth:
    def __init__(self, app_id: str, private_key: str, installation_id: str):
        self.app_id = app_id
        self.private_key = private_key
        self.installation_id = installation_id
        self._token_cache = None
        self._token_expiry = 0

    async def get_token(self) -&amp;gt; str:
        &amp;#34;&amp;#34;&amp;#34;Get cached installation token, refreshing if needed.&amp;#34;&amp;#34;&amp;#34;
        now = time.time()
        if self._token_cache and now &amp;lt; self._token_expiry:
            return self._token_cache
        
        # Generate JWT and exchange for token
        jwt = self._generate_jwt()
        token = await self._exchange_jwt_for_token(jwt)
        
        # Cache for 55 minutes (tokens last 60, safe margin)
        self._token_cache = token
        self._token_expiry = now &amp;#43; 3300
        return token

    def _generate_jwt(self) -&amp;gt; str:
        &amp;#34;&amp;#34;&amp;#34;Create signed JWT using RS256.&amp;#34;&amp;#34;&amp;#34;
        # ... WebCrypto implementation&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The worker generates a JWT signed with the GitHub App&amp;rsquo;s private key, exchanges it for an Installation Access Token, and caches that token for 55 minutes. This avoids rate limiting and keeps things snappy.&lt;/p&gt;
&lt;h2 id=&#34;design-considerations&#34;&gt;Design Considerations
&lt;/h2&gt;&lt;h3 id=&#34;why-server-side-auth&#34;&gt;Why Server-Side Auth?
&lt;/h3&gt;&lt;p&gt;The alternative would be API tokens in &lt;code&gt;.terraformrc&lt;/code&gt;. But that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Users need to generate tokens&lt;/li&gt;
&lt;li&gt;Tokens expire and need refreshing&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re exposing long-lived credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With server-side GitHub App auth, Terraform just talks to your worker — no credentials needed on the client side.&lt;/p&gt;
&lt;h3 id=&#34;token-caching-strategy&#34;&gt;Token Caching Strategy
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;# Using Cloudflare&amp;#39;s Cache API for cross-request caching
cache = caches.default
cache_key = f&amp;#34;gh-token-{installation_id}&amp;#34;
cached = await cache.get(cache_key)

if cached:
    token = cached.text
else:
    token = await github_app.get_token()
    # Cache for 55 minutes
    response = Response.new(token, headers={
        &amp;#34;Cache-Control&amp;#34;: &amp;#34;max-age=3300&amp;#34;
    })
    await cache.put(cache_key, response)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is critical because GitHub rate limits would hit hard without caching.&lt;/p&gt;
&lt;h3 id=&#34;download-url-handling&#34;&gt;Download URL Handling
&lt;/h3&gt;&lt;p&gt;Terraform expects the download endpoint to return a &lt;code&gt;X-Terraform-Get&lt;/code&gt; header with the location of the actual module archive. I point it back to my worker:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;async def handle_module_download(req, env, ns, name, provider, version):
    # Return redirect to archive endpoint
    archive_url = f&amp;#34;/archive/{ns}/{name}/{provider}/{version}&amp;#34;
    return Response.new(&amp;#34;&amp;#34;, headers={
        &amp;#34;X-Terraform-Get&amp;#34;: archive_url
    })&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the archive endpoint fetches from GitHub&amp;rsquo;s zipball URL and streams it through.&lt;/p&gt;
&lt;h2 id=&#34;constraints-and-limits&#34;&gt;Constraints and Limits
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t a &amp;ldquo;run anything anywhere&amp;rdquo; solution. There are real constraints:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Constraint&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
          &lt;th&gt;Implication&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;CPU Time&lt;/td&gt;
          &lt;td&gt;30s (Paid) / 10ms (Free)&lt;/td&gt;
          &lt;td&gt;Complex auth may timeout on free tier&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;128 MB&lt;/td&gt;
          &lt;td&gt;Enough for Python + JSON parsing&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Packages&lt;/td&gt;
          &lt;td&gt;Pyodide stdlib only&lt;/td&gt;
          &lt;td&gt;No external dependencies&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Terraform Cloud&amp;rsquo;s free tier doesn&amp;rsquo;t support private module registries. You need the paid plan.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;on-prem-deployment-with-workerd&#34;&gt;On-Prem Deployment with workerd
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes this interesting for self-hosted scenarios: these workers can run locally using &lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/workerd&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;workerd&lt;/a&gt;, Cloudflare&amp;rsquo;s Workers runtime as a container.&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# docker-compose.yml for local workerd
services:
  workerd:
    image: cloudflare/workerd:latest
    ports:
      - &amp;#34;8787:8787&amp;#34;
    volumes:
      - ./config.workerd:/etc/workerd/config.capnp
    environment:
      - PORT=8787&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;// config.workerd (JavaScript service binding syntax)
services: [
  {
    name: &amp;#34;terraform-registry&amp;#34;,
    script: readFile(&amp;#34;dist/worker.js&amp;#34;),
    bindings: {
      GITHUB_APP_ID: &amp;#34;123456&amp;#34;,
      GITHUB_INSTALLATION_ID: &amp;#34;789012&amp;#34;,
    }
  }
]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This means you can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Develop and test workers locally&lt;/li&gt;
&lt;li&gt;Run the same code on Cloudflare&amp;rsquo;s edge&lt;/li&gt;
&lt;li&gt;Deploy on-prem for air-gapped environments&lt;/li&gt;
&lt;li&gt;Use identical infrastructure everywhere&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;usage-example&#34;&gt;Usage Example
&lt;/h2&gt;&lt;p&gt;With everything configured, using the registry is straightforward:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;# main.tf
module &amp;#34;proxmox_vm&amp;#34; {
  source  = &amp;#34;registry.example.com/namespace/tf-module-proxmox-vm/proxmox&amp;#34;
  version = &amp;#34;1.0.7&amp;#34;

  configuration = {
    name        = &amp;#34;web-server&amp;#34;
    node_name   = &amp;#34;pve1&amp;#34;
    cpu = { cores = 2, type = &amp;#34;host&amp;#34; }
    memory = { dedicated = 4096 }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the CLI configuration:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;# ~/.terraformrc
host &amp;#34;registry.example.com&amp;#34; {
  services = {
    &amp;#34;modules.v1&amp;#34; = &amp;#34;https://registry.example.com/v1/modules/&amp;#34;
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No credentials needed — the worker handles everything server-side.&lt;/p&gt;
&lt;h2 id=&#34;what-most-people-get-wrong&#34;&gt;What Most People Get Wrong
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Free tier has no limits&amp;rdquo;&lt;/strong&gt; — 10ms CPU time free, 30s paid. Complex auth on free tier may timeout.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Pyodide supports all Python packages&amp;rdquo;&lt;/strong&gt; — No. ~150 bundled, no arbitrary pip install. Use WebCrypto via JavaScript instead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;One worker does everything&amp;rdquo;&lt;/strong&gt; — For high traffic, add caching. GitHub API rate limits (5K/hour) apply.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;when-to-use--when-not-to-use&#34;&gt;When to Use / When NOT to Use
&lt;/h2&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Use Private Registry&lt;/th&gt;
          &lt;th&gt;Use Terraform Cloud&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Free tier budget&lt;/td&gt;
          &lt;td&gt;Team with &amp;gt;5 users&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;10-50 modules&lt;/td&gt;
          &lt;td&gt;100+ modules&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Simple requirements&lt;/td&gt;
          &lt;td&gt;Policy enforcement needed&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Air-gapped capable&lt;/td&gt;
          &lt;td&gt;Cloud-hosted required&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;p&gt;This pattern — serverless registry with on-prem fallback — has proven so useful I&amp;rsquo;ve applied it to APT repositories, which I&amp;rsquo;ll cover in the next post. The same architecture, different protocol, same benefits.&lt;/p&gt;
&lt;p&gt;This pattern can be adapted to your own infrastructure by implementing the Terraform Module Registry Protocol on any serverless or traditional hosting platform.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
