Departments / devops / iac-generator

iac-generator

Use when provisioning or modifying cloud infrastructure as code. Generates Terraform (HCL), Bicep, or Pulumi code for common resources (AKS/EKS, RDS, S3, VNet/VPC, IAM) with remote state, encryption, and least-privilege defaults.

Department

DevOps

Safety

writes-local
Writes locally

Supported stacks

terraformbicep+azurepulumi

When to use

Do not use for one-off console experimentation or for rendering architecture diagrams.

Inputs

Outputs

Tool dependencies

Procedure

0. Detect the stack

Infer the IaC flavour from the repo before generating anything:

find . -maxdepth 3 -name '*.tf' 2>/dev/null | head           # Terraform
find . -maxdepth 3 -name '*.bicep' 2>/dev/null | head        # Bicep
find . -maxdepth 3 -name 'Pulumi.yaml' 2>/dev/null | head    # Pulumi
find . -maxdepth 3 -name 'cdk.json' 2>/dev/null | head       # AWS CDK
find . -maxdepth 3 -name 'template.yaml' 2>/dev/null | head  # CloudFormation / SAM
ls ansible/ playbooks/ 2>/dev/null                           # Ansible
cat .terraform-version .tool-versions 2>/dev/null            # pinned TF / asdf

Also check whether the user expressed a preference in their request (e.g. “generate Terraform” vs “generate Bicep”). Declared preference overrides detection.

This skill supports terraform, bicep+azure, and pulumi. If detection shows AWS CDK, CloudFormation / SAM, Ansible, Crossplane, Chef/Puppet, or Cloud-provider consoles as the source of truth, STOP and report. Dropping a Terraform module into a CDK repo (or vice versa) creates drift between two state stores and is the single biggest IaC regret teams have.

If no IaC tool is detected and the user hasn’t specified one, ask which of the three supported stacks they want. Do not guess.

1. Pick the layout

infrastructure/
  terraform/
    modules/
      eks-cluster/
      rds-postgres/
      s3-bucket/
      vpc/
      iam-role/
    envs/
      dev/
      staging/
      prod/

Each env has its own backend.tf pointing at a different state key. Modules are versionless in-repo; publish to a registry once stable.

2. Configure remote state with locking

Terraform + S3/DynamoDB:

terraform {
  required_version = ">= 1.7"
  backend "s3" {
    bucket         = "acme-tfstate-prod"
    key            = "checkout/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "acme-tfstate-locks"
    encrypt        = true
    kms_key_id     = "alias/tfstate"
  }
}

Bicep + Azure Storage:

Azure uses deployment stacks, not state files, but ARM-template outputs persist. Configure a storage account for artifact/what-if output:

az deployment sub create \
  --name checkout-prod-$(date +%s) \
  --location eastus \
  --template-file main.bicep \
  --parameters @prod.bicepparam

Pulumi + Azure Blob backend:

pulumi login azblob://acme-pulumi-state?storage_account=acmepulumistate

3. Generate the resource (examples)

VPC (Terraform, AWS)

module "vpc" {
  source             = "../../modules/vpc"
  name               = "${var.name_prefix}-vpc"
  cidr               = "10.40.0.0/16"
  azs                = ["us-east-1a", "us-east-1b", "us-east-1c"]
  public_subnets     = ["10.40.0.0/20", "10.40.16.0/20", "10.40.32.0/20"]
  private_subnets    = ["10.40.64.0/20", "10.40.80.0/20", "10.40.96.0/20"]
  enable_nat_gateway = true
  single_nat_gateway = var.env != "prod"
  enable_flow_log    = true
  flow_log_destination_type = "cloud-watch-logs"
  tags               = local.tags
}

EKS cluster (Terraform)

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.24"

  cluster_name                    = "${var.name_prefix}-${var.env}"
  cluster_version                 = "1.30"
  cluster_endpoint_public_access  = false
  cluster_endpoint_private_access = true

  enable_irsa                  = true
  cluster_encryption_config    = [{ resources = ["secrets"] }]
  cluster_enabled_log_types    = ["api", "audit", "authenticator", "controllerManager", "scheduler"]

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  eks_managed_node_groups = {
    default = {
      min_size       = var.env == "prod" ? 3 : 1
      max_size       = var.env == "prod" ? 10 : 4
      desired_size   = var.env == "prod" ? 3 : 2
      instance_types = ["m6i.large"]
      capacity_type  = var.env == "prod" ? "ON_DEMAND" : "SPOT"
      labels         = { workload = "general" }
    }
  }

  tags = local.tags
}

RDS Postgres

module "db" {
  source  = "terraform-aws-modules/rds/aws"
  version = "~> 6.10"

  identifier                          = "${var.name_prefix}-${var.env}"
  engine                              = "postgres"
  engine_version                      = "16.4"
  instance_class                      = var.env == "prod" ? "db.r6g.xlarge" : "db.t4g.medium"
  allocated_storage                   = var.env == "prod" ? 200 : 50
  storage_encrypted                   = true
  kms_key_id                          = aws_kms_key.rds.arn
  multi_az                            = var.env == "prod"
  deletion_protection                 = var.env == "prod"
  iam_database_authentication_enabled = true
  publicly_accessible                 = false
  backup_retention_period             = var.env == "prod" ? 30 : 7
  performance_insights_enabled        = true

  db_subnet_group_name   = module.vpc.database_subnet_group_name
  vpc_security_group_ids = [aws_security_group.rds.id]

  manage_master_user_password = true
  master_user_secret_kms_key_id = aws_kms_key.rds.arn

  tags = local.tags
}

Never put a raw password in code; the manage_master_user_password flag stores the generated password in Secrets Manager.

