<?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: Bader Eddine El ouerghi</title>
    <description>The latest articles on DEV Community by Bader Eddine El ouerghi (@bader_eddineelouerghi_5).</description>
    <link>https://dev.to/bader_eddineelouerghi_5</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%2F1839760%2F87a2cb2b-2ead-47aa-b3ec-f60b9b4686e4.png</url>
      <title>DEV Community: Bader Eddine El ouerghi</title>
      <link>https://dev.to/bader_eddineelouerghi_5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bader_eddineelouerghi_5"/>
    <language>en</language>
    <item>
      <title>The 7 AWS charges nobody chose</title>
      <dc:creator>Bader Eddine El ouerghi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:52:32 +0000</pubDate>
      <link>https://dev.to/bader_eddineelouerghi_5/the-7-aws-charges-nobody-chose-1202</link>
      <guid>https://dev.to/bader_eddineelouerghi_5/the-7-aws-charges-nobody-chose-1202</guid>
      <description>&lt;p&gt;Most AWS waste isn't a bad architecture decision. It's a charge that nobody decided on: a default, a leftover, a setting nobody revisited. I work across 170+ AWS accounts, and these seven show up in almost every one of them.&lt;/p&gt;

&lt;p&gt;Each section has what it is, why it happens, how to find it (read-only commands) and the fix. Prices are us-east-1 list prices; check your region.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Unattached EBS volumes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; an EC2 instance is terminated, but its data volume had &lt;code&gt;DeleteOnTermination=false&lt;/code&gt; (the default for volumes attached after launch). The disk stays, billed every month: about US$0.08/GB-month for gp3, US$0.10 for gp2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status,Values&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Volumes[].[VolumeId,Size,VolumeType,CreateTime]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; snapshot anything you're unsure about (snapshots are much cheaper per GB), then delete the volume. While you're there, convert remaining gp2 volumes to gp3: same or better performance, ~20% cheaper, no downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Public IPv4 addresses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; since February 2024, AWS charges US$0.005/hour for &lt;strong&gt;every&lt;/strong&gt; public IPv4 address — attached or not. That's about US$3.60/month each. EC2 instances in public subnets, NAT Gateways, load balancers (one per AZ), and idle Elastic IPs all count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&gt; the quickest view is the free &lt;strong&gt;Public IP Insights&lt;/strong&gt; in VPC IP Address Manager. Or list Elastic IPs that aren't associated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-addresses &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Addresses[?AssociationId==null].PublicIp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; release idle Elastic IPs, turn off "auto-assign public IPv4" on subnets that don't need it, and put instances behind a load balancer or use SSM Session Manager instead of a public IP for access.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. NAT Gateways in dev
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; a NAT Gateway costs about US$0.045/hour (≈ US$33/month) before a single byte goes through it, plus US$0.045 per GB processed. Many VPC templates create one per Availability Zone — three per VPC — and get copied into every dev and test account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-nat-gateways &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;state,Values&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"NatGateways[].[NatGatewayId,VpcId,SubnetId]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; in non-production, one NAT Gateway per VPC is plenty (you accept that one AZ failure stops outbound traffic for dev — usually fine). For sandboxes that rarely need outbound internet, consider none at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. CloudWatch Logs set to "Never expire"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; every new log group defaults to keeping logs forever. Storage is about US$0.03/GB-month — small per GB, but debug logs from years ago add up quietly, and nobody reads them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs describe-log-groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"logGroups[?retentionInDays==null].[logGroupName,storedBytes]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; set retention on every group (&lt;code&gt;aws logs put-retention-policy --log-group-name X --retention-in-days 30&lt;/code&gt;), and set it in your Terraform/CloudFormation modules so new groups never default to forever. Keep long retention only where compliance requires it — and then consider exporting to S3 with a lifecycle rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. S3 incomplete multipart uploads
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; when a large upload fails halfway, the parts already uploaded stay in the bucket. They don't appear in the console object list, but you pay for their storage — indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api list-multipart-uploads &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Uploads[].[Key,Initiated]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 Storage Lens also shows "incomplete multipart upload bytes" across all buckets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; one lifecycle rule per bucket, ideally added by default in your bucket module:&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;"Rules"&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;"abort-mpu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&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;"AbortIncompleteMultipartUpload"&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;"DaysAfterInitiation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&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;
  
  
  6. EKS clusters on old Kubernetes versions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; each Kubernetes version gets about 14 months of standard support on EKS. After that, AWS doesn't stop your cluster — it moves it to &lt;strong&gt;extended support&lt;/strong&gt;, and the control plane goes from US$0.10/hour to US$0.60/hour. That's roughly US$73 → US$438 per cluster per month, 6× more, for the same cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them:&lt;/strong&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="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws eks list-clusters &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"clusters[]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;aws eks describe-cluster &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"cluster.[name,version,upgradePolicy.supportType]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; delete clusters nobody uses (check workloads and owners first), and upgrade the rest one minor version at a time, non-prod first. The next post in this series walks through doing that safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. S3 and DynamoDB traffic through a NAT Gateway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; instances in private subnets talking to S3 or DynamoDB go through the NAT Gateway by default — at US$0.045 per GB processed. A &lt;strong&gt;gateway VPC endpoint&lt;/strong&gt; for S3 or DynamoDB does the same job for free: no hourly charge, no per-GB charge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find it:&lt;/strong&gt; check if your VPCs have gateway endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-vpc-endpoints &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vpc-endpoint-type,Values&lt;span class="o"&gt;=&lt;/span&gt;Gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"VpcEndpoints[].[VpcId,ServiceName]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare with NAT Gateway "BytesOutToDestination" in CloudWatch. If a VPC has a busy NAT Gateway and no S3 endpoint, that's usually your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; add the gateway endpoints and associate them with the private route tables. In Terraform it's one &lt;code&gt;aws_vpc_endpoint&lt;/code&gt; resource per service. Existing connections keep working; new traffic takes the free path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing them in Cost Explorer
