# Tf Snap

> Scan a cloud resource group (Azure) or tagged resources (AWS) as a blueprint and generate standardized Terraform code with CI/CD pipelines, backend storage, service credentials, and repo setup.

- Skill: `praneethvvs/tf-snap` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add praneethvvs/tf-snap`
- Raw SKILL.md: https://api.skillmd.com/api/skills/praneethvvs/tf-snap/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Praneethvvs (https://skillmd.com/u/praneethvvs)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/praneethvvs/tf-snap

---


# TF Snap

Scan a live cloud environment as a **read-only blueprint**, then generate brand-new Terraform code to recreate equivalent infrastructure. Also sets up CI/CD pipelines, backend storage, credentials, and a git repo — all from this single skill.

**Supports**: Azure (resource group) and AWS (tag-based discovery)

**Key principles**:
- No existing resources are imported. The POC environment is read-only reference material.
- ALL permissions and quotas are validated UPFRONT before any code generation.
- ALL questions are asked upfront — no follow-up questions mid-execution.
- ALL Terraform code is locally validated (`terraform plan`) before pushing.
- The skill runs autonomously — fix issues yourself, don't bug the user for obvious things.
- NEVER delete resources outside the target scope without explicit user confirmation.

**Immutable rules (NEVER violate)**:
1. **NEVER modify pipeline templates** — Pipeline YAML/buildspecs in template files are the source of truth. If a pipeline fails, diagnose the root cause (permissions, quotas, credentials config) and fix THAT. If the only fix requires a template change, STOP and ask the user: "Pipeline template needs modification — here's why and here are options."
2. **NEVER request quota increases** — If a quota is hit, present options: (a) downsize the resource, (b) use a different region/AZ, (c) remove the resource, (d) user requests increase themselves. Let the user choose.
3. **Exhaustive permission pre-flight** — Identify ALL permission/quota blockers BEFORE code generation. No surprises during pipeline runs.
4. **Handle ANY resource type** — Three-tier approach: Type Registry → Complex Templates → Dynamic Generation. `terraform plan` is the safety net.
5. **All questions upfront** — Gather ALL configuration via AskUserQuestion BEFORE starting any work. NEVER come back to ask more questions mid-execution.
6. **Problem anticipation** — After discovery and BEFORE code generation, run a comprehensive risk scan (Phase 3.5). Resolve ALL blockers before writing any .tf files.

**Autonomy rules**:
- Run autonomously for: code changes, git operations, terraform commands, AWS/Azure CLI commands obviously required for the project, SP/IAM role creation, git push.
- DO ask for confirmation on: anything that deletes resources, anything outside target scope, any permission escalation, any quota decisions, any pipeline template changes.

**Tool usage**:
- **Azure MCP server** → Azure resource discovery, storage operations, permissions checks
- **AWS CLI / aws-api MCP** → AWS tag discovery, resource details, IAM/quota checks
- **GitHub MCP server** → repo creation, environments, secrets (when GitHub Actions chosen)
- **Bash** → terraform, git, az CLI, aws CLI, gh CLI commands
- **Write** → generate all Terraform (.tf), tfvars, tfbackend, pipeline YAML files
- **AskUserQuestion** → gather configuration interactively (ALL questions batched upfront)

**Reference files by cloud/platform**:
- Azure resources + HCL: `azure/resource-mappings.md`, `azure/hcl-templates.md`
- AWS resources + HCL: `aws/resource-mappings.md`, `aws/hcl-templates.md`
- Azure DevOps pipelines: `azurepipelines/templates.md`, `azurepipelines/debugging.md`
- GitHub Actions (Azure + AWS): `githubactions/templates.md`

---

## Phase 0: Cloud & Blueprint Identification

**This is always the FIRST step.** Identify the cloud provider and the blueprint to scan.

### Step 0a: Detect cloud from arguments

If `$ARGUMENTS` is provided:
- Looks like an Azure resource group name (e.g., `rg-myproject-dev`) → default to Azure
- Looks like a tag value (e.g., `myproject` or `project=myproject`) → default to AWS
- Ambiguous → ask

### Step 0b: Ask cloud provider (1 question — do this before anything else)

```
AskUserQuestion:
  Q1: "Which cloud provider are you targeting?"
      Options: Azure | AWS
      (Skip if already determined from Step 0a)
