<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anthony Owens</title>
    <description>The latest articles on DEV Community by Anthony Owens (@tonedefdev).</description>
    <link>https://dev.to/tonedefdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948129%2F0874796c-a3ed-4920-b901-bc3968cda0bc.png</url>
      <title>DEV Community: Anthony Owens</title>
      <link>https://dev.to/tonedefdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonedefdev"/>
    <language>en</language>
    <item>
      <title>The Interface Module Pattern</title>
      <dc:creator>Anthony Owens</dc:creator>
      <pubDate>Fri, 18 Sep 2026 15:25:00 +0000</pubDate>
      <link>https://dev.to/tonedefdev/the-interface-module-pattern-58a2</link>
      <guid>https://dev.to/tonedefdev/the-interface-module-pattern-58a2</guid>
      <description>&lt;p&gt;I was doomscrolling through Reddit recently when I came across a post that caught my attention:&lt;/p&gt;


&lt;blockquote&gt;
&lt;br&gt;
&lt;a href="https://www.reddit.com/r/devops/comments/1w42bxs/anyone_else_seeing_ai_make_devopsinfra_the/" rel="noopener noreferrer"&gt;Anyone else seeing AI make DevOps/infra the bottleneck?&lt;/a&gt;&lt;br&gt; by&lt;br&gt;
&lt;a href="https://www.reddit.com/user/FaithlessnessEqual44/" rel="noopener noreferrer"&gt;u/FaithlessnessEqual44&lt;/a&gt; in&lt;br&gt;
&lt;a href="https://www.reddit.com/r/devops/" rel="noopener noreferrer"&gt;devops&lt;/a&gt;&lt;br&gt;
&lt;/blockquote&gt;


&lt;p&gt;The post describes a problem that many platform teams may recognize: AI-assisted development has made application developers dramatically more productive, but it has also increased the volume of infrastructure changes arriving for review.&lt;/p&gt;

&lt;p&gt;Generated Terraform often looks solid in isolation. The difficulty is that developers, and often the LLMs assisting them, do not have enough context about the larger environment to understand how a seemingly reasonable change could interact with EKS, IAM, networking, security, CI/CD, and the organization’s existing platform conventions.&lt;/p&gt;

&lt;p&gt;The result is an infrastructure bottleneck. AI was supposed to reduce the platform team’s workload, yet more of that workload shifted toward reviewing generated changes, explaining the surrounding architecture, correcting misunderstandings, and helping developers through multiple iterations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We are in an age where we can create content far faster than we can consume it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided to join the discussion because I had encountered a similar problem before LLMs became part of the modern development workflow. To my surprise, the comment gained significant traction, and several Redditors wanted to know what I meant by opinionated, self-service IaC built around a unified interface.&lt;/p&gt;

&lt;p&gt;So, here's my approach and an explanation of what I call the Interface Module pattern. The pattern is best defined as a narrow, opinionated infrastructure API that gives consumers a declarative way to request platform capabilities without requiring them to understand every underlying implementation detail. The result is self-service infrastructure that developers and LLMs alike can deploy independently, while the platform team retains control over security, standards, and operational behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you prefer to see an example implementation before reading about the design principles, jump to See It in Practice, where I link to two repositories I created for this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What is an Interface Module?
&lt;/h2&gt;

&lt;p&gt;An Interface Module is an IaC module that provides a minimal consumer API for orchestrating lower-level modules in an opinionated fashion without exposing users to the underlying system's complexity.&lt;/p&gt;

&lt;p&gt;The term Interface Module is not a formal &lt;code&gt;terraform&lt;/code&gt; or &lt;code&gt;opentofu&lt;/code&gt; term. I use &lt;code&gt;interface&lt;/code&gt; deliberately because the module’s primary purpose is to define the supported consumer contract, not merely to bundle resources.&lt;/p&gt;

&lt;p&gt;A normal &lt;code&gt;terraform&lt;/code&gt; module packages reusable infrastructure. An Interface Module packages an opinionated way of consuming a platform's capabilities.&lt;/p&gt;

&lt;p&gt;An Interface Module should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expose a deliberately small consumer API. The underlying modules may support significantly more capabilities, but the Interface Module exposes only the subset users need.&lt;/li&gt;
&lt;li&gt;Define a specification that is completely declarative.&lt;/li&gt;
&lt;li&gt;Hide low-level platform and system complexity through abstraction layers.&lt;/li&gt;
&lt;li&gt;Establish secure and opinionated defaults.&lt;/li&gt;
&lt;li&gt;Separate the consumer contract from the implementation details.&lt;/li&gt;
&lt;li&gt;Be released, tested, documented, and operated like a product.&lt;/li&gt;
&lt;li&gt;Allow the platform team to evolve the internals without forcing every consumer to understand them.
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Consumer["Application team"]
    Interface["Interface Module"]
    Lambda["Lambda module"]
    IAM["IAM module"]
    ECR["ECR module"]
    S3["S3 module"]

    Consumer --&amp;gt;|"lambda_functions\ns3_buckets"| Interface
    Interface --&amp;gt; Lambda
    Interface --&amp;gt; IAM
    Interface --&amp;gt; ECR
    Interface --&amp;gt; S3
    IAM --&amp;gt;|"role ARN"| Lambda
    ECR --&amp;gt;|"repository URL"| Lambda
    S3 --&amp;gt;|"bucket ARN"| Lambda&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;
  
  
  Consumer API Contracts
&lt;/h2&gt;

&lt;p&gt;Let’s start by discussing what a consumer API contract means in the context of an Interface Module. To make a self-service module easier to use, we need to design our inputs with the utmost care. In the &lt;code&gt;opentofu&lt;/code&gt; and &lt;code&gt;terraform&lt;/code&gt; world, &lt;code&gt;variables&lt;/code&gt; become our input contracts, and &lt;code&gt;outputs&lt;/code&gt; define the output contracts. For consumer-facing interfaces in an Interface Module, well-designed variables are critical.&lt;/p&gt;

&lt;p&gt;Interface Modules should typically use &lt;code&gt;for_each&lt;/code&gt; to create resources from their variables. The contracts should be modeled as &lt;code&gt;map(object)&lt;/code&gt; types. Use the key of the &lt;code&gt;map&lt;/code&gt; to form resource names. This provides early collision detection since &lt;code&gt;terraform&lt;/code&gt; and &lt;code&gt;tofu&lt;/code&gt; require &lt;code&gt;maps&lt;/code&gt; to have unique keys. This can also serve as a reference selector when coordinating lower-level modules. Use &lt;code&gt;object&lt;/code&gt; types to define complex input contracts for multiple resources, in addition to providing a mechanism for optional configurations and sane defaults.&lt;/p&gt;