&lt;/h2&gt;

&lt;p&gt;The commands above work one account and region at a time. To see how much these seven cost across the whole organization, open Cost Explorer in the management (payer) account, group by &lt;strong&gt;Usage type&lt;/strong&gt;, and filter for the last full month. Useful usage types to search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EBS:VolumeUsage.gp2&lt;/code&gt; / &lt;code&gt;.gp3&lt;/code&gt; — compare with what's attached&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PublicIPv4:InUseAddress&lt;/code&gt; and &lt;code&gt;PublicIPv4:IdleAddress&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NatGateway-Hours&lt;/code&gt; and &lt;code&gt;NatGateway-Bytes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TimedStorage-ByteHrs&lt;/code&gt; under CloudWatch (log storage)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AmazonEKS&lt;/code&gt; usage with "extendedSupport" in the name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then group the same view by &lt;strong&gt;Linked account&lt;/strong&gt;. In my experience, a few accounts — often old sandboxes and abandoned projects — hold most of the waste. That tells you where to start, and who to ask before deleting anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;None of these need a re-architecture. Most take an afternoon, and the real fix is the same each time: &lt;strong&gt;change the default&lt;/strong&gt; in the module or template that creates the resource, so the waste doesn't come back next quarter.&lt;/p&gt;

&lt;p&gt;A simple order that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the seven read-only checks above in every account and region.&lt;/li&gt;
&lt;li&gt;Fix the defaults in your IaC first (log retention, lifecycle rules, endpoints, gp3, subnet IP settings).&lt;/li&gt;
&lt;li&gt;Clean up what already exists, snapshot first when in doubt.&lt;/li&gt;
&lt;li&gt;Put a monthly reminder in the calendar to run the checks again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's the strangest AWS charge you've found? I'd love to hear it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Badereddine is a cloud and DevOps engineer in Canada who operates 170+ AWS accounts and 500+ EKS clusters day to day, and runs TERRAX, a small AWS cost and platform engineering practice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The 45 checks I run first, in the order that saves the most, are in a free checklist: &lt;a href="https://www.terrax-cloud.com/checklist.html" rel="noopener noreferrer"&gt;AWS Cost Audit Checklist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How We Built a Multi-Cloud Terraform Orchestrator: The TerraX Architecture</title>
      <dc:creator>Bader Eddine El ouerghi</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:20:43 +0000</pubDate>
      <link>https://dev.to/bader_eddineelouerghi_5/how-we-built-a-multi-cloud-terraform-orchestrator-the-terrax-architecture-5g00</link>
      <guid>https://dev.to/bader_eddineelouerghi_5/how-we-built-a-multi-cloud-terraform-orchestrator-the-terrax-architecture-5g00</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Beta is open.&lt;/strong&gt; If you want to try TerraX before finishing this article: &lt;a href="https://docs.terrax-cloud.com" rel="noopener noreferrer"&gt;docs.terrax-cloud.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why We Built This