```

Once cloud is known, all subsequent phases branch accordingly.

---

## Phase 1: Prerequisites & MCP Server Setup

### Step 1a: MCP Server Detection & Installation

Check if required MCP servers are available:

```bash
claude mcp list
```

**Required MCP servers by cloud:**

| MCP Server | Cloud | Purpose | Install Command |
|------------|-------|---------|-----------------|
| `azure-mcp` | Azure | Resource discovery, storage, permissions | `claude mcp add --scope project azure-mcp -- npx -y @azure/mcp@latest` |
| `aws-api` | AWS | Resource details, tag filtering | `claude mcp add --scope project aws-api -e AWS_REGION={region} -- uvx awslabs.aws-api-mcp-server@latest` |

If missing, install automatically. No need to ask per server.

### Step 1b: CLI & Tool Prerequisites

Run checks in parallel and present as a markdown table:

**Azure checks:**
| Check | Method | On Fail |
|-------|--------|---------|
| Azure login | `az account show` | "Run `az login`" |
| RG accessible | `az group show --name {rg}` | "Need Reader role on RG" |
| Terraform | `terraform version -json` | "Install from hashicorp.com/terraform/install" |
| Git | `git --version` | "Install git" |
| az devops ext | `az extension list \| grep azure-devops` | "Run: `az extension add --name azure-devops`" |
| gh CLI | `gh --version` | "Install from cli.github.com" (if GitHub Actions chosen) |

**AWS checks:**
| Check | Method | On Fail |
|-------|--------|---------|
| AWS credentials | `aws sts get-caller-identity` | "Run `aws configure` or set AWS_PROFILE" |
| AWS CLI version | `aws --version` | "Install from aws.amazon.com/cli" |
| Terraform | `terraform version -json` | "Install from hashicorp.com/terraform/install" |
| Git | `git --version` | "Install git" |
| gh CLI | `gh --version` | "Install from cli.github.com" (if GitHub Actions chosen) |
| aws tag permissions | `aws resourcegroupstaggingapi get-resources --resource-type-filters ec2:instance --max-results 1` | "Need ResourceGroupsTaggingAPI access" |

### Step 1c: Identity Check

**Azure:**
```bash
USER_ID=$(az ad signed-in-user show --query id -o tsv)
USER_EMAIL=$(az ad signed-in-user show --query userPrincipalName -o tsv)
SUBSCRIPTION=$(az account show --query '{id:id, name:name}' -o json)
```

**AWS:**
```bash
aws sts get-caller-identity --query '{AccountId:Account, UserId:UserId, Arn:Arn}' -o json
AWS_REGION=$(aws configure get region)
```

Present identity as confirmation to user.

---

## Phase 2: Interactive Config — ALL Questions Upfront

**CRITICAL**: Gather ALL configuration in ONE interaction. Use AskUserQuestion with multiple questions. Do NOT come back to ask more questions during execution.

The question batches below are organized by dependency. Ask Batch 1, then immediately ask Batches 2–5 together (they don't depend on discovery results).

### Batch 1 — Project Identity (ask for BOTH clouds)

- **Project name**: Full name, e.g., `tax_prediction`. Used in tags and descriptions.
- **Project prefix**: Short form, e.g., `taxpred`. Used in ALL resource names, file names, TF variable names. Max 8 chars, lowercase alphanumeric + hyphens.
- **Environments**: Which to create. Common: `dev`, `staging`, `prod`. Dev = no approval gate.
- **Author/Team email**: For tagging. Ask explicitly — do NOT auto-detect.

### Batch 2 — Blueprint Identifier (cloud-specific)

**Azure:**
- **Resource group to scan**: The POC blueprint RG. (May already be provided via `$ARGUMENTS`.)
- **Subscription**: Detect via `az account show`. Confirm with user.

**AWS:**
- **Tag key**: What tag key identifies the POC resources? (e.g., `Project`, `Environment`, `app`)
- **Tag value**: What tag value? (e.g., `tax-prediction`, `poc`)
- **AWS Region**: Which region are the POC resources in? (e.g., `us-east-1`)
- **AWS Account ID**: Confirm the account ID shown in Step 1c.

### Batch 3 — CI/CD Platform & Approvers

- **CI/CD platform**:
  - Azure: Azure DevOps or GitHub Actions
  - AWS: GitHub Actions
- **Approvers**: List of email addresses for environment approval gates. Applied to all non-dev environments.

**Azure DevOps additional questions:**
- ADO org URL (e.g., `https://dev.azure.com/cliexa`)
- ADO project name
- Agent pool name + type (hosted/self-hosted)

**GitHub Actions additional questions:**
- GitHub org or username
- Repo name (new repo will be created)
- Repo visibility (public/private)

### Batch 4 — State Backend (MANDATORY)

**Azure:**
- **Central storage account name**: Shared storage for ALL projects' state. User provides.
- **Central storage RG**: Which RG holds the state storage.
- **First project?**: If yes, offer to create the storage account.
- **Container name**: Default `{prefix}-tfstate`. Each project gets its own container.

**AWS:**
- **State S3 bucket name**: Default `st-{prefix}-{region_short}-tfstate`. User can override.
- **State DynamoDB table name**: Default `ddb-{prefix}-tfstate-lock`.
- **First project in this account?**: If yes, create the bucket + table. If no, they already exist.
- **Same region as resources?**: State bucket must be in the same region as the target resources.

### Batch 5 — Versions & Tags

- **Terraform version**: Default to latest stable. Ask if user has a constraint.
- **Provider version**:
  - Azure: azurerm version (default latest `~> 4.x`)
  - AWS: aws provider version (default latest `~> 5.x`)
- **Additional custom tags?**: `project_name`, `environment`, `author`, `created_date` are auto-added. Ask if any extra.

### Batch 6 — AWS IAM Auth Method (AWS only)

How should Terraform authenticate in CI/CD?

- **OIDC (Recommended)**: GitHub Actions assumes an IAM role via OIDC. No long-lived credentials. Set up OIDC trust policy on the Terraform IAM role.
- **IAM User + Access Keys**: Create an IAM user, generate access keys, store as secrets. Simpler but less secure.

**If OIDC chosen:**
- GitHub org/repo that will be granted OIDC access (e.g., `myorg/myproject-iac`)
- IAM role name for Terraform execution: default `role-{prefix}-terraform-{env}`

**If IAM User chosen:**
- IAM user name: default `svc-terraform-{prefix}`

