<?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: Kiran Gunturu</title>
    <description>The latest articles on DEV Community by Kiran Gunturu (@kiran_gunturu_022085015ec).</description>
    <link>https://dev.to/kiran_gunturu_022085015ec</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%2F4117834%2Fe6aa971a-020d-4a95-a93f-efae5469172a.png</url>
      <title>DEV Community: Kiran Gunturu</title>
      <link>https://dev.to/kiran_gunturu_022085015ec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiran_gunturu_022085015ec"/>
    <language>en</language>
    <item>
      <title>Building an AWS + Snowflake with Terraform: Lessons from My CI/CD Review</title>
      <dc:creator>Kiran Gunturu</dc:creator>
      <pubDate>Mon, 21 Sep 2026 17:46:40 +0000</pubDate>
      <link>https://dev.to/kiran_gunturu_022085015ec/building-an-aws-snowflake-landing-zone-with-terraform-lessons-from-my-cicd-review-1nia</link>
      <guid>https://dev.to/kiran_gunturu_022085015ec/building-an-aws-snowflake-landing-zone-with-terraform-lessons-from-my-cicd-review-1nia</guid>
      <description>&lt;p&gt;Every analytics platform has an unglamorous foundation: the raw/landing tier where source data first lands before anything clever happens to it. It's easy to click that together by hand once. It's much harder to make it reproducible across dev, UAT, and prod and to prove to yourself that what's in prod is what you reviewed.&lt;/p&gt;

&lt;p&gt;So I built the landing tier of a banking-style (FCC/AML) data platform entirely as Terraform: a single root module orchestrating five child modules, with remote state, environment promotion, and a GitLab pipeline gating every change. Then I reviewed my own code as if it were a PR and found a handful of design gaps that are worth more than the happy-path walkthrough. Both halves are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stack provisions
&lt;/h2&gt;

&lt;p&gt;One root config wires together five modules across AWS and Snowflake:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Why modular, not one big file
&lt;/h2&gt;

&lt;p&gt;The temptation with Terraform is to drop everything into &lt;code&gt;main.tf&lt;/code&gt;. Splitting into &lt;code&gt;aws_vpc&lt;/code&gt;, &lt;code&gt;aws_ec2&lt;/code&gt;, &lt;code&gt;aws_s3&lt;/code&gt;, &lt;code&gt;aws_glue&lt;/code&gt;, and &lt;code&gt;snowflake_database&lt;/code&gt; modules buys three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit dependencies.&lt;/strong&gt; The EC2 module consumes the VPC module's &lt;code&gt;subnet_id&lt;/code&gt; and &lt;code&gt;security_group_id&lt;/code&gt; outputs an implicit dependency Terraform resolves for you. The Glue module carries an explicit &lt;code&gt;depends_on = [module.s3_raw]&lt;/code&gt; so the bucket exists before the job references it. S3 and Snowflake share nothing, so they apply in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse.&lt;/strong&gt; The same VPC module can back a completely different stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewable blast radius.&lt;/strong&gt; A change to the Glue module can't silently touch networking.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97jt0ylgjolwvzhbm9x7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97jt0ylgjolwvzhbm9x7.png" alt="Module Dependency graph" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  State that won't corrupt itself
&lt;/h2&gt;

&lt;p&gt;The backend is S3 for the state file plus a DynamoDB table for locking, both encrypted:&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;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;"your-tf-state-bucket"&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;"tf_state/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;"your-tf-lock-table"&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 DynamoDB lock is the part people skip and regret. Without it, two &lt;code&gt;apply&lt;/code&gt;s running at once — a teammate and a CI job, say can race and corrupt state. The lock makes the second one wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promotion by branch, not by copy-paste
&lt;/h2&gt;

