---
name: opentofu-module
description: Use when writing, editing, or reviewing OpenTofu/Terraform modules or configurations across AWS, Azure, OCI, or homelab environments. Triggers on tasks involving HCL, tofu commands, state management, IAM, EKS, IRSA, backends, or AFT.
---

# OpenTofu Module Skill

## Overview

Write and maintain OpenTofu modules for Zoe's infrastructure. Use `tofu` commands (not `terraform`). Providers span AWS (primary), Azure, and OCI free tier.

## Workflow

1. Define variables + outputs **before** writing resources — prevents refactoring
2. Write `versions.tf` first — sets provider constraints
3. Write resources in `main.tf`
4. `tofu fmt -recursive` and `tflint` before plan
5. `checkov -d .` — fix HIGH/CRITICAL before applying
6. `tofu plan -out=plan.out` → review → `tofu apply plan.out`
7. **Never `tofu apply` without a plan file in production**

## Standard Module Structure

```
modules/<name>/
  main.tf        # resources
  variables.tf   # input variables with descriptions and types
  outputs.tf     # exported values
  versions.tf    # required_providers with version constraints
  README.md      # auto-generated by terraform-docs — do NOT write manually
```

## Key Patterns

### versions.tf

```hcl
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
```

### variables.tf

```hcl
variable "cluster_name" {
  description = "Name of the EKS cluster"
  type        = string
}

variable "tags" {
  description = "Tags to apply to all resources"
  type        = map(string)
  default     = {}
}
```

### S3 Backend

```hcl
terraform {
  backend "s3" {
    bucket         = "company-tofu-state"
    key            = "env/production/cluster/terraform.tfstate"
    region         = "us-west-2"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}
```

### IRSA (IAM Roles for Service Accounts) — EKS

Comes up constantly. Full pattern:

```hcl
data "aws_eks_cluster" "cluster" {
  name = var.cluster_name
}

data "aws_iam_openid_connect_provider" "cluster" {
  url = data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [data.aws_iam_openid_connect_provider.cluster.arn]
    }
    condition {
      test     = "StringEquals"
      variable = "${replace(data.aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub"
      values   = ["system:serviceaccount:${var.namespace}:${var.service_account_name}"]
    }
  }
}

resource "aws_iam_role" "irsa" {
  name               = "${var.cluster_name}-${var.name}-irsa"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  tags               = var.tags
}
```

### Cross-Account AssumeRole (OrganizationAccountAccessRole)

```hcl
data "aws_iam_policy_document" "trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type = "AWS"
      # Use the role ARN pattern — NOT AWSReservedSSO_* (causes MalformedPolicyDocument)
      identifiers = ["arn:aws:iam::${var.management_account_id}:role/${var.deployer_role_name}"]
    }
  }
}
```

## Critical Gotcha: AWSReservedSSO_* Roles

**NEVER use `AWSReservedSSO_*` ARNs as IAM principals in trust policies.**

- Error: `MalformedPolicyDocument: Invalid principal in policy`
- Fix: Use the underlying permission set role name pattern, or use `aws:PrincipalOrgID` condition instead

## State Operations

```bash
# Push local state to remote after manual work
tofu state push terraform.tfstate --force

# Import existing resource
tofu import aws_s3_bucket.example my-bucket-name

# Move resource between state paths (refactoring)
tofu state mv module.old.aws_instance.web module.new.aws_instance.web
```

## Toolchain Quick Reference

| Tool | Command | Purpose |
|------|---------|---------|
| Format | `tofu fmt -recursive` | Before every commit |
| Lint | `tflint` | Catch provider-specific issues |
| Security | `checkov -d .` | Fix HIGH/CRITICAL before apply |
| Docs | `terraform-docs markdown . > README.md` | README generation only |
| Plan | `tofu plan -out=plan.out` | Always use plan files in prod |

## Environment Notes

- **AWS**: EKS, EBS, EFS, IAM, S3, Lambda, CodePipeline, GuardDuty, RDS Aurora, Organizations/AFT
- **Azure**: AKS, Azure DevOps — state backend uses Azure Blob Storage
- **OCI**: Free tier, budgets, some compute
- **AFT**: Used for AWS org account provisioning via Terraform
- **State backends**: S3+DynamoDB (AWS), Azure Blob (Azure)