---

## Phase 3: Resource Discovery

**This is read-only. The POC environment is never modified.**

### Azure Discovery

**Step 1**: List all resources in the RG:
```bash
az resource list --resource-group {rg_name} --query "[].{name:name, type:type, location:location, sku:sku}" -o table
```

**Step 2**: Sub-resource discovery — use the Sub-resource Discovery table in [azure/resource-mappings.md](azure/resource-mappings.md).

**Step 3**: Filter noise — use the Noise Filter table in [azure/resource-mappings.md](azure/resource-mappings.md).

**Step 4**: Cross-RG reference detection. For each resource, check for references to other RGs. If found, ask user: "Resource X references Y in RG Z. Create similar or reference existing?"

**Step 5**: Non-idempotent resource flagging — for each discovered resource, check against the **Non-Idempotent Resource Registry** in [azure/resource-mappings.md](azure/resource-mappings.md). For every match:

- Record the POC value (actual name, IP, DNS hostname, etc.) from the live resource
- Tag it as `NON-IDEMPOTENT` in the internal discovery record
- Do NOT resolve yet — collect all of them and surface them in Phase 3.5

```bash
# Read the actual current values that will need to change:

# Storage accounts — actual name + endpoint
az storage account list --resource-group {rg_name} \
  --query "[].{name:name, blobEndpoint:primaryEndpoints.blob}" -o table

# Public IPs — actual IP + DNS label
az network public-ip list --resource-group {rg_name} \
  --query "[].{name:name, ip:ipAddress, dns:dnsSettings.fqdn, allocation:publicIPAllocationMethod}" -o table

# Web apps / Function apps — actual default hostname
az webapp list --resource-group {rg_name} \
  --query "[].{name:name, kind:kind, defaultHostName:defaultHostName}" -o table

# Key Vaults — name + check soft-delete
az keyvault list --resource-group {rg_name} --query "[].{name:name, softDeleteEnabled:properties.enableSoftDelete}" -o table

# AKS — dns prefix + FQDN + network plugin (IMMUTABLE)
az aks list --resource-group {rg_name} \
  --query "[].{name:name, dnsPrefix:dnsPrefix, fqdn:fqdn, networkPlugin:networkProfile.networkPlugin}" -o table

# SQL Servers
az sql server list --resource-group {rg_name} \
  --query "[].{name:name, fqdn:fullyQualifiedDomainName}" -o table

# Redis
az redis list --resource-group {rg_name} \
  --query "[].{name:name, hostName:hostName}" -o table

# CosmosDB
az cosmosdb list --resource-group {rg_name} \
  --query "[].{name:name, endpoint:documentEndpoint, kind:kind}" -o table

# Service Bus / Event Hub namespaces
az servicebus namespace list --resource-group {rg_name} \
  --query "[].{name:name, serviceBusEndpoint:serviceBusEndpoint, sku:sku.name}" -o table
az eventhubs namespace list --resource-group {rg_name} \
  --query "[].{name:name, serviceBusEndpoint:serviceBusEndpoint, sku:sku.name}" -o table

```

### AWS Discovery

**Step 1**: Primary tag-based discovery:
```bash
# Discover all resources with the project tag
aws resourcegroupstaggingapi get-resources \
  --tag-filters Key={tag_key},Values={tag_value} \
  --query 'ResourceTagMappingList[].{ARN:ResourceARN}' \
  --output json

# Also discover VPC sub-resources, S3 buckets, and IAM roles
# that may not appear in the tagging API — see aws/resource-mappings.md Step 3
```

**Step 2**: Parse ARNs to identify resource types — see the Tag-Based Discovery section in [aws/resource-mappings.md](aws/resource-mappings.md).

**Step 3**: Sub-resource discovery — VPC subnets/route tables/security groups, S3 bucket configurations, EKS node groups, ECS services, etc. Use the Sub-resource Discovery table in [aws/resource-mappings.md](aws/resource-mappings.md).

**Step 4**: Filter noise — default VPC and its resources, network interfaces, snapshots, build artifacts. Use the Noise Filter table in [aws/resource-mappings.md](aws/resource-mappings.md).

**Step 5**: Cross-account/cross-region reference detection. Check VPC peerings, cross-region S3 replication, cross-account IAM roles. Ask user for each: "Create similar or reference existing?"

**Step 6**: Non-idempotent resource flagging. Cross-reference every discovered resource against the **Non-Idempotent Resource Registry** in [aws/resource-mappings.md](aws/resource-mappings.md). For each match:
- Capture the POC value using the detection command in the registry (bucket name, IP, DNS name, endpoint, OIDC URL, etc.)
- Tag it as `NON-IDEMPOTENT` in the internal discovery record
- Do NOT resolve yet — collect all of them and surface them in Phase 3.5 AWS Non-Idempotent Resolution