&lt;p&gt;Here's a small example of a &lt;code&gt;lambda&lt;/code&gt; contract that would be exposed from an Interface Module to orchestrate building any number of &lt;code&gt;lambda&lt;/code&gt; resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"lambda_functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A map of Lambda Functions specs to deploy."&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;concurrent_executions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;description&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
      &lt;span class="nx"&gt;environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
      &lt;span class="nx"&gt;timeout&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nx"&gt;ecr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;image_tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;lambda_functions&lt;/code&gt; variable could then be called by a user to orchestrate the &lt;code&gt;lambda&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dev.tfvars&lt;/span&gt;
&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Processes order events"&lt;/span&gt;

      &lt;span class="nx"&gt;ecr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;image_tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2026.09.1"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lower-level &lt;code&gt;lambda&lt;/code&gt; module is then wired up to build any lambda function it receives from the &lt;code&gt;map&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lambda.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"lambda_functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/terraform-aws-lambda?ref=v1.1.1"&lt;/span&gt;

  &lt;span class="nx"&gt;concurrent_executions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concurrent_executions&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;
  &lt;span class="nx"&gt;environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment_variables&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;image_uri&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${module.ecr[each.key].repo_url}:${each.value.spec.ecr.image_tag}"&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hide Complexity
&lt;/h2&gt;

&lt;p&gt;From the previous example, we can see that two contractual inputs have already been determined for the user. The &lt;code&gt;iam_role_arn&lt;/code&gt; is provided to the lower-level &lt;code&gt;lambda&lt;/code&gt; module through the output from &lt;code&gt;module.iam.role_arns[each.key]&lt;/code&gt;. The Elastic Container Registry (ecr) module also uses its &lt;code&gt;repo_url&lt;/code&gt; output to generate the URL for the &lt;code&gt;image_uri&lt;/code&gt; while also concatenating the string with the user's supplied &lt;code&gt;image_tag&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;Here's what is happening under the hood of the interface module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# iam.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"iam"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/terraform-aws-iam?ref=v1.2.0"&lt;/span&gt;

  &lt;span class="nx"&gt;account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_account_id&lt;/span&gt;

  &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda-execution-${replace(key, "&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;")}"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ecr.tf&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_ecr_pull"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"ecr:BatchGetImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"ecr:GetDownloadUrlForLayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lambda.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringLike"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceARN"&lt;/span&gt;

      &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${replace(key, "&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;")}"&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"ecr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/terraform-aws-ecr?ref=v1.0.0"&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"defdevio/lambda-${replace(each.key, "&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;")}"&lt;/span&gt;
  &lt;span class="nx"&gt;is_immutable&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
  &lt;span class="nx"&gt;repo_policy_document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_ecr_pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a significant amount of complexity that is now completely transparent to the user. With minimal inputs, a user produces a result that is secure and ready for deployment without having to cobble the pieces together on their own. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IAM&lt;/code&gt; module takes care of the heavy lifting based on its own inherent design. It sets up the Lambda's execution role and ensures it has a condition that only allows the source account's Lambda to assume it. &lt;/p&gt;

&lt;p&gt;On the other hand, the Interface Module ensures it uses a standardized naming convention that is based on the &lt;code&gt;var.lambda_functions&lt;/code&gt; key name where each function is provided its own dedicated role, and automatically orchestrates the output contracts by wiring them into the relevant input fields.&lt;/p&gt;

&lt;p&gt;The Interface Module also sets up the ECR with a policy that allows only the Lambda functions to perform the necessary actions to pull the image they will use. It also standardizes the repository name and ensures that it follows the organization's naming conventions to keep things consistent across environments. The Interface Module decides which environments can have mutable images. &lt;/p&gt;

&lt;p&gt;In the example's case, if the environment is &lt;code&gt;dev&lt;/code&gt;, the Interface Module allows the image to be mutable to support faster development feedback loops. However, once we promote it to a higher-level environment like &lt;code&gt;test&lt;/code&gt; or &lt;code&gt;prod&lt;/code&gt;, the image is always immutable. This helps to enforce CI/CD promotion practices. We should be continuously integrating changes in our pipelines, not pushing manual changes to images we haven't sent through the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be Opinionated About Security and Non-Functional Requirements
&lt;/h2&gt;

&lt;p&gt;The previous example also demonstrates how an Interface Module can enforce security and non-functional requirements on behalf of its consumers. The ECR repositories are created with policies that grant image-pull access only to the Lambda functions declared in &lt;code&gt;var.lambda_functions&lt;/code&gt;. The policy is generated automatically from the consumer’s input, applying least-privilege access without requiring the consumer to write IAM policy documents.&lt;/p&gt;

&lt;p&gt;The policy also restricts access by the Lambda function’s source ARN. This helps protect against the AWS confused deputy problem by ensuring that another Lambda function, even within the same account, cannot use the repository simply because it runs under the same AWS service principal. These safeguards are part of the Interface Module’s implementation rather than optional configuration that each consumer must understand and reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for Personas, Not Use Cases
&lt;/h2&gt;

&lt;p&gt;Interface Modules should be designed around the behaviors and needs of specific personas within an organization. They should be opinionated enough to streamline development operations and increase delivery velocity, while the lower-level modules remain flexible enough to support platform teams and unusual integrations.&lt;/p&gt;

&lt;p&gt;Platform teams often need bespoke configurations for third-party integrations. If those requirements were enforced directly in a lower-level module, the module could become cluttered with exceptions and feature flags. Instead, the Interface Module provides the appropriate contract for each consumer group. Teams with less infrastructure experience get a focused interface with sensible safeguards, while platform teams can use the lower-level modules directly when they need capabilities outside that contract.&lt;/p&gt;

&lt;p&gt;This also means lower-level modules do not need to be fractured into separate variants for every persona. An Interface Module can expose only the capabilities appropriate for its consumers while the underlying module continues to support a broader set of use cases.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Platform["Platform team"]
    App["Application team"]

    Lower["Lower-level modules\nbroad and flexible"]
    Interface["Interface Module\nopinionated contract"]

    Platform --&amp;gt; Lower
    App --&amp;gt; Interface
    Interface --&amp;gt; Lower&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;For example, the lower-level &lt;code&gt;terraform-aws-iam&lt;/code&gt; module shown earlier does not validate whether &lt;code&gt;custom_iam_policy_statements.resources&lt;/code&gt; contains a wildcard value such as &lt;code&gt;*&lt;/code&gt;. A wildcard may be legitimate for some platform integrations, such as account-wide monitoring. Adding that validation to the lower-level module would either prevent those use cases or require a feature flag that could be easy to overlook.&lt;/p&gt;

&lt;p&gt;Let’s return to the Lambda contract from earlier with this new context in mind. We can extend it with custom IAM policy statements for cases where application-specific permissions are needed. The lower-level IAM module supports this capability, but the Interface Module can apply stricter rules for application developers.&lt;/p&gt;

&lt;p&gt;Developers commonly reach for &lt;code&gt;*&lt;/code&gt; when they are trying to get an application working quickly. The Interface Module can prevent that shortcut by rejecting wildcard values in both &lt;code&gt;actions&lt;/code&gt; and &lt;code&gt;resources&lt;/code&gt; before the configuration produces a valid plan.&lt;/p&gt;

&lt;p&gt;The relevant addition to the original contract looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Added inside `var.lambda_functions.spec`&lt;/span&gt;
&lt;span class="nx"&gt;custom_iam_policy_statements&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;conditions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then prevent users from passing wildcard values in the &lt;code&gt;resources&lt;/code&gt; or &lt;code&gt;actions&lt;/code&gt; fields by validating the input like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;alltrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_iam_policy_statements&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; 
      &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]]&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOM&lt;/span&gt;&lt;span class="sh"&gt;
    Lambda functions must not use wildcard (*) in their 
    custom IAM policy statement resources or actions.
&lt;/span&gt;&lt;span class="no"&gt;  EOM
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we pass the validated statements through the IAM orchestration we saw earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda-execution-${replace(key, "&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;")}"&lt;/span&gt;
    &lt;span class="nx"&gt;custom_iam_policy_statements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_iam_policy_statements&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wire in Dependencies Automatically
&lt;/h2&gt;

&lt;p&gt;Let’s take this one step further by showing how an Interface Module can help developers consume the opinionated services it creates. We’ll add an &lt;code&gt;s3_buckets&lt;/code&gt; contract and use it to provision the lower-level &lt;code&gt;terraform-aws-s3&lt;/code&gt; module. Then we’ll use the resulting bucket ARN as a Lambda environment variable, allowing the application to access the bucket without requiring the developer to assemble or pass those resource details manually.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Input["lambda_functions.orders"]
    BucketInput["s3_buckets.order_exports"]
    Key["resource_key_ref = orders"]
    Role["IAM role for orders"]
    Bucket["S3 bucket"]
    Env["DEFDEVIO_BUCKET_ARNS"]
    Runtime["Lambda runtime"]

    Input --&amp;gt; Role
    BucketInput --&amp;gt; Key
    Key --&amp;gt; Role
    BucketInput --&amp;gt; Bucket
    Role --&amp;gt; Bucket
    Bucket --&amp;gt; Env
    Env --&amp;gt; Runtime&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;First, we’ll define the consumer contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"s3_buckets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3 bucket specifications, optionally associated with a Lambda function key."&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource_key_ref&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;source_file_path&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;source_file_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;alltrue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_buckets&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; 
        &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; 
        &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Each non-null S3 resource_key_ref must match a key in lambda_functions."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wire it to the lower-level S3 module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# s3.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_buckets&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/terraform-aws-s3?ref=v1.2.0"&lt;/span&gt;

  &lt;span class="nx"&gt;resource_key_ref&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt;
  &lt;span class="nx"&gt;source_file_path&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_file_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_file_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_file_pattern&lt;/span&gt;

  &lt;span class="nx"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimsuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;"${var.aws_account_id}-${replace(each.key, "&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;")}-${var.aws_region}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;63&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"arn:aws:iam::%s:role/lambda-execution-%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The optional &lt;code&gt;resource_key_ref&lt;/code&gt; lets a bucket declare which Lambda function should receive access. The Interface Module resolves that reference against the roles it created for &lt;code&gt;var.lambda_functions&lt;/code&gt; and passes the resulting role ARN to the lower-level S3 module.&lt;/p&gt;

&lt;p&gt;This keeps the relationship expressed as a logical function key instead of forcing developers to manually look up or pass IAM role ARNs. The same value is also passed through to the lower-level module so it can associate each bucket with the correct Lambda function.&lt;/p&gt;

&lt;p&gt;We also added a validation block to ensure that a non-null &lt;code&gt;resource_key_ref&lt;/code&gt; matches one of the keys in the &lt;code&gt;var.lambda_functions&lt;/code&gt; map. This gives developers early feedback before planning can proceed with an invalid relationship.&lt;/p&gt;

&lt;p&gt;The Interface Module derives the bucket name from the account ID, resource key, and AWS Region. It also constructs the expected &lt;code&gt;iam_role_arn&lt;/code&gt; so the dependency can be resolved during planning, even when the related resources use &lt;code&gt;count&lt;/code&gt; or &lt;code&gt;for_each&lt;/code&gt; and are not known until apply time. &lt;/p&gt;

&lt;p&gt;This avoids collisions between environments while enforcing the platform’s naming convention. The Interface Module also ensures that the bucket name is no longer than 63 characters to comply with Amazon S3 bucket naming requirements.&lt;/p&gt;

&lt;p&gt;Next, we’ll make the resulting bucket ARN available to the Lambda function that references the bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lambda.tf&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lambda_environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;function_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;function_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"DEFDEVIO_BUCKET_ARNS"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_buckets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_arn&lt;/span&gt; 
        &lt;span class="nx"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_key_ref&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;function_key&lt;/span&gt;
      &lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"lambda_functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_functions&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/terraform-aws-lambda?ref=v1.1.1"&lt;/span&gt;

  &lt;span class="nx"&gt;concurrent_executions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concurrent_executions&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;image_uri&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${module.ecr[each.key].repo_url}:${each.value.spec.ecr.image_tag}"&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;

  &lt;span class="nx"&gt;environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment_variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_environment_variables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is another example of how the Interface Module hides complexity. We generate environment variable maps from &lt;code&gt;var.lambda_functions&lt;/code&gt; and can extend them later as the module adds support for more resource types. &lt;/p&gt;

&lt;p&gt;In this example, we create a &lt;code&gt;DEFDEVIO_BUCKET_ARNS&lt;/code&gt; environment variable containing a comma-separated list of bucket ARNs. The list includes only the buckets whose &lt;code&gt;resource_key_ref&lt;/code&gt; matches the current Lambda function’s key. &lt;/p&gt;

&lt;p&gt;The application can then discover its associated buckets without needing to look up or construct those ARNs itself. This pattern becomes especially useful for resources that generate unique names or endpoints. RDS clusters, ElastiCache clusters, and AWS Secrets Manager secrets include generated characters to ensure uniqueness. By automatically making them available to the application's runtime environment, developers no longer need to use &lt;code&gt;data&lt;/code&gt; resources or application SDKs just to fetch information already known in state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test, Test, and Test Some More!
&lt;/h2&gt;

&lt;p&gt;The key differentiator between a simple abstraction layer and an enterprise platform product is testability. IaC testing is often overlooked, but moving from treating IaC as a script to treating it as a product means automated testing must be part of the service offering. Just as software teams use unit and integration tests to build confidence in a release, the Interface Module and its lower-level modules need tests that provide the same kind of assurance.&lt;/p&gt;

&lt;p&gt;The lower-level modules define their own unit-test contracts. You can use the built-in testing frameworks in &lt;code&gt;tofu&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt; to mock providers, or use a full testing suite such as &lt;code&gt;terratest&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;I personally recommend &lt;code&gt;terratest&lt;/code&gt; because it is written in Go and can validate behavior after apply, rather than only checking mocked plan-time behavior. Go also gives you access to the &lt;code&gt;testify&lt;/code&gt; package, which provides useful &lt;code&gt;assert&lt;/code&gt; and &lt;code&gt;require&lt;/code&gt; helpers for writing readable tests. &lt;/p&gt;

&lt;p&gt;For post-apply verification, you can use Gruntwork's AWS helpers or call the AWS SDK for Go directly to inspect the resulting infrastructure and assert its actual state. In my experience, many of the most important complications appear at apply time, which is where &lt;code&gt;terratest&lt;/code&gt; shines.&lt;/p&gt;

&lt;p&gt;Here is an excerpt from the &lt;code&gt;terratest&lt;/code&gt; suite for the lower-level &lt;code&gt;terraform-aws-iam&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// main.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestCustomIAMPolicyStatements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DestroyContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndPlanAndShowWithStructContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;requireResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceChangesMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`module.iam.aws_iam_role.this["application"]`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assertTrustPolicyUsesSourceAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assertCustomPolicyContainsExpectedStatements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndApplyAndIdempotentContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test provisions the module with a preset fixture and converts the resulting plan into a Go struct. This lets us inspect the planned resources and verify that the expected trust policies and custom IAM policy statements are present. We then apply the module and run &lt;code&gt;InitAndApplyAndIdempotent&lt;/code&gt;, which verifies that a subsequent apply produces no changes. That gives us confidence that the module is both correctly configured and idempotent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The full suite performs additional assertions, but this excerpt focuses on the core testing workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, the Interface Module tests validate the orchestration layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// main.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestInterfaceModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DestroyContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndPlanAndShowWithStructContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;requireResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceChangesMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`module.iam.aws_iam_role.this["orders"]`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;requireResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceChangesMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`module.ecr["orders"].aws_ecr_repository.this`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitAndApplyAndIdempotentContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetECRRepoContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"us-west-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ecrName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ecrName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RepositoryName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"IMMUTABLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImageTagMutability&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test is very similar to the previous lower-level module test, except that it focuses more on apply-time behavior. The suite runs the plan and returns it as a Go struct, then validates that the expected resources are present based on the test fixture inputs. It runs &lt;code&gt;InitAndApplyAndIdempotent&lt;/code&gt; to verify that the module applies successfully and that a subsequent apply is a no-op. &lt;/p&gt;

&lt;p&gt;The harness then checks the resulting resource state against the actual API to ensure that the orchestration layer produces resources that follow our naming conventions, security standards, and other requirements for self-service consumption.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;terratest&lt;/code&gt; suite should run on every pull request as a required check. In GitHub Actions, this can be implemented with an &lt;code&gt;on.pull_request&lt;/code&gt; workflow and a repository ruleset that requires the check to pass before the pull request can be merged. The pull request also requires two approvals from platform team members listed as &lt;code&gt;CODEOWNERS&lt;/code&gt;. Once the pull request is merged, a separate workflow releases the module to a hosted module registry.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Word of advice: implement a proper module registry and release process before adopting this pattern! We initially only used GitHub with &lt;code&gt;refs?=&amp;lt;VERSION&amp;gt;&lt;/code&gt;; however, delivering patches and getting development teams to update versions became an administrative burden. Once we switched to a dedicated module registry, we advised our developers to use the pessimistic version constraint operator &lt;code&gt;version = ~&amp;gt; 1.0.0&lt;/code&gt;, so delivering patches became significantly easier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Delivering a Production Self-Service Solution
&lt;/h2&gt;

&lt;p&gt;The Interface Module I mentioned in my Reddit comment was much broader in production than the simplified examples in this article. It provided a catalog of the infrastructure capabilities that application teams commonly needed:&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Delivery
&lt;/h3&gt;

&lt;p&gt;Teams could deploy Argo CD applications through a centralized Helm chart with platform services such as Cilium Network Policies, Argo Events, Rollouts, Workflows, External Secrets, and Litmus Chaos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technologies:&lt;/strong&gt; Argo CD, Helm, Cilium, Argo Events, Rollouts, Workflows, External Secrets&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity and Security
&lt;/h3&gt;

&lt;p&gt;The module created IAM roles, IRSA integrations, security groups, certificates, KMS keys, and least-privilege policies for the resources each application declared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technologies:&lt;/strong&gt; IAM, IRSA, ACM, KMS, Security Groups, Secrets Manager&lt;/p&gt;

&lt;h3&gt;
  
  
  Data and Messaging
&lt;/h3&gt;

&lt;p&gt;Application teams could request databases, caches, object storage, search, queues, and notifications without assembling the dependencies between those resources themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technologies:&lt;/strong&gt; Aurora PostgreSQL, Aurora Global Database, ElastiCache, OpenSearch, S3, SNS, SQS, EventBridge&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All the modules that support IAM policies receive the relevant resource ARNs from the Interface Module so they can apply least-privilege policies. Where possible, we prefer resource-based policies over identity-based policies because this helps us avoid running into limits on the number of policies attached to an IAM role. We apply the same principle to KMS keys: we create a single KMS key for each module call and use it to encrypt any resources that support KMS encryption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The catalog’s size is less important than the fact that these capabilities can be composed through a single, application-oriented specification. A team can define its application, the resources it needs, and the relationships between those resources without orchestrating all the module dependencies on their own. This was especially helpful when platform teams needed to troubleshoot. They could look at the Interface Module configuration and quickly understand the infrastructure involved.&lt;/p&gt;

&lt;p&gt;It is worth looking at the finished Interface Module from a platform engineer’s perspective. At a glance, the application configuration shows which infrastructure the application uses and how those resources relate to one another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/defdevio/interface-module-example?ref=v1.0.1"&lt;/span&gt;

  &lt;span class="nx"&gt;lambda_functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Processes order events"&lt;/span&gt;

        &lt;span class="nx"&gt;ecr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;image_tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2026.09.1"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;environment_variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;custom_iam_policy_statements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadOrders"&lt;/span&gt;
            &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:dynamodb:${var.aws_region}:${var.aws_account_id}:table/orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;s3_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;order_exports&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;resource_key_ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"orders"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;shared_assets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;orders&lt;/code&gt; key identifies the Lambda function, its container image, runtime environment, and required DynamoDB permission. The &lt;code&gt;order_exports&lt;/code&gt; bucket is explicitly associated with that function through &lt;code&gt;resource_key_ref&lt;/code&gt;, while &lt;code&gt;shared_assets&lt;/code&gt; remains independent. The configuration describes the application’s infrastructure and relationships without exposing the IAM role ARN, ECR repository URL, or generated S3 bucket details.&lt;/p&gt;

&lt;p&gt;For application teams, this meant they could provision large infrastructure stacks without having to worry about all the usual complexity involved. The security, encryption, naming conventions, and cross-resource wiring were already figured out for them. Once teams started using it en masse, I received a significant amount of feedback from developers and leadership across the organization about how much they loved it. Many managers told me, “Our team no longer has to think about infrastructure,” which was exactly the goal I had set out to achieve.&lt;/p&gt;

&lt;p&gt;Today, the Interface Module is a widely discussed product within our technology department. Almost everyone uses it because it makes infrastructure fast and easy to provision, which has made it especially useful for prototyping. Teams have even used it to deliver prototypes during hackathons! It became popular enough that, at one point, our CFO was encouraging people in town halls to use it: “If you’re not using it, you should be!”&lt;/p&gt;

&lt;p&gt;For platform teams, this meant that a significant burden was taken off our shoulders. Development teams were building their own infrastructure, so our support could focus on adding new features, fixing bugs, and taking the module through the usual software development life cycle.&lt;/p&gt;

&lt;p&gt;With the platform team no longer spending as much time answering the same infrastructure questions, we now had additional capacity to focus on exciting new ventures like creating and using agents to make the Interface Module easier to understand and use. We now document the API specifications and product functionality in Confluence, then connect an OpenSearch Knowledge Base to index the relevant pages. Our agent uses that knowledge base as its source of truth, helping developers build new patterns and troubleshoot problems with the Interface Module as another self-service offering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Many readers coming from Reddit may have expected me to talk about Backstage, Port, or another internal developer portal. Those are all useful tools, but they carry real platform overhead. The point here is simpler: &lt;code&gt;terraform&lt;/code&gt; and &lt;code&gt;opentofu&lt;/code&gt; already give us enough building blocks to create self-service infrastructure without forcing teams into a heavier UI or TypeScript-driven platform layer. If this pattern helps you design better developer experiences for your teams, that is the outcome I hoped to illustrate!&lt;/p&gt;

&lt;p&gt;In the comments section, I’d love to hear about your experiences creating self-service infrastructure, and any feedback about the Interface Module pattern! I’m also open to answering any questions folks out there might have about this topic, or anything related to building and delivering software in the cloud!&lt;/p&gt;

&lt;p&gt;For readers who want to go deeper, I put together two concrete examples that show the pattern in practice:&lt;/p&gt;

&lt;h2&gt;
  
  
  See It in Practice
&lt;/h2&gt;

&lt;p&gt;For readers who want to jump right in or take a deeper look, I put together two concrete examples that show the pattern in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Interface Module we started building together, shown here as a complete working example with some added &lt;code&gt;terratest&lt;/code&gt; validations triggered on pull requests: &lt;a href="https://github.com/defdevio/interface-module-example#interface-module" rel="noopener noreferrer"&gt;interface-module-example&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A stack that calls the Interface Module so you can see firsthand the developer experience it is designed to emulate: &lt;a href="https://github.com/defdevio/interface-module-stack-example/blob/main/main.tf" rel="noopener noreferrer"&gt;interface-module-stack-example&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>devex</category>
    </item>
    <item>
      <title>New GitHub Copilot AI Licenses Dropped</title>
      <dc:creator>Anthony Owens</dc:creator>
      <pubDate>Mon, 01 Jun 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/tonedefdev/new-github-copilot-ai-licenses-dropped-1afa</link>
      <guid>https://dev.to/tonedefdev/new-github-copilot-ai-licenses-dropped-1afa</guid>
      <description>&lt;p&gt;It's 5:44 PM PT exactly as I write this. I was watching the clock today—like a hawk—to see how the new GitHub Copilot AI credits were going to look, and boy, was I surprised. Here are my first impressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits Go by Fast
&lt;/h2&gt;

&lt;p&gt;The first thing I did at 5:05 PM PT was ask Claude Sonnet 4.6 a troubleshooting question I was running into with a &lt;code&gt;makefile&lt;/code&gt;. Nothing major, the usual:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm just being lazy right now, please fix it virtual tech bro!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, it turns out my &lt;code&gt;virtual tech bro&lt;/code&gt; is trying to join the &lt;code&gt;tres commas&lt;/code&gt; club, and in true &lt;em&gt;Russ Hanneman&lt;/em&gt; fashion, every chain-of-thought, every tool call, and every file read/write was eating into those sweet, sweet credits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8obop1spn8dcrvgv0wvx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8obop1spn8dcrvgv0wvx.webp" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When Claude finally returned an answer to a very basic problem, I had already burned through &lt;code&gt;61&lt;/code&gt; credits of my &lt;code&gt;7,000&lt;/code&gt; monthly budget. That's a ton of usage for not a lot of gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  New VS Code Changes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adjustable Thinking Levels
&lt;/h3&gt;

&lt;p&gt;The one nice change is the ability to adjust the level of thinking across models. This used to be available in the early days, but they removed it in favor of hardcoding specific thinking modes. Claude Sonnet 4.6 (High), my favorite workhorse in the stable, is no longer locked to a single mode:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikq0272yvjjb2m1rkp0k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikq0272yvjjb2m1rkp0k.webp" width="666" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can switch to a thinking mode that works best for your situation. I'm already dropping mine down to &lt;code&gt;Medium&lt;/code&gt; in hopes of reducing AI credit consumption:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3nd186sdvw0ynitqw53.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3nd186sdvw0ynitqw53.webp" width="734" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Changes to the Model Lineup
&lt;/h3&gt;

&lt;p&gt;The freebie models are gone. The days of having &lt;code&gt;GPT-4o mini&lt;/code&gt; available for zero tokens are over. Now, every single available model consumes credits:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo3jb2k4g52zgkc8wjo6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo3jb2k4g52zgkc8wjo6.webp" width="640" height="1052"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Copilot Max Subscription
&lt;/h3&gt;

&lt;p&gt;You can now upgrade to the Max subscription, which offers &lt;code&gt;20,000&lt;/code&gt; AI credits a month. Considering how quickly credits vanish for simple tasks, it's worth considering. Like the sane, rational human being that I am, I immediately upgraded the second I saw the burn rate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I cannot wait to try an MCP server...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Easy Days Might Be Over
&lt;/h2&gt;

&lt;p&gt;This might be the end of the high-flying days I've been experiencing the last few months with GitHub Copilot Plus. Fortunately, I crammed as many features as I could into my projects before the credit system started. I'll miss getting features done without burning through requests, but hey, maybe slowing things down a bit is needed?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Forget that noise!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Local Models to the Rescue
&lt;/h2&gt;

&lt;p&gt;I have really been digging &lt;code&gt;qwen3.6-27B&lt;/code&gt; with &lt;code&gt;thinking&lt;/code&gt; enabled on &lt;code&gt;mlx_vlm&lt;/code&gt;. It has been the powerhouse for my &lt;code&gt;MLOps&lt;/code&gt; project &lt;code&gt;ferme&lt;/code&gt;, which is currently generating Alpaca pairs to fine-tune a &lt;code&gt;gemma4&lt;/code&gt; and &lt;code&gt;qwen2.5-coder&lt;/code&gt; model using &lt;code&gt;QLorA&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;My M1 Max 64GB is a tight fit, and its TPS is not the greatest, but once my synthetic seeding is done and I have all these Alpaca pairs, it might be time to repurpose &lt;code&gt;qwen3.6&lt;/code&gt; for coding tasks and &lt;code&gt;gemma4&lt;/code&gt; for simple generation and documentation tasks. Sounds like it's time to get serious about integrating &lt;code&gt;Qwen Code&lt;/code&gt; directly into my VS Code workflow!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>I got tired of paying JFrog for a secure OpenTofu / Terraform registry so I built my own</title>
      <dc:creator>Anthony Owens</dc:creator>
      <pubDate>Tue, 26 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/tonedefdev/i-got-tired-of-paying-jfrog-for-a-secure-opentofu-terraform-registry-so-i-built-my-own-5egp</link>
      <guid>https://dev.to/tonedefdev/i-got-tired-of-paying-jfrog-for-a-secure-opentofu-terraform-registry-so-i-built-my-own-5egp</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/tonedefdev/opendepot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a tale as old as time — you want to implement a secure, centralized storage system to easily distribute these awesome IaC modules that your team has developed, but you quickly find that enterprise-grade comes with an enterprise price. You could stick with the good ol' GitHub refs, but you soon realize this doesn't scale well. Delivering critical security updates to developers becomes a tedious process. You then think to yourself "if only I could use OpenTofu version constraints!" Those constraints, like the pessimistic version constraint &lt;code&gt;~&amp;gt; v1.0.0&lt;/code&gt; for modules, make delivering security patches at scale significantly less challenging, however, you only get access to them through the registry protocol.&lt;/p&gt;

&lt;p&gt;So you spend late-nights scouring GitHub and Reddit looking for open-source registry projects hoping that you don't have to "pay the piper." Before you know it, you've spent months implementing several different open-source systems only to find each one either had a painful deployment process, no turn-key migration path, missing key features, or inconsistent authentication. You feel defeated — you have deadlines, after all, so you decide to "pony up" and "pay the man" just for peace of mind so you can mark your feature done.&lt;/p&gt;

&lt;p&gt;I, for one, hate surrendering to the corporate SaaS overlords in this manner! From that painful journey I put my poor team through, and the lessons I learned along the way, I realized this was an opportunity to give back to the open-source community. That's when I first came up with the idea for OpenDepot!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;OpenDepot is an enterprise-grade OpenTofu / Terraform module and provider registry built entirely to be Kubernetes native. OpenDepot uses first-class Kubernetes primitives like Custom Resource Definitions and operators to streamline and modernize the module and provider pipeline. Instead of "pushing and praying" like I had to do with other registries, especially enterprise-grade solutions like Artifactory, OpenDepot is entirely declarative and offers administrators complete control over their supply chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitOps Way
&lt;/h2&gt;

&lt;p&gt;The preferred method to deliver a new module version is by using GitOps with ArgoCD / Flux. This allows you to keep your registry manifest in the same repo as the module itself. When it's time to update or add new features to your module, the same pull request process you already use for module code is now tied-in with its release process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-aws-eks/
└── opendepot/
    └── terraform-aws-eks.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OpenDepot Module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot.defdev.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Module&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;moduleConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
    &lt;span class="na"&gt;repoOwner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-org&lt;/span&gt;
    &lt;span class="na"&gt;repoUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/my-org/terraform-aws-eks&lt;/span&gt;
    &lt;span class="na"&gt;fileFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip&lt;/span&gt;
    &lt;span class="na"&gt;immutable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-org-opendepot-modules&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
    &lt;span class="na"&gt;githubClientConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;useAuthenticatedClient&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.10.1"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.11.0"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.12.0"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.13.0"&lt;/span&gt;   &lt;span class="c1"&gt;# added in PR #42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ArgoCD Application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/my-org/terraform-aws-eks&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-system&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release workflow then looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A developer opens a PR against their OpenTofu module repository with the code changes&lt;/li&gt;
&lt;li&gt;The same PR includes an update to the OpenDepot Module manifest, adding the new version to &lt;code&gt;spec.versions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The team reviews both the module code and the registry manifest in a single PR&lt;/li&gt;
&lt;li&gt;On approval and merge, Argo CD detects the change and syncs the &lt;code&gt;Module&lt;/code&gt; resource to the cluster&lt;/li&gt;
&lt;li&gt;OpenDepot takes over — the Module controller creates a &lt;code&gt;Version&lt;/code&gt; resource, and the Version controller fetches the archive from GitHub and uploads it to storage. Once completed, a SHA256 checksum of the archive is stored in the &lt;code&gt;status&lt;/code&gt; field before marking the Version as synced.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; You can also use a centralized repo that hosts all your OpenDepot manifests, with a single ArgoCD application that gives you a visual overview of your entire registry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Storage Configuration
&lt;/h2&gt;

