# Terraform Modules with AI: IaC for Startups Without a DevOps Engineer

> How to generate Terraform modules using AI: prompts for AWS and GCP, state management, project structure, best practices. Production-ready examples.
> Author: Roman Belov · Published: 2026-07-12 · Source: https://futurecraft.pro/blog/terraform-modules-ai/

Every resource created by clicking through a cloud provider's web console can't be reproduced, rolled back, or handed off to a new developer. When the team reaches five people, that infrastructure debt starts slowing down releases.

Terraform fixes this: all infrastructure lives in code, stored in Git, applied with a single command. But startups don't have a DevOps engineer to write modules from scratch. AI models fill that gap. One structured prompt generates a Terraform module that passes `terraform validate` and handles most typical scenarios.

This article goes from zero to working infrastructure. Prompts for generating modules, examples for AWS and GCP, state management, security, and patterns that prevent the most common mistakes.

## Terraform in 10 Minutes: The Minimum You Need

Terraform works on three concepts: providers, resources, and state.

A **provider** connects Terraform to a cloud platform. AWS, GCP, Azure, Cloudflare — each has its own provider. It knows the platform's API and translates HCL into API calls.

A **resource** describes one infrastructure object: a virtual machine, a database, a DNS record, an S3 bucket. Resources reference each other — a server uses a subnet, a subnet belongs to a VPC.

**State** records the current condition of your infrastructure. Terraform compares it against the code and calculates a diff: what to create, what to change, what to delete. Without state, Terraform has no idea which resources already exist.

Minimal project structure:

```
infrastructure/
├── main.tf          # Resources
├── variables.tf     # Input parameters
├── outputs.tf       # Output values
├── providers.tf     # Provider configuration
└── terraform.tfvars # Variable values (don't commit secrets)
```

The working cycle consists of four commands:

```bash
terraform init      # Downloads providers
terraform plan      # Shows what will change
terraform apply     # Applies changes
terraform destroy   # Deletes everything (use with caution)
```

`terraform plan` is the most important command. It shows the diff between code and live infrastructure before anything changes. Always review the plan before applying.

## Modules: Reusable Infrastructure Blocks

A Terraform module is a directory of `.tf` files that other code calls with parameters. Same principle as a function: inputs, logic, outputs.

Module structure:

```
modules/
└── vpc/
    ├── main.tf        # VPC resources, subnets, routing
    ├── variables.tf   # Parameters: CIDR, number of AZs, tags
    └── outputs.tf     # VPC ID, subnet IDs
```

Calling a module from the root config:

```hcl
module "network" {
  source = "./modules/vpc"

  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b"]
  project_name       = "myapp"
  environment        = "production"
}
```

Modules solve three problems at once. Write one module and call it for every environment — dev, staging, production — with different parameters. It hides complexity: calling code passes five parameters instead of wiring up twenty linked resources. And it can be tested on its own, separate from the rest of the infrastructure.

## Prompts for Generating Terraform Modules

An AI model generates more accurate Terraform code when the context is specific. A good prompt has six elements:

1. **Cloud provider and region** — AWS us-east-1, GCP europe-west1
2. **What to create** — specific resources and relationships between them
3. **Parameterization** — what should be a variable versus hardcoded
4. **Security** — network restrictions, IAM roles, encryption
5. **Tags and naming** — naming convention, required tags
6. **Constraints** — Terraform version, provider version, prohibited resources

Base prompt template:

```
Generate a Terraform module for [cloud provider].

Resources:
- [list of resources with key parameters]

Requirements:
- Terraform >= 1.5, provider [name] >= [version]
- All resources in [region]
- Variables: [what to parameterize]
- Outputs: [what to export]
- Security: [specific constraints]
- Tags: project, environment, managed_by = "terraform"

Structure:
- main.tf, variables.tf, outputs.tf, versions.tf
- Descriptions for each variable
- Validation for critical variables (CIDR, names)

Do not use: [prohibited patterns]
```

Skip the security section and you get resources with `0.0.0.0/0` in security groups and no encryption. Skip version constraints and the code breaks on the next provider update. Each section blocks a specific class of errors.

## Example: AWS VPC + ECS Fargate

Prompt for generating a full application stack on AWS:

```
Generate a Terraform module: AWS VPC with ECS Fargate.

Resources:
- VPC with 2 public and 2 private subnets in different AZs
- NAT Gateway (one, for cost savings)
- ECS Cluster with Fargate capacity provider
- ECS Service + Task Definition for a container
- Application Load Balancer in public subnets
- Security Groups: ALB accepts 80/443, ECS accepts only from ALB
- CloudWatch Log Group for the container

Variables:
- vpc_cidr (default "10.0.0.0/16", with CIDR format validation)
- container_image (string, no default)
- container_port (number, default 8080)
- cpu, memory (number, defaults 256/512)
- desired_count (number, default 2)
- environment (string, validation: dev|staging|production)
- project_name (string)

Outputs:
- vpc_id, public_subnet_ids, private_subnet_ids
- alb_dns_name, ecs_cluster_arn, ecs_service_name

Terraform >= 1.5, AWS provider >= 5.0.
All resources in aws_region variable (default us-east-1).
Tags: Project, Environment, ManagedBy = "terraform".
```

The output is 150–200 lines of HCL. The AI builds correct resource relationships: the ECS task definition references the log group, the ECS service security group accepts traffic only from the ALB security group, and private subnets route outbound through the NAT Gateway.

Check these two things in the generated code:

```hcl
# ECS security group — traffic only from ALB
resource "aws_security_group_rule" "ecs_ingress" {
  type                     = "ingress"
  from_port                = var.container_port
  to_port                  = var.container_port
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.alb.id
  security_group_id        = aws_security_group.ecs.id
}

# Task definition — resource limits explicitly set
resource "aws_ecs_task_definition" "app" {
  family                   = "${var.project_name}-${var.environment}"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = var.cpu
  memory                   = var.memory
  execution_role_arn       = aws_iam_role.ecs_execution.arn

  container_definitions = jsonencode([{
    name      = var.project_name
    image     = var.container_image
    essential = true
    portMappings = [{
      containerPort = var.container_port
      protocol      = "tcp"
    }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.app.name
        "awslogs-region"        = var.aws_region
        "awslogs-stream-prefix" = "ecs"
      }
    }
  }])
}
```

If the AI missed `execution_role_arn` in the task definition or skipped the IAM role with `AmazonECSTaskExecutionRolePolicy`, the container won't start. This is the most common generation error — catch it during review.

## Example: GCP Cloud Run + Cloud SQL

The same approach for Google Cloud:

```
Generate a Terraform module: GCP Cloud Run + Cloud SQL PostgreSQL.

Resources:
- VPC with private subnet
- Cloud SQL PostgreSQL 15 (db-f1-micro for dev, db-custom for prod)
- Private IP for Cloud SQL via VPC peering
- Cloud Run service connected to SQL via VPC connector
- Serverless VPC Connector
- Secret Manager for database password
- IAM bindings: Cloud Run SA with roles cloudsql.client, secretmanager.secretAccessor

Variables:
- project_id (string)
- region (default "europe-west1")
- database_name (string)
- cloud_run_image (string)
- cloud_run_cpu, cloud_run_memory (defaults "1", "512Mi")
- min_instances, max_instances (defaults 0, 10)
- environment (validation: dev|staging|production)

Outputs:
- cloud_run_url, cloud_sql_connection_name
- vpc_connector_id, database_ip

Terraform >= 1.5, Google provider >= 5.0.
Enable required APIs via google_project_service.
```

GCP requires explicitly enabling APIs (`compute.googleapis.com`, `run.googleapis.com`, `sqladmin.googleapis.com`). AI models frequently skip this. Adding "Enable required APIs via google_project_service" to the prompt makes the model generate these blocks:

```hcl
resource "google_project_service" "required" {
  for_each = toset([
    "compute.googleapis.com",
    "run.googleapis.com",
    "sqladmin.googleapis.com",
    "vpcaccess.googleapis.com",
    "secretmanager.googleapis.com",
  ])

  project = var.project_id
  service = each.value

  disable_dependent_services = false
  disable_on_destroy         = false
}
```

`disable_on_destroy = false` matters. Without it, `terraform destroy` disables the APIs and can break other resources in the project that this code doesn't manage.

## State Management: Remote Backend

By default, Terraform stores state in a local `terraform.tfstate` file. That doesn't work for a team — state needs to be shared and protected from concurrent writes.

Remote backend for AWS (S3 with native locking):

```hcl
terraform {
  backend "s3" {
    bucket       = "mycompany-terraform-state"
    key          = "production/infrastructure.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true  # S3-native locking, Terraform 1.11+
  }
}
```

Remote backend for GCP (GCS):

```hcl
terraform {
  backend "gcs" {
    bucket = "mycompany-terraform-state"
    prefix = "production/infrastructure"
  }
}
```

Prompt for generating backend infrastructure:

```
Generate Terraform code to create a remote backend.

AWS variant:
- S3 bucket with versioning and server-side encryption (AES256)
- Bucket policy: prohibit object deletion
- Lifecycle rule: retain noncurrent versions for 90 days

State locking is native to S3 via use_lockfile — no separate DynamoDB table needed.
Do not use a backend block in this code (chicken-and-egg problem).
State for this code is stored locally.
```

One nuance: the infrastructure that stores state is a separate Terraform project with local state. That solves the chicken-and-egg problem. AI models sometimes add a `backend` block to the very code that creates the state bucket. An explicit prohibition in the prompt stops this.

## State: Security and Operations

The state file contains secrets. Database passwords, API keys, private IPs — all end up in plaintext. Three rules:

**Encryption.** S3 backend supports `encrypt = true`. GCS encrypts objects by default. For stronger protection, swap managed encryption for a KMS key.

**Access.** Lock down the state bucket. Only the CI/CD pipeline and administrators should have access. Terraform Cloud and HCP Terraform handle this out of the box.

**Locking.** S3 locks state natively via `use_lockfile = true` (Terraform 1.11+). The older DynamoDB-based locking is now deprecated and slated for removal. GCS locks by default. Without locking, two simultaneous `terraform apply` runs can corrupt the state file.

State operations you'll need:

```bash
# View current resources in state
terraform state list

# Move a resource (during module refactoring)
terraform state mv 'module.old.aws_instance.web' 'module.new.aws_instance.web'

# Import an existing resource (already created manually)
terraform import 'aws_s3_bucket.data' 'my-existing-bucket'

# Remove a resource from management (without deleting it)
terraform state rm 'aws_instance.temporary'
```

`terraform import` is especially useful for startups that built their infrastructure through the console. Instead of recreating everything, import existing resources into state and keep managing them as code from that point forward.

## Prompts for Iterative Module Improvement

The first version of an AI-generated module covers the basics. Follow-up prompts add production hardening:

**Add monitoring:**

```
To the existing ECS Fargate module, add:
- CloudWatch alarms: CPU > 80%, Memory > 80%, UnhealthyHostCount > 0
- SNS Topic for notifications (email subscription via variable)
- Dashboard with CPU, Memory, Request Count, Response Time charts

Alarm actions point to the SNS topic.
Do not modify existing resources.
```

**Add autoscaling:**

```
To the ECS Service, add Application Auto Scaling:
- Target tracking on CPU utilization (target 70%)
- Target tracking on Memory utilization (target 75%)
- Scale-in cooldown 300s, scale-out cooldown 60s
- Min capacity from variable, max capacity from variable

Use aws_appautoscaling_target and aws_appautoscaling_policy.
```

**Add DNS and HTTPS:**

```
To the existing ALB, add:
- Route53 record (A alias) for domain from variable
- ACM certificate with DNS validation
- HTTPS listener (443) on ALB
- HTTP → HTTPS redirect
- Route53 validation records for ACM

Variables: domain_name, route53_zone_id.
```

Each prompt adds one feature. Easier to review and debug than generating everything at once. AI models handle incremental changes better when the context stays tight.

## Project Structure for Multiple Environments

A startup starts with one environment and quickly needs three: dev, staging, production. Terraform modules let you reuse the same code across all of them.

Recommended structure:

```
infrastructure/
├── modules/
│   ├── networking/    # VPC, subnets, NAT
│   ├── compute/       # ECS/Cloud Run, task definitions
│   ├── database/      # RDS/Cloud SQL
│   └── monitoring/    # CloudWatch/Cloud Monitoring
├── environments/
│   ├── dev/
│   │   ├── main.tf        # Module calls
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   └── ...
│   └── production/
│       └── ...
└── global/
    ├── iam/           # IAM roles, policies
    └── dns/           # Route53 zones
```

Each environment calls the same modules with different parameter values:

```hcl
# environments/dev/main.tf
module "network" {
  source = "../../modules/networking"

  cidr_block    = "10.0.0.0/16"
  nat_gateway   = false  # Save costs in dev
  project_name  = "myapp"
  environment   = "dev"
}

module "compute" {
  source = "../../modules/compute"

  vpc_id            = module.network.vpc_id
  subnet_ids        = module.network.private_subnet_ids
  container_image   = "myapp:latest"
  desired_count     = 1       # Minimum in dev
  cpu               = 256
  memory            = 512
  environment       = "dev"
}
```