```bash
# Key capture commands during AWS discovery:

# S3 bucket names (globally unique)
aws s3api list-buckets --query 'Buckets[*].Name' --output text

# Elastic IPs
aws ec2 describe-addresses \
  --filters "Name=tag:{tag_key},Values={tag_value}" \
  --query 'Addresses[*].{IP:PublicIp,AllocationId:AllocationId,Name:Tags[?Key==`Name`].Value|[0]}'

# ALB/NLB DNS names
aws elbv2 describe-load-balancers \
  --query 'LoadBalancers[*].{Name:LoadBalancerName,DNS:DNSName,Type:Type}'

# CloudFront distributions
aws cloudfront list-distributions \
  --query 'DistributionList.Items[*].{Id:Id,Domain:DomainName,Origins:Origins.Items[*].DomainName}'

# EKS OIDC issuer URL (critical for IRSA trust policies)
for CLUSTER in {discovered_eks_clusters}; do
  aws eks describe-cluster --name $CLUSTER \
    --query 'cluster.{Name:name,OIDC:identity.oidc.issuer}' --output json
done

# RDS/Aurora endpoints
aws rds describe-db-instances \
  --query 'DBInstances[*].{Id:DBInstanceIdentifier,Endpoint:Endpoint.Address,Port:Endpoint.Port}'
aws rds describe-db-clusters \
  --query 'DBClusters[*].{Id:DBClusterIdentifier,Endpoint:Endpoint,Reader:ReaderEndpoint}'

# ElastiCache endpoints
aws elasticache describe-replication-groups \
  --query 'ReplicationGroups[*].{Id:ReplicationGroupId,Primary:NodeGroups[*].PrimaryEndpoint.Address}'

# Secrets Manager — check for soft-delete window
aws secretsmanager list-secrets \
  --filters Key=tag-key,Values={tag_key} Key=tag-value,Values={tag_value} \
  --query 'SecretList[*].{Name:Name,DeletedDate:DeletedDate}'

# ACM certificates
aws acm list-certificates \
  --query 'CertificateSummaryList[*].{ARN:CertificateArn,Domain:DomainName}'

# Route 53 hosted zones + NS records
aws route53 list-hosted-zones \
  --query 'HostedZones[*].{Name:Name,Id:Id}'
# Then for each zone: aws route53 list-resource-record-sets --hosted-zone-id {id} --query 'ResourceRecordSets[?Type==`NS`]'

# DynamoDB table schema (immutable keys)
for TABLE in {discovered_dynamodb_tables}; do
  aws dynamodb describe-table --table-name $TABLE \
    --query 'Table.{Name:TableName,HashKey:KeySchema[?KeyType==`HASH`].AttributeName|[0],RangeKey:KeySchema[?KeyType==`RANGE`].AttributeName|[0],BillingMode:BillingModeSummary.BillingMode}'
done

# ECS task definition network mode (immutable)
for FAMILY in {discovered_ecs_task_families}; do
  aws ecs describe-task-definition --task-definition $FAMILY \
    --query 'taskDefinition.{Family:family,NetworkMode:networkMode}'
done

# Cognito user pool attributes (immutable)
for POOL_ID in {discovered_cognito_pool_ids}; do
  aws cognito-idp describe-user-pool --user-pool-id $POOL_ID \
    --query 'UserPool.{Id:Id,AliasAttributes:AliasAttributes,UsernameAttributes:UsernameAttributes}'
done
```

### Present Discovery Results

Present all discovered resources as a markdown table **before proceeding**:

**Azure:**
| # | Name | Type | Location | SKU/Kind |
|---|------|------|----------|---------|

**AWS:**
| # | Name/ID | AWS Service | Resource Type | Region |
|---|---------|------------|--------------|--------|

Show the table and proceed (do NOT ask for confirmation at this step — proceed to Phase 3.5 immediately, then show the combined table with risk assessment).

---

## Phase 3.5: Problem Anticipation — Comprehensive Risk Scan

> **Added in v2.0.0**: This phase runs AFTER discovery and BEFORE code generation. It proactively identifies ALL blockers and risks so that `terraform apply` succeeds on the first try. Do NOT skip this phase.

**Run ALL checks below in parallel where possible. Present results as a unified risk table.**

### Risk Table Format

Present findings as:

| # | Resource | Risk Type | Severity | Status | Required Action |
|---|---------|-----------|----------|--------|----------------|
| 1 | EC2 (t3.xlarge) | vCPU Quota | BLOCKER | 8/8 used | Choose: downsize / different AZ / user requests increase |
| 2 | S3 bucket `my-app-prod` | Name Conflict | BLOCKER | Taken | Choose: rename or use existing as data source |
| 3 | VPC (10.0.0.0/16) | CIDR Conflict | WARNING | Overlaps with existing VPC | Verify subnets don't conflict |
| 4 | ALB | Subnets in 2 AZs | BLOCKER | Only 1 AZ detected | ALB requires subnets in ≥2 AZs |
| 5 | RDS (db.r5.large) | AZ Availability | WARNING | Not available in {AZ} | Check instance class availability |
| 6 | All | IAM Role Limit | OK | 45/1000 | No action needed |

**Severity levels:**
- **BLOCKER**: Will cause `terraform apply` to fail. MUST resolve before code gen.
- **WARNING**: May cause issues; user should be aware. Proceed with caution.
- **OK**: No issue. Include in table to show the check was done.

**Rule**: Do NOT proceed to Phase 4 (code generation) if any BLOCKER items are unresolved.

---

### Azure Risk Checks

**Permission checks:**
```bash
USER_ID=$(az ad signed-in-user show --query id -o tsv)

# All role assignments for current user
az role assignment list --assignee $USER_ID --all \
  --query "[].{role:roleDefinitionName, scope:scope, condition:condition}" -o json

# ABAC condition on User Access Administrator (CRITICAL — restricted UAA blocks role assignments in TF)
az role assignment list --assignee $USER_ID --all \
  --query "[?roleDefinitionName=='User Access Administrator'].{scope:scope, condition:condition}" -o json
# If condition is non-null → RESTRICTED UAA → document role assignments as manual steps, don't put in TF

# SP state storage access
az role assignment list --scope "/subscriptions/{sub_id}/resourceGroups/{state_rg}" --include-inherited \
  --query "[?principalType=='ServicePrincipal'].{principal:principalName, role:roleDefinitionName}"
```

**Quota checks (run per discovered resource type):**
```bash
# Public IP quota (most often hit in Azure)
az network list-usages --location {region} \
  --query "[?name.value=='PublicIPAddresses'].{used:currentValue, limit:limit}"

# VM vCPU family quota (map VM size to family first — see azure/resource-mappings.md Quota Commands)
az vm list-usage --location {region} \
  --query "[?contains(name.value, '{vm_family}')].{name:name.localizedValue, used:currentValue, limit:limit}"

# Soft-deleted Key Vaults (name conflicts)
az keyvault list-deleted --query "[?name=='{kv_name}']"

# Storage account count (250 limit)
az storage account list --query "length(@)"
```

**Azure-specific risks to check:**
- ABAC conditions on User Access Administrator
- Key Vault soft-delete name conflicts
- VM family vCPU quota per discovered VM size
- Public IP quota (default 20; POC often uses them all)
- SKU availability in target region (some SKUs not in all regions)
- Storage account name global uniqueness
- Cross-RG references (VNet peering, shared subnets, org-level Key Vaults)
- **Non-idempotent resources** (see dedicated section below)

---

### Azure Non-Idempotent Resource Resolution

> **Added in v2.0.1**: Non-idempotent resources collected in Phase 3 Step 5 are resolved HERE. This is a mandatory sub-step of Phase 3.5. Do NOT generate code for any non-idempotent resource until the user has confirmed or selected a resolution.

**Severity rules:**
- **BLOCKER** — if a globally unique name would conflict directly with the POC resource (same target subscription), or if an immutable property mismatch would cause apply failure
- **WARNING** — IP/DNS changes that require post-deploy external updates (DNS records, connection strings, allow-lists)
- **INFO** — identity client ID changes with no external dependencies (TF refs handle it automatically)

**Step 1: Present non-idempotent findings table**

For each non-idempotent resource discovered, present:

| # | Resource | Type | Non-Idempotent Aspect | POC Value | Severity | Proposed Resolution |
|---|---------|------|-----------------------|-----------|----------|---------------------|
| 1 | `st-myapp-dev` | Storage Account | Globally unique name | `stamyappdev.blob.core.windows.net` | BLOCKER | Auto-name: `st{prefix}{region}{env}` |
| 2 | `pip-web-prod` | Public IP | IP address not transferable | `20.123.45.67` | WARNING | New IP assigned on first apply; update DNS/firewall after |
| 3 | `app-myapp` | App Service | Azure-assigned hostname | `app-myapp.azurewebsites.net` | WARNING | New hostname: `app-{prefix}-{region}-{env}.azurewebsites.net`; update CNAME/OAuth URIs after |
| 4 | `kv-myapp` | Key Vault | Globally unique + soft-delete | `kv-myapp.vault.azure.net` | BLOCKER | Auto-name: `kv-{prefix}-{region_short}-{env}` |
| 5 | `aks-myapp` | AKS | Immutable `network_plugin=azure` | `azure` | INFO | Same value carried over — immutable, document in TF |
| 6 | `id-myapp` | Managed Identity | New client_id | `abc-123-...` | INFO | TF references handle this; verify no external OIDC bindings |

**Step 2: For each BLOCKER — ask user explicitly**

Use `AskUserQuestion` for each group of BLOCKERs. Batch them into one question call where possible:

```
Globally unique name conflicts:
  Resource: {poc_resource_name} ({type})
  POC hostname/endpoint: {poc_value}
  Why it can't be reused: {reason}
  Options:
    A) Use auto-naming: {auto_name} (Recommended)
    B) Provide a custom name
    C) Reuse same name (only if POC resource will be deleted before apply)
```

**Step 3: For each WARNING — document and proceed**

For IP address changes and DNS hostname changes:
1. Generate the Terraform code with the new auto-name
2. Add a prominent comment block in the .tf file header:

```hcl
# =============================================================================
# POST-APPLY MANUAL STEPS REQUIRED
# =============================================================================
# The following values CHANGE from the POC environment. After `terraform apply`
# completes, update these external references:
#
# [1] Public IP Address changed:
#     POC IP:  20.123.45.67
#     New IP:  Will be assigned on first apply. Get it with:
#              terraform output -raw {public_ip_output_name}
#     Update:  DNS A-records, firewall allow-lists, app configurations
#              that reference the POC IP address.
#
# [2] App Service hostname changed:
#     POC:     app-myapp.azurewebsites.net
#     New:     {new_hostname}.azurewebsites.net (after apply)
#     Update:  CNAME records, OAuth redirect URIs, API base URL configs
#
# [3] Storage endpoints changed:
#     POC:     stamyappdev.blob.core.windows.net
#     New:     {new_name}.blob.core.windows.net
#     Update:  All connection strings, SAS URIs, app configurations
# =============================================================================
```

3. Add a dedicated `POST_APPLY_CHECKLIST.md` to the repo root listing all manual steps. This file is the user's reference after first apply.