&lt;p&gt;OpenDepot supports all three major cloud provider storage backends as well as local filesystem storage:&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS S3
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-modules&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Azure Blob
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;azureBlob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;accountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepotmodules&lt;/span&gt;
    &lt;span class="na"&gt;accountUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://opendepotmodules.blob.core.windows.net&lt;/span&gt;
    &lt;span class="na"&gt;subscriptionID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;00000000-0000-0000-0000-000000000000&lt;/span&gt;
    &lt;span class="na"&gt;resourceGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-rg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Google Cloud
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gcs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-modules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Filesystem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/opendepot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Extensible by design:&lt;/strong&gt; I designed OpenDepot to leverage a Go interface for storage. Adding and testing new providers is straightforward — provide a concrete implementation for the interface, update the API, regenerate new CRDs, and you're ready to start testing. See &lt;a href="https://github.com/tonedefdev/opendepot/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; for more details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Pre-signed URLs&lt;/strong&gt; allow you to offload large egress costs (AWS providers can be ~700MB) by redirecting clients to pull directly from cloud storage instead of proxying through your infrastructure. Configure per-module, per-provider, or globally through the Depot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-providers&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
  &lt;span class="na"&gt;presign&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15m"&lt;/span&gt;
    &lt;span class="na"&gt;fallbackToProxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fallback behavior:&lt;/strong&gt; When &lt;code&gt;fallbackToProxy&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, if a pre-signed URL cannot be generated the server proxies the download itself. Set it to &lt;code&gt;false&lt;/code&gt; to enforce that all downloads always use pre-signed URLs and never pass through your infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Filesystem storage&lt;/strong&gt; is backed by a Kubernetes Persistent Volume with any &lt;code&gt;StorageClass&lt;/code&gt; that supports &lt;code&gt;ReadWriteMany&lt;/code&gt;. The Version controller needs to write artifacts to the same volume the Server serves them from — hence the &lt;code&gt;ReadWriteMany&lt;/code&gt; requirement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Init container privileges:&lt;/strong&gt; On startup, an init container runs as root to &lt;code&gt;chown&lt;/code&gt;/&lt;code&gt;chgrp&lt;/code&gt; the directory mount so that user/group &lt;code&gt;65532&lt;/code&gt; (the user the containers run as) can read/write to it. This is the only point where elevated privileges are required — otherwise, OpenDepot runs as non-root across the board.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Depot (Pull-Based)
&lt;/h2&gt;

&lt;p&gt;If you don't follow a GitOps process, no worries! The Depot resource allows you to pull down modules and providers using version constraints, creating a private mirror for public providers with a fully defined release process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot.defdev.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Depot&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-team-depot&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;githubClientConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;useAuthenticatedClient&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;moduleConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fileFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip&lt;/span&gt;
      &lt;span class="na"&gt;immutable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-registry&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
  &lt;span class="na"&gt;moduleConfigs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
      &lt;span class="na"&gt;repoOwner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-modules&lt;/span&gt;
      &lt;span class="na"&gt;versionConstraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;21.10.1,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;21.13.0"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-azurerm-aks&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azurerm&lt;/span&gt;
      &lt;span class="na"&gt;repoOwner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure&lt;/span&gt;
      &lt;span class="na"&gt;versionConstraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.0.0"&lt;/span&gt;
  &lt;span class="na"&gt;providerConfigs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
      &lt;span class="na"&gt;operatingSystems&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;darwin&lt;/span&gt;
      &lt;span class="na"&gt;architectures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;arm64&lt;/span&gt;
      &lt;span class="na"&gt;versionConstraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5.80.0,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6.0.0"&lt;/span&gt;
      &lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-registry&lt;/span&gt;
          &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
  &lt;span class="na"&gt;pollingIntervalMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Depot will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query the &lt;code&gt;terraform-aws-modules/terraform-aws-eks&lt;/code&gt; and &lt;code&gt;azure/terraform-azurerm-aks&lt;/code&gt; GitHub repositories for releases&lt;/li&gt;
&lt;li&gt;Filter releases matching the version constraints and create &lt;code&gt;Module&lt;/code&gt; resources&lt;/li&gt;
&lt;li&gt;Query the HashiCorp Releases API for the &lt;code&gt;aws&lt;/code&gt; provider and create a &lt;code&gt;Provider&lt;/code&gt; resource for matching versions&lt;/li&gt;
&lt;li&gt;The Module and Provider controllers create &lt;code&gt;Version&lt;/code&gt; resources for each discovered version and OS/architecture&lt;/li&gt;
&lt;li&gt;The Version controller fetches archives from GitHub (modules) or HashiCorp (providers) and uploads them to the S3 bucket&lt;/li&gt;
&lt;li&gt;Re-check for new releases every 60 minutes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since your registry configuration is codified via the Depot, it now follows the same review process as other services in your stack!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Migrating from an existing registry:&lt;/strong&gt; The Depot is a very handy migration tool. Point it at your GitHub repos with a version constraint that covers your existing versions, let it ingest everything, then delete the Depot. Removing the Depot resource does &lt;strong&gt;not&lt;/strong&gt; delete any Modules or Providers — it's simply a centralized interface to ingest multiple artifacts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The CI/CD Workflow (Push-based)
&lt;/h2&gt;

&lt;p&gt;You also have the option for an entirely push-based CI/CD workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenDepot Manifest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot.defdev.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Module&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;moduleConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-eks&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
    &lt;span class="na"&gt;repoOwner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-aws-modules&lt;/span&gt;
    &lt;span class="na"&gt;repoUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/terraform-aws-modules/terraform-aws-eks&lt;/span&gt;
    &lt;span class="na"&gt;fileFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip&lt;/span&gt;
    &lt;span class="na"&gt;immutable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;storageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-modules&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
    &lt;span class="na"&gt;githubClientConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;useAuthenticatedClient&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.10.1"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.11.0"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.12.0"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21.13.0"&lt;/span&gt;  &lt;span class="c1"&gt;# added in PR #42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitHub Actions Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Module Version&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:role/opendepot-github-actions-role&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup kubeconfig&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws eks update-kubeconfig --name my-cluster --region us-west-2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish module version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;kubectl apply -f "opendepot/terraform-aws-eks.yaml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Checksum Validation Every Reconcile
&lt;/h3&gt;

&lt;p&gt;Kubernetes operators constantly reconcile resources and act on changes to any resources they manage. When a module version is being added, all previous versions are also reconciled. To ensure OpenDepot is not re-downloading providers or modules every reconciliation loop, the Version resource stores a &lt;code&gt;status.checksum&lt;/code&gt; of each archive.&lt;/p&gt;