```hcl
# environments/production/main.tf
module "network" {
  source = "../../modules/networking"

  cidr_block    = "10.1.0.0/16"
  nat_gateway   = true   # Required in production
  project_name  = "myapp"
  environment   = "production"
}

module "compute" {
  source = "../../modules/compute"

  vpc_id            = module.network.vpc_id
  subnet_ids        = module.network.private_subnet_ids
  container_image   = "myapp:v1.2.3"
  desired_count     = 3       # Redundancy
  cpu               = 1024
  memory            = 2048
  environment       = "production"
}
```

Dev skips the NAT Gateway ($32/month saved) and runs one container. Production gets NAT, three containers, and bigger resources. Same code, different parameters.

## Validating Generated Code

AI generates syntactically correct HCL most of the time. The remaining edge cases surface during validation. Three levels of checks:

**Level 1: Terraform CLI**

```bash
terraform init
terraform validate
terraform plan
```

`validate` checks syntax and resource references. `plan` checks that the provider accepts your parameters. If `plan` returns an error, paste the message back to the AI with "Fix this error in the Terraform code" and attach the full file.

**Level 2: tflint**

```bash
# Installation
brew install tflint    # macOS
tflint --init          # Download plugins

# Run
tflint --recursive
```

tflint catches things `terraform validate` misses: deprecated instance types, non-existent AMIs, suboptimal configurations. Fix prompt: "tflint outputs warnings [list]. Fix the module while preserving functionality."

**Level 3: checkov (security)**

```bash
pip install checkov
checkov -d .
```

checkov checks configuration against security best practices: encryption enabled, public access blocked, logging configured. Common findings after AI generation:

- S3 bucket missing `block_public_access`
- Security group with `0.0.0.0/0` on ingress
- RDS without `storage_encrypted = true`
- CloudWatch log group without `retention_in_days`

Turn each finding into a prompt: "Add an aws_s3_bucket_public_access_block resource to the S3 bucket with block_public_acls = true, block_public_policy = true, ignore_public_acls = true, restrict_public_buckets = true."

## Best Practices for Terraform with AI

**Provider versions.** Pin versions in `versions.tf`:

```hcl
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
```

`~> 5.0` allows `5.x` but blocks `6.0`. Without pinning, `terraform init` may pull in a new major version and break existing code.

**Naming.** Enforce a consistent convention. The prompt "All resource names in the format {project}-{environment}-{resource}" makes the AI generate predictable names instead of arbitrary ones.

**Don't hardcode AMIs and machine types.** Use data sources:

```hcl
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}
```

A hardcoded AMI ID is region-specific and goes stale within months. A data source always picks the current image.

**Run `terraform plan` in CI/CD.** Every pull request with infrastructure changes should trigger `terraform plan` and post the diff as a comment. A detailed guide on wiring this up is in the [article on generating CI/CD pipelines](/blog/cicd-pipeline-generator/).

**Never commit tfvars with secrets.** Your `.gitignore` should include:

```
*.tfvars
!terraform.tfvars.example
.terraform/
*.tfstate
*.tfstate.backup
```

Pass secrets through environment variables (`TF_VAR_database_password`) or through the provider's secret manager.

**Separate state by environment.** One state file per environment. Dev and production should never share state. A mistake in dev should have no path to production resources.

## Common AI Generation Mistakes and How to Prevent Them

AI models make predictable mistakes in Terraform code. Once you know the patterns, you can add preventive instructions to the prompt.

**Circular dependencies.** The AI creates security group A that references security group B, which references A. Terraform throws a cycle error. Fix: split rules into separate resources instead of inline `ingress`/`egress` blocks — in AWS provider 5.0+ that's `aws_vpc_security_group_ingress_rule` and `aws_vpc_security_group_egress_rule` (one rule per resource; the older `aws_security_group_rule` still works, but the newer resources are preferred). Add to the prompt: "Security group rules via separate resources, not inline."

**Forgotten depends_on.** Some resource dependencies aren't automatically inferred by Terraform. For example, an IAM role policy attachment must exist before the ECS task, but Terraform can't see that relationship through `jsonencode`. Add: "Use depends_on for implicit dependencies: IAM policy attachments → compute resources."

**Excessive permissions.** AI frequently outputs `Action: "*"` and `Resource: "*"` in IAM policies. Add to the prompt: "IAM policies with least privilege. Specific Actions and Resources. No wildcards except where unavoidable."

**Missing lifecycle rules.** AI creates resources without `prevent_destroy` on critical data and without `create_before_destroy` on resources that need zero-downtime replacement:

```hcl
resource "aws_db_instance" "main" {
  # ...
  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_ecs_service" "app" {
  # ...
  lifecycle {
    create_before_destroy = true
  }
}
```

Add to the prompt: "lifecycle: prevent_destroy for databases and state buckets. create_before_destroy for compute resources."

## Migrating from Manual Infrastructure to Terraform

A startup with existing cloud infrastructure can adopt Terraform without recreating resources. The process:

1. Describe existing resources in Terraform code (AI can help — feed it the configuration from the console)
2. Import each resource: `terraform import aws_instance.web i-1234567890abcdef0`
3. Run `terraform plan`. The diff should be empty
4. If plan shows changes, adjust the code until it matches reality
5. Resources are now managed by Terraform

Prompt for step 1:

```
I have an EC2 instance with the following parameters:
- Instance type: t3.medium
- AMI: ami-0abcdef1234567890
- VPC: vpc-12345, Subnet: subnet-67890
- Security groups: sg-11111, sg-22222
- EBS: 50GB gp3, encrypted
- Tags: Name=web-server, Environment=production

Generate a Terraform resource block that accurately describes
this instance. Include all parameters that Terraform tracks.
Don't omit default values if they differ from the actual configuration.
```

For bulk migration, there's `terraformer` — a tool that auto-generates Terraform code from existing infrastructure. The output needs refactoring: it's flat, no modules, no variables. AI models handle the job of turning terraformer output into clean modular code quite well.

## What's Next

Terraform modules with AI handle most of a startup's infrastructure needs. What comes next depends on team size:

**Up to 10 developers:** remote state in S3/GCS, manual `terraform apply`, code review for `.tf` files. That's enough.

**10–30 developers:** Terraform Cloud or Atlantis for automatic `plan` on PR and `apply` on merge. Sentinel or OPA for policy-as-code.

**30+ developers:** Platform team, internal module registry, self-service through an Internal Developer Platform.

At every stage, AI generates the configurations you need. The approach in this article scales with the team — only the prompts and the level of module abstraction change.

Setting up CI/CD for automatic apply is covered in the [pipeline generation guide](/blog/cicd-pipeline-generator/). The circuit breaker pattern for protecting infrastructure API calls from cascading failures is in the [article on resilience in edge functions](/blog/circuit-breaker-deno-edge-functions/).

---

*Need help setting up cloud infrastructure for your startup? I help startups build AI products and automate processes — [belov.works](https://belov.works).*

## FAQ

**How do you safely refactor an existing module without breaking production infrastructure?**

The key is state manipulation before code changes. Before restructuring a module, run `terraform state mv` to move resources to their new logical addresses in state — this tells Terraform the resource already exists at the new path rather than treating it as a delete-then-create operation. Always run `terraform plan` after each state move and verify the diff shows zero destructive changes before proceeding. For complex refactors involving many resources, work in a non-production environment first, compare the before-and-after plan outputs, and only then apply the same state operations to production.

**What's the best way to handle secrets that Terraform needs at apply time without storing them in state?**

Terraform's state file stores resource attributes in plaintext, including any sensitive values passed as inputs. The practical mitigation is to never pass actual secret values through Terraform variables — instead, use provider-native secret references. For AWS, store credentials in Secrets Manager and reference them with `aws_secretsmanager_secret_version` data sources that inject the value only at resource creation time. For Kubernetes, use External Secrets Operator. The goal is that `terraform.tfstate` contains only a reference (ARN, path) to the secret, not the secret value itself.

**How do AI models handle Terraform provider version upgrades, and what manual review is always required?**

AI models generate syntactically correct code for the provider version they were trained on, but provider APIs change between major versions — resource arguments get renamed, deprecated, or removed. When upgrading providers, use `terraform providers lock` to pin the new version and run `terraform plan` to surface errors; paste those errors back to the model with the full resource block and the provider changelog excerpt for accurate fixes. Manual review is always required for IAM policy changes (AI tends to be overly permissive), lifecycle rules (especially `prevent_destroy`), and any resource that manages encryption settings, where the model's defaults may not match your compliance requirements.