**Step 4: Immutable properties — embed in code**

For every immutable property discovered (AKS network plugin, Storage account kind, CosmosDB kind, etc.):
1. Write the value verbatim into the generated `variables.tf` with the comment `# IMMUTABLE — cannot be changed after creation without destroy/recreate`
2. Do NOT expose it as a variable — hardcode it or use a local with `# IMMUTABLE` comment
3. Add to `POST_APPLY_CHECKLIST.md` under "Architecture Constraints"

**Step 5: Email domain / certificate verification reminders**

If `azurerm_email_communication_service` or `azurerm_email_communication_service_domain` is discovered:
```
After apply, domain {domain} must be re-verified in the new environment.
This requires adding TXT/CNAME records to your DNS provider. The exact records
are found in: Azure Portal → Communication Services → {new_acs_name} → Email → Domains
```
Add this step to `POST_APPLY_CHECKLIST.md`.

---

### AWS Risk Checks

**Run ALL of these after discovery:**

```bash
REGION="{aws_region}"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account -o text)

# 1. EC2 vCPU quota (On-Demand Standard — applies to most instance types)
VCPU_LIMIT=$(aws service-quotas get-service-quota --service-code ec2 --quota-code L-1216C47A --query 'Quota.Value' -o text)
echo "EC2 vCPU limit: $VCPU_LIMIT"

# 2. VPC count vs limit
VPC_LIMIT=$(aws service-quotas get-service-quota --service-code vpc --quota-code L-F678F1CE --query 'Quota.Value' -o text)
VPC_USED=$(aws ec2 describe-vpcs --query 'length(Vpcs)' -o text)
echo "VPCs: $VPC_USED / $VPC_LIMIT"

# 3. Elastic IP count vs limit
EIP_LIMIT=$(aws service-quotas get-service-quota --service-code ec2 --quota-code L-0263D0A3 --query 'Quota.Value' -o text)
EIP_USED=$(aws ec2 describe-addresses --query 'length(Addresses)' -o text)
echo "Elastic IPs: $EIP_USED / $EIP_LIMIT"

# 4. S3 bucket name check (for each generated bucket name)
aws s3api head-bucket --bucket {bucket_name} 2>&1
# 404 = available, 403 = taken by another account, 200 = already yours

# 5. Secrets Manager — soft-deleted secrets block name reuse
aws secretsmanager list-secrets --include-planned-deletion \
  --query 'SecretList[?DeletedDate!=null].{Name:Name, DeletionDate:DeletedDate}'

# 6. IAM role limit
IAM_LIMIT=$(aws service-quotas get-service-quota --service-code iam --quota-code L-FE177D64 --query 'Quota.Value' -o text)
IAM_USED=$(aws iam get-account-summary --query 'SummaryMap.Roles' -o text)
echo "IAM Roles: $IAM_USED / $IAM_LIMIT"

# 7. ALB count vs limit (if ALB discovered)
ALB_LIMIT=$(aws service-quotas get-service-quota --service-code elasticloadbalancing --quota-code L-53DA6B97 --query 'Quota.Value' -o text)
ALB_USED=$(aws elbv2 describe-load-balancers --query 'length(LoadBalancers)' -o text)
echo "ALBs: $ALB_USED / $ALB_LIMIT"

# 8. Lambda concurrency (if Lambda discovered)
aws lambda get-account-settings --query 'ConcurrentExecutions'

# 9. RDS quota (if RDS discovered)
aws rds describe-account-attributes \
  --query 'AccountQuotas[?AccountQuotaName==`DBInstances`].{Used:Used, Max:Max}'

# 10. VPC CIDR conflicts (if creating new VPC)
EXISTING_CIDRS=$(aws ec2 describe-vpcs --query 'Vpcs[].CidrBlock' -o text)
echo "Existing VPC CIDRs in region: $EXISTING_CIDRS"
# Manually verify the POC VPC CIDR doesn't conflict with existing VPCs

# 11. AZ availability for discovered instance types
for INSTANCE_TYPE in {discovered_instance_types}; do
  aws ec2 describe-instance-type-offerings \
    --location-type availability-zone \
    --filters "Name=instance-type,Values=$INSTANCE_TYPE" \
    --query 'InstanceTypeOfferings[].Location' -o text
done

# 12. EKS Kubernetes version support in region (if EKS discovered)
aws eks describe-addon-versions --query 'addons[0].addonVersions[0].compatibilities[*].clusterVersion' 2>/dev/null | head -5

# 13. Service availability check (Bedrock, SageMaker, etc.)
# For Bedrock: verify models available in target region
aws bedrock list-foundation-models --query 'modelSummaries[*].modelId' 2>/dev/null | head -10
```

**AWS-specific risks to anticipate:**

