BXUP Track progress & test yourself — free
Cloud & Solutions Architecture · Hands-on lab · ~6 min read
CLOUD & SOLUTIONS ARCHITECTURE · HANDS-ON LAB

Lab 05 · Cross-Cloud IaC Translation with a Parity Gate

Translate a CloudFormation stack to Terraform with an AI assistant, then prove — not assume — parity with an offline validate/plan harness and a resource-by-resource review. The template sets two service-mapping traps that AI translators reliably fall into; your job is to catch both and document them.

Rajesh's Meridian engagement standardizes on Terraform; the client's incumbent shipped everything as CloudFormation. Format translation is exactly the kind of work AI makes 10x faster — and it is also where "compiles and plans clean" quietly diverges from "does the same thing". The parity gate is the difference between a migration and a regression.

Time & tier

~120 minutes hands-on · Core tier (maps to module M4). Cost: ₹0 — everything runs offline against a mock AWS provider (the same trick as the DevOps track's verification-pipeline lab); no cloud account, no credentials.

Setup

Install Terraform ≥ 1.5 (or OpenTofu). Create iac-parity/ with source/stack.yaml (the two blocks below joined into one file — split here only for readability) and an empty translated/ directory.

source/stack.yaml, part 1 — network + storage:

AWSTemplateFormatVersion: "2010-09-09"
Description: Meridian docs-ingest stack (translation source)

Resources:
  IngestVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.61.0.0/24
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: [{ Key: Name, Value: meridian-ingest }]

  WorkerSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref IngestVpc
      CidrBlock: 10.61.0.0/26
      AvailabilityZone: us-east-1a

  DocsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: meridian-docs-ingest-lab
      VersioningConfiguration: { Status: Enabled }

source/stack.yaml, part 2 — IAM + Lambda (append under Resources:):

  WorkerRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: meridian-ingest-worker
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      Policies:
        - PolicyName: docs-read
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: ListTheBucket
                Effect: Allow
                Action: ["s3:ListBucket"]
                Resource: !GetAtt DocsBucket.Arn
              - Sid: ReadObjects
                Effect: Allow
                Action: ["s3:GetObject", "s3:GetObjectVersion"]
                Resource: !Sub "${DocsBucket.Arn}/*"

  IngestFn:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: meridian-ingest-fn
      Runtime: python3.12
      Handler: index.handler
      Role: !GetAtt WorkerRole.Arn
      Timeout: 120
      Code:
        ZipFile: |
          import json
          def handler(event, context):
              keys = [r["s3"]["object"]["key"]
                      for r in event.get("Records", [])]
              print(json.dumps({"ingested": keys}))
              return {"count": len(keys)}

  IngestPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref IngestFn
      Action: lambda:InvokeFunction
      Principal: s3.amazonaws.com
      SourceArn: !GetAtt DocsBucket.Arn

translated/provider.tf — the offline harness (create-only plans never call AWS APIs, so this runs with fake keys):

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region                      = "us-east-1"
  access_key                  = "mock"
  secret_key                  = "mock"
  skip_credentials_validation = true
  skip_requesting_account_id  = true
  skip_metadata_api_check     = true
}

Guided steps

Step 1 — Inventory the source (15 min)

Before translating, build parity-checklist.md by hand: one row per CFN resource (6 rows) with columns CFN logical id · type · load-bearing properties · TF equivalent · parity status. Note anything that is CloudFormation-native rather than AWS-native — intrinsics (!Sub, !GetAtt), and any property where CloudFormation itself does work at deploy time rather than merely passing configuration through. Those rows are where translations die.

✅ Checkpoint: checklist with 6 rows; at least one row flagged "CFN-native behavior — watch closely".

Step 2 — AI-translate the stack (20 min)

Give your assistant the full stack.yaml: "Translate to Terraform for provider aws ~> 5.0. Preserve names, properties, and IAM semantics exactly. Flag anything that has no direct equivalent instead of improvising." Save the output into translated/ alongside the harness. Do not fix anything yet — the harness gets first look. (₹0 model note: a local ollama run qwen2.5-coder handles this translation; frontier models fall into the same traps, which is the point.)

✅ Checkpoint: translated .tf files on disk, untouched, alongside provider.tf.

Step 3 — Run the mechanical gates (25 min)

In translated/: terraform init -input=false, terraform validate, then terraform plan -input=false. Fix errors one at a time, logging each in trap-notes.md with what the AI wrote versus what the provider actually accepts. Expect friction around how the Lambda's code gets packaged: CloudFormation's ZipFile is deploy-time magic with no direct Terraform argument, and translators tend to invent one. The clean solution stays offline: an archive_file data source zipping a local index.py you extract from the template. Iterate until plan succeeds and shows all six resources.

