<?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: Raghvendra Pandey</title>
    <description>The latest articles on DEV Community by Raghvendra Pandey (@pandey-raghvendra).</description>
    <link>https://dev.to/pandey-raghvendra</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%2F3031233%2F3db6d024-1e58-41bb-983a-cb43818587d0.png</url>
      <title>DEV Community: Raghvendra Pandey</title>
      <link>https://dev.to/pandey-raghvendra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pandey-raghvendra"/>
    <language>en</language>
    <item>
      <title>Terraform Modules: Writing, Testing, and Reusing Infrastructure Code</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 11:11:19 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/terraform-modules-writing-testing-and-reusing-infrastructure-code-2d8h</link>
      <guid>https://dev.to/pandey-raghvendra/terraform-modules-writing-testing-and-reusing-infrastructure-code-2d8h</guid>
      <description>&lt;p&gt;Every Terraform codebase eventually hits the same inflection point: the root module grows unwieldy, the same patterns repeat across environments, and someone asks "can we just reuse the VPC setup from the payment team?" Modules are Terraform's answer to reuse and encapsulation. Done well, they let you compose infrastructure from tested, versioned building blocks. Done poorly, they create abstraction layers that hide problems, make debugging harder, and need to be broken apart six months later.&lt;/p&gt;

&lt;p&gt;This guide covers what modules actually are, how to structure them, the patterns that work at scale, and how to test them — including when you probably shouldn't extract a module at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a module is
&lt;/h2&gt;

&lt;p&gt;In Terraform, every directory of &lt;code&gt;.tf&lt;/code&gt; files is a module. The directory you run &lt;code&gt;terraform apply&lt;/code&gt; from is the &lt;em&gt;root module&lt;/em&gt;. Any other directory of &lt;code&gt;.tf&lt;/code&gt; files that you reference with a &lt;code&gt;module&lt;/code&gt; block is a &lt;em&gt;child module&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's it. There's no special module declaration or module keyword inside the module directory. A module is just a collection of Terraform resources that accepts inputs via &lt;code&gt;variable&lt;/code&gt; blocks and exposes outputs via &lt;code&gt;output&lt;/code&gt; blocks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Calling a module from a root module&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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;"./modules/vpc"&lt;/span&gt;      &lt;span class="c1"&gt;# local path&lt;/span&gt;
&lt;span class="c1"&gt;# or:&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;"terraform-aws-modules/vpc/aws"&lt;/span&gt;  &lt;span class="c1"&gt;# Terraform Registry&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.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;"production"&lt;/span&gt;
&lt;span class="nx"&gt;cidr&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="nx"&gt;azs&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;private_subnets&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.3.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;public_subnets&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.101.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.102.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.103.0/24"&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;
  
  
  Module structure
&lt;/h2&gt;

&lt;p&gt;A well-structured module has four files at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/vpc/
├── main.tf       # resource definitions
├── variables.tf  # input variable declarations
├── outputs.tf    # output value declarations
└── versions.tf   # required_providers and terraform version constraint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional but common additions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md     # documents inputs, outputs, usage examples
└── examples/
└── complete/ # a working example that calls the module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  variables.tf
&lt;/h3&gt;

&lt;p&gt;Variables are the public interface of a module. Every variable should have a &lt;code&gt;description&lt;/code&gt; and a &lt;code&gt;type&lt;/code&gt;. Use &lt;code&gt;default&lt;/code&gt; for optional variables; omit it for required ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"name"&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;"Name prefix for all resources created by this module."&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;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"cidr_block"&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;"CIDR block for the VPC."&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;string&lt;/span&gt;
&lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&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;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cidrhost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cidr_block&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="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Must be a valid CIDR block."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"enable_nat_gateway"&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;"Whether to create a NAT Gateway in each availability zone."&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;bool&lt;/span&gt;
&lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"tags"&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;"Tags to apply to all resources."&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;string&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;validation&lt;/code&gt; block runs before any resource creation and gives users a useful error immediately instead of an obscure provider error later.&lt;/p&gt;

&lt;h3&gt;
  
  
  outputs.tf
&lt;/h3&gt;

&lt;p&gt;Outputs expose values that callers need. Be generous with outputs — it's easier to add an output than to break consumers who discover they needed one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&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;"The ID of the VPC."&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;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_ids"&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;"List of IDs of private subnets."&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;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"public_subnet_ids"&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;"List of IDs of public subnets."&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;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"nat_gateway_ids"&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;"List of NAT Gateway IDs."&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;aws_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  versions.tf
&lt;/h3&gt;

&lt;p&gt;Constrain the Terraform version and provider versions to prevent incompatible configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5.0"&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;"hashicorp/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.0, = 5.0, 5.0` for modules — it prevents a major version bump from breaking users who haven't explicitly upgraded.

## Local modules vs registry modules

Local modules (relative paths like `./modules/vpc`) are for internal organizational abstractions. They live in your repo, version-controlled alongside the infrastructure that uses them. Advantages: fast iteration, no publishing step, easy to understand the full codebase as a unit. Disadvantages: can't be shared across repos without copy-pasting.

Registry modules are published to the Terraform Registry (public) or a private registry (Terraform Cloud, Artifactory, etc.). They're pinned by version and fetched by `terraform init`. The key attributes of a good registry module:

- Semantic versioning with a clear changelog
- Well-documented inputs and outputs
- Working examples in `examples/`
- Automated tests
- Maintained and responding to issues

The community modules at `terraform-aws-modules` on GitHub are the gold standard. The `terraform-aws-modules/vpc/aws` module, for example, manages hundreds of resource configurations across a very large variable surface. It's an excellent reference for how to structure a complex module.

## Version pinning

Always pin module versions in production. Unpinned modules pull the latest version on `terraform init -upgrade`, which can introduce breaking changes silently.

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Good — pinned to a specific minor version
&lt;/h1&gt;

&lt;p&gt;module "vpc" {&lt;br&gt;
source  = "terraform-aws-modules/vpc/aws"&lt;br&gt;
version = "~&amp;gt; 5.1"  # allows 5.1.x, not 5.2.x or 6.x&lt;/p&gt;
&lt;h1&gt;
  
  
  ...
&lt;/h1&gt;

&lt;p&gt;}&lt;/p&gt;
&lt;h1&gt;
  
  
  Bad — no version constraint
&lt;/h1&gt;

&lt;p&gt;module "vpc" {&lt;br&gt;
source = "terraform-aws-modules/vpc/aws"&lt;/p&gt;
&lt;h1&gt;
  
  
  version omitted — gets latest on terraform init -upgrade
&lt;/h1&gt;

&lt;p&gt;}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="nx"&gt;Use&lt;/span&gt; &lt;span class="err"&gt;`~&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allows&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt; &lt;span class="nx"&gt;within&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;minor&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Only&lt;/span&gt; &lt;span class="nx"&gt;upgrade&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;minor&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;major&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="nx"&gt;intentionally&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;span class="c1"&gt;## Module composition patterns&lt;/span&gt;

&lt;span class="c1"&gt;### Flat composition&lt;/span&gt;