| Risk | What to Check | Common Blocker |
|------|--------------|----------------|
| EC2 vCPU quota | `service-quotas get-service-quota L-1216C47A` | Default 32 vCPUs often exhausted |
| VPC limit | `describe-vpcs \| length` vs quota | Default 5 per region |
| Elastic IP limit | `describe-addresses \| length` vs quota | Default 5 per region |
| S3 bucket name | `head-bucket` check | Globally unique — 3-char conflict = taken |
| ALB subnet AZs | ALB requires ≥2 subnets in different AZs | Only 1 AZ subnet discovered |
| EKS version | EKS version must be supported in region | Old K8s versions retired |
| RDS Multi-AZ | Requires ≥2 subnets in different AZs in subnet group | Only 1 AZ subnet |
| Aurora instances | All AZs must have same instance class | AZ-specific availability |
| IAM role limit | Account-wide 1000 roles limit | Orgs can hit this |
| CloudFront ACM | ACM cert must be in us-east-1 | Creating in wrong region |
| Secrets name reuse | Deleted secrets block same name for recovery window | 7–30 day hold |
| OIDC thumbprint | TLS provider needed for EKS OIDC | Missing `tls` provider in versions.tf |
| GitHub Actions OIDC | OIDC provider must exist in account | Create if missing via `aws iam create-open-id-connect-provider` |

**For each BLOCKER found:**
1. Show the exact quota/conflict
2. Present options as a table
3. Wait for user to select resolution
4. Update the resource plan accordingly (rename, resize, remove, or change AZ)

**For each WARNING found:**
- Document in the generated code as a comment
- Proceed but flag it in the final summary

---

### AWS Non-Idempotent Resource Resolution

> **Added in v2.0.2**: Non-idempotent resources collected in Phase 3 Step 6 are resolved HERE. This is a mandatory sub-step of Phase 3.5 for AWS environments. Do NOT generate code for any non-idempotent resource until the user has confirmed or selected a resolution.

**Severity rules:**
- **BLOCKER** — code generation cannot proceed without user decision: globally unique name conflicts (S3 bucket already exists in same account), EKS OIDC issuer URL (must be updated in all IRSA trust policies), ACM certificate requiring re-validation on a domain you control, Secrets Manager name in soft-delete recovery window
- **WARNING** — proceed but user must act after apply: Elastic IP changes, ALB/NLB/API GW/CloudFront DNS name changes, RDS/ElastiCache/MSK/OpenSearch endpoint changes, Route 53 NS record changes, KMS key ARN changes
- **INFO** — note in comments, no action required: ECR URI format (same account = same URI, different account = updated in code)

**Step 1: Build the findings table**

Present all flagged non-idempotent resources in a single table before asking any questions:

```
## AWS Non-Idempotent Resource Findings

| Severity | Resource | POC Value | Issue | Proposed Resolution |
|----------|----------|-----------|-------|---------------------|
| BLOCKER  | S3 `my-app-data` | `my-app-data` | Bucket name taken in same account | Rename to `my-app-data-prod` |
| BLOCKER  | ACM cert `*.example.com` | `arn:aws:acm:...` | New cert requires DNS re-validation | Add CNAME record after apply (in POST_APPLY_CHECKLIST.md) |
| WARNING  | ALB `app-alb` | `app-alb-abc123.us-east-1.elb.amazonaws.com` | DNS name changes on recreation | Update Route 53 Alias record after apply |
| WARNING  | EIP `nat-eip` | `54.12.34.56` | Cannot reserve same IP | Update firewall allowlists after apply |
| WARNING  | RDS `app-db` endpoint | `app-db.xyz.us-east-1.rds.amazonaws.com` | Endpoint changes | Update app connection strings after apply |
| INFO     | ECR `app-repo` | `123456789.dkr.ecr.us-east-1.amazonaws.com/app` | Same account+region = same URI | No action needed |
```

**Step 2: Resolve BLOCKERs — use AskUserQuestion**

For each BLOCKER:

*S3 bucket name conflicts (already exists in the same account):*
```
AskUserQuestion: "S3 bucket `{poc_name}` already exists in your account.
  Options:
  (a) Auto-rename: `{poc_name}-{env}` (e.g. `my-app-data-prod`)
  (b) Auto-rename: `{poc_name}-{short_uuid}` (e.g. `my-app-data-a3f2`)
  (c) I'll provide a name manually"
```
→ If (c): collect the name, continue.
→ If (a) or (b): generate the new name, continue.

*ACM certificate re-validation:*
```
ACM requires DNS validation CNAME records to be added to your DNS provider for new certificates.
This is a mandatory manual step after terraform apply. Adding to POST_APPLY_CHECKLIST.md.
→ No user decision needed — just inform and add to checklist.
```

*Secrets Manager name in recovery window:*
```
AskUserQuestion: "Secret `{name}` is in soft-delete recovery window (deleted on {date}, available on {available_date}).
  Options:
  (a) Auto-rename: `{name}-{env}`
  (b) Force delete the old secret now: `aws secretsmanager delete-secret --secret-id {name} --force-delete-without-recovery`
  (c) Wait until {available_date} (if within a few days)"
```

*EKS OIDC URL in IAM trust policies:*
```
The new EKS cluster will have a different OIDC issuer URL.
All IAM role trust policies using IRSA must reference the NEW cluster's OIDC URL.
→ Auto-update: generated IAM role trust policies use `data.aws_eks_cluster.{name}.identity[0].oidc[0].issuer`
   as a Terraform data reference, not the hardcoded POC URL.
→ No user decision needed — auto-fix in generated code.
```

**Step 3: For each WARNING — document and generate POST_APPLY_CHECKLIST.md**

Elastic IP / NAT Gateway IP changes:
```
# In generated code comment:
# WARNING: NAT Gateway EIP will be a NEW IP address (POC was {poc_ip}).
# If this IP is in any firewall allowlists or DNS A records, update them after apply.
```