&lt;p&gt;Instead of separate root configs per environment, one config selects its variable file from the branch, driven by &lt;code&gt;workflow:rules&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; → &lt;code&gt;main.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uat&lt;/code&gt; → &lt;code&gt;uat.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;everything else (dev + feature branches) → &lt;code&gt;dev.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First match wins, and merge-request events are dropped up front. The pipeline stages are &lt;code&gt;validate → plan → apply → destroy&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftamcvkdnrpvrmqqddfkp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftamcvkdnrpvrmqqddfkp.png" alt="CI/CD Pipeline" width="799" height="161"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;plan&lt;/code&gt; writes a saved plan file and &lt;code&gt;apply&lt;/code&gt; consumes exactly that artifact, so you apply the plan you reviewed - not a fresh one computed against drifted state.&lt;/p&gt;

&lt;p&gt;Here's the pipeline running for real across the dev, uat, and main branches:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdgbkmudoysh97r419br.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdgbkmudoysh97r419br.png" alt="Pipeline Execution" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Now the part that actually taught me something
&lt;/h2&gt;

&lt;p&gt;Reviewing this as a PR, three design gaps stood out. These are the interesting bit, because they're the mistakes that look fine until they bite:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Merge requests produce no pipeline at all.&lt;/strong&gt; Because MR events are killed with &lt;code&gt;when: never&lt;/code&gt;, there's no &lt;code&gt;validate&lt;/code&gt; and no &lt;code&gt;plan&lt;/code&gt; at review time. Reviewers approve blind - they never see what the change would actually do to infrastructure. The fix is a dedicated MR job that runs &lt;code&gt;validate&lt;/code&gt; and &lt;code&gt;plan&lt;/code&gt; and posts the plan output on the merge request, so approval is based on evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;main&lt;/code&gt; auto-applies to prod with no gate - but prod can't be destroyed.&lt;/strong&gt; The pipeline applies to production automatically on merge to &lt;code&gt;main&lt;/code&gt;, yet &lt;code&gt;destroy&lt;/code&gt; is manual and blocked on &lt;code&gt;main&lt;/code&gt;. That asymmetry is backwards: teardown is protected, but the far riskier &lt;em&gt;unattended apply to prod&lt;/em&gt; isn't. Production apply is exactly where you want a manual approval gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Secrets in &lt;code&gt;*.tfvars&lt;/code&gt;, and a &lt;code&gt;.gitignore&lt;/code&gt; that misses them.&lt;/strong&gt; The variable files carried credentials, and &lt;code&gt;.gitignore&lt;/code&gt; only excluded &lt;code&gt;*.tfvars.json&lt;/code&gt; - so &lt;code&gt;*.tfvars&lt;/code&gt; was tracked. Credentials belong in CI variables or a secrets manager, the variables should be marked &lt;code&gt;sensitive = true&lt;/code&gt;, &lt;code&gt;*.tfvars&lt;/code&gt; should be ignored, and any exposed secret must be rotated and scrubbed from history.&lt;/p&gt;

&lt;p&gt;A few smaller ones: the Glue module hardcoded its job name and script path instead of using the variables passed in, so it pointed at a bucket the stack never created; the pipeline had no &lt;code&gt;terraform fmt&lt;/code&gt; check or security scanner (tfsec/Checkov); and the manual &lt;code&gt;destroy&lt;/code&gt; used &lt;code&gt;--auto-approve&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you're standing up a landing zone of your own, the patterns that held up: split into modules with clear input/output contracts, lock your state, and promote one config across environments by variable file rather than duplicating configs. And the lesson the gaps taught me: &lt;strong&gt;the CI/CD design is the real security boundary, not the Terraform.&lt;/strong&gt; A tidy &lt;code&gt;.tf&lt;/code&gt; file with a pipeline that lets prod apply unreviewed and secrets slip into git isn't safe - it just looks safe.&lt;/p&gt;

&lt;p&gt;Terraform makes infrastructure reproducible. Your pipeline is what makes it &lt;em&gt;reviewable&lt;/em&gt;. Get that second part right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full code
&lt;/h2&gt;

&lt;p&gt;If you want to dig into the actual Terraform - all five modules, the GitLab CI pipeline, and the environment variable files - the complete project is here:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://gitlab.com/kiran.gntdm-group/terraform" rel="noopener noreferrer"&gt;https://gitlab.com/kiran.gntdm-group/terraform&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions and feedback are welcome - happy to talk through any of the design choices.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