&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;simplest&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="nx"&gt;common&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt; &lt;span class="nx"&gt;several&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;responsible&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="nx"&gt;infrastructure&lt;/span&gt; &lt;span class="nx"&gt;concern&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;module "vpc" {&lt;br&gt;
source = "./modules/vpc"&lt;br&gt;
name   = var.environment&lt;br&gt;
cidr   = "10.0.0.0/16"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;module "eks" {&lt;br&gt;
source          = "./modules/eks"&lt;br&gt;
cluster_name    = "${var.environment}-cluster"&lt;br&gt;
vpc_id          = module.vpc.vpc_id&lt;br&gt;
subnet_ids      = module.vpc.private_subnet_ids&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;module "rds" {&lt;br&gt;
source     = "./modules/rds"&lt;br&gt;
name       = "${var.environment}-db"&lt;br&gt;
vpc_id     = module.vpc.vpc_id&lt;br&gt;
subnet_ids = module.vpc.private_subnet_ids&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="nx"&gt;Outputs&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;become&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;another&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;This&lt;/span&gt; &lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;Terraform&lt;/span&gt; &lt;span class="nx"&gt;builds&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="nx"&gt;graph&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;applies&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;span class="c1"&gt;### Wrapper modules&lt;/span&gt;

&lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt; &lt;span class="nx"&gt;organizational&lt;/span&gt; &lt;span class="nx"&gt;defaults&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;community&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  modules/vpc-standard/main.tf
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Internal module that wraps the community VPC module
&lt;/h1&gt;

&lt;h1&gt;
  
  
  with company-standard settings
&lt;/h1&gt;

&lt;p&gt;module "vpc" {&lt;br&gt;
source  = "terraform-aws-modules/vpc/aws"&lt;br&gt;
version = "~&amp;gt; 5.1"&lt;/p&gt;

&lt;p&gt;name = var.name&lt;br&gt;
cidr = var.cidr&lt;/p&gt;
&lt;h1&gt;
  
  
  Company standard: always enable DNS, always tag
&lt;/h1&gt;

&lt;p&gt;enable_dns_hostnames = true&lt;br&gt;
enable_dns_support   = true&lt;br&gt;
enable_nat_gateway   = true&lt;br&gt;
single_nat_gateway   = var.environment != "production"&lt;/p&gt;

&lt;p&gt;tags = merge(&lt;br&gt;
var.tags,&lt;br&gt;
{&lt;br&gt;
ManagedBy   = "terraform"&lt;br&gt;
Environment = var.environment&lt;br&gt;
Team        = var.team&lt;br&gt;
}&lt;br&gt;
)&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="nx"&gt;Consumers&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="nx"&gt;instead&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;community&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;directly&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;When&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt; &lt;span class="nx"&gt;standard&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="err"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="nx"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;span class="c1"&gt;## When to extract a module — and when not to&lt;/span&gt;

&lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="nx"&gt;common&lt;/span&gt; &lt;span class="nx"&gt;mistake&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;extracting&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt; &lt;span class="nx"&gt;too&lt;/span&gt; &lt;span class="nx"&gt;early&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;If&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extracting&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt; &lt;span class="nx"&gt;abstraction&lt;/span&gt; &lt;span class="nx"&gt;overhead&lt;/span&gt; &lt;span class="nx"&gt;without&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;reuse&lt;/span&gt; &lt;span class="nx"&gt;benefit&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;thumb&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;extract&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;you&lt;/span&gt; &lt;span class="nx"&gt;actually&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;reuse&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt; &lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;anticipation&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;possible&lt;/span&gt; &lt;span class="nx"&gt;reuse&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;span class="nx"&gt;Good&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;extraction&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="nx"&gt;VPC&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="nx"&gt;deployed&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;multiple&lt;/span&gt; &lt;span class="nx"&gt;environments&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="nx"&gt;standard&lt;/span&gt; &lt;span class="nx"&gt;RDS&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="nx"&gt;multiple&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;
&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="s2"&gt;"microservice"&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ECS&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ALB&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CloudWatch&lt;/span&gt; &lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;every&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;
&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Anything&lt;/span&gt; &lt;span class="nx"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;complex&lt;/span&gt; &lt;span class="nx"&gt;interdependencies&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;benefit&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;hiding&lt;/span&gt; &lt;span class="nx"&gt;implementation&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;

&lt;span class="nx"&gt;Poor&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="nx"&gt;one-off&lt;/span&gt; &lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="nx"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extract&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;pure&lt;/span&gt; &lt;span class="nx"&gt;overhead&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Resources&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;be&lt;/span&gt; &lt;span class="nx"&gt;modified&lt;/span&gt; &lt;span class="nx"&gt;differently&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;abstraction&lt;/span&gt; &lt;span class="nx"&gt;fights&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;customization&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Very&lt;/span&gt; &lt;span class="nx"&gt;small&lt;/span&gt; &lt;span class="nx"&gt;sets&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;–&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="nx"&gt;don&lt;/span&gt;&lt;span class="s1"&gt;'t need a module)

## Testing modules with Terratest

Terratest is a Go library for writing automated tests for infrastructure code. A Terratest test deploys real infrastructure, runs assertions against it, and tears it down:

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;// modules/vpc/tests/vpc_test.go&lt;br&gt;
package test&lt;/p&gt;

&lt;p&gt;import (&lt;br&gt;
"testing"&lt;br&gt;
"github.com/gruntwork-io/terratest/modules/terraform"&lt;br&gt;
"github.com/gruntwork-io/terratest/modules/aws"&lt;br&gt;
"github.com/stretchr/testify/assert"&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;func TestVpcModule(t *testing.T) {&lt;br&gt;
t.Parallel()&lt;/p&gt;

&lt;p&gt;opts := &amp;amp;terraform.Options{&lt;br&gt;
TerraformDir: "../examples/complete",&lt;br&gt;
Vars: map[string]interface{}{&lt;br&gt;
"name":   "test-vpc",&lt;br&gt;
"region": "us-east-1",&lt;br&gt;
},&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;defer terraform.Destroy(t, opts)&lt;br&gt;
terraform.InitAndApply(t, opts)&lt;/p&gt;

&lt;p&gt;vpcId := terraform.Output(t, opts, "vpc_id")&lt;br&gt;
assert.NotEmpty(t, vpcId)&lt;/p&gt;

&lt;p&gt;// Verify the VPC actually exists in AWS&lt;br&gt;
vpc := aws.GetVpcById(t, vpcId, "us-east-1")&lt;br&gt;
assert.Equal(t, "10.0.0.0/16", aws.GetVpcCidrBlock(t, vpcId, "us-east-1"))&lt;br&gt;
assert.True(t, *vpc.EnableDnsHostnames)&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="nx"&gt;Terratest&lt;/span&gt; &lt;span class="nx"&gt;tests&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;they&lt;/span&gt; &lt;span class="nx"&gt;deploy&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="nx"&gt;infrastructure&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="nx"&gt;money&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Reserve&lt;/span&gt; &lt;span class="nx"&gt;them&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;will&lt;/span&gt; &lt;span class="nx"&gt;be&lt;/span&gt; &lt;span class="nx"&gt;shared&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;relied&lt;/span&gt; &lt;span class="nx"&gt;upon&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="nx"&gt;multiple&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="nx"&gt;internal&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;combination&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="err"&gt;`,&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;manual&lt;/span&gt; &lt;span class="nx"&gt;testing&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;sufficient&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;

&lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="nx"&gt;lightweight&lt;/span&gt; &lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="nx"&gt;testing&lt;/span&gt; &lt;span class="nx"&gt;without&lt;/span&gt; &lt;span class="nx"&gt;deploying&lt;/span&gt; &lt;span class="nx"&gt;infrastructure&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;built&lt;/span&gt; &lt;span class="nx"&gt;into&lt;/span&gt; &lt;span class="nx"&gt;Terraform&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="err"&gt;+)&lt;/span&gt; &lt;span class="nx"&gt;lets&lt;/span&gt; &lt;span class="nx"&gt;you&lt;/span&gt; &lt;span class="nx"&gt;write&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;exercise&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nx"&gt;logic&lt;/span&gt; &lt;span class="nx"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;mock&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  modules/vpc/tests/input_validation.tftest.hcl
&lt;/h1&gt;

&lt;p&gt;run "invalid_cidr_block" {&lt;br&gt;
command = plan&lt;/p&gt;

&lt;p&gt;variables {&lt;br&gt;
name       = "test"&lt;br&gt;
cidr_block = "not-a-cidr"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;expect_failures = [var.cidr_block]&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;
&lt;span class="c1"&gt;## Module documentation&lt;/span&gt;

&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nx"&gt;terraform-docs&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;reads&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="k"&gt;module&lt;/span&gt;&lt;span class="s1"&gt;'s `variables.tf` and `outputs.tf` and generates a markdown table of inputs and outputs. Running it in CI keeps documentation in sync with the code:

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Generate docs and update README.md
&lt;/h1&gt;

&lt;p&gt;terraform-docs markdown table --output-file README.md ./modules/vpc&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
The generated section looks like:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Inputs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;Name prefix for all resources&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cidr_block&lt;/td&gt;
&lt;td&gt;CIDR block for the VPC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"10.0.0.0/16"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Outputs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vpc_id&lt;/td&gt;
&lt;td&gt;The ID of the VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private_subnet_ids&lt;/td&gt;
&lt;td&gt;List of private subnet IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


When a module is consumed by other teams, this documentation is the contract. Good documentation reduces questions and prevents misuse.

To visualize the resources a Terraform module creates and how they relate — useful during module design or review — paste the module's HCL into [InfraSketch](/) to see the architecture diagram. Module calls in a root module appear as labeled groups, with each resource inside the module shown individually.

## Related articles

- [Terraform State Explained: What It Is, How It Works, and Why It Breaks](/blog/terraform-state-explained.html)
- [Terraform Visualization: 5 Ways to See What Your Code Actually Builds](/blog/terraform-visualization-best-practices.html)
- [Terraform vs CDK vs Pulumi: Choosing Your IaC Tool](/blog/iac-tool-comparison.html)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kubernetes Resource Management: Requests, Limits, and QoS Classes</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 11:10:46 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/kubernetes-resource-management-requests-limits-and-qos-classes-5a6h</link>
      <guid>https://dev.to/pandey-raghvendra/kubernetes-resource-management-requests-limits-and-qos-classes-5a6h</guid>
      <description>&lt;p&gt;Liquid syntax error: Unknown tag 'endraw'&lt;/p&gt;
</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>GitOps Explained: How It Works, ArgoCD vs Flux, and When to Use It</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 11:10:44 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/gitops-explained-how-it-works-argocd-vs-flux-and-when-to-use-it-12ke</link>
      <guid>https://dev.to/pandey-raghvendra/gitops-explained-how-it-works-argocd-vs-flux-and-when-to-use-it-12ke</guid>
      <description>&lt;p&gt;GitOps is a way of operating infrastructure and applications where Git is the single source of truth for what should be running, and an automated agent continuously reconciles the actual state of the system to match it. The term was coined by Weaveworks in 2017, but the underlying idea — declarative desired state, version control, automated reconciliation — predates it. What GitOps added was a specific set of practices and a set of tools that made the pattern practical for Kubernetes.&lt;/p&gt;

&lt;p&gt;This guide covers what GitOps actually means (not just the marketing definition), how the reconciliation loop works, how ArgoCD and Flux compare, and where GitOps genuinely helps versus where it adds unnecessary complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four GitOps principles
&lt;/h2&gt;

&lt;p&gt;The OpenGitOps project (a CNCF working group) defines GitOps around four principles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Declarative.&lt;/strong&gt; The desired state of the entire system is expressed declaratively. This means Kubernetes YAML manifests, Helm chart values, Kustomize overlays — not imperative scripts. "Apply these manifests" rather than "run these commands." The advantage: the desired state can be stored, diffed, and audited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Versioned and immutable.&lt;/strong&gt; The desired state is stored in a versioning system (Git) that enforces immutability of history. Every change has a timestamp, an author, and a hash. Rollback means reverting a commit. Audit means reading the commit log. You always know what changed, when, and who approved it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pulled automatically.&lt;/strong&gt; Approved changes are automatically applied to the system. The GitOps agent inside the cluster watches the Git repository and pulls changes — the cluster initiates the connection, not external systems. This is a security advantage: you don't need to give CI/CD systems credentials to access the cluster. The cluster's outbound network needs to reach Git; nothing external needs inbound access to the API server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Continuously reconciled.&lt;/strong&gt; Software agents continuously compare actual state with desired state and automatically correct any divergence. If someone manually changes a Kubernetes resource (kubectl edit), the GitOps agent detects the drift and reverts it. This is the enforcement mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the reconciliation loop works
&lt;/h2&gt;

&lt;p&gt;A GitOps controller runs inside the cluster. It watches one or more Git repositories (or OCI registries) and continuously runs a control loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the latest desired state from the Git repository&lt;/li&gt;
&lt;li&gt;Read the actual state of the cluster from the Kubernetes API&lt;/li&gt;
&lt;li&gt;Compute the diff&lt;/li&gt;
&lt;li&gt;Apply changes to make actual state match desired state&lt;/li&gt;
&lt;li&gt;Wait for the reconciliation interval (typically 1–5 minutes) and repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pull-based model contrasts with the push-based model used by traditional CI/CD pipelines. In push-based deployments, a pipeline runs after a merge and pushes changes to the cluster via &lt;code&gt;kubectl apply&lt;/code&gt; or Helm. In pull-based GitOps, the cluster continuously checks whether it matches Git and corrects itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Push-based CI/CD (traditional)&lt;/span&gt;
&lt;span class="c"&gt;# Pipeline runs:&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/
&lt;span class="c"&gt;# Cluster state = whatever kubectl apply did&lt;/span&gt;

&lt;span class="c"&gt;# GitOps (pull-based)&lt;/span&gt;
&lt;span class="c"&gt;# GitOps controller runs in cluster, continuously:&lt;/span&gt;
&lt;span class="c"&gt;# 1. Reads desired state from git&lt;/span&gt;
&lt;span class="c"&gt;# 2. Compares to actual cluster state&lt;/span&gt;
&lt;span class="c"&gt;# 3. Applies the diff&lt;/span&gt;
&lt;span class="c"&gt;# Cluster state = what's in Git (enforced, not just applied once)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Git repository structure
&lt;/h2&gt;

&lt;p&gt;GitOps works best when the application code repository and the deployment configuration repository are separate. This "app repo + config repo" pattern is the most common structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App repo&lt;/strong&gt;: source code, Dockerfile, application tests. CI pipeline builds images, tags them with the git commit SHA, pushes to a registry, then opens a PR against the config repo to update the image tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config repo&lt;/strong&gt;: Kubernetes manifests, Helm values, Kustomize overlays. This is what the GitOps controller watches. Changes here trigger deployments; changes to the app repo trigger a CI pipeline that eventually updates the config repo.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Config repo structure (example)
clusters/
production/
namespaces/
api/
deployment.yaml
service.yaml
hpa.yaml
database/
statefulset.yaml
service.yaml
staging/
namespaces/
api/
deployment.yaml  # different image tag, lower resource requests
database/
statefulset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some teams keep everything in one repo using directory structure or branch strategy to separate environments. Others use separate repos per environment or per cluster. There's no single right structure — the key is that everything that defines cluster state lives in version control.&lt;/p&gt;

&lt;h2&gt;
  
  
  ArgoCD
&lt;/h2&gt;

&lt;p&gt;ArgoCD is the most widely adopted GitOps tool. It runs as a set of controllers in the cluster and exposes a web UI and CLI. The core concept is an &lt;strong&gt;Application&lt;/strong&gt; resource that maps a Git repository path to a Kubernetes namespace:&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;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;api-production&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/mycompany/config-repo&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;clusters/production/namespaces/api&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;api&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;true&lt;/span&gt;      &lt;span class="c1"&gt;# delete resources removed from git&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;span class="c1"&gt;# revert manual changes&lt;/span&gt;
&lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ArgoCD's web UI shows the sync status of every Application — green (in sync), yellow (out of sync), red (degraded). Out-of-sync resources show exactly what differs between the cluster and Git. This visual diff is one of ArgoCD's most valued features for operators who want to understand deployment state at a glance.&lt;/p&gt;

&lt;p&gt;ArgoCD uses an &lt;strong&gt;app of apps&lt;/strong&gt; pattern for managing many Applications: a root Application points to a directory of Application manifests, which ArgoCD then deploys. This bootstraps an entire cluster from a single ArgoCD Application.&lt;/p&gt;

&lt;h3&gt;
  
  
  ArgoCD's approach to Helm and Kustomize
&lt;/h3&gt;

&lt;p&gt;ArgoCD natively supports Helm charts and Kustomize overlays. You can point an Application at a Helm chart in a Git repo or a remote Helm repository, and specify values files or inline value overrides. Kustomize overlays let you maintain a base configuration and layer environment-specific patches on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flux
&lt;/h2&gt;

&lt;p&gt;Flux takes a more composable, Kubernetes-native approach. Instead of a single Application CRD, Flux uses several specialized CRDs that each handle a specific concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitRepository&lt;/strong&gt;: defines a Git source (URL, branch, interval, credentials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HelmRepository&lt;/strong&gt;: defines a Helm chart source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCIRepository&lt;/strong&gt;: defines an OCI registry source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kustomization&lt;/strong&gt;: applies Kustomize overlays from a source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HelmRelease&lt;/strong&gt;: deploys a Helm chart from a source with specified values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageRepository&lt;/strong&gt;: watches a container registry for new image tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageUpdateAutomation&lt;/strong&gt;: automatically updates image tags in Git when new images are pushed
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Flux GitRepository&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;source.toolkit.fluxcd.io/v1&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;GitRepository&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;config-repo&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;flux-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;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/mycompany/config-repo&lt;/span&gt;
&lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="c1"&gt;# Flux Kustomization&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.toolkit.fluxcd.io/v1&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;Kustomization&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;api-production&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;flux-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;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
&lt;span class="na"&gt;sourceRef&lt;/span&gt;&lt;span class="pi"&gt;:&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;GitRepository&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;config-repo&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;./clusters/production/namespaces/api&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;true&lt;/span&gt;
&lt;span class="na"&gt;targetNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flux doesn't have a built-in web UI in the same way ArgoCD does. It's managed primarily via CLI (&lt;code&gt;flux&lt;/code&gt;) and manifests. This makes it feel more "GitOps-native" to some practitioners — Flux's own configuration lives entirely in Git, managed by Flux itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  ArgoCD vs Flux: which to choose
&lt;/h2&gt;

&lt;p&gt;Both tools are mature, widely used, and maintained by strong communities. The choice usually comes down to UI preferences and composition model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose ArgoCD if&lt;/strong&gt;: your team values a visual dashboard for deployment status, you want a unified view across many clusters, or you're adopting GitOps for the first time and want a guided UI to understand what's happening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Flux if&lt;/strong&gt;: you prefer a Kubernetes-native composable model, you want to manage Flux's own configuration via GitOps, or you need the image automation features (automatic image tag updates) that Flux provides out of the box.&lt;/p&gt;

&lt;p&gt;Both support multi-cluster deployments, RBAC, SSO integration, Helm, and Kustomize. Both are CNCF graduated projects. Switching between them is possible but non-trivial — once a team is invested in ArgoCD's Application CRDs or Flux's source controller patterns, migrating means rewriting deployment configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  When GitOps makes sense
&lt;/h2&gt;

&lt;p&gt;GitOps genuinely helps in these situations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-environment management.&lt;/strong&gt; When you have dev, staging, and production environments and want changes to flow through them consistently. GitOps makes the progression explicit — a PR to update staging, review, then a PR to update production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory compliance and audit requirements.&lt;/strong&gt; Every change is a Git commit with an author and timestamp. Audit trails are the commit log. Change approval is the PR review process. This maps naturally to SOC 2, ISO 27001, and similar requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preventing configuration drift.&lt;/strong&gt; Without GitOps, someone inevitably makes a kubectl change that isn't reflected anywhere, and six months later nobody knows why that resource is configured differently from what the manifests say. Continuous reconciliation closes this gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large teams.&lt;/strong&gt; When multiple teams deploy to shared clusters, GitOps provides a structured review process via PRs rather than direct cluster access. Cluster operators review and merge config changes; they don't need to trust every team to run kubectl correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When GitOps adds overhead without value
&lt;/h2&gt;

&lt;p&gt;GitOps isn't always the right choice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small teams or solo projects.&lt;/strong&gt; A two-person team doesn't need the overhead of maintaining separate config repos and GitOps controllers. A simple CI pipeline that runs &lt;code&gt;helm upgrade&lt;/code&gt; after merge is easier to understand and maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateful workloads with complex upgrade procedures.&lt;/strong&gt; Database migrations, multi-step upgrades, or operations that require ordering guarantees don't fit well into declarative reconciliation. GitOps handles stateless workloads well; it's awkward for operations that need imperative steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early-stage applications with rapidly changing infrastructure.&lt;/strong&gt; If your deployment configuration changes multiple times per day, the PR review process becomes a bottleneck. GitOps adds value when the review gate is valuable; it adds friction when fast iteration matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrapping a cluster with GitOps
&lt;/h2&gt;

&lt;p&gt;A common pattern: use Terraform to provision the cluster (EKS, GKE, AKS), then bootstrap GitOps with a single apply command that hands off further configuration to the GitOps controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bootstrap Flux onto a new cluster&lt;/span&gt;
flux bootstrap github &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mycompany &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--repository&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;config-repo &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;clusters/production &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--personal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# After this, everything in clusters/production/ is managed by Flux&lt;/span&gt;
&lt;span class="c"&gt;# Including Flux's own configuration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After bootstrap, even adding new namespaces, installing cluster addons (cert-manager, external-dns, ingress-nginx), or configuring RBAC happens through Git commits rather than direct cluster commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/kubernetes-networking-explained.html"&gt;Kubernetes Networking Explained: Pods, Services, Ingress, and Network Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/kubernetes-resource-management.html"&gt;Kubernetes Resource Management: Requests, Limits, and QoS Classes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/iac-tool-comparison.html"&gt;Terraform vs CDK vs Pulumi: Choosing Your IaC Tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>git</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Docker Networking Explained: Bridge, Host, Overlay, and DNS</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 11:10:13 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/docker-networking-explained-bridge-host-overlay-and-dns-3pgo</link>
      <guid>https://dev.to/pandey-raghvendra/docker-networking-explained-bridge-host-overlay-and-dns-3pgo</guid>
      <description>&lt;p&gt;Docker networking confuses people because the defaults are unintuitive and the mental model isn't obvious from the CLI. A container can reach the internet by default, but two containers on the same host can't reach each other unless they're on the same network. Port publishing doesn't work the way most people expect on Linux hosts. Container-to-container DNS just works in Docker Compose but not when you run containers manually. Understanding why requires knowing a bit about how Docker uses Linux networking primitives.&lt;/p&gt;

&lt;p&gt;This guide covers each Docker network driver, how DNS works inside containers, port publishing mechanics, and how Docker Compose handles multi-container networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network drivers overview
&lt;/h2&gt;

&lt;p&gt;Docker's networking system is pluggable — different drivers implement different networking behaviors. The built-in drivers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;bridge&lt;/strong&gt; — the default for containers on a single host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;host&lt;/strong&gt; — container shares the host's network namespace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;overlay&lt;/strong&gt; — multi-host networking for Docker Swarm or Compose with multiple hosts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macvlan&lt;/strong&gt; — assigns a MAC address to each container so it appears as a physical device on the network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;none&lt;/strong&gt; — disables networking entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ipvlan&lt;/strong&gt; — similar to macvlan but operates at Layer 3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bridge networking: the default
&lt;/h2&gt;

&lt;p&gt;When you run a container without specifying a network, Docker attaches it to the &lt;code&gt;docker0&lt;/code&gt; bridge network. This is a virtual Ethernet bridge — a software switch that forwards packets between container network interfaces and the host's physical interface.&lt;/p&gt;

&lt;p&gt;Each container gets a virtual Ethernet pair (&lt;code&gt;veth&lt;/code&gt;): one end is placed inside the container's network namespace as &lt;code&gt;eth0&lt;/code&gt;, the other end is attached to &lt;code&gt;docker0&lt;/code&gt; on the host. The container gets an IP from Docker's private subnet (typically &lt;code&gt;172.17.0.0/16&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# View the default bridge network&lt;/span&gt;
docker network inspect bridge

&lt;span class="c"&gt;# Containers on the default bridge can reach the internet&lt;/span&gt;
&lt;span class="c"&gt;# via NAT rules that docker manages in iptables/nftables&lt;/span&gt;

&lt;span class="c"&gt;# But they CANNOT reach each other by name — only by IP&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; app1 nginx
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; busybox ping app1  &lt;span class="c"&gt;# This FAILS&lt;/span&gt;
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; busybox ping 172.17.0.2  &lt;span class="c"&gt;# This works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default bridge network has two important limitations: containers can't resolve each other by name, and all containers on it can communicate with each other by IP (no isolation). Both problems are solved by user-defined bridge networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-defined bridge networks
&lt;/h3&gt;

&lt;p&gt;Creating a custom bridge network gives you DNS-based service discovery and network isolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a custom bridge network&lt;/span&gt;
docker network create my-app-network

&lt;span class="c"&gt;# Containers on the same user-defined network can resolve each other by name&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; database &lt;span class="nt"&gt;--network&lt;/span&gt; my-app-network postgres
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;--network&lt;/span&gt; my-app-network my-api-image

&lt;span class="c"&gt;# From inside the api container:&lt;/span&gt;
&lt;span class="c"&gt;# ping database   ← works&lt;/span&gt;
&lt;span class="c"&gt;# curl database:5432  ← works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker's embedded DNS server (at &lt;code&gt;127.0.0.11&lt;/code&gt; inside containers) handles name resolution for user-defined networks. It maps container names and network aliases to their current IPs. When a container is restarted and gets a new IP, the DNS entry updates automatically.&lt;/p&gt;

&lt;p&gt;Containers can also be connected to multiple networks simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network connect frontend-network api
docker network connect backend-network api
&lt;span class="c"&gt;# api can now communicate with containers on both networks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Host networking
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;--network host&lt;/code&gt;, the container shares the host's network namespace. It doesn't get its own IP — it uses the host's IP directly. Port bindings are not needed because the container's ports are the host's ports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--network&lt;/span&gt; host nginx
&lt;span class="c"&gt;# nginx is now listening on the HOST's port 80, not a container port&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the overhead of network address translation and can provide meaningfully better throughput for high-performance networking workloads. However, it also means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No port isolation — if two containers try to bind the same port, one fails&lt;/li&gt;
&lt;li&gt;No network namespace isolation — the container can see all host interfaces&lt;/li&gt;
&lt;li&gt;Port publishing (&lt;code&gt;-p&lt;/code&gt;) flags are ignored; they have no effect with host networking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Host networking is useful for monitoring agents (that need to see all network traffic on the host), high-performance proxies, and cases where the NAT overhead of bridge networking is measurably impactful. It doesn't work on macOS or Windows Docker Desktop because those run Docker inside a Linux VM — the "host" network is the VM's network, not your Mac's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Port publishing
&lt;/h2&gt;

&lt;p&gt;Port publishing (&lt;code&gt;-p host_port:container_port&lt;/code&gt;) maps a port on the host to a port in the container's network namespace. Docker does this by adding iptables rules that DNAT (Destination NAT) incoming traffic on the host port to the container's IP and port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 nginx
&lt;span class="c"&gt;# Traffic arriving at host:8080 is forwarded to the container's port 80&lt;/span&gt;

docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:8080:80 nginx
&lt;span class="c"&gt;# Bind only to localhost — not accessible from other hosts&lt;/span&gt;

docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 80 nginx
&lt;span class="c"&gt;# Docker picks a random ephemeral host port; docker port  80 shows it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A subtle point: &lt;code&gt;-p 8080:80&lt;/code&gt; binds on all interfaces (&lt;code&gt;0.0.0.0:8080&lt;/code&gt;) by default. This means if your firewall is open, the port is accessible from outside the host. Always specify the bind address for sensitive ports: &lt;code&gt;-p 127.0.0.1:8080:80&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Docker's iptables rules can interact unexpectedly with firewall software like ufw. Docker modifies iptables directly, and ufw's FORWARD chain rules don't apply to Docker's DOCKER-USER chain. A container with a published port may be accessible from outside the host even if ufw blocks that port. The fix is to add rules to the DOCKER-USER chain, not to ufw's INPUT or FORWARD chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overlay networking for multi-host setups
&lt;/h2&gt;

&lt;p&gt;Overlay networks span multiple Docker hosts. They're the networking layer for Docker Swarm services and can also be used standalone with Docker Compose when running across multiple hosts.&lt;/p&gt;

&lt;p&gt;An overlay network uses VXLAN encapsulation: packets between containers on different hosts are wrapped in UDP packets and sent over the physical network. From the container's perspective, other containers are on the same flat network regardless of which host they're running on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Overlay networks require Swarm mode to be initialized&lt;/span&gt;
docker swarm init

docker network create &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--driver&lt;/span&gt; overlay &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--attachable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
my-overlay-network

&lt;span class="c"&gt;# Services on this network can reach each other by service name&lt;/span&gt;
&lt;span class="c"&gt;# regardless of which Swarm node they're running on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each overlay network gets a private subnet (default /24). Docker maintains a distributed key-value store (using Raft consensus within Swarm) to coordinate IP assignments and routing across nodes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--attachable&lt;/code&gt; flag allows standalone containers (not Swarm services) to be connected to the overlay network. Without it, only Swarm services can join.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS inside containers
&lt;/h2&gt;

&lt;p&gt;Every container has &lt;code&gt;/etc/resolv.conf&lt;/code&gt; with a nameserver entry. For user-defined networks, this points to Docker's embedded DNS at &lt;code&gt;127.0.0.11&lt;/code&gt;. For the default bridge network or host networking, it points to the host's DNS resolver.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inside a container on a user-defined network:&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/resolv.conf
&lt;span class="c"&gt;# nameserver 127.0.0.11&lt;/span&gt;
&lt;span class="c"&gt;# options ndots:0&lt;/span&gt;

&lt;span class="c"&gt;# Verify DNS is working&lt;/span&gt;
nslookup database    &lt;span class="c"&gt;# resolves to the database container's IP&lt;/span&gt;
nslookup google.com  &lt;span class="c"&gt;# external DNS also resolves via 127.0.0.11 → forwarded to host resolver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker's embedded DNS serves several record types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A records for container names and network aliases&lt;/li&gt;
&lt;li&gt;A records for service names in Swarm (round-robin across all task IPs)&lt;/li&gt;
&lt;li&gt;SRV records for Swarm services (used by DNS-based load balancers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Network aliases allow multiple containers to share the same DNS name, creating a simple round-robin load balancer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; my-net &lt;span class="nt"&gt;--network-alias&lt;/span&gt; web nginx
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; my-net &lt;span class="nt"&gt;--network-alias&lt;/span&gt; web nginx
&lt;span class="c"&gt;# DNS for "web" now round-robins between both containers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Compose networking
&lt;/h2&gt;

&lt;p&gt;Docker Compose automatically creates a user-defined bridge network for each project and connects all services to it. Services can reach each other by service name.&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
&lt;span class="c1"&gt;# No port publishing needed for internal communication&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;

&lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;

&lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="na"&gt;ports&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;80:80"&lt;/span&gt;  &lt;span class="c1"&gt;# Only nginx needs to publish to the host&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this setup: &lt;code&gt;api&lt;/code&gt; reaches &lt;code&gt;database&lt;/code&gt; at hostname &lt;code&gt;database:5432&lt;/code&gt;. &lt;code&gt;nginx&lt;/code&gt; reaches &lt;code&gt;api&lt;/code&gt; at &lt;code&gt;api:8080&lt;/code&gt;. Only &lt;code&gt;nginx&lt;/code&gt; is accessible from outside Docker.&lt;/p&gt;

&lt;p&gt;Compose also supports multiple networks for isolation:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# bridge between networks&lt;/span&gt;

&lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;frontend&lt;/code&gt; can reach &lt;code&gt;api&lt;/code&gt;. &lt;code&gt;api&lt;/code&gt; can reach &lt;code&gt;database&lt;/code&gt;. &lt;code&gt;frontend&lt;/code&gt; cannot reach &lt;code&gt;database&lt;/code&gt; directly — it has no network in common.&lt;/p&gt;

&lt;h3&gt;
  
  
  External networks
&lt;/h3&gt;

&lt;p&gt;Compose can connect services to a network created outside the Compose project:&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;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;shared-monitoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;external&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring-network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for a monitoring stack (Prometheus, Grafana) in one Compose project that needs to scrape targets from other projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Macvlan and ipvlan
&lt;/h2&gt;

&lt;p&gt;Macvlan assigns each container a unique MAC address and connects it directly to a physical network interface. The container appears as a separate physical device on the network and can get an IP from a DHCP server or a static assignment within the physical subnet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--driver&lt;/span&gt; macvlan &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--subnet&lt;/span&gt; 192.168.1.0/24 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--gateway&lt;/span&gt; 192.168.1.1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--opt&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eth0 &lt;span class="se"&gt;\&lt;/span&gt;
my-macvlan-network

docker run &lt;span class="nt"&gt;--network&lt;/span&gt; my-macvlan-network &lt;span class="nt"&gt;--ip&lt;/span&gt; 192.168.1.10 nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Macvlan is useful for containers that need to be reachable at a fixed IP on the physical network without port publishing — legacy applications that require specific IP addresses, network monitoring tools, or applications that need to be accessible from VLAN-isolated segments.&lt;/p&gt;

&lt;p&gt;The limitation: macvlan interfaces can't communicate with their parent interface by default. A container on a macvlan network can talk to other hosts on the physical network, but not to the Docker host itself. This is a Linux kernel limitation with macvlan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging container networking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Container can't reach external hosts.&lt;/strong&gt; Check that IP forwarding is enabled on the host (&lt;code&gt;sysctl net.ipv4.ip_forward&lt;/code&gt; should be 1). Check iptables for a MASQUERADE rule in the POSTROUTING chain: &lt;code&gt;iptables -t nat -L POSTROUTING&lt;/code&gt;. If Docker was recently reinstalled, iptables rules may have been lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two containers can't reach each other.&lt;/strong&gt; Check they're on the same user-defined network. The default bridge network doesn't do DNS. Run &lt;code&gt;docker network inspect&lt;/code&gt; to see which containers are connected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Published port isn't accessible from outside the host.&lt;/strong&gt; Check the host firewall. Note that Docker may have bypassed ufw by writing directly to iptables — check &lt;code&gt;iptables -L DOCKER&lt;/code&gt;. Also verify the container is actually listening on the port: &lt;code&gt;docker exec ss -tlnp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS resolution fails inside container.&lt;/strong&gt; Verify you're on a user-defined network (not the default bridge). Check &lt;code&gt;/etc/resolv.conf&lt;/code&gt; inside the container. Try &lt;code&gt;nslookup 127.0.0.11&lt;/code&gt; to test if Docker's DNS is reachable. Also check that the &lt;code&gt;--dns&lt;/code&gt; flag isn't overriding the default nameserver.&lt;/p&gt;

&lt;p&gt;Visualizing how Docker Compose services connect to each other — which services share networks and which are isolated — helps catch misconfigured networking before deploying. &lt;a href="https://dev.to/"&gt;InfraSketch&lt;/a&gt; parses Docker Compose files and maps services and their network relationships as a diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/kubernetes-networking-explained.html"&gt;Kubernetes Networking Explained: Pods, Services, Ingress, and Network Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/docker-compose-diagram-tutorial.html"&gt;Docker Compose Architecture Diagrams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/aws-vpc-architecture-explained.html"&gt;AWS VPC Architecture Explained: From Simple to Production&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AWS IAM Explained: Users, Roles, Policies, and Least Privilege</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 11:10:11 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/aws-iam-explained-users-roles-policies-and-least-privilege-l1e</link>
      <guid>https://dev.to/pandey-raghvendra/aws-iam-explained-users-roles-policies-and-least-privilege-l1e</guid>
      <description>&lt;p&gt;AWS IAM (Identity and Access Management) is the authorization layer for everything in AWS. Every API call — whether from the console, CLI, an EC2 instance, or a Lambda function — is evaluated against IAM policy. Get it wrong and you either lock out legitimate services or leave your infrastructure open to lateral movement. Despite its importance, IAM is where most teams accumulate the most technical debt, usually in the form of overly broad policies that were "temporary" and became permanent.&lt;/p&gt;

&lt;p&gt;This guide covers how IAM actually works — the principal model, how policies are evaluated, role assumption, and the patterns used most in production infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principals: who is making the request
&lt;/h2&gt;

&lt;p&gt;Every IAM interaction starts with a principal — the entity making the API call. AWS has four types of principals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM users&lt;/strong&gt; are long-lived identities with permanent credentials. They authenticate with a username/password for the console or an access key pair for the CLI/API. Creating IAM users for human access is legacy practice now — most organizations use SSO (AWS IAM Identity Center, formerly SSO) to federate human identities from an identity provider like Okta or Azure AD, then assume roles within AWS accounts. IAM users still make sense for service accounts that need permanent, programmatic API access, though roles are preferred even there when possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM roles&lt;/strong&gt; are identities that can be assumed. They don't have permanent credentials — instead, when a principal assumes a role, STS (Security Token Service) issues temporary credentials that expire (typically 1–12 hours). Roles are the preferred mechanism for granting AWS services access to other AWS services, for cross-account access, and for federated human access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM groups&lt;/strong&gt; are collections of users. A group has policies attached to it; all users in the group inherit those permissions. Groups don't support role assumption — they're only for organizing users and applying policies in bulk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS service principals&lt;/strong&gt; represent AWS services (EC2, Lambda, ECS, etc.) acting on your behalf. When you create an EC2 instance profile or a Lambda execution role, you're granting an AWS service the ability to assume that role. The service principal looks like &lt;code&gt;ec2.amazonaws.com&lt;/code&gt; or &lt;code&gt;lambda.amazonaws.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policies: what the principal is allowed to do
&lt;/h2&gt;

&lt;p&gt;Policies are JSON documents that specify what actions are allowed or denied on which resources under what conditions. IAM has six types of policies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity-based policies&lt;/strong&gt; are attached directly to users, groups, or roles. They specify what that principal is allowed (or denied) to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource-based policies&lt;/strong&gt; are attached to resources (S3 buckets, SQS queues, KMS keys, Lambda functions, etc.) rather than to principals. They specify who is allowed to do what with that resource. S3 bucket policies are the most common example. Resource-based policies support cross-account access without role assumption — you can grant a principal in another account direct access to an S3 bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission boundaries&lt;/strong&gt; cap the maximum permissions an IAM user or role can have. Even if an identity-based policy grants &lt;code&gt;s3:*&lt;/code&gt;, if the permission boundary only allows &lt;code&gt;s3:GetObject&lt;/code&gt;, only GetObject is allowed. This is useful for delegating IAM management — you can let teams create their own roles as long as those roles don't exceed the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service Control Policies (SCPs)&lt;/strong&gt; apply to entire AWS Organizations accounts. An SCP can prevent all accounts in an OU from using specific regions or services regardless of what IAM policies in those accounts say. SCPs are a ceiling, not a floor — they don't grant permissions, they only restrict them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session policies&lt;/strong&gt; are optional policies passed at role assumption time via &lt;code&gt;sts:AssumeRole&lt;/code&gt; or &lt;code&gt;sts:GetFederationToken&lt;/code&gt;. They further restrict the permissions for that specific session without modifying the role itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint policies&lt;/strong&gt; restrict what specific VPC endpoints can access.&lt;/p&gt;

&lt;h2&gt;
  
  
  How IAM evaluates a request
&lt;/h2&gt;

&lt;p&gt;When an API call arrives, IAM evaluates it through a multi-step decision tree. Understanding this order is essential for diagnosing access issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit deny check&lt;/strong&gt;: If any applicable policy (identity, resource, SCP, permission boundary) has an explicit &lt;code&gt;Deny&lt;/code&gt; for the action, the request is denied immediately. Explicit denies always win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCP check&lt;/strong&gt;: If the account is in an Organization and an SCP doesn't allow the action, the request is denied regardless of IAM policies in the account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource-based policy check&lt;/strong&gt;: If a resource-based policy explicitly allows the request (and there's no explicit deny), it's allowed. This is how cross-account access without role assumption works — the resource policy grants access to a principal in another account directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity-based policy check&lt;/strong&gt;: If an identity-based policy attached to the principal allows the action, it's allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission boundary check&lt;/strong&gt;: If a permission boundary is set, the intersection of what the identity-based policy allows and what the boundary allows is the effective permission set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default deny&lt;/strong&gt;: If none of the above granted access, the request is denied. AWS defaults to deny-everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical takeaway: cross-account access needs to be allowed in both the identity-based policy on the source side AND the resource-based policy on the target side. A common mistake is adding a resource-based policy without giving the source principal permission to assume the role or access the resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy anatomy
&lt;/h2&gt;

&lt;p&gt;An IAM policy document is JSON with a &lt;code&gt;Statement&lt;/code&gt; array. Each statement has an &lt;code&gt;Effect&lt;/code&gt; (Allow or Deny), &lt;code&gt;Action&lt;/code&gt; (which API operations), &lt;code&gt;Resource&lt;/code&gt; (which ARNs), and optionally a &lt;code&gt;Condition&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-company-artifacts/*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-company-artifacts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"s3:prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"releases/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Condition&lt;/code&gt; block can restrict access by source IP, time of day, MFA status, required tags, and many other attributes. Conditions don't grant permissions — they further restrict when an Allow applies.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Resource&lt;/code&gt; field is where least privilege is enforced. Instead of &lt;code&gt;"Resource": "*"&lt;/code&gt;, use the actual ARN of the specific resource. For S3, &lt;code&gt;arn:aws:s3:::bucket-name&lt;/code&gt; grants access to the bucket itself (for ListBucket), while &lt;code&gt;arn:aws:s3:::bucket-name/*&lt;/code&gt; grants access to objects inside it (for GetObject, PutObject).&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust policies and role assumption
&lt;/h2&gt;

&lt;p&gt;Every IAM role has two components: an identity-based policy (what the role can do) and a trust policy (who can assume the role). The trust policy is a resource-based policy on the role itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trust policy allows EC2 instances to assume the role. For human cross-account access, the principal would be an IAM role or user ARN from another account. For OIDC-based access (GitHub Actions, EKS pods via IRSA), the principal is an OIDC provider with a condition that matches the federated identity claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common patterns with Terraform
&lt;/h2&gt;

&lt;h3&gt;
  
  
  EC2 instance profile
&lt;/h3&gt;

&lt;p&gt;An instance profile is the mechanism that attaches an IAM role to an EC2 instance. When EC2 starts an instance with an instance profile, the instance metadata endpoint (&lt;code&gt;169.254.169.254&lt;/code&gt; or IMDSv2 equivalent) provides temporary credentials for the attached role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"app_instance"&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;"app-instance-role"&lt;/span&gt;

&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"app_instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&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;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;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_bucket&lt;/span&gt;&lt;span class="k"&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;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;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:secretsmanager:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:secret:app/*"&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_instance_profile"&lt;/span&gt; &lt;span class="s2"&gt;"app"&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;"app-instance-profile"&lt;/span&gt;
&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_instance&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lambda execution role
&lt;/h3&gt;

&lt;p&gt;Lambda functions need a role with at minimum the &lt;code&gt;AWSLambdaBasicExecutionRole&lt;/code&gt; managed policy (for CloudWatch Logs). Add only the additional permissions the function actually needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_execution"&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;"my-function-execution-role"&lt;/span&gt;

&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&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="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_basic"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_specific"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Action&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="s2"&gt;"dynamodb:PutItem"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&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;h3&gt;
  
  
  IRSA — IAM Roles for Service Accounts (EKS)
&lt;/h3&gt;

&lt;p&gt;IRSA allows Kubernetes pods to assume IAM roles without sharing credentials at the node level. The trust policy uses the cluster's OIDC provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_openid_connect_provider"&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;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eks_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&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="nx"&gt;oidc&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="nx"&gt;issuer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"pod_role"&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;"my-app-pod-role"&lt;/span&gt;

&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
&lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trimprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:sub"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="s2"&gt;"system:serviceaccount:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namespace&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_account_name&lt;/span&gt;&lt;span class="k"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kubernetes ServiceAccount is annotated with the role ARN. When a pod runs with that ServiceAccount, the EKS Pod Identity webhook injects environment variables that direct the AWS SDK to use WebIdentity tokens for authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-account access
&lt;/h3&gt;

&lt;p&gt;Cross-account access requires granting permission on both sides. In the target account, create a role with a trust policy that allows the source account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In the target account (account B)&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"cross_account"&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;"cross-account-access-role"&lt;/span&gt;

&lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Principal&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"arn:aws:iam::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:root"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
&lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="s2"&gt;"sts:ExternalId"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_id&lt;/span&gt;  &lt;span class="c1"&gt;# prevents confused deputy attacks&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="c1"&gt;# In the source account (account A), grant the permission to assume&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"assume_cross_account"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deployer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&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;"2012-10-17"&lt;/span&gt;
&lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&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;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
&lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:role/cross-account-access-role"&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;h2&gt;
  
  
  Implementing least privilege
&lt;/h2&gt;

&lt;p&gt;The principle of least privilege means granting only the permissions needed to perform a specific function and nothing more. In practice, it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope actions precisely&lt;/strong&gt;: Use &lt;code&gt;s3:GetObject&lt;/code&gt; not &lt;code&gt;s3:*&lt;/code&gt;. Use &lt;code&gt;ec2:DescribeInstances&lt;/code&gt; not &lt;code&gt;ec2:*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope resources precisely&lt;/strong&gt;: Use the specific resource ARN rather than &lt;code&gt;*&lt;/code&gt;. For DynamoDB: &lt;code&gt;arn:aws:dynamodb:region:account:table/specific-table&lt;/code&gt;, not &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use conditions&lt;/strong&gt;: Add conditions to restrict when permissions apply — source IP, MFA requirement, resource tags, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid wildcard patterns in sensitive services&lt;/strong&gt;: &lt;code&gt;iam:*&lt;/code&gt;, &lt;code&gt;sts:*&lt;/code&gt;, and &lt;code&gt;kms:*&lt;/code&gt; are especially dangerous. Wildcard access to IAM means the principal can grant itself any permission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS's IAM Access Analyzer and the IAM Policy Simulator help audit existing policies. Access Analyzer also has a feature that generates least-privilege policies from CloudTrail logs — it observes what an identity actually calls over a period and generates a policy based on observed usage. This is the most practical approach for brownfield permissions: run the role in production, observe for 30–90 days, then generate a tight policy from actual usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AdministratorAccess on service roles.&lt;/strong&gt; The policy &lt;code&gt;arn:aws:iam::aws:policy/AdministratorAccess&lt;/code&gt; grants full access to everything in the account. Attaching this to a Lambda function or EC2 instance because "it needs to do a lot of things" is a security catastrophe waiting to happen. A compromised instance or function with AdministratorAccess can exfiltrate data, create new users, disable logging, or delete everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing long-lived access keys in code or environment variables.&lt;/strong&gt; Access keys associated with IAM users don't expire. If they leak (in a git commit, an S3 bucket, logs), they're valid indefinitely until rotated. Use IAM roles and instance metadata instead. For CI/CD, use OIDC federation (GitHub Actions OIDC, GitLab OIDC) rather than static keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared IAM users across services or teams.&lt;/strong&gt; One IAM user shared by multiple services means you can't audit which service made which API call, can't rotate credentials for one service without affecting others, and can't revoke access for one service without affecting all. Each service gets its own role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission boundaries not used for delegated IAM.&lt;/strong&gt; If you let application teams create their own IAM roles, they can escalate their own privileges. A permission boundary constrains what roles they can create — the team can create roles, but those roles can't exceed what the permission boundary allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging access issues
&lt;/h2&gt;

&lt;p&gt;When an IAM error appears (&lt;code&gt;AccessDeniedException&lt;/code&gt; or &lt;code&gt;is not authorized to perform: on resource:&lt;/code&gt;), the debug workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the principal in the error — it's in the message as "User: arn:aws:iam::..."&lt;/li&gt;
&lt;li&gt;Use the IAM Policy Simulator in the console to test whether that principal has the required permission&lt;/li&gt;
&lt;li&gt;Check for explicit denies — SCPs, permission boundaries, resource policies&lt;/li&gt;
&lt;li&gt;Check that resource-based policies allow the cross-account action if applicable&lt;/li&gt;
&lt;li&gt;Enable CloudTrail data events if not already enabled — access denials are logged there with full context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;IAM is complex enough that AWS's documentation for specific services often lists exactly which IAM permissions are needed. The IAM actions reference at &lt;code&gt;docs.aws.amazon.com/service-authorization/latest/reference/&lt;/code&gt; is the authoritative source for every service, action, resource type, and condition key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/aws-vpc-architecture-explained.html"&gt;AWS VPC Architecture Explained: From Simple to Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-state-explained.html"&gt;Terraform State Explained: What It Is, How It Works, and Why It Breaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/iac-tool-comparison.html"&gt;Terraform vs CDK vs Pulumi: Choosing Your IaC Tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>infrastructure</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Terraform State Explained: What It Is, How It Works, and Why It Breaks</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sun, 07 Jun 2026 00:25:56 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/terraform-state-explained-what-it-is-how-it-works-and-why-it-breaks-1omp</link>
      <guid>https://dev.to/pandey-raghvendra/terraform-state-explained-what-it-is-how-it-works-and-why-it-breaks-1omp</guid>
      <description>&lt;p&gt;Terraform state is one of those concepts that every Terraform user encounters early, trips over repeatedly, and eventually develops strong opinions about. If you've ever seen &lt;code&gt;Error: state lock&lt;/code&gt;, found your &lt;code&gt;terraform.tfstate&lt;/code&gt; committed to git by accident, or watched a colleague manually delete a resource from AWS only to have Terraform recreate it, you've already met the state file.&lt;/p&gt;

&lt;p&gt;This article explains what Terraform state actually is, how Terraform uses it, what goes wrong, and how teams structure their state to stay sane at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Terraform state?
&lt;/h2&gt;

&lt;p&gt;Terraform needs to map the resources defined in your &lt;code&gt;.tf&lt;/code&gt; files to the real resources that exist in your cloud provider. That mapping is stored in the state file — by default a JSON file called &lt;code&gt;terraform.tfstate&lt;/code&gt; in your working directory.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads your &lt;code&gt;.tf&lt;/code&gt; configuration files&lt;/li&gt;
&lt;li&gt;Reads the current state file to understand what already exists&lt;/li&gt;
&lt;li&gt;Calls the cloud provider APIs to get the current real-world state&lt;/li&gt;
&lt;li&gt;Computes a diff between desired state (config) and actual state (provider)&lt;/li&gt;
&lt;li&gt;Applies the changes and updates the state file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without state, Terraform would have no way to know that the &lt;code&gt;aws_instance.web&lt;/code&gt; in your config corresponds to instance &lt;code&gt;i-0abc123def456&lt;/code&gt; in AWS. It would try to create a new one every time you run apply.&lt;/p&gt;

&lt;p&gt;State is also how Terraform tracks metadata that isn't visible in the config — resource IDs, ARNs, IP addresses assigned by the cloud provider, dependency ordering, and provider version constraints. Open any &lt;code&gt;terraform.tfstate&lt;/code&gt; file and you'll see a JSON structure with a &lt;code&gt;resources&lt;/code&gt; array where each entry contains both your config attributes and the provider-returned attributes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"terraform_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.7.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"managed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;registry.terraform.io/hashicorp/aws&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"instances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vpc-0a1b2c3d4e5f6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"cidr_block"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0a1b2c3d4e5f6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"enable_dns_hostnames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Local vs remote state
&lt;/h2&gt;

&lt;p&gt;By default, Terraform writes state to a local file. This works fine when you're learning or building something solo. It breaks down immediately in a team setting because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two engineers can't safely run Terraform at the same time&lt;/li&gt;
&lt;li&gt;The state file needs to be shared somehow — and sharing via git is dangerous (state contains sensitive values like passwords and access keys)&lt;/li&gt;
&lt;li&gt;If the file is lost or corrupted, Terraform loses track of all managed resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remote state solves this. Instead of writing to a local file, Terraform stores state in a remote backend — S3, Azure Blob Storage, GCS, Terraform Cloud, or others. The backend configuration goes in your Terraform code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;backend&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;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-company-terraform-state"&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;"production/vpc/terraform.tfstate"&lt;/span&gt;
&lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-lock"&lt;/span&gt;
&lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;dynamodb_table&lt;/code&gt; entry is not optional if you have multiple people or pipelines running Terraform. It's the locking mechanism that prevents two simultaneous applies from corrupting state.&lt;/p&gt;

&lt;h2&gt;
  
  
  State locking
&lt;/h2&gt;

&lt;p&gt;When Terraform starts an operation that modifies state (plan, apply, destroy), it acquires a lock on the state file. Any other Terraform operation that tries to acquire the same lock will wait or fail, depending on whether you pass &lt;code&gt;-lock-timeout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Locking is implemented differently per backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 + DynamoDB&lt;/strong&gt;: Terraform writes a lock record to a DynamoDB table. The record contains the operation, who started it, and a unique lock ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Cloud / HCP Terraform&lt;/strong&gt;: Locking is built into the backend — no separate database needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Blob Storage&lt;/strong&gt;: Uses blob lease mechanism, which Azure provides natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local&lt;/strong&gt;: Creates a &lt;code&gt;.terraform.tfstate.lock.info&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common locking failure is a stale lock — a lock that wasn't released because Terraform was killed mid-run (Ctrl-C, CI timeout, power loss). When this happens, subsequent runs fail with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Error locking state: Error acquiring the state lock:
ConditionalCheckFailedException: The conditional request failed

Lock Info:
ID:        abc123-def456
Path:      production/vpc/terraform.tfstate
Operation: OperationTypeApply
Who:       ci-runner@build-server
Version:   1.7.0
Created:   2026-06-01 09:23:11 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To clear a stale lock, you need to confirm the original operation actually stopped (check your CI logs), then run &lt;code&gt;terraform force-unlock&lt;/code&gt;. Never force-unlock while another operation is running — you'll get state corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  State drift
&lt;/h2&gt;

&lt;p&gt;Drift happens when the real-world state of a resource no longer matches what's in the Terraform state file. This is usually caused by manual changes — someone logs into the AWS console and modifies a security group rule, or uses the AWS CLI to change an instance type, or another tool (CloudFormation, a script, another Terraform root) touches the same resource.&lt;/p&gt;

&lt;p&gt;From Terraform's perspective, nothing changed — its state file still says the old configuration. The next time you run &lt;code&gt;terraform plan&lt;/code&gt;, it might show no changes (if the drift is in an attribute Terraform isn't managing) or show unexpected changes (if Terraform wants to revert the manual change).&lt;/p&gt;

&lt;p&gt;To detect drift explicitly, use &lt;code&gt;terraform plan -refresh-only&lt;/code&gt;. This refreshes Terraform's view of actual resource attributes without proposing any config changes. If there's drift, the plan will show what changed in the real world. You can then either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept the drift: &lt;code&gt;terraform apply -refresh-only&lt;/code&gt; updates the state file to match reality&lt;/li&gt;
&lt;li&gt;Revert the drift: Run a normal &lt;code&gt;terraform apply&lt;/code&gt; to push your config back over the manual change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams that want to detect drift continuously can run &lt;code&gt;terraform plan -refresh-only&lt;/code&gt; on a schedule (hourly, nightly) and alert if the plan is non-empty. This is sometimes called "drift detection" and it's available natively in Terraform Cloud and HCP Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing existing resources
&lt;/h2&gt;

&lt;p&gt;When you have infrastructure that predates your Terraform codebase — resources created manually, from another tool, or by another team — you need to import them into Terraform state before Terraform can manage them.&lt;/p&gt;

&lt;p&gt;The old way: &lt;code&gt;terraform import .&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Import an existing VPC&lt;/span&gt;
terraform import aws_vpc.main vpc-0a1b2c3d4e5f6

&lt;span class="c"&gt;# Import an existing S3 bucket&lt;/span&gt;
terraform import aws_s3_bucket.assets my-company-assets-bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes the resource into state, but it doesn't write the corresponding &lt;code&gt;.tf&lt;/code&gt; config. You have to write that yourself, then run &lt;code&gt;terraform plan&lt;/code&gt; to verify it produces no changes (meaning your config matches what was imported).&lt;/p&gt;

&lt;p&gt;Since Terraform 1.5, you can use import blocks in your config instead:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;
&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-0a1b2c3d4e5f6"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="c1"&gt;# ... other attributes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 1.5+ import blocks also support &lt;code&gt;terraform plan --generate-config-out=generated.tf&lt;/code&gt;, which writes the HCL config for you based on what Terraform reads from the provider. It's not perfect — some attributes require manual adjustment — but it significantly reduces the manual work of importing large amounts of existing infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  State file structure and workspaces
&lt;/h2&gt;

&lt;p&gt;Every Terraform root module has exactly one state file per workspace. Workspaces let you maintain multiple independent state files from the same configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform workspace new staging
terraform workspace new production
terraform workspace list
&lt;span class="c"&gt;# * staging&lt;/span&gt;
&lt;span class="c"&gt;#   production&lt;/span&gt;
&lt;span class="c"&gt;#   default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using S3 as a backend, workspaces are stored as separate files under a &lt;code&gt;env://&lt;/code&gt; path prefix by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://my-bucket/terraform.tfstate              # default workspace
s3://my-bucket/env:/staging/terraform.tfstate  # staging workspace
s3://my-bucket/env:/production/terraform.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workspaces are useful for managing identical infrastructure across environments (dev, staging, prod) from the same Terraform configuration, using &lt;code&gt;terraform.workspace&lt;/code&gt; to parameterize values. The downside is that a single mistake in the wrong workspace can affect production. Many teams prefer separate root modules per environment (separate directories, separate state files) rather than workspaces, because the blast radius of a mistake is confined to one directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring state for large codebases
&lt;/h2&gt;

&lt;p&gt;As infrastructure grows, keeping everything in a single Terraform root module with one state file becomes a liability. A large state file means every plan/apply operation reads and writes the entire state, locking is coarser (one lock for all infrastructure), and a corrupted state file is a catastrophe.&lt;/p&gt;

&lt;p&gt;The standard approach is to split infrastructure into multiple Terraform root modules, each with its own state file. Common split strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By layer&lt;/strong&gt;: networking, compute, databases, IAM, DNS — each in its own root. Lower layers are applied first; higher layers reference outputs via &lt;code&gt;terraform_remote_state&lt;/code&gt; data sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By environment&lt;/strong&gt;: separate roots for dev, staging, production. The same module code, different variable files and state buckets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By team&lt;/strong&gt;: each team owns their infrastructure layer. Cross-team dependencies are exposed via outputs or shared data sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terragrunt is a wrapper around Terraform that automates multi-root state management. It lets you define a hierarchy of &lt;code&gt;terragrunt.hcl&lt;/code&gt; files where each directory represents a root module with its own state key, and dependencies are declared explicitly. Running &lt;code&gt;terragrunt run-all apply&lt;/code&gt; from the repo root applies all roots in dependency order.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually in the state file (and why it's sensitive)
&lt;/h2&gt;

&lt;p&gt;Terraform state stores every attribute of every managed resource, including values that Terraform marks as &lt;code&gt;sensitive&lt;/code&gt; in the provider schema. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database passwords (RDS master passwords, Redis auth tokens)&lt;/li&gt;
&lt;li&gt;IAM access keys if you manage them with Terraform&lt;/li&gt;
&lt;li&gt;TLS private keys if you create certificates with &lt;code&gt;tls_private_key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Any secret values passed to resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why committing &lt;code&gt;terraform.tfstate&lt;/code&gt; to git is a security problem, regardless of whether the repo is private. Use remote state with server-side encryption at rest. For S3, the &lt;code&gt;encrypt = true&lt;/code&gt; backend option enables SSE-S3. Most teams pair this with a strict &lt;code&gt;.gitignore&lt;/code&gt; that excludes all &lt;code&gt;*.tfstate&lt;/code&gt; and &lt;code&gt;*.tfstate.backup&lt;/code&gt; files as a defense in depth measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering from state problems
&lt;/h2&gt;

&lt;p&gt;State problems are stressful because they can affect live infrastructure. Here's a quick reference for the most common scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State file accidentally deleted.&lt;/strong&gt; If you have backups (S3 versioning, Terraform Cloud history), restore from the most recent backup. If not, you'll need to re-import every resource — tedious but possible. Enable S3 versioning on your state bucket before you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State file corrupted (invalid JSON).&lt;/strong&gt; Terraform keeps a &lt;code&gt;terraform.tfstate.backup&lt;/code&gt; of the previous successful state. Restore it: &lt;code&gt;cp terraform.tfstate.backup terraform.tfstate&lt;/code&gt;. With remote state on S3 with versioning, use &lt;code&gt;aws s3api list-object-versions&lt;/code&gt; to find and restore a previous version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource exists in state but not in cloud.&lt;/strong&gt; Someone manually deleted the resource. Run &lt;code&gt;terraform state rm&lt;/code&gt; to remove it from state, then apply to let Terraform recreate it. Or skip the rm and let the next apply fail with a provider error and recreate from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource exists in cloud but not in state.&lt;/strong&gt; You need to import it: &lt;code&gt;terraform import&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two state files got merged or need splitting.&lt;/strong&gt; Use &lt;code&gt;terraform state mv&lt;/code&gt; and &lt;code&gt;terraform state pull&lt;/code&gt;/&lt;code&gt;push&lt;/code&gt; to move resources between state files. This is advanced — read the Terraform docs carefully and take a backup first.&lt;/p&gt;

&lt;h2&gt;
  
  
  State commands reference
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;terraform state&lt;/code&gt; subcommand group is the primary interface for inspecting and modifying state without running apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform state list&lt;/code&gt; — list all resources tracked in state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state show&lt;/code&gt; — show all attributes of a specific resource from state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state rm&lt;/code&gt; — remove a resource from state (doesn't destroy the real resource)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state mv&lt;/code&gt; — rename or move a resource within state (useful when refactoring module structure)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state pull&lt;/code&gt; — download and print remote state as JSON&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform state push&lt;/code&gt; — upload a local state file to the remote backend (dangerous — use with care)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform force-unlock&lt;/code&gt; — release a stuck state lock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding Terraform state isn't just academic — it directly affects how safely your team can make infrastructure changes. Investing in remote state with versioning and locking early pays off every time someone needs to recover from a mistake or audit what changed and when.&lt;/p&gt;

&lt;p&gt;If you want to see what your Terraform code is actually building, &lt;a href="https://dev.to/"&gt;InfraSketch&lt;/a&gt; generates architecture diagrams directly from &lt;code&gt;.tf&lt;/code&gt; files or &lt;code&gt;terraform show -json&lt;/code&gt; plan output — useful for reviewing what state will look like after the next apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/terraform-visualization-best-practices.html"&gt;Terraform Visualization: 5 Ways to See What Your Code Actually Builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-diagram-generator.html"&gt;Terraform Architecture Diagram Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/iac-tool-comparison.html"&gt;Terraform vs CDK vs Pulumi: Choosing Your IaC Tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Kubernetes Networking Explained: Pods, Services, Ingress, and Network Policies</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sun, 07 Jun 2026 00:25:24 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/kubernetes-networking-explained-pods-services-ingress-and-network-policies-2k8m</link>
      <guid>https://dev.to/pandey-raghvendra/kubernetes-networking-explained-pods-services-ingress-and-network-policies-2k8m</guid>
      <description>&lt;p&gt;Kubernetes networking is one of the most misunderstood parts of running containerized workloads. A pod can reach another pod by IP — but why does that stop working after a deployment? A service exists and resolves in DNS — but traffic isn't arriving at the application. An Ingress resource is configured — but requests return 502. These puzzles are common and they stem from the same root: Kubernetes networking has several distinct layers, each solving a different problem, and it's easy to conflate them.&lt;/p&gt;

&lt;p&gt;This article walks through how Kubernetes networking actually works at each layer — from pod networking to services to Ingress to network policy — so the next time something breaks, you have a mental model to reason from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fundamental promise: flat pod networking
&lt;/h2&gt;

&lt;p&gt;Kubernetes makes one core promise about networking: every pod can communicate directly with every other pod in the cluster without NAT. Every pod gets a real IP address from the cluster's pod CIDR range, and those IPs are routable between pods regardless of which node they're running on.&lt;/p&gt;

&lt;p&gt;This is not something Kubernetes itself implements. It's a contract that every Kubernetes-conformant CNI (Container Network Interface) plugin must fulfill. When you install Calico, Cilium, Flannel, Weave, or any other CNI, you're installing the component that actually creates this flat network. The mechanism varies — Flannel uses VXLAN overlays, Calico can use BGP for direct routing, Cilium uses eBPF — but the result is the same: pod-to-pod communication without NAT.&lt;/p&gt;

&lt;p&gt;Here's what a pod's network namespace looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; my-pod &lt;span class="nt"&gt;--&lt;/span&gt; ip addr
&lt;span class="go"&gt;1: lo:  ...
3: eth0@if12:  ...
inet 10.244.1.15/24 brd 10.244.1.255 scope global eth0

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; my-pod &lt;span class="nt"&gt;--&lt;/span&gt; ip route
&lt;span class="go"&gt;default via 10.244.1.1 dev eth0
10.244.0.0/16 via 10.244.1.1 dev eth0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod has an IP (&lt;code&gt;10.244.1.15&lt;/code&gt;) on a /24 subnet. The node this pod runs on has an IP from the same range — or a different /24 within the same /16. Traffic from this pod to &lt;code&gt;10.244.2.8&lt;/code&gt; (a pod on another node) goes through the CNI-managed overlay or routed network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you shouldn't use pod IPs directly
&lt;/h2&gt;

&lt;p&gt;If pods can communicate directly, why not just use pod IPs? Two reasons.&lt;/p&gt;

&lt;p&gt;First, pod IPs are ephemeral. When a pod is killed and rescheduled — whether from a node failure, a rolling deployment, or the scheduler moving it — it gets a new IP. Any hardcoded reference to the old IP breaks immediately.&lt;/p&gt;

&lt;p&gt;Second, horizontal scaling creates multiple pod instances. If you have three replicas of your API service, you need something that routes traffic across all three, not just the first one.&lt;/p&gt;

&lt;p&gt;Services solve both problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services: stable DNS names and load balancing
&lt;/h2&gt;

&lt;p&gt;A Kubernetes Service is a stable virtual endpoint that fronts a set of pods. It has a cluster-scoped DNS name (&lt;code&gt;my-service.my-namespace.svc.cluster.local&lt;/code&gt;) and a virtual IP (ClusterIP) that doesn't change. The Service uses a label selector to determine which pods are in its backend pool:&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;v1&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;Service&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;api&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;backend&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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes continuously watches pods with labels &lt;code&gt;app=api, tier=backend&lt;/code&gt;. When pods matching the selector are created, their IP:port is added to an Endpoints object. When they're deleted, they're removed. The Service IP stays the same; the backing Endpoints change as pods come and go.&lt;/p&gt;

&lt;p&gt;Traffic routing to services is handled by kube-proxy (or Cilium/eBPF in modern clusters). kube-proxy watches for Endpoints changes and programs iptables rules (or IPVS rules) that intercept traffic to the ClusterIP and distribute it across the current pod set.&lt;/p&gt;

&lt;h3&gt;
  
  
  ClusterIP, NodePort, and LoadBalancer
&lt;/h3&gt;

&lt;p&gt;Services have a &lt;code&gt;type&lt;/code&gt; field that controls how they're exposed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClusterIP&lt;/strong&gt; (default): The service is accessible only within the cluster. Other pods can reach it at &lt;code&gt;api.backend.svc.cluster.local:80&lt;/code&gt;. External traffic cannot reach it. This is the right type for internal services — databases, caches, backend APIs called only by other services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NodePort&lt;/strong&gt;: Opens a port on every node (in the 30000–32767 range by default) and routes traffic to the service. You can reach the service at &lt;code&gt;:&lt;/code&gt;. NodePort services are rarely used in production for external traffic because they require knowing node IPs and using non-standard ports. They're sometimes used for development or for specific cases where an external load balancer handles the stable IP layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LoadBalancer&lt;/strong&gt;: Provisions an external load balancer from the cloud provider (an AWS ALB/NLB, a GCP load balancer, an Azure LB). The load balancer gets a stable external IP and routes to the NodePort service underneath. This is the straightforward way to expose a single service externally, but it creates one load balancer per service — expensive at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  ExternalName and headless services
&lt;/h3&gt;

&lt;p&gt;Two less-common service types worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ExternalName&lt;/strong&gt;: Returns a CNAME record to an external DNS name. Useful for giving a cluster-internal alias to an external database or API: &lt;code&gt;db.backend.svc.cluster.local&lt;/code&gt; resolves to &lt;code&gt;my-db.rds.amazonaws.com&lt;/code&gt;. Services can reference external dependencies by internal name without embedding external DNS names in application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headless services&lt;/strong&gt; (&lt;code&gt;clusterIP: None&lt;/code&gt;): Instead of returning the virtual ClusterIP, DNS returns the actual pod IPs. Each DNS query returns the IPs of all pods matching the selector. This is used for stateful applications (StatefulSets) where clients need to connect to specific pod instances, not a load-balanced virtual IP. MongoDB, Cassandra, and other distributed databases use headless services so each pod is individually addressable.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS in Kubernetes
&lt;/h2&gt;

&lt;p&gt;Kubernetes runs CoreDNS (or kube-dns in older clusters) as a cluster DNS resolver. Every pod is configured with a &lt;code&gt;resolv.conf&lt;/code&gt; that points to CoreDNS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; my-pod &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/resolv.conf
&lt;span class="go"&gt;nameserver 10.96.0.10
search backend.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;search&lt;/code&gt; entries mean that short service names like &lt;code&gt;api&lt;/code&gt; are automatically expanded. A lookup for &lt;code&gt;api&lt;/code&gt; from within the &lt;code&gt;backend&lt;/code&gt; namespace first tries &lt;code&gt;api.backend.svc.cluster.local&lt;/code&gt;, finds the service, and returns. A lookup from a different namespace would need &lt;code&gt;api.backend&lt;/code&gt; or the full FQDN.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ndots:5&lt;/code&gt; setting means names with fewer than 5 dots are treated as relative and go through the search path before being treated as absolute. This can cause unexpected DNS latency if every external hostname lookup triggers multiple failed cluster-internal lookups first. Setting &lt;code&gt;ndots: 2&lt;/code&gt; or using fully-qualified hostnames (with a trailing dot) for external lookups can reduce DNS lookup latency in high-throughput services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ingress: HTTP routing at the cluster edge
&lt;/h2&gt;

&lt;p&gt;LoadBalancer services expose one service per load balancer. In practice, a production cluster might have 20 HTTP services that should all be accessible under a single external IP. Ingress handles this with HTTP-level routing — one external load balancer, multiple services, routing by hostname and path.&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;networking.k8s.io/v1&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;Ingress&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;app-ingress&lt;/span&gt;
&lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&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;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.mycompany.com&lt;/span&gt;
&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&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;/v1&lt;/span&gt;
&lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;service&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;api-v1&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&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;/v2&lt;/span&gt;
&lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;service&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;api-v2&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin.mycompany.com&lt;/span&gt;
&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&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;/&lt;/span&gt;
&lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;service&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;admin&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Ingress object is just configuration. The actual traffic routing is done by an Ingress controller — a deployment running in your cluster that watches Ingress resources and programs an actual reverse proxy accordingly. Common controllers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;nginx-ingress-controller&lt;/strong&gt;: The most widely deployed. Configures nginx based on Ingress resources. Mature, well-documented, broad feature support via annotations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt;: For EKS, provisions AWS ALBs instead of nginx. Routes HTTP/HTTPS at the ALB layer, with support for target groups, WAF integration, and certificate management via ACM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traefik&lt;/strong&gt;: Auto-discovers Kubernetes services, has a dashboard, good support for middleware (rate limiting, auth, header rewriting).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Istio Gateway&lt;/strong&gt;: Part of the Istio service mesh. More complex but enables traffic management, mTLS, and observability beyond what a simple Ingress controller provides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Ingress spec is intentionally minimal — just host/path rules and backend services. Advanced features (SSL termination, authentication, rate limiting, sticky sessions) are handled via controller-specific annotations. This means Ingress resources are somewhat controller-specific despite being a standard API object.&lt;/p&gt;

&lt;p&gt;For clusters on Kubernetes 1.19+, &lt;code&gt;ingressClassName&lt;/code&gt; replaces the older &lt;code&gt;kubernetes.io/ingress.class&lt;/code&gt; annotation and allows multiple Ingress controllers to coexist in the same cluster, each handling different Ingress objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Policies: restricting traffic between pods
&lt;/h2&gt;

&lt;p&gt;By default, Kubernetes networking is fully permissive. Any pod can reach any other pod, any service, and any external IP. In a shared cluster, this means a compromised pod can attempt to reach databases, other teams' services, or cluster control plane components.&lt;/p&gt;

&lt;p&gt;NetworkPolicy resources define firewall rules for pod-to-pod and pod-to-external traffic:&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;networking.k8s.io/v1&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;NetworkPolicy&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;api-network-policy&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;backend&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;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&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;monitoring&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="na"&gt;egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# allow DNS&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UDP&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;53&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy says: pods labeled &lt;code&gt;app=api&lt;/code&gt; in the &lt;code&gt;backend&lt;/code&gt; namespace accept ingress on port 8080 only from pods labeled &lt;code&gt;app=frontend&lt;/code&gt; or from any pod in namespaces labeled &lt;code&gt;name=monitoring&lt;/code&gt;. They can only egress to pods labeled &lt;code&gt;app=database&lt;/code&gt; on port 5432, plus DNS (port 53 UDP to any namespace).&lt;/p&gt;

&lt;p&gt;Two things to know about NetworkPolicy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NetworkPolicy requires a compatible CNI.&lt;/strong&gt; Not every CNI plugin enforces NetworkPolicy rules. Flannel does not enforce NetworkPolicy without a companion policy controller. Calico, Cilium, and Weave enforce NetworkPolicy natively. If your cluster's CNI doesn't support it, NetworkPolicy objects exist in the API but have no effect — silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default-deny is not automatic.&lt;/strong&gt; A NetworkPolicy that selects a pod switches that pod from the default-allow to the deny-unless-explicitly-allowed model. But you have to write the NetworkPolicy for it to apply. A common pattern is a default-deny policy that selects all pods in a namespace, then explicit allow policies for specific traffic:&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="c1"&gt;# Default deny all ingress and egress for all pods in namespace&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&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;NetworkPolicy&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;default-deny-all&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;backend&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;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common networking problems and how to debug them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pod can't reach a service by DNS name.&lt;/strong&gt; Check that CoreDNS is running: &lt;code&gt;kubectl get pods -n kube-system -l k8s-app=kube-dns&lt;/code&gt;. Check the service exists: &lt;code&gt;kubectl get svc&lt;/code&gt;. Check the Endpoints object has pods: &lt;code&gt;kubectl get endpoints my-service&lt;/code&gt;. If Endpoints is empty, the service's selector isn't matching any pods — check pod labels match the service selector exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service exists but traffic isn't reaching pods.&lt;/strong&gt; Check the Endpoints: &lt;code&gt;kubectl describe endpoints my-service&lt;/code&gt;. If there are endpoints, the problem is between kube-proxy and the pods. Run &lt;code&gt;kubectl exec -it debug-pod -- curl http://:&lt;/code&gt; to check direct pod connectivity. Also check that the pod's &lt;code&gt;containerPort&lt;/code&gt; matches the service's &lt;code&gt;targetPort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingress returns 502.&lt;/strong&gt; The Ingress controller reached the backend service, but the service is returning an error or not listening. Check that the backend service exists and its Endpoints are populated. Check pod logs for errors. A 502 at the Ingress layer almost always means the backend is reachable but failing, not an Ingress configuration problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NetworkPolicy broke everything.&lt;/strong&gt; If you applied a NetworkPolicy and traffic stopped working, check what DNS looks like — often the first symptom is DNS failures because the default-deny policy blocks UDP/53 egress. Make sure your network policy allows egress to the DNS port, or tests will fail mysteriously on service name resolution before you get to the actual policy issue you were trying to configure.&lt;/p&gt;

&lt;p&gt;Kubernetes manifests describe all of these components — pods, services, Ingress, NetworkPolicy — in text form. Visualizing how they connect can be useful when debugging or reviewing changes to a cluster's configuration. &lt;a href="https://dev.to/"&gt;InfraSketch&lt;/a&gt; parses Kubernetes YAML and generates diagrams that show which services expose which pods and how Ingress rules route between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/kubernetes-diagram-generator.html"&gt;Kubernetes Architecture Diagram Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/iac-tool-comparison.html"&gt;Terraform vs CDK vs Pulumi: Choosing Your IaC Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/aws-vpc-architecture-explained.html"&gt;AWS VPC Architecture Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Terraform vs CDK vs Pulumi: Choosing Your Infrastructure-as-Code Tool</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Sun, 07 Jun 2026 00:25:22 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/terraform-vs-cdk-vs-pulumi-choosing-your-infrastructure-as-code-tool-345i</link>
      <guid>https://dev.to/pandey-raghvendra/terraform-vs-cdk-vs-pulumi-choosing-your-infrastructure-as-code-tool-345i</guid>
      <description>&lt;p&gt;The IaC landscape split into two philosophies about a decade ago and hasn't fully resolved the argument since. On one side: declarative configuration languages designed specifically for infrastructure (Terraform HCL, CloudFormation YAML, Bicep). On the other: general-purpose programming languages brought to infrastructure (AWS CDK, Pulumi). Both approaches have won in production at major organizations. Neither is clearly superior.&lt;/p&gt;

&lt;p&gt;This comparison covers Terraform, AWS CDK, and Pulumi in depth — how they work, where they excel, where they struggle, and which makes sense for different team situations. It isn't a beginner introduction to any of these tools; if you're choosing between them for a real project, this assumes you've at least skimmed each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core philosophical difference
&lt;/h2&gt;

&lt;p&gt;Terraform's HCL is a purpose-built configuration language. It's not Turing-complete (no arbitrary loops, no recursion, limited conditionals). This is by design: HashiCorp's position is that infrastructure definitions should be readable, predictable, and safe to generate tooling around. When you read a &lt;code&gt;.tf&lt;/code&gt; file, you can understand what it creates without executing anything.&lt;/p&gt;

&lt;p&gt;CDK and Pulumi take the opposite position: the limitations of configuration languages are a tax on productive engineers. Why invent a domain-specific language when TypeScript already exists? Real programming languages have proper abstractions, test frameworks, package managers, IDE support, and a billion engineers who already know them. Infrastructure should be no different from application code.&lt;/p&gt;

&lt;p&gt;Both positions have merit. The choice between them often comes down to who's writing the infrastructure more than which approach is technically superior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;

&lt;p&gt;Terraform is the default choice for infrastructure-as-code in 2026. It works with every major cloud provider and hundreds of minor ones. The Terraform Registry has thousands of modules — reusable packages for common patterns like VPCs, EKS clusters, and RDS databases. That ecosystem is its most durable advantage.&lt;/p&gt;

&lt;p&gt;HCL syntax is approachable. You describe what you want to exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;count&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cidr_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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;Terraform figures out the execution order based on the references between resources. You don't specify steps — you specify the end state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Terraform struggles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic resource generation&lt;/strong&gt;: &lt;code&gt;count&lt;/code&gt; and &lt;code&gt;for_each&lt;/code&gt; are usable but awkward. Generating 20 similar resources with slight variations requires careful HCL gymnastics. CDK and Pulumi can use a &lt;code&gt;for&lt;/code&gt; loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstraction limitations&lt;/strong&gt;: You can create modules, but you can't do real object-oriented composition. CDK constructs and Pulumi component resources are substantially more powerful for building internal platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: Testing Terraform is harder than it should be. &lt;code&gt;terraform validate&lt;/code&gt; checks syntax; &lt;code&gt;terratest&lt;/code&gt; (Go) or &lt;code&gt;pytest-terraform&lt;/code&gt; can do integration tests, but unit testing logic is awkward because HCL isn't executable without a provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State management complexity&lt;/strong&gt;: As covered in depth in the &lt;a href="///blog/terraform-state-explained.html"&gt;Terraform state guide&lt;/a&gt;, managing state at scale requires careful structure and tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to choose Terraform:&lt;/strong&gt; You need multi-cloud support, your team is a mix of application and platform engineers (not all software engineers), or you're inheriting existing Terraform infrastructure. Also: when you want the broadest hiring pool and the most community resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS CDK
&lt;/h2&gt;

&lt;p&gt;CDK (Cloud Development Kit) is AWS's answer to the "write real code" camp. You write TypeScript, Python, Java, Go, or C# that constructs a tree of CDK constructs. When you run &lt;code&gt;cdk synth&lt;/code&gt;, it compiles to CloudFormation JSON or YAML, which AWS then deploys.&lt;/p&gt;

&lt;p&gt;The synthesis step is important: CDK is an abstraction on top of CloudFormation, not a deployment tool in itself. Under the hood, your resources become CloudFormation stacks with CloudFormation change sets. This means CDK inherits CloudFormation's deployment semantics, including its quirks — stack drift, resource replacement behavior, and the 500-resource-per-stack limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-ec2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-ecs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AppVpc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="na"&gt;maxAzs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;natGateways&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AppCluster&lt;/span&gt;&lt;span class="dl"&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;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;containerInsights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// L2 constructs handle security groups, IAM roles,&lt;/span&gt;
&lt;span class="c1"&gt;// log groups, and other boilerplate automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AppService&lt;/span&gt;&lt;span class="dl"&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;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;desiredCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDK's L2 constructs are its killer feature. An L2 construct is an opinionated abstraction over one or more CloudFormation resources that handles the boilerplate. &lt;code&gt;ecs.FargateService&lt;/code&gt; doesn't just create a &lt;code&gt;AWS::ECS::Service&lt;/code&gt; — it creates the service, the task definition's execution role with the right policies, the security group, and the CloudWatch log group. Getting the same result in raw CloudFormation or Terraform requires 4–6 separate resources and knowing which policies to attach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where CDK struggles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS-only&lt;/strong&gt;: CDK is designed for AWS CloudFormation. Using non-AWS resources requires CDK for Terraform (CDKTF), which is a separate project with its own quirks, or mixing in other tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFormation limits&lt;/strong&gt;: That 500-resource limit per stack bites real production systems. Large applications need careful stack decomposition. CloudFormation deployments also tend to be slower than Terraform — change sets take time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesis surprises&lt;/strong&gt;: A single CDK construct can expand to dozens of CloudFormation resources. If you're used to explicit resource definitions, the synthesized output can be surprising — and the diff on a CDK update is sometimes much larger than expected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDK versioning pain&lt;/strong&gt;: CDK 1.x to 2.x was a painful migration. The framework moves fast and breaking changes happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to choose CDK:&lt;/strong&gt; Your team is primarily software engineers who find HCL limiting, you're AWS-only, and you want L2 construct abstractions to reduce boilerplate. Also: when you're building an internal infrastructure platform that other teams consume as a library — CDK constructs compose like packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulumi
&lt;/h2&gt;

&lt;p&gt;Pulumi is similar in philosophy to CDK (real programming languages for infrastructure) but different in execution. Pulumi doesn't compile to CloudFormation — it has its own deployment engine that calls provider APIs directly, similar to Terraform. It supports TypeScript, Python, Go, C#, Java, and YAML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="na"&gt;cidrBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10.0.0.0/16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;enableDnsHostnames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privateSubnets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;availabilityZones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;az&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Subnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`private-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;az&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="na"&gt;vpcId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;cidrBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`10.0.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.0/24`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;availabilityZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;az&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 the key difference from CDK: &lt;code&gt;new aws.ec2.Vpc(...)&lt;/code&gt; isn't creating a CloudFormation resource. It's registering a resource with the Pulumi engine, which maintains its own state (either in Pulumi Cloud or in a self-managed backend like S3). The deployment runs directly against the AWS API, like Terraform.&lt;/p&gt;

&lt;p&gt;Pulumi's multi-cloud support is genuine — it has providers for AWS, Azure, GCP, Kubernetes, and many others, all within the same program. You can write a TypeScript program that creates an AWS VPC, deploys an Azure SQL database, and configures a Cloudflare DNS record, all in a single Pulumi stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Pulumi struggles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Community and ecosystem size&lt;/strong&gt;: Pulumi's ecosystem is smaller than Terraform's. The Registry has fewer published modules. When you hit an edge case, there are fewer Stack Overflow answers and community resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language complexity can become a liability&lt;/strong&gt;: The same expressiveness that makes Pulumi powerful also means infrastructure code can become complex. A Pulumi program that makes API calls, fetches secrets, and uses complex TypeScript generics is harder to review than equivalent Terraform HCL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State backend choices&lt;/strong&gt;: Pulumi's hosted backend (Pulumi Cloud) is polished but adds a vendor dependency. The self-managed S3 backend works but lacks features like the run history and policy-as-code that Pulumi Cloud provides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to choose Pulumi:&lt;/strong&gt; Your team writes TypeScript or Python daily and finds HCL genuinely limiting. You need multi-cloud in a single program. You're building Kubernetes-native infrastructure where the Pulumi Kubernetes provider is a natural fit alongside AWS/GCP resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Terraform&lt;/th&gt;
&lt;th&gt;AWS CDK&lt;/th&gt;
&lt;th&gt;Pulumi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;HCL (DSL)&lt;/td&gt;
&lt;td&gt;TS, Python, Java, Go, C#&lt;/td&gt;
&lt;td&gt;TS, Python, Go, C#, Java, YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment engine&lt;/td&gt;
&lt;td&gt;Direct provider API&lt;/td&gt;
&lt;td&gt;CloudFormation&lt;/td&gt;
&lt;td&gt;Direct provider API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-cloud&lt;/td&gt;
&lt;td&gt;Yes (native)&lt;/td&gt;
&lt;td&gt;AWS only (CDKTF for others)&lt;/td&gt;
&lt;td&gt;Yes (native)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;Self-managed or Terraform Cloud&lt;/td&gt;
&lt;td&gt;CloudFormation (AWS-managed)&lt;/td&gt;
&lt;td&gt;Self-managed or Pulumi Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abstractions&lt;/td&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;td&gt;Constructs (L1/L2/L3)&lt;/td&gt;
&lt;td&gt;Component resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Limited unit testing&lt;/td&gt;
&lt;td&gt;Jest + CDK assertions&lt;/td&gt;
&lt;td&gt;Jest/pytest + Pulumi mocking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Largest&lt;/td&gt;
&lt;td&gt;Large (AWS-focused)&lt;/td&gt;
&lt;td&gt;Growing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low (for HCL)&lt;/td&gt;
&lt;td&gt;Medium (requires knowing CDK patterns)&lt;/td&gt;
&lt;td&gt;Low (if already know the language)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hybrid approaches
&lt;/h2&gt;

&lt;p&gt;Real production environments often use more than one tool. Some common combinations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform for infrastructure, CDK for application stacks.&lt;/strong&gt; Platform teams manage VPCs, RDS clusters, and shared IAM with Terraform. Application teams deploy their ECS services and Lambda functions with CDK. The CDK stacks reference Terraform outputs via SSM parameters or remote state. This splits ownership cleanly and lets each team use the tool that fits their skillset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform for everything except Kubernetes.&lt;/strong&gt; Terraform creates the EKS cluster. Pulumi or Helm manages Kubernetes resources. Terraform's Kubernetes provider exists but is awkward for complex K8s workloads — the HCL representation of a Kubernetes deployment is verbose and hard to read. Pulumi's Kubernetes provider or plain Helm charts are usually a better fit for the K8s layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDK for core AWS, Terraform for third-party services.&lt;/strong&gt; CDK shines for AWS resources with its L2 constructs. Terraform shines for the 1000+ non-AWS providers (Datadog, PagerDuty, Cloudflare, GitHub, Vault). Many teams use CDK for their AWS infrastructure and Terraform for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration considerations
&lt;/h2&gt;

&lt;p&gt;If you're migrating existing infrastructure to a different IaC tool, the transition cost is often higher than it looks. Consider:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State migration&lt;/strong&gt;: Moving from Terraform to Pulumi or CDK means re-importing every resource or doing a careful blue-green migration. There are tools to convert Terraform state to Pulumi (&lt;code&gt;pulumi convert --from terraform&lt;/code&gt;) but they require significant manual cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team knowledge&lt;/strong&gt;: If your team knows Terraform, migrating to CDK doesn't make everyone immediately productive. The learning curve is real, especially the CDK mental model (constructs, stacks, apps, environments).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial migrations are valid&lt;/strong&gt;: You don't have to pick one tool for everything. New projects can use the new tool. Existing infrastructure can stay where it is. The overhead of two tools is often less than the disruption of a full migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version that no one talks about: OpenTofu
&lt;/h2&gt;

&lt;p&gt;In 2023, HashiCorp changed Terraform's license from MPL to BSL — a non-open-source license that restricts commercial use by competing products. The community responded by forking Terraform at the last open-source version (1.5.x) to create OpenTofu, now maintained by the Linux Foundation.&lt;/p&gt;

&lt;p&gt;OpenTofu is a drop-in replacement for Terraform with the same HCL syntax, provider compatibility, and state format. It diverges from Terraform where HashiCorp has added BSL-licensed features. For most users, OpenTofu and Terraform are functionally identical today. OpenTofu is the right choice if the license change concerns you or if you're vendor-averse; Terraform is the right choice if you want the closest alignment with the HashiCorp ecosystem and Terraform Cloud.&lt;/p&gt;

&lt;p&gt;Whichever IaC tool you use, visualizing what it actually creates helps during both development and review. &lt;a href="https://dev.to/"&gt;InfraSketch&lt;/a&gt; supports Terraform HCL, CDK synthesized JSON, and Pulumi TypeScript/Python — paste your code to see the architecture diagram without deploying anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/terraform-state-explained.html"&gt;Terraform State Explained: What It Is, How It Works, and Why It Breaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-diagram-generator.html"&gt;Terraform Architecture Diagram Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/pulumi-diagram-generator.html"&gt;Pulumi Architecture Diagram Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/cdk-architecture-diagram-generator.html"&gt;CDK Architecture Diagram Generator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>terraform</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Generate IaC Architecture Diagrams Inside Claude Code and Cursor with InfraSketch MCP</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Thu, 14 May 2026 00:51:46 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/generate-iac-architecture-diagrams-inside-claude-code-and-cursor-with-infrasketch-mcp-43be</link>
      <guid>https://dev.to/pandey-raghvendra/generate-iac-architecture-diagrams-inside-claude-code-and-cursor-with-infrasketch-mcp-43be</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Install the InfraSketch MCP server with one command (&lt;code&gt;claude mcp add infrasketch -- npx infrasketch-mcp&lt;/code&gt;), then ask Claude Code to diagram any Terraform, Kubernetes, or Pulumi code without leaving your editor. The diagram opens in your browser, your code never leaves your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The context-switching problem: diagrams while you code
&lt;/h2&gt;

&lt;p&gt;AI editors like Claude Code, Cursor, and Windsurf have gotten surprisingly good at infrastructure work. You can paste a 2,000-line Terraform module into context, ask it to refactor a subnet configuration, and get a working diff in seconds. But ask it to show you an architecture diagram of that same code? Until recently, the answer was "open a browser, paste it somewhere else, render it there."&lt;/p&gt;

&lt;p&gt;That break is annoying in a specific way. It's not just the thirty seconds — it's that you lose the thread. The AI has been building context about your codebase, your constraints, what you're trying to do. When you jump to another tool and come back, you're starting that context fresh.&lt;/p&gt;

&lt;p&gt;The InfraSketch MCP server fixes this by adding diagram generation as a tool your AI assistant can call natively. Two tools: &lt;code&gt;generate_diagram&lt;/code&gt; and &lt;code&gt;detect_iac_format&lt;/code&gt;. Ask Claude "can you show me a diagram of this?" and it calls the tool, gets a URL, and includes it in its response. You never left the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP and why it matters for infrastructure teams
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) is an open standard developed by Anthropic that lets AI assistants communicate with external tools and services through a standardized interface. Instead of an AI assistant being limited to the text in its context window, MCP lets it call out to tools — read files, run searches, query APIs, generate artifacts — and incorporate the results back into the conversation.&lt;/p&gt;

&lt;p&gt;For infrastructure teams the implication is straightforward: your AI assistant can now do things that previously required leaving the editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look up the current state of a Terraform resource in a cloud account&lt;/li&gt;
&lt;li&gt;Check a security scanner for misconfigurations&lt;/li&gt;
&lt;li&gt;Generate an architecture diagram of whatever code it's currently looking at&lt;/li&gt;
&lt;li&gt;Figure out what IaC format a file is before processing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP servers are lightweight processes that run locally alongside your editor. They communicate with the AI assistant over a stdio or SSE transport — there's no cloud intermediary. The InfraSketch MCP server is published as &lt;code&gt;infrasketch-mcp&lt;/code&gt; on npm and requires only Node.js to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing infrasketch-mcp (npx, global, Claude Code one-liner)
&lt;/h2&gt;

&lt;p&gt;The fastest way to install is the Claude Code one-liner, which registers the MCP server in your Claude Code configuration in a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add infrasketch &lt;span class="nt"&gt;--&lt;/span&gt; npx infrasketch-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Claude Code to start the InfraSketch MCP server on demand using npx, which downloads and runs the package without a global install. The server starts fresh each session and exits when Claude Code closes — no persistent daemon.&lt;/p&gt;

&lt;p&gt;If you prefer a global install to avoid npx startup overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; infrasketch-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference it directly in your MCP configuration as &lt;code&gt;infrasketch-mcp&lt;/code&gt; instead of &lt;code&gt;npx infrasketch-mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: Claude Code (claude mcp add command + settings.json method)
&lt;/h2&gt;

&lt;p&gt;After running the &lt;code&gt;claude mcp add&lt;/code&gt; command above, the server is registered automatically. To verify it's registered correctly, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;infrasketch&lt;/code&gt; in the list. If you prefer to configure it manually, or if you're deploying a shared configuration to a team, edit your Claude Code MCP settings file at &lt;code&gt;~/.claude/settings.json&lt;/code&gt; and add the server to the &lt;code&gt;mcpServers&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"infrasketch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"infrasketch-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For project-level configuration (useful when you want to check the MCP config into your repository so the whole team gets it automatically), create or edit &lt;code&gt;.claude/settings.json&lt;/code&gt; in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"infrasketch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"infrasketch-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Generate architecture diagrams from IaC code"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After editing the settings file, restart Claude Code for the changes to take effect. The MCP server will be available in all sessions in that project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: Cursor and Windsurf
&lt;/h2&gt;

&lt;p&gt;Cursor uses the same MCP configuration format. Create or edit &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; (for global configuration) or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"infrasketch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"infrasketch-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cursor, go to &lt;strong&gt;Settings → MCP&lt;/strong&gt; to verify the server appears and shows a green connected status. You may need to click "Reload MCP Servers" after saving the config file.&lt;/p&gt;

&lt;p&gt;Windsurf follows the same pattern. Edit &lt;code&gt;~/.windsurf/mcp.json&lt;/code&gt; with the same configuration block. Once the server is connected, both Cursor's Composer and Windsurf's Cascade will have access to the InfraSketch tools and can call them in response to natural language prompts about your infrastructure code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js required:&lt;/strong&gt; The MCP server requires Node.js 18 or later. Run &lt;code&gt;node --version&lt;/code&gt; to check. If you're using a version manager like nvm or fnm, make sure the active version is 18+ before starting your editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generate_diagram tool in action (with example prompt + example output)
&lt;/h2&gt;

&lt;p&gt;Once the MCP server is connected, you can ask Claude to generate diagrams in natural language. You don't need to know the tool name or its parameters — just describe what you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;Generate&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;diagram&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;this&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&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;"main-vpc"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;
&lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1a"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"igw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
&lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;igw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude will call the &lt;code&gt;generate_diagram&lt;/code&gt; MCP tool with the code and format, and return something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here's your architecture diagram:

**[View Interactive Diagram](https://infrasketch.cloud/#eJyVVdtu2zgQffdXEH...)**

The diagram shows:
- **VPC** (10.0.0.0/16) as the top-level container
- **Public Subnet** (10.0.1.0/24) in us-east-1a inside the VPC
- **Internet Gateway** attached to the VPC
- **Route Table** with a default route (0.0.0.0/0) pointing to the IGW
- **Route Table Association** linking the public subnet to the route table

You can click the link to open an interactive, zoomable diagram in your browser.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram URL is a permanent shareable link. The iframe embed lets you paste the diagram directly into documentation, Notion, Confluence, or any HTML page. The AI assistant also provides a human-readable summary of what the diagram shows, which is useful for accessibility and for quickly confirming the rendering is correct without opening a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detect_iac_format tool
&lt;/h2&gt;

&lt;p&gt;The second tool exposed by the MCP server is &lt;code&gt;detect_iac_format&lt;/code&gt;. This is useful when you have an ambiguous file and want the AI to determine its format before processing it. You can invoke it explicitly:&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="s"&gt;What IaC format is this file?&lt;/span&gt;

&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&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;Deployment&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;nginx-deployment&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&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;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&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;containers&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;nginx&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.21&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool returns the detected format (&lt;code&gt;kubernetes&lt;/code&gt; in this case) along with a confidence score. Claude Code will then use this information to call &lt;code&gt;generate_diagram&lt;/code&gt; with the correct &lt;code&gt;type&lt;/code&gt; parameter, producing an accurate diagram for Kubernetes resources rather than trying to parse it as Terraform or CloudFormation.&lt;/p&gt;

&lt;p&gt;In practice, the format detection runs automatically when you ask for a diagram — you rarely need to invoke it explicitly. It's most useful in agentic workflows where code is being passed programmatically and the format isn't known in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real workflow: reviewing a Terraform PR with Claude + InfraSketch
&lt;/h2&gt;

&lt;p&gt;Concrete example. A teammate opens a PR adding an ECS cluster with autoscaling to an existing VPC. You're reviewing it in Claude Code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask Claude to show you what changed: "What's different in the terraform/ directory compared to main?" It reads the diff and summarizes.&lt;/li&gt;
&lt;li&gt;"Generate a diagram of the full terraform/environments/prod/ directory so I can see how the ECS cluster fits in." Claude calls &lt;code&gt;generate_diagram&lt;/code&gt;, gets a URL back.&lt;/li&gt;
&lt;li&gt;Click the URL. You get an interactive, zoomable diagram — ECS cluster, task definitions, ALB, target groups, VPC subnets, security groups, all in one view.&lt;/li&gt;
&lt;li&gt;Back in Claude: "The ECS tasks are in the public subnet. Intentional?" Claude already has all the code in context and can answer immediately.&lt;/li&gt;
&lt;li&gt;Your review comment is specific — "the ECS tasks should be in the private subnet per our networking standards, the security group on line 43 is too permissive" — not just "looks good" or a vague worry about networking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reading HCL line by line is fine for small changes. For anything spanning multiple resources and subnets, the diagram gets you oriented in about ten seconds instead of ten minutes.&lt;/p&gt;

&lt;p&gt;For teams that want this diagram to appear automatically on every PR without any manual step, the &lt;a href="///blog/github-action-terraform-diagram.html"&gt;GitHub Action (&lt;code&gt;pandey-raghvendra/infrasketch@v4&lt;/code&gt;)&lt;/a&gt; posts the diagram as a PR comment whenever Terraform files change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the URL is generated (no upload, privacy-first, works offline)
&lt;/h2&gt;

&lt;p&gt;The MCP server uses the same URL-encoding approach as the CLI and browser tool. When &lt;code&gt;generate_diagram&lt;/code&gt; is called:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code is serialized into a JSON envelope with the detected format&lt;/li&gt;
&lt;li&gt;The JSON is compressed with gzip and base64-encoded&lt;/li&gt;
&lt;li&gt;The encoded string is appended as a URL fragment to &lt;code&gt;https://infrasketch.cloud/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The resulting URL is returned to the AI assistant, which includes it in its response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MCP server process runs entirely on your machine. It never makes outbound HTTP requests to InfraSketch servers. The URL it generates encodes your code in the fragment, which — by the HTTP specification — is never transmitted to a server. When you open the URL in a browser, the InfraSketch web app decodes and renders the diagram entirely client-side.&lt;/p&gt;

&lt;p&gt;This means the tool works in air-gapped environments (as long as your browser can load the web app), needs no API keys, stores nothing server-side, and leaves no audit trail. If your team has strict data residency requirements, that matters. Cloud-hosted diagramming APIs typically process your code on their servers — this one doesn't, by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining MCP with CLI and embed for full workflow
&lt;/h2&gt;

&lt;p&gt;The MCP server, CLI, and embed component are designed to complement each other across different parts of the development lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP server&lt;/strong&gt; — Use during active coding and code review inside your AI editor. Get diagrams inline without leaving your editor or conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI (npx infrasketch)&lt;/strong&gt; — Use for quick local checks, scripting, and CI pipelines. See the &lt;a href="///blog/infrasketch-cli-terraform-diagram.html"&gt;CLI guide&lt;/a&gt; for full details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Action&lt;/strong&gt; — Use for automated PR comments. Zero friction for reviewers — the diagram appears without anyone running a command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed web component&lt;/strong&gt; — Use for documentation, runbooks, and internal wikis. Keep diagrams live and synchronized with the source code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, a team that's using all four ends up with: MCP for daily coding and reviews, CLI for pre-commit sanity checks and one-off scripting, the GitHub Action for automated PR comments, and embed for the internal docs wiki. Every diagram comes from the same source of truth. Nothing ever gets uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supported formats and limitations
&lt;/h2&gt;

&lt;p&gt;The MCP server supports the same eight IaC formats as the web tool and CLI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Auto-detected&lt;/th&gt;
&lt;th&gt;Multi-file support&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;terraform&lt;/td&gt;
&lt;td&gt;Yes (.tf extension)&lt;/td&gt;
&lt;td&gt;Yes (concatenated)&lt;/td&gt;
&lt;td&gt;Best coverage of all formats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes&lt;/td&gt;
&lt;td&gt;Yes (apiVersion field)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Helm chart values not rendered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pulumi&lt;/td&gt;
&lt;td&gt;Partial (Pulumi.yaml)&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Heuristic extraction from source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudformation&lt;/td&gt;
&lt;td&gt;Yes (Resources key)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Nested stacks shown as references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Works best with synthesized output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bicep&lt;/td&gt;
&lt;td&gt;Yes (.bicep extension)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Modules shown as references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terragrunt&lt;/td&gt;
&lt;td&gt;Yes (terragrunt.hcl)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Module dependency graph included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker-compose&lt;/td&gt;
&lt;td&gt;Yes (services key)&lt;/td&gt;
&lt;td&gt;Single file&lt;/td&gt;
&lt;td&gt;Networks and volumes visualized&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The main limitation to be aware of is code size. The URL fragment approach has a practical ceiling around 2 MB of raw IaC content before browser URL limits become a concern. For most individual modules and services this is not an issue, but for very large monorepos you should point the tool at a specific subdirectory rather than the entire repository root.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the MCP server require any API key or account?
&lt;/h3&gt;

&lt;p&gt;No. Completely free, no authentication. It runs locally and talks to your AI editor over stdio. No InfraSketch account, no key, no usage limits. Just Node.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does my Terraform code get sent to InfraSketch servers?
&lt;/h3&gt;

&lt;p&gt;No. The MCP server encodes your code into a URL fragment. Fragments are never transmitted in HTTP requests — that's part of the HTTP spec, not something special we're doing. The InfraSketch web app decodes and renders everything in the browser. The InfraSketch server only ever serves the static HTML/JS/CSS files, same as any website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the MCP server with VS Code or other editors?
&lt;/h3&gt;

&lt;p&gt;Anything that supports Model Context Protocol over stdio will work. Claude Code, Cursor, and Windsurf are the most common. VS Code has MCP support in some extensions. For editors that don't support MCP at all, the CLI and browser tool still cover most of the same use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the diagram looks wrong?
&lt;/h3&gt;

&lt;p&gt;The MCP server passes your code to the renderer exactly as-is — no modification, no interpretation. So if the diagram is wrong, it's the renderer's parsing that's off, not how the AI called the tool. Open the interactive diagram in your browser, check what's there, and if you spot a bug file it on GitHub with the example code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the MCP server in a team setting with a shared configuration?
&lt;/h3&gt;

&lt;p&gt;Yes, and this is actually the easiest setup. Add the MCP config to &lt;code&gt;.claude/settings.json&lt;/code&gt; (Claude Code) or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; (Cursor) at the project root and check it into your repo. Anyone who clones gets it automatically. Since it uses &lt;code&gt;npx&lt;/code&gt;, nobody needs to install anything manually.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Diagram your infrastructure without leaving your editor Add the InfraSketch MCP server to Claude Code in one command, or try the browser tool right now — no account needed. &lt;a href="https://dev.to/"&gt;Try InfraSketch Free →&lt;/a&gt;
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/infrasketch-cli-terraform-diagram.html"&gt;InfraSketch CLI — Visualize Any IaC Repo From Your Terminal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/embed-iac-diagram-web-component.html"&gt;Embed Live IaC Diagrams on Any Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-diagram-generator.html"&gt;Terraform Diagram Generator — Visualize HCL Architecture Instantly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Visualize Terraform, Kubernetes, and Pulumi from the Terminal with npx infrasketch</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Thu, 14 May 2026 00:51:43 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/visualize-terraform-kubernetes-and-pulumi-from-the-terminal-with-npx-infrasketch-1l4k</link>
      <guid>https://dev.to/pandey-raghvendra/visualize-terraform-kubernetes-and-pulumi-from-the-terminal-with-npx-infrasketch-1l4k</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Run &lt;code&gt;npx infrasketch .&lt;/code&gt; in any IaC repository to instantly open an interactive architecture diagram in your browser. No login, no install, no file upload. Works with Terraform, Kubernetes, Pulumi, CloudFormation, CDK, Bicep, Terragrunt, and Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The copy-paste problem with IaC diagrams
&lt;/h2&gt;

&lt;p&gt;You finish a Terraform module, open Lucidchart or draw.io, manually drag in an EC2 box, an RDS cylinder, an ALB arrow — 45 minutes recreating what the code already describes. Two weeks later the code changes, the diagram doesn't, and you've got a lie pinned to your Confluence page. I've seen this on every team I've worked with.&lt;/p&gt;

&lt;p&gt;The browser-based InfraSketch tool at &lt;a href="https://dev.to/"&gt;infrasketch.cloud&lt;/a&gt; cuts most of that: paste your HCL, YAML, or JSON and get an interactive diagram instantly, no account required. But there was still friction. You had to leave your terminal, copy the file contents, navigate to a website. For anyone living in the command line, that adds up over a day.&lt;/p&gt;

&lt;p&gt;The CLI removes that last step. Point it at a file, a directory, or a raw GitHub URL, and it generates the diagram and opens your browser — usually in under two seconds. In CI where there's no browser to open, &lt;code&gt;--no-open&lt;/code&gt; prints the shareable URL to stdout so you can pipe it wherever you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What npx infrasketch does (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;The CLI is a thin Node.js wrapper published to npm as &lt;code&gt;infrasketch&lt;/code&gt;. When you run it, it reads your IaC file or directory, serializes the content into a base64-encoded JSON fragment, appends that fragment to the InfraSketch web app URL, and opens the result in your default browser.&lt;/p&gt;

&lt;p&gt;What it &lt;em&gt;doesn't&lt;/em&gt; do matters more than you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your code never leaves your machine — no upload, no server processing.&lt;/li&gt;
&lt;li&gt;No account, no API key, nothing to sign up for.&lt;/li&gt;
&lt;li&gt;It doesn't touch your IaC files. Read-only.&lt;/li&gt;
&lt;li&gt;No background process. It runs, opens the browser, and exits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diagram is rendered entirely in the browser using the URL fragment. Browsers don't send the hash to the server — they only transmit the path and query string. So when you open &lt;code&gt;https://infrasketch.cloud/#eJy...&lt;/code&gt;, the server gets a plain &lt;code&gt;GET /&lt;/code&gt; with no idea what's in the fragment. That's not a hack; it's just how HTTP works, and we're leaning into it deliberately. It also means the tool works in air-gapped environments as long as the browser can load the InfraSketch page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero-install usage: your first diagram in 10 seconds
&lt;/h2&gt;

&lt;p&gt;Because the CLI is on npm, you can run it immediately with &lt;code&gt;npx&lt;/code&gt; without installing anything globally. Navigate to any directory that contains IaC files and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The CLI will scan the current directory, detect the format automatically, and open your browser. If you have a mixed repository with, say, a &lt;code&gt;terraform/&lt;/code&gt; subdirectory and a &lt;code&gt;k8s/&lt;/code&gt; subdirectory, run it from the root — it will pick the dominant format or let you point it at a subdirectory.&lt;/p&gt;

&lt;p&gt;If you prefer a permanent global install to skip the npx overhead on every invocation, add it to your PATH once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; infrasketch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;infrasketch .&lt;/code&gt; works from any directory, just like &lt;code&gt;terraform&lt;/code&gt; or &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single file mode
&lt;/h2&gt;

&lt;p&gt;When you want to diagram a specific file rather than an entire directory, pass the file path directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch main.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single file mode is useful when you're working on a large module and want to check just the resources you're currently editing. The CLI reads only that file, so it's faster for large repositories where a full directory scan would pull in many files you don't care about right now.&lt;/p&gt;

&lt;p&gt;The format is auto-detected from the file extension and content. &lt;code&gt;.tf&lt;/code&gt; files are parsed as Terraform HCL, &lt;code&gt;.yaml&lt;/code&gt; and &lt;code&gt;.yml&lt;/code&gt; files trigger heuristic detection to distinguish between Kubernetes manifests, Docker Compose files, and CloudFormation templates. For ambiguous cases you can override with the &lt;code&gt;--type&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch template.yaml &lt;span class="nt"&gt;--type&lt;/span&gt; cloudformation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported values for &lt;code&gt;--type&lt;/code&gt; are: &lt;code&gt;terraform&lt;/code&gt;, &lt;code&gt;kubernetes&lt;/code&gt;, &lt;code&gt;pulumi&lt;/code&gt;, &lt;code&gt;cloudformation&lt;/code&gt;, &lt;code&gt;cdk&lt;/code&gt;, &lt;code&gt;bicep&lt;/code&gt;, &lt;code&gt;terragrunt&lt;/code&gt;, &lt;code&gt;docker-compose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directory scan mode — multi-file Terraform projects
&lt;/h2&gt;

&lt;p&gt;Real Terraform projects rarely live in a single file. A production module typically spans &lt;code&gt;main.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, &lt;code&gt;outputs.tf&lt;/code&gt;, &lt;code&gt;locals.tf&lt;/code&gt;, and any number of resource-specific files. Directory scan mode reads all of them and stitches them into one diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch ./terraform/environments/prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI recursively finds all &lt;code&gt;.tf&lt;/code&gt; files under the given path, concatenates their contents, and feeds the combined HCL to the diagram renderer. This gives you a complete picture of the environment rather than a partial view of whichever file you happened to open first.&lt;/p&gt;

&lt;p&gt;For Terragrunt projects where each module lives in its own subdirectory with a &lt;code&gt;terragrunt.hcl&lt;/code&gt;, point the CLI at the root of the deployment tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch ./live/prod/ &lt;span class="nt"&gt;--type&lt;/span&gt; terragrunt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will discover all &lt;code&gt;terragrunt.hcl&lt;/code&gt; files and combine the dependency graph across modules, giving you visibility into how modules reference each other — something that's notoriously hard to visualize manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; For very large Terraform codebases (hundreds of files), consider running the CLI against a specific module subdirectory rather than the repository root. The URL fragment has a practical size limit around 2 MB of raw content, which covers most real-world modules comfortably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes and Pulumi projects
&lt;/h2&gt;

&lt;p&gt;Kubernetes works the same way. Point it at a directory of YAML files and it picks up Deployments, Services, Ingresses, ConfigMaps, StatefulSets, DaemonSets — and renders how they connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch ./k8s/ &lt;span class="nt"&gt;--type&lt;/span&gt; kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--type&lt;/code&gt; flag is optional here if your YAML files have the standard &lt;code&gt;apiVersion&lt;/code&gt; and &lt;code&gt;kind&lt;/code&gt; fields — the auto-detector recognizes Kubernetes manifests reliably. But if you have a directory that mixes Kubernetes YAML with other YAML (for example, a Helm chart with both templates and values files), being explicit avoids ambiguity.&lt;/p&gt;

&lt;p&gt;For more detail on the Kubernetes diagram format and what relationships are visualized, see the &lt;a href="///blog/kubernetes-diagram-generator.html"&gt;Kubernetes diagram generator guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pulumi projects work similarly. Navigate to a Pulumi project directory (the one containing &lt;code&gt;Pulumi.yaml&lt;/code&gt;) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; pulumi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI reads the Pulumi program source — TypeScript, Python, Go, or YAML — and extracts resource declarations. Because Pulumi programs are general-purpose code rather than declarative config, the extraction is necessarily heuristic: it looks for &lt;code&gt;new aws.ec2.Instance()&lt;/code&gt;-style patterns in TypeScript/Python and equivalent patterns in other languages. Coverage is good for standard resource declarations but may miss resources created inside complex loops or conditional logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote GitHub raw URL mode
&lt;/h2&gt;

&lt;p&gt;You don't have to clone a repository to diagram it. Pass any raw GitHub URL (or any other URL that returns plain text) and the CLI will fetch it, parse it, and open the diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/main/examples/two-tier/main.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mode is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewing open-source Terraform modules before adding them as dependencies&lt;/li&gt;
&lt;li&gt;Quickly understanding the architecture of a reference implementation you found&lt;/li&gt;
&lt;li&gt;Sharing a diagram of a public repository without cloning it locally&lt;/li&gt;
&lt;li&gt;Generating diagrams in documentation pipelines where the source lives in a separate repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The URL is fetched by the CLI process on your machine (not by the browser or any server), parsed locally, and then encoded into the diagram URL. Your network request goes to GitHub's raw content CDN, not to InfraSketch servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD pipeline integration (--no-open flag)
&lt;/h2&gt;

&lt;p&gt;In a headless CI environment there's no browser to open, so the default behavior would hang or error. The &lt;code&gt;--no-open&lt;/code&gt; flag suppresses browser launch and instead prints the diagram URL to stdout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrasketch main.tf &lt;span class="nt"&gt;--no-open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This emits a single line like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://infrasketch.cloud/#eJyVVdtu2zgQ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can capture this URL and use it anywhere: a PR comment, a Slack notification, a build artifact, a release note. Here's a complete GitHub Actions workflow that posts the diagram URL as a PR comment whenever Terraform files change:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Diagram&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;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.tf'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.tfvars'&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;diagram&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;Setup Node.js&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/setup-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&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;Generate diagram URL&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diagram&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="err"&gt;U&lt;/span&gt;&lt;span class="s"&gt;RL=$(npx infrasketch . --no-open)&lt;/span&gt;
&lt;span class="err"&gt;e&lt;/span&gt;&lt;span class="s"&gt;cho "url=$URL" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt; name: Post diagram comment&lt;/span&gt;
&lt;span class="err"&gt;u&lt;/span&gt;&lt;span class="s"&gt;ses: actions/github-script@v7&lt;/span&gt;
&lt;span class="err"&gt;w&lt;/span&gt;&lt;span class="s"&gt;ith:&lt;/span&gt;
&lt;span class="err"&gt;s&lt;/span&gt;&lt;span class="s"&gt;cript: |&lt;/span&gt;
&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="s"&gt;onst url = '${{ steps.diagram.outputs.url }}';&lt;/span&gt;
&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="s"&gt;wait github.rest.issues.createComment({&lt;/span&gt;
&lt;span class="err"&gt;o&lt;/span&gt;&lt;span class="s"&gt;wner: context.repo.owner,&lt;/span&gt;
&lt;span class="err"&gt;r&lt;/span&gt;&lt;span class="s"&gt;epo: context.repo.repo,&lt;/span&gt;
&lt;span class="err"&gt;i&lt;/span&gt;&lt;span class="s"&gt;ssue_number: context.issue.number,&lt;/span&gt;
&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="s"&gt;ody: `## Architecture Diagram\n\n[View interactive diagram](${url})\n\n&amp;gt; Generated by InfraSketch CLI`&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather use the purpose-built GitHub Action instead of the raw CLI in CI, see the &lt;a href="///blog/github-action-terraform-diagram.html"&gt;GitHub Action guide&lt;/a&gt; — it handles the comment posting and diff detection automatically with less boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood (URL encoding, no upload, privacy)
&lt;/h2&gt;

&lt;p&gt;If you're going to run a tool against production Terraform code, you should know what it actually does. Here's the full flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads file contents from disk (or fetches from URL). No preprocessing — the raw content goes through as-is, including variable references and module calls.&lt;/li&gt;
&lt;li&gt;Wraps the content in a JSON envelope: &lt;code&gt;{"type":"terraform","code":"..."}&lt;/code&gt;. Multi-file mode concatenates everything into the &lt;code&gt;code&lt;/code&gt; field with separator comments between files.&lt;/li&gt;
&lt;li&gt;Gzip-compresses the JSON and base64-encodes the result into a compact ASCII string.&lt;/li&gt;
&lt;li&gt;Appends the encoded string to the InfraSketch URL as a hash fragment: &lt;code&gt;https://infrasketch.cloud/#&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Opens that URL in your default browser (&lt;code&gt;open&lt;/code&gt; on macOS, &lt;code&gt;xdg-open&lt;/code&gt; on Linux, &lt;code&gt;start&lt;/code&gt; on Windows). The web app decodes the fragment entirely client-side.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key detail is step 5. The URL fragment — everything after the &lt;code&gt;#&lt;/code&gt; — is never transmitted in HTTP requests. When your browser loads &lt;code&gt;https://infrasketch.cloud/#eJy...&lt;/code&gt;, the actual HTTP request to the server is just &lt;code&gt;GET /&lt;/code&gt;. No server ever sees your infrastructure code. That's a browser standard, not something we're doing specially — we're just building on top of it.&lt;/p&gt;

&lt;p&gt;This also means the diagram URL is a permanent, shareable link with no expiry. The entire diagram state is encoded in the URL itself — there's no server-side session or storage. You can bookmark it, share it in Slack, or save it in documentation and it will work forever, as long as the InfraSketch web app is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining CLI with GitHub Action for full automation
&lt;/h2&gt;

&lt;p&gt;The CLI is for you while you're coding. The GitHub Action is for your reviewers when they're not.&lt;/p&gt;

&lt;p&gt;Practically, the workflow goes: you write a new Terraform module for a VPC peering connection, run &lt;code&gt;npx infrasketch .&lt;/code&gt; locally to make sure the diagram looks right (subnets, route tables, peering connections all there), then open a PR. The &lt;a href="///blog/github-action-terraform-diagram.html"&gt;GitHub Action (&lt;code&gt;pandey-raghvendra/infrasketch@v4&lt;/code&gt;)&lt;/a&gt; fires automatically and posts the diagram as a comment. Reviewers click the link, see the architecture interactively, no tooling required on their end.&lt;/p&gt;

&lt;p&gt;The CLI is fast and zero-ceremony — just a command. The Action is automated and requires no one to remember to run anything. They cover different moments in the same workflow, not the same moment twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table: CLI vs browser vs GitHub Action vs MCP
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Install needed&lt;/th&gt;
&lt;th&gt;Works in CI&lt;/th&gt;
&lt;th&gt;Opens browser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Quick one-off diagrams, paste-and-view&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Already in browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI (npx infrasketch)&lt;/td&gt;
&lt;td&gt;Local dev, scripting, remote URLs&lt;/td&gt;
&lt;td&gt;Node.js (npx auto-installs)&lt;/td&gt;
&lt;td&gt;Yes (--no-open)&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Action&lt;/td&gt;
&lt;td&gt;Automated PR comments on IaC changes&lt;/td&gt;
&lt;td&gt;None (uses Action)&lt;/td&gt;
&lt;td&gt;Yes (built for it)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP Server&lt;/td&gt;
&lt;td&gt;AI-assisted coding in Claude Code / Cursor&lt;/td&gt;
&lt;td&gt;Node.js (npx auto-installs)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (via AI prompt)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does npx infrasketch work without internet access?
&lt;/h3&gt;

&lt;p&gt;First run via npx needs to download the package, so yes, internet required there. After that, &lt;code&gt;npm install -g infrasketch&lt;/code&gt; once and it runs offline. The generated URL needs a browser to load the InfraSketch web app — so some internet to load the page initially, though your IaC code never gets uploaded to any server regardless.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens with .tfvars files that contain secrets?
&lt;/h3&gt;

&lt;p&gt;The code goes into the URL fragment, which is never sent to a server. But the fragment does show up in browser history and in any URL you share. So treat the URL like you'd treat the source code — don't paste it in a public Slack channel if the file has real credentials in it. Stick to &lt;code&gt;main.tf&lt;/code&gt; or module directories for diagramming; leave the &lt;code&gt;.tfvars&lt;/code&gt; with the actual secrets out of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the CLI with Terraform workspaces?
&lt;/h3&gt;

&lt;p&gt;Yes, workspaces only affect state — the HCL source files are the same regardless of which workspace you're on. &lt;code&gt;npx infrasketch .&lt;/code&gt; works normally. If you have workspace-specific variable files like &lt;code&gt;prod.tfvars&lt;/code&gt;, you can point the CLI at them directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How large can the IaC file be before the URL gets too long?
&lt;/h3&gt;

&lt;p&gt;HCL compresses well — gzip typically gets 5-10x reduction before base64, so a 500 KB Terraform project ends up under 100 KB in the URL. In practice, URL length limits never bite you with normal module-sized codebases. If you're running against an entire monorepo root with hundreds of files, run it against a subdirectory instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I get the diagram URL without opening the browser, even locally?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;--no-open&lt;/code&gt; works anywhere, not just CI. &lt;code&gt;npx infrasketch main.tf --no-open&lt;/code&gt; on your laptop prints the URL to stdout without touching the browser. Pipe it to &lt;code&gt;pbcopy&lt;/code&gt;, drop it in a script, whatever you need.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Diagram your infrastructure in 10 seconds Run &lt;code&gt;npx infrasketch .&lt;/code&gt; in your IaC repo right now — or try the browser tool directly with no install required. &lt;a href="https://dev.to/"&gt;Try InfraSketch Free →&lt;/a&gt;
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/infrasketch-mcp-server-claude-code-cursor.html"&gt;InfraSketch MCP Server — Architecture Diagrams in Claude Code, Cursor &amp;amp; Windsurf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/github-action-terraform-diagram.html"&gt;Auto-Post Architecture Diagrams on Every IaC Pull Request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-diagram-generator.html"&gt;Terraform Diagram Generator — Visualize HCL Architecture Instantly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>How to Embed a Live IaC Architecture Diagram on Any Website with &amp;lt;infra-sketch&amp;gt;</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Thu, 14 May 2026 00:28:11 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/how-to-embed-a-live-iac-architecture-diagram-on-any-website-with-ltinfra-sketchgt-2dfh</link>
      <guid>https://dev.to/pandey-raghvendra/how-to-embed-a-live-iac-architecture-diagram-on-any-website-with-ltinfra-sketchgt-2dfh</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Add one script tag to your page, then use `` or inline your IaC code between the tags. The diagram renders interactively, updates when you update the source, and never sends your code to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why living diagrams beat static images in docs
&lt;/h2&gt;

&lt;p&gt;There's a specific kind of documentation rot that hits infrastructure teams hard: the architecture diagram that was accurate when someone drew it, is now pinned to a Confluence page, and describes an environment that no longer exists. Someone added an RDS read replica. Someone else swapped EC2 for ECS Fargate. The diagram stayed put.&lt;/p&gt;

&lt;p&gt;Static diagram images — PNGs and SVGs exported from draw.io or Lucidchart — have a structural problem. Keeping them current requires someone to remember to update them, have the original tool open, export a new image, and re-upload it. That chain breaks constantly in practice. Not because people are careless; they're just busy.&lt;/p&gt;

&lt;p&gt;A living diagram solves this by rendering from the actual IaC file every time the page loads. When the Terraform changes, the diagram changes. No export, no re-upload, no manual step. It's accurate because it's always generated from the current code, not from a snapshot.&lt;/p&gt;

&lt;p&gt;The InfraSketch embed component makes this work on any website, docs platform, or wiki that accepts custom HTML: one script tag, then drop the `` element wherever you want the diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to embed: iframe vs web component
&lt;/h2&gt;

&lt;p&gt;Two ways to embed, and which one you use depends on your platform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The iframe approach&lt;/strong&gt; works everywhere — Confluence, Notion, GitHub Pages, internal wikis, anything that allows an HTML iframe. You generate a diagram URL first (browser tool, CLI, or MCP server), then paste the iframe tag. It's a bit more manual but totally reliable across any platform, including those that restrict JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The web component&lt;/strong&gt; is cleaner for HTML-based sites. One script tag in the head, then use `` anywhere on your page — point it at a source URL or write IaC code inline between the tags. The component fetches, detects the format, and renders. For docs sites built with MkDocs, Docusaurus, Hugo, or Jekyll, this is the approach I'd recommend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the iframe embed code (from the app)
&lt;/h2&gt;

&lt;p&gt;If you have an existing diagram open in the InfraSketch web app, the fastest way to get embed code is the share button. The app generates both an iframe snippet and a direct link:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Paste this into any HTML page or any platform with an HTML/embed block. The iframe is fully self-contained — the diagram state is in the URL, so there are no external dependencies beyond the InfraSketch web app itself.&lt;/p&gt;

&lt;p&gt;For platforms that strip the &lt;code&gt;src&lt;/code&gt; attribute from iframes for security reasons (some wiki platforms and CMS editors do this), see the CORS considerations section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infra-sketch web component: one-line setup
&lt;/h2&gt;

&lt;p&gt;The web component is loaded with a single script tag that you add once to your page's &lt;code&gt; or just before &lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;After loading, the `` custom element is registered and available anywhere on the page. It renders as a responsive iframe-like widget with pan and zoom support, the same interactive experience as the full web app.&lt;/p&gt;

&lt;p&gt;The script is small, loads from a CDN, and is cached aggressively. Including it on a documentation page adds negligible overhead — the component only fetches and renders when an `` element is actually present in the DOM, so unused script inclusions don't trigger any work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedding by URL: the src= attribute
&lt;/h2&gt;

&lt;p&gt;The simplest way to embed a diagram is to point the component at a URL that returns IaC code. The component fetches the URL, detects the format, and renders the diagram:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;When the page loads, the component fetches the &lt;code&gt;src&lt;/code&gt; URL, reads the content, auto-detects the IaC format, and renders the interactive diagram. If the file changes in GitHub, the diagram on your documentation page changes the next time someone loads it — without any manual intervention.&lt;/p&gt;

&lt;p&gt;You can also pass an explicit &lt;code&gt;type&lt;/code&gt; attribute to skip format detection and ensure correct rendering for ambiguous files:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;height&lt;/code&gt; attribute sets the diagram height in pixels. If omitted, a default of 480px is used. Width is always 100% of the container element, making the component responsive to its parent layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedding inline code
&lt;/h2&gt;

&lt;p&gt;For documentation where you want to show a specific, curated snippet rather than an entire file, write the IaC code directly between the `` tags:&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;apps/v1&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;Deployment&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;api-server&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;production&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-server&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&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;containers&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;api&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org/api:latest&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&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;Service&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;api-service&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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-server&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When no &lt;code&gt;src&lt;/code&gt; attribute is provided, the component uses the element's text content as the IaC code. The &lt;code&gt;type&lt;/code&gt; attribute is recommended for inline code since there's no file extension to auto-detect from. The content is treated as plain text — HTML entities inside the element are decoded before parsing, so you can use `&lt;br&gt;
&lt;strong&gt;Tip:&lt;/strong&gt; For documentation sites that process Markdown, wrap inline IaC code in a raw HTML block to prevent the Markdown parser from mangling the whitespace or escaping characters inside the element.&lt;/p&gt;

&lt;h2&gt;
  
  
  All supported attributes (src, type, height, width)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;src&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;URL of the IaC file to fetch and render. Omit to use inline content.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;auto-detect&lt;/td&gt;
&lt;td&gt;IaC format: terraform, kubernetes, pulumi, cloudformation, cdk, bicep, terragrunt, docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;480&lt;/td&gt;
&lt;td&gt;Height of the diagram widget in pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;Width of the diagram widget. Accepts px or % values.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The component also exposes a custom event, &lt;code&gt;infra-sketch-load&lt;/code&gt;, that fires when the diagram has finished rendering. You can listen to it for analytics, lazy-load triggers, or to show/hide a loading skeleton:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
document.querySelector('infra-sketch').addEventListener('infra-sketch-load', (e) =&amp;gt; {&lt;br&gt;
console.log('Diagram rendered:', e.detail.resourceCount, 'resources');&lt;br&gt;
});&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Real examples: GitHub Pages docs, Backstage, Confluence, Notion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Pages / Jekyll / Hugo / MkDocs
&lt;/h3&gt;

&lt;p&gt;For static site generators, add the embed script to your theme's base template and use the component in any page. For MkDocs with the Material theme, add the script to &lt;code&gt;overrides/main.html&lt;/code&gt;. For Jekyll, add it to &lt;code&gt;_layouts/default.html&lt;/code&gt;. Then use the component in any Markdown file within a raw HTML block:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`markdown&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Architecture
&lt;/h2&gt;

&lt;p&gt;The following diagram shows the production VPC layout:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Backstage TechDocs
&lt;/h3&gt;

&lt;p&gt;Backstage TechDocs is built on MkDocs and renders Markdown for service catalogs. The same pattern applies — add the script to the TechDocs theme override and embed diagrams inline in your service's &lt;code&gt;docs/&lt;/code&gt; directory. Because TechDocs serves content from the same origin as Backstage, there are no CORS restrictions when fetching from GitHub raw URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confluence
&lt;/h3&gt;

&lt;p&gt;Confluence's HTML macro lets you paste raw HTML including script tags. Create an HTML macro block on your page, paste in the embed script tag and the &lt;code&gt;&lt;/code&gt; element. Alternatively, use the iframe approach — generate the iframe URL from the InfraSketch app and paste it using Confluence's iframe macro. The iframe approach is more reliable across Confluence versions since some restrict JavaScript in HTML macros by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notion
&lt;/h3&gt;

&lt;p&gt;Notion doesn't support arbitrary HTML or script tags in pages, but it does have an Embed block that accepts iframe URLs. Generate the diagram URL from the InfraSketch app (or via the CLI with &lt;code&gt;--no-open&lt;/code&gt;), then use Notion's &lt;code&gt;/embed&lt;/code&gt; command and paste the URL. Notion will render it as an embedded iframe. The result is an interactive diagram inside your Notion page that anyone with page access can pan and zoom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading from GitHub raw URLs — the recommended pattern
&lt;/h2&gt;

&lt;p&gt;Pointing the &lt;code&gt;src&lt;/code&gt; attribute at a GitHub raw URL is the most maintainable pattern for living diagrams. The URL format is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;plaintext&lt;br&gt;
https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`plaintext&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;main.tf&lt;/code&gt; is updated and merged, the next page load will show the updated diagram. No documentation update required. No diagram export needed. The diagram is always synchronized with the code in your default branch.&lt;/p&gt;

&lt;p&gt;For branch-specific documentation — for example, showing the architecture as it will look after a specific feature branch is merged — use the branch name in the URL:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`plaintext&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is particularly useful when combined with the &lt;a href="///blog/github-action-terraform-diagram.html"&gt;GitHub Action&lt;/a&gt;, which posts a diagram to the PR. You can also link to branch-specific documentation that shows what the architecture will look like after the PR is merged, and the diagram will automatically become the "post-merge" diagram once the branch is merged to main.&lt;/p&gt;

&lt;p&gt;For private repositories, GitHub raw URLs require authentication. In that case, you can either use the inline code approach (copy the relevant code into your documentation page directly) or host your own IaC files on an authenticated endpoint and point &lt;code&gt;src&lt;/code&gt; at that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error handling and CORS considerations
&lt;/h2&gt;

&lt;p&gt;When using the &lt;code&gt;src&lt;/code&gt; attribute, the web component fetches the URL from the browser using the Fetch API. This means the target URL must be accessible from the user's browser and must allow cross-origin requests via CORS headers.&lt;/p&gt;

&lt;p&gt;GitHub raw URLs (&lt;code&gt;raw.githubusercontent.com&lt;/code&gt;) are served with permissive CORS headers — they allow requests from any origin, so they work out of the box. Most public file hosting services and CDNs also support CORS for public content.&lt;/p&gt;

&lt;p&gt;If the &lt;code&gt;src&lt;/code&gt; URL is on a private server or an origin that doesn't send CORS headers, the fetch will fail with a network error. In that case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use inline content instead of &lt;code&gt;src&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configure your server to send &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; for the IaC files&lt;/li&gt;
&lt;li&gt;Use a CORS proxy (only for non-sensitive content)&lt;/li&gt;
&lt;li&gt;Generate the diagram URL server-side and embed it as a static iframe URL instead of using the web component's dynamic fetch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a fetch fails, the component shows a visible error state — it doesn't silently disappear. Non-200 response, empty body, network error — you get "Could not load diagram source" in the widget and the actual error in the browser console, so you know what went wrong.&lt;/p&gt;

&lt;p&gt;For inline content, there's no network request and no CORS consideration — the content is right there in the DOM, and the component reads it synchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: what the embed does and doesn't send
&lt;/h2&gt;

&lt;p&gt;Same privacy model as the rest of InfraSketch: your IaC code never touches InfraSketch servers. Here's exactly what happens when you use &lt;code&gt;src&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser fetches the &lt;code&gt;src&lt;/code&gt; URL — request goes to GitHub or wherever you're hosting the file, not to InfraSketch&lt;/li&gt;
&lt;li&gt;File content lands in the browser&lt;/li&gt;
&lt;li&gt;The component encodes it into a URL fragment (gzip + base64) in-memory&lt;/li&gt;
&lt;li&gt;Creates an iframe pointing to &lt;code&gt;https://infrasketch.cloud/#&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Browser loads the InfraSketch web app — the HTTP request goes to our CDN, but fragments are never included in HTTP requests (that's the HTTP spec)&lt;/li&gt;
&lt;li&gt;The web app decodes the fragment client-side and renders&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What InfraSketch servers actually see: a request for the embed script, a request for the web app assets. That's it. No IaC code, no diagram content, no infrastructure metadata. Standard CDN logs with IP, user agent, timestamp — same as any website.&lt;/p&gt;

&lt;p&gt;With inline content, step 1 doesn't happen at all. The only network contact is loading the web app assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete minimal embed page
&lt;/h2&gt;

&lt;p&gt;Here's a full, self-contained HTML page that embeds a Terraform diagram using the web component. You can use this as a starting point for a GitHub Pages documentation page or any static HTML site:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`css&lt;/p&gt;

&lt;p&gt;Network Architecture - Internal Docs&lt;/p&gt;

&lt;p&gt;body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }&lt;br&gt;
h1 { font-size: 24px; margin-bottom: 8px; }&lt;br&gt;
p { color: #555; margin-bottom: 24px; }&lt;/p&gt;

&lt;p&gt;Production Network Architecture&lt;br&gt;
This diagram is generated live from the Terraform source in the infra repository.&lt;/p&gt;

&lt;p&gt;Diagram always reflects the current &lt;code&gt;main&lt;/code&gt; branch. &lt;a href="https://github.com/your-org/infra/blob/main/terraform/networking/main.tf" rel="noopener noreferrer"&gt;View source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This page requires no build step, no server-side processing, and no InfraSketch account. It works as a standalone HTML file served from any static host.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the embed component work with private GitHub repositories?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; fetch runs in the user's browser, so private GitHub raw URLs would need a token — which you obviously can't bake into a public HTML page. For private repos, use inline content instead: copy the relevant Terraform or Kubernetes code between the tags. Or generate the URL server-side with &lt;code&gt;npx infrasketch main.tf --no-open&lt;/code&gt; and embed it as a static iframe. The state is in the fragment, so it doesn't expire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will the diagram break if infrasketch.cloud goes down?
&lt;/h3&gt;

&lt;p&gt;If the web app is unavailable, iframes and components pointing to it will show an error. If you need high reliability for critical docs, pre-generate the URL with the CLI and embed it as a static iframe — then if you ever want to self-host the web app, the same URL works unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I control the diagram layout or colors through the embed?
&lt;/h3&gt;

&lt;p&gt;Not through attributes currently. Users can pan, zoom, and move nodes interactively inside the embedded diagram. If you want a specific layout, open the diagram in the full web app, arrange it, and use the "share" URL — it encodes the adjusted node positions in the fragment. Custom theming via attributes is on the roadmap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the component support lazy loading for pages with many diagrams?
&lt;/h3&gt;

&lt;p&gt;Yes — it uses &lt;code&gt;IntersectionObserver&lt;/code&gt; internally to defer fetching and rendering until the element is near the viewport. Put 10 diagrams on a page and only the visible ones actually load. The rest wait until you scroll to them. This makes diagram-heavy runbooks and wikis load fast enough to be practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the embed component with React, Vue, or other frameworks?
&lt;/h3&gt;

&lt;p&gt;Web components work natively in all modern browsers, so any framework is fine. React treats it like any other HTML element. Vue might show an "unknown element" warning — add &lt;code&gt;infra-sketch&lt;/code&gt; to &lt;code&gt;compilerOptions.isCustomElement&lt;/code&gt; in your Vite/Vue config to silence it. No npm package needed — just the script tag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Turn your IaC docs into living architecture diagrams Add one script tag to your docs site and your diagrams stay synchronized with your code automatically. Try it in the browser first — no account needed. &lt;a href="https://dev.to/"&gt;Try InfraSketch Free →&lt;/a&gt;
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/infrasketch-cli-terraform-diagram.html"&gt;InfraSketch CLI — Visualize Any IaC Repo From Your Terminal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/infrasketch-mcp-server-claude-code-cursor.html"&gt;InfraSketch MCP Server — Architecture Diagrams in Claude Code, Cursor &amp;amp; Windsurf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/github-action-terraform-diagram.html"&gt;Auto-Post Architecture Diagrams on Every IaC Pull Request&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Cloud Cost Estimation with Infracost: How It Works and What It Covers</title>
      <dc:creator>Raghvendra Pandey</dc:creator>
      <pubDate>Tue, 12 May 2026 01:21:28 +0000</pubDate>
      <link>https://dev.to/pandey-raghvendra/visualize-infracost-on-your-architecture-diagram-1fmg</link>
      <guid>https://dev.to/pandey-raghvendra/visualize-infracost-on-your-architecture-diagram-1fmg</guid>
      <description>&lt;p&gt;Cloud infrastructure bills are notoriously hard to predict. A Terraform change that looks like a small configuration tweak — upgrading an RDS instance class, enabling Multi-AZ, adding a NAT gateway — can add hundreds of dollars per month. Without tooling, engineers discover the cost impact after the change is deployed and the first bill arrives.&lt;/p&gt;

&lt;p&gt;Infracost bridges the gap between infrastructure code and cost. It reads your Terraform (or Terragrunt) and queries cloud provider pricing APIs to generate a cost breakdown before anything is deployed. This guide covers how Infracost works, what it can and can't estimate, how to run it, and how to wire it into a CI/CD pipeline so cost changes appear on every pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Infracost works
&lt;/h2&gt;

&lt;p&gt;Infracost has three main phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parse the Terraform project.&lt;/strong&gt; Infracost runs &lt;code&gt;terraform init&lt;/code&gt; and &lt;code&gt;terraform plan&lt;/code&gt; internally, generating a plan JSON file. It reads resource types, instance types, storage sizes, replication settings, and other cost-relevant attributes from the plan output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Look up pricing.&lt;/strong&gt; Infracost maintains a pricing database synced from AWS, GCP, and Azure price lists. It queries this database to find the on-demand price for each resource configuration. For usage-based resources (data transfer, Lambda invocations, DynamoDB read/write units), it applies usage estimates from a &lt;code&gt;infracost-usage.yml&lt;/code&gt; file or uses zero as a baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Generate the output.&lt;/strong&gt; The result is a breakdown of monthly costs per resource, organized by module and resource type, plus a diff view when comparing two Terraform states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;infracost breakdown &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;
Name                                      Monthly Qty  Unit          Monthly Cost

aws_instance.web
&lt;/span&gt;&lt;span class="gp"&gt;Instance usage (Linux/UNIX, on-demand)          730  hours              $&lt;/span&gt;29.20
&lt;span class="go"&gt;root_block_device
&lt;/span&gt;&lt;span class="gp"&gt;Storage (general purpose SSD, gp3)             50  GB                  $&lt;/span&gt;4.00
&lt;span class="go"&gt;
aws_db_instance.main
&lt;/span&gt;&lt;span class="gp"&gt;Database instance (db.t3.medium, Multi-AZ)      730  hours             $&lt;/span&gt;118.26
&lt;span class="gp"&gt;Storage (general purpose SSD, gp2)              100  GB                 $&lt;/span&gt;23.00
&lt;span class="go"&gt;
aws_nat_gateway.main
&lt;/span&gt;&lt;span class="gp"&gt;NAT gateway                                      730  hours              $&lt;/span&gt;32.85
&lt;span class="gp"&gt;Data processed                                     0  GB                  $&lt;/span&gt;0.00
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;OVERALL TOTAL                                                             $&lt;/span&gt;207.31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Infracost can estimate
&lt;/h2&gt;

&lt;p&gt;Infracost covers most compute, storage, and database resources where pricing is instance-type or size based:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2 instances&lt;/strong&gt; — all instance families, Linux/Windows pricing, on-demand rates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS instances&lt;/strong&gt; — all DB engines and instance classes, Multi-AZ, storage, IOPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EKS&lt;/strong&gt; — cluster cost, node group EC2 instances, Fargate task pricing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; — standard storage pricing (per GB), request costs with usage estimates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; — data transfer with usage estimates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElastiCache&lt;/strong&gt; — Redis/Memcached node pricing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSearch&lt;/strong&gt; — instance pricing, storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT Gateway&lt;/strong&gt; — hourly cost, data processing with usage estimates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancers&lt;/strong&gt; — ALB/NLB hourly cost, LCU usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; — requires usage estimates for invocation count and duration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; — on-demand vs provisioned capacity, requires usage estimates for on-demand&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Infracost cannot estimate
&lt;/h3&gt;

&lt;p&gt;Some costs are unknowable from Terraform configuration alone because they depend entirely on runtime behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data transfer between regions or to the internet (volume varies with traffic)&lt;/li&gt;
&lt;li&gt;CloudWatch log storage and ingestion (volume depends on application log verbosity)&lt;/li&gt;
&lt;li&gt;API Gateway invocations (depends on request volume)&lt;/li&gt;
&lt;li&gt;Spot instance interruption rates&lt;/li&gt;
&lt;li&gt;Reserved instance or Savings Plans discounts (Infracost uses on-demand rates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infracost marks these as &lt;code&gt;$0.00&lt;/code&gt; with a note that usage estimates are needed, which is accurate — zero is the floor, not the expected cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage estimates
&lt;/h2&gt;

&lt;p&gt;For usage-based resources, Infracost reads a &lt;code&gt;infracost-usage.yml&lt;/code&gt; file where you specify expected usage:&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="c1"&gt;# infracost-usage.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;

&lt;span class="na"&gt;resource_usage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;aws_lambda_function.api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;monthly_requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000000&lt;/span&gt;       &lt;span class="c1"&gt;# 5M invocations/month&lt;/span&gt;
&lt;span class="na"&gt;request_duration_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;        &lt;span class="c1"&gt;# average 200ms per invocation&lt;/span&gt;
&lt;span class="na"&gt;memory_mb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;

&lt;span class="na"&gt;aws_cloudfront_distribution.cdn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;monthly_data_transfer_to_internet_gb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;

&lt;span class="na"&gt;aws_nat_gateway.main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;monthly_data_processed_gb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;

&lt;span class="na"&gt;aws_dynamodb_table.sessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;monthly_read_request_units&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt;
&lt;span class="na"&gt;monthly_write_request_units&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infracost breakdown &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--usage-file&lt;/span&gt; infracost-usage.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage estimates are approximate by definition, but even rough estimates make cost modeling far more useful than showing $0.00 for all usage-based resources. Teams often store a shared &lt;code&gt;infracost-usage.yml&lt;/code&gt; alongside production usage metrics, updated quarterly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Infracost
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;infracost

&lt;span class="c"&gt;# Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh

&lt;span class="c"&gt;# Register for a free API key (rate limiting, no data stored)&lt;/span&gt;
infracost auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic cost breakdown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show costs for current Terraform directory&lt;/span&gt;
infracost breakdown &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Output JSON for programmatic use&lt;/span&gt;
infracost breakdown &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--out-file&lt;/span&gt; infracost.json

&lt;span class="c"&gt;# Show a diff between two Terraform states&lt;/span&gt;
infracost diff &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--compare-to&lt;/span&gt; infracost-base.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cost diff between branches
&lt;/h3&gt;

&lt;p&gt;The diff workflow is the most useful for pull request review: compare the base branch cost against the PR branch cost and surface only the delta.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the base branch, generate baseline&lt;/span&gt;
git checkout main
infracost breakdown &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--out-file&lt;/span&gt; infracost-base.json

&lt;span class="c"&gt;# On the PR branch, generate diff&lt;/span&gt;
git checkout feature/upgrade-rds
infracost diff &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--compare-to&lt;/span&gt; infracost-base.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project: my-infrastructure

+ aws_db_instance.main
+ Database instance class changed: db.t3.medium to db.r6g.large
+$168.44 (+142%)

~ aws_instance.web (no change)
~ aws_nat_gateway.main (no change)

Monthly cost change: +$168.44 (from $207.31 to $375.75)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI/CD integration
&lt;/h2&gt;

&lt;p&gt;Infracost's most valuable feature is the PR comment: on every pull request that changes Terraform, Infracost posts a cost diff directly in the PR. Engineers see the impact before merging.&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="c1"&gt;# .github/workflows/infracost.yml&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;Infracost Cost Estimate&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&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;infracost&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;Setup Infracost&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;infracost/actions/setup@v3&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;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.INFRACOST_API_KEY }}&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;Generate cost estimate for base branch&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="err"&gt;g&lt;/span&gt;&lt;span class="s"&gt;it fetch origin ${{ github.base_ref }}&lt;/span&gt;
&lt;span class="err"&gt;g&lt;/span&gt;&lt;span class="s"&gt;it checkout origin/${{ github.base_ref }}&lt;/span&gt;
&lt;span class="err"&gt;i&lt;/span&gt;&lt;span class="s"&gt;nfracost breakdown --path . \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-format json \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-out-file /tmp/infracost-base.json&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt; name: Generate cost diff for PR branch&lt;/span&gt;
&lt;span class="err"&gt;r&lt;/span&gt;&lt;span class="s"&gt;un: |&lt;/span&gt;
&lt;span class="err"&gt;g&lt;/span&gt;&lt;span class="s"&gt;it checkout ${{ github.sha }}&lt;/span&gt;
&lt;span class="err"&gt;i&lt;/span&gt;&lt;span class="s"&gt;nfracost diff --path . \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-format json \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-compare-to /tmp/infracost-base.json \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-out-file /tmp/infracost-diff.json&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt; name: Post PR comment&lt;/span&gt;
&lt;span class="err"&gt;r&lt;/span&gt;&lt;span class="s"&gt;un: |&lt;/span&gt;
&lt;span class="err"&gt;i&lt;/span&gt;&lt;span class="s"&gt;nfracost comment github \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-path /tmp/infracost-diff.json \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-repo $GITHUB_REPOSITORY \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-github-token ${{ secrets.GITHUB_TOKEN }} \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-pull-request ${{ github.event.pull_request.number }} \&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;-behavior update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--behavior update&lt;/code&gt; flag updates the existing Infracost comment rather than creating a new one on each commit — keeps the PR clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failing CI on large cost increases
&lt;/h3&gt;

&lt;p&gt;You can configure Infracost to fail CI when a PR increases costs beyond a threshold. This works well as a guardrail for staging environments or cost-sensitive teams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infracost comment github &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--path&lt;/span&gt; /tmp/infracost-diff.json &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--github-token&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ secrets.GITHUB_TOKEN &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--pull-request&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ github.event.pull_request.number &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--behavior&lt;/span&gt; update &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--policy-path&lt;/span&gt; infracost-policy.rego
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# infracost-policy.rego — fail CI if monthly cost increase &amp;gt; $100&lt;/span&gt;
&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;infracost&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;maxDiff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diffTotalMonthlyCost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxDiff&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="s2"&gt;"Monthly cost increase ($%.2f) exceeds threshold ($%.2f)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diffTotalMonthlyCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxDiff&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;h2&gt;
  
  
  Cost optimization patterns
&lt;/h2&gt;

&lt;p&gt;Infracost makes cost tradeoffs explicit, which is most useful when evaluating architectural decisions. Common patterns where it helps:&lt;/p&gt;

&lt;h3&gt;
  
  
  Right-sizing instances
&lt;/h3&gt;

&lt;p&gt;Oversized instances are one of the most common sources of waste. Infracost makes the cost difference between instance types visible before deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;db.r6g.xlarge&lt;/code&gt; (~$480/month) vs &lt;code&gt;db.r6g.large&lt;/code&gt; (~$240/month) — if workload fits, this is pure savings&lt;/li&gt;
&lt;li&gt;Multi-AZ doubles RDS cost — necessary for production, wasteful for staging&lt;/li&gt;
&lt;li&gt;gp3 storage is consistently cheaper than gp2 for equivalent IOPS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  NAT Gateway costs
&lt;/h3&gt;

&lt;p&gt;NAT Gateway charges both per-hour ($0.045/hour = ~$33/month) and per-GB processed. For high-throughput workloads, data processing costs dominate. Common mitigation: VPC endpoints for S3 and DynamoDB bypass the NAT Gateway entirely, eliminating data processing charges for traffic to those services.&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&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;vpc_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;service_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.region}.s3"&lt;/span&gt;
&lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;h3&gt;
  
  
  Environment-specific sizing
&lt;/h3&gt;

&lt;p&gt;Using production instance sizes in staging and dev environments is a common source of unnecessary spend. Infracost makes the cost difference concrete:&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_class"&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="s2"&gt;"db.t3.micro"&lt;/span&gt;  &lt;span class="c1"&gt;# $13/month in dev&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Production tfvars: instance_class = "db.r6g.large"  # $240/month&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Storage class selection
&lt;/h3&gt;

&lt;p&gt;S3 Intelligent-Tiering automatically moves objects to cheaper storage tiers based on access frequency. For large buckets with unpredictable access patterns, the monitoring fee ($0.0025/1000 objects) is typically far less than the savings from automatic tiering. Infracost can model this with usage estimates for access frequency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-project and monorepo setups
&lt;/h2&gt;

&lt;p&gt;For repos with multiple Terraform projects, Infracost can aggregate costs across all of them:&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="c1"&gt;# infracost.yml — project configuration&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;

&lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="pi"&gt;:&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;./environments/production&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;Production&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;./environments/staging&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;Staging&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;./modules/shared-networking&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;Shared Networking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infracost breakdown &lt;span class="nt"&gt;--config-file&lt;/span&gt; infracost.yml &lt;span class="nt"&gt;--format&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows each project's costs separately and a combined total — useful for understanding the cost split across environments or teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Infracost output alongside your architecture
&lt;/h2&gt;

&lt;p&gt;Cost numbers are more actionable when connected to the architecture they describe. When working with Terraform that has both a Checkov security scan and an Infracost estimate, &lt;a href="https://dev.to/"&gt;InfraSketch&lt;/a&gt; lets you visualize the infrastructure from the same Terraform source: paste your configuration to generate an architecture diagram, then overlay the Infracost JSON output to see per-resource cost annotations directly on the diagram. This makes it easier to communicate cost breakdowns to stakeholders who aren't reading raw JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="///blog/terraform-modules-guide.html"&gt;Terraform Modules: Writing, Testing, and Reusing Infrastructure Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/terraform-state-explained.html"&gt;Terraform State Explained: Backends, Locking, and Remote State&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="///blog/checkov-diagram-visualization.html"&gt;Infrastructure Security Scanning with Checkov: How It Works and What It Checks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