ALB/NLB/API GW/CloudFront DNS name changes:
```
# In generated code comment:
# WARNING: DNS name will change on recreation. POC: {poc_dns}
# Update Route 53 records, CNAME entries, and any hardcoded URLs after apply.
```

Generate `POST_APPLY_CHECKLIST.md` in the repo root with **all** WARNING and BLOCKER manual steps:

```markdown
# Post-Apply Checklist — {project_name}

Generated by tf-snap. Complete these steps after first `terraform apply` succeeds.

## DNS Updates

- [ ] **ALB `{name}`**: Update Route 53 Alias/CNAME from `{poc_dns}` to `{new_dns}`
  - Get new DNS: `aws elbv2 describe-load-balancers --names {name} --query 'LoadBalancers[*].DNSName'`
- [ ] **CloudFront `{id}`**: Update all CNAME records from `{poc_cloudfront_domain}` to new CloudFront domain
  - Get new domain: `aws cloudfront get-distribution --id {id} --query 'Distribution.DomainName'`
- [ ] **API Gateway**: Update all client configurations from `{poc_endpoint}` to new endpoint
  - Get new endpoint: `aws apigatewayv2 get-apis --query 'Items[?Name==`{name}`].ApiEndpoint'`
- [ ] **Route 53 hosted zone `{domain}`**: Update domain registrar NS records to new nameservers
  - Get new NS: `aws route53 list-resource-record-sets --hosted-zone-id {id} --query 'ResourceRecordSets[?Type==`NS`].ResourceRecords[*].Value'`

## Connection String / Endpoint Updates

- [ ] **RDS `{id}`**: Update app connection string from `{poc_endpoint}` to new endpoint
  - Get new endpoint: `aws rds describe-db-instances --db-instance-identifier {id} --query 'DBInstances[*].Endpoint.Address'`
- [ ] **ElastiCache `{id}`**: Update cache connection string to new endpoint
- [ ] **MSK `{name}`**: Update Kafka producer/consumer configs to new broker addresses
- [ ] **OpenSearch `{name}`**: Update Kibana/app endpoints to new domain endpoint

## Firewall / Allowlist Updates

- [ ] **EIP / NAT Gateway**: New IP allocated (POC was `{poc_ip}`). Update any firewall allowlists, on-prem rules, or partner IP whitelists.

## Certificate Validation

- [ ] **ACM cert for `{domain}`**: Add validation CNAME record to DNS provider
  - Get CNAME: `aws acm describe-certificate --certificate-arn {arn} --query 'Certificate.DomainValidationOptions[*].ResourceRecord'`
  - Certificate will not be issued until this record propagates

## Architecture Constraints (Immutable Properties)

These values are hardcoded in Terraform and cannot be changed without destroying and recreating the resource:
- DynamoDB table `{name}`: hash_key = `{value}`, range_key = `{value}` — changing requires table recreation and data migration
- SQS FIFO queue `{name}`: name ends in `.fifo` — changing requires recreation
- ECS task definition `{family}`: network_mode = `{value}` — changing requires new task definition family
- Cognito User Pool `{id}`: alias_attributes = `{values}` — cannot be changed after creation
```

**Step 4: Immutable properties — embed in code**

For each resource with an immutable property (DynamoDB keys, SQS FIFO suffix, ECS network_mode, Cognito alias attributes):
1. Write the value verbatim in the generated HCL with `# IMMUTABLE — cannot be changed after creation without destroy/recreate`
2. Do NOT expose as a variable — hardcode it or use a `locals` value with `# IMMUTABLE` comment
3. Add to `POST_APPLY_CHECKLIST.md` under "Architecture Constraints"

**Step 5: EKS OIDC — auto-fix in generated IAM roles**

Never hardcode the POC OIDC issuer URL in IAM trust policies. Always generate:
```hcl
data "aws_eks_cluster" "{name}" {
  name = aws_eks_cluster.{name}.name
}

resource "aws_iam_role" "{service}_irsa" {
  assume_role_policy = jsonencode({
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(data.aws_eks_cluster.{name}.identity[0].oidc[0].issuer, "https://", "")}" }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = { StringEquals = { "${replace(data.aws_eks_cluster.{name}.identity[0].oidc[0].issuer, "https://", "")}:sub" = "system:serviceaccount:{namespace}:{sa_name}" } }
    }]
  })
}
```
This ensures the trust policy always uses the actual cluster OIDC URL, never the POC URL.

---

## Phase 4: Terraform Code Generation

Consult the cloud-specific reference files:

| Cloud | Resource Registry | HCL Templates |
|-------|-----------------|---------------|
| Azure | [azure/resource-mappings.md](azure/resource-mappings.md) | [azure/hcl-templates.md](azure/hcl-templates.md) |
| AWS | [aws/resource-mappings.md](aws/resource-mappings.md) | [aws/hcl-templates.md](aws/hcl-templates.md) |

### Code Generation Decision Tree (same for both clouds)

For EACH discovered resource, follow this sequence:

```
1. Look up type in the cloud-specific resource-mappings.md Type Registry
   → Get: TF resource type, category, "Deep Template?" column

2. Is "Deep Template?" = YES?
   → YES: Go to cloud-specific hcl-templates.md Section 3, find template, use it
   → NO:  Go to step 3

3. Dynamic generation:
   a. Read full resource via cloud CLI (az resource show / aws describe-*)
   b. Follo

…(truncated)