&lt;p&gt;If the checksum metadata in storage doesn't match this field, the controller first attempts to pull it from source and revalidate the checksum. If the checksum from source is still not a match, the controller stops reconciling and emits errors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tamper protection:&lt;/strong&gt; The Server will not serve any modules whose checksums do not match. The Version controller continuously reconciles storage so that any tampered archive is re-fetched and restored to its known-good state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  GPG Keys
&lt;/h3&gt;

&lt;p&gt;OpenDepot supports GPG signing for providers. When serving provider binaries, the registry protocol requires that each binary is accompanied by a SHA256 checksum file and a GPG signature so that OpenTofu and Terraform can verify the integrity of what they download. Configure OpenDepot with your GPG key via a Kubernetes Secret referenced by &lt;code&gt;server.gpg.secretName&lt;/code&gt; in the Helm chart. Once set, the Server automatically signs provider checksum files on the fly with your private key. Clients that have your public key in their trust store can verify every provider binary they pull is untampered and came from your registry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trivy Vulnerability Scans
&lt;/h3&gt;

&lt;p&gt;OpenDepot has the option to perform security scans using a separate Version controller image that comes bundled with Trivy. Trivy will perform a configuration scan of modules and store findings in the &lt;code&gt;module.status&lt;/code&gt; field. For providers, Trivy will scan the binary for each operating system and architecture, and OpenDepot will attempt to find and scan the source code, deduplicate findings, then store each in the &lt;code&gt;provider.status&lt;/code&gt; field.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blocking policy:&lt;/strong&gt; You can configure OpenDepot to block &lt;code&gt;CRITICAL&lt;/code&gt; and &lt;code&gt;HIGH&lt;/code&gt; vulnerabilities to ensure that only modules and providers with a good security posture can be reconciled and stored in your registry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dex OIDC Integration
&lt;/h3&gt;

&lt;p&gt;OpenDepot's Helm chart bundles &lt;a href="https://dexidp.io/" rel="noopener noreferrer"&gt;Dex&lt;/a&gt; as a subchart to handle OIDC authentication with an upstream IdP like Entra ID, GitHub, Okta, and many more. This is the recommended authentication method since it doesn't require cluster access or expose endpoints used to modify resources.&lt;/p&gt;

&lt;p&gt;With OIDC enabled you can leverage fine-grained access control through &lt;code&gt;GroupBinding&lt;/code&gt; custom resources. Use the &lt;a href="https://expr-lang.org/" rel="noopener noreferrer"&gt;Expr&lt;/a&gt; language to bind the &lt;code&gt;groups&lt;/code&gt; claim in a user's JWT to specific modules or providers. The &lt;code&gt;moduleResources&lt;/code&gt; field also supports glob patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot.defdev.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GroupBinding&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;01-aws-platform-team"&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opendepot-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;"aws-platform-team"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;groups'&lt;/span&gt;
  &lt;span class="na"&gt;moduleResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terraform-aws-*"&lt;/span&gt;
  &lt;span class="na"&gt;providerResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Native &lt;code&gt;tofu login&lt;/code&gt; support:&lt;/strong&gt; OIDC with Dex is the &lt;strong&gt;only&lt;/strong&gt; method that supports the native &lt;code&gt;tofu login opendepot.defdev.io&lt;/code&gt; command.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Other Authentication Methods
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes service account token&lt;/strong&gt; — Use Kubernetes RBAC permissions to control access per module or provider. Bypasses GroupBinding in favor of native Kubernetes RBAC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64-encoded kubeconfig&lt;/strong&gt; — Convenient for local kind clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anonymous auth&lt;/strong&gt; — Enable via a single Helm chart flag to host a public registry. The Server's own Service Account is used for fetching, and clients don't need an access token.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubeconfig - local use only:&lt;/strong&gt; A base64-encoded Kubernetes kubeconfig should &lt;strong&gt;never&lt;/strong&gt; be used in production. It is convenient for local &lt;code&gt;kind&lt;/code&gt; cluster testing only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fetching Artifacts
&lt;/h2&gt;

&lt;p&gt;The Server implements both the Module and Provider Registry protocols so that OpenTofu and Terraform can use OpenDepot as a drop-in registry. Crucially, the Server is &lt;strong&gt;completely read-only&lt;/strong&gt; — it provides no endpoints that allow modifications. All changes to resources require strict Kubernetes access.&lt;/p&gt;

&lt;p&gt;Reference your modules and providers in code, then run &lt;code&gt;tofu init&lt;/code&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"eks"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"opendepot.defdev.io/opendepot-system/terraform-aws-key-pair/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 21.0.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Provider
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"opendepot.defdev.io/opendepot-system/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.80"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"opendepot.defdev.io/opendepot-system/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 4.0.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure your &lt;code&gt;.tofurc&lt;/code&gt; to point at OpenDepot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="s2"&gt;"opendepot.defdev.io"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"modules.v1"&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://opendepot.defdev.io/opendepot/modules/v1/"&lt;/span&gt;
    &lt;span class="s2"&gt;"providers.v1"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://opendepot.defdev.io/opendepot/providers/v1/"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Dex configured, the full &lt;code&gt;tofu login&lt;/code&gt; + &lt;code&gt;tofu init&lt;/code&gt; flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tofu login opendepot.defdev.io
$ tofu init

Initializing the backend...
Initializing modules...
Downloading opendepot.defdev.io/opendepot-system/terraform-aws-key-pair/aws 2.0.3 for key_pair...
- key_pair in .terraform/modules/key_pair

Initializing provider plugins...

OpenTofu has been successfully initialized!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all there is to it on the consuming side! It's simple and easy to get started with OpenDepot! I ask that you try it today and share your experiences. If you have any questions, see any issues, or just want to talk about Cloud Native tooling in general — feel free to reach out to me anytime!&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tonedefdev.github.io/opendepot/" rel="noopener noreferrer"&gt;Full Documentation&lt;/a&gt; - Everything you need to get set up, configured, and running your own registry.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tonedefdev.github.io/opendepot/getting-started/quickstart/" rel="noopener noreferrer"&gt;Local Quickstart&lt;/a&gt; - Run a fully functional registry on your laptop with kind in minutes, no cloud account needed.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tonedefdev.github.io/opendepot/getting-started/installation/" rel="noopener noreferrer"&gt;Installation Guide&lt;/a&gt; - Deploy OpenDepot to your cluster with Helm.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>terraform</category>
      <category>opensource</category>
      <category>go</category>
    </item>
  </channel>
</rss>