&lt;/h2&gt;

&lt;p&gt;Managing infrastructure across AWS, GCP, Azure, and Cloudflare with a small team taught us one thing fast: Terraform itself is not the bottleneck. Everything around it is.&lt;/p&gt;

&lt;p&gt;State backends scattered across providers. Credentials living on individual laptops. No shared record of who ran a plan and what changed. And a persistent gap between "infrastructure provisioned" and "things running on that infrastructure configured" — a gap that gets filled by Bash scripts nobody wants to maintain.&lt;/p&gt;

&lt;p&gt;We wanted a platform that treated each cloud provider as a first-class object: centralized credentials, isolated state per provider, a sandboxed executor that runs &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; with full log streaming, and a workflow engine that could chain Terraform runs with script functions and pass outputs between them. That became TerraX.&lt;/p&gt;

&lt;p&gt;This article walks through the key architectural decisions — what we chose, what we nearly chose, and where we got things wrong the first time.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Overview
&lt;/h2&gt;

&lt;p&gt;TerraX is composed of four main layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Angular Frontend ]
        |
[ Spring Boot Backend — "teraflare" ]
        |
[ Go Agent — "terrax-agent" ]
        |
[ Terraform / OpenTofu executor ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend (Angular)&lt;/strong&gt; handles the UI layer: resource forms, plan/apply diff views, workflow builder, kubeconfig manager, compliance dashboards. It talks exclusively to the backend via REST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend (Spring Boot / Java)&lt;/strong&gt; is the core: provider credential storage, Terraform state management, RBAC enforcement, workflow orchestration, cost forecasting, compliance scanning. DB migrations run via Flyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent (Go)&lt;/strong&gt; is the sandboxed executor. It receives jobs from the backend, bundles workspace files, runs Terraform/OpenTofu, and streams logs back in real time. It runs separately from the backend for isolation — a misbehaving Terraform run can't take down the API server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform / OpenTofu&lt;/strong&gt; is the execution engine underneath everything. We default to OpenTofu.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Go Agent: Why Go, and How the Sandbox Works
&lt;/h2&gt;

&lt;p&gt;The decision to write the agent in Go rather than extending the Java backend was deliberate. Terraform execution is I/O-heavy and long-running — sometimes 10+ minutes for a full apply on a complex provider. We didn't want those goroutines competing with API request handling on the same JVM.&lt;/p&gt;

&lt;p&gt;Go gave us lightweight concurrency for managing multiple concurrent jobs, straightforward process management for spawning &lt;code&gt;tofu&lt;/code&gt; as a subprocess, and simple binary deployment into the Kubernetes pod that runs the executor.&lt;/p&gt;

&lt;p&gt;When the backend enqueues a job, the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receives the job payload (provider credentials, workspace ID, operation type)&lt;/li&gt;
&lt;li&gt;Bundles the workspace: pulls &lt;code&gt;.tf&lt;/code&gt; files, variable files, and state from the backend&lt;/li&gt;
&lt;li&gt;Writes them to a temporary working directory&lt;/li&gt;
&lt;li&gt;Spawns &lt;code&gt;tofu plan&lt;/code&gt; or &lt;code&gt;tofu apply&lt;/code&gt; as a subprocess&lt;/li&gt;
&lt;li&gt;Streams stdout/stderr back to the backend line by line&lt;/li&gt;
&lt;li&gt;Reports the final exit code and cleans up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key design decision here: &lt;strong&gt;state is never stored on the agent&lt;/strong&gt;. The agent is stateless — it pulls what it needs per job and pushes results back. This means you can run multiple agent instances horizontally and jobs route to whichever is available.&lt;/p&gt;

&lt;p&gt;We chose OpenTofu as the default because of the licensing clarity. The routing is configurable per job if you need Terraform for a specific provider version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Cloud Kubeconfig Generation Without the Cloud CLIs
&lt;/h2&gt;

&lt;p&gt;One of the most concrete problems we solved is kubeconfig generation across EKS, GKE, and AKS without depending on &lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;gcloud&lt;/code&gt;, or &lt;code&gt;az&lt;/code&gt; being installed anywhere.&lt;/p&gt;

&lt;p&gt;Each provider has a completely different auth model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS&lt;/strong&gt; uses an exec-plugin model. The kubeconfig calls &lt;code&gt;aws eks get-token&lt;/code&gt; at authentication time, which means the AWS CLI must be installed wherever the kubeconfig is used. We generate the kubeconfig server-side by calling &lt;code&gt;eks:DescribeCluster&lt;/code&gt; via the AWS SDK to get the cluster endpoint and CA data, then embed the exec-plugin block pointing to the AWS SDK's token endpoint. Auth still flows through the SDK — we're not storing long-lived tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GKE&lt;/strong&gt; is simpler in some ways: we call the GCP Container REST API with a service account token, pull the cluster endpoint and CA certificate, and embed a short-lived Bearer token directly in the kubeconfig. The downside is that token expiry means regenerating the kubeconfig periodically, which we handle transparently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AKS&lt;/strong&gt; provides the cleanest result: &lt;code&gt;listClusterAdminCredential&lt;/code&gt; from the Azure ARM API returns a ready-to-use kubeconfig with a cert-based auth block. No token rotation needed, though the cert-based approach means these are longer-lived credentials that need to be stored carefully.&lt;/p&gt;

&lt;p&gt;All three are stored namespaced per cluster in the backend database, accessible based on the user's role, and selectable as the target context for any downstream operation — ArgoCD installs, Helm runs, raw kubectl operations via the agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workflow Engine: Passing Outputs Between Steps
&lt;/h2&gt;

&lt;p&gt;The workflow engine is what ties infrastructure provisioning to operational automation. A workflow is a sequence of steps with conditional logic — each step is either a Terraform run or a Script Function (Python, Bash, or Docker container).&lt;/p&gt;

&lt;p&gt;The interesting part is output passing. When a Terraform apply completes, its outputs (e.g., a cluster endpoint, a load balancer IP, a database hostname) need to be available to the next step without manual copy-paste. We built a dynamic variable resolution system using reference strings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ref:var:VARIABLE_NAME&lt;/code&gt; — resolves a variable defined at the provider or workflow level&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ref:provider:ATTRIBUTE&lt;/code&gt; — resolves an attribute from the connected provider&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ref:function:OUTPUT_KEY&lt;/code&gt; — resolves an output captured from a previous Script Function step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At runtime, the backend resolves these references before passing the step payload to the agent. This means you can provision an EKS cluster in step 1, capture its endpoint as &lt;code&gt;ref:var:CLUSTER_ENDPOINT&lt;/code&gt;, and pass it to a Helm install in step 2 — without hardcoding anything.&lt;/p&gt;

&lt;p&gt;Script Functions run in isolated Docker containers or as Kubernetes jobs, with secrets injected as environment variables. Output is captured from stdout as structured key-value pairs and made available to subsequent steps via &lt;code&gt;ref:function:&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  State Isolation Per Provider
&lt;/h2&gt;

&lt;p&gt;One of the early architectural decisions that saved us the most headaches: &lt;strong&gt;one state backend per provider, not one global state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The alternative — a single shared state backend with workspaces — makes cross-provider references tempting and creates blast radius problems. A corrupted state file for your AWS provider shouldn't touch your GCP resources.&lt;/p&gt;

&lt;p&gt;In practice, each provider object in TerraX has its own isolated state stored in the backend. When the agent runs a plan or apply, it pulls the state for that specific provider, executes against it, and pushes the updated state back. Providers never share state files.&lt;/p&gt;

&lt;p&gt;The tradeoff: cross-provider data references have to go through the workflow engine's variable system, not Terraform's native &lt;code&gt;data&lt;/code&gt; sources across workspaces. For most use cases this is fine. For deeply coupled multi-cloud architectures it adds some friction — something we're still working through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compliance Scanning: OPA + Checkov
&lt;/h2&gt;

&lt;p&gt;Every Terraform configuration that passes through TerraX can be scored against compliance frameworks before apply. The scanning layer runs two tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkov&lt;/strong&gt; handles static analysis of the &lt;code&gt;.tf&lt;/code&gt; files — it understands Terraform's resource schema and checks for common misconfigurations (public S3 buckets, unencrypted RDS, overly permissive security groups, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPA (Open Policy Agent)&lt;/strong&gt; handles custom policy enforcement via Rego policies. This is where you encode organization-specific rules that Checkov doesn't cover — things like "all GCP resources must have a &lt;code&gt;cost-center&lt;/code&gt; label" or "no EKS node groups with instance types larger than &lt;code&gt;m5.xlarge&lt;/code&gt; without approval."&lt;/p&gt;

&lt;p&gt;Results are scored against PCI DSS, CIS Benchmarks, NIST CSF, and SOC 2 controls, aggregated per provider, and surfaced as a compliance score in the dashboard. One-click "fix preview" shows the Terraform change that would resolve a finding.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Browser Extension
&lt;/h2&gt;

&lt;p&gt;The Chrome extension deserves a brief mention because it solved a real problem: existing Cloudflare resources.&lt;/p&gt;

&lt;p&gt;When you have 200 DNS records, 30 WAF rules, and 15 Page Rules already configured in the Cloudflare dashboard, bringing them under Terraform management is painful. You need to write HCL for each one and run &lt;code&gt;terraform import&lt;/code&gt; with the correct Cloudflare resource ID.&lt;/p&gt;

&lt;p&gt;The extension runs on the Cloudflare dashboard pages, detects resource configurations as you browse, and sends them to TerraX where they're staged for import. It effectively automates the discovery + HCL generation step for resources you're already looking at in the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Got Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Abstracting too much from Terraform's output.&lt;/strong&gt; In early versions we summarized the plan output into a "changes" view. Users hated it. They wanted to see the raw plan, exactly as Terraform prints it. We reverted to showing the full output with syntax highlighting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming exit codes were reliable.&lt;/strong&gt; Terraform's exit code behavior across versions and providers is inconsistent enough that we had to parse stdout/stderr directly to determine plan/apply success. Lesson: don't trust exit codes alone for infrastructure tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Underestimating the import UX.&lt;/strong&gt; The most-used feature after launch wasn't resource creation — it was state import. People have years of existing infra they want under management. We initially treated import as a secondary flow and had to rebuild it properly after the first wave of feedback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drift detection is manual — no scheduled reconciliation against live cloud yet&lt;/li&gt;
&lt;li&gt;Azure resource coverage is thinner than AWS and GCP&lt;/li&gt;
&lt;li&gt;Generated HCL isn't always idiomatic for complex Terraform module structures&lt;/li&gt;
&lt;li&gt;ArgoCD SSO not fully tested end-to-end against all identity providers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try the Beta
&lt;/h2&gt;

&lt;p&gt;TerraX is in open beta. 14-day free trial, no credit card required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App: &lt;a href="https://app.terrax-cloud.com" rel="noopener noreferrer"&gt;app.terrax-cloud.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.terrax-cloud.com" rel="noopener noreferrer"&gt;docs.terrax-cloud.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have feedback — especially critical feedback on the architecture decisions above — I'm in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>terraform</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