✅ Checkpoint: terraform plan succeeds, Plan: 6 to add, with the Lambda packaged via a real local zip.

Step 4 — The parity review: where plan-clean lies (35 min)

A green plan proves the Terraform is valid, not that it is equivalent. Walk the checklist row by row against the plan output (terraform plan -no-color > plan.txt helps). Give the IAM role special hostility: extract the inline policy JSON from the plan and set it beside the CFN policy document. Check: number of statements, actions per statement, and — critically — which resource ARN shape each action is attached to. Also confirm versioning survived on the bucket, the Lambda permission's source ARN matches, and timeout/runtime carried over. Every discrepancy goes in trap-notes.md with severity and the exact fix.

Warning

IAM is where silent translation drift does real damage — a policy that merges statements or reshapes ARNs can pass validate, plan, and even deploy, then fail (or over-permit) only at runtime.

✅ Checkpoint: policy-JSON side-by-side diff exists; checklist has a parity verdict on all 6 rows.

Step 5 — Fix, re-gate, and write the parity report (25 min)

Repair everything found, re-run validate/plan, and write parity-report.md: the six-row verdict table, the two traps with a one-paragraph autopsy each (what the AI did, why it was plausible, which gate caught it — mechanical or human review), and one sentence on what you would automate next time (e.g., policy-JSON diffing as a scripted gate). If your AI happened to dodge a trap, document the dodge — knowing why a prompt avoided a failure is as valuable as catching it.

✅ Checkpoint: final plan green; parity-report.md names both traps with gate attribution.

Key Takeaway:

Translation parity has three tiers — parses, plans, behaves the same — and only the first two are mechanical. The parity review of IAM semantics and deploy-time behaviors is the architect's gate, and it is not optional.

Verification

  • Harness passesterraform validate and terraform plan both green, 6 resources, fully offline.
  • Trap 1 caughtthe Lambda-packaging trap logged with the failing gate named and a working offline fix.
  • Trap 2 caughtthe IAM drift logged from the policy diff, with the corrected statement/resource mapping shown.
  • Checklist completeall 6 rows carry an explicit parity verdict, not blanks.

Completion artifact

The iac-parity/ folder: translated stack, harness, parity-checklist.md, trap-notes.md, parity-report.md. Self-grading rubric (pass = 7/9):

  • Trap capture (0–4)2 per trap, with correct gate attribution.
  • Mechanical gates (0–2)offline harness runs clean end-to-end = 2.
  • IAM diff rigor (0–2)actual JSON side-by-side, statement- and resource-level = 2.
  • Report usefulness (0–1)a colleague could avoid both traps from your autopsies alone = 1.

Client-safe variant

Client IaC under a restrictive MSA: the harness, validate, plan, and policy diffing are all local OSS and see no LLM. For the translation step use the client-approved tenancy or a local coder model via Ollama, and pre-redact account IDs, bucket names, and CIDRs (structure, not values, drives translation quality). The parity report — with traps documented — is precisely the artifact that makes AI-assisted translation auditable under client governance.

Stuck?

  • init fails offline: the provider download needs internet once; everything after is local.
  • plan demands credentials: a skip_* flag was lost from provider.tf, or the translation added a data source that calls AWS — replace it with a literal.
  • The Lambda zip errors: write index.py next to the tf files and use data "archive_file" with source_file and output_path; it is entirely local.
  • Plan shows 5 resources: the Lambda permission is the one AIs most often drop — check for aws_lambda_permission.
  • The policy diff looks identical: diff the plan's rendered JSON, not the HCL — HCL that reads fine can still render merged statements.

Answer key (don't read before attempting)

Trap 1 — CFN-only intrinsic behavior: Code: ZipFile. CloudFormation zips inline code at deploy time; aws_lambda_function has no inline-code argument. AI translations typically hallucinate a zip_file/code argument (validate catches it) or point filename at a raw .py or a nonexistent zip (plan or apply catches it). Correct fix: extract the handler to index.py, package with data "archive_file" (type = "zip", source_file, output_path), and set filename + source_code_hash from it — fully offline.

Trap 2 — IAM policy drift on resource ARN shapes. The CFN policy is deliberately split: s3:ListBucket against the bucket ARN, and s3:GetObject/GetObjectVersion against arn…/* (objects). Translators commonly collapse this into one statement with a single resource — either everything on ${arn}/* (ListBucket silently stops working: it is a bucket-level action) or everything on the bucket ARN (object reads break) — or "helpfully" grant both actions on both resources (over-permissive). validate and plan pass in all three cases; only the human policy diff catches it. Correct fix: preserve two statements with the original action-to-ARN-shape mapping.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in