S3 bucket with versioning + encryption

resource "aws_s3_bucket" "artifacts" {
  bucket = "${var.name_prefix}-artifacts-${var.env}"
  tags   = local.tags
}

resource "aws_s3_bucket_public_access_block" "artifacts" {
  bucket                  = aws_s3_bucket.artifacts.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  rule {
    id     = "expire-old-versions"
    status = "Enabled"
    noncurrent_version_expiration { noncurrent_days = 90 }
    abort_incomplete_multipart_upload { days_after_initiation = 7 }
  }
}

IAM role (least privilege)

data "aws_iam_policy_document" "assume" {
  statement {
    effect = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [module.eks.oidc_provider_arn]
    }
    condition {
      test     = "StringEquals"
      variable = "${module.eks.oidc_provider}:sub"
      values   = ["system:serviceaccount:checkout:checkout-api"]
    }
  }
}

data "aws_iam_policy_document" "checkout_api" {
  statement {
    effect = "Allow"
    actions = ["s3:GetObject", "s3:PutObject"]
    resources = ["${aws_s3_bucket.artifacts.arn}/checkout/*"]
  }
  statement {
    effect = "Allow"
    actions = ["secretsmanager:GetSecretValue"]
    resources = [aws_secretsmanager_secret.db.arn]
  }
}

resource "aws_iam_role" "checkout_api" {
  name               = "${var.name_prefix}-checkout-api-${var.env}"
  assume_role_policy = data.aws_iam_policy_document.assume.json
  tags               = local.tags
}

resource "aws_iam_role_policy" "checkout_api" {
  role   = aws_iam_role.checkout_api.id
  policy = data.aws_iam_policy_document.checkout_api.json
}

No Action: *, no Resource: *. Scope actions to minimum set, resources to specific ARNs.

Bicep: AKS (private cluster)

param location string = resourceGroup().location
param namePrefix string
param env string
var tags = { Env: env, Owner: 'platform', ManagedBy: 'bicep' }

resource aks 'Microsoft.ContainerService/managedClusters@2024-09-01' = {
  name: '${namePrefix}-${env}'
  location: location
  identity: { type: 'SystemAssigned' }
  properties: {
    dnsPrefix: '${namePrefix}-${env}'
    kubernetesVersion: '1.30'
    networkProfile: { networkPlugin: 'azure', networkPolicy: 'cilium', loadBalancerSku: 'standard' }
    apiServerAccessProfile: { enablePrivateCluster: true, authorizedIPRanges: [] }
    agentPoolProfiles: [ {
      name: 'system', mode: 'System'
      count: env == 'prod' ? 3 : 1
      vmSize: 'Standard_D4s_v5'
      availabilityZones: ['1', '2', '3']
    } ]
    addonProfiles: {
      azureKeyvaultSecretsProvider: { enabled: true, config: { enableSecretRotation: 'true' } }
    }
  }
  tags: tags
}

Pulumi (Python) — S3 bucket

import pulumi, pulumi_aws as aws

name_prefix = pulumi.Config().require("namePrefix")
tags = {"Env": pulumi.get_stack(), "Owner": "platform", "ManagedBy": "pulumi"}

bucket = aws.s3.BucketV2(f"{name_prefix}-artifacts", tags=tags)
aws.s3.BucketVersioningV2("ver", bucket=bucket.id,
    versioning_configuration={"status": "Enabled"})
aws.s3.BucketServerSideEncryptionConfigurationV2("sse", bucket=bucket.id, rules=[{
    "apply_server_side_encryption_by_default": {"sse_algorithm": "aws:kms"},
    "bucket_key_enabled": True}])
aws.s3.BucketPublicAccessBlock("pab", bucket=bucket.id,
    block_public_acls=True, block_public_policy=True,
    ignore_public_acls=True, restrict_public_buckets=True)

4. Validate

terraform fmt -recursive
terraform init -backend=false
terraform validate
tflint --recursive
checkov -d . --framework terraform
terraform plan -out plan.bin

For Bicep:

bicep build main.bicep
az deployment sub what-if --location eastus --template-file main.bicep --parameters @prod.bicepparam
Invoke-PSRule -Module PSRule.Rules.Azure -InputPath .

5. CI integration

Refer to references/iac-best-practices.md for module structure, naming, and policy-as-code details.

Examples

Example 1 — Net-new EKS stack for checkout-api dev

Inputs: tool=terraform, cloud=aws, resources=[vpc, eks, rds-postgres, s3, iam-role-least-privilege], env=dev, region=us-east-1, name_prefix=acme-checkout.

Generates infrastructure/terraform/envs/dev/ with a VPC (single NAT), a single-AZ t4g RDS, a 2-node SPOT EKS group, one S3 bucket, and an IRSA role for the app. Remote state in s3://acme-tfstate-dev with DynamoDB locking.

Example 2 — Azure Postgres Flexible Server in prod

Inputs: tool=bicep, cloud=azure, resources=[azure-postgres-flexible], env=prod, region=eastus, name_prefix=acme-ledger.

Generates infrastructure/bicep/envs/prod/postgres.bicep with zone-redundant HA, 30-day backup retention, geo-redundant backups to westus, private endpoint in an existing VNet, and AAD authentication enabled. Password is generated via @secure() parameter and pulled from Key Vault at deploy time.

Constraints

Quality checks

Customise for your organisation

iac-generator

The LLM will rewrite this skill for your environment. Your API key and form inputs stay in your browser — only the skill and your environment go to OpenRouter.

One line. Be specific — cloud, language, framework, orchestrator.

Free text that steers the rewrite. Leave blank if nothing specific.

cost estimate: